diff --git a/.scripted b/.scripted index 85c6b5bb..fc5a1249 100644 --- a/.scripted +++ b/.scripted @@ -96,6 +96,12 @@ // "unescape_strings": false // } // }, - + +// search: { +// exclude: [ +// '**/node_modules', +// '**/client/components' +// ] +// } } diff --git a/README.md b/README.md index 89402bf9..bea07e48 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +# This repository is no longer actively maintained by VMware, Inc. + # Welcome to Scripted [![Build Status](https://travis-ci.org/scripted-editor/scripted.png?branch=master)](https://travis-ci.org/scripted-editor/scripted) diff --git a/client/.bowerrc b/client/.bowerrc new file mode 100644 index 00000000..b2a342b9 --- /dev/null +++ b/client/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "components" +} diff --git a/client/component.json b/client/bower.json similarity index 91% rename from client/component.json rename to client/bower.json index 6becbf21..ffd8f867 100644 --- a/client/component.json +++ b/client/bower.json @@ -1,4 +1,5 @@ { + "name": "scripted", "dependencies": { "requirejs": "2.1.4", "requirejs-text": "2.0.4", diff --git a/client/css/main.css b/client/css/main.css index 96ea7d6c..72484266 100644 --- a/client/css/main.css +++ b/client/css/main.css @@ -1176,3 +1176,31 @@ a.keybinding_button { } +.annotationRange.renameRefactoring { + /* + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII="); + background-color: #ffffff; + */ + outline: 1px Cyan ridge; +} +.annotationOverview.renameRefactoring { + background-color: Blue; + border: 1px solid #7f7f7f; + /* + background-color: Lavender; + border: 1px solid black; + */ + +} +/* +.annotationHTML.renameRefactoring { + /* warning triangle */ + /* + background-image: url("data:image/gif;base64,R0lGODlhEAAQANUAAP7bc//egf/ij/7ijv/jl/7kl//mnv7lnv/uwf7CTP7DTf7DT/7IW//Na/7Na//NbP7QdP/dmbltAIJNAF03AMSAJMSCLKqASa2DS6uBSquCSrGHTq6ETbCHT7WKUrKIUcCVXL+UXMOYX8GWXsSZYMiib6+ETbOIUcOXX86uhd3Muf///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAACsALAAAAAAQABAAAAZowJVwSCwaj0ihikRSJYcoBEL0XKlGkcjImQQhJBREKFnyICoThKeE/AAW6AXgdPyUAgrLJBEo0YsbAQyDhAEdRRwDDw8OaA4NDQImRBgFEJdglxAEGEQZKQcHBqOkKRpFF6mqq1WtrUEAOw=="); + */ + /* flashlight */ + background-image: url("data:image/gif;base64,R0lGODlhEAAQANUAALClrLu1ubOpsKqdp6eapKufqMTAw7attLSrsrGnr62jq8C7v765vaebpb22vLmyuMbCxsnGycfEx8G+wcrIysTBxUltof//yf///v70jergpPvws+nWc/npqvrpqvrpq/raffffnvXVkfTVkvXUkd+9f+SiOemvV+uyXa2OX7mYZqeIXKuNX/ClO7KQYqiIXJ59Vp19VpFvTo9uTZBvTpNyUJNyUf///////wAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAADgALAAAAAAQABAAAAZ4QJxwSCwajS2aS1U6DlunzcagcuKgG4sn5HJiLZ2QiHbEbj6hEapVTKVYr3OItG5TIhVGLF0npigUEAsPAjV9Q24pEhMBCAoybEUmGRcrDgcAAzNGkxcYNzAJBQSbRJ0YqBc2DaVEHJ6pGTStRBqfGBcZILRWvThBADs="); +} +*/ + + diff --git a/client/scripts/layoutManager.js b/client/scripts/layoutManager.js index e093765e..e4c45c57 100644 --- a/client/scripts/layoutManager.js +++ b/client/scripts/layoutManager.js @@ -12,8 +12,11 @@ * Brian Cavalier ******************************************************************************/ define(['require', "jquery", "jquery_ui", "scripted/utils/navHistory", "scripted/utils/pageState", - "scripted/utils/editorUtils", "scripted/utils/storage", "scripted/exec/exec-on-load"], -function(require, jQuery, jqueryUi, mNavHistory, mPageState, editorUtils, storage, execOnLoad) { + "scripted/utils/editorUtils", "scripted/utils/storage", "scripted/exec/exec-on-load", + "scripted/utils/server-options" +], +function(require, jQuery, jqueryUi, mNavHistory, mPageState, editorUtils, storage, execOnLoad, +options) { // Initialize navigator state var navigatorVisible = false; @@ -142,7 +145,9 @@ function(require, jQuery, jqueryUi, mNavHistory, mPageState, editorUtils, storag }); // add live reloading support - require(['scripted/application-manager']); + if (options.applicationManager) { + require(['scripted/application-manager']); + } require(['scripted/editor/themeManager']); diff --git a/client/scripts/orion/textview/textView.js b/client/scripts/orion/textview/textView.js index d1bbfb18..317875f1 100644 --- a/client/scripts/orion/textview/textView.js +++ b/client/scripts/orion/textview/textView.js @@ -2781,15 +2781,6 @@ define("orion/textview/textView", ['orion/textview/textModel', 'orion/textview/k } }, _handleKeyPress: function (e) { - if (this._keyPressHandlers) { - for (var kph=0; kph 31) { this._doContent(String.fromCharCode (key)); diff --git a/client/scripts/plugins/esprima/esprimaJsContentAssist.js b/client/scripts/plugins/esprima/esprimaJsContentAssist.js index 1c15dfbe..ec837e2a 100644 --- a/client/scripts/plugins/esprima/esprimaJsContentAssist.js +++ b/client/scripts/plugins/esprima/esprimaJsContentAssist.js @@ -1501,7 +1501,8 @@ define(["plugins/esprima/esprimaVisitor", "plugins/esprima/types", "plugins/espr * @return Boolean */ function leftTypeIsMoreGeneral(leftTypeObj, rightTypeObj, env) { - var leftTypeName = leftTypeObj.name, rightTypeName = rightTypeObj.name; + var leftTypeName = leftTypeObj.name || mTypes.convertToSimpleTypeName(leftTypeObj), + rightTypeName = rightTypeObj.name || mTypes.convertToSimpleTypeName(rightTypeObj); if (!leftTypeName) { if (leftTypeObj.type === 'NullLiteral' || leftTypeObj.type === 'UndefinedLiteral' || leftTypeObj.type === 'VoidLiteral') { @@ -1794,7 +1795,7 @@ define(["plugins/esprima/esprimaVisitor", "plugins/esprima/types", "plugins/espr } var type = this._allTypes[this.scope(target)]; // do not allow augmenting built in types - if (!type.$$isBuiltin) { + if (type && !type.$$isBuiltin) { // if new type name is not more general than old type, do not replace if (typeContainsProperty(type, name) && leftTypeIsMoreGeneral(typeObj, type[name].typeObj, this)) { // do nuthin @@ -1901,7 +1902,7 @@ define(["plugins/esprima/esprimaVisitor", "plugins/esprima/types", "plugins/espr }; var innerLookup = function(name, type, allTypes) { - var res = type[name]; + var res = type && type[name]; var proto = type.$$proto; if (res) { @@ -1915,9 +1916,9 @@ define(["plugins/esprima/esprimaVisitor", "plugins/esprima/types", "plugins/espr var targetType = this._allTypes[this.scope(target)]; // uncomment this if we want to hide errors where there is an unknown type being placed on the scope stack -// if (!targetType) { -// targetType = this.globalScope() -// } + if (!targetType) { + targetType = this.globalScope(); + } var res = innerLookup(swapper(name), targetType, this._allTypes); return res; }, diff --git a/client/scripts/plugins/esprima/refactoringSupport.js b/client/scripts/plugins/esprima/refactoringSupport.js new file mode 100644 index 00000000..3597ea27 --- /dev/null +++ b/client/scripts/plugins/esprima/refactoringSupport.js @@ -0,0 +1,203 @@ +/******************************************************************************* + * @license + * Copyright (c) 2013 VMware, Inc. All Rights Reserved. + * THIS FILE IS PROVIDED UNDER THE TERMS OF THE ECLIPSE PUBLIC LICENSE + * ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS FILE + * CONSTITUTES RECIPIENTS ACCEPTANCE OF THE AGREEMENT. + * You can obtain a current copy of the Eclipse Public License from + * http://www.opensource.org/licenses/eclipse-1.0.php + * + * Contributors: + * Andrew Eisenberg (VMware) - initial API and implementation + ******************************************************************************/ + +/** + * This module provides tools to help wth refactoring + */ + +/*global define */ +define(function (require) { + + var visitor = require('plugins/esprima/esprimaVisitor'); + var findSelectedWord = require('scripted/markoccurrences').findSelectedWord; + + + function inRange(range, offset) { + return range[0] <= offset && range[1] >= offset; + } + + /** + * finds all references for the given variable under the + * selection + * @param {{start:Number,end:Number}} selection the currently selected element + * @param String buffer the entire contents of the editor + * @returns [{start:Number,end:Number}] an array of elemnnts corresponding to references + * or null if invalid location + */ + return { + findVarReferences : function (buffer, selection) { + var expanded = findSelectedWord(selection.start, selection.end, buffer); + if (!expanded) { + return null; + } + var i; + var root = visitor.parse(buffer); + var parentStack = []; + var refNodes = []; + var declScopes = []; + + // is this node a valid reference??? + function isRef(node) { + var parent = parentStack[parentStack.length-1]; + if (parent.type === 'Property') { + return node === parent.value; + } + if (parent.type === 'MemberExpression') { + return node === parent.object || (node === parent.property && parent.computed); + } + + switch(parent.type) { + case 'ArrayExpression': + case 'AssignmentExpression': + case 'BinaryExpression': + case 'BlockStatement': + case 'CallExpression': + case 'CatchClause': + case 'ConditionalExpression': + case 'DoWhileStatement': + case 'ExpressionStatement': + case 'ForInStatement': + case 'ForStatement': + case 'FunctionDeclaration': + case 'FunctionExpression': + case 'IfStatement': + case 'LogicalExpression': + case 'NewExpression': + case 'Program': + case 'ReturnStatement': + case 'SwitchCase': + case 'SwitchStatement': + case 'ThrowStatement': + case 'TryStatement': + case 'UnaryExpression': + case 'UpdateExpression': + case 'VariableDeclaration': + case 'VariableDeclarator': + case 'WhileStatement': + case 'WithStatement': + return true; + + case 'BreakStatement': + case 'Identifier': + case 'LabeledStatement': + case 'Literal': + return false; + default: // in case there's anythign I missed + console.warning("Unhandled expression type in refactoring: " + parent.type); + } + } + + visitor.visit(root, null, function(node, context) { + if (node.type === 'Identifier' && node.name === expanded.word) { + var parent = parentStack[parentStack.length-1]; + if (isRef(node)) { + + refNodes.push(node); + + if (parent.type === 'VariableDeclarator' || parent.type === 'FunctionDeclaration') { + // look for enclosing scope. travel up parent stack and find enclosing + // function decl, or just use all + + // if this is a function decl, then the name is put into the scope of the enclosing. + // So, look two into the stack + var startStack = (parent.type === 'FunctionDeclaration' && node === parent.id) + ? parentStack.length-2 : parentStack.length-1; + for (i = startStack; i >= 0 ; i--) { + if (parentStack[i].type === 'FunctionExpression' || + parentStack[i].type === 'FunctionDeclaration') { + + // if the node is a parameter, then add the function as the scope + // if the node is an identifier in the function body, then add the body. + // this allows for a distinction between function parameters and vars declared in them + var found; + if (parentStack[i].params && parentStack[i].params.length > 0) { + for (var j = 0; j < parentStack[i].params.length; j++) { + if (parentStack[i].params[j] === node) { + declScopes.push(parentStack[i].range); + found = true; + break; + } + } + } + + if (!found) { + declScopes.push(parentStack[i].body.range); + } + } else if (parentStack[i].type === 'Program') { + + declScopes.push(parentStack[i].range); + break; + } + } + } + } + } + parentStack.push(node); + return true; + }, function () { parentStack.pop(); }); + + // ensure that we have found the target node (ie- it is a renamable thing + var found; + for (i = 0; i < refNodes.length; i++) { + var node = refNodes[i]; + if (node.range[0] === expanded.start && node.range[1] === expanded.end) { + found = true; + break; + } + } + if (!found) { + return null; + } + + // also include global scope in case a var is not explicitly declarared + declScopes.push(root.range); + + // next, order declScopes from smallest to biggest + declScopes.sort(function(l, r) { + var lSize = l[1] - l[0]; + var rSize = r[1] - r[0]; + return lSize - rSize; + }); + + var targetScope; + for (i = 0; i < declScopes.length; i++) { + if (inRange(declScopes[i], selection.start)) { + targetScope = declScopes[i]; + break; + } + } + + var foundRefs = []; + refNodes.forEach(function(node) { + // ensure not in range of any smaller scope, but in range of the target scope + for (i = 0; i < declScopes.length; i++) { + if (declScopes[i] === targetScope) { + // must be in this scope + if (inRange(targetScope, node.range[0])) { + foundRefs.push({start: node.range[0], end:node.range[1]}); + } + // no need to try any larget scopes + break; + } else { + // must not be in this scope + if (inRange(declScopes[i], node.range[0])) { + break; + } + } + } + }); + + return foundRefs; + } + }; +}); diff --git a/client/scripts/scripted/editor/InlineRenameRefactoring.js b/client/scripts/scripted/editor/InlineRenameRefactoring.js new file mode 100644 index 00000000..f06fd676 --- /dev/null +++ b/client/scripts/scripted/editor/InlineRenameRefactoring.js @@ -0,0 +1,209 @@ +define(['orion/textview/keyBinding','scripted/markoccurrences','plugins/esprima/refactoringSupport','scripted/editor/annotationManager','orion/textview/annotations'], +function(keyBinding, markoccurrences, refactoringSupport, annotationManager, orionAnnotationModule) { + + var State = { INACTIVE: 1, ACTIVE: 2 }; + + annotationManager.registerAnnotationType('scripted.renameRefactoring',false); + + function InlineRenameRefactoring(editor,undoStack,linkedMode) { + this._undoStack = undoStack; + this._linkedMode = linkedMode; + this._editor = editor; + this._textView = editor.getTextView(); + this._inlineRenameRefactoringMode = new InlineRenameRefactoringMode(this,undoStack); + this._state = State.INACTIVE; + var self = this; + + this._editor.pushKeyMode(this._inlineRenameRefactoringMode); + // was: + // this._textView.setKeyBinding(new keyBinding.KeyBinding('r', false, true, false, true), "rename"); + this._textView.setKeyBinding(new keyBinding.KeyBinding('r', false, true, true, false), "rename"); // Shift+Alt+R + this._textView.setAction("rename", function() { + self.activate(); + return true; + }, {name: "rename"}); + } + + InlineRenameRefactoring.prototype = { + activate: function() { + var selection = this._editor.getSelection(); + if (selection.start===selection.end) { + console.log("rename only activates on selection"); + return; + } + if (this._state === State.INACTIVE) { + console.log("rename refactoring activated"); + + // Locate the place where the selected text exists in the file + var model = this._editor.getModel(); + var selectedText=model.getText(selection.start,selection.end); + console.log("Selection is >"+selectedText+"<"); + + + var lexicalRefactoring = false; + if (lexicalRefactoring) { + var matcher = new markoccurrences.SelectionMatcher(); + var results = matcher.findMatches(selection.start,selection.end,this._editor.getText()); + if (results.matches) { + console.log("word >"+results.word+"< matchcount=#"+results.matches.length); + for (var m=0;m"+key+"<"); + + var replacementLength = word.length; + if (typed.length>0) { + replacementLength = typed.length; + } + typed = typed+ch; + this._inlineRenameRefactoring.typed = typed; + var newAnnos = []; + var offset = 0; + for (var m=0;mClick any key binding value to configure it. Scroll down to see unbound actions."); cl.append('

  • '); @@ -119,10 +121,10 @@ function (mJsRender, mJquery, mKeybinder, mKeystroke, mKeyedit, editorUtil, comm editor.getTextView()._update(); } - + /*Command help panel*/ var help_close, help_open; - + var isOpen = false; help_open = function (){ @@ -140,13 +142,13 @@ function (mJsRender, mJquery, mKeybinder, mKeystroke, mKeyedit, editorUtil, comm $('#help_open').off('click'); $('#help_open').on('click', help_open); }; - + $('#help_open').on('click', help_open); $('#help_panel').on('refresh', function () { if (isOpen) { //Don't bother to render if the panel is not visible. renderKeyHelp(); } }); - + }); - + diff --git a/client/scripts/scripted/markoccurrences.js b/client/scripts/scripted/markoccurrences.js index 6ab6e5f6..bcdedc86 100644 --- a/client/scripts/scripted/markoccurrences.js +++ b/client/scripts/scripted/markoccurrences.js @@ -10,7 +10,7 @@ * Contributors: * Andrew Eisenberg- initial API and implementation ******************************************************************************/ - + /*global require define scripted*/ /*jslint browser:true */ @@ -28,7 +28,7 @@ define(['orion/textview/annotations'], function(mAnnotations) { (char >= '0' && char <= '9') || char === '_' || char === '$'; } - + function isWord(selstart, selend, buffer) { if (selstart < 0 || selend > buffer.length || selstart > selend || !isWordChar(buffer[selstart])) { return false; @@ -42,11 +42,11 @@ define(['orion/textview/annotations'], function(mAnnotations) { } return true; } - + function isDelineatedWord(selstart, selend, buffer) { return isWord(selstart, selend, buffer) && !isWordChar(buffer[selstart-1]) && !isWordChar(buffer[selend]); } - + /** * Find word from selection * A word is a contiguous block of alphanumerics or $ or _ @@ -59,7 +59,7 @@ define(['orion/textview/annotations'], function(mAnnotations) { if (!isWord(selstart, selend, buffer)) { return null; } - + // at this point, we know there is a word selected, must find the start and the end var start, end; var i = selstart; @@ -70,11 +70,11 @@ define(['orion/textview/annotations'], function(mAnnotations) { } i--; } - - if (!start && i === 0) { + + if (!start && i === -1) { start = 0; } - + i = selend; while (i <= buffer.length) { if (!isWordChar(buffer[i])) { @@ -83,18 +83,18 @@ define(['orion/textview/annotations'], function(mAnnotations) { } i++; } - + if (!end && i === buffer.length) { end = buffer.length; } - + return { word: buffer.substring(start, end), start: start, end: end }; } - + /** * @param {String} buffer * @param {String} toFind @@ -111,21 +111,22 @@ define(['orion/textview/annotations'], function(mAnnotations) { } return matches; } - - + + var currentRequest; - + function SelectionMatcher() { // config options this.interval = 500; // inteval between caret changes and mark occurrence changes this.disable = false; // set to true if mark occurrences should be disabled this.retain = false; // set to true if marks should be retained after caret moves away - + this.isActive = true; this.initOptions(); } SelectionMatcher.prototype = { install : function(editor) { this.editor = editor; + this.isActive = true; editor.getTextView().addEventListener("Selection", this); }, initOptions : function() { @@ -143,6 +144,23 @@ define(['orion/textview/annotations'], function(mAnnotations) { } }, + deactivate: function() { + this.isActive=false; + var annotationModel = this.editor.getAnnotationModel(); + annotationModel.removeAnnotations(ANNOTATION_TYPE); + }, + + activate: function() { + this.isActive=true; + var self = this; + currentRequest = setTimeout(function() { + var sel = self.editor.getSelection(); + if (sel) { + self.markOccurrences(sel.start,sel.end); + } + }, this.interval); + }, + // TODO not used uninstall : function() { if (this.editor) { @@ -163,7 +181,7 @@ define(['orion/textview/annotations'], function(mAnnotations) { self.markOccurrences(evt.newValue.start, evt.newValue.end); }, this.interval); }, - + findMatches : function(selstart, selend, buffer) { var toFind = findSelectedWord(selstart, selend, buffer); var matches; @@ -174,11 +192,14 @@ define(['orion/textview/annotations'], function(mAnnotations) { return { matches : null, word : null }; } }, - + markOccurrences : function(selstart, selend) { + if (!this.isActive) { + return; + } /** @type AnnotationModel*/ var annotationModel = this.editor.getAnnotationModel(); - + // find new matches var buffer = this.editor.getText(); var result = this.findMatches(selstart, selend, buffer); @@ -202,7 +223,7 @@ define(['orion/textview/annotations'], function(mAnnotations) { } } }; - + // configure: persist when no selection. disable completely, settimeout - return { SelectionMatcher : SelectionMatcher }; + return { SelectionMatcher : SelectionMatcher, findSelectedWord : findSelectedWord}; }); \ No newline at end of file diff --git a/client/scripts/scripted/processConfiguration.js b/client/scripts/scripted/processConfiguration.js index 66f723df..e79f2116 100644 --- a/client/scripts/scripted/processConfiguration.js +++ b/client/scripts/scripted/processConfiguration.js @@ -26,10 +26,10 @@ define(function() { // editor.expandtab (boolean) // editor.tabsize (number) // rule: if possible (compatible), copy one config to the other - var editor_expandtab_set = dotScripted.editor && dotScripted.editor.expandtab !== null; - var editor_tabsize_set = dotScripted.editor && dotScripted.editor.tabsize !== null; - var formatter_js_indent_size_set = dotScripted.formatter && dotScripted.formatter.js && dotScripted.formatter.js.indent_size !== null; - var formatter_js_indent_char_set = dotScripted.formatter && dotScripted.formatter.js && dotScripted.formatter.js.indent_char !== null; + var editor_expandtab_set = dotScripted.editor && (typeof dotScripted.editor.expandtab !== 'undefined'); + var editor_tabsize_set = dotScripted.editor && (typeof dotScripted.editor.tabsize !== 'undefined'); + var formatter_js_indent_size_set = dotScripted.formatter && dotScripted.formatter.js && (typeof dotScripted.formatter.js.indent_size !== 'undefined'); + var formatter_js_indent_char_set = dotScripted.formatter && dotScripted.formatter.js && (typeof dotScripted.formatter.js.indent_char !== 'undefined'); // Just do the common cases for now: if (editor_expandtab_set || editor_tabsize_set) { @@ -80,16 +80,16 @@ define(function() { } else { dotScripted.editor.expandtab = true; } - if (formatter_js_indent_size_set) { - // Set the tabsize to match the indent size - var indentsize = dotScripted.formatter.js.indent_size; - if (!dotScripted.editor) { - dotScripted.editor = { - "tabsize": indentsize - }; - } else { - dotScripted.editor.tabsize = indentsize; - } + } + if (formatter_js_indent_size_set) { + // Set the tabsize to match the indent size + var indentsize = dotScripted.formatter.js.indent_size; + if (!dotScripted.editor) { + dotScripted.editor = { + "tabsize": indentsize + }; + } else { + dotScripted.editor.tabsize = indentsize; } } } diff --git a/client/scripts/scripted/utils/server-options.js b/client/scripts/scripted/utils/server-options.js new file mode 100644 index 00000000..34377486 --- /dev/null +++ b/client/scripts/scripted/utils/server-options.js @@ -0,0 +1,19 @@ +/******************************************************************************* + * @license + * Copyright (c) 2013 VMware, Inc. All Rights Reserved. + * THIS FILE IS PROVIDED UNDER THE TERMS OF THE ECLIPSE PUBLIC LICENSE + * ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS FILE + * CONSTITUTES RECIPIENTS ACCEPTANCE OF THE AGREEMENT. + * You can obtain a current copy of the Eclipse Public License from + * http://www.opensource.org/licenses/eclipse-1.0.php + * + * Contributors: + * Kris De Volder + ******************************************************************************/ +define(function (require) { + //This module provides access to a bunch of options defined on the server-side. + // The options are configured in the .js file that starts the server. e.g. + // start-cloudfoundry.js + + return JSON.parse(require('text!/options')); +}); \ No newline at end of file diff --git a/client/scripts/servlets/stub-maker.js b/client/scripts/servlets/stub-maker.js index 9de5cffc..902f3565 100644 --- a/client/scripts/servlets/stub-maker.js +++ b/client/scripts/servlets/stub-maker.js @@ -146,8 +146,8 @@ define(['when'], function(when) { if(xhrobj.readyState === 4) { // 4 means content has finished loading if (xhrobj.status===200) { callback.apply(null, JSON.parse(xhrobj.responseText)); - } else if (xhrobj.status===500) { - callback("Error: xhr request status = "+xhrobj.responseText); + } else { + callback("Error: status ["+xhrobj.status+"] "+xhrobj.responseText); } } }; @@ -169,8 +169,8 @@ define(['when'], function(when) { if(xhrobj.readyState === 4) { // 4 means content has finished loading if (xhrobj.status===200) { callback.apply(null, JSON.parse(xhrobj.responseText)); - } else if (xhrobj.status===500) { - callback("Error: xhr request status = "+xhrobj.responseText); + } else { + callback("Error: status ["+xhrobj.status+"] "+xhrobj.responseText); } } }; diff --git a/commands/start.js b/commands/start.js index e955d818..ea19c074 100644 --- a/commands/start.js +++ b/commands/start.js @@ -56,10 +56,14 @@ function start(options) { var file = options._; var suppressOpen = options.suppressOpen?'true':'false'; // console.log("path is "+path.resolve(path.dirname(module.filename),'../commands/scripted.js')); - child = spawn('node', [ path.resolve(path.dirname(module.filename),'../commands/scripted.js'), suppressOpen, file ],{ - detached:true, + child = spawn('node', [ path.resolve(path.dirname(module.filename),'../commands/scripted.js'), file, suppressOpen ],{ + detached:true, stdio: ['ignore', out, err] }); + + var logfile = tmp + '/scripted.log'; + console.log('Log file: ' + logfile); + tailf = spawn('tail', [ '-100f', logfile ],{ stdio: 'inherit' }); child.unref(); } diff --git a/manifest.yml b/manifest.yml index a5ab2ed8..09bdc092 100644 --- a/manifest.yml +++ b/manifest.yml @@ -1,9 +1,14 @@ --- applications: -- instances: 1 - memory: 1G - name: scr +- services: + scr-mongodb: + vendor: mongodb + version: "2.0" + tier: free runtime: node08 - path: . + instances: 1 url: scrptd.${target-base} + memory: 1G framework: node + name: scr + path: . diff --git a/package.json b/package.json index 3efbfba1..bb0772d5 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "scripted": "./bin/scripted" }, "scripts": { - "start" : "node server/start-cloudfoundry.js", + "start" : "node server/cloudfoundry/start-cloudfoundry.js", "test": "node tests/server/run-all.js", "postinstall": "cd client && bower install" }, @@ -41,20 +41,21 @@ "node-static": "0.5.9", "optimist": "0.3.5", "rest": "0.8.4", - "serv": "https://github.com/aclement/serv/archive/master.tar.gz", + "serv": "https://github.com/kdvolder/serv/archive/master.tar.gz", "sockjs": "0.3.1", "websocket-multiplex": "https://github.com/kdvolder/websocket-multiplex/archive/master.tar.gz", - "when": "https://github.com/cujojs/when/archive/noisy-deferred-then.tar.gz", + "when": "2.0.1", "rest": "https://github.com/s2js/rest/archive/dev.tar.gz", "express": "3.0.6", - "bower": "0.8.5", - "mime": "1.2.9", - "priorityqueuejs": "0.1.0" - }, - "devDependencies": { - "fake-fs": "https://github.com/eldargab/node-fake-fs/archive/50f54e9551d9ece5578213564268021d4976db26.tar.gz", - "nodeunit": "0.7.4" - }, - "license": "EPL", - "readmeFilename": "README.md" + "bower": "1.8.0", + "mime": "1.2.9", + "priorityqueuejs": "0.1.0", + "mongodb": "1.2.13" + }, + "devDependencies": { + "fake-fs": "https://github.com/eldargab/node-fake-fs/archive/50f54e9551d9ece5578213564268021d4976db26.tar.gz", + "nodeunit": "0.7.4" + }, + "license": "EPL-1.0", + "readmeFilename": "README.md" } diff --git a/play-area/css/main.css b/play-area/css/main.css new file mode 100644 index 00000000..1953c56b --- /dev/null +++ b/play-area/css/main.css @@ -0,0 +1,3 @@ +h1 { + color: blue +} \ No newline at end of file diff --git a/play-area/index.html b/play-area/index.html index 85b534cd..fc1f1140 100644 --- a/play-area/index.html +++ b/play-area/index.html @@ -4,10 +4,10 @@ Iearch experiment - + +

    Hello

    - -fff \ No newline at end of file + \ No newline at end of file diff --git a/play-area/kill-scripted.js b/play-area/kill-scripted.js new file mode 100644 index 00000000..32ce2e8a --- /dev/null +++ b/play-area/kill-scripted.js @@ -0,0 +1,16 @@ + +//var killUrl = 'http://localhost:8123/status'; +var killUrl = 'http://scrptd.cloudfoundry.com/status'; + +var rest = require('rest'); + +console.log('Making kill request'); +rest({ + path: killUrl, + method: 'DELETE' +}).then(function (resp) { + console.log('status = ' + resp.status.code); + console.log('entity = ' + resp.entity); +}).otherwise(function (err) { + console.error(err); +}); diff --git a/sandbox/user.home/.scriptedrc/scripted.json b/sandbox/home/.scriptedrc/scripted.json similarity index 100% rename from sandbox/user.home/.scriptedrc/scripted.json rename to sandbox/home/.scriptedrc/scripted.json diff --git a/sandbox/sample/.scripted b/sandbox/home/sample/.scripted similarity index 100% rename from sandbox/sample/.scripted rename to sandbox/home/sample/.scripted diff --git a/sandbox/sample/foo.js b/sandbox/home/sample/foo.js similarity index 100% rename from sandbox/sample/foo.js rename to sandbox/home/sample/foo.js diff --git a/server/cloudfoundry/cloudfoundry-routes.js b/server/cloudfoundry/cloudfoundry-routes.js new file mode 100644 index 00000000..2dbfde4a --- /dev/null +++ b/server/cloudfoundry/cloudfoundry-routes.js @@ -0,0 +1,21 @@ +/******************************************************************************* + * @license + * Copyright (c) 2013 VMware, Inc. All Rights Reserved. + * THIS FILE IS PROVIDED UNDER THE TERMS OF THE ECLIPSE PUBLIC LICENSE + * ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS FILE + * CONSTITUTES RECIPIENTS ACCEPTANCE OF THE AGREEMENT. + * You can obtain a current copy of the Eclipse Public License from + * http://www.opensource.org/licenses/eclipse-1.0.php + * + * Contributors: + * Kris De Volder + ******************************************************************************/ + +exports.install = function (app, filesystem) { + + app.get('/', function (req, res) { + console.log('Redirecting to sample area'); + res.redirect('/editor/home/sample'); + }); + +}; \ No newline at end of file diff --git a/server/cloudfoundry/start-cloudfoundry.js b/server/cloudfoundry/start-cloudfoundry.js new file mode 100644 index 00000000..e09566ec --- /dev/null +++ b/server/cloudfoundry/start-cloudfoundry.js @@ -0,0 +1,171 @@ +/******************************************************************************* + * @license + * Copyright (c) 2013 VMware, Inc. All Rights Reserved. + * THIS FILE IS PROVIDED UNDER THE TERMS OF THE ECLIPSE PUBLIC LICENSE + * ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS FILE + * CONSTITUTES RECIPIENTS ACCEPTANCE OF THE AGREEMENT. + * You can obtain a current copy of the Eclipse Public License from + * http://www.opensource.org/licenses/eclipse-1.0.php + * + * Contributors: + * Kris De Volder + ******************************************************************************/ + +// +// A start script to run scripted server on cloudfoundry +// + +// TODO: Before this can go 'public' +// +// - Customized readme shown when opening on a folder (done), but contents +// is still a bit iffy. + +// - A reasonable piece of sample code to pre-populate first-time visitor space. +// - Ask Scott to vacate domain name 'scripted.cloudfoundry.com' so we can use that. +// +// +//- +// (must do) links to download pages for scripted, maybe to +//scripted-editor.github.com/scripted with that page getting a little +//overhaul. + + +// optional + +// - github-fs +// - login mechanis to obtain oauth token for individual user (optional for 'demo') +// - prefetch sensitive to rate limit remaining +// - upload zip? + +// DONE +//- disable shutdown hook +//- disable play / stop button +//- (must do) decide on exec keys, do we need to shut it off? We can't expose a server to running arbitrary commands. +//- (must do) decide how to handle these things: +//(a) how do we stop +//people putting up stuff they shouldn't? Either copyrighted or offensive +//material. Do we have to care about that? Feels like we might. Do we need +// some kind of disclaimer - like the jsfiddle one. +//(b) how do we check +// the space isn't filled up? Handle rogue users filling it up? Can we +//easily see all the material that is up there? +//- (must do) accessible usage stats? We may need the numbers as +//ammunition going forward, we need to know how many users try this out. +// At least number of visitors who try it out - if this is captured in the +// server log, can we access that file? I don't think we want to track IP +// addresses of visitors (do we?) - but just a count of users creating +//projects would be useful. +// - keybindings: now that filesystem is all read-only, they can't be saved. +// 'fix' disable the keyeditor in CF version + + +var path = require('path'); + +var mappedFs = require('../plugable-fs/mapped-fs'); +var scriptedFs = require('../plugable-fs/scripted-fs'); +var githubFs = require('../plugable-fs/github-fs/github-fs'); +var compose = require('../plugable-fs/composite-fs').compose; +var readOnly = require('../plugable-fs/read-only-fs'); +//var unlistable = require('../plugable-fs/unlistable-fs'); + +var withBaseDir = mappedFs.withBaseDir; +var withPrefix = mappedFs.withPrefix; + +var scriptedHomeLocation = path.resolve(__dirname, '../..'); +console.log('scripted.home = '+scriptedHomeLocation); + +var sandbox = readOnly(mappedFs.withBaseDir(path.resolve(scriptedHomeLocation, 'sandbox'))); + +var cache = require('../plugable-fs/github-fs/rest-node-manager').configure({ + limit: 2500 // Limits number of in-memory cached nodes. +}); +var github = withPrefix('/github', githubFs.configure({ + token: require('../plugable-fs/github-fs/secret').token, + cache: cache +})); + +var scriptedHome = withPrefix('/scripted.home', readOnly(compose( + //Needed to load built-in plugins + withPrefix('/plugins', withBaseDir(scriptedHomeLocation + '/plugins')), + //Needed to load built-in completions: + withPrefix('/completions', withBaseDir(scriptedHomeLocation + '/completions')), + //Needed to provide content assist for plugin APIs: + withPrefix('/client', withBaseDir(scriptedHomeLocation + '/client')) +))); + +//var scriptedHome = withPrefix('/scripted.home', +// withBaseDir(scriptedHomeLocation) +//); + +//All of our files, with the 'slim' node-like fs API: +var corefs = compose( + github, + sandbox, + scriptedHome +); + +//Now wrap that to create our 'fat' API that scripted uses throughout its codebase. +var filesystem = scriptedFs.configure(corefs, { + userHome: '/home', + scriptedHome: '/scripted.home' +}); + +var server=require('../scriptedServer.js').start(filesystem, { + port: 8123, + cloudfoundry: true, //Enables some customization for the cf deployed scripted 'showroom' app. + applicationManager: false, //Disable the application manager. + shutdownHook: false, //Disable the 'shutdown hook' used by 'scr -k' and 'scr -r' commands. + exec: false, //Disable 'exec' related features. + keyedit: false, //Disable help side-panel's keybdingins editor as it doesn't work well with + // a shared fs, and won't work at all with a read-only fs. + help_text: [ +" _ _ _ _ ", +" | || || | | | _ ", +" | || || |_____| | ____ ___ ____ _____ _| |_ ___ ", +" | || || | ___ | |/ ___) _ \\| \\| ___ | (_ _) _ \\ ", +" | || || | ____| ( (__| |_| | | | | ____| | || |_| |", +" \\_____/|_____)\\_)____)___/|_|_|_|_____) \\__)___/ ", +"", +" ______ _ _ ", +" / _____) (_) _ | |", +" ( (____ ____ ____ _ ____ _| |_ _____ __| |", +" \\____ \\ / ___)/ ___) | _ (_ _) ___ |/ _ |", +" _____) | (___| | | | |_| || |_| ____( (_| |", +" (______/ \\____)_| |_| __/ \\__)_____)\\____|", +" |_| ", +"", +" This is a DEMO of the Scripted Editor running on 'cloudfoundry.com'.", +" Here you can quickly try out Scripted without any hassles such as", +" installing, signing-up, etc.", +"", +" Unfortunately, due to practical and legal limitations we", +" could only make this 'hassle free' publically hosted DEMO with a", +" read-only file system.", +"", +" We hope this demo will help you decide if you want to give Scripted", +" a 'real' try and install it for a more thorough try-out.", +"", +" To find out more visit our GitHub homepage:", +"", +" 'http://github.com/scripted-editor/scripted", +"", +" Some basic instructions for getting started with Scripted:", +"", +" Use the navigator on the left to select a file for editing.", +"", +" Help on all supported key bindings is available by clicking the", +" '?' icon in the top right, or simply pressing 'F1'", +"", +" To search your project for a file to open by name, press 'Cmd/Ctrl+Shift+F'", +" to show the 'Open File' dialog.", +"", +" To search for a file based simply on a string within it, press ", +" 'Cmd/Ctrl+Shift+L' to open the 'Look in files' dialog.", +"", +" The 'bars' icon next to the help icon opens the side panel which can", +" host a second editor, pressing 'Shift' when opening any link or navigable", +" JavaScript reference in Scripted will open the target in the side panel.", +" The side panel can also be opened/closed with 'Cmd/Ctrl+Shift+E'." + ] +}); + diff --git a/server/cloudfoundry/user-tracker.js b/server/cloudfoundry/user-tracker.js new file mode 100644 index 00000000..baa0ac97 --- /dev/null +++ b/server/cloudfoundry/user-tracker.js @@ -0,0 +1,180 @@ +/******************************************************************************* + * @license + * Copyright (c) 2013 VMware, Inc. All Rights Reserved. + * THIS FILE IS PROVIDED UNDER THE TERMS OF THE ECLIPSE PUBLIC LICENSE + * ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS FILE + * CONSTITUTES RECIPIENTS ACCEPTANCE OF THE AGREEMENT. + * You can obtain a current copy of the Eclipse Public License from + * http://www.opensource.org/licenses/eclipse-1.0.php + * + * Contributors: + * Kris De Volder + ******************************************************************************/ + +// +// On cloudfoundry this module +// 1) replaces the handling of the '/' url. +// 2) adds some 'user tracking' middleware +// + +var pathJoin = require('../jsdepend/utils').pathJoin; +var when = require('when'); +var mongo = require('../mongo'); //TODO: inject rather than import database instance +var glob = require('../utils/path-glob'); + +var users = mongo.collection('users'); + +/** + * Creates an error function in the context of a given http response object. + * The error fun may attache error info to the response. + */ +function errorFun(res) { + function error(err) { + //Always log errors to console. + console.error(err); + if (err.stack) { + console.error(err.stack); + } + + //If res object is available also show error to the client. + if (res) { + res.status(500); + res.header('Content-Type', 'text/plain'); + res.write(""+err); + if (err.stack) { + res.write(""+err.stack); + } + res.end(); + } + } + return error; +} + +//TODO: this cooky stuff should really be done as a connect +// middleware on every access not just on access to the 'root' path +// of the webserver. + +exports.install = function (app, filesystem) { + + var getUserHome = filesystem.getUserHome; + var isDirectory = filesystem.isDirectory; + var mkdir = filesystem.mkdir; + var copyDir = filesystem.copyDir; + var exists = filesystem.exists; + + function getUserPath(userID) { + return pathJoin(getUserHome(), userID); + } + +// Discontinued for now. We will only show a read-only sample now. +// +// function createUserArea(userID) { +// var path = getUserPath(userID); +// return isDirectory(path).then(function (isDir) { +// if (isDir) { +// //already got a user-area +// return; +// } +// //must create user area +// return copyDir('/sample', path); +// }); +// } + + function randomString(len) { + var chars = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + var str = ""; + for (var i = 0; i < len; i++) { + var code = Math.floor(Math.random()*chars.length); + str = str + chars[code]; + } + return str; + } + + function generateUserID(tries) { + tries = tries || 0; + var id = randomString(10); + return createUser(id).then(function () { + console.log('User created: '+id); + return id; //Yeah! Found one that's still available. + }, function (err) { + console.log('Faile to create User: '+id); + console.err(err); + if (++tries < 10) { + return generateUserID(tries); + } else { + //Can't find a free 'private' user id. Use the 'shared' ID. + return 'shared'; + } + }); + } + + function recordVisitor(id) { + console.log('Recording visit: '+id); + //Ignoring any errors about creating duplicate ids + return users.then(function (users) { + return mongo.findAndModify(users, + //Criteria: + {_id: id}, + //Sort: + [['_id','asc']], + //Update: + { + '$inc' : { visits: 1 }, + '$set' : { lastVisit: new Date()} + }, + //Options: + { upsert: true, 'new': true } + ).then(function (user) { + console.log('Visit recorded: '+JSON.stringify(user[0])); + return user; + }); + }); + } + + function createUser(id) { + return users.then(function (users) { + //The insert call below will reject when the user id already exists. + return mongo.insert(users, { + _id: id, + created: new Date() + }).then(function () {return id;}); + }); + } + + /** + * When one of these paths gets hit we create user tracking cookies. + */ + var trackedPaths = glob.fromJson([ + '/', '/editor/**' + ]); + + function trackUsers(req, res, next) { + var shouldTrack = trackedPaths.test(req.path); +// console.log('trackUsers ['+shouldTrack+'] '+req.path); + if (!shouldTrack) { + return next(); + } else { + var userID = req.cookies.userID; + console.log('cookie userID = '+userID); + if (!userID) { + userID = generateUserID(); + } + when(userID, recordVisitor).otherwise(errorFun(null)); + when(userID, function (userID) { + res.cookie('userID', userID, { + maxAge: 365 * 24 * 3600 * 1000 /*expires in one year*/ + }); + next(); + }).otherwise(errorFun(res)); + } + } + + app.use(trackUsers); + + // Change the redirect target for the '/' path to point to the + // sample project. + app.get('/', function (req, res) { + res.redirect('/editor/home/sample'); + }); + +}; \ No newline at end of file diff --git a/server/jsdepend/amd-reference-finder.js b/server/jsdepend/amd-reference-finder.js index 59703a87..8160cf82 100644 --- a/server/jsdepend/amd-reference-finder.js +++ b/server/jsdepend/amd-reference-finder.js @@ -10,7 +10,7 @@ * Contributors: * Kris De Volder - initial API and implementation ******************************************************************************/ - + /*global require define console module*/ if (typeof define !== 'function') { var define = require('amdefine')(module); @@ -83,17 +83,22 @@ var requirePat = objectPat({ var stringVar = variablePat('string'); var starVar = variablePat(); +var stringLitPat = objectPat({ + type: 'Literal', + value: stringVar +}); + var arrayExp = objectPat({ "type": "ArrayExpression", "elements": starVar }); -// Given a parse-tree, find references to other modules in that module. +// Given a parse-tree, find references to other modules in that module. function findReferences(tree, callback) { var foundList = []; var foundSet = {}; - + function addFound(name) { if (typeof(name)==='string') { if (!foundSet.hasOwnProperty(name)) { @@ -102,23 +107,23 @@ function findReferences(tree, callback) { } } } - + function addArrayElements(args) { for (var i = 0; i < args.length; i++) { var arg = args[i]; - if (arg.type === 'Literal' && typeof(arg.value)==='string') { + if (matches(stringLitPat, arg)) { addFound(arg.value); } } } - + walk(tree, function (node) { //dumpTree(node); if (matches(definePat, node)) { addArrayElements(defineVar.value); } else if (matches(requirePat, node)) { var arg = requireParam.value; - if (arg.type === 'Literal' && typeof(arg.value)==='string') { + if (matches(stringLitPat, arg)) { addFound(arg.value); } else if (matches(arrayExp, arg)) { addArrayElements(starVar.value); diff --git a/server/mongo.js b/server/mongo.js new file mode 100644 index 00000000..e3fb4650 --- /dev/null +++ b/server/mongo.js @@ -0,0 +1,125 @@ +/******************************************************************************* + * @license + * Copyright (c) 2013 VMware, Inc. All Rights Reserved. + * THIS FILE IS PROVIDED UNDER THE TERMS OF THE ECLIPSE PUBLIC LICENSE + * ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS FILE + * CONSTITUTES RECIPIENTS ACCEPTANCE OF THE AGREEMENT. + * You can obtain a current copy of the Eclipse Public License from + * http://www.opensource.org/licenses/eclipse-1.0.php + * + * Contributors: + * Kris De Volder + ******************************************************************************/ + +// +// Provides some convenience api to access a mongodb database. +// +// The database is automatically configure to work on a local +// testing mongodb server or on a database provided by +// cloudfoundry as a service. + +var mongodb = require('mongodb'); +var when = require('when'); +var whenFunction = require('when/node/function'); +var methodCaller = require('./utils/promises').methodCaller; +//var createCallback = whenFunction.createCallback; + +if(process.env.VCAP_SERVICES){ + var env = JSON.parse(process.env.VCAP_SERVICES); + console.log('VCAP_SERVICES = '+JSON.stringify(env, null, ' ')); + var mongo = env['mongodb-2.0'][0].credentials; +} else{ + var mongo = { + "hostname":"localhost", + "port":27017, + "username":"", + "password":"", + "name":"", + "db":"db" + }; +} + +var generate_mongo_url = function(obj){ + obj.hostname = (obj.hostname || 'localhost'); + obj.port = (obj.port || 27017); + obj.db = (obj.db || 'test'); + + if(obj.username && obj.password){ + return "mongodb://" + obj.username + ":" + obj.password + "@" + obj.hostname + ":" + obj.port + "/" + obj.db; + } + else{ + return "mongodb://" + obj.hostname + ":" + obj.port + "/" + obj.db; + } +}; + +var mongourl = generate_mongo_url(mongo); + +console.log(mongourl); + +var _connect = methodCaller('connect'); +var connection = null; +function connect() { + if (!connection) { + connection = _connect(mongodb, mongourl); + } + return connection; +} + +//function apply(f, self/*optional*/, args) { +// if (!args) { +// //Actually, self are the args in that case! +// return _apply(f, self); +// } else { +// return when(self, function (self) { +// return _apply(f.bind(self), args); +// }); +// } +//} + +var _insert = methodCaller('insert'); + +/** + * Insert an object into a mongodb collection. + * + * @return {Promise} that resolves when object was inserted or rejects if there was + * an error. + */ +exports.insert = function (collection, data, options) { + return _insert(collection, data, options || { safe: true }); +}; + + +/** + * Call 'find' on a collection. This just passes through arguments to + * mongo as is, but returns a promise instead of accepting a callback. + */ +exports.find = methodCaller('find'); + +/** + * Call 'toArray' on a Cursor... . + */ +exports.toArray = methodCaller('toArray'); + +/** + * Get a mongodb collection + */ +exports.collection = function (name) { + //TODO: use methodCaller to implement this in much less code. + var d = when.defer(); + connect().then(function (conn) { + //console.log('fetch collection connection = '+conn); + conn.collection(name, function (err, coll) { +// console.log('fetch collection err = '+err); +// console.log('fetch collection coll = '+coll); +// console.log('Connection = '+conn); + if (err) { + return d.reject(err); + } else { + return d.resolve(coll); + } + }); + }); + return d.promise; +}; + +exports.findAndModify = methodCaller('findAndModify'); \ No newline at end of file diff --git a/server/plugable-fs/github-fs/README-secret.txt b/server/plugable-fs/github-fs/README-secret.txt new file mode 100644 index 00000000..1e3010c5 --- /dev/null +++ b/server/plugable-fs/github-fs/README-secret.txt @@ -0,0 +1,12 @@ +The github-fs needs a file called +'secret.json' which contains a secret token for accessing github rest api. +The file should look like this (excluding the ----) +---------------------- +{ + "token" : "???????" +} +--------------------- + +In place of the "????????????" a secret token must be placed that was created via: + +https://github.com/settings/tokens/new diff --git a/server/plugable-fs/github-fs/github-repo-fs.V2 b/server/plugable-fs/github-fs/github-repo-fs.V2 deleted file mode 100644 index b3f11774..00000000 --- a/server/plugable-fs/github-fs/github-repo-fs.V2 +++ /dev/null @@ -1,317 +0,0 @@ -/******************************************************************************* - * @license - * Copyright (c) 2013 VMware, Inc. All Rights Reserved. - * THIS FILE IS PROVIDED UNDER THE TERMS OF THE ECLIPSE PUBLIC LICENSE - * ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS FILE - * CONSTITUTES RECIPIENTS ACCEPTANCE OF THE AGREEMENT. - * You can obtain a current copy of the Eclipse Public License from - * http://www.opensource.org/licenses/eclipse-1.0.php - * - * Contributors: - * Kris De Volder - ******************************************************************************/ - -var nodeCallback = require('../../utils/promises').nodeCallback; -var fsErrors = require('../fs-errors'); -var readonlyFs = require('../read-only-fs'); - -var pathJoin = require('../../jsdepend/utils').pathJoin; -var when = require('when'); - -var LOGGING = false; - -// -// Quick implementation of a 'github readonly fs'. -// -// Issues: -// - rest client doesn't follow 'redirects' according to github api docs, it should. -// - doesn't handle paginated results. So for large files or dirs with many subdirs -// results may be incomplete. -// - builds in-memory tree but doesn't update when tree structure changes -// in the repo. -// - in memory only grows. How to reclaim memory when not used for a long time? - -function configure(options) { - - //console.log('repoFs options = '+ JSON.stringify(options, null, ' ')); - - if (!options.token) { - throw new Error('github-repo-fs needs to be configured with a "token" for OAuth'); - } - if (!options.owner) { - throw new Error('github-repo-fs needs to be configured with a "owner"'); - } - if (!options.repo) { - throw new Error('github-repo-fs needs to be configured with a "repo"'); - } - - //The URL that should be used to fetch the 'root node' data. - var API_ROOT = 'https://api.github.com/repos/'+options.owner+'/'+options.repo+'/contents'; - - var store = {}; - var cache = { - get: function (url) { - var entry = store[url]; - if (entry) { - return entry; - } - }, - put: function (url, entry) { -// console.log('put '+url + " : "+entry); - store[url] = entry; - } - }; - - function getNode(url) { -// console.log('>> getNode '+url); - var cached = cache.get(url); -// console.log('>> getNode from cache = '+cached); - if (!cached) { - cached = new Node(url); -// console.log('>> getNode created = '+cached); - cache.put(url, cached); - } -// console.log('>> getNode returning : '+cached); - return cached; - } - - var rest = require('./github-rest-client').configure({ - token: options.token - }); - - var INTERESTING = [ - //All nodes: - 'size', 'name', 'type', 'sha', - //File nodes: - 'content', 'encoding' - ]; - function copyInterestingData(source, dest) { - INTERESTING.forEach(function (name) { - if (source.hasOwnProperty(name)) { - dest[name] = source[name]; - } else { - delete dest[name]; - } - }); - } - - function Node(url) { - this.url = url; - } - /** - * Inserts data just fetched from rest api into a node - */ - Node.prototype.setData = function (data) { - if (Array.isArray(data)) { //Directory node - //Directories are kind-a funny. When we fetch their contents we - //actually get summary info about their children as an array. - //So this is the time to create the child nodes with that data - //already in it. - var children = this.children = {}; //Will point to the urls of our children. - data.forEach(function (childData) { - if (childData.name && childData.url) { - var url = childData.url; - children[childData.name] = childData.url; - var child = getNode(url); - child.setData(childData); - } else { - console.log('Ignoring unexpected child data:'+JSON.stringify(childData, null, ' ')); - } - }); - if (!this.isDirectory()) { - //Provide minima 'dirEntry' data that makes us look like a dir - // because it seems we are a dir now, regardless of what we were before. - this.dirEntry = { - type: 'dir' - }; - } - } else { - this.dirEntry = {}; - if (!this.isDirectory()) { - //In that case shouldn't have any children! - delete this.children; - } - copyInterestingData(data, this.dirEntry); - } - }; - /** - * Fetch this node's contents from its url and store it in the node - */ - Node.prototype.fetch = function () { - //TODO: don't fetch if not needed. Right now we always fetch! - return rest({ path: this.url }).then(function (data) { - //console.log('data received for '+this.url); - //console.dir(data); - this.setData(data); - }.bind(this)); - }; - Node.prototype.getChildren = function () { - return this.fetch().then(function () { - if (!this.isDirectory()) { - return when.reject(fsErrors.isNotDirError('getChildren', this.url)); - } - if (!this.children) { - return when.reject('Internal Error: type is dir but no children'); - } - return when.resolve(this.children); - }.bind(this)); - }; - Node.prototype.getChild = function (name) { - var that = this; - return this.getChildren().then(function (children) { - var childUrl = children[name]; - return (childUrl && getNode(children[name])) || when.reject( - fsErrors.noExistError('getChild', pathJoin(that.url, name)) - ); - }); - }; - Node.prototype.navigate = function (segments) { - if (typeof(segments)==='string') { - segments = segments.split('/'); - } - if (segments.length===0) { - return this; - } else { - var segment = segments.shift(); - if (!segment) { - return this.navigate(segments); - } else { - return this.getChild(segment).then(function (child) { -// console.log('Got child: '); -// console.dir(child); - return child.navigate(segments); - }); - } - } - }; - Node.prototype.isDirectory = function () { - //Only the root node is created without a dirEntry read from the parent node. - //The root node is assumed to always be a directory. - return !this.dirEntry || this.dirEntry.type === 'dir'; - }; - Node.prototype.isFile = function () { - return this.dirEntry && this.dirEntry.type === 'file'; - }; - Node.prototype.stat = function () { - return this; - }; - Node.prototype.toString = function () { - return this.url + ' type: '+ (this.dirEntry && this.dirEntry.type); - }; - Node.prototype.readdir = function () { - return this.getChildren().then(function (children) { - return Object.keys(children); - }); - }; - Node.prototype.readFile = function (encoding) { - if (!this.isFile()) { - //Avoid expensive fetch if this looks like its not even a file - return when.reject(fsErrors.isDirError('readFile', this.url)); - } - return this.fetch().then(function () { - if (!this.isFile()) { - return when.reject(fsErrors.isDirError('readFile', this.url)); - } - var apiData = this.dirEntry; - var contents = apiData.content; - if (encoding===apiData.encoding) { - return contents; - } - //Encodings mismatch... must convert - - var buf = new Buffer(contents, apiData.encoding); - if (!encoding) { - return buf; // return the data in raw form if no encoding is specified - } else { - return buf.toString(encoding); - } - }.bind(this)); - }; - - var rootNode = new Node(API_ROOT); - - function readdir(path, callback) { - nodeCallback( - when(rootNode.navigate(path), function (node) { - console.log('node :'); - console.dir(rootNode); - return node.readdir(); - }), - callback - ); - } - - function stat(path, callback) { - nodeCallback( - when(rootNode.navigate(path), function (node) { - return node.stat(); - }), - callback - ); - } - - function notImplemented(name) { - function fun() { - var callback = arguments[arguments.length-1]; - if (typeof(callback)==='function') { - callback(new Error('Not implemented yet: github-fs function '+name)); - } - } - fun.name = name; - return fun; - } - - function logged(f) { - if (!LOGGING) { - return f; - } - var loggedF = function (handle, callback) { - console.log('>>> '+f.name + ' ' + handle); - return f(handle, function (err, result) { - if (err) { - console.log('<<< '+f.name + ' ' + handle + ' => ERROR'); - console.log(err); - } else { - console.log('<<< '+f.name + ' ' + handle + ' => ' +result.toString()); - } - return callback(err, result); - }); - }; - loggedF.name = f.name; - return loggedF; - } - - function readFile(path, encoding, callback) { - //encoding is optional! - if (!callback) { - callback = encoding; - encoding = null; - } - nodeCallback( - when(rootNode.navigate(path), function (node) { - return node.readFile(encoding); - }), - callback - ); - } - - function fetch(path) { - return rest({ - path: pathJoin(API_ROOT, path) - }); - } - - return readonlyFs({ - forTesting: { - rootNode: rootNode, - fetch: fetch, - rest: rest - }, - stat: logged(stat), - readFile: readFile, - readdir: logged(readdir) - }); - -} - -exports.configure = configure; \ No newline at end of file diff --git a/server/plugable-fs/github-fs/github-rest-client.js b/server/plugable-fs/github-fs/github-rest-client.js index a0b201bc..aa7cc859 100644 --- a/server/plugable-fs/github-fs/github-rest-client.js +++ b/server/plugable-fs/github-fs/github-rest-client.js @@ -35,6 +35,7 @@ function configure(options) { request: function (request, config) { var headers = request.headers || (request.headers = {}); headers.Authorization = "token " + token; + headers["User-Agent"] = "Scripted"; return request; } }); diff --git a/server/plugable-fs/unlistable-fs.js b/server/plugable-fs/unlistable-fs.js new file mode 100644 index 00000000..e086f93f --- /dev/null +++ b/server/plugable-fs/unlistable-fs.js @@ -0,0 +1,50 @@ +/******************************************************************************* + * @license + * Copyright (c) 2013 VMware, Inc. All Rights Reserved. + * THIS FILE IS PROVIDED UNDER THE TERMS OF THE ECLIPSE PUBLIC LICENSE + * ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS FILE + * CONSTITUTES RECIPIENTS ACCEPTANCE OF THE AGREEMENT. + * You can obtain a current copy of the Eclipse Public License from + * http://www.opensource.org/licenses/eclipse-1.0.php + * + * Contributors: + * Kris De Volder + ******************************************************************************/ + +// +// A wrapper that makes a specific path on a filesystem 'unlistable'. +// This means that when trying to readdir that path the result will +// be just an empty list. +// +// The idea is to use this to make the subdirectories 'semi-private'. +// It will be hard to guess/discover the names of the subdirectories, but if +// you know the name of *your* directory can still access or share it +// with others. +// + +var fsErrors = require('./fs-errors'); +var extend = require('../jsdepend/utils').extend; + +function create(fs, path) { + + path = path || '/'; + + /** + * Replacement for 'readdir' so that readdir('/') => [] and other paths + * are just delegated to retain their orginal behavior. + */ + function readdir(handle, k) { + if (handle===path) { + return k(null, []); + } else { + return fs.readdir(handle, k); + } + } + + return extend(fs, { + readdir: readdir + }); + +} + +module.exports = create; \ No newline at end of file diff --git a/server/routes/cloudfoundry-routes.js b/server/routes/cloudfoundry-routes.js deleted file mode 100644 index 3357525f..00000000 --- a/server/routes/cloudfoundry-routes.js +++ /dev/null @@ -1,97 +0,0 @@ -/******************************************************************************* - * @license - * Copyright (c) 2013 VMware, Inc. All Rights Reserved. - * THIS FILE IS PROVIDED UNDER THE TERMS OF THE ECLIPSE PUBLIC LICENSE - * ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS FILE - * CONSTITUTES RECIPIENTS ACCEPTANCE OF THE AGREEMENT. - * You can obtain a current copy of the Eclipse Public License from - * http://www.opensource.org/licenses/eclipse-1.0.php - * - * Contributors: - * Kris De Volder - ******************************************************************************/ - -// -// On cloudfoundry this module replaces the handling of the '/' url. -// -// It provides some logic to use a cooky to assign a unique identifier -// to a user. -// -// Instead of redirecting to user.home we then setup a sample project -// area for this user and redirect them to that area. - -var pathJoin = require('../jsdepend/utils').pathJoin; -var when = require('when'); - -exports.install = function (app, filesystem) { - - var welcomeText = 'Welcome to Scripted!'; - - var getUserHome = filesystem.getUserHome; - var isDirectory = filesystem.isDirectory; - var mkdir = filesystem.mkdir; - var copyDir = filesystem.copyDir; - var exists = filesystem.exists; - - function getUserPath(userID) { - return pathJoin(getUserHome(), userID); - } - - function createUserArea(userID) { - var path = getUserPath(userID); - return isDirectory(path).then(function (isDir) { - if (isDir) { - //already got a user-area - return; - } - //must create user area - return copyDir('/sample', path); - }); - } - - function randomString(len) { - var chars = "abcdefghijklmnopqrstuvwxyz01234567890"; - var str = ""; - for (var i = 0; i < len; i++) { - var code = Math.floor(Math.random()*chars.length); - str = str + chars[code]; - } - return str; - } - - function generateUserID(tries) { - tries = tries || 0; - var id = randomString(8); - return exists(getUserPath(id)).then(function (exists) { - if (exists) { - //already exists... try to find another one - if (++tries < 10) { - return generateUserID(tries); - } else { - //Can't find a free 'private' user id. Use the 'shared' ID. - return 'shared'; - } - } - return id; //Yeah! Found one that's still available. - }); - } - - app.get('/', function (req, res) { - console.log("Should be redirecting to user-specific area"); - - var userID = req.cookies.userID; - console.log('cookie userID = '+userID); - if (!userID) { - userID = generateUserID(); - } - when(userID, function (userID) { - res.cookie('userID', userID, { - maxAge: 365 * 24 * 3600 * 1000 /*expires in one year*/ - }); - createUserArea(userID).then(function () { - res.redirect('/editor'+getUserHome()+'/'+userID); - }); - }); - }); - -}; \ No newline at end of file diff --git a/server/routes/debug-routes.js b/server/routes/debug-routes.js index 92a009e6..7586d8c2 100644 --- a/server/routes/debug-routes.js +++ b/server/routes/debug-routes.js @@ -17,6 +17,22 @@ //var memwatch = require('memwatch'); var memwatch = null; +var mongodb = require('../mongo'); + +function errorFun(res) { + function error(err) { + console.error(err); + res.status(500); + res.header('Content-Type', 'text/plain'); + res.write(""+err); + if (err.stack) { + res.write(""+err.stack); + } + res.end(); + } + return error; +} + exports.install = function install(app) { var lastGc = null; @@ -43,4 +59,38 @@ exports.install = function install(app) { res.write(JSON.stringify(process.memoryUsage(), null, ' ')); res.end(); }); + + /** + * Retrieve a mongodb collection as json text + */ + app.get('/debug/mongodb/:collection', function (req, res) { + var name = req.params.collection; + console.log('mongo fetch collection : '+name ); + mongodb.collection(name).then(function (coll) { + return mongodb.find(coll).then(function (entries) { + console.log('find coll entries = '+entries); + res.header('Content-Type', 'application/json'); + return mongodb.toArray(entries).then(function (array) { + res.write(JSON.stringify(array, null, ' ')); + res.end(); + }); + }); + }).otherwise(errorFun(res)); + }); + + app.get('/debug/test-mongodb', function (req, res) { + console.log('test mongo request'); + var entry = { + date: new Date(), + ip: req.connection.remoteAddress + }; + mongodb.collection('testlog').then(function (collection) { + return mongodb.insert(collection, entry).then(function () { + //Woohoo! no errors so far! + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.write(JSON.stringify(entry)); + res.end('\n'); + }); + }).otherwise(errorFun(res)); + }); }; diff --git a/server/routes/debug.js b/server/routes/debug.js new file mode 100644 index 00000000..44848964 --- /dev/null +++ b/server/routes/debug.js @@ -0,0 +1,45 @@ +/******************************************************************************* + * @license + * Copyright (c) 2013 VMware, Inc. All Rights Reserved. + * THIS FILE IS PROVIDED UNDER THE TERMS OF THE ECLIPSE PUBLIC LICENSE + * ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS FILE + * CONSTITUTES RECIPIENTS ACCEPTANCE OF THE AGREEMENT. + * You can obtain a current copy of the Eclipse Public License from + * http://www.opensource.org/licenses/eclipse-1.0.php + * + * Contributors: + * Kris De Volder + ******************************************************************************/ + +var when = require('when'); +var mongo = require('../mongo'); +var insert = mongo.insert; + +mongo.collection('foo').then(function(foo) { + insert(foo, { date: new Date() }).then( + function () { + console.log('Inserted ok'); + }, + function (err) { + console.log('Problem inserting'); + console.error(err); + } + ).always(process.exit); +}); + +//// when(undefined).then(function () { +//// console.log('About to insert into '+foo); +//// return insert(foo, {_id: 1}).then(function () { +//// console.log('Inserted first entry'); +//// }); +//// }).then(function () { +//// return insert(foo, {_id: 1}).then(function () { +//// console.log('Inserted second entry'); +//// }); +//// }); +//}).otherwise(function (err) { +// console.error(err); +//}).always(function () { +// console.log('Done'); +// process.exit(); +//}); \ No newline at end of file diff --git a/server/servlets/status.js b/server/routes/status-routes.js similarity index 64% rename from server/servlets/status.js rename to server/routes/status-routes.js index 71e56947..50edef0f 100644 --- a/server/servlets/status.js +++ b/server/routes/status-routes.js @@ -12,7 +12,7 @@ * Andy Clement - overhaul ******************************************************************************/ -exports.install = function (app) { +exports.install = function (app, options) { app.get("/status",function(request, response) { response.writeHead(200, { @@ -22,14 +22,21 @@ exports.install = function (app) { response.write("\n"); response.end(); }); - + app.del("/status",function(request, response) { - response.writeHead(200, {"Content-Type": "text/plain"}); - response.write("Server will stop shortly"); - response.write("\n"); - response.end(); - console.log("Scripted is exiting..."); - process.exit(); - // this might be better: app.close(); + if (options.shutdownHook) { + response.writeHead(200, {"Content-Type": "text/plain"}); + response.write("Server will stop shortly"); + response.write("\n"); + response.end(); + console.log("Scripted is exiting..."); + process.exit(); + // this might be better: app.close(); + } else { + response.status(403); + response.header('Content-Type', 'text/plain'); + response.write('Scripted server shutdown hook is disabled'); + response.end(); + } }); }; \ No newline at end of file diff --git a/server/scriptedServer.js b/server/scriptedServer.js index 2b73213e..2ad950f1 100644 --- a/server/scriptedServer.js +++ b/server/scriptedServer.js @@ -15,12 +15,30 @@ ******************************************************************************/ /*jslint node:true */ +var jsonMerge = require('./jsdepend/json-merge'); + // Entry point for the node app // Basic construction. Some requestHandlers are used for // specific actions. Other URLs are assumed to be static content. function start(filesystem, options) { +//Default options for the 'localhost', commandline version of scripted. +// Note: any options that need to be false by default don't need to be +// added here since not setting them will already make them 'falsy'. +var defaultOptions = { + applicationManager: true, + shutdownHook: true, + exec: true, + keyedit: true +}; +options = jsonMerge(defaultOptions, options); +if (options.help_text) { + if (Array.isArray(options.help_text)) { + options.help_text = options.help_text.join('\n'); + } +} + var isCloudfoundry = (options && options.cloudfoundry); var server = require("./server").configure(filesystem, options); @@ -32,9 +50,8 @@ var requestHandlers = require("./requestHandlers").configure(filesystem); //require("./servlets/hello"); //require("./servlets/listFiles"); //Dead? require("./servlets/jsdepend-servlet").install(filesystem); -if (!isCloudfoundry) { - require("./servlets/exec-servlet"); -} +require("./servlets/exec-servlet").install(options); + //require("./servlets/config-servlet"); //these two wired up in server. //require("./servlets/kill"); diff --git a/server/server.js b/server/server.js index 6d23d21b..8e854b5d 100644 --- a/server/server.js +++ b/server/server.js @@ -50,6 +50,13 @@ function configure(filesystem, options) { app.configure(function() { app.use(express.json()); app.use(express.cookieParser()); + + if (options.cloudfoundry) { + //Add cf specific middleware + console.log('Add cf middleware'); + require('./cloudfoundry/user-tracker').install(app, filesystem); + } + app.use(app.router); app.use(onRequest); // bridge to 'servlets', we should remove over time app.use(express['static'](pathResolve(__dirname, '../client'), { maxAge: 6e5 })); @@ -59,12 +66,33 @@ function configure(filesystem, options) { })); }); - require('./servlets/status').install(app); + //Make the options available to client code. + app.get('/options', function (req, res) { + res.status(200); + res.header('Content-Type', 'application/json'); + res.write(JSON.stringify(options)); + res.end(); + }); + + //Serve alternate help.txt + if (options.help_text) { + app.get('/scripts/scripted/help.txt', function (req, res) { + console.log('Request for help.txt intercepted'); + res.status(200); + res.header('Content-Type', 'text/plain'); + res.write(options.help_text); + res.end(); + }); + } + if (options.cloudfoundry) { + console.log('Add cf routes'); - if (isCloudfoundry) { - require('./routes/cloudfoundry-routes.js').install(app, filesystem); + //Add cf specific 'routes' + require('./cloudfoundry/cloudfoundry-routes').install(app, filesystem); } + require('./routes/status-routes').install(app, options); + require('./routes/editor-routes').install(app, filesystem); require('./routes/test-routes').install(app); require('./routes/config-routes').install(app, filesystem); @@ -74,7 +102,9 @@ function configure(filesystem, options) { require('./servlets/incremental-search-servlet').install(app, filesystem); require('./servlets/incremental-file-search-servlet').install(app, filesystem); - require('./servlets/application-servlet').install(app); + if (options.applicationManager) { + require('./servlets/application-servlet').install(app); + } console.log('Server port = ' + port); app.server.listen(port, "127.0.0.1" /* only accepting connections from localhost */); diff --git a/server/servlets/application-servlet.js b/server/servlets/application-servlet.js index 80d3535f..79b6956d 100644 --- a/server/servlets/application-servlet.js +++ b/server/servlets/application-servlet.js @@ -38,11 +38,11 @@ exports.install = function (app) { */ app.get("/application/status", function(request, response) { // Return status of the server. - + // There is no path through 'serv' right now so we communicate // direct with the server it starts (see URL) var mimeclient = mime(); - + mimeclient({ path:url+"/serv/status", method:"GET" }).then(function(status_response) { response.send({"status": "running", "path": status_response.entity.path},200); },function(error) { @@ -52,6 +52,7 @@ exports.install = function (app) { /** Start the server */ app.put("/application/status", function(request, response) { + console.log('Starting app'); servStart.exec({cwd:request.param("path"),suppressOpen:request.param("suppressOpen"),logdir:request.param("path")}); response.end(); }); @@ -61,7 +62,7 @@ exports.install = function (app) { servStop.exec({}); response.end(); }); - + /** Reload a file */ app.post("/application/reload", function(request, response) { console.log("Reloading "+request.param("path")); diff --git a/server/servlets/exec-servlet.js b/server/servlets/exec-servlet.js index 3868d628..cefd1751 100644 --- a/server/servlets/exec-servlet.js +++ b/server/servlets/exec-servlet.js @@ -18,24 +18,39 @@ var makeRequestHandler = require('./servlet-utils').makeRequestHandler; var cpExec = require('child_process').exec; -function exec(cmd, callback, errback) { - - //cmd looks like this: - // {cmd: "ls -la", ...other options to pass to the nodejs exec function...} - - var options = cmd; - cmd = cmd.cmd; - - // TODO use mixin, if scipted had one... - // options.env = mixin({}, options.env, process.env); - options.env = options.env || {}; - for (var env in process.env) { - if (!(env in options.env)) { - options.env[env] = process.env[env]; - } +function install(options) { + + var exec; + + if (options.exec) { + exec = function (cmd, callback) { + + //cmd looks like this: + // {cmd: "ls -la", ...other options to pass to the nodejs exec function...} + + var options = cmd; + cmd = cmd.cmd; + + // TODO use mixin, if scipted had one... + // options.env = mixin({}, options.env, process.env); + options.env = options.env || {}; + for (var env in process.env) { + if (!(env in options.env)) { + options.env[env] = process.env[env]; + } + } + + /*var process = */cpExec(cmd, options, callback); + }; + } else { + exec = function (cmd, callback) { + callback('ERROR: exec is disabled'); + }; } - /*var process = */cpExec(cmd, options, callback); + exec.remoteType = ['JSON', 'callback']; + servlets.register('/exec', makeRequestHandler(exec)); + } -exec.remoteType = ['JSON', 'callback']; -servlets.register('/exec', makeRequestHandler(exec)); \ No newline at end of file + +exports.install = install; \ No newline at end of file diff --git a/server/start-cloudfoundry.js b/server/start-cloudfoundry.js deleted file mode 100644 index 7f63d104..00000000 --- a/server/start-cloudfoundry.js +++ /dev/null @@ -1,118 +0,0 @@ -/******************************************************************************* - * @license - * Copyright (c) 2013 VMware, Inc. All Rights Reserved. - * THIS FILE IS PROVIDED UNDER THE TERMS OF THE ECLIPSE PUBLIC LICENSE - * ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS FILE - * CONSTITUTES RECIPIENTS ACCEPTANCE OF THE AGREEMENT. - * You can obtain a current copy of the Eclipse Public License from - * http://www.opensource.org/licenses/eclipse-1.0.php - * - * Contributors: - * Kris De Volder - ******************************************************************************/ - -// -// A start script to run scripted server on cloudfoundry -// - -// TODO: Before this can go 'public' -// -// - disable 'exec' related features. Don't really want people to run exec commands -// on cf hosts. -// - remove or fix the 'Play' button. -// - Customized readme shown when opening on a folder. -// - A reasonable piece of sample code to pre-populate first-time visitor space. -// - Ask Scott to vacate domain name 'scripted.cloudfoundry.com' so we can use that. -// - Disable shutdown hook -// - auto restart after crash -// - prefetch sensitive to rate limit remaining - -//- (must do) accessible usage stats? We may need the numbers as -//ammunition going forward, we need to know how many users try this out. -// At least number of visitors who try it out - if this is captured in the -// server log, can we access that file? I don't think we want to track IP -// addresses of visitors (do we?) - but just a count of users creating -//projects would be useful. -// -//- (must do) decide how to handle these things: -//(a) how do we stop -//people putting up stuff they shouldn't? Either copyrighted or offensive -//material. Do we have to care about that? Feels like we might. Do we need -// some kind of disclaimer - like the jsfiddle one. -//(b) how do we check -// the space isn't filled up? Handle rogue users filling it up? Can we -//easily see all the material that is up there? -// -//- (must do) improve the landing page getting started text or even offer alternative text when deployed in this way. -// -//- (must do) decide on exec keys, do we need to shut it off? We can't expose a server to running arbitrary commands. -// -//- -// (must do) links to download pages for scripted, maybe to -//scripted-editor.github.com/scripted with that page getting a little -//overhaul. - - -// optional - -// - github-fs -// - needs some cache management cleanup so it can avoid running out of memory -// - cache contents of files as well -// - login mechanis to obtain oauth token for individual user (optional for 'demo') -// - upload zip? - -var path = require('path'); - -var nodefs = require('fs'); -var mappedFs = require('../server/plugable-fs/mapped-fs'); -var scriptedFs = require('../server/plugable-fs/scripted-fs'); -var githubFs = require('../server/plugable-fs/github-fs/github-fs'); -var compose = require('../server/plugable-fs/composite-fs').compose; -var readOnly = require('../server/plugable-fs/read-only-fs'); - -var withBaseDir = mappedFs.withBaseDir; -var withPrefix = mappedFs.withPrefix; - -var scriptedHomeLocation = path.resolve(__dirname, '..'); - -var sandbox = mappedFs.withBaseDir(path.resolve(__dirname, '../sandbox')); - -var cache = require('./plugable-fs/github-fs/rest-node-manager').configure({ - limit: 2500 // Limits number of in-memory cached nodes. -}); -var github = withPrefix('/github', githubFs.configure({ - token: require('./plugable-fs/github-fs/secret').token, - cache: cache -})); - -var scriptedHome = withPrefix('/scripted.home', readOnly(compose( - //Needed to load built-in plugins - withPrefix('/plugins', withBaseDir(scriptedHomeLocation + '/plugins')), - //Needed to load built-in completions: - withPrefix('/completions', withBaseDir(scriptedHomeLocation + '/completions')), - //Needed to provide content assist for plugin APIs: - withPrefix('/client', withBaseDir(scriptedHomeLocation + '/client')) -))); - -//var scriptedHome = withPrefix('/scripted.home', -// withBaseDir(scriptedHomeLocation) -//); - -//All of our files, with the 'slim' node-like fs API: -var corefs = compose( - github, - sandbox, - scriptedHome -); - -//Now wrap that to create our 'fat' API that scripted uses throughout its codebase. -var filesystem = scriptedFs.configure(corefs, { - userHome: '/user.home', - scriptedHome: '/scripted.home' -}); - -var server=require('../server/scriptedServer.js').start(filesystem, { - port: 8123, - cloudfoundry: true //Enables some customization for the cf deployed scripted 'showroom' app. -}); - diff --git a/server/utils/path-glob.js b/server/utils/path-glob.js index 2dd799dc..e2527126 100644 --- a/server/utils/path-glob.js +++ b/server/utils/path-glob.js @@ -18,7 +18,7 @@ // against ant-like path patterns using '**' and '*'. // -// IMPORTANT: +// IMPORTANT: // It is assumed that patterns are always being matched against absolute // path strings. Any 'relative' patterns will be interpreted as matching // from the file system root. @@ -26,7 +26,9 @@ // TODO: It could make sense to make the 'root dir' to which relative paths // should be made relative a configuration option or parameter. -var pathResolve = require('../jsdepend/utils').pathResolve; +var utils = require('../jsdepend/utils'); +var pathResolve = utils.pathResolve; +var endsWith = utils.endsWith; function configure(isWindows) { @@ -42,12 +44,12 @@ function configure(isWindows) { } return false; } - + /** * Regexp snippet to match a '/' depending on the platform. */ var SLASH = isWindows ? '[\\\\/]' : '/'; - + /** * Regexp snippet to match a 'non slash' depending on the platform. */ @@ -63,7 +65,7 @@ function configure(isWindows) { function pat2regexp(pat) { //TODO: something like this also exists in client-side code to // support 'afterSave' exec action triggering. - + //TODO: is all this string concatenation slow? var re = "^"; if (isWindows) { @@ -82,22 +84,26 @@ function configure(isWindows) { re = re + '[a-zA-Z]:'; } } - + //At this point there should be no more differences between win or nowin since //we already dealt with the device portion of the pattern. - + //Ensure patterns always start with a '/' if (pat[0]!=='/') { pat = '/' + pat; } - + var i = 0; while (i -1) { + res.push({ start:next, end: next+toFind.length }); + next = next+toFind.length; + } + + if (exceptions) { + var newRes = []; + for (var i = exceptions.length-1; i >= 0; i--) { + res.splice(exceptions[i], 1); + } + } + return res; + } + + function findIndexOf(contents, toFind, selectionCount) { + var next = 0, iter = 0; + while ((next = contents.indexOf(toFind, next)) > -1) { + if (iter === selectionCount) { + return { start: next, end: next + toFind.length }; + } + next = next + toFind.length; + iter++; + } + return null; + } + + function doTest(contents, toFind, selectionCount, exceptions) { + var selection = findIndexOf(contents, toFind, selectionCount); + var realResults = refactoringSupport.findVarReferences(contents, selection); + var expectedResults = findOccurrences(contents, toFind, exceptions); + assert.deepEqual(realResults, expectedResults); + } + + var tests = {}; + + tests['test simple'] = function() { + doTest("xxx", "xxx", 0); + doTest("var xxx", "xxx", 0); + doTest("var xxx;\nxxx", "xxx", 0); + doTest("var xxx;\nxxx", "xxx", 1); + doTest("var xxx;\nxxx\nxxxx", "xxx", 1, [2]); + doTest("var xxx;\nxxx\nxxxx.xxx", "xxx", 1, [2,3]); + }; + + tests['test statement kinds'] = function() { + doTest("var xxx; if (xxx) { xxx; } else { xxx; }", "xxx", 1); + doTest("var xxx; try { xxx; } catch(e) { xxx } finally { xxx }", "xxx", 1); + doTest("var xxx; xxx: xxx", "xxx", 0, [1]); + doTest("var xxx; switch (xxx) { case xxx: xxx; }", "xxx", 0); + doTest("var xxx;\nfunction foo() { return xxx; }", "xxx", 0); + doTest("var xxx;\nvar foo = function() { xxx; }", "xxx", 0); + doTest("for (var xxx; xxx; xxx) { xxx; }", "xxx", 0); + doTest("for (var xxx in xxx) { xxx; }", "xxx", 0); + doTest("var xxx; while (xxx) { xxx; }", "xxx", 0); + doTest("var xxx; throw xxx;", "xxx", 0); + doTest("var xxx; do { xxx; } while (xxx); ", "xxx", 0); + }; + tests['test expression kinds'] = function() { + doTest("var xxx; [xxx,xxx];", "xxx", 1); + doTest("var xxx; xxx[xxx];", "xxx", 1); + doTest("var xxx; ['xxx','xxx'];", "xxx", 0, [1,2]); + doTest("var xxx; xxx = xxx;", "xxx", 1); + doTest("var xxx; xxx.xxx;", "xxx", 1, [2]); + doTest("var xxx; xxx(xxx,xxx);", "xxx", 1); + doTest("var xxx; xxx++; xxx--; xxx+= xxx;", "xxx", 1); + doTest("var yyy, xxx; xxx++; xxx--; xxx+= xxx;", "xxx", 1); + }; + + tests['test function arguments'] = function() { + doTest('function f(xxx) { xxx; }', "xxx", 1); + doTest('function f(yyy,xxx) { xxx; }', "xxx", 1); + doTest('function f(yyy,xxx) { xxx; function f2(xxx) { xxx; } xxx; } xxx;', "xxx", 1, [2,3,5]); + doTest('var xxx; function f(yyy,xxx) { xxx; } xxx;', "xxx", 0, [1,2]); + doTest('function f(yyy,xxx) { xxx; } var xxx;', "xxx", 2, [0,1]); + doTest('var xxx; function f(xxx) { var xxx; xxx; } xxx;', "xxx", 2, [0,1,4]); + }; + + return tests; +}); \ No newline at end of file diff --git a/tests/client/scriptedClientServerTests.html b/tests/client/scriptedClientServerTests.html index 141448da..364d6af0 100644 --- a/tests/client/scriptedClientServerTests.html +++ b/tests/client/scriptedClientServerTests.html @@ -4,8 +4,8 @@ Client tests that require a server - - + + + +