" && !rtbody.test(elem) ?
- tmp :
- 0;
-
- j = elem && elem.childNodes.length;
- while (j--) {
- if (jQuery.nodeName((tbody = elem.childNodes[j]), "tbody") && !tbody.childNodes.length) {
- elem.removeChild(tbody);
- }
- }
- }
-
- jQuery.merge(nodes, tmp.childNodes);
-
- // Fix #12392 for WebKit and IE > 9
- tmp.textContent = "";
-
- // Fix #12392 for oldIE
- while (tmp.firstChild) {
- tmp.removeChild(tmp.firstChild);
- }
-
- // Remember the top-level container for proper cleanup
- tmp = safe.lastChild;
- }
- }
- }
-
- // Fix #11356: Clear elements from fragment
- if (tmp) {
- safe.removeChild(tmp);
- }
-
- // Reset defaultChecked for any radios and checkboxes
- // about to be appended to the DOM in IE 6/7 (#8060)
- if (!jQuery.support.appendChecked) {
- jQuery.grep(getAll(nodes, "input"), fixDefaultChecked);
- }
-
- i = 0;
- while ((elem = nodes[i++])) {
-
- // #4087 - If origin and destination elements are the same, and this is
- // that element, do not do anything
- if (selection && jQuery.inArray(elem, selection) !== -1) {
- continue;
- }
-
- contains = jQuery.contains(elem.ownerDocument, elem);
-
- // Append to fragment
- tmp = getAll(safe.appendChild(elem), "script");
-
- // Preserve script evaluation history
- if (contains) {
- setGlobalEval(tmp);
- }
-
- // Capture executables
- if (scripts) {
- j = 0;
- while ((elem = tmp[j++])) {
- if (rscriptType.test(elem.type || "")) {
- scripts.push(elem);
- }
- }
- }
- }
-
- tmp = null;
-
- return safe;
- },
-
- cleanData: function (elems, /* internal */ acceptData) {
- var elem, type, id, data,
- i = 0,
- internalKey = jQuery.expando,
- cache = jQuery.cache,
- deleteExpando = jQuery.support.deleteExpando,
- special = jQuery.event.special;
-
- for (;
- (elem = elems[i]) != null; i++) {
-
- if (acceptData || jQuery.acceptData(elem)) {
-
- id = elem[internalKey];
- data = id && cache[id];
-
- if (data) {
- if (data.events) {
- for (type in data.events) {
- if (special[type]) {
- jQuery.event.remove(elem, type);
-
- // This is a shortcut to avoid jQuery.event.remove's overhead
- } else {
- jQuery.removeEvent(elem, type, data.handle);
- }
- }
- }
-
- // Remove cache only if it was not already removed by jQuery.event.remove
- if (cache[id]) {
-
- delete cache[id];
-
- // IE does not allow us to delete expando properties from nodes,
- // nor does it have a removeAttribute function on Document nodes;
- // we must handle all of these cases
- if (deleteExpando) {
- delete elem[internalKey];
-
- } else if (typeof elem.removeAttribute !== core_strundefined) {
- elem.removeAttribute(internalKey);
-
- } else {
- elem[internalKey] = null;
- }
-
- core_deletedIds.push(id);
- }
- }
- }
- }
- },
-
- _evalUrl: function (url) {
- return jQuery.ajax({
- url: url,
- type: "GET",
- dataType: "script",
- async: false,
- global: false,
- "throws": true
- });
- }
- });
- jQuery.fn.extend({
- wrapAll: function (html) {
- if (jQuery.isFunction(html)) {
- return this.each(function (i) {
- jQuery(this).wrapAll(html.call(this, i));
- });
- }
-
- if (this[0]) {
- // The elements to wrap the target around
- var wrap = jQuery(html, this[0].ownerDocument).eq(0).clone(true);
-
- if (this[0].parentNode) {
- wrap.insertBefore(this[0]);
- }
-
- wrap.map(function () {
- var elem = this;
-
- while (elem.firstChild && elem.firstChild.nodeType === 1) {
- elem = elem.firstChild;
- }
-
- return elem;
- }).append(this);
- }
-
- return this;
- },
-
- wrapInner: function (html) {
- if (jQuery.isFunction(html)) {
- return this.each(function (i) {
- jQuery(this).wrapInner(html.call(this, i));
- });
- }
-
- return this.each(function () {
- var self = jQuery(this),
- contents = self.contents();
-
- if (contents.length) {
- contents.wrapAll(html);
-
- } else {
- self.append(html);
- }
- });
- },
-
- wrap: function (html) {
- var isFunction = jQuery.isFunction(html);
-
- return this.each(function (i) {
- jQuery(this).wrapAll(isFunction ? html.call(this, i) : html);
- });
- },
-
- unwrap: function () {
- return this.parent().each(function () {
- if (!jQuery.nodeName(this, "body")) {
- jQuery(this).replaceWith(this.childNodes);
- }
- }).end();
- }
- });
- var iframe, getStyles, curCSS,
- ralpha = /alpha\([^)]*\)/i,
- ropacity = /opacity\s*=\s*([^)]*)/,
- rposition = /^(top|right|bottom|left)$/,
- // swappable if display is none or starts with table except "table", "table-cell", or "table-caption"
- // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
- rdisplayswap = /^(none|table(?!-c[ea]).+)/,
- rmargin = /^margin/,
- rnumsplit = new RegExp("^(" + core_pnum + ")(.*)$", "i"),
- rnumnonpx = new RegExp("^(" + core_pnum + ")(?!px)[a-z%]+$", "i"),
- rrelNum = new RegExp("^([+-])=(" + core_pnum + ")", "i"),
- elemdisplay = {
- BODY: "block"
- },
-
- cssShow = {
- position: "absolute",
- visibility: "hidden",
- display: "block"
- },
- cssNormalTransform = {
- letterSpacing: 0,
- fontWeight: 400
- },
-
- cssExpand = ["Top", "Right", "Bottom", "Left"],
- cssPrefixes = ["Webkit", "O", "Moz", "ms"];
-
- // return a css property mapped to a potentially vendor prefixed property
-
- function vendorPropName(style, name) {
-
- // shortcut for names that are not vendor prefixed
- if (name in style) {
- return name;
- }
-
- // check for vendor prefixed names
- var capName = name.charAt(0).toUpperCase() + name.slice(1),
- origName = name,
- i = cssPrefixes.length;
-
- while (i--) {
- name = cssPrefixes[i] + capName;
- if (name in style) {
- return name;
- }
- }
-
- return origName;
- }
-
- function isHidden(elem, el) {
- // isHidden might be called from jQuery#filter function;
- // in that case, element will be second argument
- elem = el || elem;
- return jQuery.css(elem, "display") === "none" || !jQuery.contains(elem.ownerDocument, elem);
- }
-
- function showHide(elements, show) {
- var display, elem, hidden,
- values = [],
- index = 0,
- length = elements.length;
-
- for (; index < length; index++) {
- elem = elements[index];
- if (!elem.style) {
- continue;
- }
-
- values[index] = jQuery._data(elem, "olddisplay");
- display = elem.style.display;
- if (show) {
- // Reset the inline display of this element to learn if it is
- // being hidden by cascaded rules or not
- if (!values[index] && display === "none") {
- elem.style.display = "";
- }
-
- // Set elements which have been overridden with display: none
- // in a stylesheet to whatever the default browser style is
- // for such an element
- if (elem.style.display === "" && isHidden(elem)) {
- values[index] = jQuery._data(elem, "olddisplay", css_defaultDisplay(elem.nodeName));
- }
- } else {
-
- if (!values[index]) {
- hidden = isHidden(elem);
-
- if (display && display !== "none" || !hidden) {
- jQuery._data(elem, "olddisplay", hidden ? display : jQuery.css(elem, "display"));
- }
- }
- }
- }
-
- // Set the display of most of the elements in a second loop
- // to avoid the constant reflow
- for (index = 0; index < length; index++) {
- elem = elements[index];
- if (!elem.style) {
- continue;
- }
- if (!show || elem.style.display === "none" || elem.style.display === "") {
- elem.style.display = show ? values[index] || "" : "none";
- }
- }
-
- return elements;
- }
-
- jQuery.fn.extend({
- css: function (name, value) {
- return jQuery.access(this, function (elem, name, value) {
- var len, styles,
- map = {},
- i = 0;
-
- if (jQuery.isArray(name)) {
- styles = getStyles(elem);
- len = name.length;
-
- for (; i < len; i++) {
- map[name[i]] = jQuery.css(elem, name[i], false, styles);
- }
-
- return map;
- }
-
- return value !== undefined ?
- jQuery.style(elem, name, value) :
- jQuery.css(elem, name);
- }, name, value, arguments.length > 1);
- },
- show: function () {
- return showHide(this, true);
- },
- hide: function () {
- return showHide(this);
- },
- toggle: function (state) {
- if (typeof state === "boolean") {
- return state ? this.show() : this.hide();
- }
-
- return this.each(function () {
- if (isHidden(this)) {
- jQuery(this).show();
- } else {
- jQuery(this).hide();
- }
- });
- }
- });
-
- jQuery.extend({
- // Add in style property hooks for overriding the default
- // behavior of getting and setting a style property
- cssHooks: {
- opacity: {
- get: function (elem, computed) {
- if (computed) {
- // We should always get a number back from opacity
- var ret = curCSS(elem, "opacity");
- return ret === "" ? "1" : ret;
- }
- }
- }
- },
-
- // Don't automatically add "px" to these possibly-unitless properties
- cssNumber: {
- "columnCount": true,
- "fillOpacity": true,
- "fontWeight": true,
- "lineHeight": true,
- "opacity": true,
- "order": true,
- "orphans": true,
- "widows": true,
- "zIndex": true,
- "zoom": true
- },
-
- // Add in properties whose names you wish to fix before
- // setting or getting the value
- cssProps: {
- // normalize float css property
- "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat"
- },
-
- // Get and set the style property on a DOM Node
- style: function (elem, name, value, extra) {
- // Don't set styles on text and comment nodes
- if (!elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style) {
- return;
- }
-
- // Make sure that we're working with the right name
- var ret, type, hooks,
- origName = jQuery.camelCase(name),
- style = elem.style;
-
- name = jQuery.cssProps[origName] || (jQuery.cssProps[origName] = vendorPropName(style, origName));
-
- // gets hook for the prefixed version
- // followed by the unprefixed version
- hooks = jQuery.cssHooks[name] || jQuery.cssHooks[origName];
-
- // Check if we're setting a value
- if (value !== undefined) {
- type = typeof value;
-
- // convert relative number strings (+= or -=) to relative numbers. #7345
- if (type === "string" && (ret = rrelNum.exec(value))) {
- value = (ret[1] + 1) * ret[2] + parseFloat(jQuery.css(elem, name));
- // Fixes bug #9237
- type = "number";
- }
-
- // Make sure that NaN and null values aren't set. See: #7116
- if (value == null || type === "number" && isNaN(value)) {
- return;
- }
-
- // If a number was passed in, add 'px' to the (except for certain CSS properties)
- if (type === "number" && !jQuery.cssNumber[origName]) {
- value += "px";
- }
-
- // Fixes #8908, it can be done more correctly by specifing setters in cssHooks,
- // but it would mean to define eight (for every problematic property) identical functions
- if (!jQuery.support.clearCloneStyle && value === "" && name.indexOf("background") === 0) {
- style[name] = "inherit";
- }
-
- // If a hook was provided, use that value, otherwise just set the specified value
- if (!hooks || !("set" in hooks) || (value = hooks.set(elem, value, extra)) !== undefined) {
-
- // Wrapped to prevent IE from throwing errors when 'invalid' values are provided
- // Fixes bug #5509
- try {
- style[name] = value;
- } catch (e) {}
- }
-
- } else {
- // If a hook was provided get the non-computed value from there
- if (hooks && "get" in hooks && (ret = hooks.get(elem, false, extra)) !== undefined) {
- return ret;
- }
-
- // Otherwise just get the value from the style object
- return style[name];
- }
- },
-
- css: function (elem, name, extra, styles) {
- var num, val, hooks,
- origName = jQuery.camelCase(name);
-
- // Make sure that we're working with the right name
- name = jQuery.cssProps[origName] || (jQuery.cssProps[origName] = vendorPropName(elem.style, origName));
-
- // gets hook for the prefixed version
- // followed by the unprefixed version
- hooks = jQuery.cssHooks[name] || jQuery.cssHooks[origName];
-
- // If a hook was provided get the computed value from there
- if (hooks && "get" in hooks) {
- val = hooks.get(elem, true, extra);
- }
-
- // Otherwise, if a way to get the computed value exists, use that
- if (val === undefined) {
- val = curCSS(elem, name, styles);
- }
-
- //convert "normal" to computed value
- if (val === "normal" && name in cssNormalTransform) {
- val = cssNormalTransform[name];
- }
-
- // Return, converting to number if forced or a qualifier was provided and val looks numeric
- if (extra === "" || extra) {
- num = parseFloat(val);
- return extra === true || jQuery.isNumeric(num) ? num || 0 : val;
- }
- return val;
- }
- });
-
- // NOTE: we've included the "window" in window.getComputedStyle
- // because jsdom on node.js will break without it.
- if (window.getComputedStyle) {
- getStyles = function (elem) {
- return window.getComputedStyle(elem, null);
- };
-
- curCSS = function (elem, name, _computed) {
- var width, minWidth, maxWidth,
- computed = _computed || getStyles(elem),
-
- // getPropertyValue is only needed for .css('filter') in IE9, see #12537
- ret = computed ? computed.getPropertyValue(name) || computed[name] : undefined,
- style = elem.style;
-
- if (computed) {
-
- if (ret === "" && !jQuery.contains(elem.ownerDocument, elem)) {
- ret = jQuery.style(elem, name);
- }
-
- // A tribute to the "awesome hack by Dean Edwards"
- // Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right
- // Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels
- // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values
- if (rnumnonpx.test(ret) && rmargin.test(name)) {
-
- // Remember the original values
- width = style.width;
- minWidth = style.minWidth;
- maxWidth = style.maxWidth;
-
- // Put in the new values to get a computed value out
- style.minWidth = style.maxWidth = style.width = ret;
- ret = computed.width;
-
- // Revert the changed values
- style.width = width;
- style.minWidth = minWidth;
- style.maxWidth = maxWidth;
- }
- }
-
- return ret;
- };
- } else if (document.documentElement.currentStyle) {
- getStyles = function (elem) {
- return elem.currentStyle;
- };
-
- curCSS = function (elem, name, _computed) {
- var left, rs, rsLeft,
- computed = _computed || getStyles(elem),
- ret = computed ? computed[name] : undefined,
- style = elem.style;
-
- // Avoid setting ret to empty string here
- // so we don't default to auto
- if (ret == null && style && style[name]) {
- ret = style[name];
- }
-
- // From the awesome hack by Dean Edwards
- // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
-
- // If we're not dealing with a regular pixel number
- // but a number that has a weird ending, we need to convert it to pixels
- // but not position css attributes, as those are proportional to the parent element instead
- // and we can't measure the parent instead because it might trigger a "stacking dolls" problem
- if (rnumnonpx.test(ret) && !rposition.test(name)) {
-
- // Remember the original values
- left = style.left;
- rs = elem.runtimeStyle;
- rsLeft = rs && rs.left;
-
- // Put in the new values to get a computed value out
- if (rsLeft) {
- rs.left = elem.currentStyle.left;
- }
- style.left = name === "fontSize" ? "1em" : ret;
- ret = style.pixelLeft + "px";
-
- // Revert the changed values
- style.left = left;
- if (rsLeft) {
- rs.left = rsLeft;
- }
- }
-
- return ret === "" ? "auto" : ret;
- };
- }
-
- function setPositiveNumber(elem, value, subtract) {
- var matches = rnumsplit.exec(value);
- return matches ?
- // Guard against undefined "subtract", e.g., when used as in cssHooks
- Math.max(0, matches[1] - (subtract || 0)) + (matches[2] || "px") :
- value;
- }
-
- function augmentWidthOrHeight(elem, name, extra, isBorderBox, styles) {
- var i = extra === (isBorderBox ? "border" : "content") ?
- // If we already have the right measurement, avoid augmentation
- 4 :
- // Otherwise initialize for horizontal or vertical properties
- name === "width" ? 1 : 0,
-
- val = 0;
-
- for (; i < 4; i += 2) {
- // both box models exclude margin, so add it if we want it
- if (extra === "margin") {
- val += jQuery.css(elem, extra + cssExpand[i], true, styles);
- }
-
- if (isBorderBox) {
- // border-box includes padding, so remove it if we want content
- if (extra === "content") {
- val -= jQuery.css(elem, "padding" + cssExpand[i], true, styles);
- }
-
- // at this point, extra isn't border nor margin, so remove border
- if (extra !== "margin") {
- val -= jQuery.css(elem, "border" + cssExpand[i] + "Width", true, styles);
- }
- } else {
- // at this point, extra isn't content, so add padding
- val += jQuery.css(elem, "padding" + cssExpand[i], true, styles);
-
- // at this point, extra isn't content nor padding, so add border
- if (extra !== "padding") {
- val += jQuery.css(elem, "border" + cssExpand[i] + "Width", true, styles);
- }
- }
- }
-
- return val;
- }
-
- function getWidthOrHeight(elem, name, extra) {
-
- // Start with offset property, which is equivalent to the border-box value
- var valueIsBorderBox = true,
- val = name === "width" ? elem.offsetWidth : elem.offsetHeight,
- styles = getStyles(elem),
- isBorderBox = jQuery.support.boxSizing && jQuery.css(elem, "boxSizing", false, styles) === "border-box";
-
- // some non-html elements return undefined for offsetWidth, so check for null/undefined
- // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285
- // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668
- if (val <= 0 || val == null) {
- // Fall back to computed then uncomputed css if necessary
- val = curCSS(elem, name, styles);
- if (val < 0 || val == null) {
- val = elem.style[name];
- }
-
- // Computed unit is not pixels. Stop here and return.
- if (rnumnonpx.test(val)) {
- return val;
- }
-
- // we need the check for style in case a browser which returns unreliable values
- // for getComputedStyle silently falls back to the reliable elem.style
- valueIsBorderBox = isBorderBox && (jQuery.support.boxSizingReliable || val === elem.style[name]);
-
- // Normalize "", auto, and prepare for extra
- val = parseFloat(val) || 0;
- }
-
- // use the active box-sizing model to add/subtract irrelevant styles
- return (val +
- augmentWidthOrHeight(
- elem,
- name,
- extra || (isBorderBox ? "border" : "content"),
- valueIsBorderBox,
- styles
- )
- ) + "px";
- }
-
- // Try to determine the default display value of an element
-
- function css_defaultDisplay(nodeName) {
- var doc = document,
- display = elemdisplay[nodeName];
-
- if (!display) {
- display = actualDisplay(nodeName, doc);
-
- // If the simple way fails, read from inside an iframe
- if (display === "none" || !display) {
- // Use the already-created iframe if possible
- iframe = (iframe ||
- jQuery("")
- .css("cssText", "display:block !important")
- ).appendTo(doc.documentElement);
-
- // Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse
- doc = (iframe[0].contentWindow || iframe[0].contentDocument).document;
- doc.write("");
- doc.close();
-
- display = actualDisplay(nodeName, doc);
- iframe.detach();
- }
-
- // Store the correct default display
- elemdisplay[nodeName] = display;
- }
-
- return display;
- }
-
- // Called ONLY from within css_defaultDisplay
-
- function actualDisplay(name, doc) {
- var elem = jQuery(doc.createElement(name)).appendTo(doc.body),
- display = jQuery.css(elem[0], "display");
- elem.remove();
- return display;
- }
-
- jQuery.each(["height", "width"], function (i, name) {
- jQuery.cssHooks[name] = {
- get: function (elem, computed, extra) {
- if (computed) {
- // certain elements can have dimension info if we invisibly show them
- // however, it must have a current display style that would benefit from this
- return elem.offsetWidth === 0 && rdisplayswap.test(jQuery.css(elem, "display")) ?
- jQuery.swap(elem, cssShow, function () {
- return getWidthOrHeight(elem, name, extra);
- }) :
- getWidthOrHeight(elem, name, extra);
- }
- },
-
- set: function (elem, value, extra) {
- var styles = extra && getStyles(elem);
- return setPositiveNumber(elem, value, extra ?
- augmentWidthOrHeight(
- elem,
- name,
- extra,
- jQuery.support.boxSizing && jQuery.css(elem, "boxSizing", false, styles) === "border-box",
- styles
- ) : 0
- );
- }
- };
- });
-
- if (!jQuery.support.opacity) {
- jQuery.cssHooks.opacity = {
- get: function (elem, computed) {
- // IE uses filters for opacity
- return ropacity.test((computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "") ?
- (0.01 * parseFloat(RegExp.$1)) + "" :
- computed ? "1" : "";
- },
-
- set: function (elem, value) {
- var style = elem.style,
- currentStyle = elem.currentStyle,
- opacity = jQuery.isNumeric(value) ? "alpha(opacity=" + value * 100 + ")" : "",
- filter = currentStyle && currentStyle.filter || style.filter || "";
-
- // IE has trouble with opacity if it does not have layout
- // Force it by setting the zoom level
- style.zoom = 1;
-
- // if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652
- // if value === "", then remove inline opacity #12685
- if ((value >= 1 || value === "") &&
- jQuery.trim(filter.replace(ralpha, "")) === "" &&
- style.removeAttribute) {
-
- // Setting style.filter to null, "" & " " still leave "filter:" in the cssText
- // if "filter:" is present at all, clearType is disabled, we want to avoid this
- // style.removeAttribute is IE Only, but so apparently is this code path...
- style.removeAttribute("filter");
-
- // if there is no filter style applied in a css rule or unset inline opacity, we are done
- if (value === "" || currentStyle && !currentStyle.filter) {
- return;
- }
- }
-
- // otherwise, set new filter values
- style.filter = ralpha.test(filter) ?
- filter.replace(ralpha, opacity) :
- filter + " " + opacity;
- }
- };
- }
-
- // These hooks cannot be added until DOM ready because the support test
- // for it is not run until after DOM ready
- jQuery(function () {
- if (!jQuery.support.reliableMarginRight) {
- jQuery.cssHooks.marginRight = {
- get: function (elem, computed) {
- if (computed) {
- // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
- // Work around by temporarily setting element display to inline-block
- return jQuery.swap(elem, {
- "display": "inline-block"
- },
- curCSS, [elem, "marginRight"]);
- }
- }
- };
- }
-
- // Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084
- // getComputedStyle returns percent when specified for top/left/bottom/right
- // rather than make the css module depend on the offset module, we just check for it here
- if (!jQuery.support.pixelPosition && jQuery.fn.position) {
- jQuery.each(["top", "left"], function (i, prop) {
- jQuery.cssHooks[prop] = {
- get: function (elem, computed) {
- if (computed) {
- computed = curCSS(elem, prop);
- // if curCSS returns percentage, fallback to offset
- return rnumnonpx.test(computed) ?
- jQuery(elem).position()[prop] + "px" :
- computed;
- }
- }
- };
- });
- }
-
- });
-
- if (jQuery.expr && jQuery.expr.filters) {
- jQuery.expr.filters.hidden = function (elem) {
- // Support: Opera <= 12.12
- // Opera reports offsetWidths and offsetHeights less than zero on some elements
- return elem.offsetWidth <= 0 && elem.offsetHeight <= 0 ||
- (!jQuery.support.reliableHiddenOffsets && ((elem.style && elem.style.display) || jQuery.css(elem, "display")) === "none");
- };
-
- jQuery.expr.filters.visible = function (elem) {
- return !jQuery.expr.filters.hidden(elem);
- };
- }
-
- // These hooks are used by animate to expand properties
- jQuery.each({
- margin: "",
- padding: "",
- border: "Width"
- }, function (prefix, suffix) {
- jQuery.cssHooks[prefix + suffix] = {
- expand: function (value) {
- var i = 0,
- expanded = {},
-
- // assumes a single number if not a string
- parts = typeof value === "string" ? value.split(" ") : [value];
-
- for (; i < 4; i++) {
- expanded[prefix + cssExpand[i] + suffix] =
- parts[i] || parts[i - 2] || parts[0];
- }
-
- return expanded;
- }
- };
-
- if (!rmargin.test(prefix)) {
- jQuery.cssHooks[prefix + suffix].set = setPositiveNumber;
- }
- });
- var r20 = /%20/g,
- rbracket = /\[\]$/,
- rCRLF = /\r?\n/g,
- rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,
- rsubmittable = /^(?:input|select|textarea|keygen)/i;
-
- jQuery.fn.extend({
- serialize: function () {
- return jQuery.param(this.serializeArray());
- },
- serializeArray: function () {
- return this.map(function () {
- // Can add propHook for "elements" to filter or add form elements
- var elements = jQuery.prop(this, "elements");
- return elements ? jQuery.makeArray(elements) : this;
- })
- .filter(function () {
- var type = this.type;
- // Use .is(":disabled") so that fieldset[disabled] works
- return this.name && !jQuery(this).is(":disabled") &&
- rsubmittable.test(this.nodeName) && !rsubmitterTypes.test(type) &&
- (this.checked || !manipulation_rcheckableType.test(type));
- })
- .map(function (i, elem) {
- var val = jQuery(this).val();
-
- return val == null ?
- null :
- jQuery.isArray(val) ?
- jQuery.map(val, function (val) {
- return {
- name: elem.name,
- value: val.replace(rCRLF, "\r\n")
- };
- }) : {
- name: elem.name,
- value: val.replace(rCRLF, "\r\n")
- };
- }).get();
- }
- });
-
- //Serialize an array of form elements or a set of
- //key/values into a query string
- jQuery.param = function (a, traditional) {
- var prefix,
- s = [],
- add = function (key, value) {
- // If value is a function, invoke it and return its value
- value = jQuery.isFunction(value) ? value() : (value == null ? "" : value);
- s[s.length] = encodeURIComponent(key) + "=" + encodeURIComponent(value);
- };
-
- // Set traditional to true for jQuery <= 1.3.2 behavior.
- if (traditional === undefined) {
- traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional;
- }
-
- // If an array was passed in, assume that it is an array of form elements.
- if (jQuery.isArray(a) || (a.jquery && !jQuery.isPlainObject(a))) {
- // Serialize the form elements
- jQuery.each(a, function () {
- add(this.name, this.value);
- });
-
- } else {
- // If traditional, encode the "old" way (the way 1.3.2 or older
- // did it), otherwise encode params recursively.
- for (prefix in a) {
- buildParams(prefix, a[prefix], traditional, add);
- }
- }
-
- // Return the resulting serialization
- return s.join("&").replace(r20, "+");
- };
-
- function buildParams(prefix, obj, traditional, add) {
- var name;
-
- if (jQuery.isArray(obj)) {
- // Serialize array item.
- jQuery.each(obj, function (i, v) {
- if (traditional || rbracket.test(prefix)) {
- // Treat each array item as a scalar.
- add(prefix, v);
-
- } else {
- // Item is non-scalar (array or object), encode its numeric index.
- buildParams(prefix + "[" + (typeof v === "object" ? i : "") + "]", v, traditional, add);
- }
- });
-
- } else if (!traditional && jQuery.type(obj) === "object") {
- // Serialize object item.
- for (name in obj) {
- buildParams(prefix + "[" + name + "]", obj[name], traditional, add);
- }
-
- } else {
- // Serialize scalar item.
- add(prefix, obj);
- }
- }
- jQuery.each(("blur focus focusin focusout load resize scroll unload click dblclick " +
- "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
- "change select submit keydown keypress keyup error contextmenu").split(" "), function (i, name) {
-
- // Handle event binding
- jQuery.fn[name] = function (data, fn) {
- return arguments.length > 0 ?
- this.on(name, null, data, fn) :
- this.trigger(name);
- };
- });
-
- jQuery.fn.extend({
- hover: function (fnOver, fnOut) {
- return this.mouseenter(fnOver).mouseleave(fnOut || fnOver);
- },
-
- bind: function (types, data, fn) {
- return this.on(types, null, data, fn);
- },
- unbind: function (types, fn) {
- return this.off(types, null, fn);
- },
-
- delegate: function (selector, types, data, fn) {
- return this.on(types, selector, data, fn);
- },
- undelegate: function (selector, types, fn) {
- // ( namespace ) or ( selector, types [, fn] )
- return arguments.length === 1 ? this.off(selector, "**") : this.off(types, selector || "**", fn);
- }
- });
- var
- // Document location
- ajaxLocParts,
- ajaxLocation,
- ajax_nonce = jQuery.now(),
-
- ajax_rquery = /\?/,
- rhash = /#.*$/,
- rts = /([?&])_=[^&]*/,
- rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL
- // #7653, #8125, #8152: local protocol detection
- rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,
- rnoContent = /^(?:GET|HEAD)$/,
- rprotocol = /^\/\//,
- rurl = /^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,
-
- // Keep a copy of the old load method
- _load = jQuery.fn.load,
-
- /* Prefilters
- * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)
- * 2) These are called:
- * - BEFORE asking for a transport
- * - AFTER param serialization (s.data is a string if s.processData is true)
- * 3) key is the dataType
- * 4) the catchall symbol "*" can be used
- * 5) execution will start with transport dataType and THEN continue down to "*" if needed
- */
- prefilters = {},
-
- /* Transports bindings
- * 1) key is the dataType
- * 2) the catchall symbol "*" can be used
- * 3) selection will start with transport dataType and THEN go to "*" if needed
- */
- transports = {},
-
- // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression
- allTypes = "*/".concat("*");
-
- // #8138, IE may throw an exception when accessing
- // a field from window.location if document.domain has been set
- try {
- ajaxLocation = location.href;
- } catch (e) {
- // Use the href attribute of an A element
- // since IE will modify it given document.location
- ajaxLocation = document.createElement("a");
- ajaxLocation.href = "";
- ajaxLocation = ajaxLocation.href;
- }
-
- // Segment location into parts
- ajaxLocParts = rurl.exec(ajaxLocation.toLowerCase()) || [];
-
- // Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
-
- function addToPrefiltersOrTransports(structure) {
-
- // dataTypeExpression is optional and defaults to "*"
- return function (dataTypeExpression, func) {
-
- if (typeof dataTypeExpression !== "string") {
- func = dataTypeExpression;
- dataTypeExpression = "*";
- }
-
- var dataType,
- i = 0,
- dataTypes = dataTypeExpression.toLowerCase().match(core_rnotwhite) || [];
-
- if (jQuery.isFunction(func)) {
- // For each dataType in the dataTypeExpression
- while ((dataType = dataTypes[i++])) {
- // Prepend if requested
- if (dataType[0] === "+") {
- dataType = dataType.slice(1) || "*";
- (structure[dataType] = structure[dataType] || []).unshift(func);
-
- // Otherwise append
- } else {
- (structure[dataType] = structure[dataType] || []).push(func);
- }
- }
- }
- };
- }
-
- // Base inspection function for prefilters and transports
-
- function inspectPrefiltersOrTransports(structure, options, originalOptions, jqXHR) {
-
- var inspected = {},
- seekingTransport = (structure === transports);
-
- function inspect(dataType) {
- var selected;
- inspected[dataType] = true;
- jQuery.each(structure[dataType] || [], function (_, prefilterOrFactory) {
- var dataTypeOrTransport = prefilterOrFactory(options, originalOptions, jqXHR);
- if (typeof dataTypeOrTransport === "string" && !seekingTransport && !inspected[dataTypeOrTransport]) {
- options.dataTypes.unshift(dataTypeOrTransport);
- inspect(dataTypeOrTransport);
- return false;
- } else if (seekingTransport) {
- return !(selected = dataTypeOrTransport);
- }
- });
- return selected;
- }
-
- return inspect(options.dataTypes[0]) || !inspected["*"] && inspect("*");
- }
-
- // A special extend for ajax options
- // that takes "flat" options (not to be deep extended)
- // Fixes #9887
-
- function ajaxExtend(target, src) {
- var deep, key,
- flatOptions = jQuery.ajaxSettings.flatOptions || {};
-
- for (key in src) {
- if (src[key] !== undefined) {
- (flatOptions[key] ? target : (deep || (deep = {})))[key] = src[key];
- }
- }
- if (deep) {
- jQuery.extend(true, target, deep);
- }
-
- return target;
- }
-
- jQuery.fn.load = function (url, params, callback) {
- if (typeof url !== "string" && _load) {
- return _load.apply(this, arguments);
- }
-
- var selector, response, type,
- self = this,
- off = url.indexOf(" ");
-
- if (off >= 0) {
- selector = url.slice(off, url.length);
- url = url.slice(0, off);
- }
-
- // If it's a function
- if (jQuery.isFunction(params)) {
-
- // We assume that it's the callback
- callback = params;
- params = undefined;
-
- // Otherwise, build a param string
- } else if (params && typeof params === "object") {
- type = "POST";
- }
-
- // If we have elements to modify, make the request
- if (self.length > 0) {
- jQuery.ajax({
- url: url,
-
- // if "type" variable is undefined, then "GET" method will be used
- type: type,
- dataType: "html",
- data: params
- }).done(function (responseText) {
-
- // Save response for use in complete callback
- response = arguments;
-
- self.html(selector ?
-
- // If a selector was specified, locate the right elements in a dummy div
- // Exclude scripts to avoid IE 'Permission Denied' errors
- jQuery("").append(jQuery.parseHTML(responseText)).find(selector) :
-
- // Otherwise use the full result
- responseText);
-
- }).complete(callback && function (jqXHR, status) {
- self.each(callback, response || [jqXHR.responseText, status, jqXHR]);
- });
- }
-
- return this;
- };
-
- // Attach a bunch of functions for handling common AJAX events
- jQuery.each(["ajaxStart", "ajaxStop", "ajaxComplete", "ajaxError", "ajaxSuccess", "ajaxSend"], function (i, type) {
- jQuery.fn[type] = function (fn) {
- return this.on(type, fn);
- };
- });
-
- jQuery.extend({
-
- // Counter for holding the number of active queries
- active: 0,
-
- // Last-Modified header cache for next request
- lastModified: {},
- etag: {},
-
- ajaxSettings: {
- url: ajaxLocation,
- type: "GET",
- isLocal: rlocalProtocol.test(ajaxLocParts[1]),
- global: true,
- processData: true,
- async: true,
- contentType: "application/x-www-form-urlencoded; charset=UTF-8",
- /*
- timeout: 0,
- data: null,
- dataType: null,
- username: null,
- password: null,
- cache: null,
- throws: false,
- traditional: false,
- headers: {},
- */
-
- accepts: {
- "*": allTypes,
- text: "text/plain",
- html: "text/html",
- xml: "application/xml, text/xml",
- json: "application/json, text/javascript"
- },
-
- contents: {
- xml: /xml/,
- html: /html/,
- json: /json/
- },
-
- responseFields: {
- xml: "responseXML",
- text: "responseText",
- json: "responseJSON"
- },
-
- // Data converters
- // Keys separate source (or catchall "*") and destination types with a single space
- converters: {
-
- // Convert anything to text
- "* text": String,
-
- // Text to html (true = no transformation)
- "text html": true,
-
- // Evaluate text as a json expression
- "text json": jQuery.parseJSON,
-
- // Parse text as xml
- "text xml": jQuery.parseXML
- },
-
- // For options that shouldn't be deep extended:
- // you can add your own custom options here if
- // and when you create one that shouldn't be
- // deep extended (see ajaxExtend)
- flatOptions: {
- url: true,
- context: true
- }
- },
-
- // Creates a full fledged settings object into target
- // with both ajaxSettings and settings fields.
- // If target is omitted, writes into ajaxSettings.
- ajaxSetup: function (target, settings) {
- return settings ?
-
- // Building a settings object
- ajaxExtend(ajaxExtend(target, jQuery.ajaxSettings), settings) :
-
- // Extending ajaxSettings
- ajaxExtend(jQuery.ajaxSettings, target);
- },
-
- ajaxPrefilter: addToPrefiltersOrTransports(prefilters),
- ajaxTransport: addToPrefiltersOrTransports(transports),
-
- // Main method
- ajax: function (url, options) {
-
- // If url is an object, simulate pre-1.5 signature
- if (typeof url === "object") {
- options = url;
- url = undefined;
- }
-
- // Force options to be an object
- options = options || {};
-
- var // Cross-domain detection vars
- parts,
- // Loop variable
- i,
- // URL without anti-cache param
- cacheURL,
- // Response headers as string
- responseHeadersString,
- // timeout handle
- timeoutTimer,
-
- // To know if global events are to be dispatched
- fireGlobals,
-
- transport,
- // Response headers
- responseHeaders,
- // Create the final options object
- s = jQuery.ajaxSetup({}, options),
- // Callbacks context
- callbackContext = s.context || s,
- // Context for global events is callbackContext if it is a DOM node or jQuery collection
- globalEventContext = s.context && (callbackContext.nodeType || callbackContext.jquery) ?
- jQuery(callbackContext) :
- jQuery.event,
- // Deferreds
- deferred = jQuery.Deferred(),
- completeDeferred = jQuery.Callbacks("once memory"),
- // Status-dependent callbacks
- statusCode = s.statusCode || {},
- // Headers (they are sent all at once)
- requestHeaders = {},
- requestHeadersNames = {},
- // The jqXHR state
- state = 0,
- // Default abort message
- strAbort = "canceled",
- // Fake xhr
- jqXHR = {
- readyState: 0,
-
- // Builds headers hashtable if needed
- getResponseHeader: function (key) {
- var match;
- if (state === 2) {
- if (!responseHeaders) {
- responseHeaders = {};
- while ((match = rheaders.exec(responseHeadersString))) {
- responseHeaders[match[1].toLowerCase()] = match[2];
- }
- }
- match = responseHeaders[key.toLowerCase()];
- }
- return match == null ? null : match;
- },
-
- // Raw string
- getAllResponseHeaders: function () {
- return state === 2 ? responseHeadersString : null;
- },
-
- // Caches the header
- setRequestHeader: function (name, value) {
- var lname = name.toLowerCase();
- if (!state) {
- name = requestHeadersNames[lname] = requestHeadersNames[lname] || name;
- requestHeaders[name] = value;
- }
- return this;
- },
-
- // Overrides response content-type header
- overrideMimeType: function (type) {
- if (!state) {
- s.mimeType = type;
- }
- return this;
- },
-
- // Status-dependent callbacks
- statusCode: function (map) {
- var code;
- if (map) {
- if (state < 2) {
- for (code in map) {
- // Lazy-add the new callback in a way that preserves old ones
- statusCode[code] = [statusCode[code], map[code]];
- }
- } else {
- // Execute the appropriate callbacks
- jqXHR.always(map[jqXHR.status]);
- }
- }
- return this;
- },
-
- // Cancel the request
- abort: function (statusText) {
- var finalText = statusText || strAbort;
- if (transport) {
- transport.abort(finalText);
- }
- done(0, finalText);
- return this;
- }
- };
-
- // Attach deferreds
- deferred.promise(jqXHR).complete = completeDeferred.add;
- jqXHR.success = jqXHR.done;
- jqXHR.error = jqXHR.fail;
-
- // Remove hash character (#7531: and string promotion)
- // Add protocol if not provided (#5866: IE7 issue with protocol-less urls)
- // Handle falsy url in the settings object (#10093: consistency with old signature)
- // We also use the url parameter if available
- s.url = ((url || s.url || ajaxLocation) + "").replace(rhash, "").replace(rprotocol, ajaxLocParts[1] + "//");
-
- // Alias method option to type as per ticket #12004
- s.type = options.method || options.type || s.method || s.type;
-
- // Extract dataTypes list
- s.dataTypes = jQuery.trim(s.dataType || "*").toLowerCase().match(core_rnotwhite) || [""];
-
- // A cross-domain request is in order when we have a protocol:host:port mismatch
- if (s.crossDomain == null) {
- parts = rurl.exec(s.url.toLowerCase());
- s.crossDomain = !! (parts &&
- (parts[1] !== ajaxLocParts[1] || parts[2] !== ajaxLocParts[2] ||
- (parts[3] || (parts[1] === "http:" ? "80" : "443")) !==
- (ajaxLocParts[3] || (ajaxLocParts[1] === "http:" ? "80" : "443")))
- );
- }
-
- // Convert data if not already a string
- if (s.data && s.processData && typeof s.data !== "string") {
- s.data = jQuery.param(s.data, s.traditional);
- }
-
- // Apply prefilters
- inspectPrefiltersOrTransports(prefilters, s, options, jqXHR);
-
- // If request was aborted inside a prefilter, stop there
- if (state === 2) {
- return jqXHR;
- }
-
- // We can fire global events as of now if asked to
- fireGlobals = s.global;
-
- // Watch for a new set of requests
- if (fireGlobals && jQuery.active++ === 0) {
- jQuery.event.trigger("ajaxStart");
- }
-
- // Uppercase the type
- s.type = s.type.toUpperCase();
-
- // Determine if request has content
- s.hasContent = !rnoContent.test(s.type);
-
- // Save the URL in case we're toying with the If-Modified-Since
- // and/or If-None-Match header later on
- cacheURL = s.url;
-
- // More options handling for requests with no content
- if (!s.hasContent) {
-
- // If data is available, append data to url
- if (s.data) {
- cacheURL = (s.url += (ajax_rquery.test(cacheURL) ? "&" : "?") + s.data);
- // #9682: remove data so that it's not used in an eventual retry
- delete s.data;
- }
-
- // Add anti-cache in url if needed
- if (s.cache === false) {
- s.url = rts.test(cacheURL) ?
-
- // If there is already a '_' parameter, set its value
- cacheURL.replace(rts, "$1_=" + ajax_nonce++) :
-
- // Otherwise add one to the end
- cacheURL + (ajax_rquery.test(cacheURL) ? "&" : "?") + "_=" + ajax_nonce++;
- }
- }
-
- // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
- if (s.ifModified) {
- if (jQuery.lastModified[cacheURL]) {
- jqXHR.setRequestHeader("If-Modified-Since", jQuery.lastModified[cacheURL]);
- }
- if (jQuery.etag[cacheURL]) {
- jqXHR.setRequestHeader("If-None-Match", jQuery.etag[cacheURL]);
- }
- }
-
- // Set the correct header, if data is being sent
- if (s.data && s.hasContent && s.contentType !== false || options.contentType) {
- jqXHR.setRequestHeader("Content-Type", s.contentType);
- }
-
- // Set the Accepts header for the server, depending on the dataType
- jqXHR.setRequestHeader(
- "Accept",
- s.dataTypes[0] && s.accepts[s.dataTypes[0]] ?
- s.accepts[s.dataTypes[0]] + (s.dataTypes[0] !== "*" ? ", " + allTypes + "; q=0.01" : "") :
- s.accepts["*"]
- );
-
- // Check for headers option
- for (i in s.headers) {
- jqXHR.setRequestHeader(i, s.headers[i]);
- }
-
- // Allow custom headers/mimetypes and early abort
- if (s.beforeSend && (s.beforeSend.call(callbackContext, jqXHR, s) === false || state === 2)) {
- // Abort if not done already and return
- return jqXHR.abort();
- }
-
- // aborting is no longer a cancellation
- strAbort = "abort";
-
- // Install callbacks on deferreds
- for (i in {
- success: 1,
- error: 1,
- complete: 1
- }) {
- jqXHR[i](s[i]);
- }
-
- // Get transport
- transport = inspectPrefiltersOrTransports(transports, s, options, jqXHR);
-
- // If no transport, we auto-abort
- if (!transport) {
- done(-1, "No Transport");
- } else {
- jqXHR.readyState = 1;
-
- // Send global event
- if (fireGlobals) {
- globalEventContext.trigger("ajaxSend", [jqXHR, s]);
- }
- // Timeout
- if (s.async && s.timeout > 0) {
- timeoutTimer = setTimeout(function () {
- jqXHR.abort("timeout");
- }, s.timeout);
- }
-
- try {
- state = 1;
- transport.send(requestHeaders, done);
- } catch (e) {
- // Propagate exception as error if not done
- if (state < 2) {
- done(-1, e);
- // Simply rethrow otherwise
- } else {
- throw e;
- }
- }
- }
-
- // Callback for when everything is done
-
- function done(status, nativeStatusText, responses, headers) {
- var isSuccess, success, error, response, modified,
- statusText = nativeStatusText;
-
- // Called once
- if (state === 2) {
- return;
- }
-
- // State is "done" now
- state = 2;
-
- // Clear timeout if it exists
- if (timeoutTimer) {
- clearTimeout(timeoutTimer);
- }
-
- // Dereference transport for early garbage collection
- // (no matter how long the jqXHR object will be used)
- transport = undefined;
-
- // Cache response headers
- responseHeadersString = headers || "";
-
- // Set readyState
- jqXHR.readyState = status > 0 ? 4 : 0;
-
- // Determine if successful
- isSuccess = status >= 200 && status < 300 || status === 304;
-
- // Get response data
- if (responses) {
- response = ajaxHandleResponses(s, jqXHR, responses);
- }
-
- // Convert no matter what (that way responseXXX fields are always set)
- response = ajaxConvert(s, response, jqXHR, isSuccess);
-
- // If successful, handle type chaining
- if (isSuccess) {
-
- // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
- if (s.ifModified) {
- modified = jqXHR.getResponseHeader("Last-Modified");
- if (modified) {
- jQuery.lastModified[cacheURL] = modified;
- }
- modified = jqXHR.getResponseHeader("etag");
- if (modified) {
- jQuery.etag[cacheURL] = modified;
- }
- }
-
- // if no content
- if (status === 204 || s.type === "HEAD") {
- statusText = "nocontent";
-
- // if not modified
- } else if (status === 304) {
- statusText = "notmodified";
-
- // If we have data, let's convert it
- } else {
- statusText = response.state;
- success = response.data;
- error = response.error;
- isSuccess = !error;
- }
- } else {
- // We extract error from statusText
- // then normalize statusText and status for non-aborts
- error = statusText;
- if (status || !statusText) {
- statusText = "error";
- if (status < 0) {
- status = 0;
- }
- }
- }
-
- // Set data for the fake xhr object
- jqXHR.status = status;
- jqXHR.statusText = (nativeStatusText || statusText) + "";
-
- // Success/Error
- if (isSuccess) {
- deferred.resolveWith(callbackContext, [success, statusText, jqXHR]);
- } else {
- deferred.rejectWith(callbackContext, [jqXHR, statusText, error]);
- }
-
- // Status-dependent callbacks
- jqXHR.statusCode(statusCode);
- statusCode = undefined;
-
- if (fireGlobals) {
- globalEventContext.trigger(isSuccess ? "ajaxSuccess" : "ajaxError", [jqXHR, s, isSuccess ? success : error]);
- }
-
- // Complete
- completeDeferred.fireWith(callbackContext, [jqXHR, statusText]);
-
- if (fireGlobals) {
- globalEventContext.trigger("ajaxComplete", [jqXHR, s]);
- // Handle the global AJAX counter
- if (!(--jQuery.active)) {
- jQuery.event.trigger("ajaxStop");
- }
- }
- }
-
- return jqXHR;
- },
-
- getJSON: function (url, data, callback) {
- return jQuery.get(url, data, callback, "json");
- },
-
- getScript: function (url, callback) {
- return jQuery.get(url, undefined, callback, "script");
- }
- });
-
- jQuery.each(["get", "post"], function (i, method) {
- jQuery[method] = function (url, data, callback, type) {
- // shift arguments if data argument was omitted
- if (jQuery.isFunction(data)) {
- type = type || callback;
- callback = data;
- data = undefined;
- }
-
- return jQuery.ajax({
- url: url,
- type: method,
- dataType: type,
- data: data,
- success: callback
- });
- };
- });
-
- /* Handles responses to an ajax request:
- * - finds the right dataType (mediates between content-type and expected dataType)
- * - returns the corresponding response
- */
-
- function ajaxHandleResponses(s, jqXHR, responses) {
- var firstDataType, ct, finalDataType, type,
- contents = s.contents,
- dataTypes = s.dataTypes;
-
- // Remove auto dataType and get content-type in the process
- while (dataTypes[0] === "*") {
- dataTypes.shift();
- if (ct === undefined) {
- ct = s.mimeType || jqXHR.getResponseHeader("Content-Type");
- }
- }
-
- // Check if we're dealing with a known content-type
- if (ct) {
- for (type in contents) {
- if (contents[type] && contents[type].test(ct)) {
- dataTypes.unshift(type);
- break;
- }
- }
- }
-
- // Check to see if we have a response for the expected dataType
- if (dataTypes[0] in responses) {
- finalDataType = dataTypes[0];
- } else {
- // Try convertible dataTypes
- for (type in responses) {
- if (!dataTypes[0] || s.converters[type + " " + dataTypes[0]]) {
- finalDataType = type;
- break;
- }
- if (!firstDataType) {
- firstDataType = type;
- }
- }
- // Or just use first one
- finalDataType = finalDataType || firstDataType;
- }
-
- // If we found a dataType
- // We add the dataType to the list if needed
- // and return the corresponding response
- if (finalDataType) {
- if (finalDataType !== dataTypes[0]) {
- dataTypes.unshift(finalDataType);
- }
- return responses[finalDataType];
- }
- }
-
- /* Chain conversions given the request and the original response
- * Also sets the responseXXX fields on the jqXHR instance
- */
-
- function ajaxConvert(s, response, jqXHR, isSuccess) {
- var conv2, current, conv, tmp, prev,
- converters = {},
- // Work with a copy of dataTypes in case we need to modify it for conversion
- dataTypes = s.dataTypes.slice();
-
- // Create converters map with lowercased keys
- if (dataTypes[1]) {
- for (conv in s.converters) {
- converters[conv.toLowerCase()] = s.converters[conv];
- }
- }
-
- current = dataTypes.shift();
-
- // Convert to each sequential dataType
- while (current) {
-
- if (s.responseFields[current]) {
- jqXHR[s.responseFields[current]] = response;
- }
-
- // Apply the dataFilter if provided
- if (!prev && isSuccess && s.dataFilter) {
- response = s.dataFilter(response, s.dataType);
- }
-
- prev = current;
- current = dataTypes.shift();
-
- if (current) {
-
- // There's only work to do if current dataType is non-auto
- if (current === "*") {
-
- current = prev;
-
- // Convert response if prev dataType is non-auto and differs from current
- } else if (prev !== "*" && prev !== current) {
-
- // Seek a direct converter
- conv = converters[prev + " " + current] || converters["* " + current];
-
- // If none found, seek a pair
- if (!conv) {
- for (conv2 in converters) {
-
- // If conv2 outputs current
- tmp = conv2.split(" ");
- if (tmp[1] === current) {
-
- // If prev can be converted to accepted input
- conv = converters[prev + " " + tmp[0]] ||
- converters["* " + tmp[0]];
- if (conv) {
- // Condense equivalence converters
- if (conv === true) {
- conv = converters[conv2];
-
- // Otherwise, insert the intermediate dataType
- } else if (converters[conv2] !== true) {
- current = tmp[0];
- dataTypes.unshift(tmp[1]);
- }
- break;
- }
- }
- }
- }
-
- // Apply converter (if not an equivalence)
- if (conv !== true) {
-
- // Unless errors are allowed to bubble, catch and return them
- if (conv && s["throws"]) {
- response = conv(response);
- } else {
- try {
- response = conv(response);
- } catch (e) {
- return {
- state: "parsererror",
- error: conv ? e : "No conversion from " + prev + " to " + current
- };
- }
- }
- }
- }
- }
- }
-
- return {
- state: "success",
- data: response
- };
- }
- // Install script dataType
- jQuery.ajaxSetup({
- accepts: {
- script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"
- },
- contents: {
- script: /(?:java|ecma)script/
- },
- converters: {
- "text script": function (text) {
- jQuery.globalEval(text);
- return text;
- }
- }
- });
-
- // Handle cache's special case and global
- jQuery.ajaxPrefilter("script", function (s) {
- if (s.cache === undefined) {
- s.cache = false;
- }
- if (s.crossDomain) {
- s.type = "GET";
- s.global = false;
- }
- });
-
- // Bind script tag hack transport
- jQuery.ajaxTransport("script", function (s) {
-
- // This transport only deals with cross domain requests
- if (s.crossDomain) {
-
- var script,
- head = document.head || jQuery("head")[0] || document.documentElement;
-
- return {
-
- send: function (_, callback) {
-
- script = document.createElement("script");
-
- script.async = true;
-
- if (s.scriptCharset) {
- script.charset = s.scriptCharset;
- }
-
- script.src = s.url;
-
- // Attach handlers for all browsers
- script.onload = script.onreadystatechange = function (_, isAbort) {
-
- if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState)) {
-
- // Handle memory leak in IE
- script.onload = script.onreadystatechange = null;
-
- // Remove the script
- if (script.parentNode) {
- script.parentNode.removeChild(script);
- }
-
- // Dereference the script
- script = null;
-
- // Callback if not abort
- if (!isAbort) {
- callback(200, "success");
- }
- }
- };
-
- // Circumvent IE6 bugs with base elements (#2709 and #4378) by prepending
- // Use native DOM manipulation to avoid our domManip AJAX trickery
- head.insertBefore(script, head.firstChild);
- },
-
- abort: function () {
- if (script) {
- script.onload(undefined, true);
- }
- }
- };
- }
- });
- var oldCallbacks = [],
- rjsonp = /(=)\?(?=&|$)|\?\?/;
-
- // Default jsonp settings
- jQuery.ajaxSetup({
- jsonp: "callback",
- jsonpCallback: function () {
- var callback = oldCallbacks.pop() || (jQuery.expando + "_" + (ajax_nonce++));
- this[callback] = true;
- return callback;
- }
- });
-
- // Detect, normalize options and install callbacks for jsonp requests
- jQuery.ajaxPrefilter("json jsonp", function (s, originalSettings, jqXHR) {
-
- var callbackName, overwritten, responseContainer,
- jsonProp = s.jsonp !== false && (rjsonp.test(s.url) ?
- "url" :
- typeof s.data === "string" && !(s.contentType || "").indexOf("application/x-www-form-urlencoded") && rjsonp.test(s.data) && "data"
- );
-
- // Handle iff the expected data type is "jsonp" or we have a parameter to set
- if (jsonProp || s.dataTypes[0] === "jsonp") {
-
- // Get callback name, remembering preexisting value associated with it
- callbackName = s.jsonpCallback = jQuery.isFunction(s.jsonpCallback) ?
- s.jsonpCallback() :
- s.jsonpCallback;
-
- // Insert callback into url or form data
- if (jsonProp) {
- s[jsonProp] = s[jsonProp].replace(rjsonp, "$1" + callbackName);
- } else if (s.jsonp !== false) {
- s.url += (ajax_rquery.test(s.url) ? "&" : "?") + s.jsonp + "=" + callbackName;
- }
-
- // Use data converter to retrieve json after script execution
- s.converters["script json"] = function () {
- if (!responseContainer) {
- jQuery.error(callbackName + " was not called");
- }
- return responseContainer[0];
- };
-
- // force json dataType
- s.dataTypes[0] = "json";
-
- // Install callback
- overwritten = window[callbackName];
- window[callbackName] = function () {
- responseContainer = arguments;
- };
-
- // Clean-up function (fires after converters)
- jqXHR.always(function () {
- // Restore preexisting value
- window[callbackName] = overwritten;
-
- // Save back as free
- if (s[callbackName]) {
- // make sure that re-using the options doesn't screw things around
- s.jsonpCallback = originalSettings.jsonpCallback;
-
- // save the callback name for future use
- oldCallbacks.push(callbackName);
- }
-
- // Call if it was a function and we have a response
- if (responseContainer && jQuery.isFunction(overwritten)) {
- overwritten(responseContainer[0]);
- }
-
- responseContainer = overwritten = undefined;
- });
-
- // Delegate to script
- return "script";
- }
- });
- var xhrCallbacks, xhrSupported,
- xhrId = 0,
- // #5280: Internet Explorer will keep connections alive if we don't abort on unload
- xhrOnUnloadAbort = window.ActiveXObject && function () {
- // Abort all pending requests
- var key;
- for (key in xhrCallbacks) {
- xhrCallbacks[key](undefined, true);
- }
- };
-
- // Functions to create xhrs
-
- function createStandardXHR() {
- try {
- return new window.XMLHttpRequest();
- } catch (e) {}
- }
-
- function createActiveXHR() {
- try {
- return new window.ActiveXObject("Microsoft.XMLHTTP");
- } catch (e) {}
- }
-
- // Create the request object
- // (This is still attached to ajaxSettings for backward compatibility)
- jQuery.ajaxSettings.xhr = window.ActiveXObject ?
- /* Microsoft failed to properly
- * implement the XMLHttpRequest in IE7 (can't request local files),
- * so we use the ActiveXObject when it is available
- * Additionally XMLHttpRequest can be disabled in IE7/IE8 so
- * we need a fallback.
- */
-
- function () {
- return !this.isLocal && createStandardXHR() || createActiveXHR();
- } :
- // For all other browsers, use the standard XMLHttpRequest object
- createStandardXHR;
-
- // Determine support properties
- xhrSupported = jQuery.ajaxSettings.xhr();
- jQuery.support.cors = !! xhrSupported && ("withCredentials" in xhrSupported);
- xhrSupported = jQuery.support.ajax = !! xhrSupported;
-
- // Create transport if the browser can provide an xhr
- if (xhrSupported) {
-
- jQuery.ajaxTransport(function (s) {
- // Cross domain only allowed if supported through XMLHttpRequest
- if (!s.crossDomain || jQuery.support.cors) {
-
- var callback;
-
- return {
- send: function (headers, complete) {
-
- // Get a new xhr
- var handle, i,
- xhr = s.xhr();
-
- // Open the socket
- // Passing null username, generates a login popup on Opera (#2865)
- if (s.username) {
- xhr.open(s.type, s.url, s.async, s.username, s.password);
- } else {
- xhr.open(s.type, s.url, s.async);
- }
-
- // Apply custom fields if provided
- if (s.xhrFields) {
- for (i in s.xhrFields) {
- xhr[i] = s.xhrFields[i];
- }
- }
-
- // Override mime type if needed
- if (s.mimeType && xhr.overrideMimeType) {
- xhr.overrideMimeType(s.mimeType);
- }
-
- // X-Requested-With header
- // For cross-domain requests, seeing as conditions for a preflight are
- // akin to a jigsaw puzzle, we simply never set it to be sure.
- // (it can always be set on a per-request basis or even using ajaxSetup)
- // For same-domain requests, won't change header if already provided.
- if (!s.crossDomain && !headers["X-Requested-With"]) {
- headers["X-Requested-With"] = "XMLHttpRequest";
- }
-
- // Need an extra try/catch for cross domain requests in Firefox 3
- try {
- for (i in headers) {
- xhr.setRequestHeader(i, headers[i]);
- }
- } catch (err) {}
-
- // Do send the request
- // This may raise an exception which is actually
- // handled in jQuery.ajax (so no try/catch here)
- xhr.send((s.hasContent && s.data) || null);
-
- // Listener
- callback = function (_, isAbort) {
- var status, responseHeaders, statusText, responses;
-
- // Firefox throws exceptions when accessing properties
- // of an xhr when a network error occurred
- // http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE)
- try {
-
- // Was never called and is aborted or complete
- if (callback && (isAbort || xhr.readyState === 4)) {
-
- // Only called once
- callback = undefined;
-
- // Do not keep as active anymore
- if (handle) {
- xhr.onreadystatechange = jQuery.noop;
- if (xhrOnUnloadAbort) {
- delete xhrCallbacks[handle];
- }
- }
-
- // If it's an abort
- if (isAbort) {
- // Abort it manually if needed
- if (xhr.readyState !== 4) {
- xhr.abort();
- }
- } else {
- responses = {};
- status = xhr.status;
- responseHeaders = xhr.getAllResponseHeaders();
-
- // When requesting binary data, IE6-9 will throw an exception
- // on any attempt to access responseText (#11426)
- if (typeof xhr.responseText === "string") {
- responses.text = xhr.responseText;
- }
-
- // Firefox throws an exception when accessing
- // statusText for faulty cross-domain requests
- try {
- statusText = xhr.statusText;
- } catch (e) {
- // We normalize with Webkit giving an empty statusText
- statusText = "";
- }
-
- // Filter status for non standard behaviors
-
- // If the request is local and we have data: assume a success
- // (success with no data won't get notified, that's the best we
- // can do given current implementations)
- if (!status && s.isLocal && !s.crossDomain) {
- status = responses.text ? 200 : 404;
- // IE - #1450: sometimes returns 1223 when it should be 204
- } else if (status === 1223) {
- status = 204;
- }
- }
- }
- } catch (firefoxAccessException) {
- if (!isAbort) {
- complete(-1, firefoxAccessException);
- }
- }
-
- // Call complete if needed
- if (responses) {
- complete(status, statusText, responses, responseHeaders);
- }
- };
-
- if (!s.async) {
- // if we're in sync mode we fire the callback
- callback();
- } else if (xhr.readyState === 4) {
- // (IE6 & IE7) if it's in cache and has been
- // retrieved directly we need to fire the callback
- setTimeout(callback);
- } else {
- handle = ++xhrId;
- if (xhrOnUnloadAbort) {
- // Create the active xhrs callbacks list if needed
- // and attach the unload handler
- if (!xhrCallbacks) {
- xhrCallbacks = {};
- jQuery(window).unload(xhrOnUnloadAbort);
- }
- // Add to list of active xhrs callbacks
- xhrCallbacks[handle] = callback;
- }
- xhr.onreadystatechange = callback;
- }
- },
-
- abort: function () {
- if (callback) {
- callback(undefined, true);
- }
- }
- };
- }
- });
- }
- var fxNow, timerId,
- rfxtypes = /^(?:toggle|show|hide)$/,
- rfxnum = new RegExp("^(?:([+-])=|)(" + core_pnum + ")([a-z%]*)$", "i"),
- rrun = /queueHooks$/,
- animationPrefilters = [defaultPrefilter],
- tweeners = {
- "*": [
- function (prop, value) {
- var tween = this.createTween(prop, value),
- target = tween.cur(),
- parts = rfxnum.exec(value),
- unit = parts && parts[3] || (jQuery.cssNumber[prop] ? "" : "px"),
-
- // Starting value computation is required for potential unit mismatches
- start = (jQuery.cssNumber[prop] || unit !== "px" && +target) &&
- rfxnum.exec(jQuery.css(tween.elem, prop)),
- scale = 1,
- maxIterations = 20;
-
- if (start && start[3] !== unit) {
- // Trust units reported by jQuery.css
- unit = unit || start[3];
-
- // Make sure we update the tween properties later on
- parts = parts || [];
-
- // Iteratively approximate from a nonzero starting point
- start = +target || 1;
-
- do {
- // If previous iteration zeroed out, double until we get *something*
- // Use a string for doubling factor so we don't accidentally see scale as unchanged below
- scale = scale || ".5";
-
- // Adjust and apply
- start = start / scale;
- jQuery.style(tween.elem, prop, start + unit);
-
- // Update scale, tolerating zero or NaN from tween.cur()
- // And breaking the loop if scale is unchanged or perfect, or if we've just had enough
- } while (scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations);
- }
-
- // Update tween properties
- if (parts) {
- start = tween.start = +start || +target || 0;
- tween.unit = unit;
- // If a +=/-= token was provided, we're doing a relative animation
- tween.end = parts[1] ?
- start + (parts[1] + 1) * parts[2] : +parts[2];
- }
-
- return tween;
- }
- ]
- };
-
- // Animations created synchronously will run synchronously
-
- function createFxNow() {
- setTimeout(function () {
- fxNow = undefined;
- });
- return (fxNow = jQuery.now());
- }
-
- function createTween(value, prop, animation) {
- var tween,
- collection = (tweeners[prop] || []).concat(tweeners["*"]),
- index = 0,
- length = collection.length;
- for (; index < length; index++) {
- if ((tween = collection[index].call(animation, prop, value))) {
-
- // we're done with this property
- return tween;
- }
- }
- }
-
- function Animation(elem, properties, options) {
- var result,
- stopped,
- index = 0,
- length = animationPrefilters.length,
- deferred = jQuery.Deferred().always(function () {
- // don't match elem in the :animated selector
- delete tick.elem;
- }),
- tick = function () {
- if (stopped) {
- return false;
- }
- var currentTime = fxNow || createFxNow(),
- remaining = Math.max(0, animation.startTime + animation.duration - currentTime),
- // archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497)
- temp = remaining / animation.duration || 0,
- percent = 1 - temp,
- index = 0,
- length = animation.tweens.length;
-
- for (; index < length; index++) {
- animation.tweens[index].run(percent);
- }
-
- deferred.notifyWith(elem, [animation, percent, remaining]);
-
- if (percent < 1 && length) {
- return remaining;
- } else {
- deferred.resolveWith(elem, [animation]);
- return false;
- }
- },
- animation = deferred.promise({
- elem: elem,
- props: jQuery.extend({}, properties),
- opts: jQuery.extend(true, {
- specialEasing: {}
- }, options),
- originalProperties: properties,
- originalOptions: options,
- startTime: fxNow || createFxNow(),
- duration: options.duration,
- tweens: [],
- createTween: function (prop, end) {
- var tween = jQuery.Tween(elem, animation.opts, prop, end,
- animation.opts.specialEasing[prop] || animation.opts.easing);
- animation.tweens.push(tween);
- return tween;
- },
- stop: function (gotoEnd) {
- var index = 0,
- // if we are going to the end, we want to run all the tweens
- // otherwise we skip this part
- length = gotoEnd ? animation.tweens.length : 0;
- if (stopped) {
- return this;
- }
- stopped = true;
- for (; index < length; index++) {
- animation.tweens[index].run(1);
- }
-
- // resolve when we played the last frame
- // otherwise, reject
- if (gotoEnd) {
- deferred.resolveWith(elem, [animation, gotoEnd]);
- } else {
- deferred.rejectWith(elem, [animation, gotoEnd]);
- }
- return this;
- }
- }),
- props = animation.props;
-
- propFilter(props, animation.opts.specialEasing);
-
- for (; index < length; index++) {
- result = animationPrefilters[index].call(animation, elem, props, animation.opts);
- if (result) {
- return result;
- }
- }
-
- jQuery.map(props, createTween, animation);
-
- if (jQuery.isFunction(animation.opts.start)) {
- animation.opts.start.call(elem, animation);
- }
-
- jQuery.fx.timer(
- jQuery.extend(tick, {
- elem: elem,
- anim: animation,
- queue: animation.opts.queue
- })
- );
-
- // attach callbacks from options
- return animation.progress(animation.opts.progress)
- .done(animation.opts.done, animation.opts.complete)
- .fail(animation.opts.fail)
- .always(animation.opts.always);
- }
-
- function propFilter(props, specialEasing) {
- var index, name, easing, value, hooks;
-
- // camelCase, specialEasing and expand cssHook pass
- for (index in props) {
- name = jQuery.camelCase(index);
- easing = specialEasing[name];
- value = props[index];
- if (jQuery.isArray(value)) {
- easing = value[1];
- value = props[index] = value[0];
- }
-
- if (index !== name) {
- props[name] = value;
- delete props[index];
- }
-
- hooks = jQuery.cssHooks[name];
- if (hooks && "expand" in hooks) {
- value = hooks.expand(value);
- delete props[name];
-
- // not quite $.extend, this wont overwrite keys already present.
- // also - reusing 'index' from above because we have the correct "name"
- for (index in value) {
- if (!(index in props)) {
- props[index] = value[index];
- specialEasing[index] = easing;
- }
- }
- } else {
- specialEasing[name] = easing;
- }
- }
- }
-
- jQuery.Animation = jQuery.extend(Animation, {
-
- tweener: function (props, callback) {
- if (jQuery.isFunction(props)) {
- callback = props;
- props = ["*"];
- } else {
- props = props.split(" ");
- }
-
- var prop,
- index = 0,
- length = props.length;
-
- for (; index < length; index++) {
- prop = props[index];
- tweeners[prop] = tweeners[prop] || [];
- tweeners[prop].unshift(callback);
- }
- },
-
- prefilter: function (callback, prepend) {
- if (prepend) {
- animationPrefilters.unshift(callback);
- } else {
- animationPrefilters.push(callback);
- }
- }
- });
-
- function defaultPrefilter(elem, props, opts) {
- /* jshint validthis: true */
- var prop, value, toggle, tween, hooks, oldfire,
- anim = this,
- orig = {},
- style = elem.style,
- hidden = elem.nodeType && isHidden(elem),
- dataShow = jQuery._data(elem, "fxshow");
-
- // handle queue: false promises
- if (!opts.queue) {
- hooks = jQuery._queueHooks(elem, "fx");
- if (hooks.unqueued == null) {
- hooks.unqueued = 0;
- oldfire = hooks.empty.fire;
- hooks.empty.fire = function () {
- if (!hooks.unqueued) {
- oldfire();
- }
- };
- }
- hooks.unqueued++;
-
- anim.always(function () {
- // doing this makes sure that the complete handler will be called
- // before this completes
- anim.always(function () {
- hooks.unqueued--;
- if (!jQuery.queue(elem, "fx").length) {
- hooks.empty.fire();
- }
- });
- });
- }
-
- // height/width overflow pass
- if (elem.nodeType === 1 && ("height" in props || "width" in props)) {
- // Make sure that nothing sneaks out
- // Record all 3 overflow attributes because IE does not
- // change the overflow attribute when overflowX and
- // overflowY are set to the same value
- opts.overflow = [style.overflow, style.overflowX, style.overflowY];
-
- // Set display property to inline-block for height/width
- // animations on inline elements that are having width/height animated
- if (jQuery.css(elem, "display") === "inline" &&
- jQuery.css(elem, "float") === "none") {
-
- // inline-level elements accept inline-block;
- // block-level elements need to be inline with layout
- if (!jQuery.support.inlineBlockNeedsLayout || css_defaultDisplay(elem.nodeName) === "inline") {
- style.display = "inline-block";
-
- } else {
- style.zoom = 1;
- }
- }
- }
-
- if (opts.overflow) {
- style.overflow = "hidden";
- if (!jQuery.support.shrinkWrapBlocks) {
- anim.always(function () {
- style.overflow = opts.overflow[0];
- style.overflowX = opts.overflow[1];
- style.overflowY = opts.overflow[2];
- });
- }
- }
-
-
- // show/hide pass
- for (prop in props) {
- value = props[prop];
- if (rfxtypes.exec(value)) {
- delete props[prop];
- toggle = toggle || value === "toggle";
- if (value === (hidden ? "hide" : "show")) {
- continue;
- }
- orig[prop] = dataShow && dataShow[prop] || jQuery.style(elem, prop);
- }
- }
-
- if (!jQuery.isEmptyObject(orig)) {
- if (dataShow) {
- if ("hidden" in dataShow) {
- hidden = dataShow.hidden;
- }
- } else {
- dataShow = jQuery._data(elem, "fxshow", {});
- }
-
- // store state if its toggle - enables .stop().toggle() to "reverse"
- if (toggle) {
- dataShow.hidden = !hidden;
- }
- if (hidden) {
- jQuery(elem).show();
- } else {
- anim.done(function () {
- jQuery(elem).hide();
- });
- }
- anim.done(function () {
- var prop;
- jQuery._removeData(elem, "fxshow");
- for (prop in orig) {
- jQuery.style(elem, prop, orig[prop]);
- }
- });
- for (prop in orig) {
- tween = createTween(hidden ? dataShow[prop] : 0, prop, anim);
-
- if (!(prop in dataShow)) {
- dataShow[prop] = tween.start;
- if (hidden) {
- tween.end = tween.start;
- tween.start = prop === "width" || prop === "height" ? 1 : 0;
- }
- }
- }
- }
- }
-
- function Tween(elem, options, prop, end, easing) {
- return new Tween.prototype.init(elem, options, prop, end, easing);
- }
- jQuery.Tween = Tween;
-
- Tween.prototype = {
- constructor: Tween,
- init: function (elem, options, prop, end, easing, unit) {
- this.elem = elem;
- this.prop = prop;
- this.easing = easing || "swing";
- this.options = options;
- this.start = this.now = this.cur();
- this.end = end;
- this.unit = unit || (jQuery.cssNumber[prop] ? "" : "px");
- },
- cur: function () {
- var hooks = Tween.propHooks[this.prop];
-
- return hooks && hooks.get ?
- hooks.get(this) :
- Tween.propHooks._default.get(this);
- },
- run: function (percent) {
- var eased,
- hooks = Tween.propHooks[this.prop];
-
- if (this.options.duration) {
- this.pos = eased = jQuery.easing[this.easing](
- percent, this.options.duration * percent, 0, 1, this.options.duration
- );
- } else {
- this.pos = eased = percent;
- }
- this.now = (this.end - this.start) * eased + this.start;
-
- if (this.options.step) {
- this.options.step.call(this.elem, this.now, this);
- }
-
- if (hooks && hooks.set) {
- hooks.set(this);
- } else {
- Tween.propHooks._default.set(this);
- }
- return this;
- }
- };
-
- Tween.prototype.init.prototype = Tween.prototype;
-
- Tween.propHooks = {
- _default: {
- get: function (tween) {
- var result;
-
- if (tween.elem[tween.prop] != null &&
- (!tween.elem.style || tween.elem.style[tween.prop] == null)) {
- return tween.elem[tween.prop];
- }
-
- // passing an empty string as a 3rd parameter to .css will automatically
- // attempt a parseFloat and fallback to a string if the parse fails
- // so, simple values such as "10px" are parsed to Float.
- // complex values such as "rotate(1rad)" are returned as is.
- result = jQuery.css(tween.elem, tween.prop, "");
- // Empty strings, null, undefined and "auto" are converted to 0.
- return !result || result === "auto" ? 0 : result;
- },
- set: function (tween) {
- // use step hook for back compat - use cssHook if its there - use .style if its
- // available and use plain properties where available
- if (jQuery.fx.step[tween.prop]) {
- jQuery.fx.step[tween.prop](tween);
- } else if (tween.elem.style && (tween.elem.style[jQuery.cssProps[tween.prop]] != null || jQuery.cssHooks[tween.prop])) {
- jQuery.style(tween.elem, tween.prop, tween.now + tween.unit);
- } else {
- tween.elem[tween.prop] = tween.now;
- }
- }
- }
- };
-
- // Support: IE <=9
- // Panic based approach to setting things on disconnected nodes
-
- Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {
- set: function (tween) {
- if (tween.elem.nodeType && tween.elem.parentNode) {
- tween.elem[tween.prop] = tween.now;
- }
- }
- };
-
- jQuery.each(["toggle", "show", "hide"], function (i, name) {
- var cssFn = jQuery.fn[name];
- jQuery.fn[name] = function (speed, easing, callback) {
- return speed == null || typeof speed === "boolean" ?
- cssFn.apply(this, arguments) :
- this.animate(genFx(name, true), speed, easing, callback);
- };
- });
-
- jQuery.fn.extend({
- fadeTo: function (speed, to, easing, callback) {
-
- // show any hidden elements after setting opacity to 0
- return this.filter(isHidden).css("opacity", 0).show()
-
- // animate to the value specified
- .end().animate({
- opacity: to
- }, speed, easing, callback);
- },
- animate: function (prop, speed, easing, callback) {
- var empty = jQuery.isEmptyObject(prop),
- optall = jQuery.speed(speed, easing, callback),
- doAnimation = function () {
- // Operate on a copy of prop so per-property easing won't be lost
- var anim = Animation(this, jQuery.extend({}, prop), optall);
-
- // Empty animations, or finishing resolves immediately
- if (empty || jQuery._data(this, "finish")) {
- anim.stop(true);
- }
- };
- doAnimation.finish = doAnimation;
-
- return empty || optall.queue === false ?
- this.each(doAnimation) :
- this.queue(optall.queue, doAnimation);
- },
- stop: function (type, clearQueue, gotoEnd) {
- var stopQueue = function (hooks) {
- var stop = hooks.stop;
- delete hooks.stop;
- stop(gotoEnd);
- };
-
- if (typeof type !== "string") {
- gotoEnd = clearQueue;
- clearQueue = type;
- type = undefined;
- }
- if (clearQueue && type !== false) {
- this.queue(type || "fx", []);
- }
-
- return this.each(function () {
- var dequeue = true,
- index = type != null && type + "queueHooks",
- timers = jQuery.timers,
- data = jQuery._data(this);
-
- if (index) {
- if (data[index] && data[index].stop) {
- stopQueue(data[index]);
- }
- } else {
- for (index in data) {
- if (data[index] && data[index].stop && rrun.test(index)) {
- stopQueue(data[index]);
- }
- }
- }
-
- for (index = timers.length; index--;) {
- if (timers[index].elem === this && (type == null || timers[index].queue === type)) {
- timers[index].anim.stop(gotoEnd);
- dequeue = false;
- timers.splice(index, 1);
- }
- }
-
- // start the next in the queue if the last step wasn't forced
- // timers currently will call their complete callbacks, which will dequeue
- // but only if they were gotoEnd
- if (dequeue || !gotoEnd) {
- jQuery.dequeue(this, type);
- }
- });
- },
- finish: function (type) {
- if (type !== false) {
- type = type || "fx";
- }
- return this.each(function () {
- var index,
- data = jQuery._data(this),
- queue = data[type + "queue"],
- hooks = data[type + "queueHooks"],
- timers = jQuery.timers,
- length = queue ? queue.length : 0;
-
- // enable finishing flag on private data
- data.finish = true;
-
- // empty the queue first
- jQuery.queue(this, type, []);
-
- if (hooks && hooks.stop) {
- hooks.stop.call(this, true);
- }
-
- // look for any active animations, and finish them
- for (index = timers.length; index--;) {
- if (timers[index].elem === this && timers[index].queue === type) {
- timers[index].anim.stop(true);
- timers.splice(index, 1);
- }
- }
-
- // look for any animations in the old queue and finish them
- for (index = 0; index < length; index++) {
- if (queue[index] && queue[index].finish) {
- queue[index].finish.call(this);
- }
- }
-
- // turn off finishing flag
- delete data.finish;
- });
- }
- });
-
- // Generate parameters to create a standard animation
-
- function genFx(type, includeWidth) {
- var which,
- attrs = {
- height: type
- },
- i = 0;
-
- // if we include width, step value is 1 to do all cssExpand values,
- // if we don't include width, step value is 2 to skip over Left and Right
- includeWidth = includeWidth ? 1 : 0;
- for (; i < 4; i += 2 - includeWidth) {
- which = cssExpand[i];
- attrs["margin" + which] = attrs["padding" + which] = type;
- }
-
- if (includeWidth) {
- attrs.opacity = attrs.width = type;
- }
-
- return attrs;
- }
-
- // Generate shortcuts for custom animations
- jQuery.each({
- slideDown: genFx("show"),
- slideUp: genFx("hide"),
- slideToggle: genFx("toggle"),
- fadeIn: {
- opacity: "show"
- },
- fadeOut: {
- opacity: "hide"
- },
- fadeToggle: {
- opacity: "toggle"
- }
- }, function (name, props) {
- jQuery.fn[name] = function (speed, easing, callback) {
- return this.animate(props, speed, easing, callback);
- };
- });
-
- jQuery.speed = function (speed, easing, fn) {
- var opt = speed && typeof speed === "object" ? jQuery.extend({}, speed) : {
- complete: fn || !fn && easing || jQuery.isFunction(speed) && speed,
- duration: speed,
- easing: fn && easing || easing && !jQuery.isFunction(easing) && easing
- };
-
- opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
- opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[opt.duration] : jQuery.fx.speeds._default;
-
- // normalize opt.queue - true/undefined/null -> "fx"
- if (opt.queue == null || opt.queue === true) {
- opt.queue = "fx";
- }
-
- // Queueing
- opt.old = opt.complete;
-
- opt.complete = function () {
- if (jQuery.isFunction(opt.old)) {
- opt.old.call(this);
- }
-
- if (opt.queue) {
- jQuery.dequeue(this, opt.queue);
- }
- };
-
- return opt;
- };
-
- jQuery.easing = {
- linear: function (p) {
- return p;
- },
- swing: function (p) {
- return 0.5 - Math.cos(p * Math.PI) / 2;
- }
- };
-
- jQuery.timers = [];
- jQuery.fx = Tween.prototype.init;
- jQuery.fx.tick = function () {
- var timer,
- timers = jQuery.timers,
- i = 0;
-
- fxNow = jQuery.now();
-
- for (; i < timers.length; i++) {
- timer = timers[i];
- // Checks the timer has not already been removed
- if (!timer() && timers[i] === timer) {
- timers.splice(i--, 1);
- }
- }
-
- if (!timers.length) {
- jQuery.fx.stop();
- }
- fxNow = undefined;
- };
-
- jQuery.fx.timer = function (timer) {
- if (timer() && jQuery.timers.push(timer)) {
- jQuery.fx.start();
- }
- };
-
- jQuery.fx.interval = 13;
-
- jQuery.fx.start = function () {
- if (!timerId) {
- timerId = setInterval(jQuery.fx.tick, jQuery.fx.interval);
- }
- };
-
- jQuery.fx.stop = function () {
- clearInterval(timerId);
- timerId = null;
- };
-
- jQuery.fx.speeds = {
- slow: 600,
- fast: 200,
- // Default speed
- _default: 400
- };
-
- // Back Compat <1.8 extension point
- jQuery.fx.step = {};
-
- if (jQuery.expr && jQuery.expr.filters) {
- jQuery.expr.filters.animated = function (elem) {
- return jQuery.grep(jQuery.timers, function (fn) {
- return elem === fn.elem;
- }).length;
- };
- }
- jQuery.fn.offset = function (options) {
- if (arguments.length) {
- return options === undefined ?
- this :
- this.each(function (i) {
- jQuery.offset.setOffset(this, options, i);
- });
- }
-
- var docElem, win,
- box = {
- top: 0,
- left: 0
- },
- elem = this[0],
- doc = elem && elem.ownerDocument;
-
- if (!doc) {
- return;
- }
-
- docElem = doc.documentElement;
-
- // Make sure it's not a disconnected DOM node
- if (!jQuery.contains(docElem, elem)) {
- return box;
- }
-
- // If we don't have gBCR, just use 0,0 rather than error
- // BlackBerry 5, iOS 3 (original iPhone)
- if (typeof elem.getBoundingClientRect !== core_strundefined) {
- box = elem.getBoundingClientRect();
- }
- win = getWindow(doc);
- return {
- top: box.top + (win.pageYOffset || docElem.scrollTop) - (docElem.clientTop || 0),
- left: box.left + (win.pageXOffset || docElem.scrollLeft) - (docElem.clientLeft || 0)
- };
- };
-
- jQuery.offset = {
-
- setOffset: function (elem, options, i) {
- var position = jQuery.css(elem, "position");
-
- // set position first, in-case top/left are set even on static elem
- if (position === "static") {
- elem.style.position = "relative";
- }
-
- var curElem = jQuery(elem),
- curOffset = curElem.offset(),
- curCSSTop = jQuery.css(elem, "top"),
- curCSSLeft = jQuery.css(elem, "left"),
- calculatePosition = (position === "absolute" || position === "fixed") && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1,
- props = {}, curPosition = {}, curTop, curLeft;
-
- // need to be able to calculate position if either top or left is auto and position is either absolute or fixed
- if (calculatePosition) {
- curPosition = curElem.position();
- curTop = curPosition.top;
- curLeft = curPosition.left;
- } else {
- curTop = parseFloat(curCSSTop) || 0;
- curLeft = parseFloat(curCSSLeft) || 0;
- }
-
- if (jQuery.isFunction(options)) {
- options = options.call(elem, i, curOffset);
- }
-
- if (options.top != null) {
- props.top = (options.top - curOffset.top) + curTop;
- }
- if (options.left != null) {
- props.left = (options.left - curOffset.left) + curLeft;
- }
-
- if ("using" in options) {
- options.using.call(elem, props);
- } else {
- curElem.css(props);
- }
- }
- };
-
-
- jQuery.fn.extend({
-
- position: function () {
- if (!this[0]) {
- return;
- }
-
- var offsetParent, offset,
- parentOffset = {
- top: 0,
- left: 0
- },
- elem = this[0];
-
- // fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is it's only offset parent
- if (jQuery.css(elem, "position") === "fixed") {
- // we assume that getBoundingClientRect is available when computed position is fixed
- offset = elem.getBoundingClientRect();
- } else {
- // Get *real* offsetParent
- offsetParent = this.offsetParent();
-
- // Get correct offsets
- offset = this.offset();
- if (!jQuery.nodeName(offsetParent[0], "html")) {
- parentOffset = offsetParent.offset();
- }
-
- // Add offsetParent borders
- parentOffset.top += jQuery.css(offsetParent[0], "borderTopWidth", true);
- parentOffset.left += jQuery.css(offsetParent[0], "borderLeftWidth", true);
- }
-
- // Subtract parent offsets and element margins
- // note: when an element has margin: auto the offsetLeft and marginLeft
- // are the same in Safari causing offset.left to incorrectly be 0
- return {
- top: offset.top - parentOffset.top - jQuery.css(elem, "marginTop", true),
- left: offset.left - parentOffset.left - jQuery.css(elem, "marginLeft", true)
- };
- },
-
- offsetParent: function () {
- return this.map(function () {
- var offsetParent = this.offsetParent || docElem;
- while (offsetParent && (!jQuery.nodeName(offsetParent, "html") && jQuery.css(offsetParent, "position") === "static")) {
- offsetParent = offsetParent.offsetParent;
- }
- return offsetParent || docElem;
- });
- }
- });
-
-
- // Create scrollLeft and scrollTop methods
- jQuery.each({
- scrollLeft: "pageXOffset",
- scrollTop: "pageYOffset"
- }, function (method, prop) {
- var top = /Y/.test(prop);
-
- jQuery.fn[method] = function (val) {
- return jQuery.access(this, function (elem, method, val) {
- var win = getWindow(elem);
-
- if (val === undefined) {
- return win ? (prop in win) ? win[prop] :
- win.document.documentElement[method] :
- elem[method];
- }
-
- if (win) {
- win.scrollTo(!top ? val : jQuery(win).scrollLeft(),
- top ? val : jQuery(win).scrollTop()
- );
-
- } else {
- elem[method] = val;
- }
- }, method, val, arguments.length, null);
- };
- });
-
- function getWindow(elem) {
- return jQuery.isWindow(elem) ?
- elem :
- elem.nodeType === 9 ?
- elem.defaultView || elem.parentWindow :
- false;
- }
- // Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
- jQuery.each({
- Height: "height",
- Width: "width"
- }, function (name, type) {
- jQuery.each({
- padding: "inner" + name,
- content: type,
- "": "outer" + name
- }, function (defaultExtra, funcName) {
- // margin is only for outerHeight, outerWidth
- jQuery.fn[funcName] = function (margin, value) {
- var chainable = arguments.length && (defaultExtra || typeof margin !== "boolean"),
- extra = defaultExtra || (margin === true || value === true ? "margin" : "border");
-
- return jQuery.access(this, function (elem, type, value) {
- var doc;
-
- if (jQuery.isWindow(elem)) {
- // As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there
- // isn't a whole lot we can do. See pull request at this URL for discussion:
- // https://github.com/jquery/jquery/pull/764
- return elem.document.documentElement["client" + name];
- }
-
- // Get document width or height
- if (elem.nodeType === 9) {
- doc = elem.documentElement;
-
- // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], whichever is greatest
- // unfortunately, this causes bug #3838 in IE6/8 only, but there is currently no good, small way to fix it.
- return Math.max(
- elem.body["scroll" + name], doc["scroll" + name],
- elem.body["offset" + name], doc["offset" + name],
- doc["client" + name]
- );
- }
-
- return value === undefined ?
- // Get width or height on the element, requesting but not forcing parseFloat
- jQuery.css(elem, type, extra) :
-
- // Set width or height on the element
- jQuery.style(elem, type, value, extra);
- }, type, chainable ? margin : undefined, chainable, null);
- };
- });
- });
- // Limit scope pollution from any deprecated API
- // (function() {
-
- // The number of elements contained in the matched element set
- jQuery.fn.size = function () {
- return this.length;
- };
-
- jQuery.fn.andSelf = jQuery.fn.addBack;
-
- // })();
- if (typeof module === "object" && module && typeof module.exports === "object") {
- // Expose jQuery as module.exports in loaders that implement the Node
- // module pattern (including browserify). Do not create the global, since
- // the user will be storing it themselves locally, and globals are frowned
- // upon in the Node module world.
- module.exports = jQuery;
- } else {
- // Otherwise expose jQuery to the global object as usual
- window.jQuery = window.$ = jQuery;
-
- // Register as a named AMD module, since jQuery can be concatenated with other
- // files that may use define, but not via a proper concatenation script that
- // understands anonymous AMD modules. A named AMD is safest and most robust
- // way to register. Lowercase jquery is used because AMD module names are
- // derived from file names, and jQuery is normally delivered in a lowercase
- // file name. Do this after creating the global so that if an AMD module wants
- // to call noConflict to hide this version of jQuery, it will work.
- if (typeof define === "function" && define.amd) {
- define("jquery", [], function () {
- return jQuery;
- });
- }
- }
-
-})(window);
\ No newline at end of file
diff --git a/src/controllers/404.js b/src/controllers/404.js
index bc4e2e1d00..44dcf59174 100644
--- a/src/controllers/404.js
+++ b/src/controllers/404.js
@@ -30,18 +30,20 @@ exports.handle404 = function (req, res) {
}
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]]' });
- });
+ exports.send404(req, res);
} else {
res.status(404).type('txt').send('Not found');
}
};
+
+exports.send404 = function (req, res) {
+ 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]]' });
+ });
+};
diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js
index 5ca9c421ad..c8de5c60f1 100644
--- a/src/controllers/authentication.js
+++ b/src/controllers/authentication.js
@@ -26,13 +26,7 @@ authenticationController.register = function (req, res) {
return res.sendStatus(403);
}
- var userData = {};
-
- for (var key in req.body) {
- if (req.body.hasOwnProperty(key)) {
- userData[key] = req.body[key];
- }
- }
+ var userData = req.body;
async.waterfall([
function (next) {
@@ -88,7 +82,7 @@ authenticationController.register = function (req, res) {
return res.status(400).send(err.message);
}
- if (req.body.userLang) {
+ if (data.uid && req.body.userLang) {
user.setSetting(data.uid, 'userLang', req.body.userLang);
}
@@ -103,21 +97,18 @@ function registerAndLoginUser(req, res, userData, callback) {
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;
+ }, next);
+ },
+ function (data, next) {
+ // If interstitials are found, save registration attempt into session and abort
+ var deferRegistration = data.interstitials.length;
- if (!deferRegistration) {
- return next();
- }
- userData.register = true;
- req.session.registration = userData;
- return res.json({ referrer: nconf.get('relative_path') + '/register/complete' });
- });
+ if (!deferRegistration) {
+ return next();
+ }
+ userData.register = true;
+ req.session.registration = userData;
+ return res.json({ referrer: nconf.get('relative_path') + '/register/complete' });
},
function (next) {
user.create(userData, next);
@@ -282,14 +273,14 @@ authenticationController.doLogin = function (req, uid, callback) {
if (!uid) {
return callback();
}
-
- req.login({ uid: uid }, function (err) {
- if (err) {
- return callback(err);
- }
-
- authenticationController.onSuccessfulLogin(req, uid, callback);
- });
+ async.waterfall([
+ function (next) {
+ req.login({ uid: uid }, next);
+ },
+ function (next) {
+ authenticationController.onSuccessfulLogin(req, uid, next);
+ },
+ ], callback);
};
authenticationController.onSuccessfulLogin = function (req, uid, callback) {
@@ -312,28 +303,30 @@ authenticationController.onSuccessfulLogin = function (req, uid, callback) {
version: req.useragent.version,
});
- // Associate login session with user
- async.parallel([
- function (next) {
- user.auth.addSession(uid, req.sessionID, next);
- },
+ async.waterfall([
function (next) {
- db.setObjectField('uid:' + uid + ':sessionUUID:sessionId', uuid, req.sessionID, next);
+ async.parallel([
+ function (next) {
+ user.auth.addSession(uid, req.sessionID, next);
+ },
+ function (next) {
+ db.setObjectField('uid:' + uid + ':sessionUUID:sessionId', uuid, req.sessionID, next);
+ },
+ function (next) {
+ user.updateLastOnlineTime(uid, next);
+ },
+ ], function (err) {
+ next(err);
+ });
},
function (next) {
- user.updateLastOnlineTime(uid, next);
- },
- ], function (err) {
- if (err) {
- return callback(err);
- }
+ // Force session check for all connected socket.io clients with the same session id
+ sockets.in('sess_' + req.sessionID).emit('checkSession', uid);
- // Force session check for all connected socket.io clients with the same session id
- sockets.in('sess_' + req.sessionID).emit('checkSession', uid);
-
- plugins.fireHook('action:user.loggedIn', { uid: uid, req: req });
- callback();
- });
+ plugins.fireHook('action:user.loggedIn', { uid: uid, req: req });
+ next();
+ },
+ ], callback);
};
authenticationController.localLogin = function (req, username, password, next) {
diff --git a/src/controllers/topics.js b/src/controllers/topics.js
index ddd6fd889a..5c592f5bb3 100644
--- a/src/controllers/topics.js
+++ b/src/controllers/topics.js
@@ -49,7 +49,7 @@ topicsController.get = function (req, res, callback) {
userPrivileges = results.privileges;
- if (!userPrivileges.read || !userPrivileges['topics:read'] || (parseInt(results.topic.deleted, 10) && !userPrivileges.view_deleted)) {
+ if (!userPrivileges['topics:read'] || (parseInt(results.topic.deleted, 10) && !userPrivileges.view_deleted)) {
return helpers.notAllowed(req, res);
}
diff --git a/src/database/mongo.js b/src/database/mongo.js
index fc37c7ae74..507a9c9a86 100644
--- a/src/database/mongo.js
+++ b/src/database/mongo.js
@@ -161,8 +161,11 @@ mongoModule.createIndices = function (callback) {
mongoModule.checkCompatibility = function (callback) {
var mongoPkg = require('mongodb/package.json');
+ mongoModule.checkCompatibilityVersion(mongoPkg.version, callback);
+};
- if (semver.lt(mongoPkg.version, '2.0.0')) {
+mongoModule.checkCompatibilityVersion = function (version, callback) {
+ if (semver.lt(version, '2.0.0')) {
return callback(new Error('The `mongodb` package is out-of-date, please run `./nodebb setup` again.'));
}
@@ -173,66 +176,75 @@ mongoModule.info = function (db, callback) {
if (!db) {
return callback();
}
- async.parallel({
- serverStatus: function (next) {
- db.command({ serverStatus: 1 }, next);
- },
- stats: function (next) {
- db.command({ dbStats: 1 }, next);
+ async.waterfall([
+ function (next) {
+ async.parallel({
+ serverStatus: function (next) {
+ db.command({ serverStatus: 1 }, next);
+ },
+ stats: function (next) {
+ db.command({ dbStats: 1 }, next);
+ },
+ listCollections: function (next) {
+ getCollectionStats(db, next);
+ },
+ }, next);
},
- listCollections: function (next) {
- db.listCollections().toArray(function (err, items) {
- if (err) {
- return next(err);
- }
- async.map(items, function (collection, next) {
- db.collection(collection.name).stats(next);
- }, next);
+ function (results, next) {
+ var stats = results.stats;
+ var scale = 1024 * 1024 * 1024;
+
+ results.listCollections = results.listCollections.map(function (collectionInfo) {
+ return {
+ name: collectionInfo.ns,
+ count: collectionInfo.count,
+ size: collectionInfo.size,
+ avgObjSize: collectionInfo.avgObjSize,
+ storageSize: collectionInfo.storageSize,
+ totalIndexSize: collectionInfo.totalIndexSize,
+ indexSizes: collectionInfo.indexSizes,
+ };
});
- },
- }, function (err, results) {
- if (err) {
- return callback(err);
- }
- var stats = results.stats;
- var scale = 1024 * 1024 * 1024;
-
- results.listCollections = results.listCollections.map(function (collectionInfo) {
- return {
- name: collectionInfo.ns,
- count: collectionInfo.count,
- size: collectionInfo.size,
- avgObjSize: collectionInfo.avgObjSize,
- storageSize: collectionInfo.storageSize,
- totalIndexSize: collectionInfo.totalIndexSize,
- indexSizes: collectionInfo.indexSizes,
- };
- });
- stats.mem = results.serverStatus.mem;
- stats.mem = results.serverStatus.mem;
- stats.mem.resident = (stats.mem.resident / 1024).toFixed(2);
- stats.mem.virtual = (stats.mem.virtual / 1024).toFixed(2);
- stats.mem.mapped = (stats.mem.mapped / 1024).toFixed(2);
- stats.collectionData = results.listCollections;
- stats.network = results.serverStatus.network;
- stats.raw = JSON.stringify(stats, null, 4);
-
- stats.avgObjSize = stats.avgObjSize.toFixed(2);
- stats.dataSize = (stats.dataSize / scale).toFixed(2);
- stats.storageSize = (stats.storageSize / scale).toFixed(2);
- stats.fileSize = stats.fileSize ? (stats.fileSize / scale).toFixed(2) : 0;
- stats.indexSize = (stats.indexSize / scale).toFixed(2);
- stats.storageEngine = results.serverStatus.storageEngine ? results.serverStatus.storageEngine.name : 'mmapv1';
- stats.host = results.serverStatus.host;
- stats.version = results.serverStatus.version;
- stats.uptime = results.serverStatus.uptime;
- stats.mongo = true;
-
- callback(null, stats);
- });
+ stats.mem = results.serverStatus.mem;
+ stats.mem = results.serverStatus.mem;
+ stats.mem.resident = (stats.mem.resident / 1024).toFixed(2);
+ stats.mem.virtual = (stats.mem.virtual / 1024).toFixed(2);
+ stats.mem.mapped = (stats.mem.mapped / 1024).toFixed(2);
+ stats.collectionData = results.listCollections;
+ stats.network = results.serverStatus.network;
+ stats.raw = JSON.stringify(stats, null, 4);
+
+ stats.avgObjSize = stats.avgObjSize.toFixed(2);
+ stats.dataSize = (stats.dataSize / scale).toFixed(2);
+ stats.storageSize = (stats.storageSize / scale).toFixed(2);
+ stats.fileSize = stats.fileSize ? (stats.fileSize / scale).toFixed(2) : 0;
+ stats.indexSize = (stats.indexSize / scale).toFixed(2);
+ stats.storageEngine = results.serverStatus.storageEngine ? results.serverStatus.storageEngine.name : 'mmapv1';
+ stats.host = results.serverStatus.host;
+ stats.version = results.serverStatus.version;
+ stats.uptime = results.serverStatus.uptime;
+ stats.mongo = true;
+
+ next(null, stats);
+ },
+ ], callback);
};
-mongoModule.close = function () {
- db.close();
+function getCollectionStats(db, callback) {
+ async.waterfall([
+ function (next) {
+ db.listCollections().toArray(next);
+ },
+ function (items, next) {
+ async.map(items, function (collection, next) {
+ db.collection(collection.name).stats(next);
+ }, next);
+ },
+ ], callback);
+}
+
+mongoModule.close = function (callback) {
+ callback = callback || function () {};
+ db.close(callback);
};
diff --git a/src/database/redis.js b/src/database/redis.js
index 9ebc154705..d0d80d0038 100644
--- a/src/database/redis.js
+++ b/src/database/redis.js
@@ -1,6 +1,7 @@
'use strict';
var _ = require('underscore');
+var async = require('async');
var winston = require('winston');
var nconf = require('nconf');
var semver = require('semver');
@@ -71,10 +72,6 @@ redisModule.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')) {
@@ -101,10 +98,10 @@ redisModule.connect = function (options) {
}
var dbIdx = parseInt(nconf.get('redis:database'), 10);
- if (dbIdx) {
- cxn.select(dbIdx, function (error) {
- if (error) {
- winston.error('NodeBB could not connect to your Redis database. Redis returned the following error: ' + error.message);
+ if (dbIdx >= 0) {
+ cxn.select(dbIdx, function (err) {
+ if (err) {
+ winston.error('NodeBB could not connect to your Redis database. Redis returned the following error: ' + err.message);
process.exit();
}
});
@@ -118,46 +115,52 @@ redisModule.createIndices = function (callback) {
};
redisModule.checkCompatibility = function (callback) {
- redisModule.info(redisModule.client, function (err, info) {
- if (err) {
- return callback(err);
- }
-
- if (semver.lt(info.redis_version, '2.8.9')) {
- return callback(new Error('Your Redis version is not new enough to support NodeBB, please upgrade Redis to v2.8.9 or higher.'));
- }
+ async.waterfall([
+ function (next) {
+ redisModule.info(redisModule.client, next);
+ },
+ function (info, next) {
+ redisModule.checkCompatibilityVersion(info.redis_version, next);
+ },
+ ], callback);
+};
- callback();
- });
+redisModule.checkCompatibilityVersion = function (version, callback) {
+ if (semver.lt(version, '2.8.9')) {
+ return callback(new Error('Your Redis version is not new enough to support NodeBB, please upgrade Redis to v2.8.9 or higher.'));
+ }
+ callback();
};
-redisModule.close = function () {
- redisClient.quit();
+redisModule.close = function (callback) {
+ callback = callback || function () {};
+ redisClient.quit(callback);
};
redisModule.info = function (cxn, callback) {
if (!cxn) {
return callback();
}
- cxn.info(function (err, data) {
- if (err) {
- return callback(err);
- }
-
- var lines = data.toString().split('\r\n').sort();
- var redisData = {};
- lines.forEach(function (line) {
- var parts = line.split(':');
- if (parts[1]) {
- redisData[parts[0]] = parts[1];
- }
- });
- redisData.used_memory_human = (redisData.used_memory / (1024 * 1024 * 1024)).toFixed(2);
- redisData.raw = JSON.stringify(redisData, null, 4);
- redisData.redis = true;
-
- callback(null, redisData);
- });
+ async.waterfall([
+ function (next) {
+ cxn.info(next);
+ },
+ function (data, next) {
+ var lines = data.toString().split('\r\n').sort();
+ var redisData = {};
+ lines.forEach(function (line) {
+ var parts = line.split(':');
+ if (parts[1]) {
+ redisData[parts[0]] = parts[1];
+ }
+ });
+ redisData.used_memory_human = (redisData.used_memory / (1024 * 1024 * 1024)).toFixed(2);
+ redisData.raw = JSON.stringify(redisData, null, 4);
+ redisData.redis = true;
+
+ next(null, redisData);
+ },
+ ], callback);
};
redisModule.helpers = redisModule.helpers || {};
diff --git a/src/database/redis/list.js b/src/database/redis/list.js
index fb445573ff..f8108a194d 100644
--- a/src/database/redis/list.js
+++ b/src/database/redis/list.js
@@ -3,6 +3,9 @@
module.exports = function (redisClient, module) {
module.listPrepend = function (key, value, callback) {
callback = callback || function () {};
+ if (!key) {
+ return callback();
+ }
redisClient.lpush(key, value, function (err) {
callback(err);
});
@@ -10,6 +13,9 @@ module.exports = function (redisClient, module) {
module.listAppend = function (key, value, callback) {
callback = callback || function () {};
+ if (!key) {
+ return callback();
+ }
redisClient.rpush(key, value, function (err) {
callback(err);
});
@@ -17,11 +23,17 @@ module.exports = function (redisClient, module) {
module.listRemoveLast = function (key, callback) {
callback = callback || function () {};
+ if (!key) {
+ return callback();
+ }
redisClient.rpop(key, callback);
};
module.listRemoveAll = function (key, value, callback) {
callback = callback || function () {};
+ if (!key) {
+ return callback();
+ }
redisClient.lrem(key, 0, value, function (err) {
callback(err);
});
@@ -29,6 +41,9 @@ module.exports = function (redisClient, module) {
module.listTrim = function (key, start, stop, callback) {
callback = callback || function () {};
+ if (!key) {
+ return callback();
+ }
redisClient.ltrim(key, start, stop, function (err) {
callback(err);
});
@@ -36,6 +51,9 @@ module.exports = function (redisClient, module) {
module.getListRange = function (key, start, stop, callback) {
callback = callback || function () {};
+ if (!key) {
+ return callback();
+ }
redisClient.lrange(key, start, stop, callback);
};
};
diff --git a/src/database/redis/sorted.js b/src/database/redis/sorted.js
index 9bb004550a..282928da97 100644
--- a/src/database/redis/sorted.js
+++ b/src/database/redis/sorted.js
@@ -129,7 +129,13 @@ module.exports = function (redisClient, module) {
}
redisClient.zscore(key, value, function (err, score) {
- callback(err, !err ? parseFloat(score) : null);
+ if (err) {
+ return callback(err);
+ }
+ if (score === null) {
+ return callback(null, score);
+ }
+ callback(null, parseFloat(score));
});
};
diff --git a/src/messaging/rooms.js b/src/messaging/rooms.js
index ead62fee4d..41f09c5125 100644
--- a/src/messaging/rooms.js
+++ b/src/messaging/rooms.js
@@ -9,26 +9,33 @@ var plugins = require('../plugins');
module.exports = function (Messaging) {
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]]'));
- }
- modifyRoomData([data]);
- callback(null, data);
- });
+ async.waterfall([
+ function (next) {
+ db.getObject('chat:room:' + roomId, next);
+ },
+ function (data, next) {
+ if (!data) {
+ return callback(new Error('[[error:no-chat-room]]'));
+ }
+ modifyRoomData([data]);
+ next(null, data);
+ },
+ ], 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);
- });
+ async.waterfall([
+ function (next) {
+ db.getObjects(keys, next);
+ },
+ function (roomData, next) {
+ modifyRoomData(roomData);
+ next(null, roomData);
+ },
+ ], callback);
};
function modifyRoomData(rooms) {
@@ -96,13 +103,14 @@ module.exports = function (Messaging) {
};
Messaging.isRoomOwner = function (uid, roomId, callback) {
- db.getObjectField('chat:room:' + roomId, 'owner', function (err, owner) {
- if (err) {
- return callback(err);
- }
-
- callback(null, parseInt(uid, 10) === parseInt(owner, 10));
- });
+ async.waterfall([
+ function (next) {
+ db.getObjectField('chat:room:' + roomId, 'owner', next);
+ },
+ function (owner, next) {
+ next(null, parseInt(uid, 10) === parseInt(owner, 10));
+ },
+ ], callback);
};
Messaging.addUsersToRoom = function (uid, uids, roomId, callback) {
diff --git a/src/meta.js b/src/meta.js
index 1fb0170297..59c3447cf4 100644
--- a/src/meta.js
+++ b/src/meta.js
@@ -8,71 +8,71 @@ var nconf = require('nconf');
var pubsub = require('./pubsub');
var utils = require('./utils');
-(function (Meta) {
- Meta.reloadRequired = false;
+var Meta = module.exports;
- require('./meta/configs')(Meta);
- require('./meta/themes')(Meta);
- require('./meta/js')(Meta);
- require('./meta/css')(Meta);
- 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');
- Meta.languages = require('./meta/languages');
+Meta.reloadRequired = false;
- /* Assorted */
- 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);
- });
- };
+require('./meta/configs')(Meta);
+require('./meta/themes')(Meta);
+require('./meta/js')(Meta);
+require('./meta/css')(Meta);
+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');
+Meta.languages = require('./meta/languages');
- /**
- * Reload deprecated as of v1.1.2+, remove in v2.x
- */
- Meta.reload = function (callback) {
- restart();
- callback();
- };
+/* Assorted */
+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);
+ });
+};
- Meta.restart = function () {
- pubsub.publish('meta:restart', { hostname: os.hostname() });
- restart();
- };
+/**
+ * Reload deprecated as of v1.1.2+, remove in v2.x
+ */
+Meta.reload = function (callback) {
+ restart();
+ callback();
+};
- Meta.getSessionTTLSeconds = function () {
- var ttlDays = 60 * 60 * 24 * (parseInt(Meta.config.loginDays, 10) || 0);
- var ttlSeconds = (parseInt(Meta.config.loginSeconds, 10) || 0);
- var ttl = ttlSeconds || ttlDays || 1209600; // Default to 14 days
- return ttl;
- };
+Meta.restart = function () {
+ pubsub.publish('meta:restart', { hostname: os.hostname() });
+ restart();
+};
- if (nconf.get('isPrimary') === 'true') {
- pubsub.on('meta:restart', function (data) {
- if (data.hostname !== os.hostname()) {
- restart();
- }
- });
- }
+Meta.getSessionTTLSeconds = function () {
+ var ttlDays = 60 * 60 * 24 * (parseInt(Meta.config.loginDays, 10) || 0);
+ var ttlSeconds = (parseInt(Meta.config.loginSeconds, 10) || 0);
+ var ttl = ttlSeconds || ttlDays || 1209600; // Default to 14 days
+ return ttl;
+};
- function restart() {
- if (process.send) {
- process.send({
- action: 'restart',
- });
- } else {
- winston.error('[meta.restart] Could not restart, are you sure NodeBB was started with `./nodebb start`?');
+if (nconf.get('isPrimary') === 'true') {
+ pubsub.on('meta:restart', function (data) {
+ if (data.hostname !== os.hostname()) {
+ restart();
}
+ });
+}
+
+function restart() {
+ if (process.send) {
+ process.send({
+ action: 'restart',
+ });
+ } else {
+ winston.error('[meta.restart] Could not restart, are you sure NodeBB was started with `./nodebb start`?');
}
-}(exports));
+}
diff --git a/src/meta/build.js b/src/meta/build.js
index dfeb4d7825..edb7d526ba 100644
--- a/src/meta/build.js
+++ b/src/meta/build.js
@@ -2,7 +2,6 @@
var async = require('async');
var winston = require('winston');
-var os = require('os');
var nconf = require('nconf');
var padstart = require('lodash.padstart');
@@ -183,11 +182,14 @@ function build(targets, callback) {
var startTime;
var totalTime;
async.series([
+ async.apply(beforeBuild, targets),
function (next) {
- beforeBuild(targets, next);
- },
- function (next) {
- var parallel = os.cpus().length > 1 && !nconf.get('series');
+ var threads = parseInt(nconf.get('threads'), 10);
+ if (threads) {
+ require('./minifier').maxThreads = threads - 1;
+ }
+
+ var parallel = !nconf.get('series');
if (parallel) {
winston.info('[build] Building in parallel mode');
} else {
diff --git a/src/meta/js.js b/src/meta/js.js
index e7b22939fc..d0399c70c1 100644
--- a/src/meta/js.js
+++ b/src/meta/js.js
@@ -89,43 +89,40 @@ module.exports = function (Meta) {
};
function minifyModules(modules, fork, callback) {
- // for it to never fork
- // otherwise it spawns way too many processes
- // maybe eventually we can pool modules
- // and pass the pools to the minifer
- // to reduce the total number of threads
- fork = false;
+ var moduleDirs = modules.reduce(function (prev, mod) {
+ var dir = path.resolve(path.dirname(mod.destPath));
+ if (prev.indexOf(dir) === -1) {
+ prev.push(dir);
+ }
+ return prev;
+ }, []);
- async.eachLimit(modules, 500, function (mod, next) {
- var srcPath = mod.srcPath;
- var destPath = mod.destPath;
+ async.eachLimit(moduleDirs, 1000, mkdirp, function (err) {
+ if (err) {
+ return callback(err);
+ }
- async.parallel({
- dirped: function (cb) {
- mkdirp(path.dirname(destPath), cb);
- },
- minified: function (cb) {
- fs.readFile(srcPath, function (err, buffer) {
- if (err) {
- return cb(err);
- }
+ var filtered = modules.reduce(function (prev, mod) {
+ if (mod.srcPath.endsWith('.min.js') || path.dirname(mod.srcPath).endsWith('min')) {
+ prev.skip.push(mod);
+ } else {
+ prev.minify.push(mod);
+ }
- if (srcPath.endsWith('.min.js') || path.dirname(srcPath).endsWith('min')) {
- return cb(null, { code: buffer.toString() });
- }
+ return prev;
+ }, { minify: [], skip: [] });
- minifier.js.minify(buffer.toString(), fork, cb);
- });
+ async.parallel([
+ function (cb) {
+ minifier.js.minifyBatch(filtered.minify, fork, cb);
},
- }, function (err, results) {
- if (err) {
- return next(err);
- }
-
- var minified = results.minified;
- fs.writeFile(destPath, minified.code, next);
- });
- }, callback);
+ function (cb) {
+ async.eachLimit(filtered.skip, 500, function (mod, next) {
+ file.link(mod.srcPath, mod.destPath, next);
+ }, cb);
+ },
+ ], callback);
+ });
}
function linkModules(callback) {
diff --git a/src/meta/minifier.js b/src/meta/minifier.js
index b1f2888b16..07f33d8253 100644
--- a/src/meta/minifier.js
+++ b/src/meta/minifier.js
@@ -5,6 +5,7 @@ var async = require('async');
var fs = require('fs');
var childProcess = require('child_process');
var os = require('os');
+var winston = require('winston');
var less = require('less');
var postcss = require('postcss');
var autoprefixer = require('autoprefixer');
@@ -37,23 +38,38 @@ function setupDebugging() {
return forkProcessParams;
}
-var children = [];
+var pool = [];
+var free = [];
+
+var maxThreads = 0;
+
+Object.defineProperty(Minifier, 'maxThreads', {
+ get: function () {
+ return maxThreads;
+ },
+ set: function (val) {
+ maxThreads = val;
+ winston.verbose('[minifier] utilizing a maximum of ' + maxThreads + ' additional threads');
+ },
+ configurable: true,
+ enumerable: true,
+});
+
+Minifier.maxThreads = os.cpus().length - 1;
Minifier.killAll = function () {
- children.forEach(function (child) {
+ pool.forEach(function (child) {
child.kill('SIGTERM');
});
- children = [];
+ pool.length = 0;
};
-function removeChild(proc) {
- children = children.filter(function (child) {
- return child !== proc;
- });
-}
+function getChild() {
+ if (free.length) {
+ return free.shift();
+ }
-function forkAction(action, callback) {
var forkProcessParams = setupDebugging();
var proc = childProcess.fork(__filename, [], Object.assign({}, forkProcessParams, {
cwd: __dirname,
@@ -61,17 +77,32 @@ function forkAction(action, callback) {
minifier_child: true,
},
}));
+ pool.push(proc);
+
+ return proc;
+}
+
+function freeChild(proc) {
+ proc.removeAllListeners();
+ free.push(proc);
+}
- children.push(proc);
+function removeChild(proc) {
+ var i = pool.indexOf(proc);
+ pool.splice(i, 1);
+}
+
+function forkAction(action, callback) {
+ var proc = getChild();
proc.on('message', function (message) {
+ freeChild(proc);
+
if (message.type === 'error') {
- proc.kill();
- return callback(new Error(message.message));
+ return callback(message.err);
}
if (message.type === 'end') {
- proc.kill();
callback(null, message.result);
}
});
@@ -85,10 +116,6 @@ function forkAction(action, callback) {
type: 'action',
action: action,
});
-
- proc.on('close', function () {
- removeChild(proc);
- });
}
var actions = {};
@@ -100,7 +127,7 @@ if (process.env.minifier_child) {
if (typeof actions[action.act] !== 'function') {
process.send({
type: 'error',
- message: 'Unknown action',
+ err: Error('Unknown action'),
});
return;
}
@@ -109,7 +136,7 @@ if (process.env.minifier_child) {
if (err) {
process.send({
type: 'error',
- message: err.message,
+ err: err,
});
return;
}
@@ -124,7 +151,7 @@ if (process.env.minifier_child) {
}
function executeAction(action, fork, callback) {
- if (fork) {
+ if (fork && (pool.length - free.length) < Minifier.maxThreads) {
forkAction(action, callback);
} else {
if (typeof actions[action.act] !== 'function') {
@@ -141,7 +168,7 @@ function concat(data, callback) {
return callback(err);
}
- var output = files.join(os.EOL + ';');
+ var output = files.join('\n;');
callback(null, { code: output });
});
@@ -153,32 +180,38 @@ function concat(data, callback) {
actions.concat = concat;
function minifyJS(data, callback) {
- var minified;
+ if (data.batch) {
+ async.eachLimit(data.files, 1000, function (ref, next) {
+ var srcPath = ref.srcPath;
+ var destPath = ref.destPath;
+
+ fs.readFile(srcPath, function (err, buffer) {
+ if (err && err.code === 'ENOENT') {
+ return next(null, null);
+ }
+ if (err) {
+ return next(err);
+ }
- if (data.fromSource) {
- var sources = data.source;
- var multiple = Array.isArray(sources);
- if (!multiple) {
- sources = [sources];
- }
+ try {
+ var minified = uglifyjs.minify(buffer.toString(), {
+ // outSourceMap: data.filename + '.map',
+ compress: data.compress,
+ fromString: true,
+ output: {
+ // suppress uglify line length warnings
+ max_line_len: 400000,
+ },
+ });
- try {
- minified = sources.map(function (source) {
- return uglifyjs.minify(source, {
- // outSourceMap: data.filename + '.map',
- compress: data.compress,
- fromString: true,
- output: {
- // suppress uglify line length warnings
- max_line_len: 400000,
- },
- });
+ fs.writeFile(destPath, minified.code, next);
+ } catch (e) {
+ next(e);
+ }
});
- } catch (e) {
- return callback(e);
- }
+ }, callback);
- return callback(null, multiple ? minified : minified[0]);
+ return;
}
if (data.files && data.files.length) {
@@ -188,16 +221,16 @@ function minifyJS(data, callback) {
}
try {
- minified = uglifyjs.minify(scripts, {
+ var minified = uglifyjs.minify(scripts, {
// outSourceMap: data.filename + '.map',
compress: data.compress,
fromString: false,
});
+
+ callback(null, minified);
} catch (e) {
- return callback(e);
+ callback(e);
}
-
- callback(null, minified);
});
return;
@@ -216,11 +249,11 @@ Minifier.js.bundle = function (scripts, minify, fork, callback) {
}, fork, callback);
};
-Minifier.js.minify = function (source, fork, callback) {
+Minifier.js.minifyBatch = function (scripts, fork, callback) {
executeAction({
act: 'minifyJS',
- fromSource: true,
- source: source,
+ files: scripts,
+ batch: true,
}, fork, callback);
};
diff --git a/src/notifications.js b/src/notifications.js
index 931e0ad293..d5505f88dd 100644
--- a/src/notifications.js
+++ b/src/notifications.js
@@ -29,7 +29,7 @@ Notifications.get = function (nid, callback) {
};
Notifications.getMultiple = function (nids, callback) {
- if (!nids.length) {
+ if (!Array.isArray(nids) || !nids.length) {
return setImmediate(callback, null, []);
}
var keys = nids.map(function (nid) {
@@ -106,50 +106,47 @@ Notifications.findRelated = function (mergeIds, set, callback) {
db.getObjectsFields(keys, ['mergeId'], next);
},
- ], function (err, sets) {
- if (err) {
- return callback(err);
- }
-
- sets = sets.map(function (set) {
- return set.mergeId;
- });
+ function (sets, next) {
+ sets = sets.map(function (set) {
+ return set.mergeId;
+ });
- callback(null, _nids.filter(function (nid, idx) {
- return mergeIds.indexOf(sets[idx]) !== -1;
- }));
- });
+ next(null, _nids.filter(function (nid, idx) {
+ return mergeIds.indexOf(sets[idx]) !== -1;
+ }));
+ },
+ ], callback);
};
Notifications.create = function (data, callback) {
if (!data.nid) {
- return callback(new Error('no-notification-id'));
+ return callback(new Error('[[error:no-notification-id]]'));
}
data.importance = data.importance || 5;
- db.getObject('notifications:' + data.nid, function (err, oldNotification) {
- if (err) {
- return callback(err);
- }
-
- if (oldNotification) {
- if (parseInt(oldNotification.pid, 10) === parseInt(data.pid, 10) && parseInt(oldNotification.importance, 10) > parseInt(data.importance, 10)) {
- return callback(null, null);
+ async.waterfall([
+ function (next) {
+ db.getObject('notifications:' + data.nid, next);
+ },
+ function (oldNotification, next) {
+ if (oldNotification) {
+ if (parseInt(oldNotification.pid, 10) === parseInt(data.pid, 10) && parseInt(oldNotification.importance, 10) > parseInt(data.importance, 10)) {
+ return callback(null, null);
+ }
}
- }
-
- var now = Date.now();
- data.datetime = now;
- async.parallel([
- function (next) {
- db.sortedSetAdd('notifications', now, data.nid, next);
- },
- function (next) {
- db.setObject('notifications:' + data.nid, data, next);
- },
- ], function (err) {
- callback(err, data);
- });
- });
+ var now = Date.now();
+ data.datetime = now;
+ async.parallel([
+ function (next) {
+ db.sortedSetAdd('notifications', now, data.nid, next);
+ },
+ function (next) {
+ db.setObject('notifications:' + data.nid, data, next);
+ },
+ ], function (err) {
+ next(err, data);
+ });
+ },
+ ], callback);
};
Notifications.push = function (notification, uids, callback) {
@@ -233,25 +230,31 @@ function pushToUids(uids, notification, callback) {
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);
- }
+ async.waterfall([
+ function (next) {
+ groups.getMembers(groupName, 0, -1, next);
+ },
+ function (members, next) {
+ if (!Array.isArray(members) || !members.length) {
+ return callback();
+ }
- Notifications.push(notification, members, callback);
- });
+ Notifications.push(notification, members, next);
+ },
+ ], callback);
};
Notifications.pushGroups = function (notification, groupNames, callback) {
callback = callback || function () {};
- groups.getMembersOfGroups(groupNames, function (err, groupMembers) {
- if (err) {
- return callback(err);
- }
-
- var members = _.unique(_.flatten(groupMembers));
- Notifications.push(notification, members, callback);
- });
+ async.waterfall([
+ function (next) {
+ groups.getMembersOfGroups(groupNames, next);
+ },
+ function (groupMembers, next) {
+ var members = _.unique(_.flatten(groupMembers));
+ Notifications.push(notification, members, next);
+ },
+ ], callback);
};
Notifications.rescind = function (nid, callback) {
@@ -261,13 +264,7 @@ Notifications.rescind = function (nid, callback) {
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);
+ callback(err);
});
};
@@ -284,18 +281,22 @@ Notifications.markUnread = function (nid, uid, callback) {
if (!parseInt(uid, 10) || !nid) {
return callback();
}
+ async.waterfall([
+ function (next) {
+ db.getObject('notifications:' + nid, next);
+ },
+ function (notification, next) {
+ if (!notification) {
+ return callback(new Error('[[error:no-notification]]'));
+ }
+ notification.datetime = notification.datetime || Date.now();
- db.getObject('notifications:' + nid, function (err, notification) {
- if (err || !notification) {
- return callback(err || new Error('[[error:no-notification]]'));
- }
- notification.datetime = notification.datetime || Date.now();
-
- async.parallel([
- async.apply(db.sortedSetRemove, 'uid:' + uid + ':notifications:read', nid),
- async.apply(db.sortedSetAdd, 'uid:' + uid + ':notifications:unread', notification.datetime, nid),
- ], callback);
- });
+ async.parallel([
+ async.apply(db.sortedSetRemove, 'uid:' + uid + ':notifications:read', nid),
+ async.apply(db.sortedSetAdd, 'uid:' + uid + ':notifications:unread', notification.datetime, nid),
+ ], next);
+ },
+ ], callback);
};
Notifications.markReadMultiple = function (nids, uid, callback) {
@@ -377,9 +378,9 @@ Notifications.markAllRead = function (uid, callback) {
Notifications.prune = function (callback) {
callback = callback || function () {};
- var week = 604800000;
+ var week = 604800000;
- var cutoffTime = Date.now() - week;
+ var cutoffTime = Date.now() - week;
async.waterfall([
function (next) {
@@ -390,7 +391,7 @@ Notifications.prune = function (callback) {
return callback();
}
- var keys = nids.map(function (nid) {
+ var keys = nids.map(function (nid) {
return 'notifications:' + nid;
});
diff --git a/src/routes/feeds.js b/src/routes/feeds.js
index 5e3d75c34d..3171babbf6 100644
--- a/src/routes/feeds.js
+++ b/src/routes/feeds.js
@@ -12,6 +12,7 @@ var categories = require('../categories');
var meta = require('../meta');
var helpers = require('../controllers/helpers');
var privileges = require('../privileges');
+var controllers404 = require('../controllers/404.js');
module.exports = function (app, middleware) {
app.get('/topic/:topic_id.rss', middleware.maintenanceMode, generateForTopic);
@@ -25,10 +26,9 @@ module.exports = function (app, middleware) {
app.get('/tags/:tag.rss', middleware.maintenanceMode, generateForTag);
};
-
function generateForTopic(req, res, callback) {
if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) {
- return callback();
+ return controllers404.send404(req, res);
}
var tid = req.params.topic_id;
@@ -45,65 +45,59 @@ function generateForTopic(req, res, callback) {
}, next);
},
function (results, next) {
- if (!results.topic) {
- return callback();
+ if (!results.topic || (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted)) {
+ return controllers404.send404(req, res);
}
- if (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted) {
- return callback();
- }
- if (!results.privileges.read || !results.privileges['topics:read']) {
+ if (!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) {
- if (err) {
- return callback(err);
- }
-
- topics.modifyPostsByPrivilege(topicData, userPrivileges);
-
- var description = topicData.posts.length ? topicData.posts[0].content : '';
- var image_url = topicData.posts.length ? topicData.posts[0].picture : '';
- var author = topicData.posts.length ? topicData.posts[0].username : '';
-
- var feed = new rss({
- title: topicData.title,
- description: description,
- feed_url: nconf.get('url') + '/topic/' + tid + '.rss',
- site_url: nconf.get('url') + '/topic/' + topicData.slug,
- image_url: image_url,
- author: author,
- ttl: 60,
- });
- var dateStamp;
-
- if (topicData.posts.length > 0) {
- feed.pubDate = new Date(parseInt(topicData.posts[0].timestamp, 10)).toUTCString();
- }
-
- 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') + '/post/' + postData.pid,
- author: postData.user ? postData.user.username : '',
- date: dateStamp,
- });
+ function (topicData) {
+ topics.modifyPostsByPrivilege(topicData, userPrivileges);
+
+ var description = topicData.posts.length ? topicData.posts[0].content : '';
+ var image_url = topicData.posts.length ? topicData.posts[0].picture : '';
+ var author = topicData.posts.length ? topicData.posts[0].username : '';
+
+ var feed = new rss({
+ title: topicData.title,
+ description: description,
+ feed_url: nconf.get('url') + '/topic/' + tid + '.rss',
+ site_url: nconf.get('url') + '/topic/' + topicData.slug,
+ image_url: image_url,
+ author: author,
+ ttl: 60,
+ });
+ var dateStamp;
+
+ if (topicData.posts.length > 0) {
+ feed.pubDate = new Date(parseInt(topicData.posts[0].timestamp, 10)).toUTCString();
}
- });
- sendFeed(feed, res);
- });
+ 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') + '/post/' + postData.pid,
+ author: postData.user ? postData.user.username : '',
+ date: dateStamp,
+ });
+ }
+ });
+
+ sendFeed(feed, res);
+ },
+ ], callback);
}
function generateForUserTopics(req, res, callback) {
if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) {
- return callback();
+ return controllers404.send404(req, res);
}
var userslug = req.params.userslug;
@@ -118,24 +112,21 @@ function generateForUserTopics(req, res, callback) {
}
user.getUserFields(uid, ['uid', 'username'], next);
},
- ], function (err, userData) {
- if (err) {
- return callback(err);
- }
-
- generateForTopics({
- uid: req.uid,
- title: 'Topics by ' + userData.username,
- 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, callback);
- });
+ function (userData, next) {
+ generateForTopics({
+ uid: req.uid,
+ title: 'Topics by ' + userData.username,
+ 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);
+ },
+ ], callback);
}
function generateForCategory(req, res, next) {
if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) {
- return next();
+ return controllers404.send404(req, res);
}
var cid = req.params.category_id;
@@ -169,17 +160,15 @@ function generateForCategory(req, res, next) {
site_url: '/category/' + results.category.cid,
}, results.category.topics, next);
},
- ], function (err, feed) {
- if (err) {
- return next(err);
- }
- sendFeed(feed, res);
- });
+ function (feed) {
+ sendFeed(feed, res);
+ },
+ ], next);
}
function generateForRecent(req, res, next) {
if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) {
- return next();
+ return controllers404.send404(req, res);
}
generateForTopics({
uid: req.uid,
@@ -192,7 +181,7 @@ function generateForRecent(req, res, next) {
function generateForPopular(req, res, next) {
if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) {
- return next();
+ return controllers404.send404(req, res);
}
var terms = {
daily: 'day',
@@ -215,12 +204,10 @@ function generateForPopular(req, res, next) {
site_url: '/popular/' + (req.params.term || 'daily'),
}, topics, next);
},
- ], function (err, feed) {
- if (err) {
- return next(err);
- }
- sendFeed(feed, res);
- });
+ function (feed) {
+ sendFeed(feed, res);
+ },
+ ], next);
}
function generateForTopics(options, set, req, res, next) {
@@ -233,12 +220,10 @@ function generateForTopics(options, set, req, res, next) {
function (data, next) {
generateTopicsFeed(options, data.topics, next);
},
- ], function (err, feed) {
- if (err) {
- return next(err);
- }
- sendFeed(feed, res);
- });
+ function (feed) {
+ sendFeed(feed, res);
+ },
+ ], next);
}
function generateTopicsFeed(feedOptions, feedTopics, callback) {
@@ -254,7 +239,7 @@ function generateTopicsFeed(feedOptions, feedTopics, callback) {
feed.pubDate = new Date(parseInt(feedTopics[0].lastposttime, 10)).toUTCString();
}
- async.map(feedTopics, function (topicData, next) {
+ async.each(feedTopics, function (topicData, next) {
var feedItem = {
title: topicData.title,
url: nconf.get('url') + '/topic/' + topicData.slug,
@@ -272,83 +257,80 @@ function generateTopicsFeed(feedOptions, feedTopics, callback) {
return next(err);
}
if (!mainPost) {
- return next(null, feedItem);
+ feed.item(feedItem);
+ return next();
}
feedItem.description = mainPost.content;
feedItem.author = mainPost.user.username;
- next(null, feedItem);
+ feed.item(feedItem);
+ next();
});
- }, function (err, feedItems) {
- if (err) {
- return callback(err);
- }
- feedItems.forEach(function (feedItem) {
- if (feedItem) {
- feed.item(feedItem);
- }
- });
- callback(null, feed);
+ }, function (err) {
+ callback(err, feed);
});
}
function generateForRecentPosts(req, res, next) {
if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) {
- return next();
+ return controllers404.send404(req, res);
}
- posts.getRecentPosts(req.uid, 0, 19, 'month', function (err, posts) {
- if (err) {
- return next(err);
- }
-
- var feed = generateForPostsFeed({
- title: 'Recent Posts',
- description: 'A list of recent posts',
- feed_url: '/recentposts.rss',
- site_url: '/recentposts',
- }, posts);
-
- sendFeed(feed, res);
- });
+ async.waterfall([
+ function (next) {
+ posts.getRecentPosts(req.uid, 0, 19, 'month', next);
+ },
+ function (posts) {
+ var feed = generateForPostsFeed({
+ title: 'Recent Posts',
+ description: 'A list of recent posts',
+ feed_url: '/recentposts.rss',
+ site_url: '/recentposts',
+ }, posts);
+
+ sendFeed(feed, res);
+ },
+ ], next);
}
function generateForCategoryRecentPosts(req, res, next) {
if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) {
- return next();
+ return controllers404.send404(req, res);
}
var cid = req.params.category_id;
- async.parallel({
- privileges: function (next) {
- privileges.categories.get(cid, req.uid, next);
- },
- category: function (next) {
- categories.getCategoryData(cid, next);
- },
- posts: function (next) {
- categories.getRecentReplies(cid, req.uid, 20, next);
+ async.waterfall([
+ function (next) {
+ async.parallel({
+ privileges: function (next) {
+ privileges.categories.get(cid, req.uid, next);
+ },
+ category: function (next) {
+ categories.getCategoryData(cid, next);
+ },
+ posts: function (next) {
+ categories.getRecentReplies(cid, req.uid, 20, next);
+ },
+ }, next);
},
- }, function (err, results) {
- if (err) {
- return next(err);
- }
- if (!results.category) {
- return next();
- }
+ function (results, next) {
+ if (!results.category) {
+ return next();
+ }
- if (!results.privileges.read) {
- return helpers.notAllowed(req, res);
- }
+ if (!results.privileges.read) {
+ return helpers.notAllowed(req, res);
+ }
- var feed = generateForPostsFeed({
- title: results.category.name + ' Recent Posts',
- description: 'A list of recent posts from ' + results.category.name,
- feed_url: '/category/' + cid + '/recentposts.rss',
- site_url: '/category/' + cid + '/recentposts',
- }, results.posts);
+ var feed = generateForPostsFeed({
+ title: results.category.name + ' Recent Posts',
+ description: 'A list of recent posts from ' + results.category.name,
+ feed_url: '/category/' + cid + '/recentposts.rss',
+ site_url: '/category/' + cid + '/recentposts',
+ }, results.posts);
- sendFeed(feed, res);
- });
+ sendFeed(feed, res);
+ },
+ ], next);
}
function generateForPostsFeed(feedOptions, posts) {
@@ -377,7 +359,7 @@ function generateForPostsFeed(feedOptions, posts) {
function generateForTag(req, res, next) {
if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) {
- return next();
+ return controllers404.send404(req, res);
}
var tag = validator.escape(String(req.params.tag));
var page = parseInt(req.query.page, 10) || 1;
diff --git a/src/socket.io/helpers.js b/src/socket.io/helpers.js
index 241937d5fb..9eedab2fb0 100644
--- a/src/socket.io/helpers.js
+++ b/src/socket.io/helpers.js
@@ -169,21 +169,26 @@ SocketHelpers.sendNotificationToTopicOwner = function (tid, fromuid, command, no
};
SocketHelpers.rescindUpvoteNotification = function (pid, fromuid) {
- var nid = 'upvote:post:' + pid + ':uid:' + fromuid;
- notifications.rescind(nid);
-
- posts.getPostField(pid, 'uid', function (err, uid) {
+ var uid;
+ async.waterfall([
+ function (next) {
+ notifications.rescind('upvote:post:' + pid + ':uid:' + fromuid, next);
+ },
+ function (next) {
+ posts.getPostField(pid, 'uid', next);
+ },
+ function (_uid, next) {
+ uid = _uid;
+ user.notifications.getUnreadCount(uid, next);
+ },
+ function (count, next) {
+ websockets.in('uid_' + uid).emit('event:notifications.updateCount', count);
+ next();
+ },
+ ], function (err) {
if (err) {
- return winston.error(err);
+ winston.error(err);
}
-
- user.notifications.getUnreadCount(uid, function (err, count) {
- if (err) {
- return winston.error(err);
- }
-
- websockets.in('uid_' + uid).emit('event:notifications.updateCount', count);
- });
});
};
diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js
index a3a9ff4fe3..172d91e60f 100644
--- a/src/socket.io/modules.js
+++ b/src/socket.io/modules.js
@@ -12,16 +12,16 @@ var utils = require('../utils');
var server = require('./');
var user = require('../user');
-var SocketModules = {
- chats: {},
- sounds: {},
- settings: {},
-};
+var SocketModules = module.exports;
+
+SocketModules.chats = {};
+SocketModules.sounds = {};
+SocketModules.settings = {};
/* Chat */
SocketModules.chats.getRaw = function (socket, data, callback) {
- if (!data || !data.hasOwnProperty('mid')) {
+ if (!data || !data.hasOwnProperty('mid') || !data.hasOwnProperty('roomId')) {
return callback(new Error('[[error:invalid-data]]'));
}
async.waterfall([
@@ -57,13 +57,14 @@ SocketModules.chats.newRoom = function (socket, data, callback) {
return callback(new Error('[[error:too-many-messages]]'));
}
- Messaging.canMessageUser(socket.uid, data.touid, function (err) {
- if (err) {
- return callback(err);
- }
-
- Messaging.newRoom(socket.uid, [data.touid], callback);
- });
+ async.waterfall([
+ function (next) {
+ Messaging.canMessageUser(socket.uid, data.touid, next);
+ },
+ function (next) {
+ Messaging.newRoom(socket.uid, [data.touid], next);
+ },
+ ], callback);
};
SocketModules.chats.send = function (socket, data, callback) {
@@ -223,17 +224,21 @@ SocketModules.chats.leave = function (socket, roomid, callback) {
SocketModules.chats.edit = function (socket, data, callback) {
- if (!data || !data.roomId) {
+ if (!data || !data.roomId || !data.message) {
return callback(new Error('[[error:invalid-data]]'));
}
- Messaging.canEdit(data.mid, socket.uid, function (err, allowed) {
- if (err || !allowed) {
- return callback(err || new Error('[[error:cant-edit-chat-message]]'));
- }
-
- Messaging.editMessage(socket.uid, data.mid, data.roomId, data.message, callback);
- });
+ async.waterfall([
+ function (next) {
+ Messaging.canEdit(data.mid, socket.uid, next);
+ },
+ function (allowed, next) {
+ if (!allowed) {
+ return next(new Error('[[error:cant-edit-chat-message]]'));
+ }
+ Messaging.editMessage(socket.uid, data.mid, data.roomId, data.message, next);
+ },
+ ], callback);
};
SocketModules.chats.delete = function (socket, data, callback) {
@@ -241,13 +246,18 @@ SocketModules.chats.delete = function (socket, data, callback) {
return callback(new Error('[[error:invalid-data]]'));
}
- Messaging.canEdit(data.messageId, socket.uid, function (err, allowed) {
- if (err || !allowed) {
- return callback(err || new Error('[[error:cant-delete-chat-message]]'));
- }
+ async.waterfall([
+ function (next) {
+ Messaging.canEdit(data.messageId, socket.uid, next);
+ },
+ function (allowed, next) {
+ if (!allowed) {
+ return next(new Error('[[error:cant-delete-chat-message]]'));
+ }
- Messaging.deleteMessage(data.messageId, data.roomId, callback);
- });
+ Messaging.deleteMessage(data.messageId, data.roomId, next);
+ },
+ ], callback);
};
SocketModules.chats.canMessage = function (socket, roomId, callback) {
@@ -255,37 +265,38 @@ SocketModules.chats.canMessage = function (socket, roomId, callback) {
};
SocketModules.chats.markRead = function (socket, roomId, callback) {
- if (!socket.uid) {
+ if (!socket.uid || !roomId) {
return callback(new Error('[[error:invalid-data]]'));
}
- async.parallel({
- uidsInRoom: async.apply(Messaging.getUidsInRoom, roomId, 0, -1),
- markRead: async.apply(Messaging.markRead, socket.uid, roomId),
- }, function (err, results) {
- if (err) {
- return callback(err);
- }
-
- Messaging.pushUnreadCount(socket.uid);
- server.in('uid_' + socket.uid).emit('event:chats.markedAsRead', { roomId: roomId });
-
- if (results.uidsInRoom.indexOf(socket.uid.toString()) === -1) {
- return callback();
- }
-
- // Mark notification read
- var nids = results.uidsInRoom.filter(function (uid) {
- return parseInt(uid, 10) !== socket.uid;
- }).map(function (uid) {
- return 'chat_' + uid + '_' + roomId;
- });
-
- notifications.markReadMultiple(nids, socket.uid, function () {
- user.notifications.pushCount(socket.uid);
- });
-
- callback();
- });
+ async.waterfall([
+ function (next) {
+ async.parallel({
+ uidsInRoom: async.apply(Messaging.getUidsInRoom, roomId, 0, -1),
+ markRead: async.apply(Messaging.markRead, socket.uid, roomId),
+ }, next);
+ },
+ function (results, next) {
+ Messaging.pushUnreadCount(socket.uid);
+ server.in('uid_' + socket.uid).emit('event:chats.markedAsRead', { roomId: roomId });
+
+ if (results.uidsInRoom.indexOf(socket.uid.toString()) === -1) {
+ return callback();
+ }
+
+ // Mark notification read
+ var nids = results.uidsInRoom.filter(function (uid) {
+ return parseInt(uid, 10) !== socket.uid;
+ }).map(function (uid) {
+ return 'chat_' + uid + '_' + roomId;
+ });
+
+ notifications.markReadMultiple(nids, socket.uid, function () {
+ user.notifications.pushCount(socket.uid);
+ });
+
+ next();
+ },
+ ], callback);
};
SocketModules.chats.markAllRead = function (socket, data, callback) {
@@ -301,8 +312,8 @@ SocketModules.chats.markAllRead = function (socket, data, callback) {
};
SocketModules.chats.renameRoom = function (socket, data, callback) {
- if (!data) {
- return callback(new Error('[[error:invalid-name]]'));
+ if (!data || !data.roomId || !data.newName) {
+ return callback(new Error('[[error:invalid-data]]'));
}
async.waterfall([
@@ -333,13 +344,13 @@ SocketModules.chats.getRecentChats = function (socket, data, callback) {
SocketModules.chats.hasPrivateChat = function (socket, uid, callback) {
if (!socket.uid || !uid) {
- return callback(null, new Error('[[error:invalid-data]]'));
+ return callback(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) {
+ if (!socket.uid || !data || !data.uid || !data.roomId) {
return callback(new Error('[[error:invalid-data]]'));
}
@@ -358,5 +369,3 @@ SocketModules.chats.getMessages = function (socket, data, callback) {
SocketModules.sounds.getUserSoundMap = function getUserSoundMap(socket, data, callback) {
meta.sounds.getUserSoundMap(socket.uid, callback);
};
-
-module.exports = SocketModules;
diff --git a/src/socket.io/topics/infinitescroll.js b/src/socket.io/topics/infinitescroll.js
index 1d732de2a4..17d44712ea 100644
--- a/src/socket.io/topics/infinitescroll.js
+++ b/src/socket.io/topics/infinitescroll.js
@@ -27,7 +27,7 @@ module.exports = function (SocketTopics) {
}, next);
},
function (results, next) {
- if (!results.privileges.read || (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted)) {
+ if (!results.privileges['topics:read'] || (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted)) {
return callback(new Error('[[error:no-privileges]]'));
}
diff --git a/src/topics/posts.js b/src/topics/posts.js
index ce2b6dd457..3ed8deafc7 100644
--- a/src/topics/posts.js
+++ b/src/topics/posts.js
@@ -392,30 +392,36 @@ module.exports = function (Topics) {
function getPostReplies(pids, callerUid, callback) {
async.map(pids, function (pid, next) {
- db.getSortedSetRange('pid:' + pid + ':replies', 0, -1, function (err, replyPids) {
- if (err) {
- return next(err);
- }
-
- var uids = [];
- var count = 0;
-
- async.until(function () {
- return count === replyPids.length || uids.length === 6;
- }, function (next) {
- posts.getPostField(replyPids[count], 'uid', function (err, uid) {
- uid = parseInt(uid, 10);
- if (uids.indexOf(uid) === -1) {
- uids.push(uid);
- }
- count += 1;
- next(err);
- });
- }, function (err) {
- if (err) {
- return next(err);
- }
-
+ var replyPids;
+ var uids = [];
+ async.waterfall([
+ function (next) {
+ db.getSortedSetRange('pid:' + pid + ':replies', 0, -1, next);
+ },
+ function (_replyPids, next) {
+ replyPids = _replyPids;
+
+ var count = 0;
+
+ async.until(function () {
+ return count === replyPids.length || uids.length === 6;
+ }, function (next) {
+ async.waterfall([
+ function (next) {
+ posts.getPostField(replyPids[count], 'uid', next);
+ },
+ function (uid, next) {
+ uid = parseInt(uid, 10);
+ if (uids.indexOf(uid) === -1) {
+ uids.push(uid);
+ }
+ count += 1;
+ next();
+ },
+ ], next);
+ }, next);
+ },
+ function (next) {
async.parallel({
users: function (next) {
user.getUsersWithFields(uids, ['uid', 'username', 'userslug', 'picture'], callerUid, next);
@@ -425,17 +431,19 @@ module.exports = function (Topics) {
next(err, utils.toISOString(timestamp));
});
},
- }, function (err, replies) {
- if (replies.users.length > 5) {
- replies.users.shift();
- replies.hasMore = true;
- }
+ }, next);
+ },
+ function (replies, next) {
+ if (replies.users.length > 5) {
+ replies.users.shift();
+ replies.hasMore = true;
+ }
- replies.count = replyPids.length;
- next(err, replies);
- });
- });
- });
+ replies.count = replyPids.length;
+ replies.text = replies.count > 1 ? '[[topic:replies_to_this_post, ' + replies.count + ']]' : '[[topic:one_reply_to_this_post]]';
+ next(null, replies);
+ },
+ ], next);
}, callback);
}
};
diff --git a/src/topics/unread.js b/src/topics/unread.js
index ac26a277f9..f76206628f 100644
--- a/src/topics/unread.js
+++ b/src/topics/unread.js
@@ -267,6 +267,7 @@ module.exports = function (Topics) {
categories.markAsRead(cids, uid, next);
},
function (next) {
+ plugins.fireHook('action:topics.markAsRead', { uid: uid, tids: tids });
next(null, true);
},
], callback);
diff --git a/src/upgrade.js b/src/upgrade.js
index 22c7867eeb..2bba82dd91 100644
--- a/src/upgrade.js
+++ b/src/upgrade.js
@@ -13,9 +13,8 @@ var file = require('../src/file');
*
* 1. Copy TEMPLATE to a file name of your choice. Try to be succinct.
* 2. Open up that file and change the user-friendly name (can be longer/more descriptive than the file name)
- * and timestamp
+ * and timestamp (don't forget the timestamp!)
* 3. Add your script under the "method" property
- * 4. Append your filename to the array below for the next NodeBB version.
*/
var Upgrade = {};
diff --git a/src/user/bans.js b/src/user/bans.js
index ad51c7c07c..e2cf2193b3 100644
--- a/src/user/bans.js
+++ b/src/user/bans.js
@@ -61,7 +61,7 @@ module.exports = function (User) {
async.waterfall([
async.apply(User.getUserFields, uid, ['banned', 'banned:expire']),
function (userData, next) {
- var banned = parseInt(userData.banned, 10) === 1;
+ var banned = userData && parseInt(userData.banned, 10) === 1;
if (!banned) {
return next(null, banned);
}
diff --git a/src/user/follow.js b/src/user/follow.js
index fe3dc0931d..d2056065b4 100644
--- a/src/user/follow.js
+++ b/src/user/follow.js
@@ -57,7 +57,9 @@ module.exports = function (User) {
], next);
}
},
- ], callback);
+ ], function (err) {
+ callback(err);
+ });
}
User.getFollowing = function (uid, start, stop, callback) {
diff --git a/src/user/notifications.js b/src/user/notifications.js
index 318794fb20..953caeb2c2 100644
--- a/src/user/notifications.js
+++ b/src/user/notifications.js
@@ -10,16 +10,17 @@ var meta = require('../meta');
var notifications = require('../notifications');
var privileges = require('../privileges');
-(function (UserNotifications) {
- UserNotifications.get = function (uid, callback) {
- if (!parseInt(uid, 10)) {
- return callback(null, { read: [], unread: [] });
- }
- getNotifications(uid, 0, 9, function (err, notifications) {
- if (err) {
- return callback(err);
- }
+var UserNotifications = module.exports;
+UserNotifications.get = function (uid, callback) {
+ if (!parseInt(uid, 10)) {
+ return callback(null, { read: [], unread: [] });
+ }
+ async.waterfall([
+ function (next) {
+ getNotifications(uid, 0, 9, next);
+ },
+ function (notifications, next) {
notifications.read = notifications.read.filter(Boolean);
notifications.unread = notifications.unread.filter(Boolean);
@@ -28,326 +29,336 @@ var privileges = require('../privileges');
notifications.read.length = maxNotifs - notifications.unread.length;
}
- callback(null, notifications);
- });
- };
-
- function filterNotifications(nids, filter, callback) {
- if (!filter) {
- return setImmediate(callback, null, nids);
- }
- async.waterfall([
- function (next) {
- var keys = nids.map(function (nid) {
- return 'notifications:' + nid;
- });
- db.getObjectsFields(keys, ['nid', 'type'], next);
- },
- function (notifications, next) {
- nids = notifications.filter(function (notification) {
- return notification && notification.nid && notification.type === filter;
- }).map(function (notification) {
- return notification.nid;
- });
- next(null, nids);
- },
- ], callback);
- }
-
- UserNotifications.getAll = function (uid, filter, callback) {
- var nids;
- async.waterfall([
- function (next) {
- async.parallel({
- unread: function (next) {
- db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, -1, next);
- },
- read: function (next) {
- db.getSortedSetRevRange('uid:' + uid + ':notifications:read', 0, -1, next);
- },
- }, next);
- },
- function (results, next) {
- nids = results.unread.concat(results.read);
- db.isSortedSetMembers('notifications', nids, next);
- },
- function (exists, next) {
- var deleteNids = [];
-
- nids = nids.filter(function (nid, index) {
- if (!nid || !exists[index]) {
- deleteNids.push(nid);
- }
- return nid && exists[index];
- });
-
- deleteUserNids(deleteNids, uid, next);
- },
- function (next) {
- filterNotifications(nids, filter, next);
- },
- ], callback);
- };
-
- function deleteUserNids(nids, uid, callback) {
- callback = callback || function () {};
- if (!nids.length) {
- return setImmediate(callback);
- }
- async.parallel([
- function (next) {
- db.sortedSetRemove('uid:' + uid + ':notifications:read', nids, next);
- },
- function (next) {
- db.sortedSetRemove('uid:' + uid + ':notifications:unread', nids, next);
- },
- ], function (err) {
- callback(err);
- });
- }
+ next(null, notifications);
+ },
+ ], callback);
+};
- function getNotifications(uid, start, stop, callback) {
- async.parallel({
- unread: function (next) {
- getNotificationsFromSet('uid:' + uid + ':notifications:unread', false, uid, start, stop, next);
- },
- read: function (next) {
- getNotificationsFromSet('uid:' + uid + ':notifications:read', true, uid, start, stop, next);
- },
- }, callback);
+function filterNotifications(nids, filter, callback) {
+ if (!filter) {
+ return setImmediate(callback, null, nids);
}
-
- function getNotificationsFromSet(set, read, uid, start, stop, callback) {
- async.waterfall([
- function (next) {
- db.getSortedSetRevRange(set, start, stop, next);
- },
- function (nids, next) {
- if (!Array.isArray(nids) || !nids.length) {
- return callback(null, []);
+ async.waterfall([
+ function (next) {
+ var keys = nids.map(function (nid) {
+ return 'notifications:' + nid;
+ });
+ db.getObjectsFields(keys, ['nid', 'type'], next);
+ },
+ function (notifications, next) {
+ nids = notifications.filter(function (notification) {
+ return notification && notification.nid && notification.type === filter;
+ }).map(function (notification) {
+ return notification.nid;
+ });
+ next(null, nids);
+ },
+ ], callback);
+}
+
+UserNotifications.getAll = function (uid, filter, callback) {
+ var nids;
+ async.waterfall([
+ function (next) {
+ async.parallel({
+ unread: function (next) {
+ db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, -1, next);
+ },
+ read: function (next) {
+ db.getSortedSetRevRange('uid:' + uid + ':notifications:read', 0, -1, next);
+ },
+ }, next);
+ },
+ function (results, next) {
+ nids = results.unread.concat(results.read);
+ db.isSortedSetMembers('notifications', nids, next);
+ },
+ function (exists, next) {
+ var deleteNids = [];
+
+ nids = nids.filter(function (nid, index) {
+ if (!nid || !exists[index]) {
+ deleteNids.push(nid);
}
+ return nid && exists[index];
+ });
- UserNotifications.getNotifications(nids, uid, next);
- },
- ], callback);
+ deleteUserNids(deleteNids, uid, next);
+ },
+ function (next) {
+ filterNotifications(nids, filter, next);
+ },
+ ], callback);
+};
+
+function deleteUserNids(nids, uid, callback) {
+ callback = callback || function () {};
+ if (!nids.length) {
+ return setImmediate(callback);
}
-
- UserNotifications.getNotifications = function (nids, uid, callback) {
- var notificationData = [];
- async.waterfall([
- function (next) {
- async.parallel({
- notifications: function (next) {
- notifications.getMultiple(nids, next);
- },
- hasRead: function (next) {
- db.isSortedSetMembers('uid:' + uid + ':notifications:read', nids, next);
- },
- }, next);
- },
- function (results, next) {
- var deletedNids = [];
- notificationData = results.notifications.filter(function (notification, index) {
- if (!notification || !notification.nid) {
- deletedNids.push(nids[index]);
- }
- if (notification) {
- notification.read = results.hasRead[index];
- notification.readClass = !notification.read ? 'unread' : '';
- }
-
- return notification && notification.path;
- });
-
- deleteUserNids(deletedNids, uid, next);
- },
- function (next) {
- notifications.merge(notificationData, next);
- },
- ], 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) {
- if (err) {
- return callback(err);
- }
-
+ async.parallel([
+ function (next) {
+ db.sortedSetRemove('uid:' + uid + ':notifications:read', nids, next);
+ },
+ function (next) {
+ db.sortedSetRemove('uid:' + uid + ':notifications:unread', nids, next);
+ },
+ ], function (err) {
+ callback(err);
+ });
+}
+
+function getNotifications(uid, start, stop, callback) {
+ async.parallel({
+ unread: function (next) {
+ getNotificationsFromSet('uid:' + uid + ':notifications:unread', false, uid, start, stop, next);
+ },
+ read: function (next) {
+ getNotificationsFromSet('uid:' + uid + ':notifications:read', true, uid, start, stop, next);
+ },
+ }, callback);
+}
+
+function getNotificationsFromSet(set, read, uid, start, stop, callback) {
+ async.waterfall([
+ function (next) {
+ db.getSortedSetRevRange(set, start, stop, next);
+ },
+ function (nids, next) {
if (!Array.isArray(nids) || !nids.length) {
return callback(null, []);
}
- UserNotifications.getNotifications(nids, uid, callback);
- });
- };
-
- UserNotifications.getUnreadCount = function (uid, callback) {
- if (!parseInt(uid, 10)) {
- return callback(null, 0);
- }
+ UserNotifications.getNotifications(nids, uid, next);
+ },
+ ], callback);
+}
+
+UserNotifications.getNotifications = function (nids, uid, callback) {
+ var notificationData = [];
+ async.waterfall([
+ function (next) {
+ async.parallel({
+ notifications: function (next) {
+ notifications.getMultiple(nids, next);
+ },
+ hasRead: function (next) {
+ db.isSortedSetMembers('uid:' + uid + ':notifications:read', nids, next);
+ },
+ }, next);
+ },
+ function (results, next) {
+ var deletedNids = [];
+ notificationData = results.notifications.filter(function (notification, index) {
+ if (!notification || !notification.nid) {
+ deletedNids.push(nids[index]);
+ }
+ if (notification) {
+ notification.read = results.hasRead[index];
+ notification.readClass = !notification.read ? 'unread' : '';
+ }
- // Collapse any notifications with identical mergeIds
- async.waterfall([
- async.apply(db.getSortedSetRevRange, 'uid:' + uid + ':notifications:unread', 0, 99),
- async.apply(notifications.filterExists),
- function (nids, next) {
- var keys = nids.map(function (nid) {
- return 'notifications:' + nid;
- });
-
- db.getObjectsFields(keys, ['mergeId'], next);
- },
- function (mergeIds, next) {
- mergeIds = mergeIds.map(function (set) {
- return set.mergeId;
- });
-
- next(null, mergeIds.reduce(function (count, mergeId, idx, arr) {
- // A missing (null) mergeId means that notification is counted separately.
- if (mergeId === null || idx === arr.indexOf(mergeId)) {
- count += 1;
- }
-
- return count;
- }, 0));
- },
- ], callback);
- };
-
- UserNotifications.getUnreadByField = function (uid, field, values, callback) {
- db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function (err, nids) {
- if (err) {
- return callback(err);
- }
+ return notification && notification.path;
+ });
+ deleteUserNids(deletedNids, uid, next);
+ },
+ function (next) {
+ notifications.merge(notificationData, next);
+ },
+ ], 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.
+
+ async.waterfall([
+ function (next) {
+ db.getSortedSetRevRangeByScore('uid:' + uid + ':notifications:unread', 0, 20, '+inf', yesterday, next);
+ },
+ function (nids, next) {
if (!Array.isArray(nids) || !nids.length) {
return callback(null, []);
}
+ UserNotifications.getNotifications(nids, uid, next);
+ },
+ ], callback);
+};
+
+UserNotifications.getUnreadCount = function (uid, callback) {
+ if (!parseInt(uid, 10)) {
+ return callback(null, 0);
+ }
+
+
+ async.waterfall([
+ function (next) {
+ db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, next);
+ },
+ function (nids, next) {
+ notifications.filterExists(nids, next);
+ },
+ function (nids, next) {
var keys = nids.map(function (nid) {
return 'notifications:' + nid;
});
- db.getObjectsFields(keys, ['nid', field], function (err, notifications) {
- if (err) {
- return callback(err);
+ db.getObjectsFields(keys, ['mergeId'], next);
+ },
+ function (mergeIds, next) {
+ // Collapse any notifications with identical mergeIds
+ mergeIds = mergeIds.map(function (set) {
+ return set.mergeId;
+ });
+
+ next(null, mergeIds.reduce(function (count, mergeId, idx, arr) {
+ // A missing (null) mergeId means that notification is counted separately.
+ if (mergeId === null || idx === arr.indexOf(mergeId)) {
+ count += 1;
}
- 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;
- });
+ return count;
+ }, 0));
+ },
+ ], callback);
+};
+
+UserNotifications.getUnreadByField = function (uid, field, values, callback) {
+ var nids;
+ async.waterfall([
+ function (next) {
+ db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, next);
+ },
+ function (_nids, next) {
+ nids = _nids;
+ if (!Array.isArray(nids) || !nids.length) {
+ return callback(null, []);
+ }
- callback(null, nids);
+ var keys = nids.map(function (nid) {
+ return 'notifications:' + nid;
});
- });
- };
- UserNotifications.deleteAll = function (uid, callback) {
- if (!parseInt(uid, 10)) {
- return callback();
- }
- async.parallel([
- function (next) {
- db.delete('uid:' + uid + ':notifications:unread', next);
- },
- function (next) {
- db.delete('uid:' + uid + ':notifications:read', next);
- },
- ], callback);
- };
-
- 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;
- }
+ db.getObjectsFields(keys, ['nid', field], next);
+ },
+ function (notifications, next) {
+ 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;
+ });
- var title = topicData.title;
- if (title) {
- title = S(title).decodeHTMLEntities().s;
- }
+ next(null, nids);
+ },
+ ], callback);
+};
- notifications.create({
- type: 'new-topic',
- 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,
- }, next);
- },
- ], function (err, notification) {
- if (err) {
- return winston.error(err);
+UserNotifications.deleteAll = function (uid, callback) {
+ if (!parseInt(uid, 10)) {
+ return callback();
+ }
+ async.parallel([
+ function (next) {
+ db.delete('uid:' + uid + ':notifications:unread', next);
+ },
+ function (next) {
+ db.delete('uid:' + uid + ':notifications:read', next);
+ },
+ ], callback);
+};
+
+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;
}
- if (notification) {
- notifications.push(notification, followers);
+ var title = topicData.title;
+ if (title) {
+ title = S(title).decodeHTMLEntities().s;
}
- });
- };
- UserNotifications.sendWelcomeNotification = function (uid, callback) {
- callback = callback || function () {};
- if (!meta.config.welcomeNotification) {
- return callback();
+ notifications.create({
+ type: 'new-topic',
+ 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,
+ }, next);
+ },
+ ], function (err, notification) {
+ if (err) {
+ return winston.error(err);
}
- var path = meta.config.welcomeLink ? meta.config.welcomeLink : '#';
+ if (notification) {
+ notifications.push(notification, followers);
+ }
+ });
+};
- notifications.create({
- bodyShort: meta.config.welcomeNotification,
- path: path,
- nid: 'welcome_' + uid,
- }, function (err, notification) {
- if (err || !notification) {
- return callback(err);
- }
+UserNotifications.sendWelcomeNotification = function (uid, callback) {
+ callback = callback || function () {};
+ if (!meta.config.welcomeNotification) {
+ return callback();
+ }
- notifications.push(notification, [uid], callback);
- });
- };
-
- 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) {
- if (!err && notification) {
- notifications.push(notification, uid);
- }
- });
- };
-
- UserNotifications.pushCount = function (uid) {
- var websockets = require('./../socket.io');
- UserNotifications.getUnreadCount(uid, function (err, count) {
- if (err) {
- return winston.error(err.stack);
+ var path = meta.config.welcomeLink ? meta.config.welcomeLink : '#';
+
+ async.waterfall([
+ function (next) {
+ notifications.create({
+ bodyShort: meta.config.welcomeNotification,
+ path: path,
+ nid: 'welcome_' + uid,
+ }, next);
+ },
+ function (notification, next) {
+ if (!notification) {
+ return next();
}
+ notifications.push(notification, [uid], next);
+ },
+ ], callback);
+};
+
+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) {
+ if (!err && notification) {
+ notifications.push(notification, uid);
+ }
+ });
+};
+
+UserNotifications.pushCount = function (uid) {
+ var websockets = require('./../socket.io');
+ UserNotifications.getUnreadCount(uid, function (err, count) {
+ if (err) {
+ return winston.error(err.stack);
+ }
- websockets.in('uid_' + uid).emit('event:notifications.updateCount', count);
- });
- };
-}(exports));
+ websockets.in('uid_' + uid).emit('event:notifications.updateCount', count);
+ });
+};
diff --git a/src/user/search.js b/src/user/search.js
index 4583e0e28f..967c32c32b 100644
--- a/src/user/search.js
+++ b/src/user/search.js
@@ -68,16 +68,17 @@ 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) {
- if (err) {
- return callback(err);
- }
-
- var uids = data.map(function (data) {
- return data.split(':')[1];
- });
- callback(null, uids);
- });
+ async.waterfall([
+ function (next) {
+ db.getSortedSetRangeByLex(searchBy + ':sorted', min, max, 0, hardCap, next);
+ },
+ function (data, next) {
+ var uids = data.map(function (data) {
+ return data.split(':')[1];
+ });
+ next(null, uids);
+ },
+ ], callback);
}
function filterAndSortUids(uids, data, callback) {
@@ -94,37 +95,38 @@ module.exports = function (User) {
fields.push('flags');
}
- User.getUsersFields(uids, fields, function (err, userData) {
- if (err) {
- return callback(err);
- }
-
- if (data.onlineOnly) {
- userData = userData.filter(function (user) {
- return user && user.status !== 'offline' && (Date.now() - parseInt(user.lastonline, 10) < 300000);
- });
- }
+ async.waterfall([
+ function (next) {
+ User.getUsersFields(uids, fields, next);
+ },
+ function (userData, next) {
+ if (data.onlineOnly) {
+ 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) {
- return user && user.banned;
- });
- }
+ if (data.bannedOnly) {
+ userData = userData.filter(function (user) {
+ return user && parseInt(user.banned, 10) === 1;
+ });
+ }
- if (data.flaggedOnly) {
- userData = userData.filter(function (user) {
- return user && parseInt(user.flags, 10) > 0;
- });
- }
+ if (data.flaggedOnly) {
+ userData = userData.filter(function (user) {
+ return user && parseInt(user.flags, 10) > 0;
+ });
+ }
- sortUsers(userData, sortBy);
+ sortUsers(userData, sortBy);
- uids = userData.map(function (user) {
- return user && user.uid;
- });
+ uids = userData.map(function (user) {
+ return user && user.uid;
+ });
- callback(null, uids);
- });
+ next(null, uids);
+ },
+ ], callback);
}
function sortUsers(userData, sortBy) {
diff --git a/src/user/settings.js b/src/user/settings.js
index de0da0641d..cf65c37545 100644
--- a/src/user/settings.js
+++ b/src/user/settings.js
@@ -8,6 +8,7 @@ var meta = require('../meta');
var db = require('../database');
var plugins = require('../plugins');
+var pubsub = require('../pubsub');
var LRU = require('lru-cache');
var cache = LRU({
@@ -19,6 +20,10 @@ var cache = LRU({
module.exports = function (User) {
User.settingsCache = cache;
+ pubsub.on('user:settings:cache:del', function (uid) {
+ cache.del('user:' + uid + ':settings');
+ });
+
User.getSettings = function (uid, callback) {
if (!parseInt(uid, 10)) {
return onSettingsLoaded(0, {}, callback);
@@ -178,6 +183,7 @@ module.exports = function (User) {
},
function (next) {
cache.del('user:' + uid + ':settings');
+ pubsub.publish('user:settings:cache:del', uid);
User.getSettings(uid, next);
},
], callback);
diff --git a/test/authentication.js b/test/authentication.js
index 7b49f69efa..1d3651e183 100644
--- a/test/authentication.js
+++ b/test/authentication.js
@@ -7,8 +7,64 @@ var request = require('request');
var db = require('./mocks/databasemock');
var user = require('../src/user');
+var meta = require('../src/meta');
describe('authentication', function () {
+ function loginUser(username, password, callback) {
+ var jar = request.jar();
+ request({
+ url: nconf.get('url') + '/api/config',
+ json: true,
+ jar: jar,
+ }, function (err, response, body) {
+ if (err) {
+ return callback(err);
+ }
+
+ request.post(nconf.get('url') + '/login', {
+ form: {
+ username: username,
+ password: password,
+ },
+ json: true,
+ jar: jar,
+ headers: {
+ 'x-csrf-token': body.csrf_token,
+ },
+ }, function (err, response, body) {
+ callback(err, response, body, jar);
+ });
+ });
+ }
+
+ function registerUser(email, username, password, callback) {
+ var jar = request.jar();
+ request({
+ url: nconf.get('url') + '/api/config',
+ json: true,
+ jar: jar,
+ }, function (err, response, body) {
+ if (err) {
+ return callback(err);
+ }
+
+ request.post(nconf.get('url') + '/register', {
+ form: {
+ email: email,
+ username: username,
+ password: password,
+ },
+ json: true,
+ jar: jar,
+ headers: {
+ 'x-csrf-token': body.csrf_token,
+ },
+ }, function (err, response, body) {
+ callback(err, response, body, jar);
+ });
+ });
+ }
+
var jar = request.jar();
var regularUid;
before(function (done) {
@@ -89,43 +145,24 @@ describe('authentication', function () {
});
it('should login a user', function (done) {
- var jar = request.jar();
- request({
- url: nconf.get('url') + '/api/config',
- json: true,
- jar: jar,
- }, function (err, response, body) {
+ loginUser('regular', 'regularpwd', function (err, response, body, jar) {
assert.ifError(err);
+ assert(body);
- request.post(nconf.get('url') + '/login', {
- form: {
- username: 'regular',
- password: 'regularpwd',
- },
+ request({
+ url: nconf.get('url') + '/api/me',
json: true,
jar: jar,
- headers: {
- 'x-csrf-token': body.csrf_token,
- },
}, function (err, response, body) {
assert.ifError(err);
assert(body);
-
- request({
- url: nconf.get('url') + '/api/me',
- json: true,
- jar: jar,
- }, function (err, response, body) {
+ assert.equal(body.username, 'regular');
+ assert.equal(body.email, 'regular@nodebb.org');
+ db.getObject('uid:' + regularUid + ':sessionUUID:sessionId', function (err, sessions) {
assert.ifError(err);
- assert(body);
- assert.equal(body.username, 'regular');
- assert.equal(body.email, 'regular@nodebb.org');
- db.getObject('uid:' + regularUid + ':sessionUUID:sessionId', function (err, sessions) {
- assert.ifError(err);
- assert(sessions);
- assert(Object.keys(sessions).length > 0);
- done();
- });
+ assert(sessions);
+ assert(Object.keys(sessions).length > 0);
+ done();
});
});
});
@@ -147,6 +184,119 @@ describe('authentication', function () {
});
});
+ it('should fail to login if user does not exist', function (done) {
+ loginUser('doesnotexist', 'nopassword', function (err, response, body) {
+ assert.ifError(err);
+ assert.equal(response.statusCode, 403);
+ assert.equal(body, '[[error:invalid-login-credentials]]');
+ done();
+ });
+ });
+
+ it('should fail to login if username is empty', function (done) {
+ loginUser('', 'some password', function (err, response, body) {
+ assert.ifError(err);
+ assert.equal(response.statusCode, 403);
+ assert.equal(body, '[[error:invalid-username-or-password]]');
+ done();
+ });
+ });
+
+ it('should fail to login if password is empty', function (done) {
+ loginUser('someuser', '', function (err, response, body) {
+ assert.ifError(err);
+ assert.equal(response.statusCode, 403);
+ assert.equal(body, '[[error:invalid-username-or-password]]');
+ done();
+ });
+ });
+
+ it('should fail to login if username and password are empty', function (done) {
+ loginUser('', '', function (err, response, body) {
+ assert.ifError(err);
+ assert.equal(response.statusCode, 403);
+ assert.equal(body, '[[error:invalid-username-or-password]]');
+ done();
+ });
+ });
+
+ it('should fail to login if password is longer than 4096', function (done) {
+ var longPassword;
+ for (var i = 0; i < 5000; i++) {
+ longPassword += 'a';
+ }
+ loginUser('someuser', longPassword, function (err, response, body) {
+ assert.ifError(err);
+ assert.equal(response.statusCode, 403);
+ assert.equal(body, '[[error:password-too-long]]');
+ done();
+ });
+ });
+
+
+ it('should fail to login if local login is disabled', function (done) {
+ meta.config.allowLocalLogin = 0;
+ loginUser('someuser', 'somepass', function (err, response, body) {
+ meta.config.allowLocalLogin = 1;
+ assert.ifError(err);
+ assert.equal(response.statusCode, 403);
+ assert.equal(body, '[[error:local-login-disabled]]');
+ done();
+ });
+ });
+
+ it('should fail to register if registraton is disabled', function (done) {
+ meta.config.registrationType = 'disabled';
+ registerUser('some@user.com', 'someuser', 'somepassword', function (err, response, body) {
+ assert.ifError(err);
+ assert.equal(response.statusCode, 403);
+ assert.equal(body, 'Forbidden');
+ done();
+ });
+ });
+
+ it('should return error if invitation is not valid', function (done) {
+ meta.config.registrationType = 'invite-only';
+ registerUser('some@user.com', 'someuser', 'somepassword', function (err, response, body) {
+ meta.config.registrationType = 'normal';
+ assert.ifError(err);
+ assert.equal(response.statusCode, 400);
+ assert.equal(body, '[[error:invalid-data]]');
+ done();
+ });
+ });
+
+ it('should fail to register if email is falsy', function (done) {
+ registerUser('', 'someuser', 'somepassword', function (err, response, body) {
+ assert.ifError(err);
+ assert.equal(response.statusCode, 400);
+ assert.equal(body, '[[error:invalid-email]]');
+ done();
+ });
+ });
+
+ it('should fail to register if username is falsy or too short', function (done) {
+ registerUser('some@user.com', '', 'somepassword', function (err, response, body) {
+ assert.ifError(err);
+ assert.equal(response.statusCode, 400);
+ assert.equal(body, '[[error:username-too-short]]');
+ registerUser('some@user.com', 'a', 'somepassword', function (err, response, body) {
+ assert.ifError(err);
+ assert.equal(response.statusCode, 400);
+ assert.equal(body, '[[error:username-too-short]]');
+ done();
+ });
+ });
+ });
+
+ it('should fail to register if username is too long', function (done) {
+ registerUser('some@user.com', 'thisisareallylongusername', '123456', function (err, response, body) {
+ assert.ifError(err);
+ assert.equal(response.statusCode, 400);
+ assert.equal(body, '[[error:username-too-long]]');
+ done();
+ });
+ });
after(function (done) {
db.emptydb(done);
diff --git a/test/build.js b/test/build.js
index 1f73f5746b..ceb33962be 100644
--- a/test/build.js
+++ b/test/build.js
@@ -1,19 +1,199 @@
'use strict';
+var string = require('string');
+var path = require('path');
+var fs = require('fs');
var assert = require('assert');
+var mkdirp = require('mkdirp');
+var rimraf = require('rimraf');
+var async = require('async');
var db = require('./mocks/databasemock');
+var file = require('../src/file');
-describe('Build', function () {
+describe('minifier', function () {
before(function (done) {
- db.setupMockDefaults(done);
+ mkdirp(path.join(__dirname, '../build/test'), done);
});
- it('should build all assets', function (done) {
- this.timeout(50000);
- var build = require('../src/meta/build');
- build.buildAll(function (err) {
+ var minifier = require('../src/meta/minifier');
+ var scripts = [
+ path.resolve(__dirname, './files/1.js'),
+ path.resolve(__dirname, './files/2.js'),
+ ];
+ it('.js.bundle() should concat scripts', function (done) {
+ minifier.js.bundle(scripts, false, false, function (err, bundle) {
assert.ifError(err);
+ assert.strictEqual(
+ bundle.code,
+ '(function (window, document) {' +
+ '\n\twindow.doStuff = function () {' +
+ '\n\t\tdocument.body.innerHTML = \'Stuff has been done\';' +
+ '\n\t};' +
+ '\n})(window, document);' +
+ '\n' +
+ '\n;function foo(name, age) {' +
+ '\n\treturn \'The person known as "\' + name + \'" is \' + age + \' years old\';' +
+ '\n}' +
+ '\n'
+ );
+ done();
+ });
+ });
+
+ it('.js.bundle() should minify scripts', function (done) {
+ minifier.js.bundle(scripts, true, false, function (err, bundle) {
+ assert.ifError(err);
+ assert.strictEqual(
+ bundle.code,
+ '(function(n,o){n.doStuff=function(){o.body.innerHTML="Stuff has been done"}})(window,document);function foo(n,o){return\'The person known as "\'+n+\'" is \'+o+" years old"}'
+ );
+ done();
+ });
+ });
+
+ it('.js.minifyBatch() should minify each script', function (done) {
+ var s = scripts.map(function (script) {
+ return {
+ srcPath: script,
+ destPath: path.resolve(__dirname, '../build/test', path.basename(script)),
+ };
+ });
+ minifier.js.minifyBatch(s, false, function (err) {
+ assert.ifError(err);
+
+ assert(file.existsSync(s[0].destPath));
+ assert(file.existsSync(s[1].destPath));
+
+ fs.readFile(s[0].destPath, function (err, buffer) {
+ assert.ifError(err);
+ assert.strictEqual(
+ buffer.toString(),
+ '(function(n,o){n.doStuff=function(){o.body.innerHTML="Stuff has been done"}})(window,document);'
+ );
+ done();
+ });
+ });
+ });
+
+ var styles = [
+ '@import (inline) "./1.css";',
+ '@import "./2.less";',
+ ].join('\n');
+ var paths = [
+ path.resolve(__dirname, './files'),
+ ];
+ it('.css.bundle() should concat styles', function (done) {
+ minifier.css.bundle(styles, paths, false, false, function (err, bundle) {
+ assert.ifError(err);
+ assert.strictEqual(bundle.code, '.help { margin: 10px; } .yellow { background: yellow; }\n.help {\n display: block;\n}\n.help .blue {\n background: blue;\n}\n');
+ done();
+ });
+ });
+
+ it('.css.bundle() should minify styles', function (done) {
+ minifier.css.bundle(styles, paths, true, false, function (err, bundle) {
+ assert.ifError(err);
+ assert.strictEqual(bundle.code, '.help{margin:10px;display:block}.yellow{background:#ff0}.help .blue{background:#00f}');
+ done();
+ });
+ });
+});
+
+describe('Build', function (done) {
+ var build = require('../src/meta/build');
+
+ before(function (done) {
+ async.parallel([
+ async.apply(rimraf, path.join(__dirname, '../build/public')),
+ db.setupMockDefaults,
+ ], done);
+ });
+
+ it('should build plugin static dirs', function (done) {
+ build.build(['plugin static dirs'], function (err) {
+ assert.ifError(err);
+ assert(file.existsSync(path.join(__dirname, '../build/public/plugins/nodebb-plugin-dbsearch/dbsearch')));
+ done();
+ });
+ });
+
+ it('should build requirejs modules', function (done) {
+ build.build(['requirejs modules'], function (err) {
+ assert.ifError(err);
+ var filename = path.join(__dirname, '../build/public/src/modules/Chart.js');
+ assert(file.existsSync(filename));
+ assert(fs.readFileSync(filename).toString().startsWith('/*!\n * Chart.js'));
+ done();
+ });
+ });
+
+ it('should build client js bundle', function (done) {
+ build.build(['client js bundle'], function (err) {
+ assert.ifError(err);
+ var filename = path.join(__dirname, '../build/public/nodebb.min.js');
+ assert(file.existsSync(filename));
+ assert(fs.readFileSync(filename).length > 1000);
+ done();
+ });
+ });
+
+ it('should build admin js bundle', function (done) {
+ build.build(['admin js bundle'], function (err) {
+ assert.ifError(err);
+ var filename = path.join(__dirname, '../build/public/acp.min.js');
+ assert(file.existsSync(filename));
+ assert(fs.readFileSync(filename).length > 1000);
+ done();
+ });
+ });
+
+ it('should build client side styles', function (done) {
+ build.build(['client side styles'], function (err) {
+ assert.ifError(err);
+ var filename = path.join(__dirname, '../build/public/stylesheet.css');
+ assert(file.existsSync(filename));
+ assert(fs.readFileSync(filename).toString().startsWith('/*! normalize.css'));
+ done();
+ });
+ });
+
+ it('should build admin control panel styles', function (done) {
+ build.build(['admin control panel styles'], function (err) {
+ assert.ifError(err);
+ var filename = path.join(__dirname, '../build/public/admin.css');
+ assert(file.existsSync(filename));
+ assert(fs.readFileSync(filename).toString().startsWith('@charset "UTF-8";'));
+ done();
+ });
+ });
+
+ it('should build templates', function (done) {
+ build.build(['templates'], function (err) {
+ assert.ifError(err);
+ var filename = path.join(__dirname, '../build/public/templates/admin/header.tpl');
+ assert(file.existsSync(filename));
+ assert(fs.readFileSync(filename).toString().startsWith(''));
+ done();
+ });
+ });
+
+ it('should build languages', function (done) {
+ build.build(['languages'], function (err) {
+ assert.ifError(err);
+ var filename = path.join(__dirname, '../build/public/language/en-GB/global.json');
+ assert(file.existsSync(filename));
+ var global = fs.readFileSync(filename).toString();
+ assert.strictEqual(JSON.parse(global).home, 'Home');
+ done();
+ });
+ });
+
+ it('should build sounds', function (done) {
+ build.build(['sounds'], function (err) {
+ assert.ifError(err);
+ var filename = path.join(__dirname, '../build/public/sounds/fileMap.json');
+ assert(file.existsSync(filename));
done();
});
});
diff --git a/test/database.js b/test/database.js
index f55bf78edb..327d8095cf 100644
--- a/test/database.js
+++ b/test/database.js
@@ -2,6 +2,7 @@
var assert = require('assert');
+var nconf = require('nconf');
var db = require('./mocks/databasemock');
@@ -12,14 +13,46 @@ describe('Test database', function () {
});
});
- it('should return info about database', function (done) {
- db.info(db.client, function (err, info) {
- assert.ifError(err);
- assert(info);
- done();
+ describe('info', function () {
+ it('should return info about database', function (done) {
+ db.info(db.client, function (err, info) {
+ assert.ifError(err);
+ assert(info);
+ done();
+ });
+ });
+
+ it('should not error and return null if client is falsy', function (done) {
+ db.info(null, function (err, info) {
+ assert.ifError(err);
+ assert.equal(info, null);
+ done();
+ });
+ });
+ });
+
+ describe('checkCompatibility', function () {
+ it('should not throw', function (done) {
+ db.checkCompatibility(done);
+ });
+
+ it('should return error with a too low version', function (done) {
+ var dbName = nconf.get('database');
+ if (dbName === 'redis') {
+ db.checkCompatibilityVersion('2.4.0', function (err) {
+ assert.equal(err.message, 'Your Redis version is not new enough to support NodeBB, please upgrade Redis to v2.8.9 or higher.');
+ done();
+ });
+ } else if (dbName === 'mongo') {
+ db.checkCompatibilityVersion('1.8.0', function (err) {
+ assert.equal(err.message, 'The `mongodb` package is out-of-date, please run `./nodebb setup` again.');
+ done();
+ });
+ }
});
});
+
require('./database/keys');
require('./database/list');
require('./database/sets');
diff --git a/test/database/hash.js b/test/database/hash.js
index 7f9dc305d0..cffa761229 100644
--- a/test/database/hash.js
+++ b/test/database/hash.js
@@ -190,6 +190,15 @@ describe('Hash methods', function () {
done();
});
});
+
+ it('should return undefined for all fields if object does not exist', function (done) {
+ db.getObjectsFields(['doesnotexist1', 'doesnotexist2'], ['name', 'age'], function (err, data) {
+ assert.ifError(err);
+ assert.equal(data.name, null);
+ assert.equal(data.age, null);
+ done();
+ });
+ });
});
describe('getObjectKeys()', function () {
diff --git a/test/database/keys.js b/test/database/keys.js
index 157cc2ca97..afbf3c947c 100644
--- a/test/database/keys.js
+++ b/test/database/keys.js
@@ -102,6 +102,37 @@ describe('Key methods', function () {
});
});
+ it('should delete all sorted set elements', function (done) {
+ async.parallel([
+ function (next) {
+ db.sortedSetAdd('deletezset', 1, 'value1', next);
+ },
+ function (next) {
+ db.sortedSetAdd('deletezset', 2, 'value2', next);
+ },
+ ], function (err) {
+ if (err) {
+ return done(err);
+ }
+ db.delete('deletezset', function (err) {
+ assert.ifError(err);
+ async.parallel({
+ key1exists: function (next) {
+ db.isSortedSetMember('deletezset', 'value1', next);
+ },
+ key2exists: function (next) {
+ db.isSortedSetMember('deletezset', 'value2', next);
+ },
+ }, function (err, results) {
+ assert.equal(err, null);
+ assert.equal(results.key1exists, false);
+ assert.equal(results.key2exists, false);
+ done();
+ });
+ });
+ });
+ });
+
describe('increment', function () {
it('should initialize key to 1', function (done) {
db.increment('keyToIncrement', function (err, value) {
diff --git a/test/database/list.js b/test/database/list.js
index 8475ad2f52..7d00df3e8d 100644
--- a/test/database/list.js
+++ b/test/database/list.js
@@ -9,11 +9,18 @@ describe('List methods', function () {
describe('listAppend()', function () {
it('should append to a list', function (done) {
db.listAppend('testList1', 5, function (err) {
- assert.equal(err, null);
+ assert.ifError(err);
assert.equal(arguments.length, 1);
done();
});
});
+
+ it('should not add anyhing if key is falsy', function (done) {
+ db.listAppend(null, 3, function (err) {
+ assert.ifError(err);
+ done();
+ });
+ });
});
describe('listPrepend()', function () {
@@ -38,6 +45,13 @@ describe('List methods', function () {
done();
});
});
+
+ it('should not add anyhing if key is falsy', function (done) {
+ db.listPrepend(null, 3, function (err) {
+ assert.ifError(err);
+ done();
+ });
+ });
});
describe('getListRange()', function () {
@@ -83,6 +97,14 @@ describe('List methods', function () {
done();
});
});
+
+ it('should not get anything if key is falsy', function (done) {
+ db.getListRange(null, 0, -1, function (err, data) {
+ assert.ifError(err);
+ assert.equal(data, undefined);
+ done();
+ });
+ });
});
describe('listRemoveLast()', function () {
@@ -105,6 +127,13 @@ describe('List methods', function () {
done();
});
});
+
+ it('should not remove anyhing if key is falsy', function (done) {
+ db.listRemoveLast(null, function (err) {
+ assert.ifError(err);
+ done();
+ });
+ });
});
describe('listRemoveAll()', function () {
@@ -132,6 +161,13 @@ describe('List methods', function () {
});
});
});
+
+ it('should not remove anyhing if key is falsy', function (done) {
+ db.listRemoveAll(null, 3, function (err) {
+ assert.ifError(err);
+ done();
+ });
+ });
});
describe('listTrim()', function () {
@@ -156,6 +192,13 @@ describe('List methods', function () {
});
});
});
+
+ it('should not add anyhing if key is falsy', function (done) {
+ db.listTrim(null, 0, 3, function (err) {
+ assert.ifError(err);
+ done();
+ });
+ });
});
diff --git a/test/database/sorted.js b/test/database/sorted.js
index b90e9cf71f..9e0178d4ed 100644
--- a/test/database/sorted.js
+++ b/test/database/sorted.js
@@ -303,6 +303,7 @@ describe('Sorted Set methods', function () {
assert.equal(err, null);
assert.equal(arguments.length, 2);
assert.equal(!!score, false);
+ assert.strictEqual(score, null);
done();
});
});
@@ -312,6 +313,7 @@ describe('Sorted Set methods', function () {
assert.equal(err, null);
assert.equal(arguments.length, 2);
assert.equal(!!score, false);
+ assert.strictEqual(score, null);
done();
});
});
diff --git a/test/feeds.js b/test/feeds.js
new file mode 100644
index 0000000000..4fd0bbd90e
--- /dev/null
+++ b/test/feeds.js
@@ -0,0 +1,120 @@
+'use strict';
+
+var assert = require('assert');
+var async = require('async');
+var request = require('request');
+var nconf = require('nconf');
+
+var db = require('./mocks/databasemock');
+var topics = require('../src/topics');
+var categories = require('../src/categories');
+var groups = require('../src/groups');
+var user = require('../src/user');
+var meta = require('../src/meta');
+var privileges = require('../src/privileges');
+
+describe('feeds', function () {
+ var tid;
+ var pid;
+ var fooUid;
+ var cid;
+ before(function (done) {
+ groups.resetCache();
+ meta.config['feeds:disableRSS'] = 1;
+ async.series({
+ category: function (next) {
+ categories.create({
+ name: 'Test Category',
+ description: 'Test category created by testing script',
+ }, next);
+ },
+ user: function (next) {
+ user.create({ username: 'foo', password: 'barbar', email: 'foo@test.com' }, next);
+ },
+ }, function (err, results) {
+ if (err) {
+ return done(err);
+ }
+ cid = results.category.cid;
+ fooUid = results.user;
+
+ topics.post({ uid: results.user, title: 'test topic title', content: 'test topic content', cid: results.category.cid }, function (err, result) {
+ tid = result.topicData.tid;
+ pid = result.postData.pid;
+ done(err);
+ });
+ });
+ });
+
+
+ it('should 404', function (done) {
+ var feedUrls = [
+ nconf.get('url') + '/topic/' + tid + '.rss',
+ nconf.get('url') + '/category/' + cid + '.rss',
+ nconf.get('url') + '/recent.rss',
+ nconf.get('url') + '/popular.rss',
+ nconf.get('url') + '/popular/day.rss',
+ nconf.get('url') + '/recentposts.rss',
+ nconf.get('url') + '/category/' + cid + '/recentposts.rss',
+ nconf.get('url') + '/user/foo/topics.rss',
+ nconf.get('url') + '/tags/nodebb.rss',
+ ];
+ async.eachSeries(feedUrls, function (url, next) {
+ request(url, function (err, res) {
+ assert.ifError(err);
+ assert.equal(res.statusCode, 404);
+ next();
+ });
+ }, function (err) {
+ assert.ifError(err);
+ meta.config['feeds:disableRSS'] = 0;
+ done();
+ });
+ });
+
+ it('should 404 if topic does not exist', function (done) {
+ request(nconf.get('url') + '/topic/' + 1000 + '.rss', function (err, res) {
+ assert.ifError(err);
+ assert.equal(res.statusCode, 404);
+ done();
+ });
+ });
+
+ it('should redirect if we do not have read privilege', function (done) {
+ privileges.categories.rescind(['topics:read'], cid, 'guests', function (err) {
+ assert.ifError(err);
+ request(nconf.get('url') + '/topic/' + tid + '.rss', function (err, res, body) {
+ assert.ifError(err);
+ assert.equal(res.statusCode, 200);
+ assert(body);
+ assert(body.indexOf('Login to your account') !== -1);
+ privileges.categories.give(['topics:read'], cid, 'guests', done);
+ });
+ });
+ });
+
+ it('should 404 if user is not found', function (done) {
+ request(nconf.get('url') + '/user/doesnotexist/topics.rss', function (err, res) {
+ assert.ifError(err);
+ assert.equal(res.statusCode, 404);
+ done();
+ });
+ });
+
+ it('should redirect if we do not have read privilege', function (done) {
+ privileges.categories.rescind(['read'], cid, 'guests', function (err) {
+ assert.ifError(err);
+ request(nconf.get('url') + '/category/' + cid + '.rss', function (err, res, body) {
+ assert.ifError(err);
+ assert.equal(res.statusCode, 200);
+ assert(body);
+ assert(body.indexOf('Login to your account') !== -1);
+ privileges.categories.give(['read'], cid, 'guests', done);
+ });
+ });
+ });
+
+ after(function (done) {
+ db.emptydb(done);
+ });
+});
diff --git a/test/files/1.css b/test/files/1.css
new file mode 100644
index 0000000000..840cf64b36
--- /dev/null
+++ b/test/files/1.css
@@ -0,0 +1 @@
+.help { margin: 10px; } .yellow { background: yellow; }
\ No newline at end of file
diff --git a/test/files/1.js b/test/files/1.js
new file mode 100644
index 0000000000..b20055f8ee
--- /dev/null
+++ b/test/files/1.js
@@ -0,0 +1,5 @@
+(function (window, document) {
+ window.doStuff = function () {
+ document.body.innerHTML = 'Stuff has been done';
+ };
+})(window, document);
diff --git a/test/files/2.js b/test/files/2.js
new file mode 100644
index 0000000000..9369213316
--- /dev/null
+++ b/test/files/2.js
@@ -0,0 +1,3 @@
+function foo(name, age) {
+ return 'The person known as "' + name + '" is ' + age + ' years old';
+}
diff --git a/test/files/2.less b/test/files/2.less
new file mode 100644
index 0000000000..cdd5d5b5f2
--- /dev/null
+++ b/test/files/2.less
@@ -0,0 +1 @@
+.help { display: block; .blue { background: blue; } }
\ No newline at end of file
diff --git a/test/messaging.js b/test/messaging.js
index 436fd78a87..5a9b0ac4a3 100644
--- a/test/messaging.js
+++ b/test/messaging.js
@@ -11,7 +11,7 @@ var User = require('../src/user');
var Groups = require('../src/groups');
var Messaging = require('../src/messaging');
var helpers = require('./helpers');
-
+var socketModules = require('../src/socket.io/modules');
describe('Messaging Library', function () {
var fooUid;
@@ -55,7 +55,10 @@ describe('Messaging Library', function () {
assert.ifError(err);
Messaging.canMessageUser(herpUid, bazUid, function (err) {
assert.strictEqual(err.message, '[[error:chat-restricted]]');
- done();
+ socketModules.chats.addUserToRoom({ uid: herpUid }, { roomId: 1, username: 'baz' }, function (err) {
+ assert.equal(err.message, '[[error:chat-restricted]]');
+ done();
+ });
});
});
});
@@ -78,23 +81,93 @@ describe('Messaging Library', function () {
});
describe('rooms', function () {
- var socketModules = require('../src/socket.io/modules');
+ it('should fail to create a new chat room with invalid data', function (done) {
+ socketModules.chats.newRoom({ uid: fooUid }, null, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ done();
+ });
+ });
+
+ it('should return rate limit error on second try', function (done) {
+ var socketMock = { uid: fooUid };
+ socketModules.chats.newRoom(socketMock, { touid: bazUid }, function (err) {
+ assert.ifError(err);
+ socketModules.chats.newRoom(socketMock, { touid: bazUid }, function (err) {
+ assert.equal(err.message, '[[error:too-many-messages]]');
+ done();
+ });
+ });
+ });
+
it('should create a new chat room', function (done) {
socketModules.chats.newRoom({ uid: fooUid }, { touid: bazUid }, function (err, _roomId) {
roomId = _roomId;
assert.ifError(err);
assert(roomId);
- done();
+ socketModules.chats.canMessage({ uid: fooUid }, _roomId, function (err) {
+ assert.ifError(err);
+ done();
+ });
+ });
+ });
+
+ it('should fail to add user to room with invalid data', function (done) {
+ socketModules.chats.addUserToRoom({ uid: fooUid }, null, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: null }, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: null }, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ done();
+ });
+ });
});
});
it('should add a user to room', function (done) {
socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: 'herp' }, function (err) {
assert.ifError(err);
+ Messaging.isUserInRoom(herpUid, roomId, function (err, isInRoom) {
+ assert.ifError(err);
+ assert(isInRoom);
+ done();
+ });
+ });
+ });
+
+ it('should fail to add users to room if max is reached', function (done) {
+ meta.config.maximumUsersInChatRoom = 2;
+ socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: 'test' }, function (err) {
+ assert.equal(err.message, '[[error:cant-add-more-users-to-chat-room]]');
+ meta.config.maximumUsersInChatRoom = 0;
+ done();
+ });
+ });
+
+ it('should fail to add users to room if user does not exist', function (done) {
+ socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: 'doesnotexist' }, function (err) {
+ assert.equal(err.message, '[[error:no-user]]');
+ done();
+ });
+ });
+
+ it('should fail to add self to room', function (done) {
+ socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: 'foo' }, function (err) {
+ assert.equal(err.message, '[[error:cant-add-self-to-chat-room]]');
done();
});
});
+ it('should fail to leave room with invalid data', function (done) {
+ socketModules.chats.leave({ uid: null }, roomId, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ socketModules.chats.leave({ uid: fooUid }, null, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ done();
+ });
+ });
+ });
+
it('should leave the chat room', function (done) {
socketModules.chats.leave({ uid: bazUid }, roomId, function (err) {
assert.ifError(err);
@@ -106,6 +179,60 @@ describe('Messaging Library', function () {
});
});
+ it('should fail to remove user from room', function (done) {
+ socketModules.chats.removeUserFromRoom({ uid: fooUid }, null, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ socketModules.chats.removeUserFromRoom({ uid: fooUid }, {}, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ done();
+ });
+ });
+ });
+
+ it('should fail to remove user from room if user does not exist', function (done) {
+ socketModules.chats.removeUserFromRoom({ uid: fooUid }, { roomId: roomId, username: 'doesnotexist' }, function (err) {
+ assert.equal(err.message, '[[error:no-user]]');
+ done();
+ });
+ });
+
+ it('should remove user from room', function (done) {
+ socketModules.chats.newRoom({ uid: fooUid }, { touid: herpUid }, function (err, roomId) {
+ assert.ifError(err);
+ Messaging.isUserInRoom(herpUid, roomId, function (err, isInRoom) {
+ assert.ifError(err);
+ assert(isInRoom);
+ socketModules.chats.removeUserFromRoom({ uid: fooUid }, { roomId: roomId, username: 'herp' }, function (err) {
+ assert.equal(err.message, '[[error:cant-remove-last-user]]');
+ socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: 'baz' }, function (err) {
+ assert.ifError(err);
+ socketModules.chats.removeUserFromRoom({ uid: fooUid }, { roomId: roomId, username: 'herp' }, function (err) {
+ assert.ifError(err);
+ Messaging.isUserInRoom(herpUid, roomId, function (err, isInRoom) {
+ assert.ifError(err);
+ assert(!isInRoom);
+ done();
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+
+ it('should fail to send a message to room with invalid data', function (done) {
+ socketModules.chats.send({ uid: fooUid }, null, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ socketModules.chats.send({ uid: fooUid }, { roomId: null }, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ socketModules.chats.send({ uid: null }, { roomId: 1 }, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ done();
+ });
+ });
+ });
+ });
+
it('should send a message to a room', function (done) {
socketModules.chats.send({ uid: fooUid }, { roomId: roomId, message: 'first chat message' }, function (err, messageData) {
assert.ifError(err);
@@ -116,11 +243,39 @@ describe('Messaging Library', function () {
socketModules.chats.getRaw({ uid: fooUid }, { roomId: roomId, mid: messageData.mid }, function (err, raw) {
assert.ifError(err);
assert.equal(raw, 'first chat message');
+ setTimeout(done, 300);
+ });
+ });
+ });
+
+ it('should fail to send second message due to rate limit', function (done) {
+ var socketMock = { uid: fooUid };
+ socketModules.chats.send(socketMock, { roomId: roomId, message: 'first chat message' }, function (err) {
+ assert.ifError(err);
+ socketModules.chats.send(socketMock, { roomId: roomId, message: 'first chat message' }, function (err) {
+ assert.equal(err.message, '[[error:too-many-messages]]');
+ done();
+ });
+ });
+ });
+
+ it('should return invalid-data error', function (done) {
+ socketModules.chats.getRaw({ uid: fooUid }, null, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ socketModules.chats.getRaw({ uid: fooUid }, { }, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
});
+ it('should return not in room error', function (done) {
+ socketModules.chats.getRaw({ uid: 0 }, { roomId: roomId, mid: 1 }, function (err) {
+ assert.equal(err.message, '[[error:not-allowed]]');
+ done();
+ });
+ });
+
it('should notify offline users of message', function (done) {
Messaging.notificationSendDelay = 100;
@@ -143,6 +298,22 @@ describe('Messaging Library', function () {
});
});
+ it('should fail to get messages from room with invalid data', function (done) {
+ socketModules.chats.getMessages({ uid: null }, null, function (err, messages) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ socketModules.chats.getMessages({ uid: fooUid }, null, function (err, messages) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ socketModules.chats.getMessages({ uid: fooUid }, { uid: null }, function (err, messages) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ socketModules.chats.getMessages({ uid: fooUid }, { uid: 1, roomId: null }, function (err, messages) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ done();
+ });
+ });
+ });
+ });
+ });
+
it('should get messages from room', function (done) {
socketModules.chats.getMessages({ uid: fooUid }, {
uid: fooUid,
@@ -157,6 +328,23 @@ describe('Messaging Library', function () {
});
});
+ it('should fail to mark read with invalid data', function (done) {
+ socketModules.chats.markRead({ uid: null }, roomId, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ socketModules.chats.markRead({ uid: fooUid }, null, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ done();
+ });
+ });
+ });
+
+ it('should not error if user is not in room', function (done) {
+ socketModules.chats.markRead({ uid: herpUid }, 10, function (err) {
+ assert.ifError(err);
+ done();
+ });
+ });
+
it('should mark room read', function (done) {
socketModules.chats.markRead({ uid: fooUid }, roomId, function (err) {
assert.ifError(err);
@@ -171,10 +359,39 @@ describe('Messaging Library', function () {
});
});
+ it('should fail to rename room with invalid data', function (done) {
+ socketModules.chats.renameRoom({ uid: fooUid }, null, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ socketModules.chats.renameRoom({ uid: fooUid }, { roomId: null }, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ socketModules.chats.renameRoom({ uid: fooUid }, { roomId: roomId, newName: null }, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ done();
+ });
+ });
+ });
+ });
+
it('should rename room', function (done) {
socketModules.chats.renameRoom({ uid: fooUid }, { roomId: roomId, newName: 'new room name' }, function (err) {
assert.ifError(err);
+ done();
+ });
+ });
+
+ it('should fail to load room with invalid-data', function (done) {
+ socketModules.chats.loadRoom({ uid: fooUid }, null, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ socketModules.chats.loadRoom({ uid: fooUid }, { roomId: null }, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ done();
+ });
+ });
+ });
+ it('should fail to load room if user is not in', function (done) {
+ socketModules.chats.loadRoom({ uid: 0 }, { roomId: roomId }, function (err) {
+ assert.equal(err.message, '[[error:not-allowed]]');
done();
});
});
@@ -198,6 +415,45 @@ describe('Messaging Library', function () {
});
});
});
+
+ it('should fail to load recent chats with invalid data', function (done) {
+ socketModules.chats.getRecentChats({ uid: fooUid }, null, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ socketModules.chats.getRecentChats({ uid: fooUid }, { after: null }, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ socketModules.chats.getRecentChats({ uid: fooUid }, { after: 0, uid: null }, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ done();
+ });
+ });
+ });
+ });
+
+ it('should load recent chats of user', function (done) {
+ socketModules.chats.getRecentChats({ uid: fooUid }, { after: 0, uid: fooUid }, function (err, data) {
+ assert.ifError(err);
+ assert(Array.isArray(data.rooms));
+ done();
+ });
+ });
+
+ it('should fail to check if user has private chat with invalid data', function (done) {
+ socketModules.chats.hasPrivateChat({ uid: null }, null, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ socketModules.chats.hasPrivateChat({ uid: fooUid }, null, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ done();
+ });
+ });
+ });
+
+ it('should check if user has private chat with another uid', function (done) {
+ socketModules.chats.hasPrivateChat({ uid: fooUid }, herpUid, function (err, roomId) {
+ assert.ifError(err);
+ assert(roomId);
+ done();
+ });
+ });
});
describe('edit/delete', function () {
@@ -211,6 +467,26 @@ describe('Messaging Library', function () {
});
});
+ it('should fail to edit message with invalid data', function (done) {
+ socketModules.chats.edit({ uid: fooUid }, null, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ socketModules.chats.edit({ uid: fooUid }, { roomId: null }, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ socketModules.chats.edit({ uid: fooUid }, { roomId: 1, message: null }, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ done();
+ });
+ });
+ });
+ });
+
+ it('should fail to edit message if not own message', function (done) {
+ socketModules.chats.edit({ uid: herpUid }, { mid: 5, roomId: roomId, message: 'message edited' }, function (err) {
+ assert.equal(err.message, '[[error:cant-edit-chat-message]]');
+ done();
+ });
+ });
+
it('should edit message', function (done) {
socketModules.chats.edit({ uid: fooUid }, { mid: mid, roomId: roomId, message: 'message edited' }, function (err) {
assert.ifError(err);
@@ -222,6 +498,27 @@ describe('Messaging Library', function () {
});
});
+ it('should fail to delete message with invalid data', function (done) {
+ socketModules.chats.delete({ uid: fooUid }, null, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ socketModules.chats.delete({ uid: fooUid }, { roomId: null }, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ socketModules.chats.delete({ uid: fooUid }, { roomId: 1, messageId: null }, function (err) {
+ assert.equal(err.message, '[[error:invalid-data]]');
+ done();
+ });
+ });
+ });
+ });
+
+ it('should fail to delete message if not owner', function (done) {
+ socketModules.chats.delete({ uid: herpUid }, { messageId: mid, roomId: roomId }, function (err) {
+ assert.equal(err.message, '[[error:cant-delete-chat-message]]');
+ done();
+ });
+ });
+
+
it('should delete message', function (done) {
socketModules.chats.delete({ uid: fooUid }, { messageId: mid, roomId: roomId }, function (err) {
assert.ifError(err);
diff --git a/test/meta.js b/test/meta.js
index 8b32e90070..a92f6c74f6 100644
--- a/test/meta.js
+++ b/test/meta.js
@@ -262,6 +262,21 @@ describe('meta', function () {
});
+ describe('sounds', function () {
+ var socketModules = require('../src/socket.io/modules');
+
+ it('should getUserMap', function (done) {
+ socketModules.sounds.getUserSoundMap({ uid: 1 }, null, function (err, data) {
+ assert.ifError(err);
+ assert(data.hasOwnProperty('chat-incoming'));
+ assert(data.hasOwnProperty('chat-outgoing'));
+ assert(data.hasOwnProperty('notification'));
+ done();
+ });
+ });
+ });
+
+
after(function (done) {
db.emptydb(done);
});
diff --git a/test/mocks/databasemock.js b/test/mocks/databasemock.js
index ca280dda9e..f64c02942a 100644
--- a/test/mocks/databasemock.js
+++ b/test/mocks/databasemock.js
@@ -5,185 +5,182 @@
* ATTENTION: testing db is flushed before every use!
*/
-(function (module) {
- var async = require('async');
- var winston = require('winston');
- var path = require('path');
- var nconf = require('nconf');
- var url = require('url');
- var errorText;
-
-
- nconf.file({ file: path.join(__dirname, '../../config.json') });
- nconf.defaults({
- base_dir: path.join(__dirname, '../..'),
- themes_path: path.join(__dirname, '../../node_modules'),
- upload_path: 'public/uploads',
- views_dir: path.join(__dirname, '../../build/public/templates'),
- relative_path: '',
- });
-
- if (!nconf.get('isCluster')) {
- nconf.set('isPrimary', 'true');
- nconf.set('isCluster', 'false');
- }
-
- var dbType = nconf.get('database');
- var testDbConfig = nconf.get('test_database');
- var productionDbConfig = nconf.get(dbType);
-
- if (!testDbConfig) {
- errorText = 'test_database is not defined';
- winston.info(
- '\n===========================================================\n' +
- 'Please, add parameters for test database in config.json\n' +
- 'For example (redis):\n' +
- '"test_database": {\n' +
- ' "host": "127.0.0.1",\n' +
- ' "port": "6379",\n' +
- ' "password": "",\n' +
- ' "database": "1"\n' +
- '}\n' +
- ' or (mongo):\n' +
- '"test_database": {\n' +
- ' "host": "127.0.0.1",\n' +
- ' "port": "27017",\n' +
- ' "password": "",\n' +
- ' "database": "1\n' +
- '}\n' +
- ' or (mongo) in a replicaset\n' +
- '"test_database": {\n' +
- ' "host": "127.0.0.1,127.0.0.1,127.0.0.1",\n' +
- ' "port": "27017,27018,27019",\n' +
- ' "username": "",\n' +
- ' "password": "",\n' +
- ' "database": "nodebb_test"\n' +
- '}\n' +
- '==========================================================='
- );
- winston.error(errorText);
- throw new Error(errorText);
- }
-
- if (testDbConfig.database === productionDbConfig.database &&
- testDbConfig.host === productionDbConfig.host &&
- testDbConfig.port === productionDbConfig.port) {
- errorText = 'test_database has the same config as production db';
- winston.error(errorText);
- throw new Error(errorText);
- }
-
- nconf.set(dbType, testDbConfig);
-
- winston.info('database config');
- winston.info(dbType);
- winston.info(testDbConfig);
-
- var db = require('../../src/database');
-
- before(function (done) {
- this.timeout(30000);
- async.series([
- function (next) {
- db.init(next);
- },
- function (next) {
- setupMockDefaults(next);
- },
- function (next) {
- db.initSessionStore(next);
- },
- function (next) {
- var meta = require('../../src/meta');
-
- // nconf defaults, if not set in config
- if (!nconf.get('sessionKey')) {
- nconf.set('sessionKey', 'express.sid');
- }
- // Parse out the relative_url and other goodies from the configured URL
- var urlObject = url.parse(nconf.get('url'));
- var relativePath = urlObject.pathname !== '/' ? urlObject.pathname : '';
- nconf.set('base_url', urlObject.protocol + '//' + urlObject.host);
- nconf.set('secure', urlObject.protocol === 'https:');
- nconf.set('use_port', !!urlObject.port);
- nconf.set('relative_path', relativePath);
- nconf.set('port', urlObject.port || nconf.get('port') || nconf.get('PORT') || (nconf.get('PORT_ENV_VAR') ? nconf.get(nconf.get('PORT_ENV_VAR')) : false) || 4567);
- nconf.set('upload_path', path.join(nconf.get('base_dir'), nconf.get('upload_path')));
-
- nconf.set('core_templates_path', path.join(__dirname, '../../src/views'));
- nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-persona/templates'));
- nconf.set('theme_templates_path', meta.config['theme:templates'] ? path.join(nconf.get('themes_path'), meta.config['theme:id'], meta.config['theme:templates']) : nconf.get('base_templates_path'));
- nconf.set('theme_config', path.join(nconf.get('themes_path'), 'nodebb-theme-persona', 'theme.json'));
- nconf.set('bcrypt_rounds', 1);
-
- next();
- },
- function (next) {
- var webserver = require('../../src/webserver');
- var sockets = require('../../src/socket.io');
- sockets.init(webserver.server);
-
- require('../../src/notifications').startJobs();
- require('../../src/user').startJobs();
-
- webserver.listen(next);
- },
- ], done);
- });
-
- function setupMockDefaults(callback) {
- var meta = require('../../src/meta');
-
- async.series([
- function (next) {
- db.emptydb(next);
- },
- function (next) {
- winston.info('test_database flushed');
- setupDefaultConfigs(meta, next);
- },
- function (next) {
- meta.configs.init(next);
- },
- function (next) {
- meta.dependencies.check(next);
- },
- function (next) {
- meta.config.postDelay = 0;
- meta.config.initialPostDelay = 0;
- meta.config.newbiePostDelay = 0;
-
- enableDefaultPlugins(next);
- },
- function (next) {
- meta.themes.set({
- type: 'local',
- id: 'nodebb-theme-persona',
- }, next);
- },
- ], callback);
- }
- db.setupMockDefaults = setupMockDefaults;
-
- function setupDefaultConfigs(meta, next) {
- winston.info('Populating database with default configs, if not already set...\n');
-
- var defaults = require(path.join(nconf.get('base_dir'), 'install/data/defaults.json'));
-
- meta.configs.setOnEmpty(defaults, next);
- }
-
- function enableDefaultPlugins(callback) {
- winston.info('Enabling default plugins\n');
-
- var defaultEnabled = [
- 'nodebb-plugin-dbsearch',
- ];
-
- winston.info('[install/enableDefaultPlugins] activating default plugins', defaultEnabled);
-
- db.sortedSetAdd('plugins:active', [0], defaultEnabled, callback);
- }
-
- module.exports = db;
-}(module));
+var async = require('async');
+var winston = require('winston');
+var path = require('path');
+var nconf = require('nconf');
+var url = require('url');
+var errorText;
+
+
+nconf.file({ file: path.join(__dirname, '../../config.json') });
+nconf.defaults({
+ base_dir: path.join(__dirname, '../..'),
+ themes_path: path.join(__dirname, '../../node_modules'),
+ upload_path: 'public/uploads',
+ views_dir: path.join(__dirname, '../../build/public/templates'),
+ relative_path: '',
+});
+
+if (!nconf.get('isCluster')) {
+ nconf.set('isPrimary', 'true');
+ nconf.set('isCluster', 'false');
+}
+
+var dbType = nconf.get('database');
+var testDbConfig = nconf.get('test_database');
+var productionDbConfig = nconf.get(dbType);
+
+if (!testDbConfig) {
+ errorText = 'test_database is not defined';
+ winston.info(
+ '\n===========================================================\n' +
+ 'Please, add parameters for test database in config.json\n' +
+ 'For example (redis):\n' +
+ '"test_database": {\n' +
+ ' "host": "127.0.0.1",\n' +
+ ' "port": "6379",\n' +
+ ' "password": "",\n' +
+ ' "database": "1"\n' +
+ '}\n' +
+ ' or (mongo):\n' +
+ '"test_database": {\n' +
+ ' "host": "127.0.0.1",\n' +
+ ' "port": "27017",\n' +
+ ' "password": "",\n' +
+ ' "database": "1\n' +
+ '}\n' +
+ ' or (mongo) in a replicaset\n' +
+ '"test_database": {\n' +
+ ' "host": "127.0.0.1,127.0.0.1,127.0.0.1",\n' +
+ ' "port": "27017,27018,27019",\n' +
+ ' "username": "",\n' +
+ ' "password": "",\n' +
+ ' "database": "nodebb_test"\n' +
+ '}\n' +
+ '==========================================================='
+ );
+ winston.error(errorText);
+ throw new Error(errorText);
+}
+
+if (testDbConfig.database === productionDbConfig.database &&
+ testDbConfig.host === productionDbConfig.host &&
+ testDbConfig.port === productionDbConfig.port) {
+ errorText = 'test_database has the same config as production db';
+ winston.error(errorText);
+ throw new Error(errorText);
+}
+
+nconf.set(dbType, testDbConfig);
+
+winston.info('database config');
+winston.info(dbType);
+winston.info(testDbConfig);
+
+var db = require('../../src/database');
+module.exports = db;
+
+before(function (done) {
+ this.timeout(30000);
+ async.series([
+ function (next) {
+ db.init(next);
+ },
+ function (next) {
+ setupMockDefaults(next);
+ },
+ function (next) {
+ db.initSessionStore(next);
+ },
+ function (next) {
+ var meta = require('../../src/meta');
+
+ // nconf defaults, if not set in config
+ if (!nconf.get('sessionKey')) {
+ nconf.set('sessionKey', 'express.sid');
+ }
+ // Parse out the relative_url and other goodies from the configured URL
+ var urlObject = url.parse(nconf.get('url'));
+ var relativePath = urlObject.pathname !== '/' ? urlObject.pathname : '';
+ nconf.set('base_url', urlObject.protocol + '//' + urlObject.host);
+ nconf.set('secure', urlObject.protocol === 'https:');
+ nconf.set('use_port', !!urlObject.port);
+ nconf.set('relative_path', relativePath);
+ nconf.set('port', urlObject.port || nconf.get('port') || nconf.get('PORT') || (nconf.get('PORT_ENV_VAR') ? nconf.get(nconf.get('PORT_ENV_VAR')) : false) || 4567);
+ nconf.set('upload_path', path.join(nconf.get('base_dir'), nconf.get('upload_path')));
+
+ nconf.set('core_templates_path', path.join(__dirname, '../../src/views'));
+ nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-persona/templates'));
+ nconf.set('theme_templates_path', meta.config['theme:templates'] ? path.join(nconf.get('themes_path'), meta.config['theme:id'], meta.config['theme:templates']) : nconf.get('base_templates_path'));
+ nconf.set('theme_config', path.join(nconf.get('themes_path'), 'nodebb-theme-persona', 'theme.json'));
+ nconf.set('bcrypt_rounds', 1);
+
+ next();
+ },
+ function (next) {
+ var webserver = require('../../src/webserver');
+ var sockets = require('../../src/socket.io');
+ sockets.init(webserver.server);
+
+ require('../../src/notifications').startJobs();
+ require('../../src/user').startJobs();
+
+ webserver.listen(next);
+ },
+ ], done);
+});
+
+function setupMockDefaults(callback) {
+ var meta = require('../../src/meta');
+
+ async.series([
+ function (next) {
+ db.emptydb(next);
+ },
+ function (next) {
+ winston.info('test_database flushed');
+ setupDefaultConfigs(meta, next);
+ },
+ function (next) {
+ meta.configs.init(next);
+ },
+ function (next) {
+ meta.dependencies.check(next);
+ },
+ function (next) {
+ meta.config.postDelay = 0;
+ meta.config.initialPostDelay = 0;
+ meta.config.newbiePostDelay = 0;
+
+ enableDefaultPlugins(next);
+ },
+ function (next) {
+ meta.themes.set({
+ type: 'local',
+ id: 'nodebb-theme-persona',
+ }, next);
+ },
+ ], callback);
+}
+db.setupMockDefaults = setupMockDefaults;
+
+function setupDefaultConfigs(meta, next) {
+ winston.info('Populating database with default configs, if not already set...\n');
+
+ var defaults = require(path.join(nconf.get('base_dir'), 'install/data/defaults.json'));
+
+ meta.configs.setOnEmpty(defaults, next);
+}
+
+function enableDefaultPlugins(callback) {
+ winston.info('Enabling default plugins\n');
+
+ var defaultEnabled = [
+ 'nodebb-plugin-dbsearch',
+ ];
+
+ winston.info('[install/enableDefaultPlugins] activating default plugins', defaultEnabled);
+
+ db.sortedSetAdd('plugins:active', [0], defaultEnabled, callback);
+}
diff --git a/test/notifications.js b/test/notifications.js
index c5a22a6d2c..d2585e4029 100644
--- a/test/notifications.js
+++ b/test/notifications.js
@@ -5,7 +5,11 @@ var assert = require('assert');
var async = require('async');
var db = require('./mocks/databasemock');
+var meta = require('../src/meta');
var user = require('../src/user');
+var topics = require('../src/topics');
+var categories = require('../src/categories');
+var groups = require('../src/groups');
var notifications = require('../src/notifications');
var socketNotifications = require('../src/socket.io/notifications');
@@ -14,6 +18,7 @@ describe('Notifications', function () {
var notification;
before(function (done) {
+ groups.resetCache();
user.create({ username: 'poster' }, function (err, _uid) {
if (err) {
return done(err);
@@ -24,11 +29,19 @@ describe('Notifications', function () {
});
});
+ it('should fail to create notification without a nid', function (done) {
+ notifications.create({}, function (err) {
+ assert.equal(err.message, '[[error:no-notification-id]]');
+ done();
+ });
+ });
+
it('should create a notification', function (done) {
notifications.create({
bodyShort: 'bodyShort',
nid: 'notification_id',
path: '/notification/path',
+ pid: 1,
}, function (err, _notification) {
notification = _notification;
assert.ifError(err);
@@ -45,6 +58,29 @@ describe('Notifications', function () {
});
});
+ it('should return null if pid is same and importance is lower', function (done) {
+ notifications.create({
+ bodyShort: 'bodyShort',
+ nid: 'notification_id',
+ path: '/notification/path',
+ pid: 1,
+ importance: 1,
+ }, function (err, notification) {
+ assert.ifError(err);
+ assert.strictEqual(notification, null);
+ done();
+ });
+ });
+
+ it('should get empty array', function (done) {
+ notifications.getMultiple(null, function (err, data) {
+ assert.ifError(err);
+ assert(Array.isArray(data));
+ assert.equal(data.length, 0);
+ done();
+ });
+ });
+
it('should get notifications', function (done) {
notifications.getMultiple([notification.nid], function (err, notificationsData) {
assert.ifError(err);
@@ -55,6 +91,19 @@ describe('Notifications', function () {
});
});
+ it('should do nothing', function (done) {
+ notifications.push(null, [], function (err) {
+ assert.ifError(err);
+ notifications.push({ nid: null }, [], function (err) {
+ assert.ifError(err);
+ notifications.push(notification, [], function (err) {
+ assert.ifError(err);
+ done();
+ });
+ });
+ });
+ });
+
it('should push a notification to uid', function (done) {
notifications.push(notification, [uid], function (err) {
assert.ifError(err);
@@ -94,6 +143,16 @@ describe('Notifications', function () {
});
});
+ it('should not mark anything with invalid uid or nid', function (done) {
+ socketNotifications.markRead({ uid: null }, null, function (err) {
+ assert.ifError(err);
+ socketNotifications.markRead({ uid: uid }, null, function (err) {
+ assert.ifError(err);
+ done();
+ });
+ });
+ });
+
it('should mark a notification read', function (done) {
socketNotifications.markRead({ uid: uid }, notification.nid, function (err) {
assert.ifError(err);
@@ -109,6 +168,23 @@ describe('Notifications', function () {
});
});
+ it('should not mark anything with invalid uid or nid', function (done) {
+ socketNotifications.markUnread({ uid: null }, null, function (err) {
+ assert.ifError(err);
+ socketNotifications.markUnread({ uid: uid }, null, function (err) {
+ assert.ifError(err);
+ done();
+ });
+ });
+ });
+
+ it('should error if notification does not exist', function (done) {
+ socketNotifications.markUnread({ uid: uid }, 123123, function (err) {
+ assert.equal(err.message, '[[error:no-notification]]');
+ done();
+ });
+ });
+
it('should mark a notification unread', function (done) {
socketNotifications.markUnread({ uid: uid }, notification.nid, function (err) {
assert.ifError(err);
@@ -143,6 +219,13 @@ describe('Notifications', function () {
});
});
+ it('should not do anything', function (done) {
+ socketNotifications.markAllRead({ uid: 1000 }, null, function (err) {
+ assert.ifError(err);
+ done();
+ });
+ });
+
it('should link to the first unread post in a watched topic', function (done) {
var categories = require('../src/categories');
var topics = require('../src/topics');
@@ -251,14 +334,145 @@ describe('Notifications', function () {
});
});
+ it('should return empty with falsy uid', function (done) {
+ user.notifications.get(0, function (err, data) {
+ assert.ifError(err);
+ assert.equal(data.read.length, 0);
+ assert.equal(data.unread.length, 0);
+ done();
+ });
+ });
+
+ it('should get all notifications and filter', function (done) {
+ var nid = 'willbefiltered';
+ notifications.create({
+ bodyShort: 'bodyShort',
+ nid: nid,
+ path: '/notification/path',
+ type: 'post',
+ }, function (err, notification) {
+ assert.ifError(err);
+ notifications.push(notification, [uid], function (err) {
+ assert.ifError(err);
+ setTimeout(function () {
+ user.notifications.getAll(uid, 'post', function (err, nids) {
+ assert.ifError(err);
+ assert.notEqual(nids.indexOf(nid), -1);
+ done();
+ });
+ }, 1500);
+ });
+ });
+ });
+
+ it('should not get anything if notifications does not exist', function (done) {
+ user.notifications.getNotifications(['doesnotexistnid1', 'doesnotexistnid2'], uid, function (err, data) {
+ assert.ifError(err);
+ assert.deepEqual(data, []);
+ done();
+ });
+ });
+
+ it('should get daily notifications', function (done) {
+ user.notifications.getDailyUnread(uid, function (err, data) {
+ assert.ifError(err);
+ assert.equal(data[0].nid, 'willbefiltered');
+ done();
+ });
+ });
+
+ it('should return 0 for falsy uid', function (done) {
+ user.notifications.getUnreadCount(0, function (err, count) {
+ assert.ifError(err);
+ assert.equal(count, 0);
+ done();
+ });
+ });
+
+ it('should not do anything if uid is falsy', function (done) {
+ user.notifications.deleteAll(0, function (err) {
+ assert.ifError(err);
+ done();
+ });
+ });
+
+ it('should send notification to followers of user when he posts', function (done) {
+ var followerUid;
+ async.waterfall([
+ function (next) {
+ user.create({ username: 'follower' }, next);
+ },
+ function (_followerUid, next) {
+ followerUid = _followerUid;
+ user.follow(followerUid, uid, next);
+ },
+ function (next) {
+ categories.create({
+ name: 'Test Category',
+ description: 'Test category created by testing script',
+ }, next);
+ },
+ function (category, next) {
+ topics.post({
+ uid: uid,
+ cid: category.cid,
+ title: 'Test Topic Title',
+ content: 'The content of test topic',
+ }, next);
+ },
+ function (data, next) {
+ setTimeout(next, 1100);
+ },
+ function (next) {
+ user.notifications.getAll(followerUid, '', next);
+ },
+ ], function (err, data) {
+ assert.ifError(err);
+ assert(data);
+ done();
+ });
+ });
+
+ it('should send welcome notification', function (done) {
+ meta.config.welcomeNotification = 'welcome to the forums';
+ user.notifications.sendWelcomeNotification(uid, function (err) {
+ assert.ifError(err);
+ user.notifications.sendWelcomeNotification(uid, function (err) {
+ assert.ifError(err);
+ setTimeout(function () {
+ user.notifications.getAll(uid, '', function (err, data) {
+ meta.config.welcomeNotification = '';
+ assert.ifError(err);
+ assert.notEqual(data.indexOf('welcome_' + uid), -1);
+ done();
+ });
+ }, 1100);
+ });
+ });
+ });
+
it('should prune notifications', function (done) {
notifications.create({
bodyShort: 'bodyShort',
nid: 'tobedeleted',
path: '/notification/path',
- }, function (err) {
+ }, function (err, notification) {
assert.ifError(err);
- notifications.prune(done);
+ notifications.prune(function (err) {
+ assert.ifError(err);
+ var week = 604800000;
+ db.sortedSetAdd('notifications', Date.now() - (2 * week), notification.nid, function (err) {
+ assert.ifError(err);
+ notifications.prune(function (err) {
+ assert.ifError(err);
+ notifications.get(notification.nid, function (err, data) {
+ assert.ifError(err);
+ assert(!data);
+ done();
+ });
+ });
+ });
+ });
});
});
diff --git a/test/topics.js b/test/topics.js
index c362ca0824..4551595fa6 100644
--- a/test/topics.js
+++ b/test/topics.js
@@ -760,13 +760,13 @@ describe('Topic\'s', function () {
it('should 401 if not allowed to read as guest', function (done) {
var privileges = require('../src/privileges');
- privileges.categories.rescind(['read'], topicData.cid, 'guests', function (err) {
+ privileges.categories.rescind(['topics:read'], topicData.cid, 'guests', function (err) {
assert.ifError(err);
request(nconf.get('url') + '/api/topic/' + topicData.slug, function (err, response, body) {
assert.ifError(err);
assert.equal(response.statusCode, 401);
assert(body);
- privileges.categories.give(['read'], topicData.cid, 'guests', done);
+ privileges.categories.give(['topics:read'], topicData.cid, 'guests', done);
});
});
});
@@ -1551,7 +1551,7 @@ describe('Topic\'s', function () {
it('should return empty array if first param is empty', function (done) {
- topics.getTeasers([], function (err, teasers) {
+ topics.getTeasers([], 1, function (err, teasers) {
assert.ifError(err);
assert.equal(0, teasers.length);
done();
@@ -1559,7 +1559,7 @@ describe('Topic\'s', function () {
});
it('should get teasers with 2 params', function (done) {
- topics.getTeasers([topic1.topicData, topic2.topicData], function (err, teasers) {
+ topics.getTeasers([topic1.topicData, topic2.topicData], 1, function (err, teasers) {
assert.ifError(err);
assert.deepEqual([undefined, undefined], teasers);
done();
@@ -1568,7 +1568,7 @@ describe('Topic\'s', function () {
it('should get teasers with first posts', function (done) {
meta.config.teaserPost = 'first';
- topics.getTeasers([topic1.topicData, topic2.topicData], function (err, teasers) {
+ topics.getTeasers([topic1.topicData, topic2.topicData], 1, function (err, teasers) {
assert.ifError(err);
assert.equal(2, teasers.length);
assert(teasers[0]);
@@ -1581,7 +1581,7 @@ describe('Topic\'s', function () {
});
it('should get teasers even if one topic is falsy', function (done) {
- topics.getTeasers([null, topic2.topicData], function (err, teasers) {
+ topics.getTeasers([null, topic2.topicData], 1, function (err, teasers) {
assert.ifError(err);
assert.equal(2, teasers.length);
assert.equal(undefined, teasers[0]);
@@ -1598,7 +1598,7 @@ describe('Topic\'s', function () {
topics.reply({ uid: adminUid, content: 'reply 1 content', tid: topic1.topicData.tid }, function (err, result) {
assert.ifError(err);
topic1.topicData.teaserPid = result.pid;
- topics.getTeasers([topic1.topicData, topic2.topicData], function (err, teasers) {
+ topics.getTeasers([topic1.topicData, topic2.topicData], 1, function (err, teasers) {
assert.ifError(err);
assert(teasers[0]);
assert(teasers[1]);
@@ -1610,7 +1610,7 @@ describe('Topic\'s', function () {
});
it('should get teasers by tids', function (done) {
- topics.getTeasersByTids([topic2.topicData.tid, topic1.topicData.tid], function (err, teasers) {
+ topics.getTeasersByTids([topic2.topicData.tid, topic1.topicData.tid], 1, function (err, teasers) {
assert.ifError(err);
assert(2, teasers.length);
assert.equal(teasers[1].content, 'reply 1 content');
@@ -1619,7 +1619,7 @@ describe('Topic\'s', function () {
});
it('should return empty array ', function (done) {
- topics.getTeasersByTids([], function (err, teasers) {
+ topics.getTeasersByTids([], 1, function (err, teasers) {
assert.ifError(err);
assert.equal(0, teasers.length);
done();
@@ -1627,7 +1627,7 @@ describe('Topic\'s', function () {
});
it('should get teaser by tid', function (done) {
- topics.getTeaser(topic2.topicData.tid, function (err, teaser) {
+ topics.getTeaser(topic2.topicData.tid, 1, function (err, teaser) {
assert.ifError(err);
assert(teaser);
assert.equal(teaser.content, 'content 2');
diff --git a/test/user.js b/test/user.js
index f7d9986336..fd1d720ab5 100644
--- a/test/user.js
+++ b/test/user.js
@@ -237,6 +237,76 @@ describe('User', function () {
done();
});
});
+
+ it('should search users by ip', function (done) {
+ User.create({ username: 'ipsearch' }, function (err, uid) {
+ assert.ifError(err);
+ db.sortedSetAdd('ip:1.1.1.1:uid', [1, 1], [testUid, uid], function (err) {
+ assert.ifError(err);
+ socketUser.search({ uid: testUid }, { query: '1.1.1.1', searchBy: 'ip' }, function (err, data) {
+ assert.ifError(err);
+ assert(Array.isArray(data.users));
+ assert.equal(data.users.length, 2);
+ done();
+ });
+ });
+ });
+ });
+
+ it('should return empty array if query is empty', function (done) {
+ socketUser.search({ uid: testUid }, { query: '' }, function (err, data) {
+ assert.ifError(err);
+ assert.equal(data.users.length, 0);
+ done();
+ });
+ });
+
+ it('should filter users', function (done) {
+ User.create({ username: 'ipsearch_filter' }, function (err, uid) {
+ assert.ifError(err);
+ User.setUserFields(uid, { banned: 1, flags: 10 }, function (err) {
+ assert.ifError(err);
+ socketUser.search({ uid: testUid }, {
+ query: 'ipsearch',
+ onlineOnly: true,
+ bannedOnly: true,
+ flaggedOnly: true,
+ }, function (err, data) {
+ assert.ifError(err);
+ assert.equal(data.users[0].username, 'ipsearch_filter');
+ done();
+ });
+ });
+ });
+ });
+
+ it('should sort results by username', function (done) {
+ async.waterfall([
+ function (next) {
+ User.create({ username: 'brian' }, next);
+ },
+ function (uid, next) {
+ User.create({ username: 'baris' }, next);
+ },
+ function (uid, next) {
+ User.create({ username: 'bzari' }, next);
+ },
+ function (uid, next) {
+ User.search({
+ uid: testUid,
+ query: 'b',
+ sortBy: 'username',
+ paginate: false,
+ }, next);
+ },
+ ], function (err, data) {
+ assert.ifError(err);
+ assert.equal(data.users[0].username, 'baris');
+ assert.equal(data.users[1].username, 'brian');
+ assert.equal(data.users[2].username, 'bzari');
+ done();
+ });
+ });
});
describe('.delete()', function () {