").appendTo(i),n=i.uniqueId().attr("id");return this._addClass(s,"ui-tooltip-content"),this._addClass(i,"ui-tooltip","ui-widget ui-widget-content"),i.appendTo(this._appendTo(e)),this.tooltips[n]={element:e,tooltip:i}},_find:function(t){var e=t.data("ui-tooltip-id");return e?this.tooltips[e]:null},_removeTooltip:function(t){t.remove(),delete this.tooltips[t.attr("id")]},_appendTo:function(t){var e=t.closest(".ui-front, dialog");return e.length||(e=this.document[0].body),e},_destroy:function(){var e=this;t.each(this.tooltips,function(i,s){var n=t.Event("blur"),o=s.element;n.target=n.currentTarget=o[0],e.close(n,!0),t("#"+i).remove(),o.data("ui-tooltip-title")&&(o.attr("title")||o.attr("title",o.data("ui-tooltip-title")),o.removeData("ui-tooltip-title"))}),this.liveRegion.remove()}}),t.uiBackCompat!==!1&&t.widget("ui.tooltip",t.ui.tooltip,{options:{tooltipClass:null},_tooltip:function(){var t=this._superApply(arguments);return this.options.tooltipClass&&t.tooltip.addClass(this.options.tooltipClass),t}}),t.ui.tooltip;var f="ui-effects-",g="ui-effects-style",m="ui-effects-animated",_=t;t.effects={effect:{}},function(t,e){function i(t,e,i){var s=u[e.type]||{};return null==t?i||!e.def?null:e.def:(t=s.floor?~~t:parseFloat(t),isNaN(t)?e.def:s.mod?(t+s.mod)%s.mod:0>t?0:t>s.max?s.max:t)}function s(i){var s=l(),n=s._rgba=[];return i=i.toLowerCase(),f(h,function(t,o){var a,r=o.re.exec(i),h=r&&o.parse(r),l=o.space||"rgba";return h?(a=s[l](h),s[c[l].cache]=a[c[l].cache],n=s._rgba=a._rgba,!1):e}),n.length?("0,0,0,0"===n.join()&&t.extend(n,o.transparent),s):o[i]}function n(t,e,i){return i=(i+1)%1,1>6*i?t+6*(e-t)*i:1>2*i?e:2>3*i?t+6*(e-t)*(2/3-i):t}var o,a="backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor",r=/^([\-+])=\s*(\d+\.?\d*)/,h=[{re:/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[t[1],t[2],t[3],t[4]]}},{re:/rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[2.55*t[1],2.55*t[2],2.55*t[3],t[4]]}},{re:/#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/,parse:function(t){return[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16)]}},{re:/#([a-f0-9])([a-f0-9])([a-f0-9])/,parse:function(t){return[parseInt(t[1]+t[1],16),parseInt(t[2]+t[2],16),parseInt(t[3]+t[3],16)]}},{re:/hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,space:"hsla",parse:function(t){return[t[1],t[2]/100,t[3]/100,t[4]]}}],l=t.Color=function(e,i,s,n){return new t.Color.fn.parse(e,i,s,n)},c={rgba:{props:{red:{idx:0,type:"byte"},green:{idx:1,type:"byte"},blue:{idx:2,type:"byte"}}},hsla:{props:{hue:{idx:0,type:"degrees"},saturation:{idx:1,type:"percent"},lightness:{idx:2,type:"percent"}}}},u={"byte":{floor:!0,max:255},percent:{max:1},degrees:{mod:360,floor:!0}},d=l.support={},p=t("
")[0],f=t.each;p.style.cssText="background-color:rgba(1,1,1,.5)",d.rgba=p.style.backgroundColor.indexOf("rgba")>-1,f(c,function(t,e){e.cache="_"+t,e.props.alpha={idx:3,type:"percent",def:1}}),l.fn=t.extend(l.prototype,{parse:function(n,a,r,h){if(n===e)return this._rgba=[null,null,null,null],this;(n.jquery||n.nodeType)&&(n=t(n).css(a),a=e);var u=this,d=t.type(n),p=this._rgba=[];return a!==e&&(n=[n,a,r,h],d="array"),"string"===d?this.parse(s(n)||o._default):"array"===d?(f(c.rgba.props,function(t,e){p[e.idx]=i(n[e.idx],e)}),this):"object"===d?(n instanceof l?f(c,function(t,e){n[e.cache]&&(u[e.cache]=n[e.cache].slice())}):f(c,function(e,s){var o=s.cache;f(s.props,function(t,e){if(!u[o]&&s.to){if("alpha"===t||null==n[t])return;u[o]=s.to(u._rgba)}u[o][e.idx]=i(n[t],e,!0)}),u[o]&&0>t.inArray(null,u[o].slice(0,3))&&(u[o][3]=1,s.from&&(u._rgba=s.from(u[o])))}),this):e},is:function(t){var i=l(t),s=!0,n=this;return f(c,function(t,o){var a,r=i[o.cache];return r&&(a=n[o.cache]||o.to&&o.to(n._rgba)||[],f(o.props,function(t,i){return null!=r[i.idx]?s=r[i.idx]===a[i.idx]:e})),s}),s},_space:function(){var t=[],e=this;return f(c,function(i,s){e[s.cache]&&t.push(i)}),t.pop()},transition:function(t,e){var s=l(t),n=s._space(),o=c[n],a=0===this.alpha()?l("transparent"):this,r=a[o.cache]||o.to(a._rgba),h=r.slice();return s=s[o.cache],f(o.props,function(t,n){var o=n.idx,a=r[o],l=s[o],c=u[n.type]||{};null!==l&&(null===a?h[o]=l:(c.mod&&(l-a>c.mod/2?a+=c.mod:a-l>c.mod/2&&(a-=c.mod)),h[o]=i((l-a)*e+a,n)))}),this[n](h)},blend:function(e){if(1===this._rgba[3])return this;var i=this._rgba.slice(),s=i.pop(),n=l(e)._rgba;return l(t.map(i,function(t,e){return(1-s)*n[e]+s*t}))},toRgbaString:function(){var e="rgba(",i=t.map(this._rgba,function(t,e){return null==t?e>2?1:0:t});return 1===i[3]&&(i.pop(),e="rgb("),e+i.join()+")"},toHslaString:function(){var e="hsla(",i=t.map(this.hsla(),function(t,e){return null==t&&(t=e>2?1:0),e&&3>e&&(t=Math.round(100*t)+"%"),t});return 1===i[3]&&(i.pop(),e="hsl("),e+i.join()+")"},toHexString:function(e){var i=this._rgba.slice(),s=i.pop();return e&&i.push(~~(255*s)),"#"+t.map(i,function(t){return t=(t||0).toString(16),1===t.length?"0"+t:t}).join("")},toString:function(){return 0===this._rgba[3]?"transparent":this.toRgbaString()}}),l.fn.parse.prototype=l.fn,c.hsla.to=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e,i,s=t[0]/255,n=t[1]/255,o=t[2]/255,a=t[3],r=Math.max(s,n,o),h=Math.min(s,n,o),l=r-h,c=r+h,u=.5*c;return e=h===r?0:s===r?60*(n-o)/l+360:n===r?60*(o-s)/l+120:60*(s-n)/l+240,i=0===l?0:.5>=u?l/c:l/(2-c),[Math.round(e)%360,i,u,null==a?1:a]},c.hsla.from=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e=t[0]/360,i=t[1],s=t[2],o=t[3],a=.5>=s?s*(1+i):s+i-s*i,r=2*s-a;return[Math.round(255*n(r,a,e+1/3)),Math.round(255*n(r,a,e)),Math.round(255*n(r,a,e-1/3)),o]},f(c,function(s,n){var o=n.props,a=n.cache,h=n.to,c=n.from;l.fn[s]=function(s){if(h&&!this[a]&&(this[a]=h(this._rgba)),s===e)return this[a].slice();var n,r=t.type(s),u="array"===r||"object"===r?s:arguments,d=this[a].slice();return f(o,function(t,e){var s=u["object"===r?t:e.idx];null==s&&(s=d[e.idx]),d[e.idx]=i(s,e)}),c?(n=l(c(d)),n[a]=d,n):l(d)},f(o,function(e,i){l.fn[e]||(l.fn[e]=function(n){var o,a=t.type(n),h="alpha"===e?this._hsla?"hsla":"rgba":s,l=this[h](),c=l[i.idx];return"undefined"===a?c:("function"===a&&(n=n.call(this,c),a=t.type(n)),null==n&&i.empty?this:("string"===a&&(o=r.exec(n),o&&(n=c+parseFloat(o[2])*("+"===o[1]?1:-1))),l[i.idx]=n,this[h](l)))})})}),l.hook=function(e){var i=e.split(" ");f(i,function(e,i){t.cssHooks[i]={set:function(e,n){var o,a,r="";if("transparent"!==n&&("string"!==t.type(n)||(o=s(n)))){if(n=l(o||n),!d.rgba&&1!==n._rgba[3]){for(a="backgroundColor"===i?e.parentNode:e;(""===r||"transparent"===r)&&a&&a.style;)try{r=t.css(a,"backgroundColor"),a=a.parentNode}catch(h){}n=n.blend(r&&"transparent"!==r?r:"_default")}n=n.toRgbaString()}try{e.style[i]=n}catch(h){}}},t.fx.step[i]=function(e){e.colorInit||(e.start=l(e.elem,i),e.end=l(e.end),e.colorInit=!0),t.cssHooks[i].set(e.elem,e.start.transition(e.end,e.pos))}})},l.hook(a),t.cssHooks.borderColor={expand:function(t){var e={};return f(["Top","Right","Bottom","Left"],function(i,s){e["border"+s+"Color"]=t}),e}},o=t.Color.names={aqua:"#00ffff",black:"#000000",blue:"#0000ff",fuchsia:"#ff00ff",gray:"#808080",green:"#008000",lime:"#00ff00",maroon:"#800000",navy:"#000080",olive:"#808000",purple:"#800080",red:"#ff0000",silver:"#c0c0c0",teal:"#008080",white:"#ffffff",yellow:"#ffff00",transparent:[null,null,null,0],_default:"#ffffff"}}(_),function(){function e(e){var i,s,n=e.ownerDocument.defaultView?e.ownerDocument.defaultView.getComputedStyle(e,null):e.currentStyle,o={};if(n&&n.length&&n[0]&&n[n[0]])for(s=n.length;s--;)i=n[s],"string"==typeof n[i]&&(o[t.camelCase(i)]=n[i]);else for(i in n)"string"==typeof n[i]&&(o[i]=n[i]);return o}function i(e,i){var s,o,a={};for(s in i)o=i[s],e[s]!==o&&(n[s]||(t.fx.step[s]||!isNaN(parseFloat(o)))&&(a[s]=o));return a}var s=["add","remove","toggle"],n={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};t.each(["borderLeftStyle","borderRightStyle","borderBottomStyle","borderTopStyle"],function(e,i){t.fx.step[i]=function(t){("none"!==t.end&&!t.setAttr||1===t.pos&&!t.setAttr)&&(_.style(t.elem,i,t.end),t.setAttr=!0)}}),t.fn.addBack||(t.fn.addBack=function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}),t.effects.animateClass=function(n,o,a,r){var h=t.speed(o,a,r);return this.queue(function(){var o,a=t(this),r=a.attr("class")||"",l=h.children?a.find("*").addBack():a;l=l.map(function(){var i=t(this);return{el:i,start:e(this)}}),o=function(){t.each(s,function(t,e){n[e]&&a[e+"Class"](n[e])})},o(),l=l.map(function(){return this.end=e(this.el[0]),this.diff=i(this.start,this.end),this}),a.attr("class",r),l=l.map(function(){var e=this,i=t.Deferred(),s=t.extend({},h,{queue:!1,complete:function(){i.resolve(e)}});return this.el.animate(this.diff,s),i.promise()}),t.when.apply(t,l.get()).done(function(){o(),t.each(arguments,function(){var e=this.el;t.each(this.diff,function(t){e.css(t,"")})}),h.complete.call(a[0])})})},t.fn.extend({addClass:function(e){return function(i,s,n,o){return s?t.effects.animateClass.call(this,{add:i},s,n,o):e.apply(this,arguments)}}(t.fn.addClass),removeClass:function(e){return function(i,s,n,o){return arguments.length>1?t.effects.animateClass.call(this,{remove:i},s,n,o):e.apply(this,arguments)}}(t.fn.removeClass),toggleClass:function(e){return function(i,s,n,o,a){return"boolean"==typeof s||void 0===s?n?t.effects.animateClass.call(this,s?{add:i}:{remove:i},n,o,a):e.apply(this,arguments):t.effects.animateClass.call(this,{toggle:i},s,n,o)}}(t.fn.toggleClass),switchClass:function(e,i,s,n,o){return t.effects.animateClass.call(this,{add:i,remove:e},s,n,o)}})}(),function(){function e(e,i,s,n){return t.isPlainObject(e)&&(i=e,e=e.effect),e={effect:e},null==i&&(i={}),t.isFunction(i)&&(n=i,s=null,i={}),("number"==typeof i||t.fx.speeds[i])&&(n=s,s=i,i={}),t.isFunction(s)&&(n=s,s=null),i&&t.extend(e,i),s=s||i.duration,e.duration=t.fx.off?0:"number"==typeof s?s:s in t.fx.speeds?t.fx.speeds[s]:t.fx.speeds._default,e.complete=n||i.complete,e}function i(e){return!e||"number"==typeof e||t.fx.speeds[e]?!0:"string"!=typeof e||t.effects.effect[e]?t.isFunction(e)?!0:"object"!=typeof e||e.effect?!1:!0:!0}function s(t,e){var i=e.outerWidth(),s=e.outerHeight(),n=/^rect\((-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto)\)$/,o=n.exec(t)||["",0,i,s,0];return{top:parseFloat(o[1])||0,right:"auto"===o[2]?i:parseFloat(o[2]),bottom:"auto"===o[3]?s:parseFloat(o[3]),left:parseFloat(o[4])||0}}t.expr&&t.expr.filters&&t.expr.filters.animated&&(t.expr.filters.animated=function(e){return function(i){return!!t(i).data(m)||e(i)}}(t.expr.filters.animated)),t.uiBackCompat!==!1&&t.extend(t.effects,{save:function(t,e){for(var i=0,s=e.length;s>i;i++)null!==e[i]&&t.data(f+e[i],t[0].style[e[i]])},restore:function(t,e){for(var i,s=0,n=e.length;n>s;s++)null!==e[s]&&(i=t.data(f+e[s]),t.css(e[s],i))},setMode:function(t,e){return"toggle"===e&&(e=t.is(":hidden")?"show":"hide"),e},createWrapper:function(e){if(e.parent().is(".ui-effects-wrapper"))return e.parent();var i={width:e.outerWidth(!0),height:e.outerHeight(!0),"float":e.css("float")},s=t("
").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),n={width:e.width(),height:e.height()},o=document.activeElement;try{o.id}catch(a){o=document.body}return e.wrap(s),(e[0]===o||t.contains(e[0],o))&&t(o).trigger("focus"),s=e.parent(),"static"===e.css("position")?(s.css({position:"relative"}),e.css({position:"relative"})):(t.extend(i,{position:e.css("position"),zIndex:e.css("z-index")}),t.each(["top","left","bottom","right"],function(t,s){i[s]=e.css(s),isNaN(parseInt(i[s],10))&&(i[s]="auto")}),e.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),e.css(n),s.css(i).show()},removeWrapper:function(e){var i=document.activeElement;return e.parent().is(".ui-effects-wrapper")&&(e.parent().replaceWith(e),(e[0]===i||t.contains(e[0],i))&&t(i).trigger("focus")),e}}),t.extend(t.effects,{version:"1.12.1",define:function(e,i,s){return s||(s=i,i="effect"),t.effects.effect[e]=s,t.effects.effect[e].mode=i,s},scaledDimensions:function(t,e,i){if(0===e)return{height:0,width:0,outerHeight:0,outerWidth:0};var s="horizontal"!==i?(e||100)/100:1,n="vertical"!==i?(e||100)/100:1;return{height:t.height()*n,width:t.width()*s,outerHeight:t.outerHeight()*n,outerWidth:t.outerWidth()*s}},clipToBox:function(t){return{width:t.clip.right-t.clip.left,height:t.clip.bottom-t.clip.top,left:t.clip.left,top:t.clip.top}},unshift:function(t,e,i){var s=t.queue();e>1&&s.splice.apply(s,[1,0].concat(s.splice(e,i))),t.dequeue()},saveStyle:function(t){t.data(g,t[0].style.cssText)},restoreStyle:function(t){t[0].style.cssText=t.data(g)||"",t.removeData(g)},mode:function(t,e){var i=t.is(":hidden");return"toggle"===e&&(e=i?"show":"hide"),(i?"hide"===e:"show"===e)&&(e="none"),e},getBaseline:function(t,e){var i,s;switch(t[0]){case"top":i=0;break;case"middle":i=.5;break;case"bottom":i=1;break;default:i=t[0]/e.height}switch(t[1]){case"left":s=0;break;case"center":s=.5;break;case"right":s=1;break;default:s=t[1]/e.width}return{x:s,y:i}},createPlaceholder:function(e){var i,s=e.css("position"),n=e.position();return e.css({marginTop:e.css("marginTop"),marginBottom:e.css("marginBottom"),marginLeft:e.css("marginLeft"),marginRight:e.css("marginRight")}).outerWidth(e.outerWidth()).outerHeight(e.outerHeight()),/^(static|relative)/.test(s)&&(s="absolute",i=t("<"+e[0].nodeName+">").insertAfter(e).css({display:/^(inline|ruby)/.test(e.css("display"))?"inline-block":"block",visibility:"hidden",marginTop:e.css("marginTop"),marginBottom:e.css("marginBottom"),marginLeft:e.css("marginLeft"),marginRight:e.css("marginRight"),"float":e.css("float")}).outerWidth(e.outerWidth()).outerHeight(e.outerHeight()).addClass("ui-effects-placeholder"),e.data(f+"placeholder",i)),e.css({position:s,left:n.left,top:n.top}),i},removePlaceholder:function(t){var e=f+"placeholder",i=t.data(e);i&&(i.remove(),t.removeData(e))},cleanUp:function(e){t.effects.restoreStyle(e),t.effects.removePlaceholder(e)},setTransition:function(e,i,s,n){return n=n||{},t.each(i,function(t,i){var o=e.cssUnit(i);o[0]>0&&(n[i]=o[0]*s+o[1])}),n}}),t.fn.extend({effect:function(){function i(e){function i(){r.removeData(m),t.effects.cleanUp(r),"hide"===s.mode&&r.hide(),a()}function a(){t.isFunction(h)&&h.call(r[0]),t.isFunction(e)&&e()}var r=t(this);s.mode=c.shift(),t.uiBackCompat===!1||o?"none"===s.mode?(r[l](),a()):n.call(r[0],s,i):(r.is(":hidden")?"hide"===l:"show"===l)?(r[l](),a()):n.call(r[0],s,a)}var s=e.apply(this,arguments),n=t.effects.effect[s.effect],o=n.mode,a=s.queue,r=a||"fx",h=s.complete,l=s.mode,c=[],u=function(e){var i=t(this),s=t.effects.mode(i,l)||o;i.data(m,!0),c.push(s),o&&("show"===s||s===o&&"hide"===s)&&i.show(),o&&"none"===s||t.effects.saveStyle(i),t.isFunction(e)&&e()};return t.fx.off||!n?l?this[l](s.duration,h):this.each(function(){h&&h.call(this)}):a===!1?this.each(u).each(i):this.queue(r,u).queue(r,i)},show:function(t){return function(s){if(i(s))return t.apply(this,arguments);var n=e.apply(this,arguments);return n.mode="show",this.effect.call(this,n)}}(t.fn.show),hide:function(t){return function(s){if(i(s))return t.apply(this,arguments);var n=e.apply(this,arguments);return n.mode="hide",this.effect.call(this,n)}}(t.fn.hide),toggle:function(t){return function(s){if(i(s)||"boolean"==typeof s)return t.apply(this,arguments);var n=e.apply(this,arguments);return n.mode="toggle",this.effect.call(this,n)}}(t.fn.toggle),cssUnit:function(e){var i=this.css(e),s=[];return t.each(["em","px","%","pt"],function(t,e){i.indexOf(e)>0&&(s=[parseFloat(i),e])}),s},cssClip:function(t){return t?this.css("clip","rect("+t.top+"px "+t.right+"px "+t.bottom+"px "+t.left+"px)"):s(this.css("clip"),this)},transfer:function(e,i){var s=t(this),n=t(e.to),o="fixed"===n.css("position"),a=t("body"),r=o?a.scrollTop():0,h=o?a.scrollLeft():0,l=n.offset(),c={top:l.top-r,left:l.left-h,height:n.innerHeight(),width:n.innerWidth()},u=s.offset(),d=t("
").appendTo("body").addClass(e.className).css({top:u.top-r,left:u.left-h,height:s.innerHeight(),width:s.innerWidth(),position:o?"fixed":"absolute"}).animate(c,e.duration,e.easing,function(){d.remove(),t.isFunction(i)&&i()})}}),t.fx.step.clip=function(e){e.clipInit||(e.start=t(e.elem).cssClip(),"string"==typeof e.end&&(e.end=s(e.end,e.elem)),e.clipInit=!0),t(e.elem).cssClip({top:e.pos*(e.end.top-e.start.top)+e.start.top,right:e.pos*(e.end.right-e.start.right)+e.start.right,bottom:e.pos*(e.end.bottom-e.start.bottom)+e.start.bottom,left:e.pos*(e.end.left-e.start.left)+e.start.left})}}(),function(){var e={};t.each(["Quad","Cubic","Quart","Quint","Expo"],function(t,i){e[i]=function(e){return Math.pow(e,t+2)}}),t.extend(e,{Sine:function(t){return 1-Math.cos(t*Math.PI/2)},Circ:function(t){return 1-Math.sqrt(1-t*t)},Elastic:function(t){return 0===t||1===t?t:-Math.pow(2,8*(t-1))*Math.sin((80*(t-1)-7.5)*Math.PI/15)},Back:function(t){return t*t*(3*t-2)},Bounce:function(t){for(var e,i=4;((e=Math.pow(2,--i))-1)/11>t;);return 1/Math.pow(4,3-i)-7.5625*Math.pow((3*e-2)/22-t,2)}}),t.each(e,function(e,i){t.easing["easeIn"+e]=i,t.easing["easeOut"+e]=function(t){return 1-i(1-t)},t.easing["easeInOut"+e]=function(t){return.5>t?i(2*t)/2:1-i(-2*t+2)/2}})}();var v=t.effects;t.effects.define("blind","hide",function(e,i){var s={up:["bottom","top"],vertical:["bottom","top"],down:["top","bottom"],left:["right","left"],horizontal:["right","left"],right:["left","right"]},n=t(this),o=e.direction||"up",a=n.cssClip(),r={clip:t.extend({},a)},h=t.effects.createPlaceholder(n);r.clip[s[o][0]]=r.clip[s[o][1]],"show"===e.mode&&(n.cssClip(r.clip),h&&h.css(t.effects.clipToBox(r)),r.clip=a),h&&h.animate(t.effects.clipToBox(r),e.duration,e.easing),n.animate(r,{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("bounce",function(e,i){var s,n,o,a=t(this),r=e.mode,h="hide"===r,l="show"===r,c=e.direction||"up",u=e.distance,d=e.times||5,p=2*d+(l||h?1:0),f=e.duration/p,g=e.easing,m="up"===c||"down"===c?"top":"left",_="up"===c||"left"===c,v=0,b=a.queue().length;for(t.effects.createPlaceholder(a),o=a.css(m),u||(u=a["top"===m?"outerHeight":"outerWidth"]()/3),l&&(n={opacity:1},n[m]=o,a.css("opacity",0).css(m,_?2*-u:2*u).animate(n,f,g)),h&&(u/=Math.pow(2,d-1)),n={},n[m]=o;d>v;v++)s={},s[m]=(_?"-=":"+=")+u,a.animate(s,f,g).animate(n,f,g),u=h?2*u:u/2;h&&(s={opacity:0},s[m]=(_?"-=":"+=")+u,a.animate(s,f,g)),a.queue(i),t.effects.unshift(a,b,p+1)}),t.effects.define("clip","hide",function(e,i){var s,n={},o=t(this),a=e.direction||"vertical",r="both"===a,h=r||"horizontal"===a,l=r||"vertical"===a;s=o.cssClip(),n.clip={top:l?(s.bottom-s.top)/2:s.top,right:h?(s.right-s.left)/2:s.right,bottom:l?(s.bottom-s.top)/2:s.bottom,left:h?(s.right-s.left)/2:s.left},t.effects.createPlaceholder(o),"show"===e.mode&&(o.cssClip(n.clip),n.clip=s),o.animate(n,{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("drop","hide",function(e,i){var s,n=t(this),o=e.mode,a="show"===o,r=e.direction||"left",h="up"===r||"down"===r?"top":"left",l="up"===r||"left"===r?"-=":"+=",c="+="===l?"-=":"+=",u={opacity:0};t.effects.createPlaceholder(n),s=e.distance||n["top"===h?"outerHeight":"outerWidth"](!0)/2,u[h]=l+s,a&&(n.css(u),u[h]=c+s,u.opacity=1),n.animate(u,{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("explode","hide",function(e,i){function s(){b.push(this),b.length===u*d&&n()}function n(){p.css({visibility:"visible"}),t(b).remove(),i()}var o,a,r,h,l,c,u=e.pieces?Math.round(Math.sqrt(e.pieces)):3,d=u,p=t(this),f=e.mode,g="show"===f,m=p.show().css("visibility","hidden").offset(),_=Math.ceil(p.outerWidth()/d),v=Math.ceil(p.outerHeight()/u),b=[];for(o=0;u>o;o++)for(h=m.top+o*v,c=o-(u-1)/2,a=0;d>a;a++)r=m.left+a*_,l=a-(d-1)/2,p.clone().appendTo("body").wrap("
").css({position:"absolute",visibility:"visible",left:-a*_,top:-o*v}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:_,height:v,left:r+(g?l*_:0),top:h+(g?c*v:0),opacity:g?0:1}).animate({left:r+(g?0:l*_),top:h+(g?0:c*v),opacity:g?1:0},e.duration||500,e.easing,s)}),t.effects.define("fade","toggle",function(e,i){var s="show"===e.mode;t(this).css("opacity",s?0:1).animate({opacity:s?1:0},{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("fold","hide",function(e,i){var s=t(this),n=e.mode,o="show"===n,a="hide"===n,r=e.size||15,h=/([0-9]+)%/.exec(r),l=!!e.horizFirst,c=l?["right","bottom"]:["bottom","right"],u=e.duration/2,d=t.effects.createPlaceholder(s),p=s.cssClip(),f={clip:t.extend({},p)},g={clip:t.extend({},p)},m=[p[c[0]],p[c[1]]],_=s.queue().length;h&&(r=parseInt(h[1],10)/100*m[a?0:1]),f.clip[c[0]]=r,g.clip[c[0]]=r,g.clip[c[1]]=0,o&&(s.cssClip(g.clip),d&&d.css(t.effects.clipToBox(g)),g.clip=p),s.queue(function(i){d&&d.animate(t.effects.clipToBox(f),u,e.easing).animate(t.effects.clipToBox(g),u,e.easing),i()}).animate(f,u,e.easing).animate(g,u,e.easing).queue(i),t.effects.unshift(s,_,4)}),t.effects.define("highlight","show",function(e,i){var s=t(this),n={backgroundColor:s.css("backgroundColor")};"hide"===e.mode&&(n.opacity=0),t.effects.saveStyle(s),s.css({backgroundImage:"none",backgroundColor:e.color||"#ffff99"}).animate(n,{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("size",function(e,i){var s,n,o,a=t(this),r=["fontSize"],h=["borderTopWidth","borderBottomWidth","paddingTop","paddingBottom"],l=["borderLeftWidth","borderRightWidth","paddingLeft","paddingRight"],c=e.mode,u="effect"!==c,d=e.scale||"both",p=e.origin||["middle","center"],f=a.css("position"),g=a.position(),m=t.effects.scaledDimensions(a),_=e.from||m,v=e.to||t.effects.scaledDimensions(a,0);t.effects.createPlaceholder(a),"show"===c&&(o=_,_=v,v=o),n={from:{y:_.height/m.height,x:_.width/m.width},to:{y:v.height/m.height,x:v.width/m.width}},("box"===d||"both"===d)&&(n.from.y!==n.to.y&&(_=t.effects.setTransition(a,h,n.from.y,_),v=t.effects.setTransition(a,h,n.to.y,v)),n.from.x!==n.to.x&&(_=t.effects.setTransition(a,l,n.from.x,_),v=t.effects.setTransition(a,l,n.to.x,v))),("content"===d||"both"===d)&&n.from.y!==n.to.y&&(_=t.effects.setTransition(a,r,n.from.y,_),v=t.effects.setTransition(a,r,n.to.y,v)),p&&(s=t.effects.getBaseline(p,m),_.top=(m.outerHeight-_.outerHeight)*s.y+g.top,_.left=(m.outerWidth-_.outerWidth)*s.x+g.left,v.top=(m.outerHeight-v.outerHeight)*s.y+g.top,v.left=(m.outerWidth-v.outerWidth)*s.x+g.left),a.css(_),("content"===d||"both"===d)&&(h=h.concat(["marginTop","marginBottom"]).concat(r),l=l.concat(["marginLeft","marginRight"]),a.find("*[width]").each(function(){var i=t(this),s=t.effects.scaledDimensions(i),o={height:s.height*n.from.y,width:s.width*n.from.x,outerHeight:s.outerHeight*n.from.y,outerWidth:s.outerWidth*n.from.x},a={height:s.height*n.to.y,width:s.width*n.to.x,outerHeight:s.height*n.to.y,outerWidth:s.width*n.to.x};n.from.y!==n.to.y&&(o=t.effects.setTransition(i,h,n.from.y,o),a=t.effects.setTransition(i,h,n.to.y,a)),n.from.x!==n.to.x&&(o=t.effects.setTransition(i,l,n.from.x,o),a=t.effects.setTransition(i,l,n.to.x,a)),u&&t.effects.saveStyle(i),i.css(o),i.animate(a,e.duration,e.easing,function(){u&&t.effects.restoreStyle(i)})})),a.animate(v,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){var e=a.offset();0===v.opacity&&a.css("opacity",_.opacity),u||(a.css("position","static"===f?"relative":f).offset(e),t.effects.saveStyle(a)),i()}})}),t.effects.define("scale",function(e,i){var s=t(this),n=e.mode,o=parseInt(e.percent,10)||(0===parseInt(e.percent,10)?0:"effect"!==n?0:100),a=t.extend(!0,{from:t.effects.scaledDimensions(s),to:t.effects.scaledDimensions(s,o,e.direction||"both"),origin:e.origin||["middle","center"]},e);e.fade&&(a.from.opacity=1,a.to.opacity=0),t.effects.effect.size.call(this,a,i)}),t.effects.define("puff","hide",function(e,i){var s=t.extend(!0,{},e,{fade:!0,percent:parseInt(e.percent,10)||150});t.effects.effect.scale.call(this,s,i)}),t.effects.define("pulsate","show",function(e,i){var s=t(this),n=e.mode,o="show"===n,a="hide"===n,r=o||a,h=2*(e.times||5)+(r?1:0),l=e.duration/h,c=0,u=1,d=s.queue().length;for((o||!s.is(":visible"))&&(s.css("opacity",0).show(),c=1);h>u;u++)s.animate({opacity:c},l,e.easing),c=1-c;s.animate({opacity:c},l,e.easing),s.queue(i),t.effects.unshift(s,d,h+1)}),t.effects.define("shake",function(e,i){var s=1,n=t(this),o=e.direction||"left",a=e.distance||20,r=e.times||3,h=2*r+1,l=Math.round(e.duration/h),c="up"===o||"down"===o?"top":"left",u="up"===o||"left"===o,d={},p={},f={},g=n.queue().length;for(t.effects.createPlaceholder(n),d[c]=(u?"-=":"+=")+a,p[c]=(u?"+=":"-=")+2*a,f[c]=(u?"-=":"+=")+2*a,n.animate(d,l,e.easing);r>s;s++)n.animate(p,l,e.easing).animate(f,l,e.easing);n.animate(p,l,e.easing).animate(d,l/2,e.easing).queue(i),t.effects.unshift(n,g,h+1)}),t.effects.define("slide","show",function(e,i){var s,n,o=t(this),a={up:["bottom","top"],down:["top","bottom"],left:["right","left"],right:["left","right"]},r=e.mode,h=e.direction||"left",l="up"===h||"down"===h?"top":"left",c="up"===h||"left"===h,u=e.distance||o["top"===l?"outerHeight":"outerWidth"](!0),d={};t.effects.createPlaceholder(o),s=o.cssClip(),n=o.position()[l],d[l]=(c?-1:1)*u+n,d.clip=o.cssClip(),d.clip[a[h][1]]=d.clip[a[h][0]],"show"===r&&(o.cssClip(d.clip),o.css(l,d[l]),d.clip=s,d[l]=n),o.animate(d,{queue:!1,duration:e.duration,easing:e.easing,complete:i})});var v;t.uiBackCompat!==!1&&(v=t.effects.define("transfer",function(e,i){t(this).transfer(e,i)}))});
\ No newline at end of file
diff --git a/js/jquery.min.js b/js/jquery.min.js
index ab28a247..d467083b 100644
--- a/js/jquery.min.js
+++ b/js/jquery.min.js
@@ -1,4 +1,2 @@
-/*! jQuery v1.11.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */
-!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.1",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b=a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),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("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="
",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="
",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++d
b;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML=" ","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML=" ",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h;
-if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML=" a ",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML=" ",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function ab(){return!0}function bb(){return!1}function cb(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h ]","i"),hb=/^\s+/,ib=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,jb=/<([\w:]+)/,kb=/\s*$/g,rb={option:[1,""," "],legend:[1,""," "],area:[1,""," "],param:[1,""," "],thead:[1,""],tr:[2,""],col:[2,""],td:[3,""],_default:k.htmlSerialize?[0,"",""]:[1,"X","
"]},sb=db(y),tb=sb.appendChild(y.createElement("div"));rb.optgroup=rb.option,rb.tbody=rb.tfoot=rb.colgroup=rb.caption=rb.thead,rb.th=rb.td;function ub(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ub(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function vb(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wb(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xb(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function yb(a){var b=pb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function zb(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Ab(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Bb(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xb(b).text=a.text,yb(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!gb.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(tb.innerHTML=a.outerHTML,tb.removeChild(f=tb.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ub(f),h=ub(a),g=0;null!=(e=h[g]);++g)d[g]&&Bb(e,d[g]);if(b)if(c)for(h=h||ub(a),d=d||ub(f),g=0;null!=(e=h[g]);g++)Ab(e,d[g]);else Ab(a,f);return d=ub(f,"script"),d.length>0&&zb(d,!i&&ub(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=db(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(lb.test(f)){h=h||o.appendChild(b.createElement("div")),i=(jb.exec(f)||["",""])[1].toLowerCase(),l=rb[i]||rb._default,h.innerHTML=l[1]+f.replace(ib,"<$1>$2>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&hb.test(f)&&p.push(b.createTextNode(hb.exec(f)[0])),!k.tbody){f="table"!==i||kb.test(f)?""!==l[1]||kb.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ub(p,"input"),vb),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ub(o.appendChild(f),"script"),g&&zb(h),c)){e=0;while(f=h[e++])ob.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ub(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&zb(ub(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ub(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fb,""):void 0;if(!("string"!=typeof a||mb.test(a)||!k.htmlSerialize&&gb.test(a)||!k.leadingWhitespace&&hb.test(a)||rb[(jb.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ib,"<$1>$2>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ub(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ub(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&nb.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ub(i,"script"),xb),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ub(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,yb),j=0;f>j;j++)d=g[j],ob.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qb,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Cb,Db={};function Eb(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fb(a){var b=y,c=Db[a];return c||(c=Eb(a,b),"none"!==c&&c||(Cb=(Cb||m("")).appendTo(b.documentElement),b=(Cb[0].contentWindow||Cb[0].contentDocument).document,b.write(),b.close(),c=Eb(a,b),Cb.detach()),Db[a]=c),c}!function(){var a;k.shrinkWrapBlocks=function(){if(null!=a)return a;a=!1;var b,c,d;return c=y.getElementsByTagName("body")[0],c&&c.style?(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:1px;width:1px;zoom:1",b.appendChild(y.createElement("div")).style.width="5px",a=3!==b.offsetWidth),c.removeChild(d),a):void 0}}();var Gb=/^margin/,Hb=new RegExp("^("+S+")(?!px)[a-z%]+$","i"),Ib,Jb,Kb=/^(top|right|bottom|left)$/;a.getComputedStyle?(Ib=function(a){return a.ownerDocument.defaultView.getComputedStyle(a,null)},Jb=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ib(a),g=c?c.getPropertyValue(b)||c[b]:void 0,c&&(""!==g||m.contains(a.ownerDocument,a)||(g=m.style(a,b)),Hb.test(g)&&Gb.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0===g?g:g+""}):y.documentElement.currentStyle&&(Ib=function(a){return a.currentStyle},Jb=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ib(a),g=c?c[b]:void 0,null==g&&h&&h[b]&&(g=h[b]),Hb.test(g)&&!Kb.test(b)&&(d=h.left,e=a.runtimeStyle,f=e&&e.left,f&&(e.left=a.currentStyle.left),h.left="fontSize"===b?"1em":g,g=h.pixelLeft+"px",h.left=d,f&&(e.left=f)),void 0===g?g:g+""||"auto"});function Lb(a,b){return{get:function(){var c=a();if(null!=c)return c?void delete this.get:(this.get=b).apply(this,arguments)}}}!function(){var b,c,d,e,f,g,h;if(b=y.createElement("div"),b.innerHTML=" a ",d=b.getElementsByTagName("a")[0],c=d&&d.style){c.cssText="float:left;opacity:.5",k.opacity="0.5"===c.opacity,k.cssFloat=!!c.cssFloat,b.style.backgroundClip="content-box",b.cloneNode(!0).style.backgroundClip="",k.clearCloneStyle="content-box"===b.style.backgroundClip,k.boxSizing=""===c.boxSizing||""===c.MozBoxSizing||""===c.WebkitBoxSizing,m.extend(k,{reliableHiddenOffsets:function(){return null==g&&i(),g},boxSizingReliable:function(){return null==f&&i(),f},pixelPosition:function(){return null==e&&i(),e},reliableMarginRight:function(){return null==h&&i(),h}});function i(){var b,c,d,i;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),b.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",e=f=!1,h=!0,a.getComputedStyle&&(e="1%"!==(a.getComputedStyle(b,null)||{}).top,f="4px"===(a.getComputedStyle(b,null)||{width:"4px"}).width,i=b.appendChild(y.createElement("div")),i.style.cssText=b.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",i.style.marginRight=i.style.width="0",b.style.width="1px",h=!parseFloat((a.getComputedStyle(i,null)||{}).marginRight)),b.innerHTML="",i=b.getElementsByTagName("td"),i[0].style.cssText="margin:0;border:0;padding:0;display:none",g=0===i[0].offsetHeight,g&&(i[0].style.display="",i[1].style.display="none",g=0===i[0].offsetHeight),c.removeChild(d))}}}(),m.swap=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};var Mb=/alpha\([^)]*\)/i,Nb=/opacity\s*=\s*([^)]*)/,Ob=/^(none|table(?!-c[ea]).+)/,Pb=new RegExp("^("+S+")(.*)$","i"),Qb=new RegExp("^([+-])=("+S+")","i"),Rb={position:"absolute",visibility:"hidden",display:"block"},Sb={letterSpacing:"0",fontWeight:"400"},Tb=["Webkit","O","Moz","ms"];function Ub(a,b){if(b in a)return b;var c=b.charAt(0).toUpperCase()+b.slice(1),d=b,e=Tb.length;while(e--)if(b=Tb[e]+c,b in a)return b;return d}function Vb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=m._data(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&U(d)&&(f[g]=m._data(d,"olddisplay",Fb(d.nodeName)))):(e=U(d),(c&&"none"!==c||!e)&&m._data(d,"olddisplay",e?c:m.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}function Wb(a,b,c){var d=Pb.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function Xb(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=m.css(a,c+T[f],!0,e)),d?("content"===c&&(g-=m.css(a,"padding"+T[f],!0,e)),"margin"!==c&&(g-=m.css(a,"border"+T[f]+"Width",!0,e))):(g+=m.css(a,"padding"+T[f],!0,e),"padding"!==c&&(g+=m.css(a,"border"+T[f]+"Width",!0,e)));return g}function Yb(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=Ib(a),g=k.boxSizing&&"border-box"===m.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=Jb(a,b,f),(0>e||null==e)&&(e=a.style[b]),Hb.test(e))return e;d=g&&(k.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+Xb(a,b,c||(g?"border":"content"),d,f)+"px"}m.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Jb(a,"opacity");return""===c?"1":c}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":k.cssFloat?"cssFloat":"styleFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=m.camelCase(b),i=a.style;if(b=m.cssProps[h]||(m.cssProps[h]=Ub(i,h)),g=m.cssHooks[b]||m.cssHooks[h],void 0===c)return g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b];if(f=typeof c,"string"===f&&(e=Qb.exec(c))&&(c=(e[1]+1)*e[2]+parseFloat(m.css(a,b)),f="number"),null!=c&&c===c&&("number"!==f||m.cssNumber[h]||(c+="px"),k.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),!(g&&"set"in g&&void 0===(c=g.set(a,c,d)))))try{i[b]=c}catch(j){}}},css:function(a,b,c,d){var e,f,g,h=m.camelCase(b);return b=m.cssProps[h]||(m.cssProps[h]=Ub(a.style,h)),g=m.cssHooks[b]||m.cssHooks[h],g&&"get"in g&&(f=g.get(a,!0,c)),void 0===f&&(f=Jb(a,b,d)),"normal"===f&&b in Sb&&(f=Sb[b]),""===c||c?(e=parseFloat(f),c===!0||m.isNumeric(e)?e||0:f):f}}),m.each(["height","width"],function(a,b){m.cssHooks[b]={get:function(a,c,d){return c?Ob.test(m.css(a,"display"))&&0===a.offsetWidth?m.swap(a,Rb,function(){return Yb(a,b,d)}):Yb(a,b,d):void 0},set:function(a,c,d){var e=d&&Ib(a);return Wb(a,c,d?Xb(a,b,d,k.boxSizing&&"border-box"===m.css(a,"boxSizing",!1,e),e):0)}}}),k.opacity||(m.cssHooks.opacity={get:function(a,b){return Nb.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=m.isNumeric(b)?"alpha(opacity="+100*b+")":"",f=d&&d.filter||c.filter||"";c.zoom=1,(b>=1||""===b)&&""===m.trim(f.replace(Mb,""))&&c.removeAttribute&&(c.removeAttribute("filter"),""===b||d&&!d.filter)||(c.filter=Mb.test(f)?f.replace(Mb,e):f+" "+e)}}),m.cssHooks.marginRight=Lb(k.reliableMarginRight,function(a,b){return b?m.swap(a,{display:"inline-block"},Jb,[a,"marginRight"]):void 0}),m.each({margin:"",padding:"",border:"Width"},function(a,b){m.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+T[d]+b]=f[d]||f[d-2]||f[0];return e}},Gb.test(a)||(m.cssHooks[a+b].set=Wb)}),m.fn.extend({css:function(a,b){return V(this,function(a,b,c){var d,e,f={},g=0;if(m.isArray(b)){for(d=Ib(a),e=b.length;e>g;g++)f[b[g]]=m.css(a,b[g],!1,d);return f}return void 0!==c?m.style(a,b,c):m.css(a,b)},a,b,arguments.length>1)},show:function(){return Vb(this,!0)},hide:function(){return Vb(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){U(this)?m(this).show():m(this).hide()})}});function Zb(a,b,c,d,e){return new Zb.prototype.init(a,b,c,d,e)}m.Tween=Zb,Zb.prototype={constructor:Zb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(m.cssNumber[c]?"":"px")
-},cur:function(){var a=Zb.propHooks[this.prop];return a&&a.get?a.get(this):Zb.propHooks._default.get(this)},run:function(a){var b,c=Zb.propHooks[this.prop];return this.pos=b=this.options.duration?m.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Zb.propHooks._default.set(this),this}},Zb.prototype.init.prototype=Zb.prototype,Zb.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=m.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){m.fx.step[a.prop]?m.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[m.cssProps[a.prop]]||m.cssHooks[a.prop])?m.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Zb.propHooks.scrollTop=Zb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},m.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},m.fx=Zb.prototype.init,m.fx.step={};var $b,_b,ac=/^(?:toggle|show|hide)$/,bc=new RegExp("^(?:([+-])=|)("+S+")([a-z%]*)$","i"),cc=/queueHooks$/,dc=[ic],ec={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=bc.exec(b),f=e&&e[3]||(m.cssNumber[a]?"":"px"),g=(m.cssNumber[a]||"px"!==f&&+d)&&bc.exec(m.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,m.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function fc(){return setTimeout(function(){$b=void 0}),$b=m.now()}function gc(a,b){var c,d={height:a},e=0;for(b=b?1:0;4>e;e+=2-b)c=T[e],d["margin"+c]=d["padding"+c]=a;return b&&(d.opacity=d.width=a),d}function hc(a,b,c){for(var d,e=(ec[b]||[]).concat(ec["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function ic(a,b,c){var d,e,f,g,h,i,j,l,n=this,o={},p=a.style,q=a.nodeType&&U(a),r=m._data(a,"fxshow");c.queue||(h=m._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,n.always(function(){n.always(function(){h.unqueued--,m.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[p.overflow,p.overflowX,p.overflowY],j=m.css(a,"display"),l="none"===j?m._data(a,"olddisplay")||Fb(a.nodeName):j,"inline"===l&&"none"===m.css(a,"float")&&(k.inlineBlockNeedsLayout&&"inline"!==Fb(a.nodeName)?p.zoom=1:p.display="inline-block")),c.overflow&&(p.overflow="hidden",k.shrinkWrapBlocks()||n.always(function(){p.overflow=c.overflow[0],p.overflowX=c.overflow[1],p.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],ac.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(q?"hide":"show")){if("show"!==e||!r||void 0===r[d])continue;q=!0}o[d]=r&&r[d]||m.style(a,d)}else j=void 0;if(m.isEmptyObject(o))"inline"===("none"===j?Fb(a.nodeName):j)&&(p.display=j);else{r?"hidden"in r&&(q=r.hidden):r=m._data(a,"fxshow",{}),f&&(r.hidden=!q),q?m(a).show():n.done(function(){m(a).hide()}),n.done(function(){var b;m._removeData(a,"fxshow");for(b in o)m.style(a,b,o[b])});for(d in o)g=hc(q?r[d]:0,d,n),d in r||(r[d]=g.start,q&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function jc(a,b){var c,d,e,f,g;for(c in a)if(d=m.camelCase(c),e=b[d],f=a[c],m.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=m.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function kc(a,b,c){var d,e,f=0,g=dc.length,h=m.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=$b||fc(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:m.extend({},b),opts:m.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:$b||fc(),duration:c.duration,tweens:[],createTween:function(b,c){var d=m.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(jc(k,j.opts.specialEasing);g>f;f++)if(d=dc[f].call(j,a,k,j.opts))return d;return m.map(k,hc,j),m.isFunction(j.opts.start)&&j.opts.start.call(a,j),m.fx.timer(m.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}m.Animation=m.extend(kc,{tweener:function(a,b){m.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],ec[c]=ec[c]||[],ec[c].unshift(b)},prefilter:function(a,b){b?dc.unshift(a):dc.push(a)}}),m.speed=function(a,b,c){var d=a&&"object"==typeof a?m.extend({},a):{complete:c||!c&&b||m.isFunction(a)&&a,duration:a,easing:c&&b||b&&!m.isFunction(b)&&b};return d.duration=m.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in m.fx.speeds?m.fx.speeds[d.duration]:m.fx.speeds._default,(null==d.queue||d.queue===!0)&&(d.queue="fx"),d.old=d.complete,d.complete=function(){m.isFunction(d.old)&&d.old.call(this),d.queue&&m.dequeue(this,d.queue)},d},m.fn.extend({fadeTo:function(a,b,c,d){return this.filter(U).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=m.isEmptyObject(a),f=m.speed(b,c,d),g=function(){var b=kc(this,m.extend({},a),f);(e||m._data(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=m.timers,g=m._data(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&cc.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));(b||!c)&&m.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=m._data(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=m.timers,g=d?d.length:0;for(c.finish=!0,m.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),m.each(["toggle","show","hide"],function(a,b){var c=m.fn[b];m.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(gc(b,!0),a,d,e)}}),m.each({slideDown:gc("show"),slideUp:gc("hide"),slideToggle:gc("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){m.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),m.timers=[],m.fx.tick=function(){var a,b=m.timers,c=0;for($b=m.now();ca ",d=b.getElementsByTagName("a")[0],c=y.createElement("select"),e=c.appendChild(y.createElement("option")),a=b.getElementsByTagName("input")[0],d.style.cssText="top:1px",k.getSetAttribute="t"!==b.className,k.style=/top/.test(d.getAttribute("style")),k.hrefNormalized="/a"===d.getAttribute("href"),k.checkOn=!!a.value,k.optSelected=e.selected,k.enctype=!!y.createElement("form").enctype,c.disabled=!0,k.optDisabled=!e.disabled,a=y.createElement("input"),a.setAttribute("value",""),k.input=""===a.getAttribute("value"),a.value="t",a.setAttribute("type","radio"),k.radioValue="t"===a.value}();var lc=/\r/g;m.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=m.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,m(this).val()):a,null==e?e="":"number"==typeof e?e+="":m.isArray(e)&&(e=m.map(e,function(a){return null==a?"":a+""})),b=m.valHooks[this.type]||m.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=m.valHooks[e.type]||m.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(lc,""):null==c?"":c)}}}),m.extend({valHooks:{option:{get:function(a){var b=m.find.attr(a,"value");return null!=b?b:m.trim(m.text(a))}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(k.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&m.nodeName(c.parentNode,"optgroup"))){if(b=m(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=m.makeArray(b),g=e.length;while(g--)if(d=e[g],m.inArray(m.valHooks.option.get(d),f)>=0)try{d.selected=c=!0}catch(h){d.scrollHeight}else d.selected=!1;return c||(a.selectedIndex=-1),e}}}}),m.each(["radio","checkbox"],function(){m.valHooks[this]={set:function(a,b){return m.isArray(b)?a.checked=m.inArray(m(a).val(),b)>=0:void 0}},k.checkOn||(m.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var mc,nc,oc=m.expr.attrHandle,pc=/^(?:checked|selected)$/i,qc=k.getSetAttribute,rc=k.input;m.fn.extend({attr:function(a,b){return V(this,m.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){m.removeAttr(this,a)})}}),m.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(a&&3!==f&&8!==f&&2!==f)return typeof a.getAttribute===K?m.prop(a,b,c):(1===f&&m.isXMLDoc(a)||(b=b.toLowerCase(),d=m.attrHooks[b]||(m.expr.match.bool.test(b)?nc:mc)),void 0===c?d&&"get"in d&&null!==(e=d.get(a,b))?e:(e=m.find.attr(a,b),null==e?void 0:e):null!==c?d&&"set"in d&&void 0!==(e=d.set(a,c,b))?e:(a.setAttribute(b,c+""),c):void m.removeAttr(a,b))},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=m.propFix[c]||c,m.expr.match.bool.test(c)?rc&&qc||!pc.test(c)?a[d]=!1:a[m.camelCase("default-"+c)]=a[d]=!1:m.attr(a,c,""),a.removeAttribute(qc?c:d)},attrHooks:{type:{set:function(a,b){if(!k.radioValue&&"radio"===b&&m.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),nc={set:function(a,b,c){return b===!1?m.removeAttr(a,c):rc&&qc||!pc.test(c)?a.setAttribute(!qc&&m.propFix[c]||c,c):a[m.camelCase("default-"+c)]=a[c]=!0,c}},m.each(m.expr.match.bool.source.match(/\w+/g),function(a,b){var c=oc[b]||m.find.attr;oc[b]=rc&&qc||!pc.test(b)?function(a,b,d){var e,f;return d||(f=oc[b],oc[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,oc[b]=f),e}:function(a,b,c){return c?void 0:a[m.camelCase("default-"+b)]?b.toLowerCase():null}}),rc&&qc||(m.attrHooks.value={set:function(a,b,c){return m.nodeName(a,"input")?void(a.defaultValue=b):mc&&mc.set(a,b,c)}}),qc||(mc={set:function(a,b,c){var d=a.getAttributeNode(c);return d||a.setAttributeNode(d=a.ownerDocument.createAttribute(c)),d.value=b+="","value"===c||b===a.getAttribute(c)?b:void 0}},oc.id=oc.name=oc.coords=function(a,b,c){var d;return c?void 0:(d=a.getAttributeNode(b))&&""!==d.value?d.value:null},m.valHooks.button={get:function(a,b){var c=a.getAttributeNode(b);return c&&c.specified?c.value:void 0},set:mc.set},m.attrHooks.contenteditable={set:function(a,b,c){mc.set(a,""===b?!1:b,c)}},m.each(["width","height"],function(a,b){m.attrHooks[b]={set:function(a,c){return""===c?(a.setAttribute(b,"auto"),c):void 0}}})),k.style||(m.attrHooks.style={get:function(a){return a.style.cssText||void 0},set:function(a,b){return a.style.cssText=b+""}});var sc=/^(?:input|select|textarea|button|object)$/i,tc=/^(?:a|area)$/i;m.fn.extend({prop:function(a,b){return V(this,m.prop,a,b,arguments.length>1)},removeProp:function(a){return a=m.propFix[a]||a,this.each(function(){try{this[a]=void 0,delete this[a]}catch(b){}})}}),m.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!m.isXMLDoc(a),f&&(b=m.propFix[b]||b,e=m.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=m.find.attr(a,"tabindex");return b?parseInt(b,10):sc.test(a.nodeName)||tc.test(a.nodeName)&&a.href?0:-1}}}}),k.hrefNormalized||m.each(["href","src"],function(a,b){m.propHooks[b]={get:function(a){return a.getAttribute(b,4)}}}),k.optSelected||(m.propHooks.selected={get:function(a){var b=a.parentNode;return b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex),null}}),m.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){m.propFix[this.toLowerCase()]=this}),k.enctype||(m.propFix.enctype="encoding");var uc=/[\t\r\n\f]/g;m.fn.extend({addClass:function(a){var b,c,d,e,f,g,h=0,i=this.length,j="string"==typeof a&&a;if(m.isFunction(a))return this.each(function(b){m(this).addClass(a.call(this,b,this.className))});if(j)for(b=(a||"").match(E)||[];i>h;h++)if(c=this[h],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(uc," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=m.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0,i=this.length,j=0===arguments.length||"string"==typeof a&&a;if(m.isFunction(a))return this.each(function(b){m(this).removeClass(a.call(this,b,this.className))});if(j)for(b=(a||"").match(E)||[];i>h;h++)if(c=this[h],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(uc," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?m.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(m.isFunction(a)?function(c){m(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=m(this),f=a.match(E)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===K||"boolean"===c)&&(this.className&&m._data(this,"__className__",this.className),this.className=this.className||a===!1?"":m._data(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(uc," ").indexOf(b)>=0)return!0;return!1}}),m.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){m.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),m.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}});var vc=m.now(),wc=/\?/,xc=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;m.parseJSON=function(b){if(a.JSON&&a.JSON.parse)return a.JSON.parse(b+"");var c,d=null,e=m.trim(b+"");return e&&!m.trim(e.replace(xc,function(a,b,e,f){return c&&b&&(d=0),0===d?a:(c=e||b,d+=!f-!e,"")}))?Function("return "+e)():m.error("Invalid JSON: "+b)},m.parseXML=function(b){var c,d;if(!b||"string"!=typeof b)return null;try{a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b))}catch(e){c=void 0}return c&&c.documentElement&&!c.getElementsByTagName("parsererror").length||m.error("Invalid XML: "+b),c};var yc,zc,Ac=/#.*$/,Bc=/([?&])_=[^&]*/,Cc=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Dc=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Ec=/^(?:GET|HEAD)$/,Fc=/^\/\//,Gc=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,Hc={},Ic={},Jc="*/".concat("*");try{zc=location.href}catch(Kc){zc=y.createElement("a"),zc.href="",zc=zc.href}yc=Gc.exec(zc.toLowerCase())||[];function Lc(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(E)||[];if(m.isFunction(c))while(d=f[e++])"+"===d.charAt(0)?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Mc(a,b,c,d){var e={},f=a===Ic;function g(h){var i;return e[h]=!0,m.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Nc(a,b){var c,d,e=m.ajaxSettings.flatOptions||{};for(d in b)void 0!==b[d]&&((e[d]?a:c||(c={}))[d]=b[d]);return c&&m.extend(!0,a,c),a}function Oc(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===e&&(e=a.mimeType||b.getResponseHeader("Content-Type"));if(e)for(g in h)if(h[g]&&h[g].test(e)){i.unshift(g);break}if(i[0]in c)f=i[0];else{for(g in c){if(!i[0]||a.converters[g+" "+i[0]]){f=g;break}d||(d=g)}f=f||d}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function Pc(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}m.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:zc,type:"GET",isLocal:Dc.test(yc[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Jc,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":m.parseJSON,"text xml":m.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Nc(Nc(a,m.ajaxSettings),b):Nc(m.ajaxSettings,a)},ajaxPrefilter:Lc(Hc),ajaxTransport:Lc(Ic),ajax:function(a,b){"object"==typeof a&&(b=a,a=void 0),b=b||{};var c,d,e,f,g,h,i,j,k=m.ajaxSetup({},b),l=k.context||k,n=k.context&&(l.nodeType||l.jquery)?m(l):m.event,o=m.Deferred(),p=m.Callbacks("once memory"),q=k.statusCode||{},r={},s={},t=0,u="canceled",v={readyState:0,getResponseHeader:function(a){var b;if(2===t){if(!j){j={};while(b=Cc.exec(f))j[b[1].toLowerCase()]=b[2]}b=j[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===t?f:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return t||(a=s[c]=s[c]||a,r[a]=b),this},overrideMimeType:function(a){return t||(k.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>t)for(b in a)q[b]=[q[b],a[b]];else v.always(a[v.status]);return this},abort:function(a){var b=a||u;return i&&i.abort(b),x(0,b),this}};if(o.promise(v).complete=p.add,v.success=v.done,v.error=v.fail,k.url=((a||k.url||zc)+"").replace(Ac,"").replace(Fc,yc[1]+"//"),k.type=b.method||b.type||k.method||k.type,k.dataTypes=m.trim(k.dataType||"*").toLowerCase().match(E)||[""],null==k.crossDomain&&(c=Gc.exec(k.url.toLowerCase()),k.crossDomain=!(!c||c[1]===yc[1]&&c[2]===yc[2]&&(c[3]||("http:"===c[1]?"80":"443"))===(yc[3]||("http:"===yc[1]?"80":"443")))),k.data&&k.processData&&"string"!=typeof k.data&&(k.data=m.param(k.data,k.traditional)),Mc(Hc,k,b,v),2===t)return v;h=k.global,h&&0===m.active++&&m.event.trigger("ajaxStart"),k.type=k.type.toUpperCase(),k.hasContent=!Ec.test(k.type),e=k.url,k.hasContent||(k.data&&(e=k.url+=(wc.test(e)?"&":"?")+k.data,delete k.data),k.cache===!1&&(k.url=Bc.test(e)?e.replace(Bc,"$1_="+vc++):e+(wc.test(e)?"&":"?")+"_="+vc++)),k.ifModified&&(m.lastModified[e]&&v.setRequestHeader("If-Modified-Since",m.lastModified[e]),m.etag[e]&&v.setRequestHeader("If-None-Match",m.etag[e])),(k.data&&k.hasContent&&k.contentType!==!1||b.contentType)&&v.setRequestHeader("Content-Type",k.contentType),v.setRequestHeader("Accept",k.dataTypes[0]&&k.accepts[k.dataTypes[0]]?k.accepts[k.dataTypes[0]]+("*"!==k.dataTypes[0]?", "+Jc+"; q=0.01":""):k.accepts["*"]);for(d in k.headers)v.setRequestHeader(d,k.headers[d]);if(k.beforeSend&&(k.beforeSend.call(l,v,k)===!1||2===t))return v.abort();u="abort";for(d in{success:1,error:1,complete:1})v[d](k[d]);if(i=Mc(Ic,k,b,v)){v.readyState=1,h&&n.trigger("ajaxSend",[v,k]),k.async&&k.timeout>0&&(g=setTimeout(function(){v.abort("timeout")},k.timeout));try{t=1,i.send(r,x)}catch(w){if(!(2>t))throw w;x(-1,w)}}else x(-1,"No Transport");function x(a,b,c,d){var j,r,s,u,w,x=b;2!==t&&(t=2,g&&clearTimeout(g),i=void 0,f=d||"",v.readyState=a>0?4:0,j=a>=200&&300>a||304===a,c&&(u=Oc(k,v,c)),u=Pc(k,u,v,j),j?(k.ifModified&&(w=v.getResponseHeader("Last-Modified"),w&&(m.lastModified[e]=w),w=v.getResponseHeader("etag"),w&&(m.etag[e]=w)),204===a||"HEAD"===k.type?x="nocontent":304===a?x="notmodified":(x=u.state,r=u.data,s=u.error,j=!s)):(s=x,(a||!x)&&(x="error",0>a&&(a=0))),v.status=a,v.statusText=(b||x)+"",j?o.resolveWith(l,[r,x,v]):o.rejectWith(l,[v,x,s]),v.statusCode(q),q=void 0,h&&n.trigger(j?"ajaxSuccess":"ajaxError",[v,k,j?r:s]),p.fireWith(l,[v,x]),h&&(n.trigger("ajaxComplete",[v,k]),--m.active||m.event.trigger("ajaxStop")))}return v},getJSON:function(a,b,c){return m.get(a,b,c,"json")},getScript:function(a,b){return m.get(a,void 0,b,"script")}}),m.each(["get","post"],function(a,b){m[b]=function(a,c,d,e){return m.isFunction(c)&&(e=e||d,d=c,c=void 0),m.ajax({url:a,type:b,dataType:e,data:c,success:d})}}),m.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){m.fn[b]=function(a){return this.on(b,a)}}),m._evalUrl=function(a){return m.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},m.fn.extend({wrapAll:function(a){if(m.isFunction(a))return this.each(function(b){m(this).wrapAll(a.call(this,b))});if(this[0]){var b=m(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&1===a.firstChild.nodeType)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return this.each(m.isFunction(a)?function(b){m(this).wrapInner(a.call(this,b))}:function(){var b=m(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=m.isFunction(a);return this.each(function(c){m(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){m.nodeName(this,"body")||m(this).replaceWith(this.childNodes)}).end()}}),m.expr.filters.hidden=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0||!k.reliableHiddenOffsets()&&"none"===(a.style&&a.style.display||m.css(a,"display"))},m.expr.filters.visible=function(a){return!m.expr.filters.hidden(a)};var Qc=/%20/g,Rc=/\[\]$/,Sc=/\r?\n/g,Tc=/^(?:submit|button|image|reset|file)$/i,Uc=/^(?:input|select|textarea|keygen)/i;function Vc(a,b,c,d){var e;if(m.isArray(b))m.each(b,function(b,e){c||Rc.test(a)?d(a,e):Vc(a+"["+("object"==typeof e?b:"")+"]",e,c,d)});else if(c||"object"!==m.type(b))d(a,b);else for(e in b)Vc(a+"["+e+"]",b[e],c,d)}m.param=function(a,b){var c,d=[],e=function(a,b){b=m.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=m.ajaxSettings&&m.ajaxSettings.traditional),m.isArray(a)||a.jquery&&!m.isPlainObject(a))m.each(a,function(){e(this.name,this.value)});else for(c in a)Vc(c,a[c],b,e);return d.join("&").replace(Qc,"+")},m.fn.extend({serialize:function(){return m.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=m.prop(this,"elements");return a?m.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!m(this).is(":disabled")&&Uc.test(this.nodeName)&&!Tc.test(a)&&(this.checked||!W.test(a))}).map(function(a,b){var c=m(this).val();return null==c?null:m.isArray(c)?m.map(c,function(a){return{name:b.name,value:a.replace(Sc,"\r\n")}}):{name:b.name,value:c.replace(Sc,"\r\n")}}).get()}}),m.ajaxSettings.xhr=void 0!==a.ActiveXObject?function(){return!this.isLocal&&/^(get|post|head|put|delete|options)$/i.test(this.type)&&Zc()||$c()}:Zc;var Wc=0,Xc={},Yc=m.ajaxSettings.xhr();a.ActiveXObject&&m(a).on("unload",function(){for(var a in Xc)Xc[a](void 0,!0)}),k.cors=!!Yc&&"withCredentials"in Yc,Yc=k.ajax=!!Yc,Yc&&m.ajaxTransport(function(a){if(!a.crossDomain||k.cors){var b;return{send:function(c,d){var e,f=a.xhr(),g=++Wc;if(f.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(e in a.xhrFields)f[e]=a.xhrFields[e];a.mimeType&&f.overrideMimeType&&f.overrideMimeType(a.mimeType),a.crossDomain||c["X-Requested-With"]||(c["X-Requested-With"]="XMLHttpRequest");for(e in c)void 0!==c[e]&&f.setRequestHeader(e,c[e]+"");f.send(a.hasContent&&a.data||null),b=function(c,e){var h,i,j;if(b&&(e||4===f.readyState))if(delete Xc[g],b=void 0,f.onreadystatechange=m.noop,e)4!==f.readyState&&f.abort();else{j={},h=f.status,"string"==typeof f.responseText&&(j.text=f.responseText);try{i=f.statusText}catch(k){i=""}h||!a.isLocal||a.crossDomain?1223===h&&(h=204):h=j.text?200:404}j&&d(h,i,j,f.getAllResponseHeaders())},a.async?4===f.readyState?setTimeout(b):f.onreadystatechange=Xc[g]=b:b()},abort:function(){b&&b(void 0,!0)}}}});function Zc(){try{return new a.XMLHttpRequest}catch(b){}}function $c(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}m.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(a){return m.globalEval(a),a}}}),m.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),m.ajaxTransport("script",function(a){if(a.crossDomain){var b,c=y.head||m("head")[0]||y.documentElement;return{send:function(d,e){b=y.createElement("script"),b.async=!0,a.scriptCharset&&(b.charset=a.scriptCharset),b.src=a.url,b.onload=b.onreadystatechange=function(a,c){(c||!b.readyState||/loaded|complete/.test(b.readyState))&&(b.onload=b.onreadystatechange=null,b.parentNode&&b.parentNode.removeChild(b),b=null,c||e(200,"success"))},c.insertBefore(b,c.firstChild)},abort:function(){b&&b.onload(void 0,!0)}}}});var _c=[],ad=/(=)\?(?=&|$)|\?\?/;m.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=_c.pop()||m.expando+"_"+vc++;return this[a]=!0,a}}),m.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(ad.test(b.url)?"url":"string"==typeof b.data&&!(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&ad.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=m.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(ad,"$1"+e):b.jsonp!==!1&&(b.url+=(wc.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||m.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,_c.push(e)),g&&m.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),m.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||y;var d=u.exec(a),e=!c&&[];return d?[b.createElement(d[1])]:(d=m.buildFragment([a],b,e),e&&e.length&&m(e).remove(),m.merge([],d.childNodes))};var bd=m.fn.load;m.fn.load=function(a,b,c){if("string"!=typeof a&&bd)return bd.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>=0&&(d=m.trim(a.slice(h,a.length)),a=a.slice(0,h)),m.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(f="POST"),g.length>0&&m.ajax({url:a,type:f,dataType:"html",data:b}).done(function(a){e=arguments,g.html(d?m("").append(m.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,e||[a.responseText,b,a])}),this},m.expr.filters.animated=function(a){return m.grep(m.timers,function(b){return a===b.elem}).length};var cd=a.document.documentElement;function dd(a){return m.isWindow(a)?a:9===a.nodeType?a.defaultView||a.parentWindow:!1}m.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=m.css(a,"position"),l=m(a),n={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=m.css(a,"top"),i=m.css(a,"left"),j=("absolute"===k||"fixed"===k)&&m.inArray("auto",[f,i])>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),m.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(n.top=b.top-h.top+g),null!=b.left&&(n.left=b.left-h.left+e),"using"in b?b.using.call(a,n):l.css(n)}},m.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){m.offset.setOffset(this,a,b)});var b,c,d={top:0,left:0},e=this[0],f=e&&e.ownerDocument;if(f)return b=f.documentElement,m.contains(b,e)?(typeof e.getBoundingClientRect!==K&&(d=e.getBoundingClientRect()),c=dd(f),{top:d.top+(c.pageYOffset||b.scrollTop)-(b.clientTop||0),left:d.left+(c.pageXOffset||b.scrollLeft)-(b.clientLeft||0)}):d},position:function(){if(this[0]){var a,b,c={top:0,left:0},d=this[0];return"fixed"===m.css(d,"position")?b=d.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),m.nodeName(a[0],"html")||(c=a.offset()),c.top+=m.css(a[0],"borderTopWidth",!0),c.left+=m.css(a[0],"borderLeftWidth",!0)),{top:b.top-c.top-m.css(d,"marginTop",!0),left:b.left-c.left-m.css(d,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||cd;while(a&&!m.nodeName(a,"html")&&"static"===m.css(a,"position"))a=a.offsetParent;return a||cd})}}),m.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c=/Y/.test(b);m.fn[a]=function(d){return V(this,function(a,d,e){var f=dd(a);return void 0===e?f?b in f?f[b]:f.document.documentElement[d]:a[d]:void(f?f.scrollTo(c?m(f).scrollLeft():e,c?e:m(f).scrollTop()):a[d]=e)},a,d,arguments.length,null)}}),m.each(["top","left"],function(a,b){m.cssHooks[b]=Lb(k.pixelPosition,function(a,c){return c?(c=Jb(a,b),Hb.test(c)?m(a).position()[b]+"px":c):void 0})}),m.each({Height:"height",Width:"width"},function(a,b){m.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){m.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return V(this,function(b,c,d){var e;return m.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?m.css(b,c,g):m.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),m.fn.size=function(){return this.length},m.fn.andSelf=m.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return m});var ed=a.jQuery,fd=a.$;return m.noConflict=function(b){return a.$===m&&(a.$=fd),b&&a.jQuery===m&&(a.jQuery=ed),m},typeof b===K&&(a.jQuery=a.$=m),m});
+/*! jQuery v3.5.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(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.5.1",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0
+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),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("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(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 de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){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[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.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},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML=" ",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML=" ";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.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 l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(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?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===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]||se.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]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(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(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||j,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,j=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML=" ",y.option=!!ce.lastChild;var ge={thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function qe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function Le(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function He(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):("number"==typeof f.top&&(f.top+="px"),"number"==typeof f.left&&(f.left+="px"),c.css(f))}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=$e(y.pixelPosition,function(e,t){if(t)return t=Be(e,n),Me.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+abstract class ApiCommand extends ApiParameter
+{
+
+ /**
+ * debug flag
+ *
+ * @var boolean
+ */
+ private $debug = false;
+
+ /**
+ * is admin flag
+ *
+ * @var boolean
+ */
+ private $is_admin = false;
+
+ /**
+ * internal user data array
+ *
+ * @var array
+ */
+ private $user_data = null;
+
+ /**
+ * logger interface
+ *
+ * @var \Froxlor\FroxlorLogger
+ */
+ private $logger = null;
+
+ /**
+ * mail interface
+ *
+ * @var \Froxlor\System\Mailer
+ */
+ private $mail = null;
+
+ /**
+ * whether the call is an internal one or not
+ *
+ * @var boolean
+ */
+ private $internal_call = false;
+
+ /**
+ * language strings array
+ *
+ * @var array
+ */
+ protected $lng = null;
+
+ /**
+ * froxlor version
+ *
+ * @var string
+ */
+ protected $version = null;
+
+ /**
+ * froxlor dbversion
+ *
+ * @var int
+ */
+ protected $dbversion = null;
+
+ /**
+ * froxlor version-branding
+ *
+ * @var string
+ */
+ protected $branding = null;
+
+ /**
+ *
+ * @param array $header
+ * optional, passed via API
+ * @param array $params
+ * optional, array of parameters (var=>value) for the command
+ * @param array $userinfo
+ * optional, passed via WebInterface (instead of $header)
+ * @param boolean $internal
+ * optional whether called internally, default false
+ *
+ * @throws \Exception
+ */
+ public function __construct($header = null, $params = null, $userinfo = null, $internal = false)
+ {
+ parent::__construct($params);
+
+ $this->version = \Froxlor\Froxlor::VERSION;
+ $this->dbversion = \Froxlor\Froxlor::DBVERSION;
+ $this->branding = \Froxlor\Froxlor::BRANDING;
+
+ if (! empty($header)) {
+ $this->readUserData($header);
+ } elseif (! empty($userinfo)) {
+ $this->user_data = $userinfo;
+ $this->is_admin = (isset($userinfo['adminsession']) && $userinfo['adminsession'] == 1 && $userinfo['adminid'] > 0) ? true : false;
+ } else {
+ throw new \Exception("Invalid user data", 500);
+ }
+ $this->logger = \Froxlor\FroxlorLogger::getInstanceOf($this->user_data);
+
+ // check whether the user is deactivated
+ if ($this->getUserDetail('deactivated') == 1) {
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::LOG_ERROR, LOG_INFO, "[API] User '" . $this->getUserDetail('loginnname') . "' tried to use API but is deactivated");
+ throw new \Exception("Account suspended", 406);
+ }
+
+ $this->initLang();
+
+ /**
+ * Initialize the mailingsystem
+ */
+ $this->mail = new \Froxlor\System\Mailer(true);
+
+ if ($this->debug) {
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::LOG_ERROR, LOG_DEBUG, "[API] " . get_called_class() . ": " . json_encode($params, JSON_UNESCAPED_SLASHES));
+ }
+
+ // set internal call flag
+ $this->internal_call = $internal;
+ }
+
+ /**
+ * initialize global $lng variable to have
+ * localized strings available for the ApiCommands
+ */
+ private function initLang()
+ {
+ global $lng;
+
+ // query the whole table
+ $result_stmt = \Froxlor\Database\Database::query("SELECT * FROM `" . TABLE_PANEL_LANGUAGE . "`");
+
+ $langs = array();
+ // presort languages
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $langs[$row['language']][] = $row;
+ }
+
+ // set default language before anything else to
+ // ensure that we can display messages
+ $language = \Froxlor\Settings::Get('panel.standardlanguage');
+
+ if (isset($this->user_data['language']) && isset($langs[$this->user_data['language']])) {
+ // default: use language from session, #277
+ $language = $this->user_data['language'];
+ } elseif (isset($this->user_data['def_language'])) {
+ $language = $this->user_data['def_language'];
+ }
+
+ // include every english language file we can get
+ foreach ($langs['English'] as $value) {
+ include_once \Froxlor\FileDir::makeSecurePath(\Froxlor\Froxlor::getInstallDir() . '/' . $value['file']);
+ }
+
+ // now include the selected language if its not english
+ if ($language != 'English') {
+ if (isset($langs[$language])) {
+ foreach ($langs[$language] as $value) {
+ include_once \Froxlor\FileDir::makeSecurePath(\Froxlor\Froxlor::getInstallDir() . '/' . $value['file']);
+ }
+ } else {
+ if ($this->debug) {
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::LOG_ERROR, LOG_DEBUG, "[API] unable to include user-language '" . $language . "'. Not found in database.", 404);
+ }
+ }
+ }
+
+ // last but not least include language references file
+ include_once \Froxlor\FileDir::makeSecurePath(\Froxlor\Froxlor::getInstallDir() . '/lng/lng_references.php');
+
+ // set array for ApiCommand
+ $this->lng = $lng;
+ }
+
+ /**
+ * returns an instance of the wanted ApiCommand (e.g.
+ * Customers, Domains, etc);
+ * this is used widely in the WebInterface
+ *
+ * @param array $userinfo
+ * array of user-data
+ * @param array $params
+ * array of parameters for the command
+ * @param boolean $internal
+ * optional whether called internally, default false
+ *
+ * @return ApiCommand
+ * @throws \Exception
+ */
+ public static function getLocal($userinfo = null, $params = null, $internal = false)
+ {
+ return new static(null, $params, $userinfo, $internal);
+ }
+
+ /**
+ * admin flag
+ *
+ * @return boolean
+ */
+ protected function isAdmin()
+ {
+ return $this->is_admin;
+ }
+
+ /**
+ * internal call flag
+ *
+ * @return boolean
+ */
+ protected function isInternal()
+ {
+ return $this->internal_call;
+ }
+
+ /**
+ * return field from user-table
+ *
+ * @param string $detail
+ *
+ * @return string
+ */
+ protected function getUserDetail($detail = null)
+ {
+ return (isset($this->user_data[$detail]) ? $this->user_data[$detail] : null);
+ }
+
+ /**
+ * return user-data array
+ *
+ * @return array
+ */
+ protected function getUserData()
+ {
+ return $this->user_data;
+ }
+
+ /**
+ * return SQL when parameter $sql_search is given via API
+ *
+ * @param array $sql_search
+ * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), LIKE is used if left empty and 'value' => searchvalue
+ * @param array $query_fields
+ * optional array of placeholders mapped to the actual value which is used in the API commands when executing the statement [internal]
+ * @param boolean $append
+ * optional append to WHERE clause rather then create new one, default false [internal]
+ *
+ * @return string
+ */
+ protected function getSearchWhere(&$query_fields = array(), $append = false)
+ {
+ $search = $this->getParam('sql_search', true, array());
+ $condition = '';
+ if (! empty($search)) {
+ if ($append == true) {
+ $condition = ' AND ';
+ } else {
+ $condition = ' WHERE ';
+ }
+ $ops = array(
+ '<',
+ '>',
+ '='
+ );
+ $first = true;
+ foreach ($search as $field => $valoper) {
+ $cleanfield = str_replace(".", "", $field);
+ $sortfield = explode('.', $field);
+ foreach ($sortfield as $id => $sfield) {
+ if (substr($sfield, - 1, 1) != '`') {
+ $sfield .= '`';
+ }
+ if ($sfield[0] != '`') {
+ $sfield = '`' . $sfield;
+ }
+ $sortfield[$id] = $sfield;
+ }
+ $field = implode('.', $sortfield);
+ if (! $first) {
+ $condition .= ' AND ';
+ }
+ if (! is_array($valoper) || ! isset($valoper['op']) || empty($valoper['op'])) {
+ $condition .= $field . ' LIKE :' . $cleanfield;
+ if (! is_array($valoper)) {
+ $query_fields[':' . $cleanfield] = '%' . $valoper . '%';
+ } else {
+ $query_fields[':' . $cleanfield] = '%' . $valoper['value'] . '%';
+ }
+ } elseif (in_array($valoper['op'], $ops)) {
+ $condition .= $field . ' ' . $valoper['op'] . ':' . $cleanfield;
+ $query_fields[':' . $cleanfield] = $valoper['value'] ?? '';
+ } else {
+ continue;
+ }
+ if ($first) {
+ $first = false;
+ }
+ }
+ }
+ return $condition;
+ }
+
+ /**
+ * return LIMIT clause when at least $sql_limit parameter is given via API
+ *
+ * @param int $sql_limit
+ * optional, limit resultset, default 0
+ * @param int $sql_offset
+ * optional, offset for limitation, default 0
+ *
+ * @return string
+ */
+ protected function getLimit()
+ {
+ $limit = $this->getParam('sql_limit', true, 0);
+ $offset = $this->getParam('sql_offset', true, 0);
+
+ if (! is_numeric($limit)) {
+ $limit = 0;
+ }
+ if (! is_numeric($offset)) {
+ $offset = 0;
+ }
+
+ if ($limit > 0) {
+ return ' LIMIT ' . $offset . ',' . $limit;
+ }
+
+ return '';
+ }
+
+ /**
+ * return ORDER BY clause if parameter $sql_orderby parameter is given via API
+ *
+ * @param array $sql_orderby
+ * optional array with index = fieldname and value = ASC|DESC
+ * @param boolean $append
+ * optional append to ORDER BY clause rather then create new one, default false [internal]
+ *
+ * @return string
+ */
+ protected function getOrderBy($append = false)
+ {
+ $orderby = $this->getParam('sql_orderby', true, array());
+ $order = "";
+ if (! empty($orderby)) {
+ if ($append) {
+ $order .= ", ";
+ } else {
+ $order .= " ORDER BY ";
+ }
+
+ $nat_fields = [
+ '`c`.`loginname`',
+ '`a`.`loginname`',
+ '`adminname`',
+ '`databasename`',
+ '`username`'
+ ];
+
+ foreach ($orderby as $field => $by) {
+ $sortfield = explode('.', $field);
+ foreach ($sortfield as $id => $sfield) {
+ if (substr($sfield, - 1, 1) != '`') {
+ $sfield .= '`';
+ }
+ if ($sfield[0] != '`') {
+ $sfield = '`' . $sfield;
+ }
+ $sortfield[$id] = $sfield;
+ }
+ $field = implode('.', $sortfield);
+ $by = strtoupper($by);
+ if (! in_array($by, [
+ 'ASC',
+ 'DESC'
+ ])) {
+ $by = 'ASC';
+ }
+ if (\Froxlor\Settings::Get('panel.natsorting') == 1 && in_array($field, $nat_fields)) {
+ // Acts similar to php's natsort(), found in one comment at http://my.opera.com/cpr/blog/show.dml/160556
+ $order .= "CONCAT( IF( ASCII( LEFT( " . $field . ", 5 ) ) > 57,
+ LEFT( " . $field . ", 1 ), 0 ),
+ IF( ASCII( RIGHT( " . $field . ", 1 ) ) > 57,
+ LPAD( " . $field . ", 255, '0' ),
+ LPAD( CONCAT( " . $field . ", '-' ), 255, '0' )
+ )) " . $by . ", ";
+ } else {
+ $order .= $field . " " . $by . ", ";
+ }
+ }
+ $order = substr($order, 0, - 2);
+ }
+
+ return $order;
+ }
+
+ /**
+ * return logger instance
+ *
+ * @return \Froxlor\FroxlorLogger
+ */
+ protected function logger()
+ {
+ return $this->logger;
+ }
+
+ /**
+ * return mailer instance
+ *
+ * @return \Froxlor\System\Mailer
+ */
+ protected function mailer()
+ {
+ return $this->mail;
+ }
+
+ /**
+ * call an api-command internally
+ *
+ * @param string $command
+ * @param array|null $params
+ * @param boolean $internal
+ * optional whether called internally, default false
+ *
+ *
+ * @return array
+ */
+ protected function apiCall($command = null, $params = null, $internal = false)
+ {
+ $_command = explode(".", $command);
+ $module = __NAMESPACE__ . "\Commands\\" . $_command[0];
+ $function = $_command[1];
+ $json_result = $module::getLocal($this->getUserData(), $params, $internal)->{$function}();
+ return json_decode($json_result, true)['data'];
+ }
+
+ /**
+ * return api-compatible response in JSON format and send corresponding http-header
+ *
+ * @param int $status
+ * @param string $status_message
+ * @param mixed $data
+ *
+ * @return string json-encoded response message
+ */
+ protected function response($status, $status_message, $data = null)
+ {
+ if (isset($_SERVER["SERVER_PROTOCOL"]) && ! empty($_SERVER["SERVER_PROTOCOL"])) {
+ $resheader = $_SERVER["SERVER_PROTOCOL"] . " " . $status;
+ if (! empty($status_message)) {
+ $resheader .= ' ' . str_replace("\n", " ", $status_message);
+ }
+ header($resheader);
+ }
+
+ $response = array();
+ $response['status'] = $status;
+ $response['status_message'] = $status_message;
+ $response['data'] = $data;
+
+ $json_response = json_encode($response, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
+ return $json_response;
+ }
+
+ /**
+ * returns an array of customers the current user can access
+ *
+ * @param string $customer_hide_option
+ * optional, when called as customer, some options might be hidden due to the panel.customer_hide_options ettings
+ *
+ * @throws \Exception
+ * @return array
+ */
+ protected function getAllowedCustomerIds($customer_hide_option = '')
+ {
+ $customer_ids = array();
+ if ($this->isAdmin()) {
+ // if we're an admin, list all ftp-users of all the admins customers
+ // or optionally for one specific customer identified by id or loginname
+ $customerid = $this->getParam('customerid', true, 0);
+ $loginname = $this->getParam('loginname', true, '');
+
+ if (! empty($customerid) || ! empty($loginname)) {
+ $_result = $this->apiCall('Customers.get', array(
+ 'id' => $customerid,
+ 'loginname' => $loginname
+ ));
+ $custom_list_result = array(
+ $_result
+ );
+ } else {
+ $_custom_list_result = $this->apiCall('Customers.listing');
+ $custom_list_result = $_custom_list_result['list'];
+ }
+ foreach ($custom_list_result as $customer) {
+ $customer_ids[] = $customer['customerid'];
+ }
+ } else {
+ if (!$this->isInternal() && ! empty($customer_hide_option) && \Froxlor\Settings::IsInList('panel.customer_hide_options', $customer_hide_option)) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+ $customer_ids = array(
+ $this->getUserDetail('customerid')
+ );
+ }
+ if (empty($customer_ids)) {
+ throw new \Exception("Required resource unsatisfied.", 405);
+ }
+ return $customer_ids;
+ }
+
+ /**
+ * returns an array of customer data for customer, or by customer-id/loginname for admin/reseller
+ *
+ * @param int $customerid
+ * optional, required if loginname is empty
+ * @param string $loginname
+ * optional, required of customerid is empty
+ * @param string $customer_resource_check
+ * optional, when called as admin, check the resources of the target customer
+ *
+ * @throws \Exception
+ * @return array
+ */
+ protected function getCustomerData($customer_resource_check = '')
+ {
+ if ($this->isAdmin()) {
+ $customerid = $this->getParam('customerid', true, 0);
+ $loginname = $this->getParam('loginname', true, '');
+ $customer = $this->apiCall('Customers.get', array(
+ 'id' => $customerid,
+ 'loginname' => $loginname
+ ));
+ // check whether the customer has enough resources
+ if (! empty($customer_resource_check) && $customer[$customer_resource_check . '_used'] >= $customer[$customer_resource_check] && $customer[$customer_resource_check] != '-1') {
+ throw new \Exception("Customer has no more resources available", 406);
+ }
+ } else {
+ $customer = $this->getUserData();
+ }
+ return $customer;
+ }
+
+ /**
+ * increase/decrease a resource field for customers/admins
+ *
+ * @param string $table
+ * @param string $keyfield
+ * @param int $key
+ * @param string $operator
+ * @param string $resource
+ * @param string $extra
+ * @param int $step
+ */
+ protected static function updateResourceUsage($table = null, $keyfield = null, $key = null, $operator = '+', $resource = null, $extra = null, $step = 1)
+ {
+ $stmt = \Froxlor\Database\Database::prepare("
+ UPDATE `" . $table . "`
+ SET `" . $resource . "` = `" . $resource . "` " . $operator . " " . (int) $step . " " . $extra . "
+ WHERE `" . $keyfield . "` = :key
+ ");
+ \Froxlor\Database\Database::pexecute($stmt, array(
+ 'key' => $key
+ ), true, true);
+ }
+
+ /**
+ * return email template content from database or global language file if not found in DB
+ *
+ * @param array $customerdata
+ * @param string $group
+ * @param string $varname
+ * @param array $replace_arr
+ * @param string $default
+ *
+ * @return string
+ */
+ protected function getMailTemplate($customerdata = null, $group = null, $varname = null, $replace_arr = array(), $default = "")
+ {
+ // get template
+ $stmt = \Froxlor\Database\Database::prepare("
+ SELECT `value` FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `adminid`= :adminid
+ AND `language`= :lang AND `templategroup`= :group AND `varname`= :var
+ ");
+ $result = \Froxlor\Database\Database::pexecute_first($stmt, array(
+ "adminid" => $customerdata['adminid'],
+ "lang" => $customerdata['def_language'],
+ "group" => $group,
+ "var" => $varname
+ ), true, true);
+ $content = $default;
+ if ($result) {
+ $content = $result['value'] ?? $default;
+ }
+ // @fixme html_entity_decode
+ $content = html_entity_decode(\Froxlor\PhpHelper::replaceVariables($content, $replace_arr));
+ return $content;
+ }
+
+ /**
+ * read user data from database by api-request-header fields
+ *
+ * @param array $header
+ * api-request header
+ *
+ * @throws \Exception
+ * @return boolean
+ */
+ private function readUserData($header = null)
+ {
+ $sel_stmt = \Froxlor\Database\Database::prepare("SELECT * FROM `api_keys` WHERE `apikey` = :ak AND `secret` = :as");
+ $result = \Froxlor\Database\Database::pexecute_first($sel_stmt, array(
+ 'ak' => $header['apikey'],
+ 'as' => $header['secret']
+ ), true, true);
+ if ($result) {
+ // admin or customer?
+ if ($result['customerid'] == 0 && $result['adminid'] > 0) {
+ $this->is_admin = true;
+ $table = 'panel_admins';
+ $key = "adminid";
+ } elseif ($result['customerid'] > 0 && $result['adminid'] > 0) {
+ $this->is_admin = false;
+ $table = 'panel_customers';
+ $key = "customerid";
+ } else {
+ // neither adminid is > 0 nor customerid is > 0 - sorry man, no way
+ throw new \Exception("Invalid API credentials", 400);
+ }
+ $sel_stmt = \Froxlor\Database\Database::prepare("SELECT * FROM `" . $table . "` WHERE `" . $key . "` = :id");
+ $this->user_data = \Froxlor\Database\Database::pexecute_first($sel_stmt, array(
+ 'id' => ($this->is_admin ? $result['adminid'] : $result['customerid'])
+ ), true, true);
+ if ($this->is_admin) {
+ $this->user_data['adminsession'] = 1;
+ }
+ return true;
+ }
+ throw new \Exception("Invalid API credentials", 400);
+ }
+}
diff --git a/lib/Froxlor/Api/ApiParameter.php b/lib/Froxlor/Api/ApiParameter.php
new file mode 100644
index 00000000..6acb2309
--- /dev/null
+++ b/lib/Froxlor/Api/ApiParameter.php
@@ -0,0 +1,191 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+abstract class ApiParameter
+{
+
+ /**
+ * array of parameters passed to the command
+ *
+ * @var array
+ */
+ private $cmd_params = null;
+
+ /**
+ *
+ * @param array $params
+ * optional, array of parameters (var=>value) for the command
+ *
+ * @throws \Exception
+ */
+ public function __construct($params = null)
+ {
+ if (! is_null($params)) {
+ $params = $this->trimArray($params);
+ }
+ $this->cmd_params = $params;
+ }
+
+ /**
+ * get specific parameter from the parameterlist;
+ * check for existence and != empty if needed.
+ * Maybe more in the future
+ *
+ * @param string $param
+ * parameter to get out of the request-parameter list
+ * @param bool $optional
+ * default: false
+ * @param mixed $default
+ * value which is returned if optional=true and param is not set
+ *
+ * @throws \Exception
+ * @return mixed
+ */
+ protected function getParam($param = null, $optional = false, $default = '')
+ {
+ // does it exist?
+ if (! isset($this->cmd_params[$param])) {
+ if ($optional === false) {
+ // get module + function for better error-messages
+ $inmod = $this->getModFunctionString();
+ throw new \Exception('Requested parameter "' . $param . '" could not be found for "' . $inmod . '"', 404);
+ }
+ return $default;
+ }
+ // is it empty? - test really on string, as value 0 is being seen as empty by php
+ if ($this->cmd_params[$param] === "") {
+ if ($optional === false) {
+ // get module + function for better error-messages
+ $inmod = $this->getModFunctionString();
+ throw new \Exception('Requested parameter "' . $param . '" is empty where it should not be for "' . $inmod . '"', 406);
+ }
+ return '';
+ }
+ // everything else is fine
+ return $this->cmd_params[$param];
+ }
+
+ /**
+ * getParam wrapper for boolean parameter
+ *
+ * @param string $param
+ * parameter to get out of the request-parameter list
+ * @param bool $optional
+ * default: false
+ * @param mixed $default
+ * value which is returned if optional=true and param is not set
+ *
+ * @return string
+ */
+ protected function getBoolParam($param = null, $optional = false, $default = false)
+ {
+ $_default = '0';
+ if ($default) {
+ $_default = '1';
+ }
+ $param_value = $this->getParam($param, $optional, $_default);
+ if ($param_value && intval($param_value) != 0) {
+ return '1';
+ }
+ return '0';
+ }
+
+ /**
+ * get specific parameter which also has and unlimited-field
+ *
+ * @param string $param
+ * parameter to get out of the request-parameter list
+ * @param string $ul_field
+ * parameter to get out of the request-parameter list
+ * @param bool $optional
+ * default: false
+ * @param mixed $default
+ * value which is returned if optional=true and param is not set
+ *
+ * @return mixed
+ * @throws \Exception
+ */
+ protected function getUlParam($param = null, $ul_field = null, $optional = false, $default = 0)
+ {
+ $param_value = (int) $this->getParam($param, $optional, $default);
+ $ul_field_value = $this->getBoolParam($ul_field, true, 0);
+ if ($ul_field_value != '0') {
+ $param_value = - 1;
+ }
+ return $param_value;
+ }
+
+ /**
+ * return list of all parameters
+ *
+ * @return array
+ */
+ protected function getParamList()
+ {
+ return $this->cmd_params;
+ }
+
+ /**
+ * returns "module::function()" for better error-messages (missing parameter etc.)
+ * makes debugging a whole lot more comfortable
+ *
+ * @param int $level
+ * depth of backtrace, default 2
+ *
+ * @param int $max_level
+ * @param array|null $trace
+ *
+ * @return string
+ */
+ private function getModFunctionString($level = 1, $max_level = 5, $trace = null)
+ {
+ // which class called us
+ $_class = get_called_class();
+ if (empty($trace)) {
+ // get backtrace
+ $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
+ }
+ // check class and function
+ $class = $trace[$level]['class'];
+ $func = $trace[$level]['function'];
+ // is it the one we are looking for?
+ if ($class != $_class && $level <= $max_level) {
+ // check one level deeper
+ return $this->getModFunctionString(++ $level, $max_level, $trace);
+ }
+ return str_replace("Froxlor\\Api\\Commands\\", "", $class) . ':' . $func;
+ }
+
+ /**
+ * run 'trim' function on an array recursively
+ *
+ * @param array $input
+ *
+ * @return array
+ */
+ private function trimArray($input)
+ {
+ if (! is_array($input)) {
+ return trim($input);
+ }
+ return array_map(array(
+ $this,
+ 'trimArray'
+ ), $input);
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/Admins.php b/lib/Froxlor/Api/Commands/Admins.php
new file mode 100644
index 00000000..747edfc9
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/Admins.php
@@ -0,0 +1,854 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class Admins extends \Froxlor\Api\ApiCommand implements \Froxlor\Api\ResourceEntity
+{
+
+ /**
+ * lists all admin entries
+ *
+ * @param array $sql_search
+ * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), LIKE is used if left empty and 'value' => searchvalue
+ * @param int $sql_limit
+ * optional specify number of results to be returned
+ * @param int $sql_offset
+ * optional specify offset for resultset
+ * @param array $sql_orderby
+ * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more fields
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listing()
+ {
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) {
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] list admins");
+ $query_fields = array();
+ $result_stmt = Database::prepare("
+ SELECT *
+ FROM `" . TABLE_PANEL_ADMINS . "`" . $this->getSearchWhere($query_fields) . $this->getOrderBy() . $this->getLimit());
+ Database::pexecute($result_stmt, $query_fields, true, true);
+ $result = array();
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = $row;
+ }
+ return $this->response(200, "successful", array(
+ 'count' => count($result),
+ 'list' => $result
+ ));
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * returns the total number of admins for the given admin
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function listingCount()
+ {
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) {
+ $result_stmt = Database::prepare("
+ SELECT COUNT(*) as num_admins
+ FROM `" . TABLE_PANEL_ADMINS . "`
+ ");
+ $result = Database::pexecute_first($result_stmt, null, true, true);
+ if ($result) {
+ return $this->response(200, "successful", $result['num_admins']);
+ }
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * return an admin entry by either id or loginname
+ *
+ * @param int $id
+ * optional, the admin-id
+ * @param string $loginname
+ * optional, the loginname
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function get()
+ {
+ $id = $this->getParam('id', true, 0);
+ $ln_optional = ($id <= 0 ? false : true);
+ $loginname = $this->getParam('loginname', $ln_optional, '');
+
+ if ($this->isAdmin() && ($this->getUserDetail('change_serversettings') == 1 || ($this->getUserDetail('adminid') == $id || $this->getUserDetail('loginname') == $loginname))) {
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_ADMINS . "`
+ WHERE " . ($id > 0 ? "`adminid` = :idln" : "`loginname` = :idln"));
+ $params = array(
+ 'idln' => ($id <= 0 ? $loginname : $id)
+ );
+ $result = Database::pexecute_first($result_stmt, $params, true, true);
+ if ($result) {
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] get admin '" . $result['loginname'] . "'");
+ return $this->response(200, "successful", $result);
+ }
+ $key = ($id > 0 ? "id #" . $id : "loginname '" . $loginname . "'");
+ throw new \Exception("Admin with " . $key . " could not be found", 404);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * create a new admin user
+ *
+ * @param string $name
+ * @param string $email
+ * @param string $new_loginname
+ * @param string $admin_password
+ * optional, default auto-generated
+ * @param string $def_language
+ * optional, default is system-default language
+ * @param bool $api_allowed
+ * optional, default is true if system setting api.enabled is true, else false
+ * @param string $custom_notes
+ * optional, default empty
+ * @param bool $custom_notes_show
+ * optional, default false
+ * @param int $diskspace
+ * optional, default 0
+ * @param bool $diskspace_ul
+ * optional, default false
+ * @param int $traffic
+ * optional, default 0
+ * @param bool $traffic_ul
+ * optional, default false
+ * @param int $customers
+ * optional, default 0
+ * @param bool $customers_ul
+ * optional, default false
+ * @param int $domains
+ * optional, default 0
+ * @param bool $domains_ul
+ * optional, default false
+ * @param int $subdomains
+ * optional, default 0
+ * @param bool $subdomains_ul
+ * optional, default false
+ * @param int $emails
+ * optional, default 0
+ * @param bool $emails_ul
+ * optional, default false
+ * @param int $email_accounts
+ * optional, default 0
+ * @param bool $email_accounts_ul
+ * optional, default false
+ * @param int $email_forwarders
+ * optional, default 0
+ * @param bool $email_forwarders_ul
+ * optional, default false
+ * @param int $email_quota
+ * optional, default 0
+ * @param bool $email_quota_ul
+ * optional, default false
+ * @param int $ftps
+ * optional, default 0
+ * @param bool $ftps_ul
+ * optional, default false
+ * @param int $mysqls
+ * optional, default 0
+ * @param bool $mysqls_ul
+ * optional, default false
+ * @param bool $customers_see_all
+ * optional, default false
+ * @param bool $domains_see_all
+ * optional, default false
+ * @param bool $caneditphpsettings
+ * optional, default false
+ * @param bool $change_serversettings
+ * optional, default false
+ * @param array $ipaddress
+ * optional, list of ip-address id's; default -1 (all IP's)
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function add()
+ {
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) {
+
+ // required parameters
+ $name = $this->getParam('name');
+ $email = $this->getParam('email');
+ $loginname = $this->getParam('new_loginname');
+
+ // parameters
+ $def_language = $this->getParam('def_language', true, Settings::Get('panel.standardlanguage'));
+ $api_allowed = $this->getBoolParam('api_allowed', true, Settings::Get('api.enabled'));
+ $custom_notes = $this->getParam('custom_notes', true, '');
+ $custom_notes_show = $this->getBoolParam('custom_notes_show', true, 0);
+ $password = $this->getParam('admin_password', true, '');
+
+ $diskspace = $this->getUlParam('diskspace', 'diskspace_ul', true, 0);
+ $traffic = $this->getUlParam('traffic', 'traffic_ul', true, 0);
+ $customers = $this->getUlParam('customers', 'customers_ul', true, 0);
+ $domains = $this->getUlParam('domains', 'domains_ul', true, 0);
+ $subdomains = $this->getUlParam('subdomains', 'subdomains_ul', true, 0);
+ $emails = $this->getUlParam('emails', 'emails_ul', true, 0);
+ $email_accounts = $this->getUlParam('email_accounts', 'email_accounts_ul', true, 0);
+ $email_forwarders = $this->getUlParam('email_forwarders', 'email_forwarders_ul', true, 0);
+ $email_quota = $this->getUlParam('email_quota', 'email_quota_ul', true, 0);
+ $ftps = $this->getUlParam('ftps', 'ftps_ul', true, 0);
+ $mysqls = $this->getUlParam('mysqls', 'mysqls_ul', true, 0);
+
+ $customers_see_all = $this->getBoolParam('customers_see_all', true, 0);
+ $domains_see_all = $this->getBoolParam('domains_see_all', true, 0);
+ $caneditphpsettings = $this->getBoolParam('caneditphpsettings', true, 0);
+ $change_serversettings = $this->getBoolParam('change_serversettings', true, 0);
+ $ipaddress = $this->getParam('ipaddress', true, - 1);
+
+ // validation
+ $name = \Froxlor\Validate\Validate::validate($name, 'name', '', '', array(), true);
+ $idna_convert = new \Froxlor\Idna\IdnaWrapper();
+ $email = $idna_convert->encode(\Froxlor\Validate\Validate::validate($email, 'email', '', '', array(), true));
+ $def_language = \Froxlor\Validate\Validate::validate($def_language, 'default language', '', '', array(), true);
+ $custom_notes = \Froxlor\Validate\Validate::validate(str_replace("\r\n", "\n", $custom_notes), 'custom_notes', \Froxlor\Validate\Validate::REGEX_CONF_TEXT, '', array(), true);
+
+ if (Settings::Get('system.mail_quota_enabled') != '1') {
+ $email_quota = - 1;
+ }
+
+ $password = \Froxlor\Validate\Validate::validate($password, 'password', '', '', array(), true);
+ // only check if not empty,
+ // cause empty == generate password automatically
+ if ($password != '') {
+ $password = \Froxlor\System\Crypt::validatePassword($password, true);
+ }
+
+ $diskspace = $diskspace * 1024;
+ $traffic = $traffic * 1024 * 1024;
+
+ // Check if the account already exists
+ // do not check via api as we skip any permission checks for this task
+ $loginname_check_stmt = Database::prepare("
+ SELECT `loginname` FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `loginname` = :login
+ ");
+ $loginname_check = Database::pexecute_first($loginname_check_stmt, array(
+ 'login' => $loginname
+ ), true, true);
+
+ // Check if an admin with the loginname already exists
+ // do not check via api as we skip any permission checks for this task
+ $loginname_check_admin_stmt = Database::prepare("
+ SELECT `loginname` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `loginname` = :login
+ ");
+ $loginname_check_admin = Database::pexecute_first($loginname_check_admin_stmt, array(
+ 'login' => $loginname
+ ), true, true);
+
+ if (($loginname_check && strtolower($loginname_check['loginname']) == strtolower($loginname)) || ($loginname_check_admin && strtolower($loginname_check_admin['loginname']) == strtolower($loginname))) {
+ \Froxlor\UI\Response::standard_error('loginnameexists', $loginname, true);
+ } elseif (preg_match('/^' . preg_quote(Settings::Get('customer.accountprefix'), '/') . '([0-9]+)/', $loginname)) {
+ // Accounts which match systemaccounts are not allowed, filtering them
+ \Froxlor\UI\Response::standard_error('loginnameissystemaccount', Settings::Get('customer.accountprefix'), true);
+ } elseif (! \Froxlor\Validate\Validate::validateUsername($loginname)) {
+ \Froxlor\UI\Response::standard_error('loginnameiswrong', $loginname, true);
+ } elseif (! \Froxlor\Validate\Validate::validateEmail($email)) {
+ \Froxlor\UI\Response::standard_error('emailiswrong', $email, true);
+ } else {
+
+ if ($customers_see_all != '1') {
+ $customers_see_all = '0';
+ }
+
+ if ($domains_see_all != '1') {
+ $domains_see_all = '0';
+ }
+
+ if ($caneditphpsettings != '1') {
+ $caneditphpsettings = '0';
+ }
+
+ if ($change_serversettings != '1') {
+ $change_serversettings = '0';
+ }
+
+ if ($password == '') {
+ $password = \Froxlor\System\Crypt::generatePassword();
+ }
+
+ $_theme = Settings::Get('panel.default_theme');
+
+ $ins_data = array(
+ 'loginname' => $loginname,
+ 'password' => \Froxlor\System\Crypt::makeCryptPassword($password),
+ 'name' => $name,
+ 'email' => $email,
+ 'lang' => $def_language,
+ 'api_allowed' => $api_allowed,
+ 'change_serversettings' => $change_serversettings,
+ 'customers' => $customers,
+ 'customers_see_all' => $customers_see_all,
+ 'domains' => $domains,
+ 'domains_see_all' => $domains_see_all,
+ 'caneditphpsettings' => $caneditphpsettings,
+ 'diskspace' => $diskspace,
+ 'traffic' => $traffic,
+ 'subdomains' => $subdomains,
+ 'emails' => $emails,
+ 'accounts' => $email_accounts,
+ 'forwarders' => $email_forwarders,
+ 'quota' => $email_quota,
+ 'ftps' => $ftps,
+ 'mysqls' => $mysqls,
+ 'ip' => empty($ipaddress) ? "" : (is_array($ipaddress) && $ipaddress > 0 ? json_encode($ipaddress) : - 1),
+ 'theme' => $_theme,
+ 'custom_notes' => $custom_notes,
+ 'custom_notes_show' => $custom_notes_show
+ );
+
+ $ins_stmt = Database::prepare("
+ INSERT INTO `" . TABLE_PANEL_ADMINS . "` SET
+ `loginname` = :loginname,
+ `password` = :password,
+ `name` = :name,
+ `email` = :email,
+ `def_language` = :lang,
+ `api_allowed` = :api_allowed,
+ `change_serversettings` = :change_serversettings,
+ `customers` = :customers,
+ `customers_see_all` = :customers_see_all,
+ `domains` = :domains,
+ `domains_see_all` = :domains_see_all,
+ `caneditphpsettings` = :caneditphpsettings,
+ `diskspace` = :diskspace,
+ `traffic` = :traffic,
+ `subdomains` = :subdomains,
+ `emails` = :emails,
+ `email_accounts` = :accounts,
+ `email_forwarders` = :forwarders,
+ `email_quota` = :quota,
+ `ftps` = :ftps,
+ `mysqls` = :mysqls,
+ `ip` = :ip,
+ `theme` = :theme,
+ `custom_notes` = :custom_notes,
+ `custom_notes_show` = :custom_notes_show
+ ");
+ Database::pexecute($ins_stmt, $ins_data, true, true);
+
+ $adminid = Database::lastInsertId();
+ $ins_data['adminid'] = $adminid;
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] added admin '" . $loginname . "'");
+
+ // get all admin-data for return-array
+ $result = $this->apiCall('Admins.get', array(
+ 'id' => $adminid
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * update an admin user by given id or loginname
+ *
+ * @param int $id
+ * optional, the admin-id
+ * @param string $loginname
+ * optional, the loginname
+ * @param string $name
+ * optional
+ * @param string $email
+ * optional
+ * @param string $admin_password
+ * optional, default auto-generated
+ * @param string $def_language
+ * optional, default is system-default language
+ * @param bool $api_allowed
+ * optional, default is true if system setting api.enabled is true, else false
+ * @param string $custom_notes
+ * optional, default empty
+ * @param string $theme
+ * optional
+ * @param bool $deactivated
+ * optional, default false
+ * @param bool $custom_notes_show
+ * optional, default false
+ * @param int $diskspace
+ * optional, default 0
+ * @param bool $diskspace_ul
+ * optional, default false
+ * @param int $traffic
+ * optional, default 0
+ * @param bool $traffic_ul
+ * optional, default false
+ * @param int $customers
+ * optional, default 0
+ * @param bool $customers_ul
+ * optional, default false
+ * @param int $domains
+ * optional, default 0
+ * @param bool $domains_ul
+ * optional, default false
+ * @param int $subdomains
+ * optional, default 0
+ * @param bool $subdomains_ul
+ * optional, default false
+ * @param int $emails
+ * optional, default 0
+ * @param bool $emails_ul
+ * optional, default false
+ * @param int $email_accounts
+ * optional, default 0
+ * @param bool $email_accounts_ul
+ * optional, default false
+ * @param int $email_forwarders
+ * optional, default 0
+ * @param bool $email_forwarders_ul
+ * optional, default false
+ * @param int $email_quota
+ * optional, default 0
+ * @param bool $email_quota_ul
+ * optional, default false
+ * @param int $ftps
+ * optional, default 0
+ * @param bool $ftps_ul
+ * optional, default false
+ * @param int $mysqls
+ * optional, default 0
+ * @param bool $mysqls_ul
+ * optional, default false
+ * @param bool $customers_see_all
+ * optional, default false
+ * @param bool $domains_see_all
+ * optional, default false
+ * @param bool $caneditphpsettings
+ * optional, default false
+ * @param bool $change_serversettings
+ * optional, default false
+ * @param array $ipaddress
+ * optional, list of ip-address id's; default -1 (all IP's)
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function update()
+ {
+ if ($this->isAdmin()) {
+
+ $id = $this->getParam('id', true, 0);
+ $ln_optional = ($id <= 0 ? false : true);
+ $loginname = $this->getParam('loginname', $ln_optional, '');
+
+ $result = $this->apiCall('Admins.get', array(
+ 'id' => $id,
+ 'loginname' => $loginname
+ ));
+ $id = $result['adminid'];
+
+ if ($this->getUserDetail('change_serversettings') == 1 || $result['adminid'] == $this->getUserDetail('adminid')) {
+ // parameters
+ $name = $this->getParam('name', true, $result['name']);
+ $idna_convert = new \Froxlor\Idna\IdnaWrapper();
+ $email = $this->getParam('email', true, $idna_convert->decode($result['email']));
+ $password = $this->getParam('admin_password', true, '');
+ $def_language = $this->getParam('def_language', true, $result['def_language']);
+ $custom_notes = $this->getParam('custom_notes', true, $result['custom_notes']);
+ $custom_notes_show = $this->getBoolParam('custom_notes_show', true, $result['custom_notes_show']);
+ $theme = $this->getParam('theme', true, $result['theme']);
+
+ // you cannot edit some of the details of yourself
+ if ($result['adminid'] == $this->getUserDetail('adminid')) {
+ $api_allowed = $result['api_allowed'];
+ $deactivated = $result['deactivated'];
+ $customers = $result['customers'];
+ $domains = $result['domains'];
+ $subdomains = $result['subdomains'];
+ $emails = $result['emails'];
+ $email_accounts = $result['email_accounts'];
+ $email_forwarders = $result['email_forwarders'];
+ $email_quota = $result['email_quota'];
+ $ftps = $result['ftps'];
+ $mysqls = $result['mysqls'];
+ $customers_see_all = $result['customers_see_all'];
+ $domains_see_all = $result['domains_see_all'];
+ $caneditphpsettings = $result['caneditphpsettings'];
+ $change_serversettings = $result['change_serversettings'];
+ $diskspace = $result['diskspace'];
+ $traffic = $result['traffic'];
+ $ipaddress = ($result['ip'] != - 1 ? json_decode($result['ip'], true) : - 1);
+ } else {
+ $api_allowed = $this->getBoolParam('api_allowed', true, $result['api_allowed']);
+ $deactivated = $this->getBoolParam('deactivated', true, $result['deactivated']);
+
+ $dec_places = Settings::Get('panel.decimal_places');
+ $diskspace = $this->getUlParam('diskspace', 'diskspace_ul', true, round($result['diskspace'] / 1024, $dec_places));
+ $traffic = $this->getUlParam('traffic', 'traffic_ul', true, round($result['traffic'] / (1024 * 1024), $dec_places));
+ $customers = $this->getUlParam('customers', 'customers_ul', true, $result['customers']);
+ $domains = $this->getUlParam('domains', 'domains_ul', true, $result['domains']);
+ $subdomains = $this->getUlParam('subdomains', 'subdomains_ul', true, $result['subdomains']);
+ $emails = $this->getUlParam('emails', 'emails_ul', true, $result['emails']);
+ $email_accounts = $this->getUlParam('email_accounts', 'email_accounts_ul', true, $result['email_accounts']);
+ $email_forwarders = $this->getUlParam('email_forwarders', 'email_forwarders_ul', true, $result['email_forwarders']);
+ $email_quota = $this->getUlParam('email_quota', 'email_quota_ul', true, $result['email_quota']);
+ $ftps = $this->getUlParam('ftps', 'ftps_ul', true, $result['ftps']);
+ $mysqls = $this->getUlParam('mysqls', 'mysqls_ul', true, $result['mysqls']);
+
+ $customers_see_all = $this->getBoolParam('customers_see_all', true, $result['customers_see_all']);
+ $domains_see_all = $this->getBoolParam('domains_see_all', true, $result['domains_see_all']);
+ $caneditphpsettings = $this->getBoolParam('caneditphpsettings', true, $result['caneditphpsettings']);
+ $change_serversettings = $this->getBoolParam('change_serversettings', true, $result['change_serversettings']);
+ $ipaddress = $this->getParam('ipaddress', true, ($result['ip'] != - 1 ? json_decode($result['ip'], true) : - 1));
+
+ $diskspace = $diskspace * 1024;
+ $traffic = $traffic * 1024 * 1024;
+ }
+
+ // validation
+ $name = \Froxlor\Validate\Validate::validate($name, 'name', '', '', array(), true);
+ $idna_convert = new \Froxlor\Idna\IdnaWrapper();
+ $email = $idna_convert->encode(\Froxlor\Validate\Validate::validate($email, 'email', '', '', array(), true));
+ $def_language = \Froxlor\Validate\Validate::validate($def_language, 'default language', '', '', array(), true);
+ $custom_notes = \Froxlor\Validate\Validate::validate(str_replace("\r\n", "\n", $custom_notes), 'custom_notes', \Froxlor\Validate\Validate::REGEX_CONF_TEXT, '', array(), true);
+ $theme = \Froxlor\Validate\Validate::validate($theme, 'theme', '', '', array(), true);
+ $password = \Froxlor\Validate\Validate::validate($password, 'password', '', '', array(), true);
+
+ if (Settings::Get('system.mail_quota_enabled') != '1') {
+ $email_quota = - 1;
+ }
+
+ if (empty($theme)) {
+ $theme = Settings::Get('panel.default_theme');
+ }
+
+ if (! \Froxlor\Validate\Validate::validateEmail($email)) {
+ \Froxlor\UI\Response::standard_error('emailiswrong', $email, true);
+ } else {
+
+ if ($deactivated != '1') {
+ $deactivated = '0';
+ }
+
+ if ($customers_see_all != '1') {
+ $customers_see_all = '0';
+ }
+
+ if ($domains_see_all != '1') {
+ $domains_see_all = '0';
+ }
+
+ if ($caneditphpsettings != '1') {
+ $caneditphpsettings = '0';
+ }
+
+ if ($change_serversettings != '1') {
+ $change_serversettings = '0';
+ }
+
+ if ($password != '') {
+ $password = \Froxlor\System\Crypt::validatePassword($password, true);
+ $password = \Froxlor\System\Crypt::makeCryptPassword($password);
+ } else {
+ $password = $result['password'];
+ }
+
+ // check if a resource was set to something lower
+ // than actually used by the admin/reseller
+ $res_warning = "";
+ if ($customers != $result['customers'] && $customers != - 1 && $customers < $result['customers_used']) {
+ $res_warning .= sprintf($this->lng['error']['setlessthanalreadyused'], 'customers');
+ }
+ if ($domains != $result['domains'] && $domains != - 1 && $domains < $result['domains_used']) {
+ $res_warning .= sprintf($this->lng['error']['setlessthanalreadyused'], 'domains');
+ }
+ if ($diskspace != $result['diskspace'] && ($diskspace / 1024) != - 1 && $diskspace < $result['diskspace_used']) {
+ $res_warning .= sprintf($this->lng['error']['setlessthanalreadyused'], 'diskspace');
+ }
+ if ($traffic != $result['traffic'] && ($traffic / 1024 / 1024) != - 1 && $traffic < $result['traffic_used']) {
+ $res_warning .= sprintf($this->lng['error']['setlessthanalreadyused'], 'traffic');
+ }
+ if ($emails != $result['emails'] && $emails != - 1 && $emails < $result['emails_used']) {
+ $res_warning .= sprintf($this->lng['error']['setlessthanalreadyused'], 'emails');
+ }
+ if ($email_accounts != $result['email_accounts'] && $email_accounts != - 1 && $email_accounts < $result['email_accounts_used']) {
+ $res_warning .= sprintf($this->lng['error']['setlessthanalreadyused'], 'email accounts');
+ }
+ if ($email_forwarders != $result['email_forwarders'] && $email_forwarders != - 1 && $email_forwarders < $result['email_forwarders_used']) {
+ $res_warning .= sprintf($this->lng['error']['setlessthanalreadyused'], 'email forwarders');
+ }
+ if ($email_quota != $result['email_quota'] && $email_quota != - 1 && $email_quota < $result['email_quota_used']) {
+ $res_warning .= sprintf($this->lng['error']['setlessthanalreadyused'], 'email quota');
+ }
+ if ($ftps != $result['ftps'] && $ftps != - 1 && $ftps < $result['ftps_used']) {
+ $res_warning .= sprintf($this->lng['error']['setlessthanalreadyused'], 'ftps');
+ }
+ if ($mysqls != $result['mysqls'] && $mysqls != - 1 && $mysqls < $result['mysqls_used']) {
+ $res_warning .= sprintf($this->lng['error']['setlessthanalreadyused'], 'mysqls');
+ }
+
+ if (! empty($res_warning)) {
+ throw new \Exception($res_warning, 406);
+ }
+
+ $upd_data = array(
+ 'password' => $password,
+ 'name' => $name,
+ 'email' => $email,
+ 'lang' => $def_language,
+ 'api_allowed' => $api_allowed,
+ 'change_serversettings' => $change_serversettings,
+ 'customers' => $customers,
+ 'customers_see_all' => $customers_see_all,
+ 'domains' => $domains,
+ 'domains_see_all' => $domains_see_all,
+ 'caneditphpsettings' => $caneditphpsettings,
+ 'diskspace' => $diskspace,
+ 'traffic' => $traffic,
+ 'subdomains' => $subdomains,
+ 'emails' => $emails,
+ 'accounts' => $email_accounts,
+ 'forwarders' => $email_forwarders,
+ 'quota' => $email_quota,
+ 'ftps' => $ftps,
+ 'mysqls' => $mysqls,
+ 'ip' => empty($ipaddress) ? "" : (is_array($ipaddress) && $ipaddress > 0 ? json_encode($ipaddress) : - 1),
+ 'deactivated' => $deactivated,
+ 'custom_notes' => $custom_notes,
+ 'custom_notes_show' => $custom_notes_show,
+ 'theme' => $theme,
+ 'adminid' => $id
+ );
+
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_ADMINS . "` SET
+ `password` = :password,
+ `name` = :name,
+ `email` = :email,
+ `def_language` = :lang,
+ `api_allowed` = :api_allowed,
+ `change_serversettings` = :change_serversettings,
+ `customers` = :customers,
+ `customers_see_all` = :customers_see_all,
+ `domains` = :domains,
+ `domains_see_all` = :domains_see_all,
+ `caneditphpsettings` = :caneditphpsettings,
+ `diskspace` = :diskspace,
+ `traffic` = :traffic,
+ `subdomains` = :subdomains,
+ `emails` = :emails,
+ `email_accounts` = :accounts,
+ `email_forwarders` = :forwarders,
+ `email_quota` = :quota,
+ `ftps` = :ftps,
+ `mysqls` = :mysqls,
+ `ip` = :ip,
+ `deactivated` = :deactivated,
+ `custom_notes` = :custom_notes,
+ `custom_notes_show` = :custom_notes_show,
+ `theme` = :theme
+ WHERE `adminid` = :adminid
+ ");
+ Database::pexecute($upd_stmt, $upd_data, true, true);
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] edited admin '" . $result['loginname'] . "'");
+
+ // get all admin-data for return-array
+ $result = $this->apiCall('Admins.get', array(
+ 'id' => $result['adminid']
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ }
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * delete a admin entry by either id or loginname
+ *
+ * @param int $id
+ * optional, the admin-id
+ * @param string $loginname
+ * optional, the loginname
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function delete()
+ {
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) {
+ $id = $this->getParam('id', true, 0);
+ $ln_optional = ($id <= 0 ? false : true);
+ $loginname = $this->getParam('loginname', $ln_optional, '');
+
+ $result = $this->apiCall('Admins.get', array(
+ 'id' => $id,
+ 'loginname' => $loginname
+ ));
+ $id = $result['adminid'];
+
+ // don't be stupid
+ if ($id == $this->getUserDetail('adminid')) {
+ \Froxlor\UI\Response::standard_error('youcantdeleteyourself', '', true);
+ }
+ // can't delete the first superadmin
+ if ($id == 1) {
+ \Froxlor\UI\Response::standard_error('cannotdeletesuperadmin', '', true);
+ }
+
+ // delete admin
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_PANEL_ADMINS . "` WHERE `adminid` = :adminid
+ ");
+ Database::pexecute($del_stmt, array(
+ 'adminid' => $id
+ ), true, true);
+
+ // delete the traffic-usage
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_PANEL_TRAFFIC_ADMINS . "` WHERE `adminid` = :adminid
+ ");
+ Database::pexecute($del_stmt, array(
+ 'adminid' => $id
+ ), true, true);
+
+ // set admin-id of the old admin's customer to current admins
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET
+ `adminid` = :userid WHERE `adminid` = :adminid
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'userid' => $this->getUserDetail('adminid'),
+ 'adminid' => $id
+ ), true, true);
+
+ // set admin-id of the old admin's domains to current admins
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_DOMAINS . "` SET
+ `adminid` = :userid WHERE `adminid` = :adminid
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'userid' => $this->getUserDetail('adminid'),
+ 'adminid' => $id
+ ), true, true);
+
+ // delete old admin's api keys if exists (no customer keys)
+ $upd_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_API_KEYS . "` WHERE
+ `adminid` = :adminid AND `customerid` = '0'
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'adminid' => $id
+ ), true, true);
+
+ // set admin-id of the old admin's api-keys to current admins
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_API_KEYS . "` SET
+ `adminid` = :userid WHERE `adminid` = :adminid
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'userid' => $this->getUserDetail('adminid'),
+ 'adminid' => $id
+ ), true, true);
+
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] deleted admin '" . $result['loginname'] . "'");
+ \Froxlor\User::updateCounters();
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * unlock a locked admin by either id or loginname
+ *
+ * @param int $id
+ * optional, the admin-id
+ * @param string $loginname
+ * optional, the loginname
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function unlock()
+ {
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) {
+ $id = $this->getParam('id', true, 0);
+ $ln_optional = ($id <= 0 ? false : true);
+ $loginname = $this->getParam('loginname', $ln_optional, '');
+
+ $result = $this->apiCall('Admins.get', array(
+ 'id' => $id,
+ 'loginname' => $loginname
+ ));
+ $id = $result['adminid'];
+
+ $result_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_ADMINS . "` SET
+ `loginfail_count` = '0'
+ WHERE `adminid`= :id
+ ");
+ Database::pexecute($result_stmt, array(
+ 'id' => $id
+ ), true, true);
+ // set the new value for result-array
+ $result['loginfail_count'] = 0;
+
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] unlocked admin '" . $result['loginname'] . "'");
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * increase resource-usage
+ *
+ * @param int $adminid
+ * @param string $resource
+ * @param string $extra
+ * optional, default empty
+ * @param int $increase_by
+ * optional, default 1
+ */
+ public static function increaseUsage($adminid = 0, $resource = null, $extra = '', $increase_by = 1)
+ {
+ self::updateResourceUsage(TABLE_PANEL_ADMINS, 'adminid', $adminid, '+', $resource, $extra, $increase_by);
+ }
+
+ /**
+ * decrease resource-usage
+ *
+ * @param int $adminid
+ * @param string $resource
+ * @param string $extra
+ * optional, default empty
+ * @param int $decrease_by
+ * optional, default 1
+ */
+ public static function decreaseUsage($adminid = 0, $resource = null, $extra = '', $decrease_by = 1)
+ {
+ self::updateResourceUsage(TABLE_PANEL_ADMINS, 'adminid', $adminid, '-', $resource, $extra, $decrease_by);
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/ApiKeys.php b/lib/Froxlor/Api/Commands/ApiKeys.php
new file mode 100644
index 00000000..971d92d3
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/ApiKeys.php
@@ -0,0 +1,30 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class ApiKeys extends \Froxlor\Api\ApiCommand
+{
+
+ public function listing()
+ {}
+
+ public function listingCount()
+ {}
+}
\ No newline at end of file
diff --git a/lib/Froxlor/Api/Commands/Certificates.php b/lib/Froxlor/Api/Commands/Certificates.php
new file mode 100644
index 00000000..ee79ab74
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/Certificates.php
@@ -0,0 +1,427 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class Certificates extends \Froxlor\Api\ApiCommand implements \Froxlor\Api\ResourceEntity
+{
+
+ /**
+ * add new ssl-certificate entry for given domain by either id or domainname
+ *
+ * @param int $id
+ * optional, the domain-id
+ * @param string $domainname
+ * optional, the domainname
+ * @param string $ssl_cert_file
+ * @param string $ssl_key_file
+ * @param string $ssl_ca_file
+ * optional
+ * @param string $ssl_cert_chainfile
+ * optional
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function add()
+ {
+ $domainid = $this->getParam('domainid', true, 0);
+ $dn_optional = ($domainid <= 0 ? false : true);
+ $domainname = $this->getParam('domainname', $dn_optional, '');
+
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'domains')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ $domain = $this->apiCall('SubDomains.get', array(
+ 'id' => $domainid,
+ 'domainname' => $domainname
+ ));
+ $domainid = $domain['id'];
+
+ // parameters
+ $ssl_cert_file = $this->getParam('ssl_cert_file');
+ $ssl_key_file = $this->getParam('ssl_key_file');
+ $ssl_ca_file = $this->getParam('ssl_ca_file', true, '');
+ $ssl_cert_chainfile = $this->getParam('ssl_cert_chainfile', true, '');
+
+ // validate whether the domain does not already have an entry
+ $has_cert = true;
+ try {
+ $this->apiCall('Certificates.get', array(
+ 'id' => $domainid
+ ));
+ } catch (\Exception $e) {
+ if ($e->getCode() == 412) {
+ $has_cert = false;
+ } else {
+ throw $e;
+ }
+ }
+ if (! $has_cert) {
+ $this->addOrUpdateCertificate($domain['id'], $ssl_cert_file, $ssl_key_file, $ssl_ca_file, $ssl_cert_chainfile, true);
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] added ssl-certificate for '" . $domain['domain'] . "'");
+ $result = $this->apiCall('Certificates.get', array(
+ 'id' => $domain['id']
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("Domain '" . $domain['domain'] . "' already has a certificate. Did you mean to call update?", 406);
+ }
+
+ /**
+ * return ssl-certificate entry for given domain by either id or domainname
+ *
+ * @param int $id
+ * optional, the domain-id
+ * @param string $domainname
+ * optional, the domainname
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function get()
+ {
+ $id = $this->getParam('id', true, 0);
+ $dn_optional = ($id <= 0 ? false : true);
+ $domainname = $this->getParam('domainname', $dn_optional, '');
+
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'domains')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ $domain = $this->apiCall('SubDomains.get', array(
+ 'id' => $id,
+ 'domainname' => $domainname
+ ));
+ $domainid = $domain['id'];
+
+ $stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid`= :domainid");
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get ssl-certificate for '" . $domain['domain'] . "'");
+ $result = Database::pexecute_first($stmt, array(
+ "domainid" => $domainid
+ ));
+ if (! $result) {
+ throw new \Exception("Domain '" . $domain['domain'] . "' does not have a certificate.", 412);
+ }
+ return $this->response(200, "successful", $result);
+ }
+
+ /**
+ * update ssl-certificate entry for given domain by either id or domainname
+ *
+ * @param int $id
+ * optional, the domain-id
+ * @param string $domainname
+ * optional, the domainname
+ * @param string $ssl_cert_file
+ * @param string $ssl_key_file
+ * @param string $ssl_ca_file
+ * optional
+ * @param string $ssl_cert_chainfile
+ * optional
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function update()
+ {
+ $id = $this->getParam('id', true, 0);
+ $dn_optional = ($id <= 0 ? false : true);
+ $domainname = $this->getParam('domainname', $dn_optional, '');
+
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'domains')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ $domain = $this->apiCall('SubDomains.get', array(
+ 'id' => $id,
+ 'domainname' => $domainname
+ ));
+
+ // parameters
+ $ssl_cert_file = $this->getParam('ssl_cert_file');
+ $ssl_key_file = $this->getParam('ssl_key_file');
+ $ssl_ca_file = $this->getParam('ssl_ca_file', true, '');
+ $ssl_cert_chainfile = $this->getParam('ssl_cert_chainfile', true, '');
+ $this->addOrUpdateCertificate($domain['id'], $ssl_cert_file, $ssl_key_file, $ssl_ca_file, $ssl_cert_chainfile, false);
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] updated ssl-certificate for '" . $domain['domain'] . "'");
+ $result = $this->apiCall('Certificates.get', array(
+ 'id' => $domain['id']
+ ));
+ return $this->response(200, "successful", $result);
+ }
+
+ /**
+ * lists all certificate entries
+ *
+ * @param array $sql_search
+ * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), LIKE is used if left empty and 'value' => searchvalue
+ * @param int $sql_limit
+ * optional specify number of results to be returned
+ * @param int $sql_offset
+ * optional specify offset for resultset
+ * @param array $sql_orderby
+ * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more fields
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listing()
+ {
+ // select all my (accessable) certificates
+ $certs_stmt_query = "SELECT s.*, d.domain, d.letsencrypt, c.customerid, c.loginname
+ FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` s
+ LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` d ON `d`.`id` = `s`.`domainid`
+ LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` c ON `c`.`customerid` = `d`.`customerid`
+ WHERE ";
+
+ $qry_params = array();
+ $query_fields = array();
+ if ($this->isAdmin() && $this->getUserDetail('customers_see_all') == '0') {
+ // admin with only customer-specific permissions
+ $certs_stmt_query .= "d.adminid = :adminid ";
+ $qry_params['adminid'] = $this->getUserDetail('adminid');
+ } elseif ($this->isAdmin() == false) {
+ // customer-area
+ $certs_stmt_query .= "d.customerid = :cid ";
+ $qry_params['cid'] = $this->getUserDetail('customerid');
+ } else {
+ $certs_stmt_query .= "1 ";
+ }
+ $certs_stmt = Database::prepare($certs_stmt_query . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit());
+ $qry_params = array_merge($qry_params, $query_fields);
+ Database::pexecute($certs_stmt, $qry_params, true, true);
+ $result = array();
+ while ($cert = $certs_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ // respect froxlor-hostname
+ if ($cert['domainid'] == 0) {
+ $cert['domain'] = Settings::Get('system.hostname');
+ $cert['letsencrypt'] = Settings::Get('system.le_froxlor_enabled');
+ $cert['loginname'] = 'froxlor.panel';
+ }
+ $result[] = $cert;
+ }
+ return $this->response(200, "successful", array(
+ 'count' => count($result),
+ 'list' => $result
+ ));
+ }
+
+ /**
+ * returns the total number of certificates for the given user
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function listingCount()
+ {
+ // select all my (accessable) certificates
+ $certs_stmt_query = "SELECT COUNT(*) as num_certs
+ FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` s
+ LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` d ON `d`.`id` = `s`.`domainid`
+ LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` c ON `c`.`customerid` = `d`.`customerid`
+ WHERE ";
+ $qry_params = array();
+ if ($this->isAdmin() && $this->getUserDetail('customers_see_all') == '0') {
+ // admin with only customer-specific permissions
+ $certs_stmt_query .= "d.adminid = :adminid ";
+ $qry_params['adminid'] = $this->getUserDetail('adminid');
+ } elseif ($this->isAdmin() == false) {
+ // customer-area
+ $certs_stmt_query .= "d.customerid = :cid ";
+ $qry_params['cid'] = $this->getUserDetail('customerid');
+ } else {
+ $certs_stmt_query .= "1 ";
+ }
+ $certs_stmt = Database::prepare($certs_stmt_query);
+ $result = Database::pexecute_first($certs_stmt, $qry_params, true, true);
+ if ($result) {
+ return $this->response(200, "successful", $result['num_certs']);
+ }
+ }
+
+ /**
+ * delete certificates entry by id
+ *
+ * @param int $id
+ *
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function delete()
+ {
+ $id = $this->getParam('id');
+
+ if ($this->isAdmin() == false) {
+ $chk_stmt = Database::prepare("
+ SELECT d.domain, d.letsencrypt FROM `" . TABLE_PANEL_DOMAINS . "` d
+ LEFT JOIN `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` s ON s.domainid = d.id
+ WHERE s.`id` = :id AND d.`customerid` = :cid
+ ");
+ $chk = Database::pexecute_first($chk_stmt, array(
+ 'id' => $id,
+ 'cid' => $this->getUserDetail('customerid')
+ ));
+ } elseif ($this->isAdmin()) {
+ $chk_stmt = Database::prepare("
+ SELECT d.domain, d.letsencrypt FROM `" . TABLE_PANEL_DOMAINS . "` d
+ LEFT JOIN `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` s ON s.domainid = d.id
+ WHERE s.`id` = :id" . ($this->getUserDetail('customers_see_all') == '0' ? " AND d.`adminid` = :aid" : ""));
+ $params = array(
+ 'id' => $id
+ );
+ if ($this->getUserDetail('customers_see_all') == '0') {
+ $params['aid'] = $this->getUserDetail('adminid');
+ }
+ $chk = Database::pexecute_first($chk_stmt, $params);
+ if ($chk == false && $this->getUserDetail('change_serversettings')) {
+ // check whether it might be the froxlor-vhost certificate
+ $chk_stmt = Database::prepare("
+ SELECT \"" . Settings::Get('system.hostname') . "\" as domain, \"" . Settings::Get('system.le_froxlor_enabled') . "\" as letsencrypt FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "`
+ WHERE `id` = :id AND `domainid` = '0'");
+ $params = array(
+ 'id' => $id
+ );
+ $chk = Database::pexecute_first($chk_stmt, $params);
+ $chk['isFroxlorVhost'] = true;
+ }
+ }
+ if ($chk !== false) {
+ // additional access check by trying to get the certificate
+ if (isset($chk['isFroxlorVhost']) && $chk['isFroxlorVhost'] == true) {
+ $result = $chk;
+ } else {
+ $result = $this->apiCall('Certificates.get', array(
+ 'domainname' => $chk['domain']
+ ));
+ }
+ $del_stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE id = :id");
+ Database::pexecute($del_stmt, array(
+ 'id' => $id
+ ));
+ // trigger removing of certificate from acme.sh if let's encrypt
+ if ($chk['letsencrypt'] == '1') {
+ \Froxlor\System\Cronjob::inserttask('12', $chk['domain']);
+ }
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] removed ssl-certificate for '" . $chk['domain'] . "'");
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("Unable to determine SSL certificate. Maybe no access?", 406);
+ }
+
+ /**
+ * insert or update certificates entry
+ *
+ * @param int $domainid
+ * @param string $ssl_cert_file
+ * @param string $ssl_key_file
+ * @param string $ssl_ca_file
+ * @param string $ssl_cert_chainfile
+ * @param boolean $do_insert
+ * optional default false
+ *
+ * @return boolean
+ * @throws \Exception
+ */
+ private function addOrUpdateCertificate($domainid = 0, $ssl_cert_file = '', $ssl_key_file = '', $ssl_ca_file = '', $ssl_cert_chainfile = '', $do_insert = false)
+ {
+ if ($ssl_cert_file != '' && $ssl_key_file == '') {
+ \Froxlor\UI\Response::standard_error('sslcertificateismissingprivatekey', '', true);
+ }
+
+ $do_verify = true;
+ $expirationdate = null;
+ // no cert-file given -> forget everything
+ if ($ssl_cert_file == '') {
+ $ssl_key_file = '';
+ $ssl_ca_file = '';
+ $ssl_cert_chainfile = '';
+ $do_verify = false;
+ }
+
+ // verify certificate content
+ if ($do_verify) {
+ // array openssl_x509_parse ( mixed $x509cert [, bool $shortnames = true ] )
+ // openssl_x509_parse() returns information about the supplied x509cert, including fields such as
+ // subject name, issuer name, purposes, valid from and valid to dates etc.
+ $cert_content = openssl_x509_parse($ssl_cert_file);
+
+ if (is_array($cert_content) && isset($cert_content['subject']) && isset($cert_content['subject']['CN'])) {
+ // bool openssl_x509_check_private_key ( mixed $cert , mixed $key )
+ // Checks whether the given key is the private key that corresponds to cert.
+ if (openssl_x509_check_private_key($ssl_cert_file, $ssl_key_file) === false) {
+ \Froxlor\UI\Response::standard_error('sslcertificateinvalidcertkeypair', '', true);
+ }
+
+ // check optional stuff
+ if ($ssl_ca_file != '') {
+ $ca_content = openssl_x509_parse($ssl_ca_file);
+ if (! is_array($ca_content)) {
+ // invalid
+ \Froxlor\UI\Response::standard_error('sslcertificateinvalidca', '', true);
+ }
+ }
+ if ($ssl_cert_chainfile != '') {
+ $chain_content = openssl_x509_parse($ssl_cert_chainfile);
+ if (! is_array($chain_content)) {
+ // invalid
+ \Froxlor\UI\Response::standard_error('sslcertificateinvalidchain', '', true);
+ }
+ }
+ } else {
+ \Froxlor\UI\Response::standard_error('sslcertificateinvalidcert', '', true);
+ }
+ $expirationdate = empty($cert_content['validTo_time_t']) ? null : date("Y-m-d H:i:s", $cert_content['validTo_time_t']);
+ }
+
+ // Add/Update database entry
+ $qrystart = "UPDATE ";
+ $qrywhere = "WHERE ";
+ if ($do_insert) {
+ $qrystart = "INSERT INTO ";
+ $qrywhere = ", ";
+ }
+ $stmt = Database::prepare($qrystart . " `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` SET
+ `ssl_cert_file` = :ssl_cert_file,
+ `ssl_key_file` = :ssl_key_file,
+ `ssl_ca_file` = :ssl_ca_file,
+ `ssl_cert_chainfile` = :ssl_cert_chainfile,
+ `expirationdate` = :expirationdate
+ " . $qrywhere . " `domainid`= :domainid
+ ");
+ $params = array(
+ "ssl_cert_file" => $ssl_cert_file,
+ "ssl_key_file" => $ssl_key_file,
+ "ssl_ca_file" => $ssl_ca_file,
+ "ssl_cert_chainfile" => $ssl_cert_chainfile,
+ "expirationdate" => $expirationdate,
+ "domainid" => $domainid
+ );
+ Database::pexecute($stmt, $params, true, true);
+ // insert task to re-generate webserver-configs (#1260)
+ \Froxlor\System\Cronjob::inserttask('1');
+ return true;
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/Cronjobs.php b/lib/Froxlor/Api/Commands/Cronjobs.php
new file mode 100644
index 00000000..d6bd7889
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/Cronjobs.php
@@ -0,0 +1,191 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class Cronjobs extends \Froxlor\Api\ApiCommand implements \Froxlor\Api\ResourceEntity
+{
+
+ /**
+ * You cannot add new cronjobs yet.
+ */
+ public function add()
+ {
+ throw new \Exception('You cannot add new cronjobs yet.', 303);
+ }
+
+ /**
+ * return a cronjob entry by id
+ *
+ * @param int $id
+ * cronjob-id
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function get()
+ {
+ if ($this->isAdmin()) {
+ $id = $this->getParam('id');
+
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_CRONRUNS . "` WHERE `id` = :id
+ ");
+ $result = Database::pexecute_first($result_stmt, array(
+ 'id' => $id
+ ), true, true);
+ if ($result) {
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("cronjob with id #" . $id . " could not be found", 404);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * update a cronjob entry by given id
+ *
+ * @param int $id
+ * @param bool $isactive
+ * optional whether the cronjob is active or not
+ * @param int $interval_value
+ * optional number of seconds/minutes/hours/etc. for the interval
+ * @param string $interval_interval
+ * optional interval for the cronjob (MINUTE, HOUR, DAY, WEEK or MONTH)
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function update()
+ {
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) {
+
+ // required parameter
+ $id = $this->getParam('id');
+
+ $result = $this->apiCall('Cronjobs.get', array(
+ 'id' => $id
+ ));
+
+ // split interval
+ $cur_int = explode(" ", $result['interval']);
+
+ // parameter
+ $isactive = $this->getBoolParam('isactive', true, $result['isactive']);
+ $interval_value = $this->getParam('interval_value', true, $cur_int[0]);
+ $interval_interval = $this->getParam('interval_interval', true, $cur_int[1]);
+
+ // validation
+ if ($isactive != 1) {
+ $isactive = 0;
+ }
+ $interval_value = \Froxlor\Validate\Validate::validate($interval_value, 'interval_value', '/^([0-9]+)$/Di', 'stringisempty', array(), true);
+ $interval_interval = \Froxlor\Validate\Validate::validate($interval_interval, 'interval_interval', '', '', array(), true);
+
+ // put together interval value
+ $interval = $interval_value . ' ' . strtoupper($interval_interval);
+
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_CRONRUNS . "`
+ SET `isactive` = :isactive, `interval` = :int
+ WHERE `id` = :id
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'isactive' => $isactive,
+ 'int' => $interval,
+ 'id' => $id
+ ), true, true);
+
+ // insert task to re-generate the cron.d-file
+ \Froxlor\System\Cronjob::inserttask('99');
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] cronjob with description '" . $result['module'] . '/' . $result['cronfile'] . "' has been updated by '" . $this->getUserDetail('loginname') . "'");
+ $result = $this->apiCall('Cronjobs.get', array(
+ 'id' => $id
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * lists all cronjob entries
+ *
+ * @param array $sql_search
+ * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), LIKE is used if left empty and 'value' => searchvalue
+ * @param int $sql_limit
+ * optional specify number of results to be returned
+ * @param int $sql_offset
+ * optional specify offset for resultset
+ * @param array $sql_orderby
+ * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more fields
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listing()
+ {
+ if ($this->isAdmin()) {
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] list cronjobs");
+ $query_fields = array();
+ $result_stmt = Database::prepare("
+ SELECT `c`.* FROM `" . TABLE_PANEL_CRONRUNS . "` `c` " . $this->getSearchWhere($query_fields) . $this->getOrderBy() . $this->getLimit());
+ Database::pexecute($result_stmt, $query_fields, true, true);
+ $result = array();
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = $row;
+ }
+ return $this->response(200, "successful", array(
+ 'count' => count($result),
+ 'list' => $result
+ ));
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * returns the total number of cronjobs
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function listingCount()
+ {
+ if ($this->isAdmin()) {
+ $result_stmt = Database::prepare("
+ SELECT COUNT(*) as num_crons FROM `" . TABLE_PANEL_CRONRUNS . "` `c`
+ ");
+ $result = Database::pexecute_first($result_stmt, null, true, true);
+ if ($result) {
+ return $this->response(200, "successful", $result['num_crons']);
+ }
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * You cannot delete system cronjobs.
+ */
+ public function delete()
+ {
+ throw new \Exception('You cannot delete system cronjobs.', 303);
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/CustomerBackups.php b/lib/Froxlor/Api/Commands/CustomerBackups.php
new file mode 100644
index 00000000..bd74dac3
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/CustomerBackups.php
@@ -0,0 +1,248 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class CustomerBackups extends \Froxlor\Api\ApiCommand implements \Froxlor\Api\ResourceEntity
+{
+
+ /**
+ * check whether backup is enabled systemwide and if accessable for customer (hide_options)
+ *
+ * @throws \Exception
+ */
+ private function validateAccess()
+ {
+ if (Settings::Get('system.backupenabled') != 1) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras.backup')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+ }
+
+ /**
+ * add a new customer backup job
+ *
+ * @param string $path
+ * path to store the backup to
+ * @param bool $backup_dbs
+ * optional whether to backup databases, default is 0 (false)
+ * @param bool $backup_mail
+ * optional whether to backup mail-data, default is 0 (false)
+ * @param bool $backup_web
+ * optional whether to backup web-data, default is 0 (false)
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function add()
+ {
+ $this->validateAccess();
+
+ // required parameter
+ $path = $this->getParam('path');
+
+ // parameter
+ $backup_dbs = $this->getBoolParam('backup_dbs', true, 0);
+ $backup_mail = $this->getBoolParam('backup_mail', true, 0);
+ $backup_web = $this->getBoolParam('backup_web', true, 0);
+
+ // get customer data
+ $customer = $this->getCustomerData();
+
+ // validation
+ $path = \Froxlor\FileDir::makeCorrectDir(\Froxlor\Validate\Validate::validate($path, 'path', '', '', array(), true));
+ $userpath = $path;
+ $path = \Froxlor\FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path);
+
+ // path cannot be the customers docroot
+ if ($path == \Froxlor\FileDir::makeCorrectDir($customer['documentroot'])) {
+ \Froxlor\UI\Response::standard_error('backupfoldercannotbedocroot', '', true);
+ }
+
+ if ($backup_dbs != '1') {
+ $backup_dbs = '0';
+ }
+
+ if ($backup_mail != '1') {
+ $backup_mail = '0';
+ }
+
+ if ($backup_web != '1') {
+ $backup_web = '0';
+ }
+
+ $task_data = array(
+ 'customerid' => $customer['customerid'],
+ 'uid' => $customer['guid'],
+ 'gid' => $customer['guid'],
+ 'loginname' => $customer['loginname'],
+ 'destdir' => $path,
+ 'backup_dbs' => $backup_dbs,
+ 'backup_mail' => $backup_mail,
+ 'backup_web' => $backup_web
+ );
+ // schedule backup job
+ \Froxlor\System\Cronjob::inserttask('20', $task_data);
+
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added customer-backup job for '" . $customer['loginname'] . "'. Target directory: " . $userpath);
+ return $this->response(200, "successful", $task_data);
+ }
+
+ /**
+ * You cannot get a planned backup.
+ * Try CustomerBackups.listing()
+ */
+ public function get()
+ {
+ throw new \Exception('You cannot get a planned backup. Try CustomerBackups.listing()', 303);
+ }
+
+ /**
+ * You cannot update a planned backup.
+ * You need to delete it and re-add it.
+ */
+ public function update()
+ {
+ throw new \Exception('You cannot update a planned backup. You need to delete it and re-add it.', 303);
+ }
+
+ /**
+ * list all planned backup-jobs, if called from an admin, list all planned backup-jobs of all customers you are allowed to view, or specify id or loginname for one specific customer
+ *
+ * @param int $customerid
+ * optional, admin-only, select backup-jobs of a specific customer by id
+ * @param string $loginname
+ * optional, admin-only, select backup-jobs of a specific customer by loginname
+ * @param array $sql_search
+ * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), LIKE is used if left empty and 'value' => searchvalue
+ * @param int $sql_limit
+ * optional specify number of results to be returned
+ * @param int $sql_offset
+ * optional specify offset for resultset
+ * @param array $sql_orderby
+ * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more fields
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listing()
+ {
+ $this->validateAccess();
+
+ $customer_ids = $this->getAllowedCustomerIds('extras.backup');
+
+ // check whether there is a backup-job for this customer
+ $query_fields = array();
+ $sel_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_TASKS . "` WHERE `type` = '20'" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit());
+ Database::pexecute($sel_stmt, $query_fields, true, true);
+ $result = array();
+ while ($entry = $sel_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $entry['data'] = json_decode($entry['data'], true);
+ if (in_array($entry['data']['customerid'], $customer_ids)) {
+ $result[] = $entry;
+ }
+ }
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] list customer-backups");
+ return $this->response(200, "successful", array(
+ 'count' => count($result),
+ 'list' => $result
+ ));
+ }
+
+ /**
+ * returns the total number of planned backups
+ *
+ * @param int $customerid
+ * optional, admin-only, select backup-jobs of a specific customer by id
+ * @param string $loginname
+ * optional, admin-only, select backup-jobs of a specific customer by loginname
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function listingCount()
+ {
+ $this->validateAccess();
+
+ $customer_ids = $this->getAllowedCustomerIds('extras.backup');
+
+ // check whether there is a backup-job for this customer
+ $result_count = 0;
+ $sel_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_TASKS . "` WHERE `type` = '20'");
+ Database::pexecute($sel_stmt, null, true, true);
+ while ($entry = $sel_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $entry['data'] = json_decode($entry['data'], true);
+ if (in_array($entry['data']['customerid'], $customer_ids)) {
+ $result_count ++;
+ }
+ }
+ return $this->response(200, "successful", $result_count);
+ }
+
+ /**
+ * delete a planned backup-jobs by id, if called from an admin you need to specify the customerid/loginname
+ *
+ * @param int $backup_job_entry
+ * id of backup job
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return bool
+ */
+ public function delete()
+ {
+ // get planned backups
+ $result = $this->apiCall('CustomerBackups.listing', $this->getParamList());
+
+ $entry = $this->getParam('backup_job_entry');
+ $customer_ids = $this->getAllowedCustomerIds('extras.backup');
+
+ if ($result['count'] > 0 && $entry > 0) {
+ // prepare statement
+ $del_stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_TASKS . "` WHERE `id` = :tid");
+ // check for the correct job
+ foreach ($result['list'] as $backupjob) {
+ if ($backupjob['id'] == $entry && in_array($backupjob['data']['customerid'], $customer_ids)) {
+ Database::pexecute($del_stmt, array(
+ 'tid' => $entry
+ ), true, true);
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] deleted planned customer-backup #" . $entry);
+ return $this->response(200, "successful", true);
+ }
+ }
+ }
+ throw new \Exception('Backup job with id #' . $entry . ' could not be found', 404);
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/Customers.php b/lib/Froxlor/Api/Commands/Customers.php
new file mode 100644
index 00000000..1d9cad29
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/Customers.php
@@ -0,0 +1,1722 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class Customers extends \Froxlor\Api\ApiCommand implements \Froxlor\Api\ResourceEntity
+{
+
+ /**
+ * lists all customer entries
+ *
+ * @param array $sql_search
+ * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), LIKE is used if left empty and 'value' => searchvalue
+ * @param int $sql_limit
+ * optional specify number of results to be returned
+ * @param int $sql_offset
+ * optional specify offset for resultset
+ * @param array $sql_orderby
+ * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more fields
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listing()
+ {
+ if ($this->isAdmin()) {
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] list customers");
+ $query_fields = array();
+ $result_stmt = Database::prepare("
+ SELECT `c`.*, `a`.`loginname` AS `adminname`
+ FROM `" . TABLE_PANEL_CUSTOMERS . "` `c`, `" . TABLE_PANEL_ADMINS . "` `a`
+ WHERE " . ($this->getUserDetail('customers_see_all') ? '' : " `c`.`adminid` = :adminid AND ") . "
+ `c`.`adminid` = `a`.`adminid`" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit());
+ $params = array();
+ if ($this->getUserDetail('customers_see_all') == '0') {
+ $params = array(
+ 'adminid' => $this->getUserDetail('adminid')
+ );
+ }
+ $params = array_merge($params, $query_fields);
+ Database::pexecute($result_stmt, $params, true, true);
+ $result = array();
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = $row;
+ }
+ return $this->response(200, "successful", array(
+ 'count' => count($result),
+ 'list' => $result
+ ));
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * returns the total number of customers for the given admin
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function listingCount()
+ {
+ if ($this->isAdmin()) {
+ $result_stmt = Database::prepare("
+ SELECT COUNT(*) as num_customers
+ FROM `" . TABLE_PANEL_CUSTOMERS . "`
+ WHERE " . ($this->getUserDetail('customers_see_all') ? "1" : " `adminid` = :adminid "));
+ $params = array();
+ if ($this->getUserDetail('customers_see_all') == '0') {
+ $params = array(
+ 'adminid' => $this->getUserDetail('adminid')
+ );
+ }
+ $result = Database::pexecute_first($result_stmt, $params, true, true);
+ if ($result) {
+ return $this->response(200, "successful", $result['num_customers']);
+ }
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * return a customer entry by either id or loginname
+ *
+ * @param int $id
+ * optional, the customer-id
+ * @param string $loginname
+ * optional, the loginname
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function get()
+ {
+ $id = $this->getParam('id', true, 0);
+ $ln_optional = ($id <= 0 ? false : true);
+ $loginname = $this->getParam('loginname', $ln_optional, '');
+
+ if ($this->isAdmin()) {
+ $result_stmt = Database::prepare("
+ SELECT `c`.*, `a`.`loginname` AS `adminname`
+ FROM `" . TABLE_PANEL_CUSTOMERS . "` `c`, `" . TABLE_PANEL_ADMINS . "` `a`
+ WHERE " . ($id > 0 ? "`c`.`customerid` = :idln" : "`c`.`loginname` = :idln") . ($this->getUserDetail('customers_see_all') ? '' : " AND `c`.`adminid` = :adminid") . " AND `c`.`adminid` = `a`.`adminid`");
+ $params = array(
+ 'idln' => ($id <= 0 ? $loginname : $id)
+ );
+ if ($this->getUserDetail('customers_see_all') == '0') {
+ $params['adminid'] = $this->getUserDetail('adminid');
+ }
+ } else {
+ if (($id > 0 && $id != $this->getUserDetail('customerid')) || ! empty($loginname) && $loginname != $this->getUserDetail('loginname')) {
+ throw new \Exception("You cannot access data of other customers", 401);
+ }
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_CUSTOMERS . "`
+ WHERE " . ($id > 0 ? "`customerid` = :idln" : "`loginname` = :idln"));
+ $params = array(
+ 'idln' => ($id <= 0 ? $loginname : $id)
+ );
+ }
+ $result = Database::pexecute_first($result_stmt, $params, true, true);
+ if ($result) {
+ // check whether the admin does not want the customer to see the notes
+ if (! $this->isAdmin() && $result['custom_notes_show'] != 1) {
+ $result['custom_notes'] = "";
+ }
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] get customer '" . $result['loginname'] . "'");
+ return $this->response(200, "successful", $result);
+ }
+ $key = ($id > 0 ? "id #" . $id : "loginname '" . $loginname . "'");
+ throw new \Exception("Customer with " . $key . " could not be found", 404);
+ }
+
+ /**
+ * create a new customer with default ftp-user and standard-subdomain (if wanted)
+ *
+ * @param string $email
+ * @param string $name
+ * optional if company is set, else required
+ * @param string $firstname
+ * optional if company is set, else required
+ * @param string $company
+ * optional but required if name/firstname empty
+ * @param string $street
+ * optional
+ * @param string $zipcode
+ * optional
+ * @param string $city
+ * optional
+ * @param string $phone
+ * optional
+ * @param string $fax
+ * optional
+ * @param int $customernumber
+ * optional
+ * @param string $def_language,
+ * optional, default is system-default language
+ * @param bool $api_allowed
+ * optional, default is true if system setting api.enabled is true, else false
+ * @param int $gender
+ * optional, 0 = no-gender, 1 = male, 2 = female
+ * @param string $custom_notes
+ * optional notes
+ * @param bool $custom_notes_show
+ * optional, whether to show the content of custom_notes to the customer, default 0 (false)
+ * @param string $new_loginname
+ * optional, if empty generated automatically using customer-prefix and increasing number
+ * @param string $new_customer_password
+ * optional, if empty generated automatically and send to the customer's email if $sendpassword is 1
+ * @param bool $sendpassword
+ * optional, whether to send the password to the customer after creation, default 0 (false)
+ * @param int $diskspace
+ * optional disk-space available for customer in MB, default 0
+ * @param bool $diskspace_ul
+ * optional, whether customer should have unlimited diskspace, default 0 (false)
+ * @param int $traffic
+ * optional traffic available for customer in GB, default 0
+ * @param bool $traffic_ul
+ * optional, whether customer should have unlimited traffic, default 0 (false)
+ * @param int $subdomains
+ * optional amount of subdomains available for customer, default 0
+ * @param bool $subdomains_ul
+ * optional, whether customer should have unlimited subdomains, default 0 (false)
+ * @param int $emails
+ * optional amount of emails available for customer, default 0
+ * @param bool $emails_ul
+ * optional, whether customer should have unlimited emails, default 0 (false)
+ * @param int $email_accounts
+ * optional amount of email-accounts available for customer, default 0
+ * @param bool $email_accounts_ul
+ * optional, whether customer should have unlimited email-accounts, default 0 (false)
+ * @param int $email_forwarders
+ * optional amount of email-forwarders available for customer, default 0
+ * @param bool $email_forwarders_ul
+ * optional, whether customer should have unlimited email-forwarders, default 0 (false)
+ * @param int $email_quota
+ * optional size of email-quota available for customer in MB, default is system-setting mail_quota
+ * @param bool $email_quota_ul
+ * optional, whether customer should have unlimited email-quota, default 0 (false)
+ * @param bool $email_imap
+ * optional, whether to allow IMAP access, default 0 (false)
+ * @param bool $email_pop3
+ * optional, whether to allow POP3 access, default 0 (false)
+ * @param int $ftps
+ * optional amount of ftp-accounts available for customer, default 0
+ * @param bool $ftps_ul
+ * optional, whether customer should have unlimited ftp-accounts, default 0 (false)
+ * @param int $mysqls
+ * optional amount of mysql-databases available for customer, default 0
+ * @param bool $mysqls_ul
+ * optional, whether customer should have unlimited mysql-databases, default 0 (false)
+ * @param bool $createstdsubdomain
+ * optional, whether to create a standard-subdomain ([loginname].froxlor-hostname.tld), default 0 (false)
+ * @param bool $phpenabled
+ * optional, whether to allow usage of PHP, default 0 (false)
+ * @param array $allowed_phpconfigs
+ * optional, array of IDs of php-config that the customer is allowed to use, default empty (none)
+ * @param bool $perlenabled
+ * optional, whether to allow usage of Perl/CGI, default 0 (false)
+ * @param bool $dnsenabled
+ * optional, wether to allow usage of the DNS editor (requires activated nameserver in settings), default 0 (false)
+ * @param bool $logviewenabled
+ * optional, wether to allow acccess to webserver access/error-logs, default 0 (false)
+ * @param bool $store_defaultindex
+ * optional, whether to store the default index file to customers homedir
+ * @param int $hosting_plan_id
+ * optional, specify a hosting-plan to set certain resource-values from the plan instead of specifying them
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function add()
+ {
+ if ($this->isAdmin()) {
+ if ($this->getUserDetail('customers_used') < $this->getUserDetail('customers') || $this->getUserDetail('customers') == '-1') {
+
+ // required parameters
+ $email = $this->getParam('email');
+
+ // parameters
+ $name = $this->getParam('name', true, '');
+ $firstname = $this->getParam('firstname', true, '');
+ $company_required = (! empty($name) && empty($firstname)) || (empty($name) && ! empty($firstname)) || (empty($name) && empty($firstname));
+ $company = $this->getParam('company', ($company_required ? false : true), '');
+ $street = $this->getParam('street', true, '');
+ $zipcode = $this->getParam('zipcode', true, '');
+ $city = $this->getParam('city', true, '');
+ $phone = $this->getParam('phone', true, '');
+ $fax = $this->getParam('fax', true, '');
+ $customernumber = $this->getParam('customernumber', true, '');
+ $def_language = $this->getParam('def_language', true, Settings::Get('panel.standardlanguage'));
+ $api_allowed = $this->getBoolParam('api_allowed', true, Settings::Get('api.enabled'));
+ $gender = (int) $this->getParam('gender', true, 0);
+ $custom_notes = $this->getParam('custom_notes', true, '');
+ $custom_notes_show = $this->getBoolParam('custom_notes_show', true, 0);
+ $createstdsubdomain = $this->getBoolParam('createstdsubdomain', true, 0);
+ $password = $this->getParam('new_customer_password', true, '');
+ $sendpassword = $this->getBoolParam('sendpassword', true, 0);
+ $store_defaultindex = $this->getBoolParam('store_defaultindex', true, 0);
+ $loginname = $this->getParam('new_loginname', true, '');
+
+ // hosting-plan values
+ $hosting_plan_id = $this->getParam('hosting_plan_id', true, 0);
+ if ($hosting_plan_id > 0) {
+ $hp_result = $this->apiCall('HostingPlans.get', array(
+ 'id' => $hosting_plan_id
+ ));
+ $hp_result['value'] = json_decode($hp_result['value'], true);
+ foreach ($hp_result['value'] as $index => $value) {
+ $hp_result[$index] = $value;
+ }
+ $diskspace = $hp_result['diskspace'] ?? 0;
+ $traffic = $hp_result['traffic'] ?? 0;
+ $subdomains = $hp_result['subdomains'] ?? 0;
+ $emails = $hp_result['emails'] ?? 0;
+ $email_accounts = $hp_result['email_accounts'] ?? 0;
+ $email_forwarders = $hp_result['email_forwarders'] ?? 0;
+ $email_quota = $hp_result['email_quota'] ?? Settings::Get('system.mail_quota');
+ $email_imap = $hp_result['email_imap'] ?? 0;
+ $email_pop3 = $hp_result['email_pop3'] ?? 0;
+ $ftps = $hp_result['ftps'] ?? 0;
+ $mysqls = $hp_result['mysqls'] ?? 0;
+ $phpenabled = $hp_result['phpenabled'] ?? 0;
+ $p_allowed_phpconfigs = $hp_result['allowed_phpconfigs'] ?? 0;
+ $perlenabled = $hp_result['perlenabled'] ?? 0;
+ $dnsenabled = $hp_result['dnsenabled'] ?? 0;
+ $logviewenabled = $hp_result['logviewenabled'] ?? 0;
+ } else {
+ $diskspace = $this->getUlParam('diskspace', 'diskspace_ul', true, 0);
+ $traffic = $this->getUlParam('traffic', 'traffic_ul', true, 0);
+ $subdomains = $this->getUlParam('subdomains', 'subdomains_ul', true, 0);
+ $emails = $this->getUlParam('emails', 'emails_ul', true, 0);
+ $email_accounts = $this->getUlParam('email_accounts', 'email_accounts_ul', true, 0);
+ $email_forwarders = $this->getUlParam('email_forwarders', 'email_forwarders_ul', true, 0);
+ $email_quota = $this->getUlParam('email_quota', 'email_quota_ul', true, Settings::Get('system.mail_quota'));
+ $email_imap = $this->getBoolParam('email_imap', true, 0);
+ $email_pop3 = $this->getBoolParam('email_pop3', true, 0);
+ $ftps = $this->getUlParam('ftps', 'ftps_ul', true, 0);
+ $mysqls = $this->getUlParam('mysqls', 'mysqls_ul', true, 0);
+ $phpenabled = $this->getBoolParam('phpenabled', true, 0);
+ $p_allowed_phpconfigs = $this->getParam('allowed_phpconfigs', true, array());
+ $perlenabled = $this->getBoolParam('perlenabled', true, 0);
+ $dnsenabled = $this->getBoolParam('dnsenabled', true, 0);
+ $logviewenabled = $this->getBoolParam('logviewenabled', true, 0);
+ }
+
+ // validation
+ $name = \Froxlor\Validate\Validate::validate($name, 'name', '', '', array(), true);
+ $firstname = \Froxlor\Validate\Validate::validate($firstname, 'first name', '', '', array(), true);
+ $company = \Froxlor\Validate\Validate::validate($company, 'company', '', '', array(), true);
+ $street = \Froxlor\Validate\Validate::validate($street, 'street', '', '', array(), true);
+ $zipcode = \Froxlor\Validate\Validate::validate($zipcode, 'zipcode', '/^[0-9 \-A-Z]*$/', '', array(), true);
+ $city = \Froxlor\Validate\Validate::validate($city, 'city', '', '', array(), true);
+ $phone = \Froxlor\Validate\Validate::validate($phone, 'phone', '/^[0-9\- \+\(\)\/]*$/', '', array(), true);
+ $fax = \Froxlor\Validate\Validate::validate($fax, 'fax', '/^[0-9\- \+\(\)\/]*$/', '', array(), true);
+ $idna_convert = new \Froxlor\Idna\IdnaWrapper();
+ $email = $idna_convert->encode(\Froxlor\Validate\Validate::validate($email, 'email', '', '', array(), true));
+ $customernumber = \Froxlor\Validate\Validate::validate($customernumber, 'customer number', '/^[A-Za-z0-9 \-]*$/Di', '', array(), true);
+ $def_language = \Froxlor\Validate\Validate::validate($def_language, 'default language', '', '', array(), true);
+ $custom_notes = \Froxlor\Validate\Validate::validate(str_replace("\r\n", "\n", $custom_notes), 'custom_notes', \Froxlor\Validate\Validate::REGEX_CONF_TEXT, '', array(), true);
+
+ if (Settings::Get('system.mail_quota_enabled') != '1') {
+ $email_quota = - 1;
+ }
+
+ $password = \Froxlor\Validate\Validate::validate($password, 'password', '', '', array(), true);
+ // only check if not empty,
+ // cause empty == generate password automatically
+ if ($password != '') {
+ $password = \Froxlor\System\Crypt::validatePassword($password, true);
+ }
+
+ // gender out of range? [0,2]
+ if ($gender < 0 || $gender > 2) {
+ $gender = 0;
+ }
+
+ $allowed_phpconfigs = array();
+ if (! empty($p_allowed_phpconfigs) && is_array($p_allowed_phpconfigs)) {
+ foreach ($p_allowed_phpconfigs as $allowed_phpconfig) {
+ $allowed_phpconfig = intval($allowed_phpconfig);
+ $allowed_phpconfigs[] = $allowed_phpconfig;
+ }
+ }
+ $allowed_phpconfigs = array_map('intval', $allowed_phpconfigs);
+
+ $diskspace = $diskspace * 1024;
+ $traffic = $traffic * 1024 * 1024;
+
+ if (((($this->getUserDetail('diskspace_used') + $diskspace) > $this->getUserDetail('diskspace')) && ($this->getUserDetail('diskspace') / 1024) != '-1') || ((($this->getUserDetail('mysqls_used') + $mysqls) > $this->getUserDetail('mysqls')) && $this->getUserDetail('mysqls') != '-1') || ((($this->getUserDetail('emails_used') + $emails) > $this->getUserDetail('emails')) && $this->getUserDetail('emails') != '-1') || ((($this->getUserDetail('email_accounts_used') + $email_accounts) > $this->getUserDetail('email_accounts')) && $this->getUserDetail('email_accounts') != '-1') || ((($this->getUserDetail('email_forwarders_used') + $email_forwarders) > $this->getUserDetail('email_forwarders')) && $this->getUserDetail('email_forwarders') != '-1') || ((($this->getUserDetail('email_quota_used') + $email_quota) > $this->getUserDetail('email_quota')) && $this->getUserDetail('email_quota') != '-1' && Settings::Get('system.mail_quota_enabled') == '1') || ((($this->getUserDetail('ftps_used') + $ftps) > $this->getUserDetail('ftps')) && $this->getUserDetail('ftps') != '-1') || ((($this->getUserDetail('subdomains_used') + $subdomains) > $this->getUserDetail('subdomains')) && $this->getUserDetail('subdomains') != '-1') || (($diskspace / 1024) == '-1' && ($this->getUserDetail('diskspace') / 1024) != '-1') || ($mysqls == '-1' && $this->getUserDetail('mysqls') != '-1') || ($emails == '-1' && $this->getUserDetail('emails') != '-1') || ($email_accounts == '-1' && $this->getUserDetail('email_accounts') != '-1') || ($email_forwarders == '-1' && $this->getUserDetail('email_forwarders') != '-1') || ($email_quota == '-1' && $this->getUserDetail('email_quota') != '-1' && Settings::Get('system.mail_quota_enabled') == '1') || ($ftps == '-1' && $this->getUserDetail('ftps') != '-1') || ($subdomains == '-1' && $this->getUserDetail('subdomains') != '-1')) {
+ \Froxlor\UI\Response::standard_error('youcantallocatemorethanyouhave', '', true);
+ }
+
+ if (! \Froxlor\Validate\Validate::validateEmail($email)) {
+ \Froxlor\UI\Response::standard_error('emailiswrong', $email, true);
+ } else {
+
+ if ($loginname != '') {
+ $accountnumber = intval(Settings::Get('system.lastaccountnumber'));
+ $loginname = \Froxlor\Validate\Validate::validate($loginname, 'loginname', '/^[a-z][a-z0-9\-_]+$/i', '', array(), true);
+
+ // Accounts which match systemaccounts are not allowed, filtering them
+ if (preg_match('/^' . preg_quote(Settings::Get('customer.accountprefix'), '/') . '([0-9]+)/', $loginname)) {
+ \Froxlor\UI\Response::standard_error('loginnameissystemaccount', Settings::Get('customer.accountprefix'), true);
+ }
+
+ // Additional filtering for Bug #962
+ if (function_exists('posix_getpwnam') && ! in_array("posix_getpwnam", explode(",", ini_get('disable_functions'))) && posix_getpwnam($loginname)) {
+ \Froxlor\UI\Response::standard_error('loginnameissystemaccount', Settings::Get('customer.accountprefix'), true);
+ }
+ } else {
+ $accountnumber = intval(Settings::Get('system.lastaccountnumber')) + 1;
+ $loginname = Settings::Get('customer.accountprefix') . $accountnumber;
+ }
+
+ // Check if the account already exists
+ // do not check via api as we skip any permission checks for this task
+ $loginname_check_stmt = Database::prepare("
+ SELECT `loginname` FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `loginname` = :login
+ ");
+ $loginname_check = Database::pexecute_first($loginname_check_stmt, array(
+ 'login' => $loginname
+ ), true, true);
+
+ // Check if an admin with the loginname already exists
+ // do not check via api as we skip any permission checks for this task
+ $loginname_check_admin_stmt = Database::prepare("
+ SELECT `loginname` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `loginname` = :login
+ ");
+ $loginname_check_admin = Database::pexecute_first($loginname_check_admin_stmt, array(
+ 'login' => $loginname
+ ), true, true);
+
+ $mysql_maxlen = \Froxlor\Database\Database::getSqlUsernameLength() - strlen(Settings::Get('customer.mysqlprefix'));
+ if (($loginname_check && strtolower($loginname_check['loginname']) == strtolower($loginname)) || ($loginname_check_admin && strtolower($loginname_check_admin['loginname']) == strtolower($loginname))) {
+ \Froxlor\UI\Response::standard_error('loginnameexists', $loginname, true);
+ } elseif (! \Froxlor\Validate\Validate::validateUsername($loginname, Settings::Get('panel.unix_names'), $mysql_maxlen)) {
+ if (strlen($loginname) > $mysql_maxlen) {
+ \Froxlor\UI\Response::standard_error('loginnameiswrong2', $mysql_maxlen, true);
+ } else {
+ \Froxlor\UI\Response::standard_error('loginnameiswrong', $loginname, true);
+ }
+ }
+
+ $guid = intval(Settings::Get('system.lastguid')) + 1;
+ $documentroot = \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.documentroot_prefix') . '/' . $loginname);
+
+ if (file_exists($documentroot)) {
+ \Froxlor\UI\Response::standard_error('documentrootexists', $documentroot, true);
+ }
+
+ if ($password == '') {
+ $password = \Froxlor\System\Crypt::generatePassword();
+ }
+
+ $_theme = Settings::Get('panel.default_theme');
+
+ $ins_data = array(
+ 'adminid' => $this->getUserDetail('adminid'),
+ 'loginname' => $loginname,
+ 'passwd' => \Froxlor\System\Crypt::makeCryptPassword($password),
+ 'name' => $name,
+ 'firstname' => $firstname,
+ 'gender' => $gender,
+ 'company' => $company,
+ 'street' => $street,
+ 'zipcode' => $zipcode,
+ 'city' => $city,
+ 'phone' => $phone,
+ 'fax' => $fax,
+ 'email' => $email,
+ 'customerno' => $customernumber,
+ 'lang' => $def_language,
+ 'api_allowed' => $api_allowed,
+ 'docroot' => $documentroot,
+ 'guid' => $guid,
+ 'diskspace' => $diskspace,
+ 'traffic' => $traffic,
+ 'subdomains' => $subdomains,
+ 'emails' => $emails,
+ 'email_accounts' => $email_accounts,
+ 'email_forwarders' => $email_forwarders,
+ 'email_quota' => $email_quota,
+ 'ftps' => $ftps,
+ 'mysqls' => $mysqls,
+ 'phpenabled' => $phpenabled,
+ 'allowed_phpconfigs' => empty($allowed_phpconfigs) ? "" : json_encode($allowed_phpconfigs),
+ 'imap' => $email_imap,
+ 'pop3' => $email_pop3,
+ 'perlenabled' => $perlenabled,
+ 'dnsenabled' => $dnsenabled,
+ 'logviewenabled' => $logviewenabled,
+ 'theme' => $_theme,
+ 'custom_notes' => $custom_notes,
+ 'custom_notes_show' => $custom_notes_show
+ );
+
+ $ins_stmt = Database::prepare("
+ INSERT INTO `" . TABLE_PANEL_CUSTOMERS . "` SET
+ `adminid` = :adminid,
+ `loginname` = :loginname,
+ `password` = :passwd,
+ `name` = :name,
+ `firstname` = :firstname,
+ `gender` = :gender,
+ `company` = :company,
+ `street` = :street,
+ `zipcode` = :zipcode,
+ `city` = :city,
+ `phone` = :phone,
+ `fax` = :fax,
+ `email` = :email,
+ `customernumber` = :customerno,
+ `def_language` = :lang,
+ `api_allowed` = :api_allowed,
+ `documentroot` = :docroot,
+ `guid` = :guid,
+ `diskspace` = :diskspace,
+ `traffic` = :traffic,
+ `subdomains` = :subdomains,
+ `emails` = :emails,
+ `email_accounts` = :email_accounts,
+ `email_forwarders` = :email_forwarders,
+ `email_quota` = :email_quota,
+ `ftps` = :ftps,
+ `mysqls` = :mysqls,
+ `standardsubdomain` = '0',
+ `phpenabled` = :phpenabled,
+ `allowed_phpconfigs` = :allowed_phpconfigs,
+ `imap` = :imap,
+ `pop3` = :pop3,
+ `perlenabled` = :perlenabled,
+ `dnsenabled` = :dnsenabled,
+ `logviewenabled` = :logviewenabled,
+ `theme` = :theme,
+ `custom_notes` = :custom_notes,
+ `custom_notes_show` = :custom_notes_show
+ ");
+ Database::pexecute($ins_stmt, $ins_data, true, true);
+
+ $customerid = Database::lastInsertId();
+ $ins_data['customerid'] = $customerid;
+
+ // update admin resource-usage
+ if ($mysqls != '-1') {
+ Admins::increaseUsage($this->getUserDetail('adminid'), 'mysqls_used', '', (int) $mysqls);
+ }
+
+ if ($emails != '-1') {
+ Admins::increaseUsage($this->getUserDetail('adminid'), 'emails_used', '', (int) $emails);
+ }
+
+ if ($email_accounts != '-1') {
+ Admins::increaseUsage($this->getUserDetail('adminid'), 'email_accounts_used', '', (int) $email_accounts);
+ }
+
+ if ($email_forwarders != '-1') {
+ Admins::increaseUsage($this->getUserDetail('adminid'), 'email_forwarders_used', '', (int) $email_forwarders);
+ }
+
+ if ($email_quota != '-1') {
+ Admins::increaseUsage($this->getUserDetail('adminid'), 'email_quota_used', '', (int) $email_quota);
+ }
+
+ if ($subdomains != '-1') {
+ Admins::increaseUsage($this->getUserDetail('adminid'), 'subdomains_used', '', (int) $subdomains);
+ }
+
+ if ($ftps != '-1') {
+ Admins::increaseUsage($this->getUserDetail('adminid'), 'ftps_used', '', (int) $ftps);
+ }
+
+ if (($diskspace / 1024) != '-1') {
+ Admins::increaseUsage($this->getUserDetail('adminid'), 'diskspace_used', '', (int) $diskspace);
+ }
+
+ // update last guid
+ Settings::Set('system.lastguid', $guid, true);
+
+ if ($accountnumber != intval(Settings::Get('system.lastaccountnumber'))) {
+ // update last account number
+ Settings::Set('system.lastaccountnumber', $accountnumber, true);
+ }
+
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] added customer '" . $loginname . "'");
+ unset($ins_data);
+
+ // insert task to create homedir etc.
+ \Froxlor\System\Cronjob::inserttask('2', $loginname, $guid, $guid, $store_defaultindex);
+
+ // Using filesystem - quota, insert a task which cleans the filesystem - quota
+ \Froxlor\System\Cronjob::inserttask('10');
+
+ // Add htpasswd for the stats-pages
+ $htpasswdPassword = \Froxlor\System\Crypt::makeCryptPassword($password, true);
+
+ $ins_stmt = Database::prepare("
+ INSERT INTO `" . TABLE_PANEL_HTPASSWDS . "` SET
+ `customerid` = :customerid,
+ `username` = :username,
+ `password` = :passwd,
+ `path` = :path
+ ");
+ $ins_data = array(
+ 'customerid' => $customerid,
+ 'username' => $loginname,
+ 'passwd' => $htpasswdPassword
+ );
+
+ $stats_folder = 'webalizer';
+ if (Settings::Get('system.awstats_enabled') == '1') {
+ $stats_folder = 'awstats';
+ }
+ $ins_data['path'] = \Froxlor\FileDir::makeCorrectDir($documentroot . '/' . $stats_folder . '/');
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] automatically added " . $stats_folder . " htpasswd for user '" . $loginname . "'");
+ Database::pexecute($ins_stmt, $ins_data, true, true);
+
+ \Froxlor\System\Cronjob::inserttask('1');
+
+ // add default FTP-User
+ // also, add froxlor-local user to ftp-group (if exists!) to
+ // allow access to customer-directories from within the panel, which
+ // is necessary when pathedit = Dropdown
+ $local_users = array(
+ Settings::Get('system.httpuser')
+ );
+ if ((int) Settings::Get('system.mod_fcgid_ownvhost') == 1 || (int) Settings::Get('phpfpm.enabled_ownvhost') == 1) {
+ if ((int) Settings::Get('system.mod_fcgid') == 1) {
+ $local_user = Settings::Get('system.mod_fcgid_httpuser');
+ } else {
+ $local_user = Settings::Get('phpfpm.vhost_httpuser');
+ }
+ // check froxlor-local user membership in ftp-group
+ // without this check addition may duplicate user in list if httpuser == local_user
+ if (in_array($local_user, $local_users) == false) {
+ $local_users[] = $local_user;
+ }
+ }
+ $this->apiCall('Ftps.add', array(
+ 'customerid' => $customerid,
+ 'path' => '/',
+ 'ftp_password' => $password,
+ 'ftp_description' => "Default",
+ 'sendinfomail' => 0,
+ 'ftp_username' => $loginname,
+ 'additional_members' => $local_users,
+ 'is_defaultuser' => 1
+ ));
+
+ $_stdsubdomain = '';
+ if ($createstdsubdomain == '1') {
+ if (Settings::Get('system.stdsubdomain') !== null && Settings::Get('system.stdsubdomain') != '') {
+ $_stdsubdomain = $loginname . '.' . Settings::Get('system.stdsubdomain');
+ } else {
+ $_stdsubdomain = $loginname . '.' . Settings::Get('system.hostname');
+ }
+
+ $ins_data = array(
+ 'domain' => $_stdsubdomain,
+ 'customerid' => $customerid,
+ 'adminid' => $this->getUserDetail('adminid'),
+ 'docroot' => $documentroot,
+ 'phpenabled' => $phpenabled,
+ 'openbasedir' => '1'
+ );
+ $domainid = - 1;
+ try {
+ $std_domain = $this->apiCall('Domains.add', $ins_data);
+ $domainid = $std_domain['id'];
+ } catch (\Exception $e) {
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_ERR, "[API] Unable to add standard-subdomain: " . $e->getMessage());
+ }
+
+ if ($domainid > 0) {
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `standardsubdomain` = :domainid WHERE `customerid` = :customerid
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'domainid' => $domainid,
+ 'customerid' => $customerid
+ ), true, true);
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] automatically added standardsubdomain for user '" . $loginname . "'");
+ \Froxlor\System\Cronjob::inserttask('1');
+ }
+ }
+
+ if ($sendpassword == '1') {
+
+ $srv_hostname = Settings::Get('system.hostname');
+ if (Settings::Get('system.froxlordirectlyviahostname') == '0') {
+ $srv_hostname .= '/' . basename(\Froxlor\Froxlor::getInstallDir());
+ }
+
+ $srv_ip_stmt = Database::prepare("
+ SELECT ip, port FROM `" . TABLE_PANEL_IPSANDPORTS . "`
+ WHERE `id` = :defaultip
+ ");
+ $default_ips = Settings::Get('system.defaultip');
+ $default_ips = explode(',', $default_ips);
+ $srv_ip = Database::pexecute_first($srv_ip_stmt, array(
+ 'defaultip' => reset($default_ips)
+ ), true, true);
+
+ $replace_arr = array(
+ 'FIRSTNAME' => $firstname,
+ 'NAME' => $name,
+ 'COMPANY' => $company,
+ 'SALUTATION' => \Froxlor\User::getCorrectUserSalutation(array(
+ 'firstname' => $firstname,
+ 'name' => $name,
+ 'company' => $company
+ )),
+ 'CUSTOMER_NO' => $customernumber,
+ 'USERNAME' => $loginname,
+ 'PASSWORD' => $password,
+ 'SERVER_HOSTNAME' => $srv_hostname,
+ 'SERVER_IP' => isset($srv_ip['ip']) ? $srv_ip['ip'] : '',
+ 'SERVER_PORT' => isset($srv_ip['port']) ? $srv_ip['port'] : '',
+ 'DOMAINNAME' => $_stdsubdomain
+ );
+
+ // get template for mail subject
+ $mail_subject = $this->getMailTemplate(array(
+ 'adminid' => $this->getUserDetail('adminid'),
+ 'def_language' => $def_language
+ ), 'mails', 'createcustomer_subject', $replace_arr, $this->lng['mails']['createcustomer']['subject']);
+ // get template for mail body
+ $mail_body = $this->getMailTemplate(array(
+ 'adminid' => $this->getUserDetail('adminid'),
+ 'def_language' => $def_language
+ ), 'mails', 'createcustomer_mailbody', $replace_arr, $this->lng['mails']['createcustomer']['mailbody']);
+
+ $_mailerror = false;
+ $mailerr_msg = "";
+ try {
+ $this->mailer()->Subject = $mail_subject;
+ $this->mailer()->AltBody = $mail_body;
+ $this->mailer()->msgHTML(str_replace("\n", " ", $mail_body));
+ $this->mailer()->addAddress($email, \Froxlor\User::getCorrectUserSalutation(array(
+ 'firstname' => $firstname,
+ 'name' => $name,
+ 'company' => $company
+ )));
+ $this->mailer()->send();
+ } catch (\PHPMailer\PHPMailer\Exception $e) {
+ $mailerr_msg = $e->errorMessage();
+ $_mailerror = true;
+ } catch (\Exception $e) {
+ $mailerr_msg = $e->getMessage();
+ $_mailerror = true;
+ }
+
+ if ($_mailerror) {
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_ERR, "[API] Error sending mail: " . $mailerr_msg);
+ \Froxlor\UI\Response::standard_error('errorsendingmail', $email, true);
+ }
+
+ $this->mailer()->clearAddresses();
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] automatically sent password to user '" . $loginname . "'");
+ }
+ }
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] added customer '" . $loginname . "'");
+
+ $result = $this->apiCall('Customers.get', array(
+ 'loginname' => $loginname
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("No more resources available", 406);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * update customer entry by either id or loginname, customer can only change language, password and theme
+ *
+ * @param int $id
+ * optional, the customer-id
+ * @param string $loginname
+ * optional, the loginname
+ * @param string $email
+ * @param string $name
+ * optional if company is set, else required
+ * @param string $firstname
+ * optional if company is set, else required
+ * @param string $company
+ * optional but required if name/firstname empty
+ * @param string $street
+ * optional
+ * @param string $zipcode
+ * optional
+ * @param string $city
+ * optional
+ * @param string $phone
+ * optional
+ * @param string $fax
+ * optional
+ * @param int $customernumber
+ * optional
+ * @param string $def_language,
+ * optional, default is system-default language
+ * @param bool $api_allowed
+ * optional, default is true if system setting api.enabled is true, else false
+ * @param int $gender
+ * optional, 0 = no-gender, 1 = male, 2 = female
+ * @param string $custom_notes
+ * optional notes
+ * @param bool $custom_notes_show
+ * optional, whether to show the content of custom_notes to the customer, default 0 (false)
+ * @param string $new_customer_password
+ * optional, iset new password
+ * @param bool $sendpassword
+ * optional, whether to send the password to the customer after creation, default 0 (false)
+ * @param int $move_to_admin
+ * optional, if valid admin-id is given here, the customer's admin/reseller can be changed
+ * @param bool $deactivated
+ * optional, if 1 (true) the customer can be deactivated/suspended
+ * @param int $diskspace
+ * optional disk-space available for customer in MB, default 0
+ * @param bool $diskspace_ul
+ * optional, whether customer should have unlimited diskspace, default 0 (false)
+ * @param int $traffic
+ * optional traffic available for customer in GB, default 0
+ * @param bool $traffic_ul
+ * optional, whether customer should have unlimited traffic, default 0 (false)
+ * @param int $subdomains
+ * optional amount of subdomains available for customer, default 0
+ * @param bool $subdomains_ul
+ * optional, whether customer should have unlimited subdomains, default 0 (false)
+ * @param int $emails
+ * optional amount of emails available for customer, default 0
+ * @param bool $emails_ul
+ * optional, whether customer should have unlimited emails, default 0 (false)
+ * @param int $email_accounts
+ * optional amount of email-accounts available for customer, default 0
+ * @param bool $email_accounts_ul
+ * optional, whether customer should have unlimited email-accounts, default 0 (false)
+ * @param int $email_forwarders
+ * optional amount of email-forwarders available for customer, default 0
+ * @param bool $email_forwarders_ul
+ * optional, whether customer should have unlimited email-forwarders, default 0 (false)
+ * @param int $email_quota
+ * optional size of email-quota available for customer in MB, default is system-setting mail_quota
+ * @param bool $email_quota_ul
+ * optional, whether customer should have unlimited email-quota, default 0 (false)
+ * @param bool $email_imap
+ * optional, whether to allow IMAP access, default 0 (false)
+ * @param bool $email_pop3
+ * optional, whether to allow POP3 access, default 0 (false)
+ * @param int $ftps
+ * optional amount of ftp-accounts available for customer, default 0
+ * @param bool $ftps_ul
+ * optional, whether customer should have unlimited ftp-accounts, default 0 (false)
+ * @param int $mysqls
+ * optional amount of mysql-databases available for customer, default 0
+ * @param bool $mysqls_ul
+ * optional, whether customer should have unlimited mysql-databases, default 0 (false)
+ * @param bool $createstdsubdomain
+ * optional, whether to create a standard-subdomain ([loginname].froxlor-hostname.tld), default 0 (false)
+ * @param bool $phpenabled
+ * optional, whether to allow usage of PHP, default 0 (false)
+ * @param array $allowed_phpconfigs
+ * optional, array of IDs of php-config that the customer is allowed to use, default empty (none)
+ * @param bool $perlenabled
+ * optional, whether to allow usage of Perl/CGI, default 0 (false)
+ * @param bool $dnsenabled
+ * optional, ether to allow usage of the DNS editor (requires activated nameserver in settings), default 0 (false)
+ * @param bool $logviewenabled
+ * optional, ether to allow acccess to webserver access/error-logs, default 0 (false)
+ * @param string $theme
+ * optional, change theme
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function update()
+ {
+ $id = $this->getParam('id', true, 0);
+ $ln_optional = ($id <= 0 ? false : true);
+ $loginname = $this->getParam('loginname', $ln_optional, '');
+
+ $result = $this->apiCall('Customers.get', array(
+ 'id' => $id,
+ 'loginname' => $loginname
+ ));
+ $id = $result['customerid'];
+
+ if ($this->isAdmin()) {
+ // parameters
+ $move_to_admin = (int) ($this->getParam('move_to_admin', true, 0));
+
+ $idna_convert = new \Froxlor\Idna\IdnaWrapper();
+ $email = $this->getParam('email', true, $idna_convert->decode($result['email']));
+ $name = $this->getParam('name', true, $result['name']);
+ $firstname = $this->getParam('firstname', true, $result['firstname']);
+ $company_required = (! empty($name) && empty($firstname)) || (empty($name) && ! empty($firstname)) || (empty($name) && empty($firstname));
+ $company = $this->getParam('company', ($company_required ? false : true), $result['company']);
+ $street = $this->getParam('street', true, $result['street']);
+ $zipcode = $this->getParam('zipcode', true, $result['zipcode']);
+ $city = $this->getParam('city', true, $result['city']);
+ $phone = $this->getParam('phone', true, $result['phone']);
+ $fax = $this->getParam('fax', true, $result['fax']);
+ $customernumber = $this->getParam('customernumber', true, $result['customernumber']);
+ $def_language = $this->getParam('def_language', true, $result['def_language']);
+ $api_allowed = $this->getBoolParam('api_allowed', true, $result['api_allowed']);
+ $gender = (int) $this->getParam('gender', true, $result['gender']);
+ $custom_notes = $this->getParam('custom_notes', true, $result['custom_notes']);
+ $custom_notes_show = $this->getBoolParam('custom_notes_show', true, $result['custom_notes_show']);
+
+ $dec_places = Settings::Get('panel.decimal_places');
+ $diskspace = $this->getUlParam('diskspace', 'diskspace_ul', true, round($result['diskspace'] / 1024, $dec_places));
+ $traffic = $this->getUlParam('traffic', 'traffic_ul', true, round($result['traffic'] / (1024 * 1024), $dec_places));
+ $subdomains = $this->getUlParam('subdomains', 'subdomains_ul', true, $result['subdomains']);
+ $emails = $this->getUlParam('emails', 'emails_ul', true, $result['emails']);
+ $email_accounts = $this->getUlParam('email_accounts', 'email_accounts_ul', true, $result['email_accounts']);
+ $email_forwarders = $this->getUlParam('email_forwarders', 'email_forwarders_ul', true, $result['email_forwarders']);
+ $email_quota = $this->getUlParam('email_quota', 'email_quota_ul', true, $result['email_quota']);
+ $email_imap = $this->getParam('email_imap', true, $result['imap']);
+ $email_pop3 = $this->getParam('email_pop3', true, $result['pop3']);
+ $ftps = $this->getUlParam('ftps', 'ftps_ul', true, $result['ftps']);
+ $mysqls = $this->getUlParam('mysqls', 'mysqls_ul', true, $result['mysqls']);
+ $createstdsubdomain = $this->getBoolParam('createstdsubdomain', true, 0);
+ $password = $this->getParam('new_customer_password', true, '');
+ $phpenabled = $this->getBoolParam('phpenabled', true, $result['phpenabled']);
+ $allowed_phpconfigs = $this->getParam('allowed_phpconfigs', true, json_decode($result['allowed_phpconfigs'], true));
+ $perlenabled = $this->getBoolParam('perlenabled', true, $result['perlenabled']);
+ $dnsenabled = $this->getBoolParam('dnsenabled', true, $result['dnsenabled']);
+ $logviewenabled = $this->getBoolParam('logviewenabled', true, $result['logviewenabled']);
+ $deactivated = $this->getBoolParam('deactivated', true, $result['deactivated']);
+ $theme = $this->getParam('theme', true, $result['theme']);
+ } else {
+ // allowed parameters
+ $def_language = $this->getParam('def_language', true, $result['def_language']);
+ $password = $this->getParam('new_customer_password', true, '');
+ $theme = $this->getParam('theme', true, $result['theme']);
+ }
+
+ // validation
+ if ($this->isAdmin()) {
+ $idna_convert = new \Froxlor\Idna\IdnaWrapper();
+ $name = \Froxlor\Validate\Validate::validate($name, 'name', '', '', array(), true);
+ $firstname = \Froxlor\Validate\Validate::validate($firstname, 'first name', '', '', array(), true);
+ $company = \Froxlor\Validate\Validate::validate($company, 'company', '', '', array(), true);
+ $street = \Froxlor\Validate\Validate::validate($street, 'street', '', '', array(), true);
+ $zipcode = \Froxlor\Validate\Validate::validate($zipcode, 'zipcode', '/^[0-9 \-A-Z]*$/', '', array(), true);
+ $city = \Froxlor\Validate\Validate::validate($city, 'city', '', '', array(), true);
+ $phone = \Froxlor\Validate\Validate::validate($phone, 'phone', '/^[0-9\- \+\(\)\/]*$/', '', array(), true);
+ $fax = \Froxlor\Validate\Validate::validate($fax, 'fax', '/^[0-9\- \+\(\)\/]*$/', '', array(), true);
+ $email = $idna_convert->encode(\Froxlor\Validate\Validate::validate($email, 'email', '', '', array(), true));
+ $customernumber = \Froxlor\Validate\Validate::validate($customernumber, 'customer number', '/^[A-Za-z0-9 \-]*$/Di', '', array(), true);
+ $custom_notes = \Froxlor\Validate\Validate::validate(str_replace("\r\n", "\n", $custom_notes), 'custom_notes', \Froxlor\Validate\Validate::REGEX_CONF_TEXT, '', array(), true);
+ if (! empty($allowed_phpconfigs)) {
+ $allowed_phpconfigs = array_map('intval', $allowed_phpconfigs);
+ }
+ }
+ $def_language = \Froxlor\Validate\Validate::validate($def_language, 'default language', '', '', array(), true);
+ $theme = \Froxlor\Validate\Validate::validate($theme, 'theme', '', '', array(), true);
+
+ if (Settings::Get('system.mail_quota_enabled') != '1') {
+ $email_quota = - 1;
+ }
+
+ if (empty($theme)) {
+ $theme = Settings::Get('panel.default_theme');
+ }
+
+ if ($this->isAdmin()) {
+
+ $diskspace = $diskspace * 1024;
+ $traffic = $traffic * 1024 * 1024;
+
+ if (((($this->getUserDetail('diskspace_used') + $diskspace - $result['diskspace']) > $this->getUserDetail('diskspace')) && ($this->getUserDetail('diskspace') / 1024) != '-1') || ((($this->getUserDetail('mysqls_used') + $mysqls - $result['mysqls']) > $this->getUserDetail('mysqls')) && $this->getUserDetail('mysqls') != '-1') || ((($this->getUserDetail('emails_used') + $emails - $result['emails']) > $this->getUserDetail('emails')) && $this->getUserDetail('emails') != '-1') || ((($this->getUserDetail('email_accounts_used') + $email_accounts - $result['email_accounts']) > $this->getUserDetail('email_accounts')) && $this->getUserDetail('email_accounts') != '-1') || ((($this->getUserDetail('email_forwarders_used') + $email_forwarders - $result['email_forwarders']) > $this->getUserDetail('email_forwarders')) && $this->getUserDetail('email_forwarders') != '-1') || ((($this->getUserDetail('email_quota_used') + $email_quota - $result['email_quota']) > $this->getUserDetail('email_quota')) && $this->getUserDetail('email_quota') != '-1' && Settings::Get('system.mail_quota_enabled') == '1') || ((($this->getUserDetail('ftps_used') + $ftps - $result['ftps']) > $this->getUserDetail('ftps')) && $this->getUserDetail('ftps') != '-1') || ((($this->getUserDetail('subdomains_used') + $subdomains - $result['subdomains']) > $this->getUserDetail('subdomains')) && $this->getUserDetail('subdomains') != '-1') || (($diskspace / 1024) == '-1' && ($this->getUserDetail('diskspace') / 1024) != '-1') || ($mysqls == '-1' && $this->getUserDetail('mysqls') != '-1') || ($emails == '-1' && $this->getUserDetail('emails') != '-1') || ($email_accounts == '-1' && $this->getUserDetail('email_accounts') != '-1') || ($email_forwarders == '-1' && $this->getUserDetail('email_forwarders') != '-1') || ($email_quota == '-1' && $this->getUserDetail('email_quota') != '-1' && Settings::Get('system.mail_quota_enabled') == '1') || ($ftps == '-1' && $this->getUserDetail('ftps') != '-1') || ($subdomains == '-1' && $this->getUserDetail('subdomains') != '-1')) {
+ \Froxlor\UI\Response::standard_error('youcantallocatemorethanyouhave', '', true);
+ }
+
+ if ($email == '') {
+ \Froxlor\UI\Response::standard_error(array(
+ 'stringisempty',
+ 'emailadd'
+ ), '', true);
+ } elseif (! \Froxlor\Validate\Validate::validateEmail($email)) {
+ \Froxlor\UI\Response::standard_error('emailiswrong', $email, true);
+ }
+ }
+
+ if ($password != '') {
+ $password = \Froxlor\System\Crypt::validatePassword($password, true);
+ $password = \Froxlor\System\Crypt::makeCryptPassword($password);
+ } else {
+ $password = $result['password'];
+ }
+
+ if ($this->isAdmin()) {
+ if ($createstdsubdomain != '1') {
+ $createstdsubdomain = '0';
+ }
+
+ if ($createstdsubdomain == '1' && $result['standardsubdomain'] == '0') {
+
+ if (Settings::Get('system.stdsubdomain') !== null && Settings::Get('system.stdsubdomain') != '') {
+ $_stdsubdomain = $result['loginname'] . '.' . Settings::Get('system.stdsubdomain');
+ } else {
+ $_stdsubdomain = $result['loginname'] . '.' . Settings::Get('system.hostname');
+ }
+
+ $ins_data = array(
+ 'domain' => $_stdsubdomain,
+ 'customerid' => $result['customerid'],
+ 'adminid' => $this->getUserDetail('adminid'),
+ 'docroot' => $result['documentroot'],
+ 'phpenabled' => $phpenabled,
+ 'openbasedir' => '1'
+ );
+ $domainid = - 1;
+ try {
+ $std_domain = $this->apiCall('Domains.add', $ins_data);
+ $domainid = $std_domain['id'];
+ } catch (\Exception $e) {
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_ERR, "[API] Unable to add standard-subdomain: " . $e->getMessage());
+ }
+
+ if ($domainid > 0) {
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `standardsubdomain` = :domainid WHERE `customerid` = :customerid
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'domainid' => $domainid,
+ 'customerid' => $result['customerid']
+ ), true, true);
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] automatically added standardsubdomain for user '" . $result['loginname'] . "'");
+ \Froxlor\System\Cronjob::inserttask('1');
+ }
+ }
+
+ if ($createstdsubdomain == '0' && $result['standardsubdomain'] != '0') {
+ try {
+ $std_domain = $this->apiCall('Domains.delete', array(
+ 'id' => $result['standardsubdomain'],
+ 'is_stdsubdomain' => 1
+ ));
+ } catch (\Exception $e) {
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_ERR, "[API] Unable to delete standard-subdomain: " . $e->getMessage());
+ }
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] automatically deleted standardsubdomain for user '" . $result['loginname'] . "'");
+ \Froxlor\System\Cronjob::inserttask('1');
+ }
+
+ if ($phpenabled != $result['phpenabled'] || $perlenabled != $result['perlenabled'] || $email != $result['email']) {
+ \Froxlor\System\Cronjob::inserttask('1');
+ }
+
+ // activate/deactivate customer services
+ if ($deactivated != $result['deactivated']) {
+
+ $yesno = ($deactivated ? 'N' : 'Y');
+ $pop3 = ($deactivated ? '0' : (int) $result['pop3']);
+ $imap = ($deactivated ? '0' : (int) $result['imap']);
+
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_MAIL_USERS . "` SET `postfix`= :yesno, `pop3` = :pop3, `imap` = :imap WHERE `customerid` = :customerid
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'yesno' => $yesno,
+ 'pop3' => $pop3,
+ 'imap' => $imap,
+ 'customerid' => $id
+ ));
+
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_FTP_USERS . "` SET `login_enabled` = :yesno WHERE `customerid` = :customerid
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'yesno' => $yesno,
+ 'customerid' => $id
+ ));
+
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `deactivated`= :deactivated WHERE `customerid` = :customerid");
+ Database::pexecute($upd_stmt, array(
+ 'deactivated' => $deactivated,
+ 'customerid' => $id
+ ));
+
+ // Retrieve customer's databases
+ $databases_stmt = Database::prepare("SELECT * FROM " . TABLE_PANEL_DATABASES . " WHERE customerid = :customerid ORDER BY `dbserver`");
+ Database::pexecute($databases_stmt, array(
+ 'customerid' => $id
+ ));
+
+ Database::needRoot(true);
+ $last_dbserver = 0;
+
+ $dbm = new \Froxlor\Database\DbManager($this->logger());
+
+ // For each of them
+ $priv_changed = false;
+ while ($row_database = $databases_stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ if ($last_dbserver != $row_database['dbserver']) {
+ $dbm->getManager()->flushPrivileges();
+ Database::needRoot(true, $row_database['dbserver']);
+ $last_dbserver = $row_database['dbserver'];
+ }
+
+ foreach (array_unique(explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) {
+ $mysql_access_host = trim($mysql_access_host);
+
+ // Prevent access, if deactivated
+ if ($deactivated) {
+ // failsafe if user has been deleted manually (requires MySQL 4.1.2+)
+ $dbm->getManager()->disableUser($row_database['databasename'], $mysql_access_host);
+ } else {
+ // Otherwise grant access
+ $dbm->getManager()->enableUser($row_database['databasename'], $mysql_access_host);
+ }
+ }
+ $priv_changed = true;
+ }
+
+ // At last flush the new privileges
+ if ($priv_changed) {
+ $dbm->getManager()->flushPrivileges();
+ }
+ Database::needRoot(false);
+
+ // reactivate/deactivate api-keys
+ $valid_until = $deactivated ? 0 : - 1;
+ $stmt = Database::prepare("UPDATE `" . TABLE_API_KEYS . "` SET `valid_until` = :vu WHERE `customerid` = :id");
+ Database::pexecute($stmt, array(
+ 'id' => $id,
+ 'vu' => $valid_until
+ ), true, true);
+
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] " . ($deactivated ? 'deactivated' : 'reactivated') . " user '" . $result['loginname'] . "'");
+ \Froxlor\System\Cronjob::inserttask('1');
+ }
+
+ // Disable or enable POP3 Login for customers Mail Accounts
+ if ($email_pop3 != $result['pop3']) {
+ $upd_stmt = Database::prepare("UPDATE `" . TABLE_MAIL_USERS . "` SET `pop3` = :pop3 WHERE `customerid` = :customerid");
+ Database::pexecute($upd_stmt, array(
+ 'pop3' => $email_pop3,
+ 'customerid' => $id
+ ));
+ }
+
+ // Disable or enable IMAP Login for customers Mail Accounts
+ if ($email_imap != $result['imap']) {
+ $upd_stmt = Database::prepare("UPDATE `" . TABLE_MAIL_USERS . "` SET `imap` = :imap WHERE `customerid` = :customerid");
+ Database::pexecute($upd_stmt, array(
+ 'imap' => $email_imap,
+ 'customerid' => $id
+ ));
+ }
+ }
+
+ $upd_data = array(
+ 'customerid' => $id,
+ 'passwd' => $password,
+ 'lang' => $def_language,
+ 'theme' => $theme
+ );
+
+ if ($this->isAdmin()) {
+ $admin_upd_data = array(
+ 'name' => $name,
+ 'firstname' => $firstname,
+ 'gender' => $gender,
+ 'company' => $company,
+ 'street' => $street,
+ 'zipcode' => $zipcode,
+ 'city' => $city,
+ 'phone' => $phone,
+ 'fax' => $fax,
+ 'email' => $email,
+ 'customerno' => $customernumber,
+ 'diskspace' => $diskspace,
+ 'traffic' => $traffic,
+ 'subdomains' => $subdomains,
+ 'emails' => $emails,
+ 'email_accounts' => $email_accounts,
+ 'email_forwarders' => $email_forwarders,
+ 'email_quota' => $email_quota,
+ 'ftps' => $ftps,
+ 'mysqls' => $mysqls,
+ 'deactivated' => $deactivated,
+ 'phpenabled' => $phpenabled,
+ 'allowed_phpconfigs' => empty($allowed_phpconfigs) ? "" : json_encode($allowed_phpconfigs),
+ 'imap' => $email_imap,
+ 'pop3' => $email_pop3,
+ 'perlenabled' => $perlenabled,
+ 'dnsenabled' => $dnsenabled,
+ 'logviewenabled' => $logviewenabled,
+ 'custom_notes' => $custom_notes,
+ 'custom_notes_show' => $custom_notes_show,
+ 'api_allowed' => $api_allowed
+ );
+ $upd_data = $upd_data + $admin_upd_data;
+ }
+
+ $upd_query = "UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET
+ `def_language` = :lang,
+ `password` = :passwd,
+ `theme` = :theme";
+
+ if ($this->isAdmin()) {
+ $admin_upd_query = ",
+ `name` = :name,
+ `firstname` = :firstname,
+ `gender` = :gender,
+ `company` = :company,
+ `street` = :street,
+ `zipcode` = :zipcode,
+ `city` = :city,
+ `phone` = :phone,
+ `fax` = :fax,
+ `email` = :email,
+ `customernumber` = :customerno,
+ `diskspace` = :diskspace,
+ `traffic` = :traffic,
+ `subdomains` = :subdomains,
+ `emails` = :emails,
+ `email_accounts` = :email_accounts,
+ `email_forwarders` = :email_forwarders,
+ `ftps` = :ftps,
+ `mysqls` = :mysqls,
+ `deactivated` = :deactivated,
+ `phpenabled` = :phpenabled,
+ `allowed_phpconfigs` = :allowed_phpconfigs,
+ `email_quota` = :email_quota,
+ `imap` = :imap,
+ `pop3` = :pop3,
+ `perlenabled` = :perlenabled,
+ `dnsenabled` = :dnsenabled,
+ `logviewenabled` = :logviewenabled,
+ `custom_notes` = :custom_notes,
+ `custom_notes_show` = :custom_notes_show,
+ `api_allowed` = :api_allowed";
+ $upd_query .= $admin_upd_query;
+ }
+ $upd_query .= " WHERE `customerid` = :customerid";
+ $upd_stmt = Database::prepare($upd_query);
+ Database::pexecute($upd_stmt, $upd_data);
+
+ if ($this->isAdmin()) {
+ // Using filesystem - quota, insert a task which cleans the filesystem - quota
+ \Froxlor\System\Cronjob::inserttask('10');
+
+ $admin_update_query = "UPDATE `" . TABLE_PANEL_ADMINS . "` SET `customers_used` = `customers_used` ";
+
+ if ($mysqls != '-1' || $result['mysqls'] != '-1') {
+ $admin_update_query .= ", `mysqls_used` = `mysqls_used` ";
+
+ if ($mysqls != '-1') {
+ $admin_update_query .= " + 0" . (int) $mysqls . " ";
+ }
+ if ($result['mysqls'] != '-1') {
+ $admin_update_query .= " - 0" . (int) $result['mysqls'] . " ";
+ }
+ }
+
+ if ($emails != '-1' || $result['emails'] != '-1') {
+ $admin_update_query .= ", `emails_used` = `emails_used` ";
+
+ if ($emails != '-1') {
+ $admin_update_query .= " + 0" . (int) $emails . " ";
+ }
+ if ($result['emails'] != '-1') {
+ $admin_update_query .= " - 0" . (int) $result['emails'] . " ";
+ }
+ }
+
+ if ($email_accounts != '-1' || $result['email_accounts'] != '-1') {
+ $admin_update_query .= ", `email_accounts_used` = `email_accounts_used` ";
+
+ if ($email_accounts != '-1') {
+ $admin_update_query .= " + 0" . (int) $email_accounts . " ";
+ }
+ if ($result['email_accounts'] != '-1') {
+ $admin_update_query .= " - 0" . (int) $result['email_accounts'] . " ";
+ }
+ }
+
+ if ($email_forwarders != '-1' || $result['email_forwarders'] != '-1') {
+ $admin_update_query .= ", `email_forwarders_used` = `email_forwarders_used` ";
+
+ if ($email_forwarders != '-1') {
+ $admin_update_query .= " + 0" . (int) $email_forwarders . " ";
+ }
+ if ($result['email_forwarders'] != '-1') {
+ $admin_update_query .= " - 0" . (int) $result['email_forwarders'] . " ";
+ }
+ }
+
+ if ($email_quota != '-1' || $result['email_quota'] != '-1') {
+ $admin_update_query .= ", `email_quota_used` = `email_quota_used` ";
+
+ if ($email_quota != '-1') {
+ $admin_update_query .= " + 0" . (int) $email_quota . " ";
+ }
+ if ($result['email_quota'] != '-1') {
+ $admin_update_query .= " - 0" . (int) $result['email_quota'] . " ";
+ }
+ }
+
+ if ($subdomains != '-1' || $result['subdomains'] != '-1') {
+ $admin_update_query .= ", `subdomains_used` = `subdomains_used` ";
+
+ if ($subdomains != '-1') {
+ $admin_update_query .= " + 0" . (int) $subdomains . " ";
+ }
+ if ($result['subdomains'] != '-1') {
+ $admin_update_query .= " - 0" . (int) $result['subdomains'] . " ";
+ }
+ }
+
+ if ($ftps != '-1' || $result['ftps'] != '-1') {
+ $admin_update_query .= ", `ftps_used` = `ftps_used` ";
+
+ if ($ftps != '-1') {
+ $admin_update_query .= " + 0" . (int) $ftps . " ";
+ }
+ if ($result['ftps'] != '-1') {
+ $admin_update_query .= " - 0" . (int) $result['ftps'] . " ";
+ }
+ }
+
+ if (($diskspace / 1024) != '-1' || ($result['diskspace'] / 1024) != '-1') {
+ $admin_update_query .= ", `diskspace_used` = `diskspace_used` ";
+
+ if (($diskspace / 1024) != '-1') {
+ $admin_update_query .= " + 0" . (int) $diskspace . " ";
+ }
+ if (($result['diskspace'] / 1024) != '-1') {
+ $admin_update_query .= " - 0" . (int) $result['diskspace'] . " ";
+ }
+ }
+
+ $admin_update_query .= " WHERE `adminid` = '" . (int) $result['adminid'] . "'";
+ Database::query($admin_update_query);
+ }
+
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] edited user '" . $result['loginname'] . "'");
+
+ /*
+ * move customer to another admin/reseller; #1166
+ */
+ if ($this->isAdmin()) {
+ if ($move_to_admin > 0 && $move_to_admin != $result['adminid']) {
+ $move_result = $this->apiCall('Customers.move', array(
+ 'id' => $result['customerid'],
+ 'adminid' => $move_to_admin
+ ));
+ if ($move_result != true) {
+ \Froxlor\UI\Response::standard_error('moveofcustomerfailed', $move_result, true);
+ }
+ }
+ }
+
+ $result = $this->apiCall('Customers.get', array(
+ 'id' => $result['customerid']
+ ));
+ return $this->response(200, "successful", $result);
+ }
+
+ /**
+ * delete a customer entry by either id or loginname
+ *
+ * @param int $id
+ * optional, the customer-id
+ * @param string $loginname
+ * optional, the loginname
+ * @param bool $delete_userfiles
+ * optional, default false
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function delete()
+ {
+ if ($this->isAdmin()) {
+ $id = $this->getParam('id', true, 0);
+ $ln_optional = ($id <= 0 ? false : true);
+ $loginname = $this->getParam('loginname', $ln_optional, '');
+ $delete_userfiles = $this->getParam('delete_userfiles', true, 0);
+
+ $result = $this->apiCall('Customers.get', array(
+ 'id' => $id,
+ 'loginname' => $loginname
+ ));
+ $id = $result['customerid'];
+
+ $databases_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_DATABASES . "`
+ WHERE `customerid` = :id ORDER BY `dbserver`
+ ");
+ Database::pexecute($databases_stmt, array(
+ 'id' => $id
+ ));
+ Database::needRoot(true);
+ $last_dbserver = 0;
+
+ $dbm = new \Froxlor\Database\DbManager($this->logger());
+
+ $priv_changed = false;
+ while ($row_database = $databases_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if ($last_dbserver != $row_database['dbserver']) {
+ Database::needRoot(true, $row_database['dbserver']);
+ $dbm->getManager()->flushPrivileges();
+ $last_dbserver = $row_database['dbserver'];
+ }
+ $dbm->getManager()->deleteDatabase($row_database['databasename']);
+ $priv_changed = true;
+ }
+ if ($priv_changed) {
+ $dbm->getManager()->flushPrivileges();
+ }
+ Database::needRoot(false);
+
+ // delete customer itself
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `customerid` = :id");
+ Database::pexecute($stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ // delete customer databases
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_DATABASES . "` WHERE `customerid` = :id");
+ Database::pexecute($stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ // first gather all domain-id's to clean up panel_domaintoip, dns-entries and certificates accordingly
+ $did_stmt = Database::prepare("SELECT `id` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `customerid` = :id");
+ Database::pexecute($did_stmt, array(
+ 'id' => $id
+ ), true, true);
+ while ($row = $did_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ // remove domain->ip connection
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :did");
+ Database::pexecute($stmt, array(
+ 'did' => $row['id']
+ ), true, true);
+ // remove domain->dns entries
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_DOMAIN_DNS . "` WHERE `domain_id` = :did");
+ Database::pexecute($stmt, array(
+ 'did' => $row['id']
+ ), true, true);
+ // remove domain->certificates entries
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = :did");
+ Database::pexecute($stmt, array(
+ 'did' => $row['id']
+ ), true, true);
+ }
+ // remove customer domains
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `customerid` = :id");
+ Database::pexecute($stmt, array(
+ 'id' => $id
+ ), true, true);
+ $domains_deleted = $stmt->rowCount();
+
+ // delete htpasswds
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_HTPASSWDS . "` WHERE `customerid` = :id");
+ Database::pexecute($stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ // delete htaccess options
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_HTACCESS . "` WHERE `customerid` = :id");
+ Database::pexecute($stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ // delete potential existing sessions
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_SESSIONS . "` WHERE `userid` = :id AND `adminsession` = '0'");
+ Database::pexecute($stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ // delete traffic information
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_TRAFFIC . "` WHERE `customerid` = :id");
+ Database::pexecute($stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ // remove diskspace analysis
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_DISKSPACE . "` WHERE `customerid` = :id");
+ Database::pexecute($stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ // delete mail-accounts
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_MAIL_USERS . "` WHERE `customerid` = :id");
+ Database::pexecute($stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ // delete mail-addresses
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_MAIL_VIRTUAL . "` WHERE `customerid` = :id");
+ Database::pexecute($stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ // gather ftp-user names
+ $result2_stmt = Database::prepare("SELECT `username` FROM `" . TABLE_FTP_USERS . "` WHERE `customerid` = :id");
+ Database::pexecute($result2_stmt, array(
+ 'id' => $id
+ ), true, true);
+ while ($row = $result2_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ // delete ftp-quotatallies by username
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_FTP_QUOTATALLIES . "` WHERE `name` = :name");
+ Database::pexecute($stmt, array(
+ 'name' => $row['username']
+ ), true, true);
+ }
+
+ // remove ftp-group
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_FTP_GROUPS . "` WHERE `customerid` = :id");
+ Database::pexecute($stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ // remove ftp-users
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_FTP_USERS . "` WHERE `customerid` = :id");
+ Database::pexecute($stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ // remove api-keys
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_API_KEYS . "` WHERE `customerid` = :id");
+ Database::pexecute($stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ // Delete all waiting "create user" -tasks for this user, #276
+ // Note: the WHERE selects part of a serialized array, but it should be safe this way
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_PANEL_TASKS . "`
+ WHERE `type` = '2' AND `data` LIKE :loginname
+ ");
+ Database::pexecute($del_stmt, array(
+ 'loginname' => "%:{$result['loginname']};%"
+ ), true, true);
+
+ // update admin-resource-usage
+ Admins::decreaseUsage($this->getUserDetail('adminid'), 'customers_used');
+ Admins::decreaseUsage($this->getUserDetail('adminid'), 'domains_used', '', (int) ($domains_deleted - $result['subdomains_used']));
+
+ if ($result['mysqls'] != '-1') {
+ Admins::decreaseUsage($this->getUserDetail('adminid'), 'mysqls_used', '', (int) $result['mysqls']);
+ }
+
+ if ($result['emails'] != '-1') {
+ Admins::decreaseUsage($this->getUserDetail('adminid'), 'emails_used', '', (int) $result['emails']);
+ }
+
+ if ($result['email_accounts'] != '-1') {
+ Admins::decreaseUsage($this->getUserDetail('adminid'), 'email_accounts_used', '', (int) $result['email_accounts']);
+ }
+
+ if ($result['email_forwarders'] != '-1') {
+ Admins::decreaseUsage($this->getUserDetail('adminid'), 'email_forwarders_used', '', (int) $result['email_forwarders']);
+ }
+
+ if ($result['email_quota'] != '-1') {
+ Admins::decreaseUsage($this->getUserDetail('adminid'), 'email_quota_used', '', (int) $result['email_quota']);
+ }
+
+ if ($result['subdomains'] != '-1') {
+ Admins::decreaseUsage($this->getUserDetail('adminid'), 'subdomains_used', '', (int) $result['subdomains']);
+ }
+
+ if ($result['ftps'] != '-1') {
+ Admins::decreaseUsage($this->getUserDetail('adminid'), 'ftps_used', '', (int) $result['ftps']);
+ }
+
+ if (($result['diskspace'] / 1024) != '-1') {
+ Admins::decreaseUsage($this->getUserDetail('adminid'), 'diskspace_used', '', (int) $result['diskspace']);
+ }
+
+ // rebuild configs
+ \Froxlor\System\Cronjob::inserttask('1');
+
+ // Using nameserver, insert a task which rebuilds the server config
+ \Froxlor\System\Cronjob::inserttask('4');
+
+ if ($delete_userfiles == 1) {
+ // insert task to remove the customers files from the filesystem
+ \Froxlor\System\Cronjob::inserttask('6', $result['loginname']);
+ }
+
+ // Using filesystem - quota, insert a task which cleans the filesystem - quota
+ \Froxlor\System\Cronjob::inserttask('10');
+
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] deleted customer '" . $result['loginname'] . "'");
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * unlock a locked customer by either id or loginname
+ *
+ * @param int $id
+ * optional, the customer-id
+ * @param string $loginname
+ * optional, the loginname
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function unlock()
+ {
+ if ($this->isAdmin()) {
+ $id = $this->getParam('id', true, 0);
+ $ln_optional = ($id <= 0 ? false : true);
+ $loginname = $this->getParam('loginname', $ln_optional, '');
+
+ $result = $this->apiCall('Customers.get', array(
+ 'id' => $id,
+ 'loginname' => $loginname
+ ));
+ $id = $result['customerid'];
+
+ $result_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET
+ `loginfail_count` = '0'
+ WHERE `customerid`= :id
+ ");
+ Database::pexecute($result_stmt, array(
+ 'id' => $id
+ ), true, true);
+ // set the new value for result-array
+ $result['loginfail_count'] = 0;
+
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] unlocked customer '" . $result['loginname'] . "'");
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * Function to move a given customer to a given admin/reseller
+ * and update all its references accordingly
+ *
+ * @param int $id
+ * optional, the customer-id
+ * @param string $loginname
+ * optional, the loginname
+ * @param int $adminid
+ * target-admin-id
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function move()
+ {
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) {
+ $adminid = $this->getParam('adminid');
+ $id = $this->getParam('id', true, 0);
+ $ln_optional = ($id <= 0 ? false : true);
+ $loginname = $this->getParam('loginname', $ln_optional, '');
+
+ $c_result = $this->apiCall('Customers.get', array(
+ 'id' => $id,
+ 'loginname' => $loginname
+ ));
+ $id = $c_result['customerid'];
+
+ // check if target-admin is the current admin
+ if ($adminid == $c_result['adminid']) {
+ throw new \Exception("Cannot move customer to the same admin/reseller as he currently is assigned to", 406);
+ }
+
+ // get target admin
+ $a_result = $this->apiCall('Admins.get', array(
+ 'id' => $adminid
+ ));
+
+ // Update customer entry
+ $updCustomer_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `adminid` = :adminid WHERE `customerid` = :cid
+ ");
+ Database::pexecute($updCustomer_stmt, array(
+ 'adminid' => $adminid,
+ 'cid' => $id
+ ), true, true);
+
+ // Update customer-domains
+ $updDomains_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `adminid` = :adminid WHERE `customerid` = :cid
+ ");
+ Database::pexecute($updDomains_stmt, array(
+ 'adminid' => $adminid,
+ 'cid' => $id
+ ), true, true);
+
+ // now, recalculate the resource-usage for the old and the new admin
+ \Froxlor\User::updateCounters(false);
+
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] moved user '" . $c_result['loginname'] . "' from admin/reseller '" . $c_result['adminname'] . " to admin/reseller '" . $a_result['loginname'] . "'");
+
+ $result = $this->apiCall('Customers.get', array(
+ 'id' => $c_result['customerid']
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * increase resource-usage
+ *
+ * @param int $customerid
+ * @param string $resource
+ * @param string $extra
+ * optional, default empty
+ * @param int $increase_by
+ * optional, default 1
+ */
+ public static function increaseUsage($customerid = 0, $resource = null, $extra = '', $increase_by = 1)
+ {
+ self::updateResourceUsage(TABLE_PANEL_CUSTOMERS, 'customerid', $customerid, '+', $resource, $extra, $increase_by);
+ }
+
+ /**
+ * decrease resource-usage
+ *
+ * @param int $customerid
+ * @param string $resource
+ * @param string $extra
+ * optional, default empty
+ * @param int $decrease_by
+ * optional, default 1
+ */
+ public static function decreaseUsage($customerid = 0, $resource = null, $extra = '', $decrease_by = 1)
+ {
+ self::updateResourceUsage(TABLE_PANEL_CUSTOMERS, 'customerid', $customerid, '-', $resource, $extra, $decrease_by);
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/DirOptions.php b/lib/Froxlor/Api/Commands/DirOptions.php
new file mode 100644
index 00000000..8fb44e9a
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/DirOptions.php
@@ -0,0 +1,460 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class DirOptions extends \Froxlor\Api\ApiCommand implements \Froxlor\Api\ResourceEntity
+{
+
+ /**
+ * add options for a given directory
+ *
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ * @param string $path
+ * path relative to the customer's home-Directory
+ * @param bool $options_indexes
+ * optional, activate directory-listing for this path, default 0 (false)
+ * @param bool $options_cgi
+ * optional, allow Perl/CGI execution, default 0 (false)
+ * @param string $error404path
+ * optional, custom 404 error string/file
+ * @param string $error403path
+ * optional, custom 403 error string/file
+ * @param string $error500path
+ * optional, custom 500 error string/file
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function add()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras.pathoptions')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ // get needed customer info to reduce the email-address-counter by one
+ $customer = $this->getCustomerData();
+
+ // required parameters
+ $path = $this->getParam('path');
+
+ // parameters
+ $options_indexes = $this->getBoolParam('options_indexes', true, 0);
+ $options_cgi = $this->getBoolParam('options_cgi', true, 0);
+ $error404path = $this->getParam('error404path', true, '');
+ $error403path = $this->getParam('error403path', true, '');
+ $error500path = $this->getParam('error500path', true, '');
+
+ // validation
+ $path = \Froxlor\FileDir::makeCorrectDir(\Froxlor\Validate\Validate::validate($path, 'path', \Froxlor\Validate\Validate::REGEX_DIR, '', array(), true));
+ $userpath = $path;
+ $path = \Froxlor\FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path);
+
+ if (! empty($error404path)) {
+ $error404path = $this->correctErrorDocument($error404path, true);
+ }
+
+ if (! empty($error403path)) {
+ $error403path = $this->correctErrorDocument($error403path, true);
+ }
+
+ if (! empty($error500path)) {
+ $error500path = $this->correctErrorDocument($error500path, true);
+ }
+
+ // check for duplicate path
+ $path_dupe_check_stmt = Database::prepare("
+ SELECT `id`, `path` FROM `" . TABLE_PANEL_HTACCESS . "`
+ WHERE `path`= :path AND `customerid`= :customerid
+ ");
+ $path_dupe_check = Database::pexecute_first($path_dupe_check_stmt, array(
+ "path" => $path,
+ "customerid" => $customer['customerid']
+ ), true, true);
+
+ // duplicate check
+ if ($path_dupe_check && $path_dupe_check['path'] == $path) {
+ \Froxlor\UI\Response::standard_error('errordocpathdupe', $userpath, true);
+ }
+
+ // insert the entry
+ $stmt = Database::prepare('
+ INSERT INTO `' . TABLE_PANEL_HTACCESS . '` SET
+ `customerid` = :customerid,
+ `path` = :path,
+ `options_indexes` = :options_indexes,
+ `error404path` = :error404path,
+ `error403path` = :error403path,
+ `error500path` = :error500path,
+ `options_cgi` = :options_cgi
+ ');
+ $params = array(
+ "customerid" => $customer['customerid'],
+ "path" => $path,
+ "options_indexes" => $options_indexes,
+ "error403path" => $error403path,
+ "error404path" => $error404path,
+ "error500path" => $error500path,
+ "options_cgi" => $options_cgi
+ );
+ Database::pexecute($stmt, $params, true, true);
+ $id = Database::lastInsertId();
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] added directory-option for '" . $userpath . "'");
+ \Froxlor\System\Cronjob::inserttask('1');
+
+ $result = $this->apiCall('DirOptions.get', array(
+ 'id' => $id
+ ));
+ return $this->response(200, "successful", $result);
+ }
+
+ /**
+ * return a directory-protection entry by id
+ *
+ * @param int $id
+ * id of dir-protection entry
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function get()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ $id = $this->getParam('id', true, 0);
+
+ $params = array();
+ if ($this->isAdmin()) {
+ if ($this->getUserDetail('customers_see_all') == false) {
+ // if it's a reseller or an admin who cannot see all customers, we need to check
+ // whether the database belongs to one of his customers
+ $_custom_list_result = $this->apiCall('Customers.listing');
+ $custom_list_result = $_custom_list_result['list'];
+ $customer_ids = array();
+ foreach ($custom_list_result as $customer) {
+ $customer_ids[] = $customer['customerid'];
+ }
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_HTACCESS . "`
+ WHERE `customerid` IN (" . implode(", ", $customer_ids) . ")
+ AND `id` = :id
+ ");
+ } else {
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_HTACCESS . "`
+ WHERE `id` = :id
+ ");
+ }
+ } else {
+ if (Settings::IsInList('panel.customer_hide_options', 'extras.pathoptions')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_HTACCESS . "`
+ WHERE `customerid` = :customerid
+ AND `id` = :id
+ ");
+ $params['customerid'] = $this->getUserDetail('customerid');
+ }
+ $params['id'] = $id;
+ $result = Database::pexecute_first($result_stmt, $params, true, true);
+ if ($result) {
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] get directory options for '" . $result['path'] . "'");
+ return $this->response(200, "successful", $result);
+ }
+ $key = "id #" . $id;
+ throw new \Exception("Directory option with " . $key . " could not be found", 404);
+ }
+
+ /**
+ * update options for a given directory by id
+ *
+ * @param int $id
+ * id of dir-protection entry
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ * @param bool $options_indexes
+ * optional, activate directory-listing for this path, default 0 (false)
+ * @param bool $options_cgi
+ * optional, allow Perl/CGI execution, default 0 (false)
+ * @param string $error404path
+ * optional, custom 404 error string/file
+ * @param string $error403path
+ * optional, custom 403 error string/file
+ * @param string $error500path
+ * optional, custom 500 error string/file
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function update()
+ {
+ $id = $this->getParam('id', true, 0);
+
+ // validation
+ $result = $this->apiCall('DirOptions.get', array(
+ 'id' => $id
+ ));
+
+ // get needed customer info to reduce the email-address-counter by one
+ $customer = $this->getCustomerData();
+
+ // parameters
+ $options_indexes = $this->getBoolParam('options_indexes', true, $result['options_indexes']);
+ $options_cgi = $this->getBoolParam('options_cgi', true, $result['options_cgi']);
+ $error404path = $this->getParam('error404path', true, $result['error404path']);
+ $error403path = $this->getParam('error403path', true, $result['error403path']);
+ $error500path = $this->getParam('error500path', true, $result['error500path']);
+
+ if (! empty($error404path)) {
+ $error404path = $this->correctErrorDocument($error404path, true);
+ }
+
+ if (! empty($error403path)) {
+ $error403path = $this->correctErrorDocument($error403path, true);
+ }
+
+ if (! empty($error500path)) {
+ $error500path = $this->correctErrorDocument($error500path, true);
+ }
+
+ if (($options_indexes != $result['options_indexes']) || ($error404path != $result['error404path']) || ($error403path != $result['error403path']) || ($error500path != $result['error500path']) || ($options_cgi != $result['options_cgi'])) {
+ \Froxlor\System\Cronjob::inserttask('1');
+ $stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_HTACCESS . "`
+ SET `options_indexes` = :options_indexes,
+ `error404path` = :error404path,
+ `error403path` = :error403path,
+ `error500path` = :error500path,
+ `options_cgi` = :options_cgi
+ WHERE `customerid` = :customerid
+ AND `id` = :id
+ ");
+ $params = array(
+ "customerid" => $customer['customerid'],
+ "options_indexes" => $options_indexes,
+ "error403path" => $error403path,
+ "error404path" => $error404path,
+ "error500path" => $error500path,
+ "options_cgi" => $options_cgi,
+ "id" => $id
+ );
+ Database::pexecute($stmt, $params, true, true);
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] edited directory options for '" . str_replace($customer['documentroot'], '/', $result['path']) . "'");
+ }
+
+ $result = $this->apiCall('DirOptions.get', array(
+ 'id' => $id
+ ));
+ return $this->response(200, "successful", $result);
+ }
+
+ /**
+ * list all directory-options, if called from an admin, list all directory-options of all customers you are allowed to view, or specify id or loginname for one specific customer
+ *
+ * @param int $customerid
+ * optional, admin-only, select directory-protections of a specific customer by id
+ * @param string $loginname
+ * optional, admin-only, select directory-protections of a specific customer by loginname
+ * @param array $sql_search
+ * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), LIKE is used if left empty and 'value' => searchvalue
+ * @param int $sql_limit
+ * optional specify number of results to be returned
+ * @param int $sql_offset
+ * optional specify offset for resultset
+ * @param array $sql_orderby
+ * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more fields
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listing()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+ $customer_ids = $this->getAllowedCustomerIds('extras.pathoptions');
+
+ $result = array();
+ $query_fields = array();
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_HTACCESS . "`
+ WHERE `customerid` IN (" . implode(', ', $customer_ids) . ")" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit());
+ Database::pexecute($result_stmt, $query_fields, true, true);
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = $row;
+ }
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] list directory-options");
+ return $this->response(200, "successful", array(
+ 'count' => count($result),
+ 'list' => $result
+ ));
+ }
+
+ /**
+ * returns the total number of accessable directory options
+ *
+ * @param int $customerid
+ * optional, admin-only, select directory-protections of a specific customer by id
+ * @param string $loginname
+ * optional, admin-only, select directory-protections of a specific customer by loginname
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listingCount()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+ $customer_ids = $this->getAllowedCustomerIds('extras.pathoptions');
+
+ $result = array();
+ $result_stmt = Database::prepare("
+ SELECT COUNT(*) as num_htaccess FROM `" . TABLE_PANEL_HTACCESS . "`
+ WHERE `customerid` IN (" . implode(', ', $customer_ids) . ")
+ ");
+ $result = Database::pexecute_first($result_stmt, null, true, true);
+ if ($result) {
+ return $this->response(200, "successful", $result['num_htaccess']);
+ }
+ }
+
+ /**
+ * delete a directory-options by id
+ *
+ * @param int $id
+ * id of dir-protection entry
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function delete()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ $id = $this->getParam('id');
+
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras.pathoptions')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ // get directory-option
+ $result = $this->apiCall('DirOptions.get', array(
+ 'id' => $id
+ ));
+
+ if ($this->isAdmin()) {
+ // get customer-data
+ $customer_data = $this->apiCall('Customers.get', array(
+ 'id' => $result['customerid']
+ ));
+ } else {
+ $customer_data = $this->getUserData();
+ }
+
+ // do we have to remove the symlink and folder in suexecpath?
+ if ((int) Settings::Get('perl.suexecworkaround') == 1) {
+ $loginname = $customer_data['loginname'];
+ $suexecpath = \Froxlor\FileDir::makeCorrectDir(Settings::Get('perl.suexecpath') . '/' . $loginname . '/' . md5($result['path']) . '/');
+ $perlsymlink = \Froxlor\FileDir::makeCorrectFile($result['path'] . '/cgi-bin');
+ // remove symlink
+ if (file_exists($perlsymlink)) {
+ \Froxlor\FileDir::safe_exec('rm -f ' . escapeshellarg($perlsymlink));
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_DEBUG, "[API] deleted suexecworkaround symlink '" . $perlsymlink . "'");
+ }
+ // remove folder in suexec-path
+ if (file_exists($suexecpath)) {
+ \Froxlor\FileDir::safe_exec('rm -rf ' . escapeshellarg($suexecpath));
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_DEBUG, "[API] deleted suexecworkaround path '" . $suexecpath . "'");
+ }
+ }
+ $stmt = Database::prepare("
+ DELETE FROM `" . TABLE_PANEL_HTACCESS . "`
+ WHERE `customerid`= :customerid
+ AND `id`= :id
+ ");
+ Database::pexecute($stmt, array(
+ "customerid" => $customer_data['customerid'],
+ "id" => $id
+ ), true, true);
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] deleted directory-option for '" . str_replace($customer_data['documentroot'], '/', $result['path']) . "'");
+ \Froxlor\System\Cronjob::inserttask('1');
+ return $this->response(200, "successful", $result);
+ }
+
+ /**
+ * this functions validates a given value as ErrorDocument
+ * refs #267
+ *
+ * @param
+ * string error-document-string
+ * @param bool $throw_exception
+ *
+ * @return string error-document-string
+ *
+ */
+ private function correctErrorDocument($errdoc = null, $throw_exception = false)
+ {
+ if ($errdoc !== null && $errdoc != '') {
+ // not a URL
+ if ((strtoupper(substr($errdoc, 0, 5)) != 'HTTP:' && strtoupper(substr($errdoc, 0, 6)) != 'HTTPS:') || ! \Froxlor\Validate\Validate::validateUrl($errdoc)) {
+ // a file
+ if (substr($errdoc, 0, 1) != '"') {
+ $errdoc = \Froxlor\FileDir::makeCorrectFile($errdoc);
+ // apache needs a starting-slash (starting at the domains-docroot)
+ if (! substr($errdoc, 0, 1) == '/') {
+ $errdoc = '/' . $errdoc;
+ }
+ } else {
+ // a string (check for ending ")
+ // string won't work for lighty
+ if (Settings::Get('system.webserver') == 'lighttpd') {
+ \Froxlor\UI\Response::standard_error('stringerrordocumentnotvalidforlighty', '', $throw_exception);
+ } elseif (substr($errdoc, - 1) != '"') {
+ $errdoc .= '"';
+ }
+ }
+ } else {
+ if (Settings::Get('system.webserver') == 'lighttpd') {
+ \Froxlor\UI\Response::standard_error('urlerrordocumentnotvalidforlighty', '', $throw_exception);
+ }
+ }
+ }
+ return $errdoc;
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/DirProtections.php b/lib/Froxlor/Api/Commands/DirProtections.php
new file mode 100644
index 00000000..b79785ef
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/DirProtections.php
@@ -0,0 +1,391 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class DirProtections extends \Froxlor\Api\ApiCommand implements \Froxlor\Api\ResourceEntity
+{
+
+ /**
+ * add htaccess protection to a given directory
+ *
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ * @param string $path
+ * @param string $username
+ * @param string $directory_password
+ * @param string $directory_authname
+ * optional name/description for the protection
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function add()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras.directoryprotection')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ // get needed customer info to reduce the email-address-counter by one
+ $customer = $this->getCustomerData();
+
+ // required parameters
+ $path = $this->getParam('path');
+ $username = $this->getParam('username');
+ $password = $this->getParam('directory_password');
+
+ // parameters
+ $authname = $this->getParam('directory_authname', true, '');
+
+ // validation
+ $path = \Froxlor\FileDir::makeCorrectDir(\Froxlor\Validate\Validate::validate($path, 'path', \Froxlor\Validate\Validate::REGEX_DIR, '', array(), true));
+ $path = \Froxlor\FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path);
+ $username = \Froxlor\Validate\Validate::validate($username, 'username', '/^[a-zA-Z0-9][a-zA-Z0-9\-_]+\$?$/', '', array(), true);
+ $authname = \Froxlor\Validate\Validate::validate($authname, 'directory_authname', '/^[a-zA-Z0-9][a-zA-Z0-9\-_ ]+\$?$/', '', array(), true);
+ \Froxlor\Validate\Validate::validate($password, 'password', '', '', array(), true);
+
+ // check for duplicate usernames for the path
+ $username_path_check_stmt = Database::prepare("
+ SELECT `id`, `username`, `path` FROM `" . TABLE_PANEL_HTPASSWDS . "`
+ WHERE `username`= :username AND `path`= :path AND `customerid`= :customerid
+ ");
+ $params = array(
+ "username" => $username,
+ "path" => $path,
+ "customerid" => $customer['customerid']
+ );
+ $username_path_check = Database::pexecute_first($username_path_check_stmt, $params, true, true);
+
+ $password_enc = \Froxlor\System\Crypt::makeCryptPassword($password, true);
+
+ // duplicate check
+ if ($username_path_check && $username_path_check['username'] == $username && $username_path_check['path'] == $path) {
+ \Froxlor\UI\Response::standard_error('userpathcombinationdupe', '', true);
+ } elseif ($password == $username) {
+ \Froxlor\UI\Response::standard_error('passwordshouldnotbeusername', '', true);
+ }
+
+ // insert the entry
+ $stmt = Database::prepare("
+ INSERT INTO `" . TABLE_PANEL_HTPASSWDS . "` SET
+ `customerid` = :customerid,
+ `username` = :username,
+ `password` = :password,
+ `path` = :path,
+ `authname` = :authname
+ ");
+ $params = array(
+ "customerid" => $customer['customerid'],
+ "username" => $username,
+ "password" => $password_enc,
+ "path" => $path,
+ "authname" => $authname
+ );
+ Database::pexecute($stmt, $params, true, true);
+ $id = Database::lastInsertId();
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] added directory-protection for '" . $username . " (" . $path . ")'");
+ \Froxlor\System\Cronjob::inserttask('1');
+
+ $result = $this->apiCall('DirProtections.get', array(
+ 'id' => $id
+ ));
+ return $this->response(200, "successful", $result);
+ }
+
+ /**
+ * return a directory-protection entry by either id or username
+ *
+ * @param int $id
+ * optional, the directory-protection-id
+ * @param string $username
+ * optional, the username
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function get()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ $id = $this->getParam('id', true, 0);
+ $un_optional = ($id <= 0 ? false : true);
+ $username = $this->getParam('username', $un_optional, '');
+
+ $params = array();
+ if ($this->isAdmin()) {
+ if ($this->getUserDetail('customers_see_all') == false) {
+ // if it's a reseller or an admin who cannot see all customers, we need to check
+ // whether the database belongs to one of his customers
+ $_custom_list_result = $this->apiCall('Customers.listing');
+ $custom_list_result = $_custom_list_result['list'];
+ $customer_ids = array();
+ foreach ($custom_list_result as $customer) {
+ $customer_ids[] = $customer['customerid'];
+ }
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_HTPASSWDS . "`
+ WHERE `customerid` IN (" . implode(", ", $customer_ids) . ")
+ AND (`id` = :idun OR `username` = :idun)
+ ");
+ } else {
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_HTPASSWDS . "`
+ WHERE (`id` = :idun OR `username` = :idun)
+ ");
+ }
+ } else {
+ if (Settings::IsInList('panel.customer_hide_options', 'extras.directoryprotection')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_HTPASSWDS . "`
+ WHERE `customerid` = :customerid
+ AND (`id` = :idun OR `username` = :idun)
+ ");
+ $params['customerid'] = $this->getUserDetail('customerid');
+ }
+ $params['idun'] = ($id <= 0 ? $username : $id);
+ $result = Database::pexecute_first($result_stmt, $params, true, true);
+ if ($result) {
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] get directory protection for '" . $result['path'] . "'");
+ return $this->response(200, "successful", $result);
+ }
+ $key = ($id > 0 ? "id #" . $id : "username '" . $username . "'");
+ throw new \Exception("Directory protection with " . $key . " could not be found", 404);
+ }
+
+ /**
+ * update htaccess protection of a given directory
+ *
+ * @param int $id
+ * optional the directory-protection-id
+ * @param string $username
+ * optional, the username
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ * @param string $directory_password
+ * optional, leave empty for no change
+ * @param string $directory_authname
+ * optional name/description for the protection
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function update()
+ {
+ $id = $this->getParam('id', true, 0);
+ $un_optional = ($id <= 0 ? false : true);
+ $username = $this->getParam('username', $un_optional, '');
+
+ // validation
+ $result = $this->apiCall('DirProtections.get', array(
+ 'id' => $id,
+ 'username' => $username
+ ));
+ $id = $result['id'];
+
+ // parameters
+ $password = $this->getParam('directory_password', true, '');
+ $authname = $this->getParam('directory_authname', true, $result['authname']);
+
+ // get needed customer info
+ $customer = $this->getCustomerData();
+
+ // validation
+ $authname = \Froxlor\Validate\Validate::validate($authname, 'directory_authname', '/^[a-zA-Z0-9][a-zA-Z0-9\-_ ]+\$?$/', '', array(), true);
+ \Froxlor\Validate\Validate::validate($password, 'password', '', '', array(), true);
+
+ $upd_query = "";
+ $upd_params = array(
+ "id" => $result['id'],
+ "cid" => $customer['customerid']
+ );
+ if (! empty($password)) {
+ if ($password == $result['username']) {
+ \Froxlor\UI\Response::standard_error('passwordshouldnotbeusername', '', true);
+ }
+ $password_enc = \Froxlor\System\Crypt::makeCryptPassword($password, true);
+
+ $upd_query .= "`password`= :password_enc";
+ $upd_params['password_enc'] = $password_enc;
+ }
+ if ($authname != $result['authname']) {
+ if (! empty($upd_query)) {
+ $upd_query .= ", ";
+ }
+ $upd_query .= "`authname` = :authname";
+ $upd_params['authname'] = $authname;
+ }
+
+ // build update query
+ if (! empty($upd_query)) {
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_HTPASSWDS . "` SET " . $upd_query . " WHERE `id` = :id AND `customerid`= :cid
+ ");
+ Database::pexecute($upd_stmt, $upd_params, true, true);
+ \Froxlor\System\Cronjob::inserttask('1');
+ }
+
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] updated directory-protection '" . $result['username'] . " (" . $result['path'] . ")'");
+ $result = $this->apiCall('DirProtections.get', array(
+ 'id' => $result['id']
+ ));
+ return $this->response(200, "successful", $result);
+ }
+
+ /**
+ * list all directory-protections, if called from an admin, list all directory-protections of all customers you are allowed to view, or specify id or loginname for one specific customer
+ *
+ * @param int $customerid
+ * optional, admin-only, select directory-protections of a specific customer by id
+ * @param string $loginname
+ * optional, admin-only, select directory-protections of a specific customer by loginname
+ * @param array $sql_search
+ * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), LIKE is used if left empty and 'value' => searchvalue
+ * @param int $sql_limit
+ * optional specify number of results to be returned
+ * @param int $sql_offset
+ * optional specify offset for resultset
+ * @param array $sql_orderby
+ * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more fields
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listing()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+ $customer_ids = $this->getAllowedCustomerIds('extras.directoryprotection');
+
+ $result = array();
+ $query_fields = array();
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_HTPASSWDS . "`
+ WHERE `customerid` IN (" . implode(', ', $customer_ids) . ")" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit());
+ Database::pexecute($result_stmt, $query_fields, true, true);
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = $row;
+ }
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] list directory-protections");
+ return $this->response(200, "successful", array(
+ 'count' => count($result),
+ 'list' => $result
+ ));
+ }
+
+ /**
+ * returns the total number of accessable directory protections
+ *
+ * @param int $customerid
+ * optional, admin-only, select directory-protections of a specific customer by id
+ * @param string $loginname
+ * optional, admin-only, select directory-protections of a specific customer by loginname
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listingCount()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+ $customer_ids = $this->getAllowedCustomerIds('extras.directoryprotection');
+
+ $result = array();
+ $result_stmt = Database::prepare("
+ SELECT COUNT(*) as num_htpasswd FROM `" . TABLE_PANEL_HTPASSWDS . "`
+ WHERE `customerid` IN (" . implode(', ', $customer_ids) . ")
+ ");
+ $result = Database::pexecute_first($result_stmt, null, true, true);
+ if ($result) {
+ return $this->response(200, "successful", $result['num_htpasswd']);
+ }
+ }
+
+ /**
+ * delete a directory-protection by either id or username
+ *
+ * @param int $id
+ * optional, the directory-protection-id
+ * @param string $username
+ * optional, the username
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function delete()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ $id = $this->getParam('id', true, 0);
+ $un_optional = ($id <= 0 ? false : true);
+ $username = $this->getParam('username', $un_optional, '');
+
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras.directoryprotection')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ // get directory protection
+ $result = $this->apiCall('DirProtections.get', array(
+ 'id' => $id,
+ 'username' => $username
+ ));
+ $id = $result['id'];
+
+ if ($this->isAdmin()) {
+ // get customer-data
+ $customer_data = $this->apiCall('Customers.get', array(
+ 'id' => $result['customerid']
+ ));
+ } else {
+ $customer_data = $this->getUserData();
+ }
+
+ $stmt = Database::prepare("
+ DELETE FROM `" . TABLE_PANEL_HTPASSWDS . "` WHERE `customerid`= :customerid AND `id`= :id
+ ");
+ Database::pexecute($stmt, array(
+ "customerid" => $customer_data['customerid'],
+ "id" => $id
+ ));
+
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] deleted htpasswd for '" . $result['username'] . " (" . $result['path'] . ")'");
+ \Froxlor\System\Cronjob::inserttask('1');
+ return $this->response(200, "successful", $result);
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/DomainZones.php b/lib/Froxlor/Api/Commands/DomainZones.php
new file mode 100644
index 00000000..ddd45dfe
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/DomainZones.php
@@ -0,0 +1,522 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class DomainZones extends \Froxlor\Api\ApiCommand implements \Froxlor\Api\ResourceEntity
+{
+
+ /**
+ * add a new dns zone for a given domain by id or domainname
+ *
+ * @param int $id
+ * optional domain id
+ * @param string $domainname
+ * optional domain name
+ * @param string $record
+ * optional, default empty
+ * @param string $type
+ * optional, zone-entry type (A, AAAA, TXT, etc.), default 'A'
+ * @param int $prio
+ * optional, priority, default empty
+ * @param string $content
+ * optional, default empty
+ * @param int $ttl
+ * optional, default 18000
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function add()
+ {
+ if (Settings::Get('system.dnsenabled') != '1') {
+ throw new \Exception("DNS service not enabled on this system", 405);
+ }
+
+ if ($this->isAdmin() == false && $this->getUserDetail('dnsenabled') != '1') {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ $id = $this->getParam('id', true, 0);
+ $dn_optional = ($id <= 0 ? false : true);
+ $domainname = $this->getParam('domainname', $dn_optional, '');
+
+ // get requested domain
+ $result = $this->apiCall('SubDomains.get', array(
+ 'id' => $id,
+ 'domainname' => $domainname
+ ));
+ $id = $result['id'];
+
+ // parameters
+ $record = $this->getParam('record', true, null);
+ $type = $this->getParam('type', true, 'A');
+ $prio = $this->getParam('prio', true, null);
+ $content = $this->getParam('content', true, null);
+ $ttl = $this->getParam('ttl', true, 18000);
+
+ if ($result['parentdomainid'] != '0') {
+ throw new \Exception("DNS zones can only be generated for the main domain, not for subdomains", 406);
+ }
+
+ if ($result['subisbinddomain'] != '1') {
+ \Froxlor\UI\Response::standard_error('dns_domain_nodns', '', true);
+ }
+
+ $idna_convert = new \Froxlor\Idna\IdnaWrapper();
+ $domain = $idna_convert->encode($result['domain']);
+
+ // select all entries
+ $sel_stmt = Database::prepare("SELECT * FROM `" . TABLE_DOMAIN_DNS . "` WHERE domain_id = :did");
+ Database::pexecute($sel_stmt, array(
+ 'did' => $id
+ ), true, true);
+ $dom_entries = $sel_stmt->fetchAll(\PDO::FETCH_ASSOC);
+
+ // validation
+ $errors = array();
+ if (empty($record)) {
+ $record = "@";
+ }
+
+ $record = trim(strtolower($record));
+
+ if ($record != '@' && $record != '*') {
+ // validate record
+ if (strpos($record, '--') !== false) {
+ $errors[] = $this->lng['error']['domain_nopunycode'];
+ } else {
+ // check for wildcard-record
+ $add_wildcard_again = false;
+ if (substr($record, 0, 2) == '*.') {
+ $record = substr($record, 2);
+ $add_wildcard_again = true;
+ }
+ // convert entry
+ $record = $idna_convert->encode($record);
+
+ if ($add_wildcard_again) {
+ $record = '*.' . $record;
+ }
+
+ if (strlen($record) > 63) {
+ $errors[] = $this->lng['error']['dns_record_toolong'];
+ }
+ }
+ }
+
+ // TODO regex validate content for invalid characters
+
+ if ($ttl <= 0) {
+ $ttl = 18000;
+ }
+
+ $content = trim($content);
+ if (empty($content)) {
+ $errors[] = $this->lng['error']['dns_content_empty'];
+ }
+
+ // types
+ if ($type == 'A' && filter_var($content, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false) {
+ $errors[] = $this->lng['error']['dns_arec_noipv4'];
+ } elseif ($type == 'AAAA' && filter_var($content, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) === false) {
+ $errors[] = $this->lng['error']['dns_aaaarec_noipv6'];
+ } elseif ($type == 'CAA' && ! empty($content)) {
+ $re = '/(?\'critical\'\d)\h*(?\'type\'iodef|issue|issuewild)\h*(?\'value\'(?\'issuevalue\'"(?\'domain\'(?=.{3,128}$)(?>(?>[a-zA-Z0-9]+[a-zA-Z0-9-]*[a-zA-Z0-9]+|[a-zA-Z0-9]+)\.)*(?>[a-zA-Z]{2,}|[a-zA-Z0-9]{2,}\.[a-zA-Z]{2,}))[;\h]*(?\'parameters\'(?>[a-zA-Z0-9]{1,60}=[a-zA-Z0-9]{1,60}\h*)+)?")|(?\'iodefvalue\'"(?\'url\'(mailto:.*|http:\/\/.*|https:\/\/.*))"))/';
+ preg_match($re, $content, $matches);
+
+ if (empty($matches)) {
+ $errors[] = $this->lng['error']['dns_content_invalid'];
+ } elseif (($matches['type'] == 'issue' || $matches['type'] == 'issuewild') && ! \Froxlor\Validate\Validate::validateDomain($matches['domain'])) {
+ $errors[] = $this->lng['error']['dns_content_invalid'];
+ } elseif ($matches['type'] == 'iodef' && ! \Froxlor\Validate\Validate::validateUrl($matches['url'])) {
+ $errors[] = $this->lng['error']['dns_content_invalid'];
+ } else {
+ $content = $matches[0];
+ }
+ } elseif ($type == 'CNAME' || $type == 'DNAME') {
+ // check for trailing dot
+ if (substr($content, - 1) == '.') {
+ // remove it for checks
+ $content = substr($content, 0, - 1);
+ } else {
+ // add domain name
+ $content .= '.' . $domain;
+ }
+ if (! \Froxlor\Validate\Validate::validateDomain($content, true)) {
+ $errors[] = $this->lng['error']['dns_cname_invaliddom'];
+ } else {
+ // check whether there are RR-records for the same resource
+ foreach ($dom_entries as $existing_entries) {
+ if (($existing_entries['type'] == 'A' || $existing_entries['type'] == 'AAAA' || $existing_entries['type'] == 'MX' || $existing_entries['type'] == 'NS') && $existing_entries['record'] == $record) {
+ $errors[] = $this->lng['error']['dns_cname_nomorerr'];
+ break;
+ }
+ }
+ // check www-alias setting
+ if ($result['wwwserveralias'] == '1' && $result['iswildcarddomain'] == '0' && $record == 'www') {
+ $errors[] = $this->lng['error']['no_wwwcnamae_ifwwwalias'];
+ }
+ }
+ // append trailing dot (again)
+ $content .= '.';
+ } elseif ($type == 'LOC' && ! empty($content)) {
+ $content = $content;
+ } elseif ($type == 'MX') {
+ if ($prio === null || $prio < 0) {
+ $errors[] = $this->lng['error']['dns_mx_prioempty'];
+ }
+ // check for trailing dot
+ if (substr($content, - 1) == '.') {
+ // remove it for checks
+ $content = substr($content, 0, - 1);
+ }
+ if (! \Froxlor\Validate\Validate::validateDomain($content)) {
+ $errors[] = $this->lng['error']['dns_mx_needdom'];
+ } else {
+ // check whether there is a CNAME-record for the same resource
+ foreach ($dom_entries as $existing_entries) {
+ $fqdn = $existing_entries['record'] . '.' . $domain;
+ if ($existing_entries['type'] == 'CNAME' && $fqdn == $content) {
+ $errors[] = $this->lng['error']['dns_mx_noalias'];
+ break;
+ }
+ }
+ }
+ // append trailing dot (again)
+ $content .= '.';
+ } elseif ($type == 'NS') {
+ // check for trailing dot
+ if (substr($content, - 1) == '.') {
+ // remove it for checks
+ $content = substr($content, 0, - 1);
+ }
+ if (! \Froxlor\Validate\Validate::validateDomain($content)) {
+ $errors[] = $this->lng['error']['dns_ns_invaliddom'];
+ }
+ // append trailing dot (again)
+ $content .= '.';
+ } elseif ($type == 'RP' && ! empty($content)) {
+ $content = $content;
+ } elseif ($type == 'SRV') {
+ if ($prio === null || $prio < 0) {
+ $errors[] = $this->lng['error']['dns_srv_prioempty'];
+ }
+ // check only last part of content, as it can look like:
+ // _service._proto.name. TTL class SRV priority weight port target.
+ $_split_content = explode(" ", $content);
+ // SRV content must be [weight] [port] [target]
+ if (count($_split_content) != 3) {
+ $errors[] = $this->lng['error']['dns_srv_invalidcontent'];
+ }
+ $target = trim($_split_content[count($_split_content) - 1]);
+ if ($target != '.') {
+ // check for trailing dot
+ if (substr($target, - 1) == '.') {
+ // remove it for checks
+ $target = substr($target, 0, - 1);
+ }
+ }
+ if ($target != '.' && ! \Froxlor\Validate\Validate::validateDomain($target, true)) {
+ $errors[] = $this->lng['error']['dns_srv_needdom'];
+ } else {
+ // check whether there is a CNAME-record for the same resource
+ foreach ($dom_entries as $existing_entries) {
+ $fqdn = $existing_entries['record'] . '.' . $domain;
+ if ($existing_entries['type'] == 'CNAME' && $fqdn == $target) {
+ $errors[] = $this->lng['error']['dns_srv_noalias'];
+ break;
+ }
+ }
+ }
+ // append trailing dot if there's none
+ if (substr($content, - 1) != '.') {
+ $content .= '.';
+ }
+ } elseif ($type == 'SSHFP' && ! empty($content)) {
+ $content = $content;
+ } elseif ($type == 'TXT' && ! empty($content)) {
+ // check that TXT content is enclosed in " "
+ $content = \Froxlor\Dns\Dns::encloseTXTContent($content);
+ }
+
+ $new_entry = array(
+ 'record' => $record,
+ 'type' => $type,
+ 'prio' => (int) $prio,
+ 'content' => $content,
+ 'ttl' => (int) $ttl,
+ 'domain_id' => (int) $id
+ );
+ ksort($new_entry);
+
+ // check for duplicate
+ foreach ($dom_entries as $existing_entry) {
+ // compare json-encoded string of array
+ $check_entry = $existing_entry;
+ // new entry has no ID yet
+ unset($check_entry['id']);
+ // sort by key
+ ksort($check_entry);
+ // format integer fields to real integer (as they are read as string from the DB)
+ $check_entry['prio'] = (int) $check_entry['prio'];
+ $check_entry['ttl'] = (int) $check_entry['ttl'];
+ $check_entry['domain_id'] = (int) $check_entry['domain_id'];
+ // encode both
+ $check_entry = json_encode($check_entry);
+ $new = json_encode($new_entry);
+ // compare
+ if ($check_entry === $new) {
+ $errors[] = $this->lng['error']['dns_duplicate_entry'];
+ unset($check_entry);
+ break;
+ }
+ }
+
+ if (empty($errors)) {
+ $ins_stmt = Database::prepare("
+ INSERT INTO `" . TABLE_DOMAIN_DNS . "` SET
+ `record` = :record,
+ `type` = :type,
+ `prio` = :prio,
+ `content` = :content,
+ `ttl` = :ttl,
+ `domain_id` = :domain_id
+ ");
+ Database::pexecute($ins_stmt, $new_entry, true, true);
+ $new_entry_id = Database::lastInsertId();
+
+ // add temporary to the entries-array (no reread of DB necessary)
+ $new_entry['id'] = $new_entry_id;
+ $dom_entries[] = $new_entry;
+
+ // re-generate bind configs
+ \Froxlor\System\Cronjob::inserttask('4');
+
+ $result = $this->apiCall('DomainZones.get', array(
+ 'id' => $id
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ // return $errors
+ throw new \Exception(implode("\n", $errors), 406);
+ }
+
+ /**
+ * return a domain-dns entry by either id or domainname
+ *
+ * @param int $id
+ * optional, the domain id
+ * @param string $domainname
+ * optional, the domain name
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function get()
+ {
+ if (Settings::Get('system.dnsenabled') != '1') {
+ throw new \Exception("DNS service not enabled on this system", 405);
+ }
+
+ if ($this->isAdmin() == false && $this->getUserDetail('dnsenabled') != '1') {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ $id = $this->getParam('id', true, 0);
+ $dn_optional = ($id <= 0 ? false : true);
+ $domainname = $this->getParam('domainname', $dn_optional, '');
+
+ // get requested domain
+ $result = $this->apiCall('SubDomains.get', array(
+ 'id' => $id,
+ 'domainname' => $domainname
+ ));
+ $id = $result['id'];
+
+ if ($result['parentdomainid'] != '0') {
+ throw new \Exception("DNS zones can only be generated for the main domain, not for subdomains", 406);
+ }
+
+ if ($result['subisbinddomain'] != '1') {
+ \Froxlor\UI\Response::standard_error('dns_domain_nodns', '', true);
+ }
+
+ $zone = \Froxlor\Dns\Dns::createDomainZone($id);
+ $zonefile = (string) $zone;
+
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] get dns-zone for '" . $result['domain'] . "'");
+ return $this->response(200, "successful", explode("\n", $zonefile));
+ }
+
+ /**
+ * You cannot update a dns zone entry.
+ * You need to delete it and re-add it.
+ */
+ public function update()
+ {
+ throw new \Exception('You cannot update a dns zone entry. You need to delete it and re-add it.', 303);
+ }
+
+ /**
+ * List all entry records of a given domain by either id or domainname
+ *
+ * @param int $id
+ * optional, the domain id
+ * @param string $domainname
+ * optional, the domain name
+ * @param array $sql_search
+ * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), LIKE is used if left empty and 'value' => searchvalue
+ * @param int $sql_limit
+ * optional specify number of results to be returned
+ * @param int $sql_offset
+ * optional specify offset for resultset
+ * @param array $sql_orderby
+ * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more fields
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return bool
+ */
+ public function listing()
+ {
+ if (Settings::Get('system.dnsenabled') != '1') {
+ throw new \Exception("DNS service not enabled on this system", 405);
+ }
+
+ if ($this->isAdmin() == false && $this->getUserDetail('dnsenabled') != '1') {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ $id = $this->getParam('id', true, 0);
+ $dn_optional = ($id <= 0 ? false : true);
+ $domainname = $this->getParam('domainname', $dn_optional, '');
+
+ // get requested domain
+ $result = $this->apiCall('SubDomains.get', array(
+ 'id' => $id,
+ 'domainname' => $domainname
+ ));
+ $id = $result['id'];
+ $query_fields = array();
+ $sel_stmt = Database::prepare("SELECT * FROM `" . TABLE_DOMAIN_DNS . "` WHERE `domain_id` = :did" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit());
+ $query_fields['did'] = $id;
+ Database::pexecute($sel_stmt, $query_fields, true, true);
+ $result = [];
+ while ($row = $sel_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = $row;
+ }
+ return $this->response(200, "successful", array(
+ 'count' => count($result),
+ 'list' => $result
+ ));
+ }
+
+ /**
+ * returns the total number of domainzone-entries for given domain
+ *
+ * @param int $id
+ * optional, the domain id
+ * @param string $domainname
+ * optional, the domain name
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return bool
+ */
+ public function listingCount()
+ {
+ if (Settings::Get('system.dnsenabled') != '1') {
+ throw new \Exception("DNS service not enabled on this system", 405);
+ }
+
+ if ($this->isAdmin() == false && $this->getUserDetail('dnsenabled') != '1') {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ $id = $this->getParam('id', true, 0);
+ $dn_optional = ($id <= 0 ? false : true);
+ $domainname = $this->getParam('domainname', $dn_optional, '');
+
+ // get requested domain
+ $result = $this->apiCall('SubDomains.get', array(
+ 'id' => $id,
+ 'domainname' => $domainname
+ ));
+ $id = $result['id'];
+
+ $sel_stmt = Database::prepare("SELECT COUNT(*) as num_dns FROM `" . TABLE_DOMAIN_DNS . "` WHERE `domain_id` = :did");
+ $result = Database::pexecute_first($sel_stmt, array(
+ 'did' => $id
+ ), true, true);
+ if ($result) {
+ return $this->response(200, "successful", $result['num_dns']);
+ }
+ }
+
+ /**
+ * deletes a domain-dns entry by id
+ *
+ * @param int $entry_id
+ * @param int $id
+ * optional, the domain id
+ * @param string $domainname
+ * optional, the domain name
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return bool
+ */
+ public function delete()
+ {
+ if (Settings::Get('system.dnsenabled') != '1') {
+ throw new \Exception("DNS service not enabled on this system", 405);
+ }
+
+ if ($this->isAdmin() == false && $this->getUserDetail('dnsenabled') != '1') {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ $entry_id = $this->getParam('entry_id');
+ $id = $this->getParam('id', true, 0);
+ $dn_optional = ($id <= 0 ? false : true);
+ $domainname = $this->getParam('domainname', $dn_optional, '');
+
+ // get requested domain
+ $result = $this->apiCall('SubDomains.get', array(
+ 'id' => $id,
+ 'domainname' => $domainname
+ ));
+ $id = $result['id'];
+
+ $del_stmt = Database::prepare("DELETE FROM `" . TABLE_DOMAIN_DNS . "` WHERE `id` = :id AND `domain_id` = :did");
+ Database::pexecute($del_stmt, array(
+ 'id' => $entry_id,
+ 'did' => $id
+ ), true, true);
+ if ($del_stmt->rowCount() > 0) {
+ // re-generate bind configs
+ \Froxlor\System\Cronjob::inserttask('4');
+ return $this->response(200, "successful", true);
+ }
+ return $this->response(304, "successful", true);
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/Domains.php b/lib/Froxlor/Api/Commands/Domains.php
new file mode 100644
index 00000000..4732d029
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/Domains.php
@@ -0,0 +1,2030 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class Domains extends \Froxlor\Api\ApiCommand implements \Froxlor\Api\ResourceEntity
+{
+
+ /**
+ * lists all domain entries
+ *
+ * @param bool $with_ips
+ * optional, default true
+ * @param array $sql_search
+ * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), LIKE is used if left empty and 'value' => searchvalue
+ * @param int $sql_limit
+ * optional specify number of results to be returned
+ * @param int $sql_offset
+ * optional specify offset for resultset
+ * @param array $sql_orderby
+ * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more fields
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listing()
+ {
+ if ($this->isAdmin()) {
+ $with_ips = $this->getParam('with_ips', true, true);
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] list domains");
+ $query_fields = array();
+ $result_stmt = Database::prepare("
+ SELECT
+ `d`.*, `c`.`loginname`, `c`.`deactivated`, `c`.`name`, `c`.`firstname`, `c`.`company`, `c`.`standardsubdomain`,
+ `ad`.`id` AS `aliasdomainid`, `ad`.`domain` AS `aliasdomain`
+ FROM `" . TABLE_PANEL_DOMAINS . "` `d`
+ LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` `c` USING(`customerid`)
+ LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` `ad` ON `d`.`aliasdomain`=`ad`.`id`
+ WHERE `d`.`parentdomainid`='0' " . ($this->getUserDetail('customers_see_all') ? '' : " AND `d`.`adminid` = :adminid ") . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit());
+ $params = array();
+ if ($this->getUserDetail('customers_see_all') == '0') {
+ $params['adminid'] = $this->getUserDetail('adminid');
+ }
+ $params = array_merge($params, $query_fields);
+ Database::pexecute($result_stmt, $params, true, true);
+ $result = array();
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $row['ipsandports'] = array();
+ if ($with_ips) {
+ $row['ipsandports'] = $this->getIpsForDomain($row['id']);
+ }
+ $result[] = $row;
+ }
+ return $this->response(200, "successful", array(
+ 'count' => count($result),
+ 'list' => $result
+ ));
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * returns the total number of accessable domains
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listingCount()
+ {
+ if ($this->isAdmin()) {
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] list domains");
+ $result_stmt = Database::prepare("
+ SELECT
+ COUNT(*) as num_domains
+ FROM `" . TABLE_PANEL_DOMAINS . "` `d`
+ LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` `c` USING(`customerid`)
+ LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` `ad` ON `d`.`aliasdomain`=`ad`.`id`
+ WHERE `d`.`parentdomainid`='0' " . ($this->getUserDetail('customers_see_all') ? '' : " AND `d`.`adminid` = :adminid "));
+ $params = array();
+ if ($this->getUserDetail('customers_see_all') == '0') {
+ $params['adminid'] = $this->getUserDetail('adminid');
+ }
+ $result = Database::pexecute_first($result_stmt, $params, true, true);
+ if ($result) {
+ return $this->response(200, "successful", $result['num_domains']);
+ }
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * return a domain entry by either id or domainname
+ *
+ * @param int $id
+ * optional, the domain-id
+ * @param string $domainname
+ * optional, the domainname
+ * @param bool $with_ips
+ * optional, default true
+ * @param bool $no_std_subdomain
+ * optional, default false
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function get()
+ {
+ if ($this->isAdmin()) {
+ $id = $this->getParam('id', true, 0);
+ $dn_optional = ($id <= 0 ? false : true);
+ $domainname = $this->getParam('domainname', $dn_optional, '');
+ $with_ips = $this->getParam('with_ips', true, true);
+ $no_std_subdomain = $this->getParam('no_std_subdomain', true, false);
+
+ // convert possible idn domain to punycode
+ if (substr($domainname, 0, 4) != 'xn--') {
+ $idna_convert = new \Froxlor\Idna\IdnaWrapper();
+ $domainname = $idna_convert->encode($domainname);
+ }
+
+ $result_stmt = Database::prepare("
+ SELECT `d`.*, `c`.`customerid`
+ FROM `" . TABLE_PANEL_DOMAINS . "` `d`
+ LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` `c` USING(`customerid`)
+ WHERE `d`.`parentdomainid` = '0'
+ AND " . ($id > 0 ? "`d`.`id` = :iddn" : "`d`.`domain` = :iddn") . ($no_std_subdomain ? ' AND `d`.`id` <> `c`.`standardsubdomain`' : '') . ($this->getUserDetail('customers_see_all') ? '' : " AND `d`.`adminid` = :adminid"));
+ $params = array(
+ 'iddn' => ($id <= 0 ? $domainname : $id)
+ );
+ if ($this->getUserDetail('customers_see_all') == '0') {
+ $params['adminid'] = $this->getUserDetail('adminid');
+ }
+ $result = Database::pexecute_first($result_stmt, $params, true, true);
+ if ($result) {
+ $result['ipsandports'] = array();
+ if ($with_ips) {
+ $result['ipsandports'] = $this->getIpsForDomain($result['id']);
+ }
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] get domain '" . $result['domain'] . "'");
+ return $this->response(200, "successful", $result);
+ }
+ $key = ($id > 0 ? "id #" . $id : "domainname '" . $domainname . "'");
+ throw new \Exception("Domain with " . $key . " could not be found", 404);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * get ips connected to given domain as array
+ *
+ * @param number $domain_id
+ * @param bool $ssl_only
+ * optional, return only ssl enabled ip's, default false
+ * @return array
+ */
+ private function getIpsForDomain($domain_id = 0, $ssl_only = false)
+ {
+ $resultips_stmt = Database::prepare("
+ SELECT `ips`.* FROM `" . TABLE_DOMAINTOIP . "` AS `dti`, `" . TABLE_PANEL_IPSANDPORTS . "` AS `ips`
+ WHERE `dti`.`id_ipandports` = `ips`.`id` AND `dti`.`id_domain` = :domainid " . ($ssl_only ? " AND `ips`.`ssl` = '1'" : ""));
+
+ Database::pexecute($resultips_stmt, array(
+ 'domainid' => $domain_id
+ ));
+
+ $ipandports = array();
+ while ($rowip = $resultips_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if (filter_var($rowip['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
+ $rowip['is_ipv6'] = true;
+ }
+ $ipandports[] = $rowip;
+ }
+
+ return $ipandports;
+ }
+
+ /**
+ * add new domain entry
+ *
+ * @param string $domain
+ * domain-name
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ * @param int $adminid
+ * optional, default is the calling admin's ID
+ * @param array $ipandport
+ * optional list of ip/ports to assign to domain, default is system-default-ips
+ * @param int $subcanemaildomain
+ * optional, allow subdomains of this domain as email domains, 1 = choosable (default no), 2 = choosable (default yes), 3 = always, default 0 (never)
+ * @param bool $isemaildomain
+ * optional, allow email usage with this domain, default 0 (false)
+ * @param bool $email_only
+ * optional, restrict domain to email usage, default 0 (false)
+ * @param int $selectserveralias
+ * optional, 0 = wildcard, 1 = www-alias, 2 = none, default 0
+ * @param bool $speciallogfile
+ * optional, whether to create an exclusive web-logfile for this domain, default 0 (false)
+ * @param int $alias
+ * optional, domain-id of a domain that the new domain should be an alias of, default 0 (none)
+ * @param bool $issubof
+ * optional, domain-id of a domain this domain is a subdomain of (required for webserver-cronjob to generate the correct order), default 0 (none)
+ * @param string $registration_date
+ * optional, date of domain registration in form of YYYY-MM-DD, default empty (none)
+ * @param string $termination_date
+ * optional, date of domain termination in form of YYYY-MM-DD, default empty (none)
+ * @param bool $caneditdomain
+ * optional, whether to allow the customer to edit domain settings, default 0 (false)
+ * @param bool $isbinddomain
+ * optional, whether to generate a dns-zone or not (only of nameserver is activated), default 0 (false)
+ * @param string $zonefile
+ * optional, custom dns zone filename (only of nameserver is activated), default empty (auto-generated)
+ * @param bool $dkim
+ * optional, currently not in use, default 0 (false)
+ * @param string $specialsettings
+ * optional, custom webserver vhost-content which is added to the generated vhost, default empty
+ * @param string $ssl_specialsettings
+ * optional, custom webserver vhost-content which is added to the generated ssl-vhost, default empty
+ * @param bool $include_specialsettings
+ * optional, whether or not to include non-ssl specialsettings in the generated ssl-vhost, default false
+ * @param bool $notryfiles
+ * optional, [nginx only] do not generate the default try-files directive, default 0 (false)
+ * @param bool $writeaccesslog
+ * optional, Enable writing an access-log file for this domain, default 1 (true)
+ * @param bool $writeerrorlog
+ * optional, Enable writing an error-log file for this domain, default 1 (true)
+ * @param string $documentroot
+ * optional, specify homedir of domain by specifying a directory (relative to customer-docroot), be aware, if path starts with / it it considered a full path, not relative to customer-docroot. Also specifying a URL is possible here (redirect), default empty (autogenerated)
+ * @param bool $phpenabled
+ * optional, whether php is enabled for this domain, default 0 (false)
+ * @param bool $openbasedir
+ * optional, whether to activate openbasedir restriction for this domain, default 0 (false)
+ * @param int $phpsettingid
+ * optional, specify php-configuration that is being used by id, default 1 (system-default)
+ * @param int $mod_fcgid_starter
+ * optional number of fcgid-starters if FCGID is used, default is -1
+ * @param int $mod_fcgid_maxrequests
+ * optional number of fcgid-maxrequests if FCGID is used, default is -1
+ * @param bool $ssl_redirect
+ * optional, whether to generate a https-redirect or not, default false; requires SSL to be enabled
+ * @param bool $letsencrypt
+ * optional, whether to generate a Let's Encrypt certificate for this domain, default false; requires SSL to be enabled
+ * @param array $ssl_ipandport
+ * optional, list of ssl-enabled ip/port id's to assign to this domain, default empty
+ * @param bool $dont_use_default_ssl_ipandport_if_empty
+ * optional, do NOT set the systems default ssl ip addresses if none are given via $ssl_ipandport parameter
+ * @param bool $sslenabled
+ * optional, whether or not SSL is enabled for this domain, regardless of the assigned ssl-ips, default 1 (true)
+ * @param bool $http2
+ * optional, whether to enable http/2 for this domain (requires to be enabled in the settings), default 0 (false)
+ * @param int $hsts_maxage
+ * optional max-age value for HSTS header
+ * @param bool $hsts_sub
+ * optional whether or not to add subdomains to the HSTS header
+ * @param bool $hsts_preload
+ * optional whether or not to preload HSTS header value
+ * @param bool $ocsp_stapling
+ * optional whether to enable ocsp-stapling for this domain. default 0 (false), requires SSL
+ * @param bool $honorcipherorder
+ * optional whether to honor the (server) cipher order for this domain. default 0 (false), requires SSL
+ * @param bool $sessiontickets
+ * optional whether to enable or disable TLS sessiontickets (RFC 5077) for this domain. default 1 (true), requires SSL
+ * @param bool $override_tls
+ * optional whether or not to override system-tls settings like protocol, ssl-ciphers and if applicable tls-1.3 ciphers, requires change_serversettings flag for the admin, default false
+ * @param array $ssl_protocols
+ * optional list of allowed/used ssl/tls protocols, see system.ssl_protocols setting, only used/required if $override_tls is true, default empty or system.ssl_protocols setting if $override_tls is true
+ * @param string $ssl_cipher_list
+ * optional list of allowed/used ssl/tls ciphers, see system.ssl_cipher_list setting, only used/required if $override_tls is true, default empty or system.ssl_cipher_list setting if $override_tls is true
+ * @param string $tlsv13_cipher_list
+ * optional list of allowed/used tls-1.3 specific ciphers, see system.tlsv13_cipher_list setting, only used/required if $override_tls is true, default empty or system.tlsv13_cipher_list setting if $override_tls is true
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function add()
+ {
+ if ($this->isAdmin()) {
+ if ($this->getUserDetail('domains_used') < $this->getUserDetail('domains') || $this->getUserDetail('domains') == '-1') {
+
+ // parameters
+ $p_domain = $this->getParam('domain');
+
+ // optional parameters
+ $p_ipandports = $this->getParam('ipandport', true, explode(',', Settings::Get('system.defaultip')));
+ $adminid = intval($this->getParam('adminid', true, $this->getUserDetail('adminid')));
+ $subcanemaildomain = $this->getParam('subcanemaildomain', true, 0);
+ $isemaildomain = $this->getBoolParam('isemaildomain', true, 0);
+ $email_only = $this->getBoolParam('email_only', true, 0);
+ $serveraliasoption = $this->getParam('selectserveralias', true, 0);
+ $speciallogfile = $this->getBoolParam('speciallogfile', true, 0);
+ $aliasdomain = intval($this->getParam('alias', true, 0));
+ $issubof = $this->getParam('issubof', true, 0);
+ $registration_date = $this->getParam('registration_date', true, '');
+ $termination_date = $this->getParam('termination_date', true, '');
+ $caneditdomain = $this->getBoolParam('caneditdomain', true, 0);
+ $isbinddomain = $this->getBoolParam('isbinddomain', true, 0);
+ $zonefile = $this->getParam('zonefile', true, '');
+ $dkim = $this->getBoolParam('dkim', true, 0);
+ $specialsettings = $this->getParam('specialsettings', true, '');
+ $ssl_specialsettings = $this->getParam('ssl_specialsettings', true, '');
+ $include_specialsettings = $this->getBoolParam('include_specialsettings', true, 0);
+ $notryfiles = $this->getBoolParam('notryfiles', true, 0);
+ $writeaccesslog = $this->getBoolParam('writeaccesslog', true, 1);
+ $writeerrorlog = $this->getBoolParam('writeerrorlog', true, 1);
+ $documentroot = $this->getParam('documentroot', true, '');
+ $phpenabled = $this->getBoolParam('phpenabled', true, 0);
+ $openbasedir = $this->getBoolParam('openbasedir', true, 0);
+ $phpsettingid = $this->getParam('phpsettingid', true, 1);
+ $mod_fcgid_starter = $this->getParam('mod_fcgid_starter', true, - 1);
+ $mod_fcgid_maxrequests = $this->getParam('mod_fcgid_maxrequests', true, - 1);
+ $ssl_redirect = $this->getBoolParam('ssl_redirect', true, 0);
+ $letsencrypt = $this->getBoolParam('letsencrypt', true, 0);
+ $dont_use_default_ssl_ipandport_if_empty = $this->getBoolParam('dont_use_default_ssl_ipandport_if_empty', true, 0);
+ $p_ssl_ipandports = $this->getParam('ssl_ipandport', true, $dont_use_default_ssl_ipandport_if_empty ? array() : explode(',', Settings::Get('system.defaultsslip')));
+ $sslenabled = $this->getBoolParam('sslenabled', true, 1);
+ $http2 = $this->getBoolParam('http2', true, 0);
+ $hsts_maxage = $this->getParam('hsts_maxage', true, 0);
+ $hsts_sub = $this->getBoolParam('hsts_sub', true, 0);
+ $hsts_preload = $this->getBoolParam('hsts_preload', true, 0);
+ $ocsp_stapling = $this->getBoolParam('ocsp_stapling', true, 0);
+ $honorcipherorder = $this->getBoolParam('honorcipherorder', true, 0);
+ $sessiontickets = $this->getBoolParam('sessiontickets', true, 1);
+
+ $override_tls = $this->getBoolParam('override_tls', true, 0);
+ $p_ssl_protocols = array();
+ $ssl_cipher_list = "";
+ $tlsv13_cipher_list = "";
+
+ if ($this->getUserDetail('change_serversettings') == '1') {
+ if ($override_tls) {
+ $p_ssl_protocols = $this->getParam('ssl_protocols', true, explode(',', Settings::Get('system.ssl_protocols')));
+ $ssl_cipher_list = $this->getParam('ssl_cipher_list', true, Settings::Get('system.ssl_cipher_list'));
+ $tlsv13_cipher_list = $this->getParam('tlsv13_cipher_list', true, Settings::Get('system.tlsv13_cipher_list'));
+ }
+ }
+
+ // validation
+ $p_domain = strtolower($p_domain);
+ if ($p_domain == strtolower(Settings::Get('system.hostname'))) {
+ \Froxlor\UI\Response::standard_error('admin_domain_emailsystemhostname', '', true);
+ }
+
+ if (substr($p_domain, 0, 4) == 'xn--') {
+ \Froxlor\UI\Response::standard_error('domain_nopunycode', '', true);
+ }
+
+ $idna_convert = new \Froxlor\Idna\IdnaWrapper();
+ $domain = $idna_convert->encode(preg_replace(array(
+ '/\:(\d)+$/',
+ '/^https?\:\/\//'
+ ), '', \Froxlor\Validate\Validate::validate($p_domain, 'domain')));
+
+ // Check whether domain validation is enabled and if, validate the domain
+ if (Settings::Get('system.validate_domain') && ! \Froxlor\Validate\Validate::validateDomain($domain)) {
+ \Froxlor\UI\Response::standard_error(array(
+ 'stringiswrong',
+ 'mydomain'
+ ), '', true);
+ }
+
+ $customer = $this->getCustomerData();
+ $customerid = $customer['customerid'];
+
+ if ($this->getUserDetail('customers_see_all') == '1' && $adminid != $this->getUserDetail('adminid')) {
+ $admin_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_ADMINS . "`
+ WHERE `adminid` = :adminid AND (`domains_used` < `domains` OR `domains` = '-1')");
+ $admin = Database::pexecute_first($admin_stmt, array(
+ 'adminid' => $adminid
+ ), true, true);
+ if (empty($admin)) {
+ \Froxlor\UI\Response::dynamic_error("Selected admin cannot have any more domains or could not be found");
+ }
+ unset($admin);
+ }
+
+ // set default path if admin/reseller has "change_serversettings == false" but we still
+ // need to respect the documentroot_use_default_value - setting
+ $path_suffix = '';
+ if (Settings::Get('system.documentroot_use_default_value') == 1) {
+ $path_suffix = '/' . $domain;
+ }
+ $_documentroot = \Froxlor\FileDir::makeCorrectDir($customer['documentroot'] . $path_suffix);
+
+ $registration_date = \Froxlor\Validate\Validate::validate($registration_date, 'registration_date', '/^(19|20)\d\d[-](0[1-9]|1[012])[-](0[1-9]|[12][0-9]|3[01])$/', '', array(
+ '0000-00-00',
+ '0',
+ ''
+ ), true);
+ if ($registration_date == '0000-00-00' || empty($registration_date)) {
+ $registration_date = null;
+ }
+
+ $termination_date = \Froxlor\Validate\Validate::validate($termination_date, 'termination_date', '/^(19|20)\d\d[-](0[1-9]|1[012])[-](0[1-9]|[12][0-9]|3[01])$/', '', array(
+ '0000-00-00',
+ '0',
+ ''
+ ), true);
+ if ($termination_date == '0000-00-00' || empty($termination_date)) {
+ $termination_date = null;
+ }
+
+ if ($this->getUserDetail('change_serversettings') == '1') {
+ if (Settings::Get('system.bind_enable') == '1') {
+ $zonefile = \Froxlor\Validate\Validate::validate($zonefile, 'zonefile', '', '', array(), true);
+ } else {
+ $isbinddomain = 0;
+ $zonefile = '';
+ }
+
+ $specialsettings = \Froxlor\Validate\Validate::validate(str_replace("\r\n", "\n", $specialsettings), 'specialsettings', \Froxlor\Validate\Validate::REGEX_CONF_TEXT, '', array(), true);
+ \Froxlor\Validate\Validate::validate($documentroot, 'documentroot', \Froxlor\Validate\Validate::REGEX_DIR, '', array(), true);
+
+ // If path is empty and 'Use domain name as default value for DocumentRoot path' is enabled in settings,
+ // set default path to subdomain or domain name
+ if (! empty($documentroot)) {
+ if (substr($documentroot, 0, 1) != '/' && ! preg_match('/^https?\:\/\//', $documentroot)) {
+ $documentroot = $_documentroot . '/' . $documentroot;
+ }
+ } else {
+ $documentroot = $_documentroot;
+ }
+
+ $ssl_protocols = array();
+ if (! empty($p_ssl_protocols) && is_numeric($p_ssl_protocols)) {
+ $p_ssl_protocols = array(
+ $p_ssl_protocols
+ );
+ }
+ if (! empty($p_ssl_protocols) && ! is_array($p_ssl_protocols)) {
+ $p_ssl_protocols = json_decode($p_ssl_protocols, true);
+ }
+ if (! empty($p_ssl_protocols) && is_array($p_ssl_protocols)) {
+ $protocols_available = array(
+ 'TLSv1',
+ 'TLSv1.1',
+ 'TLSv1.2',
+ 'TLSv1.3'
+ );
+ foreach ($p_ssl_protocols as $ssl_protocol) {
+ if (! in_array(trim($ssl_protocol), $protocols_available)) {
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_DEBUG, "[API] unknown SSL protocol '" . trim($ssl_protocol) . "'");
+ continue;
+ }
+ $ssl_protocols[] = $ssl_protocol;
+ }
+ }
+ if (empty($ssl_protocols)) {
+ $override_tls = '0';
+ }
+ } else {
+ $isbinddomain = '0';
+ if (Settings::Get('system.bind_enable') == '1') {
+ $isbinddomain = '1';
+ }
+ $caneditdomain = '1';
+ $zonefile = '';
+ $dkim = '0';
+ $specialsettings = '';
+ $ssl_specialsettings = '';
+ $include_specialsettings = 0;
+ $notryfiles = '0';
+ $writeaccesslog = '1';
+ $writeerrorlog = '1';
+ $documentroot = $_documentroot;
+ $override_tls = '0';
+ $ssl_protocols = array();
+ }
+
+ if ($this->getUserDetail('caneditphpsettings') == '1' || $this->getUserDetail('change_serversettings') == '1') {
+
+ if ((int) Settings::Get('system.mod_fcgid') == 1 || (int) Settings::Get('phpfpm.enabled') == 1) {
+ $phpsettingid_check_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_PHPCONFIGS . "`
+ WHERE `id` = :phpsettingid");
+ $phpsettingid_check = Database::pexecute_first($phpsettingid_check_stmt, array(
+ 'phpsettingid' => $phpsettingid
+ ), true, true);
+
+ if (! isset($phpsettingid_check['id']) || $phpsettingid_check['id'] == '0' || $phpsettingid_check['id'] != $phpsettingid) {
+ \Froxlor\UI\Response::standard_error('phpsettingidwrong', '', true);
+ }
+
+ if ((int) Settings::Get('system.mod_fcgid') == 1) {
+ $mod_fcgid_starter = \Froxlor\Validate\Validate::validate($mod_fcgid_starter, 'mod_fcgid_starter', '/^[0-9]*$/', '', array(
+ '-1',
+ ''
+ ), true);
+ $mod_fcgid_maxrequests = \Froxlor\Validate\Validate::validate($mod_fcgid_maxrequests, 'mod_fcgid_maxrequests', '/^[0-9]*$/', '', array(
+ '-1',
+ ''
+ ), true);
+ } else {
+ $mod_fcgid_starter = '-1';
+ $mod_fcgid_maxrequests = '-1';
+ }
+ } else {
+
+ if ((int) Settings::Get('phpfpm.enabled') == 1) {
+ $phpsettingid = Settings::Get('phpfpm.defaultini');
+ } else {
+ $phpsettingid = Settings::Get('system.mod_fcgid_defaultini');
+ }
+ $mod_fcgid_starter = '-1';
+ $mod_fcgid_maxrequests = '-1';
+ }
+ } else {
+
+ $phpenabled = '1';
+ $openbasedir = '1';
+
+ if ((int) Settings::Get('phpfpm.enabled') == 1) {
+ $phpsettingid = Settings::Get('phpfpm.defaultini');
+ } else {
+ $phpsettingid = Settings::Get('system.mod_fcgid_defaultini');
+ }
+ $mod_fcgid_starter = '-1';
+ $mod_fcgid_maxrequests = '-1';
+ }
+
+ // check non-ssl IP
+ $ipandports = $this->validateIpAddresses($p_ipandports);
+ // check ssl IP
+ $ssl_ipandports = array();
+ if (Settings::Get('system.use_ssl') == "1" && ! empty($p_ssl_ipandports)) {
+ $ssl_ipandports = $this->validateIpAddresses($p_ssl_ipandports, true);
+
+ if ($this->getUserDetail('change_serversettings') == '1') {
+ $ssl_specialsettings = \Froxlor\Validate\Validate::validate(str_replace("\r\n", "\n", $ssl_specialsettings), 'ssl_specialsettings', '/^[^\0]*$/', '', array(), true);
+ }
+ }
+ if (Settings::Get('system.use_ssl') == "0" || empty($ssl_ipandports)) {
+ $ssl_redirect = 0;
+ $letsencrypt = 0;
+ $http2 = 0;
+ // we need this for the json_encode
+ // if ssl is disabled or no ssl-ip/port exists
+ $ssl_ipandports[] = - 1;
+
+ // HSTS
+ $hsts_maxage = 0;
+ $hsts_sub = 0;
+ $hsts_preload = 0;
+
+ // OCSP stapling
+ $ocsp_stapling = 0;
+
+ // vhost container settings
+ $ssl_specialsettings = '';
+ $include_specialsettings = 0;
+ }
+
+ // We can't enable let's encrypt for wildcard-domains
+ if ($serveraliasoption == '0' && $letsencrypt == '1') {
+ \Froxlor\UI\Response::standard_error('nowildcardwithletsencrypt', '', true);
+ }
+
+ // Temporarily deactivate ssl_redirect until Let's Encrypt certificate was generated
+ if ($ssl_redirect > 0 && $letsencrypt == 1) {
+ $ssl_redirect = 2;
+ }
+
+ if (! preg_match('/^https?\:\/\//', $documentroot)) {
+ if (strstr($documentroot, ":") !== false) {
+ \Froxlor\UI\Response::standard_error('pathmaynotcontaincolon', '', true);
+ } else {
+ $documentroot = \Froxlor\FileDir::makeCorrectDir($documentroot);
+ }
+ }
+
+ $domain_check_stmt = Database::prepare("
+ SELECT `id`, `domain` FROM `" . TABLE_PANEL_DOMAINS . "`
+ WHERE `domain` = :domain");
+ $domain_check = Database::pexecute_first($domain_check_stmt, array(
+ 'domain' => strtolower($domain)
+ ), true, true);
+ $aliasdomain_check = array(
+ 'id' => 0
+ );
+
+ if ($aliasdomain != 0) {
+ // Overwrite given ipandports with these of the "main" domain
+ $ipandports = array();
+ $ssl_ipandports = array();
+ $origipresult_stmt = Database::prepare("
+ SELECT `id_ipandports` FROM `" . TABLE_DOMAINTOIP . "`
+ WHERE `id_domain` = :id");
+ Database::pexecute($origipresult_stmt, array(
+ 'id' => $aliasdomain
+ ), true, true);
+ $ipdata_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `id` = :ipid");
+ while ($origip = $origipresult_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $_origip_tmp = Database::pexecute_first($ipdata_stmt, array(
+ 'ipid' => $origip['id_ipandports']
+ ), true, true);
+ if ($_origip_tmp['ssl'] == 0) {
+ $ipandports[] = $origip['id_ipandports'];
+ } else {
+ $ssl_ipandports[] = $origip['id_ipandports'];
+ }
+ }
+
+ if (count($ssl_ipandports) == 0) {
+ // we need this for the json_encode
+ // if ssl is disabled or no ssl-ip/port exists
+ $ssl_ipandports[] = - 1;
+ }
+
+ $aliasdomain_check_stmt = Database::prepare("
+ SELECT `d`.`id` FROM `" . TABLE_PANEL_DOMAINS . "` `d`, `" . TABLE_PANEL_CUSTOMERS . "` `c`
+ WHERE `d`.`customerid` = :customerid
+ AND `d`.`aliasdomain` IS NULL AND `d`.`id` <> `c`.`standardsubdomain`
+ AND `c`.`customerid` = :customerid
+ AND `d`.`id` = :aliasdomainid");
+ $alias_params = array(
+ 'customerid' => $customerid,
+ 'aliasdomainid' => $aliasdomain
+ );
+ $aliasdomain_check = Database::pexecute_first($aliasdomain_check_stmt, $alias_params, true, true);
+ }
+
+ if (count($ipandports) == 0) {
+ \Froxlor\UI\Response::standard_error('noipportgiven', '', true);
+ }
+
+ if ($email_only == '1') {
+ $isemaildomain = '1';
+ } else {
+ $email_only = '0';
+ }
+
+ if ($subcanemaildomain != '1' && $subcanemaildomain != '2' && $subcanemaildomain != '3') {
+ $subcanemaildomain = '0';
+ }
+
+ if ($serveraliasoption != '1' && $serveraliasoption != '2') {
+ $serveraliasoption = '0';
+ }
+
+ if ($issubof <= 0) {
+ $issubof = '0';
+ }
+
+ $idna_convert = new \Froxlor\Idna\IdnaWrapper();
+ if ($domain == '') {
+ \Froxlor\UI\Response::standard_error(array(
+ 'stringisempty',
+ 'mydomain'
+ ), '', true);
+ } elseif ($documentroot == '') {
+ \Froxlor\UI\Response::standard_error(array(
+ 'stringisempty',
+ 'mydocumentroot'
+ ), '', true);
+ } elseif ($customerid == 0) {
+ \Froxlor\UI\Response::standard_error('adduserfirst', '', true);
+ } elseif ($domain_check && strtolower($domain_check['domain']) == strtolower($domain)) {
+ \Froxlor\UI\Response::standard_error('domainalreadyexists', $idna_convert->decode($domain), true);
+ } elseif ($aliasdomain_check && $aliasdomain_check['id'] != $aliasdomain) {
+ \Froxlor\UI\Response::standard_error('domainisaliasorothercustomer', '', true);
+ } else {
+ $wwwserveralias = ($serveraliasoption == '1') ? '1' : '0';
+ $iswildcarddomain = ($serveraliasoption == '0') ? '1' : '0';
+
+ $ins_data = array(
+ 'domain' => $domain,
+ 'domain_ace' => $idna_convert->decode($domain),
+ 'customerid' => $customerid,
+ 'adminid' => $adminid,
+ 'documentroot' => $documentroot,
+ 'aliasdomain' => ($aliasdomain != 0 ? $aliasdomain : null),
+ 'zonefile' => $zonefile,
+ 'dkim' => $dkim,
+ 'wwwserveralias' => $wwwserveralias,
+ 'iswildcarddomain' => $iswildcarddomain,
+ 'isbinddomain' => $isbinddomain,
+ 'isemaildomain' => $isemaildomain,
+ 'email_only' => $email_only,
+ 'subcanemaildomain' => $subcanemaildomain,
+ 'caneditdomain' => $caneditdomain,
+ 'phpenabled' => $phpenabled,
+ 'openbasedir' => $openbasedir,
+ 'speciallogfile' => $speciallogfile,
+ 'specialsettings' => $specialsettings,
+ 'ssl_specialsettings' => $ssl_specialsettings,
+ 'include_specialsettings' => $include_specialsettings,
+ 'notryfiles' => $notryfiles,
+ 'writeaccesslog' => $writeaccesslog,
+ 'writeerrorlog' => $writeerrorlog,
+ 'ssl_redirect' => $ssl_redirect,
+ 'add_date' => time(),
+ 'registration_date' => $registration_date,
+ 'termination_date' => $termination_date,
+ 'phpsettingid' => $phpsettingid,
+ 'mod_fcgid_starter' => $mod_fcgid_starter,
+ 'mod_fcgid_maxrequests' => $mod_fcgid_maxrequests,
+ 'ismainbutsubto' => $issubof,
+ 'letsencrypt' => $letsencrypt,
+ 'http2' => $http2,
+ 'hsts' => $hsts_maxage,
+ 'hsts_sub' => $hsts_sub,
+ 'hsts_preload' => $hsts_preload,
+ 'ocsp_stapling' => $ocsp_stapling,
+ 'override_tls' => $override_tls,
+ 'ssl_protocols' => implode(",", $ssl_protocols),
+ 'ssl_cipher_list' => $ssl_cipher_list,
+ 'tlsv13_cipher_list' => $tlsv13_cipher_list,
+ 'sslenabled' => $sslenabled,
+ 'honorcipherorder' => $honorcipherorder,
+ 'sessiontickets' => $sessiontickets
+ );
+
+ $ins_stmt = Database::prepare("
+ INSERT INTO `" . TABLE_PANEL_DOMAINS . "` SET
+ `domain` = :domain,
+ `domain_ace` = :domain_ace,
+ `customerid` = :customerid,
+ `adminid` = :adminid,
+ `documentroot` = :documentroot,
+ `aliasdomain` = :aliasdomain,
+ `zonefile` = :zonefile,
+ `dkim` = :dkim,
+ `dkim_id` = '0',
+ `dkim_privkey` = '',
+ `dkim_pubkey` = '',
+ `wwwserveralias` = :wwwserveralias,
+ `iswildcarddomain` = :iswildcarddomain,
+ `isbinddomain` = :isbinddomain,
+ `isemaildomain` = :isemaildomain,
+ `email_only` = :email_only,
+ `subcanemaildomain` = :subcanemaildomain,
+ `caneditdomain` = :caneditdomain,
+ `phpenabled` = :phpenabled,
+ `openbasedir` = :openbasedir,
+ `speciallogfile` = :speciallogfile,
+ `specialsettings` = :specialsettings,
+ `ssl_specialsettings` = :ssl_specialsettings,
+ `include_specialsettings` = :include_specialsettings,
+ `notryfiles` = :notryfiles,
+ `writeaccesslog` = :writeaccesslog,
+ `writeerrorlog` = :writeerrorlog,
+ `ssl_redirect` = :ssl_redirect,
+ `add_date` = :add_date,
+ `registration_date` = :registration_date,
+ `termination_date` = :termination_date,
+ `phpsettingid` = :phpsettingid,
+ `mod_fcgid_starter` = :mod_fcgid_starter,
+ `mod_fcgid_maxrequests` = :mod_fcgid_maxrequests,
+ `ismainbutsubto` = :ismainbutsubto,
+ `letsencrypt` = :letsencrypt,
+ `http2` = :http2,
+ `hsts` = :hsts,
+ `hsts_sub` = :hsts_sub,
+ `hsts_preload` = :hsts_preload,
+ `ocsp_stapling` = :ocsp_stapling,
+ `override_tls` = :override_tls,
+ `ssl_protocols` = :ssl_protocols,
+ `ssl_cipher_list` = :ssl_cipher_list,
+ `tlsv13_cipher_list` = :tlsv13_cipher_list,
+ `ssl_enabled` = :sslenabled,
+ `ssl_honorcipherorder` = :honorcipherorder,
+ `ssl_sessiontickets`= :sessiontickets
+ ");
+ Database::pexecute($ins_stmt, $ins_data, true, true);
+ $domainid = Database::lastInsertId();
+ $ins_data['id'] = $domainid;
+ unset($ins_data);
+
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_ADMINS . "` SET `domains_used` = `domains_used` + 1
+ WHERE `adminid` = :adminid");
+ Database::pexecute($upd_stmt, array(
+ 'adminid' => $adminid
+ ), true, true);
+
+ $ins_stmt = Database::prepare("
+ INSERT INTO `" . TABLE_DOMAINTOIP . "` SET
+ `id_domain` = :domainid,
+ `id_ipandports` = :ipandportsid
+ ");
+
+ foreach ($ipandports as $ipportid) {
+ $ins_data = array(
+ 'domainid' => $domainid,
+ 'ipandportsid' => $ipportid
+ );
+ Database::pexecute($ins_stmt, $ins_data, true, true);
+ }
+
+ foreach ($ssl_ipandports as $ssl_ipportid) {
+ if ($ssl_ipportid > 0) {
+ $ins_data = array(
+ 'domainid' => $domainid,
+ 'ipandportsid' => $ssl_ipportid
+ );
+ Database::pexecute($ins_stmt, $ins_data, true, true);
+ }
+ }
+
+ \Froxlor\Domain\Domain::triggerLetsEncryptCSRForAliasDestinationDomain($aliasdomain, $this->logger());
+
+ \Froxlor\System\Cronjob::inserttask('1');
+ // Using nameserver, insert a task which rebuilds the server config
+ \Froxlor\System\Cronjob::inserttask('4');
+
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] added domain '" . $domain . "'");
+
+ $result = $this->apiCall('Domains.get', array(
+ 'domainname' => $domain
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ }
+ throw new \Exception("No more resources available", 406);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * update domain entry by either id or domainname
+ *
+ * @param int $id
+ * optional, the domain-id
+ * @param string $domainname
+ * optional, the domainname
+ * @param int $customerid
+ * required (if $loginname is not specified)
+ * @param string $loginname
+ * required (if $customerid is not specified)
+ * @param int $adminid
+ * optional, default is the calling admin's ID
+ * @param array $ipandport
+ * optional list of ip/ports to assign to domain, default is system-default-ips
+ * @param int $subcanemaildomain
+ * optional, allow subdomains of this domain as email domains, 1 = choosable (default no), 2 = choosable (default yes), 3 = always, default 0 (never)
+ * @param bool $isemaildomain
+ * optional, allow email usage with this domain, default 0 (false)
+ * @param bool $email_only
+ * optional, restrict domain to email usage, default 0 (false)
+ * @param int $selectserveralias
+ * optional, 0 = wildcard, 1 = www-alias, 2 = none, default 0
+ * @param bool $speciallogfile
+ * optional, whether to create an exclusive web-logfile for this domain, default 0 (false)
+ * @param bool $speciallogverified
+ * optional, when setting $speciallogfile to false, this needs to be set to true to confirm the action, default 0 (false)
+ * @param int $alias
+ * optional, domain-id of a domain that the new domain should be an alias of, default 0 (none)
+ * @param bool $issubof
+ * optional, domain-id of a domain this domain is a subdomain of (required for webserver-cronjob to generate the correct order), default 0 (none)
+ * @param string $registration_date
+ * optional, date of domain registration in form of YYYY-MM-DD, default empty (none)
+ * @param string $termination_date
+ * optional, date of domain termination in form of YYYY-MM-DD, default empty (none)
+ * @param bool $caneditdomain
+ * optional, whether to allow the customer to edit domain settings, default 0 (false)
+ * @param bool $isbinddomain
+ * optional, whether to generate a dns-zone or not (only of nameserver is activated), default 0 (false)
+ * @param string $zonefile
+ * optional, custom dns zone filename (only of nameserver is activated), default empty (auto-generated)
+ * @param bool $dkim
+ * optional, currently not in use, default 0 (false)
+ * @param string $specialsettings
+ * optional, custom webserver vhost-content which is added to the generated vhost, default empty
+ * @param string $ssl_specialsettings
+ * optional, custom webserver vhost-content which is added to the generated ssl-vhost, default empty
+ * @param bool $include_specialsettings
+ * optional, whether or not to include non-ssl specialsettings in the generated ssl-vhost, default false
+ * @param bool $specialsettingsforsubdomains
+ * optional, whether to apply specialsettings to all subdomains of this domain, default is read from setting system.apply_specialsettings_default
+ * @param bool $notryfiles
+ * optional, [nginx only] do not generate the default try-files directive, default 0 (false)
+ * @param bool $writeaccesslog
+ * optional, Enable writing an access-log file for this domain, default 1 (true)
+ * @param bool $writeerrorlog
+ * optional, Enable writing an error-log file for this domain, default 1 (true)
+ * @param string $documentroot
+ * optional, specify homedir of domain by specifying a directory (relative to customer-docroot), be aware, if path starts with / it it considered a full path, not relative to customer-docroot. Also specifying a URL is possible here (redirect), default empty (autogenerated)
+ * @param bool $phpenabled
+ * optional, whether php is enabled for this domain, default 0 (false)
+ * @param bool $phpsettingsforsubdomains
+ * optional, whether to apply php-setting to apply to all subdomains of this domain, default is read from setting system.apply_phpconfigs_default
+ * @param bool $openbasedir
+ * optional, whether to activate openbasedir restriction for this domain, default 0 (false)
+ * @param int $phpsettingid
+ * optional, specify php-configuration that is being used by id, default 1 (system-default)
+ * @param int $mod_fcgid_starter
+ * optional number of fcgid-starters if FCGID is used, default is -1
+ * @param int $mod_fcgid_maxrequests
+ * optional number of fcgid-maxrequests if FCGID is used, default is -1
+ * @param bool $ssl_redirect
+ * optional, whether to generate a https-redirect or not, default false; requires SSL to be enabled
+ * @param bool $letsencrypt
+ * optional, whether to generate a Let's Encrypt certificate for this domain, default false; requires SSL to be enabled
+ * @param array $ssl_ipandport
+ * optional, list of ssl-enabled ip/port id's to assign to this domain, if left empty, the current set value is being used, to remove all ssl ips use $remove_ssl_ipandport
+ * @param bool $remove_ssl_ipandport
+ * optional, if set to true and no $ssl_ipandport value is given, the ip's get removed, otherwise, the currently set value is used, default false
+ * @param bool $sslenabled
+ * optional, whether or not SSL is enabled for this domain, regardless of the assigned ssl-ips, default 1 (true)
+ * @param bool $http2
+ * optional, whether to enable http/2 for this domain (requires to be enabled in the settings), default 0 (false)
+ * @param int $hsts_maxage
+ * optional max-age value for HSTS header
+ * @param bool $hsts_sub
+ * optional whether or not to add subdomains to the HSTS header
+ * @param bool $hsts_preload
+ * optional whether or not to preload HSTS header value
+ * @param bool $ocsp_stapling
+ * optional whether to enable ocsp-stapling for this domain. default 0 (false), requires SSL
+ * @param bool $honorcipherorder
+ * optional whether to honor the (server) cipher order for this domain. default 0 (false), requires SSL
+ * @param bool $sessiontickets
+ * optional whether to enable or disable TLS sessiontickets (RFC 5077) for this domain. default 1 (true), requires SSL
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function update()
+ {
+ if ($this->isAdmin()) {
+
+ // parameters
+ $id = $this->getParam('id', true, 0);
+ $dn_optional = ($id <= 0 ? false : true);
+ $domainname = $this->getParam('domainname', $dn_optional, '');
+
+ // get requested domain
+ $result = $this->apiCall('Domains.get', array(
+ 'id' => $id,
+ 'domainname' => $domainname
+ ));
+ $id = $result['id'];
+
+ // optional parameters
+ $p_ipandports = $this->getParam('ipandport', true, array());
+ $adminid = intval($this->getParam('adminid', true, $result['adminid']));
+
+ if ($this->getParam('customerid', true, 0) == 0 && $this->getParam('loginname', true, '') == '') {
+ $customerid = $result['customerid'];
+ $customer = $this->apiCall('Customers.get', array(
+ 'id' => $customerid
+ ));
+ } else {
+ $customer = $this->getCustomerData();
+ $customerid = $customer['customerid'];
+ }
+
+ $subcanemaildomain = $this->getParam('subcanemaildomain', true, $result['subcanemaildomain']);
+ $isemaildomain = $this->getBoolParam('isemaildomain', true, $result['isemaildomain']);
+ $email_only = $this->getBoolParam('email_only', true, $result['email_only']);
+ $p_serveraliasoption = $this->getParam('selectserveralias', true, - 1);
+ $speciallogfile = $this->getBoolParam('speciallogfile', true, $result['speciallogfile']);
+ $speciallogverified = $this->getBoolParam('speciallogverified', true, 0);
+ $aliasdomain = intval($this->getParam('alias', true, $result['aliasdomain']));
+ $issubof = $this->getParam('issubof', true, $result['ismainbutsubto']);
+ $registration_date = $this->getParam('registration_date', true, $result['registration_date']);
+ $termination_date = $this->getParam('termination_date', true, $result['termination_date']);
+ $caneditdomain = $this->getBoolParam('caneditdomain', true, $result['caneditdomain']);
+ $isbinddomain = $this->getBoolParam('isbinddomain', true, $result['isbinddomain']);
+ $zonefile = $this->getParam('zonefile', true, $result['zonefile']);
+ $dkim = $this->getBoolParam('dkim', true, $result['dkim']);
+ $specialsettings = $this->getParam('specialsettings', true, $result['specialsettings']);
+ $ssl_specialsettings = $this->getParam('ssl_specialsettings', true, $result['ssl_specialsettings']);
+ $include_specialsettings = $this->getBoolParam('include_specialsettings', true, $result['include_specialsettings']);
+ $ssfs = $this->getBoolParam('specialsettingsforsubdomains', true, \Froxlor\Settings::Get('system.apply_specialsettings_default'));
+ $notryfiles = $this->getBoolParam('notryfiles', true, $result['notryfiles']);
+ $writeaccesslog = $this->getBoolParam('writeaccesslog', true, $result['writeaccesslog']);
+ $writeerrorlog = $this->getBoolParam('writeerrorlog', true, $result['writeerrorlog']);
+ $documentroot = $this->getParam('documentroot', true, $result['documentroot']);
+ $phpenabled = $this->getBoolParam('phpenabled', true, $result['phpenabled']);
+ $phpfs = $this->getBoolParam('phpsettingsforsubdomains', true, \Froxlor\Settings::Get('system.apply_phpconfigs_default'));
+ $openbasedir = $this->getBoolParam('openbasedir', true, $result['openbasedir']);
+ $phpsettingid = $this->getParam('phpsettingid', true, $result['phpsettingid']);
+ $mod_fcgid_starter = $this->getParam('mod_fcgid_starter', true, $result['mod_fcgid_starter']);
+ $mod_fcgid_maxrequests = $this->getParam('mod_fcgid_maxrequests', true, $result['mod_fcgid_maxrequests']);
+ $ssl_redirect = $this->getBoolParam('ssl_redirect', true, $result['ssl_redirect']);
+ $letsencrypt = $this->getBoolParam('letsencrypt', true, $result['letsencrypt']);
+ $remove_ssl_ipandport = $this->getBoolParam('remove_ssl_ipandport', true, 0);
+ $p_ssl_ipandports = $this->getParam('ssl_ipandport', true, $remove_ssl_ipandport ? array(
+ - 1
+ ) : null);
+ $sslenabled = $this->getBoolParam('sslenabled', true, $result['ssl_enabled']);
+ $http2 = $this->getBoolParam('http2', true, $result['http2']);
+ $hsts_maxage = $this->getParam('hsts_maxage', true, $result['hsts']);
+ $hsts_sub = $this->getBoolParam('hsts_sub', true, $result['hsts_sub']);
+ $hsts_preload = $this->getBoolParam('hsts_preload', true, $result['hsts_preload']);
+ $ocsp_stapling = $this->getBoolParam('ocsp_stapling', true, $result['ocsp_stapling']);
+ $honorcipherorder = $this->getBoolParam('honorcipherorder', true, $result['ssl_honorcipherorder']);
+ $sessiontickets = $this->getBoolParam('sessiontickets', true, $result['ssl_sessiontickets']);
+
+ $override_tls = $this->getBoolParam('override_tls', true, $result['override_tls']);
+
+ if ($this->getUserDetail('change_serversettings') == '1') {
+ if ($override_tls) {
+ $p_ssl_protocols = $this->getParam('ssl_protocols', true, explode(',', $result['ssl_protocols']));
+ $ssl_cipher_list = $this->getParam('ssl_cipher_list', true, $result['ssl_cipher_list']);
+ $tlsv13_cipher_list = $this->getParam('tlsv13_cipher_list', true, $result['tlsv13_cipher_list']);
+ } else {
+ $p_ssl_protocols = array();
+ $ssl_cipher_list = "";
+ $tlsv13_cipher_list = "";
+ }
+ } else {
+ $p_ssl_protocols = explode(',', $result['ssl_protocols']);
+ $ssl_cipher_list = $result['ssl_cipher_list'];
+ $tlsv13_cipher_list = $result['tlsv13_cipher_list'];
+ }
+
+ // count subdomain usage of source-domain
+ $subdomains_stmt = Database::prepare("
+ SELECT COUNT(`id`) AS count FROM `" . TABLE_PANEL_DOMAINS . "` WHERE
+ `parentdomainid` = :resultid
+ ");
+ $subdomains = Database::pexecute_first($subdomains_stmt, array(
+ 'resultid' => $result['id']
+ ), true, true);
+ $subdomains = $subdomains['count'];
+
+ // count where this domain is alias domain
+ $alias_check_stmt = Database::prepare("
+ SELECT COUNT(`id`) AS count FROM `" . TABLE_PANEL_DOMAINS . "` WHERE
+ `aliasdomain` = :resultid
+ ");
+ $alias_check = Database::pexecute_first($alias_check_stmt, array(
+ 'resultid' => $result['id']
+ ), true, true);
+ $alias_check = $alias_check['count'];
+
+ // count where we are used in email-accounts
+ $domain_emails_result_stmt = Database::prepare("
+ SELECT `email`, `email_full`, `destination`, `popaccountid` AS `number_email_forwarders`
+ FROM `" . TABLE_MAIL_VIRTUAL . "` WHERE `customerid` = :customerid AND `domainid` = :id
+ ");
+ Database::pexecute($domain_emails_result_stmt, array(
+ 'customerid' => $result['customerid'],
+ 'id' => $result['id']
+ ), true, true);
+
+ $emails = Database::num_rows();
+ $email_forwarders = 0;
+ $email_accounts = 0;
+
+ while ($domain_emails_row = $domain_emails_result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if ($domain_emails_row['destination'] != '') {
+ $domain_emails_row['destination'] = explode(' ', \Froxlor\FileDir::makeCorrectDestination($domain_emails_row['destination']));
+ $email_forwarders += count($domain_emails_row['destination']);
+ if (in_array($domain_emails_row['email_full'], $domain_emails_row['destination'])) {
+ $email_forwarders -= 1;
+ $email_accounts ++;
+ }
+ }
+ }
+
+ // handle change of customer (move domain from customer to customer)
+ if ($customerid > 0 && $customerid != $result['customerid'] && Settings::Get('panel.allow_domain_change_customer') == '1') {
+ // check whether target customer has enough resources
+ $customer_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_CUSTOMERS . "`
+ WHERE `customerid` = :customerid
+ AND (`subdomains_used` + :subdomains <= `subdomains` OR `subdomains` = '-1' )
+ AND (`emails_used` + :emails <= `emails` OR `emails` = '-1' )
+ AND (`email_forwarders_used` + :forwarders <= `email_forwarders` OR `email_forwarders` = '-1' )
+ AND (`email_accounts_used` + :accounts <= `email_accounts` OR `email_accounts` = '-1' ) " . ($this->getUserDetail('customers_see_all') ? '' : " AND `adminid` = :adminid"));
+ $params = array(
+ 'customerid' => $customerid,
+ 'subdomains' => $subdomains,
+ 'emails' => $emails,
+ 'forwarders' => $email_forwarders,
+ 'accounts' => $email_accounts
+ );
+ if ($this->getUserDetail('customers_see_all') == '0') {
+ $params['adminid'] = $this->getUserDetail('adminid');
+ }
+ $customer = Database::pexecute_first($customer_stmt, $params, true, true);
+ if (empty($customer) || $customer['customerid'] != $customerid) {
+ \Froxlor\UI\Response::standard_error('customerdoesntexist', '', true);
+ }
+ }
+
+ // handle change of admin (move domain from admin to admin)
+ if ($this->getUserDetail('customers_see_all') == '1') {
+
+ if ($adminid > 0 && $adminid != $result['adminid'] && Settings::Get('panel.allow_domain_change_admin') == '1') {
+
+ $admin_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_ADMINS . "`
+ WHERE `adminid` = :adminid AND ( `domains_used` < `domains` OR `domains` = '-1' )
+ ");
+ $admin = Database::pexecute_first($admin_stmt, array(
+ 'adminid' => $adminid
+ ), true, true);
+
+ if (empty($admin) || $admin['adminid'] != $adminid) {
+ \Froxlor\UI\Response::standard_error('admindoesntexist', '', true);
+ }
+ } else {
+ $adminid = $result['adminid'];
+ }
+ } else {
+ $adminid = $result['adminid'];
+ }
+
+ $registration_date = \Froxlor\Validate\Validate::validate($registration_date, 'registration_date', '/^(19|20)\d\d[-](0[1-9]|1[012])[-](0[1-9]|[12][0-9]|3[01])$/', '', array(
+ '0000-00-00',
+ '0',
+ ''
+ ), true);
+ if ($registration_date == '0000-00-00' || empty($registration_date)) {
+ $registration_date = null;
+ }
+ $termination_date = \Froxlor\Validate\Validate::validate($termination_date, 'termination_date', '/^(19|20)\d\d[-](0[1-9]|1[012])[-](0[1-9]|[12][0-9]|3[01])$/', '', array(
+ '0000-00-00',
+ '0',
+ ''
+ ), true);
+ if ($termination_date == '0000-00-00' || empty($termination_date)) {
+ $termination_date = null;
+ }
+
+ $serveraliasoption = '2';
+ if ($result['iswildcarddomain'] == '1') {
+ $serveraliasoption = '0';
+ } elseif ($result['wwwserveralias'] == '1') {
+ $serveraliasoption = '1';
+ }
+ if ($p_serveraliasoption > - 1) {
+ $serveraliasoption = $p_serveraliasoption;
+ }
+
+ if ($this->getUserDetail('change_serversettings') == '1') {
+
+ if (Settings::Get('system.bind_enable') == '1') {
+ $zonefile = \Froxlor\Validate\Validate::validate($zonefile, 'zonefile', '', '', array(), true);
+ } else {
+ $isbinddomain = $result['isbinddomain'];
+ $zonefile = $result['zonefile'];
+ }
+
+ if (Settings::Get('dkim.use_dkim') != '1') {
+ $dkim = $result['dkim'];
+ }
+
+ $specialsettings = \Froxlor\Validate\Validate::validate(str_replace("\r\n", "\n", $specialsettings), 'specialsettings', \Froxlor\Validate\Validate::REGEX_CONF_TEXT, '', array(), true);
+ $documentroot = \Froxlor\Validate\Validate::validate($documentroot, 'documentroot', \Froxlor\Validate\Validate::REGEX_DIR, '', array(), true);
+
+ // when moving customer and no path is specified, update would normally reuse the current document-root
+ // which would point to the wrong customer, therefore we will re-create that directory
+ if (! empty($documentroot) && $customerid > 0 && $customerid != $result['customerid'] && Settings::Get('panel.allow_domain_change_customer') == '1') {
+ if (Settings::Get('system.documentroot_use_default_value') == 1) {
+ $_documentroot = \Froxlor\FileDir::makeCorrectDir($customer['documentroot'] . '/' . $result['domain']);
+ } else {
+ $_documentroot = $customer['documentroot'];
+ }
+ // set the customers default docroot
+ $documentroot = $_documentroot;
+ }
+
+ if ($documentroot == '') {
+ // If path is empty and 'Use domain name as default value for DocumentRoot path' is enabled in settings,
+ // set default path to subdomain or domain name
+ if (Settings::Get('system.documentroot_use_default_value') == 1) {
+ $documentroot = \Froxlor\FileDir::makeCorrectDir($customer['documentroot'] . '/' . $result['domain']);
+ } else {
+ $documentroot = $customer['documentroot'];
+ }
+ }
+
+ if (! preg_match('/^https?\:\/\//', $documentroot) && strstr($documentroot, ":") !== false) {
+ \Froxlor\UI\Response::standard_error('pathmaynotcontaincolon', '', true);
+ }
+
+ $ssl_protocols = array();
+ if (! empty($p_ssl_protocols) && is_numeric($p_ssl_protocols)) {
+ $p_ssl_protocols = array(
+ $p_ssl_protocols
+ );
+ }
+ if (! empty($p_ssl_protocols) && ! is_array($p_ssl_protocols)) {
+ $p_ssl_protocols = json_decode($p_ssl_protocols, true);
+ }
+ if (! empty($p_ssl_protocols) && is_array($p_ssl_protocols)) {
+ $protocols_available = array(
+ 'TLSv1',
+ 'TLSv1.1',
+ 'TLSv1.2',
+ 'TLSv1.3'
+ );
+ foreach ($p_ssl_protocols as $ssl_protocol) {
+ if (! in_array(trim($ssl_protocol), $protocols_available)) {
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_DEBUG, "[API] unknown SSL protocol '" . trim($ssl_protocol) . "'");
+ continue;
+ }
+ $ssl_protocols[] = $ssl_protocol;
+ }
+ }
+ if (empty($ssl_protocols)) {
+ $override_tls = '0';
+ }
+ } else {
+ $isbinddomain = $result['isbinddomain'];
+ $zonefile = $result['zonefile'];
+ $dkim = $result['dkim'];
+ $specialsettings = $result['specialsettings'];
+ $ssl_specialsettings = $result['ssl_specialsettings'];
+ $include_specialsettings = $result['include_specialsettings'];
+ $ssfs = (empty($specialsettings) ? 0 : 1);
+ $notryfiles = $result['notryfiles'];
+ $writeaccesslog = $result['writeaccesslog'];
+ $writeerrorlog = $result['writeerrorlog'];
+ $documentroot = $result['documentroot'];
+ $ssl_protocols = $p_ssl_protocols;
+ $override_tls = $result['override_tls'];
+ }
+
+ if ($this->getUserDetail('caneditphpsettings') == '1' || $this->getUserDetail('change_serversettings') == '1') {
+
+ if ((int) Settings::Get('system.mod_fcgid') == 1 || (int) Settings::Get('phpfpm.enabled') == 1) {
+ $phpsettingid_check_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_PHPCONFIGS . "` WHERE `id` = :phpid
+ ");
+ $phpsettingid_check = Database::pexecute_first($phpsettingid_check_stmt, array(
+ 'phpid' => $phpsettingid
+ ), true, true);
+
+ if (! isset($phpsettingid_check['id']) || $phpsettingid_check['id'] == '0' || $phpsettingid_check['id'] != $phpsettingid) {
+ \Froxlor\UI\Response::standard_error('phpsettingidwrong', '', true);
+ }
+
+ if ((int) Settings::Get('system.mod_fcgid') == 1) {
+ $mod_fcgid_starter = \Froxlor\Validate\Validate::validate($mod_fcgid_starter, 'mod_fcgid_starter', '/^[0-9]*$/', '', array(
+ '-1',
+ ''
+ ), true);
+ $mod_fcgid_maxrequests = \Froxlor\Validate\Validate::validate($mod_fcgid_maxrequests, 'mod_fcgid_maxrequests', '/^[0-9]*$/', '', array(
+ '-1',
+ ''
+ ), true);
+ } else {
+ $mod_fcgid_starter = $result['mod_fcgid_starter'];
+ $mod_fcgid_maxrequests = $result['mod_fcgid_maxrequests'];
+ }
+ } else {
+ $phpsettingid = $result['phpsettingid'];
+ $phpfs = 1;
+ $mod_fcgid_starter = $result['mod_fcgid_starter'];
+ $mod_fcgid_maxrequests = $result['mod_fcgid_maxrequests'];
+ }
+ } else {
+ $phpenabled = $result['phpenabled'];
+ $openbasedir = $result['openbasedir'];
+ $phpsettingid = $result['phpsettingid'];
+ $phpfs = 1;
+ $mod_fcgid_starter = $result['mod_fcgid_starter'];
+ $mod_fcgid_maxrequests = $result['mod_fcgid_maxrequests'];
+ }
+
+ // check non-ssl IP
+ $ipandports = $this->validateIpAddresses($p_ipandports, false, $result['id']);
+ // check ssl IP
+ if (empty($p_ssl_ipandports) || (! is_array($p_ssl_ipandports) && is_null($p_ssl_ipandports))) {
+ foreach ($result['ipsandports'] as $ip) {
+ if ($ip['ssl'] == 1) {
+ $p_ssl_ipandports[] = $ip['id'];
+ }
+ }
+ }
+ $ssl_ipandports = array();
+ if (Settings::Get('system.use_ssl') == "1" && ! empty($p_ssl_ipandports) && $p_ssl_ipandports[0] != - 1) {
+ $ssl_ipandports = $this->validateIpAddresses($p_ssl_ipandports, true, $result['id']);
+
+ if ($this->getUserDetail('change_serversettings') == '1') {
+ $ssl_specialsettings = \Froxlor\Validate\Validate::validate(str_replace("\r\n", "\n", $ssl_specialsettings), 'ssl_specialsettings', '/^[^\0]*$/', '', array(), true);
+ }
+ }
+ if ($remove_ssl_ipandport || (! empty($p_ssl_ipandports) && $p_ssl_ipandports[0] == - 1)) {
+ $ssl_ipandports = array();
+ }
+ if (Settings::Get('system.use_ssl') == "0" || empty($ssl_ipandports)) {
+ $ssl_redirect = 0;
+ $letsencrypt = 0;
+ $http2 = 0;
+ // we need this for the json_encode
+ // if ssl is disabled or no ssl-ip/port exists
+ $ssl_ipandports[] = - 1;
+
+ // HSTS
+ $hsts_maxage = 0;
+ $hsts_sub = 0;
+ $hsts_preload = 0;
+
+ // OCSP stapling
+ $ocsp_stapling = 0;
+
+ // vhost container settings
+ $ssl_specialsettings = '';
+ $include_specialsettings = 0;
+ }
+
+ // We can't enable let's encrypt for wildcard-domains
+ if ($serveraliasoption == '0' && $letsencrypt == '1') {
+ \Froxlor\UI\Response::standard_error('nowildcardwithletsencrypt', '', true);
+ }
+
+ // Temporarily deactivate ssl_redirect until Let's Encrypt certificate was generated
+ if ($ssl_redirect > 0 && $letsencrypt == 1 && $result['letsencrypt'] != $letsencrypt) {
+ $ssl_redirect = 2;
+ }
+
+ if (! preg_match('/^https?\:\/\//', $documentroot)) {
+ if ($documentroot != $result['documentroot']) {
+ if (substr($documentroot, 0, 1) != "/") {
+ $documentroot = $customer['documentroot'] . '/' . $documentroot;
+ }
+ $documentroot = \Froxlor\FileDir::makeCorrectDir($documentroot);
+ }
+ }
+
+ if ($email_only == '1') {
+ $isemaildomain = '1';
+ } else {
+ $email_only = '0';
+ }
+
+ if ($subcanemaildomain != '1' && $subcanemaildomain != '2' && $subcanemaildomain != '3') {
+ $subcanemaildomain = '0';
+ }
+
+ $aliasdomain_check = array(
+ 'id' => 0
+ );
+
+ if ($aliasdomain != 0) {
+ // Overwrite given ipandports with these of the "main" domain
+ $ipandports = array();
+ $ssl_ipandports = array();
+ $origipresult_stmt = Database::prepare("
+ SELECT `id_ipandports` FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :aliasdomain
+ ");
+ Database::pexecute($origipresult_stmt, array(
+ 'aliasdomain' => $aliasdomain
+ ), true, true);
+ $ipdata_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `id` = :ipid");
+ while ($origip = $origipresult_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $_origip_tmp = Database::pexecute_first($ipdata_stmt, array(
+ 'ipid' => $origip['id_ipandports']
+ ), true, true);
+ if ($_origip_tmp['ssl'] == 0) {
+ $ipandports[] = $origip['id_ipandports'];
+ } else {
+ $ssl_ipandports[] = $origip['id_ipandports'];
+ }
+ }
+
+ if (count($ssl_ipandports) == 0) {
+ // we need this for the json_encode
+ // if ssl is disabled or no ssl-ip/port exists
+ $ssl_ipandports[] = - 1;
+ }
+
+ $aliasdomain_check_stmt = Database::prepare("
+ SELECT `d`.`id` FROM `" . TABLE_PANEL_DOMAINS . "` `d`, `" . TABLE_PANEL_CUSTOMERS . "` `c`
+ WHERE `d`.`customerid` = :customerid
+ AND `d`.`aliasdomain` IS NULL AND `d`.`id` <> `c`.`standardsubdomain`
+ AND `c`.`customerid` = :customerid
+ AND `d`.`id` = :aliasdomain
+ ");
+ $aliasdomain_check = Database::pexecute_first($aliasdomain_check_stmt, array(
+ 'customerid' => $customerid,
+ 'aliasdomain' => $aliasdomain
+ ), true, true);
+ }
+
+ if (count($ipandports) == 0) {
+ \Froxlor\UI\Response::standard_error('noipportgiven', '', true);
+ }
+
+ if ($aliasdomain_check['id'] != $aliasdomain) {
+ \Froxlor\UI\Response::standard_error('domainisaliasorothercustomer', '', true);
+ }
+
+ if ($issubof <= 0) {
+ $issubof = '0';
+ }
+
+ if ($serveraliasoption != '1' && $serveraliasoption != '2') {
+ $serveraliasoption = '0';
+ }
+
+ $wwwserveralias = ($serveraliasoption == '1') ? '1' : '0';
+ $iswildcarddomain = ($serveraliasoption == '0') ? '1' : '0';
+
+ if ($documentroot != $result['documentroot'] || $ssl_redirect != $result['ssl_redirect'] || $wwwserveralias != $result['wwwserveralias'] || $iswildcarddomain != $result['iswildcarddomain'] || $phpenabled != $result['phpenabled'] || $openbasedir != $result['openbasedir'] || $phpsettingid != $result['phpsettingid'] || $mod_fcgid_starter != $result['mod_fcgid_starter'] || $mod_fcgid_maxrequests != $result['mod_fcgid_maxrequests'] || $specialsettings != $result['specialsettings'] || $notryfiles != $result['notryfiles'] || $writeaccesslog != $result['writeaccesslog'] || $writeerrorlog != $result['writeerrorlog'] || $aliasdomain != $result['aliasdomain'] || $issubof != $result['ismainbutsubto'] || $email_only != $result['email_only'] || ($speciallogfile != $result['speciallogfile'] && $speciallogverified == '1') || $letsencrypt != $result['letsencrypt'] || $http2 != $result['http2'] || $hsts_maxage != $result['hsts'] || $hsts_sub != $result['hsts_sub'] || $hsts_preload != $result['hsts_preload'] || $ocsp_stapling != $result['ocsp_stapling']) {
+ \Froxlor\System\Cronjob::inserttask('1');
+ }
+
+ if ($speciallogfile != $result['speciallogfile'] && $speciallogverified != '1') {
+ $speciallogfile = $result['speciallogfile'];
+ }
+
+ if ($isbinddomain != $result['isbinddomain'] || $zonefile != $result['zonefile'] || $dkim != $result['dkim'] || $isemaildomain != $result['isemaildomain']) {
+ \Froxlor\System\Cronjob::inserttask('4');
+ }
+ // check whether nameserver has been disabled, #581
+ if ($isbinddomain != $result['isbinddomain'] && $isbinddomain == 0) {
+ \Froxlor\System\Cronjob::inserttask('11', $result['domain']);
+ }
+
+ if ($isemaildomain == '0' && $result['isemaildomain'] == '1') {
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_MAIL_USERS . "` WHERE `domainid` = :id
+ ");
+ Database::pexecute($del_stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_MAIL_VIRTUAL . "` WHERE `domainid` = :id
+ ");
+ Database::pexecute($del_stmt, array(
+ 'id' => $id
+ ), true, true);
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] deleted domain #" . $id . " from mail-tables as is-email-domain was set to 0");
+ }
+
+ // check whether LE has been disabled, so we remove the certificate
+ if ($letsencrypt == '0' && $result['letsencrypt'] == '1') {
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = :id
+ ");
+ Database::pexecute($del_stmt, array(
+ 'id' => $id
+ ), true, true);
+ // remove domain from acme.sh / lets encrypt if used
+ \Froxlor\System\Cronjob::inserttask('12', $result['domain']);
+ }
+
+ $updatechildren = '';
+
+ if ($subcanemaildomain == '0' && $result['subcanemaildomain'] != '0') {
+ $updatechildren = ", `isemaildomain` = '0' ";
+ } elseif ($subcanemaildomain == '3' && $result['subcanemaildomain'] != '3') {
+ $updatechildren = ", `isemaildomain` = '1' ";
+ }
+
+ if ($customerid != $result['customerid'] && Settings::Get('panel.allow_domain_change_customer') == '1') {
+ $upd_data = array(
+ 'customerid' => $customerid,
+ 'domainid' => $result['id']
+ );
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_MAIL_USERS . "` SET `customerid` = :customerid WHERE `domainid` = :domainid
+ ");
+ Database::pexecute($upd_stmt, $upd_data, true, true);
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_MAIL_VIRTUAL . "` SET `customerid` = :customerid WHERE `domainid` = :domainid
+ ");
+ Database::pexecute($upd_stmt, $upd_data, true, true);
+ $upd_data = array(
+ 'subdomains' => $subdomains,
+ 'emails' => $emails,
+ 'forwarders' => $email_forwarders,
+ 'accounts' => $email_accounts
+ );
+ $upd_data['customerid'] = $customerid;
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET
+ `subdomains_used` = `subdomains_used` + :subdomains,
+ `emails_used` = `emails_used` + :emails,
+ `email_forwarders_used` = `email_forwarders_used` + :forwarders,
+ `email_accounts_used` = `email_accounts_used` + :accounts
+ WHERE `customerid` = :customerid
+ ");
+ Database::pexecute($upd_stmt, $upd_data, true, true);
+
+ $upd_data['customerid'] = $result['customerid'];
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET
+ `subdomains_used` = `subdomains_used` - :subdomains,
+ `emails_used` = `emails_used` - :emails,
+ `email_forwarders_used` = `email_forwarders_used` - :forwarders,
+ `email_accounts_used` = `email_accounts_used` - :accounts
+ WHERE `customerid` = :customerid
+ ");
+ Database::pexecute($upd_stmt, $upd_data, true, true);
+ }
+
+ if ($adminid != $result['adminid'] && Settings::Get('panel.allow_domain_change_admin') == '1') {
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_ADMINS . "` SET `domains_used` = `domains_used` + 1 WHERE `adminid` = :adminid
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'adminid' => $adminid
+ ), true, true);
+
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_ADMINS . "` SET `domains_used` = `domains_used` - 1 WHERE `adminid` = :adminid
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'adminid' => $result['adminid']
+ ), true, true);
+ }
+
+ $_update_data = array();
+
+ if ($ssfs == 1) {
+ $_update_data['specialsettings'] = $specialsettings;
+ $_update_data['ssl_specialsettings'] = $ssl_specialsettings;
+ $_update_data['include_specialsettings'] = $include_specialsettings;
+ $upd_specialsettings = ", `specialsettings` = :specialsettings, `ssl_specialsettings` = :ssl_specialsettings, `include_specialsettings` = :include_specialsettings ";
+ } else {
+ $upd_specialsettings = '';
+ unset($_update_data['specialsettings']);
+ unset($_update_data['ssl_specialsettings']);
+ unset($_update_data['include_specialsettings']);
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `specialsettings`='', `ssl_specialsettings`='', `include_specialsettings`='0' WHERE `parentdomainid` = :id
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'id' => $id
+ ), true, true);
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] removed specialsettings on all subdomains of domain #" . $id);
+ }
+
+ $wwwserveralias = ($serveraliasoption == '1') ? '1' : '0';
+ $iswildcarddomain = ($serveraliasoption == '0') ? '1' : '0';
+
+ $update_data = array();
+ $update_data['customerid'] = $customerid;
+ $update_data['adminid'] = $adminid;
+ $update_data['documentroot'] = $documentroot;
+ $update_data['ssl_redirect'] = $ssl_redirect;
+ $update_data['aliasdomain'] = ($aliasdomain != 0 && $alias_check == 0) ? $aliasdomain : null;
+ $update_data['isbinddomain'] = $isbinddomain;
+ $update_data['isemaildomain'] = $isemaildomain;
+ $update_data['email_only'] = $email_only;
+ $update_data['subcanemaildomain'] = $subcanemaildomain;
+ $update_data['dkim'] = $dkim;
+ $update_data['caneditdomain'] = $caneditdomain;
+ $update_data['zonefile'] = $zonefile;
+ $update_data['wwwserveralias'] = $wwwserveralias;
+ $update_data['iswildcarddomain'] = $iswildcarddomain;
+ $update_data['phpenabled'] = $phpenabled;
+ $update_data['openbasedir'] = $openbasedir;
+ $update_data['speciallogfile'] = $speciallogfile;
+ $update_data['phpsettingid'] = $phpsettingid;
+ $update_data['mod_fcgid_starter'] = $mod_fcgid_starter;
+ $update_data['mod_fcgid_maxrequests'] = $mod_fcgid_maxrequests;
+ $update_data['specialsettings'] = $specialsettings;
+ $update_data['ssl_specialsettings'] = $ssl_specialsettings;
+ $update_data['include_specialsettings'] = $include_specialsettings;
+ $update_data['notryfiles'] = $notryfiles;
+ $update_data['writeaccesslog'] = $writeaccesslog;
+ $update_data['writeerrorlog'] = $writeerrorlog;
+ $update_data['registration_date'] = $registration_date;
+ $update_data['termination_date'] = $termination_date;
+ $update_data['ismainbutsubto'] = $issubof;
+ $update_data['letsencrypt'] = $letsencrypt;
+ $update_data['http2'] = $http2;
+ $update_data['hsts'] = $hsts_maxage;
+ $update_data['hsts_sub'] = $hsts_sub;
+ $update_data['hsts_preload'] = $hsts_preload;
+ $update_data['ocsp_stapling'] = $ocsp_stapling;
+ $update_data['override_tls'] = $override_tls;
+ $update_data['ssl_protocols'] = implode(",", $ssl_protocols);
+ $update_data['ssl_cipher_list'] = $ssl_cipher_list;
+ $update_data['tlsv13_cipher_list'] = $tlsv13_cipher_list;
+ $update_data['sslenabled'] = $sslenabled;
+ $update_data['honorcipherorder'] = $honorcipherorder;
+ $update_data['sessiontickets'] = $sessiontickets;
+ $update_data['id'] = $id;
+
+ $update_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_DOMAINS . "` SET
+ `customerid` = :customerid,
+ `adminid` = :adminid,
+ `documentroot` = :documentroot,
+ `ssl_redirect` = :ssl_redirect,
+ `aliasdomain` = :aliasdomain,
+ `isbinddomain` = :isbinddomain,
+ `isemaildomain` = :isemaildomain,
+ `email_only` = :email_only,
+ `subcanemaildomain` = :subcanemaildomain,
+ `dkim` = :dkim,
+ `caneditdomain` = :caneditdomain,
+ `zonefile` = :zonefile,
+ `wwwserveralias` = :wwwserveralias,
+ `iswildcarddomain` = :iswildcarddomain,
+ `phpenabled` = :phpenabled,
+ `openbasedir` = :openbasedir,
+ `speciallogfile` = :speciallogfile,
+ `phpsettingid` = :phpsettingid,
+ `mod_fcgid_starter` = :mod_fcgid_starter,
+ `mod_fcgid_maxrequests` = :mod_fcgid_maxrequests,
+ `specialsettings` = :specialsettings,
+ `ssl_specialsettings` = :ssl_specialsettings,
+ `include_specialsettings` = :include_specialsettings,
+ `notryfiles` = :notryfiles,
+ `writeaccesslog` = :writeaccesslog,
+ `writeerrorlog` = :writeerrorlog,
+ `registration_date` = :registration_date,
+ `termination_date` = :termination_date,
+ `ismainbutsubto` = :ismainbutsubto,
+ `letsencrypt` = :letsencrypt,
+ `http2` = :http2,
+ `hsts` = :hsts,
+ `hsts_sub` = :hsts_sub,
+ `hsts_preload` = :hsts_preload,
+ `ocsp_stapling` = :ocsp_stapling,
+ `override_tls` = :override_tls,
+ `ssl_protocols` = :ssl_protocols,
+ `ssl_cipher_list` = :ssl_cipher_list,
+ `tlsv13_cipher_list` = :tlsv13_cipher_list,
+ `ssl_enabled` = :sslenabled,
+ `ssl_honorcipherorder` = :honorcipherorder,
+ `ssl_sessiontickets` = :sessiontickets
+ WHERE `id` = :id
+ ");
+ Database::pexecute($update_stmt, $update_data, true, true);
+
+ $_update_data['customerid'] = $customerid;
+ $_update_data['adminid'] = $adminid;
+ $_update_data['phpenabled'] = $phpenabled;
+ $_update_data['openbasedir'] = $openbasedir;
+ $_update_data['mod_fcgid_starter'] = $mod_fcgid_starter;
+ $_update_data['mod_fcgid_maxrequests'] = $mod_fcgid_maxrequests;
+ $_update_data['notryfiles'] = $notryfiles;
+ $_update_data['writeaccesslog'] = $writeaccesslog;
+ $_update_data['writeerrorlog'] = $writeerrorlog;
+ $_update_data['override_tls'] = $override_tls;
+ $_update_data['ssl_protocols'] = implode(",", $ssl_protocols);
+ $_update_data['ssl_cipher_list'] = $ssl_cipher_list;
+ $_update_data['tlsv13_cipher_list'] = $tlsv13_cipher_list;
+ $_update_data['honorcipherorder'] = $honorcipherorder;
+ $_update_data['sessiontickets'] = $sessiontickets;
+ $_update_data['parentdomainid'] = $id;
+
+ // if php config is to be set for all subdomains, check here
+ $update_phpconfig = '';
+ if ($phpfs == 1) {
+ $_update_data['phpsettingid'] = $phpsettingid;
+ $update_phpconfig = ", `phpsettingid` = :phpsettingid";
+ }
+ // if we have no more ssl-ip's for this domain,
+ // all its subdomains must have "ssl-redirect = 0"
+ // and disable let's encrypt
+ $update_sslredirect = '';
+ if (count($ssl_ipandports) == 1 && $ssl_ipandports[0] == - 1) {
+ $update_sslredirect = ", `ssl_redirect` = '0', `letsencrypt` = '0' ";
+ }
+
+ $_update_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_DOMAINS . "` SET
+ `customerid` = :customerid,
+ `adminid` = :adminid,
+ `phpenabled` = :phpenabled,
+ `openbasedir` = :openbasedir,
+ `mod_fcgid_starter` = :mod_fcgid_starter,
+ `mod_fcgid_maxrequests` = :mod_fcgid_maxrequests,
+ `notryfiles` = :notryfiles,
+ `writeaccesslog` = :writeaccesslog,
+ `writeerrorlog` = :writeerrorlog,
+ `override_tls` = :override_tls,
+ `ssl_protocols` = :ssl_protocols,
+ `ssl_cipher_list` = :ssl_cipher_list,
+ `tlsv13_cipher_list` = :tlsv13_cipher_list,
+ `ssl_honorcipherorder` = :honorcipherorder,
+ `ssl_sessiontickets` = :sessiontickets
+ " . $update_phpconfig . $upd_specialsettings . $updatechildren . $update_sslredirect . "
+ WHERE `parentdomainid` = :parentdomainid
+ ");
+ Database::pexecute($_update_stmt, $_update_data, true, true);
+
+ // insert a rebuild-task
+ \Froxlor\System\Cronjob::inserttask('1');
+
+ // Cleanup domain <-> ip mapping
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :id
+ ");
+ Database::pexecute($del_stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ $ins_stmt = Database::prepare("
+ INSERT INTO `" . TABLE_DOMAINTOIP . "` SET `id_domain` = :domainid, `id_ipandports` = :ipportid
+ ");
+
+ foreach ($ipandports as $ipportid) {
+ Database::pexecute($ins_stmt, array(
+ 'domainid' => $id,
+ 'ipportid' => $ipportid
+ ), true, true);
+ }
+ foreach ($ssl_ipandports as $ssl_ipportid) {
+ if ($ssl_ipportid > 0) {
+ Database::pexecute($ins_stmt, array(
+ 'domainid' => $id,
+ 'ipportid' => $ssl_ipportid
+ ), true, true);
+ }
+ }
+
+ // Cleanup domain <-> ip mapping for subdomains
+ $domainidsresult_stmt = Database::prepare("
+ SELECT `id` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `parentdomainid` = :id
+ ");
+ Database::pexecute($domainidsresult_stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ while ($row = $domainidsresult_stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :rowid
+ ");
+ Database::pexecute($del_stmt, array(
+ 'rowid' => $row['id']
+ ), true, true);
+
+ $ins_stmt = Database::prepare("
+ INSERT INTO `" . TABLE_DOMAINTOIP . "` SET
+ `id_domain` = :rowid,
+ `id_ipandports` = :ipportid
+ ");
+
+ foreach ($ipandports as $ipportid) {
+ Database::pexecute($ins_stmt, array(
+ 'rowid' => $row['id'],
+ 'ipportid' => $ipportid
+ ), true, true);
+ }
+ foreach ($ssl_ipandports as $ssl_ipportid) {
+ if ($ssl_ipportid > 0) {
+ Database::pexecute($ins_stmt, array(
+ 'rowid' => $row['id'],
+ 'ipportid' => $ssl_ipportid
+ ), true, true);
+ }
+ }
+ }
+ if ($result['aliasdomain'] != $aliasdomain && is_numeric($result['aliasdomain'])) {
+ // trigger when domain id for alias destination has changed: both for old and new destination
+ \Froxlor\Domain\Domain::triggerLetsEncryptCSRForAliasDestinationDomain($result['aliasdomain'], $this->logger());
+ \Froxlor\Domain\Domain::triggerLetsEncryptCSRForAliasDestinationDomain($aliasdomain, $this->logger());
+ }
+ if ($result['wwwserveralias'] != $wwwserveralias || $result['letsencrypt'] != $letsencrypt) {
+ // or when wwwserveralias or letsencrypt was changed
+ \Froxlor\Domain\Domain::triggerLetsEncryptCSRForAliasDestinationDomain($aliasdomain, $this->logger());
+ if ((int) $aliasdomain === 0) {
+ // in case the wwwserveralias is set on a main domain, $aliasdomain is 0
+ // --> the call just above to triggerLetsEncryptCSRForAliasDestinationDomain
+ // is a noop...let's repeat it with the domain id of the main domain
+ \Froxlor\Domain\Domain::triggerLetsEncryptCSRForAliasDestinationDomain($id, $this->logger());
+ }
+ }
+
+ $idna_convert = new \Froxlor\Idna\IdnaWrapper();
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] updated domain '" . $idna_convert->decode($result['domain']) . "'");
+ $result = $this->apiCall('Domains.get', array(
+ 'domainname' => $result['domain']
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * delete a domain entry by either id or domainname
+ *
+ * @param int $id
+ * optional, the domain-id
+ * @param string $domainname
+ * optional, the domainname
+ * @param bool $delete_mainsubdomains
+ * optional, remove also domains that are subdomains of this domain but added as main domains; default false
+ * @param bool $is_stdsubdomain
+ * optional, default false, specify whether it's a std-subdomain you are deleting as it does not count as subdomain-resource
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function delete()
+ {
+ if ($this->isAdmin()) {
+ $id = $this->getParam('id', true, 0);
+ $dn_optional = ($id <= 0 ? false : true);
+ $domainname = $this->getParam('domainname', $dn_optional, '');
+ $is_stdsubdomain = $this->getParam('is_stdsubdomain', true, 0);
+ $remove_subbutmain_domains = $this->getParam('delete_mainsubdomains', true, 0);
+
+ $result = $this->apiCall('Domains.get', array(
+ 'id' => $id,
+ 'domainname' => $domainname
+ ));
+ $id = $result['id'];
+
+ // check for deletion of main-domains which are logically subdomains, #329
+ $rsd_sql = '';
+ if ($remove_subbutmain_domains) {
+ $rsd_sql .= " OR `ismainbutsubto` = :id";
+ }
+
+ $subresult_stmt = Database::prepare("
+ SELECT `id` FROM `" . TABLE_PANEL_DOMAINS . "`
+ WHERE (`id` = :id OR `parentdomainid` = :id " . $rsd_sql . ")");
+ Database::pexecute($subresult_stmt, array(
+ 'id' => $id
+ ), true, true);
+ $idString = array();
+ $paramString = array();
+ while ($subRow = $subresult_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $idString[] = "`domainid` = :domain_" . (int) $subRow['id'];
+ $paramString['domain_' . $subRow['id']] = $subRow['id'];
+ }
+ $idString = implode(' OR ', $idString);
+
+ if ($idString != '') {
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_MAIL_USERS . "` WHERE " . $idString);
+ Database::pexecute($del_stmt, $paramString, true, true);
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_MAIL_VIRTUAL . "` WHERE " . $idString);
+ Database::pexecute($del_stmt, $paramString, true, true);
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] deleted domain/s from mail-tables");
+ }
+
+ // if mainbutsubto-domains are not to be deleted, re-assign the (ismainbutsubto value of the main
+ // domain which is being deleted) as their new ismainbutsubto value
+ if ($remove_subbutmain_domains !== 1) {
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_DOMAINS . "` SET
+ `ismainbutsubto` = :newIsMainButSubtoValue
+ WHERE `ismainbutsubto` = :deletedMainDomainId
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'newIsMainButSubtoValue' => $result['ismainbutsubto'],
+ 'deletedMainDomainId' => $id
+ ), true, true);
+ }
+
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_PANEL_DOMAINS . "`
+ WHERE `id` = :id OR `parentdomainid` = :id " . $rsd_sql);
+ Database::pexecute($del_stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ $deleted_domains = $del_stmt->rowCount();
+
+ if ($is_stdsubdomain == 0) {
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET
+ `subdomains_used` = `subdomains_used` - :domaincount
+ WHERE `customerid` = :customerid");
+ Database::pexecute($upd_stmt, array(
+ 'domaincount' => ($deleted_domains - 1),
+ 'customerid' => $result['customerid']
+ ), true, true);
+
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_ADMINS . "` SET
+ `domains_used` = `domains_used` - 1
+ WHERE `adminid` = :adminid");
+ Database::pexecute($upd_stmt, array(
+ 'adminid' => $this->getUserDetail('adminid')
+ ), true, true);
+ }
+
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET
+ `standardsubdomain` = '0'
+ WHERE `standardsubdomain` = :id AND `customerid` = :customerid");
+ Database::pexecute($upd_stmt, array(
+ 'id' => $result['id'],
+ 'customerid' => $result['customerid']
+ ), true, true);
+
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_DOMAINTOIP . "`
+ WHERE `id_domain` = :domainid");
+ Database::pexecute($del_stmt, array(
+ 'domainid' => $id
+ ), true, true);
+
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_PANEL_DOMAINREDIRECTS . "`
+ WHERE `did` = :domainid");
+ Database::pexecute($del_stmt, array(
+ 'domainid' => $id
+ ), true, true);
+
+ // remove certificate from domain_ssl_settings, fixes #1596
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "`
+ WHERE `domainid` = :domainid");
+ Database::pexecute($del_stmt, array(
+ 'domainid' => $id
+ ), true, true);
+
+ // remove possible existing DNS entries
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_DOMAIN_DNS . "`
+ WHERE `domain_id` = :domainid
+ ");
+ Database::pexecute($del_stmt, array(
+ 'domainid' => $id
+ ), true, true);
+
+ \Froxlor\Domain\Domain::triggerLetsEncryptCSRForAliasDestinationDomain($result['aliasdomain'], $this->logger());
+
+ // remove domains DNS from powerDNS if used, #581
+ \Froxlor\System\Cronjob::inserttask('11', $result['domain']);
+
+ // remove domain from acme.sh / lets encrypt if used
+ \Froxlor\System\Cronjob::inserttask('12', $result['domain']);
+
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] deleted domain/subdomains (#" . $result['id'] . ")");
+ \Froxlor\User::updateCounters();
+ \Froxlor\System\Cronjob::inserttask('1');
+ // Using nameserver, insert a task which rebuilds the server config
+ \Froxlor\System\Cronjob::inserttask('4');
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * validate given ips
+ *
+ * @param int|string|array $p_ipandports
+ * @param boolean $ssl
+ * default false
+ * @param int $edit_id
+ * default 0
+ *
+ * @throws \Exception
+ * @return array
+ */
+ private function validateIpAddresses($p_ipandports = null, $ssl = false, $edit_id = 0)
+ {
+ // when adding a new domain and no ip is given, we try to use the
+ // system-default, check here if there is none
+ // this is not required for ssl-enabled ip's
+ if ($edit_id <= 0 && ! $ssl && empty($p_ipandports)) {
+ throw new \Exception("No IPs given, unable to add domain (no default IPs set?)", 406);
+ }
+
+ // convert given value(s) correctly
+ $ipandports = array();
+ if (! empty($p_ipandports) && is_numeric($p_ipandports)) {
+ $p_ipandports = array(
+ $p_ipandports
+ );
+ }
+ if (! empty($p_ipandports) && ! is_array($p_ipandports)) {
+ $p_ipandports = json_decode($p_ipandports, true);
+ }
+
+ // check whether there are ip usage restrictions
+ $additional_ip_condition = '';
+ $aip_param = array();
+ if ($this->getUserDetail('ip') != "-1") {
+ // handle multiple-ip-array
+ $additional_ip_condition = " AND `ip` IN (" . implode(",", json_decode($this->getUserDetail('ip'), true)) . ") ";
+ }
+
+ if (! empty($p_ipandports) && is_array($p_ipandports)) {
+ $ipandport_check_stmt = Database::prepare("
+ SELECT `id`, `ip`, `port`
+ FROM `" . TABLE_PANEL_IPSANDPORTS . "`
+ WHERE `id` = :ipandport " . ($ssl ? " AND `ssl` = '1'" : "") . $additional_ip_condition);
+ foreach ($p_ipandports as $ipandport) {
+ if (trim($ipandport) == "") {
+ continue;
+ }
+ // fix if no ip/port is checked
+ if (trim($ipandport) < 1) {
+ continue;
+ }
+ $ipandport = intval($ipandport);
+ $ip_params = array_merge(array(
+ 'ipandport' => $ipandport
+ ), $aip_param);
+ $ipandport_check = Database::pexecute_first($ipandport_check_stmt, $ip_params, true, true);
+ if (! isset($ipandport_check['id']) || $ipandport_check['id'] == '0' || $ipandport_check['id'] != $ipandport) {
+ \Froxlor\UI\Response::standard_error('ipportdoesntexist', '', true);
+ } else {
+ $ipandports[] = $ipandport;
+ }
+ }
+ } elseif ($edit_id > 0) {
+ // set currently used ip's
+ $ipsresult_stmt = Database::prepare("
+ SELECT d2i.`id_ipandports`
+ FROM `" . TABLE_DOMAINTOIP . "` d2i
+ LEFT JOIN `" . TABLE_PANEL_IPSANDPORTS . "` i ON i.id = d2i.id_ipandports
+ WHERE d2i.`id_domain` = :id AND i.`ssl` = " . ($ssl ? "'1'" : "'0'"));
+ Database::pexecute($ipsresult_stmt, array(
+ 'id' => $edit_id
+ ), true, true);
+ while ($ipsresultrow = $ipsresult_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $ipandports[] = $ipsresultrow['id_ipandports'];
+ }
+ }
+ return $ipandports;
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/EmailAccounts.php b/lib/Froxlor/Api/Commands/EmailAccounts.php
new file mode 100644
index 00000000..017359dd
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/EmailAccounts.php
@@ -0,0 +1,497 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class EmailAccounts extends \Froxlor\Api\ApiCommand implements \Froxlor\Api\ResourceEntity
+{
+
+ /**
+ * add a new email account for a given email-address either by id or emailaddr
+ *
+ * @param int $id
+ * optional email-address-id of email-address to add the account for
+ * @param string $emailaddr
+ * optional email-address to add the account for
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ * @param string $email_password
+ * password for the account
+ * @param string $alternative_email
+ * optional email address to send account information to, default is the account that is being created
+ * @param int $email_quota
+ * optional quota if enabled in MB, default 0
+ * @param bool $sendinfomail
+ * optional, sends the welcome message to the new account (needed for creation, without the user won't be able to login before any mail is received), default 1 (true)
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function add()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ if ($this->getUserDetail('email_accounts_used') < $this->getUserDetail('email_accounts') || $this->getUserDetail('email_accounts') == '-1') {
+
+ // parameter
+ $id = $this->getParam('id', true, 0);
+ $ea_optional = ($id <= 0 ? false : true);
+ $emailaddr = $this->getParam('emailaddr', $ea_optional, '');
+ $email_password = $this->getParam('email_password');
+ $alternative_email = $this->getParam('alternative_email', true, '');
+ $quota = $this->getParam('email_quota', true, 0);
+ $sendinfomail = $this->getBoolParam('sendinfomail', true, 1);
+
+ // validation
+ $quota = \Froxlor\Validate\Validate::validate($quota, 'email_quota', '/^\d+$/', 'vmailquotawrong', array(), true);
+
+ // get needed customer info to reduce the email-account-counter by one
+ $customer = $this->getCustomerData('email_accounts');
+
+ // check for imap||pop3 == 1, see #1298
+ if ($customer['imap'] != '1' && $customer['pop3'] != '1') {
+ \Froxlor\UI\Response::standard_error('notallowedtouseaccounts', '', true);
+ }
+
+ // get email address
+ $result = $this->apiCall('Emails.get', array(
+ 'id' => $id,
+ 'emailaddr' => $emailaddr
+ ));
+ $id = $result['id'];
+
+ $idna_convert = new \Froxlor\Idna\IdnaWrapper();
+ $email_full = $result['email_full'];
+ $username = $email_full;
+ $password = \Froxlor\Validate\Validate::validate($email_password, 'password', '', '', array(), true);
+ $password = \Froxlor\System\Crypt::validatePassword($password, true);
+
+ if ($result['popaccountid'] != 0) {
+ throw new \Exception("Email address '" . $email_full . "' has already an account assigned.", 406);
+ }
+
+ if (\Froxlor\Validate\Check::checkMailAccDeletionState($email_full)) {
+ \Froxlor\UI\Response::standard_error(array(
+ 'mailaccistobedeleted'
+ ), $email_full, true);
+ }
+
+ // alternative email address to send info to
+ if (Settings::Get('panel.sendalternativemail') == 1) {
+ $alternative_email = $idna_convert->encode(\Froxlor\Validate\Validate::validate($alternative_email, 'alternative_email', '', '', array(), true));
+ if (!empty($alternative_email) && ! \Froxlor\Validate\Validate::validateEmail($alternative_email)) {
+ \Froxlor\UI\Response::standard_error('alternativeemailiswrong', $alternative_email, true);
+ }
+ } else {
+ $alternative_email = '';
+ }
+
+ // validate quota if enabled
+ if (Settings::Get('system.mail_quota_enabled') == 1) {
+ if ($customer['email_quota'] != '-1' && ($quota == 0 || ($quota + $customer['email_quota_used']) > $customer['email_quota'])) {
+ \Froxlor\UI\Response::standard_error('allocatetoomuchquota', $quota, true);
+ }
+ } else {
+ // disable
+ $quota = 0;
+ }
+
+ if ($password == $email_full) {
+ \Froxlor\UI\Response::standard_error('passwordshouldnotbeusername', '', true);
+ }
+
+ // encrypt the password
+ $cryptPassword = \Froxlor\System\Crypt::makeCryptPassword($password);
+
+ $email_user = substr($email_full, 0, strrpos($email_full, "@"));
+ $email_domain = substr($email_full, strrpos($email_full, "@") + 1);
+ $maildirname = trim(Settings::Get('system.vmail_maildirname'));
+ // Add trailing slash to Maildir if needed
+ $maildirpath = $maildirname;
+ if (! empty($maildirname) && substr($maildirname, - 1) != "/") {
+ $maildirpath .= "/";
+ }
+
+ // insert data
+ $stmt = Database::prepare("INSERT INTO `" . TABLE_MAIL_USERS . "` SET
+ `customerid` = :cid,
+ `email` = :email,
+ `username` = :username," . (Settings::Get('system.mailpwcleartext') == '1' ? '`password` = :password, ' : '') . "
+ `password_enc` = :password_enc,
+ `homedir` = :homedir,
+ `maildir` = :maildir,
+ `uid` = :uid,
+ `gid` = :gid,
+ `domainid` = :domainid,
+ `postfix` = 'y',
+ `quota` = :quota,
+ `imap` = :imap,
+ `pop3` = :pop3
+ ");
+ $params = array(
+ "cid" => $customer['customerid'],
+ "email" => $email_full,
+ "username" => $username,
+ "password_enc" => $cryptPassword,
+ "homedir" => Settings::Get('system.vmail_homedir'),
+ "maildir" => $customer['loginname'] . '/' . $email_domain . "/" . $email_user . "/" . $maildirpath,
+ "uid" => Settings::Get('system.vmail_uid'),
+ "gid" => Settings::Get('system.vmail_gid'),
+ "domainid" => $result['domainid'],
+ "quota" => $quota,
+ "imap" => $customer['imap'],
+ "pop3" => $customer['pop3']
+ );
+ if (Settings::Get('system.mailpwcleartext') == '1') {
+ $params["password"] = $password;
+ }
+ Database::pexecute($stmt, $params, true, true);
+ $popaccountid = Database::lastInsertId();
+
+ // add email address to its destination field
+ $result['destination'] .= ' ' . $email_full;
+ $stmt = Database::prepare("
+ UPDATE `" . TABLE_MAIL_VIRTUAL . "` SET `destination` = :destination, `popaccountid` = :popaccountid
+ WHERE `customerid`= :cid AND `id`= :id
+ ");
+ $params = array(
+ "destination" => \Froxlor\FileDir::makeCorrectDestination($result['destination']),
+ "popaccountid" => $popaccountid,
+ "cid" => $customer['customerid'],
+ "id" => $id
+ );
+ Database::pexecute($stmt, $params, true, true);
+
+ // update customer usage
+ Customers::increaseUsage($customer['customerid'], 'email_accounts_used');
+ Customers::increaseUsage($customer['customerid'], 'email_quota_used', '', $quota);
+
+ if ($sendinfomail) {
+ // replacer array for mail to create account on server
+ $replace_arr = array(
+ 'EMAIL' => $email_full,
+ 'USERNAME' => $username,
+ 'PASSWORD' => $password,
+ 'SALUTATION' => \Froxlor\User::getCorrectUserSalutation($customer),
+ 'NAME' => $customer['name'],
+ 'FIRSTNAME' => $customer['firstname'],
+ 'COMPANY' => $customer['company'],
+ 'CUSTOMER_NO' => $customer['customernumber']
+ );
+
+ // get the customers admin
+ $stmt = Database::prepare("SELECT `name`, `email` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `adminid`= :adminid");
+ $admin = Database::pexecute_first($stmt, array(
+ "adminid" => $customer['adminid']
+ ));
+
+ // get template for mail subject
+ $mail_subject = $this->getMailTemplate($customer, 'mails', 'pop_success_subject', $replace_arr, $this->lng['mails']['pop_success']['subject']);
+ // get template for mail body
+ $mail_body = $this->getMailTemplate($customer, 'mails', 'pop_success_mailbody', $replace_arr, $this->lng['mails']['pop_success']['mailbody']);
+
+ $_mailerror = false;
+ $mailerr_msg = "";
+ try {
+ $this->mailer()->setFrom($admin['email'], \Froxlor\User::getCorrectUserSalutation($admin));
+ $this->mailer()->Subject = $mail_subject;
+ $this->mailer()->AltBody = $mail_body;
+ $this->mailer()->msgHTML(str_replace("\n", " ", $mail_body));
+ $this->mailer()->addAddress($email_full);
+ $this->mailer()->send();
+ } catch (\PHPMailer\PHPMailer\Exception $e) {
+ $mailerr_msg = $e->errorMessage();
+ $_mailerror = true;
+ } catch (\Exception $e) {
+ $mailerr_msg = $e->getMessage();
+ $_mailerror = true;
+ }
+
+ if ($_mailerror) {
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_ERR, "[API] Error sending mail: " . $mailerr_msg);
+ \Froxlor\UI\Response::standard_error('errorsendingmail', $email_full, true);
+ }
+
+ $this->mailer()->clearAddresses();
+
+ // customer wants to send the e-mail to an alternative email address too
+ if (Settings::Get('panel.sendalternativemail') == 1 && !empty($alternative_email)) {
+ // get template for mail subject
+ $mail_subject = $this->getMailTemplate($customer, 'mails', 'pop_success_alternative_subject', $replace_arr, $this->lng['mails']['pop_success_alternative']['subject']);
+ // get template for mail body
+ $mail_body = $this->getMailTemplate($customer, 'mails', 'pop_success_alternative_mailbody', $replace_arr, $this->lng['mails']['pop_success_alternative']['mailbody']);
+
+ $_mailerror = false;
+ try {
+ $this->mailer()->setFrom($admin['email'], \Froxlor\User::getCorrectUserSalutation($admin));
+ $this->mailer()->Subject = $mail_subject;
+ $this->mailer()->AltBody = $mail_body;
+ $this->mailer()->msgHTML(str_replace("\n", " ", $mail_body));
+ $this->mailer()->addAddress($idna_convert->encode($alternative_email), \Froxlor\User::getCorrectUserSalutation($customer));
+ $this->mailer()->send();
+ } catch (\PHPMailer\PHPMailer\Exception $e) {
+ $mailerr_msg = $e->errorMessage();
+ $_mailerror = true;
+ } catch (\Exception $e) {
+ $mailerr_msg = $e->getMessage();
+ $_mailerror = true;
+ }
+
+ if ($_mailerror) {
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_ERR, "[API] Error sending mail: " . $mailerr_msg);
+ \Froxlor\UI\Response::standard_error(array(
+ 'errorsendingmail'
+ ), $alternative_email, true);
+ }
+
+ $this->mailer()->clearAddresses();
+ }
+ }
+
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] added email account for '" . $result['email_full'] . "'");
+ $result = $this->apiCall('Emails.get', array(
+ 'emailaddr' => $result['email_full']
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("No more resources available", 406);
+ }
+
+ /**
+ * You cannot directly get an email account.
+ * You need to call Emails.get()
+ */
+ public function get()
+ {
+ throw new \Exception('You cannot directly get an email account. You need to call Emails.get()', 303);
+ }
+
+ /**
+ * update email-account entry for given email-address by either id or email-address
+ *
+ * @param int $id
+ * optional, the email-address-id
+ * @param string $emailaddr
+ * optional, the email-address to update
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ * @param int $email_quota
+ * optional, update quota
+ * @param string $email_password
+ * optional, update password
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function update()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ // parameter
+ $id = $this->getParam('id', true, 0);
+ $ea_optional = ($id <= 0 ? false : true);
+ $emailaddr = $this->getParam('emailaddr', $ea_optional, '');
+
+ // validation
+ $result = $this->apiCall('Emails.get', array(
+ 'id' => $id,
+ 'emailaddr' => $emailaddr
+ ));
+ $id = $result['id'];
+
+ if (empty($result['popaccountid']) || $result['popaccountid'] == 0) {
+ throw new \Exception("Email address '" . $result['email_full'] . "' has no account assigned.", 406);
+ }
+
+ $password = $this->getParam('email_password', true, '');
+ $quota = $this->getParam('email_quota', true, $result['quota']);
+
+ // get needed customer info to reduce the email-account-counter by one
+ $customer = $this->getCustomerData();
+
+ // validation
+ $quota = \Froxlor\Validate\Validate::validate($quota, 'email_quota', '/^\d+$/', 'vmailquotawrong', array(), true);
+
+ $upd_query = "";
+ $upd_params = array(
+ "id" => $result['popaccountid'],
+ "cid" => $customer['customerid']
+ );
+ if (! empty($password)) {
+ if ($password == $result['email_full']) {
+ \Froxlor\UI\Response::standard_error('passwordshouldnotbeusername', '', true);
+ }
+ $password = \Froxlor\System\Crypt::validatePassword($password, true);
+ $cryptPassword = \Froxlor\System\Crypt::makeCryptPassword($password);
+ $upd_query .= (Settings::Get('system.mailpwcleartext') == '1' ? "`password` = :password, " : '') . "`password_enc`= :password_enc";
+ $upd_params['password_enc'] = $cryptPassword;
+ if (Settings::Get('system.mailpwcleartext') == '1') {
+ $upd_params['password'] = $password;
+ }
+ }
+
+ if (Settings::Get('system.mail_quota_enabled') == 1) {
+ if ($quota != $result['quota']) {
+ if ($customer['email_quota'] != '-1' && ($quota == 0 || ($quota + $customer['email_quota_used'] - $result['quota']) > $customer['email_quota'])) {
+ \Froxlor\UI\Response::standard_error('allocatetoomuchquota', $quota, true);
+ }
+ if (! empty($upd_query)) {
+ $upd_query .= ", ";
+ }
+ $upd_query .= "`quota` = :quota";
+ $upd_params['quota'] = $quota;
+ }
+ } else {
+ // disable
+ $quota = 0;
+ }
+
+ // build update query
+ if (! empty($upd_query)) {
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_MAIL_USERS . "` SET " . $upd_query . " WHERE `id` = :id AND `customerid`= :cid
+ ");
+ Database::pexecute($upd_stmt, $upd_params, true, true);
+ }
+
+ if ($customer['email_quota'] != '-1') {
+ Customers::increaseUsage($customer['customerid'], 'email_quota_used', '', ($quota - $result['quota']));
+ Admins::increaseUsage($customer['adminid'], 'email_quota_used', '', ($quota - $result['quota']));
+ }
+
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] updated email account '" . $result['email_full'] . "'");
+ $result = $this->apiCall('Emails.get', array(
+ 'emailaddr' => $result['email_full']
+ ));
+ return $this->response(200, "successful", $result);
+ }
+
+ /**
+ * You cannot directly list email accounts.
+ * You need to call Emails.listing()
+ */
+ public function listing()
+ {
+ throw new \Exception('You cannot directly list email accounts. You need to call Emails.listing()', 303);
+ }
+
+ /**
+ * You cannot directly count email accounts.
+ * You need to call Emails.listingCount()
+ */
+ public function listingCount()
+ {
+ throw new \Exception('You cannot directly count email accounts. You need to call Emails.listingCount()', 303);
+ }
+
+ /**
+ * delete email-account entry for given email-address by either id or email-address
+ *
+ * @param int $id
+ * optional, the email-address-id
+ * @param string $emailaddr
+ * optional, the email-address to delete the account for
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ * @param bool $delete_userfiles
+ * optional, default false
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function delete()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ // parameter
+ $id = $this->getParam('id', true, 0);
+ $ea_optional = ($id <= 0 ? false : true);
+ $emailaddr = $this->getParam('emailaddr', $ea_optional, '');
+ $delete_userfiles = $this->getBoolParam('delete_userfiles', true, 0);
+
+ // validation
+ $result = $this->apiCall('Emails.get', array(
+ 'id' => $id,
+ 'emailaddr' => $emailaddr
+ ));
+ $id = $result['id'];
+
+ if (empty($result['popaccountid']) || $result['popaccountid'] == 0) {
+ throw new \Exception("Email address '" . $result['email_full'] . "' has no account assigned.", 406);
+ }
+
+ // get needed customer info to reduce the email-account-counter by one
+ $customer = $this->getCustomerData();
+
+ // delete entry
+ $stmt = Database::prepare("
+ DELETE FROM `" . TABLE_MAIL_USERS . "` WHERE `customerid`= :cid AND `id`= :id
+ ");
+ Database::pexecute($stmt, array(
+ "cid" => $customer['customerid'],
+ "id" => $result['popaccountid']
+ ), true, true);
+
+ // update mail-virtual entry
+ $result['destination'] = str_replace($result['email_full'], '', $result['destination']);
+
+ $stmt = Database::prepare("
+ UPDATE `" . TABLE_MAIL_VIRTUAL . "` SET `destination` = :dest, `popaccountid` = '0' WHERE `customerid`= :cid AND `id`= :id
+ ");
+ $params = array(
+ "dest" => \Froxlor\FileDir::makeCorrectDestination($result['destination']),
+ "cid" => $customer['customerid'],
+ "id" => $id
+ );
+ Database::pexecute($stmt, $params, true, true);
+ $result['popaccountid'] = 0;
+
+ if (Settings::Get('system.mail_quota_enabled') == '1' && $customer['email_quota'] != '-1') {
+ $quota = (int) $result['quota'];
+ } else {
+ $quota = 0;
+ }
+
+ if ($delete_userfiles) {
+ \Froxlor\System\Cronjob::inserttask('7', $customer['loginname'], $result['email_full']);
+ }
+
+ // decrease usage for customer
+ Customers::decreaseUsage($customer['customerid'], 'email_accounts_used');
+ Customers::decreaseUsage($customer['customerid'], 'email_quota_used', '', $quota);
+
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] deleted email account for '" . $result['email_full'] . "'");
+ return $this->response(200, "successful", $result);
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/EmailForwarders.php b/lib/Froxlor/Api/Commands/EmailForwarders.php
new file mode 100644
index 00000000..847bf9f6
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/EmailForwarders.php
@@ -0,0 +1,287 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class EmailForwarders extends \Froxlor\Api\ApiCommand implements \Froxlor\Api\ResourceEntity
+{
+
+ /**
+ * add new email-forwarder entry for given email-address by either id or email-address
+ *
+ * @param int $id
+ * optional, the email-address-id
+ * @param string $emailaddr
+ * optional, the email-address to add the forwarder for
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ * @param string $destination
+ * email-address to add as forwarder
+ *
+ * @access admin,customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function add()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ if ($this->getUserDetail('email_forwarders_used') < $this->getUserDetail('email_forwarders') || $this->getUserDetail('email_forwarders') == '-1') {
+
+ // parameter
+ $id = $this->getParam('id', true, 0);
+ $ea_optional = ($id <= 0 ? false : true);
+ $emailaddr = $this->getParam('emailaddr', $ea_optional, '');
+ $destination = $this->getParam('destination');
+
+ // validation
+ $idna_convert = new \Froxlor\Idna\IdnaWrapper();
+ $destination = $idna_convert->encode($destination);
+
+ $result = $this->apiCall('Emails.get', array(
+ 'id' => $id,
+ 'emailaddr' => $emailaddr
+ ));
+ $id = $result['id'];
+
+ // current destination array
+ $result['destination_array'] = explode(' ', $result['destination']);
+
+ // prepare destination
+ $destination = trim($destination);
+
+ if (! \Froxlor\Validate\Validate::validateEmail($destination)) {
+ \Froxlor\UI\Response::standard_error('destinationiswrong', $destination, true);
+ } elseif ($destination == $result['email']) {
+ \Froxlor\UI\Response::standard_error('destinationalreadyexistasmail', $destination, true);
+ } elseif (in_array($destination, $result['destination_array'])) {
+ \Froxlor\UI\Response::standard_error('destinationalreadyexist', $destination, true);
+ }
+
+ // get needed customer info to reduce the email-forwarder-counter by one
+ $customer = $this->getCustomerData('email_forwarders');
+
+ // add destination to address
+ $result['destination'] .= ' ' . $destination;
+ $stmt = Database::prepare("
+ UPDATE `" . TABLE_MAIL_VIRTUAL . "` SET `destination` = :dest
+ WHERE `customerid`= :cid AND `id`= :id
+ ");
+ $params = array(
+ "dest" => \Froxlor\FileDir::makeCorrectDestination($result['destination']),
+ "cid" => $customer['customerid'],
+ "id" => $id
+ );
+ Database::pexecute($stmt, $params, true, true);
+
+ // update customer usage
+ Customers::increaseUsage($customer['customerid'], 'email_forwarders_used');
+
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] added email forwarder for '" . $result['email_full'] . "'");
+
+ $result = $this->apiCall('Emails.get', array(
+ 'emailaddr' => $result['email_full']
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("No more resources available", 406);
+ }
+
+ /**
+ * You cannot directly get an email forwarder.
+ * Try EmailForwarders.listing()
+ */
+ public function get()
+ {
+ throw new \Exception('You cannot directly get an email forwarder. Try EmailForwarders.listing()', 303);
+ }
+
+ /**
+ * You cannot update an email forwarder.
+ * You need to delete the entry and create a new one.
+ */
+ public function update()
+ {
+ throw new \Exception('You cannot update an email forwarder. You need to delete the entry and create a new one.', 303);
+ }
+
+ /**
+ * List email forwarders for a given email address
+ *
+ * @param int $id
+ * optional, the email-address-id
+ * @param string $emailaddr
+ * optional, the email-address to delete the forwarder from
+ * @param int $customerid
+ * optional, admin-only, the customer-id
+ * @param string $loginname
+ * optional, admin-only, the loginname
+ *
+ * @access admin,customer
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listing()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ // parameter
+ $id = $this->getParam('id', true, 0);
+ $ea_optional = ($id <= 0 ? false : true);
+ $emailaddr = $this->getParam('emailaddr', $ea_optional, '');
+
+ // validation
+ $result = $this->apiCall('Emails.get', array(
+ 'id' => $id,
+ 'emailaddr' => $emailaddr
+ ));
+ $id = $result['id'];
+
+ $result['destination'] = explode(' ', $result['destination']);
+ $destination = array();
+ foreach ($result['destination'] as $index => $address) {
+ $destination[] = [
+ 'id' => $index,
+ 'address' => $address
+ ];
+ }
+
+ return $this->response(200, "successful", [
+ 'count' => count($destination),
+ 'list' => $destination
+ ]);
+ }
+
+ /**
+ * count email forwarders for a given email address
+ *
+ * @param int $id
+ * optional, the email-address-id
+ * @param string $emailaddr
+ * optional, the email-address to delete the forwarder from
+ * @param int $customerid
+ * optional, admin-only, the customer-id
+ * @param string $loginname
+ * optional, admin-only, the loginname
+ *
+ * @access admin,customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function listingCount()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ // parameter
+ $id = $this->getParam('id', true, 0);
+ $ea_optional = ($id <= 0 ? false : true);
+ $emailaddr = $this->getParam('emailaddr', $ea_optional, '');
+
+ // validation
+ $result = $this->apiCall('Emails.get', array(
+ 'id' => $id,
+ 'emailaddr' => $emailaddr
+ ));
+ $id = $result['id'];
+
+ $result['destination'] = explode(' ', $result['destination']);
+
+ return $this->response(200, "successful", count($result['destination']));
+ }
+
+ /**
+ * delete email-forwarder entry for given email-address by either id or email-address and forwarder-id
+ *
+ * @param int $id
+ * optional, the email-address-id
+ * @param string $emailaddr
+ * optional, the email-address to delete the forwarder from
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ * @param int $forwarderid
+ * id of the forwarder to delete
+ *
+ * @access admin,customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function delete()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ // parameter
+ $id = $this->getParam('id', true, 0);
+ $ea_optional = ($id <= 0 ? false : true);
+ $emailaddr = $this->getParam('emailaddr', $ea_optional, '');
+ $forwarderid = $this->getParam('forwarderid');
+
+ // validation
+ $result = $this->apiCall('Emails.get', array(
+ 'id' => $id,
+ 'emailaddr' => $emailaddr
+ ));
+ $id = $result['id'];
+
+ $result['destination'] = explode(' ', $result['destination']);
+ if (isset($result['destination'][$forwarderid]) && $result['email'] != $result['destination'][$forwarderid]) {
+
+ // get needed customer info to reduce the email-forwarder-counter by one
+ $customer = $this->getCustomerData();
+
+ // unset it from array
+ unset($result['destination'][$forwarderid]);
+ // rebuild destination-string
+ $result['destination'] = implode(' ', $result['destination']);
+ // update in DB
+ $stmt = Database::prepare("
+ UPDATE `" . TABLE_MAIL_VIRTUAL . "` SET `destination` = :dest
+ WHERE `customerid`= :cid AND `id`= :id
+ ");
+ $params = array(
+ "dest" => \Froxlor\FileDir::makeCorrectDestination($result['destination']),
+ "cid" => $customer['customerid'],
+ "id" => $id
+ );
+ Database::pexecute($stmt, $params, true, true);
+
+ // update customer usage
+ Customers::decreaseUsage($customer['customerid'], 'email_forwarders_used');
+
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] deleted email forwarder for '" . $result['email_full'] . "'");
+
+ $result = $this->apiCall('Emails.get', array(
+ 'emailaddr' => $result['email_full']
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("Unknown forwarder id", 404);
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/Emails.php b/lib/Froxlor/Api/Commands/Emails.php
new file mode 100644
index 00000000..c3bc5df5
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/Emails.php
@@ -0,0 +1,419 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class Emails extends \Froxlor\Api\ApiCommand implements \Froxlor\Api\ResourceEntity
+{
+
+ /**
+ * add a new email address
+ *
+ * @param string $email_part
+ * name of the address before @
+ * @param string $domain
+ * domain-name for the email-address
+ * @param boolean $iscatchall
+ * optional, make this address a catchall address, default: no
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function add()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ if ($this->getUserDetail('emails_used') < $this->getUserDetail('emails') || $this->getUserDetail('emails') == '-1') {
+
+ // required parameters
+ $email_part = $this->getParam('email_part');
+ $domain = $this->getParam('domain');
+
+ // parameters
+ $iscatchall = $this->getBoolParam('iscatchall', true, 0);
+
+ // validation
+ if (substr($domain, 0, 4) != 'xn--') {
+ $idna_convert = new \Froxlor\Idna\IdnaWrapper();
+ $domain = $idna_convert->encode(\Froxlor\Validate\Validate::validate($domain, 'domain', '', '', array(), true));
+ }
+
+ // check domain and whether it's an email-enabled domain
+ // use internal call because the customer might have 'domains' in customer_hide_options
+ $domain_check = $this->apiCall('SubDomains.get', array(
+ 'domainname' => $domain
+ ), true);
+ if ($domain_check['isemaildomain'] == 0) {
+ \Froxlor\UI\Response::standard_error('maindomainnonexist', $domain, true);
+ }
+
+ if (Settings::Get('catchall.catchall_enabled') != '1') {
+ $iscatchall = 0;
+ }
+
+ // check for catchall-flag
+ if ($iscatchall) {
+ $iscatchall = '1';
+ $email = '@' . $domain;
+ } else {
+ $iscatchall = '0';
+ $email = $email_part . '@' . $domain;
+ }
+
+ // full email value
+ $email_full = $email_part . '@' . $domain;
+
+ // validate it
+ if (! \Froxlor\Validate\Validate::validateEmail($email_full)) {
+ \Froxlor\UI\Response::standard_error('emailiswrong', $email_full, true);
+ }
+
+ // get needed customer info to reduce the email-address-counter by one
+ $customer = $this->getCustomerData('emails');
+
+ // duplicate check
+ $stmt = Database::prepare("
+ SELECT `id`, `email`, `email_full`, `iscatchall`, `destination`, `customerid` FROM `" . TABLE_MAIL_VIRTUAL . "`
+ WHERE (`email` = :email OR `email_full` = :emailfull )
+ AND `customerid`= :cid
+ ");
+ $params = array(
+ "email" => $email,
+ "emailfull" => $email_full,
+ "cid" => $customer['customerid']
+ );
+ $email_check = Database::pexecute_first($stmt, $params, true, true);
+
+ if ($email_check) {
+ if (strtolower($email_check['email_full']) == strtolower($email_full)) {
+ \Froxlor\UI\Response::standard_error('emailexistalready', $email_full, true);
+ } elseif ($email_check['email'] == $email) {
+ \Froxlor\UI\Response::standard_error('youhavealreadyacatchallforthisdomain', '', true);
+ }
+ }
+
+ $stmt = Database::prepare("
+ INSERT INTO `" . TABLE_MAIL_VIRTUAL . "` SET
+ `customerid` = :cid,
+ `email` = :email,
+ `email_full` = :email_full,
+ `iscatchall` = :iscatchall,
+ `domainid` = :domainid
+ ");
+ $params = array(
+ "cid" => $customer['customerid'],
+ "email" => $email,
+ "email_full" => $email_full,
+ "iscatchall" => $iscatchall,
+ "domainid" => $domain_check['id']
+ );
+ Database::pexecute($stmt, $params, true, true);
+
+ // update customer usage
+ Customers::increaseUsage($customer['customerid'], 'emails_used');
+
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] added email address '" . $email_full . "'");
+
+ $result = $this->apiCall('Emails.get', array(
+ 'emailaddr' => $email_full
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("No more resources available", 406);
+ }
+
+ /**
+ * return a email-address entry by either id or email-address
+ *
+ * @param int $id
+ * optional, the email-address-id
+ * @param string $emailaddr
+ * optional, the email-address
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function get()
+ {
+ $id = $this->getParam('id', true, 0);
+ $ea_optional = ($id <= 0 ? false : true);
+ $emailaddr = $this->getParam('emailaddr', $ea_optional, '');
+
+ $params = array();
+ $customer_ids = $this->getAllowedCustomerIds('email');
+ $params['idea'] = ($id <= 0 ? $emailaddr : $id);
+
+ $result_stmt = Database::prepare("SELECT v.`id`, v.`email`, v.`email_full`, v.`iscatchall`, v.`destination`, v.`customerid`, v.`popaccountid`, v.`domainid`, u.`quota`
+ FROM `" . TABLE_MAIL_VIRTUAL . "` v
+ LEFT JOIN `" . TABLE_MAIL_USERS . "` u ON v.`popaccountid` = u.`id`
+ WHERE v.`customerid` IN (" . implode(", ", $customer_ids) . ")
+ AND (v.`id`= :idea OR (v.`email` = :idea OR v.`email_full` = :idea))
+ ");
+ $result = Database::pexecute_first($result_stmt, $params, true, true);
+ if ($result) {
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] get email address '" . $result['email_full'] . "'");
+ return $this->response(200, "successful", $result);
+ }
+ $key = ($id > 0 ? "id #" . $id : "emailaddr '" . $emailaddr . "'");
+ throw new \Exception("Email address with " . $key . " could not be found", 404);
+ }
+
+ /**
+ * toggle catchall flag of given email address either by id or email-address
+ *
+ * @param int $id
+ * optional, the email-address-id
+ * @param string $emailaddr
+ * optional, the email-address
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ * @param boolean $iscatchall
+ * optional
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function update()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ // if enabling catchall is not allowed by settings, we do not need
+ // to run update()
+ if (Settings::Get('catchall.catchall_enabled') != '1') {
+ \Froxlor\UI\Response::standard_error(array(
+ 'operationnotpermitted',
+ 'featureisdisabled'
+ ), 'catchall', true);
+ }
+
+ $id = $this->getParam('id', true, 0);
+ $ea_optional = ($id <= 0 ? false : true);
+ $emailaddr = $this->getParam('emailaddr', $ea_optional, '');
+
+ $result = $this->apiCall('Emails.get', array(
+ 'id' => $id,
+ 'emailaddr' => $emailaddr
+ ));
+ $id = $result['id'];
+
+ // parameters
+ $iscatchall = $this->getBoolParam('iscatchall', true, $result['iscatchall']);
+
+ // get needed customer info to reduce the email-address-counter by one
+ $customer = $this->getCustomerData();
+
+ // check for catchall-flag
+ if ($iscatchall) {
+ $iscatchall = '1';
+ $email_parts = explode('@', $result['email_full']);
+ $email = '@' . $email_parts[1];
+ // catchall check
+ $stmt = Database::prepare("
+ SELECT `email_full` FROM `" . TABLE_MAIL_VIRTUAL . "`
+ WHERE `email` = :email AND `customerid` = :cid AND `iscatchall` = '1'
+ ");
+ $params = array(
+ "email" => $email,
+ "cid" => $customer['customerid']
+ );
+ $email_check = Database::pexecute_first($stmt, $params, true, true);
+ if ($email_check) {
+ \Froxlor\UI\Response::standard_error('youhavealreadyacatchallforthisdomain', '', true);
+ }
+ } else {
+ $iscatchall = '0';
+ $email = $result['email_full'];
+ }
+
+ $stmt = Database::prepare("
+ UPDATE `" . TABLE_MAIL_VIRTUAL . "`
+ SET `email` = :email , `iscatchall` = :caflag
+ WHERE `customerid`= :cid AND `id`= :id
+ ");
+ $params = array(
+ "email" => $email,
+ "caflag" => $iscatchall,
+ "cid" => $customer['customerid'],
+ "id" => $id
+ );
+ Database::pexecute($stmt, $params, true, true);
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] toggled catchall-flag for email address '" . $result['email_full'] . "'");
+
+ $result = $this->apiCall('Emails.get', array(
+ 'emailaddr' => $result['email_full']
+ ));
+ return $this->response(200, "successful", $result);
+ }
+
+ /**
+ * list all email addresses, if called from an admin, list all email addresses of all customers you are allowed to view, or specify id or loginname for one specific customer
+ *
+ * @param int $customerid
+ * optional, admin-only, select email addresses of a specific customer by id
+ * @param string $loginname
+ * optional, admin-only, select email addresses of a specific customer by loginname
+ * @param array $sql_search
+ * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), LIKE is used if left empty and 'value' => searchvalue
+ * @param int $sql_limit
+ * optional specify number of results to be returned
+ * @param int $sql_offset
+ * optional specify offset for resultset
+ * @param array $sql_orderby
+ * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more fields
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listing()
+ {
+ $customer_ids = $this->getAllowedCustomerIds('email');
+ $result = array();
+ $query_fields = array();
+ $result_stmt = Database::prepare("
+ SELECT m.`id`, m.`domainid`, m.`email`, m.`email_full`, m.`iscatchall`, u.`quota`, m.`destination`, m.`popaccountid`, d.`domain`, u.`mboxsize`
+ FROM `" . TABLE_MAIL_VIRTUAL . "` m
+ LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` d ON (m.`domainid` = d.`id`)
+ LEFT JOIN `" . TABLE_MAIL_USERS . "` u ON (m.`popaccountid` = u.`id`)
+ WHERE m.`customerid` IN (" . implode(", ", $customer_ids) . ")" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit());
+ Database::pexecute($result_stmt, $query_fields, true, true);
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = $row;
+ }
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] list email-addresses");
+ return $this->response(200, "successful", array(
+ 'count' => count($result),
+ 'list' => $result
+ ));
+ }
+
+ /**
+ * returns the total number of accessable email addresses
+ *
+ * @param int $customerid
+ * optional, admin-only, select email addresses of a specific customer by id
+ * @param string $loginname
+ * optional, admin-only, select email addresses of a specific customer by loginname
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function listingCount()
+ {
+ $customer_ids = $this->getAllowedCustomerIds('email');
+ $result_stmt = Database::prepare("
+ SELECT COUNT(*) as num_emails
+ FROM `" . TABLE_MAIL_VIRTUAL . "` m
+ LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` d ON (m.`domainid` = d.`id`)
+ LEFT JOIN `" . TABLE_MAIL_USERS . "` u ON (m.`popaccountid` = u.`id`)
+ WHERE m.`customerid` IN (" . implode(", ", $customer_ids) . ")
+ ");
+ $result = Database::pexecute_first($result_stmt, null, true, true);
+ if ($result) {
+ return $this->response(200, "successful", $result['num_emails']);
+ }
+ }
+
+ /**
+ * delete an email address by either id or username
+ *
+ * @param int $id
+ * optional, the email-address-id
+ * @param string $emailaddr
+ * optional, the email-address
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ * @param boolean $delete_userfiles
+ * optional, delete email data from filesystem, default: 0 (false)
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function delete()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ $id = $this->getParam('id', true, 0);
+ $ea_optional = ($id <= 0 ? false : true);
+ $emailaddr = $this->getParam('emailaddr', $ea_optional, '');
+
+ $result = $this->apiCall('Emails.get', array(
+ 'id' => $id,
+ 'emailaddr' => $emailaddr
+ ));
+ $id = $result['id'];
+
+ // parameters
+ $delete_userfiles = $this->getBoolParam('delete_userfiles', true, 0);
+
+ // get needed customer info to reduce the email-address-counter by one
+ $customer = $this->getCustomerData();
+
+ // check for forwarders
+ $number_forwarders = 0;
+ if ($result['destination'] != '') {
+ $result['destination'] = explode(' ', $result['destination']);
+ $number_forwarders = count($result['destination']);
+ }
+ // check whether this address is an account
+ if ($result['popaccountid'] != 0) {
+ // use EmailAccounts.delete
+ $this->apiCall('EmailAccounts.delete', array(
+ 'id' => $result['id'],
+ 'customerid' => $customer['customerid'],
+ 'delete_userfiles' => $delete_userfiles
+ ));
+ $number_forwarders --;
+ }
+
+ // decrease forwarder counter
+ Customers::decreaseUsage($customer['customerid'], 'email_forwarders_used', '', $number_forwarders);
+ Admins::decreaseUsage($customer['customerid'], 'email_forwarders_used', '', $number_forwarders);
+
+ // delete address
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_MAIL_VIRTUAL . "` WHERE `customerid`= :customerid AND `id`= :id");
+ Database::pexecute($stmt, array(
+ "customerid" => $customer['customerid'],
+ "id" => $id
+ ), true, true);
+ Customers::decreaseUsage($customer['customerid'], 'emails_used');
+
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] deleted email address '" . $result['email_full'] . "'");
+ return $this->response(200, "successful", $result);
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/FpmDaemons.php b/lib/Froxlor/Api/Commands/FpmDaemons.php
new file mode 100644
index 00000000..ba757dc0
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/FpmDaemons.php
@@ -0,0 +1,409 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class FpmDaemons extends \Froxlor\Api\ApiCommand implements \Froxlor\Api\ResourceEntity
+{
+
+ /**
+ * lists all fpm-daemon entries
+ *
+ * @param array $sql_search
+ * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), LIKE is used if left empty and 'value' => searchvalue
+ * @param int $sql_limit
+ * optional specify number of results to be returned
+ * @param int $sql_offset
+ * optional specify offset for resultset
+ * @param array $sql_orderby
+ * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more fields
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listing()
+ {
+ if ($this->isAdmin()) {
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] list fpm-daemons");
+ $query_fields = array();
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_FPMDAEMONS . "`" . $this->getSearchWhere($query_fields) . $this->getOrderBy() . $this->getLimit());
+ Database::pexecute($result_stmt, $query_fields, true, true);
+ $fpmdaemons = array();
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ $query_params = array(
+ 'id' => $row['id']
+ );
+
+ $configresult_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_PHPCONFIGS . "` WHERE `fpmsettingid` = :id");
+ Database::pexecute($configresult_stmt, $query_params, true, true);
+
+ $configs = array();
+ if (Database::num_rows() > 0) {
+ while ($row2 = $configresult_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $configs[] = $row2['description'];
+ }
+ }
+
+ if (empty($configs)) {
+ $configs[] = $this->lng['admin']['phpsettings']['notused'];
+ }
+
+ $row['configs'] = $configs;
+ $fpmdaemons[] = $row;
+ }
+
+ return $this->response(200, "successful", array(
+ 'count' => count($fpmdaemons),
+ 'list' => $fpmdaemons
+ ));
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * returns the total number of accessable fpm daemons
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function listingCount()
+ {
+ if ($this->isAdmin()) {
+ $result_stmt = Database::prepare("
+ SELECT COUNT(*) as num_fpms FROM `" . TABLE_PANEL_FPMDAEMONS . "`
+ ");
+ $result = Database::pexecute_first($result_stmt, null, true, true);
+ if ($result) {
+ return $this->response(200, "successful", $result['num_fpms']);
+ }
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * return a fpm-daemon entry by id
+ *
+ * @param int $id
+ * fpm-daemon-id
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function get()
+ {
+ if ($this->isAdmin()) {
+ $id = $this->getParam('id');
+
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_FPMDAEMONS . "` WHERE `id` = :id
+ ");
+ $result = Database::pexecute_first($result_stmt, array(
+ 'id' => $id
+ ), true, true);
+ if ($result) {
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("fpm-daemon with id #" . $id . " could not be found", 404);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * create a new fpm-daemon entry
+ *
+ * @param string $description
+ * @param string $reload_cmd
+ * @param string $config_dir
+ * @param string $pm
+ * optional, process-manager, one of 'static', 'dynamic' or 'ondemand', default 'dynamic'
+ * @param int $max_children
+ * optional, default 5
+ * @param int $start_servers
+ * optional, default 2
+ * @param int $min_spare_servers
+ * optional, default 1
+ * @param int $max_spare_servers
+ * optional, default 3
+ * @param int $max_requests
+ * optional, default 0
+ * @param int $idle_timeout
+ * optional, default 10
+ * @param string $limit_extensions
+ * optional, limit execution to the following extensions, default '.php'
+ * @param string $custom_config
+ * optional, custom settings appended to phpfpm pool configuration
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function add()
+ {
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) {
+
+ // required parameter
+ $description = $this->getParam('description');
+ $reload_cmd = $this->getParam('reload_cmd');
+ $config_dir = $this->getParam('config_dir');
+
+ // parameters
+ $pmanager = $this->getParam('pm', true, 'dynamic');
+ $max_children = $this->getParam('max_children', true, 5);
+ $start_servers = $this->getParam('start_servers', true, 2);
+ $min_spare_servers = $this->getParam('min_spare_servers', true, 1);
+ $max_spare_servers = $this->getParam('max_spare_servers', true, 3);
+ $max_requests = $this->getParam('max_requests', true, 0);
+ $idle_timeout = $this->getParam('idle_timeout', true, 10);
+ $limit_extensions = $this->getParam('limit_extensions', true, '.php');
+ $custom_config = $this->getParam('custom_config', true, '');
+
+ // validation
+ $description = \Froxlor\Validate\Validate::validate($description, 'description', '', '', array(), true);
+ $reload_cmd = \Froxlor\Validate\Validate::validate($reload_cmd, 'reload_cmd', '', '', array(), true);
+ $config_dir = \Froxlor\Validate\Validate::validate($config_dir, 'config_dir', '', '', array(), true);
+ if (! in_array($pmanager, array(
+ 'static',
+ 'dynamic',
+ 'ondemand'
+ ))) {
+ throw new \Exception("Unknown process manager", 406);
+ }
+ if (empty($limit_extensions)) {
+ $limit_extensions = '.php';
+ }
+ $limit_extensions = \Froxlor\Validate\Validate::validate($limit_extensions, 'limit_extensions', '/^(\.[a-z]([a-z0-9]+)\ ?)+$/', '', array(), true);
+
+ if (strlen($description) == 0 || strlen($description) > 50) {
+ \Froxlor\UI\Response::standard_error('descriptioninvalid', '', true);
+ }
+
+ $ins_stmt = Database::prepare("
+ INSERT INTO `" . TABLE_PANEL_FPMDAEMONS . "` SET
+ `description` = :desc,
+ `reload_cmd` = :reload_cmd,
+ `config_dir` = :config_dir,
+ `pm` = :pm,
+ `max_children` = :max_children,
+ `start_servers` = :start_servers,
+ `min_spare_servers` = :min_spare_servers,
+ `max_spare_servers` = :max_spare_servers,
+ `max_requests` = :max_requests,
+ `idle_timeout` = :idle_timeout,
+ `limit_extensions` = :limit_extensions,
+ `custom_config` = :custom_config
+ ");
+ $ins_data = array(
+ 'desc' => $description,
+ 'reload_cmd' => $reload_cmd,
+ 'config_dir' => \Froxlor\FileDir::makeCorrectDir($config_dir),
+ 'pm' => $pmanager,
+ 'max_children' => $max_children,
+ 'start_servers' => $start_servers,
+ 'min_spare_servers' => $min_spare_servers,
+ 'max_spare_servers' => $max_spare_servers,
+ 'max_requests' => $max_requests,
+ 'idle_timeout' => $idle_timeout,
+ 'limit_extensions' => $limit_extensions,
+ 'custom_config' => $custom_config
+ );
+ Database::pexecute($ins_stmt, $ins_data);
+ $id = Database::lastInsertId();
+
+ \Froxlor\System\Cronjob::inserttask('1');
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] fpm-daemon with description '" . $description . "' has been created by '" . $this->getUserDetail('loginname') . "'");
+ $result = $this->apiCall('FpmDaemons.get', array(
+ 'id' => $id
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * update a fpm-daemon entry by given id
+ *
+ * @param int $id
+ * fpm-daemon id
+ * @param string $description
+ * optional
+ * @param string $reload_cmd
+ * optional
+ * @param string $config_dir
+ * optional
+ * @param string $pm
+ * optional, process-manager, one of 'static', 'dynamic' or 'ondemand', default 'dynamic'
+ * @param int $max_children
+ * optional, default 5
+ * @param int $start_servers
+ * optional, default 2
+ * @param int $min_spare_servers
+ * optional, default 1
+ * @param int $max_spare_servers
+ * optional, default 3
+ * @param int $max_requests
+ * optional, default 0
+ * @param int $idle_timeout
+ * optional, default 10
+ * @param string $limit_extensions
+ * optional, limit execution to the following extensions, default '.php'
+ * @param string $custom_config
+ * optional, custom settings appended to phpfpm pool configuration
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function update()
+ {
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) {
+
+ // required parameter
+ $id = $this->getParam('id');
+
+ $result = $this->apiCall('FpmDaemons.get', array(
+ 'id' => $id
+ ));
+
+ // parameters
+ $description = $this->getParam('description', true, $result['description']);
+ $reload_cmd = $this->getParam('reload_cmd', true, $result['reload_cmd']);
+ $config_dir = $this->getParam('config_dir', true, $result['config_dir']);
+ $pmanager = $this->getParam('pm', true, $result['pm']);
+ $max_children = $this->getParam('max_children', true, $result['max_children']);
+ $start_servers = $this->getParam('start_servers', true, $result['start_servers']);
+ $min_spare_servers = $this->getParam('min_spare_servers', true, $result['min_spare_servers']);
+ $max_spare_servers = $this->getParam('max_spare_servers', true, $result['max_spare_servers']);
+ $max_requests = $this->getParam('max_requests', true, $result['max_requests']);
+ $idle_timeout = $this->getParam('idle_timeout', true, $result['idle_timeout']);
+ $limit_extensions = $this->getParam('limit_extensions', true, $result['limit_extensions']);
+ $custom_config = $this->getParam('custom_config', true, $result['custom_config']);
+
+ // validation
+ $description = \Froxlor\Validate\Validate::validate($description, 'description', '', '', array(), true);
+ $reload_cmd = \Froxlor\Validate\Validate::validate($reload_cmd, 'reload_cmd', '', '', array(), true);
+ $config_dir = \Froxlor\Validate\Validate::validate($config_dir, 'config_dir', '', '', array(), true);
+ if (! in_array($pmanager, array(
+ 'static',
+ 'dynamic',
+ 'ondemand'
+ ))) {
+ throw new \Exception("Unknown process manager", 406);
+ }
+ if (empty($limit_extensions)) {
+ $limit_extensions = '.php';
+ }
+ $limit_extensions = \Froxlor\Validate\Validate::validate($limit_extensions, 'limit_extensions', '/^(\.[a-z]([a-z0-9]+)\ ?)+$/', '', array(), true);
+
+ if (strlen($description) == 0 || strlen($description) > 50) {
+ \Froxlor\UI\Response::standard_error('descriptioninvalid', '', true);
+ }
+
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_FPMDAEMONS . "` SET
+ `description` = :desc,
+ `reload_cmd` = :reload_cmd,
+ `config_dir` = :config_dir,
+ `pm` = :pm,
+ `max_children` = :max_children,
+ `start_servers` = :start_servers,
+ `min_spare_servers` = :min_spare_servers,
+ `max_spare_servers` = :max_spare_servers,
+ `max_requests` = :max_requests,
+ `idle_timeout` = :idle_timeout,
+ `limit_extensions` = :limit_extensions,
+ `custom_config` = :custom_config
+ WHERE `id` = :id
+ ");
+ $upd_data = array(
+ 'desc' => $description,
+ 'reload_cmd' => $reload_cmd,
+ 'config_dir' => \Froxlor\FileDir::makeCorrectDir($config_dir),
+ 'pm' => $pmanager,
+ 'max_children' => $max_children,
+ 'start_servers' => $start_servers,
+ 'min_spare_servers' => $min_spare_servers,
+ 'max_spare_servers' => $max_spare_servers,
+ 'max_requests' => $max_requests,
+ 'idle_timeout' => $idle_timeout,
+ 'limit_extensions' => $limit_extensions,
+ 'custom_config' => $custom_config,
+ 'id' => $id
+ );
+ Database::pexecute($upd_stmt, $upd_data, true, true);
+
+ \Froxlor\System\Cronjob::inserttask('1');
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] fpm-daemon with description '" . $description . "' has been updated by '" . $this->getUserDetail('loginname') . "'");
+ $result = $this->apiCall('FpmDaemons.get', array(
+ 'id' => $id
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * delete a fpm-daemon entry by id
+ *
+ * @param int $id
+ * fpm-daemon-id
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function delete()
+ {
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) {
+ $id = $this->getParam('id');
+
+ if ($id == 1) {
+ \Froxlor\UI\Response::standard_error('cannotdeletedefaultphpconfig', '', true);
+ }
+
+ $result = $this->apiCall('FpmDaemons.get', array(
+ 'id' => $id
+ ));
+
+ // set default fpm daemon config for all php-config that use this config that is to be deleted
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_PHPCONFIGS . "` SET
+ `fpmsettingid` = '1' WHERE `fpmsettingid` = :id
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_PANEL_FPMDAEMONS . "` WHERE `id` = :id
+ ");
+ Database::pexecute($del_stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ \Froxlor\System\Cronjob::inserttask('1');
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] fpm-daemon setting '" . $result['description'] . "' has been deleted by '" . $this->getUserDetail('loginname') . "'");
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/Froxlor.php b/lib/Froxlor/Api/Commands/Froxlor.php
new file mode 100644
index 00000000..78c772c9
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/Froxlor.php
@@ -0,0 +1,468 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class Froxlor extends \Froxlor\Api\ApiCommand
+{
+
+ /**
+ * checks whether there is a newer version of froxlor available
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function checkUpdate()
+ {
+ define('UPDATE_URI', "https://version.froxlor.org/Froxlor/api/" . $this->version);
+
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings')) {
+ if (function_exists('curl_version')) {
+ // log our actions
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] checking for updates");
+
+ // check for new version
+ try {
+ $latestversion = \Froxlor\Http\HttpClient::urlGet(UPDATE_URI, true, 3);
+ } catch (\Exception $e) {
+ $latestversion = \Froxlor\Froxlor::getVersion() . "|Version-check currently unavailable, please try again later";
+ }
+ $latestversion = explode('|', $latestversion);
+
+ if (is_array($latestversion) && count($latestversion) >= 1) {
+ $_version = $latestversion[0];
+ $_message = isset($latestversion[1]) ? $latestversion[1] : '';
+ $_link = isset($latestversion[2]) ? $latestversion[2] : '';
+
+ // add the branding so debian guys are not gettings confused
+ // about their version-number
+ $version_label = $_version . $this->branding;
+ $version_link = $_link;
+ $message_addinfo = $_message;
+
+ // not numeric -> error-message
+ if (! preg_match('/^((\d+\\.)(\d+\\.)(\d+\\.)?(\d+)?(\-(svn|dev|rc)(\d+))?)$/', $_version)) {
+ // check for customized version to not output
+ // "There is a newer version of froxlor" besides the error-message
+ $isnewerversion = - 1;
+ } elseif (\Froxlor\Froxlor::versionCompare2($this->version, $_version) == - 1) {
+ // there is a newer version - yay
+ $isnewerversion = 1;
+ } else {
+ // nothing new
+ $isnewerversion = 0;
+ }
+
+ // anzeige über version-status mit ggfls. formular
+ // zum update schritt #1 -> download
+ if ($isnewerversion == 1) {
+ $text = 'There is a newer version available: "' . $_version . '" (Your current version is: ' . $this->version . ')';
+ return $this->response(200, "successful", array(
+ 'isnewerversion' => $isnewerversion,
+ 'version' => $_version,
+ 'message' => $text,
+ 'link' => $version_link,
+ 'additional_info' => $message_addinfo
+ ));
+ } elseif ($isnewerversion == 0) {
+ // all good
+ return $this->response(200, "successful", array(
+ 'isnewerversion' => $isnewerversion,
+ 'version' => $version_label,
+ 'message' => "",
+ 'link' => $version_link,
+ 'additional_info' => $message_addinfo
+ ));
+ } else {
+ \Froxlor\UI\Response::standard_error('customized_version', '', true);
+ }
+ }
+ }
+ return $this->response(300, "successful", array(
+ 'isnewerversion' => 0,
+ 'version' => $this->version . $this->branding,
+ 'message' => 'Version-check not available due to missing php-curl extension',
+ 'link' => UPDATE_URI . '/pretty',
+ 'additional_info' => ""
+ ));
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * import settings
+ *
+ * @param string $json_str
+ * content of exported froxlor-settings json file
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded bool
+ */
+ public function importSettings()
+ {
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings')) {
+ $json_str = $this->getParam('json_str');
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "User " . $this->getUserDetail('loginname') . " imported settings");
+ try {
+ \Froxlor\SImExporter::import($json_str);
+ \Froxlor\System\Cronjob::inserttask('1');
+ \Froxlor\System\Cronjob::inserttask('10');
+ // Using nameserver, insert a task which rebuilds the server config
+ \Froxlor\System\Cronjob::inserttask('4');
+ // cron.d file
+ \Froxlor\System\Cronjob::inserttask('99');
+ return $this->response(200, "successful", true);
+ } catch (\Exception $e) {
+ throw new \Exception($e->getMessage(), 406);
+ }
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * export settings
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-string
+ */
+ public function exportSettings()
+ {
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings')) {
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "User " . $this->getUserDetail('loginname') . " exported settings");
+ $json_export = \Froxlor\SImExporter::export();
+ return $this->response(200, "successful", $json_export);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * return a list of all settings
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listSettings()
+ {
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings')) {
+ $sel_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_SETTINGS . "` ORDER BY settinggroup ASC, varname ASC
+ ");
+ Database::pexecute($sel_stmt, null, true, true);
+ $result = array();
+ while ($row = $sel_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = array(
+ 'key' => $row['settinggroup'] . '.' . $row['varname'],
+ 'value' => $row['value']
+ );
+ }
+ return $this->response(200, "successful", array(
+ 'count' => count($result),
+ 'list' => $result
+ ));
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * return a setting by settinggroup.varname couple
+ *
+ * @param string $key
+ * settinggroup.varname couple
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string
+ */
+ public function getSetting()
+ {
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings')) {
+ $setting = $this->getParam('key');
+ return $this->response(200, "successful", Settings::Get($setting));
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * updates a setting
+ *
+ * @param string $key
+ * settinggroup.varname couple
+ * @param string $value
+ * optional the new value, default is ''
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string
+ */
+ public function updateSetting()
+ {
+ // currently not implemented as it requires validation too so no wrong settings are being stored via API
+ throw new \Exception("Not available yet.", 501);
+
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings')) {
+ $setting = $this->getParam('key');
+ $value = $this->getParam('value', true, '');
+ $oldvalue = Settings::Get($setting);
+ if (is_null($oldvalue)) {
+ throw new \Exception("Setting '" . $setting . "' could not be found");
+ }
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] Changing setting '" . $setting . "' from '" . $oldvalue . "' to '" . $value . "'");
+ return $this->response(200, "successful", Settings::Set($setting, $value, true));
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * returns a random password based on froxlor settings for min-length, included characters, etc.
+ *
+ * @access admin, customer
+ * @return string
+ */
+ public function generatePassword()
+ {
+ return $this->response(200, "successful", \Froxlor\System\Crypt::generatePassword());
+ }
+
+ /**
+ * can be used to remotely run the integritiy checks froxlor implements
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string
+ */
+ public function integrityCheck()
+ {
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings')) {
+ $integrity = new \Froxlor\Database\IntegrityCheck();
+ $result = $integrity->checkAll();
+ if ($result) {
+ return $this->response(200, "successful", "OK");
+ }
+ throw new \Exception("Some checks failed.", 406);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * returns a list of all available api functions
+ *
+ * @param string $module
+ * optional, return list of functions for a specific module
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function listFunctions()
+ {
+ $module = $this->getParam('module', true, '');
+
+ $functions = array();
+ if ($module != null) {
+ // check existence
+ $this->requireModules($module);
+ // now get all static functions
+ $reflection = new \ReflectionClass(__NAMESPACE__ . '\\' . $module);
+ $_functions = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);
+ foreach ($_functions as $func) {
+ if ($func->class == __NAMESPACE__ . '\\' . $module && $func->isPublic()) {
+ array_push($functions, array_merge(array(
+ 'module' => $module,
+ 'function' => $func->name
+ ), $this->getParamListFromDoc($module, $func->name)));
+ }
+ }
+ } else {
+ // check all the modules
+ $path = \Froxlor\Froxlor::getInstallDir() . '/lib/Froxlor/Api/Commands/';
+ // valid directory?
+ if (is_dir($path)) {
+ // create RecursiveIteratorIterator
+ $its = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path));
+ // check every file
+ foreach ($its as $it) {
+ // does it match the Filename pattern?
+ $matches = array();
+ if (preg_match("/^(.+)\.php$/i", $it->getFilename(), $matches)) {
+ // check for existence
+ try {
+ // set the module to be in our namespace
+ $mod = $matches[1];
+ $this->requireModules($mod);
+ } catch (\Exception $e) {
+ // @todo log?
+ continue;
+ }
+ // now get all static functions
+ $reflection = new \ReflectionClass(__NAMESPACE__ . '\\' . $mod);
+ $_functions = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);
+ foreach ($_functions as $func) {
+ if ($func->class == __NAMESPACE__ . '\\' . $mod && $func->isPublic() && ! $func->isStatic()) {
+ array_push($functions, array_merge(array(
+ 'module' => $matches[1],
+ 'function' => $func->name
+ ), $this->getParamListFromDoc($matches[1], $func->name)));
+ }
+ }
+ }
+ }
+ } else {
+ // yikes - no valid directory to check
+ throw new \Exception("Cannot search directory '" . $path . "'. No such directory.", 500);
+ }
+ }
+
+ // return the list
+ return $this->response(200, "successful", $functions);
+ }
+
+ /**
+ * generate an api-response to list all parameters and the return-value of
+ * a given module.function-combination
+ *
+ * @param string $module
+ * @param string $function
+ *
+ * @throws \Exception
+ * @return array|bool
+ */
+ private function getParamListFromDoc($module = null, $function = null)
+ {
+ try {
+ // set the module
+ $cls = new \ReflectionMethod(__NAMESPACE__ . '\\' . $module, $function);
+ $comment = $cls->getDocComment();
+ if ($comment == false) {
+ return array(
+ 'head' => 'There is no comment-block for "' . $module . '.' . $function . '"'
+ );
+ }
+
+ $clines = explode("\n", $comment);
+ $result = array();
+ $result['params'] = array();
+ $param_desc = false;
+ $r = array();
+ foreach ($clines as $c) {
+ $c = trim($c);
+ // check param-section
+ if (strpos($c, '@param')) {
+ preg_match('/^\*\s\@param\s(.+)\s(\$\w+)(\s.*)?/', $c, $r);
+ // cut $ off the parameter-name as it is not wanted in the api-request
+ $result['params'][] = array(
+ 'parameter' => substr($r[2], 1),
+ 'type' => $r[1],
+ 'desc' => (isset($r[3]) ? trim($r['3']) : '')
+ );
+ $param_desc = true;
+ } elseif (strpos($c, '@access')) {
+ // check access-section
+ preg_match('/^\*\s\@access\s(.*)/', $c, $r);
+ if (! isset($r[0]) || empty($r[0])) {
+ $r[1] = 'This function has no restrictions';
+ }
+ $result['access'] = array(
+ 'groups' => (isset($r[1]) ? trim($r[1]) : '')
+ );
+ } elseif (strpos($c, '@return')) {
+ // check return-section
+ preg_match('/^\*\s\@return\s(\w+)(\s.*)?/', $c, $r);
+ if (! isset($r[0]) || empty($r[0])) {
+ $r[1] = 'null';
+ $r[2] = 'This function has no return value given';
+ }
+ $result['return'] = array(
+ 'type' => $r[1],
+ 'desc' => (isset($r[2]) ? trim($r[2]) : '')
+ );
+ } elseif (! empty($c) && strpos($c, '@throws') === false) {
+ // check throws-section
+ if (substr($c, 0, 3) == "/**") {
+ continue;
+ }
+ if (substr($c, 0, 2) == "*/") {
+ continue;
+ }
+ if (substr($c, 0, 1) == "*") {
+ $c = trim(substr($c, 1));
+ if (empty($c)) {
+ continue;
+ }
+ if ($param_desc) {
+ $result['params'][count($result['params']) - 1]['desc'] .= $c;
+ } else {
+ if (! isset($result['head']) || empty($result['head'])) {
+ $result['head'] = $c . " ";
+ } else {
+ $result['head'] .= $c . " ";
+ }
+ }
+ }
+ }
+ }
+ $result['head'] = trim($result['head']);
+ return $result;
+ } catch (\ReflectionException $e) {
+ return array();
+ }
+ }
+
+ /**
+ * this functions is used to check the availability
+ * of a given list of modules.
+ * If either one of
+ * them are not found, throw an Exception
+ *
+ * @param string|array $modules
+ *
+ * @throws \Exception
+ */
+ private function requireModules($modules = null)
+ {
+ if ($modules != null) {
+ // no array -> create one
+ if (! is_array($modules)) {
+ $modules = array(
+ $modules
+ );
+ }
+ // check all the modules
+ foreach ($modules as $module) {
+ try {
+ $module = __NAMESPACE__ . '\\' . $module;
+ // can we use the class?
+ if (class_exists($module)) {
+ continue;
+ } else {
+ throw new \Exception('The required class "' . $module . '" could not be found but the module-file exists', 404);
+ }
+ } catch (\Exception $e) {
+ // The autoloader will throw an Exception
+ // that the required class could not be found
+ // but we want a nicer error-message for this here
+ throw new \Exception('The required module "' . $module . '" could not be found', 404);
+ }
+ }
+ }
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/Ftps.php b/lib/Froxlor/Api/Commands/Ftps.php
new file mode 100644
index 00000000..dd33c48f
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/Ftps.php
@@ -0,0 +1,646 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class Ftps extends \Froxlor\Api\ApiCommand implements \Froxlor\Api\ResourceEntity
+{
+
+ /**
+ * add a new ftp-user
+ *
+ * @param string $ftp_password
+ * password for the created database and database-user
+ * @param string $path
+ * destination path relative to the customers-homedir
+ * @param string $ftp_description
+ * optional, description for ftp-user
+ * @param bool $sendinfomail
+ * optional, send created resource-information to customer, default: false
+ * @param string $shell
+ * optional, default /bin/false (not changeable when deactivated)
+ * @param string $ftp_username
+ * optional if customer.ftpatdomain is allowed, specify an username
+ * @param string $ftp_domain
+ * optional if customer.ftpatdomain is allowed, specify a domain (customer must be owner)
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ * @param array $additional_members
+ * optional whether to add additional usernames to the group
+ * @param bool $is_defaultuser
+ * optional whether this is the standard default ftp user which is being added so no usage is decreased
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function add()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'ftp')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ $is_defaultuser = $this->getBoolParam('is_defaultuser', true, 0);
+
+ if (($this->getUserDetail('ftps_used') < $this->getUserDetail('ftps') || $this->getUserDetail('ftps') == '-1') || $this->isAdmin() && $is_defaultuser == 1) {
+
+ // required paramters
+ $path = $this->getParam('path');
+ $password = $this->getParam('ftp_password');
+
+ // parameters
+ $description = $this->getParam('ftp_description', true, '');
+ $sendinfomail = $this->getBoolParam('sendinfomail', true, 0);
+ $shell = $this->getParam('shell', true, '/bin/false');
+
+ $ftpusername = $this->getParam('ftp_username', true, '');
+ $ftpdomain = $this->getParam('ftp_domain', true, '');
+
+ $additional_members = $this->getParam('additional_members', true, array());
+
+ // validation
+ $password = \Froxlor\Validate\Validate::validate($password, 'password', '', '', array(), true);
+ $password = \Froxlor\System\Crypt::validatePassword($password, true);
+ $description = \Froxlor\Validate\Validate::validate(trim($description), 'description', '', '', array(), true);
+
+ if (Settings::Get('system.allow_customer_shell') == '1') {
+ $shell = \Froxlor\Validate\Validate::validate(trim($shell), 'shell', '', '', array(), true);
+ } else {
+ $shell = "/bin/false";
+ }
+
+ if (Settings::Get('customer.ftpatdomain') == '1') {
+ $ftpusername = \Froxlor\Validate\Validate::validate(trim($ftpusername), 'username', '/^[a-zA-Z0-9][a-zA-Z0-9\-_]+\$?$/', '', array(), true);
+ if (substr($ftpdomain, 0, 4) != 'xn--') {
+ $idna_convert = new \Froxlor\Idna\IdnaWrapper();
+ $ftpdomain = $idna_convert->encode(\Froxlor\Validate\Validate::validate($ftpdomain, 'domain', '', '', array(), true));
+ }
+ }
+
+ $params = array();
+ // get needed customer info to reduce the ftp-user-counter by one
+ if ($is_defaultuser) {
+ // no resource check for default user
+ $customer = $this->getCustomerData();
+ } else {
+ $customer = $this->getCustomerData('ftps');
+ }
+
+ if ($sendinfomail != 1) {
+ $sendinfomail = 0;
+ }
+
+ if (Settings::Get('customer.ftpatdomain') == '1' && ! $is_defaultuser) {
+ if ($ftpusername == '') {
+ \Froxlor\UI\Response::standard_error(array(
+ 'stringisempty',
+ 'username'
+ ), '', true);
+ }
+ $ftpdomain_check_stmt = Database::prepare("SELECT `id`, `domain`, `customerid` FROM `" . TABLE_PANEL_DOMAINS . "`
+ WHERE `domain` = :domain
+ AND `customerid` = :customerid");
+ $ftpdomain_check = Database::pexecute_first($ftpdomain_check_stmt, array(
+ "domain" => $ftpdomain,
+ "customerid" => $customer['customerid']
+ ), true, true);
+
+ if ($ftpdomain_check && $ftpdomain_check['domain'] != $ftpdomain) {
+ \Froxlor\UI\Response::standard_error('maindomainnonexist', $ftpdomain, true);
+ }
+ $username = $ftpusername . "@" . $ftpdomain;
+ } else {
+ if ($is_defaultuser) {
+ $username = $customer['loginname'];
+ } else {
+ $username = $customer['loginname'] . Settings::Get('customer.ftpprefix') . (intval($customer['ftp_lastaccountnumber']) + 1);
+ }
+ }
+
+ $username_check_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_FTP_USERS . "` WHERE `username` = :username
+ ");
+ $username_check = Database::pexecute_first($username_check_stmt, array(
+ "username" => $username
+ ), true, true);
+
+ if (! empty($username_check) && $username_check['username'] = $username) {
+ \Froxlor\UI\Response::standard_error('usernamealreadyexists', $username, true);
+ } elseif ($username == $password) {
+ \Froxlor\UI\Response::standard_error('passwordshouldnotbeusername', '', true);
+ } else {
+ $path = \Froxlor\FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path);
+ $cryptPassword = \Froxlor\System\Crypt::makeCryptPassword($password);
+
+ $stmt = Database::prepare("INSERT INTO `" . TABLE_FTP_USERS . "`
+ (`customerid`, `username`, `description`, `password`, `homedir`, `login_enabled`, `uid`, `gid`, `shell`)
+ VALUES (:customerid, :username, :description, :password, :homedir, 'y', :guid, :guid, :shell)");
+ $params = array(
+ "customerid" => $customer['customerid'],
+ "username" => $username,
+ "description" => $description,
+ "password" => $cryptPassword,
+ "homedir" => $path,
+ "guid" => $customer['guid'],
+ "shell" => $shell
+ );
+ Database::pexecute($stmt, $params, true, true);
+
+ $result_stmt = Database::prepare("
+ SELECT `bytes_in_used` FROM `" . TABLE_FTP_QUOTATALLIES . "` WHERE `name` = :name
+ ");
+ Database::pexecute($result_stmt, array(
+ "name" => $customer['loginname']
+ ), true, true);
+
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $stmt = Database::prepare("INSERT INTO `" . TABLE_FTP_QUOTATALLIES . "`
+ (`name`, `quota_type`, `bytes_in_used`, `bytes_out_used`, `bytes_xfer_used`, `files_in_used`, `files_out_used`, `files_xfer_used`)
+ VALUES (:name, 'user', :bytes_in_used, '0', '0', '0', '0', '0')
+ ");
+ Database::pexecute($stmt, array(
+ "name" => $username,
+ "bytes_in_used" => $row['bytes_in_used']
+ ), true, true);
+ }
+
+ // create quotatallies entry if it not exists, refs #885
+ if ($result_stmt->rowCount() == 0) {
+ $stmt = Database::prepare("INSERT INTO `" . TABLE_FTP_QUOTATALLIES . "`
+ (`name`, `quota_type`, `bytes_in_used`, `bytes_out_used`, `bytes_xfer_used`, `files_in_used`, `files_out_used`, `files_xfer_used`)
+ VALUES (:name, 'user', '0', '0', '0', '0', '0', '0')
+ ");
+ Database::pexecute($stmt, array(
+ "name" => $username
+ ), true, true);
+ }
+
+ $group_upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_FTP_GROUPS . "`
+ SET `members` = CONCAT_WS(',',`members`, :username)
+ WHERE `customerid`= :customerid AND `gid`= :guid
+ ");
+ $params = array(
+ "username" => $username,
+ "customerid" => $customer['customerid'],
+ "guid" => $customer['guid']
+ );
+
+ if ($is_defaultuser) {
+ // add the new group
+ $group_ins_stmt = Database::prepare("
+ INSERT INTO `" . TABLE_FTP_GROUPS . "`
+ SET `customerid`= :customerid, `gid`= :guid, `groupname` = :username, `members` = :username
+ ");
+ Database::pexecute($group_ins_stmt, $params, true, true);
+ } else {
+ // just update
+ Database::pexecute($group_upd_stmt, $params, true, true);
+ }
+
+ if (count($additional_members) > 0) {
+ foreach ($additional_members as $add_member) {
+ $params = array(
+ "username" => $add_member,
+ "customerid" => $customer['customerid'],
+ "guid" => $customer['guid']
+ );
+ Database::pexecute($group_upd_stmt, $params, true, true);
+ }
+ }
+
+ if (! $is_defaultuser) {
+ // update customer usage
+ Customers::increaseUsage($customer['customerid'], 'ftps_used');
+ Customers::increaseUsage($customer['customerid'], 'ftp_lastaccountnumber');
+ }
+
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] added ftp-account '" . $username . " (" . $path . ")'");
+ \Froxlor\System\Cronjob::inserttask(5);
+
+ if ($sendinfomail == 1) {
+ $replace_arr = array(
+ 'SALUTATION' => \Froxlor\User::getCorrectUserSalutation($customer),
+ 'CUST_NAME' => \Froxlor\User::getCorrectUserSalutation($customer), // < keep this for compatibility
+ 'NAME' => $customer['name'],
+ 'FIRSTNAME' => $customer['firstname'],
+ 'COMPANY' => $customer['company'],
+ 'CUSTOMER_NO' => $customer['customernumber'],
+ 'USR_NAME' => $username,
+ 'USR_PASS' => $password,
+ 'USR_PATH' => \Froxlor\FileDir::makeCorrectDir(str_replace($customer['documentroot'], "/", $path))
+ );
+ // get template for mail subject
+ $mail_subject = $this->getMailTemplate($customer, 'mails', 'new_ftpaccount_by_customer_subject', $replace_arr, $this->lng['mails']['new_ftpaccount_by_customer']['subject']);
+ // get template for mail body
+ $mail_body = $this->getMailTemplate($customer, 'mails', 'new_ftpaccount_by_customer_mailbody', $replace_arr, $this->lng['mails']['new_ftpaccount_by_customer']['mailbody']);
+
+ $_mailerror = false;
+ $mailerr_msg = "";
+ try {
+ $this->mailer()->Subject = $mail_subject;
+ $this->mailer()->AltBody = $mail_body;
+ $this->mailer()->msgHTML(str_replace("\n", " ", $mail_body));
+ $this->mailer()->addAddress($customer['email'], \Froxlor\User::getCorrectUserSalutation($customer));
+ $this->mailer()->send();
+ } catch (\PHPMailer\PHPMailer\Exception $e) {
+ $mailerr_msg = $e->errorMessage();
+ $_mailerror = true;
+ } catch (\Exception $e) {
+ $mailerr_msg = $e->getMessage();
+ $_mailerror = true;
+ }
+
+ if ($_mailerror) {
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_ERR, "[API] Error sending mail: " . $mailerr_msg);
+ \Froxlor\UI\Response::standard_error('errorsendingmail', $customer['email'], true);
+ }
+
+ $this->mailer()->clearAddresses();
+ }
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] added ftp-user '" . $username . "'");
+
+ $result = $this->apiCall('Ftps.get', array(
+ 'username' => $username
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ }
+ throw new \Exception("No more resources available", 406);
+ }
+
+ /**
+ * return a ftp-user entry by either id or username
+ *
+ * @param int $id
+ * optional, the customer-id
+ * @param string $username
+ * optional, the username
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function get()
+ {
+ $id = $this->getParam('id', true, 0);
+ $un_optional = ($id <= 0 ? false : true);
+ $username = $this->getParam('username', $un_optional, '');
+
+ $params = array();
+ if ($this->isAdmin()) {
+ if ($this->getUserDetail('customers_see_all') == false) {
+ // if it's a reseller or an admin who cannot see all customers, we need to check
+ // whether the database belongs to one of his customers
+ $_custom_list_result = $this->apiCall('Customers.listing');
+ $custom_list_result = $_custom_list_result['list'];
+ $customer_ids = array();
+ foreach ($custom_list_result as $customer) {
+ $customer_ids[] = $customer['customerid'];
+ }
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_FTP_USERS . "`
+ WHERE `customerid` IN (" . implode(", ", $customer_ids) . ")
+ AND (`id` = :idun OR `username` = :idun)
+ ");
+ } else {
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_FTP_USERS . "`
+ WHERE (`id` = :idun OR `username` = :idun)
+ ");
+ }
+ } else {
+ if (Settings::IsInList('panel.customer_hide_options', 'ftp')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_FTP_USERS . "`
+ WHERE `customerid` = :customerid
+ AND (`id` = :idun OR `username` = :idun)
+ ");
+ $params['customerid'] = $this->getUserDetail('customerid');
+ }
+ $params['idun'] = ($id <= 0 ? $username : $id);
+ $result = Database::pexecute_first($result_stmt, $params, true, true);
+ if ($result) {
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] get ftp-user '" . $result['username'] . "'");
+ return $this->response(200, "successful", $result);
+ }
+ $key = ($id > 0 ? "id #" . $id : "username '" . $username . "'");
+ throw new \Exception("FTP user with " . $key . " could not be found", 404);
+ }
+
+ /**
+ * update a given ftp-user by id or username
+ *
+ * @param int $id
+ * optional, the ftp-user-id
+ * @param string $username
+ * optional, the username
+ * @param string $ftp_password
+ * optional, update password if specified
+ * @param string $path
+ * destination path relative to the customers-homedir
+ * @param string $ftp_description
+ * optional, description for ftp-user
+ * @param string $shell
+ * optional, default /bin/false (not changeable when deactivated)
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function update()
+ {
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'ftp')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ $id = $this->getParam('id', true, 0);
+ $un_optional = ($id <= 0 ? false : true);
+ $username = $this->getParam('username', $un_optional, '');
+
+ $result = $this->apiCall('Ftps.get', array(
+ 'id' => $id,
+ 'username' => $username
+ ));
+ $id = $result['id'];
+
+ // parameters
+ $path = $this->getParam('path', true, '');
+ $password = $this->getParam('ftp_password', true, '');
+ $description = $this->getParam('ftp_description', true, $result['description']);
+ $shell = $this->getParam('shell', true, $result['shell']);
+
+ // validation
+ $password = \Froxlor\Validate\Validate::validate($password, 'password', '', '', array(), true);
+ $description = \Froxlor\Validate\Validate::validate(trim($description), 'description', '', '', array(), true);
+
+ if (Settings::Get('system.allow_customer_shell') == '1') {
+ $shell = \Froxlor\Validate\Validate::validate(trim($shell), 'shell', '', '', array(), true);
+ } else {
+ $shell = "/bin/false";
+ }
+
+ // get needed customer info to reduce the ftp-user-counter by one
+ $customer = $this->getCustomerData();
+
+ // password update?
+ if ($password != '') {
+ // validate password
+ $password = \Froxlor\System\Crypt::validatePassword($password, true);
+
+ if ($password == $result['username']) {
+ \Froxlor\UI\Response::standard_error('passwordshouldnotbeusername', '', true);
+ }
+ $cryptPassword = \Froxlor\System\Crypt::makeCryptPassword($password);
+
+ $stmt = Database::prepare("UPDATE `" . TABLE_FTP_USERS . "`
+ SET `password` = :password
+ WHERE `customerid` = :customerid
+ AND `id` = :id
+ ");
+ Database::pexecute($stmt, array(
+ "customerid" => $customer['customerid'],
+ "id" => $id,
+ "password" => $cryptPassword
+ ), true, true);
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] updated ftp-account password for '" . $result['username'] . "'");
+ }
+
+ // path update?
+ if ($path != '') {
+ $path = \Froxlor\FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path);
+
+ if ($path != $result['homedir']) {
+ $stmt = Database::prepare("UPDATE `" . TABLE_FTP_USERS . "`
+ SET `homedir` = :homedir
+ WHERE `customerid` = :customerid
+ AND `id` = :id
+ ");
+ Database::pexecute($stmt, array(
+ "homedir" => $path,
+ "customerid" => $customer['customerid'],
+ "id" => $id
+ ), true, true);
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] updated ftp-account homdir for '" . $result['username'] . "'");
+ }
+ }
+ // it's the task for "new ftp" but that will
+ // create all directories and correct their permissions
+ \Froxlor\System\Cronjob::inserttask(5);
+
+ $stmt = Database::prepare("
+ UPDATE `" . TABLE_FTP_USERS . "`
+ SET `description` = :desc, `shell` = :shell
+ WHERE `customerid` = :customerid
+ AND `id` = :id
+ ");
+ Database::pexecute($stmt, array(
+ "desc" => $description,
+ "shell" => $shell,
+ "customerid" => $customer['customerid'],
+ "id" => $id
+ ), true, true);
+
+ $result = $this->apiCall('Ftps.get', array(
+ 'username' => $result['username']
+ ));
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] updated ftp-user '" . $result['username'] . "'");
+ return $this->response(200, "successful", $result);
+ }
+
+ /**
+ * list all ftp-users, if called from an admin, list all ftp-users of all customers you are allowed to view, or specify id or loginname for one specific customer
+ *
+ * @param int $customerid
+ * optional, admin-only, select ftp-users of a specific customer by id
+ * @param string $loginname
+ * optional, admin-only, select ftp-users of a specific customer by loginname
+ * @param array $sql_search
+ * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), LIKE is used if left empty and 'value' => searchvalue
+ * @param int $sql_limit
+ * optional specify number of results to be returned
+ * @param int $sql_offset
+ * optional specify offset for resultset
+ * @param array $sql_orderby
+ * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more fields
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listing()
+ {
+ $customer_ids = $this->getAllowedCustomerIds('ftp');
+ $result = array();
+ $query_fields = array();
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_FTP_USERS . "`
+ WHERE `customerid` IN (" . implode(", ", $customer_ids) . ")" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit());
+ Database::pexecute($result_stmt, $query_fields, true, true);
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = $row;
+ }
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] list ftp-users");
+ return $this->response(200, "successful", array(
+ 'count' => count($result),
+ 'list' => $result
+ ));
+ }
+
+ /**
+ * returns the total number of accessable ftp accounts
+ *
+ * @param int $customerid
+ * optional, admin-only, select ftp-users of a specific customer by id
+ * @param string $loginname
+ * optional, admin-only, select ftp-users of a specific customer by loginname
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function listingCount()
+ {
+ $customer_ids = $this->getAllowedCustomerIds('ftp');
+ $result = array();
+ $result_stmt = Database::prepare("
+ SELECT COUNT(*) as num_ftps FROM `" . TABLE_FTP_USERS . "`
+ WHERE `customerid` IN (" . implode(", ", $customer_ids) . ")
+ ");
+ $result = Database::pexecute_first($result_stmt, null, true, true);
+ if ($result) {
+ return $this->response(200, "successful", $result['num_ftps']);
+ }
+ }
+
+ /**
+ * delete a ftp-user by either id or username
+ *
+ * @param int $id
+ * optional, the ftp-user-id
+ * @param string $username
+ * optional, the username
+ * @param bool $delete_userfiles
+ * optional, default false
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function delete()
+ {
+ $id = $this->getParam('id', true, 0);
+ $un_optional = ($id <= 0 ? false : true);
+ $username = $this->getParam('username', $un_optional, '');
+ $delete_userfiles = $this->getBoolParam('delete_userfiles', true, 0);
+
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'ftp')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ // get ftp-user
+ $result = $this->apiCall('Ftps.get', array(
+ 'id' => $id,
+ 'username' => $username
+ ));
+ $id = $result['id'];
+
+ if ($this->isAdmin()) {
+ // get customer-data
+ $customer_data = $this->apiCall('Customers.get', array(
+ 'id' => $result['customerid']
+ ));
+ } else {
+ $customer_data = $this->getUserData();
+ }
+
+ // add usage of this ftp-user to main-ftp user of customer if different
+ if ($result['username'] != $customer_data['loginname']) {
+ $stmt = Database::prepare("UPDATE `" . TABLE_FTP_USERS . "`
+ SET `up_count` = `up_count` + :up_count,
+ `up_bytes` = `up_bytes` + :up_bytes,
+ `down_count` = `down_count` + :down_count,
+ `down_bytes` = `down_bytes` + :down_bytes
+ WHERE `username` = :username
+ ");
+ $params = array(
+ "up_count" => $result['up_count'],
+ "up_bytes" => $result['up_bytes'],
+ "down_count" => $result['down_count'],
+ "down_bytes" => $result['down_bytes'],
+ "username" => $customer_data['loginname']
+ );
+ Database::pexecute($stmt, $params, true, true);
+ } else {
+ // do not allow removing default ftp-account
+ \Froxlor\UI\Response::standard_error('ftp_cantdeletemainaccount', '', true);
+ }
+
+ // remove all quotatallies
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_FTP_QUOTATALLIES . "` WHERE `name` = :name");
+ Database::pexecute($stmt, array(
+ "name" => $result['username']
+ ), true, true);
+
+ // remove user itself
+ $stmt = Database::prepare("
+ DELETE FROM `" . TABLE_FTP_USERS . "` WHERE `customerid` = :customerid AND `id` = :id
+ ");
+ Database::pexecute($stmt, array(
+ "customerid" => $customer_data['customerid'],
+ "id" => $id
+ ), true, true);
+
+ // update ftp-groups
+ $stmt = Database::prepare("
+ UPDATE `" . TABLE_FTP_GROUPS . "` SET
+ `members` = REPLACE(`members`, :username,'')
+ WHERE `customerid` = :customerid
+ ");
+ Database::pexecute($stmt, array(
+ "username" => "," . $result['username'],
+ "customerid" => $customer_data['customerid']
+ ), true, true);
+
+ // refs #293
+ if ($delete_userfiles == 1) {
+ \Froxlor\System\Cronjob::inserttask('8', $customer_data['loginname'], $result['homedir']);
+ } else {
+ if (Settings::Get('system.nssextrausers') == 1) {
+ // this is used so that the libnss-extrausers cron is fired
+ \Froxlor\System\Cronjob::inserttask(5);
+ }
+ }
+
+ // decrease ftp-user usage for customer
+ $resetaccnumber = ($customer_data['ftps_used'] == '1') ? " , `ftp_lastaccountnumber`='0'" : '';
+ Customers::decreaseUsage($customer_data['customerid'], 'ftps_used', $resetaccnumber);
+
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] deleted ftp-user '" . $result['username'] . "'");
+ return $this->response(200, "successful", $result);
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/HostingPlans.php b/lib/Froxlor/Api/Commands/HostingPlans.php
new file mode 100644
index 00000000..ca3a76bc
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/HostingPlans.php
@@ -0,0 +1,438 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class HostingPlans extends \Froxlor\Api\ApiCommand implements \Froxlor\Api\ResourceEntity
+{
+
+ /**
+ * list all available hosting plans
+ *
+ * @param array $sql_search
+ * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), LIKE is used if left empty and 'value' => searchvalue
+ * @param int $sql_limit
+ * optional specify number of results to be returned
+ * @param int $sql_offset
+ * optional specify offset for resultset
+ * @param array $sql_orderby
+ * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more fields
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listing()
+ {
+ if ($this->isAdmin()) {
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] list hosting-plans");
+ $query_fields = array();
+ $result_stmt = Database::prepare("
+ SELECT p.*, a.loginname as adminname
+ FROM `" . TABLE_PANEL_PLANS . "` p, `" . TABLE_PANEL_ADMINS . "` a
+ WHERE `p`.`adminid` = `a`.`adminid`" . ($this->getUserDetail('customers_see_all') ? '' : " AND `p`.`adminid` = :adminid ") . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit());
+ $params = array();
+ if ($this->getUserDetail('customers_see_all') == '0') {
+ $params['adminid'] = $this->getUserDetail('adminid');
+ }
+ $params = array_merge($params, $query_fields);
+ Database::pexecute($result_stmt, $params, true, true);
+ $result = array();
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = $row;
+ }
+ return $this->response(200, "successful", array(
+ 'count' => count($result),
+ 'list' => $result
+ ));
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * returns the total number of accessable hosting plans
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function listingCount()
+ {
+ if ($this->isAdmin()) {
+ $result_stmt = Database::prepare("
+ SELECT COUNT(*) as num_plans
+ FROM `" . TABLE_PANEL_PLANS . "` p, `" . TABLE_PANEL_ADMINS . "` a
+ WHERE `p`.`adminid` = `a`.`adminid`" . ($this->getUserDetail('customers_see_all') ? '' : " AND `p`.`adminid` = :adminid "));
+ $params = array();
+ if ($this->getUserDetail('customers_see_all') == '0') {
+ $params['adminid'] = $this->getUserDetail('adminid');
+ }
+ $result = Database::pexecute_first($result_stmt, $params, true, true);
+ if ($result) {
+ return $this->response(200, "successful", $result['num_plans']);
+ }
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * return a hosting-plan entry by either id or plan-name
+ *
+ * @param int $id
+ * optional, the hosting-plan-id
+ * @param string $planname
+ * optional, the hosting-plan-name
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function get()
+ {
+ if ($this->isAdmin()) {
+ $id = $this->getParam('id', true, 0);
+ $dn_optional = ($id <= 0 ? false : true);
+ $planname = $this->getParam('planname', $dn_optional, '');
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_PLANS . "` WHERE " . ($id > 0 ? "`id` = :iddn" : "`name` = :iddn") . ($this->getUserDetail('customers_see_all') ? '' : " AND `adminid` = :adminid"));
+ $params = array(
+ 'iddn' => ($id <= 0 ? $planname : $id)
+ );
+ if ($this->getUserDetail('customers_see_all') == '0') {
+ $params['adminid'] = $this->getUserDetail('adminid');
+ }
+ $result = Database::pexecute_first($result_stmt, $params, true, true);
+ if ($result) {
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] get hosting-plan '" . $result['name'] . "'");
+ return $this->response(200, "successful", $result);
+ }
+ $key = ($id > 0 ? "id #" . $id : "planname '" . $planname . "'");
+ throw new \Exception("Hosting-plan with " . $key . " could not be found", 404);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * add new hosting-plan
+ *
+ * @param string $name
+ * name of the plan
+ * @param string $description
+ * optional, description for hosting-plan
+ * @param int $diskspace
+ * optional disk-space available for customer in MB, default 0
+ * @param bool $diskspace_ul
+ * optional, whether customer should have unlimited diskspace, default 0 (false)
+ * @param int $traffic
+ * optional traffic available for customer in GB, default 0
+ * @param bool $traffic_ul
+ * optional, whether customer should have unlimited traffic, default 0 (false)
+ * @param int $subdomains
+ * optional amount of subdomains available for customer, default 0
+ * @param bool $subdomains_ul
+ * optional, whether customer should have unlimited subdomains, default 0 (false)
+ * @param int $emails
+ * optional amount of emails available for customer, default 0
+ * @param bool $emails_ul
+ * optional, whether customer should have unlimited emails, default 0 (false)
+ * @param int $email_accounts
+ * optional amount of email-accounts available for customer, default 0
+ * @param bool $email_accounts_ul
+ * optional, whether customer should have unlimited email-accounts, default 0 (false)
+ * @param int $email_forwarders
+ * optional amount of email-forwarders available for customer, default 0
+ * @param bool $email_forwarders_ul
+ * optional, whether customer should have unlimited email-forwarders, default 0 (false)
+ * @param int $email_quota
+ * optional size of email-quota available for customer in MB, default is system-setting mail_quota
+ * @param bool $email_quota_ul
+ * optional, whether customer should have unlimited email-quota, default 0 (false)
+ * @param bool $email_imap
+ * optional, whether to allow IMAP access, default 0 (false)
+ * @param bool $email_pop3
+ * optional, whether to allow POP3 access, default 0 (false)
+ * @param int $ftps
+ * optional amount of ftp-accounts available for customer, default 0
+ * @param bool $ftps_ul
+ * optional, whether customer should have unlimited ftp-accounts, default 0 (false)
+ * @param int $mysqls
+ * optional amount of mysql-databases available for customer, default 0
+ * @param bool $mysqls_ul
+ * optional, whether customer should have unlimited mysql-databases, default 0 (false)
+ * @param bool $phpenabled
+ * optional, whether to allow usage of PHP, default 0 (false)
+ * @param array $allowed_phpconfigs
+ * optional, array of IDs of php-config that the customer is allowed to use, default empty (none)
+ * @param bool $perlenabled
+ * optional, whether to allow usage of Perl/CGI, default 0 (false)
+ * @param bool $dnsenabled
+ * optional, ether to allow usage of the DNS editor (requires activated nameserver in settings), default 0 (false)
+ * @param bool $logviewenabled
+ * optional, ether to allow acccess to webserver access/error-logs, default 0 (false)
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function add()
+ {
+ if ($this->isAdmin()) {
+ $name = $this->getParam('name');
+ $description = $this->getParam('description', true, '');
+
+ $value_arr = array();
+ $value_arr['diskspace'] = $this->getUlParam('diskspace', 'diskspace_ul', true, 0);
+ $value_arr['traffic'] = $this->getUlParam('traffic', 'traffic_ul', true, 0);
+ $value_arr['subdomains'] = $this->getUlParam('subdomains', 'subdomains_ul', true, 0);
+ $value_arr['emails'] = $this->getUlParam('emails', 'emails_ul', true, 0);
+ $value_arr['email_accounts'] = $this->getUlParam('email_accounts', 'email_accounts_ul', true, 0);
+ $value_arr['email_forwarders'] = $this->getUlParam('email_forwarders', 'email_forwarders_ul', true, 0);
+ $value_arr['email_quota'] = $this->getUlParam('email_quota', 'email_quota_ul', true, Settings::Get('system.mail_quota'));
+ $value_arr['email_imap'] = $this->getBoolParam('email_imap', true, 0);
+ $value_arr['email_pop3'] = $this->getBoolParam('email_pop3', true, 0);
+ $value_arr['ftps'] = $this->getUlParam('ftps', 'ftps_ul', true, 0);
+ $value_arr['mysqls'] = $this->getUlParam('mysqls', 'mysqls_ul', true, 0);
+ $value_arr['phpenabled'] = $this->getBoolParam('phpenabled', true, 0);
+ $p_allowed_phpconfigs = $this->getParam('allowed_phpconfigs', true, array());
+ $value_arr['perlenabled'] = $this->getBoolParam('perlenabled', true, 0);
+ $value_arr['dnsenabled'] = $this->getBoolParam('dnsenabled', true, 0);
+ $value_arr['logviewenabled'] = $this->getBoolParam('logviewenabled', true, 0);
+
+ // validation
+ $name = \Froxlor\Validate\Validate::validate(trim($name), 'name', '', '', array(), true);
+ $description = \Froxlor\Validate\Validate::validate(str_replace("\r\n", "\n", $description), 'description', '/^[^\0]*$/');
+
+ if (Settings::Get('system.mail_quota_enabled') != '1') {
+ $value_arr['email_quota'] = - 1;
+ }
+
+ $value_arr['allowed_phpconfigs'] = array();
+ if (! empty($p_allowed_phpconfigs) && is_array($p_allowed_phpconfigs)) {
+ foreach ($p_allowed_phpconfigs as $allowed_phpconfig) {
+ $allowed_phpconfig = intval($allowed_phpconfig);
+ $value_arr['allowed_phpconfigs'][] = $allowed_phpconfig;
+ }
+ }
+ $value_arr['allowed_phpconfigs'] = array_map('intval', $value_arr['allowed_phpconfigs']);
+
+ $ins_stmt = Database::prepare("
+ INSERT INTO `" . TABLE_PANEL_PLANS . "`
+ SET `adminid` = :adminid, `name` = :name, `description` = :desc, `value` = :valuearr, `ts` = UNIX_TIMESTAMP();
+ ");
+ $ins_data = array(
+ 'adminid' => $this->getUserDetail('adminid'),
+ 'name' => $name,
+ 'desc' => $description,
+ 'valuearr' => json_encode($value_arr)
+ );
+ Database::pexecute($ins_stmt, $ins_data, true, true);
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] added hosting-plan '" . $name . "'");
+ $result = $this->apiCall('HostingPlans.get', array(
+ 'planname' => $name
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * update hosting-plan by either id or plan-name
+ *
+ * @param int $id
+ * optional the hosting-plan-id
+ * @param string $planname
+ * optional the hosting-plan-name
+ * @param string $name
+ * optional name of the plan
+ * @param string $description
+ * optional description for hosting-plan
+ * @param int $diskspace
+ * optional disk-space available for customer in MB, default 0
+ * @param bool $diskspace_ul
+ * optional, whether customer should have unlimited diskspace, default 0 (false)
+ * @param int $traffic
+ * optional traffic available for customer in GB, default 0
+ * @param bool $traffic_ul
+ * optional, whether customer should have unlimited traffic, default 0 (false)
+ * @param int $subdomains
+ * optional amount of subdomains available for customer, default 0
+ * @param bool $subdomains_ul
+ * optional, whether customer should have unlimited subdomains, default 0 (false)
+ * @param int $emails
+ * optional amount of emails available for customer, default 0
+ * @param bool $emails_ul
+ * optional, whether customer should have unlimited emails, default 0 (false)
+ * @param int $email_accounts
+ * optional amount of email-accounts available for customer, default 0
+ * @param bool $email_accounts_ul
+ * optional, whether customer should have unlimited email-accounts, default 0 (false)
+ * @param int $email_forwarders
+ * optional amount of email-forwarders available for customer, default 0
+ * @param bool $email_forwarders_ul
+ * optional, whether customer should have unlimited email-forwarders, default 0 (false)
+ * @param int $email_quota
+ * optional size of email-quota available for customer in MB, default is system-setting mail_quota
+ * @param bool $email_quota_ul
+ * optional, whether customer should have unlimited email-quota, default 0 (false)
+ * @param bool $email_imap
+ * optional, whether to allow IMAP access, default 0 (false)
+ * @param bool $email_pop3
+ * optional, whether to allow POP3 access, default 0 (false)
+ * @param int $ftps
+ * optional amount of ftp-accounts available for customer, default 0
+ * @param bool $ftps_ul
+ * optional, whether customer should have unlimited ftp-accounts, default 0 (false)
+ * @param int $mysqls
+ * optional amount of mysql-databases available for customer, default 0
+ * @param bool $mysqls_ul
+ * optional, whether customer should have unlimited mysql-databases, default 0 (false)
+ * @param bool $phpenabled
+ * optional, whether to allow usage of PHP, default 0 (false)
+ * @param array $allowed_phpconfigs
+ * optional, array of IDs of php-config that the customer is allowed to use, default empty (none)
+ * @param bool $perlenabled
+ * optional, whether to allow usage of Perl/CGI, default 0 (false)
+ * @param bool $dnsenabled
+ * optional, ether to allow usage of the DNS editor (requires activated nameserver in settings), default 0 (false)
+ * @param bool $logviewenabled
+ * optional, ether to allow acccess to webserver access/error-logs, default 0 (false)
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function update()
+ {
+ if ($this->isAdmin()) {
+
+ // parameters
+ $id = $this->getParam('id', true, 0);
+ $dn_optional = ($id <= 0 ? false : true);
+ $planname = $this->getParam('planname', $dn_optional, '');
+
+ // get requested hosting-plan
+ $result = $this->apiCall('HostingPlans.get', array(
+ 'id' => $id,
+ 'planname' => $planname
+ ));
+ $id = $result['id'];
+
+ $result['value'] = json_decode($result['value'], true);
+ foreach ($result['value'] as $index => $value) {
+ $result[$index] = $value;
+ }
+
+ $name = $this->getParam('name', true, $result['name']);
+ $description = $this->getParam('description', true, $result['description']);
+
+ $value_arr = array();
+ $value_arr['diskspace'] = $this->getUlParam('diskspace', 'diskspace_ul', true, $result['diskspace']);
+ $value_arr['traffic'] = $this->getUlParam('traffic', 'traffic_ul', true, $result['traffic']);
+ $value_arr['subdomains'] = $this->getUlParam('subdomains', 'subdomains_ul', true, $result['subdomains']);
+ $value_arr['emails'] = $this->getUlParam('emails', 'emails_ul', true, $result['emails']);
+ $value_arr['email_accounts'] = $this->getUlParam('email_accounts', 'email_accounts_ul', true, $result['email_accounts']);
+ $value_arr['email_forwarders'] = $this->getUlParam('email_forwarders', 'email_forwarders_ul', true, $result['email_forwarders']);
+ $value_arr['email_quota'] = $this->getUlParam('email_quota', 'email_quota_ul', true, $result['email_quota']);
+ $value_arr['email_imap'] = $this->getParam('email_imap', true, $result['email_imap']);
+ $value_arr['email_pop3'] = $this->getParam('email_pop3', true, $result['email_pop3']);
+ $value_arr['ftps'] = $this->getUlParam('ftps', 'ftps_ul', true, $result['ftps']);
+ $value_arr['mysqls'] = $this->getUlParam('mysqls', 'mysqls_ul', true, $result['mysqls']);
+ $value_arr['phpenabled'] = $this->getBoolParam('phpenabled', true, $result['phpenabled']);
+ $p_allowed_phpconfigs = $this->getParam('allowed_phpconfigs', true, $result['allowed_phpconfigs']);
+ $value_arr['perlenabled'] = $this->getBoolParam('perlenabled', true, $result['perlenabled']);
+ $value_arr['dnsenabled'] = $this->getBoolParam('dnsenabled', true, $result['dnsenabled']);
+ $value_arr['logviewenabled'] = $this->getBoolParam('logviewenabled', true, $result['logviewenabled']);
+
+ // validation
+ $name = \Froxlor\Validate\Validate::validate(trim($name), 'name', '', '', array(), true);
+ $description = \Froxlor\Validate\Validate::validate(str_replace("\r\n", "\n", $description), 'description', '/^[^\0]*$/');
+
+ if (Settings::Get('system.mail_quota_enabled') != '1') {
+ $value_arr['email_quota'] = - 1;
+ }
+
+ if (empty($name)) {
+ $name = $result['name'];
+ }
+
+ $value_arr['allowed_phpconfigs'] = array();
+ if (! empty($p_allowed_phpconfigs) && is_array($p_allowed_phpconfigs)) {
+ foreach ($p_allowed_phpconfigs as $allowed_phpconfig) {
+ $allowed_phpconfig = intval($allowed_phpconfig);
+ $value_arr['allowed_phpconfigs'][] = $allowed_phpconfig;
+ }
+ }
+ $value_arr['allowed_phpconfigs'] = array_map('intval', $value_arr['allowed_phpconfigs']);
+
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_PLANS . "`
+ SET `name` = :name, `description` = :desc, `value` = :valuearr, `ts` = UNIX_TIMESTAMP()
+ WHERE `id` = :id
+ ");
+ $update_data = array(
+ 'name' => $name,
+ 'desc' => $description,
+ 'valuearr' => json_encode($value_arr),
+ 'id' => $id
+ );
+ Database::pexecute($upd_stmt, $update_data, true, true);
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] updated hosting-plan '" . $result['name'] . "'");
+ return $this->response(200, "successful", $update_data);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * delete hosting-plan by either id or plan-name
+ *
+ * @param int $id
+ * optional the hosting-plan-id
+ * @param string $planname
+ * optional the hosting-plan-name
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function delete()
+ {
+ if ($this->isAdmin()) {
+ $id = $this->getParam('id', true, 0);
+ $dn_optional = ($id <= 0 ? false : true);
+ $planname = $this->getParam('planname', $dn_optional, '');
+
+ // get requested hosting-plan
+ $result = $this->apiCall('HostingPlans.get', array(
+ 'id' => $id,
+ 'planname' => $planname
+ ));
+ $id = $result['id'];
+
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_PANEL_PLANS . "` WHERE `id` = :id
+ ");
+ Database::pexecute($del_stmt, array(
+ 'id' => $id
+ ), true, true);
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] deleted hosting-plan '" . $result['name'] . "'");
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/IpsAndPorts.php b/lib/Froxlor/Api/Commands/IpsAndPorts.php
new file mode 100644
index 00000000..b4213ed0
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/IpsAndPorts.php
@@ -0,0 +1,599 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class IpsAndPorts extends \Froxlor\Api\ApiCommand implements \Froxlor\Api\ResourceEntity
+{
+
+ /**
+ * lists all ip/port entries
+ *
+ * @param array $sql_search
+ * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), LIKE is used if left empty and 'value' => searchvalue
+ * @param int $sql_limit
+ * optional specify number of results to be returned
+ * @param int $sql_offset
+ * optional specify offset for resultset
+ * @param array $sql_orderby
+ * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more fields
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listing()
+ {
+ if ($this->isAdmin() && ($this->getUserDetail('change_serversettings') || ! empty($this->getUserDetail('ip')))) {
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] list ips and ports");
+ $ip_where = "";
+ $append_where = false;
+ if (! empty($this->getUserDetail('ip')) && $this->getUserDetail('ip') != - 1) {
+ $ip_where = "WHERE `id` IN (" . implode(", ", json_decode($this->getUserDetail('ip'), true)) . ")";
+ $append_where = true;
+ }
+ $query_fields = array();
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_IPSANDPORTS . "` " . $ip_where . $this->getSearchWhere($query_fields, $append_where) . $this->getOrderBy() . $this->getLimit());
+ Database::pexecute($result_stmt, $query_fields, true, true);
+ $result = array();
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = $row;
+ }
+ return $this->response(200, "successful", array(
+ 'count' => count($result),
+ 'list' => $result
+ ));
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * returns the total number of accessable ip/port entries
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function listingCount()
+ {
+ if ($this->isAdmin() && ($this->getUserDetail('change_serversettings') || ! empty($this->getUserDetail('ip')))) {
+ $ip_where = "";
+ if (! empty($this->getUserDetail('ip')) && $this->getUserDetail('ip') != - 1) {
+ $ip_where = "WHERE `id` IN (" . implode(", ", json_decode($this->getUserDetail('ip'), true)) . ")";
+ }
+ $result_stmt = Database::prepare("
+ SELECT COUNT(*) as num_ips FROM `" . TABLE_PANEL_IPSANDPORTS . "` " . $ip_where);
+ $result = Database::pexecute_first($result_stmt, null, true, true);
+ if ($result) {
+ return $this->response(200, "successful", $result['num_ips']);
+ }
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * return an ip/port entry by id
+ *
+ * @param int $id
+ * ip-port-id
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function get()
+ {
+ if ($this->isAdmin() && ($this->getUserDetail('change_serversettings') || ! empty($this->getUserDetail('ip')))) {
+ $id = $this->getParam('id');
+ if (! empty($this->getUserDetail('ip')) && $this->getUserDetail('ip') != - 1) {
+ $allowed_ips = json_decode($this->getUserDetail('ip'), true);
+ if (! in_array($id, $allowed_ips)) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+ }
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `id` = :id
+ ");
+ $result = Database::pexecute_first($result_stmt, array(
+ 'id' => $id
+ ), true, true);
+ if ($result) {
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] get ip " . $result['ip'] . " " . $result['port']);
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("IP/port with id #" . $id . " could not be found", 404);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * create a new ip/port entry
+ *
+ * @param string $ip
+ * @param int $port
+ * optional, default 80
+ * @param bool $listen_statement
+ * optional, default 0 (false)
+ * @param bool $namevirtualhost_statement
+ * optional, default 0 (false)
+ * @param bool $vhostcontainer
+ * optional, default 0 (false)
+ * @param string $specialsettings
+ * optional, default empty
+ * @param bool $vhostcontainer_servername_statement
+ * optional, default 0 (false)
+ * @param string $default_vhostconf_domain
+ * optional, defatul empty
+ * @param string $docroot
+ * optional, default empty (point to froxlor)
+ * @param bool $ssl
+ * optional, default 0 (false)
+ * @param string $ssl_cert_file
+ * optional, requires $ssl = 1, default empty
+ * @param string $ssl_key_file
+ * optional, requires $ssl = 1, default empty
+ * @param string $ssl_ca_file
+ * optional, requires $ssl = 1, default empty
+ * @param string $ssl_cert_chainfile
+ * optional, requires $ssl = 1, default empty
+ * @param string $ssl_specialsettings
+ * optional, requires $ssl = 1, default empty
+ * @param bool $include_specialsettings
+ * optional, requires $ssl = 1, whether or not to include non-ssl specialsettings, default false
+ * @param string $ssl_default_vhostconf_domain
+ * optional, requires $ssl = 1, defatul empty
+ * @param bool $include_default_vhostconf_domain
+ * optional, requires $ssl = 1, whether or not to include non-ssl default_vhostconf_domain, default false
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function add()
+ {
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings')) {
+
+ $ip = \Froxlor\Validate\Validate::validate_ip2($this->getParam('ip'), false, 'invalidip', false, true, false, false, true);
+ $port = \Froxlor\Validate\Validate::validate($this->getParam('port', true, 80), 'port', \Froxlor\Validate\Validate::REGEX_PORT, array(
+ 'stringisempty',
+ 'myport'
+ ), array(), true);
+ $listen_statement = ! empty($this->getBoolParam('listen_statement', true, 0)) ? 1 : 0;
+ $namevirtualhost_statement = ! empty($this->getBoolParam('namevirtualhost_statement', true, 0)) ? 1 : 0;
+ $vhostcontainer = ! empty($this->getBoolParam('vhostcontainer', true, 0)) ? 1 : 0;
+ $specialsettings = \Froxlor\Validate\Validate::validate(str_replace("\r\n", "\n", $this->getParam('specialsettings', true, '')), 'specialsettings', \Froxlor\Validate\Validate::REGEX_CONF_TEXT, '', array(), true);
+ $vhostcontainer_servername_statement = ! empty($this->getBoolParam('vhostcontainer_servername_statement', true, 1)) ? 1 : 0;
+ $default_vhostconf_domain = \Froxlor\Validate\Validate::validate(str_replace("\r\n", "\n", $this->getParam('default_vhostconf_domain', true, '')), 'default_vhostconf_domain', \Froxlor\Validate\Validate::REGEX_CONF_TEXT, '', array(), true);
+ $docroot = \Froxlor\Validate\Validate::validate($this->getParam('docroot', true, ''), 'docroot', \Froxlor\Validate\Validate::REGEX_DIR, '', array(), true);
+
+ if ((int) Settings::Get('system.use_ssl') == 1) {
+ $ssl = ! empty($this->getBoolParam('ssl', true, 0)) ? intval($this->getBoolParam('ssl', true, 0)) : 0;
+ $ssl_cert_file = \Froxlor\Validate\Validate::validate($this->getParam('ssl_cert_file', $ssl, ''), 'ssl_cert_file', '', '', array(), true);
+ $ssl_key_file = \Froxlor\Validate\Validate::validate($this->getParam('ssl_key_file', $ssl, ''), 'ssl_key_file', '', '', array(), true);
+ $ssl_ca_file = \Froxlor\Validate\Validate::validate($this->getParam('ssl_ca_file', true, ''), 'ssl_ca_file', '', '', array(), true);
+ $ssl_cert_chainfile = \Froxlor\Validate\Validate::validate($this->getParam('ssl_cert_chainfile', true, ''), 'ssl_cert_chainfile', '', '', array(), true);
+ $ssl_specialsettings = \Froxlor\Validate\Validate::validate(str_replace("\r\n", "\n", $this->getParam('ssl_specialsettings', true, '')), 'ssl_specialsettings', \Froxlor\Validate\Validate::REGEX_CONF_TEXT, '', array(), true);
+ $include_specialsettings = ! empty($this->getBoolParam('include_specialsettings', true, 0)) ? 1 : 0;
+ $ssl_default_vhostconf_domain = \Froxlor\Validate\Validate::validate(str_replace("\r\n", "\n", $this->getParam('ssl_default_vhostconf_domain', true, '')), 'ssl_default_vhostconf_domain', \Froxlor\Validate\Validate::REGEX_CONF_TEXT, '', array(), true);
+ $include_default_vhostconf_domain = ! empty($this->getBoolParam('include_default_vhostconf_domain', true, 0)) ? 1 : 0;
+ } else {
+ $ssl = 0;
+ $ssl_cert_file = '';
+ $ssl_key_file = '';
+ $ssl_ca_file = '';
+ $ssl_cert_chainfile = '';
+ $ssl_specialsettings = '';
+ $include_specialsettings = 0;
+ $ssl_default_vhostconf_domain = '';
+ $include_default_vhostconf_domain = 0;
+ }
+
+ if ($listen_statement != '1') {
+ $listen_statement = '0';
+ }
+
+ if ($namevirtualhost_statement != '1') {
+ $namevirtualhost_statement = '0';
+ }
+
+ if ($vhostcontainer != '1') {
+ $vhostcontainer = '0';
+ }
+
+ if ($vhostcontainer_servername_statement != '1') {
+ $vhostcontainer_servername_statement = '0';
+ }
+
+ if ($ssl != '1') {
+ $ssl = '0';
+ }
+
+ if ($ssl_cert_file != '') {
+ $ssl_cert_file = \Froxlor\FileDir::makeCorrectFile($ssl_cert_file);
+ }
+
+ if ($ssl_key_file != '') {
+ $ssl_key_file = \Froxlor\FileDir::makeCorrectFile($ssl_key_file);
+ }
+
+ if ($ssl_ca_file != '') {
+ $ssl_ca_file = \Froxlor\FileDir::makeCorrectFile($ssl_ca_file);
+ }
+
+ if ($ssl_cert_chainfile != '') {
+ $ssl_cert_chainfile = \Froxlor\FileDir::makeCorrectFile($ssl_cert_chainfile);
+ }
+
+ if (strlen(trim($docroot)) > 0) {
+ $docroot = \Froxlor\FileDir::makeCorrectDir($docroot);
+ } else {
+ $docroot = '';
+ }
+
+ $result_checkfordouble_stmt = Database::prepare("
+ SELECT `id` FROM `" . TABLE_PANEL_IPSANDPORTS . "`
+ WHERE `ip` = :ip AND `port` = :port");
+ $result_checkfordouble = Database::pexecute_first($result_checkfordouble_stmt, array(
+ 'ip' => $ip,
+ 'port' => $port
+ ));
+
+ if ($result_checkfordouble && $result_checkfordouble['id'] != '') {
+ \Froxlor\UI\Response::standard_error('myipnotdouble', '', true);
+ }
+
+ $ins_stmt = Database::prepare("
+ INSERT INTO `" . TABLE_PANEL_IPSANDPORTS . "`
+ SET
+ `ip` = :ip, `port` = :port, `listen_statement` = :ls,
+ `namevirtualhost_statement` = :nvhs, `vhostcontainer` = :vhc,
+ `vhostcontainer_servername_statement` = :vhcss,
+ `specialsettings` = :ss, `ssl` = :ssl,
+ `ssl_cert_file` = :ssl_cert, `ssl_key_file` = :ssl_key,
+ `ssl_ca_file` = :ssl_ca, `ssl_cert_chainfile` = :ssl_chain,
+ `default_vhostconf_domain` = :dvhd, `docroot` = :docroot,
+ `ssl_specialsettings` = :ssl_ss, `include_specialsettings` = :incss,
+ `ssl_default_vhostconf_domain` = :ssl_dvhd, `include_default_vhostconf_domain` = :incdvhd;
+ ");
+ $ins_data = array(
+ 'ip' => $ip,
+ 'port' => $port,
+ 'ls' => $listen_statement,
+ 'nvhs' => $namevirtualhost_statement,
+ 'vhc' => $vhostcontainer,
+ 'vhcss' => $vhostcontainer_servername_statement,
+ 'ss' => $specialsettings,
+ 'ssl' => $ssl,
+ 'ssl_cert' => $ssl_cert_file,
+ 'ssl_key' => $ssl_key_file,
+ 'ssl_ca' => $ssl_ca_file,
+ 'ssl_chain' => $ssl_cert_chainfile,
+ 'dvhd' => $default_vhostconf_domain,
+ 'docroot' => $docroot,
+ 'ssl_ss' => $ssl_specialsettings,
+ 'incss' => $include_specialsettings,
+ 'ssl_dvhd' => $ssl_default_vhostconf_domain,
+ 'incdvhd' => $include_default_vhostconf_domain
+ );
+ Database::pexecute($ins_stmt, $ins_data);
+ $ins_data['id'] = Database::lastInsertId();
+
+ \Froxlor\System\Cronjob::inserttask('1');
+ // Using nameserver, insert a task which rebuilds the server config
+ \Froxlor\System\Cronjob::inserttask('4');
+
+ if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
+ $ip = '[' . $ip . ']';
+ }
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] added IP/port '" . $ip . ":" . $port . "'");
+ // get ip for return-array
+ $result = $this->apiCall('IpsAndPorts.get', array(
+ 'id' => $ins_data['id']
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * update ip/port entry by given id
+ *
+ * @param int $id
+ * @param string $ip
+ * optional
+ * @param int $port
+ * optional, default 80
+ * @param bool $listen_statement
+ * optional, default 0 (false)
+ * @param bool $namevirtualhost_statement
+ * optional, default 0 (false)
+ * @param bool $vhostcontainer
+ * optional, default 0 (false)
+ * @param string $specialsettings
+ * optional, default empty
+ * @param bool $vhostcontainer_servername_statement
+ * optional, default 0 (false)
+ * @param string $default_vhostconf_domain
+ * optional, defatul empty
+ * @param string $docroot
+ * optional, default empty (point to froxlor)
+ * @param bool $ssl
+ * optional, default 0 (false)
+ * @param string $ssl_cert_file
+ * optional, requires $ssl = 1, default empty
+ * @param string $ssl_key_file
+ * optional, requires $ssl = 1, default empty
+ * @param string $ssl_ca_file
+ * optional, requires $ssl = 1, default empty
+ * @param string $ssl_cert_chainfile
+ * optional, requires $ssl = 1, default empty
+ * @param string $ssl_specialsettings
+ * optional, requires $ssl = 1, default empty
+ * @param bool $include_specialsettings
+ * optional, requires $ssl = 1, whether or not to include non-ssl specialsettings, default false
+ * @param string $ssl_default_vhostconf_domain
+ * optional, requires $ssl = 1, defatul empty
+ * @param bool $include_default_vhostconf_domain
+ * optional, requires $ssl = 1, whether or not to include non-ssl default_vhostconf_domain, default false
+ *
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function update()
+ {
+ if ($this->isAdmin() && ($this->getUserDetail('change_serversettings') || ! empty($this->getUserDetail('ip')))) {
+ $id = $this->getParam('id');
+
+ $result = $this->apiCall('IpsAndPorts.get', array(
+ 'id' => $id
+ ));
+
+ $ip = \Froxlor\Validate\Validate::validate_ip2($this->getParam('ip', true, $result['ip']), false, 'invalidip', false, true, false, false, true);
+ $port = \Froxlor\Validate\Validate::validate($this->getParam('port', true, $result['port']), 'port', \Froxlor\Validate\Validate::REGEX_PORT, array(
+ 'stringisempty',
+ 'myport'
+ ), array(), true);
+ $listen_statement = $this->getBoolParam('listen_statement', true, $result['listen_statement']);
+ $namevirtualhost_statement = $this->getBoolParam('namevirtualhost_statement', true, $result['namevirtualhost_statement']);
+ $vhostcontainer = $this->getBoolParam('vhostcontainer', true, $result['vhostcontainer']);
+ $specialsettings = \Froxlor\Validate\Validate::validate(str_replace("\r\n", "\n", $this->getParam('specialsettings', true, $result['specialsettings'])), 'specialsettings', \Froxlor\Validate\Validate::REGEX_CONF_TEXT, '', array(), true);
+ $vhostcontainer_servername_statement = $this->getParam('vhostcontainer_servername_statement', true, $result['vhostcontainer_servername_statement']);
+ $default_vhostconf_domain = \Froxlor\Validate\Validate::validate(str_replace("\r\n", "\n", $this->getParam('default_vhostconf_domain', true, $result['default_vhostconf_domain'])), 'default_vhostconf_domain', \Froxlor\Validate\Validate::REGEX_CONF_TEXT, '', array(), true);
+ $docroot = \Froxlor\Validate\Validate::validate($this->getParam('docroot', true, $result['docroot']), 'docroot', \Froxlor\Validate\Validate::REGEX_DIR, '', array(), true);
+
+ if ((int) Settings::Get('system.use_ssl') == 1) {
+ $ssl = $this->getBoolParam('ssl', true, $result['ssl']);
+ $ssl_cert_file = \Froxlor\Validate\Validate::validate($this->getParam('ssl_cert_file', $ssl, $result['ssl_cert_file']), 'ssl_cert_file', '', '', array(), true);
+ $ssl_key_file = \Froxlor\Validate\Validate::validate($this->getParam('ssl_key_file', $ssl, $result['ssl_key_file']), 'ssl_key_file', '', '', array(), true);
+ $ssl_ca_file = \Froxlor\Validate\Validate::validate($this->getParam('ssl_ca_file', true, $result['ssl_ca_file']), 'ssl_ca_file', '', '', array(), true);
+ $ssl_cert_chainfile = \Froxlor\Validate\Validate::validate($this->getParam('ssl_cert_chainfile', true, $result['ssl_cert_chainfile']), 'ssl_cert_chainfile', '', '', array(), true);
+ $ssl_specialsettings = \Froxlor\Validate\Validate::validate(str_replace("\r\n", "\n", $this->getParam('ssl_specialsettings', true, $result['ssl_specialsettings'])), 'ssl_specialsettings', \Froxlor\Validate\Validate::REGEX_CONF_TEXT, '', array(), true);
+ $include_specialsettings = $this->getBoolParam('include_specialsettings', true, $result['include_specialsettings']);
+ $ssl_default_vhostconf_domain = \Froxlor\Validate\Validate::validate(str_replace("\r\n", "\n", $this->getParam('ssl_default_vhostconf_domain', true, $result['ssl_default_vhostconf_domain'])), 'ssl_default_vhostconf_domain', \Froxlor\Validate\Validate::REGEX_CONF_TEXT, '', array(), true);
+ $include_default_vhostconf_domain = $this->getBoolParam('include_default_vhostconf_domain', true, $result['include_default_vhostconf_domain']);
+ } else {
+ $ssl = 0;
+ $ssl_cert_file = '';
+ $ssl_key_file = '';
+ $ssl_ca_file = '';
+ $ssl_cert_chainfile = '';
+ $ssl_specialsettings = '';
+ $include_specialsettings = 0;
+ $ssl_default_vhostconf_domain = '';
+ $include_default_vhostconf_domain = 0;
+ }
+
+ $result_checkfordouble_stmt = Database::prepare("
+ SELECT `id` FROM `" . TABLE_PANEL_IPSANDPORTS . "`
+ WHERE `ip` = :ip AND `port` = :port
+ ");
+ $result_checkfordouble = Database::pexecute_first($result_checkfordouble_stmt, array(
+ 'ip' => $ip,
+ 'port' => $port
+ ));
+
+ $result_sameipotherport_stmt = Database::prepare("
+ SELECT `id` FROM `" . TABLE_PANEL_IPSANDPORTS . "`
+ WHERE `ip` = :ip AND `id` <> :id
+ ");
+ $result_sameipotherport = Database::pexecute_first($result_sameipotherport_stmt, array(
+ 'ip' => $ip,
+ 'id' => $id
+ ), true, true);
+
+ if ($listen_statement != '1') {
+ $listen_statement = '0';
+ }
+
+ if ($namevirtualhost_statement != '1') {
+ $namevirtualhost_statement = '0';
+ }
+
+ if ($vhostcontainer != '1') {
+ $vhostcontainer = '0';
+ }
+
+ if ($vhostcontainer_servername_statement != '1') {
+ $vhostcontainer_servername_statement = '0';
+ }
+
+ if ($ssl != '1') {
+ $ssl = '0';
+ }
+
+ if ($ssl_cert_file != '') {
+ $ssl_cert_file = \Froxlor\FileDir::makeCorrectFile($ssl_cert_file);
+ }
+
+ if ($ssl_key_file != '') {
+ $ssl_key_file = \Froxlor\FileDir::makeCorrectFile($ssl_key_file);
+ }
+
+ if ($ssl_ca_file != '') {
+ $ssl_ca_file = \Froxlor\FileDir::makeCorrectFile($ssl_ca_file);
+ }
+
+ if ($ssl_cert_chainfile != '') {
+ $ssl_cert_chainfile = \Froxlor\FileDir::makeCorrectFile($ssl_cert_chainfile);
+ }
+
+ if (strlen(trim($docroot)) > 0) {
+ $docroot = \Froxlor\FileDir::makeCorrectDir($docroot);
+ } else {
+ $docroot = '';
+ }
+
+ if ($result['ip'] != $ip && $result['ip'] == Settings::Get('system.ipaddress') && $result_sameipotherport == false) {
+ \Froxlor\UI\Response::standard_error('cantchangesystemip', '', true);
+ } elseif ($result_checkfordouble && $result_checkfordouble['id'] != '' && $result_checkfordouble['id'] != $id) {
+ \Froxlor\UI\Response::standard_error('myipnotdouble', '', true);
+ } else {
+
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_IPSANDPORTS . "`
+ SET
+ `ip` = :ip, `port` = :port, `listen_statement` = :ls,
+ `namevirtualhost_statement` = :nvhs, `vhostcontainer` = :vhc,
+ `vhostcontainer_servername_statement` = :vhcss,
+ `specialsettings` = :ss, `ssl` = :ssl,
+ `ssl_cert_file` = :ssl_cert, `ssl_key_file` = :ssl_key,
+ `ssl_ca_file` = :ssl_ca, `ssl_cert_chainfile` = :ssl_chain,
+ `default_vhostconf_domain` = :dvhd, `docroot` = :docroot,
+ `ssl_specialsettings` = :ssl_ss, `include_specialsettings` = :incss,
+ `ssl_default_vhostconf_domain` = :ssl_dvhd, `include_default_vhostconf_domain` = :incdvhd
+ WHERE `id` = :id;
+ ");
+ $upd_data = array(
+ 'ip' => $ip,
+ 'port' => $port,
+ 'ls' => $listen_statement,
+ 'nvhs' => $namevirtualhost_statement,
+ 'vhc' => $vhostcontainer,
+ 'vhcss' => $vhostcontainer_servername_statement,
+ 'ss' => $specialsettings,
+ 'ssl' => $ssl,
+ 'ssl_cert' => $ssl_cert_file,
+ 'ssl_key' => $ssl_key_file,
+ 'ssl_ca' => $ssl_ca_file,
+ 'ssl_chain' => $ssl_cert_chainfile,
+ 'dvhd' => $default_vhostconf_domain,
+ 'docroot' => $docroot,
+ 'ssl_ss' => $ssl_specialsettings,
+ 'incss' => $include_specialsettings,
+ 'ssl_dvhd' => $ssl_default_vhostconf_domain,
+ 'incdvhd' => $include_default_vhostconf_domain,
+ 'id' => $id
+ );
+ Database::pexecute($upd_stmt, $upd_data);
+
+ \Froxlor\System\Cronjob::inserttask('1');
+ // Using nameserver, insert a task which rebuilds the server config
+ \Froxlor\System\Cronjob::inserttask('4');
+
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] changed IP/port from '" . $result['ip'] . ":" . $result['port'] . "' to '" . $ip . ":" . $port . "'");
+
+ $result = $this->apiCall('IpsAndPorts.get', array(
+ 'id' => $result['id']
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * delete an ip/port entry by id
+ *
+ * @param int $id
+ * ip-port-id
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function delete()
+ {
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings')) {
+ $id = $this->getParam('id');
+
+ $result = $this->apiCall('IpsAndPorts.get', array(
+ 'id' => $id
+ ));
+
+ $result_checkdomain_stmt = Database::prepare("
+ SELECT `id_domain` FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_ipandports` = :id
+ ");
+ $result_checkdomain = Database::pexecute_first($result_checkdomain_stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ if (empty($result_checkdomain)) {
+ if (! in_array($result['id'], explode(',', Settings::Get('system.defaultip'))) && ! in_array($result['id'], explode(',', Settings::Get('system.defaultsslip')))) {
+
+ // check whether there is the same IP with a different port
+ // in case this ip-address is the system.ipaddress and therefore
+ // when there is one - we have an alternative
+ $result_sameipotherport_stmt = Database::prepare("
+ SELECT `id` FROM `" . TABLE_PANEL_IPSANDPORTS . "`
+ WHERE `ip` = :ip AND `id` <> :id");
+ $result_sameipotherport = Database::pexecute_first($result_sameipotherport_stmt, array(
+ 'id' => $id,
+ 'ip' => $result['ip']
+ ));
+
+ if (($result['ip'] != Settings::Get('system.ipaddress')) || ($result['ip'] == Settings::Get('system.ipaddress') && $result_sameipotherport != false)) {
+
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_PANEL_IPSANDPORTS . "`
+ WHERE `id` = :id
+ ");
+ Database::pexecute($del_stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ // also, remove connections to domains (multi-stack)
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_ipandports` = :id
+ ");
+ Database::pexecute($del_stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ \Froxlor\System\Cronjob::inserttask('1');
+ // Using nameserver, insert a task which rebuilds the server config
+ \Froxlor\System\Cronjob::inserttask('4');
+
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] deleted IP/port '" . $result['ip'] . ":" . $result['port'] . "'");
+ return $this->response(200, "successful", $result);
+ } else {
+ \Froxlor\UI\Response::standard_error('cantdeletesystemip', '', true);
+ }
+ } else {
+ \Froxlor\UI\Response::standard_error('cantdeletedefaultip', '', true);
+ }
+ } else {
+ \Froxlor\UI\Response::standard_error('ipstillhasdomains', '', true);
+ }
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/Mysqls.php b/lib/Froxlor/Api/Commands/Mysqls.php
new file mode 100644
index 00000000..d22cf46a
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/Mysqls.php
@@ -0,0 +1,515 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class Mysqls extends \Froxlor\Api\ApiCommand implements \Froxlor\Api\ResourceEntity
+{
+
+ /**
+ * add a new mysql-database
+ *
+ * @param string $mysql_password
+ * password for the created database and database-user
+ * @param int $mysql_server
+ * optional, default is 0
+ * @param string $description
+ * optional, description for database
+ * @param bool $sendinfomail
+ * optional, send created resource-information to customer, default: false
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function add()
+ {
+ // required paramters
+ $password = $this->getParam('mysql_password');
+
+ // parameters
+ $dbserver = $this->getParam('mysql_server', true, 0);
+ $databasedescription = $this->getParam('description', true, '');
+ $sendinfomail = $this->getBoolParam('sendinfomail', true, 0);
+ // get needed customer info to reduce the mysql-usage-counter by one
+ $customer = $this->getCustomerData('mysqls');
+
+ // validation
+ $password = \Froxlor\Validate\Validate::validate($password, 'password', '', '', array(), true);
+ $password = \Froxlor\System\Crypt::validatePassword($password, true);
+ $databasedescription = \Froxlor\Validate\Validate::validate(trim($databasedescription), 'description', '', '', array(), true);
+
+ // validate whether the dbserver exists
+ $dbserver = \Froxlor\Validate\Validate::validate($dbserver, html_entity_decode($this->lng['mysql']['mysql_server']), '', '', 0, true);
+ Database::needRoot(true, $dbserver);
+ Database::needSqlData();
+ $sql_root = Database::getSqlData();
+ Database::needRoot(false);
+ if (! isset($sql_root) || ! is_array($sql_root)) {
+ throw new \Exception("Database server with index #" . $dbserver . " is unknown", 404);
+ }
+
+ if ($sendinfomail != 1) {
+ $sendinfomail = 0;
+ }
+
+ $newdb_params = array(
+ 'loginname' => ($this->isAdmin() ? $customer['loginname'] : $this->getUserDetail('loginname')),
+ 'mysql_lastaccountnumber' => ($this->isAdmin() ? $customer['mysql_lastaccountnumber'] : $this->getUserDetail('mysql_lastaccountnumber'))
+ );
+ // create database, user, set permissions, etc.pp.
+ $dbm = new \Froxlor\Database\DbManager($this->logger());
+ $username = $dbm->createDatabase($newdb_params['loginname'], $password, $newdb_params['mysql_lastaccountnumber']);
+
+ // we've checked against the password in dbm->createDatabase
+ if ($username == false) {
+ \Froxlor\UI\Response::standard_error('passwordshouldnotbeusername', '', true);
+ }
+
+ // add database info to froxlor
+ $stmt = Database::prepare("
+ INSERT INTO `" . TABLE_PANEL_DATABASES . "`
+ SET
+ `customerid` = :customerid,
+ `databasename` = :databasename,
+ `description` = :description,
+ `dbserver` = :dbserver
+ ");
+ $params = array(
+ "customerid" => $customer['customerid'],
+ "databasename" => $username,
+ "description" => $databasedescription,
+ "dbserver" => $dbserver
+ );
+ Database::pexecute($stmt, $params, true, true);
+ $databaseid = Database::lastInsertId();
+ $params['id'] = $databaseid;
+
+ // update customer usage
+ Customers::increaseUsage($customer['customerid'], 'mysqls_used');
+ Customers::increaseUsage($customer['customerid'], 'mysql_lastaccountnumber');
+
+ // send info-mail?
+ if ($sendinfomail == 1) {
+ $pma = $this->lng['admin']['notgiven'];
+ if (Settings::Get('panel.phpmyadmin_url') != '') {
+ $pma = Settings::Get('panel.phpmyadmin_url');
+ }
+
+ Database::needRoot(true, $dbserver);
+ Database::needSqlData();
+ $sql_root = Database::getSqlData();
+ Database::needRoot(false);
+ $userinfo = $customer;
+
+ $replace_arr = array(
+ 'SALUTATION' => \Froxlor\User::getCorrectUserSalutation($userinfo),
+ 'CUST_NAME' => \Froxlor\User::getCorrectUserSalutation($userinfo), // < keep this for compatibility
+ 'NAME' => $userinfo['name'],
+ 'FIRSTNAME' => $userinfo['firstname'],
+ 'COMPANY' => $userinfo['company'],
+ 'CUSTOMER_NO' => $userinfo['customernumber'],
+ 'DB_NAME' => $username,
+ 'DB_PASS' => $password,
+ 'DB_DESC' => $databasedescription,
+ 'DB_SRV' => $sql_root['host'],
+ 'PMA_URI' => $pma
+ );
+
+ // get template for mail subject
+ $mail_subject = $this->getMailTemplate($userinfo, 'mails', 'new_database_by_customer_subject', $replace_arr, $this->lng['mails']['new_database_by_customer']['subject']);
+ // get template for mail body
+ $mail_body = $this->getMailTemplate($userinfo, 'mails', 'new_database_by_customer_mailbody', $replace_arr, $this->lng['mails']['new_database_by_customer']['mailbody']);
+
+ $_mailerror = false;
+ $mailerr_msg = "";
+ try {
+ $this->mailer()->Subject = $mail_subject;
+ $this->mailer()->AltBody = $mail_body;
+ $this->mailer()->msgHTML(str_replace("\n", " ", $mail_body));
+ $this->mailer()->addAddress($userinfo['email'], \Froxlor\User::getCorrectUserSalutation($userinfo));
+ $this->mailer()->send();
+ } catch (\PHPMailer\PHPMailer\Exception $e) {
+ $mailerr_msg = $e->errorMessage();
+ $_mailerror = true;
+ } catch (\Exception $e) {
+ $mailerr_msg = $e->getMessage();
+ $_mailerror = true;
+ }
+
+ if ($_mailerror) {
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_ERR, "[API] Error sending mail: " . $mailerr_msg);
+ \Froxlor\UI\Response::standard_error('errorsendingmail', $userinfo['email'], true);
+ }
+
+ $this->mailer()->clearAddresses();
+ }
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] added mysql-database '" . $username . "'");
+
+ $result = $this->apiCall('Mysqls.get', array(
+ 'dbname' => $username
+ ));
+ return $this->response(200, "successful", $result);
+ }
+
+ /**
+ * return a mysql database entry by either id or dbname
+ *
+ * @param int $id
+ * optional, the database-id
+ * @param string $dbname
+ * optional, the databasename
+ * @param int $mysql_server
+ * optional, specify database-server, default is none
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function get()
+ {
+ $id = $this->getParam('id', true, 0);
+ $dn_optional = ($id <= 0 ? false : true);
+ $dbname = $this->getParam('dbname', $dn_optional, '');
+ $dbserver = $this->getParam('mysql_server', true, - 1);
+
+ if ($this->isAdmin()) {
+ if ($this->getUserDetail('customers_see_all') != 1) {
+ // if it's a reseller or an admin who cannot see all customers, we need to check
+ // whether the database belongs to one of his customers
+ $_custom_list_result = $this->apiCall('Customers.listing');
+ $custom_list_result = $_custom_list_result['list'];
+ $customer_ids = array();
+ foreach ($custom_list_result as $customer) {
+ $customer_ids[] = $customer['customerid'];
+ }
+ if (count($customer_ids) > 0) {
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_DATABASES . "`
+ WHERE " . ($id > 0 ? "`id` = :iddn" : "`databasename` = :iddn") . ($dbserver >= 0 ? " AND `dbserver` = :dbserver" : "") . " AND `customerid` IN (" . implode(", ", $customer_ids) . ")
+ ");
+ $params = array(
+ 'iddn' => ($id <= 0 ? $dbname : $id)
+ );
+ if ($dbserver >= 0) {
+ $params['dbserver'] = $dbserver;
+ }
+ } else {
+ throw new \Exception("You do not have any customers yet", 406);
+ }
+ } else {
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_DATABASES . "`
+ WHERE " . ($id > 0 ? "`id` = :iddn" : "`databasename` = :iddn") . ($dbserver >= 0 ? " AND `dbserver` = :dbserver" : ""));
+ $params = array(
+ 'iddn' => ($id <= 0 ? $dbname : $id)
+ );
+ if ($dbserver >= 0) {
+ $params['dbserver'] = $dbserver;
+ }
+ }
+ } else {
+ if (Settings::IsInList('panel.customer_hide_options', 'mysql')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_DATABASES . "`
+ WHERE `customerid`= :customerid AND " . ($id > 0 ? "`id` = :iddn" : "`databasename` = :iddn") . ($dbserver >= 0 ? " AND `dbserver` = :dbserver" : ""));
+ $params = array(
+ 'customerid' => $this->getUserDetail('customerid'),
+ 'iddn' => ($id <= 0 ? $dbname : $id)
+ );
+ if ($dbserver >= 0) {
+ $params['dbserver'] = $dbserver;
+ }
+ }
+ $result = Database::pexecute_first($result_stmt, $params, true, true);
+ if ($result) {
+ Database::needRoot(true, $result['dbserver']);
+ $mbdata_stmt = Database::prepare("
+ SELECT SUM(data_length + index_length) as MB FROM information_schema.TABLES
+ WHERE table_schema = :table_schema
+ GROUP BY table_schema
+ ");
+ Database::pexecute($mbdata_stmt, array(
+ "table_schema" => $result['databasename']
+ ), true, true);
+ $mbdata = $mbdata_stmt->fetch(\PDO::FETCH_ASSOC);
+ Database::needRoot(false);
+ $result['size'] = $mbdata['MB'] ?? 0;
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] get database '" . $result['databasename'] . "'");
+ return $this->response(200, "successful", $result);
+ }
+ $key = ($id > 0 ? "id #" . $id : "dbname '" . $dbname . "'");
+ throw new \Exception("MySQL database with " . $key . " could not be found", 404);
+ }
+
+ /**
+ * update a mysql database entry by either id or dbname
+ *
+ * @param int $id
+ * optional, the database-id
+ * @param string $dbname
+ * optional, the databasename
+ * @param int $mysql_server
+ * optional, specify database-server, default is none
+ * @param string $mysql_password
+ * optional, update password for the database
+ * @param string $description
+ * optional, description for database
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function update()
+ {
+ $id = $this->getParam('id', true, 0);
+ $dn_optional = ($id <= 0 ? false : true);
+ $dbname = $this->getParam('dbname', $dn_optional, '');
+ $dbserver = $this->getParam('mysql_server', true, - 1);
+ $customer = $this->getCustomerData();
+
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'mysql')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ $result = $this->apiCall('Mysqls.get', array(
+ 'id' => $id,
+ 'dbname' => $dbname,
+ 'mysql_server' => $dbserver
+ ));
+ $id = $result['id'];
+
+ // paramters
+ $password = $this->getParam('mysql_password', true, '');
+ $databasedescription = $this->getParam('description', true, $result['description']);
+
+ // validation
+ $password = \Froxlor\Validate\Validate::validate($password, 'password', '', '', array(), true);
+ $databasedescription = \Froxlor\Validate\Validate::validate(trim($databasedescription), 'description', '', '', array(), true);
+
+ if ($password != '') {
+ // validate password
+ $password = \Froxlor\System\Crypt::validatePassword($password, true);
+
+ if ($password == $result['databasename']) {
+ \Froxlor\UI\Response::standard_error('passwordshouldnotbeusername', '', true);
+ }
+
+ // Begin root-session
+ Database::needRoot(true, $result['dbserver']);
+ $dbmgr = new \Froxlor\Database\DbManager($this->logger());
+ foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) {
+ $dbmgr->getManager()->grantPrivilegesTo($result['databasename'], $password, $mysql_access_host, false, true);
+ }
+
+ $stmt = Database::prepare("FLUSH PRIVILEGES");
+ Database::pexecute($stmt, null, true, true);
+ Database::needRoot(false);
+ // End root-session
+ }
+ $stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_DATABASES . "`
+ SET `description` = :desc
+ WHERE `customerid` = :customerid
+ AND `id` = :id
+ ");
+ $params = array(
+ "desc" => $databasedescription,
+ "customerid" => $customer['customerid'],
+ "id" => $id
+ );
+ Database::pexecute($stmt, $params, true, true);
+
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] updated mysql-database '" . $result['databasename'] . "'");
+ $result = $this->apiCall('Mysqls.get', array(
+ 'dbname' => $result['databasename']
+ ));
+ return $this->response(200, "successful", $result);
+ }
+
+ /**
+ * list all databases, if called from an admin, list all databases of all customers you are allowed to view, or specify id or loginname for one specific customer
+ *
+ * @param int $mysql_server
+ * optional, specify dbserver to select from, else use all available
+ * @param int $customerid
+ * optional, admin-only, select dbs of a specific customer by id
+ * @param string $loginname
+ * optional, admin-only, select dbs of a specific customer by loginname
+ * @param array $sql_search
+ * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), LIKE is used if left empty and 'value' => searchvalue
+ * @param int $sql_limit
+ * optional specify number of results to be returned
+ * @param int $sql_offset
+ * optional specify offset for resultset
+ * @param array $sql_orderby
+ * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more fields
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listing()
+ {
+ $result = array();
+ $dbserver = $this->getParam('mysql_server', true, - 1);
+ $customer_ids = $this->getAllowedCustomerIds('mysql');
+ $query_fields = array();
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_DATABASES . "`
+ WHERE `customerid`= :customerid AND `dbserver` = :dbserver" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit());
+ if ($dbserver < 0) {
+ // use all dbservers
+ $dbservers_stmt = Database::query("SELECT DISTINCT `dbserver` FROM `" . TABLE_PANEL_DATABASES . "`");
+ $dbservers = $dbservers_stmt->fetchAll(\PDO::FETCH_ASSOC);
+ } else {
+ // use specific dbserver
+ $dbservers = array(
+ array(
+ 'dbserver' => $dbserver
+ )
+ );
+ }
+
+ foreach ($customer_ids as $customer_id) {
+ foreach ($dbservers as $_dbserver) {
+ Database::pexecute($result_stmt, array_merge(array(
+ 'customerid' => $customer_id,
+ 'dbserver' => $_dbserver['dbserver']
+ ), $query_fields), true, true);
+ // Begin root-session
+ Database::needRoot(true, $_dbserver['dbserver']);
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $mbdata_stmt = Database::prepare("
+ SELECT SUM(data_length + index_length) as MB FROM information_schema.TABLES
+ WHERE table_schema = :table_schema
+ GROUP BY table_schema
+ ");
+ Database::pexecute($mbdata_stmt, array(
+ "table_schema" => $row['databasename']
+ ), true, true);
+ $mbdata = $mbdata_stmt->fetch(\PDO::FETCH_ASSOC);
+ $row['size'] = $mbdata['MB'] ?? 0;
+ $result[] = $row;
+ }
+ Database::needRoot(false);
+ }
+ }
+ return $this->response(200, "successful", array(
+ 'count' => count($result),
+ 'list' => $result
+ ));
+ }
+
+ /**
+ * returns the total number of accessable databases
+ *
+ * @param int $customerid
+ * optional, admin-only, select dbs of a specific customer by id
+ * @param string $loginname
+ * optional, admin-only, select dbs of a specific customer by loginname
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function listingCount()
+ {
+ $customer_ids = $this->getAllowedCustomerIds('mysql');
+ $result_stmt = Database::prepare("
+ SELECT COUNT(*) as num_dbs FROM `" . TABLE_PANEL_DATABASES . "`
+ WHERE `customerid` IN (" . implode(", ", $customer_ids) . ")
+ ");
+ $result = Database::pexecute_first($result_stmt, null, true, true);
+ if ($result) {
+ return $this->response(200, "successful", $result['num_dbs']);
+ }
+ }
+
+ /**
+ * delete a mysql database by either id or dbname
+ *
+ * @param int $id
+ * optional, the database-id
+ * @param string $dbname
+ * optional, the databasename
+ * @param int $mysql_server
+ * optional, specify database-server, default is none
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function delete()
+ {
+ $id = $this->getParam('id', true, 0);
+ $dn_optional = ($id <= 0 ? false : true);
+ $dbname = $this->getParam('dbname', $dn_optional, '');
+ $dbserver = $this->getParam('mysql_server', true, - 1);
+ $customer = $this->getCustomerData();
+
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'mysql')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ $result = $this->apiCall('Mysqls.get', array(
+ 'id' => $id,
+ 'dbname' => $dbname,
+ 'mysql_server' => $dbserver
+ ));
+ $id = $result['id'];
+
+ // Begin root-session
+ Database::needRoot(true, $result['dbserver']);
+ $dbm = new \Froxlor\Database\DbManager($this->logger());
+ $dbm->getManager()->deleteDatabase($result['databasename']);
+ Database::needRoot(false);
+ // End root-session
+
+ // delete from table
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_DATABASES . "` WHERE `id` = :id");
+ Database::pexecute($stmt, array(
+ "id" => $id
+ ), true, true);
+
+ // get needed customer info to reduce the mysql-usage-counter by one
+ $mysql_used = $customer['mysqls_used'];
+
+ // reduce mysql-usage-counter
+ $resetaccnumber = ($mysql_used == '1') ? " , `mysql_lastaccountnumber` = '0' " : '';
+ Customers::decreaseUsage($customer['customerid'], 'mysqls_used', $resetaccnumber);
+
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] deleted database '" . $result['databasename'] . "'");
+ return $this->response(200, "successful", $result);
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/PhpSettings.php b/lib/Froxlor/Api/Commands/PhpSettings.php
new file mode 100644
index 00000000..002ebbca
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/PhpSettings.php
@@ -0,0 +1,621 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class PhpSettings extends \Froxlor\Api\ApiCommand implements \Froxlor\Api\ResourceEntity
+{
+
+ /**
+ * lists all php-setting entries
+ *
+ * @param bool $with_subdomains
+ * optional, also include subdomains to the list domains that use the config, default 0 (false)
+ * @param array $sql_search
+ * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), LIKE is used if left empty and 'value' => searchvalue
+ * @param int $sql_limit
+ * optional specify number of results to be returned
+ * @param int $sql_offset
+ * optional specify offset for resultset
+ * @param array $sql_orderby
+ * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more fields
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listing()
+ {
+ if ($this->isAdmin()) {
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] list php-configs");
+
+ $with_subdomains = $this->getBoolParam('with_subdomains', true, false);
+ $query_fields = array();
+ $result_stmt = Database::prepare("
+ SELECT c.*, fd.description as fpmdesc
+ FROM `" . TABLE_PANEL_PHPCONFIGS . "` c
+ LEFT JOIN `" . TABLE_PANEL_FPMDAEMONS . "` fd ON fd.id = c.fpmsettingid" . $this->getSearchWhere($query_fields) . $this->getOrderBy() . $this->getLimit());
+ Database::pexecute($result_stmt, $query_fields, true, true);
+ $phpconfigs = array();
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $query_params = array(
+ 'id' => $row['id']
+ );
+
+ $query = "SELECT * FROM `" . TABLE_PANEL_DOMAINS . "`
+ WHERE `phpsettingid` = :id";
+
+ if (! $with_subdomains) {
+ $query .= " AND `parentdomainid` = '0'";
+ }
+
+ if ((int) $this->getUserDetail('domains_see_all') == 0) {
+ $query .= " AND `adminid` = :adminid";
+ $query_params['adminid'] = $this->getUserDetail('adminid');
+ }
+
+ if ((int) Settings::Get('panel.phpconfigs_hidestdsubdomain') == 1) {
+ $ssdids_res = Database::query("
+ SELECT DISTINCT `standardsubdomain` FROM `" . TABLE_PANEL_CUSTOMERS . "`
+ WHERE `standardsubdomain` > 0 ORDER BY `standardsubdomain` ASC;");
+ $ssdids = array();
+ while ($ssd = $ssdids_res->fetch(\PDO::FETCH_ASSOC)) {
+ $ssdids[] = $ssd['standardsubdomain'];
+ }
+ if (count($ssdids) > 0) {
+ $query .= " AND `id` NOT IN (" . implode(', ', $ssdids) . ")";
+ }
+ }
+
+ $domains = array();
+ $subdomains = array();
+ $domainresult_stmt = Database::prepare($query);
+ Database::pexecute($domainresult_stmt, $query_params, true, true);
+
+ if (Database::num_rows() > 0) {
+ while ($row2 = $domainresult_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if ($row2['parentdomainid'] != 0) {
+ $subdomains[] = $row2['domain'];
+ } else {
+ $domains[] = $row2['domain'];
+ }
+ }
+ }
+
+ // check whether we use that config as froxor-vhost config
+ if (Settings::Get('system.mod_fcgid_defaultini_ownvhost') == $row['id'] || Settings::Get('phpfpm.vhost_defaultini') == $row['id']) {
+ $domains[] = Settings::Get('system.hostname');
+ }
+
+ // check whether this is our default config
+ if ((Settings::Get('system.mod_fcgid') == '1' && Settings::Get('system.mod_fcgid_defaultini') == $row['id']) || (Settings::Get('phpfpm.enabled') == '1' && Settings::Get('phpfpm.defaultini') == $row['id'])) {
+ $row['is_default'] = true;
+ }
+
+ $row['domains'] = $domains;
+ $row['subdomains'] = $subdomains;
+ $phpconfigs[] = $row;
+ }
+
+ return $this->response(200, "successful", array(
+ 'count' => count($phpconfigs),
+ 'list' => $phpconfigs
+ ));
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * returns the total number of accessable php-setting entries
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function listingCount()
+ {
+ if ($this->isAdmin()) {
+ $result_stmt = Database::prepare("
+ SELECT COUNT(*) as num_phps
+ FROM `" . TABLE_PANEL_PHPCONFIGS . "` c
+ ");
+ $result = Database::pexecute_first($result_stmt, null, true, true);
+ if ($result) {
+ return $this->response(200, "successful", $result['num_phps']);
+ }
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * return a php-setting entry by id
+ *
+ * @param int $id
+ * php-settings-id
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function get()
+ {
+ if ($this->isAdmin()) {
+ $id = $this->getParam('id');
+
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_PHPCONFIGS . "` WHERE `id` = :id
+ ");
+ $result = Database::pexecute_first($result_stmt, array(
+ 'id' => $id
+ ), true, true);
+ if ($result) {
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("php-config with id #" . $id . " could not be found", 404);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * add new php-settings entry
+ *
+ * @param string $description
+ * description of the php-config
+ * @param string $phpsettings
+ * the actual ini-settings
+ * @param string $binary
+ * optional the binary to php-cgi if FCGID is used
+ * @param string $file_extensions
+ * optional allowed php-file-extensions if FCGID is used, default is 'php'
+ * @param int $mod_fcgid_starter
+ * optional number of fcgid-starters if FCGID is used, default is -1
+ * @param int $mod_fcgid_maxrequests
+ * optional number of fcgid-maxrequests if FCGID is used, default is -1
+ * @param string $mod_fcgid_umask
+ * optional umask if FCGID is used, default is '022'
+ * @param int $fpmconfig
+ * optional id of the fpm-daemon-config if FPM is used
+ * @param bool $phpfpm_enable_slowlog
+ * optional whether to write a slowlog or not if FPM is used, default is 0 (false)
+ * @param string $phpfpm_reqtermtimeout
+ * optional request terminate timeout if FPM is used, default is '60s'
+ * @param string $phpfpm_reqslowtimeout
+ * optional request slowlog timeout if FPM is used, default is '5s'
+ * @param bool $phpfpm_pass_authorizationheader
+ * optional whether to pass authorization header to webserver if FPM is used, default is 0 (false)
+ * @param bool $override_fpmconfig
+ * optional whether to override fpm-daemon-config value for the following settings if FPM is used, default is 0 (false)
+ * @param string $pm
+ * optional process-manager to use if FPM is used (allowed values are 'static', 'dynamic' and 'ondemand'), default is fpm-daemon-value
+ * @param int $max_children
+ * optional number of max children if FPM is used, default is the fpm-daemon-value
+ * @param int $start_server
+ * optional number of servers to start if FPM is used, default is fpm-daemon-value
+ * @param int $min_spare_servers
+ * optional number of minimum spare servers if FPM is used, default is fpm-daemon-value
+ * @param int $max_spare_servers
+ * optional number of maximum spare servers if FPM is used, default is fpm-daemon-value
+ * @param int $max_requests
+ * optional number of maximum requests if FPM is used, default is fpm-daemon-value
+ * @param int $idle_timeout
+ * optional number of seconds for idle-timeout if FPM is used, default is fpm-daemon-value
+ * @param string $limit_extensions
+ * optional limitation of php-file-extensions if FPM is used, default is fpm-daemon-value
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function add()
+ {
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) {
+
+ // required parameter
+ $description = $this->getParam('description');
+ $phpsettings = $this->getParam('phpsettings');
+
+ if (Settings::Get('system.mod_fcgid') == 1) {
+ $binary = $this->getParam('binary');
+ $fpm_config_id = 1;
+ } elseif (Settings::Get('phpfpm.enabled') == 1) {
+ $fpm_config_id = intval($this->getParam('fpmconfig'));
+ } else {
+ $fpm_config_id = 1;
+ }
+
+ // parameters
+ $file_extensions = $this->getParam('file_extensions', true, 'php');
+ $mod_fcgid_starter = $this->getParam('mod_fcgid_starter', true, - 1);
+ $mod_fcgid_maxrequests = $this->getParam('mod_fcgid_maxrequests', true, - 1);
+ $mod_fcgid_umask = $this->getParam('mod_fcgid_umask', true, "022");
+ $fpm_enableslowlog = $this->getBoolParam('phpfpm_enable_slowlog', true, 0);
+ $fpm_reqtermtimeout = $this->getParam('phpfpm_reqtermtimeout', true, "60s");
+ $fpm_reqslowtimeout = $this->getParam('phpfpm_reqslowtimeout', true, "5s");
+ $fpm_pass_authorizationheader = $this->getBoolParam('phpfpm_pass_authorizationheader', true, 0);
+
+ $override_fpmconfig = $this->getBoolParam('override_fpmconfig', true, 0);
+ $def_fpmconfig = $this->apiCall('FpmDaemons.get', array(
+ 'id' => $fpm_config_id
+ ));
+ $pmanager = $this->getParam('pm', true, $def_fpmconfig['pm']);
+ $max_children = $this->getParam('max_children', true, $def_fpmconfig['max_children']);
+ $start_servers = $this->getParam('start_servers', true, $def_fpmconfig['start_servers']);
+ $min_spare_servers = $this->getParam('min_spare_servers', true, $def_fpmconfig['min_spare_servers']);
+ $max_spare_servers = $this->getParam('max_spare_servers', true, $def_fpmconfig['max_spare_servers']);
+ $max_requests = $this->getParam('max_requests', true, $def_fpmconfig['max_requests']);
+ $idle_timeout = $this->getParam('idle_timeout', true, $def_fpmconfig['idle_timeout']);
+ $limit_extensions = $this->getParam('limit_extensions', true, $def_fpmconfig['limit_extensions']);
+
+ // validation
+ $description = \Froxlor\Validate\Validate::validate($description, 'description', '', '', array(), true);
+ $phpsettings = \Froxlor\Validate\Validate::validate(str_replace("\r\n", "\n", $phpsettings), 'phpsettings', '/^[^\0]*$/', '', array(), true);
+ if (Settings::Get('system.mod_fcgid') == 1) {
+ $binary = \Froxlor\FileDir::makeCorrectFile(\Froxlor\Validate\Validate::validate($binary, 'binary', '', '', array(), true));
+ $file_extensions = \Froxlor\Validate\Validate::validate($file_extensions, 'file_extensions', '/^[a-zA-Z0-9\s]*$/', '', array(), true);
+ $mod_fcgid_starter = \Froxlor\Validate\Validate::validate($mod_fcgid_starter, 'mod_fcgid_starter', '/^[0-9]*$/', '', array(
+ '-1',
+ ''
+ ), true);
+ $mod_fcgid_maxrequests = \Froxlor\Validate\Validate::validate($mod_fcgid_maxrequests, 'mod_fcgid_maxrequests', '/^[0-9]*$/', '', array(
+ '-1',
+ ''
+ ), true);
+ $mod_fcgid_umask = \Froxlor\Validate\Validate::validate($mod_fcgid_umask, 'mod_fcgid_umask', '/^[0-9]*$/', '', array(), true);
+ // disable fpm stuff
+ $fpm_config_id = 1;
+ $fpm_enableslowlog = 0;
+ $fpm_reqtermtimeout = 0;
+ $fpm_reqslowtimeout = 0;
+ $fpm_pass_authorizationheader = 0;
+ $override_fpmconfig = 0;
+ } elseif (Settings::Get('phpfpm.enabled') == 1) {
+ $fpm_reqtermtimeout = \Froxlor\Validate\Validate::validate($fpm_reqtermtimeout, 'phpfpm_reqtermtimeout', '/^([0-9]+)(|s|m|h|d)$/', '', array(), true);
+ $fpm_reqslowtimeout = \Froxlor\Validate\Validate::validate($fpm_reqslowtimeout, 'phpfpm_reqslowtimeout', '/^([0-9]+)(|s|m|h|d)$/', '', array(), true);
+ if (! in_array($pmanager, array(
+ 'static',
+ 'dynamic',
+ 'ondemand'
+ ))) {
+ throw new \Exception("Unknown process manager", 406);
+ }
+ if (empty($limit_extensions)) {
+ $limit_extensions = '.php';
+ }
+ $limit_extensions = \Froxlor\Validate\Validate::validate($limit_extensions, 'limit_extensions', '/^(\.[a-z]([a-z0-9]+)\ ?)+$/', '', array(), true);
+
+ // disable fcgid stuff
+ $binary = '/usr/bin/php-cgi';
+ $file_extensions = 'php';
+ $mod_fcgid_starter = 0;
+ $mod_fcgid_maxrequests = 0;
+ $mod_fcgid_umask = "022";
+ }
+
+ if (strlen($description) == 0 || strlen($description) > 50) {
+ \Froxlor\UI\Response::standard_error('descriptioninvalid', '', true);
+ }
+
+ $ins_stmt = Database::prepare("
+ INSERT INTO `" . TABLE_PANEL_PHPCONFIGS . "` SET
+ `description` = :desc,
+ `binary` = :binary,
+ `file_extensions` = :fext,
+ `mod_fcgid_starter` = :starter,
+ `mod_fcgid_maxrequests` = :mreq,
+ `mod_fcgid_umask` = :umask,
+ `fpm_slowlog` = :fpmslow,
+ `fpm_reqterm` = :fpmreqterm,
+ `fpm_reqslow` = :fpmreqslow,
+ `phpsettings` = :phpsettings,
+ `fpmsettingid` = :fpmsettingid,
+ `pass_authorizationheader` = :fpmpassauth,
+ `override_fpmconfig` = :ofc,
+ `pm` = :pm,
+ `max_children` = :max_children,
+ `start_servers` = :start_servers,
+ `min_spare_servers` = :min_spare_servers,
+ `max_spare_servers` = :max_spare_servers,
+ `max_requests` = :max_requests,
+ `idle_timeout` = :idle_timeout,
+ `limit_extensions` = :limit_extensions
+ ");
+ $ins_data = array(
+ 'desc' => $description,
+ 'binary' => $binary,
+ 'fext' => $file_extensions,
+ 'starter' => $mod_fcgid_starter,
+ 'mreq' => $mod_fcgid_maxrequests,
+ 'umask' => $mod_fcgid_umask,
+ 'fpmslow' => $fpm_enableslowlog,
+ 'fpmreqterm' => $fpm_reqtermtimeout,
+ 'fpmreqslow' => $fpm_reqslowtimeout,
+ 'phpsettings' => $phpsettings,
+ 'fpmsettingid' => $fpm_config_id,
+ 'fpmpassauth' => $fpm_pass_authorizationheader,
+ 'ofc' => $override_fpmconfig,
+ 'pm' => $pmanager,
+ 'max_children' => $max_children,
+ 'start_servers' => $start_servers,
+ 'min_spare_servers' => $min_spare_servers,
+ 'max_spare_servers' => $max_spare_servers,
+ 'max_requests' => $max_requests,
+ 'idle_timeout' => $idle_timeout,
+ 'limit_extensions' => $limit_extensions
+ );
+ Database::pexecute($ins_stmt, $ins_data, true, true);
+ $ins_data['id'] = Database::lastInsertId();
+
+ \Froxlor\System\Cronjob::inserttask('1');
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] php setting with description '" . $description . "' has been created by '" . $this->getUserDetail('loginname') . "'");
+
+ $result = $this->apiCall('PhpSettings.get', array(
+ 'id' => $ins_data['id']
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * update a php-setting entry by given id
+ *
+ * @param int $id
+ * @param string $description
+ * description of the php-config
+ * @param string $phpsettings
+ * the actual ini-settings
+ * @param string $binary
+ * optional the binary to php-cgi if FCGID is used
+ * @param string $file_extensions
+ * optional allowed php-file-extensions if FCGID is used, default is 'php'
+ * @param int $mod_fcgid_starter
+ * optional number of fcgid-starters if FCGID is used, default is -1
+ * @param int $mod_fcgid_maxrequests
+ * optional number of fcgid-maxrequests if FCGID is used, default is -1
+ * @param string $mod_fcgid_umask
+ * optional umask if FCGID is used, default is '022'
+ * @param int $fpmconfig
+ * optional id of the fpm-daemon-config if FPM is used
+ * @param bool $phpfpm_enable_slowlog
+ * optional whether to write a slowlog or not if FPM is used, default is 0 (false)
+ * @param string $phpfpm_reqtermtimeout
+ * optional request terminate timeout if FPM is used, default is '60s'
+ * @param string $phpfpm_reqslowtimeout
+ * optional request slowlog timeout if FPM is used, default is '5s'
+ * @param bool $phpfpm_pass_authorizationheader
+ * optional whether to pass authorization header to webserver if FPM is used, default is 0 (false)
+ * @param bool $override_fpmconfig
+ * optional whether to override fpm-daemon-config value for the following settings if FPM is used, default is 0 (false)
+ * @param string $pm
+ * optional process-manager to use if FPM is used (allowed values are 'static', 'dynamic' and 'ondemand'), default is fpm-daemon-value
+ * @param int $max_children
+ * optional number of max children if FPM is used, default is the fpm-daemon-value
+ * @param int $start_server
+ * optional number of servers to start if FPM is used, default is fpm-daemon-value
+ * @param int $min_spare_servers
+ * optional number of minimum spare servers if FPM is used, default is fpm-daemon-value
+ * @param int $max_spare_servers
+ * optional number of maximum spare servers if FPM is used, default is fpm-daemon-value
+ * @param int $max_requests
+ * optional number of maximum requests if FPM is used, default is fpm-daemon-value
+ * @param int $idle_timeout
+ * optional number of seconds for idle-timeout if FPM is used, default is fpm-daemon-value
+ * @param string $limit_extensions
+ * optional limitation of php-file-extensions if FPM is used, default is fpm-daemon-value
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function update()
+ {
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) {
+
+ // required parameter
+ $id = $this->getParam('id');
+
+ $result = $this->apiCall('PhpSettings.get', array(
+ 'id' => $id
+ ));
+
+ // parameters
+ $description = $this->getParam('description', true, $result['description']);
+ $phpsettings = $this->getParam('phpsettings', true, $result['phpsettings']);
+ $binary = $this->getParam('binary', true, $result['binary']);
+ $fpm_config_id = intval($this->getParam('fpmconfig', true, $result['fpmsettingid']));
+ $file_extensions = $this->getParam('file_extensions', true, $result['file_extensions']);
+ $mod_fcgid_starter = $this->getParam('mod_fcgid_starter', true, $result['mod_fcgid_starter']);
+ $mod_fcgid_maxrequests = $this->getParam('mod_fcgid_maxrequests', true, $result['mod_fcgid_maxrequests']);
+ $mod_fcgid_umask = $this->getParam('mod_fcgid_umask', true, $result['mod_fcgid_umask']);
+ $fpm_enableslowlog = $this->getBoolParam('phpfpm_enable_slowlog', true, $result['fpm_slowlog']);
+ $fpm_reqtermtimeout = $this->getParam('phpfpm_reqtermtimeout', true, $result['fpm_reqterm']);
+ $fpm_reqslowtimeout = $this->getParam('phpfpm_reqslowtimeout', true, $result['fpm_reqslow']);
+ $fpm_pass_authorizationheader = $this->getBoolParam('phpfpm_pass_authorizationheader', true, $result['pass_authorizationheader']);
+ $override_fpmconfig = $this->getBoolParam('override_fpmconfig', true, $result['override_fpmconfig']);
+ $pmanager = $this->getParam('pm', true, $result['pm']);
+ $max_children = $this->getParam('max_children', true, $result['max_children']);
+ $start_servers = $this->getParam('start_servers', true, $result['start_servers']);
+ $min_spare_servers = $this->getParam('min_spare_servers', true, $result['min_spare_servers']);
+ $max_spare_servers = $this->getParam('max_spare_servers', true, $result['max_spare_servers']);
+ $max_requests = $this->getParam('max_requests', true, $result['max_requests']);
+ $idle_timeout = $this->getParam('idle_timeout', true, $result['idle_timeout']);
+ $limit_extensions = $this->getParam('limit_extensions', true, $result['limit_extensions']);
+
+ // validation
+ $description = \Froxlor\Validate\Validate::validate($description, 'description', '', '', array(), true);
+ $phpsettings = \Froxlor\Validate\Validate::validate(str_replace("\r\n", "\n", $phpsettings), 'phpsettings', '/^[^\0]*$/', '', array(), true);
+ if (Settings::Get('system.mod_fcgid') == 1) {
+ $binary = \Froxlor\FileDir::makeCorrectFile(\Froxlor\Validate\Validate::validate($binary, 'binary', '', '', array(), true));
+ $file_extensions = \Froxlor\Validate\Validate::validate($file_extensions, 'file_extensions', '/^[a-zA-Z0-9\s]*$/', '', array(), true);
+ $mod_fcgid_starter = \Froxlor\Validate\Validate::validate($mod_fcgid_starter, 'mod_fcgid_starter', '/^[0-9]*$/', '', array(
+ '-1',
+ ''
+ ), true);
+ $mod_fcgid_maxrequests = \Froxlor\Validate\Validate::validate($mod_fcgid_maxrequests, 'mod_fcgid_maxrequests', '/^[0-9]*$/', '', array(
+ '-1',
+ ''
+ ), true);
+ $mod_fcgid_umask = \Froxlor\Validate\Validate::validate($mod_fcgid_umask, 'mod_fcgid_umask', '/^[0-9]*$/', '', array(), true);
+ // disable fpm stuff
+ $fpm_config_id = 1;
+ $fpm_enableslowlog = 0;
+ $fpm_reqtermtimeout = 0;
+ $fpm_reqslowtimeout = 0;
+ $fpm_pass_authorizationheader = 0;
+ $override_fpmconfig = 0;
+ } elseif (Settings::Get('phpfpm.enabled') == 1) {
+ $fpm_reqtermtimeout = \Froxlor\Validate\Validate::validate($fpm_reqtermtimeout, 'phpfpm_reqtermtimeout', '/^([0-9]+)(|s|m|h|d)$/', '', array(), true);
+ $fpm_reqslowtimeout = \Froxlor\Validate\Validate::validate($fpm_reqslowtimeout, 'phpfpm_reqslowtimeout', '/^([0-9]+)(|s|m|h|d)$/', '', array(), true);
+ if (! in_array($pmanager, array(
+ 'static',
+ 'dynamic',
+ 'ondemand'
+ ))) {
+ throw new \Exception("Unknown process manager", 406);
+ }
+ if (empty($limit_extensions)) {
+ $limit_extensions = '.php';
+ }
+ $limit_extensions = \Froxlor\Validate\Validate::validate($limit_extensions, 'limit_extensions', '/^(\.[a-z]([a-z0-9]+)\ ?)+$/', '', array(), true);
+
+ // disable fcgid stuff
+ $binary = '/usr/bin/php-cgi';
+ $file_extensions = 'php';
+ $mod_fcgid_starter = 0;
+ $mod_fcgid_maxrequests = 0;
+ $mod_fcgid_umask = "022";
+ }
+
+ if (strlen($description) == 0 || strlen($description) > 50) {
+ \Froxlor\UI\Response::standard_error('descriptioninvalid', '', true);
+ }
+
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_PHPCONFIGS . "` SET
+ `description` = :desc,
+ `binary` = :binary,
+ `file_extensions` = :fext,
+ `mod_fcgid_starter` = :starter,
+ `mod_fcgid_maxrequests` = :mreq,
+ `mod_fcgid_umask` = :umask,
+ `fpm_slowlog` = :fpmslow,
+ `fpm_reqterm` = :fpmreqterm,
+ `fpm_reqslow` = :fpmreqslow,
+ `phpsettings` = :phpsettings,
+ `fpmsettingid` = :fpmsettingid,
+ `pass_authorizationheader` = :fpmpassauth,
+ `override_fpmconfig` = :ofc,
+ `pm` = :pm,
+ `max_children` = :max_children,
+ `start_servers` = :start_servers,
+ `min_spare_servers` = :min_spare_servers,
+ `max_spare_servers` = :max_spare_servers,
+ `max_requests` = :max_requests,
+ `idle_timeout` = :idle_timeout,
+ `limit_extensions` = :limit_extensions
+ WHERE `id` = :id
+ ");
+ $upd_data = array(
+ 'desc' => $description,
+ 'binary' => $binary,
+ 'fext' => $file_extensions,
+ 'starter' => $mod_fcgid_starter,
+ 'mreq' => $mod_fcgid_maxrequests,
+ 'umask' => $mod_fcgid_umask,
+ 'fpmslow' => $fpm_enableslowlog,
+ 'fpmreqterm' => $fpm_reqtermtimeout,
+ 'fpmreqslow' => $fpm_reqslowtimeout,
+ 'phpsettings' => $phpsettings,
+ 'fpmsettingid' => $fpm_config_id,
+ 'fpmpassauth' => $fpm_pass_authorizationheader,
+ 'ofc' => $override_fpmconfig,
+ 'pm' => $pmanager,
+ 'max_children' => $max_children,
+ 'start_servers' => $start_servers,
+ 'min_spare_servers' => $min_spare_servers,
+ 'max_spare_servers' => $max_spare_servers,
+ 'max_requests' => $max_requests,
+ 'idle_timeout' => $idle_timeout,
+ 'limit_extensions' => $limit_extensions,
+ 'id' => $id
+ );
+ Database::pexecute($upd_stmt, $upd_data, true, true);
+
+ \Froxlor\System\Cronjob::inserttask('1');
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] php setting with description '" . $description . "' has been updated by '" . $this->getUserDetail('loginname') . "'");
+
+ $result = $this->apiCall('PhpSettings.get', array(
+ 'id' => $id
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+
+ /**
+ * delete a php-setting entry by id
+ *
+ * @param int $id
+ * php-settings-id
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function delete()
+ {
+ if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) {
+ $id = $this->getParam('id');
+
+ $result = $this->apiCall('PhpSettings.get', array(
+ 'id' => $id
+ ));
+
+ if ((Settings::Get('system.mod_fcgid') == '1' && Settings::Get('system.mod_fcgid_defaultini_ownvhost') == $id) || (Settings::Get('phpfpm.enabled') == '1' && Settings::Get('phpfpm.vhost_defaultini') == $id)) {
+ \Froxlor\UI\Response::standard_error('cannotdeletehostnamephpconfig', '', true);
+ }
+
+ if ((Settings::Get('system.mod_fcgid') == '1' && Settings::Get('system.mod_fcgid_defaultini') == $id) || (Settings::Get('phpfpm.enabled') == '1' && Settings::Get('phpfpm.defaultini') == $id)) {
+ \Froxlor\UI\Response::standard_error('cannotdeletedefaultphpconfig', '', true);
+ }
+
+ // set php-config to default for all domains using the
+ // config that is to be deleted
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_DOMAINS . "` SET
+ `phpsettingid` = '1' WHERE `phpsettingid` = :id
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_PANEL_PHPCONFIGS . "` WHERE `id` = :id
+ ");
+ Database::pexecute($del_stmt, array(
+ 'id' => $id
+ ), true, true);
+
+ \Froxlor\System\Cronjob::inserttask('1');
+ $this->logger()->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] php setting '" . $result['description'] . "' has been deleted by '" . $this->getUserDetail('loginname') . "'");
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/SubDomains.php b/lib/Froxlor/Api/Commands/SubDomains.php
new file mode 100644
index 00000000..3c09c6f8
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/SubDomains.php
@@ -0,0 +1,1033 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class SubDomains extends \Froxlor\Api\ApiCommand implements \Froxlor\Api\ResourceEntity
+{
+
+ /**
+ * add a new subdomain
+ *
+ * @param string $subdomain
+ * part before domain.tld to create as subdomain
+ * @param string $domain
+ * domainname of main-domain
+ * @param int $alias
+ * optional, domain-id of a domain that the new domain should be an alias of
+ * @param string $path
+ * optional, destination path relative to the customers-homedir, default is customers-homedir
+ * @param string $url
+ * optional, overwrites path value with an URL to generate a redirect, alternatively use the path parameter also for URLs
+ * @param int $openbasedir_path
+ * optional, either 0 for domains-docroot or 1 for customers-homedir
+ * @param int $phpsettingid
+ * optional, php-settings-id, if empty the $domain value is used
+ * @param int $redirectcode
+ * optional, redirect-code-id from TABLE_PANEL_REDIRECTCODES
+ * @param bool $sslenabled
+ * optional, whether or not SSL is enabled for this domain, regardless of the assigned ssl-ips, default 1 (true)
+ * @param bool $ssl_redirect
+ * optional, whether to generate a https-redirect or not, default false; requires SSL to be enabled
+ * @param bool $letsencrypt
+ * optional, whether to generate a Let's Encrypt certificate for this domain, default false; requires SSL to be enabled
+ * @param bool $http2
+ * optional, whether to enable http/2 for this subdomain (requires to be enabled in the settings), default 0 (false)
+ * @param int $hsts_maxage
+ * optional max-age value for HSTS header, default 0
+ * @param bool $hsts_sub
+ * optional whether or not to add subdomains to the HSTS header, default 0
+ * @param bool $hsts_preload
+ * optional whether or not to preload HSTS header value, default 0
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function add()
+ {
+ if ($this->getUserDetail('subdomains_used') < $this->getUserDetail('subdomains') || $this->getUserDetail('subdomains') == '-1') {
+ // parameters
+ $subdomain = $this->getParam('subdomain');
+ $domain = $this->getParam('domain');
+
+ // optional parameters
+ $aliasdomain = $this->getParam('alias', true, 0);
+ $path = $this->getParam('path', true, '');
+ $url = $this->getParam('url', true, '');
+ $openbasedir_path = $this->getParam('openbasedir_path', true, 1);
+ $phpsettingid = $this->getParam('phpsettingid', true, 0);
+ $redirectcode = $this->getParam('redirectcode', true, Settings::Get('customredirect.default'));
+ $isemaildomain = $this->getParam('isemaildomain', true, 0);
+ if (Settings::Get('system.use_ssl')) {
+ $sslenabled = $this->getBoolParam('sslenabled', true, 1);
+ $ssl_redirect = $this->getBoolParam('ssl_redirect', true, 0);
+ $letsencrypt = $this->getBoolParam('letsencrypt', true, 0);
+ $http2 = $this->getBoolParam('http2', true, 0);
+ $hsts_maxage = $this->getParam('hsts_maxage', true, 0);
+ $hsts_sub = $this->getBoolParam('hsts_sub', true, 0);
+ $hsts_preload = $this->getBoolParam('hsts_preload', true, 0);
+ } else {
+ $sslenabled = 0;
+ $ssl_redirect = 0;
+ $letsencrypt = 0;
+ $http2 = 0;
+ $hsts_maxage = 0;
+ $hsts_sub = 0;
+ $hsts_preload = 0;
+ }
+
+ // get needed customer info to reduce the subdomain-usage-counter by one
+ $customer = $this->getCustomerData('subdomains');
+
+ // validation
+ $subdomain = strtolower($subdomain);
+ if (substr($subdomain, 0, 4) == 'xn--') {
+ \Froxlor\UI\Response::standard_error('domain_nopunycode', '', true);
+ }
+
+ $idna_convert = new \Froxlor\Idna\IdnaWrapper();
+ $subdomain = $idna_convert->encode(preg_replace(array(
+ '/\:(\d)+$/',
+ '/^https?\:\/\//'
+ ), '', \Froxlor\Validate\Validate::validate($subdomain, 'subdomain', '', 'subdomainiswrong', array(), true)));
+
+ // merge the two parts together
+ $completedomain = $subdomain . '.' . $domain;
+
+ if (Settings::Get('system.validate_domain') && ! \Froxlor\Validate\Validate::validateDomain($completedomain)) {
+ \Froxlor\UI\Response::standard_error(array(
+ 'stringiswrong',
+ 'mydomain'
+ ), '', true);
+ }
+ if ($completedomain == strtolower(Settings::Get('system.hostname'))) {
+ \Froxlor\UI\Response::standard_error('admin_domain_emailsystemhostname', '', true);
+ }
+
+ // check whether the domain already exists
+ $completedomain_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_DOMAINS . "`
+ WHERE `domain` = :domain
+ AND `customerid` = :customerid
+ AND `email_only` = '0'
+ AND `caneditdomain` = '1'
+ ");
+ $completedomain_check = Database::pexecute_first($completedomain_stmt, array(
+ "domain" => $completedomain,
+ "customerid" => $customer['customerid']
+ ), true, true);
+
+ if ($completedomain_check) {
+ // no exception so far - domain exists
+ \Froxlor\UI\Response::standard_error('domainexistalready', $completedomain, true);
+ }
+
+ // alias domain checked?
+ if ($aliasdomain != 0) {
+ // also check ip/port combination to be the same, #176
+ $aliasdomain_stmt = Database::prepare("
+ SELECT `d`.`id` FROM `" . TABLE_PANEL_DOMAINS . "` `d` , `" . TABLE_PANEL_CUSTOMERS . "` `c` , `" . TABLE_DOMAINTOIP . "` `dip`
+ WHERE `d`.`aliasdomain` IS NULL
+ AND `d`.`id` = :id
+ AND `c`.`standardsubdomain` <> `d`.`id`
+ AND `d`.`customerid` = :customerid
+ AND `c`.`customerid` = `d`.`customerid`
+ AND `d`.`id` = `dip`.`id_domain`
+ AND `dip`.`id_ipandports`
+ IN (SELECT `id_ipandports` FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :id )
+ GROUP BY `d`.`domain`
+ ORDER BY `d`.`domain` ASC
+ ");
+ $aliasdomain_check = Database::pexecute_first($aliasdomain_stmt, array(
+ "id" => $aliasdomain,
+ "customerid" => $customer['customerid']
+ ), true, true);
+ if ($aliasdomain_check['id'] != $aliasdomain) {
+ \Froxlor\UI\Response::standard_error('domainisaliasorothercustomer', '', true);
+ }
+ \Froxlor\Domain\Domain::triggerLetsEncryptCSRForAliasDestinationDomain($aliasdomain, $this->logger());
+ }
+
+ // validate / correct path/url of domain
+ $_doredirect = false;
+ $path = $this->validateDomainDocumentRoot($path, $url, $customer, $completedomain, $_doredirect);
+
+ if ($openbasedir_path != 1) {
+ $openbasedir_path = 0;
+ }
+
+ // get main domain for various checks
+ $domain_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_DOMAINS . "`
+ WHERE `domain` = :domain
+ AND `customerid` = :customerid
+ AND `parentdomainid` = '0'
+ AND `email_only` = '0'
+ AND `caneditdomain` = '1'
+ ");
+ $domain_check = Database::pexecute_first($domain_stmt, array(
+ "domain" => $domain,
+ "customerid" => $customer['customerid']
+ ), true, true);
+
+ if (! $domain_check) {
+ // the given main-domain
+ \Froxlor\UI\Response::standard_error('maindomainnonexist', $domain, true);
+ } elseif ($subdomain == 'www' && $domain_check['wwwserveralias'] == '1') {
+ // you cannot add 'www' as subdomain when the maindomain generates a www-alias
+ \Froxlor\UI\Response::standard_error('wwwnotallowed', '', true);
+ } elseif ($completedomain_check && strtolower($completedomain_check['domain']) == strtolower($completedomain)) {
+ // the domain does already exist as main-domain
+ \Froxlor\UI\Response::standard_error('domainexistalready', $completedomain, true);
+ }
+
+ // if allowed, check for 'is email domain'-flag
+ if ($domain_check['subcanemaildomain'] == '1' || $domain_check['subcanemaildomain'] == '2') {
+ $isemaildomain = intval($isemaildomain);
+ } else {
+ $isemaildomain = $domain_check['subcanemaildomain'] == '3' ? 1 : 0;
+ }
+
+ if ($ssl_redirect != 0) {
+ // a ssl-redirect only works if there actually is a
+ // ssl ip/port assigned to the domain
+ if (\Froxlor\Domain\Domain::domainHasSslIpPort($domain_check['id']) == true) {
+ $ssl_redirect = '1';
+ $_doredirect = true;
+ } else {
+ \Froxlor\UI\Response::standard_error('sslredirectonlypossiblewithsslipport', '', true);
+ }
+ }
+
+ if ($letsencrypt != 0) {
+ // let's encrypt only works if there actually is a
+ // ssl ip/port assigned to the domain
+ if (\Froxlor\Domain\Domain::domainHasSslIpPort($domain_check['id']) == true) {
+ $letsencrypt = '1';
+ } else {
+ \Froxlor\UI\Response::standard_error('letsencryptonlypossiblewithsslipport', '', true);
+ }
+ }
+
+ // Temporarily deactivate ssl_redirect until Let's Encrypt certificate was generated
+ if ($ssl_redirect > 0 && $letsencrypt == 1) {
+ $ssl_redirect = 2;
+ }
+
+ // get the phpsettingid from parentdomain, #107
+ $phpsid_stmt = Database::prepare("
+ SELECT `phpsettingid` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `id` = :id
+ ");
+ $phpsid_result = Database::pexecute_first($phpsid_stmt, array(
+ "id" => $domain_check['id']
+ ), true, true);
+
+ if (! isset($phpsid_result['phpsettingid']) || (int) $phpsid_result['phpsettingid'] <= 0) {
+ // assign default config
+ $phpsid_result['phpsettingid'] = 1;
+ }
+ // check whether the customer has chosen its own php-config
+ if ($phpsettingid > 0 && $phpsettingid != $phpsid_result['phpsettingid']) {
+ $phpsid_result['phpsettingid'] = intval($phpsettingid);
+ }
+
+ // actually insert domain
+ $stmt = Database::prepare("
+ INSERT INTO `" . TABLE_PANEL_DOMAINS . "` SET
+ `customerid` = :customerid,
+ `adminid` = :adminid,
+ `domain` = :domain,
+ `domain_ace` = :domain_ace,
+ `documentroot` = :documentroot,
+ `aliasdomain` = :aliasdomain,
+ `parentdomainid` = :parentdomainid,
+ `wwwserveralias` = :wwwserveralias,
+ `isemaildomain` = :isemaildomain,
+ `iswildcarddomain` = :iswildcarddomain,
+ `phpenabled` = :phpenabled,
+ `openbasedir` = :openbasedir,
+ `openbasedir_path` = :openbasedir_path,
+ `speciallogfile` = :speciallogfile,
+ `specialsettings` = :specialsettings,
+ `ssl_specialsettings` = :ssl_specialsettings,
+ `include_specialsettings` = :include_specialsettings,
+ `ssl_redirect` = :ssl_redirect,
+ `phpsettingid` = :phpsettingid,
+ `letsencrypt` = :letsencrypt,
+ `http2` = :http2,
+ `hsts` = :hsts,
+ `hsts_sub` = :hsts_sub,
+ `hsts_preload` = :hsts_preload,
+ `ocsp_stapling` = :ocsp_stapling,
+ `override_tls` = :override_tls,
+ `ssl_protocols` = :ssl_protocols,
+ `ssl_cipher_list` = :ssl_cipher_list,
+ `tlsv13_cipher_list` = :tlsv13_cipher_list,
+ `ssl_enabled` = :sslenabled
+ ");
+ $params = array(
+ "customerid" => $customer['customerid'],
+ "adminid" => $customer['adminid'],
+ "domain" => $completedomain,
+ "domain_ace" => $idna_convert->decode($completedomain),
+ "documentroot" => $path,
+ "aliasdomain" => $aliasdomain != 0 ? $aliasdomain : null,
+ "parentdomainid" => $domain_check['id'],
+ "wwwserveralias" => $domain_check['wwwserveralias'] == '1' ? '1' : '0',
+ "iswildcarddomain" => $domain_check['iswildcarddomain'] == '1' ? '1' : '0',
+ "isemaildomain" => $isemaildomain,
+ "openbasedir" => $domain_check['openbasedir'],
+ "openbasedir_path" => $openbasedir_path,
+ "phpenabled" => $domain_check['phpenabled'],
+ "speciallogfile" => $domain_check['speciallogfile'],
+ "specialsettings" => $domain_check['specialsettings'],
+ "ssl_specialsettings" => $domain_check['ssl_specialsettings'],
+ "include_specialsettings" => $domain_check['include_specialsettings'],
+ "ssl_redirect" => $ssl_redirect,
+ "phpsettingid" => $phpsid_result['phpsettingid'],
+ "letsencrypt" => $letsencrypt,
+ "http2" => $http2,
+ "hsts" => $hsts_maxage,
+ "hsts_sub" => $hsts_sub,
+ "hsts_preload" => $hsts_preload,
+ "ocsp_stapling" => $domain_check['ocsp_stapling'],
+ "override_tls" => $domain_check['override_tls'],
+ "ssl_protocols" => $domain_check['ssl_protocols'],
+ "ssl_cipher_list" => $domain_check['ssl_cipher_list'],
+ "tlsv13_cipher_list" => $domain_check['tlsv13_cipher_list'],
+ "sslenabled" => $sslenabled
+ );
+ Database::pexecute($stmt, $params, true, true);
+ $subdomain_id = Database::lastInsertId();
+
+ $stmt = Database::prepare("
+ INSERT INTO `" . TABLE_DOMAINTOIP . "`
+ (`id_domain`, `id_ipandports`)
+ SELECT LAST_INSERT_ID(), `id_ipandports`
+ FROM `" . TABLE_DOMAINTOIP . "`
+ WHERE `id_domain` = :id_domain
+ ");
+ Database::pexecute($stmt, array(
+ "id_domain" => $domain_check['id']
+ ));
+
+ if ($_doredirect) {
+ \Froxlor\Domain\Domain::addRedirectToDomain($subdomain_id, $redirectcode);
+ }
+
+ \Froxlor\System\Cronjob::inserttask('1');
+ // Using nameserver, insert a task which rebuilds the server config
+ \Froxlor\System\Cronjob::inserttask('4');
+
+ Customers::increaseUsage($customer['customerid'], 'subdomains_used');
+
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] added subdomain '" . $completedomain . "'");
+
+ $result = $this->apiCall('SubDomains.get', array(
+ 'id' => $subdomain_id
+ ));
+ return $this->response(200, "successful", $result);
+ }
+ throw new \Exception("No more resources available", 406);
+ }
+
+ /**
+ * return a subdomain entry by either id or domainname
+ *
+ * @param int $id
+ * optional, the domain-id
+ * @param string $domainname
+ * optional, the domainname
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function get()
+ {
+ $id = $this->getParam('id', true, 0);
+ $dn_optional = ($id <= 0 ? false : true);
+ $domainname = $this->getParam('domainname', $dn_optional, '');
+
+ // convert possible idn domain to punycode
+ if (substr($domainname, 0, 4) != 'xn--') {
+ $idna_convert = new \Froxlor\Idna\IdnaWrapper();
+ $domainname = $idna_convert->encode($domainname);
+ }
+
+ if ($this->isAdmin()) {
+ if ($this->getUserDetail('customers_see_all') != 1) {
+ // if it's a reseller or an admin who cannot see all customers, we need to check
+ // whether the database belongs to one of his customers
+ $_custom_list_result = $this->apiCall('Customers.listing');
+ $custom_list_result = $_custom_list_result['list'];
+ $customer_ids = array();
+ foreach ($custom_list_result as $customer) {
+ $customer_ids[] = $customer['customerid'];
+ }
+ if (count($customer_ids) > 0) {
+ $result_stmt = Database::prepare("
+ SELECT d.*, pd.`subcanemaildomain`, pd.`isbinddomain` as subisbinddomain
+ FROM `" . TABLE_PANEL_DOMAINS . "` d, `" . TABLE_PANEL_DOMAINS . "` pd
+ WHERE " . ($id > 0 ? "d.`id` = :iddn" : "d.`domain` = :iddn") . " AND d.`customerid` IN (" . implode(", ", $customer_ids) . ")
+ AND ((d.`parentdomainid`!='0' AND pd.`id` = d.`parentdomainid`) OR (d.`parentdomainid`='0' AND pd.`id` = d.`id`))
+ ");
+ $params = array(
+ 'iddn' => ($id <= 0 ? $domainname : $id)
+ );
+ } else {
+ throw new \Exception("You do not have any customers yet", 406);
+ }
+ } else {
+ $result_stmt = Database::prepare("
+ SELECT d.*, pd.`subcanemaildomain`, pd.`isbinddomain` as subisbinddomain
+ FROM `" . TABLE_PANEL_DOMAINS . "` d, `" . TABLE_PANEL_DOMAINS . "` pd
+ WHERE " . ($id > 0 ? "d.`id` = :iddn" : "d.`domain` = :iddn") . "
+ AND ((d.`parentdomainid`!='0' AND pd.`id` = d.`parentdomainid`) OR (d.`parentdomainid`='0' AND pd.`id` = d.`id`))
+ ");
+ $params = array(
+ 'iddn' => ($id <= 0 ? $domainname : $id)
+ );
+ }
+ } else {
+ if (! $this->isInternal() && Settings::IsInList('panel.customer_hide_options', 'domains')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+ $result_stmt = Database::prepare("
+ SELECT d.*, pd.`subcanemaildomain`, pd.`isbinddomain` as subisbinddomain
+ FROM `" . TABLE_PANEL_DOMAINS . "` d, `" . TABLE_PANEL_DOMAINS . "` pd
+ WHERE d.`customerid`= :customerid AND " . ($id > 0 ? "d.`id` = :iddn" : "d.`domain` = :iddn") . "
+ AND ((d.`parentdomainid`!='0' AND pd.`id` = d.`parentdomainid`) OR (d.`parentdomainid`='0' AND pd.`id` = d.`id`))
+ ");
+ $params = array(
+ 'customerid' => $this->getUserDetail('customerid'),
+ 'iddn' => ($id <= 0 ? $domainname : $id)
+ );
+ }
+ $result = Database::pexecute_first($result_stmt, $params, true, true);
+ if ($result) {
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] get subdomain '" . $result['domain'] . "'");
+ return $this->response(200, "successful", $result);
+ }
+ $key = ($id > 0 ? "id #" . $id : "domainname '" . $domainname . "'");
+ throw new \Exception("Subdomain with " . $key . " could not be found", 404);
+ }
+
+ /**
+ * update subdomain entry by either id or domainname
+ *
+ * @param int $id
+ * optional, the domain-id
+ * @param string $domainname
+ * optional, the domainname
+ * @param int $alias
+ * optional, domain-id of a domain that the new domain should be an alias of
+ * @param string $path
+ * optional, destination path relative to the customers-homedir, default is customers-homedir
+ * @param string $url
+ * optional, overwrites path value with an URL to generate a redirect, alternatively use the path parameter also for URLs
+ * @param int $selectserveralias
+ * optional, 0 = wildcard, 1 = www-alias, 2 = none
+ * @param bool $isemaildomain
+ * optional
+ * @param int $openbasedir_path
+ * optional, either 0 for domains-docroot or 1 for customers-homedir
+ * @param int $phpsettingid
+ * optional, php-settings-id, if empty the $domain value is used
+ * @param int $redirectcode
+ * optional, redirect-code-id from TABLE_PANEL_REDIRECTCODES
+ * @param bool $sslenabled
+ * optional, whether or not SSL is enabled for this domain, regardless of the assigned ssl-ips, default 1 (true)
+ * @param bool $ssl_redirect
+ * optional, whether to generate a https-redirect or not, default false; requires SSL to be enabled
+ * @param bool $letsencrypt
+ * optional, whether to generate a Let's Encrypt certificate for this domain, default false; requires SSL to be enabled
+ * @param bool $http2
+ * optional, whether to enable http/2 for this domain (requires to be enabled in the settings), default 0 (false)
+ * @param int $hsts_maxage
+ * optional max-age value for HSTS header
+ * @param bool $hsts_sub
+ * optional whether or not to add subdomains to the HSTS header
+ * @param bool $hsts_preload
+ * optional whether or not to preload HSTS header value
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function update()
+ {
+ $id = $this->getParam('id', true, 0);
+ $dn_optional = ($id <= 0 ? false : true);
+ $domainname = $this->getParam('domainname', $dn_optional, '');
+
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'domains')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ $result = $this->apiCall('SubDomains.get', array(
+ 'id' => $id,
+ 'domainname' => $domainname
+ ));
+ $id = $result['id'];
+
+ // parameters
+ $aliasdomain = $this->getParam('alias', true, 0);
+ $path = $this->getParam('path', true, $result['documentroot']);
+ $url = $this->getParam('url', true, '');
+ // default: 0 = wildcard, 1 = www-alias, 2 = none
+ $_serveraliasdefault = $result['iswildcarddomain'] == '1' ? 0 : ($result['wwwserveralias'] == '1' ? 1 : 2);
+ $selectserveralias = $this->getParam('selectserveralias', true, $_serveraliasdefault);
+ $isemaildomain = $this->getBoolParam('isemaildomain', true, $result['isemaildomain']);
+ $openbasedir_path = $this->getParam('openbasedir_path', true, $result['openbasedir_path']);
+ $phpsettingid = $this->getParam('phpsettingid', true, $result['phpsettingid']);
+ $redirectcode = $this->getParam('redirectcode', true, \Froxlor\Domain\Domain::getDomainRedirectId($id));
+ if (Settings::Get('system.use_ssl')) {
+ $sslenabled = $this->getBoolParam('sslenabled', true, $result['ssl_enabled']);
+ $ssl_redirect = $this->getBoolParam('ssl_redirect', true, $result['ssl_redirect']);
+ $letsencrypt = $this->getBoolParam('letsencrypt', true, $result['letsencrypt']);
+ $http2 = $this->getBoolParam('http2', true, $result['http2']);
+ $hsts_maxage = $this->getParam('hsts_maxage', true, $result['hsts']);
+ $hsts_sub = $this->getBoolParam('hsts_sub', true, $result['hsts_sub']);
+ $hsts_preload = $this->getBoolParam('hsts_preload', true, $result['hsts_preload']);
+ } else {
+ $sslenabled = 0;
+ $ssl_redirect = 0;
+ $letsencrypt = 0;
+ $http2 = 0;
+ $hsts_maxage = 0;
+ $hsts_sub = 0;
+ $hsts_preload = 0;
+ }
+
+ // get needed customer info to reduce the subdomain-usage-counter by one
+ $customer = $this->getCustomerData();
+
+ $alias_stmt = Database::prepare("SELECT COUNT(`id`) AS count FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `aliasdomain`= :aliasdomain");
+ $alias_check = Database::pexecute_first($alias_stmt, array(
+ "aliasdomain" => $result['id']
+ ));
+ $alias_check = $alias_check['count'];
+
+ // alias domain checked?
+ if ($aliasdomain != 0) {
+ $aliasdomain_stmt = Database::prepare("
+ SELECT `id` FROM `" . TABLE_PANEL_DOMAINS . "` `d`,`" . TABLE_PANEL_CUSTOMERS . "` `c`
+ WHERE `d`.`customerid`= :customerid
+ AND `d`.`aliasdomain` IS NULL
+ AND `d`.`id`<>`c`.`standardsubdomain`
+ AND `c`.`customerid`= :customerid
+ AND `d`.`id`= :id
+ ");
+ $aliasdomain_check = Database::pexecute_first($aliasdomain_stmt, array(
+ "id" => $aliasdomain,
+ "customerid" => $customer['customerid']
+ ), true, true);
+ if ($aliasdomain_check['id'] != $aliasdomain) {
+ \Froxlor\UI\Response::standard_error('domainisaliasorothercustomer', '', true);
+ }
+ \Froxlor\Domain\Domain::triggerLetsEncryptCSRForAliasDestinationDomain($aliasdomain, $this->logger());
+ }
+
+ // validate / correct path/url of domain
+ $_doredirect = false;
+ $path = $this->validateDomainDocumentRoot($path, $url, $customer, $result['domain'], $_doredirect);
+
+ // set alias-fields according to selected alias mode
+ $iswildcarddomain = ($selectserveralias == '0') ? '1' : '0';
+ $wwwserveralias = ($selectserveralias == '1') ? '1' : '0';
+
+ // if allowed, check for 'is email domain'-flag
+ if ($result['parentdomainid'] != '0' && ($result['subcanemaildomain'] == '1' || $result['subcanemaildomain'] == '2') && $isemaildomain != $result['isemaildomain']) {
+ $isemaildomain = intval($isemaildomain);
+ } elseif ($result['parentdomainid'] != '0') {
+ $isemaildomain = $result['subcanemaildomain'] == '3' ? 1 : 0;
+ }
+
+ // check changes of openbasedir-path variable
+ if ($openbasedir_path != 1) {
+ $openbasedir_path = 0;
+ }
+
+ if ($ssl_redirect != 0) {
+ // a ssl-redirect only works if there actually is a
+ // ssl ip/port assigned to the domain
+ if (\Froxlor\Domain\Domain::domainHasSslIpPort($result['id']) == true) {
+ $ssl_redirect = '1';
+ $_doredirect = true;
+ } else {
+ \Froxlor\UI\Response::standard_error('sslredirectonlypossiblewithsslipport', '', true);
+ }
+ }
+
+ if ($letsencrypt != 0) {
+ // let's encrypt only works if there actually is a
+ // ssl ip/port assigned to the domain
+ if (\Froxlor\Domain\Domain::domainHasSslIpPort($result['id']) == true) {
+ $letsencrypt = '1';
+ } else {
+ \Froxlor\UI\Response::standard_error('letsencryptonlypossiblewithsslipport', '', true);
+ }
+ }
+
+ // We can't enable let's encrypt for wildcard-domains
+ if ($iswildcarddomain == '1' && $letsencrypt == '1') {
+ \Froxlor\UI\Response::standard_error('nowildcardwithletsencrypt');
+ }
+
+ // Temporarily deactivate ssl_redirect until Let's Encrypt certificate was generated
+ if ($ssl_redirect > 0 && $letsencrypt == 1 && $result['letsencrypt'] != $letsencrypt) {
+ $ssl_redirect = 2;
+ }
+
+ // is-email-domain flag changed - remove mail accounts and mail-addresses
+ if (($result['isemaildomain'] == '1') && $isemaildomain == '0') {
+ $params = array(
+ "customerid" => $customer['customerid'],
+ "domainid" => $id
+ );
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_MAIL_USERS . "` WHERE `customerid`= :customerid AND `domainid`= :domainid");
+ Database::pexecute($stmt, $params, true, true);
+ $stmt = Database::prepare("DELETE FROM `" . TABLE_MAIL_VIRTUAL . "` WHERE `customerid`= :customerid AND `domainid`= :domainid");
+ Database::pexecute($stmt, $params, true, true);
+ $idna_convert = new \Froxlor\Idna\IdnaWrapper();
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] automatically deleted mail-table entries for '" . $idna_convert->decode($result['domain']) . "'");
+ }
+
+ // handle redirect
+ if ($_doredirect) {
+ \Froxlor\Domain\Domain::updateRedirectOfDomain($id, $redirectcode);
+ }
+
+ if ($path != $result['documentroot'] || $isemaildomain != $result['isemaildomain'] || $wwwserveralias != $result['wwwserveralias'] || $iswildcarddomain != $result['iswildcarddomain'] || $aliasdomain != $result['aliasdomain'] || $openbasedir_path != $result['openbasedir_path'] || $ssl_redirect != $result['ssl_redirect'] || $letsencrypt != $result['letsencrypt'] || $hsts_maxage != $result['hsts'] || $hsts_sub != $result['hsts_sub'] || $hsts_preload != $result['hsts_preload'] || $phpsettingid != $result['phpsettingid']) {
+ $stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_DOMAINS . "` SET
+ `documentroot` = :documentroot,
+ `isemaildomain` = :isemaildomain,
+ `wwwserveralias` = :wwwserveralias,
+ `iswildcarddomain` = :iswildcarddomain,
+ `aliasdomain` = :aliasdomain,
+ `openbasedir_path` = :openbasedir_path,
+ `ssl_enabled` = :sslenabled,
+ `ssl_redirect` = :ssl_redirect,
+ `letsencrypt` = :letsencrypt,
+ `http2` = :http2,
+ `hsts` = :hsts,
+ `hsts_sub` = :hsts_sub,
+ `hsts_preload` = :hsts_preload,
+ `phpsettingid` = :phpsettingid
+ WHERE `customerid`= :customerid AND `id`= :id
+ ");
+ $params = array(
+ "documentroot" => $path,
+ "isemaildomain" => $isemaildomain,
+ "wwwserveralias" => $wwwserveralias,
+ "iswildcarddomain" => $iswildcarddomain,
+ "aliasdomain" => ($aliasdomain != 0 && $alias_check == 0) ? $aliasdomain : null,
+ "openbasedir_path" => $openbasedir_path,
+ "sslenabled" => $sslenabled,
+ "ssl_redirect" => $ssl_redirect,
+ "letsencrypt" => $letsencrypt,
+ "http2" => $http2,
+ "hsts" => $hsts_maxage,
+ "hsts_sub" => $hsts_sub,
+ "hsts_preload" => $hsts_preload,
+ "phpsettingid" => $phpsettingid,
+ "customerid" => $customer['customerid'],
+ "id" => $id
+ );
+ Database::pexecute($stmt, $params, true, true);
+
+ if ($result['aliasdomain'] != $aliasdomain && is_numeric($result['aliasdomain'])) {
+ // trigger when domain id for alias destination has changed: both for old and new destination
+ \Froxlor\Domain\Domain::triggerLetsEncryptCSRForAliasDestinationDomain($result['aliasdomain'], $this->logger());
+ \Froxlor\Domain\Domain::triggerLetsEncryptCSRForAliasDestinationDomain($aliasdomain, $this->logger());
+ }
+ if ($result['wwwserveralias'] != $wwwserveralias || $result['letsencrypt'] != $letsencrypt) {
+ // or when wwwserveralias or letsencrypt was changed
+ \Froxlor\Domain\Domain::triggerLetsEncryptCSRForAliasDestinationDomain($aliasdomain, $this->logger());
+ if ((int) $aliasdomain === 0) {
+ // in case the wwwserveralias is set on a main domain, $aliasdomain is 0
+ // --> the call just above to triggerLetsEncryptCSRForAliasDestinationDomain
+ // is a noop...let's repeat it with the domain id of the main domain
+ \Froxlor\Domain\Domain::triggerLetsEncryptCSRForAliasDestinationDomain($id, $this->logger());
+ }
+ }
+
+ // check whether LE has been disabled, so we remove the certificate
+ if ($letsencrypt == '0' && $result['letsencrypt'] == '1') {
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = :id
+ ");
+ Database::pexecute($del_stmt, array(
+ 'id' => $id
+ ), true, true);
+ // remove domain from acme.sh / lets encrypt if used
+ \Froxlor\System\Cronjob::inserttask('12', $result['domain']);
+ }
+
+ \Froxlor\System\Cronjob::inserttask('1');
+ \Froxlor\System\Cronjob::inserttask('4');
+ $idna_convert = new \Froxlor\Idna\IdnaWrapper();
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "[API] edited domain '" . $idna_convert->decode($result['domain']) . "'");
+ }
+ $result = $this->apiCall('SubDomains.get', array(
+ 'id' => $id
+ ));
+ return $this->response(200, "successful", $result);
+ }
+
+ /**
+ * lists all subdomain entries
+ *
+ * @param int $customerid
+ * optional, admin-only, select (sub)domains of a specific customer by id
+ * @param string $loginname
+ * optional, admin-only, select (sub)domains of a specific customer by loginname
+ * @param array $sql_search
+ * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), LIKE is used if left empty and 'value' => searchvalue
+ * @param int $sql_limit
+ * optional specify number of results to be returned
+ * @param int $sql_offset
+ * optional specify offset for resultset
+ * @param array $sql_orderby
+ * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more fields
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listing()
+ {
+ if ($this->isAdmin()) {
+ // if we're an admin, list all databases of all the admins customers
+ // or optionally for one specific customer identified by id or loginname
+ $customerid = $this->getParam('customerid', true, 0);
+ $loginname = $this->getParam('loginname', true, '');
+
+ if (! empty($customerid) || ! empty($loginname)) {
+ $result = $this->apiCall('Customers.get', array(
+ 'id' => $customerid,
+ 'loginname' => $loginname
+ ));
+ $custom_list_result = array(
+ $result
+ );
+ } else {
+ $_custom_list_result = $this->apiCall('Customers.listing');
+ $custom_list_result = $_custom_list_result['list'];
+ }
+ $customer_ids = array();
+ $customer_stdsubs = array();
+ foreach ($custom_list_result as $customer) {
+ $customer_ids[] = $customer['customerid'];
+ $customer_stdsubs[$customer['customerid']] = $customer['standardsubdomain'];
+ }
+ if (empty($customer_ids)) {
+ throw new \Exception("Required resource unsatisfied.", 405);
+ }
+ if (empty($customer_stdsubs)) {
+ throw new \Exception("Required resource unsatisfied.", 405);
+ }
+
+ $select_fields = [
+ '`d`.*'
+ ];
+ } else {
+ if (Settings::IsInList('panel.customer_hide_options', 'domains')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+ $customer_ids = array(
+ $this->getUserDetail('customerid')
+ );
+ $customer_stdsubs = array(
+ $this->getUserDetail('customerid') => $this->getUserDetail('standardsubdomain')
+ );
+
+ $select_fields = [
+ '`d`.`id`',
+ '`d`.`customerid`',
+ '`d`.`domain`',
+ '`d`.`domain_ace`',
+ '`d`.`documentroot`',
+ '`d`.`isbinddomain`',
+ '`d`.`isemaildomain`',
+ '`d`.`caneditdomain`',
+ '`d`.`iswildcarddomain`',
+ '`d`.`parentdomainid`',
+ '`d`.`letsencrypt`',
+ '`d`.`registration_date`',
+ '`d`.`termination_date`'
+ ];
+ }
+ $query_fields = array();
+
+ // prepare select statement
+ $domains_stmt = Database::prepare("
+ SELECT " . implode(",", $select_fields) . ", IF(`d`.`parentdomainid` > 0, `pd`.`domain_ace`, `d`.`domain_ace`) AS `parentdomainname`, `ad`.`id` AS `aliasdomainid`, `ad`.`domain` AS `aliasdomain`, `da`.`id` AS `domainaliasid`, `da`.`domain` AS `domainalias`
+ FROM `" . TABLE_PANEL_DOMAINS . "` `d`
+ LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` `ad` ON `d`.`aliasdomain`=`ad`.`id`
+ LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` `da` ON `da`.`aliasdomain`=`d`.`id`
+ LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` `pd` ON `pd`.`id`=`d`.`parentdomainid`
+ WHERE `d`.`customerid` IN (" . implode(', ', $customer_ids) . ")
+ AND `d`.`email_only` = '0'
+ AND `d`.`id` NOT IN (" . implode(', ', $customer_stdsubs) . ")" . $this->getSearchWhere($query_fields, true) . " GROUP BY `d`.`id` ORDER BY `parentdomainname` " . $this->getOrderBy(true) . $this->getLimit());
+
+ $result = array();
+ Database::pexecute($domains_stmt, $query_fields, true, true);
+ while ($row = $domains_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = $row;
+ }
+ return $this->response(200, "successful", array(
+ 'count' => count($result),
+ 'list' => $result
+ ));
+ }
+
+ /**
+ * returns the total number of accessable subdomain entries
+ *
+ * @param int $customerid
+ * optional, admin-only, select (sub)domains of a specific customer by id
+ * @param string $loginname
+ * optional, admin-only, select (sub)domains of a specific customer by loginname
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function listingCount()
+ {
+ if ($this->isAdmin()) {
+ // if we're an admin, list all databases of all the admins customers
+ // or optionally for one specific customer identified by id or loginname
+ $customerid = $this->getParam('customerid', true, 0);
+ $loginname = $this->getParam('loginname', true, '');
+
+ if (! empty($customerid) || ! empty($loginname)) {
+ $result = $this->apiCall('Customers.get', array(
+ 'id' => $customerid,
+ 'loginname' => $loginname
+ ));
+ $custom_list_result = array(
+ $result
+ );
+ } else {
+ $_custom_list_result = $this->apiCall('Customers.listing');
+ $custom_list_result = $_custom_list_result['list'];
+ }
+ $customer_ids = array();
+ $customer_stdsubs = array();
+ foreach ($custom_list_result as $customer) {
+ $customer_ids[] = $customer['customerid'];
+ $customer_stdsubs[$customer['customerid']] = $customer['standardsubdomain'];
+ }
+ } else {
+ if (Settings::IsInList('panel.customer_hide_options', 'domains')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+ $customer_ids = array(
+ $this->getUserDetail('customerid')
+ );
+ $customer_stdsubs = array(
+ $this->getUserDetail('customerid') => $this->getUserDetail('standardsubdomain')
+ );
+ }
+ // prepare select statement
+ $domains_stmt = Database::prepare("
+ SELECT COUNT(*) as num_subdom
+ FROM `" . TABLE_PANEL_DOMAINS . "` `d`
+ WHERE `d`.`customerid` IN (" . implode(', ', $customer_ids) . ")
+ AND `d`.`email_only` = '0'
+ AND `d`.`id` NOT IN (" . implode(', ', $customer_stdsubs) . ")
+ ");
+ $result = Database::pexecute_first($domains_stmt, null, true, true);
+ if ($result) {
+ return $this->response(200, "successful", $result['num_subdom']);
+ }
+ }
+
+ /**
+ * delete a subdomain by either id or domainname
+ *
+ * @param int $id
+ * optional, the domain-id
+ * @param string $domainname
+ * optional, the domainname
+ * @param int $customerid
+ * optional, required when called as admin (if $loginname is not specified)
+ * @param string $loginname
+ * optional, required when called as admin (if $customerid is not specified)
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function delete()
+ {
+ $id = $this->getParam('id', true, 0);
+ $dn_optional = ($id <= 0 ? false : true);
+ $domainname = $this->getParam('domainname', $dn_optional, '');
+
+ if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'domains')) {
+ throw new \Exception("You cannot access this resource", 405);
+ }
+
+ $result = $this->apiCall('SubDomains.get', array(
+ 'id' => $id,
+ 'domainname' => $domainname
+ ));
+ $id = $result['id'];
+
+ // get needed customer info to reduce the subdomain-usage-counter by one
+ $customer = $this->getCustomerData();
+
+ if (! $this->isAdmin() && $result['caneditdomain'] == 0) {
+ throw new \Exception("You cannot edit this resource", 405);
+ }
+
+ if ($result['isemaildomain'] == '1') {
+ // check for e-mail addresses
+ $emails_stmt = Database::prepare("
+ SELECT COUNT(`id`) AS `count` FROM `" . TABLE_MAIL_VIRTUAL . "`
+ WHERE `customerid` = :customerid AND `domainid` = :domainid
+ ");
+ $emails = Database::pexecute_first($emails_stmt, array(
+ "customerid" => $customer['customerid'],
+ "domainid" => $id
+ ), true, true);
+
+ if ($emails['count'] != '0') {
+ \Froxlor\UI\Response::standard_error('domains_cantdeletedomainwithemail', '', true);
+ }
+ }
+
+ \Froxlor\Domain\Domain::triggerLetsEncryptCSRForAliasDestinationDomain($result['aliasdomain'], $this->logger());
+
+ // delete domain from table
+ $stmt = Database::prepare("
+ DELETE FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `customerid` = :customerid AND `id` = :id
+ ");
+ Database::pexecute($stmt, array(
+ "customerid" => $customer['customerid'],
+ "id" => $id
+ ), true, true);
+
+ // remove connections to ips and domainredirects
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_DOMAINTOIP . "`
+ WHERE `id_domain` = :domainid
+ ");
+ Database::pexecute($del_stmt, array(
+ 'domainid' => $id
+ ), true, true);
+
+ // remove redirect-codes
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_PANEL_DOMAINREDIRECTS . "`
+ WHERE `did` = :domainid
+ ");
+ Database::pexecute($del_stmt, array(
+ 'domainid' => $id
+ ), true, true);
+
+ // remove certificate from domain_ssl_settings, fixes #1596
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "`
+ WHERE `domainid` = :domainid
+ ");
+ Database::pexecute($del_stmt, array(
+ 'domainid' => $id
+ ), true, true);
+
+ // remove possible existing DNS entries
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_DOMAIN_DNS . "`
+ WHERE `domain_id` = :domainid
+ ");
+ Database::pexecute($del_stmt, array(
+ 'domainid' => $id
+ ), true, true);
+
+ \Froxlor\System\Cronjob::inserttask('1');
+ // Using nameserver, insert a task which rebuilds the server config
+ \Froxlor\System\Cronjob::inserttask('4');
+ // remove domains DNS from powerDNS if used, #581
+ \Froxlor\System\Cronjob::inserttask('11', $result['domain']);
+ // remove domain from acme.sh / lets encrypt if used
+ \Froxlor\System\Cronjob::inserttask('12', $result['domain']);
+
+ // reduce subdomain-usage-counter
+ Customers::decreaseUsage($customer['customerid'], 'subdomains_used');
+
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] deleted subdomain '" . $result['domain'] . "'");
+ return $this->response(200, "successful", $result);
+ }
+
+ /**
+ * validate given path and replace with url if given and valid
+ *
+ * @param string $path
+ * @param string $url
+ * @param array $customer
+ * @param string $completedomain
+ * @param boolean $_doredirect
+ *
+ * @return string validated path
+ * @throws \Exception
+ */
+ private function validateDomainDocumentRoot($path = null, $url = null, $customer = null, $completedomain = null, &$_doredirect = false)
+ {
+ // check whether an URL was specified
+ $_doredirect = false;
+ if (! empty($url) && \Froxlor\Validate\Validate::validateUrl($url)) {
+ $path = $url;
+ $_doredirect = true;
+ } else {
+ $path = \Froxlor\Validate\Validate::validate($path, 'path', '', '', array(), true);
+ }
+
+ // check whether path is a real path
+ if (! preg_match('/^https?\:\/\//', $path) || ! \Froxlor\Validate\Validate::validateUrl($path)) {
+ if (strstr($path, ":") !== false) {
+ \Froxlor\UI\Response::standard_error('pathmaynotcontaincolon', '', true);
+ }
+ // If path is empty or '/' and 'Use domain name as default value for DocumentRoot path' is enabled in settings,
+ // set default path to subdomain or domain name
+ if ((($path == '') || ($path == '/')) && Settings::Get('system.documentroot_use_default_value') == 1) {
+ $path = \Froxlor\FileDir::makeCorrectDir($customer['documentroot'] . '/' . $completedomain);
+ } else {
+ $path = \Froxlor\FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path);
+ }
+ } else {
+ // no it's not, create a redirect
+ $_doredirect = true;
+ }
+ return $path;
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/SysLog.php b/lib/Froxlor/Api/Commands/SysLog.php
new file mode 100644
index 00000000..9e744827
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/SysLog.php
@@ -0,0 +1,211 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.6
+ *
+ */
+class SysLog extends \Froxlor\Api\ApiCommand implements \Froxlor\Api\ResourceEntity
+{
+
+ /**
+ * list all log-entries
+ *
+ * @param array $sql_search
+ * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), LIKE is used if left empty and 'value' => searchvalue
+ * @param int $sql_limit
+ * optional specify number of results to be returned
+ * @param int $sql_offset
+ * optional specify offset for resultset
+ * @param array $sql_orderby
+ * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more fields
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listing()
+ {
+ $result = array();
+ $query_fields = array();
+ if ($this->isAdmin() && $this->getUserDetail('customers_see_all') == '1') {
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_LOG . "` " . $this->getSearchWhere($query_fields) . $this->getOrderBy() . $this->getLimit());
+ } elseif ($this->isAdmin()) {
+ // get all admin customers
+ $_custom_list_result = $this->apiCall('Customers.listing');
+ $custom_list_result = $_custom_list_result['list'];
+ $customer_names = array();
+ foreach ($custom_list_result as $customer) {
+ $customer_names[] = $customer['loginname'];
+ }
+ if (count($customer_names) > 0) {
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_LOG . "`
+ WHERE `user` = :loginname OR `user` IN ('" . implode("', '", $customer_names) . "')" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit());
+ } else {
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_LOG . "`
+ WHERE `user` = :loginname" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit());
+ }
+ $query_fields['loginname'] = $this->getUserDetail('loginname');
+ } else {
+ // every one else just sees their logs
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_LOG . "`
+ WHERE `user` = :loginname AND `action` <> 99 " . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit());
+ $query_fields['loginname'] = $this->getUserDetail('loginname');
+ }
+ Database::pexecute($result_stmt, $query_fields, true, true);
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = $row;
+ }
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] list log-entries");
+ return $this->response(200, "successful", array(
+ 'count' => count($result),
+ 'list' => $result
+ ));
+ }
+
+ /**
+ * returns the total number of log-entries
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function listingCount()
+ {
+ $params = null;
+ if ($this->isAdmin() && $this->getUserDetail('customers_see_all') == '1') {
+ $result_stmt = Database::prepare("
+ SELECT COUNT(*) as num_logs FROM `" . TABLE_PANEL_LOG . "`
+ ");
+ } elseif ($this->isAdmin()) {
+ // get all admin customers
+ $_custom_list_result = $this->apiCall('Customers.listing');
+ $custom_list_result = $_custom_list_result['list'];
+ $customer_names = array();
+ foreach ($custom_list_result as $customer) {
+ $customer_names[] = $customer['loginname'];
+ }
+ if (count($customer_names) > 0) {
+ $result_stmt = Database::prepare("
+ SELECT COUNT(*) as num_logs FROM `" . TABLE_PANEL_LOG . "`
+ WHERE `user` = :loginname OR `user` IN ('" . implode("', '", $customer_names) . "')
+ ");
+ } else {
+ $result_stmt = Database::prepare("
+ SELECT COUNT(*) as num_logs FROM `" . TABLE_PANEL_LOG . "`
+ WHERE `user` = :loginname
+ ");
+ }
+ $params = [
+ 'loginname' => $this->getUserDetail('loginname')
+ ];
+ } else {
+ // every one else just sees their logs
+ $result_stmt = Database::prepare("
+ SELECT COUNT(*) as num_logs FROM `" . TABLE_PANEL_LOG . "`
+ WHERE `user` = :loginname AND `action` <> 99
+ ");
+ $params = [
+ 'loginname' => $this->getUserDetail('loginname')
+ ];
+ }
+
+ $result = Database::pexecute_first($result_stmt, $params, true, true);
+ if ($result) {
+ return $this->response(200, "successful", $result['num_logs']);
+ }
+ }
+
+ /**
+ * You cannot get log entries
+ */
+ public function get()
+ {
+ throw new \Exception('You cannot get log entries', 303);
+ }
+
+ /**
+ * You cannot add log entries
+ */
+ public function add()
+ {
+ throw new \Exception('You cannot add log entries', 303);
+ }
+
+ /**
+ * You cannot update log entries
+ */
+ public function update()
+ {
+ throw new \Exception('You cannot update log entries', 303);
+ }
+
+ /**
+ * delete log entries
+ *
+ * @param int $min_to_keep
+ * optional minutes to keep, default is 10
+ *
+ * @access admin
+ * @throws \Exception
+ * @return string json-encoded array
+ */
+ public function delete()
+ {
+ if ($this->isAdmin()) {
+ $min_to_keep = self::getParam('min_to_keep', true, 10);
+ if ($min_to_keep < 0) {
+ $min_to_keep = 0;
+ }
+ $truncatedate = time() - (60 * $min_to_keep);
+ $params = array();
+ if ($this->getUserDetail('customers_see_all') == '1') {
+ $result_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_PANEL_LOG . "` WHERE `date` < :trunc
+ ");
+ } else {
+ // get all admin customers
+ $_custom_list_result = $this->apiCall('Customers.listing');
+ $custom_list_result = $_custom_list_result['list'];
+ $customer_names = array();
+ foreach ($custom_list_result as $customer) {
+ $customer_names[] = $customer['loginname'];
+ }
+ if (count($customer_names) > 0) {
+ $result_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_PANEL_LOG . "` WHERE `date` < :trunc AND `user` = :loginname OR `user` IN ('" . implode("', '", $customer_names) . "')
+ ");
+ } else {
+ $result_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_PANEL_LOG . "` WHERE `date` < :trunc AND `user` = :loginname
+ ");
+ }
+ $params = [
+ 'loginname' => $this->getUserDetail('loginname')
+ ];
+ }
+ $params['trunc'] = $truncatedate;
+ Database::pexecute($result_stmt, $params, true, true);
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] truncated the froxlor syslog");
+ return $this->response(200, "successful", true);
+ }
+ throw new \Exception("Not allowed to execute given command.", 403);
+ }
+}
diff --git a/lib/Froxlor/Api/Commands/Traffic.php b/lib/Froxlor/Api/Commands/Traffic.php
new file mode 100644
index 00000000..caa5c670
--- /dev/null
+++ b/lib/Froxlor/Api/Commands/Traffic.php
@@ -0,0 +1,172 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class Traffic extends \Froxlor\Api\ApiCommand implements \Froxlor\Api\ResourceEntity
+{
+
+ /**
+ * You cannot add traffic data
+ *
+ * @throws \Exception
+ */
+ public function add()
+ {
+ throw new \Exception('You cannot add traffic data', 303);
+ }
+
+ /**
+ * to get specific traffic details use year, month and/or day parameter for Traffic.listing()
+ *
+ * @throws \Exception
+ */
+ public function get()
+ {
+ throw new \Exception('To get specific traffic details use year, month and/or day parameter for Traffic.listing()', 303);
+ }
+
+ /**
+ * You cannot update traffic data
+ *
+ * @throws \Exception
+ */
+ public function update()
+ {
+ throw new \Exception('You cannot update traffic data', 303);
+ }
+
+ /**
+ * list traffic information
+ *
+ * @param int $year
+ * optional, default empty
+ * @param int $month
+ * optional, default empty
+ * @param int $day
+ * optional, default empty
+ * @param int $date_from
+ * optional timestamp, default empty, if specified, $year, $month and $day will be ignored
+ * @param int $date_until
+ * optional timestamp, default empty, if specified, $year, $month and $day will be ignored
+ * @param bool $customer_traffic
+ * optional, admin-only, whether to output ones own traffic or all of ones customers, default is 0 (false)
+ * @param int $customerid
+ * optional, admin-only, select traffic of a specific customer by id
+ * @param string $loginname
+ * optional, admin-only, select traffic of a specific customer by loginname
+ *
+ * @access admin, customer
+ * @throws \Exception
+ * @return string json-encoded array count|list
+ */
+ public function listing()
+ {
+ $year = $this->getParam('year', true, "");
+ $month = $this->getParam('month', true, "");
+ $day = $this->getParam('day', true, "");
+ $date_from = $this->getParam('date_from', true, - 1);
+ $date_until = $this->getParam('date_until', true, - 1);
+ $customer_traffic = $this->getBoolParam('customer_traffic', true, 0);
+ $customer_ids = $this->getAllowedCustomerIds();
+ $result = array();
+ $params = array();
+
+ // validate parameters
+ if ($date_from >= 0 || $date_until >= 0) {
+ $year = "";
+ $month = "";
+ $day = "";
+ if ($date_from == $date_until) {
+ $date_until = -1;
+ }
+ if ($date_from >= 0 && $date_until >= 0 && $date_until < $date_from) {
+ // switch
+ $temp_ts = $date_from;
+ $date_from = $date_until;
+ $date_until = $temp_ts;
+ }
+ }
+
+ // check for year/month/day
+ $where_str = "";
+ if (! empty($year) && is_numeric($year)) {
+ $where_str .= " AND `year` = :year";
+ $params['year'] = $year;
+ }
+ if (! empty($month) && is_numeric($month)) {
+ $where_str .= " AND `month` = :month";
+ $params['month'] = $month;
+ }
+ if (! empty($day) && is_numeric($day)) {
+ $where_str .= " AND `day` = :day";
+ $params['day'] = $day;
+ }
+ if ($date_from >= 0 && $date_until >= 0) {
+ $where_str .= " AND `stamp` BETWEEN :df AND :du";
+ $params['df'] = $date_from;
+ $params['du'] = $date_until;
+ } elseif ($date_from >= 0 && $date_until < 0) {
+ $where_str .= " AND `stamp` > :df";
+ $params['df'] = $date_from;
+ } elseif ($date_from < 0 && $date_until >= 0) {
+ $where_str .= " AND `stamp` < :du";
+ $params['du'] = $date_until;
+ }
+
+ if (! $this->isAdmin() || ($this->isAdmin() && $customer_traffic)) {
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_TRAFFIC . "`
+ WHERE `customerid` IN (" . implode(", ", $customer_ids) . ")" . $where_str);
+ } else {
+ $params['adminid'] = $this->getUserDetail('adminid');
+ $result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_TRAFFIC_ADMINS . "`
+ WHERE `adminid` = :adminid" . $where_str);
+ }
+ Database::pexecute($result_stmt, $params, true, true);
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = $row;
+ }
+ $this->logger()->logAction($this->isAdmin() ? \Froxlor\FroxlorLogger::ADM_ACTION : \Froxlor\FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] list traffic");
+ return $this->response(200, "successful", array(
+ 'count' => count($result),
+ 'list' => $result
+ ));
+ }
+
+ /**
+ * You cannot count the traffic data list
+ *
+ * @throws \Exception
+ */
+ public function listingCount()
+ {
+ throw new \Exception('You cannot count the traffic data list', 303);
+ }
+
+ /**
+ * You cannot delete traffic data
+ *
+ * @throws \Exception
+ */
+ public function delete()
+ {
+ throw new \Exception('You cannot delete traffic data', 303);
+ }
+}
diff --git a/lib/Froxlor/Api/FroxlorRPC.php b/lib/Froxlor/Api/FroxlorRPC.php
new file mode 100644
index 00000000..3a21f330
--- /dev/null
+++ b/lib/Froxlor/Api/FroxlorRPC.php
@@ -0,0 +1,126 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+class FroxlorRPC
+{
+
+ /**
+ * validate a given request
+ *
+ * @param array $request
+ *
+ * @throws \Exception
+ * @return array
+ */
+ public static function validateRequest($request)
+ {
+ // check header
+ if (! isset($request['header']) || empty($request['header'])) {
+ throw new \Exception("Invalid request header", 400);
+ }
+
+ // check authorization
+ if (! isset($request['header']['apikey']) || empty($request['header']['apikey']) || ! isset($request['header']['secret']) || empty($request['header']['secret'])) {
+ throw new \Exception("No authorization credentials given", 400);
+ }
+ self::validateAuth($request['header']['apikey'], $request['header']['secret']);
+
+ // check command
+ return self::validateBody($request);
+ }
+
+ /**
+ * validates the given api credentials
+ *
+ * @param string $key
+ * @param string $secret
+ *
+ * @throws \Exception
+ * @return boolean
+ */
+ private static function validateAuth($key, $secret)
+ {
+ $sel_stmt = \Froxlor\Database\Database::prepare("
+ SELECT ak.*, a.api_allowed as admin_api_allowed, c.api_allowed as cust_api_allowed, c.deactivated
+ FROM `api_keys` ak
+ LEFT JOIN `panel_admins` a ON a.adminid = ak.adminid
+ LEFT JOIN `panel_customers` c ON c.customerid = ak.customerid
+ WHERE `apikey` = :ak AND `secret` = :as
+ ");
+ $result = \Froxlor\Database\Database::pexecute_first($sel_stmt, array(
+ 'ak' => $key,
+ 'as' => $secret
+ ), true, true);
+ if ($result) {
+ if ($result['apikey'] == $key && $result['secret'] == $secret && ($result['valid_until'] == - 1 || $result['valid_until'] >= time()) && (($result['customerid'] == 0 && $result['admin_api_allowed'] == 1) || ($result['customerid'] > 0 && $result['cust_api_allowed'] == 1 && $result['deactivated'] == 0))) {
+ // get user to check whether api call is allowed
+ if (! empty($result['allowed_from'])) {
+ // @todo allow specification and validating of whole subnets later
+ $ip_list = explode(",", $result['allowed_from']);
+ $access_ip = inet_ntop(inet_pton($_SERVER['REMOTE_ADDR']));
+ if (in_array($access_ip, $ip_list)) {
+ return true;
+ }
+ } else {
+ return true;
+ }
+ }
+ }
+ throw new \Exception("Invalid authorization credentials", 403);
+ }
+
+ /**
+ * validates the given command
+ *
+ * @param array $request
+ *
+ * @return array
+ * @throws \Exception
+ */
+ private static function validateBody($request)
+ {
+ // check body
+ if (! isset($request['body']) || empty($request['body'])) {
+ throw new \Exception("Invalid request body", 400);
+ }
+
+ // check command exists
+ if (! isset($request['body']['command']) || empty($request['body']['command'])) {
+ throw new \Exception("No command given", 400);
+ }
+
+ $command = explode(".", $request['body']['command']);
+
+ if (count($command) != 2) {
+ throw new \Exception("Invalid command", 400);
+ }
+ // simply check for file-existance, as we do not want to use our autoloader because this way
+ // it will recognize non-api classes+methods as valid commands
+ $apiclass = '\\Froxlor\\Api\\Commands\\' . $command[0];
+ if (! class_exists($apiclass) || ! @method_exists($apiclass, $command[1])) {
+ throw new \Exception("Unknown command", 400);
+ }
+ return array(
+ 'command' => array(
+ 'class' => $command[0],
+ 'method' => $command[1]
+ ),
+ 'params' => isset($request['body']['params']) ? $request['body']['params'] : null
+ );
+ }
+}
diff --git a/lib/Froxlor/Api/ResourceEntity.php b/lib/Froxlor/Api/ResourceEntity.php
new file mode 100644
index 00000000..fe3068da
--- /dev/null
+++ b/lib/Froxlor/Api/ResourceEntity.php
@@ -0,0 +1,33 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
+ */
+interface ResourceEntity
+{
+
+ public function listing();
+
+ public function listingCount();
+
+ public function get();
+
+ public function add();
+
+ public function update();
+
+ public function delete();
+}
diff --git a/lib/Froxlor/Bulk/BulkAction.php b/lib/Froxlor/Bulk/BulkAction.php
new file mode 100644
index 00000000..457ef46b
--- /dev/null
+++ b/lib/Froxlor/Bulk/BulkAction.php
@@ -0,0 +1,206 @@
+
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ * @since 0.10.0
+ *
+ */
+
+/**
+ * Abstract Class BulkAction to mass-import entities
+ *
+ * @author Michael Kaufmann (d00p)
+ *
+ */
+abstract class BulkAction
+{
+
+ /**
+ * complete path including filename of file to be imported
+ *
+ * @var string
+ */
+ private $impFile = null;
+
+ /**
+ * api-function to call for addingg entity
+ *
+ * @var string
+ */
+ private $api_call = null;
+
+ /**
+ * api-function parameter names, read from import-file (first line)
+ *
+ * @var array
+ */
+ private $api_params = null;
+
+ /**
+ * errors while importing
+ *
+ * @var array
+ */
+ private $errors = array();
+
+ /**
+ * logged in user
+ *
+ * @var array
+ */
+ protected $userinfo = array();
+
+ /**
+ * class constructor, optionally sets file and customer-id
+ *
+ * @param string $import_file
+ * @param array $userinfo
+ *
+ * @return object BulkAction instance
+ */
+ protected function __construct($import_file = null, $userinfo = array())
+ {
+ if (! empty($import_file)) {
+ $this->impFile = \Froxlor\FileDir::makeCorrectFile($import_file);
+ }
+ $this->userinfo = $userinfo;
+ }
+
+ /**
+ * import the parsed import file data with an optional separator other then semicolon
+ * and offset (maybe for header-line in csv or similar)
+ *
+ * @param string $separator
+ * @param int $offset
+ *
+ * @return array 'all' => amount of records processed, 'imported' => number of imported records
+ */
+ abstract public function doImport($separator = ";", $offset = 0);
+
+ /**
+ * setter for import-file
+ *
+ * @param string $import_file
+ *
+ * @return void
+ */
+ public function setImportFile($import_file = null)
+ {
+ $this->impFile = \Froxlor\FileDir::makeCorrectFile($import_file);
+ }
+
+ /**
+ * return the list of errors
+ *
+ * @return array
+ */
+ public function getErrors()
+ {
+ return $this->errors;
+ }
+
+ /**
+ * setter for api_call
+ *
+ * @param string $api_call
+ *
+ * @return void
+ */
+ protected function setApiCall($api_call = "")
+ {
+ $this->api_call = $api_call;
+ }
+
+ protected function importEntity($data_array = null)
+ {
+ if (empty($data_array)) return null;
+
+ $module = '\\Froxlor\\Api\\Commands\\' . substr($this->api_call, 0, strpos($this->api_call, "."));
+ $function = substr($this->api_call, strpos($this->api_call, ".") + 1);
+
+ $new_data = array();
+ foreach ($this->api_params as $idx => $param) {
+ if (isset($data_array[$idx]) && ! empty($data_array[$idx])) {
+ $new_data[$param] = $data_array[$idx];
+ }
+ }
+
+ $result = null;
+ try {
+ $json_result = $module::getLocal($this->userinfo, $new_data)->$function();
+ $result = json_decode($json_result, true)['data'];
+ } catch (\Exception $e) {
+ $this->errors[] = $e->getMessage();
+ }
+ return ! empty($result);
+ }
+
+ /**
+ * reads in the csv import file and returns an array with
+ * all the entites to be imported
+ *
+ * @param string $separator
+ *
+ * @return array
+ */
+ protected function parseImportFile($separator = ";")
+ {
+ if (empty($this->impFile)) {
+ throw new \Exception("No file was given for import");
+ }
+
+ if (! file_exists($this->impFile)) {
+ throw new \Exception("The file '" . $this->impFile . "' could not be found");
+ }
+
+ if (! is_readable($this->impFile)) {
+ throw new \Exception("Unable to read file '" . $this->impFile . "'");
+ }
+
+ if (empty($separator) || strlen($separator) != 1) {
+ throw new \Exception("Invalid separator specified: '" . $separator . "'");
+ }
+
+ $file_data = array();
+ $is_params_line = true;
+ $fh = @fopen($this->impFile, "r");
+ if ($fh) {
+ while (($line = fgets($fh)) !== false) {
+ $tmp_arr = explode($separator, $line);
+ $data_arr = array();
+ foreach ($tmp_arr as $idx => $data) {
+ if ($is_params_line) {
+ $this->api_params[$idx] = $data;
+ } else {
+ $data_arr[$idx] = $data;
+ }
+ }
+ if ($is_params_line) {
+ $is_params_line = false;
+ continue;
+ }
+ $file_data[] = array_map("trim", $data_arr);
+ }
+ $this->api_params = array_map("trim", $this->api_params);
+ } else {
+ throw new \Exception("Unable to open file '" . $this->impFile . "'");
+ }
+ fclose($fh);
+
+ return $file_data;
+ }
+
+}
diff --git a/lib/Froxlor/Bulk/DomainBulkAction.php b/lib/Froxlor/Bulk/DomainBulkAction.php
new file mode 100644
index 00000000..2c0cf280
--- /dev/null
+++ b/lib/Froxlor/Bulk/DomainBulkAction.php
@@ -0,0 +1,101 @@
+
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ * @since 0.9.33
+ *
+ */
+
+/**
+ * Class DomainBulkAction to mass-import domains for a given customer
+ *
+ * @author Michael Kaufmann (d00p)
+ *
+ */
+class DomainBulkAction extends BulkAction
+{
+
+ /**
+ *
+ * @return object DomainBulkAction instance
+ */
+ public function __construct($import_file = null, $userinfo)
+ {
+ parent::__construct($import_file, $userinfo);
+ $this->setApiCall('Domains.add');
+ }
+
+ /**
+ * import the parsed import file data with an optional separator other then semicolon
+ * and offset (maybe for header-line in csv or similar)
+ *
+ * @param string $separator
+ * @param int $offset
+ *
+ * @return array 'all' => amount of records processed, 'imported' => number of imported records
+ */
+ public function doImport($separator = ";", $offset = 0)
+ {
+ if ($this->userinfo['domains'] == "-1") {
+ $dom_unlimited = true;
+ } else {
+ $dom_unlimited = false;
+ }
+
+ $domains_used = (int) $this->userinfo['domains_used'];
+ $domains_avail = (int) $this->userinfo['domains'];
+
+ if (! is_int($offset) || $offset < 0) {
+ throw new \Exception("Invalid offset specified");
+ }
+
+ try {
+ $domain_array = $this->parseImportFile($separator);
+ } catch (\Exception $e) {
+ throw $e;
+ }
+
+ if (count($domain_array) <= 0) {
+ throw new \Exception("No domains were read from the file.");
+ }
+
+ $global_counter = 0;
+ $import_counter = 0;
+ $note = '';
+ foreach ($domain_array as $idx => $dom) {
+ if ($idx >= $offset) {
+ if ($dom_unlimited || (! $dom_unlimited && $domains_used < $domains_avail)) {
+
+ $result = $this->importEntity($dom);
+ if ($result) {
+ $import_counter ++;
+ $domains_used ++;
+ }
+ } else {
+ $note .= 'You have reached your maximum allocation of domains (' . $domains_avail . ').';
+ break;
+ }
+ }
+ $global_counter ++;
+ }
+
+ return array(
+ 'all' => $global_counter,
+ 'imported' => $import_counter,
+ 'notice' => $note
+ );
+ }
+}
diff --git a/lib/Froxlor/Cli/Action.php b/lib/Froxlor/Cli/Action.php
new file mode 100644
index 00000000..3f079d48
--- /dev/null
+++ b/lib/Froxlor/Cli/Action.php
@@ -0,0 +1,18 @@
+_args = $args;
+ }
+
+ public function getActionName()
+ {
+ return get_called_class();
+ }
+
+ abstract public function run();
+}
diff --git a/lib/Froxlor/Cli/Action/ConfigServicesAction.php b/lib/Froxlor/Cli/Action/ConfigServicesAction.php
new file mode 100644
index 00000000..464e54df
--- /dev/null
+++ b/lib/Froxlor/Cli/Action/ConfigServicesAction.php
@@ -0,0 +1,427 @@
+validate();
+ }
+
+ /**
+ * validates the parsed command line parameters
+ *
+ * @throws \Exception
+ */
+ private function validate()
+ {
+ global $lng;
+
+ $this->checkConfigParam(true);
+ $this->parseConfig();
+
+ require FROXLOR_INSTALL_DIR . '/lib/tables.inc.php';
+
+ include_once FROXLOR_INSTALL_DIR . '/lng/english.lng.php';
+ include_once FROXLOR_INSTALL_DIR . '/lng/lng_references.php';
+
+ if (array_key_exists("import-settings", $this->_args)) {
+ $this->importSettings();
+ }
+
+ if (array_key_exists("create", $this->_args)) {
+ $this->createConfig();
+ } elseif (array_key_exists("apply", $this->_args)) {
+ $this->applyConfig();
+ } elseif (array_key_exists("list-daemons", $this->_args) || array_key_exists("daemon", $this->_args)) {
+ ConfigServicesCmd::printwarn("--list-daemons and --daemon only work together with --apply");
+ }
+ }
+
+ private function importSettings()
+ {
+ if (strtoupper(substr($this->_args["import-settings"], 0, 4)) == 'HTTP') {
+ echo "Settings file seems to be an URL, trying to download" . PHP_EOL;
+ $target = "/tmp/froxlor-import-settings-" . time() . ".json";
+ if (@file_exists($target)) {
+ @unlink($target);
+ }
+ $this->downloadFile($this->_args["import-settings"], $target);
+ $this->_args["import-settings"] = $target;
+ }
+ if (! is_file($this->_args["import-settings"])) {
+ throw new \Exception("Given settings file is not a file");
+ } elseif (! file_exists($this->_args["import-settings"])) {
+ throw new \Exception("Given settings file cannot be found ('" . $this->_args["import-settings"] . "')");
+ } elseif (! is_readable($this->_args["import-settings"])) {
+ throw new \Exception("Given settings file cannot be read ('" . $this->_args["import-settings"] . "')");
+ }
+ $imp_content = file_get_contents($this->_args["import-settings"]);
+ SImExporter::import($imp_content);
+ ConfigServicesCmd::printsucc("Successfully imported settings from '" . $this->_args["import-settings"] . "'");
+ }
+
+ private function createConfig()
+ {
+ $_daemons_config = array(
+ 'distro' => ""
+ );
+
+ $config_dir = FROXLOR_INSTALL_DIR . '/lib/configfiles/';
+ // show list of available distro's
+ $distros = glob($config_dir . '*.xml');
+ // tmp array
+ $distributions_select_data = array();
+
+ //set default os.
+ $os_dist = array('ID' => 'buster');
+ $os_version = array('0' => '10');
+ $os_default = $os_dist['ID'];
+
+ //read os-release
+ if(file_exists('/etc/os-release')) {
+ $os_dist = parse_ini_file('/etc/os-release', false);
+ if(is_array($os_dist) && array_key_exists('ID', $os_dist) && array_key_exists('VERSION_ID', $os_dist)) {
+ $os_version = explode('.',$os_dist['VERSION_ID'])[0];
+ }
+ }
+
+ // read in all the distros
+ foreach ($distros as $_distribution) {
+ // get configparser object
+ $dist = new \Froxlor\Config\ConfigParser($_distribution);
+ // get distro-info
+ $dist_display = $this->getCompleteDistroName($dist);
+ // store in tmp array
+ $distributions_select_data[$dist_display] = str_replace(".xml", "", strtolower(basename($_distribution)));
+
+ //guess if this is the current distro.
+ $ver = explode('.', $dist->distributionVersion)[0];
+ if (strtolower($os_dist['ID']) == strtolower($dist->distributionName) && $os_version == $ver) {
+ $os_default = str_replace(".xml", "", strtolower(basename($_distribution)));
+ }
+ }
+
+ // sort by distribution name
+ ksort($distributions_select_data);
+
+ // list all distributions
+ $mask = "|%-50.50s |%-50.50s |\n";
+ printf($mask, str_repeat("-", 50), str_repeat("-", 50));
+ printf($mask, 'dist', 'name');
+ printf($mask, str_repeat("-", 50), str_repeat("-", 50));
+ foreach ($distributions_select_data as $name => $filename) {
+ printf($mask, $filename, $name);
+ }
+ printf($mask, str_repeat("-", 50), str_repeat("-", 50));
+ echo PHP_EOL;
+
+ while (! in_array($_daemons_config['distro'], $distributions_select_data)) {
+ $_daemons_config['distro'] = ConfigServicesCmd::getInput("choose distribution", $os_default);
+ }
+
+ // go through all services and let user check whether to include it or not
+ $configfiles = new \Froxlor\Config\ConfigParser($config_dir . '/' . $_daemons_config['distro'] . ".xml");
+ $services = $configfiles->getServices();
+
+ foreach ($services as $si => $service) {
+ echo PHP_EOL . "--- " . strtoupper($si) . " ---" . PHP_EOL . PHP_EOL;
+ $_daemons_config[$si] = "";
+
+ $daemons = $service->getDaemons();
+ $mask = "|%-50.50s |%-50.50s |\n";
+ printf($mask, str_repeat("-", 50), str_repeat("-", 50));
+ printf($mask, 'value', 'name');
+ printf($mask, str_repeat("-", 50), str_repeat("-", 50));
+ $default_daemon = "";
+ foreach ($daemons as $di => $dd) {
+ $title = $dd->title;
+ if ($dd->default) {
+ $default_daemon = $di;
+ $title = $title . " (default)";
+ }
+ printf($mask, $di, $title);
+ }
+ printf($mask, "x", "No " . $si);
+ $daemons['x'] = 'x';
+ printf($mask, str_repeat("-", 50), str_repeat("-", 50));
+ echo PHP_EOL;
+ if ($si == 'system') {
+ $_daemons_config[$si] = array();
+ // for the system/other services we need a multiple choice possibility
+ ConfigServicesCmd::println("Select every service you need. Enter empty value when done");
+ $sysservice = "";
+ do {
+ $sysservice = ConfigServicesCmd::getInput("choose service");
+ if (! empty($sysservice)) {
+ $_daemons_config[$si][] = $sysservice;
+ }
+ } while (! empty($sysservice));
+ // add 'cron' as fixed part (doesn't hurt if it exists)
+ if (! in_array('cron', $_daemons_config[$si])) {
+ $_daemons_config[$si][] = 'cron';
+ }
+ } else {
+ // for all others -> only one value
+ while (! array_key_exists($_daemons_config[$si], $daemons)) {
+ $_daemons_config[$si] = ConfigServicesCmd::getInput("choose service", $default_daemon);
+ }
+ }
+ }
+
+ echo PHP_EOL . PHP_EOL;
+ $daemons_config = json_encode($_daemons_config);
+ $output = ConfigServicesCmd::getInput("choose output-filename", "/tmp/froxlor-config-" . date('Ymd') . ".json");
+ file_put_contents($output, $daemons_config);
+ ConfigServicesCmd::printsucc("Successfully generated service-configfile '" . $output . "'");
+ echo PHP_EOL;
+ ConfigServicesCmd::printsucc("You can now apply this config running:" . PHP_EOL . "php " . FROXLOR_INSTALL_DIR . "install/scripts/config-services.php --apply=" . $output);
+ echo PHP_EOL;
+ $proceed = ConfigServicesCmd::getYesNo("Do you want to apply the config now? [y/N]", 0);
+ if ($proceed) {
+ passthru("php " . FROXLOR_INSTALL_DIR . "install/scripts/config-services.php --apply=" . $output);
+ }
+ }
+
+ private function getCompleteDistroName($cparser)
+ {
+ // get distro-info
+ $dist_display = $cparser->distributionName;
+ if ($cparser->distributionCodename != '') {
+ $dist_display .= " " . $cparser->distributionCodename;
+ }
+ if ($cparser->distributionVersion != '') {
+ $dist_display .= " (" . $cparser->distributionVersion . ")";
+ }
+ if ($cparser->deprecated) {
+ $dist_display .= " [deprecated]";
+ }
+ return $dist_display;
+ }
+
+ private function applyConfig()
+ {
+ if (strtoupper(substr($this->_args["apply"], 0, 4)) == 'HTTP') {
+ echo "Config file seems to be an URL, trying to download" . PHP_EOL;
+ $target = "/tmp/froxlor-config-" . time() . ".json";
+ if (@file_exists($target)) {
+ @unlink($target);
+ }
+ $this->downloadFile($this->_args["apply"], $target);
+ $this->_args["apply"] = $target;
+ }
+ if (! is_file($this->_args["apply"])) {
+ throw new \Exception("Given config file is not a file");
+ } elseif (! file_exists($this->_args["apply"])) {
+ throw new \Exception("Given config file cannot be found ('" . $this->_args["apply"] . "')");
+ } elseif (! is_readable($this->_args["apply"])) {
+ throw new \Exception("Given config file cannot be read ('" . $this->_args["apply"] . "')");
+ }
+
+ $config = file_get_contents($this->_args["apply"]);
+ $decoded_config = json_decode($config, true);
+
+ if (array_key_exists("list-daemons", $this->_args)) {
+ $mask = "|%-50.50s |%-50.50s |\n";
+ printf($mask, str_repeat("-", 50), str_repeat("-", 50));
+ printf($mask, 'service', 'daemon');
+ printf($mask, str_repeat("-", 50), str_repeat("-", 50));
+ foreach ($decoded_config as $service => $daemon) {
+ if (is_array($daemon) && count($daemon) > 0) {
+ foreach ($daemon as $sysdaemon) {
+ printf($mask, $service, $sysdaemon);
+ }
+ } else {
+ if ($daemon == 'x') {
+ $daemon = '--- skipped ---';
+ }
+ printf($mask, $service, $daemon);
+ }
+ }
+ printf($mask, str_repeat("-", 50), str_repeat("-", 50));
+ echo PHP_EOL;
+ exit();
+ }
+
+ $only_daemon = null;
+ if (array_key_exists("daemon", $this->_args)) {
+ $only_daemon = $this->_args['daemon'];
+ }
+
+ if (! empty($decoded_config)) {
+ $config_dir = FROXLOR_INSTALL_DIR . '/lib/configfiles/';
+ $configfiles = new \Froxlor\Config\ConfigParser($config_dir . '/' . $decoded_config['distro'] . ".xml");
+ $services = $configfiles->getServices();
+ $replace_arr = $this->getReplacerArray();
+
+ foreach ($services as $si => $service) {
+ echo PHP_EOL . "--- Configuring: " . strtoupper($si) . " ---" . PHP_EOL . PHP_EOL;
+ if (! isset($decoded_config[$si]) || $decoded_config[$si] == 'x') {
+ ConfigServicesCmd::printwarn("Skipping " . strtoupper($si) . " configuration as desired");
+ continue;
+ }
+ $daemons = $service->getDaemons();
+ foreach ($daemons as $di => $dd) {
+ // check for desired service
+ if (($si != 'system' && $decoded_config[$si] != $di) || (is_array($decoded_config[$si]) && ! in_array($di, $decoded_config[$si]))) {
+ continue;
+ }
+ ConfigServicesCmd::println("Configuring '" . $di . "'");
+
+ if (! empty($only_daemon) && $only_daemon != $di) {
+ ConfigServicesCmd::printwarn("Skipping " . $di . " configuration as desired");
+ continue;
+ }
+ // run all cmds
+ $confarr = $dd->getConfig();
+ foreach ($confarr as $action) {
+ switch ($action['type']) {
+ case "install":
+ ConfigServicesCmd::println("Installing required packages");
+ $result = null;
+ passthru(strtr($action['content'], $replace_arr), $result);
+ if (strlen($result) > 1) {
+ echo $result;
+ }
+ break;
+ case "command":
+ exec(strtr($action['content'], $replace_arr));
+ break;
+ case "file":
+ if (array_key_exists('content', $action)) {
+ ConfigServicesCmd::printwarn("Creating file '" . $action['name'] . "'");
+ file_put_contents($action['name'], trim(strtr($action['content'], $replace_arr)));
+ } elseif (array_key_exists('subcommands', $action)) {
+ foreach ($action['subcommands'] as $fileaction) {
+ if (array_key_exists('execute', $fileaction) && $fileaction['execute'] == "pre") {
+ exec(strtr($fileaction['content'], $replace_arr));
+ } elseif (array_key_exists('execute', $fileaction) && $fileaction['execute'] == "post") {
+ exec(strtr($fileaction['content'], $replace_arr));
+ } elseif ($fileaction['type'] == 'file') {
+ ConfigServicesCmd::printwarn("Creating file '" . $fileaction['name'] . "'");
+ file_put_contents($fileaction['name'], trim(strtr($fileaction['content'], $replace_arr)));
+ }
+ }
+ }
+ break;
+ }
+ }
+ }
+ }
+ // set is_configured flag
+ Settings::Set('panel.is_configured', '1', true);
+ // run cronjob at the end to ensure configs are all up to date
+ exec('php ' . FROXLOR_INSTALL_DIR . '/scripts/froxlor_master_cronjob.php --force');
+ // and done
+ ConfigServicesCmd::printsucc("All services have been configured");
+ } else {
+ ConfigServicesCmd::printerr("Unable to decode given JSON file");
+ }
+ }
+
+ private function getReplacerArray()
+ {
+ $customer_tmpdir = '/tmp/';
+ if (Settings::Get('system.mod_fcgid') == '1' && Settings::Get('system.mod_fcgid_tmpdir') != '') {
+ $customer_tmpdir = Settings::Get('system.mod_fcgid_tmpdir');
+ } elseif (Settings::Get('phpfpm.enabled') == '1' && Settings::Get('phpfpm.tmpdir') != '') {
+ $customer_tmpdir = Settings::Get('phpfpm.tmpdir');
+ }
+
+ // try to convert namserver hosts to ip's
+ $ns_ips = "";
+ if (Settings::Get('system.nameservers') != '') {
+ $nameservers = explode(',', Settings::Get('system.nameservers'));
+ foreach ($nameservers as $nameserver) {
+ $nameserver = trim($nameserver);
+ $nameserver_ips = \Froxlor\PhpHelper::gethostbynamel6($nameserver);
+ if (is_array($nameserver_ips) && count($nameserver_ips) > 0) {
+ $ns_ips .= implode(",", $nameserver_ips);
+ }
+ }
+ }
+
+ Database::needSqlData();
+ $sql = Database::getSqlData();
+
+ $replace_arr = array(
+ '' => $sql['user'],
+ '' => $sql['passwd'],
+ '' => $sql['db'],
+ '' => $sql['host'],
+ '' => isset($sql['socket']) ? $sql['socket'] : null,
+ '' => Settings::Get('system.hostname'),
+ '' => Settings::Get('system.ipaddress'),
+ '' => Settings::Get('system.nameservers'),
+ '' => $ns_ips,
+ '' => Settings::Get('system.axfrservers'),
+ '' => Settings::Get('system.vmail_homedir'),
+ '' => Settings::Get('system.vmail_uid'),
+ '' => Settings::Get('system.vmail_gid'),
+ '' => (Settings::Get('system.use_ssl') == '1') ? 'imaps pop3s' : '',
+ '' => \Froxlor\FileDir::makeCorrectDir($customer_tmpdir),
+ '' => \Froxlor\FileDir::makeCorrectDir(FROXLOR_INSTALL_DIR),
+ '' => \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.bindconf_directory')),
+ '' => Settings::Get('system.apachereload_command'),
+ '' => \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.logfiles_directory')),
+ '' => \Froxlor\FileDir::makeCorrectDir(Settings::Get('phpfpm.fastcgi_ipcdir')),
+ '' => Settings::Get('system.httpgroup')
+ );
+ return $replace_arr;
+ }
+
+ private function parseConfig()
+ {
+ define('FROXLOR_INSTALL_DIR', $this->_args['froxlor-dir']);
+ if (! class_exists('\\Froxlor\\Database\\Database')) {
+ throw new \Exception("Could not find froxlor's Database class. Is froxlor really installed to '" . FROXLOR_INSTALL_DIR . "'?");
+ }
+ if (! file_exists(FROXLOR_INSTALL_DIR . '/lib/userdata.inc.php')) {
+ throw new \Exception("Could not find froxlor's userdata.inc.php file. You should use this script only with a fully installed and setup froxlor system.");
+ }
+ }
+
+ private function checkConfigParam($needed = false)
+ {
+ if ($needed) {
+ if (! isset($this->_args["froxlor-dir"])) {
+ $this->_args["froxlor-dir"] = \Froxlor\Froxlor::getInstallDir();
+ } elseif (! is_dir($this->_args["froxlor-dir"])) {
+ throw new \Exception("Given --froxlor-dir parameter is not a directory");
+ } elseif (! file_exists($this->_args["froxlor-dir"])) {
+ throw new \Exception("Given froxlor directory cannot be found ('" . $this->_args["froxlor-dir"] . "')");
+ } elseif (! is_readable($this->_args["froxlor-dir"])) {
+ throw new \Exception("Given froxlor direcotry cannot be read ('" . $this->_args["froxlor-dir"] . "')");
+ }
+ }
+ }
+
+ private function downloadFile($src, $dest)
+ {
+ set_time_limit(0);
+ // This is the file where we save the information
+ $fp = fopen($dest, 'w+');
+ // Here is the file we are downloading, replace spaces with %20
+ $ch = curl_init(str_replace(" ", "%20", $src));
+ curl_setopt($ch, CURLOPT_TIMEOUT, 50);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+ // write curl response to file
+ curl_setopt($ch, CURLOPT_FILE, $fp);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+ // get curl response
+ curl_exec($ch);
+ curl_close($ch);
+ fclose($fp);
+ }
+}
diff --git a/lib/Froxlor/Cli/Action/SwitchServerIpAction.php b/lib/Froxlor/Cli/Action/SwitchServerIpAction.php
new file mode 100644
index 00000000..d4213eb4
--- /dev/null
+++ b/lib/Froxlor/Cli/Action/SwitchServerIpAction.php
@@ -0,0 +1,186 @@
+validate();
+ }
+
+ /**
+ * validates the parsed command line parameters
+ *
+ * @throws \Exception
+ */
+ private function validate()
+ {
+ $need_config = false;
+ if (array_key_exists("list", $this->_args) || array_key_exists("switch", $this->_args)) {
+ $need_config = true;
+ }
+
+ $this->checkConfigParam($need_config);
+
+ $this->parseConfig();
+
+ if (array_key_exists("list", $this->_args)) {
+ $this->listIPs();
+ }
+ if (array_key_exists("switch", $this->_args)) {
+ $this->switchIPs();
+ }
+ }
+
+ private function listIPs()
+ {
+ $sel_stmt = Database::prepare("SELECT * FROM panel_ipsandports ORDER BY ip ASC, port ASC");
+ Database::pexecute($sel_stmt);
+ $ips = $sel_stmt->fetchAll(\PDO::FETCH_ASSOC);
+ $mask = "|%-10.10s |%-50.50s | %10.10s |\n";
+ printf($mask, str_repeat("-", 10), str_repeat("-", 50), str_repeat("-", 10));
+ printf($mask, 'id', 'IP address', 'port');
+ printf($mask, str_repeat("-", 10), str_repeat("-", 50), str_repeat("-", 10));
+ foreach ($ips as $ipdata) {
+ printf($mask, $ipdata['id'], $ipdata['ip'], $ipdata['port']);
+ }
+ printf($mask, str_repeat("-", 10), str_repeat("-", 50), str_repeat("-", 10));
+ echo PHP_EOL . PHP_EOL;
+ }
+
+ private function switchIPs()
+ {
+ $ip_list = $this->_args['switch'];
+
+ if (empty($ip_list) || is_bool($ip_list)) {
+ throw new \Exception("No paramters given for --switch action.");
+ }
+
+ $ips_to_switch = array();
+ $ip_list = explode(" ", $ip_list);
+ foreach ($ip_list as $ips_combo) {
+ $ip_pair = explode(",", $ips_combo);
+ if (count($ip_pair) != 2) {
+ throw new \Exception("Invalid parameter given for --switch");
+ } else {
+ if (filter_var($ip_pair[0], FILTER_VALIDATE_IP) == false) {
+ throw new \Exception("Invalid source ip address: " . $ip_pair[0]);
+ }
+ if (filter_var($ip_pair[1], FILTER_VALIDATE_IP) == false) {
+ throw new \Exception("Invalid target ip address: " . $ip_pair[1]);
+ }
+ if ($ip_pair[0] == $ip_pair[1]) {
+ throw new \Exception("Source and target ip address are equal");
+ }
+ }
+ $ips_to_switch[] = $ip_pair;
+ }
+
+ if (count($ips_to_switch) > 0) {
+ $check_stmt = Database::prepare("SELECT `id` FROM panel_ipsandports WHERE `ip` = :newip");
+ $upd_stmt = Database::prepare("UPDATE panel_ipsandports SET `ip` = :newip WHERE `ip` = :oldip");
+
+ // system.ipaddress
+ $check_sysip_stmt = Database::prepare("SELECT `value` FROM `panel_settings` WHERE `settinggroup` = 'system' and `varname` = 'ipaddress'");
+ $check_sysip = Database::pexecute_first($check_sysip_stmt);
+
+ // system.mysql_access_host
+ $check_mysqlip_stmt = Database::prepare("SELECT `value` FROM `panel_settings` WHERE `settinggroup` = 'system' and `varname` = 'mysql_access_host'");
+ $check_mysqlip = Database::pexecute_first($check_mysqlip_stmt);
+
+ // system.axfrservers
+ $check_axfrip_stmt = Database::prepare("SELECT `value` FROM `panel_settings` WHERE `settinggroup` = 'system' and `varname` = 'axfrservers'");
+ $check_axfrip = Database::pexecute_first($check_axfrip_stmt);
+
+ foreach ($ips_to_switch as $ip_pair) {
+ echo "Switching IP \033[1m" . $ip_pair[0] . "\033[0m to IP \033[1m" . $ip_pair[1] . "\033[0m" . PHP_EOL;
+
+ $ip_check = Database::pexecute_first($check_stmt, array(
+ 'newip' => $ip_pair[1]
+ ));
+ if ($ip_check) {
+ SwitchServerIpCmd::printwarn("Note: " . $ip_pair[0] . " not updated to " . $ip_pair[1] . " - IP already exists in froxlor's database");
+ continue;
+ }
+
+ Database::pexecute($upd_stmt, array(
+ 'newip' => $ip_pair[1],
+ 'oldip' => $ip_pair[0]
+ ));
+ $rows_updated = $upd_stmt->rowCount();
+
+ if ($rows_updated == 0) {
+ SwitchServerIpCmd::printwarn("Note: " . $ip_pair[0] . " not updated to " . $ip_pair[1] . " (possibly no entry found in froxlor database. Use --list to see what IP addresses are added in froxlor");
+ }
+
+ // check whether the system.ipaddress needs updating
+ if ($check_sysip['value'] == $ip_pair[0]) {
+ $upd2_stmt = Database::prepare("UPDATE `panel_settings` SET `value` = :newip WHERE `settinggroup` = 'system' and `varname` = 'ipaddress'");
+ Database::pexecute($upd2_stmt, array(
+ 'newip' => $ip_pair[1]
+ ));
+ SwitchServerIpCmd::printsucc("Updated system-ipaddress from '" . $ip_pair[0] . "' to '" . $ip_pair[1] . "'");
+ }
+
+ // check whether the system.mysql_access_host needs updating
+ if (strstr($check_mysqlip['value'], $ip_pair[0]) !== false) {
+ $new_mysqlip = str_replace($ip_pair[0], $ip_pair[1], $check_mysqlip['value']);
+ $upd2_stmt = Database::prepare("UPDATE `panel_settings` SET `value` = :newmysql WHERE `settinggroup` = 'system' and `varname` = 'mysql_access_host'");
+ Database::pexecute($upd2_stmt, array(
+ 'newmysql' => $new_mysqlip
+ ));
+ SwitchServerIpCmd::printsucc("Updated mysql_access_host from '" . $check_mysqlip['value'] . "' to '" . $new_mysqlip . "'");
+ }
+
+ // check whether the system.axfrservers needs updating
+ if (strstr($check_axfrip['value'], $ip_pair[0]) !== false) {
+ $new_axfrip = str_replace($ip_pair[0], $ip_pair[1], $check_axfrip['value']);
+ $upd2_stmt = Database::prepare("UPDATE `panel_settings` SET `value` = :newaxfr WHERE `settinggroup` = 'system' and `varname` = 'axfrservers'");
+ Database::pexecute($upd2_stmt, array(
+ 'newaxfr' => $new_axfrip
+ ));
+ SwitchServerIpCmd::printsucc("Updated axfrservers from '" . $check_axfrip['value'] . "' to '" . $new_axfrip . "'");
+ }
+ }
+ }
+
+ echo PHP_EOL;
+ SwitchServerIpCmd::printwarn("*** ATTENTION *** Remember to replace IP addresses in configuration files if used anywhere.");
+ SwitchServerIpCmd::printsucc("IP addresses updated");
+ }
+
+ private function parseConfig()
+ {
+ define('FROXLOR_INSTALL_DIR', $this->_args['froxlor-dir']);
+ if (! class_exists('\\Froxlor\\Database\\Database')) {
+ throw new \Exception("Could not find froxlor's Database class. Is froxlor really installed to '" . FROXLOR_INSTALL_DIR . "'?");
+ }
+ if (! file_exists(FROXLOR_INSTALL_DIR . '/lib/userdata.inc.php')) {
+ throw new \Exception("Could not find froxlor's userdata.inc.php file. You should use this script only with a fully installed and setup froxlor system.");
+ }
+ }
+
+ private function checkConfigParam($needed = false)
+ {
+ if ($needed) {
+ if (! isset($this->_args["froxlor-dir"])) {
+ $this->_args["froxlor-dir"] = \Froxlor\Froxlor::getInstallDir();
+ } elseif (! is_dir($this->_args["froxlor-dir"])) {
+ throw new \Exception("Given --froxlor-dir parameter is not a directory");
+ } elseif (! file_exists($this->_args["froxlor-dir"])) {
+ throw new \Exception("Given froxlor directory cannot be found ('" . $this->_args["froxlor-dir"] . "')");
+ } elseif (! is_readable($this->_args["froxlor-dir"])) {
+ throw new \Exception("Given froxlor direcotry cannot be read ('" . $this->_args["froxlor-dir"] . "')");
+ }
+ }
+ }
+}
diff --git a/lib/Froxlor/Cli/CmdLineHandler.php b/lib/Froxlor/Cli/CmdLineHandler.php
new file mode 100644
index 00000000..1cb3fe46
--- /dev/null
+++ b/lib/Froxlor/Cli/CmdLineHandler.php
@@ -0,0 +1,216 @@
+ (2018-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ */
+abstract class CmdLineHandler
+{
+
+ /**
+ * internal variable for passed arguments
+ *
+ * @var array
+ */
+ private static $args = null;
+
+ /**
+ * Action object read from commandline/config
+ *
+ * @var \Froxlor\Cli\Action
+ */
+ private $action = null;
+
+ /**
+ * Returns a CmdLineHandler object with given
+ * arguments from command line
+ *
+ * @param int $argc
+ * @param array $argv
+ *
+ * @return CmdLineHandler
+ */
+ public static function processParameters($argc, $argv)
+ {
+ $me = get_called_class();
+ return new $me($argc, $argv);
+ }
+
+ /**
+ * class constructor, validates the command line parameters
+ * and sets the Action-object if valid
+ *
+ * @param int $argc
+ * @param string[] $argv
+ *
+ * @return null
+ * @throws \Exception
+ */
+ private function __construct($argc, $argv)
+ {
+ self::$args = $this->parseArgs($argv);
+ $this->action = $this->createAction();
+ $this->action->run();
+ }
+
+ /**
+ * Parses the arguments given via the command line;
+ * three types are supported:
+ * 1.
+ * --parm1 or --parm2=value
+ * 2. -xyz (multiple switches in one) or -a=value
+ * 3. parm1 parm2
+ *
+ * The 1. will be mapped as
+ * ["parm1"] => true, ["parm2"] => "value"
+ * The 2. as
+ * ["x"] => true, ["y"] => true, ["z"] => true, ["a"] => "value"
+ * And the 3. as
+ * [0] => "parm1", [1] => "parm2"
+ *
+ * @param array $argv
+ *
+ * @return array
+ */
+ private function parseArgs($argv)
+ {
+ array_shift($argv);
+ $o = array();
+ foreach ($argv as $a) {
+ if (substr($a, 0, 2) == '--') {
+ $eq = strpos($a, '=');
+ if ($eq !== false) {
+ $o[substr($a, 2, $eq - 2)] = substr($a, $eq + 1);
+ } else {
+ $k = substr($a, 2);
+ if (! isset($o[$k])) {
+ $o[$k] = true;
+ }
+ }
+ } elseif (substr($a, 0, 1) == '-') {
+ if (substr($a, 2, 1) == '=') {
+ $o[substr($a, 1, 1)] = substr($a, 3);
+ } else {
+ foreach (str_split(substr($a, 1)) as $k) {
+ if (! isset($o[$k])) {
+ $o[$k] = true;
+ }
+ }
+ }
+ } else {
+ $o[] = $a;
+ }
+ }
+ return $o;
+ }
+
+ /**
+ * Creates an Action-Object for the Action-Handler
+ *
+ * @return \Froxlor\Cli\Action
+ * @throws \Exception
+ */
+ private function createAction()
+ {
+
+ // Test for help-switch
+ if (empty(self::$args) || array_key_exists("help", self::$args) || array_key_exists("h", self::$args)) {
+ static::printHelp();
+ // end of execution
+ }
+ // check if no unknown parameters are present
+ foreach (self::$args as $arg => $value) {
+
+ if (is_numeric($arg)) {
+ throw new \Exception("Unknown parameter '" . $value . "' in argument list");
+ } elseif (! in_array($arg, static::$params) && ! in_array($arg, static::$switches)) {
+ throw new \Exception("Unknown parameter '" . $arg . "' in argument list");
+ }
+ }
+
+ // set debugger switch
+ if (isset(self::$args["d"]) && self::$args["d"] == true) {
+ // Debugger::getInstance()->setEnabled(true);
+ // Debugger::getInstance()->debug("debug output enabled");
+ }
+
+ return new static::$action_class(self::$args);
+ }
+
+ public static function getInput($prompt = "#", $default = "")
+ {
+ if (! empty($default)) {
+ $prompt .= " [" . $default . "]";
+ }
+ $result = readline($prompt . ":");
+ if (empty($result) && ! empty($default)) {
+ $result = $default;
+ }
+ return mb_strtolower($result);
+ }
+
+ public static function getYesNo($prompt = "#", $default = null)
+ {
+ $value = null;
+ $_v = null;
+
+ while (true) {
+ $_v = self::getInput($prompt);
+
+ if (strtolower($_v) == 'y' || strtolower($_v) == 'yes') {
+ $value = 1;
+ break;
+ } elseif (strtolower($_v) == 'n' || strtolower($_v) == 'no') {
+ $value = 0;
+ break;
+ } else {
+ if ($_v == '' && $default != null) {
+ $value = $default;
+ break;
+ } else {
+ echo "Sorry, response " . $_v . " not understood. Please enter 'yes' or 'no'\n";
+ $value = null;
+ continue;
+ }
+ }
+ }
+
+ return $value;
+ }
+
+ public static function println($msg = "")
+ {
+ print $msg . PHP_EOL;
+ }
+
+ private static function printcolor($msg = "", $color = "0")
+ {
+ print "\033[" . $color . "m" . $msg . "\033[0m" . PHP_EOL;
+ }
+
+ public static function printerr($msg = "")
+ {
+ self::printcolor($msg, "31");
+ }
+
+ public static function printsucc($msg = "")
+ {
+ self::printcolor($msg, "32");
+ }
+
+ public static function printwarn($msg = "")
+ {
+ self::printcolor($msg, "33");
+ }
+}
diff --git a/lib/Froxlor/Cli/ConfigServicesCmd.php b/lib/Froxlor/Cli/ConfigServicesCmd.php
new file mode 100644
index 00000000..aec0e7f9
--- /dev/null
+++ b/lib/Froxlor/Cli/ConfigServicesCmd.php
@@ -0,0 +1,79 @@
+ (2018-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ */
+class ConfigServicesCmd extends CmdLineHandler
+{
+
+ /**
+ * list of valid switches
+ *
+ * @var array
+ */
+ public static $switches = array(
+ 'h'
+ );
+
+ /**
+ * list of valid parameters
+ *
+ * @var array
+ */
+ public static $params = array(
+ 'create',
+ 'apply',
+ 'import-settings',
+ 'daemon',
+ 'list-daemons',
+ 'froxlor-dir',
+ 'help'
+ );
+
+ public static $action_class = '\\Froxlor\\Cli\\Action\\ConfigServicesAction';
+
+ public static function printHelp()
+ {
+ self::println("");
+ self::println("Help / command line parameters:");
+ self::println("");
+ // commands
+ self::println("--create\t\tlets you create a services list configuration for the 'apply' command");
+ self::println("");
+ self::println("--apply\t\t\tconfigure your services by given configuration file. To create one run the --create command");
+ self::println("\t\t\tExample: --apply=/path/to/my-config.json or --apply=http://domain.tld/my-config.json");
+ self::println("");
+ self::println("--list-daemons\t\tOutput the services that are going to be configured using a given config file. No services will be configured.");
+ self::println("\t\t\tExample: --apply=/path/to/my-config.json --list-daemons");
+ self::println("");
+ self::println("--daemon\t\tWhen running --apply you can specify a daemon. This will be the only service that gets configured");
+ self::println("\t\t\tExample: --apply=/path/to/my-config.json --daemon=apache24");
+ self::println("");
+ self::println("--import-settings\tImport settings from another froxlor installation. This should be done prior to running --apply or alternatively in the same command together.");
+ self::println("\t\t\tExample: --import-settings=/path/to/Froxlor_settings-[version]-[dbversion]-[date].json or --import-settings=http://domain.tld/Froxlor_settings-[version]-[dbversion]-[date].json");
+ self::println("");
+ self::println("--froxlor-dir\t\tpath to froxlor installation");
+ self::println("\t\t\tExample: --froxlor-dir=/var/www/froxlor/");
+ self::println("");
+ self::println("--help\t\t\tshow help screen (this)");
+ self::println("");
+ // switches
+ // self::println("-d\t\t\tenable debug output");
+ self::println("-h\t\t\tsame as --help");
+ self::println("");
+
+ die(); // end of execution
+ }
+}
diff --git a/lib/Froxlor/Cli/SwitchServerIpCmd.php b/lib/Froxlor/Cli/SwitchServerIpCmd.php
new file mode 100644
index 00000000..6839b765
--- /dev/null
+++ b/lib/Froxlor/Cli/SwitchServerIpCmd.php
@@ -0,0 +1,68 @@
+ (2018-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ */
+class SwitchServerIpCmd extends CmdLineHandler
+{
+
+ /**
+ * list of valid switches
+ *
+ * @var array
+ */
+ public static $switches = array(
+ 'h'
+ );
+
+ /**
+ * list of valid parameters
+ *
+ * @var array
+ */
+ public static $params = array(
+ 'switch',
+ 'list',
+ 'froxlor-dir',
+ 'help'
+ );
+
+ public static $action_class = '\\Froxlor\\Cli\\Action\\SwitchServerIpAction';
+
+ public static function printHelp()
+ {
+ self::println("");
+ self::println("Help / command line parameters:");
+ self::println("");
+ // commands
+ self::println("--switch\t\tlets you switch ip-address A with ip-address B");
+ self::println("\t\t\tExample: --switch=A,B");
+ self::println("\t\t\tExample: --switch=\"A1,B1 A2,B2 A3,B3 ...\"");
+ self::println("");
+ self::println("--list\t\t\tshow all currently used ip-addresses in froxlor");
+ self::println("");
+ self::println("--froxlor-dir\t\tpath to froxlor installation");
+ self::println("\t\t\tExample: --froxlor-dir=/var/www/froxlor/");
+ self::println("");
+ self::println("--help\t\t\tshow help screen (this)");
+ self::println("");
+ // switches
+ // self::println("-d\t\t\tenable debug output");
+ self::println("-h\t\t\tsame as --help");
+ self::println("");
+
+ die(); // end of execution
+ }
+}
diff --git a/lib/Froxlor/Config/ConfigDaemon.php b/lib/Froxlor/Config/ConfigDaemon.php
new file mode 100644
index 00000000..f1de8b0e
--- /dev/null
+++ b/lib/Froxlor/Config/ConfigDaemon.php
@@ -0,0 +1,560 @@
+
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Classes
+ *
+ * @since 0.9.34
+ */
+
+/**
+ * Class ConfigDaemon
+ *
+ * Parses a distributions XML - file and gives access to the configuration
+ * Not to be used directly
+ *
+ * @copyright (c) the authors
+ * @author Florian Aders
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Classes
+ */
+class ConfigDaemon
+{
+
+ /**
+ * Holding the commands for this daemon
+ *
+ * @var array
+ */
+ private $orders = array();
+
+ /**
+ * Store the parsed SimpleXMLElement for usage
+ *
+ * @var \SimpleXMLElement
+ */
+ private $fullxml;
+
+ /**
+ * Memorize if we already parsed the XML
+ *
+ * @var bool
+ */
+ private $isparsed = false;
+
+ /**
+ * Sub - area of the full - XML only holding the daemon - data we are interessted in
+ *
+ * @var \SimpleXMLElement
+ */
+ private $daemon;
+
+ /**
+ * xpath leading to this daemon in the full XML
+ *
+ * @var string
+ */
+ private $xpath;
+
+ /**
+ * cache of sql-data if used
+ */
+ private $sqldata_cache = null;
+
+ /**
+ * Human - readable title of this service
+ *
+ * @var string
+ */
+ public $title;
+
+ /**
+ * Whether this is the default daemon of the service-category
+ *
+ * @var boolean
+ */
+ public $default;
+
+ public function __construct($xml, $xpath)
+ {
+ $this->fullxml = $xml;
+ $this->xpath = $xpath;
+ $this->daemon = $this->fullxml->xpath($this->xpath);
+ $attributes = $this->daemon[0]->attributes();
+ if ($attributes['title'] != '') {
+ $this->title = $this->parseContent((string) $attributes['title']);
+ }
+ if (isset($attributes['default'])) {
+ $this->default = ($attributes['default'] == true);
+ }
+ }
+
+ /**
+ * Parse the XML and populate $this->orders
+ *
+ * @return bool
+ */
+ private function parse()
+ {
+ // We only want to parse the stuff one time
+ if ($this->isparsed == true) {
+ return true;
+ }
+
+ $preparsed = array();
+ // First: let's push everything into an array and expand all includes
+ foreach ($this->daemon[0]->children() as $order) {
+ switch ((string) $order->getName()) {
+ case "install":
+ case "file":
+ case "command":
+ // Normal stuff, just add it to the preparsed - array
+ $preparsed[] = $order;
+ break;
+ case "include":
+ // Includes, get the part we want via xpath
+ $includes = $this->fullxml->xpath((string) $order);
+ foreach ($includes[0] as $include) {
+ // The "include" is also a child, so just skip it, would make a mess later
+ if ((string) $include->getName() == 'include') {
+ continue;
+ }
+ $preparsed[] = $include;
+ }
+ break;
+ // The next 3 are groupings, MUST come first in this to work properly
+ case "commands":
+ case "files":
+ case "installs":
+ // Hold the results
+ $visibility = 1;
+ foreach ($order->children() as $child) {
+ switch ((string) $child->getName()) {
+ case "visibility":
+ $visibility += $this->checkVisibility($child);
+ break;
+ case "install":
+ case "file":
+ case "command":
+ if ($visibility > 0) {
+ $preparsed[] = $child;
+ }
+ break;
+ case "include":
+ // Includes, get the part we want via xpath
+ $includes = $this->fullxml->xpath((string) $child);
+ foreach ($includes[0] as $include) {
+ // The "include" is also a child, so just skip it, would make a mess later
+ if ((string) $include->getName() == 'include') {
+ continue;
+ }
+ $preparsed[] = $include;
+ }
+ break;
+ }
+ }
+ break;
+ }
+ }
+
+ // Go through every preparsed order and evaluate what should happen to it
+ foreach ($preparsed as $order) {
+ $parsedorder = $this->parseOrder($order);
+ // We got an array (= valid order) and the array already has a type -> add to stack
+ if (is_array($parsedorder) && array_key_exists('type', $parsedorder)) {
+ $this->orders[] = $parsedorder;
+ // We got an array, but no type, means we got multiple orders back, at them to the stack one at a time
+ } elseif (is_array($parsedorder)) {
+ foreach ($parsedorder as $neworder) {
+ $this->orders[] = $neworder;
+ }
+ }
+ }
+
+ // Switch flag to indicate we parsed our data
+ $this->isparsed = true;
+ return true;
+ }
+
+ /**
+ * Get config for this daemon
+ *
+ * The returned array will be an array of array, each sub-array looking like this:
+ * array('type' => 'install|file|command', 'content' => '')
+ * If the type is "file", an additional "name" - element will be added to the array
+ * To configure a daemon, the steps in the array must be followed in order
+ *
+ * @return array
+ */
+ public function getConfig()
+ {
+ $this->parse();
+ return $this->orders;
+ }
+
+ /**
+ * Parse a single order and return it in a format for easier usage
+ *
+ * @param
+ * SimpleXMLElement object holding a single order from the distribution - XML
+ * @return array|string
+ */
+ private function parseOrder($order)
+ {
+ $attributes = array();
+ foreach ($order->attributes() as $key => $value) {
+ $attributes[(string) $key] = (string) $value;
+ }
+
+ $parsedorder = '';
+ switch ((string) $order->getName()) {
+ case "file":
+ $parsedorder = $this->parseFile($order, $attributes);
+ break;
+ case "command":
+ $parsedorder = $this->parseCommand($order, $attributes);
+ break;
+ case "install":
+ $parsedorder = $this->parseInstall($order, $attributes);
+ break;
+ default:
+ throw new \Exception('Invalid order: ' . (string) $order->getName());
+ }
+
+ return $parsedorder;
+ }
+
+ /**
+ * Parse a install - order and return it in a format for easier usage
+ *
+ * @param
+ * SimpleXMLElement object holding a single install from the distribution - XML
+ * @return array|string
+ */
+ private function parseInstall($order, $attributes)
+ {
+ // No sub - elements, so the content can be returned directly
+ if ($order->count() == 0) {
+ return array(
+ 'type' => 'install',
+ 'content' => $this->parseContent(trim((string) $order))
+ );
+ }
+
+ // Hold the results
+ $visibility = 1;
+ $content = '';
+ foreach ($order->children() as $child) {
+ switch ((string) $child->getName()) {
+ case "visibility":
+ $visibility += $this->checkVisibility($child);
+ break;
+ case "content":
+ $content = trim((string) $child);
+ break;
+ }
+ }
+
+ if ($visibility > 0) {
+ return array(
+ 'type' => 'install',
+ 'content' => $this->parseContent($content)
+ );
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Parse a command - order and return it in a format for easier usage
+ *
+ * @param
+ * SimpleXMLElement object holding a single command from the distribution - XML
+ * @return array|string
+ */
+ private function parseCommand($order, $attributes)
+ {
+ // No sub - elements, so the content can be returned directly
+ if ($order->count() == 0) {
+ return array(
+ 'type' => 'command',
+ 'content' => $this->parseContent(trim((string) $order))
+ );
+ }
+
+ // Hold the results
+ $visibility = 1;
+ $content = '';
+ foreach ($order->children() as $child) {
+ switch ((string) $child->getName()) {
+ case "visibility":
+ $visibility += $this->checkVisibility($child);
+ break;
+ case "content":
+ $content = trim((string) $child);
+ break;
+ }
+ }
+
+ if ($visibility > 0) {
+ return array(
+ 'type' => 'command',
+ 'content' => $this->parseContent($content)
+ );
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Parse a file - order and return it in a format for easier usage
+ *
+ * @param
+ * SimpleXMLElement object holding a single file from the distribution - XML
+ * @return array|string
+ */
+ private function parseFile($order, $attributes)
+ {
+ $visibility = 1;
+ // No sub - elements, so the content can be returned directly
+ if ($order->count() == 0) {
+ $content = (string) $order;
+ } else {
+ // Hold the results
+ foreach ($order->children() as $child) {
+ switch ((string) $child->getName()) {
+ case "visibility":
+ $visibility += $this->checkVisibility($child);
+ break;
+ case "content":
+ $content = (string) $child;
+ break;
+ }
+ }
+ }
+
+ $return = array();
+ // Check if the original file should be backupped
+ // @TODO: Maybe have a backup - location somewhere central?
+ // @TODO: Use IO - class
+ if (array_key_exists('backup', $attributes)) {
+ if (array_key_exists('mode', $attributes) && $attributes['mode'] == 'append') {
+ $cmd = 'cp';
+ } else {
+ $cmd = 'mv';
+ }
+ $return[] = array(
+ 'type' => 'command',
+ 'content' => $cmd . ' "' . $this->parseContent($attributes['name']) . '" "' . $this->parseContent($attributes['name']) . '.frx.bak"',
+ 'execute' => "pre"
+ );
+ }
+
+ // Now the content of the file can be written
+ if (isset($attributes['mode'])) {
+ $return[] = array(
+ 'type' => 'file',
+ 'content' => $this->parseContent($content),
+ 'name' => $this->parseContent($attributes['name']),
+ 'mode' => $this->parseContent($attributes['mode'])
+ );
+ } else {
+ $return[] = array(
+ 'type' => 'file',
+ 'content' => $this->parseContent($content),
+ 'name' => $this->parseContent($attributes['name'])
+ );
+ }
+
+ // Let's check if the mode of the file should be changed
+ if (array_key_exists('chmod', $attributes)) {
+ $return[] = array(
+ 'type' => 'command',
+ 'content' => 'chmod ' . $attributes['chmod'] . ' "' . $this->parseContent($attributes['name']) . '"',
+ 'execute' => "post"
+ );
+ }
+
+ // Let's check if the owner of the file should be changed
+ if (array_key_exists('chown', $attributes)) {
+ $return[] = array(
+ 'type' => 'command',
+ 'content' => 'chown ' . $attributes['chown'] . ' "' . $this->parseContent($attributes['name']) . '"',
+ 'execute' => "post"
+ );
+ }
+
+ // If we have more than 1 element, we want to group this stuff for easier processing later
+ if (count($return) > 1) {
+ $return = array(
+ 'type' => 'file',
+ 'subcommands' => $return,
+ 'name' => $this->parseContent($attributes['name'])
+ );
+ }
+
+ if ($visibility > 0) {
+ return $return;
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Replace placeholders with content
+ *
+ * @param string $content
+ * @return string $content w/o placeholder
+ */
+ private function parseContent($content)
+ {
+ $content = preg_replace_callback('/\{\{(.*)\}\}/Ui', function ($matches) {
+ $match = null;
+ if (preg_match('/^settings\.(.*)$/', $matches[1], $match)) {
+ return \Froxlor\Settings::Get($match[1]);
+ } elseif (preg_match('/^lng\.(.*)(?:\.(.*)(?:\.(.*)))$/U', $matches[1], $match)) {
+ global $lng;
+ if (isset($match[1]) && $match[1] != '' && isset($match[2]) && $match[2] != '' && isset($match[3]) && $match[3] != '') {
+ return $lng[$match[1]][$match[2]][$match[3]];
+ } elseif (isset($match[1]) && $match[1] != '' && isset($match[2]) && $match[2] != '') {
+ return $lng[$match[1]][$match[2]];
+ } elseif (isset($match[1]) && $match[1] != '') {
+ return $lng[$match[1]];
+ }
+ return '';
+ } elseif (preg_match('/^const\.(.*)$/', $matches[1], $match)) {
+ return $this->returnDynamic($match[1]);
+ } elseif (preg_match('/^sql\.(.*)$/', $matches[1], $match)) {
+ if (is_null($this->sqldata_cache)) {
+ // read in sql-data (if exists)
+ if (file_exists(\Froxlor\Froxlor::getInstallDir() . "/lib/userdata.inc.php")) {
+ require \Froxlor\Froxlor::getInstallDir() . "/lib/userdata.inc.php";
+ unset($sql_root);
+ $this->sqldata_cache = $sql;
+ }
+ }
+ return isset($this->sqldata_cache[$match[1]]) ? $this->sqldata_cache[$match[1]] : '';
+ }
+ }, $content);
+ return $content;
+ }
+
+ private function returnDynamic($key = null)
+ {
+ $dynamics = [
+ 'install_dir' => \Froxlor\Froxlor::getInstallDir()
+ ];
+ return $dynamics[$key] ?? '';
+ }
+
+ /**
+ * Check if visibility should be changed
+ *
+ * @param \SimpleXMLElement $order
+ * @return int 0|-1
+ */
+ private function checkVisibility($order)
+ {
+ $attributes = array();
+ foreach ($order->attributes() as $key => $value) {
+ $attributes[(string) $key] = $this->parseContent(trim((string) $value));
+ }
+
+ $order = $this->parseContent(trim((string) $order));
+ if (! array_key_exists('mode', $attributes)) {
+ throw new \Exception('"' . $order . ' " is missing mode');
+ }
+
+ $return = 0;
+ switch ($attributes['mode']) {
+ case "isfile":
+ if (! is_file($order)) {
+ $return = - 1;
+ }
+ ;
+ break;
+ case "notisfile":
+ if (is_file($order)) {
+ $return = - 1;
+ }
+ ;
+ break;
+ case "isdir":
+ if (! is_dir($order)) {
+ $return = - 1;
+ }
+ ;
+ break;
+ case "notisdir":
+ if (is_dir($order)) {
+ $return = - 1;
+ }
+ ;
+ break;
+ case "false":
+ if ($order == true) {
+ $return = - 1;
+ }
+ ;
+ break;
+ case "true":
+ if ($order == false) {
+ $return = - 1;
+ }
+ ;
+ break;
+ case "notempty":
+ if ($order == "") {
+ $return = - 1;
+ }
+ ;
+ break;
+ case "userexists":
+ if (posix_getpwuid($order) === false) {
+ $return = - 1;
+ }
+ ;
+ break;
+ case "groupexists":
+ if (posix_getgrgid($order) === false) {
+ $return = - 1;
+ }
+ ;
+ break;
+ case "usernotexists":
+ if (is_array(posix_getpwuid($order))) {
+ $return = - 1;
+ }
+ ;
+ break;
+ case "groupnotexists":
+ if (is_array(posix_getgrgid($order))) {
+ $return = - 1;
+ }
+ ;
+ break;
+ case "usernamenotexists":
+ if (is_array(posix_getpwnam($order))) {
+ $return = - 1;
+ }
+ ;
+ break;
+ case "equals":
+ $return = (isset($attributes['value']) && $attributes['value'] == $order ? 0 : - 1);
+ break;
+ }
+ return $return;
+ }
+}
diff --git a/lib/classes/config/class.ConfigParser.php b/lib/Froxlor/Config/ConfigParser.php
similarity index 51%
rename from lib/classes/config/class.ConfigParser.php
rename to lib/Froxlor/Config/ConfigParser.php
index a294d56e..c881bcdd 100644
--- a/lib/classes/config/class.ConfigParser.php
+++ b/lib/Froxlor/Config/ConfigParser.php
@@ -1,4 +1,5 @@
- * @author Froxlor team (2010-)
- * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
- * @package Classes
- *
- * @since 0.9.34
+ * @copyright (c) the authors
+ * @author Florian Aders
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Classes
+ *
+ * @since 0.9.34
*/
/**
@@ -22,107 +23,139 @@
*
* Parses a distributions XML - file and gives access to the configuration
*
- * @copyright (c) the authors
- * @author Florian Aders
- * @author Froxlor team (2010-)
- * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
- * @package Classes
+ * @copyright (c) the authors
+ * @author Florian Aders
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Classes
*/
-class ConfigParser {
+class ConfigParser
+{
+
/**
* Holding the available services in the XML
- * @var array
+ *
+ * @var array
*/
private $services = array();
+ /**
+ * Holding the available defaults in the XML
+ *
+ * @var array
+ */
+ private $defaults = array();
+
/**
* Store the parsed SimpleXMLElement for usage
- * @var SimpleXMLElement
+ *
+ * @var \SimpleXMLElement
*/
private $xml;
/**
* Memorize if we already parsed the XML
+ *
* @var bool
*/
private $isparsed = false;
/**
* Name of the distribution this configuration is for
+ *
* @var string
*/
public $distributionName = '';
/**
* Codename of the distribution this configuration is for
+ *
* @var string
*/
public $distributionCodename = '';
/**
* Version of the distribution this configuration is for
+ *
* @var string
*/
public $distributionVersion = '';
/**
* Recommended editor
+ *
* @var string
*/
public $distributionEditor = '/bin/nano';
/**
* Show if this configuration is deprecated
+ *
* @var bool
*/
public $deprecated = false;
/**
* Constructor
- *
+ *
* Initialize the XML - ConfigParser
- * @param string $filename filename of the froxlor - configurationfile
+ *
+ * @param string $filename
+ * filename of the froxlor - configurationfile
* @return void
*/
- public function __construct($filename) {
- if (!is_readable($filename)) {
- throw new Exception('File not readable');
+ public function __construct($filename)
+ {
+ if (! is_readable($filename)) {
+ throw new \Exception('File not readable');
}
$this->xml = simplexml_load_file($filename);
if ($this->xml === false) {
$error = '';
- foreach(libxml_get_errors() as $error) {
+ foreach (libxml_get_errors() as $error) {
$error .= "\t" . $error->message;
}
throw new \Exception($error);
}
-
+
// Let's see if we can find a block in the XML
$distribution = $this->xml->xpath('//distribution');
// No distribution found - can't use this file
- if (!is_array($distribution)) {
+ if (! is_array($distribution)) {
throw new \Exception('Invalid XML, no distribution found');
}
// Search for attributes we understand
- foreach($distribution[0]->attributes() as $key => $value) {
- switch ((string)$key) {
- case "name": $this->distributionName = (string)$value; break;
- case "version": $this->distributionVersion = (string)$value; break;
- case "codename": $this->distributionCodename = (string)$value; break;
- case "defaulteditor": $this->distributionEditor = (string)$value; break;
- case "deprecated": (string)$value == 'true' ? $this->deprecated = true : $this->deprecated = false; break;
+ foreach ($distribution[0]->attributes() as $key => $value) {
+ switch ((string) $key) {
+ case "name":
+ $this->distributionName = (string) $value;
+ break;
+ case "version":
+ $this->distributionVersion = (string) $value;
+ break;
+ case "codename":
+ $this->distributionCodename = (string) $value;
+ break;
+ case "defaulteditor":
+ $this->distributionEditor = (string) $value;
+ break;
+ case "deprecated":
+ (string) $value == 'true' ? $this->deprecated = true : $this->deprecated = false;
+ break;
}
}
}
/**
* Parse the XML and populate $this->services
+ *
* @return bool
*/
- private function _parse() {
+ private function parseServices()
+ {
// We only want to parse the stuff one time
if ($this->isparsed == true) {
return true;
@@ -138,7 +171,7 @@ class ConfigParser {
// Search the attributes for "type"
foreach ($service->attributes() as $key => $value) {
if ($key == 'type') {
- $this->services[(string)$value] = new ConfigService($this->xml, '//services/service[@type="' . (string)$value . '"]');
+ $this->services[(string) $value] = new ConfigService($this->xml, '//services/service[@type="' . (string) $value . '"]');
}
}
}
@@ -148,17 +181,58 @@ class ConfigParser {
return true;
}
+ /**
+ * Parse the XML and populate $this->services
+ *
+ * @return bool
+ */
+ private function parseDefaults()
+ {
+ // We only want to parse the stuff one time
+ if ($this->isparsed == true) {
+ return true;
+ }
+
+ // Get all defaults
+ $defaults = $this->xml->xpath('//defaults');
+ foreach ($defaults as $default) {
+ $this->defaults = $default;
+ }
+
+ // Switch flag to indicate we parsed our data
+ $this->isparsed = true;
+ return true;
+ }
+
/**
* Return all services defined by the XML
- *
+ *
* The array will hold ConfigService - Objects for further handling
+ *
* @return array
*/
- public function getServices() {
+ public function getServices()
+ {
// Let's parse this shit(!)
- $this->_parse();
+ $this->parseServices();
// Return our carefully searched for services
return $this->services;
}
+
+ /**
+ * Return all defaults defined by the XML
+ *
+ * The array will hold ConfigDefaults - Objects for further handling
+ *
+ * @return array
+ */
+ public function getDefaults()
+ {
+ // Let's parse this shit(!)
+ $this->parseDefaults();
+
+ // Return our carefully searched for defaults
+ return $this->defaults;
+ }
}
diff --git a/lib/classes/config/class.ConfigService.php b/lib/Froxlor/Config/ConfigService.php
similarity index 74%
rename from lib/classes/config/class.ConfigService.php
rename to lib/Froxlor/Config/ConfigService.php
index b2b6d0ed..9b44993d 100644
--- a/lib/classes/config/class.ConfigService.php
+++ b/lib/Froxlor/Config/ConfigService.php
@@ -1,4 +1,5 @@
- * @author Froxlor team (2010-)
- * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
- * @package Classes
- *
- * @since 0.9.34
+ * @copyright (c) the authors
+ * @author Florian Aders
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Classes
+ *
+ * @since 0.9.34
*/
-
/**
* Class ConfigService
*
* Parses a distributions XML - file and gives access to the services within
* Not to be used directly
*
- * @copyright (c) the authors
- * @author Florian Aders
- * @author Froxlor team (2010-)
- * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
- * @package Classes
+ * @copyright (c) the authors
+ * @author Florian Aders
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Classes
*/
-class ConfigService {
+class ConfigService
+{
+
/**
* Holding the available daemons in this service
- * @var array
+ *
+ * @var array
*/
private $daemons = array();
/**
* Store the parsed SimpleXMLElement for usage
- * @var SimpleXMLElement
+ *
+ * @var \SimpleXMLElement
*/
private $fullxml;
/**
* Memorize if we already parsed the XML
+ *
* @var bool
*/
private $isparsed = false;
/**
* xpath leading to this service in the full XML
+ *
* @var string
*/
private $xpath;
/**
* Human - readable title of this service
+ *
* @var string
*/
public $title;
- public function __construct($xml, $xpath) {
+ public function __construct($xml, $xpath)
+ {
$this->fullxml = $xml;
$this->xpath = $xpath;
$service = $this->fullxml->xpath($this->xpath);
$attributes = $service[0]->attributes();
if ($attributes['title'] != '') {
- $this->title = $this->_parseContent((string)$attributes['title']);
+ $this->title = $this->parseContent((string) $attributes['title']);
}
}
/**
* Parse the XML and populate $this->daemons
+ *
* @return bool
*/
- private function _parse() {
+ private function parse()
+ {
// We only want to parse the stuff one time
if ($this->isparsed == true) {
return true;
@@ -89,12 +99,12 @@ class ConfigService {
$name = '';
$nametag = '';
$versiontag = '';
- foreach($daemon->attributes() as $key => $value) {
+ foreach ($daemon->attributes() as $key => $value) {
if ($key == 'name' && $name == '') {
- $name = (string)$value;
+ $name = (string) $value;
$nametag = "[@name='" . $value . "']";
} elseif ($key == 'name' && $name != '') {
- $name = (string)$value . '_' . $name;
+ $name = (string) $value . '_' . $name;
$nametag = "[@name='" . $value . "']";
} elseif ($key == 'version' && $name == '') {
$name = str_replace('.', '', $value);
@@ -105,7 +115,7 @@ class ConfigService {
}
}
if ($name == '') {
- throw new Exception ('No name attribute for daemon');
+ throw new \Exception('No name attribute for daemon');
}
$this->daemons[$name] = new ConfigDaemon($this->fullxml, $this->xpath . "/daemon" . $nametag . $versiontag);
}
@@ -117,13 +127,16 @@ class ConfigService {
/**
* Replace placeholders with content
+ *
* @param string $content
* @return string $content w/o placeholder
*/
- private function _parseContent($content) {
+ private function parseContent($content)
+ {
$content = preg_replace_callback('/\{\{(.*)\}\}/Ui', function ($matches) {
+ $match = null;
if (preg_match('/^settings\.(.*)$/', $matches[1], $match)) {
- return Settings::Get($match[1]);
+ return \Froxlor\Settings::Get($match[1]);
} elseif (preg_match('/^lng\.(.*)(?:\.(.*)(?:\.(.*)))$/U', $matches[1], $match)) {
global $lng;
if (isset($match[1]) && $match[1] != '' && isset($match[2]) && $match[2] != '' && isset($match[3]) && $match[3] != '') {
@@ -139,8 +152,9 @@ class ConfigService {
return $content;
}
- public function getDaemons() {
- $this->_parse();
+ public function getDaemons()
+ {
+ $this->parse();
return $this->daemons;
}
}
diff --git a/lib/Froxlor/Cron/CronConfig.php b/lib/Froxlor/Cron/CronConfig.php
new file mode 100644
index 00000000..774c5f51
--- /dev/null
+++ b/lib/Froxlor/Cron/CronConfig.php
@@ -0,0 +1,152 @@
+
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ * @since 0.10.0
+ *
+ */
+use Froxlor\Database\Database;
+use Froxlor\Settings;
+
+class CronConfig
+{
+
+ /**
+ * 1st: check for task of generation
+ * 2nd: if task found, generate cron.d-file
+ * 3rd: maybe restart cron?
+ */
+ public static function checkCrondConfigurationFile()
+ {
+
+ // check for task
+ Database::query("
+ SELECT * FROM `" . TABLE_PANEL_TASKS . "` WHERE `type` = '99'
+ ");
+ $num_results = Database::num_rows();
+
+ // is there a task for re-generating the cron.d-file?
+ if ($num_results > 0) {
+
+ // get all crons and their intervals
+ if (\Froxlor\FileDir::isFreeBSD()) {
+ // FreeBSD does not need a header as we are writing directly to the crontab
+ $cronfile = "\n";
+ } else {
+ $cronfile = "# automatically generated cron-configuration by froxlor\n";
+ $cronfile .= "# do not manually edit this file as it will be re-generated periodically.\n";
+ $cronfile .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n#\n";
+ }
+
+ // get all the crons
+ $result_stmt = Database::query("
+ SELECT * FROM `" . TABLE_PANEL_CRONRUNS . "` WHERE `isactive` = '1'
+ ");
+
+ $hour_delay = 0;
+ $day_delay = 5;
+ $month_delay = 7;
+ while ($row_cronentry = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ // create cron.d-entry
+ $matches = array();
+ if (preg_match("/(\d+) (MINUTE|HOUR|DAY|WEEK|MONTH)/", $row_cronentry['interval'], $matches)) {
+ if ($matches[1] == 1) {
+ $minvalue = "*";
+ } else {
+ $minvalue = "*/" . $matches[1];
+ }
+ switch ($matches[2]) {
+ case "MINUTE":
+ $cronfile .= $minvalue . " * * * * ";
+ break;
+ case "HOUR":
+ $cronfile .= $hour_delay . " " . $minvalue . " * * * ";
+ $hour_delay += 3;
+ break;
+ case "DAY":
+ if ($row_cronentry['cronfile'] == 'traffic') {
+ // traffic at exactly 0:00 o'clock
+ $cronfile .= "0 0 " . $minvalue . " * * ";
+ } else {
+ $cronfile .= $day_delay . " 0 " . $minvalue . " * * ";
+ $day_delay += 5;
+ }
+ break;
+ case "MONTH":
+ $cronfile .= $month_delay . " 0 1 " . $minvalue . " * ";
+ $month_delay += 7;
+ break;
+ case "WEEK":
+ $cronfile .= $day_delay . " 0 " . ($matches[1] * 7) . " * * ";
+ $day_delay += 5;
+ break;
+ }
+
+ // create entry-line
+ $binpath = Settings::Get("system.croncmdline");
+ // fallback as it is important
+ if ($binpath === null) {
+ $binpath = "/usr/bin/nice -n 5 /usr/bin/php -q";
+ }
+
+ $cronfile .= "root " . $binpath . " " . \Froxlor\FileDir::makeCorrectFile(\Froxlor\Froxlor::getInstallDir() . "/scripts/froxlor_master_cronjob.php") . " --" . $row_cronentry['cronfile'] . " 1> /dev/null\n";
+ }
+ }
+
+ if (\Froxlor\FileDir::isFreeBSD()) {
+ // FreeBSD handles the cron-stuff in another way. We need to directly
+ // write to the crontab file as there is not cron.d/froxlor file
+ // (settings for system.cronconfig should be set correctly of course)
+ $crontab = file_get_contents(Settings::Get("system.cronconfig"));
+
+ if ($crontab === false) {
+ die("Oh snap, we cannot read the crontab file. This should not happen.\nPlease check the path and permissions, the cron will keep trying if you don't stop the cron-service.\n\n");
+ }
+
+ // now parse out / replace our entries
+ $crontablines = explode("\n", $crontab);
+ $newcrontab = "";
+ foreach ($crontablines as $ctl) {
+ $ctl = trim($ctl);
+ if (! empty($ctl) && ! preg_match("/(.*)froxlor_master_cronjob\.php(.*)/", $ctl)) {
+ $newcrontab .= $ctl . "\n";
+ }
+ }
+
+ // re-assemble old-content + new froxlor-content
+ $newcrontab .= $cronfile;
+
+ // now continue with writing the file
+ $cronfile = $newcrontab;
+ }
+
+ // write the file
+ if (file_put_contents(Settings::Get("system.cronconfig"), $cronfile) === false) {
+ // oh snap cannot create new crond-file
+ die("Oh snap, we cannot create the cron-file. This should not happen.\nPlease check the path and permissions, the cron will keep trying if you don't stop the cron-service.\n\n");
+ }
+ // correct permissions
+ chmod(Settings::Get("system.cronconfig"), 0640);
+
+ // remove all re-generation tasks
+ Database::query("DELETE FROM `" . TABLE_PANEL_TASKS . "` WHERE `type` = '99'");
+
+ // run reload command
+ \Froxlor\FileDir::safe_exec(escapeshellcmd(Settings::Get('system.crondreload')));
+ }
+ return true;
+ }
+}
diff --git a/lib/Froxlor/Cron/Dns/Bind.php b/lib/Froxlor/Cron/Dns/Bind.php
new file mode 100644
index 00000000..2c2ae9f9
--- /dev/null
+++ b/lib/Froxlor/Cron/Dns/Bind.php
@@ -0,0 +1,159 @@
+ (2016-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ */
+class Bind extends DnsBase
+{
+
+ private $bindconf_file = "";
+
+ public function writeConfigs()
+ {
+ // tell the world what we are doing
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'Task4 started - Rebuilding froxlor_bind.conf');
+
+ // clean up
+ $this->cleanZonefiles();
+
+ // check for subfolder in bind-config-directory
+ if (! file_exists(\Froxlor\FileDir::makeCorrectDir(Settings::Get('system.bindconf_directory') . '/domains/'))) {
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'mkdir ' . escapeshellarg(\Froxlor\FileDir::makeCorrectDir(Settings::Get('system.bindconf_directory') . '/domains/')));
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg(\Froxlor\FileDir::makeCorrectDir(Settings::Get('system.bindconf_directory') . '/domains/')));
+ }
+
+ $domains = $this->getDomainList();
+
+ if (empty($domains)) {
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'No domains found for nameserver-config, skipping...');
+ return;
+ }
+
+ $this->bindconf_file = '# ' . Settings::Get('system.bindconf_directory') . 'froxlor_bind.conf' . "\n" . '# Created ' . date('d.m.Y H:i') . "\n" . '# Do NOT manually edit this file, all changes will be deleted after the next domain change at the panel.' . "\n\n";
+
+ foreach ($domains as $domain) {
+ if ($domain['ismainbutsubto'] > 0) {
+ // domains with ismainbutsubto>0 are handled by recursion within walkDomainList()
+ continue;
+ }
+ $this->walkDomainList($domain, $domains);
+ }
+
+ $bindconf_file_handler = fopen(\Froxlor\FileDir::makeCorrectFile(Settings::Get('system.bindconf_directory') . '/froxlor_bind.conf'), 'w');
+ fwrite($bindconf_file_handler, $this->bindconf_file);
+ fclose($bindconf_file_handler);
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'froxlor_bind.conf written');
+ $this->reloadDaemon();
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'Task4 finished');
+ }
+
+ private function walkDomainList($domain, $domains)
+ {
+ $zoneContent = '';
+ $subzones = '';
+
+ foreach ($domain['children'] as $child_domain_id) {
+ $subzones .= $this->walkDomainList($domains[$child_domain_id], $domains);
+ }
+
+ if ($domain['zonefile'] == '') {
+ // check for system-hostname
+ $isFroxlorHostname = false;
+ if (isset($domain['froxlorhost']) && $domain['froxlorhost'] == 1) {
+ $isFroxlorHostname = true;
+ }
+
+ if ($domain['ismainbutsubto'] == 0) {
+ $zoneContent = (string) \Froxlor\Dns\Dns::createDomainZone(($domain['id'] == 'none') ? $domain : $domain['id'], $isFroxlorHostname);
+ $domain['zonefile'] = 'domains/' . $domain['domain'] . '.zone';
+ $zonefile_name = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.bindconf_directory') . '/' . $domain['zonefile']);
+ $zonefile_handler = fopen($zonefile_name, 'w');
+ fwrite($zonefile_handler, $zoneContent . $subzones);
+ fclose($zonefile_handler);
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, '`' . $zonefile_name . '` written');
+ $this->bindconf_file .= $this->generateDomainConfig($domain);
+ } else {
+ return (string) \Froxlor\Dns\Dns::createDomainZone(($domain['id'] == 'none') ? $domain : $domain['id'], $isFroxlorHostname, true);
+ }
+ } else {
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'Added zonefile ' . $domain['zonefile'] . ' for domain ' . $domain['domain'] . ' - Note that you will also have to handle ALL records for ALL subdomains.');
+ $this->bindconf_file .= $this->generateDomainConfig($domain);
+ }
+ }
+
+ private function generateDomainConfig($domain = array())
+ {
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'Generating dns config for ' . $domain['domain']);
+
+ $bindconf_file = '# Domain ID: ' . $domain['id'] . ' - CustomerID: ' . $domain['customerid'] . ' - CustomerLogin: ' . $domain['loginname'] . "\n";
+ $bindconf_file .= 'zone "' . $domain['domain'] . '" in {' . "\n";
+ $bindconf_file .= ' type master;' . "\n";
+ $bindconf_file .= ' file "' . \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.bindconf_directory') . '/' . $domain['zonefile']) . '";' . "\n";
+ $bindconf_file .= ' allow-query { any; };' . "\n";
+
+ if (count($this->ns) > 0 || count($this->axfr) > 0) {
+ // open allow-transfer
+ $bindconf_file .= ' allow-transfer {' . "\n";
+ // put nameservers in allow-transfer
+ if (count($this->ns) > 0) {
+ foreach ($this->ns as $ns) {
+ foreach ($ns["ips"] as $ip) {
+ $ip = \Froxlor\Validate\Validate::validate_ip2($ip, true, 'invalidip', true, true, true);
+ if ($ip) {
+ $bindconf_file .= ' ' . $ip . ";\n";
+ }
+ }
+ }
+ }
+ // AXFR server #100
+ if (count($this->axfr) > 0) {
+ foreach ($this->axfr as $axfrserver) {
+ $bindconf_file .= ' ' . $axfrserver . ';' . "\n";
+ }
+ }
+ // close allow-transfer
+ $bindconf_file .= ' };' . "\n";
+ }
+
+ $bindconf_file .= '};' . "\n";
+ $bindconf_file .= "\n";
+
+ return $bindconf_file;
+ }
+
+ private function cleanZonefiles()
+ {
+ $config_dir = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.bindconf_directory') . '/domains/');
+
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'Cleaning dns zone files from ' . $config_dir);
+
+ // check directory
+ if (@is_dir($config_dir)) {
+
+ // create directory iterator
+ $its = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($config_dir));
+
+ // iterate through all subdirs, look for zone files and delete them
+ foreach ($its as $it) {
+ if ($it->isFile()) {
+ // remove file
+ \Froxlor\FileDir::safe_exec('rm -f ' . escapeshellarg(\Froxlor\FileDir::makeCorrectFile($its->getPathname())));
+ }
+ }
+ }
+ }
+}
diff --git a/lib/Froxlor/Cron/Dns/DnsBase.php b/lib/Froxlor/Cron/Dns/DnsBase.php
new file mode 100644
index 00000000..fa7bbe76
--- /dev/null
+++ b/lib/Froxlor/Cron/Dns/DnsBase.php
@@ -0,0 +1,265 @@
+ (2016-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ */
+
+/**
+ * Class DnsBase
+ *
+ * Base class for all DNS server configs
+ */
+abstract class DnsBase
+{
+
+ protected $logger = false;
+
+ protected $ns = array();
+
+ protected $mx = array();
+
+ protected $axfr = array();
+
+ abstract public function writeConfigs();
+
+ public function __construct($logger)
+ {
+ $this->logger = $logger;
+
+ $known_ns_ips = [];
+ if (Settings::Get('system.nameservers') != '') {
+ $nameservers = explode(',', Settings::Get('system.nameservers'));
+ foreach ($nameservers as $nameserver) {
+ $nameserver = trim($nameserver);
+ // DNS servers might be multi homed; allow transfer from all ip
+ // addresses of the DNS server
+ $nameserver_ips = \Froxlor\PhpHelper::gethostbynamel6($nameserver);
+ // append dot to hostname
+ if (substr($nameserver, - 1, 1) != '.') {
+ $nameserver .= '.';
+ }
+ // ignore invalid responses
+ if (! is_array($nameserver_ips)) {
+ // act like \Froxlor\PhpHelper::gethostbynamel6() and return unmodified hostname on error
+ $nameserver_ips = array(
+ $nameserver
+ );
+ } else {
+ $known_ns_ips = array_merge($known_ns_ips, $nameserver_ips);
+ }
+ $this->ns[] = array(
+ 'hostname' => $nameserver,
+ 'ips' => $nameserver_ips
+ );
+ }
+ }
+
+ if (Settings::Get('system.mxservers') != '') {
+ $mxservers = explode(',', Settings::Get('system.mxservers'));
+ foreach ($mxservers as $mxserver) {
+ if (substr($mxserver, - 1, 1) != '.') {
+ $mxserver .= '.';
+ }
+ $this->mx[] = $mxserver;
+ }
+ }
+
+ // AXFR server #100
+ if (Settings::Get('system.axfrservers') != '') {
+ $axfrservers = explode(',', Settings::Get('system.axfrservers'));
+ foreach ($axfrservers as $axfrserver) {
+ if (!in_array(trim($axfrserver), $known_ns_ips)) {
+ $this->axfr[] = trim($axfrserver);
+ }
+ }
+ }
+ }
+
+ protected function getDomainList()
+ {
+ $result_domains_stmt = Database::query("
+ SELECT
+ `d`.`id`,
+ `d`.`domain`,
+ `d`.`isemaildomain`,
+ `d`.`iswildcarddomain`,
+ `d`.`wwwserveralias`,
+ `d`.`customerid`,
+ `d`.`zonefile`,
+ `d`.`bindserial`,
+ `d`.`dkim`,
+ `d`.`dkim_id`,
+ `d`.`dkim_pubkey`,
+ `d`.`ismainbutsubto`,
+ `c`.`loginname`,
+ `c`.`guid`
+ FROM
+ `" . TABLE_PANEL_DOMAINS . "` `d`
+ LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` `c` USING(`customerid`)
+ WHERE
+ `d`.`isbinddomain` = '1'
+ ORDER BY
+ `d`.`domain` ASC
+ ");
+
+ $domains = array();
+ // don't use fetchall() to be able to set the first column to the domain id and use it later on to set the rows'
+ // array of direct children without having to search the outer array
+ while ($domain = $result_domains_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $domains[$domain["id"]] = $domain;
+ }
+
+ // frolxor-hostname (#1090)
+ if (Settings::get('system.dns_createhostnameentry') == 1) {
+ $hostname_arr = array(
+ 'id' => 'none',
+ 'domain' => Settings::Get('system.hostname'),
+ 'isbinddomain' => '1',
+ 'isemaildomain' => Settings::Get('system.dns_createmailentry'),
+ 'customerid' => 'none',
+ 'loginname' => 'froxlor.panel',
+ 'bindserial' => date('Ymd') . '00',
+ 'dkim' => '0',
+ 'iswildcarddomain' => '1',
+ 'ismainbutsubto' => '0',
+ 'zonefile' => '',
+ 'froxlorhost' => '1'
+ );
+ $domains['none'] = $hostname_arr;
+ }
+
+ if (empty($domains)) {
+ return null;
+ }
+
+ // collect domain IDs of direct child domains as arrays in ['children'] column
+ foreach (array_keys($domains) as $key) {
+ if (! isset($domains[$key]['children'])) {
+ $domains[$key]['children'] = array();
+ }
+ if ($domains[$key]['ismainbutsubto'] > 0) {
+ if (isset($domains[$domains[$key]['ismainbutsubto']])) {
+ $domains[$domains[$key]['ismainbutsubto']]['children'][] = $domains[$key]['id'];
+ } else {
+ $domains[$key]['ismainbutsubto'] = 0;
+ }
+ }
+ }
+
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, str_pad('domId', 9, ' ') . str_pad('domain', 40, ' ') . 'ismainbutsubto ' . str_pad('parent domain', 40, ' ') . "list of child domain ids");
+ foreach ($domains as $domain) {
+ $logLine = str_pad($domain['id'], 9, ' ') . str_pad($domain['domain'], 40, ' ') . str_pad($domain['ismainbutsubto'], 15, ' ') . str_pad(((isset($domains[$domain['ismainbutsubto']])) ? $domains[$domain['ismainbutsubto']]['domain'] : '-'), 40, ' ') . join(', ', $domain['children']);
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, $logLine);
+ }
+
+ return $domains;
+ }
+
+ public function reloadDaemon()
+ {
+ // reload DNS daemon
+ $cmd = Settings::Get('system.bindreload_command');
+ $cmdStatus = 1;
+ \Froxlor\FileDir::safe_exec(escapeshellcmd($cmd), $cmdStatus);
+ if ($cmdStatus === 0) {
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, Settings::Get('system.dns_server') . ' daemon reloaded');
+ } else {
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, 'Error while running `' . $cmd . '`: exit code (' . $cmdStatus . ') - please check your system logs');
+ }
+ }
+
+ public function writeDKIMconfigs()
+ {
+ if (Settings::Get('dkim.use_dkim') == '1') {
+ if (! file_exists(\Froxlor\FileDir::makeCorrectDir(Settings::Get('dkim.dkim_prefix')))) {
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'mkdir -p ' . escapeshellarg(\Froxlor\FileDir::makeCorrectDir(Settings::Get('dkim.dkim_prefix'))));
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg(\Froxlor\FileDir::makeCorrectDir(Settings::Get('dkim.dkim_prefix'))));
+ }
+
+ $dkimdomains = '';
+ $dkimkeys = '';
+ $result_domains_stmt = Database::query("
+ SELECT `id`, `domain`, `dkim`, `dkim_id`, `dkim_pubkey`, `dkim_privkey`
+ FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `dkim` = '1' ORDER BY `id` ASC
+ ");
+
+ while ($domain = $result_domains_stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ $privkey_filename = \Froxlor\FileDir::makeCorrectFile(Settings::Get('dkim.dkim_prefix') . '/dkim' . $domain['dkim_id'] . '.priv');
+ $pubkey_filename = \Froxlor\FileDir::makeCorrectFile(Settings::Get('dkim.dkim_prefix') . '/dkim' . $domain['dkim_id'] . '.public');
+
+ if ($domain['dkim_privkey'] == '' || $domain['dkim_pubkey'] == '') {
+ $max_dkim_id_stmt = Database::query("SELECT MAX(`dkim_id`) as `max_dkim_id` FROM `" . TABLE_PANEL_DOMAINS . "`");
+ $max_dkim_id = $max_dkim_id_stmt->fetch(\PDO::FETCH_ASSOC);
+ $domain['dkim_id'] = (int) $max_dkim_id['max_dkim_id'] + 1;
+ $privkey_filename = \Froxlor\FileDir::makeCorrectFile(Settings::Get('dkim.dkim_prefix') . '/dkim' . $domain['dkim_id'] . '.priv');
+ \Froxlor\FileDir::safe_exec('openssl genrsa -out ' . escapeshellarg($privkey_filename) . ' ' . Settings::Get('dkim.dkim_keylength'));
+ $domain['dkim_privkey'] = file_get_contents($privkey_filename);
+ \Froxlor\FileDir::safe_exec("chmod 0640 " . escapeshellarg($privkey_filename));
+ $pubkey_filename = \Froxlor\FileDir::makeCorrectFile(Settings::Get('dkim.dkim_prefix') . '/dkim' . $domain['dkim_id'] . '.public');
+ \Froxlor\FileDir::safe_exec('openssl rsa -in ' . escapeshellarg($privkey_filename) . ' -pubout -outform pem -out ' . escapeshellarg($pubkey_filename));
+ $domain['dkim_pubkey'] = file_get_contents($pubkey_filename);
+ \Froxlor\FileDir::safe_exec("chmod 0664 " . escapeshellarg($pubkey_filename));
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_DOMAINS . "` SET
+ `dkim_id` = :dkimid,
+ `dkim_privkey` = :privkey,
+ `dkim_pubkey` = :pubkey
+ WHERE `id` = :id
+ ");
+ $upd_data = array(
+ 'dkimid' => $domain['dkim_id'],
+ 'privkey' => $domain['dkim_privkey'],
+ 'pubkey' => $domain['dkim_pubkey'],
+ 'id' => $domain['id']
+ );
+ Database::pexecute($upd_stmt, $upd_data);
+ }
+
+ if (! file_exists($privkey_filename) && $domain['dkim_privkey'] != '') {
+ $privkey_file_handler = fopen($privkey_filename, "w");
+ fwrite($privkey_file_handler, $domain['dkim_privkey']);
+ fclose($privkey_file_handler);
+ \Froxlor\FileDir::safe_exec("chmod 0640 " . escapeshellarg($privkey_filename));
+ }
+
+ if (! file_exists($pubkey_filename) && $domain['dkim_pubkey'] != '') {
+ $pubkey_file_handler = fopen($pubkey_filename, "w");
+ fwrite($pubkey_file_handler, $domain['dkim_pubkey']);
+ fclose($pubkey_file_handler);
+ \Froxlor\FileDir::safe_exec("chmod 0644 " . escapeshellarg($pubkey_filename));
+ }
+
+ $dkimdomains .= $domain['domain'] . "\n";
+ $dkimkeys .= "*@" . $domain['domain'] . ":" . $domain['domain'] . ":" . $privkey_filename . "\n";
+ }
+
+ $dkimdomains_filename = \Froxlor\FileDir::makeCorrectFile(Settings::Get('dkim.dkim_prefix') . '/' . Settings::Get('dkim.dkim_domains'));
+ $dkimdomains_file_handler = fopen($dkimdomains_filename, "w");
+ fwrite($dkimdomains_file_handler, $dkimdomains);
+ fclose($dkimdomains_file_handler);
+ $dkimkeys_filename = \Froxlor\FileDir::makeCorrectFile(Settings::Get('dkim.dkim_prefix') . '/' . Settings::Get('dkim.dkim_dkimkeys'));
+ $dkimkeys_file_handler = fopen($dkimkeys_filename, "w");
+ fwrite($dkimkeys_file_handler, $dkimkeys);
+ fclose($dkimkeys_file_handler);
+
+ \Froxlor\FileDir::safe_exec(escapeshellcmd(Settings::Get('dkim.dkimrestart_command')));
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'Dkim-milter reloaded');
+ }
+ }
+}
diff --git a/lib/Froxlor/Cron/Dns/PowerDNS.php b/lib/Froxlor/Cron/Dns/PowerDNS.php
new file mode 100644
index 00000000..fa0e8d69
--- /dev/null
+++ b/lib/Froxlor/Cron/Dns/PowerDNS.php
@@ -0,0 +1,191 @@
+ (2016-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ */
+class PowerDNS extends DnsBase
+{
+
+ public function writeConfigs()
+ {
+ // tell the world what we are doing
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'Task4 started - Refreshing DNS database');
+
+ $domains = $this->getDomainList();
+
+ // clean up
+ $this->clearZoneTables($domains);
+
+ if (empty($domains)) {
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'No domains found for nameserver-config, skipping...');
+ return;
+ }
+
+ foreach ($domains as $domain) {
+ if ($domain['ismainbutsubto'] > 0) {
+ // domains with ismainbutsubto>0 are handled by recursion within walkDomainList()
+ continue;
+ }
+ $this->walkDomainList($domain, $domains);
+ }
+
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'PowerDNS database updated');
+ $this->reloadDaemon();
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'Task4 finished');
+ }
+
+ private function walkDomainList($domain, $domains)
+ {
+ $zoneContent = '';
+ $subzones = array();
+
+ foreach ($domain['children'] as $child_domain_id) {
+ $subzones[] = $this->walkDomainList($domains[$child_domain_id], $domains);
+ }
+
+ if ($domain['zonefile'] == '') {
+ // check for system-hostname
+ $isFroxlorHostname = false;
+ if (isset($domain['froxlorhost']) && $domain['froxlorhost'] == 1) {
+ $isFroxlorHostname = true;
+ }
+
+ if ($domain['ismainbutsubto'] == 0) {
+ $zoneContent = \Froxlor\Dns\Dns::createDomainZone(($domain['id'] == 'none') ? $domain : $domain['id'], $isFroxlorHostname);
+ if (count($subzones)) {
+ foreach ($subzones as $subzone) {
+ $zoneContent->records[] = $subzone;
+ }
+ }
+ $pdnsDomId = $this->insertZone($zoneContent->origin, $zoneContent->serial);
+ $this->insertRecords($pdnsDomId, $zoneContent->records, $zoneContent->origin);
+ $this->insertAllowedTransfers($pdnsDomId);
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'DB entries stored for zone `' . $domain['domain'] . '`');
+ } else {
+ return \Froxlor\Dns\Dns::createDomainZone(($domain['id'] == 'none') ? $domain : $domain['id'], $isFroxlorHostname, true);
+ }
+ } else {
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, 'Custom zonefiles are NOT supported when PowerDNS is selected as DNS daemon (triggered by: ' . $domain['domain'] . ')');
+ }
+ }
+
+ private function clearZoneTables($domains = null)
+ {
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'Cleaning dns zone entries from database');
+
+ $pdns_domains_stmt = \Froxlor\Dns\PowerDNS::getDB()->prepare("SELECT `id`, `name` FROM `domains` WHERE `name` = :domain");
+
+ $del_rec_stmt = \Froxlor\Dns\PowerDNS::getDB()->prepare("DELETE FROM `records` WHERE `domain_id` = :did");
+ $del_meta_stmt = \Froxlor\Dns\PowerDNS::getDB()->prepare("DELETE FROM `domainmetadata` WHERE `domain_id` = :did");
+ $del_dom_stmt = \Froxlor\Dns\PowerDNS::getDB()->prepare("DELETE FROM `domains` WHERE `id` = :did");
+
+ foreach ($domains as $domain) {
+ $pdns_domains_stmt->execute(array(
+ 'domain' => $domain['domain']
+ ));
+ $pdns_domain = $pdns_domains_stmt->fetch(\PDO::FETCH_ASSOC);
+
+ $del_rec_stmt->execute(array(
+ 'did' => $pdns_domain['id']
+ ));
+ $del_meta_stmt->execute(array(
+ 'did' => $pdns_domain['id']
+ ));
+ $del_dom_stmt->execute(array(
+ 'did' => $pdns_domain['id']
+ ));
+ }
+ }
+
+ private function insertZone($domainname, $serial = 0)
+ {
+ $ins_stmt = \Froxlor\Dns\PowerDNS::getDB()->prepare("
+ INSERT INTO domains set `name` = :domainname, `notified_serial` = :serial, `type` = 'NATIVE'
+ ");
+ $ins_stmt->execute(array(
+ 'domainname' => $domainname,
+ 'serial' => $serial
+ ));
+ $lastid = \Froxlor\Dns\PowerDNS::getDB()->lastInsertId();
+ return $lastid;
+ }
+
+ private function insertRecords($domainid = 0, $records = array(), $origin = "")
+ {
+ $ins_stmt = \Froxlor\Dns\PowerDNS::getDB()->prepare("
+ INSERT INTO records set
+ `domain_id` = :did,
+ `name` = :rec,
+ `type` = :type,
+ `content` = :content,
+ `ttl` = :ttl,
+ `prio` = :prio,
+ `disabled` = '0'
+ ");
+
+ foreach ($records as $record) {
+ if ($record instanceof \Froxlor\Dns\DnsZone) {
+ $this->insertRecords($domainid, $record->records, $record->origin);
+ continue;
+ }
+
+ if ($record->record == '@') {
+ $_record = $origin;
+ } else {
+ $_record = $record->record . "." . $origin;
+ }
+
+ $ins_data = array(
+ 'did' => $domainid,
+ 'rec' => $_record,
+ 'type' => $record->type,
+ 'content' => $record->content,
+ 'ttl' => $record->ttl,
+ 'prio' => $record->priority
+ );
+ $ins_stmt->execute($ins_data);
+ }
+ }
+
+ private function insertAllowedTransfers($domainid)
+ {
+ $ins_stmt = \Froxlor\Dns\PowerDNS::getDB()->prepare("
+ INSERT INTO domainmetadata set `domain_id` = :did, `kind` = 'ALLOW-AXFR-FROM', `content` = :value
+ ");
+
+ $ins_data = array(
+ 'did' => $domainid
+ );
+
+ if (count($this->ns) > 0 || count($this->axfr) > 0) {
+ // put nameservers in allow-transfer
+ if (count($this->ns) > 0) {
+ foreach ($this->ns as $ns) {
+ foreach ($ns["ips"] as $ip) {
+ $ins_data['value'] = $ip;
+ $ins_stmt->execute($ins_data);
+ }
+ }
+ }
+ // AXFR server #100
+ if (count($this->axfr) > 0) {
+ foreach ($this->axfr as $axfrserver) {
+ $ins_data['value'] = $axfrserver;
+ $ins_stmt->execute($ins_data);
+ }
+ }
+ }
+ }
+}
diff --git a/lib/Froxlor/Cron/FroxlorCron.php b/lib/Froxlor/Cron/FroxlorCron.php
new file mode 100644
index 00000000..3433d23e
--- /dev/null
+++ b/lib/Froxlor/Cron/FroxlorCron.php
@@ -0,0 +1,39 @@
+
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ * @since 0.10.0
+ *
+ */
+abstract class FroxlorCron
+{
+
+ abstract public static function run();
+
+ protected static $cronlog = null;
+
+ protected static $lockfile = null;
+
+ public static function getLockfile()
+ {
+ return static::$lockfile;
+ }
+
+ public static function setLockfile($lockfile = null)
+ {
+ static::$lockfile = $lockfile;
+ }
+}
diff --git a/lib/Froxlor/Cron/Http/Apache.php b/lib/Froxlor/Cron/Http/Apache.php
new file mode 100644
index 00000000..1f37b78f
--- /dev/null
+++ b/lib/Froxlor/Cron/Http/Apache.php
@@ -0,0 +1,1440 @@
+ (2003-2009)
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ */
+class Apache extends HttpConfigBase
+{
+
+ // protected
+ protected $known_vhostfilenames = array();
+
+ protected $known_diroptionsfilenames = array();
+
+ protected $known_htpasswdsfilenames = array();
+
+ protected $virtualhosts_data = array();
+
+ protected $diroptions_data = array();
+
+ protected $htpasswds_data = array();
+
+ /**
+ * indicator whether a customer is deactivated or not
+ * if yes, only the webroot will be generated
+ *
+ * @var bool
+ */
+ private $deactivated = false;
+
+ /**
+ * define a standard -statement, bug #32
+ */
+ private function createStandardDirectoryEntry()
+ {
+ $vhosts_folder = '';
+ if (is_dir(Settings::Get('system.apacheconf_vhost'))) {
+ $vhosts_folder = \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.apacheconf_vhost'));
+ } else {
+ $vhosts_folder = \Froxlor\FileDir::makeCorrectDir(dirname(Settings::Get('system.apacheconf_vhost')));
+ }
+ $vhosts_filename = \Froxlor\FileDir::makeCorrectFile($vhosts_folder . '/05_froxlor_dirfix_nofcgid.conf');
+
+ if (! isset($this->virtualhosts_data[$vhosts_filename])) {
+ $this->virtualhosts_data[$vhosts_filename] = '';
+ }
+
+ $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n";
+
+ // check for custom values, see #1638
+ $custom_opts = Settings::Get('system.apacheglobaldiropt');
+ if (! empty($custom_opts)) {
+ $this->virtualhosts_data[$vhosts_filename] .= $custom_opts . "\n";
+ } else {
+ // >=apache-2.4 enabled?
+ if (Settings::Get('system.apache24') == '1') {
+ $this->virtualhosts_data[$vhosts_filename] .= ' Require all granted' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' AllowOverride All' . "\n";
+ } else {
+ $this->virtualhosts_data[$vhosts_filename] .= ' Order allow,deny' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' allow from all' . "\n";
+ }
+ }
+ $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n";
+
+ $ocsp_cache_filename = \Froxlor\FileDir::makeCorrectFile($vhosts_folder . '/03_froxlor_ocsp_cache.conf');
+ if (Settings::Get('system.use_ssl') == '1' && Settings::Get('system.apache24') == 1) {
+ $this->virtualhosts_data[$ocsp_cache_filename] = 'SSLStaplingCache ' . Settings::Get('system.apache24_ocsp_cache_path') . "\n";
+ } else {
+ if (file_exists($ocsp_cache_filename)) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'apache::_createStandardDirectoryEntry: unlinking ' . basename($ocsp_cache_filename));
+ unlink(\Froxlor\FileDir::makeCorrectFile($ocsp_cache_filename));
+ }
+ }
+ }
+
+ /**
+ * define a default ErrorDocument-statement, bug #unknown-yet
+ */
+ private function createStandardErrorHandler()
+ {
+ if (Settings::Get('defaultwebsrverrhandler.enabled') == '1' && (Settings::Get('defaultwebsrverrhandler.err401') != '' || Settings::Get('defaultwebsrverrhandler.err403') != '' || Settings::Get('defaultwebsrverrhandler.err404') != '' || Settings::Get('defaultwebsrverrhandler.err500') != '')) {
+ $vhosts_folder = '';
+ if (is_dir(Settings::Get('system.apacheconf_vhost'))) {
+ $vhosts_folder = \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.apacheconf_vhost'));
+ } else {
+ $vhosts_folder = \Froxlor\FileDir::makeCorrectDir(dirname(Settings::Get('system.apacheconf_vhost')));
+ }
+
+ $vhosts_filename = \Froxlor\FileDir::makeCorrectFile($vhosts_folder . '/05_froxlor_default_errorhandler.conf');
+
+ if (! isset($this->virtualhosts_data[$vhosts_filename])) {
+ $this->virtualhosts_data[$vhosts_filename] = '';
+ }
+
+ $statusCodes = array(
+ '401',
+ '403',
+ '404',
+ '500'
+ );
+ foreach ($statusCodes as $statusCode) {
+ if (Settings::Get('defaultwebsrverrhandler.err' . $statusCode) != '') {
+ $defhandler = Settings::Get('defaultwebsrverrhandler.err' . $statusCode);
+ if (! \Froxlor\Validate\Validate::validateUrl($defhandler)) {
+ if (substr($defhandler, 0, 1) != '"' && substr($defhandler, - 1, 1) != '"') {
+ $defhandler = '"' . \Froxlor\FileDir::makeCorrectFile($defhandler) . '"';
+ }
+ }
+ $this->virtualhosts_data[$vhosts_filename] .= 'ErrorDocument ' . $statusCode . ' ' . $defhandler . "\n";
+ }
+ }
+ }
+ }
+
+ public function createIpPort()
+ {
+ $result_ipsandports_stmt = Database::query("SELECT * FROM `" . TABLE_PANEL_IPSANDPORTS . "` ORDER BY `ip` ASC, `port` ASC");
+
+ while ($row_ipsandports = $result_ipsandports_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if (filter_var($row_ipsandports['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
+ $ipport = '[' . $row_ipsandports['ip'] . ']:' . $row_ipsandports['port'];
+ } else {
+ $ipport = $row_ipsandports['ip'] . ':' . $row_ipsandports['port'];
+ }
+
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'apache::createIpPort: creating ip/port settings for ' . $ipport);
+ $vhosts_filename = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.apacheconf_vhost') . '/10_froxlor_ipandport_' . trim(str_replace(':', '.', $row_ipsandports['ip']), '.') . '.' . $row_ipsandports['port'] . '.conf');
+
+ if (! isset($this->virtualhosts_data[$vhosts_filename])) {
+ $this->virtualhosts_data[$vhosts_filename] = '';
+ }
+
+ if ($row_ipsandports['listen_statement'] == '1') {
+ $this->virtualhosts_data[$vhosts_filename] .= 'Listen ' . $ipport . "\n";
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, $ipport . ' :: inserted listen-statement');
+ }
+
+ if ($row_ipsandports['namevirtualhost_statement'] == '1') {
+ // >=apache-2.4 enabled?
+ if (Settings::Get('system.apache24') == '1') {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, $ipport . ' :: namevirtualhost-statement no longer needed for apache-2.4');
+ } else {
+ $this->virtualhosts_data[$vhosts_filename] .= 'NameVirtualHost ' . $ipport . "\n";
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, $ipport . ' :: inserted namevirtualhost-statement');
+ }
+ }
+
+ if ($row_ipsandports['vhostcontainer'] == '1') {
+
+ $without_vhost = $this->virtualhosts_data[$vhosts_filename];
+ $close_vhost = true;
+
+ $this->virtualhosts_data[$vhosts_filename] .= '' . "\n";
+
+ $mypath = $this->getMyPath($row_ipsandports);
+
+ $this->virtualhosts_data[$vhosts_filename] .= 'DocumentRoot "' . rtrim($mypath, "/") . '"' . "\n";
+
+ if ($row_ipsandports['vhostcontainer_servername_statement'] == '1') {
+ $this->virtualhosts_data[$vhosts_filename] .= ' ServerName ' . Settings::Get('system.hostname') . "\n";
+
+ $froxlor_aliases = Settings::Get('system.froxloraliases');
+ if (! empty($froxlor_aliases)) {
+ $froxlor_aliases = explode(",", $froxlor_aliases);
+ $aliases = "";
+ foreach ($froxlor_aliases as $falias) {
+ if (\Froxlor\Validate\Validate::validateDomain(trim($falias))) {
+ $aliases .= trim($falias) . " ";
+ }
+ }
+ $aliases = trim($aliases);
+ if (! empty($aliases)) {
+ $this->virtualhosts_data[$vhosts_filename] .= ' ServerAlias ' . $aliases . "\n";
+ }
+ }
+ }
+
+ $is_redirect = false;
+ // check for SSL redirect
+ if ($row_ipsandports['ssl'] == '0' && Settings::Get('system.le_froxlor_redirect') == '1') {
+ $is_redirect = true;
+ // check whether froxlor uses Let's Encrypt and not cert is being generated yet
+ // or a renew is ongoing - disable redirect
+ if (Settings::Get('system.le_froxlor_enabled') && ($this->froxlorVhostHasLetsEncryptCert() == false || $this->froxlorVhostLetsEncryptNeedsRenew())) {
+ $this->virtualhosts_data[$vhosts_filename] .= '# temp. disabled ssl-redirect due to Let\'s Encrypt certificate generation.' . PHP_EOL;
+ $is_redirect = false;
+ } else {
+ $_sslport = $this->checkAlternativeSslPort();
+
+ $mypath = 'https://' . Settings::Get('system.hostname') . $_sslport . '/';
+ $code = '301';
+ $modrew_red = ' [R=' . $code . ';L,NE]';
+
+ // redirect everything, not only root-directory, #541
+ $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' RewriteEngine On' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' RewriteCond %{HTTPS} off' . "\n";
+ if (Settings::Get('system.le_froxlor_enabled') == '1') {
+ $this->virtualhosts_data[$vhosts_filename] .= ' RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge' . "\n";
+ }
+ $this->virtualhosts_data[$vhosts_filename] .= ' RewriteRule ^/(.*) ' . $mypath . '$1' . $modrew_red . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' Redirect ' . $code . ' / ' . $mypath . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n";
+ }
+ }
+
+ if (! $is_redirect) {
+ // create fcgid -Part (starter is created in apache_fcgid)
+ if (Settings::Get('system.mod_fcgid_ownvhost') == '1' && Settings::Get('system.mod_fcgid') == '1') {
+ $configdir = \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.mod_fcgid_configdir') . '/froxlor.panel/' . Settings::Get('system.hostname'));
+ $this->virtualhosts_data[$vhosts_filename] .= ' FcgidIdleTimeout ' . Settings::Get('system.mod_fcgid_idle_timeout') . "\n";
+ if ((int) Settings::Get('system.mod_fcgid_wrapper') == 0) {
+ $this->virtualhosts_data[$vhosts_filename] .= ' SuexecUserGroup "' . Settings::Get('system.mod_fcgid_httpuser') . '" "' . Settings::Get('system.mod_fcgid_httpgroup') . '"' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' ScriptAlias /php/ ' . $configdir . "\n";
+ } else {
+ $domain = array(
+ 'id' => 'none',
+ 'domain' => Settings::Get('system.hostname'),
+ 'adminid' => 1, /* first admin-user (superadmin) */
+ 'mod_fcgid_starter' => - 1,
+ 'mod_fcgid_maxrequests' => - 1,
+ 'guid' => Settings::Get('system.mod_fcgid_httpuser'),
+ 'openbasedir' => 0,
+ 'email' => Settings::Get('panel.adminmail'),
+ 'loginname' => 'froxlor.panel',
+ 'documentroot' => $mypath,
+ 'customerroot' => $mypath
+ );
+ $php = new PhpInterface($domain);
+ $phpconfig = $php->getPhpConfig(Settings::Get('system.mod_fcgid_defaultini_ownvhost'));
+
+ $starter_filename = \Froxlor\FileDir::makeCorrectFile($configdir . '/php-fcgi-starter');
+ $this->virtualhosts_data[$vhosts_filename] .= ' SuexecUserGroup "' . Settings::Get('system.mod_fcgid_httpuser') . '" "' . Settings::Get('system.mod_fcgid_httpgroup') . '"' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n";
+ $file_extensions = explode(' ', $phpconfig['file_extensions']);
+ $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' SetHandler fcgid-script' . "\n";
+ foreach ($file_extensions as $file_extension) {
+ $this->virtualhosts_data[$vhosts_filename] .= ' FcgidWrapper ' . $starter_filename . ' .' . $file_extension . "\n";
+ }
+ $this->virtualhosts_data[$vhosts_filename] .= ' Options +ExecCGI' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n";
+ // >=apache-2.4 enabled?
+ if (Settings::Get('system.apache24') == '1') {
+ $mypath_dir = new \Froxlor\Http\Directory($mypath);
+ // only create the require all granted if there is not active directory-protection
+ // for this path, as this would be the first require and therefore grant all access
+ if ($mypath_dir->isUserProtected() == false) {
+ $this->virtualhosts_data[$vhosts_filename] .= ' Require all granted' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' AllowOverride All' . "\n";
+ }
+ } else {
+ $this->virtualhosts_data[$vhosts_filename] .= ' Order allow,deny' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' allow from all' . "\n";
+ }
+ $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n";
+ }
+ } elseif (Settings::Get('phpfpm.enabled') == '1' && (int) Settings::Get('phpfpm.enabled_ownvhost') == 1) {
+ // get fpm config
+ $fpm_sel_stmt = Database::prepare("
+ SELECT f.id FROM `" . TABLE_PANEL_FPMDAEMONS . "` f
+ LEFT JOIN `" . TABLE_PANEL_PHPCONFIGS . "` p ON p.fpmsettingid = f.id
+ WHERE p.id = :phpconfigid
+ ");
+ $fpm_config = Database::pexecute_first($fpm_sel_stmt, array(
+ 'phpconfigid' => Settings::Get('phpfpm.vhost_defaultini')
+ ));
+ // create php-fpm -Part (config is created in apache_fcgid)
+ $domain = array(
+ 'id' => 'none',
+ 'domain' => Settings::Get('system.hostname'),
+ 'adminid' => 1, /* first admin-user (superadmin) */
+ 'mod_fcgid_starter' => - 1,
+ 'mod_fcgid_maxrequests' => - 1,
+ 'guid' => Settings::Get('phpfpm.vhost_httpuser'),
+ 'openbasedir' => 0,
+ 'email' => Settings::Get('panel.adminmail'),
+ 'loginname' => 'froxlor.panel',
+ 'documentroot' => $mypath,
+ 'customerroot' => $mypath,
+ 'fpm_config_id' => isset($fpm_config['id']) ? $fpm_config['id'] : 1
+ );
+
+ $php = new phpinterface($domain);
+ $phpconfig = $php->getPhpConfig(Settings::Get('phpfpm.vhost_defaultini'));
+ $srvName = substr(md5($ipport), 0, 4) . '.fpm.external';
+ if ($row_ipsandports['ssl']) {
+ $srvName = substr(md5($ipport), 0, 4) . '.ssl-fpm.external';
+ }
+
+ // mod_proxy stuff for apache-2.4
+ if (Settings::Get('system.apache24') == '1' && Settings::Get('phpfpm.use_mod_proxy') == '1') {
+ $filesmatch = $phpconfig['fpm_settings']['limit_extensions'];
+ $extensions = explode(" ", $filesmatch);
+ $filesmatch = "";
+ foreach ($extensions as $ext) {
+ $filesmatch .= substr($ext, 1) . '|';
+ }
+ // start block, cut off last pipe and close block
+ $filesmatch = '(' . str_replace(".", "\.", substr($filesmatch, 0, - 1)) . ')';
+ $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' SetHandler proxy:unix:' . $php->getInterface()->getSocketFile() . '|fcgi://localhost' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n";
+ if ($phpconfig['pass_authorizationheader'] == '1') {
+ $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' CGIPassAuth On' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n";
+ }
+ } else {
+ $addheader = "";
+ if ($phpconfig['pass_authorizationheader'] == '1') {
+ $addheader = " -pass-header Authorization";
+ }
+ $this->virtualhosts_data[$vhosts_filename] .= ' FastCgiExternalServer ' . $php->getInterface()->getAliasConfigDir() . $srvName . ' -socket ' . $php->getInterface()->getSocketFile() . ' -idle-timeout ' . $phpconfig['fpm_settings']['idle_timeout'] . $addheader . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n";
+ $filesmatch = $phpconfig['fpm_settings']['limit_extensions'];
+ $extensions = explode(" ", $filesmatch);
+ $filesmatch = "";
+ foreach ($extensions as $ext) {
+ $filesmatch .= substr($ext, 1) . '|';
+ }
+ // start block, cut off last pipe and close block
+ $filesmatch = '(' . str_replace(".", "\.", substr($filesmatch, 0, - 1)) . ')';
+ $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' AddHandler php-fastcgi .php' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' Action php-fastcgi /fastcgiphp' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' Options +ExecCGI' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n";
+ // >=apache-2.4 enabled?
+ if (Settings::Get('system.apache24') == '1') {
+ $mypath_dir = new \Froxlor\Http\Directory($mypath);
+ // only create the require all granted if there is not active directory-protection
+ // for this path, as this would be the first require and therefore grant all access
+ if ($mypath_dir->isUserProtected() == false) {
+ $this->virtualhosts_data[$vhosts_filename] .= ' Require all granted' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' AllowOverride All' . "\n";
+ }
+ } else {
+ $this->virtualhosts_data[$vhosts_filename] .= ' Order allow,deny' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' allow from all' . "\n";
+ }
+ $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' Alias /fastcgiphp ' . $php->getInterface()->getAliasConfigDir() . $srvName . "\n";
+ }
+ } else {
+ // mod_php
+ $domain = array(
+ 'id' => 'none',
+ 'domain' => Settings::Get('system.hostname'),
+ 'adminid' => 1, /* first admin-user (superadmin) */
+ 'guid' => Settings::Get('system.httpuser'),
+ 'openbasedir' => 0,
+ 'email' => Settings::Get('panel.adminmail'),
+ 'loginname' => 'froxlor.panel',
+ 'documentroot' => $mypath,
+ 'customerroot' => $mypath
+ );
+ }
+ // end of ssl-redirect check
+ } else {
+ // fallback of froxlor domain-data for processSpecialConfigTemplate()
+ $domain = array(
+ 'domain' => Settings::Get('system.hostname'),
+ 'loginname' => 'froxlor.panel',
+ 'documentroot' => $mypath,
+ 'customerroot' => $mypath
+ );
+ }
+
+ /**
+ * dirprotection, see #72
+ *
+ * @todo deferred until 0.9.5, needs more testing
+ * $this->virtualhosts_data[$vhosts_filename] .= "\t\n";
+ * $this->virtualhosts_data[$vhosts_filename] .= "\t\tAllow from all\n";
+ * $this->virtualhosts_data[$vhosts_filename] .= "\t\tOptions -Indexes\n";
+ * $this->virtualhosts_data[$vhosts_filename] .= "\t \n";
+ *
+ * $this->virtualhosts_data[$vhosts_filename] .= "\t\n";
+ * $this->virtualhosts_data[$vhosts_filename] .= "\t\tOrder Deny,Allow\n";
+ * $this->virtualhosts_data[$vhosts_filename] .= "\t\tDeny from All\n";
+ * $this->virtualhosts_data[$vhosts_filename] .= "\t \n";
+ * end of dirprotection
+ */
+
+ if ($row_ipsandports['specialsettings'] != '' && ($row_ipsandports['ssl'] == '0' || ($row_ipsandports['ssl'] == '1' && Settings::Get('system.use_ssl') == '1' && $row_ipsandports['include_specialsettings'] == '1'))) {
+ $this->virtualhosts_data[$vhosts_filename] .= $this->processSpecialConfigTemplate($row_ipsandports['specialsettings'], $domain, $row_ipsandports['ip'], $row_ipsandports['port'], $row_ipsandports['ssl'] == '1') . "\n";
+ }
+
+ if ($row_ipsandports['ssl'] == '1' && Settings::Get('system.use_ssl') == '1') {
+
+ if ($row_ipsandports['ssl_specialsettings'] != '') {
+ $this->virtualhosts_data[$vhosts_filename] .= $this->processSpecialConfigTemplate($row_ipsandports['ssl_specialsettings'], $domain, $row_ipsandports['ip'], $row_ipsandports['port'], $row_ipsandports['ssl'] == '1') . "\n";
+ }
+
+ // check for required fallback
+ if (($row_ipsandports['ssl_cert_file'] == '' || ! file_exists($row_ipsandports['ssl_cert_file'])) && (Settings::Get('system.le_froxlor_enabled') == '0' || $this->froxlorVhostHasLetsEncryptCert() == false)) {
+ $row_ipsandports['ssl_cert_file'] = Settings::Get('system.ssl_cert_file');
+ if (! file_exists($row_ipsandports['ssl_cert_file'])) {
+ // explicitly disable ssl for this vhost
+ $row_ipsandports['ssl_cert_file'] = "";
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'System certificate file "' . Settings::Get('system.ssl_cert_file') . '" does not seem to exist. Disabling SSL-vhost for "' . Settings::Get('system.hostname') . '"');
+ }
+ }
+
+ if ($row_ipsandports['ssl_key_file'] == '') {
+ $row_ipsandports['ssl_key_file'] = Settings::Get('system.ssl_key_file');
+ if (! file_exists($row_ipsandports['ssl_key_file'])) {
+ // explicitly disable ssl for this vhost
+ $row_ipsandports['ssl_cert_file'] = "";
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'System certificate key-file "' . Settings::Get('system.ssl_key_file') . '" does not seem to exist. Disabling SSL-vhost for "' . Settings::Get('system.hostname') . '"');
+ }
+ }
+
+ if ($row_ipsandports['ssl_ca_file'] == '') {
+ $row_ipsandports['ssl_ca_file'] = Settings::Get('system.ssl_ca_file');
+ }
+
+ // #418
+ if ($row_ipsandports['ssl_cert_chainfile'] == '') {
+ $row_ipsandports['ssl_cert_chainfile'] = Settings::Get('system.ssl_cert_chainfile');
+ }
+
+ $domain = array(
+ 'id' => 0,
+ 'domain' => Settings::Get('system.hostname'),
+ 'adminid' => 1, /* first admin-user (superadmin) */
+ 'loginname' => 'froxlor.panel',
+ 'documentroot' => $mypath,
+ 'customerroot' => $mypath,
+ 'parentdomainid' => 0,
+ 'ssl_honorcipherorder' => Settings::Get('system.honorcipherorder'),
+ 'ssl_sessiontickets' => Settings::Get('system.sessiontickets')
+ );
+
+ // override corresponding array values
+ $domain['ssl_cert_file'] = $row_ipsandports['ssl_cert_file'];
+ $domain['ssl_key_file'] = $row_ipsandports['ssl_key_file'];
+ $domain['ssl_ca_file'] = $row_ipsandports['ssl_ca_file'];
+ $domain['ssl_cert_chainfile'] = $row_ipsandports['ssl_cert_chainfile'];
+
+ // SSL STUFF
+ $dssl = new DomainSSL();
+ // this sets the ssl-related array-indices in the $domain array
+ // if the domain has customer-defined ssl-certificates
+ $dssl->setDomainSSLFilesArray($domain);
+
+ if ($domain['ssl_cert_file'] != '') {
+
+ // check for existence, #1485
+ if (! file_exists($domain['ssl_cert_file'])) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, $ipport . ' :: certificate file "' . $domain['ssl_cert_file'] . '" does not exist! Cannot create ssl-directives');
+ } else {
+
+ $this->virtualhosts_data[$vhosts_filename] .= ' SSLEngine On' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' SSLProtocol -ALL +' . str_replace(",", " +", Settings::Get('system.ssl_protocols')) . "\n";
+ if (Settings::Get('system.apache24') == '1') {
+ if (Settings::Get('system.http2_support') == '1') {
+ $this->virtualhosts_data[$vhosts_filename] .= ' Protocols h2 http/1.1' . "\n";
+ }
+ if (! empty(Settings::Get('system.dhparams_file'))) {
+ $dhparams = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.dhparams_file'));
+ if (! file_exists($dhparams)) {
+ \Froxlor\FileDir::safe_exec('openssl dhparam -out ' . escapeshellarg($dhparams) . ' 4096');
+ }
+ $this->virtualhosts_data[$vhosts_filename] .= ' SSLOpenSSLConfCmd DHParameters "' . $dhparams . '"' . "\n";
+ }
+ $this->virtualhosts_data[$vhosts_filename] .= ' SSLCompression Off' . "\n";
+ if (Settings::Get('system.sessionticketsenabled') == '1') {
+ $this->virtualhosts_data[$vhosts_filename] .= ' SSLSessionTickets ' . ($domain['ssl_sessiontickets'] == '1' ? 'on' : 'off') . "\n";
+ }
+ }
+
+ $this->virtualhosts_data[$vhosts_filename] .= ' SSLHonorCipherOrder ' . ($domain['ssl_honorcipherorder'] == '1' ? 'on' : 'off') . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' SSLCipherSuite ' . Settings::Get('system.ssl_cipher_list') . "\n";
+ $protocols = array_map('trim', explode(",", Settings::Get('system.ssl_protocols')));
+ if (in_array("TLSv1.3", $protocols) && ! empty(Settings::Get('system.tlsv13_cipher_list')) && Settings::Get('system.apache24') == 1) {
+ $this->virtualhosts_data[$vhosts_filename] .= ' SSLCipherSuite TLSv1.3 ' . Settings::Get('system.tlsv13_cipher_list') . "\n";
+ }
+ $this->virtualhosts_data[$vhosts_filename] .= ' SSLVerifyDepth 10' . "\n";
+ $this->virtualhosts_data[$vhosts_filename] .= ' SSLCertificateFile ' . \Froxlor\FileDir::makeCorrectFile($domain['ssl_cert_file']) . "\n";
+
+ if ($domain['ssl_key_file'] != '') {
+ // check for existence, #1485
+ if (! file_exists($domain['ssl_key_file'])) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, $ipport . ' :: certificate key file "' . $domain['ssl_key_file'] . '" does not exist! Cannot create ssl-directives');
+ } else {
+ $this->virtualhosts_data[$vhosts_filename] .= ' SSLCertificateKeyFile ' . \Froxlor\FileDir::makeCorrectFile($domain['ssl_key_file']) . "\n";
+ }
+ }
+
+ if ($domain['ssl_ca_file'] != '') {
+ // check for existence, #1485
+ if (! file_exists($domain['ssl_ca_file'])) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, $ipport . ' :: certificate CA file "' . $domain['ssl_ca_file'] . '" does not exist! Cannot create ssl-directives');
+ } else {
+ $this->virtualhosts_data[$vhosts_filename] .= ' SSLCACertificateFile ' . \Froxlor\FileDir::makeCorrectFile($domain['ssl_ca_file']) . "\n";
+ }
+ }
+
+ // #418
+ if ($domain['ssl_cert_chainfile'] != '') {
+ // check for existence, #1485
+ if (! file_exists($domain['ssl_cert_chainfile'])) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, $ipport . ' :: certificate chain file "' . $domain['ssl_cert_chainfile'] . '" does not exist! Cannot create ssl-directives');
+ } else {
+ $this->virtualhosts_data[$vhosts_filename] .= ' SSLCertificateChainFile ' . \Froxlor\FileDir::makeCorrectFile($domain['ssl_cert_chainfile']) . "\n";
+ }
+ }
+ }
+ } else {
+ // if there is no cert-file specified but we are generating a ssl-vhost,
+ // we should return an empty string because this vhost would suck dick, ref #1583
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, $domain['domain'] . ' :: empty certificate file! Cannot create ssl-directives');
+ $this->virtualhosts_data[$vhosts_filename] = $without_vhost;
+ $this->virtualhosts_data[$vhosts_filename] .= '# no ssl-certificate was specified for this domain, therefore no explicit vhost-container is being generated';
+ $close_vhost = false;
+ }
+ }
+
+ if ($close_vhost) {
+ $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n";
+ }
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, $ipport . ' :: inserted vhostcontainer');
+ }
+ unset($vhosts_filename);
+ }
+
+ /**
+ * bug #32
+ */
+ $this->createStandardDirectoryEntry();
+
+ /**
+ * bug #unknown-yet
+ */
+ $this->createStandardErrorHandler();
+ }
+
+ /**
+ * We put together the needed php options in the virtualhost entries
+ *
+ * @param array $domain
+ * @param bool $ssl_vhost
+ *
+ * @return string
+ */
+ protected function composePhpOptions($domain, $ssl_vhost = false)
+ {
+ $php_options_text = '';
+
+ if ($domain['phpenabled_customer'] == 1 && $domain['phpenabled_vhost'] == '1') {
+ // This vHost has PHP enabled and we are using the regular mod_php
+ $cmail = \Froxlor\Customer\Customer::getCustomerDetail($domain['customerid'], 'email');
+ $php_options_text .= ' php_admin_value sendmail_path "/usr/sbin/sendmail -t -f ' . $cmail . '"' . PHP_EOL;
+
+ if ($domain['openbasedir'] == '1') {
+ if ($domain['openbasedir_path'] == '1' || strstr($domain['documentroot'], ":") !== false) {
+ $_phpappendopenbasedir = \Froxlor\Domain\Domain::appendOpenBasedirPath($domain['customerroot'], true);
+ } else {
+ $_phpappendopenbasedir = \Froxlor\Domain\Domain::appendOpenBasedirPath($domain['documentroot'], true);
+ }
+
+ $_custom_openbasedir = explode(':', Settings::Get('system.phpappendopenbasedir'));
+ foreach ($_custom_openbasedir as $cobd) {
+ $_phpappendopenbasedir .= \Froxlor\Domain\Domain::appendOpenBasedirPath($cobd);
+ }
+
+ $php_options_text .= ' php_admin_value open_basedir "' . $_phpappendopenbasedir . '"' . "\n";
+ }
+ } else {
+ $php_options_text .= ' # PHP is disabled for this vHost' . "\n";
+ $php_options_text .= ' php_flag engine off' . "\n";
+ }
+
+ /**
+ * check for apache-itk-support, #1400
+ * why is this here? Because it only works with mod_php
+ */
+ if (Settings::get('system.apacheitksupport') == 1) {
+ $php_options_text .= ' ' . "\n";
+ $php_options_text .= ' AssignUserID ' . $domain['loginname'] . ' ' . $domain['loginname'] . "\n";
+ $php_options_text .= ' ' . "\n";
+ }
+
+ return $php_options_text;
+ }
+
+ public function createOwnVhostStarter()
+ {
+ return;
+ }
+
+ /**
+ * We collect all servernames and Aliases
+ */
+ protected function getServerNames($domain)
+ {
+ $servernames_text = ' ServerName ' . $domain['domain'] . "\n";
+
+ $server_alias = '';
+ if ($domain['iswildcarddomain'] == '1') {
+ $server_alias = '*.' . $domain['domain'];
+ } elseif ($domain['wwwserveralias'] == '1') {
+ $server_alias = 'www.' . $domain['domain'];
+ }
+
+ if (trim($server_alias) != '') {
+ $servernames_text .= ' ServerAlias ' . $server_alias . "\n";
+ }
+
+ $alias_domains_stmt = Database::prepare("
+ SELECT `domain`, `iswildcarddomain`, `wwwserveralias`
+ FROM `" . TABLE_PANEL_DOMAINS . "`
+ WHERE `aliasdomain`= :domainid
+ ");
+ Database::pexecute($alias_domains_stmt, array(
+ 'domainid' => $domain['id']
+ ));
+
+ while (($alias_domain = $alias_domains_stmt->fetch(\PDO::FETCH_ASSOC)) !== false) {
+ $server_alias = ' ServerAlias ' . $alias_domain['domain'];
+
+ if ($alias_domain['iswildcarddomain'] == '1') {
+ $server_alias .= ' *.' . $alias_domain['domain'];
+ } else {
+ if ($alias_domain['wwwserveralias'] == '1') {
+ $server_alias .= ' www.' . $alias_domain['domain'];
+ }
+ }
+
+ $servernames_text .= $server_alias . "\n";
+ }
+
+ $servernames_text .= ' ServerAdmin ' . $domain['email'] . "\n";
+ return $servernames_text;
+ }
+
+ /**
+ * Let's get the webroot
+ */
+ protected function getWebroot($domain)
+ {
+ $webroot_text = '';
+ $domain['customerroot'] = \Froxlor\FileDir::makeCorrectDir($domain['customerroot']);
+ $domain['documentroot'] = \Froxlor\FileDir::makeCorrectDir($domain['documentroot']);
+
+ if ($domain['deactivated'] == '1' && Settings::Get('system.deactivateddocroot') != '') {
+ $webroot_text .= ' # Using docroot for deactivated users...' . "\n";
+ $webroot_text .= ' DocumentRoot "' . rtrim(\Froxlor\FileDir::makeCorrectDir(Settings::Get('system.deactivateddocroot')), "/") . "\"\n";
+ $webroot_text .= ' ' . "\n";
+ // >=apache-2.4 enabled?
+ if (Settings::Get('system.apache24') == '1') {
+ $webroot_text .= ' Require all granted' . "\n";
+ $webroot_text .= ' AllowOverride All' . "\n";
+ } else {
+ $webroot_text .= ' Order allow,deny' . "\n";
+ $webroot_text .= ' allow from all' . "\n";
+ }
+ $webroot_text .= ' ' . "\n";
+ $this->deactivated = true;
+ } else {
+ $webroot_text .= ' DocumentRoot "' . rtrim($domain['documentroot'], "/") . "\"\n";
+ $this->deactivated = false;
+ }
+
+ return $webroot_text;
+ }
+
+ /**
+ * Lets set the text part for the stats software
+ */
+ protected function getStats($domain)
+ {
+ $stats_text = '';
+
+ if ($domain['speciallogfile'] == '1') {
+ $statDomain = ($domain['parentdomainid'] == '0') ? $domain['domain'] : $domain['parentdomain'];
+ if (Settings::Get('system.awstats_enabled') == '1') {
+ $stats_text .= ' Alias /awstats "' . \Froxlor\FileDir::makeCorrectFile($domain['customerroot'] . '/awstats/' . $statDomain) . '"' . "\n";
+ $stats_text .= ' Alias /awstats-icon "' . \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.awstats_icons')) . '"' . "\n";
+ } else {
+ $stats_text .= ' Alias /webalizer "' . \Froxlor\FileDir::makeCorrectFile($domain['customerroot'] . '/webalizer/' . $statDomain) . '"' . "\n";
+ }
+ } else {
+ if ($domain['customerroot'] != $domain['documentroot']) {
+ if (Settings::Get('system.awstats_enabled') == '1') {
+ $stats_text .= ' Alias /awstats "' . \Froxlor\FileDir::makeCorrectFile($domain['customerroot'] . '/awstats/' . $domain['domain']) . '"' . "\n";
+ $stats_text .= ' Alias /awstats-icon "' . \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.awstats_icons')) . '"' . "\n";
+ } else {
+ $stats_text .= ' Alias /webalizer "' . \Froxlor\FileDir::makeCorrectFile($domain['customerroot'] . '/webalizer') . '"' . "\n";
+ }
+ } elseif (Settings::Get('system.awstats_enabled') == '1') {
+ // if the docroots are equal, we still have to set an alias for awstats
+ // because the stats are in /awstats/[domain], not just /awstats/
+ // also, the awstats-icons are someplace else too!
+ // -> webalizer does not need this!
+ $stats_text .= ' Alias /awstats "' . \Froxlor\FileDir::makeCorrectFile($domain['documentroot'] . '/awstats/' . $domain['domain']) . '"' . "\n";
+ $stats_text .= ' Alias /awstats-icon "' . \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.awstats_icons')) . '"' . "\n";
+ }
+ }
+
+ return $stats_text;
+ }
+
+ /**
+ * Lets set the logfiles
+ */
+ protected function getLogfiles($domain)
+ {
+ $logfiles_text = '';
+
+ if ($domain['speciallogfile'] == '1') {
+ if ($domain['parentdomainid'] == '0') {
+ $speciallogfile = '-' . $domain['domain'];
+ } else {
+ $speciallogfile = '-' . $domain['parentdomain'];
+ }
+ } else {
+ $speciallogfile = '';
+ }
+
+ if ($domain['writeerrorlog']) {
+ // The normal access/error - logging is enabled
+ $error_log = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.logfiles_directory') . $domain['loginname'] . $speciallogfile . '-error.log');
+ // Create the logfile if it does not exist (fixes #46)
+ touch($error_log);
+ chown($error_log, Settings::Get('system.httpuser'));
+ chgrp($error_log, Settings::Get('system.httpgroup'));
+ // set error log log-level
+ $logfiles_text .= ' LogLevel ' . \Froxlor\Settings::Get('system.errorlog_level') . "\n";
+ } else {
+ $error_log = '/dev/null';
+ }
+
+ if ($domain['writeaccesslog']) {
+ $access_log = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.logfiles_directory') . $domain['loginname'] . $speciallogfile . '-access.log');
+ // Create the logfile if it does not exist (fixes #46)
+ touch($access_log);
+ chown($access_log, Settings::Get('system.httpuser'));
+ chgrp($access_log, Settings::Get('system.httpgroup'));
+ } else {
+ $access_log = '/dev/null';
+ }
+
+ $logtype = 'combined';
+ if (Settings::Get('system.logfiles_format') != '') {
+ $logtype = 'frx_custom';
+ $logfiles_text .= ' LogFormat ' . Settings::Get('system.logfiles_format') . ' ' . $logtype . "\n";
+ }
+ if (Settings::Get('system.logfiles_type') == '2' && Settings::Get('system.logfiles_format') == '') {
+ $logtype = 'vhost_combined';
+ }
+
+ if (Settings::Get('system.logfiles_piped') == '1' && Settings::Get('system.logfiles_script') != '') {
+ // replace for error_log
+ $command = \Froxlor\PhpHelper::replaceVariables(Settings::Get('system.logfiles_script'), array(
+ 'LOGFILE' => $error_log,
+ 'DOMAIN' => $domain['domain'],
+ 'CUSTOMER' => $domain['loginname']
+ ));
+ $logfiles_text .= ' ErrorLog "|' . $command . "\"\n";
+ // replace for access_log
+ $command = \Froxlor\PhpHelper::replaceVariables(Settings::Get('system.logfiles_script'), array(
+ 'LOGFILE' => $access_log,
+ 'DOMAIN' => $domain['domain'],
+ 'CUSTOMER' => $domain['loginname']
+ ));
+ $logfiles_text .= ' CustomLog "|' . $command . '" ' . $logtype . "\n";
+ } else {
+ // Create the logfile if it does not exist (fixes #46)
+ touch($error_log);
+ chown($error_log, Settings::Get('system.httpuser'));
+ chgrp($error_log, Settings::Get('system.httpgroup'));
+ touch($access_log);
+ chown($access_log, Settings::Get('system.httpuser'));
+ chgrp($access_log, Settings::Get('system.httpgroup'));
+
+ $logfiles_text .= ' ErrorLog "' . $error_log . '"' . "\n";
+ $logfiles_text .= ' CustomLog "' . $access_log . '" ' . $logtype . "\n";
+ }
+
+ if (Settings::Get('system.awstats_enabled') == '1') {
+ if ((int) $domain['parentdomainid'] == 0) {
+ // prepare the aliases and subdomains for stats config files
+ $server_alias = '';
+ $alias_domains_stmt = Database::prepare("
+ SELECT `domain`, `iswildcarddomain`, `wwwserveralias`
+ FROM `" . TABLE_PANEL_DOMAINS . "`
+ WHERE `aliasdomain` = :domainid OR `parentdomainid` = :domainid
+ ");
+ Database::pexecute($alias_domains_stmt, array(
+ 'domainid' => $domain['id']
+ ));
+
+ while (($alias_domain = $alias_domains_stmt->fetch(\PDO::FETCH_ASSOC)) !== false) {
+
+ $server_alias .= ' ' . $alias_domain['domain'] . ' ';
+
+ if ($alias_domain['iswildcarddomain'] == '1') {
+ $server_alias .= '*.' . $alias_domain['domain'];
+ } elseif ($alias_domain['wwwserveralias'] == '1') {
+ $server_alias .= 'www.' . $alias_domain['domain'];
+ }
+ }
+
+ $alias = '';
+ if ($domain['iswildcarddomain'] == '1') {
+ $alias = '*.' . $domain['domain'];
+ } elseif ($domain['wwwserveralias'] == '1') {
+ $alias = 'www.' . $domain['domain'];
+ }
+
+ // After inserting the AWStats information,
+ // be sure to build the awstats conf file as well
+ // and chown it using $awstats_params, #258
+ // Bug 960 + Bug 970 : Use full $domain instead of custom $awstats_params as following classes depend on the informations
+ \Froxlor\Http\Statistics::createAWStatsConf(Settings::Get('system.logfiles_directory') . $domain['loginname'] . $speciallogfile . '-access.log', $domain['domain'], $alias . $server_alias, $domain['customerroot'], $domain);
+ }
+ }
+
+ return $logfiles_text;
+ }
+
+ /**
+ * Get the filename for the virtualhost
+ */
+ protected function getVhostFilename($domain, $ssl_vhost = false)
+ {
+ if ((int) $domain['parentdomainid'] == 0 && \Froxlor\Domain\Domain::isCustomerStdSubdomain((int) $domain['id']) == false && ((int) $domain['ismainbutsubto'] == 0 || \Froxlor\Domain\Domain::domainMainToSubExists($domain['ismainbutsubto']) == false)) {
+ $vhost_no = '35';
+ } elseif ((int) $domain['parentdomainid'] == 0 && \Froxlor\Domain\Domain::isCustomerStdSubdomain((int) $domain['id']) == false && (int) $domain['ismainbutsubto'] > 0) {
+ $vhost_no = '30';
+ } else {
+ // number of dots in a domain specifies it's position (and depth of subdomain) starting at 29 going downwards on higher depth
+ $vhost_no = (string) (30 - substr_count($domain['domain'], ".") + 1);
+ }
+
+ if ($ssl_vhost === true) {
+ $vhost_filename = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.apacheconf_vhost') . '/' . $vhost_no . '_froxlor_ssl_vhost_' . $domain['domain'] . '.conf');
+ } else {
+ $vhost_filename = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.apacheconf_vhost') . '/' . $vhost_no . '_froxlor_normal_vhost_' . $domain['domain'] . '.conf');
+ }
+
+ return $vhost_filename;
+ }
+
+ /**
+ * We compose the virtualhost entry for one domain
+ */
+ protected function getVhostContent($domain, $ssl_vhost = false)
+ {
+ if ($ssl_vhost === true && ($domain['ssl_redirect'] != '1' && $domain['ssl'] != '1')) {
+ return '';
+ }
+
+ $query = "SELECT * FROM `" . TABLE_PANEL_IPSANDPORTS . "` `i`, `" . TABLE_DOMAINTOIP . "` `dip`
+ WHERE dip.id_domain = :domainid AND i.id = dip.id_ipandports ";
+
+ if ($ssl_vhost === true && ($domain['ssl'] == '1' || $domain['ssl_redirect'] == '1')) {
+ // by ordering by cert-file the row with filled out SSL-Fields will be shown last, thus it is enough to fill out 1 set of SSL-Fields
+ $query .= "AND i.ssl = '1' ORDER BY i.ssl_cert_file ASC;";
+ } else {
+ $query .= "AND i.ssl = '0';";
+ }
+
+ $vhost_content = '';
+ $result_stmt = Database::prepare($query);
+ Database::pexecute($result_stmt, array(
+ 'domainid' => $domain['id']
+ ));
+
+ $ipportlist = '';
+ $_vhost_content = '';
+ while ($ipandport = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ $ipport = '';
+ $domain['ip'] = $ipandport['ip'];
+ $domain['port'] = $ipandport['port'];
+ if ($domain['ssl'] == '1') {
+ $domain['ssl_cert_file'] = $ipandport['ssl_cert_file'];
+ $domain['ssl_key_file'] = $ipandport['ssl_key_file'];
+ $domain['ssl_ca_file'] = $ipandport['ssl_ca_file'];
+ $domain['ssl_cert_chainfile'] = $ipandport['ssl_cert_chainfile'];
+
+ // SSL STUFF
+ $dssl = new DomainSSL();
+ // this sets the ssl-related array-indices in the $domain array
+ // if the domain has customer-defined ssl-certificates
+ $dssl->setDomainSSLFilesArray($domain);
+ }
+
+ if (filter_var($domain['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
+ $ipport = '[' . $domain['ip'] . ']:' . $domain['port'] . ' ';
+ } else {
+ $ipport = $domain['ip'] . ':' . $domain['port'] . ' ';
+ }
+
+ if ($ipandport['default_vhostconf_domain'] != '' && ($ssl_vhost == false || ($ssl_vhost == true && $ipandport['include_default_vhostconf_domain'] == '1'))) {
+ $_vhost_content .= $this->processSpecialConfigTemplate($ipandport['default_vhostconf_domain'], $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n";
+ }
+ if ($ipandport['ssl_default_vhostconf_domain'] != '' && $ssl_vhost == true) {
+ $_vhost_content .= $this->processSpecialConfigTemplate($ipandport['ssl_default_vhostconf_domain'], $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n";
+ }
+ $ipportlist .= $ipport;
+ }
+
+ $vhost_content .= '' . "\n";
+ $vhost_content .= $this->getServerNames($domain);
+
+ $domain['documentroot_norewrite'] = $domain['documentroot'];
+ if (($ssl_vhost == false && $domain['ssl'] == '1' && $domain['ssl_redirect'] == '1')) {
+ // We must not check if our port differs from port 443,
+ // but if there is a destination-port != 443
+ $_sslport = '';
+ // This returns the first port that is != 443 with ssl enabled, if any
+ // ordered by ssl-certificate (if any) so that the ip/port combo
+ // with certificate is used
+ $ssldestport_stmt = Database::prepare("
+ SELECT `ip`.`port` FROM " . TABLE_PANEL_IPSANDPORTS . " `ip`
+ LEFT JOIN `" . TABLE_DOMAINTOIP . "` `dip` ON (`ip`.`id` = `dip`.`id_ipandports`)
+ WHERE `dip`.`id_domain` = :domainid
+ AND `ip`.`ssl` = '1' AND `ip`.`port` != 443
+ ORDER BY `ip`.`ssl_cert_file` DESC, `ip`.`port` LIMIT 1;
+ ");
+ $ssldestport = Database::pexecute_first($ssldestport_stmt, array(
+ 'domainid' => $domain['id']
+ ));
+
+ if ($ssldestport && $ssldestport['port'] != '') {
+ $_sslport = ":" . $ssldestport['port'];
+ }
+
+ $domain['documentroot'] = 'https://%{HTTP_HOST}' . $_sslport . '/';
+ $domain['documentroot_norewrite'] = 'https://' . $domain['domain'] . $_sslport . '/';
+ }
+
+ if ($ssl_vhost === true && $domain['ssl'] == '1' && Settings::Get('system.use_ssl') == '1') {
+ if ($domain['ssl_cert_file'] == '' || ! file_exists($domain['ssl_cert_file'])) {
+ $domain['ssl_cert_file'] = Settings::Get('system.ssl_cert_file');
+ if (! file_exists($domain['ssl_cert_file'])) {
+ // explicitly disable ssl for this vhost
+ $domain['ssl_cert_file'] = "";
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'System certificate file "' . Settings::Get('system.ssl_cert_file') . '" does not seem to exist. Disabling SSL-vhost for "' . $domain['domain'] . '"');
+ }
+ }
+
+ if ($domain['ssl_key_file'] == '' || ! file_exists($domain['ssl_key_file'])) {
+ $domain['ssl_key_file'] = Settings::Get('system.ssl_key_file');
+ if (! file_exists($domain['ssl_key_file'])) {
+ // explicitly disable ssl for this vhost
+ $domain['ssl_cert_file'] = "";
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'System certificate key-file "' . Settings::Get('system.ssl_key_file') . '" does not seem to exist. Disabling SSL-vhost for "' . $domain['domain'] . '"');
+ }
+ }
+
+ if ($domain['ssl_ca_file'] == '') {
+ $domain['ssl_ca_file'] = Settings::Get('system.ssl_ca_file');
+ }
+
+ if ($domain['ssl_cert_chainfile'] == '') {
+ $domain['ssl_cert_chainfile'] = Settings::Get('system.ssl_cert_chainfile');
+ }
+
+ if ($domain['ssl_cert_file'] != '') {
+
+ $ssl_protocols = ($domain['override_tls'] == '1' && ! empty($domain['ssl_protocols'])) ? $domain['ssl_protocols'] : Settings::Get('system.ssl_protocols');
+ $ssl_cipher_list = ($domain['override_tls'] == '1' && ! empty($domain['ssl_cipher_list'])) ? $domain['ssl_cipher_list'] : Settings::Get('system.ssl_cipher_list');
+ $tlsv13_cipher_list = ($domain['override_tls'] == '1' && ! empty($domain['tlsv13_cipher_list'])) ? $domain['tlsv13_cipher_list'] : Settings::Get('system.tlsv13_cipher_list');
+
+ $vhost_content .= ' SSLEngine On' . "\n";
+ $vhost_content .= ' SSLProtocol -ALL +' . str_replace(",", " +", $ssl_protocols) . "\n";
+ if (Settings::Get('system.apache24') == '1') {
+ if (isset($domain['http2']) && $domain['http2'] == '1' && Settings::Get('system.http2_support') == '1') {
+ $vhost_content .= ' Protocols h2 http/1.1' . "\n";
+ }
+ if (! empty(Settings::Get('system.dhparams_file'))) {
+ $dhparams = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.dhparams_file'));
+ if (! file_exists($dhparams)) {
+ \Froxlor\FileDir::safe_exec('openssl dhparam -out ' . escapeshellarg($dhparams) . ' 4096');
+ }
+ $vhost_content .= ' SSLOpenSSLConfCmd DHParameters "' . $dhparams . '"' . "\n";
+ }
+ $vhost_content .= ' SSLCompression Off' . "\n";
+ if (Settings::Get('system.sessionticketsenabled') == '1') {
+ $vhost_content .= ' SSLSessionTickets ' . ($domain['ssl_sessiontickets'] == '1' ? 'on' : 'off') . "\n";
+ }
+ }
+ $vhost_content .= ' SSLHonorCipherOrder ' . ($domain['ssl_honorcipherorder'] == '1' ? 'on' : 'off') . "\n";
+ $vhost_content .= ' SSLCipherSuite ' . $ssl_cipher_list . "\n";
+ $protocols = array_map('trim', explode(",", $ssl_protocols));
+ if (in_array("TLSv1.3", $protocols) && ! empty($tlsv13_cipher_list) && Settings::Get('system.apache24') == 1) {
+ $vhost_content .= ' SSLCipherSuite TLSv1.3 ' . $tlsv13_cipher_list . "\n";
+ }
+ $vhost_content .= ' SSLVerifyDepth 10' . "\n";
+ $vhost_content .= ' SSLCertificateFile ' . \Froxlor\FileDir::makeCorrectFile($domain['ssl_cert_file']) . "\n";
+
+ if ($domain['ssl_key_file'] != '') {
+ $vhost_content .= ' SSLCertificateKeyFile ' . \Froxlor\FileDir::makeCorrectFile($domain['ssl_key_file']) . "\n";
+ }
+
+ if ($domain['ssl_ca_file'] != '') {
+ $vhost_content .= ' SSLCACertificateFile ' . \Froxlor\FileDir::makeCorrectFile($domain['ssl_ca_file']) . "\n";
+ }
+
+ if ($domain['ssl_cert_chainfile'] != '') {
+ $vhost_content .= ' SSLCertificateChainFile ' . \Froxlor\FileDir::makeCorrectFile($domain['ssl_cert_chainfile']) . "\n";
+ }
+
+ if (Settings::Get('system.apache24') == '1' && isset($domain['ocsp_stapling']) && $domain['ocsp_stapling'] == '1') {
+ $vhost_content .= ' SSLUseStapling on' . PHP_EOL;
+ }
+
+ if ($domain['hsts'] >= 0) {
+ $vhost_content .= ' ' . "\n";
+ $vhost_content .= ' Header always set Strict-Transport-Security "max-age=' . $domain['hsts'];
+ if ($domain['hsts_sub'] == 1) {
+ $vhost_content .= '; includeSubDomains';
+ }
+ if ($domain['hsts_preload'] == 1) {
+ $vhost_content .= '; preload';
+ }
+ $vhost_content .= '"' . "\n";
+ $vhost_content .= ' ' . "\n";
+ }
+ } else {
+ // if there is no cert-file specified but we are generating a ssl-vhost,
+ // we should return an empty string because this vhost would suck dick, ref #1583
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, $domain['domain'] . ' :: empty certificate file! Cannot create ssl-directives');
+ return '# no ssl-certificate was specified for this domain, therefore no explicit vhost is being generated';
+ }
+ }
+
+ // avoid using any whitespaces
+ $domain['documentroot'] = trim($domain['documentroot']);
+
+ if (preg_match('/^https?\:\/\//', $domain['documentroot'])) {
+ $corrected_docroot = $domain['documentroot'];
+
+ // Get domain's redirect code
+ $code = \Froxlor\Domain\Domain::getDomainRedirectCode($domain['id']);
+ $modrew_red = '';
+ if ($code != '') {
+ $modrew_red = ' [R=' . $code . ';L,NE]';
+ }
+
+ // redirect everything, not only root-directory, #541
+ $vhost_content .= ' ' . "\n";
+ $vhost_content .= ' RewriteEngine On' . "\n";
+ if (! $ssl_vhost) {
+ $vhost_content .= ' RewriteCond %{HTTPS} off' . "\n";
+ }
+ if ($domain['letsencrypt'] == '1') {
+ $vhost_content .= ' RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge' . "\n";
+ }
+ $vhost_content .= ' RewriteRule ^/(.*) ' . $corrected_docroot . '$1' . $modrew_red . "\n";
+ $vhost_content .= ' ' . "\n";
+ $vhost_content .= ' ' . "\n";
+ $vhost_content .= ' Redirect ' . $code . ' / ' . $domain['documentroot_norewrite'] . "\n";
+ $vhost_content .= ' ' . "\n";
+ } else {
+
+ \Froxlor\FileDir::mkDirWithCorrectOwnership($domain['customerroot'], $domain['documentroot'], $domain['guid'], $domain['guid'], true, true);
+ $vhost_content .= $this->getWebroot($domain);
+ if ($this->deactivated == false) {
+ $vhost_content .= $this->composePhpOptions($domain, $ssl_vhost);
+ $vhost_content .= $this->getStats($domain);
+ }
+ $vhost_content .= $this->getLogfiles($domain);
+
+ if ($domain['specialsettings'] != '' && ($ssl_vhost == false || ($ssl_vhost == true && $domain['include_specialsettings'] == 1))) {
+ $vhost_content .= $this->processSpecialConfigTemplate($domain['specialsettings'], $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n";
+ }
+
+ if ($domain['ssl_specialsettings'] != '' && $ssl_vhost == true) {
+ $vhost_content .= $this->processSpecialConfigTemplate($domain['ssl_specialsettings'], $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n";
+ }
+
+ if ($_vhost_content != '') {
+ $vhost_content .= $_vhost_content;
+ }
+
+ if (Settings::Get('system.default_vhostconf') != '' && ($ssl_vhost == false || ($ssl_vhost == true && Settings::Get('system.include_default_vhostconf') == 1))) {
+ $vhost_content .= $this->processSpecialConfigTemplate(Settings::Get('system.default_vhostconf'), $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n";
+ }
+
+ if (Settings::Get('system.default_sslvhostconf') != '' && $ssl_vhost == true) {
+ $vhost_content .= $this->processSpecialConfigTemplate(Settings::Get('system.default_sslvhostconf'), $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n";
+ }
+ }
+
+ $vhost_content .= ' ' . "\n";
+
+ return $vhost_content;
+ }
+
+ /**
+ * We compose the virtualhost entries for the domains
+ */
+ public function createVirtualHosts()
+ {
+ $domains = WebserverBase::getVhostsToCreate();
+ foreach ($domains as $domain) {
+
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'apache::createVirtualHosts: creating vhost container for domain ' . $domain['id'] . ', customer ' . $domain['loginname']);
+ $vhosts_filename = $this->getVhostFilename($domain);
+
+ // Apply header
+ $this->virtualhosts_data[$vhosts_filename] = '# Domain ID: ' . $domain['id'] . ' - CustomerID: ' . $domain['customerid'] . ' - CustomerLogin: ' . $domain['loginname'] . "\n";
+
+ if ($domain['deactivated'] != '1' || Settings::Get('system.deactivateddocroot') != '') {
+ // Create vhost without ssl
+ $this->virtualhosts_data[$vhosts_filename] .= $this->getVhostContent($domain, false);
+
+ if ($domain['ssl_enabled'] == '1' && ($domain['ssl'] == '1' || $domain['ssl_redirect'] == '1')) {
+ // Adding ssl stuff if enabled
+ $vhosts_filename_ssl = $this->getVhostFilename($domain, true);
+ $this->virtualhosts_data[$vhosts_filename_ssl] = '# Domain ID: ' . $domain['id'] . ' (SSL) - CustomerID: ' . $domain['customerid'] . ' - CustomerLogin: ' . $domain['loginname'] . "\n";
+ $this->virtualhosts_data[$vhosts_filename_ssl] .= $this->getVhostContent($domain, true);
+ }
+ } else {
+ $this->virtualhosts_data[$vhosts_filename] .= '# Customer deactivated and a docroot for deactivated users hasn\'t been set.' . "\n";
+ }
+ }
+ }
+
+ /**
+ * We compose the diroption entries for the paths
+ */
+ public function createFileDirOptions()
+ {
+ $result_stmt = Database::query("
+ SELECT `htac`.*, `c`.`guid`, `c`.`documentroot` AS `customerroot`
+ FROM `" . TABLE_PANEL_HTACCESS . "` `htac`
+ LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` `c` USING (`customerid`)
+ ORDER BY `htac`.`path`
+ ");
+ $diroptions = array();
+
+ while ($row_diroptions = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if ($row_diroptions['customerid'] != 0 && isset($row_diroptions['customerroot']) && $row_diroptions['customerroot'] != '') {
+ $diroptions[$row_diroptions['path']] = $row_diroptions;
+ $diroptions[$row_diroptions['path']]['htpasswds'] = array();
+ }
+ }
+
+ $result_stmt = Database::query("
+ SELECT `htpw`.*, `c`.`guid`, `c`.`documentroot` AS `customerroot`
+ FROM `" . TABLE_PANEL_HTPASSWDS . "` `htpw`
+ LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` `c` USING (`customerid`)
+ ORDER BY `htpw`.`path`, `htpw`.`username`
+ ");
+
+ while ($row_htpasswds = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if ($row_htpasswds['customerid'] != 0 && isset($row_htpasswds['customerroot']) && $row_htpasswds['customerroot'] != '') {
+ if (! isset($diroptions[$row_htpasswds['path']]) || ! is_array($diroptions[$row_htpasswds['path']])) {
+ $diroptions[$row_htpasswds['path']] = array();
+ }
+
+ $diroptions[$row_htpasswds['path']]['path'] = $row_htpasswds['path'];
+ $diroptions[$row_htpasswds['path']]['guid'] = $row_htpasswds['guid'];
+ $diroptions[$row_htpasswds['path']]['customerroot'] = $row_htpasswds['customerroot'];
+ $diroptions[$row_htpasswds['path']]['customerid'] = $row_htpasswds['customerid'];
+ $diroptions[$row_htpasswds['path']]['htpasswds'][] = $row_htpasswds;
+ }
+ }
+
+ foreach ($diroptions as $row_diroptions) {
+ $row_diroptions['path'] = \Froxlor\FileDir::makeCorrectDir($row_diroptions['path']);
+ \Froxlor\FileDir::mkDirWithCorrectOwnership($row_diroptions['customerroot'], $row_diroptions['path'], $row_diroptions['guid'], $row_diroptions['guid']);
+ $diroptions_filename = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.apacheconf_diroptions') . '/40_froxlor_diroption_' . md5($row_diroptions['path']) . '.conf');
+
+ if (! isset($this->diroptions_data[$diroptions_filename])) {
+ $this->diroptions_data[$diroptions_filename] = '';
+ }
+
+ if (is_dir($row_diroptions['path'])) {
+ $cperlenabled = \Froxlor\Customer\Customer::customerHasPerlEnabled($row_diroptions['customerid']);
+
+ $this->diroptions_data[$diroptions_filename] .= '' . "\n";
+
+ if (isset($row_diroptions['options_indexes']) && $row_diroptions['options_indexes'] == '1') {
+ $this->diroptions_data[$diroptions_filename] .= ' Options +Indexes';
+
+ // add perl options if enabled
+ if ($cperlenabled && isset($row_diroptions['options_cgi']) && $row_diroptions['options_cgi'] == '1') {
+ $this->diroptions_data[$diroptions_filename] .= ' +ExecCGI -MultiViews +SymLinksIfOwnerMatch +FollowSymLinks' . "\n";
+ } else {
+ $this->diroptions_data[$diroptions_filename] .= "\n";
+ }
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'Setting Options +Indexes for ' . $row_diroptions['path']);
+ }
+
+ if (isset($row_diroptions['options_indexes']) && $row_diroptions['options_indexes'] == '0') {
+ $this->diroptions_data[$diroptions_filename] .= ' Options -Indexes';
+
+ // add perl options if enabled
+ if ($cperlenabled && isset($row_diroptions['options_cgi']) && $row_diroptions['options_cgi'] == '1') {
+ $this->diroptions_data[$diroptions_filename] .= ' +ExecCGI -MultiViews +SymLinksIfOwnerMatch +FollowSymLinks' . "\n";
+ } else {
+ $this->diroptions_data[$diroptions_filename] .= "\n";
+ }
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'Setting Options -Indexes for ' . $row_diroptions['path']);
+ }
+
+ $statusCodes = array(
+ '404',
+ '403',
+ '500'
+ );
+ foreach ($statusCodes as $statusCode) {
+ if (isset($row_diroptions['error' . $statusCode . 'path']) && $row_diroptions['error' . $statusCode . 'path'] != '') {
+ $defhandler = $row_diroptions['error' . $statusCode . 'path'];
+ if (! \Froxlor\Validate\Validate::validateUrl($defhandler)) {
+ if (substr($defhandler, 0, 1) != '"' && substr($defhandler, - 1, 1) != '"') {
+ $defhandler = '"' . \Froxlor\FileDir::makeCorrectFile($defhandler) . '"';
+ }
+ }
+ $this->diroptions_data[$diroptions_filename] .= ' ErrorDocument ' . $statusCode . ' ' . $defhandler . "\n";
+ }
+ }
+
+ if ($cperlenabled && isset($row_diroptions['options_cgi']) && $row_diroptions['options_cgi'] == '1') {
+ $this->diroptions_data[$diroptions_filename] .= ' AllowOverride None' . "\n";
+ $this->diroptions_data[$diroptions_filename] .= ' AddHandler cgi-script .cgi .pl' . "\n";
+ // >=apache-2.4 enabled?
+ if (Settings::Get('system.apache24') == '1') {
+ $mypath_dir = new \Froxlor\Http\Directory($row_diroptions['path']);
+ // only create the require all granted if there is not active directory-protection
+ // for this path, as this would be the first require and therefore grant all access
+ if ($mypath_dir->isUserProtected() == false) {
+ $this->diroptions_data[$diroptions_filename] .= ' Require all granted' . "\n";
+ // $this->diroptions_data[$diroptions_filename] .= ' AllowOverride All' . "\n";
+ }
+ } else {
+ $this->diroptions_data[$diroptions_filename] .= ' Order allow,deny' . "\n";
+ $this->diroptions_data[$diroptions_filename] .= ' Allow from all' . "\n";
+ }
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'Enabling perl execution for ' . $row_diroptions['path']);
+
+ // check for suexec-workaround, #319
+ if ((int) Settings::Get('perl.suexecworkaround') == 1) {
+ // symlink this directory to suexec-safe-path
+ $loginname = \Froxlor\Customer\Customer::getCustomerDetail($row_diroptions['customerid'], 'loginname');
+ $suexecpath = \Froxlor\FileDir::makeCorrectDir(Settings::Get('perl.suexecpath') . '/' . $loginname . '/' . md5($row_diroptions['path']) . '/');
+
+ if (! file_exists($suexecpath)) {
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg($suexecpath));
+ \Froxlor\FileDir::safe_exec('chown -R ' . escapeshellarg($row_diroptions['guid']) . ':' . escapeshellarg($row_diroptions['guid']) . ' ' . escapeshellarg($suexecpath));
+ }
+
+ // symlink to {$givenpath}/cgi-bin
+ // NOTE: symlinks are FILES, so do not append a / here
+ $perlsymlink = \Froxlor\FileDir::makeCorrectFile($row_diroptions['path'] . '/cgi-bin');
+ if (! file_exists($perlsymlink)) {
+ \Froxlor\FileDir::safe_exec('ln -s ' . escapeshellarg($suexecpath) . ' ' . escapeshellarg($perlsymlink));
+ }
+ \Froxlor\FileDir::safe_exec('chown -h ' . escapeshellarg($row_diroptions['guid']) . ':' . escapeshellarg($row_diroptions['guid']) . ' ' . escapeshellarg($perlsymlink));
+ }
+ } else {
+ // if no perl-execution is enabled but the workaround is,
+ // we have to remove the symlink and folder in suexecpath
+ if ((int) Settings::Get('perl.suexecworkaround') == 1) {
+ $loginname = \Froxlor\Customer\Customer::getCustomerDetail($row_diroptions['customerid'], 'loginname');
+ $suexecpath = \Froxlor\FileDir::makeCorrectDir(Settings::Get('perl.suexecpath') . '/' . $loginname . '/' . md5($row_diroptions['path']) . '/');
+ $perlsymlink = \Froxlor\FileDir::makeCorrectFile($row_diroptions['path'] . '/cgi-bin');
+
+ // remove symlink
+ if (file_exists($perlsymlink)) {
+ \Froxlor\FileDir::safe_exec('rm -f ' . escapeshellarg($perlsymlink));
+ }
+ // remove folder in suexec-path
+ if (file_exists($suexecpath)) {
+ \Froxlor\FileDir::safe_exec('rm -rf ' . escapeshellarg($suexecpath));
+ }
+ }
+ }
+
+ if (count($row_diroptions['htpasswds']) > 0) {
+ $htpasswd_filename = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.apacheconf_htpasswddir') . '/' . $row_diroptions['customerid'] . '-' . md5($row_diroptions['path']) . '.htpasswd');
+
+ if (! isset($this->htpasswds_data[$htpasswd_filename])) {
+ $this->htpasswds_data[$htpasswd_filename] = '';
+ }
+
+ foreach ($row_diroptions['htpasswds'] as $row_htpasswd) {
+ $this->htpasswds_data[$htpasswd_filename] .= $row_htpasswd['username'] . ':' . $row_htpasswd['password'] . "\n";
+ }
+
+ $this->diroptions_data[$diroptions_filename] .= ' AuthType Basic' . "\n";
+ $this->diroptions_data[$diroptions_filename] .= ' AuthName "' . $row_htpasswd['authname'] . '"' . "\n";
+ $this->diroptions_data[$diroptions_filename] .= ' AuthUserFile ' . $htpasswd_filename . "\n";
+ $this->diroptions_data[$diroptions_filename] .= ' require valid-user' . "\n";
+ }
+
+ $this->diroptions_data[$diroptions_filename] .= ' ' . "\n";
+ }
+ }
+ }
+
+ /**
+ * We write the configs
+ */
+ public function writeConfigs()
+ {
+ // Write diroptions
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "apache::writeConfigs: rebuilding " . Settings::Get('system.apacheconf_diroptions'));
+
+ if (count($this->diroptions_data) > 0) {
+ $optsDir = new \Froxlor\Http\Directory(Settings::Get('system.apacheconf_diroptions'));
+ if (! $optsDir->isConfigDir()) {
+ // Save one big file
+ $diroptions_file = '';
+
+ foreach ($this->diroptions_data as $diroptions_filename => $diroptions_content) {
+ $diroptions_file .= $diroptions_content . "\n\n";
+ }
+
+ $diroptions_filename = Settings::Get('system.apacheconf_diroptions');
+
+ // Apply header
+ $diroptions_file = '# ' . basename($diroptions_filename) . "\n" . '# Created ' . date('d.m.Y H:i') . "\n" . '# Do NOT manually edit this file, all changes will be deleted after the next domain change at the panel.' . "\n" . "\n" . $diroptions_file;
+ $diroptions_file_handler = fopen($diroptions_filename, 'w');
+ fwrite($diroptions_file_handler, $diroptions_file);
+ fclose($diroptions_file_handler);
+ } else {
+ if (! file_exists(Settings::Get('system.apacheconf_diroptions'))) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'apache::writeConfigs: mkdir ' . escapeshellarg(\Froxlor\FileDir::makeCorrectDir(Settings::Get('system.apacheconf_diroptions'))));
+ \Froxlor\FileDir::safe_exec('mkdir ' . escapeshellarg(\Froxlor\FileDir::makeCorrectDir(Settings::Get('system.apacheconf_diroptions'))));
+ }
+
+ // Write a single file for every diroption
+ foreach ($this->diroptions_data as $diroptions_filename => $diroptions_file) {
+ $this->known_diroptionsfilenames[] = basename($diroptions_filename);
+
+ // Apply header
+ $diroptions_file = '# ' . basename($diroptions_filename) . "\n" . '# Created ' . date('d.m.Y H:i') . "\n" . '# Do NOT manually edit this file, all changes will be deleted after the next domain change at the panel.' . "\n" . "\n" . $diroptions_file;
+ $diroptions_file_handler = fopen($diroptions_filename, 'w');
+ fwrite($diroptions_file_handler, $diroptions_file);
+ fclose($diroptions_file_handler);
+ }
+ }
+ }
+
+ // Write htpasswds
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "apache::writeConfigs: rebuilding " . Settings::Get('system.apacheconf_htpasswddir'));
+
+ if (count($this->htpasswds_data) > 0) {
+ if (! file_exists(Settings::Get('system.apacheconf_htpasswddir'))) {
+ $umask = umask();
+ umask(0000);
+ mkdir(Settings::Get('system.apacheconf_htpasswddir'), 0751);
+ umask($umask);
+ }
+
+ $htpasswdDir = new \Froxlor\Http\Directory(Settings::Get('system.apacheconf_htpasswddir'));
+ if ($htpasswdDir->isConfigDir(true)) {
+ foreach ($this->htpasswds_data as $htpasswd_filename => $htpasswd_file) {
+ $this->known_htpasswdsfilenames[] = basename($htpasswd_filename);
+ $htpasswd_file_handler = fopen($htpasswd_filename, 'w');
+ fwrite($htpasswd_file_handler, $htpasswd_file);
+ fclose($htpasswd_file_handler);
+ }
+ } else {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_WARNING, 'WARNING!!! ' . Settings::Get('system.apacheconf_htpasswddir') . ' is not a directory. htpasswd directory protection is disabled!!!');
+ }
+ }
+
+ // Write virtualhosts
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "apache::writeConfigs: rebuilding " . Settings::Get('system.apacheconf_vhost'));
+
+ if (count($this->virtualhosts_data) > 0) {
+ $vhostDir = new \Froxlor\Http\Directory(Settings::Get('system.apacheconf_vhost'));
+ if (! $vhostDir->isConfigDir()) {
+ // Save one big file
+ $vhosts_file = '';
+
+ // sort by filename so the order is:
+ // 1. subdomains x-29
+ // 2. subdomains as main-domains 30
+ // 3. main-domains 35
+ // #437
+ ksort($this->virtualhosts_data);
+
+ foreach ($this->virtualhosts_data as $vhosts_filename => $vhost_content) {
+ $vhosts_file .= $vhost_content . "\n\n";
+ }
+
+ // Include diroptions file in case it exists
+ if (file_exists(Settings::Get('system.apacheconf_diroptions'))) {
+ $vhosts_file .= "\n" . 'Include ' . Settings::Get('system.apacheconf_diroptions') . "\n\n";
+ }
+
+ $vhosts_filename = Settings::Get('system.apacheconf_vhost');
+
+ // Apply header
+ $vhosts_file = '# ' . basename($vhosts_filename) . "\n" . '# Created ' . date('d.m.Y H:i') . "\n" . '# Do NOT manually edit this file, all changes will be deleted after the next domain change at the panel.' . "\n" . "\n" . $vhosts_file;
+ $vhosts_file_handler = fopen($vhosts_filename, 'w');
+ fwrite($vhosts_file_handler, $vhosts_file);
+ fclose($vhosts_file_handler);
+ } else {
+ if (! file_exists(Settings::Get('system.apacheconf_vhost'))) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'apache::writeConfigs: mkdir ' . escapeshellarg(\Froxlor\FileDir::makeCorrectDir(Settings::Get('system.apacheconf_vhost'))));
+ \Froxlor\FileDir::safe_exec('mkdir ' . escapeshellarg(\Froxlor\FileDir::makeCorrectDir(Settings::Get('system.apacheconf_vhost'))));
+ }
+
+ // Write a single file for every vhost
+ foreach ($this->virtualhosts_data as $vhosts_filename => $vhosts_file) {
+ $this->known_vhostfilenames[] = basename($vhosts_filename);
+
+ // Apply header
+ $vhosts_file = '# ' . basename($vhosts_filename) . "\n" . '# Created ' . date('d.m.Y H:i') . "\n" . '# Do NOT manually edit this file, all changes will be deleted after the next domain change at the panel.' . "\n" . "\n" . $vhosts_file;
+ $vhosts_file_handler = fopen($vhosts_filename, 'w');
+ fwrite($vhosts_file_handler, $vhosts_file);
+ fclose($vhosts_file_handler);
+ }
+ }
+ }
+ }
+}
diff --git a/lib/Froxlor/Cron/Http/ApacheFcgi.php b/lib/Froxlor/Cron/Http/ApacheFcgi.php
new file mode 100644
index 00000000..9e5fbe9c
--- /dev/null
+++ b/lib/Froxlor/Cron/Http/ApacheFcgi.php
@@ -0,0 +1,222 @@
+ (2003-2009)
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ */
+class ApacheFcgi extends Apache
+{
+
+ protected function composePhpOptions($domain, $ssl_vhost = false)
+ {
+ $php_options_text = '';
+
+ if ($domain['phpenabled_customer'] == 1 && $domain['phpenabled_vhost'] == '1') {
+ $php = new PhpInterface($domain);
+ $phpconfig = $php->getPhpConfig((int) $domain['phpsettingid']);
+
+ if ((int) Settings::Get('phpfpm.enabled') == 1) {
+ $srvName = 'fpm.external';
+ if ($domain['ssl'] == 1 && $ssl_vhost) {
+ $srvName = 'ssl-fpm.external';
+ }
+ // #1317 - perl is executed via apache and therefore, when using fpm, does not know the user
+ // which perl is supposed to run as, hence the need for Suexec need
+ if (\Froxlor\Customer\Customer::customerHasPerlEnabled($domain['customerid'])) {
+ $php_options_text .= ' SuexecUserGroup "' . $domain['loginname'] . '" "' . $domain['loginname'] . '"' . "\n";
+ }
+
+ // mod_proxy stuff for apache-2.4
+ if (Settings::Get('system.apache24') == '1' && Settings::Get('phpfpm.use_mod_proxy') == '1') {
+ $filesmatch = $phpconfig['fpm_settings']['limit_extensions'];
+ $extensions = explode(" ", $filesmatch);
+ $filesmatch = "";
+ foreach ($extensions as $ext) {
+ $filesmatch .= substr($ext, 1) . '|';
+ }
+ // start block, cut off last pipe and close block
+ $filesmatch = '(' . str_replace(".", "\.", substr($filesmatch, 0, - 1)) . ')';
+ $php_options_text .= ' ' . "\n";
+ $php_options_text .= ' SetHandler proxy:unix:' . $php->getInterface()->getSocketFile() . '|fcgi://localhost' . "\n";
+ $php_options_text .= ' ' . "\n";
+
+ $mypath_dir = new \Froxlor\Http\Directory($domain['documentroot']);
+
+ // only create the require all granted if there is not active directory-protection
+ // for this path, as this would be the first require and therefore grant all access
+ if ($mypath_dir->isUserProtected() == false) {
+ $php_options_text .= ' ' . "\n";
+ if ($phpconfig['pass_authorizationheader'] == '1') {
+ $php_options_text .= ' CGIPassAuth On' . "\n";
+ }
+ $php_options_text .= ' Require all granted' . "\n";
+ $php_options_text .= ' AllowOverride All' . "\n";
+ $php_options_text .= ' ' . "\n";
+ } elseif ($phpconfig['pass_authorizationheader'] == '1') {
+ // allow Pass of Authorization header
+ $php_options_text .= ' ' . "\n";
+ $php_options_text .= ' CGIPassAuth On' . "\n";
+ $php_options_text .= ' ' . "\n";
+ }
+ } else {
+ $addheader = "";
+ if ($phpconfig['pass_authorizationheader'] == '1') {
+ $addheader = " -pass-header Authorization";
+ }
+ $php_options_text .= ' FastCgiExternalServer ' . $php->getInterface()->getAliasConfigDir() . $srvName . ' -socket ' . $php->getInterface()->getSocketFile() . ' -idle-timeout ' . $phpconfig['fpm_settings']['idle_timeout'] . $addheader . "\n";
+ $php_options_text .= ' ' . "\n";
+ $filesmatch = $phpconfig['fpm_settings']['limit_extensions'];
+ $extensions = explode(" ", $filesmatch);
+ $filesmatch = "";
+ foreach ($extensions as $ext) {
+ $filesmatch .= substr($ext, 1) . '|';
+ }
+ // start block, cut off last pipe and close block
+ $filesmatch = '(' . str_replace(".", "\.", substr($filesmatch, 0, - 1)) . ')';
+ $php_options_text .= ' ' . "\n";
+ $php_options_text .= ' SetHandler php-fastcgi' . "\n";
+ $php_options_text .= ' Action php-fastcgi /fastcgiphp' . "\n";
+ $php_options_text .= ' Options +ExecCGI' . "\n";
+ $php_options_text .= ' ' . "\n";
+ // >=apache-2.4 enabled?
+ if (Settings::Get('system.apache24') == '1') {
+ $mypath_dir = new \Froxlor\Http\Directory($domain['documentroot']);
+ // only create the require all granted if there is not active directory-protection
+ // for this path, as this would be the first require and therefore grant all access
+ if ($mypath_dir->isUserProtected() == false) {
+ $php_options_text .= ' Require all granted' . "\n";
+ $php_options_text .= ' AllowOverride All' . "\n";
+ }
+ } else {
+ $php_options_text .= ' Order allow,deny' . "\n";
+ $php_options_text .= ' allow from all' . "\n";
+ }
+ $php_options_text .= ' ' . "\n";
+ $php_options_text .= ' Alias /fastcgiphp ' . $php->getInterface()->getAliasConfigDir() . $srvName . "\n";
+ }
+ } else {
+ $php_options_text .= ' FcgidIdleTimeout ' . Settings::Get('system.mod_fcgid_idle_timeout') . "\n";
+ if ((int) Settings::Get('system.mod_fcgid_wrapper') == 0) {
+ $php_options_text .= ' SuexecUserGroup "' . $domain['loginname'] . '" "' . $domain['loginname'] . '"' . "\n";
+ $php_options_text .= ' ScriptAlias /php/ ' . $php->getInterface()->getConfigDir() . "\n";
+ } else {
+ $php_options_text .= ' SuexecUserGroup "' . $domain['loginname'] . '" "' . $domain['loginname'] . '"' . "\n";
+ $php_options_text .= ' ' . "\n";
+ $file_extensions = explode(' ', $phpconfig['file_extensions']);
+ $php_options_text .= ' ' . "\n";
+ $php_options_text .= ' SetHandler fcgid-script' . "\n";
+ foreach ($file_extensions as $file_extension) {
+ $php_options_text .= ' FcgidWrapper ' . $php->getInterface()->getStarterFile() . ' .' . $file_extension . "\n";
+ }
+ $php_options_text .= ' Options +ExecCGI' . "\n";
+ $php_options_text .= ' ' . "\n";
+ // >=apache-2.4 enabled?
+ if (Settings::Get('system.apache24') == '1') {
+ $mypath_dir = new \Froxlor\Http\Directory($domain['documentroot']);
+ // only create the require all granted if there is not active directory-protection
+ // for this path, as this would be the first require and therefore grant all access
+ if ($mypath_dir->isUserProtected() == false) {
+ $php_options_text .= ' Require all granted' . "\n";
+ $php_options_text .= ' AllowOverride All' . "\n";
+ }
+ } else {
+ $php_options_text .= ' Order allow,deny' . "\n";
+ $php_options_text .= ' allow from all' . "\n";
+ }
+ $php_options_text .= ' ' . "\n";
+ }
+ }
+
+ // create starter-file | config-file
+ $php->getInterface()->createConfig($phpconfig);
+
+ // create php.ini (fpm does nothing here, as it
+ // defines ini-settings in its pool config)
+ $php->getInterface()->createIniFile($phpconfig);
+ } else {
+ $php_options_text .= ' # PHP is disabled for this vHost' . "\n";
+ }
+
+ return $php_options_text;
+ }
+
+ public function createOwnVhostStarter()
+ {
+ if (Settings::Get('system.mod_fcgid_ownvhost') == '1' || (Settings::Get('phpfpm.enabled') == '1' && Settings::Get('phpfpm.enabled_ownvhost') == '1')) {
+ $mypath = \Froxlor\Froxlor::getInstallDir();
+
+ if (Settings::Get('system.mod_fcgid_ownvhost') == '1') {
+ $user = Settings::Get('system.mod_fcgid_httpuser');
+ $group = Settings::Get('system.mod_fcgid_httpgroup');
+ } elseif (Settings::Get('phpfpm.enabled') == '1' && Settings::Get('phpfpm.enabled_ownvhost') == '1') {
+ $user = Settings::Get('phpfpm.vhost_httpuser');
+ $group = Settings::Get('phpfpm.vhost_httpgroup');
+
+ // get fpm config
+ $fpm_sel_stmt = Database::prepare("
+ SELECT f.id FROM `" . TABLE_PANEL_FPMDAEMONS . "` f
+ LEFT JOIN `" . TABLE_PANEL_PHPCONFIGS . "` p ON p.fpmsettingid = f.id
+ WHERE p.id = :phpconfigid
+ ");
+ $fpm_config = Database::pexecute_first($fpm_sel_stmt, array(
+ 'phpconfigid' => Settings::Get('phpfpm.vhost_defaultini')
+ ));
+ }
+
+ $domain = array(
+ 'id' => 'none',
+ 'domain' => Settings::Get('system.hostname'),
+ 'adminid' => 1, /* first admin-user (superadmin) */
+ 'mod_fcgid_starter' => - 1,
+ 'mod_fcgid_maxrequests' => - 1,
+ 'guid' => $user,
+ 'openbasedir' => 0,
+ 'email' => Settings::Get('panel.adminmail'),
+ 'loginname' => 'froxlor.panel',
+ 'documentroot' => $mypath,
+ 'customerroot' => $mypath,
+ 'fpm_config_id' => isset($fpm_config['id']) ? $fpm_config['id'] : 1
+ );
+
+ // all the files and folders have to belong to the local user
+ // now because we also use fcgid for our own vhost
+ \Froxlor\FileDir::safe_exec('chown -R ' . $user . ':' . $group . ' ' . escapeshellarg($mypath));
+
+ // get php.ini for our own vhost
+ $php = new PhpInterface($domain);
+
+ // get php-config
+ if (Settings::Get('phpfpm.enabled') == '1') {
+ // fpm
+ $phpconfig = $php->getPhpConfig(Settings::Get('phpfpm.vhost_defaultini'));
+ } else {
+ // fcgid
+ $phpconfig = $php->getPhpConfig(Settings::Get('system.mod_fcgid_defaultini_ownvhost'));
+ }
+
+ // create starter-file | config-file
+ $php->getInterface()->createConfig($phpconfig);
+
+ // create php.ini (fpm does nothing here, as it
+ // defines ini-settings in its pool config)
+ $php->getInterface()->createIniFile($phpconfig);
+ }
+ }
+}
diff --git a/lib/Froxlor/Cron/Http/ConfigIO.php b/lib/Froxlor/Cron/Http/ConfigIO.php
new file mode 100644
index 00000000..55002285
--- /dev/null
+++ b/lib/Froxlor/Cron/Http/ConfigIO.php
@@ -0,0 +1,313 @@
+
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ * @since 0.9.29
+ *
+ */
+class ConfigIO
+{
+
+ /**
+ * clean up former created configs, including (if enabled)
+ * awstats, fcgid, php-fpm and of course automatically created
+ * webserver vhost and diroption files
+ *
+ * @return null
+ */
+ public function cleanUp()
+ {
+
+ // old error logs
+ $this->cleanErrLogs();
+
+ // awstats files
+ $this->cleanAwstatsFiles();
+
+ // fcgid files
+ $this->cleanFcgidFiles();
+
+ // php-fpm files
+ $this->cleanFpmFiles();
+
+ // clean webserver-configs
+ $this->cleanWebserverConfigs();
+
+ // old htpasswd files
+ $this->cleanHtpasswdFiles();
+
+ // customer-specified ssl-certificates
+ $this->cleanCustomerSslCerts();
+ }
+
+ private function cleanErrLogs()
+ {
+ $err_dir = \Froxlor\FileDir::makeCorrectDir(\Froxlor\Froxlor::getInstallDir() . "/logs/");
+ if (@is_dir($err_dir)) {
+ // now get rid of old stuff
+ // (but append /*.log so we don't delete the directory)
+ $err_dir .= '/*.log';
+ \Froxlor\FileDir::safe_exec('rm -f ' . \Froxlor\FileDir::makeCorrectFile($err_dir));
+ }
+ }
+
+ /**
+ * remove customer-specified auto-generated ssl-certificates
+ * (they are being regenerated)
+ *
+ * @return null
+ */
+ private function cleanCustomerSslCerts()
+ {
+
+ /*
+ * only clean up if we're actually using SSL
+ */
+ if (Settings::Get('system.use_ssl') == '1') {
+ // get correct directory
+ $configdir = $this->getFile('system', 'customer_ssl_path');
+ if ($configdir !== false) {
+
+ $configdir = \Froxlor\FileDir::makeCorrectDir($configdir);
+
+ if (@is_dir($configdir)) {
+ // now get rid of old stuff
+ // (but append /* so we don't delete the directory)
+ $configdir .= '/*';
+ \Froxlor\FileDir::safe_exec('rm -f ' . \Froxlor\FileDir::makeCorrectFile($configdir));
+ }
+ }
+ }
+ }
+
+ /**
+ * remove webserver related configuration files before regeneration
+ *
+ * @return null
+ */
+ private function cleanWebserverConfigs()
+ {
+
+ // get directories
+ $configdirs = array();
+ $dir = $this->getFile('system', 'apacheconf_vhost');
+ if ($dir !== false)
+ $configdirs[] = \Froxlor\FileDir::makeCorrectDir($dir);
+
+ $dir = $this->getFile('system', 'apacheconf_diroptions');
+ if ($dir !== false)
+ $configdirs[] = \Froxlor\FileDir::makeCorrectDir($dir);
+
+ // file pattern
+ $pattern = "/^([0-9]){2}_(froxlor|syscp)_(.+)\.conf$/";
+
+ // check ALL the folders
+ foreach ($configdirs as $config_dir) {
+
+ // check directory
+ if (@is_dir($config_dir)) {
+
+ // create directory iterator
+ $its = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($config_dir));
+
+ // iterate through all subdirs,
+ // look for vhost/diroption files
+ // and delete them
+ foreach ($its as $it) {
+ if ($it->isFile() && preg_match($pattern, $it->getFilename())) {
+ // remove file
+ \Froxlor\FileDir::safe_exec('rm -f ' . escapeshellarg(\Froxlor\FileDir::makeCorrectFile($its->getPathname())));
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * remove htpasswd files before regeneration
+ *
+ * @return null
+ */
+ private function cleanHtpasswdFiles()
+ {
+
+ // get correct directory
+ $configdir = $this->getFile('system', 'apacheconf_htpasswddir');
+
+ if ($configdir !== false) {
+ $configdir = \Froxlor\FileDir::makeCorrectDir($configdir);
+
+ if (@is_dir($configdir)) {
+ // now get rid of old stuff
+ // (but append /* so we don't delete the directory)
+ $configdir .= '/*';
+ \Froxlor\FileDir::safe_exec('rm -f ' . \Froxlor\FileDir::makeCorrectFile($configdir));
+ }
+ }
+ }
+
+ /**
+ * remove awstats related configuration files before regeneration
+ *
+ * @return null
+ */
+ private function cleanAwstatsFiles()
+ {
+ if (Settings::Get('system.awstats_enabled') == '0') {
+ return;
+ }
+
+ // dhr: cleanout froxlor-generated awstats configs prior to re-creation
+ $awstatsclean = array();
+ $awstatsclean['header'] = "## GENERATED BY FROXLOR\n";
+ $awstatsclean['headerold'] = "## GENERATED BY SYSCP\n";
+ $awstatsclean['path'] = $this->getFile('system', 'awstats_conf');
+
+ /**
+ * don't do anything if the directory does not exist
+ * (e.g.
+ * awstats not installed yet or whatever)
+ * fixes #45
+ */
+ if ($awstatsclean['path'] !== false && is_dir($awstatsclean['path'])) {
+ $awstatsclean['dir'] = dir($awstatsclean['path']);
+ while ($awstatsclean['entry'] = $awstatsclean['dir']->read()) {
+ $awstatsclean['fullentry'] = \Froxlor\FileDir::makeCorrectFile($awstatsclean['path'] . '/' . $awstatsclean['entry']);
+ /**
+ * don't do anything if the file does not exist
+ */
+ if (@file_exists($awstatsclean['fullentry']) && $awstatsclean['entry'] != '.' && $awstatsclean['entry'] != '..') {
+ $awstatsclean['fh'] = fopen($awstatsclean['fullentry'], 'r');
+ $awstatsclean['headerRead'] = fgets($awstatsclean['fh'], strlen($awstatsclean['header']) + 1);
+ fclose($awstatsclean['fh']);
+
+ if ($awstatsclean['headerRead'] == $awstatsclean['header'] || $awstatsclean['headerRead'] == $awstatsclean['headerold']) {
+ $awstats_conf_file = \Froxlor\FileDir::makeCorrectFile($awstatsclean['fullentry']);
+ @unlink($awstats_conf_file);
+ }
+ }
+ }
+ }
+ unset($awstatsclean);
+ // end dhr
+ }
+
+ /**
+ * remove fcgid related configuration files before regeneration
+ *
+ * @return null
+ */
+ private function cleanFcgidFiles()
+ {
+ if (Settings::Get('system.mod_fcgid') == '0') {
+ return;
+ }
+
+ // get correct directory
+ $configdir = $this->getFile('system', 'mod_fcgid_configdir');
+ if ($configdir !== false) {
+
+ $configdir = \Froxlor\FileDir::makeCorrectDir($configdir);
+
+ if (@is_dir($configdir)) {
+ // create directory iterator
+ $its = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($configdir));
+
+ // iterate through all subdirs,
+ // look for php-fcgi-starter files
+ // and take immutable-flag away from them
+ // so we can delete them :)
+ foreach ($its as $it) {
+ if ($it->isFile() && $it->getFilename() == 'php-fcgi-starter') {
+ // set chattr -i
+ \Froxlor\FileDir::removeImmutable($its->getPathname());
+ }
+ }
+
+ // now get rid of old stuff
+ // (but append /* so we don't delete the directory)
+ $configdir .= '/*';
+ \Froxlor\FileDir::safe_exec('rm -rf ' . \Froxlor\FileDir::makeCorrectFile($configdir));
+ }
+ }
+ }
+
+ /**
+ * remove php-fpm related configuration files before regeneration
+ *
+ * @return null
+ */
+ private function cleanFpmFiles()
+ {
+ if (Settings::Get('phpfpm.enabled') == '0') {
+ return;
+ }
+
+ // get all fpm config paths
+ $fpmconf_sel = \Froxlor\Database\Database::prepare("SELECT config_dir FROM `" . TABLE_PANEL_FPMDAEMONS . "`");
+ \Froxlor\Database\Database::pexecute($fpmconf_sel);
+ $fpmconf_paths = $fpmconf_sel->fetchAll(\PDO::FETCH_ASSOC);
+ // clean all php-fpm config-dirs
+ foreach ($fpmconf_paths as $configdir) {
+ $configdir = \Froxlor\FileDir::makeCorrectDir($configdir['config_dir']);
+ if (@is_dir($configdir)) {
+ // now get rid of old stuff
+ // (but append /*.conf so we don't delete the directory)
+ $configdir .= '/*.conf';
+ \Froxlor\FileDir::safe_exec('rm -f ' . \Froxlor\FileDir::makeCorrectFile($configdir));
+ } else {
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . $configdir);
+ }
+ }
+
+ // also remove aliasconfigdir #1273
+ $aliasconfigdir = $this->getFile('phpfpm', 'aliasconfigdir');
+ if ($aliasconfigdir !== false) {
+ $aliasconfigdir = \Froxlor\FileDir::makeCorrectDir($aliasconfigdir);
+ if (@is_dir($aliasconfigdir)) {
+ $aliasconfigdir .= '/*';
+ \Froxlor\FileDir::safe_exec('rm -rf ' . \Froxlor\FileDir::makeCorrectFile($aliasconfigdir));
+ }
+ }
+ }
+
+ /**
+ * returns a file/direcotry from the settings and checks whether it exists
+ *
+ * @param string $group
+ * settings-group
+ * @param string $varname
+ * var-name
+ * @param boolean $check_exists
+ * check if the file exists
+ *
+ * @return string|boolean complete path including filename if any or false on error
+ */
+ private function getFile($group, $varname, $check_exists = true)
+ {
+
+ // read from settings
+ $file = Settings::Get($group . '.' . $varname);
+
+ // check whether it exists
+ if ($check_exists && @file_exists($file) == false) {
+ return false;
+ }
+ return $file;
+ }
+}
diff --git a/lib/Froxlor/Cron/Http/DomainSSL.php b/lib/Froxlor/Cron/Http/DomainSSL.php
new file mode 100644
index 00000000..82d7c56d
--- /dev/null
+++ b/lib/Froxlor/Cron/Http/DomainSSL.php
@@ -0,0 +1,129 @@
+
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ * @since 0.9.29
+ *
+ */
+class DomainSSL
+{
+
+ /**
+ * read domain-related (or if empty, parentdomain-related) ssl-certificates from the database
+ * and (if not empty) set the corresponding array-indices (ssl_cert_file, ssl_key_file,
+ * ssl_ca_file and ssl_cert_chainfile).
+ * Hence the parameter as reference.
+ *
+ * @param array $domain
+ * domain-array as reference so we can set the corresponding array-indices
+ *
+ * @return null
+ */
+ public function setDomainSSLFilesArray(array &$domain = null)
+ {
+ // check if the domain itself has a certificate defined
+ $dom_certs_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = :domid
+ ");
+ $dom_certs = Database::pexecute_first($dom_certs_stmt, array(
+ 'domid' => $domain['id']
+ ));
+
+ if (! is_array($dom_certs) || ! isset($dom_certs['ssl_cert_file']) || $dom_certs['ssl_cert_file'] == '') {
+ // maybe its parent?
+ if (isset($domain['parentdomainid']) && $domain['parentdomainid'] != 0) {
+ $dom_certs = Database::pexecute_first($dom_certs_stmt, array(
+ 'domid' => $domain['parentdomainid']
+ ));
+ }
+ }
+
+ // check if it's an array and if the most important field is set
+ if (is_array($dom_certs) && isset($dom_certs['ssl_cert_file']) && $dom_certs['ssl_cert_file'] != '') {
+ // get destination path
+ $sslcertpath = \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.customer_ssl_path'));
+ // create path if it does not exist
+ if (! file_exists($sslcertpath)) {
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg($sslcertpath));
+ }
+ // make correct files for the certificates
+ $ssl_files = array(
+ 'ssl_cert_file' => \Froxlor\FileDir::makeCorrectFile($sslcertpath . '/' . $domain['domain'] . '.crt'),
+ 'ssl_key_file' => \Froxlor\FileDir::makeCorrectFile($sslcertpath . '/' . $domain['domain'] . '.key')
+ );
+
+ if (! $this->validateCertificate($dom_certs)) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, 'Given SSL private key for ' . $domain['domain'] . ' does not seem to match the certificate. Cannot create ssl-directives');
+ return;
+ }
+
+ if (Settings::Get('system.webserver') == 'lighttpd') {
+ // put my.crt and my.key together for lighty.
+ $dom_certs['ssl_cert_file'] = trim($dom_certs['ssl_cert_file']) . "\n" . trim($dom_certs['ssl_key_file']) . "\n";
+ $ssl_files['ssl_key_file'] = '';
+ }
+
+ // initialize optional files
+ $ssl_files['ssl_ca_file'] = '';
+ $ssl_files['ssl_cert_chainfile'] = '';
+ // set them if they are != empty
+ if ($dom_certs['ssl_ca_file'] != '') {
+ $ssl_files['ssl_ca_file'] = \Froxlor\FileDir::makeCorrectFile($sslcertpath . '/' . $domain['domain'] . '_CA.pem');
+ }
+ if ($dom_certs['ssl_cert_chainfile'] != '') {
+ if (Settings::Get('system.webserver') == 'nginx') {
+ // put ca.crt in my.crt, as nginx does not support a separate chain file.
+ $dom_certs['ssl_cert_file'] = trim($dom_certs['ssl_cert_file']) . "\n" . trim($dom_certs['ssl_cert_chainfile']) . "\n";
+ } else {
+ $ssl_files['ssl_cert_chainfile'] = \Froxlor\FileDir::makeCorrectFile($sslcertpath . '/' . $domain['domain'] . '_chain.pem');
+ }
+ }
+ // will only be generated to be used externally, froxlor does not need this
+ if ($dom_certs['ssl_fullchain_file'] != '') {
+ $ssl_files['ssl_fullchain_file'] = \Froxlor\FileDir::makeCorrectFile($sslcertpath . '/' . $domain['domain'] . '_fullchain.pem');
+ }
+ // create them on the filesystem
+ foreach ($ssl_files as $type => $filename) {
+ if ($filename != '') {
+ touch($filename);
+ $_fh = fopen($filename, 'w');
+ fwrite($_fh, $dom_certs[$type]);
+ fclose($_fh);
+ if ($type == 'ssl_key_file') {
+ chmod($filename, 0600);
+ } else {
+ chmod($filename, 0644);
+ }
+ }
+ }
+ // override corresponding array values
+ $domain['ssl_cert_file'] = $ssl_files['ssl_cert_file'];
+ $domain['ssl_key_file'] = $ssl_files['ssl_key_file'];
+ $domain['ssl_ca_file'] = $ssl_files['ssl_ca_file'];
+ $domain['ssl_cert_chainfile'] = $ssl_files['ssl_cert_chainfile'];
+ }
+
+ return;
+ }
+
+ private function validateCertificate($dom_certs = array())
+ {
+ return openssl_x509_check_private_key($dom_certs['ssl_cert_file'], $dom_certs['ssl_key_file']);
+ }
+}
diff --git a/lib/Froxlor/Cron/Http/HttpConfigBase.php b/lib/Froxlor/Cron/Http/HttpConfigBase.php
new file mode 100644
index 00000000..eecbfeff
--- /dev/null
+++ b/lib/Froxlor/Cron/Http/HttpConfigBase.php
@@ -0,0 +1,170 @@
+ (2016-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ */
+
+/**
+ * Class HttpConfigBase
+ *
+ * Base class for all HTTP server configs
+ */
+class HttpConfigBase
+{
+
+ public function init()
+ {
+ // if Let's Encrypt is activated, run it before regeneration of webserver configfiles
+ if (Settings::Get('system.leenabled') == 1) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'Running Let\'s Encrypt cronjob prior to regenerating webserver config files');
+ \Froxlor\Cron\Http\LetsEncrypt\AcmeSh::$no_inserttask = true;
+ \Froxlor\Cron\Http\LetsEncrypt\AcmeSh::run(true);
+ // set last run timestamp of cronjob
+ \Froxlor\System\Cronjob::updateLastRunOfCron('letsencrypt');
+ }
+ }
+
+ public function reload()
+ {
+ $called_class = get_called_class();
+ if ((int) Settings::Get('phpfpm.enabled') == 1) {
+ // get all start/stop commands
+ $startstop_sel = Database::prepare("SELECT reload_cmd, config_dir FROM `" . TABLE_PANEL_FPMDAEMONS . "`");
+ Database::pexecute($startstop_sel);
+ $restart_cmds = $startstop_sel->fetchAll(\PDO::FETCH_ASSOC);
+ // restart all php-fpm instances
+ foreach ($restart_cmds as $restart_cmd) {
+ // check whether the config dir is empty (no domains uses this daemon)
+ // so we need to create a dummy
+ $_conffiles = glob(\Froxlor\FileDir::makeCorrectFile($restart_cmd['config_dir'] . "/*.conf"));
+ if ($_conffiles === false || empty($_conffiles)) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, $called_class . '::reload: fpm config directory "' . $restart_cmd['config_dir'] . '" is empty. Creating dummy.');
+ Fpm::createDummyPool($restart_cmd['config_dir']);
+ }
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, $called_class . '::reload: running ' . $restart_cmd['reload_cmd']);
+ \Froxlor\FileDir::safe_exec(escapeshellcmd($restart_cmd['reload_cmd']));
+ }
+ }
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, $called_class . '::reload: reloading ' . $called_class);
+ \Froxlor\FileDir::safe_exec(escapeshellcmd(Settings::Get('system.apachereload_command')));
+
+ /**
+ * nginx does not auto-spawn fcgi-processes
+ */
+ if (Settings::Get('system.webserver') == "nginx" && Settings::Get('system.phpreload_command') != '' && (int) Settings::Get('phpfpm.enabled') == 0) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, $called_class . '::reload: restarting php processes');
+ \Froxlor\FileDir::safe_exec(Settings::Get('system.phpreload_command'));
+ }
+ }
+
+ /**
+ * process special config as template, by substituting {VARIABLE} with the
+ * respective value.
+ *
+ * The following variables are known at the moment:
+ *
+ * {DOMAIN} - domain name
+ * {IP} - IP for this domain
+ * {PORT} - Port for this domain
+ * {CUSTOMER} - customer name
+ * {IS_SSL} - evaluates to 'ssl' if domain/ip is ssl, otherwise it is an empty string
+ * {DOCROOT} - document root for this domain
+ *
+ * @param
+ * $template
+ * @return string
+ */
+ protected function processSpecialConfigTemplate($template, $domain, $ip, $port, $is_ssl_vhost)
+ {
+ $templateVars = array(
+ 'DOMAIN' => $domain['domain'],
+ 'CUSTOMER' => $domain['loginname'],
+ 'IP' => $ip,
+ 'PORT' => $port,
+ 'SCHEME' => ($is_ssl_vhost) ? 'https' : 'http',
+ 'DOCROOT' => $domain['documentroot']
+ );
+ return \Froxlor\PhpHelper::replaceVariables($template, $templateVars);
+ }
+
+ protected function getMyPath($ip_port = null)
+ {
+ if (! empty($ip_port) && $ip_port['docroot'] == '') {
+ if (Settings::Get('system.froxlordirectlyviahostname')) {
+ $mypath = \Froxlor\FileDir::makeCorrectDir(\Froxlor\Froxlor::getInstallDir());
+ } else {
+ $mypath = \Froxlor\FileDir::makeCorrectDir(dirname(\Froxlor\Froxlor::getInstallDir()));
+ }
+ } else {
+ // user-defined docroot, #417
+ $mypath = \Froxlor\FileDir::makeCorrectDir($ip_port['docroot']);
+ }
+ return $mypath;
+ }
+
+ protected function checkAlternativeSslPort()
+ {
+ // We must not check if our port differs from port 443,
+ // but if there is a destination-port != 443
+ $_sslport = '';
+ // This returns the first port that is != 443 with ssl enabled,
+ // ordered by ssl-certificate (if any) so that the ip/port combo
+ // with certificate is used
+ $ssldestport_stmt = Database::prepare("
+ SELECT `ip`.`port` FROM " . TABLE_PANEL_IPSANDPORTS . " `ip`
+ WHERE `ip`.`ssl` = '1' AND `ip`.`port` != 443
+ ORDER BY `ip`.`ssl_cert_file` DESC, `ip`.`port` LIMIT 1;
+ ");
+ $ssldestport = Database::pexecute_first($ssldestport_stmt);
+
+ if ($ssldestport && $ssldestport['port'] != '') {
+ $_sslport = ":" . $ssldestport['port'];
+ }
+
+ return $_sslport;
+ }
+
+ protected function froxlorVhostHasLetsEncryptCert()
+ {
+ // check whether we have an entry with valid certificates which just does not need
+ // updating yet, so we need to skip this here
+ $froxlor_ssl_settings_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = '0'
+ ");
+ $froxlor_ssl = Database::pexecute_first($froxlor_ssl_settings_stmt);
+ if ($froxlor_ssl && ! empty($froxlor_ssl['ssl_cert_file'])) {
+ return true;
+ }
+ return false;
+ }
+
+ protected function froxlorVhostLetsEncryptNeedsRenew()
+ {
+ $froxlor_ssl_settings_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "`
+ WHERE `domainid` = '0' AND
+ (`expirationdate` < DATE_ADD(NOW(), INTERVAL 30 DAY) OR `expirationdate` IS NULL)
+ ");
+ $froxlor_ssl = Database::pexecute_first($froxlor_ssl_settings_stmt);
+ if ($froxlor_ssl && ! empty($froxlor_ssl['ssl_cert_file'])) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php b/lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php
new file mode 100644
index 00000000..47941733
--- /dev/null
+++ b/lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php
@@ -0,0 +1,612 @@
+
+ * @author Froxlor team (2016-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ * @since 0.9.35
+ *
+ */
+class AcmeSh extends \Froxlor\Cron\FroxlorCron
+{
+
+ private static $apiserver = "";
+
+ private static $acmesh = "/root/.acme.sh/acme.sh";
+
+ /**
+ *
+ * @var \PDOStatement
+ */
+ private static $updcert_stmt = null;
+
+ /**
+ *
+ * @var \PDOStatement
+ */
+ private static $upddom_stmt = null;
+
+ public static $no_inserttask = false;
+
+ /**
+ * run the task
+ *
+ * @param boolean $internal
+ * @return number
+ */
+ public static function run($internal = false)
+ {
+ // usually, this is action is called from within the tasks-jobs
+ if (! defined('CRON_IS_FORCED') && ! defined('CRON_DEBUG_FLAG') && $internal == false) {
+ // Let's Encrypt cronjob is combined with regeneration of webserver configuration files.
+ // For debugging purposes you can use the --debug switch and the --force switch to run the cron manually.
+ // check whether we MIGHT need to run although there is no task to regenerate config-files
+ $issue_froxlor = self::issueFroxlorVhost();
+ $issue_domains = self::issueDomains();
+ $renew_froxlor = self::renewFroxlorVhost();
+ $renew_domains = self::renewDomains(true);
+ if ($issue_froxlor || !empty($issue_domains) || !empty($renew_froxlor) || $renew_domains) {
+ // insert task to generate certificates and vhost-configs
+ \Froxlor\System\Cronjob::inserttask(1);
+ }
+ return 0;
+ }
+
+ // set server according to settings
+ self::$apiserver = 'https://acme-' . (Settings::Get('system.letsencryptca') == 'testing' ? 'staging-' : '') . 'v0' . \Froxlor\Settings::Get('system.leapiversion') . '.api.letsencrypt.org/directory';
+
+ // validate acme.sh installation
+ if (! self::checkInstall()) {
+ return - 1;
+ }
+
+ self::checkUpgrade();
+
+ // flag for re-generation of vhost files
+ $changedetected = 0;
+
+ // prepare update sql
+ self::$updcert_stmt = Database::prepare("
+ REPLACE INTO
+ `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "`
+ SET
+ `id` = :id,
+ `domainid` = :domainid,
+ `ssl_cert_file` = :crt,
+ `ssl_key_file` = :key,
+ `ssl_ca_file` = :ca,
+ `ssl_cert_chainfile` = :chain,
+ `ssl_csr_file` = :csr,
+ `ssl_fullchain_file` = :fullchain,
+ `expirationdate` = :expirationdate
+ ");
+
+ // prepare domain update sql
+ self::$upddom_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `ssl_redirect` = '1' WHERE `id` = :domainid");
+
+ // check whether there are certificates to issue
+ $issue_froxlor = self::issueFroxlorVhost();
+ $issue_domains = self::issueDomains();
+
+ // first - generate LE for system-vhost if enabled
+ if ($issue_froxlor) {
+ // build row
+ $certrow = array(
+ 'loginname' => 'froxlor.panel',
+ 'domain' => Settings::Get('system.hostname'),
+ 'domainid' => 0,
+ 'documentroot' => \Froxlor\Froxlor::getInstallDir(),
+ 'leprivatekey' => Settings::Get('system.leprivatekey'),
+ 'lepublickey' => Settings::Get('system.lepublickey'),
+ 'leregistered' => Settings::Get('system.leregistered'),
+ 'ssl_redirect' => Settings::Get('system.le_froxlor_redirect'),
+ 'expirationdate' => null,
+ 'ssl_cert_file' => null,
+ 'ssl_key_file' => null,
+ 'ssl_ca_file' => null,
+ 'ssl_csr_file' => null,
+ 'id' => null
+ );
+
+ // add to queue
+ $issue_domains[] = $certrow;
+ }
+
+ if (count($issue_domains)) {
+ FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Requesting " . count($issue_domains) . " new Let's Encrypt certificates");
+ self::runIssueFor($issue_domains);
+ $changedetected = 1;
+ }
+
+ // compare file-system certificates with the ones in our database
+ // and update if needed
+ $renew_froxlor = self::renewFroxlorVhost();
+ $renew_domains = self::renewDomains();
+
+ if ($renew_froxlor) {
+ // build row
+ $certrow = array(
+ 'loginname' => 'froxlor.panel',
+ 'domain' => Settings::Get('system.hostname'),
+ 'domainid' => 0,
+ 'documentroot' => \Froxlor\Froxlor::getInstallDir(),
+ 'leprivatekey' => Settings::Get('system.leprivatekey'),
+ 'lepublickey' => Settings::Get('system.lepublickey'),
+ 'leregistered' => Settings::Get('system.leregistered'),
+ 'ssl_redirect' => Settings::Get('system.le_froxlor_redirect'),
+ 'expirationdate' => is_array($renew_froxlor) ? $renew_froxlor['expirationdate'] : date('Y-m-d H:i:s', 0),
+ 'ssl_cert_file' => is_array($renew_froxlor) ? $renew_froxlor['ssl_cert_file'] : null,
+ 'ssl_key_file' => is_array($renew_froxlor) ? $renew_froxlor['ssl_key_file'] : null,
+ 'ssl_ca_file' => is_array($renew_froxlor) ? $renew_froxlor['ssl_ca_file'] : null,
+ 'ssl_csr_file' => is_array($renew_froxlor) ? $renew_froxlor['ssl_csr_file'] : null,
+ 'id' => is_array($renew_froxlor) ? $renew_froxlor['id'] : null
+ );
+ $renew_domains[] = $certrow;
+ }
+
+ foreach ($renew_domains as $domain) {
+ $cronlog = FroxlorLogger::getInstanceOf(array(
+ 'loginname' => $domain['loginname'],
+ 'adminsession' => 0
+ ));
+ if (defined('CRON_IS_FORCED') || self::checkFsFilesAreNewer($domain['domain'], $domain['expirationdate'])) {
+ self::certToDb($domain, $cronlog, array());
+ $changedetected = 1;
+ }
+ }
+
+ // If we have a change in a certificate, we need to update the webserver - configs
+ // This is easiest done by just creating a new task ;)
+ if ($changedetected) {
+ if (self::$no_inserttask == false) {
+ \Froxlor\System\Cronjob::inserttask(1);
+ }
+ FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Let's Encrypt certificates have been updated");
+ } else {
+ FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "No new certificates or certificate updates found");
+ }
+ }
+
+ /**
+ * issue certificates for a list of domains
+ */
+ private static function runIssueFor($certrows = array())
+ {
+ // prepare aliasdomain-check
+ $aliasdomains_stmt = Database::prepare("
+ SELECT
+ dom.`id` as domainid,
+ dom.`domain`,
+ dom.`wwwserveralias`
+ FROM `" . TABLE_PANEL_DOMAINS . "` AS dom
+ WHERE
+ dom.`aliasdomain` = :id
+ AND dom.`letsencrypt` = 1
+ AND dom.`iswildcarddomain` = 0
+ ");
+ // iterate through all domains
+ foreach ($certrows as $certrow) {
+ // set logger to corresponding loginname for the log to appear in the users system-log
+ $cronlog = FroxlorLogger::getInstanceOf(array(
+ 'loginname' => $certrow['loginname'],
+ 'adminsession' => 0
+ ));
+ // Only issue let's encrypt certificate if no broken ssl_redirect is enabled
+ if ($certrow['ssl_redirect'] != 2) {
+ $do_force = false;
+ if (! empty($certrow['ssl_cert_file']) && empty($certrow['expirationdate'])) {
+ // domain changed (SAN or similar)
+ $do_force = true;
+ $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Re-creating certificate for " . $certrow['domain']);
+ } else {
+ $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Creating certificate for " . $certrow['domain']);
+ }
+ $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding common-name: " . $certrow['domain']);
+ $domains = array(
+ strtolower($certrow['domain'])
+ );
+ // add www. to SAN list
+ if ($certrow['wwwserveralias'] == 1) {
+ $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: www." . $certrow['domain']);
+ $domains[] = strtolower('www.' . $certrow['domain']);
+ }
+ if ($certrow['domainid'] == 0) {
+ $froxlor_aliases = Settings::Get('system.froxloraliases');
+ if (! empty($froxlor_aliases)) {
+ $froxlor_aliases = explode(",", $froxlor_aliases);
+ foreach ($froxlor_aliases as $falias) {
+ if (\Froxlor\Validate\Validate::validateDomain(trim($falias))) {
+ $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: " . strtolower(trim($falias)));
+ $domains[] = strtolower(trim($falias));
+ }
+ }
+ }
+ } else {
+ // add alias domains (and possibly www.) to SAN list
+ Database::pexecute($aliasdomains_stmt, array(
+ 'id' => $certrow['domainid']
+ ));
+ $aliasdomains = $aliasdomains_stmt->fetchAll(\PDO::FETCH_ASSOC);
+ foreach ($aliasdomains as $aliasdomain) {
+ $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: " . $aliasdomain['domain']);
+ $domains[] = strtolower($aliasdomain['domain']);
+ if ($aliasdomain['wwwserveralias'] == 1) {
+ $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: www." . $aliasdomain['domain']);
+ $domains[] = strtolower('www.' . $aliasdomain['domain']);
+ }
+ }
+ }
+
+ self::validateDns($domains, $certrow['domainid'], $cronlog);
+
+ self::runAcmeSh($certrow, $domains, $cronlog, $do_force);
+ } else {
+ $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, "Skipping Let's Encrypt generation for " . $certrow['domain'] . " due to an enabled ssl_redirect");
+ }
+ }
+ }
+
+ /**
+ * validate dns (A / AAAA record) of domain against known system ips
+ *
+ * @param array $domains
+ * @param int $domain_id
+ * @param FroxlorLogger $cronlog
+ */
+ private static function validateDns(array &$domains, $domain_id, &$cronlog)
+ {
+ if (Settings::Get('system.le_domain_dnscheck') == '1' && ! empty($domains)) {
+ $loop_domains = $domains;
+ // ips according to our system
+ $our_ips = Domain::getIpsOfDomain($domain_id);
+ foreach ($loop_domains as $idx => $domain) {
+ $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Validating DNS of " . $domain);
+ // ips accordint to NS
+ $domain_ips = PhpHelper::gethostbynamel6($domain);
+ if ($domain_ips == false || count(array_intersect($our_ips, $domain_ips)) <= 0) {
+ // no common ips...
+ $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, "Skipping Let's Encrypt generation for " . $domain . " due to no system known IP address via DNS check");
+ unset($domains[$idx]);
+ }
+ }
+ }
+ }
+
+ private static function runAcmeSh(array $certrow, array $domains, &$cronlog = null, $force = false)
+ {
+ if (! empty($domains)) {
+
+ $acmesh_cmd = self::$acmesh . " --server " . self::$apiserver . " --issue -d " . implode(" -d ", $domains);
+ // challenge path
+ $acmesh_cmd .= " -w " . Settings::Get('system.letsencryptchallengepath');
+ if (Settings::Get('system.leecc') > 0) {
+ // ecc certificate
+ $acmesh_cmd .= " --keylength ec-" . Settings::Get('system.leecc');
+ } else {
+ $acmesh_cmd .= " --keylength " . Settings::Get('system.letsencryptkeysize');
+ }
+ if (Settings::Get('system.letsencryptreuseold') != '1') {
+ $acmesh_cmd .= " --always-force-new-domain-key";
+ }
+ if (Settings::Get('system.letsencryptca') == 'testing') {
+ $acmesh_cmd .= " --staging";
+ }
+ if ($force) {
+ $acmesh_cmd .= " --force";
+ }
+ if (defined('CRON_DEBUG_FLAG')) {
+ $acmesh_cmd .= " --debug";
+ }
+
+ $acme_result = \Froxlor\FileDir::safe_exec($acmesh_cmd);
+ // debug output of acme.sh run
+ $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, implode("\n", $acme_result));
+
+ self::certToDb($certrow, $cronlog, $acme_result);
+ }
+ }
+
+ private static function certToDb($certrow, &$cronlog, $acme_result)
+ {
+ $return = array();
+ self::readCertificateToVar(strtolower($certrow['domain']), $return, $cronlog);
+
+ if (! empty($return['crt'])) {
+
+ $newcert = openssl_x509_parse($return['crt']);
+
+ if ($newcert) {
+ // Store the new data
+ Database::pexecute(self::$updcert_stmt, array(
+ 'id' => $certrow['id'],
+ 'domainid' => $certrow['domainid'],
+ 'crt' => $return['crt'],
+ 'key' => $return['key'],
+ 'ca' => $return['chain'],
+ 'chain' => $return['chain'],
+ 'csr' => $return['csr'],
+ 'fullchain' => $return['fullchain'],
+ 'expirationdate' => date('Y-m-d H:i:s', $newcert['validTo_time_t'])
+ ));
+
+ if ($certrow['ssl_redirect'] == 3) {
+ Database::pexecute(self::$upddom_stmt, array(
+ 'domainid' => $certrow['domainid']
+ ));
+ }
+
+ $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Updated Let's Encrypt certificate for " . $certrow['domain']);
+ } else {
+ $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Got non-successful Let's Encrypt response for " . $certrow['domain'] . ":\n" . implode("\n", $acme_result));
+ }
+ } else {
+ $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not get Let's Encrypt certificate for " . $certrow['domain'] . ":\n" . implode("\n", $acme_result));
+ }
+ }
+
+ /**
+ * check whether we need to issue a new certificate for froxlor itself
+ *
+ * @return boolean
+ */
+ private static function issueFroxlorVhost()
+ {
+ if (Settings::Get('system.le_froxlor_enabled') == '1') {
+ // let's encrypt is enabled, now check whether we have a certificate
+ $froxlor_ssl_settings_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "`
+ WHERE `domainid` = '0'
+ ");
+ $froxlor_ssl = Database::pexecute_first($froxlor_ssl_settings_stmt);
+ // also check for possible existing certificate
+ if (! $froxlor_ssl && ! self::checkFsFilesAreNewer(Settings::Get('system.hostname'), date('Y-m-d H:i:s'))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * check whether we need to renew-check the certificate for froxlor itself
+ *
+ * @return boolean
+ */
+ private static function renewFroxlorVhost()
+ {
+ if (Settings::Get('system.le_froxlor_enabled') == '1') {
+ // let's encrypt is enabled, now check whether we have a certificate
+ $froxlor_ssl_settings_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "`
+ WHERE `domainid` = '0'
+ ");
+ $froxlor_ssl = Database::pexecute_first($froxlor_ssl_settings_stmt);
+ // also check for possible existing certificate
+ if ($froxlor_ssl && self::checkFsFilesAreNewer(Settings::Get('system.hostname'), $froxlor_ssl['expirationdate'])) {
+ return $froxlor_ssl;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * get a list of domains that have a lets encrypt certificate (possible renew)
+ */
+ private static function renewDomains($check = false)
+ {
+ $certificates_stmt = Database::query("
+ SELECT
+ domssl.`id`,
+ domssl.`domainid`,
+ domssl.`expirationdate`,
+ domssl.`ssl_cert_file`,
+ domssl.`ssl_key_file`,
+ dom.`domain`,
+ dom.`id` AS 'domainid',
+ dom.`ssl_redirect`,
+ cust.`loginname`
+ FROM
+ `" . TABLE_PANEL_CUSTOMERS . "` AS cust,
+ `" . TABLE_PANEL_DOMAINS . "` AS dom
+ LEFT JOIN
+ `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` AS domssl ON
+ dom.`id` = domssl.`domainid`
+ WHERE
+ dom.`customerid` = cust.`customerid`
+ AND cust.deactivated = 0
+ AND dom.`letsencrypt` = 1
+ AND dom.`aliasdomain` IS NULL
+ AND dom.`iswildcarddomain` = 0
+ ");
+ $renew_certs = $certificates_stmt->fetchAll(\PDO::FETCH_ASSOC);
+ if ($renew_certs) {
+ if ($check) {
+ foreach ($renew_certs as $cert) {
+ if (self::checkFsFilesAreNewer($cert['domain'], $cert['expirationdate'])) {
+ return true;
+ }
+ }
+ return false;
+ }
+ return $renew_certs;
+ }
+ return array();
+ }
+
+ /**
+ * get a list of domains that require a new certificate (issue)
+ */
+ private static function issueDomains()
+ {
+ $certificates_stmt = Database::query("
+ SELECT
+ domssl.`id`,
+ domssl.`domainid`,
+ domssl.`expirationdate`,
+ domssl.`ssl_cert_file`,
+ domssl.`ssl_key_file`,
+ domssl.`ssl_ca_file`,
+ domssl.`ssl_csr_file`,
+ dom.`domain`,
+ dom.`wwwserveralias`,
+ dom.`documentroot`,
+ dom.`id` AS 'domainid',
+ dom.`ssl_redirect`,
+ cust.`leprivatekey`,
+ cust.`lepublickey`,
+ cust.`leregistered`,
+ cust.`customerid`,
+ cust.`loginname`
+ FROM
+ `" . TABLE_PANEL_CUSTOMERS . "` AS cust,
+ `" . TABLE_PANEL_DOMAINS . "` AS dom
+ LEFT JOIN
+ `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` AS domssl ON
+ dom.`id` = domssl.`domainid`
+ WHERE
+ dom.`customerid` = cust.`customerid`
+ AND cust.deactivated = 0
+ AND dom.`letsencrypt` = 1
+ AND dom.`aliasdomain` IS NULL
+ AND dom.`iswildcarddomain` = 0
+ AND domssl.`expirationdate` IS NULL
+ ");
+ $customer_ssl = $certificates_stmt->fetchAll(\PDO::FETCH_ASSOC);
+ if ($customer_ssl) {
+ return $customer_ssl;
+ }
+ return array();
+ }
+
+ private static function checkFsFilesAreNewer($domain, $cert_date = 0)
+ {
+ $certificate_folder = self::getWorkingDirFromEnv($domain);
+ $ssl_file = \Froxlor\FileDir::makeCorrectFile($certificate_folder . '/' . $domain . '.cer');
+
+ if (is_dir($certificate_folder) && file_exists($ssl_file) && is_readable($ssl_file)) {
+ $cert_data = openssl_x509_parse(file_get_contents($ssl_file));
+ if ($cert_data && $cert_data['validTo_time_t'] > strtotime($cert_date)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static function getWorkingDirFromEnv($domain = "", $forced_noecc = false)
+ {
+ if (Settings::Get('system.leecc') > 0 && ! $forced_noecc) {
+ $domain .= "_ecc";
+ }
+ $env_file = FileDir::makeCorrectFile(dirname(self::$acmesh) . '/acme.sh.env');
+ if (file_exists($env_file)) {
+ $output = [];
+ $cut = << 0) {
+ $certificate_folder_noecc = self::getWorkingDirFromEnv($domain, true);
+ }
+ $certificate_folder = \Froxlor\FileDir::makeCorrectDir($certificate_folder);
+
+ if (is_dir($certificate_folder) || is_dir($certificate_folder_noecc)) {
+ foreach ([
+ 'crt' => $domain . '.cer',
+ 'key' => $domain . '.key',
+ 'chain' => 'ca.cer',
+ 'fullchain' => 'fullchain.cer',
+ 'csr' => $domain . '.csr'
+ ] as $index => $sslfile) {
+ $ssl_file = \Froxlor\FileDir::makeCorrectFile($certificate_folder . '/' . $sslfile);
+ if (file_exists($ssl_file)) {
+ $return[$index] = file_get_contents($ssl_file);
+ } else {
+ if (! empty($certificate_folder_noecc)) {
+ $ssl_file_fb = \Froxlor\FileDir::makeCorrectFile($certificate_folder_noecc . '/' . $sslfile);
+ if (file_exists($ssl_file_fb)) {
+ $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, "ECC certificates activated but found only non-ecc file");
+ $return[$index] = file_get_contents($ssl_file_fb);
+ continue;
+ }
+ }
+ $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not find file '" . $sslfile . "' in '" . $certificate_folder . "'");
+ $return[$index] = null;
+ }
+ }
+ } else {
+ $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not find certificate-folder '" . $certificate_folder . "'");
+ }
+ }
+
+ /**
+ * install acme.sh if not found yet
+ */
+ private static function checkInstall($tries = 0)
+ {
+ if (! file_exists(self::$acmesh) && $tries > 0) {
+ FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Download/installation of acme.sh seems to have failed. Re-run cronjob to try again or install manually to '" . self::$acmesh . "'");
+ echo PHP_EOL . "Download/installation of acme.sh seems to have failed. Re-run cronjob to try again or install manually to '" . self::$acmesh . "'" . PHP_EOL;
+ return false;
+ } else if (! file_exists(self::$acmesh)) {
+ FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Could not find acme.sh - installing it to /root/.acme.sh/");
+ $return = false;
+ \Froxlor\FileDir::safe_exec("wget -O - https://get.acme.sh | sh", $return, array(
+ '|'
+ ));
+ // check whether the installation worked
+ return self::checkInstall(++ $tries);
+ }
+ return true;
+ }
+
+ /**
+ * run upgrade
+ */
+ private static function checkUpgrade()
+ {
+ $acmesh_result = \Froxlor\FileDir::safe_exec(self::$acmesh . " --upgrade --auto-upgrade 0");
+ // check for activated cron
+ $acmesh_result2 = \Froxlor\FileDir::safe_exec(self::$acmesh . " --install-cronjob");
+ FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Checking for LetsEncrypt client upgrades before renewing certificates:\n" . implode("\n", $acmesh_result) . "\n" . implode("\n", $acmesh_result2));
+ }
+}
diff --git a/lib/Froxlor/Cron/Http/Lighttpd.php b/lib/Froxlor/Cron/Http/Lighttpd.php
new file mode 100644
index 00000000..fda3b6bc
--- /dev/null
+++ b/lib/Froxlor/Cron/Http/Lighttpd.php
@@ -0,0 +1,1005 @@
+ (2003-2009)
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ * @todo ssl-redirect to non-standard port
+ */
+class Lighttpd extends HttpConfigBase
+{
+
+ // protected
+ protected $lighttpd_data = array();
+
+ protected $needed_htpasswds = array();
+
+ protected $auth_backend_loaded = false;
+
+ protected $htpasswd_files = array();
+
+ protected $mod_accesslog_loaded = "0";
+
+ /**
+ * indicator whether a customer is deactivated or not
+ * if yes, only the webroot will be generated
+ *
+ * @var bool
+ */
+ private $deactivated = false;
+
+ public function createIpPort()
+ {
+ $result_ipsandports_stmt = Database::query("SELECT * FROM `" . TABLE_PANEL_IPSANDPORTS . "` ORDER BY `ip` ASC, `port` ASC");
+
+ while ($row_ipsandports = $result_ipsandports_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if (filter_var($row_ipsandports['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
+ $ip = '[' . $row_ipsandports['ip'] . ']';
+ $port = $row_ipsandports['port'];
+ $ipv6 = 'server.use-ipv6 = "enable"' . "\n";
+ } else {
+ $ip = $row_ipsandports['ip'];
+ $port = $row_ipsandports['port'];
+ $ipv6 = '';
+ }
+
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'lighttpd::createIpPort: creating ip/port settings for ' . $ip . ":" . $port);
+ $vhost_filename = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.apacheconf_vhost') . '/10_froxlor_ipandport_' . trim(str_replace(':', '.', $row_ipsandports['ip']), '.') . '.' . $row_ipsandports['port'] . '.conf');
+
+ if (! isset($this->lighttpd_data[$vhost_filename])) {
+ $this->lighttpd_data[$vhost_filename] = '';
+ }
+
+ $this->lighttpd_data[$vhost_filename] .= '$SERVER["socket"] == "' . $ip . ':' . $port . '" {' . "\n";
+
+ if ($row_ipsandports['listen_statement'] == '1') {
+ $this->lighttpd_data[$vhost_filename] .= 'server.port = ' . $port . "\n";
+ $this->lighttpd_data[$vhost_filename] .= 'server.bind = "' . $ip . '"' . "\n";
+ $this->lighttpd_data[$vhost_filename] .= $ipv6;
+ }
+
+ if ($row_ipsandports['vhostcontainer'] == '1') {
+ $myhost = str_replace('.', '\.', Settings::Get('system.hostname'));
+ $this->lighttpd_data[$vhost_filename] .= '# Froxlor default vhost' . "\n";
+ $this->lighttpd_data[$vhost_filename] .= '$HTTP["host"] =~ "^(?:www\.|)' . $myhost . '$" {' . "\n";
+
+ $mypath = $this->getMyPath($row_ipsandports);
+
+ $this->lighttpd_data[$vhost_filename] .= ' server.document-root = "' . $mypath . '"' . "\n";
+
+ $is_redirect = false;
+ // check for SSL redirect
+ if ($row_ipsandports['ssl'] == '0' && Settings::Get('system.le_froxlor_redirect') == '1') {
+ $is_redirect = true;
+ // check whether froxlor uses Let's Encrypt and not cert is being generated yet
+ // or a renew is ongoing - disable redirect
+ if (Settings::Get('system.le_froxlor_enabled') && ($this->froxlorVhostHasLetsEncryptCert() == false || $this->froxlorVhostLetsEncryptNeedsRenew())) {
+ $this->lighttpd_data[$vhost_filename] .= '# temp. disabled ssl-redirect due to Let\'s Encrypt certificate generation.' . PHP_EOL;
+ $is_redirect = false;
+ } else {
+ $_sslport = $this->checkAlternativeSslPort();
+ $mypath = 'https://' . Settings::Get('system.hostname') . $_sslport . '/';
+
+ $this->lighttpd_data[$vhost_filename] .= ' url.redirect = (' . "\n";
+ $this->lighttpd_data[$vhost_filename] .= ' "^/(.*)$" => "' . $mypath . '$1"' . "\n";
+ $this->lighttpd_data[$vhost_filename] .= ' )' . "\n";
+ }
+ }
+
+ if (! $is_redirect) {
+ /**
+ * dirprotection, see #72
+ *
+ * @todo use better regex for this, deferred until 0.9.5
+ *
+ * $this->lighttpd_data[$vhost_filename].= ' $HTTP["url"] =~ "^/(.+)\/(.+)\.php" {' . "\n";
+ * $this->lighttpd_data[$vhost_filename].= ' url.access-deny = ("")' . "\n";
+ * $this->lighttpd_data[$vhost_filename].= ' }' . "\n";
+ */
+
+ /**
+ * own php-fpm vhost
+ */
+ if ((int) Settings::Get('phpfpm.enabled') == 1 && (int) Settings::Get('phpfpm.enabled_ownvhost') == 1) {
+ // get fpm config
+ $fpm_sel_stmt = Database::prepare("
+ SELECT f.id FROM `" . TABLE_PANEL_FPMDAEMONS . "` f
+ LEFT JOIN `" . TABLE_PANEL_PHPCONFIGS . "` p ON p.fpmsettingid = f.id
+ WHERE p.id = :phpconfigid
+ ");
+ $fpm_config = Database::pexecute_first($fpm_sel_stmt, array(
+ 'phpconfigid' => Settings::Get('phpfpm.vhost_defaultini')
+ ));
+ $domain = array(
+ 'id' => 'none',
+ 'domain' => Settings::Get('system.hostname'),
+ 'adminid' => 1, /* first admin-user (superadmin) */
+ 'mod_fcgid_starter' => - 1,
+ 'mod_fcgid_maxrequests' => - 1,
+ 'guid' => Settings::Get('phpfpm.vhost_httpuser'),
+ 'openbasedir' => 0,
+ 'email' => Settings::Get('panel.adminmail'),
+ 'loginname' => 'froxlor.panel',
+ 'documentroot' => $mypath,
+ 'customerroot' => $mypath,
+ 'fpm_config_id' => isset($fpm_config['id']) ? $fpm_config['id'] : 1
+ );
+
+ $php = new PhpInterface($domain);
+
+ $this->lighttpd_data[$vhost_filename] .= ' fastcgi.server = ( ' . "\n";
+ $this->lighttpd_data[$vhost_filename] .= "\t" . '".php" => (' . "\n";
+ $this->lighttpd_data[$vhost_filename] .= "\t\t" . '"localhost" => (' . "\n";
+ $this->lighttpd_data[$vhost_filename] .= "\t\t" . '"socket" => "' . $php->getInterface()->getSocketFile() . '",' . "\n";
+ $this->lighttpd_data[$vhost_filename] .= "\t\t" . '"check-local" => "enable",' . "\n";
+ $this->lighttpd_data[$vhost_filename] .= "\t\t" . '"disable-time" => 1' . "\n";
+ $this->lighttpd_data[$vhost_filename] .= "\t" . ')' . "\n";
+ $this->lighttpd_data[$vhost_filename] .= "\t" . ')' . "\n";
+ $this->lighttpd_data[$vhost_filename] .= ' )' . "\n";
+ } else {
+ $domain = array(
+ 'id' => 'none',
+ 'domain' => Settings::Get('system.hostname'),
+ 'adminid' => 1, /* first admin-user (superadmin) */
+ 'guid' => Settings::Get('system.httpuser'),
+ 'openbasedir' => 0,
+ 'email' => Settings::Get('panel.adminmail'),
+ 'loginname' => 'froxlor.panel',
+ 'documentroot' => $mypath,
+ 'customerroot' => $mypath
+ );
+ }
+ } else {
+ // fallback of froxlor domain-data for processSpecialConfigTemplate()
+ $domain = array(
+ 'domain' => Settings::Get('system.hostname'),
+ 'loginname' => 'froxlor.panel',
+ 'documentroot' => $mypath,
+ 'customerroot' => $mypath
+ );
+ }
+
+ if ($row_ipsandports['specialsettings'] != '' && ($row_ipsandports['ssl'] == '0' || ($row_ipsandports['ssl'] == '1' && Settings::Get('system.use_ssl') == '1' && $row_ipsandports['include_specialsettings'] == '1'))) {
+ $this->lighttpd_data[$vhost_filename] .= $this->processSpecialConfigTemplate($row_ipsandports['specialsettings'], $domain, $row_ipsandports['ip'], $row_ipsandports['port'], $row_ipsandports['ssl'] == '1') . "\n";
+ }
+
+ $this->lighttpd_data[$vhost_filename] .= '}' . "\n";
+ }
+
+ if ($row_ipsandports['ssl'] == '1') {
+
+ if ($row_ipsandports['ssl_specialsettings'] != '') {
+ $this->lighttpd_data[$vhost_filename] .= $this->processSpecialConfigTemplate($row_ipsandports['ssl_specialsettings'], $domain, $row_ipsandports['ip'], $row_ipsandports['port'], $row_ipsandports['ssl'] == '1') . "\n";
+ }
+
+ // check for required fallback
+ if (($row_ipsandports['ssl_cert_file'] == '' || ! file_exists($row_ipsandports['ssl_cert_file'])) && (Settings::Get('system.le_froxlor_enabled') == '0' || $this->froxlorVhostHasLetsEncryptCert() == false)) {
+ $row_ipsandports['ssl_cert_file'] = Settings::Get('system.ssl_cert_file');
+ if (! file_exists($row_ipsandports['ssl_cert_file'])) {
+ // explicitly disable ssl for this vhost
+ $row_ipsandports['ssl_cert_file'] = "";
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'System certificate file "' . Settings::Get('system.ssl_cert_file') . '" does not seem to exist. Disabling SSL-vhost for "' . Settings::Get('system.hostname') . '"');
+ }
+ }
+
+ if ($row_ipsandports['ssl_ca_file'] == '') {
+ $row_ipsandports['ssl_ca_file'] = Settings::Get('system.ssl_ca_file');
+ }
+
+ $domain = array(
+ 'id' => 0,
+ 'domain' => Settings::Get('system.hostname'),
+ 'adminid' => 1, /* first admin-user (superadmin) */
+ 'loginname' => 'froxlor.panel',
+ 'documentroot' => $mypath,
+ 'customerroot' => $mypath,
+ 'parentdomainid' => 0
+ );
+
+ // override corresponding array values
+ $domain['ssl_cert_file'] = $row_ipsandports['ssl_cert_file'];
+ $domain['ssl_key_file'] = $row_ipsandports['ssl_key_file'];
+ $domain['ssl_ca_file'] = $row_ipsandports['ssl_ca_file'];
+ $domain['ssl_cert_chainfile'] = $row_ipsandports['ssl_cert_chainfile'];
+
+ // SSL STUFF
+ $dssl = new DomainSSL();
+ // this sets the ssl-related array-indices in the $domain array
+ // if the domain has customer-defined ssl-certificates
+ $dssl->setDomainSSLFilesArray($domain);
+
+ if ($domain['ssl_cert_file'] != '') {
+
+ // check for existence, #1485
+ if (! file_exists($domain['ssl_cert_file'])) {
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, $ip . ':' . $port . ' :: certificate file "' . $domain['ssl_cert_file'] . '" does not exist! Cannot create ssl-directives');
+ echo $ip . ':' . $port . ' :: certificate file "' . $domain['ssl_cert_file'] . '" does not exist! Cannot create SSL-directives' . "\n";
+ } else {
+ $this->lighttpd_data[$vhost_filename] .= 'ssl.engine = "enable"' . "\n";
+ $this->lighttpd_data[$vhost_filename] .= 'ssl.use-compression = "disable"' . "\n";
+ if (! empty(Settings::Get('system.dhparams_file'))) {
+ $dhparams = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.dhparams_file'));
+ if (! file_exists($dhparams)) {
+ \Froxlor\FileDir::safe_exec('openssl dhparam -out ' . escapeshellarg($dhparams) . ' 4096');
+ }
+ $this->lighttpd_data[$vhost_filename] .= 'ssl.dh-file = "' . $dhparams . '"' . "\n";
+ $this->lighttpd_data[$vhost_filename] .= 'ssl.ec-curve = "secp384r1"' . "\n";
+ }
+ $this->lighttpd_data[$vhost_filename] .= 'ssl.use-sslv2 = "disable"' . "\n";
+ $this->lighttpd_data[$vhost_filename] .= 'ssl.use-sslv3 = "disable"' . "\n";
+ $this->lighttpd_data[$vhost_filename] .= 'ssl.cipher-list = "' . Settings::Get('system.ssl_cipher_list') . '"' . "\n";
+ $this->lighttpd_data[$vhost_filename] .= 'ssl.honor-cipher-order = "enable"' . "\n";
+ $this->lighttpd_data[$vhost_filename] .= 'ssl.pemfile = "' . \Froxlor\FileDir::makeCorrectFile($domain['ssl_cert_file']) . '"' . "\n";
+
+ if ($domain['ssl_ca_file'] != '') {
+ // check for existence, #1485
+ if (! file_exists($domain['ssl_ca_file'])) {
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, $ip . ':' . $port . ' :: certificate CA file "' . $domain['ssl_ca_file'] . '" does not exist! Cannot create ssl-directives');
+ echo $ip . ':' . port . ' :: certificate CA file "' . $domain['ssl_ca_file'] . '" does not exist! SSL-directives might not be working' . "\n";
+ } else {
+ $this->lighttpd_data[$vhost_filename] .= 'ssl.ca-file = "' . \Froxlor\FileDir::makeCorrectFile($domain['ssl_ca_file']) . '"' . "\n";
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * this function will create a new file which will be included
+ * if Settings::Get('system.apacheconf_vhost') is a folder
+ * refs #70
+ */
+ $vhosts = $this->createLighttpdHosts($row_ipsandports['id'], $row_ipsandports['ssl'], $vhost_filename);
+ if ($vhosts !== null && is_array($vhosts) && isset($vhosts[0])) {
+ // sort vhosts by number (subdomains first!)
+ sort($vhosts);
+
+ foreach ($vhosts as $vhost) {
+ $this->lighttpd_data[$vhost_filename] .= ' include "' . $vhost . '"' . "\n";
+ }
+ }
+
+ $this->lighttpd_data[$vhost_filename] .= '}' . "\n";
+ }
+
+ /**
+ * bug #unknown-yet
+ */
+ $this->createStandardErrorHandler();
+ }
+
+ /**
+ * define a default server.error-handler-404-statement, bug #unknown-yet
+ */
+ private function createStandardErrorHandler()
+ {
+ if (Settings::Get('defaultwebsrverrhandler.enabled') == '1' && Settings::Get('defaultwebsrverrhandler.err404') != '') {
+ $vhost_filename = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.apacheconf_vhost') . '/05_froxlor_default_errorhandler.conf');
+
+ if (! isset($this->lighttpd_data[$vhost_filename])) {
+ $this->lighttpd_data[$vhost_filename] = '';
+ }
+
+ $defhandler = Settings::Get('defaultwebsrverrhandler.err404');
+ if (! \Froxlor\Validate\Validate::validateUrl($defhandler)) {
+ $defhandler = \Froxlor\FileDir::makeCorrectFile($defhandler);
+ }
+ $this->lighttpd_data[$vhost_filename] = 'server.error-handler-404 = "' . $defhandler . '"';
+ }
+ }
+
+ protected function createHtaccess($domain)
+ {
+ $needed_htpasswds = array();
+ $result_htpasswds_stmt = Database::prepare("
+ SELECT * FROM " . TABLE_PANEL_HTPASSWDS . "
+ WHERE `path` LIKE :docroot
+ ");
+ Database::pexecute($result_htpasswds_stmt, array(
+ 'docroot' => $domain['documentroot'] . '%'
+ ));
+
+ $htaccess_text = '';
+ while ($row_htpasswds = $result_htpasswds_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $row_htpasswds['path'] = \Froxlor\FileDir::makeCorrectDir($row_htpasswds['path']);
+ \Froxlor\FileDir::mkDirWithCorrectOwnership($domain['documentroot'], $row_htpasswds['path'], $domain['guid'], $domain['guid']);
+
+ $filename = $row_htpasswds['customerid'] . '-' . md5($row_htpasswds['path']) . '.htpasswd';
+
+ if (! in_array($row_htpasswds['path'], $needed_htpasswds)) {
+ if (! isset($this->needed_htpasswds[$filename])) {
+ $this->needed_htpasswds[$filename] = '';
+ }
+
+ if (! strstr($this->needed_htpasswds[$filename], $row_htpasswds['username'] . ':' . $row_htpasswds['password'])) {
+ $this->needed_htpasswds[$filename] .= $row_htpasswds['username'] . ':' . $row_htpasswds['password'] . "\n";
+ }
+
+ $htaccess_path = substr($row_htpasswds['path'], strlen($domain['documentroot']) - 1);
+ $htaccess_path = \Froxlor\FileDir::makeCorrectDir($htaccess_path);
+
+ $htaccess_text .= ' $HTTP["url"] =~ "^' . $htaccess_path . '" {' . "\n";
+ $htaccess_text .= ' auth.backend = "htpasswd"' . "\n";
+ $htaccess_text .= ' auth.backend.htpasswd.userfile = "' . \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.apacheconf_htpasswddir') . '/' . $filename) . '"' . "\n";
+ $htaccess_text .= ' auth.require = ( ' . "\n";
+ $htaccess_text .= ' "' . $htaccess_path . '" =>' . "\n";
+ $htaccess_text .= ' (' . "\n";
+ $htaccess_text .= ' "method" => "basic",' . "\n";
+ $htaccess_text .= ' "realm" => "' . $row_htpasswds['authname'] . '",' . "\n";
+ $htaccess_text .= ' "require" => "valid-user"' . "\n";
+ $htaccess_text .= ' )' . "\n";
+ $htaccess_text .= ' )' . "\n";
+ $htaccess_text .= ' }' . "\n";
+
+ $needed_htpasswds[] = $row_htpasswds['path'];
+ }
+ }
+
+ return $htaccess_text;
+ }
+
+ public function createVirtualHosts()
+ {
+ return;
+ }
+
+ public function createFileDirOptions()
+ {
+ return;
+ }
+
+ protected function composePhpOptions($domain)
+ {
+ return;
+ }
+
+ public function createOwnVhostStarter()
+ {
+ return;
+ }
+
+ protected function createLighttpdHosts($ipid, $ssl, $vhost_filename)
+ {
+ $domains = WebserverBase::getVhostsToCreate();
+ $included_vhosts = array();
+ foreach ($domains as $domain) {
+
+ if (is_dir(Settings::Get('system.apacheconf_vhost'))) {
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg(\Froxlor\FileDir::makeCorrectDir(Settings::Get('system.apacheconf_vhost') . '/vhosts/')));
+
+ // determine correct include-path:
+ // e.g. '/etc/lighttpd/conf-enabled/vhosts/ has to become'
+ // 'conf-enabled/vhosts/' (damn debian, but luckily works too on other distros)
+ $_tmp_path = substr(\Froxlor\FileDir::makeCorrectDir(Settings::Get('system.apacheconf_vhost')), 0, - 1);
+ $_pos = strrpos($_tmp_path, '/');
+ $_inc_path = substr($_tmp_path, $_pos + 1);
+
+ // maindomain
+ if ((int) $domain['parentdomainid'] == 0 && \Froxlor\Domain\Domain::isCustomerStdSubdomain((int) $domain['id']) == false && ((int) $domain['ismainbutsubto'] == 0 || \Froxlor\Domain\Domain::domainMainToSubExists($domain['ismainbutsubto']) == false)) {
+ $vhost_no = '50';
+ } elseif ((int) $domain['parentdomainid'] == 0 && \Froxlor\Domain\Domain::isCustomerStdSubdomain((int) $domain['id']) == false && (int) $domain['ismainbutsubto'] > 0) {
+ // sub-but-main-domain
+ $vhost_no = '51';
+ } else {
+ // subdomains
+ // number of dots in a domain specifies it's position (and depth of subdomain) starting at 89 going downwards on higher depth
+ $vhost_no = (string) (90 - substr_count($domain['domain'], ".") + 1);
+ }
+
+ if ($ssl == '1') {
+ $vhost_no = (int) $vhost_no += 10;
+ }
+
+ $vhost_filename = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.apacheconf_vhost') . '/vhosts/' . $vhost_no . '_' . $domain['domain'] . '.conf');
+ $included_vhosts[] = $_inc_path . '/vhosts/' . $vhost_no . '_' . $domain['domain'] . '.conf';
+ }
+
+ if (! isset($this->lighttpd_data[$vhost_filename])) {
+ $this->lighttpd_data[$vhost_filename] = '';
+ }
+
+ if ((! empty($this->lighttpd_data[$vhost_filename]) && ! is_dir(Settings::Get('system.apacheconf_vhost'))) || is_dir(Settings::Get('system.apacheconf_vhost'))) {
+ if ($ssl == '1') {
+ $ssl_vhost = true;
+ } else {
+ $ssl_vhost = false;
+ }
+
+ // FIXME we get duplicate entries of a vhost if it has assigned more than one IP
+ // checking if the lightt_data for that filename is empty *might* be correct
+ if ($this->lighttpd_data[$vhost_filename] == '') {
+ $this->lighttpd_data[$vhost_filename] .= $this->getVhostContent($domain, $ssl_vhost, $ipid);
+ }
+ }
+ }
+ return $included_vhosts;
+ }
+
+ protected function getVhostContent($domain, $ssl_vhost = false, $ipid = 0)
+ {
+ if ($ssl_vhost === true && $domain['ssl'] != '1' && $domain['ssl_enabled'] != '1' && $domain['ssl_redirect'] != '1') {
+ return '';
+ }
+
+ $vhost_content = '';
+ $vhost_content .= $this->getServerNames($domain) . " {\n";
+
+ // respect ssl_redirect settings, #542
+ if ($ssl_vhost == false && $domain['ssl'] == '1' && $domain['ssl_redirect'] == '1') {
+ // We must not check if our port differs from port 443,
+ // but if there is a destination-port != 443
+ $_sslport = '';
+ // This returns the first port that is != 443 with ssl enabled, if any
+ // ordered by ssl-certificate (if any) so that the ip/port combo
+ // with certificate is used
+ $ssldestport_stmt = Database::prepare("SELECT `ip`.`port` FROM " . TABLE_PANEL_IPSANDPORTS . " `ip`
+ LEFT JOIN `" . TABLE_DOMAINTOIP . "` `dip` ON (`ip`.`id` = `dip`.`id_ipandports`)
+ WHERE `dip`.`id_domain` = :domainid
+ AND `ip`.`ssl` = '1' AND `ip`.`port` != 443
+ ORDER BY `ip`.`ssl_cert_file` DESC, `ip`.`port` LIMIT 1;");
+ $ssldestport = Database::pexecute_first($ssldestport_stmt, array(
+ 'domainid' => $domain['id']
+ ));
+
+ if ($ssldestport && $ssldestport['port'] != '') {
+ $_sslport = ":" . $ssldestport['port'];
+ }
+
+ $domain['documentroot'] = 'https://%1' . $_sslport . '/';
+ }
+
+ // avoid using any whitespaces
+ $domain['documentroot'] = trim($domain['documentroot']);
+
+ if (preg_match('/^https?\:\/\//', $domain['documentroot'])) {
+ $uri = $domain['documentroot'];
+
+ // Get domain's redirect code
+ $code = \Froxlor\Domain\Domain::getDomainRedirectCode($domain['id']);
+
+ $vhost_content .= ' url.redirect-code = ' . $code . "\n";
+ $vhost_content .= ' url.redirect = (' . "\n";
+ $vhost_content .= ' "^/(.*)$" => "' . $uri . '$1"' . "\n";
+ $vhost_content .= ' )' . "\n";
+ } else {
+
+ \Froxlor\FileDir::mkDirWithCorrectOwnership($domain['customerroot'], $domain['documentroot'], $domain['guid'], $domain['guid'], true, true);
+
+ $only_webroot = false;
+ if ($ssl_vhost === false && $domain['ssl_redirect'] == '1') {
+ $only_webroot = true;
+ }
+
+ $vhost_content .= $this->getWebroot($domain, $ssl_vhost);
+ if (! $only_webroot) {
+ if ($this->deactivated == false) {
+ $vhost_content .= $this->createHtaccess($domain);
+ $vhost_content .= $this->createPathOptions($domain);
+ $vhost_content .= $this->composePhpOptions($domain);
+ $vhost_content .= $this->getStats($domain);
+
+ $ipandport_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_IPSANDPORTS . "`
+ WHERE `id` = :id
+ ");
+ $ipandport = Database::pexecute_first($ipandport_stmt, array(
+ 'id' => $ipid
+ ));
+
+ $domain['ip'] = $ipandport['ip'];
+ $domain['port'] = $ipandport['port'];
+ $domain['ssl_cert_file'] = $ipandport['ssl_cert_file'];
+ $domain['ssl_key_file'] = $ipandport['ssl_key_file'];
+ $domain['ssl_ca_file'] = $ipandport['ssl_ca_file'];
+ // #418
+ $domain['ssl_cert_chainfile'] = $ipandport['ssl_cert_chainfile'];
+
+ // SSL STUFF
+ $dssl = new DomainSSL();
+ // this sets the ssl-related array-indices in the $domain array
+ // if the domain has customer-defined ssl-certificates
+ $dssl->setDomainSSLFilesArray($domain);
+
+ $vhost_content .= $this->getSslSettings($domain, $ssl_vhost);
+
+ if ($domain['specialsettings'] != '' && ($ssl_vhost == false || ($ssl_vhost == true && $domain['include_specialsettings'] == 1))) {
+ $vhost_content .= $this->processSpecialConfigTemplate($domain['specialsettings'], $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n";
+ }
+
+ if ($domain['ssl_specialsettings'] != '' && $ssl_vhost == true) {
+ $vhost_content .= $this->processSpecialConfigTemplate($domain['ssl_specialsettings'], $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n";
+ }
+
+ if ($ipandport['default_vhostconf_domain'] != '' && ($ssl_vhost == false || ($ssl_vhost == true && $ipandport['include_default_vhostconf_domain'] == '1'))) {
+ $vhost_content .= $this->processSpecialConfigTemplate($ipandport['default_vhostconf_domain'], $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n";
+ }
+
+ if ($ipandport['ssl_default_vhostconf_domain'] != '' && $ssl_vhost == true) {
+ $vhost_content .= $this->processSpecialConfigTemplate($ipandport['ssl_default_vhostconf_domain'], $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n";
+ }
+
+ if (Settings::Get('system.default_vhostconf') != '' && ($ssl_vhost == false || ($ssl_vhost == true && Settings::Get('system.include_default_vhostconf') == 1))) {
+ $vhost_content .= $this->processSpecialConfigTemplate(Settings::Get('system.default_vhostconf'), $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n";
+ }
+
+ if (Settings::Get('system.default_sslvhostconf') != '' && $ssl_vhost == true) {
+ $vhost_content .= $this->processSpecialConfigTemplate(Settings::Get('system.default_sslvhostconf'), $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n";
+ }
+ }
+ $vhost_content .= $this->getLogFiles($domain);
+ }
+ }
+
+ $vhost_content .= '}' . "\n";
+
+ return $vhost_content;
+ }
+
+ protected function getSslSettings($domain, $ssl_vhost)
+ {
+ $ssl_settings = '';
+
+ if ($ssl_vhost === true && $domain['ssl'] == '1' && (int) Settings::Get('system.use_ssl') == 1) {
+ if ($domain['ssl_cert_file'] == '' || ! file_exists($domain['ssl_cert_file'])) {
+ $domain['ssl_cert_file'] = Settings::Get('system.ssl_cert_file');
+ if (! file_exists($domain['ssl_cert_file'])) {
+ // explicitly disable ssl for this vhost
+ $domain['ssl_cert_file'] = "";
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'System certificate file "' . Settings::Get('system.ssl_cert_file') . '" does not seem to exist. Disabling SSL-vhost for "' . $domain['domain'] . '"');
+ }
+ }
+
+ if ($domain['ssl_ca_file'] == '') {
+ $domain['ssl_ca_file'] = Settings::Get('system.ssl_ca_file');
+ }
+
+ if ($domain['ssl_cert_file'] != '') {
+
+ $ssl_cipher_list = ($domain['override_tls'] == '1' && ! empty($domain['ssl_cipher_list'])) ? $domain['ssl_cipher_list'] : Settings::Get('system.ssl_cipher_list');
+
+ // ssl.engine only necessary once in the ip/port vhost (SERVER['socket'] condition)
+ // $ssl_settings .= 'ssl.engine = "enable"' . "\n";
+ $ssl_settings .= 'ssl.use-compression = "disable"' . "\n";
+ if (! empty(Settings::Get('system.dhparams_file'))) {
+ $dhparams = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.dhparams_file'));
+ if (! file_exists($dhparams)) {
+ \Froxlor\FileDir::safe_exec('openssl dhparam -out ' . escapeshellarg($dhparams) . ' 4096');
+ }
+ $ssl_settings .= 'ssl.dh-file = "' . $dhparams . '"' . "\n";
+ $ssl_settings .= 'ssl.ec-curve = "secp384r1"' . "\n";
+ }
+ $ssl_settings .= 'ssl.use-sslv2 = "disable"' . "\n";
+ $ssl_settings .= 'ssl.use-sslv3 = "disable"' . "\n";
+ $ssl_settings .= 'ssl.cipher-list = "' . $ssl_cipher_list . '"' . "\n";
+ $ssl_settings .= 'ssl.honor-cipher-order = ' . ($domain['ssl_honorcipherorder'] == '1' ? '"enable"' : '"disable"') . "\n";
+ $ssl_settings .= 'ssl.pemfile = "' . \Froxlor\FileDir::makeCorrectFile($domain['ssl_cert_file']) . '"' . "\n";
+
+ if ($domain['ssl_ca_file'] != '') {
+ $ssl_settings .= 'ssl.ca-file = "' . \Froxlor\FileDir::makeCorrectFile($domain['ssl_ca_file']) . '"' . "\n";
+ }
+
+ if ($domain['hsts'] >= 0) {
+
+ $ssl_settings .= '$HTTP["scheme"] == "https" { setenv.add-response-header = ( "Strict-Transport-Security" => "max-age=' . $domain['hsts'];
+ if ($domain['hsts_sub'] == 1) {
+ $ssl_settings .= '; includeSubDomains';
+ }
+ if ($domain['hsts_preload'] == 1) {
+ $ssl_settings .= '; preload';
+ }
+ $ssl_settings .= '") }' . "\n";
+ }
+ }
+ }
+ return $ssl_settings;
+ }
+
+ protected function getLogFiles($domain)
+ {
+ $logfiles_text = '';
+
+ $speciallogfile = '';
+ if ($domain['speciallogfile'] == '1') {
+ if ($domain['parentdomainid'] == '0') {
+ $speciallogfile = '-' . $domain['domain'];
+ } else {
+ $speciallogfile = '-' . $domain['parentdomain'];
+ }
+ }
+
+ // The normal access/error - logging is enabled
+ // error log cannot be set conditionally see
+ // https://redmine.lighttpd.net/issues/665
+ if ($domain['writeaccesslog']) {
+ $access_log = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.logfiles_directory') . $domain['loginname'] . $speciallogfile . '-access.log');
+ // Create the logfile if it does not exist (fixes #46)
+ touch($access_log);
+ chown($access_log, Settings::Get('system.httpuser'));
+ chgrp($access_log, Settings::Get('system.httpgroup'));
+
+ $logfiles_text .= ' accesslog.filename = "' . $access_log . '"' . "\n";
+ }
+
+ if (Settings::Get('system.awstats_enabled') == '1') {
+
+ if ((int) $domain['parentdomainid'] == 0) {
+ // prepare the aliases and subdomains for stats config files
+ $server_alias = '';
+ $alias_domains_stmt = Database::prepare("
+ SELECT `domain`, `iswildcarddomain`, `wwwserveralias`
+ FROM `" . TABLE_PANEL_DOMAINS . "`
+ WHERE `aliasdomain` = :domainid OR `parentdomainid` = :domainid
+ ");
+ Database::pexecute($alias_domains_stmt, array(
+ 'domainid' => $domain['id']
+ ));
+
+ while (($alias_domain = $alias_domains_stmt->fetch(\PDO::FETCH_ASSOC)) !== false) {
+
+ $server_alias .= ' ' . $alias_domain['domain'] . ' ';
+
+ if ($alias_domain['iswildcarddomain'] == '1') {
+ $server_alias .= '*.' . $domain['domain'];
+ } else {
+ if ($alias_domain['wwwserveralias'] == '1') {
+ $server_alias .= 'www.' . $alias_domain['domain'];
+ } else {
+ $server_alias .= '';
+ }
+ }
+ }
+
+ if ($domain['iswildcarddomain'] == '1') {
+ $alias = '*.' . $domain['domain'];
+ } else {
+ if ($domain['wwwserveralias'] == '1') {
+ $alias = 'www.' . $domain['domain'];
+ } else {
+ $alias = '';
+ }
+ }
+
+ // After inserting the AWStats information,
+ // be sure to build the awstats conf file as well
+ // and chown it using $awstats_params, #258
+ // Bug 960 + Bug 970 : Use full $domain instead of custom $awstats_params as following classes depend on the informations
+ \Froxlor\Http\Statistics::createAWStatsConf(Settings::Get('system.logfiles_directory') . $domain['loginname'] . $speciallogfile . '-access.log', $domain['domain'], $alias . $server_alias, $domain['customerroot'], $domain);
+ }
+ }
+
+ return $logfiles_text;
+ }
+
+ protected function createPathOptions($domain)
+ {
+ $result_stmt = Database::prepare("
+ SELECT * FROM " . TABLE_PANEL_HTACCESS . "
+ WHERE `path` LIKE :docroot
+ ");
+ Database::pexecute($result_stmt, array(
+ 'docroot' => $domain['documentroot'] . '%'
+ ));
+
+ $path_options = '';
+ $error_string = '';
+
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ if (! empty($row['error404path'])) {
+ $defhandler = $row['error404path'];
+ if (! \Froxlor\Validate\Validate::validateUrl($defhandler)) {
+ $defhandler = \Froxlor\FileDir::makeCorrectFile($domain['documentroot'] . '/' . $defhandler);
+ }
+ $error_string .= ' server.error-handler-404 = "' . $defhandler . '"' . "\n\n";
+ }
+
+ if ($row['options_indexes'] != '0') {
+ if (! empty($error_string)) {
+ $path_options .= $error_string;
+ // reset $error_string here to prevent duplicate entries
+ $error_string = '';
+ }
+
+ $path = \Froxlor\FileDir::makeCorrectDir(substr($row['path'], strlen($domain['documentroot']) - 1));
+ \Froxlor\FileDir::mkDirWithCorrectOwnership($domain['documentroot'], $row['path'], $domain['guid'], $domain['guid']);
+
+ // We need to remove the last slash, otherwise the regex wouldn't work
+ if ($row['path'] != $domain['documentroot']) {
+ $path = substr($path, 0, - 1);
+ }
+ $path_options .= ' $HTTP["url"] =~ "^' . $path . '($|/)" {' . "\n";
+ $path_options .= "\t" . 'dir-listing.activate = "enable"' . "\n";
+ $path_options .= ' }' . "\n\n";
+ } else {
+ $path_options = $error_string;
+ }
+
+ if (\Froxlor\Customer\Customer::customerHasPerlEnabled($domain['customerid']) && $row['options_cgi'] != '0') {
+ $path = \Froxlor\FileDir::makeCorrectDir(substr($row['path'], strlen($domain['documentroot']) - 1));
+ \Froxlor\FileDir::mkDirWithCorrectOwnership($domain['documentroot'], $row['path'], $domain['guid'], $domain['guid']);
+
+ // We need to remove the last slash, otherwise the regex wouldn't work
+ if ($row['path'] != $domain['documentroot']) {
+ $path = substr($path, 0, - 1);
+ }
+ $path_options .= ' $HTTP["url"] =~ "^' . $path . '($|/)" {' . "\n";
+ $path_options .= "\t" . 'cgi.assign = (' . "\n";
+ $path_options .= "\t\t" . '".pl" => "' . \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.perl_path')) . '",' . "\n";
+ $path_options .= "\t\t" . '".cgi" => "' . \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.perl_path')) . '"' . "\n";
+ $path_options .= "\t" . ')' . "\n";
+ $path_options .= ' }' . "\n\n";
+ }
+ }
+
+ return $path_options;
+ }
+
+ protected function getDirOptions($domain)
+ {
+ $result_stmt = Database::prepare("
+ SELECT * FROM " . TABLE_PANEL_HTPASSWDS . "
+ WHERE `customerid` = :customerid
+ ");
+ Database::pexecute($result_stmt, array(
+ 'customerid' => $domain['customerid']
+ ));
+
+ while ($row_htpasswds = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if ($this->auth_backend_loaded[$domain['ipandport']] != 'yes' && $this->auth_backend_loaded[$domain['ssl_ipandport']] != 'yes') {
+ $filename = $domain['customerid'] . '.htpasswd';
+
+ if ($this->auth_backend_loaded[$domain['ipandport']] != 'yes') {
+ $this->auth_backend_loaded[$domain['ipandport']] = 'yes';
+ $diroption_text .= 'auth.backend = "htpasswd"' . "\n";
+ $diroption_text .= 'auth.backend.htpasswd.userfile = "' . \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.apacheconf_htpasswddir') . '/' . $filename) . '"' . "\n";
+ $this->needed_htpasswds[$filename] = $row_htpasswds['username'] . ':' . $row_htpasswds['password'] . "\n";
+ $diroption_text .= 'auth.require = ( ' . "\n";
+ } elseif ($this->auth_backend_loaded[$domain['ssl_ipandport']] != 'yes') {
+ $this->auth_backend_loaded[$domain['ssl_ipandport']] = 'yes';
+ $diroption_text .= 'auth.backend= "htpasswd"' . "\n";
+ $diroption_text .= 'auth.backend.htpasswd.userfile = "' . \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.apacheconf_htpasswddir') . '/' . $filename) . '"' . "\n";
+ $this->needed_htpasswds[$filename] = $row_htpasswds['username'] . ':' . $row_htpasswds['password'] . "\n";
+ $diroption_text .= 'auth.require = ( ' . "\n";
+ }
+ }
+
+ $diroption_text .= '"' . \Froxlor\FileDir::makeCorrectDir($row_htpasswds['path']) . '" =>' . "\n";
+ $diroption_text .= '(' . "\n";
+ $diroption_text .= ' "method" => "basic",' . "\n";
+ $diroption_text .= ' "realm" => "' . $row_htpasswds['authname'] . '",' . "\n";
+ $diroption_text .= ' "require" => "valid-user"' . "\n";
+ $diroption_text .= ')' . "\n";
+
+ if ($this->auth_backend_loaded[$domain['ssl_ipandport']] == 'yes') {
+ $this->needed_htpasswds[$domain['ssl_ipandport']] .= $diroption_text;
+ }
+
+ if ($this->auth_backend_loaded[$domain['ipandport']] != 'yes') {
+ $this->needed_htpasswds[$domain['ipandport']] .= $diroption_text;
+ }
+ }
+
+ return ' auth.backend.htpasswd.userfile = "' . \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.apacheconf_htpasswddir') . '/' . $filename) . '"' . "\n";
+ }
+
+ protected function getServerNames($domain)
+ {
+ $server_string = array();
+ $domain_name = str_replace('.', '\.', $domain['domain']);
+
+ if ($domain['iswildcarddomain'] == '1') {
+ $server_string[] = '(?:^|\.)' . $domain_name . '$';
+ } else {
+ if ($domain['wwwserveralias'] == '1') {
+ $server_string[] = '^(?:www\.|)' . $domain_name . '$';
+ } else {
+ $server_string[] = '^' . $domain_name . '$';
+ }
+ }
+
+ $alias_domains_stmt = Database::prepare("
+ SELECT `domain`, `iswildcarddomain`, `wwwserveralias`
+ FROM `" . TABLE_PANEL_DOMAINS . "`
+ WHERE `aliasdomain` = :domainid
+ ");
+ Database::pexecute($alias_domains_stmt, array(
+ 'domainid' => $domain['id']
+ ));
+
+ while (($alias_domain = $alias_domains_stmt->fetch(\PDO::FETCH_ASSOC)) !== false) {
+ $alias_domain_name = str_replace('.', '\.', $alias_domain['domain']);
+
+ if ($alias_domain['iswildcarddomain'] == '1') {
+ $server_string[] = '(?:^|\.)' . $alias_domain_name . '$';
+ } else {
+ if ($alias_domain['wwwserveralias'] == '1') {
+ $server_string[] = '^(?:www\.|)' . $alias_domain_name . '$';
+ } else {
+ $server_string[] = '^' . $alias_domain_name . '$';
+ }
+ }
+ }
+
+ for ($i = 0; $i < sizeof($server_string); $i ++) {
+ $data = $server_string[$i];
+
+ if (sizeof($server_string) > 1) {
+ if ($i == 0) {
+ $servernames_text = '(' . $data . '|';
+ } elseif (sizeof($server_string) - 1 == $i) {
+ $servernames_text .= $data . ')';
+ } else {
+ $servernames_text .= $data . '|';
+ }
+ } else {
+ $servernames_text = $data;
+ }
+ }
+
+ unset($data);
+
+ if ($servernames_text != '') {
+ $servernames_text = '$HTTP["host"] =~ "' . $servernames_text . '"';
+ } else {
+ $servernames_text = '$HTTP["host"] == "' . $domain['domain'] . '"';
+ }
+
+ return $servernames_text;
+ }
+
+ protected function getWebroot($domain, $ssl)
+ {
+ $webroot_text = '';
+
+ if ($domain['deactivated'] == '1' && Settings::Get('system.deactivateddocroot') != '') {
+ $webroot_text .= ' # Using docroot for deactivated users...' . "\n";
+ $webroot_text .= ' server.document-root = "' . \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.deactivateddocroot')) . "\"\n";
+ $this->deactivated = true;
+ } else {
+ if ($ssl === false && $domain['ssl_redirect'] == '1') {
+ $redirect_domain = $this->idnaConvert->encode('https://' . $domain['domain']);
+ $webroot_text .= ' url.redirect = (' . "\n";
+ $webroot_text .= "\t" . '"^/(.*)" => "' . $redirect_domain . '/$1",' . "\n";
+ $webroot_text .= "\t" . '"" => "' . $redirect_domain . '",' . "\n";
+ $webroot_text .= "\t" . '"/" => "' . $redirect_domain . '"' . "\n";
+ $webroot_text .= ' )' . "\n";
+ } elseif (preg_match("#^https?://#i", $domain['documentroot'])) {
+ $redirect_domain = $this->idnaConvert->encode($domain['documentroot']);
+ $webroot_text .= ' url.redirect = (' . "\n";
+ $webroot_text .= "\t" . '"^/(.*)" => "' . $redirect_domain . '/$1",' . "\n";
+ $webroot_text .= "\t" . '"" => "' . $redirect_domain . '",' . "\n";
+ $webroot_text .= "\t" . '"/" => "' . $redirect_domain . '"' . "\n";
+ $webroot_text .= ' )' . "\n";
+ } else {
+ $webroot_text .= ' server.document-root = "' . \Froxlor\FileDir::makeCorrectDir($domain['documentroot']) . "\"\n";
+ }
+ $this->deactivated = false;
+ }
+
+ return $webroot_text;
+ }
+
+ /**
+ * Lets set the text part for the stats software
+ */
+ protected function getStats($domain)
+ {
+ $stats_text = '';
+
+ if ($domain['speciallogfile'] == '1') {
+ if ($domain['parentdomainid'] == '0') {
+ if (Settings::Get('system.awstats_enabled') == '1') {
+ $stats_text .= ' alias.url = ( "/awstats/" => "' . \Froxlor\FileDir::makeCorrectFile($domain['customerroot'] . '/awstats/' . $domain['domain']) . '" )' . "\n";
+ $stats_text .= ' alias.url += ( "/awstats-icon" => "' . \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.awstats_icons')) . '" )' . "\n";
+ } else {
+ $stats_text .= ' alias.url = ( "/webalizer/" => "' . \Froxlor\FileDir::makeCorrectFile($domain['customerroot'] . '/webalizer/' . $domain['domain']) . '/" )' . "\n";
+ }
+ } else {
+ if (Settings::Get('system.awstats_enabled') == '1') {
+ $stats_text .= ' alias.url = ( "/awstats/" => "' . \Froxlor\FileDir::makeCorrectFile($domain['customerroot'] . '/awstats/' . $domain['parentdomain']) . '" )' . "\n";
+ $stats_text .= ' alias.url += ( "/awstats-icon" => "' . \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.awstats_icons')) . '" )' . "\n";
+ } else {
+ $stats_text .= ' alias.url = ( "/webalizer/" => "' . \Froxlor\FileDir::makeCorrectFile($domain['customerroot'] . '/webalizer/' . $domain['parentdomain']) . '/" )' . "\n";
+ }
+ }
+ } else {
+ if ($domain['customerroot'] != $domain['documentroot']) {
+ if (Settings::Get('system.awstats_enabled') == '1') {
+ $stats_text .= ' alias.url = ( "/awstats/" => "' . \Froxlor\FileDir::makeCorrectFile($domain['customerroot'] . '/awstats/' . $domain['domain']) . '" )' . "\n";
+ $stats_text .= ' alias.url += ( "/awstats-icon" => "' . \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.awstats_icons')) . '" )' . "\n";
+ } else {
+ $stats_text .= ' alias.url = ( "/webalizer/" => "' . \Froxlor\FileDir::makeCorrectFile($domain['customerroot'] . '/webalizer/') . '" )' . "\n";
+ }
+ } elseif (Settings::Get('system.awstats_enabled') == '1') {
+ // if the docroots are equal, we still have to set an alias for awstats
+ // because the stats are in /awstats/[domain], not just /awstats/
+ // also, the awstats-icons are someplace else too!
+ // -> webalizer does not need this!
+ $stats_text .= ' alias.url = ( "/awstats/" => "' . \Froxlor\FileDir::makeCorrectFile($domain['documentroot'] . '/awstats/' . $domain['domain']) . '" )' . "\n";
+ $stats_text .= ' alias.url += ( "/awstats-icon" => "' . \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.awstats_icons')) . '" )' . "\n";
+ }
+ }
+
+ return $stats_text;
+ }
+
+ public function writeConfigs()
+ {
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "lighttpd::writeConfigs: rebuilding " . Settings::Get('system.apacheconf_vhost'));
+
+ $vhostDir = new \Froxlor\Http\Directory(Settings::Get('system.apacheconf_vhost'));
+ if (! $vhostDir->isConfigDir()) {
+ // Save one big file
+ $vhosts_file = '';
+
+ // sort by filename so the order is:
+ // 1. main-domains
+ // 2. subdomains as main-domains
+ // 3. subdomains
+ // (former #437) - #833 (the numbering is done in createLighttpdHosts())
+ ksort($this->lighttpd_data);
+
+ foreach ($this->lighttpd_data as $vhosts_filename => $vhost_content) {
+ $vhosts_file .= $vhost_content . "\n\n";
+ }
+
+ $vhosts_filename = Settings::Get('system.apacheconf_vhost');
+
+ // Apply header
+ $vhosts_file = '# ' . basename($vhosts_filename) . "\n" . '# Created ' . date('d.m.Y H:i') . "\n" . '# Do NOT manually edit this file, all changes will be deleted after the next domain change at the panel.' . "\n" . "\n" . $vhosts_file;
+ $vhosts_file_handler = fopen($vhosts_filename, 'w');
+ fwrite($vhosts_file_handler, $vhosts_file);
+ fclose($vhosts_file_handler);
+ } else {
+ if (! file_exists(Settings::Get('system.apacheconf_vhost'))) {
+ $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'lighttpd::writeConfigs: mkdir ' . escapeshellarg(\Froxlor\FileDir::makeCorrectDir(Settings::Get('system.apacheconf_vhost'))));
+ \Froxlor\FileDir::safe_exec('mkdir ' . escapeshellarg(\Froxlor\FileDir::makeCorrectDir(Settings::Get('system.apacheconf_vhost'))));
+ }
+
+ // Write a single file for every vhost
+ foreach ($this->lighttpd_data as $vhosts_filename => $vhosts_file) {
+ $this->known_filenames[] = basename($vhosts_filename);
+
+ // Apply header
+ $vhosts_file = '# ' . basename($vhosts_filename) . "\n" . '# Created ' . date('d.m.Y H:i') . "\n" . '# Do NOT manually edit this file, all changes will be deleted after the next domain change at the panel.' . "\n" . "\n" . $vhosts_file;
+
+ if (! empty($vhosts_filename)) {
+ $vhosts_file_handler = fopen($vhosts_filename, 'w');
+ fwrite($vhosts_file_handler, $vhosts_file);
+ fclose($vhosts_file_handler);
+ }
+ }
+ }
+
+ // Write the diroptions
+ $htpasswdDir = new \Froxlor\Http\Directory(Settings::Get('system.apacheconf_htpasswddir'));
+ if ($htpasswdDir->isConfigDir()) {
+ foreach ($this->needed_htpasswds as $key => $data) {
+ if (! is_dir(Settings::Get('system.apacheconf_htpasswddir'))) {
+ mkdir(\Froxlor\FileDir::makeCorrectDir(Settings::Get('system.apacheconf_htpasswddir')));
+ }
+
+ $filename = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.apacheconf_htpasswddir') . '/' . $key);
+ $htpasswd_handler = fopen($filename, 'w');
+ fwrite($htpasswd_handler, $data);
+ fclose($htpasswd_handler);
+ }
+ }
+ }
+}
diff --git a/lib/Froxlor/Cron/Http/LighttpdFcgi.php b/lib/Froxlor/Cron/Http/LighttpdFcgi.php
new file mode 100644
index 00000000..03dc0689
--- /dev/null
+++ b/lib/Froxlor/Cron/Http/LighttpdFcgi.php
@@ -0,0 +1,151 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ */
+class LighttpdFcgi extends Lighttpd
+{
+
+ protected function composePhpOptions($domain)
+ {
+ $php_options_text = '';
+
+ if ($domain['phpenabled_customer'] == 1 && $domain['phpenabled_vhost'] == '1') {
+ $php = new PhpInterface($domain);
+ $phpconfig = $php->getPhpConfig((int) $domain['phpsettingid']);
+
+ // vhost data for php-fpm
+ if ((int) Settings::Get('phpfpm.enabled') == 1) {
+ $php_options_text = ' fastcgi.server = ( ' . "\n";
+ $php_options_text .= "\t" . '".php" => (' . "\n";
+ $php_options_text .= "\t\t" . '"localhost" => (' . "\n";
+ $php_options_text .= "\t\t" . '"socket" => "' . $php->getInterface()->getSocketFile() . '",' . "\n";
+ $php_options_text .= "\t\t" . '"check-local" => "enable",' . "\n";
+ $php_options_text .= "\t\t" . '"disable-time" => 1' . "\n";
+ $php_options_text .= "\t" . ')' . "\n";
+ $php_options_text .= "\t" . ')' . "\n";
+ $php_options_text .= ' )' . "\n";
+ } elseif ((int) Settings::Get('system.mod_fcgid') == 1) {
+ // vhost data for fcgid
+ $php_options_text = ' fastcgi.server = ( ' . "\n";
+ $file_extensions = explode(' ', $phpconfig['file_extensions']);
+ foreach ($file_extensions as $f_extension) {
+ $php_options_text .= "\t" . '".' . $f_extension . '" => (' . "\n";
+ $php_options_text .= "\t\t" . '"localhost" => (' . "\n";
+ $php_options_text .= "\t\t" . '"socket" => "/var/run/lighttpd/' . $domain['loginname'] . '-' . $domain['domain'] . '-php.socket",' . "\n";
+ $php_options_text .= "\t\t" . '"bin-path" => "' . $phpconfig['binary'] . ' -c ' . $php->getInterface()->getIniFile() . '",' . "\n";
+ $php_options_text .= "\t\t" . '"bin-environment" => (' . "\n";
+ if ((int) $domain['mod_fcgid_starter'] != - 1) {
+ $php_options_text .= "\t\t\t" . '"PHP_FCGI_CHILDREN" => "' . (int) $domain['mod_fcgid_starter'] . '",' . "\n";
+ } else {
+ if ((int) $phpconfig['mod_fcgid_starter'] != - 1) {
+ $php_options_text .= "\t\t\t" . '"PHP_FCGI_CHILDREN" => "' . (int) $phpconfig['mod_fcgid_starter'] . '",' . "\n";
+ } else {
+ $php_options_text .= "\t\t\t" . '"PHP_FCGI_CHILDREN" => "' . (int) Settings::Get('system.mod_fcgid_starter') . '",' . "\n";
+ }
+ }
+
+ if ((int) $domain['mod_fcgid_maxrequests'] != - 1) {
+ $php_options_text .= "\t\t\t" . '"PHP_FCGI_MAX_REQUESTS" => "' . (int) $domain['mod_fcgid_maxrequests'] . '"' . "\n";
+ } else {
+ if ((int) $phpconfig['mod_fcgid_maxrequests'] != - 1) {
+ $php_options_text .= "\t\t\t" . '"PHP_FCGI_MAX_REQUESTS" => "' . (int) $phpconfig['mod_fcgid_maxrequests'] . '"' . "\n";
+ } else {
+ $php_options_text .= "\t\t\t" . '"PHP_FCGI_MAX_REQUESTS" => "' . (int) Settings::Get('system.mod_fcgid_maxrequests') . '"' . "\n";
+ }
+ }
+
+ $php_options_text .= "\t\t" . ')' . "\n";
+ $php_options_text .= "\t" . ')' . "\n";
+ $php_options_text .= "\t" . ')' . "\n";
+ } // foreach extension
+ $php_options_text .= ' )' . "\n";
+ }
+
+ // create starter-file | config-file
+ $php->getInterface()->createConfig($phpconfig);
+
+ // create php.ini (fpm does nothing here, as it
+ // defines ini-settings in its pool config)
+ $php->getInterface()->createIniFile($phpconfig);
+ } else {
+ $php_options_text .= ' # PHP is disabled for this vHost' . "\n";
+ }
+
+ return $php_options_text;
+ }
+
+ public function createOwnVhostStarter()
+ {
+ if (Settings::Get('phpfpm.enabled') == '1' && Settings::Get('phpfpm.enabled_ownvhost') == '1') {
+ $mypath = \Froxlor\FileDir::makeCorrectDir(dirname(dirname(dirname(__FILE__)))); // /var/www/froxlor, needed for chown
+
+ $user = Settings::Get('phpfpm.vhost_httpuser');
+ $group = Settings::Get('phpfpm.vhost_httpgroup');
+
+ // get fpm config
+ $fpm_sel_stmt = Database::prepare("
+ SELECT f.id FROM `" . TABLE_PANEL_FPMDAEMONS . "` f
+ LEFT JOIN `" . TABLE_PANEL_PHPCONFIGS . "` p ON p.fpmsettingid = f.id
+ WHERE p.id = :phpconfigid
+ ");
+ $fpm_config = Database::pexecute_first($fpm_sel_stmt, array(
+ 'phpconfigid' => Settings::Get('phpfpm.vhost_defaultini')
+ ));
+
+ $domain = array(
+ 'id' => 'none',
+ 'domain' => Settings::Get('system.hostname'),
+ 'adminid' => 1, /* first admin-user (superadmin) */
+ 'mod_fcgid_starter' => - 1,
+ 'mod_fcgid_maxrequests' => - 1,
+ 'guid' => $user,
+ 'openbasedir' => 0,
+ 'email' => Settings::Get('panel.adminmail'),
+ 'loginname' => 'froxlor.panel',
+ 'documentroot' => $mypath,
+ 'customerroot' => $mypath,
+ 'fpm_config_id' => isset($fpm_config['id']) ? $fpm_config['id'] : 1
+ );
+
+ // all the files and folders have to belong to the local user
+ // now because we also use fcgid for our own vhost
+ \Froxlor\FileDir::safe_exec('chown -R ' . $user . ':' . $group . ' ' . escapeshellarg($mypath));
+
+ // get php.ini for our own vhost
+ $php = new PhpInterface($domain);
+
+ // get php-config
+ if (Settings::Get('phpfpm.enabled') == '1') {
+ // fpm
+ $phpconfig = $php->getPhpConfig(Settings::Get('phpfpm.vhost_defaultini'));
+ } else {
+ // fcgid
+ $phpconfig = $php->getPhpConfig(Settings::Get('system.mod_fcgid_defaultini_ownvhost'));
+ }
+
+ // create starter-file | config-file
+ $php->getInterface()->createConfig($phpconfig);
+
+ // create php.ini (fpm does nothing here, as it
+ // defines ini-settings in its pool config)
+ $php->getInterface()->createIniFile($phpconfig);
+ }
+ }
+}
diff --git a/lib/Froxlor/Cron/Http/Nginx.php b/lib/Froxlor/Cron/Http/Nginx.php
new file mode 100644
index 00000000..4c49628a
--- /dev/null
+++ b/lib/Froxlor/Cron/Http/Nginx.php
@@ -0,0 +1,1277 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ */
+class Nginx extends HttpConfigBase
+{
+
+ private $nginx_server = array();
+
+ // protected
+ protected $nginx_data = array();
+
+ protected $needed_htpasswds = array();
+
+ protected $auth_backend_loaded = false;
+
+ protected $htpasswds_data = array();
+
+ protected $known_htpasswdsfilenames = array();
+
+ protected $mod_accesslog_loaded = '0';
+
+ protected $vhost_root_autoindex = false;
+
+ protected $known_vhostfilenames = array();
+
+ /**
+ * indicator whether a customer is deactivated or not
+ * if yes, only the webroot will be generated
+ *
+ * @var bool
+ */
+ private $deactivated = false;
+
+ public function __construct($nginx_server = array())
+ {
+ $this->nginx_server = $nginx_server;
+ }
+
+ private function createLogformatEntry()
+ {
+ if (Settings::Get('system.logfiles_format') != '') {
+ $vhosts_folder = '';
+ if (is_dir(Settings::Get('system.apacheconf_vhost'))) {
+ $vhosts_folder = \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.apacheconf_vhost'));
+ } else {
+ $vhosts_folder = \Froxlor\FileDir::makeCorrectDir(dirname(Settings::Get('system.apacheconf_vhost')));
+ }
+
+ $vhosts_filename = \Froxlor\FileDir::makeCorrectFile($vhosts_folder . '/02_froxlor_logfiles_format.conf');
+
+ if (! isset($this->nginx_data[$vhosts_filename])) {
+ $this->nginx_data[$vhosts_filename] = '';
+ }
+
+ $logtype = 'frx_custom';
+ $this->nginx_data[$vhosts_filename] = 'log_format ' . $logtype . ' ' . Settings::Get('system.logfiles_format') . ';' . "\n";
+ }
+ }
+
+ /**
+ * define a default ErrorDocument-statement, bug #unknown-yet
+ */
+ private function createStandardErrorHandler()
+ {
+ if (Settings::Get('defaultwebsrverrhandler.enabled') == '1' && (Settings::Get('defaultwebsrverrhandler.err401') != '' || Settings::Get('defaultwebsrverrhandler.err403') != '' || Settings::Get('defaultwebsrverrhandler.err404') != '' || Settings::Get('defaultwebsrverrhandler.err500') != '')) {
+ $vhosts_folder = '';
+ if (is_dir(Settings::Get('system.apacheconf_vhost'))) {
+ $vhosts_folder = \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.apacheconf_vhost'));
+ } else {
+ $vhosts_folder = \Froxlor\FileDir::makeCorrectDir(dirname(Settings::Get('system.apacheconf_vhost')));
+ }
+
+ $vhosts_filename = \Froxlor\FileDir::makeCorrectFile($vhosts_folder . '/05_froxlor_default_errorhandler.conf');
+
+ if (! isset($this->nginx_data[$vhosts_filename])) {
+ $this->nginx_data[$vhosts_filename] = '';
+ }
+
+ $statusCodes = array(
+ '401',
+ '403',
+ '404',
+ '500'
+ );
+ foreach ($statusCodes as $statusCode) {
+ if (Settings::Get('defaultwebsrverrhandler.err' . $statusCode) != '') {
+ $defhandler = Settings::Get('defaultwebsrverrhandler.err' . $statusCode);
+ if (! \Froxlor\Validate\Validate::validateUrl($defhandler)) {
+ $defhandler = \Froxlor\FileDir::makeCorrectFile($defhandler);
+ }
+ $this->nginx_data[$vhosts_filename] .= 'error_page ' . $statusCode . ' ' . $defhandler . ';' . "\n";
+ }
+ }
+ }
+ }
+
+ public function createVirtualHosts()
+ {
+ return;
+ }
+
+ public function createFileDirOptions()
+ {
+ return;
+ }
+
+ public function createIpPort()
+ {
+ $this->createLogformatEntry();
+
+ $result_ipsandports_stmt = Database::query("
+ SELECT * FROM `" . TABLE_PANEL_IPSANDPORTS . "` ORDER BY `ip` ASC, `port` ASC
+ ");
+
+ while ($row_ipsandports = $result_ipsandports_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if (filter_var($row_ipsandports['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
+ $ip = '[' . $row_ipsandports['ip'] . ']';
+ } else {
+ $ip = $row_ipsandports['ip'];
+ }
+ $port = $row_ipsandports['port'];
+
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'nginx::createIpPort: creating ip/port settings for ' . $ip . ":" . $port);
+ $vhost_filename = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.apacheconf_vhost') . '/10_froxlor_ipandport_' . trim(str_replace(':', '.', $row_ipsandports['ip']), '.') . '.' . $row_ipsandports['port'] . '.conf');
+
+ if (! isset($this->nginx_data[$vhost_filename])) {
+ $this->nginx_data[$vhost_filename] = '';
+ }
+
+ if ($row_ipsandports['vhostcontainer'] == '1') {
+
+ $this->nginx_data[$vhost_filename] .= 'server { ' . "\n";
+
+ $mypath = $this->getMyPath($row_ipsandports);
+
+ // check for ssl before anything else so
+ // we know whether it's an ssl vhost or not
+ $ssl_vhost = false;
+ if ($row_ipsandports['ssl'] == '1') {
+ // check for required fallback
+ if (($row_ipsandports['ssl_cert_file'] == '' || ! file_exists($row_ipsandports['ssl_cert_file'])) && (Settings::Get('system.le_froxlor_enabled') == '0' || $this->froxlorVhostHasLetsEncryptCert() == false)) {
+ $row_ipsandports['ssl_cert_file'] = Settings::Get('system.ssl_cert_file');
+ if (! file_exists($row_ipsandports['ssl_cert_file'])) {
+ // explicitly disable ssl for this vhost
+ $row_ipsandports['ssl_cert_file'] = "";
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'System certificate file "' . Settings::Get('system.ssl_cert_file') . '" does not seem to exist. Disabling SSL-vhost for "' . Settings::Get('system.hostname') . '"');
+ }
+ }
+ if ($row_ipsandports['ssl_key_file'] == '') {
+ $row_ipsandports['ssl_key_file'] = Settings::Get('system.ssl_key_file');
+ if (! file_exists($row_ipsandports['ssl_key_file'])) {
+ // explicitly disable ssl for this vhost
+ $row_ipsandports['ssl_cert_file'] = "";
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'System certificate key-file "' . Settings::Get('system.ssl_key_file') . '" does not seem to exist. Disabling SSL-vhost for "' . Settings::Get('system.hostname') . '"');
+ }
+ }
+ if ($row_ipsandports['ssl_ca_file'] == '') {
+ $row_ipsandports['ssl_ca_file'] = Settings::Get('system.ssl_ca_file');
+ }
+ if ($row_ipsandports['ssl_cert_file'] != '' && file_exists($row_ipsandports['ssl_cert_file'])) {
+ $ssl_vhost = true;
+ }
+
+ $domain = array(
+ 'id' => 0,
+ 'domain' => Settings::Get('system.hostname'),
+ 'adminid' => 1, /* first admin-user (superadmin) */
+ 'loginname' => 'froxlor.panel',
+ 'documentroot' => $mypath,
+ 'customerroot' => $mypath,
+ 'parentdomainid' => 0
+ );
+
+ // override corresponding array values
+ $domain['ssl_cert_file'] = $row_ipsandports['ssl_cert_file'];
+ $domain['ssl_key_file'] = $row_ipsandports['ssl_key_file'];
+ $domain['ssl_ca_file'] = $row_ipsandports['ssl_ca_file'];
+ $domain['ssl_cert_chainfile'] = $row_ipsandports['ssl_cert_chainfile'];
+
+ // SSL STUFF
+ $dssl = new DomainSSL();
+ // this sets the ssl-related array-indices in the $domain array
+ // if the domain has customer-defined ssl-certificates
+ $dssl->setDomainSSLFilesArray($domain);
+
+ if ($domain['ssl_cert_file'] != '' && file_exists($domain['ssl_cert_file'])) {
+ // override corresponding array values
+ $row_ipsandports['ssl_cert_file'] = $domain['ssl_cert_file'];
+ $row_ipsandports['ssl_key_file'] = $domain['ssl_key_file'];
+ $row_ipsandports['ssl_ca_file'] = $domain['ssl_ca_file'];
+ $row_ipsandports['ssl_cert_chainfile'] = $domain['ssl_cert_chainfile'];
+ $ssl_vhost = true;
+ }
+ }
+
+ $http2 = $ssl_vhost == true && Settings::Get('system.http2_support') == '1';
+
+ /**
+ * this HAS to be set for the default host in nginx or else no vhost will work
+ */
+ $this->nginx_data[$vhost_filename] .= "\t" . 'listen ' . $ip . ':' . $port . ' default_server' . ($ssl_vhost == true ? ' ssl' : '') . ($http2 == true ? ' http2' : '') . ';' . "\n";
+
+ $this->nginx_data[$vhost_filename] .= "\t" . '# Froxlor default vhost' . "\n";
+
+ $aliases = "";
+ $froxlor_aliases = Settings::Get('system.froxloraliases');
+ if (! empty($froxlor_aliases)) {
+ $froxlor_aliases = explode(",", $froxlor_aliases);
+ foreach ($froxlor_aliases as $falias) {
+ if (\Froxlor\Validate\Validate::validateDomain(trim($falias))) {
+ $aliases .= trim($falias) . " ";
+ }
+ }
+ $aliases = " " . trim($aliases);
+ }
+ $this->nginx_data[$vhost_filename] .= "\t" . 'server_name ' . Settings::Get('system.hostname') . $aliases . ';' . "\n";
+
+ $logtype = 'combined';
+ if (Settings::Get('system.logfiles_format') != '') {
+ $logtype = 'frx_custom';
+ }
+ $this->nginx_data[$vhost_filename] .= "\t" . 'access_log /var/log/nginx/access.log ' . $logtype . ';' . "\n";
+
+ if (Settings::Get('system.use_ssl') == '1' && Settings::Get('system.leenabled') == '1' && Settings::Get('system.le_froxlor_enabled') == '1') {
+ $acmeConfFilename = Settings::Get('system.letsencryptacmeconf');
+ $this->nginx_data[$vhost_filename] .= "\t" . 'include ' . $acmeConfFilename . ';' . "\n";
+ }
+
+ $is_redirect = false;
+ // check for SSL redirect
+ if ($row_ipsandports['ssl'] == '0' && Settings::Get('system.le_froxlor_redirect') == '1') {
+ $is_redirect = true;
+ // check whether froxlor uses Let's Encrypt and not cert is being generated yet
+ // or a renew is ongoing - disable redirect
+ if (Settings::Get('system.le_froxlor_enabled') && ($this->froxlorVhostHasLetsEncryptCert() == false || $this->froxlorVhostLetsEncryptNeedsRenew())) {
+ $this->nginx_data[$vhost_filename] .= '# temp. disabled ssl-redirect due to Let\'s Encrypt certificate generation.' . PHP_EOL;
+ $is_redirect = false;
+ } else {
+ $_sslport = $this->checkAlternativeSslPort();
+ $mypath = 'https://' . Settings::Get('system.hostname') . $_sslport;
+ $this->nginx_data[$vhost_filename] .= "\t" . 'location / {' . "\n";
+ $this->nginx_data[$vhost_filename] .= "\t\t" . 'return 301 ' . $mypath . '$request_uri;' . "\n";
+ $this->nginx_data[$vhost_filename] .= "\t" . '}' . "\n";
+ }
+ }
+
+ if (! $is_redirect) {
+ $this->nginx_data[$vhost_filename] .= "\t" . 'root ' . $mypath . ';' . "\n";
+ $this->nginx_data[$vhost_filename] .= "\t" . 'index index.php index.html index.htm;' . "\n\n";
+ $this->nginx_data[$vhost_filename] .= "\t" . 'location / {' . "\n";
+ $this->nginx_data[$vhost_filename] .= "\t" . '}' . "\n";
+ }
+
+ if ($row_ipsandports['specialsettings'] != '' && ($row_ipsandports['ssl'] == '0' || ($row_ipsandports['ssl'] == '1' && Settings::Get('system.use_ssl') == '1' && $row_ipsandports['include_specialsettings'] == '1'))) {
+ $this->nginx_data[$vhost_filename] .= $this->processSpecialConfigTemplate($row_ipsandports['specialsettings'], array(
+ 'domain' => Settings::Get('system.hostname'),
+ 'loginname' => Settings::Get('phpfpm.vhost_httpuser'),
+ 'documentroot' => $mypath,
+ 'customerroot' => $mypath
+ ), $row_ipsandports['ip'], $row_ipsandports['port'], $row_ipsandports['ssl'] == '1') . "\n";
+ }
+
+ /**
+ * SSL config options
+ */
+ if ($row_ipsandports['ssl'] == '1') {
+ $row_ipsandports['domain'] = Settings::Get('system.hostname');
+ $row_ipsandports['ssl_honorcipherorder'] = Settings::Get('system.honorcipherorder');
+ $row_ipsandports['ssl_sessiontickets'] = Settings::Get('system.sessiontickets');
+ $this->nginx_data[$vhost_filename] .= $this->composeSslSettings($row_ipsandports);
+ if ($row_ipsandports['ssl_specialsettings'] != '') {
+ $this->nginx_data[$vhost_filename] .= $this->processSpecialConfigTemplate($row_ipsandports['ssl_specialsettings'], array(
+ 'domain' => Settings::Get('system.hostname'),
+ 'loginname' => Settings::Get('phpfpm.vhost_httpuser'),
+ 'documentroot' => $mypath,
+ 'customerroot' => $mypath
+ ), $row_ipsandports['ip'], $row_ipsandports['port'], $row_ipsandports['ssl'] == '1') . "\n";
+ }
+ }
+
+ if (! $is_redirect) {
+ $this->nginx_data[$vhost_filename] .= "\tlocation ~ \.php {\n";
+ $this->nginx_data[$vhost_filename] .= "\t\tfastcgi_split_path_info ^(.+?\.php)(/.*)$;\n";
+ $this->nginx_data[$vhost_filename] .= "\t\tinclude " . Settings::Get('nginx.fastcgiparams') . ";\n";
+ $this->nginx_data[$vhost_filename] .= "\t\tfastcgi_param SCRIPT_FILENAME \$request_filename;\n";
+ $this->nginx_data[$vhost_filename] .= "\t\tfastcgi_param PATH_INFO \$fastcgi_path_info;\n";
+ $this->nginx_data[$vhost_filename] .= "\t\ttry_files \$fastcgi_script_name =404;\n";
+
+ if ($row_ipsandports['ssl'] == '1') {
+ $this->nginx_data[$vhost_filename] .= "\t\tfastcgi_param HTTPS on;\n";
+ }
+
+ if ((int) Settings::Get('phpfpm.enabled') == 1 && (int) Settings::Get('phpfpm.enabled_ownvhost') == 1) {
+ // get fpm config
+ $fpm_sel_stmt = Database::prepare("
+ SELECT f.id FROM `" . TABLE_PANEL_FPMDAEMONS . "` f
+ LEFT JOIN `" . TABLE_PANEL_PHPCONFIGS . "` p ON p.fpmsettingid = f.id
+ WHERE p.id = :phpconfigid
+ ");
+ $fpm_config = Database::pexecute_first($fpm_sel_stmt, array(
+ 'phpconfigid' => Settings::Get('phpfpm.vhost_defaultini')
+ ));
+ $domain = array(
+ 'id' => 'none',
+ 'domain' => Settings::Get('system.hostname'),
+ 'adminid' => 1, /* first admin-user (superadmin) */
+ 'mod_fcgid_starter' => - 1,
+ 'mod_fcgid_maxrequests' => - 1,
+ 'guid' => Settings::Get('phpfpm.vhost_httpuser'),
+ 'openbasedir' => 0,
+ 'email' => Settings::Get('panel.adminmail'),
+ 'loginname' => 'froxlor.panel',
+ 'documentroot' => $mypath,
+ 'customerroot' => $mypath,
+ 'fpm_config_id' => isset($fpm_config['id']) ? $fpm_config['id'] : 1
+ );
+
+ $php = new PhpInterface($domain);
+ $this->nginx_data[$vhost_filename] .= "\t\tfastcgi_pass unix:" . $php->getInterface()->getSocketFile() . ";\n";
+ } else {
+ $this->nginx_data[$vhost_filename] .= "\t\tfastcgi_pass " . Settings::Get('system.nginx_php_backend') . ";\n";
+ }
+
+ $this->nginx_data[$vhost_filename] .= "\t\tfastcgi_index index.php;\n";
+ $this->nginx_data[$vhost_filename] .= "\t}\n";
+ }
+
+ $this->nginx_data[$vhost_filename] .= "}\n\n";
+ // End of Froxlor server{}-part
+ }
+ }
+
+ $this->createNginxHosts();
+
+ /**
+ * standard error pages
+ */
+ $this->createStandardErrorHandler();
+ }
+
+ /**
+ * create vhosts
+ */
+ protected function createNginxHosts()
+ {
+ $domains = WebserverBase::getVhostsToCreate();
+ foreach ($domains as $domain) {
+
+ if (is_dir(Settings::Get('system.apacheconf_vhost'))) {
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg(\Froxlor\FileDir::makeCorrectDir(Settings::Get('system.apacheconf_vhost'))));
+ }
+
+ $vhost_filename = $this->getVhostFilename($domain);
+
+ if (! isset($this->nginx_data[$vhost_filename])) {
+ $this->nginx_data[$vhost_filename] = '';
+ }
+
+ if ((empty($this->nginx_data[$vhost_filename]) && ! is_dir(Settings::Get('system.apacheconf_vhost'))) || is_dir(Settings::Get('system.apacheconf_vhost'))) {
+ $domain['nonexistinguri'] = '/' . md5(uniqid(microtime(), 1)) . '.htm';
+
+ // Create non-ssl host
+ $this->nginx_data[$vhost_filename] .= $this->getVhostContent($domain, false);
+ if ($domain['ssl'] == '1' || $domain['ssl_redirect'] == '1') {
+ $vhost_filename_ssl = $this->getVhostFilename($domain, true);
+ if (! isset($this->nginx_data[$vhost_filename_ssl])) {
+ $this->nginx_data[$vhost_filename_ssl] = '';
+ }
+ // Now enable ssl stuff
+ $this->nginx_data[$vhost_filename_ssl] .= $this->getVhostContent($domain, true);
+ }
+ }
+ }
+ }
+
+ protected function getVhostFilename($domain, $ssl_vhost = false)
+ {
+ if ((int) $domain['parentdomainid'] == 0 && \Froxlor\Domain\Domain::isCustomerStdSubdomain((int) $domain['id']) == false && ((int) $domain['ismainbutsubto'] == 0 || \Froxlor\Domain\Domain::domainMainToSubExists($domain['ismainbutsubto']) == false)) {
+ $vhost_no = '35';
+ } elseif ((int) $domain['parentdomainid'] == 0 && \Froxlor\Domain\Domain::isCustomerStdSubdomain((int) $domain['id']) == false && (int) $domain['ismainbutsubto'] > 0) {
+ $vhost_no = '30';
+ } else {
+ // number of dots in a domain specifies it's position (and depth of subdomain) starting at 29 going downwards on higher depth
+ $vhost_no = (string) (30 - substr_count($domain['domain'], ".") + 1);
+ }
+
+ if ($ssl_vhost === true) {
+ $vhost_filename = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.apacheconf_vhost') . '/' . $vhost_no . '_froxlor_ssl_vhost_' . $domain['domain'] . '.conf');
+ } else {
+ $vhost_filename = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.apacheconf_vhost') . '/' . $vhost_no . '_froxlor_normal_vhost_' . $domain['domain'] . '.conf');
+ }
+
+ return $vhost_filename;
+ }
+
+ protected function getVhostContent($domain, $ssl_vhost = false)
+ {
+ if ($ssl_vhost === true && $domain['ssl'] != '1' && $domain['ssl_redirect'] != '1') {
+ return '';
+ }
+
+ // check whether the customer is deactivated and NO docroot for deactivated users has been set#
+ $ddr = Settings::Get('system.deactivateddocroot');
+ if ($domain['deactivated'] == '1' && empty($ddr)) {
+ return '# Customer deactivated and a docroot for deactivated users hasn\'t been set.' . "\n";
+ }
+
+ $vhost_content = '';
+ $_vhost_content = '';
+
+ $query = "SELECT * FROM `" . TABLE_PANEL_IPSANDPORTS . "` `i`, `" . TABLE_DOMAINTOIP . "` `dip`
+ WHERE dip.id_domain = :domainid AND i.id = dip.id_ipandports ";
+
+ if ($ssl_vhost === true && ($domain['ssl'] == '1' || $domain['ssl_redirect'] == '1')) {
+ // by ordering by cert-file the row with filled out SSL-Fields will be shown last,
+ // thus it is enough to fill out 1 set of SSL-Fields
+ $query .= "AND i.ssl = 1 ORDER BY i.ssl_cert_file ASC;";
+ } else {
+ $query .= "AND i.ssl = '0';";
+ }
+
+ // start vhost
+ $vhost_content .= 'server { ' . "\n";
+
+ $result_stmt = Database::prepare($query);
+ Database::pexecute($result_stmt, array(
+ 'domainid' => $domain['id']
+ ));
+
+ while ($ipandport = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ $domain['ip'] = $ipandport['ip'];
+ $domain['port'] = $ipandport['port'];
+ if ($domain['ssl'] == '1') {
+ $domain['ssl_cert_file'] = $ipandport['ssl_cert_file'];
+ $domain['ssl_key_file'] = $ipandport['ssl_key_file'];
+ $domain['ssl_ca_file'] = $ipandport['ssl_ca_file'];
+ $domain['ssl_cert_chainfile'] = $ipandport['ssl_cert_chainfile'];
+
+ // SSL STUFF
+ $dssl = new DomainSSL();
+ // this sets the ssl-related array-indices in the $domain array
+ // if the domain has customer-defined ssl-certificates
+ $dssl->setDomainSSLFilesArray($domain);
+ }
+
+ if (filter_var($domain['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
+ $ipport = '[' . $domain['ip'] . ']:' . $domain['port'];
+ } else {
+ $ipport = $domain['ip'] . ':' . $domain['port'];
+ }
+
+ if ($ipandport['default_vhostconf_domain'] != '' && ($ssl_vhost == false || ($ssl_vhost == true && $ipandport['include_default_vhostconf_domain'] == '1'))) {
+ $_vhost_content .= $this->processSpecialConfigTemplate($ipandport['default_vhostconf_domain'], $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n";
+ }
+ if ($ipandport['ssl_default_vhostconf_domain'] != '' && $ssl_vhost == true) {
+ $_vhost_content .= $this->processSpecialConfigTemplate($ipandport['ssl_default_vhostconf_domain'], $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n";
+ }
+ $http2 = $ssl_vhost == true && (isset($domain['http2']) && $domain['http2'] == '1' && Settings::Get('system.http2_support') == '1');
+
+ $vhost_content .= "\t" . 'listen ' . $ipport . ($ssl_vhost == true ? ' ssl' : '') . ($http2 == true ? ' http2' : '') . ';' . "\n";
+ }
+
+ // get all server-names
+ $vhost_content .= $this->getServerNames($domain);
+
+ // respect ssl_redirect settings, #542
+ if ($ssl_vhost == false && $domain['ssl'] == '1' && $domain['ssl_redirect'] == '1') {
+ // We must not check if our port differs from port 443,
+ // but if there is a destination-port != 443
+ $_sslport = '';
+ // This returns the first port that is != 443 with ssl enabled, if any
+ // ordered by ssl-certificate (if any) so that the ip/port combo
+ // with certificate is used
+ $ssldestport_stmt = Database::prepare("SELECT `ip`.`port` FROM " . TABLE_PANEL_IPSANDPORTS . " `ip`
+ LEFT JOIN `" . TABLE_DOMAINTOIP . "` `dip` ON (`ip`.`id` = `dip`.`id_ipandports`)
+ WHERE `dip`.`id_domain` = :domainid
+ AND `ip`.`ssl` = '1' AND `ip`.`port` != 443
+ ORDER BY `ip`.`ssl_cert_file` DESC, `ip`.`port` LIMIT 1;");
+ $ssldestport = Database::pexecute_first($ssldestport_stmt, array(
+ 'domainid' => $domain['id']
+ ));
+
+ if ($ssldestport && $ssldestport['port'] != '') {
+ $_sslport = ":" . $ssldestport['port'];
+ }
+
+ $domain['documentroot'] = 'https://$host' . $_sslport . '/';
+ }
+
+ // avoid using any whitespaces
+ $domain['documentroot'] = trim($domain['documentroot']);
+
+ // create ssl settings first since they are required for normal and redirect vhosts
+ if ($ssl_vhost === true && $domain['ssl'] == '1' && Settings::Get('system.use_ssl') == '1') {
+ $vhost_content .= "\n" . $this->composeSslSettings($domain) . "\n";
+ }
+
+ if (Settings::Get('system.use_ssl') == '1' && Settings::Get('system.leenabled') == '1') {
+ $acmeConfFilename = Settings::Get('system.letsencryptacmeconf');
+ $vhost_content .= "\t" . 'include ' . $acmeConfFilename . ';' . "\n";
+ }
+
+ // if the documentroot is an URL we just redirect
+ if (preg_match('/^https?\:\/\//', $domain['documentroot'])) {
+ $uri = $domain['documentroot'];
+ if (substr($uri, - 1) == '/') {
+ $uri = substr($uri, 0, - 1);
+ }
+
+ // Get domain's redirect code
+ $code = \Froxlor\Domain\Domain::getDomainRedirectCode($domain['id']);
+
+ $vhost_content .= "\t" . 'location / {' . "\n";
+ $vhost_content .= "\t\t" . 'return ' . $code . ' ' . $uri . '$request_uri;' . "\n";
+ $vhost_content .= "\t" . '}' . "\n";
+ } else {
+ \Froxlor\FileDir::mkDirWithCorrectOwnership($domain['customerroot'], $domain['documentroot'], $domain['guid'], $domain['guid'], true);
+
+ $vhost_content .= $this->getLogFiles($domain);
+ $vhost_content .= $this->getWebroot($domain, $ssl_vhost);
+
+ if ($this->deactivated == false) {
+
+ $vhost_content = $this->mergeVhostCustom($vhost_content, $this->createPathOptions($domain)) . "\n";
+ $vhost_content .= $this->composePhpOptions($domain, $ssl_vhost);
+
+ $vhost_content .= isset($this->needed_htpasswds[$domain['id']]) ? $this->needed_htpasswds[$domain['id']] . "\n" : '';
+
+ if ($domain['specialsettings'] != '' && ($ssl_vhost == false || ($ssl_vhost == true && $domain['include_specialsettings'] == 1))) {
+ $vhost_content = $this->mergeVhostCustom($vhost_content, $this->processSpecialConfigTemplate($domain['specialsettings'], $domain, $domain['ip'], $domain['port'], $ssl_vhost));
+ }
+
+ if ($domain['ssl_specialsettings'] != '' && $ssl_vhost == true) {
+ $vhost_content = $this->mergeVhostCustom($vhost_content, $this->processSpecialConfigTemplate($domain['ssl_specialsettings'], $domain, $domain['ip'], $domain['port'], $ssl_vhost));
+ }
+
+ if ($_vhost_content != '') {
+ $vhost_content = $this->mergeVhostCustom($vhost_content, $_vhost_content);
+ }
+
+ if (Settings::Get('system.default_vhostconf') != '' && ($ssl_vhost == false || ($ssl_vhost == true && Settings::Get('system.include_default_vhostconf') == 1))) {
+ $vhost_content = $this->mergeVhostCustom($vhost_content, $this->processSpecialConfigTemplate(Settings::Get('system.default_vhostconf'), $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n");
+ }
+
+ if (Settings::Get('system.default_sslvhostconf') != '' && $ssl_vhost == true) {
+ $vhost_content = $this->mergeVhostCustom($vhost_content, $this->processSpecialConfigTemplate(Settings::Get('system.default_sslvhostconf'), $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n");
+ }
+ }
+ }
+ $vhost_content .= "\n}\n\n";
+
+ return $vhost_content;
+ }
+
+ private function cleanVhostStruct($vhost = null)
+ {
+ // Remove windows linebreaks
+ $vhost = str_replace("\r", "\n", $vhost);
+ // remove comments
+ $vhost = implode("\n", preg_replace('/^(\s+)?#(.*)$/', '', explode("\n", $vhost)));
+ // Break blocks into lines
+ $vhost = str_replace(array(
+ "{",
+ "}"
+ ), array(
+ " {\n",
+ "\n}"
+ ), $vhost);
+ // Break into array items
+ $vhost = explode("\n", preg_replace('/[ \t]+/', ' ', trim(preg_replace('/\t+/', '', $vhost))));
+ // Remove empty lines
+ $vhost = array_filter($vhost, function ($a) {
+ return preg_match("#\S#", $a);
+ });
+
+ // remove unnecessary whitespaces
+ $vhost = array_map("trim", $vhost);
+ // re-number array keys
+ $vhost = array_values($vhost);
+ return $vhost;
+ }
+
+ protected function mergeVhostCustom($vhost_frx, $vhost_usr)
+ {
+ // Clean froxlor defined settings
+ $vhost_frx = $this->cleanVhostStruct($vhost_frx);
+ // Clean user defined settings
+ $vhost_usr = $this->cleanVhostStruct($vhost_usr);
+
+ // Cycle through the user defined settings
+ $currentBlock = array();
+ $blockLevel = 0;
+ foreach ($vhost_usr as $line) {
+ $line = trim($line);
+ $currentBlock[] = $line;
+
+ if (strpos($line, "{") !== false) {
+ $blockLevel ++;
+ }
+ if (strpos($line, "}") !== false && $blockLevel > 0) {
+ $blockLevel --;
+ }
+
+ if ($line == "}" && $blockLevel == 0) {
+ if (in_array($currentBlock[0], $vhost_frx)) {
+ // Add to existing block
+ $pos = array_search($currentBlock[0], $vhost_frx);
+ do {
+ $pos ++;
+ } while ($vhost_frx[$pos] != "}");
+
+ for ($i = 1; $i < count($currentBlock) - 1; $i ++) {
+ array_splice($vhost_frx, $pos + $i - 1, 0, $currentBlock[$i]);
+ }
+ } else {
+ // Add to end
+ array_splice($vhost_frx, count($vhost_frx), 0, $currentBlock);
+ }
+ $currentBlock = array();
+ } elseif ($blockLevel == 0) {
+ array_splice($vhost_frx, count($vhost_frx), 0, $currentBlock);
+ $currentBlock = array();
+ }
+ }
+
+ $nextLevel = 0;
+ for ($i = 0; $i < count($vhost_frx); $i ++) {
+ if (substr_count($vhost_frx[$i], "}") != 0 && substr_count($vhost_frx[$i], "{") == 0) {
+ $nextLevel -= 1;
+ $vhost_frx[$i] .= "\n";
+ }
+ if ($nextLevel > 0) {
+ for ($j = 0; $j < $nextLevel; $j ++) {
+ $vhost_frx[$i] = " " . $vhost_frx[$i];
+ }
+ }
+ if (substr_count($vhost_frx[$i], "{") != 0 && substr_count($vhost_frx[$i], "}") == 0) {
+ $nextLevel += 1;
+ }
+ }
+
+ return implode("\n", $vhost_frx);
+ }
+
+ protected function composeSslSettings($domain_or_ip)
+ {
+ $sslsettings = '';
+
+ if ($domain_or_ip['ssl_cert_file'] == '' || ! file_exists($domain_or_ip['ssl_cert_file'])) {
+ $domain_or_ip['ssl_cert_file'] = Settings::Get('system.ssl_cert_file');
+ if (! file_exists($domain_or_ip['ssl_cert_file'])) {
+ // explicitly disable ssl for this vhost
+ $domain_or_ip['ssl_cert_file'] = "";
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'System certificate file "' . Settings::Get('system.ssl_cert_file') . '" does not seem to exist. Disabling SSL-vhost for "' . $domain_or_ip['domain'] . '"');
+ }
+ }
+
+ if ($domain_or_ip['ssl_key_file'] == '' || ! file_exists($domain_or_ip['ssl_key_file'])) {
+ // use fallback
+ $domain_or_ip['ssl_key_file'] = Settings::Get('system.ssl_key_file');
+ // check whether it exists
+ if (! file_exists($domain_or_ip['ssl_key_file'])) {
+ // explicitly disable ssl for this vhost
+ $domain_or_ip['ssl_cert_file'] = "";
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'System certificate key-file "' . Settings::Get('system.ssl_key_file') . '" does not seem to exist. Disabling SSL-vhost for "' . $domain_or_ip['domain'] . '"');
+ }
+ }
+
+ if ($domain_or_ip['ssl_ca_file'] == '') {
+ $domain_or_ip['ssl_ca_file'] = Settings::Get('system.ssl_ca_file');
+ }
+
+ // #418
+ if ($domain_or_ip['ssl_cert_chainfile'] == '') {
+ $domain_or_ip['ssl_cert_chainfile'] = Settings::Get('system.ssl_cert_chainfile');
+ }
+
+ if ($domain_or_ip['ssl_cert_file'] != '') {
+
+ // check for existence, #1485
+ if (! file_exists($domain_or_ip['ssl_cert_file'])) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, $domain_or_ip['domain'] . ' :: certificate file "' . $domain_or_ip['ssl_cert_file'] . '" does not exist! Cannot create ssl-directives');
+ } else {
+
+ $ssl_protocols = (isset($domain_or_ip['override_tls']) && $domain_or_ip['override_tls'] == '1' && ! empty($domain_or_ip['ssl_protocols'])) ? $domain_or_ip['ssl_protocols'] : Settings::Get('system.ssl_protocols');
+ $ssl_cipher_list = (isset($domain_or_ip['override_tls']) && $domain_or_ip['override_tls'] == '1' && ! empty($domain_or_ip['ssl_cipher_list'])) ? $domain_or_ip['ssl_cipher_list'] : Settings::Get('system.ssl_cipher_list');
+
+ // obsolete: ssl on now belongs to the listen block as 'ssl' at the end
+ // $sslsettings .= "\t" . 'ssl on;' . "\n";
+ $sslsettings .= "\t" . 'ssl_protocols ' . str_replace(",", " ", $ssl_protocols) . ';' . "\n";
+ $sslsettings .= "\t" . 'ssl_ciphers ' . $ssl_cipher_list . ';' . "\n";
+ if (! empty(Settings::Get('system.dhparams_file'))) {
+ $dhparams = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.dhparams_file'));
+ if (! file_exists($dhparams)) {
+ \Froxlor\FileDir::safe_exec('openssl dhparam -out ' . escapeshellarg($dhparams) . ' 4096');
+ }
+ $sslsettings .= "\t" . 'ssl_dhparam ' . $dhparams . ';' . "\n";
+ }
+ // When <1.11.0: Defaults to prime256v1, similar to first curve recommendation by Mozilla.
+ // (When specifyng just one, there's no fallback when specific curve is not supported by client.)
+ // When >1.11.0: Defaults to auto, using recommended curves provided by OpenSSL.
+ // see https://github.com/Froxlor/Froxlor/issues/652
+ // $sslsettings .= "\t" . 'ssl_ecdh_curve secp384r1;' . "\n";
+ $sslsettings .= "\t" . 'ssl_prefer_server_ciphers ' . (isset($domain_or_ip['ssl_honorcipherorder']) && $domain_or_ip['ssl_honorcipherorder'] == '1' ? 'on' : 'off') . ';' . "\n";
+ if (Settings::Get('system.sessionticketsenabled') == '1') {
+ $sslsettings .= "\t" . 'ssl_session_tickets ' . (isset($domain_or_ip['ssl_sessiontickets']) && $domain_or_ip['ssl_sessiontickets'] == '1' ? 'on' : 'off') . ';' . "\n";
+ }
+ $sslsettings .= "\t" . 'ssl_session_cache shared:SSL:10m;' . "\n";
+ $sslsettings .= "\t" . 'ssl_certificate ' . \Froxlor\FileDir::makeCorrectFile($domain_or_ip['ssl_cert_file']) . ';' . "\n";
+
+ if ($domain_or_ip['ssl_key_file'] != '') {
+ // check for existence, #1485
+ if (! file_exists($domain_or_ip['ssl_key_file'])) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, $domain_or_ip['domain'] . ' :: certificate key file "' . $domain_or_ip['ssl_key_file'] . '" does not exist! Cannot create ssl-directives');
+ } else {
+ $sslsettings .= "\t" . 'ssl_certificate_key ' . \Froxlor\FileDir::makeCorrectFile($domain_or_ip['ssl_key_file']) . ';' . "\n";
+ }
+ }
+
+ if (isset($domain_or_ip['hsts']) && $domain_or_ip['hsts'] >= 0) {
+ $sslsettings .= 'add_header Strict-Transport-Security "max-age=' . $domain_or_ip['hsts'];
+ if ($domain_or_ip['hsts_sub'] == 1) {
+ $sslsettings .= '; includeSubDomains';
+ }
+ if ($domain_or_ip['hsts_preload'] == 1) {
+ $sslsettings .= '; preload';
+ }
+ $sslsettings .= '";' . "\n";
+ }
+
+ if ((isset($domain_or_ip['ocsp_stapling']) && $domain_or_ip['ocsp_stapling'] == "1") || (isset($domain_or_ip['letsencrypt']) && $domain_or_ip['letsencrypt'] == "1")) {
+ $sslsettings .= "\t" . 'ssl_stapling on;' . "\n";
+ $sslsettings .= "\t" . 'ssl_stapling_verify on;' . "\n";
+ $sslsettings .= "\t" . 'ssl_trusted_certificate ' . \Froxlor\FileDir::makeCorrectFile($domain_or_ip['ssl_cert_file']) . ';' . "\n";
+ }
+ }
+ }
+
+ return $sslsettings;
+ }
+
+ protected function createPathOptions($domain)
+ {
+ $result_stmt = Database::prepare("
+ SELECT * FROM " . TABLE_PANEL_HTACCESS . "
+ WHERE `path` LIKE :docroot
+ ");
+ Database::pexecute($result_stmt, array(
+ 'docroot' => $domain['documentroot'] . '%'
+ ));
+
+ $path_options = '';
+ $htpasswds = $this->getHtpasswds($domain);
+
+ // for each entry in the htaccess table
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if (! empty($row['error404path'])) {
+ $defhandler = $row['error404path'];
+ if (! \Froxlor\Validate\Validate::validateUrl($defhandler)) {
+ $defhandler = \Froxlor\FileDir::makeCorrectFile($defhandler);
+ }
+ $path_options .= "\t" . 'error_page 404 ' . $defhandler . ';' . "\n";
+ }
+
+ if (! empty($row['error403path'])) {
+ $defhandler = $row['error403path'];
+ if (! \Froxlor\Validate\Validate::validateUrl($defhandler)) {
+ $defhandler = \Froxlor\FileDir::makeCorrectFile($defhandler);
+ }
+ $path_options .= "\t" . 'error_page 403 ' . $defhandler . ';' . "\n";
+ }
+
+ if (! empty($row['error500path'])) {
+ $defhandler = $row['error500path'];
+ if (! \Froxlor\Validate\Validate::validateUrl($defhandler)) {
+ $defhandler = \Froxlor\FileDir::makeCorrectFile($defhandler);
+ }
+ $path_options .= "\t" . 'error_page 500 502 503 504 ' . $defhandler . ';' . "\n";
+ }
+
+ // if ($row['options_indexes'] != '0') {
+ $path = \Froxlor\FileDir::makeCorrectDir(substr($row['path'], strlen($domain['documentroot']) - 1));
+
+ \Froxlor\FileDir::mkDirWithCorrectOwnership($domain['documentroot'], $row['path'], $domain['guid'], $domain['guid']);
+
+ $path_options .= "\t" . '# ' . $path . "\n";
+ if ($path == '/') {
+ if ($row['options_indexes'] != '0') {
+ $this->vhost_root_autoindex = true;
+ }
+ $path_options .= "\t" . 'location ' . $path . ' {' . "\n";
+ if ($this->vhost_root_autoindex) {
+ $path_options .= "\t\t" . 'autoindex on;' . "\n";
+ $this->vhost_root_autoindex = false;
+ }
+
+ // check if we have a htpasswd for this path
+ // (damn nginx does not like more than one
+ // 'location'-part with the same path)
+ if (count($htpasswds) > 0) {
+ foreach ($htpasswds as $idx => $single) {
+ switch ($single['path']) {
+ case '/awstats/':
+ case '/webalizer/':
+ // no stats-alias in "location /"-context
+ break;
+ default:
+ if ($single['path'] == '/') {
+ $path_options .= "\t\t" . 'auth_basic "' . $single['authname'] . '";' . "\n";
+ $path_options .= "\t\t" . 'auth_basic_user_file ' . \Froxlor\FileDir::makeCorrectFile($single['usrf']) . ';' . "\n";
+ if ($domain['phpenabled_customer'] == 1 && $domain['phpenabled_vhost'] == '1') {
+ $path_options .= "\t\t" . 'index index.php index.html index.htm;' . "\n";
+ } else {
+ $path_options .= "\t\t" . 'index index.html index.htm;' . "\n";
+ }
+ $path_options .= "\t\t" . 'location ~ ^(.+?\.php)(/.*)?$ {' . "\n";
+ $path_options .= "\t\t\t" . 'try_files ' . $domain['nonexistinguri'] . ' @php;' . "\n";
+ $path_options .= "\t\t" . '}' . "\n";
+ // remove already used entries so we do not have doubles
+ unset($htpasswds[$idx]);
+ }
+ }
+ }
+ }
+ $path_options .= "\t" . '}' . "\n";
+
+ $this->vhost_root_autoindex = false;
+ } else {
+ $path_options .= "\t" . 'location ' . $path . ' {' . "\n";
+ if ($this->vhost_root_autoindex || $row['options_indexes'] != '0') {
+ $path_options .= "\t\t" . 'autoindex on;' . "\n";
+ $this->vhost_root_autoindex = false;
+ }
+ $path_options .= "\t" . '} ' . "\n";
+ }
+ // }
+
+ /**
+ * Perl support
+ * required the fastCGI wrapper to be running to receive the CGI requests.
+ */
+ if (\Froxlor\Customer\Customer::customerHasPerlEnabled($domain['customerid']) && $row['options_cgi'] != '0') {
+ $path = \Froxlor\FileDir::makeCorrectDir(substr($row['path'], strlen($domain['documentroot']) - 1));
+ \Froxlor\FileDir::mkDirWithCorrectOwnership($domain['documentroot'], $row['path'], $domain['guid'], $domain['guid']);
+
+ // We need to remove the last slash, otherwise the regex wouldn't work
+ if ($row['path'] != $domain['documentroot']) {
+ $path = substr($path, 0, - 1);
+ }
+ $path_options .= "\t" . 'location ~ \(.pl|.cgi)$ {' . "\n";
+ $path_options .= "\t\t" . 'gzip off; #gzip makes scripts feel slower since they have to complete before getting gzipped' . "\n";
+ $path_options .= "\t\t" . 'fastcgi_pass ' . Settings::Get('system.perl_server') . ';' . "\n";
+ $path_options .= "\t\t" . 'fastcgi_index index.cgi;' . "\n";
+ $path_options .= "\t\t" . 'include ' . Settings::Get('nginx.fastcgiparams') . ';' . "\n";
+ $path_options .= "\t" . '}' . "\n";
+ }
+ }
+
+ // now the rest of the htpasswds
+ if (count($htpasswds) > 0) {
+ foreach ($htpasswds as $idx => $single) {
+ // if ($single['path'] != '/') {
+ switch ($single['path']) {
+ case '/awstats/':
+ case '/webalizer/':
+ $path_options .= $this->getStats($domain, $single);
+ unset($htpasswds[$idx]);
+ break;
+ default:
+ $path_options .= "\t" . 'location ' . \Froxlor\FileDir::makeCorrectDir($single['path']) . ' {' . "\n";
+ $path_options .= "\t\t" . 'auth_basic "' . $single['authname'] . '";' . "\n";
+ $path_options .= "\t\t" . 'auth_basic_user_file ' . \Froxlor\FileDir::makeCorrectFile($single['usrf']) . ';' . "\n";
+ if ($domain['phpenabled_customer'] == 1 && $domain['phpenabled_vhost'] == '1') {
+ $path_options .= "\t\t" . 'index index.php index.html index.htm;' . "\n";
+ } else {
+ $path_options .= "\t\t" . 'index index.html index.htm;' . "\n";
+ }
+ $path_options .= "\t\t" . 'location ~ ^(.+?\.php)(/.*)?$ {' . "\n";
+ $path_options .= "\t\t\t" . 'try_files ' . $domain['nonexistinguri'] . ' @php;' . "\n";
+ $path_options .= "\t\t" . '}' . "\n";
+ $path_options .= "\t" . '}' . "\n";
+ }
+ // }
+ unset($htpasswds[$idx]);
+ }
+ }
+
+ return $path_options;
+ }
+
+ protected function getHtpasswds($domain)
+ {
+ $result_stmt = Database::prepare("
+ SELECT *
+ FROM `" . TABLE_PANEL_HTPASSWDS . "` AS a
+ JOIN `" . TABLE_PANEL_DOMAINS . "` AS b USING (`customerid`)
+ WHERE b.customerid = :customerid AND b.domain = :domain
+ ");
+ Database::pexecute($result_stmt, array(
+ 'customerid' => $domain['customerid'],
+ 'domain' => $domain['domain']
+ ));
+
+ $returnval = array();
+ $x = 0;
+ while ($row_htpasswds = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if (count($row_htpasswds) > 0) {
+ $htpasswd_filename = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.apacheconf_htpasswddir') . '/' . $row_htpasswds['customerid'] . '-' . md5($row_htpasswds['path']) . '.htpasswd');
+
+ // ensure we can write to the array with index $htpasswd_filename
+ if (! isset($this->htpasswds_data[$htpasswd_filename])) {
+ $this->htpasswds_data[$htpasswd_filename] = '';
+ }
+
+ $this->htpasswds_data[$htpasswd_filename] .= $row_htpasswds['username'] . ':' . $row_htpasswds['password'] . "\n";
+
+ // if the domains and their web contents are located in a subdirectory of
+ // the nginx user, we have to evaluate the right path which is to protect
+ if (stripos($row_htpasswds['path'], $domain['documentroot']) !== false) {
+ // if the website contents is located in the user directory
+ $path = \Froxlor\FileDir::makeCorrectDir(substr($row_htpasswds['path'], strlen($domain['documentroot']) - 1));
+ } else {
+ // if the website contents is located in a subdirectory of the user
+ $matches = array();
+ preg_match('/^([\/[:print:]]*\/)([[:print:]\/]+){1}$/i', $row_htpasswds['path'], $matches);
+ $path = \Froxlor\FileDir::makeCorrectDir(substr($row_htpasswds['path'], strlen($matches[1]) - 1));
+ }
+
+ $returnval[$x]['path'] = $path;
+ $returnval[$x]['root'] = \Froxlor\FileDir::makeCorrectDir($domain['documentroot']);
+
+ // Ensure there is only one auth name per password block, otherwise
+ // the directives are inserted multiple times -> invalid config
+ $authname = $row_htpasswds['authname'];
+ for ($i = 0; $i < $x; $i ++) {
+ if ($returnval[$i]['usrf'] == $htpasswd_filename) {
+ $authname = $returnval[$i]['authname'];
+ break;
+ }
+ }
+ $returnval[$x]['authname'] = $authname;
+
+ $returnval[$x]['usrf'] = $htpasswd_filename;
+ $x ++;
+ }
+ }
+
+ // Remove duplicate entries
+ $returnval = array_map("unserialize", array_unique(array_map("serialize", $returnval)));
+
+ return $returnval;
+ }
+
+ protected function composePhpOptions($domain, $ssl_vhost = false)
+ {
+ $phpopts = '';
+ if ($domain['phpenabled_customer'] == 1 && $domain['phpenabled_vhost'] == '1') {
+ $phpopts = "\tlocation ~ \.php {\n";
+ $phpopts .= "\t\t" . 'try_files ' . $domain['nonexistinguri'] . ' @php;' . "\n";
+ $phpopts .= "\t" . '}' . "\n\n";
+
+ $phpopts .= "\tlocation @php {\n";
+ $phpopts .= "\t\tfastcgi_split_path_info ^(.+?\.php)(/.*)$;\n";
+ $phpopts .= "\t\tinclude " . Settings::Get('nginx.fastcgiparams') . ";\n";
+ $phpopts .= "\t\tfastcgi_param SCRIPT_FILENAME \$request_filename;\n";
+ $phpopts .= "\t\tfastcgi_param PATH_INFO \$fastcgi_path_info;\n";
+ $phpopts .= "\t\ttry_files \$fastcgi_script_name =404;\n";
+ $phpopts .= "\t\tfastcgi_pass " . Settings::Get('system.nginx_php_backend') . ";\n";
+ $phpopts .= "\t\tfastcgi_index index.php;\n";
+ if ($domain['ssl'] == '1' && $ssl_vhost) {
+ $phpopts .= "\t\tfastcgi_param HTTPS on;\n";
+ }
+ $phpopts .= "\t}\n\n";
+ }
+ return $phpopts;
+ }
+
+ protected function getWebroot($domain, $ssl)
+ {
+ $webroot_text = '';
+
+ if ($domain['deactivated'] == '1' && Settings::Get('system.deactivateddocroot') != '') {
+ $webroot_text .= "\t" . '# Using docroot for deactivated users...' . "\n";
+ $webroot_text .= "\t" . 'root ' . \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.deactivateddocroot')) . ';' . "\n";
+ $this->deactivated = true;
+ } else {
+ $webroot_text .= "\t" . 'root ' . \Froxlor\FileDir::makeCorrectDir($domain['documentroot']) . ';' . "\n";
+ $this->deactivated = false;
+ }
+
+ $webroot_text .= "\n\t" . 'location / {' . "\n";
+
+ if ($domain['phpenabled_customer'] == 1 && $domain['phpenabled_vhost'] == '1') {
+ $webroot_text .= "\t" . 'index index.php index.html index.htm;' . "\n";
+ if ($domain['notryfiles'] != 1) {
+ $webroot_text .= "\t\t" . 'try_files $uri $uri/ @rewrites;' . "\n";
+ }
+ } else {
+ $webroot_text .= "\t" . 'index index.html index.htm;' . "\n";
+ }
+
+ if ($this->vhost_root_autoindex) {
+ $webroot_text .= "\t\t" . 'autoindex on;' . "\n";
+ $this->vhost_root_autoindex = false;
+ }
+
+ $webroot_text .= "\t" . '}' . "\n\n";
+ if ($domain['phpenabled_customer'] == 1 && $domain['phpenabled_vhost'] == '1' && $domain['notryfiles'] != 1) {
+ $webroot_text .= "\tlocation @rewrites {\n";
+ $webroot_text .= "\t\trewrite ^ /index.php last;\n";
+ $webroot_text .= "\t}\n\n";
+ }
+
+ return $webroot_text;
+ }
+
+ protected function getStats($domain, $single)
+ {
+ $stats_text = '';
+
+ // define basic path to the stats
+ if (Settings::Get('system.awstats_enabled') == '1') {
+ $alias_dir = \Froxlor\FileDir::makeCorrectFile($domain['customerroot'] . '/awstats/');
+ } else {
+ $alias_dir = \Froxlor\FileDir::makeCorrectFile($domain['customerroot'] . '/webalizer/');
+ }
+
+ // if this is a parentdomain, we use this domain-name
+ if ($domain['parentdomainid'] == '0') {
+ $alias_dir = \Froxlor\FileDir::makeCorrectDir($alias_dir . '/' . $domain['domain']);
+ } else {
+ $alias_dir = \Froxlor\FileDir::makeCorrectDir($alias_dir . '/' . $domain['parentdomain']);
+ }
+
+ if (Settings::Get('system.awstats_enabled') == '1') {
+ // awstats
+ $stats_text .= "\t" . 'location ^~ /awstats {' . "\n";
+ } else {
+ // webalizer
+ $stats_text .= "\t" . 'location ^~ /webalizer {' . "\n";
+ }
+
+ $stats_text .= "\t\t" . 'alias ' . $alias_dir . ';' . "\n";
+ $stats_text .= "\t\t" . 'auth_basic "' . $single['authname'] . '";' . "\n";
+ $stats_text .= "\t\t" . 'auth_basic_user_file ' . \Froxlor\FileDir::makeCorrectFile($single['usrf']) . ';' . "\n";
+ $stats_text .= "\t" . '}' . "\n\n";
+
+ // awstats icons
+ if (Settings::Get('system.awstats_enabled') == '1') {
+ $stats_text .= "\t" . 'location ~ ^/awstats-icon/(.*)$ {' . "\n";
+ $stats_text .= "\t\t" . 'alias ' . \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.awstats_icons')) . '$1;' . "\n";
+ $stats_text .= "\t" . '}' . "\n\n";
+ }
+
+ return $stats_text;
+ }
+
+ protected function getLogFiles($domain)
+ {
+ $logfiles_text = '';
+
+ $speciallogfile = '';
+ if ($domain['speciallogfile'] == '1') {
+ if ($domain['parentdomainid'] == '0') {
+ $speciallogfile = '-' . $domain['domain'];
+ } else {
+ $speciallogfile = '-' . $domain['parentdomain'];
+ }
+ }
+
+ if ($domain['writeerrorlog']) {
+ // The normal access/error - logging is enabled
+ $error_log = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.logfiles_directory') . $domain['loginname'] . $speciallogfile . '-error.log');
+ // Create the logfile if it does not exist (fixes #46)
+ touch($error_log);
+ chown($error_log, Settings::Get('system.httpuser'));
+ chgrp($error_log, Settings::Get('system.httpgroup'));
+ } else {
+ $error_log = '/dev/null';
+ }
+
+ if ($domain['writeaccesslog']) {
+ $access_log = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.logfiles_directory') . $domain['loginname'] . $speciallogfile . '-access.log');
+ // Create the logfile if it does not exist (fixes #46)
+ touch($access_log);
+ chown($access_log, Settings::Get('system.httpuser'));
+ chgrp($access_log, Settings::Get('system.httpgroup'));
+ } else {
+ $access_log = '/dev/null';
+ }
+
+ $logtype = 'combined';
+ if (Settings::Get('system.logfiles_format') != '') {
+ $logtype = 'frx_custom';
+ }
+
+ $logfiles_text .= "\t" . 'access_log ' . $access_log . ' ' . $logtype . ';' . "\n";
+ $logfiles_text .= "\t" . 'error_log ' . $error_log . ' ' . \Froxlor\Settings::Get('system.errorlog_level') . ';' . "\n";
+
+ if (Settings::Get('system.awstats_enabled') == '1') {
+ if ((int) $domain['parentdomainid'] == 0) {
+ // prepare the aliases and subdomains for stats config files
+ $server_alias = '';
+ $alias_domains_stmt = Database::prepare("
+ SELECT `domain`, `iswildcarddomain`, `wwwserveralias`
+ FROM `" . TABLE_PANEL_DOMAINS . "`
+ WHERE `aliasdomain` = :domainid OR `parentdomainid` = :domainid
+ ");
+ Database::pexecute($alias_domains_stmt, array(
+ 'domainid' => $domain['id']
+ ));
+
+ while (($alias_domain = $alias_domains_stmt->fetch(\PDO::FETCH_ASSOC)) !== false) {
+ $server_alias .= ' ' . $alias_domain['domain'] . ' ';
+
+ if ($alias_domain['iswildcarddomain'] == '1') {
+ $server_alias .= '*.' . $domain['domain'];
+ } else {
+ if ($alias_domain['wwwserveralias'] == '1') {
+ $server_alias .= 'www.' . $alias_domain['domain'];
+ } else {
+ $server_alias .= '';
+ }
+ }
+ }
+
+ $alias = '';
+ if ($domain['iswildcarddomain'] == '1') {
+ $alias = '*.' . $domain['domain'];
+ } elseif ($domain['wwwserveralias'] == '1') {
+ $alias = 'www.' . $domain['domain'];
+ }
+
+ // After inserting the AWStats information,
+ // be sure to build the awstats conf file as well
+ // and chown it using $awstats_params, #258
+ // Bug 960 + Bug 970 : Use full $domain instead of custom $awstats_params as following classes depend on the informations
+ \Froxlor\Http\Statistics::createAWStatsConf(Settings::Get('system.logfiles_directory') . $domain['loginname'] . $speciallogfile . '-access.log', $domain['domain'], $alias . $server_alias, $domain['customerroot'], $domain);
+ }
+ }
+
+ return $logfiles_text;
+ }
+
+ public function createOwnVhostStarter()
+ {
+ return;
+ }
+
+ protected function getServerNames($domain)
+ {
+ $server_alias = '';
+
+ if ($domain['iswildcarddomain'] == '1') {
+ $server_alias = '*.' . $domain['domain'];
+ } elseif ($domain['wwwserveralias'] == '1') {
+ $server_alias = 'www.' . $domain['domain'];
+ }
+
+ $alias_domains_stmt = Database::prepare("
+ SELECT `domain`, `iswildcarddomain`, `wwwserveralias`
+ FROM `" . TABLE_PANEL_DOMAINS . "`
+ WHERE `aliasdomain` = :domainid
+ ");
+ Database::pexecute($alias_domains_stmt, array(
+ 'domainid' => $domain['id']
+ ));
+
+ while (($alias_domain = $alias_domains_stmt->fetch(\PDO::FETCH_ASSOC)) !== false) {
+ $server_alias .= ' ' . $alias_domain['domain'];
+
+ if ($alias_domain['iswildcarddomain'] == '1') {
+ $server_alias .= ' *.' . $alias_domain['domain'];
+ } elseif ($alias_domain['wwwserveralias'] == '1') {
+ $server_alias .= ' www.' . $alias_domain['domain'];
+ }
+ }
+
+ $servernames_text = "\t" . 'server_name ' . $domain['domain'];
+ if (trim($server_alias) != '') {
+ $servernames_text .= ' ' . $server_alias;
+ }
+ $servernames_text .= ';' . "\n";
+
+ return $servernames_text;
+ }
+
+ public function writeConfigs()
+ {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "nginx::writeConfigs: rebuilding " . Settings::Get('system.apacheconf_vhost'));
+
+ $vhostDir = new \Froxlor\Http\Directory(Settings::Get('system.apacheconf_vhost'));
+ if (! $vhostDir->isConfigDir()) {
+ // Save one big file
+ $vhosts_file = '';
+
+ // sort by filename so the order is:
+ // 1. subdomains
+ // 2. subdomains as main-domains
+ // 3. main-domains
+ ksort($this->nginx_data);
+
+ foreach ($this->nginx_data as $vhosts_filename => $vhost_content) {
+ $vhosts_file .= $vhost_content . "\n\n";
+ }
+
+ $vhosts_filename = Settings::Get('system.apacheconf_vhost');
+
+ // Apply header
+ $vhosts_file = '# ' . basename($vhosts_filename) . "\n" . '# Created ' . date('d.m.Y H:i') . "\n" . '# Do NOT manually edit this file, all changes will be deleted after the next domain change at the panel.' . "\n" . "\n" . $vhosts_file;
+ $vhosts_file_handler = fopen($vhosts_filename, 'w');
+ fwrite($vhosts_file_handler, $vhosts_file);
+ fclose($vhosts_file_handler);
+ } else {
+ if (! file_exists(Settings::Get('system.apacheconf_vhost'))) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'nginx::writeConfigs: mkdir ' . escapeshellarg(\Froxlor\FileDir::makeCorrectDir(Settings::Get('system.apacheconf_vhost'))));
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg(\Froxlor\FileDir::makeCorrectDir(Settings::Get('system.apacheconf_vhost'))));
+ }
+
+ // Write a single file for every vhost
+ foreach ($this->nginx_data as $vhosts_filename => $vhosts_file) {
+ $this->known_filenames[] = basename($vhosts_filename);
+
+ // Apply header
+ $vhosts_file = '# ' . basename($vhosts_filename) . "\n" . '# Created ' . date('d.m.Y H:i') . "\n" . '# Do NOT manually edit this file, all changes will be deleted after the next domain change at the panel.' . "\n" . "\n" . $vhosts_file;
+
+ if (! empty($vhosts_filename)) {
+ $vhosts_file_handler = fopen($vhosts_filename, 'w');
+ fwrite($vhosts_file_handler, $vhosts_file);
+ fclose($vhosts_file_handler);
+ }
+ }
+ }
+
+ // htaccess stuff
+ if (count($this->htpasswds_data) > 0) {
+ if (! file_exists(Settings::Get('system.apacheconf_htpasswddir'))) {
+ $umask = umask();
+ umask(0000);
+ mkdir(Settings::Get('system.apacheconf_htpasswddir'), 0751);
+ umask($umask);
+ } elseif (! is_dir(Settings::Get('system.apacheconf_htpasswddir'))) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_WARNING, 'WARNING!!! ' . Settings::Get('system.apacheconf_htpasswddir') . ' is not a directory. htpasswd directory protection is disabled!!!');
+ }
+
+ if (is_dir(Settings::Get('system.apacheconf_htpasswddir'))) {
+ foreach ($this->htpasswds_data as $htpasswd_filename => $htpasswd_file) {
+ $this->known_htpasswdsfilenames[] = basename($htpasswd_filename);
+ $htpasswd_file_handler = fopen($htpasswd_filename, 'w');
+ // Filter duplicate pairs of username and password
+ $htpasswd_file = implode("\n", array_unique(explode("\n", $htpasswd_file)));
+ fwrite($htpasswd_file_handler, $htpasswd_file);
+ fclose($htpasswd_file_handler);
+ }
+ }
+ }
+ }
+}
diff --git a/scripts/jobs/cron_tasks.inc.http.35.nginx_phpfpm.php b/lib/Froxlor/Cron/Http/NginxFcgi.php
similarity index 59%
rename from scripts/jobs/cron_tasks.inc.http.35.nginx_phpfpm.php
rename to lib/Froxlor/Cron/Http/NginxFcgi.php
index f47ccd82..dc004ad3 100644
--- a/scripts/jobs/cron_tasks.inc.http.35.nginx_phpfpm.php
+++ b/lib/Froxlor/Cron/Http/NginxFcgi.php
@@ -1,4 +1,9 @@
- (2010-)
- * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
- * @package Cron
- *
+ * @copyright (c) the authors
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
*/
-
-class nginx_phpfpm extends nginx
+class NginxFcgi extends Nginx
{
- protected function composePhpOptions($domain, $ssl_vhost = false) {
+
+ protected function composePhpOptions($domain, $ssl_vhost = false)
+ {
$php_options_text = '';
- if ($domain['phpenabled'] == '1') {
- $php = new phpinterface($domain);
- $phpconfig = $php->getPhpConfig((int)$domain['phpsettingid']);
-
+ if ($domain['phpenabled_customer'] == 1 && $domain['phpenabled_vhost'] == '1') {
+ $php = new PhpInterface($domain);
+ $phpconfig = $php->getPhpConfig((int) $domain['phpsettingid']);
+
$php_options_text = "\t" . 'location ~ ^(.+?\.php)(/.*)?$ {' . "\n";
$php_options_text .= "\t\t" . 'try_files ' . $domain['nonexistinguri'] . ' @php;' . "\n";
$php_options_text .= "\t" . '}' . "\n\n";
$php_options_text .= "\t" . 'location @php {' . "\n";
- $php_options_text .= "\t\t" . 'try_files $1 = 404;' . "\n\n";
+ $php_options_text .= "\t\t" . 'try_files $1 =404;' . "\n\n";
$php_options_text .= "\t\t" . 'include ' . Settings::Get('nginx.fastcgiparams') . ";\n";
- $php_options_text .= "\t\t" . 'fastcgi_split_path_info ^(.+\.php)(/.+)\$;' . "\n";
- $php_options_text .= "\t\t" . 'fastcgi_param SCRIPT_FILENAME $document_root$1;' . "\n";
+ $php_options_text .= "\t\t" . 'fastcgi_split_path_info ^(.+?\.php)(/.*)$;' . "\n";
+ $php_options_text .= "\t\t" . 'fastcgi_param SCRIPT_FILENAME $request_filename;' . "\n";
$php_options_text .= "\t\t" . 'fastcgi_param PATH_INFO $2;' . "\n";
if ($domain['ssl'] == '1' && $ssl_vhost) {
$php_options_text .= "\t\t" . 'fastcgi_param HTTPS on;' . "\n";
@@ -40,50 +46,59 @@ class nginx_phpfpm extends nginx
$php_options_text .= "\t\t" . 'fastcgi_pass unix:' . $php->getInterface()->getSocketFile() . ";\n";
$php_options_text .= "\t\t" . 'fastcgi_index index.php;' . "\n";
$php_options_text .= "\t}\n\n";
-
+
// create starter-file | config-file
$php->getInterface()->createConfig($phpconfig);
// create php.ini (fpm does nothing here, as it
// defines ini-settings in its pool config)
$php->getInterface()->createIniFile($phpconfig);
- }
- else {
- $php_options_text.= ' # PHP is disabled for this vHost' . "\n";
+ } else {
+ $php_options_text .= ' # PHP is disabled for this vHost' . "\n";
}
return $php_options_text;
}
-
- public function createOwnVhostStarter() {
- if (Settings::Get('phpfpm.enabled') == '1'
- && Settings::Get('phpfpm.enabled_ownvhost') == '1'
- ) {
- $mypath = makeCorrectDir(dirname(dirname(dirname(__FILE__)))); // /var/www/froxlor, needed for chown
+ public function createOwnVhostStarter()
+ {
+ if (Settings::Get('phpfpm.enabled') == '1' && Settings::Get('phpfpm.enabled_ownvhost') == '1') {
+ $mypath = \Froxlor\FileDir::makeCorrectDir(dirname(dirname(dirname(__FILE__)))); // /var/www/froxlor, needed for chown
$user = Settings::Get('phpfpm.vhost_httpuser');
$group = Settings::Get('phpfpm.vhost_httpgroup');
+ // get fpm config
+ $fpm_sel_stmt = Database::prepare("
+ SELECT f.id FROM `" . TABLE_PANEL_FPMDAEMONS . "` f
+ LEFT JOIN `" . TABLE_PANEL_PHPCONFIGS . "` p ON p.fpmsettingid = f.id
+ WHERE p.id = :phpconfigid
+ ");
+ $fpm_config = Database::pexecute_first($fpm_sel_stmt, array(
+ 'phpconfigid' => Settings::Get('phpfpm.vhost_defaultini')
+ ));
+
$domain = array(
'id' => 'none',
'domain' => Settings::Get('system.hostname'),
'adminid' => 1, /* first admin-user (superadmin) */
- 'mod_fcgid_starter' => -1,
- 'mod_fcgid_maxrequests' => -1,
+ 'mod_fcgid_starter' => - 1,
+ 'mod_fcgid_maxrequests' => - 1,
'guid' => $user,
'openbasedir' => 0,
'email' => Settings::Get('panel.adminmail'),
'loginname' => 'froxlor.panel',
- 'documentroot' => $mypath
+ 'documentroot' => $mypath,
+ 'customerroot' => $mypath,
+ 'fpm_config_id' => isset($fpm_config['id']) ? $fpm_config['id'] : 1
);
// all the files and folders have to belong to the local user
// now because we also use fcgid for our own vhost
- safe_exec('chown -R ' . $user . ':' . $group . ' ' . escapeshellarg($mypath));
+ \Froxlor\FileDir::safe_exec('chown -R ' . $user . ':' . $group . ' ' . escapeshellarg($mypath));
// get php.ini for our own vhost
- $php = new phpinterface($domain);
+ $php = new PhpInterface($domain);
// get php-config
if (Settings::Get('phpfpm.enabled') == '1') {
@@ -102,6 +117,4 @@ class nginx_phpfpm extends nginx
$php->getInterface()->createIniFile($phpconfig);
}
}
-
-
}
diff --git a/lib/Froxlor/Cron/Http/Php/Fcgid.php b/lib/Froxlor/Cron/Http/Php/Fcgid.php
new file mode 100644
index 00000000..410261e3
--- /dev/null
+++ b/lib/Froxlor/Cron/Http/Php/Fcgid.php
@@ -0,0 +1,264 @@
+
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ * @link http://www.nutime.de/
+ * @since 0.9.16
+ *
+ */
+class Fcgid
+{
+
+ /**
+ * Domain-Data array
+ *
+ * @var array
+ */
+ private $domain = array();
+
+ /**
+ * Admin-Date cache array
+ *
+ * @var array
+ */
+ private $admin_cache = array();
+
+ /**
+ * main constructor
+ */
+ public function __construct($domain)
+ {
+ $this->domain = $domain;
+ }
+
+ /**
+ * create fcgid-starter-file
+ *
+ * @param array $phpconfig
+ */
+ public function createConfig($phpconfig)
+ {
+
+ // create starter
+ $starter_file = "#!/bin/sh\n\n";
+ $starter_file .= "#\n";
+ $starter_file .= "# starter created/changed on " . date("Y.m.d H:i:s") . " for domain '" . $this->domain['domain'] . "' with id #" . $this->domain['id'] . " from php template '" . $phpconfig['description'] . "' with id #" . $phpconfig['id'] . "\n";
+ $starter_file .= "# Do not change anything in this file, it will be overwritten by the Froxlor Cronjob!\n";
+ $starter_file .= "#\n\n";
+ $starter_file .= "umask " . $phpconfig['mod_fcgid_umask'] . "\n";
+ $starter_file .= "PHPRC=" . escapeshellarg($this->getConfigDir()) . "\n";
+ $starter_file .= "export PHPRC\n";
+
+ // set number of processes for one domain
+ if ((int) $this->domain['mod_fcgid_starter'] != - 1) {
+ $starter_file .= "PHP_FCGI_CHILDREN=" . (int) $this->domain['mod_fcgid_starter'] . "\n";
+ } else {
+ if ((int) $phpconfig['mod_fcgid_starter'] != - 1) {
+ $starter_file .= "PHP_FCGI_CHILDREN=" . (int) $phpconfig['mod_fcgid_starter'] . "\n";
+ } else {
+ $starter_file .= "PHP_FCGI_CHILDREN=" . (int) Settings::Get('system.mod_fcgid_starter') . "\n";
+ }
+ }
+
+ $starter_file .= "export PHP_FCGI_CHILDREN\n";
+
+ // set number of maximum requests for one domain
+ if ((int) $this->domain['mod_fcgid_maxrequests'] != - 1) {
+ $starter_file .= "PHP_FCGI_MAX_REQUESTS=" . (int) $this->domain['mod_fcgid_maxrequests'] . "\n";
+ } else {
+ if ((int) $phpconfig['mod_fcgid_maxrequests'] != - 1) {
+ $starter_file .= "PHP_FCGI_MAX_REQUESTS=" . (int) $phpconfig['mod_fcgid_maxrequests'] . "\n";
+ } else {
+ $starter_file .= "PHP_FCGI_MAX_REQUESTS=" . (int) Settings::Get('system.mod_fcgid_maxrequests') . "\n";
+ }
+ }
+
+ $starter_file .= "export PHP_FCGI_MAX_REQUESTS\n";
+
+ // Set Binary
+ $starter_file .= "exec " . $phpconfig['binary'] . " -c " . escapeshellarg($this->getConfigDir()) . "\n";
+
+ // remove +i attibute, so starter can be overwritten
+ if (file_exists($this->getStarterFile())) {
+ \Froxlor\FileDir::removeImmutable($this->getStarterFile());
+ }
+
+ $starter_file_handler = fopen($this->getStarterFile(), 'w');
+ fwrite($starter_file_handler, $starter_file);
+ fclose($starter_file_handler);
+ \Froxlor\FileDir::safe_exec('chmod 750 ' . escapeshellarg($this->getStarterFile()));
+ \Froxlor\FileDir::safe_exec('chown ' . $this->domain['guid'] . ':' . $this->domain['guid'] . ' ' . escapeshellarg($this->getStarterFile()));
+ \Froxlor\FileDir::setImmutable($this->getStarterFile());
+ }
+
+ /**
+ * create customized php.ini
+ *
+ * @param array $phpconfig
+ */
+ public function createIniFile($phpconfig)
+ {
+ $openbasedir = '';
+ $openbasedirc = ';';
+
+ if ($this->domain['openbasedir'] == '1') {
+
+ $openbasedirc = '';
+ $_phpappendopenbasedir = '';
+
+ $_custom_openbasedir = explode(':', Settings::Get('system.mod_fcgid_peardir'));
+ foreach ($_custom_openbasedir as $cobd) {
+ $_phpappendopenbasedir .= \Froxlor\Domain\Domain::appendOpenBasedirPath($cobd);
+ }
+
+ $_custom_openbasedir = explode(':', Settings::Get('system.phpappendopenbasedir'));
+ foreach ($_custom_openbasedir as $cobd) {
+ $_phpappendopenbasedir .= \Froxlor\Domain\Domain::appendOpenBasedirPath($cobd);
+ }
+
+ if ($this->domain['openbasedir_path'] == '0' && strstr($this->domain['documentroot'], ":") === false) {
+ $openbasedir = \Froxlor\Domain\Domain::appendOpenBasedirPath($this->domain['documentroot'], true);
+ } else {
+ $openbasedir = \Froxlor\Domain\Domain::appendOpenBasedirPath($this->domain['customerroot'], true);
+ }
+
+ $openbasedir .= \Froxlor\Domain\Domain::appendOpenBasedirPath($this->getTempDir());
+ $openbasedir .= $_phpappendopenbasedir;
+ } else {
+ $openbasedir = 'none';
+ $openbasedirc = ';';
+ }
+
+ $admin = $this->getAdminData($this->domain['adminid']);
+ $php_ini_variables = array(
+ 'SAFE_MODE' => 'Off', // keep this for compatibility, just in case
+ 'PEAR_DIR' => Settings::Get('system.mod_fcgid_peardir'),
+ 'TMP_DIR' => $this->getTempDir(),
+ 'CUSTOMER_EMAIL' => $this->domain['email'],
+ 'ADMIN_EMAIL' => $admin['email'],
+ 'DOMAIN' => $this->domain['domain'],
+ 'CUSTOMER' => $this->domain['loginname'],
+ 'ADMIN' => $admin['loginname'],
+ 'OPEN_BASEDIR' => $openbasedir,
+ 'OPEN_BASEDIR_C' => $openbasedirc,
+ 'OPEN_BASEDIR_GLOBAL' => Settings::Get('system.phpappendopenbasedir'),
+ 'DOCUMENT_ROOT' => \Froxlor\FileDir::makeCorrectDir($this->domain['documentroot']),
+ 'CUSTOMER_HOMEDIR' => \Froxlor\FileDir::makeCorrectDir($this->domain['customerroot'])
+ );
+
+ // insert a small header for the file
+ $phpini_file = ";\n";
+ $phpini_file .= "; php.ini created/changed on " . date("Y.m.d H:i:s") . " for domain '" . $this->domain['domain'] . "' with id #" . $this->domain['id'] . " from php template '" . $phpconfig['description'] . "' with id #" . $phpconfig['id'] . "\n";
+ $phpini_file .= "; Do not change anything in this file, it will be overwritten by the Froxlor Cronjob!\n";
+ $phpini_file .= ";\n\n";
+ $phpini_file .= \Froxlor\PhpHelper::replaceVariables($phpconfig['phpsettings'], $php_ini_variables);
+ $phpini_file = str_replace('"none"', 'none', $phpini_file);
+ // $phpini_file = preg_replace('/\"+/', '"', $phpini_file);
+ $phpini_file_handler = fopen($this->getIniFile(), 'w');
+ fwrite($phpini_file_handler, $phpini_file);
+ fclose($phpini_file_handler);
+ \Froxlor\FileDir::safe_exec('chown root:0 ' . escapeshellarg($this->getIniFile()));
+ \Froxlor\FileDir::safe_exec('chmod 0644 ' . escapeshellarg($this->getIniFile()));
+ }
+
+ /**
+ * fcgid-config directory
+ *
+ * @param boolean $createifnotexists
+ * create the directory if it does not exist
+ *
+ * @return string the directory
+ */
+ public function getConfigDir($createifnotexists = true)
+ {
+ $configdir = \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.mod_fcgid_configdir') . '/' . $this->domain['loginname'] . '/' . $this->domain['domain'] . '/');
+
+ if (! is_dir($configdir) && $createifnotexists) {
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg($configdir));
+ \Froxlor\FileDir::safe_exec('chown ' . $this->domain['guid'] . ':' . $this->domain['guid'] . ' ' . escapeshellarg($configdir));
+ }
+
+ return $configdir;
+ }
+
+ /**
+ * fcgid-temp directory
+ *
+ * @param boolean $createifnotexists
+ * create the directory if it does not exist
+ *
+ * @return string the directory
+ */
+ public function getTempDir($createifnotexists = true)
+ {
+ $tmpdir = \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.mod_fcgid_tmpdir') . '/' . $this->domain['loginname'] . '/');
+
+ if (! is_dir($tmpdir) && $createifnotexists) {
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg($tmpdir));
+ \Froxlor\FileDir::safe_exec('chown -R ' . $this->domain['guid'] . ':' . $this->domain['guid'] . ' ' . escapeshellarg($tmpdir));
+ \Froxlor\FileDir::safe_exec('chmod 0750 ' . escapeshellarg($tmpdir));
+ }
+
+ return $tmpdir;
+ }
+
+ /**
+ * return path of php-starter file
+ *
+ * @return string the directory
+ */
+ public function getStarterFile()
+ {
+ $starter_filename = \Froxlor\FileDir::makeCorrectFile($this->getConfigDir() . '/php-fcgi-starter');
+ return $starter_filename;
+ }
+
+ /**
+ * return path of php.ini file
+ *
+ * @return string full with path file-name
+ */
+ public function getIniFile()
+ {
+ $phpini_filename = \Froxlor\FileDir::makeCorrectFile($this->getConfigDir() . '/php.ini');
+ return $phpini_filename;
+ }
+
+ /**
+ * return the admin-data of a specific admin
+ *
+ * @param int $adminid
+ * id of the admin-user
+ *
+ * @return array
+ */
+ private function getAdminData($adminid)
+ {
+ $adminid = intval($adminid);
+
+ if (! isset($this->admin_cache[$adminid])) {
+ $stmt = Database::prepare("
+ SELECT `email`, `loginname` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `adminid` = :id");
+ $this->admin_cache[$adminid] = Database::pexecute_first($stmt, array(
+ 'id' => $adminid
+ ));
+ }
+ return $this->admin_cache[$adminid];
+ }
+}
diff --git a/lib/Froxlor/Cron/Http/Php/Fpm.php b/lib/Froxlor/Cron/Http/Php/Fpm.php
new file mode 100644
index 00000000..c74cace8
--- /dev/null
+++ b/lib/Froxlor/Cron/Http/Php/Fpm.php
@@ -0,0 +1,416 @@
+
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ * @link http://www.nutime.de/
+ * @since 0.9.16
+ *
+ */
+class Fpm
+{
+
+ /**
+ * Domain-Data array
+ *
+ * @var array
+ */
+ private $domain = array();
+
+ /**
+ * fpm config
+ *
+ * @var array
+ */
+ private $fpm_cfg = array();
+
+ /**
+ * Admin-Date cache array
+ *
+ * @var array
+ */
+ private $admin_cache = array();
+
+ /**
+ * defines what can be used for pool-config from php.ini
+ * Mostly taken from http://php.net/manual/en/ini.list.php
+ *
+ * @var array
+ */
+ private $ini = array();
+
+ /**
+ * main constructor
+ */
+ public function __construct($domain)
+ {
+ if (! isset($domain['fpm_config_id']) || empty($domain['fpm_config_id'])) {
+ $domain['fpm_config_id'] = 1;
+ }
+ $this->domain = $domain;
+ $this->readFpmConfig($domain['fpm_config_id']);
+ $this->buildIniMapping();
+ }
+
+ private function buildIniMapping()
+ {
+ $this->ini = array(
+ 'php_flag' => explode("\n", Settings::Get('phpfpm.ini_flags')),
+ 'php_value' => explode("\n", Settings::Get('phpfpm.ini_values')),
+ 'php_admin_flag' => explode("\n", Settings::Get('phpfpm.ini_admin_flags')),
+ 'php_admin_value' => explode("\n", Settings::Get('phpfpm.ini_admin_values'))
+ );
+ }
+
+ private function readFpmConfig($fpm_config_id)
+ {
+ $stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_FPMDAEMONS . "` WHERE `id` = :id");
+ $this->fpm_cfg = Database::pexecute_first($stmt, array(
+ 'id' => $fpm_config_id
+ ));
+ }
+
+ /**
+ * create fpm-pool config
+ *
+ * @param array $phpconfig
+ */
+ public function createConfig($phpconfig)
+ {
+ $fh = @fopen($this->getConfigFile(), 'w');
+
+ if ($fh) {
+
+ if ($phpconfig['override_fpmconfig'] == 1) {
+ $this->fpm_cfg['pm'] = $phpconfig['pm'];
+ $this->fpm_cfg['max_children'] = $phpconfig['max_children'];
+ $this->fpm_cfg['start_servers'] = $phpconfig['start_servers'];
+ $this->fpm_cfg['min_spare_servers'] = $phpconfig['min_spare_servers'];
+ $this->fpm_cfg['max_spare_servers'] = $phpconfig['max_spare_servers'];
+ $this->fpm_cfg['max_requests'] = $phpconfig['max_requests'];
+ $this->fpm_cfg['idle_timeout'] = $phpconfig['idle_timeout'];
+ $this->fpm_cfg['limit_extensions'] = $phpconfig['limit_extensions'];
+ }
+
+ $fpm_pm = $this->fpm_cfg['pm'];
+ $fpm_children = (int) $this->fpm_cfg['max_children'];
+ $fpm_start_servers = (int) $this->fpm_cfg['start_servers'];
+ $fpm_min_spare_servers = (int) $this->fpm_cfg['min_spare_servers'];
+ $fpm_max_spare_servers = (int) $this->fpm_cfg['max_spare_servers'];
+ $fpm_requests = (int) $this->fpm_cfg['max_requests'];
+ $fpm_process_idle_timeout = (int) $this->fpm_cfg['idle_timeout'];
+ $fpm_limit_extensions = $this->fpm_cfg['limit_extensions'];
+ $fpm_custom_config = $this->fpm_cfg['custom_config'];
+
+ if ($fpm_children == 0) {
+ $fpm_children = 1;
+ }
+
+ $fpm_config = ';PHP-FPM configuration for "' . $this->domain['domain'] . '" created on ' . date("Y.m.d H:i:s") . "\n";
+ $fpm_config .= '[' . $this->domain['domain'] . ']' . "\n";
+ $fpm_config .= 'listen = ' . $this->getSocketFile() . "\n";
+ if ($this->domain['loginname'] == 'froxlor.panel') {
+ $fpm_config .= 'listen.owner = ' . $this->domain['guid'] . "\n";
+ $fpm_config .= 'listen.group = ' . $this->domain['guid'] . "\n";
+ } else {
+ $fpm_config .= 'listen.owner = ' . $this->domain['loginname'] . "\n";
+ $fpm_config .= 'listen.group = ' . $this->domain['loginname'] . "\n";
+ }
+ // see #1418 why this is 0660
+ $fpm_config .= 'listen.mode = 0660' . "\n";
+
+ if ($this->domain['loginname'] == 'froxlor.panel') {
+ $fpm_config .= 'user = ' . $this->domain['guid'] . "\n";
+ $fpm_config .= 'group = ' . $this->domain['guid'] . "\n";
+ } else {
+ $fpm_config .= 'user = ' . $this->domain['loginname'] . "\n";
+ $fpm_config .= 'group = ' . $this->domain['loginname'] . "\n";
+ }
+
+ $fpm_config .= 'pm = ' . $fpm_pm . "\n";
+ $fpm_config .= 'pm.max_children = ' . $fpm_children . "\n";
+
+ if ($fpm_pm == 'dynamic') {
+ // honor max_children
+ if ($fpm_children < $fpm_min_spare_servers) {
+ $fpm_min_spare_servers = $fpm_children;
+ }
+ if ($fpm_children < $fpm_max_spare_servers) {
+ $fpm_max_spare_servers = $fpm_children;
+ }
+ // failsafe, refs #955
+ if ($fpm_start_servers < $fpm_min_spare_servers) {
+ $fpm_start_servers = $fpm_min_spare_servers;
+ }
+ if ($fpm_start_servers > $fpm_max_spare_servers) {
+ $fpm_start_servers = $fpm_max_spare_servers;
+ }
+ $fpm_config .= 'pm.start_servers = ' . $fpm_start_servers . "\n";
+ $fpm_config .= 'pm.min_spare_servers = ' . $fpm_min_spare_servers . "\n";
+ $fpm_config .= 'pm.max_spare_servers = ' . $fpm_max_spare_servers . "\n";
+ } elseif ($fpm_pm == 'ondemand') {
+ $fpm_config .= 'pm.process_idle_timeout = ' . $fpm_process_idle_timeout . "\n";
+ }
+
+ $fpm_config .= 'pm.max_requests = ' . $fpm_requests . "\n";
+
+ // possible slowlog configs
+ if ($phpconfig['fpm_slowlog'] == '1') {
+ $fpm_config .= 'request_terminate_timeout = ' . $phpconfig['fpm_reqterm'] . "\n";
+ $fpm_config .= 'request_slowlog_timeout = ' . $phpconfig['fpm_reqslow'] . "\n";
+ $slowlog = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.logfiles_directory') . '/' . $this->domain['loginname'] . '-php-slow.log');
+ $fpm_config .= 'slowlog = ' . $slowlog . "\n";
+ $fpm_config .= 'catch_workers_output = yes' . "\n";
+ }
+
+ $fpm_config .= ';chroot = ' . \Froxlor\FileDir::makeCorrectDir($this->domain['documentroot']) . "\n";
+ $fpm_config .= 'security.limit_extensions = ' . $fpm_limit_extensions . "\n";
+
+ $tmpdir = \Froxlor\FileDir::makeCorrectDir(Settings::Get('phpfpm.tmpdir') . '/' . $this->domain['loginname'] . '/');
+ if (! is_dir($tmpdir)) {
+ $this->getTempDir();
+ }
+
+ $env_path = Settings::Get('phpfpm.envpath');
+ if (! empty($env_path)) {
+ $fpm_config .= 'env[PATH] = ' . $env_path . "\n";
+ }
+ $fpm_config .= 'env[TMP] = ' . $tmpdir . "\n";
+ $fpm_config .= 'env[TMPDIR] = ' . $tmpdir . "\n";
+ $fpm_config .= 'env[TEMP] = ' . $tmpdir . "\n";
+
+ $openbasedir = '';
+ if ($this->domain['loginname'] != 'froxlor.panel') {
+ if ($this->domain['openbasedir'] == '1') {
+ $_phpappendopenbasedir = '';
+ $_custom_openbasedir = explode(':', Settings::Get('phpfpm.peardir'));
+ foreach ($_custom_openbasedir as $cobd) {
+ $_phpappendopenbasedir .= \Froxlor\Domain\Domain::appendOpenBasedirPath($cobd);
+ }
+
+ $_custom_openbasedir = explode(':', Settings::Get('system.phpappendopenbasedir'));
+ foreach ($_custom_openbasedir as $cobd) {
+ $_phpappendopenbasedir .= \Froxlor\Domain\Domain::appendOpenBasedirPath($cobd);
+ }
+
+ if ($this->domain['openbasedir_path'] == '0' && strstr($this->domain['documentroot'], ":") === false) {
+ $openbasedir = \Froxlor\Domain\Domain::appendOpenBasedirPath($this->domain['documentroot'], true);
+ } else {
+ $openbasedir = \Froxlor\Domain\Domain::appendOpenBasedirPath($this->domain['customerroot'], true);
+ }
+
+ $openbasedir .= \Froxlor\Domain\Domain::appendOpenBasedirPath($this->getTempDir());
+ $openbasedir .= $_phpappendopenbasedir;
+ }
+ }
+ $fpm_config .= 'php_admin_value[session.save_path] = ' . \Froxlor\FileDir::makeCorrectDir(Settings::Get('phpfpm.tmpdir') . '/' . $this->domain['loginname'] . '/') . "\n";
+ $fpm_config .= 'php_admin_value[upload_tmp_dir] = ' . \Froxlor\FileDir::makeCorrectDir(Settings::Get('phpfpm.tmpdir') . '/' . $this->domain['loginname'] . '/') . "\n";
+
+ $admin = $this->getAdminData($this->domain['adminid']);
+ $php_ini_variables = array(
+ 'SAFE_MODE' => 'Off', // keep this for compatibility, just in case
+ 'PEAR_DIR' => Settings::Get('phpfpm.peardir'),
+ 'TMP_DIR' => $this->getTempDir(),
+ 'CUSTOMER_EMAIL' => $this->domain['email'],
+ 'ADMIN_EMAIL' => $admin['email'],
+ 'DOMAIN' => $this->domain['domain'],
+ 'CUSTOMER' => $this->domain['loginname'],
+ 'ADMIN' => $admin['loginname'],
+ 'OPEN_BASEDIR' => $openbasedir,
+ 'OPEN_BASEDIR_C' => '',
+ 'OPEN_BASEDIR_GLOBAL' => Settings::Get('system.phpappendopenbasedir'),
+ 'DOCUMENT_ROOT' => \Froxlor\FileDir::makeCorrectDir($this->domain['documentroot']),
+ 'CUSTOMER_HOMEDIR' => \Froxlor\FileDir::makeCorrectDir($this->domain['customerroot'])
+ );
+
+ $phpini = \Froxlor\PhpHelper::replaceVariables($phpconfig['phpsettings'], $php_ini_variables);
+ $phpini_array = explode("\n", $phpini);
+
+ $fpm_config .= "\n\n";
+ foreach ($phpini_array as $inisection) {
+ $is = explode("=", $inisection);
+ foreach ($this->ini as $sec => $possibles) {
+ if (in_array(trim($is[0]), $possibles)) {
+ // check explicitly for open_basedir
+ if (trim($is[0]) == 'open_basedir' && $openbasedir == '') {
+ continue;
+ }
+ $fpm_config .= $sec . '[' . trim($is[0]) . '] = ' . trim($is[1]) . "\n";
+ }
+ }
+ }
+
+ // now check if 'sendmail_path' has not beed set in the custom-php.ini
+ // if not we use our fallback-default as usual
+ if (strpos($fpm_config, 'php_admin_value[sendmail_path]') === false) {
+ $fpm_config .= 'php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f ' . $this->domain['email'] . "\n";
+ }
+
+ // append custom phpfpm configuration
+ if (! empty($fpm_custom_config)) {
+ $fpm_config .= "\n; Custom Configuration\n";
+ $fpm_config .= \Froxlor\PhpHelper::replaceVariables($fpm_custom_config, $php_ini_variables);
+ }
+
+ fwrite($fh, $fpm_config, strlen($fpm_config));
+ fclose($fh);
+ }
+ }
+
+ /**
+ * this is done via createConfig as php-fpm defines
+ * the ini-values/flags in its pool-config
+ *
+ * @param string $phpconfig
+ */
+ public function createIniFile($phpconfig)
+ {
+ return;
+ }
+
+ /**
+ * fpm-config file
+ *
+ * @param boolean $createifnotexists
+ * create the directory if it does not exist
+ *
+ * @return string the full path to the file
+ */
+ public function getConfigFile($createifnotexists = true)
+ {
+ $configdir = $this->fpm_cfg['config_dir'];
+ $config = \Froxlor\FileDir::makeCorrectFile($configdir . '/' . $this->domain['domain'] . '.conf');
+
+ if (! is_dir($configdir) && $createifnotexists) {
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg($configdir));
+ }
+
+ return $config;
+ }
+
+ /**
+ * return path of fpm-socket file
+ *
+ * @param boolean $createifnotexists
+ * create the directory if it does not exist
+ *
+ * @return string the full path to the socket
+ */
+ public function getSocketFile($createifnotexists = true)
+ {
+ $socketdir = \Froxlor\FileDir::makeCorrectDir(Settings::Get('phpfpm.fastcgi_ipcdir'));
+ // add fpm-config-id to filename so it's unique for the fpm-daemon and doesn't interfere with running configs when reuilding
+ $socket = strtolower(\Froxlor\FileDir::makeCorrectFile($socketdir . '/' . $this->domain['fpm_config_id'] . '-' . $this->domain['loginname'] . '-' . $this->domain['domain'] . '-php-fpm.socket'));
+
+ if (! is_dir($socketdir) && $createifnotexists) {
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg($socketdir));
+ \Froxlor\FileDir::safe_exec('chown -R ' . Settings::Get('system.httpuser') . ':' . Settings::Get('system.httpgroup') . ' ' . escapeshellarg($socketdir));
+ }
+
+ return $socket;
+ }
+
+ /**
+ * fpm-temp directory
+ *
+ * @param boolean $createifnotexists
+ * create the directory if it does not exist
+ *
+ * @return string the directory
+ */
+ public function getTempDir($createifnotexists = true)
+ {
+ $tmpdir = \Froxlor\FileDir::makeCorrectDir(Settings::Get('phpfpm.tmpdir') . '/' . $this->domain['loginname'] . '/');
+
+ if (! is_dir($tmpdir) && $createifnotexists) {
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg($tmpdir));
+ \Froxlor\FileDir::safe_exec('chown -R ' . $this->domain['guid'] . ':' . $this->domain['guid'] . ' ' . escapeshellarg($tmpdir));
+ \Froxlor\FileDir::safe_exec('chmod 0750 ' . escapeshellarg($tmpdir));
+ }
+
+ return $tmpdir;
+ }
+
+ /**
+ * fastcgi-fakedirectory directory
+ *
+ * @param boolean $createifnotexists
+ * create the directory if it does not exist
+ *
+ * @return string the directory
+ */
+ public function getAliasConfigDir($createifnotexists = true)
+ {
+
+ // ensure default...
+ if (Settings::Get('phpfpm.aliasconfigdir') == null) {
+ Settings::Set('phpfpm.aliasconfigdir', '/var/www/php-fpm');
+ }
+
+ $configdir = \Froxlor\FileDir::makeCorrectDir(Settings::Get('phpfpm.aliasconfigdir') . '/' . $this->domain['loginname'] . '/' . $this->domain['domain'] . '/');
+ if (! is_dir($configdir) && $createifnotexists) {
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg($configdir));
+ \Froxlor\FileDir::safe_exec('chown ' . $this->domain['guid'] . ':' . $this->domain['guid'] . ' ' . escapeshellarg($configdir));
+ }
+
+ return $configdir;
+ }
+
+ /**
+ * create a dummy fpm pool config with minimal configuration
+ * (this is used whenever a config directory is empty but needs at least one pool to startup/restart)
+ *
+ * @param string $configdir
+ */
+ public static function createDummyPool($configdir)
+ {
+ if (! is_dir($configdir)) {
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg($configdir));
+ }
+ $config = \Froxlor\FileDir::makeCorrectFile($configdir . '/dummy.conf');
+ $dummy = "[dummy]
+user = " . Settings::Get('system.httpuser') . "
+listen = /run/" . md5($configdir) . "-fpm.sock
+pm = static
+pm.max_children = 1
+";
+ file_put_contents($config, $dummy);
+ }
+
+ /**
+ * return the admin-data of a specific admin
+ *
+ * @param int $adminid
+ * id of the admin-user
+ *
+ * @return array
+ */
+ private function getAdminData($adminid)
+ {
+ $adminid = intval($adminid);
+
+ if (! isset($this->admin_cache[$adminid])) {
+ $stmt = Database::prepare("
+ SELECT `email`, `loginname` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `adminid` = :id");
+ $this->admin_cache[$adminid] = Database::pexecute_first($stmt, array(
+ 'id' => $adminid
+ ));
+ }
+ return $this->admin_cache[$adminid];
+ }
+}
diff --git a/lib/Froxlor/Cron/Http/Php/PhpInterface.php b/lib/Froxlor/Cron/Http/Php/PhpInterface.php
new file mode 100644
index 00000000..ae57f5f9
--- /dev/null
+++ b/lib/Froxlor/Cron/Http/Php/PhpInterface.php
@@ -0,0 +1,121 @@
+
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ * @link http://www.nutime.de/
+ * @since 0.9.16
+ *
+ */
+class PhpInterface
+{
+
+ /**
+ * Domain-Data array
+ *
+ * @var array
+ */
+ private $domain = array();
+
+ /**
+ * Interface object
+ *
+ * @var object
+ */
+ private $interface = null;
+
+ /**
+ * Admin-User data array
+ *
+ * @var array
+ */
+ private $admin_cache = array();
+
+ /**
+ * main constructor
+ */
+ public function __construct($domain)
+ {
+ $this->domain = $domain;
+ $this->setInterface();
+ }
+
+ /**
+ * returns the interface-object
+ * from where we can control it
+ */
+ public function getInterface()
+ {
+ return $this->interface;
+ }
+
+ /**
+ * set interface-object by type of
+ * php-interface: fcgid or php-fpm
+ * sets private $_interface variable
+ */
+ private function setInterface()
+ {
+ // php-fpm
+ if ((int) Settings::Get('phpfpm.enabled') == 1) {
+ $this->interface = new Fpm($this->domain);
+ } elseif ((int) Settings::Get('system.mod_fcgid') == 1) {
+ $this->interface = new Fcgid($this->domain);
+ }
+ }
+
+ /**
+ * return the php-configuration from the database
+ *
+ * @param int $php_config_id
+ * id of the php-configuration
+ *
+ * @return array
+ */
+ public function getPhpConfig($php_config_id)
+ {
+ $php_config_id = intval($php_config_id);
+
+ // If domain has no config, we will use the default one.
+ if ($php_config_id == 0) {
+ $php_config_id = 1;
+ }
+
+ if (! isset($this->php_configs_cache[$php_config_id])) {
+ $stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_PHPCONFIGS . "` WHERE `id` = :id");
+ $this->_php_configs_cache[$php_config_id] = Database::pexecute_first($stmt, array(
+ 'id' => $php_config_id
+ ));
+ if ((int) Settings::Get('phpfpm.enabled') == 1) {
+ $stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_FPMDAEMONS . "` WHERE `id` = :id");
+ $this->_php_configs_cache[$php_config_id]['fpm_settings'] = Database::pexecute_first($stmt, array(
+ 'id' => $this->_php_configs_cache[$php_config_id]['fpmsettingid']
+ ));
+ // override fpm daemon settings if set in php-config
+ if ($this->_php_configs_cache[$php_config_id]['override_fpmconfig'] == 1) {
+ $this->_php_configs_cache[$php_config_id]['fpm_settings']['limit_extensions'] = $this->_php_configs_cache[$php_config_id]['limit_extensions'];
+ $this->_php_configs_cache[$php_config_id]['fpm_settings']['idle_timeout'] = $this->_php_configs_cache[$php_config_id]['idle_timeout'];
+ }
+ }
+ }
+
+ return $this->_php_configs_cache[$php_config_id];
+ }
+}
diff --git a/lib/Froxlor/Cron/Http/WebserverBase.php b/lib/Froxlor/Cron/Http/WebserverBase.php
new file mode 100644
index 00000000..fbca2677
--- /dev/null
+++ b/lib/Froxlor/Cron/Http/WebserverBase.php
@@ -0,0 +1,112 @@
+
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ * @since 0.9.31
+ *
+ */
+class WebserverBase
+{
+
+ /**
+ * returns an array with all entries required for all
+ * webserver-vhost-configs
+ *
+ * @return array
+ */
+ public static function getVhostsToCreate()
+ {
+ $query = "SELECT `d`.*, `pd`.`domain` AS `parentdomain`, `c`.`loginname`,
+ `d`.`phpsettingid`, `c`.`adminid`, `c`.`guid`, `c`.`email`,
+ `c`.`documentroot` AS `customerroot`, `c`.`deactivated`,
+ `c`.`phpenabled` AS `phpenabled_customer`,
+ `d`.`phpenabled` AS `phpenabled_vhost`
+ FROM `" . TABLE_PANEL_DOMAINS . "` `d`
+
+ LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` `c` USING(`customerid`)
+ LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` `pd` ON (`pd`.`id` = `d`.`parentdomainid`)
+
+ WHERE `d`.`aliasdomain` IS NULL AND `d`.`email_only` <> '1'
+ ORDER BY `d`.`parentdomainid` DESC, `d`.`iswildcarddomain`, `d`.`domain` ASC;
+ ";
+
+ $result_domains_stmt = Database::query($query);
+
+ // prepare IP statement
+ $ip_stmt = Database::prepare("
+ SELECT `di`.`id_domain` , `p`.`ssl`, `p`.`ssl_cert_file`, `p`.`ssl_key_file`, `p`.`ssl_ca_file`, `p`.`ssl_cert_chainfile`
+ FROM `" . TABLE_DOMAINTOIP . "` `di`, `" . TABLE_PANEL_IPSANDPORTS . "` `p`
+ WHERE `p`.`id` = `di`.`id_ipandports`
+ AND `di`.`id_domain` = :domainid
+ AND `p`.`ssl` = '1'
+ ");
+
+ // prepare fpm-config select query
+ $fpm_sel_stmt = Database::prepare("
+ SELECT f.id FROM `" . TABLE_PANEL_FPMDAEMONS . "` f
+ LEFT JOIN `" . TABLE_PANEL_PHPCONFIGS . "` p ON p.fpmsettingid = f.id
+ WHERE p.id = :phpconfigid
+ ");
+
+ $domains = array();
+ while ($domain = $result_domains_stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ // set whole domain
+ $domains[$domain['domain']] = $domain;
+ // set empty-defaults for non-ssl
+ $domains[$domain['domain']]['ssl'] = '';
+ $domains[$domain['domain']]['ssl_cert_file'] = '';
+ $domains[$domain['domain']]['ssl_key_file'] = '';
+ $domains[$domain['domain']]['ssl_ca_file'] = '';
+ $domains[$domain['domain']]['ssl_cert_chainfile'] = '';
+
+ // now, if the domain has an ssl ip/port assigned, get
+ // the corresponding information from the db
+ if (\Froxlor\Domain\Domain::domainHasSslIpPort($domain['id'])) {
+
+ $ssl_ip = Database::pexecute_first($ip_stmt, array(
+ 'domainid' => $domain['id']
+ ));
+
+ // set ssl info for domain
+ $domains[$domain['domain']]['ssl'] = '1';
+ $domains[$domain['domain']]['ssl_cert_file'] = $ssl_ip['ssl_cert_file'];
+ $domains[$domain['domain']]['ssl_key_file'] = $ssl_ip['ssl_key_file'];
+ $domains[$domain['domain']]['ssl_ca_file'] = $ssl_ip['ssl_ca_file'];
+ $domains[$domain['domain']]['ssl_cert_chainfile'] = $ssl_ip['ssl_cert_chainfile'];
+ }
+
+ // read fpm-config-id if using fpm
+ if ((int) Settings::Get('phpfpm.enabled') == 1) {
+
+ $fpm_config = Database::pexecute_first($fpm_sel_stmt, array(
+ 'phpconfigid' => $domain['phpsettingid']
+ ));
+ if ($fpm_config) {
+ $domains[$domain['domain']]['fpm_config_id'] = $fpm_config['id'];
+ } else {
+ // fallback
+ $domains[$domain['domain']]['fpm_config_id'] = 1;
+ }
+ }
+ }
+
+ return $domains;
+ }
+}
diff --git a/lib/Froxlor/Cron/MasterCron.php b/lib/Froxlor/Cron/MasterCron.php
new file mode 100644
index 00000000..0c4c88e4
--- /dev/null
+++ b/lib/Froxlor/Cron/MasterCron.php
@@ -0,0 +1,366 @@
+ (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ */
+class MasterCron extends \Froxlor\Cron\FroxlorCron
+{
+
+ private static $argv = null;
+
+ private static $debugHandler = null;
+
+ public static function setArguments($argv = null)
+ {
+ self::$argv = $argv;
+ }
+
+ public static function run()
+ {
+ self::init();
+
+ $jobs_to_run = array();
+
+ $argv = self::$argv;
+ /**
+ * check for --help
+ */
+ if (count($argv) < 2 || (isset($argv[1]) && strtolower($argv[1]) == '--help')) {
+ echo "\n*** Froxlor Master Cronjob ***\n\n";
+ echo "Below are possible parameters for this file\n\n";
+ echo "--[cronname]\t\tincludes the given cron-file\n";
+ echo "--force\t\t\tforces re-generating of config-files (webserver, nameserver, etc.)\n";
+ echo "--run-task\t\trun a specific task [1 = re-generate configs, 4 = re-generate dns zones, 10 = re-set quotas, 99 = re-create cron.d-file]\n";
+ echo "--debug\t\t\toutput debug information about what is going on to STDOUT.\n";
+ echo "--no-fork\t\tdo not fork to backkground (traffic cron only).\n\n";
+ }
+
+ /**
+ * check for parameters
+ *
+ * --[cronname] include [cronname]
+ * --force to include cron_tasks even if it's not its turn
+ * --debug to output debug information
+ */
+ for ($x = 1; $x < count($argv); $x ++) {
+ // check argument
+ if (isset($argv[$x])) {
+ // --force
+ if (strtolower($argv[$x]) == '--force') {
+ // really force re-generating of config-files by
+ // inserting task 1
+ \Froxlor\System\Cronjob::inserttask('1');
+ // bind (if enabled, \Froxlor\System\Cronjob::inserttask() checks this)
+ \Froxlor\System\Cronjob::inserttask('4');
+ // set quotas (if enabled)
+ \Froxlor\System\Cronjob::inserttask('10');
+ // also regenerate cron.d-file
+ \Froxlor\System\Cronjob::inserttask('99');
+ array_push($jobs_to_run, 'tasks');
+ define('CRON_IS_FORCED', 1);
+ } elseif (strtolower($argv[$x]) == '--debug') {
+ define('CRON_DEBUG_FLAG', 1);
+ } elseif (strtolower($argv[$x]) == '--no-fork') {
+ define('CRON_NOFORK_FLAG', 1);
+ } elseif (strtolower($argv[$x]) == '--run-task') {
+ if (isset($argv[$x+1]) && in_array($argv[$x+1], [1,4,10,99])) {
+ \Froxlor\System\Cronjob::inserttask($argv[$x+1]);
+ array_push($jobs_to_run, 'tasks');
+ } else {
+ echo "Invalid argument for --run-task\n";
+ exit;
+ }
+ } elseif (substr(strtolower($argv[$x]), 0, 2) == '--') {
+ // --[cronname]
+ if (strlen($argv[$x]) > 3) {
+ $cronname = substr(strtolower($argv[$x]), 2);
+ array_push($jobs_to_run, $cronname);
+ }
+ }
+ }
+ }
+
+ $jobs_to_run = array_unique($jobs_to_run);
+
+ self::$cronlog->setCronDebugFlag(defined('CRON_DEBUG_FLAG'));
+
+ $tasks_cnt_stmt = \Froxlor\Database\Database::query("SELECT COUNT(*) as jobcnt FROM `panel_tasks`");
+ $tasks_cnt = $tasks_cnt_stmt->fetch(\PDO::FETCH_ASSOC);
+
+ // do we have anything to include?
+ if (count($jobs_to_run) > 0) {
+ // include all jobs we want to execute
+ foreach ($jobs_to_run as $cron) {
+ \Froxlor\System\Cronjob::updateLastRunOfCron($cron);
+ $cronfile = self::getCronModule($cron);
+ if ($cronfile && class_exists($cronfile)) {
+ $cronfile::run();
+ }
+ }
+ self::refreshUsers($tasks_cnt['jobcnt']);
+ }
+
+ /**
+ * we have to check the system's last guid with every cron run
+ * in case the admin installed new software which added a new user
+ * so users in the database don't conflict with system users
+ */
+ self::$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Checking system\'s last guid');
+ \Froxlor\System\Cronjob::checkLastGuid();
+
+ // shutdown cron
+ self::shutdown();
+ }
+
+ private static function refreshUsers($jobcount = 0)
+ {
+ if ($jobcount > 0) {
+ if (\Froxlor\Settings::Get('system.nssextrausers') == 1) {
+ \Froxlor\Cron\System\Extrausers::generateFiles(self::$cronlog);
+ }
+
+ // clear NSCD cache if using fcgid or fpm, #1570 - not needed for nss-extrausers
+ if ((\Froxlor\Settings::Get('system.mod_fcgid') == 1 || (int) \Froxlor\Settings::Get('phpfpm.enabled') == 1) && \Froxlor\Settings::Get('system.nssextrausers') == 0) {
+ $false_val = false;
+ \Froxlor\FileDir::safe_exec('nscd -i passwd 1> /dev/null', $false_val, array(
+ '>'
+ ));
+ \Froxlor\FileDir::safe_exec('nscd -i group 1> /dev/null', $false_val, array(
+ '>'
+ ));
+ }
+ }
+ }
+
+ private static function init()
+ {
+ if (@php_sapi_name() != 'cli' && @php_sapi_name() != 'cgi' && @php_sapi_name() != 'cgi-fcgi') {
+ die('This script will only work in the shell.');
+ }
+
+ // ensure that default timezone is set
+ if (function_exists("date_default_timezone_set") && function_exists("date_default_timezone_get")) {
+ @date_default_timezone_set(@date_default_timezone_get());
+ }
+
+ $basename = basename($_SERVER['PHP_SELF'], '.php');
+ $crontype = "";
+ if (isset(self::$argv) && is_array(self::$argv) && count(self::$argv) > 1) {
+ for ($x = 1; $x < count(self::$argv); $x ++) {
+ if (substr(strtolower(self::$argv[$x]), 0, 2) == '--' && strlen(self::$argv[$x]) > 3) {
+ $crontype = substr(strtolower(self::$argv[$x]), 2);
+ $basename .= "-" . $crontype;
+ break;
+ }
+ }
+ }
+ $lockdir = '/var/run/';
+ $lockFilename = 'froxlor_' . $basename . '.lock-';
+ $lockfName = $lockFilename . getmypid();
+ $lockfile = $lockdir . $lockfName;
+ self::setLockfile($lockfile);
+
+ // create and open the lockfile!
+ self::$debugHandler = fopen($lockfile, 'w');
+ fwrite(self::$debugHandler, 'Setting Lockfile to ' . $lockfile . "\n");
+ fwrite(self::$debugHandler, 'Setting Froxlor installation path to ' . \Froxlor\Froxlor::getInstallDir() . "\n");
+
+ if (! file_exists(\Froxlor\Froxlor::getInstallDir() . '/lib/userdata.inc.php')) {
+ die("Froxlor does not seem to be installed yet - skipping cronjob");
+ }
+
+ $sql = array();
+ $sql_root = array();
+ // Includes the Usersettings eg. MySQL-Username/Passwort etc.
+ require \Froxlor\Froxlor::getInstallDir() . '/lib/userdata.inc.php';
+ fwrite(self::$debugHandler, 'Userdatas included' . "\n");
+
+ // Legacy sql-root-information
+ if (isset($sql['root_user']) && isset($sql['root_password']) && (! isset($sql_root) || ! is_array($sql_root))) {
+ $sql_root = array(
+ 0 => array(
+ 'caption' => 'Default',
+ 'host' => $sql['host'],
+ 'user' => $sql['root_user'],
+ 'password' => $sql['root_password']
+ )
+ );
+ unset($sql['root_user']);
+ unset($sql['root_password']);
+ }
+
+ // Includes the MySQL-Tabledefinitions etc.
+ require \Froxlor\Froxlor::getInstallDir() . '/lib/tables.inc.php';
+ fwrite(self::$debugHandler, 'Table definitions included' . "\n");
+
+ // try database connection, it will throw
+ // and exception itself if failed
+ try {
+ \Froxlor\Database\Database::query("SELECT 1");
+ } catch (\Exception $e) {
+ // Do not proceed further if no database connection could be established
+ fclose(self::$debugHandler);
+ unlink($lockfile);
+ die($e->getMessage());
+ }
+
+ fwrite(self::$debugHandler, 'Database-connection established' . "\n");
+
+ // open the lockfile directory and scan for existing lockfiles
+ $lockDirHandle = opendir($lockdir);
+
+ while ($fName = readdir($lockDirHandle)) {
+
+ if ($lockFilename == substr($fName, 0, strlen($lockFilename)) && $lockfName != $fName) {
+ // Check if last run jailed out with an exception
+ $croncontent = file($lockdir . $fName);
+ $lastline = $croncontent[(count($croncontent) - 1)];
+
+ if ($lastline == '=== Keep lockfile because of exception ===') {
+ fclose(self::$debugHandler);
+ unlink($lockfile);
+ \Froxlor\System\Cronjob::dieWithMail('Last cron jailed out with an exception. Exiting...' . "\n" . 'Take a look into the contents of ' . $lockdir . $fName . '* for more information!' . "\n");
+ }
+
+ // Check if cron is running or has died.
+ $check_pid = substr(strrchr($fName, "-"), 1);
+ $check_pid_return = null;
+ system("kill -CHLD " . (int) $check_pid . " 1> /dev/null 2> /dev/null", $check_pid_return);
+
+ if ($check_pid_return == 1) {
+ // Result: Existing lockfile/pid isn't running
+ // Most likely it has died
+ //
+ // Action: Remove it and continue
+ //
+ fwrite(self::$debugHandler, 'Previous cronjob didn\'t exit clean. PID: ' . $check_pid . "\n");
+ fwrite(self::$debugHandler, 'Removing lockfile: ' . $lockdir . $fName . "\n");
+ @unlink($lockdir . $fName);
+ } else {
+ // Result: A Cronscript with this pid
+ // is still running
+ // Action: remove my own Lock and die
+ //
+ // close the current lockfile
+ fclose(self::$debugHandler);
+
+ // ... and delete it
+ unlink($lockfile);
+ \Froxlor\System\Cronjob::dieWithMail('There is already a Cronjob for ' . $crontype . ' in progress. Exiting...' . "\n" . 'Take a look into the contents of ' . $lockdir . $lockFilename . '* for more information!' . "\n");
+ }
+ }
+ }
+
+ /**
+ * if using fcgid or fpm for froxlor-vhost itself, we have to check
+ * whether the permission of the files are still correct
+ */
+ fwrite(self::$debugHandler, 'Checking froxlor file permissions' . "\n");
+ $_mypath = \Froxlor\FileDir::makeCorrectDir(\Froxlor\Froxlor::getInstallDir());
+
+ if (((int) \Froxlor\Settings::Get('system.mod_fcgid') == 1 && (int) \Froxlor\Settings::Get('system.mod_fcgid_ownvhost') == 1) || ((int) \Froxlor\Settings::Get('phpfpm.enabled') == 1 && (int) \Froxlor\Settings::Get('phpfpm.enabled_ownvhost') == 1)) {
+ $user = \Froxlor\Settings::Get('system.mod_fcgid_httpuser');
+ $group = \Froxlor\Settings::Get('system.mod_fcgid_httpgroup');
+
+ if (\Froxlor\Settings::Get('phpfpm.enabled') == 1) {
+ $user = \Froxlor\Settings::Get('phpfpm.vhost_httpuser');
+ $group = \Froxlor\Settings::Get('phpfpm.vhost_httpgroup');
+ }
+ // all the files and folders have to belong to the local user
+ // now because we also use fcgid for our own vhost
+ \Froxlor\FileDir::safe_exec('chown -R ' . $user . ':' . $group . ' ' . escapeshellarg($_mypath));
+ } else {
+ // back to webserver permission
+ $user = \Froxlor\Settings::Get('system.httpuser');
+ $group = \Froxlor\Settings::Get('system.httpgroup');
+ \Froxlor\FileDir::safe_exec('chown -R ' . $user . ':' . $group . ' ' . escapeshellarg($_mypath));
+ }
+
+ // Initialize logging
+ self::$cronlog = \Froxlor\FroxlorLogger::getInstanceOf(array(
+ 'loginname' => 'cronjob'
+ ));
+ fwrite(self::$debugHandler, 'Logger has been included' . "\n");
+
+ if (\Froxlor\Froxlor::hasUpdates() || \Froxlor\Froxlor::hasDbUpdates()) {
+ if (\Froxlor\Settings::Get('system.cron_allowautoupdate') == null || \Froxlor\Settings::Get('system.cron_allowautoupdate') == 0) {
+ /**
+ * Do not proceed further if the Database version is not the same as the script version
+ */
+ fclose(self::$debugHandler);
+ unlink($lockfile);
+ $errormessage = "Version of file doesn't match version of database. Exiting...\n\n";
+ $errormessage .= "Possible reason: Froxlor update\n";
+ $errormessage .= "Information: Current version in database: " . \Froxlor\Settings::Get('panel.version') . (! empty(\Froxlor\Froxlor::BRANDING) ? "-" . \Froxlor\Froxlor::BRANDING : "") . " (DB: " . \Froxlor\Settings::Get('panel.db_version') . ") - version of Froxlor files: " . \Froxlor\Froxlor::getVersionString() . ")\n";
+ $errormessage .= "Solution: Please visit your Foxlor admin interface for further information.\n";
+ \Froxlor\System\Cronjob::dieWithMail($errormessage);
+ }
+
+ if (\Froxlor\Settings::Get('system.cron_allowautoupdate') == 1) {
+ /**
+ * let's walk the walk - do the dangerous shit
+ */
+ self::$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_WARNING, 'Automatic update is activated and we are going to proceed without any notices');
+ self::$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_WARNING, 'all new settings etc. will be stored with the default value, that might not always be right for your system!');
+ self::$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_WARNING, "If you don't want this to happen in the future consider removing the --allow-autoupdate flag from the cronjob");
+ fwrite(self::$debugHandler, '*** WARNING *** - Automatic update is activated and we are going to proceed without any notices' . "\n");
+ fwrite(self::$debugHandler, '*** WARNING *** - all new settings etc. will be stored with the default value, that might not always be right for your system!' . "\n");
+ fwrite(self::$debugHandler, "*** WARNING *** - If you don't want this to happen in the future consider removing the --allow-autoupdate flag from the cronjob\n");
+ // including update procedures
+ define('_CRON_UPDATE', 1);
+ include_once \Froxlor\Froxlor::getInstallDir() . '/install/updatesql.php';
+ // pew - everything went better than expected
+ self::$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_WARNING, 'Automatic update done - you should check your settings to be sure everything is fine');
+ fwrite(self::$debugHandler, '*** WARNING *** - Automatic update done - you should check your settings to be sure everything is fine' . "\n");
+ }
+ }
+
+ fwrite(self::$debugHandler, 'Froxlor version and database version are correct' . "\n");
+ }
+
+ private static function shutdown()
+ {
+ // check for cron.d-generation task and create it if necessary
+ \Froxlor\Cron\CronConfig::checkCrondConfigurationFile();
+
+ if (\Froxlor\Settings::Get('logger.log_cron') == '1') {
+ \Froxlor\FroxlorLogger::getInstanceOf()->setCronLog(0);
+ fwrite(self::$debugHandler, 'Logging for cron has been shutdown' . "\n");
+ }
+
+ fclose(self::$debugHandler);
+
+ if (\Froxlor\Settings::Get('system.debug_cron') != '1') {
+ unlink(self::getLockfile());
+ }
+ }
+
+ private static function getCronModule($cronname)
+ {
+ $upd_stmt = Database::prepare("
+ SELECT `cronclass` FROM `" . TABLE_PANEL_CRONRUNS . "` WHERE `cronfile` = :cron;
+ ");
+ $cron = Database::pexecute_first($upd_stmt, array(
+ 'cron' => $cronname
+ ));
+ if ($cron) {
+ return $cron['cronclass'];
+ }
+ self::$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, "Requested cronjob '" . $cronname . "' could not be found.");
+ return false;
+ }
+}
diff --git a/lib/Froxlor/Cron/System/BackupCron.php b/lib/Froxlor/Cron/System/BackupCron.php
new file mode 100644
index 00000000..899e7717
--- /dev/null
+++ b/lib/Froxlor/Cron/System/BackupCron.php
@@ -0,0 +1,223 @@
+
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ * @since 0.9.35.1
+ *
+ */
+class BackupCron extends \Froxlor\Cron\FroxlorCron
+{
+
+ public static function run()
+ {
+ // Check Traffic-Lock
+ if (function_exists('pcntl_fork')) {
+ $BackupLock = \Froxlor\FileDir::makeCorrectFile(dirname(self::getLockfile()) . "/froxlor_cron_backup.lock");
+ if (file_exists($BackupLock) && is_numeric($BackupPid = file_get_contents($BackupLock))) {
+ if (function_exists('posix_kill')) {
+ $BackupPidStatus = @posix_kill($BackupPid, 0);
+ } else {
+ system("kill -CHLD " . $BackupPid . " 1> /dev/null 2> /dev/null", $BackupPidStatus);
+ $BackupPidStatus = $BackupPidStatus ? false : true;
+ }
+ if ($BackupPidStatus) {
+ FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'Backup run already in progress');
+ return 1;
+ }
+ }
+ // Create Backup Log and Fork
+ // We close the database - connection before we fork, so we don't share resources with the child
+ Database::needRoot(false); // this forces the connection to be set to null
+ $BackupPid = pcntl_fork();
+ // Parent
+ if ($BackupPid) {
+ file_put_contents($BackupLock, $BackupPid);
+ // unnecessary to recreate database connection here
+ return 0;
+ } elseif ($BackupPid == 0) {
+ // Child
+ posix_setsid();
+ // re-create db
+ Database::needRoot(false);
+ } else {
+ // Fork failed
+ return 1;
+ }
+ } else {
+ if (extension_loaded('pcntl')) {
+ $msg = "PHP compiled with pcntl but pcntl_fork function is not available.";
+ } else {
+ $msg = "PHP compiled without pcntl.";
+ }
+ FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_WARNING, $msg . " Not forking backup-cron, this may take a long time!");
+ }
+
+ FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'BackupCron: started - creating customer backup');
+
+ $result_tasks_stmt = Database::query("
+ SELECT * FROM `" . TABLE_PANEL_TASKS . "` WHERE `type` = '20' ORDER BY `id` ASC
+ ");
+
+ $del_stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_TASKS . "` WHERE `id` = :id");
+
+ $cronlog = FroxlorLogger::getInstanceOf();
+ $all_jobs = $result_tasks_stmt->fetchAll();
+ foreach ($all_jobs as $row) {
+
+ if ($row['data'] != '') {
+ $row['data'] = json_decode($row['data'], true);
+ }
+
+ if (is_array($row['data'])) {
+
+ if (isset($row['data']['customerid']) && isset($row['data']['loginname']) && isset($row['data']['destdir'])) {
+ $row['data']['destdir'] = \Froxlor\FileDir::makeCorrectDir($row['data']['destdir']);
+ $customerdocroot = \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.documentroot_prefix') . '/' . $row['data']['loginname'] . '/');
+
+ // create folder if not exists
+ if (! file_exists($row['data']['destdir']) && $row['data']['destdir'] != '/' && $row['data']['destdir'] != Settings::Get('system.documentroot_prefix') && $row['data']['destdir'] != $customerdocroot) {
+ FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Creating backup-destination path for customer: ' . escapeshellarg($row['data']['destdir']));
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg($row['data']['destdir']));
+ }
+
+ self::createCustomerBackup($row['data'], $customerdocroot, $cronlog);
+ }
+ }
+
+ // remove entry
+ Database::pexecute($del_stmt, array(
+ 'id' => $row['id']
+ ));
+ }
+
+ if (function_exists('pcntl_fork')) {
+ @unlink($BackupLock);
+ die();
+ }
+ }
+
+ /**
+ * depending on the give choice, the customers web-data, email-data and databases are being backup'ed
+ *
+ * @param array $data
+ *
+ * @return void
+ *
+ */
+ private static function createCustomerBackup($data = null, $customerdocroot = null, &$cronlog = null)
+ {
+ $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'Creating Backup for user "' . $data['loginname'] . '"');
+
+ // create tmp folder
+ $tmpdir = \Froxlor\FileDir::makeCorrectDir($data['destdir'] . '/.tmp/');
+ $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'Creating tmp-folder "' . $tmpdir . '"');
+ $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'shell> mkdir -p ' . escapeshellarg($tmpdir));
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg($tmpdir));
+ $create_backup_tar_data = "";
+
+ // MySQL databases
+ if ($data['backup_dbs'] == 1) {
+
+ $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'Creating mysql-folder "' . \Froxlor\FileDir::makeCorrectDir($tmpdir . '/mysql') . '"');
+ $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'shell> mkdir -p ' . escapeshellarg(\Froxlor\FileDir::makeCorrectDir($tmpdir . '/mysql')));
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg(\Froxlor\FileDir::makeCorrectDir($tmpdir . '/mysql')));
+
+ // get all customer database-names
+ $sel_stmt = Database::prepare("SELECT `databasename` FROM `" . TABLE_PANEL_DATABASES . "` WHERE `customerid` = :cid");
+ Database::pexecute($sel_stmt, array(
+ 'cid' => $data['customerid']
+ ));
+
+ Database::needRoot(true);
+ Database::needSqlData();
+ $sql_root = Database::getSqlData();
+ Database::needRoot(false);
+
+ $has_dbs = false;
+ while ($row = $sel_stmt->fetch()) {
+ $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'shell> mysqldump -u ' . escapeshellarg($sql_root['user']) . ' -pXXXXX ' . $row['databasename'] . ' > ' . \Froxlor\FileDir::makeCorrectFile($tmpdir . '/mysql/' . $row['databasename'] . '_' . date('YmdHi', time()) . '.sql'));
+ $bool_false = false;
+ \Froxlor\FileDir::safe_exec('mysqldump -u ' . escapeshellarg($sql_root['user']) . ' -p' . $sql_root['passwd'] . ' ' . $row['databasename'] . ' > ' . \Froxlor\FileDir::makeCorrectFile($tmpdir . '/mysql/' . $row['databasename'] . '_' . date('YmdHi', time()) . '.sql'), $bool_false, array(
+ '>'
+ ));
+ $has_dbs = true;
+ }
+
+ if ($has_dbs) {
+ $create_backup_tar_data .= './mysql ';
+ }
+
+ unset($sql_root);
+ }
+
+ // E-mail data
+ if ($data['backup_mail'] == 1) {
+
+ $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'Creating mail-folder "' . \Froxlor\FileDir::makeCorrectDir($tmpdir . '/mail') . '"');
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg(\Froxlor\FileDir::makeCorrectDir($tmpdir . '/mail')));
+
+ // get all customer mail-accounts
+ $sel_stmt = Database::prepare("SELECT `homedir`, `maildir` FROM `" . TABLE_MAIL_USERS . "` WHERE `customerid` = :cid");
+ Database::pexecute($sel_stmt, array(
+ 'cid' => $data['customerid']
+ ));
+
+ $tar_file_list = "";
+ $mail_homedir = "";
+ while ($row = $sel_stmt->fetch()) {
+ $tar_file_list .= escapeshellarg("./" . $row['maildir']) . " ";
+ $mail_homedir = $row['homedir'];
+ }
+
+ if (! empty($tar_file_list)) {
+ $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'shell> tar cfvz ' . escapeshellarg(\Froxlor\FileDir::makeCorrectFile($tmpdir . '/mail/' . $data['loginname'] . '-mail.tar.gz')) . ' -C ' . escapeshellarg($mail_homedir) . ' ' . trim($tar_file_list));
+ \Froxlor\FileDir::safe_exec('tar cfz ' . escapeshellarg(\Froxlor\FileDir::makeCorrectFile($tmpdir . '/mail/' . $data['loginname'] . '-mail.tar.gz')) . ' -C ' . escapeshellarg($mail_homedir) . ' ' . trim($tar_file_list));
+ $create_backup_tar_data .= './mail ';
+ }
+ }
+
+ // Web data
+ if ($data['backup_web'] == 1) {
+
+ $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'Creating web-folder "' . \Froxlor\FileDir::makeCorrectDir($tmpdir . '/web') . '"');
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg(\Froxlor\FileDir::makeCorrectDir($tmpdir . '/web')));
+ $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'shell> tar cfz ' . escapeshellarg(\Froxlor\FileDir::makeCorrectFile($tmpdir . '/web/' . $data['loginname'] . '-web.tar.gz')) . ' --exclude=' . escapeshellarg(str_replace($customerdocroot, "./", \Froxlor\FileDir::makeCorrectFile($tmpdir . '/*'))) . ' --exclude=' . escapeshellarg(str_replace($customerdocroot, "./", substr(\Froxlor\FileDir::makeCorrectDir($tmpdir), 0, - 1))) . ' -C ' . escapeshellarg($customerdocroot) . ' .');
+ \Froxlor\FileDir::safe_exec('tar cfz ' . escapeshellarg(\Froxlor\FileDir::makeCorrectFile($tmpdir . '/web/' . $data['loginname'] . '-web.tar.gz')) . ' --exclude=' . escapeshellarg(str_replace($customerdocroot, "./", \Froxlor\FileDir::makeCorrectFile($tmpdir . '/*'))) . ' --exclude=' . escapeshellarg(str_replace($customerdocroot, "./", substr(\Froxlor\FileDir::makeCorrectFile($tmpdir), 0, - 1))) . ' -C ' . escapeshellarg($customerdocroot) . ' .');
+ $create_backup_tar_data .= './web ';
+ }
+
+ if (! empty($create_backup_tar_data)) {
+ $backup_file = \Froxlor\FileDir::makeCorrectFile($tmpdir . '/' . $data['loginname'] . '-backup_' . date('YmdHi', time()) . '.tar.gz');
+ $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'Creating backup-file "' . $backup_file . '"');
+ // pack all archives in tmp-dir to one
+ $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'shell> tar cfz ' . escapeshellarg($backup_file) . ' -C ' . escapeshellarg($tmpdir) . ' ' . trim($create_backup_tar_data));
+ \Froxlor\FileDir::safe_exec('tar cfz ' . escapeshellarg($backup_file) . ' -C ' . escapeshellarg($tmpdir) . ' ' . trim($create_backup_tar_data));
+ // move to destination directory
+ $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'shell> mv ' . escapeshellarg($backup_file) . ' ' . escapeshellarg($data['destdir']));
+ \Froxlor\FileDir::safe_exec('mv ' . escapeshellarg($backup_file) . ' ' . escapeshellarg($data['destdir']));
+ // remove tmp-files
+ $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'shell> rm -rf ' . escapeshellarg($tmpdir));
+ \Froxlor\FileDir::safe_exec('rm -rf ' . escapeshellarg($tmpdir));
+ // set owner to customer
+ $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'shell> chown -R ' . (int) $data['uid'] . ':' . (int) $data['gid'] . ' ' . escapeshellarg($data['destdir']));
+ \Froxlor\FileDir::safe_exec('chown -R ' . (int) $data['uid'] . ':' . (int) $data['gid'] . ' ' . escapeshellarg($data['destdir']));
+ }
+ }
+}
diff --git a/lib/Froxlor/Cron/System/Extrausers.php b/lib/Froxlor/Cron/System/Extrausers.php
new file mode 100644
index 00000000..74f1aea5
--- /dev/null
+++ b/lib/Froxlor/Cron/System/Extrausers.php
@@ -0,0 +1,99 @@
+ (2017-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ */
+class Extrausers
+{
+
+ public static function generateFiles(&$cronlog)
+ {
+ // passwd
+ $passwd = '/var/lib/extrausers/passwd';
+ $sql = "SELECT customerid,username,'x' as password,uid,gid,'Froxlor User' as comment,homedir,shell, login_enabled FROM ftp_users ORDER BY uid ASC";
+ self::generateFile($passwd, $sql, $cronlog);
+
+ // group
+ $group = '/var/lib/extrausers/group';
+ $sql = "SELECT groupname,'x' as password,gid,members FROM ftp_groups ORDER BY gid ASC";
+ self::generateFile($group, $sql, $cronlog);
+
+ // shadow
+ $shadow = '/var/lib/extrausers/shadow';
+ $sql = "SELECT username,password FROM ftp_users ORDER BY gid ASC";
+ self::generateFile($shadow, $sql, $cronlog);
+
+ // set correct permissions
+ @chmod('/var/lib/extrausers/', 0755);
+ @chmod('/var/lib/extrausers/passwd', 0644);
+ @chmod('/var/lib/extrausers/group', 0644);
+ @chmod('/var/lib/extrausers/shadow', 0640);
+ }
+
+ private static function generateFile($file, $query, &$cronlog)
+ {
+ $type = basename($file);
+ $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Creating ' . $type . ' file');
+
+ if (! file_exists($file)) {
+ $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, $type . ' file does not yet exist');
+ @mkdir(dirname($file), 0750, true);
+ touch($file);
+ }
+
+ $data_sel_stmt = Database::query($query);
+ $data_content = "";
+ $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Writing ' . $data_sel_stmt->rowCount() . ' entries to ' . $type . ' file');
+ while ($u = $data_sel_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ switch ($type) {
+ case 'passwd':
+ // get user real name
+ $salutation_array = array(
+ 'firstname' => \Froxlor\Customer\Customer::getCustomerDetail($u['customerid'], 'firstname'),
+ 'name' => \Froxlor\Customer\Customer::getCustomerDetail($u['customerid'], 'name'),
+ 'company' => \Froxlor\Customer\Customer::getCustomerDetail($u['customerid'], 'company')
+ );
+ $u['comment'] = self::cleanString(\Froxlor\User::getCorrectUserSalutation($salutation_array));
+ if ($u['login_enabled'] != 'Y') {
+ $u['password'] = '*';
+ $u['shell'] = '/bin/false';
+ $u['comment'] = 'Locked Froxlor User';
+ }
+ $line = $u['username'] . ':' . $u['password'] . ':' . $u['uid'] . ':' . $u['gid'] . ':' . $u['comment'] . ':' . $u['homedir'] . ':' . $u['shell'] . PHP_EOL;
+ break;
+ case 'group':
+ $line = $u['groupname'] . ':' . $u['password'] . ':' . $u['gid'] . ':' . $u['members'] . PHP_EOL;
+ break;
+ case 'shadow':
+ $line = $u['username'] . ':' . $u['password'] . ':' . floor(time() / 86400 - 1) . ':0:99999:7:::' . PHP_EOL;
+ break;
+ }
+ $data_content .= $line;
+ }
+ if (file_put_contents($file, $data_content) !== false) {
+ $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Succesfully wrote ' . $type . ' file');
+ } else {
+ $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Error when writing ' . $type . ' file entries');
+ }
+ }
+
+ private static function cleanString($string = null)
+ {
+ $allowed = "/[^a-z0-9\\.\\-\\_\\ ]/i";
+ return preg_replace($allowed, "", $string);
+ }
+}
diff --git a/lib/Froxlor/Cron/System/MailboxsizeCron.php b/lib/Froxlor/Cron/System/MailboxsizeCron.php
new file mode 100644
index 00000000..d35df4ef
--- /dev/null
+++ b/lib/Froxlor/Cron/System/MailboxsizeCron.php
@@ -0,0 +1,89 @@
+
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ * @since 0.9.29.1
+ *
+ */
+class MailboxsizeCron extends \Froxlor\Cron\FroxlorCron
+{
+
+ public static function run()
+ {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'calculating mailspace usage');
+
+ $maildirs_stmt = \Froxlor\Database\Database::query("
+ SELECT `id`, CONCAT(`homedir`, `maildir`) AS `maildirpath` FROM `" . TABLE_MAIL_USERS . "` ORDER BY `id`
+ ");
+
+ $upd_stmt = \Froxlor\Database\Database::prepare("
+ UPDATE `" . TABLE_MAIL_USERS . "` SET `mboxsize` = :size WHERE `id` = :id
+ ");
+
+ while ($maildir = $maildirs_stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ $_maildir = \Froxlor\FileDir::makeCorrectDir($maildir['maildirpath']);
+
+ if (file_exists($_maildir) && is_dir($_maildir)) {
+ $maildirsize = \Froxlor\FileDir::makeCorrectFile($_maildir . '/maildirsize');
+
+ // When quota is enabled and maildirsize file exists, use that to calculate size
+ if (\Froxlor\Settings::Get('system.mail_quota_enabled') == 1 && file_exists($maildirsize)) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'found maildirsize file in ' . $_maildir);
+ $file = file($maildirsize, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
+ // Remove header
+ array_shift($file);
+ $emailusage = 0;
+ // Sum up all the changes (line 2 -> end)
+ foreach ($file as $line) {
+ $parts = explode(' ', $line);
+ if (!empty($parts[0])) {
+ $emailusage += floatval($parts[0]);
+ }
+ }
+ } else {
+ // if quota is disabled or maildirsize file does not exist, compute with du
+ // mail-address allows many special characters, see http://en.wikipedia.org/wiki/Email_address#Local_part
+ $return = false;
+ $back = \Froxlor\FileDir::safe_exec('du -sk ' . escapeshellarg($_maildir), $return, array(
+ '|',
+ '&',
+ '`',
+ '$',
+ '~',
+ '?'
+ ));
+ foreach ($back as $backrow) {
+ $emailusage = explode(' ', $backrow);
+ }
+ $emailusage = floatval($emailusage['0']);
+
+ // as freebsd does not have the -b flag for 'du' which gives
+ // the size in bytes, we use "-sk" for all and calculate from KiB
+ $emailusage *= 1024;
+
+ unset($back);
+ }
+ \Froxlor\Database\Database::pexecute($upd_stmt, array(
+ 'size' => $emailusage,
+ 'id' => $maildir['id']
+ ));
+ } else {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_WARNING, 'maildir ' . $_maildir . ' does not exist');
+ }
+ }
+ }
+}
diff --git a/lib/Froxlor/Cron/System/TasksCron.php b/lib/Froxlor/Cron/System/TasksCron.php
new file mode 100644
index 00000000..b78575f0
--- /dev/null
+++ b/lib/Froxlor/Cron/System/TasksCron.php
@@ -0,0 +1,443 @@
+ (2003-2009)
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ */
+class TasksCron extends \Froxlor\Cron\FroxlorCron
+{
+
+ public static function run()
+ {
+ /**
+ * LOOK INTO TASKS TABLE TO SEE IF THERE ARE ANY UNDONE JOBS
+ */
+ self::$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "TasksCron: Searching for tasks to do");
+ // no type 99 (regenerate cron.d-file) and no type 20 (customer backup)
+ // order by type descending to re-create bind and then webserver at the end
+ $result_tasks_stmt = Database::query("
+ SELECT `id`, `type`, `data` FROM `" . TABLE_PANEL_TASKS . "` WHERE `type` <> '99' AND `type` <> '20' ORDER BY `type` DESC, `id` ASC
+ ");
+ $num_results = Database::num_rows();
+ $resultIDs = array();
+
+ while ($row = $result_tasks_stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ $resultIDs[] = $row['id'];
+
+ if ($row['data'] != '') {
+ $row['data'] = json_decode($row['data'], true);
+ }
+
+ if ($row['type'] == '1') {
+ /**
+ * TYPE=1 MEANS TO REBUILD APACHE VHOSTS.CONF
+ */
+ self::rebuildWebserverConfigs();
+ } elseif ($row['type'] == '2') {
+ /**
+ * TYPE=2 MEANS TO CREATE A NEW HOME AND CHOWN
+ */
+ self::createNewHome($row);
+ } elseif ($row['type'] == '4' && (int) Settings::Get('system.bind_enable') != 0) {
+ /**
+ * TYPE=4 MEANS THAT SOMETHING IN THE BIND CONFIG HAS CHANGED.
+ * REBUILD froxlor_bind.conf IF BIND IS ENABLED
+ */
+ self::rebuildDnsConfigs();
+ } elseif ($row['type'] == '5') {
+ /**
+ * TYPE=5 MEANS THAT A NEW FTP-ACCOUNT HAS BEEN CREATED, CREATE THE DIRECTORY
+ */
+ self::createNewFtpHome($row);
+ } elseif ($row['type'] == '6') {
+ /**
+ * TYPE=6 MEANS THAT A CUSTOMER HAS BEEN DELETED AND THAT WE HAVE TO REMOVE ITS FILES
+ */
+ self::deleteCustomerData($row);
+ } elseif ($row['type'] == '7') {
+ /**
+ * TYPE=7 Customer deleted an email account and wants the data to be deleted on the filesystem
+ */
+ self::deleteEmailData($row);
+ } elseif ($row['type'] == '8') {
+ /**
+ * TYPE=8 Customer deleted a ftp account and wants the homedir to be deleted on the filesystem
+ * refs #293
+ */
+ self::deleteFtpData($row);
+ } elseif ($row['type'] == '10' && (int) Settings::Get('system.diskquota_enabled') != 0) {
+ /**
+ * TYPE=10 Set the filesystem - quota
+ */
+ self::setFilesystemQuota();
+ } elseif ($row['type'] == '11' && Settings::Get('system.dns_server') == 'PowerDNS') {
+ /**
+ * TYPE=11 domain has been deleted, remove from pdns database if used
+ */
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, "Removing PowerDNS entries for domain " . $row['data']['domain']);
+ \Froxlor\Dns\PowerDNS::cleanDomainZone($row['data']['domain']);
+ } elseif ($row['type'] == '12') {
+ /**
+ * TYPE=12 domain has been deleted, remove from acme.sh/let's encrypt directory if used
+ */
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, "Removing Let's Encrypt entries for domain " . $row['data']['domain']);
+ \Froxlor\Domain\Domain::doLetsEncryptCleanUp($row['data']['domain']);
+ }
+ }
+
+ if ($num_results != 0) {
+ $where = array();
+ $where_data = array();
+ foreach ($resultIDs as $id) {
+ $where[] = "`id` = :id_" . (int) $id;
+ $where_data['id_' . $id] = $id;
+ }
+ $where = implode(' OR ', $where);
+ $del_stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_TASKS . "` WHERE " . $where);
+ Database::pexecute($del_stmt, $where_data);
+ unset($resultIDs);
+ unset($where);
+ }
+
+ Database::query("UPDATE `" . TABLE_PANEL_SETTINGS . "` SET `value` = UNIX_TIMESTAMP() WHERE `settinggroup` = 'system' AND `varname` = 'last_tasks_run';");
+ }
+
+ private static function rebuildWebserverConfigs()
+ {
+
+ if (Settings::Get('system.webserver') == "apache2") {
+ $websrv = '\\Froxlor\\Cron\\Http\\Apache';
+ if (Settings::Get('system.mod_fcgid') == 1 || Settings::Get('phpfpm.enabled') == 1) {
+ $websrv .= 'Fcgi';
+ }
+ } elseif (Settings::Get('system.webserver') == "lighttpd") {
+ $websrv = '\\Froxlor\\Cron\\Http\\Lighttpd';
+ if (Settings::Get('system.mod_fcgid') == 1 || Settings::Get('phpfpm.enabled') == 1) {
+ $websrv .= 'Fcgi';
+ }
+ } elseif (Settings::Get('system.webserver') == "nginx") {
+ $websrv = '\\Froxlor\\Cron\\Http\\Nginx';
+ if (Settings::Get('phpfpm.enabled') == 1) {
+ $websrv .= 'Fcgi';
+ }
+ }
+
+ // get configuration-I/O object
+ $configio = new \Froxlor\Cron\Http\ConfigIO();
+ // get webserver object
+ $webserver = new $websrv();
+
+ if (isset($webserver)) {
+ $webserver->init();
+ // clean up old configs
+ $configio->cleanUp();
+ $webserver->createIpPort();
+ $webserver->createVirtualHosts();
+ $webserver->createFileDirOptions();
+ $webserver->writeConfigs();
+ $webserver->createOwnVhostStarter();
+ $webserver->reload();
+ } else {
+ echo "Please check you Webserver settings\n";
+ }
+
+ // if we use php-fpm and have a local user for froxlor, we need to
+ // add the webserver-user to the local-group in order to allow the webserver
+ // to access the fpm-socket
+ if (Settings::Get('phpfpm.enabled') == 1 && function_exists("posix_getgrnam")) {
+ // get group info about the local-user's group (e.g. froxlorlocal)
+ $groupinfo = posix_getgrnam(Settings::Get('phpfpm.vhost_httpgroup'));
+ // check group members
+ if (isset($groupinfo['members']) && ! in_array(Settings::Get('system.httpuser'), $groupinfo['members'])) {
+ // webserver has no access, add it
+ if (\Froxlor\FileDir::isFreeBSD()) {
+ \Froxlor\FileDir::safe_exec('pw usermod ' . escapeshellarg(Settings::Get('system.httpuser')) . ' -G ' . escapeshellarg(Settings::Get('phpfpm.vhost_httpgroup')));
+ } else {
+ \Froxlor\FileDir::safe_exec('usermod -a -G ' . escapeshellarg(Settings::Get('phpfpm.vhost_httpgroup')) . ' ' . escapeshellarg(Settings::Get('system.httpuser')));
+ }
+ }
+ }
+
+ // Tell the Let's Encrypt cron it's okay to generate the certificate and enable the redirect afterwards
+ $upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `ssl_redirect` = '3' WHERE `ssl_redirect` = '2'");
+ Database::pexecute($upd_stmt);
+ }
+
+ private static function createNewHome($row = null)
+ {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'TasksCron: Task2 started - create new home');
+
+ if (is_array($row['data'])) {
+ // define paths
+ $userhomedir = \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.documentroot_prefix') . '/' . $row['data']['loginname'] . '/');
+ $usermaildir = \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.vmail_homedir') . '/' . $row['data']['loginname'] . '/');
+
+ // stats directory
+ if (Settings::Get('system.awstats_enabled') == '1') {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: mkdir -p ' . escapeshellarg($userhomedir . 'awstats'));
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg($userhomedir . 'awstats'));
+ // in case we changed from the other stats -> remove old
+ // (yes i know, the stats are lost - that's why you should not change all the time!)
+ if (file_exists($userhomedir . 'webalizer')) {
+ \Froxlor\FileDir::safe_exec('rm -rf ' . escapeshellarg($userhomedir . 'webalizer'));
+ }
+ } else {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: mkdir -p ' . escapeshellarg($userhomedir . 'webalizer'));
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg($userhomedir . 'webalizer'));
+ // in case we changed from the other stats -> remove old
+ // (yes i know, the stats are lost - that's why you should not change all the time!)
+ if (file_exists($userhomedir . 'awstats')) {
+ \Froxlor\FileDir::safe_exec('rm -rf ' . escapeshellarg($userhomedir . 'awstats'));
+ }
+ }
+
+ // maildir
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: mkdir -p ' . escapeshellarg($usermaildir));
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg($usermaildir));
+
+ // check if admin of customer has added template for new customer directories
+ if ((int) $row['data']['store_defaultindex'] == 1) {
+ \Froxlor\FileDir::storeDefaultIndex($row['data']['loginname'], $userhomedir, \Froxlor\FroxlorLogger::getInstanceOf(), true);
+ }
+
+ // strip of last slash of paths to have correct chown results
+ $userhomedir = (substr($userhomedir, 0, - 1) == '/') ? substr($userhomedir, 0, - 1) : $userhomedir;
+ $usermaildir = (substr($usermaildir, 0, - 1) == '/') ? substr($usermaildir, 0, - 1) : $usermaildir;
+
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: chown -R ' . (int) $row['data']['uid'] . ':' . (int) $row['data']['gid'] . ' ' . escapeshellarg($userhomedir));
+ \Froxlor\FileDir::safe_exec('chown -R ' . (int) $row['data']['uid'] . ':' . (int) $row['data']['gid'] . ' ' . escapeshellarg($userhomedir));
+ // don't allow others to access the directory (webserver will be the group via libnss-mysql)
+ if (Settings::Get('system.mod_fcgid') == 1 || Settings::Get('phpfpm.enabled') == 1) {
+ // fcgid or fpm
+ \Froxlor\FileDir::safe_exec('chmod 0750 ' . escapeshellarg($userhomedir));
+ } else {
+ // mod_php -> no libnss-mysql -> no webserver-user in group
+ \Froxlor\FileDir::safe_exec('chmod 0755 ' . escapeshellarg($userhomedir));
+ }
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: chown -R ' . (int) Settings::Get('system.vmail_uid') . ':' . (int) Settings::Get('system.vmail_gid') . ' ' . escapeshellarg($usermaildir));
+ \Froxlor\FileDir::safe_exec('chown -R ' . (int) Settings::Get('system.vmail_uid') . ':' . (int) Settings::Get('system.vmail_gid') . ' ' . escapeshellarg($usermaildir));
+
+ if (Settings::Get('system.nssextrausers') == 1) {
+ // explicitly create files after user has been created to avoid unknown user issues for apache/php-fpm when task#1 runs after this
+ $extrausers_log = \Froxlor\FroxlorLogger::getInstanceOf();
+ Extrausers::generateFiles($extrausers_log);
+ }
+
+ // clear NSCD cache if using fcgid or fpm, #1570 - not needed for nss-extrausers
+ if ((Settings::Get('system.mod_fcgid') == 1 || (int) Settings::Get('phpfpm.enabled') == 1) && Settings::Get('system.nssextrausers') == 0) {
+ $false_val = false;
+ \Froxlor\FileDir::safe_exec('nscd -i passwd 1> /dev/null', $false_val, array(
+ '>'
+ ));
+ \Froxlor\FileDir::safe_exec('nscd -i group 1> /dev/null', $false_val, array(
+ '>'
+ ));
+ }
+ }
+ }
+
+ private static function rebuildDnsConfigs()
+ {
+ $dnssrv = '\\Froxlor\\Cron\\Dns\\' . Settings::Get('system.dns_server');
+
+ $nameserver = new $dnssrv(\Froxlor\FroxlorLogger::getInstanceOf());
+
+ if (Settings::Get('dkim.use_dkim') == '1') {
+ $nameserver->writeDKIMconfigs();
+ }
+
+ $nameserver->writeConfigs();
+ }
+
+ private static function createNewFtpHome($row = null)
+ {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'Creating new FTP-home');
+ $result_directories_stmt = Database::query("
+ SELECT `f`.`homedir`, `f`.`uid`, `f`.`gid`, `c`.`documentroot` AS `customerroot`
+ FROM `" . TABLE_FTP_USERS . "` `f` LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` `c` USING (`customerid`)
+ ");
+
+ while ($directory = $result_directories_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ \Froxlor\FileDir::mkDirWithCorrectOwnership($directory['customerroot'], $directory['homedir'], $directory['uid'], $directory['gid']);
+ }
+ }
+
+ private static function deleteCustomerData($row = null)
+ {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'TasksCron: Task6 started - deleting customer data');
+
+ if (is_array($row['data'])) {
+ if (isset($row['data']['loginname'])) {
+ // remove homedir
+ $homedir = \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.documentroot_prefix') . '/' . $row['data']['loginname']);
+
+ if (file_exists($homedir) && $homedir != '/' && $homedir != Settings::Get('system.documentroot_prefix') && substr($homedir, 0, strlen(Settings::Get('system.documentroot_prefix'))) == Settings::Get('system.documentroot_prefix')) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: rm -rf ' . escapeshellarg($homedir));
+ \Froxlor\FileDir::safe_exec('rm -rf ' . escapeshellarg($homedir));
+ }
+
+ // remove maildir
+ $maildir = \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.vmail_homedir') . '/' . $row['data']['loginname']);
+
+ if (file_exists($maildir) && $maildir != '/' && $maildir != Settings::Get('system.vmail_homedir') && substr($maildir, 0, strlen(Settings::Get('system.vmail_homedir'))) == Settings::Get('system.vmail_homedir') && is_dir($maildir) && fileowner($maildir) == Settings::Get('system.vmail_uid') && filegroup($maildir) == Settings::Get('system.vmail_gid')) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: rm -rf ' . escapeshellarg($maildir));
+ // mail-address allows many special characters, see http://en.wikipedia.org/wiki/Email_address#Local_part
+ $return = false;
+ \Froxlor\FileDir::safe_exec('rm -rf ' . escapeshellarg($maildir), $return, array(
+ '|',
+ '&',
+ '`',
+ '$',
+ '?'
+ ));
+ }
+
+ // remove tmpdir if it exists
+ $tmpdir = \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.mod_fcgid_tmpdir') . '/' . $row['data']['loginname'] . '/');
+
+ if (file_exists($tmpdir) && is_dir($tmpdir) && $tmpdir != "/" && $tmpdir != Settings::Get('system.mod_fcgid_tmpdir') && substr($tmpdir, 0, strlen(Settings::Get('system.mod_fcgid_tmpdir'))) == Settings::Get('system.mod_fcgid_tmpdir')) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: rm -rf ' . escapeshellarg($tmpdir));
+ \Froxlor\FileDir::safe_exec('rm -rf ' . escapeshellarg($tmpdir));
+ }
+
+ // webserver logs
+ $logsdir = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.logfiles_directory') . '/' . $row['data']['loginname']);
+
+ if (file_exists($logsdir) && $logsdir != '/' && $logsdir != \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.logfiles_directory')) && substr($logsdir, 0, strlen(Settings::Get('system.logfiles_directory'))) == Settings::Get('system.logfiles_directory')) {
+ // build up wildcard for webX-{access,error}.log{*}
+ $logfiles .= '-*';
+ \Froxlor\FileDir::safe_exec('rm -f ' . escapeshellarg($logfiles));
+ }
+ }
+ }
+ }
+
+ private static function deleteEmailData($row = null)
+ {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'TasksCron: Task7 started - deleting customer e-mail data');
+
+ if (is_array($row['data'])) {
+
+ if (isset($row['data']['loginname']) && isset($row['data']['email'])) {
+ // remove specific maildir
+ $email_full = $row['data']['email'];
+ if (empty($email_full)) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, 'FATAL: Task7 asks to delete a email account but email field is empty!');
+ }
+ $email_user = substr($email_full, 0, strrpos($email_full, "@"));
+ $email_domain = substr($email_full, strrpos($email_full, "@") + 1);
+ $maildirname = trim(Settings::Get('system.vmail_maildirname'));
+ // Add trailing slash to Maildir if needed
+ $maildirpath = $maildirname;
+ if (! empty($maildirname) and substr($maildirname, - 1) != "/") {
+ $maildirpath .= "/";
+ }
+
+ $maildir = \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.vmail_homedir') . '/' . $row['data']['loginname'] . '/' . $email_domain . '/' . $email_user);
+
+ if ($maildir != '/' && ! empty($maildir) && ! empty($email_full) && $maildir != Settings::Get('system.vmail_homedir') && substr($maildir, 0, strlen(Settings::Get('system.vmail_homedir'))) == Settings::Get('system.vmail_homedir') && is_dir($maildir) && is_dir(\Froxlor\FileDir::makeCorrectDir($maildir . '/' . $maildirpath)) && fileowner($maildir) == Settings::Get('system.vmail_uid') && filegroup($maildir) == Settings::Get('system.vmail_gid')) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: rm -rf ' . escapeshellarg($maildir));
+ // mail-address allows many special characters, see http://en.wikipedia.org/wiki/Email_address#Local_part
+ $return = false;
+ \Froxlor\FileDir::safe_exec('rm -rf ' . escapeshellarg($maildir), $return, array(
+ '|',
+ '&',
+ '`',
+ '$',
+ '~',
+ '?'
+ ));
+ } else {
+ // backward-compatibility for old folder-structure
+ $maildir_old = \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.vmail_homedir') . '/' . $row['data']['loginname'] . '/' . $row['data']['email']);
+
+ if ($maildir_old != '/' && ! empty($maildir_old) && $maildir_old != Settings::Get('system.vmail_homedir') && substr($maildir_old, 0, strlen(Settings::Get('system.vmail_homedir'))) == Settings::Get('system.vmail_homedir') && is_dir($maildir_old) && fileowner($maildir_old) == Settings::Get('system.vmail_uid') && filegroup($maildir_old) == Settings::Get('system.vmail_gid')) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: rm -rf ' . escapeshellarg($maildir_old));
+ // mail-address allows many special characters, see http://en.wikipedia.org/wiki/Email_address#Local_part
+ $return = false;
+ \Froxlor\FileDir::safe_exec('rm -rf ' . escapeshellarg($maildir_old), $return, array(
+ '|',
+ '&',
+ '`',
+ '$',
+ '~',
+ '?'
+ ));
+ }
+ }
+ }
+ }
+ }
+
+ private static function deleteFtpData($row = null)
+ {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'TasksCron: Task8 started - deleting customer ftp homedir');
+
+ if (is_array($row['data'])) {
+
+ if (isset($row['data']['loginname']) && isset($row['data']['homedir'])) {
+ // remove specific homedir
+ $ftphomedir = \Froxlor\FileDir::makeCorrectDir($row['data']['homedir']);
+ $customerdocroot = \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.documentroot_prefix') . '/' . $row['data']['loginname'] . '/');
+
+ if (file_exists($ftphomedir) && $ftphomedir != '/' && $ftphomedir != Settings::Get('system.documentroot_prefix') && $ftphomedir != $customerdocroot) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: rm -rf ' . escapeshellarg($ftphomedir));
+ \Froxlor\FileDir::safe_exec('rm -rf ' . escapeshellarg($ftphomedir));
+ }
+ }
+ }
+ }
+
+ private static function setFilesystemQuota()
+ {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'TasksCron: Task10 started - setting filesystem quota');
+
+ $usedquota = \Froxlor\FileDir::getFilesystemQuota();
+
+ // Check whether we really have entries to check
+ if (is_array($usedquota) && count($usedquota) > 0) {
+ // Select all customers Froxlor knows about
+ $result_stmt = Database::query("SELECT `guid`, `loginname`, `diskspace` FROM `" . TABLE_PANEL_CUSTOMERS . "`;");
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ // We do not want to set a quota for root by accident
+ if ($row['guid'] != 0) {
+ $used_quota = isset($usedquota[$row['guid']]) ? $usedquota[$row['guid']]['block']['hard'] : 0;
+ // The user has no quota in Froxlor, but on the filesystem
+ if (($row['diskspace'] == 0 || $row['diskspace'] == - 1024) && $used_quota != 0) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, "Disabling quota for " . $row['loginname']);
+ if (\Froxlor\FileDir::isFreeBSD()) {
+ \Froxlor\FileDir::safe_exec(Settings::Get('system.diskquota_quotatool_path') . " -e " . escapeshellarg(Settings::Get('system.diskquota_customer_partition')) . ":0:0 " . $row['guid']);
+ } else {
+ \Froxlor\FileDir::safe_exec(Settings::Get('system.diskquota_quotatool_path') . " -u " . $row['guid'] . " -bl 0 -q 0 " . escapeshellarg(Settings::Get('system.diskquota_customer_partition')));
+ }
+ } elseif ($row['diskspace'] != $used_quota && $row['diskspace'] != - 1024) {
+ // The user quota in Froxlor is different than on the filesystem
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, "Setting quota for " . $row['loginname'] . " from " . $used_quota . " to " . $row['diskspace']);
+ if (\Froxlor\FileDir::isFreeBSD()) {
+ \Froxlor\FileDir::safe_exec(Settings::Get('system.diskquota_quotatool_path') . " -e " . escapeshellarg(Settings::Get('system.diskquota_customer_partition')) . ":" . $row['diskspace'] . ":" . $row['diskspace'] . " " . $row['guid']);
+ } else {
+ \Froxlor\FileDir::safe_exec(Settings::Get('system.diskquota_quotatool_path') . " -u " . $row['guid'] . " -bl " . $row['diskspace'] . " -q " . $row['diskspace'] . " " . escapeshellarg(Settings::Get('system.diskquota_customer_partition')));
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/Froxlor/Cron/Traffic/ReportsCron.php b/lib/Froxlor/Cron/Traffic/ReportsCron.php
new file mode 100644
index 00000000..bd24d0ac
--- /dev/null
+++ b/lib/Froxlor/Cron/Traffic/ReportsCron.php
@@ -0,0 +1,566 @@
+ (2003-2009)
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ */
+use Froxlor\Database\Database;
+use Froxlor\Settings;
+
+class ReportsCron extends \Froxlor\Cron\FroxlorCron
+{
+
+ public static function run()
+ {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'Web- and Traffic-usage reporting started...');
+ $yesterday = time() - (60 * 60 * 24);
+
+ /**
+ * Initialize the mailingsystem
+ */
+ $mail = new \Froxlor\System\Mailer(true);
+
+ if ((int) Settings::Get('system.report_trafficmax') > 0) {
+ // Warn the customers at xx% traffic-usage
+ $result_stmt = Database::prepare("
+ SELECT `c`.`customerid`, `c`.`customernumber`, `c`.`adminid`, `c`.`name`, `c`.`firstname`,
+ `c`.`company`, `c`.`traffic`, `c`.`email`, `c`.`def_language`,
+ `a`.`name` AS `adminname`, `a`.`email` AS `adminmail`,
+ (SELECT SUM(`t`.`http` + `t`.`ftp_up` + `t`.`ftp_down` + `t`.`mail`)
+ FROM `" . TABLE_PANEL_TRAFFIC . "` `t`
+ WHERE `t`.`customerid` = `c`.`customerid` AND `t`.`year` = :year AND `t`.`month` = :month
+ ) as `traffic_used`
+ FROM `" . TABLE_PANEL_CUSTOMERS . "` AS `c`
+ LEFT JOIN `" . TABLE_PANEL_ADMINS . "` AS `a`
+ ON `a`.`adminid` = `c`.`adminid` WHERE `c`.`reportsent` <> '1'
+ ");
+
+ $result_data = array(
+ 'year' => date("Y", $yesterday),
+ 'month' => date("m", $yesterday)
+ );
+ Database::pexecute($result_stmt, $result_data);
+
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if (isset($row['traffic']) && $row['traffic'] > 0 && $row['traffic_used'] != null && (($row['traffic_used'] * 100) / $row['traffic']) >= (int) Settings::Get('system.report_trafficmax')) {
+ $rep_userinfo = array(
+ 'name' => $row['name'],
+ 'firstname' => $row['firstname'],
+ 'company' => $row['company'],
+ 'customernumber' => $row['customernumber']
+ );
+ $replace_arr = array(
+ 'SALUTATION' => \Froxlor\User::getCorrectUserSalutation($rep_userinfo),
+ 'NAME' => $rep_userinfo['name'],
+ 'FIRSTNAME' => $rep_userinfo['firstname'],
+ 'COMPANY' => $rep_userinfo['company'],
+ 'CUSTOMER_NO' => $rep_userinfo['customernumber'],
+ 'TRAFFIC' => round(($row['traffic'] / 1024), 2), /* traffic is stored in KB, template uses MB */
+ 'TRAFFICUSED' => round(($row['traffic_used'] / 1024), 2), /* traffic is stored in KB, template uses MB */
+ 'USAGE_PERCENT' => round(($row['traffic_used'] * 100) / $row['traffic'], 2),
+ 'MAX_PERCENT' => Settings::Get('system.report_trafficmax')
+ );
+
+ $lngfile_stmt = Database::prepare("
+ SELECT `file` FROM `" . TABLE_PANEL_LANGUAGE . "`
+ WHERE `language` = :deflang
+ ");
+ $lngfile = Database::pexecute_first($lngfile_stmt, array(
+ 'deflang' => $row['def_language']
+ ));
+
+ if ($lngfile !== null) {
+ $langfile = $lngfile['file'];
+ } else {
+ $lngfile = Database::pexecute_first($lngfile_stmt, array(
+ 'deflang' => Settings::Get('panel.standardlanguage')
+ ));
+ $langfile = $lngfile['file'];
+ }
+
+ // include english language file (fallback)
+ include \Froxlor\FileDir::makeCorrectFile(\Froxlor\Froxlor::getInstallDir() . '/lng/english.lng.php');
+ // include admin/customer language file
+ if ($lngfile != 'lng/english.lng.php') {
+ include \Froxlor\FileDir::makeCorrectFile(\Froxlor\Froxlor::getInstallDir() . '/' . $langfile);
+ }
+
+ // Get mail templates from database; the ones from 'admin' are fetched for fallback
+ $result2_stmt = Database::prepare("
+ SELECT `value` FROM `" . TABLE_PANEL_TEMPLATES . "`
+ WHERE `adminid` = :adminid
+ AND `language` = :lang
+ AND `templategroup` = 'mails' AND `varname` = :varname
+ ");
+ $result2_data = array(
+ 'adminid' => $row['adminid'],
+ 'lang' => $row['def_language'],
+ 'varname' => 'trafficmaxpercent_subject'
+ );
+ $result2 = Database::pexecute_first($result2_stmt, $result2_data);
+ $mail_subject = html_entity_decode(\Froxlor\PhpHelper::replaceVariables((($result2 !== false && $result2['value'] != '') ? $result2['value'] : $lng['mails']['trafficmaxpercent']['subject']), $replace_arr));
+
+ $result2_data['varname'] = 'trafficmaxpercent_mailbody';
+ $result2 = Database::pexecute_first($result2_stmt, $result2_data);
+ $mail_body = html_entity_decode(\Froxlor\PhpHelper::replaceVariables((($result2 !== false && $result2['value'] != '') ? $result2['value'] : $lng['mails']['trafficmaxpercent']['mailbody']), $replace_arr));
+
+ $_mailerror = false;
+ $mailerr_msg = "";
+ try {
+ $mail->SetFrom($row['adminmail'], $row['adminname']);
+ $mail->Subject = $mail_subject;
+ $mail->AltBody = $mail_body;
+ $mail->MsgHTML(nl2br($mail_body));
+ $mail->AddAddress($row['email'], $row['firstname'] . ' ' . $row['name']);
+ $mail->Send();
+ } catch (\PHPMailer\PHPMailer\Exception $e) {
+ $mailerr_msg = $e->errorMessage();
+ $_mailerror = true;
+ } catch (\Exception $e) {
+ $mailerr_msg = $e->getMessage();
+ $_mailerror = true;
+ }
+
+ if ($_mailerror) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, 'Error sending mail: ' . $mailerr_msg);
+ echo 'Error sending mail: ' . $mailerr_msg . "\n";
+ }
+
+ $mail->ClearAddresses();
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `reportsent` = '1'
+ WHERE `customerid` = :customerid
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'customerid' => $row['customerid']
+ ));
+
+ unset($lng);
+ }
+ }
+
+ // Warn the admins at xx% traffic-usage
+ $result_stmt = Database::prepare("
+ SELECT `a`.*,
+ (SELECT SUM(`t`.`http` + `t`.`ftp_up` + `t`.`ftp_down` + `t`.`mail`)
+ FROM `" . TABLE_PANEL_TRAFFIC_ADMINS . "` `t`
+ WHERE `t`.`adminid` = `a`.`adminid` AND `t`.`year` = :year AND `t`.`month` = :month
+ ) as `traffic_used_total`
+ FROM `" . TABLE_PANEL_ADMINS . "` `a` WHERE `a`.`reportsent` = '0'
+ ");
+
+ $result_data = array(
+ 'year' => date("Y", $yesterday),
+ 'month' => date("m", $yesterday)
+ );
+ Database::pexecute($result_stmt, $result_data);
+
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ if (isset($row['traffic']) && $row['traffic'] > 0 && (($row['traffic_used_total'] * 100) / $row['traffic']) >= (int) Settings::Get('system.report_trafficmax')) {
+
+ $replace_arr = array(
+ 'NAME' => $row['name'],
+ 'TRAFFIC' => round(($row['traffic'] / 1024), 2), /* traffic is stored in KB, template uses MB */
+ 'TRAFFICUSED' => round(($row['traffic_used_total'] / 1024), 2), /* traffic is stored in KB, template uses MB */
+ 'USAGE_PERCENT' => round(($row['traffic_used_total'] * 100) / $row['traffic'], 2),
+ 'MAX_PERCENT' => Settings::Get('system.report_trafficmax')
+ );
+
+ $lngfile_stmt = Database::prepare("
+ SELECT `file` FROM `" . TABLE_PANEL_LANGUAGE . "`
+ WHERE `language` = :deflang
+ ");
+ $lngfile = Database::pexecute_first($lngfile_stmt, array(
+ 'deflang' => $row['def_language']
+ ));
+
+ if ($lngfile !== null) {
+ $langfile = $lngfile['file'];
+ } else {
+ $lngfile = Database::pexecute_first($lngfile_stmt, array(
+ 'deflang' => Settings::Get('panel.standardlanguage')
+ ));
+ $langfile = $lngfile['file'];
+ }
+
+ // include english language file (fallback)
+ include \Froxlor\FileDir::makeCorrectFile(\Froxlor\Froxlor::getInstallDir() . '/lng/english.lng.php');
+ // include admin/customer language file
+ if ($lngfile != 'lng/english.lng.php') {
+ include \Froxlor\FileDir::makeCorrectFile(\Froxlor\Froxlor::getInstallDir() . '/' . $langfile);
+ }
+
+ // Get mail templates from database; the ones from 'admin' are fetched for fallback
+ $result2_stmt = Database::prepare("
+ SELECT `value` FROM `" . TABLE_PANEL_TEMPLATES . "`
+ WHERE `adminid` = :adminid
+ AND `language` = :lang
+ AND `templategroup` = 'mails' AND `varname` = :varname
+ ");
+ $result2_data = array(
+ 'adminid' => $row['adminid'],
+ 'lang' => $row['def_language'],
+ 'varname' => 'trafficmaxpercent_subject'
+ );
+ $result2 = Database::pexecute_first($result2_stmt, $result2_data);
+ $mail_subject = html_entity_decode(\Froxlor\PhpHelper::replaceVariables((($result2 !== false && $result2['value'] != '') ? $result2['value'] : $lng['mails']['trafficmaxpercent']['subject']), $replace_arr));
+
+ $result2_data['varname'] = 'trafficmaxpercent_mailbody';
+ $result2 = Database::pexecute_first($result2_stmt, $result2_data);
+ $mail_body = html_entity_decode(\Froxlor\PhpHelper::replaceVariables((($result2 !== false && $result2['value'] != '') ? $result2['value'] : $lng['mails']['trafficmaxpercent']['mailbody']), $replace_arr));
+
+ $_mailerror = false;
+ $mailerr_msg = "";
+ try {
+ $mail->SetFrom($row['email'], $row['name']);
+ $mail->Subject = $mail_subject;
+ $mail->AltBody = $mail_body;
+ $mail->MsgHTML(nl2br($mail_body));
+ $mail->AddAddress($row['email'], $row['name']);
+ $mail->Send();
+ } catch (\PHPMailer\PHPMailer\Exception $e) {
+ $mailerr_msg = $e->errorMessage();
+ $_mailerror = true;
+ } catch (\Exception $e) {
+ $mailerr_msg = $e->getMessage();
+ $_mailerror = true;
+ }
+
+ if ($_mailerror) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, "Error sending mail: " . $mailerr_msg);
+ echo "Error sending mail: " . $mailerr_msg . "\n";
+ }
+
+ $mail->ClearAddresses();
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_ADMINS . "` SET `reportsent` = '1'
+ WHERE `adminid` = :adminid
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'adminid' => $row['adminid']
+ ));
+ }
+
+ // Another month, let's build our report
+ if (date('d') == '01') {
+
+ $mail_subject = 'Trafficreport ' . date("m/y", $yesterday) . ' for ' . $row['name'];
+ $mail_body = 'Trafficreport ' . date("m/y", $yesterday) . ' for ' . $row['name'] . "\n";
+ $mail_body .= '---------------------------------------------------------------' . "\n";
+ $mail_body .= 'Loginname Traffic used (Percent) | Traffic available' . "\n";
+ $customers_stmt = Database::prepare("
+ SELECT `c`.*,
+ (SELECT SUM(`t`.`http` + `t`.`ftp_up` + `t`.`ftp_down` + `t`.`mail`)
+ FROM `" . TABLE_PANEL_TRAFFIC . "` `t`
+ WHERE `t`.`customerid` = `c`.`customerid` AND `t`.`year` = :year AND `t`.`month` = :month
+ ) as `traffic_used_total`
+ FROM `" . TABLE_PANEL_CUSTOMERS . "` `c` WHERE `c`.`adminid` = :adminid
+ ");
+ $customers_data = array(
+ 'year' => date("Y", $yesterday),
+ 'month' => date("m", $yesterday),
+ 'adminid' => $row['adminid']
+ );
+ Database::pexecute($customers_stmt, $customers_data);
+
+ while ($customer = $customers_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $t = $customer['traffic_used_total'] / 1048576;
+ if ($customer['traffic'] > 0) {
+ $p = (($customer['traffic_used_total'] * 100) / $customer['traffic']);
+ $tg = $customer['traffic'] / 1048576;
+ $str = sprintf('%00.1f GB ( %00.1f %% )', $t, $p);
+ $mail_body .= sprintf('%-15s', $customer['loginname']) . ' ' . sprintf('%-25s', $str) . ' ' . sprintf('%00.1f GB', $tg) . "\n";
+ } elseif ($customer['traffic'] == 0) {
+ $str = sprintf('%00.1f GB ( - )', $t);
+ $mail_body .= sprintf('%-15s', $customer['loginname']) . ' ' . sprintf('%-25s', $str) . ' ' . '0' . "\n";
+ } else {
+ $str = sprintf('%00.1f GB ( - )', $t);
+ $mail_body .= sprintf('%-15s', $customer['loginname']) . ' ' . sprintf('%-25s', $str) . ' ' . 'unlimited' . "\n";
+ }
+ }
+
+ $mail_body .= '---------------------------------------------------------------' . "\n";
+
+ $t = $row['traffic_used_total'] / 1048576;
+ if ($row['traffic'] > 0) {
+ $p = (($row['traffic_used_total'] * 100) / $row['traffic']);
+ $tg = $row['traffic'] / 1048576;
+ $str = sprintf('%00.1f GB ( %00.1f %% )', $t, $p);
+ $mail_body .= sprintf('%-15s', $row['loginname']) . ' ' . sprintf('%-25s', $str) . ' ' . sprintf('%00.1f GB', $tg) . "\n";
+ } elseif ($row['traffic'] == 0) {
+ $str = sprintf('%00.1f GB ( - )', $t);
+ $mail_body .= sprintf('%-15s', $row['loginname']) . ' ' . sprintf('%-25s', $str) . ' ' . '0' . "\n";
+ } else {
+ $str = sprintf('%00.1f GB ( - )', $t);
+ $mail_body .= sprintf('%-15s', $row['loginname']) . ' ' . sprintf('%-25s', $str) . ' ' . 'unlimited' . "\n";
+ }
+
+ $_mailerror = false;
+ $mailerr_msg = "";
+ try {
+ $mail->SetFrom($row['email'], $row['name']);
+ $mail->Subject = $mail_subject;
+ $mail->Body = $mail_body;
+ $mail->AddAddress($row['email'], $row['name']);
+ $mail->Send();
+ } catch (\PHPMailer\PHPMailer\Exception $e) {
+ $mailerr_msg = $e->errorMessage();
+ $_mailerror = true;
+ } catch (\Exception $e) {
+ $mailerr_msg = $e->getMessage();
+ $_mailerror = true;
+ }
+
+ if ($_mailerror) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, 'Error sending mail: ' . $mailerr_msg);
+ echo 'Error sending mail: ' . $mailerr_msg . "\n";
+ }
+
+ $mail->ClearAddresses();
+
+ unset($lng);
+ }
+ }
+ } // trafficmax > 0
+
+ // include diskspace-usage report, #466
+ self::usageDiskspace();
+
+ // Another month, reset the reportstatus
+ if (date('d') == '01') {
+ Database::query("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `reportsent` = '0';");
+ Database::query("UPDATE `" . TABLE_PANEL_ADMINS . "` SET `reportsent` = '0';");
+ }
+ }
+
+ private static function usageDiskspace()
+ {
+ if ((int) Settings::Get('system.report_webmax') > 0) {
+ /**
+ * report about diskusage for customers
+ */
+ $result_stmt = Database::query("
+ SELECT `c`.`customerid`, `c`.`customernumber`, `c`.`adminid`, `c`.`name`, `c`.`firstname`,
+ `c`.`company`, `c`.`diskspace`, `c`.`diskspace_used`, `c`.`email`, `c`.`def_language`,
+ `a`.`name` AS `adminname`, `a`.`email` AS `adminmail`
+ FROM `" . TABLE_PANEL_CUSTOMERS . "` AS `c`
+ LEFT JOIN `" . TABLE_PANEL_ADMINS . "` AS `a`
+ ON `a`.`adminid` = `c`.`adminid`
+ WHERE `c`.`diskspace` > '0' AND `c`.`reportsent` <> '2'
+ ");
+
+ $mail = new \Froxlor\System\Mailer(true);
+
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ if (isset($row['diskspace']) && $row['diskspace_used'] != null && $row['diskspace_used'] > 0 && (($row['diskspace_used'] * 100) / $row['diskspace']) >= (int) Settings::Get('system.report_webmax')) {
+
+ $rep_userinfo = array(
+ 'name' => $row['name'],
+ 'firstname' => $row['firstname'],
+ 'company' => $row['company'],
+ 'customernumber' => $row['customernumber']
+ );
+ $replace_arr = array(
+ 'SALUTATION' => \Froxlor\User::getCorrectUserSalutation($rep_userinfo),
+ 'NAME' => $rep_userinfo['name'],
+ 'FIRSTNAME' => $rep_userinfo['firstname'],
+ 'COMPANY' => $rep_userinfo['company'],
+ 'CUSTOMER_NO' => $rep_userinfo['customernumber'],
+ 'DISKAVAILABLE' => round(($row['diskspace'] / 1024), 2), /* traffic is stored in KB, template uses MB */
+ 'DISKUSED' => round($row['diskspace_used'] / 1024, 2), /* traffic is stored in KB, template uses MB */
+ 'USAGE_PERCENT' => round(($row['diskspace_used'] * 100) / $row['diskspace'], 2),
+ 'MAX_PERCENT' => Settings::Get('system.report_webmax')
+ );
+
+ $lngfile_stmt = Database::prepare("
+ SELECT `file` FROM `" . TABLE_PANEL_LANGUAGE . "`
+ WHERE `language` = :deflang
+ ");
+ $lngfile = Database::pexecute_first($lngfile_stmt, array(
+ 'deflang' => $row['def_language']
+ ));
+
+ if ($lngfile !== null) {
+ $langfile = $lngfile['file'];
+ } else {
+ $lngfile = Database::pexecute_first($lngfile_stmt, array(
+ 'deflang' => Settings::Get('panel.standardlanguage')
+ ));
+ $langfile = $lngfile['file'] ?? 'lng/english.lng.php';
+ }
+
+ // include english language file (fallback)
+ include \Froxlor\FileDir::makeCorrectFile(\Froxlor\Froxlor::getInstallDir() . '/lng/english.lng.php');
+ // include admin/customer language file
+ if ($lngfile != 'lng/english.lng.php') {
+ include \Froxlor\FileDir::makeCorrectFile(\Froxlor\Froxlor::getInstallDir() . '/' . $langfile);
+ }
+
+ // Get mail templates from database; the ones from 'admin' are fetched for fallback
+ $result2_stmt = Database::prepare("
+ SELECT `value` FROM `" . TABLE_PANEL_TEMPLATES . "`
+ WHERE `adminid` = :adminid
+ AND `language` = :lang
+ AND `templategroup` = 'mails' AND `varname` = :varname
+ ");
+ $result2_data = array(
+ 'adminid' => $row['adminid'],
+ 'lang' => $row['def_language'],
+ 'varname' => 'diskmaxpercent_subject'
+ );
+ $result2 = Database::pexecute_first($result2_stmt, $result2_data);
+ $mail_subject = html_entity_decode(\Froxlor\PhpHelper::replaceVariables((($result2 !== false && $result2['value'] != '') ? $result2['value'] : $lng['mails']['diskmaxpercent']['subject']), $replace_arr));
+
+ $result2_data['varname'] = 'diskmaxpercent_mailbody';
+ $result2 = Database::pexecute_first($result2_stmt, $result2_data);
+ $mail_body = html_entity_decode(\Froxlor\PhpHelper::replaceVariables((($result2 !== false && $result2['value'] != '') ? $result2['value'] : $lng['mails']['diskmaxpercent']['mailbody']), $replace_arr));
+
+ $_mailerror = false;
+ $mailerr_msg = "";
+ try {
+ $mail->SetFrom($row['adminmail'], $row['adminname']);
+ $mail->Subject = $mail_subject;
+ $mail->AltBody = $mail_body;
+ $mail->MsgHTML(nl2br($mail_body));
+ $mail->AddAddress($row['email'], $row['name']);
+ $mail->Send();
+ } catch (\PHPMailer\PHPMailer\Exception $e) {
+ $mailerr_msg = $e->errorMessage();
+ $_mailerror = true;
+ } catch (\Exception $e) {
+ $mailerr_msg = $e->getMessage();
+ $_mailerror = true;
+ }
+
+ if ($_mailerror) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, "Error sending mail: " . $mailerr_msg);
+ echo "Error sending mail: " . $mailerr_msg . "\n";
+ }
+
+ $mail->ClearAddresses();
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `reportsent` = '2'
+ WHERE `customerid` = :customerid
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'customerid' => $row['customerid']
+ ));
+
+ unset($lng);
+ }
+ }
+
+ /**
+ * report about diskusage for admins/reseller
+ */
+ $result_stmt = Database::query("
+ SELECT `a`.* FROM `" . TABLE_PANEL_ADMINS . "` `a` WHERE `a`.`reportsent` <> '2'
+ ");
+
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ if (isset($row['diskspace']) && $row['diskspace_used'] != null && $row['diskspace_used'] > 0 && (($row['diskspace_used'] * 100) / $row['diskspace']) >= (int) Settings::Get('system.report_webmax')) {
+
+ $replace_arr = array(
+ 'NAME' => $row['name'],
+ 'DISKAVAILABLE' => ($row['diskspace'] / 1024), /* traffic is stored in KB, template uses MB */
+ 'DISKUSED' => round($row['diskspace_used'] / 1024, 2), /* traffic is stored in KB, template uses MB */
+ 'USAGE_PERCENT' => ($row['diskspace_used'] * 100) / $row['diskspace'],
+ 'MAX_PERCENT' => Settings::Get('system.report_webmax')
+ );
+
+ $lngfile_stmt = Database::prepare("
+ SELECT `file` FROM `" . TABLE_PANEL_LANGUAGE . "`
+ WHERE `language` = :deflang
+ ");
+ $lngfile = Database::pexecute_first($lngfile_stmt, array(
+ 'deflang' => $row['def_language']
+ ));
+
+ if ($lngfile !== null) {
+ $langfile = $lngfile['file'];
+ } else {
+ $lngfile = Database::pexecute_first($lngfile_stmt, array(
+ 'deflang' => Settings::Get('panel.standardlanguage')
+ ));
+ $langfile = $lngfile['file'];
+ }
+
+ // include english language file (fallback)
+ include \Froxlor\FileDir::makeCorrectFile(\Froxlor\Froxlor::getInstallDir() . '/lng/english.lng.php');
+ // include admin/customer language file
+ if ($lngfile != 'lng/english.lng.php') {
+ include \Froxlor\FileDir::makeCorrectFile(\Froxlor\Froxlor::getInstallDir() . '/' . $langfile);
+ }
+
+ // Get mail templates from database; the ones from 'admin' are fetched for fallback
+ $result2_stmt = Database::prepare("
+ SELECT `value` FROM `" . TABLE_PANEL_TEMPLATES . "`
+ WHERE `adminid` = :adminid
+ AND `language` = :lang
+ AND `templategroup` = 'mails' AND `varname` = :varname
+ ");
+ $result2_data = array(
+ 'adminid' => $row['adminid'],
+ 'lang' => $row['def_language'],
+ 'varname' => 'diskmaxpercent_subject'
+ );
+ $result2 = Database::pexecute_first($result2_stmt, $result2_data);
+ $mail_subject = html_entity_decode(\Froxlor\PhpHelper::replaceVariables((($result2 !== false && $result2['value'] != '') ? $result2['value'] : $lng['mails']['diskmaxpercent']['subject']), $replace_arr));
+
+ $result2_data['varname'] = 'diskmaxpercent_mailbody';
+ $result2 = Database::pexecute_first($result2_stmt, $result2_data);
+ $mail_body = html_entity_decode(\Froxlor\PhpHelper::replaceVariables((($result2 !== false && $result2['value'] != '') ? $result2['value'] : $lng['mails']['diskmaxpercent']['mailbody']), $replace_arr));
+
+ $_mailerror = false;
+ $mailerr_msg = "";
+ try {
+ $mail->SetFrom($row['email'], $row['name']);
+ $mail->Subject = $mail_subject;
+ $mail->AltBody = $mail_body;
+ $mail->MsgHTML(nl2br($mail_body));
+ $mail->AddAddress($row['email'], $row['name']);
+ $mail->Send();
+ } catch (\PHPMailer\PHPMailer\Exception $e) {
+ $mailerr_msg = $e->errorMessage();
+ $_mailerror = true;
+ } catch (\Exception $e) {
+ $mailerr_msg = $e->getMessage();
+ $_mailerror = true;
+ }
+
+ if ($_mailerror) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, "Error sending mail: " . $mailerr_msg);
+ echo "Error sending mail: " . $mailerr_msg . "\n";
+ }
+
+ $mail->ClearAddresses();
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_ADMINS . "` SET `reportsent` = '2'
+ WHERE `adminid` = :adminid
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'adminid' => $row['adminid']
+ ));
+
+ unset($lng);
+ }
+ }
+ } // webmax > 0
+ }
+}
diff --git a/lib/Froxlor/Cron/Traffic/TrafficCron.php b/lib/Froxlor/Cron/Traffic/TrafficCron.php
new file mode 100644
index 00000000..e11b2d08
--- /dev/null
+++ b/lib/Froxlor/Cron/Traffic/TrafficCron.php
@@ -0,0 +1,910 @@
+ (2003-2009)
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ */
+use Froxlor\Database\Database;
+use Froxlor\Settings;
+
+class TrafficCron extends \Froxlor\Cron\FroxlorCron
+{
+
+ public static function run()
+ {
+
+ // Check Traffic-Lock
+ if (function_exists('pcntl_fork') && ! defined('CRON_NOFORK_FLAG')) {
+ $TrafficLock = \Froxlor\FileDir::makeCorrectFile("/var/run/froxlor_cron_traffic.lock");
+ if (file_exists($TrafficLock) && is_numeric($TrafficPid = file_get_contents($TrafficLock))) {
+ if (function_exists('posix_kill')) {
+ $TrafficPidStatus = @posix_kill($TrafficPid, 0);
+ } else {
+ system("kill -CHLD " . $TrafficPid . " 1> /dev/null 2> /dev/null", $TrafficPidStatus);
+ $TrafficPidStatus = $TrafficPidStatus ? false : true;
+ }
+ if ($TrafficPidStatus) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'Traffic Run already in progress');
+ return 1;
+ }
+ }
+ // Create Traffic Log and Fork
+ // We close the database - connection before we fork, so we don't share resources with the child
+ Database::needRoot(false); // this forces the connection to be set to null
+ $TrafficPid = pcntl_fork();
+ // Parent
+ if ($TrafficPid) {
+ file_put_contents($TrafficLock, $TrafficPid);
+ // unnecessary to recreate database connection here
+ return 0;
+ } elseif ($TrafficPid == 0) {
+ // Child
+ posix_setsid();
+ // re-create db
+ Database::needRoot(false);
+ } else {
+ // Fork failed
+ return 1;
+ }
+ } else {
+ if (extension_loaded('pcntl')) {
+ $msg = "PHP compiled with pcntl but pcntl_fork function is not available.";
+ } else {
+ $msg = "PHP compiled without pcntl.";
+ }
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, $msg . " Not forking traffic-cron, this may take a long time!");
+ }
+
+ /**
+ * TRAFFIC AND DISKUSAGE MESSURE
+ */
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'Traffic run started...');
+ $admin_traffic = array();
+ $domainlist = array();
+ $speciallogfile_domainlist = array();
+ $result_domainlist_stmt = Database::query("
+ SELECT `id`, `domain`, `customerid`, `parentdomainid`, `speciallogfile`
+ FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `aliasdomain` IS NULL AND `email_only` <> '1';
+ ");
+
+ while ($row_domainlist = $result_domainlist_stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ if (! isset($domainlist[$row_domainlist['customerid']])) {
+ $domainlist[$row_domainlist['customerid']] = array();
+ }
+
+ $domainlist[$row_domainlist['customerid']][$row_domainlist['id']] = $row_domainlist['domain'];
+
+ if ($row_domainlist['parentdomainid'] == '0' && $row_domainlist['speciallogfile'] == '1') {
+ if (! isset($speciallogfile_domainlist[$row_domainlist['customerid']])) {
+ $speciallogfile_domainlist[$row_domainlist['customerid']] = array();
+ }
+ $speciallogfile_domainlist[$row_domainlist['customerid']][$row_domainlist['id']] = $row_domainlist['domain'];
+ }
+ }
+
+ $mysqlusage_all = array();
+ $databases_stmt = Database::query("SELECT * FROM " . TABLE_PANEL_DATABASES . " ORDER BY `dbserver`");
+ $last_dbserver = 0;
+
+ $databases_list = array();
+ Database::needRoot(true);
+ $databases_list_result_stmt = Database::query("SHOW DATABASES");
+ while ($databases_list_row = $databases_list_result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $databases_list[] = strtolower($databases_list_row['Database']);
+ }
+
+ while ($row_database = $databases_stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ if ($last_dbserver != $row_database['dbserver']) {
+ Database::needRoot(true, $row_database['dbserver']);
+ $last_dbserver = $row_database['dbserver'];
+
+ $databases_list = array();
+ $databases_list_result_stmt = Database::query("SHOW DATABASES");
+ while ($databases_list_row = $databases_list_result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $databases_list[] = strtolower($databases_list_row['Database']);
+ }
+ }
+
+ if (in_array(strtolower($row_database['databasename']), $databases_list)) {
+ // sum up data_length and index_length
+ $mysql_usage_result_stmt = Database::prepare("
+ SELECT SUM(data_length + index_length) AS customerusage
+ FROM information_schema.TABLES
+ WHERE table_schema = :database
+ GROUP BY table_schema;
+ ");
+ // get the result
+ $mysql_usage_row = Database::pexecute_first($mysql_usage_result_stmt, array(
+ 'database' => $row_database['databasename']
+ ));
+ // initialize counter for customer
+ if (! isset($mysqlusage_all[$row_database['customerid']])) {
+ $mysqlusage_all[$row_database['customerid']] = 0;
+ }
+ // sum up result
+ if ($mysql_usage_row) {
+ $mysqlusage_all[$row_database['customerid']] += floatval($mysql_usage_row['customerusage']);
+ } else {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_WARNING, "Cannot get usage for database " . $row_database['databasename'] . ".");
+ }
+ } else {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_WARNING, "Seems like the database " . $row_database['databasename'] . " had been removed manually.");
+ }
+ }
+
+ Database::needRoot(false);
+
+ // We are using the file-system quota, this will speed up the diskusage - collection
+ if (Settings::Get('system.diskquota_enabled')) {
+ $usedquota = \Froxlor\FileDir::getFilesystemQuota();
+ }
+
+ /**
+ * MAIL-Traffic
+ */
+ if (Settings::Get("system.mailtraffic_enabled")) {
+ $mailTrafficCalc = new \Froxlor\MailLogParser(Settings::Get("system.last_traffic_run"));
+ }
+
+ $result_stmt = Database::query("SELECT * FROM `" . TABLE_PANEL_CUSTOMERS . "` ORDER BY `customerid` ASC");
+
+ $currentDate = date("Y-m-d");
+
+ $current_stamp = time();
+ $current_year = date('Y', $current_stamp);
+ $current_month = date('m', $current_stamp);
+ $current_day = date('d', $current_stamp);
+
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ /**
+ * HTTP-Traffic
+ */
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'http traffic for ' . $row['loginname'] . ' started...');
+ $httptraffic = 0;
+
+ if (isset($domainlist[$row['customerid']]) && is_array($domainlist[$row['customerid']]) && count($domainlist[$row['customerid']]) != 0) {
+ // Examining which caption to use for default webalizer stats...
+ if ($row['standardsubdomain'] != '0') {
+ // ... of course we'd prefer to use the standardsubdomain ...
+ $caption = $domainlist[$row['customerid']][$row['standardsubdomain']];
+ } else {
+ // ... but if there is no standardsubdomain, we have to use the loginname ...
+ $caption = $row['loginname'];
+
+ // ... which results in non-usable links to files in the stats, so lets have a look if we find a domain which is not speciallogfiledomain
+ foreach ($domainlist[$row['customerid']] as $domainid => $domain) {
+
+ if (! isset($speciallogfile_domainlist[$row['customerid']]) || ! isset($speciallogfile_domainlist[$row['customerid']][$domainid])) {
+ $caption = $domain;
+ break;
+ }
+ }
+ }
+
+ $httptraffic = 0;
+ reset($domainlist[$row['customerid']]);
+
+ if (isset($speciallogfile_domainlist[$row['customerid']]) && is_array($speciallogfile_domainlist[$row['customerid']]) && count($speciallogfile_domainlist[$row['customerid']]) != 0) {
+ reset($speciallogfile_domainlist[$row['customerid']]);
+ if (Settings::Get('system.awstats_enabled') == '0') {
+ foreach ($speciallogfile_domainlist[$row['customerid']] as $domainid => $domain) {
+ $httptraffic += floatval(self::callWebalizerGetTraffic($row['loginname'] . '-' . $domain, $row['documentroot'] . '/webalizer/' . $domain . '/', $domain, $domainlist[$row['customerid']]));
+ }
+ }
+ }
+
+ reset($domainlist[$row['customerid']]);
+
+ // callAwstatsGetTraffic is called ONLY HERE and
+ // *not* also in the special-logfiles-loop, because the function
+ // will iterate through all customer-domains and the awstats-configs
+ // know the logfile-name, #246
+ if (Settings::Get('system.awstats_enabled') == '1') {
+ $httptraffic += floatval(self::callAwstatsGetTraffic($row['customerid'], $row['documentroot'] . '/awstats/', $domainlist[$row['customerid']], $current_stamp));
+ } else {
+ $httptraffic += floatval(self::callWebalizerGetTraffic($row['loginname'], $row['documentroot'] . '/webalizer/', $caption, $domainlist[$row['customerid']]));
+ }
+
+ // make the stuff readable for the customer, #258
+ \Froxlor\Http\Statistics::makeChownWithNewStats($row);
+ }
+
+ /**
+ * FTP-Traffic
+ */
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'ftp traffic for ' . $row['loginname'] . ' started...');
+ $ftptraffic_stmt = Database::prepare("
+ SELECT SUM(`up_bytes`) AS `up_bytes_sum`, SUM(`down_bytes`) AS `down_bytes_sum`
+ FROM `" . TABLE_FTP_USERS . "` WHERE `customerid` = :customerid
+ ");
+ $ftptraffic = Database::pexecute_first($ftptraffic_stmt, array(
+ 'customerid' => $row['customerid']
+ ));
+
+ if (! is_array($ftptraffic)) {
+ $ftptraffic = array(
+ 'up_bytes_sum' => 0,
+ 'down_bytes_sum' => 0
+ );
+ }
+
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_FTP_USERS . "` SET `up_bytes` = '0', `down_bytes` = '0' WHERE `customerid` = :customerid
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'customerid' => $row['customerid']
+ ));
+
+ /**
+ * Mail-Traffic
+ */
+ $mailtraffic = 0;
+ if (Settings::Get("system.mailtraffic_enabled")) {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'mail traffic usage for ' . $row['loginname'] . " started...");
+
+ $domains_stmt = Database::prepare("SELECT domain FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `customerid` = :cid");
+ Database::pexecute($domains_stmt, array(
+ "cid" => $row['customerid']
+ ));
+ while ($domainRow = $domains_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $domainMailTraffic = $mailTrafficCalc->getDomainTraffic($domainRow["domain"]);
+ if (! is_array($domainMailTraffic)) {
+ continue;
+ }
+
+ foreach ($domainMailTraffic as $dateTraffic => $dayTraffic) {
+ $dayTraffic = floatval($dayTraffic / 1024);
+
+ list ($year, $month, $day) = explode("-", $dateTraffic);
+ if ($dateTraffic == $currentDate) {
+ $mailtraffic = $dayTraffic;
+ } else {
+ // Check if an entry for the given day exists
+ $stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_TRAFFIC . "`
+ WHERE `customerid` = :cid
+ AND `year` = :year
+ AND `month` = :month
+ AND `day` = :day
+ ");
+ $params = array(
+ "cid" => $row['customerid'],
+ "year" => $year,
+ "month" => $month,
+ "day" => $day
+ );
+ Database::pexecute($stmt, $params);
+ if ($stmt->rowCount() > 0) {
+ $updRow = $stmt->fetch(\PDO::FETCH_ASSOC);
+ $upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_TRAFFIC . "` SET
+ `mail` = :mail
+ WHERE `id` = :id
+ ");
+ Database::pexecute($upd_stmt, array(
+ "mail" => $updRow['mail'] + $dayTraffic,
+ "id" => $updRow['id']
+ ));
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Total Traffic
+ */
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'total traffic for ' . $row['loginname'] . ' started');
+ $current_traffic = array();
+ $current_traffic['http'] = floatval($httptraffic);
+ $current_traffic['ftp_up'] = floatval(($ftptraffic['up_bytes_sum'] / 1024));
+ $current_traffic['ftp_down'] = floatval(($ftptraffic['down_bytes_sum'] / 1024));
+ $current_traffic['mail'] = floatval($mailtraffic);
+ $current_traffic['all'] = $current_traffic['http'] + $current_traffic['ftp_up'] + $current_traffic['ftp_down'] + $current_traffic['mail'];
+
+ $ins_data = array(
+ 'customerid' => $row['customerid'],
+ 'year' => $current_year,
+ 'month' => $current_month,
+ 'day' => $current_day,
+ 'stamp' => $current_stamp,
+ 'http' => $current_traffic['http'],
+ 'ftp_up' => $current_traffic['ftp_up'],
+ 'ftp_down' => $current_traffic['ftp_down'],
+ 'mail' => $current_traffic['mail']
+ );
+ $ins_stmt = Database::prepare("
+ INSERT INTO `" . TABLE_PANEL_TRAFFIC . "` SET
+ `customerid` = :customerid,
+ `year` = :year,
+ `month` = :month,
+ `day` = :day,
+ `stamp` = :stamp,
+ `http` = :http,
+ `ftp_up` = :ftp_up,
+ `ftp_down` = :ftp_down,
+ `mail` = :mail
+ ");
+ Database::pexecute($ins_stmt, $ins_data);
+
+ $sum_month_traffic_stmt = Database::prepare("
+ SELECT SUM(`http`) AS `http`, SUM(`ftp_up`) AS `ftp_up`, SUM(`ftp_down`) AS `ftp_down`, SUM(`mail`) AS `mail`
+ FROM `" . TABLE_PANEL_TRAFFIC . "` WHERE `year` = :year AND `month` = :month AND `customerid` = :customerid
+ ");
+ $sum_month_traffic = Database::pexecute_first($sum_month_traffic_stmt, array(
+ 'year' => $current_year,
+ 'month' => $current_month,
+ 'customerid' => $row['customerid']
+ ));
+ $sum_month_traffic['all'] = $sum_month_traffic['http'] + $sum_month_traffic['ftp_up'] + $sum_month_traffic['ftp_down'] + $sum_month_traffic['mail'];
+
+ if (! isset($admin_traffic[$row['adminid']]) || ! is_array($admin_traffic[$row['adminid']])) {
+ $admin_traffic[$row['adminid']]['http'] = 0;
+ $admin_traffic[$row['adminid']]['ftp_up'] = 0;
+ $admin_traffic[$row['adminid']]['ftp_down'] = 0;
+ $admin_traffic[$row['adminid']]['mail'] = 0;
+ $admin_traffic[$row['adminid']]['all'] = 0;
+ $admin_traffic[$row['adminid']]['sum_month'] = 0;
+ }
+
+ $admin_traffic[$row['adminid']]['http'] += $current_traffic['http'];
+ $admin_traffic[$row['adminid']]['ftp_up'] += $current_traffic['ftp_up'];
+ $admin_traffic[$row['adminid']]['ftp_down'] += $current_traffic['ftp_down'];
+ $admin_traffic[$row['adminid']]['mail'] += $current_traffic['mail'];
+ $admin_traffic[$row['adminid']]['all'] += $current_traffic['all'];
+ $admin_traffic[$row['adminid']]['sum_month'] += $sum_month_traffic['all'];
+
+ /**
+ * WebSpace-Usage
+ */
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'calculating webspace usage for ' . $row['loginname']);
+ $webspaceusage = 0;
+
+ // Using repquota, it's faster using this tool than using du traversing the complete directory
+ if (Settings::Get('system.diskquota_enabled') && isset($usedquota[$row['guid']]['block']['used']) && $usedquota[$row['guid']]['block']['used'] >= 1) {
+ // We may use the array we created earlier, the used diskspace is stored in [][block][used]
+ $webspaceusage = floatval($usedquota[$row['guid']]['block']['used']);
+ } else {
+
+ // Use the old fashioned way with "du"
+ if (file_exists($row['documentroot']) && is_dir($row['documentroot'])) {
+ $back = \Froxlor\FileDir::safe_exec('du -sk ' . escapeshellarg($row['documentroot']) . '');
+ foreach ($back as $backrow) {
+ $webspaceusage = explode(' ', $backrow);
+ }
+
+ $webspaceusage = floatval($webspaceusage['0']);
+ unset($back);
+ } else {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_WARNING, 'documentroot ' . $row['documentroot'] . ' does not exist');
+ }
+ }
+
+ /**
+ * MailSpace-Usage
+ */
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'calculating mailspace usage for ' . $row['loginname']);
+ $emailusage = 0;
+
+ $maildir = \Froxlor\FileDir::makeCorrectDir(Settings::Get('system.vmail_homedir') . $row['loginname']);
+ if (file_exists($maildir) && is_dir($maildir)) {
+ $back = \Froxlor\FileDir::safe_exec('du -sk ' . escapeshellarg($maildir) . '');
+ foreach ($back as $backrow) {
+ $emailusage = explode(' ', $backrow);
+ }
+
+ $emailusage = floatval($emailusage['0']);
+ unset($back);
+ } else {
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_WARNING, 'maildir ' . $maildir . ' does not exist');
+ }
+
+ /**
+ * MySQLSpace-Usage
+ */
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, 'calculating mysqlspace usage for ' . $row['loginname']);
+ $mysqlusage = 0;
+
+ if (isset($mysqlusage_all[$row['customerid']])) {
+ $mysqlusage = floatval($mysqlusage_all[$row['customerid']] / 1024);
+ }
+
+ $current_diskspace = array();
+ $current_diskspace['webspace'] = floatval($webspaceusage);
+ $current_diskspace['mail'] = floatval($emailusage);
+ $current_diskspace['mysql'] = floatval($mysqlusage);
+ $current_diskspace['all'] = $current_diskspace['webspace'] + $current_diskspace['mail'] + $current_diskspace['mysql'];
+
+ $ins_data = array(
+ 'customerid' => $row['customerid'],
+ 'year' => $current_year,
+ 'month' => $current_month,
+ 'day' => $current_day,
+ 'stamp' => $current_stamp,
+ 'webspace' => $current_diskspace['webspace'],
+ 'mail' => $current_diskspace['mail'],
+ 'mysql' => $current_diskspace['mysql']
+ );
+ $ins_stmt = Database::prepare("
+ INSERT INTO `" . TABLE_PANEL_DISKSPACE . "` SET
+ `customerid` = :customerid,
+ `year` = :year,
+ `month` = :month,
+ `day` = :day,
+ `stamp` = :stamp,
+ `webspace` = :webspace,
+ `mail` = :mail,
+ `mysql` = :mysql
+ ");
+ Database::pexecute($ins_stmt, $ins_data);
+
+ if (! isset($admin_diskspace[$row['adminid']]) || ! is_array($admin_diskspace[$row['adminid']])) {
+ $admin_diskspace[$row['adminid']] = array();
+ $admin_diskspace[$row['adminid']]['webspace'] = 0;
+ $admin_diskspace[$row['adminid']]['mail'] = 0;
+ $admin_diskspace[$row['adminid']]['mysql'] = 0;
+ $admin_diskspace[$row['adminid']]['all'] = 0;
+ }
+
+ $admin_diskspace[$row['adminid']]['webspace'] += $current_diskspace['webspace'];
+ $admin_diskspace[$row['adminid']]['mail'] += $current_diskspace['mail'];
+ $admin_diskspace[$row['adminid']]['mysql'] += $current_diskspace['mysql'];
+ $admin_diskspace[$row['adminid']]['all'] += $current_diskspace['all'];
+
+ /**
+ * Total Usage
+ */
+ $diskusage = floatval($webspaceusage + $emailusage + $mysqlusage);
+
+ $upd_data = array(
+ 'diskspace' => $current_diskspace['all'],
+ 'traffic' => $sum_month_traffic['all'],
+ 'customerid' => $row['customerid']
+ );
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET
+ `diskspace_used` = :diskspace,
+ `traffic_used` = :traffic
+ WHERE `customerid` = :customerid
+ ");
+ Database::pexecute($upd_stmt, $upd_data);
+
+ /**
+ * Proftpd Quota
+ */
+ $upd_data = array(
+ 'biu' => ($current_diskspace['all'] * 1024),
+ 'loginname' => $row['loginname'],
+ 'loginnamelike' => $row['loginname'] . Settings::Get('customer.ftpprefix') . "%"
+ );
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_FTP_QUOTATALLIES . "` SET
+ `bytes_in_used` = :biu WHERE `name` = :loginname OR `name` LIKE :loginnamelike
+ ");
+ Database::pexecute($upd_stmt, $upd_data);
+
+ /**
+ * Pureftpd Quota
+ */
+ if (Settings::Get('system.ftpserver') == "pureftpd") {
+
+ $result_quota_stmt = Database::prepare("
+ SELECT homedir FROM `" . TABLE_FTP_USERS . "` WHERE customerid = :customerid
+ ");
+ Database::pexecute($result_quota_stmt, array(
+ 'customerid' => $row['customerid']
+ ));
+
+ // get correct user
+ if ((Settings::Get('system.mod_fcgid') == 1 || Settings::Get('phpfpm.enabled') == 1) && $row['deactivated'] == '0') {
+ $user = $row['loginname'];
+ $group = $row['loginname'];
+ } else {
+ $user = $row['guid'];
+ $group = $row['guid'];
+ }
+
+ while ($row_quota = $result_quota_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $quotafile = "" . $row_quota['homedir'] . ".ftpquota";
+ $fh = fopen($quotafile, 'w');
+ $stringdata = "0 " . $current_diskspace['all'] * 1024 . "";
+ fwrite($fh, $stringdata);
+ fclose($fh);
+ \Froxlor\FileDir::safe_exec('chown ' . $user . ':' . $group . ' ' . escapeshellarg($quotafile) . '');
+ }
+ }
+ }
+
+ /**
+ * Admin Usage
+ */
+ $result_stmt = Database::query("SELECT `adminid` FROM `" . TABLE_PANEL_ADMINS . "` ORDER BY `adminid` ASC");
+
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ if (isset($admin_traffic[$row['adminid']])) {
+
+ $ins_data = array(
+ 'adminid' => $row['adminid'],
+ 'year' => $current_year,
+ 'month' => $current_month,
+ 'day' => $current_day,
+ 'stamp' => $current_stamp,
+ 'http' => $admin_traffic[$row['adminid']]['http'],
+ 'ftp_up' => $admin_traffic[$row['adminid']]['ftp_up'],
+ 'ftp_down' => $admin_traffic[$row['adminid']]['ftp_down'],
+ 'mail' => $admin_traffic[$row['adminid']]['mail']
+ );
+ $ins_stmt = Database::prepare("
+ INSERT INTO `" . TABLE_PANEL_TRAFFIC_ADMINS . "` SET
+ `adminid` = :adminid,
+ `year` = :year,
+ `month` = :month,
+ `day` = :day,
+ `stamp` = :stamp,
+ `http` = :http,
+ `ftp_up` = :ftp_up,
+ `ftp_down` = :ftp_down,
+ `mail` = :mail
+ ");
+ Database::pexecute($ins_stmt, $ins_data);
+
+ $upd_data = array(
+ 'traffic' => $admin_traffic[$row['adminid']]['sum_month'],
+ 'adminid' => $row['adminid']
+ );
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_ADMINS . "` SET
+ `traffic_used` = :traffic
+ WHERE `adminid` = :adminid
+ ");
+ Database::pexecute($upd_stmt, $upd_data);
+ }
+
+ if (isset($admin_diskspace[$row['adminid']])) {
+ $upd_data = array(
+ 'diskspace' => $admin_diskspace[$row['adminid']]['all'],
+ 'adminid' => $row['adminid']
+ );
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_ADMINS . "` SET
+ `diskspace_used` = :diskspace
+ WHERE `adminid` = :adminid
+ ");
+ Database::pexecute($upd_stmt, $upd_data);
+ }
+ }
+
+ Database::query("UPDATE `" . TABLE_PANEL_SETTINGS . "` SET `value` = UNIX_TIMESTAMP() WHERE `settinggroup` = 'system' AND `varname` = 'last_traffic_run'");
+
+ if (function_exists('pcntl_fork') && ! defined('CRON_NOFORK_FLAG')) {
+ @unlink($TrafficLock);
+ die();
+ }
+ }
+
+ private static function awstatsDoSingleDomain($domain, $outputdir)
+ {
+ $returnval = 0;
+
+ $domainconfig = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.awstats_conf') . '/awstats.' . $domain . '.conf');
+
+ if (file_exists($domainconfig)) {
+
+ $outputdir = \Froxlor\FileDir::makeCorrectDir($outputdir . '/' . $domain);
+ $staticOutputdir = \Froxlor\FileDir::makeCorrectDir($outputdir . '/' . date('Y') . '-' . date('m'));
+
+ if (! is_dir($staticOutputdir)) {
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg($staticOutputdir));
+ }
+
+ // check for correct path of awstats_buildstaticpages.pl
+ $awbsp = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.awstats_path') . '/awstats_buildstaticpages.pl');
+ $awprog = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.awstats_awstatspath') . '/awstats.pl');
+
+ if (! file_exists($awbsp)) {
+ echo "WANRING: Necessary awstats_buildstaticpages.pl script could not be found, no traffic is being calculated and no stats are generated. Please check your AWStats-Path setting";
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_WARNING, "Necessary awstats_buildstaticpages.pl script could not be found, no traffic is being calculated and no stats are generated. Please check your AWStats-Path setting");
+ exit();
+ }
+
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Running awstats_buildstaticpages.pl for domain '" . $domain . "' (Output: '" . $staticOutputdir . "')");
+ \Froxlor\FileDir::safe_exec($awbsp . ' -awstatsprog=' . escapeshellarg($awprog) . ' -update -month=' . date('m') . ' -year=' . date('Y') . ' -config=' . $domain . ' -dir=' . escapeshellarg($staticOutputdir));
+
+ // update our awstats index files
+ self::awstatsGenerateIndex($domain, $outputdir);
+
+ // the default selection is 'current',
+ // so link the latest dir to it
+ $new_current = \Froxlor\FileDir::makeCorrectFile($outputdir . '/current');
+ \Froxlor\FileDir::safe_exec('ln -fTs ' . escapeshellarg($staticOutputdir) . ' ' . escapeshellarg($new_current));
+
+ // statistics file looks like: 'awstats[month][year].[domain].txt'
+ $file = \Froxlor\FileDir::makeCorrectFile($outputdir . '/awstats' . date('mY', time()) . '.' . $domain . '.txt');
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Gathering traffic information from '" . $file . "'");
+
+ if (file_exists($file)) {
+
+ $content = @file_get_contents($file);
+ if ($content !== false) {
+ $content_array = explode("\n", $content);
+
+ $count_bdw = false;
+ foreach ($content_array as $line) {
+ // skip empty lines and comments
+ if (trim($line) == '' || substr(trim($line), 0, 1) == '#') {
+ continue;
+ }
+
+ $parts = explode(' ', $line);
+
+ if (isset($parts[0]) && strtoupper($parts[0]) == 'BEGIN_DOMAIN') {
+ $count_bdw = true;
+ }
+
+ if ($count_bdw) {
+ if (isset($parts[0]) && strtoupper($parts[0]) == 'END_DOMAIN') {
+ $count_bdw = false;
+ break;
+ } elseif (isset($parts[3])) {
+ $returnval += floatval($parts[3]);
+ }
+ }
+ }
+ }
+ }
+ }
+ return $returnval;
+ }
+
+ private static function awstatsGenerateIndex($domain, $outputdir)
+ {
+
+ // Generation header
+ $header = "\n";
+
+ // Looking for {year}-{month} directories
+ $entries = array();
+ foreach (scandir($outputdir) as $a) {
+ if (is_dir(\Froxlor\FileDir::makeCorrectDir($outputdir . '/' . $a)) && preg_match('/^[0-9]{4}-[0-9]{2}$/', $a)) {
+ array_push($entries, '' . $a . ' ');
+ }
+ }
+
+ // These are the variables we will replace
+ $regex = array(
+ '/\{SITE_DOMAIN\}/',
+ '/\{SELECT_ENTRIES\}/'
+ );
+
+ $replace = array(
+ $domain,
+ implode($entries)
+ );
+
+ // File names
+ $index_file = \Froxlor\Froxlor::getInstallDir() . '/templates/misc/awstats/index.html';
+ $index_file = \Froxlor\FileDir::makeCorrectFile($index_file);
+ $nav_file = \Froxlor\Froxlor::getInstallDir() . '/templates/misc/awstats/nav.html';
+ $nav_file = \Froxlor\FileDir::makeCorrectFile($nav_file);
+
+ // Write the index file
+ // 'index.html' used to be a symlink (ignore errors in case this is the first run and no index.html exists yet)
+ @unlink(\Froxlor\FileDir::makeCorrectFile($outputdir . '/' . 'index.html'));
+
+ $awstats_index_file = fopen(\Froxlor\FileDir::makeCorrectFile($outputdir . '/' . 'index.html'), 'w');
+ $awstats_index_tpl = fopen($index_file, 'r');
+
+ // Write the header
+ fwrite($awstats_index_file, $header);
+
+ // Write the configuration file
+ while (($line = fgets($awstats_index_tpl, 4096)) !== false) {
+ if (! preg_match('/^#/', $line) && trim($line) != '') {
+ fwrite($awstats_index_file, preg_replace($regex, $replace, $line));
+ }
+ }
+ fclose($awstats_index_file);
+ fclose($awstats_index_tpl);
+
+ // Write the nav file
+ $awstats_nav_file = fopen(\Froxlor\FileDir::makeCorrectFile($outputdir . '/' . 'nav.html'), 'w');
+ $awstats_nav_tpl = fopen($nav_file, 'r');
+
+ // Write the header
+ fwrite($awstats_nav_file, $header);
+
+ // Write the configuration file
+ while (($line = fgets($awstats_nav_tpl, 4096)) !== false) {
+ if (! preg_match('/^#/', $line) && trim($line) != '') {
+ fwrite($awstats_nav_file, preg_replace($regex, $replace, $line));
+ }
+ }
+ fclose($awstats_nav_file);
+ fclose($awstats_nav_tpl);
+
+ return;
+ }
+
+ private static function callAwstatsGetTraffic($customerid, $outputdir, $usersdomainlist, $current_stamp)
+ {
+ $returnval = 0;
+
+ foreach ($usersdomainlist as $singledomain) {
+ // as we check for the config-model awstats will only parse
+ // 'real' domains and no subdomains which are aliases in the
+ // model-config-file.
+ $returnval += self::awstatsDoSingleDomain($singledomain, $outputdir);
+ }
+
+ /**
+ * as of #124, awstats traffic is saved in bytes instead
+ * of kilobytes (like webalizer does)
+ */
+ $returnval = floatval($returnval / 1024);
+
+ /**
+ * now, because this traffic is being saved daily, we have to
+ * subtract the values from all the month's values to return
+ * a sane value for our panel_traffic and to remain the whole stats
+ * (awstats overwrites the customers .html stats-files)
+ */
+ if ($customerid !== false) {
+
+ $result_stmt = Database::prepare("
+ SELECT SUM(`http`) as `trafficmonth` FROM `" . TABLE_PANEL_TRAFFIC . "`
+ WHERE `customerid` = :customerid
+ AND `year` = :year AND `month` = :month
+ ");
+ $result_data = array(
+ 'customerid' => $customerid,
+ 'year' => date('Y', $current_stamp),
+ 'month' => date('m', $current_stamp)
+ );
+ $result = Database::pexecute_first($result_stmt, $result_data);
+
+ if (is_array($result) && isset($result['trafficmonth'])) {
+ $returnval = ($returnval - floatval($result['trafficmonth']));
+ }
+ }
+
+ return floatval($returnval);
+ }
+
+ /**
+ * Function which make webalizer statistics and returns used traffic since last run
+ *
+ * @param
+ * string Name of logfile
+ * @param
+ * string Place where stats should be build
+ * @param
+ * string Caption for webalizer output
+ * @return int Used traffic
+ * @author Florian Lippert
+ */
+ private static function callWebalizerGetTraffic($logfile, $outputdir, $caption, $usersdomainlist)
+ {
+ $returnval = 0;
+
+ $logfile = \Froxlor\FileDir::makeCorrectFile(Settings::Get('system.logfiles_directory') . $logfile . '-access.log');
+ if (file_exists($logfile)) {
+ $domainargs = '';
+ foreach ($usersdomainlist as $domain) {
+ // hide referer
+ $domainargs .= ' -r ' . escapeshellarg($domain);
+ }
+
+ $outputdir = \Froxlor\FileDir::makeCorrectDir($outputdir);
+ if (! file_exists($outputdir)) {
+ \Froxlor\FileDir::safe_exec('mkdir -p ' . escapeshellarg($outputdir));
+ }
+
+ if (file_exists($outputdir . 'webalizer.hist.1')) {
+ @unlink($outputdir . 'webalizer.hist.1');
+ }
+
+ if (file_exists($outputdir . 'webalizer.hist') && ! file_exists($outputdir . 'webalizer.hist.1')) {
+ \Froxlor\FileDir::safe_exec('cp ' . escapeshellarg($outputdir . 'webalizer.hist') . ' ' . escapeshellarg($outputdir . 'webalizer.hist.1'));
+ }
+
+ $verbosity = '';
+ if (Settings::Get('system.webalizer_quiet') == '1') {
+ $verbosity = '-q';
+ } elseif (Settings::Get('system.webalizer_quiet') == '2') {
+ $verbosity = '-Q';
+ }
+
+ $we = '/usr/bin/webalizer';
+
+ // FreeBSD uses other paths, #140
+ if (! file_exists($we)) {
+ $we = '/usr/local/bin/webalizer';
+ }
+
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Running webalizer for domain '" . $caption . "'");
+ \Froxlor\FileDir::safe_exec($we . ' ' . $verbosity . ' -p -o ' . escapeshellarg($outputdir) . ' -n ' . escapeshellarg($caption) . $domainargs . ' ' . escapeshellarg($logfile));
+
+ /**
+ * Format of webalizer.hist-files:
+ * Month: $webalizer_hist_row['0']
+ * Year: $webalizer_hist_row['1']
+ * KB: $webalizer_hist_row['5']
+ */
+ $httptraffic = array();
+ $webalizer_hist = @file_get_contents($outputdir . 'webalizer.hist');
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Gathering traffic information from '" . $webalizer_hist . "'");
+
+ $webalizer_hist_rows = explode("\n", $webalizer_hist);
+ foreach ($webalizer_hist_rows as $webalizer_hist_row) {
+ if ($webalizer_hist_row != '') {
+
+ $webalizer_hist_row = explode(' ', $webalizer_hist_row);
+
+ if (isset($webalizer_hist_row['0']) && isset($webalizer_hist_row['1']) && isset($webalizer_hist_row['5'])) {
+ $month = intval($webalizer_hist_row['0']);
+ $year = intval($webalizer_hist_row['1']);
+ $traffic = floatval($webalizer_hist_row['5']);
+
+ if (! isset($httptraffic[$year])) {
+ $httptraffic[$year] = array();
+ }
+
+ $httptraffic[$year][$month] = $traffic;
+ }
+ }
+ }
+
+ reset($httptraffic);
+ $httptrafficlast = array();
+ $webalizer_lasthist = @file_get_contents($outputdir . 'webalizer.hist.1');
+ \Froxlor\FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Gathering traffic information from '" . $webalizer_lasthist . "'");
+
+ $webalizer_lasthist_rows = explode("\n", $webalizer_lasthist);
+ foreach ($webalizer_lasthist_rows as $webalizer_lasthist_row) {
+ if ($webalizer_lasthist_row != '') {
+
+ $webalizer_lasthist_row = explode(' ', $webalizer_lasthist_row);
+
+ if (isset($webalizer_lasthist_row['0']) && isset($webalizer_lasthist_row['1']) && isset($webalizer_lasthist_row['5'])) {
+ $month = intval($webalizer_lasthist_row['0']);
+ $year = intval($webalizer_lasthist_row['1']);
+ $traffic = floatval($webalizer_lasthist_row['5']);
+
+ if (! isset($httptrafficlast[$year])) {
+ $httptrafficlast[$year] = array();
+ }
+
+ $httptrafficlast[$year][$month] = $traffic;
+ }
+ }
+ }
+
+ reset($httptrafficlast);
+ foreach ($httptraffic as $year => $months) {
+ foreach ($months as $month => $traffic) {
+ if (! isset($httptrafficlast[$year][$month])) {
+ $returnval += $traffic;
+ } elseif ($httptrafficlast[$year][$month] < $httptraffic[$year][$month]) {
+ $returnval += ($httptraffic[$year][$month] - $httptrafficlast[$year][$month]);
+ }
+ }
+ }
+ }
+
+ return floatval($returnval);
+ }
+}
diff --git a/lib/Froxlor/Customer/Customer.php b/lib/Froxlor/Customer/Customer.php
new file mode 100644
index 00000000..2b04d6dd
--- /dev/null
+++ b/lib/Froxlor/Customer/Customer.php
@@ -0,0 +1,75 @@
+ $customerid
+ ));
+
+ if (isset($customer[$varname])) {
+ return $customer[$varname];
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * returns the loginname of a customer by given uid
+ *
+ * @param int $uid
+ * uid of customer
+ *
+ * @return string customers loginname
+ */
+ public static function getLoginNameByUid($uid = null)
+ {
+ $result_stmt = Database::prepare("
+ SELECT `loginname` FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `guid` = :guid
+ ");
+ $result = Database::pexecute_first($result_stmt, array(
+ 'guid' => $uid
+ ));
+
+ if (is_array($result) && isset($result['loginname'])) {
+ return $result['loginname'];
+ }
+ return false;
+ }
+
+ /**
+ * Function customerHasPerlEnabled
+ *
+ * returns true or false whether perl is
+ * enabled for the given customer
+ *
+ * @param
+ * int customer-id
+ *
+ * @return boolean
+ */
+ public static function customerHasPerlEnabled($cid = 0)
+ {
+ if ($cid > 0) {
+ $result_stmt = Database::prepare("
+ SELECT `perlenabled` FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `customerid` = :cid");
+ Database::pexecute($result_stmt, array(
+ 'cid' => $cid
+ ));
+ $result = $result_stmt->fetch(\PDO::FETCH_ASSOC);
+
+ if (is_array($result) && isset($result['perlenabled'])) {
+ return ($result['perlenabled'] == '1') ? true : false;
+ }
+ }
+ return false;
+ }
+}
diff --git a/lib/Froxlor/Database/Database.php b/lib/Froxlor/Database/Database.php
new file mode 100644
index 00000000..d0791148
--- /dev/null
+++ b/lib/Froxlor/Database/Database.php
@@ -0,0 +1,532 @@
+
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Classes
+ *
+ * @since 0.9.31
+ *
+ */
+
+/**
+ * Class Database
+ *
+ * Wrapper-class for PHP-PDO
+ *
+ * @copyright (c) the authors
+ * @author Michael Kaufmann
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Classes
+ *
+ * @method static \PDOStatement prepare($statement, array $driver_options = null) Prepares a statement for execution and returns a statement object
+ * @method static \PDOStatement query ($statement) Executes an SQL statement, returning a result set as a PDOStatement object
+ * @method static string lastInsertId ($name = null) Returns the ID of the last inserted row or sequence value
+ * @method static string quote ($string, $parameter_type = null) Quotes a string for use in a query.
+ */
+class Database
+{
+
+ /**
+ * current database link
+ *
+ * @var object
+ */
+ private static $link = null;
+
+ /**
+ * indicator whether to use root-connection or not
+ */
+ private static $needroot = false;
+
+ /**
+ * indicator which database-server we're on (not really used)
+ */
+ private static $dbserver = 0;
+
+ /**
+ * used database-name
+ */
+ private static $dbname = null;
+
+ /**
+ * sql-access data
+ */
+ private static $needsqldata = false;
+
+ private static $sqldata = null;
+
+ /**
+ * Wrapper for PDOStatement::execute so we can catch the PDOException
+ * and display the error nicely on the panel
+ *
+ * @param \PDOStatement $stmt
+ * @param array $params
+ * (optional)
+ * @param bool $showerror
+ * suppress errordisplay (default true)
+ */
+ public static function pexecute(&$stmt, $params = null, $showerror = true, $json_response = false)
+ {
+ try {
+ $stmt->execute($params);
+ } catch (\PDOException $e) {
+ self::showerror($e, $showerror, $json_response, $stmt);
+ }
+ }
+
+ /**
+ * Wrapper for PDOStatement::execute so we can catch the PDOException
+ * and display the error nicely on the panel - also fetches the
+ * result from the statement and returns the resulting array
+ *
+ * @param \PDOStatement $stmt
+ * @param array $params
+ * (optional)
+ * @param bool $showerror
+ * suppress errordisplay (default true)
+ *
+ * @return array
+ */
+ public static function pexecute_first(&$stmt, $params = null, $showerror = true, $json_response = false)
+ {
+ self::pexecute($stmt, $params, $showerror, $json_response);
+ return $stmt->fetch(\PDO::FETCH_ASSOC);
+ }
+
+ /**
+ * returns the number of found rows of the last select query
+ *
+ * @return int
+ */
+ public static function num_rows()
+ {
+ return Database::query("SELECT FOUND_ROWS()")->fetchColumn();
+ }
+
+ /**
+ * returns the database-name which is used
+ *
+ * @return string
+ */
+ public static function getDbName()
+ {
+ return self::$dbname;
+ }
+
+ /**
+ * enabled the usage of a root-connection to the database
+ * Note: must be called *before* any prepare/query/etc.
+ * and should be called again with 'false'-parameter to resume
+ * the 'normal' database-connection
+ *
+ * @param bool $needroot
+ * @param int $dbserver
+ * optional
+ */
+ public static function needRoot($needroot = false, $dbserver = 0)
+ {
+ // force re-connecting to the db with corresponding user
+ // and set the $dbserver (mostly to 0 = default)
+ self::setServer($dbserver);
+ self::$needroot = $needroot;
+ }
+
+ /**
+ * enable the temporary access to sql-access data
+ * note: if you want root-sqldata you need to
+ * call needRoot(true) first.
+ * Also, this will
+ * only give you the data ONCE as it disable itself
+ * after the first access to the data
+ */
+ public static function needSqlData()
+ {
+ self::$needsqldata = true;
+ self::$sqldata = array();
+ self::$link = null;
+ // we need a connection here because
+ // if getSqlData() is called RIGHT after
+ // this function and no "real" PDO
+ // function was called, getDB() wasn't
+ // involved and no data collected
+ self::getDB();
+ }
+
+ /**
+ * returns the sql-access data as array using indeces
+ * 'user', 'passwd' and 'host'.
+ * Returns false if not enabled
+ *
+ * @return array|bool
+ */
+ public static function getSqlData()
+ {
+ $return = false;
+ if (self::$sqldata !== null && is_array(self::$sqldata) && isset(self::$sqldata['user'])) {
+ $return = self::$sqldata;
+ // automatically disable sql-data
+ self::$sqldata = null;
+ self::$needsqldata = false;
+ }
+ return $return;
+ }
+
+ /**
+ * return number of characters that are allowed to use as username
+ *
+ * @return int
+ */
+ public static function getSqlUsernameLength()
+ {
+ // MySQL user names can be up to 32 characters long (16 characters before MySQL 5.7.8).
+ $mysql_max = 32;
+ if (version_compare(Database::getAttribute(\PDO::ATTR_SERVER_VERSION), '5.7.8', '<')) {
+ $mysql_max = 16;
+ }
+ return $mysql_max;
+ }
+
+ /**
+ * let's us interact with the PDO-Object by using static
+ * call like "Database::function()"
+ *
+ * @param string $name
+ * @param mixed $args
+ *
+ * @return mixed
+ */
+ public static function __callStatic($name, $args)
+ {
+ $callback = array(
+ self::getDB(),
+ $name
+ );
+ $result = null;
+ try {
+ $result = call_user_func_array($callback, $args);
+ } catch (\PDOException $e) {
+ self::showerror($e);
+ }
+ return $result;
+ }
+
+ /**
+ * set the database-server (relevant for root-connection)
+ *
+ * @param int $dbserver
+ */
+ private static function setServer($dbserver = 0)
+ {
+ self::$dbserver = $dbserver;
+ self::$link = null;
+ }
+
+ /**
+ * function that will be called on every static call
+ * which connects to the database if necessary
+ *
+ * @param bool $root
+ *
+ * @return object
+ */
+ private static function getDB()
+ {
+ if (! extension_loaded('pdo') || in_array("mysql", \PDO::getAvailableDrivers()) == false) {
+ self::showerror(new \Exception("The php PDO extension or PDO-MySQL driver is not available"));
+ }
+
+ // do we got a connection already?
+ if (self::$link) {
+ // return it
+ return self::$link;
+ }
+
+ // include userdata.inc.php
+ require \Froxlor\Froxlor::getInstallDir() . "/lib/userdata.inc.php";
+
+ // le format
+ if (self::$needroot == true && isset($sql['root_user']) && isset($sql['root_password']) && (! isset($sql_root) || ! is_array($sql_root))) {
+ $sql_root = array(
+ 0 => array(
+ 'caption' => 'Default',
+ 'host' => $sql['host'],
+ 'socket' => (isset($sql['socket']) ? $sql['socket'] : null),
+ 'user' => $sql['root_user'],
+ 'password' => $sql['root_password']
+ )
+ );
+ unset($sql['root_user']);
+ unset($sql['root_password']);
+ }
+
+ // either root or unprivileged user
+ if (self::$needroot) {
+ $caption = $sql_root[self::$dbserver]['caption'];
+ $user = $sql_root[self::$dbserver]['user'];
+ $password = $sql_root[self::$dbserver]['password'];
+ $host = $sql_root[self::$dbserver]['host'];
+ $socket = isset($sql_root[self::$dbserver]['socket']) ? $sql_root[self::$dbserver]['socket'] : null;
+ $port = isset($sql_root[self::$dbserver]['port']) ? $sql_root[self::$dbserver]['port'] : '3306';
+ } else {
+ $caption = 'localhost';
+ $user = $sql["user"];
+ $password = $sql["password"];
+ $host = $sql["host"];
+ $socket = isset($sql['socket']) ? $sql['socket'] : null;
+ $port = isset($sql['port']) ? $sql['port'] : '3306';
+ }
+
+ // save sql-access-data if needed
+ if (self::$needsqldata) {
+ self::$sqldata = array(
+ 'user' => $user,
+ 'passwd' => $password,
+ 'host' => $host,
+ 'port' => $port,
+ 'socket' => $socket,
+ 'db' => $sql["db"],
+ 'caption' => $caption
+ );
+ }
+
+ // build up connection string
+ $driver = 'mysql';
+ $dsn = $driver . ":";
+ $options = array(
+ 'PDO::MYSQL_ATTR_INIT_COMMAND' => 'SET names utf8'
+ );
+ $attributes = array(
+ 'ATTR_ERRMODE' => 'ERRMODE_EXCEPTION'
+ );
+
+ $dbconf["dsn"] = array(
+ 'dbname' => $sql["db"],
+ 'charset' => 'utf8'
+ );
+
+ if ($socket != null) {
+ $dbconf["dsn"]['unix_socket'] = \Froxlor\FileDir::makeCorrectFile($socket);
+ } else {
+ $dbconf["dsn"]['host'] = $host;
+ $dbconf["dsn"]['port'] = $port;
+ }
+
+ self::$dbname = $sql["db"];
+
+ // add options to dsn-string
+ foreach ($dbconf["dsn"] as $k => $v) {
+ $dsn .= $k . "=" . $v . ";";
+ }
+
+ // clean up
+ unset($dbconf);
+
+ // try to connect
+ try {
+ self::$link = new \PDO($dsn, $user, $password, $options);
+ } catch (\PDOException $e) {
+ self::showerror($e);
+ }
+
+ // set attributes
+ foreach ($attributes as $k => $v) {
+ self::$link->setAttribute(constant("PDO::" . $k), constant("PDO::" . $v));
+ }
+
+ $version_server = self::$link->getAttribute(\PDO::ATTR_SERVER_VERSION);
+ $sql_mode = 'NO_ENGINE_SUBSTITUTION';
+ if (version_compare($version_server, '8.0.11', '<')) {
+ $sql_mode .= ',NO_AUTO_CREATE_USER';
+ }
+ self::$link->exec('SET sql_mode = "' . $sql_mode . '"');
+
+ // return PDO instance
+ return self::$link;
+ }
+
+ /**
+ * display a nice error if it occurs and log everything
+ *
+ * @param \PDOException $error
+ * @param bool $showerror
+ * if set to false, the error will be logged but we go on
+ */
+ private static function showerror($error, $showerror = true, $json_response = false, \PDOStatement $stmt = null)
+ {
+ global $userinfo, $theme, $linker;
+
+ // include userdata.inc.php
+ require \Froxlor\Froxlor::getInstallDir() . "/lib/userdata.inc.php";
+
+ // le format
+ if (isset($sql['root_user']) && isset($sql['root_password']) && (! isset($sql_root) || ! is_array($sql_root))) {
+ $sql_root = array(
+ 0 => array(
+ 'caption' => 'Default',
+ 'host' => $sql['host'],
+ 'socket' => (isset($sql['socket']) ? $sql['socket'] : null),
+ 'user' => $sql['root_user'],
+ 'password' => $sql['root_password']
+ )
+ );
+ }
+
+ $substitutions = array(
+ $sql['password'] => 'DB_UNPRIV_PWD',
+ $sql_root[0]['password'] => 'DB_ROOT_PWD'
+ );
+
+ // hide username/password in messages
+ $error_message = $error->getMessage();
+ $error_trace = $error->getTraceAsString();
+ // error-message
+ $error_message = self::substitute($error_message, $substitutions);
+ // error-trace
+ $error_trace = self::substitute($error_trace, $substitutions);
+
+ if ($error->getCode() == 2003) {
+ $error_message = "Unable to connect to database. Either the mysql-server is not running or your user/password is wrong.";
+ $error_trace = "";
+ }
+
+ /**
+ * log to a file, so we can actually ask people for the error
+ * (no one seems to find the stuff in the syslog)
+ */
+ $sl_dir = \Froxlor\FileDir::makeCorrectDir(\Froxlor\Froxlor::getInstallDir() . "/logs/");
+ if (! file_exists($sl_dir)) {
+ @mkdir($sl_dir, 0755);
+ }
+ openlog("froxlor", LOG_PID | LOG_PERROR, LOG_LOCAL0);
+ syslog(LOG_WARNING, str_replace("\n", " ", $error_message));
+ syslog(LOG_WARNING, str_replace("\n", " ", "--- DEBUG: " . $error_trace));
+ closelog();
+
+ /**
+ * log error for reporting
+ */
+ $errid = substr(md5(microtime()), 5, 5);
+ $err_file = \Froxlor\FileDir::makeCorrectFile($sl_dir . "/" . $errid . "_sql-error.log");
+ $errlog = @fopen($err_file, 'w');
+ @fwrite($errlog, "|CODE " . $error->getCode() . "\n");
+ @fwrite($errlog, "|MSG " . $error_message . "\n");
+ @fwrite($errlog, "|FILE " . $error->getFile() . "\n");
+ @fwrite($errlog, "|LINE " . $error->getLine() . "\n");
+ @fwrite($errlog, "|TRACE\n" . $error_trace . "\n");
+ @fclose($errlog);
+
+ if (empty($sql['debug'])) {
+ $error_trace = '';
+ } elseif (! is_null($stmt)) {
+ $error_trace .= " " . $stmt->queryString;
+ }
+
+ if ($showerror && $json_response) {
+ $exception_message = $error_message;
+ if (isset($sql['debug']) && $sql['debug'] == true) {
+ $exception_message .= "\n\n" . $error_trace;
+ }
+ throw new \Exception($exception_message, 500);
+ }
+
+ if ($showerror) {
+ // fallback
+ $theme = 'Sparkle';
+
+ // clean up sensitive data
+ unset($sql);
+ unset($sql_root);
+
+ if ((isset($theme) && $theme != '') && ! isset($_SERVER['SHELL']) || (isset($_SERVER['SHELL']) && $_SERVER['SHELL'] == '')) {
+ // if we're not on the shell, output a nice error
+ $_errtpl = dirname($sl_dir) . '/templates/' . $theme . '/misc/dberrornice.tpl';
+ if (file_exists($_errtpl)) {
+ $err_hint = file_get_contents($_errtpl);
+ // replace values
+ $err_hint = str_replace("", $error_message, $err_hint);
+ $err_hint = str_replace("", $error_trace, $err_hint);
+ $err_hint = str_replace("", date('Y', time()), $err_hint);
+
+ $err_report_html = '';
+ if (is_array($userinfo) && (($userinfo['adminsession'] == '1' && \Froxlor\Settings::Get('system.allow_error_report_admin') == '1') || ($userinfo['adminsession'] == '0' && \Froxlor\Settings::Get('system.allow_error_report_customer') == '1'))) {
+ $err_report_html = 'Report error ';
+ $err_report_html = str_replace(" ", $linker->getLink(array(
+ 'section' => 'index',
+ 'page' => 'send_error_report',
+ 'errorid' => $errid
+ )), $err_report_html);
+ }
+ $err_hint = str_replace("", $err_report_html, $err_hint);
+
+ // show
+ die($err_hint);
+ }
+ }
+ die("We are sorry, but a MySQL - error occurred. The administrator may find more information in the syslog");
+ }
+ }
+
+ /**
+ * Substitutes patterns in content.
+ *
+ * @param string $content
+ * @param array $substitutions
+ * @param int $minLength
+ * @return string
+ */
+ private static function substitute($content, array $substitutions, $minLength = 6)
+ {
+ $replacements = array();
+
+ foreach ($substitutions as $search => $replace) {
+ $replacements = $replacements + self::createShiftedSubstitutions($search, $replace, $minLength);
+ }
+
+ $content = str_replace(array_keys($replacements), array_values($replacements), $content);
+
+ return $content;
+ }
+
+ /**
+ * Creates substitutions, shifted by length, e.g.
+ *
+ * _createShiftedSubstitutions('abcdefgh', 'value', 4):
+ * array(
+ * 'abcdefgh' => 'value',
+ * 'abcdefg' => 'value',
+ * 'abcdef' => 'value',
+ * 'abcde' => 'value',
+ * 'abcd' => 'value',
+ * )
+ *
+ * @param string $search
+ * @param string $replace
+ * @param int $minLength
+ * @return array
+ */
+ private static function createShiftedSubstitutions($search, $replace, $minLength)
+ {
+ $substitutions = array();
+ $length = strlen($search);
+
+ if ($length > $minLength) {
+ for ($shiftedLength = $length; $shiftedLength >= $minLength; $shiftedLength --) {
+ $substitutions[substr($search, 0, $shiftedLength)] = $replace;
+ }
+ }
+
+ return $substitutions;
+ }
+}
diff --git a/lib/Froxlor/Database/DbManager.php b/lib/Froxlor/Database/DbManager.php
new file mode 100644
index 00000000..c9fb7b45
--- /dev/null
+++ b/lib/Froxlor/Database/DbManager.php
@@ -0,0 +1,198 @@
+
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Classes
+ *
+ * @since 0.9.31
+ *
+ */
+
+/**
+ * Class DbManager
+ *
+ * Wrapper-class for database-management like creating
+ * and removing databases, users and permissions
+ *
+ * @copyright (c) the authors
+ * @author Michael Kaufmann
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Classes
+ */
+class DbManager
+{
+
+ /**
+ * FroxlorLogger object
+ *
+ * @var object
+ */
+ private $log = null;
+
+ /**
+ * Manager object
+ *
+ * @var object
+ */
+ private $manager = null;
+
+ /**
+ * main constructor
+ *
+ * @param \Froxlor\FroxlorLogger $log
+ */
+ public function __construct($log = null)
+ {
+ $this->log = $log;
+ $this->setManager();
+ }
+
+ /**
+ * creates a new database and a user with the
+ * same name with all privileges granted on the db.
+ * DB-name and user-name are being generated and
+ * the password for the user will be set
+ *
+ * @param string $loginname
+ * @param string $password
+ * @param int $last_accnumber
+ *
+ * @return string|bool $username if successful or false of username is equal to the password
+ */
+ public function createDatabase($loginname = null, $password = null, $last_accnumber = 0)
+ {
+ Database::needRoot(true);
+
+ // check whether we shall create a random username
+ if (strtoupper(Settings::Get('customer.mysqlprefix')) == 'RANDOM') {
+ // get all usernames from db-manager
+ $allsqlusers = $this->getManager()->getAllSqlUsers();
+ // generate random username
+ $username = $loginname . '-' . substr(md5(uniqid(microtime(), 1)), 20, 3);
+ // check whether it exists on the DBMS
+ while (in_array($username, $allsqlusers)) {
+ $username = $loginname . '-' . substr(md5(uniqid(microtime(), 1)), 20, 3);
+ }
+ } else {
+ $username = $loginname . Settings::Get('customer.mysqlprefix') . (intval($last_accnumber) + 1);
+ }
+
+ // don't use a password that is the same as the username
+ if ($username == $password) {
+ return false;
+ }
+
+ // now create the database itself
+ $this->getManager()->createDatabase($username);
+ $this->log->logAction(\Froxlor\FroxlorLogger::USR_ACTION, LOG_INFO, "created database '" . $username . "'");
+
+ // and give permission to the user on every access-host we have
+ foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) {
+ $this->getManager()->grantPrivilegesTo($username, $password, $mysql_access_host);
+ $this->log->logAction(\Froxlor\FroxlorLogger::USR_ACTION, LOG_NOTICE, "grant all privileges for '" . $username . "'@'" . $mysql_access_host . "'");
+ }
+
+ $this->getManager()->flushPrivileges();
+
+ Database::needRoot(false);
+
+ return $username;
+ }
+
+ /**
+ * returns the manager-object
+ * from where we can control it
+ */
+ public function getManager()
+ {
+ return $this->manager;
+ }
+
+ /**
+ * set manager-object by type of
+ * dbms: mysql only for now
+ *
+ * sets private $_manager variable
+ */
+ private function setManager()
+ {
+ // TODO read different dbms from settings later
+ $this->manager = new \Froxlor\Database\Manager\DbManagerMySQL($this->log);
+ }
+
+ public static function correctMysqlUsers($mysql_access_host_array)
+ {
+ // get sql-root access data
+ Database::needRoot(true);
+ Database::needSqlData();
+ $sql_root = Database::getSqlData();
+ Database::needRoot(false);
+
+ $dbservers_stmt = Database::query("SELECT DISTINCT `dbserver` FROM `" . TABLE_PANEL_DATABASES . "`");
+ while ($dbserver = $dbservers_stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ Database::needRoot(true, $dbserver['dbserver']);
+ Database::needSqlData();
+ $sql_root = Database::getSqlData();
+
+ $dbm = new DbManager(\Froxlor\FroxlorLogger::getInstanceOf());
+ $users = $dbm->getManager()->getAllSqlUsers(false);
+
+ $databases = array(
+ $sql_root['db']
+ );
+ $databases_result_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_DATABASES . "`
+ WHERE `dbserver` = :mysqlserver
+ ");
+ Database::pexecute($databases_result_stmt, array(
+ 'mysqlserver' => $dbserver['dbserver']
+ ));
+
+ while ($databases_row = $databases_result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $databases[] = $databases_row['databasename'];
+ }
+
+ foreach ($databases as $username) {
+
+ if (isset($users[$username]) && is_array($users[$username]) && isset($users[$username]['hosts']) && is_array($users[$username]['hosts'])) {
+
+ $password = $users[$username]['password'];
+
+ foreach ($mysql_access_host_array as $mysql_access_host) {
+
+ $mysql_access_host = trim($mysql_access_host);
+
+ if (! in_array($mysql_access_host, $users[$username]['hosts'])) {
+ $dbm->getManager()->grantPrivilegesTo($username, $password, $mysql_access_host, true);
+ }
+ }
+
+ foreach ($users[$username]['hosts'] as $mysql_access_host) {
+
+ if (! in_array($mysql_access_host, $mysql_access_host_array)) {
+ $dbm->getManager()->deleteUser($username, $mysql_access_host);
+ }
+ }
+ }
+ }
+
+ $dbm->getManager()->flushPrivileges();
+ Database::needRoot(false);
+ }
+ }
+}
diff --git a/lib/Froxlor/Database/IntegrityCheck.php b/lib/Froxlor/Database/IntegrityCheck.php
new file mode 100644
index 00000000..e2f32e68
--- /dev/null
+++ b/lib/Froxlor/Database/IntegrityCheck.php
@@ -0,0 +1,512 @@
+
+ * @author Froxlor team (2014-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Integrity
+ *
+ * IntegrityCheck - class
+ */
+class IntegrityCheck
+{
+
+ // Store all available checks
+ public $available = array();
+
+ // logger object
+ private $log = null;
+
+ /**
+ * Constructor
+ * Parses all available checks into $this->available
+ */
+ public function __construct()
+ {
+ global $userinfo;
+ if (! isset($userinfo) || ! is_array($userinfo)) {
+ $userinfo = array(
+ 'loginname' => 'integrity-check'
+ );
+ }
+ $this->log = \Froxlor\FroxlorLogger::getInstanceOf($userinfo);
+ $this->available = get_class_methods($this);
+ unset($this->available[array_search('__construct', $this->available)]);
+ unset($this->available[array_search('checkAll', $this->available)]);
+ unset($this->available[array_search('fixAll', $this->available)]);
+ sort($this->available);
+ }
+
+ /**
+ * Check all occurring integrity problems at once
+ */
+ public function checkAll()
+ {
+ $integrityok = true;
+ foreach ($this->available as $check) {
+ $integrityok = $this->$check() ? $integrityok : false;
+ }
+ return $integrityok;
+ }
+
+ /**
+ * Fix all occurring integrity problems at once with default settings
+ */
+ public function fixAll()
+ {
+ $integrityok = true;
+ foreach ($this->available as $check) {
+ $integrityok = $this->$check(true) ? $integrityok : false;
+ }
+ return $integrityok;
+ }
+
+ /**
+ * check whether the froxlor database and its tables are in utf-8 character-set
+ *
+ * @param bool $fix
+ * fix db charset/collation if not utf8
+ *
+ * @return boolean
+ */
+ public function databaseCharset($fix = false)
+ {
+
+ // get characterset
+ $cs_stmt = Database::prepare('SELECT default_character_set_name FROM information_schema.SCHEMATA WHERE schema_name = :dbname');
+ $resp = Database::pexecute_first($cs_stmt, array(
+ 'dbname' => Database::getDbName()
+ ));
+ $charset = isset($resp['default_character_set_name']) ? $resp['default_character_set_name'] : null;
+ if (! empty($charset) && strtolower($charset) != 'utf8') {
+ $this->log->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "database charset seems to be different from UTF-8, integrity-check can fix that");
+ if ($fix) {
+ // fix database
+ Database::query('ALTER DATABASE `' . Database::getDbName() . '` CHARACTER SET utf8 COLLATE utf8_general_ci');
+ // fix all tables
+ $handle = Database::query('SHOW FULL TABLES WHERE Table_type != "VIEW"');
+ while ($row = $handle->fetch(\PDO::FETCH_BOTH)) {
+ $table = $row[0];
+ Database::query('ALTER TABLE `' . $table . '` CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;');
+ }
+ $this->log->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_WARNING, "database charset was different from UTF-8, integrity-check fixed that");
+ } else {
+ return false;
+ }
+ }
+
+ if ($fix) {
+ return $this->databaseCharset();
+ }
+ return true;
+ }
+
+ /**
+ * Check the integrity of the domain to ip/port - association
+ *
+ * @param bool $fix
+ * Fix everything found directly
+ */
+ public function domainIpTable($fix = false)
+ {
+ $ips = array();
+ $domains = array();
+ $ipstodomains = array();
+ $admips = array();
+
+ if ($fix) {
+ // Prepare insert / delete statement for the fixes
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_DOMAINTOIP . "`
+ WHERE `id_domain` = :domainid AND `id_ipandports` = :ipandportid ");
+ $ins_stmt = Database::prepare("
+ INSERT INTO `" . TABLE_DOMAINTOIP . "`
+ SET `id_domain` = :domainid, `id_ipandports` = :ipandportid ");
+
+ // Cache all IPs the admins have assigned
+ $adm_stmt = Database::prepare("SELECT `adminid`, `ip` FROM `" . TABLE_PANEL_ADMINS . "` ORDER BY `adminid` ASC");
+ Database::pexecute($adm_stmt);
+ $default_ips = explode(',', Settings::Get('system.defaultip'));
+ $default_ssl_ips = explode(',', Settings::Get('system.defaultsslip'));
+ while ($row = $adm_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if ($row['ip'] < 0 || is_null($row['ip']) || empty($row['ip'])) {
+ // Admin uses default-IP
+ $admips[$row['adminid']] = array_merge($default_ips, $default_ssl_ips);
+ } else {
+ $admips[$row['adminid']] = array(
+ $row['ip']
+ );
+ }
+ }
+ }
+
+ // Cache all available ip/port - combinations
+ $result_stmt = Database::prepare("SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` ORDER BY `id` ASC");
+ Database::pexecute($result_stmt);
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $ips[$row['id']] = $row['ip'] . ':' . $row['port'];
+ }
+
+ // Cache all configured domains
+ $result_stmt = Database::prepare("SELECT `id`, `adminid` FROM `" . TABLE_PANEL_DOMAINS . "` ORDER BY `id` ASC");
+ Database::pexecute($result_stmt);
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $domains[$row['id']] = $row['adminid'];
+ }
+
+ // Check if every domain to ip/port - association is valid in TABLE_DOMAINTOIP
+ $result_stmt = Database::prepare("SELECT `id_domain`, `id_ipandports` FROM `" . TABLE_DOMAINTOIP . "`");
+ Database::pexecute($result_stmt);
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if (! array_key_exists($row['id_ipandports'], $ips)) {
+ if ($fix) {
+ Database::pexecute($del_stmt, array(
+ 'domainid' => $row['id_domain'],
+ 'ipandportid' => $row['id_ipandports']
+ ));
+ $this->log->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_WARNING, "found an ip/port-id in domain <> ip table which does not exist, integrity check fixed this");
+ } else {
+ $this->log->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "found an ip/port-id in domain <> ip table which does not exist, integrity check can fix this");
+ return false;
+ }
+ }
+ if (! array_key_exists($row['id_domain'], $domains)) {
+ if ($fix) {
+ Database::pexecute($del_stmt, array(
+ 'domainid' => $row['id_domain'],
+ 'ipandportid' => $row['id_ipandports']
+ ));
+ $this->log->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_WARNING, "found a domain-id in domain <> ip table which does not exist, integrity check fixed this");
+ } else {
+ $this->log->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "found a domain-id in domain <> ip table which does not exist, integrity check can fix this");
+ return false;
+ }
+ }
+ // Save one IP/Port combination per domain, so we know, if one domain is missing an IP
+ $ipstodomains[$row['id_domain']] = $row['id_ipandports'];
+ }
+
+ // Check that all domains have at least one IP/Port combination
+ foreach ($domains as $domainid => $adminid) {
+ if (! array_key_exists($domainid, $ipstodomains)) {
+ if ($fix) {
+ foreach ($admips[$adminid] as $defaultip) {
+ Database::pexecute($ins_stmt, array(
+ 'domainid' => $domainid,
+ 'ipandportid' => $defaultip
+ ));
+ }
+ $this->log->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_WARNING, "found a domain-id with no entry in domain <> ip table, integrity check fixed this");
+ } else {
+ $this->log->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "found a domain-id with no entry in domain <> ip table, integrity check can fix this");
+ return false;
+ }
+ }
+ }
+
+ if ($fix) {
+ return $this->domainIpTable();
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Check if all subdomains have ssl-redirect = 0 if domain has no ssl-port
+ *
+ * @param bool $fix
+ * Fix everything found directly
+ */
+ public function subdomainSslRedirect($fix = false)
+ {
+ $ips = array();
+ $parentdomains = array();
+ $subdomains = array();
+
+ if ($fix) {
+ // Prepare update statement for the fixes
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_DOMAINS . "`
+ SET `ssl_redirect` = 0 WHERE `parentdomainid` = :domainid");
+ }
+
+ // Cache all ssl ip/port - combinations
+ $result_stmt = Database::prepare("SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `ssl` = 1 ORDER BY `id` ASC");
+ Database::pexecute($result_stmt);
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $ips[$row['id']] = $row['ip'] . ':' . $row['port'];
+ }
+
+ // Cache all configured domains
+ $result_stmt = Database::prepare("SELECT `id`, `parentdomainid`, `ssl_redirect` FROM `" . TABLE_PANEL_DOMAINS . "` ORDER BY `id` ASC");
+ $ip_stmt = Database::prepare("SELECT `id_domain`, `id_ipandports` FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :domainid");
+ Database::pexecute($result_stmt);
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if ($row['parentdomainid'] == 0) {
+ // All parentdomains by default have no ssl - ip/port
+ $parentdomains[$row['id']] = false;
+ Database::pexecute($ip_stmt, array(
+ 'domainid' => $row['id']
+ ));
+ while ($iprow = $ip_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ // If the parentdomain has an ip/port assigned which we know is SSL enabled, set the parentdomain to "true"
+ if (array_key_exists($iprow['id_ipandports'], $ips)) {
+ $parentdomains[$row['id']] = true;
+ }
+ }
+ } elseif ($row['ssl_redirect'] == 1) {
+ // All subdomains with enabled ssl_redirect enabled are stored
+ if (! isset($subdomains[$row['parentdomainid']])) {
+ $subdomains[$row['parentdomainid']] = array();
+ }
+ $subdomains[$row['parentdomainid']][] = $row['id'];
+ }
+ }
+
+ // Check if every parentdomain with enabled ssl_redirect as SSL enabled
+ foreach ($parentdomains as $id => $sslavailable) {
+ // This parentdomain has no subdomains
+ if (! isset($subdomains[$id])) {
+ continue;
+ }
+ // This parentdomain has SSL enabled, doesn't matter what status the subdomains have
+ if ($sslavailable) {
+ continue;
+ }
+
+ // At this point only parentdomains reside which have ssl_redirect enabled subdomains
+ if ($fix) {
+ // We make a blanket update to all subdomains of this parentdomain, doesn't matter which one is wrong, all have to be disabled
+ Database::pexecute($upd_stmt, array(
+ 'domainid' => $id
+ ));
+ $this->log->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_WARNING, "found a subdomain with ssl_redirect=1 but parent-domain has ssl=0, integrity check fixed this");
+ } else {
+ // It's just the check, let the function fail
+ $this->log->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "found a subdomain with ssl_redirect=1 but parent-domain has ssl=0, integrity check can fix this");
+ return false;
+ }
+ }
+
+ if ($fix) {
+ return $this->subdomainSslRedirect();
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Check if all subdomain have letsencrypt = 0 if domain has no ssl-port
+ *
+ * @param bool $fix
+ * Fix everything found directly
+ */
+ public function subdomainLetsencrypt($fix = false)
+ {
+ $ips = array();
+ $parentdomains = array();
+ $subdomains = array();
+
+ if ($fix) {
+ // Prepare update statement for the fixes
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_DOMAINS . "`
+ SET `letsencrypt` = 0 WHERE `parentdomainid` = :domainid");
+ }
+
+ // Cache all ssl ip/port - combinations
+ $result_stmt = Database::prepare("SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `ssl` = 1 ORDER BY `id` ASC");
+ Database::pexecute($result_stmt);
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $ips[$row['id']] = $row['ip'] . ':' . $row['port'];
+ }
+
+ // Cache all configured domains
+ $result_stmt = Database::prepare("SELECT `id`, `parentdomainid`, `letsencrypt` FROM `" . TABLE_PANEL_DOMAINS . "` ORDER BY `id` ASC");
+ $ip_stmt = Database::prepare("SELECT `id_domain`, `id_ipandports` FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :domainid");
+ Database::pexecute($result_stmt);
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if ($row['parentdomainid'] == 0) {
+ // All parentdomains by default have no ssl - ip/port
+ $parentdomains[$row['id']] = false;
+ Database::pexecute($ip_stmt, array(
+ 'domainid' => $row['id']
+ ));
+ while ($iprow = $ip_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ // If the parentdomain has an ip/port assigned which we know is SSL enabled, set the parentdomain to "true"
+ if (array_key_exists($iprow['id_ipandports'], $ips)) {
+ $parentdomains[$row['id']] = true;
+ }
+ }
+ } elseif ($row['letsencrypt'] == 1) {
+ // All subdomains with enabled letsencrypt enabled are stored
+ if (! isset($subdomains[$row['parentdomainid']])) {
+ $subdomains[$row['parentdomainid']] = array();
+ }
+ $subdomains[$row['parentdomainid']][] = $row['id'];
+ }
+ }
+
+ // Check if every parentdomain with enabled letsencrypt as SSL enabled
+ foreach ($parentdomains as $id => $sslavailable) {
+ // This parentdomain has no subdomains
+ if (! isset($subdomains[$id])) {
+ continue;
+ }
+ // This parentdomain has SSL enabled, doesn't matter what status the subdomains have
+ if ($sslavailable) {
+ continue;
+ }
+
+ // At this point only parentdomains reside which have letsencrypt enabled subdomains
+ if ($fix) {
+ // We make a blanket update to all subdomains of this parentdomain, doesn't matter which one is wrong, all have to be disabled
+ Database::pexecute($upd_stmt, array(
+ 'domainid' => $id
+ ));
+ $this->log->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_WARNING, "found a subdomain with letsencrypt=1 but parent-domain has ssl=0, integrity check fixed this");
+ } else {
+ // It's just the check, let the function fail
+ $this->log->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "found a subdomain with letsencrypt=1 but parent-domain has ssl=0, integrity check can fix this");
+ return false;
+ }
+ }
+
+ if ($fix) {
+ return $this->subdomainLetsencrypt();
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * check whether the webserveruser is in
+ * the customers groups when fcgid / php-fpm is used
+ *
+ * @param bool $fix
+ * fix member/groups
+ *
+ * @return boolean
+ */
+ public function webserverGroupMemberForFcgidPhpFpm($fix = false)
+ {
+ if (Settings::Get('system.mod_fcgid') == 0 && Settings::Get('phpfpm.enabled') == 0) {
+ return true;
+ }
+
+ // get all customers that don't have the webserver-user in their group
+ $cwg_stmt = Database::prepare("
+ SELECT `id` FROM `" . TABLE_FTP_GROUPS . "` WHERE NOT FIND_IN_SET(:webserveruser, `members`)
+ ");
+ Database::pexecute($cwg_stmt, array(
+ 'webserveruser' => Settings::Get('system.httpuser')
+ ));
+
+ if ($cwg_stmt->rowCount() > 0) {
+ $this->log->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "Customers are missing the webserver-user as group-member, integrity-check can fix that");
+ if ($fix) {
+ // prepare update statement
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_FTP_GROUPS . "` SET `members` = CONCAT(`members`, :additionaluser)
+ WHERE `id` = :id
+ ");
+ $upd_data = array(
+ 'additionaluser' => "," . Settings::Get('system.httpuser')
+ );
+
+ while ($cwg_row = $cwg_stmt->fetch()) {
+ $upd_data['id'] = $cwg_row['id'];
+ Database::pexecute($upd_stmt, $upd_data);
+ }
+ $this->log->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "Customers were missing the webserver-user as group-member, integrity-check fixed that");
+ } else {
+ return false;
+ }
+ }
+
+ if ($fix) {
+ return $this->webserverGroupMemberForFcgidPhpFpm();
+ }
+ return true;
+ }
+
+ /**
+ * check whether the local froxlor user is in
+ * the customers groups when fcgid / php-fpm and
+ * fcgid/fpm in froxlor vhost is used
+ *
+ * @param bool $fix
+ * fix member/groups
+ *
+ * @return boolean
+ */
+ public function froxlorLocalGroupMemberForFcgidPhpFpm($fix = false)
+ {
+ if (Settings::Get('system.mod_fcgid') == 0 && Settings::Get('phpfpm.enabled') == 0) {
+ return true;
+ }
+
+ if (Settings::get('system.mod_fcgid') == 1) {
+ if (Settings::get('system.mod_fcgid_ownvhost') == 0) {
+ return true;
+ } else {
+ $localuser = Settings::Get('system.mod_fcgid_httpuser');
+ }
+ }
+
+ if (Settings::get('phpfpm.enabled') == 1) {
+ if (Settings::get('phpfpm.enabled_ownvhost') == 0) {
+ return true;
+ } else {
+ $localuser = Settings::Get('phpfpm.vhost_httpuser');
+ }
+ }
+
+ // get all customers that don't have the webserver-user in their group
+ $cwg_stmt = Database::prepare("
+ SELECT `id` FROM `" . TABLE_FTP_GROUPS . "` WHERE NOT FIND_IN_SET(:localuser, `members`)
+ ");
+ Database::pexecute($cwg_stmt, array(
+ 'localuser' => $localuser
+ ));
+
+ if ($cwg_stmt->rowCount() > 0) {
+ $this->log->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "Customers are missing the local froxlor-user as group-member, integrity-check can fix that");
+ if ($fix) {
+ // prepare update statement
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_FTP_GROUPS . "` SET `members` = CONCAT(`members`, :additionaluser)
+ WHERE `id` = :id
+ ");
+ $upd_data = array(
+ 'additionaluser' => "," . $localuser
+ );
+
+ while ($cwg_row = $cwg_stmt->fetch()) {
+ $upd_data['id'] = $cwg_row['id'];
+ Database::pexecute($upd_stmt, $upd_data);
+ }
+ $this->log->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_NOTICE, "Customers were missing the local froxlor-user as group-member, integrity-check fixed that");
+ } else {
+ return false;
+ }
+ }
+
+ if ($fix) {
+ return $this->froxlorLocalGroupMemberForFcgidPhpFpm();
+ }
+ return true;
+ }
+}
diff --git a/lib/Froxlor/Database/Manager/DbManagerMySQL.php b/lib/Froxlor/Database/Manager/DbManagerMySQL.php
new file mode 100644
index 00000000..bf2f01ca
--- /dev/null
+++ b/lib/Froxlor/Database/Manager/DbManagerMySQL.php
@@ -0,0 +1,263 @@
+
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Classes
+ *
+ * @since 0.9.31
+ *
+ */
+
+/**
+ * Class DbManagerMySQL
+ *
+ * Explicit class for database-management like creating
+ * and removing databases, users and permissions for MySQL
+ *
+ * @copyright (c) the authors
+ * @author Michael Kaufmann
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Classes
+ */
+class DbManagerMySQL
+{
+
+ /**
+ * FroxlorLogger object
+ *
+ * @var object
+ */
+ private $log = null;
+
+ /**
+ * main constructor
+ *
+ * @param \Froxlor\FroxlorLogger $log
+ */
+ public function __construct(&$log = null)
+ {
+ $this->log = $log;
+ }
+
+ /**
+ * creates a database
+ *
+ * @param string $dbname
+ */
+ public function createDatabase($dbname = null)
+ {
+ Database::query("CREATE DATABASE `" . $dbname . "`");
+ }
+
+ /**
+ * grants access privileges on a database with the same
+ * username and sets the password for that user the given access_host
+ *
+ * @param string $username
+ * @param string $password
+ * @param string $access_host
+ * @param bool $p_encrypted
+ * optional, whether the password is encrypted or not, default false
+ * @param bool $update
+ * optional, whether to update the password only (not create user)
+ */
+ public function grantPrivilegesTo($username = null, $password = null, $access_host = null, $p_encrypted = false, $update = false)
+ {
+ if (! $update) {
+ // create user
+ if ($p_encrypted) {
+ if (version_compare(Database::getAttribute(\PDO::ATTR_SERVER_VERSION), '5.7.0', '<')) {
+ $stmt = Database::prepare("
+ CREATE USER '" . $username . "'@'" . $access_host . "' IDENTIFIED BY PASSWORD :password
+ ");
+ } else {
+ $stmt = Database::prepare("
+ CREATE USER '" . $username . "'@'" . $access_host . "' IDENTIFIED WITH mysql_native_password AS :password
+ ");
+ }
+ } else {
+ $stmt = Database::prepare("
+ CREATE USER '" . $username . "'@'" . $access_host . "' IDENTIFIED BY :password
+ ");
+ }
+ Database::pexecute($stmt, array(
+ "password" => $password
+ ));
+ // grant privileges
+ $stmt = Database::prepare("
+ GRANT ALL ON `" . $username . "`.* TO :username@:host
+ ");
+ Database::pexecute($stmt, array(
+ "username" => $username,
+ "host" => $access_host
+ ));
+ } else {
+ // set password
+ if (version_compare(Database::getAttribute(\PDO::ATTR_SERVER_VERSION), '5.7.6', '<')) {
+ if ($p_encrypted) {
+ $stmt = Database::prepare("SET PASSWORD FOR :username@:host = :password");
+ } else {
+ $stmt = Database::prepare("SET PASSWORD FOR :username@:host = PASSWORD(:password)");
+ }
+ } else {
+ if ($p_encrypted) {
+ $stmt = Database::prepare("ALTER USER :username@:host IDENTIFIED WITH mysql_native_password AS :password");
+ } else {
+ $stmt = Database::prepare("ALTER USER :username@:host IDENTIFIED BY :password");
+ }
+ }
+ Database::pexecute($stmt, array(
+ "username" => $username,
+ "host" => $access_host,
+ "password" => $password
+ ));
+ }
+ }
+
+ /**
+ * removes the given database from the dbms and also
+ * takes away any privileges from a user to that db
+ *
+ * @param string $dbname
+ */
+ public function deleteDatabase($dbname = null)
+ {
+ if (Database::getAttribute(\PDO::ATTR_SERVER_VERSION) < '5.0.2') {
+ // failsafe if user has been deleted manually (requires MySQL 4.1.2+)
+ $stmt = Database::prepare("REVOKE ALL PRIVILEGES, GRANT OPTION FROM `" . $dbname . "`");
+ Database::pexecute($stmt, array(), false);
+ }
+
+ $host_res_stmt = Database::prepare("
+ SELECT `Host` FROM `mysql`.`user` WHERE `User` = :dbname");
+ Database::pexecute($host_res_stmt, array(
+ 'dbname' => $dbname
+ ));
+
+ // as of MySQL 5.0.2 this also revokes privileges. (requires MySQL 4.1.2+)
+ if (version_compare(Database::getAttribute(\PDO::ATTR_SERVER_VERSION), '5.7.0', '<')) {
+ $drop_stmt = Database::prepare("DROP USER :dbname@:host");
+ } else {
+ $drop_stmt = Database::prepare("DROP USER IF EXISTS :dbname@:host");
+ }
+ while ($host = $host_res_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ Database::pexecute($drop_stmt, array(
+ 'dbname' => $dbname,
+ 'host' => $host['Host']
+ ), false);
+ }
+
+ $drop_stmt = Database::prepare("DROP DATABASE IF EXISTS `" . $dbname . "`");
+ Database::pexecute($drop_stmt);
+ }
+
+ /**
+ * removes a user from the dbms and revokes all privileges
+ *
+ * @param string $username
+ * @param string $host
+ */
+ public function deleteUser($username = null, $host = null)
+ {
+ if (Database::getAttribute(\PDO::ATTR_SERVER_VERSION) < '5.0.2') {
+ // Revoke privileges (only required for MySQL 4.1.2 - 5.0.1)
+ $stmt = Database::prepare("REVOKE ALL PRIVILEGES ON * . * FROM `" . $username . "`@`" . $host . "`");
+ Database::pexecute($stmt);
+ }
+ // as of MySQL 5.0.2 this also revokes privileges. (requires MySQL 4.1.2+)
+ if (version_compare(Database::getAttribute(\PDO::ATTR_SERVER_VERSION), '5.7.0', '<')) {
+ $stmt = Database::prepare("DROP USER :username@:host");
+ } else {
+ $stmt = Database::prepare("DROP USER IF EXISTS :username@:host");
+ }
+ Database::pexecute($stmt, array(
+ "username" => $username,
+ "host" => $host
+ ));
+ }
+
+ /**
+ * removes permissions from a user
+ *
+ * @param string $username
+ * @param string $host
+ * (unused in mysql)
+ */
+ public function disableUser($username = null, $host = null)
+ {
+ $stmt = Database::prepare('REVOKE ALL PRIVILEGES, GRANT OPTION FROM `' . $username . '`@`' . $host . '`');
+ Database::pexecute($stmt, array(), false);
+ }
+
+ /**
+ * re-grant permissions to a user
+ *
+ * @param string $username
+ * @param string $host
+ */
+ public function enableUser($username = null, $host = null)
+ {
+ // check whether user exists to avoid errors
+ $exist_check_stmt = Database::prepare("SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '" . $username . "' AND host = '" . $host . "')");
+ $exist_check = Database::pexecute_first($exist_check_stmt);
+ if ($exist_check && array_pop($exist_check) == '1') {
+ Database::query('GRANT ALL PRIVILEGES ON `' . $username . '`.* TO `' . $username . '`@`' . $host . '`');
+ Database::query('GRANT ALL PRIVILEGES ON `' . str_replace('_', '\_', $username) . '` . * TO `' . $username . '`@`' . $host . '`');
+ }
+ }
+
+ /**
+ * flushes the privileges...pretty obvious eh?
+ */
+ public function flushPrivileges()
+ {
+ Database::query("FLUSH PRIVILEGES");
+ }
+
+ /**
+ * return an array of all usernames used in that DBMS
+ *
+ * @param bool $user_only
+ * if false, * will be selected from mysql.user and slightly different array will be generated
+ *
+ * @return array
+ */
+ public function getAllSqlUsers($user_only = true)
+ {
+ if ($user_only == false) {
+ $result_stmt = Database::prepare('SELECT * FROM mysql.user');
+ } else {
+ $result_stmt = Database::prepare('SELECT `User` FROM mysql.user');
+ }
+ Database::pexecute($result_stmt);
+ $allsqlusers = array();
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if ($user_only == false) {
+ if (! isset($allsqlusers[$row['User']]) || ! is_array($allsqlusers[$row['User']])) {
+ $allsqlusers[$row['User']] = array(
+ 'password' => $row['Password'] ?? $row['authentication_string'],
+ 'hosts' => array()
+ );
+ }
+ $allsqlusers[$row['User']]['hosts'][] = $row['Host'];
+ } else {
+ $allsqlusers[] = $row['User'];
+ }
+ }
+ return $allsqlusers;
+ }
+}
diff --git a/lib/Froxlor/Dns/Dns.php b/lib/Froxlor/Dns/Dns.php
new file mode 100644
index 00000000..36692bf0
--- /dev/null
+++ b/lib/Froxlor/Dns/Dns.php
@@ -0,0 +1,444 @@
+ $domain_id
+ );
+
+ $where_clause = '';
+ if ($area == 'admin') {
+ if ($userinfo['domains_see_all'] != '1') {
+ $where_clause = '`adminid` = :uid AND ';
+ $dom_data['uid'] = $userinfo['userid'];
+ }
+ } else {
+ $where_clause = '`customerid` = :uid AND ';
+ $dom_data['uid'] = $userinfo['userid'];
+ }
+
+ $dom_stmt = Database::prepare("
+ SELECT domain, isbinddomain
+ FROM `" . TABLE_PANEL_DOMAINS . "`
+ WHERE " . $where_clause . " id = :did
+ ");
+ $domain = Database::pexecute_first($dom_stmt, $dom_data);
+
+ if ($domain) {
+ if ($domain['isbinddomain'] != '1') {
+ \Froxlor\UI\Response::standard_error('dns_domain_nodns');
+ }
+ $idna_convert = new \Froxlor\Idna\IdnaWrapper();
+ return $idna_convert->decode($domain['domain']);
+ }
+ \Froxlor\UI\Response::standard_error('dns_notfoundorallowed');
+ }
+
+ public static function createDomainZone($domain_id, $froxlorhostname = false, $isMainButSubTo = false)
+ {
+ if (! $froxlorhostname) {
+ // get domain-name
+ $dom_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_DOMAINS . "` WHERE id = :did");
+ $domain = Database::pexecute_first($dom_stmt, array(
+ 'did' => $domain_id
+ ));
+ } else {
+ $domain = $domain_id;
+ }
+
+ if ($domain['isbinddomain'] != '1') {
+ return;
+ }
+
+ $dom_entries = array();
+ if (! $froxlorhostname) {
+ // select all entries
+ $sel_stmt = Database::prepare("SELECT * FROM `" . TABLE_DOMAIN_DNS . "` WHERE domain_id = :did ORDER BY id ASC");
+ Database::pexecute($sel_stmt, array(
+ 'did' => $domain_id
+ ));
+ $dom_entries = $sel_stmt->fetchAll(\PDO::FETCH_ASSOC);
+ }
+
+ // check for required records
+ $required_entries = array();
+
+ self::addRequiredEntry('@', 'A', $required_entries);
+ self::addRequiredEntry('@', 'AAAA', $required_entries);
+ if (! $isMainButSubTo) {
+ self::addRequiredEntry('@', 'NS', $required_entries);
+ }
+ if ($domain['isemaildomain'] === '1') {
+ self::addRequiredEntry('@', 'MX', $required_entries);
+ if (Settings::Get('system.dns_createmailentry')) {
+ foreach (array(
+ 'imap',
+ 'pop3',
+ 'mail',
+ 'smtp'
+ ) as $record) {
+ foreach (array(
+ 'AAAA',
+ 'A'
+ ) as $type) {
+ self::addRequiredEntry($record, $type, $required_entries);
+ }
+ }
+ }
+ }
+
+ // additional required records by setting
+ if ($domain['iswildcarddomain'] == '1') {
+ self::addRequiredEntry('*', 'A', $required_entries);
+ self::addRequiredEntry('*', 'AAAA', $required_entries);
+ } elseif ($domain['wwwserveralias'] == '1') {
+ self::addRequiredEntry('www', 'A', $required_entries);
+ self::addRequiredEntry('www', 'AAAA', $required_entries);
+ }
+
+ if (! $froxlorhostname) {
+ // additional required records for subdomains
+ $subdomains_stmt = Database::prepare("
+ SELECT `domain`, `iswildcarddomain`, `wwwserveralias` FROM `" . TABLE_PANEL_DOMAINS . "`
+ WHERE `parentdomainid` = :domainid
+ ");
+ Database::pexecute($subdomains_stmt, array(
+ 'domainid' => $domain_id
+ ));
+
+ while ($subdomain = $subdomains_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ // Listing domains is enough as there currently is no support for choosing
+ // different ips for a subdomain => use same IPs as toplevel
+ self::addRequiredEntry(str_replace('.' . $domain['domain'], '', $subdomain['domain']), 'A', $required_entries);
+ self::addRequiredEntry(str_replace('.' . $domain['domain'], '', $subdomain['domain']), 'AAAA', $required_entries);
+
+ // Check whether to add a www.-prefix
+ if ($subdomain['iswildcarddomain'] == '1') {
+ self::addRequiredEntry('*.' . str_replace('.' . $domain['domain'], '', $subdomain['domain']), 'A', $required_entries);
+ self::addRequiredEntry('*.' . str_replace('.' . $domain['domain'], '', $subdomain['domain']), 'AAAA', $required_entries);
+ } elseif ($subdomain['wwwserveralias'] == '1') {
+ self::addRequiredEntry('www.' . str_replace('.' . $domain['domain'], '', $subdomain['domain']), 'A', $required_entries);
+ self::addRequiredEntry('www.' . str_replace('.' . $domain['domain'], '', $subdomain['domain']), 'AAAA', $required_entries);
+ }
+ }
+ }
+
+ // additional required records for CAA if activated
+ if (Settings::Get('system.dns_createcaaentry') && Settings::Get('system.use_ssl') == "1" && !empty($domain['p_ssl_ipandports'])) {
+ // check for CAA content later
+ self::addRequiredEntry('@CAA@', 'CAA', $required_entries);
+ }
+
+ // additional required records for SPF and DKIM if activated
+ if ($domain['isemaildomain'] == '1') {
+ if (Settings::Get('spf.use_spf') == '1') {
+ // check for SPF content later
+ self::addRequiredEntry('@SPF@', 'TXT', $required_entries);
+ }
+ if (Settings::Get('dkim.use_dkim') == '1') {
+ // check for DKIM content later
+ self::addRequiredEntry('dkim' . $domain['dkim_id'] . '._domainkey', 'TXT', $required_entries);
+ }
+ }
+
+ $primary_ns = null;
+ $zonerecords = array();
+
+ // now generate all records and unset the required entries we have
+ foreach ($dom_entries as $entry) {
+ if (array_key_exists($entry['type'], $required_entries) && array_key_exists(md5($entry['record']), $required_entries[$entry['type']])) {
+ unset($required_entries[$entry['type']][md5($entry['record'])]);
+ }
+ if (Settings::Get('system.dns_createcaaentry') == '1' && $entry['type'] == 'CAA' && strtolower(substr($entry['content'], 0, 7)) == '"v=caa1') {
+ // unset special CAA required-entry
+ unset($required_entries[$entry['type']][md5("@CAA@")]);
+ }
+ if (Settings::Get('spf.use_spf') == '1' && $entry['type'] == 'TXT' && $entry['record'] == '@' && (strtolower(substr($entry['content'], 0, 7)) == '"v=spf1' || strtolower(substr($entry['content'], 0, 6)) == 'v=spf1') ) {
+ // unset special spf required-entry
+ unset($required_entries[$entry['type']][md5("@SPF@")]);
+ }
+ if (empty($primary_ns) && $entry['type'] == 'NS') {
+ // use the first NS entry as primary ns
+ $primary_ns = $entry['content'];
+ }
+ // check for CNAME on @, www- or wildcard-Alias and remove A/AAAA record accordingly
+ foreach (['@', 'www', '*'] as $crceord) {
+ if ($entry['type'] == 'CNAME' && $entry['record'] == '@' && (array_key_exists(md5($crceord), $required_entries['A']) || array_key_exists(md5($crceord), $required_entries['AAAA']))) {
+ unset($required_entries['A'][md5($crceord)]);
+ unset($required_entries['AAAA'][md5($crceord)]);
+ }
+ }
+ $zonerecords[] = new DnsEntry($entry['record'], $entry['type'], $entry['content'], $entry['prio'], $entry['ttl']);
+ }
+
+ // add missing required entries
+ if (! empty($required_entries)) {
+
+ // A / AAAA records
+ if (array_key_exists("A", $required_entries) || array_key_exists("AAAA", $required_entries)) {
+ if ($froxlorhostname) {
+ // use all available IP's for the froxlor-hostname
+ $result_ip_stmt = Database::prepare("
+ SELECT `ip` FROM `" . TABLE_PANEL_IPSANDPORTS . "` GROUP BY `ip`
+ ");
+ Database::pexecute($result_ip_stmt);
+ } else {
+ $result_ip_stmt = Database::prepare("
+ SELECT `p`.`ip` AS `ip`
+ FROM `" . TABLE_PANEL_IPSANDPORTS . "` `p`, `" . TABLE_DOMAINTOIP . "` `di`
+ WHERE `di`.`id_domain` = :domainid AND `p`.`id` = `di`.`id_ipandports`
+ GROUP BY `p`.`ip`;
+ ");
+ Database::pexecute($result_ip_stmt, array(
+ 'domainid' => $domain_id
+ ));
+ }
+ $all_ips = $result_ip_stmt->fetchAll(\PDO::FETCH_ASSOC);
+
+ foreach ($all_ips as $ip) {
+ foreach ($required_entries as $type => $records) {
+ foreach ($records as $record) {
+ if ($type == 'A' && filter_var($ip['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false) {
+ $zonerecords[] = new DnsEntry($record, 'A', $ip['ip']);
+ } elseif ($type == 'AAAA' && filter_var($ip['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false) {
+ $zonerecords[] = new DnsEntry($record, 'AAAA', $ip['ip']);
+ }
+ }
+ }
+ }
+ unset($required_entries['A']);
+ unset($required_entries['AAAA']);
+ }
+
+ // NS records
+ if (array_key_exists("NS", $required_entries)) {
+ if (Settings::Get('system.nameservers') != '') {
+ $nameservers = explode(',', Settings::Get('system.nameservers'));
+ foreach ($nameservers as $nameserver) {
+ $nameserver = trim($nameserver);
+ // append dot to hostname
+ if (substr($nameserver, - 1, 1) != '.') {
+ $nameserver .= '.';
+ }
+ foreach ($required_entries as $type => $records) {
+ if ($type == 'NS') {
+ foreach ($records as $record) {
+ if (empty($primary_ns)) {
+ // use the first NS entry as primary ns
+ $primary_ns = $nameserver;
+ }
+ $zonerecords[] = new DnsEntry($record, 'NS', $nameserver);
+ }
+ }
+ }
+ }
+ unset($required_entries['NS']);
+ }
+ }
+
+ // MX records
+ if (array_key_exists("MX", $required_entries)) {
+ if (Settings::Get('system.mxservers') != '') {
+ $mxservers = explode(',', Settings::Get('system.mxservers'));
+ foreach ($mxservers as $mxserver) {
+ $mxserver = trim($mxserver);
+ if (substr($mxserver, - 1, 1) != '.') {
+ $mxserver .= '.';
+ }
+ // split in prio and server
+ $mx_details = explode(" ", $mxserver);
+ if (count($mx_details) == 1) {
+ $mx_details[1] = $mx_details[0];
+ $mx_details[0] = 10;
+ }
+ foreach ($required_entries as $type => $records) {
+ if ($type == 'MX') {
+ foreach ($records as $record) {
+ $zonerecords[] = new DnsEntry($record, 'MX', $mx_details[1], $mx_details[0]);
+ }
+ }
+ }
+ }
+ unset($required_entries['MX']);
+ }
+ }
+
+ // TXT (SPF and DKIM)
+ if (array_key_exists("TXT", $required_entries)) {
+
+ if (Settings::Get('dkim.use_dkim') == '1') {
+ $dkim_entries = self::generateDkimEntries($domain);
+ }
+
+ foreach ($required_entries as $type => $records) {
+ if ($type == 'TXT') {
+ foreach ($records as $record) {
+ if ($record == '@SPF@') {
+ $txt_content = Settings::Get('spf.spf_entry');
+ $zonerecords[] = new DnsEntry('@', 'TXT', self::encloseTXTContent($txt_content));
+ } elseif ($record == 'dkim' . $domain['dkim_id'] . '._domainkey' && ! empty($dkim_entries)) {
+ // check for multiline entry
+ $multiline = false;
+ if (substr($dkim_entries[0], 0, 1) == '(') {
+ $multiline = true;
+ }
+ $zonerecords[] = new DnsEntry($record, 'TXT', self::encloseTXTContent($dkim_entries[0], $multiline));
+ }
+ }
+ }
+ }
+ }
+
+ // CAA
+ if (array_key_exists("CAA", $required_entries)) {
+ foreach ($required_entries as $type => $records) {
+ if ($type == 'CAA') {
+ foreach ($records as $record) {
+ if ($record == '@CAA@') {
+ $caa_entries = explode(PHP_EOL, Settings::Get('caa.caa_entry'));
+ if ($domain['letsencrypt'] == 1) {
+ $le_entry = $domain['iswildcarddomain'] == '1' ? '0 issuewild "letsencrypt.org"' : '0 issue "letsencrypt.org"';
+ array_push($caa_entries, $le_entry);
+ }
+
+ foreach ($caa_entries as $entry) {
+ $zonerecords[] = new DnsEntry('@', 'CAA', $entry);
+ // additional required records by subdomain setting
+ if ($domain['wwwserveralias'] == '1') {
+ $zonerecords[] = new DnsEntry('www', 'CAA', $entry);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (empty($primary_ns)) {
+ // TODO log error: no NS given, use system-hostname
+ $primary_ns = Settings::Get('system.hostname');
+ }
+
+ if (! $isMainButSubTo) {
+ $date = date('Ymd');
+ $domain['bindserial'] = (preg_match('/^' . $date . '/', $domain['bindserial']) ? $domain['bindserial'] + 1 : $date . '00');
+ if (! $froxlorhostname) {
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_DOMAINS . "` SET
+ `bindserial` = :serial
+ WHERE `id` = :id
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'serial' => $domain['bindserial'],
+ 'id' => $domain['id']
+ ));
+ }
+
+ // PowerDNS does not like multi-line-format
+ $soa_content = $primary_ns . " " . self::escapeSoaAdminMail(Settings::Get('panel.adminmail')) . " ";
+ $soa_content .= $domain['bindserial'] . " ";
+ // TODO for now, dummy time-periods
+ $soa_content .= "3600 900 604800 " . (int) Settings::Get('system.defaultttl');
+
+ $soa_record = new DnsEntry('@', 'SOA', $soa_content);
+ array_unshift($zonerecords, $soa_record);
+ }
+
+ $zone = new DnsZone((int) Settings::Get('system.defaultttl'), $domain['domain'], $domain['bindserial'], $zonerecords);
+
+ return $zone;
+ }
+
+ private static function addRequiredEntry($record = '@', $type = 'A', &$required = array())
+ {
+ if (! isset($required[$type])) {
+ $required[$type] = array();
+ }
+ $required[$type][md5($record)] = $record;
+ }
+
+ public static function encloseTXTContent($txt_content, $isMultiLine = false)
+ {
+ // check that TXT content is enclosed in " "
+ if ($isMultiLine == false && Settings::Get('system.dns_server') != 'PowerDNS') {
+ if (substr($txt_content, 0, 1) != '"') {
+ $txt_content = '"' . $txt_content;
+ }
+ if (substr($txt_content, - 1) != '"') {
+ $txt_content .= '"';
+ }
+ }
+ if (Settings::Get('system.dns_server') == 'PowerDNS') {
+ // no quotation for PowerDNS
+ if (substr($txt_content, 0, 1) == '"') {
+ $txt_content = substr($txt_content, 1);
+ }
+ if (substr($txt_content, - 1) == '"') {
+ $txt_content = substr($txt_content, 0, - 1);
+ }
+ }
+ return $txt_content;
+ }
+
+ private static function escapeSoaAdminMail($email)
+ {
+ $mail_parts = explode("@", $email);
+ $escpd_mail = str_replace(".", "\.", $mail_parts[0]) . "." . $mail_parts[1] . ".";
+ return $escpd_mail;
+ }
+
+ private static function generateDkimEntries($domain)
+ {
+ $zone_dkim = array();
+
+ if (Settings::Get('dkim.use_dkim') == '1' && $domain['dkim'] == '1' && $domain['dkim_pubkey'] != '') {
+ // start
+ $dkim_txt = 'v=DKIM1;';
+
+ // algorithm
+ $algorithm = explode(',', Settings::Get('dkim.dkim_algorithm'));
+ $alg = '';
+ foreach ($algorithm as $a) {
+ if ($a == 'all') {
+ break;
+ } else {
+ $alg .= $a . ':';
+ }
+ }
+
+ if ($alg != '') {
+ $alg = substr($alg, 0, - 1);
+ $dkim_txt .= 'h=' . $alg . ';';
+ }
+
+ // notes
+ if (trim(Settings::Get('dkim.dkim_notes') != '')) {
+ $dkim_txt .= 'n=' . trim(Settings::Get('dkim.dkim_notes')) . ';';
+ }
+
+ // key
+ $dkim_txt .= 'k=rsa;p=' . trim(preg_replace('/-----BEGIN PUBLIC KEY-----(.+)-----END PUBLIC KEY-----/s', '$1', str_replace("\n", '', $domain['dkim_pubkey']))) . ';';
+
+ // service-type
+ if (Settings::Get('dkim.dkim_servicetype') == '1') {
+ $dkim_txt .= 's=email;';
+ }
+
+ // end-part
+ $dkim_txt .= 't=s';
+
+ // dkim-entry
+ $zone_dkim[] = $dkim_txt;
+ }
+
+ return $zone_dkim;
+ }
+}
diff --git a/lib/Froxlor/Dns/DnsEntry.php b/lib/Froxlor/Dns/DnsEntry.php
new file mode 100644
index 00000000..6d388ac1
--- /dev/null
+++ b/lib/Froxlor/Dns/DnsEntry.php
@@ -0,0 +1,74 @@
+ (2016-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Classes
+ *
+ */
+class DnsEntry
+{
+
+ public $record;
+
+ public $ttl;
+
+ public $class = 'IN';
+
+ public $type;
+
+ public $priority;
+
+ public $content;
+
+ public function __construct($record = '', $type = 'A', $content = null, $prio = 0, $ttl = 0, $class = 'IN')
+ {
+ $this->record = $record;
+ $this->type = $type;
+ $this->content = $content;
+ $this->priority = $prio;
+ $this->ttl = ($ttl <= 0 ? Settings::Get('system.defaultttl') : $ttl);
+ $this->class = $class;
+ }
+
+ public function __toString()
+ {
+ $_content = $this->content;
+ // check content length for txt records for bind9 (multiline)
+ if (Settings::Get('system.dns_server') != 'pdns' && $this->type == 'TXT' && strlen($_content) >= 255) {
+ // split string
+ $_contentlines = str_split($_content, 254);
+ // first line
+ $_l = array_shift($_contentlines);
+ // check for starting quote
+ if (substr($_l, 0, 1) == '"') {
+ $_l = substr($_l, 1);
+ }
+ $_content = '("' . $_l . '"' . PHP_EOL;
+ $_l = array_pop($_contentlines);
+ // check for ending quote
+ if (substr($_l, - 1) == '"') {
+ $_l = substr($_l, 0, - 1);
+ }
+ foreach ($_contentlines as $_cl) {
+ // lines in between
+ $_content .= "\t\t\t\t" . '"' . $_cl . '"' . PHP_EOL;
+ }
+ // last line
+ $_content .= "\t\t\t\t" . '"' . $_l . '")';
+ }
+ $result = $this->record . "\t" . $this->ttl . "\t" . $this->class . "\t" . $this->type . "\t" . (($this->priority >= 0 && ($this->type == 'MX' || $this->type == 'SRV')) ? $this->priority . "\t" : "") . $_content . PHP_EOL;
+ return $result;
+ }
+}
diff --git a/lib/Froxlor/Dns/DnsZone.php b/lib/Froxlor/Dns/DnsZone.php
new file mode 100644
index 00000000..863b56f2
--- /dev/null
+++ b/lib/Froxlor/Dns/DnsZone.php
@@ -0,0 +1,50 @@
+ (2016-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Classes
+ *
+ */
+class DnsZone
+{
+
+ public $ttl;
+
+ public $origin;
+
+ public $serial;
+
+ public $records;
+
+ public function __construct($ttl = 0, $origin = '', $serial = '', $records = null)
+ {
+ $this->ttl = ($ttl <= 0 ? Settings::Get('system.defaultttl') : $ttl);
+ $this->origin = $origin;
+ $this->serial = $serial;
+ $this->records = $records;
+ }
+
+ public function __toString()
+ {
+ $_zonefile = "\$TTL " . $this->ttl . PHP_EOL;
+ $_zonefile .= "\$ORIGIN " . $this->origin . "." . PHP_EOL;
+ if (! empty($this->records)) {
+ foreach ($this->records as $record) {
+ $_zonefile .= (string) $record;
+ }
+ }
+ return $_zonefile;
+ }
+}
diff --git a/lib/Froxlor/Dns/PowerDNS.php b/lib/Froxlor/Dns/PowerDNS.php
new file mode 100644
index 00000000..b8ba6692
--- /dev/null
+++ b/lib/Froxlor/Dns/PowerDNS.php
@@ -0,0 +1,137 @@
+ (2016-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ */
+class PowerDNS
+{
+
+ private static $pdns_db = null;
+
+ private static function connectToPdnsDb()
+ {
+ // get froxlor pdns config
+ $cf = Settings::Get('system.bindconf_directory') . '/froxlor/pdns_froxlor.conf';
+ $config = \Froxlor\FileDir::makeCorrectFile($cf);
+
+ if (! file_exists($config)) {
+ die('PowerDNS configuration file (' . $config . ') not found. Did you go through the configuration templates?' . PHP_EOL);
+ }
+ $lines = file($config);
+ $mysql_data = array();
+ foreach ($lines as $line) {
+ $line = trim($line);
+ if (strtolower(substr($line, 0, 6)) == 'gmysql') {
+ $namevalue = explode("=", $line);
+ $mysql_data[$namevalue[0]] = $namevalue[1];
+ }
+ }
+
+ // build up connection string
+ $driver = 'mysql';
+ $dsn = $driver . ":";
+ $options = array(
+ 'PDO::MYSQL_ATTR_INIT_COMMAND' => 'SET names utf8'
+ );
+ $attributes = array(
+ 'ATTR_ERRMODE' => 'ERRMODE_EXCEPTION'
+ );
+ $dbconf = array();
+
+ $dbconf["dsn"] = array(
+ 'dbname' => $mysql_data["gmysql-dbname"],
+ 'charset' => 'utf8'
+ );
+
+ if (isset($mysql_data['gmysql-socket']) && ! empty($mysql_data['gmysql-socket'])) {
+ $dbconf["dsn"]['unix_socket'] = \Froxlor\FileDir::makeCorrectFile($mysql_data['gmysql-socket']);
+ } else {
+ $dbconf["dsn"]['host'] = $mysql_data['gmysql-host'];
+ $dbconf["dsn"]['port'] = $mysql_data['gmysql-port'];
+ }
+
+ // add options to dsn-string
+ foreach ($dbconf["dsn"] as $k => $v) {
+ $dsn .= $k . "=" . $v . ";";
+ }
+
+ // clean up
+ unset($dbconf);
+
+ // try to connect
+ try {
+ self::$pdns_db = new \PDO($dsn, $mysql_data['gmysql-user'], $mysql_data['gmysql-password'], $options);
+ } catch (\PDOException $e) {
+ die($e->getMessage());
+ }
+
+ // set attributes
+ foreach ($attributes as $k => $v) {
+ self::$pdns_db->setAttribute(constant("PDO::" . $k), constant("PDO::" . $v));
+ }
+
+ $version_server = self::$pdns_db->getAttribute(\PDO::ATTR_SERVER_VERSION);
+ $sql_mode = 'NO_ENGINE_SUBSTITUTION';
+ if (version_compare($version_server, '8.0.11', '<')) {
+ $sql_mode .= ',NO_AUTO_CREATE_USER';
+ }
+ self::$pdns_db->exec('SET sql_mode = "' . $sql_mode . '"');
+ }
+
+ /**
+ * get pdo database connection to powerdns database
+ *
+ * @return \PDO
+ */
+ public static function getDB()
+ {
+ if (! isset(self::$pdns_db) || (self::$pdns_db instanceof \PDO) == false) {
+ self::connectToPdnsDb();
+ }
+ return self::$pdns_db;
+ }
+
+ /**
+ * remove all records and entries of a given domain
+ *
+ * @param array $domain
+ */
+ public static function cleanDomainZone($domain = null)
+ {
+ if (is_array($domain) && isset($domain['domain'])) {
+ $pdns_domains_stmt = self::getDB()->prepare("SELECT `id`, `name` FROM `domains` WHERE `name` = :domain");
+ $del_rec_stmt = self::getDB()->prepare("DELETE FROM `records` WHERE `domain_id` = :did");
+ $del_meta_stmt = self::getDB()->prepare("DELETE FROM `domainmetadata` WHERE `domain_id` = :did");
+ $del_dom_stmt = self::getDB()->prepare("DELETE FROM `domains` WHERE `id` = :did");
+
+ $pdns_domains_stmt->execute(array(
+ 'domain' => $domain['domain']
+ ));
+ $pdns_domain = $pdns_domains_stmt->fetch(\PDO::FETCH_ASSOC);
+
+ $del_rec_stmt->execute(array(
+ 'did' => $pdns_domain['id']
+ ));
+ $del_meta_stmt->execute(array(
+ 'did' => $pdns_domain['id']
+ ));
+ $del_dom_stmt->execute(array(
+ 'did' => $pdns_domain['id']
+ ));
+ }
+ }
+}
diff --git a/lib/Froxlor/Domain/Domain.php b/lib/Froxlor/Domain/Domain.php
new file mode 100644
index 00000000..c70c6cac
--- /dev/null
+++ b/lib/Froxlor/Domain/Domain.php
@@ -0,0 +1,390 @@
+ 0) {
+ $sel_stmt = Database::prepare("
+ SELECT i.ip FROM `" . TABLE_PANEL_IPSANDPORTS . "` `i`
+ LEFT JOIN `" . TABLE_DOMAINTOIP . "` `dip` ON dip.id_ipandports = i.id
+ AND dip.id_domain = :domainid
+ GROUP BY i.ip
+ ");
+ $sel_param = array(
+ 'domainid' => $domain_id
+ );
+ } else {
+ // assuming froxlor.vhost (id = 0)
+ $sel_stmt = Database::prepare("
+ SELECT ip FROM `" . TABLE_PANEL_IPSANDPORTS . "`
+ GROUP BY ip
+ ");
+ $sel_param = array();
+ }
+ Database::pexecute($sel_stmt, $sel_param);
+ $result = array();
+ while ($ip = $sel_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = $ip['ip'];
+ }
+ return $result;
+ }
+
+ /**
+ * return an array of all enabled redirect-codes
+ *
+ * @return array array of enabled redirect-codes
+ */
+ public static function getRedirectCodesArray()
+ {
+ $sql = "SELECT * FROM `" . TABLE_PANEL_REDIRECTCODES . "` WHERE `enabled` = '1' ORDER BY `id` ASC";
+ $result_stmt = Database::query($sql);
+
+ $codes = array();
+ while ($rc = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $codes[] = $rc;
+ }
+
+ return $codes;
+ }
+
+ /**
+ * return an array of all enabled redirect-codes
+ * for the settings form
+ *
+ * @param bool $add_desc
+ * optional, default true, add the code-description
+ *
+ * @return array array of enabled redirect-codes
+ */
+ public static function getRedirectCodes($add_desc = true)
+ {
+ global $lng;
+
+ $sql = "SELECT * FROM `" . TABLE_PANEL_REDIRECTCODES . "` WHERE `enabled` = '1' ORDER BY `id` ASC";
+ $result_stmt = Database::query($sql);
+
+ $codes = array();
+ while ($rc = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $codes[$rc['id']] = $rc['code'];
+ if ($add_desc) {
+ $codes[$rc['id']] .= ' (' . $lng['redirect_desc'][$rc['desc']] . ')';
+ }
+ }
+
+ return $codes;
+ }
+
+ /**
+ * returns the redirect-code for a given
+ * domain-id
+ *
+ * @param integer $domainid
+ * id of the domain
+ *
+ * @return string redirect-code
+ */
+ public static function getDomainRedirectCode($domainid = 0)
+ {
+
+ // get system default
+ $default = '301';
+ if (\Froxlor\Settings::Get('customredirect.enabled') == '1') {
+ $all_codes = self::getRedirectCodes(false);
+ $_default = $all_codes[\Froxlor\Settings::Get('customredirect.default')];
+ $default = ($_default == '---') ? $default : $_default;
+ }
+ $code = $default;
+ if ($domainid > 0) {
+
+ $result_stmt = Database::prepare("
+ SELECT `r`.`code` as `redirect`
+ FROM `" . TABLE_PANEL_REDIRECTCODES . "` `r`, `" . TABLE_PANEL_DOMAINREDIRECTS . "` `rc`
+ WHERE `r`.`id` = `rc`.`rid` and `rc`.`did` = :domainid
+ ");
+ $result = Database::pexecute_first($result_stmt, array(
+ 'domainid' => $domainid
+ ));
+
+ if (is_array($result) && isset($result['redirect'])) {
+ $code = ($result['redirect'] == '---') ? $default : $result['redirect'];
+ }
+ }
+ return $code;
+ }
+
+ /**
+ * returns the redirect-id for a given
+ * domain-id
+ *
+ * @param integer $domainid
+ * id of the domain
+ *
+ * @return integer redirect-code-id
+ */
+ public static function getDomainRedirectId($domainid = 0)
+ {
+ $code = 1;
+ if ($domainid > 0) {
+ $result_stmt = Database::prepare("
+ SELECT `r`.`id` as `redirect`
+ FROM `" . TABLE_PANEL_REDIRECTCODES . "` `r`, `" . TABLE_PANEL_DOMAINREDIRECTS . "` `rc`
+ WHERE `r`.`id` = `rc`.`rid` and `rc`.`did` = :domainid
+ ");
+ $result = Database::pexecute_first($result_stmt, array(
+ 'domainid' => $domainid
+ ));
+
+ if (is_array($result) && isset($result['redirect'])) {
+ $code = (int) $result['redirect'];
+ }
+ }
+ return $code;
+ }
+
+ /**
+ * adds a redirectcode for a domain
+ *
+ * @param integer $domainid
+ * id of the domain to add the code for
+ * @param integer $redirect
+ * selected redirect-id
+ *
+ * @return null
+ */
+ public static function addRedirectToDomain($domainid = 0, $redirect = 1)
+ {
+ if ($domainid > 0) {
+ $ins_stmt = Database::prepare("
+ INSERT INTO `" . TABLE_PANEL_DOMAINREDIRECTS . "` SET `rid` = :rid, `did` = :did
+ ");
+ Database::pexecute($ins_stmt, array(
+ 'rid' => $redirect,
+ 'did' => $domainid
+ ));
+ }
+ }
+
+ /**
+ * updates the redirectcode of a domain
+ * if redirect-code is false, nothing happens
+ *
+ * @param integer $domainid
+ * id of the domain to update
+ * @param integer $redirect
+ * selected redirect-id or false
+ *
+ * @return null
+ */
+ public static function updateRedirectOfDomain($domainid = 0, $redirect = false)
+ {
+ if ($redirect == false) {
+ return;
+ }
+
+ if ($domainid > 0) {
+ $del_stmt = Database::prepare("
+ DELETE FROM `" . TABLE_PANEL_DOMAINREDIRECTS . "` WHERE `did` = :domainid
+ ");
+ Database::pexecute($del_stmt, array(
+ 'domainid' => $domainid
+ ));
+
+ $ins_stmt = Database::prepare("
+ INSERT INTO `" . TABLE_PANEL_DOMAINREDIRECTS . "` SET `rid` = :rid, `did` = :did
+ ");
+ Database::pexecute($ins_stmt, array(
+ 'rid' => $redirect,
+ 'did' => $domainid
+ ));
+ }
+ }
+
+ /**
+ * check whether a domain has subdomains added as full-domains
+ * #329
+ *
+ * @param int $id
+ * domain-id
+ *
+ * @return boolean
+ */
+ public static function domainHasMainSubDomains($id = 0)
+ {
+ $result_stmt = Database::prepare("
+ SELECT COUNT(`id`) as `mainsubs` FROM `" . TABLE_PANEL_DOMAINS . "`
+ WHERE `ismainbutsubto` = :id");
+ $result = Database::pexecute_first($result_stmt, array(
+ 'id' => $id
+ ));
+
+ if (isset($result['mainsubs']) && $result['mainsubs'] > 0) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * check whether a subof-domain exists
+ * #329
+ *
+ * @param int $id
+ * subof-domain-id
+ *
+ * @return boolean
+ */
+ public static function domainMainToSubExists($id = 0)
+ {
+ $result_stmt = Database::prepare("
+ SELECT `id` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `id` = :id");
+ Database::pexecute($result_stmt, array(
+ 'id' => $id
+ ));
+ $result = $result_stmt->fetch(\PDO::FETCH_ASSOC);
+
+ if (isset($result['id']) && $result['id'] > 0) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Check whether a given domain has an ssl-ip/port assigned
+ *
+ * @param int $domainid
+ *
+ * @return boolean
+ */
+ public static function domainHasSslIpPort($domainid = 0)
+ {
+ $result_stmt = Database::prepare("
+ SELECT `dt`.* FROM `" . TABLE_DOMAINTOIP . "` `dt`, `" . TABLE_PANEL_IPSANDPORTS . "` `iap`
+ WHERE `dt`.`id_ipandports` = `iap`.`id` AND `iap`.`ssl` = '1' AND `dt`.`id_domain` = :domainid;");
+ Database::pexecute($result_stmt, array(
+ 'domainid' => $domainid
+ ));
+ $result = $result_stmt->fetch(\PDO::FETCH_ASSOC);
+
+ if (is_array($result) && isset($result['id_ipandports'])) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * returns true or false whether a given domain id
+ * is the std-subdomain of a customer
+ *
+ * @param
+ * int domain-id
+ *
+ * @return boolean
+ */
+ public static function isCustomerStdSubdomain($did = 0)
+ {
+ if ($did > 0) {
+ $result_stmt = Database::prepare("
+ SELECT `customerid` FROM `" . TABLE_PANEL_CUSTOMERS . "`
+ WHERE `standardsubdomain` = :did
+ ");
+ $result = Database::pexecute_first($result_stmt, array(
+ 'did' => $did
+ ));
+
+ if (is_array($result) && isset($result['customerid']) && $result['customerid'] > 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static function triggerLetsEncryptCSRForAliasDestinationDomain($aliasDestinationDomainID, $log)
+ {
+ if (isset($aliasDestinationDomainID) && $aliasDestinationDomainID > 0) {
+ $log->logAction(\Froxlor\FroxlorLogger::ADM_ACTION, LOG_INFO, "LetsEncrypt CSR triggered for domain ID " . $aliasDestinationDomainID);
+ $upd_stmt = Database::prepare("UPDATE
+ `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "`
+ SET
+ `expirationdate` = null
+ WHERE
+ domainid = :domainid
+ ");
+ Database::pexecute($upd_stmt, array(
+ 'domainid' => $aliasDestinationDomainID
+ ));
+ }
+ }
+
+ public static function doLetsEncryptCleanUp($domainname = null)
+ {
+ // @ see \Froxlor\Cron\Http\LetsEncrypt\AcmeSh.php
+ $acmesh = \Froxlor\Cron\Http\LetsEncrypt\AcmeSh::getAcmeSh();
+ if (file_exists($acmesh)) {
+ $certificate_folder = \Froxlor\Cron\Http\LetsEncrypt\AcmeSh::getWorkingDirFromEnv($domainname);
+ if (file_exists($certificate_folder)) {
+ $params = " --remove -d " . $domainname;
+ if (\Froxlor\Settings::Get('system.leecc') > 0) {
+ $params .= " --ecc";
+ }
+ // run remove command
+ \Froxlor\FileDir::safe_exec($acmesh . $params);
+ // remove certificates directory
+ @unlink($certificate_folder);
+ }
+ }
+ return true;
+ }
+
+ /**
+ * checks give path for security issues
+ * and returns a string that can be appended
+ * to a line for a open_basedir directive
+ *
+ * @param string $path
+ * the path to check and append
+ * @param boolean $first
+ * if true, no ':' will be prefixed to the path
+ *
+ * @return string
+ */
+ public static function appendOpenBasedirPath($path = '', $first = false)
+ {
+ if ($path != '' && $path != '/' && (! preg_match("#^/dev#i", $path) || preg_match("#^/dev/urandom#i", $path)) && ! preg_match("#^/proc#i", $path) && ! preg_match("#^/etc#i", $path) && ! preg_match("#^/sys#i", $path) && ! preg_match("#:#", $path)) {
+
+ if (preg_match("#^/dev/urandom#i", $path)) {
+ $path = \Froxlor\FileDir::makeCorrectFile($path);
+ } else {
+ $path = \Froxlor\FileDir::makeCorrectDir($path);
+ }
+
+ // check for php-version that requires the trailing
+ // slash to be removed as it does not allow the usage
+ // of the subfolders within the given folder, fixes #797
+ if ((PHP_MINOR_VERSION == 2 && PHP_VERSION_ID >= 50216) || PHP_VERSION_ID >= 50304) {
+ // check trailing slash
+ if (substr($path, - 1, 1) == '/') {
+ // remove it
+ $path = substr($path, 0, - 1);
+ }
+ }
+
+ if ($first) {
+ return $path;
+ }
+
+ return ':' . $path;
+ }
+ return '';
+ }
+}
diff --git a/lib/Froxlor/Domain/IpAddr.php b/lib/Froxlor/Domain/IpAddr.php
new file mode 100644
index 00000000..62f9fea4
--- /dev/null
+++ b/lib/Froxlor/Domain/IpAddr.php
@@ -0,0 +1,83 @@
+fetch(\PDO::FETCH_ASSOC)) {
+
+ if (! isset($system_ipaddress_array[$row['ip']]) && ! in_array($row['ip'], $system_ipaddress_array)) {
+ if (filter_var($row['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
+ $row['ip'] = '[' . $row['ip'] . ']';
+ }
+ $system_ipaddress_array[$row['ip']] = $row['ip'];
+ }
+ }
+
+ return $system_ipaddress_array;
+ }
+
+ public static function getIpPortCombinations($ssl = false)
+ {
+ global $userinfo;
+
+ $additional_conditions_params = array();
+ $additional_conditions_array = array();
+
+ if ($userinfo['ip'] != '-1') {
+ $admin_ip_stmt = Database::prepare("
+ SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `id` = IN (:ipid)
+ ");
+ $myips = implode(",", json_decode($userinfo['ip'], true));
+ Database::pexecute($admin_ip_stmt, array(
+ 'ipid' => $myips
+ ));
+ $additional_conditions_array[] = "`ip` IN (:adminips)";
+ $additional_conditions_params['adminips'] = $myips;
+ }
+
+ if ($ssl !== null) {
+ $additional_conditions_array[] = "`ssl` = :ssl";
+ $additional_conditions_params['ssl'] = ($ssl === true ? '1' : '0');
+ }
+
+ $additional_conditions = '';
+ if (count($additional_conditions_array) > 0) {
+ $additional_conditions = " WHERE " . implode(" AND ", $additional_conditions_array) . " ";
+ }
+
+ $result_stmt = Database::prepare("
+ SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` " . $additional_conditions . " ORDER BY `ip` ASC, `port` ASC
+ ");
+
+ Database::pexecute($result_stmt, $additional_conditions_params);
+ $system_ipaddress_array = array();
+
+ while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if (filter_var($row['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
+ $row['ip'] = '[' . $row['ip'] . ']';
+ }
+ $system_ipaddress_array[$row['id']] = $row['ip'] . ':' . $row['port'];
+ }
+
+ return $system_ipaddress_array;
+ }
+
+ public static function getSslIpPortCombinations()
+ {
+ global $lng;
+ return array(
+ '' => $lng['panel']['none_value']
+ ) + self::getIpPortCombinations(true);
+ }
+}
diff --git a/lib/Froxlor/FileDir.php b/lib/Froxlor/FileDir.php
new file mode 100644
index 00000000..d7a5e55c
--- /dev/null
+++ b/lib/Froxlor/FileDir.php
@@ -0,0 +1,629 @@
+',
+ '<',
+ '`',
+ '$',
+ '~',
+ '?'
+ );
+
+ $acheck = false;
+ if ($allowedChars != null && is_array($allowedChars) && count($allowedChars) > 0) {
+ $acheck = true;
+ }
+
+ foreach ($disallowed as $dc) {
+ if ($acheck && in_array($dc, $allowedChars))
+ continue;
+ // check for bad signs in execute command
+ if (stristr($exec_string, $dc)) {
+ die("SECURITY CHECK FAILED!\nThe execute string '" . $exec_string . "' is a possible security risk!\nPlease check your whole server for security problems by hand!\n");
+ }
+ }
+
+ // execute the command and return output
+ $return = '';
+
+ // -------------------------------------------------------------------------------
+ if ($return_value == false) {
+ exec($exec_string, $return);
+ } else {
+ exec($exec_string, $return, $return_value);
+ }
+
+ return $return;
+ }
+
+ /**
+ * Creates a directory below a users homedir and sets all directories,
+ * which had to be created below with correct Owner/Group
+ * (Copied from cron_tasks.php:rev1189 as we'll need this more often in future)
+ *
+ * @param string $homeDir
+ * The homedir of the user
+ * @param string $dirToCreate
+ * The dir which should be created
+ * @param int $uid
+ * The uid of the user
+ * @param int $gid
+ * The gid of the user
+ * @param bool $placeindex
+ * Place standard-index.html into the new folder
+ * @param bool $allow_notwithinhomedir
+ * Allow creating a directory out of the customers docroot
+ *
+ * @return bool true if everything went okay, false if something went wrong
+ */
+ public static function mkDirWithCorrectOwnership($homeDir, $dirToCreate, $uid, $gid, $placeindex = false, $allow_notwithinhomedir = false)
+ {
+ if ($homeDir != '' && $dirToCreate != '') {
+ $homeDir = self::makeCorrectDir($homeDir);
+ $dirToCreate = self::makeCorrectDir($dirToCreate);
+
+ if (substr($dirToCreate, 0, strlen($homeDir)) == $homeDir) {
+ $subdir = substr($dirToCreate, strlen($homeDir) - 1);
+ $within_homedir = true;
+ } else {
+ $subdir = $dirToCreate;
+ $within_homedir = false;
+ }
+
+ $subdir = self::makeCorrectDir($subdir);
+ $subdirs = array();
+
+ if ($within_homedir || ! $allow_notwithinhomedir) {
+ $subdirlen = strlen($subdir);
+ $offset = 0;
+
+ while ($offset < $subdirlen) {
+ $offset = strpos($subdir, '/', $offset);
+ $subdirelem = substr($subdir, 0, $offset);
+ $offset ++;
+ array_push($subdirs, self::makeCorrectDir($homeDir . $subdirelem));
+ }
+ } else {
+ array_push($subdirs, $dirToCreate);
+ }
+
+ $subdirs = array_unique($subdirs);
+ sort($subdirs);
+ foreach ($subdirs as $sdir) {
+ if (! is_dir($sdir)) {
+ $sdir = self::makeCorrectDir($sdir);
+ self::safe_exec('mkdir -p ' . escapeshellarg($sdir));
+ // place index
+ if ($placeindex) {
+ $loginname = \Froxlor\Customer\Customer::getLoginNameByUid($uid);
+ if ($loginname !== false) {
+ self::storeDefaultIndex($loginname, $sdir, null);
+ }
+ }
+ self::safe_exec('chown -R ' . (int) $uid . ':' . (int) $gid . ' ' . escapeshellarg($sdir));
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * checks a directory against disallowed paths which could
+ * lead to a damaged system if you use them
+ *
+ * @param string $fieldname
+ * @param array $fielddata
+ * @param mixed $newfieldvalue
+ *
+ * @return boolean|array
+ */
+ public static function checkDisallowedPaths($path = null)
+ {
+
+ /*
+ * disallow base-directories and /
+ */
+ $disallowed_values = array(
+ "/",
+ "/bin/",
+ "/boot/",
+ "/dev/",
+ "/etc/",
+ "/home/",
+ "/lib/",
+ "/lib32/",
+ "/lib64/",
+ "/opt/",
+ "/proc/",
+ "/root/",
+ "/run/",
+ "/sbin/",
+ "/sys/",
+ "/tmp/",
+ "/usr/",
+ "/var/"
+ );
+
+ $path = self::makeCorrectDir($path);
+
+ // check if it's a disallowed path
+ if (in_array($path, $disallowed_values)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * store the default index-file in a given destination folder
+ *
+ * @param string $loginname
+ * customers loginname
+ * @param string $destination
+ * path where to create the file
+ * @param object $logger
+ * FroxlorLogger object
+ * @param boolean $force
+ * force creation whatever the settings say (needed for task #2, create new user)
+ *
+ * @return null
+ */
+ public static function storeDefaultIndex($loginname = null, $destination = null, $logger = null, $force = false)
+ {
+ if ($force || (int) Settings::Get('system.store_index_file_subs') == 1) {
+ $result_stmt = Database::prepare("
+ SELECT `t`.`value`, `c`.`email` AS `customer_email`, `a`.`email` AS `admin_email`, `c`.`loginname` AS `customer_login`, `a`.`loginname` AS `admin_login`
+ FROM `" . TABLE_PANEL_CUSTOMERS . "` AS `c` INNER JOIN `" . TABLE_PANEL_ADMINS . "` AS `a`
+ ON `c`.`adminid` = `a`.`adminid`
+ INNER JOIN `" . TABLE_PANEL_TEMPLATES . "` AS `t`
+ ON `a`.`adminid` = `t`.`adminid`
+ WHERE `varname` = 'index_html' AND `c`.`loginname` = :loginname");
+ Database::pexecute($result_stmt, array(
+ 'loginname' => $loginname
+ ));
+
+ if (Database::num_rows() > 0) {
+
+ $template = $result_stmt->fetch(\PDO::FETCH_ASSOC);
+
+ $replace_arr = array(
+ 'SERVERNAME' => Settings::Get('system.hostname'),
+ 'CUSTOMER' => $template['customer_login'],
+ 'ADMIN' => $template['admin_login'],
+ 'CUSTOMER_EMAIL' => $template['customer_email'],
+ 'ADMIN_EMAIL' => $template['admin_email']
+ );
+
+ // replaceVariables
+ $htmlcontent = PhpHelper::replaceVariables($template['value'], $replace_arr);
+ $indexhtmlpath = self::makeCorrectFile($destination . '/index.' . Settings::Get('system.index_file_extension'));
+ $index_html_handler = fopen($indexhtmlpath, 'w');
+ fwrite($index_html_handler, $htmlcontent);
+ fclose($index_html_handler);
+ if ($logger !== null) {
+ $logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Creating \'index.' . Settings::Get('system.index_file_extension') . '\' for Customer \'' . $template['customer_login'] . '\' based on template in directory ' . escapeshellarg($indexhtmlpath));
+ }
+ } else {
+ $destination = self::makeCorrectDir($destination);
+ if ($logger !== null) {
+ $logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: cp -a ' . \Froxlor\Froxlor::getInstallDir() . '/templates/misc/standardcustomer/* ' . escapeshellarg($destination));
+ }
+ self::safe_exec('cp -a ' . \Froxlor\Froxlor::getInstallDir() . '/templates/misc/standardcustomer/* ' . escapeshellarg($destination));
+ }
+ }
+ return;
+ }
+
+ /**
+ * Function which returns a correct filename, means to add a slash at the beginning if there wasn't one
+ *
+ * @param string $filename
+ * the filename
+ *
+ * @return string the corrected filename
+ */
+ public static function makeCorrectFile($filename)
+ {
+ if (! isset($filename) || trim($filename) == '') {
+ $error = 'Given filename for function ' . __FUNCTION__ . ' is empty.' . "\n";
+ $error .= 'This is very dangerous and should not happen.' . "\n";
+ $error .= 'Please inform the Froxlor team about this issue so they can fix it.';
+ echo $error;
+ // so we can see WHERE this happened
+ debug_print_backtrace();
+ die();
+ }
+
+ if (substr($filename, 0, 1) != '/') {
+ $filename = '/' . $filename;
+ }
+
+ $filename = self::makeSecurePath($filename);
+ return $filename;
+ }
+
+ /**
+ * Function which returns a correct dirname, means to add slashes at the beginning and at the end if there weren't some
+ *
+ * @param string $path
+ * the path to correct
+ *
+ * @throws \Exception
+ * @return string the corrected path
+ */
+ public static function makeCorrectDir($dir)
+ {
+ if (is_string($dir) && strlen($dir) > 0) {
+ $dir = trim($dir);
+ if (substr($dir, - 1, 1) != '/') {
+ $dir .= '/';
+ }
+ if (substr($dir, 0, 1) != '/') {
+ $dir = '/' . $dir;
+ }
+ return self::makeSecurePath($dir);
+ }
+ throw new \Exception("Cannot validate directory in " . __FUNCTION__ . " which is very dangerous.");
+ }
+
+ /**
+ * Function which returns a secure path, means to remove all multiple dots and slashes
+ *
+ * @param string $path
+ * the path to secure
+ *
+ * @return string the corrected path
+ */
+ public static function makeSecurePath($path)
+ {
+
+ // check for bad characters, some are allowed with escaping
+ // but we generally don't want them in our directory-names,
+ // thx to aaronmueller for this snipped
+ $badchars = array(
+ ':',
+ ';',
+ '|',
+ '&',
+ '>',
+ '<',
+ '`',
+ '$',
+ '~',
+ '?',
+ "\0"
+ );
+ foreach ($badchars as $bc) {
+ $path = str_replace($bc, "", $path);
+ }
+
+ $search = array(
+ '#/+#',
+ '#\.+#'
+ );
+ $replace = array(
+ '/',
+ '.'
+ );
+ $path = preg_replace($search, $replace, $path);
+ // don't just replace a space with an escaped space
+ // it might be escaped already
+ $path = str_replace("\ ", " ", $path);
+ $path = str_replace(" ", "\ ", $path);
+
+ return $path;
+ }
+
+ /**
+ * Function which returns a correct destination for Postfix Virtual Table
+ *
+ * @param
+ * string The destinations
+ * @return string the corrected destinations
+ * @author Florian Lippert
+ */
+ public static function makeCorrectDestination($destination)
+ {
+ $search = '/ +/';
+ $replace = ' ';
+ $destination = preg_replace($search, $replace, $destination);
+
+ if (substr($destination, 0, 1) == ' ') {
+ $destination = substr($destination, 1);
+ }
+
+ if (substr($destination, - 1, 1) == ' ') {
+ $destination = substr($destination, 0, strlen($destination) - 1);
+ }
+
+ return $destination;
+ }
+
+ /**
+ * Returns a valid html tag for the chosen $fieldType for paths
+ *
+ * @param
+ * string path The path to start searching in
+ * @param
+ * integer uid The uid which must match the found directories
+ * @param
+ * integer gid The gid which must match the found direcotries
+ * @param
+ * string value the value for the input-field
+ *
+ * @return string The html tag for the chosen $fieldType
+ *
+ * @author Martin Burchert
+ * @author Manuel Bernhardt
+ */
+ public static function makePathfield($path, $uid, $gid, $value = '', $dom = false)
+ {
+ global $lng;
+
+ $value = str_replace($path, '', $value);
+ $field = array();
+
+ // path is given without starting slash
+ // but dirList holds the paths with starting slash
+ // so we just add one here to get the correct
+ // default path selected, #225
+ if (substr($value, 0, 1) != '/' && ! $dom) {
+ $value = '/' . $value;
+ }
+
+ $fieldType = \Froxlor\Settings::Get('panel.pathedit');
+
+ if ($fieldType == 'Manual') {
+
+ $field = array(
+ 'type' => 'text',
+ 'value' => htmlspecialchars($value)
+ );
+ } elseif ($fieldType == 'Dropdown') {
+
+ $dirList = self::findDirs($path, $uid, $gid);
+ natcasesort($dirList);
+
+ if (sizeof($dirList) > 0) {
+ if (sizeof($dirList) <= 100) {
+ $_field = '';
+ foreach ($dirList as $dir) {
+ if (strpos($dir, $path) === 0) {
+ $dir = substr($dir, strlen($path));
+ // docroot cut off of current directory == empty -> directory is the docroot
+ if (empty($dir)) {
+ $dir = '/';
+ }
+ $dir = self::makeCorrectDir($dir);
+ }
+ $_field .= \Froxlor\UI\HTML::makeoption($dir, $dir, $value);
+ }
+ $field = array(
+ 'type' => 'select',
+ 'value' => $_field
+ );
+ } else {
+ // remove starting slash we added
+ // for the Dropdown, #225
+ $value = substr($value, 1);
+ // $field = $lng['panel']['toomanydirs'];
+ $field = array(
+ 'type' => 'text',
+ 'value' => htmlspecialchars($value),
+ 'note' => $lng['panel']['toomanydirs']
+ );
+ }
+ } else {
+ // $field = $lng['panel']['dirsmissing'];
+ // $field = ' ';
+ $field = array(
+ 'type' => 'hidden',
+ 'value' => '/',
+ 'note' => $lng['panel']['dirsmissing']
+ );
+ }
+ }
+
+ return $field;
+ }
+
+ /**
+ * Returns an array of found directories
+ *
+ * This function checks every found directory if they match either $uid or $gid, if they do
+ * the found directory is valid. It uses recursive-iterators to find subdirectories.
+ *
+ * @param string $path
+ * the path to start searching in
+ * @param int $uid
+ * the uid which must match the found directories
+ * @param int $gid
+ * the gid which must match the found direcotries
+ *
+ * @return array Array of found valid paths
+ */
+ private static function findDirs($path, $uid, $gid)
+ {
+ $_fileList = array();
+ $path = self::makeCorrectDir($path);
+
+ // valid directory?
+ if (is_dir($path)) {
+
+ // Will exclude everything under these directories
+ $exclude = array(
+ 'awstats',
+ 'webalizer'
+ );
+
+ /**
+ *
+ * @param SplFileInfo $file
+ * @param mixed $key
+ * @param RecursiveCallbackFilterIterator $iterator
+ * @return bool True if you need to recurse or if the item is acceptable
+ */
+ $filter = function ($file, $key, $iterator) use ($exclude) {
+ if (in_array($file->getFilename(), $exclude)) {
+ return false;
+ }
+ return true;
+ };
+
+ // create RecursiveIteratorIterator
+ $its = new \RecursiveIteratorIterator(new \RecursiveCallbackFilterIterator(new System\IgnorantRecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS), $filter));
+ // we can limit the recursion-depth, but will it be helpful or
+ // will people start asking "why do I only see 2 subdirectories, i want to use /a/b/c"
+ // let's keep this in mind and see whether it will be useful
+ // @TODO
+ // $its->setMaxDepth(2);
+
+ // check every file
+ foreach ($its as $fullFileName => $it) {
+ if ($it->isDir() && (fileowner($fullFileName) == $uid || filegroup($fullFileName) == $gid)) {
+ $_fileList[] = self::makeCorrectDir(dirname($fullFileName));
+ }
+ }
+ $_fileList[] = $path;
+ }
+
+ return array_unique($_fileList);
+ }
+
+ /**
+ * check if the system is FreeBSD (if exact)
+ * or BSD-based (NetBSD, OpenBSD, etc.
+ * if exact = false [default])
+ *
+ * @param boolean $exact
+ * whether to check explicitly for FreeBSD or *BSD
+ *
+ * @return boolean
+ */
+ public static function isFreeBSD($exact = false)
+ {
+ if (($exact && PHP_OS == 'FreeBSD') || (! $exact && stristr(PHP_OS, 'BSD'))) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * set the immutable flag for a file
+ *
+ * @param string $filename
+ * the file to set the flag for
+ *
+ * @return boolean
+ */
+ public static function setImmutable($filename = null)
+ {
+ \Froxlor\FileDir::safe_exec(self::getImmutableFunction(false) . escapeshellarg($filename));
+ }
+
+ /**
+ * removes the immutable flag for a file
+ *
+ * @param string $filename
+ * the file to set the flag for
+ *
+ * @return boolean
+ */
+ public static function removeImmutable($filename = null)
+ {
+ \Froxlor\FileDir::safe_exec(self::getImmutableFunction(true) . escapeshellarg($filename));
+ }
+
+ /**
+ * internal function to check whether
+ * to use chattr (Linux) or chflags (FreeBSD)
+ *
+ * @param boolean $remove
+ * whether to use +i|schg (false) or -i|noschg (true)
+ *
+ * @return string functionname + parameter (not the file)
+ */
+ private static function getImmutableFunction($remove = false)
+ {
+ if (self::isFreeBSD()) {
+ // FreeBSD style
+ return 'chflags ' . (($remove === true) ? 'noschg ' : 'schg ');
+ } else {
+ // Linux style
+ return 'chattr ' . (($remove === true) ? '-i ' : '+i ');
+ }
+ }
+
+ public static function getFilesystemQuota()
+ {
+
+ // enabled at all?
+ if (Settings::Get('system.diskquota_enabled')) {
+
+ // set linux defaults
+ $repquota_params = "-np";
+ // $quota_line_regex = "/^#([0-9]+)\s*[+-]{2}\s*(\d+)\s*(\d+)\s*(\d+)\s*(\d+)\s*(\d+)\s*(\d+)\s*(\d+)\s*(\d+)/i";
+ $quota_line_regex = "/^#([0-9]+)\s+[+-]{2}\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/i";
+
+ // check for freebsd - which needs other values
+ if (self::isFreeBSD()) {
+ $repquota_params = "-nu";
+ $quota_line_regex = "/^([0-9]+)\s+[+-]{2}\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)/i";
+ }
+
+ // Fetch all quota in the desired partition
+ $repquota = array();
+ exec(Settings::Get('system.diskquota_repquota_path') . " " . $repquota_params . " " . escapeshellarg(Settings::Get('system.diskquota_customer_partition')), $repquota);
+
+ $usedquota = array();
+ foreach ($repquota as $tmpquota) {
+ $matches = null;
+ // Let's see if the line matches a quota - line
+ if (preg_match($quota_line_regex, $tmpquota, $matches)) {
+
+ // It matches - put it into an array with userid as key (for easy lookup later)
+ $usedquota[$matches[1]] = array(
+ 'block' => array(
+ 'used' => $matches[2],
+ 'soft' => $matches[3],
+ 'hard' => $matches[4],
+ 'grace' => (self::isFreeBSD() ? '0' : $matches[5])
+ ),
+ 'file' => array(
+ 'used' => $matches[6],
+ 'soft' => $matches[7],
+ 'hard' => $matches[8],
+ 'grace' => (self::isFreeBSD() ? '0' : $matches[9])
+ )
+ );
+ }
+ }
+
+ return $usedquota;
+ }
+ return false;
+ }
+}
diff --git a/lib/Froxlor/Froxlor.php b/lib/Froxlor/Froxlor.php
new file mode 100644
index 00000000..c5d94e7d
--- /dev/null
+++ b/lib/Froxlor/Froxlor.php
@@ -0,0 +1,277 @@
+ $new_version
+ ));
+ Settings::Set('panel.db_version', $new_version);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Function updateToVersion
+ *
+ * updates the panel.version field
+ * to the given value (no checks here!)
+ *
+ * @param string $new_version
+ * new-version
+ *
+ * @return bool true on success, else false
+ */
+ public static function updateToVersion($new_version = null)
+ {
+ if ($new_version !== null && $new_version != '') {
+ $upd_stmt = Database::prepare("
+ UPDATE `" . TABLE_PANEL_SETTINGS . "` SET `value` = :newversion
+ WHERE `settinggroup` = 'panel' AND `varname` = 'version'");
+ Database::pexecute($upd_stmt, array(
+ 'newversion' => $new_version
+ ));
+ Settings::Set('panel.version', $new_version);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Function isFroxlor
+ *
+ * checks if the panel is froxlor
+ *
+ * @return bool true if panel is froxlor, else false
+ */
+ public static function isFroxlor()
+ {
+ if (Settings::Get('panel.frontend') !== null && Settings::Get('panel.frontend') == 'froxlor') {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Function isFroxlorVersion
+ *
+ * checks if a given version is the
+ * current one (and panel is froxlor)
+ *
+ * @param string $to_check
+ * version to check
+ *
+ * @return bool true if version to check matches, else false
+ */
+ public static function isFroxlorVersion($to_check = null)
+ {
+ if (Settings::Get('panel.frontend') == 'froxlor' && Settings::Get('panel.version') == $to_check) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * compare of froxlor versions
+ *
+ * @param string $a
+ * @param string $b
+ *
+ * @return integer 0 if equal, 1 if a>b and -1 if b>a
+ */
+ public static function versionCompare2($a, $b)
+ {
+
+ // split version into pieces and remove trailing .0
+ $a = explode(".", $a);
+ $b = explode(".", $b);
+
+ self::parseVersionArray($a);
+ self::parseVersionArray($b);
+
+ while (count($a) != count($b)) {
+ if (count($a) < count($b)) {
+ $a[] = '0';
+ } elseif (count($b) < count($a)) {
+ $b[] = '0';
+ }
+ }
+
+ foreach ($a as $depth => $aVal) {
+ // iterate over each piece of A
+ if (isset($b[$depth])) {
+ // if B matches A to this depth, compare the values
+ if ($aVal > $b[$depth]) {
+ return 1; // A > B
+ } elseif ($aVal < $b[$depth]) {
+ return - 1; // B > A
+ }
+ // an equal result is inconclusive at this point
+ } else {
+ // if B does not match A to this depth, then A comes after B in sort order
+ return 1; // so A > B
+ }
+ }
+ // at this point, we know that to the depth that A and B extend to, they are equivalent.
+ // either the loop ended because A is shorter than B, or both are equal.
+ return (count($a) < count($b)) ? - 1 : 0;
+ }
+
+ private static function parseVersionArray(&$arr = null)
+ {
+ // -svn or -dev or -rc ?
+ if (stripos($arr[count($arr) - 1], '-') !== false) {
+ $x = explode("-", $arr[count($arr) - 1]);
+ $arr[count($arr) - 1] = $x[0];
+ if (stripos($x[1], 'rc') !== false) {
+ $arr[] = '-1';
+ $arr[] = '2'; // rc > dev > svn
+ // number of rc
+ $arr[] = substr($x[1], 2);
+ } elseif (stripos($x[1], 'dev') !== false) {
+ $arr[] = '-1';
+ $arr[] = '1'; // svn < dev < rc
+ // number of dev
+ $arr[] = substr($x[1], 3);
+ } elseif (stripos($x[1], 'svn') !== false) {
+ // -svn version are deprecated
+ $arr[] = '-1';
+ // svn < dev < rc
+ $arr[] = '0';
+ // number of svn
+ $arr[] = substr($x[1], 3);
+ }
+ }
+ }
+}
diff --git a/lib/Froxlor/FroxlorLogger.php b/lib/Froxlor/FroxlorLogger.php
new file mode 100644
index 00000000..f44e4e15
--- /dev/null
+++ b/lib/Froxlor/FroxlorLogger.php
@@ -0,0 +1,274 @@
+initMonolog();
+ self::$userinfo = $userinfo;
+ self::$logtypes = array();
+
+ if ((Settings::Get('logger.logtypes') == null || Settings::Get('logger.logtypes') == '') && (Settings::Get('logger.enabled') !== null && Settings::Get('logger.enabled'))) {
+ self::$logtypes[0] = 'syslog';
+ self::$logtypes[1] = 'mysql';
+ } else {
+ if (Settings::Get('logger.logtypes') !== null && Settings::Get('logger.logtypes') != '') {
+ self::$logtypes = explode(',', Settings::Get('logger.logtypes'));
+ } else {
+ self::$logtypes = null;
+ }
+ }
+
+ if (self::$is_initialized == false) {
+ foreach (self::$logtypes as $logger) {
+
+ switch ($logger) {
+ case 'syslog':
+ self::$ml->pushHandler(new SyslogHandler('froxlor', LOG_USER, Logger::DEBUG));
+ break;
+ case 'file':
+ $logger_logfile = Settings::Get('logger.logfile');
+ // is_writable needs an existing file to check if it's actually writable
+ @touch($logger_logfile);
+ if (empty($logger_logfile) || ! is_writable($logger_logfile)) {
+ Settings::Set('logger.logfile', '/tmp/froxlor.log');
+ }
+ self::$ml->pushHandler(new StreamHandler($logger_logfile, Logger::DEBUG));
+ break;
+ case 'mysql':
+ self::$ml->pushHandler(new MysqlHandler(Logger::DEBUG));
+ break;
+ }
+ }
+ self::$is_initialized = true;
+ }
+ }
+
+ /**
+ * return FroxlorLogger instance
+ *
+ * @param array $userinfo
+ *
+ * @return FroxlorLogger
+ */
+ public static function getInstanceOf($userinfo = array())
+ {
+ if (empty($userinfo)) {
+ $userinfo = array(
+ 'loginname' => 'system'
+ );
+ }
+ return new FroxlorLogger($userinfo);
+ }
+
+ /**
+ * initiate monolog object
+ *
+ * @return \Monolog\Logger
+ */
+ private function initMonolog()
+ {
+ if (empty(self::$ml)) {
+ // get Theme object
+ self::$ml = new Logger('froxlor');
+ }
+ return self::$ml;
+ }
+
+ /**
+ * logs a given text to all enabled logger-facilities
+ *
+ * @param int $action
+ * @param int $type
+ * @param string $text
+ */
+ public function logAction($action = \Froxlor\FroxlorLogger::USR_ACTION, $type = LOG_NOTICE, $text = null)
+ {
+ // not logging normal stuff if not set to "paranoid" logging
+ if (! self::$crondebug_flag && Settings::Get('logger.severity') == '1' && $type > LOG_NOTICE) {
+ return;
+ }
+
+ if (empty(self::$ml)) {
+ $this->initMonolog();
+ }
+
+ if (self::$crondebug_flag || ($action == \Froxlor\FroxlorLogger::CRON_ACTION && $type <= LOG_WARNING)) {
+ echo "[" . $this->getLogLevelDesc($type) . "] " . $text . PHP_EOL;
+ }
+
+ // warnings, errors and critical mesages WILL be logged
+ if (Settings::Get('logger.log_cron') == '0' && $action == \Froxlor\FroxlorLogger::CRON_ACTION && $type > LOG_WARNING) {
+ return;
+ }
+
+ $logExtra = array(
+ 'source' => $this->getActionTypeDesc($action),
+ 'action' => $action,
+ 'user' => self::$userinfo['loginname']
+ );
+
+ switch ($type) {
+ case LOG_DEBUG:
+ self::$ml->addDebug($text, $logExtra);
+ break;
+ case LOG_INFO:
+ self::$ml->addInfo($text, $logExtra);
+ break;
+ case LOG_NOTICE:
+ self::$ml->addNotice($text, $logExtra);
+ break;
+ case LOG_WARNING:
+ self::$ml->addWarning($text, $logExtra);
+ break;
+ case LOG_ERR:
+ self::$ml->addError($text, $logExtra);
+ break;
+ default:
+ self::$ml->addDebug($text, $logExtra);
+ }
+ }
+
+ /**
+ * Set whether to log cron-runs
+ *
+ * @param bool $_cronlog
+ *
+ * @return boolean
+ */
+ public function setCronLog($_cronlog = 0)
+ {
+ $_cronlog = (int) $_cronlog;
+
+ if ($_cronlog < 0 || $_cronlog > 2) {
+ $_cronlog = 0;
+ }
+ Settings::Set('logger.log_cron', $_cronlog);
+ return $_cronlog;
+ }
+
+ /**
+ * setter for crondebug-flag
+ *
+ * @param bool $_flag
+ *
+ * @return void
+ */
+ public function setCronDebugFlag($_flag = false)
+ {
+ self::$crondebug_flag = (bool) $_flag;
+ }
+
+ public function getLogLevelDesc($type)
+ {
+ switch ($type) {
+ case LOG_INFO:
+ $_type = 'information';
+ break;
+ case LOG_NOTICE:
+ $_type = 'notice';
+ break;
+ case LOG_WARNING:
+ $_type = 'warning';
+ break;
+ case LOG_ERR:
+ $_type = 'error';
+ break;
+ case LOG_CRIT:
+ $_type = 'critical';
+ break;
+ case LOG_DEBUG:
+ $_type = 'debug';
+ break;
+ default:
+ $_type = 'unknown';
+ break;
+ }
+ return $_type;
+ }
+
+ private function getActionTypeDesc($action)
+ {
+ switch ($action) {
+ case \Froxlor\FroxlorLogger::USR_ACTION:
+ $_action = 'user';
+ break;
+ case \Froxlor\FroxlorLogger::ADM_ACTION:
+ $_action = 'admin';
+ break;
+ case \Froxlor\FroxlorLogger::RES_ACTION:
+ $_action = 'reseller';
+ break;
+ case \Froxlor\FroxlorLogger::CRON_ACTION:
+ $_action = 'cron';
+ break;
+ case \Froxlor\FroxlorLogger::LOGIN_ACTION:
+ $_action = 'login';
+ break;
+ default:
+ $_action = 'unknown';
+ break;
+ }
+ return $_action;
+ }
+}
diff --git a/lib/functions/formfields/file/function.validateFormFieldFile.php b/lib/Froxlor/FroxlorTwoFactorAuth.php
similarity index 56%
rename from lib/functions/formfields/file/function.validateFormFieldFile.php
rename to lib/Froxlor/FroxlorTwoFactorAuth.php
index 3680ae9d..7a0f5d01 100644
--- a/lib/functions/formfields/file/function.validateFormFieldFile.php
+++ b/lib/Froxlor/FroxlorTwoFactorAuth.php
@@ -1,4 +1,5 @@
(2010-)
- * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
- * @package Functions
- *
+ * @copyright (c) the authors
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package API
+ * @since 0.10.0
+ *
*/
-
-function validateFormFieldFile($fieldname, $fielddata, $newfieldvalue)
+class FroxlorTwoFactorAuth extends \RobThree\Auth\TwoFactorAuth
{
- return true;
}
diff --git a/lib/classes/io/class.frxDirectory.php b/lib/Froxlor/Http/Directory.php
similarity index 54%
rename from lib/classes/io/class.frxDirectory.php
rename to lib/Froxlor/Http/Directory.php
index 730dd9da..ce7327c8 100644
--- a/lib/classes/io/class.frxDirectory.php
+++ b/lib/Froxlor/Http/Directory.php
@@ -1,4 +1,5 @@
- * @author Froxlor team (2010-)
- * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
- * @package Cron
- *
- * @since 0.9.33
- *
+ * @copyright (c) the authors
+ * @author Michael Kaufmann
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ * @since 0.9.33
+ *
*/
+use Froxlor\Database\Database;
+use Froxlor\FileDir;
/**
* Class frxDirectory handles directory actions and gives information
* about a given directory in connections with its usage in froxlor
*
* @author Michael Kaufmann (d00p)
- *
+ *
*/
-class frxDirectory {
+class Directory
+{
/**
* directory string
*
* @var string
*/
- private $_dir = null;
+ private $dir = null;
/**
* class constructor, optionally set directory
*
* @param string $dir
*/
- public function __construct($dir = null) {
- $this->_dir = $dir;
+ public function __construct($dir = null)
+ {
+ $this->dir = $dir;
}
/**
* check whether the directory has options set in panel_htaccess
*/
- public function hasUserOptions() {
+ public function hasUserOptions()
+ {
$uo_stmt = Database::prepare("
- SELECT COUNT(`id`) as `usropts` FROM `".TABLE_PANEL_HTACCESS."` WHERE `path` = :dir
+ SELECT COUNT(`id`) as `usropts` FROM `" . TABLE_PANEL_HTACCESS . "` WHERE `path` = :dir
");
- $uo_res = Database::pexecute_first($uo_stmt, array('dir' => makeCorrectDir($this->_dir)));
+ $uo_res = Database::pexecute_first($uo_stmt, array(
+ 'dir' => FileDir::makeCorrectDir($this->dir)
+ ));
if ($uo_res != false && isset($uo_res['usropts'])) {
return ($uo_res['usropts'] > 0 ? true : false);
}
@@ -60,11 +68,14 @@ class frxDirectory {
/**
* check whether the directory is protected using panel_htpasswd
*/
- public function isUserProtected() {
+ public function isUserProtected()
+ {
$up_stmt = Database::prepare("
- SELECT COUNT(`id`) as `usrprot` FROM `".TABLE_PANEL_HTPASSWDS."` WHERE `path` = :dir
+ SELECT COUNT(`id`) as `usrprot` FROM `" . TABLE_PANEL_HTPASSWDS . "` WHERE `path` = :dir
");
- $up_res = Database::pexecute_first($up_stmt, array('dir' => makeCorrectDir($this->_dir)));
+ $up_res = Database::pexecute_first($up_stmt, array(
+ 'dir' => FileDir::makeCorrectDir($this->dir)
+ ));
if ($up_res != false && isset($up_res['usrprot'])) {
return ($up_res['usrprot'] > 0 ? true : false);
}
@@ -75,26 +86,27 @@ class frxDirectory {
* Checks if a given directory is valid for multiple configurations
* or should rather be used as a single file
*
- * @param bool $ifexists also check whether file/dir exists
- *
+ * @param bool $ifexists
+ * also check whether file/dir exists
+ *
* @return bool true if usable as dir, false otherwise
*/
- public function isConfigDir($ifexists = false) {
-
- if (is_null($this->_dir)) {
- trigger_error(__CLASS__.'::'.__FUNCTION__.' has been called with a null value', E_USER_WARNING);
+ public function isConfigDir($ifexists = false)
+ {
+ if (is_null($this->dir)) {
+ trigger_error(__CLASS__ . '::' . __FUNCTION__ . ' has been called with a null value', E_USER_WARNING);
return false;
}
- if (file_exists($this->_dir)) {
- if (is_dir($this->_dir)) {
+ if (file_exists($this->dir)) {
+ if (is_dir($this->dir)) {
$returnval = true;
} else {
$returnval = false;
}
} else {
- if (!$ifexists) {
- if (substr($this->_dir, -1) == '/') {
+ if (! $ifexists) {
+ if (substr($this->dir, - 1) == '/') {
$returnval = true;
} else {
$returnval = false;
@@ -105,5 +117,4 @@ class frxDirectory {
}
return $returnval;
}
-
}
diff --git a/lib/Froxlor/Http/HttpClient.php b/lib/Froxlor/Http/HttpClient.php
new file mode 100644
index 00000000..5b0e764a
--- /dev/null
+++ b/lib/Froxlor/Http/HttpClient.php
@@ -0,0 +1,62 @@
+fetch(\PDO::FETCH_ASSOC)) {
+ if (! isset($configs_array[$row['id']]) && ! in_array($row['id'], $configs_array)) {
+ $configs_array[$row['id']] = html_entity_decode($row['description']);
+ }
+ }
+ }
+ return $configs_array;
+ }
+}
diff --git a/lib/Froxlor/Http/Statistics.php b/lib/Froxlor/Http/Statistics.php
new file mode 100644
index 00000000..85b63e19
--- /dev/null
+++ b/lib/Froxlor/Http/Statistics.php
@@ -0,0 +1,140 @@
+ (2003-2009)
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Classes
+ *
+ */
+
+/**
+ * Class for wrapping a specific idna conversion class and offering a standard interface
+ *
+ * @package Functions
+ */
+class IdnaWrapper
+{
+
+ /**
+ * idna converter we use
+ *
+ * @var object
+ */
+ private $idna_converter;
+
+ /**
+ * Class constructor.
+ * Creates a new idna converter
+ */
+ public function __construct()
+ {
+ // Instantiate it
+ $this->idna_converter = new \Algo26\IdnaConvert\IdnaConvert();
+ }
+
+ /**
+ * Encode a domain name, a email address or a list of one of both.
+ *
+ * @param
+ * string May be either a single domain name, e single email address or a list of one
+ * separated either by ',', ';' or ' '.
+ *
+ * @return string Returns either a single domain name, a single email address or a list of one of
+ * both separated by the same string as the input.
+ */
+ public function encode($to_encode)
+ {
+ $to_encode = $this->isUtf8($to_encode) ? $to_encode : utf8_encode($to_encode);
+ try {
+ return $this->idna_converter->encode($to_encode);
+ } catch (\InvalidArgumentException $iae) {
+ if ($iae->getCode() == 100) {
+ return $to_encode;
+ }
+ throw $iae;
+ }
+ }
+
+ /**
+ * Decode a domain name, a email address or a list of one of both.
+ *
+ * @param
+ * string May be either a single domain name, e single email address or a list of one
+ * separated either by ',', ';' or ' '.
+ *
+ * @return string Returns either a single domain name, a single email address or a list of one of
+ * both separated by the same string as the input.
+ */
+ public function decode($to_decode)
+ {
+ return $this->idna_converter->decode($to_decode);
+ }
+
+ /**
+ * check whether a string is utf-8 encoded or not
+ *
+ * @param string $string
+ *
+ * @return boolean
+ */
+ private function isUtf8($string = null)
+ {
+ if (function_exists("mb_detect_encoding")) {
+ if (mb_detect_encoding($string, 'UTF-8, ISO-8859-1') === 'UTF-8') {
+ return true;
+ }
+ return false;
+ }
+ $strlen = strlen($string);
+ for ($i = 0; $i < $strlen; $i ++) {
+ $ord = ord($string[$i]);
+ if ($ord < 0x80) {
+ continue; // 0bbbbbbb
+ } elseif (($ord & 0xE0) === 0xC0 && $ord > 0xC1) {
+ $n = 1; // 110bbbbb (exkl C0-C1)
+ } elseif (($ord & 0xF0) === 0xE0) {
+ $n = 2; // 1110bbbb
+ } elseif (($ord & 0xF8) === 0xF0 && $ord < 0xF5) {
+ $n = 3; // 11110bbb (exkl F5-FF)
+ } else {
+ // ungültiges UTF-8-Zeichen
+ return false;
+ }
+ // $n Folgebytes? // 10bbbbbb
+ for ($c = 0; $c < $n; $c ++) {
+ if (++ $i === $strlen || (ord($string[$i]) & 0xC0) !== 0x80) {
+ // ungültiges UTF-8-Zeichen
+ return false;
+ }
+ }
+ }
+ // kein ungültiges UTF-8-Zeichen gefunden
+ return true;
+ }
+}
diff --git a/lib/classes/mail/class.mailLogParser.php b/lib/Froxlor/MailLogParser.php
similarity index 53%
rename from lib/classes/mail/class.mailLogParser.php
rename to lib/Froxlor/MailLogParser.php
index aebddd64..4a9ef696 100644
--- a/lib/classes/mail/class.mailLogParser.php
+++ b/lib/Froxlor/MailLogParser.php
@@ -1,4 +1,7 @@
- * @author Froxlor team (2010-)
- * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
- * @package Cron
- *
- * @since 0.9.32
- *
+ * @copyright (c) the authors
+ * @author Roman Schmerold
+ * @author Froxlor team (2010-)
+ * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
+ * @package Cron
+ *
+ * @since 0.9.32
+ *
*/
-
-class MailLogParser {
+class MailLogParser
+{
private $startTime;
+
private $domainTraffic = array();
+
private $myDomains = array();
+
private $mails = array();
/**
* constructor
- * @param string logFile
- * @param int startTime
- * @param string logFileExim
+ *
+ * @param
+ * string logFile
+ * @param
+ * int startTime
+ * @param
+ * string logFileExim
*/
- public function __construct($startTime = 0) {
+ public function __construct($startTime = 0)
+ {
$this->startTime = $startTime;
// Get all domains from Database
$stmt = Database::prepare("SELECT domain FROM `" . TABLE_PANEL_DOMAINS . "`");
Database::pexecute($stmt, array());
- while ($domain_row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+ while ($domain_row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$this->myDomains[] = $domain_row["domain"];
}
// Parse MTA traffic
if (Settings::Get("system.mtaserver") == "postfix") {
- $this->_parsePostfixLog(Settings::Get("system.mtalog"));
- $this->_parsePostfixLog(Settings::Get("system.mtalog") . ".1");
+ $this->parsePostfixLog(Settings::Get("system.mtalog"));
+ $this->parsePostfixLog(Settings::Get("system.mtalog") . ".1");
} elseif (Settings::Get("system.mtaserver") == "exim4") {
- $this->_parseExim4Log(Settings::Get("system.mtalog"));
+ $this->parseExim4Log(Settings::Get("system.mtalog"));
}
// Parse MDA traffic
if (Settings::Get("system.mdaserver") == "dovecot") {
- $this->_parseDovecotLog(Settings::Get("system.mdalog"));
- $this->_parsePostfixLog(Settings::Get("system.mdalog") . ".1");
+ $this->parseDovecotLog(Settings::Get("system.mdalog"));
+ $this->parsePostfixLog(Settings::Get("system.mdalog") . ".1");
} elseif (Settings::Get("system.mdaserver") == "courier") {
- $this->_parseCourierLog(Settings::Get("system.mdalog"));
- $this->_parsePostfixLog(Settings::Get("system.mdalog") . ".1");
+ $this->parseCourierLog(Settings::Get("system.mdalog"));
+ $this->parsePostfixLog(Settings::Get("system.mdalog") . ".1");
}
}
-
/**
* parsePostfixLog
* parses the traffic from a postfix logfile
- * @param logFile
+ *
+ * @param string $logFile
+ * logFile
*/
- private function _parsePostfixLog($logFile) {
+ private function parsePostfixLog($logFile)
+ {
// Check if file exists
- if (!file_exists($logFile)) {
+ if (! file_exists($logFile)) {
return false;
}
// Open the log file
try {
$file_handle = fopen($logFile, "r");
- if (!$file_handle) {
- throw new Exception("Could not open the file!");
+ if (! $file_handle) {
+ throw new \Exception("Could not open the file!");
}
- } catch (Exception $e) {
- echo "Error (File: ".$e->getFile().", line ". $e->getLine()."): ".$e->getMessage();
+ } catch (\Exception $e) {
+ echo "Error (File: " . $e->getFile() . ", line " . $e->getLine() . "): " . $e->getMessage();
return false;
}
- while (!feof($file_handle)) {
+ while (! feof($file_handle)) {
unset($matches);
$line = fgets($file_handle);
- $timestamp = $this->_getLogTimestamp($line);
+ $timestamp = $this->getLogTimestamp($line);
if ($this->startTime < $timestamp) {
- if (preg_match("/postfix\/qmgr.*(?::|\])\s([A-Z\d]+).*from=(?:.*\@([a-z\A-Z\d\.\-]+))?>?, size=(\d+),/", $line, $matches)) {
+ if (preg_match("/postfix\/qmgr.*(?::|\])\s([A-Z\d]+).*from=(?:.*\@([a-zA-Z\d\.\-]+))?>?, size=(\d+),/", $line, $matches)) {
// Postfix from
- $this->mails[$matches[1]] = array("domainFrom" => strtolower($matches[2]), "size" => $matches[3]);
- } elseif (preg_match("/postfix\/(?:pipe|smtp).*(?::|\])\s([A-Z\d]+).*to=(?:.*\@([a-z\A-Z\d\.\-]+))?>?,/", $line, $matches)) {
+ $this->mails[$matches[1]] = array(
+ "domainFrom" => strtolower($matches[2]),
+ "size" => $matches[3]
+ );
+ } elseif (preg_match("/postfix\/(?:pipe|smtp).*(?::|\])\s([A-Z\d]+).*to=(?:.*\@([a-zA-Z\d\.\-]+))?>?,/", $line, $matches)) {
// Postfix to
if (array_key_exists($matches[1], $this->mails)) {
$this->mails[$matches[1]]["domainTo"] = strtolower($matches[2]);
@@ -101,12 +117,12 @@ class MailLogParser {
if (in_array($mail["domainFrom"], $this->myDomains) || in_array($mail["domainTo"], $this->myDomains)) {
// Outgoing traffic
if (array_key_exists("domainFrom", $mail)) {
- $this->_addDomainTraffic($mail["domainFrom"], $mail["size"], $timestamp);
+ $this->addDomainTraffic($mail["domainFrom"], $mail["size"], $timestamp);
}
// Incoming traffic
if (array_key_exists("domainTo", $mail) && in_array($mail["domainTo"], $this->myDomains)) {
- $this->_addDomainTraffic($mail["domainTo"], $mail["size"], $timestamp);
+ $this->addDomainTraffic($mail["domainTo"], $mail["size"], $timestamp);
}
}
unset($mail);
@@ -118,41 +134,43 @@ class MailLogParser {
return true;
}
-
/**
* parseExim4Log
* parses the smtp traffic from a exim4 logfile
- * @param logFile
+ *
+ * @param string $logFile
+ * logFile
*/
- private function _parseExim4Log($logFile) {
+ private function parseExim4Log($logFile)
+ {
// Check if file exists
- if (!file_exists($logFile)) {
+ if (! file_exists($logFile)) {
return false;
}
// Open the log file
try {
$file_handle = fopen($logFile, "r");
- if (!$file_handle) {
- throw new Exception("Could not open the file!");
+ if (! $file_handle) {
+ throw new \Exception("Could not open the file!");
}
- } catch (Exception $e) {
- echo "Error (File: ".$e->getFile().", line ". $e->getLine()."): ".$e->getMessage();
+ } catch (\Exception $e) {
+ echo "Error (File: " . $e->getFile() . ", line " . $e->getLine() . "): " . $e->getMessage();
return false;
}
- while (!feof($file_handle)) {
+ while (! feof($file_handle)) {
unset($matches);
$line = fgets($file_handle);
- $timestamp = $this->_getLogTimestamp($line);
+ $timestamp = $this->getLogTimestamp($line);
if ($this->startTime < $timestamp) {
if (preg_match("/<= .*@([a-z0-9.\-]+) .*S=(\d+)/i", $line, $matches)) {
// Outgoing traffic
- $this->_addDomainTraffic($matches[1], $matches[2], $timestamp);
- } elseif (preg_match("/=> .*.*@([a-z0-9.\-]+)>? .*S=(\d+)/i" , $line, $matches)) {
+ $this->addDomainTraffic($matches[1], $matches[2], $timestamp);
+ } elseif (preg_match("/=> .*.*@([a-z0-9.\-]+)>? .*S=(\d+)/i", $line, $matches)) {
// Incoming traffic
- $this->_addDomainTraffic($matches[1], $matches[2], $timestamp);
+ $this->addDomainTraffic($matches[1], $matches[2], $timestamp);
}
}
}
@@ -160,41 +178,43 @@ class MailLogParser {
return true;
}
-
/**
* parseDovecotLog
* parses the dovecot imap/pop3 traffic from logfile
- * @param logFile
+ *
+ * @param string $logFile
+ * logFile
*/
- private function _parseDovecotLog($logFile) {
+ private function parseDovecotLog($logFile)
+ {
// Check if file exists
- if (!file_exists($logFile)) {
+ if (! file_exists($logFile)) {
return false;
}
// Open the log file
try {
$file_handle = fopen($logFile, "r");
- if (!$file_handle) {
- throw new Exception("Could not open the file!");
+ if (! $file_handle) {
+ throw new \Exception("Could not open the file!");
}
- } catch (Exception $e) {
- echo "Error (File: ".$e->getFile().", line ". $e->getLine()."): ".$e->getMessage();
+ } catch (\Exception $e) {
+ echo "Error (File: " . $e->getFile() . ", line " . $e->getLine() . "): " . $e->getMessage();
return false;
}
- while (!feof($file_handle)) {
+ while (! feof($file_handle)) {
unset($matches);
$line = fgets($file_handle);
- $timestamp = $this->_getLogTimestamp($line);
+ $timestamp = $this->getLogTimestamp($line);
if ($this->startTime < $timestamp) {
- if (preg_match("/dovecot.*(?::|\]) imap\(.*@([a-z0-9\.\-]+)\):.*(?:in=(\d+) out=(\d+)|bytes=(\d+)\/(\d+))/i", $line, $matches)) {
+ if (preg_match("/dovecot.*(?::|\]) imap\(.*@([a-z0-9\.\-]+)\)(<\d+><[a-z0-9+\/=]+>)?:.*(?:in=(\d+) out=(\d+)|bytes=(\d+)\/(\d+))/i", $line, $matches)) {
// Dovecot IMAP
- $this->_addDomainTraffic($matches[1], (int)$matches[2] + (int)$matches[3], $timestamp);
- } elseif (preg_match("/dovecot.*(?::|\]) pop3\(.*@([a-z0-9\.\-]+)\):.*in=(\d+).*out=(\d+)/i", $line, $matches)) {
+ $this->addDomainTraffic($matches[1], (int) $matches[3] + (int) $matches[4], $timestamp);
+ } elseif (preg_match("/dovecot.*(?::|\]) pop3\(.*@([a-z0-9\.\-]+)\)(<\d+><[a-z0-9+\/=]+>)?:.*in=(\d+).*out=(\d+)/i", $line, $matches)) {
// Dovecot POP3
- $this->_addDomainTraffic($matches[1], (int)$matches[2] + (int)$matches[3], $timestamp);
+ $this->addDomainTraffic($matches[1], (int) $matches[3] + (int) $matches[4], $timestamp);
}
}
}
@@ -202,38 +222,40 @@ class MailLogParser {
return true;
}
-
/**
* parseCourierLog
* parses the dovecot imap/pop3 traffic from logfile
- * @param logFile
+ *
+ * @param string $logFile
+ * logFile
*/
- private function _parseCourierLog($logFile) {
+ private function parseCourierLog($logFile)
+ {
// Check if file exists
- if (!file_exists($logFile)) {
+ if (! file_exists($logFile)) {
return false;
}
// Open the log file
try {
$file_handle = fopen($logFile, "r");
- if (!$file_handle) {
- throw new Exception("Could not open the file!");
+ if (! $file_handle) {
+ throw new \Exception("Could not open the file!");
}
- } catch (Exception $e) {
- echo "Error (File: ".$e->getFile().", line ". $e->getLine()."): ".$e->getMessage();
+ } catch (\Exception $e) {
+ echo "Error (File: " . $e->getFile() . ", line " . $e->getLine() . "): " . $e->getMessage();
return false;
}
- while (!feof($file_handle)) {
+ while (! feof($file_handle)) {
unset($matches);
$line = fgets($file_handle);
- $timestamp = $this->_getLogTimestamp($line);
+ $timestamp = $this->getLogTimestamp($line);
if ($this->startTime < $timestamp) {
if (preg_match("/(?:imapd|pop3d)(?:-ssl)?.*(?::|\]).*user=.*@([a-z0-9\.\-]+),.*rcvd=(\d+), sent=(\d+),/i", $line, $matches)) {
// Courier IMAP & POP3
- $this->_addDomainTraffic($matches[1], (int)$matches[2] + (int)$matches[3], $timestamp);
+ $this->addDomainTraffic($matches[1], (int) $matches[2] + (int) $matches[3], $timestamp);
}
}
}
@@ -241,34 +263,40 @@ class MailLogParser {
return true;
}
-
/**
* _addDomainTraffic
* adds the traffic to the domain array if we own the domain
- * @param string domain
- * @param int traffic
+ *
+ * @param
+ * string domain
+ * @param
+ * int traffic
*/
- private function _addDomainTraffic($domain, $traffic, $timestamp) {
+ private function addDomainTraffic($domain, $traffic, $timestamp)
+ {
$date = date("Y-m-d", $timestamp);
if (in_array($domain, $this->myDomains)) {
if (array_key_exists($domain, $this->domainTraffic) && array_key_exists($date, $this->domainTraffic[$domain])) {
- $this->domainTraffic[$domain][$date] += (int)$traffic;
+ $this->domainTraffic[$domain][$date] += (int) $traffic;
} else {
- if (!array_key_exists($domain, $this->domainTraffic)) {
+ if (! array_key_exists($domain, $this->domainTraffic)) {
$this->domainTraffic[$domain] = array();
}
- $this->domainTraffic[$domain][$date] = (int)$traffic;
+ $this->domainTraffic[$domain][$date] = (int) $traffic;
}
}
}
-
/**
* getLogTimestamp
- * @param string line
- * return int
+ *
+ * @param
+ * string line
+ * return int
*/
- private function _getLogTimestamp($line) {
+ private function getLogTimestamp($line)
+ {
+ $matches = null;
if (preg_match("/((?:[A-Z]{3}\s{1,2}\d{1,2}|\d{4}-\d{2}-\d{2}) \d{2}:\d{2}:\d{2})/i", $line, $matches)) {
$timestamp = strtotime($matches[1]);
if ($timestamp > ($this->startTime + 60 * 60 * 24)) {
@@ -281,20 +309,20 @@ class MailLogParser {
}
}
-
/**
* getDomainTraffic
* returns the traffic of a given domain or 0 if the domain has no traffic
- * @param string domain
- * return array
+ *
+ * @param
+ * string domain
+ * return array
*/
- public function getDomainTraffic($domain) {
+ public function getDomainTraffic($domain)
+ {
if (array_key_exists($domain, $this->domainTraffic)) {
return $this->domainTraffic[$domain];
} else {
return 0;
}
}
-
-
}
diff --git a/lib/Froxlor/PhpHelper.php b/lib/Froxlor/PhpHelper.php
new file mode 100644
index 00000000..375ac131
--- /dev/null
+++ b/lib/Froxlor/PhpHelper.php
@@ -0,0 +1,410 @@
+
+ */
+ public static function htmlentitiesArray($subject, $fields = '', $quote_style = ENT_QUOTES, $charset = 'UTF-8')
+ {
+ if (is_array($subject)) {
+ if (! is_array($fields)) {
+ $fields = self::arrayTrim(explode(' ', $fields));
+ }
+
+ foreach ($subject as $field => $value) {
+ if ((! is_array($fields) || empty($fields)) || (is_array($fields) && ! empty($fields) && in_array($field, $fields))) {
+ // Just call ourselve to manage multi-dimensional arrays
+ $subject[$field] = self::htmlentitiesArray($subject[$field], $fields, $quote_style, $charset);
+ }
+ }
+ } else {
+ $subject = htmlentities($subject, $quote_style, $charset);
+ }
+
+ return $subject;
+ }
+
+ /**
+ * Replaces Strings in an array, with the advantage that you
+ * can select which fields should be str_replace'd
+ *
+ * @param
+ * mixed String or array of strings to search for
+ * @param
+ * mixed String or array to replace with
+ * @param
+ * array The subject array
+ * @param
+ * string The fields which should be checked for, separated by spaces
+ * @return array The str_replace'd array
+ * @author Florian Lippert
+ */
+ public static function strReplaceArray($search, $replace, $subject, $fields = '')
+ {
+ if (is_array($subject)) {
+ $fields = self::arrayTrim(explode(' ', $fields));
+ foreach ($subject as $field => $value) {
+ if ((! is_array($fields) || empty($fields)) || (is_array($fields) && ! empty($fields) && in_array($field, $fields))) {
+ $subject[$field] = str_replace($search, $replace, $subject[$field]);
+ }
+ }
+ } else {
+ $subject = str_replace($search, $replace, $subject);
+ }
+
+ return $subject;
+ }
+
+ /**
+ * froxlor php error handler
+ *
+ * @param int $errno
+ * @param string $errstr
+ * @param string $errfile
+ * @param int $errline
+ * @param array $errcontext
+ *
+ * @return void|boolean
+ */
+ public static function phpErrHandler($errno, $errstr, $errfile, $errline, $errcontext = array())
+ {
+ if (! (error_reporting() & $errno)) {
+ // This error code is not included in error_reporting
+ return;
+ }
+
+ if (! isset($_SERVER['SHELL']) || (isset($_SERVER['SHELL']) && $_SERVER['SHELL'] == '')) {
+ global $theme;
+
+ // fallback
+ if (empty($theme)) {
+ $theme = "Sparkle";
+ }
+ // prevent possible file-path-disclosure
+ $errfile = str_replace(\Froxlor\Froxlor::getInstallDir(), "", $errfile);
+ // if we're not on the shell, output a nicer error-message
+ $err_hint = file_get_contents(\Froxlor\Froxlor::getInstallDir() . '/templates/' . $theme . '/misc/phperrornice.tpl');
+ // replace values
+ $err_hint = str_replace("", '#' . $errno . ' ' . $errstr, $err_hint);
+ $err_hint = str_replace("", $errfile . ':' . $errline, $err_hint);
+
+ // show
+ echo $err_hint;
+ // return true to ignore php standard error-handler
+ return true;
+ }
+
+ // of on shell, use the php standard error-handler
+ return false;
+ }
+
+ public static function loadConfigArrayDir()
+ {
+ // Workaround until we use gettext
+ global $lng, $theme;
+
+ // we now use dynamic function parameters
+ // so we can read from more than one directory
+ // and still be valid for old calls
+ $numargs = func_num_args();
+ if ($numargs <= 0) {
+ return null;
+ }
+
+ // variable that holds all dirs that will
+ // be parsed for inclusion
+ $configdirs = array();
+ // if one of the parameters is an array
+ // we assume that this is a list of
+ // setting-groups to be selected
+ $selection = null;
+ for ($x = 0; $x < $numargs; $x ++) {
+ $arg = func_get_arg($x);
+ if (is_array($arg) && isset($arg[0])) {
+ $selection = $arg;
+ } else {
+ $configdirs[] = $arg;
+ }
+ }
+
+ $data = array();
+ $data_files = array();
+ $has_data = false;
+
+ foreach ($configdirs as $data_dirname) {
+ if (is_dir($data_dirname)) {
+ $data_dirhandle = opendir($data_dirname);
+ while (false !== ($data_filename = readdir($data_dirhandle))) {
+ if ($data_filename != '.' && $data_filename != '..' && $data_filename != '' && substr($data_filename, - 4) == '.php') {
+ $data_files[] = $data_dirname . $data_filename;
+ }
+ }
+ $has_data = true;
+ }
+ }
+
+ if ($has_data) {
+ sort($data_files);
+ foreach ($data_files as $data_filename) {
+ $data = array_merge_recursive($data, include $data_filename);
+ }
+ }
+
+ // if we have specific setting-groups
+ // to select, we'll handle this here
+ // (this is for multiserver-client settings)
+ $_data = array();
+ if ($selection != null && is_array($selection) && isset($selection[0])) {
+ $_data['groups'] = array();
+ foreach ($data['groups'] as $group => $data) {
+ if (in_array($group, $selection)) {
+ $_data['groups'][$group] = $data;
+ }
+ }
+ $data = $_data;
+ }
+
+ return $data;
+ }
+
+ /**
+ * ipv6 aware gethostbynamel function
+ *
+ * @param string $host
+ * @param boolean $try_a
+ * default true
+ * @return boolean|array
+ */
+ public static function gethostbynamel6($host, $try_a = true)
+ {
+ $dns6 = @dns_get_record($host, DNS_AAAA);
+ if (!is_array($dns6)) {
+ // no record or failed to check
+ $dns6 = [];
+ }
+ if ($try_a == true) {
+ $dns4 = @dns_get_record($host, DNS_A);
+ if (!is_array($dns4)) {
+ // no record or failed to check
+ $dns4 = [];
+ }
+ $dns = array_merge($dns4, $dns6);
+ } else {
+ $dns = $dns6;
+ }
+ $ips = array();
+ foreach ($dns as $record) {
+ if ($record["type"] == "A") {
+ $ips[] = $record["ip"];
+ }
+ if ($record["type"] == "AAAA") {
+ $ips[] = $record["ipv6"];
+ }
+ }
+ if (count($ips) < 1) {
+ return false;
+ } else {
+ return $ips;
+ }
+ }
+
+ /**
+ * Function randomStr
+ *
+ * generate a pseudo-random string of bytes
+ *
+ * @param int $length
+ *
+ * @return string
+ */
+ public static function randomStr($length)
+ {
+ if (version_compare(PHP_VERSION, '7.0.0') >= 0) {
+ return random_bytes($length);
+ } elseif (function_exists('openssl_random_pseudo_bytes')) {
+ return openssl_random_pseudo_bytes($length);
+ } else {
+ $pr_bits = '';
+ $fp = @fopen('/dev/urandom', 'rb');
+ if ($fp !== false) {
+ $pr_bits .= @fread($fp, $length);
+ @fclose($fp);
+ } else {
+ $pr_bits = substr(rand(time(), getrandmax()) . rand(time(), getrandmax()), 0, $length);
+ }
+ return $pr_bits;
+ }
+ }
+
+ /**
+ * Return human readable sizes
+ *
+ * @param int $size
+ * size in bytes
+ * @param string $max
+ * maximum unit
+ * @param string $system
+ * 'si' for SI, 'bi' for binary prefixes
+ *
+ * @param
+ * string
+ */
+ public static function sizeReadable($size, $max = null, $system = 'si', $retstring = '%01.2f %s')
+ {
+ // Pick units
+ $systems = array(
+ 'si' => array(
+ 'prefix' => array(
+ 'B',
+ 'KB',
+ 'MB',
+ 'GB',
+ 'TB',
+ 'PB'
+ ),
+ 'size' => 1000
+ ),
+ 'bi' => array(
+ 'prefix' => array(
+ 'B',
+ 'KiB',
+ 'MiB',
+ 'GiB',
+ 'TiB',
+ 'PiB'
+ ),
+ 'size' => 1024
+ )
+ );
+ $sys = isset($systems[$system]) ? $systems[$system] : $systems['si'];
+
+ // Max unit to display
+ $depth = count($sys['prefix']) - 1;
+ if ($max && false !== $d = array_search($max, $sys['prefix'])) {
+ $depth = $d;
+ }
+ // Loop
+ $i = 0;
+ while ($size >= $sys['size'] && $i < $depth) {
+ $size /= $sys['size'];
+ $i ++;
+ }
+
+ return sprintf($retstring, $size, $sys['prefix'][$i]);
+ }
+
+ /**
+ * Replaces all occurrences of variables defined in the second argument
+ * in the first argument with their values.
+ *
+ * @param string $text
+ * The string that should be searched for variables
+ * @param array $vars
+ * The array containing the variables with their values
+ *
+ * @return string The submitted string with the variables replaced.
+ */
+ public static function replaceVariables($text, $vars)
+ {
+ $pattern = "/\{([a-zA-Z0-9\-_]+)\}/";
+ $matches = array();
+
+ if (count($vars) > 0 && preg_match_all($pattern, $text, $matches)) {
+ for ($i = 0; $i < count($matches[1]); $i ++) {
+ $current = $matches[1][$i];
+
+ if (isset($vars[$current])) {
+ $var = $vars[$current];
+ $text = str_replace("{" . $current . "}", $var, $text);
+ }
+ }
+ }
+
+ $text = str_replace('\n', "\n", $text);
+ return $text;
+ }
+
+ /**
+ * Returns array with all empty-values removed
+ *
+ * @param array $source
+ * The array to trim
+ * @return array The trim'med array
+ */
+ public static function arrayTrim($source)
+ {
+ $returnval = array();
+ if (is_array($source)) {
+ $source = array_map('trim', $source);
+ $returnval = array_filter($source, function ($value) {
+ return $value !== '';
+ });
+ } else {
+ $returnval = $source;
+ }
+ return $returnval;
+ }
+
+ /**
+ * function to check a super-global passed by reference
+ * so it gets automatically updated
+ *
+ * @param array $global
+ * @param \voku\helper\AntiXSS $antiXss
+ */
+ public static function cleanGlobal(&$global, &$antiXss)
+ {
+ if (isset($global) && ! empty($global)) {
+ $tmp = $global;
+ foreach ($tmp as $index => $value) {
+ $global[$index] = $antiXss->xss_clean($value);
+ }
+ }
+ }
+}
diff --git a/lib/Froxlor/SImExporter.php b/lib/Froxlor/SImExporter.php
new file mode 100644
index 00000000..7120aaad
--- /dev/null
+++ b/lib/Froxlor/SImExporter.php
@@ -0,0 +1,132 @@
+
+ * @author Froxlor team