From 364000ba78c7f292b1b8c1d07e1cb1c7b911aeb8 Mon Sep 17 00:00:00 2001 From: Ruben Daniels Date: Sun, 8 Mar 2015 22:25:47 +0000 Subject: [PATCH 01/38] Enable the SDK by default --- plugins/c9.core/ext.js | 2 +- plugins/c9.ide.plugins/debug.js | 2 +- plugins/c9.ide.plugins/installer.js | 2 +- plugins/c9.ide.plugins/loader.js | 2 +- plugins/c9.ide.plugins/manager.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/c9.core/ext.js b/plugins/c9.core/ext.js index fe7d79c9..5d613650 100644 --- a/plugins/c9.core/ext.js +++ b/plugins/c9.core/ext.js @@ -689,7 +689,7 @@ define(function(require, exports, module) { * plugin.freezePublicAPI({ * doSomething : doSomething * }); - * }); + * } * }); * * @class Plugin diff --git a/plugins/c9.ide.plugins/debug.js b/plugins/c9.ide.plugins/debug.js index a3fb632b..f2b6ff30 100644 --- a/plugins/c9.ide.plugins/debug.js +++ b/plugins/c9.ide.plugins/debug.js @@ -33,7 +33,7 @@ define(function(require, exports, module) { // var emit = plugin.getEmitter(); var ENABLED = c9.location.indexOf("debug=2") > -1; - var HASSDK = c9.location.indexOf("sdk=1") > -1; + var HASSDK = c9.location.indexOf("sdk=0") === -1; var loaded = false; function load() { diff --git a/plugins/c9.ide.plugins/installer.js b/plugins/c9.ide.plugins/installer.js index 27526d2e..dbe02907 100644 --- a/plugins/c9.ide.plugins/installer.js +++ b/plugins/c9.ide.plugins/installer.js @@ -20,7 +20,7 @@ define(function(require, exports, module) { var plugin = new Plugin("Ajax.org", main.consumes); // var emit = plugin.getEmitter(); - var HASSDK = c9.location.indexOf("sdk=1") > -1; + var HASSDK = c9.location.indexOf("sdk=0") === -1; var queue = []; var installing; diff --git a/plugins/c9.ide.plugins/loader.js b/plugins/c9.ide.plugins/loader.js index 03a523fb..78455b7f 100644 --- a/plugins/c9.ide.plugins/loader.js +++ b/plugins/c9.ide.plugins/loader.js @@ -23,7 +23,7 @@ define(function(require, exports, module) { // var emit = plugin.getEmitter(); var ENABLED = c9.location.indexOf("plugins=0") == -1; - var HASSDK = c9.location.indexOf("sdk=1") > -1; + var HASSDK = c9.location.indexOf("sdk=0") === -1; var plugins = options.plugins; var names = []; diff --git a/plugins/c9.ide.plugins/manager.js b/plugins/c9.ide.plugins/manager.js index f306fe76..89aa6516 100644 --- a/plugins/c9.ide.plugins/manager.js +++ b/plugins/c9.ide.plugins/manager.js @@ -99,7 +99,7 @@ define(function(require, exports, module) { }); // var emit = plugin.getEmitter(); - var HASSDK = c9.location.indexOf("sdk=1") > -1; + var HASSDK = c9.location.indexOf("sdk=0") === -1; var model, datagrid, filterbox; var btnUninstall, btnReport, btnReadme, btnCloud9; From 6d5f72d2a509ae44ea734713ad675eda532682e9 Mon Sep 17 00:00:00 2001 From: Matthijs van Henten Date: Mon, 9 Mar 2015 13:46:59 +0000 Subject: [PATCH 02/38] Revert "Revert "Merge pull request +6235 from c9/mvh-redis-stats-performance"" This reverts commit a9eb23a4039719de8b4670e23d8b3d70015602d7. Conflicts: package.json plugins/c9.api/project.js plugins/c9.docker/stats_collector.js plugins/c9.ide.performancestats/stats.js plugins/c9.sapi/docker_test.js --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 96cc49bd..9a327eec 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dependencies": { "acorn": ">=0.11.0", "amd-loader": "~0.0.5", - "async": "^0.9.0", + "async": "~0.2.7", "base64id": "~0.1.0", "c9": "0.1.0", "connect": "~2.12.0", From e56242ec24121cb7c0d9331121c3f69157bf7e56 Mon Sep 17 00:00:00 2001 From: Matthijs van Henten Date: Mon, 9 Mar 2015 16:31:22 +0000 Subject: [PATCH 03/38] fixes bug ( undefined was cut up to "und") --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9a327eec..96cc49bd 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dependencies": { "acorn": ">=0.11.0", "amd-loader": "~0.0.5", - "async": "~0.2.7", + "async": "^0.9.0", "base64id": "~0.1.0", "c9": "0.1.0", "connect": "~2.12.0", From bc2af84013785e83e744a906d01f94646eb969fe Mon Sep 17 00:00:00 2001 From: Ruben Daniels Date: Tue, 10 Mar 2015 01:46:07 +0000 Subject: [PATCH 04/38] Simplify the panel api by having the panel object create the bar element --- package.json | 6 ++--- plugins/c9.ide.keys/panel.js | 3 +-- plugins/c9.ide.keys/panel.xml | 8 +++--- plugins/c9.ide.panels/panel.js | 48 ++++++++++++++++++---------------- plugins/c9.ide.tree/tree.js | 4 +-- plugins/c9.ide.tree/tree.xml | 18 ++++++------- 6 files changed, 42 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index 8b40c569..c0e0d2e7 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ }, "licenses": [], "c9plugins": { - "c9.ide.language": "#a6b859c38e", + "c9.ide.language": "#1ec8e1fa35", "c9.ide.language.css": "#afda1f867c", "c9.ide.language.generic": "#87a4a44671", "c9.ide.language.html": "#fa4833e117", @@ -65,7 +65,7 @@ "c9.ide.find": "#989c06e6a7", "c9.ide.find.infiles": "#f98dfef554", "c9.ide.find.replace": "#e4daf722b8", - "c9.ide.run.debug": "#df6615768a", + "c9.ide.run.debug": "#cb3d2e8a44", "c9.ide.ace.emmet": "#e5f1a92ac3", "c9.ide.ace.gotoline": "#4d1a93172c", "c9.ide.ace.keymaps": "#6c4bb65b1f", @@ -84,7 +84,7 @@ "c9.ide.immediate": "#e9ba147cc2", "c9.ide.installer": "#24e7d6f399", "c9.ide.mount": "#32e79866ee", - "c9.ide.navigate": "#eab638cc2f", + "c9.ide.navigate": "#c2b2a7ba7b", "c9.ide.newresource": "#9a7464cc47", "c9.ide.openfiles": "#28a4f5af16", "c9.ide.preview": "#3c4dded23f", diff --git a/plugins/c9.ide.keys/panel.js b/plugins/c9.ide.keys/panel.js index f11bc665..4fe0a554 100644 --- a/plugins/c9.ide.keys/panel.js +++ b/plugins/c9.ide.keys/panel.js @@ -24,7 +24,6 @@ define(function(require, exports, module) { var plugin = new Panel("Ajax.org", main.consumes, { index: options.index || 300, caption: "Commands", - elementName: "winCommands", minWidth: 150, autohide: true, where: options.where || "left" @@ -69,7 +68,7 @@ define(function(require, exports, module) { var treeParent = plugin.getElement("commandsList"); txtFilter = plugin.getElement("txtFilter"); - winCommands = plugin.getElement("winCommands"); + winCommands = options.aml; // Create the Ace Tree tree = new Tree(treeParent.$int); diff --git a/plugins/c9.ide.keys/panel.xml b/plugins/c9.ide.keys/panel.xml index 84982a9e..36a61e18 100644 --- a/plugins/c9.ide.keys/panel.xml +++ b/plugins/c9.ide.keys/panel.xml @@ -1,7 +1,5 @@ - - - - + + \ No newline at end of file diff --git a/plugins/c9.ide.panels/panel.js b/plugins/c9.ide.panels/panel.js index 065f4dff..149e0638 100644 --- a/plugins/c9.ide.panels/panel.js +++ b/plugins/c9.ide.panels/panel.js @@ -17,19 +17,18 @@ define(function(require, module, exports) { var uCaseFirst = require("c9/string").uCaseFirst; function Panel(developer, deps, options) { - // Panel extends ext.Plugin var plugin = new Plugin(developer, deps); var emit = plugin.getEmitter(); var autohide = options.autohide || false; var index = options.index || 100; - var className = options.className; + var buttonCSSClass = options.buttonCSSClass; + var panelCSSClass = options.panelCSSClass; var caption = options.caption; - var elementName = options.elementName; var width = options.width; var minWidth = options.minWidth; - var mnuItem, button, area, lastPanel, xpath, where; + var mnuItem, button, area, lastPanel, xpath, where, aml; plugin.on("load", function(){ xpath = "state/panels/" + plugin.name; @@ -153,23 +152,27 @@ define(function(require, module, exports) { if (drawn) return false; drawn = true; + aml = area.aml.appendChild(new ui.bar({ + "skin": "panel-bar", + "class" : panelCSSClass || "", + "visible": false + })); + plugin.addElement(aml); + emit.sticky("draw", { - html: area.container, - aml: area.aml + html: aml.$int, + aml: aml }); - var aml = plugin.getElement(elementName); - if (aml) { - aml.$ext.style.zIndex = 100; - aml.$ext.style.minWidth = ""; //Needed for the anims - aml.$ext.style.position = "absolute"; - aml.$ext.style.left = where == "left" ? area.width + "px" : 0; - aml.$ext.style.top = 0; - aml.$ext.style.right = where == "right" ? area.width + "px" : 0; - aml.$ext.style.bottom = 0; - - aml.$display = apf.CSSPREFIX + "Flex"; - } + aml.$ext.style.zIndex = 100; + aml.$ext.style.minWidth = ""; //Needed for the anims + aml.$ext.style.position = "absolute"; + aml.$ext.style.left = where == "left" ? area.width + "px" : 0; + aml.$ext.style.top = 0; + aml.$ext.style.right = where == "right" ? area.width + "px" : 0; + aml.$ext.style.bottom = 0; + + aml.$display = apf.CSSPREFIX + "Flex"; return true; } @@ -178,7 +181,6 @@ define(function(require, module, exports) { area = toArea; try { - var aml = plugin.getElement(elementName); if (aml) area.aml.appendChild(aml); } catch (e) {} @@ -203,7 +205,7 @@ define(function(require, module, exports) { state: true, caption: caption, auto: false, - "class" : className || "", + "class" : buttonCSSClass || "", onmousedown: function(){ panels.areas[where].toggle(plugin.name, autohide, true); }, @@ -312,8 +314,8 @@ define(function(require, module, exports) { * autohiding panel. The developer is responsible for hiding * the panel. This behavior will animate the panel during * @param hide and show over other panels, if there are any. - * @param {String} [options.elementName] Specifies the name of the aml element that renders the panel - * @param {String} [options.className] Specifies the name of the css class that is applied to the panel + * @param {String} [options.buttonCSSClass] Specifies the name of the css class that is applied to the button + * @param {String} [options.panelCSSClass] Specifies the name of the css class that is applied to the panel * @param {Number} [options.width] Specifies the default width of the panel * @param {Number} [options.minWidth] Specifies the minimal width of the panel * @param {String} [options.where] Accepts "left" or "right" to determine where the panel is added @@ -350,7 +352,7 @@ define(function(require, module, exports) { * @private * @readonly */ - get aml(){ return plugin.getElement(elementName); }, + get aml(){ return aml; }, /** * The area that this panel is a part of. diff --git a/plugins/c9.ide.tree/tree.js b/plugins/c9.ide.tree/tree.js index 08481212..995ff049 100644 --- a/plugins/c9.ide.tree/tree.js +++ b/plugins/c9.ide.tree/tree.js @@ -43,7 +43,7 @@ define(function(require, exports, module) { var plugin = new Panel("Ajax.org", main.consumes, { index: options.index || 100, caption: "Workspace", - elementName: "winFilesViewer", + panelCSSClass: "workspace_files", minWidth: 130, where: options.where || "left" }); @@ -217,7 +217,7 @@ define(function(require, exports, module) { // Fetch UI elements container = plugin.getElement("container"); - winFilesViewer = plugin.getElement("winFilesViewer"); + winFilesViewer = options.aml // Create the Ace Tree tree = new Tree(container.$int); diff --git a/plugins/c9.ide.tree/tree.xml b/plugins/c9.ide.tree/tree.xml index 7728e596..6197f861 100644 --- a/plugins/c9.ide.tree/tree.xml +++ b/plugins/c9.ide.tree/tree.xml @@ -1,12 +1,10 @@ - - - - - - - - + + + + + + \ No newline at end of file From 9f09732f1548a08085899a772b619a0c76ea4fb3 Mon Sep 17 00:00:00 2001 From: Matthijs van Henten Date: Tue, 10 Mar 2015 10:35:39 +0000 Subject: [PATCH 05/38] gets rid of warnings, update to current version --- node_modules/c9/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node_modules/c9/package.json b/node_modules/c9/package.json index 86dd673e..8a96d768 100644 --- a/node_modules/c9/package.json +++ b/node_modules/c9/package.json @@ -6,11 +6,11 @@ "author": "Cloud9 Inc. ", "dependencies": { - "async": "~0.2.7", + "async": "~0.9.0", "tmp": "~0.0.20" }, "scripts": { "test": "for file in *_test.js; do node $file; done" } -} \ No newline at end of file +} From d90c3787b7071cd269edec4a1316feaee7d485b8 Mon Sep 17 00:00:00 2001 From: nightwing Date: Mon, 9 Mar 2015 15:54:24 +0400 Subject: [PATCH 06/38] make apf additions to Array.prototype non enumerable --- plugins/c9.ide.ui/lib_apf.js | 236 ++++++++--------------------------- 1 file changed, 49 insertions(+), 187 deletions(-) diff --git a/plugins/c9.ide.ui/lib_apf.js b/plugins/c9.ide.ui/lib_apf.js index 3566c786..7a18a4a9 100644 --- a/plugins/c9.ide.ui/lib_apf.js +++ b/plugins/c9.ide.ui/lib_apf.js @@ -2589,13 +2589,22 @@ apf.DATE = 5; apf.REGEXP = 6; apf.FUNCTION = 7; -Array.prototype.dataType = apf.ARRAY; -Number.prototype.dataType = apf.NUMBER; -Date.prototype.dataType = apf.DATE; -Boolean.prototype.dataType = apf.BOOLEAN; -String.prototype.dataType = apf.STRING; -RegExp.prototype.dataType = apf.REGEXP; -Function.prototype.dataType = apf.FUNCTION; +function defineProp(obj, name, val) { + Object.defineProperty(obj, name, { + value: val, + enumerable: false, + writable: true, + configurable: true, + }); +} + +defineProp(Array.prototype, "dataType", apf.ARRAY); +defineProp(Number.prototype, "dataType", apf.NUMBER); +defineProp(Date.prototype, "dataType", apf.DATE); +defineProp(Boolean.prototype, "dataType", apf.BOOLEAN); +defineProp(String.prototype, "dataType", apf.STRING); +defineProp(RegExp.prototype, "dataType", apf.REGEXP); +defineProp(Function.prototype, "dataType", apf.FUNCTION); /* @@ -2606,10 +2615,10 @@ Function.prototype.dataType = apf.FUNCTION; * @type Function * @see apf.extend */ -Function.prototype.extend = function() { +defineProp(Function.prototype, "extend", function() { apf.extend.apply(this, [this].concat(Array.prototype.slice.call(arguments))); return this; -}; +}); /* * Attach a Function object to an event as handler method. If apf.AbstractEvent @@ -2622,7 +2631,7 @@ Function.prototype.extend = function() { * @type Function * @see apf.AbstractEvent */ -Function.prototype.bindWithEvent = function() { +defineProp(Function.prototype, "bindWithEvent", function() { var __method = this, args = Array.prototype.slice.call(arguments), o = args.shift(), @@ -2634,48 +2643,19 @@ Function.prototype.bindWithEvent = function() { return __method.apply(o, [event].concat(args) .concat(Array.prototype.slice.call(arguments))); } -}; - -/* - * The bind function creates a new function (a bound function) that calls the - * function that is its this value (the bound function's target function) with - * a specified this parameter, which cannot be overridden. bind also accepts - * leading default arguments to provide to the target function when the bound - * function is called. A bound function may also be constructed using the new - * operator: doing so acts as though the target function had instead been - * constructed. The provided this value is ignored, while prepended arguments - * are provided to the emulated function. - * - * @param {Object} context The 'this' context of the bound function - * @type Function - */ -if (!Function.prototype.bind) - Function.prototype.bind = function(context /*, arg1, arg2... */) { - if (typeof this !== 'function') throw new TypeError(); - var _arguments = Array.prototype.slice.call(arguments, 1), - _this = this, - _concat = Array.prototype.concat, - _function = function() { - return _this.apply(this instanceof _dummy ? this : context, - _concat.apply(_arguments, arguments)); - }, - _dummy = function() {}; - _dummy.prototype = _this.prototype; - _function.prototype = new _dummy(); - return _function; -}; +}); /* * Copy an array, like this statement would: 'this.concat([])', but then do it * recursively. */ -Array.prototype.copy = function(){ +defineProp(Array.prototype, "copy", function(){ var ar = []; for (var i = 0, j = this.length; i < j; i++) ar[i] = this[i] && this[i].copy ? this[i].copy() : this[i]; return ar; -}; +}); /* * Concatenate the current Array instance with one (or more) other Arrays, like @@ -2685,13 +2665,13 @@ Array.prototype.copy = function(){ * @param {Array} array1, array2, array3, etc. * @type {Array} */ -Array.prototype.merge = function(){ +defineProp(Array.prototype, "merge", function(){ for (var i = 0, k = arguments.length; i < k; i++) { for (var j = 0, l = arguments[i].length; j < l; j++) { this.push(arguments[i][j]); } } -}; +}); /* * Add the values of one or more arrays to the current instance by using the @@ -2701,7 +2681,7 @@ Array.prototype.merge = function(){ * @type {Array} * @see Array.copy */ -Array.prototype.arrayAdd = function(){ +defineProp(Array.prototype, "arrayAdd", function(){ var s = this.copy(); for (var i = 0, k = arguments.length; i < k; i++) { for (var j = 0, l = s.length; j < l; j++) { @@ -2710,7 +2690,7 @@ Array.prototype.arrayAdd = function(){ } return s; -}; +}); /* * Check if an object is contained within the current Array instance. @@ -2718,12 +2698,12 @@ Array.prototype.arrayAdd = function(){ * @param {Mixed} obj The value to check for inside the Array * @type {Boolean} */ -Array.prototype.equals = function(obj) { +defineProp(Array.prototype, "equals", function(obj) { for (var i = 0, j = this.length; i < j; i++) if (this[i] != obj[i]) return false; return true; -}; +}); /* * Make sure that an array instance contains only unique values (NO duplicates). @@ -2821,7 +2801,7 @@ var uniqueBenvie = function(){ }(); if (typeof Set !== "undefined") { - Array.prototype.makeUnique = function(){ + defineProp(Array.prototype, "makeUnique", function(){ var out = [], seen = new Set, i = this.length; @@ -2834,12 +2814,12 @@ if (typeof Set !== "undefined") { } return out; - } + }); } else { - Array.prototype.makeUnique = function(){ + defineProp(Array.prototype, "makeUnique", function(){ return uniqueBenvie(this); - }; + }); } /* @@ -2850,45 +2830,10 @@ else { * @type {Boolean} * @see Array.indexOf */ -Array.prototype.contains = function(obj, from) { +defineProp(Array.prototype, "contains", function(obj, from) { return this.indexOf(obj, from) != -1; -}; +}); -/* - * Search for the index of the first occurence of a value 'obj' inside an array - * instance. - * July 29, 2008: added 'from' argument support to indexOf() - * - * @param {Mixed} obj The value to search for inside the array - * @param {Number} [from] Left offset index to start the search from - * @type {Number} - */ -Array.prototype.indexOf = Array.prototype.indexOf || function(obj, from) { - var len = this.length; - for (var i = (from < 0) ? Math.max(0, len + from) : from || 0; i < len; i++) { - if (this[i] === obj) - return i; - } - return -1; -}; - -/* - * Search for the index of the last occurence of a value 'obj' inside an array - * instance. - * - * @param {Mixed} obj The value to search for inside the array - * @param {Number} [from] Left offset index to start the search from - * @type {Number} - */ -Array.prototype.lastIndexOf = Array.prototype.lastIndexOf || function(obj, from) { - //same as indexOf(), but in reverse loop, JS spec 1.6 - var len = this.length; - for (var i = (from >= len) ? len - 1 : (from < 0) ? from + len : len - 1; i >= 0; i--) { - if (this[i] === obj) - return i; - } - return -1; -}; /* * Like Array.push, but only invoked when the value 'item' is already present @@ -2897,7 +2842,7 @@ Array.prototype.lastIndexOf = Array.prototype.lastIndexOf || function(obj, from) * @param {Mixed} item, item, ... * @type {Array} */ -Array.prototype.pushUnique = function(){ +defineProp(Array.prototype, "pushUnique", function(){ var item, i = 0, l = arguments.length; @@ -2907,7 +2852,7 @@ Array.prototype.pushUnique = function(){ this.push(item); } return this; -}; +}); /* * @todo: Ruben: could you please comment on this function? Seems to serve a very @@ -2915,7 +2860,7 @@ Array.prototype.pushUnique = function(){ * * I also could not find an occurrence in our codebase. */ -Array.prototype.search = function(){ +defineProp(Array.prototype, "search", function(){ for (var i = 0, length = arguments.length; i < length; i++) { if (typeof this[i] != "array") continue; @@ -2926,7 +2871,7 @@ Array.prototype.search = function(){ return this[i]; } } -}; +}); /* * Iterate through each value of an array instance from left to right (front to @@ -2935,13 +2880,12 @@ Array.prototype.search = function(){ * @param {Function} fn * @type {Array} */ -Array.prototype.each = -Array.prototype.forEach = Array.prototype.forEach || function(fn) { +defineProp(Array.prototype, "each", function(fn) { for (var i = 0, l = this.length; i < l; i++) if (fn.call(this, this[i], i, this) === false) break; return this; -} +}); /* * Search for a value 'obj' inside an array instance and remove it when found. @@ -2949,7 +2893,7 @@ Array.prototype.forEach = Array.prototype.forEach || function(fn) { * @type {Mixed} obj * @type {Array} */ -Array.prototype.remove = function(obj) { +defineProp(Array.prototype, "remove", function(obj) { for (var i = this.length - 1; i >= 0; i--) { if (this[i] != obj) continue; @@ -2958,7 +2902,7 @@ Array.prototype.remove = function(obj) { } return this; -}; +}); /* * Remove an item from an array instance which can be identified with key 'i' @@ -2966,10 +2910,10 @@ Array.prototype.remove = function(obj) { * @param {Number} i * @return {Mixed} The removed item */ -Array.prototype.removeIndex = function(i) { +defineProp(Array.prototype, "removeIndex", function(i) { if (!this.length) return null; return this.splice(i, 1); -}; +}); /* * Insert a new value at a specific object; alias for Array.splice. @@ -2978,9 +2922,9 @@ Array.prototype.removeIndex = function(i) { * @param {Number} i Index to insert 'obj' at * @type {Number} */ -Array.prototype.insertIndex = function(obj, i) { +defineProp(Array.prototype, "insertIndex", function(obj, i) { this.splice(i, 0, obj); -}; +}); /* * Reverses the order of the elements of an array; the first becomes the last, @@ -2988,89 +2932,7 @@ Array.prototype.insertIndex = function(obj, i) { * * @type {Array} */ -Array.prototype.invert = -Array.prototype.reverse = Array.prototype.reverse || function(){ - var l = this.length - 1; - for (var temp, i = 0; i < Math.ceil(0.5 * l); i++) { - temp = this[i]; - this[i] = this[l - i] - this[l - i] = temp; - } - - return this; -}; - - - -/* - * Attempt to fully comply (in terms of functionality) with the JS specification, - * up 'till version 1.7: - * @link http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Array - */ - -/* - * Creates a new array with all of the elements of this array for which the - * provided filtering function returns true. - * - * @param {Function} fn Function to test each element of the array. - * @param {Object} bind Object to use as this when executing callback. - * @type {Array} - */ -Array.prototype.filter = Array.prototype.filter || function(fn, bind) { - var results = []; - for (var i = 0, l = this.length; i < l; i++) { - if (fn.call(bind, this[i], i, this)) - results.push(this[i]); - } - return results; -}; - -/* - * Returns true if every element in this array satisfies the provided testing - * function. - * - * @param {Function} fn Function to test for each element. - * @param {Object} bind Object to use as this when executing callback. - * @type {Boolean} - */ -Array.prototype.every = Array.prototype.every || function(fn, bind) { - for (var i = 0, l = this.length; i < l; i++) { - if (!fn.call(bind, this[i], i, this)) - return false; - } - return true; -}; - -/* - * Creates a new array with the results of calling a provided function on every - * element in this array. - * - * @param {Function} fn Function that produces an element of the new Array from an element of the current one. - * @param {Object} bind Object to use as this when executing callback. - * @type {Array} - */ -Array.prototype.map = Array.prototype.map || function(fn, bind) { - var results = []; - for (var i = 0, l = this.length; i < l; i++) - results[i] = fn.call(bind, this[i], i, this); - return results; -}; - -/* - * Tests whether some element in the array passes the test implemented by the - * provided function. - * - * @param {Function} fn Function to test for each element. - * @param {Object} bind Object to use as this when executing callback. - * @type {Boolean} - */ -Array.prototype.some = Array.prototype.some || function(fn, bind) { - for (var i = 0, l = this.length; i < l; i++) { - if (fn.call(bind, this[i], i, this)) - return true; - } - return false; -}; +defineProp(Array.prototype, "invert", Array.prototype.reverse); @@ -22453,8 +22315,8 @@ apf.runNonIe = function (){ //Document.prototype.onreadystatechange = null; Document.prototype.parseError = 0; - Array.prototype.item = function(i){return this[i];}; - Array.prototype.expr = ""; + defineProp(Array.prototype, "item", function(i){return this[i];}); + defineProp(Array.prototype, "expr", ""); /*try{ XMLDocument.prototype.readyState = 0; From 8bfc04c5aa1e8ca33b162f2e4a1414248f0ea957 Mon Sep 17 00:00:00 2001 From: nightwing Date: Tue, 10 Mar 2015 00:46:40 +0400 Subject: [PATCH 07/38] remove custom tab style rule when tab is closed --- plugins/c9.ide.editors/tab.js | 16 +++++++++++----- plugins/c9.ide.ui/lib_apf.js | 9 +++++++++ plugins/c9.ide.ui/ui.js | 4 ++++ 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/plugins/c9.ide.editors/tab.js b/plugins/c9.ide.editors/tab.js index cba64a38..1c197575 100644 --- a/plugins/c9.ide.editors/tab.js +++ b/plugins/c9.ide.editors/tab.js @@ -40,7 +40,7 @@ define(function(require, module, exports) { } amlTab && amlTab.setAttribute("class", this.names.join(" ")); } - } + }; function initStyleSheet(fg, bg) { var cssClass = plugin.name.replace(/[^a-zA-Z0-9\-_\u00A0-\uFFFF]/g, "-"); @@ -49,10 +49,14 @@ define(function(require, module, exports) { rule = "." + cssClass + ".curbtn .tab_middle, ." + cssClass + ".curbtn .tab_middle::after, ." + cssClass + ".curbtn .tab_middle::before"; - - ui.importStylesheet([ - [rule, "background-color:" + (bg || "inherit") + ";" - + "color:" + (fg || "inherit") + ";"] + if (!bg) bg = "inherit"; + if (!fg) fg = "inherit"; + + ( + ui.setStyleRule(rule, "background-color", bg, stylesheet) && + ui.setStyleRule(rule, "foreground-color", fg, stylesheet) + ) || ui.importStylesheet([ + [rule, "background-color:" + bg + ";" + "color:" + fg + ";"] ], window, stylesheet); } @@ -280,6 +284,8 @@ define(function(require, module, exports) { plugin.on("unload", function(e) { closed = true; + if (rule) + ui.removeStyleRule(rule, stylesheet); // If there are no more pages left, reset location var last = amlPane.getPages().length === 0; if (last) diff --git a/plugins/c9.ide.ui/lib_apf.js b/plugins/c9.ide.ui/lib_apf.js index 7a18a4a9..41703d3a 100644 --- a/plugins/c9.ide.ui/lib_apf.js +++ b/plugins/c9.ide.ui/lib_apf.js @@ -3786,6 +3786,15 @@ apf.setStyleRule = function(name, type, value, stylesheet, win) { return !!rule; }; +apf.removeStyleRule = function(name, stylesheet, win) { + var rule = findCssRule(name, stylesheet, win); + if (rule) { + var i = Array.prototype.indexOf.call(rule.parentStyleSheet.cssRules, rule); + if (i != -1) + rule.parentStyleSheet.deleteRule(i); + } + return !!rule; +} /** * This method gets a single CSS rule. * @param {String} name The CSS name of the rule (i.e. `.cls` or `#id`). diff --git a/plugins/c9.ide.ui/ui.js b/plugins/c9.ide.ui/ui.js index b1623901..f44aec7c 100644 --- a/plugins/c9.ide.ui/ui.js +++ b/plugins/c9.ide.ui/ui.js @@ -636,6 +636,10 @@ define(function(require, module, exports) { */ setStyleRule: apf.setStyleRule, + /** + * @ignore + */ + removeStyleRule: apf.removeStyleRule, /** * @ignore */ From e2b9fc5b3879c468e65e68c90f5da4f44cbb7ac6 Mon Sep 17 00:00:00 2001 From: nightwing Date: Tue, 10 Mar 2015 00:47:43 +0400 Subject: [PATCH 08/38] cleanup --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 96cc49bd..61463672 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ }, "licenses": [], "c9plugins": { - "c9.ide.language": "#34de5e56ae", + "c9.ide.language": "#8cc3cba38e", "c9.ide.language.css": "#afda1f867c", "c9.ide.language.generic": "#87a4a44671", "c9.ide.language.html": "#fa4833e117", From 60680beae5f08c5c6c21240c320d8aafa0386dee Mon Sep 17 00:00:00 2001 From: nightwing Date: Tue, 10 Mar 2015 18:05:03 +0400 Subject: [PATCH 09/38] fix node 0.12 use strict error --- scripts/makestatic.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/makestatic.js b/scripts/makestatic.js index d640ac78..d7198542 100755 --- a/scripts/makestatic.js +++ b/scripts/makestatic.js @@ -56,7 +56,10 @@ function main(config, settings, options, callback) { var plugins = require(config)(settings, optimist(process.argv)) .map(function(plugin) { + if (typeof plugin == "string") + plugin = { packagePath: plugin }; plugin.packaging = true; + if (plugin.packagePath == "connect-architect/connect") { plugin.packagePath = "./c9.static/connect"; } From 1da498156ae0c2e540d21ff267f807b30b8bb6e4 Mon Sep 17 00:00:00 2001 From: nightwing Date: Tue, 10 Mar 2015 20:14:36 +0400 Subject: [PATCH 10/38] fix require for paths ending with .js in the worker --- node_modules/ace/lib/ace/worker/worker.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/node_modules/ace/lib/ace/worker/worker.js b/node_modules/ace/lib/ace/worker/worker.js index 564dec61..65f690c3 100644 --- a/node_modules/ace/lib/ace/worker/worker.js +++ b/node_modules/ace/lib/ace/worker/worker.js @@ -71,7 +71,8 @@ window.require = function(parentId, id) { if (!window.require.tlns) return console.log("unable to load " + id); chunks[0] = window.require.tlns[chunks[0]] || chunks[0]; - var path = chunks.join("/") + ".js"; + var path = chunks.join("/"); + if (path.slice(-3) != ".js") path += ".js"; window.require.id = id; window.require.modules[id] = {}; // prevent infinite loop on broken modules From fbcd10e618e11df67d77012d6451ed11fbd6542e Mon Sep 17 00:00:00 2001 From: nightwing Date: Tue, 10 Mar 2015 20:31:07 +0400 Subject: [PATCH 11/38] fix +6546: Add Alt-Shift-T to reopen the last closed tab --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 61463672..be9116d6 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "c9.ide.ace.split": "#0ae0151c78", "c9.ide.ace.statusbar": "#d7b45bb7c3", "c9.ide.ace.stripws": "#34426a03d1", - "c9.ide.behaviors": "#3dbcad4203", + "c9.ide.behaviors": "#6aad7006a0", "c9.ide.closeconfirmation": "#a28bfd8272", "c9.ide.configuration": "#b8470f4107", "c9.ide.dialog.wizard": "#a588b64050", From 6060d44764b22a70ce5cd19c2c9e8b728c0cc781 Mon Sep 17 00:00:00 2001 From: nightwing Date: Sat, 7 Mar 2015 00:23:19 +0400 Subject: [PATCH 12/38] fix +6465 Disable cmd+r in editor --- plugins/c9.ide.keys/commands.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/plugins/c9.ide.keys/commands.js b/plugins/c9.ide.keys/commands.js index a8f88a09..f1e30fe9 100644 --- a/plugins/c9.ide.keys/commands.js +++ b/plugins/c9.ide.keys/commands.js @@ -60,7 +60,7 @@ define(function(require, exports, module) { group: "ignore", bindKey: { win: "F12|Ctrl-Shift-I", - mac: "F12|Cmd-`|Cmd-R|Cmd-Option-I|Cmd-H|Cmd-M" + mac: "F12|Cmd-`|Cmd-Option-I|Cmd-H|Cmd-M" }, exec: function(){}, passEvent: true, @@ -69,8 +69,8 @@ define(function(require, exports, module) { name: "cancelBrowserAction", group: "ignore", bindKey: { - mac: "Command-S", - win: "Ctrl-S|Alt-Left|Alt-Right", + mac: "Command-S|Cmd-R", + win: "Ctrl-S|Ctrl-R|Alt-Left|Alt-Right", position: -10000 }, exec: function(){}, @@ -308,12 +308,7 @@ define(function(require, exports, module) { function getExceptionList(){ // Whitelist certain IDE keys for use from terminal and preview - return [{ - bindKey: { win: "F12|Ctrl-Shift-I", mac: "F12|Cmd-`|Cmd-R|Cmd-Option-I|Cmd-H|Cmd-M" }, - name: "passKeysToBrowser", - passEvent: true, - exec: function(){} - }, + return [ { bindKey: { win: null, mac: "Command-O" }, name: "navigateAlt", From c3d3d451da72e99f03d50acd2cdf4ffabc4c1387 Mon Sep 17 00:00:00 2001 From: nightwing Date: Tue, 10 Mar 2015 20:51:09 +0400 Subject: [PATCH 13/38] fix +6032 pasting line numbers to navigate doesn't work --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index be9116d6..6939f453 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "c9.ide.immediate": "#e9ba147cc2", "c9.ide.installer": "#24e7d6f399", "c9.ide.mount": "#32e79866ee", - "c9.ide.navigate": "#eab638cc2f", + "c9.ide.navigate": "#d4cccbcab4", "c9.ide.newresource": "#9a7464cc47", "c9.ide.openfiles": "#28a4f5af16", "c9.ide.preview": "#3c4dded23f", From ae7a32dd0f73ca0d50e3fd5f6f587055e3939bab Mon Sep 17 00:00:00 2001 From: nightwing Date: Tue, 10 Mar 2015 20:56:20 +0400 Subject: [PATCH 14/38] fix +5853 Emmet doesn't work with handlebars template files --- node_modules/ace/lib/ace/ext/emmet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node_modules/ace/lib/ace/ext/emmet.js b/node_modules/ace/lib/ace/ext/emmet.js index ceb155ef..4faacef2 100644 --- a/node_modules/ace/lib/ace/ext/emmet.js +++ b/node_modules/ace/lib/ace/ext/emmet.js @@ -393,7 +393,7 @@ exports.updateCommands = function(editor, enabled) { }; exports.isSupportedMode = function(modeId) { - return modeId && /css|less|scss|sass|stylus|html|php|twig|ejs/.test(modeId); + return modeId && /css|less|scss|sass|stylus|html|php|twig|ejs|handlebars/.test(modeId); }; var onChangeMode = function(e, target) { From 89d1e2f7197f59444a1de60d4afb14b86b8fe00d Mon Sep 17 00:00:00 2001 From: nightwing Date: Tue, 10 Mar 2015 21:29:20 +0400 Subject: [PATCH 15/38] fix emmet in sdk --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 6939f453..7d8809b9 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "connect": "~2.12.0", "debug": "~0.7.4", "ejs": "~0.8.3", + "emmet": "git://github.com/cloud9ide/emmet-core.git#cb4a2dcf86", "engine.io": "~1.5.1", "engine.io-client": "~1.5.1", "eslint": "git://github.com/cloud9ide/eslint.git#e2d052aafd81ea0aa6d1d4fd9f88f3613e386160", From d403fedbcfd9c2a3de58c4707eae3a4c08f2e318 Mon Sep 17 00:00:00 2001 From: nightwing Date: Tue, 10 Mar 2015 21:50:36 +0400 Subject: [PATCH 16/38] remove dead code --- plugins/c9.ide.ui/lib_apf.js | 593 ----------------------------------- 1 file changed, 593 deletions(-) diff --git a/plugins/c9.ide.ui/lib_apf.js b/plugins/c9.ide.ui/lib_apf.js index 41703d3a..c7893793 100644 --- a/plugins/c9.ide.ui/lib_apf.js +++ b/plugins/c9.ide.ui/lib_apf.js @@ -6448,12 +6448,6 @@ apf.extend(apf.config, { }, - "login" : function(value, x) { - apf.auth.init(x); - }, - - - "debug" : function(value) { @@ -27071,593 +27065,6 @@ apf.aml.setElement("appsettings", apf.appsettings); -/** - * @define auth Centralized authentication handling. Not being logged in, after being - * offline for a while can put the application - * in a complex undefined state. The auth element makes sure the state is always - * properly managed. When it gets signalled 'authentication required' it dispatches the - * appropriate events to display a login box. It can automatically retry logging - * in to one or more services using in memory stored username/password - * combinations. It will queue all requests that require authentication until - * the application is logged in again and will then empty the queue. - * Example: - * This example sets up apf.auth with two services that it can log into. - * - * - * - * - * - * - * - * - * Example: - * A login window with different states managed by apf.auth - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * Username - * - * - * Password - * - * - * - * Log in - * - * Log out - * - * - * @event beforelogin Fires before the log in request is sent to the service - * cancelable: Prevents the log in from happening - * @event beforelogout Fires before the log out request is sent to the service - * cancelable: Prevents the log out from happening - * @event logincheck Fires when log in data is received. Login is sometimes very complex, this event is dispatched to allow a custom check if a log in succeeded. - * bubbles: yes - * object: - * {Object} data the data received from the log in request - * {Number} state the return code of the log in request - * @event loginfail Fires when a log in attempt has failed - * @event loginsuccess Fires when a log in attempt succeeded - * @event logoutcheck Fires when log out data is received. Login is sometimes very complex, this event is dispatched to allow a custom check if a log out succeeded. - * bubbles: yes - * object: - * {Object} data the data received from the log out request - * {Number} state the return code of the log out request - * @event logoutfail Fires when a log out attempt has failed - * @event logoutsuccess Fires when a log out attempt succeeded - * @event authrequired Fires when log in credentials are required, either because they are incorrect, or because they are unavailable. - * bubbles: yes - * - * @inherits apf.Class - * - * @attribute {String} login the {@link term.datainstruction data instruction} on how to log in to a service. - * @attribute {String} logout the {@link term.datainstruction data instruction} on how to log out of a service. - * @attribute {Boolean} autostart whether to fire authrequired at startup. Defaults to true. - * @attribute {String} window the id of the window element that offers a log in form to the user. DEPRECATED. - * @attribute {String} authreq-state the id of the state element which is activated when logging in failed because the credentials where incorrect. - * @attribute {String} login-state the id of the state element which is activated when logging in succeeded. - * @attribute {String} waiting-state the id of the state element which is activated when the user is waiting while the application is logging in. - * @attribute {String} fail-state the id of the state element which is activated when logging in failed because the credentials where incorrect. - * @attribute {String} error-state the id of the state element which is activated when logging in failed because of an error (i.e. network disconnected). - * @attribute {String} logout-state the id of the state element which is activated when the user is logged out. - * @attribute {String} model the id of the model element which gets the data loaded given at login success. - * @attribute {String} remember whether to remember the login credentials after the first successful login attempt. Will only be used i.c.w. RPC - * @allowchild service - * @define service Element specifying a server to log into. - * @attribute {String} name the unique identifier of the service - * @attribute {String} login the {@link term.datainstruction data instruction} on how to log in to a service - * @attribute {String} logout the {@link term.datainstruction data instruction} on how to log out of a service - * @see apf.appsettings - * - * @default_private - */ - -apf.auth = function(struct, tagName) { - this.$init(tagName || "auth", apf.NODE_HIDDEN, struct); - - this.$services = {}; - this.$cache = {}; - this.$queue = []; - this.$credentials = null; -}; - -apf.aml.setElement("auth", apf.auth); - -(function(){ - this.autostart = true; - this.authenticated = false; - this.enablequeue = false; - - this.$retry = true; - this.loggedIn = false; - this.$needsLogin = false; - this.$hasHost = false; - - /** - * Indicates the state of the log in process. - * Possible values: - * 0 idle - * 1 logging in - * 2 logging out - */ - this.inProcess = 0; - - //1 = force no bind rule, 2 = force bind rule - this.$attrExcludePropBind = apf.extend({ - login: 1, - logout: 1 - }, this.$attrExcludePropBind); - - this.$booleanProperties["autostart"] = true; - this.$booleanProperties["remember"] = true; - - this.$supportedProperties.push("login", "logout", "fail-state", "error-state", - "login-state", "logout-state", "waiting-state", "window", "autostart", - "remember", "authenticated", "enablequeue"); - - this.$propHandlers["login"] = - this.$propHandlers["login-state"] = function(value) { - this.$services["default"] = value ? this : null; - this.$needsLogin = value ? true : false; - }; - - this.register = function(node) { - this.$services[node.name] = node; - this.$needsLogin = true; - }; - - this.unregister = function(node) { - var prop; - delete this.$services[node.name]; - if (!(prop in this.$services)) - this.$needsLogin = false; - }; - - this.addEventListener("DOMNodeInsertedIntoDocument", function(e) { - this.inited = true; - - if (this.parentNode && this.parentNode.$setAuth) { - this.parentNode.$setAuth(this); - this.$hasHost = true; - } - - if (this.autostart && !this.$hasHost) { - var _self = this; - apf.addEventListener("load", function(){ - apf.addEventListener("login", function(){ - _self.authRequired(); - apf.removeEventListener("load", arguments.callee); - }); - }); - } - }); - - this.addEventListener("authrequired", function(){ - if (self[this.window]) { - this.win = self[this.window]; - if (this.win) { - this.win.show(); - return false; - } - } - - if (self[this["authreq-state"]]) { - this.state = self[this["authreq-state"]]; - if (this.state) { - this.state.activate(); - return false; - } - } - }); - - this.addEventListener("beforelogin", function(){ - if (self[this["waiting-state"]]) { - this.state = self[this["waiting-state"]]; - if (this.state) - this.state.activate(); - } - }); - - var knownHttpAuthErrors = {401:1, 403:1} - function failFunction(e) { - var st = (e.state == apf.TIMEOUT || !knownHttpAuthErrors[e.status] - ? self[this["error-state"]] - : self[this["fail-state"]]) || self[this["fail-state"]] - - if (st) { - this.state = st; - if (this.state) { - this.state.activate(); - return false; - } - } - } - - this.addEventListener("loginfail", failFunction); - this.addEventListener("logoutfail", failFunction); - - this.addEventListener("logoutsuccess", function(){ - if (self[this["logout-state"]]) { - this.state = self[this["logout-state"]]; - if (this.state) - this.state.activate(); - } - }); - - this.addEventListener("loginsuccess", function(e) { - if (self[this.window]) { - this.win = self[this.window]; - if (this.win) - this.win.hide(); - } - - if (self[this["login-state"]]) { - this.state = self[this["login-state"]]; - if (this.state) - this.state.activate(); - } - - - if (e.data && this.model) { - this.model = apf.nameserver.get("model", this.model); - if (this.model) - this.model.load(e.data); - } - - }); - - /** - * Log in to one or more services - * @param {String} username the username portion of the credentials used to log in with - * @param {String} password the password portion of the credentials used to log in with - * @param {Function} [callback] code to be called when the application succeeds or fails logging in - * @param {Object} [options] extra settings and variables for the login. These variables will be available in the {@link term.datainstruction data instruction} which is called to execute the actual log in. - * Properties: - * {Array} services a list of names of services to be logged in to - * {String} service the name of a single service to log in to - */ - this.logIn = function(username, password, callback, options) { - if (!options) options = {}; - - options.username = username; - options.password = password; - - if (this.dispatchEvent("beforelogin", options) === false) - return false; - - this.inProcess = 1; //Logging in - - var pos = 0, - len = 0, - _self = this, - doneCallback = function() { - if (len != ++pos) - return; - - _self.inProcess = 0; //Idle - _self.loggedIn = true; - _self.clearQueue(); - - if (callback) - callback(); - }; - - if (this.$hasHost) { // child of Teleport element - this.$credentials = options; - callback = this.$hostCallback; - this.$hostCallback = null; - len = 1; - doneCallback(); - this.dispatchEvent("loginsuccess", { - state: 1, - data: null, - bubbles: true, - username: username, - password: password - }); - if (!this.remember) - this.$credentials = null; - } - else { - if (!options.service) { - var s = options.$services || this.$services; - for (var name in s) { - len++; - this.$do(name, options, "in", null, doneCallback); - } - } - else if (options.service) { - len = 1; - this.$do(options.service, options, "in", null, doneCallback); - } - } - }; - - this.relogin = function(){ - if (this.dispatchEvent("beforerelogin") === false) - return false; - - - - //@todo shouldn't I be using inProces here? - var name, pos = 0, len = 0, _self = this, - doneCallback = function(){ - if (len != ++pos) - return; - - _self.inProcess = 0; //Idle - _self.loggedIn = true; - _self.clearQueue(); - }; - - for (name in this.$services) { - if (!this.$cache[name]) - return false; - len++; - this.$do(name, this.$cache[name], "in", true, doneCallback); - } - - return true; - }; - - this.$do = function(service, options, type, isRelogin, callback) { - var xmlNode = this.$services[service], - _self = options.userdata = this; - - - - - - //Execute login call - options.callback = function(data, state, extra) { - if (state == apf.TIMEOUT && extra.retries < apf.maxHttpRetries) - return extra.tpModule.retry(extra.id); - - /* - Login is sometimes very complex, so this check is - here to test the data for login information - */ - var result = _self.dispatchEvent("log" + type + "check", - apf.extend({ - state: state, - data: data, - service: service, - bubbles: true - }, extra)), - - loginFailed = typeof result == "boolean" - ? !result - : !(state == apf.SUCCESS || type == "out" && extra.status == 401); - - if (loginFailed) { - _self.inProcess = 0; //Idle - - if (isRelogin) //If we're retrying then we'll step out here - return _self.authRequired(); - - - - var commError = new Error(apf.formatErrorString(0, null, - "Logging " + type, "Error logging in: " + extra.message)); - - if (_self.dispatchEvent("log" + type + "fail", apf.extend({ - error: commError, - service: service, - state: state, - data: data, - bubbles: true, - username: options.username, - password: options.password - }, extra)) !== false) - throw commError; //@todo ouch, too harsh? - - //@todo Call auth required again?? - - _self.setProperty("authenticated", false); - - return; - } - - if (type == "in") { - //If we use retry, cache the login information - if (!isRelogin && _self.$retry) { - var cacheItem = {}; - for (var prop in options) { - if ("object|array".indexOf(typeof options[prop]) == -1) - cacheItem[prop] = options[prop]; - } - _self.$cache[service || "default"] = cacheItem; - } - } - else { - //Remove cached credentials - if (_self.$cache[service || "default"]) - _self.$cache[service || "default"] = null; - - //_self.authRequired(); - } - - if (callback) - callback(); - - _self.dispatchEvent("log" + type + "success", apf.extend({}, extra, { - state: state, - service: service, - data: data, - bubbles: true, - username: options.username, - password: options.password - })); - - - - _self.setProperty("authenticated", true); - }; - apf.saveData(xmlNode.getAttribute("log" + type), options); - }; - - this.clearQueue = function(){ - if (!this.loggedIn) //Queue should only be cleared when we're logged in - return; - - var queue = this.$queue.slice(); - this.$queue.length = 0; - - for (var i = 0; i < queue.length; i++) { - var qItem = queue[i]; - - //We might be logged out somewhere in this process (think sync) - if (!this.loggedIn) { - this.$queue.push(qItem); - continue; - } - - //Specialty retry (protocol specific) - if (qItem.retry) - qItem.$retry.call(qItem.object); - - //Standard TelePort Module retry - else if (qItem.id) - qItem.tpModule.retry(qItem.id); - - - } - - //The queue might be filled somehow - if (this.$queue.length) - this.clearQueue(); - }; - - /** - * Log out of one or more services - * @param {Function} [callback] code to be called when the application succeeds or fails logging out - * @param {Object} [options] extra settings and variables for the login. These variables will be available out the {@link term.datainstruction data instruction} which is called to execute the actual log out. - * Properties: - * {Array} services a list of names of services to be logged out of - * {String} service the name of a single service to log out of - */ - this.logOut = function(callback, options) { - if (!options) options = {}; - - if (this.dispatchEvent("beforelogout", options) === false) - return; - - this.loggedIn = false; - - if (!options.service) { - for (var name in this.$services) - this.$do(name, options, "out", null, callback); - } - else if (options.service) - this.$do(options.service, options, "out", null, callback); - - }; - - this.getCredentials = function(service) { - var cache = this.$cache[service || "default"]; - return !cache ? ["", ""] : [cache.username, cache.password]; - }; - - /** - * Signals services that a log in is required and fires authrequired event - * @param {Object} [options] information on how to reconstruct a failed action, that detected a log in was required. (i.e. When an HTTP call fails with a 401 Auth Required the options object contains information on how to retry the http request) - * @param {Object} [forceNoRetry] don't try to log in with stored credentials. - */ - this.authRequired = function(options, forceNoRetry) { - // If we're already logging in return - if (options && options.userdata == this) - return; - - // If we're supposed to be logged in we'll try to log in automatically - if (this.loggedIn && !forceNoRetry && this.$retry && this.relogin()) { - var result = false; - } - else if (this.inProcess != 1) { //If we're not already logging in - if (this.$hasHost && typeof options == "function") { //inside Teleport element - if (this.$credentials) - return options(); - this.$hostCallback = options; - } - /* - Apparently our credentials aren't valid anymore, - or retry is turned off. If this event returns false - the developer will call apf.auth.login() at a later date. - */ - var result = this.dispatchEvent("authrequired", apf.extend({ - bubbles: true, - data: options && options.data - }, options)); - } - - this.loggedIn = false; - - if (result === false) { - if (this.enablequeue && options) //Add communication to queue for later processing - this.$queue.push(options); - - return true; //cancels error state in protocol - } - }; - -}).call(apf.auth.prototype = new apf.AmlElement()); - - - - - - - /** * This element displays a skinnable rectangle which can contain other From 7299a29c06d92cf8c159e91fbc7353e5cdb61aa5 Mon Sep 17 00:00:00 2001 From: Brady Dowling Date: Tue, 10 Mar 2015 12:36:05 -0600 Subject: [PATCH 17/38] Removed forward slashes Removed forward slashes causing runner to dysfunction. See https://github.com/c9/core/issues/26#issuecomment-77999985 --- configs/client-default-local.js | 158 ++++++++++++++++++ local/install.sh | 242 ---------------------------- package.json | 48 +++--- plugins/c9.ide.login/login.css | 36 +++++ plugins/c9.ide.login/login.js | 194 ++++++++++++++++++++++ plugins/c9.login/legacy.js | 123 -------------- plugins/c9.login/legacy_strategy.js | 73 --------- plugins/c9.login/login.js | 135 ---------------- plugins/c9.login/strategy.js | 45 ------ 9 files changed, 412 insertions(+), 642 deletions(-) create mode 100644 configs/client-default-local.js delete mode 100755 local/install.sh create mode 100644 plugins/c9.ide.login/login.css create mode 100644 plugins/c9.ide.login/login.js delete mode 100644 plugins/c9.login/legacy.js delete mode 100644 plugins/c9.login/legacy_strategy.js delete mode 100644 plugins/c9.login/login.js delete mode 100644 plugins/c9.login/strategy.js diff --git a/configs/client-default-local.js b/configs/client-default-local.js new file mode 100644 index 00000000..b2ed60a1 --- /dev/null +++ b/configs/client-default-local.js @@ -0,0 +1,158 @@ +var join = require("path").join; + +module.exports = function(options) { + var config = require("./client-default")(options); + return module.exports.makeLocal(config, options); +}; + +module.exports.makeLocal = function(config, options) { + var c9Ws = options.remoteWorkspace; // true when opening c9 workspace as local + var root = options.workspaceDir; + + var nodeBin = options.nodeBin || ["node"]; + var settingDir = options.settingDir || options.installPath; + + if (!c9Ws) { + // Local version + options.local = true; + options.projectName = root.substr(root.lastIndexOf("/") + 1); + options.debug = 2; + options.env = "local"; + } + + for (var i = config.length - 1; i >= 0; i--) { + // if (config[i].packagePath == "plugins/c9.cli.bridge/bridge") + // config[i].port = 55556; + if (config[i].packagePath == "plugins/c9.ide.welcome/welcome" && !c9Ws) { + config[i].intro = + "Welcome to your brand new Cloud9. Use this welcome screen " + + "to tweak the look & feel of the Cloud9 user interface. " + + "If you prefer a more advanced IDE experience, you can choose " + + "to change the layout below. " + + "\n\n" + + "On the right you can find videos and documentation for Cloud9 " + + "IDE. Happy Coding!"; + } + // else if (config[i].packagePath == "plugins/c9.ide.login/login") { + // config.splice(i, 1); + // } + else if (config[i].packagePath == "plugins/c9.ide.run/run" && !c9Ws) { + config[i].runnerPath = join(settingDir, "/runners"); + } + else if (config[i].packagePath == "plugins/c9.ide.ui/menus") { + config[i].autoInit = false; + } + else if (config[i].packagePath == "plugins/c9.ide.save/autosave") { + config[i].slowChangeTimeout = 500; + } + else if (config[i].packagePath == "plugins/c9.ide.run.build/build" && !c9Ws) { + config[i].builderPath = join(settingDir, "/builders"); + } + else if (config[i].packagePath == "plugins/c9.ide.editors/metadata" && !c9Ws) { + config[i].path = join(settingDir, "/metadata"); + config[i].changeCheckInterval = 2000; + } + else if (config[i].packagePath == "plugins/c9.ide.feedback/feedback") { + config[i].screenshotSupport = false; + } + // else if (config[i].packagePath == "plugins/c9.ide.feedback/feedback") { + // config[i] = { + // packagePath : "plugins/c9.ide.help/help", + // staticPrefix : options.staticPrefix + "/plugins/c9.ide.help" + // }; + // } + + else if (config[i].packagePath == "plugins/c9.core/c9") { + config[i].local = true; + } + else if (config[i].packagePath == "plugins/c9.ide.clipboard/html5") + config[i].packagePath = "plugins/c9.ide.local/clipboard"; + else if (config[i].packagePath == "plugins/c9.ide.configuration/configure") + config[i].pathFromFavorite = true; + else if (config[i].packagePath == "plugins/c9.core/settings" && !c9Ws) { + // todo: Don't show console when opening a file? + // config[i].template = ; + config[i].projectConfigPath = join(settingDir, ""); + config[i].userConfigPath = join(settingDir, ""); + config[i].stateConfigPath = join(settingDir, ""); + } else if (config[i].packagePath == "plugins/c9.ide.log/log" && !c9Ws) { + config[i].source = "desktop"; + } else if (config[i].packagePath == "plugins/c9.ide.info/info" && c9Ws) { + config[i].packagePath = "plugins/c9.ide.local/info"; + } else if (config[i].packagePath == "plugins/c9.ide.ui/menus" && c9Ws) { + config[i].autoInit = false; + } else if (config[i].packagePath == "plugins/c9.ide.tree/tree") { + config[i].defaultExpanded = !config.hosted; + } + } + + // Add local modules + var includes = [{ + packagePath: "plugins/c9.ide.local/local", + options: options, + }, { + packagePath: "plugins/c9.ide.local/windowframe", + staticPrefix: options.staticPrefix + "/plugins/c9.ide.local" + }, { + packagePath: "plugins/c9.ide.local/update", + host: options.update && options.update.host || "localhost", // "update.c9.io", + port: options.update && options.update.port || "8888", // "443" + path: options.update && options.update.path, + protocol: options.update && options.update.protocol, + installPath: options.correctedInstallPath, + bashBin: options.bashBin, + nodeBin: nodeBin + }, { + packagePath: "plugins/c9.ide.local/projectmanager" + }, { + packagePath: "plugins/c9.ide.local/open" + }, { + packagePath: "plugins/c9.ide.local/nativemenus" + }, !c9Ws && { + packagePath: "plugins/c9.ide.local/info", + installPath: options.correctedInstallPath, + settingDir: settingDir, + cookie: options.user.cookie, + user: { + id: options.user.id, + name: options.user.name, + fullname: options.user.fullname, + email: options.user.email, + pubkey: options.user.pubkey + }, + project: { + id: options.project.id, + name: options.project.name, + contents: options.project.contents, + descr: options.project.descr + } + }].filter(Boolean); + + var excludes = c9Ws ? {} : { + "plugins/c9.ide.newresource/open": true, + "plugins/c9.ide.info/info": true, + // "plugins/c9.ide.login/login": true, + "plugins/c9.ide.collab/connect": true, + "plugins/c9.ide.collab/collab": true, + "plugins/c9.ide.collab/collabpanel": true, + "plugins/c9.ide.collab/workspace": true, + "plugins/c9.ide.collab/util": true, + "plugins/c9.ide.collab/ot/document": true, + "plugins/c9.ide.collab/cursor_layer": true, + "plugins/c9.ide.collab/author_layer": true, + "plugins/c9.ide.collab/timeslider/timeslider": true, + "plugins/c9.ide.notifications/notifications": true, + "plugins/c9.ide.collab/members/members_panel": true, + "plugins/c9.ide.collab/share/share": true, + "plugins/c9.ide.collab/members/members": true, + "plugins/c9.ide.collab/chat/chat": true, + "plugins/c9.ide.feedback/nps": true, + "plugins/c9.ide.download/download": true + }; + + config = config.concat(includes).filter(function (p) { + return !excludes[p] && !excludes[p.packagePath]; + }); + + return config; +}; diff --git a/local/install.sh b/local/install.sh deleted file mode 100755 index 4075cb6c..00000000 --- a/local/install.sh +++ /dev/null @@ -1,242 +0,0 @@ -#!/bin/bash -e -set -e -has() { - type "$1" > /dev/null 2>&1 - return $? -} - -# Redirect stdout ( > ) into a named pipe ( >() ) running "tee" -exec > >(tee /tmp/installlog.txt) - -# Without this, only stdout would be captured - i.e. your -# log file would not contain any error messages. -exec 2>&1 - -NODE_VERSION=v0.10.28 -APPSUPPORT_USER=$HOME/.c9 -SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -RUNTIME=$SCRIPT/.. -INSTALL_DIR=/tmp/c9-`date '+%s'` -ORIGINAL_USER=`basename $HOME` -OSX_INSTALLER_PATH=$2 - -start() { - if [ $# -lt 1 ]; then - start base - return - fi - - # Try to figure out the os and arch for binary fetching - local uname="$(uname -a)" - local os= - local arch="$(uname -m)" - case "$uname" in - Linux\ *) os=linux ;; - Darwin\ *) os=darwin ;; - SunOS\ *) os=sunos ;; - FreeBSD\ *) os=freebsd ;; - esac - case "$uname" in - *x86_64*) arch=x64 ;; - *i*86*) arch=x86 ;; - *armv6l*) arch=arm-pi ;; - esac - - if [ $os != "linux" ] && [ $os != "darwin" ]; then - echo "Unsupported Platform: $os $arch" 1>&2 - exit 1 - fi - - if [ $arch != "x64" ] && [ $arch != "x86" ]; then - echo "Unsupported Architecture: $os $arch" 1>&2 - exit 1 - fi - - if [ $os == "darwin" ]; then - APPSUPPORT_USER="$HOME/Library/Application Support/Cloud9" - APPTARGET=$OSX_INSTALLER_PATH - APPSUPPORT="/Library/Application Support/Cloud9" - RUNTIME="${APPTARGET}/Contents/Resources/app.nw" - fi - - case $1 in - "help" ) - echo - echo "Cloud9 Installer" - echo - echo "Usage:" - echo " install help Show this message" - echo " install install [name [name ...]] Download and install a set of packages" - echo " install ls List available packages" - echo - ;; - - "ls" ) - echo "!node - Node.js" - echo "!tmux_install - TMUX" - echo "!nak - NAK" - echo "!vfsextend - VFS extend" - echo "!ptyjs - pty.js" - echo "!c9cli - C9 CLI" - echo "!sc - Sauce Connect" - echo "coffee - Coffee Script" - echo "less - Less" - echo "sass - Sass" - echo "typescript - TypeScript" - echo "stylus - Stylus" - # echo "go - Go" - # echo "heroku - Heroku" - # echo "rhc - RedHat OpenShift" - # echo "gae - Google AppEngine" - ;; - - "install" ) - shift - - # make sure dirs are around - mkdir -p "$APPSUPPORT/bin" - mkdir -p "$APPSUPPORT/node_modules" - cd "$APPSUPPORT" - - cp -a "$SCRIPT" "$INSTALL_DIR" - - # install packages - while [ $# -ne 0 ] - do - time eval ${1} $os $arch - shift - done - - # finalize - #pushd $APPSUPPORT/node_modules/.bin - #for FILE in $APPSUPPORT/node_modules/.bin/*; do - # if [ `uname` == Darwin ]; then - # sed -i "" -E s:'#!/usr/bin/env node':"#!$NODE":g $(readlink $FILE) - # else - # sed -i -E s:'#!/usr/bin/env node':"#!$NODE":g $(readlink $FILE) - # fi - #done - #popd - - VERSION=`cat $RUNTIME/version || echo 1` - echo 1 > "$APPSUPPORT/installed" - echo $VERSION > "$APPSUPPORT/version" - - # set chown/chmod of application dirs for update - echo "Testing existence of APPTARGET (${APPTARGET})" - if [ -d "$APPTARGET" ]; then - echo "Updating permissions of APPTARGET (${APPTARGET})" - chown -R root:admin "$APPTARGET" || chown -R root:staff "$APPTARGET" - chmod -R 775 "$APPTARGET" - fi - - echo "Testing existence of APPSUPPORT (${APPSUPPORT})" - if [ -d "$APPSUPPORT" ]; then - echo "Updating permissions of APPSUPPORT (${APPSUPPORT})" - chown -R root:admin "$APPSUPPORT" || chown -R root:staff "$APPSUPPORT" - chmod -R 775 "$APPSUPPORT" - fi - - echo "Testing existence of APPSUPPORT_USER (${APPSUPPORT_USER})" - if [ -n "$ORIGINAL_USER" ] && [ -d "$APPSUPPORT_USER" ]; then - echo "Updating permissions of APPSUPPORT_USER (${APPSUPPORT_USER})" - chown -R $ORIGINAL_USER "$APPSUPPORT_USER" - fi - - rm -Rf $INSTALL_DIR - - echo :Done. - ;; - - "base" ) - echo "Installing base packages. Use '`basename $0` help' for more options" - start install node tmux_install nak ptyjs sc vfsextend c9cli - ;; - - * ) - start base - ;; - esac -} - -# NodeJS - -node(){ - # clean up - rm -rf node - rm -rf node-$NODE_VERSION* - - echo :Installing Node $NODE_VERSION - - cd "$INSTALL_DIR" - tar xvfz node-$NODE_VERSION-$1-$2.tar.gz - rm -Rf "$APPSUPPORT/node" - mv node-$NODE_VERSION-$1-$2 "$APPSUPPORT/node" -} - -tmux_install(){ - echo :Installing TMUX - mkdir -p "$APPSUPPORT/bin" - - if [ $os = "darwin" ]; then - cd "$INSTALL_DIR" - python rudix.py -i libevent-2.0.21-0.pkg - python rudix.py -i tmux-1.9-0.pkg - - if ! type "/usr/local/bin/tmux"; then - echo "Installation Failed" - exit 100 - fi - - ln -sf "/usr/local/bin/tmux" "$APPSUPPORT/bin/tmux" - # Linux - else - echo "Unsupported" - fi -} - -vfsextend(){ - echo :Installing VFS extend - cd "$INSTALL_DIR" - tar xvfz c9-vfs-extend.tar.gz - rm -Rf "$APPSUPPORT/c9-vfs-extend" - mv c9-vfs-extend "$APPSUPPORT" -} - -sc(){ - echo :Installing Sauce Connect - cd "$INSTALL_DIR" - tar xvzf sc-4.0-latest.tar.gz - rm -rf "$APPSUPPORT/sc" - mv sc-4.0-latest "$APPSUPPORT/sc" -} - -nak(){ - echo :Installing Nak - cd "$INSTALL_DIR" - tar -zxvf nak.tar.gz - mkdir -p "$APPSUPPORT/node_modules/.bin" - rm -Rf "$APPSUPPORT/node_modules/nak" - mv nak "$APPSUPPORT/node_modules" - ln -s "$APPSUPPORT/node_modules/nak/bin/nak" "$APPSUPPORT/node_modules/.bin/nak" &2> /dev/null -} - -ptyjs(){ - echo :Installing pty.js - cd "$INSTALL_DIR" - tar -zxvf pty-$NODE_VERSION-$1-$2.tar.gz - mkdir -p "$APPSUPPORT/node_modules" - rm -Rf "$APPSUPPORT/node_modules/pty.js" - mv pty.js "$APPSUPPORT/node_modules" -} - -c9cli(){ - if [ -d "/usr/local/bin/" ]; then - chmod +x "$RUNTIME/bin/c9" - ln -s -f "$RUNTIME/bin/c9" /usr/local/bin/c9 - else - echo "unable to add c9cli to the path" - fi -} - -start $@ diff --git a/package.json b/package.json index 8727ca2b..1cf6e203 100644 --- a/package.json +++ b/package.json @@ -47,31 +47,31 @@ }, "licenses": [], "c9plugins": { - "c9.ide.language": "#9f588f9152", - "c9.ide.language.css": "#89deece6c0", - "c9.ide.language.generic": "#50161ba888", - "c9.ide.language.html": "#e393db66ae", - "c9.ide.language.html.diff": "#31965d98b3", - "c9.ide.language.javascript": "#5b6237f875", - "c9.ide.language.javascript.immediate": "#0f7c640825", - "c9.ide.language.javascript.eslint": "#8fbaa9cc96", - "c9.ide.language.javascript.tern": "#3d678a103a", - "c9.ide.language.javascript.infer": "#1ae097af44", - "c9.ide.language.jsonalyzer": "#45a20496be", + "c9.ide.language": "#854575579b", + "c9.ide.language.css": "#afda1f867c", + "c9.ide.language.generic": "#87a4a44671", + "c9.ide.language.html": "#fa4833e117", + "c9.ide.language.html.diff": "#a7311cfc9f", + "c9.ide.language.javascript": "#26cf518b28", + "c9.ide.language.javascript.immediate": "#9a2cce9121", + "c9.ide.language.javascript.eslint": "#1baacc275b", + "c9.ide.language.javascript.tern": "#a65ad88dd9", + "c9.ide.language.javascript.infer": "#ebb2daf81a", + "c9.ide.language.jsonalyzer": "#c5dfe5fb7e", "c9.ide.collab": "#116fe74942", - "c9.ide.local": "#d5c324ee5b", - "c9.ide.find": "#be3bca94b7", - "c9.ide.find.infiles": "#462928475c", - "c9.ide.find.replace": "#fe41fa768d", - "c9.ide.run.debug": "#9a05fadc55", + "c9.ide.local": "#2bfd7ff051", + "c9.ide.find": "#989c06e6a7", + "c9.ide.find.infiles": "#f98dfef554", + "c9.ide.find.replace": "#e4daf722b8", + "c9.ide.run.debug": "#f5d664c787", "c9.ide.ace.emmet": "#e5f1a92ac3", "c9.ide.ace.gotoline": "#4d1a93172c", - "c9.ide.ace.keymaps": "#422e83553b", - "c9.ide.ace.repl": "#26bca9ee17", + "c9.ide.ace.keymaps": "#6c4bb65b1f", + "c9.ide.ace.repl": "#ada99852fa", "c9.ide.ace.split": "#0ae0151c78", "c9.ide.ace.statusbar": "#d7b45bb7c3", "c9.ide.ace.stripws": "#34426a03d1", - "c9.ide.behaviors": "#f5aaf10aff", + "c9.ide.behaviors": "#6aad7006a0", "c9.ide.closeconfirmation": "#a28bfd8272", "c9.ide.configuration": "#b8470f4107", "c9.ide.dialog.wizard": "#a588b64050", @@ -81,21 +81,21 @@ "c9.ide.imgeditor": "#08bbc53578", "c9.ide.immediate": "#e9ba147cc2", "c9.ide.installer": "#24e7d6f399", - "c9.ide.mount": "#c9d598b254", - "c9.ide.navigate": "#7985b5a50b", + "c9.ide.mount": "#32e79866ee", + "c9.ide.navigate": "#64156c7f4a", "c9.ide.newresource": "#9a7464cc47", "c9.ide.openfiles": "#28a4f5af16", "c9.ide.preview": "#3c4dded23f", "c9.ide.preview.browser": "#be197b0464", "c9.ide.preview.markdown": "#bf952685f6", - "c9.ide.pubsub": "#7dd0a37571", + "c9.ide.pubsub": "#92ec19ed3a", "c9.ide.readonly": "#f6f07bbe42", "c9.ide.recentfiles": "#7c099abf40", "c9.ide.remote": "#37773d905b", "c9.ide.run": "#0a8cc7f0a9", - "c9.ide.run.build": "#6726030127", + "c9.ide.run.build": "#ea47baab4d", "c9.ide.save": "#a32a8f4346", - "c9.ide.terminal.monitor": "#8e025b3ae1", + "c9.ide.terminal.monitor": "#df9936daa2", "c9.ide.theme.flat": "#5c7c27ab74", "c9.ide.threewaymerge": "#229382aa0b", "c9.ide.undo": "#b028bcb4d5", diff --git a/plugins/c9.ide.login/login.css b/plugins/c9.ide.login/login.css new file mode 100644 index 00000000..3a5d245e --- /dev/null +++ b/plugins/c9.ide.login/login.css @@ -0,0 +1,36 @@ +.c9-menu-btn.titlebar{ + position: absolute; + z-index: 100000000; + right: 24px; + top: 1px; + height: 19px; +} + +.c9-menu-btn.titlebar:not(.c9-menu-btnDown){ + height: 18px; + border-bottom: 1px solid black; +} + +.fullscreen>.btnName{ + display: none; +} + +.btnName.c9-menu-btnIcon{ + text-indent: -2000px; + overflow: hidden; +} +.btnName{ + padding: @menu-name-button-padding; + margin-left: @menu-name-button-diff !important; +} +.btnName.c9-menu-btnmenuDown{ + margin-left: 0 !important; +} +.btnName .icon{ + background-size: @menu-name-button-icon-width @menu-name-button-icon-height !important; + top: @menu-name-button-icon-top !important; + left: @menu-name-button-icon-left !important; + width: @menu-name-button-icon-width; + height: @menu-name-button-icon-height; + border-radius: @menu-name-button-icon-border-radius; +} \ No newline at end of file diff --git a/plugins/c9.ide.login/login.js b/plugins/c9.ide.login/login.js new file mode 100644 index 00000000..817480a5 --- /dev/null +++ b/plugins/c9.ide.login/login.js @@ -0,0 +1,194 @@ +define(function(require, exports, module) { + main.consumes = [ + "Plugin", "ui", "menus", "info", "layout", "http", "util", + "vfs.endpoint", "auth", "dialog.alert", "c9" + ]; + main.provides = ["login"]; + return main; + + function main(options, imports, register) { + var Plugin = imports.Plugin; + var ui = imports.ui; + var c9 = imports.c9; + var menus = imports.menus; + var layout = imports.layout; + var http = imports.http; + var util = imports.util; + var info = imports.info; + var auth = imports.auth; + var alert = imports["dialog.alert"].show; + + var vfsEndpoint = imports["vfs.endpoint"]; + + /***** Initialization *****/ + + var ideBaseUrl = options.ideBaseUrl; + var dashboardUrl = options.dashboardUrl; + var accountUrl = options.accountUrl; + var lastUser, mnuUser; + + var plugin = new Plugin("Ajax.org", main.consumes); + var emit = plugin.getEmitter(); + + var loaded = false; + function load() { + if (loaded) return false; + loaded = true; + + info.getUser(function(err, user) { + updateButton({user: user}); + }); + + auth.on("relogin", onReLogin); + } + + /***** Methods *****/ + + function updateButton(e) { + var user = e.user; + if (lastUser && lastUser.id == user.id) + return; + plugin.cleanUp(); + info.on("change", updateButton, plugin); + createButton(user); + lastUser = user; + + emit.sticky("ready", { name: user.fullname, id: user.id }, plugin); + } + + function createButton(user) { + var name = "user_" + user.id; + + // todo cleanup seems to not work well + // without this menu is empty after logging out and back in + if (lastUser) + menus.remove("user_" + lastUser.id); + menus.remove(name); + + var parent = layout.findParent(plugin); + + // Insert CSS + ui.insertCss(require("text!./login.css"), plugin); + + // Create Menu + mnuUser = new ui.menu(); + plugin.addElement(mnuUser); + + // Add named button + var icon = util.getGravatarUrl(user.email, 32, ""); + menus.addItemByPath(name + "/", mnuUser, 110000, plugin); + + // Add Divider + ui.insertByIndex(parent, new ui.divider({ + skin: "c9-divider-double", + "class" : "extrasdivider" + }), 870, plugin); + + // Add sub menu items + var c = 500; + menus.addItemByPath(name + "/Dashboard", new ui.item({ + onclick: function() { window.open(dashboardUrl); } + }), c += 100, plugin); + menus.addItemByPath(name + "/Account", new ui.item({ + onclick: function() { window.open(accountUrl); } + }), c += 100, plugin); + menus.addItemByPath(name + "/Home", new ui.item({ + onclick: function() { window.open(ideBaseUrl); } + }), c += 100, plugin); + + if (!options.noLogout) { + menus.addItemByPath(name + "/~", new ui.divider(), c += 100, plugin); + menus.addItemByPath(name + "/Log out", new ui.item({ + onclick: function() { + if (!c9.local) + return signout(); + auth.logout(function() { + info.login(true); + }); + } + }), c += 100, plugin); + } + + var button = menus.get(name).item; + button.setAttribute("class", "btnName"); + button.setAttribute("icon", icon); + button.setAttribute("iconsize", "16px 16px"); + button.setAttribute("tooltip", user.fullname); + button.setAttribute("caption", user.fullname); + ui.insertByIndex(parent, button, 600, plugin); + + if (c9.local) { + function minimize(){ + apf.document.documentElement.appendChild(button); + ui.setStyleClass(button.$ext, "titlebar"); + } + function restore(){ + ui.insertByIndex(parent, button, 870, plugin); + ui.setStyleClass(button.$ext, "", ["titlebar"]); + } + + menus.on("minimize", minimize, plugin); + menus.on("restore", restore, plugin); + + if (menus.minimized) + minimize(); + } + } + + function signout() { + vfsEndpoint.clearCache(); + auth.logout(function() { location.href = ideBaseUrl; }); + } + + function onReLogin() { + if (!c9.local) { + alert("Logged out", + "You have been logged in as a different user", + "Please hit OK to reload the IDE.", + function() { + vfsEndpoint.clearCache(); + auth.logout(function() { + document.location.reload(); + }); + }); + } + } + + /***** Lifecycle *****/ + + plugin.on("load", function() { + load(); + }); + plugin.on("enable", function() { + + }); + plugin.on("disable", function() { + + }); + plugin.on("unload", function() { + loaded = false; + }); + + /***** Register and define API *****/ + + /** + * + **/ + plugin.freezePublicAPI({ + get menu(){ return mnuUser; }, + + _events: [ + /** + * @event ready + */ + "ready" + ], + createButton: createButton, + updateButton: updateButton + }); + + register(null, { + login: plugin + }); + } +}); \ No newline at end of file diff --git a/plugins/c9.login/legacy.js b/plugins/c9.login/legacy.js deleted file mode 100644 index aba436fe..00000000 --- a/plugins/c9.login/legacy.js +++ /dev/null @@ -1,123 +0,0 @@ -"use strict"; - -var assert = require("assert"); -var url = require("url"); -var Cloud9LegayStrategy = require("./legacy_strategy"); -var cookieSignature = require("cookie-signature"); -var decrypt = require("c9/crypt").decrypt; -var login = require("connect-ensure-login"); - -plugin.consumes = [ - "db", - "passport", - "connect.redirect", - "connect.cookieparser", - "session-store" -]; -plugin.provides = ["c9.login"]; - -module.exports = plugin; - -function plugin(options, imports, register) { - assert(options.appId, "Option 'appId' is required"); - assert(options.ideBaseUrl, "Option 'ideBaseUrl' is required"); - assert(options.baseUrl, "Option 'baseUrl' is required"); - assert(options.ssoCookie, "Option 'ssoCookie' is required"); - assert(options.ssoSecret, "Option 'ssoSecret' is required"); - - var db = imports.db; - var passport = imports.passport; - var sessionStore = imports["session-store"]; - - // use the 'proxy' cookie to have federated logout - passport.useStart(function(req, res, next) { - var hash; - // anonymous login - if (!req.cookies || !(hash = req.cookies[options.ssoCookie])) - return done(); - - var encrypted = cookieSignature.unsign(hash, options.ssoSecret); - if (!encrypted) - return done(); - - var sessionId = decrypt(encrypted, options.ssoSecret); - - sessionStore.get(sessionId, function(err, session) { - if (err) return done(err); - done(null, session && session.uid); - }); - - function done(err, ssoUid) { - if (err) return next(err); - ssoUid = ssoUid || -1; - var session = req.session; - if (session && session.passport && session.passport.user && session.passport.user != ssoUid) { - return req.session.regenerate(function(err) { - if (err) return next(err); - - if (session.returnTo) - req.session.returnTo = session.returnTo; - - delete req.user; - next(); - }); - } - else { - if (!req.session.passport) - req.session.passport = {}; - - req.session.passport.user = ssoUid; - next(); - } - } - }); - - var cloud9Strategy = new Cloud9LegayStrategy({ - clientID: options.appId, - ideBaseUrl: options.ideBaseUrl, - callback: options.baseUrl + "/auth/c9l/callback", - db: db - }); - - passport.use(cloud9Strategy); - - passport.section.get("/c9l", passport.authenticate("c9l")); - passport.section.get("/c9l/callback", [ - passport.authenticate("c9l"), - function(req, res, next) { - var user = req.user; - - if (user) { - req.login(user, function(err) { - if (err) return next(err); - res.returnTo(req, "/"); - }); - } - else { - res.redirect("/auth/c9l"); - } - } - ]); - - register(null, { - "c9.login": { - ensureLoggedIn: function() { - return function(req, res, next) { - var redirect = options.baseUrl + "/_auth/c9l"; - var nonce = req.parsedUrl.query.__c9_preview_id__; - - if (nonce) { - redirect += "?nonce=" + encodeURIComponent(nonce); - delete req.parsedUrl.query.__c9_preview_id__; - delete req.parsedUrl.search; - req.originalUrl = url.format(req.parsedUrl); - } - - login.ensureLoggedIn({ - redirectTo: redirect - })(req, res, next); - }; - } - } - }); -} \ No newline at end of file diff --git a/plugins/c9.login/legacy_strategy.js b/plugins/c9.login/legacy_strategy.js deleted file mode 100644 index bdc78f30..00000000 --- a/plugins/c9.login/legacy_strategy.js +++ /dev/null @@ -1,73 +0,0 @@ -var passport = require('passport'); -var util = require('util'); -var InternalOAuthError = require("passport-oauth").InternalOAuthError; - - -function Cloud9Legacy(options) { - passport.Strategy.call(this); - this.name = 'c9l'; - - this.clientID = options.clientID; - this.ideBaseUrl = options.ideBaseUrl; - this.callback = options.callback; - this.db = options.db; -} - -/** - * Inherit from `passport.Strategy`. - */ -util.inherits(Cloud9Legacy, passport.Strategy); - -/** - * Authenticate request based on the contents of a HTTP Basic authorization - * header. - * - * @param {Object} req - * @api protected - */ -Cloud9Legacy.prototype.authenticate = function(req, options) { - var that = this; - options = options || {}; - - // the callback handler - if (req.query && req.query.code) { - this.db.AccessToken - .findOne({ - token: req.query.code - }) - .populate("user") - .exec(function(err, token) { - if (err) - return that.error(new InternalOAuthError('failed to obtain access token', err)); - - req.session.token = req.query.code; - that.success(token.user); - }); - return; - } - - var nonce = req.parsedUrl.query.nonce; - if (nonce) { - this.redirect( - this.ideBaseUrl + - "/api/nc/auth" + - "?response_type=nonce" + - "&client_id=" + encodeURIComponent(this.clientID + "_nonce") + - "&nonce=" + encodeURIComponent(nonce) - ); - } - else { - this.redirect( - this.ideBaseUrl + - "/api/nc/auth" + - "?response_type=token" + - "&client_id=" + encodeURIComponent(this.clientID) + - "&login_hint=" + encodeURIComponent(options.loginHint || "") - ); - } -}; - -/** - * Expose `Cloud9Legacy`. - */ -module.exports = Cloud9Legacy; \ No newline at end of file diff --git a/plugins/c9.login/login.js b/plugins/c9.login/login.js deleted file mode 100644 index 45d67244..00000000 --- a/plugins/c9.login/login.js +++ /dev/null @@ -1,135 +0,0 @@ -"use strict"; - -var assert = require("assert"); -var frontdoor = require("frontdoor"); -var cookie = require("cookie"); -var Passport = require("passport").Passport; -var Cloud9Strategy = require("./strategy"); - -plugin.consumes = [ - "session", - "connect.redirect" -]; -plugin.provides = ["c9.login"]; - -module.exports = plugin; - -function plugin(options, imports, register) { - assert(options.appId, "Option 'appId' is required"); - assert(options.appSecret, "Option 'appSecret' is required"); - assert(options.callback, "Option 'callback' is required"); - assert(options.logout, "Option 'logout' is required"); - assert(options.baseUrl, "Option 'baseUrl' is required"); - assert(options.domain, "Option 'domain' is required"); - assert(options.ssoCookie, "Option 'ssoCookie' is required"); - assert(options.ssoCookie.name, "Option 'ssoCookie.name' is required"); - assert(options.ssoCookie.maxAge, "Option 'ssoCookie.maxAge' is required"); - - var session = imports.session; - var passport = new Passport(); - - session.use(passport.initialize()); - session.use(function(req, res, next) { - passport.session()(req, res, function(err) { - if (err) return next(err); - if (!req.user) return next(); - - var uid = req.cookies[options.ssoCookie.name]; - if (uid != req.user.id) { - req.logout(); - return next(); - } - - next(); - }); - }); - - passport.serializeUser(function(user, done) { - var id; - try { - id = JSON.stringify(user); - } - catch (e) { - return done(e); - } - done(null, id); - }); - - passport.deserializeUser(function(id, done) { - var user; - try { - user = JSON.parse(id); - } - catch (e) { - return done(e); - } - done(null, user); - }); - - var cloud9Strategy = new Cloud9Strategy({ - clientID: options.appId, - clientSecret: options.appSecret, - callbackURL: options.callback, - userProfileURL: options.userProfileURL, - baseUrl: options.baseUrl, - }, function(accessToken, refreshToken, params, profile, done) { - var user = { - id: profile.id, - username: profile.username, - fullname: profile.displayName ? profile.displayName.trim() : profile.username, - token: accessToken - }; - done(null, user); - }); - - passport.use(cloud9Strategy); - - var api = frontdoor(); - passport.section = api.section("auth"); - session.use(api); - - passport.section.get("/logout", function(req, res, next) { - res.redirect(options.baseUrl + "/logout?redirect_uri=" + encodeURIComponent(options.logout)); - }); - passport.section.get("/cloud9", passport.authenticate("cloud9")); - passport.section.get("/cloud9/callback", function(req, res, next) { - passport.authenticate("cloud9", function(err, user, info) { - if (err) return next(err); - - if (user) { - req.login(user, function(err) { - if (err) return next(err); - setCookie(res, req.user.id, options.ssoCookie.maxAge); - res.returnTo(req, "/"); - }); - } - else { - res.redirect("/auth/cloud9"); - } - - })(req, res, next); - }); - - passport.section.get("/cloud9/logout", function(req, res, next) { - req.logout(); - clearCookie(res); - res.redirect("/"); - }); - - function clearCookie(res) { - setCookie(res, "", new Date(1)); - } - function setCookie(res, value, ttl) { - res.setHeader("Set-Cookie", cookie.serialize(options.ssoCookie.name, value, { - domain: "." + options.domain, - path: "/", - expires: ttl instanceof Date ? ttl : new Date(Date.now() + ttl), - secure: true, - httpOnly: true - })); - } - - register(null, { - "c9.login": passport - }); -} \ No newline at end of file diff --git a/plugins/c9.login/strategy.js b/plugins/c9.login/strategy.js deleted file mode 100644 index 3443d5e4..00000000 --- a/plugins/c9.login/strategy.js +++ /dev/null @@ -1,45 +0,0 @@ -var util = require('util'); -var OAuth2Strategy = require('passport-oauth').OAuth2Strategy; -var InternalOAuthError = require('passport-oauth').InternalOAuthError; - -function Strategy(options, verify) { - options = options || {}; - var baseUrl = options.baseUrl || "https://auth.c9.io/oauth"; - - options.authorizationURL = baseUrl + "/authorize"; - options.tokenURL = baseUrl + "/access_token"; - options.scopeSeparator = ","; - - OAuth2Strategy.call(this, options, verify); - this.name = "cloud9"; - this._userProfileURL = options.userProfileURL || "https://api.c9.io/user"; -} - -util.inherits(Strategy, OAuth2Strategy); - -Strategy.prototype.userProfile = function(accessToken, done) { - this._oauth2.useAuthorizationHeaderforGET(true); - this._oauth2.get(this._userProfileURL, accessToken, function (err, body, res) { - if (err) - return done(new InternalOAuthError('failed to fetch user profile', err)); - - try { - var json = JSON.parse(body); - - var profile = { provider: "cloud9" }; - profile.id = json.id; - profile.displayName = json.name; - profile.username = json.login; - profile.emails = [{ value: json.email }]; - - profile._raw = body; - profile._json = json; - - done(null, profile); - } catch (e) { - done(e); - } - }); -}; - -module.exports = Strategy; \ No newline at end of file From 0f58974f6574ede9a1d0dc8aedb81e3f3005c994 Mon Sep 17 00:00:00 2001 From: nightwing Date: Wed, 11 Mar 2015 00:04:21 +0400 Subject: [PATCH 18/38] fix typo --- plugins/c9.ide.errorhandler/raygun.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/c9.ide.errorhandler/raygun.js b/plugins/c9.ide.errorhandler/raygun.js index 663567c7..fd3732a4 100644 --- a/plugins/c9.ide.errorhandler/raygun.js +++ b/plugins/c9.ide.errorhandler/raygun.js @@ -1419,7 +1419,7 @@ window.TraceKit = TraceKit; var blackListedErrors = { 'Error with empty message': {}, - 'Script error': {} + 'Script error.': {} }; function processUnhandledException(stackTrace, options) { var stack = [], From 1d892967189dc7e5184f600931a4a4f486c2e3ca Mon Sep 17 00:00:00 2001 From: Brady Dowling Date: Tue, 10 Mar 2015 15:03:54 -0600 Subject: [PATCH 19/38] Modified typescript builder I've implemented the changes mentioned in the previously referenced ticket and tested them. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1cf6e203..f806b330 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "c9.ide.recentfiles": "#7c099abf40", "c9.ide.remote": "#37773d905b", "c9.ide.run": "#0a8cc7f0a9", - "c9.ide.run.build": "#ea47baab4d", + "c9.ide.run.build": "#2ef7140669", "c9.ide.save": "#a32a8f4346", "c9.ide.terminal.monitor": "#df9936daa2", "c9.ide.theme.flat": "#5c7c27ab74", From ed6c46a839b23dc68a30a553fe3efb93c3bc788f Mon Sep 17 00:00:00 2001 From: Brady Dowling Date: Tue, 10 Mar 2015 15:04:58 -0600 Subject: [PATCH 20/38] Removed extraneous "--out" parameter --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f806b330..99ffd558 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "c9.ide.recentfiles": "#7c099abf40", "c9.ide.remote": "#37773d905b", "c9.ide.run": "#0a8cc7f0a9", - "c9.ide.run.build": "#2ef7140669", + "c9.ide.run.build": "#90a5f49175", "c9.ide.save": "#a32a8f4346", "c9.ide.terminal.monitor": "#df9936daa2", "c9.ide.theme.flat": "#5c7c27ab74", From a5d7037ca250099a0df9e4b9b85c9ee6b7e83487 Mon Sep 17 00:00:00 2001 From: Dan Armendariz Date: Tue, 10 Mar 2015 21:49:30 -0400 Subject: [PATCH 21/38] issue 6 fix, gdb's setState() properly sets state var --- local/install.sh | 242 ----------------------------------------------- package.json | 48 +++++----- 2 files changed, 24 insertions(+), 266 deletions(-) delete mode 100755 local/install.sh diff --git a/local/install.sh b/local/install.sh deleted file mode 100755 index 4075cb6c..00000000 --- a/local/install.sh +++ /dev/null @@ -1,242 +0,0 @@ -#!/bin/bash -e -set -e -has() { - type "$1" > /dev/null 2>&1 - return $? -} - -# Redirect stdout ( > ) into a named pipe ( >() ) running "tee" -exec > >(tee /tmp/installlog.txt) - -# Without this, only stdout would be captured - i.e. your -# log file would not contain any error messages. -exec 2>&1 - -NODE_VERSION=v0.10.28 -APPSUPPORT_USER=$HOME/.c9 -SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -RUNTIME=$SCRIPT/.. -INSTALL_DIR=/tmp/c9-`date '+%s'` -ORIGINAL_USER=`basename $HOME` -OSX_INSTALLER_PATH=$2 - -start() { - if [ $# -lt 1 ]; then - start base - return - fi - - # Try to figure out the os and arch for binary fetching - local uname="$(uname -a)" - local os= - local arch="$(uname -m)" - case "$uname" in - Linux\ *) os=linux ;; - Darwin\ *) os=darwin ;; - SunOS\ *) os=sunos ;; - FreeBSD\ *) os=freebsd ;; - esac - case "$uname" in - *x86_64*) arch=x64 ;; - *i*86*) arch=x86 ;; - *armv6l*) arch=arm-pi ;; - esac - - if [ $os != "linux" ] && [ $os != "darwin" ]; then - echo "Unsupported Platform: $os $arch" 1>&2 - exit 1 - fi - - if [ $arch != "x64" ] && [ $arch != "x86" ]; then - echo "Unsupported Architecture: $os $arch" 1>&2 - exit 1 - fi - - if [ $os == "darwin" ]; then - APPSUPPORT_USER="$HOME/Library/Application Support/Cloud9" - APPTARGET=$OSX_INSTALLER_PATH - APPSUPPORT="/Library/Application Support/Cloud9" - RUNTIME="${APPTARGET}/Contents/Resources/app.nw" - fi - - case $1 in - "help" ) - echo - echo "Cloud9 Installer" - echo - echo "Usage:" - echo " install help Show this message" - echo " install install [name [name ...]] Download and install a set of packages" - echo " install ls List available packages" - echo - ;; - - "ls" ) - echo "!node - Node.js" - echo "!tmux_install - TMUX" - echo "!nak - NAK" - echo "!vfsextend - VFS extend" - echo "!ptyjs - pty.js" - echo "!c9cli - C9 CLI" - echo "!sc - Sauce Connect" - echo "coffee - Coffee Script" - echo "less - Less" - echo "sass - Sass" - echo "typescript - TypeScript" - echo "stylus - Stylus" - # echo "go - Go" - # echo "heroku - Heroku" - # echo "rhc - RedHat OpenShift" - # echo "gae - Google AppEngine" - ;; - - "install" ) - shift - - # make sure dirs are around - mkdir -p "$APPSUPPORT/bin" - mkdir -p "$APPSUPPORT/node_modules" - cd "$APPSUPPORT" - - cp -a "$SCRIPT" "$INSTALL_DIR" - - # install packages - while [ $# -ne 0 ] - do - time eval ${1} $os $arch - shift - done - - # finalize - #pushd $APPSUPPORT/node_modules/.bin - #for FILE in $APPSUPPORT/node_modules/.bin/*; do - # if [ `uname` == Darwin ]; then - # sed -i "" -E s:'#!/usr/bin/env node':"#!$NODE":g $(readlink $FILE) - # else - # sed -i -E s:'#!/usr/bin/env node':"#!$NODE":g $(readlink $FILE) - # fi - #done - #popd - - VERSION=`cat $RUNTIME/version || echo 1` - echo 1 > "$APPSUPPORT/installed" - echo $VERSION > "$APPSUPPORT/version" - - # set chown/chmod of application dirs for update - echo "Testing existence of APPTARGET (${APPTARGET})" - if [ -d "$APPTARGET" ]; then - echo "Updating permissions of APPTARGET (${APPTARGET})" - chown -R root:admin "$APPTARGET" || chown -R root:staff "$APPTARGET" - chmod -R 775 "$APPTARGET" - fi - - echo "Testing existence of APPSUPPORT (${APPSUPPORT})" - if [ -d "$APPSUPPORT" ]; then - echo "Updating permissions of APPSUPPORT (${APPSUPPORT})" - chown -R root:admin "$APPSUPPORT" || chown -R root:staff "$APPSUPPORT" - chmod -R 775 "$APPSUPPORT" - fi - - echo "Testing existence of APPSUPPORT_USER (${APPSUPPORT_USER})" - if [ -n "$ORIGINAL_USER" ] && [ -d "$APPSUPPORT_USER" ]; then - echo "Updating permissions of APPSUPPORT_USER (${APPSUPPORT_USER})" - chown -R $ORIGINAL_USER "$APPSUPPORT_USER" - fi - - rm -Rf $INSTALL_DIR - - echo :Done. - ;; - - "base" ) - echo "Installing base packages. Use '`basename $0` help' for more options" - start install node tmux_install nak ptyjs sc vfsextend c9cli - ;; - - * ) - start base - ;; - esac -} - -# NodeJS - -node(){ - # clean up - rm -rf node - rm -rf node-$NODE_VERSION* - - echo :Installing Node $NODE_VERSION - - cd "$INSTALL_DIR" - tar xvfz node-$NODE_VERSION-$1-$2.tar.gz - rm -Rf "$APPSUPPORT/node" - mv node-$NODE_VERSION-$1-$2 "$APPSUPPORT/node" -} - -tmux_install(){ - echo :Installing TMUX - mkdir -p "$APPSUPPORT/bin" - - if [ $os = "darwin" ]; then - cd "$INSTALL_DIR" - python rudix.py -i libevent-2.0.21-0.pkg - python rudix.py -i tmux-1.9-0.pkg - - if ! type "/usr/local/bin/tmux"; then - echo "Installation Failed" - exit 100 - fi - - ln -sf "/usr/local/bin/tmux" "$APPSUPPORT/bin/tmux" - # Linux - else - echo "Unsupported" - fi -} - -vfsextend(){ - echo :Installing VFS extend - cd "$INSTALL_DIR" - tar xvfz c9-vfs-extend.tar.gz - rm -Rf "$APPSUPPORT/c9-vfs-extend" - mv c9-vfs-extend "$APPSUPPORT" -} - -sc(){ - echo :Installing Sauce Connect - cd "$INSTALL_DIR" - tar xvzf sc-4.0-latest.tar.gz - rm -rf "$APPSUPPORT/sc" - mv sc-4.0-latest "$APPSUPPORT/sc" -} - -nak(){ - echo :Installing Nak - cd "$INSTALL_DIR" - tar -zxvf nak.tar.gz - mkdir -p "$APPSUPPORT/node_modules/.bin" - rm -Rf "$APPSUPPORT/node_modules/nak" - mv nak "$APPSUPPORT/node_modules" - ln -s "$APPSUPPORT/node_modules/nak/bin/nak" "$APPSUPPORT/node_modules/.bin/nak" &2> /dev/null -} - -ptyjs(){ - echo :Installing pty.js - cd "$INSTALL_DIR" - tar -zxvf pty-$NODE_VERSION-$1-$2.tar.gz - mkdir -p "$APPSUPPORT/node_modules" - rm -Rf "$APPSUPPORT/node_modules/pty.js" - mv pty.js "$APPSUPPORT/node_modules" -} - -c9cli(){ - if [ -d "/usr/local/bin/" ]; then - chmod +x "$RUNTIME/bin/c9" - ln -s -f "$RUNTIME/bin/c9" /usr/local/bin/c9 - else - echo "unable to add c9cli to the path" - fi -} - -start $@ diff --git a/package.json b/package.json index f078729d..d01fb2de 100644 --- a/package.json +++ b/package.json @@ -49,31 +49,31 @@ }, "licenses": [], "c9plugins": { - "c9.ide.language": "#9f588f9152", - "c9.ide.language.css": "#89deece6c0", - "c9.ide.language.generic": "#50161ba888", - "c9.ide.language.html": "#e393db66ae", - "c9.ide.language.html.diff": "#31965d98b3", - "c9.ide.language.javascript": "#5b6237f875", - "c9.ide.language.javascript.immediate": "#0f7c640825", - "c9.ide.language.javascript.eslint": "#8fbaa9cc96", - "c9.ide.language.javascript.tern": "#3d678a103a", - "c9.ide.language.javascript.infer": "#1ae097af44", - "c9.ide.language.jsonalyzer": "#45a20496be", - "c9.ide.collab": "#116fe74942", - "c9.ide.local": "#d5c324ee5b", - "c9.ide.find": "#be3bca94b7", - "c9.ide.find.infiles": "#462928475c", - "c9.ide.find.replace": "#fe41fa768d", - "c9.ide.run.debug": "#f2d348f27a", + "c9.ide.language": "#854575579b", + "c9.ide.language.css": "#afda1f867c", + "c9.ide.language.generic": "#87a4a44671", + "c9.ide.language.html": "#fa4833e117", + "c9.ide.language.html.diff": "#a7311cfc9f", + "c9.ide.language.javascript": "#26cf518b28", + "c9.ide.language.javascript.immediate": "#9a2cce9121", + "c9.ide.language.javascript.eslint": "#1baacc275b", + "c9.ide.language.javascript.tern": "#a65ad88dd9", + "c9.ide.language.javascript.infer": "#ebb2daf81a", + "c9.ide.language.jsonalyzer": "#c5dfe5fb7e", + "c9.ide.collab": "#b94018ab2b", + "c9.ide.local": "#2bfd7ff051", + "c9.ide.find": "#989c06e6a7", + "c9.ide.find.infiles": "#f98dfef554", + "c9.ide.find.replace": "#e4daf722b8", + "c9.ide.run.debug": "#b48e0147a3", "c9.ide.ace.emmet": "#e5f1a92ac3", "c9.ide.ace.gotoline": "#4d1a93172c", - "c9.ide.ace.keymaps": "#422e83553b", - "c9.ide.ace.repl": "#26bca9ee17", + "c9.ide.ace.keymaps": "#6c4bb65b1f", + "c9.ide.ace.repl": "#ada99852fa", "c9.ide.ace.split": "#0ae0151c78", "c9.ide.ace.statusbar": "#d7b45bb7c3", "c9.ide.ace.stripws": "#34426a03d1", - "c9.ide.behaviors": "#f5aaf10aff", + "c9.ide.behaviors": "#6aad7006a0", "c9.ide.closeconfirmation": "#a28bfd8272", "c9.ide.configuration": "#b8470f4107", "c9.ide.dialog.wizard": "#a588b64050", @@ -83,21 +83,21 @@ "c9.ide.imgeditor": "#08bbc53578", "c9.ide.immediate": "#e9ba147cc2", "c9.ide.installer": "#24e7d6f399", - "c9.ide.mount": "#c9d598b254", - "c9.ide.navigate": "#7985b5a50b", + "c9.ide.mount": "#32e79866ee", + "c9.ide.navigate": "#64156c7f4a", "c9.ide.newresource": "#9a7464cc47", "c9.ide.openfiles": "#28a4f5af16", "c9.ide.preview": "#3c4dded23f", "c9.ide.preview.browser": "#be197b0464", "c9.ide.preview.markdown": "#bf952685f6", - "c9.ide.pubsub": "#7dd0a37571", + "c9.ide.pubsub": "#92ec19ed3a", "c9.ide.readonly": "#f6f07bbe42", "c9.ide.recentfiles": "#7c099abf40", "c9.ide.remote": "#37773d905b", "c9.ide.run": "#0a8cc7f0a9", "c9.ide.run.build": "#6726030127", "c9.ide.save": "#a32a8f4346", - "c9.ide.terminal.monitor": "#8e025b3ae1", + "c9.ide.terminal.monitor": "#df9936daa2", "c9.ide.theme.flat": "#5c7c27ab74", "c9.ide.threewaymerge": "#229382aa0b", "c9.ide.undo": "#b028bcb4d5", From 36e31246c41cf18c57fff0b14f59aee83c9c8c4b Mon Sep 17 00:00:00 2001 From: Fabian Jakobs Date: Wed, 11 Mar 2015 11:02:40 +0000 Subject: [PATCH 22/38] update netutil --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 96cc49bd..3c5f7d61 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "msgpack-js": "~0.1.1", "msgpack-js-browser": "~0.1.4", "nak": "", - "netutil": "~0.0.1", + "netutil": "~0.0.2", "optimist": "~0.6.0", "qs": "0.6.6", "rusha": "~0.7.2", From ef4b8fe93d67a3a3cd908aba1a4dd7fdbec15ca9 Mon Sep 17 00:00:00 2001 From: nightwing Date: Wed, 11 Mar 2015 16:06:10 +0400 Subject: [PATCH 23/38] remove old scripts --- scripts/initlocal.sh | 71 -------------------------------------------- 1 file changed, 71 deletions(-) delete mode 100755 scripts/initlocal.sh diff --git a/scripts/initlocal.sh b/scripts/initlocal.sh deleted file mode 100755 index edf9e11d..00000000 --- a/scripts/initlocal.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/bash -e - -NODE_VERSION=v0.10.21 - -cd $HOME -mkdir -p .c9/bin -mkdir -p .c9/node_modules -cd .c9 - -rm -rf node -rm -rf node-$NODE_VERSION* - -echo :Installing Node $NODE_VERSION -echo Downloading Node $NODE_VERSION... -curl -sSOL http://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-darwin-x64.tar.gz -tar xvfz node-$NODE_VERSION-darwin-x64.tar.gz -mv node-$NODE_VERSION-darwin-x64 node -rm node-$NODE_VERSION-darwin-x64.tar.gz - -NPM=$HOME/.c9/node/bin/npm -NODE=$HOME/.c9/node/bin/node - -echo :Installing pty.js -$NPM install pty.js -echo :Installing Nak -$NPM install https://github.com/c9/nak/tarball/ea1299a3688f307d2269c93bd9692101eb4f262e -echo :Installing Coffee Script -$NPM install coffee -echo :Installing Less -$NPM install less -echo :Installing Sass -$NPM install sass -echo :Installing TypeScript -$NPM install typescript - -echo :Correcting Paths -for FILE in $HOME/.c9/node_modules/.bin/* -do - perl -i -p -e 's/#!\/usr\/bin\/env node/#!'${NODE//\//\\\/}'/' $(readlink $FILE) -done - -echo :Installing TMUX -echo Downloading TMUX 1.6... -curl -sSOL http://downloads.sourceforge.net/tmux/tmux-1.6.tar.gz -echo Downloading Libevent 2.0... -curl -sSOL http://downloads.sourceforge.net/project/levent/libevent/libevent-2.0/libevent-2.0.16-stable.tar.gz - -# Unpack the sources - -tar xzf tmux-1.6.tar.gz -tar xzf libevent-2.0.16-stable.tar.gz - -# Compiling libevent - -cd libevent-2.0.16-stable -./configure --prefix=/opt -make -#sudo make install - -# Compiling tmux - -cd ../tmux-1.6 -LDFLAGS="-L/opt/lib" CPPFLAGS="-I/opt/include" LIBS="-lresolv" ./configure --prefix=/opt -make -#sudo make install - -mkdir -p ~/.c9/bin -cp ./tmux ~/.c9/bin/tmux - -echo 1 > $HOME/.c9/installed -echo :Done. \ No newline at end of file From d7f6d76f22c4e00b904e7cb1c92fbeca21e7889f Mon Sep 17 00:00:00 2001 From: nightwing Date: Wed, 11 Mar 2015 16:07:16 +0400 Subject: [PATCH 24/38] do not allow install-sdk.sh to silently fail --- scripts/install-sdk.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install-sdk.sh b/scripts/install-sdk.sh index 2ce6fe1a..0ef7f71b 100755 --- a/scripts/install-sdk.sh +++ b/scripts/install-sdk.sh @@ -69,7 +69,7 @@ updateAllPackages() { for m in ${c9packages[@]}; do echo $m; i=$(($i + 1)) echo "updating plugin ${blue}$i${resetColor} of ${blue}$count${resetColor}" - updatePackage $m || true + updatePackage $m done } From c506748bf00d33f76903f26f19594ff709f0ab97 Mon Sep 17 00:00:00 2001 From: Fabian Jakobs Date: Wed, 11 Mar 2015 12:10:19 +0000 Subject: [PATCH 25/38] some cleanup --- plugins/c9.error/raygun.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/c9.error/raygun.js b/plugins/c9.error/raygun.js index 19e81863..1681248f 100644 --- a/plugins/c9.error/raygun.js +++ b/plugins/c9.error/raygun.js @@ -33,11 +33,10 @@ function plugin(options, imports, register) { client = clients[client]; client._send = client.send; client.send = function(exception, customData, callback, request) { - var ex = exception; if (!exception.stack) - ex = new Error(exception.message || exception); + exception = new Error(exception.message || exception); - return this._send(ex, customData, callback, request); + return this._send.apply(this, arguments); }; } From 4f60554327dd077313d8b2e6e54222206a867df0 Mon Sep 17 00:00:00 2001 From: nightwing Date: Wed, 11 Mar 2015 17:26:03 +0400 Subject: [PATCH 26/38] use ~/.c9 instead of appdir on mac --- local/server.js | 2 +- settings/local.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/local/server.js b/local/server.js index ac7c4a62..707e8137 100644 --- a/local/server.js +++ b/local/server.js @@ -23,7 +23,7 @@ var SAFE_PORTS = [2222, 2310, 3000, 3001, 3030, 3210, 3333, 4000, 4001, 7777, 8000, 8001, 8003, 8031, 8080, 8081, 8765, 8777, 8888, 9000, 9001, 9080, 9090, 9876, 9877, 9999, 49221]; -var installPath = process.platform == "darwin" +var installPath = process.platform == "dar-win" // disabled for sdk ? "/Library/Application Support/Cloud9" : join(process.env.HOME, ".c9"); diff --git a/settings/local.js b/settings/local.js index e10b625d..a04636db 100644 --- a/settings/local.js +++ b/settings/local.js @@ -3,7 +3,7 @@ module.exports = function(manifest, installPath, settingDir) { var fs = require("fs"); if (typeof installPath != "string") { - installPath = process.platform == "darwin" + installPath = process.platform == "darwin" && false // disabled for sdk ? "/Library/Application Support/Cloud9" : path.join(process.env.HOME, ".c9"); } From 9c6188fd38a9e76e6e7247072dc8e83cdcafd75a Mon Sep 17 00:00:00 2001 From: Ruben Daniels Date: Wed, 11 Mar 2015 16:29:50 +0000 Subject: [PATCH 27/38] Disable new plugin menu item when not sdk=1 --- plugins/c9.ide.panels/panel.js | 4 ++++ plugins/c9.ide.plugins/manager.js | 19 +++++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/plugins/c9.ide.panels/panel.js b/plugins/c9.ide.panels/panel.js index 149e0638..588da214 100644 --- a/plugins/c9.ide.panels/panel.js +++ b/plugins/c9.ide.panels/panel.js @@ -353,6 +353,10 @@ define(function(require, module, exports) { * @readonly */ get aml(){ return aml; }, + /** + * @property {HTMLElement} container + */ + get container(){ return aml.$ext; }, /** * The area that this panel is a part of. diff --git a/plugins/c9.ide.plugins/manager.js b/plugins/c9.ide.plugins/manager.js index 89aa6516..ec27acfd 100644 --- a/plugins/c9.ide.plugins/manager.js +++ b/plugins/c9.ide.plugins/manager.js @@ -100,6 +100,7 @@ define(function(require, exports, module) { // var emit = plugin.getEmitter(); var HASSDK = c9.location.indexOf("sdk=0") === -1; + var ENABLED = c9.location.indexOf("sdk=1") > -1; var model, datagrid, filterbox; var btnUninstall, btnReport, btnReadme, btnCloud9; @@ -122,14 +123,16 @@ define(function(require, exports, module) { // updateCommandsFromSettings(); // }, plugin); - menus.addItemByPath("File/New Plugin", null, 210, plugin); - Object.keys(TEMPLATES).forEach(function(name){ - menus.addItemByPath("File/New Plugin/" + TEMPLATES[name], new ui.item({ - onclick: function(){ - createNewPlugin(name); - } - }), 210, plugin); - }); + if (ENABLED) { + menus.addItemByPath("File/New Plugin", null, 210, plugin); + Object.keys(TEMPLATES).forEach(function(name){ + menus.addItemByPath("File/New Plugin/" + TEMPLATES[name], new ui.item({ + onclick: function(){ + createNewPlugin(name); + } + }), 210, plugin); + }); + } } var drawn; From 0793a655089f21acd08bcfc2576e93d39a0d2070 Mon Sep 17 00:00:00 2001 From: Alex Brausewetter Date: Thu, 12 Mar 2015 11:45:02 +0000 Subject: [PATCH 28/38] Always display showhidden setting in dropdown --- plugins/c9.ide.tree/tree.js | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/c9.ide.tree/tree.js b/plugins/c9.ide.tree/tree.js index 0977224e..915fb80c 100644 --- a/plugins/c9.ide.tree/tree.js +++ b/plugins/c9.ide.tree/tree.js @@ -314,7 +314,6 @@ define(function(require, exports, module) { id: "mnuitemHiddenFiles", type: "check", caption: "Show Hidden Files", - visible: "{tree.container.visible}", checked: "user/projecttree/@showhidden", onclick: function(e) { setTimeout(function() { From 8e3c77691794a5eafd88c556284fde81ae0ab1ae Mon Sep 17 00:00:00 2001 From: nightwing Date: Thu, 12 Mar 2015 18:08:56 +0400 Subject: [PATCH 29/38] fix emmet ref in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9e7ee81c..0a6f77b2 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "connect": "~2.12.0", "debug": "~0.7.4", "ejs": "~0.8.3", - "emmet": "git://github.com/cloud9ide/emmet-core.git#cb4a2dcf86", + "emmet": "git://github.com/cloud9ide/emmet-core.git#2ff6dc06ad", "engine.io": "~1.5.1", "engine.io-client": "~1.5.1", "eslint": "git://github.com/cloud9ide/eslint.git#e2d052aafd81ea0aa6d1d4fd9f88f3613e386160", From e45d34e881434723bb7167ffdafac0838547af85 Mon Sep 17 00:00:00 2001 From: nightwing Date: Thu, 12 Mar 2015 21:00:06 +0400 Subject: [PATCH 30/38] fix pty.js version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0a6f77b2..d29fc7ad 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "form-data": "~0.2.0" }, "optionalDependencies": { - "pty.js": "~0.2.3", + "pty.js": "git://github.com/cloud9ide/pty.js.git#10e31e23ed", "heapdump": "0.2.10" }, "licenses": [], From 898c34de0ed76ffce99b809f7e645c1468aa4ffb Mon Sep 17 00:00:00 2001 From: nightwing Date: Fri, 13 Mar 2015 15:23:19 +0400 Subject: [PATCH 31/38] set crossOrigin attribute on cdn scripts (+6425) --- node_modules/architect-build/build_support/mini_require.js | 4 ++++ plugins/c9.login.client/bootstrap.js | 1 + 2 files changed, 5 insertions(+) diff --git a/node_modules/architect-build/build_support/mini_require.js b/node_modules/architect-build/build_support/mini_require.js index 4c4d9b2d..ad08d9c3 100644 --- a/node_modules/architect-build/build_support/mini_require.js +++ b/node_modules/architect-build/build_support/mini_require.js @@ -211,6 +211,10 @@ var loadScript = function(path, id, callback) { s.src = path; s.charset = 'utf-8'; s.async = true; + + if (path.lastIndexOf(require.MODULE_LOAD_URL, 0) == 0) + s.crossOrigin = true; + head.appendChild(s); s.onload = s.onreadystatechange = function(_, isAbort) { diff --git a/plugins/c9.login.client/bootstrap.js b/plugins/c9.login.client/bootstrap.js index 9f62d313..1405cd89 100644 --- a/plugins/c9.login.client/bootstrap.js +++ b/plugins/c9.login.client/bootstrap.js @@ -93,6 +93,7 @@ function loadScript(path, token, callback) { var and = path.indexOf("?") >= 0 ? "&" : "?"; s.src = path + (token ? and + "access_token=" + encodeURIComponent(token) : ""); + s.crossOrigin = true; head.appendChild(s); s.onload = s.onreadystatechange = function(_, isAbort) { From 0ed6b33ac79e2cb4254035e08fd7ee5fa92913e8 Mon Sep 17 00:00:00 2001 From: nightwing Date: Fri, 13 Mar 2015 17:38:46 +0000 Subject: [PATCH 32/38] fix debugger not working for non-ascii files --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d29fc7ad..b371c44e 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "c9.ide.find": "#989c06e6a7", "c9.ide.find.infiles": "#f98dfef554", "c9.ide.find.replace": "#e4daf722b8", - "c9.ide.run.debug": "#f5d664c787", + "c9.ide.run.debug": "#1110d83af6", "c9.ide.ace.emmet": "#e5f1a92ac3", "c9.ide.ace.gotoline": "#4d1a93172c", "c9.ide.ace.keymaps": "#6c4bb65b1f", From c99f7ae442b22200e1d0dc5700e5df5fdc01ebbd Mon Sep 17 00:00:00 2001 From: nightwing Date: Fri, 13 Mar 2015 18:23:42 +0000 Subject: [PATCH 33/38] move breakpoints when renaming a file --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b371c44e..f74e5fe2 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "c9.ide.find": "#989c06e6a7", "c9.ide.find.infiles": "#f98dfef554", "c9.ide.find.replace": "#e4daf722b8", - "c9.ide.run.debug": "#1110d83af6", + "c9.ide.run.debug": "#5d6e22cedc", "c9.ide.ace.emmet": "#e5f1a92ac3", "c9.ide.ace.gotoline": "#4d1a93172c", "c9.ide.ace.keymaps": "#6c4bb65b1f", From b09727c02dab945dbf3f4fedb9f357d94bc32041 Mon Sep 17 00:00:00 2001 From: nightwing Date: Fri, 13 Mar 2015 18:50:07 +0000 Subject: [PATCH 34/38] sometimes breakpoint path is undefined +6602 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f74e5fe2..943d3989 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "c9.ide.find": "#989c06e6a7", "c9.ide.find.infiles": "#f98dfef554", "c9.ide.find.replace": "#e4daf722b8", - "c9.ide.run.debug": "#5d6e22cedc", + "c9.ide.run.debug": "#379e508be6", "c9.ide.ace.emmet": "#e5f1a92ac3", "c9.ide.ace.gotoline": "#4d1a93172c", "c9.ide.ace.keymaps": "#6c4bb65b1f", From 82b56224baaa2c47519c9ddd568848ce7e99d253 Mon Sep 17 00:00:00 2001 From: nightwing Date: Fri, 13 Mar 2015 00:22:29 +0400 Subject: [PATCH 35/38] add collab-server to the sdk --- configs/api.standalone.js | 226 ++ package.json | 3 +- plugins/c9.vfs.extend/collab-server.js | 2971 ++++++++++++++++++++++++ 3 files changed, 3198 insertions(+), 2 deletions(-) create mode 100644 configs/api.standalone.js create mode 100644 plugins/c9.vfs.extend/collab-server.js diff --git a/configs/api.standalone.js b/configs/api.standalone.js new file mode 100644 index 00000000..c36cdca8 --- /dev/null +++ b/configs/api.standalone.js @@ -0,0 +1,226 @@ +#!/usr/bin/env node +"use strict"; + +module.exports.addApi = function(plugins, config) { + config.apiUrl = ""; + var apiPlugins = [{ + setup: function(options, imports, register) { + mockRedis(options, imports, register); + }, + provides: ["mq", "db", "mailer"], + consumes: ["api"] + }, { + packagePath: "./c9.logtimestamp/logtimestamp", + mode: config.mode + }, + "./c9.error/logger.raygun_mock", + "./c9.api/ping", + { + packagePath: "./c9.api/health", + revision: config.manifest.revision, + version: config.manifest.version, + }, + "./c9.api/user", + "./c9.api/project", + "./c9.api/applications", + "./c9.api/session", + "./c9.api/collab", + "./c9.api/settings", + "./c9.api/vfs", + "./c9.api/preview", + "connect-architect/connect.bodyparser", + "connect-architect/connect.query", + "./c9.passport/passport", + "./c9.passport/bearer", + "./c9.passport/basic"]; + + return plugins.concat(apiPlugins); +}; + +function mockRedis(options, imports, register) { + var api = imports.api; + if (api.bindUser) + return; + + api.bindUser = function(call) { + return function(req, res, next) { + call(req.user, req.params, function(err, json) { + if (err) return next(err); + res.json(json); + }); + }; + }; + api.authenticate = function() { + return function(req, res, next) { + req.user = new User(req.query.access_token); + next(); + }; + }; + var users = { + "-1": { + name: "John Doe", email: "johndoe@example.org", + } + }; + + var projects = [{ + owner: -1, members: ["rw", -1, 1, 2, 3, "r", 4, 5] + }]; + + function User(id) { + if (typeof id == "object") + id = id.id; + if (!/^\d+/.test(id)) + id = -1; + var u = users[id]; + if (!u) { + u = users[id] = { + name: "user" + id, + email: "user" + id + "@c9.io", + fullname: "User " + id + }; + } + + u.id = id; + return { + name: u.name, + fullname: u.fullname, + email: u.email, + id: id + }; + } + + function Project(id) { + if (typeof id == "object") + id = id.id; + var p = projects[id]; + if (!p) + return console.log(id); + return { + isPrivate: function() { + return false; + }, + owner: new User(p.owner), + getMembers: function(cb) { + var memebers = [], acl; + var ownerId = this.owner.id; + p.members.forEach(function(memberId) { + if (typeof memberId == "string") + return (acl = memberId); + memebers.push(new Member(memberId, acl, ownerId)); + }); + cb && cb(null, memebers); + return memebers; + }, + id: id + }; + } + + function Member(id, acl, ownerId) { + return { + user: id, + status: "", + acl: acl, + role: id == ownerId ? "a" : "c", + save: function(project, cb) { + cb(); + }, + remove: function(project, cb) { + var p = projects[project.id]; + var i = p.members.indexOf(id); + if (i != -1) + p.members.splice(i, 1); + cb(); + } + }; + } + + function DB(type) { + var key, query; + var dbApi = { + findOne: function(keyv, cb) { + key = keyv; + if (cb) + return dbApi.exec(cb); + return dbApi; + }, + populate: function(queryV) { + query = queryV; + return dbApi; + }, + exec: function(cb) { + var result; + switch (type) { + case "Project": + result = new Project(0); + break; + case "User": + result = new User(key.uid); + break; + case "AccessToken": + if (query == "user") { + var id = /\d/.test(key.token) ? key.token : -1; + result = {user: new User(id)}; + } + break; + case "WorkspaceMember": + var p = key.project; + var user = new User(key.user); + result = p.getMembers().filter(function(m) { + return m.user == user.id; + })[0]; + break; + default: + console.log(":((("); + } + cb(null, result); + return dbApi; + }, + }; + dbApi.ROLE_NONE = "n"; + dbApi.ROLE_VISITOR = "v"; // @deprecated + dbApi.ROLE_COLLABORATOR = "c"; + dbApi.ROLE_ADMIN = "a"; + dbApi.ACL_RW = "rw"; + dbApi.ACL_R = "r"; + dbApi.COLLABSTATE_PENDING_ADMIN = "pending-admin"; + dbApi.COLLABSTATE_PENDING_USER = "pending-user"; // @deprecated + dbApi.COLLABSTATE_PENDING_NONE = "pending-none"; + return dbApi; + } + + var pubsub = { + publish: function() { + + } + }; + + function noop() { return {}; } + + register(null, { + "mq": { + connection: noop, + close: noop, + onReady: noop, + onceReady: noop, + }, + "db": { + User: new DB("User"), + Project: new DB("Project"), + Remote: new DB("Remote"), + AccessToken: new DB("AccessToken"), + WorkspaceMember: new DB("WorkspaceMember"), + Vfs: new DB("Vfs"), + DockerHost: new DB("DockerHost"), + Container: new DB("Container"), + Image: new DB("Image"), + Lock: new DB("Lock"), + Nonce: new DB("Nonce"), + // PubSub as part of the database infrastructure + getSubscriber: function() { return pubsub }, + getPublisher: function() { return pubsub }, + }, + mailer: { + + } + }); +} diff --git a/package.json b/package.json index 943d3989..088f9328 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,7 @@ "form-data": "~0.2.0" }, "optionalDependencies": { - "pty.js": "git://github.com/cloud9ide/pty.js.git#10e31e23ed", - "heapdump": "0.2.10" + "pty.js": "git://github.com/cloud9ide/pty.js.git#10e31e23ed" }, "licenses": [], "c9plugins": { diff --git a/plugins/c9.vfs.extend/collab-server.js b/plugins/c9.vfs.extend/collab-server.js new file mode 100644 index 00000000..8e77d0fe --- /dev/null +++ b/plugins/c9.vfs.extend/collab-server.js @@ -0,0 +1,2971 @@ +// Uglify +// uglifyjs -c -m -o collab-server.js plugins/c9.vfs.extend/collab-server.js +// Deploy to shared space: +// scp -o LogLevel=ERROR -o StrictHostKeyChecking=no -i c9/node_modules/settings/keys/deploy collab-server.js 52ee501f50044657c4000005@project-livec99d49a9ef92.rhcloud.com:/var/lib/openshift/52ee501f50044657c4000005/app-root/data/759814/root/c9-vfs-extend/collab-server.js +"use strict"; +var Fs = require("fs"); +var Path = require("path"); +var net = require("net"); +var Stream = require("stream").Stream; +var crypto = require('crypto'); +var events = require("events"); +var exists = Fs.exists || Path.exists; + +var localfsAPI; // Set on VFS register +var DEFAULT_NL_CHAR_FILE = "\n"; +var DEFAULT_NL_CHAR_DOC = ""; + +// Models +var User, Document, Revision, Workspace, ChatMessage; +var basePath; +var PID; +var dbFilePath; + +// Cache the workspace state got from the database +var cachedWS; +var cachedUsers; + +var debug = false; + +function getHomeDir() { + return process.env.OPENSHIFT_DATA_DIR || process.env.HOME; +} + +function getProjectWD() { + var env = process.env; + var pidStr = env.BASE_PROC ? "" : String(PID); + return Path.resolve(env.BASE_PROC || env.OPENSHIFT_DATA_DIR || env.HOME, ".c9", pidStr); +} + +/** + * Checks if the collab server required modules are installed + * npm: sqlite3 & sequelize + */ +function installServer(callback) { + function checkInstalled() { + try { + require("sqlite3"); + require("sequelize"); + return true; + } catch (err) { + return false; + } + } + + if (!checkInstalled()) { + var err = new Error("[vfs-collab] Missing dependencies - NODE_PATH: " + process.env.NODE_PATH + "; node " + process.version); + err.code = "EFATAL"; + return callback(err); + } + callback(); +} + +/** + * Wrap Sequelize callback-style to NodeJS"s standard callback-style + */ +function wrapSeq(fun, next) { + return fun.success(function () { + next.apply(null, [null].concat(Array.prototype.slice.apply(arguments))); + }).error(next); +} + +/** + * Initialize the collab server sqlite3 database + * - Define modules mapping to tables + * - Declare relationships + * - Sync sequelize modules + * - Create and cache the Workspace metadata + * - Set synchronous = 0 for fastest IO performance + * - Create indices, if not existing + * + * @param {Boolean} readonly Whether the intention is only to read from the + * database (if true, initialization is skipped) + * @param {Function} callback + */ +function initDB(readonly, callback) { + var Sequelize = require("sequelize"); + var MAX_LOG_LINE_LENGTH = 151; + + dbFilePath = dbFilePath || Path.join(getProjectWD(), "collab.db"); + var sequelize = new Sequelize("c9-collab", "c9", "c9-collab-secret", { + // the sql dialect of the database + dialect: "sqlite", + omitNull: true, + storage: dbFilePath, + // capture only the most important pieces of a sql statement or query + logging: function (log) { + if (!debug) + return; + + var lines = log.split(/\r\n|\n|\r/); + var firstLine = lines[0]; + firstLine = firstLine.length < MAX_LOG_LINE_LENGTH ? firstLine : (firstLine.substring(0, MAX_LOG_LINE_LENGTH) + "..."); + var lastLine = lines[lines.length-1]; + lastLine = lastLine.length < MAX_LOG_LINE_LENGTH ? lastLine : (lastLine.substring(lastLine.length - MAX_LOG_LINE_LENGTH) + "..."); + console.error("[vfs-collab] DB", lines.length === 1 + ? (lines[0].length <= (2*MAX_LOG_LINE_LENGTH) ? lines[0] : (firstLine + lines[0].substring(Math.max(MAX_LOG_LINE_LENGTH, lines[0].length - MAX_LOG_LINE_LENGTH)))) + : (firstLine + " --- " + lastLine)); + }, + + define: { + // don"t use camelcase for automatically added attributes but underscore style + // so updatedAt will be updated_at + underscored: true, + freezeTableName: false, + charset: "utf8", + collate: "utf8_general_ci", + classMethods: {}, + instanceMethods: {} + }, + + // sync after each association (see below). If set to false, you need to sync manually after setting all associations. Default: true + syncOnAssociation: true, + + // use pooling in order to reduce db connection overload and to increase speed + // currently only for mysql and postgresql (since v1.5.0) + pool: { maxConnections: 5, maxIdleTime: 30} + }); + + Store.User = User = sequelize.define("User", { + uid: { type: Sequelize.STRING, primaryKey: true }, + fullname: { type: Sequelize.STRING }, + email: { type: Sequelize.STRING } + }, { + timestamps: true + }); + + Store.Workspace = Workspace = sequelize.define("Workspace", { + authorPool: { type: Sequelize.TEXT }, // Stringified JSON - uid -> 1,2, ...etc. + colorPool: { type: Sequelize.TEXT }, // Stringified JSON - uid --> "{r: 256, g: 0, b: 0}" + basePath: { type: Sequelize.STRING, allowNull: false }, + migration: { type: Sequelize.INTEGER, defaultValue: 0 } + }, { + timestamps: true + }); + + Store.Document = Document = sequelize.define("Document", { + id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, + path: { type: Sequelize.STRING, unique: true }, + contents: { type: Sequelize.TEXT }, + fsHash: { type: Sequelize.STRING }, + authAttribs: { type: Sequelize.TEXT }, // Stringified JSON + starRevNums: { type: Sequelize.TEXT }, // Stringified JSON list of integers + revNum: { type: Sequelize.INTEGER, defaultValue: 0 }, + created_at: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.NOW }, + updated_at: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.NOW }, + newLineChar: { type: Sequelize.STRING, defaultValue: DEFAULT_NL_CHAR_DOC }, // "" or "\n" or "\r\n" + }, { + timestamps: false + }); + + Store.Revision = Revision = sequelize.define("Revision", { + id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, + operation: { type: Sequelize.TEXT }, // Stringified JSON Array - can be empty for rev:0 + author: { type: Sequelize.STRING }, // userId if exists, 0 in syncing operations, -1 in undo non authored text + revNum: { type: Sequelize.INTEGER }, + created_at: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.NOW }, + updated_at: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.NOW } + }, { + timestamps: false + }); + + Store.ChatMessage = ChatMessage = sequelize.define("ChatMessage", { + id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, + text: { type: Sequelize.STRING }, + userId: { type: Sequelize.STRING, allowNull: false }, + timestamp: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.NOW } + }, { + timestamps: true + }); + + Document.hasMany(Revision); + Revision.belongsTo(Document); + + if (readonly) + return callback(); + + // Add migrations here e.g. ALTER TABLE ... ADD COLUMN ... + var migrations = [ + { query: "CREATE INDEX DocumentRevisionsIndex ON Revisions(document_id)", skipError: true }, + { query: "CREATE INDEX ChatMessageTimestampIndex ON ChatMessages(timestamp)", skipError: true }, + { query: "DELETE FROM Documents" }, + { query: "DELETE FROM Revisions" }, + { query: "ALTER TABLE Documents ADD COLUMN newLineChar VARCHAR(255)" } + ]; + + async.series([ + function(next) { + // http://www.sqlite.org/pragma.html + wrapSeq(sequelize.query("PRAGMA synchronous = 0;"), next); + }, + // Document.drop(), // Cleanup on init + // Revision.drop(), // Cleanup on init + function(next) { + wrapSeq(User.sync(), next); + }, + function(next) { + wrapSeq(Workspace.sync(), next); + }, + function(next) { + wrapSeq(sequelize.query("ALTER TABLE Workspaces ADD COLUMN migration INTEGER DEFAULT 0"), next.bind(null, null)); + }, + function(next) { + wrapSeq(Document.sync(), next); + }, + function(next) { + wrapSeq(Revision.sync(), next); + }, + function(next) { + wrapSeq(ChatMessage.sync(), next); + }, + function (next) { + wrapSeq(Workspace.findOrCreate({id: 1}, { + authorPool: "{}", + colorPool: "{}", + basePath: basePath, + migration: migrations.length + }), function(err, ws) { + if (err) + return next(err); + ws.authorPool = JSON.parse(ws.authorPool); + ws.colorPool = JSON.parse(ws.colorPool); + cachedWS = ws; + next(); + }); + }, + function (next) { + var migNum = cachedWS.migration || 0; + if (migNum === migrations.length) + return next(); + var migApplied = migNum; + async.forEachSeries(migrations.slice(migNum - migrations.length), function (migration, next) { + console.error("[vfs-collab] applying database migration:", migration.query); + wrapSeq(sequelize.query(migration.query), function(err) { + if (err && !migration.skipError) + return next(err); + migApplied++; + cachedWS.migration = migApplied; + Store.saveWorkspaceState(cachedWS, next); + }); + }, function(err) { + if (cachedWS.migration != migrations.length) + err = (err ? (err + " -- ") : "") + "Not all migrations could be applied!"; + next(err); + }); + } + ], function(err) { + if (!err) + return callback(); + console.error("[vfs-collab] initDB attemp failed:", err); + if (err.code !== "SQLITE_CORRUPT" && err.code !== "SQLITE_NOTADB") + return callback(err); // not sure how to handle any other errors if any + console.error("[vfs-collab] initDB found a corrupted database - backing up and starting with a fresh collab database"); + Fs.rename(dbFilePath, dbFilePath + ".old", function (err) { + if (err) + return callback(err); + initDB(readonly, callback); + }); + }); +} + +/**************** operations.js ******************/ +var operations = (function() { +/** + * Get a diff operation to transform a text document from: `fromText` to `toText` + * + * @param {String} fromText + * @param {String} toText + * @return {Operation} op + */ +function operation(fromText, toText) { + var dmp = new diff_match_patch(); + var diffs = dmp.diff_main(fromText, toText); + dmp.diff_cleanupSemantic(diffs); + var d, type, val; + var op = []; + for (var i = 0; i < diffs.length; i++) { + d = diffs[i]; + type = d[0]; + val = d[1]; + switch(type) { + case DIFF_EQUAL: + op.push("r" + val.length); + break; + case DIFF_INSERT: + op.push("i" + val); + break; + case DIFF_DELETE: + op.push("d" + val); + break; + } + } + return op; +} + +// Simple edit constructors. + +function insert(chars) { + return "i" + chars; +} + +function del(chars) { + return "d" + chars; +} + +function retain(n) { + return "r" + String(n); +} + +/** + * Return the type of a sub-edit + * + * @param {String} edit + * @return {String} type of the operation + */ +function type(edit) { + switch (edit[0]) { + case "r": + return "retain"; + case "d": + return "delete"; + case "i": + return "insert"; + default: + throw new TypeError("Unknown type of edit: ", edit); + } +} + +/** + * Return the value of a sub-edit + * + * @param {String} sub-edit + * @return the value of the operation + * - Retain: the number of characters to retain + * - Insert/Delete: the text to insert or delete + */ +function val(edit) { + return type(edit) === "retain" ? ~~edit.slice(1) : edit.slice(1); +} + +/** + * Return the length of a sub-edit + * + * @param {String} edit + * @return {Number} the length of the operation + * - Retain: the number of characters to retain + * - Insert/Delete: the text length to insert or delete + */ +function length(edit) { + return type(edit) === "retain" ? ~~edit.slice(1) : edit.length - 1; +} + +/** + * Split a sub-edit on a index: idx + * + * @param {String} edit + * @return [{String}] an array of length 2 of the sub-operaion splitted to 2 operaions + */ +function split(edit, idx) { + if (type(edit) === "retain") { + var rCount = ~~edit.slice(1); + return [ + "r" + idx, + "r" + (rCount - idx) + ]; + } + else { + return [ + edit[0] + edit.substring(1, idx + 1), + edit[0] + edit.substring(idx + 1) + ]; + } +} + +/** + * Pack an operation to a minimal operation + * + * @param {Operation} op + * @return {Operation} packed + */ +function pack(op) { + var packed = op.slice(); + var i = 0; + while (i < packed.length - 1) { + if (packed[i][0] === packed[i+1][0]) + packed.splice(i, 2, packed[i][0] + (val(packed[i]) + val(packed[i+1]))); + else + i++; + } + return packed; +} + +/** + * Inverse an operation to undo revert its effect on a document + * + * @param {Operation} op + * @return {Operation} inversed + */ +function inverse(op) { + var edit, t, v, inversed = new Array(op.length); + for (var i = 0, el = op.length; i < el; i++) { + edit = op[i]; + t = type(edit); + v = val(edit); + switch (t) { + case "retain": + inversed[i] = op[i]; + break; + case "insert": + inversed[i] = del(v); + break; + case "delete": + inversed[i] = insert(v); + break; + } + } + return inversed; +} + +return { + insert: insert, + del: del, + retain: retain, + type: type, + val: val, + length: length, + split: split, + pack: pack, + operation: operation, + inverse: inverse +}; + +})(); + +/**************** apply.js ******************/ + +function OTError(expected, actual) { + var err = new Error("OT removed text mismatch"); + err.expected = expected; + err.actual = actual; + err.code = "EMISMATCH"; + return err; +} + +/** + * Apply an operation on a string document and return the resulting new document text. + * + * @param {Opeartion} op - e.g. ["r2", "iabc", "r12"] + * @param {String} doc + * @return {String} newDoc + */ + function applyContents(op, doc) { + var val, newDoc = ""; + for (var i = 0, len = op.length; i < len; i += 1) { + val = op[i].slice(1); + switch (op[i][0]) { + case "r": // retain + val = Number(val); + newDoc += doc.slice(0, val); + doc = doc.slice(val); + break; + case "i": // insert + newDoc += val; + break; + case "d": // delete + if (doc.indexOf(val) !== 0) + throw new OTError(val, doc.slice(0, 10)); + else + doc = doc.slice(val.length); + break; + default: + throw new TypeError("Unknown operation: " + operations.type(op[i])); + } + } + return newDoc; +} + + +/**************** author_attributes.js ******************/ +/** + * This is a specifically designed data structure that tends to behave as a relaxed B-Tree + * to optimize author attributes processing time, disk usage and network overhead + * + * It optimizes on two main factors: + * - insert/delete/traversal/find time: The B-tree try to maintain a minimal depth, so minimal processing needed for those operations: O(log with base minKeySize) + * - Parsing/Stringification time and disk usage: the nodes are implemented as arrays with the first element + * indicating the number of entries in the node + * + * @param minKeySize - the minimum number of entries in a node + * @param maxKeySize - the maximum number of entries in a node + * + * @author Mostafa + * @author Harutyun + */ +function AuthorAttributes(minKeySize, maxKeySize) { + // 2 * x ---> [length, [value]] + minKeySize = minKeySize || 20; // 4 + maxKeySize = maxKeySize || (5 * minKeySize); // 8 + + function addValue(nodes, index, startI, length, id) { + var i = startI; + var len = nodes[i]; + var val = nodes[i+1]; + if (index < 0 || index > len) + throw new Error("Invalid index passed!"); + + if (val === id) { + nodes[i] += length; + } else if (index === len) { + if (nodes[i+3] == id) + nodes[i+2]+=length; + else + nodes.splice(i + 2, 0, length, id); + } else if (index === 0) { + if (nodes[i-1] == id) + nodes[i-2] += length; + else + nodes.splice(i , 0, length, id); + } else { + nodes.splice(i, 2, index, val, length, id, len - index, val); + } + } + + function split(parent, nodes, pos) { + var splitPos = (nodes.length >> 2) << 1; + var leftLen = 0, rightLen = 0; + var right = nodes.splice(splitPos, splitPos + 2); + + for (var i = 0; i < right.length; i += 2) + rightLen += right[i]; + + if (parent) { + parent.splice(pos + 2, 0, rightLen, right); + parent[pos] -= rightLen; + } else { + var left = nodes.splice(0, splitPos + 2); + for (var i = 0; i < left.length; i += 2) + leftLen += left[i]; + nodes.push(leftLen, left, rightLen, right); + } + } + + function insert(nodes, index, length, id) { + if (nodes.length === 0) { + nodes.push(length, id); + return; + } + var spilled = _insert(nodes, index, length, id); + if (spilled) + split(null, nodes, null); + // sanityCheck(nodes) + } + + function _insert(nodes, index, length, id) { + for (var i = 0; i < nodes.length; i += 2) { + var len = nodes[i]; + if (index <= len) { + var node = nodes[i+1]; + if (Array.isArray(node)) { + nodes[i] += length; + var spilled = _insert(node, index, length, id); + if (spilled) + split(nodes, nodes[i+1], i); + } + else { + addValue(nodes, index, i, length, id); + } + return nodes.length > maxKeySize; + } + index -= len; + } + } + + function remove(nodes, index, length) { + // console.log("remove:", index, length); + var removedTotal = 0; + for (var i = 0; i < nodes.length; i += 2) { + var len = nodes[i]; // node.length + var ch = nodes[i + 1]; + var removed; + if (index <= len) { + if (Array.isArray(ch)) + removed = remove(ch, index, length); + else + removed = Math.max(0, Math.min(length, len - index)); + + // console.log("Removed:", removed); + nodes[i] -= removed; // node.length + length -= removed; + removedTotal += removed; + if (!nodes[i]) { + nodes.splice(i, 2); + i -= 2; + } + else if (Array.isArray(ch) && ch.length < minKeySize && + (ch.length + nodes.length) <= maxKeySize) { + // Move elements from child to parent + nodes.splice.apply(nodes, [i, 2].concat(ch)); + } + if (!length) + break; + index = 0; + } + else { + index -= len; + } + } + + for (var j = 0; j < nodes.length - 2; j += 2) { + // console.log("CHECK:", nodes[j].id, nodes[j+1].id); + if (!nodes[j] || nodes[j+1] !== nodes[j+3]) + continue; + nodes[j] += nodes[j + 2]; + nodes.splice(j+1, 2); + j -= 2; + } + // sanityCheck(nodes); + return removedTotal; + } + + + function apply(nodes, op, authPoolId) { + authPoolId = authPoolId || 0; + + var index = 0; + var opLen; + for (var i = 0; i < op.length; i++) { + opLen = operations.length(op[i]); + switch (operations.type(op[i])) { + case "retain": + index += opLen; + break; + case "insert": + insert(nodes, index, opLen, authPoolId); + index += opLen; + break; + case "delete": + remove(nodes, index, opLen); + break; + default: + throw new TypeError("Unknown operation: " + operations.type(op[i])); + } + } + } + + return { + apply: apply, + // insert: insert, + // remove: remove + }; +} + +var applyAuthororAttributes = AuthorAttributes().apply; + +/** + * Hash a string (document content) for easier comparison of state changes + */ +function hashString(str) { + // if ((str + "").indexOf("\r") != -1) debugger + return crypto.createHash('md5').update(str).digest("hex"); +} + +/** + * Normalize document path to discard workspace prefix + */ +function getDocPath(path) { + if (path.indexOf(basePath) === 0) + return path.substring(basePath.length+1); + return path; +} + +var emitter = new events.EventEmitter(); + +/** + * Document Store database wrapper utility to ease persistence/retrieval and update of entities such as: + * Documents, Revisions, Workspace, ChatMessages, Users + */ +var Store = (function () { + /** + * Create a `Document` from a template with path, contents + * Also, create its Revision#0 record + * @param {Object} tmpl + * @param {Function} callback + */ + function newDocument(tmpl, callback) { + var contents = tmpl.contents || ""; + wrapSeq(Document.create({ + contents: new Buffer(contents), + path: tmpl.path, + fsHash: tmpl.fsHash || hashString(contents), + authAttribs: contents.length ? JSON.stringify([contents.length, null]) : "[]", + starRevNums: "[]", + newLineChar: tmpl.newLineChar || DEFAULT_NL_CHAR_DOC, + revNum: 0 + }), function (err, doc) { + if (err) + return callback(err); + wrapSeq(Revision.create({ + document_id: doc.id, + operation: new Buffer("[]"), + revNum: 0 + }), function (err, rev) { + if (err) + return callback(err); + doc.revisions = parseRevisions([rev]); + callback(null, parseDocument(doc)); + }); + }); + } + + /* + function moveDocument(docId, newPath, callback) { + wrapSeq(Document.find(docId), function (err, doc) { + if (err || !doc) + return callback(err || "No document found to rename!"); + doc.path = newPath; + wrapSeq(doc.save(), callback); + }); + } + */ + + function parseDocument(doc) { + if (doc.authAttribs) + doc.authAttribs = JSON.parse(doc.authAttribs); + if (doc.starRevNums) + doc.starRevNums = JSON.parse(doc.starRevNums); + doc.contents = doc.contents && doc.contents.toString(); // because it can be a buffer + return doc; + } + + function parseDocumentCallback(callback) { + return function (err, doc) { + if (err || !doc) + return callback(err); + + callback(null, parseDocument(doc)); + }; + } + + /** + * Get a `Document` from the database given its path + * @param {String} path the document path to query the database with + * @param [{String}] attributes - optional + * @param {Function} callback + * @param {Object} callback.err + * @param {Object} callback.result The result, or null if getDocument() failed (might even though err is null) + */ + function getDocument(path, attributes, callback) { + var query = { where: {path: getDocPath(path)} }; + if (!callback) { + callback = attributes; + attributes = undefined; + } + else { + attributes.unshift("id"); + query.attributes = attributes; // ["id", other attributes] + } + + return wrapSeq(Document.find(query), parseDocumentCallback(callback)); + } + + /** + * Get the revisions of a certain document + * @param {Document} doc + * @param {Function} callback + */ + function getRevisions(doc, callback) { + wrapSeq(doc.getRevisions(), function (err, revisions) { + if (err) + return callback(err); + callback(null, parseRevisions(revisions)); + }); + } + + /** + * In-place parsing of revisions + * @param [{Revision}] revisions + */ + function parseRevisions(revisions) { + revisions.forEach(function (rev) { + // rev.operation can be a buffer and is always a stringified JSON array + rev.operation = JSON.parse(rev.operation.toString()); + }); + revisions.sort(function(a, b) { + return a.revNum - b.revNum; + }); + return revisions; + } + + function prepareAttributes(doc, attributes) { + var update = {}; + for (var i = 0; i < attributes.length; i++) + update[attributes[i]] = doc[attributes[i]]; + return update; + } + + /** + * Save a document with changes to the database + * @param [{String}] attributes - optional + * @param {Function} callback + */ + function saveDocument(doc, attributes, callback) { + if (!callback) { + callback = attributes; + attributes = undefined; + } + else { + // attributes.push("updated_at"); + } + var authAttribs = doc.authAttribs; + var starRevNums = doc.starRevNums; + doc.authAttribs = JSON.stringify(authAttribs); + doc.starRevNums = JSON.stringify(starRevNums); + doc.contents = new Buffer(doc.contents); + // doc.updated_at = new Date(doc.updated_at); + + return wrapSeq( + attributes ? doc.updateAttributes(prepareAttributes(doc, attributes)) : doc.save(), + function(err) { + doc.authAttribs = authAttribs; + doc.starRevNums = starRevNums; + callback(err, doc); + } + ); + } + + /** + * Gets the latest workspace state with the most important properties being: aurhorPool and colorPool + * @param {Function} callback + */ + function getWorkspaceState(callback) { + // the table has only a single entry + if (cachedWS) + return callback(null, cachedWS); + wrapSeq(Workspace.find(1), function (err, ws) { + if (err || !ws) + return callback(err || "No workspace state found!"); + ws.authorPool = JSON.parse(ws.authorPool); + ws.colorPool = JSON.parse(ws.colorPool); + cachedWS = ws; + callback(null, ws); + }); + } + + /** + * Save the workspace with changes to the database + * @param {Workspace} ws + * @param {Function} callback + */ + function saveWorkspaceState(ws, callback) { + var authorPool = ws.authorPool; + var colorPool = ws.colorPool; + ws.authorPool = JSON.stringify(authorPool); + ws.colorPool = JSON.stringify(colorPool); + return wrapSeq(ws.save(), function(err, savedWS) { + if (err) { + cachedWS = null; + return callback(err); + } + savedWS.authorPool = authorPool; + savedWS.colorPool = colorPool; + cachedWS = savedWS; + callback(null, savedWS); + }); + } + + /** + * Save a document with changes to the database + * @param {Function} callback + */ + function getUsers(callback) { + if (cachedUsers) + return callback(null, cachedUsers); + wrapSeq(User.all(), function (err, users) { + cachedUsers = users; + callback(err, users); + }); + } + + /** + * Add uer's chat message to the database + * @param {String} text + * @param {String} userId + * @param {Function} callback + */ + function saveChatMessage(text, userId, callback) { + wrapSeq(ChatMessage.create({ + text: text, + userId: userId + }), callback); + } + + /** + * Get the most recent chat messages + * @param {Number} limit - optional + * @param {Function} callback + */ + function recentChatHistory(limit, callback) { + limit = limit || 100; + wrapSeq(ChatMessage.findAll({ + order: 'timestamp DESC', + limit: limit + }), function(err, history) { + if (err) + return callback(err); + callback(null, history.reverse()); + }); + } + + return { + newDocument: newDocument, + // moveDocument: moveDocument, // not used + getDocument: getDocument, + getRevisions: getRevisions, + saveDocument: saveDocument, + getWorkspaceState: getWorkspaceState, + saveWorkspaceState: saveWorkspaceState, + getUsers: getUsers, + saveChatMessage: saveChatMessage, + recentChatHistory: recentChatHistory + }; +})(); + + +// This object should have the following structure: +// +// { : { : true } } +var documents = {}; + +// This object should have the following structure: +// +// { : { fs.FSWatcher } } +var watchers; + +// This object should have the following structure: +// +// { : } +var clients; + +// SQLite doesn't provide atomic instructions or locks +// So this variable expresses in-process locks +// Used to block concurrent edit updates while the document is being processed +// +// { : [{Function}] } +var locks = {}; +function lock(key, callback) { + if (!locks[key]) { + locks[key] = []; + return callback(); + } + + var watchdog = setTimeout(function() { + throw Error("[vfs-collab] Lock timeout"); // log & suicide + }, 60000); + return locks[key].push(function() { + clearTimeout(watchdog); + callback(); + }); +} + +function unlock(key) { + var lock = locks[key]; + if (!lock || !lock.length) + return delete locks[key]; + var next = lock.shift(); + next(); +} + +// Selected using colors.html +var featuredColors = [ + {r: 255, g: 146, b: 45}, + {r: 157, g: 47, b: 254}, + {r: 105, g: 215, b: 83}, + {r: 255, g: 105, b: 130}, + {r: 200, g: 109, b: 218}, + {r: 210, g: 230, b: 51}, + {r: 6, g: 134, b: 255}, + {r: 254, g: 13, b: 244}, + {r: 188, g: 255, b: 86}, + {r: 255, g: 212, b: 125}, + {r: 107, g: 4, b: 255}, + {r: 66, g: 248, b: 255} +]; + +// An algorithm to select bright random colors +function randomColor() { + var a,b,c; + do { + a = Math.random(); + b = Math.random(); + c = Math.max(a,b); + } while (c < 0.001); + + // scale them such that the larger number scales to 1.0f + var scale = 1.0 / c; + a *= scale; + b *= scale; + + // Pick third value, ensure it's dark. + c = Math.random() * 0.5; + var rgb = new Array(3); + + var idx = Math.floor(Math.random() * 3) % 3; + rgb[idx] = a; + + var rnd2 = Math.floor(Math.random() * 2) + 1; + var idx2 = (rnd2 + idx) % 3; + rgb[idx2] = b; + + var idx3 = 3 - (idx + idx2); + rgb[idx3] = c; + + rgb = rgb.map(function(x) { + return Math.floor(255 * x); + }); + return {r: rgb[0], g: rgb[1], b: rgb[2]}; +} + +/** + * Handle new collab connections (can be reconnects) + * Sync user's info to the collab database and select a color and aurhor id for him/her if not previously set + * Send USER_JOIN notifications to other connected members + * Send handshake CONNECT message to the user with the needed workspace info and chat history + */ +function handleConnect(userIds, client) { + var userId = userIds.userId; + var clientId = userIds.clientId; + + function done(err) { + if (!err) + return; + console.error(err); + client.send({ + type: "CONNECT", + error: err + }); + } + + // Make sure to cache user's info + syncUserInfo(); + + function syncUserInfo() { + if (!userId) + return done("[vfs-collab] Anonyous users connections not supported"); + + var fullname = userIds.fullname; + var email = userIds.email; + + wrapSeq(User.find({where: {uid: userId}}), function (err, user) { + if (err) + return done("[vfs-collab] syncUserInfo " + String(err)); + + if (!user) { + return wrapSeq(User.create({ + uid: userId, + fullname: fullname, + email: email + }), function(err, createdUser) { + if (err) + return done("[vfs-collab] Failed creating user " + String(err)); + cachedUsers && cachedUsers.push(createdUser); + augmentWorkspaceInfo(); + }); + } + + if (user.fullname == fullname && user.email == email) + return augmentWorkspaceInfo(); + + user.fullname = fullname; + user.email = email; + wrapSeq(user.save(), function (err, user) { + if (err) + return done("[vfs-collab] Failed updating user " + String(err)); + augmentWorkspaceInfo(); + }); + }); + } + + function augmentWorkspaceInfo() { + Store.getWorkspaceState(function (err, ws) { + if (err) + return done("[vfs-collab] augmentWorkspaceInfo " + String(err)); + var authorPool = ws.authorPool; + var colorPool = ws.colorPool; + + if (authorPool[userId] && colorPool[userId]) + return doConnect(authorPool, colorPool); + + if (!authorPool[userId]) + authorPool[userId] = Object.keys(authorPool).length + 1; + if (!colorPool[userId]) + colorPool[userId] = featuredColors[authorPool[userId]-1] || randomColor(); + Store.saveWorkspaceState(ws, function (err) { + if (err) + return done("[vfs-collab] augmentWorkspaceInfo " + String(err)); + doConnect(authorPool, colorPool); + }); + }); + } + + function doConnect(authorPool, colorPool) { + Store.getUsers(function (err, users) { + if (err) + return done("[vfs-collab] getUsers " + String(err)); + + if (users.length > 1) + console.error("[vfs-collab] User", userIds.userId, "is connecting to a workspace with", + users.length - 1, "other workspace members"); + + var onlineUsers = {}; + var idleUsers = {}; + for (var clId in clients) { + var cl = clients[clId]; + var uid = cl.userIds.userId; + if (!onlineUsers[uid]) + onlineUsers[uid] = []; + onlineUsers[uid].push(clId); + var idleClinet = cl.state === "idle"; + if (typeof idleUsers[uid] === "undefined") + idleUsers[uid] = idleClinet; // set through a USER_STATE message + else + idleUsers[uid] = idleUsers[uid] && idleClinet; + } + + if (Object.keys(onlineUsers).length > 1) + console.error("[vfs-collab] User", userIds.userId, "is connecting Collab with", + Object.keys(clients).length-1, "other clients & online workspace members", onlineUsers); + + var usersMap = {}; + users.forEach(function (user) { + var uid = user.uid; + var onlineUserClients = onlineUsers[uid] || []; + var onlineState; + if (idleUsers[uid]) + onlineState = "idle"; + else if (onlineUserClients.length) + onlineState = "online"; + else + onlineState = "offline"; + usersMap[uid] = { + email: user.email, + fullname: user.fullname, + uid: user.uid, + clients: onlineUserClients, + online: onlineUserClients.length, + state: onlineState, + author: authorPool[uid], + color: colorPool[uid] + }; + }); + + broadcast({ + type: "USER_JOIN", + data: { + userId: userId, + clientId: clientId, + user: usersMap[userId] + } + }, client); + + Store.recentChatHistory(100, function (err, chatHistory) { + if (err) + console.error("[vfs-collab] recentChatHistory", err); + + client.send({ + type: "CONNECT", + data: { + myClientId: clientId, + myUserId: userId, + fs: userIds.fs, + authorPool: authorPool, + colorPool: colorPool, + users: usersMap, + chatHistory: chatHistory || [] + } + }); + }); + }); + } +} + +/** + * Returns true if the users has read access to the filesystem + */ +function collabReadAccess(fs) { + return (/r/).test(fs); +} + +/** + * Returns true if the users has write access to the filesystem + */ +function collabWriteAccess(fs) { + return (/w/).test(fs); +} + +/** + * Apply a user's operation to a document + * @param {Object} userIds - user descriptor with: uid, email, fullname, fs, clientId + * @param {String} docId - the document path + * @param {Document} doc - the document to apply the operation on + * @param {Operation} op - the operation to applly + * @param {Function} callback + */ +function applyOperation(userIds, docId, doc, op, callback) { + userIds = userIds || {userId: 0}; + var userId = userIds.userId; + Store.getWorkspaceState(function (err, ws) { + if (err) + return callback(err); + try { + doc.contents = applyContents(op, doc.contents); + applyAuthororAttributes(doc.authAttribs, op, ws.authorPool[userId]); + + wrapSeq(Revision.create({ + operation: new Buffer(JSON.stringify(op)), + author: userId, + revNum: doc.revNum + 1, + document_id: doc.id + }), next); + } catch (e) { + return next(e); + } + }); + function next(err) { + if (err) + return callback(err); + doc.revNum++; + Store.saveDocument(doc, /*["contents", "authAttribs", "revNum"],*/ function (err) { + if (err) + return callback(err); + var msg = { + docId: docId, + clientId: userIds.clientId, + userId: userId, + revNum: doc.revNum, + op: op + }; + callback(null, msg); + }); + } +} + +/** + * Handle user's EDIT_UPDATE for a document + * @param {Object} userIds - user descriptor with: uid, email, fullname, fs, clientId + * @param {Socket} client - the connected collab client + * @param {Object} data - the EDIT_UPDATE message data with the document id, revision number and applied operation + */ +function handleEditUpdate(userIds, client, data) { + var docId = data.docId; + var clientId = userIds.clientId; + var newRev = data.revNum; + var docL; + + function done(err) { + unlock(docId); + if (err) { + console.error("[vfs-collab]", err); + syncCommit(err); + } + } + + // the user's edit couldn't be commited, please try again + function syncCommit(err) { + client.send({ + type: "SYNC_COMMIT", + data: { + docId: docId, + revNum: docL && docL.revNum, + reason: err.message || err, + code: err.code || "SYNC_E" + } + }); + } + + if (!documents[docId] || !documents[docId][clientId] || !client.openDocIds[docId]) + return done("Trying to update a non-member document!", + docId, clientId, documents[docId] && Object.keys(documents[docId]), Object.keys(client.openDocIds), + Object.keys(documents), Object.keys(clients)); + + if (!collabWriteAccess(userIds.fs)) + return done("User " + userIds.userId + " doesn't have write access to edit document " + docId + " - fs: " + userIds.fs); + + // Lock a document while updating - to stop any possible inconsistencies + lock(docId, function () { + Store.getDocument(docId, function (err, doc) { + if (err || !doc) + return done(err || ("No Document to update! " + docId)); + + docL = doc; + + if (doc.revNum !== newRev-1) { // conflicting versions + var err2 = new Error("Version log: " + docId + " " + doc.revNum + " " + newRev); + err2.code = "VERSION_E"; + return done(err2); + } + + // message.author for udno auth attributes + applyOperation(userIds, docId, doc, data.op, function (err, msg) { + if (err) { + var err2 = new Error("OT Error: " + String(err)); + err2.code = "OT_E"; + return done(err2); + } + + msg.selection = data.selection; + + broadcast({ + type: "EDIT_UPDATE", + data: msg + }, client, docId); + + delete msg.op; + delete msg.selection; + + client.send({ + type: "EDIT_UPDATE", + data: msg + }); + + emitter.emit("afterEditUpdate", { + docId: docId, + path: getAbsolutePath(docId), + doc: doc + }); + + done(); + }); + }); + }); +} + +/** + * Handle user's UPDATE_NL_CHAR for a document + * @param {Object} userIds - user descriptor with: uid, email, fullname, fs, clientId + * @param {Socket} client - the connected collab client + * @param {Object} data - the UPDATE_NL_CHAR message data with the document id, newLineChar + */ +function handleUpdateNlChar(userIds, client, data) { + var docId = data.docId; + var newLineChar = data.newLineChar || ""; + + var nlCharLog; + switch (newLineChar) { + case "\n": + nlCharLog = "\\n"; + break; + case "\r\n": + nlCharLog = "\\r\\n"; + break; + default: + nlCharLog = newLineChar.length + ":" + newLineChar; + return missingInfo(); + } + + if (!docId) + return missingInfo(); + + function missingInfo() { + console.error("[vfs-collab] updateNlChar missing info:", docId, nlCharLog); + } + + function done(err) { + unlock(docId); + if (err) + console.error("[vfs-collab] updateNlChar failed:", err); + } + + // Lock a document while updating - to stop any possible inconsistencies + lock(docId, function () { + Store.getDocument(docId, function(err, doc) { + if (err || !doc) + return done((err || "updateNlChar of a non-collab document!") + " : " + docId); + if (doc.newLineChar == newLineChar) + return done(); + doc.newLineChar = newLineChar; + Store.saveDocument(doc, /*["newLineChar"],*/ function (err) { + if (err) + return done(err); + console.error("[vfs-collab] updateNlChar changed", newLineChar); + + broadcast({ + type: "UPDATE_NL_CHAR", + data: { + docId: docId, + newLineChar: newLineChar + } + }, client, docId); + done(); + }); + }); + }); +} + +/** + * Handle user's CHAT_MESSAGE + * @param {Object} userIds - user descriptor with: uid, email, fullname, fs, clientId + * @param {Socket} client - the connected collab client + * @param {Object} data - the CHAT_MESSAGE data with the chat text + */ +function handleChatMessage(userIds, client, data) { + var text = data.text; + var userId = userIds.userId; + + // Save the chat message and broadcast it + Store.saveChatMessage(text, userId, function (err, message) { + if (err) + return console.error("[vfs-collab] saveChatMessage:", err); + var msg = { + type: "CHAT_MESSAGE", + data: { + id: message.id, + userId: userId, + timestamp: message.timestamp, + text: text + } + }; + + broadcast(msg); + }); +} + +/** + * Handle user's CURSOR_UPDATE messages + * @param {Object} userIds - user descriptor with: uid, email, fullname, fs, clientId + * @param {Socket} client - the connected collab client + * @param {Object} data - the CURSOR_UPDATE data with the document id and the user selection + */ +function handleCursorUpdate(userIds, client, data) { + var docId = data.docId; + var clientId = userIds.clientId; + + if (!documents[docId] || !documents[docId][clientId] || !client.openDocIds[docId]) + return console.error("[vfs-collab] Trying to select in a non-member document!", + docId, clientId, documents[docId] && Object.keys(documents[docId]), Object.keys(client.openDocIds), + Object.keys(documents), Object.keys(clients)); + + documents[docId][clientId].selection = data.selection; + data.clientId = clientId; + data.userId = userIds.userId; + broadcast({ + type: "CURSOR_UPDATE", + data: data + }, client, docId); +} + +/** + * Broadcast a message to all or a selected group of connected collab clients + * @param {Object} message - the message to broadcast + * @param {Socket} sender - optional, when we want to exclude the sender from the group to send the message to + * @param {String} docId - the document id or path + */ +function broadcast(message, sender, docId) { + var toClientIds = docId ? documents[docId] : clients; + var audienceNum = 0; + for (var clientId in toClientIds) { + var client = clients[clientId]; + // Exclude sender if exists + if (client === sender || !client) + continue; + client.send(message); + audienceNum++; + } + // if (audienceNum) + // console.error("[vfs-collab] Broadcast to:", audienceNum, "clients", message); +} + +function getAbsolutePath(docId) { + if (docId[0] === "~") + return Path.join(getHomeDir(), docId.substring(1)); + else + return Path.join(basePath, docId); +} + +/** + * Watch documents for other filesystem changes and sync them back to the collab documents + * @param docId - the document id or path + */ +function initVfsWatcher(docId) { + var absPath = getAbsolutePath(docId); + + function done(err) { + if (err) + console.error("[vfs-collab] WATCH ERR:", docId, err); + unlock(docId); + } + + // Check if a collab document sync is needed, apply it and save to the filesystem + function doWatcherSync(stats, next) { + var mtime = new Date(stats.mtime).getTime(); + var watcher = watchers[docId]; + var timeDiff = mtime - watcher.mtime; + if (watcher.mtime && timeDiff < 1) + return; + lock(docId, function () { + console.error("[vfs-collab] WATCH SYNC:", docId, timeDiff); + watcher.mtime = mtime; + Store.getDocument(docId, function (err, oldDoc) { + if (err) + return next(err); + syncDocument(docId, oldDoc, function (err, doc2) { + if (err) + return next(err); + doSaveDocument(docId, doc2, -1, true, next); + }); + }); + }); + } + + localfsAPI.watch(absPath, {}, function (err, meta) { + if (err) + return console.error("[vfs-collab] WATCH INIT ERR:", docId, err); + + var watcher = meta.watcher; + watcher.on("change", function (event, filename, stat, files) { + console.error("[vfs-collab] WATCH CHANGE:", docId, "mtime:", new Date(stat.mtime).getTime()); + doWatcherSync(stat, done); + }); + watcher.on("error", function(err){ + console.error("[vfs-collab] WATCH ERR:", docId, err); + }); + watchers[docId] = watcher; + watcher.mtime = Date.now(); + Fs.stat(absPath, function (err, stat) { + if (err) return; + watcher.mtime = new Date(stat.mtime).getTime(); + }); + }); +} + +/** + * Handle user's JOIN_DOC messages - a user is joining a document to collaborate on + * @param {Object} userIds - user descriptor with: uid, email, fullname, fs, clientId + * @param {Socket} client - the connected collab client + * @param {Object} data - the JOIN_DOC data with the document id + */ +function handleJoinDocument(userIds, client, data) { + var docId = data.docId; + var clientId = userIds.clientId; + var userId = userIds.userId; + + function done(err) { + if (err) { + console.error("[vfs-collab] handleJoinDocument ERR:", docId, err); + client.send({ + type: "JOIN_DOC", + data: { + clientId: clientId, + docId: docId, + err: err + } + }); + } + unlock(docId); + } + + lock(docId, function() { + Store.getDocument(docId, function(err, doc) { + if (err) + return done("getDocument " + String(err)); + + if (doc && documents[docId]) + return fetchMetadataThenJoinDocument(doc); + + console.error("[vfs-collab] Joining a closed document", docId, " - Syncing"); + syncDocument(docId, doc, function(err, doc2) { + if (err) + return done(err); + fetchMetadataThenJoinDocument(doc2); + }); + }); + }); + + function fetchMetadataThenJoinDocument(doc) { + localfsAPI.getMetadata(docId, { sandbox: basePath }, function(err, metadata) { + if (err) + console.error("[vfs-collab] Warning: failed to fetch metadata!", docId, err); + var file = getAbsolutePath(docId); + isVeryLargeFile(file, doc.contents, function(err, isLarge) { + if (err) + console.error("[vfs-collab] isVeryLargeFile failed:", err); + if (!isLarge) + return joinDocument(doc, String(metadata || "")); + client.send({ + type: "LARGE_DOC", + data: { + userId: userId, + clientId: clientId, + docId: docId, + response: true + } + }); + }); + }); + } + + function joinDocument(doc, metadata) { + if (!documents[docId]) { + documents[docId] = {}; + initVfsWatcher(docId); + console.error("[vfs-collab] User", userId, "is joining document", docId); + } + else { + console.error("[vfs-collab] User", userId, "is joining a document", docId, "with", + Object.keys(documents[docId]).length, "other document members"); + } + + var docHash = hashString(doc.contents); + + var clientDoc = JSON.stringify({ + selections: documents[docId], + authAttribs: doc.authAttribs, + contents: doc.contents.toString(), + metadata: metadata, + fsHash: doc.fsHash, + docHash: docHash, + revNum: doc.revNum, + newLineChar: doc.newLineChar, + created_at: doc.created_at, + updated_at: doc.updated_at + }); + + documents[docId][clientId] = userIds; + client.openDocIds[docId] = true; + + // Cut the document to pices and stream to the client + var chunkSize = 10*1024; // 10 KB + var contentsLen = clientDoc.length; + var chunksLen = Math.ceil(contentsLen / chunkSize); + for (var i = 0; i < contentsLen; i += chunkSize) { + var chunk = clientDoc.slice(i, i + chunkSize); + client.send({ + type: "JOIN_DOC", + data: { + userId: userId, + clientId: clientId, + docId: docId, + reqId: data.reqId, + chunkNum: (i / chunkSize) + 1, + chunksLength: chunksLen, + chunk: chunk + } + }); + } + + broadcast({ + type: "JOIN_DOC", + data: { + docId: docId, + userId: userId, + clientId: clientId + } + }, client); + + done(); + } +} + +/** + * Normalize text line terminators for collab index-based calculations to seamlessly work + * @param {String} text + * @return {String} normalized + */ +function normalizeTextLT(text) { + return text.replace(/\r\n|\r/g, "\n"); +} + +// return "\n" or "\r\n" or null +function detectNewLineChar(text) { + // Must be the strictly same as on the client + // (and note that Ace doesn't have \r newLine mode) + var match = text.match(/^.*?(\r\n|\n)/m); + return match && match[1]; +} + +/** + * Synchronize collab document state with the filesystem state (utilizing hashes) + * + * @param {String} docId - the document id or path + * @param {Document} doc - the collab document + * @param {Function} callback + */ +function syncDocument(docId, doc, callback) { + var file = getAbsolutePath(docId); + isBinaryFile(file, function (err, isBinary) { + if (err) + return callback(new Error("SYNC: Binary check failed - ERR: " + String(err))); + if (isBinary) + return callback(new Error("SYNC: Binary file opened " + isBinary)); + + isVeryLargeFile(file, null, function(err, isLarge) { + if (err) + return callback(err); + + if (!isLarge) + return doSyncDocument(); + + console.error("[vfs-collab] File is too large, ignoring: " + file); + err = new Error("File is too large"); + err.code = "ELARGE"; + callback(err); + }); + }); + + + function doSyncDocument() { + Fs.readFile(file, "utf8", function (err, contents) { + if (err) + return callback(err); + + // "\n" or "\r\n" or null + var newLineChar = detectNewLineChar(contents); + var oldNewLineChar = doc && doc.newLineChar || DEFAULT_NL_CHAR_DOC; + var normContents = normalizeTextLT(contents); + + var fsHash = hashString(normContents); + + // HACK: fsHash from database is unreliable (https://github.com/c9/newclient/issues/3980) + if (doc) + doc.fsHash = hashString(doc.contents); + + if (!doc) { + console.error("[vfs-collab] SYNC: Creating document:", docId, fsHash); + + Store.newDocument({ + path: docId, + contents: normContents, + fsHash: fsHash, + newLineChar: newLineChar + }, callback); + } + // update database OT state + else if (fsHash !== doc.fsHash && doc.contents != normContents) { + // if (doc.contents != normalizeTextLT(doc.contents)) { + // debugger + // doc.contents = normalizeTextLT(doc.contents); + // } + var op = operations.operation(doc.contents, normContents); + console.error("[vfs-collab] SYNC: Updating document:", docId, op.length, fsHash, doc.fsHash); + // non-user sync operation + doc.fsHash = fsHash; // applyOperation will save it for me + + doc.newLineChar = newLineChar || oldNewLineChar; + applyOperation(null, docId, doc, op, function (err, msg) { + if (err) + return callback("SYNC: Failed updating OT database document state! " + String(err)); + msg.sync = true; + broadcast({ + type: "EDIT_UPDATE", + data: msg + }, null, docId); + + checkNewLineChar(); + callback(null, doc); + }); + } + else { + checkNewLineChar(); + callback(null, doc); + } + + function checkNewLineChar() { + if (newLineChar && oldNewLineChar !== newLineChar) { + broadcast({ + type: "UPDATE_NL_CHAR", + data: { + oldNewLineChar: oldNewLineChar, + newLineChar: newLineChar + } + }, null, docId); + doc.newLineChar = newLineChar || oldNewLineChar; + } + } + }); + } +} + +/** + * Handle user's GET_REVISIONS messages - retrive the revision history of the file + * + * @param {Object} userIds - user descriptor with: uid, email, fullname, fs, clientId + * @param {Socket} client - the connected collab client + * @param {Object} data - the JOIN_DOC data with the document id + */ +function handleGetRevisions(userIds, client, data) { + var docId = data.docId; + + function done(err) { + if (err) + console.error("[vfs-collab] handleGetRevisions ERR:", docId, err); + unlock(docId); + } + + lock(docId, function () { + Store.getDocument(docId, function (err, doc) { + if (err) + return done("getDocument " + String(err)); + + Store.getRevisions(doc, function (err, revisions) { + if (err || !revisions) + return done("getRevisions " + (revisions || []).length + " " + String(err)); + + var docRevisions = JSON.stringify({ + revisions: revisions, + starRevNums: doc.starRevNums, + revNum: doc.revNum + }); + + // Cut the revisions into pices and stream to the client + var chunkSize = 10*1024; // 10 KB + var contentsLen = docRevisions.length; + var chunksLen = Math.ceil(contentsLen / chunkSize); + for (var i = 0; i < contentsLen; i += chunkSize) { + var chunk = docRevisions.slice(i, i + chunkSize); + client.send({ + type: "GET_REVISIONS", + data: { + userId: userIds.userId, + clientId: userIds.clientId, + docId: data.docId, + chunkNum: (i / chunkSize) + 1, + chunksLength: chunksLen, + chunk: chunk + } + }); + } + done(); + }); + }); + }); +} + +/** + * Handle user's SAVE_FILE messages - save collab documents + * @param {Object} userIds - user descriptor with: uid, email, fullname, fs, clientId + * @param {Socket} client - the connected collab client + * @param {Object} data - the SAVE_FILE data with the document id and wether to sielently save (with auto-save enabled) or star the save + */ +function handleSaveFile(userIds, client, data) { + var st = Date.now(); + var docId = data.docId; + var userId = userIds.userId; + + function done(err) { + unlock(docId); + if (err) { + console.error("[vfs-collab]", err); + client.send({ + type: "FILE_SAVED", + data: { + docId: docId, + err: err + } + }); + } + } + + console.error("[vfs-collab] Saving file", docId); + + lock(docId, function () { + Store.getDocument(docId, ["contents", "revNum", "starRevNums"], function (err, doc) { + if (err || !doc) + return done((err || "Writing a non-collab document!") + " : " + docId); + + if (watchers[docId]) + watchers[docId].mtime = Date.now(); + + var absPath = getAbsolutePath(docId); + var fileContents = doc.contents.replace(/\n/g, doc.newLineChar || DEFAULT_NL_CHAR_FILE); + + function mkfileWriteFile() { + var options = { bufferWrite: true }; + var stream = options.stream = new Stream(); + stream.readable = true; + localfsAPI.mkfile(absPath, options, writeFileCallback); + stream.emit("data", fileContents); + stream.emit("end"); + } + + /* + function regularWriteFile() { + Fs.writeFile(absPath, doc.contents, "utf8", writeFileCallback); + } + */ + + function writeFileCallback(err) { + if (err) + return done("Failed saving file ! : " + docId + " ERR: " + String(err)); + doSaveDocument(docId, doc, userId, !data.silent, function (err) { + console.error("[vfs-collab] Saving took", Date.now() - st, "ms - file:", docId, !err); + done(err); + }); + } + + mkfileWriteFile(); + }); + }); +} + +/** + * Apply the save to the collab document, update the hash and optionally add a star revision + * @param {String} docId - the document id or path + * @param {Document} doc - the collab document + * @param {String} userId - the user id + * @param {Boolean} star - add a star to the document if not triggered by auto-save + * @param {Function} callback + */ +function doSaveDocument(docId, doc, userId, star, callback) { + if (star && doc.starRevNums.indexOf(doc.revNum) === -1) + doc.starRevNums.push(doc.revNum); + + var fsHash = doc.fsHash = hashString(doc.contents); + Store.saveDocument(doc, /*["fsHash", "starRevNums"],*/ function (err) { + if (err) + return callback(err); + console.error("[vfs-collab] starRevision added", doc.revNum); + var data = { + userId: userId, + docId: docId, + star: star, + revNum: doc.revNum, + fsHash: fsHash + }; + broadcast({ + type: "FILE_SAVED", + data: data + }, null, docId); + callback(); + }); +} + +/** + * Handle user's LEAVE_DOC messages - client closing a collab document + * @param {Object} userIds - user descriptor with: uid, email, fullname, fs, clientId + * @param {Socket} client - the connected collab client + * @param {Object} data - the LEAVE_DOC data with the document id + */ +function handleLeaveDocument(userIds, client, data) { + var docId = data.docId; + var userId = userIds.userId; + var clientId = userIds.clientId; + if (!documents[docId] || !documents[docId][clientId] || !client.openDocIds[docId]) + return console.error("[vfs-collab] Trying to leave a non-member document!", + docId, clientId, documents[docId] && Object.keys(documents[docId]), Object.keys(client.openDocIds), + Object.keys(documents), Object.keys(clients)); + delete client.openDocIds[docId]; + console.error("[vfs-collab]", clientId, "is leaving document", docId); + delete documents[docId][clientId]; + if (!Object.keys(documents[docId]).length) { + console.error("[vfs-collab] Closing document", docId); + closeDocument(docId); + } + + broadcast({ + type: "LEAVE_DOC", + data: { + docId: docId, + userId: userId, + clientId: clientId + } + }, client); +} + +/** + * Handle user's LARGE_DOC messages - document has grown too large for collab + * + * @param {Object} userIds - user descriptor with: uid, email, fullname, fs, clientId + * @param {Socket} client - the connected collab client + * @param {Object} data - the LEAVE_DOC data with the document id + */ +function handleLargeDocument(userIds, client, data) { + var docId = data.docId; + var userId = userIds.userId; + var clientId = userIds.clientId; + console.error("[vfs-collab] ", docId); + delete documents[docId][clientId]; + if (!Object.keys(documents[docId]).length) { + console.log("[vfs-collab] File has grown too large, ignoring: " + docId); + closeDocument(docId); + } + + broadcast({ + type: "LARGE_DOC", + data: { + docId: docId, + userId: userId, + clientId: clientId + } + }, client); +} + +/** + * Handle user's USER_STATE messages - update connected clients with user state + * @param {Object} userIds - user descriptor with: uid, email, fullname, fs, clientId + * @param {Socket} client - the connected collab client + * @param {Object} data - the JOIN_DOC data with the document id + */ +function handleUserState(userIds, client, data) { + var userId = userIds.userId; + var clientId = userIds.clientId; + console.error("[vfs-collab]", clientId, "is switching to", data.state); + clients[clientId].state = data.state; + var isUserIdle = Object.keys(clients) + .map(function(cliId) { + return clients[cliId]; + }).filter(function(cl) { + return cl.userIds.userId === userId; + }).reduce(function(isIdle, cl) { + return isIdle && cl.state === "idle"; + }, true); + + broadcast({ + type: "USER_STATE", + data: { + state: isUserIdle ? "idle" : "online", + userId: userId, + clientId: clientId + } + }, client); +} + +/** + * Clears specific chat messages or complete chat history + * + * @param {Object} userIds - user descriptor with: uid, email, fullname, fs, clientId + * @param {Socket} client - the connected collab client + * @param {Object} data - either id: $id of the message to be deleted or clear: true to clear all of the chat history + */ +function handleClearChat(userIds, client, data) { + console.error("[vfs-collab] Clear chat history: ", data.id, data.clear, userIds.fs); + + if (!collabWriteAccess(userIds.fs)) + return console.error("[vfs-collab] clearChat: User don't have write access!"); + + var stmt; + if (data.clear) + stmt = ChatMessage.destroy({}, {truncate: true}); + else if (data.id) + stmt = ChatMessage.destroy({id: data.id}); + else + return console.error("[vfs-collab] clearChat: Invalid message", data); + + wrapSeq(stmt, function(err) { + console.error("[vfs-collab] Chat clear:", err ? err : "SUCCESS"); + if (err) + return; + broadcast({ + type: "CLEAR_CHAT", + data: data + }); + }); +} + +/** + * Clears specific chat messages or complete chat history + * + * @param {Object} userIds - user descriptor with: uid, email, fullname, fs, clientId + * @param {Socket} client - the connected collab client + * @param {Object} data - either id: $id of the message to be deleted or clear: true to clear all of the chat history + */ +function broadcastUserMessage(userIds, client, data) { + console.error("[vfs-collab] Clear chat history: ", data.id, data.clear, userIds.fs); + + broadcast({ + type: "MESSAGE", + data: data + }, client); +} + +/** + * Handle any user message by routing to its proper handler + * + * @param {Object} userIds - user descriptor with: uid, email, fullname, fs, clientId + * @param {Socket} client - the connected collab client + * @param {Object} data - the SAVE_FILE data with the document id and wether to sielently save (with auto-save enabled) or star the save + */ +function handleUserMessage(userIds, client, message) { + var data = message.data || {}; + var docId = data.docId || ""; + if (docId[0] === "/") + data.docId = docId.slice(1); + switch (message.type) { + case "JOIN_DOC": + handleJoinDocument(userIds, client, data); + break; + case "GET_REVISIONS": + handleGetRevisions(userIds, client, data); + break; + case "LEAVE_DOC": + handleLeaveDocument(userIds, client, data); + break; + case "LARGE_DOC": + handleLargeDocument(userIds, client, data); + break; + case "EDIT_UPDATE": + handleEditUpdate(userIds, client, data); + break; + case "UPDATE_NL_CHAR": + handleUpdateNlChar(userIds, client, data); + break; + case "CURSOR_UPDATE": + handleCursorUpdate(userIds, client, data); + break; + case "SAVE_FILE": + handleSaveFile(userIds, client, data); + break; + case "CHAT_MESSAGE": + handleChatMessage(userIds, client, data); + break; + case "USER_STATE": + handleUserState(userIds, client, data); + break; + case "CLEAR_CHAT": + handleClearChat(userIds, client, data); + break; + case "PING": + client.send({type: "PING"}); + break; + case "MESSAGE": + broadcastUserMessage(userIds, client, data); + break; + default: + throw new Error("Unknown message message type: " + message.type); + } +} + + +/** + * @param {Object} userIds - user descriptor with: uid, email, fullname, fs, clientId + * @param {Socket} client - the just-connected collab client + */ +function onConnect(userIds, client) { + var userId = userIds.userId; + var clientId = userIds.clientId; + + console.error("[vfs-collab] CONNECTED UserID: " + userId + " & ClientId: " + clientId); + + client.on("message", function (messag) { + // console.error("[vfs-collab] Message from ", userIds, ": " + messag); + try { + messag = JSON.parse(messag); + } catch (e) { + return console.error("[vfs-collab] Can't parse client data!", messag); + } + try { + handleUserMessage(userIds, client, messag); + } catch (e) { + return console.error("[vfs-collab] Can't handle user messag", messag, e); + } + }); + + handleConnect(userIds, client); + + client.on("disconnect", function () { + for (var docId in client.openDocIds) + handleLeaveDocument(userIds, client, {docId: docId}); + broadcast({ + type: "USER_LEAVE", + data: { + userId: userId, + clientId: clientId + } + }, client); + console.error("[vfs-collab] DISCONNECTED a socket with userId " + userId); + }); +} + +var compressTimers = {}; + +/** + * Close a document because it's no more open for collaboration, close the watcher and schedule a compression + * @param {String} docId - the document id or path + */ +function closeDocument(docId) { + delete documents[docId]; + + if (compressTimers[docId]) + clearTimeout(compressTimers[docId]); + compressTimers[docId] = setTimeout(function () { + delete compressTimers[docId]; + compressDocument(docId, { + MAX_REVISION_NUM: 256, + COMPRESSED_REV_NUM: 128 + }); + }, 100000); + + if (watchers[docId]) { + watchers[docId].close(); + delete watchers[docId]; + } +} + +/** + * Pack documents' revisions if they go beyond a certain threshould: options.MAX_REVISION_NUM + * to put it back to a reasonable number of revisions: options.COMPRESSED_REV_NUM + * + * It applies multiple heuristic algorithms to combine revisions trying not to lose any authorship information + * + * @param {String} docId - the document id or path + * @param {Object} options - compression configuration parameters + * @param {Function} callback + */ +function compressDocument(docId, options, callback) { + if (documents[docId]) + return; + + var ALREADY_COMPRESSED = "ALREADY_COMPRESSED"; + var MAX_REVISION_NUM = options.MAX_REVISION_NUM; + var COMPRESSED_REV_NUM = options.COMPRESSED_REV_NUM; + + var doc, revisions, path; + var newRevisions, newStarRevNums; + var starsHash, rev0Contents, lastRevTime, docTimeDiff, optimalRevTimeDiff; + + // compaction modes + var mergeDifferentAuthors = false; + var isAggressive = false; + + var secondTime = 1000; + var minuteTime = secondTime * 60; + var hourTime = minuteTime * 60; + var dayTime = hourTime * 24; + var fourDaysTime = dayTime << 2; + + function done(err) { + unlock(docId); + if (err === ALREADY_COMPRESSED) + err = undefined; + if (err) + console.error("[vfs-collab] ERROR Closing Document", docId, err); + callback && callback(err); + } + + function cloneRevision(rev, revNum) { + return { + document_id: rev.document_id, + operation: rev.operation.slice(), + author: rev.author, + revNum: revNum, + created_at: rev.created_at, + updated_at: rev.updated_at + }; + } + + function shouldMergeTimeDiff(rev, lastRev) { + if (lastRev.author != rev.author) { + if (mergeDifferentAuthors) + lastRev.author = "0"; + else + return false; + } + + var latestRevDiff = lastRevTime - rev.created_at; + var prevRevDiff = rev.created_at - lastRev.created_at; + + if (isAggressive) + return prevRevDiff < (optimalRevTimeDiff << 1); + + if (latestRevDiff < hourTime) + // previous revision is < 8-seconds away (co-editing) + return prevRevDiff < (secondTime << 3); + else if (latestRevDiff < dayTime) + // previous revision is < 4-minutes away + return prevRevDiff < (minuteTime << 2); + else if (latestRevDiff < fourDaysTime) + // previous revision is < 1-hour away + return prevRevDiff < (hourTime); + else + return prevRevDiff < optimalRevTimeDiff; + } + + lock(docId, function() { + async.series([ + function (next) { + Store.getDocument(docId, function (err, docL) { + if (err || !docL) + return next(err || "No document to close!"); + path = docL.path; + doc = docL; + next(); + }); + }, + function (next) { + Store.getRevisions(doc, function (err, revisionsL) { + if (err || !revisionsL) + return next(err || "No document revisions found!"); + if (revisionsL.length < MAX_REVISION_NUM) + return next(ALREADY_COMPRESSED); + revisions = revisionsL; + next(); + }); + }, + function prepare(next) { + // compress to the latest N/2 saves only + var newStars = doc.starRevNums.slice(-COMPRESSED_REV_NUM); + + starsHash = {}; + var i; + for (i = 0; i < newStars.length; i++) + starsHash[newStars[i]] = true; + + rev0Contents = doc.contents; + for (i = revisions.length - 1; i > 0; i--) { + var op = operations.inverse(revisions[i].operation); + revisions[i].contents = rev0Contents; + rev0Contents = applyContents(op, rev0Contents); + } + + lastRevTime = revisions[revisions.length-1].created_at; + docTimeDiff = lastRevTime - revisions[0].created_at; + optimalRevTimeDiff = docTimeDiff / COMPRESSED_REV_NUM; + + next(); + }, + function compressDoc(next) { + var shouldCompress = revisions.length - COMPRESSED_REV_NUM; + + console.error("[vfs-collab] Compress document trial", docId, shouldCompress, mergeDifferentAuthors, isAggressive); + + newRevisions = [ cloneRevision(revisions[0], 0) ]; + newStarRevNums = []; + + var lastRev = {author: -9}; + var prevContents, prevLastContents; + var lastContents = rev0Contents; + var i, rev; + for (i = 1; i < revisions.length && shouldCompress; i++) { + rev = revisions[i]; + prevLastContents = lastContents; + lastContents = applyContents(rev.operation, lastContents); + // Check if can merge revisions and clear lastRev's author if different & can merge different authors + // TODO: remove the side-effect on parameters the function do + if (shouldMergeTimeDiff(rev, lastRev)) { + var compressedOp = operations.operation(prevContents, lastContents); + lastRev.operation = compressedOp; + shouldCompress--; + } + else { + lastRev = cloneRevision(rev, newRevisions.length); + newRevisions.push(lastRev); + prevContents = prevLastContents; + } + if (starsHash[i] && !lastRev.isStar) { + newStarRevNums.push(lastRev.revNum); + lastRev.isStar = true; + } + } + if (!shouldCompress) { + while (i < revisions.length) { + newRevisions.push(cloneRevision(revisions[i++], newRevisions.length)); + } + } + else if (!mergeDifferentAuthors) { + console.error("[vfs-collab] Merge single-author failed to compact the document enough", revisions.length, newRevisions.length); + mergeDifferentAuthors = true; + return compressDoc(next); + } + else if (!isAggressive) { + console.error("[vfs-collab] Merge multi-author failed to compact the document enough", revisions.length, newRevisions.length); + isAggressive = true; + return compressDoc(next); + } + else if (newRevisions.length >= MAX_REVISION_NUM) { + console.error("[vfs-collab] All compression modes failed to compact the document enough", revisions.length, newRevisions.length); + } + + console.error("[vfs-collab] Compressed document:", revisions.length, newRevisions.length, + "Different Authors:", mergeDifferentAuthors, + "isAggressive:", isAggressive); + + // var newContents = rev0Contents; + // for (i = 1; i < newRevisions.length; i++) { + // var newRev = newRevisions[i]; + // newContents = applyContents(newRev.operation, newContents); + // } + // console.error("[vfs-collab] Compressed document:", newContents == doc.contents, revisions.length, newRevisions.length); + // console.error("[vfs-collab] New Revisions:", newRevisions); + // console.error("[vfs-collab] Stars:", doc.starRevNums, newStarRevNums); + + next(); + }, + function (next) { + wrapSeq(Revision.destroy({document_id: doc.id}), next); + }, + function (next) { + doc.starRevNums = newStarRevNums; + doc.revNum = newRevisions.length - 1; + Store.saveDocument(doc, /*["revNum", "starRevNums"],*/ next); + }, + function (next) { + newRevisions.forEach(function(newRev) { + delete newRev.isStar; + newRev.operation = JSON.stringify(newRev.operation); + }); + wrapSeq(Revision.bulkCreate(newRevisions), next); + } + ], done); + }); +} + +// ********* VFS Stream, net.Socket Collab Communication Infrastructure ************ // + +/** + * Create the collab socket net.Server + * The net.Server is file-socket to allow multiple collab-enabled workspaces on SSH workspaces + */ +function createServer() { + var server = net.createServer(function(client) { + + // console.error("[vfs-collab] Client connected"); + var userIds; + var isClosed = false; + + client.send = function (msg) { + if (isClosed) + return; + msg.command = msg.command || "vfs-collab"; + var strMsg = JSON.stringify(msg); + client.write(strMsg + "\0\0"); + }; + + client.on("data", function handShake(data) { + client.removeListener("data", handShake); + client.on("data", onData); + + userIds = JSON.parse(data); + if (!collabReadAccess(userIds.fs)) + return console.error("[vfs-collab] Client don't have read access to workspace! - " + + "Note that visitors of private workspaces can't use collab features"); + + client.userIds = userIds; + client.openDocIds = {}; + clients[userIds.clientId] = client; + // console.error("[vfs-collab] Server handshaked", Object.keys(clients).length); + + // handshaking the client + client.write(data.toString()); + + if (server.collabInited) + onConnect(userIds, client); + else + server.once("collabInited", function() { + onConnect(userIds, client); + }); + }); + + var buff = []; + + function onData(data) { + data = data.toString(); + var idx; + while (true) { + idx = data.indexOf("\0\0"); + if (idx === -1) + return data && buff.push(data); + buff.push(data.substring(0, idx)); + var clientMsg = buff.join(""); + data = data.substring(idx + 2); + buff = []; + client.emit("message", clientMsg); + } + } + + client.on("close", onClose); + client.on("end", onClose); + + function onClose() { + if (isClosed) + return; + isClosed = true; + delete clients[userIds.clientId]; + client.emit("disconnect"); + // console.error("[vfs-collab] Client disconnected", Object.keys(clients).length); + } + + client.on("error", function (err) { + onClose(); + console.error("[vfs-collab] CLIENT SOCKET ERROR", err); + client.destroy(); + }); + }); + return server; +} + + +function initSocket(userIds, callback) { + // var COLLAB_PORT = 33366; + // var COLLAB_HOST = process.env.OPENSHIFT_DIY_IP || "localhost"; + + var projectWD = getProjectWD(); + var server; + var isServer = false; + + // startServer(); + // file sockets can have multiple servers open on the same path + // So, we connect first + var sockPath = process.platform == "win32" + ? "\\\\.\\pipe\\"+ projectWD +"\\collab.sock" + : Path.join(projectWD, "collab.sock"); + clientConnect(); + + function startServer() { + server = createServer(); + console.error("[vfs-collab] PID:", PID, "Socket:", sockPath, + "ClinetId:", userIds.clientId, " & UserId:", userIds.userId); + + async.series([ + function (next) { + // Create the directoty ~/.c9 if not existing + Fs.mkdir(Path.dirname(projectWD), function (err) { + if (err && err.code !== "EEXIST") + return next(err); + next(); + }); + }, + function (next) { + // Create the directoty ~/.c9/$pid if not existing + Fs.mkdir(projectWD, function (err) { + if (err && err.code !== "EEXIST") + return next(err); + next(); + }); + }, + function (next) { + // Remove the stale socket, if existing at ~/.c9/$pid/collab.sock + Fs.unlink(sockPath, function (err) { + if (err && err.code !== "ENOENT") + return next(err); + next(); + }); + }, + ], function(err) { + if (err) + return callback(err); + + function closeServerThenCallback(err) { + try { + console.error("[vfs-collab] Shuting down a faulty collab server - reason: ", err); + server.close(); + } catch(e) { + console.error("[vfs-collab] Can't shutdown faulty collab server", e); + } + callback(err); + } + + server.listen(sockPath, function () { + isServer = true; + server.collabInited = false; + + // init server state + documents = {}; + watchers = {}; + clients = {}; + + // Check server installation, init the server and then connect the client to the inited collab server + installServer(function (err) { + if (err) return closeServerThenCallback(err); + + initDB(false, function (err) { + if (err) + return closeServerThenCallback(err); + server.collabInited = true; + clientConnect(); + server.emit("collabInited"); + }); + }); + + server.on("close", function () { + console.error("[vfs-collab] Server closed"); + // Should handover to another server (if exists) + // e.g. Elect the first client as the new master. + }); + }); + + server.on("error", function (err) { + // if another connection/thread was able to listen as collab-server, let's just connect to it + if (err.code === "EADDRINUSE") + return clientConnect(); + console.error("[vfs-collab] Server error", err); + }); + }); + } + + // Connect to a collab client + // If this fails to connect or the socket file doesn't exist, we try to create the server first + function clientConnect() { + var stream = new Stream(); + stream.readable = true; + + var client = net.connect(sockPath, function () { + client.setTimeout(0); + client.setNoDelay(true); + client.setKeepAlive(true); + + client.userIds = userIds; + client.clientStream = stream; + // console.error("[vfs-collab] User connected:", userIds.clientId); + + client.on("data", function handShake(data) { + // console.error("[vfs-collab]", "Client handshaked", data.toString()); + client.removeListener("data", handShake); + client.on("data", onData); + }); + + var buff = []; + + function onData(data) { + data = data.toString(); + var idx; + while (true) { + idx = data.indexOf("\0\0"); + if (idx === -1) + return buff.push(data); + buff.push(data.substring(0, idx)); + var streamData = buff.join(""); + data = data.substring(idx + 2); + buff = []; + stream.emit("data", streamData); + } + } + + client.on("close", function() { + // console.error("[vfs-collab] Connection closed :", userIds.userId); + stream.emit("end"); + }); + + client.write(JSON.stringify(userIds), "utf8", function() { + callback(null, client, isServer && server); + }); + }); + + client.on("error", function (err) { + if (err && (err.code === "ECONNREFUSED" || err.code === "ENOENT")) { + startServer(); + } + else { + console.error("[vfs-collab] CLIENT SOCK ERR", err, client.userIds); + // mock client.write + client.write = function () { + console.error("[vfs-collab] CLIENT SOCK WRITE AFTER ERROR", client.userIds); + console.trace(); + }; + stream.emit("end"); + } + }); + } +} + +/** + * Export the vfs extend API hook + * Receive the user and project identification thorugh the vfs-extend server-verified options + * + * @param {Vfs} vfs - an instance of localfs.js + * @param {Object} options - { user: {}, project: {} } + * @param {Function} register - register the collab server API + */ +var exports = module.exports = function(vfs, options, register) { + + var vfsClientMap = {}; + var isMaster; + + localfsAPI = vfs; + + function connect(opts, callback) { + var user = options.user; + var project = options.project; + var clientId = opts.clientId; + + if (!user || !project || !clientId || !opts.basePath) + return callback(new Error("[OT] Invalid or icomplete collab options passed: " + opts.basePath + " " + clientId )); + + PID = project.pid || project.id; + basePath = Path.normalize(opts.basePath); + + var userIds = { + userId: user.uid || user.id, + email: user.email, + fullname: user.fullname, + clientId: clientId, + fs: options.readonly ? "r" : "rw" + }; + + function cleanOldClient() { + if (!vfsClientMap[clientId]) + return; + console.error("[vfs-collab] Disposing old client - possible reconnect?", clientId); + dispose(clientId); + } + + cleanOldClient(); + + initSocket(userIds, function (err, client, server) { + if (err) + return callback(err.message ? err : new Error(err)); + + client.netServer = server; + + cleanOldClient(); + vfsClientMap[clientId] = client; + isMaster = !!server; + + callback(null, { + stream: client.clientStream, + isMaster: isMaster + }); + }); + } + + function send(clientId, msg) { + // console.error("[vfs-collab] IN-STREAM", msg); + var client = vfsClientMap[clientId]; + if (client) + client.write(JSON.stringify(msg)+"\0\0"); + } + + function dispose(clientId) { + var client = vfsClientMap[clientId]; + if (!client) + return; + client.end(); + client.destroy(); + // TODO: properly handover + // if (client.netServer) + // client.netServer.close(); + delete vfsClientMap[clientId]; + } + + /** + * Get a `Document` from the database given its path + * @param {String} path the document path to query the database with + * @param [{String}] attributes - optional + * @param {Function} callback + * @param {Object} callback.err + * @param {Object} callback.result The result, or null if getDocument() failed (might even though err is null) + */ + function getDocument(path, attributes, callback) { + if (!Document) { + console.log("Initializing collab db for read access"); + return initDB(true, getDocument.bind(null, path, attributes, callback)); + } + Store.getDocument(path, attributes, callback); + } + + register(null, { + connect: connect, + send: send, + dispose: dispose, + getDocument: getDocument, + emitter: emitter + }); +}; + +// export for testing +exports.Store = Store; +exports.compressDocument = compressDocument; + +/* Google diff match patch library: https://code.google.com/p/google-diff-match-patch/ */ + +var DIFF_EQUAL = 0; +var DIFF_INSERT = 1; +var DIFF_DELETE = -1; +function diff_match_patch(){this.Diff_Timeout=1;this.Diff_EditCost=4;this.Match_Threshold=0.5;this.Match_Distance=1E3;this.Patch_DeleteThreshold=0.5;this.Patch_Margin=4;this.Match_MaxBits=32} +diff_match_patch.prototype.diff_main=function(a,b,c,d){"undefined"==typeof d&&(d=0>=this.Diff_Timeout?Number.MAX_VALUE:(new Date).getTime()+1E3*this.Diff_Timeout);if(null==a||null==b)throw Error("Null input. (diff_main)");if(a==b)return a?[[0,a]]:[];"undefined"==typeof c&&(c=!0);var e=c,f=this.diff_commonPrefix(a,b),c=a.substring(0,f),a=a.substring(f),b=b.substring(f),f=this.diff_commonSuffix(a,b),g=a.substring(a.length-f),a=a.substring(0,a.length-f),b=b.substring(0,b.length-f),a=this.diff_compute_(a,b,e,d);c&&a.unshift([0,c]);g&&a.push([0,g]);this.diff_cleanupMerge(a);return a}; +diff_match_patch.prototype.diff_compute_=function(a,b,c,d){if(!a)return[[1,b]];if(!b)return[[-1,a]];var e=a.length>b.length?a:b,f=a.length>b.length?b:a,g=e.indexOf(f);if(-1!=g)return c=[[1,e.substring(0,g)],[0,f],[1,e.substring(g+f.length)]],a.length>b.length&&(c[0][0]=c[2][0]=-1),c;if(1==f.length)return[[-1,a],[1,b]];return(e=this.diff_halfMatch_(a,b))?(f=e[0],a=e[1],g=e[2],b=e[3],e=e[4],f=this.diff_main(f,g,c,d),c=this.diff_main(a,b,c,d),f.concat([[0,e]],c)):c&&100c);u++){for(var n=-u+q;n<=u-s;n+=2){var l=g+n,m;m=n==-u||n!=u&&j[l-1]d)s+=2;else if(r>e)q+=2;else if(p&&(l=g+k-n,0<=l&&l=t)return this.diff_bisectSplit_(a,b,m,r,c)}}for(n=-u+o;n<=u-v;n+=2){l=g+n;t=n==-u||n!=u&&i[l-1]d)v+=2;else if(m>e)o+=2;else if(!p&&(l=g+k-n,0<=l&&l=t)))return this.diff_bisectSplit_(a,b,m,r,c)}}return[[-1,a],[1,b]]}; +diff_match_patch.prototype.diff_bisectSplit_=function(a,b,c,d,e){var f=a.substring(0,c),g=b.substring(0,d),a=a.substring(c),b=b.substring(d),f=this.diff_main(f,g,!1,e),e=this.diff_main(a,b,!1,e);return f.concat(e)}; +diff_match_patch.prototype.diff_linesToChars_=function(a,b){function c(a){for(var b="",c=0,f=-1,g=d.length;fd?a=a.substring(c-d):c=a.length?[h,j,n,l,g]:null}if(0>=this.Diff_Timeout)return null;var d=a.length>b.length?a:b,e=a.length>b.length?b:a;if(4>d.length||2*e.lengthd[4].length?g:d:d:g;var j;a.length>b.length?(g=h[0],d=h[1],e=h[2],j=h[3]):(e=h[0],j=h[1],g=h[2],d=h[3]);h=h[4];return[g,d,e,j,h]}; +diff_match_patch.prototype.diff_cleanupSemantic=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=0,h=0,j=0,i=0;f=e){if(d>=b.length/2||d>=c.length/2)a.splice(f,0,[0,c.substring(0,d)]),a[f-1][1]=b.substring(0,b.length-d),a[f+1][1]=c.substring(d),f++}else if(e>=b.length/2||e>=c.length/2)a.splice(f,0,[0,b.substring(0,e)]),a[f-1][0]=1,a[f-1][1]=c.substring(0,c.length-e),a[f+1][0]=-1,a[f+1][1]=b.substring(e),f++;f++}f++}}; +diff_match_patch.prototype.diff_cleanupSemanticLossless=function(a){function b(a,b){if(!a||!b)return 6;var c=a.charAt(a.length-1),d=b.charAt(0),e=c.match(diff_match_patch.nonAlphaNumericRegex_),f=d.match(diff_match_patch.nonAlphaNumericRegex_),g=e&&c.match(diff_match_patch.whitespaceRegex_),h=f&&d.match(diff_match_patch.whitespaceRegex_),c=g&&c.match(diff_match_patch.linebreakRegex_),d=h&&d.match(diff_match_patch.linebreakRegex_),i=c&&a.match(diff_match_patch.blanklineEndRegex_),j=d&&b.match(diff_match_patch.blanklineStartRegex_);return i||j?5:c||d?4:e&&!g&&h?3:g||h?2:e||f?1:0}for(var c=1;c=i&&(i=k,g=d,h=e,j=f)}a[c-1][1]!=g&&(g?a[c-1][1]=g:(a.splice(c-1,1),c--),a[c][1]=h,j?a[c+1][1]=j:(a.splice(c+1,1),c--))}c++}};diff_match_patch.nonAlphaNumericRegex_=/[^a-zA-Z0-9]/;diff_match_patch.whitespaceRegex_=/\s/;diff_match_patch.linebreakRegex_=/[\r\n]/;diff_match_patch.blanklineEndRegex_=/\n\r?\n$/;diff_match_patch.blanklineStartRegex_=/^\r?\n\r?\n/; +diff_match_patch.prototype.diff_cleanupEfficiency=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=!1,h=!1,j=!1,i=!1;fb)break;e=c;f=d}return a.length!=g&&-1===a[g][0]?f:f+(b-e)}; +diff_match_patch.prototype.diff_prettyHtml=function(a){for(var b=[],c=/&/g,d=//g,f=/\n/g,g=0;g");switch(h){case 1:b[g]=''+j+"";break;case -1:b[g]=''+j+"";break;case 0:b[g]=""+j+""}}return b.join("")}; +diff_match_patch.prototype.diff_text1=function(a){for(var b=[],c=0;cthis.Match_MaxBits)throw Error("Pattern too long for this browser.");var e=this.match_alphabet_(b),f=this,g=this.Match_Threshold,h=a.indexOf(b,c);-1!=h&&(g=Math.min(d(0,h),g),h=a.lastIndexOf(b,c+b.length),-1!=h&&(g=Math.min(d(0,h),g)));for(var j=1<=i;o--){var v=e[a.charAt(o-1)];k[o]=0===s?(k[o+1]<<1|1)&v:(k[o+1]<<1|1)&v|(q[o+1]|q[o])<<1|1|q[o+1];if(k[o]&j&&(v=d(s,o-1),v<=g))if(g=v,h=o-1,h>c)i=Math.max(1,2*c-h);else break}if(d(s+1,c)>g)break;q=k}return h}; +diff_match_patch.prototype.match_alphabet_=function(a){for(var b={},c=0;c=2*this.Patch_Margin&&e&&(this.patch_addContext_(a,h),c.push(a),a=new diff_match_patch.patch_obj,e=0,h=d,f=g)}1!==i&&(f+=k.length);-1!==i&&(g+=k.length)}e&&(this.patch_addContext_(a,h),c.push(a));return c}; +diff_match_patch.prototype.patch_deepCopy=function(a){for(var b=[],c=0;cthis.Match_MaxBits){if(j=this.match_main(b,h.substring(0,this.Match_MaxBits),g),-1!=j&&(i=this.match_main(b,h.substring(h.length-this.Match_MaxBits),g+h.length-this.Match_MaxBits),-1==i||j>=i))j=-1}else j=this.match_main(b,h,g);if(-1==j)e[f]=!1,d-=a[f].length2-a[f].length1;else if(e[f]=!0,d=j-g,g=-1==i?b.substring(j,j+h.length):b.substring(j,i+this.Match_MaxBits),h==g)b=b.substring(0,j)+this.diff_text2(a[f].diffs)+b.substring(j+h.length);else if(g=this.diff_main(h,g,!1),h.length>this.Match_MaxBits&&this.diff_levenshtein(g)/h.length>this.Patch_DeleteThreshold)e[f]=!1;else{this.diff_cleanupSemanticLossless(g);for(var h=0,k,i=0;ie[0][1].length){var f=b-e[0][1].length;e[0][1]=c.substring(e[0][1].length)+e[0][1];d.start1-=f;d.start2-=f;d.length1+=f;d.length2+=f}d=a[a.length-1];e=d.diffs;0==e.length||0!=e[e.length-1][0]?(e.push([0,c]),d.length1+=b,d.length2+=b):b>e[e.length-1][1].length&&(f=b-e[e.length-1][1].length,e[e.length-1][1]+=c.substring(0,f),d.length1+=f,d.length2+=f);return c}; +diff_match_patch.prototype.patch_splitMax=function(a){for(var b=this.Match_MaxBits,c=0;c2*b?(h.length1+=i.length,e+=i.length,j=!1,h.diffs.push([g,i]),d.diffs.shift()):(i=i.substring(0,b-h.length1-this.Patch_Margin),h.length1+=i.length,e+=i.length,0===g?(h.length2+=i.length,f+=i.length):j=!1,h.diffs.push([g,i]),i==d.diffs[0][1]?d.diffs.shift():d.diffs[0][1]=d.diffs[0][1].substring(i.length))}g=this.diff_text2(h.diffs);g=g.substring(g.length-this.Patch_Margin);i=this.diff_text1(d.diffs).substring(0,this.Patch_Margin);""!==i&&(h.length1+=i.length,h.length2+=i.length,0!==h.diffs.length&&0===h.diffs[h.diffs.length-1][0]?h.diffs[h.diffs.length-1][1]+=i:h.diffs.push([0,i]));j||a.splice(++c,0,h)}}}; +diff_match_patch.prototype.patch_toText=function(a){for(var b=[],c=0;c= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF) { + // UTF-8 BOM. This isn't binary. + return false; + } + + for (var i = 0; i < total_bytes; i++) { + if (bytes[i] === 0) { // NULL byte--it's binary! + return true; + } + else if ((bytes[i] < 7 || bytes[i] > 14) && (bytes[i] < 32 || bytes[i] > 127)) { + // UTF-8 detection + if (bytes[i] > 191 && bytes[i] < 224 && i + 1 < total_bytes) { + i++; + if (bytes[i] < 192) { + continue; + } + } + else if (bytes[i] > 223 && bytes[i] < 239 && i + 2 < total_bytes) { + i++; + if (bytes[i] < 192 && bytes[i + 1] < 192) { + i++; + continue; + } + } + suspicious_bytes++; + // Read at least 32 bytes before making a decision + if (i > 32 && (suspicious_bytes * 100) / total_bytes > 10) { + return true; + } + } + } + + if ((suspicious_bytes * 100) / total_bytes > 10) { + return true; + } + + return false; + } +} + +function isVeryLargeFile(file, contents, callback) { + Fs.stat(file, function(err, stat) { + if (err) return callback(err); + + callback(null, stat.size > 1024 * 1024 || contents && contents.length > 1024 * 1024); + }); +} + +/* +// Quick testing: +basePath = __dirname; +dbFilePath = __dirname + "/test.db"; +// dbFilePath = "/home/ubuntu/newclient/corrupted_collab.db"; +initDB(false, function(err){ + if (err) + return console.error("initDB error:", err); + console.error("DB inited"); + Store.newDocument({ + path: "test.txt", + contents: Fs.readFileSync(__dirname + "/../template.js", "utf8") + }, function (err) { + if (err) + return console.error(err); + console.error("Test document created"); + Store.getDocument("test.txt", function (err, doc) { + console.error("ERR1", err); + // console.error(JSON.stringify(doc)); + Store.getWorkspaceState(function (err, ws) { + console.log("ERR2:", err); + }); + }); + }); +}); +lock("abc", function () { + console.log("first locking"); + setTimeout(function () { + unlock("abc"); + }, 100); +}); + +lock("abc", function () { + console.log("second locking"); + setTimeout(function () { + unlock("abc"); + }, 100); +}); +*/ From 329ac9812e5f679a6307c9c0c1b3ece2aea672b1 Mon Sep 17 00:00:00 2001 From: nightwing Date: Sun, 15 Mar 2015 18:41:41 +0000 Subject: [PATCH 36/38] do not add crossOrigin attribute on same origin scripts --- plugins/c9.login.client/bootstrap.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/c9.login.client/bootstrap.js b/plugins/c9.login.client/bootstrap.js index 1405cd89..6cf144b5 100644 --- a/plugins/c9.login.client/bootstrap.js +++ b/plugins/c9.login.client/bootstrap.js @@ -93,7 +93,8 @@ function loadScript(path, token, callback) { var and = path.indexOf("?") >= 0 ? "&" : "?"; s.src = path + (token ? and + "access_token=" + encodeURIComponent(token) : ""); - s.crossOrigin = true; + if (s.src.indexOf("://" + window.location.host) == -1) + s.crossOrigin = true; head.appendChild(s); s.onload = s.onreadystatechange = function(_, isAbort) { From 8a32d89a6fbb9c2584dad8df7613545b33a72669 Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Mon, 16 Mar 2015 12:09:14 +0100 Subject: [PATCH 37/38] Merge pull request +6611 from c9/hotfix-vfs Decrease timeout --- plugins/c9.vfs.client/endpoint.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/c9.vfs.client/endpoint.js b/plugins/c9.vfs.client/endpoint.js index 95e20d6e..345511de 100644 --- a/plugins/c9.vfs.client/endpoint.js +++ b/plugins/c9.vfs.client/endpoint.js @@ -166,7 +166,7 @@ define(function(require, exports, module) { var server = servers[i]; auth.request(server.url + "/" + options.pid, { method: "POST", - timeout: 120000, + timeout: 20000, body: { version: version }, From 435927d9932d0a3ba793c47a55139f319e0a1486 Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Mon, 16 Mar 2015 15:15:51 +0100 Subject: [PATCH 38/38] Merge pull request +6605 from c9/netlimit Add automation of bandwidth limiting script --- package.json | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 088f9328..d1268343 100644 --- a/package.json +++ b/package.json @@ -49,23 +49,24 @@ }, "licenses": [], "c9plugins": { - "c9.ide.language": "#854575579b", + "c9.ide.language": "#4a23a36945", "c9.ide.language.css": "#afda1f867c", "c9.ide.language.generic": "#87a4a44671", "c9.ide.language.html": "#fa4833e117", "c9.ide.language.html.diff": "#a7311cfc9f", - "c9.ide.language.javascript": "#26cf518b28", + "c9.ide.language.javascript": "#8479d0a9c1", "c9.ide.language.javascript.immediate": "#9a2cce9121", - "c9.ide.language.javascript.eslint": "#1baacc275b", - "c9.ide.language.javascript.tern": "#a65ad88dd9", + "c9.ide.language.javascript.eslint": "#8832423ad1", + "c9.ide.language.javascript.tern": "#7aab8b0b6a", "c9.ide.language.javascript.infer": "#ebb2daf81a", - "c9.ide.language.jsonalyzer": "#c5dfe5fb7e", - "c9.ide.collab": "#b94018ab2b", + "c9.ide.language.jsonalyzer": "#a1057f20db", + "c9.ide.collab": "#7b09419b5c", "c9.ide.local": "#2bfd7ff051", "c9.ide.find": "#989c06e6a7", "c9.ide.find.infiles": "#f98dfef554", "c9.ide.find.replace": "#e4daf722b8", "c9.ide.run.debug": "#379e508be6", + "c9.automate": "#86bf1ee1ca", "c9.ide.ace.emmet": "#e5f1a92ac3", "c9.ide.ace.gotoline": "#4d1a93172c", "c9.ide.ace.keymaps": "#6c4bb65b1f", @@ -78,7 +79,7 @@ "c9.ide.configuration": "#b8470f4107", "c9.ide.dialog.wizard": "#a588b64050", "c9.ide.fontawesome": "#781602c5d8", - "c9.ide.format": "#1ae38e60e6", + "c9.ide.format": "#f51451ac57", "c9.ide.help.support": "#60e88f5680", "c9.ide.imgeditor": "#08bbc53578", "c9.ide.immediate": "#e9ba147cc2", @@ -87,18 +88,19 @@ "c9.ide.navigate": "#64156c7f4a", "c9.ide.newresource": "#9a7464cc47", "c9.ide.openfiles": "#28a4f5af16", - "c9.ide.preview": "#3c4dded23f", - "c9.ide.preview.browser": "#be197b0464", - "c9.ide.preview.markdown": "#bf952685f6", + "c9.ide.preview": "#dba2f4214d", + "c9.ide.preview.browser": "#ac18aaf31d", + "c9.ide.preview.markdown": "#ab8d30ad9f", "c9.ide.pubsub": "#92ec19ed3a", "c9.ide.readonly": "#f6f07bbe42", "c9.ide.recentfiles": "#7c099abf40", - "c9.ide.remote": "#37773d905b", - "c9.ide.run": "#f5a056e6ce", + "c9.ide.remote": "#cd45e81d2f", + "c9.ide.run": "#71c5562e42", "c9.ide.run.build": "#915e48b363", + "c9.ide.run.debug.xdebug": "#b91d23f48b", "c9.ide.save": "#a32a8f4346", - "c9.ide.terminal.monitor": "#df9936daa2", - "c9.ide.theme.flat": "#5c7c27ab74", + "c9.ide.terminal.monitor": "#b0b4d03280", + "c9.ide.theme.flat": "#b1d65fa9bb", "c9.ide.threewaymerge": "#229382aa0b", "c9.ide.undo": "#b028bcb4d5", "c9.ide.upload": "#0bd010d3dc",