diff --git a/.eslintrc b/.eslintrc index 36d90769..0837f9d3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,13 +8,18 @@ env: builtin: true mocha: true jasmine: false - es6: false + es6: true plugins: - react - + +extends: + - eslint:recommended + - plugin:react/recommended + rules: handle-callback-err: 1 + max-len: [1, 140, 4, { ignorePattern: "\": " }] no-debugger: 1 no-undef: 1 no-inner-declarations: [1, "functions"] @@ -22,60 +27,77 @@ rules: no-new-func: 1 no-new-wrappers: 1 no-cond-assign: [1, "except-parens"] - no-debugger: 3 - no-dupe-keys: 3 - no-eval: 3 + no-dupe-keys: 1 + no-eval: 1 + no-console: 0 no-func-assign: 1 no-invalid-regexp: 1 - no-irregular-whitespace: 3 + no-irregular-whitespace: 1 no-negated-in-lhs: 1 - no-regex-spaces: 3 + no-regex-spaces: 1 no-unreachable: 1 - use-isnan: 2 + use-isnan: 1 valid-typeof: 1 - no-redeclare: 3 + no-redeclare: 1 no-with: 1 - radix: 3 - no-delete-var: 2 - no-label-var: 3 - no-shadow-restricted-names: 2 - handle-callback-err: 1 - no-new-require: 2 - no-unused-vars: [3, {vars: "all", args: "none"}] - no-undef: 1 - semi: 3 - no-extra-semi: 3 + radix: 1 + no-delete-var: 1 + no-label-var: 1 + no-shadow-restricted-names: 1 + no-new-require: 1 + no-unused-vars: [1, {vars: "all", args: "none"}] + semi: 1 + no-extra-semi: 1 + one-var: [1, "never"] + arrow-parens: 1 + no-confusing-arrow: 1 + camelcase: [1, { properties: "never" }] + id-blacklist: [1, "uname", "cb", "acct"] + default-case: 1 + eqeqeq: [1, "smart"] // React rules - display-name: [1, { acceptTranspilerName: true }] - jsx-curly-spacing: 1 - no-deprecated: [1, {react: "0.13.0"}] - no-did-mount-set-state: 1 - no-did-update-set-state: 1 - no-direct-mutation-state: 1 - no-is-mounted: 1 - no-unknown-property: 1 - prefer-es6-class: [1, "never"] - prop-types: 1 - react-in-jsx-scope: 1 - self-closing-comp: 1 - sort-comp: 1 - wrap-multilines: 1 - jsx-boolean-value: [1, "always"] - jsx-equals-spacing: 1 - jsx-indent-props: 1 - jsx-key: 1 - jsx-indent: 1 - jsx-max-props-per-line: [1, { maximum: 5 }] - jsx-no-bind: 1 - jsx-no-duplicate-props: 1 - jsx-no-undef: 1 - jsx-pascal-case: 1 - jsx-uses-react: 1 + react/jsx-curly-spacing: 1 + react/no-deprecated: 1 + react/no-did-mount-set-state: 1 + react/no-did-update-set-state: 1 + react/no-direct-mutation-state: 1 + react/no-is-mounted: 1 + react/no-unknown-property: 1 + react/prefer-es6-class: 1 + react/prop-types: 1 + react/react-in-jsx-scope: 1 + react/self-closing-comp: 1 + react/sort-comp: 1 + # This rule is not compatible with eslint < 3 + # react/jsx-wrap-multilines: 1 + react/jsx-boolean-value: [1, "always"] + react/jsx-equals-spacing: 1 + react/jsx-indent-props: 1 + react/jsx-key: 1 + react/jsx-indent: 1 + react/jsx-max-props-per-line: [1, { maximum: 5 }] + react/jsx-no-bind: 1 + react/jsx-no-duplicate-props: 1 + react/jsx-no-undef: 1 + react/jsx-pascal-case: 1 + react/jsx-uses-react: 1 - default-case: 3 - keyword-spacing: [1, {"before": true, "after": true, "overrides": { "catch": {"after": "maybe" } }}] + keyword-spacing: [1, {"before": true, "after": true, "overrides": { "catch": {"after": true } }}] + key-spacing: [1, { beforeColon: false, afterColon: true, mode: "strict" }] space-in-parens: [1, "never"] - // space-before-function-paren: [3, {"named": "never", "anonymous": "never"}] - spaced-comment: 3 + space-infix-ops: [1] + arrow-spacing: 1 + generator-star-spacing: [1, {"before": false, "after": true}] + space-before-blocks: [1, "always"] + // TODO: after eslint update in IDE + // space-before-function-paren: [1, {"named": "never", "anonymous": "ignore"}] + // object-curly-newline: [1, { ObjectExpression: { "multiline": true }, "ObjectPattern": "never" }] + // DISABLED: causes issue with define(function() in client-side code + // indent: [1, 4, { SwitchCase: 1, outerIIFEBody: 0, MemberExpression: 1, FunctionDeclaration: { body: 0, parameters: 1 }, FunctionExpression: { body: 0, parameters: 1 } }] + object-curly-spacing: [1, "always", { objectsInObjects: false, arraysInObjects: false }] + spaced-comment: 1 + comma-spacing: 1 + no-multi-spaces: 1 + no-lone-blocks: 1 // valid-jsdoc: [1, { requireReturn: false, requireParamDescription: false, prefer: { "return": "return" } }] diff --git a/node_modules/c9/git.js b/node_modules/c9/git.js index dea02285..6228914a 100644 --- a/node_modules/c9/git.js +++ b/node_modules/c9/git.js @@ -1,7 +1,5 @@ "use strict"; -require("amd-loader"); - var Fs = require("fs"); var Path = require("path"); var exec = require("child_process").exec; diff --git a/node_modules/c9/ratelimit.js b/node_modules/c9/ratelimit.js index 2b4c4ef7..1b045036 100644 --- a/node_modules/c9/ratelimit.js +++ b/node_modules/c9/ratelimit.js @@ -30,8 +30,15 @@ function ratelimit(key, duration, max) { return true; } + // Returns a deep value from an object. E.g. resolveValue({user: {id: 5}}, "user.id") === 5 + function resolveValue(obj, path) { + return path.split('.').reduce(function(prev, curr) { + return prev ? prev[curr] : undefined; + }, obj); + } + return function(req, res, next) { - var handle = req.params[key]; + var handle = resolveValue(req.params, key); requests[handle] = requests[handle] || []; if (requests[handle].length >= max) { diff --git a/node_modules/c9/ratelimit_test.js b/node_modules/c9/ratelimit_test.js index fd674350..7cff5de9 100644 --- a/node_modules/c9/ratelimit_test.js +++ b/node_modules/c9/ratelimit_test.js @@ -5,6 +5,7 @@ require("c9/inline-mocha")(module); var ratelimit = require("./ratelimit"); var assert = require("assert"); var async = require("async"); +var sinon = require("sinon"); describe("ratelimit", function() { @@ -31,7 +32,16 @@ describe("ratelimit", function() { }); }); - + it("Should work with deep keys", function (done) { + var limiter = ratelimit("user.id", 10, 1); + limiter({params: {user: {id: "hey"}}}, null, function (err) { + assert(!err, err); + limiter({params: {user: {id: "yay"}}}, null, function (err) { + assert(!err, err); + done(); + }); + }); + }); it("Should work again after a delay", function (done) { var limiter = ratelimit("username", 10, 1); @@ -68,37 +78,27 @@ describe("ratelimit", function() { }); it("Should expire keys at the correct times", function (done) { + var clock = sinon.useFakeTimers(); var limiter = ratelimit("username", 50, 2); - async.series([ - function(next) { - limiter({params: {username: "mario"}}, null, function(err) { - assert(!err, err); - setTimeout(next, 25); - }); - }, - function (next) { - limiter({params: {username: "mario"}}, null, function(err) { - assert(!err, err); - setTimeout(next, 40); - }); - }, - function (next) { - limiter({params: {username: "mario"}}, null, function(err) { - assert(!err, err); - limiter({params: {username: "mario"}}, null, function(err) { - assert(err); - assert.equal(err.code, 429); - setTimeout(next, 20); - }); - }); - }, - function (next) { - limiter({params: {username: "mario"}}, null, function(err) { - assert(!err, err); - next(); - }); - } - ], done); + limiter({params: {username: "mario"}}, null, function(err){ + assert(!err, err); + }); + clock.tick(40); + limiter({params: {username: "mario"}}, null, function(err){ + assert(!err, err); + }); + clock.tick(45); + limiter({params: {username: "mario"}}, null, function(err){ + assert(!err, err); + }); + limiter({params: {username: "mario"}}, null, function(err){ + assert(err); + assert.equal(err.code, 429); + }); + clock.tick(40); + limiter({params: {username: "mario"}}, null, function(err){ + assert(!err, err); + }); + done(); }); - }); \ No newline at end of file diff --git a/node_modules/c9/urls_test.js b/node_modules/c9/urls_test.js index fe9dd2f5..cabb9a83 100644 --- a/node_modules/c9/urls_test.js +++ b/node_modules/c9/urls_test.js @@ -51,14 +51,6 @@ describe("urls", function() { assert.equal(settings.ide.baseUrlPattern, "https://ide.$DOMAIN"); }); - it("behaves like an architect plugin", function(next) { - urls({}, { "error.logger": urls.mockErrorLogger }, function(err, result) { - assert(!err, err); - assert(result.urls.getBaseUrl); - next(); - }); - }); - it("can get the base url for a request", function() { var mockRequest = { host: "preview.c9.io" @@ -87,10 +79,8 @@ describe("urls", function() { var mockRequest = { host: "preview.cloud9beta.com" }; - urls.mockErrorLogger.log = sinon.spy(); var baseUrl = urls.getBaseUrl(mockRequest, "https://ide.$DOMAIN", "https://$DOMAIN"); assert.equal(baseUrl, "https://c9.io"); - assert(urls.mockErrorLogger.log.called); }); it("can get the base url in dogfooding mode", function() { diff --git a/package.json b/package.json index 2d8879fe..af5fd707 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "c9", "description": "New Cloud9 Client", - "version": "3.1.3131", + "version": "3.1.3152", "author": "Ajax.org B.V. ", "private": true, "main": "bin/c9", @@ -58,7 +58,7 @@ "c9" ], "c9plugins": { - "c9.ide.language": "#f55c832376", + "c9.ide.language": "#241bd6b7de", "c9.ide.language.core": "#bfb5dd2acc", "c9.ide.language.css": "#46ad561506", "c9.ide.language.generic": "#b47cbe58f9", @@ -66,17 +66,17 @@ "c9.ide.language.html.diff": "#7d6cecfb90", "c9.ide.language.javascript": "#a5c1d05394", "c9.ide.language.javascript.immediate": "#82c426dbca", - "c9.ide.language.javascript.eslint": "#cb9e3f5a8e", + "c9.ide.language.javascript.eslint": "#66c856d7ce", "c9.ide.language.javascript.tern": "#0545a6385d", "c9.ide.language.javascript.infer": "#b9c2e4bdb8", "c9.ide.language.jsonalyzer": "#a0549e14ff", "c9.ide.language.codeintel": "#0fe92d6f46", - "c9.ide.collab": "#54aa1cbee0", + "c9.ide.collab": "#f60595d380", "c9.ide.local": "#9169fec157", "c9.ide.find": "#e632ecf4be", "c9.ide.find.infiles": "#ad9ff74638", "c9.ide.find.replace": "#8468067976", - "c9.ide.run.debug": "#f10c7e7d19", + "c9.ide.run.debug": "#8963fb45c4", "c9.automate": "#47e2c429c9", "c9.ide.ace.emmet": "#6dc4585e02", "c9.ide.ace.gotoline": "#d33220b1e0", diff --git a/plugins/c9.core/util.js b/plugins/c9.core/util.js index af2f4d63..dc50b101 100644 --- a/plugins/c9.core/util.js +++ b/plugins/c9.core/util.js @@ -196,12 +196,19 @@ define(function(require, exports, module) { return "<" + tag + " " + plugin.toXmlAttributes(attrs) + (noclose ? ">" : " />"); }; + function isMd5String(str) { + return /^[0-9a-f]{32}$/.test(str); + } + /** * Returns the gravatar url for this user * @param {Number} size the size of the image */ plugin.getGravatarUrl = function getGravatarUrl(email, size, defaultImage) { - var md5Email = apf.crypto.MD5.hex_md5((email || "").trim().toLowerCase()); + var md5Email = email + if (!isMd5String(md5Email)) { + md5Email = apf.crypto.MD5.hex_md5((email || "").trim().toLowerCase()); + } return "https://secure.gravatar.com/avatar/" + md5Email + "?s=" + size + "&d=" + (defaultImage || "retro"); }; diff --git a/plugins/c9.core/util_test.js b/plugins/c9.core/util_test.js index ef668547..57b4df7c 100644 --- a/plugins/c9.core/util_test.js +++ b/plugins/c9.core/util_test.js @@ -9,6 +9,7 @@ require(["lib/architect/architect", "lib/chai/chai"], function (architect, chai) expect.setupArchitectTest([ "plugins/c9.core/ext", "plugins/c9.core/util", + "plugins/c9.ide.ui/lib_apf", // Mock plugins { consumes: [], @@ -32,6 +33,15 @@ require(["lib/architect/architect", "lib/chai/chai"], function (architect, chai) }); }); + describe("getGravatarUrl", function() { + it("Should hash a normal email", function() { + expect(util.getGravatarUrl("test@test.com", 32)).to.match(/^https:\/\/secure.gravatar.com\/avatar\/b642b4217b34b1e8d3bd915fc65c4452.*/); + }); + it("Should use not re-hash an md5 passed in", function() { + expect(util.getGravatarUrl("b642b4217b34b1e8d3bd915fc65c4452", 32)).to.match(/^https:\/\/secure.gravatar.com\/avatar\/b642b4217b34b1e8d3bd915fc65c4452.*/); + }); + }); + describe('normalizePath', function() { var normalizePath = util.normalizePath; it('should handle home in workspaceDir', function() { diff --git a/plugins/c9.ide.layout.classic/less/c9-menu-btn.less b/plugins/c9.ide.layout.classic/less/c9-menu-btn.less index d677145b..598715e1 100644 --- a/plugins/c9.ide.layout.classic/less/c9-menu-btn.less +++ b/plugins/c9.ide.layout.classic/less/c9-menu-btn.less @@ -65,7 +65,7 @@ display: none; } -.c9-menu-btn.nosize .icon { background-size: auto !important; } +.c9-menu-btn.nosize .icon { background-size: auto; } .c9-menu-btn.preferences{ padding: @preference-menu-button-padding !important; @@ -116,4 +116,4 @@ .c9-menu-btn.c9btn.offline{ color: #d22a3f; text-shadow: 0 1px black; -} \ No newline at end of file +} diff --git a/plugins/c9.ide.terminal/aceterm/hover_link.js b/plugins/c9.ide.terminal/aceterm/hover_link.js index 2296476e..b46c5768 100644 --- a/plugins/c9.ide.terminal/aceterm/hover_link.js +++ b/plugins/c9.ide.terminal/aceterm/hover_link.js @@ -309,7 +309,7 @@ var HoverLink = function(editor) { else if (prompt.command === "git") { // git status var prefix = line.substr(0, match.start); if (match.start + value.length == line.length - && /^(#|[ MDR?A]{2})\s+([\w\s]+:\s+)?$/.test(prefix) + && /^(#|[ MDRU?A]{2})\s+([\w\s]+:\s+)?$/.test(prefix) ) { match.type = "path"; } else { diff --git a/plugins/c9.vfs.server/vfs.server_test.js b/plugins/c9.vfs.server/vfs.server_test.js new file mode 100644 index 00000000..492907e3 --- /dev/null +++ b/plugins/c9.vfs.server/vfs.server_test.js @@ -0,0 +1,122 @@ +#!/usr/bin/env node +"use strict"; +"use server"; + + +require("c9/inline-mocha")(module); +if (typeof define === "undefined") { + require("amd-loader"); +} + +var sinon = require("sinon"); +var assert = require("assert"); +var vfsServer = require("./vfs.server"); +var mockDb = {}; +var mockCache = { + remove: sinon.stub() +}; +var mockApi = { + section: sinon.stub().returns({ + registerType: sinon.stub(), + post: sinon.stub(), + get: sinon.stub(), + delete: sinon.stub(), + all: sinon.stub() + }), + use: sinon.stub(), + ensureAdmin: sinon.stub(), + get: sinon.stub(), + authenticate: sinon.stub() +}; +var mockRender = { + setTemplatePath: sinon.stub() +}; +var mockConnect = { + getModule: sinon.stub().returns({ + compress: sinon.stub() + }) +}; + +describe(__filename, function() { + var server; + beforeEach(function (done) { + vfsServer({testing: true}, { + "db": mockDb, + "vfs.cache": mockCache, + "api": mockApi, + "connect.render": mockRender, + "connect": mockConnect, + }, function (err, _server) { + if (err) return done(err); + server = _server["vfs.server"]; + done(); + }); + }); + + describe("handlePublish", function() { + beforeEach(function() { + mockCache.remove = sinon.stub(); + }); + + describe("remove_member", function() { + it("Should kill the removed members VFS connection", function (done) { + var vfs = { + id: "9c123", + uid: "123" + }; + var message = JSON.stringify({ + action: "remove_member", + body: { + uid: "123" + } + }); + server.handlePublish(vfs, message); + setTimeout(function() { + assert(mockCache.remove.calledWith(vfs.id)); + done(); + }, 150); + }); + + it("Should not kill the other members VFS connection", function (done) { + var vfs = { + id: "9c123", + uid: "456" + }; + var message = JSON.stringify({ + action: "remove_member", + body: { + uid: "123" + } + }); + server.handlePublish(vfs, message); + setTimeout(function() { + assert.equal(mockCache.remove.callCount, 0); + done(); + }, 150); + }); + }); + + describe("update_member_access", function() { + it("Should kill the members VFS connection so they rejoin with the new access level", function (done) { + var vfs = { + id: "9c123", + uid: "123" + }; + var message = JSON.stringify({ + action: "update_member_access", + body: { + uid: "123" + } + }); + server.handlePublish(vfs, message); + setTimeout(function() { + assert(mockCache.remove.calledWith(vfs.id)); + done(); + }, 150); + }); + }); + + }); + + +}); \ No newline at end of file