").text(i.label)).appendTo(e)},_move:function(t,e){return this.menu.element.is(":visible")?this.menu.isFirstItem()&&/^previous/.test(t)||this.menu.isLastItem()&&/^next/.test(t)?(this.isMultiLine||this._value(this.term),this.menu.blur(),void 0):(this.menu[t](e),void 0):(this.search(null,e),void 0)},widget:function(){return this.menu.element},_value:function(){return this.valueMethod.apply(this.element,arguments)},_keyEvent:function(t,e){(!this.isMultiLine||this.menu.element.is(":visible"))&&(this._move(t,e),e.preventDefault())},_isContentEditable:function(t){if(!t.length)return!1;var e=t.prop("contentEditable");return"inherit"===e?this._isContentEditable(t.parent()):"true"===e}}),t.extend(t.ui.autocomplete,{escapeRegex:function(t){return t.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")},filter:function(e,i){var s=RegExp(t.ui.autocomplete.escapeRegex(i),"i");return t.grep(e,function(t){return s.test(t.label||t.value||t)})}}),t.widget("ui.autocomplete",t.ui.autocomplete,{options:{messages:{noResults:"No search results.",results:function(t){return t+(t>1?" results are":" result is")+" available, use up and down arrow keys to navigate."}}},__response:function(e){var i;this._superApply(arguments),this.options.disabled||this.cancelSearch||(i=e&&e.length?this.options.messages.results(e.length):this.options.messages.noResults,this.liveRegion.children().hide(),t("
").text(i).appendTo(this.liveRegion))}}),t.ui.autocomplete,t.extend(t.ui,{datepicker:{version:"1.12.0"}});var c;t.extend(i.prototype,{markerClassName:"hasDatepicker",maxRows:4,_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(t){return o(this._defaults,t||{}),this},_attachDatepicker:function(e,i){var s,n,o;s=e.nodeName.toLowerCase(),n="div"===s||"span"===s,e.id||(this.uuid+=1,e.id="dp"+this.uuid),o=this._newInst(t(e),n),o.settings=t.extend({},i||{}),"input"===s?this._connectDatepicker(e,o):n&&this._inlineDatepicker(e,o)},_newInst:function(e,i){var n=e[0].id.replace(/([^A-Za-z0-9_\-])/g,"\\\\$1");return{id:n,input:e,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:i,dpDiv:i?s(t("
")):this.dpDiv}},_connectDatepicker:function(e,i){var s=t(e);i.append=t([]),i.trigger=t([]),s.hasClass(this.markerClassName)||(this._attachments(s,i),s.addClass(this.markerClassName).on("keydown",this._doKeyDown).on("keypress",this._doKeyPress).on("keyup",this._doKeyUp),this._autoSize(i),t.data(e,"datepicker",i),i.settings.disabled&&this._disableDatepicker(e))},_attachments:function(e,i){var s,n,o,a=this._get(i,"appendText"),r=this._get(i,"isRTL");i.append&&i.append.remove(),a&&(i.append=t("
"+a+" "),e[r?"before":"after"](i.append)),e.off("focus",this._showDatepicker),i.trigger&&i.trigger.remove(),s=this._get(i,"showOn"),("focus"===s||"both"===s)&&e.on("focus",this._showDatepicker),("button"===s||"both"===s)&&(n=this._get(i,"buttonText"),o=this._get(i,"buttonImage"),i.trigger=t(this._get(i,"buttonImageOnly")?t("
").addClass(this._triggerClass).attr({src:o,alt:n,title:n}):t("
").addClass(this._triggerClass).html(o?t("
").attr({src:o,alt:n,title:n}):n)),e[r?"before":"after"](i.trigger),i.trigger.on("click",function(){return t.datepicker._datepickerShowing&&t.datepicker._lastInput===e[0]?t.datepicker._hideDatepicker():t.datepicker._datepickerShowing&&t.datepicker._lastInput!==e[0]?(t.datepicker._hideDatepicker(),t.datepicker._showDatepicker(e[0])):t.datepicker._showDatepicker(e[0]),!1}))},_autoSize:function(t){if(this._get(t,"autoSize")&&!t.inline){var e,i,s,n,o=new Date(2009,11,20),a=this._get(t,"dateFormat");a.match(/[DM]/)&&(e=function(t){for(i=0,s=0,n=0;t.length>n;n++)t[n].length>i&&(i=t[n].length,s=n);return s},o.setMonth(e(this._get(t,a.match(/MM/)?"monthNames":"monthNamesShort"))),o.setDate(e(this._get(t,a.match(/DD/)?"dayNames":"dayNamesShort"))+20-o.getDay())),t.input.attr("size",this._formatDate(t,o).length)}},_inlineDatepicker:function(e,i){var s=t(e);s.hasClass(this.markerClassName)||(s.addClass(this.markerClassName).append(i.dpDiv),t.data(e,"datepicker",i),this._setDate(i,this._getDefaultDate(i),!0),this._updateDatepicker(i),this._updateAlternate(i),i.settings.disabled&&this._disableDatepicker(e),i.dpDiv.css("display","block"))},_dialogDatepicker:function(e,i,s,n,a){var r,l,h,c,u,d=this._dialogInst;return d||(this.uuid+=1,r="dp"+this.uuid,this._dialogInput=t("
"),this._dialogInput.on("keydown",this._doKeyDown),t("body").append(this._dialogInput),d=this._dialogInst=this._newInst(this._dialogInput,!1),d.settings={},t.data(this._dialogInput[0],"datepicker",d)),o(d.settings,n||{}),i=i&&i.constructor===Date?this._formatDate(d,i):i,this._dialogInput.val(i),this._pos=a?a.length?a:[a.pageX,a.pageY]:null,this._pos||(l=document.documentElement.clientWidth,h=document.documentElement.clientHeight,c=document.documentElement.scrollLeft||document.body.scrollLeft,u=document.documentElement.scrollTop||document.body.scrollTop,this._pos=[l/2-100+c,h/2-150+u]),this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px"),d.settings.onSelect=s,this._inDialog=!0,this.dpDiv.addClass(this._dialogClass),this._showDatepicker(this._dialogInput[0]),t.blockUI&&t.blockUI(this.dpDiv),t.data(this._dialogInput[0],"datepicker",d),this},_destroyDatepicker:function(e){var i,s=t(e),n=t.data(e,"datepicker");s.hasClass(this.markerClassName)&&(i=e.nodeName.toLowerCase(),t.removeData(e,"datepicker"),"input"===i?(n.append.remove(),n.trigger.remove(),s.removeClass(this.markerClassName).off("focus",this._showDatepicker).off("keydown",this._doKeyDown).off("keypress",this._doKeyPress).off("keyup",this._doKeyUp)):("div"===i||"span"===i)&&s.removeClass(this.markerClassName).empty(),c===n&&(c=null))},_enableDatepicker:function(e){var i,s,n=t(e),o=t.data(e,"datepicker");n.hasClass(this.markerClassName)&&(i=e.nodeName.toLowerCase(),"input"===i?(e.disabled=!1,o.trigger.filter("button").each(function(){this.disabled=!1}).end().filter("img").css({opacity:"1.0",cursor:""})):("div"===i||"span"===i)&&(s=n.children("."+this._inlineClass),s.children().removeClass("ui-state-disabled"),s.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!1)),this._disabledInputs=t.map(this._disabledInputs,function(t){return t===e?null:t}))},_disableDatepicker:function(e){var i,s,n=t(e),o=t.data(e,"datepicker");n.hasClass(this.markerClassName)&&(i=e.nodeName.toLowerCase(),"input"===i?(e.disabled=!0,o.trigger.filter("button").each(function(){this.disabled=!0}).end().filter("img").css({opacity:"0.5",cursor:"default"})):("div"===i||"span"===i)&&(s=n.children("."+this._inlineClass),s.children().addClass("ui-state-disabled"),s.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!0)),this._disabledInputs=t.map(this._disabledInputs,function(t){return t===e?null:t}),this._disabledInputs[this._disabledInputs.length]=e)},_isDisabledDatepicker:function(t){if(!t)return!1;for(var e=0;this._disabledInputs.length>e;e++)if(this._disabledInputs[e]===t)return!0;return!1},_getInst:function(e){try{return t.data(e,"datepicker")}catch(i){throw"Missing instance data for this datepicker"}},_optionDatepicker:function(e,i,s){var n,a,r,l,h=this._getInst(e);return 2===arguments.length&&"string"==typeof i?"defaults"===i?t.extend({},t.datepicker._defaults):h?"all"===i?t.extend({},h.settings):this._get(h,i):null:(n=i||{},"string"==typeof i&&(n={},n[i]=s),h&&(this._curInst===h&&this._hideDatepicker(),a=this._getDateDatepicker(e,!0),r=this._getMinMaxDate(h,"min"),l=this._getMinMaxDate(h,"max"),o(h.settings,n),null!==r&&void 0!==n.dateFormat&&void 0===n.minDate&&(h.settings.minDate=this._formatDate(h,r)),null!==l&&void 0!==n.dateFormat&&void 0===n.maxDate&&(h.settings.maxDate=this._formatDate(h,l)),"disabled"in n&&(n.disabled?this._disableDatepicker(e):this._enableDatepicker(e)),this._attachments(t(e),h),this._autoSize(h),this._setDate(h,a),this._updateAlternate(h),this._updateDatepicker(h)),void 0)},_changeDatepicker:function(t,e,i){this._optionDatepicker(t,e,i)},_refreshDatepicker:function(t){var e=this._getInst(t);e&&this._updateDatepicker(e)},_setDateDatepicker:function(t,e){var i=this._getInst(t);i&&(this._setDate(i,e),this._updateDatepicker(i),this._updateAlternate(i))},_getDateDatepicker:function(t,e){var i=this._getInst(t);return i&&!i.inline&&this._setDateFromField(i,e),i?this._getDate(i):null},_doKeyDown:function(e){var i,s,n,o=t.datepicker._getInst(e.target),a=!0,r=o.dpDiv.is(".ui-datepicker-rtl");if(o._keyEvent=!0,t.datepicker._datepickerShowing)switch(e.keyCode){case 9:t.datepicker._hideDatepicker(),a=!1;break;case 13:return n=t("td."+t.datepicker._dayOverClass+":not(."+t.datepicker._currentClass+")",o.dpDiv),n[0]&&t.datepicker._selectDay(e.target,o.selectedMonth,o.selectedYear,n[0]),i=t.datepicker._get(o,"onSelect"),i?(s=t.datepicker._formatDate(o),i.apply(o.input?o.input[0]:null,[s,o])):t.datepicker._hideDatepicker(),!1;case 27:t.datepicker._hideDatepicker();break;case 33:t.datepicker._adjustDate(e.target,e.ctrlKey?-t.datepicker._get(o,"stepBigMonths"):-t.datepicker._get(o,"stepMonths"),"M");break;case 34:t.datepicker._adjustDate(e.target,e.ctrlKey?+t.datepicker._get(o,"stepBigMonths"):+t.datepicker._get(o,"stepMonths"),"M");break;case 35:(e.ctrlKey||e.metaKey)&&t.datepicker._clearDate(e.target),a=e.ctrlKey||e.metaKey;break;case 36:(e.ctrlKey||e.metaKey)&&t.datepicker._gotoToday(e.target),a=e.ctrlKey||e.metaKey;break;case 37:(e.ctrlKey||e.metaKey)&&t.datepicker._adjustDate(e.target,r?1:-1,"D"),a=e.ctrlKey||e.metaKey,e.originalEvent.altKey&&t.datepicker._adjustDate(e.target,e.ctrlKey?-t.datepicker._get(o,"stepBigMonths"):-t.datepicker._get(o,"stepMonths"),"M");break;case 38:(e.ctrlKey||e.metaKey)&&t.datepicker._adjustDate(e.target,-7,"D"),a=e.ctrlKey||e.metaKey;break;case 39:(e.ctrlKey||e.metaKey)&&t.datepicker._adjustDate(e.target,r?-1:1,"D"),a=e.ctrlKey||e.metaKey,e.originalEvent.altKey&&t.datepicker._adjustDate(e.target,e.ctrlKey?+t.datepicker._get(o,"stepBigMonths"):+t.datepicker._get(o,"stepMonths"),"M");break;case 40:(e.ctrlKey||e.metaKey)&&t.datepicker._adjustDate(e.target,7,"D"),a=e.ctrlKey||e.metaKey;break;default:a=!1}else 36===e.keyCode&&e.ctrlKey?t.datepicker._showDatepicker(this):a=!1;a&&(e.preventDefault(),e.stopPropagation())},_doKeyPress:function(e){var i,s,n=t.datepicker._getInst(e.target);return t.datepicker._get(n,"constrainInput")?(i=t.datepicker._possibleChars(t.datepicker._get(n,"dateFormat")),s=String.fromCharCode(null==e.charCode?e.keyCode:e.charCode),e.ctrlKey||e.metaKey||" ">s||!i||i.indexOf(s)>-1):void 0},_doKeyUp:function(e){var i,s=t.datepicker._getInst(e.target);if(s.input.val()!==s.lastVal)try{i=t.datepicker.parseDate(t.datepicker._get(s,"dateFormat"),s.input?s.input.val():null,t.datepicker._getFormatConfig(s)),i&&(t.datepicker._setDateFromField(s),t.datepicker._updateAlternate(s),t.datepicker._updateDatepicker(s))}catch(n){}return!0},_showDatepicker:function(i){if(i=i.target||i,"input"!==i.nodeName.toLowerCase()&&(i=t("input",i.parentNode)[0]),!t.datepicker._isDisabledDatepicker(i)&&t.datepicker._lastInput!==i){var s,n,a,r,l,h,c;s=t.datepicker._getInst(i),t.datepicker._curInst&&t.datepicker._curInst!==s&&(t.datepicker._curInst.dpDiv.stop(!0,!0),s&&t.datepicker._datepickerShowing&&t.datepicker._hideDatepicker(t.datepicker._curInst.input[0])),n=t.datepicker._get(s,"beforeShow"),a=n?n.apply(i,[i,s]):{},a!==!1&&(o(s.settings,a),s.lastVal=null,t.datepicker._lastInput=i,t.datepicker._setDateFromField(s),t.datepicker._inDialog&&(i.value=""),t.datepicker._pos||(t.datepicker._pos=t.datepicker._findPos(i),t.datepicker._pos[1]+=i.offsetHeight),r=!1,t(i).parents().each(function(){return r|="fixed"===t(this).css("position"),!r}),l={left:t.datepicker._pos[0],top:t.datepicker._pos[1]},t.datepicker._pos=null,s.dpDiv.empty(),s.dpDiv.css({position:"absolute",display:"block",top:"-1000px"}),t.datepicker._updateDatepicker(s),l=t.datepicker._checkOffset(s,l,r),s.dpDiv.css({position:t.datepicker._inDialog&&t.blockUI?"static":r?"fixed":"absolute",display:"none",left:l.left+"px",top:l.top+"px"}),s.inline||(h=t.datepicker._get(s,"showAnim"),c=t.datepicker._get(s,"duration"),s.dpDiv.css("z-index",e(t(i))+1),t.datepicker._datepickerShowing=!0,t.effects&&t.effects.effect[h]?s.dpDiv.show(h,t.datepicker._get(s,"showOptions"),c):s.dpDiv[h||"show"](h?c:null),t.datepicker._shouldFocusInput(s)&&s.input.trigger("focus"),t.datepicker._curInst=s))}},_updateDatepicker:function(e){this.maxRows=4,c=e,e.dpDiv.empty().append(this._generateHTML(e)),this._attachHandlers(e);var i,s=this._getNumberOfMonths(e),o=s[1],a=17,r=e.dpDiv.find("."+this._dayOverClass+" a");r.length>0&&n.apply(r.get(0)),e.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""),o>1&&e.dpDiv.addClass("ui-datepicker-multi-"+o).css("width",a*o+"em"),e.dpDiv[(1!==s[0]||1!==s[1]?"add":"remove")+"Class"]("ui-datepicker-multi"),e.dpDiv[(this._get(e,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl"),e===t.datepicker._curInst&&t.datepicker._datepickerShowing&&t.datepicker._shouldFocusInput(e)&&e.input.trigger("focus"),e.yearshtml&&(i=e.yearshtml,setTimeout(function(){i===e.yearshtml&&e.yearshtml&&e.dpDiv.find("select.ui-datepicker-year:first").replaceWith(e.yearshtml),i=e.yearshtml=null},0))},_shouldFocusInput:function(t){return t.input&&t.input.is(":visible")&&!t.input.is(":disabled")&&!t.input.is(":focus")},_checkOffset:function(e,i,s){var n=e.dpDiv.outerWidth(),o=e.dpDiv.outerHeight(),a=e.input?e.input.outerWidth():0,r=e.input?e.input.outerHeight():0,l=document.documentElement.clientWidth+(s?0:t(document).scrollLeft()),h=document.documentElement.clientHeight+(s?0:t(document).scrollTop());return i.left-=this._get(e,"isRTL")?n-a:0,i.left-=s&&i.left===e.input.offset().left?t(document).scrollLeft():0,i.top-=s&&i.top===e.input.offset().top+r?t(document).scrollTop():0,i.left-=Math.min(i.left,i.left+n>l&&l>n?Math.abs(i.left+n-l):0),i.top-=Math.min(i.top,i.top+o>h&&h>o?Math.abs(o+r):0),i},_findPos:function(e){for(var i,s=this._getInst(e),n=this._get(s,"isRTL");e&&("hidden"===e.type||1!==e.nodeType||t.expr.filters.hidden(e));)e=e[n?"previousSibling":"nextSibling"];return i=t(e).offset(),[i.left,i.top]},_hideDatepicker:function(e){var i,s,n,o,a=this._curInst;!a||e&&a!==t.data(e,"datepicker")||this._datepickerShowing&&(i=this._get(a,"showAnim"),s=this._get(a,"duration"),n=function(){t.datepicker._tidyDialog(a)},t.effects&&(t.effects.effect[i]||t.effects[i])?a.dpDiv.hide(i,t.datepicker._get(a,"showOptions"),s,n):a.dpDiv["slideDown"===i?"slideUp":"fadeIn"===i?"fadeOut":"hide"](i?s:null,n),i||n(),this._datepickerShowing=!1,o=this._get(a,"onClose"),o&&o.apply(a.input?a.input[0]:null,[a.input?a.input.val():"",a]),this._lastInput=null,this._inDialog&&(this._dialogInput.css({position:"absolute",left:"0",top:"-100px"}),t.blockUI&&(t.unblockUI(),t("body").append(this.dpDiv))),this._inDialog=!1)},_tidyDialog:function(t){t.dpDiv.removeClass(this._dialogClass).off(".ui-datepicker-calendar")},_checkExternalClick:function(e){if(t.datepicker._curInst){var i=t(e.target),s=t.datepicker._getInst(i[0]);(i[0].id!==t.datepicker._mainDivId&&0===i.parents("#"+t.datepicker._mainDivId).length&&!i.hasClass(t.datepicker.markerClassName)&&!i.closest("."+t.datepicker._triggerClass).length&&t.datepicker._datepickerShowing&&(!t.datepicker._inDialog||!t.blockUI)||i.hasClass(t.datepicker.markerClassName)&&t.datepicker._curInst!==s)&&t.datepicker._hideDatepicker()}},_adjustDate:function(e,i,s){var n=t(e),o=this._getInst(n[0]);this._isDisabledDatepicker(n[0])||(this._adjustInstDate(o,i+("M"===s?this._get(o,"showCurrentAtPos"):0),s),this._updateDatepicker(o))},_gotoToday:function(e){var i,s=t(e),n=this._getInst(s[0]);this._get(n,"gotoCurrent")&&n.currentDay?(n.selectedDay=n.currentDay,n.drawMonth=n.selectedMonth=n.currentMonth,n.drawYear=n.selectedYear=n.currentYear):(i=new Date,n.selectedDay=i.getDate(),n.drawMonth=n.selectedMonth=i.getMonth(),n.drawYear=n.selectedYear=i.getFullYear()),this._notifyChange(n),this._adjustDate(s)},_selectMonthYear:function(e,i,s){var n=t(e),o=this._getInst(n[0]);o["selected"+("M"===s?"Month":"Year")]=o["draw"+("M"===s?"Month":"Year")]=parseInt(i.options[i.selectedIndex].value,10),this._notifyChange(o),this._adjustDate(n)},_selectDay:function(e,i,s,n){var o,a=t(e);t(n).hasClass(this._unselectableClass)||this._isDisabledDatepicker(a[0])||(o=this._getInst(a[0]),o.selectedDay=o.currentDay=t("a",n).html(),o.selectedMonth=o.currentMonth=i,o.selectedYear=o.currentYear=s,this._selectDate(e,this._formatDate(o,o.currentDay,o.currentMonth,o.currentYear)))},_clearDate:function(e){var i=t(e);this._selectDate(i,"")},_selectDate:function(e,i){var s,n=t(e),o=this._getInst(n[0]);i=null!=i?i:this._formatDate(o),o.input&&o.input.val(i),this._updateAlternate(o),s=this._get(o,"onSelect"),s?s.apply(o.input?o.input[0]:null,[i,o]):o.input&&o.input.trigger("change"),o.inline?this._updateDatepicker(o):(this._hideDatepicker(),this._lastInput=o.input[0],"object"!=typeof o.input[0]&&o.input.trigger("focus"),this._lastInput=null)},_updateAlternate:function(e){var i,s,n,o=this._get(e,"altField");o&&(i=this._get(e,"altFormat")||this._get(e,"dateFormat"),s=this._getDate(e),n=this.formatDate(i,s,this._getFormatConfig(e)),t(o).val(n))},noWeekends:function(t){var e=t.getDay();return[e>0&&6>e,""]},iso8601Week:function(t){var e,i=new Date(t.getTime());return i.setDate(i.getDate()+4-(i.getDay()||7)),e=i.getTime(),i.setMonth(0),i.setDate(1),Math.floor(Math.round((e-i)/864e5)/7)+1},parseDate:function(e,i,s){if(null==e||null==i)throw"Invalid arguments";if(i="object"==typeof i?""+i:i+"",""===i)return null;var n,o,a,r,l=0,h=(s?s.shortYearCutoff:null)||this._defaults.shortYearCutoff,c="string"!=typeof h?h:(new Date).getFullYear()%100+parseInt(h,10),u=(s?s.dayNamesShort:null)||this._defaults.dayNamesShort,d=(s?s.dayNames:null)||this._defaults.dayNames,p=(s?s.monthNamesShort:null)||this._defaults.monthNamesShort,f=(s?s.monthNames:null)||this._defaults.monthNames,g=-1,m=-1,_=-1,v=-1,b=!1,y=function(t){var i=e.length>n+1&&e.charAt(n+1)===t;return i&&n++,i},w=function(t){var e=y(t),s="@"===t?14:"!"===t?20:"y"===t&&e?4:"o"===t?3:2,n="y"===t?s:1,o=RegExp("^\\d{"+n+","+s+"}"),a=i.substring(l).match(o);if(!a)throw"Missing number at position "+l;return l+=a[0].length,parseInt(a[0],10)},k=function(e,s,n){var o=-1,a=t.map(y(e)?n:s,function(t,e){return[[e,t]]}).sort(function(t,e){return-(t[1].length-e[1].length)});if(t.each(a,function(t,e){var s=e[1];return i.substr(l,s.length).toLowerCase()===s.toLowerCase()?(o=e[0],l+=s.length,!1):void 0}),-1!==o)return o+1;throw"Unknown name at position "+l},x=function(){if(i.charAt(l)!==e.charAt(n))throw"Unexpected literal at position "+l;l++};for(n=0;e.length>n;n++)if(b)"'"!==e.charAt(n)||y("'")?x():b=!1;else switch(e.charAt(n)){case"d":_=w("d");break;case"D":k("D",u,d);break;case"o":v=w("o");break;case"m":m=w("m");break;case"M":m=k("M",p,f);break;case"y":g=w("y");break;case"@":r=new Date(w("@")),g=r.getFullYear(),m=r.getMonth()+1,_=r.getDate();
+break;case"!":r=new Date((w("!")-this._ticksTo1970)/1e4),g=r.getFullYear(),m=r.getMonth()+1,_=r.getDate();break;case"'":y("'")?x():b=!0;break;default:x()}if(i.length>l&&(a=i.substr(l),!/^\s+/.test(a)))throw"Extra/unparsed characters found in date: "+a;if(-1===g?g=(new Date).getFullYear():100>g&&(g+=(new Date).getFullYear()-(new Date).getFullYear()%100+(c>=g?0:-100)),v>-1)for(m=1,_=v;;){if(o=this._getDaysInMonth(g,m-1),o>=_)break;m++,_-=o}if(r=this._daylightSavingAdjust(new Date(g,m-1,_)),r.getFullYear()!==g||r.getMonth()+1!==m||r.getDate()!==_)throw"Invalid date";return r},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:1e7*60*60*24*(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925)),formatDate:function(t,e,i){if(!e)return"";var s,n=(i?i.dayNamesShort:null)||this._defaults.dayNamesShort,o=(i?i.dayNames:null)||this._defaults.dayNames,a=(i?i.monthNamesShort:null)||this._defaults.monthNamesShort,r=(i?i.monthNames:null)||this._defaults.monthNames,l=function(e){var i=t.length>s+1&&t.charAt(s+1)===e;return i&&s++,i},h=function(t,e,i){var s=""+e;if(l(t))for(;i>s.length;)s="0"+s;return s},c=function(t,e,i,s){return l(t)?s[e]:i[e]},u="",d=!1;if(e)for(s=0;t.length>s;s++)if(d)"'"!==t.charAt(s)||l("'")?u+=t.charAt(s):d=!1;else switch(t.charAt(s)){case"d":u+=h("d",e.getDate(),2);break;case"D":u+=c("D",e.getDay(),n,o);break;case"o":u+=h("o",Math.round((new Date(e.getFullYear(),e.getMonth(),e.getDate()).getTime()-new Date(e.getFullYear(),0,0).getTime())/864e5),3);break;case"m":u+=h("m",e.getMonth()+1,2);break;case"M":u+=c("M",e.getMonth(),a,r);break;case"y":u+=l("y")?e.getFullYear():(10>e.getFullYear()%100?"0":"")+e.getFullYear()%100;break;case"@":u+=e.getTime();break;case"!":u+=1e4*e.getTime()+this._ticksTo1970;break;case"'":l("'")?u+="'":d=!0;break;default:u+=t.charAt(s)}return u},_possibleChars:function(t){var e,i="",s=!1,n=function(i){var s=t.length>e+1&&t.charAt(e+1)===i;return s&&e++,s};for(e=0;t.length>e;e++)if(s)"'"!==t.charAt(e)||n("'")?i+=t.charAt(e):s=!1;else switch(t.charAt(e)){case"d":case"m":case"y":case"@":i+="0123456789";break;case"D":case"M":return null;case"'":n("'")?i+="'":s=!0;break;default:i+=t.charAt(e)}return i},_get:function(t,e){return void 0!==t.settings[e]?t.settings[e]:this._defaults[e]},_setDateFromField:function(t,e){if(t.input.val()!==t.lastVal){var i=this._get(t,"dateFormat"),s=t.lastVal=t.input?t.input.val():null,n=this._getDefaultDate(t),o=n,a=this._getFormatConfig(t);try{o=this.parseDate(i,s,a)||n}catch(r){s=e?"":s}t.selectedDay=o.getDate(),t.drawMonth=t.selectedMonth=o.getMonth(),t.drawYear=t.selectedYear=o.getFullYear(),t.currentDay=s?o.getDate():0,t.currentMonth=s?o.getMonth():0,t.currentYear=s?o.getFullYear():0,this._adjustInstDate(t)}},_getDefaultDate:function(t){return this._restrictMinMax(t,this._determineDate(t,this._get(t,"defaultDate"),new Date))},_determineDate:function(e,i,s){var n=function(t){var e=new Date;return e.setDate(e.getDate()+t),e},o=function(i){try{return t.datepicker.parseDate(t.datepicker._get(e,"dateFormat"),i,t.datepicker._getFormatConfig(e))}catch(s){}for(var n=(i.toLowerCase().match(/^c/)?t.datepicker._getDate(e):null)||new Date,o=n.getFullYear(),a=n.getMonth(),r=n.getDate(),l=/([+\-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g,h=l.exec(i);h;){switch(h[2]||"d"){case"d":case"D":r+=parseInt(h[1],10);break;case"w":case"W":r+=7*parseInt(h[1],10);break;case"m":case"M":a+=parseInt(h[1],10),r=Math.min(r,t.datepicker._getDaysInMonth(o,a));break;case"y":case"Y":o+=parseInt(h[1],10),r=Math.min(r,t.datepicker._getDaysInMonth(o,a))}h=l.exec(i)}return new Date(o,a,r)},a=null==i||""===i?s:"string"==typeof i?o(i):"number"==typeof i?isNaN(i)?s:n(i):new Date(i.getTime());return a=a&&"Invalid Date"==""+a?s:a,a&&(a.setHours(0),a.setMinutes(0),a.setSeconds(0),a.setMilliseconds(0)),this._daylightSavingAdjust(a)},_daylightSavingAdjust:function(t){return t?(t.setHours(t.getHours()>12?t.getHours()+2:0),t):null},_setDate:function(t,e,i){var s=!e,n=t.selectedMonth,o=t.selectedYear,a=this._restrictMinMax(t,this._determineDate(t,e,new Date));t.selectedDay=t.currentDay=a.getDate(),t.drawMonth=t.selectedMonth=t.currentMonth=a.getMonth(),t.drawYear=t.selectedYear=t.currentYear=a.getFullYear(),n===t.selectedMonth&&o===t.selectedYear||i||this._notifyChange(t),this._adjustInstDate(t),t.input&&t.input.val(s?"":this._formatDate(t))},_getDate:function(t){var e=!t.currentYear||t.input&&""===t.input.val()?null:this._daylightSavingAdjust(new Date(t.currentYear,t.currentMonth,t.currentDay));return e},_attachHandlers:function(e){var i=this._get(e,"stepMonths"),s="#"+e.id.replace(/\\\\/g,"\\");e.dpDiv.find("[data-handler]").map(function(){var e={prev:function(){t.datepicker._adjustDate(s,-i,"M")},next:function(){t.datepicker._adjustDate(s,+i,"M")},hide:function(){t.datepicker._hideDatepicker()},today:function(){t.datepicker._gotoToday(s)},selectDay:function(){return t.datepicker._selectDay(s,+this.getAttribute("data-month"),+this.getAttribute("data-year"),this),!1},selectMonth:function(){return t.datepicker._selectMonthYear(s,this,"M"),!1},selectYear:function(){return t.datepicker._selectMonthYear(s,this,"Y"),!1}};t(this).on(this.getAttribute("data-event"),e[this.getAttribute("data-handler")])})},_generateHTML:function(t){var e,i,s,n,o,a,r,l,h,c,u,d,p,f,g,m,_,v,b,y,w,k,x,C,D,T,I,M,P,S,N,H,z,A,O,W,E,F,L,R=new Date,Y=this._daylightSavingAdjust(new Date(R.getFullYear(),R.getMonth(),R.getDate())),B=this._get(t,"isRTL"),j=this._get(t,"showButtonPanel"),q=this._get(t,"hideIfNoPrevNext"),K=this._get(t,"navigationAsDateFormat"),U=this._getNumberOfMonths(t),V=this._get(t,"showCurrentAtPos"),X=this._get(t,"stepMonths"),$=1!==U[0]||1!==U[1],G=this._daylightSavingAdjust(t.currentDay?new Date(t.currentYear,t.currentMonth,t.currentDay):new Date(9999,9,9)),J=this._getMinMaxDate(t,"min"),Q=this._getMinMaxDate(t,"max"),Z=t.drawMonth-V,te=t.drawYear;if(0>Z&&(Z+=12,te--),Q)for(e=this._daylightSavingAdjust(new Date(Q.getFullYear(),Q.getMonth()-U[0]*U[1]+1,Q.getDate())),e=J&&J>e?J:e;this._daylightSavingAdjust(new Date(te,Z,1))>e;)Z--,0>Z&&(Z=11,te--);for(t.drawMonth=Z,t.drawYear=te,i=this._get(t,"prevText"),i=K?this.formatDate(i,this._daylightSavingAdjust(new Date(te,Z-X,1)),this._getFormatConfig(t)):i,s=this._canAdjustMonth(t,-1,te,Z)?"
"+i+" ":q?"":"
"+i+" ",n=this._get(t,"nextText"),n=K?this.formatDate(n,this._daylightSavingAdjust(new Date(te,Z+X,1)),this._getFormatConfig(t)):n,o=this._canAdjustMonth(t,1,te,Z)?"
"+n+" ":q?"":"
"+n+" ",a=this._get(t,"currentText"),r=this._get(t,"gotoCurrent")&&t.currentDay?G:Y,a=K?this.formatDate(a,r,this._getFormatConfig(t)):a,l=t.inline?"":"
"+this._get(t,"closeText")+" ",h=j?"
"+(B?l:"")+(this._isInRange(t,r)?""+a+" ":"")+(B?"":l)+"
":"",c=parseInt(this._get(t,"firstDay"),10),c=isNaN(c)?0:c,u=this._get(t,"showWeek"),d=this._get(t,"dayNames"),p=this._get(t,"dayNamesMin"),f=this._get(t,"monthNames"),g=this._get(t,"monthNamesShort"),m=this._get(t,"beforeShowDay"),_=this._get(t,"showOtherMonths"),v=this._get(t,"selectOtherMonths"),b=this._getDefaultDate(t),y="",k=0;U[0]>k;k++){for(x="",this.maxRows=4,C=0;U[1]>C;C++){if(D=this._daylightSavingAdjust(new Date(te,Z,t.selectedDay)),T=" ui-corner-all",I="",$){if(I+="
"}for(I+="
"+"",M=u?""+this._get(t,"weekHeader")+" ":"",w=0;7>w;w++)P=(w+c)%7,M+="=5?" class='ui-datepicker-week-end'":"")+">"+""+p[P]+" ";for(I+=M+" ",S=this._getDaysInMonth(te,Z),te===t.selectedYear&&Z===t.selectedMonth&&(t.selectedDay=Math.min(t.selectedDay,S)),N=(this._getFirstDayOfMonth(te,Z)-c+7)%7,H=Math.ceil((N+S)/7),z=$?this.maxRows>H?this.maxRows:H:H,this.maxRows=z,A=this._daylightSavingAdjust(new Date(te,Z,1-N)),O=0;z>O;O++){for(I+="",W=u?""+this._get(t,"calculateWeek")(A)+" ":"",w=0;7>w;w++)E=m?m.apply(t.input?t.input[0]:null,[A]):[!0,""],F=A.getMonth()!==Z,L=F&&!v||!E[0]||J&&J>A||Q&&A>Q,W+=""+(F&&!_?" ":L?""+A.getDate()+" ":""+A.getDate()+" ")+" ",A.setDate(A.getDate()+1),A=this._daylightSavingAdjust(A);I+=W+" "}Z++,Z>11&&(Z=0,te++),I+="
"+($?"
"+(U[0]>0&&C===U[1]-1?"
":""):""),x+=I}y+=x}return y+=h,t._keyEvent=!1,y},_generateMonthYearHeader:function(t,e,i,s,n,o,a,r){var l,h,c,u,d,p,f,g,m=this._get(t,"changeMonth"),_=this._get(t,"changeYear"),v=this._get(t,"showMonthAfterYear"),b="
",y="";if(o||!m)y+=""+a[e]+" ";else{for(l=s&&s.getFullYear()===i,h=n&&n.getFullYear()===i,y+="",c=0;12>c;c++)(!l||c>=s.getMonth())&&(!h||n.getMonth()>=c)&&(y+=""+r[c]+" ");y+=" "}if(v||(b+=y+(!o&&m&&_?"":" ")),!t.yearshtml)if(t.yearshtml="",o||!_)b+=""+i+" ";else{for(u=this._get(t,"yearRange").split(":"),d=(new Date).getFullYear(),p=function(t){var e=t.match(/c[+\-].*/)?i+parseInt(t.substring(1),10):t.match(/[+\-].*/)?d+parseInt(t,10):parseInt(t,10);return isNaN(e)?d:e},f=p(u[0]),g=Math.max(f,p(u[1]||"")),f=s?Math.max(f,s.getFullYear()):f,g=n?Math.min(g,n.getFullYear()):g,t.yearshtml+="";g>=f;f++)t.yearshtml+=""+f+" ";t.yearshtml+=" ",b+=t.yearshtml,t.yearshtml=null}return b+=this._get(t,"yearSuffix"),v&&(b+=(!o&&m&&_?"":" ")+y),b+="
"},_adjustInstDate:function(t,e,i){var s=t.selectedYear+("Y"===i?e:0),n=t.selectedMonth+("M"===i?e:0),o=Math.min(t.selectedDay,this._getDaysInMonth(s,n))+("D"===i?e:0),a=this._restrictMinMax(t,this._daylightSavingAdjust(new Date(s,n,o)));t.selectedDay=a.getDate(),t.drawMonth=t.selectedMonth=a.getMonth(),t.drawYear=t.selectedYear=a.getFullYear(),("M"===i||"Y"===i)&&this._notifyChange(t)},_restrictMinMax:function(t,e){var i=this._getMinMaxDate(t,"min"),s=this._getMinMaxDate(t,"max"),n=i&&i>e?i:e;return s&&n>s?s:n},_notifyChange:function(t){var e=this._get(t,"onChangeMonthYear");e&&e.apply(t.input?t.input[0]:null,[t.selectedYear,t.selectedMonth+1,t])},_getNumberOfMonths:function(t){var e=this._get(t,"numberOfMonths");return null==e?[1,1]:"number"==typeof e?[1,e]:e},_getMinMaxDate:function(t,e){return this._determineDate(t,this._get(t,e+"Date"),null)},_getDaysInMonth:function(t,e){return 32-this._daylightSavingAdjust(new Date(t,e,32)).getDate()},_getFirstDayOfMonth:function(t,e){return new Date(t,e,1).getDay()},_canAdjustMonth:function(t,e,i,s){var n=this._getNumberOfMonths(t),o=this._daylightSavingAdjust(new Date(i,s+(0>e?e:n[0]*n[1]),1));return 0>e&&o.setDate(this._getDaysInMonth(o.getFullYear(),o.getMonth())),this._isInRange(t,o)},_isInRange:function(t,e){var i,s,n=this._getMinMaxDate(t,"min"),o=this._getMinMaxDate(t,"max"),a=null,r=null,l=this._get(t,"yearRange");return l&&(i=l.split(":"),s=(new Date).getFullYear(),a=parseInt(i[0],10),r=parseInt(i[1],10),i[0].match(/[+\-].*/)&&(a+=s),i[1].match(/[+\-].*/)&&(r+=s)),(!n||e.getTime()>=n.getTime())&&(!o||e.getTime()<=o.getTime())&&(!a||e.getFullYear()>=a)&&(!r||r>=e.getFullYear())},_getFormatConfig:function(t){var e=this._get(t,"shortYearCutoff");return e="string"!=typeof e?e:(new Date).getFullYear()%100+parseInt(e,10),{shortYearCutoff:e,dayNamesShort:this._get(t,"dayNamesShort"),dayNames:this._get(t,"dayNames"),monthNamesShort:this._get(t,"monthNamesShort"),monthNames:this._get(t,"monthNames")}},_formatDate:function(t,e,i,s){e||(t.currentDay=t.selectedDay,t.currentMonth=t.selectedMonth,t.currentYear=t.selectedYear);var n=e?"object"==typeof e?e:this._daylightSavingAdjust(new Date(s,i,e)):this._daylightSavingAdjust(new Date(t.currentYear,t.currentMonth,t.currentDay));return this.formatDate(this._get(t,"dateFormat"),n,this._getFormatConfig(t))}}),t.fn.datepicker=function(e){if(!this.length)return this;t.datepicker.initialized||(t(document).on("mousedown",t.datepicker._checkExternalClick),t.datepicker.initialized=!0),0===t("#"+t.datepicker._mainDivId).length&&t("body").append(t.datepicker.dpDiv);var i=Array.prototype.slice.call(arguments,1);return"string"!=typeof e||"isDisabled"!==e&&"getDate"!==e&&"widget"!==e?"option"===e&&2===arguments.length&&"string"==typeof arguments[1]?t.datepicker["_"+e+"Datepicker"].apply(t.datepicker,[this[0]].concat(i)):this.each(function(){"string"==typeof e?t.datepicker["_"+e+"Datepicker"].apply(t.datepicker,[this].concat(i)):t.datepicker._attachDatepicker(this,e)}):t.datepicker["_"+e+"Datepicker"].apply(t.datepicker,[this[0]].concat(i))},t.datepicker=new i,t.datepicker.initialized=!1,t.datepicker.uuid=(new Date).getTime(),t.datepicker.version="1.12.0",t.datepicker});
\ No newline at end of file
diff --git a/src/categories.js b/src/categories.js
index 539a643f80..e058a452e6 100644
--- a/src/categories.js
+++ b/src/categories.js
@@ -35,14 +35,18 @@ var privileges = require('./privileges');
return next(new Error('[[error:invalid-cid]]'));
}
category = categories[0];
- if (parseInt(data.uid, 10)) {
- Categories.markAsRead([data.cid], data.uid);
- }
async.parallel({
topics: function(next) {
Categories.getCategoryTopics(data, next);
},
+ topicCount: function(next) {
+ if (Array.isArray(data.set)) {
+ db.sortedSetIntersectCard(data.set, next);
+ } else {
+ next(null, category.topic_count);
+ }
+ },
isIgnored: function(next) {
Categories.isIgnored([data.cid], data.uid, next);
}
@@ -52,6 +56,7 @@ var privileges = require('./privileges');
category.topics = results.topics.topics;
category.nextStart = results.topics.nextStart;
category.isIgnored = results.isIgnored[0];
+ category.topic_count = results.topicCount;
plugins.fireHook('filter:category.get', {category: category, uid: data.uid}, next);
},
@@ -292,7 +297,7 @@ var privileges = require('./privileges');
for (i; i < len; ++i) {
category = categories[i];
- if (!category.hasOwnProperty('parentCid')) {
+ if (!category.hasOwnProperty('parentCid') || category.parentCid === null) {
category.parentCid = 0;
}
@@ -305,6 +310,39 @@ var privileges = require('./privileges');
return tree;
};
+ Categories.buildForSelect = function(uid, callback) {
+ function recursive(category, categoriesData, level) {
+ if (category.link) {
+ return;
+ }
+
+ var bullet = level ? '• ' : '';
+ category.value = category.cid;
+ category.text = level + bullet + category.name
+ categoriesData.push(category);
+
+ category.children.forEach(function(child) {
+ recursive(child, categoriesData, ' ' + level);
+ });
+ }
+ Categories.getCategoriesByPrivilege('cid:0:children', uid, 'read', function(err, categories) {
+ if (err) {
+ return callback(err);
+ }
+
+ var categoriesData = [];
+
+ categories = categories.filter(function(category) {
+ return category && !category.link && !parseInt(category.parentCid, 10);
+ });
+
+ categories.forEach(function(category) {
+ recursive(category, categoriesData, '');
+ });
+ callback(null, categoriesData);
+ });
+ };
+
Categories.getIgnorers = function(cid, start, stop, callback) {
db.getSortedSetRevRange('cid:' + cid + ':ignorers', start, stop, callback);
};
diff --git a/src/categories/create.js b/src/categories/create.js
index 6e13d8689d..5c3a81578d 100644
--- a/src/categories/create.js
+++ b/src/categories/create.js
@@ -3,9 +3,9 @@
var async = require('async');
var db = require('../database');
-var privileges = require('../privileges');
var groups = require('../groups');
var plugins = require('../plugins');
+var privileges = require('../privileges');
var utils = require('../../public/src/utils');
module.exports = function(Categories) {
@@ -49,7 +49,7 @@ module.exports = function(Categories) {
function(data, next) {
category = data.category;
- var defaultPrivileges = ['find', 'read', 'topics:read', 'topics:create', 'topics:reply', 'upload:post:image'];
+ var defaultPrivileges = ['find', 'read', 'topics:read', 'topics:create', 'topics:reply', 'posts:edit', 'posts:delete', 'topics:delete', 'upload:post:image'];
async.series([
async.apply(db.setObject, 'category:' + category.cid, category),
diff --git a/src/categories/data.js b/src/categories/data.js
index fb166a47af..a9f70ab359 100644
--- a/src/categories/data.js
+++ b/src/categories/data.js
@@ -42,7 +42,7 @@ module.exports = function(Categories) {
return;
}
- category.name = validator.escape(category.name || '');
+ category.name = validator.escape(String(category.name || ''));
category.disabled = category.hasOwnProperty('disabled') ? parseInt(category.disabled, 10) === 1 : undefined;
category.icon = category.icon || 'hidden';
if (category.hasOwnProperty('post_count')) {
@@ -58,7 +58,7 @@ module.exports = function(Categories) {
}
if (category.description) {
- category.description = validator.escape(category.description);
+ category.description = validator.escape(String(category.description));
category.descriptionParsed = category.descriptionParsed || category.description;
}
}
diff --git a/src/categories/delete.js b/src/categories/delete.js
index 63f9fe324b..897e45916e 100644
--- a/src/categories/delete.js
+++ b/src/categories/delete.js
@@ -5,8 +5,8 @@ var db = require('../database');
var batch = require('../batch');
var plugins = require('../plugins');
var topics = require('../topics');
-var privileges = require('../privileges');
var groups = require('../groups');
+var privileges = require('../privileges');
module.exports = function(Categories) {
diff --git a/src/categories/recentreplies.js b/src/categories/recentreplies.js
index 7a5e1cc1e8..e7a1d0ba4e 100644
--- a/src/categories/recentreplies.js
+++ b/src/categories/recentreplies.js
@@ -9,9 +9,9 @@ var _ = require('underscore');
var db = require('../database');
var posts = require('../posts');
var topics = require('../topics');
-var categories = require('../categories');
var privileges = require('../privileges');
+
module.exports = function(Categories) {
Categories.getRecentReplies = function(cid, uid, count, callback) {
@@ -32,6 +32,39 @@ module.exports = function(Categories) {
], callback);
};
+ Categories.updateRecentTid = function(cid, tid, callback) {
+ async.parallel({
+ count: function(next) {
+ db.sortedSetCard('cid:' + cid + ':recent_tids', next);
+ },
+ numRecentReplies: function(next) {
+ db.getObjectField('category:' + cid, 'numRecentReplies', next);
+ }
+ }, function(err, results) {
+ if (err) {
+ return callback(err);
+ }
+
+ if (results.count < results.numRecentReplies) {
+ return db.sortedSetAdd('cid:' + cid + ':recent_tids', Date.now(), tid, callback);
+ }
+ async.waterfall([
+ function(next) {
+ db.getSortedSetRangeWithScores('cid:' + cid + ':recent_tids', 0, results.count - results.numRecentReplies, next);
+ },
+ function(data, next) {
+ if (!data.length) {
+ return next();
+ }
+ db.sortedSetsRemoveRangeByScore(['cid:' + cid + ':recent_tids'], '-inf', data[data.length - 1].score, next);
+ },
+ function(next) {
+ db.sortedSetAdd('cid:' + cid + ':recent_tids', Date.now(), tid, next);
+ }
+ ], callback);
+ });
+ };
+
Categories.getRecentTopicReplies = function(categoryData, uid, callback) {
if (!Array.isArray(categoryData) || !categoryData.length) {
return callback();
@@ -39,7 +72,10 @@ module.exports = function(Categories) {
async.waterfall([
function(next) {
- async.map(categoryData, getRecentTopicTids, next);
+ var keys = categoryData.map(function(category) {
+ return 'cid:' + category.cid + ':recent_tids';
+ });
+ db.getSortedSetsMembers(keys, next);
},
function(results, next) {
var tids = _.flatten(results);
@@ -62,45 +98,6 @@ module.exports = function(Categories) {
], callback);
};
- function getRecentTopicTids(category, callback) {
- var count = parseInt(category.numRecentReplies, 10);
- if (!count) {
- return callback(null, []);
- }
-
- if (count === 1) {
- async.waterfall([
- function (next) {
- db.getSortedSetRevRange('cid:' + category.cid + ':pids', 0, 0, next);
- },
- function (pid, next) {
- posts.getPostField(pid, 'tid', next);
- },
- function (tid, next) {
- next(null, [tid]);
- }
- ], callback);
- return;
- }
-
- async.parallel({
- pinnedTids: function(next) {
- db.getSortedSetRevRangeByScore('cid:' + category.cid + ':tids', 0, -1, '+inf', Date.now(), next);
- },
- tids: function(next) {
- db.getSortedSetRevRangeByScore('cid:' + category.cid + ':tids', 0, Math.max(1, count), Date.now(), '-inf', next);
- }
- }, function(err, results) {
- if (err) {
- return callback(err);
- }
-
- results.tids = results.tids.concat(results.pinnedTids);
-
- callback(null, results.tids);
- });
- }
-
function getTopics(tids, callback) {
var topicData;
async.waterfall([
@@ -121,7 +118,7 @@ module.exports = function(Categories) {
});
async.parallel({
- categoryData: async.apply(categories.getCategoriesFields, cids, ['cid', 'parentCid']),
+ categoryData: async.apply(Categories.getCategoriesFields, cids, ['cid', 'parentCid']),
teasers: async.apply(topics.getTeasers, _topicData),
}, next);
},
@@ -137,7 +134,7 @@ module.exports = function(Categories) {
teaser.tid = teaser.uid = teaser.user.uid = undefined;
teaser.topic = {
slug: topicData[index].slug,
- title: validator.escape(topicData[index].title)
+ title: validator.escape(String(topicData[index].title))
};
}
});
diff --git a/src/categories/topics.js b/src/categories/topics.js
index 8ee7105256..b1a0bfae54 100644
--- a/src/categories/topics.js
+++ b/src/categories/topics.js
@@ -53,16 +53,16 @@ module.exports = function(Categories) {
};
Categories.getTopicIds = function(set, reverse, start, stop, callback) {
- if (reverse) {
- db.getSortedSetRevRange(set, start, stop, callback);
+ if (Array.isArray(set)) {
+ db[reverse ? 'getSortedSetRevIntersect' : 'getSortedSetIntersect']({sets: set, start: start, stop: stop}, callback);
} else {
- db.getSortedSetRange(set, start, stop, callback);
+ db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, stop, callback);
}
};
Categories.getTopicIndex = function(tid, callback) {
topics.getTopicField(tid, 'cid', function(err, cid) {
- if(err) {
+ if (err) {
return callback(err);
}
@@ -89,6 +89,9 @@ module.exports = function(Categories) {
db.sortedSetAdd('cid:' + cid + ':tids', postData.timestamp, postData.tid, next);
}
},
+ function(next){
+ Categories.updateRecentTid(cid, postData.tid, next);
+ },
function(next) {
db.sortedSetIncrBy('cid:' + cid + ':tids:posts', 1, postData.tid, next);
}
diff --git a/src/categories/unread.js b/src/categories/unread.js
index d62aaa3c6e..37496e09b7 100644
--- a/src/categories/unread.js
+++ b/src/categories/unread.js
@@ -1,8 +1,8 @@
"use strict";
-var async = require('async'),
- db = require('../database');
+var async = require('async');
+var db = require('../database');
module.exports = function(Categories) {
diff --git a/src/categories/update.js b/src/categories/update.js
index 78e97e4076..7c45663073 100644
--- a/src/categories/update.js
+++ b/src/categories/update.js
@@ -1,11 +1,11 @@
'use strict';
-var async = require('async'),
- db = require('../database'),
- utils = require('../../public/src/utils'),
- translator = require('../../public/src/modules/translator'),
- plugins = require('../plugins');
+var async = require('async');
+var db = require('../database');
+var utils = require('../../public/src/utils');
+var translator = require('../../public/src/modules/translator');
+var plugins = require('../plugins');
module.exports = function(Categories) {
diff --git a/src/controllers/accounts/chats.js b/src/controllers/accounts/chats.js
index 4af94c5521..b1d23ebeb3 100644
--- a/src/controllers/accounts/chats.js
+++ b/src/controllers/accounts/chats.js
@@ -4,74 +4,112 @@ var async = require('async');
var messaging = require('../../messaging');
var meta = require('../../meta');
+var user = require('../../user');
var helpers = require('../helpers');
-
var chatsController = {};
chatsController.get = function(req, res, callback) {
if (parseInt(meta.config.disableChat, 10) === 1) {
return callback();
}
+ var uid;
+ var username;
+ var recentChats;
- messaging.getRecentChats(req.uid, 0, 19, function(err, recentChats) {
+ async.waterfall([
+ function(next) {
+ async.parallel({
+ uid: async.apply(user.getUidByUserslug, req.params.userslug),
+ username: async.apply(user.getUsernameByUserslug, req.params.userslug)
+ }, next);
+ },
+ function(results, next) {
+ uid = results.uid;
+ username = results.username;
+ if (!uid) {
+ return callback();
+ }
+ messaging.getRecentChats(uid, 0, 19, next);
+ },
+ function(_recentChats, next) {
+ recentChats = _recentChats;
+ if (!req.params.roomid) {
+ return res.render('chats', {
+ rooms: recentChats.rooms,
+ uid: uid,
+ userslug: req.params.userslug,
+ nextStart: recentChats.nextStart,
+ allowed: true,
+ title: '[[pages:chats]]',
+ breadcrumbs: helpers.buildBreadcrumbs([{text: username, url: '/user/' + req.params.userslug}, {text: '[[pages:chats]]'}])
+ });
+ }
+ messaging.isUserInRoom(req.uid, req.params.roomid, next);
+ },
+ function(inRoom, next) {
+ if (!inRoom && parseInt(req.uid, 10) === parseInt(uid, 10)) {
+ return callback();
+ }
+ async.parallel({
+ users: async.apply(messaging.getUsersInRoom, req.params.roomid, 0, -1),
+ messages: async.apply(messaging.getMessages, {
+ uid: uid,
+ roomId: req.params.roomid,
+ since: 'recent',
+ isNew: false
+ }),
+ room: async.apply(messaging.getRoomData, req.params.roomid)
+ }, next);
+ }
+ ], function(err, data) {
if (err) {
return callback(err);
}
+ var room = data.room;
+ room.messages = data.messages;
- if (!req.params.roomid) {
- return res.render('chats', {
- rooms: recentChats.rooms,
- nextStart: recentChats.nextStart,
- allowed: true,
- title: '[[pages:chats]]',
- breadcrumbs: helpers.buildBreadcrumbs([{text: '[[pages:chats]]'}])
- });
- }
-
- async.waterfall([
- function (next) {
- messaging.isUserInRoom(req.uid, req.params.roomid, next);
- },
- function (inRoom, next) {
- if (!inRoom) {
- return callback();
- }
-
- async.parallel({
- users: async.apply(messaging.getUsersInRoom, req.params.roomid, 0, -1),
- messages: async.apply(messaging.getMessages, {
- uid: req.uid,
- roomId: req.params.roomid,
- since: 'recent',
- isNew: false
- }),
- room: async.apply(messaging.getRoomData, req.params.roomid)
- }, next);
- }
- ], function(err, data) {
- if (err) {
- return callback(err);
- }
- var room = data.room;
- room.messages = data.messages;
-
- room.isOwner = parseInt(room.owner, 10) === parseInt(req.uid, 10);
- room.users = data.users.filter(function(user) {
- return user && parseInt(user.uid, 10) && parseInt(user.uid, 10) !== req.uid;
- });
-
- room.rooms = recentChats.rooms;
- room.nextStart = recentChats.nextStart;
- room.title = room.roomName;
- room.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[pages:chats]]', url: '/chats'}, {text: room.roomName}]);
- room.maximumUsersInChatRoom = parseInt(meta.config.maximumUsersInChatRoom, 10) || 0;
- room.maximumChatMessageLength = parseInt(meta.config.maximumChatMessageLength, 10) || 1000;
- room.showUserInput = !room.maximumUsersInChatRoom || room.maximumUsersInChatRoom > 2;
-
- res.render('chats', room);
+ room.isOwner = parseInt(room.owner, 10) === parseInt(req.uid, 10);
+ room.users = data.users.filter(function(user) {
+ return user && parseInt(user.uid, 10) && parseInt(user.uid, 10) !== req.uid;
});
+
+ room.groupChat = room.hasOwnProperty('groupChat') ? room.groupChat : room.users.length > 2;
+ room.rooms = recentChats.rooms;
+ room.uid = uid;
+ room.userslug = req.params.userslug;
+ room.nextStart = recentChats.nextStart;
+ room.title = room.roomName;
+ room.breadcrumbs = helpers.buildBreadcrumbs([
+ {text: username, url: '/user/' + req.params.userslug},
+ {text: '[[pages:chats]]', url: '/user/' + req.params.userslug + '/chats'},
+ {text: room.roomName}
+ ]);
+ room.maximumUsersInChatRoom = parseInt(meta.config.maximumUsersInChatRoom, 10) || 0;
+ room.maximumChatMessageLength = parseInt(meta.config.maximumChatMessageLength, 10) || 1000;
+ room.showUserInput = !room.maximumUsersInChatRoom || room.maximumUsersInChatRoom > 2;
+
+ res.render('chats', room);
});
};
+chatsController.redirectToChat = function(req, res, next) {
+ var roomid = parseInt(req.params.roomid, 10);
+ if (!req.uid) {
+ return next();
+ }
+ user.getUserField(req.uid, 'userslug', function(err, userslug) {
+ if (err || !userslug) {
+ return next(err);
+ }
+
+ if (!roomid) {
+ return helpers.redirect(res, '/user/' + userslug + '/chats');
+ }
+ helpers.redirect(res, '/user/' + userslug + '/chats/' + roomid);
+ });
+};
+
+
+
module.exports = chatsController;
\ No newline at end of file
diff --git a/src/controllers/accounts/follow.js b/src/controllers/accounts/follow.js
index f9dc72c6f3..f54d825081 100644
--- a/src/controllers/accounts/follow.js
+++ b/src/controllers/accounts/follow.js
@@ -1,10 +1,11 @@
'use strict';
-var async = require('async'),
+var async = require('async');
- user = require('../../user'),
- helpers = require('../helpers'),
- accountHelpers = require('./helpers');
+var user = require('../../user');
+var helpers = require('../helpers');
+var accountHelpers = require('./helpers');
+var pagination = require('../../pagination');
var followController = {};
@@ -19,9 +20,14 @@ followController.getFollowers = function(req, res, next) {
function getFollow(tpl, name, req, res, callback) {
var userData;
+ var page = parseInt(req.query.page, 10) || 1;
+ var resultsPerPage = 50;
+ var start = Math.max(0, page - 1) * resultsPerPage;
+ var stop = start + resultsPerPage - 1;
+
async.waterfall([
function(next) {
- accountHelpers.getBaseUser(req.params.userslug, req.uid, next);
+ accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next);
},
function(data, next) {
userData = data;
@@ -29,7 +35,7 @@ function getFollow(tpl, name, req, res, callback) {
return callback();
}
var method = name === 'following' ? 'getFollowing' : 'getFollowers';
- user[method](userData.uid, 0, 49, next);
+ user[method](userData.uid, start, stop, next);
}
], function(err, users) {
if (err) {
@@ -37,8 +43,10 @@ function getFollow(tpl, name, req, res, callback) {
}
userData.users = users;
- userData.nextStart = 50;
userData.title = '[[pages:' + tpl + ', ' + userData.username + ']]';
+ var count = name === 'following' ? userData.followingCount : userData.followerCount;
+ var pageCount = Math.ceil(count / resultsPerPage);
+ userData.pagination = pagination.create(page, pageCount);
userData.breadcrumbs = helpers.buildBreadcrumbs([{text: userData.username, url: '/user/' + userData.userslug}, {text: '[[user:' + name + ']]'}]);
res.render(tpl, userData);
diff --git a/src/controllers/accounts/groups.js b/src/controllers/accounts/groups.js
index e19034c908..7957b3c251 100644
--- a/src/controllers/accounts/groups.js
+++ b/src/controllers/accounts/groups.js
@@ -1,11 +1,11 @@
'use strict';
-var async = require('async'),
+var async = require('async');
- groups = require('../../groups'),
- helpers = require('../helpers'),
- accountHelpers = require('./helpers');
+var groups = require('../../groups');
+var helpers = require('../helpers');
+var accountHelpers = require('./helpers');
var groupsController = {};
@@ -15,7 +15,7 @@ groupsController.get = function(req, res, callback) {
var groupsData;
async.waterfall([
function (next) {
- accountHelpers.getBaseUser(req.params.userslug, req.uid, next);
+ accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next);
},
function (_userData, next) {
userData = _userData;
diff --git a/src/controllers/accounts/helpers.js b/src/controllers/accounts/helpers.js
index bed89c030c..7af9cfa3e4 100644
--- a/src/controllers/accounts/helpers.js
+++ b/src/controllers/accounts/helpers.js
@@ -3,6 +3,7 @@
var async = require('async');
var validator = require('validator');
+var winston = require('winston');
var user = require('../../user');
var groups = require('../../groups');
@@ -35,12 +36,18 @@ helpers.getUserDataByUserSlug = function(userslug, callerUID, callback) {
isGlobalModerator: function(next) {
user.isGlobalModerator(callerUID, next);
},
+ isFollowing: function(next) {
+ user.isFollowing(callerUID, uid, next);
+ },
ips: function(next) {
user.getIPs(uid, 4, next);
},
profile_links: function(next) {
plugins.fireHook('filter:user.profileLinks', [], next);
},
+ profile_menu: function(next) {
+ plugins.fireHook('filter:user.profileMenu', {uid: uid, callerUID: callerUID, links: []}, next);
+ },
groups: function(next) {
groups.getUserGroups([uid], next);
},
@@ -80,36 +87,44 @@ helpers.getUserDataByUserSlug = function(userslug, callerUID, callback) {
userData.ips = results.ips;
}
+ if (!isAdmin && !isGlobalModerator) {
+ userData.moderationNote = undefined;
+ }
+
userData.uid = userData.uid;
userData.yourid = callerUID;
userData.theirid = userData.uid;
userData.isAdmin = isAdmin;
userData.isGlobalModerator = isGlobalModerator;
+ userData.isAdminOrGlobalModerator = isAdmin || isGlobalModerator;
userData.canBan = isAdmin || isGlobalModerator;
userData.canChangePassword = isAdmin || (isSelf && parseInt(meta.config['password:disableEdit'], 10) !== 1);
userData.isSelf = isSelf;
+ userData.isFollowing = results.isFollowing;
userData.showHidden = isSelf || isAdmin || isGlobalModerator;
userData.groups = Array.isArray(results.groups) && results.groups.length ? results.groups[0] : [];
userData.disableSignatures = meta.config.disableSignatures !== undefined && parseInt(meta.config.disableSignatures, 10) === 1;
userData['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1;
userData['downvote:disabled'] = parseInt(meta.config['downvote:disabled'], 10) === 1;
userData['email:confirmed'] = !!parseInt(userData['email:confirmed'], 10);
- userData.profile_links = filterLinks(results.profile_links, isSelf);
+ userData.profile_links = filterLinks(results.profile_links.concat(results.profile_menu.links), isSelf);
+
userData.sso = results.sso.associations;
userData.status = user.getStatus(userData);
userData.banned = parseInt(userData.banned, 10) === 1;
- userData.website = validator.escape(userData.website || '');
+ userData.website = validator.escape(String(userData.website || ''));
userData.websiteLink = !userData.website.startsWith('http') ? 'http://' + userData.website : userData.website;
userData.websiteName = userData.website.replace(validator.escape('http://'), '').replace(validator.escape('https://'), '');
userData.followingCount = parseInt(userData.followingCount, 10) || 0;
userData.followerCount = parseInt(userData.followerCount, 10) || 0;
- userData.email = validator.escape(userData.email || '');
- userData.fullname = validator.escape(userData.fullname || '');
- userData.location = validator.escape(userData.location || '');
- userData.signature = validator.escape(userData.signature || '');
- userData.aboutme = validator.escape(userData.aboutme || '');
- userData.birthday = validator.escape(userData.birthday || '');
+ userData.email = validator.escape(String(userData.email || ''));
+ userData.fullname = validator.escape(String(userData.fullname || ''));
+ userData.location = validator.escape(String(userData.location || ''));
+ userData.signature = validator.escape(String(userData.signature || ''));
+ userData.aboutme = validator.escape(String(userData.aboutme || ''));
+ userData.birthday = validator.escape(String(userData.birthday || ''));
+ userData.moderationNote = validator.escape(String(userData.moderationNote || ''));
userData['cover:url'] = userData['cover:url'] || require('../../coverPhoto').getDefaultProfileCover(userData.uid);
userData['cover:position'] = userData['cover:position'] || '50% 50%';
@@ -123,54 +138,8 @@ helpers.getUserDataByUserSlug = function(userslug, callerUID, callback) {
helpers.getBaseUser = function(userslug, callerUID, callback) {
- async.waterfall([
- function (next) {
- user.getUidByUserslug(userslug, next);
- },
- function (uid, next) {
- if (!uid) {
- return callback(null, null);
- }
-
- async.parallel({
- user: function(next) {
- user.getUserFields(uid, ['uid', 'username', 'userslug', 'picture', 'cover:url', 'cover:position', 'status', 'lastonline', 'groupTitle'], next);
- },
- isAdmin: function(next) {
- user.isAdministrator(callerUID, next);
- },
- isGlobalModerator: function(next) {
- user.isGlobalModerator(callerUID, next);
- },
- isFollowing: function(next) {
- user.isFollowing(callerUID, uid, next);
- },
- profile_links: function(next) {
- plugins.fireHook('filter:user.profileLinks', [], next);
- }
- }, next);
- },
- function (results, next) {
- if (!results.user) {
- return callback();
- }
-
- results.user.yourid = callerUID;
- results.user.theirid = results.user.uid;
- results.user.status = user.getStatus(results.user);
- results.user.isSelf = parseInt(callerUID, 10) === parseInt(results.user.uid, 10);
- results.user.isFollowing = results.isFollowing;
- results.user.showHidden = results.user.isSelf || results.isAdmin || results.isGlobalModerator;
- results.user.profile_links = filterLinks(results.profile_links, results.user.isSelf);
-
- results.user['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1;
- results.user['downvote:disabled'] = parseInt(meta.config['downvote:disabled'], 10) === 1;
- results.user['cover:url'] = results.user['cover:url'] || require('../../coverPhoto').getDefaultProfileCover(results.user.uid);
- results.user['cover:position'] = results.user['cover:position'] || '50% 50%';
-
- next(null, results.user);
- }
- ], callback);
+ winston.warn('helpers.getBaseUser deprecated please use helpers.getUserDataByUserSlug');
+ helpers.getUserDataByUserSlug(userslug, callerUID, callback);
};
function filterLinks(links, self) {
diff --git a/src/controllers/accounts/info.js b/src/controllers/accounts/info.js
index 6662c60cd6..63dea030ec 100644
--- a/src/controllers/accounts/info.js
+++ b/src/controllers/accounts/info.js
@@ -1,31 +1,44 @@
'use strict';
-var async = require('async'),
- _ = require('underscore'),
+var async = require('async');
- user = require('../../user'),
- helpers = require('../helpers'),
- accountHelpers = require('./helpers');
+var user = require('../../user');
+var helpers = require('../helpers');
+var accountHelpers = require('./helpers');
var infoController = {};
-infoController.get = function(req, res, next) {
- accountHelpers.getBaseUser(req.params.userslug, req.uid, function(err, userData) {
- async.parallel({
- ips: async.apply(user.getIPs, res.locals.uid, 4),
- history: async.apply(user.getModerationHistory, res.locals.uid),
- fields: async.apply(user.getUserFields, res.locals.uid, ['banned'])
- }, function(err, data) {
- data = _.extend(userData, {
- ips: data.ips,
- history: data.history
- }, data.fields);
+infoController.get = function(req, res, callback) {
+ var userData;
+ async.waterfall([
+ function(next) {
+ accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next);
+ },
+ function(_userData, next) {
+ userData = _userData;
+ if (!userData) {
+ return callback();
+ }
+ async.parallel({
+ history: async.apply(user.getModerationHistory, userData.uid),
+ sessions: async.apply(user.auth.getSessions, userData.uid, req.sessionID),
+ usernames: async.apply(user.getHistory, 'user:' + userData.uid + ':usernames'),
+ emails: async.apply(user.getHistory, 'user:' + userData.uid + ':emails')
+ }, next);
+ }
+ ], function(err, data) {
+ if (err) {
+ return callback(err);
+ }
- userData.title = '[[pages:account/info]]';
- userData.breadcrumbs = helpers.buildBreadcrumbs([{text: userData.username, url: '/user/' + userData.userslug}, {text: '[[user:settings]]'}]);
+ userData.history = data.history;
+ userData.sessions = data.sessions;
+ userData.usernames = data.usernames;
+ userData.emails = data.emails;
+ userData.title = '[[pages:account/info]]';
+ userData.breadcrumbs = helpers.buildBreadcrumbs([{text: userData.username, url: '/user/' + userData.userslug}, {text: '[[user:account_info]]'}]);
- res.render('account/info', data);
- });
+ res.render('account/info', userData);
});
};
diff --git a/src/controllers/accounts/posts.js b/src/controllers/accounts/posts.js
index 7e1e67a8bd..08737f0cde 100644
--- a/src/controllers/accounts/posts.js
+++ b/src/controllers/accounts/posts.js
@@ -1,15 +1,15 @@
'use strict';
-var async = require('async'),
+var async = require('async');
- db = require('../../database'),
- user = require('../../user'),
- posts = require('../../posts'),
- topics = require('../../topics'),
- pagination = require('../../pagination'),
- helpers = require('../helpers'),
- accountHelpers = require('./helpers');
+var db = require('../../database');
+var user = require('../../user');
+var posts = require('../../posts');
+var topics = require('../../topics');
+var pagination = require('../../pagination');
+var helpers = require('../helpers');
+var accountHelpers = require('./helpers');
var postsController = {};
@@ -103,7 +103,7 @@ function getFromUserSet(data, req, res, next) {
user.getSettings(req.uid, next);
},
userData: function(next) {
- accountHelpers.getBaseUser(req.params.userslug, req.uid, next);
+ accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next);
}
}, function(err, results) {
if (err || !results.userData) {
diff --git a/src/controllers/accounts/profile.js b/src/controllers/accounts/profile.js
index 74e919d8f5..ee66a1a4d7 100644
--- a/src/controllers/accounts/profile.js
+++ b/src/controllers/accounts/profile.js
@@ -118,6 +118,9 @@ profileController.get = function(req, res, callback) {
}
);
}
+ userData.selectedGroup = userData.groups.find(function(group) {
+ return group && group.name === userData.groupTitle;
+ });
plugins.fireHook('filter:user.account', {userData: userData, uid: req.uid}, next);
}
diff --git a/src/controllers/accounts/session.js b/src/controllers/accounts/session.js
index dd9f0f4a88..20c32909df 100644
--- a/src/controllers/accounts/session.js
+++ b/src/controllers/accounts/session.js
@@ -13,10 +13,17 @@ sessionController.revoke = function(req, res, next) {
}
var _id;
-
+ var uid;
async.waterfall([
function (next) {
- db.getSortedSetRange('uid:' + res.locals.uid + ':sessions', 0, -1, next);
+ user.getUidByUserslug(req.params.userslug, next);
+ },
+ function (_uid, next) {
+ uid = _uid;
+ if (!uid) {
+ return next(new Error('[[error:no-session-found]]'));
+ }
+ db.getSortedSetRange('uid:' + uid + ':sessions', 0, -1, next);
},
function (sids, done) {
async.eachSeries(sids, function(sid, next) {
@@ -38,7 +45,7 @@ sessionController.revoke = function(req, res, next) {
return next(new Error('[[error:no-session-found]]'));
}
- user.auth.revokeSession(_id, res.locals.uid, next);
+ user.auth.revokeSession(_id, uid, next);
}
], function(err) {
if (err) {
diff --git a/src/controllers/accounts/settings.js b/src/controllers/accounts/settings.js
index 483a7e5eb1..9b16050934 100644
--- a/src/controllers/accounts/settings.js
+++ b/src/controllers/accounts/settings.js
@@ -3,7 +3,6 @@
var async = require('async');
var user = require('../../user');
-var groups = require('../../groups');
var languages = require('../../languages');
var meta = require('../../meta');
var plugins = require('../../plugins');
@@ -21,7 +20,7 @@ settingsController.get = function(req, res, callback) {
var userData;
async.waterfall([
function(next) {
- accountHelpers.getBaseUser(req.params.userslug, req.uid, next);
+ accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next);
},
function(_userData, next) {
userData = _userData;
@@ -32,30 +31,37 @@ settingsController.get = function(req, res, callback) {
settings: function(next) {
user.getSettings(userData.uid, next);
},
- userGroups: function(next) {
- groups.getUserGroupsFromSet('groups:createtime', [userData.uid], next);
- },
languages: function(next) {
languages.list(next);
},
homePageRoutes: function(next) {
getHomePageRoutes(next);
},
- ips: function (next) {
- user.getIPs(userData.uid, 4, next);
+ sounds: function(next) {
+ meta.sounds.getFiles(next);
},
- sessions: async.apply(user.auth.getSessions, userData.uid, req.sessionID)
+ soundsMapping: function(next) {
+ meta.sounds.getMapping(userData.uid, next);
+ }
}, next);
},
function(results, next) {
userData.settings = results.settings;
- userData.userGroups = results.userGroups[0].filter(function(group) {
- return group && group.userTitleEnabled && !groups.isPrivilegeGroup(group.name) && group.name !== 'registered-users';
- });
userData.languages = results.languages;
userData.homePageRoutes = results.homePageRoutes;
- userData.ips = results.ips;
- userData.sessions = results.sessions;
+
+ var soundSettings = {
+ 'notificationSound': 'notification',
+ 'incomingChatSound': 'chat-incoming',
+ 'outgoingChatSound': 'chat-outgoing'
+ };
+
+ Object.keys(soundSettings).forEach(function(setting) {
+ userData[setting] = Object.keys(results.sounds).map(function(name) {
+ return {name: name, selected: name === results.soundsMapping[soundSettings[setting]]};
+ });
+ });
+
plugins.fireHook('filter:user.customSettings', {settings: results.settings, customSettings: [], uid: req.uid}, next);
},
function(data, next) {
@@ -118,10 +124,6 @@ settingsController.get = function(req, res, callback) {
skin.selected = skin.value === userData.settings.bootswatchSkin;
});
- userData.userGroups.forEach(function(group) {
- group.selected = group.name === userData.settings.groupTitle;
- });
-
userData.languages.forEach(function(language) {
language.selected = language.code === userData.settings.userLang;
});
diff --git a/src/controllers/admin.js b/src/controllers/admin.js
index c3ce96d205..7f622466cd 100644
--- a/src/controllers/admin.js
+++ b/src/controllers/admin.js
@@ -16,7 +16,7 @@ var adminController = {
logs: require('./admin/logs'),
errors: require('./admin/errors'),
database: require('./admin/database'),
- postCache: require('./admin/postCache'),
+ cache: require('./admin/cache'),
plugins: require('./admin/plugins'),
languages: require('./admin/languages'),
settings: require('./admin/settings'),
diff --git a/src/controllers/admin/cache.js b/src/controllers/admin/cache.js
new file mode 100644
index 0000000000..116c928020
--- /dev/null
+++ b/src/controllers/admin/cache.js
@@ -0,0 +1,35 @@
+'use strict';
+
+var cacheController = {};
+
+cacheController.get = function(req, res, next) {
+ var postCache = require('../../posts/cache');
+ var groupCache = require('../../groups').cache;
+
+ var avgPostSize = 0;
+ var percentFull = 0;
+ if (postCache.itemCount > 0) {
+ avgPostSize = parseInt((postCache.length / postCache.itemCount), 10);
+ percentFull = ((postCache.length / postCache.max) * 100).toFixed(2);
+ }
+
+ res.render('admin/advanced/cache', {
+ postCache: {
+ length: postCache.length,
+ max: postCache.max,
+ itemCount: postCache.itemCount,
+ percentFull: percentFull,
+ avgPostSize: avgPostSize
+ },
+ groupCache: {
+ length: groupCache.length,
+ max: groupCache.max,
+ itemCount: groupCache.itemCount,
+ percentFull: ((groupCache.length / groupCache.max) * 100).toFixed(2),
+ dump: req.query.debug ? JSON.stringify(groupCache.dump(), null, 4) : false
+ }
+ });
+};
+
+
+module.exports = cacheController;
\ No newline at end of file
diff --git a/src/controllers/admin/categories.js b/src/controllers/admin/categories.js
index 04001e0f65..c33dde5c7c 100644
--- a/src/controllers/admin/categories.js
+++ b/src/controllers/admin/categories.js
@@ -48,6 +48,10 @@ categoriesController.getAnalytics = function(req, res, next) {
name: async.apply(categories.getCategoryField, req.params.category_id, 'name'),
analytics: async.apply(analytics.getCategoryAnalytics, req.params.category_id)
}, function(err, data) {
+ if (err) {
+ return next(err);
+ }
+
res.render('admin/manage/category-analytics', data);
});
};
diff --git a/src/controllers/admin/dashboard.js b/src/controllers/admin/dashboard.js
index 24a65983f1..84ee4fd3d0 100644
--- a/src/controllers/admin/dashboard.js
+++ b/src/controllers/admin/dashboard.js
@@ -19,8 +19,8 @@ dashboardController.get = function(req, res, next) {
var notices = [
{
done: !meta.reloadRequired,
- doneText: 'Reload not required',
- notDoneText:'Reload required'
+ doneText: 'Restart not required',
+ notDoneText:'Restart required'
},
{
done: plugins.hasListeners('filter:search.query'),
diff --git a/src/controllers/admin/errors.js b/src/controllers/admin/errors.js
index 4b59932965..b41d6bdab4 100644
--- a/src/controllers/admin/errors.js
+++ b/src/controllers/admin/errors.js
@@ -1,27 +1,35 @@
'use strict';
-var async = require('async'),
- json2csv = require('json-2-csv').json2csv;
+var async = require('async');
+var json2csv = require('json-2-csv').json2csv;
-var meta = require('../../meta'),
- analytics = require('../../analytics');
+var meta = require('../../meta');
+var analytics = require('../../analytics');
var errorsController = {};
-errorsController.get = function(req, res) {
+errorsController.get = function(req, res, next) {
async.parallel({
'not-found': async.apply(meta.errors.get, true),
analytics: async.apply(analytics.getErrorAnalytics)
}, function(err, data) {
+ if (err) {
+ return next(err);
+ }
+
res.render('admin/advanced/errors', data);
});
};
-errorsController.export = function(req, res) {
+errorsController.export = function(req, res, next) {
async.waterfall([
async.apply(meta.errors.get, false),
async.apply(json2csv)
], function(err, csv) {
+ if (err) {
+ return next(err);
+ }
+
res.set('Content-Type', 'text/csv').set('Content-Disposition', 'attachment; filename="404.csv"').send(csv);
});
};
diff --git a/src/controllers/admin/events.js b/src/controllers/admin/events.js
index ceee1e2a70..9f7bd9a2f2 100644
--- a/src/controllers/admin/events.js
+++ b/src/controllers/admin/events.js
@@ -1,18 +1,38 @@
'use strict';
+var async = require('async');
+
+var db = require('../../database');
var events = require('../../events');
+var pagination = require('../../pagination');
var eventsController = {};
eventsController.get = function(req, res, next) {
- events.getEvents(0, 19, function(err, events) {
+
+ var page = parseInt(req.query.page, 10) || 1;
+ var itemsPerPage = 20;
+ var start = (page - 1) * itemsPerPage;
+ var stop = start + itemsPerPage - 1;
+
+ async.parallel({
+ eventCount: function(next) {
+ db.sortedSetCard('events:time', next);
+ },
+ events: function(next) {
+ events.getEvents(start, stop, next);
+ }
+ }, function(err, results) {
if (err) {
return next(err);
}
+ var pageCount = Math.max(1, Math.ceil(results.eventCount / itemsPerPage));
+
res.render('admin/advanced/events', {
- events: events,
+ events: results.events,
+ pagination: pagination.create(page, pageCount),
next: 20
});
});
diff --git a/src/controllers/admin/flags.js b/src/controllers/admin/flags.js
index b94c282094..b5edc7b5be 100644
--- a/src/controllers/admin/flags.js
+++ b/src/controllers/admin/flags.js
@@ -2,47 +2,97 @@
var async = require('async');
var validator = require('validator');
+
var posts = require('../../posts');
+var user = require('../../user');
+var categories = require('../../categories');
var analytics = require('../../analytics');
+var pagination = require('../../pagination');
var flagsController = {};
-flagsController.get = function(req, res, next) {
- var sortBy = req.query.sortBy || 'count';
- var byUsername = req.query.byUsername || '';
- var start = 0;
- var stop = 19;
+var itemsPerPage = 20;
- async.waterfall([
- function (next) {
- async.parallel({
- posts: function(next) {
- if (byUsername) {
- posts.getUserFlags(byUsername, sortBy, req.uid, start, stop, next);
- } else {
- var set = sortBy === 'count' ? 'posts:flags:count' : 'posts:flagged';
- posts.getFlags(set, req.uid, start, stop, next);
- }
- },
- analytics: function(next) {
- analytics.getDailyStatsForSet('analytics:flags', Date.now(), 30, next);
- }
- }, next);
+flagsController.get = function(req, res, next) {
+ var byUsername = req.query.byUsername || '';
+ var cid = req.query.cid || 0;
+ var sortBy = req.query.sortBy || 'count';
+ var page = parseInt(req.query.page, 10) || 1;
+
+ async.parallel({
+ categories: function(next) {
+ categories.buildForSelect(req.uid, next);
+ },
+ flagData: function(next) {
+ getFlagData(req, next);
+ },
+ analytics: function(next) {
+ analytics.getDailyStatsForSet('analytics:flags', Date.now(), 30, next);
+ },
+ assignees: function(next) {
+ user.getAdminsandGlobalMods(next);
}
- ], function (err, results) {
+ }, function (err, results) {
if (err) {
return next(err);
}
+
+ // Minimise data set for assignees so tjs does less work
+ results.assignees = results.assignees.map(function(userObj) {
+ return {
+ uid: userObj.uid,
+ username: userObj.username
+ };
+ });
+
+ var pageCount = Math.max(1, Math.ceil(results.flagData.count / itemsPerPage));
+
+ results.categories.forEach(function(category) {
+ category.selected = parseInt(category.cid, 10) === parseInt(cid, 10);
+ });
+
var data = {
- posts: results.posts,
+ posts: results.flagData.posts,
+ assignees: results.assignees,
analytics: results.analytics,
- next: stop + 1,
+ categories: results.categories,
byUsername: validator.escape(String(byUsername)),
+ sortByCount: sortBy === 'count',
+ sortByTime: sortBy === 'time',
+ pagination: pagination.create(page, pageCount, req.query),
title: '[[pages:flagged-posts]]'
};
res.render('admin/manage/flags', data);
});
};
+function getFlagData(req, callback) {
+ var sortBy = req.query.sortBy || 'count';
+ var byUsername = req.query.byUsername || '';
+ var cid = req.query.cid || 0;
+ var page = parseInt(req.query.page, 10) || 1;
+ var start = (page - 1) * itemsPerPage;
+ var stop = start + itemsPerPage - 1;
+
+ var sets = [sortBy === 'count' ? 'posts:flags:count' : 'posts:flagged'];
+
+ async.waterfall([
+ function(next) {
+ if (byUsername) {
+ user.getUidByUsername(byUsername, next);
+ } else {
+ process.nextTick(next, null, 0);
+ }
+ },
+ function(uid, next) {
+ if (uid) {
+ sets.push('uid:' + uid + ':flag:pids');
+ }
+
+ posts.getFlags(sets, cid, req.uid, start, stop, next);
+ }
+ ], callback);
+}
+
module.exports = flagsController;
diff --git a/src/controllers/admin/homepage.js b/src/controllers/admin/homepage.js
index f503dd8865..89ef0db928 100644
--- a/src/controllers/admin/homepage.js
+++ b/src/controllers/admin/homepage.js
@@ -49,6 +49,10 @@ homePageController.get = function(req, res, next) {
name: 'Popular'
}
].concat(categoryData)}, function(err, data) {
+ if (err) {
+ return next(err);
+ }
+
data.routes.push({
route: '',
name: 'Custom'
diff --git a/src/controllers/admin/info.js b/src/controllers/admin/info.js
index 2459ad7140..80b0829487 100644
--- a/src/controllers/admin/info.js
+++ b/src/controllers/admin/info.js
@@ -25,7 +25,7 @@ infoController.get = function(req, res, next) {
return (a.os.hostname < b.os.hostname) ? -1 : (a.os.hostname > b.os.hostname) ? 1 : 0;
});
res.render('admin/development/info', {info: data, infoJSON: JSON.stringify(data, null, 4), host: os.hostname(), port: nconf.get('port')});
- }, 300);
+ }, 500);
};
pubsub.on('sync:node:info:start', function() {
@@ -62,9 +62,8 @@ function getNodeInfo(callback) {
};
async.parallel({
- pubsub: function(next) {
- pubsub.publish('sync:stats:start');
- next();
+ stats: function(next) {
+ rooms.getLocalStats(next);
},
gitInfo: function(next) {
getGitInfo(next);
@@ -74,7 +73,7 @@ function getNodeInfo(callback) {
return callback(err);
}
data.git = results.gitInfo;
- data.stats = rooms.stats[data.os.hostname + ':' + data.process.port];
+ data.stats = results.stats;
callback(null, data);
});
}
diff --git a/src/controllers/admin/postCache.js b/src/controllers/admin/postCache.js
deleted file mode 100644
index bbfd222586..0000000000
--- a/src/controllers/admin/postCache.js
+++ /dev/null
@@ -1,26 +0,0 @@
-'use strict';
-
-var postCacheController = {};
-
-postCacheController.get = function(req, res, next) {
- var cache = require('../../posts/cache');
- var avgPostSize = 0;
- var percentFull = 0;
- if (cache.itemCount > 0) {
- avgPostSize = parseInt((cache.length / cache.itemCount), 10);
- percentFull = ((cache.length / cache.max) * 100).toFixed(2);
- }
-
- res.render('admin/advanced/post-cache', {
- cache: {
- length: cache.length,
- max: cache.max,
- itemCount: cache.itemCount,
- percentFull: percentFull,
- avgPostSize: avgPostSize
- }
- });
-};
-
-
-module.exports = postCacheController;
\ No newline at end of file
diff --git a/src/controllers/admin/settings.js b/src/controllers/admin/settings.js
index bf0975058a..13e7b42722 100644
--- a/src/controllers/admin/settings.js
+++ b/src/controllers/admin/settings.js
@@ -19,19 +19,27 @@ settingsController.get = function(req, res, next) {
function renderEmail(req, res, next) {
- var fs = require('fs'),
- path = require('path'),
- utils = require('../../../public/src/utils');
+ var fs = require('fs');
+ var path = require('path');
+ var utils = require('../../../public/src/utils');
var emailsPath = path.join(__dirname, '../../../public/templates/emails');
utils.walk(emailsPath, function(err, emails) {
+ if (err) {
+ return next(err);
+ }
+
async.map(emails, function(email, next) {
var path = email.replace(emailsPath, '').substr(1).replace('.tpl', '');
fs.readFile(email, function(err, original) {
+ if (err) {
+ return next(err);
+ }
+
var text = meta.config['email:custom:' + path] ? meta.config['email:custom:' + path] : original.toString();
- next(err, {
+ next(null, {
path: path,
fullpath: email,
text: text,
@@ -39,6 +47,10 @@ function renderEmail(req, res, next) {
});
});
}, function(err, emails) {
+ if (err) {
+ return next(err);
+ }
+
res.render('admin/settings/email', {
emails: emails,
sendable: emails.filter(function(email) {
diff --git a/src/controllers/admin/sounds.js b/src/controllers/admin/sounds.js
index 6e7ebf3f19..b583125c5a 100644
--- a/src/controllers/admin/sounds.js
+++ b/src/controllers/admin/sounds.js
@@ -6,6 +6,10 @@ var soundsController = {};
soundsController.get = function(req, res, next) {
meta.sounds.getFiles(function(err, sounds) {
+ if (err) {
+ return next(err);
+ }
+
sounds = Object.keys(sounds).map(function(name) {
return {
name: name
diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js
index 90dae8a243..b507e092cf 100644
--- a/src/controllers/admin/uploads.js
+++ b/src/controllers/admin/uploads.js
@@ -61,6 +61,10 @@ uploadsController.uploadTouchIcon = function(req, res, next) {
if (validateUpload(req, res, next, uploadedFile, allowedTypes)) {
file.saveFileToLocal('touchicon-orig.png', 'system', uploadedFile.path, function(err, imageObj) {
+ if (err) {
+ return next(err);
+ }
+
// Resize the image into squares for use as touch icons at various DPIs
async.each(sizes, function(size, next) {
async.series([
diff --git a/src/controllers/admin/users.js b/src/controllers/admin/users.js
index e682045fce..6be5af1e73 100644
--- a/src/controllers/admin/users.js
+++ b/src/controllers/admin/users.js
@@ -5,7 +5,8 @@ var user = require('../../user');
var meta = require('../../meta');
var db = require('../../database');
var pagination = require('../../pagination');
-
+var events = require('../../events');
+var plugins = require('../../plugins');
var usersController = {};
@@ -86,6 +87,7 @@ usersController.registrationQueue = function(req, res, next) {
var start = (page - 1) * 20;
var stop = start + itemsPerPage - 1;
var invitations;
+
async.parallel({
registrationQueueCount: function(next) {
db.sortedSetCard('registration:queue', next);
@@ -93,6 +95,9 @@ usersController.registrationQueue = function(req, res, next) {
users: function(next) {
user.getRegistrationQueue(start, stop, next);
},
+ customHeaders: function(next) {
+ plugins.fireHook('filter:admin.registrationQueue.customHeaders', {headers: []}, next);
+ },
invites: function(next) {
async.waterfall([
function(next) {
@@ -131,6 +136,7 @@ usersController.registrationQueue = function(req, res, next) {
}
var pageCount = Math.max(1, Math.ceil(data.registrationQueueCount / itemsPerPage));
data.pagination = pagination.create(page, pageCount);
+ data.customHeaders = data.customHeaders.headers;
res.render('admin/manage/registration', data);
});
};
@@ -180,6 +186,12 @@ function render(req, res, data) {
}
usersController.getCSV = function(req, res, next) {
+ events.log({
+ type: 'getUsersCSV',
+ uid: req.user.uid,
+ ip: req.ip
+ });
+
user.getUsersCSV(function(err, data) {
if (err) {
return next(err);
diff --git a/src/controllers/api.js b/src/controllers/api.js
index 706a36cef9..42cc74e7e7 100644
--- a/src/controllers/api.js
+++ b/src/controllers/api.js
@@ -12,7 +12,6 @@ var categories = require('../categories');
var privileges = require('../privileges');
var plugins = require('../plugins');
var widgets = require('../widgets');
-var helpers = require('../controllers/helpers');
var accountHelpers = require('../controllers/accounts/helpers');
var apiController = {};
@@ -22,8 +21,8 @@ apiController.getConfig = function(req, res, next) {
config.environment = process.env.NODE_ENV;
config.relative_path = nconf.get('relative_path');
config.version = nconf.get('version');
- config.siteTitle = validator.escape(meta.config.title || meta.config.browserTitle || 'NodeBB');
- config.browserTitle = validator.escape(meta.config.browserTitle || meta.config.title || 'NodeBB');
+ config.siteTitle = validator.escape(String(meta.config.title || meta.config.browserTitle || 'NodeBB'));
+ config.browserTitle = validator.escape(String(meta.config.browserTitle || meta.config.title || 'NodeBB'));
config.titleLayout = (meta.config.titleLayout || '{pageTitle} | {browserTitle}').replace(/{/g, '{').replace(/}/g, '}');
config.showSiteTitle = parseInt(meta.config.showSiteTitle, 10) === 1;
config.minimumTitleLength = meta.config.minimumTitleLength;
@@ -53,7 +52,7 @@ apiController.getConfig = function(req, res, next) {
config['theme:id'] = meta.config['theme:id'];
config['theme:src'] = meta.config['theme:src'];
config.defaultLang = meta.config.defaultLang || 'en_GB';
- config.userLang = req.query.lang ? validator.escape(req.query.lang) : config.defaultLang;
+ config.userLang = req.query.lang ? validator.escape(String(req.query.lang)) : config.defaultLang;
config.loggedIn = !!req.user;
config['cache-buster'] = meta.config['cache-buster'] || '';
config.requireEmailConfirmation = parseInt(meta.config.requireEmailConfirmation, 10) === 1;
@@ -68,25 +67,19 @@ apiController.getConfig = function(req, res, next) {
if (!req.user) {
return next(null, config);
}
- user.getSettings(req.uid, function(err, settings) {
- if (err) {
- return next(err);
- }
- config.usePagination = settings.usePagination;
- config.topicsPerPage = settings.topicsPerPage;
- config.postsPerPage = settings.postsPerPage;
- config.notificationSounds = settings.notificationSounds;
- config.userLang = (req.query.lang ? validator.escape(req.query.lang) : null) || settings.userLang || config.defaultLang;
- config.openOutgoingLinksInNewTab = settings.openOutgoingLinksInNewTab;
- config.topicPostSort = settings.topicPostSort || config.topicPostSort;
- config.categoryTopicSort = settings.categoryTopicSort || config.categoryTopicSort;
- config.topicSearchEnabled = settings.topicSearchEnabled || false;
- config.delayImageLoading = settings.delayImageLoading !== undefined ? settings.delayImageLoading : true;
- config.bootswatchSkin = settings.bootswatchSkin || config.bootswatchSkin;
- next(null, config);
- });
+ user.getSettings(req.uid, next);
},
- function (config, next) {
+ function (settings, next) {
+ config.usePagination = settings.usePagination;
+ config.topicsPerPage = settings.topicsPerPage;
+ config.postsPerPage = settings.postsPerPage;
+ config.userLang = (req.query.lang ? validator.escape(String(req.query.lang)) : null) || settings.userLang || config.defaultLang;
+ config.openOutgoingLinksInNewTab = settings.openOutgoingLinksInNewTab;
+ config.topicPostSort = settings.topicPostSort || config.topicPostSort;
+ config.categoryTopicSort = settings.categoryTopicSort || config.categoryTopicSort;
+ config.topicSearchEnabled = settings.topicSearchEnabled || false;
+ config.delayImageLoading = settings.delayImageLoading !== undefined ? settings.delayImageLoading : true;
+ config.bootswatchSkin = settings.bootswatchSkin || config.bootswatchSkin;
plugins.fireHook('filter:config.get', config, next);
}
], function(err, config) {
diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js
index bc301b1873..43707bfccd 100644
--- a/src/controllers/authentication.js
+++ b/src/controllers/authentication.js
@@ -6,6 +6,7 @@ var passport = require('passport');
var nconf = require('nconf');
var validator = require('validator');
var _ = require('underscore');
+var url = require('url');
var db = require('../database');
var meta = require('../meta');
@@ -96,6 +97,10 @@ function registerAndLoginUser(req, res, userData, callback) {
userData: userData,
interstitials: []
}, function(err, data) {
+ if (err) {
+ return next(err);
+ }
+
// If interstitials are found, save registration attempt into session and abort
var deferRegistration = data.interstitials.length;
@@ -144,6 +149,10 @@ authenticationController.registerComplete = function(req, res, next) {
userData: req.session.registration,
interstitials: []
}, function(err, data) {
+ if (err) {
+ return next(err);
+ }
+
var callbacks = data.interstitials.reduce(function(memo, cur) {
if (cur.hasOwnProperty('callback') && typeof cur.callback === 'function') {
memo.push(async.apply(cur.callback, req.session.registration, req.body));
@@ -160,7 +169,7 @@ authenticationController.registerComplete = function(req, res, next) {
} else {
res.redirect(nconf.get('relative_path') + '/');
}
- }
+ };
async.parallel(callbacks, function(err) {
if (err) {
@@ -179,7 +188,7 @@ authenticationController.registerComplete = function(req, res, next) {
});
};
-authenticationController.registerAbort = function(req, res, next) {
+authenticationController.registerAbort = function(req, res) {
// End the session and redirect to home
req.session.destroy(function() {
res.redirect(nconf.get('relative_path') + '/');
@@ -189,7 +198,11 @@ authenticationController.registerAbort = function(req, res, next) {
authenticationController.login = function(req, res, next) {
// Handle returnTo data
if (req.body.hasOwnProperty('returnTo') && !req.session.returnTo) {
- req.session.returnTo = req.body.returnTo;
+ // As req.body is data obtained via userland, it is untrusted, restrict to internal links only
+ var parsed = url.parse(req.body.returnTo);
+ var isInternal = utils.isInternalURI(url.parse(req.body.returnTo), nconf.get('url_parsed'), nconf.get('relative_path'));
+
+ req.session.returnTo = isInternal ? req.body.returnTo : nconf.get('url');
}
if (plugins.hasListeners('action:auth.overrideLogin')) {
@@ -243,6 +256,10 @@ function continueLogin(req, res, next) {
winston.verbose('[auth] Triggering password reset for uid ' + userData.uid + ' due to password policy');
req.session.passwordExpired = true;
user.reset.generate(userData.uid, function(err, code) {
+ if (err) {
+ return res.status(403).send(err.message);
+ }
+
res.status(200).send(nconf.get('relative_path') + '/reset/' + code);
});
} else {
@@ -305,6 +322,9 @@ authenticationController.onSuccessfulLogin = function(req, uid, callback) {
},
function (next) {
db.setObjectField('uid:' + uid + 'sessionUUID:sessionId', uuid, req.sessionID, next);
+ },
+ function (next) {
+ user.updateLastOnlineTime(uid, next);
}
], function(err) {
if (err) {
@@ -362,7 +382,16 @@ authenticationController.localLogin = function(req, username, password, next) {
return next(new Error('[[error:invalid-user-data]]'));
}
if (result.banned) {
- return next(new Error('[[error:user-banned]]'));
+ // Retrieve ban reason and show error
+ return user.getLatestBanInfo(uid, function(err, banInfo) {
+ if (err) {
+ next(err);
+ } else if (banInfo.reason) {
+ next(new Error('[[error:user-banned-reason, ' + banInfo.reason + ']]'));
+ } else {
+ next(new Error('[[error:user-banned]]'));
+ }
+ });
}
Password.compare(password, userData.password, next);
diff --git a/src/controllers/categories.js b/src/controllers/categories.js
index 642467c6b0..fa685e3705 100644
--- a/src/controllers/categories.js
+++ b/src/controllers/categories.js
@@ -13,10 +13,10 @@ var categoriesController = {};
categoriesController.list = function(req, res, next) {
res.locals.metaTags = [{
name: "title",
- content: validator.escape(meta.config.title || 'NodeBB')
+ content: validator.escape(String(meta.config.title || 'NodeBB'))
}, {
name: "description",
- content: validator.escape(meta.config.description || '')
+ content: validator.escape(String(meta.config.description || ''))
}, {
property: 'og:title',
content: '[[pages:categories]]'
diff --git a/src/controllers/category.js b/src/controllers/category.js
index 6c74519c1a..bf76c8e09f 100644
--- a/src/controllers/category.js
+++ b/src/controllers/category.js
@@ -20,6 +20,7 @@ categoryController.get = function(req, res, callback) {
var currentPage = parseInt(req.query.page, 10) || 1;
var pageCount = 1;
var userPrivileges;
+ var settings;
if ((req.params.topic_index && !utils.isNumber(req.params.topic_index)) || !utils.isNumber(cid)) {
return callback();
@@ -54,7 +55,7 @@ categoryController.get = function(req, res, callback) {
return helpers.redirect(res, '/category/' + results.categoryData.slug);
}
- var settings = results.userSettings;
+ settings = results.userSettings;
var topicIndex = utils.isNumber(req.params.topic_index) ? parseInt(req.params.topic_index, 10) - 1 : 0;
var topicCount = parseInt(results.categoryData.topic_count, 10);
pageCount = Math.max(1, Math.ceil(topicCount / settings.topicsPerPage));
@@ -89,7 +90,7 @@ categoryController.get = function(req, res, callback) {
var start = (currentPage - 1) * settings.topicsPerPage + topicIndex;
var stop = start + settings.topicsPerPage - 1;
- next(null, {
+ var payload = {
cid: cid,
set: set,
reverse: reverse,
@@ -97,19 +98,24 @@ categoryController.get = function(req, res, callback) {
stop: stop,
uid: req.uid,
settings: settings
- });
- },
- function (payload, next) {
- user.getUidByUserslug(req.query.author, function(err, uid) {
- payload.targetUid = uid;
- if (uid) {
- payload.set = 'cid:' + cid + ':uid:' + uid + ':tids';
+ };
+
+ async.waterfall([
+ function(next) {
+ user.getUidByUserslug(req.query.author, next);
+ },
+ function(uid, next) {
+ payload.targetUid = uid;
+ if (uid) {
+ payload.set = 'cid:' + cid + ':uid:' + uid + ':tids';
+ }
+
+ if (req.query.tag) {
+ payload.set = [payload.set, 'tag:' + req.query.tag + ':topics'];
+ }
+ categories.getCategoryById(payload, next);
}
- next(err, payload);
- });
- },
- function (payload, next) {
- categories.getCategoryById(payload, next);
+ ], next);
},
function (categoryData, next) {
@@ -190,9 +196,14 @@ categoryController.get = function(req, res, callback) {
}
];
+ if (parseInt(req.uid, 10)) {
+ categories.markAsRead([cid], req.uid);
+ }
+
categoryData['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1;
categoryData.rssFeedUrl = nconf.get('relative_path') + '/category/' + categoryData.cid + '.rss';
categoryData.title = categoryData.name;
+ pageCount = Math.max(1, Math.ceil(categoryData.topic_count / settings.topicsPerPage));
categoryData.pagination = pagination.create(currentPage, pageCount, req.query);
categoryData.pagination.rel.forEach(function(rel) {
rel.href = nconf.get('url') + '/category/' + categoryData.slug + rel.href;
diff --git a/src/controllers/groups.js b/src/controllers/groups.js
index 106df26075..55b408183a 100644
--- a/src/controllers/groups.js
+++ b/src/controllers/groups.js
@@ -46,23 +46,31 @@ groupsController.getGroupsFromSet = function(uid, sort, start, stop, callback) {
};
groupsController.details = function(req, res, callback) {
+ var groupName;
async.waterfall([
- async.apply(groups.exists, res.locals.groupName),
- function (exists, next) {
- if (!exists) {
+ function(next) {
+ groups.getGroupNameByGroupSlug(req.params.slug, next);
+ },
+ function(_groupName, next) {
+ groupName = _groupName;
+ if (!groupName) {
return callback();
}
-
- groups.isHidden(res.locals.groupName, next);
+ async.parallel({
+ exists: async.apply(groups.exists, groupName),
+ hidden: async.apply(groups.isHidden, groupName)
+ }, next);
},
- function (hidden, next) {
- if (!hidden) {
+ function (results, next) {
+ if (!results.exists) {
+ return callback();
+ }
+ if (!results.hidden) {
return next();
}
-
async.parallel({
- isMember: async.apply(groups.isMember, req.uid, res.locals.groupName),
- isInvited: async.apply(groups.isInvited, req.uid, res.locals.groupName)
+ isMember: async.apply(groups.isMember, req.uid, groupName),
+ isInvited: async.apply(groups.isInvited, req.uid, groupName)
}, function(err, checks) {
if (err || checks.isMember || checks.isInvited) {
return next(err);
@@ -73,16 +81,21 @@ groupsController.details = function(req, res, callback) {
function (next) {
async.parallel({
group: function(next) {
- groups.get(res.locals.groupName, {
+ groups.get(groupName, {
uid: req.uid,
truncateUserList: true,
userListCount: 20
}, next);
},
posts: function(next) {
- groups.getLatestMemberPosts(res.locals.groupName, 10, req.uid, next);
+ groups.getLatestMemberPosts(groupName, 10, req.uid, next);
},
- isAdminOrGlobalMod: async.apply(user.isAdminOrGlobalMod, req.uid)
+ isAdmin:function(next) {
+ user.isAdministrator(req.uid, next);
+ },
+ isGlobalMod: function(next) {
+ user.isGlobalModerator(req.uid, next);
+ }
}, next);
}
], function(err, results) {
@@ -93,7 +106,7 @@ groupsController.details = function(req, res, callback) {
if (!results.group) {
return callback();
}
- results.group.isOwner = results.group.isOwner || results.isAdminOrGlobalMod;
+ results.group.isOwner = results.group.isOwner || results.isAdmin || (results.isGlobalMod && !results.group.system);
results.title = '[[pages:group, ' + results.group.displayName + ']]';
results.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[pages:groups]]', url: '/groups' }, {text: results.group.displayName}]);
results.allowPrivateGroups = parseInt(meta.config.allowPrivateGroups, 10) === 1;
@@ -119,7 +132,7 @@ groupsController.members = function(req, res, next) {
var breadcrumbs = helpers.buildBreadcrumbs([
{text: '[[pages:groups]]', url: '/groups' },
- {text: validator.escape(groupName), url: '/groups/' + req.params.slug},
+ {text: validator.escape(String(groupName)), url: '/groups/' + req.params.slug},
{text: '[[groups:details.members]]'}
]);
diff --git a/src/controllers/helpers.js b/src/controllers/helpers.js
index c91d68acc7..2010fbc922 100644
--- a/src/controllers/helpers.js
+++ b/src/controllers/helpers.js
@@ -67,7 +67,7 @@ helpers.buildCategoryBreadcrumbs = function(cid, callback) {
if (!parseInt(data.disabled, 10)) {
breadcrumbs.unshift({
- text: validator.escape(data.name),
+ text: validator.escape(String(data.name)),
url: nconf.get('relative_path') + '/category/' + data.slug
});
}
@@ -119,7 +119,7 @@ helpers.buildBreadcrumbs = function(crumbs) {
helpers.buildTitle = function(pageTitle) {
var titleLayout = meta.config.titleLayout || '{pageTitle} | {browserTitle}';
- var browserTitle = validator.escape(meta.config.browserTitle || meta.config.title || 'NodeBB');
+ var browserTitle = validator.escape(String(meta.config.browserTitle || meta.config.title || 'NodeBB'));
pageTitle = pageTitle || '';
var title = titleLayout.replace('{pageTitle}', function() {
return pageTitle;
diff --git a/src/controllers/index.js b/src/controllers/index.js
index f0b9547c0d..103dbf70f3 100644
--- a/src/controllers/index.js
+++ b/src/controllers/index.js
@@ -8,7 +8,6 @@ var winston = require('winston');
var meta = require('../meta');
var user = require('../user');
var plugins = require('../plugins');
-var sitemap = require('../sitemap');
var helpers = require('./helpers');
var Controllers = {
@@ -27,7 +26,8 @@ var Controllers = {
authentication: require('./authentication'),
api: require('./api'),
admin: require('./admin'),
- globalMods: require('./globalmods')
+ globalMods: require('./globalmods'),
+ sitemap: require('./sitemap')
};
@@ -202,6 +202,10 @@ Controllers.registerInterstitial = function(req, res, next) {
userData: req.session.registration,
interstitials: []
}, function(err, data) {
+ if (err) {
+ return next(err);
+ }
+
if (!data.interstitials.length) {
return next();
}
@@ -212,7 +216,12 @@ Controllers.registerInterstitial = function(req, res, next) {
var errors = req.flash('error');
async.parallel(renders, function(err, sections) {
+ if (err) {
+ return next(err);
+ }
+
res.render('registerComplete', {
+ title: '[[pages:registration-complete]]',
errors: errors,
sections: sections
});
@@ -251,65 +260,6 @@ Controllers.confirmEmail = function(req, res) {
});
};
-Controllers.sitemap = {};
-Controllers.sitemap.render = function(req, res, next) {
- sitemap.render(function(err, tplData) {
- if (err) {
- return next(err);
- }
-
- Controllers.render('sitemap', tplData, function(err, xml) {
- res.header('Content-Type', 'application/xml');
- res.send(xml);
- });
- });
-};
-
-Controllers.sitemap.getPages = function(req, res, next) {
- if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) {
- return next();
- }
-
- sitemap.getPages(function(err, xml) {
- if (err) {
- return next(err);
- }
- res.header('Content-Type', 'application/xml');
- res.send(xml);
- });
-};
-
-Controllers.sitemap.getCategories = function(req, res, next) {
- if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) {
- return next();
- }
-
- sitemap.getCategories(function(err, xml) {
- if (err) {
- return next(err);
- }
- res.header('Content-Type', 'application/xml');
- res.send(xml);
- });
-};
-
-Controllers.sitemap.getTopicPage = function(req, res, next) {
- if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) {
- return next();
- }
-
- sitemap.getTopicPage(parseInt(req.params[0], 10), function(err, xml) {
- if (err) {
- return next(err);
- } else if (!xml) {
- return next();
- }
-
- res.header('Content-Type', 'application/xml');
- res.send(xml);
- });
-};
-
Controllers.robots = function (req, res) {
res.set('Content-Type', 'text/plain');
@@ -369,9 +319,9 @@ Controllers.manifest = function(req, res) {
};
Controllers.outgoing = function(req, res) {
- var url = req.query.url;
+ var url = req.query.url || '';
var data = {
- url: validator.escape(String(url)),
+ outgoing: validator.escape(String(url)),
title: meta.config.title,
breadcrumbs: helpers.buildBreadcrumbs([{text: '[[notifications:outgoing_link]]'}])
};
@@ -390,6 +340,10 @@ Controllers.termsOfUse = function(req, res, next) {
res.render('tos', {termsOfUse: meta.config.termsOfUse});
};
+Controllers.ping = function(req, res) {
+ res.status(200).send(req.path === '/sping' ? 'healthy' : '200');
+};
+
Controllers.handle404 = function(req, res) {
var relativePath = nconf.get('relative_path');
var isLanguage = new RegExp('^' + relativePath + '/language/.*/.*.json');
@@ -423,8 +377,8 @@ Controllers.handle404 = function(req, res) {
if (res.locals.isAPI) {
return res.json({path: validator.escape(path.replace(/^\/api/, '')), title: '[[global:404.title]]'});
}
-
- req.app.locals.middleware.buildHeader(req, res, function() {
+ var middleware = require('../middleware');
+ middleware.buildHeader(req, res, function() {
res.render('404', {path: validator.escape(path), title: '[[global:404.title]]'});
});
} else {
@@ -432,6 +386,36 @@ Controllers.handle404 = function(req, res) {
}
};
+Controllers.handleURIErrors = function(err, req, res, next) {
+ // Handle cases where malformed URIs are passed in
+ if (err instanceof URIError) {
+ var tidMatch = req.path.match(/^\/topic\/(\d+)\//);
+ var cidMatch = req.path.match(/^\/category\/(\d+)\//);
+
+ if (tidMatch) {
+ res.redirect(nconf.get('relative_path') + tidMatch[0]);
+ } else if (cidMatch) {
+ res.redirect(nconf.get('relative_path') + cidMatch[0]);
+ } else {
+ winston.warn('[controller] Bad request: ' + req.path);
+ if (res.locals.isAPI) {
+ res.status(400).json({
+ error: '[[global:400.title]]'
+ });
+ } else {
+ var middleware = require('../middleware');
+ middleware.buildHeader(req, res, function() {
+ res.render('400', { error: validator.escape(String(err.message)) });
+ });
+ }
+ }
+
+ return;
+ } else {
+ next(err);
+ }
+};
+
Controllers.handleErrors = function(err, req, res, next) {
switch (err.code) {
case 'EBADCSRFTOKEN':
@@ -453,8 +437,9 @@ Controllers.handleErrors = function(err, req, res, next) {
if (res.locals.isAPI) {
res.json({path: validator.escape(path), error: err.message});
} else {
- req.app.locals.middleware.buildHeader(req, res, function() {
- res.render('500', {path: validator.escape(path), error: validator.escape(String(err.message))});
+ var middleware = require('../middleware');
+ middleware.buildHeader(req, res, function() {
+ res.render('500', { path: validator.escape(path), error: validator.escape(String(err.message)) });
});
}
};
diff --git a/src/controllers/search.js b/src/controllers/search.js
index 2a7f6b145c..8c016c759c 100644
--- a/src/controllers/search.js
+++ b/src/controllers/search.js
@@ -28,7 +28,7 @@ searchController.search = function(req, res, next) {
}
var data = {
- query: req.params.term,
+ query: req.query.term,
searchIn: req.query.in || 'posts',
postedBy: req.query.by,
categories: req.query.categories,
@@ -45,64 +45,30 @@ searchController.search = function(req, res, next) {
};
async.parallel({
- categories: async.apply(buildCategories, req.uid),
+ categories: async.apply(categories.buildForSelect, req.uid),
search: async.apply(search.search, data)
}, function(err, results) {
if (err) {
return next(err);
}
+
+ var categoriesData = [
+ {value: 'all', text: '[[unread:all_categories]]'},
+ {value: 'watched', text: '[[category:watched-categories]]'}
+ ].concat(results.categories);
+
var searchData = results.search;
- searchData.categories = results.categories;
+ searchData.categories = categoriesData;
searchData.categoriesCount = results.categories.length;
searchData.pagination = pagination.create(page, searchData.pageCount, req.query);
searchData.showAsPosts = !req.query.showAs || req.query.showAs === 'posts';
searchData.showAsTopics = req.query.showAs === 'topics';
searchData.title = '[[global:header.search]]';
searchData.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[global:search]]'}]);
- searchData.expandSearch = !req.params.term;
+ searchData.expandSearch = !req.query.term;
res.render('search', searchData);
});
};
-function buildCategories(uid, callback) {
- categories.getCategoriesByPrivilege('cid:0:children', uid, 'read', function(err, categories) {
- if (err) {
- return callback(err);
- }
-
- var categoriesData = [
- {value: 'all', text: '[[unread:all_categories]]'},
- {value: 'watched', text: '[[category:watched-categories]]'}
- ];
-
- categories = categories.filter(function(category) {
- return category && !category.link && !parseInt(category.parentCid, 10);
- });
-
- categories.forEach(function(category) {
- recursive(category, categoriesData, '');
- });
- callback(null, categoriesData);
- });
-}
-
-
-function recursive(category, categoriesData, level) {
- if (category.link) {
- return;
- }
-
- var bullet = level ? '• ' : '';
-
- categoriesData.push({
- value: category.cid,
- text: level + bullet + category.name
- });
-
- category.children.forEach(function(child) {
- recursive(child, categoriesData, ' ' + level);
- });
-}
-
module.exports = searchController;
diff --git a/src/controllers/sitemap.js b/src/controllers/sitemap.js
new file mode 100644
index 0000000000..eee344fc92
--- /dev/null
+++ b/src/controllers/sitemap.js
@@ -0,0 +1,68 @@
+'use strict';
+
+var sitemap = require('../sitemap');
+var meta = require('../meta');
+
+var sitemapController = {};
+sitemapController.render = function(req, res, next) {
+ sitemap.render(function(err, tplData) {
+ if (err) {
+ return next(err);
+ }
+ var Controllers = require('./index');
+ Controllers.render('sitemap', tplData, function(err, xml) {
+ if (err) {
+ return next(err);
+ }
+ res.header('Content-Type', 'application/xml');
+ res.send(xml);
+ });
+ });
+};
+
+sitemapController.getPages = function(req, res, next) {
+ if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) {
+ return next();
+ }
+
+ sitemap.getPages(function(err, xml) {
+ if (err) {
+ return next(err);
+ }
+ res.header('Content-Type', 'application/xml');
+ res.send(xml);
+ });
+};
+
+sitemapController.getCategories = function(req, res, next) {
+ if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) {
+ return next();
+ }
+
+ sitemap.getCategories(function(err, xml) {
+ if (err) {
+ return next(err);
+ }
+ res.header('Content-Type', 'application/xml');
+ res.send(xml);
+ });
+};
+
+sitemapController.getTopicPage = function(req, res, next) {
+ if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) {
+ return next();
+ }
+
+ sitemap.getTopicPage(parseInt(req.params[0], 10), function(err, xml) {
+ if (err) {
+ return next(err);
+ } else if (!xml) {
+ return next();
+ }
+
+ res.header('Content-Type', 'application/xml');
+ res.send(xml);
+ });
+};
+
+module.exports = sitemapController;
\ No newline at end of file
diff --git a/src/controllers/tags.js b/src/controllers/tags.js
index 45ab3424aa..ba1ec919b2 100644
--- a/src/controllers/tags.js
+++ b/src/controllers/tags.js
@@ -13,7 +13,7 @@ var helpers = require('./helpers');
var tagsController = {};
tagsController.getTag = function(req, res, next) {
- var tag = validator.escape(req.params.tag);
+ var tag = validator.escape(String(req.params.tag));
var page = parseInt(req.query.page, 10) || 1;
var templateData = {
@@ -44,7 +44,6 @@ tagsController.getTag = function(req, res, next) {
},
function (results, next) {
if (Array.isArray(results.tids) && !results.tids.length) {
- topics.deleteTag(req.params.tag);
return res.render('tag', templateData);
}
topicCount = results.topicCount;
@@ -83,9 +82,7 @@ tagsController.getTags = function(req, res, next) {
if (err) {
return next(err);
}
- tags = tags.filter(function(tag) {
- return tag && tag.score > 0;
- });
+ tags = tags.filter(Boolean);
var data = {
tags: tags,
nextStart: 100,
diff --git a/src/controllers/topics.js b/src/controllers/topics.js
index 9d106a85f2..d017f1d5c8 100644
--- a/src/controllers/topics.js
+++ b/src/controllers/topics.js
@@ -283,7 +283,7 @@ topicsController.get = function(req, res, callback) {
}
if (markedRead) {
topics.pushUnreadCount(req.uid);
- topics.markTopicNotificationsRead(tid, req.uid);
+ topics.markTopicNotificationsRead([tid], req.uid);
}
});
}
diff --git a/src/controllers/unread.js b/src/controllers/unread.js
index 57e9a87eff..8860f541d9 100644
--- a/src/controllers/unread.js
+++ b/src/controllers/unread.js
@@ -101,12 +101,13 @@ function getWatchedCategories(uid, selectedCid, callback) {
privileges.categories.filterCids('read', cids, uid, next);
},
function (cids, next) {
- categories.getCategoriesFields(cids, ['cid', 'name', 'slug', 'icon', 'link', 'color', 'bgColor'], next);
+ categories.getCategoriesFields(cids, ['cid', 'name', 'slug', 'icon', 'link', 'color', 'bgColor', 'parentCid'], next);
},
function (categoryData, next) {
categoryData = categoryData.filter(function(category) {
return category && !category.link;
});
+
var selectedCategory;
categoryData.forEach(function(category) {
category.selected = parseInt(category.cid, 10) === parseInt(selectedCid, 10);
@@ -114,11 +115,27 @@ function getWatchedCategories(uid, selectedCid, callback) {
selectedCategory = category;
}
});
- next(null, {categories: categoryData, selectedCategory: selectedCategory});
+
+ var categoriesData = [];
+ var tree = categories.getTree(categoryData, 0);
+
+ tree.forEach(function(category) {
+ recursive(category, categoriesData, '');
+ });
+
+ next(null, {categories: categoriesData, selectedCategory: selectedCategory});
}
], callback);
}
+function recursive(category, categoriesData, level) {
+ category.level = level;
+ categoriesData.push(category);
+
+ category.children.forEach(function(child) {
+ recursive(child, categoriesData, ' ' + level);
+ });
+}
unreadController.unreadTotal = function(req, res, next) {
var filter = req.params.filter || '';
diff --git a/src/controllers/users.js b/src/controllers/users.js
index 4ee7e5a9ec..4f503bb6a3 100644
--- a/src/controllers/users.js
+++ b/src/controllers/users.js
@@ -11,10 +11,61 @@ var helpers = require('./helpers');
var usersController = {};
+
+usersController.index = function(req, res, next) {
+ var section = req.query.section || 'joindate';
+ var sectionToController = {
+ joindate: usersController.getUsersSortedByJoinDate,
+ online: usersController.getOnlineUsers,
+ 'sort-posts': usersController.getUsersSortedByPosts,
+ 'sort-reputation': usersController.getUsersSortedByReputation,
+ banned: usersController.getBannedUsers,
+ flagged: usersController.getFlaggedUsers
+ };
+
+ if (req.query.term) {
+ usersController.search(req, res, next);
+ } else if (sectionToController[section]) {
+ sectionToController[section](req, res, next);
+ } else {
+ usersController.getUsersSortedByJoinDate(req, res, next);
+ }
+};
+
+usersController.search = function(req, res, next) {
+ async.parallel({
+ search: function(next) {
+ user.search({
+ query: req.query.term,
+ searchBy: req.query.searchBy || 'username',
+ page: req.query.page || 1,
+ sortBy: req.query.sortBy,
+ onlineOnly: req.query.onlineOnly === 'true',
+ bannedOnly: req.query.bannedOnly === 'true',
+ flaggedOnly: req.query.flaggedOnly === 'true'
+ }, next);
+ },
+ isAdminOrGlobalMod: function(next) {
+ user.isAdminOrGlobalMod(req.uid, next);
+ }
+ }, function(err, results) {
+ if (err) {
+ return next(err);
+ }
+
+ var section = req.query.section || 'joindate';
+
+ results.search.isAdminOrGlobalMod = results.isAdminOrGlobalMod;
+ results.search.pagination = pagination.create(req.query.page, results.search.pageCount, req.query);
+ results.search['section_' + section] = true;
+ render(req, res, results.search, next);
+ });
+};
+
usersController.getOnlineUsers = function(req, res, next) {
async.parallel({
users: function(next) {
- usersController.getUsers('users:online', req.uid, req.query.page, next);
+ usersController.getUsers('users:online', req.uid, req.query, next);
},
guests: function(next) {
require('../socket.io/admin/rooms').getTotalGuestCount(next);
@@ -56,7 +107,7 @@ usersController.getUsersSortedByJoinDate = function(req, res, next) {
};
usersController.getBannedUsers = function(req, res, next) {
- usersController.getUsers('users:banned', req.uid, req.query.page, function(err, userData) {
+ usersController.getUsers('users:banned', req.uid, req.query, function(err, userData) {
if (err) {
return next(err);
}
@@ -70,7 +121,7 @@ usersController.getBannedUsers = function(req, res, next) {
};
usersController.getFlaggedUsers = function(req, res, next) {
- usersController.getUsers('users:flags', req.uid, req.query.page, function(err, userData) {
+ usersController.getUsers('users:flags', req.uid, req.query, function(err, userData) {
if (err) {
return next(err);
}
@@ -84,15 +135,16 @@ usersController.getFlaggedUsers = function(req, res, next) {
};
usersController.renderUsersPage = function(set, req, res, next) {
- usersController.getUsers(set, req.uid, req.query.page, function(err, userData) {
+ usersController.getUsers(set, req.uid, req.query, function(err, userData) {
if (err) {
return next(err);
}
+
render(req, res, userData, next);
});
};
-usersController.getUsers = function(set, uid, page, callback) {
+usersController.getUsers = function(set, uid, query, callback) {
var setToData = {
'users:postcount': {title: '[[pages:users/sort-posts]]', crumb: '[[users:top_posters]]'},
'users:reputation': {title: '[[pages:users/sort-reputation]]', crumb: '[[users:most_reputation]]'},
@@ -102,23 +154,24 @@ usersController.getUsers = function(set, uid, page, callback) {
'users:flags': {title: '[[pages:users/most-flags]]', crumb: '[[users:most_flags]]'},
};
+ if (!setToData[set]) {
+ setToData[set] = {title: '', crumb: ''};
+ }
+
var breadcrumbs = [{text: setToData[set].crumb}];
if (set !== 'users:joindate') {
breadcrumbs.unshift({text: '[[global:users]]', url: '/users'});
}
- page = parseInt(page, 10) || 1;
+ var page = parseInt(query.page, 10) || 1;
var resultsPerPage = parseInt(meta.config.userSearchResultsPerPage, 10) || 50;
var start = Math.max(0, page - 1) * resultsPerPage;
var stop = start + resultsPerPage - 1;
async.parallel({
- isAdministrator: function(next) {
- user.isAdministrator(uid, next);
- },
- isGlobalMod: function(next) {
- user.isGlobalModerator(uid, next);
+ isAdminOrGlobalMod: function(next) {
+ user.isAdminOrGlobalMod(uid, next);
},
usersData: function(next) {
usersController.getUsersAndCount(set, uid, start, stop, next);
@@ -130,16 +183,14 @@ usersController.getUsers = function(set, uid, page, callback) {
var pageCount = Math.ceil(results.usersData.count / resultsPerPage);
var userData = {
- loadmore_display: results.usersData.count > (stop - start + 1) ? 'block' : 'hide',
users: results.usersData.users,
- pagination: pagination.create(page, pageCount),
+ pagination: pagination.create(page, pageCount, query),
userCount: results.usersData.count,
title: setToData[set].title || '[[pages:users/latest]]',
breadcrumbs: helpers.buildBreadcrumbs(breadcrumbs),
- setName: set,
- isAdminOrGlobalMod: results.isAdministrator || results.isGlobalMod
+ isAdminOrGlobalMod: results.isAdminOrGlobalMod
};
- userData['route_' + set] = true;
+ userData['section_' + (query.section || 'joindate')] = true;
callback(null, userData);
});
};
diff --git a/src/database/mongo/hash.js b/src/database/mongo/hash.js
index dbf294119e..585abea8b6 100644
--- a/src/database/mongo/hash.js
+++ b/src/database/mongo/hash.js
@@ -231,9 +231,11 @@ module.exports = function(db, module) {
module.incrObjectFieldBy = function(key, field, value, callback) {
callback = callback || helpers.noop;
- if (!key) {
+ value = parseInt(value, 10);
+ if (!key || isNaN(value)) {
return callback();
}
+
var data = {};
field = helpers.fieldToString(field);
data[field] = value;
diff --git a/src/database/mongo/sorted.js b/src/database/mongo/sorted.js
index 9e9f72969e..0b7ce568be 100644
--- a/src/database/mongo/sorted.js
+++ b/src/database/mongo/sorted.js
@@ -1,6 +1,7 @@
"use strict";
var async = require('async');
+var utils = require('../../../public/src/utils');
module.exports = function(db, module) {
var helpers = module.helpers.mongo;
@@ -17,6 +18,9 @@ module.exports = function(db, module) {
value = helpers.valueToString(value);
db.collection('objects').update({_key: key, value: value}, {$set: {score: parseInt(score, 10)}}, {upsert:true, w: 1}, function(err) {
+ if (err && err.message.startsWith('E11000 duplicate key error')) {
+ return module.sortedSetAdd(key, score, value, callback);
+ }
callback(err);
});
};
@@ -140,8 +144,13 @@ module.exports = function(db, module) {
key = {$in: key};
}
+ var limit = stop - start + 1;
+ if (limit <= 0) {
+ limit = 0;
+ }
+
db.collection('objects').find({_key: key}, {fields: fields})
- .limit(stop - start + 1)
+ .limit(limit)
.skip(start)
.sort({score: sort})
.toArray(function(err, data) {
@@ -381,12 +390,12 @@ module.exports = function(db, module) {
map[item.value] = item.score;
});
- var returnData = new Array(values.length),
- score;
+ var returnData = new Array(values.length);
+ var score;
for(var i=0; i
0) {
pipeline.push({ $limit: limit });
}
- pipeline.push({ $project: { _id: 0, value: '$_id.value' }});
+ var project = { _id: 0, value: '$_id.value' };
+ if (params.withScores) {
+ project.score = '$totalScore';
+ }
+ pipeline.push({ $project: project });
db.collection('objects').aggregate(pipeline, function(err, data) {
if (err || !data) {
return callback(err);
}
- data = data.map(function(item) {
- return item.value;
- });
+ if (!params.withScores) {
+ data = data.map(function(item) {
+ return item.value;
+ });
+ }
+
callback(null, data);
});
}
@@ -595,4 +638,99 @@ module.exports = function(db, module) {
callback
);
};
+
+
+ module.sortedSetIntersectCard = function(keys, callback) {
+ if (!Array.isArray(keys) || !keys.length) {
+ return callback(null, 0);
+ }
+
+ var pipeline = [
+ { $match: { _key: {$in: keys}} },
+ { $group: { _id: {value: '$value'}, count: {$sum: 1}} },
+ { $match: { count: keys.length} },
+ { $group: { _id: null, count: { $sum: 1 } } }
+ ];
+
+ db.collection('objects').aggregate(pipeline, function(err, data) {
+ callback(err, Array.isArray(data) && data.length ? data[0].count : 0);
+ });
+ };
+
+ module.getSortedSetIntersect = function(params, callback) {
+ params.sort = 1;
+ getSortedSetRevIntersect(params, callback);
+ };
+
+ module.getSortedSetRevIntersect = function(params, callback) {
+ params.sort = -1;
+ getSortedSetRevIntersect(params, callback);
+ };
+
+ function getSortedSetRevIntersect(params, callback) {
+ var sets = params.sets;
+ var start = params.hasOwnProperty('start') ? params.start : 0;
+ var stop = params.hasOwnProperty('stop') ? params.stop : -1;
+ var weights = params.weights || [];
+ var aggregate = {};
+
+ if (params.aggregate) {
+ aggregate['$' + params.aggregate.toLowerCase()] = '$score';
+ } else {
+ aggregate.$sum = '$score';
+ }
+
+ var limit = stop - start + 1;
+ if (limit <= 0) {
+ limit = 0;
+ }
+
+ var pipeline = [{ $match: { _key: {$in: sets}} }];
+
+ weights.forEach(function(weight, index) {
+ if (weight !== 1) {
+ pipeline.push({
+ $project: {
+ value: 1,
+ score: {
+ $cond: { if: { $eq: [ "$_key", sets[index] ] }, then: { $multiply: [ '$score', weight ] }, else: '$score' }
+ }
+ }
+ });
+ }
+ });
+
+ pipeline.push({ $group: { _id: {value: '$value'}, totalScore: aggregate, count: {$sum: 1}} });
+ pipeline.push({ $match: { count: sets.length} });
+ pipeline.push({ $sort: { totalScore: params.sort} });
+
+ if (start) {
+ pipeline.push({ $skip: start });
+ }
+
+ if (limit > 0) {
+ pipeline.push({ $limit: limit });
+ }
+
+ var project = { _id: 0, value: '$_id.value'};
+ if (params.withScores) {
+ project.score = '$totalScore';
+ }
+ pipeline.push({ $project: project });
+
+ db.collection('objects').aggregate(pipeline, function(err, data) {
+ if (err || !data) {
+ return callback(err);
+ }
+
+ if (!params.withScores) {
+ data = data.map(function(item) {
+ return item.value;
+ });
+ }
+
+ callback(null, data);
+ });
+ }
+
};
diff --git a/src/database/redis.js b/src/database/redis.js
index 8af568b5d9..214c9ef4a1 100644
--- a/src/database/redis.js
+++ b/src/database/redis.js
@@ -65,15 +65,18 @@
};
module.connect = function(options) {
- var redis_socket_or_host = nconf.get('redis:host'),
- cxn, dbIdx;
-
- options = options || {};
+ var redis_socket_or_host = nconf.get('redis:host');
+ var cxn;
if (!redis) {
redis = require('redis');
}
+ options = options || {};
+ if (nconf.get('redis:password')) {
+ options.auth_pass = nconf.get('redis:password');
+ }
+
if (redis_socket_or_host && redis_socket_or_host.indexOf('/') >= 0) {
/* If redis.host contains a path name character, use the unix dom sock connection. ie, /tmp/redis.sock */
cxn = redis.createClient(nconf.get('redis:host'), options);
@@ -91,7 +94,7 @@
cxn.auth(nconf.get('redis:password'));
}
- dbIdx = parseInt(nconf.get('redis:database'), 10);
+ var dbIdx = parseInt(nconf.get('redis:database'), 10);
if (dbIdx) {
cxn.select(dbIdx, function(error) {
if(error) {
diff --git a/src/database/redis/sets.js b/src/database/redis/sets.js
index 9443ed928f..1eaab00cc5 100644
--- a/src/database/redis/sets.js
+++ b/src/database/redis/sets.js
@@ -5,6 +5,12 @@ module.exports = function(redisClient, module) {
module.setAdd = function(key, value, callback) {
callback = callback || function() {};
+ if (!Array.isArray(value)) {
+ value = [value];
+ }
+ if (!value.length) {
+ return callback();
+ }
redisClient.sadd(key, value, function(err, res) {
callback(err);
});
diff --git a/src/database/redis/sorted.js b/src/database/redis/sorted.js
index 0341c043f7..b906e10575 100644
--- a/src/database/redis/sorted.js
+++ b/src/database/redis/sorted.js
@@ -29,7 +29,7 @@ module.exports = function(redisClient, module) {
args.push(scores[i], values[i]);
}
- redisClient.zadd(args, function(err, res) {
+ redisClient.zadd(args, function(err) {
callback(err);
});
}
@@ -42,7 +42,7 @@ module.exports = function(redisClient, module) {
multi.zadd(keys[i], score, value);
}
- multi.exec(function(err, res) {
+ multi.exec(function(err) {
callback(err);
});
};
@@ -53,13 +53,13 @@ module.exports = function(redisClient, module) {
value = [value];
}
- helpers.multiKeyValues(redisClient, 'zrem', key, value, function(err, result) {
+ helpers.multiKeyValues(redisClient, 'zrem', key, value, function(err) {
callback(err);
});
};
module.sortedSetsRemove = function(keys, value, callback) {
- helpers.multiKeysValue(redisClient, 'zrem', keys, value, function(err, result) {
+ helpers.multiKeysValue(redisClient, 'zrem', keys, value, function(err) {
callback(err);
});
};
@@ -70,7 +70,7 @@ module.exports = function(redisClient, module) {
for(var i=0; i (parseInt(meta.config.maximumGroupNameLength, 10) || 255)) {
+ if (!Groups.isPrivilegeGroup(name) && name.length > (parseInt(meta.config.maximumGroupNameLength, 10) || 255)) {
return callback(new Error('[[error:group-name-too-long]]'));
}
diff --git a/src/groups/membership.js b/src/groups/membership.js
index 3e18979f96..17e70a12dc 100644
--- a/src/groups/membership.js
+++ b/src/groups/membership.js
@@ -8,10 +8,20 @@ var user = require('../user');
var utils = require('../../public/src/utils');
var plugins = require('../plugins');
var notifications = require('../notifications');
-var db = require('./../database');
+var db = require('../database');
+
+var pubsub = require('../pubsub');
+var LRU = require('lru-cache');
+
+var cache = LRU({
+ max: 40000,
+ maxAge: 1000 * 60 * 60
+});
module.exports = function(Groups) {
+ Groups.cache = cache;
+
Groups.join = function(groupName, uid, callback) {
callback = callback || function() {};
@@ -69,6 +79,7 @@ module.exports = function(Groups) {
async.parallel(tasks, next);
},
function(results, next) {
+ clearCache(uid, groupName);
setGroupTitleIfNotSet(groupName, uid, next);
},
function(next) {
@@ -222,6 +233,7 @@ module.exports = function(Groups) {
], next);
},
function(results, next) {
+ clearCache(uid, groupName);
Groups.getGroupFields(groupName, ['hidden', 'memberCount'], next);
},
function(groupData, next) {
@@ -296,26 +308,119 @@ module.exports = function(Groups) {
}), callback);
};
+ Groups.resetCache = function() {
+ pubsub.publish('group:cache:reset');
+ cache.reset();
+ };
+
+ pubsub.on('group:cache:reset', function() {
+ cache.reset();
+ });
+
+ function clearCache(uid, groupName) {
+ pubsub.publish('group:cache:del', {uid: uid, groupName: groupName});
+ cache.del(uid + ':' + groupName);
+ }
+
+ pubsub.on('group:cache:del', function(data) {
+ cache.del(data.uid + ':' + data.groupName);
+ });
+
Groups.isMember = function(uid, groupName, callback) {
if (!uid || parseInt(uid, 10) <= 0) {
return callback(null, false);
}
- db.isSortedSetMember('group:' + groupName + ':members', uid, callback);
+
+ var cacheKey = uid + ':' + groupName;
+ if (cache.has(cacheKey)) {
+ return process.nextTick(callback, null, cache.get(cacheKey));
+ }
+
+ db.isSortedSetMember('group:' + groupName + ':members', uid, function(err, isMember) {
+ if (err) {
+ return callback(err);
+ }
+
+ cache.set(cacheKey, isMember);
+ callback(null, isMember);
+ });
};
Groups.isMembers = function(uids, groupName, callback) {
- db.isSortedSetMembers('group:' + groupName + ':members', uids, callback);
+ if (!groupName || !uids.length) {
+ return callback(null, uids.map(function() {return false;}));
+ }
+
+ var nonCachedUids = [];
+ uids.forEach(function(uid) {
+ if (!cache.has(uid + ':' + groupName)) {
+ nonCachedUids.push(uid);
+ }
+ });
+
+ if (!nonCachedUids.length) {
+ var result = uids.map(function(uid) {
+ return cache.get(uid + ':' + groupName);
+ });
+ return process.nextTick(callback, null, result);
+ }
+
+ db.isSortedSetMembers('group:' + groupName + ':members', nonCachedUids, function(err, isMembers) {
+ if (err) {
+ return callback(err);
+ }
+
+ nonCachedUids.forEach(function(uid, index) {
+ cache.set(uid + ':' + groupName, isMembers[index]);
+ });
+
+ var result = uids.map(function(uid) {
+ return cache.get(uid + ':' + groupName);
+ });
+
+ callback(null, result);
+ });
};
Groups.isMemberOfGroups = function(uid, groups, callback) {
- if (!uid || parseInt(uid, 10) <= 0) {
+ if (!uid || parseInt(uid, 10) <= 0 || !groups.length) {
return callback(null, groups.map(function() {return false;}));
}
- groups = groups.map(function(groupName) {
+
+ var nonCachedGroups = [];
+
+ groups.forEach(function(groupName) {
+ if (!cache.has(uid + ':' + groupName)) {
+ nonCachedGroups.push(groupName);
+ }
+ });
+
+ // are they all cached?
+ if (!nonCachedGroups.length) {
+ var result = groups.map(function(groupName) {
+ return cache.get(uid + ':' + groupName);
+ });
+ return process.nextTick(callback, null, result);
+ }
+
+ var nonCachedGroupsMemberSets = nonCachedGroups.map(function(groupName) {
return 'group:' + groupName + ':members';
});
- db.isMemberOfSortedSets(groups, uid, callback);
+ db.isMemberOfSortedSets(nonCachedGroupsMemberSets, uid, function(err, isMembers) {
+ if (err) {
+ return callback(err);
+ }
+
+ nonCachedGroups.forEach(function(groupName, index) {
+ cache.set(uid + ':' + groupName, isMembers[index]);
+ });
+
+ var result = groups.map(function(groupName) {
+ return cache.get(uid + ':' + groupName);
+ });
+ callback(null, result);
+ });
};
Groups.getMemberCount = function(groupName, callback) {
diff --git a/src/image.js b/src/image.js
index fca120d1e5..f1e12c81d5 100644
--- a/src/image.js
+++ b/src/image.js
@@ -1,11 +1,11 @@
'use strict';
-var fs = require('fs'),
- Jimp = require('jimp'),
- async = require('async'),
- plugins = require('./plugins');
+var fs = require('fs');
+var Jimp = require('jimp');
+var async = require('async');
+var plugins = require('./plugins');
-var image = {};
+var image = module.exports;
image.resizeImage = function(data, callback) {
if (plugins.hasListeners('filter:image.resize')) {
@@ -106,12 +106,10 @@ image.size = function(path, callback) {
callback(err, data ? data.bitmap : null);
});
}
-}
+};
image.convertImageToBase64 = function(path, callback) {
fs.readFile(path, function(err, data) {
callback(err, data ? data.toString('base64') : null);
});
};
-
-module.exports = image;
diff --git a/src/install.js b/src/install.js
index f9fae4ffdf..5e642d0177 100644
--- a/src/install.js
+++ b/src/install.js
@@ -179,12 +179,10 @@ function completeConfigSetup(err, config, next) {
function setupDefaultConfigs(next) {
process.stdout.write('Populating database with default configs, if not already set...\n');
- var meta = require('./meta'),
- defaults = require(path.join(__dirname, '../', 'install/data/defaults.json'));
+ var meta = require('./meta');
+ var defaults = require(path.join(__dirname, '../', 'install/data/defaults.json'));
- async.each(Object.keys(defaults), function (key, next) {
- meta.configs.setOnEmpty(key, defaults[key], next);
- }, function (err) {
+ meta.configs.setOnEmpty(defaults, function (err) {
if (err) {
return next(err);
}
@@ -257,6 +255,9 @@ function createAdmin(callback) {
type: 'string'
}],
success = function(err, results) {
+ if (err) {
+ return callback(err);
+ }
if (!results) {
return callback(new Error('aborted'));
}
diff --git a/src/languages.js b/src/languages.js
index 2ad64fd588..70dbf4f4ae 100644
--- a/src/languages.js
+++ b/src/languages.js
@@ -30,6 +30,10 @@ Languages.get = function(code, key, callback) {
var languageData;
fs.readFile(path.join(__dirname, '../public/language/', code, key), { encoding: 'utf-8' }, function(err, data) {
+ if (err && err.code !== 'ENOENT') {
+ return callback(err);
+ }
+
// If language file in core cannot be read, then no language file present
try {
languageData = JSON.parse(data) || {};
diff --git a/src/messaging.js b/src/messaging.js
index 62dd569015..d3809c9676 100644
--- a/src/messaging.js
+++ b/src/messaging.js
@@ -49,21 +49,22 @@ var async = require('async'),
};
Messaging.getMessages = function(params, callback) {
- var uid = params.uid,
- roomId = params.roomId,
- since = params.since,
- isNew = params.isNew,
- count = params.count || 250,
- markRead = params.markRead || true;
+ var uid = params.uid;
+ var roomId = params.roomId;
+ var since = params.since;
+ var isNew = params.isNew;
+ var start = params.hasOwnProperty('start') ? params.start : 0;
+ var count = params.count || 250;
+ var markRead = params.markRead || true;
var min = params.count ? 0 : Date.now() - (terms[since] || terms.day);
if (since === 'recent') {
- count = 49;
+ count = 50;
min = 0;
}
- db.getSortedSetRevRangeByScore('uid:' + uid + ':chat:room:' + roomId + ':mids', 0, count, '+inf', min, function(err, mids) {
+ db.getSortedSetRevRangeByScore('uid:' + uid + ':chat:room:' + roomId + ':mids', start, count, '+inf', min, function(err, mids) {
if (err) {
return callback(err);
}
@@ -71,10 +72,24 @@ var async = require('async'),
if (!Array.isArray(mids) || !mids.length) {
return callback(null, []);
}
+ var indices = {};
+ mids.forEach(function(mid, index) {
+ indices[mid] = start + index;
+ });
mids.reverse();
- Messaging.getMessagesData(mids, uid, roomId, isNew, callback);
+ Messaging.getMessagesData(mids, uid, roomId, isNew, function(err, messageData) {
+ if (err) {
+ return callback(err);
+ }
+
+ for(var i=0; i 2;
+ room.unread = results.unread[index];
+ room.teaser = results.teasers[index];
+
+ room.users.forEach(function(userData) {
if (userData && parseInt(userData.uid, 10)) {
userData.status = user.getStatus(userData);
}
});
- data.users = data.users.filter(function(user) {
+ room.users = room.users.filter(function(user) {
return user && parseInt(user.uid, 10);
});
- data.lastUser = data.users[0];
- data.usernames = data.users.map(function(user) {
+ room.lastUser = room.users[0];
+
+ room.usernames = room.users.map(function(user) {
return user.username;
}).join(', ');
- return data;
});
- callback(null, {rooms: rooms, nextStart: stop + 1});
+ callback(null, {rooms: results.roomData, nextStart: stop + 1});
});
});
};
Messaging.getTeaser = function (uid, roomId, callback) {
+ var teaser;
async.waterfall([
function (next) {
db.getSortedSetRevRange('uid:' + uid + ':chat:room:' + roomId + ':mids', 0, 0, next);
@@ -300,14 +322,22 @@ var async = require('async'),
if (!mids || !mids.length) {
return next(null, null);
}
- Messaging.getMessageFields(mids[0], ['content', 'timestamp'], next);
+ Messaging.getMessageFields(mids[0], ['fromuid', 'content', 'timestamp'], next);
},
- function (teaser, next) {
- if (teaser && teaser.content) {
+ function (_teaser, next) {
+ teaser = _teaser;
+ if (!teaser) {
+ return callback();
+ }
+ if (teaser.content) {
teaser.content = S(teaser.content).stripTags().decodeHTMLEntities().s;
- teaser.timestampISO = utils.toISOString(teaser.timestamp);
- }
+ }
+ teaser.timestampISO = utils.toISOString(teaser.timestamp);
+ user.getUserFields(teaser.fromuid, ['uid', 'username', 'userslug', 'picture', 'status', 'lastonline'] , next);
+ },
+ function(user, next) {
+ teaser.user = user;
next(null, teaser);
}
], callback);
diff --git a/src/messaging/rooms.js b/src/messaging/rooms.js
index 0c9c5bb01f..7433da44c6 100644
--- a/src/messaging/rooms.js
+++ b/src/messaging/rooms.js
@@ -13,14 +13,36 @@ module.exports = function(Messaging) {
if (err || !data) {
return callback(err || new Error('[[error:no-chat-room]]'));
}
- data.roomName = data.roomName || '[[modules:chat.roomname, ' + roomId + ']]';
- if (data.roomName) {
- data.roomName = validator.escape(data.roomName);
- }
+ modifyRoomData([data]);
callback(null, data);
});
};
+ Messaging.getRoomsData = function(roomIds, callback) {
+ var keys = roomIds.map(function(roomId) {
+ return 'chat:room:' + roomId;
+ });
+ db.getObjects(keys, function(err, roomData) {
+ if (err) {
+ return callback(err);
+ }
+ modifyRoomData(roomData);
+ callback(null, roomData);
+ });
+ };
+
+ function modifyRoomData(rooms) {
+ rooms.forEach(function(data) {
+ if (data) {
+ data.roomName = data.roomName || '[[modules:chat.roomname, ' + data.roomId + ']]';
+ data.roomName = validator.escape(String(data.roomName));
+ if (data.hasOwnProperty('groupChat')) {
+ data.groupChat = parseInt(data.groupChat, 10) === 1;
+ }
+ }
+ });
+ }
+
Messaging.newRoom = function(uid, toUids, callback) {
var roomId;
var now = Date.now();
@@ -87,6 +109,18 @@ module.exports = function(Messaging) {
return now;
});
db.sortedSetAdd('chat:room:' + roomId + ':uids', timestamps, uids, next);
+ },
+ function(next) {
+ async.parallel({
+ userCount: async.apply(db.sortedSetCard, 'chat:room:' + roomId + ':uids'),
+ roomData: async.apply(db.getObject, 'chat:room:' + roomId)
+ }, next);
+ },
+ function(results, next) {
+ if (!results.roomData.hasOwnProperty('groupChat') && results.userCount > 2) {
+ return db.setObjectField('chat:room:' + roomId, 'groupChat', 1, next);
+ }
+ next();
}
], callback);
};
diff --git a/src/meta.js b/src/meta.js
index 1cbd1af0ae..2aca05f308 100644
--- a/src/meta.js
+++ b/src/meta.js
@@ -1,18 +1,14 @@
"use strict";
-var async = require('async'),
- winston = require('winston'),
- templates = require('templates.js'),
- os = require('os'),
- nconf = require('nconf'),
+var async = require('async');
+var winston = require('winston');
- user = require('./user'),
- groups = require('./groups'),
- languages = require('./languages'),
- emitter = require('./emitter'),
- pubsub = require('./pubsub'),
- auth = require('./routes/authentication'),
- utils = require('../public/src/utils');
+var os = require('os');
+var nconf = require('nconf');
+var user = require('./user');
+var groups = require('./groups');
+var pubsub = require('./pubsub');
+var utils = require('../public/src/utils');
(function (Meta) {
Meta.reloadRequired = false;
@@ -41,50 +37,14 @@ var async = require('async'),
});
};
+ /**
+ * Reload deprecated as of v1.1.2+, remove in v2.x
+ */
Meta.reload = function(callback) {
- pubsub.publish('meta:reload', {hostname: os.hostname()});
- reload(callback);
+ restart();
+ callback();
};
- pubsub.on('meta:reload', function(data) {
- if (data.hostname !== os.hostname()) {
- reload();
- }
- });
-
- function reload(callback) {
- callback = callback || function() {};
-
- var plugins = require('./plugins');
- async.series([
- function (next) {
- plugins.fireHook('static:app.reload', {}, next);
- },
- async.apply(plugins.clearRequireCache),
- async.apply(Meta.css.minify),
- async.apply(Meta.js.minify, 'nodebb.min.js'),
- async.apply(Meta.js.minify, 'acp.min.js'),
- async.apply(Meta.sounds.init),
- async.apply(languages.init),
- async.apply(Meta.templates.compile),
- async.apply(plugins.reload),
- async.apply(plugins.reloadRoutes),
- async.apply(auth.reloadRoutes),
- function(next) {
- Meta.config['cache-buster'] = utils.generateUUID();
- templates.flush();
- next();
- }
- ], function(err) {
- if (!err) {
- emitter.emit('nodebb:ready');
- }
- Meta.reloadRequired = false;
-
- callback(err);
- });
- }
-
Meta.restart = function() {
pubsub.publish('meta:restart', {hostname: os.hostname()});
restart();
diff --git a/src/meta/configs.js b/src/meta/configs.js
index 69cc375b85..723e58083d 100644
--- a/src/meta/configs.js
+++ b/src/meta/configs.js
@@ -1,11 +1,12 @@
'use strict';
-var winston = require('winston'),
- db = require('../database'),
- pubsub = require('../pubsub'),
- nconf = require('nconf'),
- utils = require('../../public/src/utils');
+var winston = require('winston');
+var nconf = require('nconf');
+
+var db = require('../database');
+var pubsub = require('../pubsub');
+var utils = require('../../public/src/utils');
module.exports = function(Meta) {
@@ -117,14 +118,20 @@ module.exports = function(Meta) {
}
});
- Meta.configs.setOnEmpty = function (field, value, callback) {
- Meta.configs.get(field, function (err, curValue) {
+ Meta.configs.setOnEmpty = function (values, callback) {
+ db.getObject('config', function(err, data) {
if (err) {
return callback(err);
}
-
- if (!curValue) {
- Meta.configs.set(field, value, callback);
+ data = data || {};
+ var empty = {};
+ Object.keys(values).forEach(function(key) {
+ if (!data.hasOwnProperty(key)) {
+ empty[key] = values[key];
+ }
+ });
+ if (Object.keys(empty).length) {
+ db.setObject('config', empty, callback);
} else {
callback();
}
diff --git a/src/meta/css.js b/src/meta/css.js
index 478acab284..284effeceb 100644
--- a/src/meta/css.js
+++ b/src/meta/css.js
@@ -39,8 +39,7 @@ module.exports = function(Meta) {
paths = [
baseThemePath,
path.join(__dirname, '../../node_modules'),
- path.join(__dirname, '../../public/vendor/fontawesome/less'),
- path.join(__dirname, '../../public/vendor/bootstrap/less')
+ path.join(__dirname, '../../public/vendor/fontawesome/less')
],
source = '@import "font-awesome";';
@@ -66,7 +65,7 @@ module.exports = function(Meta) {
var acpSource = source;
- source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui-1.10.4.custom.min.css";';
+ source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";';
source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";';
source += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";';
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/flags.less";';
@@ -77,8 +76,9 @@ module.exports = function(Meta) {
source = '@import "./theme";\n' + source;
acpSource += '\n@import "..' + path.sep + 'public/less/admin/admin";\n';
- acpSource += '\n@import "..' + path.sep + 'public/less/generics.less";';
- acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";';
+ acpSource += '\n@import "..' + path.sep + 'public/less/generics.less";\n';
+ acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";\n';
+ acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";';
var fromFile = nconf.get('from-file') || '';
@@ -170,6 +170,10 @@ module.exports = function(Meta) {
winston.verbose('[meta/css] Reading stylesheet ' + filePath.split('/').pop() + ' from file');
fs.readFile(filePath, function(err, file) {
+ if (err) {
+ return callback(err);
+ }
+
Meta.css[filename] = file;
callback();
});
diff --git a/src/meta/errors.js b/src/meta/errors.js
index 4449a55f57..9c8d1ff772 100644
--- a/src/meta/errors.js
+++ b/src/meta/errors.js
@@ -20,6 +20,10 @@ module.exports = function(Meta) {
Meta.errors.get = function(escape, callback) {
db.getSortedSetRevRangeByScoreWithScores('errors:404', 0, -1, '+inf', '-inf', function(err, data) {
+ if (err) {
+ return callback(err);
+ }
+
data = data.map(function(nfObject) {
nfObject.value = escape ? validator.escape(nfObject.value) : nfObject.value;
return nfObject;
diff --git a/src/meta/js.js b/src/meta/js.js
index e25ac45ba0..e729937559 100644
--- a/src/meta/js.js
+++ b/src/meta/js.js
@@ -17,15 +17,16 @@ module.exports = function(Meta) {
target: {},
scripts: {
base: [
- 'public/vendor/jquery/js/jquery.js',
+ './node_modules/jquery/dist/jquery.js',
'./node_modules/socket.io-client/socket.io.js',
'public/vendor/jquery/timeago/jquery.timeago.js',
'public/vendor/jquery/js/jquery.form.min.js',
'public/vendor/visibility/visibility.min.js',
- 'public/vendor/bootstrap/js/bootstrap.min.js',
+ 'public/vendor/bootstrap/js/bootstrap.js',
'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.min.js',
'public/vendor/jquery/textcomplete/jquery.textcomplete.js',
'public/vendor/requirejs/require.js',
+ 'public/src/require-config.js',
'public/vendor/bootbox/bootbox.min.js',
'public/vendor/tinycon/tinycon.js',
'public/vendor/xregexp/xregexp.js',
@@ -36,8 +37,8 @@ module.exports = function(Meta) {
'public/src/app.js',
'public/src/ajaxify.js',
'public/src/overrides.js',
- 'public/src/variables.js',
- 'public/src/widgets.js'
+ 'public/src/widgets.js',
+ "./node_modules/promise-polyfill/promise.js"
],
// files listed below are only available client-side, or are bundled in to reduce # of network requests on cold load
@@ -79,8 +80,8 @@ module.exports = function(Meta) {
// modules listed below are routed through express (/src/modules) so they can be defined anonymously
modules: {
"Chart.js": './node_modules/chart.js/dist/Chart.min.js',
- "mousetrap.js": './node_modules/mousetrap/mousetrap.js',
-
+ "mousetrap.js": './node_modules/mousetrap/mousetrap.min.js',
+ "jqueryui.js": 'public/vendor/jquery/js/jquery-ui.js',
"buzz.js": 'public/vendor/buzz/buzz.js'
}
}
@@ -267,6 +268,10 @@ module.exports = function(Meta) {
}
async.map(paths, fs.readFile, function(err, files) {
+ if (err) {
+ return callback(err);
+ }
+
Meta.js.target[target] = {
cache: files[0],
map: files[1] || ''
diff --git a/src/meta/settings.js b/src/meta/settings.js
index ed6702a943..4fba3fcb2e 100644
--- a/src/meta/settings.js
+++ b/src/meta/settings.js
@@ -38,14 +38,21 @@ module.exports = function(Meta) {
db.setObjectField('settings:' + hash, field, value, callback);
};
- Meta.settings.setOnEmpty = function (hash, field, value, callback) {
- Meta.settings.getOne(hash, field, function (err, curValue) {
+ Meta.settings.setOnEmpty = function (hash, values, callback) {
+ db.getObject('settings:' + hash, function(err, settings) {
if (err) {
return callback(err);
}
+ settings = settings || {};
+ var empty = {};
+ Object.keys(values).forEach(function(key) {
+ if (!settings.hasOwnProperty(key)) {
+ empty[key] = values[key];
+ }
+ });
- if (!curValue) {
- Meta.settings.setOne(hash, field, value, callback);
+ if (Object.keys(empty).length) {
+ db.setObject('settings:' + hash, empty, callback);
} else {
callback();
}
diff --git a/src/meta/sounds.js b/src/meta/sounds.js
index c802458b14..9a73942773 100644
--- a/src/meta/sounds.js
+++ b/src/meta/sounds.js
@@ -1,15 +1,16 @@
'use strict';
-var path = require('path'),
- fs = require('fs'),
- nconf = require('nconf'),
- winston = require('winston'),
- rimraf = require('rimraf'),
- mkdirp = require('mkdirp'),
- async = require('async'),
+var path = require('path');
+var fs = require('fs');
+var nconf = require('nconf');
+var winston = require('winston');
+var rimraf = require('rimraf');
+var mkdirp = require('mkdirp');
+var async = require('async');
- plugins = require('../plugins'),
- db = require('../database');
+var user = require('../user');
+var plugins = require('../plugins');
+var db = require('../database');
module.exports = function(Meta) {
@@ -58,20 +59,31 @@ module.exports = function(Meta) {
});
};
- Meta.sounds.getMapping = function(callback) {
- db.getObject('settings:sounds', function(err, sounds) {
- if (err || !sounds) {
- // Send default sounds
- var defaults = {
- 'notification': 'notification.mp3',
- 'chat-incoming': 'waterdrop-high.mp3',
- 'chat-outgoing': undefined
- };
-
- return callback(null, defaults);
+ Meta.sounds.getMapping = function(uid, callback) {
+ async.parallel({
+ defaultMapping: function(next) {
+ db.getObject('settings:sounds', next);
+ },
+ userSettings: function(next) {
+ user.getSettings(uid, next);
}
+ }, function(err, results) {
+ if (err) {
+ return callback(err);
+ }
+ var userSettings = results.userSettings;
+ var defaultMapping = results.defaultMapping || {};
+ var soundMapping = {};
+ soundMapping.notification = (userSettings.notificationSound || userSettings.notificationSound === '') ?
+ userSettings.notificationSound : defaultMapping.notification || '';
- callback(null, sounds);
+ soundMapping['chat-incoming'] = (userSettings.incomingChatSound || userSettings.incomingChatSound === '') ?
+ userSettings.incomingChatSound : defaultMapping['chat-incoming'] || '';
+
+ soundMapping['chat-outgoing'] = (userSettings.outgoingChatSound || userSettings.outgoingChatSound === '') ?
+ userSettings.outgoingChatSound : defaultMapping['chat-outgoing'] || '';
+
+ callback(null, soundMapping);
});
};
diff --git a/src/meta/tags.js b/src/meta/tags.js
index 50afe9a1db..15b2135b21 100644
--- a/src/meta/tags.js
+++ b/src/meta/tags.js
@@ -14,7 +14,7 @@ module.exports = function(Meta) {
tags: function(next) {
var defaultTags = [{
name: 'viewport',
- content: 'width=device-width, initial-scale=1.0, user-scalable=no'
+ content: 'width=device-width, initial-scale=1.0'
}, {
name: 'content-type',
content: 'text/html; charset=UTF-8',
@@ -50,9 +50,6 @@ module.exports = function(Meta) {
}, {
rel: "manifest",
href: nconf.get('relative_path') + '/manifest.json'
- }, {
- rel: "prefetch",
- href: nconf.get('relative_path') + '/vendor/jquery/js/jquery-ui-1.10.4.custom.js' + (Meta.config['cache-buster'] ? '?v=' + Meta.config['cache-buster'] : '')
}];
// Touch icons for mobile-devices
@@ -89,6 +86,10 @@ module.exports = function(Meta) {
plugins.fireHook('filter:meta.getLinkTags', defaultLinks, next);
}
}, function(err, results) {
+ if (err) {
+ return callback(err);
+ }
+
meta = results.tags.concat(meta || []).map(function(tag) {
if (!tag || typeof tag.content !== 'string') {
winston.warn('Invalid meta tag. ', tag);
@@ -96,7 +97,7 @@ module.exports = function(Meta) {
}
if (!tag.noEscape) {
- tag.content = validator.escape(tag.content);
+ tag.content = validator.escape(String(tag.content));
}
return tag;
@@ -124,7 +125,7 @@ module.exports = function(Meta) {
if (!hasDescription) {
meta.push({
name: 'description',
- content: validator.escape(Meta.config.description || '')
+ content: validator.escape(String(Meta.config.description || ''))
});
}
}
diff --git a/src/meta/themes.js b/src/meta/themes.js
index c3d912a222..dd93a206c2 100644
--- a/src/meta/themes.js
+++ b/src/meta/themes.js
@@ -56,6 +56,10 @@ module.exports = function(Meta) {
});
}, function (err, themes) {
+ if (err) {
+ return callback(err);
+ }
+
themes = themes.filter(function (theme) {
return (theme !== undefined);
});
@@ -161,6 +165,4 @@ module.exports = function(Meta) {
nconf.set('theme_templates_path', themePath);
nconf.set('theme_config', path.join(nconf.get('themes_path'), themeObj.id, 'theme.json'));
};
-
-
};
\ No newline at end of file
diff --git a/src/middleware/admin.js b/src/middleware/admin.js
index 2ee0f7fd80..5e4399f226 100644
--- a/src/middleware/admin.js
+++ b/src/middleware/admin.js
@@ -1,130 +1,125 @@
"use strict";
-var app,
- middleware = {},
- nconf = require('nconf'),
- async = require('async'),
- winston = require('winston'),
- user = require('../user'),
- meta = require('../meta'),
- plugins = require('../plugins'),
+var async = require('async');
+var winston = require('winston');
+var user = require('../user');
+var meta = require('../meta');
+var plugins = require('../plugins');
- controllers = {
- api: require('../controllers/api'),
- helpers: require('../controllers/helpers')
- };
-
-middleware.isAdmin = function(req, res, next) {
- winston.warn('[middleware.admin.isAdmin] deprecation warning, no need to use this from plugins!');
-
- if (!req.user) {
- return controllers.helpers.notAllowed(req, res);
- }
-
- user.isAdministrator(req.user.uid, function (err, isAdmin) {
- if (err || isAdmin) {
- return next(err);
- }
-
- controllers.helpers.notAllowed(req, res);
- });
+var controllers = {
+ api: require('../controllers/api'),
+ helpers: require('../controllers/helpers')
};
-middleware.buildHeader = function(req, res, next) {
- res.locals.renderAdminHeader = true;
+module.exports = function(middleware) {
+ middleware.admin = {};
+ middleware.admin.isAdmin = function(req, res, next) {
+ winston.warn('[middleware.admin.isAdmin] deprecation warning, no need to use this from plugins!');
- async.parallel({
- config: function(next) {
- controllers.api.getConfig(req, res, next);
- },
- footer: function(next) {
- app.render('admin/footer', {}, next);
- }
- }, function(err, results) {
- if (err) {
- return next(err);
+ if (!req.user) {
+ return controllers.helpers.notAllowed(req, res);
}
- res.locals.config = results.config;
- res.locals.adminFooter = results.footer;
- next();
- });
-};
+ user.isAdministrator(req.user.uid, function (err, isAdmin) {
+ if (err || isAdmin) {
+ return next(err);
+ }
-middleware.renderHeader = function(req, res, data, next) {
- var custom_header = {
- 'plugins': [],
- 'authentication': []
+ controllers.helpers.notAllowed(req, res);
+ });
};
- user.getUserFields(req.uid, ['username', 'userslug', 'email', 'picture', 'email:confirmed'], function(err, userData) {
- if (err) {
- return next(err);
- }
-
- userData.uid = req.uid;
- userData['email:confirmed'] = parseInt(userData['email:confirmed'], 10) === 1;
+ middleware.admin.buildHeader = function(req, res, next) {
+ res.locals.renderAdminHeader = true;
async.parallel({
- scripts: function(next) {
- plugins.fireHook('filter:admin.scripts.get', [], function(err, scripts) {
- if (err) {
- return next(err);
- }
- var arr = [];
- scripts.forEach(function(script) {
- arr.push({src: script});
- });
-
- next(null, arr);
- });
- },
- custom_header: function(next) {
- plugins.fireHook('filter:admin.header.build', custom_header, next);
- },
config: function(next) {
controllers.api.getConfig(req, res, next);
},
- configs: function(next) {
- meta.configs.list(next);
+ footer: function(next) {
+ req.app.render('admin/footer', {}, next);
}
}, function(err, results) {
if (err) {
return next(err);
}
+
res.locals.config = results.config;
-
- var acpPath = req.path.slice(1).split('/');
- acpPath.forEach(function(path, i) {
- acpPath[i] = path.charAt(0).toUpperCase() + path.slice(1);
- });
- acpPath = acpPath.join(' > ');
-
- var templateValues = {
- config: results.config,
- configJSON: JSON.stringify(results.config),
- relative_path: results.config.relative_path,
- adminConfigJSON: encodeURIComponent(JSON.stringify(results.configs)),
- user: userData,
- userJSON: JSON.stringify(userData).replace(/'/g, "\\'"),
- plugins: results.custom_header.plugins,
- authentication: results.custom_header.authentication,
- scripts: results.scripts,
- 'cache-buster': meta.config['cache-buster'] ? 'v=' + meta.config['cache-buster'] : '',
- env: process.env.NODE_ENV ? true : false,
- title: (acpPath || 'Dashboard') + ' | NodeBB Admin Control Panel',
- bodyClass: data.bodyClass
- };
-
- templateValues.template = {name: res.locals.template};
- templateValues.template[res.locals.template] = true;
-
- app.render('admin/header', templateValues, next);
+ res.locals.adminFooter = results.footer;
+ next();
});
- });
-};
+ };
-module.exports = function(webserver) {
- app = webserver;
- return middleware;
+ middleware.admin.renderHeader = function(req, res, data, next) {
+ var custom_header = {
+ 'plugins': [],
+ 'authentication': []
+ };
+
+ user.getUserFields(req.uid, ['username', 'userslug', 'email', 'picture', 'email:confirmed'], function(err, userData) {
+ if (err) {
+ return next(err);
+ }
+
+ userData.uid = req.uid;
+ userData['email:confirmed'] = parseInt(userData['email:confirmed'], 10) === 1;
+
+ async.parallel({
+ scripts: function(next) {
+ plugins.fireHook('filter:admin.scripts.get', [], function(err, scripts) {
+ if (err) {
+ return next(err);
+ }
+ var arr = [];
+ scripts.forEach(function(script) {
+ arr.push({src: script});
+ });
+
+ next(null, arr);
+ });
+ },
+ custom_header: function(next) {
+ plugins.fireHook('filter:admin.header.build', custom_header, next);
+ },
+ config: function(next) {
+ controllers.api.getConfig(req, res, next);
+ },
+ configs: function(next) {
+ meta.configs.list(next);
+ }
+ }, function(err, results) {
+ if (err) {
+ return next(err);
+ }
+ res.locals.config = results.config;
+
+ var acpPath = req.path.slice(1).split('/');
+ acpPath.forEach(function(path, i) {
+ acpPath[i] = path.charAt(0).toUpperCase() + path.slice(1);
+ });
+ acpPath = acpPath.join(' > ');
+
+ var templateValues = {
+ config: results.config,
+ configJSON: JSON.stringify(results.config),
+ relative_path: results.config.relative_path,
+ adminConfigJSON: encodeURIComponent(JSON.stringify(results.configs)),
+ user: userData,
+ userJSON: JSON.stringify(userData).replace(/'/g, "\\'"),
+ plugins: results.custom_header.plugins,
+ authentication: results.custom_header.authentication,
+ scripts: results.scripts,
+ 'cache-buster': meta.config['cache-buster'] ? 'v=' + meta.config['cache-buster'] : '',
+ env: process.env.NODE_ENV ? true : false,
+ title: (acpPath || 'Dashboard') + ' | NodeBB Admin Control Panel',
+ bodyClass: data.bodyClass
+ };
+
+ templateValues.template = {name: res.locals.template};
+ templateValues.template[res.locals.template] = true;
+
+ req.app.render('admin/header', templateValues, next);
+ });
+ });
+ };
};
diff --git a/src/middleware/header.js b/src/middleware/header.js
index 4f25a55ca5..57c532f83e 100644
--- a/src/middleware/header.js
+++ b/src/middleware/header.js
@@ -2,6 +2,7 @@
var async = require('async');
var nconf = require('nconf');
+var validator = require('validator');
var db = require('../database');
var user = require('../user');
@@ -15,7 +16,7 @@ var controllers = {
helpers: require('../controllers/helpers')
};
-module.exports = function(app, middleware) {
+module.exports = function(middleware) {
middleware.buildHeader = function(req, res, next) {
res.locals.renderHeader = true;
@@ -27,7 +28,10 @@ module.exports = function(app, middleware) {
controllers.api.getConfig(req, res, next);
},
footer: function(next) {
- app.render('footer', {loggedIn: (req.user ? parseInt(req.user.uid, 10) !== 0 : false)}, next);
+ req.app.render('footer', {
+ loggedIn: !!req.uid,
+ title: validator.escape(String(meta.config.title || meta.config.browserTitle || 'NodeBB'))
+ }, next);
},
plugins: function(next) {
plugins.fireHook('filter:middleware.buildHeader', {req: req, locals: res.locals}, next);
@@ -116,6 +120,7 @@ module.exports = function(app, middleware) {
results.user.isAdmin = results.isAdmin;
results.user.isGlobalMod = results.isGlobalMod;
results.user.uid = parseInt(results.user.uid, 10);
+ results.user.email = String(results.user.email).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
results.user['email:confirmed'] = parseInt(results.user['email:confirmed'], 10) === 1;
results.user.isEmailConfirmSent = !!results.isEmailConfirmSent;
@@ -156,7 +161,7 @@ module.exports = function(app, middleware) {
return callback(err);
}
- app.render('header', data.templateValues, callback);
+ req.app.render('header', data.templateValues, callback);
});
});
};
diff --git a/src/middleware/headers.js b/src/middleware/headers.js
new file mode 100644
index 0000000000..39c9520e59
--- /dev/null
+++ b/src/middleware/headers.js
@@ -0,0 +1,51 @@
+'use strict';
+
+
+var meta = require('../meta');
+var _ = require('underscore');
+
+
+module.exports = function(middleware) {
+
+ middleware.addHeaders = function (req, res, next) {
+ var defaults = {
+ 'X-Powered-By': 'NodeBB',
+ 'X-Frame-Options': 'SAMEORIGIN',
+ 'Access-Control-Allow-Origin': 'null' // yes, string null.
+ };
+ var headers = {
+ 'X-Powered-By': meta.config['powered-by'],
+ 'X-Frame-Options': meta.config['allow-from-uri'] ? 'ALLOW-FROM ' + meta.config['allow-from-uri'] : undefined,
+ 'Access-Control-Allow-Origin': meta.config['access-control-allow-origin'],
+ 'Access-Control-Allow-Methods': meta.config['access-control-allow-methods'],
+ 'Access-Control-Allow-Headers': meta.config['access-control-allow-headers']
+ };
+
+ _.defaults(headers, defaults);
+ headers = _.pick(headers, Boolean); // Remove falsy headers
+
+ for(var key in headers) {
+ if (headers.hasOwnProperty(key)) {
+ res.setHeader(key, headers[key]);
+ }
+ }
+
+ next();
+ };
+
+ middleware.addExpiresHeaders = function(req, res, next) {
+ if (req.app.enabled('cache')) {
+ res.setHeader("Cache-Control", "public, max-age=5184000");
+ res.setHeader("Expires", new Date(Date.now() + 5184000000).toUTCString());
+ } else {
+ res.setHeader("Cache-Control", "public, max-age=0");
+ res.setHeader("Expires", new Date().toUTCString());
+ }
+
+ next();
+ };
+
+};
+
+
+
diff --git a/src/middleware/index.js b/src/middleware/index.js
index 867af6fed2..7d5b5a4e80 100644
--- a/src/middleware/index.js
+++ b/src/middleware/index.js
@@ -1,83 +1,202 @@
"use strict";
-var meta = require('../meta'),
- db = require('../database'),
- file = require('../file'),
- auth = require('../routes/authentication'),
+var async = require('async');
+var fs = require('fs');
+var path = require('path');
+var csrf = require('csurf');
+var validator = require('validator');
+var nconf = require('nconf');
+var ensureLoggedIn = require('connect-ensure-login');
+var toobusy = require('toobusy-js');
- path = require('path'),
- nconf = require('nconf'),
- flash = require('connect-flash'),
- templates = require('templates.js'),
- bodyParser = require('body-parser'),
- cookieParser = require('cookie-parser'),
- compression = require('compression'),
- favicon = require('serve-favicon'),
- session = require('express-session'),
- useragent = require('express-useragent');
+var plugins = require('../plugins');
+var languages = require('../languages');
+var meta = require('../meta');
+var user = require('../user');
+var groups = require('../groups');
+var analytics = require('../analytics');
+
+var controllers = {
+ api: require('./../controllers/api'),
+ helpers: require('../controllers/helpers')
+};
var middleware = {};
-function setupFavicon(app) {
- var faviconPath = path.join(__dirname, '../../', 'public', meta.config['brand:favicon'] ? meta.config['brand:favicon'] : 'favicon.ico');
- if (file.existsSync(faviconPath)) {
- app.use(nconf.get('relative_path'), favicon(faviconPath));
+middleware.applyCSRF = csrf();
+
+middleware.ensureLoggedIn = ensureLoggedIn.ensureLoggedIn(nconf.get('relative_path') + '/login');
+
+require('./admin')(middleware);
+require('./header')(middleware);
+require('./render')(middleware);
+require('./maintenance')(middleware);
+require('./user')(middleware);
+require('./headers')(middleware);
+
+middleware.authenticate = function(req, res, next) {
+ if (req.user) {
+ return next();
+ } else if (plugins.hasListeners('action:middleware.authenticate')) {
+ return plugins.fireHook('action:middleware.authenticate', {
+ req: req,
+ res: res,
+ next: next
+ });
}
+
+ controllers.helpers.notAllowed(req, res);
+};
+
+middleware.pageView = function(req, res, next) {
+ analytics.pageView({
+ ip: req.ip,
+ path: req.path,
+ uid: req.uid
+ });
+
+ plugins.fireHook('action:middleware.pageView', {req: req});
+
+ if (req.user) {
+ user.updateLastOnlineTime(req.user.uid);
+ if (req.path.startsWith('/api/users') || req.path.startsWith('/users')) {
+ user.updateOnlineUsers(req.user.uid, next);
+ } else {
+ user.updateOnlineUsers(req.user.uid);
+ next();
+ }
+ } else {
+ next();
+ }
+};
+
+
+middleware.pluginHooks = function(req, res, next) {
+ async.each(plugins.loadedHooks['filter:router.page'] || [], function(hookObj, next) {
+ hookObj.method(req, res, next);
+ }, function() {
+ // If it got here, then none of the subscribed hooks did anything, or there were no hooks
+ next();
+ });
+};
+
+middleware.validateFiles = function(req, res, next) {
+ if (!Array.isArray(req.files.files) || !req.files.files.length) {
+ return next(new Error(['[[error:invalid-files]]']));
+ }
+
+ next();
+};
+
+middleware.prepareAPI = function(req, res, next) {
+ res.locals.isAPI = true;
+ next();
+};
+
+middleware.routeTouchIcon = function(req, res) {
+ if (meta.config['brand:touchIcon'] && validator.isURL(meta.config['brand:touchIcon'])) {
+ return res.redirect(meta.config['brand:touchIcon']);
+ } else {
+ return res.sendFile(path.join(__dirname, '../../public', meta.config['brand:touchIcon'] || '/logo.png'), {
+ maxAge: req.app.enabled('cache') ? 5184000000 : 0
+ });
+ }
+};
+
+middleware.privateTagListing = function(req, res, next) {
+ if (!req.user && parseInt(meta.config.privateTagListing, 10) === 1) {
+ controllers.helpers.notAllowed(req, res);
+ } else {
+ next();
+ }
+};
+
+middleware.exposeGroupName = function(req, res, next) {
+ expose('groupName', groups.getGroupNameByGroupSlug, 'slug', req, res, next);
+};
+
+middleware.exposeUid = function(req, res, next) {
+ expose('uid', user.getUidByUserslug, 'userslug', req, res, next);
+};
+
+function expose(exposedField, method, field, req, res, next) {
+ if (!req.params.hasOwnProperty(field)) {
+ return next();
+ }
+ method(req.params[field], function(err, id) {
+ if (err) {
+ return next(err);
+ }
+
+ res.locals[exposedField] = id;
+ next();
+ });
}
-module.exports = function(app) {
- var relativePath = nconf.get('relative_path');
-
- middleware = require('./middleware')(app);
-
- app.engine('tpl', templates.__express);
- app.set('view engine', 'tpl');
- app.set('views', nconf.get('views_dir'));
- app.set('json spaces', process.env.NODE_ENV === 'development' ? 4 : 0);
- app.use(flash());
-
- app.enable('view cache');
-
- app.use(compression());
-
- setupFavicon(app);
-
- app.use(relativePath + '/apple-touch-icon', middleware.routeTouchIcon);
-
- app.use(bodyParser.urlencoded({extended: true}));
- app.use(bodyParser.json());
- app.use(cookieParser());
- app.use(useragent.express());
-
- var cookie = {
- maxAge: 1000 * 60 * 60 * 24 * (parseInt(meta.config.loginDays, 10) || 14)
- };
-
- if (meta.config.cookieDomain) {
- cookie.domain = meta.config.cookieDomain;
+middleware.privateUploads = function(req, res, next) {
+ if (req.user || parseInt(meta.config.privateUploads, 10) !== 1) {
+ return next();
}
-
- if (nconf.get('secure')) {
- cookie.secure = true;
+ if (req.path.startsWith('/uploads/files')) {
+ return res.status(403).json('not-allowed');
}
-
- if (relativePath !== '') {
- cookie.path = relativePath;
- }
-
- app.use(session({
- store: db.sessionStore,
- secret: nconf.get('secret'),
- key: 'express.sid',
- cookie: cookie,
- resave: true,
- saveUninitialized: true
- }));
-
- app.use(middleware.addHeaders);
- app.use(middleware.processRender);
- auth.initialize(app, middleware);
-
- return middleware;
+ next();
};
+
+middleware.busyCheck = function(req, res, next) {
+ if (global.env === 'production' && (!meta.config.hasOwnProperty('eventLoopCheckEnabled') || parseInt(meta.config.eventLoopCheckEnabled, 10) === 1) && toobusy()) {
+ analytics.increment('errors:503');
+ res.status(503).type('text/html').sendFile(path.join(__dirname, '../../public/503.html'));
+ } else {
+ next();
+ }
+};
+
+middleware.applyBlacklist = function(req, res, next) {
+ meta.blacklist.test(req.ip, function(err) {
+ next(err);
+ });
+};
+
+middleware.processLanguages = function(req, res, next) {
+ var code = req.params.code;
+ var key = req.path.match(/[\w]+\.json/);
+
+ if (code && key) {
+ languages.get(code, key[0], function(err, language) {
+ if (err) {
+ return next(err);
+ }
+
+ res.status(200).json(language);
+ });
+ } else {
+ res.status(404).json('{}');
+ }
+};
+
+middleware.processTimeagoLocales = function(req, res, next) {
+ var fallback = req.path.indexOf('-short') === -1 ? 'jquery.timeago.en.js' : 'jquery.timeago.en-short.js',
+ localPath = path.join(__dirname, '../../public/vendor/jquery/timeago/locales', req.path),
+ exists;
+
+ try {
+ exists = fs.accessSync(localPath, fs.F_OK | fs.R_OK);
+ } catch(e) {
+ exists = false;
+ }
+
+ if (exists) {
+ res.status(200).sendFile(localPath, {
+ maxAge: req.app.enabled('cache') ? 5184000000 : 0
+ });
+ } else {
+ res.status(200).sendFile(path.join(__dirname, '../../public/vendor/jquery/timeago/locales', fallback), {
+ maxAge: req.app.enabled('cache') ? 5184000000 : 0
+ });
+ }
+};
+
+
+module.exports = middleware;
diff --git a/src/middleware/maintenance.js b/src/middleware/maintenance.js
index f03a804bb9..33079b88ef 100644
--- a/src/middleware/maintenance.js
+++ b/src/middleware/maintenance.js
@@ -3,8 +3,6 @@
var nconf = require('nconf');
var meta = require('../meta');
var user = require('../user');
-var translator = require('../../public/src/modules/translator');
-
module.exports = function(middleware) {
@@ -15,42 +13,46 @@ module.exports = function(middleware) {
var url = req.url.replace(nconf.get('relative_path'), '');
var allowedRoutes = [
- '^/login',
- '^/stylesheet.css',
- '^/favicon',
- '^/nodebb.min.js',
- '^/vendor/fontawesome/fonts/fontawesome-webfont.woff',
- '^/src/(modules|client)/[\\w/]+.js',
- '^/templates/[\\w/]+.tpl',
- '^/api/login',
- '^/api/widgets/render',
- '^/language/.+',
- '^/uploads/system/site-logo.png'
- ],
- render = function() {
- res.status(503);
- var data = {
- site_title: meta.config.title || 'NodeBB',
- message: meta.config.maintenanceModeMessage
- };
- if (!isApiRoute.test(url)) {
- middleware.buildHeader(req, res, function() {
- res.render('503', data);
- });
- } else {
- res.json(data);
+ '^/ping',
+ '^/sping',
+ '^/login',
+ '^/stylesheet.css',
+ '^/favicon',
+ '^/nodebb.min.js',
+ '^/vendor/fontawesome/fonts/fontawesome-webfont.woff',
+ '^/src/(modules|client)/[\\w/]+.js',
+ '^/templates/[\\w/]+.tpl',
+ '^/api/login',
+ '^/api/widgets/render',
+ '^/language/.+',
+ '^/uploads/system/site-logo.png'
+ ];
+ var render = function() {
+ res.status(503);
+ var data = {
+ site_title: meta.config.title || 'NodeBB',
+ message: meta.config.maintenanceModeMessage
+ };
+ if (!isApiRoute.test(url)) {
+ middleware.buildHeader(req, res, function() {
+ res.render('503', data);
+ });
+ } else {
+ res.json(data);
+ }
+ };
+
+ var isAllowed = function(url) {
+ for(var x=0,numAllowed=allowedRoutes.length,route;x Date.now() - 3600000) {
- var timeLeft = parseInt(loginTime, 10) - (Date.now() - 3600000);
- if (timeLeft < 300000) {
- req.session.meta.datetime += 300000;
- }
-
- return next();
- }
-
- req.session.returnTo = req.path.replace(/^\/api/, '');
- req.session.forceLogin = 1;
- if (res.locals.isAPI) {
- res.status(401).json({});
- } else {
- res.redirect(nconf.get('relative_path') + '/login');
- }
- });
- return;
- }
-
- if (res.locals.isAPI) {
- return controllers.helpers.notAllowed(req, res);
- }
-
- middleware.buildHeader(req, res, function() {
- controllers.helpers.notAllowed(req, res);
- });
- });
-};
-
-middleware.routeTouchIcon = function(req, res) {
- if (meta.config['brand:touchIcon'] && validator.isURL(meta.config['brand:touchIcon'])) {
- return res.redirect(meta.config['brand:touchIcon']);
- } else {
- return res.sendFile(path.join(__dirname, '../../public', meta.config['brand:touchIcon'] || '/logo.png'), {
- maxAge: app.enabled('cache') ? 5184000000 : 0
- });
- }
-};
-
-middleware.addExpiresHeaders = function(req, res, next) {
- if (app.enabled('cache')) {
- res.setHeader("Cache-Control", "public, max-age=5184000");
- res.setHeader("Expires", new Date(Date.now() + 5184000000).toUTCString());
- } else {
- res.setHeader("Cache-Control", "public, max-age=0");
- res.setHeader("Expires", new Date().toUTCString());
- }
-
- next();
-};
-
-middleware.privateTagListing = function(req, res, next) {
- if (!req.user && parseInt(meta.config.privateTagListing, 10) === 1) {
- controllers.helpers.notAllowed(req, res);
- } else {
- next();
- }
-};
-
-middleware.exposeGroupName = function(req, res, next) {
- expose('groupName', groups.getGroupNameByGroupSlug, 'slug', req, res, next);
-};
-
-middleware.exposeUid = function(req, res, next) {
- expose('uid', user.getUidByUserslug, 'userslug', req, res, next);
-};
-
-function expose(exposedField, method, field, req, res, next) {
- if (!req.params.hasOwnProperty(field)) {
- return next();
- }
- method(req.params[field], function(err, id) {
- if (err) {
- return next(err);
- }
-
- res.locals[exposedField] = id;
- next();
- });
-}
-
-middleware.requireUser = function(req, res, next) {
- if (req.user) {
- return next();
- }
-
- res.status(403).render('403', {title: '[[global:403.title]]'});
-};
-
-middleware.privateUploads = function(req, res, next) {
- if (req.user || parseInt(meta.config.privateUploads, 10) !== 1) {
- return next();
- }
- if (req.path.startsWith('/uploads/files')) {
- return res.status(403).json('not-allowed');
- }
- next();
-};
-
-middleware.busyCheck = function(req, res, next) {
- if (global.env === 'production' && (!meta.config.hasOwnProperty('eventLoopCheckEnabled') || parseInt(meta.config.eventLoopCheckEnabled, 10) === 1) && toobusy()) {
- analytics.increment('errors:503');
- res.status(503).type('text/html').sendFile(path.join(__dirname, '../../public/503.html'));
- } else {
- next();
- }
-};
-
-middleware.applyBlacklist = function(req, res, next) {
- meta.blacklist.test(req.ip, function(err) {
- next(err);
- });
-};
-
-middleware.processLanguages = function(req, res, next) {
- var code = req.params.code;
- var key = req.path.match(/[\w]+\.json/);
-
- if (code && key) {
- languages.get(code, key[0], function(err, language) {
- res.status(200).json(language);
- });
- } else {
- res.status(404).json('{}');
- }
-};
-
-middleware.processTimeagoLocales = function(req, res, next) {
- var fallback = req.path.indexOf('-short') === -1 ? 'jquery.timeago.en.js' : 'jquery.timeago.en-short.js',
- localPath = path.join(__dirname, '../../public/vendor/jquery/timeago/locales', req.path),
- exists;
-
- try {
- exists = fs.accessSync(localPath, fs.F_OK | fs.R_OK);
- } catch(e) {
- exists = false;
- }
-
- if (exists) {
- res.status(200).sendFile(localPath, {
- maxAge: app.enabled('cache') ? 5184000000 : 0
- });
- } else {
- res.status(200).sendFile(path.join(__dirname, '../../public/vendor/jquery/timeago/locales', fallback), {
- maxAge: app.enabled('cache') ? 5184000000 : 0
- });
- }
-};
-
-middleware.registrationComplete = function(req, res, next) {
- // If the user's session contains registration data, redirect the user to complete registration
- if (!req.session.hasOwnProperty('registration')) {
- return next();
- } else {
- if (!req.path.endsWith('/register/complete')) {
- controllers.helpers.redirect(res, '/register/complete');
- } else {
- return next();
- }
- }
-};
-
-module.exports = function(webserver) {
- app = webserver;
- middleware.admin = require('./admin')(webserver);
-
- require('./header')(app, middleware);
- require('./render')(middleware);
- require('./maintenance')(middleware);
-
- return middleware;
-};
diff --git a/src/middleware/render.js b/src/middleware/render.js
index 63c5662268..b25f1d63f0 100644
--- a/src/middleware/render.js
+++ b/src/middleware/render.js
@@ -81,7 +81,7 @@ module.exports = function(middleware) {
}
str = template + str;
var language = res.locals.config ? res.locals.config.userLang || 'en_GB' : 'en_GB';
- language = req.query.lang ? validator.escape(req.query.lang) : language;
+ language = req.query.lang ? validator.escape(String(req.query.lang)) : language;
translator.translate(str, language, function(translated) {
translated = translator.unescape(translated);
translated = translated + '';
diff --git a/src/middleware/user.js b/src/middleware/user.js
new file mode 100644
index 0000000000..b70f7639d7
--- /dev/null
+++ b/src/middleware/user.js
@@ -0,0 +1,154 @@
+'use strict';
+
+var async = require('async');
+var nconf = require('nconf');
+var meta = require('../meta');
+var user = require('../user');
+
+var controllers = {
+ helpers: require('../controllers/helpers')
+};
+
+module.exports = function(middleware) {
+
+ middleware.checkGlobalPrivacySettings = function(req, res, next) {
+ if (!req.user && !!parseInt(meta.config.privateUserInfo, 10)) {
+ return controllers.helpers.notAllowed(req, res);
+ }
+
+ next();
+ };
+
+ middleware.checkAccountPermissions = function(req, res, next) {
+ // This middleware ensures that only the requested user and admins can pass
+ async.waterfall([
+ function (next) {
+ middleware.authenticate(req, res, next);
+ },
+ function (next) {
+ user.getUidByUserslug(req.params.userslug, next);
+ },
+ function (uid, next) {
+ if (parseInt(uid, 10) === req.uid) {
+ return next(null, true);
+ }
+
+ user.isAdminOrGlobalMod(req.uid, next);
+ }
+ ], function (err, allowed) {
+ if (err || allowed) {
+ return next(err);
+ }
+ controllers.helpers.notAllowed(req, res);
+ });
+ };
+
+ middleware.redirectToAccountIfLoggedIn = function(req, res, next) {
+ if (req.session.forceLogin) {
+ return next();
+ }
+
+ if (!req.user) {
+ return next();
+ }
+ user.getUserField(req.user.uid, 'userslug', function (err, userslug) {
+ if (err) {
+ return next(err);
+ }
+ controllers.helpers.redirect(res, '/user/' + userslug);
+ });
+ };
+
+ middleware.redirectUidToUserslug = function(req, res, next) {
+ var uid = parseInt(req.params.uid, 10);
+ if (!uid) {
+ return next();
+ }
+ user.getUserField(uid, 'userslug', function(err, userslug) {
+ if (err || !userslug) {
+ return next(err);
+ }
+
+ var path = req.path.replace(/^\/api/, '')
+ .replace('uid', 'user')
+ .replace(uid, function() { return userslug; });
+ controllers.helpers.redirect(res, path);
+ });
+ };
+
+ middleware.isAdmin = function(req, res, next) {
+ if (!req.uid) {
+ return controllers.helpers.notAllowed(req, res);
+ }
+
+ user.isAdministrator(req.uid, function (err, isAdmin) {
+ if (err) {
+ return next(err);
+ }
+
+ if (isAdmin) {
+ user.hasPassword(req.uid, function(err, hasPassword) {
+ if (err) {
+ return next(err);
+ }
+
+ if (!hasPassword) {
+ return next();
+ }
+
+ var loginTime = req.session.meta ? req.session.meta.datetime : 0;
+ if (loginTime && parseInt(loginTime, 10) > Date.now() - 3600000) {
+ var timeLeft = parseInt(loginTime, 10) - (Date.now() - 3600000);
+ if (timeLeft < 300000) {
+ req.session.meta.datetime += 300000;
+ }
+
+ return next();
+ }
+
+ req.session.returnTo = req.path.replace(/^\/api/, '');
+ req.session.forceLogin = 1;
+ if (res.locals.isAPI) {
+ res.status(401).json({});
+ } else {
+ res.redirect(nconf.get('relative_path') + '/login');
+ }
+ });
+ return;
+ }
+
+ if (res.locals.isAPI) {
+ return controllers.helpers.notAllowed(req, res);
+ }
+
+ middleware.buildHeader(req, res, function() {
+ controllers.helpers.notAllowed(req, res);
+ });
+ });
+ };
+
+ middleware.requireUser = function(req, res, next) {
+ if (req.user) {
+ return next();
+ }
+
+ res.status(403).render('403', {title: '[[global:403.title]]'});
+ };
+
+ middleware.registrationComplete = function(req, res, next) {
+ // If the user's session contains registration data, redirect the user to complete registration
+ if (!req.session.hasOwnProperty('registration')) {
+ return next();
+ } else {
+ if (!req.path.endsWith('/register/complete')) {
+ controllers.helpers.redirect(res, '/register/complete');
+ } else {
+ return next();
+ }
+ }
+ };
+
+};
+
+
+
diff --git a/src/notifications.js b/src/notifications.js
index 1d466f374b..e4df61f260 100644
--- a/src/notifications.js
+++ b/src/notifications.js
@@ -37,50 +37,39 @@ var utils = require('../public/src/utils');
return callback(err);
}
- if (!Array.isArray(notifications) || !notifications.length) {
+ notifications = notifications.filter(Boolean);
+ if (!notifications.length) {
return callback(null, []);
}
- async.map(notifications, function(notification, next) {
- if (!notification) {
- return next(null, null);
+ var userKeys = notifications.map(function(notification) {
+ return notification.from;
+ });
+
+ User.getUsersFields(userKeys, ['username', 'userslug', 'picture'], function(err, usersData) {
+ if (err) {
+ return callback(err);
}
+ notifications.forEach(function(notification, index) {
+ notification.datetimeISO = utils.toISOString(notification.datetime);
- notification.datetimeISO = utils.toISOString(notification.datetime);
-
- if (notification.bodyLong) {
- notification.bodyLong = S(notification.bodyLong).escapeHTML().s;
- }
-
- if (notification.from && !notification.image) {
- User.getUserFields(notification.from, ['username', 'userslug', 'picture'], function(err, userData) {
- if (err) {
- return next(err);
- }
- notification.image = userData.picture || null;
- notification.user = userData;
-
- if (userData.username === '[[global:guest]]') {
- notification.bodyShort = notification.bodyShort.replace(/([\s\S]*?),[\s\S]*?,([\s\S]*?)/, '$1, [[global:guest]], $2');
- }
-
- next(null, notification);
- });
- return;
- } else if (notification.image) {
- switch(notification.image) {
- case 'brand:logo':
- notification.image = meta.config['brand:logo'] || nconf.get('relative_path') + '/logo.png';
- break;
+ if (notification.bodyLong) {
+ notification.bodyLong = S(notification.bodyLong).escapeHTML().s;
}
- return next(null, notification);
- } else {
- notification.image = meta.config['brand:logo'] || nconf.get('relative_path') + '/logo.png';
- return next(null, notification);
- }
+ notification.user = usersData[index];
+ if (notification.user) {
+ notification.image = notification.user.picture || null;
+ if (notification.user.username === '[[global:guest]]') {
+ notification.bodyShort = notification.bodyShort.replace(/([\s\S]*?),[\s\S]*?,([\s\S]*?)/, '$1, [[global:guest]], $2');
+ }
+ } else if (notification.image === 'brand:logo' || !notification.image) {
+ notification.image = meta.config['brand:logo'] || nconf.get('relative_path') + '/logo.png';
+ }
+ });
- }, callback);
+ callback(null, notifications);
+ });
});
};
@@ -495,6 +484,8 @@ var utils = require('../public/src/utils');
} else if (numUsers > 2) {
notifications[modifyIndex].bodyShort = '[[' + mergeId + '_multiple, ' + usernames[0] + ', ' + (numUsers - 1) + titleEscaped + ']]';
}
+
+ notifications[modifyIndex].path = set[set.length-1].path;
break;
case 'new_register':
diff --git a/src/pagination.js b/src/pagination.js
index 15b8aa6eb3..69896ccd3f 100644
--- a/src/pagination.js
+++ b/src/pagination.js
@@ -35,6 +35,8 @@ pagination.create = function(currentPage, pageCount, queryObj) {
queryObj = queryObj || {};
+ delete queryObj._;
+
var pages = pagesToShow.map(function(page) {
queryObj.page = page;
return {page: page, active: page === currentPage, qs: qs.stringify(queryObj)};
diff --git a/src/password.js b/src/password.js
index 13f8c11f72..20f4c79a95 100644
--- a/src/password.js
+++ b/src/password.js
@@ -12,9 +12,11 @@
};
function forkChild(message, callback) {
- var child = fork('./bcrypt', {
- silent: true
- });
+ var forkProcessParams = {};
+ if(global.v8debug || parseInt(process.execArgv.indexOf('--debug'), 10) !== -1) {
+ forkProcessParams = {execArgv: ['--debug=' + (5859), '--nolazy']};
+ }
+ var child = fork('./bcrypt', [], forkProcessParams);
child.on('message', function(msg) {
if (msg.err) {
diff --git a/src/plugins.js b/src/plugins.js
index 6eba5de2ce..af3bc887c9 100644
--- a/src/plugins.js
+++ b/src/plugins.js
@@ -10,12 +10,10 @@ var nconf = require('nconf');
var db = require('./database');
var emitter = require('./emitter');
-var translator = require('../public/src/modules/translator');
var utils = require('../public/src/utils');
var hotswap = require('./hotswap');
var file = require('./file');
-var controllers = require('./controllers');
var app;
var middleware;
@@ -94,6 +92,10 @@ var middleware;
function(next) {
// Build language code list
fs.readdir(path.join(__dirname, '../public/language'), function(err, directories) {
+ if (err) {
+ return next(err);
+ }
+
Plugins.languageCodes = directories.filter(function(code) {
return code !== 'TODO';
});
@@ -145,11 +147,13 @@ var middleware;
Plugins.reloadRoutes = function(callback) {
callback = callback || function() {};
var router = express.Router();
+
router.hotswapId = 'plugins';
router.render = function() {
app.render.apply(app, arguments);
};
+ var controllers = require('./controllers');
Plugins.fireHook('static:app.load', {app: app, router: router, middleware: middleware, controllers: controllers}, function(err) {
if (err) {
return winston.error('[plugins] Encountered error while executing post-router plugins hooks: ' + err.message);
@@ -206,7 +210,11 @@ var middleware;
}
});
} else {
- winston.warn('[plugins/' + plugin.id + '] A templates directory was defined for this plugin, but was not found.');
+ if (err) {
+ winston.error(err);
+ } else {
+ winston.warn('[plugins/' + plugin.id + '] A templates directory was defined for this plugin, but was not found.');
+ }
}
next(false);
@@ -252,6 +260,7 @@ var middleware;
}, function(err, res, body) {
if (err) {
winston.error('Error parsing plugins : ' + err.message);
+ return callback(err);
}
Plugins.normalise(body, callback);
@@ -392,37 +401,10 @@ var middleware;
next();
});
}, function(err) {
- next(null, plugins);
+ next(err, plugins);
});
}
], callback);
};
- Plugins.clearRequireCache = function(next) {
- var cached = Object.keys(require.cache);
- async.waterfall([
- async.apply(async.map, Plugins.libraryPaths, fs.realpath),
- function(paths, next) {
- paths = paths.map(function(pluginLib) {
- var parent = path.dirname(pluginLib);
- return cached.filter(function(libPath) {
- return libPath.indexOf(parent) !== -1;
- });
- }).reduce(function(prev, cur) {
- return prev.concat(cur);
- });
-
- Plugins.fireHook('filter:plugins.clearRequireCache', {paths: paths}, next);
- },
- function(data, next) {
- for (var x=0,numPaths=data.paths.length;x postDeleteDuration * 1000)) {
- return callback(new Error('[[error:post-delete-duration-expired, ' + meta.config.postDeleteDuration + ']]'));
+ return callback(null, {flag: false, message: '[[error:post-delete-duration-expired, ' + meta.config.postDeleteDuration + ']]'});
}
- callback(null, results.isOwner);
+
+ callback(null, {flag: results.isOwner, message: '[[error:no-privileges]]'});
});
};
@@ -218,26 +223,31 @@ module.exports = function(privileges) {
};
function isPostEditable(pid, uid, callback) {
+ var tid;
async.waterfall([
function(next) {
posts.getPostFields(pid, ['tid', 'timestamp'], next);
},
function(postData, next) {
+ tid = postData.tid;
var postEditDuration = parseInt(meta.config.postEditDuration, 10);
if (postEditDuration && Date.now() - parseInt(postData.timestamp, 10) > postEditDuration * 1000) {
- return callback(null, {isEditExpired: true});
+ return callback(null, {flag: false, message: '[[error:post-edit-duration-expired, ' + meta.config.postEditDuration + ']]'});
}
topics.isLocked(postData.tid, next);
},
function(isLocked, next) {
if (isLocked) {
- return callback(null, {isLocked: true});
+ return callback(null, {flag: false, message: '[[error:topic-locked]]'});
}
- posts.isOwner(pid, uid, next);
+ async.parallel({
+ owner: async.apply(posts.isOwner, pid, uid),
+ edit: async.apply(privileges.posts.can, 'posts:edit', pid, uid)
+ }, next);
},
- function(isOwner, next) {
- next(null, {editable: isOwner});
+ function(result, next) {
+ next(null, {flag: result.owner && result.edit, message: '[[error:no-privileges]]'});
}
], callback);
}
@@ -258,4 +268,4 @@ module.exports = function(privileges) {
}
], callback);
}
-};
+};
\ No newline at end of file
diff --git a/src/privileges/topics.js b/src/privileges/topics.js
index d1c8958045..921acc2481 100644
--- a/src/privileges/topics.js
+++ b/src/privileges/topics.js
@@ -2,7 +2,9 @@
'use strict';
var async = require('async');
+var _ = require('underscore');
+var meta = require('../meta');
var topics = require('../topics');
var user = require('../user');
var helpers = require('./helpers');
@@ -15,17 +17,13 @@ module.exports = function(privileges) {
privileges.topics.get = function(tid, uid, callback) {
var topic;
+ var privs = ['topics:reply', 'topics:read', 'topics:delete', 'posts:edit', 'posts:delete', 'read'];
async.waterfall([
- async.apply(topics.getTopicFields, tid, ['cid', 'uid', 'locked']),
+ async.apply(topics.getTopicFields, tid, ['cid', 'uid', 'locked', 'deleted']),
function(_topic, next) {
topic = _topic;
async.parallel({
- 'topics:reply': async.apply(helpers.isUserAllowedTo, 'topics:reply', uid, [topic.cid]),
- 'topics:read': async.apply(helpers.isUserAllowedTo, 'topics:read', uid, [topic.cid]),
- read: async.apply(helpers.isUserAllowedTo, 'read', uid, [topic.cid]),
- isOwner: function(next) {
- next(null, !!parseInt(uid, 10) && parseInt(uid, 10) === parseInt(topic.uid, 10));
- },
+ privileges: async.apply(helpers.isUserAllowedTo, privs, uid, topic.cid),
isAdministrator: async.apply(user.isAdministrator, uid),
isModerator: async.apply(user.isModerator, uid, topic.cid),
disabled: async.apply(categories.getCategoryField, topic.cid, 'disabled')
@@ -36,20 +34,26 @@ module.exports = function(privileges) {
return callback(err);
}
+ var privData = _.object(privs, results.privileges);
var disabled = parseInt(results.disabled, 10) === 1;
var locked = parseInt(topic.locked, 10) === 1;
+ var deleted = parseInt(topic.deleted, 10) === 1;
+ var isOwner = !!parseInt(uid, 10) && parseInt(uid, 10) === parseInt(topic.uid, 10);
var isAdminOrMod = results.isAdministrator || results.isModerator;
var editable = isAdminOrMod;
- var deletable = isAdminOrMod || results.isOwner;
+ var deletable = isAdminOrMod || (isOwner && privData['topics:delete']);
plugins.fireHook('filter:privileges.topics.get', {
- 'topics:reply': (results['topics:reply'][0] && !locked) || isAdminOrMod,
- read: results.read[0] || isAdminOrMod,
- 'topics:read': results['topics:read'][0] || isAdminOrMod,
+ 'topics:reply': (privData['topics:reply'] && !locked && !deleted) || isAdminOrMod,
+ 'topics:read': privData['topics:read'] || isAdminOrMod,
+ 'topics:delete': (isOwner && privData['topics:delete']) || isAdminOrMod,
+ 'posts:edit': (privData['posts:edit'] && !locked) || isAdminOrMod,
+ 'posts:delete': (privData['posts:delete'] && !locked) || isAdminOrMod,
+ read: privData.read || isAdminOrMod,
view_thread_tools: editable || deletable,
editable: editable,
deletable: deletable,
- view_deleted: isAdminOrMod || results.isOwner,
+ view_deleted: isAdminOrMod || isOwner,
isAdminOrMod: isAdminOrMod,
disabled: disabled,
tid: tid,
@@ -176,6 +180,46 @@ module.exports = function(privileges) {
], callback);
};
+ privileges.topics.canDelete = function(tid, uid, callback) {
+ var topicData;
+ async.waterfall([
+ function(next) {
+ topics.getTopicFields(tid, ['cid', 'postcount'], next);
+ },
+ function(_topicData, next) {
+ topicData = _topicData;
+ async.parallel({
+ isModerator: async.apply(user.isModerator, uid, topicData.cid),
+ isAdministrator: async.apply(user.isAdministrator, uid),
+ isOwner: async.apply(topics.isOwner, tid, uid),
+ 'topics:delete': async.apply(helpers.isUserAllowedTo, 'topics:delete', uid, [topicData.cid])
+ }, next);
+ }
+ ], function(err, results) {
+ if (err) {
+ return callback(err);
+ }
+
+ if (results.isModerator || results.isAdministrator) {
+ return callback(null, true);
+ }
+
+ var preventTopicDeleteAfterReplies = parseInt(meta.config.preventTopicDeleteAfterReplies, 10) || 0;
+ if (preventTopicDeleteAfterReplies && (topicData.postcount - 1) >= preventTopicDeleteAfterReplies) {
+ var langKey = preventTopicDeleteAfterReplies > 1 ?
+ '[[error:cant-delete-topic-has-replies, ' + meta.config.preventTopicDeleteAfterReplies + ']]':
+ '[[error:cant-delete-topic-has-reply]]';
+ return callback(new Error(langKey));
+ }
+
+ if (!results['topics:delete'][0]) {
+ return callback(null, false);
+ }
+
+ callback(null, results.isOwner);
+ });
+ };
+
privileges.topics.canEdit = function(tid, uid, callback) {
privileges.topics.isOwnerOrAdminOrMod(tid, uid, callback);
};
@@ -207,4 +251,4 @@ module.exports = function(privileges) {
}
], callback);
};
-};
+};
\ No newline at end of file
diff --git a/src/privileges/users.js b/src/privileges/users.js
index 4ee2d69461..e49ca793ae 100644
--- a/src/privileges/users.js
+++ b/src/privileges/users.js
@@ -136,4 +136,4 @@ module.exports = function(privileges) {
});
}
-};
+};
\ No newline at end of file
diff --git a/src/reset.js b/src/reset.js
index f1e27738e8..56669b6e2d 100644
--- a/src/reset.js
+++ b/src/reset.js
@@ -82,7 +82,12 @@ function resetTheme(themeId) {
type: 'local',
id: themeId
}, function(err) {
- winston.info('[reset] Theme reset to ' + themeId);
+ if (err) {
+ winston.warn('[reset] Failed to reset theme to ' + themeId);
+ } else {
+ winston.info('[reset] Theme reset to ' + themeId);
+ }
+
process.exit();
});
}
diff --git a/src/rewards/admin.js b/src/rewards/admin.js
index e6c32bc34b..0bde13ac00 100644
--- a/src/rewards/admin.js
+++ b/src/rewards/admin.js
@@ -46,6 +46,10 @@ rewards.save = function(data, callback) {
}
async.each(data, save, function(err) {
+ if (err) {
+ return callback(err);
+ }
+
saveConditions(data, callback);
});
};
@@ -125,6 +129,10 @@ function getActiveRewards(callback) {
}
db.getSetMembers('rewards:list', function(err, rewards) {
+ if (err) {
+ return callback(err);
+ }
+
async.eachSeries(rewards, load, function(err) {
callback(err, activeRewards);
});
diff --git a/src/rewards/index.js b/src/rewards/index.js
index c811ce933c..bbccab69f3 100644
--- a/src/rewards/index.js
+++ b/src/rewards/index.js
@@ -111,14 +111,22 @@ function getRewardsByRewardData(rewards, callback) {
function checkCondition(reward, method, callback) {
method(function(err, value) {
+ if (err) {
+ return callback(err);
+ }
+
plugins.fireHook('filter:rewards.checkConditional:' + reward.conditional, {left: value, right: reward.value}, function(err, bool) {
- callback(bool);
+ callback(err || bool);
});
});
}
function giveRewards(uid, rewards, callback) {
getRewardsByRewardData(rewards, function(err, rewardData) {
+ if (err) {
+ return callback(err);
+ }
+
async.each(rewards, function(reward, next) {
plugins.fireHook('action:rewards.award:' + reward.rid, {uid: uid, reward: rewardData[rewards.indexOf(reward)]});
db.sortedSetIncrBy('uid:' + uid + ':rewards', 1, reward.id, next);
diff --git a/src/routes/accounts.js b/src/routes/accounts.js
index 5bd0a474e3..9d17b8f86a 100644
--- a/src/routes/accounts.js
+++ b/src/routes/accounts.js
@@ -5,7 +5,7 @@ var setupPageRoute = helpers.setupPageRoute;
module.exports = function (app, middleware, controllers) {
var middlewares = [middleware.checkGlobalPrivacySettings];
- var accountMiddlewares = [middleware.checkGlobalPrivacySettings, middleware.checkAccountPermissions, middleware.exposeUid];
+ var accountMiddlewares = [middleware.checkGlobalPrivacySettings, middleware.checkAccountPermissions];
setupPageRoute(app, '/uid/:uid/:section?', middleware, [], middleware.redirectUidToUserslug);
@@ -28,8 +28,9 @@ module.exports = function (app, middleware, controllers) {
setupPageRoute(app, '/user/:userslug/info', middleware, accountMiddlewares, controllers.accounts.info.get);
setupPageRoute(app, '/user/:userslug/settings', middleware, accountMiddlewares, controllers.accounts.settings.get);
- app.delete('/api/user/:userslug/session/:uuid', [middleware.requireUser, middleware.exposeUid], controllers.accounts.session.revoke);
+ app.delete('/api/user/:userslug/session/:uuid', [middleware.requireUser], controllers.accounts.session.revoke);
setupPageRoute(app, '/notifications', middleware, [middleware.authenticate], controllers.accounts.notifications.get);
- setupPageRoute(app, '/chats/:roomid?', middleware, [middleware.authenticate], controllers.accounts.chats.get);
+ setupPageRoute(app, '/user/:userslug/chats/:roomid?', middleware, accountMiddlewares, controllers.accounts.chats.get);
+ setupPageRoute(app, '/chats/:roomid?', middleware, [], controllers.accounts.chats.redirectToChat);
};
diff --git a/src/routes/admin.js b/src/routes/admin.js
index 543b9e8489..b84e744685 100644
--- a/src/routes/admin.js
+++ b/src/routes/admin.js
@@ -85,7 +85,7 @@ function addRoutes(router, middleware, controllers) {
router.get('/advanced/logs', middlewares, controllers.admin.logs.get);
router.get('/advanced/errors', middlewares, controllers.admin.errors.get);
router.get('/advanced/errors/export', middlewares, controllers.admin.errors.export);
- router.get('/advanced/post-cache', middlewares, controllers.admin.postCache.get);
+ router.get('/advanced/cache', middlewares, controllers.admin.cache.get);
router.get('/development/logger', middlewares, controllers.admin.logger.get);
router.get('/development/info', middlewares, controllers.admin.info.get);
diff --git a/src/routes/debug.js b/src/routes/debug.js
index b81938ccc9..87c536c4c6 100644
--- a/src/routes/debug.js
+++ b/src/routes/debug.js
@@ -1,11 +1,13 @@
"use strict";
-var express = require('express'),
- nconf = require('nconf'),
- user = require('./../user'),
- categories = require('./../categories'),
- topics = require('./../topics'),
- posts = require('./../posts');
+var express = require('express');
+var nconf = require('nconf');
+var winston = require('winston');
+var user = require('../user');
+var categories = require('../categories');
+var topics = require('../topics');
+var posts = require('../posts');
+var db = require('../database');
module.exports = function(app, middleware, controllers) {
var router = express.Router();
@@ -16,6 +18,10 @@ module.exports = function(app, middleware, controllers) {
}
user.getUserData(req.params.uid, function (err, data) {
+ if (err) {
+ winston.error(err);
+ }
+
if (data) {
res.send(data);
} else {
@@ -28,6 +34,10 @@ module.exports = function(app, middleware, controllers) {
router.get('/cid/:cid', function (req, res) {
categories.getCategoryData(req.params.cid, function (err, data) {
+ if (err) {
+ winston.error(err);
+ }
+
if (data) {
res.send(data);
} else {
@@ -38,6 +48,10 @@ module.exports = function(app, middleware, controllers) {
router.get('/tid/:tid', function (req, res) {
topics.getTopicData(req.params.tid, function (err, data) {
+ if (err) {
+ winston.error(err);
+ }
+
if (data) {
res.send(data);
} else {
@@ -48,6 +62,10 @@ module.exports = function(app, middleware, controllers) {
router.get('/pid/:pid', function (req, res) {
posts.getPostData(req.params.pid, function (err, data) {
+ if (err) {
+ winston.error(err);
+ }
+
if (data) {
res.send(data);
} else {
diff --git a/src/routes/index.js b/src/routes/index.js
index 1e51f5d59e..b4248120c9 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -3,15 +3,12 @@
var nconf = require('nconf');
var path = require('path');
var async = require('async');
-var winston = require('winston');
var controllers = require('../controllers');
var plugins = require('../plugins');
var user = require('../user');
var express = require('express');
-var validator = require('validator');
var accountRoutes = require('./accounts');
-
var metaRoutes = require('./meta');
var apiRoutes = require('./api');
var adminRoutes = require('./admin');
@@ -33,9 +30,12 @@ function mainRoutes(app, middleware, controllers) {
setupPageRoute(app, '/compose', middleware, [], controllers.compose);
setupPageRoute(app, '/confirm/:code', middleware, [], controllers.confirmEmail);
setupPageRoute(app, '/outgoing', middleware, [], controllers.outgoing);
- setupPageRoute(app, '/search/:term?', middleware, [], controllers.search.search);
+ setupPageRoute(app, '/search', middleware, [], controllers.search.search);
setupPageRoute(app, '/reset/:code?', middleware, [], controllers.reset);
setupPageRoute(app, '/tos', middleware, [], controllers.termsOfUse);
+
+ app.get('/ping', controllers.ping);
+ app.get('/sping', controllers.ping);
}
function globalModRoutes(app, middleware, controllers) {
@@ -70,16 +70,11 @@ function categoryRoutes(app, middleware, controllers) {
function userRoutes(app, middleware, controllers) {
var middlewares = [middleware.checkGlobalPrivacySettings];
- setupPageRoute(app, '/users', middleware, middlewares, controllers.users.getUsersSortedByJoinDate);
- setupPageRoute(app, '/users/online', middleware, middlewares, controllers.users.getOnlineUsers);
- setupPageRoute(app, '/users/sort-posts', middleware, middlewares, controllers.users.getUsersSortedByPosts);
- setupPageRoute(app, '/users/sort-reputation', middleware, middlewares, controllers.users.getUsersSortedByReputation);
- setupPageRoute(app, '/users/banned', middleware, middlewares, controllers.users.getBannedUsers);
- setupPageRoute(app, '/users/flagged', middleware, middlewares, controllers.users.getFlaggedUsers);
+ setupPageRoute(app, '/users', middleware, middlewares, controllers.users.index);
}
function groupRoutes(app, middleware, controllers) {
- var middlewares = [middleware.checkGlobalPrivacySettings, middleware.exposeGroupName];
+ var middlewares = [middleware.checkGlobalPrivacySettings];
setupPageRoute(app, '/groups', middleware, middlewares, controllers.groups.list);
setupPageRoute(app, '/groups/:slug', middleware, middlewares, controllers.groups.details);
@@ -88,15 +83,15 @@ function groupRoutes(app, middleware, controllers) {
module.exports = function(app, middleware, hotswapIds) {
var routers = [
- express.Router(), // plugin router
- express.Router(), // main app router
- express.Router() // auth router
- ],
- router = routers[1],
- pluginRouter = routers[0],
- authRouter = routers[2],
- relativePath = nconf.get('relative_path'),
- ensureLoggedIn = require('connect-ensure-login');
+ express.Router(), // plugin router
+ express.Router(), // main app router
+ express.Router() // auth router
+ ];
+ var router = routers[1];
+ var pluginRouter = routers[0];
+ var authRouter = routers[2];
+ var relativePath = nconf.get('relative_path');
+ var ensureLoggedIn = require('connect-ensure-login');
if (Array.isArray(hotswapIds) && hotswapIds.length) {
for(var idx,x=0;x 2;
results.roomData.isOwner = parseInt(results.roomData.owner, 10) === socket.uid;
results.roomData.maximumUsersInChatRoom = parseInt(meta.config.maximumUsersInChatRoom, 10) || 0;
results.roomData.showUserInput = !results.roomData.maximumUsersInChatRoom || results.roomData.maximumUsersInChatRoom > 2;
@@ -280,7 +283,7 @@ SocketModules.chats.renameRoom = function(socket, data, callback) {
Messaging.getUidsInRoom(data.roomId, 0, -1, next);
},
function (uids, next) {
- var eventData = {roomId: data.roomId, newName: validator.escape(data.newName)};
+ var eventData = {roomId: data.roomId, newName: validator.escape(String(data.newName))};
uids.forEach(function(uid) {
server.in('uid_' + uid).emit('event:chats.roomRename', eventData);
});
@@ -293,10 +296,18 @@ SocketModules.chats.getRecentChats = function(socket, data, callback) {
if (!data || !utils.isNumber(data.after)) {
return callback(new Error('[[error:invalid-data]]'));
}
- var start = parseInt(data.after, 10),
- stop = start + 9;
+ var start = parseInt(data.after, 10);
+ var stop = start + 9;
+ if (socket.uid === parseInt(data.uid, 10)) {
+ return Messaging.getRecentChats(socket.uid, start, stop, callback);
+ }
- Messaging.getRecentChats(socket.uid, start, stop, callback);
+ user.isAdminOrGlobalMod(socket.uid, function(err, isAdminOrGlobalMod) {
+ if (err || !isAdminOrGlobalMod) {
+ return callback(err || new Error('[[error:no-privileges]]'));
+ }
+ Messaging.getRecentChats(data.uid, start, stop, callback);
+ });
};
SocketModules.chats.hasPrivateChat = function(socket, uid, callback) {
@@ -306,6 +317,28 @@ SocketModules.chats.hasPrivateChat = function(socket, uid, callback) {
Messaging.hasPrivateChat(socket.uid, uid, callback);
};
+SocketModules.chats.getMessages = function(socket, data, callback) {
+ if (!socket.uid || !data.uid || !data.roomId) {
+ return callback(new Error('[[error:invalid-data]]'));
+ }
+ var params = {
+ uid: data.uid,
+ roomId: data.roomId,
+ start: parseInt(data.start, 10) + 1,
+ count: 50,
+ markRead: false
+ };
+ if (socket.uid === parseInt(data.uid, 10)) {
+ return Messaging.getMessages(params, callback);
+ }
+ user.isAdminOrGlobalMod(socket.uid, function(err, isAdminOrGlobalMod) {
+ if (err || !isAdminOrGlobalMod) {
+ return callback(err || new Error('[[error:no-privileges]]'));
+ }
+ Messaging.getMessages(params, callback);
+ });
+};
+
/* Sounds */
SocketModules.sounds.getSounds = function(socket, data, callback) {
// Read sounds from local directory
@@ -313,12 +346,12 @@ SocketModules.sounds.getSounds = function(socket, data, callback) {
};
SocketModules.sounds.getMapping = function(socket, data, callback) {
- meta.sounds.getMapping(callback);
+ meta.sounds.getMapping(socket.uid, callback);
};
SocketModules.sounds.getData = function(socket, data, callback) {
async.parallel({
- mapping: async.apply(meta.sounds.getMapping),
+ mapping: async.apply(meta.sounds.getMapping, socket.uid),
files: async.apply(meta.sounds.getFiles)
}, callback);
};
diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js
index 3251c3700c..6c86458fc2 100644
--- a/src/socket.io/posts.js
+++ b/src/socket.io/posts.js
@@ -30,6 +30,7 @@ SocketPosts.reply = function(socket, data, callback) {
data.uid = socket.uid;
data.req = websockets.reqFromSocket(socket);
+ data.timestamp = Date.now();
topics.reply(data, function(err, postData) {
if (err) {
diff --git a/src/socket.io/posts/edit.js b/src/socket.io/posts/edit.js
index 41a7c1134f..22b7aa0ea2 100644
--- a/src/socket.io/posts/edit.js
+++ b/src/socket.io/posts/edit.js
@@ -31,16 +31,9 @@ module.exports = function(SocketPosts) {
return callback(new Error('[[error:content-too-long, ' + meta.config.maximumPostLength + ']]'));
}
- posts.edit({
- uid: socket.uid,
- handle: data.handle,
- pid: data.pid,
- title: data.title,
- content: data.content,
- topic_thumb: data.topic_thumb,
- tags: data.tags,
- req: websockets.reqFromSocket(socket)
- }, function(err, result) {
+ data.uid = socket.uid;
+ data.req = websockets.reqFromSocket(socket);
+ posts.edit(data, function(err, result) {
if (err) {
return callback(err);
}
diff --git a/src/socket.io/posts/flag.js b/src/socket.io/posts/flag.js
index 2ad5dcd2b8..583de733fb 100644
--- a/src/socket.io/posts/flag.js
+++ b/src/socket.io/posts/flag.js
@@ -98,7 +98,7 @@ module.exports = function(SocketPosts) {
return next(err);
}
- plugins.fireHook('action:post.flag', {post: post, flaggingUser: flaggingUser});
+ plugins.fireHook('action:post.flag', {post: post, reason: data.reason, flaggingUser: flaggingUser});
notifications.push(notification, results.admins.concat(results.moderators).concat(results.globalMods), next);
});
}
@@ -136,14 +136,12 @@ module.exports = function(SocketPosts) {
], callback);
};
- SocketPosts.getMoreFlags = function(socket, data, callback) {
- if (!data || !parseInt(data.after, 10)) {
+ SocketPosts.updateFlag = function(socket, data, callback) {
+ if (!data || !(data.pid && data.data)) {
return callback('[[error:invalid-data]]');
}
- var sortBy = data.sortBy || 'count';
- var byUsername = data.byUsername || '';
- var start = parseInt(data.after, 10);
- var stop = start + 19;
+
+ var payload = {};
async.waterfall([
function (next) {
@@ -154,16 +152,15 @@ module.exports = function(SocketPosts) {
return next(new Error('[[no-privileges]]'));
}
- if (byUsername) {
- posts.getUserFlags(byUsername, sortBy, socket.uid, start, stop, next);
- } else {
- var set = sortBy === 'count' ? 'posts:flags:count' : 'posts:flagged';
- posts.getFlags(set, socket.uid, start, stop, next);
- }
- },
- function (posts, next) {
- next(null, {posts: posts, next: stop + 1});
+ // Translate form data into object
+ payload = data.data.reduce(function(memo, cur) {
+ memo[cur.name] = cur.value;
+ return memo;
+ }, payload);
+
+ next(null, socket.uid, data.pid, payload);
},
+ async.apply(posts.updateFlagData)
], callback);
- };
+ }
};
diff --git a/src/socket.io/posts/tools.js b/src/socket.io/posts/tools.js
index bcb7e59642..91e8dca241 100644
--- a/src/socket.io/posts/tools.js
+++ b/src/socket.io/posts/tools.js
@@ -28,6 +28,12 @@ module.exports = function(SocketPosts) {
isAdminOrMod: function(next) {
privileges.categories.isAdminOrMod(data.cid, socket.uid, next);
},
+ canEdit: function(next) {
+ privileges.posts.canEdit(data.pid, socket.uid, next);
+ },
+ canDelete: function(next) {
+ privileges.posts.canDelete(data.pid, socket.uid, next);
+ },
favourited: function(next) {
favourites.getFavouritesByPostIDs([data.pid], socket.uid, next);
},
@@ -41,11 +47,14 @@ module.exports = function(SocketPosts) {
if (err) {
return callback(err);
}
+
results.posts.tools = results.tools.tools;
results.posts.deleted = parseInt(results.posts.deleted, 10) === 1;
results.posts.favourited = results.favourited[0];
results.posts.selfPost = socket.uid && socket.uid === parseInt(results.posts.uid, 10);
- results.posts.display_moderator_tools = results.isAdminOrMod || results.posts.selfPost;
+ results.posts.display_edit_tools = results.canEdit.flag;
+ results.posts.display_delete_tools = results.canDelete.flag;
+ results.posts.display_moderator_tools = results.posts.display_edit_tools || results.posts.display_delete_tools;
results.posts.display_move_tools = results.isAdminOrMod;
callback(null, results);
});
@@ -165,4 +174,4 @@ module.exports = function(SocketPosts) {
}, callback);
}
-};
\ No newline at end of file
+};
diff --git a/src/socket.io/topics.js b/src/socket.io/topics.js
index 48f24700b1..5887483242 100644
--- a/src/socket.io/topics.js
+++ b/src/socket.io/topics.js
@@ -22,16 +22,11 @@ SocketTopics.post = function(socket, data, callback) {
return callback(new Error('[[error:invalid-data]]'));
}
- topics.post({
- uid: socket.uid,
- handle: data.handle,
- title: data.title,
- content: data.content,
- cid: data.category_id,
- thumb: data.topic_thumb,
- tags: data.tags,
- req: websockets.reqFromSocket(socket)
- }, function(err, result) {
+ data.uid = socket.uid;
+ data.req = websockets.reqFromSocket(socket);
+ data.timestamp = Date.now();
+
+ topics.post(data, function(err, result) {
if (err) {
return callback(err);
}
diff --git a/src/socket.io/topics/infinitescroll.js b/src/socket.io/topics/infinitescroll.js
index 5f089bd52e..7401c60e1f 100644
--- a/src/socket.io/topics/infinitescroll.js
+++ b/src/socket.io/topics/infinitescroll.js
@@ -1,7 +1,7 @@
'use strict';
var async = require('async');
-var user = require('../../user');
+
var topics = require('../../topics');
var privileges = require('../../privileges');
var meta = require('../../meta');
@@ -19,9 +19,6 @@ module.exports = function(SocketTopics) {
privileges: function(next) {
privileges.topics.get(data.tid, socket.uid, next);
},
- settings: function(next) {
- user.getSettings(socket.uid, next);
- },
topic: function(next) {
topics.getTopicFields(data.tid, ['postcount', 'deleted'], next);
}
@@ -35,10 +32,10 @@ module.exports = function(SocketTopics) {
}
var set = 'tid:' + data.tid + ':posts';
- if (results.settings.topicPostSort === 'most_votes') {
+ if (data.topicPostSort === 'most_votes') {
set = 'tid:' + data.tid + ':posts:votes';
}
- var reverse = results.settings.topicPostSort === 'newest_to_oldest' || results.settings.topicPostSort === 'most_votes';
+ var reverse = data.topicPostSort === 'newest_to_oldest' || data.topicPostSort === 'most_votes';
var start = Math.max(0, parseInt(data.after, 10));
var infScrollPostsPerPage = 10;
diff --git a/src/socket.io/topics/tags.js b/src/socket.io/topics/tags.js
index f55ec377fb..dc604a450a 100644
--- a/src/socket.io/topics/tags.js
+++ b/src/socket.io/topics/tags.js
@@ -4,6 +4,10 @@ var topics = require('../../topics');
var utils = require('../../../public/src/utils');
module.exports = function(SocketTopics) {
+ SocketTopics.autocompleteTags = function(socket, data, callback) {
+ topics.autocompleteTags(data, callback);
+ };
+
SocketTopics.searchTags = function(socket, data, callback) {
topics.searchTags(data, callback);
};
@@ -27,9 +31,7 @@ module.exports = function(SocketTopics) {
if (err) {
return callback(err);
}
- tags = tags.filter(function(tag) {
- return tag && tag.score > 0;
- });
+ tags = tags.filter(Boolean);
callback(null, {tags: tags, nextStart: stop + 1});
});
};
diff --git a/src/socket.io/topics/unread.js b/src/socket.io/topics/unread.js
index a5582d6892..e9279d3f4a 100644
--- a/src/socket.io/topics/unread.js
+++ b/src/socket.io/topics/unread.js
@@ -19,18 +19,17 @@ module.exports = function(SocketTopics) {
topics.pushUnreadCount(socket.uid);
- for (var i=0; i 19) {
+ break;
+ }
}
}
- matches = matches.slice(0, 20).sort(function(a, b) {
+ matches = matches.sort(function(a, b) {
return a > b;
});
-
- plugins.fireHook('filter:tags.search', {data: data, matches: matches}, function(err, data) {
- callback(err, data ? data.matches : []);
- });
+ callback(null, matches);
});
- };
+ }
Topics.searchAndLoadTags = function(data, callback) {
var searchResult = {
@@ -316,8 +381,8 @@ module.exports = function(Topics) {
return plugins.fireHook('filter:topic.getRelatedTopics', {topic: topicData, uid: uid}, callback);
}
- var maximumTopics = parseInt(meta.config.maximumRelatedTopics, 10);
- if (maximumTopics === 0 || !topicData.tags.length) {
+ var maximumTopics = parseInt(meta.config.maximumRelatedTopics, 10) || 0;
+ if (maximumTopics === 0 || !topicData.tags || !topicData.tags.length) {
return callback(null, []);
}
diff --git a/src/topics/thumb.js b/src/topics/thumb.js
new file mode 100644
index 0000000000..5829e080c4
--- /dev/null
+++ b/src/topics/thumb.js
@@ -0,0 +1,91 @@
+
+'use strict';
+
+var async = require('async');
+var nconf = require('nconf');
+var winston = require('winston');
+var path = require('path');
+var fs = require('fs');
+var request = require('request');
+var mime = require('mime');
+var validator = require('validator');
+
+var meta = require('../meta');
+var image = require('../image');
+var file = require('../file');
+var plugins = require('../plugins');
+
+module.exports = function(Topics) {
+
+ Topics.resizeAndUploadThumb = function(data, callback) {
+ if (!data.thumb || !validator.isURL(data.thumb)) {
+ return callback();
+ }
+
+ var pathToUpload;
+ var filename;
+
+ async.waterfall([
+ function(next) {
+ request.head(data.thumb, next);
+ },
+ function(res, body, next) {
+
+ var type = res.headers['content-type'];
+ if (!type.match(/image./)) {
+ return next(new Error('[[error:invalid-file]]'));
+ }
+
+ var extension = path.extname(data.thumb);
+ if (!extension) {
+ extension = '.' + mime.extension(type);
+ }
+ filename = Date.now() + '-topic-thumb' + extension;
+ pathToUpload = path.join(nconf.get('base_dir'), nconf.get('upload_path'), 'files', filename);
+
+ request(data.thumb).pipe(fs.createWriteStream(pathToUpload)).on('close', next);
+ },
+ function(next) {
+ file.isFileTypeAllowed(pathToUpload, next);
+ },
+ function(next) {
+ var size = parseInt(meta.config.topicThumbSize, 10) || 120;
+ image.resizeImage({
+ path: pathToUpload,
+ extension: path.extname(pathToUpload),
+ width: size,
+ height: size
+ }, next);
+ },
+ function(next) {
+ if (!plugins.hasListeners('filter:uploadImage')) {
+ data.thumb = path.join(nconf.get('upload_url'), 'files', filename);
+ return callback();
+ }
+
+ plugins.fireHook('filter:uploadImage', {image: {path: pathToUpload, name: ''}, uid: data.uid}, next);
+ },
+ function(uploadedFile, next) {
+ deleteFile(pathToUpload);
+ data.thumb = uploadedFile.url;
+ next();
+ }
+ ], function(err) {
+ if (err) {
+ deleteFile(pathToUpload);
+ }
+ callback(err);
+ });
+ };
+
+ function deleteFile(path) {
+ if (path) {
+ fs.unlink(path, function(err) {
+ if (err) {
+ winston.error(err);
+ }
+ });
+ }
+ }
+
+};
diff --git a/src/topics/tools.js b/src/topics/tools.js
index fd5321eb6b..3975193f76 100644
--- a/src/topics/tools.js
+++ b/src/topics/tools.js
@@ -1,11 +1,12 @@
'use strict';
-var async = require('async'),
+var async = require('async');
- db = require('../database'),
- categories = require('../categories'),
- plugins = require('../plugins'),
- privileges = require('../privileges');
+var db = require('../database');
+var categories = require('../categories');
+var meta = require('../meta');
+var plugins = require('../plugins');
+var privileges = require('../privileges');
module.exports = function(Topics) {
@@ -32,10 +33,10 @@ module.exports = function(Topics) {
if (!exists) {
return next(new Error('[[error:no-topic]]'));
}
- privileges.topics.isOwnerOrAdminOrMod(tid, uid, next);
+ privileges.topics.canDelete(tid, uid, next);
},
- function (isOwnerOrAdminOrMod, next) {
- if (!isOwnerOrAdminOrMod) {
+ function (canDelete, next) {
+ if (!canDelete) {
return next(new Error('[[error:no-privileges]]'));
}
Topics.getTopicFields(tid, ['tid', 'cid', 'uid', 'deleted', 'title', 'mainPid'], next);
diff --git a/src/topics/unread.js b/src/topics/unread.js
index 61fef8b65e..d1c2a0bb15 100644
--- a/src/topics/unread.js
+++ b/src/topics/unread.js
@@ -279,9 +279,7 @@ module.exports = function(Topics) {
db.getSortedSetRevRangeByScore('topics:recent', 0, -1, '+inf', Topics.unreadCutoff(), next);
},
function (tids, next) {
- for (var i=0; i 0 && Date.now() < until) {
+ if (until > 0 && now < until) {
tasks.push(async.apply(db.sortedSetAdd, 'users:banned:expire', until, uid));
+ tasks.push(async.apply(User.setUserField, uid, 'banned:expire', until));
} else {
until = 0;
}
+ if (reason) {
+ tasks.push(async.apply(db.sortedSetAdd, 'banned:' + uid + ':reasons', now, reason));
+ }
+
async.series(tasks, function (err) {
if (err) {
return callback(err);
@@ -91,7 +105,7 @@ module.exports = function(User) {
User.unban = function(uid, callback) {
async.waterfall([
function (next) {
- User.setUserField(uid, 'banned', 0, next);
+ User.setUserFields(uid, {banned: 0, 'banned:expire': 0}, next);
},
function (next) {
db.sortedSetsRemove(['users:banned', 'users:banned:expire'], uid, next);
@@ -103,6 +117,32 @@ module.exports = function(User) {
], callback);
};
+ User.isBanned = function(uid, callback) {
+ async.waterfall([
+ async.apply(User.getUserFields, uid, ['banned', 'banned:expire']),
+ function(userData, next) {
+ var banned = parseInt(userData.banned, 10) === 1;
+ if (!banned) {
+ return next(null, banned);
+ }
+
+ // If they are banned, see if the ban has expired
+ var stillBanned = !userData['banned:expire'] || Date.now() < userData['banned:expire'];
+
+ if (stillBanned) {
+ return next(null, true);
+ }
+ async.parallel([
+ async.apply(db.sortedSetRemove.bind(db), 'users:banned:expire', uid),
+ async.apply(db.sortedSetRemove.bind(db), 'users:banned', uid),
+ async.apply(User.setUserFields, uid, {banned:0, 'banned:expire': 0})
+ ], function(err) {
+ next(err, false);
+ });
+ }
+ ], callback);
+ };
+
User.resetFlags = function(uids, callback) {
if (!Array.isArray(uids) || !uids.length) {
return callback();
diff --git a/src/user/approval.js b/src/user/approval.js
index 8ccd5192aa..25dcf3479a 100644
--- a/src/user/approval.js
+++ b/src/user/approval.js
@@ -11,7 +11,7 @@ var notifications = require('../notifications');
var groups = require('../groups');
var translator = require('../../public/src/modules/translator');
var utils = require('../../public/src/utils');
-
+var plugins = require('../plugins');
module.exports = function(User) {
@@ -31,8 +31,10 @@ module.exports = function(User) {
ip: userData.ip,
hashedPassword: hashedPassword
};
-
- db.setObject('registration:queue:name:' + userData.username, data, next);
+ plugins.fireHook('filter:user.addToApprovalQueue', {data: data, userData: userData}, next);
+ },
+ function(results, next) {
+ db.setObject('registration:queue:name:' + userData.username, results.data, next);
},
function(next) {
db.sortedSetAdd('registration:queue', Date.now(), userData.username, next);
@@ -166,28 +168,52 @@ module.exports = function(User) {
// temporary: see http://www.stopforumspam.com/forum/viewtopic.php?id=6392
user.ip = user.ip.replace('::ffff:', '');
- request({
- method: 'get',
- url: 'http://api.stopforumspam.org/api' +
- '?ip=' + encodeURIComponent(user.ip) +
- '&email=' + encodeURIComponent(user.email) +
- '&username=' + encodeURIComponent(user.username) +
- '&f=json',
- json: true
- }, function (err, response, body) {
- if (err) {
- return next(null, user);
- }
- if (response.statusCode === 200 && body) {
- user.spamData = body;
- user.usernameSpam = body.username ? (body.username.frequency > 0 || body.username.appears > 0) : true;
- user.emailSpam = body.email ? (body.email.frequency > 0 || body.email.appears > 0) : true;
- user.ipSpam = body.ip ? (body.ip.frequency > 0 || body.ip.appears > 0) : true;
- }
+ async.parallel([
+ function(next) {
+ User.getUidsFromSet('ip:' + user.ip + ':uid', 0, -1, function(err, uids) {
+ if (err) {
+ return next(err);
+ }
- next(null, user);
+ User.getUsersFields(uids, ['uid', 'username', 'picture'], function(err, ipMatch) {
+ user.ipMatch = ipMatch;
+ next(err);
+ });
+ });
+ },
+ function(next) {
+ request({
+ method: 'get',
+ url: 'http://api.stopforumspam.org/api' +
+ '?ip=' + encodeURIComponent(user.ip) +
+ '&email=' + encodeURIComponent(user.email) +
+ '&username=' + encodeURIComponent(user.username) +
+ '&f=json',
+ json: true
+ }, function (err, response, body) {
+ if (err) {
+ return next();
+ }
+ if (response.statusCode === 200 && body) {
+ user.spamData = body;
+ user.usernameSpam = body.username ? (body.username.frequency > 0 || body.username.appears > 0) : true;
+ user.emailSpam = body.email ? (body.email.frequency > 0 || body.email.appears > 0) : true;
+ user.ipSpam = body.ip ? (body.ip.frequency > 0 || body.ip.appears > 0) : true;
+ }
+
+ next();
+ });
+ }
+ ], function(err) {
+ next(err, user);
});
}, next);
+ },
+ function(users, next) {
+ plugins.fireHook('filter:user.getRegistrationQueue', {users: users}, next);
+ },
+ function(results, next) {
+ next(null, results.users);
}
], callback);
};
diff --git a/src/user/auth.js b/src/user/auth.js
index f2fa917962..b8ff96053b 100644
--- a/src/user/auth.js
+++ b/src/user/auth.js
@@ -101,7 +101,7 @@ module.exports = function(User) {
async.each(expiredSids, function(sid, next) {
User.auth.revokeSession(sid, uid, next);
}, function(err) {
- next(null, sessions);
+ next(err, sessions);
});
}
], function (err, sessions) {
diff --git a/src/user/create.js b/src/user/create.js
index bd79270a93..d01cfaaf64 100644
--- a/src/user/create.js
+++ b/src/user/create.js
@@ -14,7 +14,7 @@ module.exports = function(User) {
data.username = data.username.trim();
data.userslug = utils.slugify(data.username);
if (data.email !== undefined) {
- data.email = validator.escape(data.email.trim());
+ data.email = validator.escape(String(data.email).trim());
}
User.isDataValid(data, function(err) {
@@ -26,7 +26,7 @@ module.exports = function(User) {
var userData = {
'username': data.username,
'userslug': data.userslug,
- 'email': data.email,
+ 'email': data.email || '',
'joindate': timestamp,
'lastonline': timestamp,
'picture': '',
@@ -131,6 +131,9 @@ module.exports = function(User) {
async.apply(User.reset.updateExpiry, userData.uid)
], next);
});
+ },
+ function(next) {
+ User.updateDigestSetting(userData.uid, meta.config.dailyDigestSetting, next);
}
], next);
},
diff --git a/src/user/data.js b/src/user/data.js
index 8e179ca119..d0c70ce80c 100644
--- a/src/user/data.js
+++ b/src/user/data.js
@@ -50,21 +50,15 @@ module.exports = function(User) {
addField('uploadedpicture');
}
+ if (fields.indexOf('status') !== -1) {
+ addField('lastonline');
+ }
+
db.getObjectsFields(keys, fields, function(err, users) {
if (err) {
return callback(err);
}
- if (fields.indexOf('banned') !== -1) {
- // Also retrieve ban expiry for these users
- db.sortedSetScores('users:banned:expire', uids, function(err, scores) {
- users = users.map(function(userObj, idx) {
- userObj.banned_until = scores[idx] || 0;
- userObj.banned_until_readable = scores[idx] ? new Date(scores[idx]).toString() : 'Not Banned';
- });
- });
- }
-
modifyUserData(users, fieldsToRemove, callback);
});
};
@@ -104,7 +98,9 @@ module.exports = function(User) {
return;
}
- user.username = validator.escape(user.username ? user.username.toString() : '');
+ if (user.hasOwnProperty('username')) {
+ user.username = validator.escape(user.username ? user.username.toString() : '');
+ }
if (user.password) {
user.password = undefined;
@@ -125,6 +121,10 @@ module.exports = function(User) {
user.uploadedpicture = user.uploadedpicture.startsWith('http') ? user.uploadedpicture : nconf.get('relative_path') + user.uploadedpicture;
}
+ if (user.hasOwnProperty('status') && parseInt(user.lastonline, 10)) {
+ user.status = User.getStatus(user);
+ }
+
for(var i=0; i 23 || digestHour < 0) {
+ digestHour = 0;
+ }
+
+ // Terminate any active cron jobs
+ for(var jobId in jobs) {
+ if (jobs.hasOwnProperty(jobId)) {
+ winston.verbose('[user/jobs] Terminating job (' + jobId + ')');
+ jobs[jobId].stop();
+ delete jobs[jobId];
+ ++terminated;
+ }
+ }
+ winston.verbose('[user/jobs] ' + terminated + ' jobs terminated');
+
+ jobs['digest.daily'] = new cronJob('0 0 ' + digestHour + ' * * *', function() {
+ winston.verbose('[user/jobs] Digest job (daily) started.');
User.digest.execute('day');
}, null, true);
+ winston.verbose('[user/jobs] Starting job (digest.daily)');
+ ++started;
- new cronJob('0 0 17 * * 0', function() {
- winston.verbose('[user.startJobs] Digest job (weekly) started.');
+ jobs['digest.weekly'] = new cronJob('0 0 ' + digestHour + ' * * 0', function() {
+ winston.verbose('[user/jobs] Digest job (weekly) started.');
User.digest.execute('week');
}, null, true);
+ winston.verbose('[user/jobs] Starting job (digest.weekly)');
+ ++started;
- new cronJob('0 0 17 1 * *', function() {
- winston.verbose('[user.startJobs] Digest job (monthly) started.');
+ jobs['digest.monthly'] = new cronJob('0 0 ' + digestHour + ' 1 * *', function() {
+ winston.verbose('[user/jobs] Digest job (monthly) started.');
User.digest.execute('month');
}, null, true);
+ winston.verbose('[user/jobs] Starting job (digest.monthly)');
+ ++started;
- new cronJob('0 0 0 * * *', User.reset.clean, null, true);
+ jobs['reset.clean'] = new cronJob('0 0 0 * * *', User.reset.clean, null, true);
+ winston.verbose('[user/jobs] Starting job (reset.clean)');
+ ++started;
+
+ winston.verbose('[user/jobs] ' + started + ' jobs started');
+
+ if (typeof callback === 'function') {
+ callback();
+ }
+
+ return;
};
};
diff --git a/src/user/notifications.js b/src/user/notifications.js
index b08cb6aaff..dcc7d3eee3 100644
--- a/src/user/notifications.js
+++ b/src/user/notifications.js
@@ -154,7 +154,7 @@ var privileges = require('../privileges');
});
};
- UserNotifications.getUnreadByField = function(uid, field, value, callback) {
+ UserNotifications.getUnreadByField = function(uid, field, values, callback) {
db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function(err, nids) {
if (err) {
return callback(err);
@@ -173,9 +173,9 @@ var privileges = require('../privileges');
return callback(err);
}
- value = value ? value.toString() : '';
+ values = values.map(function() { return values.toString(); });
nids = notifications.filter(function(notification) {
- return notification && notification[field] && notification[field].toString() === value;
+ return notification && notification[field] && values.indexOf(notification[field].toString()) !== -1;
}).map(function(notification) {
return notification.nid;
});
diff --git a/src/user/picture.js b/src/user/picture.js
index 38d013cab0..fcac5c5c6f 100644
--- a/src/user/picture.js
+++ b/src/user/picture.js
@@ -1,20 +1,20 @@
'use strict';
-var async = require('async'),
- path = require('path'),
- fs = require('fs'),
- os = require('os'),
- nconf = require('nconf'),
- crypto = require('crypto'),
- winston = require('winston'),
- request = require('request'),
- mime = require('mime'),
+var async = require('async');
+var path = require('path');
+var fs = require('fs');
+var os = require('os');
+var nconf = require('nconf');
+var crypto = require('crypto');
+var winston = require('winston');
+var request = require('request');
+var mime = require('mime');
- plugins = require('../plugins'),
- file = require('../file'),
- image = require('../image'),
- meta = require('../meta'),
- db = require('../database');
+var plugins = require('../plugins');
+var file = require('../file');
+var image = require('../image');
+var meta = require('../meta');
+var db = require('../database');
module.exports = function(User) {
@@ -28,16 +28,19 @@ module.exports = function(User) {
var keepAllVersions = parseInt(meta.config['profile:keepAllUserImages'], 10) === 1;
var uploadedImage;
+ if (parseInt(meta.config.allowProfileImageUploads) !== 1) {
+ return callback(new Error('[[error:profile-image-uploads-disabled]]'));
+ }
+
+ if (picture.size > uploadSize * 1024) {
+ return callback(new Error('[[error:file-too-big, ' + uploadSize + ']]'));
+ }
+
+ if (!extension) {
+ return callback(new Error('[[error:invalid-image-extension]]'));
+ }
+
async.waterfall([
- function(next) {
- next(parseInt(meta.config.allowProfileImageUploads) !== 1 ? new Error('[[error:profile-image-uploads-disabled]]') : null);
- },
- function(next) {
- next(picture.size > uploadSize * 1024 ? new Error('[[error:file-too-big, ' + uploadSize + ']]') : null);
- },
- function(next) {
- next(!extension ? new Error('[[error:invalid-image-extension]]') : null);
- },
function(next) {
if (plugins.hasListeners('filter:uploadImage')) {
return plugins.fireHook('filter:uploadImage', {image: picture, uid: updateUid}, next);
@@ -200,6 +203,10 @@ module.exports = function(User) {
], function(err) {
if (err) {
return fs.unlink(data.file.path, function(unlinkErr) {
+ if (unlinkErr) {
+ winston.error(unlinkErr);
+ }
+
callback(err); // send back the original error
});
}
diff --git a/src/user/profile.js b/src/user/profile.js
index 8b2a83f152..d3a172ec79 100644
--- a/src/user/profile.js
+++ b/src/user/profile.js
@@ -49,6 +49,10 @@ module.exports = function(User) {
}
User.getUserField(uid, 'email', function(err, email) {
+ if (err) {
+ return next(err);
+ }
+
if(email === data.email) {
return next();
}
@@ -174,6 +178,7 @@ module.exports = function(User) {
function(next) {
db.sortedSetAdd('email:uid', uid, newEmail.toLowerCase(), next);
},
+ async.apply(db.sortedSetAdd, 'user:' + uid + ':emails', Date.now(), newEmail + ':' + Date.now()),
function(next) {
db.sortedSetAdd('email:sorted', 0, newEmail.toLowerCase() + ':' + uid, next);
},
@@ -215,7 +220,8 @@ module.exports = function(User) {
function(next) {
async.series([
async.apply(db.sortedSetRemove, 'username:sorted', userData.username.toLowerCase() + ':' + uid),
- async.apply(db.sortedSetAdd, 'username:sorted', 0, newUsername.toLowerCase() + ':' + uid)
+ async.apply(db.sortedSetAdd, 'username:sorted', 0, newUsername.toLowerCase() + ':' + uid),
+ async.apply(db.sortedSetAdd, 'user:' + uid + ':usernames', Date.now(), newUsername + ':' + Date.now())
], next);
},
], callback);
diff --git a/src/user/search.js b/src/user/search.js
index 6bb7084b1f..5186b0d497 100644
--- a/src/user/search.js
+++ b/src/user/search.js
@@ -84,7 +84,16 @@ module.exports = function(User) {
function filterAndSortUids(uids, data, callback) {
var sortBy = data.sortBy || 'joindate';
- var fields = ['uid', 'status', 'lastonline', 'banned', 'flags', sortBy];
+ var fields = ['uid', sortBy];
+ if (data.onlineOnly) {
+ fields = fields.concat(['status', 'lastonline']);
+ }
+ if (data.bannedOnly) {
+ fields.push('banned');
+ }
+ if (data.flaggedOnly) {
+ fields.push('flags');
+ }
User.getUsersFields(uids, fields, function(err, userData) {
if (err) {
diff --git a/src/user/settings.js b/src/user/settings.js
index 7b78ea3c72..984bee50f2 100644
--- a/src/user/settings.js
+++ b/src/user/settings.js
@@ -65,7 +65,6 @@ module.exports = function(User) {
settings.usePagination = parseInt(getSetting(settings, 'usePagination', 0), 10) === 1;
settings.topicsPerPage = Math.min(settings.topicsPerPage ? parseInt(settings.topicsPerPage, 10) : defaultTopicsPerPage, defaultTopicsPerPage);
settings.postsPerPage = Math.min(settings.postsPerPage ? parseInt(settings.postsPerPage, 10) : defaultPostsPerPage, defaultPostsPerPage);
- settings.notificationSounds = parseInt(getSetting(settings, 'notificationSounds', 0), 10) === 1;
settings.userLang = settings.userLang || meta.config.defaultLang || 'en_GB';
settings.topicPostSort = getSetting(settings, 'topicPostSort', 'oldest_to_newest');
settings.categoryTopicSort = getSetting(settings, 'categoryTopicSort', 'newest_to_oldest');
@@ -113,7 +112,6 @@ module.exports = function(User) {
usePagination: data.usePagination,
topicsPerPage: Math.min(data.topicsPerPage, parseInt(meta.config.topicsPerPage, 10) || 20),
postsPerPage: Math.min(data.postsPerPage, parseInt(meta.config.postsPerPage, 10) || 20),
- notificationSounds: data.notificationSounds,
userLang: data.userLang || meta.config.defaultLang,
followTopicsOnCreate: data.followTopicsOnCreate,
followTopicsOnReply: data.followTopicsOnReply,
@@ -122,9 +120,11 @@ module.exports = function(User) {
restrictChat: data.restrictChat,
topicSearchEnabled: data.topicSearchEnabled,
delayImageLoading: data.delayImageLoading,
- groupTitle: data.groupTitle,
homePageRoute : ((data.homePageRoute === 'custom' ? data.homePageCustom : data.homePageRoute) || '').replace(/^\//, ''),
- scrollToMyPost: data.scrollToMyPost
+ scrollToMyPost: data.scrollToMyPost,
+ notificationSound: data.notificationSound,
+ incomingChatSound: data.incomingChatSound,
+ outgoingChatSound: data.outgoingChatSound
};
if (data.bootswatchSkin) {
@@ -136,7 +136,7 @@ module.exports = function(User) {
db.setObject('user:' + uid + ':settings', settings, next);
},
function(next) {
- updateDigestSetting(uid, data.dailyDigestFreq, next);
+ User.updateDigestSetting(uid, data.dailyDigestFreq, next);
},
function(next) {
User.getSettings(uid, next);
@@ -144,7 +144,7 @@ module.exports = function(User) {
], callback);
};
- function updateDigestSetting(uid, dailyDigestFreq, callback) {
+ User.updateDigestSetting = function(uid, dailyDigestFreq, callback) {
async.waterfall([
function(next) {
db.sortedSetsRemove(['digest:day:uids', 'digest:week:uids', 'digest:month:uids'], uid, next);
@@ -157,7 +157,7 @@ module.exports = function(User) {
}
}
], callback);
- }
+ };
User.setSetting = function(uid, key, value, callback) {
db.setObjectField('user:' + uid + ':settings', key, value, callback);
diff --git a/src/views/400.tpl b/src/views/400.tpl
new file mode 100644
index 0000000000..9c263fcff1
--- /dev/null
+++ b/src/views/400.tpl
@@ -0,0 +1,4 @@
+
+
[[global:400.title]]
+
[[global:400.message, {config.relative_path}]]
+
diff --git a/src/views/500-embed.tpl b/src/views/500-embed.tpl
index 537cbac136..9d911e9848 100644
--- a/src/views/500-embed.tpl
+++ b/src/views/500-embed.tpl
@@ -1,9 +1,8 @@
\ No newline at end of file
diff --git a/src/views/admin/advanced/cache.tpl b/src/views/admin/advanced/cache.tpl
new file mode 100644
index 0000000000..d8e3860367
--- /dev/null
+++ b/src/views/admin/advanced/cache.tpl
@@ -0,0 +1,49 @@
+
+
+
+
+
Post Cache
+
+
+
Posts in Cache
+
{postCache.itemCount}
+
+
Average Post Size
+
{postCache.avgPostSize}
+
+
Length / Max
+
{postCache.length} / {postCache.max}
+
+
+
+ {postCache.percentFull}% Full
+
+
+
+
+
+
+
Group Cache
+
+
+
Items in Cache
+
{groupCache.itemCount}
+
+
Length / Max
+
{groupCache.length} / {groupCache.max}
+
+
+
+ {groupCache.percentFull}% Full
+
+
+
+
+
{groupCache.dump}
+
+
+
+
+
+
+
diff --git a/src/views/admin/advanced/events.tpl b/src/views/admin/advanced/events.tpl
index f69d7928a5..f1c0973501 100644
--- a/src/views/admin/advanced/events.tpl
+++ b/src/views/admin/advanced/events.tpl
@@ -23,6 +23,7 @@
{events.jsonString}