Repository: filearts/plunker Branch: master Commit: 84071165b63e Files: 360 Total size: 8.7 MB Directory structure: gitextract_2z_jhlig/ ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── config.sample.json ├── package.json ├── server.js └── servers/ ├── api/ │ ├── .gitignore │ ├── .npmignore │ ├── errors.coffee │ ├── index.coffee │ ├── lib/ │ │ └── database.coffee │ ├── middleware/ │ │ ├── cache.coffee │ │ ├── cors.coffee │ │ ├── json.coffee │ │ ├── session.coffee │ │ └── user.coffee │ ├── package.json │ ├── schema/ │ │ ├── packages/ │ │ │ ├── create.json │ │ │ └── update.json │ │ └── plunks/ │ │ ├── create.json │ │ └── update.json │ └── server.js ├── embed/ │ ├── .gitignore │ ├── assets/ │ │ ├── bootstrap/ │ │ │ ├── js/ │ │ │ │ ├── bootstrap-affix.js │ │ │ │ ├── bootstrap-alert.js │ │ │ │ ├── bootstrap-all.js │ │ │ │ ├── bootstrap-button.js │ │ │ │ ├── bootstrap-carousel.js │ │ │ │ ├── bootstrap-collapse.js │ │ │ │ ├── bootstrap-dropdown.js │ │ │ │ ├── bootstrap-modal.js │ │ │ │ ├── bootstrap-popover.js │ │ │ │ ├── bootstrap-scrollspy.js │ │ │ │ ├── bootstrap-tab.js │ │ │ │ ├── bootstrap-tooltip.js │ │ │ │ ├── bootstrap-transition.js │ │ │ │ └── bootstrap-typeahead.js │ │ │ └── less/ │ │ │ ├── accordion.less │ │ │ ├── alerts.less │ │ │ ├── bootstrap.less │ │ │ ├── breadcrumbs.less │ │ │ ├── button-groups.less │ │ │ ├── buttons.less │ │ │ ├── carousel.less │ │ │ ├── close.less │ │ │ ├── code.less │ │ │ ├── component-animations.less │ │ │ ├── dropdowns.less │ │ │ ├── font-awesome.less │ │ │ ├── forms.less │ │ │ ├── grid.less │ │ │ ├── hero-unit.less │ │ │ ├── labels-badges.less │ │ │ ├── layouts.less │ │ │ ├── mixins.less │ │ │ ├── modals.less │ │ │ ├── navbar.less │ │ │ ├── navs.less │ │ │ ├── pager.less │ │ │ ├── pagination.less │ │ │ ├── popovers.less │ │ │ ├── progress-bars.less │ │ │ ├── reset.less │ │ │ ├── responsive-1200px-min.less │ │ │ ├── responsive-767px-max.less │ │ │ ├── responsive-768px-979px.less │ │ │ ├── responsive-navbar.less │ │ │ ├── responsive-utilities.less │ │ │ ├── responsive.less │ │ │ ├── scaffolding.less │ │ │ ├── sprites.less │ │ │ ├── tables.less │ │ │ ├── thumbnails.less │ │ │ ├── tooltip.less │ │ │ ├── type.less │ │ │ ├── utilities.less │ │ │ ├── variables.less │ │ │ └── wells.less │ │ ├── css/ │ │ │ ├── components/ │ │ │ │ └── navbar.less │ │ │ ├── pages/ │ │ │ │ └── embed.less │ │ │ └── vendor/ │ │ │ └── prettify.less │ │ └── js/ │ │ ├── pages/ │ │ │ └── embed.coffee │ │ ├── services/ │ │ │ ├── importer.coffee │ │ │ ├── plunks.coffee │ │ │ └── url.coffee │ │ └── vendor/ │ │ ├── angular-sanitize.js │ │ ├── angular-ui.js │ │ ├── angular.js │ │ ├── jquery.cookie.js │ │ ├── jquery.js │ │ ├── overthrow.js │ │ ├── prettify.js │ │ └── showdown.js │ ├── index.coffee │ ├── middleware/ │ │ └── expose.coffee │ ├── package.json │ ├── server.js │ └── views/ │ └── embed.jade ├── raw/ │ ├── .gitignore │ ├── index.coffee │ ├── package.json │ ├── server.js │ └── views/ │ └── directory.jade ├── run/ │ ├── .gitignore │ ├── index.coffee │ ├── middleware/ │ │ ├── cors.coffee │ │ └── json.coffee │ ├── package.json │ ├── schema/ │ │ └── previews/ │ │ └── create.json │ ├── server.js │ └── views/ │ └── directory.jade └── www/ ├── .gitignore ├── assets/ │ ├── bootstrap/ │ │ ├── js/ │ │ │ ├── bootstrap-affix.js │ │ │ ├── bootstrap-alert.js │ │ │ ├── bootstrap-all.js │ │ │ ├── bootstrap-button.js │ │ │ ├── bootstrap-carousel.js │ │ │ ├── bootstrap-collapse.js │ │ │ ├── bootstrap-dropdown.js │ │ │ ├── bootstrap-modal.js │ │ │ ├── bootstrap-popover.js │ │ │ ├── bootstrap-scrollspy.js │ │ │ ├── bootstrap-tab.js │ │ │ ├── bootstrap-tooltip.js │ │ │ ├── bootstrap-transition.js │ │ │ └── bootstrap-typeahead.js │ │ └── less/ │ │ ├── accordion.less │ │ ├── alerts.less │ │ ├── bootstrap.less │ │ ├── breadcrumbs.less │ │ ├── button-groups.less │ │ ├── buttons.less │ │ ├── carousel.less │ │ ├── close.less │ │ ├── code.less │ │ ├── component-animations.less │ │ ├── dropdowns.less │ │ ├── font-awesome.less │ │ ├── forms.less │ │ ├── grid.less │ │ ├── hero-unit.less │ │ ├── labels-badges.less │ │ ├── layouts.less │ │ ├── mixins.less │ │ ├── modals.less │ │ ├── navbar.less │ │ ├── navs.less │ │ ├── pager.less │ │ ├── pagination.less │ │ ├── popovers.less │ │ ├── progress-bars.less │ │ ├── reset.less │ │ ├── responsive-1200px-min.less │ │ ├── responsive-767px-max.less │ │ ├── responsive-768px-979px.less │ │ ├── responsive-navbar.less │ │ ├── responsive-utilities.less │ │ ├── responsive.less │ │ ├── scaffolding.less │ │ ├── sprites.less │ │ ├── tables.less │ │ ├── thumbnails.less │ │ ├── tooltip.less │ │ ├── type.less │ │ ├── utilities.less │ │ ├── variables.less │ │ └── wells.less │ ├── css/ │ │ ├── common/ │ │ │ └── style.less │ │ ├── components/ │ │ │ ├── discussion.less │ │ │ ├── gallery.less │ │ │ ├── importer.less │ │ │ ├── multipanel.less │ │ │ ├── navbar.less │ │ │ ├── previewer.less │ │ │ ├── share.less │ │ │ ├── sidebar.less │ │ │ ├── statusbar.less │ │ │ ├── streamer.less │ │ │ ├── tags.less │ │ │ ├── toolbar.less │ │ │ └── userpanel.less │ │ ├── pages/ │ │ │ ├── editor.less │ │ │ ├── landing.less │ │ │ └── preview.less │ │ └── vendor/ │ │ ├── jquery-layout.less │ │ └── prettify.less │ └── js/ │ ├── directives/ │ │ ├── ace.coffee │ │ ├── builder.coffee │ │ ├── card.coffee │ │ ├── chat.coffee │ │ ├── discussion.coffee │ │ ├── layout.coffee │ │ ├── multipanel.coffee │ │ ├── share.coffee │ │ ├── statusbar.coffee │ │ ├── toolbar.coffee │ │ └── userpanel.coffee │ ├── pages/ │ │ ├── editor.coffee │ │ └── landing.coffee │ ├── services/ │ │ ├── catalogue.coffee │ │ ├── downloader.coffee │ │ ├── importer.coffee │ │ ├── modes.coffee │ │ ├── notifier.coffee │ │ ├── pages.coffee │ │ ├── panels.coffee │ │ ├── panes/ │ │ │ ├── builder.coffee │ │ │ ├── explore.coffee │ │ │ ├── lint.coffee │ │ │ ├── previewer.coffee │ │ │ ├── readme.coffee │ │ │ └── streamer.coffee │ │ ├── panes-dev/ │ │ │ ├── about.coffee │ │ │ ├── compiler.coffee │ │ │ ├── discussion.coffee │ │ │ └── settings.coffee │ │ ├── plunks.coffee │ │ ├── scratch.coffee │ │ ├── session.coffee │ │ ├── url.coffee │ │ └── whitelist.coffee │ ├── socialbuttons.js │ └── vendor/ │ ├── ace/ │ │ ├── ace.js │ │ ├── keybinding-emacs.js │ │ ├── keybinding-vim.js │ │ ├── mode-c9search.js │ │ ├── mode-c_cpp.js │ │ ├── mode-clojure.js │ │ ├── mode-coffee.js │ │ ├── mode-coldfusion.js │ │ ├── mode-csharp.js │ │ ├── mode-css.js │ │ ├── mode-diff.js │ │ ├── mode-glsl.js │ │ ├── mode-golang.js │ │ ├── mode-groovy.js │ │ ├── mode-haxe.js │ │ ├── mode-html.js │ │ ├── mode-jade.js │ │ ├── mode-java.js │ │ ├── mode-javascript.js │ │ ├── mode-json.js │ │ ├── mode-jsp.js │ │ ├── mode-jsx.js │ │ ├── mode-latex.js │ │ ├── mode-less.js │ │ ├── mode-liquid.js │ │ ├── mode-lua.js │ │ ├── mode-luapage.js │ │ ├── mode-markdown.js │ │ ├── mode-ocaml.js │ │ ├── mode-perl.js │ │ ├── mode-pgsql.js │ │ ├── mode-php.js │ │ ├── mode-powershell.js │ │ ├── mode-python.js │ │ ├── mode-ruby.js │ │ ├── mode-scad.js │ │ ├── mode-scala.js │ │ ├── mode-scss.js │ │ ├── mode-sh.js │ │ ├── mode-sql.js │ │ ├── mode-svg.js │ │ ├── mode-tcl.js │ │ ├── mode-text.js │ │ ├── mode-textile.js │ │ ├── mode-xml.js │ │ ├── mode-xquery.js │ │ ├── mode-yaml.js │ │ ├── theme-ambiance.js │ │ ├── theme-chrome.js │ │ ├── theme-clouds.js │ │ ├── theme-clouds_midnight.js │ │ ├── theme-cobalt.js │ │ ├── theme-crimson_editor.js │ │ ├── theme-dawn.js │ │ ├── theme-dreamweaver.js │ │ ├── theme-eclipse.js │ │ ├── theme-github.js │ │ ├── theme-idle_fingers.js │ │ ├── theme-kr_theme.js │ │ ├── theme-merbivore.js │ │ ├── theme-merbivore_soft.js │ │ ├── theme-mono_industrial.js │ │ ├── theme-monokai.js │ │ ├── theme-pastel_on_dark.js │ │ ├── theme-solarized_dark.js │ │ ├── theme-solarized_light.js │ │ ├── theme-textmate.js │ │ ├── theme-tomorrow.js │ │ ├── theme-tomorrow_night.js │ │ ├── theme-tomorrow_night_blue.js │ │ ├── theme-tomorrow_night_bright.js │ │ ├── theme-tomorrow_night_eighties.js │ │ ├── theme-twilight.js │ │ ├── theme-vibrant_ink.js │ │ ├── theme-xcode.js │ │ ├── worker-coffee.js │ │ ├── worker-css.js │ │ ├── worker-javascript.js │ │ ├── worker-json.js │ │ └── worker-xquery.js │ ├── angular-cookies.js │ ├── angular-jq.js │ ├── angular-resource.js │ ├── angular-sanitize.js │ ├── angular-ui.js │ ├── angular.js │ ├── backbone.js │ ├── beautify.js │ ├── handlebars.js │ ├── jquery.cookie.js │ ├── jquery.history.js │ ├── jquery.js │ ├── jquery.layout.js │ ├── jquery.lazyload.js │ ├── jquery.timeago.js │ ├── jquery.ui.custom.js │ ├── jszip.js │ ├── noty/ │ │ ├── jquery.noty.js │ │ ├── layouts/ │ │ │ ├── bottom.js │ │ │ ├── bottomCenter.js │ │ │ ├── bottomLeft.js │ │ │ ├── bottomRight.js │ │ │ ├── center.js │ │ │ ├── centerLeft.js │ │ │ ├── centerRight.js │ │ │ ├── inline.js │ │ │ ├── top.js │ │ │ ├── topCenter.js │ │ │ ├── topLeft.js │ │ │ └── topRight.js │ │ ├── promise.js │ │ └── themes/ │ │ └── default.js │ ├── overthrow.js │ ├── page.js │ ├── postmessage.js │ ├── prettify.js │ ├── semver.js │ ├── showdown.js │ └── underscore.js ├── index.coffee ├── middleware/ │ ├── error.coffee │ ├── expose.coffee │ └── session.coffee ├── models/ │ └── user.coffee ├── package.json ├── server.js └── views/ ├── auth/ │ ├── error.jade │ └── success.jade ├── editor.jade ├── landing.jade └── partials/ ├── discuss.jade ├── home.jade ├── preview.jade ├── tag.jade ├── tags.jade ├── user.jade └── users.jade ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ lib-cov *.seed *.log *.csv *.dat *.out *.pid *.gz pids logs results tests config.json node_modules npm-debug.log .c9revisions builtAssets ================================================ FILE: .gitmodules ================================================ [submodule "servers/www/assets/select2"] path = servers/www/assets/select2 url = git://github.com/ivaynberg/select2.git ================================================ FILE: LICENSE ================================================ Copyright (C) 2015 Filearts Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Plunker [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/filearts/plunker?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) The next generation of lightweight collaborative online editing. #### WARNING: This repository does not contain the code for what you see running on http://plnkr.co > The current code for Plunker is in the repositories listed below Originally, Plunker was coded in a single repository with different sub-servers existing in the `/servers` path. The entire application was run on a single server. However, with increasing popularity, reality decided to come hang out and make everyone's lives difficult. The solution was simple; since the components of Plunker were designed as 'sub-servers', it should be easy to split them out and run them separately. However, having different logical entities with different functions in the same repository doesn't make sense. I decided to create separate repositories for each of the Plunker servers that are currently deployed on Nodejitsu. They are as follows: #### Plunker component repositories * [plunker_api](//github.com/filearts/plunker_api) The server that connects to a mongodb database and serves requests over a restful api. * [plunker_www](//github.com/filearts/plunker_www) The server that is responsible for hosting and running the front-end that users see and touch everyday. * [plunker-run-plugin](//github.com/ggoodman/plunker-run-plugin) The server that allows for previewing of plunks and temporary previews and also does the dynamic transpilation. * [plunker_collab](//github.com/filearts/plunker_collab) The server that serves the code necessary for collaborative coding as well as doing the actual operational transformation over a browserchannel connection. * [plunker_embed](//github.com/filearts/plunker_embed) The server that hosts the embedded views of plunks. ### Plunker config files Each server, once cloned locally, requires one or two `config.json` files to run. **Servers that use the environment-specific config files `config.development.json` and `config.production.json`:** * plunker_api * plunker_www * plunker_run * plunker_collab Only `plunker_embed` uses a single `config.json` file. **Sample configuration file:** > Not all fields are required by each server, but if all are present no harm *should* come to any small animals. ```javascript { "host": "hostname.com", "url": { "www": "http://hostname.com", "collab": "http://collab.hostname.com", "api": "http://api.hostname.com", "embed": "http://embed.hostname.com", "run": "http://run.hostname.com", "carbonadsH": "OOPS, this is pretty specific to my current deploy", "carbonadsV": "OOPS, this is pretty specific to my current deploy" }, "port": 8080, "oauth": { "github": { "id": "series_of_random_chars", "secret": "longer_series_of_random_chars" } } } ``` # Everything below this point is out of date or incorrect! > ...And there be dragons # Usage ``` git clone git://github.com/filearts/plunker.git git submodule update --init npm install node server.js ``` ## Editor API ### `POST /edit/` You can send a `POST` request to `/edit/` to bootstrap the editor with the basic structure of a plunk. The JSON format for this is described below. ```javascript { "description": "Description of Plunk", "tags": ["array", "of", "tags"], "files": [ { "filename": "index.html", "content": "" }, { "filename": "script.js", "content": "alert('hello world');" } ] } ``` ## License Copyright [Filearts](https://github.com/filearts) ================================================ FILE: config.sample.json ================================================ { "host": "localhost", // (Required) The hostname at which Plunk will be run "nosubdomains": true, // Run Plunker off of a single domain (instead of {api,run,embed,raw}.host) "oauth": { "github": { // To enable logging in via github "id": "111111111111111", "secret": "11111111111111111111111111111111" } }, "mongodb": { "hostname": "localhost", "port": 27017, "pathname": "plunker", "auth": "plunker:plunker" } } ================================================ FILE: package.json ================================================ { "name": "plunker", "subdomain": "plunker", "scripts": { "start": "server.js" }, "engines": { "node": "0.6.x" }, "version": "0.3.13-4", "private": true, "dependencies": { "coffee-script": "1.3.x", "express": "2.5.x", "gzippo": "0.1.x", "jade": "0.25.x", "less": "1.3.x", "mime": "1.2.x", "underscore": "1.3.x", "backbone": "0.9.x", "connect-assets": "2.2.x", "share": "0.5.x", "express-subdomains": "0.0.x", "authom": "0.4.x", "nconf": "0.5.x", "json-schema": "https://github.com/kriszyp/json-schema/tarball/master", "request": "2.9.x", "connect": "1.x", "async": "0.1.x", "mongoose": "3.x", "lru-cache": "1.1.x" }, "bundledDependencies": [ "json-schema" ] } ================================================ FILE: server.js ================================================ // Everything starts better with coffee var coffee = require("coffee-script"); var express = require("express"); var nconf = require("nconf").use("memory") .argv() .env() .file({file: "config.json"}) .defaults({ "PORT": 8080 }); if (!nconf.get("host")) { console.error("The host option is required for Plunker to start"); } else { var host = nconf.get("host"); // Configure global paths if (nconf.get("nosubdomains")) { nconf.set("url:www", "http://" + host); nconf.set("url:raw", "http://" + host + "/raw"); nconf.set("url:run", "http://" + host + "/run"); nconf.set("url:api", "http://" + host + "/api"); } else { nconf.set("url:www", "http://" + host); nconf.set("url:raw", "http://raw." + host); nconf.set("url:run", "http://run." + host); nconf.set("url:api", "http://api." + host); } // Create and start the parent server express.createServer() //.use(express.logger()) .use(require("express-subdomains").use("raw").use("api").use("run").use("embed").middleware) .use("/api", require("./servers/api")) .use("/raw", require("./servers/raw")) .use("/run", require("./servers/run")) .use("/embed", require("./servers/embed")) .use(require("./servers/www")) .listen(nconf.get("PORT")); console.log("Started plunker in", nconf.get("NODE_ENV") || "development", "at", nconf.get("host"), "on port", nconf.get("PORT"), "using subdomains:", !nconf.get("nosubdomains")); } ================================================ FILE: servers/api/.gitignore ================================================ lib-cov *.seed *.log *.csv *.dat *.out *.pid *.gz pids logs results tests node_modules npm-debug.log .c9revisions builtAssets ================================================ FILE: servers/api/.npmignore ================================================ lib-cov *.seed *.log *.csv *.dat *.out *.pid *.gz pids logs results tests node_modules npm-debug.log .c9revisions builtAssets ================================================ FILE: servers/api/errors.coffee ================================================ class APIError extends Error toJSON: -> {@code, @message} module.exports = ParseError: class extends APIError constructor: -> @code = 400 @message = "Problems parsing JSON" ValidationError: class extends APIError constructor: (@errors) -> @code = 422 @message = "Validation failed" toJSON: -> {@errors, @code, @message} NotFound: class extends APIError constructor: -> @code = 404 @message = "Not found" PermissionDenied: class extends APIError constructor: -> @code = 404 @message = "Permission denied" InternalServerError: class extends APIError constructor: -> @code = 500 @message = "Internal server error" ================================================ FILE: servers/api/index.coffee ================================================ nconf = require("nconf") request = require("request") mime = require("mime") express = require("express") url = require("url") querystring = require("querystring") _ = require("underscore")._ validator = require("json-schema") mime = require("mime") gate = require("json-gate") semver = require("semver") apiErrors = require("./errors") apiUrl = nconf.get('url:api') module.exports = app = express.createServer() genid = (len = 16, prefix = "", keyspace = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") -> prefix += keyspace.charAt(Math.floor(Math.random() * keyspace.length)) while len-- > 0 prefix database = require("./lib/database") Session = database.model("Session") User = database.model("User") Plunk = database.model("Plunk") Package = database.model("Package") PRUNE_FREQUENCY = 1000 * 60 * 60 * 6 # Prune the sessions every 6 hours SCORE_INCREMENT = 1000 * 60 * 60 * 6 # Each vote bumps the plunk forward 6 hours pruneSessions = -> console.log "Pruning sessions" Session.prune() setInterval pruneSessions, PRUNE_FREQUENCY pruneSessions() app.configure -> app.use require("./middleware/cors").middleware() app.use require("./middleware/cache").middleware() app.use require("./middleware/json").middleware() app.use require("./middleware/session").middleware(sessions: database.model("Session")) app.use app.router app.use (err, req, res, next) -> json = if err.toJSON? then err.toJSON() else message: err.message or "Unknown error" code: err.code or 500 res.json(json, json.code) throw err app.set "jsonp callback", true ### # RESTful sessions ### createSession = (token, user, cb) -> session = new Session last_access: new Date keychain: {} session.user = user if user session.save (err) -> cb(err, session) # Convenience endpoint to get the current session or create a new one app.get "/session", (req, res, next) -> res.header "Cache-Control", "no-cache" if req.session then res.json(req.session) else createSession null, null, (err, session) -> if err then next(err) else res.json(session, 201) app.post "/sessions", (req, res, next) -> createSession null, null, (err, session) -> if err then next(err) else res.json(session, 201) app.get "/sessions/:id", (req, res, next) -> Session.findById(req.params.id).populate("user").exec (err, session) -> if err then next(err) else unless session then next(new apiErrors.NotFound) else if Date.now() - session.last_access.valueOf() > nconf.get("session:max_age") then next(new apiErrors.NotFound) else unless session.user then res.json(session.toJSON()) else User.findById session.user, (err, user) -> if err then next(err) else res.json(_.extend(session, user: user.toJSON())) app.del "/sessions/:id/user", (req, res, next) -> Session.findById req.params.id, (err, session) -> if err then next(err) else unless session then next(new apiErrors.NotFound) else session.user = null session.save (err) -> if err then next(err) else res.json(session) app.post "/sessions/:id/user", (req, res, next) -> Session.findById req.params.id, (err, session) -> if err then next(new apiErrors.NotFound) else unless token = req.param("token") then next(new apiErrors.MissingArgument("token")) else sessid = req.param("id") request.get "https://api.github.com/user?access_token=#{token}", (err, response, body) -> return next(new apiErrors.Error(err)) if err return next(new apiErrors.PermissionDenied) if response.status >= 400 try body = JSON.parse(body) catch e return next(new apiErrors.ParseError) service_id = "github:#{body.id}" createUser = (cb) -> user_json = login: body.login gravatar_id: body.gravatar_id service_id: service_id User.create(user_json, cb) withUser = (err, user) -> if err then next(err) else session.user = user session.auth = service_name: "github" service_token: token session.save (err) -> if err then next(err) else res.json(_.extend(session.toJSON(), user: user.toJSON()), 201) User.findOne { service_id: service_id }, (err, user) -> unless err or not user then withUser(null, user) else createUser(withUser) ### # Plunks ### ownsPlunk = (session, json) -> owner = false if session owner ||= !!(json.user and session.user and json.user.login is session.user.login) owner ||= !!(session.keychain and session.keychain.id(json.id)?.token is json.token) owner preparePlunk = (session, plunk, populate = {}) -> json = _.extend plunk.toJSON(), populate delete json.token unless ownsPlunk(session, json) delete json.voters if json.files then json.files = do -> files = {} for file in json.files file.raw_url = "#{json.raw_url}#{file.filename}" files[file.filename] = file files json.thumbed = session?.user? and plunk.voters.indexOf(""+session.user._id) >= 0 json preparePlunks = (session, plunks) -> _.map plunks, (plunk) -> preparePlunk(session, plunk) fetchPlunks = (options, req, res, next) -> page = parseInt(req.param("p", "1"), 10) limit = parseInt(req.param("pp", "8")) options.baseUrl ||= "#{apiUrl}/plunks" search = options.query or {} if req.user search.$or = [ 'private': $ne: true , user: req.user._id ] else search.private = $ne: true query = Plunk.find(search) query.sort(options.sort or {updated_at: -1}) query.select("-files") query.populate("user").paginate page, limit, (err, plunks, count, pages, current) -> if err then next(err) else link = [] if current < pages link.push "<#{options.baseUrl}?p=#{page+1}&pp=#{limit}>; rel=\"next\"" link.push "<#{options.baseUrl}?p=#{pages}&pp=#{limit}>; rel=\"last\"" if current > 1 link.push "<#{options.baseUrl}?p=#{page-1}&pp=#{limit}>; rel=\"prev\"" link.push "<#{options.baseUrl}?p=1&pp=#{limit}>; rel=\"first\"" res.header("Link", link.join(", ")) if link.length res.json(preparePlunks(req.session, plunks)) # List plunks app.get "/plunks", (req, res, next) -> fetchPlunks({}, req, res, next) # List plunks app.get "/plunks/trending", (req, res, next) -> options = baseUrl: "#{apiUrl}/plunks/trending" sort: "-score -updated_at" fetchPlunks(options, req, res, next) # List plunks app.get "/plunks/popular", (req, res, next) -> options = baseUrl: "#{apiUrl}/plunks/popular" sort: "-thumbs -updated_at" fetchPlunks(options, req, res, next) # Create plunk app.post "/plunks", (req, res, next) -> json = req.body schema = require("./schema/plunks/create") {valid, errors} = validator.validate(json, schema) # Despite its awesomeness, revalidator does not support disallow or additionalProperties; we need to check plunk.files size if json.files and _.isEmpty(json.files) valid = false errors.push attribute: "minProperties" property: "files" message: "A minimum of one file is required" unless valid then next(new apiErrors.ValidationError(errors)) else json.files = _.map json.files, (file, filename) -> filename: filename content: file.content mime: mime.lookup(filename, "text/plain") json.tags = _.uniq(json.tags) if json.tags plunk = new Plunk(json) plunk.user = req.user._id if req.user # TODO: This is inefficient as the number space fills up; consider: http://www.faqs.org/patents/app/20090063601 # Keep generating new ids until not taken savePlunk = -> plunk._id = if json.private then genid(20) else genid(6) plunk.save (err) -> if err if err.code is 11000 then savePlunk() else next(err) else unless req.user and req.session and req.session.keychain req.session.keychain.push _id: plunk._id, token: plunk.token req.session.save() populate = {} populate.user = req.user.toJSON() if req.user # User is not populated so we shove it in **UGLY** res.json(preparePlunk(req.session, plunk, populate), 201) savePlunk() # Read plunk app.get "/plunks/:id", (req, res, next) -> Plunk.findById(req.params.id).populate("user").exec (err, plunk) -> if err or not plunk then next(new apiErrors.NotFound) else res.json(preparePlunk(req.session, plunk)) # Update plunk app.post "/plunks/:id", (req, res, next) -> Plunk.findById(req.params.id).populate("user").exec (err, plunk) -> if err or not plunk or not ownsPlunk(req.session, plunk) then next(new apiErrors.NotFound) else json = req.body schema = require("./schema/plunks/update") {valid, errors} = validator.validate(json, schema) # Despite its awesomeness, validator does not support disallow or additionalProperties; we need to check plunk.files size if json.files and _.isEmpty(json.files) valid = false errors.push attribute: "minProperties" property: "files" message: "A minimum of one file is required" unless valid then next(new apiErrors.ValidationError(errors)) else oldFiles = {} for file, index in plunk.files oldFiles[file.filename] = file for filename, file of json.files # Attempt to delete if file is null oldFiles[filename].remove() if oldFiles[filename] # Modification to an existing file else if old = oldFiles[filename] if file.filename old.filename = file.filename old.mime = mime.lookup(file.filename, "text/plain") if file.content? old.content = file.content if file.filename or file.content then old.save() # New file; handle only if content provided else if file.content plunk.files.push filename: filename content: file.content if json.tags plunk.tags ||= [] for tagname, add of json.tags if add plunk.tags.push(tagname) else plunk.tags.splice(idx, 1) if (idx = plunk.tags.indexOf(tagname)) >= 0 plunk.tags = _.uniq(plunk.tags) plunk.updated_at = new Date plunk.description = json.description if json.description plunk.user = req.user._id if req.user plunk.save (err) -> if err then next(new apiErrors.InternalServerError(err)) else populate = {} populate.user = req.user.toJSON() if req.user res.json(preparePlunk(req.session, plunk, populate)) # Obtain a list of a plunk's forks app.get "/plunks/:id/forks", (req, res, next) -> Plunk.findOne({_id: req.params.id}).exec (err, plunk) -> if err or not plunk then next(new apiErrors.NotFound) else options = query: {fork_of: req.params.id} baseUrl: "#{apiUrl}/plunk/#{req.params.id}/forks" sort: "-updated_at" fetchPlunks(options, req, res, next) # Give a thumbs-up to a plunk app.post "/plunks/:id/thumb", (req, res, next) -> unless req.user then return next(new apiErrors.NotFound) Plunk.findOne(_id: req.params.id).where("voters").ne(req.user).exec (err, plunk) -> if err or not plunk then next(new apiErrors.NotFound) else plunk.score ||= plunk.created_at.valueOf() plunk.thumbs ||= 0 plunk.voters.addToSet(req.user._id) plunk.score += SCORE_INCREMENT plunk.thumbs++ plunk.save (err) -> if err then next(new apiErrors.InternalServerError(err)) else res.json({ thumbs: plunk.get("thumbs"), score: plunk.score}, 201) # Remove a thumbs-up to a plunk app.del "/plunks/:id/thumb", (req, res, next) -> unless req.user then return next(new apiErrors.NotFound) Plunk.findOne(_id: req.params.id).where("voters").equals(req.user).exec (err, plunk) -> if err or not plunk then next(new apiErrors.NotFound) else plunk.voters.remove(req.user) plunk.score -= SCORE_INCREMENT plunk.thumbs-- plunk.save (err) -> if err then next(new apiErrors.InternalServerError(err)) else res.json({ thumbs: plunk.get("thumbs"), score: plunk.score}, 200) # Fork an existing plunk app.post "/plunks/:id/forks", (req, res, next) -> Plunk.findById(req.params.id).populate("user").exec (err, parent) -> if err or not parent then next(new apiErrors.NotFound) else json = req.body schema = require("./schema/plunks/create") {valid, errors} = validator.validate(json, schema) # Despite its awesomeness, revalidator does not support disallow or additionalProperties; we need to check plunk.files size if json.files and _.isEmpty(json.files) valid = false errors.push attribute: "minProperties" property: "files" message: "A minimum of one file is required" unless valid then next(new apiErrors.ValidationError(errors)) else json.files = _.map json.files, (file, filename) -> filename: filename content: file.content mime: mime.lookup(filename, "text/plain") json.tags = _.uniq(json.tags) if json.tags plunk = new Plunk(json) plunk.user = req.user._id if req.user plunk.fork_of = parent._id # TODO: This is inefficient as the number space fills up; consider: http://www.faqs.org/patents/app/20090063601 # Keep generating new ids until not taken savePlunk = -> plunk._id = if json.private then genid(20) else genid(6) plunk.save (err) -> if err if err.code is 11000 then savePlunk() else next(err) else # Update syntax to avoid triggering auto-update of updated_at on parent parent.forks.push(plunk._id) parent.save() if not req.user and req.session and req.session.keychain req.session.keychain.push _id: plunk._id, token: plunk.token req.session.save() populate = {} populate.user = req.user.toJSON() if req.user res.json(preparePlunk(req.session, plunk, populate), 201) savePlunk() # Delete plunk app.del "/plunks/:id", (req, res, next) -> Plunk.findById(req.params.id).populate("user").exec (err, plunk) -> if err or not plunk or not ownsPlunk(req.session, plunk) then next(new apiErrors.NotFound) else plunk.remove -> res.send(204) fetchUser = (req, res, next) -> User.findOne({login: req.params.username}).exec (err, user) -> if err or not user then next(new apiErrors.NotFound) else req.found_user = user next() # Fetch a user app.get "/users/:username", fetchUser, (req, res, next) -> res.json(req.found_user) # List a user's plunks app.get "/users/:username/plunks", fetchUser, (req, res, next) -> options = query: {user: req.found_user._id} baseUrl: "#{apiUrl}/users/#{req.params.username}/plunks" fetchPlunks(options, req, res, next) # List plunks a user gave a thumbs-up app.get "/users/:username/thumbed", fetchUser, (req, res, next) -> options = query: {voters: req.found_user._id} baseUrl: "#{apiUrl}/users/#{req.params.username}/thumbed" fetchPlunks(options, req, res, next) app.get "/tags", (req, res, next) -> Plunk.aggregate [ $unwind: "$tags" , $group: _id: "$tags", count: { $sum: 1 } ], (err, json) -> if err then res.send(404, err) else res.json(json) # List plunks having a specific tag app.get "/tags/:tagname/plunks", (req, res, next) -> options = query: {tags: req.params.tagname} baseUrl: "#{apiUrl}/tags/#{req.params.tagname}/plunks" fetchPlunks(options, req, res, next) createSchema = gate.createSchema(require("./schema/packages/create.json")) updateSchema = gate.createSchema(require("./schema/packages/update.json")) withUser = (req, res, next) -> unless req.user then res.send(400) else next() withPackage = (req, res, next) -> Package.findOne({name: req.params.name}).select("-_id -versions._id").exec (err, pkg) -> if err then res.send(404) else req.package = pkg next() preparePackage = (session, pkg, populate = {}) -> json = _.extend pkg.toJSON(), populate delete json.id json.editable = true if session?.user and 0 <= json.maintainers.indexOf(session.user.login) json.versions.sort (v1, v2) -> semver.rcompare(v1.semver, v2.semver) json preparePackages = (session, pkgs) -> _.map pkgs, (pkg) -> preparePackage(session, pkg) app.get "/packages", (req, res, next) -> Package.find({}).select("-_id -versions._id").exec (err, docs) -> if err then res.send(err, 404) else res.json(preparePackages(req.session, docs)) app.post "/packages", withUser, (req, res, next) -> createSchema.validate req.body, (err, json) -> if err then res.json err, 400 else json.maintainers = [req.user.login] versions = [] versions.push versionDef for version, versionDef of req.body.versions json.versions = versions Package.create json, (err, pkg) -> if err if err.code is 11000 then res.json "A package with that name already exists", 409 else res.json err.message, 500 else res.json preparePackage(req.session, pkg), 201 app.get "/packages/:name", withPackage, (req, res, next) -> res.json(preparePackage(req.session, req.package)) app.post "/packages/:name", withUser, (req, res, next) -> updateSchema.validate req.body, (err, json) -> if err console.log "Invalid request", arguments... res.json err, 400 else for keyword, val of json.keywords if val is null then (json.$pullAll ||= keywords: []).keywords.push keyword else (json.$pushAll ||= keywords: []).keywords.push keyword delete json.keywords Package.findOneAndUpdate name: req.params.name maintainers: req.user.login , json, (err, pkg) -> if err then res.json(err, 404) else res.json(preparePackage(req.session, pkg), 200) app.del "/packages/:name", withUser, (req, res, next) -> Package.findOneAndRemove name: req.params.name maintainers: req.user.login , (err, pkg) -> if err then res.json(err, 404) else if pkg then res.send(204) else res.send(404) app.all "*", (req, res, next) -> next new apiErrors.NotFound ================================================ FILE: servers/api/lib/database.coffee ================================================ mongoose = require("mongoose") nconf = require("nconf") mime = require("mime") url = require("url") mime = require("mime") mongoose.connect "mongodb:" + url.format(nconf.get("mongodb")) connectTimeout = setTimeout(errorConnecting, 1000 * 30) apiUrl = nconf.get('url:api') wwwUrl = nconf.get('url:www') runUrl = nconf.get('url:run') errorConnecting = -> console.error "Error connecting to mongodb" process.exit(1) mongoose.connection.on "open", -> clearTimeout(connectTimeout) {Schema, Document, Query} = mongoose {ObjectId} = Schema genid = (len = 16, prefix = "", keyspace = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") -> prefix += keyspace.charAt(Math.floor(Math.random() * keyspace.length)) while len-- > 0 prefix # Change object _id to normal id Document::toJSON = -> json = @toObject(json: true, virtuals: true) json.id = json._id if json._id delete json._id delete json.__v json Query::paginate = (page, limit, cb) -> page = Math.max(1, parseInt(page, 10)) limit = Math.max(4, Math.min(12, parseInt(limit, 10))) # [4, 10] query = @ model = @model query.skip(page * limit - limit).limit(limit).exec (err, docs) -> if err then return cb(err, null, null) model.count query._conditions, (err, count) -> if err then return cb(err, null, null) cb(null, docs, count, Math.ceil(count / limit), page) lastModified = (schema, options = {}) -> schema.add updated_at: Date schema.pre "save", (next) -> @updated_at = new Date next() if options.index then schema.path("updated_at").index(options.index) TokenSchema = new Schema _id: String token: String SessionSchema = new Schema user: type: Schema.ObjectId ref: "User" last_access: { type: Date, index: true, 'default': Date.now } public_id: { type: String, 'default': genid } auth: {} keychain: [TokenSchema] SessionSchema.virtual("url").get -> apiUrl + "/sessions/#{@_id}" SessionSchema.virtual("user_url").get -> apiUrl + "/sessions/#{@_id}/user" SessionSchema.virtual("age").get -> Date.now() - @last_access SessionSchema.plugin(lastModified) SessionSchema.statics.prune = (max_age = 1000 * 60 * 60 * 24 * 7 * 2, cb = ->) -> @find({}).where("last_access").lt(new Date(Date.now() - max_age)).remove() mongoose.model "Session", SessionSchema mongoose.model "User", UserSchema = new Schema login: { type: String, index: true } gravatar_id: String service_id: { type: String, index: { unique: true } } profile: {} PlunkFileSchema = new Schema filename: String content: String PlunkFileSchema.virtual("mime").get -> mime.lookup(@filename, "text/plain") PlunkVoteSchema = new Schema user: { type: Schema.ObjectId, ref: "User" } created_at: { type: Date, 'default': Date.now } PlunkSchema = new Schema _id: { type: String, index: true } description: String score: { type: Number, 'default': Date.now } thumbs: { type: Number, 'default': 0 } created_at: { type: Date, 'default': Date.now } updated_at: { type: Date, 'default': Date.now } token: { type: String, 'default': genid.bind(null, 16) } 'private': { type: Boolean, 'default': false } source: {} files: [PlunkFileSchema] user: { type: Schema.ObjectId, ref: "User", index: true } comments: { type: Number, 'default': 0 } fork_of: { type: String, ref: "Plunk", index: true } forks: [{ type: String, ref: "Plunk", index: true }] tags: [{ type: String, index: true}] voters: [{ type: Schema.ObjectId, ref: "Users", index: true }] PlunkSchema.index(score: -1, updated_at: -1) PlunkSchema.index(thumbs: -1, updated_at: -1) PlunkSchema.virtual("url").get -> apiUrl + "/plunks/#{@_id}" PlunkSchema.virtual("raw_url").get -> runUrl + "/plunks/#{@_id}/" PlunkSchema.virtual("comments_url").get -> wwwUrl + "/#{@_id}/comments" mongoose.model "Plunk", PlunkSchema PackageVersionSchema = new Schema semver: String scripts: [String] styles: [String] PackageSchema = new Schema name: { type: String, match: /^[-_.a-z0-9]+$/i, index: true, unique: true } description: { type: String } homepage: String keywords: [{type: String, index: true}] versions: [PackageVersionSchema] maintainers: [{ type: String, index: true }] mongoose.model "Package", PackageSchema module.exports = mongoose ================================================ FILE: servers/api/middleware/cache.coffee ================================================ module.exports.middleware = (config = {}) -> (req, res, next) -> res.header("Cache-Control", "no-cache") res.header("Expires", "0") next() ================================================ FILE: servers/api/middleware/cors.coffee ================================================ nconf = require("nconf") module.exports.middleware = (config = {}) -> valid = [nconf.get('url:www'), nconf.get('url:embed'), "http://plnkr.co"] (req, res, next) -> # Just send the headers all the time. That way we won't miss the right request ;-) # Other CORS middleware just wouldn't work for me # TODO: Minimize these headers to only those needed at the right time res.header("Access-Control-Allow-Origin", if req.headers.origin in valid then req.headers.origin else "*") res.header("Access-Control-Allow-Methods", "OPTIONS,GET,PUT,POST,DELETE") res.header("Access-Control-Allow-Headers", "Authorization, User-Agent, Referer, X-Requested-With, Proxy-Authorization, Proxy-Connection, Accept-Language, Accept-Encoding, Accept-Charset, Connection, Content-Length, Host, Origin, Pragma, Accept-Charset, Cache-Control, Accept, Content-Type") res.header("Access-Control-Expose-Headers", "Link") res.header("Access-Control-Max-Age", "60") if "OPTIONS" == req.method then res.send(200) else next() ================================================ FILE: servers/api/middleware/json.coffee ================================================ apiErrors = require("../errors") module.exports.middleware = (config = {}) -> (req, res, next) -> if "GET" == req.method or "HEAD" == req.method then return next() req.body ||= {} buf = ''; req.setEncoding('utf8'); req.on "data", (chunk) -> buf += chunk req.on "end", -> return next() unless buf try req.body = JSON.parse(buf) next() catch err next(new apiErrors.ParseError()) ================================================ FILE: servers/api/middleware/session.coffee ================================================ nconf = require("nconf") module.exports.middleware = (config = {}) -> (req, res, next) -> if req.query.sessid then sessid = req.query.sessid else if auth = req.header("Authorization") then [header, sessid] = auth.match(/^token (\S+)$/i) if sessid and sessid.length then config.sessions.findById(sessid).populate("user").exec (err, session) -> return next(err) if err return next() unless session return next() if Date.now() - session.last_access.valueOf() > nconf.get("session:max_age") session.last_access = new Date session.save -> # Don't wait for the response req.session = session req.user = session.user if session.user next() else next() ================================================ FILE: servers/api/middleware/user.coffee ================================================ module.exports.middleware = (config = {}) -> (req, res, next) -> unless req.session and req.session.user then next() else config.users.findById req.session.user, (err, user) -> return next(err) if err unless user delete req.user delete req.session.user else req.user = user next() ================================================ FILE: servers/api/package.json ================================================ { "name": "plunker-api", "subdomain": "plunker-api", "domains": [ "api.plnkr.co" ], "scripts": { "start": "server.js" }, "engines": { "node": "0.8.x" }, "version": "0.3.14-27", "private": true, "dependencies": { "coffee-script": "1.3.x", "express": "2.5.11", "mime": "1.2.x", "underscore": "1.3.x", "backbone": "0.9.x", "nconf": "0.5.x", "json-schema": "https://github.com/kriszyp/json-schema/tarball/master", "request": "2.9.x", "connect": "1.x", "async": "0.1.x", "mongoose": "3.4.x", "lru-cache": "1.1.x", "json-gate": "0.8.x", "semver": "1.1.x" } } ================================================ FILE: servers/api/schema/packages/create.json ================================================ { "type": "object", "additionalProperties": false, "properties": { "name": { "type": "string", "required": true }, "description": { "type": "string", "default": "" }, "homepage": { "type": "string", "default": "" }, "keywords": { "type": "array", "additionalItems": false, "uniqueItems": true, "items": { "type": "string" } }, "versions": { "required": true, "type": "object", "additionalProperties": false, "patternProperties": { "^(\\d+\\.\\d+\\.\\d+)(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$": { "type": "object", "required": true, "additionalProperties": false, "properties": { "semver": { "type": "string", "required": true, "pattern": "^(\\d+\\.\\d+\\.\\d+)(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$" }, "scripts": { "type": "array", "items": { "type": "string" } }, "styles": { "type": "array", "items": { "type": "string" } } } } } } } } ================================================ FILE: servers/api/schema/packages/update.json ================================================ { "type": "object", "additionalProperties": false, "properties": { "name": { "type": "string" }, "description": { "type": "string", "default": "" }, "homepage": { "type": "string", "default": "" }, "keywords": { "type": "object", "additionalProperties": false, "patternProperties": { "^(.+)?$": { "type": "boolean", "required": true } } } } } ================================================ FILE: servers/api/schema/plunks/create.json ================================================ { "type": "object", "additionalProperties": false, "properties": { "description": { "type": "string", "default": "" }, "tags": { "type": "array", "additionalItems": false, "uniqueItems": true, "items": { "type": "string" } }, "private": { "type": "boolean", "default": false }, "source": { "type": [ { "type": "null" }, { "type": "object", "properties": { "type": { "type": "string", "required": true, "enum": [ "plunker_no_de", "gist" ] }, "url": { "type": "string", "required": true }, "title": { "type": "string", "required": true }, "description": { "type": "string" } } } ] }, "files": { "required": true, "type": "object", "additionalProperties": false, "patternProperties": { "^[a-zA-z0-9_-]+(\\.[a-zA-Z0-9]+)?$": { "type": "object", "properties": { "content": { "type": "string", "required": true } } } } } } } ================================================ FILE: servers/api/schema/plunks/update.json ================================================ { "type": "object", "additionalProperties": false, "minProperties": 1, "properties": { "description": { "type": "string", "default": "" }, "tags": { "type": "object", "additionalProperties": false, "patternProperties": { "^[a-zA-Z0-9_][-a-zA-Z0-9_ :]*$": { "type": "boolean" } } }, "files": { "type": "object", "additionalProperties": false, "patternProperties": { "^[-a-zA-Z0-9_]+(\\.[a-zA-Z0-9]+)?$": { "type": [ { "type": "null" }, { "type": "object", "properties": { "filename": { "type": "string" }, "content": { "type": "string" } }, "minProperties": 1 } ] } } } } } ================================================ FILE: servers/api/server.js ================================================ // Everything starts better with coffee var coffee = require("coffee-script"); var express = require("express"); var nconf = require("nconf").use("memory") .argv() .env() .file({file: "config.json"}) .defaults({ "PORT": 8080 }); if (!nconf.get("host")) { console.error("The host option is required for Plunker to start"); } else { //process.env.NODE_ENV = "production"; var host = nconf.get("host"); // Configure global paths if (nconf.get("nosubdomains")) { nconf.set("url:www", "http://" + host); nconf.set("url:raw", "http://" + host + "/raw"); nconf.set("url:run", "http://" + host + "/run"); nconf.set("url:api", "http://" + host + "/api"); } else { nconf.set("url:www", "http://" + host); nconf.set("url:raw", "http://raw." + host); nconf.set("url:run", "http://run." + host); nconf.set("url:api", "http://api." + host); } // Create and start the parent server require("./index").listen(nconf.get("PORT")); console.log("Started plunker-www in", nconf.get("NODE_ENV") || "development", "at", nconf.get("host"), "on port", nconf.get("PORT"), "using subdomains:", !nconf.get("nosubdomains")); } ================================================ FILE: servers/embed/.gitignore ================================================ lib-cov *.seed *.log *.csv *.dat *.out *.pid *.gz pids logs results tests node_modules npm-debug.log .c9revisions builtAssets ================================================ FILE: servers/embed/assets/bootstrap/js/bootstrap-affix.js ================================================ /* ========================================================== * bootstrap-affix.js v2.1.0 * http://twitter.github.com/bootstrap/javascript.html#affix * ========================================================== * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ========================================================== */ !function ($) { "use strict"; // jshint ;_; /* AFFIX CLASS DEFINITION * ====================== */ var Affix = function (element, options) { this.options = $.extend({}, $.fn.affix.defaults, options) this.$window = $(window).on('scroll.affix.data-api', $.proxy(this.checkPosition, this)) this.$element = $(element) this.checkPosition() } Affix.prototype.checkPosition = function () { if (!this.$element.is(':visible')) return var scrollHeight = $(document).height() , scrollTop = this.$window.scrollTop() , position = this.$element.offset() , offset = this.options.offset , offsetBottom = offset.bottom , offsetTop = offset.top , reset = 'affix affix-top affix-bottom' , affix if (typeof offset != 'object') offsetBottom = offsetTop = offset if (typeof offsetTop == 'function') offsetTop = offset.top() if (typeof offsetBottom == 'function') offsetBottom = offset.bottom() affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ? false : offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? 'bottom' : offsetTop != null && scrollTop <= offsetTop ? 'top' : false if (this.affixed === affix) return this.affixed = affix this.unpin = affix == 'bottom' ? position.top - scrollTop : null this.$element.removeClass(reset).addClass('affix' + (affix ? '-' + affix : '')) } /* AFFIX PLUGIN DEFINITION * ======================= */ $.fn.affix = function (option) { return this.each(function () { var $this = $(this) , data = $this.data('affix') , options = typeof option == 'object' && option if (!data) $this.data('affix', (data = new Affix(this, options))) if (typeof option == 'string') data[option]() }) } $.fn.affix.Constructor = Affix $.fn.affix.defaults = { offset: 0 } /* AFFIX DATA-API * ============== */ $(window).on('load', function () { $('[data-spy="affix"]').each(function () { var $spy = $(this) , data = $spy.data() data.offset = data.offset || {} data.offsetBottom && (data.offset.bottom = data.offsetBottom) data.offsetTop && (data.offset.top = data.offsetTop) $spy.affix(data) }) }) }(window.jQuery); ================================================ FILE: servers/embed/assets/bootstrap/js/bootstrap-alert.js ================================================ /* ========================================================== * bootstrap-alert.js v2.1.0 * http://twitter.github.com/bootstrap/javascript.html#alerts * ========================================================== * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ========================================================== */ !function ($) { "use strict"; // jshint ;_; /* ALERT CLASS DEFINITION * ====================== */ var dismiss = '[data-dismiss="alert"]' , Alert = function (el) { $(el).on('click', dismiss, this.close) } Alert.prototype.close = function (e) { var $this = $(this) , selector = $this.attr('data-target') , $parent if (!selector) { selector = $this.attr('href') selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 } $parent = $(selector) e && e.preventDefault() $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) $parent.trigger(e = $.Event('close')) if (e.isDefaultPrevented()) return $parent.removeClass('in') function removeElement() { $parent .trigger('closed') .remove() } $.support.transition && $parent.hasClass('fade') ? $parent.on($.support.transition.end, removeElement) : removeElement() } /* ALERT PLUGIN DEFINITION * ======================= */ $.fn.alert = function (option) { return this.each(function () { var $this = $(this) , data = $this.data('alert') if (!data) $this.data('alert', (data = new Alert(this))) if (typeof option == 'string') data[option].call($this) }) } $.fn.alert.Constructor = Alert /* ALERT DATA-API * ============== */ $(function () { $('body').on('click.alert.data-api', dismiss, Alert.prototype.close) }) }(window.jQuery); ================================================ FILE: servers/embed/assets/bootstrap/js/bootstrap-all.js ================================================ //= require bootstrap-transition //= require bootstrap-button //= require bootstrap-dropdown ================================================ FILE: servers/embed/assets/bootstrap/js/bootstrap-button.js ================================================ /* ============================================================ * bootstrap-button.js v2.1.0 * http://twitter.github.com/bootstrap/javascript.html#buttons * ============================================================ * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ============================================================ */ !function ($) { "use strict"; // jshint ;_; /* BUTTON PUBLIC CLASS DEFINITION * ============================== */ var Button = function (element, options) { this.$element = $(element) this.options = $.extend({}, $.fn.button.defaults, options) } Button.prototype.setState = function (state) { var d = 'disabled' , $el = this.$element , data = $el.data() , val = $el.is('input') ? 'val' : 'html' state = state + 'Text' data.resetText || $el.data('resetText', $el[val]()) $el[val](data[state] || this.options[state]) // push to event loop to allow forms to submit setTimeout(function () { state == 'loadingText' ? $el.addClass(d).attr(d, d) : $el.removeClass(d).removeAttr(d) }, 0) } Button.prototype.toggle = function () { var $parent = this.$element.parent('[data-toggle="buttons-radio"]') $parent && $parent .find('.active') .removeClass('active') this.$element.toggleClass('active') } /* BUTTON PLUGIN DEFINITION * ======================== */ $.fn.button = function (option) { return this.each(function () { var $this = $(this) , data = $this.data('button') , options = typeof option == 'object' && option if (!data) $this.data('button', (data = new Button(this, options))) if (option == 'toggle') data.toggle() else if (option) data.setState(option) }) } $.fn.button.defaults = { loadingText: 'loading...' } $.fn.button.Constructor = Button /* BUTTON DATA-API * =============== */ $(function () { $('body').on('click.button.data-api', '[data-toggle^=button]', function ( e ) { var $btn = $(e.target) if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') $btn.button('toggle') }) }) }(window.jQuery); ================================================ FILE: servers/embed/assets/bootstrap/js/bootstrap-carousel.js ================================================ /* ========================================================== * bootstrap-carousel.js v2.1.0 * http://twitter.github.com/bootstrap/javascript.html#carousel * ========================================================== * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ========================================================== */ !function ($) { "use strict"; // jshint ;_; /* CAROUSEL CLASS DEFINITION * ========================= */ var Carousel = function (element, options) { this.$element = $(element) this.options = options this.options.slide && this.slide(this.options.slide) this.options.pause == 'hover' && this.$element .on('mouseenter', $.proxy(this.pause, this)) .on('mouseleave', $.proxy(this.cycle, this)) } Carousel.prototype = { cycle: function (e) { if (!e) this.paused = false this.options.interval && !this.paused && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) return this } , to: function (pos) { var $active = this.$element.find('.item.active') , children = $active.parent().children() , activePos = children.index($active) , that = this if (pos > (children.length - 1) || pos < 0) return if (this.sliding) { return this.$element.one('slid', function () { that.to(pos) }) } if (activePos == pos) { return this.pause().cycle() } return this.slide(pos > activePos ? 'next' : 'prev', $(children[pos])) } , pause: function (e) { if (!e) this.paused = true if (this.$element.find('.next, .prev').length && $.support.transition.end) { this.$element.trigger($.support.transition.end) this.cycle() } clearInterval(this.interval) this.interval = null return this } , next: function () { if (this.sliding) return return this.slide('next') } , prev: function () { if (this.sliding) return return this.slide('prev') } , slide: function (type, next) { var $active = this.$element.find('.item.active') , $next = next || $active[type]() , isCycling = this.interval , direction = type == 'next' ? 'left' : 'right' , fallback = type == 'next' ? 'first' : 'last' , that = this , e = $.Event('slide', { relatedTarget: $next[0] }) this.sliding = true isCycling && this.pause() $next = $next.length ? $next : this.$element.find('.item')[fallback]() if ($next.hasClass('active')) return if ($.support.transition && this.$element.hasClass('slide')) { this.$element.trigger(e) if (e.isDefaultPrevented()) return $next.addClass(type) $next[0].offsetWidth // force reflow $active.addClass(direction) $next.addClass(direction) this.$element.one($.support.transition.end, function () { $next.removeClass([type, direction].join(' ')).addClass('active') $active.removeClass(['active', direction].join(' ')) that.sliding = false setTimeout(function () { that.$element.trigger('slid') }, 0) }) } else { this.$element.trigger(e) if (e.isDefaultPrevented()) return $active.removeClass('active') $next.addClass('active') this.sliding = false this.$element.trigger('slid') } isCycling && this.cycle() return this } } /* CAROUSEL PLUGIN DEFINITION * ========================== */ $.fn.carousel = function (option) { return this.each(function () { var $this = $(this) , data = $this.data('carousel') , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option) , action = typeof option == 'string' ? option : options.slide if (!data) $this.data('carousel', (data = new Carousel(this, options))) if (typeof option == 'number') data.to(option) else if (action) data[action]() else if (options.interval) data.cycle() }) } $.fn.carousel.defaults = { interval: 5000 , pause: 'hover' } $.fn.carousel.Constructor = Carousel /* CAROUSEL DATA-API * ================= */ $(function () { $('body').on('click.carousel.data-api', '[data-slide]', function ( e ) { var $this = $(this), href , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 , options = !$target.data('modal') && $.extend({}, $target.data(), $this.data()) $target.carousel(options) e.preventDefault() }) }) }(window.jQuery); ================================================ FILE: servers/embed/assets/bootstrap/js/bootstrap-collapse.js ================================================ /* ============================================================= * bootstrap-collapse.js v2.1.0 * http://twitter.github.com/bootstrap/javascript.html#collapse * ============================================================= * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ============================================================ */ !function ($) { "use strict"; // jshint ;_; /* COLLAPSE PUBLIC CLASS DEFINITION * ================================ */ var Collapse = function (element, options) { this.$element = $(element) this.options = $.extend({}, $.fn.collapse.defaults, options) if (this.options.parent) { this.$parent = $(this.options.parent) } this.options.toggle && this.toggle() } Collapse.prototype = { constructor: Collapse , dimension: function () { var hasWidth = this.$element.hasClass('width') return hasWidth ? 'width' : 'height' } , show: function () { var dimension , scroll , actives , hasData if (this.transitioning) return dimension = this.dimension() scroll = $.camelCase(['scroll', dimension].join('-')) actives = this.$parent && this.$parent.find('> .accordion-group > .in') if (actives && actives.length) { hasData = actives.data('collapse') if (hasData && hasData.transitioning) return actives.collapse('hide') hasData || actives.data('collapse', null) } this.$element[dimension](0) this.transition('addClass', $.Event('show'), 'shown') $.support.transition && this.$element[dimension](this.$element[0][scroll]) } , hide: function () { var dimension if (this.transitioning) return dimension = this.dimension() this.reset(this.$element[dimension]()) this.transition('removeClass', $.Event('hide'), 'hidden') this.$element[dimension](0) } , reset: function (size) { var dimension = this.dimension() this.$element .removeClass('collapse') [dimension](size || 'auto') [0].offsetWidth this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') return this } , transition: function (method, startEvent, completeEvent) { var that = this , complete = function () { if (startEvent.type == 'show') that.reset() that.transitioning = 0 that.$element.trigger(completeEvent) } this.$element.trigger(startEvent) if (startEvent.isDefaultPrevented()) return this.transitioning = 1 this.$element[method]('in') $.support.transition && this.$element.hasClass('collapse') ? this.$element.one($.support.transition.end, complete) : complete() } , toggle: function () { this[this.$element.hasClass('in') ? 'hide' : 'show']() } } /* COLLAPSIBLE PLUGIN DEFINITION * ============================== */ $.fn.collapse = function (option) { return this.each(function () { var $this = $(this) , data = $this.data('collapse') , options = typeof option == 'object' && option if (!data) $this.data('collapse', (data = new Collapse(this, options))) if (typeof option == 'string') data[option]() }) } $.fn.collapse.defaults = { toggle: true } $.fn.collapse.Constructor = Collapse /* COLLAPSIBLE DATA-API * ==================== */ $(function () { $('body').on('click.collapse.data-api', '[data-toggle=collapse]', function (e) { var $this = $(this), href , target = $this.attr('data-target') || e.preventDefault() || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 , option = $(target).data('collapse') ? 'toggle' : $this.data() $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') $(target).collapse(option) }) }) }(window.jQuery); ================================================ FILE: servers/embed/assets/bootstrap/js/bootstrap-dropdown.js ================================================ /* ============================================================ * bootstrap-dropdown.js v2.1.0 * http://twitter.github.com/bootstrap/javascript.html#dropdowns * ============================================================ * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ============================================================ */ !function ($) { "use strict"; // jshint ;_; /* DROPDOWN CLASS DEFINITION * ========================= */ var toggle = '[data-toggle=dropdown]' , Dropdown = function (element) { var $el = $(element).on('click.dropdown.data-api', this.toggle) $('html').on('click.dropdown.data-api', function () { $el.parent().removeClass('open') }) } Dropdown.prototype = { constructor: Dropdown , toggle: function (e) { var $this = $(this) , $parent , isActive if ($this.is('.disabled, :disabled')) return $parent = getParent($this) isActive = $parent.hasClass('open') clearMenus() if (!isActive) { $parent.toggleClass('open') $this.focus() } return false } , keydown: function (e) { var $this , $items , $active , $parent , isActive , index if (!/(38|40|27)/.test(e.keyCode)) return $this = $(this) e.preventDefault() e.stopPropagation() if ($this.is('.disabled, :disabled')) return $parent = getParent($this) isActive = $parent.hasClass('open') if (!isActive || (isActive && e.keyCode == 27)) return $this.click() $items = $('[role=menu] li:not(.divider) a', $parent) if (!$items.length) return index = $items.index($items.filter(':focus')) if (e.keyCode == 38 && index > 0) index-- // up if (e.keyCode == 40 && index < $items.length - 1) index++ // down if (!~index) index = 0 $items .eq(index) .focus() } } function clearMenus() { getParent($(toggle)) .removeClass('open') } function getParent($this) { var selector = $this.attr('data-target') , $parent if (!selector) { selector = $this.attr('href') selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 } $parent = $(selector) $parent.length || ($parent = $this.parent()) return $parent } /* DROPDOWN PLUGIN DEFINITION * ========================== */ $.fn.dropdown = function (option) { return this.each(function () { var $this = $(this) , data = $this.data('dropdown') if (!data) $this.data('dropdown', (data = new Dropdown(this))) if (typeof option == 'string') data[option].call($this) }) } $.fn.dropdown.Constructor = Dropdown /* APPLY TO STANDARD DROPDOWN ELEMENTS * =================================== */ $(function () { $('html') .on('click.dropdown.data-api touchstart.dropdown.data-api', clearMenus) $('body') .on('click.dropdown touchstart.dropdown.data-api', '.dropdown', function (e) { e.stopPropagation() }) .on('click.dropdown.data-api touchstart.dropdown.data-api' , toggle, Dropdown.prototype.toggle) .on('keydown.dropdown.data-api touchstart.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) }) }(window.jQuery); ================================================ FILE: servers/embed/assets/bootstrap/js/bootstrap-modal.js ================================================ /* ========================================================= * bootstrap-modal.js v2.1.0 * http://twitter.github.com/bootstrap/javascript.html#modals * ========================================================= * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ========================================================= */ !function ($) { "use strict"; // jshint ;_; /* MODAL CLASS DEFINITION * ====================== */ var Modal = function (element, options) { this.options = options this.$element = $(element) .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) this.options.remote && this.$element.find('.modal-body').load(this.options.remote) } Modal.prototype = { constructor: Modal , toggle: function () { return this[!this.isShown ? 'show' : 'hide']() } , show: function () { var that = this , e = $.Event('show') this.$element.trigger(e) if (this.isShown || e.isDefaultPrevented()) return $('body').addClass('modal-open') this.isShown = true this.escape() this.backdrop(function () { var transition = $.support.transition && that.$element.hasClass('fade') if (!that.$element.parent().length) { that.$element.appendTo(document.body) //don't move modals dom position } that.$element .show() if (transition) { that.$element[0].offsetWidth // force reflow } that.$element .addClass('in') .attr('aria-hidden', false) .focus() that.enforceFocus() transition ? that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) : that.$element.trigger('shown') }) } , hide: function (e) { e && e.preventDefault() var that = this e = $.Event('hide') this.$element.trigger(e) if (!this.isShown || e.isDefaultPrevented()) return this.isShown = false $('body').removeClass('modal-open') this.escape() $(document).off('focusin.modal') this.$element .removeClass('in') .attr('aria-hidden', true) $.support.transition && this.$element.hasClass('fade') ? this.hideWithTransition() : this.hideModal() } , enforceFocus: function () { var that = this $(document).on('focusin.modal', function (e) { if (that.$element[0] !== e.target && !that.$element.has(e.target).length) { that.$element.focus() } }) } , escape: function () { var that = this if (this.isShown && this.options.keyboard) { this.$element.on('keyup.dismiss.modal', function ( e ) { e.which == 27 && that.hide() }) } else if (!this.isShown) { this.$element.off('keyup.dismiss.modal') } } , hideWithTransition: function () { var that = this , timeout = setTimeout(function () { that.$element.off($.support.transition.end) that.hideModal() }, 500) this.$element.one($.support.transition.end, function () { clearTimeout(timeout) that.hideModal() }) } , hideModal: function (that) { this.$element .hide() .trigger('hidden') this.backdrop() } , removeBackdrop: function () { this.$backdrop.remove() this.$backdrop = null } , backdrop: function (callback) { var that = this , animate = this.$element.hasClass('fade') ? 'fade' : '' if (this.isShown && this.options.backdrop) { var doAnimate = $.support.transition && animate this.$backdrop = $('