").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/public/vendor/jquery/textcomplete/jquery.textcomplete.js b/public/vendor/jquery/textcomplete/jquery.textcomplete.js
index ad1d508450..b4ccd18cfc 100644
--- a/public/vendor/jquery/textcomplete/jquery.textcomplete.js
+++ b/public/vendor/jquery/textcomplete/jquery.textcomplete.js
@@ -17,7 +17,7 @@
* Repository: https://github.com/yuku-t/jquery-textcomplete
* License: MIT (https://github.com/yuku-t/jquery-textcomplete/blob/master/LICENSE)
* Author: Yuku Takahashi
- * Version: 1.3.1
+ * Version: 1.7.3
*/
if (typeof jQuery === 'undefined') {
@@ -137,10 +137,6 @@ if (typeof jQuery === 'undefined') {
return Object.prototype.toString.call(obj) === '[object String]';
};
- var isFunction = function (obj) {
- return Object.prototype.toString.call(obj) === '[object Function]';
- };
-
var uniqueId = 0;
function Completer(element, option) {
@@ -148,32 +144,46 @@ if (typeof jQuery === 'undefined') {
this.id = 'textcomplete' + uniqueId++;
this.strategies = [];
this.views = [];
- this.option = $.extend({}, Completer._getDefaults(), option);
+ this.option = $.extend({}, Completer.defaults, option);
if (!this.$el.is('input[type=text]') && !this.$el.is('input[type=search]') && !this.$el.is('textarea') && !element.isContentEditable && element.contentEditable != 'true') {
throw new Error('textcomplete must be called on a Textarea or a ContentEditable.');
}
- if (element === document.activeElement) {
+ // use ownerDocument to fix iframe / IE issues
+ if (element === element.ownerDocument.activeElement) {
// element has already been focused. Initialize view objects immediately.
this.initialize()
} else {
// Initialize view objects lazily.
var self = this;
this.$el.one('focus.' + this.id, function () { self.initialize(); });
+
+ // Special handling for CKEditor: lazy init on instance load
+ if ((!this.option.adapter || this.option.adapter == 'CKEditor') && typeof CKEDITOR != 'undefined' && (this.$el.is('textarea'))) {
+ CKEDITOR.on("instanceReady", function(event) {
+ event.editor.once("focus", function(event2) {
+ // replace the element with the Iframe element and flag it as CKEditor
+ self.$el = $(event.editor.editable().$);
+ if (!self.option.adapter) {
+ self.option.adapter = $.fn.textcomplete['CKEditor'];
+ self.option.ckeditor_instance = event.editor;
+ }
+ self.initialize();
+ });
+ });
+ }
}
}
- Completer._getDefaults = function () {
- if (!Completer.DEFAULTS) {
- Completer.DEFAULTS = {
- appendTo: $('body'),
- zIndex: '100'
- };
- }
-
- return Completer.DEFAULTS;
- }
+ Completer.defaults = {
+ appendTo: 'body',
+ className: '', // deprecated option
+ dropdownClassName: 'dropdown-menu textcomplete-dropdown',
+ maxCount: 10,
+ zIndex: '100',
+ rightEdgeOffset: 30
+ };
$.extend(Completer.prototype, {
// Public properties
@@ -185,12 +195,26 @@ if (typeof jQuery === 'undefined') {
adapter: null,
dropdown: null,
$el: null,
+ $iframe: null,
// Public methods
// --------------
initialize: function () {
var element = this.$el.get(0);
+
+ // check if we are in an iframe
+ // we need to alter positioning logic if using an iframe
+ if (this.$el.prop('ownerDocument') !== document && window.frames.length) {
+ for (var iframeIndex = 0; iframeIndex < window.frames.length; iframeIndex++) {
+ if (this.$el.prop('ownerDocument') === window.frames[iframeIndex].document) {
+ this.$iframe = $(window.frames[iframeIndex].frameElement);
+ break;
+ }
+ }
+ }
+
+
// Initialize view objects.
this.dropdown = new $.fn.textcomplete.Dropdown(element, this, this.option);
var Adapter, viewName;
@@ -282,7 +306,7 @@ if (typeof jQuery === 'undefined') {
var strategy = this.strategies[i];
var context = strategy.context(text);
if (context || context === '') {
- var matchRegexp = isFunction(strategy.match) ? strategy.match(text) : strategy.match;
+ var matchRegexp = $.isFunction(strategy.match) ? strategy.match(text) : strategy.match;
if (isString(context)) { text = context; }
var match = text.match(matchRegexp);
if (match) { return [strategy, match[strategy.index], match]; }
@@ -400,7 +424,7 @@ if (typeof jQuery === 'undefined') {
var $parent = option.appendTo;
if (!($parent instanceof $)) { $parent = $($parent); }
var $el = $('
')
- .addClass('dropdown-menu textcomplete-dropdown')
+ .addClass(option.dropdownClassName)
.attr('id', 'textcomplete-dropdown-' + option._oid)
.css({
display: 'none',
@@ -423,7 +447,7 @@ if (typeof jQuery === 'undefined') {
footer: null,
header: null,
id: null,
- maxCount: 10,
+ maxCount: null,
placement: '',
shown: false,
data: [], // Shown zipped data.
@@ -446,8 +470,8 @@ if (typeof jQuery === 'undefined') {
render: function (zippedData) {
var contentsHtml = this._buildContents(zippedData);
- var unzippedData = $.map(this.data, function (d) { return d.value; });
- if (this.data.length) {
+ var unzippedData = $.map(zippedData, function (d) { return d.value; });
+ if (zippedData.length) {
var strategy = zippedData[0].strategy;
if (strategy.id) {
this.$el.attr('data-strategy', strategy.id);
@@ -481,7 +505,7 @@ if (typeof jQuery === 'undefined') {
return false;
if($(this).css('position') === 'fixed') {
pos.top -= $window.scrollTop();
- pos.left -= $window.scrollLeft();
+ pos.left -= $window.scrollLeft();
position = 'fixed';
return false;
}
@@ -786,7 +810,10 @@ if (typeof jQuery === 'undefined') {
var windowScrollBottom = $window.scrollTop() + $window.height();
var height = this.$el.height();
if ((this.$el.position().top + height) > windowScrollBottom) {
- this.$el.offset({top: windowScrollBottom - height});
+ // only do this if we are not in an iframe
+ if (!this.completer.$iframe) {
+ this.$el.offset({top: windowScrollBottom - height});
+ }
}
},
@@ -795,9 +822,15 @@ if (typeof jQuery === 'undefined') {
// to the document width so we don't know if we would have overrun it. As a heuristic to avoid that clipping
// (which makes our elements wrap onto the next line and corrupt the next item), if we're close to the right
// edge, move left. We don't know how far to move left, so just keep nudging a bit.
- var tolerance = 30; // pixels. Make wider than vertical scrollbar because we might not be able to use that space.
- while (this.$el.offset().left + this.$el.width() > $window.width() - tolerance) {
- this.$el.offset({left: this.$el.offset().left - tolerance});
+ var tolerance = this.option.rightEdgeOffset; // pixels. Make wider than vertical scrollbar because we might not be able to use that space.
+ var lastOffset = this.$el.offset().left, offset;
+ var width = this.$el.width();
+ var maxLeft = $window.width() - tolerance;
+ while (lastOffset + width > maxLeft) {
+ this.$el.offset({left: lastOffset - tolerance});
+ offset = this.$el.offset().left;
+ if (offset >= lastOffset) { break; }
+ lastOffset = offset;
}
},
@@ -1002,6 +1035,7 @@ if (typeof jQuery === 'undefined') {
case 13: // ENTER
case 40: // DOWN
case 38: // UP
+ case 27: // ESC
return true;
}
if (clickEvent.ctrlKey) switch (clickEvent.keyCode) {
@@ -1035,12 +1069,14 @@ if (typeof jQuery === 'undefined') {
var pre = this.getTextFromHeadToCaret();
var post = this.el.value.substring(this.el.selectionEnd);
var newSubstr = strategy.replace(value, e);
+ var regExp;
if (typeof newSubstr !== 'undefined') {
if ($.isArray(newSubstr)) {
post = newSubstr[1] + post;
newSubstr = newSubstr[0];
}
- pre = pre.replace(strategy.match, newSubstr);
+ regExp = $.isFunction(strategy.match) ? strategy.match(pre) : strategy.match;
+ pre = pre.replace(regExp, newSubstr);
this.$el.val(pre + post);
this.el.selectionStart = this.el.selectionEnd = pre.length;
}
@@ -1056,9 +1092,29 @@ if (typeof jQuery === 'undefined') {
_getCaretRelativePosition: function () {
var p = $.fn.textcomplete.getCaretCoordinates(this.el, this.el.selectionStart);
return {
- top: p.top + parseInt(this.$el.css('line-height'), 10) - this.$el.scrollTop(),
- left: p.left - this.$el.scrollLeft()
+ top: p.top + this._calculateLineHeight() - this.$el.scrollTop(),
+ left: p.left - this.$el.scrollLeft(),
+ lineHeight: this._calculateLineHeight()
};
+ },
+
+ _calculateLineHeight: function () {
+ var lineHeight = parseInt(this.$el.css('line-height'), 10);
+ if (isNaN(lineHeight)) {
+ // http://stackoverflow.com/a/4515470/1297336
+ var parentNode = this.el.parentNode;
+ var temp = document.createElement(this.el.nodeName);
+ var style = this.el.style;
+ temp.setAttribute(
+ 'style',
+ 'margin:0px;padding:0px;font-family:' + style.fontFamily + ';font-size:' + style.fontSize
+ );
+ temp.innerHTML = 'test';
+ parentNode.appendChild(temp);
+ lineHeight = temp.clientHeight;
+ parentNode.removeChild(temp);
+ }
+ return lineHeight;
}
});
@@ -1087,12 +1143,14 @@ if (typeof jQuery === 'undefined') {
var pre = this.getTextFromHeadToCaret();
var post = this.el.value.substring(pre.length);
var newSubstr = strategy.replace(value, e);
+ var regExp;
if (typeof newSubstr !== 'undefined') {
if ($.isArray(newSubstr)) {
post = newSubstr[1] + post;
newSubstr = newSubstr[0];
}
- pre = pre.replace(strategy.match, newSubstr);
+ regExp = $.isFunction(strategy.match) ? strategy.match(pre) : strategy.match;
+ pre = pre.replace(regExp, newSubstr);
this.$el.val(pre + post);
this.el.focus();
var range = this.el.createTextRange();
@@ -1138,30 +1196,35 @@ if (typeof jQuery === 'undefined') {
// When an dropdown item is selected, it is executed.
select: function (value, strategy, e) {
var pre = this.getTextFromHeadToCaret();
- var sel = window.getSelection()
+ // use ownerDocument instead of window to support iframes
+ var sel = this.el.ownerDocument.getSelection();
+
var range = sel.getRangeAt(0);
var selection = range.cloneRange();
selection.selectNodeContents(range.startContainer);
var content = selection.toString();
var post = content.substring(range.startOffset);
var newSubstr = strategy.replace(value, e);
+ var regExp;
if (typeof newSubstr !== 'undefined') {
if ($.isArray(newSubstr)) {
post = newSubstr[1] + post;
newSubstr = newSubstr[0];
}
- pre = pre.replace(strategy.match, newSubstr);
+ regExp = $.isFunction(strategy.match) ? strategy.match(pre) : strategy.match;
+ pre = pre.replace(regExp, newSubstr)
+ .replace(/ $/, " "); //   necessary at least for CKeditor to not eat spaces
range.selectNodeContents(range.startContainer);
range.deleteContents();
// create temporary elements
- var preWrapper = document.createElement("div");
+ var preWrapper = this.el.ownerDocument.createElement("div");
preWrapper.innerHTML = pre;
- var postWrapper = document.createElement("div");
+ var postWrapper = this.el.ownerDocument.createElement("div");
postWrapper.innerHTML = post;
// create the fragment thats inserted
- var fragment = document.createDocumentFragment();
+ var fragment = this.el.ownerDocument.createDocumentFragment();
var childNode;
var lastOfPre;
while (childNode = preWrapper.firstChild) {
@@ -1194,8 +1257,8 @@ if (typeof jQuery === 'undefined') {
//
// Dropdown's position will be decided using the result.
_getCaretRelativePosition: function () {
- var range = window.getSelection().getRangeAt(0).cloneRange();
- var node = document.createElement('span');
+ var range = this.el.ownerDocument.getSelection().getRangeAt(0).cloneRange();
+ var node = this.el.ownerDocument.createElement('span');
range.insertNode(node);
range.selectNodeContents(node);
range.deleteContents();
@@ -1204,6 +1267,17 @@ if (typeof jQuery === 'undefined') {
position.left -= this.$el.offset().left;
position.top += $node.height() - this.$el.offset().top;
position.lineHeight = $node.height();
+
+ // special positioning logic for iframes
+ // this is typically used for contenteditables such as tinymce or ckeditor
+ if (this.completer.$iframe) {
+ var iframePosition = this.completer.$iframe.offset();
+ position.top += iframePosition.top;
+ position.left += iframePosition.left;
+ //subtract scrollTop from element in iframe
+ position.top -= this.$el.scrollTop();
+ }
+
$node.remove();
return position;
},
@@ -1217,7 +1291,7 @@ if (typeof jQuery === 'undefined') {
// this.getTextFromHeadToCaret()
// // => ' wor' // not '
hello wor'
getTextFromHeadToCaret: function () {
- var range = window.getSelection().getRangeAt(0);
+ var range = this.el.ownerDocument.getSelection().getRangeAt(0);
var selection = range.cloneRange();
selection.selectNodeContents(range.startContainer);
return selection.toString().substring(0, range.startOffset);
@@ -1227,6 +1301,39 @@ if (typeof jQuery === 'undefined') {
$.fn.textcomplete.ContentEditable = ContentEditable;
}(jQuery);
+// NOTE: TextComplete plugin has contenteditable support but it does not work
+// fine especially on old IEs.
+// Any pull requests are REALLY welcome.
+
++function ($) {
+ 'use strict';
+
+ // CKEditor adapter
+ // =======================
+ //
+ // Adapter for CKEditor, based on contenteditable elements.
+ function CKEditor (element, completer, option) {
+ this.initialize(element, completer, option);
+ }
+
+ $.extend(CKEditor.prototype, $.fn.textcomplete.ContentEditable.prototype, {
+ _bindEvents: function () {
+ var $this = this;
+ this.option.ckeditor_instance.on('key', function(event) {
+ var domEvent = event.data;
+ $this._onKeyup(domEvent);
+ if ($this.completer.dropdown.shown && $this._skipSearch(domEvent)) {
+ return false;
+ }
+ }, null, null, 1); // 1 = Priority = Important!
+ // we actually also need the native event, as the CKEditor one is happening to late
+ this.$el.on('keyup.' + this.id, $.proxy(this._onKeyup, this));
+ },
+});
+
+ $.fn.textcomplete.CKEditor = CKEditor;
+}(jQuery);
+
// The MIT License (MIT)
//
// Copyright (c) 2015 Jonathan Ong me@jongleberry.com
@@ -1248,7 +1355,7 @@ if (typeof jQuery === 'undefined') {
//
// https://github.com/component/textarea-caret-position
-(function () {
+(function ($) {
// The properties that we copy into a mirrored div.
// Note that some browsers, such as Firefox,
@@ -1369,13 +1476,9 @@ function getCaretCoordinates(element, position, options) {
return coordinates;
}
-if (typeof module != 'undefined' && typeof module.exports != 'undefined') {
- module.exports = getCaretCoordinates;
-} else if(isBrowser){
- window.$.fn.textcomplete.getCaretCoordinates = getCaretCoordinates;
-}
+$.fn.textcomplete.getCaretCoordinates = getCaretCoordinates;
-}());
+}(jQuery));
return jQuery;
-}));
\ No newline at end of file
+}));
diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.az-short.js b/public/vendor/jquery/timeago/locales/jquery.timeago.az-short.js
new file mode 100644
index 0000000000..a430a45768
--- /dev/null
+++ b/public/vendor/jquery/timeago/locales/jquery.timeago.az-short.js
@@ -0,0 +1,20 @@
+// Azerbaijani shortened
+jQuery.timeago.settings.strings = {
+ prefixAgo: null,
+ prefixFromNow: null,
+ suffixAgo: "",
+ suffixFromNow: "",
+ seconds: '1 dəq',
+ minute: '1 dəq',
+ minutes: '%d dəq',
+ hour: '1 saat',
+ hours: '%d saat',
+ day: '1 gün',
+ days: '%d gün',
+ month: '1 ay',
+ months: '%d ay',
+ year: '1 il',
+ years: '%d il',
+ wordSeparator: '',
+ numbers: []
+};
diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.az.js b/public/vendor/jquery/timeago/locales/jquery.timeago.az.js
new file mode 100644
index 0000000000..1e04a23a83
--- /dev/null
+++ b/public/vendor/jquery/timeago/locales/jquery.timeago.az.js
@@ -0,0 +1,20 @@
+// Azerbaijani
+jQuery.timeago.settings.strings = {
+ prefixAgo: null,
+ prefixFromNow: null,
+ suffixAgo: 'əvvəl',
+ suffixFromNow: 'sonra',
+ seconds: 'saniyələr',
+ minute: '1 dəqiqə',
+ minutes: '%d dəqiqə',
+ hour: '1 saat',
+ hours: '%d saat',
+ day: '1 gün',
+ days: '%d gün',
+ month: '1 ay',
+ months: '%d ay',
+ year: '1 il',
+ years: '%d il',
+ wordSeparator: '',
+ numbers: []
+};
diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.de-short.js b/public/vendor/jquery/timeago/locales/jquery.timeago.de-short.js
index 10f158de08..09427ec976 100644
--- a/public/vendor/jquery/timeago/locales/jquery.timeago.de-short.js
+++ b/public/vendor/jquery/timeago/locales/jquery.timeago.de-short.js
@@ -4,17 +4,17 @@ jQuery.timeago.settings.strings = {
prefixFromNow: null,
suffixAgo: "",
suffixFromNow: "",
- seconds: "sec",
- minute: "1min",
- minutes: "%dmin",
+ seconds: "s",
+ minute: "1m",
+ minutes: "%dm",
hour: "1h",
hours: "%dh",
- day: "1d",
- days: "%dd",
- month: "1Mon",
- months: "%dMon",
- year: "1Jhr",
- years: "%dJhr",
+ day: "1T.",
+ days: "%dT.",
+ month: "1Mt.",
+ months: "%dMt.",
+ year: "1J.",
+ years: "%dJ.",
wordSeparator: " ",
numbers: []
};
diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.ja.js b/public/vendor/jquery/timeago/locales/jquery.timeago.ja.js
index fd81f275d0..95a7cd2a7a 100644
--- a/public/vendor/jquery/timeago/locales/jquery.timeago.ja.js
+++ b/public/vendor/jquery/timeago/locales/jquery.timeago.ja.js
@@ -11,8 +11,8 @@ jQuery.timeago.settings.strings = {
hours: "約 %d 時間",
day: "約 1 日",
days: "約 %d 日",
- month: "約 1 月",
- months: "約 %d 月",
+ month: "約 1 ヶ月",
+ months: "約 %d ヶ月",
year: "約 1 年",
years: "約 %d 年",
wordSeparator: ""
diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.lv.js b/public/vendor/jquery/timeago/locales/jquery.timeago.lv.js
new file mode 100644
index 0000000000..eb02391563
--- /dev/null
+++ b/public/vendor/jquery/timeago/locales/jquery.timeago.lv.js
@@ -0,0 +1,20 @@
+//Latvian
+jQuery.timeago.settings.strings = {
+ prefixAgo: "pirms",
+ prefixFromNow: null,
+ suffixAgo: null,
+ suffixFromNow: "no šī brīža",
+ seconds: "%d sek.",
+ minute: "min.",
+ minutes: "%d min.",
+ hour: "st.",
+ hours: "%d st.",
+ day: "1 d.",
+ days: "%d d.",
+ month: "mēnesis.",
+ months: "%d mēnesis.",
+ year: "gads",
+ years: "%d gads",
+ wordSeparator: " ",
+ numbers: []
+};
diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.sl.js b/public/vendor/jquery/timeago/locales/jquery.timeago.sl.js
index 57d4f6020c..b8ab587d82 100644
--- a/public/vendor/jquery/timeago/locales/jquery.timeago.sl.js
+++ b/public/vendor/jquery/timeago/locales/jquery.timeago.sl.js
@@ -28,7 +28,7 @@
},
month: "en mesec",
months: function (value) {
- return numpf(value, ["%d mescov", "%d mesec", "%d mesca", "%d mesce"]);
+ return numpf(value, ["%d mesecev", "%d mesec", "%d meseca", "%d mesece"]);
},
year: "eno leto",
years: function (value) {
diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.sr.js b/public/vendor/jquery/timeago/locales/jquery.timeago.sr.js
new file mode 100644
index 0000000000..c75a972e77
--- /dev/null
+++ b/public/vendor/jquery/timeago/locales/jquery.timeago.sr.js
@@ -0,0 +1,49 @@
+// Serbian
+(function () {
+ var numpf;
+
+ numpf = function (n, f, s, t) {
+ var n10;
+ n10 = n % 10;
+ if (n10 === 1 && (n === 1 || n > 20)) {
+ return f;
+ } else if (n10 > 1 && n10 < 5 && (n > 20 || n < 10)) {
+ return s;
+ } else {
+ return t;
+ }
+ };
+
+ jQuery.timeago.settings.strings = {
+ prefixAgo: "пре",
+ prefixFromNow: "за",
+ suffixAgo: null,
+ suffixFromNow: null,
+ second: "секунд",
+ seconds: function (value) {
+ return numpf(value, "%d секунд", "%d секунде", "%d секунди");
+ },
+ minute: "један минут",
+ minutes: function (value) {
+ return numpf(value, "%d минут", "%d минута", "%d минута");
+ },
+ hour: "један сат",
+ hours: function (value) {
+ return numpf(value, "%d сат", "%d сата", "%d сати");
+ },
+ day: "један дан",
+ days: function (value) {
+ return numpf(value, "%d дан", "%d дана", "%d дана");
+ },
+ month: "месец дана",
+ months: function (value) {
+ return numpf(value, "%d месец", "%d месеца", "%d месеци");
+ },
+ year: "годину дана",
+ years: function (value) {
+ return numpf(value, "%d годину", "%d године", "%d година");
+ },
+ wordSeparator: " "
+ };
+
+}).call(this);
diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.tr-short.js b/public/vendor/jquery/timeago/locales/jquery.timeago.tr-short.js
new file mode 100644
index 0000000000..ebc2277b49
--- /dev/null
+++ b/public/vendor/jquery/timeago/locales/jquery.timeago.tr-short.js
@@ -0,0 +1,20 @@
+// Turkish shortened
+jQuery.timeago.settings.strings = {
+ prefixAgo: null,
+ prefixFromNow: null,
+ suffixAgo: "",
+ suffixFromNow: "",
+ seconds: "1sn",
+ minute: "1d",
+ minutes: "%dd",
+ hour: "1s",
+ hours: "%ds",
+ day: "1g",
+ days: "%dg",
+ month: "1ay",
+ months: "%day",
+ year: "1y",
+ years: "%dy",
+ wordSeparator: " ",
+ numbers: []
+};
diff --git a/public/vendor/mousetrap/mousetrap.js b/public/vendor/mousetrap/mousetrap.js
deleted file mode 100644
index 01709ffd9a..0000000000
--- a/public/vendor/mousetrap/mousetrap.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/* mousetrap v1.4.6 craig.is/killing/mice */
-(function(J,r,f){function s(a,b,d){a.addEventListener?a.addEventListener(b,d,!1):a.attachEvent("on"+b,d)}function A(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return h[a.which]?h[a.which]:B[a.which]?B[a.which]:String.fromCharCode(a.which).toLowerCase()}function t(a){a=a||{};var b=!1,d;for(d in n)a[d]?b=!0:n[d]=0;b||(u=!1)}function C(a,b,d,c,e,v){var g,k,f=[],h=d.type;if(!l[a])return[];"keyup"==h&&w(a)&&(b=[a]);for(g=0;g
g||h.hasOwnProperty(g)&&(p[h[g]]=g)}e=p[d]?"keydown":"keypress"}"keypress"==e&&f.length&&(e="keydown");return{key:c,modifiers:f,action:e}}function F(a,b,d,c,e){q[a+":"+d]=b;a=a.replace(/\s+/g," ");var f=a.split(" ");1":".","?":"/","|":"\\"},G={option:"alt",command:"meta","return":"enter",escape:"esc",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p,l={},q={},n={},D,z=!1,I=!1,u=!1;for(f=1;20>f;++f)h[111+f]="f"+f;for(f=0;9>=f;++f)h[f+96]=f;s(r,"keypress",y);s(r,"keydown",y);s(r,"keyup",y);var m={bind:function(a,b,d){a=a instanceof Array?a:[a];for(var c=0;cn?t:n>e?e:n}function t(n){return 100*(-1+n)}function e(n,e,r){var i;return i="translate3d"===c.positionUsing?{transform:"translate3d("+t(n)+"%,0,0)"}:"translate"===c.positionUsing?{transform:"translate("+t(n)+"%,0)"}:{"margin-left":t(n)+"%"},i.transition="all "+e+"ms "+r,i}function r(n,t){var e="string"==typeof n?n:o(n);return e.indexOf(" "+t+" ")>=0}function i(n,t){var e=o(n),i=e+t;r(e,t)||(n.className=i.substring(1))}function s(n,t){var e,i=o(n);r(n,t)&&(e=i.replace(" "+t+" "," "),n.className=e.substring(1,e.length-1))}function o(n){return(" "+(n.className||"")+" ").replace(/\s+/gi," ")}function a(n){n&&n.parentNode&&n.parentNode.removeChild(n)}var u={};u.version="0.1.6";var c=u.settings={minimum:.08,easing:"ease",positionUsing:"",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,showSpinner:!0,barSelector:'[role="bar"]',spinnerSelector:'[role="spinner"]',parent:"body",template:''};u.configure=function(n){var t,e;for(t in n)e=n[t],void 0!==e&&n.hasOwnProperty(t)&&(c[t]=e);return this},u.status=null,u.set=function(t){var r=u.isStarted();t=n(t,c.minimum,1),u.status=1===t?null:t;var i=u.render(!r),s=i.querySelector(c.barSelector),o=c.speed,a=c.easing;return i.offsetWidth,l(function(n){""===c.positionUsing&&(c.positionUsing=u.getPositioningCSS()),f(s,e(t,o,a)),1===t?(f(i,{transition:"none",opacity:1}),i.offsetWidth,setTimeout(function(){f(i,{transition:"all "+o+"ms linear",opacity:0}),setTimeout(function(){u.remove(),n()},o)},o)):setTimeout(n,o)}),this},u.isStarted=function(){return"number"==typeof u.status},u.start=function(){u.status||u.set(0);var n=function(){setTimeout(function(){u.status&&(u.trickle(),n())},c.trickleSpeed)};return c.trickle&&n(),this},u.done=function(n){return n||u.status?u.inc(.3+.5*Math.random()).set(1):this},u.inc=function(t){var e=u.status;return e?("number"!=typeof t&&(t=(1-e)*n(Math.random()*e,.1,.95)),e=n(e+t,0,.994),u.set(e)):u.start()},u.trickle=function(){return u.inc(Math.random()*c.trickleRate)},function(){var n=0,t=0;u.promise=function(e){return e&&"resolved"!=e.state()?(0==t&&u.start(),n++,t++,e.always(function(){t--,0==t?(n=0,u.done()):u.set((n-t)/n)}),this):this}}(),u.render=function(n){if(u.isRendered())return document.getElementById("nprogress");i(document.documentElement,"nprogress-busy");var e=document.createElement("div");e.id="nprogress",e.innerHTML=c.template;var r,s=e.querySelector(c.barSelector),o=n?"-100":t(u.status||0),l=document.querySelector(c.parent);return f(s,{transition:"all 0 linear",transform:"translate3d("+o+"%,0,0)"}),c.showSpinner||(r=e.querySelector(c.spinnerSelector),r&&a(r)),l!=document.body&&i(l,"nprogress-custom-parent"),l.appendChild(e),e},u.remove=function(){s(document.documentElement,"nprogress-busy"),s(document.querySelector(c.parent),"nprogress-custom-parent");var n=document.getElementById("nprogress");n&&a(n)},u.isRendered=function(){return!!document.getElementById("nprogress")},u.getPositioningCSS=function(){var n=document.body.style,t="WebkitTransform"in n?"Webkit":"MozTransform"in n?"Moz":"msTransform"in n?"ms":"OTransform"in n?"O":"";return t+"Perspective"in n?"translate3d":t+"Transform"in n?"translate":"margin"};var l=function(){function n(){var e=t.shift();e&&e(n)}var t=[];return function(e){t.push(e),1==t.length&&n()}}(),f=function(){function n(n){return n.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,function(n,t){return t.toUpperCase()})}function t(n){var t=document.body.style;if(n in t)return n;for(var e,r=i.length,s=n.charAt(0).toUpperCase()+n.slice(1);r--;)if(e=i[r]+s,e in t)return e;return n}function e(e){return e=n(e),s[e]||(s[e]=t(e))}function r(n,t,r){t=e(t),n.style[t]=r}var i=["Webkit","O","Moz","ms"],s={};return function(n,t){var e,i,s=arguments;if(2==s.length)for(e in t)i=t[e],void 0!==i&&t.hasOwnProperty(e)&&r(n,e,i);else r(n,s[1],s[2])}}();return u});
\ No newline at end of file
diff --git a/public/vendor/requirejs/require.js b/public/vendor/requirejs/require.js
index f04b8c3f7d..857eb5b700 100644
--- a/public/vendor/requirejs/require.js
+++ b/public/vendor/requirejs/require.js
@@ -1,36 +1,36 @@
/*
- RequireJS 2.1.6 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
- Available via the MIT or new BSD license.
- see: http://github.com/jrburke/requirejs for details
+ RequireJS 2.2.0 Copyright jQuery Foundation and other contributors.
+ Released under MIT license, http://github.com/requirejs/requirejs/LICENSE
*/
var requirejs,require,define;
-(function(ba){function J(b){return"[object Function]"===N.call(b)}function K(b){return"[object Array]"===N.call(b)}function z(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(J(n)){if(this.events.error&&this.map.isDefine||h.onError!==ca)try{e=k.execCb(c,n,b,e)}catch(d){a=d}else e=k.execCb(c,n,b,e);this.map.isDefine&&((b=this.module)&&void 0!==b.exports&&b.exports!==
-this.exports?e=b.exports:void 0===e&&this.usingExports&&(e=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",w(this.error=a)}else e=n;this.exports=e;if(this.map.isDefine&&!this.ignore&&(r[c]=e,h.onResourceLoad))h.onResourceLoad(k,this.map,this.depMaps);y(c);this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=
-!0)}}else this.fetch()}},callPlugin:function(){var a=this.map,b=a.id,d=l(a.prefix);this.depMaps.push(d);u(d,"defined",v(this,function(e){var n,d;d=this.map.name;var g=this.map.parentMap?this.map.parentMap.name:null,C=k.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(e.normalize&&(d=e.normalize(d,function(a){return c(a,g,!0)})||""),e=l(a.prefix+"!"+d,this.map.parentMap),u(e,"defined",v(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),
-d=m(q,e.id)){this.depMaps.push(e);if(this.events.error)d.on("error",v(this,function(a){this.emit("error",a)}));d.enable()}}else n=v(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),n.error=v(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];H(q,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&y(a.map.id)});w(a)}),n.fromText=v(this,function(e,c){var d=a.name,g=l(d),i=Q;c&&(e=c);i&&(Q=!1);s(g);t(j.config,b)&&(j.config[d]=j.config[b]);try{h.exec(e)}catch(D){return w(B("fromtexteval",
-"fromText eval for "+b+" failed: "+D,D,[b]))}i&&(Q=!0);this.depMaps.push(g);k.completeLoad(d);C([d],n)}),e.load(a.name,C,n,j)}));k.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){W[this.map.id]=this;this.enabling=this.enabled=!0;z(this.depMaps,v(this,function(a,b){var c,e;if("string"===typeof a){a=l(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=m(P,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;u(a,"defined",v(this,function(a){this.defineDep(b,
-a);this.check()}));this.errback&&u(a,"error",v(this,this.errback))}c=a.id;e=q[c];!t(P,c)&&(e&&!e.enabled)&&k.enable(a,this)}));H(this.pluginMaps,v(this,function(a){var b=m(q,a.id);b&&!b.enabled&&k.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){z(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};k={config:j,contextName:b,registry:q,defined:r,urlFetched:V,defQueue:I,Module:$,makeModuleMap:l,
-nextTick:h.nextTick,onError:w,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=j.pkgs,c=j.shim,e={paths:!0,config:!0,map:!0};H(a,function(a,b){e[b]?"map"===b?(j.map||(j.map={}),S(j[b],a,!0,!0)):S(j[b],a,!0):j[b]=a});a.shim&&(H(a.shim,function(a,b){K(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=k.makeShimExports(a);c[b]=a}),j.shim=c);a.packages&&(z(a.packages,function(a){a="string"===typeof a?{name:a}:a;b[a.name]={name:a.name,
-location:a.location||a.name,main:(a.main||"main").replace(ka,"").replace(fa,"")}}),j.pkgs=b);H(q,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=l(b))});if(a.deps||a.callback)k.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ba,arguments));return b||a.exports&&da(a.exports)}},makeRequire:function(a,f){function d(e,c,g){var i,j;f.enableBuildCallback&&(c&&J(c))&&(c.__requireJsBuild=!0);if("string"===typeof e){if(J(c))return w(B("requireargs",
-"Invalid require call"),g);if(a&&t(P,e))return P[e](q[a.id]);if(h.get)return h.get(k,e,a,d);i=l(e,a,!1,!0);i=i.id;return!t(r,i)?w(B("notloaded",'Module name "'+i+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):r[i]}M();k.nextTick(function(){M();j=s(l(null,a));j.skipMap=f.skipMap;j.init(e,c,g,{enabled:!0});E()});return d}f=f||{};S(d,{isBrowser:A,toUrl:function(b){var d,f=b.lastIndexOf("."),g=b.split("/")[0];if(-1!==f&&(!("."===g||".."===g)||1g.attachEvent.toString().indexOf("[native code"))&&!Z?(Q=!0,g.attachEvent("onreadystatechange",b.onScriptLoad)):(g.addEventListener("load",b.onScriptLoad,!1),g.addEventListener("error",b.onScriptError,!1)),g.src=d,M=g,E?y.insertBefore(g,E):y.appendChild(g),
-M=null,g;if(ea)try{importScripts(d),b.completeLoad(c)}catch(l){b.onError(B("importscripts","importScripts failed for "+c+" at "+d,l,[c]))}};A&&O(document.getElementsByTagName("script"),function(b){y||(y=b.parentNode);if(L=b.getAttribute("data-main"))return s=L,u.baseUrl||(F=s.split("/"),s=F.pop(),ga=F.length?F.join("/")+"/":"./",u.baseUrl=ga),s=s.replace(fa,""),h.jsExtRegExp.test(s)&&(s=L),u.deps=u.deps?u.deps.concat(s):[s],!0});define=function(b,c,d){var h,g;"string"!==typeof b&&(d=c,c=b,b=null);
-K(c)||(d=c,c=null);!c&&J(d)&&(c=[],d.length&&(d.toString().replace(ma,"").replace(na,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));if(Q){if(!(h=M))R&&"interactive"===R.readyState||O(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return R=b}),h=R;h&&(b||(b=h.getAttribute("data-requiremodule")),g=G[h.getAttribute("data-requirecontext")])}(g?g.defQueue:U).push([b,c,d])};define.amd={jQuery:!0};h.exec=function(b){return eval(b)};
-h(u)}})(this);
\ No newline at end of file
+(function(ga){function ka(b,c,d,g){return g||""}function K(b){return"[object Function]"===Q.call(b)}function L(b){return"[object Array]"===Q.call(b)}function y(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(K(k)){if(this.events.error&&this.map.isDefine||g.onError!==
+ha)try{h=l.execCb(c,k,b,h)}catch(d){a=d}else h=l.execCb(c,k,b,h);this.map.isDefine&&void 0===h&&((b=this.module)?h=b.exports:this.usingExports&&(h=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",A(this.error=a)}else h=k;this.exports=h;if(this.map.isDefine&&!this.ignore&&(v[c]=h,g.onResourceLoad)){var f=[];y(this.depMaps,function(a){f.push(a.normalizedMap||a)});g.onResourceLoad(l,this.map,f)}C(c);
+this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}},callPlugin:function(){var a=this.map,b=a.id,d=q(a.prefix);this.depMaps.push(d);w(d,"defined",z(this,function(h){var k,f,d=e(fa,this.map.id),M=this.map.name,r=this.map.parentMap?this.map.parentMap.name:null,m=l.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(h.normalize&&(M=h.normalize(M,function(a){return c(a,r,!0)})||
+""),f=q(a.prefix+"!"+M,this.map.parentMap),w(f,"defined",z(this,function(a){this.map.normalizedMap=f;this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),h=e(t,f.id)){this.depMaps.push(f);if(this.events.error)h.on("error",z(this,function(a){this.emit("error",a)}));h.enable()}}else d?(this.map.url=l.nameToUrl(d),this.load()):(k=z(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),k.error=z(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];D(t,function(a){0===
+a.map.id.indexOf(b+"_unnormalized")&&C(a.map.id)});A(a)}),k.fromText=z(this,function(h,c){var d=a.name,f=q(d),M=S;c&&(h=c);M&&(S=!1);u(f);x(p.config,b)&&(p.config[d]=p.config[b]);try{g.exec(h)}catch(e){return A(F("fromtexteval","fromText eval for "+b+" failed: "+e,e,[b]))}M&&(S=!0);this.depMaps.push(f);l.completeLoad(d);m([d],k)}),h.load(a.name,m,k,p))}));l.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){Z[this.map.id]=this;this.enabling=this.enabled=!0;y(this.depMaps,z(this,function(a,
+b){var c,h;if("string"===typeof a){a=q(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=e(R,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;w(a,"defined",z(this,function(a){this.undefed||(this.defineDep(b,a),this.check())}));this.errback?w(a,"error",z(this,this.errback)):this.events.error&&w(a,"error",z(this,function(a){this.emit("error",a)}))}c=a.id;h=t[c];x(R,c)||!h||h.enabled||l.enable(a,this)}));D(this.pluginMaps,z(this,function(a){var b=e(t,a.id);
+b&&!b.enabled&&l.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){y(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};l={config:p,contextName:b,registry:t,defined:v,urlFetched:W,defQueue:G,defQueueMap:{},Module:da,makeModuleMap:q,nextTick:g.nextTick,onError:A,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");if("string"===typeof a.urlArgs){var b=
+a.urlArgs;a.urlArgs=function(a,c){return(-1===c.indexOf("?")?"?":"&")+b}}var c=p.shim,h={paths:!0,bundles:!0,config:!0,map:!0};D(a,function(a,b){h[b]?(p[b]||(p[b]={}),Y(p[b],a,!0,!0)):p[b]=a});a.bundles&&D(a.bundles,function(a,b){y(a,function(a){a!==b&&(fa[a]=b)})});a.shim&&(D(a.shim,function(a,b){L(a)&&(a={deps:a});!a.exports&&!a.init||a.exportsFn||(a.exportsFn=l.makeShimExports(a));c[b]=a}),p.shim=c);a.packages&&y(a.packages,function(a){var b;a="string"===typeof a?{name:a}:a;b=a.name;a.location&&
+(p.paths[b]=a.location);p.pkgs[b]=a.name+"/"+(a.main||"main").replace(na,"").replace(U,"")});D(t,function(a,b){a.inited||a.map.unnormalized||(a.map=q(b,null,!0))});(a.deps||a.callback)&&l.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ga,arguments));return b||a.exports&&ia(a.exports)}},makeRequire:function(a,n){function m(c,d,f){var e,r;n.enableBuildCallback&&d&&K(d)&&(d.__requireJsBuild=!0);if("string"===typeof c){if(K(d))return A(F("requireargs",
+"Invalid require call"),f);if(a&&x(R,c))return R[c](t[a.id]);if(g.get)return g.get(l,c,a,m);e=q(c,a,!1,!0);e=e.id;return x(v,e)?v[e]:A(F("notloaded",'Module name "'+e+'" has not been loaded yet for context: '+b+(a?"":". Use require([])")))}P();l.nextTick(function(){P();r=u(q(null,a));r.skipMap=n.skipMap;r.init(c,d,f,{enabled:!0});H()});return m}n=n||{};Y(m,{isBrowser:E,toUrl:function(b){var d,f=b.lastIndexOf("."),g=b.split("/")[0];-1!==f&&("."!==g&&".."!==g||1e.attachEvent.toString().indexOf("[native code")||ca?(e.addEventListener("load",b.onScriptLoad,!1),e.addEventListener("error",b.onScriptError,!1)):(S=!0,e.attachEvent("onreadystatechange",b.onScriptLoad));e.src=d;if(m.onNodeCreated)m.onNodeCreated(e,m,c,d);P=e;H?C.insertBefore(e,H):C.appendChild(e);P=null;return e}if(ja)try{setTimeout(function(){},
+0),importScripts(d),b.completeLoad(c)}catch(q){b.onError(F("importscripts","importScripts failed for "+c+" at "+d,q,[c]))}};E&&!w.skipDataMain&&X(document.getElementsByTagName("script"),function(b){C||(C=b.parentNode);if(O=b.getAttribute("data-main"))return u=O,w.baseUrl||-1!==u.indexOf("!")||(I=u.split("/"),u=I.pop(),T=I.length?I.join("/")+"/":"./",w.baseUrl=T),u=u.replace(U,""),g.jsExtRegExp.test(u)&&(u=O),w.deps=w.deps?w.deps.concat(u):[u],!0});define=function(b,c,d){var e,g;"string"!==typeof b&&
+(d=c,c=b,b=null);L(c)||(d=c,c=null);!c&&K(d)&&(c=[],d.length&&(d.toString().replace(qa,ka).replace(ra,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));S&&(e=P||pa())&&(b||(b=e.getAttribute("data-requiremodule")),g=J[e.getAttribute("data-requirecontext")]);g?(g.defQueue.push([b,c,d]),g.defQueueMap[b]=!0):V.push([b,c,d])};define.amd={jQuery:!0};g.exec=function(b){return eval(b)};g(w)}})(this);
diff --git a/public/vendor/tinycon/tinycon.js b/public/vendor/tinycon/tinycon.js
index 3317cc0d03..3e3657cdf8 100644
--- a/public/vendor/tinycon/tinycon.js
+++ b/public/vendor/tinycon/tinycon.js
@@ -1,188 +1,200 @@
/*!
* Tinycon - A small library for manipulating the Favicon
* Tom Moor, http://tommoor.com
- * Copyright (c) 2012 Tom Moor
- * MIT Licensed
- * @version 0.6.1
+ * Copyright (c) 2015 Tom Moor
+ * @license MIT Licensed
+ * @version 0.6.4
*/
(function(){
- var Tinycon = {};
- var currentFavicon = null;
- var originalFavicon = null;
- var originalTitle = document.title;
- var faviconImage = null;
- var canvas = null;
- var options = {};
- var r = window.devicePixelRatio || 1;
- var size = 16 * r;
- var defaults = {
- width: 7,
- height: 9,
- font: 9 * r + 'px arial',
- colour: '#ffffff',
- background: '#F03D25',
- fallback: true,
- crossOrigin: true,
- abbreviate: true
- };
+ var Tinycon = {};
+ var currentFavicon = null;
+ var originalFavicon = null;
+ var faviconImage = null;
+ var canvas = null;
+ var options = {};
+ var r = window.devicePixelRatio || 1;
+ var size = 16 * r;
+ var defaults = {
+ width: 7,
+ height: 9,
+ font: 10 * r + 'px arial',
+ color: '#ffffff',
+ background: '#F03D25',
+ fallback: true,
+ crossOrigin: true,
+ abbreviate: true
+ };
- var ua = (function () {
- var agent = navigator.userAgent.toLowerCase();
- // New function has access to 'agent' via closure
- return function (browser) {
- return agent.indexOf(browser) !== -1;
- };
- }());
+ var ua = (function () {
+ var agent = navigator.userAgent.toLowerCase();
+ // New function has access to 'agent' via closure
+ return function (browser) {
+ return agent.indexOf(browser) !== -1;
+ };
+ }());
- var browser = {
- ie: ua('msie'),
- chrome: ua('chrome'),
- webkit: ua('chrome') || ua('safari'),
- safari: ua('safari') && !ua('chrome'),
- mozilla: ua('mozilla') && !ua('chrome') && !ua('safari')
- };
+ var browser = {
+ ie: ua('trident'),
+ chrome: ua('chrome'),
+ webkit: ua('chrome') || ua('safari'),
+ safari: ua('safari') && !ua('chrome'),
+ mozilla: ua('mozilla') && !ua('chrome') && !ua('safari')
+ };
- // private methods
- var getFaviconTag = function(){
+ // private methods
+ var getFaviconTag = function(){
- var links = document.getElementsByTagName('link');
+ var links = document.getElementsByTagName('link');
- for(var i=0, len=links.length; i < len; i++) {
- if ((links[i].getAttribute('rel') || '').match(/\bicon\b/)) {
- return links[i];
- }
- }
+ for(var i=0, len=links.length; i < len; i++) {
+ if ((links[i].getAttribute('rel') || '').match(/\bicon\b/i)) {
+ return links[i];
+ }
+ }
- return false;
- };
+ return false;
+ };
- var removeFaviconTag = function(){
+ var removeFaviconTag = function(){
- var links = document.getElementsByTagName('link');
- var head = document.getElementsByTagName('head')[0];
+ var links = document.getElementsByTagName('link');
+ var head = document.getElementsByTagName('head')[0];
- for(var i=0, len=links.length; i < len; i++) {
- var exists = (typeof(links[i]) !== 'undefined');
- if (exists && (links[i].getAttribute('rel') || '').match(/\bicon\b/)) {
- head.removeChild(links[i]);
- }
- }
- };
+ for(var i=0, len=links.length; i < len; i++) {
+ var exists = (typeof(links[i]) !== 'undefined');
+ if (exists && (links[i].getAttribute('rel') || '').match(/\bicon\b/i)) {
+ head.removeChild(links[i]);
+ }
+ }
+ };
- var getCurrentFavicon = function(){
+ var getCurrentFavicon = function(){
- if (!originalFavicon || !currentFavicon) {
- var tag = getFaviconTag();
- originalFavicon = currentFavicon = tag ? tag.getAttribute('href') : '/favicon.ico';
- }
+ if (!originalFavicon || !currentFavicon) {
+ var tag = getFaviconTag();
+ currentFavicon = tag ? tag.getAttribute('href') : '/favicon.ico';
+ if (!originalFavicon) {
+ originalFavicon = currentFavicon;
+ }
+ }
- return currentFavicon;
- };
+ return currentFavicon;
+ };
- var getCanvas = function (){
+ var getCanvas = function (){
- if (!canvas) {
- canvas = document.createElement("canvas");
- canvas.width = size;
- canvas.height = size;
- }
+ if (!canvas) {
+ canvas = document.createElement("canvas");
+ canvas.width = size;
+ canvas.height = size;
+ }
- return canvas;
- };
+ return canvas;
+ };
- var setFaviconTag = function(url){
- removeFaviconTag();
+ var setFaviconTag = function(url){
+ if(url){
+ removeFaviconTag();
- var link = document.createElement('link');
- link.type = 'image/x-icon';
- link.rel = 'icon';
- link.href = url;
- document.getElementsByTagName('head')[0].appendChild(link);
- };
+ var link = document.createElement('link');
+ link.type = 'image/x-icon';
+ link.rel = 'icon';
+ link.href = url;
+ document.getElementsByTagName('head')[0].appendChild(link);
+ }
+ };
- var log = function(message){
- if (window.console) window.console.log(message);
- };
+ var log = function(message){
+ if (window.console) window.console.log(message);
+ };
- var drawFavicon = function(label, colour) {
+ var drawFavicon = function(label, color) {
- // fallback to updating the browser title if unsupported
- if (!getCanvas().getContext || browser.ie || browser.safari || options.fallback === 'force') {
- return updateTitle(label);
- }
+ // fallback to updating the browser title if unsupported
+ if (!getCanvas().getContext || browser.ie || browser.safari || options.fallback === 'force') {
+ return updateTitle(label);
+ }
- var context = getCanvas().getContext("2d");
- var colour = colour || '#000000';
- var src = getCurrentFavicon();
+ var context = getCanvas().getContext("2d");
+ var color = color || '#000000';
+ var src = getCurrentFavicon();
- faviconImage = document.createElement('img');
- faviconImage.onload = function() {
+ faviconImage = document.createElement('img');
+ faviconImage.onload = function() {
- // clear canvas
- context.clearRect(0, 0, size, size);
+ // clear canvas
+ context.clearRect(0, 0, size, size);
- // draw the favicon
- context.drawImage(faviconImage, 0, 0, faviconImage.width, faviconImage.height, 0, 0, size, size);
+ // draw the favicon
+ context.drawImage(faviconImage, 0, 0, faviconImage.width, faviconImage.height, 0, 0, size, size);
- // draw bubble over the top
- if ((label + '').length > 0) drawBubble(context, label, colour);
+ // draw bubble over the top
+ if ((label + '').length > 0) drawBubble(context, label, color);
- // refresh tag in page
- refreshFavicon();
- };
+ // refresh tag in page
+ refreshFavicon();
+ };
- // allow cross origin resource requests if the image is not a data:uri
- // as detailed here: https://github.com/mrdoob/three.js/issues/1305
- if (!src.match(/^data/) && options.crossOrigin) {
- faviconImage.crossOrigin = 'anonymous';
- }
+ // allow cross origin resource requests if the image is not a data:uri
+ // as detailed here: https://github.com/mrdoob/three.js/issues/1305
+ if (!src.match(/^data/) && options.crossOrigin) {
+ faviconImage.crossOrigin = 'anonymous';
+ }
- faviconImage.src = src;
- };
+ faviconImage.src = src;
+ };
- var updateTitle = function(label) {
+ var updateTitle = function(label) {
- if (options.fallback) {
- if ((label + '').length > 0) {
- document.title = '(' + label + ') ' + originalTitle;
- } else {
- document.title = originalTitle;
- }
- }
- };
+ if (options.fallback) {
+ // Grab the current title that we can prefix with the label
+ var originalTitle = document.title;
- var drawBubble = function(context, label, colour) {
+ // Strip out the old label if there is one
+ if (originalTitle[0] === '(') {
+ originalTitle = originalTitle.slice(originalTitle.indexOf(' '));
+ }
- // automatic abbreviation for long (>2 digits) numbers
- if (typeof label == 'number' && label > 99 && options.abbreviate) {
- label = abbreviateNumber(label);
- }
+ if ((label + '').length > 0) {
+ document.title = '(' + label + ') ' + originalTitle;
+ } else {
+ document.title = originalTitle;
+ }
+ }
+ };
- // bubble needs to be larger for double digits
- var len = (label + '').length-1;
+ var drawBubble = function(context, label, color) {
- var width = options.width * r + (6 * r * len),
- height = options.height * r;
+ // automatic abbreviation for long (>2 digits) numbers
+ if (typeof label == 'number' && label > 99 && options.abbreviate) {
+ label = abbreviateNumber(label);
+ }
- var top = size - height,
+ // bubble needs to be larger for double digits
+ var len = (label + '').length-1;
+
+ var width = options.width * r + (6 * r * len),
+ height = options.height * r;
+
+ var top = size - height,
left = size - width - r,
bottom = 16 * r,
right = 16 * r,
radius = 2 * r;
- // webkit seems to render fonts lighter than firefox
- context.font = (browser.webkit ? 'bold ' : '') + options.font;
- context.fillStyle = options.background;
- context.strokeStyle = options.background;
- context.lineWidth = r;
+ // webkit seems to render fonts lighter than firefox
+ context.font = (browser.webkit ? 'bold ' : '') + options.font;
+ context.fillStyle = options.background;
+ context.strokeStyle = options.background;
+ context.lineWidth = r;
- // bubble
- context.beginPath();
+ // bubble
+ context.beginPath();
context.moveTo(left + radius, top);
- context.quadraticCurveTo(left, top, left, top + radius);
- context.lineTo(left, bottom - radius);
+ context.quadraticCurveTo(left, top, left, top + radius);
+ context.lineTo(left, bottom - radius);
context.quadraticCurveTo(left, bottom, left + radius, bottom);
context.lineTo(right - radius, bottom);
context.quadraticCurveTo(right, bottom, right, bottom - radius);
@@ -191,77 +203,85 @@
context.closePath();
context.fill();
- // bottom shadow
- context.beginPath();
- context.strokeStyle = "rgba(0,0,0,0.3)";
- context.moveTo(left + radius / 2.0, bottom);
- context.lineTo(right - radius / 2.0, bottom);
- context.stroke();
+ // bottom shadow
+ context.beginPath();
+ context.strokeStyle = "rgba(0,0,0,0.3)";
+ context.moveTo(left + radius / 2.0, bottom);
+ context.lineTo(right - radius / 2.0, bottom);
+ context.stroke();
- // label
- context.fillStyle = options.colour;
- context.textAlign = "right";
- context.textBaseline = "top";
+ // label
+ context.fillStyle = options.color;
+ context.textAlign = "right";
+ context.textBaseline = "top";
- // unfortunately webkit/mozilla are a pixel different in text positioning
- context.fillText(label, r === 2 ? 29 : 15, browser.mozilla ? 7*r : 6*r);
- };
+ // unfortunately webkit/mozilla are a pixel different in text positioning
+ context.fillText(label, r === 2 ? 29 : 15, browser.mozilla ? 7*r : 6*r);
+ };
- var refreshFavicon = function(){
- // check support
- if (!getCanvas().getContext) return;
+ var refreshFavicon = function(){
+ // check support
+ if (!getCanvas().getContext) return;
- setFaviconTag(getCanvas().toDataURL());
- };
+ setFaviconTag(getCanvas().toDataURL());
+ };
- var abbreviateNumber = function(label) {
- var metricPrefixes = [
- ['G', 1000000000],
- ['M', 1000000],
- ['k', 1000]
- ];
+ var abbreviateNumber = function(label) {
+ var metricPrefixes = [
+ ['G', 1000000000],
+ ['M', 1000000],
+ ['k', 1000]
+ ];
- for(var i = 0; i < metricPrefixes.length; ++i) {
- if (label >= metricPrefixes[i][1]) {
- label = round(label / metricPrefixes[i][1]) + metricPrefixes[i][0];
- break;
- }
- }
+ for(var i = 0; i < metricPrefixes.length; ++i) {
+ if (label >= metricPrefixes[i][1]) {
+ label = round(label / metricPrefixes[i][1]) + metricPrefixes[i][0];
+ break;
+ }
+ }
- return label;
- };
+ return label;
+ };
- var round = function (value, precision) {
- var number = new Number(value);
- return number.toFixed(precision);
- };
+ var round = function (value, precision) {
+ var number = new Number(value);
+ return number.toFixed(precision);
+ };
- // public methods
- Tinycon.setOptions = function(custom){
- options = {};
+ // public methods
+ Tinycon.setOptions = function(custom){
+ options = {};
- for(var key in defaults){
- options[key] = custom.hasOwnProperty(key) ? custom[key] : defaults[key];
- }
- return this;
- };
+ // account for deprecated UK English spelling
+ if (custom.colour) {
+ custom.color = custom.colour;
+ }
- Tinycon.setImage = function(url){
- currentFavicon = url;
- refreshFavicon();
- return this;
- };
+ for(var key in defaults){
+ options[key] = custom.hasOwnProperty(key) ? custom[key] : defaults[key];
+ }
+ return this;
+ };
- Tinycon.setBubble = function(label, colour) {
- label = label || '';
- drawFavicon(label, colour);
- return this;
- };
+ Tinycon.setImage = function(url){
+ currentFavicon = url;
+ refreshFavicon();
+ return this;
+ };
- Tinycon.reset = function(){
- setFaviconTag(originalFavicon);
- };
+ Tinycon.setBubble = function(label, color) {
+ label = label || '';
+ drawFavicon(label, color);
+ return this;
+ };
- Tinycon.setOptions(defaults);
- window.Tinycon = Tinycon;
-})();
\ No newline at end of file
+ Tinycon.reset = function(){
+ currentFavicon = originalFavicon;
+ setFaviconTag(originalFavicon);
+ };
+
+ Tinycon.setOptions(defaults);
+
+ window.Tinycon = Tinycon;
+
+})();
diff --git a/src/analytics.js b/src/analytics.js
index c1ede42eba..ab834b75b2 100644
--- a/src/analytics.js
+++ b/src/analytics.js
@@ -6,7 +6,7 @@ var winston = require('winston');
var db = require('./database');
-(function(Analytics) {
+(function (Analytics) {
var counters = {};
var pageViews = 0;
@@ -15,24 +15,24 @@ var db = require('./database');
var isCategory = /^(?:\/api)?\/category\/(\d+)/;
- new cronJob('*/10 * * * *', function() {
+ new cronJob('*/10 * * * *', function () {
Analytics.writeData();
}, null, true);
- Analytics.increment = function(keys) {
+ Analytics.increment = function (keys) {
keys = Array.isArray(keys) ? keys : [keys];
- keys.forEach(function(key) {
+ keys.forEach(function (key) {
counters[key] = counters[key] || 0;
++counters[key];
});
};
- Analytics.pageView = function(payload) {
+ Analytics.pageView = function (payload) {
++pageViews;
if (payload.ip) {
- db.sortedSetScore('ip:recent', payload.ip, function(err, score) {
+ db.sortedSetScore('ip:recent', payload.ip, function (err, score) {
if (err) {
return;
}
@@ -58,7 +58,7 @@ var db = require('./database');
}
};
- Analytics.writeData = function() {
+ Analytics.writeData = function () {
var today = new Date();
var month = new Date();
var dbQueue = [];
@@ -92,14 +92,14 @@ var db = require('./database');
}
}
- async.parallel(dbQueue, function(err) {
+ async.parallel(dbQueue, function (err) {
if (err) {
winston.error('[analytics] Encountered error while writing analytics to data store: ' + err.message);
}
});
};
- Analytics.getHourlyStatsForSet = function(set, hour, numHours, callback) {
+ Analytics.getHourlyStatsForSet = function (set, hour, numHours, callback) {
var terms = {},
hoursArr = [];
@@ -111,19 +111,19 @@ var db = require('./database');
hour.setHours(hour.getHours() - 1, 0, 0, 0);
}
- db.sortedSetScores(set, hoursArr, function(err, counts) {
+ db.sortedSetScores(set, hoursArr, function (err, counts) {
if (err) {
return callback(err);
}
- hoursArr.forEach(function(term, index) {
+ hoursArr.forEach(function (term, index) {
terms[term] = parseInt(counts[index], 10) || 0;
});
var termsArr = [];
hoursArr.reverse();
- hoursArr.forEach(function(hour) {
+ hoursArr.forEach(function (hour) {
termsArr.push(terms[hour]);
});
@@ -131,36 +131,36 @@ var db = require('./database');
});
};
- Analytics.getDailyStatsForSet = function(set, day, numDays, callback) {
+ Analytics.getDailyStatsForSet = function (set, day, numDays, callback) {
var daysArr = [];
day = new Date(day);
- day.setDate(day.getDate()+1); // set the date to tomorrow, because getHourlyStatsForSet steps *backwards* 24 hours to sum up the values
+ day.setDate(day.getDate() + 1); // set the date to tomorrow, because getHourlyStatsForSet steps *backwards* 24 hours to sum up the values
day.setHours(0, 0, 0, 0);
- async.whilst(function() {
+ async.whilst(function () {
return numDays--;
- }, function(next) {
- Analytics.getHourlyStatsForSet(set, day.getTime()-(1000*60*60*24*numDays), 24, function(err, day) {
+ }, function (next) {
+ Analytics.getHourlyStatsForSet(set, day.getTime() - (1000 * 60 * 60 * 24 * numDays), 24, function (err, day) {
if (err) {
return next(err);
}
- daysArr.push(day.reduce(function(cur, next) {
- return cur+next;
+ daysArr.push(day.reduce(function (cur, next) {
+ return cur + next;
}));
next();
});
- }, function(err) {
+ }, function (err) {
callback(err, daysArr);
});
};
- Analytics.getUnwrittenPageviews = function() {
+ Analytics.getUnwrittenPageviews = function () {
return pageViews;
};
- Analytics.getMonthlyPageViews = function(callback) {
+ Analytics.getMonthlyPageViews = function (callback) {
var thisMonth = new Date();
var lastMonth = new Date();
thisMonth.setMonth(thisMonth.getMonth(), 1);
@@ -170,7 +170,7 @@ var db = require('./database');
var values = [thisMonth.getTime(), lastMonth.getTime()];
- db.sortedSetScores('analytics:pageviews:month', values, function(err, scores) {
+ db.sortedSetScores('analytics:pageviews:month', values, function (err, scores) {
if (err) {
return callback(err);
}
@@ -178,7 +178,7 @@ var db = require('./database');
});
};
- Analytics.getCategoryAnalytics = function(cid, callback) {
+ Analytics.getCategoryAnalytics = function (cid, callback) {
async.parallel({
'pageviews:hourly': async.apply(Analytics.getHourlyStatsForSet, 'analytics:pageviews:byCid:' + cid, Date.now(), 24),
'pageviews:daily': async.apply(Analytics.getDailyStatsForSet, 'analytics:pageviews:byCid:' + cid, Date.now(), 30),
@@ -187,4 +187,11 @@ var db = require('./database');
}, callback);
};
+ Analytics.getErrorAnalytics = function (callback) {
+ async.parallel({
+ 'not-found': async.apply(Analytics.getDailyStatsForSet, 'analytics:errors:404', Date.now(), 7),
+ 'toobusy': async.apply(Analytics.getDailyStatsForSet, 'analytics:errors:503', Date.now(), 7)
+ }, callback);
+ };
+
}(exports));
\ No newline at end of file
diff --git a/src/batch.js b/src/batch.js
index 1a425e1a21..ca0944b80d 100644
--- a/src/batch.js
+++ b/src/batch.js
@@ -6,17 +6,17 @@ var async = require('async'),
db = require('./database'),
utils = require('../public/src/utils');
-(function(Batch) {
+(function (Batch) {
var DEFAULT_BATCH_SIZE = 100;
- Batch.processSortedSet = function(setKey, process, options, callback) {
+ Batch.processSortedSet = function (setKey, process, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
- callback = typeof callback === 'function' ? callback : function(){};
+ callback = typeof callback === 'function' ? callback : function () {};
options = options || {};
if (typeof process !== 'function') {
@@ -29,7 +29,7 @@ var async = require('async'),
}
// custom done condition
- options.doneIf = typeof options.doneIf === 'function' ? options.doneIf : function(){};
+ options.doneIf = typeof options.doneIf === 'function' ? options.doneIf : function (){};
var batch = options.batch || DEFAULT_BATCH_SIZE;
var start = 0;
@@ -37,11 +37,11 @@ var async = require('async'),
var done = false;
async.whilst(
- function() {
+ function () {
return !done;
},
- function(next) {
- db.getSortedSetRange(setKey, start, stop, function(err, ids) {
+ function (next) {
+ db.getSortedSetRange(setKey, start, stop, function (err, ids) {
if (err) {
return next(err);
}
@@ -49,7 +49,7 @@ var async = require('async'),
done = true;
return next();
}
- process(ids, function(err) {
+ process(ids, function (err) {
if (err) {
return next(err);
}
@@ -63,4 +63,52 @@ var async = require('async'),
);
};
+ Batch.processArray = function (array, process, options, callback) {
+ if (typeof options === 'function') {
+ callback = options;
+ options = {};
+ }
+
+ callback = typeof callback === 'function' ? callback : function () {};
+ options = options || {};
+
+ if (!Array.isArray(array) || !array.length) {
+ return callback();
+ }
+ if (typeof process !== 'function') {
+ return callback(new Error('[[error:process-not-a-function]]'));
+ }
+
+ var batch = options.batch || DEFAULT_BATCH_SIZE;
+ var start = 0;
+ var done = false;
+
+ async.whilst(
+ function () {
+ return !done;
+ },
+ function (next) {
+ var currentBatch = array.slice(start, start + batch);
+ if (!currentBatch.length) {
+ done = true;
+ return next();
+ }
+ process(currentBatch, function (err) {
+ if (err) {
+ return next(err);
+ }
+ start = start + batch;
+ if (options.interval) {
+ setTimeout(next, options.interval);
+ } else {
+ next();
+ }
+ });
+ },
+ function (err) {
+ callback(err);
+ }
+ );
+ };
+
}(exports));
diff --git a/src/categories.js b/src/categories.js
index cd2b18a787..4e2c435f8e 100644
--- a/src/categories.js
+++ b/src/categories.js
@@ -9,7 +9,7 @@ var Groups = require('./groups');
var plugins = require('./plugins');
var privileges = require('./privileges');
-(function(Categories) {
+(function (Categories) {
require('./categories/data')(Categories);
require('./categories/create')(Categories);
@@ -20,11 +20,11 @@ var privileges = require('./privileges');
require('./categories/recentreplies')(Categories);
require('./categories/update')(Categories);
- Categories.exists = function(cid, callback) {
+ Categories.exists = function (cid, callback) {
db.isSortedSetMember('categories:cid', cid, callback);
};
- Categories.getCategoryById = function(data, callback) {
+ Categories.getCategoryById = function (data, callback) {
var category;
async.waterfall([
function (next) {
@@ -35,15 +35,19 @@ 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) {
+ topics: function (next) {
Categories.getCategoryTopics(data, next);
},
- isIgnored: function(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);
}
}, 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);
},
@@ -61,24 +66,24 @@ var privileges = require('./privileges');
], callback);
};
- Categories.isIgnored = function(cids, uid, callback) {
- user.getIgnoredCategories(uid, function(err, ignoredCids) {
+ Categories.isIgnored = function (cids, uid, callback) {
+ user.getIgnoredCategories(uid, function (err, ignoredCids) {
if (err) {
return callback(err);
}
- cids = cids.map(function(cid) {
+ cids = cids.map(function (cid) {
return ignoredCids.indexOf(cid.toString()) !== -1;
});
callback(null, cids);
});
};
- Categories.getPageCount = function(cid, uid, callback) {
+ Categories.getPageCount = function (cid, uid, callback) {
async.parallel({
topicCount: async.apply(Categories.getCategoryField, cid, 'topic_count'),
settings: async.apply(user.getSettings, uid)
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return callback(err);
}
@@ -91,8 +96,8 @@ var privileges = require('./privileges');
});
};
- Categories.getAllCategories = function(uid, callback) {
- db.getSortedSetRange('categories:cid', 0, -1, function(err, cids) {
+ Categories.getAllCategories = function (uid, callback) {
+ db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) {
if (err || !Array.isArray(cids) || !cids.length) {
return callback(err, []);
}
@@ -101,22 +106,22 @@ var privileges = require('./privileges');
});
};
- Categories.getCategoriesByPrivilege = function(set, uid, privilege, callback) {
+ Categories.getCategoriesByPrivilege = function (set, uid, privilege, callback) {
async.waterfall([
- function(next) {
+ function (next) {
db.getSortedSetRange(set, 0, -1, next);
},
- function(cids, next) {
+ function (cids, next) {
privileges.categories.filterCids(privilege, cids, uid, next);
},
- function(cids, next) {
+ function (cids, next) {
Categories.getCategories(cids, uid, next);
}
], callback);
};
- Categories.getModerators = function(cid, callback) {
- Groups.getMembers('cid:' + cid + ':privileges:mods', 0, -1, function(err, uids) {
+ Categories.getModerators = function (cid, callback) {
+ Groups.getMembers('cid:' + cid + ':privileges:mods', 0, -1, function (err, uids) {
if (err || !Array.isArray(uids) || !uids.length) {
return callback(err, []);
}
@@ -126,7 +131,7 @@ var privileges = require('./privileges');
};
- Categories.getCategories = function(cids, uid, callback) {
+ Categories.getCategories = function (cids, uid, callback) {
if (!Array.isArray(cids)) {
return callback(new Error('[[error:invalid-cid]]'));
}
@@ -136,19 +141,19 @@ var privileges = require('./privileges');
}
async.parallel({
- categories: function(next) {
+ categories: function (next) {
Categories.getCategoriesData(cids, next);
},
- children: function(next) {
+ children: function (next) {
Categories.getChildren(cids, uid, next);
},
- parents: function(next) {
+ parents: function (next) {
Categories.getParents(cids, next);
},
- hasRead: function(next) {
+ hasRead: function (next) {
Categories.hasReadCategories(cids, uid, next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return callback(err);
}
@@ -156,7 +161,7 @@ var privileges = require('./privileges');
var categories = results.categories;
var hasRead = results.hasRead;
uid = parseInt(uid, 10);
- for(var i=0; i 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/edit.js b/src/controllers/accounts/edit.js
index 09e824ecd9..3fe2c57092 100644
--- a/src/controllers/accounts/edit.js
+++ b/src/controllers/accounts/edit.js
@@ -10,12 +10,13 @@ var user = require('../../user');
var meta = require('../../meta');
var plugins = require('../../plugins');
var helpers = require('../helpers');
+var groups = require('../../groups');
var accountHelpers = require('./helpers');
var editController = {};
-editController.get = function(req, res, callback) {
- accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, function(err, userData) {
+editController.get = function (req, res, callback) {
+ accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, function (err, userData) {
if (err || !userData) {
return callback(err);
}
@@ -25,15 +26,21 @@ editController.get = function(req, res, callback) {
userData.maximumProfileImageSize = parseInt(meta.config.maximumProfileImageSize, 10);
userData.allowProfileImageUploads = parseInt(meta.config.allowProfileImageUploads) === 1;
userData.allowAccountDelete = parseInt(meta.config.allowAccountDelete, 10) === 1;
-
+
+ userData.groups = userData.groups.filter(function (group) {
+ return group && group.userTitleEnabled && !groups.isPrivilegeGroup(group.name) && group.name !== 'registered-users';
+ });
+ userData.groups.forEach(function (group) {
+ group.selected = group.name === userData.groupTitle;
+ });
userData.title = '[[pages:account/edit, ' + userData.username + ']]';
userData.breadcrumbs = helpers.buildBreadcrumbs([{text: userData.username, url: '/user/' + userData.userslug}, {text: '[[user:edit]]'}]);
userData.editButtons = [];
- plugins.fireHook('filter:user.account.edit', userData, function(err, userData) {
+ plugins.fireHook('filter:user.account.edit', userData, function (err, userData) {
if (err) {
- return next(err);
+ return callback(err);
}
res.render('account/edit', userData);
@@ -41,20 +48,20 @@ editController.get = function(req, res, callback) {
});
};
-editController.password = function(req, res, next) {
+editController.password = function (req, res, next) {
renderRoute('password', req, res, next);
};
-editController.username = function(req, res, next) {
+editController.username = function (req, res, next) {
renderRoute('username', req, res, next);
};
-editController.email = function(req, res, next) {
+editController.email = function (req, res, next) {
renderRoute('email', req, res, next);
};
function renderRoute(name, req, res, next) {
- getUserData(req, next, function(err, userData) {
+ getUserData(req, next, function (err, userData) {
if (err) {
return next(err);
}
@@ -80,17 +87,17 @@ function renderRoute(name, req, res, next) {
function getUserData(req, next, callback) {
var userData;
async.waterfall([
- function(next) {
+ function (next) {
accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next);
},
- function(data, next) {
+ function (data, next) {
userData = data;
if (!userData) {
return next();
}
db.getObjectField('user:' + userData.uid, 'password', next);
}
- ], function(err, password) {
+ ], function (err, password) {
if (err) {
return callback(err);
}
@@ -106,10 +113,10 @@ editController.uploadPicture = function (req, res, next) {
var updateUid;
async.waterfall([
- function(next) {
+ function (next) {
user.getUidByUserslug(req.params.userslug, next);
},
- function(uid, next) {
+ function (uid, next) {
updateUid = uid;
if (parseInt(req.uid, 10) === parseInt(uid, 10)) {
return next(null, true);
@@ -117,16 +124,18 @@ editController.uploadPicture = function (req, res, next) {
user.isAdminOrGlobalMod(req.uid, next);
},
- function(isAllowed, next) {
+ function (isAllowed, next) {
if (!isAllowed) {
return helpers.notAllowed(req, res);
}
-
+
user.uploadPicture(updateUid, userPhoto, next);
}
- ], function(err, image) {
- fs.unlink(userPhoto.path, function(err) {
- winston.error('unable to delete picture ' + userPhoto.path, err);
+ ], function (err, image) {
+ fs.unlink(userPhoto.path, function (err) {
+ if (err) {
+ winston.warn('[user/picture] Unable to delete picture ' + userPhoto.path, err);
+ }
});
if (err) {
return next(err);
@@ -136,13 +145,13 @@ editController.uploadPicture = function (req, res, next) {
});
};
-editController.uploadCoverPicture = function(req, res, next) {
+editController.uploadCoverPicture = function (req, res, next) {
var params = JSON.parse(req.body.params);
user.updateCoverPicture({
file: req.files.files[0],
uid: params.uid
- }, function(err, image) {
+ }, function (err, image) {
if (err) {
return next(err);
}
diff --git a/src/controllers/accounts/follow.js b/src/controllers/accounts/follow.js
index f9dc72c6f3..7d1e91b5e2 100644
--- a/src/controllers/accounts/follow.js
+++ b/src/controllers/accounts/follow.js
@@ -1,44 +1,52 @@
'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 = {};
-followController.getFollowing = function(req, res, next) {
+followController.getFollowing = function (req, res, next) {
getFollow('account/following', 'following', req, res, next);
};
-followController.getFollowers = function(req, res, next) {
+followController.getFollowers = function (req, res, next) {
getFollow('account/followers', 'followers', 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);
+ function (next) {
+ accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next);
},
- function(data, next) {
+ function (data, next) {
userData = data;
if (!userData) {
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) {
+ ], function (err, users) {
if (err) {
return callback(err);
}
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..038d63de1d 100644
--- a/src/controllers/accounts/groups.js
+++ b/src/controllers/accounts/groups.js
@@ -1,21 +1,21 @@
'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 = {};
-groupsController.get = function(req, res, callback) {
+groupsController.get = function (req, res, callback) {
var userData;
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;
@@ -27,19 +27,19 @@ groupsController.get = function(req, res, callback) {
},
function (_groupsData, next) {
groupsData = _groupsData[0];
- var groupNames = groupsData.filter(Boolean).map(function(group) {
+ var groupNames = groupsData.filter(Boolean).map(function (group) {
return group.name;
});
groups.getMemberUsers(groupNames, 0, 3, next);
},
function (members, next) {
- groupsData.forEach(function(group, index) {
+ groupsData.forEach(function (group, index) {
group.members = members[index];
});
next();
}
- ], function(err) {
+ ], function (err) {
if (err) {
return callback(err);
}
diff --git a/src/controllers/accounts/helpers.js b/src/controllers/accounts/helpers.js
index 8acbdc08c8..1046529442 100644
--- a/src/controllers/accounts/helpers.js
+++ b/src/controllers/accounts/helpers.js
@@ -3,16 +3,17 @@
var async = require('async');
var validator = require('validator');
+var winston = require('winston');
var user = require('../../user');
var groups = require('../../groups');
-var plugins =require('../../plugins');
+var plugins = require('../../plugins');
var meta = require('../../meta');
var utils = require('../../../public/src/utils');
var helpers = {};
-helpers.getUserDataByUserSlug = function(userslug, callerUID, callback) {
+helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) {
async.waterfall([
function (next) {
user.getUidByUserslug(userslug, next);
@@ -23,28 +24,34 @@ helpers.getUserDataByUserSlug = function(userslug, callerUID, callback) {
}
async.parallel({
- userData : function(next) {
+ userData : function (next) {
user.getUserData(uid, next);
},
- userSettings : function(next) {
+ userSettings : function (next) {
user.getSettings(uid, next);
},
- isAdmin : function(next) {
+ isAdmin : function (next) {
user.isAdministrator(callerUID, next);
},
- isGlobalModerator: function(next) {
+ isGlobalModerator: function (next) {
user.isGlobalModerator(callerUID, next);
},
- ips: function(next) {
+ isFollowing: function (next) {
+ user.isFollowing(callerUID, uid, next);
+ },
+ ips: function (next) {
user.getIPs(uid, 4, next);
},
- profile_links: function(next) {
+ profile_links: function (next) {
plugins.fireHook('filter:user.profileLinks', [], next);
},
- groups: function(next) {
+ profile_menu: function (next) {
+ plugins.fireHook('filter:user.profileMenu', {uid: uid, callerUID: callerUID, links: []}, next);
+ },
+ groups: function (next) {
groups.getUserGroups([uid], next);
},
- sso: function(next) {
+ sso: function (next) {
plugins.fireHook('filter:auth.list', {uid: uid, associations: []}, next);
}
}, next);
@@ -80,35 +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.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%';
@@ -121,55 +137,13 @@ 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'], next);
- },
- isAdmin: function(next) {
- user.isAdministrator(callerUID, next);
- },
- isGlobalModerator: function(next) {
- user.isGlobalModerator(callerUID, 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.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);
+helpers.getBaseUser = function (userslug, callerUID, callback) {
+ winston.warn('helpers.getBaseUser deprecated please use helpers.getUserDataByUserSlug');
+ helpers.getUserDataByUserSlug(userslug, callerUID, callback);
};
function filterLinks(links, self) {
- return links.filter(function(link) {
+ return links.filter(function (link) {
return link && (link.public || self);
});
}
diff --git a/src/controllers/accounts/info.js b/src/controllers/accounts/info.js
new file mode 100644
index 0000000000..77ab2f275e
--- /dev/null
+++ b/src/controllers/accounts/info.js
@@ -0,0 +1,45 @@
+'use strict';
+
+var async = require('async');
+
+var user = require('../../user');
+var helpers = require('../helpers');
+var accountHelpers = require('./helpers');
+
+var infoController = {};
+
+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.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', userData);
+ });
+};
+
+module.exports = infoController;
\ No newline at end of file
diff --git a/src/controllers/accounts/notifications.js b/src/controllers/accounts/notifications.js
index aa60892f47..91b9eb1d07 100644
--- a/src/controllers/accounts/notifications.js
+++ b/src/controllers/accounts/notifications.js
@@ -6,8 +6,8 @@ var user = require('../../user'),
var notificationsController = {};
-notificationsController.get = function(req, res, next) {
- user.notifications.getAll(req.uid, 0, 39, function(err, notifications) {
+notificationsController.get = function (req, res, next) {
+ user.notifications.getAll(req.uid, 0, 39, function (err, notifications) {
if (err) {
return next(err);
}
diff --git a/src/controllers/accounts/posts.js b/src/controllers/accounts/posts.js
index 7e1e67a8bd..53ff073dad 100644
--- a/src/controllers/accounts/posts.js
+++ b/src/controllers/accounts/posts.js
@@ -1,31 +1,31 @@
'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 = {};
-postsController.getFavourites = function(req, res, next) {
+postsController.getBookmarks = function (req, res, next) {
var data = {
- template: 'account/favourites',
- set: 'favourites',
+ template: 'account/bookmarks',
+ set: 'bookmarks',
type: 'posts',
- noItemsFoundKey: '[[topic:favourites.has_no_favourites]]',
+ noItemsFoundKey: '[[topic:bookmarks.has_no_bookmarks]]',
method: posts.getPostSummariesFromSet,
- crumb: '[[user:favourites]]'
+ crumb: '[[user:bookmarks]]'
};
getFromUserSet(data, req, res, next);
};
-postsController.getPosts = function(req, res, next) {
+postsController.getPosts = function (req, res, next) {
var data = {
template: 'account/posts',
set: 'posts',
@@ -37,7 +37,7 @@ postsController.getPosts = function(req, res, next) {
getFromUserSet(data, req, res, next);
};
-postsController.getUpVotedPosts = function(req, res, next) {
+postsController.getUpVotedPosts = function (req, res, next) {
var data = {
template: 'account/upvoted',
set: 'upvote',
@@ -49,7 +49,7 @@ postsController.getUpVotedPosts = function(req, res, next) {
getFromUserSet(data, req, res, next);
};
-postsController.getDownVotedPosts = function(req, res, next) {
+postsController.getDownVotedPosts = function (req, res, next) {
var data = {
template: 'account/downvoted',
set: 'downvote',
@@ -61,7 +61,7 @@ postsController.getDownVotedPosts = function(req, res, next) {
getFromUserSet(data, req, res, next);
};
-postsController.getBestPosts = function(req, res, next) {
+postsController.getBestPosts = function (req, res, next) {
var data = {
template: 'account/best',
set: 'posts:votes',
@@ -73,7 +73,7 @@ postsController.getBestPosts = function(req, res, next) {
getFromUserSet(data, req, res, next);
};
-postsController.getWatchedTopics = function(req, res, next) {
+postsController.getWatchedTopics = function (req, res, next) {
var data = {
template: 'account/watched',
set: 'followed_tids',
@@ -85,7 +85,7 @@ postsController.getWatchedTopics = function(req, res, next) {
getFromUserSet(data, req, res, next);
};
-postsController.getTopics = function(req, res, next) {
+postsController.getTopics = function (req, res, next) {
var data = {
template: 'account/topics',
set: 'topics',
@@ -99,13 +99,13 @@ postsController.getTopics = function(req, res, next) {
function getFromUserSet(data, req, res, next) {
async.parallel({
- settings: function(next) {
+ settings: function (next) {
user.getSettings(req.uid, next);
},
- userData: function(next) {
- accountHelpers.getBaseUser(req.params.userslug, req.uid, next);
+ userData: function (next) {
+ accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err || !results.userData) {
return next(err);
}
@@ -118,19 +118,19 @@ function getFromUserSet(data, req, res, next) {
var itemsPerPage = (data.template === 'account/topics' || data.template === 'account/watched') ? results.settings.topicsPerPage : results.settings.postsPerPage;
async.parallel({
- itemCount: function(next) {
+ itemCount: function (next) {
if (results.settings.usePagination) {
db.sortedSetCard(setName, next);
} else {
next(null, 0);
}
},
- data: function(next) {
+ data: function (next) {
var start = (page - 1) * itemsPerPage;
var stop = start + itemsPerPage - 1;
data.method(setName, req.uid, start, stop, next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return next(err);
}
diff --git a/src/controllers/accounts/profile.js b/src/controllers/accounts/profile.js
index e52b5f4861..f0fa1e1378 100644
--- a/src/controllers/accounts/profile.js
+++ b/src/controllers/accounts/profile.js
@@ -14,7 +14,7 @@ var helpers = require('../helpers');
var profileController = {};
-profileController.get = function(req, res, callback) {
+profileController.get = function (req, res, callback) {
var lowercaseSlug = req.params.userslug.toLowerCase();
if (req.params.userslug !== lowercaseSlug) {
@@ -43,18 +43,18 @@ profileController.get = function(req, res, callback) {
}
async.parallel({
- isFollowing: function(next) {
+ isFollowing: function (next) {
user.isFollowing(req.uid, userData.theirid, next);
},
- posts: function(next) {
+ posts: function (next) {
posts.getPostSummariesFromSet('uid:' + userData.theirid + ':posts', req.uid, 0, 9, next);
},
- signature: function(next) {
+ signature: function (next) {
posts.parseSignature(userData, req.uid, next);
},
- aboutme: function(next) {
+ aboutme: function (next) {
if (userData.aboutme) {
- plugins.fireHook('filter:parse.raw', userData.aboutme, next);
+ plugins.fireHook('filter:parse.aboutme', userData.aboutme, next);
} else {
next();
}
@@ -118,10 +118,13 @@ 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);
}
- ], function(err, results) {
+ ], function (err, results) {
if (err) {
return callback(err);
}
diff --git a/src/controllers/accounts/session.js b/src/controllers/accounts/session.js
index 8fdb180ded..7e31906f26 100644
--- a/src/controllers/accounts/session.js
+++ b/src/controllers/accounts/session.js
@@ -7,20 +7,27 @@ var user = require('../../user');
var sessionController = {};
-sessionController.revoke = function(req, res, next) {
+sessionController.revoke = function (req, res, next) {
if (!req.params.hasOwnProperty('uuid')) {
return next();
}
var _id;
-
+ var uid;
async.waterfall([
function (next) {
- db.getSortedSetRange('uid:' + req.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) {
- db.sessionStore.get(sid, function(err, sessionObj) {
+ async.eachSeries(sids, function (sid, next) {
+ db.sessionStore.get(sid, function (err, sessionObj) {
if (err) {
return next(err);
}
@@ -38,9 +45,9 @@ sessionController.revoke = function(req, res, next) {
return next(new Error('[[error:no-session-found]]'));
}
- user.auth.revokeSession(_id, req.uid, next);
+ user.auth.revokeSession(_id, uid, next);
}
- ], function(err) {
+ ], function (err) {
if (err) {
return res.status(500).send(err.message);
} else {
diff --git a/src/controllers/accounts/settings.js b/src/controllers/accounts/settings.js
index b5b020c118..44499e7e68 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');
@@ -17,53 +16,60 @@ var accountHelpers = require('./helpers');
var settingsController = {};
-settingsController.get = function(req, res, callback) {
+settingsController.get = function (req, res, callback) {
var userData;
async.waterfall([
- function(next) {
- accountHelpers.getBaseUser(req.params.userslug, req.uid, next);
+ function (next) {
+ accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next);
},
- function(_userData, next) {
+ function (_userData, next) {
userData = _userData;
if (!userData) {
return callback();
}
async.parallel({
- settings: function(next) {
+ settings: function (next) {
user.getSettings(userData.uid, next);
},
- userGroups: function(next) {
- groups.getUserGroupsFromSet('groups:createtime', [userData.uid], next);
- },
- languages: function(next) {
+ languages: function (next) {
languages.list(next);
},
- homePageRoutes: function(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) {
+ 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) {
+ function (data, next) {
userData.customSettings = data.customSettings;
userData.disableEmailSubscriptions = parseInt(meta.config.disableEmailSubscriptions, 10) === 1;
next();
}
- ], function(err) {
+ ], function (err) {
if (err) {
return callback(err);
}
@@ -96,19 +102,29 @@ settingsController.get = function(req, res, callback) {
{ "name": "Yeti", "value": "yeti" }
];
- userData.homePageRoutes.forEach(function(route) {
+ var isCustom = true;
+ userData.homePageRoutes.forEach(function (route) {
route.selected = route.route === userData.settings.homePageRoute;
+ if (route.selected) {
+ isCustom = false;
+ }
});
- userData.bootswatchSkinOptions.forEach(function(skin) {
+ if (isCustom && userData.settings.homePageRoute === 'none') {
+ isCustom = false;
+ }
+
+ userData.homePageRoutes.push({
+ route: 'custom',
+ name: 'Custom',
+ selected: isCustom
+ });
+
+ userData.bootswatchSkinOptions.forEach(function (skin) {
skin.selected = skin.value === userData.settings.bootswatchSkin;
});
- userData.userGroups.forEach(function(group) {
- group.selected = group.name === userData.settings.groupTitle;
- });
-
- userData.languages.forEach(function(language) {
+ userData.languages.forEach(function (language) {
language.selected = language.code === userData.settings.userLang;
});
@@ -116,6 +132,8 @@ settingsController.get = function(req, res, callback) {
userData.allowUserHomePage = parseInt(meta.config.allowUserHomePage, 10) === 1;
+ userData.inTopicSearchAvailable = plugins.hasListeners('filter:topic.search');
+
userData.title = '[[pages:account/settings]]';
userData.breadcrumbs = helpers.buildBreadcrumbs([{text: userData.username, url: '/user/' + userData.userslug}, {text: '[[user:settings]]'}]);
@@ -136,46 +154,38 @@ function getHomePageRoutes(callback) {
categories.getCategoriesFields(cids, ['name', 'slug'], next);
},
function (categoryData, next) {
- categoryData = categoryData.map(function(category) {
+ categoryData = categoryData.map(function (category) {
return {
route: 'category/' + category.slug,
name: 'Category: ' + category.name
};
});
- next(null, categoryData);
+
+ categoryData = categoryData || [];
+
+ plugins.fireHook('filter:homepage.get', {routes: [
+ {
+ route: 'categories',
+ name: 'Categories'
+ },
+ {
+ route: 'unread',
+ name: 'Unread'
+ },
+ {
+ route: 'recent',
+ name: 'Recent'
+ },
+ {
+ route: 'popular',
+ name: 'Popular'
+ }
+ ].concat(categoryData)}, next);
+ },
+ function (data, next) {
+ next(null, data.routes);
}
- ], function(err, categoryData) {
- if (err) {
- return callback(err);
- }
- categoryData = categoryData || [];
-
- plugins.fireHook('filter:homepage.get', {routes: [
- {
- route: 'categories',
- name: 'Categories'
- },
- {
- route: 'recent',
- name: 'Recent'
- },
- {
- route: 'popular',
- name: 'Popular'
- }
- ].concat(categoryData)}, function(err, data) {
- if (err) {
- return callback(err);
- }
-
- data.routes.push({
- route: 'custom',
- name: 'Custom'
- });
-
- callback(null, data.routes);
- });
- });
+ ], callback);
}
diff --git a/src/controllers/admin.js b/src/controllers/admin.js
index 2bba60cae6..7f622466cd 100644
--- a/src/controllers/admin.js
+++ b/src/controllers/admin.js
@@ -14,8 +14,9 @@ var adminController = {
},
events: require('./admin/events'),
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/appearance.js b/src/controllers/admin/appearance.js
index 8d60efda23..8956bd175d 100644
--- a/src/controllers/admin/appearance.js
+++ b/src/controllers/admin/appearance.js
@@ -2,7 +2,7 @@
var appearanceController = {};
-appearanceController.get = function(req, res, next) {
+appearanceController.get = function (req, res, next) {
var term = req.params.term ? req.params.term : 'themes';
res.render('admin/appearance/' + term, {});
diff --git a/src/controllers/admin/blacklist.js b/src/controllers/admin/blacklist.js
index 2c0104f742..d70b1d1d79 100644
--- a/src/controllers/admin/blacklist.js
+++ b/src/controllers/admin/blacklist.js
@@ -4,8 +4,8 @@ var meta = require('../../meta');
var blacklistController = {};
-blacklistController.get = function(req, res, next) {
- meta.blacklist.get(function(err, rules) {
+blacklistController.get = function (req, res, next) {
+ meta.blacklist.get(function (err, rules) {
if (err) {
return next(err);
}
diff --git a/src/controllers/admin/cache.js b/src/controllers/admin/cache.js
new file mode 100644
index 0000000000..21ef6ff086
--- /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 5444a087ea..8a59bb0b4e 100644
--- a/src/controllers/admin/categories.js
+++ b/src/controllers/admin/categories.js
@@ -6,25 +6,30 @@ var categories = require('../../categories');
var privileges = require('../../privileges');
var analytics = require('../../analytics');
var plugins = require('../../plugins');
-var translator = require('../../../public/src/modules/translator')
+var translator = require('../../../public/src/modules/translator');
var categoriesController = {};
-categoriesController.get = function(req, res, next) {
+categoriesController.get = function (req, res, next) {
async.parallel({
category: async.apply(categories.getCategories, [req.params.category_id], req.user.uid),
privileges: async.apply(privileges.categories.list, req.params.category_id)
- }, function(err, data) {
+ }, function (err, data) {
if (err) {
return next(err);
}
+ var category = data.category[0];
- plugins.fireHook('filter:admin.category.get', { req: req, res: res, category: data.category[0], privileges: data.privileges }, function(err, data) {
+ if (!category) {
+ return next();
+ }
+
+ plugins.fireHook('filter:admin.category.get', { req: req, res: res, category: category, privileges: data.privileges }, function (err, data) {
if (err) {
return next(err);
}
- data.category.name = translator.escape(data.category.name);
+ data.category.name = translator.escape(String(data.category.name));
res.render('admin/manage/category', {
category: data.category,
privileges: data.privileges
@@ -33,16 +38,20 @@ categoriesController.get = function(req, res, next) {
});
};
-categoriesController.getAll = function(req, res, next) {
+categoriesController.getAll = function (req, res, next) {
// Categories list will be rendered on client side with recursion, etc.
res.render('admin/manage/categories', {});
};
-categoriesController.getAnalytics = function(req, res, next) {
+categoriesController.getAnalytics = function (req, res, next) {
async.parallel({
name: async.apply(categories.getCategoryField, req.params.category_id, 'name'),
analytics: async.apply(analytics.getCategoryAnalytics, req.params.category_id)
- }, function(err, data) {
+ }, 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..22105b6652 100644
--- a/src/controllers/admin/dashboard.js
+++ b/src/controllers/admin/dashboard.js
@@ -10,17 +10,17 @@ var plugins = require('../../plugins');
var dashboardController = {};
-dashboardController.get = function(req, res, next) {
+dashboardController.get = function (req, res, next) {
async.parallel({
- stats: function(next) {
+ stats: function (next) {
getStats(next);
},
- notices: function(next) {
+ notices: function (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'),
@@ -32,7 +32,7 @@ dashboardController.get = function(req, res, next) {
];
plugins.fireHook('filter:admin.notices', notices, next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return next(err);
}
@@ -46,19 +46,19 @@ dashboardController.get = function(req, res, next) {
function getStats(callback) {
async.parallel([
- function(next) {
+ function (next) {
getStatsForSet('ip:recent', 'uniqueIPCount', next);
},
- function(next) {
+ function (next) {
getStatsForSet('users:joindate', 'userCount', next);
},
- function(next) {
+ function (next) {
getStatsForSet('posts:pid', 'postCount', next);
},
- function(next) {
+ function (next) {
getStatsForSet('topics:tid', 'topicCount', next);
}
- ], function(err, results) {
+ ], function (err, results) {
if (err) {
return callback(err);
}
@@ -80,23 +80,23 @@ function getStatsForSet(set, field, callback) {
var now = Date.now();
async.parallel({
- day: function(next) {
+ day: function (next) {
db.sortedSetCount(set, now - terms.day, '+inf', next);
},
- week: function(next) {
+ week: function (next) {
db.sortedSetCount(set, now - terms.week, '+inf', next);
},
- month: function(next) {
+ month: function (next) {
db.sortedSetCount(set, now - terms.month, '+inf', next);
},
- alltime: function(next) {
+ alltime: function (next) {
getGlobalField(field, next);
}
}, callback);
}
function getGlobalField(field, callback) {
- db.getObjectField('global', field, function(err, count) {
+ db.getObjectField('global', field, function (err, count) {
callback(err, parseInt(count, 10) || 0);
});
}
diff --git a/src/controllers/admin/database.js b/src/controllers/admin/database.js
index d15db3f09c..5a28b95ec4 100644
--- a/src/controllers/admin/database.js
+++ b/src/controllers/admin/database.js
@@ -7,18 +7,17 @@ var databaseController = {};
-databaseController.get = function(req, res, next) {
+databaseController.get = function (req, res, next) {
async.parallel({
- redis: function(next) {
+ redis: function (next) {
if (nconf.get('redis')) {
var rdb = require('../../database/redis');
- var cxn = rdb.connect();
- rdb.info(cxn, next);
+ rdb.info(rdb.client, next);
} else {
next();
}
},
- mongo: function(next) {
+ mongo: function (next) {
if (nconf.get('mongo')) {
var mdb = require('../../database/mongo');
mdb.info(mdb.client, next);
@@ -26,7 +25,7 @@ databaseController.get = function(req, res, next) {
next();
}
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return next(err);
}
diff --git a/src/controllers/admin/errors.js b/src/controllers/admin/errors.js
new file mode 100644
index 0000000000..4cacd425c9
--- /dev/null
+++ b/src/controllers/admin/errors.js
@@ -0,0 +1,38 @@
+'use strict';
+
+var async = require('async');
+var json2csv = require('json-2-csv').json2csv;
+
+var meta = require('../../meta');
+var analytics = require('../../analytics');
+
+var errorsController = {};
+
+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, 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);
+ });
+};
+
+
+module.exports = errorsController;
\ No newline at end of file
diff --git a/src/controllers/admin/events.js b/src/controllers/admin/events.js
index ceee1e2a70..8a4d63bad1 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) {
+eventsController.get = function (req, res, next) {
+
+ 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 cb2f830365..1b31a95ff4 100644
--- a/src/controllers/admin/flags.js
+++ b/src/controllers/admin/flags.js
@@ -1,38 +1,103 @@
"use strict";
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) {
- 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);
- }
- }
- ], function (err, posts) {
+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, res, next);
+ },
+ analytics: function (next) {
+ analytics.getDailyStatsForSet('analytics:flags', Date.now(), 30, next);
+ },
+ assignees: async.apply(user.getAdminsandGlobalModsandModerators)
+ }, 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
+ };
+ });
+
+ // If res.locals.cids is populated, then slim down the categories list
+ if (res.locals.cids) {
+ results.categories = results.categories.filter(function (category) {
+ return res.locals.cids.indexOf(String(category.cid)) !== -1;
+ });
+ }
+
+ 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: posts,
- next: stop + 1,
- byUsername: byUsername,
+ posts: results.flagData.posts,
+ assignees: results.assignees,
+ analytics: results.analytics,
+ 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, res, callback) {
+ var sortBy = req.query.sortBy || 'count';
+ var byUsername = req.query.byUsername || '';
+ var cid = req.query.cid || res.locals.cids || 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/groups.js b/src/controllers/admin/groups.js
index 03b3514327..19aeb4f2f4 100644
--- a/src/controllers/admin/groups.js
+++ b/src/controllers/admin/groups.js
@@ -12,17 +12,17 @@ var async = require('async'),
var groupsController = {};
-groupsController.list = function(req, res, next) {
+groupsController.list = function (req, res, next) {
var page = parseInt(req.query.page, 10) || 1;
var groupsPerPage = 20;
var pageCount = 0;
async.waterfall([
- function(next) {
+ function (next) {
db.getSortedSetRevRange('groups:createtime', 0, -1, next);
},
- function(groupNames, next) {
- groupNames = groupNames.filter(function(name) {
+ function (groupNames, next) {
+ groupNames = groupNames.filter(function (name) {
return name.indexOf(':privileges:') === -1 && name !== 'registered-users';
});
pageCount = Math.ceil(groupNames.length / groupsPerPage);
@@ -33,10 +33,10 @@ groupsController.list = function(req, res, next) {
groupNames = groupNames.slice(start, stop + 1);
groups.getGroupsData(groupNames, next);
},
- function(groupData, next) {
+ function (groupData, next) {
next(null, {groups: groupData, pagination: pagination.create(page, pageCount)});
}
- ], function(err, data) {
+ ], function (err, data) {
if (err) {
return next(err);
}
@@ -49,19 +49,19 @@ groupsController.list = function(req, res, next) {
});
};
-groupsController.get = function(req, res, callback) {
+groupsController.get = function (req, res, callback) {
var groupName = req.params.name;
async.waterfall([
- function(next){
+ function (next){
groups.exists(groupName, next);
},
- function(exists, next) {
+ function (exists, next) {
if (!exists) {
return callback();
}
groups.get(groupName, {uid: req.uid, truncateUserList: true, userListCount: 20}, next);
}
- ], function(err, group) {
+ ], function (err, group) {
if (err) {
return callback(err);
}
diff --git a/src/controllers/admin/homepage.js b/src/controllers/admin/homepage.js
index f503dd8865..9fdf3a2371 100644
--- a/src/controllers/admin/homepage.js
+++ b/src/controllers/admin/homepage.js
@@ -10,19 +10,19 @@ var plugins = require('../../plugins');
var homePageController = {};
-homePageController.get = function(req, res, next) {
+homePageController.get = function (req, res, next) {
async.waterfall([
- function(next) {
+ function (next) {
db.getSortedSetRange('categories:cid', 0, -1, next);
},
- function(cids, next) {
+ function (cids, next) {
privileges.categories.filterCids('find', cids, 0, next);
},
- function(cids, next) {
+ function (cids, next) {
categories.getCategoriesFields(cids, ['name', 'slug'], next);
},
- function(categoryData, next) {
- categoryData = categoryData.map(function(category) {
+ function (categoryData, next) {
+ categoryData = categoryData.map(function (category) {
return {
route: 'category/' + category.slug,
name: 'Category: ' + category.name
@@ -30,7 +30,7 @@ homePageController.get = function(req, res, next) {
});
next(null, categoryData);
}
- ], function(err, categoryData) {
+ ], function (err, categoryData) {
if (err || !categoryData) {
categoryData = [];
}
@@ -48,7 +48,11 @@ homePageController.get = function(req, res, next) {
route: 'popular',
name: 'Popular'
}
- ].concat(categoryData)}, function(err, data) {
+ ].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..6c60d6a048 100644
--- a/src/controllers/admin/info.js
+++ b/src/controllers/admin/info.js
@@ -13,23 +13,23 @@ var infoController = {};
var info = {};
-infoController.get = function(req, res, next) {
+infoController.get = function (req, res, next) {
info = {};
pubsub.publish('sync:node:info:start');
- setTimeout(function() {
+ setTimeout(function () {
var data = [];
- Object.keys(info).forEach(function(key) {
+ Object.keys(info).forEach(function (key) {
data.push(info[key]);
});
- data.sort(function(a, b) {
+ data.sort(function (a, b) {
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() {
- getNodeInfo(function(err, data) {
+pubsub.on('sync:node:info:start', function () {
+ getNodeInfo(function (err, data) {
if (err) {
return winston.error(err);
}
@@ -37,7 +37,7 @@ pubsub.on('sync:node:info:start', function() {
});
});
-pubsub.on('sync:node:info:end', function(data) {
+pubsub.on('sync:node:info:end', function (data) {
info[data.id] = data.data;
});
@@ -57,39 +57,38 @@ function getNodeInfo(callback) {
platform: os.platform(),
arch: os.arch(),
release: os.release(),
- load: os.loadavg().map(function(load){ return load.toFixed(2); }).join(', ')
+ load: os.loadavg().map(function (load){ return load.toFixed(2); }).join(', ')
}
};
async.parallel({
- pubsub: function(next) {
- pubsub.publish('sync:stats:start');
- next();
+ stats: function (next) {
+ rooms.getLocalStats(next);
},
- gitInfo: function(next) {
+ gitInfo: function (next) {
getGitInfo(next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return callback(err);
}
data.git = results.gitInfo;
- data.stats = rooms.stats[data.os.hostname + ':' + data.process.port];
+ data.stats = results.stats;
callback(null, data);
});
}
function getGitInfo(callback) {
function get(cmd, callback) {
- exec(cmd, function(err, stdout) {
+ exec(cmd, function (err, stdout) {
callback(err, stdout ? stdout.replace(/\n$/, '') : '');
});
}
async.parallel({
- hash: function(next) {
+ hash: function (next) {
get('git rev-parse HEAD', next);
},
- branch: function(next) {
+ branch: function (next) {
get('git rev-parse --abbrev-ref HEAD', next);
}
}, callback);
diff --git a/src/controllers/admin/languages.js b/src/controllers/admin/languages.js
index 85c6d60484..292cd2a3b4 100644
--- a/src/controllers/admin/languages.js
+++ b/src/controllers/admin/languages.js
@@ -6,13 +6,13 @@ var meta = require('../../meta');
var languagesController = {};
-languagesController.get = function(req, res, next) {
- languages.list(function(err, languages) {
+languagesController.get = function (req, res, next) {
+ languages.list(function (err, languages) {
if (err) {
return next(err);
}
- languages.forEach(function(language) {
+ languages.forEach(function (language) {
language.selected = language.code === (meta.config.defaultLang || 'en_GB');
});
diff --git a/src/controllers/admin/logger.js b/src/controllers/admin/logger.js
index 45c9f246c9..7ae327a858 100644
--- a/src/controllers/admin/logger.js
+++ b/src/controllers/admin/logger.js
@@ -2,7 +2,7 @@
var loggerController = {};
-loggerController.get = function(req, res) {
+loggerController.get = function (req, res) {
res.render('admin/development/logger', {});
};
diff --git a/src/controllers/admin/logs.js b/src/controllers/admin/logs.js
index f3ae601dd4..6723d3795f 100644
--- a/src/controllers/admin/logs.js
+++ b/src/controllers/admin/logs.js
@@ -6,8 +6,8 @@ var meta = require('../../meta');
var logsController = {};
-logsController.get = function(req, res, next) {
- meta.logs.get(function(err, logs) {
+logsController.get = function (req, res, next) {
+ meta.logs.get(function (err, logs) {
if (err) {
return next(err);
}
diff --git a/src/controllers/admin/navigation.js b/src/controllers/admin/navigation.js
index 463c525eff..423f21721c 100644
--- a/src/controllers/admin/navigation.js
+++ b/src/controllers/admin/navigation.js
@@ -2,14 +2,14 @@
var navigationController = {};
-navigationController.get = function(req, res, next) {
- require('../../navigation/admin').getAdmin(function(err, data) {
+navigationController.get = function (req, res, next) {
+ require('../../navigation/admin').getAdmin(function (err, data) {
if (err) {
return next(err);
}
- data.enabled.forEach(function(enabled, index) {
+ data.enabled.forEach(function (enabled, index) {
enabled.index = index;
enabled.selected = index === 0;
});
diff --git a/src/controllers/admin/plugins.js b/src/controllers/admin/plugins.js
index a4733e4c51..f1a72720ac 100644
--- a/src/controllers/admin/plugins.js
+++ b/src/controllers/admin/plugins.js
@@ -5,10 +5,10 @@ var plugins = require('../../plugins');
var pluginsController = {};
-pluginsController.get = function(req, res, next) {
+pluginsController.get = function (req, res, next) {
async.parallel({
- compatible: function(next) {
- plugins.list(function(err, plugins) {
+ compatible: function (next) {
+ plugins.list(function (err, plugins) {
if (err || !Array.isArray(plugins)) {
plugins = [];
}
@@ -16,8 +16,8 @@ pluginsController.get = function(req, res, next) {
next(null, plugins);
});
},
- all: function(next) {
- plugins.list(false, function(err, plugins) {
+ all: function (next) {
+ plugins.list(false, function (err, plugins) {
if (err || !Array.isArray(plugins)) {
plugins = [];
}
@@ -25,22 +25,28 @@ pluginsController.get = function(req, res, next) {
next(null, plugins);
});
}
- }, function(err, payload) {
+ }, function (err, payload) {
if (err) {
return next(err);
}
- var compatiblePkgNames = payload.compatible.map(function(pkgData) {
+ var compatiblePkgNames = payload.compatible.map(function (pkgData) {
return pkgData.name;
});
res.render('admin/extend/plugins' , {
- installed: payload.compatible.filter(function(plugin) {
+ installed: payload.compatible.filter(function (plugin) {
return plugin.installed;
}),
- download: payload.compatible.filter(function(plugin) {
+ upgradeCount: payload.compatible.reduce(function (count, current) {
+ if (current.installed && current.outdated) {
+ ++count;
+ }
+ return count;
+ }, 0),
+ download: payload.compatible.filter(function (plugin) {
return !plugin.installed;
}),
- incompatible: payload.all.filter(function(plugin) {
+ incompatible: payload.all.filter(function (plugin) {
return compatiblePkgNames.indexOf(plugin.name) === -1;
})
});
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/rewards.js b/src/controllers/admin/rewards.js
index 063abb6807..8ff05c75b3 100644
--- a/src/controllers/admin/rewards.js
+++ b/src/controllers/admin/rewards.js
@@ -2,8 +2,8 @@
var rewardsController = {};
-rewardsController.get = function(req, res, next) {
- require('../../rewards/admin').get(function(err, data) {
+rewardsController.get = function (req, res, next) {
+ require('../../rewards/admin').get(function (err, data) {
if (err) {
return next(err);
}
diff --git a/src/controllers/admin/settings.js b/src/controllers/admin/settings.js
index bf0975058a..a6afb80cfc 100644
--- a/src/controllers/admin/settings.js
+++ b/src/controllers/admin/settings.js
@@ -4,7 +4,7 @@ var settingsController = {};
var async = require('async'),
meta = require('../../meta');
-settingsController.get = function(req, res, next) {
+settingsController.get = function (req, res, next) {
var term = req.params.term ? req.params.term : 'general';
switch (req.params.term) {
@@ -19,29 +19,41 @@ 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) {
- async.map(emails, function(email, next) {
+ 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) {
+ 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,
original: original.toString()
});
});
- }, function(err, emails) {
+ }, function (err, emails) {
+ if (err) {
+ return next(err);
+ }
+
res.render('admin/settings/email', {
emails: emails,
- sendable: emails.filter(function(email) {
+ sendable: emails.filter(function (email) {
return email.path.indexOf('_plaintext') === -1 && email.path.indexOf('partials') === -1;
})
});
diff --git a/src/controllers/admin/social.js b/src/controllers/admin/social.js
index d8f87af060..11c7982701 100644
--- a/src/controllers/admin/social.js
+++ b/src/controllers/admin/social.js
@@ -5,8 +5,8 @@ var social = require('../../social');
var socialController = {};
-socialController.get = function(req, res, next) {
- social.getPostSharing(function(err, posts) {
+socialController.get = function (req, res, next) {
+ social.getPostSharing(function (err, posts) {
if (err) {
return next(err);
}
diff --git a/src/controllers/admin/sounds.js b/src/controllers/admin/sounds.js
index 6e7ebf3f19..801a2067ac 100644
--- a/src/controllers/admin/sounds.js
+++ b/src/controllers/admin/sounds.js
@@ -4,9 +4,13 @@ var meta = require('../../meta');
var soundsController = {};
-soundsController.get = function(req, res, next) {
- meta.sounds.getFiles(function(err, sounds) {
- sounds = Object.keys(sounds).map(function(name) {
+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/tags.js b/src/controllers/admin/tags.js
index 22e3b32d67..a645e2ef11 100644
--- a/src/controllers/admin/tags.js
+++ b/src/controllers/admin/tags.js
@@ -4,8 +4,8 @@ var topics = require('../../topics');
var tagsController = {};
-tagsController.get = function(req, res, next) {
- topics.getTags(0, 199, function(err, tags) {
+tagsController.get = function (req, res, next) {
+ topics.getTags(0, 199, function (err, tags) {
if (err) {
return next(err);
}
diff --git a/src/controllers/admin/themes.js b/src/controllers/admin/themes.js
index e5ef8a9343..4f6f3e1f3b 100644
--- a/src/controllers/admin/themes.js
+++ b/src/controllers/admin/themes.js
@@ -5,9 +5,9 @@ var file = require('../../file');
var themesController = {};
-themesController.get = function(req, res, next) {
+themesController.get = function (req, res, next) {
var themeDir = path.join(__dirname, '../../../node_modules/' + req.params.theme);
- file.exists(themeDir, function(exists) {
+ file.exists(themeDir, function (exists) {
if (!exists) {
return next();
}
diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js
index aea79e3934..d1664cb8f1 100644
--- a/src/controllers/admin/uploads.js
+++ b/src/controllers/admin/uploads.js
@@ -1,26 +1,26 @@
"use strict";
-var fs = require('fs'),
- path = require('path'),
- async = require('async'),
- nconf = require('nconf'),
- winston = require('winston'),
- file = require('../../file'),
- image = require('../../image'),
- plugins = require('../../plugins');
+var fs = require('fs');
+var path = require('path');
+var async = require('async');
+var nconf = require('nconf');
+var winston = require('winston');
+var file = require('../../file');
+var image = require('../../image');
+var plugins = require('../../plugins');
+var allowedImageTypes = ['image/png', 'image/jpeg', 'image/pjpeg', 'image/jpg', 'image/gif', 'image/svg+xml'];
var uploadsController = {};
-uploadsController.uploadCategoryPicture = function(req, res, next) {
+uploadsController.uploadCategoryPicture = function (req, res, next) {
var uploadedFile = req.files.files[0];
- var allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/svg+xml'],
- params = null;
+ var params = null;
try {
params = JSON.parse(req.body.params);
} catch (e) {
- fs.unlink(uploadedFile.path, function(err) {
+ fs.unlink(uploadedFile.path, function (err) {
if (err) {
winston.error(err);
}
@@ -28,19 +28,19 @@ uploadsController.uploadCategoryPicture = function(req, res, next) {
return next(e);
}
- if (validateUpload(req, res, next, uploadedFile, allowedTypes)) {
+ if (validateUpload(req, res, next, uploadedFile, allowedImageTypes)) {
var filename = 'category-' + params.cid + path.extname(uploadedFile.name);
uploadImage(filename, 'category', uploadedFile, req, res, next);
}
};
-uploadsController.uploadFavicon = function(req, res, next) {
+uploadsController.uploadFavicon = function (req, res, next) {
var uploadedFile = req.files.files[0];
var allowedTypes = ['image/x-icon', 'image/vnd.microsoft.icon'];
if (validateUpload(req, res, next, uploadedFile, allowedTypes)) {
- file.saveFileToLocal('favicon.ico', 'system', uploadedFile.path, function(err, image) {
- fs.unlink(uploadedFile.path, function(err) {
+ file.saveFileToLocal('favicon.ico', 'system', uploadedFile.path, function (err, image) {
+ fs.unlink(uploadedFile.path, function (err) {
if (err) {
winston.error(err);
}
@@ -54,15 +54,19 @@ uploadsController.uploadFavicon = function(req, res, next) {
}
};
-uploadsController.uploadTouchIcon = function(req, res, next) {
+uploadsController.uploadTouchIcon = function (req, res, next) {
var uploadedFile = req.files.files[0],
allowedTypes = ['image/png'],
sizes = [36, 48, 72, 96, 144, 192];
if (validateUpload(req, res, next, uploadedFile, allowedTypes)) {
- file.saveFileToLocal('touchicon-orig.png', 'system', uploadedFile.path, function(err, imageObj) {
+ 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.each(sizes, function (size, next) {
async.series([
async.apply(file.saveFileToLocal, 'touchicon-' + size + '.png', 'system', uploadedFile.path),
async.apply(image.resizeImage, {
@@ -72,8 +76,8 @@ uploadsController.uploadTouchIcon = function(req, res, next) {
height: size
})
], next);
- }, function(err) {
- fs.unlink(uploadedFile.path, function(err) {
+ }, function (err) {
+ fs.unlink(uploadedFile.path, function (err) {
if (err) {
winston.error(err);
}
@@ -89,14 +93,14 @@ uploadsController.uploadTouchIcon = function(req, res, next) {
}
};
-uploadsController.uploadLogo = function(req, res, next) {
+uploadsController.uploadLogo = function (req, res, next) {
upload('site-logo', req, res, next);
};
-uploadsController.uploadSound = function(req, res, next) {
+uploadsController.uploadSound = function (req, res, next) {
var uploadedFile = req.files.files[0];
- file.saveFileToLocal(uploadedFile.name, 'sounds', uploadedFile.path, function(err) {
+ file.saveFileToLocal(uploadedFile.name, 'sounds', uploadedFile.path, function (err) {
if (err) {
return next(err);
}
@@ -110,7 +114,7 @@ uploadsController.uploadSound = function(req, res, next) {
fs.symlink(filePath, path.join(soundsPath, path.basename(filePath)), 'file');
}
- fs.unlink(uploadedFile.path, function(err) {
+ fs.unlink(uploadedFile.path, function (err) {
if (err) {
return next(err);
}
@@ -120,14 +124,18 @@ uploadsController.uploadSound = function(req, res, next) {
});
};
-uploadsController.uploadDefaultAvatar = function(req, res, next) {
+uploadsController.uploadDefaultAvatar = function (req, res, next) {
upload('avatar-default', req, res, next);
};
+uploadsController.uploadOgImage = function (req, res, next) {
+ upload('og:image', req, res, next);
+};
+
function upload(name, req, res, next) {
var uploadedFile = req.files.files[0];
- var allowedTypes = ['image/png', 'image/jpeg', 'image/pjpeg', 'image/jpg', 'image/gif'];
- if (validateUpload(req, res, next, uploadedFile, allowedTypes)) {
+
+ if (validateUpload(req, res, next, uploadedFile, allowedImageTypes)) {
var filename = name + path.extname(uploadedFile.name);
uploadImage(filename, 'system', uploadedFile, req, res, next);
}
@@ -135,7 +143,7 @@ function upload(name, req, res, next) {
function validateUpload(req, res, next, uploadedFile, allowedTypes) {
if (allowedTypes.indexOf(uploadedFile.type) === -1) {
- fs.unlink(uploadedFile.path, function(err) {
+ fs.unlink(uploadedFile.path, function (err) {
if (err) {
winston.error(err);
}
@@ -150,7 +158,7 @@ function validateUpload(req, res, next, uploadedFile, allowedTypes) {
function uploadImage(filename, folder, uploadedFile, req, res, next) {
function done(err, image) {
- fs.unlink(uploadedFile.path, function(err) {
+ fs.unlink(uploadedFile.path, function (err) {
if (err) {
winston.error(err);
}
diff --git a/src/controllers/admin/users.js b/src/controllers/admin/users.js
index 7e5bd530d6..426d8b1c19 100644
--- a/src/controllers/admin/users.js
+++ b/src/controllers/admin/users.js
@@ -1,116 +1,92 @@
"use strict";
var async = require('async');
+var validator = require('validator');
+
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 = {};
-usersController.search = function(req, res, next) {
+var userFields = ['uid', 'username', 'userslug', 'email', 'postcount', 'joindate', 'banned',
+ 'reputation', 'picture', 'flags', 'lastonline', 'email:confirmed'];
+
+usersController.search = function (req, res, next) {
res.render('admin/manage/users', {
search_display: '',
users: []
});
};
-usersController.sortByJoinDate = function(req, res, next) {
- getUsers('users:joindate', 'latest', req, res, next);
+usersController.sortByJoinDate = function (req, res, next) {
+ getUsers('users:joindate', 'latest', undefined, undefined, req, res, next);
};
-usersController.notValidated = function(req, res, next) {
- getUsers('users:notvalidated', 'notvalidated', req, res, next);
+usersController.notValidated = function (req, res, next) {
+ getUsers('users:notvalidated', 'notvalidated', undefined, undefined, req, res, next);
};
-usersController.noPosts = function(req, res, next) {
- getUsersByScore('users:postcount', 'noposts', 0, 0, req, res, next);
+usersController.noPosts = function (req, res, next) {
+ getUsers('users:postcount', 'noposts', '-inf', 0, req, res, next);
};
-usersController.inactive = function(req, res, next) {
+usersController.flagged = function (req, res, next) {
+ getUsers('users:flags', 'mostflags', 1, '+inf', req, res, next);
+};
+
+usersController.inactive = function (req, res, next) {
var timeRange = 1000 * 60 * 60 * 24 * 30 * (parseInt(req.query.months, 10) || 3);
var cutoff = Date.now() - timeRange;
- getUsersByScore('users:online', 'inactive', '-inf', cutoff, req, res, next);
+ getUsers('users:online', 'inactive', '-inf', cutoff, req, res, next);
};
-function getUsersByScore(set, section, min, max, req, res, callback) {
- var page = parseInt(req.query.page, 10) || 1;
- var resultsPerPage = 25;
- var start = Math.max(0, page - 1) * resultsPerPage;
- var count = 0;
-
- async.waterfall([
- function (next) {
- async.parallel({
- count: function (next) {
- db.sortedSetCount(set, min, max, next);
- },
- uids: function (next) {
- db.getSortedSetRevRangeByScore(set, start, resultsPerPage, max, min, next);
- }
- }, next);
- },
- function (results, next) {
- count = results.count;
- user.getUsers(results.uids, req.uid, next);
- }
- ], function(err, users) {
- if (err) {
- return callback(err);
- }
- users = users.filter(function(user) {
- return user && parseInt(user.uid, 10);
- });
- var data = {
- users: users,
- page: page,
- pageCount: Math.ceil(count / resultsPerPage)
- };
- data[section] = true;
- render(req, res, data);
- });
-}
-
-usersController.banned = function(req, res, next) {
- getUsers('users:banned', 'banned', req, res, next);
+usersController.banned = function (req, res, next) {
+ getUsers('users:banned', 'banned', undefined, undefined, req, res, next);
};
-usersController.registrationQueue = function(req, res, next) {
+usersController.registrationQueue = function (req, res, next) {
var page = parseInt(req.query.page, 10) || 1;
var itemsPerPage = 20;
var start = (page - 1) * 20;
var stop = start + itemsPerPage - 1;
var invitations;
+
async.parallel({
- registrationQueueCount: function(next) {
+ registrationQueueCount: function (next) {
db.sortedSetCard('registration:queue', next);
},
- users: function(next) {
+ users: function (next) {
user.getRegistrationQueue(start, stop, next);
},
- invites: function(next) {
+ customHeaders: function (next) {
+ plugins.fireHook('filter:admin.registrationQueue.customHeaders', {headers: []}, next);
+ },
+ invites: function (next) {
async.waterfall([
- function(next) {
+ function (next) {
user.getAllInvites(next);
},
- function(_invitations, next) {
+ function (_invitations, next) {
invitations = _invitations;
- async.map(invitations, function(invites, next) {
+ async.map(invitations, function (invites, next) {
user.getUserField(invites.uid, 'username', next);
}, next);
},
- function(usernames, next) {
- invitations.forEach(function(invites, index) {
+ function (usernames, next) {
+ invitations.forEach(function (invites, index) {
invites.username = usernames[index];
});
- async.map(invitations, function(invites, next) {
+ async.map(invitations, function (invites, next) {
async.map(invites.invitations, user.getUsernameByEmail, next);
}, next);
},
- function(usernames, next) {
- invitations.forEach(function(invites, index) {
- invites.invitations = invites.invitations.map(function(email, i) {
+ function (usernames, next) {
+ invitations.forEach(function (invites, index) {
+ invites.invitations = invites.invitations.map(function (email, i) {
return {
email: email,
username: usernames[index][i] === '[[global:guest]]' ? '' : usernames[index][i]
@@ -121,35 +97,53 @@ usersController.registrationQueue = function(req, res, next) {
}
], next);
}
- }, function(err, data) {
+ }, function (err, data) {
if (err) {
return next(err);
}
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);
});
};
-function getUsers(set, section, req, res, next) {
+function getUsers(set, section, min, max, req, res, next) {
var page = parseInt(req.query.page, 10) || 1;
- var resultsPerPage = 25;
+ var resultsPerPage = 50;
var start = Math.max(0, page - 1) * resultsPerPage;
var stop = start + resultsPerPage - 1;
+ var byScore = min !== undefined && max !== undefined;
async.parallel({
- count: function(next) {
- db.sortedSetCard(set, next);
+ count: function (next) {
+ if (byScore) {
+ db.sortedSetCount(set, min, max, next);
+ } else {
+ db.sortedSetCard(set, next);
+ }
},
- users: function(next) {
- user.getUsersFromSet(set, req.uid, start, stop, next);
+ users: function (next) {
+ async.waterfall([
+ function (next) {
+ if (byScore) {
+ db.getSortedSetRevRangeByScore(set, start, resultsPerPage, max, min, next);
+ } else {
+ user.getUidsFromSet(set, start, stop, next);
+ }
+ },
+ function (uids, next) {
+ user.getUsersWithFields(uids, userFields, req.uid, next);
+ }
+ ], next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return next(err);
}
- results.users = results.users.filter(function(user) {
+ results.users = results.users.filter(function (user) {
+ user.email = validator.escape(String(user.email || ''));
return user && parseInt(user.uid, 10);
});
var data = {
@@ -166,11 +160,23 @@ function render(req, res, data) {
data.search_display = 'hidden';
data.pagination = pagination.create(data.page, data.pageCount, req.query);
data.requireEmailConfirmation = parseInt(meta.config.requireEmailConfirmation, 10) === 1;
+
+ var registrationType = meta.config.registrationType;
+
+ data.inviteOnly = registrationType === 'invite-only' || registrationType === 'admin-invite-only';
+ data.adminInviteOnly = registrationType === 'admin-invite-only';
+
res.render('admin/manage/users', data);
}
-usersController.getCSV = function(req, res, next) {
- user.getUsersCSV(function(err, 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/admin/widgets.js b/src/controllers/admin/widgets.js
index 8dd93fbead..c2d0d1e667 100644
--- a/src/controllers/admin/widgets.js
+++ b/src/controllers/admin/widgets.js
@@ -2,8 +2,8 @@
var widgetsController = {};
-widgetsController.get = function(req, res, next) {
- require('../../widgets/admin').get(function(err, data) {
+widgetsController.get = function (req, res, next) {
+ require('../../widgets/admin').get(function (err, data) {
if (err) {
return next(err);
}
diff --git a/src/controllers/api.js b/src/controllers/api.js
index 77ae7131ad..36261a23c1 100644
--- a/src/controllers/api.js
+++ b/src/controllers/api.js
@@ -12,16 +12,17 @@ var categories = require('../categories');
var privileges = require('../privileges');
var plugins = require('../plugins');
var widgets = require('../widgets');
+var accountHelpers = require('../controllers/accounts/helpers');
var apiController = {};
-apiController.getConfig = function(req, res, next) {
+apiController.getConfig = function (req, res, next) {
var config = {};
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;
@@ -51,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 || 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;
@@ -66,27 +67,22 @@ 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 || 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.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) {
+ ], function (err, config) {
if (err) {
return next(err);
}
@@ -100,7 +96,7 @@ apiController.getConfig = function(req, res, next) {
};
-apiController.renderWidgets = function(req, res, next) {
+apiController.renderWidgets = function (req, res, next) {
var areas = {
template: req.query.template,
locations: req.query.locations,
@@ -116,10 +112,11 @@ apiController.renderWidgets = function(req, res, next) {
template: areas.template,
url: areas.url,
locations: areas.locations,
+ isMobile: req.query.isMobile === 'true'
},
req,
res,
- function(err, widgets) {
+ function (err, widgets) {
if (err) {
return next(err);
}
@@ -127,105 +124,158 @@ apiController.renderWidgets = function(req, res, next) {
});
};
-apiController.getObject = function(req, res, next) {
- apiController.getObjectByType(req.uid, req.params.type, req.params.id, function(err, results) {
- if (err) {
- return next(err);
+apiController.getPostData = function (pid, uid, callback) {
+ async.parallel({
+ privileges: function (next) {
+ privileges.posts.get([pid], uid, next);
+ },
+ post: function (next) {
+ posts.getPostData(pid, next);
+ }
+ }, function (err, results) {
+ if (err || !results.post) {
+ return callback(err);
}
- res.json(results);
+ var post = results.post;
+ var privileges = results.privileges[0];
+
+ if (!privileges.read || !privileges['topics:read']) {
+ return callback();
+ }
+
+ post.ip = privileges.isAdminOrMod ? post.ip : undefined;
+ var selfPost = uid && uid === parseInt(post.uid, 10);
+ if (post.deleted && !(privileges.isAdminOrMod || selfPost)) {
+ post.content = '[[topic:post_is_deleted]]';
+ }
+ callback(null, post);
});
};
-apiController.getObjectByType = function(uid, type, id, callback) {
- var methods = {
- post: {
- canRead: privileges.posts.can,
- data: posts.getPostData
+apiController.getTopicData = function (tid, uid, callback) {
+ async.parallel({
+ privileges: function (next) {
+ privileges.topics.get(tid, uid, next);
},
- topic: {
- canRead: privileges.topics.can,
- data: topics.getTopicData
- },
- category: {
- canRead: privileges.categories.can,
- data: categories.getCategoryData
+ topic: function (next) {
+ topics.getTopicData(tid, next);
}
+ }, function (err, results) {
+ if (err || !results.topic) {
+ return callback(err);
+ }
+
+ if (!results.privileges.read || !results.privileges['topics:read'] || (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted)) {
+ return callback();
+ }
+ callback(null, results.topic);
+ });
+};
+
+apiController.getCategoryData = function (cid, uid, callback) {
+ async.parallel({
+ privileges: function (next) {
+ privileges.categories.get(cid, uid, next);
+ },
+ category: function (next) {
+ categories.getCategoryData(cid, next);
+ }
+ }, function (err, results) {
+ if (err || !results.category) {
+ return callback(err);
+ }
+
+ if (!results.privileges.read) {
+ return callback();
+ }
+ callback(null, results.category);
+ });
+};
+
+
+apiController.getObject = function (req, res, next) {
+ var methods = {
+ post: apiController.getPostData,
+ topic: apiController.getTopicData,
+ category: apiController.getCategoryData
};
-
- if (!methods[type]) {
- return callback();
+ var method = methods[req.params.type];
+ if (!method) {
+ return next();
}
+ method(req.params.id, req.uid, function (err, result) {
+ if (err || !result) {
+ return next(err);
+ }
+ res.json(result);
+ });
+};
+
+apiController.getCurrentUser = function (req, res, next) {
+ if (!req.uid) {
+ return res.status(401).json('not-authorized');
+ }
async.waterfall([
function (next) {
- methods[type].canRead('read', id, uid, next);
+ user.getUserField(req.uid, 'userslug', next);
},
- function (canRead, next) {
- if (!canRead) {
- return next(new Error('[[error:no-privileges]]'));
+ function (userslug, next) {
+ accountHelpers.getUserDataByUserSlug(userslug, req.uid, next);
+ }
+ ], function (err, userData) {
+ if (err) {
+ return next(err);
+ }
+ res.json(userData);
+ });
+};
+
+apiController.getUserByUID = function (req, res, next) {
+ byType('uid', req, res, next);
+};
+
+apiController.getUserByUsername = function (req, res, next) {
+ byType('username', req, res, next);
+};
+
+apiController.getUserByEmail = function (req, res, next) {
+ byType('email', req, res, next);
+};
+
+function byType(type, req, res, next) {
+ apiController.getUserDataByField(req.uid, type, req.params[type], function (err, data) {
+ if (err || !data) {
+ return next(err);
+ }
+ res.json(data);
+ });
+}
+
+apiController.getUserDataByField = function (callerUid, field, fieldValue, callback) {
+ async.waterfall([
+ function (next) {
+ if (field === 'uid') {
+ next(null, fieldValue);
+ } else if (field === 'username') {
+ user.getUidByUsername(fieldValue, next);
+ } else if (field === 'email') {
+ user.getUidByEmail(fieldValue, next);
+ } else {
+ next();
}
- methods[type].data(id, next);
- }
- ], callback);
-};
-
-apiController.getUserByUID = function(req, res, next) {
- var uid = req.params.uid ? req.params.uid : 0;
-
- apiController.getUserDataByUID(req.uid, uid, function(err, data) {
- if (err) {
- return next(err);
- }
- res.json(data);
- });
-};
-
-apiController.getUserByUsername = function(req, res, next) {
- var username = req.params.username ? req.params.username : 0;
-
- apiController.getUserDataByUsername(req.uid, username, function(err, data) {
- if (err) {
- return next(err);
- }
- res.json(data);
- });
-};
-
-apiController.getUserByEmail = function(req, res, next) {
- var email = req.params.email ? req.params.email : 0;
-
- apiController.getUserDataByEmail(req.uid, email, function(err, data) {
- if (err) {
- return next(err);
- }
- res.json(data);
- });
-};
-
-apiController.getUserDataByUsername = function(callerUid, username, callback) {
- async.waterfall([
- function(next) {
- user.getUidByUsername(username, next);
},
- function(uid, next) {
+ function (uid, next) {
+ if (!uid) {
+ return next();
+ }
apiController.getUserDataByUID(callerUid, uid, next);
}
], callback);
};
-apiController.getUserDataByEmail = function(callerUid, email, callback) {
- async.waterfall([
- function(next) {
- user.getUidByEmail(email, next);
- },
- function(uid, next) {
- apiController.getUserDataByUID(callerUid, uid, next);
- }
- ], callback);
-};
-
-apiController.getUserDataByUID = function(callerUid, uid, callback) {
+apiController.getUserDataByUID = function (callerUid, uid, callback) {
if (!parseInt(callerUid, 10) && parseInt(meta.config.privateUserInfo, 10) === 1) {
return callback(new Error('[[error:no-privileges]]'));
}
@@ -237,7 +287,7 @@ apiController.getUserDataByUID = function(callerUid, uid, callback) {
async.parallel({
userData: async.apply(user.getUserData, uid),
settings: async.apply(user.getSettings, uid)
- }, function(err, results) {
+ }, function (err, results) {
if (err || !results.userData) {
return callback(err || new Error('[[error:no-user]]'));
}
@@ -249,8 +299,8 @@ apiController.getUserDataByUID = function(callerUid, uid, callback) {
});
};
-apiController.getModerators = function(req, res, next) {
- categories.getModerators(req.params.cid, function(err, moderators) {
+apiController.getModerators = function (req, res, next) {
+ categories.getModerators(req.params.cid, function (err, moderators) {
if (err) {
return next(err);
}
@@ -259,7 +309,7 @@ apiController.getModerators = function(req, res, next) {
};
-apiController.getRecentPosts = function(req, res, next) {
+apiController.getRecentPosts = function (req, res, next) {
posts.getRecentPosts(req.uid, 0, 19, req.params.term, function (err, data) {
if (err) {
return next(err);
diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js
index 8db2c9e580..0111f62573 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');
@@ -16,7 +17,7 @@ var Password = require('../password');
var authenticationController = {};
-authenticationController.register = function(req, res, next) {
+authenticationController.register = function (req, res, next) {
var registrationType = meta.config.registrationType || 'normal';
if (registrationType === 'disabled') {
@@ -32,14 +33,14 @@ authenticationController.register = function(req, res, next) {
}
async.waterfall([
- function(next) {
- if (registrationType === 'invite-only') {
+ function (next) {
+ if (registrationType === 'invite-only' || registrationType === 'admin-invite-only') {
user.verifyInvitation(userData, next);
} else {
next();
}
},
- function(next) {
+ function (next) {
if (!userData.email) {
return next(new Error('[[error:invalid-email]]'));
}
@@ -54,18 +55,28 @@ authenticationController.register = function(req, res, next) {
user.isPasswordValid(userData.password, next);
},
- function(next) {
+ function (next) {
res.locals.processLogin = true; // set it to false in plugin if you wish to just register only
plugins.fireHook('filter:register.check', {req: req, res: res, userData: userData}, next);
},
- function(data, next) {
- if (registrationType === 'normal' || registrationType === 'invite-only') {
+ function (data, next) {
+ if (registrationType === 'normal' || registrationType === 'invite-only' || registrationType === 'admin-invite-only') {
registerAndLoginUser(req, res, userData, next);
} else if (registrationType === 'admin-approval') {
addToApprovalQueue(req, userData, next);
+ } else if (registrationType === 'admin-approval-ip') {
+ db.sortedSetCard('ip:' + req.ip + ':uid', function (err, count) {
+ if (err) {
+ next(err);
+ } else if (count) {
+ addToApprovalQueue(req, userData, next);
+ } else {
+ registerAndLoginUser(req, res, userData, next);
+ }
+ });
}
}
- ], function(err, data) {
+ ], function (err, data) {
if (err) {
return res.status(400).send(err.message);
}
@@ -81,10 +92,31 @@ authenticationController.register = function(req, res, next) {
function registerAndLoginUser(req, res, userData, callback) {
var uid;
async.waterfall([
- function(next) {
+ function (next) {
+ plugins.fireHook('filter:register.interstitial', {
+ 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;
+
+ if (!deferRegistration) {
+ return next();
+ } else {
+ userData.register = true;
+ req.session.registration = userData;
+ return res.json({ referrer: nconf.get('relative_path') + '/register/complete' });
+ }
+ });
+ },
+ function (next) {
user.create(userData, next);
},
- function(_uid, next) {
+ function (_uid, next) {
uid = _uid;
if (res.locals.processLogin) {
authenticationController.doLogin(req, uid, next);
@@ -92,7 +124,7 @@ function registerAndLoginUser(req, res, userData, callback) {
next();
}
},
- function(next) {
+ function (next) {
user.deleteInvitationKey(userData.email);
plugins.fireHook('filter:register.complete', {uid: uid, referrer: req.body.referrer || nconf.get('relative_path') + '/'}, next);
}
@@ -101,20 +133,76 @@ function registerAndLoginUser(req, res, userData, callback) {
function addToApprovalQueue(req, userData, callback) {
async.waterfall([
- function(next) {
+ function (next) {
userData.ip = req.ip;
user.addToApprovalQueue(userData, next);
},
- function(next) {
+ function (next) {
next(null, {message: '[[register:registration-added-to-queue]]'});
}
], callback);
}
-authenticationController.login = function(req, res, next) {
+authenticationController.registerComplete = function (req, res, next) {
+ // For the interstitials that respond, execute the callback with the form body
+ plugins.fireHook('filter:register.interstitial', {
+ 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));
+ }
+
+ return memo;
+ }, []);
+
+ var done = function () {
+ delete req.session.registration;
+
+ if (req.session.returnTo) {
+ res.redirect(req.session.returnTo);
+ } else {
+ res.redirect(nconf.get('relative_path') + '/');
+ }
+ };
+
+ async.parallel(callbacks, function (err) {
+ if (err) {
+ req.flash('error', err.message);
+ return res.redirect(nconf.get('relative_path') + '/register/complete');
+ }
+
+ if (req.session.registration.register === true) {
+ res.locals.processLogin = true;
+ registerAndLoginUser(req, res, req.session.registration, done);
+ } else {
+ // Clear registration data in session
+ done();
+ }
+ });
+ });
+};
+
+authenticationController.registerAbort = function (req, res) {
+ // End the session and redirect to home
+ req.session.destroy(function () {
+ res.redirect(nconf.get('relative_path') + '/');
+ });
+};
+
+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')) {
@@ -124,7 +212,7 @@ authenticationController.login = function(req, res, next) {
var loginWith = meta.config.allowLoginWith || 'username-email';
if (req.body.username && utils.isEmailValid(req.body.username) && loginWith.indexOf('email') !== -1) {
- user.getUsernameByEmail(req.body.username, function(err, username) {
+ user.getUsernameByEmail(req.body.username, function (err, username) {
if (err) {
return next(err);
}
@@ -139,7 +227,7 @@ authenticationController.login = function(req, res, next) {
};
function continueLogin(req, res, next) {
- passport.authenticate('local', function(err, userData, info) {
+ passport.authenticate('local', function (err, userData, info) {
if (err) {
return res.status(403).send(err.message);
}
@@ -167,11 +255,15 @@ function continueLogin(req, res, next) {
if (passwordExpiry && passwordExpiry < Date.now()) {
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) {
+ 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 {
- authenticationController.doLogin(req, userData.uid, function(err) {
+ authenticationController.doLogin(req, userData.uid, function (err) {
if (err) {
return res.status(403).send(err.message);
}
@@ -189,12 +281,12 @@ function continueLogin(req, res, next) {
})(req, res, next);
}
-authenticationController.doLogin = function(req, uid, callback) {
+authenticationController.doLogin = function (req, uid, callback) {
if (!uid) {
return callback();
}
- req.login({uid: uid}, function(err) {
+ req.login({uid: uid}, function (err) {
if (err) {
return callback(err);
}
@@ -203,11 +295,13 @@ authenticationController.doLogin = function(req, uid, callback) {
});
};
-authenticationController.onSuccessfulLogin = function(req, uid, callback) {
- callback = callback || function() {};
+authenticationController.onSuccessfulLogin = function (req, uid, callback) {
+ callback = callback || function () {};
var uuid = utils.generateUUID();
req.session.meta = {};
+ delete req.session.forceLogin;
+
// Associate IP used during login with user account
user.logIP(uid, req.ip);
req.session.meta.ip = req.ip;
@@ -228,8 +322,11 @@ 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) {
+ ], function (err) {
if (err) {
return callback(err);
}
@@ -238,7 +335,7 @@ authenticationController.onSuccessfulLogin = function(req, uid, callback) {
});
};
-authenticationController.localLogin = function(req, username, password, next) {
+authenticationController.localLogin = function (req, username, password, next) {
if (!username) {
return next(new Error('[[error:invalid-username]]'));
}
@@ -262,11 +359,14 @@ authenticationController.localLogin = function(req, username, password, next) {
},
function (next) {
async.parallel({
- userData: function(next) {
- db.getObjectFields('user:' + uid, ['password', 'banned', 'passwordExpiry'], next);
+ userData: function (next) {
+ db.getObjectFields('user:' + uid, ['password', 'passwordExpiry'], next);
},
- isAdmin: function(next) {
+ isAdmin: function (next) {
user.isAdministrator(uid, next);
+ },
+ banned: function (next) {
+ user.isBanned(uid, next);
}
}, next);
},
@@ -278,13 +378,22 @@ authenticationController.localLogin = function(req, username, password, next) {
if (!result.isAdmin && parseInt(meta.config.allowLocalLogin, 10) === 0) {
return next(new Error('[[error:local-login-disabled]]'));
}
-
if (!userData || !userData.password) {
return next(new Error('[[error:invalid-user-data]]'));
}
- if (userData.banned && parseInt(userData.banned, 10) === 1) {
- return next(new Error('[[error:user-banned]]'));
+ if (result.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);
},
function (passwordMatch, next) {
@@ -297,18 +406,19 @@ authenticationController.localLogin = function(req, username, password, next) {
], next);
};
-authenticationController.logout = function(req, res, next) {
+authenticationController.logout = function (req, res, next) {
if (req.user && parseInt(req.user.uid, 10) > 0 && req.sessionID) {
var uid = parseInt(req.user.uid, 10);
- user.auth.revokeSession(req.sessionID, uid, function(err) {
+ user.auth.revokeSession(req.sessionID, uid, function (err) {
if (err) {
return next(err);
}
req.logout();
+ req.session.destroy();
- // action:user.loggedOut deprecated in > v0.9.3
- plugins.fireHook('action:user.loggedOut', {req: req, res: res, uid: uid});
- plugins.fireHook('static:user.loggedOut', {req: req, res: res, uid: uid}, function() {
+ user.setUserField(uid, 'lastonline', Date.now() - 300000);
+
+ plugins.fireHook('static:user.loggedOut', {req: req, res: res, uid: uid}, function () {
res.status(200).send('');
});
});
diff --git a/src/controllers/categories.js b/src/controllers/categories.js
index 7ed087c704..9a18e7f1dd 100644
--- a/src/controllers/categories.js
+++ b/src/controllers/categories.js
@@ -1,25 +1,22 @@
"use strict";
-
var async = require('async');
var nconf = require('nconf');
var validator = require('validator');
var categories = require('../categories');
var meta = require('../meta');
-var plugins = require('../plugins');
-
var helpers = require('./helpers');
var categoriesController = {};
-categoriesController.list = function(req, res, next) {
+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]]'
@@ -28,14 +25,14 @@ categoriesController.list = function(req, res, next) {
content: 'website'
}];
- if (meta.config['brand:logo']) {
- var brandLogo = meta.config['brand:logo'];
- if (!brandLogo.startsWith('http')) {
- brandLogo = nconf.get('url') + brandLogo;
+ var ogImage = meta.config['og:image'] || meta.config['brand:logo'] || '';
+ if (ogImage) {
+ if (!ogImage.startsWith('http')) {
+ ogImage = nconf.get('url') + ogImage;
}
res.locals.metaTags.push({
property: 'og:image',
- content: brandLogo
+ content: ogImage
});
}
@@ -51,33 +48,32 @@ categoriesController.list = function(req, res, next) {
categories.flattenCategories(allCategories, categoryData);
categories.getRecentTopicReplies(allCategories, req.uid, next);
- },
- function (next) {
- var data = {
- title: '[[pages:categories]]',
- categories: categoryData
- };
-
- if (req.path.startsWith('/api/categories') || req.path.startsWith('/categories')) {
- data.breadcrumbs = helpers.buildBreadcrumbs([{text: data.title}]);
- }
-
- data.categories.forEach(function(category) {
- if (category && Array.isArray(category.posts) && category.posts.length) {
- category.teaser = {
- url: nconf.get('relative_path') + '/topic/' + category.posts[0].topic.slug + '/' + category.posts[0].index,
- timestampISO: category.posts[0].timestampISO
- };
- }
- });
-
- plugins.fireHook('filter:categories.build', {req: req, res: res, templateData: data}, next);
}
- ], function(err, data) {
+ ], function (err) {
if (err) {
return next(err);
}
- res.render('categories', data.templateData);
+
+ var data = {
+ title: '[[pages:categories]]',
+ categories: categoryData
+ };
+
+ if (req.path.startsWith('/api/categories') || req.path.startsWith('/categories')) {
+ data.breadcrumbs = helpers.buildBreadcrumbs([{text: data.title}]);
+ }
+
+ data.categories.forEach(function (category) {
+ if (category && Array.isArray(category.posts) && category.posts.length) {
+ category.teaser = {
+ url: nconf.get('relative_path') + '/topic/' + category.posts[0].topic.slug + '/' + category.posts[0].index,
+ timestampISO: category.posts[0].timestampISO,
+ pid: category.posts[0].pid
+ };
+ }
+ });
+
+ res.render('categories', data);
});
};
diff --git a/src/controllers/category.js b/src/controllers/category.js
index f80ad122a7..2a660ee63c 100644
--- a/src/controllers/category.js
+++ b/src/controllers/category.js
@@ -9,18 +9,18 @@ var privileges = require('../privileges');
var user = require('../user');
var categories = require('../categories');
var meta = require('../meta');
-var plugins = require('../plugins');
var pagination = require('../pagination');
var helpers = require('./helpers');
var utils = require('../../public/src/utils');
var categoryController = {};
-categoryController.get = function(req, res, callback) {
+categoryController.get = function (req, res, callback) {
var cid = req.params.category_id;
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();
@@ -29,13 +29,13 @@ categoryController.get = function(req, res, callback) {
async.waterfall([
function (next) {
async.parallel({
- categoryData: function(next) {
+ categoryData: function (next) {
categories.getCategoryFields(cid, ['slug', 'disabled', 'topic_count'], next);
},
- privileges: function(next) {
+ privileges: function (next) {
privileges.categories.get(cid, req.uid, next);
},
- userSettings: function(next) {
+ userSettings: function (next) {
user.getSettings(req.uid, next);
}
}, next);
@@ -55,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));
@@ -76,20 +76,21 @@ categoryController.get = function(req, res, callback) {
topicIndex = 0;
}
- var set = 'cid:' + cid + ':tids',
- reverse = false;
-
- if (settings.categoryTopicSort === 'newest_to_oldest') {
+ var set = 'cid:' + cid + ':tids';
+ var reverse = false;
+ // `sort` qs has priority over user setting
+ var sort = req.query.sort || settings.categoryTopicSort;
+ if (sort === 'newest_to_oldest') {
reverse = true;
- } else if (settings.categoryTopicSort === 'most_posts') {
+ } else if (sort === 'most_posts') {
reverse = true;
set = 'cid:' + cid + ':tids:posts';
}
- var start = (currentPage - 1) * settings.topicsPerPage + topicIndex,
- stop = start + settings.topicsPerPage - 1;
+ 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) {
@@ -126,7 +132,7 @@ categoryController.get = function(req, res, callback) {
url: nconf.get('relative_path') + '/category/' + categoryData.slug
}
];
- helpers.buildCategoryBreadcrumbs(categoryData.parentCid, function(err, crumbs) {
+ helpers.buildCategoryBreadcrumbs(categoryData.parentCid, function (err, crumbs) {
if (err) {
return next(err);
}
@@ -140,68 +146,71 @@ categoryController.get = function(req, res, callback) {
}
var allCategories = [];
categories.flattenCategories(allCategories, categoryData.children);
- categories.getRecentTopicReplies(allCategories, req.uid, function(err) {
+ categories.getRecentTopicReplies(allCategories, req.uid, function (err) {
next(err, categoryData);
});
- },
- function (categoryData, next) {
- categoryData.privileges = userPrivileges;
- categoryData.showSelect = categoryData.privileges.editable;
-
- res.locals.metaTags = [
- {
- name: 'title',
- content: categoryData.name
- },
- {
- property: 'og:title',
- content: categoryData.name
- },
- {
- name: 'description',
- content: categoryData.description
- },
- {
- property: "og:type",
- content: 'website'
- }
- ];
-
- if (categoryData.backgroundImage) {
- res.locals.metaTags.push({
- name: 'og:image',
- content: categoryData.backgroundImage
- });
- }
-
- res.locals.linkTags = [
- {
- rel: 'alternate',
- type: 'application/rss+xml',
- href: nconf.get('url') + '/category/' + cid + '.rss'
- },
- {
- rel: 'up',
- href: nconf.get('url')
- }
- ];
-
- categoryData['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1;
- categoryData.rssFeedUrl = nconf.get('relative_path') + '/category/' + categoryData.cid + '.rss';
- categoryData.title = categoryData.name;
- categoryData.pagination = pagination.create(currentPage, pageCount);
- categoryData.pagination.rel.forEach(function(rel) {
- rel.href = nconf.get('url') + '/category/' + categoryData.slug + rel.href;
- res.locals.linkTags.push(rel);
- });
-
- plugins.fireHook('filter:category.build', {req: req, res: res, templateData: categoryData}, next);
}
- ], function (err, data) {
+ ], function (err, categoryData) {
if (err) {
return callback(err);
}
- res.render('category', data.templateData);
+
+ categoryData.privileges = userPrivileges;
+ categoryData.showSelect = categoryData.privileges.editable;
+
+ res.locals.metaTags = [
+ {
+ name: 'title',
+ content: categoryData.name
+ },
+ {
+ property: 'og:title',
+ content: categoryData.name
+ },
+ {
+ name: 'description',
+ content: categoryData.description
+ },
+ {
+ property: "og:type",
+ content: 'website'
+ }
+ ];
+
+ if (categoryData.backgroundImage) {
+ res.locals.metaTags.push({
+ name: 'og:image',
+ content: categoryData.backgroundImage
+ });
+ }
+
+ res.locals.linkTags = [
+ {
+ rel: 'alternate',
+ type: 'application/rss+xml',
+ href: nconf.get('url') + '/category/' + cid + '.rss'
+ },
+ {
+ rel: 'up',
+ href: nconf.get('url')
+ }
+ ];
+
+ 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;
+ res.locals.linkTags.push(rel);
+ });
+
+ res.render('category', categoryData);
});
};
diff --git a/src/controllers/globalmods.js b/src/controllers/globalmods.js
index 3275c7929e..7e4fd1ffec 100644
--- a/src/controllers/globalmods.js
+++ b/src/controllers/globalmods.js
@@ -1,23 +1,12 @@
"use strict";
var user = require('../user');
-var adminFlagsController = require('./admin/flags');
var adminBlacklistController = require('./admin/blacklist');
var globalModsController = {};
-globalModsController.flagged = function(req, res, next) {
- user.isAdminOrGlobalMod(req.uid, function(err, isAdminOrGlobalMod) {
- if (err || !isAdminOrGlobalMod) {
- return next(err);
- }
-
- adminFlagsController.get(req, res, next);
- });
-};
-
-globalModsController.ipBlacklist = function(req, res, next) {
- user.isAdminOrGlobalMod(req.uid, function(err, isAdminOrGlobalMod) {
+globalModsController.ipBlacklist = function (req, res, next) {
+ user.isAdminOrGlobalMod(req.uid, function (err, isAdminOrGlobalMod) {
if (err || !isAdminOrGlobalMod) {
return next(err);
}
diff --git a/src/controllers/groups.js b/src/controllers/groups.js
index 837ba3b1f6..53b7064f59 100644
--- a/src/controllers/groups.js
+++ b/src/controllers/groups.js
@@ -1,19 +1,20 @@
"use strict";
-var async = require('async'),
- nconf = require('nconf'),
- validator = require('validator'),
- meta = require('../meta'),
- groups = require('../groups'),
- user = require('../user'),
- helpers = require('./helpers'),
- plugins = require('../plugins'),
- groupsController = {};
+var async = require('async');
+var nconf = require('nconf');
+var validator = require('validator');
-groupsController.list = function(req, res, next) {
+var meta = require('../meta');
+var groups = require('../groups');
+var user = require('../user');
+var helpers = require('./helpers');
+
+var groupsController = {};
+
+groupsController.list = function (req, res, next) {
var sort = req.query.sort || 'alpha';
- groupsController.getGroupsFromSet(req.uid, sort, 0, 14, function(err, data) {
+ groupsController.getGroupsFromSet(req.uid, sort, 0, 14, function (err, data) {
if (err) {
return next(err);
}
@@ -23,7 +24,7 @@ groupsController.list = function(req, res, next) {
});
};
-groupsController.getGroupsFromSet = function(uid, sort, start, stop, callback) {
+groupsController.getGroupsFromSet = function (uid, sort, start, stop, callback) {
var set = 'groups:visible:name';
if (sort === 'count') {
set = 'groups:visible:memberCount';
@@ -31,7 +32,7 @@ groupsController.getGroupsFromSet = function(uid, sort, start, stop, callback) {
set = 'groups:visible:createtime';
}
- groups.getGroupsFromSet(set, uid, start, stop, function(err, groups) {
+ groups.getGroupsFromSet(set, uid, start, stop, function (err, groups) {
if (err) {
return callback(err);
}
@@ -44,25 +45,33 @@ groupsController.getGroupsFromSet = function(uid, sort, start, stop, callback) {
});
};
-groupsController.details = function(req, res, 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)
- }, function(err, checks) {
+ 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);
}
@@ -71,55 +80,59 @@ groupsController.details = function(req, res, callback) {
},
function (next) {
async.parallel({
- group: function(next) {
- groups.get(res.locals.groupName, {
+ group: function (next) {
+ groups.get(groupName, {
uid: req.uid,
truncateUserList: true,
userListCount: 20
}, next);
},
- posts: function(next) {
- groups.getLatestMemberPosts(res.locals.groupName, 10, req.uid, next);
+ posts: function (next) {
+ groups.getLatestMemberPosts(groupName, 10, req.uid, next);
},
- isAdmin: async.apply(user.isAdministrator, req.uid)
+ isAdmin:function (next) {
+ user.isAdministrator(req.uid, next);
+ },
+ isGlobalMod: function (next) {
+ user.isGlobalModerator(req.uid, next);
+ }
}, next);
- },
- function (results, next) {
- if (!results.group) {
- return callback();
- }
- 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;
- plugins.fireHook('filter:group.build', {req: req, res: res, templateData: results}, next);
}
- ], function(err, results) {
+ ], function (err, results) {
if (err) {
return callback(err);
}
- res.render('groups/details', results.templateData);
+ if (!results.group) {
+ return callback();
+ }
+ 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;
+
+ res.render('groups/details', results);
});
};
-groupsController.members = function(req, res, next) {
+groupsController.members = function (req, res, next) {
var groupName;
async.waterfall([
- function(next) {
+ function (next) {
groups.getGroupNameByGroupSlug(req.params.slug, next);
},
- function(_groupName, next) {
+ function (_groupName, next) {
groupName = _groupName;
user.getUsersFromSet('group:' + groupName + ':members', req.uid, 0, 49, next);
},
- ], function(err, users) {
+ ], function (err, users) {
if (err || !groupName) {
return next(err);
}
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]]'}
]);
@@ -132,13 +145,13 @@ groupsController.members = function(req, res, next) {
});
};
-groupsController.uploadCover = function(req, res, next) {
+groupsController.uploadCover = function (req, res, next) {
var params = JSON.parse(req.body.params);
groups.updateCover(req.uid, {
file: req.files.files[0].path,
groupName: params.groupName
- }, function(err, image) {
+ }, function (err, image) {
if (err) {
return next(err);
}
diff --git a/src/controllers/helpers.js b/src/controllers/helpers.js
index 058a1849b1..13a91aaa4c 100644
--- a/src/controllers/helpers.js
+++ b/src/controllers/helpers.js
@@ -1,43 +1,52 @@
'use strict';
-var nconf = require('nconf'),
- async = require('async'),
- validator = require('validator'),
+var nconf = require('nconf');
+var async = require('async');
+var validator = require('validator');
+var winston = require('winston');
- translator = require('../../public/src/modules/translator'),
- categories = require('../categories'),
- plugins = require('../plugins'),
- meta = require('../meta');
+var categories = require('../categories');
+var plugins = require('../plugins');
+var meta = require('../meta');
var helpers = {};
-helpers.notAllowed = function(req, res, error) {
- if (req.uid) {
- if (res.locals.isAPI) {
- res.status(403).json({
- path: req.path.replace(/^\/api/, ''),
- loggedIn: !!req.uid, error: error,
- title: '[[global:403.title]]'
- });
- } else {
- res.status(403).render('403', {
- path: req.path,
- loggedIn: !!req.uid, error: error,
- title: '[[global:403.title]]'
- });
+helpers.notAllowed = function (req, res, error) {
+ plugins.fireHook('filter:helpers.notAllowed', {
+ req: req,
+ res: res,
+ error: error
+ }, function (err, data) {
+ if (err) {
+ return winston.error(err);
}
- } else {
- if (res.locals.isAPI) {
- req.session.returnTo = nconf.get('relative_path') + req.url.replace(/^\/api/, '');
- res.status(401).json('not-authorized');
+ if (req.uid) {
+ if (res.locals.isAPI) {
+ res.status(403).json({
+ path: req.path.replace(/^\/api/, ''),
+ loggedIn: !!req.uid, error: error,
+ title: '[[global:403.title]]'
+ });
+ } else {
+ res.status(403).render('403', {
+ path: req.path,
+ loggedIn: !!req.uid, error: error,
+ title: '[[global:403.title]]'
+ });
+ }
} else {
- req.session.returnTo = nconf.get('relative_path') + req.url;
- res.redirect(nconf.get('relative_path') + '/login');
+ if (res.locals.isAPI) {
+ req.session.returnTo = nconf.get('relative_path') + req.url.replace(/^\/api/, '');
+ res.status(401).json('not-authorized');
+ } else {
+ req.session.returnTo = nconf.get('relative_path') + req.url;
+ res.redirect(nconf.get('relative_path') + '/login');
+ }
}
- }
+ });
};
-helpers.redirect = function(res, url) {
+helpers.redirect = function (res, url) {
if (res.locals.isAPI) {
res.status(308).json(url);
} else {
@@ -45,20 +54,20 @@ helpers.redirect = function(res, url) {
}
};
-helpers.buildCategoryBreadcrumbs = function(cid, callback) {
+helpers.buildCategoryBreadcrumbs = function (cid, callback) {
var breadcrumbs = [];
- async.whilst(function() {
+ async.whilst(function () {
return parseInt(cid, 10);
- }, function(next) {
- categories.getCategoryFields(cid, ['name', 'slug', 'parentCid', 'disabled'], function(err, data) {
+ }, function (next) {
+ categories.getCategoryFields(cid, ['name', 'slug', 'parentCid', 'disabled'], function (err, data) {
if (err) {
return next(err);
}
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
});
}
@@ -66,11 +75,18 @@ helpers.buildCategoryBreadcrumbs = function(cid, callback) {
cid = data.parentCid;
next();
});
- }, function(err) {
+ }, function (err) {
if (err) {
return callback(err);
}
+ if (!meta.config.homePageRoute && meta.config.homePageCustom) {
+ breadcrumbs.unshift({
+ text: '[[global:header.categories]]',
+ url: nconf.get('relative_path') + '/categories'
+ });
+ }
+
breadcrumbs.unshift({
text: '[[global:home]]',
url: nconf.get('relative_path') + '/'
@@ -80,7 +96,7 @@ helpers.buildCategoryBreadcrumbs = function(cid, callback) {
});
};
-helpers.buildBreadcrumbs = function(crumbs) {
+helpers.buildBreadcrumbs = function (crumbs) {
var breadcrumbs = [
{
text: '[[global:home]]',
@@ -88,7 +104,7 @@ helpers.buildBreadcrumbs = function(crumbs) {
}
];
- crumbs.forEach(function(crumb) {
+ crumbs.forEach(function (crumb) {
if (crumb) {
if (crumb.url) {
crumb.url = nconf.get('relative_path') + crumb.url;
@@ -100,14 +116,14 @@ helpers.buildBreadcrumbs = function(crumbs) {
return breadcrumbs;
};
-helpers.buildTitle = function(pageTitle) {
+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() {
+ var title = titleLayout.replace('{pageTitle}', function () {
return pageTitle;
- }).replace('{browserTitle}', function() {
+ }).replace('{browserTitle}', function () {
return browserTitle;
});
return title;
diff --git a/src/controllers/index.js b/src/controllers/index.js
index fa35523c2b..6f73886986 100644
--- a/src/controllers/index.js
+++ b/src/controllers/index.js
@@ -3,15 +3,16 @@
var async = require('async');
var nconf = require('nconf');
var validator = require('validator');
+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 = {
topics: require('./topics'),
+ posts: require('./posts'),
categories: require('./categories'),
category: require('./category'),
unread: require('./unread'),
@@ -25,14 +26,16 @@ var Controllers = {
authentication: require('./authentication'),
api: require('./api'),
admin: require('./admin'),
- globalMods: require('./globalmods')
+ globalMods: require('./globalmods'),
+ mods: require('./mods'),
+ sitemap: require('./sitemap')
};
-Controllers.home = function(req, res, next) {
- var route = meta.config.homePageRoute || meta.config.homePageCustom || 'categories';
+Controllers.home = function (req, res, next) {
+ var route = meta.config.homePageRoute || (meta.config.homePageCustom || '').replace(/^\/+/, '') || 'categories';
- user.getSettings(req.uid, function(err, settings) {
+ user.getSettings(req.uid, function (err, settings) {
if (err) {
return next(err);
}
@@ -48,6 +51,8 @@ Controllers.home = function(req, res, next) {
if (route === 'categories' || route === '/') {
Controllers.categories.list(req, res, next);
+ } else if (route === 'unread') {
+ Controllers.unread.get(req, res, next);
} else if (route === 'recent') {
Controllers.recent.get(req, res, next);
} else if (route === 'popular') {
@@ -67,9 +72,9 @@ Controllers.home = function(req, res, next) {
});
};
-Controllers.reset = function(req, res, next) {
+Controllers.reset = function (req, res, next) {
if (req.params.code) {
- user.reset.validate(req.params.code, function(err, valid) {
+ user.reset.validate(req.params.code, function (err, valid) {
if (err) {
return next(err);
}
@@ -94,76 +99,144 @@ Controllers.reset = function(req, res, next) {
};
-Controllers.login = function(req, res, next) {
- var data = {},
- loginStrategies = require('../routes/authentication').getLoginStrategies(),
- registrationType = meta.config.registrationType || 'normal';
+Controllers.login = function (req, res, next) {
+ var data = {};
+ var loginStrategies = require('../routes/authentication').getLoginStrategies();
+ var registrationType = meta.config.registrationType || 'normal';
+
+ var allowLoginWith = (meta.config.allowLoginWith || 'username-email');
+
+ var errorText;
+ if (req.query.error === 'csrf-invalid') {
+ errorText = '[[error:csrf-invalid]]';
+ } else if (req.query.error) {
+ errorText = validator.escape(String(req.query.error));
+ }
data.alternate_logins = loginStrategies.length > 0;
data.authentication = loginStrategies;
data.allowLocalLogin = parseInt(meta.config.allowLocalLogin, 10) === 1 || parseInt(req.query.local, 10) === 1;
- data.allowRegistration = registrationType === 'normal' || registrationType === 'admin-approval';
- data.allowLoginWith = '[[login:' + (meta.config.allowLoginWith || 'username-email') + ']]';
+ data.allowRegistration = registrationType === 'normal' || registrationType === 'admin-approval' || registrationType === 'admin-approval-ip';
+ data.allowLoginWith = '[[login:' + allowLoginWith + ']]';
data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[global:login]]'}]);
- data.error = req.flash('error')[0];
+ data.error = req.flash('error')[0] || errorText;
data.title = '[[pages:login]]';
- res.render('login', data);
+ if (!data.allowLocalLogin && !data.allowRegistration && data.alternate_logins && data.authentication.length === 1) {
+ if (res.locals.isAPI) {
+ return helpers.redirect(res, {
+ external: data.authentication[0].url
+ });
+ } else {
+ return res.redirect(data.authentication[0].url);
+ }
+ }
+ if (req.uid) {
+ user.getUserFields(req.uid, ['username', 'email'], function (err, user) {
+ if (err) {
+ return next(err);
+ }
+ data.username = allowLoginWith === 'email' ? user.email : user.username;
+ data.alternate_logins = [];
+ res.render('login', data);
+ });
+ } else {
+ res.render('login', data);
+ }
+
};
-Controllers.register = function(req, res, next) {
+Controllers.register = function (req, res, next) {
var registrationType = meta.config.registrationType || 'normal';
if (registrationType === 'disabled') {
return next();
}
+ var errorText;
+ if (req.query.error === 'csrf-invalid') {
+ errorText = '[[error:csrf-invalid]]';
+ }
+
async.waterfall([
- function(next) {
+ function (next) {
if (registrationType === 'invite-only' || registrationType === 'admin-invite-only') {
user.verifyInvitation(req.query, next);
} else {
next();
}
},
- function(next) {
+ function (next) {
plugins.fireHook('filter:parse.post', {postData: {content: meta.config.termsOfUse || ''}}, next);
- },
- function(tos, next) {
- var loginStrategies = require('../routes/authentication').getLoginStrategies();
- var data = {
- 'register_window:spansize': loginStrategies.length ? 'col-md-6' : 'col-md-12',
- 'alternate_logins': !!loginStrategies.length
- };
-
- data.authentication = loginStrategies;
-
- data.minimumUsernameLength = parseInt(meta.config.minimumUsernameLength, 10);
- data.maximumUsernameLength = parseInt(meta.config.maximumUsernameLength, 10);
- data.minimumPasswordLength = parseInt(meta.config.minimumPasswordLength, 10);
- data.termsOfUse = tos.postData.content;
- data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[register:register]]'}]);
- data.regFormEntry = [];
- data.error = req.flash('error')[0];
- data.title = '[[pages:register]]';
-
- plugins.fireHook('filter:register.build', {req: req, res: res, templateData: data}, next);
}
- ], function(err, data) {
+ ], function (err, termsOfUse) {
if (err) {
return next(err);
}
- res.render('register', data.templateData);
+ var loginStrategies = require('../routes/authentication').getLoginStrategies();
+ var data = {
+ 'register_window:spansize': loginStrategies.length ? 'col-md-6' : 'col-md-12',
+ 'alternate_logins': !!loginStrategies.length
+ };
+
+ data.authentication = loginStrategies;
+
+ data.minimumUsernameLength = parseInt(meta.config.minimumUsernameLength, 10);
+ data.maximumUsernameLength = parseInt(meta.config.maximumUsernameLength, 10);
+ data.minimumPasswordLength = parseInt(meta.config.minimumPasswordLength, 10);
+ data.termsOfUse = termsOfUse.postData.content;
+ data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[register:register]]'}]);
+ data.regFormEntry = [];
+ data.error = req.flash('error')[0] || errorText;
+ data.title = '[[pages:register]]';
+
+ res.render('register', data);
});
};
-Controllers.compose = function(req, res, next) {
+Controllers.registerInterstitial = function (req, res, next) {
+ if (!req.session.hasOwnProperty('registration')) {
+ return res.redirect(nconf.get('relative_path') + '/register');
+ }
+
+ plugins.fireHook('filter:register.interstitial', {
+ userData: req.session.registration,
+ interstitials: []
+ }, function (err, data) {
+ if (err) {
+ return next(err);
+ }
+
+ if (!data.interstitials.length) {
+ return next();
+ }
+
+ var renders = data.interstitials.map(function (interstitial) {
+ return async.apply(req.app.render.bind(req.app), interstitial.template, interstitial.data || {});
+ });
+ 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
+ });
+ });
+ });
+};
+
+Controllers.compose = function (req, res, next) {
plugins.fireHook('filter:composer.build', {
req: req,
res: res,
next: next,
templateData: {}
- }, function(err, data) {
+ }, function (err, data) {
if (err) {
return next(err);
}
@@ -179,7 +252,7 @@ Controllers.compose = function(req, res, next) {
});
};
-Controllers.confirmEmail = function(req, res, next) {
+Controllers.confirmEmail = function (req, res) {
user.email.confirm(req.params.code, function (err) {
res.render('confirm', {
error: err ? err.message : '',
@@ -188,61 +261,6 @@ Controllers.confirmEmail = function(req, res, next) {
});
};
-Controllers.sitemap = {};
-Controllers.sitemap.render = function(req, res, next) {
- sitemap.render(function(err, tplData) {
- 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');
@@ -255,7 +273,7 @@ Controllers.robots = function (req, res) {
}
};
-Controllers.manifest = function(req, res) {
+Controllers.manifest = function (req, res) {
var manifest = {
name: meta.config.title || 'NodeBB',
start_url: nconf.get('relative_path') + '/',
@@ -301,13 +319,13 @@ Controllers.manifest = function(req, res) {
res.status(200).json(manifest);
};
-Controllers.outgoing = function(req, res, next) {
- var url = req.query.url,
- data = {
- url: validator.escape(url),
- title: meta.config.title,
- breadcrumbs: helpers.buildBreadcrumbs([{text: '[[notifications:outgoing_link]]'}])
- };
+Controllers.outgoing = function (req, res) {
+ var url = req.query.url || '';
+ var data = {
+ outgoing: validator.escape(String(url)),
+ title: meta.config.title,
+ breadcrumbs: helpers.buildBreadcrumbs([{text: '[[notifications:outgoing_link]]'}])
+ };
if (url) {
res.render('outgoing', data);
@@ -316,11 +334,115 @@ Controllers.outgoing = function(req, res, next) {
}
};
-Controllers.termsOfUse = function(req, res, next) {
+Controllers.termsOfUse = function (req, res, next) {
if (!meta.config.termsOfUse) {
return 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');
+ var isClientScript = new RegExp('^' + relativePath + '\\/src\\/.+\\.js');
+
+ if (plugins.hasListeners('action:meta.override404')) {
+ return plugins.fireHook('action:meta.override404', {
+ req: req,
+ res: res,
+ error: {}
+ });
+ }
+
+ if (isClientScript.test(req.url)) {
+ res.type('text/javascript').status(200).send('');
+ } else if (isLanguage.test(req.url)) {
+ res.status(200).json({});
+ } else if (req.path.startsWith(relativePath + '/uploads') || (req.get('accept') && req.get('accept').indexOf('text/html') === -1) || req.path === '/favicon.ico') {
+ meta.errors.log404(req.path || '');
+ res.sendStatus(404);
+ } else if (req.accepts('html')) {
+ if (process.env.NODE_ENV === 'development') {
+ winston.warn('Route requested but not found: ' + req.url);
+ }
+
+ meta.errors.log404(req.path.replace(/^\/api/, '') || '');
+ res.status(404);
+
+ var path = String(req.path || '');
+
+ if (res.locals.isAPI) {
+ return res.json({path: validator.escape(path.replace(/^\/api/, '')), title: '[[global:404.title]]'});
+ }
+ var middleware = require('../middleware');
+ middleware.buildHeader(req, res, function () {
+ res.render('404', {path: validator.escape(path), title: '[[global:404.title]]'});
+ });
+ } else {
+ res.status(404).type('txt').send('Not found');
+ }
+};
+
+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':
+ winston.error(req.path + '\n', err.message);
+ return res.sendStatus(403);
+ case 'blacklisted-ip':
+ return res.status(403).type('text/plain').send(err.message);
+ }
+
+ if (parseInt(err.status, 10) === 302 && err.path) {
+ return res.locals.isAPI ? res.status(302).json(err.path) : res.redirect(err.path);
+ }
+
+ winston.error(req.path + '\n', err.stack);
+
+ res.status(err.status || 500);
+
+ var path = String(req.path || '');
+ if (res.locals.isAPI) {
+ res.json({path: validator.escape(path), error: err.message});
+ } else {
+ var middleware = require('../middleware');
+ middleware.buildHeader(req, res, function () {
+ res.render('500', { path: validator.escape(path), error: validator.escape(String(err.message)) });
+ });
+ }
+};
+
module.exports = Controllers;
diff --git a/src/controllers/mods.js b/src/controllers/mods.js
new file mode 100644
index 0000000000..0079412f87
--- /dev/null
+++ b/src/controllers/mods.js
@@ -0,0 +1,27 @@
+"use strict";
+
+var async = require('async');
+
+var user = require('../user');
+var adminFlagsController = require('./admin/flags');
+
+var modsController = {};
+
+modsController.flagged = function (req, res, next) {
+ async.parallel({
+ isAdminOrGlobalMod: async.apply(user.isAdminOrGlobalMod, req.uid),
+ moderatedCids: async.apply(user.getModeratedCids, req.uid)
+ }, function (err, results) {
+ if (err || !(results.isAdminOrGlobalMod || !!results.moderatedCids.length)) {
+ return next(err);
+ }
+
+ if (!results.isAdminOrGlobalMod && results.moderatedCids.length) {
+ res.locals.cids = results.moderatedCids;
+ }
+
+ adminFlagsController.get(req, res, next);
+ });
+};
+
+module.exports = modsController;
diff --git a/src/controllers/popular.js b/src/controllers/popular.js
index 813f3527b8..f38edd594c 100644
--- a/src/controllers/popular.js
+++ b/src/controllers/popular.js
@@ -1,15 +1,15 @@
'use strict';
-var nconf = require('nconf'),
- topics = require('../topics'),
- plugins = require('../plugins'),
- meta = require('../meta'),
- helpers = require('./helpers');
+var nconf = require('nconf');
+var topics = require('../topics');
+var meta = require('../meta');
+var helpers = require('./helpers');
var popularController = {};
-var anonCache = {}, lastUpdateTime = 0;
+var anonCache = {};
+var lastUpdateTime = 0;
var terms = {
daily: 'day',
@@ -17,7 +17,7 @@ var terms = {
monthly: 'month'
};
-popularController.get = function(req, res, next) {
+popularController.get = function (req, res, next) {
var term = terms[req.params.term];
@@ -39,7 +39,7 @@ popularController.get = function(req, res, next) {
}
}
- topics.getPopular(term, req.uid, meta.config.topicsPerList, function(err, topics) {
+ topics.getPopular(term, req.uid, meta.config.topicsPerList, function (err, topics) {
if (err) {
return next(err);
}
@@ -48,7 +48,8 @@ popularController.get = function(req, res, next) {
topics: topics,
'feeds:disableRSS': parseInt(meta.config['feeds:disableRSS'], 10) === 1,
rssFeedUrl: nconf.get('relative_path') + '/popular/' + (req.params.term || 'daily') + '.rss',
- title: '[[pages:popular-' + term + ']]'
+ title: '[[pages:popular-' + term + ']]',
+ term: term
};
if (req.path.startsWith('/api/popular') || req.path.startsWith('/popular')) {
@@ -66,12 +67,7 @@ popularController.get = function(req, res, next) {
lastUpdateTime = Date.now();
}
- plugins.fireHook('filter:popular.build', {req: req, res: res, term: term, templateData: data}, function(err, data) {
- if (err) {
- return next(err);
- }
- res.render('popular', data.templateData);
- });
+ res.render('popular', data);
});
};
diff --git a/src/controllers/posts.js b/src/controllers/posts.js
new file mode 100644
index 0000000000..dae990e171
--- /dev/null
+++ b/src/controllers/posts.js
@@ -0,0 +1,24 @@
+"use strict";
+
+var posts = require('../posts');
+var helpers = require('./helpers');
+
+var postsController = {};
+
+postsController.redirectToPost = function (req, res, callback) {
+ var pid = parseInt(req.params.pid, 10);
+ if (!pid) {
+ return callback();
+ }
+
+ posts.generatePostPath(pid, req.uid, function (err, path) {
+ if (err || !path) {
+ return callback(err);
+ }
+
+ helpers.redirect(res, path);
+ });
+};
+
+
+module.exports = postsController;
diff --git a/src/controllers/recent.js b/src/controllers/recent.js
index 242d26ac12..72c0f45721 100644
--- a/src/controllers/recent.js
+++ b/src/controllers/recent.js
@@ -1,38 +1,69 @@
'use strict';
-var nconf = require('nconf');
var async = require('async');
+var nconf = require('nconf');
+
+var db = require('../database');
+var privileges = require('../privileges');
+var user = require('../user');
var topics = require('../topics');
var meta = require('../meta');
var helpers = require('./helpers');
-var plugins = require('../plugins');
+var pagination = require('../pagination');
var recentController = {};
-recentController.get = function(req, res, next) {
-
- var stop = (parseInt(meta.config.topicsPerList, 10) || 20) - 1;
+recentController.get = function (req, res, next) {
+ var page = parseInt(req.query.page, 10) || 1;
+ var pageCount = 1;
+ var stop = 0;
+ var topicCount = 0;
+ var settings;
async.waterfall([
function (next) {
- topics.getTopicsFromSet('topics:recent', req.uid, 0, stop, next);
+ async.parallel({
+ settings: function (next) {
+ user.getSettings(req.uid, next);
+ },
+ tids: function (next) {
+ db.getSortedSetRevRange('topics:recent', 0, 199, next);
+ }
+ }, next);
},
- function (data, next) {
- data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1;
- data.rssFeedUrl = nconf.get('relative_path') + '/recent.rss';
- data.title = '[[pages:recent]]';
- if (req.path.startsWith('/api/recent') || req.path.startsWith('/recent')) {
- data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[recent:title]]'}]);
- }
+ function (results, next) {
+ settings = results.settings;
+ privileges.topics.filterTids('read', results.tids, req.uid, next);
+ },
+ function (tids, next) {
+ var start = Math.max(0, (page - 1) * settings.topicsPerPage);
+ stop = start + settings.topicsPerPage - 1;
- plugins.fireHook('filter:recent.build', {req: req, res: res, templateData: data}, next);
+ topicCount = tids.length;
+ pageCount = Math.max(1, Math.ceil(topicCount / settings.topicsPerPage));
+ tids = tids.slice(start, stop + 1);
+
+ topics.getTopicsByTids(tids, req.uid, next);
}
- ], function(err, data) {
+ ], function (err, topics) {
if (err) {
return next(err);
}
- res.render('recent', data.templateData);
+
+ var data = {};
+ data.topics = topics;
+ data.nextStart = stop + 1;
+ data.set = 'topics:recent';
+ data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1;
+ data.rssFeedUrl = nconf.get('relative_path') + '/recent.rss';
+ data.title = '[[pages:recent]]';
+ data.pagination = pagination.create(page, pageCount);
+ if (req.path.startsWith('/api/recent') || req.path.startsWith('/recent')) {
+ data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[recent:title]]'}]);
+ }
+
+ res.render('recent', data);
});
};
diff --git a/src/controllers/search.js b/src/controllers/search.js
index dee14e7c1f..5967cfc88e 100644
--- a/src/controllers/search.js
+++ b/src/controllers/search.js
@@ -1,19 +1,19 @@
'use strict';
-var async = require('async'),
+var async = require('async');
- meta = require('../meta'),
- plugins = require('../plugins'),
- search = require('../search'),
- categories = require('../categories'),
- pagination = require('../pagination'),
- helpers = require('./helpers');
+var meta = require('../meta');
+var plugins = require('../plugins');
+var search = require('../search');
+var categories = require('../categories');
+var pagination = require('../pagination');
+var helpers = require('./helpers');
var searchController = {};
-searchController.search = function(req, res, next) {
+searchController.search = function (req, res, next) {
if (!plugins.hasListeners('filter:search.query')) {
return next();
}
@@ -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,69 +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) {
+ }, 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;
- plugins.fireHook('filter:search.build', {data: data, results: searchData}, function(err, data) {
- if (err) {
- return next(err);
- }
- res.render('search', data.results);
- });
+ 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..42b0ae1076
--- /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);
+ }
+
+ req.app.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 af8f6058c0..a433694220 100644
--- a/src/controllers/tags.js
+++ b/src/controllers/tags.js
@@ -5,15 +5,16 @@ var async = require('async');
var nconf = require('nconf');
var validator = require('validator');
-var meta = require('../meta');
+var user = require('../user');
var topics = require('../topics');
+var pagination = require('../pagination');
var helpers = require('./helpers');
var tagsController = {};
-tagsController.getTag = function(req, res, next) {
- var tag = validator.escape(req.params.tag);
- var stop = (parseInt(meta.config.topicsPerList, 10) || 20) - 1;
+tagsController.getTag = function (req, res, next) {
+ var tag = validator.escape(String(req.params.tag));
+ var page = parseInt(req.query.page, 10) || 1;
var templateData = {
topics: [],
@@ -21,20 +22,34 @@ tagsController.getTag = function(req, res, next) {
breadcrumbs: helpers.buildBreadcrumbs([{text: '[[tags:tags]]', url: '/tags'}, {text: tag}]),
title: '[[pages:tag, ' + tag + ']]'
};
-
+ var settings;
+ var topicCount = 0;
async.waterfall([
function (next) {
- topics.getTagTids(req.params.tag, 0, stop, next);
+ user.getSettings(req.uid, next);
},
- function (tids, next) {
- if (Array.isArray(tids) && !tids.length) {
- topics.deleteTag(req.params.tag);
+ function (_settings, next) {
+ settings = _settings;
+ var start = Math.max(0, (page - 1) * settings.topicsPerPage);
+ var stop = start + settings.topicsPerPage - 1;
+ templateData.nextStart = stop + 1;
+ async.parallel({
+ topicCount: function (next) {
+ topics.getTagTopicCount(tag, next);
+ },
+ tids: function (next) {
+ topics.getTagTids(req.params.tag, start, stop, next);
+ }
+ }, next);
+ },
+ function (results, next) {
+ if (Array.isArray(results.tids) && !results.tids.length) {
return res.render('tag', templateData);
}
-
- topics.getTopics(tids, req.uid, next);
+ topicCount = results.topicCount;
+ topics.getTopics(results.tids, req.uid, next);
}
- ], function(err, topics) {
+ ], function (err, topics) {
if (err) {
return next(err);
}
@@ -54,20 +69,20 @@ tagsController.getTag = function(req, res, next) {
}
];
templateData.topics = topics;
- templateData.nextStart = stop + 1;
+
+ var pageCount = Math.max(1, Math.ceil(topicCount / settings.topicsPerPage));
+ templateData.pagination = pagination.create(page, pageCount);
res.render('tag', templateData);
});
};
-tagsController.getTags = function(req, res, next) {
- topics.getTags(0, 99, function(err, tags) {
+tagsController.getTags = function (req, res, next) {
+ topics.getTags(0, 99, function (err, tags) {
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 c3c2847778..c739052937 100644
--- a/src/controllers/topics.js
+++ b/src/controllers/topics.js
@@ -17,9 +17,8 @@ var utils = require('../../public/src/utils');
var topicsController = {};
-topicsController.get = function(req, res, callback) {
+topicsController.get = function (req, res, callback) {
var tid = req.params.topic_id;
- var sort = req.query.sort;
var currentPage = parseInt(req.query.page, 10) || 1;
var pageCount = 1;
var userPrivileges;
@@ -32,13 +31,13 @@ topicsController.get = function(req, res, callback) {
async.waterfall([
function (next) {
async.parallel({
- privileges: function(next) {
+ privileges: function (next) {
privileges.topics.get(tid, req.uid, next);
},
- settings: function(next) {
+ settings: function (next) {
user.getSettings(req.uid, next);
},
- topic: function(next) {
+ topic: function (next) {
topics.getTopicData(tid, next);
}
}, next);
@@ -50,14 +49,17 @@ topicsController.get = function(req, res, callback) {
userPrivileges = results.privileges;
- if (!userPrivileges.read || (parseInt(results.topic.deleted, 10) && !userPrivileges.view_deleted)) {
+ if (!userPrivileges.read || !userPrivileges['topics:read'] || (parseInt(results.topic.deleted, 10) && !userPrivileges.view_deleted)) {
return helpers.notAllowed(req, res);
}
if (!res.locals.isAPI && (!req.params.slug || results.topic.slug !== tid + '/' + req.params.slug) && (results.topic.slug && results.topic.slug !== tid + '/')) {
var url = '/topic/' + results.topic.slug;
if (req.params.post_index){
- url += '/'+req.params.post_index;
+ url += '/' + req.params.post_index;
+ }
+ if (currentPage > 1) {
+ url += '?page=' + currentPage;
}
return helpers.redirect(res, url);
}
@@ -76,18 +78,13 @@ topicsController.get = function(req, res, callback) {
var set = 'tid:' + tid + ':posts';
var reverse = false;
-
// `sort` qs has priority over user setting
+ var sort = req.query.sort || settings.topicPostSort;
if (sort === 'newest_to_oldest') {
reverse = true;
} else if (sort === 'most_votes') {
reverse = true;
set = 'tid:' + tid + ':posts:votes';
- } else if (settings.topicPostSort === 'newest_to_oldest') {
- reverse = true;
- } else if (settings.topicPostSort === 'most_votes') {
- reverse = true;
- set = 'tid:' + tid + ':posts:votes';
}
var postIndex = 0;
@@ -97,7 +94,9 @@ topicsController.get = function(req, res, callback) {
req.params.post_index = 0;
}
if (!settings.usePagination) {
- currentPage = 1;
+ if (req.params.post_index !== 0) {
+ currentPage = 1;
+ }
if (reverse) {
postIndex = Math.max(0, postCount - (req.params.post_index || postCount) - Math.ceil(settings.postsPerPage / 2));
} else {
@@ -140,7 +139,7 @@ topicsController.get = function(req, res, callback) {
}
];
- helpers.buildCategoryBreadcrumbs(data.topicData.category.parentCid, function(err, crumbs) {
+ helpers.buildCategoryBreadcrumbs(data.topicData.category.parentCid, function (err, crumbs) {
if (err) {
return next(err);
}
@@ -150,7 +149,7 @@ topicsController.get = function(req, res, callback) {
},
function (topicData, next) {
function findPost(index) {
- for(var i=0; i data.pageCount)) {
+ req.query.page = Math.max(1, Math.min(data.pageCount, page));
+ return helpers.redirect(res, '/unread?' + querystring.stringify(req.query));
+ }
+
+ data.categories = results.watchedCategories.categories;
+ data.selectedCategory = results.watchedCategories.selectedCategory;
+
+ if (req.path.startsWith('/api/unread') || req.path.startsWith('/unread')) {
+ data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[unread:title]]'}]);
+ }
+
+ data.title = '[[pages:unread]]';
+ data.filters = [{
+ name: '[[unread:all-topics]]',
+ url: 'unread',
+ selected: filter === '',
+ filter: ''
+ }, {
+ name: '[[unread:new-topics]]',
+ url: 'unread/new',
+ selected: filter === 'new',
+ filter: 'new'
+ }, {
+ name: '[[unread:watched-topics]]',
+ url: 'unread/watched',
+ selected: filter === 'watched',
+ filter: 'watched'
+ }];
+
+ data.selectedFilter = data.filters.filter(function (filter) {
+ return filter && filter.selected;
+ })[0];
+
+ data.querystring = cid ? ('?cid=' + validator.escape(String(cid))) : '';
+
+ res.render('unread', data);
});
};
+function getWatchedCategories(uid, selectedCid, callback) {
+ async.waterfall([
+ function (next) {
+ user.getWatchedCategories(uid, next);
+ },
+ function (cids, next) {
+ privileges.categories.filterCids('read', cids, uid, next);
+ },
+ function (cids, 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;
+ });
-unreadController.unreadTotal = function(req, res, next) {
- topics.getTotalUnread(req.uid, function (err, data) {
+ var selectedCategory;
+ categoryData.forEach(function (category) {
+ category.selected = parseInt(category.cid, 10) === parseInt(selectedCid, 10);
+ if (category.selected) {
+ selectedCategory = category;
+ }
+ });
+
+ 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 || '';
+
+ if (!validFilter[filter]) {
+ return next();
+ }
+
+ topics.getTotalUnread(req.uid, filter, function (err, data) {
if (err) {
return next(err);
}
diff --git a/src/controllers/uploads.js b/src/controllers/uploads.js
index 59253c5666..f2896ec8f6 100644
--- a/src/controllers/uploads.js
+++ b/src/controllers/uploads.js
@@ -6,22 +6,19 @@ var async = require('async');
var nconf = require('nconf');
var validator = require('validator');
var winston = require('winston');
+var mime = require('mime');
var meta = require('../meta');
var file = require('../file');
var plugins = require('../plugins');
var image = require('../image');
+var privileges = require('../privileges');
var uploadsController = {};
-uploadsController.upload = function(req, res, filesIterator, next) {
+uploadsController.upload = function (req, res, filesIterator) {
var files = req.files.files;
- if (!req.user && meta.config.allowGuestUploads !== '1') {
- deleteTempFiles(files);
- return res.status(403).json('[[error:guest-upload-disabled]]');
- }
-
if (!Array.isArray(files)) {
return res.status(500).json('invalid files');
}
@@ -30,7 +27,7 @@ uploadsController.upload = function(req, res, filesIterator, next) {
files = files[0];
}
- async.map(files, filesIterator, function(err, images) {
+ async.map(files, filesIterator, function (err, images) {
deleteTempFiles(files);
if (err) {
@@ -43,39 +40,104 @@ uploadsController.upload = function(req, res, filesIterator, next) {
});
};
-uploadsController.uploadPost = function(req, res, next) {
- uploadsController.upload(req, res, function(uploadedFile, next) {
+uploadsController.uploadPost = function (req, res, next) {
+ uploadsController.upload(req, res, function (uploadedFile, next) {
var isImage = uploadedFile.type.match(/image./);
- if (isImage && plugins.hasListeners('filter:uploadImage')) {
- return plugins.fireHook('filter:uploadImage', {image: uploadedFile, uid: req.uid}, next);
+ if (isImage) {
+ uploadAsImage(req, uploadedFile, next);
+ } else {
+ uploadAsFile(req, uploadedFile, next);
}
-
- async.waterfall([
- function(next) {
- if (isImage) {
- file.isFileTypeAllowed(uploadedFile.path, next);
- } else {
- next();
- }
- },
- function (next) {
- if (parseInt(meta.config.allowFileUploads, 10) !== 1) {
- return next(new Error('[[error:uploads-are-disabled]]'));
- }
- uploadFile(req.uid, uploadedFile, next);
- }
- ], next);
}, next);
};
-uploadsController.uploadThumb = function(req, res, next) {
+function uploadAsImage(req, uploadedFile, callback) {
+ async.waterfall([
+ function (next) {
+ privileges.categories.can('upload:post:image', req.body.cid, req.uid, next);
+ },
+ function (canUpload, next) {
+ if (!canUpload) {
+ return next(new Error('[[error:no-privileges]]'));
+ }
+ if (plugins.hasListeners('filter:uploadImage')) {
+ return plugins.fireHook('filter:uploadImage', {image: uploadedFile, uid: req.uid}, callback);
+ }
+ file.isFileTypeAllowed(uploadedFile.path, next);
+ },
+ function (next) {
+ uploadFile(req.uid, uploadedFile, next);
+ },
+ function (fileObj, next) {
+ if (parseInt(meta.config.maximumImageWidth, 10) === 0) {
+ return next(null, fileObj);
+ }
+
+ resizeImage(fileObj, next);
+ }
+ ], callback);
+}
+
+function uploadAsFile(req, uploadedFile, callback) {
+ async.waterfall([
+ function (next) {
+ privileges.categories.can('upload:post:file', req.body.cid, req.uid, next);
+ },
+ function (canUpload, next) {
+ if (!canUpload) {
+ return next(new Error('[[error:no-privileges]]'));
+ }
+ if (parseInt(meta.config.allowFileUploads, 10) !== 1) {
+ return next(new Error('[[error:uploads-are-disabled]]'));
+ }
+ uploadFile(req.uid, uploadedFile, next);
+ }
+ ], callback);
+}
+
+function resizeImage(fileObj, callback) {
+ async.waterfall([
+ function (next) {
+ image.size(fileObj.path, next);
+ },
+ function (imageData, next) {
+ if (imageData.width < (parseInt(meta.config.maximumImageWidth, 10) || 760)) {
+ return callback(null, fileObj);
+ }
+
+ var dirname = path.dirname(fileObj.path);
+ var extname = path.extname(fileObj.path);
+ var basename = path.basename(fileObj.path, extname);
+
+ image.resizeImage({
+ path: fileObj.path,
+ target: path.join(dirname, basename + '-resized' + extname),
+ extension: extname,
+ width: parseInt(meta.config.maximumImageWidth, 10) || 760
+ }, next);
+ },
+ function (next) {
+
+ // Return the resized version to the composer/postData
+ var dirname = path.dirname(fileObj.url);
+ var extname = path.extname(fileObj.url);
+ var basename = path.basename(fileObj.url, extname);
+
+ fileObj.url = path.join(dirname, basename + '-resized' + extname);
+
+ next(null, fileObj);
+ }
+ ], callback);
+}
+
+uploadsController.uploadThumb = function (req, res, next) {
if (parseInt(meta.config.allowTopicsThumbnail, 10) !== 1) {
deleteTempFiles(req.files.files);
return next(new Error('[[error:topic-thumbnails-are-disabled]]'));
}
- uploadsController.upload(req, res, function(uploadedFile, next) {
- file.isFileTypeAllowed(uploadedFile.path, function(err) {
+ uploadsController.upload(req, res, function (uploadedFile, next) {
+ file.isFileTypeAllowed(uploadedFile.path, function (err) {
if (err) {
return next(err);
}
@@ -90,7 +152,7 @@ uploadsController.uploadThumb = function(req, res, next) {
extension: path.extname(uploadedFile.name),
width: size,
height: size
- }, function(err) {
+ }, function (err) {
if (err) {
return next(err);
}
@@ -105,7 +167,7 @@ uploadsController.uploadThumb = function(req, res, next) {
}, next);
};
-uploadsController.uploadGroupCover = function(uid, uploadedFile, callback) {
+uploadsController.uploadGroupCover = function (uid, uploadedFile, callback) {
if (plugins.hasListeners('filter:uploadImage')) {
return plugins.fireHook('filter:uploadImage', {image: uploadedFile, uid: uid}, callback);
}
@@ -114,7 +176,7 @@ uploadsController.uploadGroupCover = function(uid, uploadedFile, callback) {
return plugins.fireHook('filter:uploadFile', {file: uploadedFile, uid: uid}, callback);
}
- file.isFileTypeAllowed(uploadedFile.path, function(err) {
+ file.isFileTypeAllowed(uploadedFile.path, function (err) {
if (err) {
return callback(err);
}
@@ -138,6 +200,9 @@ function uploadFile(uid, uploadedFile, callback) {
if (meta.config.hasOwnProperty('allowedFileExtensions')) {
var allowed = file.allowedExtensions();
var extension = path.extname(uploadedFile.name);
+ if (!extension) {
+ extension = '.' + mime.extension(uploadedFile.type);
+ }
if (allowed.length > 0 && allowed.indexOf(extension) === -1) {
return callback(new Error('[[error:invalid-file-type, ' + allowed.join(', ') + ']]'));
}
@@ -147,24 +212,31 @@ function uploadFile(uid, uploadedFile, callback) {
}
function saveFileToLocal(uploadedFile, callback) {
+ var extension = path.extname(uploadedFile.name);
+ if (!extension && uploadedFile.type) {
+ extension = '.' + mime.extension(uploadedFile.type);
+ }
+
var filename = uploadedFile.name || 'upload';
- filename = Date.now() + '-' + validator.escape(filename).substr(0, 255);
- file.saveFileToLocal(filename, 'files', uploadedFile.path, function(err, upload) {
+ filename = Date.now() + '-' + validator.escape(filename.replace(extension, '')).substr(0, 255) + extension;
+
+ file.saveFileToLocal(filename, 'files', uploadedFile.path, function (err, upload) {
if (err) {
return callback(err);
}
callback(null, {
url: nconf.get('relative_path') + upload.url,
+ path: upload.path,
name: uploadedFile.name
});
});
}
function deleteTempFiles(files) {
- async.each(files, function(file, next) {
- fs.unlink(file.path, function(err) {
+ async.each(files, function (file, next) {
+ fs.unlink(file.path, function (err) {
if (err) {
winston.error(err);
}
diff --git a/src/controllers/users.js b/src/controllers/users.js
index 31ac0bd1cc..baf18a5b64 100644
--- a/src/controllers/users.js
+++ b/src/controllers/users.js
@@ -5,29 +5,79 @@ var user = require('../user');
var meta = require('../meta');
var pagination = require('../pagination');
-var plugins = require('../plugins');
var db = require('../database');
var helpers = require('./helpers');
var usersController = {};
-usersController.getOnlineUsers = function(req, res, next) {
+
+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({
- users: function(next) {
- usersController.getUsers('users:online', req.uid, req.query.page, next);
+ 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);
},
- guests: function(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, next);
+ },
+ guests: function (next) {
require('../socket.io/admin/rooms').getTotalGuestCount(next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return next(err);
}
var userData = results.users;
var hiddenCount = 0;
if (!userData.isAdminOrGlobalMod) {
- userData.users = userData.users.filter(function(user) {
+ userData.users = userData.users.filter(function (user) {
if (user && user.status === 'offline') {
hiddenCount ++;
}
@@ -41,23 +91,23 @@ usersController.getOnlineUsers = function(req, res, next) {
});
};
-usersController.getUsersSortedByPosts = function(req, res, next) {
+usersController.getUsersSortedByPosts = function (req, res, next) {
usersController.renderUsersPage('users:postcount', req, res, next);
};
-usersController.getUsersSortedByReputation = function(req, res, next) {
+usersController.getUsersSortedByReputation = function (req, res, next) {
if (parseInt(meta.config['reputation:disabled'], 10) === 1) {
return next();
}
usersController.renderUsersPage('users:reputation', req, res, next);
};
-usersController.getUsersSortedByJoinDate = function(req, res, next) {
+usersController.getUsersSortedByJoinDate = function (req, res, next) {
usersController.renderUsersPage('users:joindate', req, res, next);
};
-usersController.getBannedUsers = function(req, res, next) {
- usersController.getUsers('users:banned', req.uid, req.query.page, function(err, userData) {
+usersController.getBannedUsers = function (req, res, next) {
+ usersController.getUsers('users:banned', req.uid, req.query, function (err, userData) {
if (err) {
return next(err);
}
@@ -70,93 +120,103 @@ usersController.getBannedUsers = function(req, res, next) {
});
};
-usersController.renderUsersPage = function(set, req, res, next) {
- usersController.getUsers(set, req.uid, req.query.page, function(err, userData) {
+usersController.getFlaggedUsers = function (req, res, next) {
+ usersController.getUsers('users:flags', req.uid, req.query, function (err, userData) {
if (err) {
return next(err);
}
+
+ if (!userData.isAdminOrGlobalMod) {
+ return next();
+ }
+
render(req, res, userData, next);
});
};
-usersController.getUsers = function(set, uid, page, callback) {
- var setToTitles = {
- 'users:postcount': '[[pages:users/sort-posts]]',
- 'users:reputation': '[[pages:users/sort-reputation]]',
- 'users:joindate': '[[pages:users/latest]]',
- 'users:online': '[[pages:users/online]]',
- 'users:banned': '[[pages:users/banned]]'
+usersController.renderUsersPage = function (set, req, res, next) {
+ 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, 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]]'},
+ 'users:joindate': {title: '[[pages:users/latest]]', crumb: '[[global:users]]'},
+ 'users:online': {title: '[[pages:users/online]]', crumb: '[[global:online]]'},
+ 'users:banned': {title: '[[pages:users/banned]]', crumb: '[[user:banned]]'},
+ 'users:flags': {title: '[[pages:users/most-flags]]', crumb: '[[users:most_flags]]'},
};
- var setToCrumbs = {
- 'users:postcount': '[[users:top_posters]]',
- 'users:reputation': '[[users:most_reputation]]',
- 'users:joindate': '[[global:users]]',
- 'users:online': '[[global:online]]',
- 'users:banned': '[[user:banned]]'
- };
+ if (!setToData[set]) {
+ setToData[set] = {title: '', crumb: ''};
+ }
- var breadcrumbs = [{text: setToCrumbs[set]}];
+ 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);
+ isAdminOrGlobalMod: function (next) {
+ user.isAdminOrGlobalMod(uid, next);
},
- isGlobalMod: function(next) {
- user.isGlobalModerator(uid, next);
- },
- usersData: function(next) {
+ usersData: function (next) {
usersController.getUsersAndCount(set, uid, start, stop, next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return callback(err);
}
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),
- title: setToTitles[set] || '[[pages:users/latest]]',
+ 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);
});
};
-usersController.getUsersAndCount = function(set, uid, start, stop, callback) {
+usersController.getUsersAndCount = function (set, uid, start, stop, callback) {
async.parallel({
- users: function(next) {
+ users: function (next) {
user.getUsersFromSet(set, uid, start, stop, next);
},
- count: function(next) {
+ count: function (next) {
if (set === 'users:online') {
var now = Date.now();
db.sortedSetCount('users:online', now - 300000, '+inf', next);
} else if (set === 'users:banned') {
db.sortedSetCard('users:banned', next);
+ } else if (set === 'users:flags') {
+ db.sortedSetCard('users:flags', next);
} else {
db.getObjectField('global', 'userCount', next);
}
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return callback(err);
}
- results.users = results.users.filter(function(user) {
+ results.users = results.users.filter(function (user) {
return user && parseInt(user.uid, 10);
});
@@ -165,26 +225,22 @@ usersController.getUsersAndCount = function(set, uid, start, stop, callback) {
};
function render(req, res, data, next) {
- plugins.fireHook('filter:users.build', {req: req, res: res, templateData: data }, function(err, data) {
+ var registrationType = meta.config.registrationType;
+
+ data.maximumInvites = meta.config.maximumInvites;
+ data.inviteOnly = registrationType === 'invite-only' || registrationType === 'admin-invite-only';
+ data.adminInviteOnly = registrationType === 'admin-invite-only';
+ data['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1;
+
+ user.getInvitesNumber(req.uid, function (err, numInvites) {
if (err) {
return next(err);
}
- var registrationType = meta.config.registrationType;
+ res.append('X-Total-Count', data.userCount);
+ data.invites = numInvites;
- data.templateData.maximumInvites = meta.config.maximumInvites;
- data.templateData.inviteOnly = registrationType === 'invite-only' || registrationType === 'admin-invite-only';
- data.templateData.adminInviteOnly = registrationType === 'admin-invite-only';
- data.templateData['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1;
-
- user.getInvitesNumber(req.uid, function(err, num) {
- if (err) {
- return next(err);
- }
-
- data.templateData.invites = num;
- res.render('users', data.templateData);
- });
+ res.render('users', data);
});
}
diff --git a/src/coverPhoto.js b/src/coverPhoto.js
index d699ace785..699e4ee374 100644
--- a/src/coverPhoto.js
+++ b/src/coverPhoto.js
@@ -5,11 +5,11 @@ var meta = require('./meta');
var nconf = require('nconf');
-coverPhoto.getDefaultGroupCover = function(groupName) {
+coverPhoto.getDefaultGroupCover = function (groupName) {
return getCover('groups', groupName);
};
-coverPhoto.getDefaultProfileCover = function(uid) {
+coverPhoto.getDefaultProfileCover = function (uid) {
return getCover('profile', parseInt(uid, 10));
};
diff --git a/src/database/mongo.js b/src/database/mongo.js
index 0e7e8e2d2a..92bfd264dc 100644
--- a/src/database/mongo.js
+++ b/src/database/mongo.js
@@ -1,7 +1,7 @@
'use strict';
-(function(module) {
+(function (module) {
var winston = require('winston'),
async = require('async'),
@@ -33,20 +33,21 @@
name: 'mongo:password',
description: 'Password of your MongoDB database',
hidden: true,
- before: function(value) { value = value || nconf.get('mongo:password') || ''; return value; }
+ default: nconf.get('mongo:password') || '',
+ before: function (value) { value = value || nconf.get('mongo:password') || ''; return value; }
},
{
name: "mongo:database",
- description: "Which database to use",
- 'default': nconf.get('mongo:database') || 0
+ description: "MongoDB database name",
+ 'default': nconf.get('mongo:database') || 'nodebb'
}
];
module.helpers = module.helpers || {};
module.helpers.mongo = require('./mongo/helpers');
- module.init = function(callback) {
- callback = callback || function() {};
+ module.init = function (callback) {
+ callback = callback || function () {};
try {
var sessionStore;
mongoClient = require('mongodb').MongoClient;
@@ -74,7 +75,7 @@
nconf.set('mongo:port', 27017);
}
if (!nconf.get('mongo:database')) {
- nconf.set('mongo:database', '0');
+ nconf.set('mongo:database', 'nodebb');
}
var hosts = nconf.get('mongo:host').split(',');
@@ -95,7 +96,7 @@
connOptions = _.deepExtend((nconf.get('mongo:options') || {}), connOptions);
- mongoClient.connect(connString, connOptions, function(err, _db) {
+ mongoClient.connect(connString, connOptions, function (err, _db) {
if (err) {
winston.error("NodeBB could not connect to your Mongo database. Mongo returned the following error: " + err.message);
return callback(err);
@@ -110,8 +111,13 @@
db: db
});
} else {
+ // Initial Redis database
+ var rdb = require('./redis');
+ // Create a new redis connection and store it in module (skeleton)
+ rdb.client = rdb.connect();
+
module.sessionStore = new sessionStore({
- client: require('./redis').connect(),
+ client: rdb.client,
ttl: 60 * 60 * 24 * 14
});
}
@@ -141,7 +147,7 @@
async.apply(createIndex, 'objects', {_key: 1, score: -1}, {background: true}),
async.apply(createIndex, 'objects', {_key: 1, value: -1}, {background: true, unique: true, sparse: true}),
async.apply(createIndex, 'objects', {expireAt: 1}, {expireAfterSeconds: 0, background: true})
- ], function(err) {
+ ], function (err) {
if (err) {
winston.error('Error creating index ' + err.message);
}
@@ -155,7 +161,7 @@
});
};
- module.checkCompatibility = function(callback) {
+ module.checkCompatibility = function (callback) {
var mongoPkg = require.main.require('./node_modules/mongodb/package.json'),
err = semver.lt(mongoPkg.version, '2.0.0') ? new Error('The `mongodb` package is out-of-date, please run `./nodebb setup` again.') : null;
@@ -165,32 +171,32 @@
callback(err);
};
- module.info = function(db, callback) {
+ module.info = function (db, callback) {
async.parallel({
- serverStatus: function(next) {
+ serverStatus: function (next) {
db.command({'serverStatus': 1}, next);
},
- stats: function(next) {
+ stats: function (next) {
db.command({'dbStats': 1}, next);
},
- listCollections: function(next) {
- db.listCollections().toArray(function(err, items) {
+ listCollections: function (next) {
+ db.listCollections().toArray(function (err, items) {
if (err) {
return next(err);
}
- async.map(items, function(collection, next) {
+ async.map(items, function (collection, next) {
db.collection(collection.name).stats(next);
}, next);
});
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return callback(err);
}
var stats = results.stats;
var scale = 1024 * 1024;
- results.listCollections = results.listCollections.map(function(collectionInfo) {
+ results.listCollections = results.listCollections.map(function (collectionInfo) {
return {
name: collectionInfo.ns,
count: collectionInfo.count,
@@ -222,7 +228,7 @@
});
};
- module.close = function() {
+ module.close = function () {
db.close();
};
diff --git a/src/database/mongo/hash.js b/src/database/mongo/hash.js
index dbf294119e..7baf70b401 100644
--- a/src/database/mongo/hash.js
+++ b/src/database/mongo/hash.js
@@ -1,20 +1,20 @@
"use strict";
-module.exports = function(db, module) {
+module.exports = function (db, module) {
var helpers = module.helpers.mongo;
- module.setObject = function(key, data, callback) {
+ module.setObject = function (key, data, callback) {
callback = callback || helpers.noop;
if (!key) {
return callback();
}
- db.collection('objects').update({_key: key}, {$set: data}, {upsert: true, w: 1}, function(err) {
+ db.collection('objects').update({_key: key}, {$set: data}, {upsert: true, w: 1}, function (err) {
callback(err);
});
};
- module.setObjectField = function(key, field, value, callback) {
+ module.setObjectField = function (key, field, value, callback) {
callback = callback || helpers.noop;
if (!field) {
return callback();
@@ -25,18 +25,18 @@ module.exports = function(db, module) {
module.setObject(key, data, callback);
};
- module.getObject = function(key, callback) {
+ module.getObject = function (key, callback) {
if (!key) {
return callback();
}
db.collection('objects').findOne({_key: key}, {_id: 0, _key: 0}, callback);
};
- module.getObjects = function(keys, callback) {
+ module.getObjects = function (keys, callback) {
if (!Array.isArray(keys) || !keys.length) {
return callback(null, []);
}
- db.collection('objects').find({_key: {$in: keys}}, {_id: 0}).toArray(function(err, data) {
+ db.collection('objects').find({_key: {$in: keys}}, {_id: 0}).toArray(function (err, data) {
if (err) {
return callback(err);
}
@@ -44,7 +44,7 @@ module.exports = function(db, module) {
var map = helpers.toMap(data);
var returnData = [];
- 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) {
+ 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);
});
}
- module.sortedSetIncrBy = function(key, increment, value, callback) {
+ module.sortedSetIncrBy = function (key, increment, value, callback) {
callback = callback || helpers.noop;
if (!key) {
return callback();
}
var data = {};
- value = helpers.fieldToString(value);
+ value = helpers.valueToString(value);
data.score = parseInt(increment, 10);
- db.collection('objects').findAndModify({_key: key, value: value}, {}, {$inc: data}, {new: true, upsert: true}, function(err, result) {
+ db.collection('objects').findAndModify({_key: key, value: value}, {}, {$inc: data}, {new: true, upsert: true}, function (err, result) {
// if there is duplicate key error retry the upsert
// https://github.com/NodeBB/NodeBB/issues/4467
// https://jira.mongodb.org/browse/SERVER-14322
@@ -535,7 +578,7 @@ module.exports = function(db, module) {
});
};
- module.getSortedSetRangeByLex = function(key, min, max, start, count, callback) {
+ module.getSortedSetRangeByLex = function (key, min, max, start, count, callback) {
var query = {_key: key};
if (min !== '-') {
query.value = {$gte: min};
@@ -548,18 +591,18 @@ module.exports = function(db, module) {
.sort({value: 1})
.skip(start)
.limit(count === -1 ? 0 : count)
- .toArray(function(err, data) {
+ .toArray(function (err, data) {
if (err) {
return callback(err);
}
- data = data.map(function(item) {
+ data = data.map(function (item) {
return item && item.value;
});
callback(err, data);
});
};
- module.processSortedSet = function(setKey, process, batch, callback) {
+ module.processSortedSet = function (setKey, process, batch, callback) {
var done = false;
var ids = [];
var cursor = db.collection('objects').find({_key: setKey})
@@ -568,11 +611,11 @@ module.exports = function(db, module) {
.batchSize(batch);
async.whilst(
- function() {
+ function () {
return !done;
},
- function(next) {
- cursor.next(function(err, item) {
+ function (next) {
+ cursor.next(function (err, item) {
if (err) {
return next(err);
}
@@ -586,7 +629,7 @@ module.exports = function(db, module) {
return next(null);
}
- process(ids, function(err) {
+ process(ids, function (err) {
ids = [];
return next(err);
});
@@ -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 239090aa95..3b05148c9e 100644
--- a/src/database/redis.js
+++ b/src/database/redis.js
@@ -1,6 +1,6 @@
'use strict';
-(function(module) {
+(function (module) {
var winston = require('winston'),
nconf = require('nconf'),
@@ -25,7 +25,8 @@
name: 'redis:password',
description: 'Password of your Redis database',
hidden: true,
- before: function(value) { value = value || nconf.get('redis:password') || ''; return value; }
+ default: nconf.get('redis:password') || '',
+ before: function (value) { value = value || nconf.get('redis:password') || ''; return value; }
},
{
name: "redis:database",
@@ -34,7 +35,7 @@
}
];
- module.init = function(callback) {
+ module.init = function (callback) {
try {
redis = require('redis');
connectRedis = require('connect-redis')(session);
@@ -63,16 +64,19 @@
}
};
- module.connect = function(options) {
- var redis_socket_or_host = nconf.get('redis:host'),
- cxn, dbIdx;
-
- options = options || {};
+ module.connect = function (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);
@@ -90,9 +94,9 @@
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) {
+ cxn.select(dbIdx, function (error) {
if(error) {
winston.error("NodeBB could not connect to your Redis database. Redis returned the following error: " + error.message);
process.exit();
@@ -103,8 +107,8 @@
return cxn;
};
- module.checkCompatibility = function(callback) {
- module.info(module.client, function(err, info) {
+ module.checkCompatibility = function (callback) {
+ module.info(module.client, function (err, info) {
if (err) {
return callback(err);
}
@@ -118,11 +122,11 @@
});
};
- module.close = function() {
+ module.close = function () {
redisClient.quit();
};
- module.info = function(cxn, callback) {
+ module.info = function (cxn, callback) {
cxn.info(function (err, data) {
if (err) {
return callback(err);
diff --git a/src/database/redis/hash.js b/src/database/redis/hash.js
index ce79d0bc6f..a728852f1a 100644
--- a/src/database/redis/hash.js
+++ b/src/database/redis/hash.js
@@ -1,50 +1,50 @@
"use strict";
-module.exports = function(redisClient, module) {
+module.exports = function (redisClient, module) {
var helpers = module.helpers.redis;
- module.setObject = function(key, data, callback) {
- callback = callback || function() {};
- redisClient.hmset(key, data, function(err) {
+ module.setObject = function (key, data, callback) {
+ callback = callback || function () {};
+ redisClient.hmset(key, data, function (err) {
callback(err);
});
};
- module.setObjectField = function(key, field, value, callback) {
- callback = callback || function() {};
- redisClient.hset(key, field, value, function(err) {
+ module.setObjectField = function (key, field, value, callback) {
+ callback = callback || function () {};
+ redisClient.hset(key, field, value, function (err) {
callback(err);
});
};
- module.getObject = function(key, callback) {
+ module.getObject = function (key, callback) {
redisClient.hgetall(key, callback);
};
- module.getObjects = function(keys, callback) {
+ module.getObjects = function (keys, callback) {
helpers.multiKeys(redisClient, 'hgetall', keys, callback);
};
- module.getObjectField = function(key, field, callback) {
- module.getObjectFields(key, [field], function(err, data) {
+ module.getObjectField = function (key, field, callback) {
+ module.getObjectFields(key, [field], function (err, data) {
callback(err, data ? data[field] : null);
});
};
- module.getObjectFields = function(key, fields, callback) {
- module.getObjectsFields([key], fields, function(err, results) {
+ module.getObjectFields = function (key, fields, callback) {
+ module.getObjectsFields([key], fields, function (err, results) {
callback(err, results ? results[0] : null);
});
};
- module.getObjectsFields = function(keys, fields, callback) {
+ module.getObjectsFields = function (keys, fields, callback) {
if (!Array.isArray(fields) || !fields.length) {
- return callback(null, keys.map(function() { return {}; }));
+ return callback(null, keys.map(function () { return {}; }));
}
var multi = redisClient.multi();
- for(var x=0; x (parseInt(meta.config.maximumGroupNameLength, 10) || 255)) {
+ return callback(new Error('[[error:group-name-too-long]]'));
+ }
+
if (name.indexOf('/') !== -1) {
return callback(new Error('[[error:invalid-group-name]]'));
}
diff --git a/src/groups/delete.js b/src/groups/delete.js
index 8e665249a8..0838dd2407 100644
--- a/src/groups/delete.js
+++ b/src/groups/delete.js
@@ -1,14 +1,14 @@
'use strict';
-var async = require('async'),
- plugins = require('../plugins'),
- utils = require('../../public/src/utils'),
- db = require('./../database');
+var async = require('async');
+var plugins = require('../plugins');
+var utils = require('../../public/src/utils');
+var db = require('./../database');
-module.exports = function(Groups) {
+module.exports = function (Groups) {
- Groups.destroy = function(groupName, callback) {
- Groups.getGroupsData([groupName], function(err, groupsData) {
+ Groups.destroy = function (groupName, callback) {
+ Groups.getGroupsData([groupName], function (err, groupsData) {
if (err) {
return callback(err);
}
@@ -16,6 +16,7 @@ module.exports = function(Groups) {
return callback();
}
var groupObj = groupsData[0];
+
plugins.fireHook('action:group.destroy', groupObj);
async.parallel([
@@ -29,17 +30,23 @@ module.exports = function(Groups) {
async.apply(db.delete, 'group:' + groupName + ':invited'),
async.apply(db.delete, 'group:' + groupName + ':owners'),
async.apply(db.deleteObjectField, 'groupslug:groupname', utils.slugify(groupName)),
- function(next) {
- db.getSortedSetRange('groups:createtime', 0, -1, function(err, groups) {
+ function (next) {
+ db.getSortedSetRange('groups:createtime', 0, -1, function (err, groups) {
if (err) {
return next(err);
}
- async.each(groups, function(group, next) {
+ async.each(groups, function (group, next) {
db.sortedSetRemove('group:' + group + ':members', groupName, next);
}, next);
});
}
- ], callback);
+ ], function (err) {
+ if (err) {
+ return callback(err);
+ }
+ Groups.resetCache();
+ callback();
+ });
});
};
};
diff --git a/src/groups/membership.js b/src/groups/membership.js
index 747fa9d3d7..d03dba3e59 100644
--- a/src/groups/membership.js
+++ b/src/groups/membership.js
@@ -1,86 +1,112 @@
'use strict';
-var async = require('async'),
- winston = require('winston'),
- _ = require('underscore'),
+var async = require('async');
+var winston = require('winston');
+var _ = require('underscore');
- user = require('../user'),
- utils = require('../../public/src/utils'),
- plugins = require('../plugins'),
- notifications = require('../notifications'),
- db = require('./../database');
+var user = require('../user');
+var utils = require('../../public/src/utils');
+var plugins = require('../plugins');
+var notifications = require('../notifications');
+var db = require('../database');
-module.exports = function(Groups) {
- Groups.join = function(groupName, uid, callback) {
- function join() {
- var tasks = [
- async.apply(db.sortedSetAdd, 'group:' + groupName + ':members', Date.now(), uid),
- async.apply(db.incrObjectField, 'group:' + groupName, 'memberCount')
- ];
+var pubsub = require('../pubsub');
+var LRU = require('lru-cache');
- async.waterfall([
- function(next) {
- async.parallel({
- isAdmin: function(next) {
- user.isAdministrator(uid, next);
- },
- isHidden: function(next) {
- Groups.isHidden(groupName, next);
- }
- }, next);
- },
- function(results, next) {
- if (results.isAdmin) {
- tasks.push(async.apply(db.setAdd, 'group:' + groupName + ':owners', uid));
- }
- if (!results.isHidden) {
- tasks.push(async.apply(db.sortedSetIncrBy, 'groups:visible:memberCount', 1, groupName));
- }
- async.parallel(tasks, next);
- },
- function(results, next) {
- user.setGroupTitle(groupName, uid, next);
- },
- function(next) {
- plugins.fireHook('action:group.join', {
- groupName: groupName,
- uid: uid
- });
- next();
- }
- ], callback);
- }
+var cache = LRU({
+ max: 40000,
+ maxAge: 1000 * 60 * 60
+});
- callback = callback || function() {};
+module.exports = function (Groups) {
+
+ Groups.cache = cache;
+
+ Groups.join = function (groupName, uid, callback) {
+ callback = callback || function () {};
if (!groupName) {
return callback(new Error('[[error:invalid-data]]'));
}
- Groups.exists(groupName, function(err, exists) {
- if (err) {
+ async.waterfall([
+ function (next) {
+ Groups.isMember(uid, groupName, next);
+ },
+ function (isMember, next) {
+ if (isMember) {
+ return callback();
+ }
+ Groups.exists(groupName, next);
+ },
+ function (exists, next) {
+ if (exists) {
+ return next();
+ }
+ Groups.create({
+ name: groupName,
+ description: '',
+ hidden: 1
+ }, function (err) {
+ if (err && err.message !== '[[error:group-already-exists]]') {
+ winston.error('[groups.join] Could not create new hidden group: ' + err.message);
+ return callback(err);
+ }
+ next();
+ });
+ },
+ function (next) {
+ async.parallel({
+ isAdmin: function (next) {
+ user.isAdministrator(uid, next);
+ },
+ isHidden: function (next) {
+ Groups.isHidden(groupName, next);
+ }
+ }, next);
+ },
+ function (results, next) {
+ var tasks = [
+ async.apply(db.sortedSetAdd, 'group:' + groupName + ':members', Date.now(), uid),
+ async.apply(db.incrObjectField, 'group:' + groupName, 'memberCount')
+ ];
+ if (results.isAdmin) {
+ tasks.push(async.apply(db.setAdd, 'group:' + groupName + ':owners', uid));
+ }
+ if (!results.isHidden) {
+ tasks.push(async.apply(db.sortedSetIncrBy, 'groups:visible:memberCount', 1, groupName));
+ }
+ async.parallel(tasks, next);
+ },
+ function (results, next) {
+ clearCache(uid, groupName);
+ setGroupTitleIfNotSet(groupName, uid, next);
+ },
+ function (next) {
+ plugins.fireHook('action:group.join', {
+ groupName: groupName,
+ uid: uid
+ });
+ next();
+ }
+ ], callback);
+ };
+
+ function setGroupTitleIfNotSet(groupName, uid, callback) {
+ if (groupName === 'registered-users' || Groups.isPrivilegeGroup(groupName)) {
+ return callback();
+ }
+
+ db.getObjectField('user:' + uid, 'groupTitle', function (err, currentTitle) {
+ if (err || (currentTitle || currentTitle === '')) {
return callback(err);
}
- if (exists) {
- return join();
- }
-
- Groups.create({
- name: groupName,
- description: '',
- hidden: 1
- }, function(err) {
- if (err && err.message !== '[[error:group-already-exists]]') {
- winston.error('[groups.join] Could not create new hidden group: ' + err.message);
- return callback(err);
- }
- join();
- });
+ user.setUserField(uid, 'groupTitle', groupName, callback);
});
- };
+ }
- Groups.requestMembership = function(groupName, uid, callback) {
+ Groups.requestMembership = function (groupName, uid, callback) {
async.waterfall([
async.apply(inviteOrRequestMembership, groupName, uid, 'request'),
function (next) {
@@ -88,7 +114,7 @@ module.exports = function(Groups) {
},
function (username, next) {
async.parallel({
- notification: function(next) {
+ notification: function (next) {
notifications.create({
bodyShort: '[[groups:request.notification_title, ' + username + ']]',
bodyLong: '[[groups:request.notification_text, ' + username + ', ' + groupName + ']]',
@@ -97,7 +123,7 @@ module.exports = function(Groups) {
from: uid
}, next);
},
- owners: function(next) {
+ owners: function (next) {
Groups.getOwners(groupName, next);
}
}, next);
@@ -111,7 +137,7 @@ module.exports = function(Groups) {
], callback);
};
- Groups.acceptMembership = function(groupName, uid, callback) {
+ Groups.acceptMembership = function (groupName, uid, callback) {
// Note: For simplicity, this method intentially doesn't check the caller uid for ownership!
async.waterfall([
async.apply(db.setRemove, 'group:' + groupName + ':pending', uid),
@@ -120,7 +146,7 @@ module.exports = function(Groups) {
], callback);
};
- Groups.rejectMembership = function(groupName, uid, callback) {
+ Groups.rejectMembership = function (groupName, uid, callback) {
// Note: For simplicity, this method intentially doesn't check the caller uid for ownership!
async.parallel([
async.apply(db.setRemove, 'group:' + groupName + ':pending', uid),
@@ -128,7 +154,7 @@ module.exports = function(Groups) {
], callback);
};
- Groups.invite = function(groupName, uid, callback) {
+ Groups.invite = function (groupName, uid, callback) {
async.waterfall([
async.apply(inviteOrRequestMembership, groupName, uid, 'invite'),
async.apply(notifications.create, {
@@ -138,11 +164,7 @@ module.exports = function(Groups) {
path: '/groups/' + utils.slugify(groupName)
}),
function (notification, next) {
- if (!notification) {
- return next();
- }
-
- notifications.push(notification, [uid]);
+ notifications.push(notification, [uid], next);
}
], callback);
};
@@ -155,7 +177,7 @@ module.exports = function(Groups) {
var set = type === 'invite' ? 'group:' + groupName + ':invited' : 'group:' + groupName + ':pending';
async.waterfall([
- function(next) {
+ function (next) {
async.parallel({
exists: async.apply(Groups.exists, groupName),
isMember: async.apply(Groups.isMember, uid, groupName),
@@ -163,20 +185,20 @@ module.exports = function(Groups) {
isInvited: async.apply(Groups.isInvited, uid, groupName)
}, next);
},
- function(checks, next) {
+ function (checks, next) {
if (!checks.exists) {
return next(new Error('[[error:no-group]]'));
} else if (checks.isMember) {
- return next(new Error('[[error:group-already-member]]'));
+ return callback();
} else if (type === 'invite' && checks.isInvited) {
- return next(new Error('[[error:group-already-invited]]'));
+ return callback();
} else if (type === 'request' && checks.isPending) {
return next(new Error('[[error:group-already-requested]]'));
}
db.setAdd(set, uid, next);
},
- function(next) {
+ function (next) {
plugins.fireHook(hookName, {
groupName: groupName,
uid: uid
@@ -186,53 +208,68 @@ module.exports = function(Groups) {
], callback);
}
- Groups.leave = function(groupName, uid, callback) {
- callback = callback || function() {};
+ Groups.leave = function (groupName, uid, callback) {
+ callback = callback || function () {};
- var tasks = [
- async.apply(db.sortedSetRemove, 'group:' + groupName + ':members', uid),
- async.apply(db.setRemove, 'group:' + groupName + ':owners', uid),
- async.apply(db.decrObjectField, 'group:' + groupName, 'memberCount')
- ];
-
- async.parallel(tasks, function(err) {
- if (err) {
- return callback(err);
- }
-
- plugins.fireHook('action:group.leave', {
- groupName: groupName,
- uid: uid
- });
-
- Groups.getGroupFields(groupName, ['hidden', 'memberCount'], function(err, groupData) {
- if (err || !groupData) {
- return callback(err);
+ async.waterfall([
+ function (next) {
+ Groups.isMember(uid, groupName, next);
+ },
+ function (isMember, next) {
+ if (!isMember) {
+ return callback();
}
- if (parseInt(groupData.hidden, 10) === 1 && parseInt(groupData.memberCount, 10) === 0) {
- Groups.destroy(groupName, callback);
+ Groups.exists(groupName, next);
+ },
+ function (exists, next) {
+ if (!exists) {
+ return callback();
+ }
+ async.parallel([
+ async.apply(db.sortedSetRemove, 'group:' + groupName + ':members', uid),
+ async.apply(db.setRemove, 'group:' + groupName + ':owners', uid),
+ async.apply(db.decrObjectField, 'group:' + groupName, 'memberCount')
+ ], next);
+ },
+ function (results, next) {
+ clearCache(uid, groupName);
+ Groups.getGroupFields(groupName, ['hidden', 'memberCount'], next);
+ },
+ function (groupData, next) {
+ if (!groupData) {
+ return callback();
+ }
+ if (Groups.isPrivilegeGroup(groupName) && parseInt(groupData.memberCount, 10) === 0) {
+ Groups.destroy(groupName, next);
} else {
if (parseInt(groupData.hidden, 10) !== 1) {
- db.sortedSetAdd('groups:visible:memberCount', groupData.memberCount, groupName, callback);
+ db.sortedSetAdd('groups:visible:memberCount', groupData.memberCount, groupName, next);
} else {
- callback();
+ next();
}
}
- });
- });
+ },
+ function (next) {
+ plugins.fireHook('action:group.leave', {
+ groupName: groupName,
+ uid: uid
+ });
+ next();
+ }
+ ], callback);
};
- Groups.leaveAllGroups = function(uid, callback) {
+ Groups.leaveAllGroups = function (uid, callback) {
async.waterfall([
- function(next) {
+ function (next) {
db.getSortedSetRange('groups:createtime', 0, -1, next);
},
- function(groups, next) {
- async.each(groups, function(groupName, next) {
+ function (groups, next) {
+ async.each(groups, function (groupName, next) {
async.parallel([
- function(next) {
- Groups.isMember(uid, groupName, function(err, isMember) {
+ function (next) {
+ Groups.isMember(uid, groupName, function (err, isMember) {
if (!err && isMember) {
Groups.leave(groupName, uid, next);
} else {
@@ -240,7 +277,7 @@ module.exports = function(Groups) {
}
});
},
- function(next) {
+ function (next) {
Groups.rejectMembership(groupName, uid, next);
}
], next);
@@ -249,13 +286,13 @@ module.exports = function(Groups) {
], callback);
};
- Groups.getMembers = function(groupName, start, stop, callback) {
+ Groups.getMembers = function (groupName, start, stop, callback) {
db.getSortedSetRevRange('group:' + groupName + ':members', start, stop, callback);
};
- Groups.getMemberUsers = function(groupNames, start, stop, callback) {
- async.map(groupNames, function(groupName, next) {
- Groups.getMembers(groupName, start, stop, function(err, uids) {
+ Groups.getMemberUsers = function (groupNames, start, stop, callback) {
+ async.map(groupNames, function (groupName, next) {
+ Groups.getMembers(groupName, start, stop, function (err, uids) {
if (err) {
return next(err);
}
@@ -265,36 +302,129 @@ module.exports = function(Groups) {
}, callback);
};
- Groups.getMembersOfGroups = function(groupNames, callback) {
- db.getSortedSetsMembers(groupNames.map(function(name) {
+ Groups.getMembersOfGroups = function (groupNames, callback) {
+ db.getSortedSetsMembers(groupNames.map(function (name) {
return 'group:' + name + ':members';
}), callback);
};
- Groups.isMember = function(uid, groupName, 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);
- };
- Groups.isMembers = function(uids, groupName, callback) {
- db.isSortedSetMembers('group:' + groupName + ':members', uids, callback);
- };
-
- Groups.isMemberOfGroups = function(uid, groups, callback) {
- if (!uid || parseInt(uid, 10) <= 0) {
- return callback(null, groups.map(function() {return false;}));
+ var cacheKey = uid + ':' + groupName;
+ if (cache.has(cacheKey)) {
+ return process.nextTick(callback, null, cache.get(cacheKey));
}
- groups = groups.map(function(groupName) {
+
+ 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) {
+ 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 || !groups.length) {
+ return callback(null, groups.map(function () {return false;}));
+ }
+
+ 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) {
- db.getObjectField('group:' + groupName, 'memberCount', function(err, count) {
+ Groups.getMemberCount = function (groupName, callback) {
+ db.getObjectField('group:' + groupName, 'memberCount', function (err, count) {
if (err) {
return callback(err);
}
@@ -302,8 +432,8 @@ module.exports = function(Groups) {
});
};
- Groups.isMemberOfGroupList = function(uid, groupListKey, callback) {
- db.getSortedSetRange('group:' + groupListKey + ':members', 0, -1, function(err, groupNames) {
+ Groups.isMemberOfGroupList = function (uid, groupListKey, callback) {
+ db.getSortedSetRange('group:' + groupListKey + ':members', 0, -1, function (err, groupNames) {
if (err) {
return callback(err);
}
@@ -312,7 +442,7 @@ module.exports = function(Groups) {
return callback(null, false);
}
- Groups.isMemberOfGroups(uid, groupNames, function(err, isMembers) {
+ Groups.isMemberOfGroups(uid, groupNames, function (err, isMembers) {
if (err) {
return callback(err);
}
@@ -322,12 +452,12 @@ module.exports = function(Groups) {
});
};
- Groups.isMemberOfGroupsList = function(uid, groupListKeys, callback) {
- var sets = groupListKeys.map(function(groupName) {
+ Groups.isMemberOfGroupsList = function (uid, groupListKeys, callback) {
+ var sets = groupListKeys.map(function (groupName) {
return 'group:' + groupName + ':members';
});
- db.getSortedSetsMembers(sets, function(err, members) {
+ db.getSortedSetsMembers(sets, function (err, members) {
if (err) {
return callback(err);
}
@@ -335,19 +465,19 @@ module.exports = function(Groups) {
var uniqueGroups = _.unique(_.flatten(members));
uniqueGroups = Groups.internals.removeEphemeralGroups(uniqueGroups);
- Groups.isMemberOfGroups(uid, uniqueGroups, function(err, isMembers) {
+ Groups.isMemberOfGroups(uid, uniqueGroups, function (err, isMembers) {
if (err) {
return callback(err);
}
var map = {};
- uniqueGroups.forEach(function(groupName, index) {
+ uniqueGroups.forEach(function (groupName, index) {
map[groupName] = isMembers[index];
});
- var result = members.map(function(groupNames) {
- for (var i=0; i b.slug;
- }).sort(function(a, b) {
+ }).sort(function (a, b) {
return b.memberCount - a.memberCount;
});
break;
case 'date':
- groups = groups.sort(function(a, b) {
+ groups = groups.sort(function (a, b) {
return b.createtime - a.createtime;
});
break;
case 'alpha': // intentional fall-through
default:
- groups = groups.sort(function(a, b) {
+ groups = groups.sort(function (a, b) {
return a.slug > b.slug ? 1 : -1;
});
}
@@ -63,7 +63,7 @@ module.exports = function(Groups) {
next(null, groups);
};
- Groups.searchMembers = function(data, callback) {
+ Groups.searchMembers = function (data, callback) {
function findUids(query, searchBy, callback) {
if (!query) {
@@ -73,15 +73,15 @@ module.exports = function(Groups) {
query = query.toLowerCase();
async.waterfall([
- function(next) {
+ function (next) {
Groups.getMembers(data.groupName, 0, -1, next);
},
- function(members, next) {
+ function (members, next) {
user.getUsersFields(members, ['uid'].concat([searchBy]), next);
},
- function(users, next) {
+ function (users, next) {
var uids = [];
- for(var i=0; i origRatio) {
- desiredRatio = 1/desiredRatio;
- }
- if (origRatio >= 1) {
- y = 0; // height is the smaller dimension here
- x = Math.floor((w/2) - (h * desiredRatio / 2));
- crop = async.apply(image.crop.bind(image), x, y, h * desiredRatio, h);
+ if (origRatio !== desiredRatio) {
+ if (desiredRatio > origRatio) {
+ desiredRatio = 1 / desiredRatio;
+ }
+ if (origRatio >= 1) {
+ y = 0; // height is the smaller dimension here
+ x = Math.floor((w / 2) - (h * desiredRatio / 2));
+ crop = async.apply(image.crop.bind(image), x, y, h * desiredRatio, h);
+ } else {
+ x = 0; // width is the smaller dimension here
+ y = Math.floor(h / 2 - (w * desiredRatio / 2));
+ crop = async.apply(image.crop.bind(image), x, y, w, w * desiredRatio);
+ }
} else {
- x = 0; // width is the smaller dimension here
- y = Math.floor(h/2 - (w * desiredRatio / 2));
- crop = async.apply(image.crop.bind(image), x, y, w, w * desiredRatio);
+ // Simple resize given either width, height, or both
+ crop = async.apply(setImmediate);
}
async.waterfall([
crop,
- function(image, next) {
- image.resize(data.width, data.height, next);
+ function (_image, next) {
+ if (typeof _image === 'function' && !next) {
+ next = _image;
+ _image = image;
+ }
+
+ if ((data.width && data.height) || (w > data.width) || (h > data.height)) {
+ _image.resize(data.width || Jimp.AUTO, data.height || Jimp.AUTO, next);
+ } else {
+ next(null, image);
+ }
},
- function(image, next) {
+ function (image, next) {
image.write(data.target || data.path, next);
}
- ], function(err) {
+ ], function (err) {
callback(err);
});
});
}
};
-image.normalise = function(path, extension, callback) {
+image.normalise = function (path, extension, callback) {
if (plugins.hasListeners('filter:image.normalise')) {
plugins.fireHook('filter:image.normalise', {
path: path,
extension: extension
- }, function(err, data) {
+ }, function (err) {
callback(err);
});
} else {
- new Jimp(path, function(err, image) {
+ new Jimp(path, function (err, image) {
if (err) {
return callback(err);
}
- image.write(path + '.png', function(err) {
+ image.write(path + '.png', function (err) {
callback(err);
});
});
}
};
-image.convertImageToBase64 = function(path, callback) {
- fs.readFile(path, function(err, data) {
+image.size = function (path, callback) {
+ if (plugins.hasListeners('filter:image.size')) {
+ plugins.fireHook('filter:image.size', {
+ path: path,
+ }, function (err, image) {
+ callback(err, image);
+ });
+ } else {
+ new Jimp(path, function (err, data) {
+ 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 aae92e5c23..711932bb00 100644
--- a/src/install.js
+++ b/src/install.js
@@ -43,8 +43,8 @@ questions.optional = [
];
function checkSetupFlag(next) {
- var envSetupKeys = ['database'],
- setupVal;
+ var setupVal;
+
try {
if (nconf.get('setup')) {
setupVal = JSON.parse(nconf.get('setup'));
@@ -74,14 +74,10 @@ function checkSetupFlag(next) {
process.exit();
}
- } else if (envSetupKeys.every(function(key) {
- return nconf.stores.env.store.hasOwnProperty(key);
- })) {
- install.values = envSetupKeys.reduce(function(config, key) {
- config[key] = nconf.stores.env.store[key];
- return config;
- }, {});
-
+ } else if (nconf.get('database')) {
+ install.values = {
+ database: nconf.get('database')
+ };
next();
} else {
next();
@@ -129,14 +125,14 @@ function setupConfig(next) {
prompt.colors = false;
if (!install.values) {
- prompt.get(questions.main, function(err, config) {
+ prompt.get(questions.main, function (err, config) {
if (err) {
process.stdout.write('\n\n');
winston.warn('NodeBB setup ' + err.message);
process.exit();
}
- configureDatabases(config, function(err, config) {
+ configureDatabases(config, function (err, config) {
completeConfigSetup(err, config, next);
});
});
@@ -151,7 +147,7 @@ function setupConfig(next) {
config[question.name] = install.values[question.name] || question['default'] || undefined;
});
- configureDatabases(config, function(err, config) {
+ configureDatabases(config, function (err, config) {
completeConfigSetup(err, config, next);
});
}
@@ -172,7 +168,7 @@ function completeConfigSetup(err, config, next) {
}
}
- install.save(config, function(err) {
+ install.save(config, function (err) {
if (err) {
return next(err);
}
@@ -183,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);
}
@@ -200,7 +194,7 @@ function setupDefaultConfigs(next) {
function enableDefaultTheme(next) {
var meta = require('./meta');
- meta.configs.get('theme:id', function(err, id) {
+ meta.configs.get('theme:id', function (err, id) {
if (err || id) {
process.stdout.write('Previous theme detected, skipping enabling default theme\n');
return next(err);
@@ -260,7 +254,10 @@ function createAdmin(callback) {
hidden: true,
type: 'string'
}],
- success = function(err, results) {
+ success = function (err, results) {
+ if (err) {
+ return callback(err);
+ }
if (!results) {
return callback(new Error('aborted'));
}
@@ -271,20 +268,20 @@ function createAdmin(callback) {
}
var adminUid;
async.waterfall([
- function(next) {
+ function (next) {
User.create({username: results.username, password: results.password, email: results.email}, next);
},
- function(uid, next) {
+ function (uid, next) {
adminUid = uid;
Groups.join('administrators', uid, next);
},
- function(next) {
+ function (next) {
Groups.show('administrators', next);
},
- function(next) {
+ function (next) {
Groups.ownership.grant(adminUid, 'administrators', next);
}
- ], function(err) {
+ ], function (err) {
if (err) {
return callback(err);
}
@@ -385,7 +382,7 @@ function createCategories(next) {
function createMenuItems(next) {
var db = require('./database');
- db.exists('navigation:enabled', function(err, exists) {
+ db.exists('navigation:enabled', function (err, exists) {
if (err || exists) {
return next(err);
}
@@ -401,13 +398,13 @@ function createWelcomePost(next) {
Topics = require('./topics');
async.parallel([
- function(next) {
+ function (next) {
fs.readFile(path.join(__dirname, '../', 'install/data/welcome.md'), next);
},
- function(next) {
+ function (next) {
db.getObjectField('global', 'topicCount', next);
}
- ], function(err, results) {
+ ], function (err, results) {
if (err) {
return next(err);
}
@@ -441,7 +438,7 @@ function enableDefaultPlugins(next) {
'nodebb-rewards-essentials',
'nodebb-plugin-soundpack-default',
'nodebb-plugin-emoji-extended',
- 'nodebb-plugin-emoji-apple'
+ 'nodebb-plugin-emoji-one'
],
customDefaults = nconf.get('defaultPlugins');
@@ -457,14 +454,14 @@ function enableDefaultPlugins(next) {
}
}
- defaultEnabled = defaultEnabled.filter(function(plugin, index, array) {
+ defaultEnabled = defaultEnabled.filter(function (plugin, index, array) {
return array.indexOf(plugin) === index;
});
winston.info('[install/enableDefaultPlugins] activating default plugins', defaultEnabled);
var db = require('./database');
- var order = defaultEnabled.map(function(plugin, index) {
+ var order = defaultEnabled.map(function (plugin, index) {
return index;
});
db.sortedSetAdd('plugins:active', order, defaultEnabled, next);
@@ -473,13 +470,13 @@ function enableDefaultPlugins(next) {
function setCopyrightWidget(next) {
var db = require('./database');
async.parallel({
- footerJSON: function(next) {
+ footerJSON: function (next) {
fs.readFile(path.join(__dirname, '../', 'install/data/footer.json'), next);
},
- footer: function(next) {
+ footer: function (next) {
db.getObjectField('widgets:global', 'footer', next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return next(err);
}
@@ -510,7 +507,7 @@ install.setup = function (callback) {
setCopyrightWidget,
function (next) {
var upgrade = require('./upgrade');
- upgrade.check(function(err, uptodate) {
+ upgrade.check(function (err, uptodate) {
if (err) {
return next(err);
}
diff --git a/src/languages.js b/src/languages.js
index de1d82bcff..51def2f922 100644
--- a/src/languages.js
+++ b/src/languages.js
@@ -3,20 +3,67 @@
var fs = require('fs'),
path = require('path'),
async = require('async'),
+ LRU = require('lru-cache'),
+ _ = require('underscore');
- Languages = {};
+var plugins = require('./plugins');
-Languages.list = function(callback) {
+var Languages = {};
+
+Languages.init = function (next) {
+ if (Languages.hasOwnProperty('_cache')) {
+ Languages._cache.reset();
+ } else {
+ Languages._cache = LRU(100);
+ }
+
+ next();
+};
+
+Languages.get = function (code, key, callback) {
+ var combined = [code, key].join('/');
+
+ if (Languages._cache && Languages._cache.has(combined)) {
+ return callback(null, Languages._cache.get(combined));
+ }
+
+ 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) || {};
+ } catch (e) {
+ languageData = {};
+ }
+
+ if (plugins.customLanguages.hasOwnProperty(combined)) {
+ _.extendOwn(languageData, plugins.customLanguages[combined]);
+ }
+
+ if (Languages._cache) {
+ Languages._cache.set(combined, languageData);
+ }
+
+ callback(null, languageData);
+ });
+};
+
+Languages.list = function (callback) {
var languagesPath = path.join(__dirname, '../public/language'),
languages = [];
- fs.readdir(languagesPath, function(err, files) {
+ fs.readdir(languagesPath, function (err, files) {
if (err) {
return callback(err);
}
- async.each(files, function(folder, next) {
- fs.stat(path.join(languagesPath, folder), function(err, stat) {
+ async.each(files, function (folder, next) {
+ fs.stat(path.join(languagesPath, folder), function (err, stat) {
if (err) {
return next(err);
}
@@ -27,7 +74,7 @@ Languages.list = function(callback) {
var configPath = path.join(languagesPath, folder, 'language.json');
- fs.readFile(configPath, function(err, stream) {
+ fs.readFile(configPath, function (err, stream) {
if (err) {
next();
}
@@ -35,12 +82,12 @@ Languages.list = function(callback) {
next();
});
});
- }, function(err) {
+ }, function (err) {
if (err) {
return callback(err);
}
// Sort alphabetically
- languages = languages.sort(function(a, b) {
+ languages = languages.sort(function (a, b) {
return a.code > b.code ? 1 : -1;
});
diff --git a/src/logger.js b/src/logger.js
index e646046bd9..4d6b8f717c 100644
--- a/src/logger.js
+++ b/src/logger.js
@@ -30,20 +30,20 @@ var opts = {
/* -- Logger -- */
-(function(Logger) {
+(function (Logger) {
- Logger.init = function(app) {
+ Logger.init = function (app) {
opts.express.app = app;
/* Open log file stream & initialize express logging if meta.config.logger* variables are set */
Logger.setup();
};
- Logger.setup = function() {
+ Logger.setup = function () {
Logger.setup_one('loggerPath', meta.config.loggerPath);
};
- Logger.setup_one = function(key, value) {
+ Logger.setup_one = function (key, value) {
/*
* 1. Open the logger stream: stdout or file
* 2. Re-initialize the express logger hijack
@@ -54,7 +54,7 @@ var opts = {
}
};
- Logger.setup_one_log = function(value) {
+ Logger.setup_one_log = function (value) {
/*
* If logging is currently enabled, create a stream.
* Otherwise, close the current stream
@@ -72,7 +72,7 @@ var opts = {
}
};
- Logger.open = function(value) {
+ Logger.open = function (value) {
/* Open the streams to log to: either a path or stdout */
var stream;
if(value) {
@@ -91,7 +91,7 @@ var opts = {
}
if(stream) {
- stream.on('error', function(err) {
+ stream.on('error', function (err) {
winston.error(err.message);
});
}
@@ -101,14 +101,14 @@ var opts = {
return stream;
};
- Logger.close = function(stream) {
+ Logger.close = function (stream) {
if(stream.f !== process.stdout && stream.f) {
stream.end();
}
stream.f = null;
};
- Logger.monitorConfig = function(socket, data) {
+ Logger.monitorConfig = function (socket, data) {
/*
* This monitor's when a user clicks "save" in the Logger section of the admin panel
*/
@@ -117,7 +117,7 @@ var opts = {
Logger.io(socket);
};
- Logger.express_open = function() {
+ Logger.express_open = function () {
if(opts.express.set !== 1) {
opts.express.set = 1;
opts.express.app.use(Logger.expressLogger);
@@ -128,7 +128,7 @@ var opts = {
opts.express.ofn = morgan('combined', {stream : opts.streams.log.f});
};
- Logger.expressLogger = function(req,res,next) {
+ Logger.expressLogger = function (req,res,next) {
/*
* The new express.logger
*
@@ -141,21 +141,21 @@ var opts = {
}
};
- Logger.prepare_io_string = function(_type, _uid, _args) {
+ Logger.prepare_io_string = function (_type, _uid, _args) {
/*
* This prepares the output string for intercepted socket.io events
*
* The format is: io:
*/
try {
- return 'io: '+_uid+' '+_type+' '+util.inspect(Array.prototype.slice.call(_args))+'\n';
+ return 'io: ' + _uid + ' ' + _type + ' ' + util.inspect(Array.prototype.slice.call(_args)) + '\n';
} catch(err) {
winston.info("Logger.prepare_io_string: Failed", err);
return "error";
}
};
- Logger.io_close = function(socket) {
+ Logger.io_close = function (socket) {
/*
* Restore all hijacked sockets to their original emit/on functions
*/
@@ -177,7 +177,7 @@ var opts = {
}
};
- Logger.io = function(socket) {
+ Logger.io = function (socket) {
/*
* Go through all of the currently established sockets & hook their .emit/.on
*/
@@ -194,12 +194,12 @@ var opts = {
}
};
- Logger.io_one = function(socket, uid) {
+ Logger.io_one = function (socket, uid) {
/*
* This function replaces a socket's .emit/.on functions in order to intercept events
*/
function override(method, name, errorMsg) {
- return function() {
+ return function () {
if(opts.streams.log.f) {
opts.streams.log.f.write(Logger.prepare_io_string(name, uid, arguments));
}
diff --git a/src/messaging.js b/src/messaging.js
index 1974313c81..a88ceabbbe 100644
--- a/src/messaging.js
+++ b/src/messaging.js
@@ -1,20 +1,19 @@
'use strict';
-var async = require('async'),
- winston = require('winston'),
- S = require('string'),
+var async = require('async');
+var winston = require('winston');
+var S = require('string');
+var db = require('./database');
+var user = require('./user');
+var plugins = require('./plugins');
+var meta = require('./meta');
+var utils = require('../public/src/utils');
+var notifications = require('./notifications');
+var userNotifications = require('./user/notifications');
- db = require('./database'),
- user = require('./user'),
- plugins = require('./plugins'),
- meta = require('./meta'),
- utils = require('../public/src/utils'),
- notifications = require('./notifications'),
- userNotifications = require('./user/notifications');
-
-(function(Messaging) {
+(function (Messaging) {
require('./messaging/create')(Messaging);
require('./messaging/delete')(Messaging);
@@ -23,62 +22,66 @@ var async = require('async'),
require('./messaging/unread')(Messaging);
require('./messaging/notifications')(Messaging);
- var terms = {
- day: 86400000,
- week: 604800000,
- month: 2592000000,
- threemonths: 7776000000
- };
-
- Messaging.getMessageField = function(mid, field, callback) {
- Messaging.getMessageFields(mid, [field], function(err, fields) {
+ Messaging.getMessageField = function (mid, field, callback) {
+ Messaging.getMessageFields(mid, [field], function (err, fields) {
callback(err, fields ? fields[field] : null);
});
};
- Messaging.getMessageFields = function(mid, fields, callback) {
+ Messaging.getMessageFields = function (mid, fields, callback) {
db.getObjectFields('message:' + mid, fields, callback);
};
- Messaging.setMessageField = function(mid, field, content, callback) {
+ Messaging.setMessageField = function (mid, field, content, callback) {
db.setObjectField('message:' + mid, field, content, callback);
};
- Messaging.setMessageFields = function(mid, data, callback) {
+ Messaging.setMessageFields = function (mid, data, callback) {
db.setObject('message:' + mid, data, callback);
};
- Messaging.getMessages = function(params, callback) {
- var uid = params.uid,
- roomId = params.roomId,
- since = params.since,
- isNew = params.isNew,
- count = params.count || parseInt(meta.config.chatMessageInboxSize, 10) || 250,
- markRead = params.markRead || true;
+ Messaging.getMessages = function (params, callback) {
+ var uid = params.uid;
+ var roomId = params.roomId;
+ var isNew = params.isNew || false;
+ var start = params.hasOwnProperty('start') ? params.start : 0;
+ var stop = parseInt(start, 10) + ((params.count || 50) - 1);
+ var markRead = params.markRead || true;
- var min = params.count ? 0 : Date.now() - (terms[since] || terms.day);
+ var indices = {};
+ async.waterfall([
+ function (next) {
+ canGetMessages(params.callerUid, params.uid, next);
+ },
+ function (canGet, next) {
+ if (!canGet) {
+ return callback(null, null);
+ }
+ db.getSortedSetRevRange('uid:' + uid + ':chat:room:' + roomId + ':mids', start, stop, next);
+ },
+ function (mids, next) {
+ if (!Array.isArray(mids) || !mids.length) {
+ return callback(null, []);
+ }
- if (since === 'recent') {
- count = 49;
- min = 0;
- }
+ mids.forEach(function (mid, index) {
+ indices[mid] = start + index;
+ });
- db.getSortedSetRevRangeByScore('uid:' + uid + ':chat:room:' + roomId + ':mids', 0, count, '+inf', min, function(err, mids) {
- if (err) {
- return callback(err);
+ mids.reverse();
+
+ Messaging.getMessagesData(mids, uid, roomId, isNew, next);
+ },
+ function (messageData, next) {
+ messageData.forEach(function (messageData) {
+ messageData.index = indices[messageData.messageId.toString()];
+ });
+ next(null, messageData);
}
-
- if (!Array.isArray(mids) || !mids.length) {
- return callback(null, []);
- }
-
- mids.reverse();
-
- Messaging.getMessagesData(mids, uid, roomId, isNew, callback);
- });
+ ], callback);
if (markRead) {
- notifications.markRead('chat_' + roomId + '_' + uid, uid, function(err) {
+ notifications.markRead('chat_' + roomId + '_' + uid, uid, function (err) {
if (err) {
winston.error('[messaging] Could not mark notifications related to this chat as read: ' + err.message);
}
@@ -88,9 +91,19 @@ var async = require('async'),
}
};
- Messaging.getMessagesData = function(mids, uid, roomId, isNew, callback) {
+ function canGetMessages(callerUid, uid, callback) {
+ plugins.fireHook('filter:messaging.canGetMessages', {
+ callerUid: callerUid,
+ uid: uid,
+ canGet: parseInt(callerUid, 10) === parseInt(uid, 10)
+ }, function (err, data) {
+ callback(err, data ? data.canGet : false);
+ });
+ }
- var keys = mids.map(function(mid) {
+ Messaging.getMessagesData = function (mids, uid, roomId, isNew, callback) {
+
+ var keys = mids.map(function (mid) {
return 'message:' + mid;
});
@@ -101,48 +114,52 @@ var async = require('async'),
db.getObjects(keys, next);
},
function (_messages, next) {
- messages = _messages.map(function(msg, idx) {
+ messages = _messages.map(function (msg, idx) {
if (msg) {
msg.messageId = parseInt(mids[idx], 10);
}
return msg;
}).filter(Boolean);
- var uids = messages.map(function(msg) {
+ var uids = messages.map(function (msg) {
return msg && msg.fromuid;
});
user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'status'], next);
},
function (users, next) {
- messages.forEach(function(message, index) {
+ messages.forEach(function (message, index) {
message.fromUser = users[index];
var self = parseInt(message.fromuid, 10) === parseInt(uid, 10);
message.self = self ? 1 : 0;
message.timestampISO = utils.toISOString(message.timestamp);
message.newSet = false;
+ message.roomId = String(message.roomId || roomId);
if (message.hasOwnProperty('edited')) {
message.editedISO = new Date(parseInt(message.edited, 10)).toISOString();
}
});
- async.map(messages, function(message, next) {
- Messaging.parse(message.content, message.fromuid, uid, roomId, isNew, function(result) {
+ async.map(messages, function (message, next) {
+ Messaging.parse(message.content, message.fromuid, uid, roomId, isNew, function (err, result) {
+ if (err) {
+ return next(err);
+ }
message.content = result;
message.cleanedContent = S(result).stripTags().decodeHTMLEntities().s;
next(null, message);
});
}, next);
},
- function(messages, next) {
+ function (messages, next) {
if (messages.length > 1) {
// Add a spacer in between messages with time gaps between them
- messages = messages.map(function(message, index) {
+ messages = messages.map(function (message, index) {
// Compare timestamps with the previous message, and check if a spacer needs to be added
- if (index > 0 && parseInt(message.timestamp, 10) > parseInt(messages[index-1].timestamp, 10) + (1000*60*5)) {
+ if (index > 0 && parseInt(message.timestamp, 10) > parseInt(messages[index - 1].timestamp, 10) + (1000 * 60 * 5)) {
// If it's been 5 minutes, this is a new set of messages
message.newSet = true;
- } else if (index > 0 && message.fromuid !== messages[index-1].fromuid) {
+ } else if (index > 0 && message.fromuid !== messages[index - 1].fromuid) {
// If the previous message was from the other person, this is also a new set
message.newSet = true;
}
@@ -156,25 +173,25 @@ var async = require('async'),
var key = 'uid:' + uid + ':chat:room:' + roomId + ':mids';
async.waterfall([
async.apply(db.sortedSetRank, key, messages[0].messageId),
- function(index, next) {
+ function (index, next) {
// Continue only if this isn't the first message in sorted set
if (index > 0) {
- db.getSortedSetRange(key, index-1, index-1, next);
+ db.getSortedSetRange(key, index - 1, index - 1, next);
} else {
messages[0].newSet = true;
return next(undefined, messages);
}
},
- function(mid, next) {
+ function (mid, next) {
Messaging.getMessageFields(mid, ['fromuid', 'timestamp'], next);
}
- ], function(err, fields) {
+ ], function (err, fields) {
if (err) {
return next(err);
}
if (
- (parseInt(messages[0].timestamp, 10) > parseInt(fields.timestamp, 10) + (1000*60*5)) ||
+ (parseInt(messages[0].timestamp, 10) > parseInt(fields.timestamp, 10) + (1000 * 60 * 5)) ||
(parseInt(messages[0].fromuid, 10) !== parseInt(fields.fromuid, 10))
) {
// If it's been 5 minutes, this is a new set of messages
@@ -192,9 +209,9 @@ var async = require('async'),
};
Messaging.parse = function (message, fromuid, uid, roomId, isNew, callback) {
- plugins.fireHook('filter:parse.raw', message, function(err, parsed) {
+ plugins.fireHook('filter:parse.raw', message, function (err, parsed) {
if (err) {
- return callback(message);
+ return callback(err);
}
var messageData = {
@@ -207,20 +224,20 @@ var async = require('async'),
parsedMessage: parsed
};
- plugins.fireHook('filter:messaging.parse', messageData, function(err, messageData) {
- callback(messageData.parsedMessage);
+ plugins.fireHook('filter:messaging.parse', messageData, function (err, messageData) {
+ callback(err, messageData ? messageData.parsedMessage : '');
});
});
};
- Messaging.isNewSet = function(uid, roomId, timestamp, callback) {
+ Messaging.isNewSet = function (uid, roomId, timestamp, callback) {
var setKey = 'uid:' + uid + ':chat:room:' + roomId + ':mids';
async.waterfall([
- function(next) {
+ function (next) {
db.getSortedSetRevRangeWithScores(setKey, 0, 0, next);
},
- function(messages, next) {
+ function (messages, next) {
if (messages && messages.length) {
next(null, parseInt(timestamp, 10) > parseInt(messages[0].score, 10) + (1000 * 60 * 5));
} else {
@@ -231,66 +248,91 @@ var async = require('async'),
};
- Messaging.getRecentChats = function(uid, start, stop, callback) {
- db.getSortedSetRevRange('uid:' + uid + ':chat:rooms', start, stop, function(err, roomIds) {
- if (err) {
- return callback(err);
- }
-
- async.parallel({
- unread: function(next) {
- db.isSortedSetMembers('uid:' + uid + ':chat:rooms:unread', roomIds, next);
- },
- users: function(next) {
- async.map(roomIds, function(roomId, next) {
- db.getSortedSetRevRange('chat:room:' + roomId + ':uids', 0, 3, function(err, uids) {
- if (err) {
- return next(err);
- }
- uids = uids.filter(function(value) {
- return value && parseInt(value, 10) !== parseInt(uid, 10);
+ Messaging.getRecentChats = function (callerUid, uid, start, stop, callback) {
+ async.waterfall([
+ function (next) {
+ canGetRecentChats(callerUid, uid, next);
+ },
+ function (canGet, next) {
+ if (!canGet) {
+ return callback(null, null);
+ }
+ db.getSortedSetRevRange('uid:' + uid + ':chat:rooms', start, stop, next);
+ },
+ function (roomIds, next) {
+ async.parallel({
+ roomData: function (next) {
+ Messaging.getRoomsData(roomIds, next);
+ },
+ unread: function (next) {
+ db.isSortedSetMembers('uid:' + uid + ':chat:rooms:unread', roomIds, next);
+ },
+ users: function (next) {
+ async.map(roomIds, function (roomId, next) {
+ db.getSortedSetRevRange('chat:room:' + roomId + ':uids', 0, 9, function (err, uids) {
+ if (err) {
+ return next(err);
+ }
+ uids = uids.filter(function (value) {
+ return value && parseInt(value, 10) !== parseInt(uid, 10);
+ });
+ user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'status', 'lastonline'] , next);
});
- user.getUsersFields(uids, ['uid', 'username', 'picture', 'status', 'lastonline'] , next);
- });
- }, next);
- },
- teasers: function(next) {
- async.map(roomIds, function(roomId, next) {
- Messaging.getTeaser(uid, roomId, next);
- }, next);
- }
- }, function(err, results) {
- if (err) {
- return callback(err);
- }
- var rooms = results.users.map(function(users, index) {
- var data = {
- users: users,
- unread: results.unread[index],
- roomId: roomIds[index],
- teaser: results.teasers[index]
- };
- data.users.forEach(function(userData) {
+ }, next);
+ },
+ teasers: function (next) {
+ async.map(roomIds, function (roomId, next) {
+ Messaging.getTeaser(uid, roomId, next);
+ }, next);
+ }
+ }, next);
+ },
+ function (results, next) {
+ results.roomData.forEach(function (room, index) {
+ room.users = results.users[index];
+ room.groupChat = room.hasOwnProperty('groupChat') ? room.groupChat : room.users.length > 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) {
- return user.username;
- }).join(', ');
- return data;
+ room.lastUser = room.users[0];
+
+ room.usernames = Messaging.generateUsernames(room.users, uid);
});
- callback(null, {rooms: rooms, nextStart: stop + 1});
- });
- });
+ next(null, {rooms: results.roomData, nextStart: stop + 1});
+ }
+ ], callback);
};
+ Messaging.generateUsernames = function (users, excludeUid) {
+ users = users.filter(function (user) {
+ return user && parseInt(user.uid, 10) !== excludeUid;
+ });
+ return users.map(function (user) {
+ return user.username;
+ }).join(', ');
+ };
+
+ function canGetRecentChats(callerUid, uid, callback) {
+ plugins.fireHook('filter:messaging.canGetRecentChats', {
+ callerUid: callerUid,
+ uid: uid,
+ canGet: parseInt(callerUid, 10) === parseInt(uid, 10)
+ }, function (err, data) {
+ callback(err, data ? data.canGet : false);
+ });
+ }
+
Messaging.getTeaser = function (uid, roomId, callback) {
+ var teaser;
async.waterfall([
function (next) {
db.getSortedSetRevRange('uid:' + uid + ':chat:room:' + roomId + ':mids', 0, 0, next);
@@ -299,20 +341,28 @@ 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);
};
- Messaging.canMessageUser = function(uid, toUid, callback) {
+ Messaging.canMessageUser = function (uid, toUid, callback) {
if (parseInt(meta.config.disableChat) === 1 || !uid || uid === toUid) {
return callback(new Error('[[error:chat-disabled]]'));
}
@@ -342,7 +392,7 @@ var async = require('async'),
isFollowing: async.apply(user.isFollowing, toUid, uid)
}, next);
},
- function(results, next) {
+ function (results, next) {
if (!results.settings.restrictChat || results.isAdmin || results.isFollowing) {
return next();
}
@@ -352,7 +402,7 @@ var async = require('async'),
], callback);
};
- Messaging.canMessageRoom = function(uid, roomId, callback) {
+ Messaging.canMessageRoom = function (uid, roomId, callback) {
if (parseInt(meta.config.disableChat) === 1 || !uid) {
return callback(new Error('[[error:chat-disabled]]'));
}
@@ -368,7 +418,7 @@ var async = require('async'),
Messaging.getUserCountInRoom(roomId, next);
},
- function(count, next) {
+ function (count, next) {
if (count < 2) {
return next(new Error('[[error:no-users-in-room]]'));
}
@@ -389,7 +439,7 @@ var async = require('async'),
], callback);
};
- Messaging.hasPrivateChat = function(uid, withUid, callback) {
+ Messaging.hasPrivateChat = function (uid, withUid, callback) {
async.waterfall([
function (next) {
async.parallel({
@@ -398,7 +448,7 @@ var async = require('async'),
}, next);
},
function (results, next) {
- var roomIds = results.myRooms.filter(function(roomId) {
+ var roomIds = results.myRooms.filter(function (roomId) {
return roomId && results.theirRooms.indexOf(roomId) !== -1;
});
@@ -408,10 +458,10 @@ var async = require('async'),
var index = 0;
var roomId = 0;
- async.whilst(function() {
+ async.whilst(function () {
return index < roomIds.length && !roomId;
- }, function(next) {
- Messaging.getUserCountInRoom(roomIds[index], function(err, count) {
+ }, function (next) {
+ Messaging.getUserCountInRoom(roomIds[index], function (err, count) {
if (err) {
return next(err);
}
@@ -423,7 +473,7 @@ var async = require('async'),
next();
}
});
- }, function(err) {
+ }, function (err) {
next(err, roomId);
});
}
diff --git a/src/messaging/create.js b/src/messaging/create.js
index 3b2b5dcf26..face15f589 100644
--- a/src/messaging/create.js
+++ b/src/messaging/create.js
@@ -7,9 +7,9 @@ var plugins = require('../plugins');
var db = require('../database');
-module.exports = function(Messaging) {
+module.exports = function (Messaging) {
- Messaging.sendMessage = function(uid, roomId, content, timestamp, callback) {
+ Messaging.sendMessage = function (uid, roomId, content, timestamp, callback) {
async.waterfall([
function (next) {
Messaging.checkContent(content, next);
@@ -27,7 +27,7 @@ module.exports = function(Messaging) {
], callback);
};
- Messaging.checkContent = function(content, callback) {
+ Messaging.checkContent = function (content, callback) {
if (!content) {
return callback(new Error('[[error:invalid-chat-message]]'));
}
@@ -38,7 +38,7 @@ module.exports = function(Messaging) {
callback();
};
- Messaging.addMessage = function(fromuid, roomId, content, timestamp, callback) {
+ Messaging.addMessage = function (fromuid, roomId, content, timestamp, callback) {
var mid;
var message;
var isNewSet;
@@ -55,7 +55,8 @@ module.exports = function(Messaging) {
message = {
content: content,
timestamp: timestamp,
- fromuid: fromuid
+ fromuid: fromuid,
+ roomId: roomId
};
plugins.fireHook('filter:messaging.save', message, next);
@@ -97,21 +98,21 @@ module.exports = function(Messaging) {
], callback);
};
- Messaging.addRoomToUsers = function(roomId, uids, timestamp, callback) {
+ Messaging.addRoomToUsers = function (roomId, uids, timestamp, callback) {
if (!uids.length) {
return callback();
}
- var keys = uids.map(function(uid) {
+ var keys = uids.map(function (uid) {
return 'uid:' + uid + ':chat:rooms';
});
db.sortedSetsAdd(keys, timestamp, roomId, callback);
};
- Messaging.addMessageToUsers = function(roomId, uids, mid, timestamp, callback) {
+ Messaging.addMessageToUsers = function (roomId, uids, mid, timestamp, callback) {
if (!uids.length) {
return callback();
}
- var keys = uids.map(function(uid) {
+ var keys = uids.map(function (uid) {
return 'uid:' + uid + ':chat:room:' + roomId + ':mids';
});
db.sortedSetsAdd(keys, timestamp, mid, callback);
diff --git a/src/messaging/delete.js b/src/messaging/delete.js
index 518ab9fafb..e9f48232d1 100644
--- a/src/messaging/delete.js
+++ b/src/messaging/delete.js
@@ -3,9 +3,9 @@
var async = require('async');
var db = require('../database');
-module.exports = function(Messaging) {
+module.exports = function (Messaging) {
- Messaging.deleteMessage = function(mid, roomId, callback) {
+ Messaging.deleteMessage = function (mid, roomId, callback) {
async.waterfall([
function (next) {
Messaging.getUidsInRoom(roomId, 0, -1, next);
@@ -14,12 +14,12 @@ module.exports = function(Messaging) {
if (!uids.length) {
return next();
}
- var keys = uids.map(function(uid) {
+ var keys = uids.map(function (uid) {
return 'uid:' + uid + ':chat:room:' + roomId + 'mids';
});
db.sortedSetsRemove(keys, roomId, next);
},
- function(next) {
+ function (next) {
db.delete('message:' + mid, next);
}
], callback);
diff --git a/src/messaging/edit.js b/src/messaging/edit.js
index 5b2472c4e4..c60e264cbf 100644
--- a/src/messaging/edit.js
+++ b/src/messaging/edit.js
@@ -8,12 +8,12 @@ var user = require('../user');
var sockets = require('../socket.io');
-module.exports = function(Messaging) {
+module.exports = function (Messaging) {
- Messaging.editMessage = function(uid, mid, roomId, content, callback) {
+ Messaging.editMessage = function (uid, mid, roomId, content, callback) {
var uids;
async.waterfall([
- function(next) {
+ function (next) {
Messaging.getMessageField(mid, 'content', next);
},
function (raw, next) {
@@ -34,7 +34,7 @@ module.exports = function(Messaging) {
Messaging.getMessagesData([mid], uid, roomId, true, next);
},
function (messages, next) {
- uids.forEach(function(uid) {
+ uids.forEach(function (uid) {
sockets.in('uid_' + uid).emit('event:chats.edit', {
messages: messages
});
@@ -44,7 +44,7 @@ module.exports = function(Messaging) {
], callback);
};
- Messaging.canEdit = function(messageId, uid, callback) {
+ Messaging.canEdit = function (messageId, uid, callback) {
if (parseInt(meta.config.disableChat) === 1) {
return callback(null, false);
}
@@ -64,14 +64,14 @@ module.exports = function(Messaging) {
Messaging.getMessageField(messageId, 'fromuid', next);
},
- function(fromUid, next) {
+ function (fromUid, next) {
if (parseInt(fromUid, 10) === parseInt(uid, 10)) {
return callback(null, true);
}
user.isAdministrator(uid, next);
},
- function(isAdmin, next) {
+ function (isAdmin, next) {
next(null, isAdmin);
}
], callback);
diff --git a/src/messaging/notifications.js b/src/messaging/notifications.js
index c93cb16590..6d9500c4ba 100644
--- a/src/messaging/notifications.js
+++ b/src/messaging/notifications.js
@@ -2,20 +2,20 @@
var async = require('async');
var nconf = require('nconf');
+var winston = require('winston');
var user = require('../user');
var emailer = require('../emailer');
var notifications = require('../notifications');
var meta = require('../meta');
-var utils = require('../../public/src/utils');
var sockets = require('../socket.io');
-module.exports = function(Messaging) {
+module.exports = function (Messaging) {
Messaging.notifyQueue = {}; // Only used to notify a user of a new chat message, see Messaging.notifyUser
- Messaging.notifyUsersInRoom = function(fromUid, roomId, messageObj) {
- Messaging.getUidsInRoom(roomId, 0, -1, function(err, uids) {
+ Messaging.notifyUsersInRoom = function (fromUid, roomId, messageObj) {
+ Messaging.getUidsInRoom(roomId, 0, -1, function (err, uids) {
if (err) {
return;
}
@@ -25,7 +25,7 @@ module.exports = function(Messaging) {
fromUid: fromUid,
message: messageObj
};
- uids.forEach(function(uid) {
+ uids.forEach(function (uid) {
data.self = parseInt(uid, 10) === parseInt(fromUid) ? 1 : 0;
Messaging.pushUnreadCount(uid);
sockets.in('uid_' + uid).emit('event:chats.receive', data);
@@ -42,8 +42,8 @@ module.exports = function(Messaging) {
};
}
- queueObj.timeout = setTimeout(function() {
- sendNotifications(fromUid, uids, roomId, queueObj.message, function(err) {
+ queueObj.timeout = setTimeout(function () {
+ sendNotifications(fromUid, uids, roomId, queueObj.message, function (err) {
if (!err) {
delete Messaging.notifyQueue[fromUid + ':' + roomId];
}
@@ -53,12 +53,12 @@ module.exports = function(Messaging) {
};
function sendNotifications(fromuid, uids, roomId, messageObj, callback) {
- user.isOnline(uids, function(err, isOnline) {
+ user.isOnline(uids, function (err, isOnline) {
if (err) {
return callback(err);
}
- uids = uids.filter(function(uid, index) {
+ uids = uids.filter(function (uid, index) {
return !isOnline[index] && parseInt(fromuid, 10) !== parseInt(uid, 10);
});
@@ -72,33 +72,52 @@ module.exports = function(Messaging) {
nid: 'chat_' + fromuid + '_' + roomId,
from: fromuid,
path: '/chats/' + messageObj.roomId
- }, function(err, notification) {
+ }, function (err, notification) {
if (!err && notification) {
notifications.push(notification, uids, callback);
}
});
- if (parseInt(meta.config.disableEmailSubscriptions, 10) === 1) {
- return callback();
+ sendNotificationEmails(uids, messageObj);
+ });
+ }
+
+ function sendNotificationEmails(uids, messageObj) {
+ if (parseInt(meta.config.disableEmailSubscriptions, 10) === 1) {
+ return;
+ }
+
+ async.parallel({
+ userData: function (next) {
+ user.getUsersFields(uids, ['uid', 'username', 'userslug'], next);
+ },
+ userSettings: function (next) {
+ user.getMultipleUserSettings(uids, next);
+ }
+ }, function (err, results) {
+ if (err) {
+ return winston.error(err);
}
- user.getMultipleUserSettings(uids, function(err, userSettings) {
+ results.userData = results.userData.filter(function (userData, index) {
+ return userData && results.userSettings[index] && results.userSettings[index].sendChatNotifications;
+ });
+
+ async.each(results.userData, function (userData, next) {
+ emailer.send('notif_chat', userData.uid, {
+ subject: '[[email:notif.chat.subject, ' + messageObj.fromUser.username + ']]',
+ summary: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]',
+ message: messageObj,
+ site_title: meta.config.title || 'NodeBB',
+ url: nconf.get('url'),
+ roomId: messageObj.roomId,
+ username: userData.username,
+ userslug: userData.userslug
+ }, next);
+ }, function (err) {
if (err) {
- return callback(err);
+ winston.error(err);
}
- userSettings = userSettings.filter(function(settings) {
- return settings && settings.sendChatNotifications;
- });
- async.each(userSettings, function(settings, next) {
- emailer.send('notif_chat', settings.uid, {
- subject: '[[email:notif.chat.subject, ' + messageObj.fromUser.username + ']]',
- summary: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]',
- message: messageObj,
- site_title: meta.config.title || 'NodeBB',
- url: nconf.get('url'),
- fromUserslug: utils.slugify(messageObj.fromUser.username)
- }, next);
- }, callback);
});
});
}
diff --git a/src/messaging/rooms.js b/src/messaging/rooms.js
index 0c9c5bb01f..f16843662f 100644
--- a/src/messaging/rooms.js
+++ b/src/messaging/rooms.js
@@ -5,23 +5,46 @@ var validator = require('validator');
var db = require('../database');
var user = require('../user');
+var plugins = require('../plugins');
-module.exports = function(Messaging) {
+module.exports = function (Messaging) {
- Messaging.getRoomData = function(roomId, callback) {
- db.getObject('chat:room:' + roomId, function(err, data) {
+ Messaging.getRoomData = function (roomId, callback) {
+ db.getObject('chat:room:' + roomId, function (err, data) {
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.newRoom = function(uid, toUids, callback) {
+ 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 || '';
+ 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();
async.waterfall([
@@ -51,20 +74,30 @@ module.exports = function(Messaging) {
], callback);
};
- Messaging.isUserInRoom = function(uid, roomId, callback) {
- db.isSortedSetMember('chat:room:' + roomId + ':uids', uid, callback);
+ Messaging.isUserInRoom = function (uid, roomId, callback) {
+ async.waterfall([
+ function (next) {
+ db.isSortedSetMember('chat:room:' + roomId + ':uids', uid, next);
+ },
+ function (inRoom, next) {
+ plugins.fireHook('filter:messaging.isUserInRoom', {uid: uid, roomId: roomId, inRoom: inRoom}, next);
+ },
+ function (data, next) {
+ next(null, data.inRoom);
+ }
+ ], callback);
};
- Messaging.roomExists = function(roomId, callback) {
+ Messaging.roomExists = function (roomId, callback) {
db.exists('chat:room:' + roomId + ':uids', callback);
};
- Messaging.getUserCountInRoom = function(roomId, callback) {
+ Messaging.getUserCountInRoom = function (roomId, callback) {
db.sortedSetCard('chat:room:' + roomId + ':uids', callback);
};
- Messaging.isRoomOwner = function(uid, roomId, callback) {
- db.getObjectField('chat:room:' + roomId, 'owner', function(err, owner) {
+ Messaging.isRoomOwner = function (uid, roomId, callback) {
+ db.getObjectField('chat:room:' + roomId, 'owner', function (err, owner) {
if (err) {
return callback(err);
}
@@ -73,7 +106,7 @@ module.exports = function(Messaging) {
});
};
- Messaging.addUsersToRoom = function(uid, uids, roomId, callback) {
+ Messaging.addUsersToRoom = function (uid, uids, roomId, callback) {
async.waterfall([
function (next) {
Messaging.isUserInRoom(uid, roomId, next);
@@ -83,15 +116,27 @@ module.exports = function(Messaging) {
return next(new Error('[[error:cant-add-users-to-chat-room]]'));
}
var now = Date.now();
- var timestamps = uids.map(function() {
+ var timestamps = uids.map(function () {
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);
};
- Messaging.removeUsersFromRoom = function(uid, uids, roomId, callback) {
+ Messaging.removeUsersFromRoom = function (uid, uids, roomId, callback) {
async.waterfall([
function (next) {
async.parallel({
@@ -111,16 +156,16 @@ module.exports = function(Messaging) {
], callback);
};
- Messaging.leaveRoom = function(uids, roomId, callback) {
+ Messaging.leaveRoom = function (uids, roomId, callback) {
async.waterfall([
function (next) {
db.sortedSetRemove('chat:room:' + roomId + ':uids', uids, next);
},
function (next) {
- var keys = uids.map(function(uid) {
+ var keys = uids.map(function (uid) {
return 'uid:' + uid + ':chat:rooms';
});
- keys.concat(uids.map(function(uid) {
+ keys.concat(uids.map(function (uid) {
return 'uid:' + uid + ':chat:rooms:unread';
}));
db.sortedSetsRemove(keys, roomId, next);
@@ -128,22 +173,22 @@ module.exports = function(Messaging) {
], callback);
};
- Messaging.getUidsInRoom = function(roomId, start, stop, callback) {
+ Messaging.getUidsInRoom = function (roomId, start, stop, callback) {
db.getSortedSetRevRange('chat:room:' + roomId + ':uids', start, stop, callback);
};
- Messaging.getUsersInRoom = function(roomId, start, stop, callback) {
+ Messaging.getUsersInRoom = function (roomId, start, stop, callback) {
async.waterfall([
function (next) {
Messaging.getUidsInRoom(roomId, start, stop, next);
},
function (uids, next) {
- user.getUsersFields(uids, ['username', 'uid', 'picture', 'status'], next);
+ user.getUsersFields(uids, ['uid', 'username', 'picture', 'status'], next);
}
], callback);
};
- Messaging.renameRoom = function(uid, roomId, newName, callback) {
+ Messaging.renameRoom = function (uid, roomId, newName, callback) {
if (!newName) {
return callback(new Error('[[error:invalid-name]]'));
}
diff --git a/src/messaging/unread.js b/src/messaging/unread.js
index 0562551540..91c9a364ac 100644
--- a/src/messaging/unread.js
+++ b/src/messaging/unread.js
@@ -5,20 +5,20 @@ var async = require('async');
var db = require('../database');
var sockets = require('../socket.io');
-module.exports = function(Messaging) {
+module.exports = function (Messaging) {
- Messaging.getUnreadCount = function(uid, callback) {
+ Messaging.getUnreadCount = function (uid, callback) {
if (!parseInt(uid, 10)) {
return callback(null, 0);
}
db.sortedSetCard('uid:' + uid + ':chat:rooms:unread', callback);
};
- Messaging.pushUnreadCount = function(uid) {
+ Messaging.pushUnreadCount = function (uid) {
if (!parseInt(uid, 10)) {
return callback(null, 0);
}
- Messaging.getUnreadCount(uid, function(err, unreadCount) {
+ Messaging.getUnreadCount(uid, function (err, unreadCount) {
if (err) {
return;
}
@@ -26,15 +26,15 @@ module.exports = function(Messaging) {
});
};
- Messaging.markRead = function(uid, roomId, callback) {
+ Messaging.markRead = function (uid, roomId, callback) {
db.sortedSetRemove('uid:' + uid + ':chat:rooms:unread', roomId, callback);
};
- Messaging.markAllRead = function(uid, callback) {
+ Messaging.markAllRead = function (uid, callback) {
db.delete('uid:' + uid + ':chat:rooms:unread', callback);
};
- Messaging.markUnread = function(uids, roomId, callback) {
+ Messaging.markUnread = function (uids, roomId, callback) {
async.waterfall([
function (next) {
Messaging.roomExists(roomId, next);
@@ -43,7 +43,7 @@ module.exports = function(Messaging) {
if (!exists) {
return next(new Error('[[error:chat-room-does-not-exist]]'));
}
- var keys = uids.map(function(uid) {
+ var keys = uids.map(function (uid) {
return 'uid:' + uid + ':chat:rooms:unread';
});
diff --git a/src/meta.js b/src/meta.js
index ffc85c98bf..c732de15f4 100644
--- a/src/meta.js
+++ b/src/meta.js
@@ -1,17 +1,12 @@
"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');
+var os = require('os');
+var nconf = require('nconf');
- user = require('./user'),
- groups = require('./groups'),
- emitter = require('./emitter'),
- pubsub = require('./pubsub'),
- auth = require('./routes/authentication'),
- utils = require('../public/src/utils');
+var pubsub = require('./pubsub');
+var utils = require('../public/src/utils');
(function (Meta) {
Meta.reloadRequired = false;
@@ -23,72 +18,40 @@ var async = require('async'),
require('./meta/sounds')(Meta);
require('./meta/settings')(Meta);
require('./meta/logs')(Meta);
+ require('./meta/errors')(Meta);
require('./meta/tags')(Meta);
require('./meta/dependencies')(Meta);
Meta.templates = require('./meta/templates');
Meta.blacklist = require('./meta/blacklist');
/* Assorted */
- Meta.userOrGroupExists = function(slug, callback) {
+ Meta.userOrGroupExists = function (slug, callback) {
+ var user = require('./user');
+ var groups = require('./groups');
slug = utils.slugify(slug);
async.parallel([
async.apply(user.existsBySlug, slug),
async.apply(groups.existsBySlug, slug)
- ], function(err, results) {
- callback(err, results ? results.some(function(result) { return result; }) : false);
+ ], function (err, results) {
+ callback(err, results ? results.some(function (result) { return result; }) : false);
});
};
- Meta.reload = function(callback) {
- pubsub.publish('meta:reload', {hostname: os.hostname()});
- reload(callback);
+ /**
+ * Reload deprecated as of v1.1.2+, remove in v2.x
+ */
+ Meta.reload = function (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(plugins.reload),
- async.apply(plugins.reloadRoutes),
- 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(Meta.templates.compile),
- 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() {
+ Meta.restart = function () {
pubsub.publish('meta:restart', {hostname: os.hostname()});
restart();
};
if (nconf.get('isPrimary') === 'true') {
- pubsub.on('meta:restart', function(data) {
+ pubsub.on('meta:restart', function (data) {
if (data.hostname !== os.hostname()) {
restart();
}
diff --git a/src/meta/blacklist.js b/src/meta/blacklist.js
index 47381e487b..679dc3cec9 100644
--- a/src/meta/blacklist.js
+++ b/src/meta/blacklist.js
@@ -9,11 +9,11 @@ var Blacklist = {
_rules: []
};
-Blacklist.load = function(callback) {
+Blacklist.load = function (callback) {
async.waterfall([
async.apply(db.get, 'ip-blacklist-rules'),
async.apply(Blacklist.validate)
- ], function(err, rules) {
+ ], function (err, rules) {
if (err) {
return callback(err);
}
@@ -33,8 +33,8 @@ Blacklist.load = function(callback) {
});
};
-Blacklist.save = function(rules, callback) {
- db.set('ip-blacklist-rules', rules, function(err) {
+Blacklist.save = function (rules, callback) {
+ db.set('ip-blacklist-rules', rules, function (err) {
if (err) {
return callback(err);
}
@@ -42,15 +42,15 @@ Blacklist.save = function(rules, callback) {
});
};
-Blacklist.get = function(callback) {
+Blacklist.get = function (callback) {
db.get('ip-blacklist-rules', callback);
};
-Blacklist.test = function(clientIp, callback) {
+Blacklist.test = function (clientIp, callback) {
if (
Blacklist._rules.ipv4.indexOf(clientIp) === -1 // not explicitly specified in ipv4 list
&& Blacklist._rules.ipv6.indexOf(clientIp) === -1 // not explicitly specified in ipv6 list
- && !Blacklist._rules.cidr.some(function(subnet) {
+ && !Blacklist._rules.cidr.some(function (subnet) {
return ip.cidrSubnet(subnet).contains(clientIp);
}) // not in a blacklisted cidr range
) {
@@ -71,7 +71,7 @@ Blacklist.test = function(clientIp, callback) {
}
};
-Blacklist.validate = function(rules, callback) {
+Blacklist.validate = function (rules, callback) {
rules = (rules || '').split('\n');
var ipv4 = [];
var ipv6 = [];
@@ -84,13 +84,13 @@ Blacklist.validate = function(rules, callback) {
// Filter out blank lines and lines starting with the hash character (comments)
// Also trim inputs and remove inline comments
- rules = rules.map(function(rule) {
+ rules = rules.map(function (rule) {
rule = rule.replace(inlineCommentMatch, '').trim();
return rule.length && !rule.startsWith('#') ? rule : null;
}).filter(Boolean);
// Filter out invalid rules
- rules = rules.filter(function(rule) {
+ rules = rules.filter(function (rule) {
if (whitelist.indexOf(rule) !== -1) {
invalid.push(rule);
return false;
diff --git a/src/meta/configs.js b/src/meta/configs.js
index 3ff42f66ae..0a8435e531 100644
--- a/src/meta/configs.js
+++ b/src/meta/configs.js
@@ -1,13 +1,14 @@
'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');
-module.exports = function(Meta) {
+var db = require('../database');
+var pubsub = require('../pubsub');
+var utils = require('../../public/src/utils');
+
+module.exports = function (Meta) {
Meta.config = {};
Meta.configs = {};
@@ -46,12 +47,12 @@ module.exports = function(Meta) {
};
Meta.configs.set = function (field, value, callback) {
- callback = callback || function() {};
+ callback = callback || function () {};
if (!field) {
return callback(new Error('invalid config field'));
}
- db.setObjectField('config', field, value, function(err) {
+ db.setObjectField('config', field, value, function (err) {
if (err) {
return callback(err);
}
@@ -63,12 +64,12 @@ module.exports = function(Meta) {
});
};
- Meta.configs.setMultiple = function(data, callback) {
- processConfig(data, function(err) {
+ Meta.configs.setMultiple = function (data, callback) {
+ processConfig(data, function (err) {
if (err) {
return callback(err);
}
- db.setObject('config', data, function(err) {
+ db.setObject('config', data, function (err) {
if (err) {
return callback(err);
}
@@ -91,7 +92,7 @@ module.exports = function(Meta) {
var less = require('less');
less.render(data.customCSS, {
compress: true
- }, function(err, lessObject) {
+ }, function (err, lessObject) {
if (err) {
winston.error('[less] Could not convert custom LESS to CSS! Please check your syntax.');
return callback(null, '');
@@ -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();
}
@@ -135,4 +142,4 @@ module.exports = function(Meta) {
db.deleteObjectField('config', field);
};
-};
\ No newline at end of file
+};
diff --git a/src/meta/css.js b/src/meta/css.js
index 12c5d49c20..ce31ea1f13 100644
--- a/src/meta/css.js
+++ b/src/meta/css.js
@@ -1,36 +1,35 @@
'use strict';
-var winston = require('winston'),
- nconf = require('nconf'),
- fs = require('fs'),
- path = require('path'),
- less = require('less'),
- crypto = require('crypto'),
- async = require('async'),
- autoprefixer = require('autoprefixer'),
- postcss = require('postcss'),
+var winston = require('winston');
+var nconf = require('nconf');
+var fs = require('fs');
+var path = require('path');
+var less = require('less');
+var async = require('async');
+var autoprefixer = require('autoprefixer');
+var postcss = require('postcss');
- plugins = require('../plugins'),
- emitter = require('../emitter'),
- db = require('../database'),
- file = require('../file'),
- utils = require('../../public/src/utils');
+var plugins = require('../plugins');
+var emitter = require('../emitter');
+var db = require('../database');
+var file = require('../file');
+var utils = require('../../public/src/utils');
-module.exports = function(Meta) {
+module.exports = function (Meta) {
Meta.css = {};
Meta.css.cache = undefined;
Meta.css.acpCache = undefined;
- Meta.css.minify = function(callback) {
- callback = callback || function() {};
+ Meta.css.minify = function (callback) {
+ callback = callback || function () {};
if (nconf.get('isPrimary') !== 'true') {
winston.verbose('[meta/css] Cluster worker ' + process.pid + ' skipping LESS/CSS compilation');
return callback();
}
winston.verbose('[meta/css] Minifying LESS/CSS');
- db.getObjectFields('config', ['theme:type', 'theme:id'], function(err, themeData) {
+ db.getObjectFields('config', ['theme:type', 'theme:id'], function (err, themeData) {
if (err) {
return callback(err);
}
@@ -40,34 +39,35 @@ 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";',
- acpSource = '@import "font-awesome";';
+ source = '@import "font-awesome";';
plugins.lessFiles = filterMissingFiles(plugins.lessFiles);
plugins.cssFiles = filterMissingFiles(plugins.cssFiles);
async.waterfall([
- function(next) {
+ function (next) {
getStyleSource(plugins.lessFiles, '\n@import ".', '.less', next);
},
- function(src, next) {
+ function (src, next) {
source += src;
getStyleSource(plugins.cssFiles, '\n@import (inline) ".', '.css', next);
},
- function(src, next) {
+ function (src, next) {
source += src;
next();
}
- ], function(err) {
+ ], function (err) {
if (err) {
return callback(err);
}
- source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui-1.10.4.custom.min.css";';
+ var acpSource = source;
+
+ 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";';
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/blacklist.less";';
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/generics.less";';
@@ -76,14 +76,15 @@ 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') || '';
async.series([
- function(next) {
+ function (next) {
if (fromFile.match('clientLess')) {
winston.info('[minifier] Compiling front-end LESS files skipped');
return Meta.css.getFromFile(path.join(__dirname, '../../public/stylesheet.css'), 'cache', next);
@@ -91,7 +92,7 @@ module.exports = function(Meta) {
minify(source, paths, 'cache', next);
},
- function(next) {
+ function (next) {
if (fromFile.match('acpLess')) {
winston.info('[minifier] Compiling ACP LESS files skipped');
return Meta.css.getFromFile(path.join(__dirname, '../../public/admin.css'), 'acpCache', next);
@@ -99,7 +100,7 @@ module.exports = function(Meta) {
minify(acpSource, paths, 'acpCache', next);
}
- ], function(err, minified) {
+ ], function (err, minified) {
if (err) {
return callback(err);
}
@@ -125,7 +126,7 @@ module.exports = function(Meta) {
var pluginDirectories = [],
source = '';
- files.forEach(function(styleFile) {
+ files.forEach(function (styleFile) {
if (styleFile.endsWith(extension)) {
source += prefix + path.sep + styleFile + '";';
} else {
@@ -133,27 +134,27 @@ module.exports = function(Meta) {
}
});
- async.each(pluginDirectories, function(directory, next) {
- utils.walk(directory, function(err, styleFiles) {
+ async.each(pluginDirectories, function (directory, next) {
+ utils.walk(directory, function (err, styleFiles) {
if (err) {
return next(err);
}
- styleFiles.forEach(function(styleFile) {
+ styleFiles.forEach(function (styleFile) {
source += prefix + path.sep + styleFile + '";';
});
next();
});
- }, function(err) {
+ }, function (err) {
callback(err, source);
});
}
- Meta.css.commitToFile = function(filename, callback) {
+ Meta.css.commitToFile = function (filename, callback) {
var file = (filename === 'acpCache' ? 'admin' : 'stylesheet') + '.css';
- fs.writeFile(path.join(__dirname, '../../public/' + file), Meta.css[filename], function(err) {
+ fs.writeFile(path.join(__dirname, '../../public/' + file), Meta.css[filename], function (err) {
if (!err) {
winston.verbose('[meta/css] ' + file + ' committed to disk.');
} else {
@@ -165,10 +166,14 @@ module.exports = function(Meta) {
});
};
- Meta.css.getFromFile = function(filePath, filename, callback) {
+ Meta.css.getFromFile = function (filePath, filename, callback) {
winston.verbose('[meta/css] Reading stylesheet ' + filePath.split('/').pop() + ' from file');
- fs.readFile(filePath, function(err, file) {
+ fs.readFile(filePath, function (err, file) {
+ if (err) {
+ return callback(err);
+ }
+
Meta.css[filename] = file;
callback();
});
@@ -178,7 +183,7 @@ module.exports = function(Meta) {
less.render(source, {
paths: paths,
compress: true
- }, function(err, lessOutput) {
+ }, function (err, lessOutput) {
if (err) {
winston.error('[meta/css] Could not minify LESS/CSS: ' + err.message);
if (typeof callback === 'function') {
@@ -195,8 +200,8 @@ module.exports = function(Meta) {
Meta.css[destination] = result.css;
// Save the compiled CSS in public/ so things like nginx can serve it
- if (nconf.get('isPrimary') === 'true') {
- return Meta.css.commitToFile(destination, function() {
+ if (nconf.get('isPrimary') === 'true' && (nconf.get('local-assets') === undefined || nconf.get('local-assets') !== false)) {
+ return Meta.css.commitToFile(destination, function () {
if (typeof callback === 'function') {
callback(null, result.css);
}
@@ -212,7 +217,7 @@ module.exports = function(Meta) {
}
function filterMissingFiles(files) {
- return files.filter(function(filePath) {
+ return files.filter(function (filePath) {
var exists = file.existsSync(path.join(__dirname, '../../node_modules', filePath));
if (!exists) {
winston.warn('[meta/css] File not found! ' + filePath);
diff --git a/src/meta/dependencies.js b/src/meta/dependencies.js
index 5482ad3e7f..f115ff6bfc 100644
--- a/src/meta/dependencies.js
+++ b/src/meta/dependencies.js
@@ -1,24 +1,27 @@
'use strict';
-var path = require('path'),
- fs = require('fs'),
- async = require('async'),
- semver = require('semver'),
- winston = require('winston'),
+var path = require('path');
+var fs = require('fs');
+var async = require('async');
+var semver = require('semver');
+var winston = require('winston');
- pkg = require('../../package.json');
+var pkg = require('../../package.json');
-module.exports = function(Meta) {
+module.exports = function (Meta) {
Meta.dependencies = {};
- Meta.dependencies.check = function(callback) {
+ Meta.dependencies.check = function (callback) {
var modules = Object.keys(pkg.dependencies);
+ var depsOutdated = false;
+ var depsMissing = false;
+
winston.verbose('Checking dependencies for outdated modules');
- async.every(modules, function(module, next) {
+ async.every(modules, function (module, next) {
fs.readFile(path.join(__dirname, '../../node_modules/', module, 'package.json'), {
encoding: 'utf-8'
- }, function(err, pkgData) {
+ }, function (err, pkgData) {
// If a bundled plugin/theme is not present, skip the dep check (#3384)
if (err && err.code === 'ENOENT' && (module === 'nodebb-rewards-essentials' || module.startsWith('nodebb-plugin') || module.startsWith('nodebb-theme'))) {
winston.warn('[meta/dependencies] Bundled plugin ' + module + ' not found, skipping dependency check.');
@@ -33,15 +36,23 @@ module.exports = function(Meta) {
next(true);
} else {
process.stdout.write('[' + 'outdated'.yellow + '] ' + module.bold + ' installed v' + pkgData.version + ', package.json requires ' + pkg.dependencies[module] + '\n');
- next(false);
+ depsOutdated = true;
+ next(true);
}
} catch(e) {
- winston.error('[meta/dependencies] Could not read: ' + module);
- process.exit();
+ process.stdout.write('[' + 'missing'.red + '] ' + module.bold + ' is a required dependency but could not be found\n');
+ depsMissing = true;
+ next(true);
}
});
- }, function(ok) {
- callback(!ok && global.env !== 'development' ? new Error('dependencies-out-of-date') : null);
+ }, function (ok) {
+ if (depsMissing) {
+ callback(new Error('dependencies-missing'));
+ } else if (depsOutdated) {
+ callback(global.env !== 'development' ? new Error('dependencies-out-of-date') : null);
+ } else {
+ callback(null);
+ }
});
};
};
diff --git a/src/meta/errors.js b/src/meta/errors.js
new file mode 100644
index 0000000000..58e381e270
--- /dev/null
+++ b/src/meta/errors.js
@@ -0,0 +1,37 @@
+'use strict';
+
+var validator = require('validator');
+
+var db = require('../database');
+var analytics = require('../analytics');
+
+module.exports = function (Meta) {
+
+ Meta.errors = {};
+
+ Meta.errors.log404 = function (route, callback) {
+ callback = callback || function () {};
+ route = route.replace(/\/$/, ''); // remove trailing slashes
+ analytics.increment('errors:404');
+ db.sortedSetIncrBy('errors:404', 1, route, callback);
+ };
+
+ Meta.errors.get = function (escape, callback) {
+ db.getSortedSetRevRangeWithScores('errors:404', 0, -1, function (err, data) {
+ if (err) {
+ return callback(err);
+ }
+
+ data = data.map(function (nfObject) {
+ nfObject.value = escape ? validator.escape(String(nfObject.value || '')) : nfObject.value;
+ return nfObject;
+ });
+
+ callback(null, data);
+ });
+ };
+
+ Meta.errors.clear = function (callback) {
+ db.delete('errors:404', callback);
+ };
+};
diff --git a/src/meta/js.js b/src/meta/js.js
index 740b5e08ee..cfb588125d 100644
--- a/src/meta/js.js
+++ b/src/meta/js.js
@@ -1,47 +1,47 @@
'use strict';
-var winston = require('winston'),
- fork = require('child_process').fork,
- path = require('path'),
- async = require('async'),
- nconf = require('nconf'),
- fs = require('fs'),
- file = require('../file'),
- plugins = require('../plugins'),
- emitter = require('../emitter'),
- utils = require('../../public/src/utils');
+var winston = require('winston');
+var fork = require('child_process').fork;
+var path = require('path');
+var async = require('async');
+var nconf = require('nconf');
+var fs = require('fs');
+var file = require('../file');
+var plugins = require('../plugins');
+var emitter = require('../emitter');
+var utils = require('../../public/src/utils');
-module.exports = function(Meta) {
+module.exports = function (Meta) {
Meta.js = {
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',
'public/vendor/xregexp/unicode/unicode-base.js',
- 'public/vendor/buzz/buzz.min.js',
- 'public/vendor/mousetrap/mousetrap.js',
- 'public/vendor/autosize.js',
'./node_modules/templates.js/lib/templates.js',
'public/src/utils.js',
'public/src/sockets.js',
'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
rjs: [
'public/src/client/footer.js',
'public/src/client/chats.js',
@@ -61,7 +61,6 @@ module.exports = function(Meta) {
'public/src/client/category.js',
'public/src/client/categoryTools.js',
- 'public/src/modules/csrf.js',
'public/src/modules/translator.js',
'public/src/modules/notifications.js',
'public/src/modules/chat.js',
@@ -76,11 +75,53 @@ module.exports = function(Meta) {
'public/src/modules/helpers.js',
'public/src/modules/sounds.js',
'public/src/modules/string.js'
- ]
+ ],
+
+ // 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.min.js',
+ "jqueryui.js": 'public/vendor/jquery/js/jquery-ui.js',
+ "buzz.js": 'public/vendor/buzz/buzz.js'
+ }
}
};
- Meta.js.minify = function(target, callback) {
+ Meta.js.bridgeModules = function (app, callback) {
+ // Add routes for AMD-type modules to serve those files
+ var numBridged = 0,
+ addRoute = function (relPath) {
+ var relativePath = nconf.get('relative_path');
+
+ app.get(relativePath + '/src/modules/' + relPath, function (req, res) {
+ return res.sendFile(path.join(__dirname, '../../', Meta.js.scripts.modules[relPath]), {
+ maxAge: app.enabled('cache') ? 5184000000 : 0
+ });
+ });
+ };
+
+ async.series([
+ function (next) {
+ for(var relPath in Meta.js.scripts.modules) {
+ if (Meta.js.scripts.modules.hasOwnProperty(relPath)) {
+ addRoute(relPath);
+ ++numBridged;
+ }
+ }
+
+ next();
+ }
+ ], function (err) {
+ if (err) {
+ winston.error('[meta/js] Encountered error while bridging modules:' + err.message);
+ }
+
+ winston.verbose('[meta/js] ' + numBridged + ' of ' + Object.keys(Meta.js.scripts.modules).length + ' modules bridged');
+ callback(err);
+ });
+ };
+
+ Meta.js.minify = function (target, callback) {
if (nconf.get('isPrimary') !== 'true') {
if (typeof callback === 'function') {
callback();
@@ -96,7 +137,7 @@ module.exports = function(Meta) {
Meta.js.target[target] = {};
- Meta.js.prepare(target, function() {
+ Meta.js.prepare(target, function () {
minifier.send({
action: 'js',
minify: global.env !== 'development',
@@ -104,7 +145,7 @@ module.exports = function(Meta) {
});
});
- minifier.on('message', function(message) {
+ minifier.on('message', function (message) {
switch(message.type) {
case 'end':
Meta.js.target[target].cache = message.minified;
@@ -119,11 +160,18 @@ module.exports = function(Meta) {
});
}
- Meta.js.commitToFile(target, function() {
+ if (nconf.get('local-assets') === undefined || nconf.get('local-assets') !== false) {
+ return Meta.js.commitToFile(target, function () {
+ if (typeof callback === 'function') {
+ callback();
+ }
+ });
+ } else {
+ emitter.emit('meta:js.compiled');
if (typeof callback === 'function') {
- callback();
+ return callback();
}
- });
+ }
break;
case 'error':
@@ -140,12 +188,12 @@ module.exports = function(Meta) {
});
};
- Meta.js.prepare = function(target, callback) {
+ Meta.js.prepare = function (target, callback) {
var pluginsScripts = [];
var pluginDirectories = [];
- pluginsScripts = plugins[target === 'nodebb.min.js' ? 'clientScripts' : 'acpScripts'].filter(function(path) {
+ pluginsScripts = plugins[target === 'nodebb.min.js' ? 'clientScripts' : 'acpScripts'].filter(function (path) {
if (path.endsWith('.js')) {
return true;
}
@@ -154,12 +202,12 @@ module.exports = function(Meta) {
return false;
});
- async.each(pluginDirectories, function(directory, next) {
- utils.walk(directory, function(err, scripts) {
+ async.each(pluginDirectories, function (directory, next) {
+ utils.walk(directory, function (err, scripts) {
pluginsScripts = pluginsScripts.concat(scripts);
next(err);
});
- }, function(err) {
+ }, function (err) {
if (err) {
return callback(err);
}
@@ -172,7 +220,7 @@ module.exports = function(Meta) {
Meta.js.target[target].scripts = Meta.js.target[target].scripts.concat(Meta.js.scripts.rjs);
}
- Meta.js.target[target].scripts = Meta.js.target[target].scripts.map(function(script) {
+ Meta.js.target[target].scripts = Meta.js.target[target].scripts.map(function (script) {
return path.relative(basePath, script).replace(/\\/g, '/');
});
@@ -180,13 +228,13 @@ module.exports = function(Meta) {
});
};
- Meta.js.killMinifier = function() {
+ Meta.js.killMinifier = function () {
if (Meta.js.minifierProc) {
Meta.js.minifierProc.kill('SIGTERM');
}
};
- Meta.js.commitToFile = function(target, callback) {
+ Meta.js.commitToFile = function (target, callback) {
fs.writeFile(path.join(__dirname, '../../public/' + target), Meta.js.target[target].cache, function (err) {
if (err) {
winston.error('[meta/js] ' + err.message);
@@ -198,12 +246,12 @@ module.exports = function(Meta) {
});
};
- Meta.js.getFromFile = function(target, callback) {
+ Meta.js.getFromFile = function (target, callback) {
var scriptPath = path.join(__dirname, '../../public/' + target),
mapPath = path.join(__dirname, '../../public/' + target + '.map'),
paths = [scriptPath];
- file.exists(scriptPath, function(exists) {
+ file.exists(scriptPath, function (exists) {
if (!exists) {
winston.warn('[meta/js] ' + target + ' not found on disk, re-minifying');
Meta.js.minify(target, callback);
@@ -214,12 +262,16 @@ module.exports = function(Meta) {
return callback();
}
- file.exists(mapPath, function(exists) {
+ file.exists(mapPath, function (exists) {
if (exists) {
paths.push(mapPath);
}
- async.map(paths, fs.readFile, function(err, files) {
+ 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/logs.js b/src/meta/logs.js
index 0b38b61a34..b335ff281e 100644
--- a/src/meta/logs.js
+++ b/src/meta/logs.js
@@ -1,19 +1,19 @@
'use strict';
-var path = require('path'),
- fs = require('fs'),
- winston = require('winston');
+var path = require('path');
+var fs = require('fs');
+var winston = require('winston');
-module.exports = function(Meta) {
+module.exports = function (Meta) {
Meta.logs = {
path: path.join('logs', path.sep, 'output.log')
};
- Meta.logs.get = function(callback) {
+ Meta.logs.get = function (callback) {
fs.readFile(this.path, {
encoding: 'utf-8'
- }, function(err, logs) {
+ }, function (err, logs) {
if (err) {
winston.error('[meta/logs] Could not retrieve logs: ' + err.message);
}
@@ -22,7 +22,7 @@ module.exports = function(Meta) {
});
};
- Meta.logs.clear = function(callback) {
+ Meta.logs.clear = function (callback) {
fs.truncate(this.path, 0, callback);
};
};
\ No newline at end of file
diff --git a/src/meta/settings.js b/src/meta/settings.js
index ed6702a943..299286abdd 100644
--- a/src/meta/settings.js
+++ b/src/meta/settings.js
@@ -1,25 +1,25 @@
'use strict';
-var db = require('../database'),
- plugins = require('../plugins');
+var db = require('../database');
+var plugins = require('../plugins');
-module.exports = function(Meta) {
+module.exports = function (Meta) {
Meta.settings = {};
- Meta.settings.get = function(hash, callback) {
- db.getObject('settings:' + hash, function(err, settings) {
+ Meta.settings.get = function (hash, callback) {
+ db.getObject('settings:' + hash, function (err, settings) {
callback(err, settings || {});
});
};
- Meta.settings.getOne = function(hash, field, callback) {
+ Meta.settings.getOne = function (hash, field, callback) {
db.getObjectField('settings:' + hash, field, callback);
};
- Meta.settings.set = function(hash, values, callback) {
+ Meta.settings.set = function (hash, values, callback) {
var key = 'settings:' + hash;
- db.setObject(key, values, function(err) {
+ db.setObject(key, values, function (err) {
if (err) {
return callback(err);
}
@@ -34,18 +34,25 @@ module.exports = function(Meta) {
});
};
- Meta.settings.setOne = function(hash, field, value, callback) {
+ Meta.settings.setOne = function (hash, field, value, callback) {
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 4b85f681cb..6068f16f5f 100644
--- a/src/meta/sounds.js
+++ b/src/meta/sounds.js
@@ -1,21 +1,21 @@
'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 plugins = require('../plugins');
+var db = require('../database');
-module.exports = function(Meta) {
+module.exports = function (Meta) {
Meta.sounds = {};
- Meta.sounds.init = function(callback) {
+ Meta.sounds.init = function (callback) {
if (nconf.get('isPrimary') === 'true') {
setupSounds(callback);
} else {
@@ -25,21 +25,21 @@ module.exports = function(Meta) {
}
};
- Meta.sounds.getFiles = function(callback) {
+ Meta.sounds.getFiles = function (callback) {
async.waterfall([
- function(next) {
+ function (next) {
fs.readdir(path.join(__dirname, '../../public/sounds'), next);
},
- function(sounds, next) {
- fs.readdir(path.join(__dirname, '../../public/uploads/sounds'), function(err, uploaded) {
+ function (sounds, next) {
+ fs.readdir(path.join(__dirname, '../../public/uploads/sounds'), function (err, uploaded) {
next(err, sounds.concat(uploaded));
});
}
- ], function(err, files) {
+ ], function (err, files) {
var localList = {};
// Filter out hidden files
- files = files.filter(function(filename) {
+ files = files.filter(function (filename) {
return !filename.startsWith('.');
});
@@ -50,7 +50,7 @@ module.exports = function(Meta) {
}
// Return proper paths
- files.forEach(function(filename) {
+ files.forEach(function (filename) {
localList[filename] = nconf.get('relative_path') + '/sounds/' + filename;
});
@@ -58,20 +58,32 @@ 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) {
+ var user = require('../user');
+ 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);
});
};
@@ -79,50 +91,67 @@ module.exports = function(Meta) {
var soundsPath = path.join(__dirname, '../../public/sounds');
async.waterfall([
- function(next) {
+ function (next) {
fs.readdir(path.join(__dirname, '../../public/uploads/sounds'), next);
},
- function(uploaded, next) {
- uploaded = uploaded.map(function(filename) {
+ function (uploaded, next) {
+ uploaded = uploaded.filter(function (filename) {
+ return !filename.startsWith('.');
+ }).map(function (filename) {
return path.join(__dirname, '../../public/uploads/sounds', filename);
});
- plugins.fireHook('filter:sounds.get', uploaded, function(err, filePaths) {
+ plugins.fireHook('filter:sounds.get', uploaded, function (err, filePaths) {
if (err) {
winston.error('Could not initialise sound files:' + err.message);
return;
}
+ if (nconf.get('local-assets') === false) {
+ // Don't regenerate the public/sounds/ directory. Instead, create a mapping for the router to use
+ Meta.sounds._filePathHash = filePaths.reduce(function (hash, filePath) {
+ hash[path.basename(filePath)] = filePath;
+ return hash;
+ }, {});
+
+ winston.verbose('[sounds] Sounds OK');
+ if (typeof next === 'function') {
+ return next();
+ } else {
+ return;
+ }
+ }
+
// Clear the sounds directory
async.series([
- function(next) {
+ function (next) {
rimraf(soundsPath, next);
},
- function(next) {
+ function (next) {
mkdirp(soundsPath, next);
}
- ], function(err) {
+ ], function (err) {
if (err) {
winston.error('Could not initialise sound files:' + err.message);
return;
}
// Link paths
- async.each(filePaths, function(filePath, next) {
+ async.each(filePaths, function (filePath, next) {
if (process.platform === 'win32') {
fs.link(filePath, path.join(soundsPath, path.basename(filePath)), next);
} else {
fs.symlink(filePath, path.join(soundsPath, path.basename(filePath)), 'file', next);
}
- }, function(err) {
+ }, function (err) {
if (!err) {
winston.verbose('[sounds] Sounds OK');
} else {
winston.error('[sounds] Could not initialise sounds: ' + err.message);
}
- if (typeof callback === 'function') {
- callback();
+ if (typeof next === 'function') {
+ next();
}
});
});
diff --git a/src/meta/tags.js b/src/meta/tags.js
index 3747db61da..1d64a7f93d 100644
--- a/src/meta/tags.js
+++ b/src/meta/tags.js
@@ -1,20 +1,20 @@
'use strict';
-var nconf = require('nconf'),
- validator = require('validator'),
- async = require('async'),
- winston = require('winston'),
- plugins = require('../plugins');
+var nconf = require('nconf');
+var validator = require('validator');
+var async = require('async');
+var winston = require('winston');
+var plugins = require('../plugins');
-module.exports = function(Meta) {
+module.exports = function (Meta) {
Meta.tags = {};
- Meta.tags.parse = function(meta, link, callback) {
+ Meta.tags.parse = function (meta, link, callback) {
async.parallel({
- tags: function(next) {
+ 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',
@@ -42,11 +42,11 @@ module.exports = function(Meta) {
}];
plugins.fireHook('filter:meta.getMetaTags', defaultTags, next);
},
- links: function(next) {
+ links: function (next) {
var defaultLinks = [{
rel: "icon",
type: "image/x-icon",
- href: nconf.get('relative_path') + '/favicon.ico?' + Meta.config['cache-buster']
+ href: nconf.get('relative_path') + '/favicon.ico' + (Meta.config['cache-buster'] ? '?' + Meta.config['cache-buster'] : '')
}, {
rel: "manifest",
href: nconf.get('relative_path') + '/manifest.json'
@@ -85,15 +85,19 @@ module.exports = function(Meta) {
}
plugins.fireHook('filter:meta.getLinkTags', defaultLinks, next);
}
- }, function(err, results) {
- meta = results.tags.concat(meta || []).map(function(tag) {
+ }, 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);
return tag;
}
if (!tag.noEscape) {
- tag.content = validator.escape(tag.content);
+ tag.content = validator.escape(String(tag.content));
}
return tag;
@@ -112,7 +116,7 @@ module.exports = function(Meta) {
function addDescription(meta) {
var hasDescription = false;
- meta.forEach(function(tag) {
+ meta.forEach(function (tag) {
if (tag.name === 'description') {
hasDescription = true;
}
@@ -121,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/templates.js b/src/meta/templates.js
index 88578b235e..d335709461 100644
--- a/src/meta/templates.js
+++ b/src/meta/templates.js
@@ -1,22 +1,22 @@
"use strict";
-var mkdirp = require('mkdirp'),
- rimraf = require('rimraf'),
- winston = require('winston'),
- async = require('async'),
- path = require('path'),
- fs = require('fs'),
- nconf = require('nconf'),
+var mkdirp = require('mkdirp');
+var rimraf = require('rimraf');
+var winston = require('winston');
+var async = require('async');
+var path = require('path');
+var fs = require('fs');
+var nconf = require('nconf');
- emitter = require('../emitter'),
- plugins = require('../plugins'),
- utils = require('../../public/src/utils'),
+var emitter = require('../emitter');
+var plugins = require('../plugins');
+var utils = require('../../public/src/utils');
- Templates = {},
- searchIndex = {};
+var Templates = {};
+var searchIndex = {};
-Templates.compile = function(callback) {
- callback = callback || function() {};
+Templates.compile = function (callback) {
+ callback = callback || function () {};
var fromFile = nconf.get('from-file') || '';
if (nconf.get('isPrimary') === 'false' || fromFile.match('tpl')) {
@@ -58,13 +58,13 @@ function preparePaths(baseTemplatesPaths, callback) {
function (next) {
mkdirp(viewsPath, next);
},
- function(viewsPath, next) {
+ function (viewsPath, next) {
plugins.fireHook('static:templates.precompile', {}, next);
},
- function(next) {
+ function (next) {
plugins.getTemplates(next);
}
- ], function(err, pluginTemplates) {
+ ], function (err, pluginTemplates) {
if (err) {
return callback(err);
}
@@ -72,13 +72,13 @@ function preparePaths(baseTemplatesPaths, callback) {
winston.verbose('[meta/templates] Compiling templates');
async.parallel({
- coreTpls: function(next) {
+ coreTpls: function (next) {
utils.walk(coreTemplatesPath, next);
},
- baseThemes: function(next) {
- async.map(baseTemplatesPaths, function(baseTemplatePath, next) {
- utils.walk(baseTemplatePath, function(err, paths) {
- paths = paths.map(function(tpl) {
+ baseThemes: function (next) {
+ async.map(baseTemplatesPaths, function (baseTemplatePath, next) {
+ utils.walk(baseTemplatePath, function (err, paths) {
+ paths = paths.map(function (tpl) {
return {
base: baseTemplatePath,
path: tpl.replace(baseTemplatePath, '')
@@ -89,17 +89,17 @@ function preparePaths(baseTemplatesPaths, callback) {
});
}, next);
}
- }, function(err, data) {
+ }, function (err, data) {
var baseThemes = data.baseThemes,
coreTpls = data.coreTpls,
paths = {};
- coreTpls.forEach(function(el, i) {
+ coreTpls.forEach(function (el, i) {
paths[coreTpls[i].replace(coreTemplatesPath, '')] = coreTpls[i];
});
- baseThemes.forEach(function(baseTpls) {
- baseTpls.forEach(function(el, i) {
+ baseThemes.forEach(function (baseTpls) {
+ baseTpls.forEach(function (el, i) {
paths[baseTpls[i].path] = path.join(baseTpls[i].base, baseTpls[i].path);
});
});
@@ -121,12 +121,12 @@ function compile(callback) {
viewsPath = nconf.get('views_dir');
- preparePaths(baseTemplatesPaths, function(err, paths) {
+ preparePaths(baseTemplatesPaths, function (err, paths) {
if (err) {
return callback(err);
}
- async.each(Object.keys(paths), function(relativePath, next) {
+ async.each(Object.keys(paths), function (relativePath, next) {
var file = fs.readFileSync(paths[relativePath]).toString(),
matches = null,
regex = /[ \t]*[ \t]*/;
@@ -148,13 +148,13 @@ function compile(callback) {
mkdirp.sync(path.join(viewsPath, relativePath.split('/').slice(0, -1).join('/')));
fs.writeFile(path.join(viewsPath, relativePath), file, next);
- }, function(err) {
+ }, function (err) {
if (err) {
winston.error('[meta/templates] ' + err.stack);
return callback(err);
}
- compileIndex(viewsPath, function() {
+ compileIndex(viewsPath, function () {
winston.verbose('[meta/templates] Successfully compiled templates.');
emitter.emit('templates:compiled');
diff --git a/src/meta/themes.js b/src/meta/themes.js
index c3d912a222..416b9cb00d 100644
--- a/src/meta/themes.js
+++ b/src/meta/themes.js
@@ -1,17 +1,16 @@
'use strict';
-var nconf = require('nconf'),
- winston = require('winston'),
- fs = require('fs'),
- path = require('path'),
- async = require('async'),
+var nconf = require('nconf');
+var winston = require('winston');
+var fs = require('fs');
+var path = require('path');
+var async = require('async');
- file = require('../file'),
- db = require('../database'),
- meta = require('../meta');
+var file = require('../file');
+var db = require('../database');
-module.exports = function(Meta) {
+module.exports = function (Meta) {
Meta.themes = {};
Meta.themes.get = function (callback) {
@@ -56,6 +55,10 @@ module.exports = function(Meta) {
});
}, function (err, themes) {
+ if (err) {
+ return callback(err);
+ }
+
themes = themes.filter(function (theme) {
return (theme !== undefined);
});
@@ -65,7 +68,7 @@ module.exports = function(Meta) {
});
};
- Meta.themes.set = function(data, callback) {
+ Meta.themes.set = function (data, callback) {
var themeData = {
'theme:type': data.type,
'theme:id': data.id,
@@ -77,17 +80,17 @@ module.exports = function(Meta) {
switch(data.type) {
case 'local':
async.waterfall([
- async.apply(meta.configs.get, 'theme:id'),
- function(current, next) {
+ async.apply(Meta.configs.get, 'theme:id'),
+ function (current, next) {
async.series([
async.apply(db.sortedSetRemove, 'plugins:active', current),
async.apply(db.sortedSetAdd, 'plugins:active', 0, data.id)
- ], function(err) {
+ ], function (err) {
next(err);
});
},
- function(next) {
- fs.readFile(path.join(nconf.get('themes_path'), data.id, 'theme.json'), function(err, config) {
+ function (next) {
+ fs.readFile(path.join(nconf.get('themes_path'), data.id, 'theme.json'), function (err, config) {
if (!err) {
config = JSON.parse(config.toString());
next(null, config);
@@ -96,7 +99,7 @@ module.exports = function(Meta) {
}
});
},
- function(config, next) {
+ function (config, next) {
themeData['theme:staticDir'] = config.staticDir ? config.staticDir : '';
themeData['theme:templates'] = config.templates ? config.templates : '';
themeData['theme:src'] = '';
@@ -117,20 +120,20 @@ module.exports = function(Meta) {
}
};
- Meta.themes.setupPaths = function(callback) {
+ Meta.themes.setupPaths = function (callback) {
async.parallel({
themesData: Meta.themes.get,
- currentThemeId: function(next) {
+ currentThemeId: function (next) {
db.getObjectField('config', 'theme:id', next);
}
- }, function(err, data) {
+ }, function (err, data) {
if (err) {
return callback(err);
}
var themeId = data.currentThemeId || 'nodebb-theme-persona';
- var themeObj = data.themesData.filter(function(themeObj) {
+ var themeObj = data.themesData.filter(function (themeObj) {
return themeObj.id === themeId;
})[0];
@@ -147,7 +150,7 @@ module.exports = function(Meta) {
});
};
- Meta.themes.setPath = function(themeObj) {
+ Meta.themes.setPath = function (themeObj) {
// Theme's templates path
var themePath = nconf.get('base_templates_path'),
fallback = path.join(nconf.get('themes_path'), themeObj.id, 'templates');
@@ -161,6 +164,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 ceeabdca99..3240eaf0af 100644
--- a/src/middleware/admin.js
+++ b/src/middleware/admin.js
@@ -1,129 +1,122 @@
"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();
- });
-};
-
-middleware.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);
+ user.isAdministrator(req.user.uid, function (err, isAdmin) {
+ if (err || isAdmin) {
+ return next(err);
}
- }, function(err, results) {
+
+ controllers.helpers.notAllowed(req, res);
+ });
+ };
+
+ middleware.admin.buildHeader = function (req, res, next) {
+ res.locals.renderAdminHeader = true;
+
+ controllers.api.getConfig(req, res, function (err, config) {
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'
- };
-
- templateValues.template = {name: res.locals.template};
- templateValues.template[res.locals.template] = true;
-
- app.render('admin/header', templateValues, next);
+ res.locals.config = config;
+ 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);
+ });
+ });
+ };
+
+
+ middleware.admin.renderFooter = function (req, res, data, next) {
+ req.app.render('admin/footer', data, next);
+ };
};
diff --git a/src/middleware/header.js b/src/middleware/header.js
index c1ed6d60c2..2741a599f8 100644
--- a/src/middleware/header.js
+++ b/src/middleware/header.js
@@ -3,50 +3,44 @@
var async = require('async');
var nconf = require('nconf');
+var db = require('../database');
var user = require('../user');
var meta = require('../meta');
var plugins = require('../plugins');
var navigation = require('../navigation');
-var translator = require('../../public/src/modules/translator');
var controllers = {
api: require('../controllers/api'),
helpers: require('../controllers/helpers')
};
-module.exports = function(app, middleware) {
+module.exports = function (middleware) {
- middleware.buildHeader = function(req, res, next) {
+ middleware.buildHeader = function (req, res, next) {
res.locals.renderHeader = true;
res.locals.isAPI = false;
-
- middleware.applyCSRF(req, res, function() {
- async.parallel({
- config: function(next) {
- controllers.api.getConfig(req, res, next);
- },
- footer: function(next) {
- app.render('footer', {loggedIn: (req.user ? parseInt(req.user.uid, 10) !== 0 : false)}, next);
- },
- plugins: function(next) {
- plugins.fireHook('filter:middleware.buildHeader', {req: req, locals: res.locals}, next);
- }
- }, function(err, results) {
- if (err) {
- return next(err);
- }
-
+ async.waterfall([
+ function (next) {
+ middleware.applyCSRF(req, res, next);
+ },
+ function (next) {
+ async.parallel({
+ config: function (next) {
+ controllers.api.getConfig(req, res, next);
+ },
+ plugins: function (next) {
+ plugins.fireHook('filter:middleware.buildHeader', {req: req, locals: res.locals}, next);
+ }
+ }, next);
+ },
+ function (results, next) {
res.locals.config = results.config;
-
- translator.translate(results.footer, results.config.defaultLang, function(parsedTemplate) {
- res.locals.footer = parsedTemplate;
- next();
- });
- });
- });
+ next();
+ }
+ ], next);
};
- middleware.renderHeader = function(req, res, data, callback) {
+ middleware.renderHeader = function (req, res, data, callback) {
var registrationType = meta.config.registrationType || 'normal';
var templateValues = {
bootswatchCSS: meta.config['theme:src'],
@@ -56,8 +50,8 @@ module.exports = function(app, middleware) {
'brand:logo': meta.config['brand:logo'] || '',
'brand:logo:url': meta.config['brand:logo:url'] || '',
'brand:logo:alt': meta.config['brand:logo:alt'] || '',
- 'brand:logo:display': meta.config['brand:logo']?'':'hide',
- allowRegistration: registrationType === 'normal' || registrationType === 'admin-approval',
+ 'brand:logo:display': meta.config['brand:logo'] ? '' : 'hide',
+ allowRegistration: registrationType === 'normal' || registrationType === 'admin-approval' || registrationType === 'admin-approval-ip',
searchEnabled: plugins.hasListeners('filter:search.query'),
config: res.locals.config,
relative_path: nconf.get('relative_path'),
@@ -67,32 +61,45 @@ module.exports = function(app, middleware) {
templateValues.configJSON = JSON.stringify(res.locals.config);
async.parallel({
- scripts: function(next) {
+ scripts: function (next) {
plugins.fireHook('filter:scripts.get', [], next);
},
- isAdmin: function(next) {
+ isAdmin: function (next) {
user.isAdministrator(req.uid, next);
},
- isGlobalMod: function(next) {
+ isGlobalMod: function (next) {
user.isGlobalModerator(req.uid, next);
},
- user: function(next) {
+ isModerator: function (next) {
+ user.isModeratorOfAnyCategory(req.uid, next);
+ },
+ user: function (next) {
+ var userData = {
+ uid: 0,
+ username: '[[global:guest]]',
+ userslug: '',
+ email: '',
+ picture: meta.config.defaultAvatar,
+ status: 'offline',
+ banned: false,
+ reputation: 0,
+ 'email:confirmed': false
+ };
if (req.uid) {
- user.getUserFields(req.uid, ['username', 'userslug', 'email', 'picture', 'status', 'email:confirmed', 'banned'], next);
+ user.getUserFields(req.uid, Object.keys(userData), next);
} else {
- next(null, {
- username: '[[global:guest]]',
- userslug: '',
- picture: meta.config.defaultAvatar,
- status: 'offline',
- banned: false,
- uid: 0
- });
+ next(null, userData);
}
},
+ isEmailConfirmSent: function (next) {
+ if (!meta.config.requireEmailConfirmation || !req.uid) {
+ return next(null, false);
+ }
+ db.get('uid:' + req.uid + ':confirm:email:sent', next);
+ },
navigation: async.apply(navigation.get),
tags: async.apply(meta.tags.parse, res.locals.metaTags, res.locals.linkTags)
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return callback(err);
}
@@ -104,8 +111,11 @@ module.exports = function(app, middleware) {
results.user.isAdmin = results.isAdmin;
results.user.isGlobalMod = results.isGlobalMod;
+ results.user.isMod = !!results.isModerator;
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;
if (parseInt(meta.config.disableCustomUserSkins, 10) !== 1 && res.locals.config.bootswatchSkin !== 'default') {
templateValues.bootswatchCSS = '//maxcdn.bootstrapcdn.com/bootswatch/latest/' + res.locals.config.bootswatchSkin + '/bootstrap.min.css';
@@ -117,6 +127,7 @@ module.exports = function(app, middleware) {
templateValues.linkTags = results.tags.link;
templateValues.isAdmin = results.user.isAdmin;
templateValues.isGlobalMod = results.user.isGlobalMod;
+ templateValues.showModMenu = results.user.isAdmin || results.user.isGlobalMod || results.user.isMod;
templateValues.user = results.user;
templateValues.userJSON = JSON.stringify(results.user);
templateValues.useCustomCSS = parseInt(meta.config.useCustomCSS, 10) === 1 && meta.config.customCSS;
@@ -131,7 +142,7 @@ module.exports = function(app, middleware) {
templateValues.template = {name: res.locals.template};
templateValues.template[res.locals.template] = true;
- templateValues.scripts = results.scripts.map(function(script) {
+ templateValues.scripts = results.scripts.map(function (script) {
return {src: script};
});
@@ -139,23 +150,31 @@ module.exports = function(app, middleware) {
modifyTitle(templateValues);
}
- plugins.fireHook('filter:middleware.renderHeader', {templateValues: templateValues, req: req, res: res}, function(err, data) {
+ plugins.fireHook('filter:middleware.renderHeader', {templateValues: templateValues, req: req, res: res}, function (err, data) {
if (err) {
return callback(err);
}
- app.render('header', data.templateValues, callback);
+ req.app.render('header', data.templateValues, callback);
});
});
};
+ middleware.renderFooter = function (req, res, data, callback) {
+ plugins.fireHook('filter:middleware.renderFooter', {templateValues: data, req: req, res: res}, function (err, data) {
+ if (err) {
+ return callback(err);
+ }
+ req.app.render('footer', data.templateValues, callback);
+ });
+ };
function modifyTitle(obj) {
var title = controllers.helpers.buildTitle('[[pages:home]]');
obj.browserTitle = title;
if (obj.metaTags) {
- obj.metaTags.forEach(function(tag, i) {
+ obj.metaTags.forEach(function (tag, i) {
if (tag.property === 'og:title') {
obj.metaTags[i].content = title;
}
diff --git a/src/middleware/headers.js b/src/middleware/headers.js
new file mode 100644
index 0000000000..f6e0a22562
--- /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 1cbac02323..fdef19db32 100644
--- a/src/middleware/index.js
+++ b/src/middleware/index.js
@@ -1,79 +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');
}
-
- 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..3abe15f6f8 100644
--- a/src/middleware/maintenance.js
+++ b/src/middleware/maintenance.js
@@ -3,54 +3,56 @@
var nconf = require('nconf');
var meta = require('../meta');
var user = require('../user');
-var translator = require('../../public/src/modules/translator');
+module.exports = function (middleware) {
-module.exports = function(middleware) {
-
- middleware.maintenanceMode = function(req, res, next) {
+ middleware.maintenanceMode = function (req, res, next) {
if (parseInt(meta.config.maintenanceMode, 10) !== 1) {
return next();
}
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 < numAllowed;x++) {
+ route = new RegExp(allowedRoutes[x]);
+ if (route.test(url)) {
+ return true;
}
- },
- isAllowed = function(url) {
- for(var x=0,numAllowed=allowedRoutes.length,route;x' + ajaxifyData + '';
- fn(err, translated);
- });
- });
- } else {
- str = str + '';
- fn(err, str);
+
+ return res.json(options);
+ }
+
+ ajaxifyData = JSON.stringify(options).replace(/<\//g, '<\\/');
+
+ async.parallel({
+ header: function (next) {
+ renderHeaderFooter('renderHeader', req, res, options, next);
+ },
+ content: function (next) {
+ render.call(self, template, options, next);
+ },
+ footer: function (next) {
+ renderHeaderFooter('renderFooter', req, res, options, next);
+ }
+ }, next);
+ },
+ function (results, next) {
+ var str = results.header +
+ (res.locals.postHeader || '') +
+ results.content +
+ (res.locals.preFooter || '') +
+ results.footer;
+
+ translate(str, req, res, next);
+ },
+ function (translated, next) {
+ next(null, translated + '');
}
- });
+ ], fn);
};
next();
};
+ function renderHeaderFooter(method, req, res, options, next) {
+ if (res.locals.renderHeader) {
+ middleware[method](req, res, options, next);
+ } else if (res.locals.renderAdminHeader) {
+ middleware.admin[method](req, res, options, next);
+ } else {
+ next(null, '');
+ }
+ }
+
+ function translate(str, req, res, next) {
+ var language = res.locals.config ? res.locals.config.userLang || 'en_GB' : 'en_GB';
+ language = req.query.lang ? validator.escape(String(req.query.lang)) : language;
+ translator.translate(str, language, function (translated) {
+ next(null, translator.unescape(translated));
+ });
+ }
+
function buildBodyClass(req) {
- var clean = req.path.replace(/^\/api/, '').replace(/^\//, '');
+ var clean = req.path.replace(/^\/api/, '').replace(/^\/|\/$/g, '');
var parts = clean.split('/').slice(0, 3);
- parts.forEach(function(p, index) {
+ parts.forEach(function (p, index) {
parts[index] = index ? parts[0] + '-' + p : 'page-' + (p || 'home');
});
return parts.join(' ');
diff --git a/src/middleware/user.js b/src/middleware/user.js
new file mode 100644
index 0000000000..a9dc90eb94
--- /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/navigation/admin.js b/src/navigation/admin.js
index a803b5465b..a7caee97e0 100644
--- a/src/navigation/admin.js
+++ b/src/navigation/admin.js
@@ -9,9 +9,9 @@ var admin = {},
admin.cache = null;
-admin.save = function(data, callback) {
+admin.save = function (data, callback) {
var order = Object.keys(data),
- items = data.map(function(item, idx) {
+ items = data.map(function (item, idx) {
var data = {};
for (var i in item) {
@@ -26,29 +26,29 @@ admin.save = function(data, callback) {
admin.cache = null;
async.waterfall([
- function(next) {
+ function (next) {
db.delete('navigation:enabled', next);
},
- function(next) {
+ function (next) {
db.sortedSetAdd('navigation:enabled', order, items, next);
}
], callback);
};
-admin.getAdmin = function(callback) {
+admin.getAdmin = function (callback) {
async.parallel({
enabled: admin.get,
available: getAvailable
}, callback);
};
-admin.get = function(callback) {
- db.getSortedSetRange('navigation:enabled', 0, -1, function(err, data) {
+admin.get = function (callback) {
+ db.getSortedSetRange('navigation:enabled', 0, -1, function (err, data) {
if (err) {
return callback(err);
}
- data = data.map(function(item, idx) {
+ data = data.map(function (item, idx) {
return JSON.parse(item)[idx];
});
@@ -57,17 +57,12 @@ admin.get = function(callback) {
};
function getAvailable(callback) {
- var core = require('../../install/data/navigation.json').map(function(item) {
+ var core = require('../../install/data/navigation.json').map(function (item) {
item.core = true;
return item;
});
- // DEPRECATION: backwards compatibility for filter:header.build, will be removed soon.
- plugins.fireHook('filter:header.build', {navigation: []}, function(err, data) {
- core = core.concat(data.navigation);
-
- plugins.fireHook('filter:navigation.available', core, callback);
- });
+ plugins.fireHook('filter:navigation.available', core, callback);
}
module.exports = admin;
\ No newline at end of file
diff --git a/src/navigation/index.js b/src/navigation/index.js
index ad4627fd5b..5563c44c4b 100644
--- a/src/navigation/index.js
+++ b/src/navigation/index.js
@@ -6,19 +6,19 @@ var translator = require('../../public/src/modules/translator');
var navigation = {};
-navigation.get = function(callback) {
+navigation.get = function (callback) {
if (admin.cache) {
return callback(null, admin.cache);
}
- admin.get(function(err, data) {
+ admin.get(function (err, data) {
if (err) {
return callback(err);
}
- data = data.filter(function(item) {
+ data = data.filter(function (item) {
return item && item.enabled;
- }).map(function(item) {
+ }).map(function (item) {
if (!item.route.startsWith('http')) {
item.route = nconf.get('relative_path') + item.route;
}
diff --git a/src/notifications.js b/src/notifications.js
index 94cb5313b8..c70c0cbb0a 100644
--- a/src/notifications.js
+++ b/src/notifications.js
@@ -1,122 +1,130 @@
'use strict';
-var async = require('async'),
- winston = require('winston'),
- cron = require('cron').CronJob,
- nconf = require('nconf'),
- S = require('string'),
- _ = require('underscore'),
+var async = require('async');
+var winston = require('winston');
+var cron = require('cron').CronJob;
+var nconf = require('nconf');
+var S = require('string');
+var _ = require('underscore');
- db = require('./database'),
- User = require('./user'),
- groups = require('./groups'),
- meta = require('./meta'),
- plugins = require('./plugins');
+var db = require('./database');
+var User = require('./user');
+var groups = require('./groups');
+var meta = require('./meta');
+var batch = require('./batch');
+var plugins = require('./plugins');
+var utils = require('../public/src/utils');
-(function(Notifications) {
+(function (Notifications) {
- Notifications.init = function() {
+ Notifications.init = function () {
winston.verbose('[notifications.init] Registering jobs.');
new cron('*/30 * * * *', Notifications.prune, null, true);
};
- Notifications.get = function(nid, callback) {
- Notifications.getMultiple([nid], function(err, notifications) {
+ Notifications.get = function (nid, callback) {
+ Notifications.getMultiple([nid], function (err, notifications) {
callback(err, Array.isArray(notifications) && notifications.length ? notifications[0] : null);
});
};
- Notifications.getMultiple = function(nids, callback) {
- var keys = nids.map(function(nid) {
+ Notifications.getMultiple = function (nids, callback) {
+ var keys = nids.map(function (nid) {
return 'notifications:' + nid;
});
- db.getObjects(keys, function(err, notifications) {
+ db.getObjects(keys, function (err, notifications) {
if (err) {
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);
- 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);
+ });
});
};
- Notifications.findRelated = function(mergeIds, set, callback) {
+ Notifications.filterExists = function (nids, callback) {
+ // Removes nids that have been pruned
+ db.isSortedSetMembers('notifications', nids, function (err, exists) {
+ if (err) {
+ return callback(err);
+ }
+
+ nids = nids.filter(function (notifId, idx) {
+ return exists[idx];
+ });
+
+ callback(null, nids);
+ });
+ };
+
+ Notifications.findRelated = function (mergeIds, set, callback) {
// A related notification is one in a zset that has the same mergeId
var _nids;
async.waterfall([
async.apply(db.getSortedSetRevRange, set, 0, -1),
- function(nids, next) {
+ function (nids, next) {
_nids = nids;
- var keys = nids.map(function(nid) {
+ var keys = nids.map(function (nid) {
return 'notifications:' + nid;
});
db.getObjectsFields(keys, ['mergeId'], next);
},
- ], function(err, sets) {
+ ], function (err, sets) {
if (err) {
return callback(err);
}
- sets = sets.map(function(set) {
+ sets = sets.map(function (set) {
return set.mergeId;
});
- callback(null, _nids.filter(function(nid, idx) {
+ callback(null, _nids.filter(function (nid, idx) {
return mergeIds.indexOf(sets[idx]) !== -1;
}));
});
};
- Notifications.create = function(data, callback) {
+ Notifications.create = function (data, callback) {
if (!data.nid) {
return callback(new Error('no-notification-id'));
}
data.importance = data.importance || 5;
- db.getObject('notifications:' + data.nid, function(err, oldNotification) {
+ db.getObject('notifications:' + data.nid, function (err, oldNotification) {
if (err) {
return callback(err);
}
@@ -130,22 +138,22 @@ var async = require('async'),
var now = Date.now();
data.datetime = now;
async.parallel([
- function(next) {
+ function (next) {
db.sortedSetAdd('notifications', now, data.nid, next);
},
- function(next) {
+ function (next) {
db.setObject('notifications:' + data.nid, data, next);
}
- ], function(err) {
+ ], function (err) {
callback(err, data);
});
});
};
- Notifications.push = function(notification, uids, callback) {
- callback = callback || function() {};
+ Notifications.push = function (notification, uids, callback) {
+ callback = callback || function () {};
- if (!notification.nid) {
+ if (!notification || !notification.nid) {
return callback();
}
@@ -153,7 +161,7 @@ var async = require('async'),
uids = [uids];
}
- uids = uids.filter(function(uid, index, array) {
+ uids = uids.filter(function (uid, index, array) {
return parseInt(uid, 10) && array.indexOf(uid) === index;
});
@@ -161,85 +169,65 @@ var async = require('async'),
return callback();
}
- var done = false;
- var start = 0;
- var batchSize = 50;
-
- setTimeout(function() {
- async.whilst(
- function() {
- return !done;
- },
- function(next) {
- var currentUids = uids.slice(start, start + batchSize);
- if (!currentUids.length) {
- done = true;
- return next();
- }
- pushToUids(currentUids, notification, function(err) {
- if (err) {
- return next(err);
- }
- start = start + batchSize;
-
- setTimeout(next, 1000);
- });
- },
- function(err) {
- if (err) {
- winston.error(err.stack);
- }
+ setTimeout(function () {
+ batch.processArray(uids, function (uids, next) {
+ pushToUids(uids, notification, next);
+ }, {interval: 1000}, function (err) {
+ if (err) {
+ winston.error(err.stack);
}
- );
+ });
}, 1000);
callback();
};
function pushToUids(uids, notification, callback) {
+ var oneWeekAgo = Date.now() - 604800000;
var unreadKeys = [];
var readKeys = [];
- uids.forEach(function(uid) {
- unreadKeys.push('uid:' + uid + ':notifications:unread');
- readKeys.push('uid:' + uid + ':notifications:read');
- });
+ async.waterfall([
+ function (next) {
+ plugins.fireHook('filter:notification.push', {notification: notification, uids: uids}, next);
+ },
+ function (data, next) {
+ uids = data.uids;
+ notification = data.notification;
+
+ uids.forEach(function (uid) {
+ unreadKeys.push('uid:' + uid + ':notifications:unread');
+ readKeys.push('uid:' + uid + ':notifications:read');
+ });
- var oneWeekAgo = Date.now() - 604800000;
- async.series([
- function(next) {
db.sortedSetsAdd(unreadKeys, notification.datetime, notification.nid, next);
},
- function(next) {
+ function (next) {
db.sortedSetsRemove(readKeys, notification.nid, next);
},
- function(next) {
+ function (next) {
db.sortedSetsRemoveRangeByScore(unreadKeys, '-inf', oneWeekAgo, next);
},
- function(next) {
+ function (next) {
db.sortedSetsRemoveRangeByScore(readKeys, '-inf', oneWeekAgo, next);
- }
- ], function(err) {
- if (err) {
- return callback(err);
- }
+ },
+ function (next) {
+ var websockets = require('./socket.io');
+ if (websockets.server) {
+ uids.forEach(function (uid) {
+ websockets.in('uid_' + uid).emit('event:new_notification', notification);
+ });
+ }
- plugins.fireHook('action:notification.pushed', {notification: notification, uids: uids});
-
- var websockets = require('./socket.io');
- if (websockets.server) {
- uids.forEach(function(uid) {
- websockets.in('uid_' + uid).emit('event:new_notification', notification);
- });
+ plugins.fireHook('action:notification.pushed', {notification: notification, uids: uids});
+ next();
}
-
- callback();
- });
+ ], callback);
}
- Notifications.pushGroup = function(notification, groupName, callback) {
- callback = callback || function() {};
- groups.getMembers(groupName, 0, -1, function(err, members) {
+ Notifications.pushGroup = function (notification, groupName, callback) {
+ callback = callback || function () {};
+ groups.getMembers(groupName, 0, -1, function (err, members) {
if (err || !Array.isArray(members) || !members.length) {
return callback(err);
}
@@ -248,21 +236,38 @@ var async = require('async'),
});
};
- Notifications.markRead = function(nid, uid, callback) {
- callback = callback || function() {};
+ Notifications.rescind = function (nid, callback) {
+ callback = callback || function () {};
+
+ async.parallel([
+ async.apply(db.sortedSetRemove, 'notifications', nid),
+ async.apply(db.delete, 'notifications:' + nid)
+ ], function (err) {
+ if (err) {
+ winston.error('Encountered error rescinding notification (' + nid + '): ' + err.message);
+ } else {
+ winston.verbose('[notifications/rescind] Rescinded notification "' + nid + '"');
+ }
+
+ callback(err, nid);
+ });
+ };
+
+ Notifications.markRead = function (nid, uid, callback) {
+ callback = callback || function () {};
if (!parseInt(uid, 10) || !nid) {
return callback();
}
Notifications.markReadMultiple([nid], uid, callback);
};
- Notifications.markUnread = function(nid, uid, callback) {
- callback = callback || function() {};
+ Notifications.markUnread = function (nid, uid, callback) {
+ callback = callback || function () {};
if (!parseInt(uid, 10) || !nid) {
return callback();
}
- db.getObject('notifications:' + nid, function(err, notification) {
+ db.getObject('notifications:' + nid, function (err, notification) {
if (err || !notification) {
return callback(err || new Error('[[error:no-notification]]'));
}
@@ -275,24 +280,24 @@ var async = require('async'),
});
};
- Notifications.markReadMultiple = function(nids, uid, callback) {
- callback = callback || function() {};
+ Notifications.markReadMultiple = function (nids, uid, callback) {
+ callback = callback || function () {};
nids = nids.filter(Boolean);
if (!Array.isArray(nids) || !nids.length) {
return callback();
}
- var notificationKeys = nids.map(function(nid) {
+ var notificationKeys = nids.map(function (nid) {
return 'notifications:' + nid;
});
async.waterfall([
async.apply(db.getObjectsFields, notificationKeys, ['mergeId']),
- function(mergeIds, next) {
+ function (mergeIds, next) {
// Isolate mergeIds and find related notifications
- mergeIds = mergeIds.map(function(set) {
+ mergeIds = mergeIds.map(function (set) {
return set.mergeId;
- }).reduce(function(memo, mergeId, idx, arr) {
+ }).reduce(function (memo, mergeId, idx, arr) {
if (mergeId && idx === arr.indexOf(mergeId)) {
memo.push(mergeId);
}
@@ -301,45 +306,45 @@ var async = require('async'),
Notifications.findRelated(mergeIds, 'uid:' + uid + ':notifications:unread', next);
},
- function(relatedNids, next) {
- notificationKeys = _.union(nids, relatedNids).map(function(nid) {
+ function (relatedNids, next) {
+ notificationKeys = _.union(nids, relatedNids).map(function (nid) {
return 'notifications:' + nid;
});
db.getObjectsFields(notificationKeys, ['nid', 'datetime'], next);
}
- ], function(err, notificationData) {
+ ], function (err, notificationData) {
if (err) {
return callback(err);
}
// Filter out notifications that didn't exist
- notificationData = notificationData.filter(function(notification) {
+ notificationData = notificationData.filter(function (notification) {
return notification && notification.nid;
});
// Extract nid
- nids = notificationData.map(function(notification) {
+ nids = notificationData.map(function (notification) {
return notification.nid;
});
- var datetimes = notificationData.map(function(notification) {
+ var datetimes = notificationData.map(function (notification) {
return (notification && notification.datetime) || Date.now();
});
async.parallel([
- function(next) {
+ function (next) {
db.sortedSetRemove('uid:' + uid + ':notifications:unread', nids, next);
},
- function(next) {
+ function (next) {
db.sortedSetAdd('uid:' + uid + ':notifications:read', datetimes, nids, next);
}
], callback);
});
};
- Notifications.markAllRead = function(uid, callback) {
- db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function(err, nids) {
+ Notifications.markAllRead = function (uid, callback) {
+ db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function (err, nids) {
if (err) {
return callback(err);
}
@@ -352,13 +357,13 @@ var async = require('async'),
});
};
- Notifications.prune = function() {
+ Notifications.prune = function () {
var week = 604800000,
numPruned = 0;
var cutoffTime = Date.now() - week;
- db.getSortedSetRangeByScore('notifications', 0, 500, '-inf', cutoffTime, function(err, nids) {
+ db.getSortedSetRangeByScore('notifications', 0, 500, '-inf', cutoffTime, function (err, nids) {
if (err) {
return winston.error(err.message);
}
@@ -367,20 +372,20 @@ var async = require('async'),
return;
}
- var keys = nids.map(function(nid) {
+ var keys = nids.map(function (nid) {
return 'notifications:' + nid;
});
numPruned = nids.length;
async.parallel([
- function(next) {
+ function (next) {
db.sortedSetRemove('notifications', nids, next);
},
- function(next) {
+ function (next) {
db.deleteAll(keys, next);
}
- ], function(err) {
+ ], function (err) {
if (err) {
return winston.error('Encountered error pruning notifications: ' + err.message);
}
@@ -388,10 +393,9 @@ var async = require('async'),
});
};
- Notifications.merge = function(notifications, callback) {
+ Notifications.merge = function (notifications, callback) {
// When passed a set of notification objects, merge any that can be merged
var mergeIds = [
- 'notifications:favourited_your_post_in',
'notifications:upvoted_your_post_in',
'notifications:user_started_following_you',
'notifications:user_posted_to',
@@ -400,8 +404,8 @@ var async = require('async'),
],
isolated, differentiators, differentiator, modifyIndex, set;
- notifications = mergeIds.reduce(function(notifications, mergeId) {
- isolated = notifications.filter(function(notifObj) {
+ notifications = mergeIds.reduce(function (notifications, mergeId) {
+ isolated = notifications.filter(function (notifObj) {
if (!notifObj || !notifObj.hasOwnProperty('mergeId')) {
return false;
}
@@ -414,7 +418,7 @@ var async = require('async'),
}
// Each isolated mergeId may have multiple differentiators, so process each separately
- differentiators = isolated.reduce(function(cur, next) {
+ differentiators = isolated.reduce(function (cur, next) {
differentiator = next.mergeId.split('|')[1] || 0;
if (cur.indexOf(differentiator) === -1) {
cur.push(differentiator);
@@ -423,11 +427,11 @@ var async = require('async'),
return cur;
}, []);
- differentiators.forEach(function(differentiator) {
+ differentiators.forEach(function (differentiator) {
if (differentiator === 0 && differentiators.length === 1) {
set = isolated;
} else {
- set = isolated.filter(function(notifObj) {
+ set = isolated.filter(function (notifObj) {
return notifObj.mergeId === (mergeId + '|' + differentiator);
});
}
@@ -438,14 +442,14 @@ var async = require('async'),
}
switch(mergeId) {
- case 'notifications:favourited_your_post_in': // intentional fall-through
+ // intentional fall-through
case 'notifications:upvoted_your_post_in':
case 'notifications:user_started_following_you':
case 'notifications:user_posted_to':
case 'notifications:user_flagged_post_in':
- var usernames = set.map(function(notifObj) {
+ var usernames = set.map(function (notifObj) {
return notifObj && notifObj.user && notifObj.user.username;
- }).filter(function(username, idx, array) {
+ }).filter(function (username, idx, array) {
return array.indexOf(username) === idx;
});
var numUsers = usernames.length;
@@ -459,6 +463,8 @@ var async = require('async'),
} 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':
@@ -467,7 +473,7 @@ var async = require('async'),
}
// Filter out duplicates
- notifications = notifications.filter(function(notifObj, idx) {
+ notifications = notifications.filter(function (notifObj, idx) {
if (!notifObj || !notifObj.mergeId) {
return true;
}
@@ -481,7 +487,7 @@ var async = require('async'),
plugins.fireHook('filter:notifications.merge', {
notifications: notifications
- }, function(err, data) {
+ }, function (err, data) {
callback(err, data.notifications);
});
};
diff --git a/src/pagination.js b/src/pagination.js
index 15b8aa6eb3..347c71ae5c 100644
--- a/src/pagination.js
+++ b/src/pagination.js
@@ -4,7 +4,7 @@ var qs = require('querystring');
var pagination = {};
-pagination.create = function(currentPage, pageCount, queryObj) {
+pagination.create = function (currentPage, pageCount, queryObj) {
if (pageCount <= 1) {
return {
prev: {page: 1, active: currentPage > 1},
@@ -23,24 +23,26 @@ pagination.create = function(currentPage, pageCount, queryObj) {
var next = Math.min(pageCount, currentPage + 1);
var startPage = currentPage - 2;
- for(var i=0; i<5; ++i) {
+ for(var i = 0; i < 5; ++i) {
pagesToShow.push(startPage + i);
}
- pagesToShow = pagesToShow.filter(function(page, index, array) {
+ pagesToShow = pagesToShow.filter(function (page, index, array) {
return page > 0 && page <= pageCount && array.indexOf(page) === index;
- }).sort(function(a, b) {
+ }).sort(function (a, b) {
return a - b;
});
queryObj = queryObj || {};
- var pages = pagesToShow.map(function(page) {
+ delete queryObj._;
+
+ var pages = pagesToShow.map(function (page) {
queryObj.page = page;
return {page: page, active: page === currentPage, qs: qs.stringify(queryObj)};
});
- for (i=pages.length - 1; i>0; --i) {
+ for (i = pages.length - 1; i > 0; --i) {
if (pages[i - 1].page !== pages[i].page - 1) {
pages.splice(i, 0, {separator: true});
}
diff --git a/src/password.js b/src/password.js
index 13f8c11f72..2744cbefba 100644
--- a/src/password.js
+++ b/src/password.js
@@ -1,22 +1,24 @@
'use strict';
-(function(module) {
+(function (module) {
var fork = require('child_process').fork;
- module.hash = function(rounds, password, callback) {
+ module.hash = function (rounds, password, callback) {
forkChild({type: 'hash', rounds: rounds, password: password}, callback);
};
- module.compare = function(password, hash, callback) {
+ module.compare = function (password, hash, callback) {
forkChild({type: 'compare', password: password, hash: hash}, callback);
};
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) {
+ child.on('message', function (msg) {
if (msg.err) {
return callback(new Error(msg.err));
}
@@ -28,4 +30,4 @@
}
return module;
-})(exports);
\ No newline at end of file
+}(exports));
\ No newline at end of file
diff --git a/src/plugins.js b/src/plugins.js
index 853c6f44f2..1274b829ef 100644
--- a/src/plugins.js
+++ b/src/plugins.js
@@ -10,16 +10,14 @@ 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;
-(function(Plugins) {
+(function (Plugins) {
require('./plugins/install')(Plugins);
require('./plugins/load')(Plugins);
require('./plugins/hooks')(Plugins);
@@ -31,20 +29,20 @@ var middleware;
Plugins.lessFiles = [];
Plugins.clientScripts = [];
Plugins.acpScripts = [];
- Plugins.customLanguages = [];
+ Plugins.customLanguages = {};
Plugins.customLanguageFallbacks = {};
Plugins.libraryPaths = [];
Plugins.versionWarning = [];
Plugins.initialized = false;
- Plugins.requireLibrary = function(pluginID, libraryPath) {
+ Plugins.requireLibrary = function (pluginID, libraryPath) {
Plugins.libraries[pluginID] = require(libraryPath);
Plugins.libraryPaths.push(libraryPath);
};
- Plugins.init = function(nbbApp, nbbMiddleware, callback) {
- callback = callback || function() {};
+ Plugins.init = function (nbbApp, nbbMiddleware, callback) {
+ callback = callback || function () {};
if (Plugins.initialized) {
return callback();
}
@@ -57,7 +55,7 @@ var middleware;
winston.verbose('[plugins] Initializing plugins system');
}
- Plugins.reload(function(err) {
+ Plugins.reload(function (err) {
if (err) {
winston.error('[plugins] NodeBB encountered a problem while loading plugins', err.message);
return callback(err);
@@ -73,7 +71,7 @@ var middleware;
});
};
- Plugins.reload = function(callback) {
+ Plugins.reload = function (callback) {
// Resetting all local plugin data
Plugins.libraries = {};
Plugins.loadedHooks = {};
@@ -85,44 +83,58 @@ var middleware;
Plugins.acpScripts.length = 0;
Plugins.libraryPaths.length = 0;
- Plugins.registerHook('core', {
- hook: 'static:app.load',
- method: addLanguages
- });
+ // Plugins.registerHook('core', {
+ // hook: 'static:app.load',
+ // method: addLanguages
+ // });
async.waterfall([
- function(next) {
+ 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';
+ });
+
+ next();
+ });
+ },
+ function (next) {
db.getSortedSetRange('plugins:active', 0, -1, next);
},
- function(plugins, next) {
+ function (plugins, next) {
if (!Array.isArray(plugins)) {
return next();
}
- plugins = plugins.filter(function(plugin){
+ plugins = plugins.filter(function (plugin){
return plugin && typeof plugin === 'string';
- }).map(function(plugin){
+ }).map(function (plugin){
return path.join(__dirname, '../node_modules/', plugin);
});
- async.filter(plugins, file.exists, function(plugins) {
+ async.filter(plugins, file.exists, function (plugins) {
async.eachSeries(plugins, Plugins.loadPlugin, next);
});
},
- function(next) {
+ function (next) {
// If some plugins are incompatible, throw the warning here
if (Plugins.versionWarning.length && nconf.get('isPrimary') === 'true') {
process.stdout.write('\n');
winston.warn('[plugins/load] The following plugins may not be compatible with your version of NodeBB. This may cause unintended behaviour or crashing. In the event of an unresponsive NodeBB caused by this plugin, run `./nodebb reset -p PLUGINNAME` to disable it.');
- for(var x=0,numPlugins=Plugins.versionWarning.length;x b.name ) {
return 1;
} else if (a.name < b.name ){
@@ -326,41 +345,41 @@ var middleware;
});
};
- Plugins.showInstalled = function(callback) {
+ Plugins.showInstalled = function (callback) {
var npmPluginPath = path.join(__dirname, '../node_modules');
async.waterfall([
async.apply(fs.readdir, npmPluginPath),
- function(dirs, next) {
- dirs = dirs.filter(function(dir){
+ function (dirs, next) {
+ dirs = dirs.filter(function (dir){
return dir.startsWith('nodebb-plugin-') ||
dir.startsWith('nodebb-widget-') ||
dir.startsWith('nodebb-rewards-') ||
dir.startsWith('nodebb-theme-');
- }).map(function(dir){
+ }).map(function (dir){
return path.join(npmPluginPath, dir);
});
- async.filter(dirs, function(dir, callback){
- fs.stat(dir, function(err, stats){
+ async.filter(dirs, function (dir, callback){
+ fs.stat(dir, function (err, stats){
callback(!err && stats.isDirectory());
});
- }, function(plugins){
+ }, function (plugins){
next(null, plugins);
});
},
- function(files, next) {
+ function (files, next) {
var plugins = [];
- async.each(files, function(file, next) {
+ async.each(files, function (file, next) {
async.waterfall([
- function(next) {
+ function (next) {
Plugins.loadPluginInfo(file, next);
},
- function(pluginData, next) {
- Plugins.isActive(pluginData.name, function(err, active) {
+ function (pluginData, next) {
+ Plugins.isActive(pluginData.name, function (err, active) {
if (err) {
return next(new Error('no-active-state'));
}
@@ -373,7 +392,7 @@ var middleware;
next(null, pluginData);
});
}
- ], function(err, pluginData) {
+ ], function (err, pluginData) {
if (err) {
return next(); // Silently fail
}
@@ -381,61 +400,11 @@ var middleware;
plugins.push(pluginData);
next();
});
- }, function(err) {
- next(null, plugins);
+ }, function (err) {
+ 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 2) {
+ parts.pop();
+ }
+ var hook = parts.join(':');
}
if (data.hook && data.method) {
@@ -44,7 +54,7 @@ module.exports = function(Plugins) {
}
if (typeof data.method === 'string' && data.method.length > 0) {
- method = data.method.split('.').reduce(function(memo, prop) {
+ method = data.method.split('.').reduce(function (memo, prop) {
if (memo && memo[prop]) {
return memo[prop];
} else {
@@ -61,12 +71,13 @@ module.exports = function(Plugins) {
register();
} else {
winston.warn('[plugins/' + id + '] Hook method mismatch: ' + data.hook + ' => ' + data.method);
+ return callback();
}
}
};
- Plugins.fireHook = function(hook, params, callback) {
- callback = typeof callback === 'function' ? callback : function() {};
+ Plugins.fireHook = function (hook, params, callback) {
+ callback = typeof callback === 'function' ? callback : function () {};
var hookList = Plugins.loadedHooks[hook];
var hookType = hook.split(':')[0];
@@ -92,7 +103,7 @@ module.exports = function(Plugins) {
return callback(null, params);
}
- async.reduce(hookList, params, function(params, hookObj, next) {
+ async.reduce(hookList, params, function (params, hookObj, next) {
if (typeof hookObj.method !== 'function') {
if (global.env === 'development') {
winston.warn('[plugins] Expected method for hook \'' + hook + '\' in plugin \'' + hookObj.id + '\' not found, skipping.');
@@ -102,7 +113,7 @@ module.exports = function(Plugins) {
hookObj.method(params, next);
- }, function(err, values) {
+ }, function (err, values) {
if (err) {
winston.error('[plugins] ' + hook + ', ' + err.message);
}
@@ -115,7 +126,7 @@ module.exports = function(Plugins) {
if (!Array.isArray(hookList) || !hookList.length) {
return callback();
}
- async.each(hookList, function(hookObj, next) {
+ async.each(hookList, function (hookObj, next) {
if (typeof hookObj.method !== 'function') {
if (global.env === 'development') {
@@ -133,18 +144,18 @@ module.exports = function(Plugins) {
if (!Array.isArray(hookList) || !hookList.length) {
return callback();
}
- async.each(hookList, function(hookObj, next) {
+ async.each(hookList, function (hookObj, next) {
if (typeof hookObj.method === 'function') {
var timedOut = false;
- var timeoutId = setTimeout(function() {
+ var timeoutId = setTimeout(function () {
winston.warn('[plugins] Callback timed out, hook \'' + hook + '\' in plugin \'' + hookObj.id + '\'');
timedOut = true;
next();
}, 5000);
try {
- hookObj.method(params, function() {
+ hookObj.method(params, function () {
clearTimeout(timeoutId);
if (!timedOut) {
next.apply(null, arguments);
@@ -162,7 +173,7 @@ module.exports = function(Plugins) {
}, callback);
}
- Plugins.hasListeners = function(hook) {
+ Plugins.hasListeners = function (hook) {
return !!(Plugins.loadedHooks[hook] && Plugins.loadedHooks[hook].length > 0);
};
-};
\ No newline at end of file
+};
diff --git a/src/plugins/install.js b/src/plugins/install.js
index 1ea826784c..acde7a22e7 100644
--- a/src/plugins/install.js
+++ b/src/plugins/install.js
@@ -12,35 +12,35 @@ var winston = require('winston'),
pubsub = require('../pubsub');
-module.exports = function(Plugins) {
+module.exports = function (Plugins) {
if (nconf.get('isPrimary') === 'true') {
- pubsub.on('plugins:toggleInstall', function(data) {
+ pubsub.on('plugins:toggleInstall', function (data) {
if (data.hostname !== os.hostname()) {
toggleInstall(data.id, data.version);
}
});
- pubsub.on('plugins:upgrade', function(data) {
+ pubsub.on('plugins:upgrade', function (data) {
if (data.hostname !== os.hostname()) {
upgrade(data.id, data.version);
}
});
}
- Plugins.toggleActive = function(id, callback) {
- callback = callback || function() {};
+ Plugins.toggleActive = function (id, callback) {
+ callback = callback || function () {};
var isActive;
async.waterfall([
- function(next) {
+ function (next) {
Plugins.isActive(id, next);
},
- function(_isActive, next) {
+ function (_isActive, next) {
isActive = _isActive;
if (isActive) {
db.sortedSetRemove('plugins:active', id, next);
} else {
- db.sortedSetCard('plugins:active', function(err, count) {
+ db.sortedSetCard('plugins:active', function (err, count) {
if (err) {
return next(err);
}
@@ -48,12 +48,12 @@ module.exports = function(Plugins) {
});
}
},
- function(next) {
+ function (next) {
meta.reloadRequired = true;
Plugins.fireHook(isActive ? 'action:plugin.deactivate' : 'action:plugin.activate', id);
next();
}
- ], function(err) {
+ ], function (err) {
if (err) {
winston.warn('[plugins] Could not toggle active state on plugin \'' + id + '\'');
return callback(err);
@@ -62,40 +62,44 @@ module.exports = function(Plugins) {
});
};
- Plugins.toggleInstall = function(id, version, callback) {
+ Plugins.toggleInstall = function (id, version, callback) {
pubsub.publish('plugins:toggleInstall', {hostname: os.hostname(), id: id, version: version});
toggleInstall(id, version, callback);
};
function toggleInstall(id, version, callback) {
- Plugins.isInstalled(id, function(err, installed) {
+ Plugins.isInstalled(id, function (err, installed) {
if (err) {
return callback(err);
}
var type = installed ? 'uninstall' : 'install';
async.waterfall([
- function(next) {
+ function (next) {
Plugins.isActive(id, next);
},
- function(active, next) {
+ function (active, next) {
if (active) {
- Plugins.toggleActive(id, function(err, status) {
+ Plugins.toggleActive(id, function (err, status) {
next(err);
});
return;
}
next();
},
- function(next) {
+ function (next) {
var command = installed ? ('npm uninstall ' + id) : ('npm install ' + id + '@' + (version || 'latest'));
runNpmCommand(command, next);
}
- ], function(err) {
+ ], function (err) {
if (err) {
return callback(err);
}
- Plugins.get(id, function(err, pluginData) {
+ Plugins.get(id, function (err, pluginData) {
+ if (err) {
+ return callback(err);
+ }
+
Plugins.fireHook('action:plugin.' + type, id);
callback(null, pluginData);
});
@@ -113,39 +117,39 @@ module.exports = function(Plugins) {
});
}
- Plugins.upgrade = function(id, version, callback) {
+ Plugins.upgrade = function (id, version, callback) {
pubsub.publish('plugins:upgrade', {hostname: os.hostname(), id: id, version: version});
upgrade(id, version, callback);
};
function upgrade(id, version, callback) {
async.waterfall([
- function(next) {
+ function (next) {
runNpmCommand('npm install ' + id + '@' + (version || 'latest'), next);
},
- function(next) {
+ function (next) {
Plugins.isActive(id, next);
},
- function(isActive, next) {
+ function (isActive, next) {
meta.reloadRequired = isActive;
next(null, isActive);
}
], callback);
}
- Plugins.isInstalled = function(id, callback) {
+ Plugins.isInstalled = function (id, callback) {
var pluginDir = path.join(__dirname, '../../node_modules', id);
- fs.stat(pluginDir, function(err, stats) {
+ fs.stat(pluginDir, function (err, stats) {
callback(null, err ? false : stats.isDirectory());
});
};
- Plugins.isActive = function(id, callback) {
+ Plugins.isActive = function (id, callback) {
db.isSortedSetMember('plugins:active', id, callback);
};
- Plugins.getActive = function(callback) {
+ Plugins.getActive = function (callback) {
db.getSortedSetRange('plugins:active', 0, -1, callback);
};
};
\ No newline at end of file
diff --git a/src/plugins/load.js b/src/plugins/load.js
index cf1ff27d9a..836e0d2c4a 100644
--- a/src/plugins/load.js
+++ b/src/plugins/load.js
@@ -7,14 +7,16 @@ var fs = require('fs'),
winston = require('winston'),
nconf = require('nconf'),
_ = require('underscore'),
- file = require('../file'),
- utils = require('../../public/src/utils');
+ file = require('../file');
+
+var utils = require('../../public/src/utils'),
+ meta = require('../meta');
-module.exports = function(Plugins) {
+module.exports = function (Plugins) {
- Plugins.loadPlugin = function(pluginPath, callback) {
- Plugins.loadPluginInfo(pluginPath, function(err, pluginData) {
+ Plugins.loadPlugin = function (pluginPath, callback) {
+ Plugins.loadPluginInfo(pluginPath, function (err, pluginData) {
if (err) {
if (err.message === '[[error:parse-error]]') {
return callback();
@@ -25,25 +27,28 @@ module.exports = function(Plugins) {
checkVersion(pluginData);
async.parallel([
- function(next) {
+ function (next) {
registerHooks(pluginData, pluginPath, next);
},
- function(next) {
+ function (next) {
mapStaticDirectories(pluginData, pluginPath, next);
},
- function(next) {
+ function (next) {
mapFiles(pluginData, 'css', 'cssFiles', next);
},
- function(next) {
+ function (next) {
mapFiles(pluginData, 'less', 'lessFiles', next);
},
- function(next) {
+ function (next) {
mapClientSideScripts(pluginData, next);
},
- function(next) {
+ function (next) {
+ mapClientModules(pluginData, next);
+ },
+ function (next) {
loadLanguages(pluginData, next);
}
- ], function(err) {
+ ], function (err) {
if (err) {
winston.verbose('[plugins] Could not load plugin : ' + pluginData.id);
return callback(err);
@@ -84,7 +89,7 @@ module.exports = function(Plugins) {
}
if (Array.isArray(pluginData.hooks) && pluginData.hooks.length > 0) {
- async.each(pluginData.hooks, function(hook, next) {
+ async.each(pluginData.hooks, function (hook, next) {
Plugins.registerHook(pluginData.id, hook, next);
}, callback);
} else {
@@ -109,7 +114,7 @@ module.exports = function(Plugins) {
var realPath = pluginData.staticDirs[mappedPath];
var staticDir = path.join(pluginPath, realPath);
- file.exists(staticDir, function(exists) {
+ file.exists(staticDir, function (exists) {
if (exists) {
Plugins.staticDirs[pluginData.id + '/' + mappedPath] = staticDir;
} else {
@@ -134,7 +139,7 @@ module.exports = function(Plugins) {
winston.verbose('[plugins] Found ' + pluginData[type].length + ' ' + type + ' file(s) for plugin ' + pluginData.id);
}
- Plugins[globalArray] = Plugins[globalArray].concat(pluginData[type].map(function(file) {
+ Plugins[globalArray] = Plugins[globalArray].concat(pluginData[type].map(function (file) {
return path.join(pluginData.id, file);
}));
}
@@ -147,19 +152,61 @@ module.exports = function(Plugins) {
winston.verbose('[plugins] Found ' + pluginData.scripts.length + ' js file(s) for plugin ' + pluginData.id);
}
- Plugins.clientScripts = Plugins.clientScripts.concat(pluginData.scripts.map(function(file) {
- return path.join(__dirname, '../../node_modules/', pluginData.id, file);
- }));
+ Plugins.clientScripts = Plugins.clientScripts.concat(pluginData.scripts.map(function (file) {
+ return resolveModulePath(path.join(__dirname, '../../node_modules/', pluginData.id, file), file);
+ })).filter(Boolean);
}
if (Array.isArray(pluginData.acpScripts)) {
if (global.env === 'development') {
- winston.verbose('[plugins] Found ' + pluginData.acpScripts.length + ' js file(s) for plugin ' + pluginData.id);
+ winston.verbose('[plugins] Found ' + pluginData.acpScripts.length + ' ACP js file(s) for plugin ' + pluginData.id);
}
- Plugins.acpScripts = Plugins.acpScripts.concat(pluginData.acpScripts.map(function(file) {
- return path.join(__dirname, '../../node_modules/', pluginData.id, file);
- }));
+ Plugins.acpScripts = Plugins.acpScripts.concat(pluginData.acpScripts.map(function (file) {
+ return resolveModulePath(path.join(__dirname, '../../node_modules/', pluginData.id, file), file);
+ })).filter(Boolean);
+ }
+
+ callback();
+ }
+
+ function mapClientModules(pluginData, callback) {
+ if (!pluginData.hasOwnProperty('modules')) {
+ return callback();
+ }
+
+ var modules = {};
+
+ if (Array.isArray(pluginData.modules)) {
+ if (global.env === 'development') {
+ winston.verbose('[plugins] Found ' + pluginData.modules.length + ' AMD-style module(s) for plugin ' + pluginData.id);
+ }
+
+ var strip = pluginData.hasOwnProperty('modulesStrip') ? parseInt(pluginData.modulesStrip, 10) : 0;
+
+ pluginData.modules.forEach(function (file) {
+ if (strip) {
+ modules[file.replace(new RegExp('\.?(\/[^\/]+){' + strip + '}\/'), '')] = path.join('./node_modules/', pluginData.id, file);
+ } else {
+ modules[path.basename(file)] = path.join('./node_modules/', pluginData.id, file);
+ }
+ });
+
+ meta.js.scripts.modules = _.extend(meta.js.scripts.modules, modules);
+ } else {
+ var keys = Object.keys(pluginData.modules);
+
+ if (global.env === 'development') {
+ winston.verbose('[plugins] Found ' + keys.length + ' AMD-style module(s) for plugin ' + pluginData.id);
+ }
+
+ for (var name in pluginData.modules) {
+ if (pluginData.modules.hasOwnProperty(name)) {
+ modules[name] = path.join('./node_modules/', pluginData.id, pluginData.modules[name]);
+ }
+ }
+
+ meta.js.scripts.modules = _.extend(meta.js.scripts.modules, modules);
}
callback();
@@ -173,56 +220,87 @@ module.exports = function(Plugins) {
var pathToFolder = path.join(__dirname, '../../node_modules/', pluginData.id, pluginData.languages),
fallbackMap = {};
- utils.walk(pathToFolder, function(err, languages) {
- var arr = [];
+ utils.walk(pathToFolder, function (err, languages) {
+ if (err) {
+ return callback(err);
+ }
- async.each(languages, function(pathToLang, next) {
- fs.readFile(pathToLang, function(err, file) {
+ async.each(languages, function (pathToLang, next) {
+ fs.readFile(pathToLang, function (err, file) {
if (err) {
return next(err);
}
- var json;
+ var data;
+ var route = pathToLang.replace(pathToFolder + '/', '');
try {
- json = JSON.parse(file.toString());
+ data = JSON.parse(file.toString());
} catch (err) {
winston.error('[plugins] Unable to parse custom language file: ' + pathToLang + '\r\n' + err.stack);
return next(err);
}
- arr.push({
- file: json,
- route: pathToLang.replace(pathToFolder, '')
- });
+ Plugins.customLanguages[route] = Plugins.customLanguages[route] || {};
+ _.extendOwn(Plugins.customLanguages[route], data);
if (pluginData.defaultLang && pathToLang.endsWith(pluginData.defaultLang + '/' + path.basename(pathToLang))) {
- fallbackMap[path.basename(pathToLang, '.json')] = path.join(pathToFolder, pluginData.defaultLang, path.basename(pathToLang));
+ Plugins.languageCodes.map(function (code) {
+ if (pluginData.defaultLang !== code) {
+ return code + '/' + path.basename(pathToLang);
+ } else {
+ return null;
+ }
+ }).filter(Boolean).forEach(function (key) {
+ Plugins.customLanguages[key] = _.defaults(Plugins.customLanguages[key] || {}, data);
+ });
}
next();
});
- }, function(err) {
+ }, function (err) {
if (err) {
return callback(err);
}
- Plugins.customLanguages = Plugins.customLanguages.concat(arr);
- _.extendOwn(Plugins.customLanguageFallbacks, fallbackMap);
-
callback();
});
});
}
- Plugins.loadPluginInfo = function(pluginPath, callback) {
+ function resolveModulePath(fullPath, relPath) {
+ /**
+ * With npm@3, dependencies can become flattened, and appear at the root level.
+ * This method resolves these differences if it can.
+ */
+ var matches = fullPath.match(/node_modules/g);
+ var atRootLevel = !matches || matches.length === 1;
+
+ try {
+ fs.statSync(fullPath);
+ winston.verbose('[plugins/load] File found: ' + fullPath);
+ return fullPath;
+ } catch (e) {
+ // File not visible to the calling process, ascend to root level if possible and try again
+ if (!atRootLevel && relPath) {
+ winston.verbose('[plugins/load] File not found: ' + fullPath + ' (Ascending)');
+ return resolveModulePath(path.join(__dirname, '../..', relPath));
+ } else {
+ // Already at root level, file was simply not found
+ winston.warn('[plugins/load] File not found: ' + fullPath + ' (Ignoring)');
+ return null;
+ }
+ }
+ }
+
+ Plugins.loadPluginInfo = function (pluginPath, callback) {
async.parallel({
- package: function(next) {
+ package: function (next) {
fs.readFile(path.join(pluginPath, 'package.json'), next);
},
- plugin: function(next) {
+ plugin: function (next) {
fs.readFile(path.join(pluginPath, 'plugin.json'), next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return callback(err);
}
@@ -240,7 +318,7 @@ module.exports = function(Plugins) {
callback(null, pluginData);
} catch(err) {
var pluginDir = pluginPath.split(path.sep);
- pluginDir = pluginDir[pluginDir.length -1];
+ pluginDir = pluginDir[pluginDir.length - 1];
winston.error('[plugins/' + pluginDir + '] Error in plugin.json or package.json! ' + err.message);
@@ -248,4 +326,4 @@ module.exports = function(Plugins) {
}
});
};
-};
\ No newline at end of file
+};
diff --git a/src/posts.js b/src/posts.js
index a3d19e7a7f..975846859b 100644
--- a/src/posts.js
+++ b/src/posts.js
@@ -1,16 +1,16 @@
'use strict';
-var async = require('async'),
- _ = require('underscore'),
+var async = require('async');
+var _ = require('underscore');
- db = require('./database'),
- utils = require('../public/src/utils'),
- user = require('./user'),
- topics = require('./topics'),
- privileges = require('./privileges'),
- plugins = require('./plugins');
+var db = require('./database');
+var utils = require('../public/src/utils');
+var user = require('./user');
+var topics = require('./topics');
+var privileges = require('./privileges');
+var plugins = require('./plugins');
-(function(Posts) {
+(function (Posts) {
require('./posts/create')(Posts);
require('./posts/delete')(Posts);
@@ -23,48 +23,52 @@ var async = require('async'),
require('./posts/recent')(Posts);
require('./posts/flags')(Posts);
require('./posts/tools')(Posts);
+ require('./posts/votes')(Posts);
+ require('./posts/bookmarks')(Posts);
- Posts.exists = function(pid, callback) {
+ Posts.exists = function (pid, callback) {
db.isSortedSetMember('posts:pid', pid, callback);
};
- Posts.getPidsFromSet = function(set, start, stop, reverse, callback) {
+ Posts.getPidsFromSet = function (set, start, stop, reverse, callback) {
if (isNaN(start) || isNaN(stop)) {
return callback(null, []);
}
db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, stop, callback);
};
- Posts.getPostsByPids = function(pids, uid, callback) {
+ Posts.getPostsByPids = function (pids, uid, callback) {
if (!Array.isArray(pids) || !pids.length) {
return callback(null, []);
}
var keys = [];
- for (var x=0, numPids=pids.length; x 0) {
+ db.sortedSetAdd('uid:' + postData.uid + ':posts:votes', postData.votes, postData.pid, next);
+ } else {
+ db.sortedSetRemove('uid:' + postData.uid + ':posts:votes', postData.pid, next);
+ }
} else {
next();
}
@@ -240,14 +248,14 @@ var async = require('async'),
if (parseInt(mainPid, 10) === parseInt(postData.pid, 10)) {
return next();
}
- db.sortedSetAdd('tid:' + postData.tid + ':posts:votes', voteCount, postData.pid, next);
+ db.sortedSetAdd('tid:' + postData.tid + ':posts:votes', postData.votes, postData.pid, next);
}
], next);
},
function (next) {
- Posts.setPostField(postData.pid, 'votes', voteCount, next);
+ Posts.setPostFields(postData.pid, {upvotes: postData.upvotes, downvotes: postData.downvotes}, next);
}
- ], function(err) {
+ ], function (err) {
callback(err);
});
};
diff --git a/src/posts/bookmarks.js b/src/posts/bookmarks.js
new file mode 100644
index 0000000000..b60da91c77
--- /dev/null
+++ b/src/posts/bookmarks.js
@@ -0,0 +1,107 @@
+'use strict';
+
+var async = require('async');
+
+var db = require('../database');
+var plugins = require('../plugins');
+
+module.exports = function (Posts) {
+
+ Posts.bookmark = function (pid, uid, callback) {
+ toggleBookmark('bookmark', pid, uid, callback);
+ };
+
+ Posts.unbookmark = function (pid, uid, callback) {
+ toggleBookmark('unbookmark', pid, uid, callback);
+ };
+
+ function toggleBookmark(type, pid, uid, callback) {
+ if (!parseInt(uid, 10)) {
+ return callback(new Error('[[error:not-logged-in]]'));
+ }
+ var isBookmarking = type === 'bookmark';
+
+ async.parallel({
+ owner: function (next) {
+ Posts.getPostField(pid, 'uid', next);
+ },
+ postData: function (next) {
+ Posts.getPostFields(pid, ['pid', 'uid'], next);
+ },
+ hasBookmarked: function (next) {
+ Posts.hasBookmarked(pid, uid, next);
+ }
+ }, function (err, results) {
+ if (err) {
+ return callback(err);
+ }
+
+ if (isBookmarking && results.hasBookmarked) {
+ return callback(new Error('[[error:already-bookmarked]]'));
+ }
+
+ if (!isBookmarking && !results.hasBookmarked) {
+ return callback(new Error('[[error:already-unbookmarked]]'));
+ }
+
+ async.waterfall([
+ function (next) {
+ if (isBookmarking) {
+ db.sortedSetAdd('uid:' + uid + ':bookmarks', Date.now(), pid, next);
+ } else {
+ db.sortedSetRemove('uid:' + uid + ':bookmarks', pid, next);
+ }
+ },
+ function (next) {
+ db[isBookmarking ? 'setAdd' : 'setRemove']('pid:' + pid + ':users_bookmarked', uid, next);
+ },
+ function (next) {
+ db.setCount('pid:' + pid + ':users_bookmarked', next);
+ },
+ function (count, next) {
+ results.postData.bookmarks = count;
+ Posts.setPostField(pid, 'bookmarks', count, next);
+ }
+ ], function (err) {
+ if (err) {
+ return callback(err);
+ }
+
+ var current = results.hasBookmarked ? 'bookmarked' : 'unbookmarked';
+
+ plugins.fireHook('action:post.' + type, {
+ pid: pid,
+ uid: uid,
+ owner: results.owner,
+ current: current
+ });
+
+ callback(null, {
+ post: results.postData,
+ isBookmarked: isBookmarking
+ });
+ });
+ });
+ }
+
+ Posts.hasBookmarked = function (pid, uid, callback) {
+ if (!parseInt(uid, 10)) {
+ if (Array.isArray(pid)) {
+ callback(null, pid.map(function () { return false; }));
+ } else {
+ callback(null, false);
+ }
+ return;
+ }
+
+ if (Array.isArray(pid)) {
+ var sets = pid.map(function (pid) {
+ return 'pid:' + pid + ':users_bookmarked';
+ });
+
+ db.isMemberOfSets(sets, uid, callback);
+ } else {
+ db.isSetMember('pid:' + pid + ':users_bookmarked', uid, callback);
+ }
+ };
+};
diff --git a/src/posts/cache.js b/src/posts/cache.js
index 592d313f48..62277c46e3 100644
--- a/src/posts/cache.js
+++ b/src/posts/cache.js
@@ -1,11 +1,10 @@
var LRU = require('lru-cache');
+var meta = require('../meta');
var cache = LRU({
- max: 1048576,
+ max: parseInt(meta.config.postCacheSize, 10) || 1048576,
length: function (n) { return n.length; },
maxAge: 1000 * 60 * 60
});
-
-
module.exports = cache;
\ No newline at end of file
diff --git a/src/posts/category.js b/src/posts/category.js
index 68ae42474b..a1f3e8afbc 100644
--- a/src/posts/category.js
+++ b/src/posts/category.js
@@ -1,47 +1,50 @@
'use strict';
-var async = require('async'),
- topics = require('../topics');
+var async = require('async');
+var _ = require('underscore');
-module.exports = function(Posts) {
+var db = require('../database');
+var topics = require('../topics');
- Posts.getCidByPid = function(pid, callback) {
+module.exports = function (Posts) {
+
+ Posts.getCidByPid = function (pid, callback) {
async.waterfall([
- function(next) {
+ function (next) {
Posts.getPostField(pid, 'tid', next);
},
- function(tid, next) {
+ function (tid, next) {
topics.getTopicField(tid, 'cid', next);
- }
+ }
], callback);
};
- Posts.getCidsByPids = function(pids, callback) {
- Posts.getPostsFields(pids, ['tid'], function(err, posts) {
+ Posts.getCidsByPids = function (pids, callback) {
+ Posts.getPostsFields(pids, ['tid'], function (err, posts) {
if (err) {
return callback(err);
}
- var tids = posts.map(function(post) {
+ var tids = posts.map(function (post) {
return post.tid;
- }).filter(function(tid, index, array) {
+ }).filter(function (tid, index, array) {
return tid && array.indexOf(tid) === index;
});
- topics.getTopicsFields(tids, ['cid'], function(err, topics) {
+ topics.getTopicsFields(tids, ['cid'], function (err, topics) {
if (err) {
return callback(err);
}
var map = {};
- topics.forEach(function(topic, index) {
+ topics.forEach(function (topic, index) {
if (topic) {
map[tids[index]] = topic.cid;
}
});
- var cids = posts.map(function(post) {
+ var cids = posts.map(function (post) {
return map[post.tid];
});
@@ -49,4 +52,34 @@ module.exports = function(Posts) {
});
});
};
+
+ Posts.filterPidsByCid = function (pids, cid, callback) {
+ if (!cid) {
+ return callback(null, pids);
+ }
+
+ if (!Array.isArray(cid) || cid.length === 1) {
+ // Single cid
+ db.isSortedSetMembers('cid:' + parseInt(cid, 10) + ':pids', pids, function (err, isMembers) {
+ if (err) {
+ return callback(err);
+ }
+ pids = pids.filter(function (pid, index) {
+ return pid && isMembers[index];
+ });
+ callback(null, pids);
+ });
+ } else {
+ // Multiple cids
+ async.map(cid, function (cid, next) {
+ Posts.filterPidsByCid(pids, cid, next);
+ }, function (err, pidsArr) {
+ if (err) {
+ return callback(err);
+ }
+
+ callback(null, _.union.apply(_, pidsArr));
+ });
+ }
+ };
};
\ No newline at end of file
diff --git a/src/posts/create.js b/src/posts/create.js
index 041d662915..1054c6d694 100644
--- a/src/posts/create.js
+++ b/src/posts/create.js
@@ -1,23 +1,24 @@
'use strict';
-var async = require('async'),
- _ = require('underscore'),
+var async = require('async');
+var _ = require('underscore');
- meta = require('../meta'),
- db = require('../database'),
- plugins = require('../plugins'),
- user = require('../user'),
- topics = require('../topics'),
- categories = require('../categories');
+var meta = require('../meta');
+var db = require('../database');
+var plugins = require('../plugins');
+var user = require('../user');
+var topics = require('../topics');
+var categories = require('../categories');
-module.exports = function(Posts) {
- Posts.create = function(data, callback) {
+module.exports = function (Posts) {
+
+ Posts.create = function (data, callback) {
// This is an internal method, consider using Topics.reply instead
- var uid = data.uid,
- tid = data.tid,
- content = data.content.toString(),
- timestamp = data.timestamp || Date.now();
+ var uid = data.uid;
+ var tid = data.tid;
+ var content = data.content.toString();
+ var timestamp = data.timestamp || Date.now();
if (!uid && parseInt(uid, 10) !== 0) {
return callback(new Error('[[error:invalid-uid]]'));
@@ -26,10 +27,10 @@ module.exports = function(Posts) {
var postData;
async.waterfall([
- function(next) {
+ function (next) {
db.incrObjectField('global', 'nextPid', next);
},
- function(pid, next) {
+ function (pid, next) {
postData = {
'pid': pid,
@@ -37,10 +38,6 @@ module.exports = function(Posts) {
'tid': tid,
'content': content,
'timestamp': timestamp,
- 'reputation': 0,
- 'votes': 0,
- 'editor': '',
- 'edited': 0,
'deleted': 0
};
@@ -52,25 +49,29 @@ module.exports = function(Posts) {
postData.ip = data.ip;
}
- if (parseInt(uid, 10) === 0 && data.handle) {
+ if (data.handle && !parseInt(uid, 10)) {
postData.handle = data.handle;
}
plugins.fireHook('filter:post.save', postData, next);
},
- function(postData, next) {
+ function (postData, next) {
+ plugins.fireHook('filter:post.create', {post: postData, data: data}, next);
+ },
+ function (data, next) {
+ postData = data.post;
db.setObject('post:' + postData.pid, postData, next);
},
- function(next) {
+ function (next) {
async.parallel([
- function(next) {
+ function (next) {
user.onNewPostMade(postData, next);
},
- function(next) {
+ function (next) {
topics.onNewPostMade(postData, next);
},
- function(next) {
- topics.getTopicFields(tid, ['cid', 'pinned'], function(err, topicData) {
+ function (next) {
+ topics.getTopicFields(tid, ['cid', 'pinned'], function (err, topicData) {
if (err) {
return next(err);
}
@@ -78,20 +79,20 @@ module.exports = function(Posts) {
categories.onNewPostMade(topicData.cid, topicData.pinned, postData, next);
});
},
- function(next) {
+ function (next) {
db.sortedSetAdd('posts:pid', timestamp, postData.pid, next);
},
- function(next) {
+ function (next) {
db.incrObjectField('global', 'postCount', next);
}
- ], function(err) {
+ ], function (err) {
if (err) {
return next(err);
}
plugins.fireHook('filter:post.get', postData, next);
});
},
- function(postData, next) {
+ function (postData, next) {
plugins.fireHook('action:post.save', _.clone(postData));
next(null, postData);
}
diff --git a/src/posts/delete.js b/src/posts/delete.js
index a77c153cb2..bdaae4a70c 100644
--- a/src/posts/delete.js
+++ b/src/posts/delete.js
@@ -8,9 +8,9 @@ var topics = require('../topics');
var user = require('../user');
var plugins = require('../plugins');
-module.exports = function(Posts) {
+module.exports = function (Posts) {
- Posts.delete = function(pid, uid, callback) {
+ Posts.delete = function (pid, uid, callback) {
var postData;
async.waterfall([
function (next) {
@@ -24,31 +24,29 @@ module.exports = function(Posts) {
},
function (_post, next) {
postData = _post;
- topics.getTopicField(_post.tid, 'cid', next);
+ topics.getTopicFields(_post.tid, ['tid', 'cid', 'pinned'], next);
},
- function (cid, next) {
+ function (topicData, next) {
async.parallel([
- function(next) {
- updateTopicTimestamp(postData.tid, next);
+ function (next) {
+ updateTopicTimestamp(topicData, next);
},
- function(next) {
- db.sortedSetRemove('cid:' + cid + ':pids', pid, next);
+ function (next) {
+ db.sortedSetRemove('cid:' + topicData.cid + ':pids', pid, next);
},
- function(next) {
- Posts.dismissFlag(pid, next);
- },
- function(next) {
+ function (next) {
topics.updateTeaser(postData.tid, next);
}
- ], function(err) {
- plugins.fireHook('action:post.delete', pid);
- next(err, postData);
- });
+ ], next);
+ },
+ function (results, next) {
+ plugins.fireHook('action:post.delete', pid);
+ next(null, postData);
}
], callback);
};
- Posts.restore = function(pid, uid, callback) {
+ Posts.restore = function (pid, uid, callback) {
var postData;
async.waterfall([
function (next) {
@@ -62,48 +60,59 @@ module.exports = function(Posts) {
},
function (_post, next) {
postData = _post;
- topics.getTopicField(_post.tid, 'cid', next);
+ topics.getTopicFields(_post.tid, ['tid', 'cid', 'pinned'], next);
},
- function (cid, next) {
- postData.cid = cid;
+ function (topicData, next) {
+ postData.cid = topicData.cid;
async.parallel([
- function(next) {
- updateTopicTimestamp(postData.tid, next);
+ function (next) {
+ updateTopicTimestamp(topicData, next);
},
- function(next) {
- db.sortedSetAdd('cid:' + cid + ':pids', postData.timestamp, pid, next);
+ function (next) {
+ db.sortedSetAdd('cid:' + topicData.cid + ':pids', postData.timestamp, pid, next);
},
- function(next) {
+ function (next) {
topics.updateTeaser(postData.tid, next);
}
- ], function(err) {
- plugins.fireHook('action:post.restore', _.clone(postData));
- next(err, postData);
- });
+ ], next);
+ },
+ function (results, next) {
+ plugins.fireHook('action:post.restore', _.clone(postData));
+ next(null, postData);
}
], callback);
};
- function updateTopicTimestamp(tid, callback) {
- topics.getLatestUndeletedPid(tid, function(err, pid) {
- if(err || !pid) {
- return callback(err);
+ function updateTopicTimestamp(topicData, callback) {
+ var timestamp;
+ async.waterfall([
+ function (next) {
+ topics.getLatestUndeletedPid(topicData.tid, next);
+ },
+ function (pid, next) {
+ if (!parseInt(pid, 10)) {
+ return callback();
+ }
+ Posts.getPostField(pid, 'timestamp', next);
+ },
+ function (_timestamp, next) {
+ timestamp = _timestamp;
+ if (!parseInt(timestamp, 10)) {
+ return callback();
+ }
+ topics.updateTimestamp(topicData.tid, timestamp, next);
+ },
+ function (next) {
+ if (parseInt(topicData.pinned, 10) !== 1) {
+ db.sortedSetAdd('cid:' + topicData.cid + ':tids', timestamp, topicData.tid, next);
+ } else {
+ next();
+ }
}
-
- Posts.getPostField(pid, 'timestamp', function(err, timestamp) {
- if (err) {
- return callback(err);
- }
-
- if (timestamp) {
- return topics.updateTimestamp(tid, timestamp, callback);
- }
- callback();
- });
- });
+ ], callback);
}
- Posts.purge = function(pid, uid, callback) {
+ Posts.purge = function (pid, uid, callback) {
async.waterfall([
function (next) {
Posts.exists(pid, next);
@@ -123,7 +132,7 @@ module.exports = function(Posts) {
deletePostFromCategoryRecentPosts(pid, next);
},
function (next) {
- deletePostFromUsersFavourites(pid, next);
+ deletePostFromUsersBookmarks(pid, next);
},
function (next) {
deletePostFromUsersVotes(pid, next);
@@ -134,7 +143,7 @@ module.exports = function(Posts) {
function (next) {
Posts.dismissFlag(pid, next);
}
- ], function(err) {
+ ], function (err) {
if (err) {
return next(err);
}
@@ -146,7 +155,7 @@ module.exports = function(Posts) {
};
function deletePostFromTopicAndUser(pid, callback) {
- Posts.getPostFields(pid, ['tid', 'uid'], function(err, postData) {
+ Posts.getPostFields(pid, ['tid', 'uid'], function (err, postData) {
if (err) {
return callback(err);
}
@@ -155,12 +164,12 @@ module.exports = function(Posts) {
'tid:' + postData.tid + ':posts',
'tid:' + postData.tid + ':posts:votes',
'uid:' + postData.uid + ':posts'
- ], pid, function(err) {
+ ], pid, function (err) {
if (err) {
return callback(err);
}
- topics.getTopicFields(postData.tid, ['cid'], function(err, topicData) {
+ topics.getTopicFields(postData.tid, ['tid', 'cid', 'pinned'], function (err, topicData) {
if (err) {
return callback(err);
}
@@ -175,13 +184,19 @@ module.exports = function(Posts) {
function (next) {
topics.decreasePostCount(postData.tid, next);
},
- function(next) {
+ function (next) {
topics.updateTeaser(postData.tid, next);
},
- function(next) {
+ function (next) {
+ updateTopicTimestamp(topicData, next);
+ },
+ function (next) {
db.sortedSetIncrBy('cid:' + topicData.cid + ':tids:posts', -1, postData.tid, next);
},
- function(next) {
+ function (next) {
+ db.sortedSetIncrBy('tid:' + postData.tid + ':posters', -1, postData.uid, next);
+ },
+ function (next) {
user.incrementUserPostCountBy(postData.uid, -1, next);
}
], callback);
@@ -191,12 +206,12 @@ module.exports = function(Posts) {
}
function deletePostFromCategoryRecentPosts(pid, callback) {
- db.getSortedSetRange('categories:cid', 0, -1, function(err, cids) {
+ db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) {
if (err) {
return callback(err);
}
- var sets = cids.map(function(cid) {
+ var sets = cids.map(function (cid) {
return 'cid:' + cid + ':pids';
});
@@ -204,55 +219,55 @@ module.exports = function(Posts) {
});
}
- function deletePostFromUsersFavourites(pid, callback) {
- db.getSetMembers('pid:' + pid + ':users_favourited', function(err, uids) {
+ function deletePostFromUsersBookmarks(pid, callback) {
+ db.getSetMembers('pid:' + pid + ':users_bookmarked', function (err, uids) {
if (err) {
return callback(err);
}
- var sets = uids.map(function(uid) {
- return 'uid:' + uid + ':favourites';
+ var sets = uids.map(function (uid) {
+ return 'uid:' + uid + ':bookmarks';
});
- db.sortedSetsRemove(sets, pid, function(err) {
+ db.sortedSetsRemove(sets, pid, function (err) {
if (err) {
return callback(err);
}
- db.delete('pid:' + pid + ':users_favourited', callback);
+ db.delete('pid:' + pid + ':users_bookmarked', callback);
});
});
}
function deletePostFromUsersVotes(pid, callback) {
async.parallel({
- upvoters: function(next) {
+ upvoters: function (next) {
db.getSetMembers('pid:' + pid + ':upvote', next);
},
- downvoters: function(next) {
+ downvoters: function (next) {
db.getSetMembers('pid:' + pid + ':downvote', next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return callback(err);
}
- var upvoterSets = results.upvoters.map(function(uid) {
+ var upvoterSets = results.upvoters.map(function (uid) {
return 'uid:' + uid + ':upvote';
});
- var downvoterSets = results.downvoters.map(function(uid) {
+ var downvoterSets = results.downvoters.map(function (uid) {
return 'uid:' + uid + ':downvote';
});
async.parallel([
- function(next) {
+ function (next) {
db.sortedSetsRemove(upvoterSets, pid, next);
},
- function(next) {
+ function (next) {
db.sortedSetsRemove(downvoterSets, pid, next);
},
- function(next) {
+ function (next) {
db.deleteAll(['pid:' + pid + ':upvote', 'pid:' + pid + ':downvote'], next);
}
], callback);
diff --git a/src/posts/edit.js b/src/posts/edit.js
index e8af9ae2d4..e8a5ea8395 100644
--- a/src/posts/edit.js
+++ b/src/posts/edit.js
@@ -1,25 +1,25 @@
'use strict';
-var async = require('async'),
- validator = require('validator'),
- _ = require('underscore'),
- db = require('../database'),
- topics = require('../topics'),
- user = require('../user'),
- privileges = require('../privileges'),
- plugins = require('../plugins'),
- cache = require('./cache'),
- pubsub = require('../pubsub'),
- utils = require('../../public/src/utils');
+var async = require('async');
+var validator = require('validator');
+var _ = require('underscore');
-module.exports = function(Posts) {
+var db = require('../database');
+var topics = require('../topics');
+var user = require('../user');
+var privileges = require('../privileges');
+var plugins = require('../plugins');
+var cache = require('./cache');
+var pubsub = require('../pubsub');
+var utils = require('../../public/src/utils');
- pubsub.on('post:edit', function(pid) {
+module.exports = function (Posts) {
+
+ pubsub.on('post:edit', function (pid) {
cache.del(pid);
});
- Posts.edit = function(data, callback) {
- var now = Date.now();
+ Posts.edit = function (data, callback) {
var postData;
var results;
@@ -28,8 +28,8 @@ module.exports = function(Posts) {
privileges.posts.canEdit(data.pid, data.uid, next);
},
function (canEdit, next) {
- if (!canEdit) {
- return next(new Error('[[error:no-privileges]]'));
+ if (!canEdit.flag) {
+ return next(new Error(canEdit.message));
}
Posts.getPostData(data.pid, next);
},
@@ -37,30 +37,26 @@ module.exports = function(Posts) {
if (!_postData) {
return next(new Error('[[error:no-post]]'));
}
+
postData = _postData;
postData.content = data.content;
- postData.edited = now;
+ postData.edited = Date.now();
postData.editor = data.uid;
- plugins.fireHook('filter:post.edit', {req: data.req, post: postData, uid: data.uid}, next);
+ if (data.handle) {
+ postData.handle = data.handle;
+ }
+ plugins.fireHook('filter:post.edit', {req: data.req, post: postData, data: data, uid: data.uid}, next);
},
function (result, next) {
postData = result.post;
- var updateData = {
- edited: postData.edited,
- editor: postData.editor,
- content: postData.content
- };
- if (data.handle) {
- updateData.handle = data.handle;
- }
- Posts.setPostFields(data.pid, updateData, next);
+ Posts.setPostFields(data.pid, postData, next);
},
function (next) {
async.parallel({
- editor: function(next) {
+ editor: function (next) {
user.getUserFields(data.uid, ['username', 'userslug'], next);
},
- topic: function(next) {
+ topic: function (next) {
editMainPost(data, postData, next);
}
}, next);
@@ -72,8 +68,8 @@ module.exports = function(Posts) {
plugins.fireHook('action:post.edit', _.clone(postData));
- cache.del(postData.pid);
- pubsub.publish('post:edit', postData.pid);
+ cache.del(String(postData.pid));
+ pubsub.publish('post:edit', String(postData.pid));
Posts.parsePost(postData, next);
},
@@ -89,13 +85,13 @@ module.exports = function(Posts) {
var title = data.title ? data.title.trim() : '';
async.parallel({
- topic: function(next) {
+ topic: function (next) {
topics.getTopicFields(tid, ['cid', 'title'], next);
},
- isMain: function(next) {
+ isMain: function (next) {
Posts.isMain(data.pid, next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return callback(err);
}
@@ -121,29 +117,31 @@ module.exports = function(Posts) {
topicData.slug = tid + '/' + (utils.slugify(title) || 'topic');
}
- topicData.thumb = data.topic_thumb || '';
+ topicData.thumb = data.thumb || '';
data.tags = data.tags || [];
async.waterfall([
- async.apply(plugins.fireHook, 'filter:topic.edit', {req: data.req, topic: topicData}),
- function(results, next) {
+ function (next) {
+ plugins.fireHook('filter:topic.edit', {req: data.req, topic: topicData, data: data}, next);
+ },
+ function (results, next) {
db.setObject('topic:' + tid, results.topic, next);
},
- function(next) {
+ function (next) {
topics.updateTags(tid, data.tags, next);
},
- function(next) {
+ function (next) {
topics.getTopicTagsObjects(tid, next);
},
- function(tags, next) {
+ function (tags, next) {
topicData.tags = data.tags;
plugins.fireHook('action:topic.edit', topicData);
next(null, {
tid: tid,
cid: results.topic.cid,
uid: postData.uid,
- title: validator.escape(title),
+ title: validator.escape(String(title)),
oldTitle: results.topic.title,
slug: topicData.slug,
isMainPost: true,
diff --git a/src/posts/flags.js b/src/posts/flags.js
index 3adb6541f2..84116346af 100644
--- a/src/posts/flags.js
+++ b/src/posts/flags.js
@@ -2,63 +2,78 @@
'use strict';
-var async = require('async'),
- db = require('../database'),
- user = require('../user');
+var async = require('async');
+var winston = require('winston');
+var db = require('../database');
+var user = require('../user');
+var analytics = require('../analytics');
+module.exports = function (Posts) {
-module.exports = function(Posts) {
-
- Posts.flag = function(post, uid, reason, callback) {
+ Posts.flag = function (post, uid, reason, callback) {
if (!parseInt(uid, 10) || !reason) {
return callback();
}
- async.parallel({
- hasFlagged: async.apply(hasFlagged, post.pid, uid),
- exists: async.apply(Posts.exists, post.pid)
- }, function(err, results) {
- if (err || !results.exists) {
- return callback(err || new Error('[[error:no-post]]'));
- }
- if (results.hasFlagged) {
- return callback(new Error('[[error:already-flagged]]'));
- }
- var now = Date.now();
-
- async.parallel([
- function(next) {
- db.sortedSetAdd('posts:flagged', now, post.pid, next);
- },
- function(next) {
- db.sortedSetIncrBy('posts:flags:count', 1, post.pid, next);
- },
- function(next) {
- db.incrObjectField('post:' + post.pid, 'flags', next);
- },
- function(next) {
- db.sortedSetAdd('pid:' + post.pid + ':flag:uids', now, uid, next);
- },
- function(next) {
- db.sortedSetAdd('pid:' + post.pid + ':flag:uid:reason', 0, uid + ':' + reason, next);
- },
- function(next) {
- if (parseInt(post.uid, 10)) {
- db.sortedSetAdd('uid:' + post.uid + ':flag:pids', now, post.pid, next);
- } else {
- next();
- }
- },
- function(next) {
- if (parseInt(post.uid, 10)) {
- db.setAdd('uid:' + post.uid + ':flagged_by', uid, next);
- } else {
- next();
- }
+ async.waterfall([
+ function (next) {
+ async.parallel({
+ hasFlagged: async.apply(hasFlagged, post.pid, uid),
+ exists: async.apply(Posts.exists, post.pid)
+ }, next);
+ },
+ function (results, next) {
+ if (!results.exists) {
+ return next(new Error('[[error:no-post]]'));
}
- ], function(err) {
- callback(err);
- });
+
+ if (results.hasFlagged) {
+ return next(new Error('[[error:already-flagged]]'));
+ }
+
+ var now = Date.now();
+ async.parallel([
+ function (next) {
+ db.sortedSetAdd('posts:flagged', now, post.pid, next);
+ },
+ function (next) {
+ db.sortedSetIncrBy('posts:flags:count', 1, post.pid, next);
+ },
+ function (next) {
+ db.incrObjectField('post:' + post.pid, 'flags', next);
+ },
+ function (next) {
+ db.sortedSetAdd('pid:' + post.pid + ':flag:uids', now, uid, next);
+ },
+ function (next) {
+ db.sortedSetAdd('pid:' + post.pid + ':flag:uid:reason', 0, uid + ':' + reason, next);
+ },
+ function (next) {
+ if (parseInt(post.uid, 10)) {
+ async.parallel([
+ async.apply(db.sortedSetIncrBy, 'users:flags', 1, post.uid),
+ async.apply(db.incrObjectField, 'user:' + post.uid, 'flags'),
+ async.apply(db.sortedSetAdd, 'uid:' + post.uid + ':flag:pids', now, post.pid)
+ ], next);
+ } else {
+ next();
+ }
+ }
+ ], next);
+ },
+ function (data, next) {
+ if (data[1] === 1) { // Only update state on new flag
+ Posts.updateFlagData(uid, post.pid, {
+ state: 'open'
+ }, next);
+ }
+ }
+ ], function (err) {
+ if (err) {
+ return callback(err);
+ }
+ analytics.increment('flags');
+ callback();
});
};
@@ -66,51 +81,109 @@ module.exports = function(Posts) {
db.isSortedSetMember('pid:' + pid + ':flag:uids', uid, callback);
}
- Posts.dismissFlag = function(pid, callback) {
- async.parallel([
- function(next) {
- db.getObjectField('post:' + pid, 'uid', function(err, uid) {
- if (err) {
- return next(err);
- }
+ Posts.dismissFlag = function (pid, callback) {
+ async.waterfall([
+ function (next) {
+ db.getObjectFields('post:' + pid, ['pid', 'uid', 'flags'], next);
+ },
+ function (postData, next) {
+ if (!postData.pid) {
+ return callback();
+ }
+ async.parallel([
+ function (next) {
+ if (parseInt(postData.uid, 10)) {
+ if (parseInt(postData.flags, 10) > 0) {
+ async.parallel([
+ async.apply(db.sortedSetIncrBy, 'users:flags', -postData.flags, postData.uid),
+ async.apply(db.incrObjectFieldBy, 'user:' + postData.uid, 'flags', -postData.flags)
+ ], next);
+ } else {
+ next();
+ }
+ } else {
+ next();
+ }
+ },
+ function (next) {
+ db.sortedSetsRemove([
+ 'posts:flagged',
+ 'posts:flags:count',
+ 'uid:' + postData.uid + ':flag:pids'
+ ], pid, next);
+ },
+ function (next) {
+ async.series([
+ function (next) {
+ db.getSortedSetRange('pid:' + pid + ':flag:uids', 0, -1, function (err, uids) {
+ if (err) {
+ return next(err);
+ }
- db.sortedSetsRemove([
- 'posts:flagged',
- 'posts:flags:count',
- 'uid:' + uid + ':flag:pids'
- ], pid, next);
- });
+ async.each(uids, function (uid, next) {
+ var nid = 'post_flag:' + pid + ':uid:' + uid;
+ async.parallel([
+ async.apply(db.delete, 'notifications:' + nid),
+ async.apply(db.sortedSetRemove, 'notifications', 'post_flag:' + pid + ':uid:' + uid)
+ ], next);
+ }, next);
+ });
+ },
+ async.apply(db.delete, 'pid:' + pid + ':flag:uids')
+ ], next);
+ },
+ async.apply(db.deleteObjectField, 'post:' + pid, 'flags'),
+ async.apply(db.delete, 'pid:' + pid + ':flag:uid:reason'),
+ async.apply(db.deleteObjectFields, 'post:' + pid, ['flag:state', 'flag:assignee', 'flag:notes', 'flag:history'])
+ ], next);
},
- function(next) {
- db.deleteObjectField('post:' + pid, 'flags', next);
- },
- function(next) {
- db.delete('pid:' + pid + ':flag:uids', next);
- },
- function(next) {
- db.delete('pid:' + pid + ':flag:uid:reason', next);
+ function (results, next) {
+ db.sortedSetsRemoveRangeByScore(['users:flags'], '-inf', 0, next);
}
- ], function(err) {
- callback(err);
- });
+ ], callback);
};
- Posts.dismissAllFlags = function(callback) {
- db.getSortedSetRange('posts:flagged', 0, -1, function(err, pids) {
+ Posts.dismissAllFlags = function (callback) {
+ db.getSortedSetRange('posts:flagged', 0, -1, function (err, pids) {
if (err) {
return callback(err);
}
- async.eachLimit(pids, 50, Posts.dismissFlag, callback);
+ async.eachSeries(pids, Posts.dismissFlag, callback);
});
};
- Posts.getFlags = function(set, uid, start, stop, callback) {
+ Posts.dismissUserFlags = function (uid, callback) {
+ db.getSortedSetRange('uid:' + uid + ':flag:pids', 0, -1, function (err, pids) {
+ if (err) {
+ return callback(err);
+ }
+ async.eachSeries(pids, Posts.dismissFlag, callback);
+ });
+ };
+
+ Posts.getFlags = function (set, cid, uid, start, stop, callback) {
async.waterfall([
function (next) {
- db.getSortedSetRevRange(set, start, stop, next);
+ if (Array.isArray(set)) {
+ db.getSortedSetRevIntersect({sets: set, start: start, stop: -1, aggregate: 'MAX'}, next);
+ } else {
+ db.getSortedSetRevRange(set, start, -1, next);
+ }
+ },
+ function (pids, next) {
+ if (cid) {
+ Posts.filterPidsByCid(pids, cid, next);
+ } else {
+ process.nextTick(next, null, pids);
+ }
},
function (pids, next) {
getFlaggedPostsWithReasons(pids, uid, next);
+ },
+ function (posts, next) {
+ var count = posts.length;
+ var end = stop - start + 1;
+ next(null, {posts: posts.slice(0, stop === -1 ? undefined : end), count: count});
}
], callback);
};
@@ -119,31 +192,31 @@ module.exports = function(Posts) {
async.waterfall([
function (next) {
async.parallel({
- uidsReasons: function(next) {
- async.map(pids, function(pid, next) {
+ uidsReasons: function (next) {
+ async.map(pids, function (pid, next) {
db.getSortedSetRange('pid:' + pid + ':flag:uid:reason', 0, -1, next);
}, next);
},
- posts: function(next) {
- Posts.getPostSummaryByPids(pids, uid, {stripTags: false, extraFields: ['flags']}, next);
+ posts: function (next) {
+ Posts.getPostSummaryByPids(pids, uid, {stripTags: false, extraFields: ['flags', 'flag:assignee', 'flag:state', 'flag:notes', 'flag:history']}, next);
}
}, next);
},
function (results, next) {
- async.map(results.uidsReasons, function(uidReasons, next) {
- async.map(uidReasons, function(uidReason, next) {
+ async.map(results.uidsReasons, function (uidReasons, next) {
+ async.map(uidReasons, function (uidReason, next) {
var uid = uidReason.split(':')[0];
var reason = uidReason.substr(uidReason.indexOf(':') + 1);
- user.getUserFields(uid, ['username', 'userslug', 'picture'], function(err, userData) {
+ user.getUserFields(uid, ['username', 'userslug', 'picture'], function (err, userData) {
next(err, {user: userData, reason: reason});
});
}, next);
- }, function(err, reasons) {
+ }, function (err, reasons) {
if (err) {
return callback(err);
}
- results.posts.forEach(function(post, index) {
+ results.posts.forEach(function (post, index) {
if (post) {
post.flagReasons = reasons[index];
}
@@ -151,33 +224,183 @@ module.exports = function(Posts) {
next(null, results.posts);
});
+ },
+ async.apply(Posts.expandFlagHistory),
+ function (posts, next) {
+ // Parse out flag data into its own object inside each post hash
+ async.map(posts, function (postObj, next) {
+ for(var prop in postObj) {
+ postObj.flagData = postObj.flagData || {};
+
+ if (postObj.hasOwnProperty(prop) && prop.startsWith('flag:')) {
+ postObj.flagData[prop.slice(5)] = postObj[prop];
+
+ if (prop === 'flag:state') {
+ switch(postObj[prop]) {
+ case 'open':
+ postObj.flagData.labelClass = 'info';
+ break;
+ case 'wip':
+ postObj.flagData.labelClass = 'warning';
+ break;
+ case 'resolved':
+ postObj.flagData.labelClass = 'success';
+ break;
+ case 'rejected':
+ postObj.flagData.labelClass = 'danger';
+ break;
+ }
+ }
+
+ delete postObj[prop];
+ }
+ }
+
+ if (postObj.flagData.assignee) {
+ user.getUserFields(parseInt(postObj.flagData.assignee, 10), ['username', 'picture'], function (err, userData) {
+ if (err) {
+ return next(err);
+ }
+
+ postObj.flagData.assigneeUser = userData;
+ next(null, postObj);
+ });
+ } else {
+ setImmediate(next.bind(null, null, postObj));
+ }
+ }, next);
}
], callback);
}
- Posts.getUserFlags = function(byUsername, sortBy, callerUID, start, stop, callback) {
- async.waterfall([
- function(next) {
- user.getUidByUsername(byUsername, next);
- },
- function(uid, next) {
- if (!uid) {
- return next(null, []);
+ Posts.updateFlagData = function (uid, pid, flagObj, callback) {
+ // Retrieve existing flag data to compare for history-saving purposes
+ var changes = [];
+ var changeset = {};
+ var prop;
+
+ Posts.getPostData(pid, function (err, postData) {
+ if (err) {
+ return callback(err);
+ }
+
+ // Track new additions
+ for(prop in flagObj) {
+ if (flagObj.hasOwnProperty(prop) && !postData.hasOwnProperty('flag:' + prop) && flagObj[prop].length) {
+ changes.push(prop);
}
- db.getSortedSetRevRange('uid:' + uid + ':flag:pids', 0, -1, next);
- },
- function(pids, next) {
- getFlaggedPostsWithReasons(pids, callerUID, next);
- },
- function(posts, next) {
- if (sortBy === 'count') {
- posts.sort(function(a, b) {
- return b.flags - a.flags;
+ }
+
+ // Track changed items
+ for(prop in postData) {
+ if (
+ postData.hasOwnProperty(prop) && prop.startsWith('flag:') &&
+ flagObj.hasOwnProperty(prop.slice(5)) &&
+ postData[prop] !== flagObj[prop.slice(5)]
+ ) {
+ changes.push(prop.slice(5));
+ }
+ }
+
+ changeset = changes.reduce(function (memo, prop) {
+ memo['flag:' + prop] = flagObj[prop];
+ return memo;
+ }, {});
+
+ // Append changes to history string
+ if (changes.length) {
+ try {
+ var history = JSON.parse(postData['flag:history'] || '[]');
+
+ changes.forEach(function (property) {
+ switch(property) {
+ case 'assignee': // intentional fall-through
+ case 'state':
+ history.unshift({
+ uid: uid,
+ type: property,
+ value: flagObj[property],
+ timestamp: Date.now()
+ });
+ break;
+
+ case 'notes':
+ history.unshift({
+ uid: uid,
+ type: property,
+ timestamp: Date.now()
+ });
+ }
});
+
+ changeset['flag:history'] = JSON.stringify(history);
+ } catch (e) {
+ winston.warn('[posts/updateFlagData] Unable to deserialise post flag history, likely malformed data');
+ }
+ }
+
+ // Save flag data into post hash
+ if (changes.length) {
+ Posts.setPostFields(pid, changeset, callback);
+ } else {
+ setImmediate(callback);
+ }
+ });
+ };
+
+ Posts.expandFlagHistory = function (posts, callback) {
+ // Expand flag history
+ async.map(posts, function (post, next) {
+ var history;
+ try {
+ history = JSON.parse(post['flag:history'] || '[]');
+ } catch (e) {
+ winston.warn('[posts/getFlags] Unable to deserialise post flag history, likely malformed data');
+ return callback(e);
+ }
+
+ async.map(history, function (event, next) {
+ event.timestampISO = new Date(event.timestamp).toISOString();
+
+ async.parallel([
+ function (next) {
+ user.getUserFields(event.uid, ['username', 'picture'], function (err, userData) {
+ if (err) {
+ return next(err);
+ }
+
+ event.user = userData;
+ next();
+ });
+ },
+ function (next) {
+ if (event.type === 'assignee') {
+ user.getUserField(parseInt(event.value, 10), 'username', function (err, username) {
+ if (err) {
+ return next(err);
+ }
+
+ event.label = username || 'Unknown user';
+ next(null);
+ });
+ } else if (event.type === 'state') {
+ event.label = '[[topic:flag_manage_state_' + event.value + ']]';
+ setImmediate(next);
+ } else {
+ setImmediate(next);
+ }
+ }
+ ], function (err) {
+ next(err, event);
+ });
+ }, function (err, history) {
+ if (err) {
+ return next(err);
}
- next(null, posts.slice(start, stop));
- }
- ], callback);
+ post['flag:history'] = history;
+ next(null, post);
+ });
+ }, callback);
};
};
diff --git a/src/posts/parse.js b/src/posts/parse.js
index 1af374305d..28af97c5e6 100644
--- a/src/posts/parse.js
+++ b/src/posts/parse.js
@@ -1,17 +1,22 @@
-
'use strict';
+var nconf = require('nconf');
+var url = require('url');
+var winston = require('winston');
+
var cache = require('./cache');
var plugins = require('../plugins');
var translator = require('../../public/src/modules/translator');
-module.exports = function(Posts) {
+var urlRegex = /href="([^"]+)"/g;
- Posts.parsePost = function(postData, callback) {
+module.exports = function (Posts) {
+
+ Posts.parsePost = function (postData, callback) {
postData.content = postData.content || '';
- if (postData.pid && cache.has(postData.pid)) {
- postData.content = cache.get(postData.pid);
+ if (postData.pid && cache.has(String(postData.pid))) {
+ postData.content = cache.get(String(postData.pid));
return callback(null, postData);
}
@@ -20,7 +25,7 @@ module.exports = function(Posts) {
postData.content = postData.content.toString();
}
- plugins.fireHook('filter:parse.post', {postData: postData}, function(err, data) {
+ plugins.fireHook('filter:parse.post', {postData: postData}, function (err, data) {
if (err) {
return callback(err);
}
@@ -28,16 +33,44 @@ module.exports = function(Posts) {
data.postData.content = translator.escape(data.postData.content);
if (global.env === 'production' && data.postData.pid) {
- cache.set(data.postData.pid, data.postData.content);
+ cache.set(String(data.postData.pid), data.postData.content);
}
callback(null, data.postData);
});
};
- Posts.parseSignature = function(userData, uid, callback) {
+ Posts.parseSignature = function (userData, uid, callback) {
userData.signature = userData.signature || '';
plugins.fireHook('filter:parse.signature', {userData: userData, uid: uid}, callback);
};
+
+ Posts.relativeToAbsolute = function (content) {
+ // Turns relative links in post body to absolute urls
+ var parsed, current, absolute;
+
+ while ((current = urlRegex.exec(content)) !== null) {
+ if (current[1]) {
+ try {
+ parsed = url.parse(current[1]);
+ if (!parsed.protocol) {
+ if (current[1].startsWith('/')) {
+ // Internal link
+ absolute = nconf.get('url') + current[1];
+ } else {
+ // External link
+ absolute = '//' + current[1];
+ }
+
+ content = content.slice(0, current.index + 6) + absolute + content.slice(current.index + 6 + current[1].length);
+ }
+ } catch(err) {
+ winston.verbose(err.messsage);
+ }
+ }
+ }
+
+ return content;
+ };
};
diff --git a/src/posts/recent.js b/src/posts/recent.js
index f7d588acff..aec0ea4637 100644
--- a/src/posts/recent.js
+++ b/src/posts/recent.js
@@ -5,14 +5,14 @@ var async = require('async'),
privileges = require('../privileges');
-module.exports = function(Posts) {
+module.exports = function (Posts) {
var terms = {
day: 86400000,
week: 604800000,
month: 2592000000
};
- Posts.getRecentPosts = function(uid, start, stop, term, callback) {
+ Posts.getRecentPosts = function (uid, start, stop, term, callback) {
var min = 0;
if (terms[term]) {
min = Date.now() - terms[term];
@@ -21,30 +21,30 @@ module.exports = function(Posts) {
var count = parseInt(stop, 10) === -1 ? stop : stop - start + 1;
async.waterfall([
- function(next) {
+ function (next) {
db.getSortedSetRevRangeByScore('posts:pid', start, count, '+inf', min, next);
},
- function(pids, next) {
+ function (pids, next) {
privileges.posts.filter('read', pids, uid, next);
},
- function(pids, next) {
+ function (pids, next) {
Posts.getPostSummaryByPids(pids, uid, {stripTags: true}, next);
}
], callback);
};
- Posts.getRecentPosterUids = function(start, stop, callback) {
+ Posts.getRecentPosterUids = function (start, stop, callback) {
async.waterfall([
- function(next) {
+ function (next) {
db.getSortedSetRevRange('posts:pid', start, stop, next);
},
- function(pids, next) {
+ function (pids, next) {
Posts.getPostsFields(pids, ['uid'], next);
},
- function(postData, next) {
- postData = postData.map(function(post) {
+ function (postData, next) {
+ postData = postData.map(function (post) {
return post && post.uid;
- }).filter(function(value, index, array) {
+ }).filter(function (value, index, array) {
return value && array.indexOf(value) === index;
});
next(null, postData);
diff --git a/src/posts/summary.js b/src/posts/summary.js
index 3cb5586bc6..270d9d480b 100644
--- a/src/posts/summary.js
+++ b/src/posts/summary.js
@@ -1,20 +1,20 @@
'use strict';
-var async = require('async'),
- validator = require('validator'),
- S = require('string'),
+var async = require('async');
+var validator = require('validator');
+var S = require('string');
- db = require('../database'),
- user = require('../user'),
- plugins = require('../plugins'),
- categories = require('../categories'),
- utils = require('../../public/src/utils');
+var db = require('../database');
+var user = require('../user');
+var plugins = require('../plugins');
+var categories = require('../categories');
+var utils = require('../../public/src/utils');
-module.exports = function(Posts) {
+module.exports = function (Posts) {
- Posts.getPostSummaryByPids = function(pids, uid, options, callback) {
+ Posts.getPostSummaryByPids = function (pids, uid, options, callback) {
if (!Array.isArray(pids) || !pids.length) {
return callback(null, []);
}
@@ -23,109 +23,112 @@ module.exports = function(Posts) {
options.parse = options.hasOwnProperty('parse') ? options.parse : true;
options.extraFields = options.hasOwnProperty('extraFields') ? options.extraFields : [];
- var fields = ['pid', 'tid', 'content', 'uid', 'timestamp', 'deleted'].concat(options.extraFields);
+ var fields = ['pid', 'tid', 'content', 'uid', 'timestamp', 'deleted', 'upvotes', 'downvotes'].concat(options.extraFields);
- Posts.getPostsFields(pids, fields, function(err, posts) {
- if (err) {
- return callback(err);
- }
+ var posts;
+ async.waterfall([
+ function (next) {
+ Posts.getPostsFields(pids, fields, next);
+ },
+ function (_posts, next) {
+ posts = _posts.filter(Boolean);
- posts = posts.filter(function(p) {
- return !!p && parseInt(p.deleted, 10) !== 1;
- });
-
- var uids = [], topicKeys = [];
- for(var i=0; i postDeleteDuration * 1000)) {
+ return callback(null, {flag: false, message: '[[error:post-delete-duration-expired, ' + meta.config.postDeleteDuration + ']]'});
+ }
+
+ callback(null, {flag: results.isOwner, message: '[[error:no-privileges]]'});
+ });
+ };
+
+ privileges.posts.canMove = function (pid, uid, callback) {
+ posts.isMain(pid, function (err, isMain) {
if (err || isMain) {
return callback(err || new Error('[[error:cant-move-mainpost]]'));
}
@@ -124,7 +204,7 @@ module.exports = function(privileges) {
});
};
- privileges.posts.canPurge = function(pid, uid, callback) {
+ privileges.posts.canPurge = function (pid, uid, callback) {
async.waterfall([
function (next) {
posts.getCidByPid(pid, next);
@@ -143,34 +223,39 @@ module.exports = function(privileges) {
};
function isPostEditable(pid, uid, callback) {
+ var tid;
async.waterfall([
- function(next) {
+ function (next) {
posts.getPostFields(pid, ['tid', 'timestamp'], next);
},
- function(postData, 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) {
+ 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);
}
function isAdminOrMod(pid, uid, callback) {
helpers.some([
- function(next) {
- posts.getCidByPid(pid, function(err, cid) {
+ function (next) {
+ posts.getCidByPid(pid, function (err, cid) {
if (err || !cid) {
return next(err, false);
}
@@ -178,9 +263,9 @@ module.exports = function(privileges) {
user.isModerator(uid, cid, next);
});
},
- function(next) {
+ function (next) {
user.isAdministrator(uid, next);
}
], callback);
}
-};
+};
\ No newline at end of file
diff --git a/src/privileges/topics.js b/src/privileges/topics.js
index 83e635fde9..39ad054462 100644
--- a/src/privileges/topics.js
+++ b/src/privileges/topics.js
@@ -2,52 +2,58 @@
'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');
var categories = require('../categories');
var plugins = require('../plugins');
-module.exports = function(privileges) {
+module.exports = function (privileges) {
privileges.topics = {};
- privileges.topics.get = function(tid, uid, callback) {
+ 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']),
- function(_topic, next) {
+ 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]),
- read: async.apply(helpers.isUserAllowedTo, 'read', uid, [topic.cid]),
- isOwner: function(next) {
- next(null, 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')
}, next);
}
- ], function(err, results) {
+ ], function (err, results) {
if (err) {
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: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,
@@ -56,8 +62,8 @@ module.exports = function(privileges) {
});
};
- privileges.topics.can = function(privilege, tid, uid, callback) {
- topics.getTopicField(tid, 'cid', function(err, cid) {
+ privileges.topics.can = function (privilege, tid, uid, callback) {
+ topics.getTopicField(tid, 'cid', function (err, cid) {
if (err) {
return callback(err);
}
@@ -66,98 +72,86 @@ module.exports = function(privileges) {
});
};
- privileges.topics.filterTids = function(privilege, tids, uid, callback) {
+ privileges.topics.filterTids = function (privilege, tids, uid, callback) {
if (!Array.isArray(tids) || !tids.length) {
return callback(null, []);
}
-
+ var cids;
+ var topicsData;
async.waterfall([
- function(next) {
+ function (next) {
topics.getTopicsFields(tids, ['tid', 'cid', 'deleted'], next);
},
- function(topicsData, next) {
- var cids = topicsData.map(function(topic) {
+ function (_topicsData, next) {
+ topicsData = _topicsData;
+ cids = topicsData.map(function (topic) {
return topic.cid;
- }).filter(function(cid, index, array) {
+ }).filter(function (cid, index, array) {
return cid && array.indexOf(cid) === index;
});
- async.parallel({
- categories: function(next) {
- categories.getCategoriesFields(cids, ['disabled'], next);
- },
- allowedTo: function(next) {
- helpers.isUserAllowedTo(privilege, uid, cids, next);
- },
- isModerators: function(next) {
- user.isModerator(uid, cids, next);
- },
- isAdmin: function(next) {
- user.isAdministrator(uid, next);
- }
- }, function(err, results) {
- if (err) {
- return next(err);
- }
- var isModOf = {};
- cids = cids.filter(function(cid, index) {
- isModOf[cid] = results.isModerators[index];
- return !results.categories[index].disabled &&
- (results.allowedTo[index] || results.isAdmin || results.isModerators[index]);
- });
+ privileges.categories.getBase(privilege, cids, uid, next);
+ },
+ function (results, next) {
- tids = topicsData.filter(function(topic) {
- return cids.indexOf(topic.cid) !== -1 &&
- (parseInt(topic.deleted, 10) !== 1 || results.isAdmin || isModOf[topic.cid]);
- }).map(function(topic) {
- return topic.tid;
- });
+ var isModOf = {};
+ cids = cids.filter(function (cid, index) {
+ isModOf[cid] = results.isModerators[index];
+ return !results.categories[index].disabled &&
+ (results.allowedTo[index] || results.isAdmin || results.isModerators[index]);
+ });
- plugins.fireHook('filter:privileges.topics.filter', {
- privilege: privilege,
- uid: uid,
- tids: tids
- }, function(err, data) {
- next(err, data ? data.tids : null);
- });
+ tids = topicsData.filter(function (topic) {
+ return cids.indexOf(topic.cid) !== -1 &&
+ (parseInt(topic.deleted, 10) !== 1 || results.isAdmin || isModOf[topic.cid]);
+ }).map(function (topic) {
+ return topic.tid;
+ });
+
+ plugins.fireHook('filter:privileges.topics.filter', {
+ privilege: privilege,
+ uid: uid,
+ tids: tids
+ }, function (err, data) {
+ next(err, data ? data.tids : null);
});
}
], callback);
};
- privileges.topics.filterUids = function(privilege, tid, uids, callback) {
+ privileges.topics.filterUids = function (privilege, tid, uids, callback) {
if (!Array.isArray(uids) || !uids.length) {
return callback(null, []);
}
- uids = uids.filter(function(uid, index, array) {
+ uids = uids.filter(function (uid, index, array) {
return array.indexOf(uid) === index;
});
async.waterfall([
- function(next) {
+ function (next) {
topics.getTopicFields(tid, ['tid', 'cid', 'deleted'], next);
},
- function(topicData, next) {
+ function (topicData, next) {
async.parallel({
- disabled: function(next) {
+ disabled: function (next) {
categories.getCategoryField(topicData.cid, 'disabled', next);
},
- allowedTo: function(next) {
+ allowedTo: function (next) {
helpers.isUsersAllowedTo(privilege, uids, topicData.cid, next);
},
- isModerators: function(next) {
+ isModerators: function (next) {
user.isModerator(uids, topicData.cid, next);
},
- isAdmins: function(next) {
+ isAdmins: function (next) {
user.isAdministrator(uids, next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return next(err);
}
- uids = uids.filter(function(uid, index) {
+ uids = uids.filter(function (uid, index) {
return parseInt(results.disabled, 10) !== 1 &&
((results.allowedTo[index] && parseInt(topicData.deleted, 10) !== 1) || results.isAdmins[index] || results.isModerators[index]);
});
@@ -168,7 +162,7 @@ module.exports = function(privileges) {
], callback);
};
- privileges.topics.canPurge = function(tid, uid, callback) {
+ privileges.topics.canPurge = function (tid, uid, callback) {
async.waterfall([
function (next) {
topics.getTopicField(tid, 'cid', next);
@@ -186,35 +180,75 @@ module.exports = function(privileges) {
], callback);
};
- privileges.topics.canEdit = function(tid, uid, 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);
};
- privileges.topics.isOwnerOrAdminOrMod = function(tid, uid, callback) {
+ privileges.topics.isOwnerOrAdminOrMod = function (tid, uid, callback) {
helpers.some([
- function(next) {
+ function (next) {
topics.isOwner(tid, uid, next);
},
- function(next) {
+ function (next) {
privileges.topics.isAdminOrMod(tid, uid, next);
}
], callback);
};
- privileges.topics.isAdminOrMod = function(tid, uid, callback) {
+ privileges.topics.isAdminOrMod = function (tid, uid, callback) {
helpers.some([
- function(next) {
- topics.getTopicField(tid, 'cid', function(err, cid) {
+ function (next) {
+ topics.getTopicField(tid, 'cid', function (err, cid) {
if (err) {
return next(err);
}
user.isModerator(uid, cid, next);
});
},
- function(next) {
+ function (next) {
user.isAdministrator(uid, next);
}
], callback);
};
-};
+};
\ No newline at end of file
diff --git a/src/privileges/users.js b/src/privileges/users.js
index 4ee2d69461..4f6341be9f 100644
--- a/src/privileges/users.js
+++ b/src/privileges/users.js
@@ -6,11 +6,11 @@ var async = require('async');
var groups = require('../groups');
var plugins = require('../plugins');
-module.exports = function(privileges) {
+module.exports = function (privileges) {
privileges.users = {};
- privileges.users.isAdministrator = function(uid, callback) {
+ privileges.users.isAdministrator = function (uid, callback) {
if (Array.isArray(uid)) {
groups.isMembers(uid, 'administrators', callback);
} else {
@@ -18,7 +18,7 @@ module.exports = function(privileges) {
}
};
- privileges.users.isGlobalModerator = function(uid, callback) {
+ privileges.users.isGlobalModerator = function (uid, callback) {
if (Array.isArray(uid)) {
groups.isMembers(uid, 'Global Moderators', callback);
} else {
@@ -26,7 +26,7 @@ module.exports = function(privileges) {
}
};
- privileges.users.isModerator = function(uid, cid, callback) {
+ privileges.users.isModerator = function (uid, cid, callback) {
if (Array.isArray(cid)) {
isModeratorOfCategories(cid, uid, callback);
} else {
@@ -40,48 +40,48 @@ module.exports = function(privileges) {
function isModeratorOfCategories(cids, uid, callback) {
if (!parseInt(uid, 10)) {
- return filterIsModerator(cids, uid, cids.map(function() {return false;}), callback);
+ return filterIsModerator(cids, uid, cids.map(function () {return false;}), callback);
}
- privileges.users.isGlobalModerator(uid, function(err, isGlobalModerator) {
+ privileges.users.isGlobalModerator(uid, function (err, isGlobalModerator) {
if (err) {
return callback(err);
}
if (isGlobalModerator) {
- return filterIsModerator(cids, uid, cids.map(function() {return true;}), callback);
+ return filterIsModerator(cids, uid, cids.map(function () {return true;}), callback);
}
- var uniqueCids = cids.filter(function(cid, index, array) {
+ var uniqueCids = cids.filter(function (cid, index, array) {
return array.indexOf(cid) === index;
});
- var groupNames = uniqueCids.map(function(cid) {
+ var groupNames = uniqueCids.map(function (cid) {
return 'cid:' + cid + ':privileges:mods'; // At some point we should *probably* change this to "moderate" as well
});
- var groupListNames = uniqueCids.map(function(cid) {
+ var groupListNames = uniqueCids.map(function (cid) {
return 'cid:' + cid + ':privileges:groups:moderate';
});
async.parallel({
user: async.apply(groups.isMemberOfGroups, uid, groupNames),
group: async.apply(groups.isMemberOfGroupsList, uid, groupListNames)
- }, function(err, checks) {
+ }, function (err, checks) {
if (err) {
return callback(err);
}
- var isMembers = checks.user.map(function(isMember, idx) {
+ var isMembers = checks.user.map(function (isMember, idx) {
return isMember || checks.group[idx];
}),
map = {};
- uniqueCids.forEach(function(cid, index) {
+ uniqueCids.forEach(function (cid, index) {
map[cid] = isMembers[index];
});
- var isModerator = cids.map(function(cid) {
+ var isModerator = cids.map(function (cid) {
return map[cid];
});
@@ -95,12 +95,12 @@ module.exports = function(privileges) {
async.apply(privileges.users.isGlobalModerator, uids),
async.apply(groups.isMembers, uids, 'cid:' + cid + ':privileges:mods'),
async.apply(groups.isMembersOfGroupList, uids, 'cid:' + cid + ':privileges:groups:moderate')
- ], function(err, checks) {
+ ], function (err, checks) {
if (err) {
return callback(err);
}
- var isModerator = checks[0].map(function(isMember, idx) {
+ var isModerator = checks[0].map(function (isMember, idx) {
return isMember || checks[1][idx] || checks[2][idx];
});
@@ -113,7 +113,7 @@ module.exports = function(privileges) {
async.apply(privileges.users.isGlobalModerator, uid),
async.apply(groups.isMember, uid, 'cid:' + cid + ':privileges:mods'),
async.apply(groups.isMemberOfGroupList, uid, 'cid:' + cid + ':privileges:groups:moderate')
- ], function(err, checks) {
+ ], function (err, checks) {
if (err) {
return callback(err);
}
@@ -124,7 +124,7 @@ module.exports = function(privileges) {
}
function filterIsModerator(cid, uid, isModerator, callback) {
- plugins.fireHook('filter:user.isModerator', {uid: uid, cid: cid, isModerator: isModerator}, function(err, data) {
+ plugins.fireHook('filter:user.isModerator', {uid: uid, cid: cid, isModerator: isModerator}, function (err, data) {
if (err) {
return callback(err);
}
@@ -136,4 +136,4 @@ module.exports = function(privileges) {
});
}
-};
+};
\ No newline at end of file
diff --git a/src/pubsub.js b/src/pubsub.js
index 82414a5c31..a2e11746e1 100644
--- a/src/pubsub.js
+++ b/src/pubsub.js
@@ -8,7 +8,7 @@ var nconf = require('nconf'),
var channelName;
-var PubSub = function() {
+var PubSub = function () {
var self = this;
if (nconf.get('redis')) {
var redis = require('./database/redis');
@@ -18,7 +18,7 @@ var PubSub = function() {
channelName = 'db:' + nconf.get('redis:database') + 'pubsub_channel';
subClient.subscribe(channelName);
- subClient.on('message', function(channel, message) {
+ subClient.on('message', function (channel, message) {
if (channel !== channelName) {
return;
}
@@ -35,7 +35,7 @@ var PubSub = function() {
util.inherits(PubSub, EventEmitter);
-PubSub.prototype.publish = function(event, data) {
+PubSub.prototype.publish = function (event, data) {
if (this.pubClient) {
this.pubClient.publish(channelName, JSON.stringify({event: event, data: data}));
} else {
diff --git a/src/reset.js b/src/reset.js
index f1e27738e8..f38a2be4fd 100644
--- a/src/reset.js
+++ b/src/reset.js
@@ -8,8 +8,8 @@ var db = require('./database');
var Reset = {};
-Reset.reset = function() {
- db.init(function(err) {
+Reset.reset = function () {
+ db.init(function (err) {
if (err) {
winston.error(err.message);
process.exit();
@@ -32,7 +32,7 @@ Reset.reset = function() {
} else if (nconf.get('s')) {
resetSettings();
} else if (nconf.get('a')) {
- require('async').series([resetWidgets, resetThemes, resetPlugins, resetSettings], function(err) {
+ require('async').series([resetWidgets, resetThemes, resetPlugins, resetSettings], function (err) {
if (!err) {
winston.info('[reset] Reset complete.');
} else {
@@ -59,7 +59,7 @@ Reset.reset = function() {
function resetSettings(callback) {
var meta = require('./meta');
- meta.configs.set('allowLocalLogin', 1, function(err) {
+ meta.configs.set('allowLocalLogin', 1, function (err) {
winston.info('[reset] Settings reset to default');
if (typeof callback === 'function') {
callback(err);
@@ -73,7 +73,7 @@ function resetTheme(themeId) {
var meta = require('./meta');
var fs = require('fs');
- fs.access('node_modules/' + themeId + '/package.json', function(err, fd) {
+ fs.access('node_modules/' + themeId + '/package.json', function (err, fd) {
if (err) {
winston.warn('[reset] Theme `%s` is not installed on this forum', themeId);
process.exit();
@@ -81,8 +81,13 @@ function resetTheme(themeId) {
meta.themes.set({
type: 'local',
id: themeId
- }, function(err) {
- winston.info('[reset] Theme reset to ' + themeId);
+ }, function (err) {
+ if (err) {
+ winston.warn('[reset] Failed to reset theme to ' + themeId);
+ } else {
+ winston.info('[reset] Theme reset to ' + themeId);
+ }
+
process.exit();
});
}
@@ -95,7 +100,7 @@ function resetThemes(callback) {
meta.themes.set({
type: 'local',
id: 'nodebb-theme-persona'
- }, function(err) {
+ }, function (err) {
winston.info('[reset] Theme reset to Persona');
if (typeof callback === 'function') {
callback(err);
@@ -110,7 +115,7 @@ function resetPlugin(pluginId) {
async.waterfall([
async.apply(db.isSortedSetMember, 'plugins:active', pluginId),
- function(isMember, next) {
+ function (isMember, next) {
active = isMember;
if (isMember) {
@@ -119,7 +124,7 @@ function resetPlugin(pluginId) {
next();
}
}
- ], function(err) {
+ ], function (err) {
if (err) {
winston.error('[reset] Could not disable plugin: %s encountered error %s', pluginId, err.message);
} else {
@@ -136,7 +141,7 @@ function resetPlugin(pluginId) {
}
function resetPlugins(callback) {
- db.delete('plugins:active', function(err) {
+ db.delete('plugins:active', function (err) {
winston.info('[reset] All Plugins De-activated');
if (typeof callback === 'function') {
callback(err);
@@ -147,7 +152,7 @@ function resetPlugins(callback) {
}
function resetWidgets(callback) {
- require('./widgets').reset(function(err) {
+ require('./widgets').reset(function (err) {
winston.info('[reset] All Widgets moved to Draft Zone');
if (typeof callback === 'function') {
callback(err);
diff --git a/src/rewards/admin.js b/src/rewards/admin.js
index e6c32bc34b..fcfacf5877 100644
--- a/src/rewards/admin.js
+++ b/src/rewards/admin.js
@@ -6,7 +6,7 @@ var rewards = {},
db = require('../database');
-rewards.save = function(data, callback) {
+rewards.save = function (data, callback) {
function save(data, next) {
function commit(err, id) {
if (err) {
@@ -16,16 +16,16 @@ rewards.save = function(data, callback) {
data.id = id;
async.series([
- function(next) {
+ function (next) {
rewards.delete(data, next);
},
- function(next) {
+ function (next) {
db.setAdd('rewards:list', data.id, next);
},
- function(next) {
+ function (next) {
db.setObject('rewards:id:' + data.id, data, next);
},
- function(next) {
+ function (next) {
db.setObject('rewards:id:' + data.id + ':rewards', rewardsData, next);
}
], next);
@@ -45,42 +45,46 @@ rewards.save = function(data, callback) {
}
}
- async.each(data, save, function(err) {
+ async.each(data, save, function (err) {
+ if (err) {
+ return callback(err);
+ }
+
saveConditions(data, callback);
});
};
-rewards.delete = function(data, callback) {
+rewards.delete = function (data, callback) {
async.parallel([
- function(next) {
+ function (next) {
db.setRemove('rewards:list', data.id, next);
},
- function(next) {
+ function (next) {
db.delete('rewards:id:' + data.id, next);
},
- function(next) {
+ function (next) {
db.delete('rewards:id:' + data.id + ':rewards', next);
}
], callback);
};
-rewards.get = function(callback) {
+rewards.get = function (callback) {
async.parallel({
active: getActiveRewards,
- conditions: function(next) {
+ conditions: function (next) {
plugins.fireHook('filter:rewards.conditions', [], next);
},
- conditionals: function(next) {
+ conditionals: function (next) {
plugins.fireHook('filter:rewards.conditionals', [], next);
},
- rewards: function(next) {
+ rewards: function (next) {
plugins.fireHook('filter:rewards.rewards', [], next);
}
}, callback);
};
function saveConditions(data, callback) {
- db.delete('conditions:active', function(err) {
+ db.delete('conditions:active', function (err) {
if (err) {
return callback(err);
}
@@ -88,7 +92,7 @@ function saveConditions(data, callback) {
var conditions = [],
rewardsPerCondition = {};
- data.forEach(function(reward) {
+ data.forEach(function (reward) {
conditions.push(reward.condition);
rewardsPerCondition[reward.condition] = rewardsPerCondition[reward.condition] || [];
rewardsPerCondition[reward.condition].push(reward.id);
@@ -96,7 +100,7 @@ function saveConditions(data, callback) {
db.setAdd('conditions:active', conditions, callback);
- async.each(Object.keys(rewardsPerCondition), function(condition, next) {
+ async.each(Object.keys(rewardsPerCondition), function (condition, next) {
db.setAdd('condition:' + condition + ':rewards', rewardsPerCondition[condition], next);
}, callback);
});
@@ -107,13 +111,13 @@ function getActiveRewards(callback) {
function load(id, next) {
async.parallel({
- main: function(next) {
+ main: function (next) {
db.getObject('rewards:id:' + id, next);
},
- rewards: function(next) {
+ rewards: function (next) {
db.getObject('rewards:id:' + id + ':rewards', next);
}
- }, function(err, data) {
+ }, function (err, data) {
if (data.main) {
data.main.disabled = data.main.disabled === 'true';
data.main.rewards = data.rewards;
@@ -124,8 +128,12 @@ function getActiveRewards(callback) {
});
}
- db.getSetMembers('rewards:list', function(err, rewards) {
- async.eachSeries(rewards, load, function(err) {
+ 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 1b46b190d9..4ba403fce8 100644
--- a/src/rewards/index.js
+++ b/src/rewards/index.js
@@ -6,10 +6,10 @@ var rewards = {},
async = require('async');
-rewards.checkConditionAndRewardUser = function(uid, condition, method, callback) {
+rewards.checkConditionAndRewardUser = function (uid, condition, method, callback) {
async.waterfall([
- function(next) {
- isConditionActive(condition, function(err, isActive) {
+ function (next) {
+ isConditionActive(condition, function (err, isActive) {
if (!isActive) {
return back(err);
}
@@ -17,16 +17,16 @@ rewards.checkConditionAndRewardUser = function(uid, condition, method, callback)
next(err);
});
},
- function(next) {
- getIDsByCondition(condition, function(err, ids) {
+ function (next) {
+ getIDsByCondition(condition, function (err, ids) {
next(err, ids);
});
},
- function(ids, next) {
+ function (ids, next) {
getRewardDataByIDs(ids, next);
},
- function(rewards, next) {
- filterCompletedRewards(uid, rewards, function(err, filtered) {
+ function (rewards, next) {
+ filterCompletedRewards(uid, rewards, function (err, filtered) {
if (!filtered || !filtered.length) {
return back(err);
}
@@ -34,14 +34,14 @@ rewards.checkConditionAndRewardUser = function(uid, condition, method, callback)
next(err, filtered);
});
},
- function(rewards, next) {
- async.filter(rewards, function(reward, next) {
+ function (rewards, next) {
+ async.filter(rewards, function (reward, next) {
if (!reward) {
return next(false);
}
checkCondition(reward, method, next);
- }, function(eligible) {
+ }, function (eligible) {
if (!eligible) {
return next(false);
}
@@ -68,18 +68,18 @@ function getIDsByCondition(condition, callback) {
}
function filterCompletedRewards(uid, rewards, callback) {
- db.getSortedSetRangeByScoreWithScores('uid:' + uid + ':rewards', 0, -1, 1, '+inf', function(err, data) {
+ db.getSortedSetRangeByScoreWithScores('uid:' + uid + ':rewards', 0, -1, 1, '+inf', function (err, data) {
if (err) {
return callback(err);
}
var userRewards = {};
- data.forEach(function(obj) {
+ data.forEach(function (obj) {
userRewards[obj.value] = parseInt(obj.score, 10);
});
- rewards = rewards.filter(function(reward) {
+ rewards = rewards.filter(function (reward) {
if (!reward) {
return false;
}
@@ -90,7 +90,7 @@ function filterCompletedRewards(uid, rewards, callback) {
return true;
}
- return (userRewards[reward.id] > reward.claimable) ? false : true;
+ return (userRewards[reward.id] >= reward.claimable) ? false : true;
});
callback(false, rewards);
@@ -98,28 +98,36 @@ function filterCompletedRewards(uid, rewards, callback) {
}
function getRewardDataByIDs(ids, callback) {
- db.getObjects(ids.map(function(id) {
+ db.getObjects(ids.map(function (id) {
return 'rewards:id:' + id;
}), callback);
}
function getRewardsByRewardData(rewards, callback) {
- db.getObjects(rewards.map(function(reward) {
+ db.getObjects(rewards.map(function (reward) {
return 'rewards:id:' + reward.id + ':rewards';
}), callback);
}
function checkCondition(reward, method, callback) {
- method(function(err, value) {
- plugins.fireHook('filter:rewards.checkConditional:' + reward.conditional, {left: value, right: reward.value}, function(err, bool) {
- callback(bool);
+ 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(err || bool);
});
});
}
function giveRewards(uid, rewards, callback) {
- getRewardsByRewardData(rewards, function(err, rewardData) {
- async.each(rewards, function(reward, next) {
+ 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);
}, callback);
diff --git a/src/routes/accounts.js b/src/routes/accounts.js
index 8c7a505d0b..7cda2228b0 100644
--- a/src/routes/accounts.js
+++ b/src/routes/accounts.js
@@ -7,6 +7,8 @@ module.exports = function (app, middleware, controllers) {
var middlewares = [middleware.checkGlobalPrivacySettings];
var accountMiddlewares = [middleware.checkGlobalPrivacySettings, middleware.checkAccountPermissions];
+ setupPageRoute(app, '/uid/:uid/:section?', middleware, [], middleware.redirectUidToUserslug);
+
setupPageRoute(app, '/user/:userslug', middleware, middlewares, controllers.accounts.profile.get);
setupPageRoute(app, '/user/:userslug/following', middleware, middlewares, controllers.accounts.follow.getFollowing);
setupPageRoute(app, '/user/:userslug/followers', middleware, middlewares, controllers.accounts.follow.getFollowers);
@@ -15,7 +17,7 @@ module.exports = function (app, middleware, controllers) {
setupPageRoute(app, '/user/:userslug/best', middleware, middlewares, controllers.accounts.posts.getBestPosts);
setupPageRoute(app, '/user/:userslug/groups', middleware, middlewares, controllers.accounts.groups.get);
- setupPageRoute(app, '/user/:userslug/favourites', middleware, accountMiddlewares, controllers.accounts.posts.getFavourites);
+ setupPageRoute(app, '/user/:userslug/bookmarks', middleware, accountMiddlewares, controllers.accounts.posts.getBookmarks);
setupPageRoute(app, '/user/:userslug/watched', middleware, accountMiddlewares, controllers.accounts.posts.getWatchedTopics);
setupPageRoute(app, '/user/:userslug/upvoted', middleware, accountMiddlewares, controllers.accounts.posts.getUpVotedPosts);
setupPageRoute(app, '/user/:userslug/downvoted', middleware, accountMiddlewares, controllers.accounts.posts.getDownVotedPosts);
@@ -23,10 +25,12 @@ module.exports = function (app, middleware, controllers) {
setupPageRoute(app, '/user/:userslug/edit/username', middleware, accountMiddlewares, controllers.accounts.edit.username);
setupPageRoute(app, '/user/:userslug/edit/email', middleware, accountMiddlewares, controllers.accounts.edit.email);
setupPageRoute(app, '/user/:userslug/edit/password', middleware, accountMiddlewares, controllers.accounts.edit.password);
+ setupPageRoute(app, '/user/:userslug/info', middleware, accountMiddlewares, controllers.accounts.info.get);
setupPageRoute(app, '/user/:userslug/settings', middleware, accountMiddlewares, controllers.accounts.settings.get);
- app.delete('/user/:userslug/session/:uuid', accountMiddlewares, 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, middlewares, 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 6ce4fb5f48..7d7029482c 100644
--- a/src/routes/admin.js
+++ b/src/routes/admin.js
@@ -15,6 +15,7 @@ function apiRoutes(router, middleware, controllers) {
router.post('/uploadfavicon', middlewares, controllers.admin.uploads.uploadFavicon);
router.post('/uploadTouchIcon', middlewares, controllers.admin.uploads.uploadTouchIcon);
router.post('/uploadlogo', middlewares, controllers.admin.uploads.uploadLogo);
+ router.post('/uploadOgImage', middlewares, controllers.admin.uploads.uploadOgImage);
router.post('/upload/sound', middlewares, controllers.admin.uploads.uploadSound);
router.post('/uploadDefaultAvatar', middlewares, controllers.admin.uploads.uploadDefaultAvatar);
}
@@ -64,6 +65,7 @@ function addRoutes(router, middleware, controllers) {
router.get('/manage/users/not-validated', middlewares, controllers.admin.users.notValidated);
router.get('/manage/users/no-posts', middlewares, controllers.admin.users.noPosts);
router.get('/manage/users/inactive', middlewares, controllers.admin.users.inactive);
+ router.get('/manage/users/flagged', middlewares, controllers.admin.users.flagged);
router.get('/manage/users/banned', middlewares, controllers.admin.users.banned);
router.get('/manage/registration', middlewares, controllers.admin.users.registrationQueue);
@@ -81,13 +83,15 @@ function addRoutes(router, middleware, controllers) {
router.get('/advanced/database', middlewares, controllers.admin.database.get);
router.get('/advanced/events', middlewares, controllers.admin.events.get);
router.get('/advanced/logs', middlewares, controllers.admin.logs.get);
- router.get('/advanced/post-cache', middlewares, controllers.admin.postCache.get);
+ router.get('/advanced/errors', middlewares, controllers.admin.errors.get);
+ router.get('/advanced/errors/export', middlewares, controllers.admin.errors.export);
+ 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);
}
-module.exports = function(app, middleware, controllers) {
+module.exports = function (app, middleware, controllers) {
app.use('/admin/', adminRouter(middleware, controllers));
app.use('/api/admin/', apiRouter(middleware, controllers));
};
diff --git a/src/routes/api.js b/src/routes/api.js
index 34cf142b37..be76336c77 100644
--- a/src/routes/api.js
+++ b/src/routes/api.js
@@ -1,10 +1,10 @@
"use strict";
-var express = require('express'),
+var express = require('express');
- uploadsController = require('../controllers/uploads');
+var uploadsController = require('../controllers/uploads');
-module.exports = function(app, middleware, controllers) {
+module.exports = function (app, middleware, controllers) {
var router = express.Router();
app.use('/api', router);
@@ -12,6 +12,7 @@ module.exports = function(app, middleware, controllers) {
router.get('/config', middleware.applyCSRF, controllers.api.getConfig);
router.get('/widgets/render', controllers.api.renderWidgets);
+ router.get('/me', middleware.checkGlobalPrivacySettings, controllers.api.getCurrentUser);
router.get('/user/uid/:uid', middleware.checkGlobalPrivacySettings, controllers.api.getUserByUID);
router.get('/user/username/:username', middleware.checkGlobalPrivacySettings, controllers.api.getUserByUsername);
router.get('/user/email/:email', middleware.checkGlobalPrivacySettings, controllers.api.getUserByEmail);
@@ -22,8 +23,9 @@ module.exports = function(app, middleware, controllers) {
router.get('/categories/:cid/moderators', controllers.api.getModerators);
router.get('/recent/posts/:term?', controllers.api.getRecentPosts);
- router.get('/unread/total', middleware.authenticate, controllers.unread.unreadTotal);
+ router.get('/unread/:filter?/total', middleware.authenticate, controllers.unread.unreadTotal);
router.get('/topic/teaser/:topic_id', controllers.topics.teaser);
+ router.get('/topic/pagination/:topic_id', controllers.topics.pagination);
var multipart = require('connect-multiparty');
var multipartMiddleware = multipart();
diff --git a/src/routes/authentication.js b/src/routes/authentication.js
index 8e1824cad6..c6f1359fc2 100644
--- a/src/routes/authentication.js
+++ b/src/routes/authentication.js
@@ -1,4 +1,4 @@
-(function(Auth) {
+(function (Auth) {
"use strict";
var passport = require('passport'),
@@ -13,11 +13,11 @@
loginStrategies = [];
- Auth.initialize = function(app, middleware) {
+ Auth.initialize = function (app, middleware) {
app.use(passport.initialize());
app.use(passport.session());
- app.use(function(req, res, next) {
+ app.use(function (req, res, next) {
req.uid = req.user ? parseInt(req.user.uid, 10) : 0;
next();
});
@@ -26,11 +26,11 @@
Auth.middleware = middleware;
};
- Auth.getLoginStrategies = function() {
+ Auth.getLoginStrategies = function () {
return loginStrategies;
};
- Auth.reloadRoutes = function(callback) {
+ Auth.reloadRoutes = function (callback) {
var router = express.Router();
router.hotswapId = 'auth';
@@ -43,16 +43,17 @@
passport.use(new passportLocal({passReqToCallback: true}, controllers.authentication.localLogin));
}
- plugins.fireHook('filter:auth.init', loginStrategies, function(err) {
+ plugins.fireHook('filter:auth.init', loginStrategies, function (err) {
if (err) {
winston.error('filter:auth.init - plugin failure');
return callback(err);
}
- loginStrategies.forEach(function(strategy) {
+ loginStrategies.forEach(function (strategy) {
if (strategy.url) {
router.get(strategy.url, passport.authenticate(strategy.name, {
- scope: strategy.scope
+ scope: strategy.scope,
+ prompt: strategy.prompt || undefined
}));
}
@@ -63,6 +64,8 @@
});
router.post('/register', Auth.middleware.applyCSRF, Auth.middleware.applyBlacklist, controllers.authentication.register);
+ router.post('/register/complete', Auth.middleware.applyCSRF, Auth.middleware.applyBlacklist, controllers.authentication.registerComplete);
+ router.get('/register/abort', controllers.authentication.registerAbort);
router.post('/login', Auth.middleware.applyCSRF, Auth.middleware.applyBlacklist, controllers.authentication.login);
router.post('/logout', Auth.middleware.applyCSRF, controllers.authentication.logout);
@@ -73,11 +76,11 @@
});
};
- passport.serializeUser(function(user, done) {
+ passport.serializeUser(function (user, done) {
done(null, user.uid);
});
- passport.deserializeUser(function(uid, done) {
+ passport.deserializeUser(function (uid, done) {
done(null, {
uid: uid
});
diff --git a/src/routes/debug.js b/src/routes/debug.js
index b81938ccc9..9c5efe6c93 100644
--- a/src/routes/debug.js
+++ b/src/routes/debug.js
@@ -1,13 +1,15 @@
"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) {
+module.exports = function (app, middleware, controllers) {
var router = express.Router();
router.get('/uid/:uid', function (req, res) {
@@ -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 {
@@ -56,7 +74,7 @@ module.exports = function(app, middleware, controllers) {
});
});
- router.get('/test', function(req, res) {
+ router.get('/test', function (req, res) {
res.redirect(404);
});
diff --git a/src/routes/feeds.js b/src/routes/feeds.js
index bfa2945fb5..7a178ab9f0 100644
--- a/src/routes/feeds.js
+++ b/src/routes/feeds.js
@@ -23,10 +23,10 @@ function generateForTopic(req, res, callback) {
async.waterfall([
function (next) {
async.parallel({
- privileges: function(next) {
+ privileges: function (next) {
privileges.topics.get(tid, req.uid, next);
},
- topic: function(next) {
+ topic: function (next) {
topics.getTopicData(tid, next);
}
}, next);
@@ -38,13 +38,13 @@ function generateForTopic(req, res, callback) {
if (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted) {
return callback();
}
- if (!results.privileges.read) {
+ if (!results.privileges.read || !results.privileges['topics:read']) {
return helpers.notAllowed(req, res);
}
userPrivileges = results.privileges;
topics.getTopicWithPosts(results.topic, 'tid:' + tid + ':posts', req.uid, 0, 25, false, next);
}
- ], function(err, topicData) {
+ ], function (err, topicData) {
if (err) {
return callback(err);
}
@@ -70,14 +70,14 @@ function generateForTopic(req, res, callback) {
feed.pubDate = new Date(parseInt(topicData.posts[0].timestamp, 10)).toUTCString();
}
- topicData.posts.forEach(function(postData) {
+ topicData.posts.forEach(function (postData) {
if (!postData.deleted) {
dateStamp = new Date(parseInt(parseInt(postData.edited, 10) === 0 ? postData.timestamp : postData.edited, 10)).toUTCString();
feed.item({
title: 'Reply to ' + topicData.title + ' on ' + dateStamp,
description: postData.content,
- url: nconf.get('url') + '/topic/' + topicData.slug + (postData.index ? '/' + (postData.index + 1) : ''),
+ url: nconf.get('url') + '/post/' + postData.pid,
author: postData.user ? postData.user.username : '',
date: dateStamp
});
@@ -105,9 +105,9 @@ function generateForUserTopics(req, res, callback) {
}
user.getUserFields(uid, ['uid', 'username'], next);
}
- ], function(err, userData) {
+ ], function (err, userData) {
if (err) {
- return next(err);
+ return callback(err);
}
generateForTopics({
@@ -116,7 +116,7 @@ function generateForUserTopics(req, res, callback) {
description: 'A list of topics that are posted by ' + userData.username,
feed_url: '/user/' + userslug + '/topics.rss',
site_url: '/user/' + userslug + '/topics'
- }, 'uid:' + userData.uid + ':topics', req, res, next);
+ }, 'uid:' + userData.uid + ':topics', req, res, callback);
});
}
@@ -129,10 +129,10 @@ function generateForCategory(req, res, next) {
async.waterfall([
function (next) {
async.parallel({
- privileges: function(next) {
+ privileges: function (next) {
privileges.categories.get(cid, req.uid, next);
},
- category: function(next) {
+ category: function (next) {
categories.getCategoryById({
cid: cid,
set: 'cid:' + cid + ':tids',
@@ -156,7 +156,7 @@ function generateForCategory(req, res, next) {
site_url: '/category/' + results.category.cid,
}, results.category.topics, next);
}
- ], function(err, feed) {
+ ], function (err, feed) {
if (err) {
return next(err);
}
@@ -189,7 +189,7 @@ function generateForPopular(req, res, next) {
};
var term = terms[req.params.term] || 'day';
- topics.getPopular(term, req.uid, 19, function(err, topics) {
+ topics.getPopular(term, req.uid, 19, function (err, topics) {
if (err) {
return next(err);
}
@@ -200,7 +200,7 @@ function generateForPopular(req, res, next) {
description: 'A list of topics that are sorted by post count',
feed_url: '/popular/' + (req.params.term || 'daily') + '.rss',
site_url: '/popular/' + (req.params.term || 'daily')
- }, topics, function(err, feed) {
+ }, topics, function (err, feed) {
if (err) {
return next(err);
}
@@ -215,7 +215,7 @@ function generateForTopics(options, set, req, res, next) {
return next(err);
}
- generateTopicsFeed(options, data.topics, function(err, feed) {
+ generateTopicsFeed(options, data.topics, function (err, feed) {
if (err) {
return next(err);
}
@@ -238,7 +238,7 @@ function generateTopicsFeed(feedOptions, feedTopics, callback) {
feed.pubDate = new Date(parseInt(feedTopics[0].lastposttime, 10)).toUTCString();
}
- async.map(feedTopics, function(topicData, next) {
+ async.map(feedTopics, function (topicData, next) {
var feedItem = {
title: topicData.title,
url: nconf.get('url') + '/topic/' + topicData.slug,
@@ -251,7 +251,7 @@ function generateTopicsFeed(feedOptions, feedTopics, callback) {
return next(null, feedItem);
}
- topics.getMainPost(topicData.tid, feedOptions.uid, function(err, mainPost) {
+ topics.getMainPost(topicData.tid, feedOptions.uid, function (err, mainPost) {
if (err) {
return next(err);
}
@@ -262,11 +262,11 @@ function generateTopicsFeed(feedOptions, feedTopics, callback) {
feedItem.author = mainPost.user.username;
next(null, feedItem);
});
- }, function(err, feedItems) {
+ }, function (err, feedItems) {
if (err) {
return callback(err);
}
- feedItems.forEach(function(feedItem) {
+ feedItems.forEach(function (feedItem) {
if (feedItem) {
feed.item(feedItem);
}
@@ -280,7 +280,7 @@ function generateForRecentPosts(req, res, next) {
return next();
}
- posts.getRecentPosts(req.uid, 0, 19, 'month', function(err, posts) {
+ posts.getRecentPosts(req.uid, 0, 19, 'month', function (err, posts) {
if (err) {
return next(err);
}
@@ -303,16 +303,16 @@ function generateForCategoryRecentPosts(req, res, next) {
var cid = req.params.category_id;
async.parallel({
- privileges: function(next) {
+ privileges: function (next) {
privileges.categories.get(cid, req.uid, next);
},
- category: function(next) {
+ category: function (next) {
categories.getCategoryData(cid, next);
},
- posts: function(next) {
+ posts: function (next) {
categories.getRecentReplies(cid, req.uid, 20, next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return next(err);
}
@@ -346,11 +346,11 @@ function generateForPostsFeed(feedOptions, posts) {
feed.pubDate = new Date(parseInt(posts[0].timestamp, 10)).toUTCString();
}
- posts.forEach(function(postData) {
+ posts.forEach(function (postData) {
feed.item({
title: postData.topic ? postData.topic.title : '',
description: postData.content,
- url: nconf.get('url') + '/topic/' + (postData.topic ? postData.topic.slug : '#') + '/'+postData.index,
+ url: nconf.get('url') + '/post/' + postData.pid,
author: postData.user ? postData.user.username : '',
date: new Date(parseInt(postData.timestamp, 10)).toUTCString()
});
@@ -364,7 +364,7 @@ function sendFeed(feed, res) {
res.type('xml').set('Content-Length', Buffer.byteLength(xml)).send(xml);
}
-module.exports = function(app, middleware, controllers){
+module.exports = function (app, middleware, controllers){
app.get('/topic/:topic_id.rss', generateForTopic);
app.get('/category/:category_id.rss', generateForCategory);
app.get('/recent.rss', generateForRecent);
diff --git a/src/routes/helpers.js b/src/routes/helpers.js
index 3dcca80bc4..052d99292c 100644
--- a/src/routes/helpers.js
+++ b/src/routes/helpers.js
@@ -2,8 +2,8 @@
var helpers = {};
-helpers.setupPageRoute = function(router, name, middleware, middlewares, controller) {
- middlewares = middlewares.concat([middleware.pageView, middleware.pluginHooks]);
+helpers.setupPageRoute = function (router, name, middleware, middlewares, controller) {
+ middlewares = middlewares.concat([middleware.registrationComplete, middleware.pageView, middleware.pluginHooks]);
router.get(name, middleware.busyCheck, middleware.buildHeader, middlewares, controller);
router.get('/api' + name, middlewares, controller);
diff --git a/src/routes/index.js b/src/routes/index.js
index a80eb2e46a..e1e5c79d80 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -1,23 +1,21 @@
"use strict";
-var nconf = require('nconf'),
- path = require('path'),
- async = require('async'),
- winston = require('winston'),
- controllers = require('../controllers'),
- plugins = require('../plugins'),
- express = require('express'),
- validator = require('validator'),
+var nconf = require('nconf');
+var path = require('path');
+var async = require('async');
+var controllers = require('../controllers');
+var plugins = require('../plugins');
+var user = require('../user');
+var express = require('express');
- accountRoutes = require('./accounts'),
-
- metaRoutes = require('./meta'),
- apiRoutes = require('./api'),
- adminRoutes = require('./admin'),
- feedRoutes = require('./feeds'),
- pluginRoutes = require('./plugins'),
- authRoutes = require('./authentication'),
- helpers = require('./helpers');
+var accountRoutes = require('./accounts');
+var metaRoutes = require('./meta');
+var apiRoutes = require('./api');
+var adminRoutes = require('./admin');
+var feedRoutes = require('./feeds');
+var pluginRoutes = require('./plugins');
+var authRoutes = require('./authentication');
+var helpers = require('./helpers');
var setupPageRoute = helpers.setupPageRoute;
@@ -28,17 +26,24 @@ function mainRoutes(app, middleware, controllers) {
setupPageRoute(app, '/login', middleware, loginRegisterMiddleware, controllers.login);
setupPageRoute(app, '/register', middleware, loginRegisterMiddleware, controllers.register);
+ setupPageRoute(app, '/register/complete', middleware, [], controllers.registerInterstitial);
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 modRoutes(app, middleware, controllers) {
+ setupPageRoute(app, '/posts/flags', middleware, [], controllers.mods.flagged);
}
function globalModRoutes(app, middleware, controllers) {
setupPageRoute(app, '/ip-blacklist', middleware, [], controllers.globalMods.ipBlacklist);
- setupPageRoute(app, '/posts/flags', middleware, [], controllers.globalMods.flagged);
}
function topicRoutes(app, middleware, controllers) {
@@ -46,6 +51,10 @@ function topicRoutes(app, middleware, controllers) {
setupPageRoute(app, '/topic/:topic_id/:slug?', middleware, [], controllers.topics.get);
}
+function postRoutes(app, middleware, controllers) {
+ setupPageRoute(app, '/post/:pid', middleware, [], controllers.posts.redirectToPost);
+}
+
function tagRoutes(app, middleware, controllers) {
setupPageRoute(app, '/tags/:tag', middleware, [middleware.privateTagListing], controllers.tags.getTag);
setupPageRoute(app, '/tags', middleware, [middleware.privateTagListing], controllers.tags.getTags);
@@ -55,7 +64,7 @@ function categoryRoutes(app, middleware, controllers) {
setupPageRoute(app, '/categories', middleware, [], controllers.categories.list);
setupPageRoute(app, '/popular/:term?', middleware, [], controllers.popular.get);
setupPageRoute(app, '/recent', middleware, [], controllers.recent.get);
- setupPageRoute(app, '/unread', middleware, [middleware.authenticate], controllers.unread.get);
+ setupPageRoute(app, '/unread/:filter?', middleware, [middleware.authenticate], controllers.unread.get);
setupPageRoute(app, '/category/:category_id/:slug/:topic_index', middleware, [], controllers.category.get);
setupPageRoute(app, '/category/:category_id/:slug?', middleware, [], controllers.category.get);
@@ -64,33 +73,37 @@ 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', 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);
setupPageRoute(app, '/groups/:slug/members', middleware, middlewares, controllers.groups.members);
}
-module.exports = function(app, middleware) {
- var router = express.Router(),
- pluginRouter = express.Router(),
- authRouter = express.Router(),
- relativePath = nconf.get('relative_path'),
- ensureLoggedIn = require('connect-ensure-login');
+module.exports = function (app, middleware, hotswapIds) {
+ var routers = [
+ 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');
- pluginRouter.render = function() {
- app.render.apply(app, arguments);
- };
- controllers.render = function() {
+ if (Array.isArray(hotswapIds) && hotswapIds.length) {
+ for(var idx,x = 0;x < hotswapIds.length;x++) {
+ idx = routers.push(express.Router()) - 1;
+ routers[idx].hotswapId = hotswapIds[x];
+ }
+ }
+
+ pluginRouter.render = function () {
app.render.apply(app, arguments);
};
@@ -112,6 +125,8 @@ module.exports = function(app, middleware) {
mainRoutes(router, middleware, controllers);
topicRoutes(router, middleware, controllers);
+ postRoutes(router, middleware, controllers);
+ modRoutes(router, middleware, controllers);
globalModRoutes(router, middleware, controllers);
tagRoutes(router, middleware, controllers);
categoryRoutes(router, middleware, controllers);
@@ -119,98 +134,28 @@ module.exports = function(app, middleware) {
userRoutes(router, middleware, controllers);
groupRoutes(router, middleware, controllers);
- app.use(relativePath, pluginRouter);
- app.use(relativePath, router);
- app.use(relativePath, authRouter);
+ for(var x = 0;x < routers.length;x++) {
+ app.use(relativePath, routers[x]);
+ }
if (process.env.NODE_ENV === 'development') {
require('./debug')(app, middleware, controllers);
}
app.use(middleware.privateUploads);
-
+ app.use(relativePath + '/language/:code', middleware.processLanguages);
app.use(relativePath, express.static(path.join(__dirname, '../../', 'public'), {
maxAge: app.enabled('cache') ? 5184000000 : 0
}));
-
- handle404(app, middleware);
- handleErrors(app, middleware);
-
+ app.use('/vendor/jquery/timeago/locales', middleware.processTimeagoLocales);
+ app.use(controllers.handle404);
+ app.use(controllers.handleURIErrors);
+ app.use(controllers.handleErrors);
// Add plugin routes
async.series([
async.apply(plugins.reloadRoutes),
- async.apply(authRoutes.reloadRoutes)
+ async.apply(authRoutes.reloadRoutes),
+ async.apply(user.addInterstitials)
]);
};
-
-function handle404(app, middleware) {
- var relativePath = nconf.get('relative_path');
- var isLanguage = new RegExp('^' + relativePath + '/language/.*/.*.json');
- var isClientScript = new RegExp('^' + relativePath + '\\/src\\/.+\\.js');
-
- app.use(function(req, res) {
- if (plugins.hasListeners('action:meta.override404')) {
- return plugins.fireHook('action:meta.override404', {
- req: req,
- res: res,
- error: {}
- });
- }
-
- if (isClientScript.test(req.url)) {
- res.type('text/javascript').status(200).send('');
- } else if (isLanguage.test(req.url)) {
- res.status(200).json({});
- } else if (req.path.startsWith(relativePath + '/uploads')) {
- res.status(404).send('');
- } else if (req.path === '/favicon.ico') {
- res.status(404).send('');
- } else if (req.accepts('html')) {
- if (process.env.NODE_ENV === 'development') {
- winston.warn('Route requested but not found: ' + req.url);
- }
-
- res.status(404);
-
- if (res.locals.isAPI) {
- return res.json({path: validator.escape(req.path.replace(/^\/api/, '') || ''), title: '[[global:404.title]]'});
- }
-
- middleware.buildHeader(req, res, function() {
- res.render('404', {path: validator.escape(req.path || ''), title: '[[global:404.title]]'});
- });
- } else {
- res.status(404).type('txt').send('Not found');
- }
- });
-}
-
-function handleErrors(app, middleware) {
- app.use(function(err, req, res, next) {
- switch (err.code) {
- case 'EBADCSRFTOKEN':
- winston.error(req.path + '\n', err.message);
- return res.sendStatus(403);
- case 'blacklisted-ip':
- return res.status(403).type('text/plain').send(err.message);
- }
-
- if (parseInt(err.status, 10) === 302 && err.path) {
- return res.locals.isAPI ? res.status(302).json(err.path) : res.redirect(err.path);
- }
-
- winston.error(req.path + '\n', err.stack);
-
- res.status(err.status || 500);
-
- if (res.locals.isAPI) {
- res.json({path: validator.escape(req.path || ''), error: err.message});
- } else {
- middleware.buildHeader(req, res, function() {
- res.render('500', {path: validator.escape(req.path || ''), error: validator.escape(err.message)});
- });
- }
- });
-}
-
diff --git a/src/routes/meta.js b/src/routes/meta.js
index 65757566de..c62465c8d0 100644
--- a/src/routes/meta.js
+++ b/src/routes/meta.js
@@ -1,11 +1,14 @@
"use strict";
-var meta = require('../meta'),
- middleware = require('../middleware');
+var path = require('path');
+var nconf = require('nconf');
+
+var meta = require('../meta');
-function sendMinifiedJS(req, res, next) {
- var cache = meta.js.target['nodebb.min.js'] ? meta.js.target['nodebb.min.js'].cache : '';
+function sendMinifiedJS(req, res) {
+ var target = path.basename(req.path);
+ var cache = meta.js.target[target] ? meta.js.target[target].cache : '';
res.type('text/javascript').send(cache);
}
@@ -19,18 +22,29 @@ function sendMinifiedJS(req, res, next) {
// }
// };
-function sendStylesheet(req, res, next) {
+function sendStylesheet(req, res) {
res.type('text/css').status(200).send(meta.css.cache);
}
-function sendACPStylesheet(req, res, next) {
+function sendACPStylesheet(req, res) {
res.type('text/css').status(200).send(meta.css.acpCache);
}
-module.exports = function(app, middleware, controllers) {
+function sendSoundFile(req, res, next) {
+ var resolved = meta.sounds._filePathHash[path.basename(req.path)];
+
+ if (resolved) {
+ res.status(200).sendFile(resolved);
+ } else {
+ next();
+ }
+}
+
+module.exports = function (app, middleware, controllers) {
app.get('/stylesheet.css', middleware.addExpiresHeaders, sendStylesheet);
app.get('/admin.css', middleware.addExpiresHeaders, sendACPStylesheet);
app.get('/nodebb.min.js', middleware.addExpiresHeaders, sendMinifiedJS);
+ app.get('/acp.min.js', middleware.addExpiresHeaders, sendMinifiedJS);
// app.get('/nodebb.min.js.map', middleware.addExpiresHeaders, sendJSSourceMap);
app.get('/sitemap.xml', controllers.sitemap.render);
app.get('/sitemap/pages.xml', controllers.sitemap.getPages);
@@ -39,4 +53,8 @@ module.exports = function(app, middleware, controllers) {
app.get('/robots.txt', controllers.robots);
app.get('/manifest.json', controllers.manifest);
app.get('/css/previews/:theme', controllers.admin.themes.get);
+
+ if (nconf.get('local-assets') === false) {
+ app.get('/sounds/*', middleware.addExpiresHeaders, sendSoundFile);
+ }
};
diff --git a/src/routes/plugins.js b/src/routes/plugins.js
index 72af49fbed..37e23c5ee6 100644
--- a/src/routes/plugins.js
+++ b/src/routes/plugins.js
@@ -1,41 +1,40 @@
"use strict";
-var _ = require('underscore'),
- nconf = require('nconf'),
- path = require('path'),
- fs = require('fs'),
- validator = require('validator'),
- async = require('async'),
- winston = require('winston'),
+var _ = require('underscore');
+var path = require('path');
- plugins = require('../plugins'),
- helpers = require('../controllers/helpers');
+var plugins = require('../plugins');
-
-module.exports = function(app, middleware, controllers) {
+module.exports = function (app, middleware, controllers) {
// Static Assets
- app.get('/plugins/:id/*', middleware.addExpiresHeaders, function(req, res, next) {
- var relPath = req._parsedUrl.pathname.replace('/plugins/', ''),
- matches = _.map(plugins.staticDirs, function(realPath, mappedPath) {
- if (relPath.match(mappedPath)) {
- return mappedPath;
- } else {
- return null;
- }
- }).filter(Boolean);
+ app.get('/plugins/:id/*', middleware.addExpiresHeaders, function (req, res, next) {
- if (!matches) {
+ var relPath = req._parsedUrl.pathname.replace('/plugins/', '');
+
+ var matches = _.map(plugins.staticDirs, function (realPath, mappedPath) {
+ if (relPath.match(mappedPath)) {
+ var pathToFile = path.join(plugins.staticDirs[mappedPath], decodeURIComponent(relPath.slice(mappedPath.length)));
+ if (pathToFile.startsWith(plugins.staticDirs[mappedPath])) {
+ return pathToFile;
+ }
+ }
+
+ return null;
+ }).filter(Boolean);
+
+ if (!matches || !matches.length) {
return next();
}
- matches = matches.map(function(mappedPath) {
- return path.join(plugins.staticDirs[mappedPath], decodeURIComponent(relPath.slice(mappedPath.length)));
+ res.sendFile(matches[0], {}, function (err) {
+ if (err) {
+ if (err.code === 'ENOENT') {
+ // File doesn't exist, this isn't an error, to send to 404 handler
+ return next();
+ } else {
+ return next(err);
+ }
+ }
});
-
- if (matches.length) {
- res.sendFile(matches[0]);
- } else {
- next();
- }
});
-};
+};
\ No newline at end of file
diff --git a/src/search.js b/src/search.js
index 9b7306c7fa..7f03634625 100644
--- a/src/search.js
+++ b/src/search.js
@@ -16,7 +16,7 @@ var search = {};
module.exports = search;
-search.search = function(data, callback) {
+search.search = function (data, callback) {
var start = process.hrtime();
var searchIn = data.searchIn || 'titlesposts';
@@ -34,7 +34,7 @@ search.search = function(data, callback) {
}
},
function (result, next) {
- result.search_query = validator.escape(data.query || '');
+ result.search_query = validator.escape(String(data.query || ''));
result.time = (process.elapsedTimeSince(start) / 1000).toFixed(2);
next(null, result);
}
@@ -44,26 +44,26 @@ search.search = function(data, callback) {
function searchInContent(data, callback) {
data.uid = data.uid || 0;
async.parallel({
- searchCids: function(next) {
+ searchCids: function (next) {
getSearchCids(data, next);
},
- searchUids: function(next) {
+ searchUids: function (next) {
getSearchUids(data, next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return callback(err);
}
async.parallel({
- pids: function(next) {
+ pids: function (next) {
if (data.searchIn === 'posts' || data.searchIn === 'titlesposts') {
search.searchQuery('post', data.query, results.searchCids, results.searchUids, next);
} else {
next(null, []);
}
},
- tids: function(next) {
+ tids: function (next) {
if (data.searchIn === 'titles' || data.searchIn === 'titlesposts') {
search.searchQuery('topic', data.query, results.searchCids, results.searchUids, next);
} else {
@@ -81,22 +81,22 @@ function searchInContent(data, callback) {
}
async.waterfall([
- function(next) {
+ function (next) {
topics.getMainPids(results.tids, next);
},
- function(mainPids, next) {
- results.pids = mainPids.concat(results.pids).map(function(pid) {
+ function (mainPids, next) {
+ results.pids = mainPids.concat(results.pids).map(function (pid) {
return pid && pid.toString();
- }).filter(function(pid, index, array) {
+ }).filter(function (pid, index, array) {
return pid && array.indexOf(pid) === index;
});
privileges.posts.filter('read', results.pids, data.uid, next);
},
- function(pids, next) {
+ function (pids, next) {
filterAndSort(pids, data, next);
},
- function(pids, next) {
+ function (pids, next) {
matchCount = pids.length;
if (data.page) {
var start = Math.max(0, (data.page - 1)) * 10;
@@ -105,7 +105,7 @@ function searchInContent(data, callback) {
posts.getPostSummaryByPids(pids, data.uid, {}, next);
},
- function(posts, next) {
+ function (posts, next) {
next(null, {posts: posts, matchCount: matchCount, pageCount: Math.max(1, Math.ceil(parseInt(matchCount, 10) / 10))});
}
], callback);
@@ -114,7 +114,7 @@ function searchInContent(data, callback) {
}
function filterAndSort(pids, data, callback) {
- getMatchedPosts(pids, data, function(err, posts) {
+ getMatchedPosts(pids, data, function (err, posts) {
if (err) {
return callback(err);
}
@@ -129,7 +129,7 @@ function filterAndSort(pids, data, callback) {
sortPosts(posts, data);
- pids = posts.map(function(post) {
+ pids = posts.map(function (post) {
return post && post.pid;
});
@@ -162,21 +162,21 @@ function getMatchedPosts(pids, data, callback) {
var posts;
async.waterfall([
- function(next) {
- var keys = pids.map(function(pid) {
+ function (next) {
+ var keys = pids.map(function (pid) {
return 'post:' + pid;
});
db.getObjectsFields(keys, postFields, next);
},
- function(_posts, next) {
- posts = _posts.filter(function(post) {
+ function (_posts, next) {
+ posts = _posts.filter(function (post) {
return post && parseInt(post.deleted, 10) !== 1;
});
async.parallel({
- users: function(next) {
+ users: function (next) {
if (data.sortBy && data.sortBy.startsWith('user')) {
- var uids = posts.map(function(post) {
+ var uids = posts.map(function (post) {
return post.uid;
});
user.getUsersFields(uids, ['username'], next);
@@ -184,22 +184,22 @@ function getMatchedPosts(pids, data, callback) {
next();
}
},
- topics: function(next) {
+ topics: function (next) {
var topics;
async.waterfall([
- function(next) {
- var topicKeys = posts.map(function(post) {
+ function (next) {
+ var topicKeys = posts.map(function (post) {
return 'topic:' + post.tid;
});
db.getObjectsFields(topicKeys, topicFields, next);
},
- function(_topics, next) {
+ function (_topics, next) {
topics = _topics;
async.parallel({
- teasers: function(next) {
+ teasers: function (next) {
if (topicFields.indexOf('teaserPid') !== -1) {
- var teaserKeys = topics.map(function(topic) {
+ var teaserKeys = topics.map(function (topic) {
return 'post:' + topic.teaserPid;
});
db.getObjectsFields(teaserKeys, ['timestamp'], next);
@@ -207,23 +207,23 @@ function getMatchedPosts(pids, data, callback) {
next();
}
},
- categories: function(next) {
+ categories: function (next) {
if (!categoryFields.length) {
return next();
}
- var cids = topics.map(function(topic) {
+ var cids = topics.map(function (topic) {
return 'category:' + topic.cid;
});
db.getObjectsFields(cids, categoryFields, next);
}
}, next);
}
- ], function(err, results) {
+ ], function (err, results) {
if (err) {
return next(err);
}
- topics.forEach(function(topic, index) {
+ topics.forEach(function (topic, index) {
if (topic && results.categories && results.categories[index]) {
topic.category = results.categories[index];
}
@@ -237,9 +237,9 @@ function getMatchedPosts(pids, data, callback) {
}
}, next);
},
- function(results, next) {
+ function (results, next) {
- posts.forEach(function(post, index) {
+ posts.forEach(function (post, index) {
if (results.topics && results.topics[index]) {
post.topic = results.topics[index];
if (results.topics[index].category) {
@@ -255,7 +255,7 @@ function getMatchedPosts(pids, data, callback) {
}
});
- posts = posts.filter(function(post) {
+ posts = posts.filter(function (post) {
return post && post.topic && parseInt(post.topic.deleted, 10) !== 1;
});
@@ -268,11 +268,11 @@ function filterByPostcount(posts, postCount, repliesFilter) {
postCount = parseInt(postCount, 10);
if (postCount) {
if (repliesFilter === 'atleast') {
- posts = posts.filter(function(post) {
+ posts = posts.filter(function (post) {
return post.topic && post.topic.postcount >= postCount;
});
} else {
- posts = posts.filter(function(post) {
+ posts = posts.filter(function (post) {
return post.topic && post.topic.postcount <= postCount;
});
}
@@ -285,11 +285,11 @@ function filterByTimerange(posts, timeRange, timeFilter) {
if (timeRange) {
var time = Date.now() - timeRange;
if (timeFilter === 'newer') {
- posts = posts.filter(function(post) {
+ posts = posts.filter(function (post) {
return post.timestamp >= time;
});
} else {
- posts = posts.filter(function(post) {
+ posts = posts.filter(function (post) {
return post.timestamp <= time;
});
}
@@ -298,15 +298,15 @@ function filterByTimerange(posts, timeRange, timeFilter) {
}
function sortPosts(posts, data) {
- if (!posts.length) {
+ if (!posts.length || !data.sortBy) {
return;
}
- data.sortBy = data.sortBy || 'timestamp';
+
data.sortDirection = data.sortDirection || 'desc';
var direction = data.sortDirection === 'desc' ? 1 : -1;
if (data.sortBy === 'timestamp') {
- posts.sort(function(p1, p2) {
+ posts.sort(function (p1, p2) {
return direction * (p2.timestamp - p1.timestamp);
});
@@ -323,11 +323,11 @@ function sortPosts(posts, data) {
var isNumeric = utils.isNumber(firstPost[fields[0]][fields[1]]);
if (isNumeric) {
- posts.sort(function(p1, p2) {
+ posts.sort(function (p1, p2) {
return direction * (p2[fields[0]][fields[1]] - p1[fields[0]][fields[1]]);
});
} else {
- posts.sort(function(p1, p2) {
+ posts.sort(function (p1, p2) {
if (p1[fields[0]][fields[1]] > p2[fields[0]][fields[1]]) {
return direction;
} else if (p1[fields[0]][fields[1]] < p2[fields[0]][fields[1]]) {
@@ -345,10 +345,10 @@ function getSearchCids(data, callback) {
if (data.categories.indexOf('all') !== -1) {
async.waterfall([
- function(next) {
+ function (next) {
db.getSortedSetRange('categories:cid', 0, -1, next);
},
- function(cids, next) {
+ function (cids, next) {
privileges.categories.filterCids('read', cids, data.uid, next);
}
], callback);
@@ -356,26 +356,26 @@ function getSearchCids(data, callback) {
}
async.parallel({
- watchedCids: function(next) {
+ watchedCids: function (next) {
if (data.categories.indexOf('watched') !== -1) {
user.getWatchedCategories(data.uid, next);
} else {
next(null, []);
}
},
- childrenCids: function(next) {
+ childrenCids: function (next) {
if (data.searchChildren) {
getChildrenCids(data.categories, data.uid, next);
} else {
next(null, []);
}
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return callback(err);
}
- var cids = results.watchedCids.concat(results.childrenCids).concat(data.categories).filter(function(cid, index, array) {
+ var cids = results.watchedCids.concat(results.childrenCids).concat(data.categories).filter(function (cid, index, array) {
return cid && array.indexOf(cid) === index;
});
@@ -384,7 +384,7 @@ function getSearchCids(data, callback) {
}
function getChildrenCids(cids, uid, callback) {
- categories.getChildren(cids, uid, function(err, childrenCategories) {
+ categories.getChildren(cids, uid, function (err, childrenCategories) {
if (err) {
return callback(err);
}
@@ -392,9 +392,9 @@ function getChildrenCids(cids, uid, callback) {
var childrenCids = [];
var allCategories = [];
- childrenCategories.forEach(function(childrens) {
+ childrenCategories.forEach(function (childrens) {
categories.flattenCategories(allCategories, childrens);
- childrenCids = childrenCids.concat(allCategories.map(function(category) {
+ childrenCids = childrenCids.concat(allCategories.map(function (category) {
return category && category.cid;
}));
});
@@ -411,7 +411,7 @@ function getSearchUids(data, callback) {
}
}
-search.searchQuery = function(index, content, cids, uids, callback) {
+search.searchQuery = function (index, content, cids, uids, callback) {
plugins.fireHook('filter:search.query', {
index: index,
content: content,
diff --git a/src/sitemap.js b/src/sitemap.js
index 4c7efb73b5..d21bb96241 100644
--- a/src/sitemap.js
+++ b/src/sitemap.js
@@ -1,16 +1,15 @@
'use strict';
-var path = require('path'),
- async = require('async'),
- sm = require('sitemap'),
- url = require('url'),
- nconf = require('nconf'),
- db = require('./database'),
- categories = require('./categories'),
- topics = require('./topics'),
- privileges = require('./privileges'),
- meta = require('./meta'),
- utils = require('../public/src/utils');
+var async = require('async');
+var sm = require('sitemap');
+var nconf = require('nconf');
+
+var db = require('./database');
+var categories = require('./categories');
+var topics = require('./topics');
+var privileges = require('./privileges');
+var meta = require('./meta');
+var utils = require('../public/src/utils');
var sitemap = {
maps: {
@@ -18,7 +17,7 @@ var sitemap = {
}
};
-sitemap.render = function(callback) {
+sitemap.render = function (callback) {
var numTopics = parseInt(meta.config.sitemapTopics, 10) || 500;
var returnData = {
url: nconf.get('url'),
@@ -28,17 +27,17 @@ sitemap.render = function(callback) {
async.waterfall([
async.apply(db.getSortedSetRange, 'topics:recent', 0, -1),
- function(tids, next) {
+ function (tids, next) {
privileges.topics.filterTids('read', tids, 0, next);
}
- ], function(err, tids) {
+ ], function (err, tids) {
if (err) {
numPages = 1;
} else {
numPages = Math.ceil(tids.length / numTopics);
}
- for(var x=1;x<=numPages;x++) {
+ for(var x = 1;x <= numPages;x++) {
returnData.topics.push(x);
}
@@ -46,49 +45,32 @@ sitemap.render = function(callback) {
});
};
-sitemap.getStaticUrls = function(callback) {
- callback(null, [{
- url: '',
- changefreq: 'weekly',
- priority: '0.6'
- }, {
- url: '/recent',
- changefreq: 'daily',
- priority: '0.4'
- }, {
- url: '/users',
- changefreq: 'daily',
- priority: '0.4'
- }, {
- url: '/groups',
- changefreq: 'daily',
- priority: '0.4'
- }]);
-};
-
-sitemap.getPages = function(callback) {
- if (sitemap.maps.pages && sitemap.maps.pages.cache.length) {
+sitemap.getPages = function (callback) {
+ if (
+ sitemap.maps.pages &&
+ Date.now() < parseInt(sitemap.maps.pages.cacheSetTimestamp, 10) + parseInt(sitemap.maps.pages.cacheResetPeriod, 10)
+ ) {
return sitemap.maps.pages.toXML(callback);
}
var urls = [{
url: '',
changefreq: 'weekly',
- priority: '0.6'
+ priority: 0.6
}, {
url: '/recent',
changefreq: 'daily',
- priority: '0.4'
+ priority: 0.4
}, {
url: '/users',
changefreq: 'daily',
- priority: '0.4'
+ priority: 0.4
}, {
url: '/groups',
changefreq: 'daily',
- priority: '0.4'
+ priority: 0.4
}];
-
+
sitemap.maps.pages = sm.createSitemap({
hostname: nconf.get('url'),
cacheTime: 1000 * 60 * 60 * 24, // Cached for 24 hours
@@ -98,23 +80,26 @@ sitemap.getPages = function(callback) {
sitemap.maps.pages.toXML(callback);
};
-sitemap.getCategories = function(callback) {
- if (sitemap.maps.categories && sitemap.maps.categories.cache.length) {
+sitemap.getCategories = function (callback) {
+ if (
+ sitemap.maps.categories &&
+ Date.now() < parseInt(sitemap.maps.categories.cacheSetTimestamp, 10) + parseInt(sitemap.maps.categories.cacheResetPeriod, 10)
+ ) {
return sitemap.maps.categories.toXML(callback);
}
var categoryUrls = [];
- categories.getCategoriesByPrivilege('categories:cid', 0, 'find', function(err, categoriesData) {
+ categories.getCategoriesByPrivilege('categories:cid', 0, 'find', function (err, categoriesData) {
if (err) {
return callback(err);
}
- categoriesData.forEach(function(category) {
+ categoriesData.forEach(function (category) {
if (category) {
categoryUrls.push({
url: '/category/' + category.slug,
changefreq: 'weekly',
- priority: '0.4'
+ priority: 0.4
});
}
});
@@ -129,7 +114,7 @@ sitemap.getCategories = function(callback) {
});
};
-sitemap.getTopicPage = function(page, callback) {
+sitemap.getTopicPage = function (page, callback) {
if (parseInt(page, 10) <= 0) {
return callback();
}
@@ -138,49 +123,52 @@ sitemap.getTopicPage = function(page, callback) {
var min = (parseInt(page, 10) - 1) * numTopics;
var max = min + numTopics;
- if (sitemap.maps.topics[page-1] && sitemap.maps.topics[page-1].cache.length) {
- return sitemap.maps.topics[page-1].toXML(callback);
+ if (
+ sitemap.maps.topics[page - 1] &&
+ Date.now() < parseInt(sitemap.maps.topics[page - 1].cacheSetTimestamp, 10) + parseInt(sitemap.maps.topics[page - 1].cacheResetPeriod, 10)
+ ) {
+ return sitemap.maps.topics[page - 1].toXML(callback);
}
var topicUrls = [];
async.waterfall([
- function(next) {
+ function (next) {
db.getSortedSetRevRange('topics:recent', min, max, next);
},
- function(tids, next) {
+ function (tids, next) {
privileges.topics.filterTids('read', tids, 0, next);
},
- function(tids, next) {
+ function (tids, next) {
topics.getTopicsFields(tids, ['tid', 'title', 'slug', 'lastposttime'], next);
}
- ], function(err, topics) {
+ ], function (err, topics) {
if (err) {
return callback(err);
}
- topics.forEach(function(topic) {
+ topics.forEach(function (topic) {
if (topic) {
topicUrls.push({
url: '/topic/' + topic.slug,
lastmodISO: utils.toISOString(topic.lastposttime),
changefreq: 'daily',
- priority: '0.6'
+ priority: 0.6
});
}
});
- sitemap.maps.topics[page-1] = sm.createSitemap({
+ sitemap.maps.topics[page - 1] = sm.createSitemap({
hostname: nconf.get('url'),
cacheTime: 1000 * 60 * 60, // Cached for 1 hour
urls: topicUrls
});
- sitemap.maps.topics[page-1].toXML(callback);
+ sitemap.maps.topics[page - 1].toXML(callback);
});
};
-sitemap.clearCache = function() {
+sitemap.clearCache = function () {
if (sitemap.obj) {
sitemap.obj.clearCache();
}
diff --git a/src/social.js b/src/social.js
index fec8fb036c..0c71daadba 100644
--- a/src/social.js
+++ b/src/social.js
@@ -8,7 +8,7 @@ var social = {};
social.postSharing = null;
-social.getPostSharing = function(callback) {
+social.getPostSharing = function (callback) {
if (social.postSharing) {
return callback(null, social.postSharing);
}
@@ -32,16 +32,16 @@ social.getPostSharing = function(callback) {
];
async.waterfall([
- function(next) {
+ function (next) {
plugins.fireHook('filter:social.posts', networks, next);
},
- function(networks, next) {
- db.getSetMembers('social:posts.activated', function(err, activated) {
+ function (networks, next) {
+ db.getSetMembers('social:posts.activated', function (err, activated) {
if (err) {
return next(err);
}
- networks.forEach(function(network, i) {
+ networks.forEach(function (network, i) {
networks[i].activated = (activated.indexOf(network.id) !== -1);
});
@@ -52,19 +52,19 @@ social.getPostSharing = function(callback) {
], callback);
};
-social.getActivePostSharing = function(callback) {
- social.getPostSharing(function(err, networks) {
+social.getActivePostSharing = function (callback) {
+ social.getPostSharing(function (err, networks) {
if (err) {
return callback(err);
}
- networks = networks.filter(function(network) {
+ networks = networks.filter(function (network) {
return network && network.activated;
});
callback(null, networks);
});
};
-social.setActivePostSharingNetworks = function(networkIDs, callback) {
+social.setActivePostSharingNetworks = function (networkIDs, callback) {
async.waterfall([
function (next) {
db.delete('social:posts.activated', next);
diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js
index 6dd22134ba..e59e15d55c 100644
--- a/src/socket.io/admin.js
+++ b/src/socket.io/admin.js
@@ -1,47 +1,46 @@
"use strict";
-var async = require('async'),
- winston = require('winston'),
+var async = require('async');
+var winston = require('winston');
+var nconf = require('nconf');
+var meta = require('../meta');
+var plugins = require('../plugins');
+var widgets = require('../widgets');
+var user = require('../user');
+var logger = require('../logger');
+var events = require('../events');
+var emailer = require('../emailer');
+var db = require('../database');
+var analytics = require('../analytics');
+var index = require('./index');
- meta = require('../meta'),
- plugins = require('../plugins'),
- widgets = require('../widgets'),
- user = require('../user'),
+var SocketAdmin = {
+ user: require('./admin/user'),
+ categories: require('./admin/categories'),
+ groups: require('./admin/groups'),
+ tags: require('./admin/tags'),
+ rewards: require('./admin/rewards'),
+ navigation: require('./admin/navigation'),
+ rooms: require('./admin/rooms'),
+ social: require('./admin/social'),
+ themes: {},
+ plugins: {},
+ widgets: {},
+ config: {},
+ settings: {},
+ email: {},
+ analytics: {},
+ logs: {},
+ errors: {}
+};
- logger = require('../logger'),
- events = require('../events'),
- emailer = require('../emailer'),
- db = require('../database'),
- analytics = require('../analytics'),
- index = require('./index'),
-
-
- SocketAdmin = {
- user: require('./admin/user'),
- categories: require('./admin/categories'),
- groups: require('./admin/groups'),
- tags: require('./admin/tags'),
- rewards: require('./admin/rewards'),
- navigation: require('./admin/navigation'),
- rooms: require('./admin/rooms'),
- social: require('./admin/social'),
- themes: {},
- plugins: {},
- widgets: {},
- config: {},
- settings: {},
- email: {},
- analytics: {},
- logs: {}
- };
-
-SocketAdmin.before = function(socket, method, data, next) {
+SocketAdmin.before = function (socket, method, data, next) {
if (!socket.uid) {
return;
}
- user.isAdministrator(socket.uid, function(err, isAdmin) {
+ user.isAdministrator(socket.uid, function (err, isAdmin) {
if (err || isAdmin) {
return next(err);
}
@@ -50,23 +49,7 @@ SocketAdmin.before = function(socket, method, data, next) {
});
};
-SocketAdmin.reload = function(socket, data, callback) {
- events.log({
- type: 'reload',
- uid: socket.uid,
- ip: socket.ip
- });
- if (process.send) {
- process.send({
- action: 'reload'
- });
- callback();
- } else {
- meta.reload(callback);
- }
-};
-
-SocketAdmin.restart = function(socket, data, callback) {
+SocketAdmin.restart = function (socket, data, callback) {
events.log({
type: 'restart',
uid: socket.uid,
@@ -76,21 +59,29 @@ SocketAdmin.restart = function(socket, data, callback) {
callback();
};
-SocketAdmin.fireEvent = function(socket, data, callback) {
+/**
+ * Reload deprecated as of v1.1.2+, remove in v2.x
+ */
+SocketAdmin.reload = SocketAdmin.restart;
+
+SocketAdmin.fireEvent = function (socket, data, callback) {
index.server.emit(data.name, data.payload || {});
callback();
};
-SocketAdmin.themes.getInstalled = function(socket, data, callback) {
+SocketAdmin.themes.getInstalled = function (socket, data, callback) {
meta.themes.get(callback);
};
-SocketAdmin.themes.set = function(socket, data, callback) {
+SocketAdmin.themes.set = function (socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
- var wrappedCallback = function(err) {
+ var wrappedCallback = function (err) {
+ if (err) {
+ return callback(err);
+ }
meta.themes.set(data, callback);
};
if (data.type === 'bootswatch') {
@@ -100,22 +91,22 @@ SocketAdmin.themes.set = function(socket, data, callback) {
}
};
-SocketAdmin.plugins.toggleActive = function(socket, plugin_id, callback) {
+SocketAdmin.plugins.toggleActive = function (socket, plugin_id, callback) {
require('../posts/cache').reset();
plugins.toggleActive(plugin_id, callback);
};
-SocketAdmin.plugins.toggleInstall = function(socket, data, callback) {
+SocketAdmin.plugins.toggleInstall = function (socket, data, callback) {
require('../posts/cache').reset();
plugins.toggleInstall(data.id, data.version, callback);
};
-SocketAdmin.plugins.getActive = function(socket, data, callback) {
+SocketAdmin.plugins.getActive = function (socket, data, callback) {
plugins.getActive(callback);
};
-SocketAdmin.plugins.orderActivePlugins = function(socket, data, callback) {
- async.each(data, function(plugin, next) {
+SocketAdmin.plugins.orderActivePlugins = function (socket, data, callback) {
+ async.each(data, function (plugin, next) {
if (plugin && plugin.name) {
db.sortedSetAdd('plugins:active', plugin.order || 0, plugin.name, next);
} else {
@@ -124,11 +115,11 @@ SocketAdmin.plugins.orderActivePlugins = function(socket, data, callback) {
}, callback);
};
-SocketAdmin.plugins.upgrade = function(socket, data, callback) {
+SocketAdmin.plugins.upgrade = function (socket, data, callback) {
plugins.upgrade(data.id, data.version, callback);
};
-SocketAdmin.widgets.set = function(socket, data, callback) {
+SocketAdmin.widgets.set = function (socket, data, callback) {
if(!data) {
return callback(new Error('[[error:invalid-data]]'));
}
@@ -136,12 +127,12 @@ SocketAdmin.widgets.set = function(socket, data, callback) {
widgets.setArea(data, callback);
};
-SocketAdmin.config.set = function(socket, data, callback) {
+SocketAdmin.config.set = function (socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
- meta.configs.set(data.key, data.value, function(err) {
+ meta.configs.set(data.key, data.value, function (err) {
if (err) {
return callback(err);
}
@@ -157,12 +148,12 @@ SocketAdmin.config.set = function(socket, data, callback) {
});
};
-SocketAdmin.config.setMultiple = function(socket, data, callback) {
+SocketAdmin.config.setMultiple = function (socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
- meta.configs.setMultiple(data, function(err) {
+ meta.configs.setMultiple(data, function (err) {
if(err) {
return callback(err);
}
@@ -182,33 +173,34 @@ SocketAdmin.config.setMultiple = function(socket, data, callback) {
});
};
-SocketAdmin.config.remove = function(socket, key, callback) {
+SocketAdmin.config.remove = function (socket, key, callback) {
meta.configs.remove(key);
callback();
};
-SocketAdmin.settings.get = function(socket, data, callback) {
+SocketAdmin.settings.get = function (socket, data, callback) {
meta.settings.get(data.hash, callback);
};
-SocketAdmin.settings.set = function(socket, data, callback) {
+SocketAdmin.settings.set = function (socket, data, callback) {
meta.settings.set(data.hash, data.values, callback);
};
-SocketAdmin.settings.clearSitemapCache = function(socket, data, callback) {
+SocketAdmin.settings.clearSitemapCache = function (socket, data, callback) {
require('../sitemap').clearCache();
callback();
};
-SocketAdmin.email.test = function(socket, data, callback) {
+SocketAdmin.email.test = function (socket, data, callback) {
var site_title = meta.config.title || 'NodeBB';
emailer.send(data.template, socket.uid, {
subject: '[' + site_title + '] Test Email',
- site_title: site_title
+ site_title: site_title,
+ url: nconf.get('url')
}, callback);
};
-SocketAdmin.analytics.get = function(socket, data, callback) {
+SocketAdmin.analytics.get = function (socket, data, callback) {
// Default returns views from past 24 hours, by hour
if (data.units === 'days') {
data.amount = 30;
@@ -219,25 +211,25 @@ SocketAdmin.analytics.get = function(socket, data, callback) {
if (data && data.graph && data.units && data.amount) {
if (data.graph === 'traffic') {
async.parallel({
- uniqueVisitors: function(next) {
+ uniqueVisitors: function (next) {
if (data.units === 'days') {
analytics.getDailyStatsForSet('analytics:uniquevisitors', data.until || Date.now(), data.amount, next);
} else {
analytics.getHourlyStatsForSet('analytics:uniquevisitors', data.until || Date.now(), data.amount, next);
}
},
- pageviews: function(next) {
+ pageviews: function (next) {
if (data.units === 'days') {
analytics.getDailyStatsForSet('analytics:pageviews', data.until || Date.now(), data.amount, next);
} else {
analytics.getHourlyStatsForSet('analytics:pageviews', data.until || Date.now(), data.amount, next);
}
},
- monthlyPageViews: function(next) {
+ monthlyPageViews: function (next) {
analytics.getMonthlyPageViews(next);
}
- }, function(err, data) {
- data.pastDay = data.pageviews.reduce(function(a, b) {return parseInt(a, 10) + parseInt(b, 10);});
+ }, function (err, data) {
+ data.pastDay = data.pageviews.reduce(function (a, b) {return parseInt(a, 10) + parseInt(b, 10);});
data.pageviews[data.pageviews.length - 1] = parseInt(data.pageviews[data.pageviews.length - 1], 10) + analytics.getUnwrittenPageviews();
callback(err, data);
});
@@ -247,29 +239,19 @@ SocketAdmin.analytics.get = function(socket, data, callback) {
}
};
-SocketAdmin.logs.get = function(socket, data, callback) {
+SocketAdmin.logs.get = function (socket, data, callback) {
meta.logs.get(callback);
};
-SocketAdmin.logs.clear = function(socket, data, callback) {
+SocketAdmin.logs.clear = function (socket, data, callback) {
meta.logs.clear(callback);
};
-SocketAdmin.getMoreEvents = function(socket, next, callback) {
- var start = parseInt(next, 10);
- if (start < 0) {
- return callback(null, {data: [], next: next});
- }
- var stop = start + 10;
- events.getEvents(start, stop, function(err, events) {
- if (err) {
- return callback(err);
- }
- callback(null, {events: events, next: stop + 1});
- });
+SocketAdmin.errors.clear = function (socket, data, callback) {
+ meta.errors.clear(callback);
};
-SocketAdmin.deleteAllEvents = function(socket, data, callback) {
+SocketAdmin.deleteAllEvents = function (socket, data, callback) {
events.deleteAll(callback);
};
diff --git a/src/socket.io/admin/categories.js b/src/socket.io/admin/categories.js
index 3b35847366..b90ec3e4b6 100644
--- a/src/socket.io/admin/categories.js
+++ b/src/socket.io/admin/categories.js
@@ -9,7 +9,7 @@ var privileges = require('../../privileges');
var plugins = require('../../plugins');
var Categories = {};
-Categories.create = function(socket, data, callback) {
+Categories.create = function (socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
@@ -17,18 +17,18 @@ Categories.create = function(socket, data, callback) {
categories.create(data, callback);
};
-Categories.getAll = function(socket, data, callback) {
+Categories.getAll = function (socket, data, callback) {
async.waterfall([
async.apply(db.getSortedSetRange, 'categories:cid', 0, -1),
async.apply(categories.getCategoriesData),
- function(categories, next) {
+ function (categories, next) {
//Hook changes, there is no req, and res
plugins.fireHook('filter:admin.categories.get', {categories: categories}, next);
},
- function(result, next){
+ function (result, next){
next(null, categories.getTree(result.categories, 0));
}
- ], function(err, categoriesTree) {
+ ], function (err, categoriesTree) {
if (err) {
return callback(err);
}
@@ -37,15 +37,15 @@ Categories.getAll = function(socket, data, callback) {
});
};
-Categories.getNames = function(socket, data, callback) {
+Categories.getNames = function (socket, data, callback) {
categories.getAllCategoryFields(['cid', 'name'], callback);
};
-Categories.purge = function(socket, cid, callback) {
+Categories.purge = function (socket, cid, callback) {
categories.purge(cid, socket.uid, callback);
};
-Categories.update = function(socket, data, callback) {
+Categories.update = function (socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
@@ -53,13 +53,13 @@ Categories.update = function(socket, data, callback) {
categories.update(data, callback);
};
-Categories.setPrivilege = function(socket, data, callback) {
- if(!data) {
+Categories.setPrivilege = function (socket, data, callback) {
+ if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
if (Array.isArray(data.privilege)) {
- async.each(data.privilege, function(privilege, next) {
+ async.each(data.privilege, function (privilege, next) {
groups[data.set ? 'join' : 'leave']('cid:' + data.cid + ':privileges:' + privilege, data.member, next);
}, callback);
} else {
@@ -67,49 +67,40 @@ Categories.setPrivilege = function(socket, data, callback) {
}
};
-Categories.getPrivilegeSettings = function(socket, cid, callback) {
+Categories.getPrivilegeSettings = function (socket, cid, callback) {
privileges.categories.list(cid, callback);
};
-Categories.copyPrivilegesToChildren = function(socket, cid, callback) {
- async.parallel({
- category: function(next) {
- categories.getCategories([cid], socket.uid, next);
- },
- privileges: function(next) {
- privileges.categories.list(cid, next);
- }
- }, function(err, results) {
+Categories.copyPrivilegesToChildren = function (socket, cid, callback) {
+ categories.getCategories([cid], socket.uid, function (err, categories) {
if (err) {
return callback(err);
}
- var category = results.category[0];
+ var category = categories[0];
- async.eachSeries(category.children, function(child, next) {
- copyPrivilegesToChildrenRecursive(child, results.privileges.groups, next);
+ async.eachSeries(category.children, function (child, next) {
+ copyPrivilegesToChildrenRecursive(cid, child, next);
}, callback);
});
};
-function copyPrivilegesToChildrenRecursive(category, privilegeGroups, callback) {
- async.eachSeries(privilegeGroups, function(privGroup, next) {
- var privs = Object.keys(privGroup.privileges);
- async.each(privs, function(privilege, next) {
- var isSet = privGroup.privileges[privilege];
- groups[isSet ? 'join' : 'leave']('cid:' + category.cid + ':privileges:' + privilege, privGroup.name, next);
- }, next);
- }, function(err) {
+function copyPrivilegesToChildrenRecursive(parentCid, category, callback) {
+ categories.copyPrivilegesFrom(parentCid, category.cid, function (err) {
if (err) {
return callback(err);
}
- async.eachSeries(category.children, function(child, next) {
- copyPrivilegesToChildrenRecursive(child, privilegeGroups, next);
+ async.eachSeries(category.children, function (child, next) {
+ copyPrivilegesToChildrenRecursive(parentCid, child, next);
}, callback);
});
}
-Categories.copySettingsFrom = function(socket, data, callback) {
- categories.copySettingsFrom(data.fromCid, data.toCid, callback);
+Categories.copySettingsFrom = function (socket, data, callback) {
+ categories.copySettingsFrom(data.fromCid, data.toCid, true, callback);
+};
+
+Categories.copyPrivilegesFrom = function (socket, data, callback) {
+ categories.copyPrivilegesFrom(data.fromCid, data.toCid, callback);
};
module.exports = Categories;
\ No newline at end of file
diff --git a/src/socket.io/admin/groups.js b/src/socket.io/admin/groups.js
index 206604ec8f..6b9e404ae1 100644
--- a/src/socket.io/admin/groups.js
+++ b/src/socket.io/admin/groups.js
@@ -1,12 +1,15 @@
"use strict";
var async = require('async');
-var groups = require('../../groups'),
- Groups = {};
+var groups = require('../../groups');
-Groups.create = function(socket, data, callback) {
- if(!data) {
+var Groups = {};
+
+Groups.create = function (socket, data, callback) {
+ if (!data) {
return callback(new Error('[[error:invalid-data]]'));
+ } else if (groups.isPrivilegeGroup(data.name)) {
+ return callback(new Error('[[error:invalid-group-name]]'));
}
groups.create({
@@ -16,7 +19,7 @@ Groups.create = function(socket, data, callback) {
}, callback);
};
-Groups.join = function(socket, data, callback) {
+Groups.join = function (socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
@@ -34,7 +37,7 @@ Groups.join = function(socket, data, callback) {
], callback);
};
-Groups.leave = function(socket, data, callback) {
+Groups.leave = function (socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
@@ -56,7 +59,7 @@ Groups.leave = function(socket, data, callback) {
], callback);
};
-Groups.update = function(socket, data, callback) {
+Groups.update = function (socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
diff --git a/src/socket.io/admin/navigation.js b/src/socket.io/admin/navigation.js
index 0f3f7c168e..2f4d9817b4 100644
--- a/src/socket.io/admin/navigation.js
+++ b/src/socket.io/admin/navigation.js
@@ -3,7 +3,7 @@
var navigationAdmin = require('../../navigation/admin'),
SocketNavigation = {};
-SocketNavigation.save = function(socket, data, callback) {
+SocketNavigation.save = function (socket, data, callback) {
navigationAdmin.save(data, callback);
};
diff --git a/src/socket.io/admin/rewards.js b/src/socket.io/admin/rewards.js
index b130a25455..266d5f532f 100644
--- a/src/socket.io/admin/rewards.js
+++ b/src/socket.io/admin/rewards.js
@@ -3,11 +3,11 @@
var rewardsAdmin = require('../../rewards/admin'),
SocketRewards = {};
-SocketRewards.save = function(socket, data, callback) {
+SocketRewards.save = function (socket, data, callback) {
rewardsAdmin.save(data, callback);
};
-SocketRewards.delete = function(socket, data, callback) {
+SocketRewards.delete = function (socket, data, callback) {
rewardsAdmin.delete(data, callback);
};
diff --git a/src/socket.io/admin/rooms.js b/src/socket.io/admin/rooms.js
index 50ba4661de..a4ebfeb44e 100644
--- a/src/socket.io/admin/rooms.js
+++ b/src/socket.io/admin/rooms.js
@@ -16,8 +16,8 @@ var SocketRooms = {
};
-pubsub.on('sync:stats:start', function() {
- getLocalStats(function(err, stats) {
+pubsub.on('sync:stats:start', function () {
+ SocketRooms.getLocalStats(function (err, stats) {
if (err) {
return winston.error(err);
}
@@ -25,11 +25,11 @@ pubsub.on('sync:stats:start', function() {
});
});
-pubsub.on('sync:stats:end', function(data) {
+pubsub.on('sync:stats:end', function (data) {
stats[data.id] = data.stats;
});
-pubsub.on('sync:stats:guests', function() {
+pubsub.on('sync:stats:guests', function () {
var io = require('../index').server;
var roomClients = io.sockets.adapter.rooms;
@@ -37,23 +37,23 @@ pubsub.on('sync:stats:guests', function() {
pubsub.publish('sync:stats:guests:end', guestCount);
});
-SocketRooms.getTotalGuestCount = function(callback) {
+SocketRooms.getTotalGuestCount = function (callback) {
var count = 0;
- pubsub.on('sync:stats:guests:end', function(guestCount) {
+ pubsub.on('sync:stats:guests:end', function (guestCount) {
count += guestCount;
});
pubsub.publish('sync:stats:guests');
- setTimeout(function() {
+ setTimeout(function () {
pubsub.removeAllListeners('sync:stats:guests:end');
callback(null, count);
}, 100);
-}
+};
-SocketRooms.getAll = function(socket, data, callback) {
+SocketRooms.getAll = function (socket, data, callback) {
pubsub.publish('sync:stats:start');
totals.onlineGuestCount = 0;
@@ -79,7 +79,7 @@ SocketRooms.getAll = function(socket, data, callback) {
totals.users.topics += stats[instance].users.topics;
totals.users.category += stats[instance].users.category;
- stats[instance].topics.forEach(function(topic) {
+ stats[instance].topics.forEach(function (topic) {
totals.topics[topic.tid] = totals.topics[topic.tid] || {count: 0, tid: topic.tid};
totals.topics[topic.tid].count += topic.count;
});
@@ -87,27 +87,27 @@ SocketRooms.getAll = function(socket, data, callback) {
}
var topTenTopics = [];
- Object.keys(totals.topics).forEach(function(tid) {
+ Object.keys(totals.topics).forEach(function (tid) {
topTenTopics.push({tid: tid, count: totals.topics[tid].count});
});
- topTenTopics = topTenTopics.sort(function(a, b) {
+ topTenTopics = topTenTopics.sort(function (a, b) {
return b.count - a.count;
}).slice(0, 10);
- var topTenTids = topTenTopics.map(function(topic) {
+ var topTenTids = topTenTopics.map(function (topic) {
return topic.tid;
});
- topics.getTopicsFields(topTenTids, ['title'], function(err, titles) {
+ topics.getTopicsFields(topTenTids, ['title'], function (err, titles) {
if (err) {
return callback(err);
}
totals.topics = {};
- topTenTopics.forEach(function(topic, index) {
+ topTenTopics.forEach(function (topic, index) {
totals.topics[topic.tid] = {
value: topic.count || 0,
- title: validator.escape(titles[index].title)
+ title: validator.escape(String(titles[index].title))
};
});
@@ -115,7 +115,7 @@ SocketRooms.getAll = function(socket, data, callback) {
});
};
-SocketRooms.getOnlineUserCount = function(io) {
+SocketRooms.getOnlineUserCount = function (io) {
if (!io) {
return 0;
}
@@ -129,7 +129,7 @@ SocketRooms.getOnlineUserCount = function(io) {
return count;
};
-function getLocalStats(callback) {
+SocketRooms.getLocalStats = function (callback) {
var io = require('../index').server;
if (!io) {
@@ -144,7 +144,7 @@ function getLocalStats(callback) {
users: {
categories: roomClients.categories ? roomClients.categories.length : 0,
recent: roomClients.recent_topics ? roomClients.recent_topics.length : 0,
- unread: roomClients.unread_topics ? roomClients.unread_topics.length: 0,
+ unread: roomClients.unread_topics ? roomClients.unread_topics.length : 0,
topics: 0,
category: 0
},
@@ -166,13 +166,13 @@ function getLocalStats(callback) {
}
}
- topTenTopics = topTenTopics.sort(function(a, b) {
+ topTenTopics = topTenTopics.sort(function (a, b) {
return b.count - a.count;
}).slice(0, 10);
socketData.topics = topTenTopics;
callback(null, socketData);
-}
+};
module.exports = SocketRooms;
\ No newline at end of file
diff --git a/src/socket.io/admin/social.js b/src/socket.io/admin/social.js
index 68b3241c64..77227ea760 100644
--- a/src/socket.io/admin/social.js
+++ b/src/socket.io/admin/social.js
@@ -3,7 +3,7 @@
var social = require('../../social'),
SocketSocial = {};
-SocketSocial.savePostSharingNetworks = function(socket, data, callback) {
+SocketSocial.savePostSharingNetworks = function (socket, data, callback) {
social.setActivePostSharingNetworks(data, callback);
};
diff --git a/src/socket.io/admin/tags.js b/src/socket.io/admin/tags.js
index ad7ffa7201..c00740d9d5 100644
--- a/src/socket.io/admin/tags.js
+++ b/src/socket.io/admin/tags.js
@@ -1,9 +1,17 @@
"use strict";
-var topics = require('../../topics'),
- Tags = {};
+var topics = require('../../topics');
+var Tags = {};
-Tags.update = function(socket, data, callback) {
+Tags.create = function (socket, data, callback) {
+ if (!data) {
+ return callback(new Error('[[error:invalid-data]]'));
+ }
+
+ topics.createEmptyTag(data.tag, callback);
+};
+
+Tags.update = function (socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
@@ -11,7 +19,7 @@ Tags.update = function(socket, data, callback) {
topics.updateTag(data.tag, data, callback);
};
-Tags.deleteTags = function(socket, data, callback) {
+Tags.deleteTags = function (socket, data, callback) {
topics.deleteTags(data.tags, callback);
};
diff --git a/src/socket.io/admin/user.js b/src/socket.io/admin/user.js
index 2633425dc4..3d0a114695 100644
--- a/src/socket.io/admin/user.js
+++ b/src/socket.io/admin/user.js
@@ -1,7 +1,8 @@
"use strict";
-
var async = require('async');
+var validator = require('validator');
+
var db = require('../../database');
var groups = require('../../groups');
var user = require('../../user');
@@ -10,35 +11,35 @@ var meta = require('../../meta');
var User = {};
-User.makeAdmins = function(socket, uids, callback) {
+User.makeAdmins = function (socket, uids, callback) {
if(!Array.isArray(uids)) {
return callback(new Error('[[error:invalid-data]]'));
}
- user.getUsersFields(uids, ['banned'], function(err, userData) {
+ user.getUsersFields(uids, ['banned'], function (err, userData) {
if (err) {
return callback(err);
}
- for(var i=0; i 20) {
- socket.previousEvents.shift();
- }
-
- if (!eventName.startsWith('admin.') && ratelimit.isFlooding(socket)) {
- winston.warn('[socket.io] Too many emits! Disconnecting uid : ' + socket.uid + '. Events : ' + socket.previousEvents);
- return socket.disconnect();
- }
-
- async.waterfall([
- function (next) {
- validateSession(socket, next);
- },
- function (next) {
- if (Namespaces[namespace].before) {
- Namespaces[namespace].before(socket, eventName, params, next);
- } else {
- next();
- }
- },
- function (next) {
- methodToCall(socket, params, next);
- }
- ], function(err, result) {
- callback(err ? {message: err.message} : null, result);
- });
-}
-
-function requireModules() {
- var modules = ['admin', 'categories', 'groups', 'meta', 'modules',
- 'notifications', 'plugins', 'posts', 'topics', 'user', 'blacklist'
- ];
-
- modules.forEach(function(module) {
- Namespaces[module] = require('./' + module);
- });
-}
-
-function validateSession(socket, callback) {
- var req = socket.request;
- if (!req.signedCookies || !req.signedCookies['express.sid']) {
- return callback(new Error('[[error:invalid-session]]'));
- }
- db.sessionStore.get(req.signedCookies['express.sid'], function(err, sessionData) {
- if (err || !sessionData) {
- return callback(err || new Error('[[error:invalid-session]]'));
- }
-
- callback();
- });
-}
-
-function authorize(socket, callback) {
- var request = socket.request;
-
- if (!request) {
- return callback(new Error('[[error:not-authorized]]'));
- }
-
- async.waterfall([
- function(next) {
- cookieParser(request, {}, next);
- },
- function(next) {
- db.sessionStore.get(request.signedCookies['express.sid'], function(err, sessionData) {
- if (err) {
- return next(err);
- }
- if (sessionData && sessionData.passport && sessionData.passport.user) {
- request.session = sessionData;
- socket.uid = parseInt(sessionData.passport.user, 10);
- } else {
- socket.uid = 0;
- }
- next();
- });
- }
- ], callback);
-}
-
-function addRedisAdapter(io) {
- if (nconf.get('redis')) {
- var redisAdapter = require('socket.io-redis');
- var redis = require('../database/redis');
- var pub = redis.connect({return_buffers: true});
- var sub = redis.connect({return_buffers: true});
-
- io.adapter(redisAdapter({pubClient: pub, subClient: sub}));
- } else if (nconf.get('isCluster') === 'true') {
- winston.warn('[socket.io] Clustering detected, you are advised to configure Redis as a websocket store.');
- }
-}
-
-Sockets.in = function(room) {
- return io.in(room);
-};
-
-Sockets.getUserSocketCount = function(uid) {
- if (!io) {
- return 0;
- }
-
- var room = io.sockets.adapter.rooms['uid_' + uid];
- return room ? room.length : 0;
-};
-
-
-Sockets.reqFromSocket = function(socket) {
- var headers = socket.request.headers;
- var host = headers.host;
- var referer = headers.referer || '';
-
- return {
- ip: headers['x-forwarded-for'] || socket.ip,
- host: host,
- protocol: socket.request.connection.encrypted ? 'https' : 'http',
- secure: !!socket.request.connection.encrypted,
- url: referer,
- path: referer.substr(referer.indexOf(host) + host.length),
- headers: headers
+ Sockets.server = io;
+ };
+
+ function onConnection(socket) {
+ socket.ip = socket.request.headers['x-forwarded-for'] || socket.request.connection.remoteAddress;
+
+ logger.io_one(socket, socket.uid);
+
+ onConnect(socket);
+
+ socket.on('*', function (payload) {
+ onMessage(socket, payload);
+ });
+ }
+
+ function onConnect(socket) {
+ if (socket.uid) {
+ socket.join('uid_' + socket.uid);
+ socket.join('online_users');
+ } else {
+ socket.join('online_guests');
+ }
+
+ io.sockets.sockets[socket.id].emit('checkSession', socket.uid);
+ }
+
+ function onMessage(socket, payload) {
+ if (!payload.data.length) {
+ return winston.warn('[socket.io] Empty payload');
+ }
+
+ var eventName = payload.data[0];
+ var params = payload.data[1];
+ var callback = typeof payload.data[payload.data.length - 1] === 'function' ? payload.data[payload.data.length - 1] : function () {
+ };
+
+ if (!eventName) {
+ return winston.warn('[socket.io] Empty method name');
+ }
+
+ var parts = eventName.toString().split('.');
+ var namespace = parts[0];
+ var methodToCall = parts.reduce(function (prev, cur) {
+ if (prev !== null && prev[cur]) {
+ return prev[cur];
+ } else {
+ return null;
+ }
+ }, Namespaces);
+
+ if (!methodToCall) {
+ if (process.env.NODE_ENV === 'development') {
+ winston.warn('[socket.io] Unrecognized message: ' + eventName);
+ }
+ return;
+ }
+
+ socket.previousEvents = socket.previousEvents || [];
+ socket.previousEvents.push(eventName);
+ if (socket.previousEvents.length > 20) {
+ socket.previousEvents.shift();
+ }
+
+ if (!eventName.startsWith('admin.') && ratelimit.isFlooding(socket)) {
+ winston.warn('[socket.io] Too many emits! Disconnecting uid : ' + socket.uid + '. Events : ' + socket.previousEvents);
+ return socket.disconnect();
+ }
+
+ async.waterfall([
+ function (next) {
+ validateSession(socket, next);
+ },
+ function (next) {
+ if (Namespaces[namespace].before) {
+ Namespaces[namespace].before(socket, eventName, params, next);
+ } else {
+ next();
+ }
+ },
+ function (next) {
+ methodToCall(socket, params, next);
+ }
+ ], function (err, result) {
+ callback(err ? {message: err.message} : null, result);
+ });
+ }
+
+ function requireModules() {
+ var modules = ['admin', 'categories', 'groups', 'meta', 'modules',
+ 'notifications', 'plugins', 'posts', 'topics', 'user', 'blacklist'
+ ];
+
+ modules.forEach(function (module) {
+ Namespaces[module] = require('./' + module);
+ });
+ }
+
+ function validateSession(socket, callback) {
+ var req = socket.request;
+ if (!req.signedCookies || !req.signedCookies[nconf.get('sessionKey')]) {
+ return callback(new Error('[[error:invalid-session]]'));
+ }
+ db.sessionStore.get(req.signedCookies[nconf.get('sessionKey')], function (err, sessionData) {
+ if (err || !sessionData) {
+ return callback(err || new Error('[[error:invalid-session]]'));
+ }
+
+ callback();
+ });
+ }
+
+ function authorize(socket, callback) {
+ var request = socket.request;
+
+ if (!request) {
+ return callback(new Error('[[error:not-authorized]]'));
+ }
+
+ async.waterfall([
+ function (next) {
+ cookieParser(request, {}, next);
+ },
+ function (next) {
+ db.sessionStore.get(request.signedCookies[nconf.get('sessionKey')], function (err, sessionData) {
+ if (err) {
+ return next(err);
+ }
+ if (sessionData && sessionData.passport && sessionData.passport.user) {
+ request.session = sessionData;
+ socket.uid = parseInt(sessionData.passport.user, 10);
+ } else {
+ socket.uid = 0;
+ }
+ next();
+ });
+ }
+ ], callback);
+ }
+
+ function addRedisAdapter(io) {
+ if (nconf.get('redis')) {
+ var redisAdapter = require('socket.io-redis');
+ var redis = require('../database/redis');
+ var pub = redis.connect();
+ var sub = redis.connect({return_buffers: true});
+ io.adapter(redisAdapter({pubClient: pub, subClient: sub}));
+ } else if (nconf.get('isCluster') === 'true') {
+ winston.warn('[socket.io] Clustering detected, you are advised to configure Redis as a websocket store.');
+ }
+ }
+
+ Sockets.in = function (room) {
+ return io.in(room);
+ };
+
+ Sockets.getUserSocketCount = function (uid) {
+ if (!io) {
+ return 0;
+ }
+
+ var room = io.sockets.adapter.rooms['uid_' + uid];
+ return room ? room.length : 0;
};
-};
-module.exports = Sockets;
+ Sockets.reqFromSocket = function (socket, payload, event) {
+ var headers = socket.request.headers;
+ var host = headers.host;
+ var referer = headers.referer || '';
+ var data = ((payload || {}).data || []);
+
+ if (!host) {
+ host = url.parse(referer).host;
+ }
+
+ return {
+ uid: socket.uid,
+ params: data[1],
+ method: event || data[0],
+ body: payload,
+ ip: headers['x-forwarded-for'] || socket.ip,
+ host: host,
+ protocol: socket.request.connection.encrypted ? 'https' : 'http',
+ secure: !!socket.request.connection.encrypted,
+ url: referer,
+ path: referer.substr(referer.indexOf(host) + host.length),
+ headers: headers
+ };
+ };
+
+}(exports));
diff --git a/src/socket.io/meta.js b/src/socket.io/meta.js
index 39603c8d0c..13fe018654 100644
--- a/src/socket.io/meta.js
+++ b/src/socket.io/meta.js
@@ -11,14 +11,14 @@ var meta = require('../meta'),
rooms: {}
};
-SocketMeta.reconnected = function(socket, data, callback) {
+SocketMeta.reconnected = function (socket, data, callback) {
if (socket.uid) {
topics.pushUnreadCount(socket.uid);
user.notifications.pushCount(socket.uid);
}
};
-emitter.on('nodebb:ready', function() {
+emitter.on('nodebb:ready', function () {
websockets.server.emit('event:nodebb.ready', {
'cache-buster': meta.config['cache-buster']
});
@@ -27,7 +27,7 @@ emitter.on('nodebb:ready', function() {
/* Rooms */
-SocketMeta.rooms.enter = function(socket, data, callback) {
+SocketMeta.rooms.enter = function (socket, data, callback) {
if (!socket.uid) {
return callback();
}
@@ -53,7 +53,7 @@ SocketMeta.rooms.enter = function(socket, data, callback) {
callback();
};
-SocketMeta.rooms.leaveCurrent = function(socket, data, callback) {
+SocketMeta.rooms.leaveCurrent = function (socket, data, callback) {
if (!socket.uid || !socket.currentRoom) {
return callback();
}
@@ -68,6 +68,9 @@ function leaveCurrentRoom(socket) {
}
}
-
+SocketMeta.getServerTime = function (socket, data, callback) {
+ // Returns server time in milliseconds
+ callback(null, Date.now());
+};
module.exports = SocketMeta;
diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js
index ebd27730e4..011a68567f 100644
--- a/src/socket.io/modules.js
+++ b/src/socket.io/modules.js
@@ -5,33 +5,21 @@ var validator = require('validator');
var meta = require('../meta');
var notifications = require('../notifications');
+var plugins = require('../plugins');
var Messaging = require('../messaging');
var utils = require('../../public/src/utils');
var server = require('./');
var user = require('../user');
var SocketModules = {
- chats: {},
- sounds: {},
- settings: {}
- };
+ chats: {},
+ sounds: {},
+ settings: {}
+};
/* Chat */
-SocketModules.chats.get = function(socket, data, callback) {
- if(!data || !data.roomId) {
- return callback(new Error('[[error:invalid-data]]'));
- }
-
- Messaging.getMessages({
- uid: socket.uid,
- roomId: data.roomId,
- since: data.since,
- isNew: false
- }, callback);
-};
-
-SocketModules.chats.getRaw = function(socket, data, callback) {
+SocketModules.chats.getRaw = function (socket, data, callback) {
if (!data || !data.hasOwnProperty('mid')) {
return callback(new Error('[[error:invalid-data]]'));
}
@@ -48,20 +36,16 @@ SocketModules.chats.getRaw = function(socket, data, callback) {
], callback);
};
-SocketModules.chats.newRoom = function(socket, data, callback) {
+SocketModules.chats.newRoom = function (socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
- var now = Date.now();
- // Websocket rate limiting
- socket.lastChatMessageTime = socket.lastChatMessageTime || 0;
- if (now - socket.lastChatMessageTime < 200) {
+
+ if (rateLimitExceeded(socket)) {
return callback(new Error('[[error:too-many-messages]]'));
- } else {
- socket.lastChatMessageTime = now;
}
- Messaging.canMessageUser(socket.uid, data.touid, function(err) {
+ Messaging.canMessageUser(socket.uid, data.touid, function (err) {
if (err) {
return callback(err);
}
@@ -70,27 +54,30 @@ SocketModules.chats.newRoom = function(socket, data, callback) {
});
};
-SocketModules.chats.send = function(socket, data, callback) {
- if (!data || !data.roomId) {
+SocketModules.chats.send = function (socket, data, callback) {
+ if (!data || !data.roomId || !socket.uid) {
return callback(new Error('[[error:invalid-data]]'));
}
- var now = Date.now();
-
- // Websocket rate limiting
- socket.lastChatMessageTime = socket.lastChatMessageTime || 0;
- if (now - socket.lastChatMessageTime < 200) {
+ if (rateLimitExceeded(socket)) {
return callback(new Error('[[error:too-many-messages]]'));
- } else {
- socket.lastChatMessageTime = now;
}
async.waterfall([
+ function (next) {
+ plugins.fireHook('filter:messaging.send', {
+ data: data,
+ uid: socket.uid
+ }, function (err, results) {
+ data = results.data;
+ next(err);
+ });
+ },
function (next) {
Messaging.canMessageRoom(socket.uid, data.roomId, next);
},
function (next) {
- Messaging.sendMessage(socket.uid, data.roomId, data.message, now, next);
+ Messaging.sendMessage(socket.uid, data.roomId, data.message, Date.now(), next);
},
function (message, next) {
Messaging.notifyUsersInRoom(socket.uid, data.roomId, message);
@@ -100,7 +87,19 @@ SocketModules.chats.send = function(socket, data, callback) {
], callback);
};
-SocketModules.chats.loadRoom = function(socket, data, callback) {
+function rateLimitExceeded(socket) {
+ var now = Date.now();
+ socket.lastChatMessageTime = socket.lastChatMessageTime || 0;
+ var delay = meta.config.hasOwnProperty('chatMessageDelay') ? parseInt(meta.config.chatMessageDelay, 10) : 200;
+ if (now - socket.lastChatMessageTime < delay) {
+ return true;
+ } else {
+ socket.lastChatMessageTime = now;
+ }
+ return false;
+}
+
+SocketModules.chats.loadRoom = function (socket, data, callback) {
if (!data || !data.roomId) {
return callback(new Error('[[error:invalid-data]]'));
}
@@ -116,11 +115,20 @@ SocketModules.chats.loadRoom = function(socket, data, callback) {
async.parallel({
roomData: async.apply(Messaging.getRoomData, data.roomId),
- users: async.apply(Messaging.getUsersInRoom, data.roomId, 0, -1)
+ users: async.apply(Messaging.getUsersInRoom, data.roomId, 0, -1),
+ messages: async.apply(Messaging.getMessages, {
+ callerUid: socket.uid,
+ uid: data.uid || socket.uid,
+ roomId: data.roomId,
+ isNew: false
+ }),
}, next);
},
function (results, next) {
results.roomData.users = results.users;
+ results.roomData.usernames = Messaging.generateUsernames(results.users, socket.uid);
+ results.roomData.messages = results.messages;
+ results.roomData.groupChat = results.roomData.hasOwnProperty('groupChat') ? results.roomData.groupChat : results.users.length > 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;
@@ -129,10 +137,11 @@ SocketModules.chats.loadRoom = function(socket, data, callback) {
], callback);
};
-SocketModules.chats.addUserToRoom = function(socket, data, callback) {
+SocketModules.chats.addUserToRoom = function (socket, data, callback) {
if (!data || !data.roomId || !data.username) {
return callback(new Error('[[error:invalid-data]]'));
}
+ var uid;
async.waterfall([
function (next) {
Messaging.getUserCountInRoom(data.roomId, next);
@@ -147,19 +156,31 @@ SocketModules.chats.addUserToRoom = function(socket, data, callback) {
function (next) {
user.getUidByUsername(data.username, next);
},
- function (uid, next) {
+ function (_uid, next) {
+ uid = _uid;
if (!uid) {
return next(new Error('[[error:no-user]]'));
}
if (socket.uid === parseInt(uid, 10)) {
return next(new Error('[[error:cant-add-self-to-chat-room]]'));
}
+ async.parallel({
+ settings: async.apply(user.getSettings, uid),
+ isAdmin: async.apply(user.isAdministrator, socket.uid),
+ isFollowing: async.apply(user.isFollowing, uid, socket.uid)
+ }, next);
+ },
+ function (results, next) {
+ if (results.settings.restrictChat && !results.isAdmin && !results.isFollowing) {
+ return next(new Error('[[error:chat-restricted]]'));
+ }
+
Messaging.addUsersToRoom(socket.uid, [uid], data.roomId, next);
}
], callback);
};
-SocketModules.chats.removeUserFromRoom = function(socket, data, callback) {
+SocketModules.chats.removeUserFromRoom = function (socket, data, callback) {
if (!data || !data.roomId) {
return callback(new Error('[[error:invalid-data]]'));
}
@@ -177,7 +198,7 @@ SocketModules.chats.removeUserFromRoom = function(socket, data, callback) {
], callback);
};
-SocketModules.chats.leave = function(socket, roomid, callback) {
+SocketModules.chats.leave = function (socket, roomid, callback) {
if (!socket.uid || !roomid) {
return callback(new Error('[[error:invalid-data]]'));
}
@@ -186,12 +207,12 @@ SocketModules.chats.leave = function(socket, roomid, callback) {
};
-SocketModules.chats.edit = function(socket, data, callback) {
+SocketModules.chats.edit = function (socket, data, callback) {
if (!data || !data.roomId) {
return callback(new Error('[[error:invalid-data]]'));
}
- Messaging.canEdit(data.mid, socket.uid, function(err, allowed) {
+ Messaging.canEdit(data.mid, socket.uid, function (err, allowed) {
if (err || !allowed) {
return callback(err || new Error('[[error:cant-edit-chat-message]]'));
}
@@ -200,12 +221,12 @@ SocketModules.chats.edit = function(socket, data, callback) {
});
};
-SocketModules.chats.delete = function(socket, data, callback) {
+SocketModules.chats.delete = function (socket, data, callback) {
if (!data || !data.roomId || !data.messageId) {
return callback(new Error('[[error:invalid-data]]'));
}
- Messaging.canEdit(data.messageId, socket.uid, function(err, allowed) {
+ Messaging.canEdit(data.messageId, socket.uid, function (err, allowed) {
if (err || !allowed) {
return callback(err || new Error('[[error:cant-delete-chat-message]]'));
}
@@ -214,15 +235,18 @@ SocketModules.chats.delete = function(socket, data, callback) {
});
};
-SocketModules.chats.canMessage = function(socket, roomId, callback) {
+SocketModules.chats.canMessage = function (socket, roomId, callback) {
Messaging.canMessageRoom(socket.uid, roomId, callback);
};
-SocketModules.chats.markRead = function(socket, roomId, callback) {
+SocketModules.chats.markRead = function (socket, roomId, callback) {
+ if (!socket.uid) {
+ return callback(new Error('[[error:invalid-data]]'));
+ }
async.parallel({
usersInRoom: async.apply(Messaging.getUidsInRoom, roomId, 0, -1),
markRead: async.apply(Messaging.markRead, socket.uid, roomId)
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return callback(err);
}
@@ -230,13 +254,13 @@ SocketModules.chats.markRead = function(socket, roomId, callback) {
Messaging.pushUnreadCount(socket.uid);
// Mark notification read
- var nids = results.usersInRoom.filter(function(uid) {
+ var nids = results.usersInRoom.filter(function (uid) {
return parseInt(uid, 10) !== socket.uid;
- }).map(function(uid) {
+ }).map(function (uid) {
return 'chat_' + uid + '_' + roomId;
});
- notifications.markReadMultiple(nids, socket.uid, function() {
+ notifications.markReadMultiple(nids, socket.uid, function () {
user.notifications.pushCount(socket.uid);
});
@@ -245,7 +269,7 @@ SocketModules.chats.markRead = function(socket, roomId, callback) {
});
};
-SocketModules.chats.markAllRead = function(socket, data, callback) {
+SocketModules.chats.markAllRead = function (socket, data, callback) {
async.waterfall([
function (next) {
Messaging.markAllRead(socket.uid, next);
@@ -257,7 +281,7 @@ SocketModules.chats.markAllRead = function(socket, data, callback) {
], callback);
};
-SocketModules.chats.renameRoom = function(socket, data, callback) {
+SocketModules.chats.renameRoom = function (socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-name]]'));
}
@@ -270,8 +294,8 @@ 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)};
- uids.forEach(function(uid) {
+ var eventData = {roomId: data.roomId, newName: validator.escape(String(data.newName))};
+ uids.forEach(function (uid) {
server.in('uid_' + uid).emit('event:chats.roomRename', eventData);
});
next();
@@ -279,36 +303,56 @@ SocketModules.chats.renameRoom = function(socket, data, callback) {
], callback);
};
-SocketModules.chats.getRecentChats = function(socket, data, callback) {
- if (!data || !utils.isNumber(data.after)) {
+SocketModules.chats.getRecentChats = function (socket, data, callback) {
+ if (!data || !utils.isNumber(data.after) || !utils.isNumber(data.uid)) {
return callback(new Error('[[error:invalid-data]]'));
}
- var start = parseInt(data.after, 10),
- stop = start + 9;
-
- Messaging.getRecentChats(socket.uid, start, stop, callback);
+ var start = parseInt(data.after, 10);
+ var stop = start + 9;
+ Messaging.getRecentChats(socket.uid, data.uid, start, stop, callback);
};
-SocketModules.chats.hasPrivateChat = function(socket, uid, callback) {
+SocketModules.chats.hasPrivateChat = function (socket, uid, callback) {
if (!socket.uid || !uid) {
return callback(null, new Error('[[error:invalid-data]]'));
}
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 = {
+ callerUid: socket.uid,
+ uid: data.uid,
+ roomId: data.roomId,
+ start: parseInt(data.start, 10) || 0,
+ count: 50,
+ markRead: false
+ };
+
+ if (data.hasOwnProperty('markRead')) {
+ params.markRead = data.markRead;
+ }
+
+ Messaging.getMessages(params, callback);
+};
+
/* Sounds */
-SocketModules.sounds.getSounds = function(socket, data, callback) {
+SocketModules.sounds.getSounds = function (socket, data, callback) {
// Read sounds from local directory
meta.sounds.getFiles(callback);
};
-SocketModules.sounds.getMapping = function(socket, data, callback) {
- meta.sounds.getMapping(callback);
+SocketModules.sounds.getMapping = function (socket, data, callback) {
+ meta.sounds.getMapping(socket.uid, callback);
};
-SocketModules.sounds.getData = function(socket, data, 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/notifications.js b/src/socket.io/notifications.js
index 121ede2a96..ea32cd7394 100644
--- a/src/socket.io/notifications.js
+++ b/src/socket.io/notifications.js
@@ -7,7 +7,7 @@ var utils = require('../../public/src/utils');
var SocketNotifs = {};
-SocketNotifs.get = function(socket, data, callback) {
+SocketNotifs.get = function (socket, data, callback) {
if (data && Array.isArray(data.nids) && socket.uid) {
user.notifications.getNotifications(data.nids, socket.uid, callback);
} else {
@@ -15,7 +15,7 @@ SocketNotifs.get = function(socket, data, callback) {
}
};
-SocketNotifs.loadMore = function(socket, data, callback) {
+SocketNotifs.loadMore = function (socket, data, callback) {
if (!data || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0) {
return callback(new Error('[[error:invalid-data]]'));
}
@@ -24,7 +24,7 @@ SocketNotifs.loadMore = function(socket, data, callback) {
}
var start = parseInt(data.after, 10);
var stop = start + 20;
- user.notifications.getAll(socket.uid, start, stop, function(err, notifications) {
+ user.notifications.getAll(socket.uid, start, stop, function (err, notifications) {
if (err) {
return callback(err);
}
@@ -32,11 +32,11 @@ SocketNotifs.loadMore = function(socket, data, callback) {
});
};
-SocketNotifs.getCount = function(socket, data, callback) {
+SocketNotifs.getCount = function (socket, data, callback) {
user.notifications.getUnreadCount(socket.uid, callback);
};
-SocketNotifs.deleteAll = function(socket, data, callback) {
+SocketNotifs.deleteAll = function (socket, data, callback) {
if (!socket.uid) {
return callback(new Error('[[error:no-privileges]]'));
}
@@ -44,40 +44,16 @@ SocketNotifs.deleteAll = function(socket, data, callback) {
user.notifications.deleteAll(socket.uid, callback);
};
-SocketNotifs.markRead = function(socket, nid, callback) {
+SocketNotifs.markRead = function (socket, nid, callback) {
notifications.markRead(nid, socket.uid, callback);
};
-SocketNotifs.markUnread = function(socket, nid, callback) {
+SocketNotifs.markUnread = function (socket, nid, callback) {
notifications.markUnread(nid, socket.uid, callback);
};
-SocketNotifs.markAllRead = function(socket, data, callback) {
+SocketNotifs.markAllRead = function (socket, data, callback) {
notifications.markAllRead(socket.uid, callback);
};
-SocketNotifs.generatePath = function(socket, nid, callback) {
- if (!socket.uid) {
- return callback(new Error('[[error:no-privileges]]'));;
- }
- async.waterfall([
- function (next) {
- notifications.get(nid, next);
- },
- function (notification, next) {
- if (!notification) {
- return next(null, '');
- }
- user.notifications.generateNotificationPaths([notification], socket.uid, next);
- },
- function (notificationsData, next) {
- if (notificationsData && notificationsData.length) {
- next(null, notificationsData[0].path);
- } else {
- next();
- }
- }
- ], callback);
-};
-
module.exports = SocketNotifs;
diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js
index 5e31e0c611..379f2315ae 100644
--- a/src/socket.io/posts.js
+++ b/src/socket.io/posts.js
@@ -8,7 +8,6 @@ var meta = require('../meta');
var topics = require('../topics');
var user = require('../user');
var websockets = require('./index');
-var socketTopics = require('./topics');
var socketHelpers = require('./helpers');
var utils = require('../../public/src/utils');
@@ -16,31 +15,29 @@ var apiController = require('../controllers/api');
var SocketPosts = {};
-
require('./posts/edit')(SocketPosts);
require('./posts/move')(SocketPosts);
-require('./posts/favourites')(SocketPosts);
+require('./posts/votes')(SocketPosts);
+require('./posts/bookmarks')(SocketPosts);
require('./posts/tools')(SocketPosts);
require('./posts/flag')(SocketPosts);
-SocketPosts.reply = function(socket, data, callback) {
+SocketPosts.reply = function (socket, data, callback) {
if (!data || !data.tid || !data.content) {
return callback(new Error('[[error:invalid-data]]'));
}
data.uid = socket.uid;
data.req = websockets.reqFromSocket(socket);
+ data.timestamp = Date.now();
- topics.reply(data, function(err, postData) {
+ topics.reply(data, function (err, postData) {
if (err) {
return callback(err);
}
var result = {
posts: [postData],
- privileges: {
- 'topics:reply': true
- },
'reputation:disabled': parseInt(meta.config['reputation:disabled'], 10) === 1,
'downvote:disabled': parseInt(meta.config['downvote:disabled'], 10) === 1,
};
@@ -52,25 +49,21 @@ SocketPosts.reply = function(socket, data, callback) {
user.updateOnlineUsers(socket.uid);
socketHelpers.notifyNew(socket.uid, 'newPost', result);
-
- if (data.lock) {
- socketTopics.doTopicAction('lock', 'event:topic_locked', socket, {tids: [postData.topic.tid], cid: postData.topic.cid});
- }
});
};
-SocketPosts.getRawPost = function(socket, pid, callback) {
+SocketPosts.getRawPost = function (socket, pid, callback) {
async.waterfall([
- function(next) {
+ function (next) {
privileges.posts.can('read', pid, socket.uid, next);
},
- function(canRead, next) {
+ function (canRead, next) {
if (!canRead) {
return next(new Error('[[error:no-privileges]]'));
}
posts.getPostFields(pid, ['content', 'deleted'], next);
},
- function(postData, next) {
+ function (postData, next) {
if (parseInt(postData.deleted, 10) === 1) {
return next(new Error('[[error:no-post]]'));
}
@@ -79,37 +72,27 @@ SocketPosts.getRawPost = function(socket, pid, callback) {
], callback);
};
-SocketPosts.getPost = function(socket, pid, callback) {
- async.waterfall([
- function(next) {
- apiController.getObjectByType(socket.uid, 'post', pid, next);
- },
- function(postData, next) {
- if (parseInt(postData.deleted, 10) === 1) {
- return next(new Error('[[error:no-post]]'));
- }
- next(null, postData);
- }
- ], callback);
+SocketPosts.getPost = function (socket, pid, callback) {
+ apiController.getPostData(pid, socket.uid, callback);
};
-SocketPosts.loadMoreFavourites = function(socket, data, callback) {
- loadMorePosts('uid:' + data.uid + ':favourites', socket.uid, data, callback);
+SocketPosts.loadMoreBookmarks = function (socket, data, callback) {
+ loadMorePosts('uid:' + data.uid + ':bookmarks', socket.uid, data, callback);
};
-SocketPosts.loadMoreUserPosts = function(socket, data, callback) {
+SocketPosts.loadMoreUserPosts = function (socket, data, callback) {
loadMorePosts('uid:' + data.uid + ':posts', socket.uid, data, callback);
};
-SocketPosts.loadMoreBestPosts = function(socket, data, callback) {
+SocketPosts.loadMoreBestPosts = function (socket, data, callback) {
loadMorePosts('uid:' + data.uid + ':posts:votes', socket.uid, data, callback);
};
-SocketPosts.loadMoreUpVotedPosts = function(socket, data, callback) {
+SocketPosts.loadMoreUpVotedPosts = function (socket, data, callback) {
loadMorePosts('uid:' + data.uid + ':upvote', socket.uid, data, callback);
};
-SocketPosts.loadMoreDownVotedPosts = function(socket, data, callback) {
+SocketPosts.loadMoreDownVotedPosts = function (socket, data, callback) {
loadMorePosts('uid:' + data.uid + ':downvote', socket.uid, data, callback);
};
@@ -118,17 +101,17 @@ function loadMorePosts(set, uid, data, callback) {
return callback(new Error('[[error:invalid-data]]'));
}
- var start = Math.max(0, parseInt(data.after, 10)),
- stop = start + 9;
+ var start = Math.max(0, parseInt(data.after, 10));
+ var stop = start + 9;
posts.getPostSummariesFromSet(set, uid, start, stop, callback);
}
-SocketPosts.getCategory = function(socket, pid, callback) {
+SocketPosts.getCategory = function (socket, pid, callback) {
posts.getCidByPid(pid, callback);
};
-SocketPosts.getPidIndex = function(socket, data, callback) {
+SocketPosts.getPidIndex = function (socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
diff --git a/src/socket.io/posts/bookmarks.js b/src/socket.io/posts/bookmarks.js
new file mode 100644
index 0000000000..d0bb84256c
--- /dev/null
+++ b/src/socket.io/posts/bookmarks.js
@@ -0,0 +1,16 @@
+'use strict';
+
+
+var helpers = require('./helpers');
+
+module.exports = function (SocketPosts) {
+
+ SocketPosts.bookmark = function (socket, data, callback) {
+ helpers.postCommand(socket, 'bookmark', 'bookmarked', '', data, callback);
+ };
+
+ SocketPosts.unbookmark = function (socket, data, callback) {
+ helpers.postCommand(socket, 'unbookmark', 'bookmarked', '', data, callback);
+ };
+
+};
\ No newline at end of file
diff --git a/src/socket.io/posts/edit.js b/src/socket.io/posts/edit.js
index e5ad7a0afb..63333eac29 100644
--- a/src/socket.io/posts/edit.js
+++ b/src/socket.io/posts/edit.js
@@ -2,6 +2,7 @@
var async = require('async');
var winston = require('winston');
+var validator = require('validator');
var posts = require('../../posts');
var groups = require('../../groups');
@@ -9,9 +10,9 @@ var events = require('../../events');
var meta = require('../../meta');
var websockets = require('../index');
-module.exports = function(SocketPosts) {
+module.exports = function (SocketPosts) {
- SocketPosts.edit = function(socket, data, callback) {
+ SocketPosts.edit = function (socket, data, callback) {
if (!socket.uid) {
return callback(new Error('[[error:not-logged-in]]'));
} else if (!data || !data.pid || !data.content) {
@@ -30,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);
}
@@ -49,8 +43,8 @@ module.exports = function(SocketPosts) {
type: 'topic-rename',
uid: socket.uid,
ip: socket.ip,
- oldTitle: result.topic.oldTitle,
- newTitle: result.topic.title
+ oldTitle: validator.escape(String(result.topic.oldTitle)),
+ newTitle: validator.escape(String(result.topic.title))
});
}
@@ -65,16 +59,16 @@ module.exports = function(SocketPosts) {
async.parallel({
admins: async.apply(groups.getMembers, 'administrators', 0, -1),
moderators: async.apply(groups.getMembers, 'cid:' + result.topic.cid + ':privileges:mods', 0, -1)
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return winston.error(err);
}
- var uids = results.admins.concat(results.moderators).filter(function(uid, index, array) {
+ var uids = results.admins.concat(results.moderators).filter(function (uid, index, array) {
return uid && array.indexOf(uid) === index;
});
- uids.forEach(function(uid) {
+ uids.forEach(function (uid) {
websockets.in('uid_' + uid).emit('event:post_edited', result);
});
});
diff --git a/src/socket.io/posts/favourites.js b/src/socket.io/posts/favourites.js
deleted file mode 100644
index 6883ece06a..0000000000
--- a/src/socket.io/posts/favourites.js
+++ /dev/null
@@ -1,160 +0,0 @@
-'use strict';
-
-var async = require('async');
-
-var db = require('../../database');
-var user = require('../../user');
-var posts = require('../../posts');
-var favourites = require('../../favourites');
-var plugins = require('../../plugins');
-var websockets = require('../index');
-var privileges = require('../../privileges');
-var socketHelpers = require('../helpers');
-
-module.exports = function(SocketPosts) {
- SocketPosts.getVoters = function(socket, data, callback) {
- if (!data || !data.pid || !data.cid) {
- return callback(new Error('[[error:invalid-data]]'));
- }
-
- async.waterfall([
- function (next) {
- privileges.categories.isAdminOrMod(data.cid, socket.uid, next);
- },
- function (isAdminOrMod, next) {
- if (!isAdminOrMod) {
- return next(new Error('[[error:no-privileges]]'));
- }
-
- async.parallel({
- upvoteUids: function(next) {
- db.getSetMembers('pid:' + data.pid + ':upvote', next);
- },
- downvoteUids: function(next) {
- db.getSetMembers('pid:' + data.pid + ':downvote', next);
- }
- }, next);
- },
- function (results, next) {
- async.parallel({
- upvoters: function(next) {
- user.getUsersFields(results.upvoteUids, ['username', 'userslug', 'picture'], next);
- },
- upvoteCount: function(next) {
- next(null, results.upvoteUids.length);
- },
- downvoters: function(next) {
- user.getUsersFields(results.downvoteUids, ['username', 'userslug', 'picture'], next);
- },
- downvoteCount: function(next) {
- next(null, results.downvoteUids.length);
- }
- }, next);
- }
- ], callback);
- };
-
- SocketPosts.getUpvoters = function(socket, pids, callback) {
- if (!Array.isArray(pids)) {
- return callback(new Error('[[error:invalid-data]]'));
- }
- favourites.getUpvotedUidsByPids(pids, function(err, data) {
- if (err || !Array.isArray(data) || !data.length) {
- return callback(err, []);
- }
-
- async.map(data, function(uids, next) {
- var otherCount = 0;
- if (uids.length > 6) {
- otherCount = uids.length - 5;
- uids = uids.slice(0, 5);
- }
- user.getUsernamesByUids(uids, function(err, usernames) {
- next(err, {
- otherCount: otherCount,
- usernames: usernames
- });
- });
- }, callback);
- });
- };
-
- SocketPosts.upvote = function(socket, data, callback) {
- favouriteCommand(socket, 'upvote', 'voted', 'notifications:upvoted_your_post_in', data, callback);
- };
-
- SocketPosts.downvote = function(socket, data, callback) {
- favouriteCommand(socket, 'downvote', 'voted', '', data, callback);
- };
-
- SocketPosts.unvote = function(socket, data, callback) {
- favouriteCommand(socket, 'unvote', 'voted', '', data, callback);
- };
-
- SocketPosts.favourite = function(socket, data, callback) {
- favouriteCommand(socket, 'favourite', 'favourited', 'notifications:favourited_your_post_in', data, callback);
- };
-
- SocketPosts.unfavourite = function(socket, data, callback) {
- favouriteCommand(socket, 'unfavourite', 'favourited', '', data, callback);
- };
-
- function favouriteCommand(socket, command, eventName, notification, data, callback) {
- if (!socket.uid) {
- return callback(new Error('[[error:not-logged-in]]'))
- }
- if(!data || !data.pid || !data.room_id) {
- return callback(new Error('[[error:invalid-data]]'));
- }
- async.parallel({
- exists: function(next) {
- posts.exists(data.pid, next);
- },
- deleted: function(next) {
- posts.getPostField(data.pid, 'deleted', next);
- }
- }, function(err, results) {
- if (err || !results.exists) {
- return callback(err || new Error('[[error:invalid-pid]]'));
- }
-
- if (parseInt(results.deleted, 10) === 1) {
- return callback(new Error('[[error:post-deleted]]'));
- }
-
- /*
- hooks:
- filter.post.upvote
- filter.post.downvote
- filter.post.unvote
- filter.post.favourite
- filter.post.unfavourite
- */
- plugins.fireHook('filter:post.' + command, {data: data, uid: socket.uid}, function(err, filteredData) {
- if (err) {
- return callback(err);
- }
-
- executeFavouriteCommand(socket, command, eventName, notification, filteredData.data, callback);
- });
- });
- }
-
- function executeFavouriteCommand(socket, command, eventName, notification, data, callback) {
- favourites[command](data.pid, socket.uid, function(err, result) {
- if (err) {
- return callback(err);
- }
-
- if (result && eventName) {
- socket.emit('posts.' + command, result);
- websockets.in(data.room_id).emit('event:' + eventName, result);
- }
-
- if (result && notification) {
- socketHelpers.sendNotificationToPostOwner(data.pid, socket.uid, notification);
- }
- callback();
- });
- }
-};
\ No newline at end of file
diff --git a/src/socket.io/posts/flag.js b/src/socket.io/posts/flag.js
index bbf7a4721c..e34808990a 100644
--- a/src/socket.io/posts/flag.js
+++ b/src/socket.io/posts/flag.js
@@ -12,9 +12,9 @@ var notifications = require('../../notifications');
var plugins = require('../../plugins');
var meta = require('../../meta');
-module.exports = function(SocketPosts) {
+module.exports = function (SocketPosts) {
- SocketPosts.flag = function(socket, data, callback) {
+ SocketPosts.flag = function (socket, data, callback) {
if (!socket.uid) {
return callback(new Error('[[error:not-logged-in]]'));
}
@@ -23,8 +23,8 @@ module.exports = function(SocketPosts) {
return callback(new Error('[[error:invalid-data]]'));
}
- var flaggingUser = {},
- post;
+ var flaggingUser = {};
+ var post;
async.waterfall([
function (next) {
@@ -40,14 +40,12 @@ module.exports = function(SocketPosts) {
},
function (topicData, next) {
post.topic = topicData;
- next();
- },
- function (next) {
+
async.parallel({
- isAdminOrMod: function(next) {
+ isAdminOrMod: function (next) {
privileges.categories.isAdminOrMod(post.topic.cid, socket.uid, next);
},
- userData: function(next) {
+ userData: function (next) {
user.getUserFields(socket.uid, ['username', 'reputation', 'banned'], next);
}
}, next);
@@ -68,16 +66,16 @@ module.exports = function(SocketPosts) {
},
function (next) {
async.parallel({
- post: function(next) {
+ post: function (next) {
posts.parsePost(post, next);
},
- admins: function(next) {
+ admins: function (next) {
groups.getMembers('administrators', 0, -1, next);
},
globalMods: function (next) {
groups.getMembers('Global Moderators', 0, -1, next);
},
- moderators: function(next) {
+ moderators: function (next) {
groups.getMembers('cid:' + post.topic.cid + ':privileges:mods', 0, -1, next);
}
}, next);
@@ -90,23 +88,24 @@ module.exports = function(SocketPosts) {
bodyShort: '[[notifications:user_flagged_post_in, ' + flaggingUser.username + ', ' + titleEscaped + ']]',
bodyLong: post.content,
pid: data.pid,
+ path: '/post/' + data.pid,
nid: 'post_flag:' + data.pid + ':uid:' + socket.uid,
from: socket.uid,
mergeId: 'notifications:user_flagged_post_in|' + data.pid,
topicTitle: post.topic.title
- }, function(err, notification) {
+ }, function (err, notification) {
if (err || !notification) {
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);
});
}
], callback);
};
- SocketPosts.dismissFlag = function(socket, pid, callback) {
+ SocketPosts.dismissFlag = function (socket, pid, callback) {
if (!pid || !socket.uid) {
return callback('[[error:invalid-data]]');
}
@@ -123,7 +122,7 @@ module.exports = function(SocketPosts) {
], callback);
};
- SocketPosts.dismissAllFlags = function(socket, data, callback) {
+ SocketPosts.dismissAllFlags = function (socket, data, callback) {
async.waterfall([
function (next) {
user.isAdminOrGlobalMod(socket.uid, next);
@@ -137,34 +136,36 @@ 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) {
- user.isAdminOrGlobalMod(socket.uid, next);
+ async.parallel([
+ async.apply(user.isAdminOrGlobalMod, socket.uid),
+ async.apply(user.isModeratorOfAnyCategory, socket.uid)
+ ], function (err, results) {
+ next(err, results[0] || results[1]);
+ });
},
- function (isAdminOrGlobalModerator, next) {
- if (!isAdminOrGlobalModerator) {
+ function (allowed, next) {
+ if (!allowed) {
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/helpers.js b/src/socket.io/posts/helpers.js
new file mode 100644
index 0000000000..657ab0f64a
--- /dev/null
+++ b/src/socket.io/posts/helpers.js
@@ -0,0 +1,71 @@
+'use strict';
+
+
+var async = require('async');
+var posts = require('../../posts');
+var plugins = require('../../plugins');
+var websockets = require('../index');
+var socketHelpers = require('../helpers');
+
+var helpers = module.exports;
+
+helpers.postCommand = function (socket, command, eventName, notification, data, callback) {
+ if (!socket.uid) {
+ return callback(new Error('[[error:not-logged-in]]'));
+ }
+ if (!data || !data.pid || !data.room_id) {
+ return callback(new Error('[[error:invalid-data]]'));
+ }
+ async.parallel({
+ exists: function (next) {
+ posts.exists(data.pid, next);
+ },
+ deleted: function (next) {
+ posts.getPostField(data.pid, 'deleted', next);
+ }
+ }, function (err, results) {
+ if (err || !results.exists) {
+ return callback(err || new Error('[[error:invalid-pid]]'));
+ }
+
+ if (parseInt(results.deleted, 10) === 1) {
+ return callback(new Error('[[error:post-deleted]]'));
+ }
+
+ /*
+ hooks:
+ filter:post.upvote
+ filter:post.downvote
+ filter:post.unvote
+ filter:post.bookmark
+ filter:post.unbookmark
+ */
+ plugins.fireHook('filter:post.' + command, {data: data, uid: socket.uid}, function (err, filteredData) {
+ if (err) {
+ return callback(err);
+ }
+
+ executeCommand(socket, command, eventName, notification, filteredData.data, callback);
+ });
+ });
+};
+
+function executeCommand(socket, command, eventName, notification, data, callback) {
+ posts[command](data.pid, socket.uid, function (err, result) {
+ if (err) {
+ return callback(err);
+ }
+
+ if (result && eventName) {
+ socket.emit('posts.' + command, result);
+ websockets.in(data.room_id).emit('event:' + eventName, result);
+ }
+
+ if (result && notification) {
+ socketHelpers.sendNotificationToPostOwner(data.pid, socket.uid, command, notification);
+ } else if (result && command === 'unvote') {
+ socketHelpers.rescindUpvoteNotification(data.pid, socket.uid);
+ }
+ callback();
+ });
+}
\ No newline at end of file
diff --git a/src/socket.io/posts/move.js b/src/socket.io/posts/move.js
index 637ef7da1f..dc830566eb 100644
--- a/src/socket.io/posts/move.js
+++ b/src/socket.io/posts/move.js
@@ -5,9 +5,9 @@ var privileges = require('../../privileges');
var topics = require('../../topics');
var socketHelpers = require('../helpers');
-module.exports = function(SocketPosts) {
+module.exports = function (SocketPosts) {
- SocketPosts.movePost = function(socket, data, callback) {
+ SocketPosts.movePost = function (socket, data, callback) {
if (!socket.uid) {
return callback(new Error('[[error:not-logged-in]]'));
}
@@ -28,7 +28,7 @@ module.exports = function(SocketPosts) {
topics.movePostToTopic(data.pid, data.tid, next);
},
function (next) {
- socketHelpers.sendNotificationToPostOwner(data.pid, socket.uid, 'notifications:moved_your_post');
+ socketHelpers.sendNotificationToPostOwner(data.pid, socket.uid, 'move', 'notifications:moved_your_post');
next();
}
], callback);
diff --git a/src/socket.io/posts/tools.js b/src/socket.io/posts/tools.js
index 5c59b28918..ae04ed6c6d 100644
--- a/src/socket.io/posts/tools.js
+++ b/src/socket.io/posts/tools.js
@@ -1,93 +1,113 @@
'use strict';
var async = require('async');
+var winston = require('winston');
+var validator = require('validator');
var posts = require('../../posts');
+var topics = require('../../topics');
var events = require('../../events');
var websockets = require('../index');
var socketTopics = require('../topics');
var privileges = require('../../privileges');
var plugins = require('../../plugins');
var social = require('../../social');
-var favourites = require('../../favourites');
-module.exports = function(SocketPosts) {
+module.exports = function (SocketPosts) {
- SocketPosts.loadPostTools = function(socket, data, callback) {
+ SocketPosts.loadPostTools = function (socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
async.parallel({
- posts: function(next) {
- posts.getPostFields(data.pid, ['deleted', 'reputation', 'uid'], next);
+ posts: function (next) {
+ posts.getPostFields(data.pid, ['deleted', 'bookmarks', 'uid'], next);
},
- isAdminOrMod: function(next) {
+ isAdminOrMod: function (next) {
privileges.categories.isAdminOrMod(data.cid, socket.uid, next);
},
- favourited: function(next) {
- favourites.getFavouritesByPostIDs([data.pid], socket.uid, next);
+ canEdit: function (next) {
+ privileges.posts.canEdit(data.pid, socket.uid, next);
},
- tools: function(next) {
+ canDelete: function (next) {
+ privileges.posts.canDelete(data.pid, socket.uid, next);
+ },
+ bookmarked: function (next) {
+ posts.hasBookmarked(data.pid, socket.uid, next);
+ },
+ tools: function (next) {
plugins.fireHook('filter:post.tools', {pid: data.pid, uid: socket.uid, tools: []}, next);
},
- postSharing: function(next) {
+ postSharing: function (next) {
social.getActivePostSharing(next);
}
- }, function(err, results) {
+ }, function (err, results) {
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.bookmarked = results.bookmarked;
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);
});
};
- SocketPosts.delete = function(socket, data, callback) {
- doPostAction('delete', 'event:post_deleted', socket, data, callback);
- };
-
- SocketPosts.restore = function(socket, data, callback) {
- doPostAction('restore', 'event:post_restored', socket, data, callback);
- };
-
- SocketPosts.deletePosts = function(socket, data, callback) {
- if (!data || !Array.isArray(data.pids)) {
+ SocketPosts.delete = function (socket, data, callback) {
+ if (!data || !data.pid) {
return callback(new Error('[[error:invalid-data]]'));
}
- async.each(data.pids, function(pid, next) {
- SocketPosts.delete(socket, {pid: pid, tid: data.tid}, next);
- }, callback);
+ var postData;
+ async.waterfall([
+ function (next) {
+ posts.tools.delete(socket.uid, data.pid, next);
+ },
+ function (_postData, next) {
+ postData = _postData;
+ isMainAndLastPost(data.pid, next);
+ },
+ function (results, next) {
+ if (results.isMain && results.isLast) {
+ deleteTopicOf(data.pid, socket, next);
+ } else {
+ next();
+ }
+ },
+ function (next) {
+ websockets.in('topic_' + data.tid).emit('event:post_deleted', postData);
+
+ events.log({
+ type: 'post-delete',
+ uid: socket.uid,
+ pid: data.pid,
+ ip: socket.ip
+ });
+
+ next();
+ }
+ ], callback);
};
- SocketPosts.purgePosts = function(socket, data, callback) {
- if (!data || !Array.isArray(data.pids)) {
- return callback(new Error('[[error:invalid-data]]'));
- }
- async.each(data.pids, function(pid, next) {
- SocketPosts.purge(socket, {pid: pid, tid: data.tid}, next);
- }, callback);
- };
-
- function doPostAction(command, eventName, socket, data, callback) {
- if (!data) {
+ SocketPosts.restore = function (socket, data, callback) {
+ if (!data || !data.pid) {
return callback(new Error('[[error:invalid-data]]'));
}
- posts.tools[command](socket.uid, data.pid, function(err, postData) {
+ posts.tools.restore(socket.uid, data.pid, function (err, postData) {
if (err) {
return callback(err);
}
- websockets.in('topic_' + data.tid).emit(eventName, postData);
+ websockets.in('topic_' + data.tid).emit('event:post_restored', postData);
events.log({
- type: 'post-' + command,
+ type: 'post-restore',
uid: socket.uid,
pid: data.pid,
ip: socket.ip
@@ -95,22 +115,46 @@ module.exports = function(SocketPosts) {
callback();
});
- }
+ };
- SocketPosts.purge = function(socket, data, callback) {
+ SocketPosts.deletePosts = function (socket, data, callback) {
+ if (!data || !Array.isArray(data.pids)) {
+ return callback(new Error('[[error:invalid-data]]'));
+ }
+ async.each(data.pids, function (pid, next) {
+ SocketPosts.delete(socket, {pid: pid, tid: data.tid}, next);
+ }, callback);
+ };
+
+ SocketPosts.purgePosts = function (socket, data, callback) {
+ if (!data || !Array.isArray(data.pids)) {
+ return callback(new Error('[[error:invalid-data]]'));
+ }
+ async.each(data.pids, function (pid, next) {
+ SocketPosts.purge(socket, {pid: pid, tid: data.tid}, next);
+ }, callback);
+ };
+
+ SocketPosts.purge = function (socket, data, callback) {
function purgePost() {
- posts.tools.purge(socket.uid, data.pid, function(err) {
+ posts.tools.purge(socket.uid, data.pid, function (err) {
if (err) {
return callback(err);
}
websockets.in('topic_' + data.tid).emit('event:post_purged', data.pid);
- events.log({
- type: 'post-purge',
- uid: socket.uid,
- pid: data.pid,
- ip: socket.ip
+ topics.getTopicField(data.tid, 'title', function (err, title) {
+ if (err) {
+ return winston.error(err);
+ }
+ events.log({
+ type: 'post-purge',
+ uid: socket.uid,
+ pid: data.pid,
+ ip: socket.ip,
+ title: validator.escape(String(title))
+ });
});
callback();
@@ -121,7 +165,7 @@ module.exports = function(SocketPosts) {
return callback(new Error('[[error:invalid-data]]'));
}
- isMainAndLastPost(data.pid, function(err, results) {
+ isMainAndLastPost(data.pid, function (err, results) {
if (err) {
return callback(err);
}
@@ -134,26 +178,30 @@ module.exports = function(SocketPosts) {
return callback(new Error('[[error:cant-purge-main-post]]'));
}
- posts.getTopicFields(data.pid, ['tid', 'cid'], function(err, topic) {
- if (err) {
- return callback(err);
- }
- socketTopics.doTopicAction('delete', 'event:topic_deleted', socket, {tids: [topic.tid], cid: topic.cid}, callback);
- });
+ deleteTopicOf(data.pid, socket, callback);
});
};
+ function deleteTopicOf(pid, socket, callback) {
+ posts.getTopicFields(pid, ['tid', 'cid'], function (err, topic) {
+ if (err) {
+ return callback(err);
+ }
+ socketTopics.doTopicAction('delete', 'event:topic_deleted', socket, {tids: [topic.tid], cid: topic.cid}, callback);
+ });
+ }
+
function isMainAndLastPost(pid, callback) {
async.parallel({
- isMain: function(next) {
+ isMain: function (next) {
posts.isMain(pid, next);
},
- isLast: function(next) {
- posts.getTopicFields(pid, ['postcount'], function(err, topic) {
+ isLast: function (next) {
+ posts.getTopicFields(pid, ['postcount'], function (err, topic) {
next(err, topic ? parseInt(topic.postcount, 10) === 1 : false);
});
}
}, callback);
}
-};
\ No newline at end of file
+};
diff --git a/src/socket.io/posts/votes.js b/src/socket.io/posts/votes.js
new file mode 100644
index 0000000000..e3a9510aaa
--- /dev/null
+++ b/src/socket.io/posts/votes.js
@@ -0,0 +1,92 @@
+'use strict';
+
+var async = require('async');
+
+var db = require('../../database');
+var user = require('../../user');
+var posts = require('../../posts');
+var privileges = require('../../privileges');
+var helpers = require('./helpers');
+
+module.exports = function (SocketPosts) {
+
+ SocketPosts.getVoters = function (socket, data, callback) {
+ if (!data || !data.pid || !data.cid) {
+ return callback(new Error('[[error:invalid-data]]'));
+ }
+
+ async.waterfall([
+ function (next) {
+ privileges.categories.isAdminOrMod(data.cid, socket.uid, next);
+ },
+ function (isAdminOrMod, next) {
+ if (!isAdminOrMod) {
+ return next(new Error('[[error:no-privileges]]'));
+ }
+
+ async.parallel({
+ upvoteUids: function (next) {
+ db.getSetMembers('pid:' + data.pid + ':upvote', next);
+ },
+ downvoteUids: function (next) {
+ db.getSetMembers('pid:' + data.pid + ':downvote', next);
+ }
+ }, next);
+ },
+ function (results, next) {
+ async.parallel({
+ upvoters: function (next) {
+ user.getUsersFields(results.upvoteUids, ['username', 'userslug', 'picture'], next);
+ },
+ upvoteCount: function (next) {
+ next(null, results.upvoteUids.length);
+ },
+ downvoters: function (next) {
+ user.getUsersFields(results.downvoteUids, ['username', 'userslug', 'picture'], next);
+ },
+ downvoteCount: function (next) {
+ next(null, results.downvoteUids.length);
+ }
+ }, next);
+ }
+ ], callback);
+ };
+
+ SocketPosts.getUpvoters = function (socket, pids, callback) {
+ if (!Array.isArray(pids)) {
+ return callback(new Error('[[error:invalid-data]]'));
+ }
+
+ posts.getUpvotedUidsByPids(pids, function (err, data) {
+ if (err || !Array.isArray(data) || !data.length) {
+ return callback(err, []);
+ }
+
+ async.map(data, function (uids, next) {
+ var otherCount = 0;
+ if (uids.length > 6) {
+ otherCount = uids.length - 5;
+ uids = uids.slice(0, 5);
+ }
+ user.getUsernamesByUids(uids, function (err, usernames) {
+ next(err, {
+ otherCount: otherCount,
+ usernames: usernames
+ });
+ });
+ }, callback);
+ });
+ };
+
+ SocketPosts.upvote = function (socket, data, callback) {
+ helpers.postCommand(socket, 'upvote', 'voted', 'notifications:upvoted_your_post_in', data, callback);
+ };
+
+ SocketPosts.downvote = function (socket, data, callback) {
+ helpers.postCommand(socket, 'downvote', 'voted', '', data, callback);
+ };
+
+ SocketPosts.unvote = function (socket, data, callback) {
+ helpers.postCommand(socket, 'unvote', 'voted', '', data, callback);
+ };
+};
\ No newline at end of file
diff --git a/src/socket.io/topics.js b/src/socket.io/topics.js
index c96a77265e..2fdecf550f 100644
--- a/src/socket.io/topics.js
+++ b/src/socket.io/topics.js
@@ -17,29 +17,20 @@ require('./topics/tools')(SocketTopics);
require('./topics/infinitescroll')(SocketTopics);
require('./topics/tags')(SocketTopics);
-SocketTopics.post = function(socket, data, callback) {
+SocketTopics.post = function (socket, data, callback) {
if (!data) {
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);
}
- if (data.lock) {
- SocketTopics.doTopicAction('lock', 'event:topic_locked', socket, {tids: [result.topicData.tid], cid: result.topicData.cid});
- }
-
callback(null, result.topicData);
socket.emit('event:new_post', {posts: [result.postData]});
@@ -49,18 +40,18 @@ SocketTopics.post = function(socket, data, callback) {
});
};
-SocketTopics.postcount = function(socket, tid, callback) {
+SocketTopics.postcount = function (socket, tid, callback) {
topics.getTopicField(tid, 'postcount', callback);
};
-SocketTopics.bookmark = function(socket, data, callback) {
+SocketTopics.bookmark = function (socket, data, callback) {
if (!socket.uid || !data) {
return callback(new Error('[[error:invalid-data]]'));
}
topics.setUserBookmark(data.tid, socket.uid, data.index, callback);
};
-SocketTopics.createTopicFromPosts = function(socket, data, callback) {
+SocketTopics.createTopicFromPosts = function (socket, data, callback) {
if (!socket.uid) {
return callback(new Error('[[error:not-logged-in]]'));
}
@@ -69,14 +60,21 @@ SocketTopics.createTopicFromPosts = function(socket, data, callback) {
return callback(new Error('[[error:invalid-data]]'));
}
- topics.createTopicFromPosts(socket.uid, data.title, data.pids, callback);
+ topics.createTopicFromPosts(socket.uid, data.title, data.pids, data.fromTid, callback);
};
-SocketTopics.toggleFollow = function(socket, tid, callback) {
- followCommand(topics.toggleFollow, socket, tid, callback);
+SocketTopics.changeWatching = function (socket, data, callback) {
+ if (!data.tid || !data.type) {
+ return callback(new Error('[[error:invalid-data]]'));
+ }
+ var commands = ['follow', 'unfollow', 'ignore'];
+ if (commands.indexOf(data.type) === -1) {
+ return callback(new Error('[[error:invalid-command]]'));
+ }
+ followCommand(topics[data.type], socket, data.tid, callback);
};
-SocketTopics.follow = function(socket, tid, callback) {
+SocketTopics.follow = function (socket, tid, callback) {
followCommand(topics.follow, socket, tid, callback);
};
@@ -88,12 +86,18 @@ function followCommand(method, socket, tid, callback) {
method(tid, socket.uid, callback);
}
-SocketTopics.search = function(socket, data, callback) {
+SocketTopics.isFollowed = function (socket, tid, callback) {
+ topics.isFollowing([tid], socket.uid, function (err, isFollowing) {
+ callback(err, Array.isArray(isFollowing) && isFollowing.length ? isFollowing[0] : false);
+ });
+};
+
+SocketTopics.search = function (socket, data, callback) {
topics.search(data.tid, data.term, callback);
};
-SocketTopics.isModerator = function(socket, tid, callback) {
- topics.getTopicField(tid, 'cid', function(err, cid) {
+SocketTopics.isModerator = function (socket, tid, callback) {
+ topics.getTopicField(tid, 'cid', function (err, cid) {
if (err) {
return callback(err);
}
@@ -102,17 +106,7 @@ SocketTopics.isModerator = function(socket, tid, callback) {
};
SocketTopics.getTopic = function (socket, tid, callback) {
- async.waterfall([
- function (next) {
- apiController.getObjectByType(socket.uid, 'topic', tid, next);
- },
- function (topicData, next) {
- if (parseInt(topicData.deleted, 10) === 1) {
- return next(new Error('[[error:no-topic]]'));
- }
- next(null, topicData);
- }
- ], callback);
+ apiController.getTopicData(tid, socket.uid, callback);
};
module.exports = SocketTopics;
diff --git a/src/socket.io/topics/infinitescroll.js b/src/socket.io/topics/infinitescroll.js
index 5c45c6e4fb..45c354620a 100644
--- a/src/socket.io/topics/infinitescroll.js
+++ b/src/socket.io/topics/infinitescroll.js
@@ -1,31 +1,28 @@
'use strict';
var async = require('async');
-var user = require('../../user');
+
var topics = require('../../topics');
var privileges = require('../../privileges');
var meta = require('../../meta');
var utils = require('../../../public/src/utils');
var social = require('../../social');
-module.exports = function(SocketTopics) {
+module.exports = function (SocketTopics) {
- SocketTopics.loadMore = function(socket, data, callback) {
+ SocketTopics.loadMore = function (socket, data, callback) {
if (!data || !data.tid || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0) {
return callback(new Error('[[error:invalid-data]]'));
}
async.parallel({
- privileges: function(next) {
+ privileges: function (next) {
privileges.topics.get(data.tid, socket.uid, next);
},
- settings: function(next) {
- user.getSettings(socket.uid, next);
- },
- topic: function(next) {
+ topic: function (next) {
topics.getTopicFields(data.tid, ['postcount', 'deleted'], next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return callback(err);
}
@@ -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;
@@ -61,19 +58,19 @@ module.exports = function(SocketTopics) {
stop = Math.max(0, stop);
async.parallel({
- mainPost: function(next) {
+ mainPost: function (next) {
if (start > 0) {
return next();
}
topics.getMainPost(data.tid, socket.uid, next);
},
- posts: function(next) {
+ posts: function (next) {
topics.getTopicPosts(data.tid, set, start, stop, socket.uid, reverse, next);
},
postSharing: function (next) {
social.getActivePostSharing(next);
}
- }, function(err, topicData) {
+ }, function (err, topicData) {
if (err) {
return callback(err);
}
@@ -91,18 +88,18 @@ module.exports = function(SocketTopics) {
});
};
- SocketTopics.loadMoreUnreadTopics = function(socket, data, callback) {
+ SocketTopics.loadMoreUnreadTopics = function (socket, data, callback) {
if (!data || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0) {
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;
- topics.getUnreadTopics(data.cid, socket.uid, start, stop, callback);
+ topics.getUnreadTopics(data.cid, socket.uid, start, stop, data.filter, callback);
};
- SocketTopics.loadMoreFromSet = function(socket, data, callback) {
+ SocketTopics.loadMoreFromSet = function (socket, data, callback) {
if (!data || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0 || !data.set) {
return callback(new Error('[[error:invalid-data]]'));
}
diff --git a/src/socket.io/topics/move.js b/src/socket.io/topics/move.js
index 80fc2e3772..699c5e5d35 100644
--- a/src/socket.io/topics/move.js
+++ b/src/socket.io/topics/move.js
@@ -6,41 +6,41 @@ var categories = require('../../categories');
var privileges = require('../../privileges');
var socketHelpers = require('../helpers');
-module.exports = function(SocketTopics) {
+module.exports = function (SocketTopics) {
- SocketTopics.move = function(socket, data, callback) {
+ SocketTopics.move = function (socket, data, callback) {
if (!data || !Array.isArray(data.tids) || !data.cid) {
return callback(new Error('[[error:invalid-data]]'));
}
- async.eachLimit(data.tids, 10, function(tid, next) {
+ async.eachLimit(data.tids, 10, function (tid, next) {
var topicData;
async.waterfall([
- function(next) {
+ function (next) {
privileges.topics.isAdminOrMod(tid, socket.uid, next);
},
- function(canMove, next) {
+ function (canMove, next) {
if (!canMove) {
return next(new Error('[[error:no-privileges]]'));
}
next();
},
- function(next) {
+ function (next) {
topics.getTopicFields(tid, ['cid', 'slug'], next);
},
- function(_topicData, next) {
+ function (_topicData, next) {
topicData = _topicData;
topicData.tid = tid;
topics.tools.move(tid, data.cid, socket.uid, next);
}
- ], function(err) {
+ ], function (err) {
if (err) {
return next(err);
}
socketHelpers.emitToTopicAndCategory('event:topic_moved', topicData);
- socketHelpers.sendNotificationToTopicOwner(tid, socket.uid, 'notifications:moved_your_topic');
+ socketHelpers.sendNotificationToTopicOwner(tid, socket.uid, 'move', 'notifications:moved_your_topic');
next();
});
@@ -48,7 +48,7 @@ module.exports = function(SocketTopics) {
};
- SocketTopics.moveAll = function(socket, data, callback) {
+ SocketTopics.moveAll = function (socket, data, callback) {
if (!data || !data.cid || !data.currentCid) {
return callback(new Error('[[error:invalid-data]]'));
}
@@ -65,7 +65,7 @@ module.exports = function(SocketTopics) {
categories.getTopicIds('cid:' + data.currentCid + ':tids', true, 0, -1, next);
},
function (tids, next) {
- async.eachLimit(tids, 50, function(tid, next) {
+ async.eachLimit(tids, 50, function (tid, next) {
topics.tools.move(tid, data.cid, socket.uid, next);
}, next);
}
diff --git a/src/socket.io/topics/tags.js b/src/socket.io/topics/tags.js
index f55ec377fb..ab0a0221ea 100644
--- a/src/socket.io/topics/tags.js
+++ b/src/socket.io/topics/tags.js
@@ -3,19 +3,23 @@
var topics = require('../../topics');
var utils = require('../../../public/src/utils');
-module.exports = function(SocketTopics) {
- SocketTopics.searchTags = function(socket, data, callback) {
+module.exports = function (SocketTopics) {
+ SocketTopics.autocompleteTags = function (socket, data, callback) {
+ topics.autocompleteTags(data, callback);
+ };
+
+ SocketTopics.searchTags = function (socket, data, callback) {
topics.searchTags(data, callback);
};
- SocketTopics.searchAndLoadTags = function(socket, data, callback) {
+ SocketTopics.searchAndLoadTags = function (socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
topics.searchAndLoadTags(data, callback);
};
- SocketTopics.loadMoreTags = function(socket, data, callback) {
+ SocketTopics.loadMoreTags = function (socket, data, callback) {
if (!data || !utils.isNumber(data.after)) {
return callback(new Error('[[error:invalid-data]]'));
}
@@ -23,13 +27,11 @@ module.exports = function(SocketTopics) {
var start = parseInt(data.after, 10);
var stop = start + 99;
- topics.getTags(start, stop, function(err, tags) {
+ topics.getTags(start, stop, function (err, tags) {
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/tools.js b/src/socket.io/topics/tools.js
index f3d9ad4688..62e159f9fd 100644
--- a/src/socket.io/topics/tools.js
+++ b/src/socket.io/topics/tools.js
@@ -1,15 +1,18 @@
'use strict';
var async = require('async');
+var winston = require('winston');
+var validator = require('validator');
+
var topics = require('../../topics');
var events = require('../../events');
var privileges = require('../../privileges');
var plugins = require('../../plugins');
var socketHelpers = require('../helpers');
-module.exports = function(SocketTopics) {
+module.exports = function (SocketTopics) {
- SocketTopics.loadTopicTools = function(socket, data, callback) {
+ SocketTopics.loadTopicTools = function (socket, data, callback) {
if (!socket.uid) {
return callback(new Error('[[error:no-privileges]]'));
}
@@ -20,10 +23,10 @@ module.exports = function(SocketTopics) {
async.waterfall([
function (next) {
async.parallel({
- topic: function(next) {
+ topic: function (next) {
topics.getTopicData(data.tid, next);
},
- privileges: function(next) {
+ privileges: function (next) {
privileges.topics.get(data.tid, socket.uid, next);
}
}, next);
@@ -43,36 +46,36 @@ module.exports = function(SocketTopics) {
], callback);
};
- SocketTopics.delete = function(socket, data, callback) {
+ SocketTopics.delete = function (socket, data, callback) {
SocketTopics.doTopicAction('delete', 'event:topic_deleted', socket, data, callback);
};
- SocketTopics.restore = function(socket, data, callback) {
+ SocketTopics.restore = function (socket, data, callback) {
SocketTopics.doTopicAction('restore', 'event:topic_restored', socket, data, callback);
};
- SocketTopics.purge = function(socket, data, callback) {
+ SocketTopics.purge = function (socket, data, callback) {
SocketTopics.doTopicAction('purge', 'event:topic_purged', socket, data, callback);
};
- SocketTopics.lock = function(socket, data, callback) {
+ SocketTopics.lock = function (socket, data, callback) {
SocketTopics.doTopicAction('lock', 'event:topic_locked', socket, data, callback);
};
- SocketTopics.unlock = function(socket, data, callback) {
+ SocketTopics.unlock = function (socket, data, callback) {
SocketTopics.doTopicAction('unlock', 'event:topic_unlocked', socket, data, callback);
};
- SocketTopics.pin = function(socket, data, callback) {
+ SocketTopics.pin = function (socket, data, callback) {
SocketTopics.doTopicAction('pin', 'event:topic_pinned', socket, data, callback);
};
- SocketTopics.unpin = function(socket, data, callback) {
+ SocketTopics.unpin = function (socket, data, callback) {
SocketTopics.doTopicAction('unpin', 'event:topic_unpinned', socket, data, callback);
};
- SocketTopics.doTopicAction = function(action, event, socket, data, callback) {
- callback = callback || function() {};
+ SocketTopics.doTopicAction = function (action, event, socket, data, callback) {
+ callback = callback || function () {};
if (!socket.uid) {
return callback(new Error('[[error:no-privileges]]'));
}
@@ -85,8 +88,8 @@ module.exports = function(SocketTopics) {
return callback();
}
- async.each(data.tids, function(tid, next) {
- topics.tools[action](tid, socket.uid, function(err, data) {
+ async.each(data.tids, function (tid, next) {
+ topics.tools[action](tid, socket.uid, function (err, data) {
if (err) {
return next(err);
}
@@ -94,11 +97,17 @@ module.exports = function(SocketTopics) {
socketHelpers.emitToTopicAndCategory(event, data);
if (action === 'delete' || action === 'restore' || action === 'purge') {
- events.log({
- type: 'topic-' + action,
- uid: socket.uid,
- ip: socket.ip,
- tid: tid
+ topics.getTopicField(tid, 'title', function (err, title) {
+ if (err) {
+ return winston.error(err);
+ }
+ events.log({
+ type: 'topic-' + action,
+ uid: socket.uid,
+ ip: socket.ip,
+ tid: tid,
+ title: validator.escape(String(title))
+ });
});
}
diff --git a/src/socket.io/topics/unread.js b/src/socket.io/topics/unread.js
index 056b80b041..029a0c9e74 100644
--- a/src/socket.io/topics/unread.js
+++ b/src/socket.io/topics/unread.js
@@ -5,36 +5,35 @@ var async = require('async');
var user = require('../../user');
var topics = require('../../topics');
-module.exports = function(SocketTopics) {
+module.exports = function (SocketTopics) {
- SocketTopics.markAsRead = function(socket, tids, callback) {
+ SocketTopics.markAsRead = function (socket, tids, callback) {
if (!Array.isArray(tids) || !socket.uid) {
return callback(new Error('[[error:invalid-data]]'));
}
- topics.markAsRead(tids, socket.uid, function(err) {
+ topics.markAsRead(tids, socket.uid, function (err) {
if (err) {
return callback(err);
}
topics.pushUnreadCount(socket.uid);
- for (var i=0; i max) {
return next(new Error('[[error:invite-maximum-met, ' + invites + ', ' + max + ']]'));
}
next();
},
- function(next) {
+ function (next) {
user.sendInvitationEmail(socket.uid, email, next);
}
], callback);
@@ -337,17 +307,38 @@ SocketUser.invite = function(socket, email, callback) {
};
-SocketUser.getUserByUID = function(socket, uid, callback) {
- apiController.getUserDataByUID(socket.uid, uid, callback);
+SocketUser.getUserByUID = function (socket, uid, callback) {
+ apiController.getUserDataByField(socket.uid, 'uid', uid, callback);
};
-SocketUser.getUserByUsername = function(socket, username, callback) {
- apiController.getUserDataByUsername(socket.uid, username, callback);
+SocketUser.getUserByUsername = function (socket, username, callback) {
+ apiController.getUserDataByField(socket.uid, 'username', username, callback);
};
-SocketUser.getUserByEmail = function(socket, email, callback) {
- apiController.getUserDataByEmail(socket.uid, email, callback);
+SocketUser.getUserByEmail = function (socket, email, callback) {
+ apiController.getUserDataByField(socket.uid, 'email', email, callback);
};
+SocketUser.setModerationNote = function (socket, data, callback) {
+ if (!socket.uid || !data || !data.uid) {
+ return callback(new Error('[[error:invalid-data]]'));
+ }
+
+ async.waterfall([
+ function (next) {
+ user.isAdminOrGlobalMod(socket.uid, next);
+ },
+ function (isAdminOrGlobalMod, next) {
+ if (!isAdminOrGlobalMod) {
+ return next(new Error('[[error:no-privileges]]'));
+ }
+ if (data.note) {
+ user.setUserField(data.uid, 'moderationNote', data.note, next);
+ } else {
+ db.deleteObjectField('user:' + data.uid, 'moderationNote', next);
+ }
+ }
+ ], callback);
+};
module.exports = SocketUser;
diff --git a/src/socket.io/user/ban.js b/src/socket.io/user/ban.js
index 574490b74d..cdc1e20e6d 100644
--- a/src/socket.io/user/ban.js
+++ b/src/socket.io/user/ban.js
@@ -5,14 +5,25 @@ var user = require('../../user');
var websockets = require('../index');
var events = require('../../events');
-module.exports = function(SocketUser) {
+module.exports = function (SocketUser) {
- SocketUser.banUsers = function(socket, uids, callback) {
- toggleBan(socket.uid, uids, SocketUser.banUser, function(err) {
+ SocketUser.banUsers = function (socket, data, callback) {
+ // Backwards compatibility
+ if (Array.isArray(data)) {
+ data = {
+ uids: data,
+ until: 0,
+ reason: ''
+ };
+ }
+
+ toggleBan(socket.uid, data.uids, function (uid, next) {
+ banUser(uid, data.until || 0, data.reason || '', next);
+ }, function (err) {
if (err) {
return callback(err);
}
- async.each(uids, function(uid, next) {
+ async.each(data.uids, function (uid, next) {
events.log({
type: 'user-ban',
uid: socket.uid,
@@ -23,8 +34,21 @@ module.exports = function(SocketUser) {
});
};
- SocketUser.unbanUsers = function(socket, uids, callback) {
- toggleBan(socket.uid, uids, user.unban, callback);
+ SocketUser.unbanUsers = function (socket, uids, callback) {
+ toggleBan(socket.uid, uids, user.unban, function (err) {
+ if (err) {
+ return callback(err);
+ }
+
+ async.each(uids, function (uid, next) {
+ events.log({
+ type: 'user-unban',
+ uid: socket.uid,
+ targetUid: uid,
+ ip: socket.ip
+ }, next);
+ }, callback);
+ });
};
function toggleBan(uid, uids, method, callback) {
@@ -45,7 +69,7 @@ module.exports = function(SocketUser) {
], callback);
}
- SocketUser.banUser = function(uid, callback) {
+ function banUser(uid, until, reason, callback) {
async.waterfall([
function (next) {
user.isAdministrator(uid, next);
@@ -54,14 +78,13 @@ module.exports = function(SocketUser) {
if (isAdmin) {
return next(new Error('[[error:cant-ban-other-admins]]'));
}
- user.ban(uid, next);
+ user.ban(uid, until, reason, next);
},
function (next) {
websockets.in('uid_' + uid).emit('event:banned');
next();
}
], callback);
- };
-
+ }
};
diff --git a/src/socket.io/user/picture.js b/src/socket.io/user/picture.js
index cf886b1d48..7539d1687e 100644
--- a/src/socket.io/user/picture.js
+++ b/src/socket.io/user/picture.js
@@ -7,9 +7,9 @@ var path = require('path');
var user = require('../../user');
var plugins = require('../../plugins');
-module.exports = function(SocketUser) {
+module.exports = function (SocketUser) {
- SocketUser.changePicture = function(socket, data, callback) {
+ SocketUser.changePicture = function (socket, data, callback) {
if (!socket.uid) {
return callback('[[error:invalid-uid]]');
}
@@ -37,7 +37,11 @@ module.exports = function(SocketUser) {
uid: socket.uid,
type: type,
picture: undefined
- }, function(err, returnData) {
+ }, function (err, returnData) {
+ if (err) {
+ return next(err);
+ }
+
next(null, returnData.picture || '');
});
break;
@@ -49,22 +53,22 @@ module.exports = function(SocketUser) {
], callback);
};
- SocketUser.uploadProfileImageFromUrl = function(socket, data, callback) {
+ SocketUser.uploadProfileImageFromUrl = function (socket, data, callback) {
if (!socket.uid || !data.url || !data.uid) {
return callback(new Error('[[error:invalid-data]]'));
}
- user.isAdminOrSelf(socket.uid, data.uid, function(err) {
+ user.isAdminOrSelf(socket.uid, data.uid, function (err) {
if (err) {
return callback(err);
}
- user.uploadFromUrl(data.uid, data.url, function(err, uploadedImage) {
+ user.uploadFromUrl(data.uid, data.url, function (err, uploadedImage) {
callback(err, uploadedImage ? uploadedImage.url : null);
});
});
};
- SocketUser.removeUploadedPicture = function(socket, data, callback) {
+ SocketUser.removeUploadedPicture = function (socket, data, callback) {
if (!socket.uid || !data.uid) {
return callback(new Error('[[error:invalid-data]]'));
}
@@ -76,9 +80,9 @@ module.exports = function(SocketUser) {
function (next) {
user.getUserFields(data.uid, ['uploadedpicture', 'picture'], next);
},
- function(userData, next) {
+ function (userData, next) {
if (!userData.uploadedpicture.startsWith('http')) {
- require('fs').unlink(path.join(__dirname, '../../../public', userData.uploadedpicture), function(err) {
+ require('fs').unlink(path.join(__dirname, '../../../public', userData.uploadedpicture), function (err) {
if (err) {
winston.error(err);
}
@@ -93,7 +97,7 @@ module.exports = function(SocketUser) {
], callback);
};
- SocketUser.getProfilePictures = function(socket, data, callback) {
+ SocketUser.getProfilePictures = function (socket, data, callback) {
if (!data || !data.uid) {
return callback(new Error('[[error:invalid-data]]'));
}
@@ -104,7 +108,7 @@ module.exports = function(SocketUser) {
pictures: []
}),
uploaded: async.apply(user.getUserField, data.uid, 'uploadedpicture')
- }, function(err, data) {
+ }, function (err, data) {
if (err) {
return callback(err);
}
diff --git a/src/socket.io/user/profile.js b/src/socket.io/user/profile.js
index 52c9fcd801..63726cb31d 100644
--- a/src/socket.io/user/profile.js
+++ b/src/socket.io/user/profile.js
@@ -6,9 +6,9 @@ var user = require('../../user');
var meta = require('../../meta');
var events = require('../../events');
-module.exports = function(SocketUser) {
+module.exports = function (SocketUser) {
- SocketUser.changeUsernameEmail = function(socket, data, callback) {
+ SocketUser.changeUsernameEmail = function (socket, data, callback) {
if (!data || !data.uid || !socket.uid) {
return callback(new Error('[[error:invalid-data]]'));
}
@@ -23,12 +23,16 @@ module.exports = function(SocketUser) {
], callback);
};
- SocketUser.updateCover = function(socket, data, callback) {
+ SocketUser.updateCover = function (socket, data, callback) {
if (!socket.uid) {
return callback(new Error('[[error:no-privileges]]'));
}
- user.isAdministrator(socket.uid, function(err, isAdmin) {
+ user.isAdministrator(socket.uid, function (err, isAdmin) {
+ if (err) {
+ return callback(err);
+ }
+
if (!isAdmin && data.uid !== socket.uid) {
return callback(new Error('[[error:no-privileges]]'));
}
@@ -37,12 +41,12 @@ module.exports = function(SocketUser) {
});
};
- SocketUser.removeCover = function(socket, data, callback) {
+ SocketUser.removeCover = function (socket, data, callback) {
if (!socket.uid) {
return callback(new Error('[[error:no-privileges]]'));
}
- user.isAdminOrSelf(socket.uid, data.uid, function(err) {
+ user.isAdminOrSelf(socket.uid, data.uid, function (err) {
if (err) {
return callback(err);
}
@@ -54,14 +58,14 @@ module.exports = function(SocketUser) {
async.parallel({
isAdmin: async.apply(user.isAdministrator, uid),
hasPassword: async.apply(user.hasPassword, data.uid),
- passwordMatch: function(next) {
+ passwordMatch: function (next) {
if (data.password) {
user.isPasswordCorrect(data.uid, data.password, next);
} else {
next(null, false);
}
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return callback(err);
}
@@ -79,7 +83,7 @@ module.exports = function(SocketUser) {
});
}
- SocketUser.changePassword = function(socket, data, callback) {
+ SocketUser.changePassword = function (socket, data, callback) {
if (!data || !data.uid) {
return callback(new Error('[[error:invalid-data]]'));
}
@@ -87,7 +91,7 @@ module.exports = function(SocketUser) {
return callback('[[error:invalid-uid]]');
}
- user.changePassword(socket.uid, data, function(err) {
+ user.changePassword(socket.uid, data, function (err) {
if (err) {
return callback(err);
}
@@ -102,7 +106,7 @@ module.exports = function(SocketUser) {
});
};
- SocketUser.updateProfile = function(socket, data, callback) {
+ SocketUser.updateProfile = function (socket, data, callback) {
if (!socket.uid) {
return callback('[[error:invalid-uid]]');
}
@@ -124,7 +128,7 @@ module.exports = function(SocketUser) {
user.isAdminOrGlobalMod(socket.uid, next);
},
- function(isAdminOrGlobalMod, next) {
+ function (isAdminOrGlobalMod, next) {
if (!isAdminOrGlobalMod && socket.uid !== parseInt(data.uid, 10)) {
return next(new Error('[[error:no-privileges]]'));
}
diff --git a/src/socket.io/user/search.js b/src/socket.io/user/search.js
index ecef1127c2..9c3774089d 100644
--- a/src/socket.io/user/search.js
+++ b/src/socket.io/user/search.js
@@ -4,9 +4,9 @@ var user = require('../../user');
var meta = require('../../meta');
var pagination = require('../../pagination');
-module.exports = function(SocketUser) {
+module.exports = function (SocketUser) {
- SocketUser.search = function(socket, data, callback) {
+ SocketUser.search = function (socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
@@ -20,8 +20,9 @@ module.exports = function(SocketUser) {
sortBy: data.sortBy,
onlineOnly: data.onlineOnly,
bannedOnly: data.bannedOnly,
+ flaggedOnly: data.flaggedOnly,
uid: socket.uid
- }, function(err, result) {
+ }, function (err, result) {
if (err) {
return callback(err);
}
diff --git a/src/socket.io/user/status.js b/src/socket.io/user/status.js
index c911ecf588..2ed389dacc 100644
--- a/src/socket.io/user/status.js
+++ b/src/socket.io/user/status.js
@@ -3,13 +3,13 @@
var user = require('../../user');
var websockets = require('../index');
-module.exports = function(SocketUser) {
- SocketUser.checkStatus = function(socket, uid, callback) {
+module.exports = function (SocketUser) {
+ SocketUser.checkStatus = function (socket, uid, callback) {
if (!socket.uid) {
- return callback('[[error:invalid-uid]]');
+ return callback(new Error('[[error:invalid-uid]]'));
}
- user.getUserFields(uid, ['lastonline', 'status'], function(err, userData) {
+ user.getUserFields(uid, ['lastonline', 'status'], function (err, userData) {
if (err) {
return callback(err);
}
@@ -18,7 +18,7 @@ module.exports = function(SocketUser) {
});
};
- SocketUser.setStatus = function(socket, status, callback) {
+ SocketUser.setStatus = function (socket, status, callback) {
if (!socket.uid) {
return callback(new Error('[[error:invalid-uid]]'));
}
@@ -27,7 +27,12 @@ module.exports = function(SocketUser) {
if (allowedStatus.indexOf(status) === -1) {
return callback(new Error('[[error:invalid-user-status]]'));
}
- user.setUserField(socket.uid, 'status', status, function(err) {
+
+ var data = {status: status};
+ if (status !== 'offline') {
+ data.lastonline = Date.now();
+ }
+ user.setUserFields(socket.uid, data, function (err) {
if (err) {
return callback(err);
}
diff --git a/src/topics.js b/src/topics.js
index 385d3ae558..d7eb658e3b 100644
--- a/src/topics.js
+++ b/src/topics.js
@@ -12,8 +12,7 @@ var categories = require('./categories');
var privileges = require('./privileges');
var social = require('./social');
-(function(Topics) {
-
+(function (Topics) {
require('./topics/data')(Topics);
require('./topics/create')(Topics);
@@ -29,20 +28,21 @@ var social = require('./social');
require('./topics/teaser')(Topics);
require('./topics/suggested')(Topics);
require('./topics/tools')(Topics);
+ require('./topics/thumb')(Topics);
- Topics.exists = function(tid, callback) {
+ Topics.exists = function (tid, callback) {
db.isSortedSetMember('topics:tid', tid, callback);
};
- Topics.getPageCount = function(tid, uid, callback) {
- Topics.getTopicField(tid, 'postcount', function(err, postCount) {
+ Topics.getPageCount = function (tid, uid, callback) {
+ Topics.getTopicField(tid, 'postcount', function (err, postCount) {
if (err) {
return callback(err);
}
if (!parseInt(postCount, 10)) {
return callback(null, 1);
}
- user.getSettings(uid, function(err, settings) {
+ user.getSettings(uid, function (err, settings) {
if (err) {
return callback(err);
}
@@ -52,19 +52,19 @@ var social = require('./social');
});
};
- Topics.getTidPage = function(tid, uid, callback) {
+ Topics.getTidPage = function (tid, uid, callback) {
if(!tid) {
return callback(new Error('[[error:invalid-tid]]'));
}
async.parallel({
- index: function(next) {
+ index: function (next) {
categories.getTopicIndex(tid, next);
},
- settings: function(next) {
+ settings: function (next) {
user.getSettings(uid, next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return callback(err);
}
@@ -72,32 +72,32 @@ var social = require('./social');
});
};
- Topics.getTopicsFromSet = function(set, uid, start, stop, callback) {
+ Topics.getTopicsFromSet = function (set, uid, start, stop, callback) {
async.waterfall([
- function(next) {
+ function (next) {
db.getSortedSetRevRange(set, start, stop, next);
},
- function(tids, next) {
+ function (tids, next) {
Topics.getTopics(tids, uid, next);
},
- function(topics, next) {
+ function (topics, next) {
next(null, {topics: topics, nextStart: stop + 1});
}
], callback);
};
- Topics.getTopics = function(tids, uid, callback) {
+ Topics.getTopics = function (tids, uid, callback) {
async.waterfall([
- function(next) {
+ function (next) {
privileges.topics.filterTids('read', tids, uid, next);
},
- function(tids, next) {
+ function (tids, next) {
Topics.getTopicsByTids(tids, uid, next);
}
], callback);
};
- Topics.getTopicsByTids = function(tids, uid, callback) {
+ Topics.getTopicsByTids = function (tids, uid, callback) {
if (!Array.isArray(tids) || !tids.length) {
return callback(null, []);
}
@@ -105,14 +105,14 @@ var social = require('./social');
var uids, cids, topics;
async.waterfall([
- function(next) {
+ function (next) {
Topics.getTopicsData(tids, next);
},
- function(_topics, next) {
+ function (_topics, next) {
function mapFilter(array, field) {
- return array.map(function(topic) {
+ return array.map(function (topic) {
return topic && topic[field] && topic[field].toString();
- }).filter(function(value, index, array) {
+ }).filter(function (value, index, array) {
return utils.isNumber(value) && array.indexOf(value) === index;
});
}
@@ -122,31 +122,34 @@ var social = require('./social');
cids = mapFilter(topics, 'cid');
async.parallel({
- users: function(next) {
+ users: function (next) {
user.getUsersFields(uids, ['uid', 'username', 'fullname', 'userslug', 'reputation', 'postcount', 'picture', 'signature', 'banned', 'status'], next);
},
- categories: function(next) {
+ categories: function (next) {
categories.getCategoriesFields(cids, ['cid', 'name', 'slug', 'icon', 'image', 'bgColor', 'color', 'disabled'], next);
},
- hasRead: function(next) {
+ hasRead: function (next) {
Topics.hasReadTopics(tids, uid, next);
},
- bookmarks: function(next) {
+ isIgnored: function (next) {
+ Topics.isIgnoring(tids, uid, next);
+ },
+ bookmarks: function (next) {
Topics.getUserBookmarks(tids, uid, next);
},
- teasers: function(next) {
+ teasers: function (next) {
Topics.getTeasers(topics, next);
},
- tags: function(next) {
+ tags: function (next) {
Topics.getTopicsTagsObjects(tids, next);
}
}, next);
},
- function(results, next) {
+ function (results, next) {
var users = _.object(uids, results.users);
var categories = _.object(cids, results.categories);
- for (var i=0; i parseInt(max, 10)) {
- return callback(new Error('[[error:'+ maxError + ', ' + max + ']]'));
+ return callback(new Error('[[error:' + maxError + ', ' + max + ']]'));
}
callback();
}
- function guestHandleValid(data) {
- if (parseInt(meta.config.allowGuestHandles, 10) === 1 && parseInt(data.uid, 10) === 0 &&
- data.handle && data.handle.length > meta.config.maximumUsernameLength) {
- return false;
+ function guestHandleValid(data, callback) {
+ if (parseInt(meta.config.allowGuestHandles, 10) === 1 && parseInt(data.uid, 10) === 0 && data.handle) {
+ if (data.handle.length > meta.config.maximumUsernameLength) {
+ return callback(new Error('[[error:guest-handle-invalid]]'));
+ }
+ user.existsBySlug(utils.slugify(data.handle), function (err, exists) {
+ if (err || exists) {
+ return callback(err || new Error('[[error:username-taken]]'));
+ }
+ callback();
+ });
+ return;
}
- return true;
+ callback();
}
};
diff --git a/src/topics/data.js b/src/topics/data.js
index 9d153b5b78..a0196e1508 100644
--- a/src/topics/data.js
+++ b/src/topics/data.js
@@ -6,28 +6,28 @@ var db = require('../database');
var categories = require('../categories');
var utils = require('../../public/src/utils');
-module.exports = function(Topics) {
+module.exports = function (Topics) {
- Topics.getTopicField = function(tid, field, callback) {
+ Topics.getTopicField = function (tid, field, callback) {
db.getObjectField('topic:' + tid, field, callback);
};
- Topics.getTopicFields = function(tid, fields, callback) {
+ Topics.getTopicFields = function (tid, fields, callback) {
db.getObjectFields('topic:' + tid, fields, callback);
};
- Topics.getTopicsFields = function(tids, fields, callback) {
+ Topics.getTopicsFields = function (tids, fields, callback) {
if (!Array.isArray(tids) || !tids.length) {
return callback(null, []);
}
- var keys = tids.map(function(tid) {
+ var keys = tids.map(function (tid) {
return 'topic:' + tid;
});
db.getObjectsFields(keys, fields, callback);
};
- Topics.getTopicData = function(tid, callback) {
- db.getObject('topic:' + tid, function(err, topic) {
+ Topics.getTopicData = function (tid, callback) {
+ db.getObject('topic:' + tid, function (err, topic) {
if (err || !topic) {
return callback(err);
}
@@ -37,14 +37,14 @@ module.exports = function(Topics) {
});
};
- Topics.getTopicsData = function(tids, callback) {
+ Topics.getTopicsData = function (tids, callback) {
var keys = [];
- for (var i=0; i= (meta.config.minimumTagLength || 3) && array.indexOf(tag) === index;
});
- var keys = tags.map(function(tag) {
+ var keys = tags.map(function (tag) {
return 'tag:' + tag + ':topics';
});
async.parallel([
async.apply(db.setAdd, 'topic:' + tid + ':tags', tags),
async.apply(db.sortedSetsAdd, keys, timestamp, tid)
- ], function(err) {
+ ], function (err) {
if (err) {
return next(err);
}
@@ -48,13 +48,36 @@ module.exports = function(Topics) {
], callback);
};
- Topics.updateTag = function(tag, data, callback) {
+ Topics.createEmptyTag = function (tag, callback) {
+ if (!tag) {
+ return callback(new Error('[[error:invalid-tag]]'));
+ }
+
+ tag = utils.cleanUpTag(tag, meta.config.maximumTagLength);
+ if (tag.length < (meta.config.minimumTagLength || 3)) {
+ return callback(new Error('[[error:tag-too-short]]'));
+ }
+
+ async.waterfall([
+ function (next) {
+ db.isSortedSetMember('tags:topic:count', tag, next);
+ },
+ function (isMember, next) {
+ if (isMember) {
+ return next();
+ }
+ db.sortedSetAdd('tags:topic:count', 0, tag, next);
+ }
+ ], callback);
+ };
+
+ Topics.updateTag = function (tag, data, callback) {
db.setObject('tag:' + tag, data, callback);
};
function updateTagCount(tag, callback) {
- callback = callback || function() {};
- Topics.getTagTopicCount(tag, function(err, count) {
+ callback = callback || function () {};
+ Topics.getTagTopicCount(tag, function (err, count) {
if (err) {
return callback(err);
}
@@ -64,42 +87,42 @@ module.exports = function(Topics) {
});
}
- Topics.getTagTids = function(tag, start, stop, callback) {
+ Topics.getTagTids = function (tag, start, stop, callback) {
db.getSortedSetRevRange('tag:' + tag + ':topics', start, stop, callback);
};
- Topics.getTagTopicCount = function(tag, callback) {
+ Topics.getTagTopicCount = function (tag, callback) {
db.sortedSetCard('tag:' + tag + ':topics', callback);
};
- Topics.deleteTags = function(tags, callback) {
+ Topics.deleteTags = function (tags, callback) {
if (!Array.isArray(tags) || !tags.length) {
return callback();
}
async.series([
- function(next) {
+ function (next) {
removeTagsFromTopics(tags, next);
},
- function(next) {
- var keys = tags.map(function(tag) {
+ function (next) {
+ var keys = tags.map(function (tag) {
return 'tag:' + tag + ':topics';
});
db.deleteAll(keys, next);
},
- function(next) {
+ function (next) {
db.sortedSetRemove('tags:topic:count', tags, next);
}
], callback);
};
function removeTagsFromTopics(tags, callback) {
- async.eachLimit(tags, 50, function(tag, next) {
- db.getSortedSetRange('tag:' + tag + ':topics', 0, -1, function(err, tids) {
+ async.eachLimit(tags, 50, function (tag, next) {
+ db.getSortedSetRange('tag:' + tag + ':topics', 0, -1, function (err, tids) {
if (err || !tids.length) {
return next(err);
}
- var keys = tids.map(function(tid) {
+ var keys = tids.map(function (tid) {
return 'topic:' + tid + ':tags';
});
@@ -108,13 +131,13 @@ module.exports = function(Topics) {
}, callback);
}
- Topics.deleteTag = function(tag) {
+ Topics.deleteTag = function (tag) {
db.delete('tag:' + tag + ':topics');
db.sortedSetRemove('tags:topic:count', tag);
};
- Topics.getTags = function(start, stop, callback) {
- db.getSortedSetRevRangeWithScores('tags:topic:count', start, stop, function(err, tags) {
+ Topics.getTags = function (start, stop, callback) {
+ db.getSortedSetRevRangeWithScores('tags:topic:count', start, stop, function (err, tags) {
if (err) {
return callback(err);
}
@@ -123,17 +146,17 @@ module.exports = function(Topics) {
});
};
- Topics.getTagData = function(tags, callback) {
- var keys = tags.map(function(tag) {
+ Topics.getTagData = function (tags, callback) {
+ var keys = tags.map(function (tag) {
return 'tag:' + tag.value;
});
- db.getObjects(keys, function(err, tagData) {
+ db.getObjects(keys, function (err, tagData) {
if (err) {
return callback(err);
}
- tags.forEach(function(tag, index) {
+ tags.forEach(function (tag, index) {
tag.color = tagData[index] ? tagData[index].color : '';
tag.bgColor = tagData[index] ? tagData[index].bgColor : '';
});
@@ -141,53 +164,53 @@ module.exports = function(Topics) {
});
};
- Topics.getTopicTags = function(tid, callback) {
+ Topics.getTopicTags = function (tid, callback) {
db.getSetMembers('topic:' + tid + ':tags', callback);
};
- Topics.getTopicTagsObjects = function(tid, callback) {
- Topics.getTopicsTagsObjects([tid], function(err, data) {
+ Topics.getTopicTagsObjects = function (tid, callback) {
+ Topics.getTopicsTagsObjects([tid], function (err, data) {
callback(err, Array.isArray(data) && data.length ? data[0] : []);
});
};
- Topics.getTopicsTagsObjects = function(tids, callback) {
- var sets = tids.map(function(tid) {
+ Topics.getTopicsTagsObjects = function (tids, callback) {
+ var sets = tids.map(function (tid) {
return 'topic:' + tid + ':tags';
});
- db.getSetsMembers(sets, function(err, topicTags) {
+ db.getSetsMembers(sets, function (err, topicTags) {
if (err) {
return callback(err);
}
var uniqueTopicTags = _.uniq(_.flatten(topicTags));
- var tags = uniqueTopicTags.map(function(tag) {
+ var tags = uniqueTopicTags.map(function (tag) {
return {value: tag};
});
async.parallel({
- tagData: function(next) {
+ tagData: function (next) {
Topics.getTagData(tags, next);
},
- counts: function(next) {
+ counts: function (next) {
db.sortedSetScores('tags:topic:count', uniqueTopicTags, next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return callback(err);
}
- results.tagData.forEach(function(tag, index) {
+ results.tagData.forEach(function (tag, index) {
tag.score = results.counts[index] ? results.counts[index] : 0;
});
var tagData = _.object(uniqueTopicTags, results.tagData);
- topicTags.forEach(function(tags, index) {
+ topicTags.forEach(function (tags, index) {
if (Array.isArray(tags)) {
- topicTags[index] = tags.map(function(tag) {return tagData[tag];});
+ topicTags[index] = tags.map(function (tag) {return tagData[tag];});
}
});
@@ -196,79 +219,121 @@ module.exports = function(Topics) {
});
};
- Topics.updateTags = function(tid, tags, callback) {
- callback = callback || function() {};
+ Topics.updateTags = function (tid, tags, callback) {
+ callback = callback || function () {};
async.waterfall([
- function(next) {
+ function (next) {
Topics.deleteTopicTags(tid, next);
},
- function(next) {
+ function (next) {
Topics.getTopicField(tid, 'timestamp', next);
},
- function(timestamp, next) {
+ function (timestamp, next) {
Topics.createTags(tags, tid, timestamp, next);
}
], callback);
};
- Topics.deleteTopicTags = function(tid, callback) {
- Topics.getTopicTags(tid, function(err, tags) {
+ Topics.deleteTopicTags = function (tid, callback) {
+ Topics.getTopicTags(tid, function (err, tags) {
if (err) {
return callback(err);
}
async.series([
- function(next) {
+ function (next) {
db.delete('topic:' + tid + ':tags', next);
},
- function(next) {
- var sets = tags.map(function(tag) {
+ function (next) {
+ var sets = tags.map(function (tag) {
return 'tag:' + tag + ':topics';
});
db.sortedSetsRemove(sets, tid, next);
},
- function(next) {
- async.each(tags, function(tag, next) {
+ function (next) {
+ async.each(tags, function (tag, next) {
updateTagCount(tag, next);
}, next);
}
- ], function(err, results) {
+ ], function (err) {
callback(err);
});
});
};
- Topics.searchTags = function(data, callback) {
+ Topics.searchTags = function (data, callback) {
+ function done(matches) {
+ plugins.fireHook('filter:tags.search', {data: data, matches: matches}, function (err, data) {
+ callback(err, data ? data.matches : []);
+ });
+ }
+
+
if (!data || !data.query) {
return callback(null, []);
}
- db.getSortedSetRevRange('tags:topic:count', 0, -1, function(err, tags) {
- if (err) {
- return callback(null, []);
- }
-
- data.query = data.query.toLowerCase();
-
- var matches = [];
- for(var i=0; i b;
- });
-
- plugins.fireHook('filter:tags.search', {data: data, matches: matches}, function(err, data) {
- callback(err, data ? data.matches : []);
- });
+ done(matches);
});
};
- Topics.searchAndLoadTags = function(data, callback) {
+ Topics.autocompleteTags = function (data, callback) {
+ if (!data || !data.query) {
+ return callback(null, []);
+ }
+
+ if (plugins.hasListeners('filter:topics.autocompleteTags')) {
+ return plugins.fireHook('filter:topics.autocompleteTags', {data: data}, function (err, data) {
+ if (err) {
+ return callback(err);
+ }
+ callback(null, data.matches);
+ });
+ }
+
+ findMatches(data.query, callback);
+ };
+
+ function findMatches(query, callback) {
+ db.getSortedSetRevRange('tags:topic:count', 0, -1, function (err, tags) {
+ if (err) {
+ return callback(err);
+ }
+
+ query = query.toLowerCase();
+
+ var matches = [];
+ for(var i = 0; i < tags.length; ++i) {
+ if (tags[i].toLowerCase().startsWith(query)) {
+ matches.push(tags[i]);
+ if (matches.length > 19) {
+ break;
+ }
+ }
+ }
+
+ matches = matches.sort(function (a, b) {
+ return a > b;
+ });
+ callback(null, matches);
+ });
+ }
+
+ Topics.searchAndLoadTags = function (data, callback) {
var searchResult = {
tags: [],
matchCount: 0,
@@ -278,29 +343,29 @@ module.exports = function(Topics) {
if (!data.query || !data.query.length) {
return callback(null, searchResult);
}
- Topics.searchTags(data, function(err, tags) {
+ Topics.searchTags(data, function (err, tags) {
if (err) {
return callback(err);
}
async.parallel({
- counts: function(next) {
+ counts: function (next) {
db.sortedSetScores('tags:topic:count', tags, next);
},
- tagData: function(next) {
- tags = tags.map(function(tag) {
+ tagData: function (next) {
+ tags = tags.map(function (tag) {
return {value: tag};
});
Topics.getTagData(tags, next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return callback(err);
}
- results.tagData.forEach(function(tag, index) {
+ results.tagData.forEach(function (tag, index) {
tag.score = results.counts[index];
});
- results.tagData.sort(function(a, b) {
+ results.tagData.sort(function (a, b) {
return b.score - a.score;
});
searchResult.tags = results.tagData;
@@ -311,13 +376,13 @@ module.exports = function(Topics) {
});
};
- Topics.getRelatedTopics = function(topicData, uid, callback) {
+ Topics.getRelatedTopics = function (topicData, uid, callback) {
if (plugins.hasListeners('filter:topic.getRelatedTopics')) {
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, []);
}
@@ -334,7 +399,7 @@ module.exports = function(Topics) {
Topics.getTopics(tids, uid, next);
},
function (topics, next) {
- topics = topics.filter(function(topic) {
+ topics = topics.filter(function (topic) {
return topic && !topic.deleted && parseInt(topic.uid, 10) !== parseInt(uid, 10);
});
next(null, topics);
diff --git a/src/topics/teaser.js b/src/topics/teaser.js
index 5d730de74d..1467dd194f 100644
--- a/src/topics/teaser.js
+++ b/src/topics/teaser.js
@@ -2,20 +2,18 @@
'use strict';
-var async = require('async'),
- S = require('string'),
+var async = require('async');
+var S = require('string');
- meta = require('../meta'),
- db = require('../database'),
- user = require('../user'),
- posts = require('../posts'),
- plugins = require('../plugins'),
- utils = require('../../public/src/utils');
+var meta = require('../meta');
+var user = require('../user');
+var posts = require('../posts');
+var plugins = require('../plugins');
+var utils = require('../../public/src/utils');
+module.exports = function (Topics) {
-module.exports = function(Topics) {
-
- Topics.getTeasers = function(topics, callback) {
+ Topics.getTeasers = function (topics, callback) {
if (!Array.isArray(topics) || !topics.length) {
return callback(null, []);
}
@@ -25,35 +23,52 @@ module.exports = function(Topics) {
var postData;
var tidToPost = {};
- topics.forEach(function(topic) {
+ topics.forEach(function (topic) {
counts.push(topic && (parseInt(topic.postcount, 10) || 0));
if (topic) {
- teaserPids.push(meta.config.teaserPost === 'first' ? topic.mainPid : topic.teaserPid);
+ if (topic.teaserPid === 'null') {
+ delete topic.teaserPid;
+ }
+
+ switch(meta.config.teaserPost) {
+ case 'first':
+ teaserPids.push(topic.mainPid);
+ break;
+
+ case 'last-post':
+ teaserPids.push(topic.teaserPid || topic.mainPid);
+ break;
+
+ case 'last-reply': // intentional fall-through
+ default:
+ teaserPids.push(topic.teaserPid);
+ break;
+ }
}
});
async.waterfall([
- function(next) {
+ function (next) {
posts.getPostsFields(teaserPids, ['pid', 'uid', 'timestamp', 'tid', 'content'], next);
},
- function(_postData, next) {
+ function (_postData, next) {
postData = _postData;
- var uids = postData.map(function(post) {
+ var uids = postData.map(function (post) {
return post.uid;
- }).filter(function(uid, index, array) {
+ }).filter(function (uid, index, array) {
return array.indexOf(uid) === index;
});
user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture'], next);
},
- function(usersData, next) {
+ function (usersData, next) {
var users = {};
- usersData.forEach(function(user) {
+ usersData.forEach(function (user) {
users[user.uid] = user;
});
- async.each(postData, function(post, next) {
+ async.each(postData, function (post, next) {
// If the post author isn't represented in the retrieved users' data, then it means they were deleted, assume guest.
if (!users.hasOwnProperty(post.uid)) {
post.uid = 0;
@@ -65,8 +80,8 @@ module.exports = function(Topics) {
posts.parsePost(post, next);
}, next);
},
- function(next) {
- var teasers = topics.map(function(topic, index) {
+ function (next) {
+ var teasers = topics.map(function (topic, index) {
if (!topic) {
return null;
}
@@ -74,7 +89,7 @@ module.exports = function(Topics) {
tidToPost[topic.tid].index = meta.config.teaserPost === 'first' ? 1 : counts[index];
if (tidToPost[topic.tid].content) {
var s = S(tidToPost[topic.tid].content);
- tidToPost[topic.tid].content = s.stripTags.apply(s, utils.stripTags.concat(['img'])).s;
+ tidToPost[topic.tid].content = s.stripTags.apply(s, utils.stripTags).s;
}
}
return tidToPost[topic.tid];
@@ -82,40 +97,44 @@ module.exports = function(Topics) {
plugins.fireHook('filter:teasers.get', {teasers: teasers}, next);
},
- function(data, next) {
+ function (data, next) {
next(null, data.teasers);
}
], callback);
};
- Topics.getTeasersByTids = function(tids, callback) {
+ Topics.getTeasersByTids = function (tids, callback) {
if (!Array.isArray(tids) || !tids.length) {
return callback(null, []);
}
async.waterfall([
- function(next) {
+ function (next) {
Topics.getTopicsFields(tids, ['tid', 'postcount', 'teaserPid'], next);
},
- function(topics, next) {
+ function (topics, next) {
Topics.getTeasers(topics, next);
}
], callback);
};
- Topics.getTeaser = function(tid, callback) {
- Topics.getTeasersByTids([tid], function(err, teasers) {
+ Topics.getTeaser = function (tid, callback) {
+ Topics.getTeasersByTids([tid], function (err, teasers) {
callback(err, Array.isArray(teasers) && teasers.length ? teasers[0] : null);
});
};
- Topics.updateTeaser = function(tid, callback) {
- Topics.getLatestUndeletedReply(tid, function(err, pid) {
+ Topics.updateTeaser = function (tid, callback) {
+ Topics.getLatestUndeletedReply(tid, function (err, pid) {
if (err) {
return callback(err);
}
pid = pid || null;
- Topics.setTopicField(tid, 'teaserPid', pid, callback);
+ if (pid) {
+ Topics.setTopicField(tid, 'teaserPid', pid, callback);
+ } else {
+ Topics.deleteTopicField(tid, 'teaserPid', callback);
+ }
});
};
};
\ No newline at end of file
diff --git a/src/topics/thumb.js b/src/topics/thumb.js
new file mode 100644
index 0000000000..7d22365293
--- /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 1cb02ae514..6fce256f27 100644
--- a/src/topics/tools.js
+++ b/src/topics/tools.js
@@ -1,24 +1,25 @@
'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) {
+module.exports = function (Topics) {
var topicTools = {};
Topics.tools = topicTools;
- topicTools.delete = function(tid, uid, callback) {
+ topicTools.delete = function (tid, uid, callback) {
toggleDelete(tid, uid, true, callback);
};
- topicTools.restore = function(tid, uid, callback) {
+ topicTools.restore = function (tid, uid, callback) {
toggleDelete(tid, uid, false, callback);
};
@@ -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);
@@ -72,13 +73,13 @@ module.exports = function(Topics) {
], callback);
}
- topicTools.purge = function(tid, uid, callback) {
+ topicTools.purge = function (tid, uid, callback) {
var cid;
async.waterfall([
- function(next) {
+ function (next) {
Topics.exists(tid, next);
},
- function(exists, next) {
+ function (exists, next) {
if (!exists) {
return callback();
}
@@ -102,16 +103,16 @@ module.exports = function(Topics) {
], callback);
};
- topicTools.lock = function(tid, uid, callback) {
+ topicTools.lock = function (tid, uid, callback) {
toggleLock(tid, uid, true, callback);
};
- topicTools.unlock = function(tid, uid, callback) {
+ topicTools.unlock = function (tid, uid, callback) {
toggleLock(tid, uid, false, callback);
};
function toggleLock(tid, uid, lock, callback) {
- callback = callback || function() {};
+ callback = callback || function () {};
var cid;
@@ -148,11 +149,11 @@ module.exports = function(Topics) {
], callback);
}
- topicTools.pin = function(tid, uid, callback) {
+ topicTools.pin = function (tid, uid, callback) {
togglePin(tid, uid, true, callback);
};
- topicTools.unpin = function(tid, uid, callback) {
+ topicTools.unpin = function (tid, uid, callback) {
togglePin(tid, uid, false, callback);
};
@@ -172,7 +173,7 @@ module.exports = function(Topics) {
topicData = _topicData;
privileges.categories.isAdminOrMod(_topicData.cid, uid, next);
},
- function(isAdminOrMod, next) {
+ function (isAdminOrMod, next) {
if (!isAdminOrMod) {
return next(new Error('[[error:no-privileges]]'));
}
@@ -181,7 +182,7 @@ module.exports = function(Topics) {
async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids', pin ? Math.pow(2, 53) : topicData.lastposttime, tid)
], next);
},
- function(results, next) {
+ function (results, next) {
var data = {
tid: tid,
isPinned: pin,
@@ -196,7 +197,7 @@ module.exports = function(Topics) {
], callback);
}
- topicTools.move = function(tid, cid, uid, callback) {
+ topicTools.move = function (tid, cid, uid, callback) {
var topic;
async.waterfall([
function (next) {
@@ -218,16 +219,16 @@ module.exports = function(Topics) {
function (next) {
var timestamp = parseInt(topic.pinned, 10) ? Math.pow(2, 53) : topic.lastposttime;
async.parallel([
- function(next) {
+ function (next) {
db.sortedSetAdd('cid:' + cid + ':tids', timestamp, tid, next);
},
- function(next) {
+ function (next) {
topic.postcount = topic.postcount || 0;
db.sortedSetAdd('cid:' + cid + ':tids:posts', topic.postcount, tid, next);
}
], next);
}
- ], function(err) {
+ ], function (err) {
if (err) {
return callback(err);
}
@@ -242,9 +243,12 @@ module.exports = function(Topics) {
categories.incrementCategoryFieldBy(cid, 'topic_count', 1, next);
},
function (next) {
- Topics.setTopicField(tid, 'cid', cid, next);
+ Topics.setTopicFields(tid, {
+ cid: cid,
+ oldCid: oldCid
+ }, next);
}
- ], function(err) {
+ ], function (err) {
if (err) {
return callback(err);
}
diff --git a/src/topics/unread.js b/src/topics/unread.js
index 28846640b7..b4a5d07514 100644
--- a/src/topics/unread.js
+++ b/src/topics/unread.js
@@ -12,15 +12,20 @@ var privileges = require('../privileges');
var meta = require('../meta');
var utils = require('../../public/src/utils');
-module.exports = function(Topics) {
+module.exports = function (Topics) {
- Topics.getTotalUnread = function(uid, callback) {
- Topics.getUnreadTids(0, uid, 0, 99, function(err, tids) {
- callback(err, tids ? tids.length : 0);
+ Topics.getTotalUnread = function (uid, filter, callback) {
+ if (!callback) {
+ callback = filter;
+ filter = '';
+ }
+ Topics.getUnreadTids(0, uid, filter, function (err, tids) {
+ callback(err, Array.isArray(tids) ? tids.length : 0);
});
};
- Topics.getUnreadTopics = function(cid, uid, start, stop, callback) {
+
+ Topics.getUnreadTopics = function (cid, uid, start, stop, filter, callback) {
var unreadTopics = {
showSelect: true,
@@ -29,16 +34,25 @@ module.exports = function(Topics) {
};
async.waterfall([
- function(next) {
- Topics.getUnreadTids(cid, uid, start, stop, next);
+ function (next) {
+ Topics.getUnreadTids(cid, uid, filter, next);
},
- function(tids, next) {
+ function (tids, next) {
+ unreadTopics.topicCount = tids.length;
+
if (!tids.length) {
return next(null, []);
}
+
+ if (stop === -1) {
+ tids = tids.slice(start);
+ } else {
+ tids = tids.slice(start, stop + 1);
+ }
+
Topics.getTopicsByTids(tids, uid, next);
},
- function(topicData, next) {
+ function (topicData, next) {
if (!Array.isArray(topicData) || !topicData.length) {
return next(null, unreadTopics);
}
@@ -50,11 +64,11 @@ module.exports = function(Topics) {
], callback);
};
- Topics.unreadCutoff = function() {
+ Topics.unreadCutoff = function () {
return Date.now() - (parseInt(meta.config.unreadCutoff, 10) || 2) * 86400000;
};
- Topics.getUnreadTids = function(cid, uid, start, stop, callback) {
+ Topics.getUnreadTids = function (cid, uid, filter, callback) {
uid = parseInt(uid, 10);
if (uid === 0) {
return callback(null, []);
@@ -62,80 +76,120 @@ module.exports = function(Topics) {
var cutoff = Topics.unreadCutoff();
- async.parallel({
- ignoredCids: function(next) {
- user.getIgnoredCategories(uid, next);
+ var ignoredCids;
+
+ async.waterfall([
+ function (next) {
+ async.parallel({
+ ignoredCids: function (next) {
+ if (filter === 'watched') {
+ return next(null, []);
+ }
+ user.getIgnoredCategories(uid, next);
+ },
+ ignoredTids: function (next) {
+ user.getIgnoredTids(uid, 0, -1, next);
+ },
+ recentTids: function (next) {
+ db.getSortedSetRevRangeByScoreWithScores('topics:recent', 0, -1, '+inf', cutoff, next);
+ },
+ userScores: function (next) {
+ db.getSortedSetRevRangeByScoreWithScores('uid:' + uid + ':tids_read', 0, -1, '+inf', cutoff, next);
+ },
+ tids_unread: function (next) {
+ db.getSortedSetRevRangeWithScores('uid:' + uid + ':tids_unread', 0, -1, next);
+ }
+ }, next);
},
- recentTids: function(next) {
- db.getSortedSetRevRangeByScoreWithScores('topics:recent', 0, -1, '+inf', cutoff, next);
+ function (results, next) {
+ if (results.recentTids && !results.recentTids.length && !results.tids_unread.length) {
+ return callback(null, []);
+ }
+
+ ignoredCids = results.ignoredCids;
+
+ var userRead = {};
+ results.userScores.forEach(function (userItem) {
+ userRead[userItem.value] = userItem.score;
+ });
+
+ results.recentTids = results.recentTids.concat(results.tids_unread);
+ results.recentTids.sort(function (a, b) {
+ return b.score - a.score;
+ });
+
+ var tids = results.recentTids.filter(function (recentTopic) {
+ if (results.ignoredTids.indexOf(recentTopic.value.toString()) !== -1) {
+ return false;
+ }
+ switch (filter) {
+ case 'new':
+ return !userRead[recentTopic.value];
+ default:
+ return !userRead[recentTopic.value] || recentTopic.score > userRead[recentTopic.value];
+ }
+ }).map(function (topic) {
+ return topic.value;
+ }).filter(function (tid, index, array) {
+ return array.indexOf(tid) === index;
+ });
+
+ if (filter === 'watched') {
+ filterWatchedTids(uid, tids, next);
+ } else {
+ next(null, tids);
+ }
},
- userScores: function(next) {
- db.getSortedSetRevRangeByScoreWithScores('uid:' + uid + ':tids_read', 0, -1, '+inf', cutoff, next);
- },
- tids_unread: function(next) {
- db.getSortedSetRevRangeWithScores('uid:' + uid + ':tids_unread', 0, -1, next);
+ function (tids, next) {
+
+ tids = tids.slice(0, 200);
+
+ filterTopics(uid, tids, cid, ignoredCids, filter, next);
}
- }, function(err, results) {
+ ], callback);
+ };
+
+ function filterWatchedTids(uid, tids, callback) {
+ db.sortedSetScores('uid:' + uid + ':followed_tids', tids, function (err, scores) {
if (err) {
return callback(err);
}
-
- if (results.recentTids && !results.recentTids.length && !results.tids_unread.length) {
- return callback(null, []);
- }
-
- var userRead = {};
- results.userScores.forEach(function(userItem) {
- userRead[userItem.value] = userItem.score;
- });
-
- results.recentTids = results.recentTids.concat(results.tids_unread);
- results.recentTids.sort(function(a, b) {
- return b.score - a.score;
- });
-
- var tids = results.recentTids.filter(function(recentTopic) {
- return !userRead[recentTopic.value] || recentTopic.score > userRead[recentTopic.value];
- }).map(function(topic) {
- return topic.value;
- }).filter(function(tid, index, array) {
- return array.indexOf(tid) === index;
- });
-
- tids = tids.slice(0, 100);
-
- filterTopics(uid, tids, cid, results.ignoredCids, function(err, tids) {
- if (err) {
- return callback(err);
- }
-
- if (stop === -1) {
- tids = tids.slice(start);
- } else {
- tids = tids.slice(start, stop + 1);
- }
-
- callback(null, tids);
+ tids = tids.filter(function (tid, index) {
+ return tid && !!scores[index];
});
+ callback(null, tids);
});
- };
+ }
- function filterTopics(uid, tids, cid, ignoredCids, callback) {
+ function filterTopics(uid, tids, cid, ignoredCids, filter, callback) {
if (!Array.isArray(ignoredCids) || !tids.length) {
return callback(null, tids);
}
async.waterfall([
- function(next) {
+ function (next) {
privileges.topics.filterTids('read', tids, uid, next);
},
- function(tids, next) {
- Topics.getTopicsFields(tids, ['tid', 'cid'], next);
+ function (tids, next) {
+ async.parallel({
+ topics: function (next) {
+ Topics.getTopicsFields(tids, ['tid', 'cid'], next);
+ },
+ isTopicsFollowed: function (next) {
+ if (filter === 'watched' || filter === 'new') {
+ return next(null, []);
+ }
+ db.sortedSetScores('uid:' + uid + ':followed_tids', tids, next);
+ }
+ }, next);
},
- function(topics, next) {
- tids = topics.filter(function(topic) {
- return topic && topic.cid && ignoredCids.indexOf(topic.cid.toString()) === -1 && (!cid || parseInt(cid, 10) === parseInt(topic.cid, 10));
- }).map(function(topic) {
+ function (results, next) {
+ var topics = results.topics;
+ tids = topics.filter(function (topic, index) {
+ return topic && topic.cid &&
+ (!!results.isTopicsFollowed[index] || ignoredCids.indexOf(topic.cid.toString()) === -1) &&
+ (!cid || parseInt(cid, 10) === parseInt(topic.cid, 10));
+ }).map(function (topic) {
return topic.tid;
});
next(null, tids);
@@ -143,13 +197,13 @@ module.exports = function(Topics) {
], callback);
}
- Topics.pushUnreadCount = function(uid, callback) {
- callback = callback || function() {};
+ Topics.pushUnreadCount = function (uid, callback) {
+ callback = callback || function () {};
if (!uid || parseInt(uid, 10) === 0) {
return callback();
}
- Topics.getTotalUnread(uid, function(err, count) {
+ Topics.getTotalUnread(uid, function (err, count) {
if (err) {
return callback(err);
}
@@ -159,17 +213,17 @@ module.exports = function(Topics) {
});
};
- Topics.markAsUnreadForAll = function(tid, callback) {
+ Topics.markAsUnreadForAll = function (tid, callback) {
Topics.markCategoryUnreadForAll(tid, callback);
};
- Topics.markAsRead = function(tids, uid, callback) {
- callback = callback || function() {};
+ Topics.markAsRead = function (tids, uid, callback) {
+ callback = callback || function () {};
if (!Array.isArray(tids) || !tids.length) {
return callback();
}
- tids = tids.filter(function(tid, index, array) {
+ tids = tids.filter(function (tid, index, array) {
return tid && utils.isNumber(tid) && array.indexOf(tid) === index;
});
@@ -185,7 +239,7 @@ module.exports = function(Topics) {
}, next);
},
function (results, next) {
- tids = tids.filter(function(tid, index) {
+ tids = tids.filter(function (tid, index) {
return results.topicScores[index] && (!results.userScores[index] || results.userScores[index] < results.topicScores[index]);
});
@@ -194,20 +248,20 @@ module.exports = function(Topics) {
}
var now = Date.now();
- var scores = tids.map(function() {
+ var scores = tids.map(function () {
return now;
});
async.parallel({
markRead: async.apply(db.sortedSetAdd, 'uid:' + uid + ':tids_read', scores, tids),
markUnread: async.apply(db.sortedSetRemove, 'uid:' + uid + ':tids_unread', tids),
- topicData: async.apply( Topics.getTopicsFields, tids, ['cid'])
+ topicData: async.apply(Topics.getTopicsFields, tids, ['cid'])
}, next);
},
function (results, next) {
- var cids = results.topicData.map(function(topic) {
+ var cids = results.topicData.map(function (topic) {
return topic && topic.cid;
- }).filter(function(topic, index, array) {
+ }).filter(function (topic, index, array) {
return topic && array.indexOf(topic) === index;
});
@@ -219,15 +273,13 @@ module.exports = function(Topics) {
], callback);
};
- Topics.markAllRead = function(uid, callback) {
+ Topics.markAllRead = function (uid, callback) {
async.waterfall([
function (next) {
db.getSortedSetRevRangeByScore('topics:recent', 0, -1, '+inf', Topics.unreadCutoff(), next);
},
function (tids, next) {
- for (var i=0; i= results.recentScores[index]));
@@ -293,13 +351,13 @@ module.exports = function(Topics) {
});
};
- Topics.hasReadTopic = function(tid, uid, callback) {
- Topics.hasReadTopics([tid], uid, function(err, hasRead) {
+ Topics.hasReadTopic = function (tid, uid, callback) {
+ Topics.hasReadTopics([tid], uid, function (err, hasRead) {
callback(err, Array.isArray(hasRead) && hasRead.length ? hasRead[0] : false);
});
};
- Topics.markUnread = function(tid, uid, callback) {
+ Topics.markUnread = function (tid, uid, callback) {
async.waterfall([
function (next) {
Topics.exists(tid, next);
diff --git a/src/topics/user.js b/src/topics/user.js
index 4db380efdf..ab56a5f778 100644
--- a/src/topics/user.js
+++ b/src/topics/user.js
@@ -2,40 +2,23 @@
'use strict';
-var async = require('async'),
- db = require('../database'),
- posts = require('../posts');
+var async = require('async');
+var db = require('../database');
+var posts = require('../posts');
+module.exports = function (Topics) {
-module.exports = function(Topics) {
-
- Topics.isOwner = function(tid, uid, callback) {
+ Topics.isOwner = function (tid, uid, callback) {
uid = parseInt(uid, 10);
if (!uid) {
return callback(null, false);
}
- Topics.getTopicField(tid, 'uid', function(err, author) {
+ Topics.getTopicField(tid, 'uid', function (err, author) {
callback(err, parseInt(author, 10) === uid);
});
};
- Topics.getUids = function(tid, callback) {
- async.waterfall([
- function(next) {
- Topics.getPids(tid, next);
- },
- function(pids, next) {
- posts.getPostsFields(pids, ['uid'], next);
- },
- function(postData, next) {
- var uids = postData.map(function(post) {
- return post && post.uid;
- }).filter(function(uid, index, array) {
- return uid && array.indexOf(uid) === index;
- });
-
- next(null, uids);
- }
- ], callback);
+ Topics.getUids = function (tid, callback) {
+ db.getSortedSetRevRangeByScore('tid:' + tid + ':posters', 0, -1, '+inf', 1, callback);
};
};
\ No newline at end of file
diff --git a/src/upgrade.js b/src/upgrade.js
index f6f1ce0093..03c776dce3 100644
--- a/src/upgrade.js
+++ b/src/upgrade.js
@@ -6,20 +6,20 @@ var db = require('./database'),
Upgrade = {},
- minSchemaDate = Date.UTC(2015, 7, 18), // This value gets updated every new MINOR version
+ minSchemaDate = Date.UTC(2015, 10, 6), // This value gets updated every new MAJOR version
schemaDate, thisSchemaDate,
// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema
- latestSchema = Date.UTC(2016, 1, 25);
+ latestSchema = Date.UTC(2016, 9, 8);
-Upgrade.check = function(callback) {
- db.get('schemaDate', function(err, value) {
+Upgrade.check = function (callback) {
+ db.get('schemaDate', function (err, value) {
if (err) {
return callback(err);
}
if (!value) {
- db.set('schemaDate', latestSchema, function(err) {
+ db.set('schemaDate', latestSchema, function (err) {
if (err) {
return callback(err);
}
@@ -33,21 +33,25 @@ Upgrade.check = function(callback) {
});
};
-Upgrade.update = function(schemaDate, callback) {
+Upgrade.update = function (schemaDate, callback) {
db.set('schemaDate', schemaDate, callback);
};
-Upgrade.upgrade = function(callback) {
+Upgrade.upgrade = function (callback) {
var updatesMade = false;
winston.info('Beginning database schema update');
async.series([
- function(next) {
+ function (next) {
// Prepare for upgrade & check to make sure the upgrade is possible
- db.get('schemaDate', function(err, value) {
+ db.get('schemaDate', function (err, value) {
+ if (err) {
+ return next(err);
+ }
+
if(!value) {
- db.set('schemaDate', latestSchema, function() {
+ db.set('schemaDate', latestSchema, function () {
next();
});
schemaDate = latestSchema;
@@ -62,96 +66,14 @@ Upgrade.upgrade = function(callback) {
}
});
},
- function(next) {
- thisSchemaDate = Date.UTC(2015, 8, 30);
- if (schemaDate < thisSchemaDate) {
- updatesMade = true;
- winston.info('[2015/09/30] Converting default Gravatar image to default User Avatar');
-
- async.waterfall([
- async.apply(db.isObjectField, 'config', 'customGravatarDefaultImage'),
- function(keyExists, _next) {
- if (keyExists) {
- _next();
- } else {
- winston.info('[2015/09/30] Converting default Gravatar image to default User Avatar skipped');
- Upgrade.update(thisSchemaDate, next);
- next();
- }
- },
- async.apply(db.getObjectField, 'config', 'customGravatarDefaultImage'),
- async.apply(db.setObjectField, 'config', 'defaultAvatar'),
- async.apply(db.deleteObjectField, 'config', 'customGravatarDefaultImage')
- ], function(err) {
- if (err) {
- return next(err);
- }
-
- winston.info('[2015/09/30] Converting default Gravatar image to default User Avatar done');
- Upgrade.update(thisSchemaDate, next);
- });
- } else {
- winston.info('[2015/09/30] Converting default Gravatar image to default User Avatar skipped');
- next();
- }
- },
- function(next) {
- thisSchemaDate = Date.UTC(2015, 10, 6);
- if (schemaDate < thisSchemaDate) {
- updatesMade = true;
- winston.info('[2015/11/06] Removing gravatar');
-
- db.getSortedSetRange('users:joindate', 0, -1, function(err, uids) {
- if (err) {
- return next(err);
- }
-
- async.eachLimit(uids, 500, function(uid, next) {
- db.getObjectFields('user:' + uid, ['picture', 'gravatarpicture'], function(err, userData) {
- if (err) {
- return next(err);
- }
-
- if (!userData.picture || !userData.gravatarpicture) {
- return next();
- }
-
- if (userData.gravatarpicture === userData.picture) {
- async.series([
- function (next) {
- db.setObjectField('user:' + uid, 'picture', '', next);
- },
- function (next) {
- db.deleteObjectField('user:' + uid, 'gravatarpicture', next);
- }
- ], next);
- } else {
- db.deleteObjectField('user:' + uid, 'gravatarpicture', next);
- }
- });
- }, function(err) {
- if (err) {
- return next(err);
- }
-
- winston.info('[2015/11/06] Gravatar pictures removed!');
- Upgrade.update(thisSchemaDate, next);
- });
- });
-
- } else {
- winston.info('[2015/11/06] Gravatar removal skipped');
- next();
- }
- },
- function(next) {
+ function (next) {
thisSchemaDate = Date.UTC(2015, 11, 15);
if (schemaDate < thisSchemaDate) {
updatesMade = true;
winston.info('[2015/12/15] Upgrading chats');
- db.getObjectFields('global', ['nextMid', 'nextChatRoomId'], function(err, globalData) {
+ db.getObjectFields('global', ['nextMid', 'nextChatRoomId'], function (err, globalData) {
if (err) {
return next(err);
}
@@ -160,16 +82,16 @@ Upgrade.upgrade = function(callback) {
var roomId = globalData.nextChatRoomId || 1;
var currentMid = 1;
- async.whilst(function() {
+ async.whilst(function () {
return currentMid <= globalData.nextMid;
- }, function(next) {
- db.getObject('message:' + currentMid, function(err, message) {
+ }, function (next) {
+ db.getObject('message:' + currentMid, function (err, message) {
function addMessageToUids(roomId, callback) {
async.parallel([
- function(next) {
+ function (next) {
db.sortedSetAdd('uid:' + message.fromuid + ':chat:room:' + roomId + ':mids', msgTime, currentMid, next);
},
- function(next) {
+ function (next) {
db.sortedSetAdd('uid:' + message.touid + ':chat:room:' + roomId + ':mids', msgTime, currentMid, next);
}
], callback);
@@ -186,7 +108,7 @@ Upgrade.upgrade = function(callback) {
if (rooms[pairID]) {
winston.info('adding message ' + currentMid + ' to existing roomID ' + roomId);
- addMessageToUids(rooms[pairID], function(err) {
+ addMessageToUids(rooms[pairID], function (err) {
if (err) {
return next(err);
}
@@ -196,19 +118,19 @@ Upgrade.upgrade = function(callback) {
} else {
winston.info('adding message ' + currentMid + ' to new roomID ' + roomId);
async.parallel([
- function(next) {
+ function (next) {
db.sortedSetAdd('uid:' + message.fromuid + ':chat:rooms', msgTime, roomId, next);
},
- function(next) {
+ function (next) {
db.sortedSetAdd('uid:' + message.touid + ':chat:rooms', msgTime, roomId, next);
},
- function(next) {
+ function (next) {
db.sortedSetAdd('chat:room:' + roomId + ':uids', [msgTime, msgTime + 1], [message.fromuid, message.touid], next);
},
- function(next) {
+ function (next) {
addMessageToUids(roomId, next);
}
- ], function(err) {
+ ], function (err) {
if (err) {
return next(err);
}
@@ -219,7 +141,7 @@ Upgrade.upgrade = function(callback) {
});
}
});
- }, function(err) {
+ }, function (err) {
if (err) {
return next(err);
}
@@ -233,22 +155,22 @@ Upgrade.upgrade = function(callback) {
next();
}
},
- function(next) {
+ function (next) {
thisSchemaDate = Date.UTC(2015, 11, 23);
if (schemaDate < thisSchemaDate) {
updatesMade = true;
winston.info('[2015/12/23] Upgrading chat room hashes');
- db.getObjectField('global', 'nextChatRoomId', function(err, nextChatRoomId) {
+ db.getObjectField('global', 'nextChatRoomId', function (err, nextChatRoomId) {
if (err) {
return next(err);
}
var currentChatRoomId = 1;
- async.whilst(function() {
+ async.whilst(function () {
return currentChatRoomId <= nextChatRoomId;
- }, function(next) {
- db.getSortedSetRange('chat:room:' + currentChatRoomId + ':uids', 0, 0, function(err, uids) {
+ }, function (next) {
+ db.getSortedSetRange('chat:room:' + currentChatRoomId + ':uids', 0, 0, function (err, uids) {
if (err) {
return next(err);
}
@@ -257,7 +179,7 @@ Upgrade.upgrade = function(callback) {
return next();
}
- db.setObject('chat:room:' + currentChatRoomId, {owner: uids[0], roomId: currentChatRoomId}, function(err) {
+ db.setObject('chat:room:' + currentChatRoomId, {owner: uids[0], roomId: currentChatRoomId}, function (err) {
if (err) {
return next(err);
}
@@ -265,7 +187,7 @@ Upgrade.upgrade = function(callback) {
next();
});
});
- }, function(err) {
+ }, function (err) {
if (err) {
return next(err);
}
@@ -279,7 +201,7 @@ Upgrade.upgrade = function(callback) {
next();
}
},
- function(next) {
+ function (next) {
thisSchemaDate = Date.UTC(2016, 0, 11);
if (schemaDate < thisSchemaDate) {
@@ -289,7 +211,7 @@ Upgrade.upgrade = function(callback) {
async.waterfall([
async.apply(db.getObjectField, 'config', 'theme:id'),
async.apply(db.sortedSetAdd, 'plugins:active', 0)
- ], function(err) {
+ ], function (err) {
if (err) {
return next(err);
}
@@ -302,7 +224,7 @@ Upgrade.upgrade = function(callback) {
next();
}
},
- function(next) {
+ function (next) {
thisSchemaDate = Date.UTC(2016, 0, 14);
if (schemaDate < thisSchemaDate) {
@@ -311,9 +233,9 @@ Upgrade.upgrade = function(callback) {
var batch = require('./batch');
- batch.processSortedSet('posts:pid', function(ids, next) {
- async.eachSeries(ids, function(id, next) {
- db.getObjectFields('post:' + id, ['pid', 'uid', 'votes'], function(err, postData) {
+ batch.processSortedSet('posts:pid', function (ids, next) {
+ async.eachSeries(ids, function (id, next) {
+ db.getObjectFields('post:' + id, ['pid', 'uid', 'votes'], function (err, postData) {
if (err) {
return next(err);
}
@@ -324,7 +246,7 @@ Upgrade.upgrade = function(callback) {
db.sortedSetAdd('uid:' + postData.uid + ':posts:votes', postData.votes, postData.pid, next);
});
}, next);
- }, {}, function(err) {
+ }, {}, function (err) {
if (err) {
return next(err);
}
@@ -336,7 +258,7 @@ Upgrade.upgrade = function(callback) {
next();
}
},
- function(next) {
+ function (next) {
thisSchemaDate = Date.UTC(2016, 0, 20);
if (schemaDate < thisSchemaDate) {
@@ -345,9 +267,9 @@ Upgrade.upgrade = function(callback) {
var batch = require('./batch');
var now = Date.now();
- batch.processSortedSet('users:joindate', function(ids, next) {
- async.eachSeries(ids, function(id, next) {
- db.getObjectFields('user:' + id, ['uid', 'email:confirmed'], function(err, userData) {
+ batch.processSortedSet('users:joindate', function (ids, next) {
+ async.eachSeries(ids, function (id, next) {
+ db.getObjectFields('user:' + id, ['uid', 'email:confirmed'], function (err, userData) {
if (err) {
return next(err);
}
@@ -358,7 +280,7 @@ Upgrade.upgrade = function(callback) {
db.sortedSetAdd('users:notvalidated', now, userData.uid, next);
});
}, next);
- }, {}, function(err) {
+ }, {}, function (err) {
if (err) {
return next(err);
}
@@ -370,7 +292,7 @@ Upgrade.upgrade = function(callback) {
next();
}
},
- function(next) {
+ function (next) {
thisSchemaDate = Date.UTC(2016, 0, 23);
if (schemaDate < thisSchemaDate) {
@@ -398,7 +320,7 @@ Upgrade.upgrade = function(callback) {
function (groupData, next) {
groups.show('Global Moderators', next);
}
- ], function(err) {
+ ], function (err) {
if (err) {
return next(err);
}
@@ -411,7 +333,7 @@ Upgrade.upgrade = function(callback) {
next();
}
},
- function(next) {
+ function (next) {
thisSchemaDate = Date.UTC(2016, 1, 25);
if (schemaDate < thisSchemaDate) {
@@ -426,7 +348,7 @@ Upgrade.upgrade = function(callback) {
function (next) {
db.deleteObjectField('config', 'disableSocialButtons', next);
}
- ], function(err) {
+ ], function (err) {
if (err) {
return next(err);
}
@@ -438,10 +360,557 @@ Upgrade.upgrade = function(callback) {
winston.info('[2016/02/25] Social: Post Sharing skipped!');
next();
}
+ },
+ function (next) {
+ thisSchemaDate = Date.UTC(2016, 3, 14);
+
+ if (schemaDate < thisSchemaDate) {
+ updatesMade = true;
+ winston.info('[2016/04/14] Group title from settings to user profile');
+
+ var user = require('./user');
+ var batch = require('./batch');
+ var count = 0;
+ batch.processSortedSet('users:joindate', function (uids, next) {
+ winston.info('upgraded ' + count + ' users');
+ user.getMultipleUserSettings(uids, function (err, settings) {
+ if (err) {
+ return next(err);
+ }
+ count += uids.length;
+ settings = settings.filter(function (setting) {
+ return setting && setting.groupTitle;
+ });
+
+ async.each(settings, function (setting, next) {
+ db.setObjectField('user:' + setting.uid, 'groupTitle', setting.groupTitle, next);
+ }, next);
+ });
+ }, {}, function (err) {
+ if (err) {
+ return next(err);
+ }
+
+ winston.info('[2016/04/14] Group title from settings to user profile done');
+ Upgrade.update(thisSchemaDate, next);
+ });
+ } else {
+ winston.info('[2016/04/14] Group title from settings to user profile skipped!');
+ next();
+ }
+ },
+ function (next) {
+ thisSchemaDate = Date.UTC(2016, 3, 18);
+
+ if (schemaDate < thisSchemaDate) {
+ updatesMade = true;
+ winston.info('[2016/04/19] Users post count per tid');
+
+ var batch = require('./batch');
+ var topics = require('./topics');
+ var count = 0;
+ batch.processSortedSet('topics:tid', function (tids, next) {
+ winston.info('upgraded ' + count + ' topics');
+ count += tids.length;
+ async.each(tids, function (tid, next) {
+ db.delete('tid:' + tid + ':posters', function (err) {
+ if (err) {
+ return next(err);
+ }
+ topics.getPids(tid, function (err, pids) {
+ if (err) {
+ return next(err);
+ }
+
+ if (!pids.length) {
+ return next();
+ }
+
+ async.eachSeries(pids, function (pid, next) {
+ db.getObjectField('post:' + pid, 'uid', function (err, uid) {
+ if (err) {
+ return next(err);
+ }
+ if (!parseInt(uid, 10)) {
+ return next();
+ }
+ db.sortedSetIncrBy('tid:' + tid + ':posters', 1, uid, next);
+ });
+ }, next);
+ });
+ });
+ }, next);
+ }, {}, function (err) {
+ if (err) {
+ return next(err);
+ }
+
+ winston.info('[2016/04/19] Users post count per tid done');
+ Upgrade.update(thisSchemaDate, next);
+ });
+ } else {
+ winston.info('[2016/04/19] Users post count per tid skipped!');
+ next();
+ }
+ },
+ function (next) {
+ thisSchemaDate = Date.UTC(2016, 3, 29);
+
+ if (schemaDate < thisSchemaDate) {
+ updatesMade = true;
+ winston.info('[2016/04/29] Dismiss flags from deleted topics');
+
+ var posts = require('./posts'),
+ topics = require('./topics');
+
+ var pids, tids;
+
+ async.waterfall([
+ async.apply(db.getSortedSetRange, 'posts:flagged', 0, -1),
+ function (_pids, next) {
+ pids = _pids;
+ posts.getPostsFields(pids, ['tid'], next);
+ },
+ function (_tids, next) {
+ tids = _tids.map(function (a) {
+ return a.tid;
+ });
+
+ topics.getTopicsFields(tids, ['deleted'], next);
+ },
+ function (state, next) {
+ var toDismiss = state.map(function (a, idx) {
+ return parseInt(a.deleted, 10) === 1 ? pids[idx] : null;
+ }).filter(Boolean);
+
+ winston.info('[2016/04/29] ' + toDismiss.length + ' dismissable flags found');
+ async.each(toDismiss, posts.dismissFlag, next);
+ }
+ ], function (err) {
+ if (err) {
+ return next(err);
+ }
+
+ winston.info('[2016/04/29] Dismiss flags from deleted topics done');
+ Upgrade.update(thisSchemaDate, next);
+ });
+ } else {
+ winston.info('[2016/04/29] Dismiss flags from deleted topics skipped!');
+ next();
+ }
+ },
+ function (next) {
+ thisSchemaDate = Date.UTC(2016, 4, 28);
+
+ if (schemaDate < thisSchemaDate) {
+ updatesMade = true;
+ winston.info('[2016/05/28] Giving topics:read privs to any group that was previously allowed to Find & Access Category');
+
+ var groupsAPI = require('./groups');
+ var privilegesAPI = require('./privileges');
+
+ db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) {
+ if (err) {
+ return next(err);
+ }
+
+ async.eachSeries(cids, function (cid, next) {
+ privilegesAPI.categories.list(cid, function (err, data) {
+ if (err) {
+ return next(err);
+ }
+
+ var groups = data.groups;
+ var users = data.users;
+
+ async.waterfall([
+ function (next) {
+ async.eachSeries(groups, function (group, next) {
+ if (group.privileges['groups:read']) {
+ return groupsAPI.join('cid:' + cid + ':privileges:groups:topics:read', group.name, function (err) {
+ if (!err) {
+ winston.info('cid:' + cid + ':privileges:groups:topics:read granted to gid: ' + group.name);
+ }
+
+ return next(err);
+ });
+ }
+
+ next(null);
+ }, next);
+ },
+ function (next) {
+ async.eachSeries(users, function (user, next) {
+ if (user.privileges.read) {
+ return groupsAPI.join('cid:' + cid + ':privileges:topics:read', user.uid, function (err) {
+ if (!err) {
+ winston.info('cid:' + cid + ':privileges:topics:read granted to uid: ' + user.uid);
+ }
+
+ return next(err);
+ });
+ }
+
+ next(null);
+ }, next);
+ }
+ ], function (err) {
+ if (!err) {
+ winston.info('-- cid ' + cid + ' upgraded');
+ }
+
+ next(err);
+ });
+ });
+ }, function (err) {
+ if (err) {
+ return next(err);
+ }
+
+ winston.info('[2016/05/28] Giving topics:read privs to any group that was previously allowed to Find & Access Category - done');
+ Upgrade.update(thisSchemaDate, next);
+ });
+ });
+ } else {
+ winston.info('[2016/05/28] Giving topics:read privs to any group that was previously allowed to Find & Access Category - skipped!');
+ next();
+ }
+ },
+ function (next) {
+ thisSchemaDate = Date.UTC(2016, 5, 13);
+
+ if (schemaDate < thisSchemaDate) {
+ updatesMade = true;
+ winston.info('[2016/06/13] Store upvotes/downvotes separately');
+
+ var batch = require('./batch');
+ var posts = require('./posts');
+ var count = 0;
+ batch.processSortedSet('posts:pid', function (pids, next) {
+ winston.info('upgraded ' + count + ' posts');
+ count += pids.length;
+ async.each(pids, function (pid, next) {
+ async.parallel({
+ upvotes: function (next) {
+ db.setCount('pid:' + pid + ':upvote', next);
+ },
+ downvotes: function (next) {
+ db.setCount('pid:' + pid + ':downvote', next);
+ }
+ }, function (err, results) {
+ if (err) {
+ return next(err);
+ }
+ var data = {};
+
+ if (parseInt(results.upvotes, 10) > 0) {
+ data.upvotes = results.upvotes;
+ }
+ if (parseInt(results.downvotes, 10) > 0) {
+ data.downvotes = results.downvotes;
+ }
+
+ if (Object.keys(data).length) {
+ posts.setPostFields(pid, data, next);
+ } else {
+ next();
+ }
+ }, next);
+ }, next);
+ }, {}, function (err) {
+ if (err) {
+ return next(err);
+ }
+
+ winston.info('[2016/06/13] Store upvotes/downvotes separately done');
+ Upgrade.update(thisSchemaDate, next);
+ });
+ } else {
+ winston.info('[2016/06/13] Store upvotes/downvotes separately skipped!');
+ next();
+ }
+ },
+ function (next) {
+ thisSchemaDate = Date.UTC(2016, 6, 12);
+
+ if (schemaDate < thisSchemaDate) {
+ updatesMade = true;
+ winston.info('[2016/07/12] Giving upload privileges');
+ var privilegesAPI = require('./privileges');
+ var meta = require('./meta');
+
+ db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) {
+ if (err) {
+ return next(err);
+ }
+
+ async.eachSeries(cids, function (cid, next) {
+ privilegesAPI.categories.list(cid, function (err, data) {
+ if (err) {
+ return next(err);
+ }
+ async.eachSeries(data.groups, function (group, next) {
+ if (group.name === 'guests' && parseInt(meta.config.allowGuestUploads, 10) !== 1) {
+ return next();
+ }
+ if (group.privileges['groups:read']) {
+ privilegesAPI.categories.give(['upload:post:image'], cid, group.name, next);
+ } else {
+ next();
+ }
+ }, next);
+ });
+ }, function (err) {
+ if (err) {
+ return next(err);
+ }
+
+ winston.info('[2016/07/12] Upload privileges done');
+ Upgrade.update(thisSchemaDate, next);
+ });
+ });
+ } else {
+ winston.info('[2016/07/12] Upload privileges skipped!');
+ next();
+ }
+ },
+ function (next) {
+ thisSchemaDate = Date.UTC(2016, 7, 5);
+
+ if (schemaDate < thisSchemaDate) {
+ updatesMade = true;
+ winston.info('[2016/08/05] Removing best posts with negative scores');
+ var batch = require('./batch');
+ batch.processSortedSet('users:joindate', function (ids, next) {
+ async.each(ids, function (id, next) {
+ console.log('processing uid ' + id);
+ db.sortedSetsRemoveRangeByScore(['uid:' + id + ':posts:votes'], '-inf', 0, next);
+ }, next);
+ }, {}, function (err) {
+ if (err) {
+ return next(err);
+ }
+ winston.info('[2016/08/05] Removing best posts with negative scores done!');
+ Upgrade.update(thisSchemaDate, next);
+ });
+
+ } else {
+ winston.info('[2016/08/05] Removing best posts with negative scores skipped!');
+ next();
+ }
+ },
+ function (next) {
+ thisSchemaDate = Date.UTC(2016, 8, 7);
+
+ if (schemaDate < thisSchemaDate) {
+ updatesMade = true;
+ winston.info('[2016/08/07] Granting edit/delete/delete topic on existing categories');
+
+ var groupsAPI = require('./groups');
+ var privilegesAPI = require('./privileges');
+
+ db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) {
+ if (err) {
+ return next(err);
+ }
+
+ async.eachSeries(cids, function (cid, next) {
+ privilegesAPI.categories.list(cid, function (err, data) {
+ if (err) {
+ return next(err);
+ }
+
+ var groups = data.groups;
+ var users = data.users;
+
+ async.waterfall([
+ function (next) {
+ async.eachSeries(groups, function (group, next) {
+ if (group.privileges['groups:topics:reply']) {
+ return async.parallel([
+ async.apply(groupsAPI.join, 'cid:' + cid + ':privileges:groups:posts:edit', group.name),
+ async.apply(groupsAPI.join, 'cid:' + cid + ':privileges:groups:posts:delete', group.name)
+ ], function (err) {
+ if (!err) {
+ winston.info('cid:' + cid + ':privileges:groups:posts:edit, cid:' + cid + ':privileges:groups:posts:delete granted to gid: ' + group.name);
+ }
+
+ return next(err);
+ });
+ }
+
+ next(null);
+ }, next);
+ },
+ function (next) {
+ async.eachSeries(groups, function (group, next) {
+ if (group.privileges['groups:topics:create']) {
+ return groupsAPI.join('cid:' + cid + ':privileges:groups:topics:delete', group.name, function (err) {
+ if (!err) {
+ winston.info('cid:' + cid + ':privileges:groups:topics:delete granted to gid: ' + group.name);
+ }
+
+ return next(err);
+ });
+ }
+
+ next(null);
+ }, next);
+ },
+ function (next) {
+ async.eachSeries(users, function (user, next) {
+ if (user.privileges['topics:reply']) {
+ return async.parallel([
+ async.apply(groupsAPI.join, 'cid:' + cid + ':privileges:posts:edit', user.uid),
+ async.apply(groupsAPI.join, 'cid:' + cid + ':privileges:posts:delete', user.uid)
+ ], function (err) {
+ if (!err) {
+ winston.info('cid:' + cid + ':privileges:posts:edit, cid:' + cid + ':privileges:posts:delete granted to uid: ' + user.uid);
+ }
+
+ return next(err);
+ });
+ }
+
+ next(null);
+ }, next);
+ },
+ function (next) {
+ async.eachSeries(users, function (user, next) {
+ if (user.privileges['topics:create']) {
+ return groupsAPI.join('cid:' + cid + ':privileges:topics:delete', user.uid, function (err) {
+ if (!err) {
+ winston.info('cid:' + cid + ':privileges:topics:delete granted to uid: ' + user.uid);
+ }
+
+ return next(err);
+ });
+ }
+
+ next(null);
+ }, next);
+ }
+ ], function (err) {
+ if (!err) {
+ winston.info('-- cid ' + cid + ' upgraded');
+ }
+
+ next(err);
+ });
+ });
+ }, function (err) {
+ if (err) {
+ return next(err);
+ }
+
+ winston.info('[2016/08/07] Granting edit/delete/delete topic on existing categories - done');
+ Upgrade.update(thisSchemaDate, next);
+ });
+ });
+ } else {
+ winston.info('[2016/08/07] Granting edit/delete/delete topic on existing categories - skipped!');
+ next();
+ }
+ },
+ function (next) {
+ thisSchemaDate = Date.UTC(2016, 8, 22);
+
+ if (schemaDate < thisSchemaDate) {
+ updatesMade = true;
+ winston.info('[2016/09/22] Setting category recent tids');
+
+
+ db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) {
+ if (err) {
+ return next(err);
+ }
+
+ async.eachSeries(cids, function (cid, next) {
+ db.getSortedSetRevRange('cid:' + cid + ':pids', 0, 0, function (err, pid) {
+ if (err || !pid) {
+ return next(err);
+ }
+ db.getObjectFields('post:' + pid, ['tid', 'timestamp'], function (err, postData) {
+ if (err || !postData || !postData.tid) {
+ return next(err);
+ }
+ db.sortedSetAdd('cid:' + cid + ':recent_tids', postData.timestamp, postData.tid, next);
+ });
+ });
+ }, function (err) {
+ if (err) {
+ return next(err);
+ }
+
+ winston.info('[2016/09/22] Setting category recent tids - done');
+ Upgrade.update(thisSchemaDate, next);
+ });
+ });
+ } else {
+ winston.info('[2016/09/22] Setting category recent tids - skipped!');
+ next();
+ }
+ },
+ function (next) {
+ function upgradePosts(next) {
+ var batch = require('./batch');
+
+ batch.processSortedSet('posts:pid', function (ids, next) {
+ async.each(ids, function (id, next) {
+ console.log('processing pid ' + id);
+ async.waterfall([
+ function (next) {
+ db.rename('pid:' + id + ':users_favourited', 'pid:' + id + ':users_bookmarked', next);
+ },
+ function (next) {
+ db.getObjectField('post:' + id, 'reputation', next);
+ },
+ function (reputation, next) {
+ if (parseInt(reputation, 10)) {
+ db.setObjectField('post:' + id, 'bookmarks', reputation, next);
+ } else {
+ next();
+ }
+ },
+ function (next) {
+ db.deleteObjectField('post:' + id, 'reputation', next);
+ }
+ ], next);
+ }, next);
+ }, {}, next);
+ }
+
+ function upgradeUsers(next) {
+ var batch = require('./batch');
+
+ batch.processSortedSet('users:joindate', function (ids, next) {
+ async.each(ids, function (id, next) {
+ console.log('processing uid ' + id);
+ db.rename('uid:' + id + ':favourites', 'uid:' + id + ':bookmarks', next);
+ }, next);
+ }, {}, next);
+ }
+
+ thisSchemaDate = Date.UTC(2016, 9, 8);
+
+ if (schemaDate < thisSchemaDate) {
+ updatesMade = true;
+ winston.info('[2016/10/8] favourite -> bookmark refactor');
+ async.series([upgradePosts, upgradeUsers], function (err) {
+ if (err) {
+ return next(err);
+ }
+ winston.info('[2016/08/05] favourite- bookmark refactor done!');
+ Upgrade.update(thisSchemaDate, next);
+ });
+ } else {
+ winston.info('[2016/10/8] favourite -> bookmark refactor - skipped!');
+ next();
+ }
}
// Add new schema updates here
// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema IN LINE 24!!!
- ], function(err) {
+ ], function (err) {
if (!err) {
if(updatesMade) {
winston.info('[upgrade] Schema update complete!');
diff --git a/src/user.js b/src/user.js
index 19c60fc1fd..7cc01c89f3 100644
--- a/src/user.js
+++ b/src/user.js
@@ -1,14 +1,16 @@
'use strict';
-var async = require('async'),
+var async = require('async');
+var _ = require('underscore');
- plugins = require('./plugins'),
- db = require('./database'),
- topics = require('./topics'),
- privileges = require('./privileges'),
- utils = require('../public/src/utils');
+var groups = require('./groups');
+var plugins = require('./plugins');
+var db = require('./database');
+var topics = require('./topics');
+var privileges = require('./privileges');
+var meta = require('./meta');
-(function(User) {
+(function (User) {
User.email = require('./user/email');
User.notifications = require('./user/notifications');
@@ -19,6 +21,7 @@ var async = require('async'),
require('./user/auth')(User);
require('./user/create')(User);
require('./user/posts')(User);
+ require('./user/topics')(User);
require('./user/categories')(User);
require('./user/follow')(User);
require('./user/profile')(User);
@@ -31,10 +34,11 @@ var async = require('async'),
require('./user/approval')(User);
require('./user/invite')(User);
require('./user/password')(User);
+ require('./user/info')(User);
- User.updateLastOnlineTime = function(uid, callback) {
- callback = callback || function() {};
- User.getUserFields(uid, ['status', 'lastonline'], function(err, userData) {
+ User.updateLastOnlineTime = function (uid, callback) {
+ callback = callback || function () {};
+ db.getObjectFields('user:' + uid, ['status', 'lastonline'], function (err, userData) {
var now = Date.now();
if (err || userData.status === 'offline' || now - parseInt(userData.lastonline, 10) < 300000) {
return callback(err);
@@ -43,21 +47,21 @@ var async = require('async'),
});
};
- User.updateOnlineUsers = function(uid, callback) {
- callback = callback || function() {};
+ User.updateOnlineUsers = function (uid, callback) {
+ callback = callback || function () {};
var now = Date.now();
async.waterfall([
- function(next) {
+ function (next) {
db.sortedSetScore('users:online', uid, next);
},
- function(userOnlineTime, next) {
+ function (userOnlineTime, next) {
if (now - parseInt(userOnlineTime, 10) < 300000) {
return callback();
}
db.sortedSetAdd('users:online', now, uid, next);
},
- function(next) {
+ function (next) {
topics.pushUnreadCount(uid);
plugins.fireHook('action:user.online', {uid: uid, timestamp: now});
next();
@@ -65,7 +69,7 @@ var async = require('async'),
], callback);
};
- User.getUidsFromSet = function(set, start, stop, callback) {
+ User.getUidsFromSet = function (set, start, stop, callback) {
if (set === 'users:online') {
var count = parseInt(stop, 10) === -1 ? stop : stop - start + 1;
var now = Date.now();
@@ -75,47 +79,45 @@ var async = require('async'),
}
};
- User.getUsersFromSet = function(set, uid, start, stop, callback) {
+ User.getUsersFromSet = function (set, uid, start, stop, callback) {
async.waterfall([
- function(next) {
+ function (next) {
User.getUidsFromSet(set, start, stop, next);
},
- function(uids, next) {
+ function (uids, next) {
User.getUsers(uids, uid, next);
}
], callback);
};
- User.getUsers = function(uids, uid, callback) {
- var fields = ['uid', 'username', 'userslug', 'picture', 'status', 'banned', 'joindate', 'postcount', 'reputation', 'email:confirmed', 'lastonline'];
-
+ User.getUsersWithFields = function (uids, fields, uid, callback) {
async.waterfall([
function (next) {
plugins.fireHook('filter:users.addFields', {fields: fields}, next);
},
function (data, next) {
- data.fields = data.fields.filter(function(field, index, array) {
+ data.fields = data.fields.filter(function (field, index, array) {
return array.indexOf(field) === index;
});
async.parallel({
- userData: function(next) {
+ userData: function (next) {
User.getUsersFields(uids, data.fields, next);
},
- isAdmin: function(next) {
+ isAdmin: function (next) {
User.isAdministrator(uids, next);
}
}, next);
},
function (results, next) {
- results.userData.forEach(function(user, index) {
+ results.userData.forEach(function (user, index) {
if (user) {
user.status = User.getStatus(user);
- user.joindateISO = utils.toISOString(user.joindate);
user.administrator = results.isAdmin[index];
user.banned = parseInt(user.banned, 10) === 1;
+ user.banned_until = parseInt(user['banned:expire'], 10) || 0;
+ user.banned_until_readable = user.banned_until ? new Date(user.banned_until).toString() : 'Not Banned';
user['email:confirmed'] = parseInt(user['email:confirmed'], 10) === 1;
- user.lastonlineISO = utils.toISOString(user.lastonline) || user.joindateISO;
}
});
plugins.fireHook('filter:userlist.get', {users: results.userData, uid: uid}, next);
@@ -126,25 +128,32 @@ var async = require('async'),
], callback);
};
- User.getStatus = function(userData) {
- var isOnline = Date.now() - parseInt(userData.lastonline, 10) < 300000;
+ User.getUsers = function (uids, uid, callback) {
+ var fields = ['uid', 'username', 'userslug', 'picture', 'status', 'flags',
+ 'banned', 'banned:expire', 'joindate', 'postcount', 'reputation', 'email:confirmed', 'lastonline'];
+
+ User.getUsersWithFields(uids, fields, uid, callback);
+ };
+
+ User.getStatus = function (userData) {
+ var isOnline = (Date.now() - parseInt(userData.lastonline, 10)) < 300000;
return isOnline ? (userData.status || 'online') : 'offline';
};
- User.isOnline = function(uid, callback) {
+ User.isOnline = function (uid, callback) {
if (Array.isArray(uid)) {
- db.sortedSetScores('users:online', uid, function(err, lastonline) {
+ db.sortedSetScores('users:online', uid, function (err, lastonline) {
if (err) {
return callback(err);
}
var now = Date.now();
- var isOnline = uid.map(function(uid, index) {
+ var isOnline = uid.map(function (uid, index) {
return now - lastonline[index] < 300000;
});
callback(null, isOnline);
});
} else {
- db.sortedSetScore('users:online', uid, function(err, lastonline) {
+ db.sortedSetScore('users:online', uid, function (err, lastonline) {
if (err) {
return callback(err);
}
@@ -155,41 +164,41 @@ var async = require('async'),
};
- User.exists = function(uid, callback) {
+ User.exists = function (uid, callback) {
db.isSortedSetMember('users:joindate', uid, callback);
};
- User.existsBySlug = function(userslug, callback) {
- User.getUidByUserslug(userslug, function(err, exists) {
+ User.existsBySlug = function (userslug, callback) {
+ User.getUidByUserslug(userslug, function (err, exists) {
callback(err, !! exists);
});
};
- User.getUidByUsername = function(username, callback) {
+ User.getUidByUsername = function (username, callback) {
if (!username) {
return callback(null, 0);
}
db.sortedSetScore('username:uid', username, callback);
};
- User.getUidsByUsernames = function(usernames, callback) {
+ User.getUidsByUsernames = function (usernames, callback) {
db.sortedSetScores('username:uid', usernames, callback);
};
- User.getUidByUserslug = function(userslug, callback) {
+ User.getUidByUserslug = function (userslug, callback) {
if (!userslug) {
return callback(null, 0);
}
db.sortedSetScore('userslug:uid', userslug, callback);
};
- User.getUsernamesByUids = function(uids, callback) {
- User.getUsersFields(uids, ['username'], function(err, users) {
+ User.getUsernamesByUids = function (uids, callback) {
+ User.getUsersFields(uids, ['username'], function (err, users) {
if (err) {
return callback(err);
}
- users = users.map(function(user) {
+ users = users.map(function (user) {
return user.username;
});
@@ -197,23 +206,23 @@ var async = require('async'),
});
};
- User.getUsernameByUserslug = function(slug, callback) {
+ User.getUsernameByUserslug = function (slug, callback) {
async.waterfall([
- function(next) {
+ function (next) {
User.getUidByUserslug(slug, next);
},
- function(uid, next) {
+ function (uid, next) {
User.getUserField(uid, 'username', next);
}
], callback);
};
- User.getUidByEmail = function(email, callback) {
+ User.getUidByEmail = function (email, callback) {
db.sortedSetScore('email:uid', email.toLowerCase(), callback);
};
- User.getUsernameByEmail = function(email, callback) {
- db.sortedSetScore('email:uid', email.toLowerCase(), function(err, uid) {
+ User.getUsernameByEmail = function (email, callback) {
+ db.sortedSetScore('email:uid', email.toLowerCase(), function (err, uid) {
if (err) {
return callback(err);
}
@@ -221,32 +230,38 @@ var async = require('async'),
});
};
- User.isModerator = function(uid, cid, callback) {
+ User.isModerator = function (uid, cid, callback) {
privileges.users.isModerator(uid, cid, callback);
};
- User.isAdministrator = function(uid, callback) {
+ User.isModeratorOfAnyCategory = function (uid, callback) {
+ User.getModeratedCids(uid, function (err, cids) {
+ callback(err, Array.isArray(cids) ? !!cids.length : false);
+ });
+ };
+
+ User.isAdministrator = function (uid, callback) {
privileges.users.isAdministrator(uid, callback);
};
- User.isGlobalModerator = function(uid, callback) {
+ User.isGlobalModerator = function (uid, callback) {
privileges.users.isGlobalModerator(uid, callback);
};
- User.isAdminOrGlobalMod = function(uid, callback) {
+ User.isAdminOrGlobalMod = function (uid, callback) {
async.parallel({
isAdmin: async.apply(User.isAdministrator, uid),
isGlobalMod: async.apply(User.isGlobalModerator, uid)
- }, function(err, results) {
+ }, function (err, results) {
callback(err, results ? (results.isAdmin || results.isGlobalMod) : false);
});
};
- User.isAdminOrSelf = function(callerUid, uid, callback) {
+ User.isAdminOrSelf = function (callerUid, uid, callback) {
if (parseInt(callerUid, 10) === parseInt(uid, 10)) {
return callback();
}
- User.isAdministrator(callerUid, function(err, isAdmin) {
+ User.isAdministrator(callerUid, function (err, isAdmin) {
if (err || !isAdmin) {
return callback(err || new Error('[[error:no-privileges]]'));
}
@@ -254,5 +269,99 @@ var async = require('async'),
});
};
+ User.getAdminsandGlobalMods = function (callback) {
+ async.parallel({
+ admins: async.apply(groups.getMembers, 'administrators', 0, -1),
+ mods: async.apply(groups.getMembers, 'Global Moderators', 0, -1)
+ }, function (err, results) {
+ if (err) {
+ return callback(err);
+ }
+ var uids = results.admins.concat(results.mods).filter(function (uid, index, array) {
+ return uid && array.indexOf(uid) === index;
+ });
+ User.getUsersData(uids, callback);
+ });
+ };
+
+ User.getAdminsandGlobalModsandModerators = function (callback) {
+ async.parallel([
+ async.apply(groups.getMembers, 'administrators', 0, -1),
+ async.apply(groups.getMembers, 'Global Moderators', 0, -1),
+ async.apply(User.getModeratorUids)
+ ], function (err, results) {
+ if (err) {
+ return callback(err);
+ }
+
+ User.getUsersData(_.union.apply(_, results), callback);
+ });
+ };
+
+ User.getModeratorUids = function (callback) {
+ async.waterfall([
+ async.apply(db.getSortedSetRange, 'categories:cid', 0, -1),
+ function (cids, next) {
+ var groupNames = cids.map(function (cid) {
+ return 'cid:' + cid + ':privileges:mods';
+ });
+
+ groups.getMembersOfGroups(groupNames, function (err, memberSets) {
+ if (err) {
+ return next(err);
+ }
+
+ next(null, _.union.apply(_, memberSets));
+ });
+ }
+ ], callback);
+ };
+
+ User.getModeratedCids = function (uid, callback) {
+ var cids;
+ async.waterfall([
+ function (next) {
+ db.getSortedSetRange('categories:cid', 0, -1, next);
+ },
+ function (_cids, next) {
+ cids = _cids;
+ User.isModerator(uid, cids, next);
+ },
+ function (isMods, next) {
+ cids = cids.filter(function (cid, index) {
+ return cid && isMods[index];
+ });
+ next(null, cids);
+ }
+ ], callback);
+ };
+
+ User.addInterstitials = function (callback) {
+ plugins.registerHook('core', {
+ hook: 'filter:register.interstitial',
+ method: function (data, callback) {
+ if (meta.config.termsOfUse && !data.userData.acceptTos) {
+ data.interstitials.push({
+ template: 'partials/acceptTos',
+ data: {
+ termsOfUse: meta.config.termsOfUse
+ },
+ callback: function (userData, formData, next) {
+ if (formData['agree-terms'] === 'on') {
+ userData.acceptTos = true;
+ }
+
+ next(userData.acceptTos ? null : new Error('[[register:terms_of_use_error]]'));
+ }
+ });
+ }
+
+ callback(null, data);
+ }
+ });
+
+ callback();
+ };
+
}(exports));
diff --git a/src/user/admin.js b/src/user/admin.js
index 004e048c93..6a1eb5f458 100644
--- a/src/user/admin.js
+++ b/src/user/admin.js
@@ -3,11 +3,13 @@
var async = require('async');
var db = require('../database');
+var posts = require('../posts');
var plugins = require('../plugins');
+var winston = require('winston');
-module.exports = function(User) {
+module.exports = function (User) {
- User.logIP = function(uid, ip) {
+ User.logIP = function (uid, ip) {
var now = Date.now();
db.sortedSetAdd('uid:' + uid + ':ip', now, ip || 'Unknown');
if (ip) {
@@ -15,19 +17,18 @@ module.exports = function(User) {
}
};
- User.getIPs = function(uid, stop, callback) {
- db.getSortedSetRevRange('uid:' + uid + ':ip', 0, stop, function(err, ips) {
+ User.getIPs = function (uid, stop, callback) {
+ db.getSortedSetRevRange('uid:' + uid + ':ip', 0, stop, function (err, ips) {
if (err) {
return callback(err);
}
- callback(null, ips.map(function(ip) {
- return {ip:ip};
- }));
+ callback(null, ips);
});
};
- User.getUsersCSV = function(callback) {
+ User.getUsersCSV = function (callback) {
+ winston.info('[user/getUsersCSV] Compiling User CSV data');
var csvContent = '';
async.waterfall([
@@ -35,13 +36,13 @@ module.exports = function(User) {
db.getSortedSetRangeWithScores('username:uid', 0, -1, next);
},
function (users, next) {
- var uids = users.map(function(user) {
+ var uids = users.map(function (user) {
return user.score;
});
User.getUsersFields(uids, ['uid', 'email', 'username'], next);
},
function (usersData, next) {
- usersData.forEach(function(user) {
+ usersData.forEach(function (user) {
if (user) {
csvContent += user.email + ',' + user.username + ',' + user.uid + '\n';
}
@@ -52,29 +53,62 @@ module.exports = function(User) {
], callback);
};
- User.ban = function(uid, callback) {
- async.waterfall([
- function (next) {
- User.setUserField(uid, 'banned', 1, next);
- },
- function (next) {
- db.sortedSetAdd('users:banned', Date.now(), uid, next);
- },
- function (next) {
- plugins.fireHook('action:user.banned', {uid: uid});
- next();
+ User.ban = function (uid, until, reason, callback) {
+ // "until" (optional) is unix timestamp in milliseconds
+ // "reason" (optional) is a string
+ if (!callback && typeof until === 'function') {
+ callback = until;
+ until = 0;
+ reason = '';
+ } else if (!callback && typeof reason === 'function') {
+ callback = reason;
+ reason = '';
+ }
+
+ var now = Date.now();
+
+ until = parseInt(until, 10);
+ if (isNaN(until)) {
+ return callback(new Error('[[error:ban-expiry-missing]]'));
+ }
+
+ var tasks = [
+ async.apply(User.setUserField, uid, 'banned', 1),
+ async.apply(db.sortedSetAdd, 'users:banned', now, uid),
+ async.apply(db.sortedSetAdd, 'uid:' + uid + ':bans', 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);
}
- ], callback);
+
+ plugins.fireHook('action:user.banned', {
+ uid: uid,
+ until: until > 0 ? until : undefined
+ });
+ callback();
+ });
};
- User.unban = function(uid, callback) {
- db.delete('uid:' + uid + ':flagged_by');
+ 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.sortedSetRemove('users:banned', uid, next);
+ db.sortedSetsRemove(['users:banned', 'users:banned:expire'], uid, next);
},
function (next) {
plugins.fireHook('action:user.unbanned', {uid: uid});
@@ -83,13 +117,39 @@ module.exports = function(User) {
], callback);
};
- User.resetFlags = function(uids, 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();
}
- var keys = uids.map(function(uid) {
- return 'uid:' + uid + ':flagged_by';
- });
- db.deleteAll(keys, callback);
+
+ async.eachSeries(uids, function (uid, next) {
+ posts.dismissUserFlags(uid, next);
+ }, callback);
};
};
diff --git a/src/user/approval.js b/src/user/approval.js
index 94e0f097e5..ac54bcfc67 100644
--- a/src/user/approval.js
+++ b/src/user/approval.js
@@ -11,33 +11,35 @@ 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) {
-module.exports = function(User) {
-
- User.addToApprovalQueue = function(userData, callback) {
+ User.addToApprovalQueue = function (userData, callback) {
userData.userslug = utils.slugify(userData.username);
async.waterfall([
- function(next) {
+ function (next) {
User.isDataValid(userData, next);
},
- function(next) {
+ function (next) {
User.hashPassword(userData.password, next);
},
- function(hashedPassword, next) {
+ function (hashedPassword, next) {
var data = {
username: userData.username,
email: userData.email,
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(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);
},
- function(next) {
+ function (next) {
sendNotificationToAdmins(userData.username, next);
}
], callback);
@@ -49,7 +51,7 @@ module.exports = function(User) {
nid: 'new_register:' + username,
path: '/admin/manage/registration',
mergeId: 'new_register'
- }, function(err, notification) {
+ }, function (err, notification) {
if (err || !notification) {
return callback(err);
}
@@ -58,27 +60,27 @@ module.exports = function(User) {
});
}
- User.acceptRegistration = function(username, callback) {
+ User.acceptRegistration = function (username, callback) {
var uid;
var userData;
async.waterfall([
- function(next) {
+ function (next) {
db.getObject('registration:queue:name:' + username, next);
},
- function(_userData, next) {
+ function (_userData, next) {
if (!_userData) {
return callback(new Error('[[error:invalid-data]]'));
}
userData = _userData;
User.create(userData, next);
},
- function(_uid, next) {
+ function (_uid, next) {
uid = _uid;
User.setUserField(uid, 'password', userData.hashedPassword, next);
},
- function(next) {
+ function (next) {
var title = meta.config.title || meta.config.browserTitle || 'NodeBB';
- translator.translate('[[email:welcome-to, ' + title + ']]', meta.config.defaultLang, function(subject) {
+ translator.translate('[[email:welcome-to, ' + title + ']]', meta.config.defaultLang, function (subject) {
var data = {
site_title: title,
username: username,
@@ -90,10 +92,10 @@ module.exports = function(User) {
emailer.send('registration_accepted', uid, data, next);
});
},
- function(next) {
+ function (next) {
removeFromQueue(username, next);
},
- function(next) {
+ function (next) {
markNotificationRead(username, next);
}
], callback);
@@ -106,14 +108,14 @@ module.exports = function(User) {
groups.getMembers('administrators', 0, -1, next);
},
function (uids, next) {
- async.each(uids, function(uid, next) {
+ async.each(uids, function (uid, next) {
notifications.markRead(nid, uid, next);
}, next);
}
], callback);
}
- User.rejectRegistration = function(username, callback) {
+ User.rejectRegistration = function (username, callback) {
async.waterfall([
function (next) {
removeFromQueue(username, next);
@@ -128,26 +130,26 @@ module.exports = function(User) {
async.parallel([
async.apply(db.sortedSetRemove, 'registration:queue', username),
async.apply(db.delete, 'registration:queue:name:' + username)
- ], function(err, results) {
+ ], function (err) {
callback(err);
});
}
- User.getRegistrationQueue = function(start, stop, callback) {
+ User.getRegistrationQueue = function (start, stop, callback) {
var data;
async.waterfall([
- function(next) {
+ function (next) {
db.getSortedSetRevRangeWithScores('registration:queue', start, stop, next);
},
- function(_data, next) {
+ function (_data, next) {
data = _data;
- var keys = data.filter(Boolean).map(function(user) {
+ var keys = data.filter(Boolean).map(function (user) {
return 'registration:queue:name:' + user.value;
});
db.getObjects(keys, next);
},
- function(users, next) {
- users = users.map(function(user, index) {
+ function (users, next) {
+ users = users.map(function (user, index) {
if (!user) {
return null;
}
@@ -158,7 +160,7 @@ module.exports = function(User) {
return user;
}).filter(Boolean);
- async.map(users, function(user, next) {
+ async.map(users, function (user, next) {
if (!user) {
return next(null, user);
}
@@ -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) {
- user.spamData = body;
- user.usernameSpam = body.username.frequency > 0 || body.username.appears > 0;
- user.emailSpam = body.email.frequency > 0 || body.email.appears > 0;
- user.ipSpam = body.ip.frequency > 0 || body.ip.appears > 0;
- }
+ 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 99bdfb8c94..a60f59fea4 100644
--- a/src/user/auth.js
+++ b/src/user/auth.js
@@ -1,16 +1,16 @@
'use strict';
-var async = require('async'),
- winston = require('winston'),
- db = require('../database'),
- meta = require('../meta'),
- events = require('../events');
+var async = require('async');
+var winston = require('winston');
+var db = require('../database');
+var meta = require('../meta');
+var events = require('../events');
-module.exports = function(User) {
+module.exports = function (User) {
User.auth = {};
- User.auth.logAttempt = function(uid, ip, callback) {
- db.exists('lockout:' + uid, function(err, exists) {
+ User.auth.logAttempt = function (uid, ip, callback) {
+ db.exists('lockout:' + uid, function (err, exists) {
if (err) {
return callback(err);
}
@@ -19,14 +19,14 @@ module.exports = function(User) {
return callback(new Error('[[error:account-locked]]'));
}
- db.increment('loginAttempts:' + uid, function(err, attempts) {
+ db.increment('loginAttempts:' + uid, function (err, attempts) {
if (err) {
return callback(err);
}
if ((meta.config.loginAttempts || 5) < attempts) {
// Lock out the account
- db.set('lockout:' + uid, '', function(err) {
+ db.set('lockout:' + uid, '', function (err) {
if (err) {
return callback(err);
}
@@ -49,18 +49,18 @@ module.exports = function(User) {
});
};
- User.auth.clearLoginAttempts = function(uid) {
+ User.auth.clearLoginAttempts = function (uid) {
db.delete('loginAttempts:' + uid);
};
- User.auth.resetLockout = function(uid, callback) {
+ User.auth.resetLockout = function (uid, callback) {
async.parallel([
async.apply(db.delete, 'loginAttempts:' + uid),
async.apply(db.delete, 'lockout:' + uid)
], callback);
};
- User.auth.getSessions = function(uid, curSessionId, callback) {
+ User.auth.getSessions = function (uid, curSessionId, callback) {
var _sids;
// curSessionId is optional
@@ -76,7 +76,7 @@ module.exports = function(User) {
async.map(sids, db.sessionStore.get.bind(db.sessionStore), next);
},
function (sessions, next) {
- sessions.forEach(function(sessionObj, idx) {
+ sessions.forEach(function (sessionObj, idx) {
if (sessionObj && sessionObj.meta) {
sessionObj.meta.current = curSessionId === _sids[idx];
}
@@ -86,7 +86,7 @@ module.exports = function(User) {
var expiredSids = [],
expired;
- sessions = sessions.filter(function(sessionObj, idx) {
+ sessions = sessions.filter(function (sessionObj, idx) {
expired = !sessionObj || !sessionObj.hasOwnProperty('passport') ||
!sessionObj.passport.hasOwnProperty('user') ||
parseInt(sessionObj.passport.user, 10) !== parseInt(uid, 10);
@@ -98,29 +98,29 @@ module.exports = function(User) {
return !expired;
});
- async.each(expiredSids, function(sid, next) {
+ async.each(expiredSids, function (sid, next) {
User.auth.revokeSession(sid, uid, next);
- }, function(err) {
- next(null, sessions);
+ }, function (err) {
+ next(err, sessions);
});
}
], function (err, sessions) {
- callback(err, sessions ? sessions.map(function(sessObj) {
+ callback(err, sessions ? sessions.map(function (sessObj) {
sessObj.meta.datetimeISO = new Date(sessObj.meta.datetime).toISOString();
return sessObj.meta;
}) : undefined);
});
};
- User.auth.addSession = function(uid, sessionId, callback) {
- callback = callback || function() {};
+ User.auth.addSession = function (uid, sessionId, callback) {
+ callback = callback || function () {};
db.sortedSetAdd('uid:' + uid + ':sessions', Date.now(), sessionId, callback);
};
- User.auth.revokeSession = function(sessionId, uid, callback) {
+ User.auth.revokeSession = function (sessionId, uid, callback) {
winston.verbose('[user.auth] Revoking session ' + sessionId + ' for user ' + uid);
- db.sessionStore.get(sessionId, function(err, sessionObj) {
+ db.sessionStore.get(sessionId, function (err, sessionObj) {
if (err) {
return callback(err);
}
@@ -138,11 +138,11 @@ module.exports = function(User) {
});
};
- User.auth.revokeAllSessions = function(uid, callback) {
+ User.auth.revokeAllSessions = function (uid, callback) {
async.waterfall([
async.apply(db.getSortedSetRange, 'uid:' + uid + ':sessions', 0, -1),
function (sids, next) {
- async.each(sids, function(sid, next) {
+ async.each(sids, function (sid, next) {
User.auth.revokeSession(sid, uid, next);
}, next);
}
diff --git a/src/user/categories.js b/src/user/categories.js
index 612b5ec14e..fee8fc8bb0 100644
--- a/src/user/categories.js
+++ b/src/user/categories.js
@@ -5,33 +5,33 @@ var async = require('async');
var db = require('../database');
var categories = require('../categories');
-module.exports = function(User) {
+module.exports = function (User) {
- User.getIgnoredCategories = function(uid, callback) {
+ User.getIgnoredCategories = function (uid, callback) {
db.getSortedSetRange('uid:' + uid + ':ignored:cids', 0, -1, callback);
};
- User.getWatchedCategories = function(uid, callback) {
+ User.getWatchedCategories = function (uid, callback) {
async.parallel({
- ignored: function(next) {
+ ignored: function (next) {
User.getIgnoredCategories(uid, next);
},
- all: function(next) {
+ all: function (next) {
db.getSortedSetRange('categories:cid', 0, -1, next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return callback(err);
}
- var watched = results.all.filter(function(cid) {
+ var watched = results.all.filter(function (cid) {
return cid && results.ignored.indexOf(cid) === -1;
});
callback(null, watched);
});
};
- User.ignoreCategory = function(uid, cid, callback) {
+ User.ignoreCategory = function (uid, cid, callback) {
if (!uid) {
return callback();
}
@@ -45,11 +45,14 @@ module.exports = function(User) {
return next(new Error('[[error:no-category]]'));
}
db.sortedSetAdd('uid:' + uid + ':ignored:cids', Date.now(), cid, next);
+ },
+ function (next) {
+ db.sortedSetAdd('cid:' + cid + ':ignorers', Date.now(), uid, next);
}
], callback);
};
- User.watchCategory = function(uid, cid, callback) {
+ User.watchCategory = function (uid, cid, callback) {
if (!uid) {
return callback();
}
@@ -63,6 +66,9 @@ module.exports = function(User) {
return next(new Error('[[error:no-category]]'));
}
db.sortedSetRemove('uid:' + uid + ':ignored:cids', cid, next);
+ },
+ function (next) {
+ db.sortedSetRemove('cid:' + cid + ':ignorers', uid, next);
}
], callback);
};
diff --git a/src/user/create.js b/src/user/create.js
index ae69f1ad5d..46508d1d0b 100644
--- a/src/user/create.js
+++ b/src/user/create.js
@@ -8,18 +8,16 @@ var plugins = require('../plugins');
var groups = require('../groups');
var meta = require('../meta');
+module.exports = function (User) {
-module.exports = function(User) {
-
- User.create = function(data, callback) {
-
+ User.create = function (data, callback) {
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) {
+ User.isDataValid(data, function (err) {
if (err) {
return callback(err);
}
@@ -28,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': '',
@@ -48,13 +46,13 @@ module.exports = function(User) {
};
async.parallel({
- renamedUsername: function(next) {
+ renamedUsername: function (next) {
renameUsername(userData, next);
},
- userData: function(next) {
+ userData: function (next) {
plugins.fireHook('filter:user.create', {user: userData, data: data}, next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return callback(err);
}
@@ -67,44 +65,44 @@ module.exports = function(User) {
}
async.waterfall([
- function(next) {
+ function (next) {
db.incrObjectField('global', 'nextUid', next);
},
- function(uid, next) {
+ function (uid, next) {
userData.uid = uid;
db.setObject('user:' + uid, userData, next);
},
- function(next) {
+ function (next) {
async.parallel([
- function(next) {
+ function (next) {
db.incrObjectField('global', 'userCount', next);
},
- function(next) {
+ function (next) {
db.sortedSetAdd('username:uid', userData.uid, userData.username, next);
},
- function(next) {
+ function (next) {
db.sortedSetAdd('username:sorted', 0, userData.username.toLowerCase() + ':' + userData.uid, next);
},
- function(next) {
+ function (next) {
db.sortedSetAdd('userslug:uid', userData.uid, userData.userslug, next);
},
- function(next) {
+ function (next) {
var sets = ['users:joindate', 'users:online'];
if (parseInt(userData.uid) !== 1) {
sets.push('users:notvalidated');
}
db.sortedSetsAdd(sets, timestamp, userData.uid, next);
},
- function(next) {
+ function (next) {
db.sortedSetsAdd(['users:postcount', 'users:reputation'], 0, userData.uid, next);
},
- function(next) {
+ function (next) {
groups.join('registered-users', userData.uid, next);
},
- function(next) {
+ function (next) {
User.notifications.sendWelcomeNotification(userData.uid, next);
},
- function(next) {
+ function (next) {
if (userData.email) {
async.parallel([
async.apply(db.sortedSetAdd, 'email:uid', userData.uid, userData.email.toLowerCase()),
@@ -118,12 +116,12 @@ module.exports = function(User) {
next();
}
},
- function(next) {
+ function (next) {
if (!data.password) {
return next();
}
- User.hashPassword(data.password, function(err, hash) {
+ User.hashPassword(data.password, function (err, hash) {
if (err) {
return next(err);
}
@@ -133,10 +131,13 @@ module.exports = function(User) {
async.apply(User.reset.updateExpiry, userData.uid)
], next);
});
+ },
+ function (next) {
+ User.updateDigestSetting(userData.uid, meta.config.dailyDigestFreq, next);
}
], next);
},
- function(results, next) {
+ function (results, next) {
if (userNameChanged) {
User.notifications.sendNameChangeNotification(userData.uid, userData.username);
}
@@ -148,28 +149,28 @@ module.exports = function(User) {
});
};
- User.isDataValid = function(userData, callback) {
+ User.isDataValid = function (userData, callback) {
async.parallel({
- emailValid: function(next) {
+ emailValid: function (next) {
if (userData.email) {
next(!utils.isEmailValid(userData.email) ? new Error('[[error:invalid-email]]') : null);
} else {
next();
}
},
- userNameValid: function(next) {
+ userNameValid: function (next) {
next((!utils.isUserNameValid(userData.username) || !userData.userslug) ? new Error('[[error:invalid-username, ' + userData.username + ']]') : null);
},
- passwordValid: function(next) {
+ passwordValid: function (next) {
if (userData.password) {
User.isPasswordValid(userData.password, next);
} else {
next();
}
},
- emailAvailable: function(next) {
+ emailAvailable: function (next) {
if (userData.email) {
- User.email.available(userData.email, function(err, available) {
+ User.email.available(userData.email, function (err, available) {
if (err) {
return next(err);
}
@@ -179,12 +180,12 @@ module.exports = function(User) {
next();
}
}
- }, function(err) {
+ }, function (err) {
callback(err);
});
};
- User.isPasswordValid = function(password, callback) {
+ User.isPasswordValid = function (password, callback) {
if (!password || !utils.isPasswordValid(password)) {
return callback(new Error('[[error:invalid-password]]'));
}
@@ -201,15 +202,15 @@ module.exports = function(User) {
};
function renameUsername(userData, callback) {
- meta.userOrGroupExists(userData.userslug, function(err, exists) {
+ meta.userOrGroupExists(userData.userslug, function (err, exists) {
if (err || !exists) {
return callback(err);
}
var newUsername = '';
- async.forever(function(next) {
+ async.forever(function (next) {
newUsername = userData.username + (Math.floor(Math.random() * 255) + 1);
- User.existsBySlug(newUsername, function(err, exists) {
+ User.existsBySlug(newUsername, function (err, exists) {
if (err) {
return callback(err);
}
@@ -219,7 +220,7 @@ module.exports = function(User) {
next();
}
});
- }, function(username) {
+ }, function (username) {
callback(null, username);
});
});
diff --git a/src/user/data.js b/src/user/data.js
index 66fedbefcd..cbaf066ded 100644
--- a/src/user/data.js
+++ b/src/user/data.js
@@ -6,25 +6,26 @@ var winston = require('winston');
var db = require('../database');
var plugins = require('../plugins');
+var utils = require('../../public/src/utils');
-module.exports = function(User) {
+module.exports = function (User) {
var iconBackgrounds = ['#f44336', '#e91e63', '#9c27b0', '#673ab7', '#3f51b5', '#2196f3',
'#009688', '#1b5e20', '#33691e', '#827717', '#e65100', '#ff5722', '#795548', '#607d8b'];
- User.getUserField = function(uid, field, callback) {
- User.getUserFields(uid, [field], function(err, user) {
+ User.getUserField = function (uid, field, callback) {
+ User.getUserFields(uid, [field], function (err, user) {
callback(err, user ? user[field] : null);
});
};
- User.getUserFields = function(uid, fields, callback) {
- User.getUsersFields([uid], fields, function(err, users) {
+ User.getUserFields = function (uid, fields, callback) {
+ User.getUsersFields([uid], fields, function (err, users) {
callback(err, users ? users[0] : null);
});
};
- User.getUsersFields = function(uids, fields, callback) {
+ User.getUsersFields = function (uids, fields, callback) {
var fieldsToRemove = [];
function addField(field) {
if (fields.indexOf(field) === -1) {
@@ -37,7 +38,7 @@ module.exports = function(User) {
return callback(null, []);
}
- var keys = uids.map(function(uid) {
+ var keys = uids.map(function (uid) {
return 'user:' + uid;
});
@@ -50,7 +51,11 @@ module.exports = function(User) {
addField('uploadedpicture');
}
- db.getObjectsFields(keys, fields, function(err, users) {
+ if (fields.indexOf('status') !== -1) {
+ addField('lastonline');
+ }
+
+ db.getObjectsFields(keys, fields, function (err, users) {
if (err) {
return callback(err);
}
@@ -59,27 +64,27 @@ module.exports = function(User) {
});
};
- User.getMultipleUserFields = function(uids, fields, callback) {
+ User.getMultipleUserFields = function (uids, fields, callback) {
winston.warn('[deprecated] User.getMultipleUserFields is deprecated please use User.getUsersFields');
User.getUsersFields(uids, fields, callback);
};
- User.getUserData = function(uid, callback) {
- User.getUsersData([uid], function(err, users) {
+ User.getUserData = function (uid, callback) {
+ User.getUsersData([uid], function (err, users) {
callback(err, users ? users[0] : null);
});
};
- User.getUsersData = function(uids, callback) {
+ User.getUsersData = function (uids, callback) {
if (!Array.isArray(uids) || !uids.length) {
return callback(null, []);
}
- var keys = uids.map(function(uid) {
+ var keys = uids.map(function (uid) {
return 'user:' + uid;
});
- db.getObjects(keys, function(err, users) {
+ db.getObjects(keys, function (err, users) {
if (err) {
return callback(err);
}
@@ -89,12 +94,14 @@ module.exports = function(User) {
};
function modifyUserData(users, fieldsToRemove, callback) {
- users.forEach(function(user) {
+ users.forEach(function (user) {
if (!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;
@@ -115,25 +122,37 @@ module.exports = function(User) {
user.uploadedpicture = user.uploadedpicture.startsWith('http') ? user.uploadedpicture : nconf.get('relative_path') + user.uploadedpicture;
}
- 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 9ad100db2b..7182c1a223 100644
--- a/src/user/notifications.js
+++ b/src/user/notifications.js
@@ -1,27 +1,22 @@
'use strict';
-var async = require('async'),
- nconf = require('nconf'),
- winston = require('winston'),
- S = require('string'),
+var async = require('async');
+var winston = require('winston');
+var S = require('string');
- user = require('../user'),
- db = require('../database'),
- meta = require('../meta'),
- notifications = require('../notifications'),
- posts = require('../posts'),
- topics = require('../topics'),
- privileges = require('../privileges'),
- utils = require('../../public/src/utils');
+var db = require('../database');
+var meta = require('../meta');
+var notifications = require('../notifications');
+var privileges = require('../privileges');
-(function(UserNotifications) {
+(function (UserNotifications) {
- UserNotifications.get = function(uid, callback) {
+ UserNotifications.get = function (uid, callback) {
if (!parseInt(uid, 10)) {
return callback(null , {read: [], unread: []});
}
- getNotifications(uid, 0, 9, function(err, notifications) {
+ getNotifications(uid, 0, 9, function (err, notifications) {
if (err) {
return callback(err);
}
@@ -38,13 +33,13 @@ var async = require('async'),
});
};
- UserNotifications.getAll = function(uid, start, stop, callback) {
- getNotifications(uid, start, stop, function(err, notifs) {
+ UserNotifications.getAll = function (uid, start, stop, callback) {
+ getNotifications(uid, start, stop, function (err, notifs) {
if (err) {
return callback(err);
}
notifs = notifs.unread.concat(notifs.read);
- notifs = notifs.filter(Boolean).sort(function(a, b) {
+ notifs = notifs.filter(Boolean).sort(function (a, b) {
return b.datetime - a.datetime;
});
@@ -54,10 +49,10 @@ var async = require('async'),
function getNotifications(uid, start, stop, callback) {
async.parallel({
- unread: function(next) {
+ unread: function (next) {
getNotificationsFromSet('uid:' + uid + ':notifications:unread', false, uid, start, stop, next);
},
- read: function(next) {
+ read: function (next) {
getNotificationsFromSet('uid:' + uid + ':notifications:read', true, uid, start, stop, next);
}
}, callback);
@@ -68,7 +63,7 @@ var async = require('async'),
async.waterfall([
async.apply(db.getSortedSetRevRange, set, start, stop),
- function(nids, next) {
+ function (nids, next) {
if(!Array.isArray(nids) || !nids.length) {
return callback(null, []);
}
@@ -76,10 +71,10 @@ var async = require('async'),
setNids = nids;
UserNotifications.getNotifications(nids, uid, next);
},
- function(notifs, next) {
+ function (notifs, next) {
var deletedNids = [];
- notifs.forEach(function(notification, index) {
+ notifs.forEach(function (notification, index) {
if (!notification) {
winston.verbose('[notifications.get] nid ' + setNids[index] + ' not found. Removing.');
deletedNids.push(setNids[index]);
@@ -98,99 +93,23 @@ var async = require('async'),
], callback);
}
- UserNotifications.getNotifications = function(nids, uid, callback) {
- notifications.getMultiple(nids, function(err, notifications) {
+ UserNotifications.getNotifications = function (nids, uid, callback) {
+ notifications.getMultiple(nids, function (err, notifications) {
if (err) {
return callback(err);
}
-
- UserNotifications.generateNotificationPaths(notifications, uid, callback);
- });
- };
-
- UserNotifications.generateNotificationPaths = function (notifications, uid, callback) {
- var pids = notifications.map(function(notification) {
- return notification ? notification.pid : null;
- });
-
- generatePostPaths(pids, uid, function(err, pidToPaths) {
- if (err) {
- return callback(err);
- }
-
- notifications = notifications.map(function(notification, index) {
- if (!notification) {
- return null;
- }
-
- notification.path = pidToPaths[notification.pid] || notification.path || null;
-
- if (notification.nid.startsWith('follow')) {
- notification.path = '/user/' + notification.user.userslug;
- }
-
- notification.datetimeISO = utils.toISOString(notification.datetime);
- return notification;
- }).filter(function(notification) {
- // Remove notifications that do not resolve to a path
- return notification && notification.path !== null;
+ notifications = notifications.filter(function (notification) {
+ return notification && notification.path;
});
-
callback(null, notifications);
});
};
- function generatePostPaths(pids, uid, callback) {
- pids = pids.filter(Boolean);
- var postKeys = pids.map(function(pid) {
- return 'post:' + pid;
- });
- db.getObjectsFields(postKeys, ['pid', 'tid'], function(err, postData) {
- if (err) {
- return callback(err);
- }
-
- var topicKeys = postData.map(function(post) {
- return post ? 'topic:' + post.tid : null;
- });
-
- async.parallel({
- indices: function(next) {
- posts.getPostIndices(postData, uid, next);
- },
- topics: function(next) {
- db.getObjectsFields(topicKeys, ['slug', 'deleted'], next);
- }
- }, function(err, results) {
- if (err) {
- return callback(err);
- }
-
- var pidToPaths = {};
- pids.forEach(function(pid, index) {
- if (parseInt(results.topics[index].deleted, 10) === 1) {
- pidToPaths[pid] = null;
- return;
- }
-
- var slug = results.topics[index] ? results.topics[index].slug : null;
- var postIndex = utils.isNumber(results.indices[index]) ? parseInt(results.indices[index], 10) + 1 : null;
-
- if (slug && postIndex) {
- pidToPaths[pid] = '/topic/' + slug + '/' + postIndex;
- }
- });
-
- callback(null, pidToPaths);
- });
- });
- }
-
- UserNotifications.getDailyUnread = function(uid, callback) {
+ UserNotifications.getDailyUnread = function (uid, callback) {
var yesterday = Date.now() - (1000 * 60 * 60 * 24); // Approximate, can be more or less depending on time changes, makes no difference really.
- db.getSortedSetRevRangeByScore('uid:' + uid + ':notifications:unread', 0, 20, '+inf', yesterday, function(err, nids) {
+ db.getSortedSetRevRangeByScore('uid:' + uid + ':notifications:unread', 0, 20, '+inf', yesterday, function (err, nids) {
if (err) {
return callback(err);
}
@@ -203,7 +122,7 @@ var async = require('async'),
});
};
- UserNotifications.getUnreadCount = function(uid, callback) {
+ UserNotifications.getUnreadCount = function (uid, callback) {
if (!parseInt(uid, 10)) {
return callback(null, 0);
}
@@ -211,20 +130,21 @@ var async = require('async'),
// Collapse any notifications with identical mergeIds
async.waterfall([
async.apply(db.getSortedSetRevRange, 'uid:' + uid + ':notifications:unread', 0, 99),
- function(nids, next) {
- var keys = nids.map(function(nid) {
+ async.apply(notifications.filterExists),
+ function (nids, next) {
+ var keys = nids.map(function (nid) {
return 'notifications:' + nid;
});
db.getObjectsFields(keys, ['mergeId'], next);
}
- ], function(err, mergeIds) {
+ ], function (err, mergeIds) {
// A missing (null) mergeId means that notification is counted separately.
- mergeIds = mergeIds.map(function(set) {
+ mergeIds = mergeIds.map(function (set) {
return set.mergeId;
});
- callback(err, mergeIds.reduce(function(count, cur, idx, arr) {
+ callback(err, mergeIds.reduce(function (count, cur, idx, arr) {
if (cur === null || idx === arr.indexOf(cur)) {
++count;
}
@@ -234,8 +154,8 @@ var async = require('async'),
});
};
- UserNotifications.getUnreadByField = function(uid, field, value, callback) {
- db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function(err, nids) {
+ UserNotifications.getUnreadByField = function (uid, field, values, callback) {
+ db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function (err, nids) {
if (err) {
return callback(err);
}
@@ -244,19 +164,19 @@ var async = require('async'),
return callback(null, []);
}
- var keys = nids.map(function(nid) {
+ var keys = nids.map(function (nid) {
return 'notifications:' + nid;
});
- db.getObjectsFields(keys, ['nid', field], function(err, notifications) {
+ db.getObjectsFields(keys, ['nid', field], function (err, notifications) {
if (err) {
return callback(err);
}
- value = value ? value.toString() : '';
- nids = notifications.filter(function(notification) {
- return notification && notification[field] && notification[field].toString() === value;
- }).map(function(notification) {
+ values = values.map(function () { return values.toString(); });
+ nids = notifications.filter(function (notification) {
+ return notification && notification[field] && values.indexOf(notification[field].toString()) !== -1;
+ }).map(function (notification) {
return notification.nid;
});
@@ -265,28 +185,35 @@ var async = require('async'),
});
};
- UserNotifications.deleteAll = function(uid, callback) {
+ UserNotifications.deleteAll = function (uid, callback) {
if (!parseInt(uid, 10)) {
return callback();
}
async.parallel([
- function(next) {
+ function (next) {
db.delete('uid:' + uid + ':notifications:unread', next);
},
- function(next) {
+ function (next) {
db.delete('uid:' + uid + ':notifications:read', next);
}
], callback);
};
- UserNotifications.sendTopicNotificationToFollowers = function(uid, topicData, postData) {
- db.getSortedSetRange('followers:' + uid, 0, -1, function(err, followers) {
- if (err || !Array.isArray(followers) || !followers.length) {
- return;
- }
-
- privileges.categories.filterUids('read', topicData.cid, followers, function(err, followers) {
- if (err || !followers.length) {
+ UserNotifications.sendTopicNotificationToFollowers = function (uid, topicData, postData) {
+ var followers;
+ async.waterfall([
+ function (next) {
+ db.getSortedSetRange('followers:' + uid, 0, -1, next);
+ },
+ function (followers, next) {
+ if (!Array.isArray(followers) || !followers.length) {
+ return;
+ }
+ privileges.categories.filterUids('read', topicData.cid, followers, next);
+ },
+ function (_followers, next) {
+ followers = _followers;
+ if (!followers.length) {
return;
}
@@ -299,20 +226,25 @@ var async = require('async'),
bodyShort: '[[notifications:user_posted_topic, ' + postData.user.username + ', ' + title + ']]',
bodyLong: postData.content,
pid: postData.pid,
+ path: '/post/' + postData.pid,
nid: 'tid:' + postData.tid + ':uid:' + uid,
tid: postData.tid,
from: uid
- }, function(err, notification) {
- if (!err && notification) {
- notifications.push(notification, followers);
- }
- });
- });
+ }, next);
+ }
+ ], function (err, notification) {
+ if (err) {
+ return winston.error(err);
+ }
+
+ if (notification) {
+ notifications.push(notification, followers);
+ }
});
};
- UserNotifications.sendWelcomeNotification = function(uid, callback) {
- callback = callback || function() {};
+ UserNotifications.sendWelcomeNotification = function (uid, callback) {
+ callback = callback || function () {};
if (!meta.config.welcomeNotification) {
return callback();
}
@@ -323,7 +255,7 @@ var async = require('async'),
bodyShort: meta.config.welcomeNotification,
path: path,
nid: 'welcome_' + uid
- }, function(err, notification) {
+ }, function (err, notification) {
if (err || !notification) {
return callback(err);
}
@@ -332,22 +264,22 @@ var async = require('async'),
});
};
- UserNotifications.sendNameChangeNotification = function(uid, username) {
+ UserNotifications.sendNameChangeNotification = function (uid, username) {
notifications.create({
bodyShort: '[[user:username_taken_workaround, ' + username + ']]',
image: 'brand:logo',
nid: 'username_taken:' + uid,
datetime: Date.now()
- }, function(err, notification) {
+ }, function (err, notification) {
if (!err && notification) {
notifications.push(notification, uid);
}
});
};
- UserNotifications.pushCount = function(uid) {
+ UserNotifications.pushCount = function (uid) {
var websockets = require('./../socket.io');
- UserNotifications.getUnreadCount(uid, function(err, count) {
+ UserNotifications.getUnreadCount(uid, function (err, count) {
if (err) {
return winston.error(err.stack);
}
diff --git a/src/user/password.js b/src/user/password.js
index e60ae9b753..8e9b7780e3 100644
--- a/src/user/password.js
+++ b/src/user/password.js
@@ -6,9 +6,9 @@ var nconf = require('nconf');
var db = require('../database');
var Password = require('../password');
-module.exports = function(User) {
+module.exports = function (User) {
- User.hashPassword = function(password, callback) {
+ User.hashPassword = function (password, callback) {
if (!password) {
return callback(null, password);
}
@@ -16,7 +16,7 @@ module.exports = function(User) {
Password.hash(nconf.get('bcrypt_rounds') || 12, password, callback);
};
- User.isPasswordCorrect = function(uid, password, callback) {
+ User.isPasswordCorrect = function (uid, password, callback) {
password = password || '';
async.waterfall([
function (next) {
@@ -27,7 +27,7 @@ module.exports = function(User) {
return callback(null, true);
}
- User.isPasswordValid(password, function(err) {
+ User.isPasswordValid(password, function (err) {
if (err) {
return next(err);
}
@@ -38,8 +38,8 @@ module.exports = function(User) {
], callback);
};
- User.hasPassword = function(uid, callback) {
- db.getObjectField('user:' + uid, 'password', function(err, hashedPassword) {
+ User.hasPassword = function (uid, callback) {
+ db.getObjectField('user:' + uid, 'password', function (err, hashedPassword) {
callback(err, !!hashedPassword);
});
};
diff --git a/src/user/picture.js b/src/user/picture.js
index c9a760f10e..64f9c900c2 100644
--- a/src/user/picture.js
+++ b/src/user/picture.js
@@ -1,22 +1,22 @@
'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) {
+module.exports = function (User) {
User.uploadPicture = function (uid, picture, callback) {
@@ -25,30 +25,34 @@ module.exports = function(User) {
var updateUid = uid;
var imageDimension = parseInt(meta.config.profileImageDimension, 10) || 128;
var convertToPNG = parseInt(meta.config['profile:convertProfileImageToPNG'], 10) === 1;
+ 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) {
+ function (next) {
if (plugins.hasListeners('filter:uploadImage')) {
return plugins.fireHook('filter:uploadImage', {image: picture, uid: updateUid}, next);
}
- var filename = updateUid + '-profileimg' + (convertToPNG ? '.png' : extension);
+ var filename = updateUid + '-profileimg' + (keepAllVersions ? '-' + Date.now() : '') + (convertToPNG ? '.png' : extension);
async.waterfall([
- function(next) {
+ function (next) {
file.isFileTypeAllowed(picture.path, next);
},
- function(next) {
+ function (next) {
image.resizeImage({
path: picture.path,
extension: extension,
@@ -56,48 +60,38 @@ module.exports = function(User) {
height: imageDimension
}, next);
},
- function(next) {
- if (convertToPNG) {
- image.normalise(picture.path, extension, next);
- } else {
- next();
+ function (next) {
+ if (!convertToPNG) {
+ return next();
}
- },
- function(next) {
- User.getUserField(updateUid, 'uploadedpicture', next);
- },
- function(oldpicture, next) {
- if (!oldpicture) {
- return file.saveFileToLocal(filename, 'profile', picture.path, next);
- }
- var oldpicturePath = path.join(nconf.get('base_dir'), nconf.get('upload_path'), 'profile', path.basename(oldpicture));
-
- fs.unlink(oldpicturePath, function (err) {
- if (err) {
- winston.error(err);
- }
-
- file.saveFileToLocal(filename, 'profile', picture.path, next);
+ async.series([
+ async.apply(image.normalise, picture.path, extension),
+ async.apply(fs.rename, picture.path + '.png', picture.path)
+ ], function (err) {
+ next(err);
});
},
+ function (next) {
+ file.saveFileToLocal(filename, 'profile', picture.path, next);
+ },
], next);
},
- function(_image, next) {
+ function (_image, next) {
uploadedImage = _image;
User.setUserFields(updateUid, {uploadedpicture: uploadedImage.url, picture: uploadedImage.url}, next);
},
- function(next) {
+ function (next) {
next(null, uploadedImage);
}
], callback);
};
- User.uploadFromUrl = function(uid, url, callback) {
+ User.uploadFromUrl = function (uid, url, callback) {
if (!plugins.hasListeners('filter:uploadImage')) {
return callback(new Error('[[error:no-plugin]]'));
}
- request.head(url, function(err, res) {
+ request.head(url, function (err, res) {
if (err) {
return callback(err);
}
@@ -115,7 +109,7 @@ module.exports = function(User) {
}
var picture = {url: url, name: ''};
- plugins.fireHook('filter:uploadImage', {image: picture, uid: uid}, function(err, image) {
+ plugins.fireHook('filter:uploadImage', {image: picture, uid: uid}, function (err, image) {
if (err) {
return callback(err);
}
@@ -125,11 +119,12 @@ module.exports = function(User) {
});
};
- User.updateCoverPosition = function(uid, position, callback) {
+ User.updateCoverPosition = function (uid, position, callback) {
User.setUserField(uid, 'cover:position', position, callback);
};
- User.updateCoverPicture = function(data, callback) {
+ User.updateCoverPicture = function (data, callback) {
+ var keepAllVersions = parseInt(meta.config['profile:keepAllUserImages'], 10) === 1;
var url, md5sum;
if (!data.imageData && data.position) {
@@ -141,7 +136,7 @@ module.exports = function(User) {
}
async.waterfall([
- function(next) {
+ function (next) {
var size = data.file ? data.file.size : data.imageData.length;
meta.config.maximumCoverImageSize = meta.config.maximumCoverImageSize || 2048;
if (size > parseInt(meta.config.maximumCoverImageSize, 10) * 1024) {
@@ -166,7 +161,7 @@ module.exports = function(User) {
encoding: 'base64'
}, next);
},
- function(next) {
+ function (next) {
var image = {
name: 'profileCover',
path: data.file.path,
@@ -177,7 +172,7 @@ module.exports = function(User) {
return plugins.fireHook('filter:uploadImage', {image: image, uid: data.uid}, next);
}
- var filename = data.uid + '-profilecover';
+ var filename = data.uid + '-profilecover' + (keepAllVersions ? '-' + Date.now() : '');
async.waterfall([
function (next) {
file.isFileTypeAllowed(data.file.path, next);
@@ -193,27 +188,31 @@ module.exports = function(User) {
}
], next);
},
- function(uploadData, next) {
+ function (uploadData, next) {
url = uploadData.url;
User.setUserField(data.uid, 'cover:url', uploadData.url, next);
},
- function(next) {
- fs.unlink(data.file.path, function(err) {
+ function (next) {
+ fs.unlink(data.file.path, function (err) {
if (err) {
winston.error(err);
}
next();
});
}
- ], function(err) {
+ ], function (err) {
if (err) {
- return fs.unlink(data.file.path, function(unlinkErr) {
+ return fs.unlink(data.file.path, function (unlinkErr) {
+ if (unlinkErr) {
+ winston.error(unlinkErr);
+ }
+
callback(err); // send back the original error
});
}
if (data.position) {
- User.updateCoverPosition(data.uid, data.position, function(err) {
+ User.updateCoverPosition(data.uid, data.position, function (err) {
callback(err, {url: url});
});
} else {
@@ -222,7 +221,7 @@ module.exports = function(User) {
});
};
- User.removeCoverPicture = function(data, callback) {
+ User.removeCoverPicture = function (data, callback) {
db.deleteObjectField('user:' + data.uid, 'cover:url', callback);
};
-};
\ No newline at end of file
+};
diff --git a/src/user/posts.js b/src/user/posts.js
index 78a2db0923..37b5e92b16 100644
--- a/src/user/posts.js
+++ b/src/user/posts.js
@@ -1,28 +1,28 @@
'use strict';
-var async = require('async'),
- db = require('../database'),
- meta = require('../meta'),
- privileges = require('../privileges');
+var async = require('async');
+var db = require('../database');
+var meta = require('../meta');
+var privileges = require('../privileges');
-module.exports = function(User) {
+module.exports = function (User) {
- User.isReadyToPost = function(uid, cid, callback) {
+ User.isReadyToPost = function (uid, cid, callback) {
if (parseInt(uid, 10) === 0) {
return callback();
}
async.parallel({
- userData: function(next) {
+ userData: function (next) {
User.getUserFields(uid, ['banned', 'lastposttime', 'joindate', 'email', 'email:confirmed', 'reputation'], next);
},
- exists: function(next) {
+ exists: function (next) {
db.exists('user:' + uid, next);
},
- isAdminOrMod: function(next) {
+ isAdminOrMod: function (next) {
privileges.categories.isAdminOrMod(cid, uid, next);
}
- }, function(err, results) {
+ }, function (err, results) {
if (err) {
return callback(err);
}
@@ -62,37 +62,30 @@ module.exports = function(User) {
});
};
- User.onNewPostMade = function(postData, callback) {
+ User.onNewPostMade = function (postData, callback) {
async.series([
- function(next) {
+ function (next) {
User.addPostIdToUser(postData.uid, postData.pid, postData.timestamp, next);
},
- function(next) {
+ function (next) {
User.incrementUserPostCountBy(postData.uid, 1, next);
},
- function(next) {
+ function (next) {
User.setUserField(postData.uid, 'lastposttime', postData.timestamp, next);
},
- function(next) {
+ function (next) {
User.updateLastOnlineTime(postData.uid, next);
}
], callback);
};
- User.addPostIdToUser = function(uid, pid, timestamp, callback) {
+ User.addPostIdToUser = function (uid, pid, timestamp, callback) {
db.sortedSetAdd('uid:' + uid + ':posts', timestamp, pid, callback);
};
- User.addTopicIdToUser = function(uid, tid, timestamp, callback) {
- async.parallel([
- async.apply(db.sortedSetAdd, 'uid:' + uid + ':topics', timestamp, tid),
- async.apply(User.incrementUserFieldBy, uid, 'topiccount', 1)
- ], callback);
- };
-
- User.incrementUserPostCountBy = function(uid, value, callback) {
- callback = callback || function() {};
- User.incrementUserFieldBy(uid, 'postcount', value, function(err, newpostcount) {
+ User.incrementUserPostCountBy = function (uid, value, callback) {
+ callback = callback || function () {};
+ User.incrementUserFieldBy(uid, 'postcount', value, function (err, newpostcount) {
if (err) {
return callback(err);
}
@@ -103,8 +96,8 @@ module.exports = function(User) {
});
};
- User.getPostIds = function(uid, start, stop, callback) {
- db.getSortedSetRevRange('uid:' + uid + ':posts', start, stop, function(err, pids) {
+ User.getPostIds = function (uid, start, stop, callback) {
+ db.getSortedSetRevRange('uid:' + uid + ':posts', start, stop, function (err, pids) {
callback(err, Array.isArray(pids) ? pids : []);
});
};
diff --git a/src/user/profile.js b/src/user/profile.js
index 50aa92c1d0..7ebf7cfafd 100644
--- a/src/user/profile.js
+++ b/src/user/profile.js
@@ -7,137 +7,147 @@ var S = require('string');
var utils = require('../../public/src/utils');
var meta = require('../meta');
var db = require('../database');
+var groups = require('../groups');
var plugins = require('../plugins');
-module.exports = function(User) {
+module.exports = function (User) {
- User.updateProfile = function(uid, data, callback) {
- var fields = ['username', 'email', 'fullname', 'website', 'location', 'birthday', 'signature', 'aboutme'];
+ User.updateProfile = function (uid, data, callback) {
+ var fields = ['username', 'email', 'fullname', 'website', 'location',
+ 'groupTitle', 'birthday', 'signature', 'aboutme', 'picture', 'uploadedpicture'];
- plugins.fireHook('filter:user.updateProfile', {uid: uid, data: data, fields: fields}, function(err, data) {
- if (err) {
- return callback(err);
- }
+ async.waterfall([
+ function (next) {
+ plugins.fireHook('filter:user.updateProfile', {uid: uid, data: data, fields: fields}, next);
+ },
+ function (data, next) {
+ fields = data.fields;
+ data = data.data;
- fields = data.fields;
- data = data.data;
-
- function isAboutMeValid(next) {
- if (data.aboutme !== undefined && data.aboutme.length > meta.config.maximumAboutMeLength) {
- next(new Error('[[error:about-me-too-long, ' + meta.config.maximumAboutMeLength + ']]'));
- } else {
- next();
- }
- }
-
- function isSignatureValid(next) {
- if (data.signature !== undefined && data.signature.length > meta.config.maximumSignatureLength) {
- next(new Error('[[error:signature-too-long, ' + meta.config.maximumSignatureLength + ']]'));
- } else {
- next();
- }
- }
-
- function isEmailAvailable(next) {
- if (!data.email) {
- return next();
- }
-
- if (!utils.isEmailValid(data.email)) {
- return next(new Error('[[error:invalid-email]]'));
- }
-
- User.getUserField(uid, 'email', function(err, email) {
- if(email === data.email) {
+ async.series([
+ async.apply(isAboutMeValid, data),
+ async.apply(isSignatureValid, data),
+ async.apply(isEmailAvailable, data, uid),
+ async.apply(isUsernameAvailable, data, uid),
+ async.apply(isGroupTitleValid, data)
+ ], function (err) {
+ next(err);
+ });
+ },
+ function (next) {
+ async.each(fields, function (field, next) {
+ if (!(data[field] !== undefined && typeof data[field] === 'string')) {
return next();
}
- User.email.available(data.email, function(err, available) {
- if (err) {
- return next(err);
- }
+ data[field] = data[field].trim();
- next(!available ? new Error('[[error:email-taken]]') : null);
- });
- });
+ if (field === 'email') {
+ return updateEmail(uid, data.email, next);
+ } else if (field === 'username') {
+ return updateUsername(uid, data.username, next);
+ } else if (field === 'fullname') {
+ return updateFullname(uid, data.fullname, next);
+ } else if (field === 'signature') {
+ data[field] = S(data[field]).stripTags().s;
+ }
+
+ User.setUserField(uid, field, data[field], next);
+ }, next);
+ },
+ function (next) {
+ plugins.fireHook('action:user.updateProfile', {data: data, uid: uid});
+ User.getUserFields(uid, ['email', 'username', 'userslug', 'picture', 'icon:text', 'icon:bgColor'], next);
}
-
- function isUsernameAvailable(next) {
- if (!data.username) {
- return next();
- }
- data.username = data.username.trim();
- User.getUserFields(uid, ['username', 'userslug'], function(err, userData) {
- if (err) {
- return next(err);
- }
-
- var userslug = utils.slugify(data.username);
-
- if (data.username.length < meta.config.minimumUsernameLength) {
- return next(new Error('[[error:username-too-short]]'));
- }
-
- if (data.username.length > meta.config.maximumUsernameLength) {
- return next(new Error('[[error:username-too-long]]'));
- }
-
- if (!utils.isUserNameValid(data.username) || !userslug) {
- return next(new Error('[[error:invalid-username]]'));
- }
-
- if (userslug === userData.userslug) {
- return next();
- }
-
- User.existsBySlug(userslug, function(err, exists) {
- if (err) {
- return next(err);
- }
-
- next(exists ? new Error('[[error:username-taken]]') : null);
- });
- });
- }
-
- async.series([isAboutMeValid, isSignatureValid, isEmailAvailable, isUsernameAvailable], function(err) {
- if (err) {
- return callback(err);
- }
-
- async.each(fields, updateField, function(err) {
- if (err) {
- return callback(err);
- }
- plugins.fireHook('action:user.updateProfile', {data: data, uid: uid});
- User.getUserFields(uid, ['email', 'username', 'userslug', 'picture', 'icon:text', 'icon:bgColor'], callback);
- });
- });
-
- function updateField(field, next) {
- if (!(data[field] !== undefined && typeof data[field] === 'string')) {
- return next();
- }
-
- data[field] = data[field].trim();
-
- if (field === 'email') {
- return updateEmail(uid, data.email, next);
- } else if (field === 'username') {
- return updateUsername(uid, data.username, next);
- } else if (field === 'fullname') {
- return updateFullname(uid, data.fullname, next);
- } else if (field === 'signature') {
- data[field] = S(data[field]).stripTags().s;
- }
-
- User.setUserField(uid, field, data[field], next);
- }
- });
+ ], callback);
};
+ function isAboutMeValid(data, callback) {
+ if (data.aboutme !== undefined && data.aboutme.length > meta.config.maximumAboutMeLength) {
+ callback(new Error('[[error:about-me-too-long, ' + meta.config.maximumAboutMeLength + ']]'));
+ } else {
+ callback();
+ }
+ }
+
+ function isSignatureValid(data, callback) {
+ if (data.signature !== undefined && data.signature.length > meta.config.maximumSignatureLength) {
+ callback(new Error('[[error:signature-too-long, ' + meta.config.maximumSignatureLength + ']]'));
+ } else {
+ callback();
+ }
+ }
+
+ function isEmailAvailable(data, uid, callback) {
+ if (!data.email) {
+ return callback();
+ }
+
+ if (!utils.isEmailValid(data.email)) {
+ return callback(new Error('[[error:invalid-email]]'));
+ }
+
+ async.waterfall([
+ function (next) {
+ User.getUserField(uid, 'email', next);
+ },
+ function (email, next) {
+ if (email === data.email) {
+ return callback();
+ }
+ User.email.available(data.email, next);
+ },
+ function (available, next) {
+ next(!available ? new Error('[[error:email-taken]]') : null);
+ }
+ ], callback);
+ }
+
+ function isUsernameAvailable(data, uid, callback) {
+ if (!data.username) {
+ return callback();
+ }
+ data.username = data.username.trim();
+ async.waterfall([
+ function (next) {
+ User.getUserFields(uid, ['username', 'userslug'], next);
+ },
+ function (userData, next) {
+ var userslug = utils.slugify(data.username);
+
+ if (data.username.length < meta.config.minimumUsernameLength) {
+ return next(new Error('[[error:username-too-short]]'));
+ }
+
+ if (data.username.length > meta.config.maximumUsernameLength) {
+ return next(new Error('[[error:username-too-long]]'));
+ }
+
+ if (!utils.isUserNameValid(data.username) || !userslug) {
+ return next(new Error('[[error:invalid-username]]'));
+ }
+
+ if (userslug === userData.userslug) {
+ return callback();
+ }
+ User.existsBySlug(userslug, next);
+ },
+ function (exists, next) {
+ next(exists ? new Error('[[error:username-taken]]') : null);
+ }
+ ], callback);
+ }
+
+ function isGroupTitleValid(data, callback) {
+ if (data.groupTitle === 'registered-users' || groups.isPrivilegeGroup(data.groupTitle)) {
+ callback(new Error('[[error:invalid-group-title]]'));
+ } else {
+ callback();
+ }
+ }
+
function updateEmail(uid, newEmail, callback) {
- User.getUserFields(uid, ['email', 'picture', 'uploadedpicture'], function(err, userData) {
+ User.getUserFields(uid, ['email', 'picture', 'uploadedpicture'], function (err, userData) {
if (err) {
return callback(err);
}
@@ -150,22 +160,23 @@ module.exports = function(User) {
async.series([
async.apply(db.sortedSetRemove, 'email:uid', userData.email.toLowerCase()),
async.apply(db.sortedSetRemove, 'email:sorted', userData.email.toLowerCase() + ':' + uid)
- ], function(err) {
+ ], function (err) {
if (err) {
return callback(err);
}
async.parallel([
- function(next) {
+ function (next) {
db.sortedSetAdd('email:uid', uid, newEmail.toLowerCase(), next);
},
- function(next) {
+ async.apply(db.sortedSetAdd, 'user:' + uid + ':emails', Date.now(), newEmail + ':' + Date.now()),
+ function (next) {
db.sortedSetAdd('email:sorted', 0, newEmail.toLowerCase() + ':' + uid, next);
},
- function(next) {
+ function (next) {
User.setUserField(uid, 'email', newEmail, next);
},
- function(next) {
+ function (next) {
if (parseInt(meta.config.requireEmailConfirmation, 10) === 1 && newEmail) {
User.email.sendValidationEmail(uid, newEmail);
}
@@ -184,23 +195,24 @@ module.exports = function(User) {
return callback();
}
- User.getUserFields(uid, ['username', 'userslug'], function(err, userData) {
+ User.getUserFields(uid, ['username', 'userslug'], function (err, userData) {
if (err) {
return callback(err);
}
async.parallel([
- function(next) {
+ function (next) {
updateUidMapping('username', uid, newUsername, userData.username, next);
},
- function(next) {
+ function (next) {
var newUserslug = utils.slugify(newUsername);
updateUidMapping('userslug', uid, newUserslug, userData.userslug, next);
},
- function(next) {
+ 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);
@@ -213,13 +225,13 @@ module.exports = function(User) {
}
async.series([
- function(next) {
+ function (next) {
db.sortedSetRemove(field + ':uid', oldValue, next);
},
- function(next) {
+ function (next) {
User.setUserField(uid, field, value, next);
},
- function(next) {
+ function (next) {
if (value) {
db.sortedSetAdd(field + ':uid', uid, value, next);
} else {
@@ -231,16 +243,16 @@ module.exports = function(User) {
function updateFullname(uid, newFullname, callback) {
async.waterfall([
- function(next) {
+ function (next) {
User.getUserField(uid, 'fullname', next);
},
- function(fullname, next) {
+ function (fullname, next) {
updateUidMapping('fullname', uid, newFullname, fullname, next);
}
], callback);
}
- User.changePassword = function(uid, data, callback) {
+ User.changePassword = function (uid, data, callback) {
if (!uid || !data || !data.uid) {
return callback(new Error('[[error:invalid-uid]]'));
}
@@ -267,7 +279,7 @@ module.exports = function(User) {
async.parallel([
async.apply(User.setUserField, data.uid, 'password', hashedPassword),
async.apply(User.reset.updateExpiry, data.uid)
- ], function(err) {
+ ], function (err) {
next(err);
});
}
diff --git a/src/user/reset.js b/src/user/reset.js
index 222e988de6..39bf1f0e07 100644
--- a/src/user/reset.js
+++ b/src/user/reset.js
@@ -12,32 +12,32 @@ var async = require('async'),
meta = require('../meta'),
emailer = require('../emailer');
-(function(UserReset) {
+(function (UserReset) {
var twoHours = 7200000;
- UserReset.validate = function(code, callback) {
+ UserReset.validate = function (code, callback) {
async.waterfall([
- function(next) {
+ function (next) {
db.getObjectField('reset:uid', code, next);
},
- function(uid, next) {
+ function (uid, next) {
if (!uid) {
return callback(null, false);
}
db.sortedSetScore('reset:issueDate', code, next);
},
- function(issueDate, next) {
+ function (issueDate, next) {
next(null, parseInt(issueDate, 10) > Date.now() - twoHours);
}
], callback);
};
- UserReset.generate = function(uid, callback) {
+ UserReset.generate = function (uid, callback) {
var code = utils.generateUUID();
async.parallel([
async.apply(db.setObjectField, 'reset:uid', code, uid),
async.apply(db.sortedSetAdd, 'reset:issueDate', Date.now(), code)
- ], function(err) {
+ ], function (err) {
callback(err, code);
});
};
@@ -56,13 +56,13 @@ var async = require('async'),
], callback);
}
- UserReset.send = function(email, callback) {
+ UserReset.send = function (email, callback) {
var uid;
async.waterfall([
- function(next) {
+ function (next) {
user.getUidByEmail(email, next);
},
- function(_uid, next) {
+ function (_uid, next) {
if (!_uid) {
return next(new Error('[[error:invalid-email]]'));
}
@@ -70,18 +70,18 @@ var async = require('async'),
uid = _uid;
canGenerate(uid, next);
},
- function(next) {
+ function (next) {
db.sortedSetAdd('reset:issueDate:uid', Date.now(), uid, next);
},
- function(next) {
+ function (next) {
UserReset.generate(uid, next);
},
- function(code, next) {
- translator.translate('[[email:password-reset-requested, ' + (meta.config.title || 'NodeBB') + ']]', meta.config.defaultLang, function(subject) {
+ function (code, next) {
+ translator.translate('[[email:password-reset-requested, ' + (meta.config.title || 'NodeBB') + ']]', meta.config.defaultLang, function (subject) {
next(null, subject, code);
});
},
- function(subject, code, next) {
+ function (subject, code, next) {
var reset_link = nconf.get('url') + '/reset/' + code;
emailer.send('reset', uid, {
site_title: (meta.config.title || 'NodeBB'),
@@ -94,22 +94,22 @@ var async = require('async'),
], callback);
};
- UserReset.commit = function(code, password, callback) {
+ UserReset.commit = function (code, password, callback) {
var uid;
async.waterfall([
- function(next) {
+ function (next) {
user.isPasswordValid(password, next);
},
- function(next) {
+ function (next) {
UserReset.validate(code, next);
},
- function(validated, next) {
+ function (validated, next) {
if (!validated) {
return next(new Error('[[error:reset-code-not-valid]]'));
}
db.getObjectField('reset:uid', code, next);
},
- function(_uid, next) {
+ function (_uid, next) {
uid = _uid;
if (!uid) {
return next(new Error('[[error:reset-code-not-valid]]'));
@@ -117,7 +117,7 @@ var async = require('async'),
user.hashPassword(password, next);
},
- function(hash, next) {
+ function (hash, next) {
async.parallel([
async.apply(user.setUserField, uid, 'password', hash),
async.apply(db.deleteObjectField, 'reset:uid', code),
@@ -130,28 +130,28 @@ var async = require('async'),
], callback);
};
- UserReset.updateExpiry = function(uid, callback) {
+ UserReset.updateExpiry = function (uid, callback) {
var oneDay = 1000 * 60 * 60 * 24;
var expireDays = parseInt(meta.config.passwordExpiryDays || 0, 10);
var expiry = Date.now() + (oneDay * expireDays);
- callback = callback || function() {};
+ callback = callback || function () {};
user.setUserField(uid, 'passwordExpiry', expireDays > 0 ? expiry : 0, callback);
};
- UserReset.clean = function(callback) {
+ UserReset.clean = function (callback) {
async.waterfall([
- function(next) {
+ function (next) {
async.parallel({
- tokens: function(next) {
+ tokens: function (next) {
db.getSortedSetRangeByScore('reset:issueDate', 0, -1, '-inf', Date.now() - twoHours, next);
},
- uids: function(next) {
+ uids: function (next) {
db.getSortedSetRangeByScore('reset:issueDate:uid', 0, -1, '-inf', Date.now() - twoHours, next);
}
}, next);
},
- function(results, next) {
+ function (results, next) {
if (!results.tokens.length && !results.uids.length) {
return next();
}
diff --git a/src/user/search.js b/src/user/search.js
index b2a1e83a80..65d0e41a6a 100644
--- a/src/user/search.js
+++ b/src/user/search.js
@@ -1,14 +1,14 @@
'use strict';
-var async = require('async'),
- meta = require('../meta'),
- plugins = require('../plugins'),
- db = require('../database');
+var async = require('async');
+var meta = require('../meta');
+var plugins = require('../plugins');
+var db = require('../database');
-module.exports = function(User) {
+module.exports = function (User) {
- User.search = function(data, callback) {
+ User.search = function (data, callback) {
var query = data.query || '';
var searchBy = data.searchBy || 'username';
var page = data.page || 1;
@@ -23,20 +23,20 @@ module.exports = function(User) {
var searchResult = {};
async.waterfall([
- function(next) {
+ function (next) {
if (data.findUids) {
data.findUids(query, searchBy, next);
} else {
findUids(query, searchBy, next);
}
},
- function(uids, next) {
+ function (uids, next) {
filterAndSortUids(uids, data, next);
},
- function(uids, next) {
+ function (uids, next) {
plugins.fireHook('filter:users.search', {uids: uids, uid: uid}, next);
},
- function(data, next) {
+ function (data, next) {
var uids = data.uids;
searchResult.matchCount = uids.length;
@@ -50,7 +50,7 @@ module.exports = function(User) {
User.getUsers(uids, uid, next);
},
- function(userData, next) {
+ function (userData, next) {
searchResult.timing = (process.elapsedTimeSince(startTime) / 1000).toFixed(2);
searchResult.users = userData;
next(null, searchResult);
@@ -69,12 +69,12 @@ module.exports = function(User) {
var resultsPerPage = parseInt(meta.config.userSearchResultsPerPage, 10) || 20;
var hardCap = resultsPerPage * 10;
- db.getSortedSetRangeByLex(searchBy + ':sorted', min, max, 0, hardCap, function(err, data) {
+ db.getSortedSetRangeByLex(searchBy + ':sorted', min, max, 0, hardCap, function (err, data) {
if (err) {
return callback(err);
}
- var uids = data.map(function(data) {
+ var uids = data.map(function (data) {
return data.split(':')[1];
});
callback(null, uids);
@@ -84,28 +84,43 @@ module.exports = function(User) {
function filterAndSortUids(uids, data, callback) {
var sortBy = data.sortBy || 'joindate';
- var fields = ['uid', 'status', 'lastonline', 'banned', 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) {
+ User.getUsersFields(uids, fields, function (err, userData) {
if (err) {
return callback(err);
}
if (data.onlineOnly) {
- userData = userData.filter(function(user) {
+ userData = userData.filter(function (user) {
return user && user.status !== 'offline' && (Date.now() - parseInt(user.lastonline, 10) < 300000);
});
}
-
- if(data.bannedOnly) {
- userData = userData.filter(function(user) {
+
+ if (data.bannedOnly) {
+ userData = userData.filter(function (user) {
return user && user.banned;
});
}
+ if (data.flaggedOnly) {
+ userData = userData.filter(function (user) {
+ return user && parseInt(user.flags, 10) > 0;
+ });
+ }
+
sortUsers(userData, sortBy);
- uids = userData.map(function(user) {
+ uids = userData.map(function (user) {
return user && user.uid;
});
@@ -115,11 +130,11 @@ module.exports = function(User) {
function sortUsers(userData, sortBy) {
if (sortBy === 'joindate' || sortBy === 'postcount' || sortBy === 'reputation') {
- userData.sort(function(u1, u2) {
+ userData.sort(function (u1, u2) {
return u2[sortBy] - u1[sortBy];
});
} else {
- userData.sort(function(u1, u2) {
+ userData.sort(function (u1, u2) {
if(u1[sortBy] < u2[sortBy]) {
return -1;
} else if(u1[sortBy] > u2[sortBy]) {
@@ -133,13 +148,13 @@ module.exports = function(User) {
function searchByIP(ip, uid, callback) {
var start = process.hrtime();
async.waterfall([
- function(next) {
+ function (next) {
db.getSortedSetRevRange('ip:' + ip + ':uid', 0, -1, next);
},
- function(uids, next) {
+ function (uids, next) {
User.getUsers(uids, uid, next);
},
- function(users, next) {
+ function (users, next) {
var diff = process.hrtime(start);
var timing = (diff[0] * 1e3 + diff[1] / 1e6).toFixed(1);
next(null, {timing: timing, users: users});
diff --git a/src/user/settings.js b/src/user/settings.js
index 04e23e1152..29666cfce5 100644
--- a/src/user/settings.js
+++ b/src/user/settings.js
@@ -1,19 +1,19 @@
'use strict';
-var async = require('async'),
- meta = require('../meta'),
- db = require('../database'),
- plugins = require('../plugins');
+var async = require('async');
+var meta = require('../meta');
+var db = require('../database');
+var plugins = require('../plugins');
-module.exports = function(User) {
+module.exports = function (User) {
- User.getSettings = function(uid, callback) {
+ User.getSettings = function (uid, callback) {
if (!parseInt(uid, 10)) {
return onSettingsLoaded(0, {}, callback);
}
- db.getObject('user:' + uid + ':settings', function(err, settings) {
+ db.getObject('user:' + uid + ':settings', function (err, settings) {
if (err) {
return callback(err);
}
@@ -22,33 +22,33 @@ module.exports = function(User) {
});
};
- User.getMultipleUserSettings = function(uids, callback) {
+ User.getMultipleUserSettings = function (uids, callback) {
if (!Array.isArray(uids) || !uids.length) {
return callback(null, []);
}
- var keys = uids.map(function(uid) {
+ var keys = uids.map(function (uid) {
return 'user:' + uid + ':settings';
});
- db.getObjects(keys, function(err, settings) {
+ db.getObjects(keys, function (err, settings) {
if (err) {
return callback(err);
}
- for (var i=0; i meta.config.postsPerPage) {
return callback(new Error('[[error:invalid-pagination-value, 2, ' + meta.config.postsPerPage + ']]'));
}
@@ -112,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,
@@ -120,9 +119,12 @@ module.exports = function(User) {
sendPostNotifications: data.sendPostNotifications,
restrictChat: data.restrictChat,
topicSearchEnabled: data.topicSearchEnabled,
- groupTitle: data.groupTitle,
- homePageRoute: data.homePageCustom || data.homePageRoute,
- scrollToMyPost: data.scrollToMyPost
+ delayImageLoading: data.delayImageLoading,
+ homePageRoute : ((data.homePageRoute === 'custom' ? data.homePageCustom : data.homePageRoute) || '').replace(/^\//, ''),
+ scrollToMyPost: data.scrollToMyPost,
+ notificationSound: data.notificationSound,
+ incomingChatSound: data.incomingChatSound,
+ outgoingChatSound: data.outgoingChatSound
};
if (data.bootswatchSkin) {
@@ -130,24 +132,24 @@ module.exports = function(User) {
}
async.waterfall([
- function(next) {
+ function (next) {
db.setObject('user:' + uid + ':settings', settings, next);
},
- function(next) {
- updateDigestSetting(uid, data.dailyDigestFreq, next);
+ function (next) {
+ User.updateDigestSetting(uid, data.dailyDigestFreq, next);
},
- function(next) {
+ function (next) {
User.getSettings(uid, next);
}
], callback);
};
- function updateDigestSetting(uid, dailyDigestFreq, callback) {
+ User.updateDigestSetting = function (uid, dailyDigestFreq, callback) {
async.waterfall([
- function(next) {
+ function (next) {
db.sortedSetsRemove(['digest:day:uids', 'digest:week:uids', 'digest:month:uids'], uid, next);
},
- function(next) {
+ function (next) {
if (['day', 'week', 'month'].indexOf(dailyDigestFreq) !== -1) {
db.sortedSetAdd('digest:' + dailyDigestFreq + ':uids', Date.now(), uid, next);
} else {
@@ -155,22 +157,9 @@ module.exports = function(User) {
}
}
], callback);
- }
+ };
- User.setSetting = function(uid, key, value, callback) {
+ User.setSetting = function (uid, key, value, callback) {
db.setObjectField('user:' + uid + ':settings', key, value, callback);
};
-
- User.setGroupTitle = function(groupName, uid, callback) {
- if (groupName === 'registered-users') {
- return callback();
- }
- db.getObjectField('user:' + uid + ':settings', 'groupTitle', function(err, currentTitle) {
- if (err || (currentTitle || currentTitle === '')) {
- return callback(err);
- }
-
- User.setSetting(uid, 'groupTitle', groupName, callback);
- });
- };
};
diff --git a/src/user/topics.js b/src/user/topics.js
new file mode 100644
index 0000000000..53dade36e9
--- /dev/null
+++ b/src/user/topics.js
@@ -0,0 +1,19 @@
+'use strict';
+
+var async = require('async');
+var db = require('../database');
+
+module.exports = function (User) {
+
+ User.getIgnoredTids = function (uid, start, stop, callback) {
+ db.getSortedSetRevRange('uid:' + uid + ':ignored_tids', start, stop, callback);
+ };
+
+ User.addTopicIdToUser = function (uid, tid, timestamp, callback) {
+ async.parallel([
+ async.apply(db.sortedSetAdd, 'uid:' + uid + ':topics', timestamp, tid),
+ async.apply(User.incrementUserFieldBy, uid, 'topiccount', 1)
+ ], callback);
+ };
+
+};
\ No newline at end of file
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..bd01bf99e0
--- /dev/null
+++ b/src/views/admin/advanced/cache.tpl
@@ -0,0 +1,66 @@
+
+
+
+
+
Post Cache
+
+
+
Posts in Cache
+
{postCache.itemCount}
+
+
Average Post Size
+
{postCache.avgPostSize}
+
+
Length / Max
+
{postCache.length} / {postCache.max}
+
+
+
+ {postCache.percentFull}% Full
+
+
+
+ Post Cache Size
+
+
+
+
+
+
+
Group Cache
+
+
+
Items in Cache
+
{groupCache.itemCount}
+
+
Length / Max
+
{groupCache.length} / {groupCache.max}
+
+
+
+ {groupCache.percentFull}% Full
+
+
+
+
+
{groupCache.dump}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/admin/advanced/errors.tpl b/src/views/admin/advanced/errors.tpl
new file mode 100644
index 0000000000..d3c23e78b7
--- /dev/null
+++ b/src/views/admin/advanced/errors.tpl
@@ -0,0 +1,66 @@
+
+
+
+
+
+
404 Not Found
+
+
+
+ Route
+ Count
+
+
+
+
+ {../value}
+ {../score}
+
+
+
+
+
+
+ Hooray! There are no routes that were not found.
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
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}