├── .gitignore ├── .gitmodules ├── .slugignore ├── Gruntfile.js ├── LICENSE ├── README.md ├── fabfile.py ├── frontend ├── .jshintrc ├── SpecRunner.html ├── footer.css ├── header.css ├── index.html ├── mongoWebShell.css ├── spec │ ├── disableConsole.js │ ├── globals.js │ └── mws │ │ ├── Coll.spec.js │ │ ├── Cursor.spec.js │ │ ├── DB.spec.js │ │ ├── Readline.spec.js │ │ ├── Shell.spec.js │ │ ├── config.spec.js │ │ ├── events.spec.js │ │ ├── init.spec.js │ │ ├── keyword.spec.js │ │ ├── mutateSource.spec.js │ │ ├── request.spec.js │ │ └── util.spec.js └── src │ ├── head.js │ ├── lib │ └── sinon │ │ └── sinon.js │ ├── mws │ ├── Coll.js │ ├── CollectionNameError.js │ ├── Cursor.js │ ├── Readline.js │ ├── Shell.js │ ├── config.js │ ├── db.js │ ├── events.js │ ├── init.js │ ├── jsonUtils.js │ ├── keyword.js │ ├── mutateSource.js │ ├── request.js │ └── util.js │ └── tail.js ├── license.sh ├── licenses ├── LICENSE.css ├── LICENSE.html ├── LICENSE.js └── LICENSE.py ├── package.json ├── pre-commit.sh ├── requirements.txt ├── run_tests.py ├── sample.yml ├── tests ├── __init__.py ├── test_crontab.py ├── test_mws_views.py └── test_util.py └── webapps ├── __init__.py ├── configs ├── __init__.py ├── ivs.py └── server.py ├── ivs ├── __init__.py ├── app.py ├── initializers │ ├── __init__.py │ └── util.py ├── test │ ├── test_initializers_views.py │ ├── test_util.py │ ├── test_validation_utils.py │ └── test_validators_views.py ├── verify │ ├── ValidationTest.py │ └── __init__.py └── views.py ├── lib ├── MWSServerError.py ├── __init__.py ├── conf.py ├── db.py ├── decorators.py ├── log.py └── util.py └── server ├── __init__.py ├── app.py ├── crontab.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | # local settings 2 | .env 3 | deploy.yml 4 | 5 | # packages 6 | node_modules/ 7 | venv/ 8 | 9 | # build 10 | .grunt/ 11 | _SpecRunner.html 12 | *.pyc 13 | .tmp/ 14 | 15 | # env! 16 | webapps/*/devel 17 | webapps/*/staging 18 | webapps/*/prod 19 | 20 | 21 | # scripts 22 | webapps/ivs/initializers/scripts 23 | webapps/ivs/verify/scripts 24 | 25 | # misc 26 | .DS_Store 27 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "frontend/src/lib/falafel"] 2 | path = frontend/src/lib/falafel 3 | url = git://github.com/mcomella/node-falafel.git 4 | [submodule "frontend/src/lib/esprima"] 5 | path = frontend/src/lib/esprima 6 | url = git://github.com/ariya/esprima 7 | [submodule "frontend/src/lib/jasmine"] 8 | path = frontend/src/lib/jasmine 9 | url = https://github.com/pivotal/jasmine 10 | [submodule "frontend/src/lib/noty"] 11 | path = frontend/src/lib/noty 12 | url = git://github.com/needim/noty.git 13 | [submodule "frontend/src/lib/codemirror"] 14 | path = frontend/src/lib/codemirror 15 | url = https://github.com/marijnh/CodeMirror.git 16 | [submodule "frontend/src/lib/es5-shim"] 17 | path = frontend/src/lib/es5-shim 18 | url = git://github.com/kriskowal/es5-shim 19 | [submodule "frontend/src/lib/suspend.js"] 20 | path = frontend/src/lib/suspend.js 21 | url = git://github.com/FuegoFro/suspend.js.git 22 | [submodule "frontend/src/lib/bootstrap"] 23 | path = frontend/src/lib/bootstrap 24 | url = git@github.com:twbs/bootstrap.git 25 | -------------------------------------------------------------------------------- /.slugignore: -------------------------------------------------------------------------------- 1 | frontend/spec/ 2 | tests/ 3 | 4 | standalone_sample/ 5 | 6 | # XXX: Prevent heroku from detecting a node app. 7 | package.json 8 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | require('load-grunt-tasks')(grunt); 5 | require('time-grunt')(grunt); 6 | 7 | var dest = 'frontend/dist'; 8 | 9 | var concatConfig = { 10 | name: 'concat', 11 | createConfig: function(context, block) { 12 | /* Modified version of https://github.com/yeoman/grunt-usemin/blob/master/lib/config/concat.js 13 | `file.nonull = true` fails during build if a provided file doesn't exist */ 14 | var path = require('path'); 15 | 16 | var cfg = {files: []}; 17 | var outfile = path.join(context.outDir, block.dest); 18 | 19 | // Depending whether or not we're the last of the step we're not going to output the same thing 20 | var files = {}; 21 | files.dest = outfile; 22 | files.src = []; 23 | files.nonull = true; 24 | context.inFiles.forEach(function(f) { files.src.push(path.join(context.inDir, f));} ); 25 | cfg.files.push(files); 26 | context.outFiles = [block.dest]; 27 | return cfg; 28 | } 29 | }; 30 | 31 | grunt.initConfig({ 32 | watch: { 33 | livereload: { 34 | options: { 35 | livereload: 35730 36 | }, 37 | files: [ 38 | 'frontend/*.html', 39 | 'frontend/src/mws/**/*.js' 40 | ] 41 | } 42 | }, 43 | copy: { 44 | templates: { 45 | expand: true, 46 | cwd: 'frontend/', 47 | src: '*.html', 48 | dest: dest 49 | } 50 | }, 51 | clean: { 52 | dist: [dest + '/*', '!' + dest + '/.git*', '.tmp/*'] 53 | }, 54 | useminPrepare: { 55 | html: [ 56 | 'frontend/*.html' 57 | ], 58 | options: { 59 | flow: { 60 | steps: { 61 | js: [concatConfig, 'uglifyjs'], 62 | css: ['cssmin'] 63 | }, 64 | post: {} 65 | }, 66 | dest: dest, 67 | root: './frontend' 68 | } 69 | }, 70 | usemin: { 71 | html: dest + '/*.html', 72 | options: { 73 | // List of directories to look for revved versions of assets 74 | assetsDirs: [dest] 75 | } 76 | }, 77 | filerev: { 78 | options: { 79 | length: 10 80 | }, 81 | files: { 82 | src: ['frontend/dist/**/*.{js,css}'] 83 | } 84 | } 85 | }); 86 | 87 | 88 | grunt.registerTask('build', [ 89 | 'clean:dist', 90 | 'useminPrepare', 91 | 'copy:templates', 92 | 'concat', 93 | 'uglify', 94 | 'cssmin', 95 | 'filerev', 96 | 'usemin' 97 | ]); 98 | 99 | }; 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DISCLAIMER 2 | ========== 3 | 4 | Please note: all tools/ scripts in this repo are released for use "AS IS" without any warranties of any kind, including, but not limited to their installation, use, or performance. We disclaim any and all warranties, either express or implied, including but not limited to any warranty of noninfringement, merchantability, and/ or fitness for a particular purpose. We do not warrant that the technology will meet your requirements, that the operation thereof will be uninterrupted or error-free, or that any errors will be corrected. 5 | Any use of these scripts and tools is at your own risk. There is no guarantee that they have been through thorough testing in a comparable environment and we are not responsible for any damage or data loss incurred with their use. 6 | You are responsible for reviewing and testing any scripts you run thoroughly before use in any non-testing environment. 7 | 8 | mongo-web-shell 9 | =============== 10 | A recreation of the interactive mongoDB shell for the browser. 11 | 12 | There are three components related to the shell captured in this repo: 13 | 14 | 1. The server webapp - A flask application that provides a RESTish API for \ 15 | interacting with MongoDB. 16 | 2. The tutorial static html page - A static html file that initializes a \ 17 | shell, and hosts a simple tutorial. 18 | 3. The init-verification webapp - An api for confirming the validitiy of the \ 19 | contents of a users' databases. 20 | 21 | 22 | Installation 23 | ------------ 24 | __Requirements__: 25 | 26 | * [mongoDB][mongoDB install] 27 | * [node.js][] 28 | * [Python 2.6+] 29 | * [virtualenv][] 30 | 31 | After the above requirements are installed, clone the repo: 32 | 33 | git clone git@github.com:10gen-labs/mongo-web-shell.git && \ 34 | cd mongo-web-shell 35 | 36 | Create and activate a virtualenv: 37 | 38 | virtualenv venv && \ 39 | source venv/bin/activate 40 | 41 | In addition to some git submodules, the back-end dependencies are managed by 42 | pip, while the front-end dependencies are managed via npm. 43 | 44 | git submodule init && git submodule update 45 | pip install -r requirements.txt 46 | npm install 47 | 48 | [Grunt][] is used to build the front-end code. 49 | 50 | npm install -g grunt-cli 51 | 52 | ### Linters 53 | All committed code should be linted. 54 | 55 | Front-end: [jshint][]. Installation will occur in the `package.json` 56 | dependencies. 57 | 58 | Back-end: [pep8][]. Installation via pip within the virtual environment is 59 | recommended: 60 | 61 | pip install pep8 62 | 63 | Building 64 | -------- 65 | The default Grunt task will build the front-end code. 66 | 67 | grunt 68 | 69 | To perform this build whenever any source files change, use the `watch` target. 70 | 71 | grunt watch 72 | 73 | Running 74 | ------- 75 | After the installation and build above, launch a mongod instance that will be 76 | accessed by the back-end server: 77 | 78 | mongod 79 | 80 | Make sure that you have activated your virtualenv 81 | 82 | 83 | ### Environment variables 84 | 85 | `CONFIG_FILENAME` - the configuration file that Flask will use to start the server, see `sample.yml` for an example. 86 | Also see `webapps/lib/conf.py` for different default configuration paths depending on environment. This path is resolved 87 | relative to the root directory of the repo unless it begins with a /, in which case it will be evaluated as absolute. 88 | 89 | `MWS_FLASK_STATIC` - if set, this will have Flask serve static the static files, note that this should only be used 90 | for development, and production webservers such as apache (see below) or nginx should be used for production. 91 | 92 | ### The Server Webapp 93 | 94 | The server webapp can be run with: 95 | 96 | python -m webapps.server.app 97 | 98 | To set any environment variables for the script, use the `env` command. For example to run the server using the sample 99 | configuration file and to use Flask to serve static files, use the following command from the root of the git repo: 100 | 101 | env CONFIG_FILENAME=sample.yml MWS_FLASK_STATIC='' python -m webapps.server.app 102 | 103 | Make sure that you've run `grunt` to build the assets first. 104 | 105 | Note - for production, both for performance and security concerns, static assets should be served 106 | through apache or nginx, not Flask. 107 | 108 | Tests 109 | ----- 110 | ### Front-end 111 | Lint the code and run the test suite via the `test` target. 112 | 113 | grunt test 114 | 115 | To lint by hand, use the `jshint` target. 116 | 117 | grunt jshint 118 | 119 | To test in a browser environment, open `frontend/SpecRunner.html`. 120 | 121 | ### Back-end 122 | From within a virtual environment: 123 | 124 | python run_tests.py 125 | 126 | Lint via pep8. 127 | 128 | pep8 mongows tests run*.py 129 | 130 | Configuration 131 | ------------- 132 | 133 | In a development environment, you can specify configuration in a number of ways. 134 | 135 | You can choose to specify configurations by 136 | 137 | 1. Specify a yaml file containing configuration. For example, on my system 138 | I specify this: 139 | export CONFIG_FILENAME=sample.yml 140 | 141 | Alternatively, you can specify an absolute path: 142 | export CONFIG_FILENAME=/home/ian/development/mongo-web-shell/sample.yml 143 | 144 | In staging and production, because apache doesn't play well with environment 145 | variables, we default to /opt/10gen/trymongo-/shared/config.yml 146 | 147 | 2. Any of the variables that appear in `webapps/lib/conf.py` can be 148 | overridden with environment variables - they will override anything in the 149 | configuration file. 150 | 151 | More info 152 | --------- 153 | See the project [wiki][]. 154 | 155 | [wiki-config]: https://github.com/10gen-labs/mongo-web-shell/wiki/Configuration 156 | [Grunt]: http://gruntjs.com/ 157 | [jshint]: http://jshint.org/ 158 | [mongoDB install]: http://docs.mongodb.org/manual/installation/ 159 | [node.js]: http://nodejs.org/ 160 | [pep8]: https://github.com/jcrocholl/pep8 161 | [virtualenv]: http://www.virtualenv.org/en/latest/ 162 | [wiki]: https://github.com/10gen-labs/mongo-web-shell/wiki 163 | -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | from contextlib import nested 2 | from datetime import datetime 3 | import time 4 | 5 | from fabric.api import run, env, task, execute, runs_once, parallel 6 | from fabric.context_managers import cd, prefix 7 | import requests 8 | 9 | from webapps.lib.conf import get_deploy_config 10 | 11 | c = get_deploy_config() 12 | 13 | env.use_ssh_config = True 14 | 15 | 16 | config = { 17 | 'basedir': '/opt/10gen/', 18 | 'project': 'trymongo', 19 | 'origin': 'git@github.com:10gen-labs/mongo-web-shell.git', 20 | 'credentials': '/opt/10gen/{0}-{1}/shared/config.yml' 21 | } 22 | 23 | env.roledefs = { 24 | 'staging': ['trymongo-staging.10gen.cc'], 25 | 'prod': ['trymongo-1.10gen.cc', 'trymongo-2.10gen.cc'] 26 | } 27 | 28 | f_credentials = c.get('flowdock_credentials') 29 | f_user = f_credentials.get('user') 30 | f_pass = f_credentials.get('password') 31 | flowdock_url = 'https://api.flowdock.com/flows/10gen/tools/messages' 32 | 33 | datetime_format = '%Y%m%d%H%M%S' 34 | 35 | def broadcast(msg): 36 | if f_user and f_pass: 37 | requests.post(flowdock_url, auth=(f_user, f_pass), data={ 38 | 'event': 'message', 39 | 'content': msg 40 | }) 41 | 42 | ## Wrap with runs_once to prevent this task from running multiple times per host 43 | @runs_once 44 | @task 45 | def deploy(refspec): 46 | releasedir_map = execute(load_deploy, refspec) 47 | execute(swap_deploy, releasedir_map) 48 | 49 | @task 50 | def swap_deploy(releasedir_map): 51 | ## releasedir_map is a dict that maps host to releasedir 52 | releasedir = releasedir_map[env['host']] 53 | 54 | role = env.roles[0] 55 | project_user = config['project'] + '-' + role 56 | projectdir = project_user + '/' 57 | basedir = config['basedir'] + projectdir + 'releases/' 58 | 59 | try: 60 | with cd(config['basedir'] + projectdir): 61 | run('ln -sfn {0} {1}'.format(basedir+releasedir, 'current')) 62 | 63 | run('sudo /etc/init.d/httpd reload') 64 | broadcast('Swap successful') 65 | except SystemExit: 66 | with cd(basedir): 67 | run('mkdir -p duds') 68 | run('mv {0} duds'.format(releasedir)) 69 | 70 | 71 | @parallel 72 | def load_deploy(refspec): 73 | role = env.roles[0] 74 | project_user = config['project'] + '-' + role 75 | projectdir = project_user + '/' 76 | releasedir = time.strftime(datetime_format) 77 | basedir = config['basedir'] + projectdir + 'releases/' 78 | try: 79 | user = run('whoami') 80 | broadcast('{0} is deploying {1} to {2}'.format(user, refspec, env['host'])) 81 | 82 | with cd(basedir): 83 | run('mkdir {0}'.format(releasedir)) 84 | run('git clone {0} {1}'.format(config['origin'], releasedir)) 85 | 86 | with cd(basedir + releasedir): 87 | run('git checkout {0}'.format(refspec)) 88 | run('git submodule init') 89 | run('git submodule update') 90 | run('virtualenv venv') 91 | with nested( 92 | prefix('source venv/bin/activate'), 93 | prefix('export PIP_DOWNLOAD_CACHE={0}shared/.pip_download_cache'.format(config['basedir'] + projectdir)) 94 | ): 95 | run('pip install --allow-all-external -r requirements.txt') 96 | 97 | run('npm cache clean') 98 | run('npm install --production') 99 | run('grunt build') 100 | run('touch webapps/server/{0}'.format(role)) 101 | 102 | # Upgrade ownership and permissions 103 | run('chgrp -R {0} .'.format(project_user)) 104 | run('chmod -R g+w .') 105 | 106 | broadcast('Deploy download complete') 107 | return releasedir 108 | 109 | except SystemExit: 110 | with cd(basedir): 111 | run('mkdir -p duds') 112 | run('mv {0} duds'.format(releasedir)) 113 | 114 | broadcast(':rage3: Deploy failed') 115 | 116 | def get_basedir(role): 117 | projectdir = config['project'] + '-' + role + '/' 118 | return config['basedir'] + projectdir 119 | 120 | def get_releasedir(role): 121 | return get_basedir(role) + 'releases/' 122 | 123 | @task 124 | def remove_duds(): 125 | role = env.roles[0] 126 | releasedir = get_releasedir(role) 127 | with cd(releasedir): 128 | run('rm -rf duds/*') 129 | broadcast('duds/ directory emptied!') 130 | 131 | @task 132 | def cleanup(keep_count=3): 133 | role = env.roles[0] 134 | releasedir = get_releasedir(role) 135 | with cd(releasedir): 136 | remove_count = run("ls -1 | grep -P '\d+' | head -n -{0} | wc -l".format(keep_count)) 137 | if remove_count: 138 | run("ls -1 | grep -P '\d+' | head -n -{0} | xargs rm -rf".format(keep_count)) 139 | 140 | broadcast('{0} release directories removed. Thank you for taking out the trash!'.format(remove_count)) 141 | 142 | @task 143 | def rollback(): 144 | role = env.roles[0] 145 | basedir = get_basedir(role) 146 | with cd(basedir): 147 | currentdir = run("readlink current | awk -F/ '{print $NF}'") 148 | newdir = run("ls -1 releases | grep -P '\d+' | awk '$1 < {0}' | tail -n 1".format(currentdir)) 149 | if newdir: 150 | run("ln -sfn {0} {1}".format(basedir + 'releases/' + newdir, 'current')) 151 | run("touch {0}".format(role)) 152 | deploy_time = datetime.strptime(newdir, datetime_format) 153 | broadcast(":construction_worker: {0} rolled back to {1} (originally deployed @ {2})".format(env['host'], newdir, deploy_time)) 154 | else: 155 | broadcast(":rage3: nothing to roll back to") 156 | 157 | @task 158 | def rollforward(): 159 | role = env.roles[0] 160 | basedir = get_basedir(role) 161 | with cd(basedir): 162 | currentdir = run("readlink current | awk -F/ '{print $NF}'") 163 | newdir = run("ls -1 releases | grep -P '\d+' | awk '$1 > {0}' | head -n 1".format(currentdir)) 164 | if newdir: 165 | run("ln -sfn {0} {1}".format(basedir + 'releases/' + newdir, 'current')) 166 | run("touch {0}".format(role)) 167 | deploy_time = datetime.strptime(newdir, datetime_format) 168 | broadcast(":construction_worker: {0} rolled forward to {1} (originally deployed @ {2})".format(env['host'], newdir, deploy_time)) 169 | else: 170 | broadcast(":rage3: nothing to roll forward to") 171 | 172 | -------------------------------------------------------------------------------- /frontend/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise" : true, 3 | "camelcase" : true, 4 | "curly" : true, 5 | "eqeqeq" : true, 6 | "forin" : true, 7 | "immed" : true, 8 | "indent" : 2, 9 | "latedef" : false, 10 | "newcap" : true, 11 | "noarg" : true, 12 | "noempty" : true, 13 | "nonew" : true, 14 | "plusplus" : false, 15 | "quotmark" : "single", 16 | "undef" : true, 17 | "unused" : true, 18 | "strict" : false, 19 | "trailing" : true, 20 | 21 | "maxlen" : 100, 22 | 23 | "asi" : false, 24 | "boss" : false, 25 | "debug" : false, 26 | "eqnull" : false, 27 | "es5" : false, 28 | "esnext" : false, 29 | "evil" : false, 30 | "expr" : false, 31 | "funcscope" : false, 32 | "globalstrict" : false, 33 | "iterator" : false, 34 | "lastsemic" : false, 35 | "laxbreak" : false, 36 | "laxcomma" : false, 37 | "loopfunc" : false, 38 | "multistr" : false, 39 | "proto" : false, 40 | "scripturl" : false, 41 | "smarttabs" : false, 42 | "shadow" : false, 43 | "sub" : false, 44 | "supernew" : false, 45 | "validthis" : false, 46 | 47 | "browser" : true, 48 | "couch" : false, 49 | "devel" : false, 50 | "dojo" : false, 51 | "jquery" : true, 52 | "mootools" : false, 53 | "node" : false, 54 | "nonstandard" : false, 55 | "prototypejs" : false, 56 | "rhino" : false, 57 | "wsh" : false, 58 | 59 | "nomen" : false, 60 | "onevar" : false, 61 | "passfail" : false, 62 | "white" : false 63 | } 64 | -------------------------------------------------------------------------------- /frontend/SpecRunner.html: -------------------------------------------------------------------------------- 1 | 17 | 18 | 20 | 21 | 22 | Jasmine Spec Runner 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /frontend/footer.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | position: absolute; 3 | bottom: 0; 4 | width: 100%; 5 | margin-top: 4em; 6 | margin-bottom: -20px; 7 | padding: 20px 0; 8 | background-color: white; 9 | z-index: 100; 10 | } 11 | 12 | .social { 13 | height: 21px; 14 | margin-top: 8px; 15 | float: right; 16 | } 17 | 18 | .footer .social .img { 19 | background-repeat: no-repeat; 20 | background-size: 21px 21px; 21 | width: 21px; 22 | height: 21px; 23 | display: inline-block; 24 | } 25 | 26 | .footer .social .twitter-icon .img { 27 | background-image: url(http://media.mongodb.org.s3.amazonaws.com/social-twitter.png); 28 | } 29 | 30 | .footer .social .youtube-icon .img { 31 | background-image: url(http://media.mongodb.org.s3.amazonaws.com/social-youtube.png); 32 | } 33 | 34 | .footer .social .facebook-icon .img { 35 | background-image: url(http://media.mongodb.org.s3.amazonaws.com/social-facebook.png); 36 | } 37 | 38 | .footer .social .gplus-icon .img { 39 | background-image: url(http://media.mongodb.org.s3.amazonaws.com/social-gplus.png); 40 | } 41 | 42 | @media only screen and (-webkit-min-device-pixel-ratio: 2) { 43 | .footer .social .twitter-icon .img { 44 | background-image: url(http://media.mongodb.org.s3.amazonaws.com/social-twitter@2x.png); 45 | } 46 | 47 | .footer .social .youtube-icon .img { 48 | background-image: url(http://media.mongodb.org.s3.amazonaws.com/social-youtube@2x.png); 49 | } 50 | 51 | .footer .social .facebook-icon .img { 52 | background-image: url(http://media.mongodb.org.s3.amazonaws.com/social-facebook@2x.png); 53 | } 54 | 55 | .footer .social .gplus-icon .img { 56 | background-image: url(http://media.mongodb.org.s3.amazonaws.com/social-gplus@2x.png); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /frontend/header.css: -------------------------------------------------------------------------------- 1 | /* header-db */ 2 | 3 | #header-db { 4 | min-height: 100px; 5 | background-color: #402817; 6 | } 7 | 8 | #header-db .container { 9 | position: relative; 10 | } 11 | 12 | #logo { 13 | float: left; 14 | margin-top: 23px; 15 | } 16 | 17 | #logo img { 18 | width: 200px; 19 | } 20 | 21 | div.gsc-control-cse-en, div.gsc-control-cse { padding: 0 !important; } 22 | 23 | form.gsc-search-box { 24 | background-color: rgba(255,255,255,0.3); 25 | border-radius: 6px; 26 | border: 1px solid #3b2920; 27 | } 28 | 29 | .gsc-search-box.gsc-search-box-tools .gsc-search-box .gsc-input { 30 | padding: 0; 31 | } 32 | 33 | div.gsc-input-box, 34 | .gsc-search-box .gsc-input>input:hover, .gsc-input-box-hover, 35 | .gsc-search-box .gsc-input>input:focus, .gsc-input-box-focus { 36 | border: 0; 37 | background: transparent; 38 | box-shadow: none; 39 | } 40 | 41 | /* [name] selector is a hack to override google's stylsheet */ 42 | .gsc-input input.gsc-input[name="search"] { 43 | background-color: transparent !important; 44 | color: white; 45 | font-weight: 300; 46 | font-size: 15px; 47 | height: 1.3em !important; 48 | } 49 | 50 | .gsc-input input.gsc-input::-webkit-input-placeholder, 51 | .gsc-input input.gsc-input:-moz-input-placeholder, 52 | .gsc-input input.gsc-input::-moz-input-placeholder, 53 | .gsc-input input.gsc-input:-ms-input-placeholder { 54 | color: #CCC; 55 | } 56 | 57 | /* Clear search button */ 58 | .gsib_b { 59 | display: none; 60 | } 61 | 62 | /* [title] selector is a hack to override google's stylesheet */ 63 | input.gsc-search-button[title], input.gsc-search-button:hover[title], input.gsc-search-button:focus[title] { 64 | border-color: transparent; 65 | background-color: transparent; 66 | padding: 0 8px; 67 | box-sizing: content-box; 68 | -moz-box-sizing: content-box; 69 | -webkit-box-sizing: content-box; 70 | } 71 | 72 | #etp { 73 | color: white; 74 | margin-top: 30px; 75 | } 76 | 77 | #etp ul { 78 | padding: 0; 79 | margin: 0; 80 | } 81 | 82 | #etp li { 83 | display: inline; 84 | list-style-type: none; 85 | font-size: 14px; 86 | font-weight: 300; 87 | } 88 | 89 | #header-db .search-db { 90 | height: 50px; 91 | margin-bottom: -11px; 92 | transition: width 0.3s linear; 93 | -webkit-transition: width 0.3s linear; 94 | -moz-transition: width 0.3s linear; 95 | -o-transition: width 0.3s linear; 96 | padding: 11px 0 11px 0; 97 | margin-left: 10px; 98 | } 99 | 100 | #header-db .search-db.narrow { 101 | width: 98px; 102 | } 103 | 104 | #header-db .search-db.wide { 105 | width: 226px; 106 | } 107 | 108 | .navbar-header { 109 | margin-top: 24px; 110 | } 111 | 112 | .navbar-header .navbar-toggle { 113 | border-color: #f1f2f5; 114 | } 115 | 116 | .navbar-header .navbar-toggle .icon-bar { 117 | background-color: #f1f2f5; 118 | } 119 | 120 | #etp .nav { 121 | float: right; 122 | } 123 | 124 | #etp li>a { 125 | color: white; 126 | } 127 | 128 | #etp li>a:hover, 129 | #etp li>a:focus { 130 | background-color: #53341e; 131 | } 132 | 133 | @media (max-width: 992px) { 134 | #etp .nav { 135 | float: none; 136 | } 137 | 138 | #header-db .search-db { 139 | margin-bottom: 0; 140 | margin-left: 10px; 141 | margin-right: 10px; 142 | } 143 | 144 | #header-db .search-db, 145 | #header-db .search-db.narrow, 146 | #header-db .search-db.wide { 147 | width: auto; 148 | padding-top: 5px; 149 | } 150 | 151 | #etp { 152 | margin-top: 15px; 153 | 154 | width: 100%; 155 | 156 | padding-left: 0; 157 | padding-right: 0; 158 | 159 | margin-left: 0; 160 | } 161 | 162 | #etp li { 163 | display: block; 164 | float: none; 165 | } 166 | 167 | #etp li>a:hover, 168 | #etp li>a:focus { 169 | background-color: #784b2b; 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /frontend/mongoWebShell.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | html { 16 | height: 100%; 17 | margin: 0; 18 | } 19 | body { 20 | height: 100%; 21 | background-color: #f3f4eb; 22 | margin: 0; 23 | } 24 | h1 { 25 | margin-top: 0px; 26 | margin-bottom: 0px; 27 | } 28 | .scroll-wrapper { 29 | height: 100%; 30 | min-height: 780px; 31 | position: relative; 32 | } 33 | 34 | .mongo-web-shell { 35 | height: 30em; 36 | padding: 8px 16px; 37 | font-size: 15px; 38 | font-family: monospace; 39 | margin-top: 15px; 40 | } 41 | 42 | .mongo-web-shell p { 43 | padding: 0; 44 | margin: 0; 45 | } 46 | 47 | .content-wrapper { 48 | padding-top: 15px; 49 | } 50 | 51 | .mws-responses { 52 | margin-left: -8px; 53 | } 54 | 55 | .mws-input-wrapper { 56 | line-height: 29px; 57 | position: relative; 58 | } 59 | 60 | .mws-input { 61 | display: inline-block; 62 | position: absolute; 63 | top: 0; 64 | left: 10px; 65 | } 66 | 67 | .mws-prompt { 68 | display: inline-block; 69 | position: absolute; 70 | top: 0; 71 | left: 1px; 72 | } 73 | 74 | .mws-scroll-wrapper { 75 | overflow-y: auto; 76 | height: 100%; 77 | } 78 | 79 | /* Try to make the scrollbar less ugly, at least on IE and WebKit */ 80 | .mongo-web-shell ::-webkit-scrollbar { 81 | width: 7px; 82 | } 83 | 84 | .mongo-web-shell ::-webkit-scrollbar-track { 85 | visibility: hidden; /* doesn't seem to work */ 86 | background: transparent; 87 | } 88 | 89 | .mongo-web-shell ::-webkit-scrollbar-thumb { 90 | border-radius: 4px; 91 | background: rgba(0,0,0,0.5); 92 | } 93 | 94 | .mongo-web-shell ::-webkit-scrollbar:window-inactive { 95 | visibility: hidden; 96 | } 97 | 98 | /* Code Mirror overrides */ 99 | .mongo-web-shell .CodeMirror { 100 | height: auto; 101 | } 102 | 103 | div.CodeMirror-vscrollbar { 104 | overflow: visible; 105 | } 106 | 107 | .cm-s-solarized.CodeMirror { 108 | -moz-box-shadow: none; 109 | -webkit-box-shadow: none; 110 | box-shadow: none; 111 | background-image: none; 112 | } 113 | 114 | /* Allows removal of highlighting on a line */ 115 | .mws-cm-plain-text, .mws-cm-plain-text span { 116 | color: inherit !important; 117 | } 118 | -------------------------------------------------------------------------------- /frontend/spec/disableConsole.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | var ATTR = [ 17 | 'debug', 18 | 'error', 19 | 'info', 20 | 'log', 21 | 'warn' 22 | ]; 23 | var console = {}; 24 | ATTR.forEach(function (attr) { 25 | console[attr] = function () {}; 26 | }); 27 | -------------------------------------------------------------------------------- /frontend/spec/globals.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /* exported CONST, MWS_HOST */ 17 | /** 18 | * This file contains various global variables that are used throughout 19 | * testing. 20 | */ 21 | $.ready = function () {}; // Prevent mongo.init.run() from running. 22 | 23 | var MWS_HOST = 'http://mwshost.example.com'; 24 | 25 | var CONST = { 26 | rootClass: 'mongo-web-shell', 27 | domConfig: { 28 | dataAttrKeys: { 29 | cssPath: 'css-path', 30 | mwsHost: 'mws-host' 31 | }, 32 | defaults: { 33 | cssPath: 'mongoWebShell.css', 34 | mwsHost: '', 35 | baseUrlPostfix: '/mws/' 36 | } 37 | }, 38 | scriptName: 'mongoWebShell.js' 39 | }; 40 | -------------------------------------------------------------------------------- /frontend/spec/mws/DB.spec.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /* global describe, it, beforeEach, mongo, spyOn, expect, jasmine */ 17 | describe('The DB class', function () { 18 | var db; 19 | beforeEach(function () { 20 | spyOn(mongo.util, 'getDBResURL').andReturn('/test/url'); 21 | var shell = { 22 | insertResponseLine: jasmine.createSpy('insertResponseLine') 23 | }; 24 | db = new mongo.DB(shell, 'testdb'); 25 | }); 26 | 27 | it('saves the name and shell', function () { 28 | var name = 'mydb'; 29 | var shell = {}; 30 | var db = new mongo.DB(shell, name); 31 | expect(db.name).toEqual(name); 32 | expect(db.shell).toBe(shell); 33 | }); 34 | 35 | it('has a nice string representation', function () { 36 | db.name = 'mydb'; 37 | expect(db.toString()).toEqual('mydb'); 38 | }); 39 | 40 | it('uses method missing to create collections', function () { 41 | var referenceCollection = {name: 'refcoll'}; 42 | spyOn(mongo, 'Coll').andReturn(referenceCollection); 43 | 44 | var coll = db.__methodMissing('testcoll'); 45 | expect(coll).toBe(referenceCollection); 46 | expect(db.testcoll).toBe(coll); 47 | }); 48 | 49 | describe('showing collection names', function () { 50 | var makeRequest; 51 | 52 | beforeEach(function () { 53 | makeRequest = spyOn(mongo.request, 'makeRequest'); 54 | }); 55 | 56 | it('constructs and uses the db url', function () { 57 | var shell = {mwsResourceID: 'my_res_id'}; 58 | var url = '/my/test/url'; 59 | mongo.util.getDBResURL.andReturn(url); 60 | db.shell = shell; 61 | 62 | db.getCollectionNames(); 63 | expect(mongo.util.getDBResURL).toHaveBeenCalledWith('my_res_id'); 64 | expect(makeRequest.calls[0].args[0]).toEqual(url + 'getCollectionNames'); 65 | }); 66 | 67 | it('doesn\'t pass in HTTP params', function () { 68 | db.getCollectionNames(); 69 | expect(makeRequest.calls[0].args[1]).toBeUndefined(); 70 | }); 71 | 72 | it('uses the get HTTP method', function () { 73 | db.getCollectionNames(); 74 | expect(makeRequest.calls[0].args[2]).toEqual('GET'); 75 | }); 76 | 77 | it('uses the db\'s shell', function () { 78 | db.getCollectionNames(); 79 | expect(makeRequest.calls[0].args[4]).toBe(db.shell); 80 | }); 81 | 82 | it('passes the callback through', function () { 83 | var callback = function () {return 'my callback';}; 84 | db.getCollectionNames(callback); 85 | expect(makeRequest.calls[0].args[5]).toBe(callback); 86 | }); 87 | }); 88 | }); -------------------------------------------------------------------------------- /frontend/spec/mws/config.spec.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /* global describe, expect, it, mongo */ 17 | describe('The const module', function () { 18 | it('stores keycode constants', function () { 19 | var key = mongo.config.keycodes; 20 | expect(key.enter).toBe(13); 21 | expect(key.left).toBe(37); 22 | expect(key.up).toBe(38); 23 | expect(key.right).toBe(39); 24 | expect(key.down).toBe(40); 25 | }); 26 | 27 | it('stores the keep alive timeout', function () { 28 | expect(mongo.config.keepAliveTime).toBeDefined(); 29 | }); 30 | 31 | it('stores the root element CSS selector', function () { 32 | expect(mongo.config.rootElementSelector).toBeDefined(); 33 | }); 34 | 35 | it('stores the script name', function () { 36 | expect(mongo.config.scriptName).toBeDefined(); 37 | }); 38 | 39 | it('stores the shell batch size', function () { 40 | expect(mongo.config.shellBatchSize).toBeDefined(); 41 | }); 42 | 43 | it('gets and stores the MWS host', function () { 44 | expect(mongo.config.mwsHost).toEqual('http://mwshost.example.com'); 45 | }); 46 | 47 | it('generates and stores the baseUrl', function(){ 48 | expect(mongo.config.baseUrl).toBeDefined(); 49 | expect(mongo.config.baseUrl.indexOf(mongo.config.mwsHost) > -1).toBe(true); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /frontend/spec/mws/keyword.spec.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /* global afterEach, beforeEach, describe, expect, it, jasmine, mongo, spyOn, sinon */ 17 | describe('The keyword module', function () { 18 | var mk = mongo.keyword; 19 | var shell; 20 | 21 | beforeEach(function () { 22 | shell = new mongo.Shell($('
'), 0); 23 | spyOn(shell, 'insertResponseLine'); 24 | spyOn(shell, 'insertResponseArray'); 25 | }); 26 | 27 | describe('when parsing source', function () { 28 | it('returns whether or not the source is a keyword expression', function () { 29 | var wasKeyword = mongo.keyword.handleKeywords(shell, 'this.is = not.a.keyword'); 30 | expect(wasKeyword).toBe(false); 31 | 32 | spyOn(mongo.keyword, 'it'); 33 | wasKeyword = mongo.keyword.handleKeywords(shell, 'it is a keyword'); 34 | expect(wasKeyword).toBe(true); 35 | }); 36 | 37 | it('calls the appropriate keyword function', function () { 38 | mongo.keyword.myAwesomeKeyword = jasmine.createSpy('my keyword'); 39 | mongo.keyword.handleKeywords(shell, 'myAwesomeKeyword foo bar'); 40 | expect(mongo.keyword.myAwesomeKeyword).toHaveBeenCalledWith(shell, ['foo', 'bar']); 41 | }); 42 | }); 43 | 44 | it('prints a batch from the last used cursor', function () { 45 | var cursor = new mongo.Cursor({shell: shell}); 46 | spyOn(cursor, '_printBatch').andCallThrough(); 47 | cursor._executed = true; 48 | cursor._storeQueryResult(['foo']); 49 | 50 | shell.lastUsedCursor = null; 51 | mk.it(shell); 52 | expect(shell.insertResponseLine).toHaveBeenCalledWith('no cursor'); 53 | 54 | shell.lastUsedCursor = cursor; 55 | expect(cursor._printBatch).not.toHaveBeenCalled(); 56 | mk.it(shell); 57 | expect(cursor._printBatch).toHaveBeenCalled(); 58 | 59 | shell.insertResponseLine.reset(); 60 | mk.it(shell); 61 | expect(shell.insertResponseLine).toHaveBeenCalledWith('no cursor'); 62 | 63 | }); 64 | 65 | describe('the show keyword', function () { 66 | beforeEach(function () { 67 | shell.db = {getCollectionNames: jasmine.createSpy('getCollectionNames')}; 68 | }); 69 | 70 | it('handles unimplemented arguments', function () { 71 | mongo.keyword.show(shell, ['doesNotExist']); 72 | expect(shell.insertResponseLine).toHaveBeenCalledWith('ERROR: Not yet implemented'); 73 | }); 74 | 75 | it('can list collections', function () { 76 | mongo.keyword.show(shell, ['collections']); 77 | expect(shell.db.getCollectionNames.calls.length).toEqual(1); 78 | var callback = shell.db.getCollectionNames.calls[0].args[0]; 79 | var r = {result: ['a', 'b', 'c']}; 80 | callback(r); 81 | expect(shell.insertResponseLine.calls.length).toEqual(3); 82 | expect(shell.insertResponseLine.calls[0].args).toEqual(['a']); 83 | expect(shell.insertResponseLine.calls[1].args).toEqual(['b']); 84 | expect(shell.insertResponseLine.calls[2].args).toEqual(['c']); 85 | }); 86 | 87 | it('can list tables', function () { 88 | mongo.keyword.show(shell, ['tables']); 89 | expect(shell.db.getCollectionNames.calls.length).toEqual(1); 90 | var callback = shell.db.getCollectionNames.calls[0].args[0]; 91 | var r = {result: ['a', 'b', 'c']}; 92 | callback(r); 93 | expect(shell.insertResponseLine.calls.length).toEqual(3); 94 | expect(shell.insertResponseLine.calls[0].args).toEqual(['a']); 95 | expect(shell.insertResponseLine.calls[1].args).toEqual(['b']); 96 | expect(shell.insertResponseLine.calls[2].args).toEqual(['c']); 97 | }); 98 | 99 | it('requires at least one argument', function () { 100 | var message = 'ERROR: show requires at least one argument'; 101 | mongo.keyword.show(shell, []); 102 | expect(shell.insertResponseLine.calls.length).toBe(1); 103 | expect(shell.insertResponseLine).toHaveBeenCalledWith(message); 104 | }); 105 | }); 106 | 107 | it('warns the user that the "use" keyword is disabled', function () { 108 | var message = 'Cannot change db: functionality disabled.'; 109 | mk.use(shell); 110 | expect(shell.insertResponseLine).toHaveBeenCalledWith(message); 111 | }); 112 | 113 | describe('the reset keyword', function(){ 114 | beforeEach(function(){ 115 | shell.readline = jasmine.createSpyObj('Readline', ['getLastCommand']); 116 | shell.readline.getLastCommand.andReturn('not reset'); 117 | mongo.keyword._resetHasBeenCalled = false; 118 | }); 119 | 120 | it('requires confirmation on first run', function(){ 121 | spyOn(mongo.request, 'makeRequest'); 122 | shell.readline.getLastCommand.andReturn('reset'); 123 | mongo.keyword.reset(shell); 124 | expect(mongo.request.makeRequest).not.toHaveBeenCalled(); 125 | }); 126 | 127 | it('confirms before reset', function(){ 128 | spyOn(mongo.request, 'makeRequest'); 129 | shell.readline.getLastCommand.andReturn('not reset'); 130 | mongo.keyword.reset(shell); 131 | expect(mongo.request.makeRequest).not.toHaveBeenCalled(); 132 | }); 133 | 134 | describe('assuming the reset is confirmed', function(){ 135 | var xhr, requests = []; 136 | 137 | beforeEach(function(){ 138 | xhr = sinon.useFakeXMLHttpRequest(); 139 | xhr.onCreate = function (xhr) { requests.push(xhr); }; 140 | 141 | shell.readline.getLastCommand.andReturn('reset'); 142 | mongo.config = {baseUrl: '/test_url/'}; 143 | shell.mwsResourceID = 'test_res_id'; 144 | mongo.keyword.reset(shell); 145 | }); 146 | 147 | afterEach(function () { 148 | xhr.restore(); 149 | }); 150 | 151 | it('drops the database', function(){ 152 | var makeRequest = spyOn(mongo.request, 'makeRequest'); 153 | mongo.keyword.reset(shell); 154 | expect(makeRequest.calls[0].args[0]).toEqual('/test_url/test_res_id/db'); 155 | expect(makeRequest.calls[0].args[2]).toEqual('DELETE'); 156 | expect(makeRequest.calls[0].args[4]).toBe(shell); 157 | }); 158 | 159 | it('runs the initialization scripts', function(){ 160 | spyOn(mongo.init, 'runInitializationScripts'); 161 | 162 | mongo.keyword.reset(shell); 163 | requests[0].respond(204); 164 | expect(mongo.init.runInitializationScripts).toHaveBeenCalled(); 165 | }); 166 | 167 | it('requires confirmation to reset again immediately', function () { 168 | var makeRequest = spyOn(mongo.request, 'makeRequest'); 169 | // Already confirmed, should be called 170 | mongo.keyword.reset(shell); 171 | expect(makeRequest.calls.length).toEqual(1); 172 | 173 | // Re-resetting, should re-confirm 174 | mongo.keyword.reset(shell); 175 | expect(makeRequest.calls.length).toEqual(1); 176 | mongo.keyword.reset(shell); 177 | expect(makeRequest.calls.length).toEqual(2); 178 | }); 179 | }); 180 | }); 181 | 182 | describe('the help keyword', function(){ 183 | it('prints out the help message', function(){ 184 | mongo.keyword.help(shell); 185 | expect(shell.insertResponseLine).toHaveBeenCalled(); 186 | }); 187 | }); 188 | 189 | }); 190 | -------------------------------------------------------------------------------- /frontend/spec/mws/mutateSource.spec.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /* global describe, expect, it, mongo */ 17 | describe('The mutateSource module', function () { 18 | it('replaces object field reads with a getter function', function () { 19 | var source = 'a.foo + 1; foo[bar()]["baz"]'; 20 | var mutated = mongo.mutateSource.swapMemberAccesses(source); 21 | 22 | var expected = '__get(a, "foo") + 1; __get(__get(foo, bar()), "baz")'; 23 | expect(mutated).toEqual(expected); 24 | }); 25 | 26 | it('doesn\'t wrap bracketed identifiers in quotes', function () { 27 | var source = 'i = 0; a[i];'; 28 | var mutated = mongo.mutateSource.swapMemberAccesses(source); 29 | 30 | var expected = 'i = 0; __get(a, i);'; 31 | expect(mutated).toEqual(expected); 32 | }); 33 | 34 | it('doesn\'t change object field writes', function () { 35 | var source = 'a.foo = a.foo; foo.bar.baz = "hello"'; 36 | var mutated = mongo.mutateSource.swapMemberAccesses(source); 37 | 38 | var expected = 'a.foo = __get(a, "foo"); __get(foo, "bar").baz = "hello"'; 39 | expect(mutated).toEqual(expected); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /frontend/spec/mws/request.spec.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /* jshint camelcase: false */ 17 | /* global afterEach, beforeEach, describe, expect, it, jasmine, mongo, sinon, spyOn, noty:true */ 18 | describe('The request module', function () { 19 | var RES_URL = 'resURL/'; 20 | var configStore; 21 | var xhr, requests; 22 | 23 | beforeEach(function () { 24 | spyOn(mongo.util, 'getDBCollectionResURL').andReturn(RES_URL); 25 | spyOn(mongo.util, 'getDBResURL').andReturn(RES_URL); 26 | configStore = mongo.config; 27 | mongo.config = {}; 28 | 29 | xhr = sinon.useFakeXMLHttpRequest(); 30 | xhr.onCreate = function (xhr) { requests.push(xhr); }; 31 | requests = []; 32 | }); 33 | 34 | afterEach(function () { 35 | mongo.config = configStore; 36 | configStore = null; 37 | 38 | xhr.restore(); 39 | }); 40 | 41 | describe('making a request', function () { 42 | var url_, data_, method_, name_, shell_; 43 | 44 | beforeEach(function () { 45 | url_ = 'http://test.com/'; 46 | data_ = {test: 'my data'}; 47 | method_ = 'POST'; 48 | name_ = 'test'; 49 | shell_ = new mongo.Shell($('
'), 0); 50 | }); 51 | 52 | it('uses the given url and HTTP method', function () { 53 | var url = 'http://test.com/'; 54 | var method = 'POST'; 55 | mongo.request.makeRequest(url, data_, method, name_, shell_); 56 | 57 | expect(requests[0].url).toEqual(url); 58 | expect(requests[0].method).toEqual(method); 59 | }); 60 | 61 | it('stringifies JSON', function () { 62 | var data = {foo: 'mydata'}; 63 | mongo.request.makeRequest(url_, data, method_, name_, shell_); 64 | 65 | var stringifiedData = JSON.stringify(data); 66 | expect(requests[0].requestBody).toEqual(stringifiedData); 67 | expect(requests[0].requestHeaders['Content-Type']).toMatch('application/json'); 68 | expect(requests[0].requestHeaders.Accept).toMatch('^application/json, text/javascript'); 69 | }); 70 | 71 | it('calls onSuccess appropriately', function () { 72 | var onSuccess = jasmine.createSpy(); 73 | 74 | mongo.request.makeRequest(url_, data_, method_, name_, shell_, onSuccess); 75 | requests[0].respond(500, '', 'INTERNAL SERVER ERROR'); 76 | expect(onSuccess).not.toHaveBeenCalled(); 77 | 78 | mongo.request.makeRequest(url_, data_, method_, name_, shell_, onSuccess); 79 | expect(onSuccess).not.toHaveBeenCalled(); 80 | var originalData = {msg: 'Success'}; 81 | var responseBody = JSON.stringify(originalData); 82 | requests[1].respond(200, '', responseBody); 83 | expect(onSuccess).toHaveBeenCalledWith(originalData); 84 | }); 85 | 86 | it('writes failure reasons to the shell', function () { 87 | spyOn(shell_, 'insertResponseLine'); 88 | mongo.request.makeRequest(url_, data_, method_, name_, shell_); 89 | requests[0].respond(200, '', JSON.stringify({foo: 'bar'})); 90 | expect(shell_.insertResponseLine).not.toHaveBeenCalled(); 91 | 92 | // Error, no details 93 | mongo.request.makeRequest(url_, data_, method_, name_, shell_); 94 | var errResponse = JSON.stringify({error: 400, reason: 'My Reason', detail: ''}); 95 | requests[1].respond(400, '', errResponse); 96 | expect(shell_.insertResponseLine).toHaveBeenCalledWith('ERROR: My Reason'); 97 | 98 | // Error with details 99 | mongo.request.makeRequest(url_, data_, method_, name_, shell_); 100 | errResponse = JSON.stringify({error: 400, reason: 'My Reason', detail: 'Some details'}); 101 | requests[2].respond(400, '', errResponse); 102 | expect(shell_.insertResponseLine).toHaveBeenCalledWith('ERROR: My Reason\nSome details'); 103 | 104 | expect(shell_.insertResponseLine.calls.length).toEqual(2); 105 | }); 106 | 107 | it('is asynchronous by default', function () { 108 | mongo.request.makeRequest(url_, data_, method_, name_, shell_, null); 109 | expect(requests[0].async).toBe(true); 110 | 111 | mongo.request.makeRequest(url_, data_, method_, name_, shell_, null, true); 112 | expect(requests[1].async).toBe(true); 113 | 114 | mongo.request.makeRequest(url_, data_, method_, name_, shell_, null, false); 115 | expect(requests[2].async).toBe(false); 116 | }); 117 | 118 | it('pauses and resumes evaluation', function () { 119 | var pause = spyOn(shell_.evaluator, 'pause').andCallThrough(); 120 | var resume = spyOn(shell_.evaluator, 'resume'); 121 | 122 | mongo.request.makeRequest(url_, data_, method_, name_, shell_); 123 | expect(pause).toHaveBeenCalled(); 124 | expect(resume).not.toHaveBeenCalled(); 125 | requests[0].respond(200, '', '{}'); 126 | expect(resume).toHaveBeenCalled(); 127 | 128 | pause.reset(); 129 | resume.reset(); 130 | 131 | mongo.request.makeRequest(url_, data_, method_, name_, shell_); 132 | expect(pause).toHaveBeenCalled(); 133 | expect(resume).not.toHaveBeenCalled(); 134 | requests[1].respond(500, '', JSON.stringify({reason: 'Error occured', detail: 'Details'})); 135 | expect(resume.mostRecentCall.args[1]).toEqual(new Error('ERROR: Error occured\nDetails')); 136 | expect(resume.mostRecentCall.args[2]).toEqual(true); 137 | }); 138 | }); 139 | 140 | describe('keeps the session alive and', function(){ 141 | var requestSuccess = function(success){ 142 | var shell = {mwsResourceID: 'my_resource'}; 143 | mongo.request.keepAlive(shell); 144 | requests[0].respond(success ? 204 : 500, {}, null); 145 | }; 146 | 147 | beforeEach(function(){ 148 | noty = jasmine.createSpy(); 149 | }); 150 | 151 | it('makes a keepalive request', function () { 152 | mongo.config.baseUrl = 'base'; 153 | var resourceID = 'iu'; 154 | var expectedURL = mongo.config.baseUrl + resourceID + '/keep-alive'; 155 | mongo.request.keepAlive(resourceID); 156 | expect(requests.length).toBe(1); 157 | var req = requests[0]; 158 | expect(req.method).toBe('POST'); 159 | expect(req.url).toBe(expectedURL); 160 | expect(req.requestBody).toBe(null); 161 | // There is nothing to test for if the request succeeds or not. 162 | }); 163 | 164 | it('notifies the user on disconnection', function(){ 165 | requestSuccess(false); 166 | expect(noty).toHaveBeenCalled(); 167 | }); 168 | 169 | it('does not create notification on success', function(){ 170 | requestSuccess(true); 171 | expect(noty).not.toHaveBeenCalled(); 172 | }); 173 | 174 | it('closes notification once on success', function(){ 175 | spyOn(window, 'setTimeout').andCallThrough(); 176 | jasmine.Clock.useMock(); 177 | mongo.keepaliveNotification = jasmine.createSpyObj('keepaliveNotification', 178 | ['close', 'setText']); 179 | mongo.keepaliveNotification.close.andCallFake(function(){ 180 | delete mongo.keepaliveNotification; 181 | }); 182 | 183 | requestSuccess(true); 184 | expect(mongo.keepaliveNotification.setText).toHaveBeenCalledWith('and we\'re back!'); 185 | expect(window.setTimeout).toHaveBeenCalled(); 186 | 187 | expect(mongo.keepaliveNotification.close).not.toHaveBeenCalled(); 188 | jasmine.Clock.tick(1501); 189 | expect(mongo.keepaliveNotification).toBe(undefined); 190 | 191 | window.setTimeout.reset(); 192 | 193 | requestSuccess(true); 194 | expect(window.setTimeout).not.toHaveBeenCalled(); 195 | expect(mongo.keepaliveNotification).toBe(undefined); 196 | }); 197 | }); 198 | 199 | /** 200 | * Valids the requests themselves, rather than the actions taken upon their 201 | * failure or success. 202 | */ 203 | describe('creates a request that', function () { 204 | it('creates an MWS resource', function () { 205 | var baseUrl = '/mws/'; 206 | mongo.config.baseUrl = baseUrl; 207 | var callbackSpy = jasmine.createSpy('callback'); 208 | var shellSpy = jasmine.createSpyObj('Shell', ['insertResponseLine', 'insertError']); 209 | 210 | mongo.request.createMWSResource([shellSpy], callbackSpy); 211 | expect(requests.length).toBe(1); 212 | var req = requests[0]; 213 | expect(req.method).toBe('POST'); 214 | expect(req.url).toBe(baseUrl); 215 | expect(req.requestBody).toBe(null); 216 | 217 | var body = {res_id: 'iu'}; 218 | // TODO: FF23 complains 'not well-formed' for response body, but 219 | // continues testing anyway. Chromium is fine. 220 | req.respond(200, '', JSON.stringify(body)); 221 | expect(callbackSpy).toHaveBeenCalledWith(body); 222 | expect(shellSpy.insertResponseLine).not.toHaveBeenCalled(); 223 | 224 | // Failure: invalid data. 225 | mongo.request.createMWSResource([shellSpy], callbackSpy); 226 | req = requests[1]; 227 | req.respond(200, '', JSON.stringify({daebak: 'iu'})); 228 | expect(shellSpy.insertError).toHaveBeenCalled(); 229 | 230 | // Failure: HTTP error. 231 | mongo.request.createMWSResource([shellSpy], callbackSpy); 232 | req = requests[2]; 233 | req.respond(404, '', ''); 234 | expect(shellSpy.insertResponseLine).toHaveBeenCalled(); 235 | 236 | expect(callbackSpy.calls.length).toBe(1); 237 | }); 238 | 239 | it('keeps the shell mws resource alive', function () { 240 | mongo.config.baseUrl = 'base'; 241 | var resourceID = 'iu'; 242 | var expectedURL = mongo.config.baseUrl + resourceID + 243 | '/keep-alive'; 244 | mongo.request.keepAlive(resourceID); 245 | expect(requests.length).toBe(1); 246 | var req = requests[0]; 247 | expect(req.method).toBe('POST'); 248 | expect(req.url).toBe(expectedURL); 249 | expect(req.requestBody).toBe(null); 250 | // There is nothing to test for if the request succeeds or not. 251 | }); 252 | }); 253 | }); 254 | -------------------------------------------------------------------------------- /frontend/spec/mws/util.spec.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /* jshint loopfunc: true */ 17 | /* global afterEach, beforeEach, describe, expect, it, jasmine, mongo */ 18 | var console; // Stubbed later. 19 | describe('The util module', function () { 20 | var KeyValProto = function () {}; 21 | KeyValProto.prototype.key = 'val'; 22 | 23 | describe('when providing console protection', function () { 24 | var CONSOLE_EXPANDED_FUNC = ['debug', 'error', 'info', 'warn']; 25 | var consoleStore; 26 | 27 | beforeEach(function () { 28 | consoleStore = console; 29 | }); 30 | 31 | afterEach(function () { 32 | console = consoleStore; 33 | consoleStore = null; 34 | }); 35 | 36 | it('does nothing if all of the console methods exist', function () { 37 | console = {log: function () {} }; 38 | CONSOLE_EXPANDED_FUNC.forEach(function (key) { 39 | console[key] = function () {}; 40 | }); 41 | var old = console; 42 | mongo.util.enableConsoleProtection(); 43 | expect(old).toEqual(console); 44 | }); 45 | 46 | it('sets the expanded methods to log if one does not exist', function () { 47 | var logFunc = function () { return 'aoeu'; }; 48 | var expected = {log: logFunc}; 49 | CONSOLE_EXPANDED_FUNC.forEach(function (key) { 50 | expected[key] = logFunc; 51 | }); 52 | 53 | for (var i = 0; i < CONSOLE_EXPANDED_FUNC.length; i++) { 54 | // Setup: Reset the console and remove one expanded method from the 55 | // console. 56 | console = {log: logFunc}; 57 | var removeIndex = i; 58 | CONSOLE_EXPANDED_FUNC.forEach(function (key, index) { 59 | if (index === removeIndex) { return; } 60 | console[key] = function () {}; 61 | }); 62 | 63 | mongo.util.enableConsoleProtection(); 64 | expect(console).toEqual(expected); 65 | } 66 | }); 67 | 68 | var expectConsoleKeysToEqualFunction = function () { 69 | expect(console.log).toEqual(jasmine.any(Function)); 70 | CONSOLE_EXPANDED_FUNC.forEach(function (key) { 71 | expect(console[key]).toEqual(jasmine.any(Function)); 72 | }); 73 | }; 74 | 75 | it('sets all methods to a function if log doesn\'t exist', function () { 76 | console = {}; 77 | mongo.util.enableConsoleProtection(); 78 | expectConsoleKeysToEqualFunction(); 79 | }); 80 | 81 | it('sets all methods to a function if console is undefined', function () { 82 | console = undefined; 83 | mongo.util.enableConsoleProtection(); 84 | expectConsoleKeysToEqualFunction(); 85 | }); 86 | }); 87 | 88 | it('determines if a given variable is numeric', function () { 89 | var isNumeric = mongo.util.isNumeric; 90 | // 9007199254740992 is the max value in JavaScript's number type. 91 | var numeric = [-9007199254740992, -4, -1, 0, 1, 4, 9007199254740992]; 92 | numeric.forEach(function (number) { 93 | expect(isNumeric(number)).toBe(true); 94 | }); 95 | 96 | var nonNumeric = [undefined, null, NaN, [], {}, false, true, '0', '1', 97 | 'number', [4], {key: 4}]; 98 | nonNumeric.forEach(function (number) { 99 | expect(isNumeric(number)).toBe(false); 100 | }); 101 | }); 102 | 103 | it('creates a resource URL from the given parameters', function () { 104 | var configStore = mongo.config; 105 | var gru = mongo.util.getDBCollectionResURL; 106 | mongo.config = {baseUrl: '/kpop/'}; 107 | expect(gru('iu', 'jjang')).toBe('/kpop/iu/db/jjang/'); 108 | mongo.config = {baseUrl: 'mws/'}; 109 | expect(gru(30, 2)).toBe('mws/30/db/2/'); 110 | expect(gru(null)).toBe('mws/null/db/undefined/'); 111 | mongo.config = {baseUrl: 123}; 112 | expect(gru('a', 'b')).toBe('123a/db/b/'); 113 | mongo.config = configStore; 114 | }); 115 | 116 | describe('provides an interface for stringifying objects that', function(){ 117 | it('prints nonobjects', function(){ 118 | [ 119 | ['mongo', 'mongo'], 120 | [123, '123'], 121 | [false, 'false'], 122 | [true, 'true'] 123 | ].forEach(function(e){ 124 | expect(mongo.util.toString(e[0])).toEqual(e[1]); 125 | }); 126 | }); 127 | 128 | it('prints stringified objects', function(){ 129 | [ 130 | [{}, '{ }'], 131 | [{name: 'mongo'}, '{ "name" : "mongo" }'], 132 | [{parent: {nested: {key: 'val'}}}, '{ "parent" : { "nested" : { "key" : "val" } } }'] 133 | ].forEach(function(e){ 134 | expect(mongo.util.toString(e[0])).toEqual(e[1]); 135 | }); 136 | }); 137 | 138 | it('it uses the toString for objects for which it is a function', function(){ 139 | function A(){} 140 | A.prototype.toString = function(){ return 'hello!'; }; 141 | var a = new A(); 142 | expect(mongo.util.toString(a)).toEqual('hello!'); 143 | }); 144 | 145 | it('refuses to print circular structures', function(){ 146 | var a = {}; 147 | a.a = a; 148 | expect(mongo.util.toString(a)).toMatch(/^ERROR: /); 149 | }); 150 | 151 | it('works on null and undefined values', function () { 152 | expect(mongo.util.toString(null)).toEqual('null'); 153 | expect(mongo.util.toString(undefined)).toEqual('undefined'); 154 | }); 155 | 156 | it('puts the _id field first', function () { 157 | var str = mongo.util.toString({a: 1, _id: 'foo'}); 158 | var exp = '{ "_id" : "foo", "a" : 1 }'; 159 | expect(str).toEqual(exp); 160 | }); 161 | 162 | it('prints object ids properly', function () { 163 | var original = [ 164 | {_id: {$oid: 'abcdef010123456789abcdef'}}, 165 | {a: {b: {$oid: '0123456789abcdef01234567'}}}, 166 | {b: {$oid: '0123456789abcdef0123456'}}, // Too short 167 | {c: {$oid: 12345678901234567890123}}, // Not a string 168 | {d: {$oid: 'abcdef010123456789abcdef', foo: 'bar'}} // Extra keys 169 | ]; 170 | var results = $.map(original, mongo.util.toString); 171 | 172 | var exp = [ 173 | '{ "_id" : ObjectId("abcdef010123456789abcdef") }', 174 | '{ "a" : { "b" : ObjectId("0123456789abcdef01234567") } }', 175 | '{ "b" : { "$oid" : "0123456789abcdef0123456" } }', 176 | '{ "c" : { "$oid" : 1.2345678901234568e+22 } }', 177 | '{ "d" : { "$oid" : "abcdef010123456789abcdef", "foo" : "bar" } }' 178 | ]; 179 | for (var i = 0; i < results.length; i++) { 180 | expect(results[i]).toEqual(exp[i]); 181 | } 182 | }); 183 | 184 | it('prints nonobjects', function(){ 185 | [ 186 | ['mongo', 'mongo'], 187 | [123, '123'], 188 | [false, 'false'], 189 | [true, 'true'] 190 | ].forEach(function(e){ 191 | expect(mongo.util.toString(e[0])).toEqual(e[1]); 192 | }); 193 | }); 194 | 195 | it('works on null and undefined values', function () { 196 | expect(mongo.util.toString(null)).toEqual('null'); 197 | expect(mongo.util.toString(undefined)).toEqual('undefined'); 198 | }); 199 | 200 | it('prints arrays', function(){ 201 | var result = mongo.util.toString([1, 2, 'red', 'blue']); 202 | expect(result).toEqual('[ 1, 2, "red", "blue" ]'); 203 | }); 204 | 205 | it('pretty prints long outputs', function () { 206 | // More than 80 char output string 207 | var original = { 208 | 'this is a very long key': 'this is a very long value', 209 | 'this is also a very long key': 'this is also a very long value' 210 | }; 211 | var expected = '{\n' + 212 | '\t"this is a very long key" : "this is a very long value",\n' + 213 | '\t"this is also a very long key" : "this is also a very long value"\n' + 214 | '}'; 215 | expect(mongo.util.toString(original)).toEqual(expected); 216 | }); 217 | }); 218 | 219 | describe('member getter', function () { 220 | var obj; 221 | beforeEach(function () { 222 | obj = { 223 | foo: 'test', 224 | __methodMissing: jasmine.createSpy('method missing') 225 | }; 226 | }); 227 | 228 | it('returns existing fields', function () { 229 | expect(mongo.util.__get(obj, 'foo')).toEqual('test'); 230 | }); 231 | 232 | it('binds returned functions properly', function () { 233 | var obj = { 234 | count: 0, 235 | incr: function () { 236 | this.count++; 237 | } 238 | }; 239 | expect(obj.count).toEqual(0); 240 | mongo.util.__get(obj, 'incr')(); 241 | expect(obj.count).toEqual(1); 242 | }); 243 | 244 | it('returns undefined for non-existent fields if there is no method missing', function () { 245 | delete obj.__methodMissing; 246 | expect(mongo.util.__get(obj, 'bar')).toBeUndefined(); 247 | }); 248 | 249 | it('calls method missing, if defined, for non-existent fields', function () { 250 | mongo.util.__get(obj, 'foo'); 251 | expect(obj.__methodMissing).not.toHaveBeenCalled(); 252 | mongo.util.__get(obj, 'bar'); 253 | expect(obj.__methodMissing).toHaveBeenCalledWith('bar'); 254 | }); 255 | }); 256 | }); 257 | -------------------------------------------------------------------------------- /frontend/src/head.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /* exported mongo */ 17 | var mongo = { 18 | config: null, 19 | shells: {} // {shellID: mongo.Shell} 20 | }; 21 | -------------------------------------------------------------------------------- /frontend/src/lib/sinon/sinon.js: -------------------------------------------------------------------------------- 1 | ../../../node_modules/sinon/pkg/sinon.js -------------------------------------------------------------------------------- /frontend/src/mws/Coll.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /* jshint camelcase: false */ 17 | /* global mongo, console */ 18 | mongo.Coll = function (db, name) { 19 | if (name.length > 80){ 20 | throw new mongo.CollectionNameError('Collection name must be 80 characters or less'); 21 | } 22 | 23 | if (name.match(/(\$|\0)/)){ 24 | throw new mongo.CollectionNameError('Collection name may not contain $ or \\0'); 25 | } 26 | 27 | if (name.match(/^system\./)){ 28 | throw new mongo.CollectionNameError('Collection name may not begin with system.*'); 29 | } 30 | 31 | if (name === ''){ 32 | throw new mongo.CollectionNameError('Collection name may not be empty'); 33 | } 34 | 35 | this.name = name; 36 | this.db = db; 37 | this.shell = db.shell; 38 | this.urlBase = mongo.util.getDBCollectionResURL(db.shell.mwsResourceID, name); 39 | }; 40 | 41 | mongo.Coll.prototype.toString = function () { 42 | return this.db.toString() + '.' + this.name; 43 | }; 44 | 45 | 46 | /** 47 | * Prepare a cursor iterator that implements next() in a way the shell can 48 | * utilize. 49 | */ 50 | mongo.Coll.prototype.find = function (query, projection) { 51 | var url = this.urlBase + 'find'; 52 | var shell = this.shell; 53 | query = query || null; 54 | projection = projection || null; 55 | var cursor; 56 | 57 | mongo.events.functionTrigger(this.shell, 'db.collection.find', arguments, 58 | {collection: this.name}); 59 | params = { 60 | query: query, 61 | projection: projection 62 | }; 63 | 64 | function findRequest(onSuccess, extraParams){ 65 | $.extend(params, extraParams); 66 | mongo.request.makeRequest(url, params, 'GET', 'dbCollectionFind', shell, 67 | onSuccess); 68 | } 69 | 70 | cursor = new mongo.Cursor(this, findRequest); 71 | return cursor; 72 | }; 73 | 74 | mongo.Coll.prototype.findOne = function (query, projection) { 75 | var self = this; 76 | var url = this.urlBase + 'find_one'; 77 | var shell = this.shell; 78 | query = query || null; 79 | projection = projection || null; 80 | var params = { 81 | query: query, 82 | projection: projection 83 | }; 84 | 85 | 86 | mongo.events.functionTrigger(this.shell, 'db.collection.findOne', arguments, 87 | {collection: this.name}); 88 | var context = shell.evaluator.pause(); 89 | mongo.request.makeRequest(url, params, 'GET', 'dbCollectionFindOne', shell, 90 | function(data){ 91 | shell.evaluator.resume(context, data); 92 | }); 93 | }; 94 | 95 | mongo.Coll.prototype.count = function (query) { 96 | var self = this; 97 | var url = this.urlBase + 'count'; 98 | var shell = this.shell; 99 | query = query || null; 100 | var params = {query: query}; 101 | mongo.events.functionTrigger(shell, 'db.collection.count', arguments, 102 | {collection: this.name}); 103 | var context = shell.evaluator.pause(); 104 | mongo.request.makeRequest(url, params,'GET', 'dbCollectionCount', shell, 105 | function(data){ 106 | shell.evaluator.resume(context, data); 107 | }); 108 | }; 109 | 110 | mongo.Coll.prototype.insert = function (doc) { 111 | var url = this.urlBase + 'insert'; 112 | var params = {document: doc}; 113 | var shell = this.shell; 114 | mongo.events.functionTrigger(this.shell, 'db.collection.insert', arguments, 115 | {collection: this.name}); 116 | var context = shell.evaluator.pause(); 117 | tst = mongo.request.makeRequest(url, params, 'POST', 'dbCollectionInsert', shell, 118 | function(data){ 119 | shell.evaluator.resume(context, data['pretty']); 120 | }); 121 | }; 122 | 123 | mongo.Coll.prototype.save = function (doc) { 124 | var url = this.urlBase + 'save'; 125 | var params = {document: doc}; 126 | var shell = this.shell; 127 | mongo.events.functionTrigger(this.shell, 'db.collection.save', arguments, 128 | {collection: this.name}); 129 | var context = shell.evaluator.pause(); 130 | mongo.request.makeRequest(url, params, 'POST', 'dbCollectionSave', shell, 131 | function(data){ 132 | shell.evaluator.resume(context, data['pretty']); 133 | }); 134 | }; 135 | 136 | /** 137 | * Makes a remove request to the mongod instance on the backing server. On 138 | * success, the item(s) are removed from the collection, otherwise a failure 139 | * message is printed and an error is thrown. 140 | */ 141 | mongo.Coll.prototype.remove = function (constraint, options) { 142 | var shell = this.shell; 143 | if (typeof(options) !== "object"){ 144 | var justOne = options 145 | options = {'justOne': !!justOne} 146 | } 147 | if (!constraint) { 148 | shell.insertError("remove needs a query"); 149 | return; 150 | } 151 | var url = this.urlBase + 'remove'; 152 | var params = {constraint: constraint, options: options}; 153 | mongo.events.functionTrigger(this.shell, 'db.collection.remove', arguments, 154 | {collection: this.name}); 155 | var context = shell.evaluator.pause(); 156 | mongo.request.makeRequest(url, params, 'DELETE', 'dbCollectionRemove', shell, 157 | function(data){ 158 | shell.evaluator.resume(context, data['pretty']); 159 | }); 160 | }; 161 | 162 | /** 163 | * Makes an update request to the mongod instance on the backing server. On 164 | * success, the item(s) are updated in the collection, otherwise a failure 165 | * message is printed and an error is thrown. 166 | * 167 | * Optionally, an object which specifies whether to perform an upsert and/or 168 | * a multiple update may be used instead of the individual upsert and multi 169 | * parameters. 170 | */ 171 | mongo.Coll.prototype.update = function (query, update, upsert, multi) { 172 | var url = this.urlBase + 'update'; 173 | var shell = this.shell; 174 | mongo.events.functionTrigger(this.shell, 'db.collection.update', arguments, 175 | {collection: this.name}); 176 | 177 | // handle options document for 2.2+ 178 | if (typeof upsert === 'object'){ 179 | if (multi !== undefined){ 180 | var msg = 'Fourth argument must be empty when specifying upsert and multi with an object'; 181 | this.shell.insertError(msg); 182 | throw {message: 'dbCollectionUpdate: Syntax error'}; 183 | } 184 | multi = upsert.multi; 185 | upsert = upsert.upsert; 186 | } 187 | 188 | var params = {query: query, update: update, upsert: !!upsert, multi: !!multi}; 189 | var context = shell.evaluator.pause(); 190 | mongo.request.makeRequest(url, params, 'PUT', 'dbCollectionUpdate', shell, 191 | function(data){ 192 | shell.evaluator.resume(context, data['pretty']); 193 | }); 194 | }; 195 | 196 | /** 197 | * Makes a drop request to the mongod instance on the backing server. On 198 | * success, the collection is dropped from the database, otherwise a failure 199 | * message is printed and an error is thrown. 200 | */ 201 | mongo.Coll.prototype.drop = function () { 202 | var url = this.urlBase + 'drop'; 203 | var shell = this.shell; 204 | mongo.events.functionTrigger(this.shell, 'db.collection.drop', arguments, 205 | {collection: this.name}); 206 | var context = shell.evaluator.pause(); 207 | mongo.request.makeRequest(url, null, 'DELETE', 'dbCollectionDrop', shell, 208 | function(){ 209 | shell.evaluator.resume(context, true); 210 | }); 211 | }; 212 | 213 | /** 214 | * Makes an aggregation request to the mongod instance on the backing server. 215 | * A cursor object is created, and returned to the shell. 216 | */ 217 | mongo.Coll.prototype.aggregate = function() { 218 | var shell = this.shell; 219 | var query; 220 | if (arguments.length === 1 && $.isArray(arguments[0])) { 221 | query = arguments[0]; 222 | } else { 223 | query = $.makeArray(arguments); 224 | } 225 | var url = this.urlBase + 'aggregate'; 226 | mongo.events.functionTrigger(this.shell, 'db.collection.aggregate', 227 | arguments, {collection: this.name}); 228 | function aggregateRequest(onSuccess){ 229 | mongo.request.makeRequest(url, query, 'GET', 'dbCollectionAggregate', shell, onSuccess); 230 | } 231 | 232 | cursor = new mongo.Cursor(this, aggregateRequest); 233 | return cursor; 234 | }; 235 | 236 | mongo.Coll.prototype.__methodMissing = function (field) { 237 | var unimplemented = { 238 | 'createIndex': 0, 239 | 'copyTo': 0, 240 | 'distinct': 0, 241 | 'dropIndex': 0, 242 | 'dropIndexes': 0, 243 | 'ensureIndex': 0, 244 | 'findAndModify': 0, 245 | 'getIndexes': 0, 246 | 'getShardDistribution': 0, 247 | 'getShardVersion': 0, 248 | 'group': 0, 249 | 'isCapped': 0, 250 | 'mapReduce': 0, 251 | 'reIndex': 0, 252 | 'renameCollection': 0, 253 | 'stats': 0, 254 | 'storageSize': 0, 255 | 'totalSize': 0, 256 | 'totalIndexSize': 0, 257 | 'validate': 0 258 | }; 259 | var msg; 260 | 261 | if (unimplemented.hasOwnProperty(field)) { 262 | msg = ' is not implemented.'; 263 | } else { 264 | msg = ' is not a function on collections.'; 265 | } 266 | this.shell.insertError(field + msg); 267 | return mongo.util.noOp; 268 | }; 269 | -------------------------------------------------------------------------------- /frontend/src/mws/CollectionNameError.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /* global mongo */ 17 | 18 | mongo.CollectionNameError = function(message) { 19 | this.name = 'CollectionNameError'; 20 | this.message = message || 'Invalid collection name'; 21 | }; 22 | mongo.CollectionNameError.prototype = new Error(); 23 | mongo.CollectionNameError.prototype.constructor = mongo.CollectionNameError; 24 | -------------------------------------------------------------------------------- /frontend/src/mws/Cursor.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /* global console, mongo, Error */ 17 | mongo.Cursor = function (collection, query) { 18 | this._coll = collection; 19 | this._shell = collection.shell; 20 | this.urlBase = mongo.util.getDBCollectionResURL(this._shell.mwsResourceID, collection.name); 21 | this._query = query; 22 | this._executed = false; 23 | this._result = []; 24 | this._retrieved = 0; 25 | this._count = 0; 26 | this._batchSize = collection.shell.getShellBatchSize(); 27 | }; 28 | 29 | 30 | /** 31 | * Execute either the passed in function, or the getMore function, calling the 32 | * success callback in either case. 33 | */ 34 | mongo.Cursor.prototype._executeQuery = function (onSuccess) { 35 | var wrappedSuccess = function (data) { 36 | if (data) { 37 | mongo.events.callbackTrigger(this._shell, 'cursor.execute', data.result.slice()); 38 | this._storeQueryResult(data.result); 39 | this._cursorId = data.cursor_id || this._cursorId; 40 | this._count = data.count || this._count; 41 | this._retrieved += data.result.length; 42 | this._hasNext = this._retrieved < this._count; 43 | } 44 | if (onSuccess) { 45 | onSuccess(); 46 | } 47 | }.bind(this); 48 | 49 | if (!this._executed) { 50 | var extraParams = {}; 51 | extraParams['sort'] = this._sort; 52 | extraParams['limit'] = this._limit; 53 | extraParams['skip'] = this._skip; 54 | this._query(wrappedSuccess, extraParams); 55 | this._executed = true; 56 | } else { 57 | this._getMore(wrappedSuccess); 58 | } 59 | }; 60 | 61 | 62 | mongo.Cursor.prototype._getMore = function (callback) { 63 | if ((!this._executed || this._retrieved < this._count) && 64 | (this._result.length === 0)) { 65 | var url = this.urlBase + "next"; 66 | var params = {}; 67 | params['cursor_id'] = this._cursorId; 68 | params['retrieved'] = this._retrieved; 69 | mongo.request.makeRequest(url, params, 'GET', 'CursorGetMore', this._shell, 70 | callback); 71 | } else if (callback) { 72 | callback(); 73 | } 74 | } 75 | 76 | 77 | mongo.Cursor.prototype._printBatch = function () { 78 | var self = this; 79 | 80 | function printBatch() { 81 | self._shell.lastUsedCursor = self; 82 | var n = 0; 83 | function recursiveFetchLoop(){ 84 | self.next(function(next){ 85 | self._shell.insertResponseLine(next); 86 | n++; 87 | if (n < self._batchSize) { 88 | if (self.hasNext()) { 89 | recursiveFetchLoop(); 90 | } 91 | } else { 92 | self._shell.insertResponseLine('Type "it" for more'); 93 | } 94 | }); 95 | } 96 | recursiveFetchLoop(); 97 | } 98 | if (!this._executed){ 99 | var context = this._shell.evaluator.pause(); 100 | this._executeQuery(function(){ 101 | printBatch() 102 | self._shell.evaluator.resume(context); 103 | }); 104 | } else { 105 | printBatch(); 106 | } 107 | }; 108 | 109 | 110 | mongo.Cursor.prototype._storeQueryResult = function (result) { 111 | // For efficiency, we reverse the result. This allows us to pop() as we 112 | // iterate over the result set, both freeing the reference and preventing a 113 | // reindexing on each removal from the array as with unshift/splice(). 114 | 115 | // We add this on after any previously received results in preparation for 116 | // receiving results in batches. 117 | this._result = result.reverse().concat(this._result); 118 | }; 119 | 120 | 121 | /** 122 | * If a query has been executed from this cursor, prints an error message and 123 | * returns true. Otherwise returns false. 124 | */ 125 | mongo.Cursor.prototype._warnIfExecuted = function (methodName) { 126 | if (this._executed) { 127 | this._shell.insertResponseLine('Warning: Cannot call ' + methodName + 128 | ' on already executed mongo.Cursor.' + this); 129 | console.warn('Cannot call', methodName, 'on already executed ' + 130 | 'mongo.Cursor.', this); 131 | } 132 | return this._executed; 133 | }; 134 | 135 | 136 | /** 137 | * If a query has been executed from this cursor, throw an Error. Otherwise 138 | * returns false. 139 | */ 140 | mongo.Cursor.prototype._ensureNotExecuted = function (methodName) { 141 | if (this._executed) { 142 | throw new Error('Cannot ' + methodName + ' results after query has been executed.'); 143 | } 144 | }; 145 | 146 | 147 | mongo.Cursor.prototype.hasNext = function () { 148 | return (this._result.length > 0 || this._hasNext); 149 | }; 150 | 151 | 152 | mongo.Cursor.prototype.next = function (callback) { 153 | if (this._result.length > 0) { 154 | callback(this._result.pop()); 155 | return; 156 | } 157 | var context = this._shell.evaluator.pause(); 158 | this._executeQuery(function () { 159 | var next, isError; 160 | if (this._result.length === 0) { 161 | next = new Error('Cursor does not have any more elements.'); 162 | isError = true; 163 | this._shell.lastUsedCursor = null; 164 | /* 165 | If count is a multiple of batchsize, send another executeQuery 166 | to clean up a lingering cursor 167 | */ 168 | if(this._count % this._batchSize === 0){ 169 | this._executeQuery(); 170 | } 171 | } else { 172 | next = this._result.pop(); 173 | isError = false; 174 | } 175 | if (callback && !isError) { 176 | callback(next); 177 | } 178 | this._shell.evaluator.resume(context, next, isError); 179 | }.bind(this)); 180 | }; 181 | 182 | 183 | mongo.Cursor.prototype.sort = function (sort) { 184 | this._ensureNotExecuted('sort'); 185 | if (!$.isPlainObject(sort)){ 186 | throw new Error('Sort must be an object'); 187 | } 188 | this._sort = sort; 189 | console.debug('mongo.Cursor would be sorted with', sort, this); 190 | return this; 191 | }; 192 | 193 | 194 | mongo.Cursor.prototype.skip = function (skip) { 195 | this._ensureNotExecuted('skip'); 196 | if (!mongo.util.isInteger(skip)) { 197 | throw new Error('Skip amount must be an integer.'); 198 | } 199 | this._skip = skip; 200 | return this; 201 | }; 202 | 203 | 204 | mongo.Cursor.prototype.limit = function (limit) { 205 | this._ensureNotExecuted('limit'); 206 | if (!mongo.util.isInteger(limit)) { 207 | throw new Error('Limit amount must be an integer.'); 208 | } 209 | this._limit = limit; 210 | return this; 211 | }; 212 | 213 | 214 | mongo.Cursor.prototype.batchSize = function () { 215 | throw new Error('batchSize() is disallowed in the web shell'); 216 | }; 217 | 218 | 219 | mongo.Cursor.prototype.toArray = function (callback) { 220 | var result = []; 221 | while(this.hasNext()){ 222 | result.push(this.next()); 223 | } 224 | return result; 225 | }; 226 | 227 | 228 | mongo.Cursor.prototype.count = function (useSkipLimit) { 229 | useSkipLimit = !!useSkipLimit; 230 | // If the cursor already has a count, use that. 231 | if (this._count) { return this._count; } 232 | 233 | // Otherwise, execute the cursor, and get the count. 234 | var context = this._shell.evaluator.pause(); 235 | this._executeQuery(function () { 236 | this._shell.evaluator.resume(context, this._count, false); 237 | }.bind(this)); 238 | }; 239 | 240 | 241 | mongo.Cursor.prototype.size = function () { 242 | return this.count(true); 243 | }; 244 | 245 | 246 | mongo.Cursor.prototype.toString = function () { 247 | var query = this._query || {}; 248 | return 'Cursor: ' + this._coll.toString() + ' -> ' + mongo.jsonUtils.tojson(query); 249 | }; 250 | 251 | 252 | mongo.Cursor.prototype.__methodMissing = function (field) { 253 | if (mongo.util.isInteger(field)) { 254 | var context = this._shell.evaluator.pause(); 255 | this.toArray(function (arr) { 256 | this._shell.evaluator.resume(context, arr[field]); 257 | }.bind(this)); 258 | } 259 | }; 260 | -------------------------------------------------------------------------------- /frontend/src/mws/Readline.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /* global mongo */ 17 | mongo.Readline = function (codemirror, submitFunction) { 18 | this.inputBox = codemirror; 19 | this.submitFunction = submitFunction; 20 | if (localStorage){ 21 | this.history = localStorage[mongo.config.shellHistoryKey]; 22 | } 23 | this.history = this.history ? JSON.parse(this.history) : []; // Newest entries at Array.length 24 | this.historyIndex = this.history.length; 25 | this.historyFirstCommand = ''; 26 | 27 | this.inputBox.on('keydown', function (codemirror, event) {this.keydown(event);}.bind(this)); 28 | }; 29 | 30 | mongo.Readline.prototype.keydown = function (event) { 31 | var key = mongo.config.keycodes; 32 | var line; 33 | switch (event.keyCode) { 34 | case key.up: 35 | line = this.getOlderHistoryEntry(); 36 | break; 37 | case key.down: 38 | line = this.getNewerHistoryEntry(); 39 | break; 40 | case key.enter: 41 | this.submit(this.inputBox.getValue()); 42 | break; 43 | default: 44 | return; 45 | } 46 | 47 | if (line !== undefined && line !== null) { 48 | this.inputBox.setValue(line); 49 | this.moveCursorToEnd(); 50 | } 51 | if (event.preventDefault) { 52 | event.preventDefault(); 53 | } else { 54 | // IE8 55 | event.returnValue = false; 56 | } 57 | }; 58 | 59 | /** 60 | * Returns a more recent line from the stored command history. The most recent 61 | * line returned is the empty string and after that is returned, subsequent 62 | * calls to this method without resetting or traversing the history will return 63 | * undefined. A call to this method when the history is empty will return 64 | * undefined. 65 | */ 66 | mongo.Readline.prototype.getNewerHistoryEntry = function () { 67 | if (this.history.length === 0) { return undefined; } 68 | 69 | var old = this.historyIndex; 70 | this.historyIndex = Math.min(this.historyIndex + 1, this.history.length); 71 | if (this.historyIndex === this.history.length) { 72 | if (old !== this.historyIndex) { 73 | return this.historyFirstCommand; 74 | } 75 | return undefined; 76 | } 77 | return this.history[this.historyIndex]; 78 | }; 79 | 80 | /** 81 | * Returns a less recent line from the stored command history. If the least 82 | * recent command is returned, subsequent calls to this method without 83 | * resetting or traversing the history will return this same command. A call to 84 | * this method when the history is empty will return undefined. 85 | */ 86 | mongo.Readline.prototype.getOlderHistoryEntry = function () { 87 | if (this.history.length === 0) { return undefined; } 88 | 89 | if (this.historyIndex === this.history.length) { 90 | this.historyFirstCommand = this.inputBox.getValue(); 91 | } 92 | 93 | this.historyIndex = Math.max(this.historyIndex - 1, 0); 94 | return this.history[this.historyIndex]; 95 | }; 96 | 97 | /** 98 | * Stores the given line to the command history and resets the history index. 99 | */ 100 | mongo.Readline.prototype.submit = function (line) { 101 | 102 | this.history.push(line); 103 | 104 | if (localStorage){ 105 | var history = localStorage[mongo.config.shellHistoryKey]; 106 | history = history ? JSON.parse(history) : []; 107 | history.push(line); 108 | if (history.length > mongo.config.shellHistorySize){ 109 | history.shift(); 110 | } 111 | localStorage[mongo.config.shellHistoryKey] = JSON.stringify(history); 112 | } 113 | 114 | this.historyIndex = this.history.length; 115 | this.submitFunction(); 116 | }; 117 | 118 | mongo.Readline.prototype.moveCursorToEnd = function() { 119 | var lastLine = this.inputBox.lineCount() - 1; 120 | var lastChar = this.inputBox.getLine(lastLine).length - 1; 121 | this.inputBox.setCursor({ 122 | line: lastLine, 123 | pos: lastChar 124 | }); 125 | }; 126 | 127 | mongo.Readline.prototype.getLastCommand = function(){ 128 | // By the time our code is able to call this function, we will already have 129 | // added the current command to the history, which we want to ignore. 130 | return this.history[this.history.length - 2]; 131 | }; 132 | -------------------------------------------------------------------------------- /frontend/src/mws/Shell.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /* jshint evil: true, newcap: false */ 17 | /* global console, mongo, CodeMirror, Evaluator */ 18 | mongo.Shell = function (rootElement, shellID) { 19 | this.$rootElement = $(rootElement); 20 | 21 | this.hasShownResponse = false; 22 | this.id = shellID; 23 | this.mwsResourceID = null; 24 | this.readline = null; 25 | this.lastUsedCursor = null; 26 | this.shellBatchSize = mongo.config.shellBatchSize; 27 | this.db = new mongo.DB(this, 'test'); 28 | 29 | this.injectHTML(); 30 | this.attachClickListener(); 31 | }; 32 | 33 | mongo.Shell.prototype.injectHTML = function () { 34 | // TODO: Use client-side templating instead. 35 | this.$rootElement.addClass('cm-s-solarized').addClass('cm-s-dark'); 36 | this.$rootElement.html( 37 | '
' + 38 | // We're injecting into
. The previous HTML 39 | // content is used to fill the shell. 40 | this.$rootElement.html() + 41 | '
' + 42 | '
' + 43 | '
>
' + 44 | '
' + 45 | '
' + 46 | '
' 47 | ); 48 | this.$responseWrapper = this.$rootElement.find('.mws-responses'); 49 | this.responseBlock = CodeMirror(this.$responseWrapper.get(0), { 50 | readOnly: true, 51 | lineWrapping: true, 52 | theme: 'solarized dark' 53 | }); 54 | // We want the response box to be hidden until there is a response to show 55 | // (it gets shown in insertResponseLine). 56 | this.$responseWrapper.css({display: 'none'}); 57 | 58 | this.inputBox = CodeMirror(this.$rootElement.find('.mws-input').get(0), { 59 | matchBrackets: true, 60 | lineWrapping: true, 61 | readOnly: 'nocursor', 62 | theme: 'solarized dark' 63 | }); 64 | $(this.inputBox.getWrapperElement()).css({background: 'transparent'}); 65 | 66 | // Start with prompt hidden 67 | this.$inputPrompt = this.$rootElement.find('.mws-prompt').hide(); 68 | 69 | this.$inputWrapper = this.$rootElement.find('.mws-input-wrapper'); 70 | this.$scrollWrapper = this.$rootElement.find('.mws-scroll-wrapper'); 71 | 72 | // Todo: We should whitelist what is available in this namespace 73 | // e.g. get rid of parent 74 | this.evaluator = new Evaluator(); 75 | 76 | this.evaluator.setGlobal('print', function(){ 77 | this.insertResponseLine($.makeArray(arguments).map(function(e){ 78 | return mongo.util.toString(e); 79 | }).join(' ')); 80 | }.bind(this)); 81 | this.evaluator.setGlobal('ObjectId', function(oid) { 82 | return {'$oid': oid}; 83 | }); 84 | this.evaluator.setGlobal('__get', mongo.util.__get); 85 | this.evaluator.setGlobal('db', this.db); 86 | }; 87 | 88 | mongo.Shell.prototype.clear = function () { 89 | this.responseBlock.setValue(""); 90 | this.hasShownResponse = false; 91 | }; 92 | 93 | mongo.Shell.prototype.attachClickListener = function () { 94 | this.$rootElement.click(function () { 95 | this.inputBox.focus(); 96 | this.inputBox.refresh(); 97 | this.responseBlock.setSelection({line: 0, ch: 0}); 98 | }.bind(this)); 99 | }; 100 | 101 | mongo.Shell.prototype.attachInputHandler = function (mwsResourceID) { 102 | this.mwsResourceID = mwsResourceID; 103 | this.readline = new mongo.Readline(this.inputBox, this.handleInput.bind(this)); 104 | this.enableInput(true); 105 | }; 106 | 107 | /** 108 | * Retrieves the input from the mongo web shell, evaluates it, handles the 109 | * responses (indirectly via callbacks), and clears the input field. 110 | */ 111 | mongo.Shell.prototype.handleInput = function () { 112 | var userInput = this.inputBox.getValue(); 113 | if(userInput.trim === ''){ 114 | this.insertResponseLine('>'); 115 | } 116 | this.insertResponseLine(userInput, '> '); 117 | this.inputBox.setValue(''); 118 | if (!mongo.keyword.handleKeywords(this, userInput)) { 119 | try { 120 | var mutatedSrc = mongo.mutateSource.swapMemberAccesses(userInput); 121 | this.eval(mutatedSrc); 122 | } catch (err) { 123 | this.insertError(err); 124 | } 125 | } 126 | }; 127 | 128 | /** 129 | * Calls eval on the given array of javascript statements. This method will 130 | * throw any exceptions eval throws. 131 | */ 132 | mongo.Shell.prototype.eval = function (src) { 133 | this.evaluator.eval(src, function (out, isError) { 134 | if (isError) { 135 | this.insertError(out); 136 | } else { 137 | if (out instanceof mongo.Cursor) { 138 | out._printBatch(); 139 | } else if(out != undefined) { 140 | this.insertResponseLine(out); 141 | } 142 | } 143 | }.bind(this)); 144 | }; 145 | 146 | mongo.Shell.prototype.enableInput = function (bool) { 147 | var readOnly = bool ? false : 'nocursor'; 148 | this.inputBox.setOption('readOnly', readOnly); 149 | if (bool) { 150 | this.$inputPrompt.show(); 151 | } else { 152 | this.$inputPrompt.hide(); 153 | } 154 | }; 155 | 156 | mongo.Shell.prototype.focus = function() { 157 | this.inputBox.focus(); 158 | }; 159 | 160 | mongo.Shell.prototype.insertResponseArray = function (data) { 161 | for (var i = 0; i < data.length; i++) { 162 | this.insertResponseLine(data[i], null, true); 163 | } 164 | this.responseBlock.refresh(); 165 | }; 166 | 167 | mongo.Shell.prototype.insertResponseLine = function (data, prepend, noRefresh) { 168 | var lastLine = this.responseBlock.lineCount() - 1; 169 | var lastChar = this.responseBlock.getLine(lastLine).length; 170 | var lastPos = {line: lastLine, ch: lastChar}; 171 | var isString = typeof(data) === 'string'; 172 | var separator = this.hasShownResponse ? '\n' : ''; 173 | 174 | data = mongo.util.toString(data); 175 | if (prepend) { 176 | data = prepend + data; 177 | var padding = Array(prepend.length + 1).join(' '); 178 | data = data.replace(/\n/g, '\n' + padding); 179 | } 180 | this.responseBlock.replaceRange(separator + data, lastPos); 181 | 182 | if (isString && !prepend) { 183 | var newLines = data.match(/\n/g); 184 | var insertedLines = newLines ? newLines.length + 1 : 1; 185 | var totalLines = this.responseBlock.lineCount(); 186 | var startInsertedResponse = totalLines - insertedLines; 187 | for (var i = startInsertedResponse; i < totalLines; i++) { 188 | this.responseBlock.addLineClass(i, 'text', 'mws-cm-plain-text'); 189 | } 190 | } 191 | 192 | this.hasShownResponse = true; 193 | this.$responseWrapper.css({display: ''}); 194 | this.$inputWrapper.css({marginTop: '-8px'}); 195 | 196 | if (!noRefresh) { 197 | this.responseBlock.refresh(); 198 | } 199 | // Reset scroll distance so the input is not hidden at the bottom. 200 | this.$scrollWrapper.scrollTop(this.$scrollWrapper.get(0).scrollHeight); 201 | }; 202 | 203 | mongo.Shell.prototype.insertError = function (err) { 204 | if (err instanceof Error || err instanceof this.evaluator.getGlobal('Error')) { 205 | err = err.toString(); 206 | } else if (err.message) { 207 | err = 'ERROR: ' + err.message; 208 | } else { 209 | err = 'ERROR: ' + err; 210 | } 211 | this.insertResponseLine(err); 212 | }; 213 | 214 | /** 215 | * Returns the shellBatchSize from the shell's local vars if it's valid, 216 | * otherwise throws an error. 217 | */ 218 | mongo.Shell.prototype.getShellBatchSize = function () { 219 | var size = this.shellBatchSize; 220 | if (!mongo.util.isNumeric(size)) { 221 | this.insertResponseLine('ERROR: Please set ' + 222 | 'DBQuery.shellBatchSize to a valid numerical value.'); 223 | console.debug('Please set DBQuery.shellBatchSize to a valid numerical ' + 224 | 'value.'); 225 | // TODO: Make the error throwing more robust. 226 | throw 'Bad shell batch size.'; 227 | } 228 | return size; 229 | }; 230 | -------------------------------------------------------------------------------- /frontend/src/mws/config.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /* global mongo */ 17 | mongo.config = (function () { 18 | var mwsHost = window.MWS_HOST || ''; 19 | var KEYCODES = { 20 | enter: 13, 21 | left: 37, 22 | up: 38, 23 | right: 39, 24 | down: 40 25 | }; 26 | 27 | return { 28 | keycodes: KEYCODES, 29 | keepAliveTime: 30000, 30 | rootElementSelector: '.mongo-web-shell', 31 | scriptName: 'mongoWebShell', 32 | shellBatchSize: 20, 33 | shellHistoryKey: 'mongo.history', 34 | shellHistorySize: 500, 35 | mwsHost: mwsHost, 36 | baseUrl: mwsHost + '/server/mws/' 37 | }; 38 | }()); 39 | -------------------------------------------------------------------------------- /frontend/src/mws/db.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /* global mongo, console */ 17 | /* jshint unused: false */ 18 | mongo.DB = function (shell, name) { 19 | this.name = name; 20 | this.shell = shell; 21 | }; 22 | 23 | mongo.DB.prototype.toString = function () { 24 | return this.name; 25 | }; 26 | 27 | mongo.DB.prototype.listCollections = function () { 28 | mongo.keyword.show(this.shell, ['collections']); 29 | }; 30 | 31 | mongo.DB.prototype.getName = function() { 32 | return this.name; 33 | }; 34 | 35 | mongo.DB.prototype.__methodMissing = function (field) { 36 | var unsupported = { 37 | 'addUser': 0, 38 | 'adminCommand': 0, 39 | 'auth': 0, 40 | 'cloneDatabase': 0, 41 | 'commandHelp': 0, 42 | 'copyDatabase': 0, 43 | 'createCollection': 0, 44 | 'currentOp': 0, 45 | 'dropDatabase': 0, 46 | 'eval': 0, 47 | 'fsyncLock': 0, 48 | 'fsyncUnlock': 0, 49 | 'getCollection': 0, 50 | 'getLastError': 0, 51 | 'getLastErrorObj': 0, 52 | 'getMongo': 0, 53 | 'getPrevError': 0, 54 | 'getProfilingLevel': 0, 55 | 'getProfilingStatus': 0, 56 | 'getReplicationInfo': 0, 57 | 'getSiblingDB': 0, 58 | 'hostInfo': 0, 59 | 'isMaster': 0, 60 | 'killOp': 0, 61 | 'listCommands': 0, 62 | 'loadServerScripts': 0, 63 | 'logout': 0, 64 | 'printCollectionStats': 0, 65 | 'printReplicationInfo': 0, 66 | 'printShardingStatus': 0, 67 | 'printSlaveReplicationInfo': 0, 68 | 'removeUser': 0, 69 | 'repairDatabase': 0, 70 | 'resetError': 0, 71 | 'runCommand': 0, 72 | 'serverStatus': 0, 73 | 'setProfilingLevel': 0, 74 | 'setVerboseShell': 0, 75 | 'shutdownServer': 0, 76 | 'stats': 0, 77 | 'version': 0 78 | }; 79 | 80 | if (unsupported.hasOwnProperty(field)) { 81 | this.shell.insertError('The web shell does not support db.'+ field + '()'); 82 | return mongo.util.noOp; 83 | } else { 84 | this[field] = new mongo.Coll(this, field); 85 | return this[field]; 86 | } 87 | }; 88 | 89 | mongo.DB.prototype.getCollectionNames = function (callback) { 90 | var url = mongo.util.getDBResURL(this.shell.mwsResourceID) + 'getCollectionNames'; 91 | mongo.request.makeRequest(url, undefined, 'GET', 'getCollectionNames', this.shell, callback); 92 | }; -------------------------------------------------------------------------------- /frontend/src/mws/events.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /* global mongo, console */ 17 | /* jshint noarg: false */ 18 | mongo.events = (function(){ 19 | var trigger = function(shell, event, data){ 20 | data = $.extend({shell: shell, event: event}, data); 21 | console.info('[' + shell.id + '] ' + event + ' triggered with data ', data); 22 | 23 | var handlers = shell.events; 24 | handlers = handlers && handlers[event]; 25 | if (handlers){ 26 | $.each(handlers, function(id, f){ 27 | f(data); 28 | }); 29 | } 30 | }; 31 | 32 | var functionTrigger = function(shell, event, args, data){ 33 | data = $.extend({'arguments': args}, data); 34 | mongo.events.trigger(shell, event, data); 35 | }; 36 | 37 | var callbackTrigger = function(shell, event, result, data){ 38 | data = $.extend({result: result}, data); 39 | mongo.events.trigger(shell, event, data); 40 | }; 41 | 42 | var bind = function(shell, event, handler, data, filter){ 43 | return $.Deferred(function(deferred){ 44 | if (!shell.events){ 45 | shell.events = {}; 46 | } 47 | 48 | if (!shell.events[event]){ 49 | shell.events[event] = {}; 50 | } 51 | 52 | var wrap = function(d){ 53 | var passedData = $.extend({}, data, d); 54 | if (typeof filter === 'function' && !filter.call(shell, passedData)){ 55 | return; 56 | } 57 | if (typeof handler === 'function'){ 58 | handler.call(shell, passedData); 59 | } 60 | deferred.resolveWith(shell, [passedData]); 61 | }; 62 | 63 | if (typeof handler === 'function'){ 64 | if (!handler.id){ handler.id = ++mongo.events._id; } 65 | shell.events[event][handler.id] = wrap; 66 | } 67 | }).promise(); 68 | }; 69 | 70 | var bindOnce = function(shell, event, handler, data, filter){ 71 | return mongo.events.bind(shell, event, handler, data, filter).done(function(){ 72 | mongo.events.unbind(shell, event, handler); 73 | }); 74 | }; 75 | 76 | var bindAll = function(event, handler, data, filter){ 77 | return mongo.shells.map(function(e){ 78 | return mongo.events.bind(e, event, handler, data, filter); 79 | }); 80 | }; 81 | 82 | var unbind = function(shell, event, handler){ 83 | if (handler){ 84 | if (!handler.id){ 85 | return; // handler was never bound to function 86 | } 87 | delete shell.events[event][handler.id]; 88 | } else { 89 | // unbind all handlers 90 | delete shell.events[event]; 91 | } 92 | }; 93 | 94 | var unbindAll = function(event, handler){ 95 | mongo.shells.forEach(function(e){ 96 | mongo.events.unbind(e, event, handler); 97 | }); 98 | }; 99 | 100 | return { 101 | trigger: trigger, 102 | functionTrigger: functionTrigger, 103 | callbackTrigger: callbackTrigger, 104 | bind: bind, 105 | bindOnce: bindOnce, 106 | bindAll: bindAll, 107 | unbind: unbind, 108 | unbindAll: unbindAll, 109 | _id: 0 110 | }; 111 | })(); 112 | -------------------------------------------------------------------------------- /frontend/src/mws/init.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /* jshint camelcase: false */ 17 | /* global mongo, console */ 18 | /** 19 | * Injects a mongo web shell into the DOM wherever an element of class 20 | * 'mongo-web-shell' can be found. Additionally sets up the resources 21 | * required by the web shell, including the mws REST resource, the mws 22 | * CSS stylesheets, and calls any initialization urls 23 | */ 24 | mongo.init = (function(){ 25 | // This file is getting pretty bogged down in callbacks. I spent the better 26 | // part of a day thinking about how to clean it up and make it more 27 | // maintainable and extensible, but this was the best I could come up with. 28 | // - Danny 29 | var initializationUrls = {}; 30 | 31 | function uniqueArray(original) { 32 | // Taken from http://stackoverflow.com/a/1961068 33 | var seen = {}, unique = []; 34 | for (var i = 0, l = original.length; i < l; ++i) { 35 | if (seen.hasOwnProperty(original[i])) { 36 | continue; 37 | } 38 | unique.push(original[i]); 39 | seen[original[i]] = 1; 40 | } 41 | return unique; 42 | } 43 | 44 | function condenseJsonArray(jsonArray) { 45 | // Each JSON should be a top level object in which the keys are collection 46 | // names which map to an array of documents which are to be inserted into 47 | // the specified collection. 48 | var condensedJson = {}; 49 | $.each(jsonArray, function (i, jsonData) { 50 | $.each(jsonData, function (collection, documents) { 51 | if (!$.isArray(documents)) { 52 | console.error('Json format is incorrect, top level collection ' + 53 | 'name ' + collection + 'does not map to an array: ' + jsonData); 54 | } else { 55 | var oldJson = condensedJson[collection] || []; 56 | condensedJson[collection] = oldJson.concat(documents); 57 | } 58 | }); 59 | }); 60 | return condensedJson; 61 | } 62 | 63 | function ensureAllRequests(ajaxOptions, callback) { 64 | var requests = $.map(ajaxOptions, function (options) { 65 | return $.ajax(options); 66 | }); 67 | // When expects each promise to be passed in as an argument, but we have 68 | // an array, so we need to use apply. 69 | $.when.apply($, requests).then(function () { 70 | callback(); 71 | }); 72 | } 73 | 74 | var run = function () { 75 | mongo.util.enableConsoleProtection(); 76 | 77 | var initUrls = []; 78 | var initJson = []; 79 | var initJsonUrls = []; 80 | // For now, assume a single resource id for all shells 81 | // Initialize all shells and grab any initialization urls 82 | $(mongo.config.rootElementSelector).each(function (index, shellElement) { 83 | var initUrl = shellElement.getAttribute('data-initialization-url'); 84 | if (initUrl) { 85 | initUrls.push(initUrl); 86 | } 87 | var jsonAttr = shellElement.getAttribute('data-initialization-json'); 88 | if (jsonAttr && jsonAttr[0] === '{' && jsonAttr[jsonAttr.length - 1] === '}') { 89 | // If it looks like a JSON object, assume it is supposed to be and try to parse it 90 | try { 91 | initJson.push(JSON.parse(jsonAttr)); 92 | } catch (e) { 93 | console.error('Unable to parse initialization json: ' + jsonAttr); 94 | } 95 | } else if (jsonAttr) { 96 | // Otherwise assume it's a URL that points to JSON data 97 | initJsonUrls.push(jsonAttr); 98 | } 99 | mongo.shells[index] = new mongo.Shell(shellElement, index); 100 | }); 101 | 102 | // Request a resource ID, give it to all the shells, and keep it alive 103 | mongo.request.createMWSResource(mongo.shells, function (data) { 104 | setInterval( 105 | function () { mongo.request.keepAlive(data.res_id); }, 106 | mongo.config.keepAliveTime 107 | ); 108 | 109 | // Need to make sure that urls are unique and converted to $.ajax options 110 | initUrls = $.map(uniqueArray(initUrls), function (url) { 111 | return { 112 | type: 'POST', 113 | url: url, 114 | data: JSON.stringify({res_id: data.res_id}), 115 | contentType: 'application/json' 116 | }; 117 | }); 118 | initJsonUrls = $.map(uniqueArray(initJsonUrls), function (url) { 119 | return { 120 | type: 'GET', 121 | url: url, 122 | success: function (data) { 123 | if (typeof (data) === 'string') { 124 | data = JSON.parse(data); 125 | } 126 | initJson.push(data); 127 | } 128 | }; 129 | }); 130 | 131 | // Get all of the remote JSON literals 132 | ensureAllRequests(initJsonUrls, function () { 133 | // Condense JSON to a single object 134 | initJson = condenseJsonArray(initJson); 135 | 136 | // Add local JSON literal to initialization requests 137 | if (Object.keys(initJson).length > 0) { 138 | initUrls.push({ 139 | type: 'POST', 140 | url: '/init/load_json', 141 | data: JSON.stringify({ 142 | res_id: data.res_id, 143 | collections: initJson 144 | }), 145 | contentType: 'application/json' 146 | }); 147 | } 148 | initializationUrls[data.res_id] = initUrls; 149 | 150 | var finishSetup = function () { 151 | $.each(mongo.shells, function (i, shell) { 152 | shell.attachInputHandler(data.res_id); 153 | }); 154 | }; 155 | if (data.is_new) { 156 | mongo.init.runInitializationScripts(data.res_id, finishSetup); 157 | } else { 158 | finishSetup(); 159 | } 160 | }); 161 | }); 162 | }; 163 | 164 | var runInitializationScripts = function(res_id, callback){ 165 | // Send requests to all initialization urls for a res id, then call the 166 | // callback when all are done. 167 | ensureAllRequests(initializationUrls[res_id], callback); 168 | }; 169 | 170 | return { 171 | run: run, 172 | runInitializationScripts: runInitializationScripts 173 | }; 174 | })(); 175 | -------------------------------------------------------------------------------- /frontend/src/mws/jsonUtils.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /* global mongo */ 17 | mongo.jsonUtils = (function () { 18 | var tojson = function(x, indent, nolint){ 19 | if (x === null || x === undefined) { 20 | return String(x); 21 | } 22 | indent = indent || ''; 23 | 24 | switch (typeof x) { 25 | case 'string': 26 | var out = new Array(x.length+1); 27 | out[0] = '"'; 28 | for (var i=0; i/dev/null \ 16 | | diff licenses/LICENSE.$ext - &>/dev/null 17 | 18 | # If they do not match and the file is not empty, output name of file 19 | if [[ $? -ne 0 ]] && [[ $filesize -gt 0 ]]; then 20 | echo $1 21 | 22 | # If not run with --check flag, then prepend license to file 23 | if [[ $2 != '--check' ]]; then 24 | cp $1 licenses/temp$$ 25 | cat licenses/LICENSE.$ext licenses/temp$$ > $1 26 | rm licenses/temp$$ 27 | fi 28 | fi 29 | fi 30 | } 31 | 32 | # Trap signals and remove temp file on interrupt 33 | trap "rm -f licenses/temp$$" SIGHUP SIGINT SIGPIPE SIGTERM 34 | 35 | # List all files in the current tree with certain exclusions, execute applyLicense foreach 36 | find . -type f ! -path './frontend/lib/*' \ 37 | ! -path './node_modules/*' \ 38 | ! -path './venv/*' \ 39 | ! -path './.git/*' \ 40 | ! -path './frontend/dist/*' \ 41 | ! -path './.grunt/*' \ 42 | ! -path './_SpecRunner.html' \ 43 | | while read file; do applyLicense "$file" $1; done 44 | -------------------------------------------------------------------------------- /licenses/LICENSE.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | -------------------------------------------------------------------------------- /licenses/LICENSE.html: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /licenses/LICENSE.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 10gen Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | -------------------------------------------------------------------------------- /licenses/LICENSE.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongo-web-shell", 3 | "version": "0.0.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/10gen-labs/mongo-web-shell" 7 | }, 8 | "dependencies": { 9 | "grunt": "~0.4.1", 10 | "time-grunt": "~0.1.1", 11 | "grunt-usemin": "~2.0.0", 12 | "grunt-contrib-copy": "~0.4.1", 13 | "grunt-contrib-uglify": "~0.2.5", 14 | "grunt-contrib-cssmin": "~0.6.1", 15 | "grunt-contrib-concat": "~0.3.0", 16 | "grunt-contrib-clean": "~0.4.1", 17 | "grunt-ngmin": "0.0.3", 18 | "grunt-filerev": "~0.1.1", 19 | "load-grunt-tasks": "~0.2.0" 20 | }, 21 | "devDependencies": { 22 | "grunt-contrib-watch": "~0.4.3", 23 | "grunt-contrib-jshint": "~0.5.2", 24 | "grunt-contrib-jasmine": "~0.4.2", 25 | "grunt-shell": "~0.2.2", 26 | "sinon": "~1.7.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git status | egrep '# (Untracked files|Changes not staged for commit):' 4 | if [[ $? -eq 0 ]]; then 5 | echo 'Please stage your changes or stash them before you commit.' 6 | exit 1 7 | fi 8 | 9 | . ./venv/bin/activate 10 | 11 | echo 'Checking for licenses' 12 | missing=`./license.sh --check` 13 | if [[ -n $missing ]]; then 14 | echo The following files are missing licenses: 15 | echo "$missing" 16 | exit 1 17 | fi 18 | 19 | echo "Running tests..." 20 | grunt_out=`grunt test 2>&1` 21 | exit_code=$? 22 | if [ $exit_code != 0 ]; then 23 | echo "$grunt_out" 24 | exit 1 25 | fi 26 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.9 2 | Jinja2==2.6 3 | PyYAML==3.10 4 | Werkzeug==0.8.3 5 | distribute==0.6.35 6 | wsgiref==0.1.2 7 | pymongo==2.7 8 | mock==1.0.1 9 | APScheduler==2.1.0 10 | requests==2.0.0 11 | itsdangerous==0.23 12 | nose==1.3.0 13 | rednose==0.4.1 14 | unittest2==0.5.1 15 | -------------------------------------------------------------------------------- /run_tests.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import nose 16 | 17 | if __name__ == '__main__': 18 | result = nose.runmodule(name='tests', argv=[ 19 | '', '-s', '--verbose', '--logging-level=INFO', '--rednose', ]) 20 | -------------------------------------------------------------------------------- /sample.yml: -------------------------------------------------------------------------------- 1 | DB_HOSTS: 2 | - localhost 3 | SECRET_KEY: 'lumberjack' -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | import flask 17 | from unittest2 import defaultTestLoader, TestCase 18 | 19 | from webapps.server.app import create_app 20 | 21 | if not flask.has_app_context(): 22 | app = create_app() 23 | app.testing = True 24 | app.config['QUOTA_NUM_COLLECTIONS'] = None 25 | 26 | _logger = logging.getLogger(__name__) 27 | 28 | 29 | class MongoWSTestCase(TestCase): 30 | """A generic test case for the mongows package.""" 31 | 32 | def setUp(self): 33 | self.real_app = app 34 | self.app = app.test_client() 35 | ctx = app.app_context() 36 | ctx.push() 37 | 38 | def tearDown(self): 39 | pass 40 | 41 | 42 | def load_tests(): 43 | """Returns the test modules for the mongows package. 44 | 45 | The expected output of this function is defined by the unittest module's 46 | load_tests protocol. unittest.main() will runs tests on the modules 47 | returned by this function. 48 | 49 | """ 50 | return defaultTestLoader.discover(__name__) 51 | -------------------------------------------------------------------------------- /tests/test_crontab.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from datetime import datetime, timedelta 16 | import mock 17 | from webapps.server import crontab 18 | from webapps.server.crontab import run_scheduler, EXPIRE_SESSION_EVERY 19 | from webapps.lib.db import get_db 20 | from webapps.lib.util import to_coll_name 21 | from tests import MongoWSTestCase 22 | 23 | 24 | class ExpireSessionsTestCase(MongoWSTestCase): 25 | def setUp(self): 26 | super(ExpireSessionsTestCase, self).setUp() 27 | self.context = self.real_app.app_context() 28 | self.context.push() 29 | self.db = get_db() 30 | 31 | # All test data should be before this date 32 | # We can assume we don't have any real data before this date 33 | self.test_before_date = datetime(2012, 7, 6) 34 | self.db.clients.remove({'timestamp': {'$lt': self.test_before_date}}) 35 | 36 | def tearDown(self): 37 | super(ExpireSessionsTestCase, self).tearDown() 38 | # Remove test entries from db 39 | self.db.clients.remove({'timestamp': {'$lt': self.test_before_date}}) 40 | self.context.pop() 41 | 42 | @mock.patch('webapps.server.crontab.datetime') 43 | def test_removes_old_sessions_and_associated_collections(self, 44 | datetime_mock): 45 | dates = [ 46 | datetime(2012, 7, 2), 47 | datetime(2012, 7, 3), 48 | datetime(2012, 7, 4), 49 | datetime(2012, 7, 5), 50 | ] 51 | for session_res_id, date in enumerate(dates): 52 | collections = [] 53 | for collection in xrange(3): 54 | collections.append(collection) 55 | collection_name = to_coll_name(session_res_id, 56 | collection) 57 | self.db[collection_name].insert({'foo': 'barr'}) 58 | 59 | self.db.clients.insert({ 60 | 'session_id': session_res_id, 61 | 'res_id': session_res_id, 62 | 'collections': collections, 63 | 'timestamp': date, 64 | }) 65 | 66 | # Want to get rid of everything before 2012/7/4 67 | delta = timedelta(seconds=crontab.EXPIRE_SESSION_DURATION) 68 | datetime_mock.now.return_value = datetime(2012, 7, 4) - delta 69 | 70 | crontab.expire_sessions(self.real_app) 71 | 72 | # Should grab all remaining records we inserted 73 | res = self.db.clients.find({ 74 | 'timestamp': {'$lt': self.test_before_date} 75 | }) 76 | self.assertEqual(res.count(), 2) 77 | actual_dates = [r['timestamp'] for r in res] 78 | expected_dates = dates[-2:] # Only the last two should survive 79 | self.assertItemsEqual(actual_dates, expected_dates) 80 | 81 | # Make sure collections were dropped 82 | coll_names = self.db.collection_names() 83 | self.assertNotIn('00', coll_names) 84 | self.assertNotIn('01', coll_names) 85 | self.assertNotIn('02', coll_names) 86 | self.assertNotIn('10', coll_names) 87 | self.assertNotIn('11', coll_names) 88 | self.assertNotIn('12', coll_names) 89 | 90 | self.assertIn('20', coll_names) 91 | self.assertIn('21', coll_names) 92 | self.assertIn('22', coll_names) 93 | self.assertIn('30', coll_names) 94 | self.assertIn('31', coll_names) 95 | self.assertIn('32', coll_names) 96 | 97 | for name in ['20', '21', '22', '30', '31', '32']: 98 | self.db[name].drop() 99 | 100 | @mock.patch('webapps.server.crontab.Scheduler') 101 | @mock.patch('webapps.server.crontab.expire_sessions') 102 | def test_run_scheduler_starts_expire_sessions_job(self, 103 | expire_sessions_mock, 104 | scheduler_cls_mock): 105 | scheduler_mock = mock.MagicMock() 106 | scheduler_cls_mock.return_value = scheduler_mock 107 | 108 | run_scheduler(mock.sentinel.app) 109 | 110 | self.assertTrue(scheduler_mock.add_interval_job.called) 111 | args = scheduler_mock.add_interval_job.call_args 112 | self.assertEqual(len(args[0]), 1) 113 | self.assertEqual(len(args[1]), 1) 114 | func = args[0][0] 115 | secs = args[1]['seconds'] 116 | 117 | func() 118 | expire_sessions_mock.assert_called_with(mock.sentinel.app) 119 | self.assertEqual(secs, EXPIRE_SESSION_EVERY) 120 | -------------------------------------------------------------------------------- /tests/test_util.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import StringIO 16 | from bson.json_util import dumps 17 | import mock 18 | from werkzeug.exceptions import NotFound, InternalServerError 19 | from webapps.lib.db import get_db 20 | from webapps.lib.util import UseResId, get_collection_names 21 | from webapps.lib import CLIENTS_COLLECTION 22 | from webapps.lib.MWSServerError import MWSServerError 23 | from tests import MongoWSTestCase 24 | 25 | 26 | class UseResIdTestCase(MongoWSTestCase): 27 | def test_mangles_collection_names_automatically(self): 28 | with self.real_app.app_context(): 29 | with UseResId('myresid.') as db: 30 | coll = db.foo 31 | self.assertEqual(coll.name, 'myresid.foo') 32 | 33 | def test_updates_collection_list(self): 34 | with self.real_app.app_context(): 35 | db = get_db() 36 | res_id = 'myresid.' 37 | 38 | # Setup resource id record 39 | clients_collection = db[CLIENTS_COLLECTION] 40 | clients_collection.remove({'res_id': res_id}) 41 | clients_collection.insert({ 42 | 'res_id': res_id, 43 | 'collections': [] 44 | }) 45 | 46 | with UseResId(res_id) as db: 47 | self.assertItemsEqual(get_collection_names(res_id), []) 48 | db.foo.insert({'message': 'test'}) 49 | self.assertItemsEqual(get_collection_names(res_id), ['foo']) 50 | self.assertItemsEqual(list(db.foo.find({}, {'_id': 0})), 51 | [{'message': 'test'}]) 52 | 53 | db.bar.update({}, {'message': 'test'}, upsert=True) 54 | self.assertItemsEqual(get_collection_names(res_id), ['foo', 'bar']) 55 | self.assertItemsEqual(list(db.bar.find({}, {'_id': 0})), 56 | [{'message': 'test'}]) 57 | 58 | db.foo.drop() 59 | self.assertItemsEqual(get_collection_names(res_id), ['bar']) 60 | self.assertNotIn(res_id + 'foo', get_collection_names(res_id)) 61 | 62 | 63 | class QuotaCollectionsTestCase(UseResIdTestCase): 64 | def setUp(self): 65 | super(QuotaCollectionsTestCase, self).setUp() 66 | self.old_quota = self.real_app.config['QUOTA_NUM_COLLECTIONS'] 67 | self.res_id = 'myresid.' 68 | with self.real_app.app_context(): 69 | collections = get_collection_names(self.res_id) 70 | with UseResId(self.res_id) as db: 71 | for c in collections: 72 | db.drop_collection(c) 73 | 74 | def tearDown(self): 75 | self.real_app.config['QUOTA_NUM_COLLECTIONS'] = self.old_quota 76 | 77 | def test_quota_collections(self): 78 | self.real_app.config['QUOTA_NUM_COLLECTIONS'] = 2 79 | 80 | with self.real_app.app_context(): 81 | with UseResId(self.res_id) as db: 82 | db.a.insert({'a': 1}) 83 | db.b.insert({'b': 1}) 84 | with self.assertRaises(MWSServerError) as cm: 85 | db.c.insert({'c': 1}) 86 | self.assertEqual(cm.exception.error, 429) 87 | 88 | for c in ['a', 'b']: 89 | db.drop_collection(c) 90 | 91 | def test_quota_collections_zero(self): 92 | self.real_app.config['QUOTA_NUM_COLLECTIONS'] = 0 93 | 94 | with self.real_app.app_context(): 95 | with UseResId(self.res_id) as db: 96 | with self.assertRaises(MWSServerError) as cm: 97 | db.a.insert({'a': 1}) 98 | self.assertEqual(cm.exception.error, 429) 99 | 100 | db.drop_collection('a') 101 | -------------------------------------------------------------------------------- /webapps/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /webapps/configs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mongodb-labs/mongo-web-shell/b306390e26888f0a0bf77cf99528e4e6bc41f0a8/webapps/configs/__init__.py -------------------------------------------------------------------------------- /webapps/configs/ivs.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # flask.config settings. 16 | DEBUG = True 17 | SECRET_KEY = 'foo' 18 | 19 | # Misc settings. 20 | HOST = '0.0.0.0' 21 | PORT = 8081 22 | DATA_DIR = '/tmp' 23 | 24 | # Rate limit settings 25 | RATELIMIT_COLLECTION = 'ivs_ratelimit' 26 | RATELIMIT_QUOTA = 3 # requests per expiry 27 | RATELIMIT_EXPIRY = 10 # expiry in seconds 28 | 29 | # DB Settings 30 | DB_HOSTS = ['localhost'] 31 | DB_PORT = 27017 32 | DB_NAME = 'mongows' 33 | 34 | # edX integration 35 | EDX_SHARED_KEY = 'wanderlust' 36 | GRADING_SERVER_URL = 'http://localhost' 37 | GRADING_API_KEY = 'i4mm3' 38 | GRADING_API_SECRET = 's0s3cr3t' 39 | -------------------------------------------------------------------------------- /webapps/configs/server.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | This module defines constants that are intended to be imported into the Flask 17 | app configuration by default. 18 | 19 | """ 20 | # flask.config settings. 21 | DEBUG = False 22 | SECRET_KEY = 'foo' 23 | 24 | 25 | #DB Settings 26 | DB_HOSTS = ["localhost"] 27 | DB_PORT = 27017 28 | DB_NAME = 'mongows' 29 | 30 | 31 | HOST = "0.0.0.0" 32 | PORT = 5000 33 | 34 | RATELIMIT_COLLECTION = 'server_ratelimit' 35 | RATELIMIT_QUOTA = 500 # requests per expiry 36 | RATELIMIT_EXPIRY = 60 # expiry in seconds 37 | 38 | QUOTA_COLLECTION_SIZE = 5 * 1024 * 1024 # size quota in bytes 39 | 40 | # QUOTA_NUM_COLLECTIONS: number of collections per res_id 41 | # False: unlimited number of collections, no quota 42 | # 0: user is unable to create additional collections 43 | # 1+: user may have up to # collections per res_id 44 | QUOTA_NUM_COLLECTIONS = 8 45 | 46 | # Cursors config 47 | CURSOR_BATCH_SIZE = 20 # default max docs to return for a query 48 | 49 | # Logging config 50 | ADMIN_EMAILS = '' 51 | LOG_FILE_PATH = '' 52 | -------------------------------------------------------------------------------- /webapps/ivs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mongodb-labs/mongo-web-shell/b306390e26888f0a0bf77cf99528e4e6bc41f0a8/webapps/ivs/__init__.py -------------------------------------------------------------------------------- /webapps/ivs/app.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | 17 | from flask import Flask 18 | 19 | from webapps.lib.log import configure_logging 20 | from webapps.lib.conf import update_config 21 | from webapps.lib.util import get_environment 22 | from webapps.ivs.views import ivs 23 | 24 | 25 | _here = os.path.dirname(os.path.abspath(__file__)) 26 | environment = get_environment(_here) 27 | 28 | 29 | def create_app(): 30 | app = Flask(__name__) 31 | app.config.from_object('webapps.configs.ivs') 32 | # Overrides the config with any environment variables that might 33 | # be set 34 | update_config(app, 'IVS', environment) 35 | configure_logging(app, environment) 36 | app.register_blueprint(ivs) 37 | return app 38 | 39 | 40 | app = create_app() 41 | -------------------------------------------------------------------------------- /webapps/ivs/initializers/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /webapps/ivs/initializers/util.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import logging 17 | from subprocess import Popen 18 | from bson.json_util import loads 19 | from werkzeug.exceptions import NotFound, InternalServerError 20 | from webapps.lib.db import get_db 21 | from webapps.lib.util import get_collection_names, UseResId 22 | 23 | 24 | from flask import current_app 25 | 26 | SEEK_SET = 0 27 | 28 | _logger = logging.getLogger(__name__) 29 | 30 | 31 | def cleanup_collections(res_id): 32 | with UseResId(res_id) as db: 33 | db.drop_database() 34 | 35 | 36 | def load_data_from_mongoexport(res_id, export_location, collection_name, 37 | remove_id=False): 38 | """ 39 | This file should come from mongoexport, with or without the --jsonArray 40 | flag. That is to say, it should either be a series of documents, each on 41 | its own line, or a single array of documents. All documents will be 42 | inserted into the given collection. 43 | """ 44 | export_location = _data_file_path(export_location) 45 | with open(export_location) as export: 46 | first_char = export.read(1) 47 | export.seek(0, SEEK_SET) 48 | if first_char == '[': 49 | # Data is already in an array 50 | documents = loads(export.read()) 51 | else: 52 | # Each line of data is an object 53 | documents = [] 54 | for line in export: 55 | documents.append(loads(line)) 56 | if remove_id: 57 | _remove_id(documents) 58 | 59 | with UseResId(res_id) as db: 60 | db[collection_name].insert(documents) 61 | 62 | 63 | def load_data_from_json(res_id, file_name, remove_id=False): 64 | """ 65 | The top level of this file should be an object who's keys are collection 66 | names which map to an array of documents to be inserted into the collection 67 | """ 68 | file_name = _data_file_path(file_name) 69 | with open(file_name) as json_file: 70 | collections = loads(json_file.read()) 71 | with UseResId(res_id) as db: 72 | for collection, documents in collections.iteritems(): 73 | if remove_id: 74 | _remove_id(documents) 75 | db[collection].insert(documents) 76 | 77 | 78 | def load_data_from_mongodump(res_id, dump_location, collection_name): 79 | """ 80 | The dump location should point to a .bson file, not a directory structure 81 | as created by mongodump. Instead, use the .bson files inside this 82 | directory structure. 83 | """ 84 | dump_location = _data_file_path(dump_location) 85 | if not os.path.exists(dump_location): 86 | raise NotFound('Unable to find dump file') 87 | p = Popen(( 88 | 'mongorestore', 89 | '-d', 'mws', 90 | '-c', '%s%s' % (res_id, collection_name), 91 | dump_location 92 | )) 93 | p.communicate() # Wait for process to finish 94 | if p.poll() != 0: 95 | raise InternalServerError('Loading dumped data failed') 96 | with UseResId(res_id) as db: 97 | db.ensure_client_collection(collection_name) 98 | 99 | 100 | def _remove_id(documents): 101 | """ Removes the _id field from each document in the array """ 102 | for document in documents: 103 | if '_id' in document: 104 | del document['_id'] 105 | 106 | 107 | def _data_file_path(path): 108 | """ 109 | Returns the full path of the data file with respect to the configured 110 | data directory (specified via config). 111 | """ 112 | data_dir = current_app.config.get('DATA_DIR', '') 113 | return os.path.join(data_dir, path) 114 | -------------------------------------------------------------------------------- /webapps/ivs/test/test_initializers_views.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import types 16 | from bson.json_util import dumps 17 | import mock 18 | import sys 19 | from tests import MongoWSTestCase 20 | 21 | 22 | class InitializersTestCase(MongoWSTestCase): 23 | def create_test_script(self, **kwargs): 24 | # Create module test_script in scripts 25 | test_script = types.ModuleType('test_script') 26 | run_mock = mock.MagicMock(**kwargs) 27 | test_script.__dict__.update({'run': run_mock}) 28 | sys.modules['mongows.initializers.scripts.test_script'] = test_script 29 | return run_mock 30 | 31 | def make_init_request(self, script_name, data=None): 32 | if data is None: 33 | data = {'res_id': 'test_res_id'} 34 | return self.app.post('/init/%s' % script_name, data=dumps(data), 35 | content_type='application/json') 36 | 37 | def test_imports_and_runs_the_specified_file(self): 38 | run_mock = self.create_test_script(return_value=('ok', 200)) 39 | 40 | response = self.make_init_request('test_script', {'res_id': 'foo'}) 41 | 42 | run_mock.assert_called_once_with('foo') 43 | self.assertEqual(response.data, 'ok') 44 | self.assertEqual(response.status_code, 200) 45 | del sys.modules['mongows.initializers.scripts.test_script'] 46 | 47 | def test_returns_404_when_accessing_nonexistent_script(self): 48 | response = self.make_init_request('does_not_exist') 49 | 50 | expected_message = 'Unknown initialization script does_not_exist' 51 | self.assertEqual(response.data, expected_message) 52 | self.assertEqual(response.status_code, 404) 53 | 54 | def test_passes_parsed_json_when_there_are_extra_keys(self): 55 | data = { 56 | 'res_id': 'my_res_id', 57 | 'extra_data': {'my': 'data'} 58 | } 59 | 60 | run_mock = self.create_test_script(return_value=('', 204)) 61 | 62 | self.make_init_request('test_script', data) 63 | run_mock.assert_called_once_with('my_res_id', data) 64 | 65 | def test_custom_return_value(self): 66 | run_mock = self.create_test_script(return_value=('{"key":"val"}', 200)) 67 | 68 | response = self.make_init_request('test_script', {'res_id': 'foo'}) 69 | 70 | run_mock.assert_called_once_with('foo') 71 | self.assertEqual(response.data, '{"key":"val"}') 72 | self.assertEqual(response.status_code, 200) 73 | del sys.modules['mongows.initializers.scripts.test_script'] 74 | 75 | def test_exception_catching(self): 76 | run_mock = self.create_test_script(side_effect=IOError('error!')) 77 | 78 | response = self.make_init_request('test_script', {'res_id': 'foo'}) 79 | 80 | run_mock.assert_called_once_with('foo') 81 | error = '{"reason": "IOError", "detail": "error!", "error": 500}' 82 | self.assertEqual(response.data, error) 83 | self.assertEqual(response.status_code, 500) 84 | del sys.modules['mongows.initializers.scripts.test_script'] 85 | -------------------------------------------------------------------------------- /webapps/ivs/test/test_util.py: -------------------------------------------------------------------------------- 1 | from webapps.ivs.initializers.util import ( 2 | load_data_from_mongoexport, 3 | load_data_from_json, 4 | load_data_from_mongodump, 5 | ) 6 | 7 | 8 | class InitializersTestCase(MongoWSTestCase): 9 | def mock_open(self, open_mock, contents): 10 | file_wrapper = mock.MagicMock(spec=file) 11 | open_mock.return_value = file_wrapper 12 | file_handle = StringIO.StringIO(contents) 13 | file_wrapper.__enter__.return_value = file_handle 14 | 15 | @mock.patch('__builtin__.open') 16 | def test_loads_exported_data(self, open_mock): 17 | documents = [ 18 | {'_id': 1, 'msg': 'my test string'}, 19 | {'_id': 2, 'message': 'my other string'}, 20 | {'_id': 3, 'foo': 'bar', 'greeting': 'hi there'}, 21 | ] 22 | file_contents = '\n'.join([dumps(doc) for doc in documents]) 23 | self.mock_open(open_mock, file_contents) 24 | 25 | with self.real_app.app_context(): 26 | db = get_db() 27 | 28 | # Test normally (keeping the _id) 29 | load_data_from_mongoexport('myresid.', 'my/file/location', 30 | 'mycoll') 31 | open_mock.assert_called_with('my/file/location') 32 | collection_contents = list(db['myresid.mycoll'].find()) 33 | self.assertItemsEqual(collection_contents, documents) 34 | 35 | db['myresid.mycoll'].drop() 36 | 37 | # Test removing the _id 38 | load_data_from_mongoexport('myresid.', 'my/file/location', 39 | 'mycoll', True) 40 | collection_contents = list(db['myresid.mycoll'].find()) 41 | for doc in collection_contents: 42 | # Should not be any of the given _id's 43 | self.assertNotIn(doc['_id'], (1, 2, 3)) 44 | db['myresid.mycoll'].drop() 45 | 46 | @mock.patch('__builtin__.open') 47 | def test_loads_exported_array_data(self, open_mock): 48 | documents = [ 49 | {'_id': 1, 'msg': 'my test string'}, 50 | {'_id': 2, 'message': 'my other string'}, 51 | {'_id': 3, 'foo': 'bar', 'greeting': 'hi there'}, 52 | ] 53 | file_contents = dumps(documents) 54 | self.mock_open(open_mock, file_contents) 55 | 56 | with self.real_app.app_context(): 57 | db = get_db() 58 | 59 | load_data_from_mongoexport('myresid.', 60 | 'my/file/location', 'mycoll') 61 | 62 | open_mock.assert_called_with('my/file/location') 63 | collection_contents = list(db['myresid.mycoll'].find()) 64 | self.assertItemsEqual(collection_contents, documents) 65 | 66 | db['myresid.mycoll'].drop() 67 | 68 | @mock.patch('__builtin__.open') 69 | def test_loads_json_data(self, open_mock): 70 | documents = { 71 | 'first_coll': [ 72 | {'_id': 1, 'msg': 'my test string'}, 73 | {'_id': 2, 'message': 'my other string'}, 74 | {'_id': 3, 'foo': 'bar', 'greeting': 'hi there'}, 75 | ], 76 | 'viewing_preferences': [ 77 | {'_id': 2, 'tv_shows': ['archer', 'firefly']}, 78 | {'_id': 3, 'tv_shows': ['arrested development'], 79 | 'movies': ['shawshank redemption']}, 80 | ], 81 | } 82 | file_contents = dumps(documents) 83 | 84 | with self.real_app.app_context(): 85 | db = get_db() 86 | 87 | # Test normally (keeping the _id) 88 | self.mock_open(open_mock, file_contents) 89 | load_data_from_json('myresid.', 'my/file/location') 90 | 91 | open_mock.assert_called_with('my/file/location') 92 | first_coll_contents = list(db['myresid.first_coll'].find()) 93 | self.assertItemsEqual(first_coll_contents, documents['first_coll']) 94 | viewing_prefs_contents = list( 95 | db['myresid.viewing_preferences'].find() 96 | ) 97 | self.assertItemsEqual(viewing_prefs_contents, 98 | documents['viewing_preferences']) 99 | 100 | db['myresid.first_coll'].drop() 101 | db['myresid.viewing_preferences'].drop() 102 | 103 | # Test removing the _id's 104 | self.mock_open(open_mock, file_contents) 105 | load_data_from_json('myresid.', 'my/file/location', True) 106 | 107 | first_coll_contents = list(db['myresid.first_coll'].find()) 108 | for doc in first_coll_contents: 109 | self.assertNotIn(doc['_id'], (1, 2, 3)) 110 | viewing_prefs_contents = list( 111 | db['myresid.viewing_preferences'].find() 112 | ) 113 | for doc in viewing_prefs_contents: 114 | self.assertNotIn(doc['_id'], (1, 2, 3)) 115 | 116 | db['myresid.first_coll'].drop() 117 | db['myresid.viewing_preferences'].drop() 118 | 119 | @mock.patch('mongows.initializers.util.Popen') 120 | @mock.patch('mongows.initializers.util.os') 121 | def test_loads_dumped_bson_data(self, os_mock, Popen_mock): 122 | popen_instance = mock.MagicMock() 123 | popen_instance.poll.return_value = 0 124 | Popen_mock.return_value = popen_instance 125 | os_mock.path.exists.return_value = True 126 | 127 | with self.real_app.app_context(): 128 | res_id = 'myresid.' 129 | collection_name = 'collname' 130 | dump_location = '/my/dump/location' 131 | load_data_from_mongodump(res_id, dump_location, collection_name) 132 | Popen_mock.assert_called_with(( 133 | 'mongorestore', 134 | '-d', 'mws', 135 | '-c', '%s%s' % (res_id, collection_name), 136 | dump_location 137 | )) 138 | popen_instance.communicate.assert_called_once_with() # no args 139 | self.assertIn(collection_name, get_collection_names(res_id)) 140 | 141 | @mock.patch('mongows.initializers.util.Popen') 142 | @mock.patch('mongows.initializers.util.os') 143 | def test_loading_nonexistant_dump_throws_404(self, os_mock, Popen_mock): 144 | # Popen_mock is needed to make sure a subprocess isn't actually created 145 | run_load = lambda: load_data_from_mongodump('myresid.', 146 | '/does/not/exist', 147 | 'collname') 148 | os_mock.path.exists.return_value = False 149 | self.assertRaises(NotFound, run_load) 150 | 151 | @mock.patch('mongows.initializers.util.Popen') 152 | @mock.patch('mongows.initializers.util.os') 153 | def test_mongorestore_errors_throw_500(self, os_mock, Popen_mock): 154 | os_mock.path.exists.return_value = True 155 | popen_instance = mock.MagicMock() 156 | Popen_mock.return_value = popen_instance 157 | popen_instance.poll.return_value = 1 # error code from mongorestore 158 | 159 | run_load = lambda: load_data_from_mongodump('myresid.', 160 | '/does/not/exist', 161 | 'collname') 162 | 163 | self.assertRaises(InternalServerError, run_load) 164 | -------------------------------------------------------------------------------- /webapps/ivs/test/test_validation_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from mongows.mws.db import get_db 16 | from tests import MongoWSTestCase 17 | from mongows.validators.ValidationTest import ValidationTest 18 | 19 | 20 | class ValidationUtilsTestCase(MongoWSTestCase): 21 | def setUp(self): 22 | class ValidationTestCase(ValidationTest): 23 | def run(self): 24 | pass 25 | 26 | self.db = get_db() 27 | self.validator = ValidationTestCase('test_') 28 | self.coll = 'test_coll' 29 | self.db.drop_collection(self.coll) 30 | 31 | def tearDown(self): 32 | self.db.drop_collection(self.coll) 33 | 34 | 35 | class CollectionEqualsUnitTest(ValidationUtilsTestCase): 36 | def test_equals(self): 37 | self.assertTrue(self.validator.collection_equals('coll', [])) 38 | self.assertFalse(self.validator.collection_equals('coll', [{'a': 1}])) 39 | for i in range(3): 40 | self.db.test_coll.insert({'a': i}) 41 | self.assertTrue(self.validator.collection_equals('coll', [ 42 | {'a': 0}, {'a': 1}, {'a': 2}])) 43 | self.assertFalse(self.validator.collection_equals('coll', [{'a': 1}])) 44 | 45 | def test_equals_id(self): 46 | self.assertTrue(self.validator.collection_equals('coll', [], True)) 47 | for i in range(3): 48 | self.db.test_coll.insert({'_id': 'id%s' % i, 'a': i}) 49 | self.assertFalse(self.validator.collection_equals('coll', [ 50 | {'a': 0}, {'a': 1}, {'a': 2}], True)) 51 | self.assertTrue(self.validator.collection_equals('coll', [ 52 | {'_id': 'id0', 'a': 0}, 53 | {'_id': 'id1', 'a': 1}, 54 | {'_id': 'id2', 'a': 2}], True)) 55 | 56 | def test_recursive(self): 57 | data = { 58 | 'a': { 59 | 'b': { 60 | 'c': 1, 61 | 'd': [2, 3, 4] 62 | }, 63 | 'e': {'f': 'five'} 64 | }, 65 | 'g': 6., 66 | 'h': True 67 | } 68 | self.db.test_coll.insert(dict(data)) # insert modifies data, use copy 69 | self.assertTrue(self.validator.collection_equals('coll', [data])) 70 | 71 | data2 = { 72 | 'h': True, 73 | 'a': { 74 | 'e': {'f': 'five'}, 75 | 'b': { 76 | 'd': [2, 3, 4], 77 | 'c': 1 78 | } 79 | }, 80 | 'g': 6. 81 | } 82 | 83 | self.assertTrue(self.validator.collection_equals('coll', [data2])) 84 | 85 | 86 | class CollectionContainsUnitTest(ValidationUtilsTestCase): 87 | def test_contains(self): 88 | for i in range(3): 89 | self.db.test_coll.insert({'a': i}) 90 | self.assertTrue(self.validator.collection_contains('coll', 91 | [{'a': 0}, 92 | {'a': 1}, 93 | {'a': 2}])) 94 | self.assertTrue(self.validator.collection_contains('coll', [{'a': 1}])) 95 | self.assertFalse(self.validator.collection_contains('coll', 96 | [{'a': 1}, 97 | {'b': 2}])) 98 | 99 | def test_contains_id(self): 100 | for i in range(3): 101 | self.db.test_coll.insert({'_id': 'id%s' % i, 'a': i}) 102 | self.assertFalse(self.validator.collection_contains('coll', 103 | [{'a': 1}], True)) 104 | self.assertTrue(self.validator.collection_contains('coll', [ 105 | {'_id': 'id1', 'a': 1}], True)) 106 | self.assertFalse(self.validator.collection_contains('coll', [ 107 | {'_id': 'id1', 'a': 1}, {'_id': 'id2', 'b': 2}]), True) 108 | 109 | def test_contains_multiple(self): 110 | for i in range(3): 111 | self.db.test_coll.insert({'a': 0}) 112 | self.assertTrue(self.validator.collection_contains('coll', 113 | [{'a': 0}, 114 | {'a': 0}])) 115 | self.assertFalse(self.validator.collection_contains('coll', 116 | [{'a': 0}, 117 | {'a': 0}, 118 | {'a': 0}, 119 | {'a': 0}])) 120 | 121 | 122 | class CollectionContainsAnyUnitTest(ValidationUtilsTestCase): 123 | def test_contains_any(self): 124 | for i in range(3): 125 | self.db.test_coll.insert({'a': i}) 126 | self.assertTrue(self.validator.collection_contains_any('coll', 127 | [{'a': 0}, 128 | {'a': 1}, 129 | {'a': 2}])) 130 | self.assertTrue(self.validator.collection_contains_any('coll', 131 | [{'a': 1}])) 132 | self.assertTrue(self.validator.collection_contains_any('coll', 133 | [{'a': 1}, 134 | {'b': 2}])) 135 | 136 | def test_contains_any_id(self): 137 | for i in range(3): 138 | self.db.test_coll.insert({'_id': 'id%s' % i, 'a': i}) 139 | self.assertFalse(self.validator.collection_contains_any('coll', 140 | [{'a': 1}], True)) 141 | self.assertTrue(self.validator.collection_contains_any('coll', [ 142 | {'_id': 'id1', 'a': 1}], True)) 143 | self.assertTrue(self.validator.collection_contains_any('coll', [ 144 | {'_id': 'id1', 'a': 1}, {'_id': 'id2', 'b': 2}], True)) 145 | 146 | 147 | def CollectionContainsNoneUnitTest(ValidationUtilsTestCase): 148 | def test_contains_none(self): 149 | for i in range(3): 150 | self.db.test_coll.insert({'a': i}) 151 | self.assertFalse(self.validator.collection_contains_none('coll', 152 | [{'a': 1}])) 153 | self.assertFalse(self.validator.collection_contains_none('coll', 154 | [{'a': 1}, 155 | {'b': 2}])) 156 | self.assertTrue(self.validator.collection_contains_none('coll', 157 | [{'b': 2}])) 158 | 159 | def test_contains_none_id(self): 160 | for i in range(3): 161 | self.db.test_coll.insert({'a': i}) 162 | self.assertTrue(self.validator.collection_contains_none('coll', 163 | [{'a': 1}], 164 | True)) 165 | self.assertFalse(self.validator.collection_contains_none('coll', [ 166 | {'_id': 'id1', 'a': 1}])) 167 | self.assertTrue(self.validator.collection_contains_none('coll', 168 | [{'_id': 'id2', 169 | 'b': 2}])) 170 | -------------------------------------------------------------------------------- /webapps/ivs/test/test_validators_views.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import types 16 | import mock 17 | import sys 18 | from tests import MongoWSTestCase 19 | from mongows.validators.ValidationTest import ValidationTest 20 | 21 | 22 | class ValidatorsTestCase(MongoWSTestCase): 23 | def test_imports_and_runs_the_specified_file(self): 24 | # Create module test_script in scripts 25 | test_script = types.ModuleType('test_script') 26 | run_mock = mock.MagicMock() 27 | 28 | class ValidationTestCase(ValidationTest): 29 | def run(self): 30 | run_mock(self.res_id) 31 | return 'ok', 200 32 | 33 | test_script.__dict__.update({'ValidationTestCase': ValidationTestCase}) 34 | sys.modules['mongows.validators.scripts.test_script'] = test_script 35 | 36 | response = self.app.post('/validate/test_script', 37 | data={'res_id': 'foo'}) 38 | 39 | self.assertEqual(response.data, 'ok') 40 | self.assertEqual(response.status_code, 200) 41 | run_mock.assert_called_once_with('foo') 42 | del sys.modules['mongows.validators.scripts.test_script'] 43 | 44 | def test_returns_404_when_accessing_nonexistent_script(self): 45 | response = self.app.post('/validate/test_script', 46 | data={'res_id': 'foo'}) 47 | 48 | expected_message = 'Unknown validation script test_script' 49 | self.assertEqual(response.data, expected_message) 50 | self.assertEqual(response.status_code, 404) 51 | -------------------------------------------------------------------------------- /webapps/ivs/verify/ValidationTest.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from webapps.lib.db import get_db 16 | from webapps.lib.util import UseResId 17 | from abc import ABCMeta, abstractmethod 18 | from collections import Counter 19 | 20 | 21 | class ValidationTest: 22 | __metaclass__ = ABCMeta 23 | 24 | def __init__(self, res_id): 25 | self.res_id = res_id 26 | self.db = get_db() 27 | 28 | def __hashable(self, d): 29 | if isinstance(d, dict): 30 | return tuple({k: self.__hashable(d[k]) for k in d}.items()) 31 | elif isinstance(d, list): 32 | return tuple([self.__hashable(v) for v in d]) 33 | else: 34 | return d 35 | 36 | def __precompute(self, collection, data_only, data, check_id): 37 | with UseResId(self.res_id): 38 | query = {'$or': data} if data_only else {} 39 | projection = None if check_id else {'_id': 0} 40 | result = self.db[collection].find(query, projection) 41 | data = (self.__hashable(x) for x in data) 42 | result = (self.__hashable(x) for x in result) 43 | return data, result 44 | 45 | # Collection must exactly equal the data set 46 | def collection_equals(self, collection, data, check_id=False): 47 | data, result = self.__precompute(collection, False, data, check_id) 48 | return Counter(result) == Counter(data) 49 | 50 | # Data must be a subset of collection 51 | def collection_contains(self, collection, data, check_id=False): 52 | data, result = self.__precompute(collection, True, data, check_id) 53 | data = Counter(data) 54 | result = Counter(result) 55 | return all(data[key] <= result[key] for key in data) 56 | 57 | # Collection must contain one or more of the elements in data 58 | def collection_contains_any(self, collection, data, check_id=False): 59 | data, result = self.__precompute(collection, True, data, check_id) 60 | data = set(data) 61 | result = set(result) 62 | return any(x in result for x in data) 63 | 64 | # Collection does not contain any of the elements in data 65 | def collection_contains_none(self, collection, data, check_id=False): 66 | data, result = self.__precompute(collection, True, data, check_id) 67 | data = set(data) 68 | result = set(result) 69 | return all(x not in result for x in data) 70 | 71 | # Require all inheriting classes to implement a run method 72 | @abstractmethod 73 | def run(self): 74 | pass 75 | -------------------------------------------------------------------------------- /webapps/ivs/verify/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /webapps/ivs/views.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import json 16 | import requests 17 | import traceback 18 | 19 | from flask import Blueprint, current_app, jsonify, request, session 20 | 21 | from importlib import import_module 22 | from itsdangerous import BadSignature, Signer 23 | 24 | from webapps.lib import CLIENTS_COLLECTION 25 | from webapps.lib.db import get_db 26 | from webapps.lib.decorators import ratelimit 27 | from webapps.lib.MWSServerError import MWSServerError 28 | 29 | 30 | ivs = Blueprint( 31 | 'ivs', __name__, url_prefix='/ivs', template_folder='templates', 32 | static_url_path='', static_folder='../../frontend' 33 | ) 34 | 35 | import logging 36 | _logger = logging.getLogger(__name__) 37 | 38 | 39 | @ivs.route('/init/', methods=['POST']) 40 | @ratelimit 41 | def init(script_name): 42 | res_id = _get_res_id() 43 | try: 44 | module = 'webapps.ivs.initializers.scripts.{0}'.format(script_name) 45 | module = import_module(module) 46 | except ImportError as e: 47 | raise MWSServerError(404, str(e)) 48 | try: 49 | module.run(res_id, request) 50 | except Exception as e: 51 | _logger.error('Init script {0} threw exception {1}'.format( 52 | script_name, str(e))) 53 | _logger.error('Traceback: {0}'.format(traceback.format_exc())) 54 | raise MWSServerError(500, type(e).__name__, str(e)) 55 | return jsonify( 56 | success=True, 57 | msg='Collection initialized successfully') 58 | 59 | 60 | @ivs.route('/verify/', methods=['POST']) 61 | @ratelimit 62 | def verify(script_name): 63 | res_id = _get_res_id() 64 | user_id = _get_user_id() 65 | if 'course_id' not in request.values or 'problem_id' not in request.values: 66 | raise MWSServerError(400, "Course or Problem not specified.") 67 | else: 68 | course_id = request.values['course_id'] 69 | problem_id = request.values['problem_id'] 70 | try: 71 | module = 'webapps.ivs.verify.scripts.{0}'.format(script_name) 72 | module = import_module(module) 73 | except ImportError as e: 74 | raise MWSServerError(404, str(e)) 75 | try: 76 | results = module.run(res_id, request) 77 | except Exception as e: 78 | _logger.error('Verification script {0} threw exception {1}'.format( 79 | script_name, str(e))) 80 | _logger.error('Traceback: {0}'.format(traceback.format_exc())) 81 | raise MWSServerError(500, type(e).__name__, str(e)) 82 | server_url = current_app.config.get('GRADING_SERVER_URL') 83 | # add grading credentials 84 | results.update({ 85 | 'api_key': current_app.config.get('GRADING_API_KEY'), 86 | 'api_secret': current_app.config.get('GRADING_API_SECRET') 87 | }) 88 | post_url = '{0}/api/v1/grade/{1}/{2}/{3}'.format( 89 | server_url, 90 | course_id, 91 | problem_id, 92 | user_id) 93 | response = requests.post(post_url, data=results) 94 | if response.status_code != 200: 95 | raise MWSServerError(response.status_code, response.text) 96 | return jsonify(**(json.loads(response.text))) 97 | 98 | 99 | def _get_user_id(): 100 | if current_app.config.get('DEBUG'): 101 | return request.values.get('user_id') 102 | 103 | if 'mws-track-id' not in request.cookies: 104 | raise MWSServerError(400, "Invalid request (missing cookie)") 105 | 106 | key = current_app.config.get('EDX_SHARED_KEY') 107 | s = Signer(key) 108 | try: 109 | user_id = s.unsign(request.cookies['mws-track-id']) 110 | except (BadSignature, TypeError) as e: 111 | _logger.exception(e) 112 | raise MWSServerError(403, "Invalid request (invalid cookie)") 113 | return user_id 114 | 115 | 116 | def _get_res_id(): 117 | if 'session_id' not in session: 118 | raise MWSServerError(400, "Invalid request (missing session)") 119 | session_id = session['session_id'] 120 | clients = get_db()[CLIENTS_COLLECTION] 121 | doc = clients.find_one({'session_id': session_id}, {'res_id': 1, '_id': 0}) 122 | if not doc: 123 | raise MWSServerError(500, "Resource id not associated with session") 124 | return doc['res_id'] 125 | -------------------------------------------------------------------------------- /webapps/lib/MWSServerError.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from werkzeug.exceptions import HTTPException 16 | from werkzeug.wrappers import BaseResponse 17 | 18 | from bson.json_util import dumps 19 | 20 | 21 | class MWSServerError(HTTPException): 22 | def __init__(self, error=500, message='', detail=''): 23 | self.error = error 24 | self.message = message 25 | self.detail = detail 26 | 27 | def get_body(self, environ): 28 | return dumps({ 29 | 'error': self.error, 30 | 'reason': self.message, 31 | 'detail': self.detail 32 | }) 33 | 34 | def get_response(self, environ): 35 | headers = [('Content-Type', 'application/json')] 36 | return BaseResponse(self.get_body(environ), self.error, headers) 37 | 38 | def __str__(self): 39 | return "{0} {1}".format(self.detail, self.message) 40 | -------------------------------------------------------------------------------- /webapps/lib/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | CLIENTS_COLLECTION = 'clients' 16 | -------------------------------------------------------------------------------- /webapps/lib/conf.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import yaml 17 | # The environment variable name and the key in app.config[key]. 18 | _ENVVAR = [ 19 | ('ADMIN_EMAILS', str), 20 | ('DEBUG', bool), 21 | ('DB_HOSTS', list) 22 | ] 23 | _PREFIXED_ENVVAR = [ 24 | # Web Shell server 25 | ('SERVER_HOST', str), 26 | ('SERVER_PORT', int), 27 | # Init Verification service 28 | ('IVS_EDX_SHARED_KEY', str), 29 | ('IVS_GRADING_SERVER_URL', str), 30 | ('IVS_GRADING_API_KEY', str), 31 | ('IVS_GRADING_API_SECRET', str), 32 | ('IVS_DATA_DIR', str), 33 | ('IVS_HOST', str), 34 | ('IVS_PORT', int), 35 | ] 36 | 37 | config_location_map = { 38 | 'staging': '/opt/10gen/trymongo-staging/shared/config.yml', 39 | 'prod': '/opt/10gen/trymongo-prod/shared/config.yml' 40 | } 41 | 42 | 43 | def get_deploy_config(): 44 | with open('deploy.yml', 'r') as conf_file: 45 | conf = yaml.load(conf_file) 46 | return conf 47 | 48 | 49 | def update_config(app, prefix, environment): 50 | """Overrides the flask app's configuration with envvar where applicable.""" 51 | config = {} 52 | if 'CONFIG_FILENAME' in os.environ: 53 | path = os.environ.get('CONFIG_FILENAME') 54 | try: 55 | with open(path, 'r') as _config_file: 56 | config = yaml.load(_config_file) 57 | except IOError as e: 58 | print("Expected to find a file at {0}, or {1}; proceeding without".format(path, os.path.join(os.getcwd(), path))) 59 | else: 60 | try: 61 | path = config_location_map[environment] 62 | with open(path, 'r') as f: 63 | config = yaml.load(f) 64 | except IOError as e: 65 | print("Expected to find a file at {0}, proceeding without.".format(path)) 66 | except KeyError: 67 | print("No default config file path set for the {0} environment, proceeding without".format(environment)) 68 | 69 | for key, value in config.items(): 70 | app.config[key] = value 71 | 72 | for envvar in _ENVVAR: 73 | key, t = envvar 74 | val = os.environ.get(key, app.config[key]) 75 | val = t(val) 76 | app.config[key] = val 77 | 78 | for envvar in _PREFIXED_ENVVAR: 79 | key, t = envvar 80 | if key.startswith(prefix): 81 | key = key[len(prefix) + 1:] 82 | val = os.environ.get(key, app.config[key]) 83 | val = t(val) 84 | app.config[key] = val 85 | -------------------------------------------------------------------------------- /webapps/lib/db.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | from urlparse import urlparse 17 | 18 | from flask import current_app 19 | import pymongo 20 | from pymongo.cursor_manager import CursorManager 21 | 22 | from MWSServerError import MWSServerError 23 | 24 | _logger = logging.getLogger(__name__) 25 | 26 | db = None 27 | 28 | 29 | class KeepAliveCursorManager(CursorManager): 30 | """A cursor manager that does not kill cursors 31 | """ 32 | def __init__(self, connection): 33 | super(KeepAliveCursorManager, self).__init__(connection) 34 | 35 | def close(self, cursor_id): 36 | # refuse to kill the cursor 37 | pass 38 | 39 | 40 | def get_keepalive_db(MWSExceptions=True): 41 | config = current_app.config 42 | try: 43 | client = pymongo.MongoClient( 44 | ', '.join(config.get('DB_HOSTS')), 45 | config.get('DB_PORT')) 46 | client.set_cursor_manager(KeepAliveCursorManager) 47 | db = client[config.get('DB_NAME')] 48 | if 'username' in config: 49 | db.authenticate(config.get('username'), config.get('password')) 50 | return db 51 | except Exception as e: 52 | if MWSExceptions: 53 | _logger.error(e) 54 | debug = config['DEBUG'] 55 | msg = str(e) if debug else 'An unexpected error occurred.' 56 | raise MWSServerError(500, msg) 57 | raise 58 | 59 | 60 | def get_db(MWSExceptions=True): 61 | global db 62 | config = current_app.config 63 | # TODO: Ensure MongoClient connection is still active. 64 | if db: 65 | return db 66 | try: 67 | client = pymongo.MongoClient( 68 | ', '.join(config.get('DB_HOSTS')), 69 | config.get('DB_PORT')) 70 | db = client[config.get('DB_NAME')] 71 | if 'username' in config: 72 | db.authenticate(config.get('username'), config.get('password')) 73 | return db 74 | except Exception as e: 75 | if MWSExceptions: 76 | _logger.error(e) 77 | debug = config['DEBUG'] 78 | msg = str(e) if debug else 'An unexpected error occurred.' 79 | raise MWSServerError(500, msg) 80 | raise 81 | -------------------------------------------------------------------------------- /webapps/lib/decorators.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from functools import update_wrapper 3 | from flask import current_app, session 4 | 5 | from .MWSServerError import MWSServerError 6 | from .db import get_db 7 | from . import CLIENTS_COLLECTION 8 | 9 | 10 | def user_has_access(res_id, session_id): 11 | query = {'res_id': res_id, 'session_id': session_id} 12 | coll = get_db()[CLIENTS_COLLECTION] 13 | return_value = coll.find_one(query) 14 | return False if return_value is None else True 15 | 16 | 17 | def check_session_id(f): 18 | def wrapped_function(*args, **kwargs): 19 | session_id = session.get('session_id') 20 | if session_id is None: 21 | raise MWSServerError(401, 'There is no session_id cookie') 22 | if not user_has_access(kwargs['res_id'], session_id): 23 | error = 'Session error. User does not have access to res_id' 24 | raise MWSServerError(403, error) 25 | return f(*args, **kwargs) 26 | return update_wrapper(wrapped_function, f) 27 | 28 | 29 | def ratelimit(f): 30 | def wrapped_function(*args, **kwargs): 31 | session_id = session.get('session_id') 32 | if session_id is None: 33 | error = 'Cannot rate limit without session_id cookie' 34 | raise MWSServerError(401, error) 35 | 36 | config = current_app.config 37 | coll = get_db()[config['RATELIMIT_COLLECTION']] 38 | coll.insert({'session_id': session_id, 'timestamp': datetime.now()}) 39 | 40 | delta = timedelta(seconds=config['RATELIMIT_EXPIRY']) 41 | expiry = datetime.now() - delta 42 | accesses = coll.find({'session_id': session_id, 43 | 'timestamp': {'$gt': expiry}}) 44 | if accesses.count() > config['RATELIMIT_QUOTA']: 45 | raise MWSServerError(429, 'Rate limit exceeded') 46 | 47 | return f(*args, **kwargs) 48 | return update_wrapper(wrapped_function, f) 49 | -------------------------------------------------------------------------------- /webapps/lib/log.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import logging 3 | import logging.handlers 4 | from logging.handlers import SMTPHandler 5 | 6 | 7 | class ResponseContextFilter(logging.Filter): 8 | """ 9 | This is a filter which injects the web.py context into the log. 10 | """ 11 | def filter(self, record): 12 | hostname = socket.gethostname() 13 | record.host = hostname 14 | from flask import request 15 | if request: 16 | record.method = request.method 17 | record.headers = request.headers 18 | record.environ = request.environ 19 | record.data = request.data 20 | record.path = request.path 21 | else: 22 | record.method = "" 23 | record.headers = "" 24 | record.environ = "" 25 | record.data = "" 26 | record.path = "" 27 | return True 28 | 29 | 30 | def configure_logging(app, environment): 31 | """Configures the logging module for the app. 32 | """ 33 | simple = logging.Formatter( 34 | fmt=( 35 | "%(levelname)s %(asctime)s(%(name)s#%(lineno)d)" 36 | "[%(method)s %(host)s%(path)s]" 37 | "%(data)s - %(message)s" 38 | ) 39 | ) 40 | 41 | email = logging.Formatter( 42 | fmt=( 43 | "%(asctime)s - %(levelname)s %(name)s\n" 44 | "%(pathname)s@%(funcName)s#%(lineno)d\n" 45 | "%(method)s @%(host)s%(path)s\n\n" 46 | "HEADERS: %(headers)s\n\n" 47 | "INPUT: %(data)s\n\n" 48 | "%(message)s" 49 | 50 | ) 51 | ) 52 | 53 | if environment == "devel": 54 | ch = logging.StreamHandler() 55 | ch.addFilter(ResponseContextFilter()) 56 | ch.setFormatter(simple) 57 | rl = logging.getLogger() 58 | rl.setLevel(logging.DEBUG) 59 | rl.addHandler(ch) 60 | elif environment == "staging": 61 | rl = logging.getLogger() 62 | rl.setLevel(logging.DEBUG) 63 | if app.config.get('LOG_FILE_PATH', None): 64 | fh = logging.handlers.TimedRotatingFileHandler( 65 | app.config.get('LOG_FILE_PATH'), 66 | when='midnight', 67 | backupCount=30) 68 | fh.addFilter(ResponseContextFilter()) 69 | fh.setFormatter(simple) 70 | fh.setLevel(logging.INFO) 71 | rl.addHandler(fh) 72 | elif environment == "prod": 73 | rl = logging.getLogger() 74 | rl.setLevel(logging.DEBUG) 75 | if app.config.get('ADMIN_EMAILS', None): 76 | eh = SMTPHandler('127.0.0.1', 77 | 'noc+mws@10gen.com', 78 | app.config.get('ADMIN_EMAILS'), 'MWS Failure') 79 | eh.setLevel(logging.ERROR) 80 | eh.setFormatter(email) 81 | rl.addHandler(eh) 82 | if app.config.get('LOG_FILE_PATH', None): 83 | fh = logging.handlers.TimedRotatingFileHandler( 84 | app.config.get('LOG_FILE_PATH'), 85 | when='midnight', 86 | backupCount=30) 87 | fh.addFilter(ResponseContextFilter()) 88 | fh.setFormatter(simple) 89 | fh.setLevel(logging.INFO) 90 | rl.addHandler(fh) 91 | -------------------------------------------------------------------------------- /webapps/lib/util.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from pymongo.collection import Collection 16 | from pymongo.database import Database 17 | from pymongo.errors import OperationFailure 18 | from db import get_db 19 | from MWSServerError import MWSServerError 20 | from flask import current_app 21 | from . import CLIENTS_COLLECTION 22 | 23 | import logging 24 | import os 25 | 26 | _logger = logging.getLogger(__name__) 27 | 28 | 29 | def get_collection_names(res_id): 30 | """ 31 | Get the collection names associated with a given resource id. Should not be 32 | called from within a 'with UseResId(res_id)' block. 33 | """ 34 | one = get_db()[CLIENTS_COLLECTION].find_one( 35 | {'res_id': res_id}, {'collections': 1, '_id': 0} 36 | ) 37 | return one['collections'] 38 | 39 | 40 | def get_environment(basedir): 41 | """Get the application environment context (e.g., devel, staging, prod). 42 | 43 | Determines the context by looking for a corresponding file under basedir. 44 | If there are multiple files, we prefer devel, then staging, and then prod. 45 | If no file exists, then returns the empty string.""" 46 | for env in ('devel', 'staging', 'prod'): 47 | if os.path.exists(os.path.join(basedir, env)): 48 | return env 49 | return 'devel' 50 | 51 | def to_coll_name(res_id, name): 52 | return "{0}{1}".format(res_id, name) 53 | 54 | class WrappedCollection(object): 55 | 56 | def __init__(self, db, coll_name, res_id): 57 | self.res_id = res_id 58 | self.database = db 59 | self.db = db 60 | self.unqualified_name = coll_name 61 | 62 | self.db.ensure_client_collection(coll_name) 63 | 64 | coll_name = to_coll_name(self.res_id, coll_name) 65 | self.coll = self.db.db[coll_name] 66 | 67 | 68 | @property 69 | def name(self): 70 | return self.coll.name 71 | 72 | @property 73 | def full_name(self): 74 | return self.coll.full_name 75 | 76 | @property 77 | def uuid_subtype(self): 78 | return self.coll.uuid_subtype 79 | 80 | def insert(self, *args, **kwargs): 81 | return self.coll.insert(*args, **kwargs) 82 | 83 | def update(self, *args, **kwargs): 84 | return self.coll.update(*args, **kwargs) 85 | 86 | def save(self, *args, **kwargs): 87 | return self.coll.save(*args, **kwargs) 88 | 89 | def find(self, *args, **kwargs): 90 | return self.coll.find(*args, **kwargs) 91 | 92 | def find_one(self, *args, **kwargs): 93 | return self.coll.find_one(*args, **kwargs) 94 | 95 | def count(self, *args, **kwargs): 96 | return self.coll.count(*args, **kwargs) 97 | 98 | def find_and_modify(self, *args, **kwargs): 99 | return self.coll.find_and_modify(*args, **kwargs) 100 | 101 | def aggregate(self, *args, **kwargs): 102 | return self.coll.aggregate(*args, **kwargs) 103 | 104 | def remove(self, *args, **kwargs): 105 | return self.coll.remove(*args, **kwargs) 106 | 107 | def drop(self): 108 | return self.db.drop_collection(self.unqualified_name) 109 | 110 | def size(self): 111 | try: 112 | return self.db.db.command({'collstats': self.coll.name}).get('size', 0) 113 | except OperationFailure as e: 114 | return 0 115 | 116 | 117 | class WrappedDatabase(object): 118 | 119 | def __init__(self, database, res_id): 120 | self.db = database 121 | self.res_id = res_id 122 | 123 | def __getitem__(self, name): 124 | return WrappedCollection(self, name, self.res_id) 125 | 126 | def __getattr__(self, name): 127 | return WrappedCollection(self, name, self.res_id) 128 | 129 | @property 130 | def connection(self): 131 | return self.db.connection 132 | 133 | def _fix_incoming(self, *args, **kwargs): 134 | return self.db._fix_incoming(*args, **kwargs) 135 | 136 | def drop_database(self): 137 | collections = self.db[CLIENTS_COLLECTION].find_one( 138 | {'res_id': self.res_id}, 139 | {'collections': 1} 140 | ) 141 | for collection in collections['collections']: 142 | self.drop_collection(collection) 143 | 144 | def drop_collection(self, name): 145 | self.db.drop_collection(to_coll_name(self.res_id, name)) 146 | self.remove_client_collection(name) 147 | 148 | def ensure_client_collection(self, name): 149 | limit = current_app.config.get('QUOTA_NUM_COLLECTIONS') 150 | 151 | if limit is not None: 152 | data = self.db[CLIENTS_COLLECTION].find_one( 153 | {'res_id': self.res_id}, 154 | {'collections': 1} 155 | ) 156 | 157 | if data and len(set(data['collections']).union([name])) > limit: 158 | raise MWSServerError(429, 'Max number of collections exceeded') 159 | 160 | self.db[CLIENTS_COLLECTION].update( 161 | {'res_id': self.res_id}, 162 | {'$addToSet': {'collections': name}}, 163 | multi=True 164 | ) 165 | 166 | def remove_client_collection(self, name): 167 | self.db[CLIENTS_COLLECTION].update( 168 | {'res_id': self.res_id}, 169 | {'$pull': {'collections': name}}, 170 | multi=True 171 | ) 172 | 173 | 174 | class UseResId: 175 | def __init__(self, res_id, db=None): 176 | self.res_id = str(res_id) 177 | self.db = db 178 | 179 | def __enter__(self): 180 | if self.db is None: 181 | db = get_db() 182 | else: 183 | db = self.db 184 | return WrappedDatabase(db, self.res_id) 185 | 186 | def __exit__(self, exc_type, exc_val, exc_tb): 187 | pass 188 | -------------------------------------------------------------------------------- /webapps/server/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .views import mws 16 | 17 | -------------------------------------------------------------------------------- /webapps/server/app.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | from flask import Flask, redirect 17 | import logging 18 | 19 | from webapps.lib.conf import update_config 20 | from webapps.lib.log import configure_logging 21 | from webapps.lib.util import get_environment 22 | from webapps.server import crontab 23 | from webapps.server.views import mws 24 | 25 | 26 | _logger = logging.getLogger(__name__) 27 | 28 | 29 | if 'MWS_SERVER_TESTING' in os.environ: 30 | environment = 'test' 31 | else: 32 | _here = os.path.dirname(os.path.abspath(__file__)) 33 | environment = get_environment(_here) 34 | 35 | _flask_serve_static = 'MWS_FLASK_STATIC' in os.environ 36 | if _flask_serve_static: 37 | static_options = {'static_folder': '../../frontend', 38 | 'static_url_path': ''} 39 | else: 40 | static_options = {} 41 | 42 | 43 | def create_app(): 44 | app = Flask(__name__, **static_options) 45 | if _flask_serve_static: 46 | app.add_url_rule(rule='/', view_func=lambda: redirect('index.html', 302)) 47 | app.config.from_object('webapps.configs.server') 48 | # Overrides the config with any environment variables that might 49 | # be set 50 | update_config(app, 'SERVER', environment) 51 | configure_logging(app, environment) 52 | app.register_blueprint(mws) 53 | crontab.run_scheduler(app) 54 | return app 55 | 56 | 57 | app = application = create_app() 58 | 59 | 60 | if __name__ == '__main__': 61 | app.run() 62 | -------------------------------------------------------------------------------- /webapps/server/crontab.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 10gen Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from datetime import datetime, timedelta 16 | from apscheduler.scheduler import Scheduler 17 | from webapps.lib.db import get_db 18 | from webapps.lib.util import to_coll_name 19 | 20 | EXPIRE_SESSION_EVERY = 600 21 | EXPIRE_SESSION_DURATION = 1800 22 | 23 | 24 | def run_scheduler(app): 25 | scheduler = Scheduler() 26 | 27 | expire_wrapper = lambda: expire_sessions(app) 28 | scheduler.add_interval_job(expire_wrapper, seconds=EXPIRE_SESSION_EVERY) 29 | 30 | scheduler.start() 31 | print "APScheduler started successfully" 32 | 33 | 34 | def expire_sessions(app): 35 | with app.app_context(): 36 | db = get_db(MWSExceptions=False) 37 | delta = timedelta(seconds=EXPIRE_SESSION_DURATION) 38 | exp = datetime.now() - delta 39 | sessions = db.clients.find({'timestamp': {'$lt': exp}}) 40 | for sess in sessions: 41 | db.clients.remove(sess) 42 | # Todo: Only remove collections if no one else is using this res_id 43 | res_id = sess['res_id'] 44 | for c in sess['collections']: 45 | db.drop_collection(to_coll_name(res_id, c)) 46 | app.logger.info('Timed out expired sessions dead before %s' % exp) 47 | --------------------------------------------------------------------------------