"
6 | ],
7 | "name": "@shields_io/camp",
8 | "description": "Fork of Scoutcamp maintained for Shields",
9 | "version": "18.1.1",
10 | "homepage": "https://github.com/badges/sc",
11 | "repository": "badges/sc",
12 | "main": "lib/camp.js",
13 | "scripts": {
14 | "test": "make test"
15 | },
16 | "dependencies": {
17 | "cookies": "~0.7.1",
18 | "fleau": "~16.2.0",
19 | "formidable": "~1.2.0",
20 | "multilog": "~14.11.22",
21 | "socket.io": "^2.0.4",
22 | "spdy": "^4.0.2",
23 | "handle-thing": "^2.0.1",
24 | "ws": "^6.1.0"
25 | },
26 | "devDependencies": {},
27 | "optionalDependencies": {},
28 | "engines": {
29 | "node": ">=0.11.8"
30 | },
31 | "license": "MIT",
32 | "directories": {
33 | "lib": "./lib",
34 | "doc": "./doc",
35 | "examples": "node app 1234"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright © 2011-present Thaddée Tyl
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/web/index.html:
--------------------------------------------------------------------------------
1 |
2 | ScoutCamp
3 |
4 |
22 |
23 |
24 | Demos:
25 |
32 |
33 |
34 |
35 | Success
36 |
37 | You're on the web!
38 | Please tinker, chop, thrash and fool around!
39 |
--------------------------------------------------------------------------------
/web/ws-chat.html:
--------------------------------------------------------------------------------
1 |
2 | Chat
3 |
4 |
5 |
6 |
9 |
10 |
20 |
21 |
44 |
--------------------------------------------------------------------------------
/web/socket.io-chat.html:
--------------------------------------------------------------------------------
1 |
2 | Chat
3 |
4 |
5 |
6 |
9 |
10 |
20 |
21 |
22 |
46 |
--------------------------------------------------------------------------------
/web/chat.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Chat
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
23 |
24 |
49 |
50 |
--------------------------------------------------------------------------------
/lib/Walkthrough:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 888888b, 8888 8888B 8888B 888888888
6 | 88 `*8; dP `88' `38^ 88P^^^^^`
7 | 88 88 dP `88. ;8" 88
8 | 88 88 dP `88. ;8P 88
9 | 88 88 dP `88. .8P 888888
10 | 88 .88 dP `88. .8? 8P```'
11 | 88 .;8p dP .X8i8Y. 8R........
12 | 8888884P 8888 8888888 8888888888
13 |
14 |
15 | __ __ __
16 | into the /_ / / /_ /_
17 | / / /__ /__ __/
18 |
19 |
20 | of
21 | ____ __ ___ _ _ _____ __ ___ _ _ ___
22 | / __// _/ _ \| || |_ _| / _/ \_ | \/ | _ \
23 | \__ \ |_| _ || || | | | | |_ /_ | | _/
24 | /___/\__\___/\____/ |_| \__\|___|_||_|_|
25 |
26 |
27 | lib/ Major server-side files.
28 | camp.js Main export point.
29 | mime.json List of default mime types associated with file extensions.
30 | plate.js Default template engine.
31 |
32 | doc/ Documentation folder (including the website).
33 | Readme.md Must-read.
34 |
35 | app.js Example server. Run it with `sudo node app`
36 | web/ Example document root folder.
37 | js/
38 | *.js Files necessary to build scout.js.
39 |
40 | scout.js Compressed client-side library.
41 | Designed to be `cp .node_modules/camp/scout.js web/`.
42 |
43 | test/ Testing directory. Feel free to add your own stuff.
44 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ScoutCamp
5 |
6 | ScoutCamp
7 |
8 | /skoutkamp/ Web apps done right.
9 |
10 |
11 |
12 | "Hassle-free Node.js server framework.
13 | Arbitrary routers. Cookies. Forms. Auth.
14 | Compiled templates. Fetch. WebSockets. Server-Side Events."
15 |
16 |
17 |
26 |
27 | Instructions
28 |
29 |
30 | $ npm install camp
31 |
32 |
33 |
34 | Start with the
35 | readme .
36 |
37 | Look at an
38 | example .
39 |
40 | Read the
41 | reference .
42 |
43 |
44 |
60 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | // Server demo. `node app [PORT [https]]`
2 | // © 2011-2017 Thaddée Tyl, Jan Keromnes. LGPL.
3 |
4 | const port = +process.argv[2] || +process.env.PORT || 1234
5 | let camp = require('./lib/camp.js')
6 | let sc = camp.start({port: port, secure: process.argv[3] === 'https'})
7 | console.log('http://[::1]:' + port)
8 |
9 | // Templating demo: /template.html?title=Hello&info=[Redacted].
10 | sc.path('/template.html')
11 |
12 | // Templating demo with multiple templates and path parameter.
13 | // /html.template/Hello/World
14 | let flip = sc.template(['web/template.html', 'web/flip.html'])
15 | sc.path('/html.template/:title/:info', (req, res) => {
16 | res.template(req.data, flip)
17 | })
18 |
19 | // Doctor demo: /doctor?text=…
20 | let replies = ['Ok.', 'Oh⁉', 'Is that so?', 'How interesting!',
21 | 'Hm…', 'What do you mean?', 'So say we all.']
22 | sc.post('/doctor', (req, res) => {
23 | replies.push(req.query.text)
24 | res.json({reply: replies[Math.random() * replies.length|0]})
25 | })
26 |
27 | // Chat demo
28 | let chat = sc.eventSource('/all')
29 | sc.post('/talk', (req, res) => {chat.send(req.data); res.end()})
30 |
31 | // WebSocket chat demo
32 | sc.wsBroadcast('/chat', (req, res) => res.send(req.data))
33 |
34 | // Socket.io chat demo
35 | const ioChat = sc.io.of('/chat');
36 | ioChat.on('connection', socket =>
37 | socket.on('message', msg => ioChat.send(msg)));
38 |
39 | // Not found demo
40 | sc.notFound('/*.lol', (req, res) => res.file('/404.html'))
41 |
42 | // Basic authentication demo
43 | sc.get('/secret', (req, res) => {
44 | if (req.username === 'Caesar' && req.password === '1234') {
45 | res.end('Congrats, you found it!')
46 | } else {
47 | res.statusCode = 401
48 | res.setHeader('WWW-Authenticate', 'Basic')
49 | res.end('Nothing to hide here!')
50 | }
51 | })
52 |
53 | // Low-level handler
54 | sc.handle((req, res, down) => {
55 | res.setHeader('X-Open-Source', 'https://github.com/espadrine/sc/')
56 | down()
57 | })
58 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | // test.js: A module for unit tests.
2 | // Copyright © 2011-2013 Thaddee Tyl, Jan Keromnes. All rights reserved.
3 | // Code covered by the LGPL license.
4 |
5 | var Test = function () { this.n = 0; this.errors = 0; };
6 |
7 | // Deep inequality.
8 | function notEqual(a, b) {
9 | if (a instanceof Array || a instanceof Object) {
10 | if (typeof a === typeof b) {
11 | for (var key in a) {
12 | if (notEqual(a[key], b[key])) {
13 | return true;
14 | }
15 | }
16 | for (var key in b) {
17 | if (notEqual(a[key], b[key])) {
18 | return true;
19 | }
20 | }
21 | return false;
22 | } else {
23 | return true;
24 | }
25 | } else {
26 | return a !== b;
27 | }
28 | }
29 |
30 | Test.prototype.eq = function (a, b, msg) {
31 | this.n ++;
32 | if (notEqual(a, b)) {
33 | console.log ('#' + this.n + ' failed: got ' + JSON.stringify (a) +
34 | ' instead of ' + JSON.stringify (b));
35 | if (msg) {
36 | console.log (msg.split('\n').map(function(e){return ' '+e;}).join('\n'));
37 | }
38 | this.errors ++;
39 | }
40 | };
41 |
42 | Test.prototype.each = function (tests, result) {
43 | var len = tests.length;
44 | var i = tests.length;
45 | function endTest () {
46 | i++;
47 | // When all tests have been performed…
48 | if (i === len) { result (); }
49 | }
50 | for (var j = 0; j < tests.length; j++) {
51 | tests[j] (endTest);
52 | }
53 | };
54 |
55 | Test.prototype.seq = function (tests, result) {
56 | var len = tests.length;
57 | var i = 0;
58 | function endTest() {
59 | i++;
60 | // When all tests have been performed…
61 | if (i === len) { result (); }
62 | else { tests[i] (endTest); }
63 | }
64 | tests[i] (endTest);
65 | };
66 |
67 | Test.prototype.tldr = function () {
68 | if (this.errors === 0) { console.log ('All ' + this.n + ' tests passed.');
69 | } else if (this.errors === this.n) { console.log ('All tests failed.');
70 | } else {
71 | console.log ((this.n - this.errors) + ' tests passed out of ' +
72 | this.n + ' (' +
73 | (100 * (1 - this.errors/this.n)).toFixed (2) + '%).');
74 | }
75 | };
76 |
77 | Test.prototype.exit = function () {
78 | process.exit((this.errors > 0)? 1: 0);
79 | };
80 |
81 | module.exports = Test;
82 |
83 |
--------------------------------------------------------------------------------
/doc/screen.css:
--------------------------------------------------------------------------------
1 | html {
2 | color: #444;
3 | text-shadow: #EC7979 0px 1px 4px;
4 | background: url('img/bg1.png'), url('img/bg3.png'),url('img/bg2.png'), linen; /* linen = #faf0e6. */
5 | font-family: "Droid Sans", sans-serif;
6 | overflow-y: scroll;
7 | }
8 |
9 | ::-moz-selection { background: #ff5e99; color: #fff; text-shadow: none; }
10 | ::selection { background: #ff5e99; color: #fff; text-shadow: none; }
11 |
12 | h1 {
13 | text-align: right;
14 | font-family: "Ubuntu Mono", monospace;
15 | border-bottom: solid 1px #222;
16 | }
17 | span.purple {
18 | color: purple;
19 | }
20 |
21 | #subtitle {
22 | text-align: center;
23 | }
24 |
25 | h2, h3, table, form {
26 | margin: auto 20px;
27 | }
28 | p, pre, ol, ul, dl {
29 | margin: 12pt 70px;
30 | }
31 |
32 | ol>li, ul>li, dl>dt {
33 | margin-top: 4pt;
34 | }
35 |
36 | h2, h3 {
37 | color: purple;
38 | font-family: 'IM Fell DW Pica', serif;
39 | font-style: italic;
40 | margin-top: 40px;
41 | }
42 |
43 | pre, code, kbd {
44 | font-family: "Ubuntu Mono", monospace;
45 | background-color: #faffe6;
46 | }
47 | pre {
48 | padding: 10px;
49 | border: 1px dashed purple;
50 | letter-spacing: 1px;
51 | text-indent: 0px;
52 | }
53 |
54 | a, a:visited {
55 | color: purple;
56 | text-shadow: 777 0px 1px 4px;
57 | text-decoration: none;
58 | }
59 | a:hover {
60 | color: rgba(1,1,1,0.4);
61 | text-shadow: purple 0px 0px 1px;
62 | }
63 |
64 | textarea {
65 | width: 500px;
66 | height: 150px;
67 | }
68 |
69 | hr {
70 | height: 1px;
71 | border: 0px;
72 | background-color: #423;
73 | margin: 3em;
74 | color:
75 | }
76 |
77 |
78 | /* Heads */
79 |
80 | span.ipa { font-style: auto; font-family: sans-serif; margin-right: 7px; }
81 | .right { text-align: right; }
82 | .awesome { font-size: 1.2em; width: 50%; margin: 1cm auto; }
83 | .speech { font-style: italic; font-size: 1.4em; font-family: "Linux Libertine O",serif; }
84 | .entry {
85 | font-size: x-large;
86 | font-weight: bold;
87 | margin-right: 13px;
88 | }
89 | .glow {
90 | text-shadow: hsl(260, 70%, 70%) 0px 0px 42px;
91 | background: none;
92 | text-decoration: underline;
93 | }
94 |
95 |
96 | /* Manual */
97 |
98 | .manual {
99 | text-indent: 7px;
100 | }
101 |
102 |
103 | /* Downloads */
104 | .downloads {
105 | margin: auto;
106 | border-spacing: 7pt;
107 | }
108 | .downloads a {
109 | display: block;
110 | padding: 12pt;
111 | border-radius: 3px;
112 | -moz-border-radius: 3px;
113 |
114 | color: #c80;
115 | text-shadow: #b34d4d 1px 1px 0px, #644 -1px -1px 1px,
116 | #c90 2px 2px 0px, #c90 3px 3px 0px;
117 | box-shadow: #94b -1px -1px 0px inset, #c95 -2px -2px 1px inset,
118 | #94b 1px 1px 0px inset, #950 2px 2px 1px inset,
119 | #606 0px 0px 50px inset;
120 | -webkit-transition: 0.3s ease-out box-shadow;
121 | -moz-transition: 0.3s ease-out box-shadow;
122 | transition: 0.3s ease-out box-shadow;
123 |
124 | letter-spacing: 2.5pt;
125 | font-family: "Ubuntu Mono", monospace;
126 | font-size: 14pt;
127 | font-style: italic;
128 | font-weight: bold;
129 | text-decoration: none;
130 | }
131 | .downloads a:hover {
132 | box-shadow: #94b -1px -1px 0px inset, #c99 -2px -2px 1px inset,
133 | #94b 1px 1px 0px inset, #953 2px 2px 1px inset,
134 | #606 0px 0px 20px inset; /* Main one */
135 | }
136 | .scout {
137 | background-color: hsl(280, 50%, 45%); /* #9020d0; */
138 | }
139 | .scoutmin {
140 | background-color: hsl(280, 70%, 50%); /* #9020d0; */
141 | }
142 | .camp {
143 | background-color: hsl(260, 70%, 60%);
144 | }
145 | .zipball {
146 | background-color: #e46787;
147 | }
148 | .browse {
149 | background-color: #7dc4e8; /* or hsl(110, 75%, 60%) ? */
150 | }
151 | .nodejs {
152 | background-color: #bea3f5;
153 | }
154 |
155 | /* Comments */
156 | div.idea {
157 | border: 1px solid #503;
158 | -webkit-box-shadow: 0.7pt 0.7pt 4px #503 inset;
159 | -moz-box-shadow: 0.7pt 0.7pt 4px #503 inset;
160 | border-radius: 3px;
161 | }
162 |
163 | table.comment tr {
164 | border-radius: 5px 7px;
165 | }
166 | table.comment tr:nth-child(2n+1) { background-color: rgba(200,0,200, 0.2); }
167 | table.comment tr:nth-child(2n) { background-color: rgba(200,0,200, 0.1); }
168 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile: Publish your website, start/stop your server.
2 | # Copyright © 2011-2015 Thaddee Tyl, Jan Keromnes. All rights reserved.
3 | # Code covered by the LGPL license.
4 |
5 | # The name of your main server file.
6 | SERVER = app.js
7 |
8 | # The folder where your precious website is.
9 | WEB = web
10 |
11 | # The folder where your minified, production-ready website should be published.
12 | # Warning: `make` and `make clean` will delete this folder.
13 | PUBLISH = publish
14 |
15 | # The JS minifier. Change the priority order to your convenience.
16 | # It must accept some JS in stdin, and produce the result on stdout.
17 | # Note: you must create `google-closure.sh` yourself if you want it.
18 | JSMIN = uglifyjs jsmin google-closure.sh js-minifier
19 |
20 | # The suffix to use for minified files.
21 | MIN = min
22 |
23 | # To make your server secure, generate SSL certificates (e.g. `make https`),
24 | # then start it with something like `SECURE=secure make start`.
25 | ifdef SECURE
26 | PORT ?= 443
27 | SECURE = secure
28 | else
29 | PORT ?= 80
30 | SECURE = insecure
31 | endif
32 | DEBUG ?= 0
33 |
34 | # The output of console.log statements goes in this file when you `make`.
35 | # Note: when you `make debug`, the output appears on the console.
36 | LOG = node.log
37 |
38 | # You can define custom rules and settings in such a file.
39 | -include local.mk
40 |
41 | # Default behavior for `make`.
42 | all: publish stop start
43 |
44 | # Publish your website.
45 | publish: clean copy minify
46 |
47 | # Try your unpublished website.
48 | debug: stop startweb
49 |
50 | # Delete generated files and logs.
51 | clean:
52 | @echo "clean"
53 | @rm -rf $(LOG) $(PUBLISH)
54 |
55 | # Simply copy all website files over to your publishing folder.
56 | copy:
57 | @echo "copy"
58 | @cp -rf $(WEB) $(PUBLISH)
59 |
60 | # Minify everything we can inside your publishing folder.
61 | minify:
62 | @echo "minify"
63 | @for ajsmin in $(JSMIN); do \
64 | if which $$ajsmin > /dev/null; then chosenjsmin=$$ajsmin; break; fi; \
65 | done; \
66 | if which $$chosenjsmin > /dev/null ; then \
67 | for file in `find $(PUBLISH) -name '*\.js'`; do \
68 | $$chosenjsmin < "$${file}" > "$${file}$(MIN)"; \
69 | mv "$${file}$(MIN)" "$${file}"; \
70 | done; \
71 | else \
72 | echo ' `sudo make jsmin` or install uglifyjs for minification.'; \
73 | fi
74 |
75 | # Stop any previously-started Camp server.
76 | stop:
77 | @echo "stop"
78 | @for pid in `ps aux | grep -v make | grep node | grep $(SERVER) | awk '{print $$2}'` ; do \
79 | kill -9 $$pid 2> /dev/null ; \
80 | if [ "$$?" -ne "0" ] ; then \
81 | sudo kill -9 $$pid 2> /dev/null ; \
82 | fi \
83 | done; \
84 |
85 | # Start a Camp server with your published website (for production).
86 | start:
87 | @echo "start"
88 | @if [ `id -u` -ne "0" -a $(PORT) -lt 1024 ] ; \
89 | then \
90 | sudo node $(SERVER) $(PORT) $(SECURE) $(DEBUG) >> $(LOG) ; \
91 | else \
92 | node $(SERVER) $(PORT) $(SECURE) $(DEBUG) >> $(LOG) ; \
93 | fi
94 |
95 | # Start a Camp server with your unpublished website (for development).
96 | startweb:
97 | @echo "start web"
98 | @if [ `id -u` -ne "0" -a $(PORT) -lt 1024 ] ; \
99 | then \
100 | sudo node $(SERVER) $(PORT) $(SECURE) $(DEBUG) >> $(LOG) ; \
101 | else \
102 | node $(SERVER) $(PORT) $(SECURE) $(DEBUG) >> $(LOG) ; \
103 | fi
104 |
105 | # Run the ScoutCamp tests.
106 | test:
107 | node test/test-api.js
108 |
109 | # Update a ScoutCamp fork.
110 | # Warning: overwrites this Makefile, you may want to create a local.mk!
111 | update:
112 | @git clone https://github.com/espadrine/sc
113 | @cp sc/web/js/scout.js ./$(WEB)/js/scout.js
114 | @cp sc/lib/* ./lib/
115 | @cp sc/Makefile .
116 | @rm -rf sc/
117 |
118 | # Install Doug Crockford's `jsmin` in `/usr/bin/jsmin`.
119 | jsmin:
120 | @if [ `id -u` = "0" ] ; \
121 | then wget "http://crockford.com/javascript/jsmin.c" && gcc -o /usr/bin/jsmin jsmin.c ; \
122 | rm -rf jsmin.c ; \
123 | else echo ' `sudo make jsmin`'; fi
124 |
125 | # Generate self-signed HTTPS credentials.
126 | https: https.crt
127 |
128 | # Delete HTTPS credentials.
129 | rmhttps:
130 | @echo "delete https credentials"
131 | @rm -rf https.key https.csr https.crt
132 |
133 | # Generate an SSL certificate secret key. Never share this!
134 | https.key:
135 | @openssl genrsa -out https.key 4096
136 | @chmod 400 https.key # read by owner
137 |
138 | # Generate a CSR (Certificate Signing Request) for someone to sign your
139 | # SSL certificate.
140 | https.csr: https.key
141 | @openssl req -new -sha256 -key https.key -out https.csr
142 | @chmod 400 https.csr # read by owner
143 |
144 | # Create a self-signed SSL certificate.
145 | # Warning: web users will be shown a useless security warning.
146 | https.crt: https.key https.csr
147 | @openssl x509 -req -days 365 -in https.csr -signkey https.key -out https.crt
148 | @chmod 444 https.crt # read by all
149 |
150 | # Download Scout's JS dependencies into your website's js/ folder.
151 | scout-update:
152 | @curl https://raw.github.com/jquery/sizzle/master/sizzle.js > $(WEB)/js/sizzle.js 2> /dev/null
153 | @curl https://raw.github.com/douglascrockford/JSON-js/master/json2.js > $(WEB)/js/json2.js 2> /dev/null
154 | @curl https://raw.github.com/remy/polyfills/master/EventSource.js > $(WEB)/js/EventSource.js 2> /dev/null
155 |
156 | # Build the `scout.js` client script.
157 | scout-build:
158 | @for ajsmin in $(JSMIN); do \
159 | if which $$ajsmin > /dev/null; then chosenjsmin=$$ajsmin; break; fi; \
160 | done; \
161 | cat $(WEB)/js/sizzle.js $(WEB)/js/json2.js $(WEB)/js/EventSource.js $(WEB)/js/additions.js | $$ajsmin > $(WEB)/js/scout.js
162 | @cp $(WEB)/js/scout.js .
163 |
164 | # This is a self-documenting Makefile.
165 | help:
166 | @cat Makefile | less
167 |
168 | wtf: help
169 |
170 | ?: wtf
171 |
172 | coffee:
173 | @echo "\n ) (\n ( ) )\n _..,-(--,.._\n .-;'-.,____,.-';\n (( | |\n \`-; ;\n \\ /\n .-''\`-.____.-'''-.\n ( '------' )\n \`--..________..--'\n";
174 |
175 | me a:
176 | @cd .
177 |
178 | sandwich:
179 | @if [ `id -u` = "0" ] ; then echo "OKAY." ; else echo "What? Make it yourself." ; fi
180 |
181 | .PHONY: all publish debug clean copy minify stop start startweb test update jsmin https scout-update scout-build help wtf ? coffee me a sandwich
182 |
183 |
--------------------------------------------------------------------------------
/web/js/EventSource.js:
--------------------------------------------------------------------------------
1 | ;(function (global) {
2 |
3 | if ("EventSource" in global) return;
4 |
5 | var reTrim = /^(\s|\u00A0)+|(\s|\u00A0)+$/g;
6 |
7 | var EventSource = function (url) {
8 | var eventsource = this,
9 | interval = 500, // polling interval
10 | lastEventId = null,
11 | cache = '';
12 |
13 | if (!url || typeof url != 'string') {
14 | throw new SyntaxError('Not enough arguments');
15 | }
16 |
17 | this.URL = url;
18 | this.readyState = this.CONNECTING;
19 | this._pollTimer = null;
20 | this._xhr = null;
21 |
22 | function pollAgain(interval) {
23 | eventsource._pollTimer = setTimeout(function () {
24 | poll.call(eventsource);
25 | }, interval);
26 | }
27 |
28 | function poll() {
29 | try { // force hiding of the error message... insane?
30 | if (eventsource.readyState == eventsource.CLOSED) return;
31 |
32 | // NOTE: IE7 and upwards support
33 | var xhr = new XMLHttpRequest();
34 | xhr.open('GET', eventsource.URL, true);
35 | xhr.setRequestHeader('Accept', 'text/event-stream');
36 | xhr.setRequestHeader('Cache-Control', 'no-cache');
37 | // we must make use of this on the server side if we're working with Android - because they don't trigger
38 | // readychange until the server connection is closed
39 | xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
40 |
41 | if (lastEventId != null) xhr.setRequestHeader('Last-Event-ID', lastEventId);
42 | cache = '';
43 |
44 | xhr.timeout = 50000;
45 | xhr.onreadystatechange = function () {
46 | if (this.readyState == 3 || (this.readyState == 4 && this.status == 200)) {
47 | // on success
48 | if (eventsource.readyState == eventsource.CONNECTING) {
49 | eventsource.readyState = eventsource.OPEN;
50 | eventsource.dispatchEvent('open', { type: 'open' });
51 | }
52 |
53 | var responseText = '';
54 | try {
55 | responseText = this.responseText || '';
56 | } catch (e) {}
57 |
58 | // process this.responseText
59 | var parts = responseText.substr(cache.length).split("\n"),
60 | eventType = 'message',
61 | data = [],
62 | i = 0,
63 | line = '';
64 |
65 | cache = responseText;
66 |
67 | // TODO handle 'event' (for buffer name), retry
68 | for (; i < parts.length; i++) {
69 | line = parts[i].replace(reTrim, '');
70 | if (line.indexOf('event') == 0) {
71 | eventType = line.replace(/event:?\s*/, '');
72 | } else if (line.indexOf('retry') == 0) {
73 | retry = parseInt(line.replace(/retry:?\s*/, ''));
74 | if(!isNaN(retry)) { interval = retry; }
75 | } else if (line.indexOf('data') == 0) {
76 | data.push(line.replace(/data:?\s*/, ''));
77 | } else if (line.indexOf('id:') == 0) {
78 | lastEventId = line.replace(/id:?\s*/, '');
79 | } else if (line.indexOf('id') == 0) { // this resets the id
80 | lastEventId = null;
81 | } else if (line == '') {
82 | if (data.length) {
83 | var event = new MessageEvent(data.join('\n'), eventsource.url, lastEventId);
84 | eventsource.dispatchEvent(eventType, event);
85 | data = [];
86 | eventType = 'message';
87 | }
88 | }
89 | }
90 |
91 | if (this.readyState == 4) pollAgain(interval);
92 | // don't need to poll again, because we're long-loading
93 | } else if (eventsource.readyState !== eventsource.CLOSED) {
94 | if (this.readyState == 4) { // and some other status
95 | // dispatch error
96 | eventsource.readyState = eventsource.CONNECTING;
97 | eventsource.dispatchEvent('error', { type: 'error' });
98 | pollAgain(interval);
99 | } else if (this.readyState == 0) { // likely aborted
100 | pollAgain(interval);
101 | } else {
102 | }
103 | }
104 | };
105 |
106 | xhr.send();
107 |
108 | setTimeout(function () {
109 | if (true || xhr.readyState == 3) xhr.abort();
110 | }, xhr.timeout);
111 |
112 | eventsource._xhr = xhr;
113 |
114 | } catch (e) { // in an attempt to silence the errors
115 | eventsource.dispatchEvent('error', { type: 'error', data: e.message }); // ???
116 | }
117 | };
118 |
119 | poll(); // init now
120 | };
121 |
122 | EventSource.prototype = {
123 | close: function () {
124 | // closes the connection - disabling the polling
125 | this.readyState = this.CLOSED;
126 | clearInterval(this._pollTimer);
127 | this._xhr.abort();
128 | },
129 | CONNECTING: 0,
130 | OPEN: 1,
131 | CLOSED: 2,
132 | dispatchEvent: function (type, event) {
133 | var handlers = this['_' + type + 'Handlers'];
134 | if (handlers) {
135 | for (var i = 0; i < handlers.length; i++) {
136 | handlers[i].call(this, event);
137 | }
138 | }
139 |
140 | if (this['on' + type]) {
141 | this['on' + type].call(this, event);
142 | }
143 | },
144 | addEventListener: function (type, handler) {
145 | if (!this['_' + type + 'Handlers']) {
146 | this['_' + type + 'Handlers'] = [];
147 | }
148 |
149 | this['_' + type + 'Handlers'].push(handler);
150 | },
151 | removeEventListener: function (type, handler) {
152 | var handlers = this['_' + type + 'Handlers'];
153 | if (!handlers) {
154 | return;
155 | }
156 | for (var i = handlers.length - 1; i >= 0; --i) {
157 | if (handlers[i] === handler) {
158 | handlers.splice(i, 1);
159 | break;
160 | }
161 | }
162 | },
163 | onerror: null,
164 | onmessage: null,
165 | onopen: null,
166 | readyState: 0,
167 | URL: ''
168 | };
169 |
170 | var MessageEvent = function (data, origin, lastEventId) {
171 | this.data = data;
172 | this.origin = origin;
173 | this.lastEventId = lastEventId || '';
174 | };
175 |
176 | MessageEvent.prototype = {
177 | data: null,
178 | type: 'message',
179 | lastEventId: '',
180 | origin: ''
181 | };
182 |
183 | if ('module' in global) module.exports = EventSource;
184 | global.EventSource = EventSource;
185 |
186 | })(this);
187 |
--------------------------------------------------------------------------------
/web/js/additions.js:
--------------------------------------------------------------------------------
1 | /* Scout is an ajax library.
2 | * Copyright © 2010-2013 Thaddee Tyl. All rights reserved.
3 | * Produced under the MIT license.
4 | *
5 | * Requires Sizzle.js
6 | * Copyright 2010, The Dojo Foundation
7 | * Released under the MIT, BSD, and GPL licenses.
8 | *
9 | * Requires Json2.js by Douglas Crockford.
10 | *
11 | * Requires EventSource.js by Remy Sharp
12 | * under the MIT license .
13 | */
14 |
15 | var Scout = function(){};
16 | Scout = (function Scoutmaker () {
17 |
18 | /* xhr is in a closure. */
19 | var xhr;
20 | if (window.XMLHttpRequest) {
21 | xhr = new XMLHttpRequest();
22 | if (xhr.overrideMimeType) {
23 | xhr.overrideMimeType('text/xml'); /* Old Mozilla browsers. */
24 | }
25 | } else { /* Betting on IE, I know no other implementation. */
26 | try {
27 | xhr = new ActiveXObject("Msxml2.XMLHTTP");
28 | } catch (e) {
29 | xhr = new ActiveXObject("Microsoft.XMLHTTP");
30 | }
31 | }
32 |
33 |
34 | /* "On" property (beginning with the default parameters). */
35 |
36 | var params = {
37 | method: 'POST',
38 | resp: function (resp, xhr) {},
39 | error: function (status, xhr) {},
40 | partial: function (raw, resp, xhr) {}
41 | };
42 |
43 | /* Convert object literal to xhr-sendable. */
44 | var toxhrsend = function (data) {
45 | var str = '', start = true;
46 | var jsondata = '';
47 | for (var key in data) {
48 | if (typeof (jsondata = JSON.stringify(data[key])) === 'string') {
49 | str += (start? '': '&');
50 | str += encodeURIComponent(key) + '=' + encodeURIComponent(jsondata);
51 | if (start) { start = false; }
52 | }
53 | }
54 | return str;
55 | };
56 |
57 | var sendxhr = function (target, params) {
58 | if (params.action) { params.url = '/$' + params.action; }
59 | /* XHR stuff now. */
60 | if (params.url) {
61 | /* We have somewhere to go to. */
62 | xhr.onreadystatechange = function () {
63 | switch (xhr.readyState) {
64 | case 3:
65 | if (params.partial === undefined) {
66 | var raw = xhr.responseText;
67 | var resp;
68 | try {
69 | resp = JSON.parse(raw);
70 | } catch (e) {}
71 | params.partial.apply(target, [raw, resp, xhr]);
72 | }
73 | break;
74 | case 4:
75 | if (xhr.status === 200) {
76 | var resp = JSON.parse(xhr.responseText);
77 | params.resp.apply(target, [resp, xhr]);
78 | } else {
79 | params.error.apply(target, [xhr.status, xhr]);
80 | }
81 | break;
82 | }
83 | };
84 |
85 | // foolproof: POST requests with nothing to send are
86 | // converted to GET requests.
87 | if (params.method === 'POST'
88 | && (params.data === {} || params.data === undefined)) {
89 | params.method = 'GET';
90 | }
91 | xhr.open(params.method,
92 | params.url + (params.method === 'POST'? '':
93 | '?' + toxhrsend(params.data)),
94 | true,
95 | params.user,
96 | params.password);
97 |
98 | if (params.method === 'POST' && params.data !== {}) {
99 | xhr.setRequestHeader('Content-Type',
100 | 'application/x-www-form-urlencoded');
101 | xhr.send(toxhrsend(params.data));
102 | } else {
103 | xhr.send(null);
104 | }
105 | }
106 | };
107 |
108 | var onprop = function (eventName, before) {
109 | /* Defaults. */
110 | before = before || function (params, e, xhr) {};
111 |
112 | /* Event Listener callback. */
113 | var listenerfunc = function (e) {
114 | /* IE wrapper. */
115 | if (!e && window.event) { e = event; }
116 | var target = e.target || e.srcElement || undefined;
117 |
118 | /* We must not change page unless otherwise stated. */
119 | if (eventName === 'submit') {
120 | if (e.preventDefault) { e.preventDefault(); }
121 | else { e.returnValue = false; }
122 | }
123 | /*window.event.cancelBubble = true;
124 | if (e.stopPropagation) e.stopPropagation();*/
125 |
126 | /* User action before xhr send. */
127 | before.apply(target, [params, e, xhr]);
128 |
129 | sendxhr(target, params);
130 | };
131 |
132 | if (document.addEventListener) {
133 | this.addEventListener(eventName, listenerfunc, false);
134 | } else { /* Hoping only IE lacks addEventListener. */
135 | this.attachEvent('on' + eventName, listenerfunc);
136 | }
137 | };
138 |
139 | /* End of "on" property. */
140 |
141 |
142 | var ret = function (id) {
143 | /* Get the corresponding html element. */
144 | var domelt = document.querySelector(id);
145 | if (!domelt) {
146 | return { on: function () {} };
147 | }
148 |
149 | /* Now that we have the elt and onprop, assign it. */
150 | domelt.on = onprop;
151 | return domelt;
152 | };
153 | ret.send = function (before) {
154 | /* Fool-safe XHR creation if the current XHR object is in use. */
155 | if (xhr.readyState === 1) { return Scoutmaker().send(before); }
156 |
157 | before = before || function (params, xhr) {};
158 |
159 | return function () {
160 | before.apply(undefined, [params, xhr]);
161 | sendxhr(undefined, params);
162 | };
163 | };
164 | ret.maker = Scoutmaker;
165 |
166 | /* Wrapper for EventSource. */
167 | ret.eventSource = function (channel) {
168 | if (channel[0] !== '/') { channel = '/$' + channel; }
169 | var es = new EventSource(channel);
170 | es.onrecv = function (cb) {
171 | es.onmessage = function (event) {
172 | cb(JSON.parse(event.data));
173 | };
174 | };
175 | return es;
176 | };
177 |
178 | /* Wrapper for socket.io – if downloaded. */
179 | if (window.io) {
180 | ret.socket = function (namespace) {
181 | if (namespace === undefined) {
182 | namespace = '/'; // Default namespace.
183 | }
184 | return io.connect(namespace, {
185 | resource: '$socket.io'
186 | });
187 | };
188 | }
189 |
190 | /* If WebSockets are available, have them ready. */
191 | if (window.WebSocket) {
192 | var wsSend = function wsSendJSON (json) {
193 | // Bound by the socket.
194 | this.send(JSON.stringify(json));
195 | };
196 | ret.webSocket = function newWebSocket (channel) {
197 | var socket = new WebSocket(
198 | // Trick: use the end of either http: or https:.
199 | 'ws' + window.location.protocol.slice(4) + '//' +
200 | window.location.host +
201 | '/$websocket:' + channel);
202 | socket.sendjson = wsSend.bind(socket);
203 | return socket;
204 | };
205 | }
206 |
207 |
208 | return ret;
209 | })();
210 |
--------------------------------------------------------------------------------
/lib/License:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
167 |
--------------------------------------------------------------------------------
/test/test-api.js:
--------------------------------------------------------------------------------
1 | var camp = require('../lib/camp');
2 | var fleau = require('fleau');
3 | var http = require('http');
4 | var mime = require('../lib/mime.json');
5 | var Test = require('./test');
6 | var t = new Test();
7 | var WebSocket = require('ws');
8 |
9 | var get = function (path, callback) {
10 | http.get('http://localhost:' + portNumber + path, callback);
11 | };
12 | var post = function (path, postData, contentType, callback) {
13 | var options = {
14 | hostname: 'localhost',
15 | port: portNumber,
16 | path: path,
17 | method: 'POST'
18 | };
19 | var req = http.request(options, callback);
20 | if (contentType) req.setHeader('Content-Type', contentType);
21 | if (postData) req.write(postData);
22 | req.end();
23 | };
24 |
25 | var launchTests = function () {
26 | t.seq([
27 | function t0 (next) {
28 | get('', function (res) {
29 | t.eq(res.httpVersion, '1.1', "Server must be HTTP 1.1.");
30 | t.eq(res.headers['transfer-encoding'], 'chunked',
31 | "Connection should be chunked by default.");
32 | res.on('data', function (content) {
33 | t.eq('' + content, '404',
34 | "Did not receive content of index.html.");
35 | next();
36 | });
37 | });
38 | },
39 |
40 | function t1 (next) {
41 | // Using a streamed route.
42 | // Create a stream out of the following string.
43 | var template = '{{= text in plain}}\n{{for comment in comments{{\n- {{= comment in plain}}}} }}';
44 |
45 | server.route( /^\/blog$/, function (query, match, end) {
46 | end ({
47 | text: 'My, what a silly blog.',
48 | comments: ['first comment!', 'second comment…']
49 | }, {
50 | string: template, // A stream.
51 | reader: fleau
52 | });
53 | });
54 |
55 | // Test that now.
56 | get('/blog', function (res) {
57 | var content = '';
58 | res.on('data', function (chunk) {
59 | content += '' + chunk;
60 | });
61 | res.on('end', function () {
62 | t.eq(content, 'My, what a silly blog.\n\n- first comment!\n- second comment…',
63 | "Routing a streamed template should work.");
64 | next();
65 | });
66 | });
67 | },
68 |
69 | function t2 (next) {
70 | // Using `sc.path` with/out named non-slash placeholders (i.e. ':foo').
71 | server.path('foo', function (req, res) {
72 | res.end('foo');
73 | });
74 |
75 | server.path('foo/:bar', function (req, res) {
76 | res.end('bar=' + req.data.bar);
77 | });
78 |
79 | server.path('foo/:bar/baz', function (req, res) {
80 | res.end('baz');
81 | });
82 |
83 | get('/foo', function (res) {
84 | res.on('data', function (body) {
85 | t.eq(String(body), 'foo',
86 | 'Basic sc.path should work.');
87 |
88 | get('/foo/quux', function (res) {
89 | res.on('data', function (body) {
90 | t.eq(String(body), 'bar=quux',
91 | 'Named sc.path placeholder should work.');
92 |
93 | get('/foo/quux/baz', function (res) {
94 | res.on('data', function (body) {
95 | t.eq(String(body), 'baz',
96 | 'Named sc.path placeholder should not pre-empt sub-paths.');
97 | next();
98 | });
99 | });
100 | });
101 | });
102 | });
103 | }); // Mmmh… I love spaghetti!
104 | },
105 |
106 | function t3 (next) {
107 | var data = { message: '☃' };
108 |
109 | server.path('json', function (req, res) {
110 | res.statusCode = 418; // I'm a teapot (see RFC 2324).
111 | res.json(data, null, 2);
112 | });
113 |
114 | get('/json', function (res) {
115 | t.eq(res.statusCode, 418,
116 | 'Setting `res.statusCode` before `res.json(data)` should work.');
117 | t.eq(res.headers['content-type'], mime.json,
118 | 'Served content type should always be "' + mime.json + '".');
119 | var body = '';
120 | res.on('data', function (chunk) {
121 | body += String(chunk);
122 | });
123 | res.on('end', function () {
124 | t.eq(String(body).trim(), JSON.stringify(data, null, 2),
125 | '`res.json(data, null, 2)` should return human-readable JSON.');
126 | next();
127 | });
128 | });
129 | },
130 |
131 | function t4 (next) {
132 | var data = { id: '☃' };
133 | var things = {};
134 |
135 | server.get('things/:thing', function (req, res) {
136 | var thing = things[req.query.thing];
137 | if (!thing) {
138 | res.statusCode = 404;
139 | res.end('Could not find the thing :(');
140 | return;
141 | }
142 | res.json(thing);
143 | });
144 |
145 | server.post('things/:thing', function (req, res) {
146 | t.eq(req.headers['content-type'], mime.json,
147 | 'Request content type should be "' + mime.json + '".');
148 | var json = '';
149 | req.on('data', function (chunk) {
150 | json += String(chunk);
151 | });
152 | req.on('end', function () {
153 | var thing = JSON.parse(json);
154 | t.eq(data.id, thing.id,
155 | 'Handling JSON post data should work.');
156 | things[req.query.thing] = thing;
157 | res.statusCode = 201; // Created.
158 | res.end('Created the thing! :)');
159 | });
160 | });
161 |
162 | post('/things/snowman', JSON.stringify(data), mime.json, function (res) {
163 | t.eq(res.statusCode, 201,
164 | 'Response status should be 201, not ' + res.statusCode + '.');
165 | get('/things/snowman', function (res) {
166 | var json = '';
167 | res.on('data', function (chunk) {
168 | json += String(chunk);
169 | });
170 | res.on('end', function () {
171 | t.eq(data.id, JSON.parse(json).id,
172 | 'Should receive the original data back.');
173 | next();
174 | });
175 | });
176 | });
177 | },
178 |
179 | function t5 (next) {
180 | var name = 'Robert\'); DROP TABLE Students;--';
181 | var data = encodeURI('Name="' + name + '"');
182 | var urlencoded = 'application\/x-www-form-urlencoded';
183 |
184 | server.path('/Students.php', function (req, res) {
185 | t.eq(req.query.Name, name, 'POST data should be processed.');
186 | if (server.saveRequestChunks) {
187 | t.eq(req.savedChunks && req.savedChunks.toString(), data,
188 | 'Processed chunks should be saved in the request when required.');
189 | } else {
190 | t.eq(req.savedChunks, undefined,
191 | 'Processed chunks should not be saved when it isn\'t required.');
192 | }
193 |
194 | res.statusCode = 500; // Internal Server Error (not really)
195 | res.end('mysql> Query OK, 1337 rows affected (0.03 sec)\n');
196 | });
197 |
198 | server.saveRequestChunks = false;
199 | post('/Students.php', data, urlencoded, function (res) {
200 | t.eq(res.statusCode, 500,
201 | 'Response status should be 500, not ' + res.statusCode + '.');
202 | server.saveRequestChunks = true;
203 | post('/Students.php', data, urlencoded, function (res) {
204 | t.eq(res.statusCode, 500,
205 | 'Response status should be 500, not ' + res.statusCode + '.');
206 | next();
207 | });
208 | });
209 | },
210 |
211 | function t6 (next) {
212 | server.path('/redirection', function (req, res) {
213 | res.redirect('/other/path');
214 | });
215 | get('/redirection', function(res) {
216 | t.eq(res.statusCode, 303, 'Redirection should be a 303');
217 | t.eq(res.headers['location'], '/other/path',
218 | 'Redirection should send the right location header');
219 | next();
220 | });
221 | },
222 |
223 | function t7 (next) {
224 | server.ws('/chat', function (socket) {
225 | socket.on('message', function (data) {
226 | var replaced = String(data).replace(/hunter2/g, '*******');
227 | socket.send(replaced);
228 | });
229 | });
230 |
231 | var socket = new WebSocket('ws://localhost:' + portNumber + '/chat');
232 | socket.on('open', function () {
233 | socket.send('you can go hunter2 my hunter2-ing hunter2');
234 | });
235 | socket.on('message', function (data) {
236 | t.eq(String(data), 'you can go ******* my *******-ing *******',
237 | 'No matter how many times you type hunter2, WebSocket chat should show *******');
238 | next();
239 | });
240 | },
241 | ], function end () {
242 | t.tldr();
243 | t.exit();
244 | });
245 | };
246 |
247 | // FIXME: is there a good way to make a server get a port for testing?
248 | var server;
249 | var portNumber = 8000;
250 | var startServer = function () {
251 | server = camp.start({port:portNumber, documentRoot:'./test/web'});
252 | server.on('listening', launchTests);
253 | };
254 | var serverStartDomain = require('domain').create();
255 | serverStartDomain.on('error', function (err) {
256 | if (err.code === 'EADDRINUSE') {
257 | portNumber++;
258 | serverStartDomain.run(startServer);
259 | } else {
260 | throw err;
261 | }
262 | });
263 | serverStartDomain.run(startServer);
264 |
265 |
--------------------------------------------------------------------------------
/web/js/json2.js:
--------------------------------------------------------------------------------
1 | /*
2 | json2.js
3 | 2013-05-26
4 |
5 | Public Domain.
6 |
7 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
8 |
9 | See http://www.JSON.org/js.html
10 |
11 |
12 | This code should be minified before deployment.
13 | See http://javascript.crockford.com/jsmin.html
14 |
15 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
16 | NOT CONTROL.
17 |
18 |
19 | This file creates a global JSON object containing two methods: stringify
20 | and parse.
21 |
22 | JSON.stringify(value, replacer, space)
23 | value any JavaScript value, usually an object or array.
24 |
25 | replacer an optional parameter that determines how object
26 | values are stringified for objects. It can be a
27 | function or an array of strings.
28 |
29 | space an optional parameter that specifies the indentation
30 | of nested structures. If it is omitted, the text will
31 | be packed without extra whitespace. If it is a number,
32 | it will specify the number of spaces to indent at each
33 | level. If it is a string (such as '\t' or ' '),
34 | it contains the characters used to indent at each level.
35 |
36 | This method produces a JSON text from a JavaScript value.
37 |
38 | When an object value is found, if the object contains a toJSON
39 | method, its toJSON method will be called and the result will be
40 | stringified. A toJSON method does not serialize: it returns the
41 | value represented by the name/value pair that should be serialized,
42 | or undefined if nothing should be serialized. The toJSON method
43 | will be passed the key associated with the value, and this will be
44 | bound to the value
45 |
46 | For example, this would serialize Dates as ISO strings.
47 |
48 | Date.prototype.toJSON = function (key) {
49 | function f(n) {
50 | // Format integers to have at least two digits.
51 | return n < 10 ? '0' + n : n;
52 | }
53 |
54 | return this.getUTCFullYear() + '-' +
55 | f(this.getUTCMonth() + 1) + '-' +
56 | f(this.getUTCDate()) + 'T' +
57 | f(this.getUTCHours()) + ':' +
58 | f(this.getUTCMinutes()) + ':' +
59 | f(this.getUTCSeconds()) + 'Z';
60 | };
61 |
62 | You can provide an optional replacer method. It will be passed the
63 | key and value of each member, with this bound to the containing
64 | object. The value that is returned from your method will be
65 | serialized. If your method returns undefined, then the member will
66 | be excluded from the serialization.
67 |
68 | If the replacer parameter is an array of strings, then it will be
69 | used to select the members to be serialized. It filters the results
70 | such that only members with keys listed in the replacer array are
71 | stringified.
72 |
73 | Values that do not have JSON representations, such as undefined or
74 | functions, will not be serialized. Such values in objects will be
75 | dropped; in arrays they will be replaced with null. You can use
76 | a replacer function to replace those with JSON values.
77 | JSON.stringify(undefined) returns undefined.
78 |
79 | The optional space parameter produces a stringification of the
80 | value that is filled with line breaks and indentation to make it
81 | easier to read.
82 |
83 | If the space parameter is a non-empty string, then that string will
84 | be used for indentation. If the space parameter is a number, then
85 | the indentation will be that many spaces.
86 |
87 | Example:
88 |
89 | text = JSON.stringify(['e', {pluribus: 'unum'}]);
90 | // text is '["e",{"pluribus":"unum"}]'
91 |
92 |
93 | text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
94 | // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
95 |
96 | text = JSON.stringify([new Date()], function (key, value) {
97 | return this[key] instanceof Date ?
98 | 'Date(' + this[key] + ')' : value;
99 | });
100 | // text is '["Date(---current time---)"]'
101 |
102 |
103 | JSON.parse(text, reviver)
104 | This method parses a JSON text to produce an object or array.
105 | It can throw a SyntaxError exception.
106 |
107 | The optional reviver parameter is a function that can filter and
108 | transform the results. It receives each of the keys and values,
109 | and its return value is used instead of the original value.
110 | If it returns what it received, then the structure is not modified.
111 | If it returns undefined then the member is deleted.
112 |
113 | Example:
114 |
115 | // Parse the text. Values that look like ISO date strings will
116 | // be converted to Date objects.
117 |
118 | myData = JSON.parse(text, function (key, value) {
119 | var a;
120 | if (typeof value === 'string') {
121 | a =
122 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
123 | if (a) {
124 | return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
125 | +a[5], +a[6]));
126 | }
127 | }
128 | return value;
129 | });
130 |
131 | myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
132 | var d;
133 | if (typeof value === 'string' &&
134 | value.slice(0, 5) === 'Date(' &&
135 | value.slice(-1) === ')') {
136 | d = new Date(value.slice(5, -1));
137 | if (d) {
138 | return d;
139 | }
140 | }
141 | return value;
142 | });
143 |
144 |
145 | This is a reference implementation. You are free to copy, modify, or
146 | redistribute.
147 | */
148 |
149 | /*jslint evil: true, regexp: true */
150 |
151 | /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
152 | call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
153 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
154 | lastIndex, length, parse, prototype, push, replace, slice, stringify,
155 | test, toJSON, toString, valueOf
156 | */
157 |
158 |
159 | // Create a JSON object only if one does not already exist. We create the
160 | // methods in a closure to avoid creating global variables.
161 |
162 | if (typeof JSON !== 'object') {
163 | JSON = {};
164 | }
165 |
166 | (function () {
167 | 'use strict';
168 |
169 | function f(n) {
170 | // Format integers to have at least two digits.
171 | return n < 10 ? '0' + n : n;
172 | }
173 |
174 | if (typeof Date.prototype.toJSON !== 'function') {
175 |
176 | Date.prototype.toJSON = function () {
177 |
178 | return isFinite(this.valueOf())
179 | ? this.getUTCFullYear() + '-' +
180 | f(this.getUTCMonth() + 1) + '-' +
181 | f(this.getUTCDate()) + 'T' +
182 | f(this.getUTCHours()) + ':' +
183 | f(this.getUTCMinutes()) + ':' +
184 | f(this.getUTCSeconds()) + 'Z'
185 | : null;
186 | };
187 |
188 | String.prototype.toJSON =
189 | Number.prototype.toJSON =
190 | Boolean.prototype.toJSON = function () {
191 | return this.valueOf();
192 | };
193 | }
194 |
195 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
196 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
197 | gap,
198 | indent,
199 | meta = { // table of character substitutions
200 | '\b': '\\b',
201 | '\t': '\\t',
202 | '\n': '\\n',
203 | '\f': '\\f',
204 | '\r': '\\r',
205 | '"' : '\\"',
206 | '\\': '\\\\'
207 | },
208 | rep;
209 |
210 |
211 | function quote(string) {
212 |
213 | // If the string contains no control characters, no quote characters, and no
214 | // backslash characters, then we can safely slap some quotes around it.
215 | // Otherwise we must also replace the offending characters with safe escape
216 | // sequences.
217 |
218 | escapable.lastIndex = 0;
219 | return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
220 | var c = meta[a];
221 | return typeof c === 'string'
222 | ? c
223 | : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
224 | }) + '"' : '"' + string + '"';
225 | }
226 |
227 |
228 | function str(key, holder) {
229 |
230 | // Produce a string from holder[key].
231 |
232 | var i, // The loop counter.
233 | k, // The member key.
234 | v, // The member value.
235 | length,
236 | mind = gap,
237 | partial,
238 | value = holder[key];
239 |
240 | // If the value has a toJSON method, call it to obtain a replacement value.
241 |
242 | if (value && typeof value === 'object' &&
243 | typeof value.toJSON === 'function') {
244 | value = value.toJSON(key);
245 | }
246 |
247 | // If we were called with a replacer function, then call the replacer to
248 | // obtain a replacement value.
249 |
250 | if (typeof rep === 'function') {
251 | value = rep.call(holder, key, value);
252 | }
253 |
254 | // What happens next depends on the value's type.
255 |
256 | switch (typeof value) {
257 | case 'string':
258 | return quote(value);
259 |
260 | case 'number':
261 |
262 | // JSON numbers must be finite. Encode non-finite numbers as null.
263 |
264 | return isFinite(value) ? String(value) : 'null';
265 |
266 | case 'boolean':
267 | case 'null':
268 |
269 | // If the value is a boolean or null, convert it to a string. Note:
270 | // typeof null does not produce 'null'. The case is included here in
271 | // the remote chance that this gets fixed someday.
272 |
273 | return String(value);
274 |
275 | // If the type is 'object', we might be dealing with an object or an array or
276 | // null.
277 |
278 | case 'object':
279 |
280 | // Due to a specification blunder in ECMAScript, typeof null is 'object',
281 | // so watch out for that case.
282 |
283 | if (!value) {
284 | return 'null';
285 | }
286 |
287 | // Make an array to hold the partial results of stringifying this object value.
288 |
289 | gap += indent;
290 | partial = [];
291 |
292 | // Is the value an array?
293 |
294 | if (Object.prototype.toString.apply(value) === '[object Array]') {
295 |
296 | // The value is an array. Stringify every element. Use null as a placeholder
297 | // for non-JSON values.
298 |
299 | length = value.length;
300 | for (i = 0; i < length; i += 1) {
301 | partial[i] = str(i, value) || 'null';
302 | }
303 |
304 | // Join all of the elements together, separated with commas, and wrap them in
305 | // brackets.
306 |
307 | v = partial.length === 0
308 | ? '[]'
309 | : gap
310 | ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']'
311 | : '[' + partial.join(',') + ']';
312 | gap = mind;
313 | return v;
314 | }
315 |
316 | // If the replacer is an array, use it to select the members to be stringified.
317 |
318 | if (rep && typeof rep === 'object') {
319 | length = rep.length;
320 | for (i = 0; i < length; i += 1) {
321 | if (typeof rep[i] === 'string') {
322 | k = rep[i];
323 | v = str(k, value);
324 | if (v) {
325 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
326 | }
327 | }
328 | }
329 | } else {
330 |
331 | // Otherwise, iterate through all of the keys in the object.
332 |
333 | for (k in value) {
334 | if (Object.prototype.hasOwnProperty.call(value, k)) {
335 | v = str(k, value);
336 | if (v) {
337 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
338 | }
339 | }
340 | }
341 | }
342 |
343 | // Join all of the member texts together, separated with commas,
344 | // and wrap them in braces.
345 |
346 | v = partial.length === 0
347 | ? '{}'
348 | : gap
349 | ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}'
350 | : '{' + partial.join(',') + '}';
351 | gap = mind;
352 | return v;
353 | }
354 | }
355 |
356 | // If the JSON object does not yet have a stringify method, give it one.
357 |
358 | if (typeof JSON.stringify !== 'function') {
359 | JSON.stringify = function (value, replacer, space) {
360 |
361 | // The stringify method takes a value and an optional replacer, and an optional
362 | // space parameter, and returns a JSON text. The replacer can be a function
363 | // that can replace values, or an array of strings that will select the keys.
364 | // A default replacer method can be provided. Use of the space parameter can
365 | // produce text that is more easily readable.
366 |
367 | var i;
368 | gap = '';
369 | indent = '';
370 |
371 | // If the space parameter is a number, make an indent string containing that
372 | // many spaces.
373 |
374 | if (typeof space === 'number') {
375 | for (i = 0; i < space; i += 1) {
376 | indent += ' ';
377 | }
378 |
379 | // If the space parameter is a string, it will be used as the indent string.
380 |
381 | } else if (typeof space === 'string') {
382 | indent = space;
383 | }
384 |
385 | // If there is a replacer, it must be a function or an array.
386 | // Otherwise, throw an error.
387 |
388 | rep = replacer;
389 | if (replacer && typeof replacer !== 'function' &&
390 | (typeof replacer !== 'object' ||
391 | typeof replacer.length !== 'number')) {
392 | throw new Error('JSON.stringify');
393 | }
394 |
395 | // Make a fake root object containing our value under the key of ''.
396 | // Return the result of stringifying the value.
397 |
398 | return str('', {'': value});
399 | };
400 | }
401 |
402 |
403 | // If the JSON object does not yet have a parse method, give it one.
404 |
405 | if (typeof JSON.parse !== 'function') {
406 | JSON.parse = function (text, reviver) {
407 |
408 | // The parse method takes a text and an optional reviver function, and returns
409 | // a JavaScript value if the text is a valid JSON text.
410 |
411 | var j;
412 |
413 | function walk(holder, key) {
414 |
415 | // The walk method is used to recursively walk the resulting structure so
416 | // that modifications can be made.
417 |
418 | var k, v, value = holder[key];
419 | if (value && typeof value === 'object') {
420 | for (k in value) {
421 | if (Object.prototype.hasOwnProperty.call(value, k)) {
422 | v = walk(value, k);
423 | if (v !== undefined) {
424 | value[k] = v;
425 | } else {
426 | delete value[k];
427 | }
428 | }
429 | }
430 | }
431 | return reviver.call(holder, key, value);
432 | }
433 |
434 |
435 | // Parsing happens in four stages. In the first stage, we replace certain
436 | // Unicode characters with escape sequences. JavaScript handles many characters
437 | // incorrectly, either silently deleting them, or treating them as line endings.
438 |
439 | text = String(text);
440 | cx.lastIndex = 0;
441 | if (cx.test(text)) {
442 | text = text.replace(cx, function (a) {
443 | return '\\u' +
444 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
445 | });
446 | }
447 |
448 | // In the second stage, we run the text against regular expressions that look
449 | // for non-JSON patterns. We are especially concerned with '()' and 'new'
450 | // because they can cause invocation, and '=' because it can cause mutation.
451 | // But just to be safe, we want to reject all unexpected forms.
452 |
453 | // We split the second stage into 4 regexp operations in order to work around
454 | // crippling inefficiencies in IE's and Safari's regexp engines. First we
455 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
456 | // replace all simple value tokens with ']' characters. Third, we delete all
457 | // open brackets that follow a colon or comma or that begin the text. Finally,
458 | // we look to see that the remaining characters are only whitespace or ']' or
459 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
460 |
461 | if (/^[\],:{}\s]*$/
462 | .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
463 | .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
464 | .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
465 |
466 | // In the third stage we use the eval function to compile the text into a
467 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity
468 | // in JavaScript: it can begin a block or an object literal. We wrap the text
469 | // in parens to eliminate the ambiguity.
470 |
471 | j = eval('(' + text + ')');
472 |
473 | // In the optional fourth stage, we recursively walk the new structure, passing
474 | // each name/value pair to a reviver function for possible transformation.
475 |
476 | return typeof reviver === 'function'
477 | ? walk({'': j}, '')
478 | : j;
479 | }
480 |
481 | // If the text is not JSON parseable, then a SyntaxError is thrown.
482 |
483 | throw new SyntaxError('JSON.parse');
484 | };
485 | }
486 | }());
487 |
--------------------------------------------------------------------------------
/doc/Readme.md:
--------------------------------------------------------------------------------
1 | SCOUT CAMP
2 | ==========
3 |
4 |
5 | Dealing with server systems can be alleviated by systems which allow clear
6 | distinction between:
7 |
8 | * serving pages; and
9 | * powering applications.
10 |
11 |
12 | Camp.js
13 | -------
14 |
15 | We start the web server.
16 |
17 | ```js
18 | var camp = require('camp').start();
19 | ```
20 |
21 | The `start()` function has the following properties:
22 |
23 | - `documentRoot`: the path to the directory containing the static files you
24 | serve (and the template files, potentially). If your website is made of HTML
25 | pages, this is where they are located. Defaults to `./web`.
26 | - `templateReader`: the default template engine used. See below.
27 | - `passphrase`, `key`, `cert`, `ca`: in the case of a secure website (using
28 | HTTPS), those are fields you may specify to indicate where to find information
29 | about the website's security. Defaults include "https.key", "https.crt", and,
30 | as the CA (Certificate Authority, a list of certificates) an empty list.
31 | - `setuid`: once the server has made the connection, set the user id to
32 | something else. This is particularly useful if you don't want the server to
33 | run as the almighty root user. However, executing this requires to be root (so
34 | you will need to use `sudo` or the like to run the server).
35 | - `saveRequestChunks`: some requests will have their data processed by Camp
36 | before reaching your handlers, in order to fill the Augmented Request's
37 | `req.data` dictionary, but this means that you won't receive any of the
38 | processed chunks by doing `req.on('data',function(chunk){})` in your handler.
39 | If you need to access these raw chunks (e.g. to pipe complete requests to a
40 | different server), you'll find them in `req.chunks` when `saveRequestChunks`
41 | is set to `true`.
42 |
43 | The result of `require('camp')` can also be useful, for instance, to log
44 | warnings from the server. The logging system uses
45 | [multilog](https://www.npmjs.org/package/multilog).
46 |
47 | ```js
48 | var Camp = require('camp');
49 | Camp.log.unpipe('warn', 'stderr');
50 | // There are three streams: warn, error, and all.
51 | // warn and error are individually piped to stderr by default.
52 | ```
53 |
54 | ### Ajax
55 |
56 | The Camp.js engine targets ease of use of both serving plain html files and ajax
57 | calls. By default, when given a request, it looks for files in the `./web/`
58 | directory. However, it also has the concept of Ajax actions.
59 |
60 | ```js
61 | camp.ajax.on('getinfo', function(json, end, ask) {
62 | console.log(json);
63 | end(json); // Send that back to the client.
64 | });
65 | ```
66 |
67 | An action maps a string to the path request `/$`. When a client asks
68 | for this resource, sending in information stored in the "json" parameter,
69 | Camp.js will send it back the object literal that the callback function gives.
70 |
71 | In the example given, it merely sends back whatever information the client
72 | gives, which is a very contrived example.
73 |
74 | The purpose of this distinction between normally served html pages and ajax
75 | actions is to treat servers more like applications. You first serve the
76 | graphical interface, in html and css, and then, you let the user interact with
77 | the server's data seemlessly through ajax calls.
78 |
79 | Note that the `json` parameter given is a single object containing all
80 | parameters from the following sources:
81 |
82 | - the query string from GET requests
83 | - POST requests with enctype application/x-www-form-urlencoded
84 | - POST requests with enctype multipart/form-data. This one uses the same API as
85 | [formidable](https://github.com/felixge/node-formidable) for file objects.
86 |
87 | You also get an [Ask](#the-ask-class) object, see below.
88 |
89 | Before downloading POST Ajax data, you can hook a function up using the
90 | following code:
91 |
92 | ```js
93 | camp.ajaxReq.on('getinfo', function(ask) { … });
94 | ```
95 |
96 | That can be useful to give information about the progress of an upload, for
97 | instance, using `ask.form.on('progress', function(bytesReceived, bytesExpected) {})`.
98 |
99 | ### EventSource
100 |
101 | Let's build a path named `/path`. When we receive a call on `/talk`, we send
102 | the data it gives us to the EventSource path.
103 |
104 | ```js
105 | // This is actually a full-fledged chat.
106 | var chat = camp.eventSource ( '/all' );
107 | camp.post('/talk', function(req, res) { chat.send(req.data); res.end(); });
108 | ```
109 |
110 | This EventSource object we get has two methods:
111 |
112 | - The `send` method takes a JSON object and emits the `message` event to the
113 | client. It is meant to be used with `es.onrecv`.
114 | - The `emit` method takes an event name and a textual message and emits this
115 | event with that message to the client. It is meant to be used with
116 | `es.on(event, callback)`.
117 |
118 | ### WebSocket
119 |
120 | We also include the raw duplex communication system provided by the WebSocket
121 | protocol.
122 |
123 | ```js
124 | camp.ws('/path', function(socket));
125 | ```
126 |
127 | Every time a WebSocket connection is initiated (say, by a Web browser), the
128 | function is run. The `socket` is an instance of [ws.WebSocket]
129 | (https://github.com/einaros/ws/blob/master/doc/ws.md#class-wswebsocket).
130 | Usually, you only need to know about `socket.on('message', function(data))`,
131 | and `socket.send(data)`.
132 |
133 | This function returns an instance of a [WebSocket server]
134 | (https://github.com/einaros/ws/blob/master/doc/ws.md#class-wsserver)
135 | for that path.
136 | Most notably, it has a `wsServer.clients` list of opened sockets on a path.
137 |
138 | A map from paths to WebSocket servers is available at:
139 |
140 | ```js
141 | camp.wsChannels[path];
142 | ```
143 |
144 | For the purpose of broadcasting (ie, sending messages to every connected socket
145 | on the path), we provide the following function.
146 |
147 | ```js
148 | camp.wsBroadcast('/path', function recv(req, res))
149 | ```
150 |
151 | The `recv` function is run once every time a client sends data.
152 |
153 | - Its `req` parameter provides `req.data` (the data that a client sent), and
154 | `req.flags` (`req.flags.binary` is true if binary data is received;
155 | `req.flags.masked` if the data was masked).
156 | - Its `res` parameter provides `res.send(data)`, which sends the same data to each socket on the path.
157 |
158 | Client-side, obviously, your browser needs to have a
159 | [WebSocket API](http://caniuse.com/#feat=websockets).
160 | The client-side code may look like this.
161 |
162 | ```js
163 | // `socket` is a genuine WebSocket instance.
164 | var socket = new WebSocket('/path');
165 | socket.send(JSON.stringify({ some: "data" }));
166 | ```
167 |
168 | ### Socket.io
169 |
170 | Be warned before you read on: the Socket.io interface is deprecated.
171 | Use the WebSocket interface provided above instead.
172 | Also, do not use *both* the socket.io interface and the WebSocket interface.
173 | That seems to be asking for trouble.
174 |
175 | We also include the duplex communication system that socket.io provides. When
176 | you start the server, by default, socket.io is already launched. You can use its
177 | APIs as documented at from the `camp.io` object.
178 |
179 | ```js
180 | camp.io.sockets.on('connection', function (socket) { … });
181 | ```
182 |
183 | On the client-side, `Scout.js` also provides shortcuts, through its
184 | `Scout.socket(namespace)` function. Calling `Scout.socket()` returns the
185 | documented Socket.io object that you can use according to their API.
186 |
187 | ```js
188 | var io = Scout.socket();
189 | io.emit('event name', {data: 'to send'});
190 | io.on('event name', function (jsonObject) { … });
191 | ```
192 |
193 | ### Handlers
194 |
195 | If you want a bit of code to be executed on every request, or if you want to
196 | manually manage requests at a low level without all that fluff described above,
197 | you can add handlers to the server.
198 |
199 | Each request goes through each handler you provided in the order you provided
200 | them. Unless a handler calls `next()`, the request gets caught by that handler:
201 |
202 | 1. None of the handlers after that one get called,
203 | 2. None of the subsequent layers of Camp (such as WebSocket, EventSource,
204 | Route…) get called.
205 |
206 | Otherwise, all the handlers get called, and the request will get caught by one
207 | of the subsequent layers of Camp.
208 |
209 | ```js
210 | var addOSSHeader = function(req, res, next) {
211 | ask.res.setHeader('X-Open-Source', 'https://github.com/espadrine/sc/');
212 | next();
213 | };
214 | camp.handle(addOSSHeader);
215 | // There's no reason to remove that amazing handler, but if that was what
216 | // floated your boat, here is how you would do that:
217 | camp.removeHandler(addOSSHeader);
218 | ```
219 |
220 |
221 | Templates
222 | ---------
223 |
224 | An associated possibility, very much linked to the normal use of Camp.js, is to
225 | handle templates. Those are server-side preprocessed files.
226 |
227 | ### Basic Usage
228 |
229 | Mostly, you first decide where to put your template file. Let's say we have
230 | such a file at `/first/post.html` (from the root of the web/ or publish/
231 | directory).
232 |
233 | ```js
234 | var posts = ['This is the f1rst p0st!'];
235 |
236 | camp.path( 'first/post.html', function(req, res) {
237 | res.template({
238 | text: posts[0],
239 | comments: ['first comment!', 'second comment…']
240 | });
241 | });
242 | ```
243 |
244 | `req` is an Augmented Request, and `res` an Augmented Response.
245 | Therefore, if the request is `/first/post.html?key=value`, then `req.data.key`
246 | will be "value".
247 |
248 | `res.template(scope, templates)` responds to the request with a list of
249 | templates (produced with `Camp.template()` or `camp.template()`), a single
250 | template, or no template:
251 | in the latter case, the URI's path will be treated as a template file on disk
252 | under `documentRoot`. This is the case here with "first/post.html".
253 |
254 | The file `/web/first/post.html` might look like this:
255 |
256 | ```html
257 |
258 | {{= text in html}}
259 |
260 | {{for comment in comments {{
261 | {{= comment in html}}
262 | }}}}
263 |
264 | ```
265 |
266 | Because it will be preprocessed server-side, the browser will actually receive
267 | the following file:
268 |
269 | ```html
270 |
271 | This is the f1rst p0st!
272 |
273 | first comment!
274 | second comment...
275 |
276 | ```
277 |
278 | If you need to specify a different template, you can do so:
279 |
280 | ```js
281 | var postsTemplate = Camp.template( './templates/posts.html' );
282 | camp.path('posts', function(req, res) {
283 | res.template({comments: comments}, postsTemplate);
284 | });
285 | ```
286 |
287 | `Camp.template(paths, options)` takes an Array of String paths to templating
288 | files (or a single path to a templating file), and the following options:
289 | - reader: the template reader function in use, defaulting to
290 | `camp.templateReader`, which defaults to
291 | [Fleau](https://github.com/espadrine/fleau).
292 | - asString: boolean; use the string as a template, not as a file path.
293 | - callback: function taking a function(scope) → readableStream.
294 | If you don't want the template creation to be synchronous, use this.
295 | We return nothing from the function if `callback` is set.
296 |
297 | This function returns a function(scope) → readableStream, unless `callback` is
298 | set.
299 |
300 | So this is how to be explicit about the template. On the opposite extreme, you
301 | can be extra implicit: the URL path will them be used as the template path on
302 | disk, and `req.data` will be used as the template's scope.
303 |
304 | ```js
305 | // Supports ?mobile=true
306 | camp.path('blog.html');
307 | ```
308 |
309 |
310 | ## Fall through
311 |
312 | ```js
313 | camp.notFound( 'blog/*', function(req, res) {
314 | res.file('/templates/404.html');
315 | });
316 | ```
317 |
318 | The `camp.notFound()` function works in exactly the same way as the
319 | `camp.path()` function, with two important differences:
320 |
321 | 1. It only gets used when nothing else matches the path, including paths and
322 | static files on disk under `documentRoot`,
323 | 2. It responds with a 404 (Not Found) status code.
324 |
325 |
326 |
327 | Camp In Depth
328 | -------------
329 |
330 | In Camp.js, there is a lot more than meets the eye. Up until now, we have only
331 | discussed the default behaviour of ScoutCamp. For most uses, this is actually
332 | more than enough. Sometimes, however, you need to dig a little deeper.
333 |
334 | ### The Camp Object
335 |
336 | `Camp.start` is the simple way to launch the server in a single line. You may
337 | not know, however, that it returns an `http.Server` (or an `https.Server`)
338 | subclass instance. As a result, you can use all node.js' HTTP and HTTPS
339 | methods.
340 |
341 | You may provide the `start` function with a JSON object defining the server's
342 | settings. It defaults to this:
343 |
344 | ```js
345 | {
346 | port: 80, // The port to listen to.
347 | hostname: '::', // The hostname to use as a server
348 | security: {
349 | secure: true,
350 | key: 'https.key', // Either the name of a file on disk,
351 | cert: 'https.crt', // or the content as a String.
352 | ca: ['https.ca']
353 | }
354 | }
355 | ```
356 |
357 | If you provide the relevant HTTPS files and set the `secure` option to true, the
358 | server will be secure.
359 |
360 | `Camp.createServer()` creates a Camp instance directly, and
361 | `Camp.createSecureServer(settings)` creates an HTTPS Camp instance. The latter
362 | takes the same parameters as `https.Server`.
363 |
364 | `Camp.Camp` and `Camp.SecureCamp` are the class constructors.
365 |
366 |
367 | ### The stack
368 |
369 | Camp is stack-based. When we receive a request, it goes through all the layers
370 | of the stack until it hits the bottom. It should never hit the bottom: each
371 | layer can either pass it on to the next, or end the request (by sending a
372 | response).
373 |
374 | The default stack is defined this way:
375 |
376 | ```js
377 | campInstance.stack = [wsLayer, ajaxLayer, eventSourceLayer, pathLayer
378 | routeLayer, staticLayer, notfoundLayer];
379 | ```
380 |
381 | Each element of the stack `function(req, res, next){}` takes two parameters:
382 |
383 | - augmented [IncomingMessage][] (`req`) and [ServerResponse][] (`res`)
384 | (more on that below),
385 | - a `next` function, which the layer may call if it will not send an HTTP
386 | response itself. The layer that does catch the request and responds fully to
387 | it will not call `next()`, the others will call `next()`.
388 |
389 | [IncomingMessage]: https://nodejs.org/api/http.html#http_class_http_incomingmessage
390 | [ServerResponse]: https://nodejs.org/api/http.html#http_class_http_serverresponse
391 |
392 | You can add layers to the stack with `handle()`, which is described way above.
393 |
394 | ```js
395 | camp.handle(function(ask, next) {
396 | ask.res.setHeader('X-Open-Source', 'https://github.com/espadrine/sc/');
397 | next();
398 | });
399 | ```
400 |
401 | By default, it inserts it before the `wsLayer`, but after other inserted
402 | handlers. Its insertion point is at `camp.stackInsertion` (an integer).
403 |
404 | ### Ask and Augmented Request
405 |
406 | The **Ask class** is a way to provide a lot of useful elements associated with
407 | a request. It contains the following fields:
408 |
409 | - server: the Camp instance,
410 | - req: the [http.IncomingMessage](http://nodejs.org/api/http.html#http_http_incomingmessage) object.
411 | - res: the [http.ServerResponse](http://nodejs.org/api/http.html#http_class_http_serverresponse) object.
412 | - uri: the URI.
413 | - path: the pathname associated with the request.
414 | - query: the query taken from the URI.
415 | - cookies: using the [cookies](https://github.com/pillarjs/cookies) library.
416 | - form: a `formidable.IncomingForm` object as specified by
417 | the [formidable](https://github.com/felixge/node-formidable)
418 | library API. Noteworthy are `form.uploadDir` (where the files are uploaded,
419 | this property is settable),
420 | `form.path` (where the uploaded file resides),
421 | and `form.on('progress', function(bytesReceived, bytesExpected) {})`.
422 | - username, password: in the case of a Basic Authentication HTTP request, parses
423 | the contents of the request and places the username and password as strings in
424 | those fields.
425 |
426 | An `Ask` instance is provided as an extra parameter to
427 | `camp.route(pattern, function(query, path, end, ask))`
428 | (see the start of section "Diving In"),
429 | and as a parameter in each function of the server's stack
430 | `function(ask, next)`
431 | (see the start of section "The stack").
432 |
433 | An **Augmented Request** is an [IncomingMessage][] which has several additional
434 | fields which you can also find in `Ask`: `server`, `uri`, `form`, `path`,
435 | `data` (which is the same as `query`), `username`, `password`, `cookies`.
436 |
437 | It also contains form information for `multipart/form-data` requests in the
438 | following fields:
439 | - form: a `formidable.IncomingForm` object as specified by
440 | the [formidable](https://github.com/felixge/node-formidable)
441 | library API. Noteworthy are `form.uploadDir` (where the files are uploaded)
442 | and `form.on('progress', function(bytesReceived, bytesExpected) {})`.
443 | - fields: a map from the field name (eg, `fieldname` for
444 | ` `) to the corresponding form values.
445 | - files: a map from the field name (eg, `fieldname` for
446 | ` `) to a list of files, each with properties:
447 | - path: the location on disk where the the file resides
448 | - name: the name of the file, as asserted by the uploader.
449 |
450 | An **Augmented Response** is a [ServerResponse][] which also has:
451 |
452 | - `template(scope, templates)`: responds to the request with a list of templates
453 | (produced with `Camp.template()` or `camp.template()`), a single template, or
454 | no template (in which case, the URI's path will be treated as a template file
455 | on disk under `documentRoot`). The `scope` is a JS object used to fill in the
456 | template.
457 | - `file(path)`: responds to the request with the contents of the file at `path`,
458 | on disk under `documentRoot`.
459 | - `json(data, replacer, space)`: responds to the request with stringified JSON
460 | data. Arguments are passed to `JSON.stringify()`, so you can use either
461 | `res.json({a: 42})` (minified) or `res.json({a: 42}, null, 2)`
462 | (human-readable).
463 | - `compressed()`: returns a writable stream. All data sent to that stream gets
464 | compressed and sent as a response.
465 | - `redirect(path)`: responds to the request with a 303 redirection to a path
466 | or URL.
467 |
468 | Note: `file(path)` leverages browser caching by comparing `If-Modified-Since`
469 | request headers against actual file timestamps, and saves time and bandwidth by
470 | replying "304 Not Modified" with no content to requests where the browser
471 | already knows the latest version of a file. However, this header is limited to
472 | second-level precision by specification, so any file changes happening within
473 | the same second, or within a 2-second window in the case of leap seconds, cause
474 | a small risk of browsers fetching and caching a stale version of the file in
475 | between these changes. Such a cached version would remain stale until the next
476 | file change and subsequent browser request updating the cache.
477 |
478 | Additionally, you can set the mime type of the response with
479 | `req.mime('png')`, for instance.
480 |
481 | ### Default layers
482 |
483 | The default layers provided are generated from what we call units, which are
484 | exported as shown below. Each unit is a function that takes a server instance
485 | and returns a layer (`function(ask, next){}`).
486 |
487 | - `Camp.ajaxUnit` (seen previously)
488 | - `Camp.socketUnit` (idem)
489 | - `Camp.wsUnit` (idem)
490 | - `Camp.eventSourceUnit` (idem)
491 | - `Camp.pathUnit` (idem)
492 | - `Camp.routeUnit` (idem)
493 | - `Camp.staticUnit` (idem, relies on `camp.documentRoot` which specifies the
494 | location of the root of your static web files. The default is "./web".
495 | - `Camp.notfoundUnit` (idem)
496 |
497 | Scout.js
498 | --------
499 |
500 | ### XHR
501 |
502 | Browsers' built-in Ajax libraries are usually poor. They are not cross-browser
503 | (because of Internet Explorer) and they can quickly become a hassle. Scout.js
504 | is a javascript library to remove that hassle.
505 |
506 | With Scout.js, one can easily target a specific element in the page which
507 | must trigger an XHR(XML Http Request) when a specific event is fired. This is
508 | what you do, most of the time, anyway. Otherwise, it is also easy to attach an
509 | XHR upon a "setTimeout", and so on.
510 |
511 | ```js
512 | Scout ( '#id-of-element' ).on ( 'click', function (params, evt, xhr) {
513 | params.action = 'getinfo';
514 | var sent = this.parentNode.textContent;
515 | params.data = { ready: true, data: sent };
516 | params.resp = function ( resp, xhr ) {
517 | if (resp.data === sent) {
518 | console.log ('Got exactly what we sent.');
519 | }
520 | };
521 | });
522 |
523 | // or...
524 |
525 | setTimeout ( Scout.send ( function ( params, xhr ) { … } ), 1000 );
526 | ```
527 |
528 | One thing that can bite is the fact that each Scout object only has one XHR
529 | object inside. If you do two Ajax roundtrips at the same time, with the same
530 | Scout object, one will cancel the other.
531 |
532 | This behavior is very easy to spot. On the Web Inspector of your navigator, in
533 | the "Network" tab, if a `$action` POST request is red (or cancelled), it means
534 | that it was killed by another XHR call.
535 |
536 | The cure is to create another Scout object through the
537 | `var newScout = Scout.maker()` call.
538 |
539 | ### Server-Sent Events
540 |
541 | All modern browsers support a mechanism for receiving a continuous,
542 | event-driven flow of information from the server. This technology is called
543 | *Server-Sent Events*.
544 |
545 | The bad news about it is that it is a hassle to set up server-side. The good
546 | news is that you are using ScoutCamp, which makes it a breeze. Additionally,
547 | ScoutCamp makes it work even in IE7.
548 |
549 | ```js
550 | var es = Scout.eventSource('/path');
551 |
552 | es.on('eventName', function (data) {
553 | // `data` is a string.
554 | });
555 |
556 | es.onrecv(function (json) {
557 | // `json` is a JSON object.
558 | });
559 | ```
560 | - - -
561 |
562 | Thaddee Tyl, author of ScoutCamp.
563 |
--------------------------------------------------------------------------------
/scout.js:
--------------------------------------------------------------------------------
1 | (function(window){var i,support,cachedruns,Expr,getText,isXML,compile,outermostContext,sortInput,setDocument,document,docElem,documentIsHTML,rbuggyQSA,rbuggyMatches,matches,contains,expando="sizzle"+-new Date,preferredDoc=window.document,dirruns=0,done=0,classCache=createCache(),tokenCache=createCache(),compilerCache=createCache(),hasDuplicate=false,sortOrder=function(a,b){if(a===b){hasDuplicate=true;return 0}return 0},strundefined=typeof undefined,MAX_NEGATIVE=1<<31,hasOwn={}.hasOwnProperty,arr=[],pop=arr.pop,push_native=arr.push,push=arr.push,slice=arr.slice,indexOf=arr.indexOf||function(elem){var i=0,len=this.length;for(;i+~]|"+whitespace+")"+whitespace+"*"),rsibling=new RegExp(whitespace+"*[+~]"),rattributeQuotes=new RegExp("="+whitespace+"*([^\\]'\"]*)"+whitespace+"*\\]","g"),rpseudo=new RegExp(pseudos),ridentifier=new RegExp("^"+identifier+"$"),matchExpr={ID:new RegExp("^#("+characterEncoding+")"),CLASS:new RegExp("^\\.("+characterEncoding+")"),TAG:new RegExp("^("+characterEncoding.replace("w","w*")+")"),ATTR:new RegExp("^"+attributes),PSEUDO:new RegExp("^"+pseudos),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+whitespace+"*(even|odd|(([+-]|)(\\d*)n|)"+whitespace+"*(?:([+-]|)"+whitespace+"*(\\d+)|))"+whitespace+"*\\)|)","i"),bool:new RegExp("^(?:"+booleans+")$","i"),needsContext:new RegExp("^"+whitespace+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+whitespace+"*((?:-\\d)?\\d*)"+whitespace+"*\\)|)(?=[^-]|$)","i")},rnative=/^[^{]+\{\s*\[native \w/,rquickExpr=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,rinputs=/^(?:input|select|textarea|button)$/i,rheader=/^h\d$/i,rescape=/'|\\/g,runescape=new RegExp("\\\\([\\da-f]{1,6}"+whitespace+"?|("+whitespace+")|.)","ig"),funescape=function(_,escaped,escapedWhitespace){var high="0x"+escaped-65536;return high!==high||escapedWhitespace?escaped:high<0?String.fromCharCode(high+65536):String.fromCharCode(high>>10|55296,high&1023|56320)};try{push.apply(arr=slice.call(preferredDoc.childNodes),preferredDoc.childNodes);arr[preferredDoc.childNodes.length].nodeType}catch(e){push={apply:arr.length?function(target,els){push_native.apply(target,slice.call(els))}:function(target,els){var j=target.length,i=0;while(target[j++]=els[i++]){}target.length=j-1}}}function Sizzle(selector,context,results,seed){var match,elem,m,nodeType,i,groups,old,nid,newContext,newSelector;if((context?context.ownerDocument||context:preferredDoc)!==document){setDocument(context)}context=context||document;results=results||[];if(!selector||typeof selector!=="string"){return results}if((nodeType=context.nodeType)!==1&&nodeType!==9){return[]}if(documentIsHTML&&!seed){if(match=rquickExpr.exec(selector)){if(m=match[1]){if(nodeType===9){elem=context.getElementById(m);if(elem&&elem.parentNode){if(elem.id===m){results.push(elem);return results}}else{return results}}else{if(context.ownerDocument&&(elem=context.ownerDocument.getElementById(m))&&contains(context,elem)&&elem.id===m){results.push(elem);return results}}}else if(match[2]){push.apply(results,context.getElementsByTagName(selector));return results}else if((m=match[3])&&support.getElementsByClassName&&context.getElementsByClassName){push.apply(results,context.getElementsByClassName(m));return results}}if(support.qsa&&(!rbuggyQSA||!rbuggyQSA.test(selector))){nid=old=expando;newContext=context;newSelector=nodeType===9&&selector;if(nodeType===1&&context.nodeName.toLowerCase()!=="object"){groups=tokenize(selector);if(old=context.getAttribute("id")){nid=old.replace(rescape,"\\$&")}else{context.setAttribute("id",nid)}nid="[id='"+nid+"'] ";i=groups.length;while(i--){groups[i]=nid+toSelector(groups[i])}newContext=rsibling.test(selector)&&context.parentNode||context;newSelector=groups.join(",")}if(newSelector){try{push.apply(results,newContext.querySelectorAll(newSelector));return results}catch(qsaError){}finally{if(!old){context.removeAttribute("id")}}}}}return select(selector.replace(rtrim,"$1"),context,results,seed)}function createCache(){var keys=[];function cache(key,value){if(keys.push(key+=" ")>Expr.cacheLength){delete cache[keys.shift()]}return cache[key]=value}return cache}function markFunction(fn){fn[expando]=true;return fn}function assert(fn){var div=document.createElement("div");try{return!!fn(div)}catch(e){return false}finally{if(div.parentNode){div.parentNode.removeChild(div)}div=null}}function addHandle(attrs,handler){var arr=attrs.split("|"),i=attrs.length;while(i--){Expr.attrHandle[arr[i]]=handler}}function siblingCheck(a,b){var cur=b&&a,diff=cur&&a.nodeType===1&&b.nodeType===1&&(~b.sourceIndex||MAX_NEGATIVE)-(~a.sourceIndex||MAX_NEGATIVE);if(diff){return diff}if(cur){while(cur=cur.nextSibling){if(cur===b){return-1}}}return a?1:-1}function createInputPseudo(type){return function(elem){var name=elem.nodeName.toLowerCase();return name==="input"&&elem.type===type}}function createButtonPseudo(type){return function(elem){var name=elem.nodeName.toLowerCase();return(name==="input"||name==="button")&&elem.type===type}}function createPositionalPseudo(fn){return markFunction(function(argument){argument=+argument;return markFunction(function(seed,matches){var j,matchIndexes=fn([],seed.length,argument),i=matchIndexes.length;while(i--){if(seed[j=matchIndexes[i]]){seed[j]=!(matches[j]=seed[j])}}})})}isXML=Sizzle.isXML=function(elem){var documentElement=elem&&(elem.ownerDocument||elem).documentElement;return documentElement?documentElement.nodeName!=="HTML":false};support=Sizzle.support={};setDocument=Sizzle.setDocument=function(node){var doc=node?node.ownerDocument||node:preferredDoc,parent=doc.defaultView;if(doc===document||doc.nodeType!==9||!doc.documentElement){return document}document=doc;docElem=doc.documentElement;documentIsHTML=!isXML(doc);if(parent&&parent.attachEvent&&parent!==parent.top){parent.attachEvent("onbeforeunload",function(){setDocument()})}support.attributes=assert(function(div){div.className="i";return!div.getAttribute("className")});support.getElementsByTagName=assert(function(div){div.appendChild(doc.createComment(""));return!div.getElementsByTagName("*").length});support.getElementsByClassName=assert(function(div){div.innerHTML="
";div.firstChild.className="i";return div.getElementsByClassName("i").length===2});support.getById=assert(function(div){docElem.appendChild(div).id=expando;return!doc.getElementsByName||!doc.getElementsByName(expando).length});if(support.getById){Expr.find["ID"]=function(id,context){if(typeof context.getElementById!==strundefined&&documentIsHTML){var m=context.getElementById(id);return m&&m.parentNode?[m]:[]}};Expr.filter["ID"]=function(id){var attrId=id.replace(runescape,funescape);return function(elem){return elem.getAttribute("id")===attrId}}}else{delete Expr.find["ID"];Expr.filter["ID"]=function(id){var attrId=id.replace(runescape,funescape);return function(elem){var node=typeof elem.getAttributeNode!==strundefined&&elem.getAttributeNode("id");return node&&node.value===attrId}}}Expr.find["TAG"]=support.getElementsByTagName?function(tag,context){if(typeof context.getElementsByTagName!==strundefined){return context.getElementsByTagName(tag)}}:function(tag,context){var elem,tmp=[],i=0,results=context.getElementsByTagName(tag);if(tag==="*"){while(elem=results[i++]){if(elem.nodeType===1){tmp.push(elem)}}return tmp}return results};Expr.find["CLASS"]=support.getElementsByClassName&&function(className,context){if(typeof context.getElementsByClassName!==strundefined&&documentIsHTML){return context.getElementsByClassName(className)}};rbuggyMatches=[];rbuggyQSA=[];if(support.qsa=rnative.test(doc.querySelectorAll)){assert(function(div){div.innerHTML=" ";if(!div.querySelectorAll("[selected]").length){rbuggyQSA.push("\\["+whitespace+"*(?:value|"+booleans+")")}if(!div.querySelectorAll(":checked").length){rbuggyQSA.push(":checked")}});assert(function(div){var input=doc.createElement("input");input.setAttribute("type","hidden");div.appendChild(input).setAttribute("t","");if(div.querySelectorAll("[t^='']").length){rbuggyQSA.push("[*^$]="+whitespace+"*(?:''|\"\")")}if(!div.querySelectorAll(":enabled").length){rbuggyQSA.push(":enabled",":disabled")}div.querySelectorAll("*,:x");rbuggyQSA.push(",.*:")})}if(support.matchesSelector=rnative.test(matches=docElem.webkitMatchesSelector||docElem.mozMatchesSelector||docElem.oMatchesSelector||docElem.msMatchesSelector)){assert(function(div){support.disconnectedMatch=matches.call(div,"div");matches.call(div,"[s!='']:x");rbuggyMatches.push("!=",pseudos)})}rbuggyQSA=rbuggyQSA.length&&new RegExp(rbuggyQSA.join("|"));rbuggyMatches=rbuggyMatches.length&&new RegExp(rbuggyMatches.join("|"));contains=rnative.test(docElem.contains)||docElem.compareDocumentPosition?function(a,b){var adown=a.nodeType===9?a.documentElement:a,bup=b&&b.parentNode;return a===bup||!!(bup&&bup.nodeType===1&&(adown.contains?adown.contains(bup):a.compareDocumentPosition&&a.compareDocumentPosition(bup)&16))}:function(a,b){if(b){while(b=b.parentNode){if(b===a){return true}}}return false};sortOrder=docElem.compareDocumentPosition?function(a,b){if(a===b){hasDuplicate=true;return 0}var compare=b.compareDocumentPosition&&a.compareDocumentPosition&&a.compareDocumentPosition(b);if(compare){if(compare&1||!support.sortDetached&&b.compareDocumentPosition(a)===compare){if(a===doc||contains(preferredDoc,a)){return-1}if(b===doc||contains(preferredDoc,b)){return 1}return sortInput?indexOf.call(sortInput,a)-indexOf.call(sortInput,b):0}return compare&4?-1:1}return a.compareDocumentPosition?-1:1}:function(a,b){var cur,i=0,aup=a.parentNode,bup=b.parentNode,ap=[a],bp=[b];if(a===b){hasDuplicate=true;return 0}else if(!aup||!bup){return a===doc?-1:b===doc?1:aup?-1:bup?1:sortInput?indexOf.call(sortInput,a)-indexOf.call(sortInput,b):0}else if(aup===bup){return siblingCheck(a,b)}cur=a;while(cur=cur.parentNode){ap.unshift(cur)}cur=b;while(cur=cur.parentNode){bp.unshift(cur)}while(ap[i]===bp[i]){i++}return i?siblingCheck(ap[i],bp[i]):ap[i]===preferredDoc?-1:bp[i]===preferredDoc?1:0};return doc};Sizzle.matches=function(expr,elements){return Sizzle(expr,null,null,elements)};Sizzle.matchesSelector=function(elem,expr){if((elem.ownerDocument||elem)!==document){setDocument(elem)}expr=expr.replace(rattributeQuotes,"='$1']");if(support.matchesSelector&&documentIsHTML&&(!rbuggyMatches||!rbuggyMatches.test(expr))&&(!rbuggyQSA||!rbuggyQSA.test(expr))){try{var ret=matches.call(elem,expr);if(ret||support.disconnectedMatch||elem.document&&elem.document.nodeType!==11){return ret}}catch(e){}}return Sizzle(expr,document,null,[elem]).length>0};Sizzle.contains=function(context,elem){if((context.ownerDocument||context)!==document){setDocument(context)}return contains(context,elem)};Sizzle.attr=function(elem,name){if((elem.ownerDocument||elem)!==document){setDocument(elem)}var fn=Expr.attrHandle[name.toLowerCase()],val=fn&&hasOwn.call(Expr.attrHandle,name.toLowerCase())?fn(elem,name,!documentIsHTML):undefined;return val===undefined?support.attributes||!documentIsHTML?elem.getAttribute(name):(val=elem.getAttributeNode(name))&&val.specified?val.value:null:val};Sizzle.error=function(msg){throw new Error("Syntax error, unrecognized expression: "+msg)};Sizzle.uniqueSort=function(results){var elem,duplicates=[],j=0,i=0;hasDuplicate=!support.detectDuplicates;sortInput=!support.sortStable&&results.slice(0);results.sort(sortOrder);if(hasDuplicate){while(elem=results[i++]){if(elem===results[i]){j=duplicates.push(i)}}while(j--){results.splice(duplicates[j],1)}}return results};getText=Sizzle.getText=function(elem){var node,ret="",i=0,nodeType=elem.nodeType;if(!nodeType){for(;node=elem[i];i++){ret+=getText(node)}}else if(nodeType===1||nodeType===9||nodeType===11){if(typeof elem.textContent==="string"){return elem.textContent}else{for(elem=elem.firstChild;elem;elem=elem.nextSibling){ret+=getText(elem)}}}else if(nodeType===3||nodeType===4){return elem.nodeValue}return ret};Expr=Sizzle.selectors={cacheLength:50,createPseudo:markFunction,match:matchExpr,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:true}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:true},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(match){match[1]=match[1].replace(runescape,funescape);match[3]=(match[4]||match[5]||"").replace(runescape,funescape);if(match[2]==="~="){match[3]=" "+match[3]+" "}return match.slice(0,4)},CHILD:function(match){match[1]=match[1].toLowerCase();if(match[1].slice(0,3)==="nth"){if(!match[3]){Sizzle.error(match[0])}match[4]=+(match[4]?match[5]+(match[6]||1):2*(match[3]==="even"||match[3]==="odd"));match[5]=+(match[7]+match[8]||match[3]==="odd")}else if(match[3]){Sizzle.error(match[0])}return match},PSEUDO:function(match){var excess,unquoted=!match[5]&&match[2];if(matchExpr["CHILD"].test(match[0])){return null}if(match[3]&&match[4]!==undefined){match[2]=match[4]}else if(unquoted&&rpseudo.test(unquoted)&&(excess=tokenize(unquoted,true))&&(excess=unquoted.indexOf(")",unquoted.length-excess)-unquoted.length)){match[0]=match[0].slice(0,excess);match[2]=unquoted.slice(0,excess)}return match.slice(0,3)}},filter:{TAG:function(nodeNameSelector){var nodeName=nodeNameSelector.replace(runescape,funescape).toLowerCase();return nodeNameSelector==="*"?function(){return true}:function(elem){return elem.nodeName&&elem.nodeName.toLowerCase()===nodeName}},CLASS:function(className){var pattern=classCache[className+" "];return pattern||(pattern=new RegExp("(^|"+whitespace+")"+className+"("+whitespace+"|$)"))&&classCache(className,function(elem){return pattern.test(typeof elem.className==="string"&&elem.className||typeof elem.getAttribute!==strundefined&&elem.getAttribute("class")||"")})},ATTR:function(name,operator,check){return function(elem){var result=Sizzle.attr(elem,name);if(result==null){return operator==="!="}if(!operator){return true}result+="";return operator==="="?result===check:operator==="!="?result!==check:operator==="^="?check&&result.indexOf(check)===0:operator==="*="?check&&result.indexOf(check)>-1:operator==="$="?check&&result.slice(-check.length)===check:operator==="~="?(" "+result+" ").indexOf(check)>-1:operator==="|="?result===check||result.slice(0,check.length+1)===check+"-":false}},CHILD:function(type,what,argument,first,last){var simple=type.slice(0,3)!=="nth",forward=type.slice(-4)!=="last",ofType=what==="of-type";return first===1&&last===0?function(elem){return!!elem.parentNode}:function(elem,context,xml){var cache,outerCache,node,diff,nodeIndex,start,dir=simple!==forward?"nextSibling":"previousSibling",parent=elem.parentNode,name=ofType&&elem.nodeName.toLowerCase(),useCache=!xml&&!ofType;if(parent){if(simple){while(dir){node=elem;while(node=node[dir]){if(ofType?node.nodeName.toLowerCase()===name:node.nodeType===1){return false}}start=dir=type==="only"&&!start&&"nextSibling"}return true}start=[forward?parent.firstChild:parent.lastChild];if(forward&&useCache){outerCache=parent[expando]||(parent[expando]={});cache=outerCache[type]||[];nodeIndex=cache[0]===dirruns&&cache[1];diff=cache[0]===dirruns&&cache[2];node=nodeIndex&&parent.childNodes[nodeIndex];while(node=++nodeIndex&&node&&node[dir]||(diff=nodeIndex=0)||start.pop()){if(node.nodeType===1&&++diff&&node===elem){outerCache[type]=[dirruns,nodeIndex,diff];break}}}else if(useCache&&(cache=(elem[expando]||(elem[expando]={}))[type])&&cache[0]===dirruns){diff=cache[1]}else{while(node=++nodeIndex&&node&&node[dir]||(diff=nodeIndex=0)||start.pop()){if((ofType?node.nodeName.toLowerCase()===name:node.nodeType===1)&&++diff){if(useCache){(node[expando]||(node[expando]={}))[type]=[dirruns,diff]}if(node===elem){break}}}}diff-=last;return diff===first||diff%first===0&&diff/first>=0}}},PSEUDO:function(pseudo,argument){var args,fn=Expr.pseudos[pseudo]||Expr.setFilters[pseudo.toLowerCase()]||Sizzle.error("unsupported pseudo: "+pseudo);if(fn[expando]){return fn(argument)}if(fn.length>1){args=[pseudo,pseudo,"",argument];return Expr.setFilters.hasOwnProperty(pseudo.toLowerCase())?markFunction(function(seed,matches){var idx,matched=fn(seed,argument),i=matched.length;while(i--){idx=indexOf.call(seed,matched[i]);seed[idx]=!(matches[idx]=matched[i])}}):function(elem){return fn(elem,0,args)}}return fn}},pseudos:{not:markFunction(function(selector){var input=[],results=[],matcher=compile(selector.replace(rtrim,"$1"));return matcher[expando]?markFunction(function(seed,matches,context,xml){var elem,unmatched=matcher(seed,null,xml,[]),i=seed.length;while(i--){if(elem=unmatched[i]){seed[i]=!(matches[i]=elem)}}}):function(elem,context,xml){input[0]=elem;matcher(input,null,xml,results);return!results.pop()}}),has:markFunction(function(selector){return function(elem){return Sizzle(selector,elem).length>0}}),contains:markFunction(function(text){return function(elem){return(elem.textContent||elem.innerText||getText(elem)).indexOf(text)>-1}}),lang:markFunction(function(lang){if(!ridentifier.test(lang||"")){Sizzle.error("unsupported lang: "+lang)}lang=lang.replace(runescape,funescape).toLowerCase();return function(elem){var elemLang;do{if(elemLang=documentIsHTML?elem.lang:elem.getAttribute("xml:lang")||elem.getAttribute("lang")){elemLang=elemLang.toLowerCase();return elemLang===lang||elemLang.indexOf(lang+"-")===0}}while((elem=elem.parentNode)&&elem.nodeType===1);return false}}),target:function(elem){var hash=window.location&&window.location.hash;return hash&&hash.slice(1)===elem.id},root:function(elem){return elem===docElem},focus:function(elem){return elem===document.activeElement&&(!document.hasFocus||document.hasFocus())&&!!(elem.type||elem.href||~elem.tabIndex)},enabled:function(elem){return elem.disabled===false},disabled:function(elem){return elem.disabled===true},checked:function(elem){var nodeName=elem.nodeName.toLowerCase();return nodeName==="input"&&!!elem.checked||nodeName==="option"&&!!elem.selected},selected:function(elem){if(elem.parentNode){elem.parentNode.selectedIndex}return elem.selected===true},empty:function(elem){for(elem=elem.firstChild;elem;elem=elem.nextSibling){if(elem.nodeName>"@"||elem.nodeType===3||elem.nodeType===4){return false}}return true},parent:function(elem){return!Expr.pseudos["empty"](elem)},header:function(elem){return rheader.test(elem.nodeName)},input:function(elem){return rinputs.test(elem.nodeName)},button:function(elem){var name=elem.nodeName.toLowerCase();return name==="input"&&elem.type==="button"||name==="button"},text:function(elem){var attr;return elem.nodeName.toLowerCase()==="input"&&elem.type==="text"&&((attr=elem.getAttribute("type"))==null||attr.toLowerCase()===elem.type)},first:createPositionalPseudo(function(){return[0]}),last:createPositionalPseudo(function(matchIndexes,length){return[length-1]}),eq:createPositionalPseudo(function(matchIndexes,length,argument){return[argument<0?argument+length:argument]}),even:createPositionalPseudo(function(matchIndexes,length){var i=0;for(;i=0;){matchIndexes.push(i)}return matchIndexes}),gt:createPositionalPseudo(function(matchIndexes,length,argument){var i=argument<0?argument+length:argument;for(;++i1?function(elem,context,xml){var i=matchers.length;while(i--){if(!matchers[i](elem,context,xml)){return false}}return true}:matchers[0]}function condense(unmatched,map,filter,context,xml){var elem,newUnmatched=[],i=0,len=unmatched.length,mapped=map!=null;for(;i-1){seed[temp]=!(results[temp]=elem)}}}}else{matcherOut=condense(matcherOut===results?matcherOut.splice(preexisting,matcherOut.length):matcherOut);if(postFinder){postFinder(null,results,matcherOut,xml)}else{push.apply(results,matcherOut)}}})}function matcherFromTokens(tokens){var checkContext,matcher,j,len=tokens.length,leadingRelative=Expr.relative[tokens[0].type],implicitRelative=leadingRelative||Expr.relative[" "],i=leadingRelative?1:0,matchContext=addCombinator(function(elem){return elem===checkContext},implicitRelative,true),matchAnyContext=addCombinator(function(elem){return indexOf.call(checkContext,elem)>-1},implicitRelative,true),matchers=[function(elem,context,xml){return!leadingRelative&&(xml||context!==outermostContext)||((checkContext=context).nodeType?matchContext(elem,context,xml):matchAnyContext(elem,context,xml))}];for(;i1&&elementMatcher(matchers),i>1&&toSelector(tokens.slice(0,i-1).concat({value:tokens[i-2].type===" "?"*":""})).replace(rtrim,"$1"),matcher,i0,byElement=elementMatchers.length>0,superMatcher=function(seed,context,xml,results,expandContext){var elem,j,matcher,setMatched=[],matchedCount=0,i="0",unmatched=seed&&[],outermost=expandContext!=null,contextBackup=outermostContext,elems=seed||byElement&&Expr.find["TAG"]("*",expandContext&&context.parentNode||context),dirrunsUnique=dirruns+=contextBackup==null?1:Math.random()||.1,len=elems.length;if(outermost){outermostContext=context!==document&&context;cachedruns=matcherCachedRuns}for(;i!==len&&(elem=elems[i])!=null;i++){if(byElement&&elem){j=0;while(matcher=elementMatchers[j++]){if(matcher(elem,context,xml)){results.push(elem);break}}if(outermost){dirruns=dirrunsUnique;cachedruns=++matcherCachedRuns}}if(bySet){if(elem=!matcher&&elem){matchedCount--}if(seed){unmatched.push(elem)}}}matchedCount+=i;if(bySet&&i!==matchedCount){j=0;while(matcher=setMatchers[j++]){matcher(unmatched,setMatched,context,xml)}if(seed){if(matchedCount>0){while(i--){if(!(unmatched[i]||setMatched[i])){setMatched[i]=pop.call(results)}}}setMatched=condense(setMatched)}push.apply(results,setMatched);if(outermost&&!seed&&setMatched.length>0&&matchedCount+setMatchers.length>1){Sizzle.uniqueSort(results)}}if(outermost){dirruns=dirrunsUnique;outermostContext=contextBackup}return unmatched};return bySet?markFunction(superMatcher):superMatcher}compile=Sizzle.compile=function(selector,group){var i,setMatchers=[],elementMatchers=[],cached=compilerCache[selector+" "];if(!cached){if(!group){group=tokenize(selector)}i=group.length;while(i--){cached=matcherFromTokens(group[i]);if(cached[expando]){setMatchers.push(cached)}else{elementMatchers.push(cached)}}cached=compilerCache(selector,matcherFromGroupMatchers(elementMatchers,setMatchers))}return cached};function multipleContexts(selector,contexts,results){var i=0,len=contexts.length;for(;i2&&(token=tokens[0]).type==="ID"&&support.getById&&context.nodeType===9&&documentIsHTML&&Expr.relative[tokens[1].type]){context=(Expr.find["ID"](token.matches[0].replace(runescape,funescape),context)||[])[0];if(!context){return results}selector=selector.slice(tokens.shift().value.length)}i=matchExpr["needsContext"].test(selector)?0:tokens.length;while(i--){token=tokens[i];if(Expr.relative[type=token.type]){break}if(find=Expr.find[type]){if(seed=find(token.matches[0].replace(runescape,funescape),rsibling.test(tokens[0].type)&&context.parentNode||context)){tokens.splice(i,1);selector=seed.length&&toSelector(tokens);if(!selector){push.apply(results,seed);return results}break}}}}}compile(selector,match)(seed,context,!documentIsHTML,results,rsibling.test(selector));return results}support.sortStable=expando.split("").sort(sortOrder).join("")===expando;support.detectDuplicates=hasDuplicate;setDocument();support.sortDetached=assert(function(div1){return div1.compareDocumentPosition(document.createElement("div"))&1});if(!assert(function(div){div.innerHTML=" ";return div.firstChild.getAttribute("href")==="#"})){addHandle("type|href|height|width",function(elem,name,isXML){if(!isXML){return elem.getAttribute(name,name.toLowerCase()==="type"?1:2)}})}if(!support.attributes||!assert(function(div){div.innerHTML=" ";div.firstChild.setAttribute("value","");return div.firstChild.getAttribute("value")===""})){addHandle("value",function(elem,name,isXML){if(!isXML&&elem.nodeName.toLowerCase()==="input"){return elem.defaultValue}})}if(!assert(function(div){return div.getAttribute("disabled")==null})){addHandle(booleans,function(elem,name,isXML){var val;if(!isXML){return(val=elem.getAttributeNode(name))&&val.specified?val.value:elem[name]===true?name.toLowerCase():null}})}if(typeof define==="function"&&define.amd){define(function(){return Sizzle})}else{window.Sizzle=Sizzle}})(window);if(typeof JSON!=="object"){JSON={}}(function(){"use strict";function f(n){return n<10?"0"+n:n}if(typeof Date.prototype.toJSON!=="function"){Date.prototype.toJSON=function(){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(){return this.valueOf()}}var cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'"'+string.replace(escapable,function(a){var c=meta[a];return typeof c==="string"?c:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+string+'"'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==="object"&&typeof value.toJSON==="function"){value=value.toJSON(key)}if(typeof rep==="function"){value=rep.call(holder,key,value)}switch(typeof value){case"string":return quote(value);case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null"}gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==="[object Array]"){length=value.length;for(i=0;i=0;--i){if(handlers[i]===handler){handlers.splice(i,1);break}}},onerror:null,onmessage:null,onopen:null,readyState:0,URL:""};var MessageEvent=function(data,origin,lastEventId){this.data=data;this.origin=origin;this.lastEventId=lastEventId||""};MessageEvent.prototype={data:null,type:"message",lastEventId:"",origin:""};if("module"in global)module.exports=EventSource;global.EventSource=EventSource})(this);var Scout=function(){};Scout=function Scoutmaker(){var xhr;if(window.XMLHttpRequest){xhr=new XMLHttpRequest;if(xhr.overrideMimeType){xhr.overrideMimeType("text/xml")}}else{try{xhr=new ActiveXObject("Msxml2.XMLHTTP")}catch(e){xhr=new ActiveXObject("Microsoft.XMLHTTP")}}var params={method:"POST",resp:function(resp,xhr){},error:function(status,xhr){},partial:function(raw,resp,xhr){}};var toxhrsend=function(data){var str="",start=true;var jsondata="";for(var key in data){if(typeof(jsondata=JSON.stringify(data[key]))==="string"){str+=start?"":"&";str+=encodeURIComponent(key)+"="+encodeURIComponent(jsondata);if(start){start=false}}}return str};var sendxhr=function(target,params){if(params.action){params.url="/$"+params.action}if(params.url){xhr.onreadystatechange=function(){switch(xhr.readyState){case 3:if(params.partial===undefined){var raw=xhr.responseText;var resp;try{resp=JSON.parse(raw)}catch(e){}params.partial.apply(target,[raw,resp,xhr])}break;case 4:if(xhr.status===200){var resp=JSON.parse(xhr.responseText);params.resp.apply(target,[resp,xhr])}else{params.error.apply(target,[xhr.status,xhr])}break}};if(params.method==="POST"&&(params.data==={}||params.data===undefined)){params.method="GET"}xhr.open(params.method,params.url+(params.method==="POST"?"":"?"+toxhrsend(params.data)),true,params.user,params.password);if(params.method==="POST"&¶ms.data!=={}){xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");xhr.send(toxhrsend(params.data))}else{xhr.send(null)}}};var onprop=function(eventName,before){before=before||function(params,e,xhr){};var listenerfunc=function(e){if(!e&&window.event){e=event}var target=e.target||e.srcElement||undefined;if(eventName==="submit"){if(e.preventDefault){e.preventDefault()}else{e.returnValue=false}}before.apply(target,[params,e,xhr]);sendxhr(target,params)};if(document.addEventListener){this.addEventListener(eventName,listenerfunc,false)}else{this.attachEvent("on"+eventName,listenerfunc)}};var ret=function(id){var domelt=document.querySelector(id);if(!domelt){return{on:function(){}}}domelt.on=onprop;return domelt};ret.send=function(before){if(xhr.readyState===1){return Scoutmaker().send(before)}before=before||function(params,xhr){};return function(){before.apply(undefined,[params,xhr]);sendxhr(undefined,params)}};ret.maker=Scoutmaker;ret.eventSource=function(channel){var es=new EventSource("/$"+channel);es.onrecv=function(cb){es.onmessage=function(event){cb(JSON.parse(event.data))}};return es};if(window.io){ret.socket=function(namespace){if(namespace===undefined){namespace="/"}return io.connect(namespace,{resource:"$socket.io"})}}if(window.WebSocket){var wsSend=function wsSendJSON(json){this.send(JSON.stringify(json))};ret.webSocket=function newWebSocket(channel){var socket=new WebSocket("ws"+window.location.protocol.slice(4)+"//"+window.location.host+"/$websocket:"+channel);socket.sendjson=wsSend.bind(socket);return socket}}return ret}();
--------------------------------------------------------------------------------
/web/js/scout.js:
--------------------------------------------------------------------------------
1 | (function(window){var i,support,cachedruns,Expr,getText,isXML,compile,outermostContext,sortInput,setDocument,document,docElem,documentIsHTML,rbuggyQSA,rbuggyMatches,matches,contains,expando="sizzle"+-new Date,preferredDoc=window.document,dirruns=0,done=0,classCache=createCache(),tokenCache=createCache(),compilerCache=createCache(),hasDuplicate=false,sortOrder=function(a,b){if(a===b){hasDuplicate=true;return 0}return 0},strundefined=typeof undefined,MAX_NEGATIVE=1<<31,hasOwn={}.hasOwnProperty,arr=[],pop=arr.pop,push_native=arr.push,push=arr.push,slice=arr.slice,indexOf=arr.indexOf||function(elem){var i=0,len=this.length;for(;i+~]|"+whitespace+")"+whitespace+"*"),rsibling=new RegExp(whitespace+"*[+~]"),rattributeQuotes=new RegExp("="+whitespace+"*([^\\]'\"]*)"+whitespace+"*\\]","g"),rpseudo=new RegExp(pseudos),ridentifier=new RegExp("^"+identifier+"$"),matchExpr={ID:new RegExp("^#("+characterEncoding+")"),CLASS:new RegExp("^\\.("+characterEncoding+")"),TAG:new RegExp("^("+characterEncoding.replace("w","w*")+")"),ATTR:new RegExp("^"+attributes),PSEUDO:new RegExp("^"+pseudos),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+whitespace+"*(even|odd|(([+-]|)(\\d*)n|)"+whitespace+"*(?:([+-]|)"+whitespace+"*(\\d+)|))"+whitespace+"*\\)|)","i"),bool:new RegExp("^(?:"+booleans+")$","i"),needsContext:new RegExp("^"+whitespace+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+whitespace+"*((?:-\\d)?\\d*)"+whitespace+"*\\)|)(?=[^-]|$)","i")},rnative=/^[^{]+\{\s*\[native \w/,rquickExpr=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,rinputs=/^(?:input|select|textarea|button)$/i,rheader=/^h\d$/i,rescape=/'|\\/g,runescape=new RegExp("\\\\([\\da-f]{1,6}"+whitespace+"?|("+whitespace+")|.)","ig"),funescape=function(_,escaped,escapedWhitespace){var high="0x"+escaped-65536;return high!==high||escapedWhitespace?escaped:high<0?String.fromCharCode(high+65536):String.fromCharCode(high>>10|55296,high&1023|56320)};try{push.apply(arr=slice.call(preferredDoc.childNodes),preferredDoc.childNodes);arr[preferredDoc.childNodes.length].nodeType}catch(e){push={apply:arr.length?function(target,els){push_native.apply(target,slice.call(els))}:function(target,els){var j=target.length,i=0;while(target[j++]=els[i++]){}target.length=j-1}}}function Sizzle(selector,context,results,seed){var match,elem,m,nodeType,i,groups,old,nid,newContext,newSelector;if((context?context.ownerDocument||context:preferredDoc)!==document){setDocument(context)}context=context||document;results=results||[];if(!selector||typeof selector!=="string"){return results}if((nodeType=context.nodeType)!==1&&nodeType!==9){return[]}if(documentIsHTML&&!seed){if(match=rquickExpr.exec(selector)){if(m=match[1]){if(nodeType===9){elem=context.getElementById(m);if(elem&&elem.parentNode){if(elem.id===m){results.push(elem);return results}}else{return results}}else{if(context.ownerDocument&&(elem=context.ownerDocument.getElementById(m))&&contains(context,elem)&&elem.id===m){results.push(elem);return results}}}else if(match[2]){push.apply(results,context.getElementsByTagName(selector));return results}else if((m=match[3])&&support.getElementsByClassName&&context.getElementsByClassName){push.apply(results,context.getElementsByClassName(m));return results}}if(support.qsa&&(!rbuggyQSA||!rbuggyQSA.test(selector))){nid=old=expando;newContext=context;newSelector=nodeType===9&&selector;if(nodeType===1&&context.nodeName.toLowerCase()!=="object"){groups=tokenize(selector);if(old=context.getAttribute("id")){nid=old.replace(rescape,"\\$&")}else{context.setAttribute("id",nid)}nid="[id='"+nid+"'] ";i=groups.length;while(i--){groups[i]=nid+toSelector(groups[i])}newContext=rsibling.test(selector)&&context.parentNode||context;newSelector=groups.join(",")}if(newSelector){try{push.apply(results,newContext.querySelectorAll(newSelector));return results}catch(qsaError){}finally{if(!old){context.removeAttribute("id")}}}}}return select(selector.replace(rtrim,"$1"),context,results,seed)}function createCache(){var keys=[];function cache(key,value){if(keys.push(key+=" ")>Expr.cacheLength){delete cache[keys.shift()]}return cache[key]=value}return cache}function markFunction(fn){fn[expando]=true;return fn}function assert(fn){var div=document.createElement("div");try{return!!fn(div)}catch(e){return false}finally{if(div.parentNode){div.parentNode.removeChild(div)}div=null}}function addHandle(attrs,handler){var arr=attrs.split("|"),i=attrs.length;while(i--){Expr.attrHandle[arr[i]]=handler}}function siblingCheck(a,b){var cur=b&&a,diff=cur&&a.nodeType===1&&b.nodeType===1&&(~b.sourceIndex||MAX_NEGATIVE)-(~a.sourceIndex||MAX_NEGATIVE);if(diff){return diff}if(cur){while(cur=cur.nextSibling){if(cur===b){return-1}}}return a?1:-1}function createInputPseudo(type){return function(elem){var name=elem.nodeName.toLowerCase();return name==="input"&&elem.type===type}}function createButtonPseudo(type){return function(elem){var name=elem.nodeName.toLowerCase();return(name==="input"||name==="button")&&elem.type===type}}function createPositionalPseudo(fn){return markFunction(function(argument){argument=+argument;return markFunction(function(seed,matches){var j,matchIndexes=fn([],seed.length,argument),i=matchIndexes.length;while(i--){if(seed[j=matchIndexes[i]]){seed[j]=!(matches[j]=seed[j])}}})})}isXML=Sizzle.isXML=function(elem){var documentElement=elem&&(elem.ownerDocument||elem).documentElement;return documentElement?documentElement.nodeName!=="HTML":false};support=Sizzle.support={};setDocument=Sizzle.setDocument=function(node){var doc=node?node.ownerDocument||node:preferredDoc,parent=doc.defaultView;if(doc===document||doc.nodeType!==9||!doc.documentElement){return document}document=doc;docElem=doc.documentElement;documentIsHTML=!isXML(doc);if(parent&&parent.attachEvent&&parent!==parent.top){parent.attachEvent("onbeforeunload",function(){setDocument()})}support.attributes=assert(function(div){div.className="i";return!div.getAttribute("className")});support.getElementsByTagName=assert(function(div){div.appendChild(doc.createComment(""));return!div.getElementsByTagName("*").length});support.getElementsByClassName=assert(function(div){div.innerHTML="
";div.firstChild.className="i";return div.getElementsByClassName("i").length===2});support.getById=assert(function(div){docElem.appendChild(div).id=expando;return!doc.getElementsByName||!doc.getElementsByName(expando).length});if(support.getById){Expr.find["ID"]=function(id,context){if(typeof context.getElementById!==strundefined&&documentIsHTML){var m=context.getElementById(id);return m&&m.parentNode?[m]:[]}};Expr.filter["ID"]=function(id){var attrId=id.replace(runescape,funescape);return function(elem){return elem.getAttribute("id")===attrId}}}else{delete Expr.find["ID"];Expr.filter["ID"]=function(id){var attrId=id.replace(runescape,funescape);return function(elem){var node=typeof elem.getAttributeNode!==strundefined&&elem.getAttributeNode("id");return node&&node.value===attrId}}}Expr.find["TAG"]=support.getElementsByTagName?function(tag,context){if(typeof context.getElementsByTagName!==strundefined){return context.getElementsByTagName(tag)}}:function(tag,context){var elem,tmp=[],i=0,results=context.getElementsByTagName(tag);if(tag==="*"){while(elem=results[i++]){if(elem.nodeType===1){tmp.push(elem)}}return tmp}return results};Expr.find["CLASS"]=support.getElementsByClassName&&function(className,context){if(typeof context.getElementsByClassName!==strundefined&&documentIsHTML){return context.getElementsByClassName(className)}};rbuggyMatches=[];rbuggyQSA=[];if(support.qsa=rnative.test(doc.querySelectorAll)){assert(function(div){div.innerHTML=" ";if(!div.querySelectorAll("[selected]").length){rbuggyQSA.push("\\["+whitespace+"*(?:value|"+booleans+")")}if(!div.querySelectorAll(":checked").length){rbuggyQSA.push(":checked")}});assert(function(div){var input=doc.createElement("input");input.setAttribute("type","hidden");div.appendChild(input).setAttribute("t","");if(div.querySelectorAll("[t^='']").length){rbuggyQSA.push("[*^$]="+whitespace+"*(?:''|\"\")")}if(!div.querySelectorAll(":enabled").length){rbuggyQSA.push(":enabled",":disabled")}div.querySelectorAll("*,:x");rbuggyQSA.push(",.*:")})}if(support.matchesSelector=rnative.test(matches=docElem.webkitMatchesSelector||docElem.mozMatchesSelector||docElem.oMatchesSelector||docElem.msMatchesSelector)){assert(function(div){support.disconnectedMatch=matches.call(div,"div");matches.call(div,"[s!='']:x");rbuggyMatches.push("!=",pseudos)})}rbuggyQSA=rbuggyQSA.length&&new RegExp(rbuggyQSA.join("|"));rbuggyMatches=rbuggyMatches.length&&new RegExp(rbuggyMatches.join("|"));contains=rnative.test(docElem.contains)||docElem.compareDocumentPosition?function(a,b){var adown=a.nodeType===9?a.documentElement:a,bup=b&&b.parentNode;return a===bup||!!(bup&&bup.nodeType===1&&(adown.contains?adown.contains(bup):a.compareDocumentPosition&&a.compareDocumentPosition(bup)&16))}:function(a,b){if(b){while(b=b.parentNode){if(b===a){return true}}}return false};sortOrder=docElem.compareDocumentPosition?function(a,b){if(a===b){hasDuplicate=true;return 0}var compare=b.compareDocumentPosition&&a.compareDocumentPosition&&a.compareDocumentPosition(b);if(compare){if(compare&1||!support.sortDetached&&b.compareDocumentPosition(a)===compare){if(a===doc||contains(preferredDoc,a)){return-1}if(b===doc||contains(preferredDoc,b)){return 1}return sortInput?indexOf.call(sortInput,a)-indexOf.call(sortInput,b):0}return compare&4?-1:1}return a.compareDocumentPosition?-1:1}:function(a,b){var cur,i=0,aup=a.parentNode,bup=b.parentNode,ap=[a],bp=[b];if(a===b){hasDuplicate=true;return 0}else if(!aup||!bup){return a===doc?-1:b===doc?1:aup?-1:bup?1:sortInput?indexOf.call(sortInput,a)-indexOf.call(sortInput,b):0}else if(aup===bup){return siblingCheck(a,b)}cur=a;while(cur=cur.parentNode){ap.unshift(cur)}cur=b;while(cur=cur.parentNode){bp.unshift(cur)}while(ap[i]===bp[i]){i++}return i?siblingCheck(ap[i],bp[i]):ap[i]===preferredDoc?-1:bp[i]===preferredDoc?1:0};return doc};Sizzle.matches=function(expr,elements){return Sizzle(expr,null,null,elements)};Sizzle.matchesSelector=function(elem,expr){if((elem.ownerDocument||elem)!==document){setDocument(elem)}expr=expr.replace(rattributeQuotes,"='$1']");if(support.matchesSelector&&documentIsHTML&&(!rbuggyMatches||!rbuggyMatches.test(expr))&&(!rbuggyQSA||!rbuggyQSA.test(expr))){try{var ret=matches.call(elem,expr);if(ret||support.disconnectedMatch||elem.document&&elem.document.nodeType!==11){return ret}}catch(e){}}return Sizzle(expr,document,null,[elem]).length>0};Sizzle.contains=function(context,elem){if((context.ownerDocument||context)!==document){setDocument(context)}return contains(context,elem)};Sizzle.attr=function(elem,name){if((elem.ownerDocument||elem)!==document){setDocument(elem)}var fn=Expr.attrHandle[name.toLowerCase()],val=fn&&hasOwn.call(Expr.attrHandle,name.toLowerCase())?fn(elem,name,!documentIsHTML):undefined;return val===undefined?support.attributes||!documentIsHTML?elem.getAttribute(name):(val=elem.getAttributeNode(name))&&val.specified?val.value:null:val};Sizzle.error=function(msg){throw new Error("Syntax error, unrecognized expression: "+msg)};Sizzle.uniqueSort=function(results){var elem,duplicates=[],j=0,i=0;hasDuplicate=!support.detectDuplicates;sortInput=!support.sortStable&&results.slice(0);results.sort(sortOrder);if(hasDuplicate){while(elem=results[i++]){if(elem===results[i]){j=duplicates.push(i)}}while(j--){results.splice(duplicates[j],1)}}return results};getText=Sizzle.getText=function(elem){var node,ret="",i=0,nodeType=elem.nodeType;if(!nodeType){for(;node=elem[i];i++){ret+=getText(node)}}else if(nodeType===1||nodeType===9||nodeType===11){if(typeof elem.textContent==="string"){return elem.textContent}else{for(elem=elem.firstChild;elem;elem=elem.nextSibling){ret+=getText(elem)}}}else if(nodeType===3||nodeType===4){return elem.nodeValue}return ret};Expr=Sizzle.selectors={cacheLength:50,createPseudo:markFunction,match:matchExpr,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:true}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:true},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(match){match[1]=match[1].replace(runescape,funescape);match[3]=(match[4]||match[5]||"").replace(runescape,funescape);if(match[2]==="~="){match[3]=" "+match[3]+" "}return match.slice(0,4)},CHILD:function(match){match[1]=match[1].toLowerCase();if(match[1].slice(0,3)==="nth"){if(!match[3]){Sizzle.error(match[0])}match[4]=+(match[4]?match[5]+(match[6]||1):2*(match[3]==="even"||match[3]==="odd"));match[5]=+(match[7]+match[8]||match[3]==="odd")}else if(match[3]){Sizzle.error(match[0])}return match},PSEUDO:function(match){var excess,unquoted=!match[5]&&match[2];if(matchExpr["CHILD"].test(match[0])){return null}if(match[3]&&match[4]!==undefined){match[2]=match[4]}else if(unquoted&&rpseudo.test(unquoted)&&(excess=tokenize(unquoted,true))&&(excess=unquoted.indexOf(")",unquoted.length-excess)-unquoted.length)){match[0]=match[0].slice(0,excess);match[2]=unquoted.slice(0,excess)}return match.slice(0,3)}},filter:{TAG:function(nodeNameSelector){var nodeName=nodeNameSelector.replace(runescape,funescape).toLowerCase();return nodeNameSelector==="*"?function(){return true}:function(elem){return elem.nodeName&&elem.nodeName.toLowerCase()===nodeName}},CLASS:function(className){var pattern=classCache[className+" "];return pattern||(pattern=new RegExp("(^|"+whitespace+")"+className+"("+whitespace+"|$)"))&&classCache(className,function(elem){return pattern.test(typeof elem.className==="string"&&elem.className||typeof elem.getAttribute!==strundefined&&elem.getAttribute("class")||"")})},ATTR:function(name,operator,check){return function(elem){var result=Sizzle.attr(elem,name);if(result==null){return operator==="!="}if(!operator){return true}result+="";return operator==="="?result===check:operator==="!="?result!==check:operator==="^="?check&&result.indexOf(check)===0:operator==="*="?check&&result.indexOf(check)>-1:operator==="$="?check&&result.slice(-check.length)===check:operator==="~="?(" "+result+" ").indexOf(check)>-1:operator==="|="?result===check||result.slice(0,check.length+1)===check+"-":false}},CHILD:function(type,what,argument,first,last){var simple=type.slice(0,3)!=="nth",forward=type.slice(-4)!=="last",ofType=what==="of-type";return first===1&&last===0?function(elem){return!!elem.parentNode}:function(elem,context,xml){var cache,outerCache,node,diff,nodeIndex,start,dir=simple!==forward?"nextSibling":"previousSibling",parent=elem.parentNode,name=ofType&&elem.nodeName.toLowerCase(),useCache=!xml&&!ofType;if(parent){if(simple){while(dir){node=elem;while(node=node[dir]){if(ofType?node.nodeName.toLowerCase()===name:node.nodeType===1){return false}}start=dir=type==="only"&&!start&&"nextSibling"}return true}start=[forward?parent.firstChild:parent.lastChild];if(forward&&useCache){outerCache=parent[expando]||(parent[expando]={});cache=outerCache[type]||[];nodeIndex=cache[0]===dirruns&&cache[1];diff=cache[0]===dirruns&&cache[2];node=nodeIndex&&parent.childNodes[nodeIndex];while(node=++nodeIndex&&node&&node[dir]||(diff=nodeIndex=0)||start.pop()){if(node.nodeType===1&&++diff&&node===elem){outerCache[type]=[dirruns,nodeIndex,diff];break}}}else if(useCache&&(cache=(elem[expando]||(elem[expando]={}))[type])&&cache[0]===dirruns){diff=cache[1]}else{while(node=++nodeIndex&&node&&node[dir]||(diff=nodeIndex=0)||start.pop()){if((ofType?node.nodeName.toLowerCase()===name:node.nodeType===1)&&++diff){if(useCache){(node[expando]||(node[expando]={}))[type]=[dirruns,diff]}if(node===elem){break}}}}diff-=last;return diff===first||diff%first===0&&diff/first>=0}}},PSEUDO:function(pseudo,argument){var args,fn=Expr.pseudos[pseudo]||Expr.setFilters[pseudo.toLowerCase()]||Sizzle.error("unsupported pseudo: "+pseudo);if(fn[expando]){return fn(argument)}if(fn.length>1){args=[pseudo,pseudo,"",argument];return Expr.setFilters.hasOwnProperty(pseudo.toLowerCase())?markFunction(function(seed,matches){var idx,matched=fn(seed,argument),i=matched.length;while(i--){idx=indexOf.call(seed,matched[i]);seed[idx]=!(matches[idx]=matched[i])}}):function(elem){return fn(elem,0,args)}}return fn}},pseudos:{not:markFunction(function(selector){var input=[],results=[],matcher=compile(selector.replace(rtrim,"$1"));return matcher[expando]?markFunction(function(seed,matches,context,xml){var elem,unmatched=matcher(seed,null,xml,[]),i=seed.length;while(i--){if(elem=unmatched[i]){seed[i]=!(matches[i]=elem)}}}):function(elem,context,xml){input[0]=elem;matcher(input,null,xml,results);return!results.pop()}}),has:markFunction(function(selector){return function(elem){return Sizzle(selector,elem).length>0}}),contains:markFunction(function(text){return function(elem){return(elem.textContent||elem.innerText||getText(elem)).indexOf(text)>-1}}),lang:markFunction(function(lang){if(!ridentifier.test(lang||"")){Sizzle.error("unsupported lang: "+lang)}lang=lang.replace(runescape,funescape).toLowerCase();return function(elem){var elemLang;do{if(elemLang=documentIsHTML?elem.lang:elem.getAttribute("xml:lang")||elem.getAttribute("lang")){elemLang=elemLang.toLowerCase();return elemLang===lang||elemLang.indexOf(lang+"-")===0}}while((elem=elem.parentNode)&&elem.nodeType===1);return false}}),target:function(elem){var hash=window.location&&window.location.hash;return hash&&hash.slice(1)===elem.id},root:function(elem){return elem===docElem},focus:function(elem){return elem===document.activeElement&&(!document.hasFocus||document.hasFocus())&&!!(elem.type||elem.href||~elem.tabIndex)},enabled:function(elem){return elem.disabled===false},disabled:function(elem){return elem.disabled===true},checked:function(elem){var nodeName=elem.nodeName.toLowerCase();return nodeName==="input"&&!!elem.checked||nodeName==="option"&&!!elem.selected},selected:function(elem){if(elem.parentNode){elem.parentNode.selectedIndex}return elem.selected===true},empty:function(elem){for(elem=elem.firstChild;elem;elem=elem.nextSibling){if(elem.nodeName>"@"||elem.nodeType===3||elem.nodeType===4){return false}}return true},parent:function(elem){return!Expr.pseudos["empty"](elem)},header:function(elem){return rheader.test(elem.nodeName)},input:function(elem){return rinputs.test(elem.nodeName)},button:function(elem){var name=elem.nodeName.toLowerCase();return name==="input"&&elem.type==="button"||name==="button"},text:function(elem){var attr;return elem.nodeName.toLowerCase()==="input"&&elem.type==="text"&&((attr=elem.getAttribute("type"))==null||attr.toLowerCase()===elem.type)},first:createPositionalPseudo(function(){return[0]}),last:createPositionalPseudo(function(matchIndexes,length){return[length-1]}),eq:createPositionalPseudo(function(matchIndexes,length,argument){return[argument<0?argument+length:argument]}),even:createPositionalPseudo(function(matchIndexes,length){var i=0;for(;i=0;){matchIndexes.push(i)}return matchIndexes}),gt:createPositionalPseudo(function(matchIndexes,length,argument){var i=argument<0?argument+length:argument;for(;++i1?function(elem,context,xml){var i=matchers.length;while(i--){if(!matchers[i](elem,context,xml)){return false}}return true}:matchers[0]}function condense(unmatched,map,filter,context,xml){var elem,newUnmatched=[],i=0,len=unmatched.length,mapped=map!=null;for(;i-1){seed[temp]=!(results[temp]=elem)}}}}else{matcherOut=condense(matcherOut===results?matcherOut.splice(preexisting,matcherOut.length):matcherOut);if(postFinder){postFinder(null,results,matcherOut,xml)}else{push.apply(results,matcherOut)}}})}function matcherFromTokens(tokens){var checkContext,matcher,j,len=tokens.length,leadingRelative=Expr.relative[tokens[0].type],implicitRelative=leadingRelative||Expr.relative[" "],i=leadingRelative?1:0,matchContext=addCombinator(function(elem){return elem===checkContext},implicitRelative,true),matchAnyContext=addCombinator(function(elem){return indexOf.call(checkContext,elem)>-1},implicitRelative,true),matchers=[function(elem,context,xml){return!leadingRelative&&(xml||context!==outermostContext)||((checkContext=context).nodeType?matchContext(elem,context,xml):matchAnyContext(elem,context,xml))}];for(;i1&&elementMatcher(matchers),i>1&&toSelector(tokens.slice(0,i-1).concat({value:tokens[i-2].type===" "?"*":""})).replace(rtrim,"$1"),matcher,i0,byElement=elementMatchers.length>0,superMatcher=function(seed,context,xml,results,expandContext){var elem,j,matcher,setMatched=[],matchedCount=0,i="0",unmatched=seed&&[],outermost=expandContext!=null,contextBackup=outermostContext,elems=seed||byElement&&Expr.find["TAG"]("*",expandContext&&context.parentNode||context),dirrunsUnique=dirruns+=contextBackup==null?1:Math.random()||.1,len=elems.length;if(outermost){outermostContext=context!==document&&context;cachedruns=matcherCachedRuns}for(;i!==len&&(elem=elems[i])!=null;i++){if(byElement&&elem){j=0;while(matcher=elementMatchers[j++]){if(matcher(elem,context,xml)){results.push(elem);break}}if(outermost){dirruns=dirrunsUnique;cachedruns=++matcherCachedRuns}}if(bySet){if(elem=!matcher&&elem){matchedCount--}if(seed){unmatched.push(elem)}}}matchedCount+=i;if(bySet&&i!==matchedCount){j=0;while(matcher=setMatchers[j++]){matcher(unmatched,setMatched,context,xml)}if(seed){if(matchedCount>0){while(i--){if(!(unmatched[i]||setMatched[i])){setMatched[i]=pop.call(results)}}}setMatched=condense(setMatched)}push.apply(results,setMatched);if(outermost&&!seed&&setMatched.length>0&&matchedCount+setMatchers.length>1){Sizzle.uniqueSort(results)}}if(outermost){dirruns=dirrunsUnique;outermostContext=contextBackup}return unmatched};return bySet?markFunction(superMatcher):superMatcher}compile=Sizzle.compile=function(selector,group){var i,setMatchers=[],elementMatchers=[],cached=compilerCache[selector+" "];if(!cached){if(!group){group=tokenize(selector)}i=group.length;while(i--){cached=matcherFromTokens(group[i]);if(cached[expando]){setMatchers.push(cached)}else{elementMatchers.push(cached)}}cached=compilerCache(selector,matcherFromGroupMatchers(elementMatchers,setMatchers))}return cached};function multipleContexts(selector,contexts,results){var i=0,len=contexts.length;for(;i2&&(token=tokens[0]).type==="ID"&&support.getById&&context.nodeType===9&&documentIsHTML&&Expr.relative[tokens[1].type]){context=(Expr.find["ID"](token.matches[0].replace(runescape,funescape),context)||[])[0];if(!context){return results}selector=selector.slice(tokens.shift().value.length)}i=matchExpr["needsContext"].test(selector)?0:tokens.length;while(i--){token=tokens[i];if(Expr.relative[type=token.type]){break}if(find=Expr.find[type]){if(seed=find(token.matches[0].replace(runescape,funescape),rsibling.test(tokens[0].type)&&context.parentNode||context)){tokens.splice(i,1);selector=seed.length&&toSelector(tokens);if(!selector){push.apply(results,seed);return results}break}}}}}compile(selector,match)(seed,context,!documentIsHTML,results,rsibling.test(selector));return results}support.sortStable=expando.split("").sort(sortOrder).join("")===expando;support.detectDuplicates=hasDuplicate;setDocument();support.sortDetached=assert(function(div1){return div1.compareDocumentPosition(document.createElement("div"))&1});if(!assert(function(div){div.innerHTML=" ";return div.firstChild.getAttribute("href")==="#"})){addHandle("type|href|height|width",function(elem,name,isXML){if(!isXML){return elem.getAttribute(name,name.toLowerCase()==="type"?1:2)}})}if(!support.attributes||!assert(function(div){div.innerHTML=" ";div.firstChild.setAttribute("value","");return div.firstChild.getAttribute("value")===""})){addHandle("value",function(elem,name,isXML){if(!isXML&&elem.nodeName.toLowerCase()==="input"){return elem.defaultValue}})}if(!assert(function(div){return div.getAttribute("disabled")==null})){addHandle(booleans,function(elem,name,isXML){var val;if(!isXML){return(val=elem.getAttributeNode(name))&&val.specified?val.value:elem[name]===true?name.toLowerCase():null}})}if(typeof define==="function"&&define.amd){define(function(){return Sizzle})}else{window.Sizzle=Sizzle}})(window);if(typeof JSON!=="object"){JSON={}}(function(){"use strict";function f(n){return n<10?"0"+n:n}if(typeof Date.prototype.toJSON!=="function"){Date.prototype.toJSON=function(){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(){return this.valueOf()}}var cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'"'+string.replace(escapable,function(a){var c=meta[a];return typeof c==="string"?c:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+string+'"'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==="object"&&typeof value.toJSON==="function"){value=value.toJSON(key)}if(typeof rep==="function"){value=rep.call(holder,key,value)}switch(typeof value){case"string":return quote(value);case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null"}gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==="[object Array]"){length=value.length;for(i=0;i=0;--i){if(handlers[i]===handler){handlers.splice(i,1);break}}},onerror:null,onmessage:null,onopen:null,readyState:0,URL:""};var MessageEvent=function(data,origin,lastEventId){this.data=data;this.origin=origin;this.lastEventId=lastEventId||""};MessageEvent.prototype={data:null,type:"message",lastEventId:"",origin:""};if("module"in global)module.exports=EventSource;global.EventSource=EventSource})(this);var Scout=function(){};Scout=function Scoutmaker(){var xhr;if(window.XMLHttpRequest){xhr=new XMLHttpRequest;if(xhr.overrideMimeType){xhr.overrideMimeType("text/xml")}}else{try{xhr=new ActiveXObject("Msxml2.XMLHTTP")}catch(e){xhr=new ActiveXObject("Microsoft.XMLHTTP")}}var params={method:"POST",resp:function(resp,xhr){},error:function(status,xhr){},partial:function(raw,resp,xhr){}};var toxhrsend=function(data){var str="",start=true;var jsondata="";for(var key in data){if(typeof(jsondata=JSON.stringify(data[key]))==="string"){str+=start?"":"&";str+=encodeURIComponent(key)+"="+encodeURIComponent(jsondata);if(start){start=false}}}return str};var sendxhr=function(target,params){if(params.action){params.url="/$"+params.action}if(params.url){xhr.onreadystatechange=function(){switch(xhr.readyState){case 3:if(params.partial===undefined){var raw=xhr.responseText;var resp;try{resp=JSON.parse(raw)}catch(e){}params.partial.apply(target,[raw,resp,xhr])}break;case 4:if(xhr.status===200){var resp=JSON.parse(xhr.responseText);params.resp.apply(target,[resp,xhr])}else{params.error.apply(target,[xhr.status,xhr])}break}};if(params.method==="POST"&&(params.data==={}||params.data===undefined)){params.method="GET"}xhr.open(params.method,params.url+(params.method==="POST"?"":"?"+toxhrsend(params.data)),true,params.user,params.password);if(params.method==="POST"&¶ms.data!=={}){xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");xhr.send(toxhrsend(params.data))}else{xhr.send(null)}}};var onprop=function(eventName,before){before=before||function(params,e,xhr){};var listenerfunc=function(e){if(!e&&window.event){e=event}var target=e.target||e.srcElement||undefined;if(eventName==="submit"){if(e.preventDefault){e.preventDefault()}else{e.returnValue=false}}before.apply(target,[params,e,xhr]);sendxhr(target,params)};if(document.addEventListener){this.addEventListener(eventName,listenerfunc,false)}else{this.attachEvent("on"+eventName,listenerfunc)}};var ret=function(id){var domelt=document.querySelector(id);if(!domelt){return{on:function(){}}}domelt.on=onprop;return domelt};ret.send=function(before){if(xhr.readyState===1){return Scoutmaker().send(before)}before=before||function(params,xhr){};return function(){before.apply(undefined,[params,xhr]);sendxhr(undefined,params)}};ret.maker=Scoutmaker;ret.eventSource=function(channel){var es=new EventSource("/$"+channel);es.onrecv=function(cb){es.onmessage=function(event){cb(JSON.parse(event.data))}};return es};if(window.io){ret.socket=function(namespace){if(namespace===undefined){namespace="/"}return io.connect(namespace,{resource:"$socket.io"})}}if(window.WebSocket){var wsSend=function wsSendJSON(json){this.send(JSON.stringify(json))};ret.webSocket=function newWebSocket(channel){var socket=new WebSocket("ws"+window.location.protocol.slice(4)+"//"+window.location.host+"/$websocket:"+channel);socket.sendjson=wsSend.bind(socket);return socket}}return ret}();
--------------------------------------------------------------------------------
/lib/camp.js:
--------------------------------------------------------------------------------
1 | // Server-side Ajax handler that wraps around node.js.
2 | // Copyright © Thaddee Tyl, Jan Keromnes. All rights reserved.
3 | // Code covered by the LGPL license.
4 |
5 | "use strict";
6 |
7 | var templateReader = require('fleau');
8 | var formidable = require('formidable');
9 | var WebSocket = require('ws');
10 | var Cookies = require('cookies');
11 |
12 | var EventEmitter = require ('events').EventEmitter;
13 | var inherits = require('util').inherits;
14 | var http = require('http');
15 | var https = require('https');
16 | var spdy = require('spdy');
17 | var log = require('multilog');
18 | var p = require('path');
19 | var fs = require('fs');
20 | var url = require('url');
21 | var zlib = require('zlib');
22 | var stream = require('stream');
23 | var querystring = require('querystring');
24 |
25 | // Logs.
26 | log.pipe('warn', 'all');
27 | log.pipe('error', 'all');
28 | log.pipe('error', 'stderr');
29 | log.pipe('warn', 'stderr');
30 |
31 |
32 |
33 |
34 | var mime = require('./mime.json');
35 | var binaries = [
36 | 'pdf', 'ps', 'odt', 'ods', 'odp', 'xls', 'doc', 'ppt', 'dvi', 'ttf',
37 | 'swf', 'rar', 'zip', 'tar', 'gz', 'ogg', 'mp3', 'mpeg', 'wav', 'wma',
38 | 'gif', 'jpg', 'jpeg', 'png', 'svg', 'tiff', 'ico', 'mp4', 'ogv', 'mov',
39 | 'webm', 'wmv'
40 | ];
41 | var serverStartTime = new Date();
42 | var serverStartTimeGMTString = serverStartTime.toGMTString();
43 |
44 |
45 | // Augment IncomingMessage and ServerResponse.
46 | function augmentReqRes(req, res, server) {
47 | req.server = server;
48 | req.uri = url.parse(req.url, true);
49 | // The form is used for multipart data.
50 | req.form = undefined;
51 | try {
52 | req.path = decodeURIComponent(req.uri.pathname);
53 | } catch(e) { // Using `escape` should not kill the server.
54 | req.path = unescape(req.uri.pathname);
55 | }
56 | req.query = req.uri.query;
57 | req.data = req.query;
58 | // Check (and add fields) for basic authentication.
59 | req.username = undefined;
60 | req.password = undefined;
61 | if (req.headers.authorization) {
62 | var authorization = req.headers.authorization;
63 | var authComponent = authorization.split(/\s+/);
64 | if (authComponent[0] === 'Basic') {
65 | // Username / password.
66 | if (typeof authComponent[1] === 'string' || authComponent[1] instanceof String) {
67 | var up = Buffer.from(authComponent[1], 'base64').toString().split(':');
68 | req.username = up[0];
69 | req.password = up[1];
70 | }
71 | }
72 | }
73 | // Cookies
74 | req.cookies = new Cookies(req, res);
75 | // Templates
76 | res.template = function(scope, templates) {
77 | // If there is no template.
78 | if (templates === undefined) {
79 | // We assume that the template is for a file on disk under documentRoot.
80 | var realpath = protectedPath(req.server.documentRoot, req.path);
81 | templates = [server.template(realpath)];
82 | }
83 | // If there is only one template.
84 | if (!(templates instanceof Array)) { templates = [templates]; }
85 |
86 | if (templates.length > 0) {
87 | res.mime(p.extname(templates[0].paths[0]).slice(1));
88 | }
89 | var templateStreams = templates.map(function(template) {
90 | if (typeof template === 'string') {
91 | return streamFromString(template);
92 | } else if (typeof template === 'function') {
93 | return template(scope);
94 | } else {
95 | log('Template ' + template + ' does not have a valid type', 'warn');
96 | return streamFromString('');
97 | }
98 | });
99 | var template = concatStreams(templateStreams);
100 | template.on('error', function(err) {
101 | log(err.stack, 'error');
102 | res.end('Not Found\n');
103 | });
104 | template.pipe(res.compressed());
105 | };
106 | // Sending a file
107 | res.file = function(path) {
108 | respondWithFile(req, res, path, function ifNoFile() {
109 | log('The file "' + path + '", which was meant to be sent back, ' +
110 | 'was not found', 'error');
111 | res.statusCode = 404;
112 | res.end('Not Found\n');
113 | });
114 | };
115 | // Sending JSON data
116 | res.json = function (data, replacer, space) {
117 | res.setHeader('Content-Type', mime.json);
118 | var json = JSON.stringify(data, replacer, space) + (space ? '\n' : '');
119 | res.compressed().end(json);
120 | };
121 | res.compressed = function() {
122 | return getCompressedStream(req, res) || res;
123 | };
124 | res.redirect = function(path) {
125 | res.setHeader('Location', path);
126 | res.statusCode = 303;
127 | res.end();
128 | };
129 | }
130 |
131 | // Set a content type based on a file extension.
132 | http.ServerResponse.prototype.mime =
133 | function mimeFromExt(ext) {
134 | this.setHeader('Content-Type', mime[ext] || 'text/plain');
135 | }
136 |
137 | // Ask is a model of the client's request / response environment.
138 | // It will be slowly deprecated.
139 | function Ask(server, req, res) {
140 | this.server = server;
141 | this.req = req;
142 | this.res = res;
143 | this.uri = req.uri;
144 | this.form = req.form;
145 | this.path = req.path;
146 | this.query = req.query;
147 | this.username = req.username;
148 | this.password = req.password;
149 | this.cookies = req.cookies;
150 | }
151 |
152 | // Set the mime type of the response.
153 | Ask.prototype.mime = function (type) {
154 | this.res.setHeader('Content-Type', type);
155 | };
156 |
157 | function addToQuery(ask, obj) {
158 | for (var item in obj) {
159 | ask.query[item] = obj[item];
160 | }
161 | }
162 |
163 | function jsonFromQuery(query, obj) {
164 | obj = obj || Object.create(null);
165 | // First attempt to decode the query as JSON
166 | // (ie, 'foo="bar"&baz={"something":"else"}').
167 | try {
168 | var items = query.split('&');
169 | for (var i = 0; i < items.length; i++) {
170 | // Each element of key=value is then again split along `=`.
171 | var elems = items[i].split('=');
172 | obj[decodeURIComponent(elems[0])] =
173 | JSON.parse(decodeURIComponent(elems[1]));
174 | }
175 | } catch(e) {
176 | // Couldn't parse as JSON.
177 | try {
178 | var newobj = querystring.parse(query);
179 | } catch(e) {
180 | log('Error while parsing query ', JSON.stringify(query) + '\n'
181 | + '(' + e.toString() + ')\n'
182 | , 'error');
183 | }
184 | var keys = Object.keys(newobj);
185 | for (var i = 0; i < keys.length; i++) {
186 | obj[keys[i]] = newobj[keys[i]];
187 | }
188 | }
189 | }
190 |
191 | // We'll need to parse the query as a literal.
192 | // Ask objects already have ask.query set after the URL query part.
193 | // This function updates ask.query with:
194 | // - application/x-www-form-urlencoded
195 | // - multipart/form-data
196 | function getQueries(req, end) {
197 | var urlencoded = /^application\/x-www-form-urlencoded/;
198 | var multipart = /^multipart\/form-data/;
199 | var contentType = req.headers['content-type'] || '';
200 |
201 | if (multipart.test(contentType)) {
202 | // Multipart data.
203 | req.form = req.form || new formidable.IncomingForm();
204 | req.form.multiples = true;
205 | req.form.parse(req, function(err, fields, files) {
206 | req.fields = fields;
207 | req.files = files;
208 | // Ensure that files are arrays.
209 | for (var key in req.files) {
210 | if (!(req.files[key] instanceof Array)) {
211 | req.files[key] = [req.files[key]];
212 | }
213 | }
214 | if (err == null) {
215 | addToQuery(req, req.fields);
216 | addToQuery(req, req.files);
217 | }
218 | end(err);
219 | });
220 |
221 | } else if (urlencoded.test(contentType)) {
222 | // URL encoded data.
223 | var chunks;
224 | var gotrequest = function (chunk) {
225 | if (chunk !== undefined) {
226 | if (chunks === undefined) {
227 | chunks = chunk;
228 | } else {
229 | chunks = Buffer.concat([chunks, chunk]);
230 | }
231 | }
232 | };
233 | req.on('data', gotrequest);
234 | req.on('end', function(err) {
235 | if (req.server.saveRequestChunks) {
236 | req.savedChunks = chunks;
237 | }
238 | var strquery = chunks? chunks.toString(): '';
239 | jsonFromQuery(strquery, req.query);
240 | end(err);
241 | });
242 | } else {
243 | // URL query parameters.
244 | var search = req.uri.search || '';
245 | jsonFromQuery(search.slice(1), req.query);
246 | end();
247 | }
248 | }
249 |
250 | // Return a writable response stream, using compression when possible.
251 | function getCompressedStream(req, res) {
252 | var encoding = req.headers['accept-encoding'] || '';
253 | var stream;
254 | var contentEncodingHeader = res.getHeader('Content-Encoding');
255 | if ((contentEncodingHeader === 'gzip') || /\bgzip\b/.test(encoding)) {
256 | if (!contentEncodingHeader) {
257 | res.setHeader('Content-Encoding', 'gzip');
258 | }
259 | stream = zlib.createGzip();
260 | stream.pipe(res);
261 | } else if ((contentEncodingHeader === 'deflate') ||
262 | /\bdeflate\b/.test(encoding)) {
263 | if (!contentEncodingHeader) {
264 | res.setHeader('Content-Encoding', 'deflate');
265 | }
266 | stream = zlib.createDeflate();
267 | stream.pipe(res);
268 | }
269 | return stream;
270 | }
271 |
272 | // Concatenate an array of streams into a single stream.
273 | function concatStreams(array) {
274 | var concat = new stream.PassThrough();
275 |
276 | function pipe(i) {
277 | if (i < array.length - 1) {
278 | array[i].pipe(concat, {end: false});
279 | array[i].on('end', function () { pipe(i + 1) });
280 | } else {
281 | array[i].pipe(concat);
282 | }
283 | }
284 | if (array.length > 0) {
285 | pipe(0);
286 | }
287 |
288 | return concat;
289 | }
290 |
291 | // paths: paths to templating file (String), or to an Array of templating files.
292 | // options:
293 | // - reader: template reader function.
294 | // - asString: use the string as a template, not as a file path.
295 | // - callback: function taking a function(scope) → readableStream.
296 | // If you don't want the template creation to be synchronous, use this.
297 | // We return nothing from the function if `callback` is set.
298 | // Returns a function(scope) → readableStream, unless `callback` is set.
299 | function template(paths, options) {
300 | options = options || {};
301 | var callback = options.callback;
302 | var reader = options.reader || this.templateReader;
303 |
304 | // Deal with a list of paths in the general case.
305 | if (!(paths instanceof Array)) { paths = [paths]; }
306 |
307 | var input = '';
308 | if (!callback) {
309 | for (var i = 0, pathLen = paths.length; i < pathLen; i++) {
310 | if (options.asString) {
311 | input += paths[i];
312 | } else {
313 | input += '' + fs.readFileSync(paths[i]);
314 | }
315 | }
316 | var result = reader.create(input);
317 | result.paths = paths;
318 | return result;
319 |
320 | } else {
321 | // We have a callback.
322 | var pathLen = paths.length;
323 | var pathCounter = 0;
324 | paths.forEach(function(path, i) {
325 | if (options.asString) {
326 | var getInput = function(cb) { cb(path); };
327 | } else {
328 | var getInput = function(cb) {
329 | fs.readFile(path, function(err, string) {
330 | if (err != null) {
331 | log('Error reading template file:\n' + err, 'error');
332 | return cb('');
333 | }
334 | return cb('' + string);
335 | });
336 | };
337 | }
338 | getInput(function(fileInput) {
339 | pathCounter++;
340 | input += fileInput;
341 |
342 | if (pathCounter >= pathLen) {
343 | var result = reader.create(input);
344 | result.paths = paths;
345 | callback(result);
346 | }
347 | });
348 | });
349 | }
350 | }
351 |
352 |
353 |
354 |
355 | // Camp class is classy.
356 | //
357 | // Camp has a router function that returns the stack of functions to call, one
358 | // after the other, in order to process the request.
359 |
360 | function augmentServer(server, opts) {
361 | server.templateReader = opts.templateReader || templateReader;
362 | server.documentRoot = opts.documentRoot || p.join(process.cwd(), 'web');
363 | server.saveRequestChunks = !!opts.saveRequestChunks;
364 | server.template = template;
365 | server.stack = [];
366 | server.staticMaxAge = opts.staticMaxAge
367 | server.stackInsertion = 0;
368 | defaultRoute.forEach(function(mkfn) { server.handle(mkfn(server)); });
369 | server.stackInsertion = 0;
370 | server.on('request', function(req, res) { listener(server, req, res) });
371 | }
372 |
373 | function Camp(opts) {
374 | http.Server.call(this);
375 | augmentServer(this, opts);
376 | }
377 | inherits(Camp, http.Server);
378 |
379 | function SecureCamp(opts) {
380 | https.Server.call(this, opts);
381 | augmentServer(this, opts);
382 | }
383 | inherits(SecureCamp, https.Server);
384 |
385 | function SpdyCamp(opts) {
386 | spdy.Server.call(this, opts);
387 | augmentServer(this, opts);
388 | }
389 | inherits(SpdyCamp, spdy.Server);
390 |
391 | Camp.prototype.handle = SecureCamp.prototype.handle = SpdyCamp.prototype.handle =
392 | function handle(fn) {
393 | this.stack.splice(this.stackInsertion, 0, fn);
394 | this.stackInsertion++;
395 | };
396 |
397 | Camp.prototype.removeHandler = SecureCamp.prototype.removeHandler = SpdyCamp.prototype.removeHandler =
398 | function removeHandler(fn) {
399 | var index = this.stack.indexOf(fn);
400 | if (index < 0) { return; }
401 | if (index < this.stackInsertion) {
402 | this.stackInsertion--;
403 | }
404 | this.stack.splice(index, 1);
405 | };
406 |
407 | // Default request listener.
408 |
409 | function listener(server, req, res) {
410 | augmentReqRes(req, res, server);
411 | var ask = new Ask(server, req, res);
412 | req.ask = ask; // Legacy.
413 | bubble(ask, 0);
414 | }
415 |
416 | // The bubble goes through each layer of the stack until it reaches the surface.
417 | // The surface is a Server Error, btw.
418 | function bubble(ask, layer) {
419 | ask.server.stack[layer](ask.req, ask.res, function next() {
420 | if (ask.server.stack.length > layer + 1) bubble(ask, layer + 1);
421 | else {
422 | ask.res.statusCode = 500;
423 | ask.res.end('Internal Server Error\n');
424 | }
425 | });
426 | }
427 |
428 |
429 | // On-demand loading of socket.io.
430 | Camp.prototype.socketIo = SecureCamp.prototype.socketIo = SpdyCamp.prototype.socketIo
431 | = null;
432 | var socketIoProperty = {
433 | get: function() {
434 | if (this.socketIo === null) {
435 | this.socketIo = require('socket.io')(this, {path: '/$socket.io'});
436 | // Add socketUnit only once.
437 | this.stack.unshift(socketUnit(this));
438 | }
439 | return this.socketIo;
440 | },
441 | };
442 | Object.defineProperty(Camp.prototype, 'io', socketIoProperty);
443 | Object.defineProperty(SecureCamp.prototype, 'io', socketIoProperty);
444 | Object.defineProperty(SpdyCamp.prototype, 'io', socketIoProperty);
445 |
446 |
447 |
448 | // Generic unit. Deprecated.
449 | function genericUnit (server) {
450 | var processors = [];
451 | server.handler = function (f) { processors.push(f); };
452 | return function genericLayer (req, res, next) {
453 | for (var i = 0; i < processors.length; i++) {
454 | var keep = processors[i](req.ask);
455 | if (keep) { return; } // Don't call next, nor the rest.
456 | }
457 | next(); // We never catch that request.
458 | };
459 | }
460 |
461 | // Socket.io unit.
462 | function socketUnit (server) {
463 | var io = server.io;
464 | // Client-side:
465 |
466 | return function socketLayer (req, res, next) {
467 | // Socket.io doesn't care about anything but /$socket.io now.
468 | if (req.path.slice(1, 11) !== '$socket.io') next();
469 | };
470 | }
471 |
472 | // WebSocket unit.
473 | function wsUnit (server) {
474 | var chanPool = server.wsChannels = {};
475 | // Main WebSocket API:
476 | // ws(channel :: String, conListener :: function(socket))
477 | server.ws = function ws (channel, conListener) {
478 | if (channel[0] !== '/') {
479 | channel = '/$websocket:' + channel; // Deprecated API.
480 | }
481 | if (chanPool[channel] !== undefined) {
482 | chanPool[channel].close();
483 | }
484 | chanPool[channel] = new WebSocket.Server({
485 | server: server,
486 | path: channel,
487 | });
488 | chanPool[channel].on('connection', conListener);
489 | return chanPool[channel];
490 | };
491 | // WebSocket broadcast API.
492 | // webBroadcast(channel :: String, recvListener :: function(data, end))
493 | server.wsBroadcast = function wsBroadcast (channel, recvListener) {
494 | if (channel[0] === '/') {
495 | return server.ws(channel, function (socket) {
496 | socket.on('message', function wsBroadcastRecv (data, flags) {
497 | recvListener({data: data, flags: flags}, {
498 | send: function wsBroadcastSend (dataBack) {
499 | chanPool[channel].clients.forEach(function (s) {
500 | s.send(dataBack);
501 | });
502 | },
503 | });
504 | });
505 | });
506 | } else { // Deprecated API
507 | return server.ws(channel, function (socket) {
508 | socket.on('message', function wsBroadcastRecv (data, flags) {
509 | recvListener(data, function wsBroadcastSend (dataBack) {
510 | chanPool[channel].clients.forEach(function (s) { s.send(dataBack); });
511 | });
512 | });
513 | });
514 | }
515 | };
516 |
517 | return function wsLayer (req, res, next) {
518 | // This doesn't actually get run, since ws overrides it at the root.
519 | if (chanPool[req.path] === undefined) return next();
520 | };
521 | }
522 |
523 | // Ajax unit.
524 | function ajaxUnit (server) {
525 | var ajax = server.ajax = new EventEmitter();
526 | // Register events to be fired before loading the ajax data.
527 | var ajaxReq = server.ajaxReq = new EventEmitter();
528 |
529 | return function ajaxLayer (req, res, next) {
530 | if (req.path[1] !== '$') { return next(); }
531 | var action = req.path.slice(2);
532 |
533 | if (ajax.listeners(action).length <= 0) { return next(); }
534 |
535 | res.setHeader('Content-Type', mime.json);
536 |
537 | ajaxReq.emit(action, req.ask);
538 | // Get all data requests.
539 | getQueries(req, function(err) {
540 | if (err == null) {
541 | ajax.emit(action, req.query, function ajaxEnd(data) {
542 | res.compressed().end(JSON.stringify(data || {}));
543 | }, req.ask);
544 | } else {
545 | log('While parsing', req.url + ':\n'
546 | + err
547 | , 'error');
548 | return next();
549 | }
550 | });
551 | };
552 | }
553 |
554 |
555 | // EventSource unit.
556 | //
557 | // Note: great inspiration was taken from Remy Sharp's code.
558 | function eventSourceUnit (server) {
559 | var sources = {};
560 |
561 | function Source () {
562 | this.conn = [];
563 | this.history = [];
564 | this.lastMsgId = 0;
565 | }
566 |
567 | Source.prototype.removeConn = function(res) {
568 | var i = this.conn.indexOf(res);
569 | if (i !== -1) {
570 | this.conn.splice(i, 1);
571 | }
572 | };
573 |
574 | Source.prototype.sendSSE = function (res, id, event, message) {
575 | var data = '';
576 | if (event !== null) {
577 | data += 'event:' + event + '\n';
578 | }
579 |
580 | // Blank id resets the id counter.
581 | if (id !== null) {
582 | data += 'id:' + id + '\n';
583 | } else {
584 | data += 'id\n';
585 | }
586 |
587 | if (message) {
588 | data += 'data:' + message.split('\n').join('\ndata:') + '\n';
589 | }
590 | data += '\n';
591 |
592 | res.write(data);
593 |
594 | if (res.hasOwnProperty('xhr')) {
595 | clearTimeout(res.xhr);
596 | var self = this;
597 | res.xhr = setTimeout(function () {
598 | res.end();
599 | self.removeConn(res);
600 | }, 250);
601 | }
602 | };
603 |
604 | Source.prototype.emit = function (event, msg) {
605 | this.lastMsgId++;
606 | this.history.push({
607 | id: this.lastMsgId,
608 | event: event,
609 | msg: msg
610 | });
611 |
612 | for (var i = 0; i < this.conn.length; i++) {
613 | this.sendSSE(this.conn[i], this.lastMsgId, event, msg);
614 | }
615 | }
616 |
617 | Source.prototype.send = function (msg) {
618 | this.emit(null, JSON.stringify(msg));
619 | }
620 |
621 | function eventSource (channel) {
622 | if (channel[0] !== '/') { channel = '/$' + channel; }
623 | return sources[channel] = new Source();
624 | }
625 |
626 | server.eventSource = eventSource;
627 |
628 |
629 | return function eventSourceLayer (req, res, next) {
630 | if (sources[req.path] === undefined) return next();
631 | var source = sources[req.path];
632 | if (!source || req.headers.accept !== 'text/event-stream')
633 | return next(); // Don't bother if the client cannot handle it.
634 |
635 | // Remy Sharp's Polyfill support.
636 | if (req.headers['x-requested-with'] == 'XMLHttpRequest') {
637 | res.xhr = null;
638 | }
639 |
640 | res.setHeader('Content-Type', 'text/event-stream');
641 | res.setHeader('Cache-Control', 'no-cache');
642 |
643 | if (req.headers['last-event-id']) {
644 | var id = parseInt(req.headers['last-event-id']);
645 | for (var i = 0; i < source.history.length; i++)
646 | if (source.history[i].id > id)
647 | source.sendSSE(res, source.history[i].id,
648 | source.history[i].event, source.history[i].msg);
649 | } else res.write('id\n\n'); // Reset id.
650 |
651 | source.conn.push(res);
652 |
653 | // Every 15s, send a comment (avoids proxy dropping HTTP connection).
654 | var to = setInterval(function () {res.write(':\n');}, 15000);
655 |
656 | // This can only end in blood.
657 | req.on('close', function () {
658 | source.removeConn(res);
659 | clearInterval(to);
660 | });
661 | };
662 | }
663 |
664 | function protectedPath(documentRoot, path) {
665 | return p.join(documentRoot, p.join('/', path));
666 | }
667 |
668 | function setHeadersForCacheLength(res, cacheLengthSeconds) {
669 | const now = new Date();
670 | const cacheControl = `max-age=${cacheLengthSeconds}, s-maxage=${cacheLengthSeconds}`;
671 | const expires = new Date(now.getTime() + cacheLengthSeconds * 1000).toGMTString();
672 |
673 | res.setHeader('Cache-Control', cacheControl);
674 | res.setHeader('Expires', expires);
675 | }
676 |
677 | // End the response `res` with file at path `path`.
678 | // If the file does not exist, we call `ifNoFile()`.
679 | function respondWithFile(req, res, path, ifNoFile) {
680 | ifNoFile = ifNoFile || function() {};
681 | // We use `documentRoot` as the root wherein we seek files.
682 | var realpath = protectedPath(req.server.documentRoot, path);
683 | fs.stat(realpath, function(err, stats) {
684 | if (err) return ifNoFile();
685 |
686 | if (stats.isDirectory()) {
687 | realpath = p.join(realpath, 'index.html');
688 | }
689 | res.mime(p.extname(realpath).slice(1));
690 |
691 | if (req.server.staticMaxAge) {
692 | setHeadersForCacheLength(res, req.server.staticMaxAge);
693 | }
694 |
695 | // Cache management (compare timestamps at second-level precision).
696 | var since = req.headers['if-modified-since'];
697 | if (since && (Math.floor(serverStartTime / 1000) <= Math.floor(new Date(since) / 1000))) {
698 | res.statusCode = 304; // not modified.
699 | res.end();
700 | return;
701 | }
702 | res.setHeader('Last-Modified', serverStartTimeGMTString);
703 |
704 | // Connect the output of the file to the network!
705 | var raw = fs.createReadStream(realpath);
706 | raw.on('error', function(err) {
707 | log(err.stack, 'error');
708 | res.statusCode = 404;
709 | res.end('Not Found\n');
710 | });
711 | raw.pipe(res.compressed());
712 | });
713 | }
714 |
715 | // Static unit.
716 | function staticUnit (server) {
717 | return function staticLayer (req, res, next) {
718 | respondWithFile(req, res, req.path, next);
719 | };
720 | }
721 |
722 | function escapeRegex(string) {
723 | return string.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&');
724 | }
725 |
726 | // Convert a String to a RegExp, escaping everything.
727 | // Make * stand for anything and :foo be a named non-slash placeholder.
728 | // ** is a star, :: is a colon.
729 | function regexFromString(string) {
730 | var r = /::|\*\*|:[a-zA-Z_]+|\*/g;
731 | var match;
732 | var regexString = '^';
733 | var previousIndex = 0;
734 | var regexKeys = [];
735 | var starKey = 0;
736 | while ((match = r.exec(string)) !== null) {
737 | var matched = match[0];
738 | var index = match.index;
739 | regexString += escapeRegex(string.slice(previousIndex, index));
740 | previousIndex = index;
741 | if (matched === '**') {
742 | regexString += '\\*\\*';
743 | previousIndex += 2;
744 | } else if (matched === '::') {
745 | regexString += '::';
746 | previousIndex += 2;
747 | } else if (matched === '*') {
748 | regexString += '(.*)';
749 | regexKeys.push(starKey);
750 | previousIndex += 1;
751 | starKey++;
752 | } else { // :foo
753 | regexString += '([^/]+)';
754 | regexKeys.push(matched.slice(1));
755 | previousIndex += matched.length;
756 | }
757 | }
758 | regexString += escapeRegex(string.slice(previousIndex)) + '$';
759 | var regex = new RegExp(regexString);
760 | regex.keys = regexKeys;
761 | return regex;
762 | }
763 |
764 | function Path(matcher) {
765 | this.matcher = matcher;
766 | if (matcher instanceof RegExp) {
767 | this.match = function(path) {
768 | return path.match(matcher);
769 | };
770 | } else if (typeof matcher === 'string') {
771 | if (matcher[0] !== '/' && matcher[0] !== '*') {
772 | matcher = '/' + matcher;
773 | }
774 | if (/[:*]/.test(matcher)) {
775 | matcher = regexFromString(matcher);
776 | }
777 |
778 | // Takes a String path. If the matcher doesn't match it, return null;
779 | // otherwise, return an object mapping keys (from the matcher) to the String
780 | // corresponding to it in the path. Matchers like :foo have key 'foo',
781 | // matchers like * have an integer key starting from 0.
782 | this.match = function(path) {
783 | var matched;
784 | if ((typeof matcher === 'string') && (path === matcher)) {
785 | return {};
786 | } else if ((matcher instanceof RegExp)
787 | && (matched = matcher.exec(path))) {
788 | var data = {};
789 | for (var i = 0; i < matcher.keys.length; i++) {
790 | var key = matcher.keys[i];
791 | var value = matched[i + 1];
792 | data[key] = value;
793 | }
794 | return data;
795 | } else { return null; }
796 | };
797 | }
798 | }
799 |
800 | // Path-like unit.
801 | //
802 | // The optional `httpMethods` array allows specifying which HTTP methods should
803 | // have their own specific helpers like `server.get()`, `server.post()`, etc.
804 | // Warning: Calling `pathLikeUnit` several times with the same HTTP methods will
805 | // overwrite any corresponding `server[method]` helpers!
806 | function pathLikeUnit(serverMethod, statusCode, httpMethods) {
807 | httpMethods = httpMethods || [];
808 | return function pathLikeUnit(server) {
809 | var callbacks = [];
810 |
811 | var addCallback = function (path, callback) {
812 | callbacks.push({
813 | path: new Path(path),
814 | methods: null,
815 | callback: callback
816 | });
817 | };
818 | server[serverMethod] = addCallback;
819 |
820 | // HTTP method-specific helpers like server.get(), server.post(), etc.
821 | httpMethods.forEach(function (method) {
822 | server[method.toLowerCase()] = function (path, callback) {
823 | callbacks.push({
824 | path: new Path(path),
825 | methods: [method],
826 | callback: callback
827 | });
828 | };
829 | });
830 |
831 | return function pathLayer (req, res, next) {
832 | var pathLen = callbacks.length;
833 | var matched = null;
834 | var cbindex = -1;
835 | for (var i = 0; i < pathLen; i++) {
836 | matched = callbacks[i].path.match(req.path);
837 | if (matched == null) {
838 | continue;
839 | }
840 | var methods = callbacks[i].methods;
841 | if (methods != null && methods.indexOf(req.method) === -1) {
842 | continue;
843 | }
844 | cbindex = i; break;
845 | }
846 | if (cbindex >= 0) {
847 | getQueries(req, function(err) {
848 | if (err != null) {
849 | log('While getting queries for ' + req.url + ' in pathUnit:\n'
850 | + err.stack, 'error');
851 | }
852 | for (var key in matched) {
853 | req.data[key] = matched[key];
854 | }
855 | res.statusCode = statusCode;
856 | var cb = callbacks[cbindex] && callbacks[cbindex].callback;
857 | if (cb != null) {
858 | cb(req, res);
859 | } else {
860 | res.template(req.data);
861 | }
862 | });
863 | } else { next(); }
864 | };
865 | };
866 | }
867 | var pathUnit = pathLikeUnit('path', 200, http.METHODS);
868 | var notFoundUnit = pathLikeUnit('notFound', 404);
869 |
870 | // Template unit.
871 | function routeUnit (server) {
872 | var regexes = [];
873 | var callbacks = [];
874 |
875 | function route (paths, literalCall) {
876 | regexes.push(RegExp(paths));
877 | callbacks.push(literalCall);
878 | }
879 |
880 | server.route = route;
881 |
882 | return function routeLayer (req, res, next) {
883 | var matched = null;
884 | var cbindex = -1;
885 | for (var i = 0; i < regexes.length; i++) {
886 | matched = req.path.match (regexes[i]);
887 | if (matched !== null) { cbindex = i; break; }
888 | }
889 | if (cbindex >= 0) {
890 | catchpath(req, res, matched, callbacks[cbindex], server.templateReader);
891 | } else {
892 | next();
893 | }
894 | };
895 | }
896 |
897 | // Not Fount unit — in fact, mostly a copy&paste of the route unit.
898 | function notfoundUnit (server) {
899 | var regexes = [];
900 | var callbacks = [];
901 |
902 | function notfound (paths, literalCall) {
903 | regexes.push(RegExp(paths));
904 | callbacks.push(literalCall);
905 | }
906 |
907 | server.notfound = notfound;
908 |
909 | return function notfoundLayer (req, res) {
910 | res.statusCode = 404;
911 | var matched = null;
912 | var cbindex = -1;
913 | for (var i = 0; i < regexes.length; i++) {
914 | matched = req.path.match (regexes[i]);
915 | if (matched !== null) { cbindex = i; break; }
916 | }
917 | if (cbindex >= 0) {
918 | catchpath(req, res, matched, callbacks[cbindex], server.templateReader);
919 | } else {
920 | res.end('Not Found\n');
921 | }
922 | };
923 | }
924 |
925 | // Route *and* not found units — see what I did there?
926 |
927 | function catchpath (req, res, pathmatch, callback, templateReader) {
928 | getQueries(req, function gotQueries(err) {
929 | if (err != null) {
930 | log('While getting queries for ' + req.uri + ':\n'
931 | + err
932 | , 'error');
933 | } else {
934 | // params: template parameters (JSON-serializable).
935 | callback(req.query, pathmatch, function end (params, options) {
936 | options = options || {};
937 | var templates = options.template || pathmatch[0];
938 | var reader = options.reader || templateReader;
939 | if (Object(options.string) instanceof String) {
940 | templates = [streamFromString(options.string)];
941 | } else if (Object(params) instanceof String) {
942 | templates = [streamFromString(params)];
943 | } else if (!Array.isArray(templates)) {
944 | templates = [templates];
945 | }
946 | if (!res.getHeader('Content-Type') // Allow overriding.
947 | && (Object(templates[0]) instanceof String)) {
948 | res.mime(p.extname(templates[0]).slice(1));
949 | }
950 | for (var i = 0; i < templates.length; i++) {
951 | if (Object(templates[i]) instanceof String) {
952 | // `templates[i]` is a string path for a file.
953 | var templatePath = p.join(req.server.documentRoot, templates[i]);
954 | templates[i] = fs.createReadStream(templatePath);
955 | }
956 | }
957 |
958 | var template = concatStreams(templates);
959 |
960 | template.on('error', function(err) {
961 | log(err.stack, 'error');
962 | res.end('Not Found\n');
963 | });
964 |
965 | if (params === null || reader === null) {
966 | // No data was given. Same behaviour as static.
967 | template.pipe(res);
968 | } else {
969 | reader(template, res, params, function errorcb(err) {
970 | if (err) {
971 | log(err.stack, 'error');
972 | res.end('Not Found\n');
973 | }
974 | });
975 | }
976 | }, req.ask);
977 | }
978 | });
979 | }
980 |
981 | // The default routing function:
982 | //
983 | // - if the request is of the form /$socket.io, it runs the socket.io unit.
984 | // (By default, that is not in. Using `server.io` loads the library.)
985 | // - if the request is of the form /$websocket:, it runs the websocket unit.
986 | // - if the request is of the form /$..., it runs the ajax / eventSource unit.
987 | // - if the request is a registered template, it runs the template unit.
988 | // - if the request isn't a registered route, it runs the static unit.
989 | // - else, it runs the notfound unit.
990 |
991 | var defaultRoute = [genericUnit, wsUnit, ajaxUnit, eventSourceUnit,
992 | pathUnit, routeUnit, staticUnit,
993 | notFoundUnit, notfoundUnit];
994 |
995 | function streamFromString(string) {
996 | var sstream = new stream.Readable();
997 | sstream._read = function() { sstream.push(string); sstream.push(null); };
998 | return sstream;
999 | }
1000 |
1001 |
1002 |
1003 |
1004 |
1005 |
1006 | // Internal start function.
1007 | //
1008 |
1009 | function createServer () { return new Camp(); }
1010 |
1011 | function createSecureServer (opts) { return new SecureCamp(opts); }
1012 |
1013 | function createSpdyServer (opts) { return new SpdyCamp(opts); }
1014 |
1015 | var KEY_HEADER = /^-+BEGIN \w* ?PRIVATE KEY-+/;
1016 | var CERT_HEADER = /^-+BEGIN CERTIFICATE-+/;
1017 | function createServerWithSettings (settings) {
1018 | var server;
1019 | settings.hostname = settings.hostname || '::';
1020 |
1021 | // Are we running https?
1022 | if (settings.secure) { // Yep
1023 | var key, cert;
1024 | if (KEY_HEADER.test(settings.key)) {
1025 | key = settings.key;
1026 | } else {
1027 | key = fs.readFileSync(settings.key);
1028 | }
1029 | if (CERT_HEADER.test(settings.cert)) {
1030 | cert = settings.cert;
1031 | } else {
1032 | cert = fs.readFileSync(settings.cert);
1033 | }
1034 | settings.key = key;
1035 | settings.cert = cert;
1036 | settings.ca = settings.ca.map(function(file) {
1037 | try {
1038 | var ca;
1039 | if (CERT_HEADER.test(file)) {
1040 | ca = file;
1041 | } else {
1042 | ca = fs.readFileSync(file);
1043 | }
1044 | return ca;
1045 | } catch (e) { log('CA file not found: ' + file, 'error'); }
1046 | });
1047 | if (settings.spdy === false) {
1048 | server = new SecureCamp(settings);
1049 | } else {
1050 | server = new SpdyCamp(settings);
1051 | }
1052 | } else { // Nope
1053 | server = new Camp(settings);
1054 | }
1055 | if (settings.setuid) {
1056 | server.on('listening', function switchuid() {
1057 | process.setuid(settings.setuid);
1058 | });
1059 | }
1060 |
1061 | server.listenAsConfigured = function() {
1062 | return this.listen(settings.port, settings.hostname);
1063 | }
1064 |
1065 | return server;
1066 | }
1067 |
1068 |
1069 | // Each camp instance creates an HTTP / HTTPS server automatically.
1070 | //
1071 | function create (settings) {
1072 |
1073 | settings = settings || {};
1074 |
1075 | // Populate security values with the corresponding files.
1076 | if (settings.secure) {
1077 | settings.passphrase = settings.passphrase || '1234';
1078 | settings.key = settings.key || 'https.key';
1079 | settings.cert = settings.cert || 'https.crt';
1080 | settings.ca = settings.ca || [];
1081 | }
1082 |
1083 | settings.port = settings.port || (settings.secure ? 443 : 80);
1084 |
1085 | return createServerWithSettings(settings);
1086 | };
1087 |
1088 | function start (settings) {
1089 | return create(settings).listenAsConfigured();
1090 | }
1091 |
1092 |
1093 | exports.create = create;
1094 | exports.start = start;
1095 | exports.createServer = createServer;
1096 | exports.createSecureServer = createSecureServer;
1097 | exports.Camp = Camp;
1098 | exports.SecureCamp = SecureCamp;
1099 | exports.SpdyCamp = SpdyCamp;
1100 |
1101 | exports.genericUnit = genericUnit;
1102 | exports.socketUnit = socketUnit;
1103 | exports.wsUnit = wsUnit;
1104 | exports.ajaxUnit = ajaxUnit;
1105 | exports.eventSourceUnit = eventSourceUnit;
1106 | exports.pathUnit = pathUnit;
1107 | exports.routeUnit = routeUnit;
1108 | exports.staticUnit = staticUnit;
1109 | exports.notfoundUnit = notfoundUnit;
1110 |
1111 | exports.template = template;
1112 | exports.templateReader = templateReader;
1113 | exports.augmentReqRes = augmentReqRes;
1114 | exports.mime = mime;
1115 | exports.binaries = binaries;
1116 | exports.log = log;
1117 |
--------------------------------------------------------------------------------