mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge remote-tracking branch 'origin/follower-node0.10' into follower
Conflicts: docs/client/concepts.html meteor packages/follower-livedata/follower.js packages/livedata/livedata_server.js packages/webapp/.npm/package/npm-shrinkwrap.json packages/webapp/package.js packages/webapp/webapp_server.js scripts/generate-dev-bundle.sh tools/bundler.js tools/meteor.js tools/server/boot.js
This commit is contained in:
35
.mailmap
Normal file
35
.mailmap
Normal file
@@ -0,0 +1,35 @@
|
||||
# This makes it easier to find GitHub usernames for History.md.
|
||||
#
|
||||
# This controls 'git shortlog'. eg, run:
|
||||
# git shortlog -s release/0.6.5.1..HEAD
|
||||
# to get a sorted list of all committers to revisions in HEAD but not
|
||||
# in 0.6.5.1.
|
||||
#
|
||||
# For any emails that show up in the shortlog that aren't in one of
|
||||
# these lists, figure out their GitHub username and add them.
|
||||
|
||||
GITHUB: ansman <nicklas@ansman.se>
|
||||
GITHUB: awwx <andrew.wilcox@gmail.com>
|
||||
GITHUB: codeinthehole <david.winterbottom@gmail.com>
|
||||
GITHUB: jacott <geoffjacobsen@gmail.com>
|
||||
GITHUB: Maxhodges <Max@whiterabbitpress.com>
|
||||
GITHUB: meawoppl <meawoppl@gmail.com>
|
||||
GITHUB: mitar <mitar.git@tnode.com>
|
||||
GITHUB: mizzao <mizzao@gmail.com>
|
||||
GITHUB: mquandalle <maxime.quandalle@gmail.com>
|
||||
GITHUB: nathan-muir <ndmuir@gmail.com>
|
||||
GITHUB: RobertLowe <robert@iblargz.com>
|
||||
GITHUB: ryw <ry@rywalker.com>
|
||||
GITHUB: sdarnell <stephen@darnell.plus.com>
|
||||
GITHUB: timhaines <tmhaines@gmail.com>
|
||||
|
||||
METEOR: avital <avital@thewe.net>
|
||||
METEOR: debergalis <matt@meteor.com>
|
||||
METEOR: dgreensp <dgreenspan@alum.mit.edu>
|
||||
METEOR: estark37 <emily@meteor.com>
|
||||
METEOR: estark37 <estark37@gmail.com>
|
||||
METEOR: glasser <glasser@meteor.com>
|
||||
METEOR: gschmidt <geoff@geoffschmidt.com>
|
||||
METEOR: n1mmy <nim@meteor.com>
|
||||
METEOR: sixolet <naomi@meteor.com>
|
||||
METEOR: Slava <slava@meteor.com>
|
||||
245
History.md
245
History.md
@@ -1,7 +1,248 @@
|
||||
## vNEXT
|
||||
|
||||
* Write dates to Mongo as ISODate rather than Integer; existing data can be
|
||||
converted by passing it through `new Date()`. #1228
|
||||
* Fix `meteor run` with settings files containing non-ASCII characters. #1497
|
||||
|
||||
* Support `EJSON.clone` for `Meteor.Error`. As a result, they are properly
|
||||
stringified in DDP even if thrown through a `Future`. #1482
|
||||
|
||||
* Fail explicitly when publishing non-cursors.
|
||||
|
||||
* Implement `$each`, `$sort`, and `$slice` options for minimongo's `$push`
|
||||
modifier. #1492
|
||||
|
||||
* Increase the maximum size spiderable will return for a page from 200kB
|
||||
to 5MB.
|
||||
|
||||
* Upgraded dependencies:
|
||||
* SockJS server from 0.3.7 to 0.3.8
|
||||
|
||||
Patches contributed by GitHub user mcbain.
|
||||
|
||||
|
||||
## v0.6.6.2
|
||||
|
||||
* Upgrade Node from 0.10.20 to 0.10.21 (security update).
|
||||
|
||||
|
||||
## v0.6.6.1
|
||||
|
||||
* Fix file watching on OSX. Work around Node issue #6251 by not using
|
||||
fs.watch. #1483
|
||||
|
||||
|
||||
## v0.6.6
|
||||
|
||||
|
||||
#### Security
|
||||
|
||||
* Add `browser-policy` package for configuring and sending
|
||||
Content-Security-Policy and X-Frame-Options HTTP headers.
|
||||
[See the docs](http://docs.meteor.com/#browserpolicy) for more.
|
||||
|
||||
* Use cryptographically strong pseudorandom number generators when available.
|
||||
|
||||
#### MongoDB
|
||||
|
||||
* Add upsert support. `Collection.update` now supports the `{upsert:
|
||||
true}` option. Additionally, add a `Collection.upsert` method which
|
||||
returns the newly inserted object id if applicable.
|
||||
|
||||
* `update` and `remove` now return the number of documents affected. #1046
|
||||
|
||||
* `$near` operator for `2d` and `2dsphere` indices.
|
||||
|
||||
* The `fields` option to the collection methods `find` and `findOne` now works
|
||||
on the client as well. (Operators such as `$elemMatch` and `$` are not yet
|
||||
supported in `fields` projections.) #1287
|
||||
|
||||
* Pass an index and the cursor itself to the callbacks in `cursor.forEach` and
|
||||
`cursor.map`, just like the corresponding `Array` methods. #63
|
||||
|
||||
* Support `c.find(query, {limit: N}).count()` on the client. #654
|
||||
|
||||
* Improve behavior of `$ne`, `$nin`, and `$not` selectors with objects containing
|
||||
arrays. #1451
|
||||
|
||||
* Fix various bugs if you had two documents with the same _id field in
|
||||
String and ObjectID form.
|
||||
|
||||
#### Accounts
|
||||
|
||||
* [Behavior Change] Expire login tokens periodically. Defaults to 90
|
||||
days. Use `Accounts.config({loginExpirationInDays: null})` to disable
|
||||
token expiration.
|
||||
|
||||
* [Behavior Change] Write dates generated by Meteor Accounts to Mongo as
|
||||
Date instead of number; existing data can be converted by passing it
|
||||
through `new Date()`. #1228
|
||||
|
||||
* Log out and close connections for users if they are deleted from the
|
||||
database.
|
||||
|
||||
* Add Meteor.logoutOtherClients() for logging out other connections
|
||||
logged in as the current user.
|
||||
|
||||
* `restrictCreationByEmailDomain` option in `Accounts.config` to restrict new
|
||||
users to emails of specific domain (eg. only users with @meteor.com emails) or
|
||||
a custom validator. #1332
|
||||
|
||||
* Support OAuth1 services that require request token secrets as well as
|
||||
authentication token secrets. #1253
|
||||
|
||||
* Warn if `Accounts.config` is only called on the client. #828
|
||||
|
||||
* Fix bug where callbacks to login functions could be called multiple
|
||||
times when the client reconnects.
|
||||
|
||||
#### DDP
|
||||
|
||||
* Fix infinite loop if a client disconnects while a long yielding method is
|
||||
running.
|
||||
|
||||
* Unfinished code to support DDP session resumption has been removed. Meteor
|
||||
servers now stop processing messages from clients and reclaim memory
|
||||
associated with them as soon as they are disconnected instead of a few minutes
|
||||
later.
|
||||
|
||||
#### Tools
|
||||
|
||||
* The pre-0.6.5 `Package.register_extension` API has been removed. Use
|
||||
`Package._transitional_registerBuildPlugin` instead, which was introduced in
|
||||
0.6.5. (A bug prevented the 0.6.5 reimplementation of `register_extension`
|
||||
from working properly anyway.)
|
||||
|
||||
* Support using an HTTP proxy in the `meteor` command line tool. This
|
||||
allows the `update`, `deploy`, `logs`, and `mongo` commands to work
|
||||
behind a proxy. Use the standard `http_proxy` environment variable to
|
||||
specify your proxy endpoint. #429, #689, #1338
|
||||
|
||||
* Build Linux binaries on an older Linux machine. Meteor now supports
|
||||
running on Linux machines with glibc 2.9 or newer (Ubuntu 10.04+, RHEL
|
||||
and CentOS 6+, Fedora 10+, Debian 6+). Improve error message when running
|
||||
on Linux with unsupported glibc, and include Mongo stderr if it fails
|
||||
to start.
|
||||
|
||||
* Install NPM modules with `--force` to avoid corrupted local caches.
|
||||
|
||||
* Rebuild NPM modules in packages when upgrading to a version of Meteor that
|
||||
uses a different version of Node.
|
||||
|
||||
* Disable the Mongo http interface. This lets you run meteor on two ports
|
||||
differing by 1000 at the same time.
|
||||
|
||||
#### Misc
|
||||
|
||||
* [Known issue] Breaks support for pre-release OSX 10.9 'Mavericks'.
|
||||
Will be addressed shortly. See issues:
|
||||
https://github.com/joyent/node/issues/6251
|
||||
https://github.com/joyent/node/issues/6296
|
||||
|
||||
* `EJSON.stringify` now takes options:
|
||||
- `canonical` causes objects keys to be stringified in sorted order
|
||||
- `indent` allows formatting control over the EJSON stringification
|
||||
|
||||
* EJSON now supports `Infinity`, `-Infinity` and `NaN`.
|
||||
|
||||
* Check that the argument to `EJSON.parse` is a string. #1401
|
||||
|
||||
* Better error from functions that use `Meteor._wrapAsync` (eg collection write
|
||||
methods and `HTTP` methods) and in DDP server message processing. #1387
|
||||
|
||||
* Support `appcache` on Chrome for iOS.
|
||||
|
||||
* Support literate CoffeeScript files with the extension `.coffee.md` (in
|
||||
addition to the already-supported `.litcoffee` extension). #1407
|
||||
|
||||
* Make `madewith` package work again (broken in 0.6.5). #1448
|
||||
|
||||
* Better error when passing a string to `{{#each}}`. #722
|
||||
|
||||
* Add support for JSESSIONID cookies for sticky sessions. Set the
|
||||
`USE_JSESSIONID` environment variable to enable placing a JSESSIONID
|
||||
cookie on sockjs requests.
|
||||
|
||||
* Simplify the static analysis used to detect package-scope variables.
|
||||
|
||||
* Upgraded dependencies:
|
||||
* Node from 0.8.24 to 0.10.20
|
||||
* MongoDB from 2.4.4 to 2.4.6
|
||||
* MongoDB driver from 1.3.17 to 1.3.19
|
||||
* http-proxy from 0.10.1 to a pre-release of 1.0.0
|
||||
* stylus from 0.30.1 to 0.37.0
|
||||
* nib from 0.8.2 to 1.0.0
|
||||
* optimist from 0.3.5 to 0.6.0
|
||||
* semver from 1.1.0 to 2.1.0
|
||||
* request from 2.12.0 to 2.27.0
|
||||
* keypress from 0.1.0 to 0.2.1
|
||||
* underscore from 1.5.1 to 1.5.2
|
||||
* fstream from 0.1.21 to 0.1.24
|
||||
* tar from 0.1.14 to 0.1.18
|
||||
* source-map from 0.1.26 to 0.1.30
|
||||
* source-map-support from a fork of 0.1.8 to 0.2.3
|
||||
* escope from a fork of 0.0.15 to 1.0.0
|
||||
* estraverse from 1.1.2-1 to 1.3.1
|
||||
* simplesmtp from 0.1.25 to 0.3.10
|
||||
* stream-buffers from 0.2.3 to 0.2.5
|
||||
* websocket from 1.0.7 to 1.0.8
|
||||
* cli-color from 0.2.2 to 0.2.3
|
||||
* clean-css from 1.0.11 to 1.1.2
|
||||
* UglifyJS2 from a fork of 2.3.6 to a different fork of 2.4.0
|
||||
* connect from 2.7.10 to 2.9.0
|
||||
* send from 0.1.0 to 0.1.4
|
||||
* useragent from 2.0.1 to 2.0.7
|
||||
* replaced byline with eachline 2.3.3
|
||||
|
||||
Patches contributed by GitHub users ansman, awwx, codeinthehole, jacott,
|
||||
Maxhodges, meawoppl, mitar, mizzao, mquandalle, nathan-muir, RobertLowe, ryw,
|
||||
sdarnell, and timhaines.
|
||||
|
||||
|
||||
## v0.6.5.1
|
||||
|
||||
* Fix syntax errors on lines that end with a backslash. #1326
|
||||
|
||||
* Fix serving static files with special characters in their name. #1339
|
||||
|
||||
* Upgrade `esprima` JavaScript parser to fix bug parsing complex regexps.
|
||||
|
||||
* Export `Spiderable` from `spiderable` package to allow users to set
|
||||
`Spiderable.userAgentRegExps` to control what user agents are treated
|
||||
as spiders.
|
||||
|
||||
* Add EJSON to standard-app-packages. #1343
|
||||
|
||||
* Fix bug in d3 tab character parsing.
|
||||
|
||||
* Fix regression when using Mongo ObjectIDs in Spark templates.
|
||||
|
||||
|
||||
* Increase the maximum size spiderable will return for a page from 200kB
|
||||
to 5MB.
|
||||
|
||||
|
||||
## v0.6.5.2
|
||||
|
||||
* Upgrade Node from 0.8.24 to 0.8.26 (security patch)
|
||||
|
||||
|
||||
## v0.6.5.1
|
||||
|
||||
* Fix syntax errors on lines that end with a backslash. #1326
|
||||
|
||||
* Fix serving static files with special characters in their name. #1339
|
||||
|
||||
* Upgrade `esprima` JavaScript parser to fix bug parsing complex regexps.
|
||||
|
||||
* Export `Spiderable` from `spiderable` package to allow users to set
|
||||
`Spiderable.userAgentRegExps` to control what user agents are treated
|
||||
as spiders.
|
||||
|
||||
* Add EJSON to standard-app-packages. #1343
|
||||
|
||||
* Fix bug in d3 tab character parsing.
|
||||
|
||||
* Fix regression when using Mongo ObjectIDs in Spark templates.
|
||||
|
||||
|
||||
* Increase the maximum size spiderable will return for a page from 200kB
|
||||
to 5MB.
|
||||
|
||||
63
LICENSE.txt
63
LICENSE.txt
@@ -90,6 +90,7 @@ github-url-from-git: https://github.com/visionmedia/node-github-url-from-git
|
||||
pause: https://github.com/visionmedia/node-pause
|
||||
range-parser: https://github.com/visionmedia/node-range-parser
|
||||
send: https://github.com/visionmedia/send
|
||||
methods: https://github.com/visionmedia/node-methods
|
||||
----------
|
||||
|
||||
Copyright (c) 2010 TJ Holowaychuk <tj@vision-media.ca>
|
||||
@@ -191,13 +192,6 @@ rbytes: https://github.com/akdubya/rbytes
|
||||
Copyright (c) 2010 Aleksander Williams
|
||||
|
||||
|
||||
----------
|
||||
formidable: https://github.com/felixge/node-formidable
|
||||
----------
|
||||
|
||||
By Felix Geisendörfer and Tim Koschuetzki, Debuggable, Ltd.
|
||||
|
||||
|
||||
----------
|
||||
colors: https://github.com/Marak/colors.js
|
||||
----------
|
||||
@@ -245,9 +239,11 @@ Copyright (c) 2012 Nathan Rajlich <nathan@tootallnate.net>
|
||||
|
||||
----------
|
||||
faye-websocket: https://github.com/faye/faye-websocket-node
|
||||
websocket-driver: https://github.com/faye/websocket-driver-node
|
||||
----------
|
||||
|
||||
Copyright (c) 2009-2012 James Coglan
|
||||
Copyright (c) 2009-2013 James Coglan
|
||||
Copyright (c) 2010-2013 James Coglan
|
||||
|
||||
|
||||
----------
|
||||
@@ -291,6 +287,7 @@ archy: https://github.com/substack/node-archy
|
||||
shell-quote: https://github.com/substack/node-shell-quote
|
||||
deep-equal: https://github.com/substack/node-deep-equal
|
||||
editor: https://github.com/substack/node-editor
|
||||
minimist: https://github.com/substack/node-minimist
|
||||
----------
|
||||
|
||||
Copyright 2010, 2011, 2012, 2013 James Halliday (mail@substack.net)
|
||||
@@ -328,6 +325,7 @@ Felix Geisendörfer (felix@debuggable.com)
|
||||
|
||||
----------
|
||||
form-data: https://github.com/felixge/node-form-data
|
||||
multiparty: https://github.com/superjoe30/node-multiparty
|
||||
----------
|
||||
|
||||
Copyright (c) 2012 Felix Geisendörfer (felix@debuggable.com) and contributors
|
||||
@@ -377,13 +375,6 @@ buffer-crc32: https://github.com/brianloveswords/buffer-crc32
|
||||
Copyright (c) 2013 Brian J. Brennan
|
||||
|
||||
|
||||
----------
|
||||
byline: https://github.com/jahewson/node-byline
|
||||
----------
|
||||
|
||||
node-byline (C) 2011-2013 John Hewson
|
||||
|
||||
|
||||
----------
|
||||
child-process-close: https://github.com/piscisaureus/child-process-close
|
||||
----------
|
||||
@@ -475,6 +466,8 @@ Copyright (c) 2011 SDA Software Associates Inc.
|
||||
|
||||
----------
|
||||
sha: https://github.com/ForbesLindesay/sha
|
||||
type-of: https://github.com/ForbesLindesay/type-of
|
||||
uglify-to-browserify: https://github.com/ForbesLindesay/uglify-to-browserify
|
||||
----------
|
||||
|
||||
Copyright (c) 2013 Forbes Lindesay
|
||||
@@ -516,6 +509,38 @@ Copyright 2011, Robert Mustacchi. All rights reserved.
|
||||
Copyright 2011, Joyent, Inc. All rights reserved.
|
||||
|
||||
|
||||
----------
|
||||
eachline: https://github.com/williamwicks/node-eachline
|
||||
----------
|
||||
|
||||
Copyright (c) 2013 William Wicks
|
||||
|
||||
|
||||
----------
|
||||
eventemitter2: https://github.com/hij1nx/EventEmitter2
|
||||
----------
|
||||
|
||||
Copyright (c) 2011 hij1nx http://www.twitter.com/hij1nx
|
||||
|
||||
|
||||
----------
|
||||
stream-counter: https://github.com/superjoe30/node-stream-counter
|
||||
----------
|
||||
|
||||
Copyright (c) 2013 Andrew Kelley
|
||||
|
||||
----------
|
||||
uid2: https://github.com/coreh/uid2
|
||||
----------
|
||||
|
||||
Copyright (c) 2013 Marco Aurelio
|
||||
|
||||
----------
|
||||
geojson-utils: https://github.com/maxogden/geojson-js-utils
|
||||
----------
|
||||
|
||||
Copyright (c) 2010 Max Ogden
|
||||
|
||||
==============
|
||||
Apache License
|
||||
==============
|
||||
@@ -531,7 +556,8 @@ Unless required by applicable law or agreed to in writing, software distributed
|
||||
"""
|
||||
|
||||
----------
|
||||
mongo-drivers: https://github.com/mongodb/node-mongodb-native
|
||||
mongodb: (Node driver) https://github.com/mongodb/node-mongodb-native
|
||||
kerberos: https://github.com/christkv/kerberos
|
||||
----------
|
||||
|
||||
Copyright 2009 - 2010 Christian Amor Kvalheim.
|
||||
@@ -1309,7 +1335,7 @@ Other
|
||||
=====
|
||||
|
||||
----------
|
||||
mimelib: https://github.com/andris9/mimelib
|
||||
mimelib-noiconv: https://github.com/andris9/mimelib
|
||||
mailcomposer: https://github.com/andris9/mailcomposer
|
||||
simplesmtp: https://github.com/andris9/simplesmtp
|
||||
rai: https://github.com/andris9/rai
|
||||
@@ -1407,7 +1433,7 @@ For more information, please refer to <http://unlicense.org/>
|
||||
|
||||
|
||||
----------
|
||||
mongodb: http://www.mongodb.org/
|
||||
MongoDB: http://www.mongodb.org/
|
||||
----------
|
||||
|
||||
LICENSE
|
||||
@@ -1711,6 +1737,7 @@ The externally maintained libraries used by libuv are:
|
||||
|
||||
----------
|
||||
nodejs: http://nodejs.org/
|
||||
readable-stream: https://github.com/isaacs/readable-stream/
|
||||
----------
|
||||
|
||||
|
||||
|
||||
@@ -56,8 +56,8 @@ From your checkout, you can read the docs locally. The `/docs` directory is a
|
||||
meteor application, so simply change into the `/docs` directory and launch
|
||||
the app:
|
||||
|
||||
cd docs/
|
||||
meteor
|
||||
cd docs/
|
||||
../meteor
|
||||
|
||||
You'll then be able to read the docs locally in your browser at
|
||||
`http://localhost:3000/`
|
||||
|
||||
@@ -1 +1 @@
|
||||
galaxy-appconfig-2
|
||||
galaxy-follower-5
|
||||
|
||||
@@ -62,9 +62,9 @@ that Meteor will call each time a client subscribes to the name.
|
||||
|
||||
Publish functions can return a
|
||||
[`Collection.Cursor`](#meteor_collection_cursor), in which case Meteor
|
||||
will publish that cursor's documents. You can also return an array of
|
||||
`Collection.Cursor`s, in which case Meteor will publish all of the
|
||||
cursors.
|
||||
will publish that cursor's documents to each subscribed client. You can
|
||||
also return an array of `Collection.Cursor`s, in which case Meteor will
|
||||
publish all of the cursors.
|
||||
|
||||
{{#warning}}
|
||||
If you return multiple cursors in an array, they currently must all be from
|
||||
@@ -92,16 +92,15 @@ different collections. We hope to lift this restriction in a future release.
|
||||
];
|
||||
});
|
||||
|
||||
Otherwise, the publish function should call the functions
|
||||
[`added`](#publish_added) (when a new document is added to the published record
|
||||
set), [`changed`](#publish_changed) (when some fields on a document in the
|
||||
record set are changed or cleared), and [`removed`](#publish_removed) (when
|
||||
documents are removed from the published record set) to inform subscribers about
|
||||
documents. These methods are provided by `this` in your publish function.
|
||||
|
||||
|
||||
|
||||
<!-- TODO discuss ready -->
|
||||
Alternatively, a publish function can directly control its published
|
||||
record set by calling the functions [`added`](#publish_added) (to add a
|
||||
new document to the published record set), [`changed`](#publish_changed)
|
||||
(to change or clear some fields on a document already in the published
|
||||
record set), and [`removed`](#publish_removed) (to remove documents from
|
||||
the published record set). Publish functions that use these functions
|
||||
should also call [`ready`](#publish_ready) once the initial record set
|
||||
is complete. These methods are provided by `this` in your publish
|
||||
function.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -634,7 +633,7 @@ In this release, Minimongo has some limitations:
|
||||
of selectors.
|
||||
* `$` to denote the matched array position is not
|
||||
supported in modifier.
|
||||
* `findAndModify`, upsert, aggregate functions, and
|
||||
* `findAndModify`, aggregate functions, and
|
||||
map/reduce aren't supported.
|
||||
|
||||
All of these will be addressed in a future release. For full
|
||||
@@ -723,24 +722,27 @@ handlers and a browser's JavaScript console.
|
||||
`multi` to true, and can use an arbitrary [Mongo
|
||||
selector](#selectors) to find the documents to modify. It bypasses
|
||||
any access control rules set up by [`allow`](#allow) and
|
||||
[`deny`](#deny).
|
||||
[`deny`](#deny). The number of affected documents will be returned
|
||||
from the `update` call if you don't pass a callback.
|
||||
|
||||
- Untrusted code can only modify a single document at once, specified
|
||||
by its `_id`. The modification is allowed only after checking any
|
||||
applicable [`allow`](#allow) and [`deny`](#deny) rules.
|
||||
applicable [`allow`](#allow) and [`deny`](#deny) rules. The number
|
||||
of affected documents will be returned to the callback. Untrusted
|
||||
code cannot perform upserts, except in insecure mode.
|
||||
|
||||
On the server, if you don't provide a callback, then `update` blocks
|
||||
until the database acknowledges the write, or throws an exception if
|
||||
something went wrong. If you do provide a callback, `update` returns
|
||||
immediately. Once the update completes, the callback is called with a
|
||||
single error argument in the case of failure, or no arguments if the
|
||||
update was successful.
|
||||
single error argument in the case of failure, or a second argument
|
||||
indicating the number of affected documents if the update was successful.
|
||||
|
||||
On the client, `update` never blocks. If you do not provide a callback
|
||||
and the update fails on the server, then Meteor will log a warning to
|
||||
the console. If you provide a callback, Meteor will call that function
|
||||
with an error argument if there was an error, or no arguments if the
|
||||
update was successful.
|
||||
with an error argument if there was an error, or a second argument
|
||||
indicating the number of affected documents if the update was successful.
|
||||
|
||||
Client example:
|
||||
|
||||
@@ -766,9 +768,18 @@ Server example:
|
||||
}
|
||||
});
|
||||
|
||||
{{#warning}}
|
||||
The Mongo `upsert` feature is not implemented.
|
||||
{{/warning}}
|
||||
You can use `update` to perform a Mongo upsert by setting the `upsert`
|
||||
option to true. You can also use the [`upsert`](#upsert) method to perform an
|
||||
upsert that returns the _id of the document that was inserted (if there was one)
|
||||
in addition to the number of affected documents.
|
||||
|
||||
{{> api_box upsert}}
|
||||
|
||||
Modify documents that match `selector` according to `modifier`, or insert
|
||||
a document if no documents were modified. `upsert` is the same as calling
|
||||
`update` with the `upsert` option set to true, except that the return
|
||||
value of `upsert` is an object that contain the keys `numberAffected`
|
||||
and `insertedId`. (`update` returns only the number of affected documents.)
|
||||
|
||||
{{> api_box remove}}
|
||||
|
||||
@@ -784,7 +795,8 @@ handlers and a browser's JavaScript console.
|
||||
find the documents to remove, and can remove more than one document
|
||||
at once by passing a selector that matches multiple documents. It
|
||||
bypasses any access control rules set up by [`allow`](#allow) and
|
||||
[`deny`](#deny).
|
||||
[`deny`](#deny). The number of removed documents will be returned
|
||||
from `remove` if you don't pass a callback.
|
||||
|
||||
As a safety measure, if `selector` is omitted (or is `undefined`),
|
||||
no documents will be removed. Set `selector` to `{}` if you really
|
||||
@@ -792,20 +804,22 @@ handlers and a browser's JavaScript console.
|
||||
|
||||
- Untrusted code can only remove a single document at a time,
|
||||
specified by its `_id`. The document is removed only after checking
|
||||
any applicable [`allow`](#allow) and [`deny`](#deny) rules.
|
||||
any applicable [`allow`](#allow) and [`deny`](#deny) rules. The
|
||||
number of removed documents will be returned to the callback.
|
||||
|
||||
On the server, if you don't provide a callback, then `remove` blocks
|
||||
until the database acknowledges the write, or throws an exception if
|
||||
until the database acknowledges the write and then returns the number
|
||||
of removed documents, or throws an exception if
|
||||
something went wrong. If you do provide a callback, `remove` returns
|
||||
immediately. Once the remove completes, the callback is called with a
|
||||
single error argument in the case of failure, or no arguments if the
|
||||
remove was successful.
|
||||
single error argument in the case of failure, or a second argument
|
||||
indicating the number of removed documents if the remove was successful.
|
||||
|
||||
On the client, `remove` never blocks. If you do not provide a callback
|
||||
and the remove fails on the server, then Meteor will log a warning to
|
||||
the console. If you provide a callback, Meteor will call that function
|
||||
with an error argument if there was an error, or no arguments if the
|
||||
remove was successful.
|
||||
and the remove fails on the server, then Meteor will log a warning to the
|
||||
console. If you provide a callback, Meteor will call that function with an
|
||||
error argument if there was an error, or a second argument indicating the number
|
||||
of removed documents if the remove was successful.
|
||||
|
||||
Client example:
|
||||
|
||||
@@ -966,6 +980,8 @@ cursor, use [`forEach`](#foreach), [`map`](#map), or [`fetch`](#fetch).
|
||||
|
||||
{{> api_box cursor_foreach}}
|
||||
|
||||
This interface is compatible with [Array.forEach](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach).
|
||||
|
||||
When called from a reactive computation, `forEach` registers dependencies on
|
||||
the matching documents.
|
||||
|
||||
@@ -981,6 +997,8 @@ Examples:
|
||||
|
||||
{{> api_box cursor_map}}
|
||||
|
||||
This interface is compatible with [Array.map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map).
|
||||
|
||||
When called from a reactive computation, `map` registers dependencies on
|
||||
the matching documents.
|
||||
|
||||
@@ -1258,11 +1276,26 @@ is a dictionary whose keys are field names and whose values are `0`.
|
||||
// Users.find({}, {fields: {password: 0, hash: 0}})
|
||||
|
||||
To return an object that only includes the specified field, use `1` as
|
||||
the value. The `_id` field is still included in the result.
|
||||
the value. The `_id` field is still included in the result.
|
||||
|
||||
// Users.find({}, {fields: {firstname: 1, lastname: 1}})
|
||||
|
||||
It is not possible to mix inclusion and exclusion styles.
|
||||
It is not possible to mix inclusion and exclusion styles (except for the cases
|
||||
when `_id` is included by default or explicitly excluded). Field operators such
|
||||
as `$` and `$elemMatch` are not available on the client side yet.
|
||||
|
||||
More advanced example:
|
||||
|
||||
Users.insert({ alterEgos: [{ name: "Kira", alliance: "murderer" },
|
||||
{ name: "L", alliance: "police" }],
|
||||
name: "Yagami Light" });
|
||||
|
||||
Users.findOne({}, { fields: { 'alterEgos.name': 1, _id: 0 } });
|
||||
|
||||
// returns { alterEgos: [{ name: "Kira" }, { name: "L" }] }
|
||||
|
||||
See <a href="http://docs.mongodb.org/manual/tutorial/project-fields-from-query-results/#projection">
|
||||
the MongoDB docs</a> for details of the nested field rules and array behavior.
|
||||
|
||||
{{/api_box_inline}}
|
||||
|
||||
@@ -1482,6 +1515,12 @@ animation while the login request is being processed.
|
||||
|
||||
{{> api_box logout}}
|
||||
|
||||
{{> api_box logoutOtherClients}}
|
||||
|
||||
For example, when called in a user's browser, connections in that browser
|
||||
remain logged in, but any other browsers or DDP clients logged in as that user
|
||||
will be logged out.
|
||||
|
||||
{{> api_box loginWithPassword}}
|
||||
|
||||
This function is provided by the `accounts-password` package. See the
|
||||
@@ -2834,17 +2873,18 @@ these functions, add the HTTP package to your project with `$ meteor add http`.
|
||||
|
||||
{{> api_box httpcall}}
|
||||
|
||||
This function initiates an HTTP request to a remote server. It returns
|
||||
a result object with the contents of the HTTP response. The result
|
||||
object is detailed below.
|
||||
This function initiates an HTTP request to a remote server.
|
||||
|
||||
On the server, this function can be run either synchronously or
|
||||
asynchronously. If the callback is omitted, it runs synchronously,
|
||||
and the results are returned once the request completes. This is
|
||||
asynchronously. If the callback is omitted, it runs synchronously
|
||||
and the results are returned once the request completes successfully.
|
||||
If the request was not successful, an error is thrown.
|
||||
This is
|
||||
useful when making server-to-server HTTP API calls from within Meteor
|
||||
methods, as the method can succeed or fail based on the results of the
|
||||
synchronous HTTP call. In this case, consider using
|
||||
[`this.unblock()`](#method_unblock) to allow other methods to run in
|
||||
[`this.unblock()`](#method_unblock) to allow other methods on the same
|
||||
connection to run in
|
||||
the mean time. On the client, this function must be used
|
||||
asynchronously by passing a callback.
|
||||
|
||||
@@ -2864,7 +2904,8 @@ standard `x-www-form-urlencoded` content type, unless the `content`
|
||||
or `data` option is used to specify a body, in which case the
|
||||
parameters will be appended to the URL instead.
|
||||
|
||||
The callback receives two arguments, `error` and `result`. The
|
||||
When run in asynchronous mode, the callback receives two arguments,
|
||||
`error` and `result`. The
|
||||
`error` argument will contain an Error if the request fails in any
|
||||
way, including a network error, time-out, or an HTTP status code in
|
||||
the 400 or 500 range. In case of a 4xx/5xx HTTP status code, the
|
||||
@@ -2899,11 +2940,14 @@ Example server method:
|
||||
Meteor.methods({checkTwitter: function (userId) {
|
||||
check(userId, String);
|
||||
this.unblock();
|
||||
var result = HTTP.call("GET", "http://api.twitter.com/xyz",
|
||||
{params: {user: userId}});
|
||||
if (result.statusCode === 200)
|
||||
return true
|
||||
return false;
|
||||
try {
|
||||
var result = HTTP.call("GET", "http://api.twitter.com/xyz",
|
||||
{params: {user: userId}});
|
||||
return true;
|
||||
} catch (e) {
|
||||
// Got a network error, time-out or HTTP error in the 400 or 500 range.
|
||||
return false;
|
||||
}
|
||||
}});
|
||||
|
||||
Example asynchronous HTTP call:
|
||||
@@ -2911,7 +2955,7 @@ Example asynchronous HTTP call:
|
||||
HTTP.call("POST", "http://api.twitter.com/xyz",
|
||||
{data: {some: "json", stuff: 1}},
|
||||
function (error, result) {
|
||||
if (result.statusCode === 200) {
|
||||
if (!error) {
|
||||
Session.set("twizzled", true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -73,8 +73,7 @@ Template.api.release = {
|
||||
descr: ["`Meteor.release` is a string containing the name of the " +
|
||||
"[release](#meteorupdate) with which the project was built (for " +
|
||||
"example, `\"" +
|
||||
// Put the current release in the docs as the example)
|
||||
(Meteor.release ? Meteor.release : '0.6.0') +
|
||||
Meteor.release +
|
||||
"\"`). It is `undefined` if the project was built using a git " +
|
||||
"checkout of Meteor."]
|
||||
};
|
||||
@@ -89,9 +88,17 @@ Template.api.ejsonParse = {
|
||||
|
||||
Template.api.ejsonStringify = {
|
||||
id: "ejson_stringify",
|
||||
name: "EJSON.stringify(val)",
|
||||
name: "EJSON.stringify(val, [options])",
|
||||
locus: "Anywhere",
|
||||
args: [ {name: "val", type: "EJSON-compatible value", descr: "A value to stringify."} ],
|
||||
options: [
|
||||
{name: "indent",
|
||||
type: "Boolean, Integer, or String",
|
||||
descr: "Indents objects and arrays for easy readability. When `true`, indents by 2 spaces; when an integer, indents by that number of spaces; and when a string, uses the string as the indentation pattern."},
|
||||
{name: "canonical",
|
||||
type: "Boolean",
|
||||
descr: "When `true`, stringifies keys in an object in sorted order."}
|
||||
],
|
||||
descr: ["Serialize a value to a string.\n\nFor EJSON values, the serialization " +
|
||||
"fully represents the value. For non-EJSON values, serializes the " +
|
||||
"same way as `JSON.stringify`."]
|
||||
@@ -116,10 +123,15 @@ Template.api.ejsonToJSONValue = {
|
||||
|
||||
Template.api.ejsonEquals = {
|
||||
id: "ejson_equals",
|
||||
name: "EJSON.equals(a, b)", //doc options?
|
||||
name: "EJSON.equals(a, b, [options])",
|
||||
locus: "Anywhere",
|
||||
args: [ {name: "a", type: "EJSON-compatible object"},
|
||||
{name: "b", type: "EJSON-compatible object"} ],
|
||||
options: [
|
||||
{name: "keyOrderSensitive",
|
||||
type: "Boolean",
|
||||
descr: "Compare in key sensitive order, if supported by the JavaScript implementation. For example, `{a: 1, b: 2}` is equal to `{b: 2, a: 1}` only when `keyOrderSensitive` is `false`. The default is `false`."}
|
||||
],
|
||||
descr: ["Return true if `a` and `b` are equal to each other. Return false otherwise." +
|
||||
" Uses the `equals` method on `a` if present, otherwise performs a deep comparison."]
|
||||
},
|
||||
@@ -482,7 +494,7 @@ Template.api.meteor_collection = {
|
||||
options: [
|
||||
{name: "connection",
|
||||
type: "Object",
|
||||
descr: "The Meteor connection that will manage this collection. Uses the default connection if not specified. Pass `null` to specify no connection. Unmanaged (`name` is null) collections cannot specify a connection."
|
||||
descr: "The server connection that will manage this collection. Uses the default connection if not specified. Pass the return value of calling [`DDP.connect`](#ddp_connect) to specify a different server. Pass `null` to specify no connection. Unmanaged (`name` is null) collections cannot specify a connection."
|
||||
},
|
||||
{name: "idGeneration",
|
||||
type: "String",
|
||||
@@ -585,7 +597,7 @@ Template.api.update = {
|
||||
id: "update",
|
||||
name: "<em>collection</em>.update(selector, modifier, [options], [callback])",
|
||||
locus: "Anywhere",
|
||||
descr: ["Modify one or more documents in the collection"],
|
||||
descr: ["Modify one or more documents in the collection. Returns the number of affected documents."],
|
||||
args: [
|
||||
{name: "selector",
|
||||
type: "Mongo selector, or object id",
|
||||
@@ -597,7 +609,37 @@ Template.api.update = {
|
||||
descr: "Specifies how to modify the documents"},
|
||||
{name: "callback",
|
||||
type: "Function",
|
||||
descr: "Optional. If present, called with an error object as its argument."}
|
||||
descr: "Optional. If present, called with an error object as the first argument and, if no error, the number of affected documents as the second."}
|
||||
],
|
||||
options: [
|
||||
{name: "multi",
|
||||
type: "Boolean",
|
||||
descr: "True to modify all matching documents; false to only modify one of the matching documents (the default)."},
|
||||
{name: "upsert",
|
||||
type: "Boolean",
|
||||
descr: "True to insert a document if no matching documents are found."}
|
||||
]
|
||||
};
|
||||
|
||||
Template.api.upsert = {
|
||||
id: "upsert",
|
||||
name: "<em>collection</em>.upsert(selector, modifier, [options], [callback])",
|
||||
locus: "Anywhere",
|
||||
descr: ["Modify one or more documents in the collection, or insert one if no matching documents were found. " +
|
||||
"Returns an object with keys `numberAffected` (the number of documents modified) " +
|
||||
" and `insertedId` (the unique _id of the document that was inserted, if any)."],
|
||||
args: [
|
||||
{name: "selector",
|
||||
type: "Mongo selector, or object id",
|
||||
type_link: "selectors",
|
||||
descr: "Specifies which documents to modify"},
|
||||
{name: "modifier",
|
||||
type: "Mongo modifier",
|
||||
type_link: "modifiers",
|
||||
descr: "Specifies how to modify the documents"},
|
||||
{name: "callback",
|
||||
type: "Function",
|
||||
descr: "Optional. If present, called with an error object as the first argument and, if no error, the number of affected documents as the second."}
|
||||
],
|
||||
options: [
|
||||
{name: "multi",
|
||||
@@ -606,6 +648,7 @@ Template.api.update = {
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
Template.api.remove = {
|
||||
id: "remove",
|
||||
name: "<em>collection</em>.remove(selector, [callback])",
|
||||
@@ -675,25 +718,31 @@ Template.api.cursor_fetch = {
|
||||
|
||||
Template.api.cursor_foreach = {
|
||||
id: "foreach",
|
||||
name: "<em>cursor</em>.forEach(callback)",
|
||||
name: "<em>cursor</em>.forEach(callback, [thisArg])",
|
||||
locus: "Anywhere",
|
||||
descr: ["Call `callback` once for each matching document, sequentially and synchronously."],
|
||||
args: [
|
||||
{name: "callback",
|
||||
type: "Function",
|
||||
descr: "Function to call."}
|
||||
descr: "Function to call. It will be called with three arguments: the document, a 0-based index, and <em>cursor</em> itself."},
|
||||
{name: "thisArg",
|
||||
type: "Any",
|
||||
descr: "An object which will be the value of `this` inside `callback`."}
|
||||
]
|
||||
};
|
||||
|
||||
Template.api.cursor_map = {
|
||||
id: "map",
|
||||
name: "<em>cursor</em>.map(callback)",
|
||||
name: "<em>cursor</em>.map(callback, [thisArg])",
|
||||
locus: "Anywhere",
|
||||
descr: ["Map callback over all matching documents. Returns an Array."],
|
||||
args: [
|
||||
{name: "callback",
|
||||
type: "Function",
|
||||
descr: "Function to call."}
|
||||
descr: "Function to call. It will be called with three arguments: the document, a 0-based index, and <em>cursor</em> itself."},
|
||||
{name: "thisArg",
|
||||
type: "Any",
|
||||
descr: "An object which will be the value of `this` inside `callback`."}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1026,6 +1075,20 @@ Template.api.logout = {
|
||||
]
|
||||
};
|
||||
|
||||
Template.api.logoutOtherClients = {
|
||||
id: "meteor_logoutotherclients",
|
||||
name: "Meteor.logoutOtherClients([callback])",
|
||||
locus: "Client",
|
||||
descr: ["Log out other clients logged in as the current user, but does not log out the client that calls this function."],
|
||||
args: [
|
||||
{
|
||||
name: "callback",
|
||||
type: "Function",
|
||||
descr: "Optional callback. Called with no arguments on success, or with a single `Error` argument on failure."
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
Template.api.loginWithPassword = {
|
||||
id: "meteor_loginwithpassword",
|
||||
@@ -1100,6 +1163,16 @@ Template.api.accounts_config = {
|
||||
name: "forbidClientAccountCreation",
|
||||
type: "Boolean",
|
||||
descr: "Calls to [`createUser`](#accounts_createuser) from the client will be rejected. In addition, if you are using [accounts-ui](#accountsui), the \"Create account\" link will not be available."
|
||||
},
|
||||
{
|
||||
name: "restrictCreationByEmailDomain",
|
||||
type: "String Or Function",
|
||||
descr: "If set, only allow new users with an email in the specified domain or if the predicate function returns true. Works with password-based sign-in and external services that expose email addresses (Google, Facebook, GitHub). All existing users still can log in after enabling this option. Example: `Accounts.config({ restrictCreationByEmailDomain: 'school.edu' })`."
|
||||
},
|
||||
{
|
||||
name: "loginExpirationInDays",
|
||||
type: "Number",
|
||||
descr: "The number of days from when a user logs in until their token expires and they are logged out. Defaults to 90. Set to `null` to disable login expiration."
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -824,9 +824,9 @@ To get started, run
|
||||
$ meteor bundle myapp.tgz
|
||||
|
||||
This command will generate a fully-contained Node.js application in the form of
|
||||
a tarball. To run this application, you need to provide Node.js 0.8 and a
|
||||
a tarball. To run this application, you need to provide Node.js 0.10 and a
|
||||
MongoDB server. (The current release of Meteor has been tested with Node
|
||||
0.8.26.) You can then run the application by invoking node, specifying the HTTP
|
||||
0.10.21.) You can then run the application by invoking node, specifying the HTTP
|
||||
port for the application to listen on, and the MongoDB endpoint. If you don't
|
||||
already have a MongoDB server, we can recommend our friends at
|
||||
[MongoHQ](http://mongohq.com).
|
||||
@@ -913,8 +913,7 @@ quick tips:
|
||||
an example. Build plugins are fully-fledged Meteor programs in their
|
||||
own right and have their own namespace, package dependencies, source
|
||||
files and npm requirements. The old `register_extension` API is
|
||||
deprecated and should not be used as it will prevent your package
|
||||
from being cached, slowing down builds.
|
||||
removed.
|
||||
|
||||
* It is possible to create weak dependencies between packages. If
|
||||
package A has a weak dependency on package B, it means that
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
Template.headline.release = function () {
|
||||
// XXX This is commented out because for now galaxy apps have to be on a
|
||||
// different Meteor release that has a bug fix.
|
||||
return "0.6.5";
|
||||
// return Meteor.release || "(checkout)";
|
||||
return Meteor.release || "(checkout)";
|
||||
};
|
||||
|
||||
|
||||
@@ -155,6 +152,7 @@ var toc = [
|
||||
{instance: "collection", name: "findOne"},
|
||||
{instance: "collection", name: "insert"},
|
||||
{instance: "collection", name: "update"},
|
||||
{instance: "collection", name: "upsert"},
|
||||
{instance: "collection", name: "remove"},
|
||||
{instance: "collection", name: "allow"},
|
||||
{instance: "collection", name: "deny"}
|
||||
@@ -191,6 +189,7 @@ var toc = [
|
||||
"Meteor.users",
|
||||
"Meteor.loggingIn",
|
||||
"Meteor.logout",
|
||||
"Meteor.logoutOtherClients",
|
||||
"Meteor.loginWithPassword",
|
||||
{name: "Meteor.loginWithFacebook", id: "meteor_loginwithexternalservice"},
|
||||
{name: "Meteor.loginWithGithub", id: "meteor_loginwithexternalservice"},
|
||||
@@ -335,6 +334,7 @@ var toc = [
|
||||
"audit-argument-checks",
|
||||
"backbone",
|
||||
"bootstrap",
|
||||
"browser-policy",
|
||||
"coffeescript",
|
||||
"d3",
|
||||
"force-ssl",
|
||||
|
||||
@@ -22,6 +22,7 @@ and removed with:
|
||||
{{> pkg_audit_argument_checks}}
|
||||
{{> pkg_backbone}}
|
||||
{{> pkg_bootstrap}}
|
||||
{{> pkg_browser_policy}}
|
||||
{{> pkg_coffeescript}}
|
||||
{{> pkg_d3}}
|
||||
{{> pkg_force_ssl}}
|
||||
|
||||
@@ -44,7 +44,8 @@ You can also disable the application cache for specific browsers:
|
||||
});
|
||||
|
||||
The supported browsers that can be enabled or disabled are `android`,
|
||||
`chrome`, `chromium`, `firefox`, `ie`, `mobileSafari` and `safari`.
|
||||
`chrome`, `chromium`, `chromeMobileIOS`, `firefox`, `ie`,
|
||||
`mobileSafari` and `safari`.
|
||||
|
||||
Browsers limit the amount of data they will put in the application
|
||||
cache, which can vary due to factors such as how much disk space is
|
||||
|
||||
165
docs/client/packages/browser-policy.html
Normal file
165
docs/client/packages/browser-policy.html
Normal file
@@ -0,0 +1,165 @@
|
||||
<template name="pkg_browser_policy">
|
||||
{{#better_markdown}}
|
||||
## `browser-policy`
|
||||
|
||||
The `browser-policy` package lets you set security-related policies that will be
|
||||
enforced by newer browsers. These policies help you prevent and mitigate common
|
||||
attacks like cross-site scripting and clickjacking.
|
||||
|
||||
When you add `browser-policy` to your app, you get default configurations for
|
||||
the HTTP headers X-Frame-Options and Content-Security-Policy. X-Frame-Options
|
||||
tells the browser which websites are allowed to frame your app. You should only
|
||||
let trusted websites frame your app, because malicious sites could harm your
|
||||
users with <a href="https://www.owasp.org/index.php/Clickjacking">clickjacking
|
||||
attacks</a>.
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Security/CSP/Introducing_Content_Security_Policy">Content-Security-Policy</a>
|
||||
tells the browser where your app can load content from, which encourages safe
|
||||
practices and mitigates the damage of a cross-site-scripting attack.
|
||||
`browser-policy` also provides functions for you to configure these policies if
|
||||
the defaults are not suitable.
|
||||
|
||||
If you only want to use Content-Security-Policy or X-Frame-Options but not both,
|
||||
you can add the individual packages `browser-policy-content` or
|
||||
`browser-policy-framing` instead of `browser-policy`.
|
||||
|
||||
For most apps, we recommend that you take the following steps:
|
||||
|
||||
* Add `browser-policy` to your app to enable a starter policy. With this starter
|
||||
policy, your app's client code will be able to load content (images, scripts,
|
||||
fonts, etc.) only from its own origin, except that XMLHttpRequests and WebSocket
|
||||
connections can go to any origin. Further, your app's client code will not be
|
||||
able to use functions such as `eval()` that convert strings to code. Users'
|
||||
browsers will only let your app be framed by web pages on the same origin as
|
||||
your app.
|
||||
* You can use the functions described below to customize the policies. If your
|
||||
app does not need any inline Javascript such as inline `<script>` tags, we
|
||||
recommend that you modify the policy by calling
|
||||
`BrowserPolicy.content.disallowInlineScripts()` in server code. This will result
|
||||
in one extra round trip when your app is loaded, but will help prevent
|
||||
cross-site scripting attacks by disabling all scripts except those loaded from a
|
||||
`script src` attribute.
|
||||
|
||||
Meteor determines the browser policy when the server starts up, so you should
|
||||
call `BrowserPolicy` functions in top-level application code or in
|
||||
`Meteor.startup`.
|
||||
|
||||
#### Frame options
|
||||
|
||||
By default, if you add `browser-policy` or `browser-policy-framing`, only web
|
||||
pages on the same origin as your app are allowed to frame your app. You can use
|
||||
the following functions to modify this policy.
|
||||
<dl class="callbacks">
|
||||
{{#dtdd "BrowserPolicy.framing.disallow()"}}
|
||||
Your app will never render inside a frame or iframe.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.framing.restrictToOrigin(origin)"}}
|
||||
Your app will only render inside frames loaded by `origin`. You can only call
|
||||
this function once with a single origin, and cannot use wildcards or specify
|
||||
multiple origins that are allowed to frame your app. (This is a limitation of
|
||||
the X-Frame-Options header.) Example values of `origin` include
|
||||
"http://example.com" and "https://foo.example.com".
|
||||
{{#warning}}
|
||||
This value of the X-Frame-Options header is not yet supported in Chrome or
|
||||
Safari and will be ignored in those browsers.
|
||||
{{/warning}}
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.framing.allowAll()"}}
|
||||
This unsets the X-Frame-Options header, so that your app can be framed by
|
||||
any webpage.
|
||||
{{/dtdd}}
|
||||
</dl>
|
||||
|
||||
#### Content options
|
||||
|
||||
You can use the functions in this section to control how different types of
|
||||
content can be loaded on your site.
|
||||
|
||||
You can use the following functions to adjust policies on where Javascript and
|
||||
CSS can be run:
|
||||
|
||||
<dl class="callbacks">
|
||||
{{#dtdd "BrowserPolicy.content.allowInlineScripts()"}}
|
||||
Allows inline `<script>` tags, `javascript:` URLs, and inline event handlers.
|
||||
The default policy already allows inline scripts.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.content.disallowInlineScripts()"}}
|
||||
Disallows inline Javascript. Calling this function results in an extra
|
||||
round-trip on page load to retrieve Meteor runtime configuration that is usually
|
||||
part of an inline script tag.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.content.allowEval()"}}
|
||||
Allows the creation of Javascript code from strings using function such as `eval()`.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.content.disallowEval()"}}
|
||||
Disallows eval and related functions. The default policy already disallows eval.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.content.allowInlineStyles()"}}
|
||||
Allows inline style tags and style attributes. The default policy already allows
|
||||
inline styles.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.content.disallowInlineStyles()"}}
|
||||
Disallows inline CSS.
|
||||
{{/dtdd}}
|
||||
</dl>
|
||||
|
||||
Finally, you can configure a whitelist of allowed requests that various types of
|
||||
content can make. The following functions are defined for the content types
|
||||
script, object, image, media, font, and connect.
|
||||
|
||||
<dl class="callbacks">
|
||||
{{#dtdd "BrowserPolicy.content.allow<ContentType>Origin(origin)"}}
|
||||
Allows this type of content to be loaded from the given origin. `origin` is a
|
||||
string and can include an optional scheme (such as `http` or `https`), an
|
||||
optional wildcard at the beginning, and an optional port which can be a
|
||||
wildcard. Examples include `example.com`, `https://*.example.com`, and
|
||||
`example.com:*`. You can call these functions multiple times with different
|
||||
origins to specify a whitelist of allowed origins.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.content.allow<ContentType>DataUrl()"}}
|
||||
Allows this type of content to be loaded from a `data:` URL.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.content.allow<ContentType>SameOrigin()"}}
|
||||
Allows this type of content to be loaded from the same origin as your app.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.content.disallow<ContentType>()"}}
|
||||
Disallows this type of content on your app.
|
||||
{{/dtdd}}
|
||||
</dl>
|
||||
|
||||
You can also set policies for all these types of content at once, using these
|
||||
functions:
|
||||
|
||||
* `BrowserPolicy.content.allowSameOriginForAll()`,
|
||||
* `BrowserPolicy.content.allowDataUrlForAll()`,
|
||||
* `BrowserPolicy.content.allowOriginForAll(origin)`
|
||||
* `BrowserPolicy.content.disallowAll()`
|
||||
|
||||
For example, if you want to allow the
|
||||
origin `https://foo.com` for all types of content but you want to disable
|
||||
`<object>` tags, you can call
|
||||
`BrowserPolicy.content.allowOriginForAll("https://foo.com")` followed by
|
||||
`BrowserPolicy.content.disallowObject()`.
|
||||
|
||||
Other examples of using the `BrowserPolicy.content` API:
|
||||
|
||||
* `BrowserPolicy.content.disallowFont()` causes the browser to disallow all
|
||||
`<font>` tags.
|
||||
* `BrowserPolicy.content.allowImageOrigin("https://example.com")`
|
||||
allows images to have their `src` attributes point to images served from
|
||||
`https://example.com`.
|
||||
* `BrowserPolicy.content.allowConnectOrigin("https://example.com")` allows XMLHttpRequest
|
||||
and WebSocket connections to `https://example.com`.
|
||||
|
||||
|
||||
{{/better_markdown}}
|
||||
</template>
|
||||
@@ -3,8 +3,11 @@
|
||||
## `random`
|
||||
|
||||
The `random` package provides several functions for generating random
|
||||
numbers. It uses a Meteor-provided random number generator that does not depend
|
||||
on the browser's facilities.
|
||||
numbers. It uses a cryptographically strong pseudorandom number generator when
|
||||
possible, but falls back to a weaker random number generator when
|
||||
cryptographically strong randomness is not available (on older browsers or on
|
||||
servers that don't have enough entropy to seed the cryptographically strong
|
||||
generator).
|
||||
|
||||
<dl class="callbacks">
|
||||
{{#dtdd "Random.id()"}}
|
||||
@@ -25,10 +28,5 @@ Returns a random string of `n` hexadecimal digits.
|
||||
{{/dtdd}}
|
||||
</dl>
|
||||
|
||||
{{#note}}
|
||||
In the current implementation, random values do not come from a
|
||||
cryptographically strong pseudorandom number generator. Future releases will
|
||||
improve this, particularly on the server.
|
||||
{{/note}}
|
||||
{{/better_markdown}}
|
||||
</template>
|
||||
|
||||
5
docs/lib/release-override.js
Normal file
5
docs/lib/release-override.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// While galaxy apps are on their own special meteor releases, override
|
||||
// Meteor.release here.
|
||||
if (Meteor.isClient) {
|
||||
Meteor.release = Meteor.release ? "0.6.6.2" : undefined;
|
||||
}
|
||||
@@ -7,4 +7,3 @@ standard-app-packages
|
||||
autopublish
|
||||
insecure
|
||||
preserve-inputs
|
||||
random
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.6.5
|
||||
0.6.6.2
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.6.5
|
||||
0.6.6.2
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
Meteor.subscribe("directory");
|
||||
Meteor.subscribe("parties");
|
||||
|
||||
// If no party selected, select one.
|
||||
// If no party selected, or if the selected party was deleted, select one.
|
||||
Meteor.startup(function () {
|
||||
Deps.autorun(function () {
|
||||
if (! Session.get("selected")) {
|
||||
var selected = Session.get("selected");
|
||||
if (! selected || ! Parties.findOne(selected)) {
|
||||
var party = Parties.findOne();
|
||||
if (party)
|
||||
Session.set("selected", party._id);
|
||||
else
|
||||
Session.set("selected", null);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -213,19 +216,17 @@ Template.createDialog.events({
|
||||
var coords = Session.get("createCoords");
|
||||
|
||||
if (title.length && description.length) {
|
||||
Meteor.call('createParty', {
|
||||
var id = createParty({
|
||||
title: title,
|
||||
description: description,
|
||||
x: coords.x,
|
||||
y: coords.y,
|
||||
public: public
|
||||
}, function (error, party) {
|
||||
if (! error) {
|
||||
Session.set("selected", party);
|
||||
if (! public && Meteor.users.find().count() > 1)
|
||||
openInviteDialog();
|
||||
}
|
||||
});
|
||||
|
||||
Session.set("selected", id);
|
||||
if (! public && Meteor.users.find().count() > 1)
|
||||
openInviteDialog();
|
||||
Session.set("showCreateDialog", false);
|
||||
} else {
|
||||
Session.set("createError",
|
||||
|
||||
@@ -52,6 +52,12 @@ var Coordinate = Match.Where(function (x) {
|
||||
return x >= 0 && x <= 1;
|
||||
});
|
||||
|
||||
createParty = function (options) {
|
||||
var id = Random.id();
|
||||
Meteor.call('createParty', _.extend({ _id: id }, options));
|
||||
return id;
|
||||
};
|
||||
|
||||
Meteor.methods({
|
||||
// options should include: title, description, x, y, public
|
||||
createParty: function (options) {
|
||||
@@ -60,7 +66,8 @@ Meteor.methods({
|
||||
description: NonEmptyString,
|
||||
x: Coordinate,
|
||||
y: Coordinate,
|
||||
public: Match.Optional(Boolean)
|
||||
public: Match.Optional(Boolean),
|
||||
_id: Match.Optional(NonEmptyString)
|
||||
});
|
||||
|
||||
if (options.title.length > 100)
|
||||
@@ -70,7 +77,9 @@ Meteor.methods({
|
||||
if (! this.userId)
|
||||
throw new Meteor.Error(403, "You must be logged in");
|
||||
|
||||
return Parties.insert({
|
||||
var id = options._id || Random.id();
|
||||
Parties.insert({
|
||||
_id: id,
|
||||
owner: this.userId,
|
||||
x: options.x,
|
||||
y: options.y,
|
||||
@@ -80,6 +89,7 @@ Meteor.methods({
|
||||
invited: [],
|
||||
rsvps: []
|
||||
});
|
||||
return id;
|
||||
},
|
||||
|
||||
invite: function (partyId, userId) {
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.6.5
|
||||
0.6.6.2
|
||||
|
||||
@@ -7,4 +7,3 @@ standard-app-packages
|
||||
insecure
|
||||
jquery
|
||||
preserve-inputs
|
||||
random
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.6.5
|
||||
0.6.6.2
|
||||
|
||||
@@ -132,9 +132,8 @@ Template.scratchpad.events({
|
||||
'click button, keyup input': function (evt) {
|
||||
var textbox = $('#scratchpad input');
|
||||
// if we clicked the button or hit enter
|
||||
if (evt.type === "click" ||
|
||||
(evt.type === "keyup" && evt.which === 13)) {
|
||||
|
||||
if ((evt.type === "click" || (evt.type === "keyup" && evt.which === 13))
|
||||
&& textbox.val()) {
|
||||
var word_id = Words.insert({player_id: Session.get('player_id'),
|
||||
game_id: game() && game()._id,
|
||||
word: textbox.val().toUpperCase(),
|
||||
|
||||
@@ -102,9 +102,12 @@ Meteor.methods({
|
||||
var word = Words.findOne(word_id);
|
||||
var game = Games.findOne(word.game_id);
|
||||
|
||||
// client and server can both check: must be at least three chars
|
||||
// long, not already used, and possible to make on the board.
|
||||
if (word.length < 3
|
||||
// client and server can both check that the game has time remaining, and
|
||||
// that the word is at least three chars, isn't already used, and is
|
||||
// possible to make on the board.
|
||||
if (game.clock === 0
|
||||
|| !word.word
|
||||
|| word.word.length < 3
|
||||
|| Words.find({game_id: word.game_id, word: word.word}).count() > 1
|
||||
|| paths_for_word(game.board, word.word).length === 0) {
|
||||
Words.update(word._id, {$set: {score: 0, state: 'bad'}});
|
||||
@@ -127,8 +130,8 @@ Meteor.methods({
|
||||
if (Meteor.isServer) {
|
||||
DICTIONARY = {};
|
||||
_.each(Assets.getText("enable2k.txt").split("\n"), function (line) {
|
||||
// Skip comment lines
|
||||
if (line.indexOf("//") !== 0) {
|
||||
// Skip blanks and comment lines
|
||||
if (line && line.indexOf("//") !== 0) {
|
||||
DICTIONARY[line] = true;
|
||||
}
|
||||
});
|
||||
|
||||
2
meteor
2
meteor
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
BUNDLE_VERSION=0.3.21
|
||||
BUNDLE_VERSION=0.3.23
|
||||
|
||||
# OS Check. Put here because here is where we download the precompiled
|
||||
# bundles that are arch specific.
|
||||
|
||||
@@ -70,6 +70,8 @@ Accounts.callLoginMethod = function (options) {
|
||||
if (!options[f])
|
||||
options[f] = function () {};
|
||||
});
|
||||
// make sure we only call the user's callback once.
|
||||
var onceUserCallback = _.once(options.userCallback);
|
||||
|
||||
var reconnected = false;
|
||||
|
||||
@@ -91,20 +93,37 @@ Accounts.callLoginMethod = function (options) {
|
||||
if (err || !result || !result.token) {
|
||||
Meteor.connection.onReconnect = null;
|
||||
} else {
|
||||
Meteor.connection.onReconnect = function() {
|
||||
Meteor.connection.onReconnect = function () {
|
||||
reconnected = true;
|
||||
Accounts.callLoginMethod({
|
||||
methodArguments: [{resume: result.token}],
|
||||
// Reconnect quiescence ensures that the user doesn't see an
|
||||
// intermediate state before the login method finishes. So we don't
|
||||
// need to show a logging-in animation.
|
||||
_suppressLoggingIn: true,
|
||||
userCallback: function (error) {
|
||||
if (error) {
|
||||
makeClientLoggedOut();
|
||||
}
|
||||
options.userCallback(error);
|
||||
}});
|
||||
// If our token was updated in storage, use the latest one.
|
||||
var storedToken = storedLoginToken();
|
||||
if (storedToken) {
|
||||
result = {
|
||||
token: storedToken,
|
||||
tokenExpires: storedLoginTokenExpires()
|
||||
};
|
||||
}
|
||||
if (! result.tokenExpires)
|
||||
result.tokenExpires = Accounts._tokenExpiration(new Date());
|
||||
if (Accounts._tokenExpiresSoon(result.tokenExpires)) {
|
||||
makeClientLoggedOut();
|
||||
} else {
|
||||
Accounts.callLoginMethod({
|
||||
methodArguments: [{resume: result.token}],
|
||||
// Reconnect quiescence ensures that the user doesn't see an
|
||||
// intermediate state before the login method finishes. So we don't
|
||||
// need to show a logging-in animation.
|
||||
_suppressLoggingIn: true,
|
||||
userCallback: function (error) {
|
||||
if (error) {
|
||||
makeClientLoggedOut();
|
||||
}
|
||||
// Possibly a weird callback to call, but better than nothing if
|
||||
// there is a reconnect between "login result received" and "data
|
||||
// ready".
|
||||
onceUserCallback(error);
|
||||
}});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -128,19 +147,19 @@ Accounts.callLoginMethod = function (options) {
|
||||
if (error || !result) {
|
||||
error = error || new Error(
|
||||
"No result from call to " + options.methodName);
|
||||
options.userCallback(error);
|
||||
onceUserCallback(error);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
options.validateResult(result);
|
||||
} catch (e) {
|
||||
options.userCallback(e);
|
||||
onceUserCallback(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make the client logged in. (The user data should already be loaded!)
|
||||
makeClientLoggedIn(result.id, result.token);
|
||||
options.userCallback();
|
||||
makeClientLoggedIn(result.id, result.token, result.tokenExpires);
|
||||
onceUserCallback();
|
||||
};
|
||||
|
||||
if (!options._suppressLoggingIn)
|
||||
@@ -158,8 +177,8 @@ makeClientLoggedOut = function() {
|
||||
Meteor.connection.onReconnect = null;
|
||||
};
|
||||
|
||||
makeClientLoggedIn = function(userId, token) {
|
||||
storeLoginToken(userId, token);
|
||||
makeClientLoggedIn = function(userId, token, tokenExpires) {
|
||||
storeLoginToken(userId, token, tokenExpires);
|
||||
Meteor.connection.setUserId(userId);
|
||||
};
|
||||
|
||||
@@ -174,6 +193,26 @@ Meteor.logout = function (callback) {
|
||||
});
|
||||
};
|
||||
|
||||
Meteor.logoutOtherClients = function (callback) {
|
||||
// Our connection is going to be closed, but we don't want to call the
|
||||
// onReconnect handler until the result comes back for this method, because
|
||||
// the token will have been deleted on the server. Instead, wait until we get
|
||||
// a new token and call the reconnect handler with that.
|
||||
// XXX this is messy.
|
||||
// XXX what if login gets called before the callback runs?
|
||||
var origOnReconnect = Meteor.connection.onReconnect;
|
||||
var userId = Meteor.userId();
|
||||
Meteor.connection.onReconnect = null;
|
||||
Meteor.apply('logoutOtherClients', [], { wait: true },
|
||||
function (error, result) {
|
||||
Meteor.connection.onReconnect = origOnReconnect;
|
||||
if (! error)
|
||||
storeLoginToken(userId, result.token, result.tokenExpires);
|
||||
Meteor.connection.onReconnect();
|
||||
callback && callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
///
|
||||
/// LOGIN SERVICES
|
||||
///
|
||||
|
||||
@@ -4,6 +4,18 @@ Accounts = {};
|
||||
// and accounts-ui-unstyled.
|
||||
Accounts._options = {};
|
||||
|
||||
// how long (in days) until a login token expires
|
||||
var DEFAULT_LOGIN_EXPIRATION_DAYS = 90;
|
||||
// Clients don't try to auto-login with a token that is going to expire within
|
||||
// .1 * DEFAULT_LOGIN_EXPIRATION_DAYS, capped at MIN_TOKEN_LIFETIME_CAP_SECS.
|
||||
// Tries to avoid abrupt disconnects from expiring tokens.
|
||||
var MIN_TOKEN_LIFETIME_CAP_SECS = 3600; // one hour
|
||||
// how often (in milliseconds) we check for expired tokens
|
||||
EXPIRE_TOKENS_INTERVAL_MS = 600 * 1000; // 10 minutes
|
||||
// how long we wait before logging out clients when Meteor.logoutOtherClients is
|
||||
// called
|
||||
CONNECTION_CLOSE_DELAY_MS = 10 * 1000;
|
||||
|
||||
// Set up config for the accounts system. Call this on both the client
|
||||
// and the server.
|
||||
//
|
||||
@@ -19,10 +31,31 @@ Accounts._options = {};
|
||||
// client signups.
|
||||
// - forbidClientAccountCreation {Boolean}
|
||||
// Do not allow clients to create accounts directly.
|
||||
// - restrictCreationByEmailDomain {Function or String}
|
||||
// Require created users to have an email matching the function or
|
||||
// having the string as domain.
|
||||
// - loginExpirationInDays {Number}
|
||||
// Number of days since login until a user is logged out (login token
|
||||
// expires).
|
||||
//
|
||||
Accounts.config = function(options) {
|
||||
// We don't want users to accidentally only call Accounts.config on the
|
||||
// client, where some of the options will have partial effects (eg removing
|
||||
// the "create account" button from accounts-ui if forbidClientAccountCreation
|
||||
// is set, or redirecting Google login to a specific-domain page) without
|
||||
// having their full effects.
|
||||
if (Meteor.isServer) {
|
||||
__meteor_runtime_config__.accountsConfigCalled = true;
|
||||
} else if (!__meteor_runtime_config__.accountsConfigCalled) {
|
||||
// XXX would be nice to "crash" the client and replace the UI with an error
|
||||
// message, but there's no trivial way to do this.
|
||||
Meteor._debug("Accounts.config was called on the client but not on the " +
|
||||
"server; some configuration options may not take effect.");
|
||||
}
|
||||
|
||||
// validate option keys
|
||||
var VALID_KEYS = ["sendVerificationEmail", "forbidClientAccountCreation"];
|
||||
var VALID_KEYS = ["sendVerificationEmail", "forbidClientAccountCreation",
|
||||
"restrictCreationByEmailDomain", "loginExpirationInDays"];
|
||||
_.each(_.keys(options), function (key) {
|
||||
if (!_.contains(VALID_KEYS, key)) {
|
||||
throw new Error("Accounts.config: Invalid key: " + key);
|
||||
@@ -39,6 +72,11 @@ Accounts.config = function(options) {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If the user set loginExpirationInDays to null, then we need to clear the
|
||||
// timer that periodically expires tokens.
|
||||
if (Meteor.isServer)
|
||||
maybeStopExpireTokensInterval();
|
||||
};
|
||||
|
||||
// Users table. Don't use the normal autopublish, since we want to hide
|
||||
@@ -66,3 +104,21 @@ Accounts.LoginCancelledError.numericError = 0x8acdc2f;
|
||||
Accounts.LoginCancelledError.prototype = new Error();
|
||||
Accounts.LoginCancelledError.prototype.name = 'Accounts.LoginCancelledError';
|
||||
|
||||
getTokenLifetimeMs = function () {
|
||||
return (Accounts._options.loginExpirationInDays ||
|
||||
DEFAULT_LOGIN_EXPIRATION_DAYS) * 24 * 60 * 60 * 1000;
|
||||
};
|
||||
|
||||
Accounts._tokenExpiration = function (when) {
|
||||
// We pass when through the Date constructor for backwards compatibility;
|
||||
// `when` used to be a number.
|
||||
return new Date((new Date(when)).getTime() + getTokenLifetimeMs());
|
||||
};
|
||||
|
||||
Accounts._tokenExpiresSoon = function (when) {
|
||||
var minLifetimeMs = .1 * getTokenLifetimeMs();
|
||||
var minLifetimeCapMs = MIN_TOKEN_LIFETIME_CAP_SECS * 1000;
|
||||
if (minLifetimeMs > minLifetimeCapMs)
|
||||
minLifetimeMs = minLifetimeCapMs;
|
||||
return new Date() > (new Date(when) - minLifetimeMs);
|
||||
};
|
||||
|
||||
@@ -34,7 +34,11 @@ Meteor.user = function () {
|
||||
// @param handler {Function} A function that receives an options object
|
||||
// (as passed as an argument to the `login` method) and returns one of:
|
||||
// - `undefined`, meaning don't handle;
|
||||
// - {id: userId, token: *}, if the user logged in successfully.
|
||||
// - {id: userId, token: *, tokenExpires: *}, if the user logged in
|
||||
// successfully. tokenExpires is optional and intends to provide a hint to the
|
||||
// client as to when the token will expire. If not provided, the client will
|
||||
// call Accounts._tokenExpiration, passing it the date that it received the
|
||||
// token.
|
||||
// - throw an error, if the user failed to log in.
|
||||
//
|
||||
Accounts.registerLoginHandler = function(handler) {
|
||||
@@ -74,15 +78,64 @@ Meteor.methods({
|
||||
var result = tryAllLoginHandlers(options);
|
||||
if (result !== null) {
|
||||
this.setUserId(result.id);
|
||||
this._sessionData.loginToken = result.token;
|
||||
this._setLoginToken(result.token);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
logout: function() {
|
||||
if (this._sessionData.loginToken && this.userId)
|
||||
removeLoginToken(this.userId, this._sessionData.loginToken);
|
||||
var token = this._getLoginToken();
|
||||
this._setLoginToken(null);
|
||||
if (token && this.userId)
|
||||
removeLoginToken(this.userId, token);
|
||||
this.setUserId(null);
|
||||
},
|
||||
|
||||
// Delete all the current user's tokens and close all open connections logged
|
||||
// in as this user. Returns a fresh new login token that this client can
|
||||
// use. Tests set Accounts._noConnectionCloseDelayForTest to delete tokens
|
||||
// immediately instead of using a delay.
|
||||
//
|
||||
// @returns {Object} Object with token and tokenExpires keys.
|
||||
logoutOtherClients: function () {
|
||||
var self = this;
|
||||
var user = Meteor.users.findOne(self.userId, {
|
||||
fields: {
|
||||
"services.resume.loginTokens": true
|
||||
}
|
||||
});
|
||||
if (user) {
|
||||
// Save the current tokens in the database to be deleted in
|
||||
// CONNECTION_CLOSE_DELAY_MS ms. This gives other connections in the
|
||||
// caller's browser time to find the fresh token in localStorage. We save
|
||||
// the tokens in the database in case we crash before actually deleting
|
||||
// them.
|
||||
var tokens = user.services.resume.loginTokens;
|
||||
var newToken = Accounts._generateStampedLoginToken();
|
||||
var userId = self.userId;
|
||||
Meteor.users.update(self.userId, {
|
||||
$set: {
|
||||
"services.resume.loginTokensToDelete": tokens,
|
||||
"services.resume.haveLoginTokensToDelete": true
|
||||
},
|
||||
$push: { "services.resume.loginTokens": newToken }
|
||||
});
|
||||
Meteor.setTimeout(function () {
|
||||
// The observe on Meteor.users will take care of closing the connections
|
||||
// associated with `tokens`.
|
||||
deleteSavedTokens(userId, tokens);
|
||||
}, Accounts._noConnectionCloseDelayForTest ? 0 :
|
||||
CONNECTION_CLOSE_DELAY_MS);
|
||||
// We do not set the login token on this connection, but instead the
|
||||
// observe closes the connection and the client will reconnect with the
|
||||
// new token.
|
||||
return {
|
||||
token: newToken.token,
|
||||
tokenExpires: Accounts._tokenExpiration(newToken.when)
|
||||
};
|
||||
} else {
|
||||
throw new Error("You are not logged in.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -100,11 +153,23 @@ Accounts.registerLoginHandler(function(options) {
|
||||
var user = Meteor.users.findOne({
|
||||
"services.resume.loginTokens.token": ""+options.resume
|
||||
});
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "Couldn't find login token");
|
||||
|
||||
if (!user) {
|
||||
throw new Meteor.Error(403, "You've been logged out by the server. " +
|
||||
"Please login again.");
|
||||
}
|
||||
|
||||
var token = _.find(user.services.resume.loginTokens, function (token) {
|
||||
return token.token === options.resume;
|
||||
});
|
||||
|
||||
var tokenExpires = Accounts._tokenExpiration(token.when);
|
||||
if (new Date() >= tokenExpires)
|
||||
throw new Meteor.Error(403, "Your session has expired. Please login again.");
|
||||
|
||||
return {
|
||||
token: options.resume,
|
||||
tokenExpires: tokenExpires,
|
||||
id: user._id
|
||||
};
|
||||
});
|
||||
@@ -115,7 +180,9 @@ Accounts._generateStampedLoginToken = function () {
|
||||
return {token: Random.id(), when: (new Date)};
|
||||
};
|
||||
|
||||
removeLoginToken = function (userId, loginToken) {
|
||||
// Deletes the given loginToken from the database. This will cause all
|
||||
// connections associated with the token to be closed.
|
||||
var removeLoginToken = function (userId, loginToken) {
|
||||
Meteor.users.update(userId, {
|
||||
$pull: {
|
||||
"services.resume.loginTokens": { "token": loginToken }
|
||||
@@ -123,6 +190,64 @@ removeLoginToken = function (userId, loginToken) {
|
||||
});
|
||||
};
|
||||
|
||||
///
|
||||
/// TOKEN EXPIRATION
|
||||
///
|
||||
|
||||
var expireTokenInterval;
|
||||
|
||||
// Deletes expired tokens from the database and closes all open connections
|
||||
// associated with these tokens.
|
||||
//
|
||||
// Exported for tests. Also, the arguments are only used by
|
||||
// tests. oldestValidDate is simulate expiring tokens without waiting
|
||||
// for them to actually expire. userId is used by tests to only expire
|
||||
// tokens for the test user.
|
||||
var expireTokens = Accounts._expireTokens = function (oldestValidDate, userId) {
|
||||
var tokenLifetimeMs = getTokenLifetimeMs();
|
||||
|
||||
// when calling from a test with extra arguments, you must specify both!
|
||||
if ((oldestValidDate && !userId) || (!oldestValidDate && userId)) {
|
||||
throw new Error("Bad test. Must specify both oldestValidDate and userId.");
|
||||
}
|
||||
|
||||
oldestValidDate = oldestValidDate ||
|
||||
(new Date(new Date() - tokenLifetimeMs));
|
||||
var userFilter = userId ? {_id: userId} : {};
|
||||
|
||||
|
||||
// Backwards compatible with older versions of meteor that stored login token
|
||||
// timestamps as numbers.
|
||||
Meteor.users.update(_.extend(userFilter, {
|
||||
$or: [
|
||||
{ "services.resume.loginTokens.when": { $lt: oldestValidDate } },
|
||||
{ "services.resume.loginTokens.when": { $lt: +oldestValidDate } }
|
||||
]
|
||||
}), {
|
||||
$pull: {
|
||||
"services.resume.loginTokens": {
|
||||
$or: [
|
||||
{ when: { $lt: oldestValidDate } },
|
||||
{ when: { $lt: +oldestValidDate } }
|
||||
]
|
||||
}
|
||||
}
|
||||
}, { multi: true });
|
||||
// The observe on Meteor.users will take care of closing connections for
|
||||
// expired tokens.
|
||||
};
|
||||
|
||||
maybeStopExpireTokensInterval = function () {
|
||||
if (_.has(Accounts._options, "loginExpirationInDays") &&
|
||||
Accounts._options.loginExpirationInDays === null &&
|
||||
expireTokenInterval) {
|
||||
Meteor.clearInterval(expireTokenInterval);
|
||||
expireTokenInterval = null;
|
||||
}
|
||||
};
|
||||
|
||||
expireTokenInterval = Meteor.setInterval(expireTokens,
|
||||
EXPIRE_TOKENS_INTERVAL_MS);
|
||||
|
||||
///
|
||||
/// CREATE USER HOOKS
|
||||
@@ -164,6 +289,7 @@ Accounts.insertUserDoc = function (options, user) {
|
||||
if (options.generateLoginToken) {
|
||||
var stampedToken = Accounts._generateStampedLoginToken();
|
||||
result.token = stampedToken.token;
|
||||
result.tokenExpires = Accounts._tokenExpiration(stampedToken.when);
|
||||
Meteor._ensure(user, 'services', 'resume');
|
||||
if (_.has(user.services.resume, 'loginTokens'))
|
||||
user.services.resume.loginTokens.push(stampedToken);
|
||||
@@ -213,6 +339,49 @@ Accounts.validateNewUser = function (func) {
|
||||
validateNewUserHooks.push(func);
|
||||
};
|
||||
|
||||
// XXX Find a better place for this utility function
|
||||
// Like Perl's quotemeta: quotes all regexp metacharacters. See
|
||||
// https://github.com/substack/quotemeta/blob/master/index.js
|
||||
var quotemeta = function (str) {
|
||||
return String(str).replace(/(\W)/g, '\\$1');
|
||||
};
|
||||
|
||||
// Helper function: returns false if email does not match company domain from
|
||||
// the configuration.
|
||||
var testEmailDomain = function (email) {
|
||||
var domain = Accounts._options.restrictCreationByEmailDomain;
|
||||
return !domain ||
|
||||
(_.isFunction(domain) && domain(email)) ||
|
||||
(_.isString(domain) &&
|
||||
(new RegExp('@' + quotemeta(domain) + '$', 'i')).test(email));
|
||||
};
|
||||
|
||||
// Validate new user's email or Google/Facebook/GitHub account's email
|
||||
Accounts.validateNewUser(function (user) {
|
||||
var domain = Accounts._options.restrictCreationByEmailDomain;
|
||||
if (!domain)
|
||||
return true;
|
||||
|
||||
var emailIsGood = false;
|
||||
if (!_.isEmpty(user.emails)) {
|
||||
emailIsGood = _.any(user.emails, function (email) {
|
||||
return testEmailDomain(email.address);
|
||||
});
|
||||
} else if (!_.isEmpty(user.services)) {
|
||||
// Find any email of any service and check it
|
||||
emailIsGood = _.any(user.services, function (service) {
|
||||
return service.email && testEmailDomain(service.email);
|
||||
});
|
||||
}
|
||||
|
||||
if (emailIsGood)
|
||||
return true;
|
||||
|
||||
if (_.isString(domain))
|
||||
throw new Meteor.Error(403, "@" + domain + " email required");
|
||||
else
|
||||
throw new Meteor.Error(403, "Email doesn't match the criteria.");
|
||||
});
|
||||
|
||||
///
|
||||
/// MANAGING USER OBJECTS
|
||||
@@ -280,7 +449,11 @@ Accounts.updateOrCreateUserFromExternalService = function(
|
||||
user._id,
|
||||
{$set: setAttrs,
|
||||
$push: {'services.resume.loginTokens': stampedToken}});
|
||||
return {token: stampedToken.token, id: user._id};
|
||||
return {
|
||||
token: stampedToken.token,
|
||||
id: user._id,
|
||||
tokenExpires: Accounts._tokenExpiration(stampedToken.when)
|
||||
};
|
||||
} else {
|
||||
// Create a new user with the service data. Pass other options through to
|
||||
// insertUserDoc.
|
||||
@@ -426,3 +599,72 @@ Meteor.users._ensureIndex('username', {unique: 1, sparse: 1});
|
||||
Meteor.users._ensureIndex('emails.address', {unique: 1, sparse: 1});
|
||||
Meteor.users._ensureIndex('services.resume.loginTokens.token',
|
||||
{unique: 1, sparse: 1});
|
||||
// For taking care of logoutOtherClients calls that crashed before the tokens
|
||||
// were deleted.
|
||||
Meteor.users._ensureIndex('services.resume.haveLoginTokensToDelete',
|
||||
{ sparse: 1 });
|
||||
// For expiring login tokens
|
||||
Meteor.users._ensureIndex("services.resume.loginTokens.when", { sparse: 1 });
|
||||
|
||||
///
|
||||
/// CLEAN UP FOR `logoutOtherClients`
|
||||
///
|
||||
|
||||
var deleteSavedTokens = function (userId, tokensToDelete) {
|
||||
if (tokensToDelete) {
|
||||
Meteor.users.update(userId, {
|
||||
$unset: {
|
||||
"services.resume.haveLoginTokensToDelete": 1,
|
||||
"services.resume.loginTokensToDelete": 1
|
||||
},
|
||||
$pullAll: {
|
||||
"services.resume.loginTokens": tokensToDelete
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Meteor.startup(function () {
|
||||
// If we find users who have saved tokens to delete on startup, delete them
|
||||
// now. It's possible that the server could have crashed and come back up
|
||||
// before new tokens are found in localStorage, but this shouldn't happen very
|
||||
// often. We shouldn't put a delay here because that would give a lot of power
|
||||
// to an attacker with a stolen login token and the ability to crash the
|
||||
// server.
|
||||
var users = Meteor.users.find({
|
||||
"services.resume.haveLoginTokensToDelete": true
|
||||
}, {
|
||||
"services.resume.loginTokensToDelete": 1
|
||||
});
|
||||
users.forEach(function (user) {
|
||||
deleteSavedTokens(user._id, user.services.resume.loginTokensToDelete);
|
||||
});
|
||||
});
|
||||
|
||||
///
|
||||
/// LOGGING OUT DELETED USERS
|
||||
///
|
||||
|
||||
var closeTokensForUser = function (userTokens) {
|
||||
Meteor.server._closeAllForTokens(_.map(userTokens, function (token) {
|
||||
return token.token;
|
||||
}));
|
||||
};
|
||||
|
||||
Meteor.users.find({}, { fields: { "services.resume": 1 }}).observe({
|
||||
changed: function (newUser, oldUser) {
|
||||
var removedTokens = [];
|
||||
if (newUser.services && newUser.services.resume &&
|
||||
oldUser.services && oldUser.services.resume) {
|
||||
removedTokens = _.difference(oldUser.services.resume.loginTokens || [],
|
||||
newUser.services.resume.loginTokens || []);
|
||||
} else if (oldUser.services && oldUser.services.resume) {
|
||||
removedTokens = oldUser.services.resume.loginTokens || [];
|
||||
}
|
||||
closeTokensForUser(removedTokens);
|
||||
},
|
||||
removed: function (oldUser) {
|
||||
if (oldUser.services && oldUser.services.resume)
|
||||
closeTokensForUser(oldUser.services.resume.loginTokens || []);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -178,3 +178,33 @@ Tinytest.add('accounts - insertUserDoc email', function (test) {
|
||||
Meteor.users.remove(result.id);
|
||||
Meteor.users.remove(result3.id);
|
||||
});
|
||||
|
||||
// More token expiration tests are in accounts-password
|
||||
Tinytest.addAsync('accounts - expire numeric token', function (test, onComplete) {
|
||||
var userIn = { username: Random.id() };
|
||||
var result = Accounts.insertUserDoc({ profile: {
|
||||
name: 'Foo Bar'
|
||||
} }, userIn);
|
||||
var date = new Date(new Date() - 5000);
|
||||
Meteor.users.update(result.id, {
|
||||
$set: {
|
||||
"services.resume.loginTokens": [{
|
||||
token: Random.id(),
|
||||
when: date
|
||||
}, {
|
||||
token: Random.id(),
|
||||
when: +date
|
||||
}]
|
||||
}
|
||||
});
|
||||
var observe = Meteor.users.find(result.id).observe({
|
||||
changed: function (newUser) {
|
||||
if (newUser.services && newUser.services.resume &&
|
||||
_.isEmpty(newUser.services.resume.loginTokens)) {
|
||||
observe.stop();
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
});
|
||||
Accounts._expireTokens(new Date(), result.id);
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ Accounts._enableAutoLogin = function () {
|
||||
|
||||
// Key names to use in localStorage
|
||||
var loginTokenKey = "Meteor.loginToken";
|
||||
var loginTokenExpiresKey = "Meteor.loginTokenExpires";
|
||||
var userIdKey = "Meteor.userId";
|
||||
|
||||
// Call this from the top level of the test file for any test that does
|
||||
@@ -37,9 +38,12 @@ Accounts._isolateLoginTokenForTest = function () {
|
||||
userIdKey = userIdKey + Random.id();
|
||||
};
|
||||
|
||||
storeLoginToken = function(userId, token) {
|
||||
storeLoginToken = function(userId, token, tokenExpires) {
|
||||
Meteor._localStorage.setItem(userIdKey, userId);
|
||||
Meteor._localStorage.setItem(loginTokenKey, token);
|
||||
if (! tokenExpires)
|
||||
tokenExpires = Accounts._tokenExpiration(new Date());
|
||||
Meteor._localStorage.setItem(loginTokenExpiresKey, tokenExpires);
|
||||
|
||||
// to ensure that the localstorage poller doesn't end up trying to
|
||||
// connect a second time
|
||||
@@ -49,6 +53,7 @@ storeLoginToken = function(userId, token) {
|
||||
unstoreLoginToken = function() {
|
||||
Meteor._localStorage.removeItem(userIdKey);
|
||||
Meteor._localStorage.removeItem(loginTokenKey);
|
||||
Meteor._localStorage.removeItem(loginTokenExpiresKey);
|
||||
|
||||
// to ensure that the localstorage poller doesn't end up trying to
|
||||
// connect a second time
|
||||
@@ -58,14 +63,23 @@ unstoreLoginToken = function() {
|
||||
// This is private, but it is exported for now because it is used by a
|
||||
// test in accounts-password.
|
||||
//
|
||||
var storedLoginToken = Accounts._storedLoginToken = function() {
|
||||
storedLoginToken = Accounts._storedLoginToken = function() {
|
||||
return Meteor._localStorage.getItem(loginTokenKey);
|
||||
};
|
||||
|
||||
storedLoginTokenExpires = function () {
|
||||
return Meteor._localStorage.getItem(loginTokenExpiresKey);
|
||||
};
|
||||
|
||||
var storedUserId = function() {
|
||||
return Meteor._localStorage.getItem(userIdKey);
|
||||
};
|
||||
|
||||
var unstoreLoginTokenIfExpiresSoon = function () {
|
||||
var tokenExpires = Meteor._localStorage.getItem(loginTokenExpiresKey);
|
||||
if (tokenExpires && Accounts._tokenExpiresSoon(new Date(tokenExpires)))
|
||||
unstoreLoginToken();
|
||||
};
|
||||
|
||||
///
|
||||
/// AUTO-LOGIN
|
||||
@@ -74,6 +88,7 @@ var storedUserId = function() {
|
||||
if (autoLoginEnabled) {
|
||||
// Immediately try to log in via local storage, so that any DDP
|
||||
// messages are sent after we have established our user account
|
||||
unstoreLoginTokenIfExpiresSoon();
|
||||
var token = storedLoginToken();
|
||||
if (token) {
|
||||
// On startup, optimistically present us as logged in while the
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Package.describe({
|
||||
summary: "Password support for accounts."
|
||||
summary: "Password support for accounts"
|
||||
});
|
||||
|
||||
Package.on_use(function(api) {
|
||||
|
||||
@@ -98,7 +98,12 @@ Accounts.registerLoginHandler(function (options) {
|
||||
Meteor.users.update(
|
||||
userId, {$push: {'services.resume.loginTokens': stampedLoginToken}});
|
||||
|
||||
return {token: stampedLoginToken.token, id: userId, HAMK: serialized.HAMK};
|
||||
return {
|
||||
token: stampedLoginToken.token,
|
||||
tokenExpires: Accounts._tokenExpiration(stampedLoginToken.when),
|
||||
id: userId,
|
||||
HAMK: serialized.HAMK
|
||||
};
|
||||
});
|
||||
|
||||
// Handler to login with plaintext password.
|
||||
@@ -137,7 +142,11 @@ Accounts.registerLoginHandler(function (options) {
|
||||
Meteor.users.update(
|
||||
user._id, {$push: {'services.resume.loginTokens': stampedLoginToken}});
|
||||
|
||||
return {token: stampedLoginToken.token, id: user._id};
|
||||
return {
|
||||
token: stampedLoginToken.token,
|
||||
tokenExpires: Accounts._tokenExpiration(stampedLoginToken.when),
|
||||
id: user._id
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -307,21 +316,40 @@ Meteor.methods({resetPassword: function (token, newVerifier) {
|
||||
|
||||
var stampedLoginToken = Accounts._generateStampedLoginToken();
|
||||
|
||||
// Update the user record by:
|
||||
// - Changing the password verifier to the new one
|
||||
// - Replacing all valid login tokens with new ones (changing
|
||||
// password should invalidate existing sessions).
|
||||
// - Forgetting about the reset token that was just used
|
||||
// - Verifying their email, since they got the password reset via email.
|
||||
Meteor.users.update({_id: user._id, 'emails.address': email}, {
|
||||
$set: {'services.password.srp': newVerifier,
|
||||
'services.resume.loginTokens': [stampedLoginToken],
|
||||
'emails.$.verified': true},
|
||||
$unset: {'services.password.reset': 1}
|
||||
});
|
||||
// NOTE: We're about to invalidate tokens on the user, who we might be
|
||||
// logged in as. Make sure to avoid logging ourselves out if this
|
||||
// happens. But also make sure not to leave the connection in a state
|
||||
// of having a bad token set if things fail.
|
||||
var oldToken = this._getLoginToken();
|
||||
this._setLoginToken(null);
|
||||
|
||||
try {
|
||||
// Update the user record by:
|
||||
// - Changing the password verifier to the new one
|
||||
// - Replacing all valid login tokens with new ones (changing
|
||||
// password should invalidate existing sessions).
|
||||
// - Forgetting about the reset token that was just used
|
||||
// - Verifying their email, since they got the password reset via email.
|
||||
Meteor.users.update({_id: user._id, 'emails.address': email}, {
|
||||
$set: {'services.password.srp': newVerifier,
|
||||
'services.resume.loginTokens': [stampedLoginToken],
|
||||
'emails.$.verified': true},
|
||||
$unset: {'services.password.reset': 1}
|
||||
});
|
||||
} catch (err) {
|
||||
// update failed somehow. reset to old token.
|
||||
this._setLoginToken(oldToken);
|
||||
throw err;
|
||||
}
|
||||
|
||||
this._setLoginToken(stampedLoginToken.token);
|
||||
this.setUserId(user._id);
|
||||
return {token: stampedLoginToken.token, id: user._id};
|
||||
|
||||
return {
|
||||
token: stampedLoginToken.token,
|
||||
tokenExpires: Accounts._tokenExpiration(stampedLoginToken.when),
|
||||
id: user._id
|
||||
};
|
||||
}});
|
||||
|
||||
///
|
||||
@@ -408,7 +436,12 @@ Meteor.methods({verifyEmail: function (token) {
|
||||
$push: {'services.resume.loginTokens': stampedLoginToken}});
|
||||
|
||||
this.setUserId(user._id);
|
||||
return {token: stampedLoginToken.token, id: user._id};
|
||||
this._setLoginToken(stampedLoginToken.token);
|
||||
return {
|
||||
token: stampedLoginToken.token,
|
||||
tokenExpires: Accounts._tokenExpiration(stampedLoginToken.when),
|
||||
id: user._id
|
||||
};
|
||||
}});
|
||||
|
||||
|
||||
@@ -482,6 +515,7 @@ Meteor.methods({createUser: function (options) {
|
||||
|
||||
// client gets logged in as the new user afterwards.
|
||||
this.setUserId(result.id);
|
||||
this._setLoginToken(result.token);
|
||||
return result;
|
||||
}});
|
||||
|
||||
@@ -516,5 +550,5 @@ Accounts.createUser = function (options, callback) {
|
||||
///
|
||||
Meteor.users._ensureIndex('emails.validationTokens.token',
|
||||
{unique: 1, sparse: 1});
|
||||
Meteor.users._ensureIndex('emails.password.reset.token',
|
||||
Meteor.users._ensureIndex('services.password.reset.token',
|
||||
{unique: 1, sparse: 1});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
Accounts._noConnectionCloseDelayForTest = true;
|
||||
|
||||
if (Meteor.isClient) (function () {
|
||||
|
||||
// XXX note, only one test can do login/logout things at once! for
|
||||
@@ -17,40 +19,30 @@ if (Meteor.isClient) (function () {
|
||||
test.equal(Meteor.user().username, someUsername);
|
||||
});
|
||||
};
|
||||
var waitForLoggedOutStep = function (test, expect) {
|
||||
pollUntil(expect, function () {
|
||||
return Meteor.userId() === null;
|
||||
}, 10 * 1000, 100);
|
||||
};
|
||||
|
||||
// declare variable outside the testAsyncMulti, so we can refer to
|
||||
// them from multiple tests, but initialize them to new values inside
|
||||
// the test so when we use the 'debug' link in the tests, they get new
|
||||
// values and the tests don't fail.
|
||||
var username, username2, username3;
|
||||
var userId1, userId3;
|
||||
var email;
|
||||
var password, password2, password3;
|
||||
testAsyncMulti("passwords - basic login with password", [
|
||||
function (test, expect) {
|
||||
// setup
|
||||
this.username = Random.id();
|
||||
this.email = Random.id() + '-intercept@example.com';
|
||||
this.password = 'password';
|
||||
|
||||
testAsyncMulti("passwords - long series", [
|
||||
function (test, expect) {
|
||||
username = Random.id();
|
||||
username2 = Random.id();
|
||||
username3 = Random.id();
|
||||
// use -intercept so that we don't print to the console
|
||||
email = Random.id() + '-intercept@example.com';
|
||||
password = 'password';
|
||||
password2 = 'password2';
|
||||
password3 = 'password3';
|
||||
},
|
||||
function (test, expect) {
|
||||
Accounts.createUser(
|
||||
{username: username, email: email, password: password},
|
||||
loggedInAs(username, test, expect));
|
||||
{username: this.username, email: this.email, password: this.password},
|
||||
loggedInAs(this.username, test, expect));
|
||||
},
|
||||
function (test, expect) {
|
||||
userId1 = Meteor.userId();
|
||||
test.notEqual(userId1, null);
|
||||
test.notEqual(Meteor.userId(), null);
|
||||
},
|
||||
logoutStep,
|
||||
function (test, expect) {
|
||||
Meteor.loginWithPassword(username, password,
|
||||
loggedInAs(username, test, expect));
|
||||
Meteor.loginWithPassword(this.username, this.password,
|
||||
loggedInAs(this.username, test, expect));
|
||||
},
|
||||
logoutStep,
|
||||
// This next step tests reactive contexts which are reactive on
|
||||
@@ -65,7 +57,7 @@ if (Meteor.isClient) (function () {
|
||||
});
|
||||
// At the beginning, we're not logged in.
|
||||
test.isFalse(loaded);
|
||||
Meteor.loginWithPassword(username, password, expect(function (error) {
|
||||
Meteor.loginWithPassword(this.username, this.password, expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
test.notEqual(Meteor.userId(), null);
|
||||
// By the time of the login callback, the user should be loaded.
|
||||
@@ -78,18 +70,43 @@ if (Meteor.isClient) (function () {
|
||||
},
|
||||
logoutStep,
|
||||
function (test, expect) {
|
||||
Meteor.loginWithPassword({username: username}, password,
|
||||
loggedInAs(username, test, expect));
|
||||
Meteor.loginWithPassword({username: this.username}, this.password,
|
||||
loggedInAs(this.username, test, expect));
|
||||
},
|
||||
logoutStep,
|
||||
function (test, expect) {
|
||||
Meteor.loginWithPassword(email, password,
|
||||
loggedInAs(username, test, expect));
|
||||
Meteor.loginWithPassword(this.email, this.password,
|
||||
loggedInAs(this.username, test, expect));
|
||||
},
|
||||
logoutStep,
|
||||
function (test, expect) {
|
||||
Meteor.loginWithPassword({email: email}, password,
|
||||
loggedInAs(username, test, expect));
|
||||
Meteor.loginWithPassword({email: this.email}, this.password,
|
||||
loggedInAs(this.username, test, expect));
|
||||
},
|
||||
logoutStep
|
||||
]);
|
||||
|
||||
|
||||
testAsyncMulti("passwords - plain text passwords", [
|
||||
function (test, expect) {
|
||||
// setup
|
||||
this.username = Random.id();
|
||||
this.email = Random.id() + '-intercept@example.com';
|
||||
this.password = 'password';
|
||||
|
||||
// create user with raw password (no API, need to invoke callLoginMethod
|
||||
// directly)
|
||||
Accounts.callLoginMethod({
|
||||
methodName: 'createUser',
|
||||
methodArguments: [{username: this.username, password: this.password}],
|
||||
userCallback: loggedInAs(this.username, test, expect)
|
||||
});
|
||||
},
|
||||
logoutStep,
|
||||
// check can login normally with this password.
|
||||
function(test, expect) {
|
||||
Meteor.loginWithPassword({username: this.username}, this.password,
|
||||
loggedInAs(this.username, test, expect));
|
||||
},
|
||||
logoutStep,
|
||||
// plain text password. no API for this, have to invoke callLoginMethod
|
||||
@@ -97,7 +114,7 @@ if (Meteor.isClient) (function () {
|
||||
function (test, expect) {
|
||||
Accounts.callLoginMethod({
|
||||
// wrong password
|
||||
methodArguments: [{user: {email: email}, password: password2}],
|
||||
methodArguments: [{user: {username: this.username}, password: 'wrong'}],
|
||||
userCallback: expect(function (error) {
|
||||
test.isTrue(error);
|
||||
test.isFalse(Meteor.user());
|
||||
@@ -106,138 +123,127 @@ if (Meteor.isClient) (function () {
|
||||
function (test, expect) {
|
||||
Accounts.callLoginMethod({
|
||||
// right password
|
||||
methodArguments: [{user: {email: email}, password: password}],
|
||||
userCallback: loggedInAs(username, test, expect)
|
||||
methodArguments: [{user: {username: this.username},
|
||||
password: this.password}],
|
||||
userCallback: loggedInAs(this.username, test, expect)
|
||||
});
|
||||
},
|
||||
logoutStep
|
||||
]);
|
||||
|
||||
|
||||
testAsyncMulti("passwords - changing passwords", [
|
||||
function (test, expect) {
|
||||
// setup
|
||||
this.username = Random.id();
|
||||
this.email = Random.id() + '-intercept@example.com';
|
||||
this.password = 'password';
|
||||
this.password2 = 'password2';
|
||||
|
||||
Accounts.createUser(
|
||||
{username: this.username, email: this.email, password: this.password},
|
||||
loggedInAs(this.username, test, expect));
|
||||
},
|
||||
// change password with bad old password. we stay logged in.
|
||||
function (test, expect) {
|
||||
Accounts.changePassword(password2, password2, expect(function (error) {
|
||||
var self = this;
|
||||
Accounts.changePassword('wrong', 'doesntmatter', expect(function (error) {
|
||||
test.isTrue(error);
|
||||
test.equal(Meteor.user().username, username);
|
||||
test.equal(Meteor.user().username, self.username);
|
||||
}));
|
||||
},
|
||||
// change password with good old password.
|
||||
function (test, expect) {
|
||||
Accounts.changePassword(password, password2,
|
||||
loggedInAs(username, test, expect));
|
||||
Accounts.changePassword(this.password, this.password2,
|
||||
loggedInAs(this.username, test, expect));
|
||||
},
|
||||
logoutStep,
|
||||
// old password, failed login
|
||||
function (test, expect) {
|
||||
Meteor.loginWithPassword(email, password, expect(function (error) {
|
||||
Meteor.loginWithPassword(this.email, this.password, expect(function (error) {
|
||||
test.isTrue(error);
|
||||
test.isFalse(Meteor.user());
|
||||
}));
|
||||
},
|
||||
// new password, success
|
||||
function (test, expect) {
|
||||
Meteor.loginWithPassword(email, password2,
|
||||
loggedInAs(username, test, expect));
|
||||
Meteor.loginWithPassword(this.email, this.password2,
|
||||
loggedInAs(this.username, test, expect));
|
||||
},
|
||||
logoutStep,
|
||||
// create user with raw password (no API, need to invoke callLoginMethod
|
||||
// directly)
|
||||
logoutStep
|
||||
]);
|
||||
|
||||
|
||||
testAsyncMulti("passwords - new user hooks", [
|
||||
function (test, expect) {
|
||||
Accounts.callLoginMethod({
|
||||
methodName: 'createUser',
|
||||
methodArguments: [{username: username2, password: password2}],
|
||||
userCallback: loggedInAs(username2, test, expect)
|
||||
});
|
||||
// setup
|
||||
this.username = Random.id();
|
||||
this.email = Random.id() + '-intercept@example.com';
|
||||
this.password = 'password';
|
||||
},
|
||||
logoutStep,
|
||||
function(test, expect) {
|
||||
Meteor.loginWithPassword({username: username2}, password2,
|
||||
loggedInAs(username2, test, expect));
|
||||
},
|
||||
logoutStep,
|
||||
// test Accounts.validateNewUser
|
||||
function(test, expect) {
|
||||
Accounts.createUser({username: username3, password: password3,
|
||||
// should fail the new user validators
|
||||
profile: {invalid: true}},
|
||||
expect(function (error) {
|
||||
test.equal(error.error, 403);
|
||||
test.equal(
|
||||
error.reason,
|
||||
"User validation failed");
|
||||
}));
|
||||
Accounts.createUser(
|
||||
{username: this.username, password: this.password,
|
||||
// should fail the new user validators
|
||||
profile: {invalid: true}},
|
||||
expect(function (error) {
|
||||
test.equal(error.error, 403);
|
||||
test.equal(error.reason, "User validation failed");
|
||||
}));
|
||||
},
|
||||
logoutStep,
|
||||
function(test, expect) {
|
||||
Accounts.createUser({username: username3, password: password3,
|
||||
// should fail the new user validator with a special
|
||||
// exception
|
||||
profile: {invalidAndThrowException: true}},
|
||||
expect(function (error) {
|
||||
test.equal(
|
||||
error.reason,
|
||||
"An exception thrown within Accounts.validateNewUser");
|
||||
}));
|
||||
Accounts.createUser(
|
||||
{username: this.username, password: this.password,
|
||||
// should fail the new user validator with a special
|
||||
// exception
|
||||
profile: {invalidAndThrowException: true}},
|
||||
expect(function (error) {
|
||||
test.equal(
|
||||
error.reason,
|
||||
"An exception thrown within Accounts.validateNewUser");
|
||||
}));
|
||||
},
|
||||
// test Accounts.onCreateUser
|
||||
function(test, expect) {
|
||||
Accounts.createUser(
|
||||
{username: username3, password: password3,
|
||||
{username: this.username, password: this.password,
|
||||
testOnCreateUserHook: true},
|
||||
loggedInAs(username3, test, expect));
|
||||
loggedInAs(this.username, test, expect));
|
||||
},
|
||||
function(test, expect) {
|
||||
test.equal(Meteor.user().profile.touchedByOnCreateUser, true);
|
||||
},
|
||||
logoutStep
|
||||
]);
|
||||
|
||||
|
||||
testAsyncMulti("passwords - Meteor.user()", [
|
||||
function (test, expect) {
|
||||
// setup
|
||||
this.username = Random.id();
|
||||
this.password = 'password';
|
||||
|
||||
Accounts.createUser(
|
||||
{username: this.username, password: this.password,
|
||||
testOnCreateUserHook: true},
|
||||
loggedInAs(this.username, test, expect));
|
||||
},
|
||||
// test Meteor.user(). This test properly belongs in
|
||||
// accounts-base/accounts_tests.js, but this is where the tests that
|
||||
// actually log in are.
|
||||
function(test, expect) {
|
||||
var self = this;
|
||||
var clientUser = Meteor.user();
|
||||
Meteor.call('testMeteorUser', expect(function (err, result) {
|
||||
test.equal(result._id, clientUser._id);
|
||||
test.equal(result.username, clientUser.username);
|
||||
test.equal(result.username, self.username);
|
||||
test.equal(result.profile.touchedByOnCreateUser, true);
|
||||
test.equal(err, undefined);
|
||||
}));
|
||||
},
|
||||
// test the default Meteor.users allow rule. This test properly belongs in
|
||||
// accounts-base/accounts_tests.js, but this is where the tests that
|
||||
// actually log in are.
|
||||
function(test, expect) {
|
||||
userId3 = Meteor.userId();
|
||||
test.notEqual(userId3, null);
|
||||
// Can't update fields other than profile.
|
||||
Meteor.users.update(
|
||||
userId3, {$set: {disallowed: true, 'profile.updated': 42}},
|
||||
expect(function (err) {
|
||||
test.isTrue(err);
|
||||
test.equal(err.error, 403);
|
||||
test.isFalse(_.has(Meteor.user(), 'disallowed'));
|
||||
test.isFalse(_.has(Meteor.user().profile, 'updated'));
|
||||
}));
|
||||
},
|
||||
function(test, expect) {
|
||||
// Can't update another user.
|
||||
Meteor.users.update(
|
||||
userId1, {$set: {'profile.updated': 42}},
|
||||
expect(function (err) {
|
||||
test.isTrue(err);
|
||||
test.equal(err.error, 403);
|
||||
}));
|
||||
},
|
||||
function(test, expect) {
|
||||
// Can't update using a non-ID selector. (This one is thrown client-side.)
|
||||
test.throws(function () {
|
||||
Meteor.users.update(
|
||||
{username: username3}, {$set: {'profile.updated': 42}});
|
||||
});
|
||||
test.isFalse(_.has(Meteor.user().profile, 'updated'));
|
||||
},
|
||||
function(test, expect) {
|
||||
// Can update own profile using ID.
|
||||
Meteor.users.update(
|
||||
userId3, {$set: {'profile.updated': 42}},
|
||||
expect(function (err) {
|
||||
test.isFalse(err);
|
||||
test.equal(42, Meteor.user().profile.updated);
|
||||
}));
|
||||
},
|
||||
function(test, expect) {
|
||||
// Test that even with no published fields, we still have a document.
|
||||
Meteor.call('clearUsernameAndProfile', expect(function() {
|
||||
@@ -250,27 +256,221 @@ if (Meteor.isClient) (function () {
|
||||
function(test, expect) {
|
||||
var clientUser = Meteor.user();
|
||||
test.equal(clientUser, null);
|
||||
test.equal(Meteor.userId(), null);
|
||||
Meteor.call('testMeteorUser', expect(function (err, result) {
|
||||
test.equal(err, undefined);
|
||||
test.equal(result, null);
|
||||
}));
|
||||
}
|
||||
]);
|
||||
|
||||
testAsyncMulti("passwords - allow rules", [
|
||||
// create a second user to have an id for in a later test
|
||||
function (test, expect) {
|
||||
this.otherUsername = Random.id();
|
||||
Accounts.createUser(
|
||||
{username: this.otherUsername, password: 'dontcare',
|
||||
testOnCreateUserHook: true},
|
||||
loggedInAs(this.otherUsername, test, expect));
|
||||
},
|
||||
function (test, expect) {
|
||||
this.otherUserId = Meteor.userId();
|
||||
},
|
||||
function (test, expect) {
|
||||
// real setup
|
||||
this.username = Random.id();
|
||||
this.password = 'password';
|
||||
|
||||
Accounts.createUser(
|
||||
{username: this.username, password: this.password,
|
||||
testOnCreateUserHook: true},
|
||||
loggedInAs(this.username, test, expect));
|
||||
},
|
||||
// test the default Meteor.users allow rule. This test properly belongs in
|
||||
// accounts-base/accounts_tests.js, but this is where the tests that
|
||||
// actually log in are.
|
||||
function(test, expect) {
|
||||
this.userId = Meteor.userId();
|
||||
test.notEqual(this.userId, null);
|
||||
test.notEqual(this.userId, this.otherUserId);
|
||||
// Can't update fields other than profile.
|
||||
Meteor.users.update(
|
||||
this.userId, {$set: {disallowed: true, 'profile.updated': 42}},
|
||||
expect(function (err) {
|
||||
test.isTrue(err);
|
||||
test.equal(err.error, 403);
|
||||
test.isFalse(_.has(Meteor.user(), 'disallowed'));
|
||||
test.isFalse(_.has(Meteor.user().profile, 'updated'));
|
||||
}));
|
||||
},
|
||||
function(test, expect) {
|
||||
// Can't update another user.
|
||||
Meteor.users.update(
|
||||
this.otherUserId, {$set: {'profile.updated': 42}},
|
||||
expect(function (err) {
|
||||
test.isTrue(err);
|
||||
test.equal(err.error, 403);
|
||||
}));
|
||||
},
|
||||
function(test, expect) {
|
||||
// Can't update using a non-ID selector. (This one is thrown client-side.)
|
||||
test.throws(function () {
|
||||
Meteor.users.update(
|
||||
{username: this.username}, {$set: {'profile.updated': 42}});
|
||||
});
|
||||
test.isFalse(_.has(Meteor.user().profile, 'updated'));
|
||||
},
|
||||
function(test, expect) {
|
||||
// Can update own profile using ID.
|
||||
Meteor.users.update(
|
||||
this.userId, {$set: {'profile.updated': 42}},
|
||||
expect(function (err) {
|
||||
test.isFalse(err);
|
||||
test.equal(42, Meteor.user().profile.updated);
|
||||
}));
|
||||
},
|
||||
logoutStep
|
||||
]);
|
||||
|
||||
|
||||
testAsyncMulti("passwords - tokens", [
|
||||
function (test, expect) {
|
||||
// setup
|
||||
this.username = Random.id();
|
||||
this.password = 'password';
|
||||
|
||||
Accounts.createUser(
|
||||
{username: this.username, password: this.password},
|
||||
loggedInAs(this.username, test, expect));
|
||||
},
|
||||
|
||||
function(test, expect) {
|
||||
// test logging out invalidates our token
|
||||
var expectLoginError = expect(function (err) {
|
||||
test.isTrue(err);
|
||||
});
|
||||
Meteor.loginWithPassword(username, password2, function (error) {
|
||||
test.equal(error, undefined);
|
||||
test.equal(Meteor.user().username, username);
|
||||
var token = Accounts._storedLoginToken();
|
||||
Meteor.logout(function () {
|
||||
Meteor.loginWithToken(token, expectLoginError);
|
||||
});
|
||||
var token = Accounts._storedLoginToken();
|
||||
test.isTrue(token);
|
||||
Meteor.logout(function () {
|
||||
Meteor.loginWithToken(token, expectLoginError);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
function(test, expect) {
|
||||
var self = this;
|
||||
// Test that login tokens get expired. We should get logged out when a
|
||||
// token expires, and not be able to log in again with the same token.
|
||||
var expectNoError = expect(function (err) {
|
||||
test.isFalse(err);
|
||||
});
|
||||
|
||||
Meteor.loginWithPassword(this.username, this.password, function (error) {
|
||||
self.token = Accounts._storedLoginToken();
|
||||
test.isTrue(self.token);
|
||||
expectNoError(error);
|
||||
Meteor.call("expireTokens");
|
||||
});
|
||||
},
|
||||
waitForLoggedOutStep,
|
||||
function (test, expect) {
|
||||
var token = Accounts._storedLoginToken();
|
||||
test.isFalse(token);
|
||||
},
|
||||
function (test, expect) {
|
||||
// Test that once expireTokens is finished, we can't login again with our
|
||||
// previous token.
|
||||
Meteor.loginWithToken(this.token, expect(function (err, result) {
|
||||
test.isTrue(err);
|
||||
test.equal(Meteor.userId(), null);
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
var self = this;
|
||||
|
||||
// copied from livedata/client_convenience.js
|
||||
var ddpUrl = '/';
|
||||
if (typeof __meteor_runtime_config__ !== "undefined") {
|
||||
if (__meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL)
|
||||
ddpUrl = __meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL;
|
||||
}
|
||||
// XXX can we get the url from the existing connection somehow
|
||||
// instead?
|
||||
|
||||
// Test that Meteor.logoutOtherClients logs out a second authenticated
|
||||
// connection while leaving Meteor.connection logged in.
|
||||
var token;
|
||||
var userId;
|
||||
self.secondConn = DDP.connect(ddpUrl);
|
||||
|
||||
var expectLoginError = expect(function (err) {
|
||||
test.isTrue(err);
|
||||
});
|
||||
var expectValidToken = expect(function (err, result) {
|
||||
test.isFalse(err);
|
||||
test.isTrue(result);
|
||||
self.tokenFromLogoutOthers = result.token;
|
||||
});
|
||||
var expectSecondConnLoggedIn = expect(function (err, result) {
|
||||
test.equal(result.token, token);
|
||||
test.isFalse(err);
|
||||
// This test will fail if an unrelated reconnect triggers before the
|
||||
// connection is logged out. In general our tests aren't resilient to
|
||||
// mid-test reconnects.
|
||||
self.secondConn.onReconnect = function () {
|
||||
self.secondConn.call("login", { resume: token }, expectLoginError);
|
||||
};
|
||||
Meteor.call("logoutOtherClients", expectValidToken);
|
||||
});
|
||||
|
||||
Meteor.loginWithPassword(this.username, this.password, expect(function (err) {
|
||||
test.isFalse(err);
|
||||
token = Accounts._storedLoginToken();
|
||||
self.beforeLogoutOthersToken = token;
|
||||
test.isTrue(token);
|
||||
userId = Meteor.userId();
|
||||
self.secondConn.call("login", { resume: token },
|
||||
expectSecondConnLoggedIn);
|
||||
}));
|
||||
},
|
||||
// Test that logoutOtherClients logged out Meteor.connection and that the
|
||||
// previous token is no longer valid.
|
||||
waitForLoggedOutStep,
|
||||
function (test, expect) {
|
||||
var self = this;
|
||||
var token = Accounts._storedLoginToken();
|
||||
test.isFalse(token);
|
||||
this.secondConn.close();
|
||||
Meteor.loginWithToken(
|
||||
self.beforeLogoutOthersToken,
|
||||
expect(function (err) {
|
||||
test.isTrue(err);
|
||||
test.isFalse(Meteor.userId());
|
||||
})
|
||||
);
|
||||
},
|
||||
// Test that logoutOtherClients returned a new token that we can use to
|
||||
// log in.
|
||||
function (test, expect) {
|
||||
var self = this;
|
||||
Meteor.loginWithToken(
|
||||
self.tokenFromLogoutOthers,
|
||||
expect(function (err) {
|
||||
test.isFalse(err);
|
||||
test.isTrue(Meteor.userId());
|
||||
})
|
||||
);
|
||||
},
|
||||
logoutStep,
|
||||
function (test, expect) {
|
||||
var self = this;
|
||||
// Test that deleting a user logs out that user's connections.
|
||||
Meteor.loginWithPassword(this.username, this.password, function (err) {
|
||||
test.isFalse(err);
|
||||
Meteor.call("removeUser", self.username);
|
||||
});
|
||||
},
|
||||
waitForLoggedOutStep
|
||||
]);
|
||||
|
||||
}) ();
|
||||
|
||||
|
||||
|
||||
@@ -32,8 +32,6 @@ Accounts.config({
|
||||
});
|
||||
|
||||
|
||||
// This test properly belongs in accounts-base/accounts_tests.js, but
|
||||
// this is where the tests that actually log in are.
|
||||
Meteor.methods({
|
||||
testMeteorUser: function () { return Meteor.user(); },
|
||||
clearUsernameAndProfile: function () {
|
||||
@@ -41,5 +39,12 @@ Meteor.methods({
|
||||
throw new Error("Not logged in!");
|
||||
Meteor.users.update(this.userId,
|
||||
{$unset: {profile: 1, username: 1}});
|
||||
},
|
||||
|
||||
expireTokens: function () {
|
||||
Accounts._expireTokens(new Date(), this.userId);
|
||||
},
|
||||
removeUser: function (username) {
|
||||
Meteor.users.remove({ "username": username });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Package.describe({
|
||||
summary: "Simple templates to add login widgets to an app."
|
||||
summary: "Simple templates to add login widgets to an app"
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Package.describe({
|
||||
summary: "API for Persistant Storage, PubSub and Request"
|
||||
summary: "API for Persistent Storage, PubSub and Request"
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
|
||||
@@ -6,6 +6,7 @@ var knownBrowsers = [
|
||||
'android',
|
||||
'chrome',
|
||||
'chromium',
|
||||
'chromeMobileIOS',
|
||||
'firefox',
|
||||
'ie',
|
||||
'mobileSafari',
|
||||
@@ -16,6 +17,7 @@ var browsersEnabledByDefault = [
|
||||
'android',
|
||||
'chrome',
|
||||
'chromium',
|
||||
'chromeMobileIOS',
|
||||
'ie',
|
||||
'mobileSafari',
|
||||
'safari'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Package.describe({
|
||||
summary: "enable the application cache in the browser"
|
||||
summary: "Enable the application cache in the browser"
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
|
||||
1
packages/browser-policy-common/.gitignore
vendored
Normal file
1
packages/browser-policy-common/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.build*
|
||||
27
packages/browser-policy-common/browser-policy-common.js
Normal file
27
packages/browser-policy-common/browser-policy-common.js
Normal file
@@ -0,0 +1,27 @@
|
||||
BrowserPolicy = {};
|
||||
|
||||
var inTest = false;
|
||||
|
||||
BrowserPolicy._runningTest = function () {
|
||||
return inTest;
|
||||
};
|
||||
|
||||
BrowserPolicy._setRunningTest = function () {
|
||||
inTest = true;
|
||||
};
|
||||
|
||||
WebApp.connectHandlers.use(function (req, res, next) {
|
||||
// Never set headers inside tests because they could break other tests.
|
||||
if (BrowserPolicy._runningTest())
|
||||
return next();
|
||||
|
||||
var xFrameOptions = BrowserPolicy.framing &&
|
||||
BrowserPolicy.framing._constructXFrameOptions();
|
||||
var csp = BrowserPolicy.content &&
|
||||
BrowserPolicy.content._constructCsp();
|
||||
if (xFrameOptions)
|
||||
res.setHeader("X-Frame-Options", xFrameOptions);
|
||||
if (csp)
|
||||
res.setHeader("Content-Security-Policy", csp);
|
||||
next();
|
||||
});
|
||||
10
packages/browser-policy-common/package.js
Normal file
10
packages/browser-policy-common/package.js
Normal file
@@ -0,0 +1,10 @@
|
||||
Package.describe({
|
||||
summary: "Common code for browser-policy packages",
|
||||
internal: true
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use('webapp', 'server');
|
||||
api.add_files('browser-policy-common.js', 'server');
|
||||
api.export('BrowserPolicy', 'server');
|
||||
});
|
||||
1
packages/browser-policy-content/.gitignore
vendored
Normal file
1
packages/browser-policy-content/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.build*
|
||||
238
packages/browser-policy-content/browser-policy-content.js
Normal file
238
packages/browser-policy-content/browser-policy-content.js
Normal file
@@ -0,0 +1,238 @@
|
||||
// By adding this package, you get the following default policy:
|
||||
// No eval or other string-to-code, and content can only be loaded from the
|
||||
// same origin as the app (except for XHRs and websocket connections, which can
|
||||
// go to any origin).
|
||||
//
|
||||
// Apps should call BrowserPolicy.content.disallowInlineScripts() if they are
|
||||
// not using any inline script tags and are willing to accept an extra round
|
||||
// trip on page load.
|
||||
//
|
||||
// BrowserPolicy.content functions for tweaking CSP:
|
||||
// allowInlineScripts()
|
||||
// disallowInlineScripts(): adds extra round-trip to page load time
|
||||
// allowInlineStyles()
|
||||
// disallowInlineStyles()
|
||||
// allowEval()
|
||||
// disallowEval()
|
||||
//
|
||||
// For each type of content (script, object, image, media, font, connect,
|
||||
// style), there are the following functions:
|
||||
// allow<content type>Origin(origin): allows the type of content to be loaded
|
||||
// from the given origin
|
||||
// allow<content type>DataUrl(): allows the content to be loaded from data: URLs
|
||||
// allow<content type>SameOrigin(): allows the content to be loaded from the
|
||||
// same origin
|
||||
// disallow<content type>(): disallows this type of content all together (can't
|
||||
// be called for script)
|
||||
//
|
||||
// The following functions allow you to set rules for all types of content at
|
||||
// once:
|
||||
// allowAllContentOrigin(origin)
|
||||
// allowAllContentDataUrl()
|
||||
// allowAllContentSameOrigin()
|
||||
// disallowAllContent()
|
||||
//
|
||||
|
||||
var cspSrcs;
|
||||
var cachedCsp; // Avoid constructing the header out of cspSrcs when possible.
|
||||
|
||||
// CSP keywords have to be single-quoted.
|
||||
var unsafeInline = "'unsafe-inline'";
|
||||
var unsafeEval = "'unsafe-eval'";
|
||||
var selfKeyword = "'self'";
|
||||
var noneKeyword = "'none'";
|
||||
|
||||
BrowserPolicy.content = {};
|
||||
|
||||
var parseCsp = function (csp) {
|
||||
var policies = csp.split("; ");
|
||||
cspSrcs = {};
|
||||
_.each(policies, function (policy) {
|
||||
if (policy[policy.length - 1] === ";")
|
||||
policy = policy.substring(0, policy.length - 1);
|
||||
var srcs = policy.split(" ");
|
||||
var directive = srcs[0];
|
||||
if (_.indexOf(srcs, noneKeyword) !== -1)
|
||||
cspSrcs[directive] = null;
|
||||
else
|
||||
cspSrcs[directive] = srcs.slice(1);
|
||||
});
|
||||
|
||||
if (cspSrcs["default-src"] === undefined)
|
||||
throw new Error("Content Security Policies used with " +
|
||||
"browser-policy must specify a default-src.");
|
||||
|
||||
// Copy default-src sources to other directives.
|
||||
_.each(cspSrcs, function (sources, directive) {
|
||||
cspSrcs[directive] = _.union(sources || [], cspSrcs["default-src"] || []);
|
||||
});
|
||||
};
|
||||
|
||||
var removeCspSrc = function (directive, src) {
|
||||
cspSrcs[directive] = _.without(cspSrcs[directive] || [], src);
|
||||
};
|
||||
|
||||
// Prepare for a change to cspSrcs. Ensure that we have a key in the dictionary
|
||||
// and clear any cached CSP.
|
||||
var prepareForCspDirective = function (directive) {
|
||||
cspSrcs = cspSrcs || {};
|
||||
cachedCsp = null;
|
||||
if (! _.has(cspSrcs, directive))
|
||||
cspSrcs[directive] = _.clone(cspSrcs["default-src"]);
|
||||
};
|
||||
|
||||
var setDefaultPolicy = function () {
|
||||
// By default, unsafe inline scripts and styles are allowed, since we expect
|
||||
// many apps will use them for analytics, etc. Unsafe eval is disallowed, and
|
||||
// the only allowable content source is the same origin or data, except for
|
||||
// connect which allows anything (since meteor.com apps make websocket
|
||||
// connections to a lot of different origins).
|
||||
BrowserPolicy.content.setPolicy("default-src 'self'; " +
|
||||
"script-src 'self' 'unsafe-inline'; " +
|
||||
"connect-src *; " +
|
||||
"img-src data: 'self'; " +
|
||||
"style-src 'self' 'unsafe-inline';");
|
||||
};
|
||||
|
||||
var setWebAppInlineScripts = function (value) {
|
||||
if (! BrowserPolicy._runningTest())
|
||||
WebAppInternals.setInlineScriptsAllowed(value);
|
||||
};
|
||||
|
||||
_.extend(BrowserPolicy.content, {
|
||||
// Exported for tests and browser-policy-common.
|
||||
_constructCsp: function () {
|
||||
if (! cspSrcs || _.isEmpty(cspSrcs))
|
||||
return null;
|
||||
|
||||
if (cachedCsp)
|
||||
return cachedCsp;
|
||||
|
||||
var header = _.map(cspSrcs, function (srcs, directive) {
|
||||
srcs = srcs || [];
|
||||
if (_.isEmpty(srcs))
|
||||
srcs = [noneKeyword];
|
||||
var directiveCsp = _.uniq(srcs).join(" ");
|
||||
return directive + " " + directiveCsp + ";";
|
||||
});
|
||||
|
||||
header = header.join(" ");
|
||||
cachedCsp = header;
|
||||
return header;
|
||||
},
|
||||
_reset: function () {
|
||||
cachedCsp = null;
|
||||
setDefaultPolicy();
|
||||
},
|
||||
|
||||
setPolicy: function (csp) {
|
||||
cachedCsp = null;
|
||||
parseCsp(csp);
|
||||
setWebAppInlineScripts(
|
||||
BrowserPolicy.content._keywordAllowed("script-src", unsafeInline)
|
||||
);
|
||||
},
|
||||
|
||||
_keywordAllowed: function (directive, keyword) {
|
||||
return (cspSrcs[directive] &&
|
||||
_.indexOf(cspSrcs[directive], keyword) !== -1);
|
||||
},
|
||||
|
||||
// Helpers for creating content security policies
|
||||
|
||||
allowInlineScripts: function () {
|
||||
prepareForCspDirective("script-src");
|
||||
cspSrcs["script-src"].push(unsafeInline);
|
||||
setWebAppInlineScripts(true);
|
||||
},
|
||||
disallowInlineScripts: function () {
|
||||
prepareForCspDirective("script-src");
|
||||
removeCspSrc("script-src", unsafeInline);
|
||||
setWebAppInlineScripts(false);
|
||||
},
|
||||
allowEval: function () {
|
||||
prepareForCspDirective("script-src");
|
||||
cspSrcs["script-src"].push(unsafeEval);
|
||||
},
|
||||
disallowEval: function () {
|
||||
prepareForCspDirective("script-src");
|
||||
removeCspSrc("script-src", unsafeEval);
|
||||
},
|
||||
allowInlineStyles: function () {
|
||||
prepareForCspDirective("style-src");
|
||||
cspSrcs["style-src"].push(unsafeInline);
|
||||
},
|
||||
disallowInlineStyles: function () {
|
||||
prepareForCspDirective("style-src");
|
||||
removeCspSrc("style-src", unsafeInline);
|
||||
},
|
||||
|
||||
// Functions for setting defaults
|
||||
allowSameOriginForAll: function () {
|
||||
BrowserPolicy.content.allowOriginForAll(selfKeyword);
|
||||
},
|
||||
allowDataUrlForAll: function () {
|
||||
BrowserPolicy.content.allowOriginForAll("data:");
|
||||
},
|
||||
allowOriginForAll: function (origin) {
|
||||
prepareForCspDirective("default-src");
|
||||
_.each(_.keys(cspSrcs), function (directive) {
|
||||
cspSrcs[directive].push(origin);
|
||||
});
|
||||
},
|
||||
disallowAll: function () {
|
||||
cachedCsp = null;
|
||||
cspSrcs = {
|
||||
"default-src": []
|
||||
};
|
||||
setWebAppInlineScripts(false);
|
||||
}
|
||||
});
|
||||
|
||||
// allow<Resource>Origin, allow<Resource>Data, allow<Resource>self, and
|
||||
// disallow<Resource> methods for each type of resource.
|
||||
_.each(["script", "object", "img", "media",
|
||||
"font", "connect", "style"],
|
||||
function (resource) {
|
||||
var directive = resource + "-src";
|
||||
var methodResource;
|
||||
if (resource !== "img") {
|
||||
methodResource = resource.charAt(0).toUpperCase() +
|
||||
resource.slice(1);
|
||||
} else {
|
||||
methodResource = "Image";
|
||||
}
|
||||
var allowMethodName = "allow" + methodResource + "Origin";
|
||||
var disallowMethodName = "disallow" + methodResource;
|
||||
var allowDataMethodName = "allow" + methodResource + "DataUrl";
|
||||
var allowSelfMethodName = "allow" + methodResource + "SameOrigin";
|
||||
|
||||
var disallow = function () {
|
||||
cachedCsp = null;
|
||||
cspSrcs[directive] = [];
|
||||
};
|
||||
|
||||
BrowserPolicy.content[allowMethodName] = function (src) {
|
||||
prepareForCspDirective(directive);
|
||||
cspSrcs[directive].push(src);
|
||||
};
|
||||
if (resource === "script") {
|
||||
BrowserPolicy.content[disallowMethodName] = function () {
|
||||
disallow();
|
||||
setWebAppInlineScripts(false);
|
||||
};
|
||||
} else {
|
||||
BrowserPolicy.content[disallowMethodName] = disallow;
|
||||
}
|
||||
BrowserPolicy.content[allowDataMethodName] = function () {
|
||||
prepareForCspDirective(directive);
|
||||
cspSrcs[directive].push("data:");
|
||||
};
|
||||
BrowserPolicy.content[allowSelfMethodName] = function () {
|
||||
prepareForCspDirective(directive);
|
||||
cspSrcs[directive].push(selfKeyword);
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
setDefaultPolicy();
|
||||
9
packages/browser-policy-content/package.js
Normal file
9
packages/browser-policy-content/package.js
Normal file
@@ -0,0 +1,9 @@
|
||||
Package.describe({
|
||||
summary: "Configure content security policies"
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.imply(["browser-policy-common"], "server");
|
||||
api.add_files("browser-policy-content.js", "server");
|
||||
api.use(["underscore", "browser-policy-common", "webapp"], "server");
|
||||
});
|
||||
1
packages/browser-policy-framing/.gitignore
vendored
Normal file
1
packages/browser-policy-framing/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.build*
|
||||
39
packages/browser-policy-framing/browser-policy-framing.js
Normal file
39
packages/browser-policy-framing/browser-policy-framing.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// By adding this package, you get a default policy where only web pages on the
|
||||
// same origin as your app can frame your app.
|
||||
//
|
||||
// For controlling which origins can frame this app,
|
||||
// BrowserPolicy.framing.disallow()
|
||||
// BrowserPolicy.framing.restrictToOrigin(origin)
|
||||
// BrowserPolicy.framing.allowByAnyOrigin()
|
||||
|
||||
var defaultXFrameOptions = "SAMEORIGIN";
|
||||
var xFrameOptions = defaultXFrameOptions;
|
||||
|
||||
BrowserPolicy.framing = {};
|
||||
|
||||
_.extend(BrowserPolicy.framing, {
|
||||
// Exported for tests and browser-policy-common.
|
||||
_constructXFrameOptions: function () {
|
||||
return xFrameOptions;
|
||||
},
|
||||
_reset: function () {
|
||||
xFrameOptions = defaultXFrameOptions;
|
||||
},
|
||||
|
||||
disallow: function () {
|
||||
xFrameOptions = "DENY";
|
||||
},
|
||||
// ALLOW-FROM not supported in Chrome or Safari.
|
||||
restrictToOrigin: function (origin) {
|
||||
// Trying to specify two allow-from throws to prevent users from
|
||||
// accidentally overwriting an allow-from origin when they think they are
|
||||
// adding multiple origins.
|
||||
if (xFrameOptions && xFrameOptions.indexOf("ALLOW-FROM") === 0)
|
||||
throw new Error("You can only specify one origin that is allowed to" +
|
||||
" frame this app.");
|
||||
xFrameOptions = "ALLOW-FROM " + origin;
|
||||
},
|
||||
allowAll: function () {
|
||||
xFrameOptions = null;
|
||||
}
|
||||
});
|
||||
9
packages/browser-policy-framing/package.js
Normal file
9
packages/browser-policy-framing/package.js
Normal file
@@ -0,0 +1,9 @@
|
||||
Package.describe({
|
||||
summary: "Restrict which websites can frame your app"
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.imply(["browser-policy-common"], "server");
|
||||
api.use(["underscore", "browser-policy-common"], "server");
|
||||
api.add_files("browser-policy-framing.js", "server");
|
||||
});
|
||||
1
packages/browser-policy/.gitignore
vendored
Normal file
1
packages/browser-policy/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.build*
|
||||
129
packages/browser-policy/browser-policy-test.js
Normal file
129
packages/browser-policy/browser-policy-test.js
Normal file
@@ -0,0 +1,129 @@
|
||||
BrowserPolicy._setRunningTest();
|
||||
|
||||
var cspsEqual = function (csp1, csp2) {
|
||||
var cspToObj = function (csp) {
|
||||
csp = csp.substring(0, csp.length - 1);
|
||||
var parts = _.map(csp.split("; "), function (part) {
|
||||
return part.split(" ");
|
||||
});
|
||||
var keys = _.map(parts, _.first);
|
||||
var values = _.map(parts, _.rest);
|
||||
_.each(values, function (value) {
|
||||
value.sort();
|
||||
});
|
||||
return _.object(keys, values);
|
||||
};
|
||||
|
||||
return EJSON.equals(cspToObj(csp1), cspToObj(csp2));
|
||||
};
|
||||
|
||||
// It's important to call _reset() at the beginnning of these tests; otherwise
|
||||
// the headers left over at the end of the last test run will be used.
|
||||
|
||||
Tinytest.add("browser-policy - csp", function (test) {
|
||||
var defaultCsp = "default-src 'self'; script-src 'self' 'unsafe-inline'; " +
|
||||
"connect-src * 'self'; img-src data: 'self'; style-src 'self' 'unsafe-inline';"
|
||||
|
||||
BrowserPolicy.content._reset();
|
||||
// Default policy
|
||||
test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(), defaultCsp));
|
||||
test.isTrue(BrowserPolicy.content._keywordAllowed("script-src", "'unsafe-inline'"));
|
||||
|
||||
// Redundant whitelisting (inline scripts already allowed in default policy)
|
||||
BrowserPolicy.content.allowInlineScripts();
|
||||
test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(), defaultCsp));
|
||||
|
||||
// Disallow inline scripts
|
||||
BrowserPolicy.content.disallowInlineScripts();
|
||||
test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(),
|
||||
"default-src 'self'; script-src 'self'; " +
|
||||
"connect-src * 'self'; img-src data: 'self'; style-src 'self' 'unsafe-inline';"));
|
||||
test.isFalse(BrowserPolicy.content._keywordAllowed("script-src", "'unsafe-inline'"));
|
||||
|
||||
// Allow eval
|
||||
BrowserPolicy.content.allowEval();
|
||||
test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(), "default-src 'self'; script-src 'self' 'unsafe-eval'; " +
|
||||
"connect-src * 'self'; img-src data: 'self'; style-src 'self' 'unsafe-inline';"));
|
||||
|
||||
// Disallow inline styles
|
||||
BrowserPolicy.content.disallowInlineStyles();
|
||||
test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(), "default-src 'self'; script-src 'self' 'unsafe-eval'; " +
|
||||
"connect-src * 'self'; img-src data: 'self'; style-src 'self';"));
|
||||
|
||||
// Allow data: urls everywhere
|
||||
BrowserPolicy.content.allowDataUrlForAll();
|
||||
test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(),
|
||||
"default-src 'self' data:; script-src 'self' 'unsafe-eval' data:; " +
|
||||
"connect-src * data: 'self'; img-src data: 'self'; style-src 'self' data:;"));
|
||||
|
||||
// Disallow everything
|
||||
BrowserPolicy.content.disallowAll();
|
||||
test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(), "default-src 'none';"));
|
||||
test.isFalse(BrowserPolicy.content._keywordAllowed("script-src", "'unsafe-inline'"));
|
||||
|
||||
// Put inline scripts back in
|
||||
BrowserPolicy.content.allowInlineScripts();
|
||||
test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(),
|
||||
"default-src 'none'; script-src 'unsafe-inline';"));
|
||||
test.isTrue(BrowserPolicy.content._keywordAllowed("script-src", "'unsafe-inline'"));
|
||||
|
||||
// Add 'self' to all content types
|
||||
BrowserPolicy.content.allowSameOriginForAll();
|
||||
test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(),
|
||||
"default-src 'self'; script-src 'self' 'unsafe-inline';"));
|
||||
test.isTrue(BrowserPolicy.content._keywordAllowed("script-src", "'unsafe-inline'"));
|
||||
|
||||
// Disallow all content except same-origin scripts
|
||||
BrowserPolicy.content.disallowAll();
|
||||
BrowserPolicy.content.allowScriptSameOrigin();
|
||||
test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(),
|
||||
"default-src 'none'; script-src 'self';"));
|
||||
test.isFalse(BrowserPolicy.content._keywordAllowed("script-src", "'unsafe-inline'"));
|
||||
|
||||
// Starting with all content same origin, disallowScript() and then allow
|
||||
// inline scripts. Result should be that that only inline scripts can execute,
|
||||
// not same-origin scripts.
|
||||
BrowserPolicy.content.disallowAll();
|
||||
BrowserPolicy.content.allowSameOriginForAll();
|
||||
test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(), "default-src 'self';"));
|
||||
BrowserPolicy.content.disallowScript();
|
||||
test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(),
|
||||
"default-src 'self'; script-src 'none';"));
|
||||
test.isFalse(BrowserPolicy.content._keywordAllowed("script-src", "'unsafe-inline'"));
|
||||
BrowserPolicy.content.allowInlineScripts();
|
||||
test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(),
|
||||
"default-src 'self'; script-src 'unsafe-inline';"));
|
||||
test.isTrue(BrowserPolicy.content._keywordAllowed("script-src", "'unsafe-inline'"));
|
||||
|
||||
// Starting with all content same origin, allow inline scripts. (Should result
|
||||
// in both same origin and inline scripts allowed.)
|
||||
BrowserPolicy.content.disallowAll();
|
||||
BrowserPolicy.content.allowSameOriginForAll();
|
||||
BrowserPolicy.content.allowInlineScripts();
|
||||
test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(),
|
||||
"default-src 'self'; script-src 'self' 'unsafe-inline';"));
|
||||
BrowserPolicy.content.disallowInlineScripts();
|
||||
test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(),
|
||||
"default-src 'self'; script-src 'self';"));
|
||||
|
||||
// Allow same origin for all content, then disallow object entirely.
|
||||
BrowserPolicy.content.disallowAll();
|
||||
BrowserPolicy.content.allowSameOriginForAll();
|
||||
BrowserPolicy.content.disallowObject();
|
||||
test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(),
|
||||
"default-src 'self'; object-src 'none';"));
|
||||
});
|
||||
|
||||
Tinytest.add("browser-policy - x-frame-options", function (test) {
|
||||
BrowserPolicy.framing._reset();
|
||||
test.equal(BrowserPolicy.framing._constructXFrameOptions(), "SAMEORIGIN");
|
||||
BrowserPolicy.framing.disallow();
|
||||
test.equal(BrowserPolicy.framing._constructXFrameOptions(), "DENY");
|
||||
BrowserPolicy.framing.allowAll();
|
||||
test.equal(BrowserPolicy.framing._constructXFrameOptions(), null);
|
||||
BrowserPolicy.framing.restrictToOrigin("foo.com");
|
||||
test.equal(BrowserPolicy.framing._constructXFrameOptions(), "ALLOW-FROM foo.com");
|
||||
test.throws(function () {
|
||||
BrowserPolicy.framing.restrictToOrigin("bar.com");
|
||||
});
|
||||
});
|
||||
13
packages/browser-policy/package.js
Normal file
13
packages/browser-policy/package.js
Normal file
@@ -0,0 +1,13 @@
|
||||
Package.describe({
|
||||
summary: "Configure security policies enforced by the browser"
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use(['browser-policy-content', 'browser-policy-framing'], 'server');
|
||||
api.imply(['browser-policy-common'], 'server');
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
api.use(["tinytest", "browser-policy", "ejson"], "server");
|
||||
api.add_files("browser-policy-test.js", "server");
|
||||
});
|
||||
@@ -3,9 +3,10 @@ Tinytest.add("coffeescript - presence", function(test) {
|
||||
});
|
||||
Tinytest.add("literate coffeescript - presence", function(test) {
|
||||
test.isTrue(Meteor.__LITCOFFEESCRIPT_PRESENT);
|
||||
test.isTrue(Meteor.__COFFEEMDSCRIPT_PRESENT);
|
||||
});
|
||||
|
||||
Tinytest.add("coffeescript - exported variable", function(test) {
|
||||
test.equal(COFFEESCRIPT_EXPORTED, 123);
|
||||
test.equal(Package['coffeescript-test-helper'].COFFEESCRIPT_EXPORTED, 123);
|
||||
});
|
||||
});
|
||||
@@ -16,9 +16,10 @@ Package.on_test(function (api) {
|
||||
api.use(['coffeescript-test-helper'], ['client', 'server']);
|
||||
api.add_files([
|
||||
'coffeescript_test_setup.js',
|
||||
'coffeescript_tests.coffee',
|
||||
'coffeescript_strict_tests.coffee',
|
||||
'litcoffeescript_tests.litcoffee',
|
||||
'tests/coffeescript_tests.coffee',
|
||||
'tests/coffeescript_strict_tests.coffee',
|
||||
'tests/litcoffeescript_tests.litcoffee',
|
||||
'tests/litcoffeescript_tests.coffee.md',
|
||||
'coffeescript_tests.js'
|
||||
], ['client', 'server']);
|
||||
});
|
||||
|
||||
@@ -113,13 +113,14 @@ var addSharedHeader = function (source, sourceMap) {
|
||||
};
|
||||
};
|
||||
|
||||
var handler = function (compileStep) {
|
||||
var handler = function (compileStep, isLiterate) {
|
||||
var source = compileStep.read().toString('utf8');
|
||||
var outputFile = compileStep.inputPath + ".js";
|
||||
|
||||
var options = {
|
||||
bare: true,
|
||||
filename: compileStep.inputPath,
|
||||
literate: path.extname(compileStep.inputPath) === '.litcoffee',
|
||||
literate: !!isLiterate,
|
||||
// Return a source map.
|
||||
sourceMap: true,
|
||||
// Include the original source in the source map (sourcesContent field).
|
||||
@@ -152,6 +153,11 @@ var handler = function (compileStep) {
|
||||
});
|
||||
};
|
||||
|
||||
Plugin.registerSourceHandler("coffee", handler);
|
||||
Plugin.registerSourceHandler("litcoffee", handler);
|
||||
var literateHandler = function (compileStep) {
|
||||
return handler(compileStep, true);
|
||||
}
|
||||
|
||||
Plugin.registerSourceHandler("coffee", handler);
|
||||
Plugin.registerSourceHandler("litcoffee", literateHandler);
|
||||
Plugin.registerSourceHandler("coffee.md", literateHandler);
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
This file is just the same as `coffeescript_tests.coffee`, first we set a
|
||||
property, which we check for in `coffeescript_tests.js`, and then a trivial
|
||||
testcase.
|
||||
|
||||
Meteor.__COFFEEMDSCRIPT_PRESENT = true
|
||||
Tinytest.add "markdown coffeescript - compile", (test) -> test.isTrue true
|
||||
@@ -1,10 +1,13 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"optimist": {
|
||||
"version": "0.4.0",
|
||||
"version": "0.6.0",
|
||||
"dependencies": {
|
||||
"wordwrap": {
|
||||
"version": "0.0.2"
|
||||
},
|
||||
"minimist": {
|
||||
"version": "0.0.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
Package.describe({
|
||||
summary: "Helpers for control programs"
|
||||
summary: "Helpers for control programs",
|
||||
internal: true
|
||||
});
|
||||
|
||||
Npm.depends({optimist: '0.4.0'});
|
||||
Npm.depends({optimist: '0.6.0'});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use(['underscore', 'livedata', 'mongo-livedata', 'follower-livedata'], 'server');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
Package.describe({
|
||||
summary: "Default control program for an application"
|
||||
summary: "Default control program for an application",
|
||||
internal: true
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
|
||||
2
packages/d3/package.js
vendored
2
packages/d3/package.js
vendored
@@ -1,5 +1,5 @@
|
||||
Package.describe({
|
||||
summary: "Library for manipulating documents based on data."
|
||||
summary: "Library for manipulating documents based on data"
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
|
||||
@@ -18,6 +18,10 @@ EJSON.addType = function (name, factory) {
|
||||
customTypes[name] = factory;
|
||||
};
|
||||
|
||||
var isInfOrNan = function (obj) {
|
||||
return _.isNaN(obj) || obj === Infinity || obj === -Infinity;
|
||||
};
|
||||
|
||||
var builtinConverters = [
|
||||
{ // Date
|
||||
matchJSONValue: function (obj) {
|
||||
@@ -33,6 +37,26 @@ var builtinConverters = [
|
||||
return new Date(obj.$date);
|
||||
}
|
||||
},
|
||||
{ // NaN, Inf, -Inf. (These are the only objects with typeof !== 'object'
|
||||
// which we match.)
|
||||
matchJSONValue: function (obj) {
|
||||
return _.has(obj, '$InfNaN') && _.size(obj) === 1;
|
||||
},
|
||||
matchObject: isInfOrNan,
|
||||
toJSONValue: function (obj) {
|
||||
var sign;
|
||||
if (_.isNaN(obj))
|
||||
sign = 0;
|
||||
else if (obj === Infinity)
|
||||
sign = 1;
|
||||
else
|
||||
sign = -1;
|
||||
return {$InfNaN: sign};
|
||||
},
|
||||
fromJSONValue: function (obj) {
|
||||
return obj.$InfNaN/0;
|
||||
}
|
||||
},
|
||||
{ // Binary
|
||||
matchJSONValue: function (obj) {
|
||||
return _.has(obj, '$binary') && _.size(obj) === 1;
|
||||
@@ -104,14 +128,23 @@ EJSON._isCustomType = function (obj) {
|
||||
// for both arrays and objects, in-place modification.
|
||||
var adjustTypesToJSONValue =
|
||||
EJSON._adjustTypesToJSONValue = function (obj) {
|
||||
// Is it an atom that we need to adjust?
|
||||
if (obj === null)
|
||||
return null;
|
||||
var maybeChanged = toJSONValueHelper(obj);
|
||||
if (maybeChanged !== undefined)
|
||||
return maybeChanged;
|
||||
|
||||
// Other atoms are unchanged.
|
||||
if (typeof obj !== 'object')
|
||||
return obj;
|
||||
|
||||
// Iterate over array or object structure.
|
||||
_.each(obj, function (value, key) {
|
||||
if (typeof value !== 'object' && value !== undefined)
|
||||
if (typeof value !== 'object' && value !== undefined &&
|
||||
!isInfOrNan(value))
|
||||
return; // continue
|
||||
|
||||
var changed = toJSONValueHelper(value);
|
||||
if (changed) {
|
||||
obj[key] = changed;
|
||||
@@ -158,6 +191,11 @@ EJSON._adjustTypesFromJSONValue = function (obj) {
|
||||
var maybeChanged = fromJSONValueHelper(obj);
|
||||
if (maybeChanged !== obj)
|
||||
return maybeChanged;
|
||||
|
||||
// Other atoms are unchanged.
|
||||
if (typeof obj !== 'object')
|
||||
return obj;
|
||||
|
||||
_.each(obj, function (value, key) {
|
||||
if (typeof value === 'object') {
|
||||
var changed = fromJSONValueHelper(value);
|
||||
@@ -206,11 +244,18 @@ EJSON.fromJSONValue = function (item) {
|
||||
}
|
||||
};
|
||||
|
||||
EJSON.stringify = function (item) {
|
||||
return JSON.stringify(EJSON.toJSONValue(item));
|
||||
EJSON.stringify = function (item, options) {
|
||||
var json = EJSON.toJSONValue(item);
|
||||
if (options && (options.canonical || options.indent)) {
|
||||
return EJSON._canonicalStringify(json, options);
|
||||
} else {
|
||||
return JSON.stringify(json);
|
||||
}
|
||||
};
|
||||
|
||||
EJSON.parse = function (item) {
|
||||
if (typeof item !== 'string')
|
||||
throw new Error("EJSON.parse argument should be a string");
|
||||
return EJSON.fromJSONValue(JSON.parse(item));
|
||||
};
|
||||
|
||||
@@ -224,6 +269,9 @@ EJSON.equals = function (a, b, options) {
|
||||
var keyOrderSensitive = !!(options && options.keyOrderSensitive);
|
||||
if (a === b)
|
||||
return true;
|
||||
if (_.isNaN(a) && _.isNaN(b))
|
||||
return true; // This differs from the IEEE spec for NaN equality, b/c we don't want
|
||||
// anything ever with a NaN to be poisoned from becoming equal to anything.
|
||||
if (!a || !b) // if either one is falsy, they'd have to be === to be equal
|
||||
return false;
|
||||
if (!(typeof a === 'object' && typeof b === 'object'))
|
||||
@@ -305,6 +353,7 @@ EJSON.clone = function (v) {
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
// XXX: Use something better than underscore's isArray
|
||||
if (_.isArray(v) || _.isArguments(v)) {
|
||||
// For some reason, _.map doesn't work in this context on Opera (weird test
|
||||
// failures).
|
||||
|
||||
@@ -52,6 +52,31 @@ Tinytest.add("ejson - equality and falsiness", function (test) {
|
||||
test.isFalse(EJSON.equals({foo: "foo"}, undefined));
|
||||
});
|
||||
|
||||
Tinytest.add("ejson - NaN and Inf", function (test) {
|
||||
test.equal(EJSON.parse("{\"$InfNaN\": 1}"), Infinity);
|
||||
test.equal(EJSON.parse("{\"$InfNaN\": -1}"), -Infinity);
|
||||
test.isTrue(_.isNaN(EJSON.parse("{\"$InfNaN\": 0}")));
|
||||
test.equal(EJSON.parse(EJSON.stringify(Infinity)), Infinity);
|
||||
test.equal(EJSON.parse(EJSON.stringify(-Infinity)), -Infinity);
|
||||
test.isTrue(_.isNaN(EJSON.parse(EJSON.stringify(NaN))));
|
||||
test.isTrue(EJSON.equals(NaN, NaN));
|
||||
test.isTrue(EJSON.equals(Infinity, Infinity));
|
||||
test.isTrue(EJSON.equals(-Infinity, -Infinity));
|
||||
test.isFalse(EJSON.equals(Infinity, -Infinity));
|
||||
test.isFalse(EJSON.equals(Infinity, NaN));
|
||||
test.isFalse(EJSON.equals(Infinity, 0));
|
||||
test.isFalse(EJSON.equals(NaN, 0));
|
||||
|
||||
test.isTrue(EJSON.equals(
|
||||
EJSON.parse("{\"a\": {\"$InfNaN\": 1}}"),
|
||||
{a: Infinity}
|
||||
));
|
||||
test.isTrue(EJSON.equals(
|
||||
EJSON.parse("{\"a\": {\"$InfNaN\": 0}}"),
|
||||
{a: NaN}
|
||||
));
|
||||
});
|
||||
|
||||
Tinytest.add("ejson - clone", function (test) {
|
||||
var cloneTest = function (x, identical) {
|
||||
var y = EJSON.clone(x);
|
||||
@@ -72,3 +97,90 @@ Tinytest.add("ejson - clone", function (test) {
|
||||
};
|
||||
testCloneArgs(1, 2, "foo", [4]);
|
||||
});
|
||||
|
||||
Tinytest.add("ejson - stringify", function (test) {
|
||||
test.equal(EJSON.stringify(null), "null");
|
||||
test.equal(EJSON.stringify(true), "true");
|
||||
test.equal(EJSON.stringify(false), "false");
|
||||
test.equal(EJSON.stringify(123), "123");
|
||||
test.equal(EJSON.stringify("abc"), "\"abc\"");
|
||||
|
||||
test.equal(EJSON.stringify([1, 2, 3]),
|
||||
"[1,2,3]"
|
||||
);
|
||||
test.equal(EJSON.stringify([1, 2, 3], {indent: true}),
|
||||
"[\n 1,\n 2,\n 3\n]"
|
||||
);
|
||||
test.equal(EJSON.stringify([1, 2, 3], {canonical: false}),
|
||||
"[1,2,3]"
|
||||
);
|
||||
test.equal(EJSON.stringify([1, 2, 3], {indent: true, canonical: false}),
|
||||
"[\n 1,\n 2,\n 3\n]"
|
||||
);
|
||||
|
||||
test.equal(EJSON.stringify([1, 2, 3], {indent: 4}),
|
||||
"[\n 1,\n 2,\n 3\n]"
|
||||
);
|
||||
test.equal(EJSON.stringify([1, 2, 3], {indent: '--'}),
|
||||
"[\n--1,\n--2,\n--3\n]"
|
||||
);
|
||||
|
||||
test.equal(
|
||||
EJSON.stringify(
|
||||
{b: [2, {d: 4, c: 3}], a: 1},
|
||||
{canonical: true}
|
||||
),
|
||||
"{\"a\":1,\"b\":[2,{\"c\":3,\"d\":4}]}"
|
||||
);
|
||||
test.equal(
|
||||
EJSON.stringify(
|
||||
{b: [2, {d: 4, c: 3}], a: 1},
|
||||
{
|
||||
indent: true,
|
||||
canonical: true
|
||||
}
|
||||
),
|
||||
"{\n" +
|
||||
" \"a\": 1,\n" +
|
||||
" \"b\": [\n" +
|
||||
" 2,\n" +
|
||||
" {\n" +
|
||||
" \"c\": 3,\n" +
|
||||
" \"d\": 4\n" +
|
||||
" }\n" +
|
||||
" ]\n" +
|
||||
"}"
|
||||
);
|
||||
test.equal(
|
||||
EJSON.stringify(
|
||||
{b: [2, {d: 4, c: 3}], a: 1},
|
||||
{canonical: false}
|
||||
),
|
||||
"{\"b\":[2,{\"d\":4,\"c\":3}],\"a\":1}"
|
||||
);
|
||||
test.equal(
|
||||
EJSON.stringify(
|
||||
{b: [2, {d: 4, c: 3}], a: 1},
|
||||
{indent: true, canonical: false}
|
||||
),
|
||||
"{\n" +
|
||||
" \"b\": [\n" +
|
||||
" 2,\n" +
|
||||
" {\n" +
|
||||
" \"d\": 4,\n" +
|
||||
" \"c\": 3\n" +
|
||||
" }\n" +
|
||||
" ],\n" +
|
||||
" \"a\": 1\n" +
|
||||
"}"
|
||||
|
||||
);
|
||||
});
|
||||
|
||||
Tinytest.add("ejson - parse", function (test) {
|
||||
test.equal(EJSON.parse("[1,2,3]"), [1,2,3]);
|
||||
test.throws(
|
||||
function () { EJSON.parse(null) },
|
||||
/argument should be a string/
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ Package.on_use(function (api) {
|
||||
api.export('EJSON');
|
||||
api.export('EJSONTest', {testOnly: true});
|
||||
api.add_files('ejson.js', ['client', 'server']);
|
||||
api.add_files('stringify.js', ['client', 'server']);
|
||||
api.add_files('base64.js', ['client', 'server']);
|
||||
});
|
||||
|
||||
|
||||
118
packages/ejson/stringify.js
Normal file
118
packages/ejson/stringify.js
Normal file
@@ -0,0 +1,118 @@
|
||||
// Based on json2.js from https://github.com/douglascrockford/JSON-js
|
||||
//
|
||||
// json2.js
|
||||
// 2012-10-08
|
||||
//
|
||||
// Public Domain.
|
||||
//
|
||||
// NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
|
||||
|
||||
function quote(string) {
|
||||
return JSON.stringify(string);
|
||||
}
|
||||
|
||||
var str = function (key, holder, singleIndent, outerIndent, canonical) {
|
||||
|
||||
// Produce a string from holder[key].
|
||||
|
||||
var i; // The loop counter.
|
||||
var k; // The member key.
|
||||
var v; // The member value.
|
||||
var length;
|
||||
var innerIndent = outerIndent;
|
||||
var partial;
|
||||
var value = holder[key];
|
||||
|
||||
// What happens next depends on the value's type.
|
||||
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
return quote(value);
|
||||
case 'number':
|
||||
// JSON numbers must be finite. Encode non-finite numbers as null.
|
||||
return isFinite(value) ? String(value) : 'null';
|
||||
case 'boolean':
|
||||
return String(value);
|
||||
// If the type is 'object', we might be dealing with an object or an array or
|
||||
// null.
|
||||
case 'object':
|
||||
// Due to a specification blunder in ECMAScript, typeof null is 'object',
|
||||
// so watch out for that case.
|
||||
if (!value) {
|
||||
return 'null';
|
||||
}
|
||||
// Make an array to hold the partial results of stringifying this object value.
|
||||
innerIndent = outerIndent + singleIndent;
|
||||
partial = [];
|
||||
|
||||
// Is the value an array?
|
||||
if (_.isArray(value) || _.isArguments(value)) {
|
||||
|
||||
// The value is an array. Stringify every element. Use null as a placeholder
|
||||
// for non-JSON values.
|
||||
|
||||
length = value.length;
|
||||
for (i = 0; i < length; i += 1) {
|
||||
partial[i] = str(i, value, singleIndent, innerIndent, canonical) || 'null';
|
||||
}
|
||||
|
||||
// Join all of the elements together, separated with commas, and wrap them in
|
||||
// brackets.
|
||||
|
||||
if (partial.length === 0) {
|
||||
v = '[]';
|
||||
} else if (innerIndent) {
|
||||
v = '[\n' + innerIndent + partial.join(',\n' + innerIndent) + '\n' + outerIndent + ']';
|
||||
} else {
|
||||
v = '[' + partial.join(',') + ']';
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
|
||||
// Iterate through all of the keys in the object.
|
||||
var keys = _.keys(value);
|
||||
if (canonical)
|
||||
keys = keys.sort();
|
||||
_.each(keys, function (k) {
|
||||
v = str(k, value, singleIndent, innerIndent, canonical);
|
||||
if (v) {
|
||||
partial.push(quote(k) + (innerIndent ? ': ' : ':') + v);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Join all of the member texts together, separated with commas,
|
||||
// and wrap them in braces.
|
||||
|
||||
if (partial.length === 0) {
|
||||
v = '{}';
|
||||
} else if (innerIndent) {
|
||||
v = '{\n' + innerIndent + partial.join(',\n' + innerIndent) + '\n' + outerIndent + '}';
|
||||
} else {
|
||||
v = '{' + partial.join(',') + '}';
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
// If the JSON object does not yet have a stringify method, give it one.
|
||||
|
||||
EJSON._canonicalStringify = function (value, options) {
|
||||
// Make a fake root object containing our value under the key of ''.
|
||||
// Return the result of stringifying the value.
|
||||
options = _.extend({
|
||||
indent: "",
|
||||
canonical: false
|
||||
}, options);
|
||||
if (options.indent === true) {
|
||||
options.indent = " ";
|
||||
} else if (typeof options.indent === 'number') {
|
||||
var newIndent = "";
|
||||
for (var i = 0; i < options.indent; i++) {
|
||||
newIndent += ' ';
|
||||
}
|
||||
options.indent = newIndent;
|
||||
}
|
||||
return str('', {'': value}, options.indent, "", options.canonical);
|
||||
};
|
||||
4
packages/email/.npm/package/npm-shrinkwrap.json
generated
4
packages/email/.npm/package/npm-shrinkwrap.json
generated
@@ -9,7 +9,7 @@
|
||||
}
|
||||
},
|
||||
"simplesmtp": {
|
||||
"version": "0.1.25",
|
||||
"version": "0.3.10",
|
||||
"dependencies": {
|
||||
"rai": {
|
||||
"version": "0.1.7"
|
||||
@@ -20,7 +20,7 @@
|
||||
}
|
||||
},
|
||||
"stream-buffers": {
|
||||
"version": "0.2.3"
|
||||
"version": "0.2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@ Package.describe({
|
||||
summary: "Send email messages"
|
||||
});
|
||||
|
||||
// Pinned at older version. 0.1.16+ uses mimelib, not mimelib-noiconv
|
||||
// which is much bigger. We need a better solution.
|
||||
Npm.depends({mailcomposer: "0.1.15", simplesmtp: "0.1.25", "stream-buffers": "0.2.3"});
|
||||
Npm.depends({
|
||||
// Pinned at older version. 0.1.16+ uses mimelib, not mimelib-noiconv which is
|
||||
// much bigger. We need a better solution.
|
||||
mailcomposer: "0.1.15",
|
||||
simplesmtp: "0.3.10",
|
||||
"stream-buffers": "0.2.5"});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use('underscore', 'server');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
var fs = Npm.require('fs');
|
||||
var Future = Npm.require('fibers/future');
|
||||
|
||||
var readFile = Meteor._wrapAsync(fs.readFile);
|
||||
|
||||
@@ -38,12 +39,15 @@ Follower = {
|
||||
makeElectorTries(urlSet, { priority: 0, reset: true });
|
||||
}
|
||||
var tryingUrl = null;
|
||||
var outstandingGetElectorate = false;
|
||||
var conn = null;
|
||||
var leader = null;
|
||||
var connected = null;
|
||||
var intervalHandle = null;
|
||||
|
||||
// Used to defer all method calls until we're sure that we connected to the
|
||||
// right leadership group.
|
||||
var connectedToLeadershipGroup = new Future();
|
||||
|
||||
var findFewestTries = function () {
|
||||
var min = 10000;
|
||||
var minElector = null;
|
||||
@@ -74,11 +78,14 @@ Follower = {
|
||||
};
|
||||
|
||||
var tryElector = function (url) {
|
||||
if (tryingUrl) {
|
||||
electorTries[tryingUrl]++;
|
||||
}
|
||||
url = url || findFewestTries();
|
||||
//console.log("trying", url, electorTries, tryingUrl);
|
||||
// console.log("trying", url, electorTries, tryingUrl);
|
||||
|
||||
// Don't keep trying the same url as fast as we can if it's not working.
|
||||
if (electorTries[url] > 2) {
|
||||
Meteor._sleepForMs(3 * 1000);
|
||||
}
|
||||
|
||||
if (conn) {
|
||||
conn._reconnectImpl({
|
||||
url: url
|
||||
@@ -87,21 +94,26 @@ Follower = {
|
||||
conn = DDP.connect(url);
|
||||
conn._reconnectImpl = conn.reconnect;
|
||||
}
|
||||
tryingUrl = url;
|
||||
|
||||
if (!outstandingGetElectorate) {
|
||||
outstandingGetElectorate = true;
|
||||
if (tryingUrl) {
|
||||
electorTries[tryingUrl]++;
|
||||
tryingUrl = url;
|
||||
} else {
|
||||
tryingUrl = url;
|
||||
conn.call('getElectorate', options.group, function (err, res) {
|
||||
outstandingGetElectorate = false;
|
||||
connected = tryingUrl;
|
||||
tryingUrl = null;
|
||||
if (err) {
|
||||
electorTries[url]++;
|
||||
tryElector();
|
||||
return;
|
||||
}
|
||||
tryingUrl = null;
|
||||
if (! connectedToLeadershipGroup.isResolved()) {
|
||||
connectedToLeadershipGroup["return"]();
|
||||
}
|
||||
// we got an answer! Connected!
|
||||
electorTries[url] = 0;
|
||||
if (res.leader === connected) {
|
||||
if (res.leader === url) {
|
||||
// we're good.
|
||||
|
||||
} else {
|
||||
@@ -109,6 +121,7 @@ Follower = {
|
||||
// is connectable.
|
||||
if (electorTries[res.leader] == 0) {
|
||||
tryElector(res.leader);
|
||||
|
||||
} else {
|
||||
// XXX: leader is probably down, we're probably going to elect
|
||||
// soon. Wait for the next round.
|
||||
@@ -117,29 +130,28 @@ Follower = {
|
||||
}
|
||||
updateElectorate(res);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
tryElector();
|
||||
|
||||
var checkConnection = function () {
|
||||
if (conn.status().status !== 'connected' || connected !== leader) {
|
||||
tryElector();
|
||||
} else {
|
||||
conn.call('getElectorate', options.group, function (err, res) {
|
||||
if (err) {
|
||||
electorTries[connected]++;
|
||||
tryElector();
|
||||
} else if (res.leader !== leader) {
|
||||
updateElectorate(res);
|
||||
tryElector(res.leader);
|
||||
} else {
|
||||
//console.log("updating electorate with", res);
|
||||
updateElectorate(res);
|
||||
if (conn.status().status !== 'connected' || connected !== leader) {
|
||||
tryElector();
|
||||
} else {
|
||||
conn.call('getElectorate', options.group, function (err, res) {
|
||||
if (err) {
|
||||
electorTries[connected]++;
|
||||
tryElector();
|
||||
} else {
|
||||
if (! connectedToLeadershipGroup.isResolved()) {
|
||||
connectedToLeadershipGroup["return"]();
|
||||
}
|
||||
});
|
||||
}
|
||||
//console.log("updating electorate with", res);
|
||||
updateElectorate(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var monitorConnection = function () {
|
||||
@@ -176,6 +188,15 @@ Follower = {
|
||||
return electorTries;
|
||||
};
|
||||
|
||||
// Assumes that `call` is implemented in terms of `apply`. All method calls
|
||||
// should be deferred until we are sure we've connected to the right
|
||||
// leadership group.
|
||||
conn._applyImpl = conn.apply;
|
||||
conn.apply = function (/* arguments */) {
|
||||
connectedToLeadershipGroup.wait();
|
||||
return conn._applyImpl.apply(conn, arguments);
|
||||
};
|
||||
|
||||
return conn;
|
||||
|
||||
}
|
||||
|
||||
1
packages/geojson-utils/.gitignore
vendored
Normal file
1
packages/geojson-utils/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.build*
|
||||
380
packages/geojson-utils/geojson-utils.js
Normal file
380
packages/geojson-utils/geojson-utils.js
Normal file
@@ -0,0 +1,380 @@
|
||||
(function () {
|
||||
var gju = {};
|
||||
|
||||
// Export the geojson object for **CommonJS**
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = gju;
|
||||
}
|
||||
|
||||
// adapted from http://www.kevlindev.com/gui/math/intersection/Intersection.js
|
||||
gju.lineStringsIntersect = function (l1, l2) {
|
||||
var intersects = [];
|
||||
for (var i = 0; i <= l1.coordinates.length - 2; ++i) {
|
||||
for (var j = 0; j <= l2.coordinates.length - 2; ++j) {
|
||||
var a1 = {
|
||||
x: l1.coordinates[i][1],
|
||||
y: l1.coordinates[i][0]
|
||||
},
|
||||
a2 = {
|
||||
x: l1.coordinates[i + 1][1],
|
||||
y: l1.coordinates[i + 1][0]
|
||||
},
|
||||
b1 = {
|
||||
x: l2.coordinates[j][1],
|
||||
y: l2.coordinates[j][0]
|
||||
},
|
||||
b2 = {
|
||||
x: l2.coordinates[j + 1][1],
|
||||
y: l2.coordinates[j + 1][0]
|
||||
},
|
||||
ua_t = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x),
|
||||
ub_t = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x),
|
||||
u_b = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y);
|
||||
if (u_b != 0) {
|
||||
var ua = ua_t / u_b,
|
||||
ub = ub_t / u_b;
|
||||
if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
|
||||
intersects.push({
|
||||
'type': 'Point',
|
||||
'coordinates': [a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y)]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (intersects.length == 0) intersects = false;
|
||||
return intersects;
|
||||
}
|
||||
|
||||
// Bounding Box
|
||||
|
||||
function boundingBoxAroundPolyCoords (coords) {
|
||||
var xAll = [], yAll = []
|
||||
|
||||
for (var i = 0; i < coords[0].length; i++) {
|
||||
xAll.push(coords[0][i][1])
|
||||
yAll.push(coords[0][i][0])
|
||||
}
|
||||
|
||||
xAll = xAll.sort(function (a,b) { return a - b })
|
||||
yAll = yAll.sort(function (a,b) { return a - b })
|
||||
|
||||
return [ [xAll[0], yAll[0]], [xAll[xAll.length - 1], yAll[yAll.length - 1]] ]
|
||||
}
|
||||
|
||||
gju.pointInBoundingBox = function (point, bounds) {
|
||||
return !(point.coordinates[1] < bounds[0][0] || point.coordinates[1] > bounds[1][0] || point.coordinates[0] < bounds[0][1] || point.coordinates[0] > bounds[1][1])
|
||||
}
|
||||
|
||||
// Point in Polygon
|
||||
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html#Listing the Vertices
|
||||
|
||||
function pnpoly (x,y,coords) {
|
||||
var vert = [ [0,0] ]
|
||||
|
||||
for (var i = 0; i < coords.length; i++) {
|
||||
for (var j = 0; j < coords[i].length; j++) {
|
||||
vert.push(coords[i][j])
|
||||
}
|
||||
vert.push([0,0])
|
||||
}
|
||||
|
||||
var inside = false
|
||||
for (var i = 0, j = vert.length - 1; i < vert.length; j = i++) {
|
||||
if (((vert[i][0] > y) != (vert[j][0] > y)) && (x < (vert[j][1] - vert[i][1]) * (y - vert[i][0]) / (vert[j][0] - vert[i][0]) + vert[i][1])) inside = !inside
|
||||
}
|
||||
|
||||
return inside
|
||||
}
|
||||
|
||||
gju.pointInPolygon = function (p, poly) {
|
||||
var coords = (poly.type == "Polygon") ? [ poly.coordinates ] : poly.coordinates
|
||||
|
||||
var insideBox = false
|
||||
for (var i = 0; i < coords.length; i++) {
|
||||
if (gju.pointInBoundingBox(p, boundingBoxAroundPolyCoords(coords[i]))) insideBox = true
|
||||
}
|
||||
if (!insideBox) return false
|
||||
|
||||
var insidePoly = false
|
||||
for (var i = 0; i < coords.length; i++) {
|
||||
if (pnpoly(p.coordinates[1], p.coordinates[0], coords[i])) insidePoly = true
|
||||
}
|
||||
|
||||
return insidePoly
|
||||
}
|
||||
|
||||
gju.numberToRadius = function (number) {
|
||||
return number * Math.PI / 180;
|
||||
}
|
||||
|
||||
gju.numberToDegree = function (number) {
|
||||
return number * 180 / Math.PI;
|
||||
}
|
||||
|
||||
// written with help from @tautologe
|
||||
gju.drawCircle = function (radiusInMeters, centerPoint, steps) {
|
||||
var center = [centerPoint.coordinates[1], centerPoint.coordinates[0]],
|
||||
dist = (radiusInMeters / 1000) / 6371,
|
||||
// convert meters to radiant
|
||||
radCenter = [gju.numberToRadius(center[0]), gju.numberToRadius(center[1])],
|
||||
steps = steps || 15,
|
||||
// 15 sided circle
|
||||
poly = [[center[0], center[1]]];
|
||||
for (var i = 0; i < steps; i++) {
|
||||
var brng = 2 * Math.PI * i / steps;
|
||||
var lat = Math.asin(Math.sin(radCenter[0]) * Math.cos(dist)
|
||||
+ Math.cos(radCenter[0]) * Math.sin(dist) * Math.cos(brng));
|
||||
var lng = radCenter[1] + Math.atan2(Math.sin(brng) * Math.sin(dist) * Math.cos(radCenter[0]),
|
||||
Math.cos(dist) - Math.sin(radCenter[0]) * Math.sin(lat));
|
||||
poly[i] = [];
|
||||
poly[i][1] = gju.numberToDegree(lat);
|
||||
poly[i][0] = gju.numberToDegree(lng);
|
||||
}
|
||||
return {
|
||||
"type": "Polygon",
|
||||
"coordinates": [poly]
|
||||
};
|
||||
}
|
||||
|
||||
// assumes rectangle starts at lower left point
|
||||
gju.rectangleCentroid = function (rectangle) {
|
||||
var bbox = rectangle.coordinates[0];
|
||||
var xmin = bbox[0][0],
|
||||
ymin = bbox[0][1],
|
||||
xmax = bbox[2][0],
|
||||
ymax = bbox[2][1];
|
||||
var xwidth = xmax - xmin;
|
||||
var ywidth = ymax - ymin;
|
||||
return {
|
||||
'type': 'Point',
|
||||
'coordinates': [xmin + xwidth / 2, ymin + ywidth / 2]
|
||||
};
|
||||
}
|
||||
|
||||
// from http://www.movable-type.co.uk/scripts/latlong.html
|
||||
gju.pointDistance = function (pt1, pt2) {
|
||||
var lon1 = pt1.coordinates[0],
|
||||
lat1 = pt1.coordinates[1],
|
||||
lon2 = pt2.coordinates[0],
|
||||
lat2 = pt2.coordinates[1],
|
||||
dLat = gju.numberToRadius(lat2 - lat1),
|
||||
dLon = gju.numberToRadius(lon2 - lon1),
|
||||
a = Math.pow(Math.sin(dLat / 2), 2) + Math.cos(gju.numberToRadius(lat1))
|
||||
* Math.cos(gju.numberToRadius(lat2)) * Math.pow(Math.sin(dLon / 2), 2),
|
||||
c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
// Earth radius is 6371 km
|
||||
return (6371 * c) * 1000; // returns meters
|
||||
},
|
||||
|
||||
// checks if geometry lies entirely within a circle
|
||||
// works with Point, LineString, Polygon
|
||||
gju.geometryWithinRadius = function (geometry, center, radius) {
|
||||
if (geometry.type == 'Point') {
|
||||
return gju.pointDistance(geometry, center) <= radius;
|
||||
} else if (geometry.type == 'LineString' || geometry.type == 'Polygon') {
|
||||
var point = {};
|
||||
var coordinates;
|
||||
if (geometry.type == 'Polygon') {
|
||||
// it's enough to check the exterior ring of the Polygon
|
||||
coordinates = geometry.coordinates[0];
|
||||
} else {
|
||||
coordinates = geometry.coordinates;
|
||||
}
|
||||
for (var i in coordinates) {
|
||||
point.coordinates = coordinates[i];
|
||||
if (gju.pointDistance(point, center) > radius) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// adapted from http://paulbourke.net/geometry/polyarea/javascript.txt
|
||||
gju.area = function (polygon) {
|
||||
var area = 0;
|
||||
// TODO: polygon holes at coordinates[1]
|
||||
var points = polygon.coordinates[0];
|
||||
var j = points.length - 1;
|
||||
var p1, p2;
|
||||
|
||||
for (var i = 0; i < points.length; j = i++) {
|
||||
var p1 = {
|
||||
x: points[i][1],
|
||||
y: points[i][0]
|
||||
};
|
||||
var p2 = {
|
||||
x: points[j][1],
|
||||
y: points[j][0]
|
||||
};
|
||||
area += p1.x * p2.y;
|
||||
area -= p1.y * p2.x;
|
||||
}
|
||||
|
||||
area /= 2;
|
||||
return area;
|
||||
},
|
||||
|
||||
// adapted from http://paulbourke.net/geometry/polyarea/javascript.txt
|
||||
gju.centroid = function (polygon) {
|
||||
var f, x = 0,
|
||||
y = 0;
|
||||
// TODO: polygon holes at coordinates[1]
|
||||
var points = polygon.coordinates[0];
|
||||
var j = points.length - 1;
|
||||
var p1, p2;
|
||||
|
||||
for (var i = 0; i < points.length; j = i++) {
|
||||
var p1 = {
|
||||
x: points[i][1],
|
||||
y: points[i][0]
|
||||
};
|
||||
var p2 = {
|
||||
x: points[j][1],
|
||||
y: points[j][0]
|
||||
};
|
||||
f = p1.x * p2.y - p2.x * p1.y;
|
||||
x += (p1.x + p2.x) * f;
|
||||
y += (p1.y + p2.y) * f;
|
||||
}
|
||||
|
||||
f = gju.area(polygon) * 6;
|
||||
return {
|
||||
'type': 'Point',
|
||||
'coordinates': [y / f, x / f]
|
||||
};
|
||||
},
|
||||
|
||||
gju.simplify = function (source, kink) { /* source[] array of geojson points */
|
||||
/* kink in metres, kinks above this depth kept */
|
||||
/* kink depth is the height of the triangle abc where a-b and b-c are two consecutive line segments */
|
||||
kink = kink || 20;
|
||||
source = source.map(function (o) {
|
||||
return {
|
||||
lng: o.coordinates[0],
|
||||
lat: o.coordinates[1]
|
||||
}
|
||||
});
|
||||
|
||||
var n_source, n_stack, n_dest, start, end, i, sig;
|
||||
var dev_sqr, max_dev_sqr, band_sqr;
|
||||
var x12, y12, d12, x13, y13, d13, x23, y23, d23;
|
||||
var F = (Math.PI / 180.0) * 0.5;
|
||||
var index = new Array(); /* aray of indexes of source points to include in the reduced line */
|
||||
var sig_start = new Array(); /* indices of start & end of working section */
|
||||
var sig_end = new Array();
|
||||
|
||||
/* check for simple cases */
|
||||
|
||||
if (source.length < 3) return (source); /* one or two points */
|
||||
|
||||
/* more complex case. initialize stack */
|
||||
|
||||
n_source = source.length;
|
||||
band_sqr = kink * 360.0 / (2.0 * Math.PI * 6378137.0); /* Now in degrees */
|
||||
band_sqr *= band_sqr;
|
||||
n_dest = 0;
|
||||
sig_start[0] = 0;
|
||||
sig_end[0] = n_source - 1;
|
||||
n_stack = 1;
|
||||
|
||||
/* while the stack is not empty ... */
|
||||
while (n_stack > 0) {
|
||||
|
||||
/* ... pop the top-most entries off the stacks */
|
||||
|
||||
start = sig_start[n_stack - 1];
|
||||
end = sig_end[n_stack - 1];
|
||||
n_stack--;
|
||||
|
||||
if ((end - start) > 1) { /* any intermediate points ? */
|
||||
|
||||
/* ... yes, so find most deviant intermediate point to
|
||||
either side of line joining start & end points */
|
||||
|
||||
x12 = (source[end].lng() - source[start].lng());
|
||||
y12 = (source[end].lat() - source[start].lat());
|
||||
if (Math.abs(x12) > 180.0) x12 = 360.0 - Math.abs(x12);
|
||||
x12 *= Math.cos(F * (source[end].lat() + source[start].lat())); /* use avg lat to reduce lng */
|
||||
d12 = (x12 * x12) + (y12 * y12);
|
||||
|
||||
for (i = start + 1, sig = start, max_dev_sqr = -1.0; i < end; i++) {
|
||||
|
||||
x13 = source[i].lng() - source[start].lng();
|
||||
y13 = source[i].lat() - source[start].lat();
|
||||
if (Math.abs(x13) > 180.0) x13 = 360.0 - Math.abs(x13);
|
||||
x13 *= Math.cos(F * (source[i].lat() + source[start].lat()));
|
||||
d13 = (x13 * x13) + (y13 * y13);
|
||||
|
||||
x23 = source[i].lng() - source[end].lng();
|
||||
y23 = source[i].lat() - source[end].lat();
|
||||
if (Math.abs(x23) > 180.0) x23 = 360.0 - Math.abs(x23);
|
||||
x23 *= Math.cos(F * (source[i].lat() + source[end].lat()));
|
||||
d23 = (x23 * x23) + (y23 * y23);
|
||||
|
||||
if (d13 >= (d12 + d23)) dev_sqr = d23;
|
||||
else if (d23 >= (d12 + d13)) dev_sqr = d13;
|
||||
else dev_sqr = (x13 * y12 - y13 * x12) * (x13 * y12 - y13 * x12) / d12; // solve triangle
|
||||
if (dev_sqr > max_dev_sqr) {
|
||||
sig = i;
|
||||
max_dev_sqr = dev_sqr;
|
||||
}
|
||||
}
|
||||
|
||||
if (max_dev_sqr < band_sqr) { /* is there a sig. intermediate point ? */
|
||||
/* ... no, so transfer current start point */
|
||||
index[n_dest] = start;
|
||||
n_dest++;
|
||||
} else { /* ... yes, so push two sub-sections on stack for further processing */
|
||||
n_stack++;
|
||||
sig_start[n_stack - 1] = sig;
|
||||
sig_end[n_stack - 1] = end;
|
||||
n_stack++;
|
||||
sig_start[n_stack - 1] = start;
|
||||
sig_end[n_stack - 1] = sig;
|
||||
}
|
||||
} else { /* ... no intermediate points, so transfer current start point */
|
||||
index[n_dest] = start;
|
||||
n_dest++;
|
||||
}
|
||||
}
|
||||
|
||||
/* transfer last point */
|
||||
index[n_dest] = n_source - 1;
|
||||
n_dest++;
|
||||
|
||||
/* make return array */
|
||||
var r = new Array();
|
||||
for (var i = 0; i < n_dest; i++)
|
||||
r.push(source[index[i]]);
|
||||
|
||||
return r.map(function (o) {
|
||||
return {
|
||||
type: "Point",
|
||||
coordinates: [o.lng, o.lat]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// http://www.movable-type.co.uk/scripts/latlong.html#destPoint
|
||||
gju.destinationPoint = function (pt, brng, dist) {
|
||||
dist = dist/6371; // convert dist to angular distance in radians
|
||||
brng = gju.numberToRadius(brng);
|
||||
|
||||
var lat1 = gju.numberToRadius(pt.coordinates[0]);
|
||||
var lon1 = gju.numberToRadius(pt.coordinates[1]);
|
||||
|
||||
var lat2 = Math.asin( Math.sin(lat1)*Math.cos(dist) +
|
||||
Math.cos(lat1)*Math.sin(dist)*Math.cos(brng) );
|
||||
var lon2 = lon1 + Math.atan2(Math.sin(brng)*Math.sin(dist)*Math.cos(lat1),
|
||||
Math.cos(dist)-Math.sin(lat1)*Math.sin(lat2));
|
||||
lon2 = (lon2+3*Math.PI) % (2*Math.PI) - Math.PI; // normalise to -180..+180º
|
||||
|
||||
return {
|
||||
'type': 'Point',
|
||||
'coordinates': [gju.numberToDegree(lat2), gju.numberToDegree(lon2)]
|
||||
};
|
||||
};
|
||||
|
||||
})();
|
||||
102
packages/geojson-utils/geojson-utils.tests.js
Normal file
102
packages/geojson-utils/geojson-utils.tests.js
Normal file
@@ -0,0 +1,102 @@
|
||||
var gju = GeoJSON;
|
||||
|
||||
Tinytest.add("geojson-utils - line intersects", function (test) {
|
||||
var diagonalUp = { "type": "LineString","coordinates": [
|
||||
[0, 0], [10, 10]
|
||||
]}
|
||||
var diagonalDown = { "type": "LineString","coordinates": [
|
||||
[10, 0], [0, 10]
|
||||
]}
|
||||
var farAway = { "type": "LineString","coordinates": [
|
||||
[100, 100], [110, 110]
|
||||
]}
|
||||
|
||||
test.isTrue(gju.lineStringsIntersect(diagonalUp, diagonalDown));
|
||||
test.isFalse(gju.lineStringsIntersect(diagonalUp, farAway));
|
||||
});
|
||||
|
||||
// Used by two tests
|
||||
var box = {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[ [0, 0], [10, 0], [10, 10], [0, 10] ]
|
||||
]
|
||||
};
|
||||
|
||||
Tinytest.add("geojson-utils - inside/outside of the box", function (test) {
|
||||
|
||||
var inBox = {"type": "Point", "coordinates": [5, 5]}
|
||||
var outBox = {"type": "Point", "coordinates": [15, 15]}
|
||||
|
||||
test.isTrue(gju.pointInPolygon(inBox, box));
|
||||
test.isFalse(gju.pointInPolygon(outBox, box));
|
||||
});
|
||||
|
||||
Tinytest.add("geojson-utils - drawCircle", function (test) {
|
||||
test.length(gju.drawCircle(10, {"type": "Point", "coordinates": [0, 0]}).
|
||||
coordinates[0], 15);
|
||||
test.length(gju.drawCircle(10, {"type": "Point", "coordinates": [0, 0]}, 50).
|
||||
coordinates[0], 50);
|
||||
});
|
||||
|
||||
Tinytest.add("geojson-utils - centroid", function (test) {
|
||||
var centroid = gju.rectangleCentroid(box)
|
||||
test.equal(centroid.coordinates[0], 5);
|
||||
test.equal(centroid.coordinates[1], 5);
|
||||
});
|
||||
|
||||
Tinytest.add("geojson-utils - point distance", function (test) {
|
||||
var fairyLand = {"type": "Point",
|
||||
"coordinates": [-122.260000705719, 37.80919060818706]}
|
||||
var navalBase = {"type": "Point",
|
||||
"coordinates": [-122.32083320617676, 37.78774223089045]}
|
||||
test.equal(Math.floor(gju.pointDistance(fairyLand, navalBase)), 5852);
|
||||
});
|
||||
|
||||
Tinytest.add("geojson-utils - points distance generated tests", function (test) {
|
||||
var floatEqual = function (a, b) {
|
||||
test.isTrue(Math.abs(a - b) < 0.000001);
|
||||
};
|
||||
|
||||
// Pairs of points we will be looking a distance between
|
||||
var tests = [[[-19.416501816827804,-13.442164216190577], [8.694866622798145,-8.511979941977188]],
|
||||
[[151.2841189110186,-56.14564002258703], [167.77983197313733,0.05544793023727834]],
|
||||
[[100.28413630579598,-88.02313695591874], [36.48786173714325,53.44207073468715]],
|
||||
[[-70.34899035631679,76.51596869179048], [154.91605914011598,-73.60970971290953]],
|
||||
[[96.28682994353585,58.77288202662021], [-118.33936230326071,72.07877089688554]],
|
||||
[[140.35530551429838,10.507104953983799], [-67.73368513956666,38.075836981181055]],
|
||||
[[69.55582775664516,86.25450283149257], [-18.446230484172702,6.116170521359891]],
|
||||
[[163.83647522330284,-65.7211532376241], [-159.2198902608361,-78.42975475382991]],
|
||||
[[-178.9383797585033,-54.87420454365201], [-175.35227065649815,-84.04084282391705]],
|
||||
[[-48.63219943456352,11.284161149058491], [-179.12627786491066,-51.95622375886887]],
|
||||
[[140.29684206470847,-67.20720696030185], [-109.37452355003916,36.03131077555008]],
|
||||
[[-154.6698773431126,58.322094617411494], [61.18583445576951,-4.3424885796848685]],
|
||||
[[122.5562841903884,10.43972848681733], [-11.756078708684072,-43.86124441982247]],
|
||||
[[-67.91648306301795,-86.38826347864233], [163.577536230674,12.987319261068478]],
|
||||
[[91.65140007715672,17.595150742679834], [135.80393003183417,22.307532118167728]],
|
||||
[[-112.70280818711035,34.45729674655013], [-127.42168210959062,-25.51327457977459]],
|
||||
[[-161.55807900894433,-77.40711871231906], [-92.66313794790767,-89.12077954714186]],
|
||||
[[39.966264681424946,9.890176948625594], [-159.88646019320004,40.60383598925546]],
|
||||
[[-57.48232689569704,86.64061016729102], [59.53941993578337,-75.73194969259202]],
|
||||
[[-142.0938081513159,80.76813141163439], [14.891517050098628,64.56322408467531]]];
|
||||
|
||||
// correct distance between two points
|
||||
var answers = [3115066.2536578891, 6423493.2321747802, 15848950.0402601473,
|
||||
18714226.5425080135, 5223022.7731127860, 13874476.3135112207,
|
||||
9314403.3309389465, 1831929.5917785936, 3244710.9344544266,
|
||||
13691492.4666933995, 14525055.6462231465, 13261602.4336371962,
|
||||
14275427.5511620939, 11699799.3615680672, 4628773.1129429890,
|
||||
6846704.0253010122, 1368055.9401701286, 14041503.0409814864,
|
||||
18560499.7346975356, 3793112.6186894816];
|
||||
|
||||
_.each(tests, function (pair, testN) {
|
||||
var distance = GeoJSON.pointDistance.apply(this, _.map(pair, toGeoJSONPoint));
|
||||
test.isTrue(Math.abs(distance - answers[testN]) < 0.00000001,
|
||||
"Wrong distance between points " + JSON.stringify(pair) + ": " + distance);
|
||||
});
|
||||
|
||||
function toGeoJSONPoint (coordinates) {
|
||||
return { type: "Point", coordinates: coordinates };
|
||||
}
|
||||
});
|
||||
|
||||
16
packages/geojson-utils/package.js
Normal file
16
packages/geojson-utils/package.js
Normal file
@@ -0,0 +1,16 @@
|
||||
Package.describe({
|
||||
summary: 'GeoJSON utility functions (from https://github.com/maxogden/geojson-js-utils)',
|
||||
internal: true
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.export('GeoJSON');
|
||||
api.add_files(['pre.js', 'geojson-utils.js', 'post.js']);
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
api.use('tinytest');
|
||||
api.use('geojson-utils');
|
||||
api.add_files(['geojson-utils.tests.js'], 'client');
|
||||
});
|
||||
|
||||
4
packages/geojson-utils/post.js
Normal file
4
packages/geojson-utils/post.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// This exports object was created in pre.js. Now copy the `exports` object
|
||||
// from it into the package-scope variable `GeoJSON`, which will get exported.
|
||||
GeoJSON = module.exports;
|
||||
|
||||
4
packages/geojson-utils/pre.js
Normal file
4
packages/geojson-utils/pre.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// Define an object named exports. This will cause geojson-utils.js to put `gju`
|
||||
// as a field on it, instead of in the global namespace. See also post.js.
|
||||
module = {exports:{}};
|
||||
|
||||
@@ -44,6 +44,15 @@ Google.requestCredential = function (options, credentialRequestCompleteCallback)
|
||||
'&access_type=' + accessType +
|
||||
'&approval_prompt=' + approvalPrompt;
|
||||
|
||||
// Use Google's domain-specific login page if we want to restrict creation to
|
||||
// a particular email domain. (Don't use it if restrictCreationByEmailDomain
|
||||
// is a function.) Note that all this does is change Google's UI ---
|
||||
// accounts-base/accounts_server.js still checks server-side that the server
|
||||
// has the proper email address after the OAuth conversation.
|
||||
if (typeof Accounts._options.restrictCreationByEmailDomain === 'string') {
|
||||
loginUrl += '&hd=' + encodeURIComponent(Accounts._options.restrictCreationByEmailDomain);
|
||||
}
|
||||
|
||||
Oauth.initiateLogin(credentialToken,
|
||||
loginUrl,
|
||||
credentialRequestCompleteCallback,
|
||||
|
||||
@@ -249,11 +249,16 @@ testAsyncMulti("httpcall - methods", [
|
||||
test.equal(result.statusCode, 200);
|
||||
var data = result.data;
|
||||
test.equal(data.url, "/foo");
|
||||
|
||||
// IE <= 8 turns seems to turn POSTs with no body into
|
||||
// GETs, inexplicably.
|
||||
if (Meteor.isClient && $.browser.msie && $.browser.version <= 8
|
||||
&& meth === "POST")
|
||||
meth = "GET";
|
||||
//
|
||||
// XXX Except now it doesn't!? Not sure what changed, but
|
||||
// these lines now break the test...
|
||||
// if (Meteor.isClient && $.browser.msie && $.browser.version <= 8
|
||||
// && meth === "POST")
|
||||
// meth = "GET";
|
||||
|
||||
test.equal(data.method, meth);
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -1,42 +1,3 @@
|
||||
Tinytest.add("js-analyze - findGlobalDottedRefs", function (test) {
|
||||
|
||||
var R = JSAnalyze.READ;
|
||||
var W = JSAnalyze.WRITE;
|
||||
var run = function (source) {
|
||||
return JSAnalyze.findGlobalDottedRefs(source);
|
||||
};
|
||||
|
||||
test.equal(run('x'), {x: R});
|
||||
test.equal(run('x + y'), {x: R, y: R});
|
||||
test.equal(run('x = y'), {x: W, y: R});
|
||||
test.equal(run('var x; x = y'), {y: R});
|
||||
test.equal(run('var y; x = y'), {x: W});
|
||||
test.equal(run('var x,y; x = y'), {});
|
||||
test.equal(run('for (x in y);'), {x: W, y: R});
|
||||
test.equal(run('for (var x in y);'), {y: R});
|
||||
test.equal(run('x++'), {x: W});
|
||||
test.equal(run('var x = y'), {y: R});
|
||||
test.equal(run('a.b[c.d]'), {'a.b': R, 'c.d': R});
|
||||
test.equal(run('foo.bar[baz][c.d].z = 3'), {'foo.bar': W, baz: R, 'c.d': R});
|
||||
test.equal(run('foo.bar(baz)[c.d].z = 3'), {'foo.bar': R, baz: R, 'c.d': R});
|
||||
test.equal(run('var x = y.z; x.a = y; z.b;'), {'y.z': R, 'z.b': R, 'y': R});
|
||||
test.equal(run('Foo.Bar'), {'Foo.Bar': R});
|
||||
test.equal(run('Foo.Bar = 3'), {'Foo.Bar': W});
|
||||
test.equal(run(
|
||||
'(function (a, d) { var b = a, c; return f(a.z, b.z, c.z, d.z, e.z); })()'),
|
||||
{ 'f': R, 'e.z': R });
|
||||
test.equal(run('try { Foo } catch (e) { e }'), {'Foo': R});
|
||||
test.equal(run('try { Foo } catch (e) { Foo }'), {'Foo': R});
|
||||
test.equal(run('try { Foo } catch (Foo) { Foo }'), {'Foo': R});
|
||||
test.equal(run('try { e } catch (Foo) { Foo }'), {'e': R});
|
||||
test.equal(run('var x = function y () { return String(y); }'), {'String': R});
|
||||
test.equal(run('a[b=c] = d'), {a: W, b: W, c: R, d: R});
|
||||
test.equal(run('a.a.a[b.b.b=c.c.c] = d.d.d'),
|
||||
{'a.a.a': W, 'b.b.b': W, 'c.c.c': R, 'd.d.d': R});
|
||||
// Without ignoreEval, this thinks J is global.
|
||||
test.equal(run('function x(){var J;J=3;eval("foo");}'), {eval: R});
|
||||
});
|
||||
|
||||
Tinytest.add("js-analyze - findAssignedGlobals", function (test) {
|
||||
|
||||
var run = function (source, expected) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
Package.describe({
|
||||
summary: "Tests for JavaScript code analysis for Meteor"
|
||||
summary: "Tests for JavaScript code analysis for Meteor",
|
||||
internal: true
|
||||
});
|
||||
|
||||
// The tests are in a separate package so that it is possible to compile
|
||||
|
||||
10
packages/js-analyze/.npm/package/npm-shrinkwrap.json
generated
10
packages/js-analyze/.npm/package/npm-shrinkwrap.json
generated
@@ -3,11 +3,13 @@
|
||||
"esprima": {
|
||||
"from": "https://github.com/ariya/esprima/tarball/2a41dbf0ddadade0b09a9a7cc9a0c8df9c434018"
|
||||
},
|
||||
"estraverse": {
|
||||
"version": "1.1.2-1"
|
||||
},
|
||||
"escope": {
|
||||
"from": "https://github.com/meteor/escope/tarball/fef31f03797be5718080f811f9ca6e5297c90d2a"
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"estraverse": {
|
||||
"version": "1.3.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
var esprima = Npm.require('esprima');
|
||||
var escope = Npm.require('escope');
|
||||
var estraverse = Npm.require('estraverse');
|
||||
|
||||
var Syntax = estraverse.Syntax;
|
||||
|
||||
JSAnalyze = {};
|
||||
|
||||
JSAnalyze.READ = 1;
|
||||
JSAnalyze.WRITE = 2;
|
||||
|
||||
// Like esprima.parse, but annotates any thrown error with $ParseError = true.
|
||||
var esprimaParse = function (source) {
|
||||
try {
|
||||
@@ -22,111 +16,6 @@ var esprimaParse = function (source) {
|
||||
}
|
||||
};
|
||||
|
||||
// Analyze the JavaScript source code `source` and return a dictionary
|
||||
// of all the global dotted references.
|
||||
//
|
||||
// A global dotted reference is an expression of the form `Foo` or
|
||||
// `Foo.Bar.Baz`, where `Foo` is an access to a global variable not defined
|
||||
// in `source`.
|
||||
//
|
||||
// The value in the dictionary is either `JSAnalyze.READ`, if the reference
|
||||
// is only ever read and not written, or `JSAnalyze.WRITE` if the reference
|
||||
// is written (only or in addition). A dotted reference is mapped to WRITE
|
||||
// if it is every assigned to, or if it is part of a larger
|
||||
// expression that is assigned to which consists of a series of dotted
|
||||
// or bracketed member access expressions. For example, in
|
||||
// `Foo.Bar[baz].blah = 3`, the dotted reference `Foo.Bar` is reported
|
||||
// as WRITE.
|
||||
JSAnalyze.findGlobalDottedRefs = function (source) {
|
||||
// escope's analyzer treats vars in the top-level "Program" node as globals.
|
||||
// The newline is necessary in case source ends with a comment.
|
||||
source = '(function () {' + source + '\n})';
|
||||
|
||||
var parseTree = esprimaParse(source);
|
||||
var scoper = escope.analyze(parseTree, {ignoreEval: true});
|
||||
|
||||
var currentScope = null;
|
||||
var dottedExpressionStack = [];
|
||||
|
||||
var globalsFound = {};
|
||||
|
||||
// Add _parent pointers to the tree
|
||||
estraverse.traverse(parseTree, {
|
||||
enter: function (node, parent) {
|
||||
node._parent = parent;
|
||||
}
|
||||
});
|
||||
|
||||
estraverse.traverse(parseTree, {
|
||||
enter: function (node, parent) {
|
||||
currentScope = scoper.acquire(node) || currentScope;
|
||||
|
||||
if (node.type === Syntax.Identifier) {
|
||||
var ref = null;
|
||||
// Find an `escope.Reference` in the current Scope whose
|
||||
// identifier node is `===` to `node`. If found, this
|
||||
// means escope determined this site to be a reference
|
||||
// rather than some other identifier (like the `x` in
|
||||
// `var x` or `a.x`).
|
||||
for (var i = 0; i < currentScope.references.length; i++) {
|
||||
if (currentScope.references[i].identifier === node) {
|
||||
ref = currentScope.references[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ref && ! ref.resolved) {
|
||||
// global; not resolved to a local
|
||||
var name = node.name;
|
||||
var expr = node;
|
||||
// find outer expression with dots, e.g. Foo.Bar.Baz
|
||||
while (expr._parent &&
|
||||
expr._parent.type === Syntax.MemberExpression &&
|
||||
expr._parent.object === expr &&
|
||||
! expr._parent.computed) {
|
||||
expr = expr._parent;
|
||||
name += '.' + expr.property.name;
|
||||
}
|
||||
// now expand expression to include bracketed access,
|
||||
// e.g. Foo.Bar.Baz[3].blah
|
||||
while (expr._parent &&
|
||||
expr._parent.type === Syntax.MemberExpression &&
|
||||
expr._parent.object === expr) {
|
||||
expr = expr._parent;
|
||||
}
|
||||
var accessType;
|
||||
// position of `expr`, aka `outer`, now determines whether this
|
||||
// access is a READ or WRITE (which encompasses read/write)
|
||||
var outer = expr;
|
||||
var outerParent = expr._parent;
|
||||
switch (outerParent.type) {
|
||||
case Syntax.AssignmentExpression:
|
||||
accessType = ((outerParent.left === outer) ? JSAnalyze.WRITE
|
||||
: JSAnalyze.READ);
|
||||
break;
|
||||
case Syntax.UpdateExpression: // prefix or postfix `++` or `--`
|
||||
accessType = JSAnalyze.WRITE;
|
||||
break;
|
||||
case Syntax.ForInStatement:
|
||||
accessType = (outerParent.left === outer ?
|
||||
JSAnalyze.WRITE : JSAnalyze.READ);
|
||||
break;
|
||||
default:
|
||||
accessType = JSAnalyze.READ;
|
||||
break;
|
||||
}
|
||||
globalsFound[name] = Math.max(globalsFound[name] || 0, accessType);
|
||||
}
|
||||
}
|
||||
},
|
||||
leave: function (node, parent) {
|
||||
currentScope = scoper.release(node) || currentScope;
|
||||
}
|
||||
});
|
||||
|
||||
return globalsFound;
|
||||
};
|
||||
|
||||
|
||||
// Analyze the JavaScript source code `source` and return a dictionary of all
|
||||
// globals which are assigned to in the package. The values in the dictionary
|
||||
// are all `true`.
|
||||
@@ -158,80 +47,12 @@ JSAnalyze.findAssignedGlobals = function (source) {
|
||||
// But it can't pull references outward, so for our purposes it is safe to
|
||||
// ignore.
|
||||
var scoper = escope.analyze(parseTree, {ignoreEval: true});
|
||||
var globalScope = scoper.scopes[0];
|
||||
|
||||
var currentScope = null;
|
||||
var assignedGlobals = {};
|
||||
|
||||
// XXX This whole traversal is somewhat overkill. escope actually already
|
||||
// calculates the implicit global variables. The following code ALMOST works,
|
||||
// and doesn't even require ignoreEval:
|
||||
//
|
||||
// var globalScope = scoper.acquire(parseTree);
|
||||
// if (globalScope.type !== 'global')
|
||||
// throw new Error("Unexpected scope type " + globalScope.type);
|
||||
//
|
||||
// // can't use underscore because we want to have no dependencies
|
||||
// for (var i = 0; i < globalScope.variables.length; ++i) {
|
||||
// var variable = globalScope.variables[i];
|
||||
// // Because this is the global scope, and we wrap source in a function,
|
||||
// // the only variables in it should have a single implicit definition.
|
||||
// if (variable.defs.length !== 1)
|
||||
// throw new Error("Unexpected def length", variable.defs.length);
|
||||
// if (variable.defs[0].type !== escope.Variable.ImplicitGlobalVariable)
|
||||
// throw new Error("Unexpected def type", variable.defs[0].type);
|
||||
// assignedGlobals[variable.name] = true;
|
||||
// }
|
||||
//
|
||||
// Unfortunately, escope's ImplicitGlobalVariable search has several bugs.
|
||||
// https://github.com/Constellation/escope/issues/17
|
||||
|
||||
|
||||
// Traverse the tree looking for assignments to unreferenced variables.
|
||||
estraverse.traverse(parseTree, {
|
||||
enter: function (node, parent) {
|
||||
currentScope = scoper.acquire(node) || currentScope;
|
||||
|
||||
// We only care about identifiers.
|
||||
if (node.type !== Syntax.Identifier)
|
||||
return;
|
||||
// We already know this one is an assigned global.
|
||||
// (Avoid using _.has here so that we have no Meteor package
|
||||
// dependencies.)
|
||||
if (assignedGlobals.hasOwnProperty(node.name))
|
||||
return;
|
||||
|
||||
var ref = null;
|
||||
// Find an `escope.Reference` in the current Scope whose identifier node
|
||||
// is `===` to `node`. If found, this means escope determined this site
|
||||
// to be a reference rather than some other identifier (like the `x` in
|
||||
// `var x` or `a.x`).
|
||||
for (var i = 0; i < currentScope.references.length; i++) {
|
||||
if (currentScope.references[i].identifier === node) {
|
||||
ref = currentScope.references[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If this isn't a reference at all, or it's been resolved to a local, do
|
||||
// nothing.
|
||||
if (!ref || ref.resolved)
|
||||
return;
|
||||
|
||||
// OK, it's a global. But is it being assigned to? The situations where a
|
||||
// global is assigned to are:
|
||||
// - left-hand side of an '=' assignment
|
||||
// - the `x` in `for (x in y)` (without a `var`)
|
||||
// (You might think that ++, --, and the += family of operators also
|
||||
// write to globals, but they can't in and of themselves conjure up
|
||||
// a reference where none existed before.)
|
||||
if ((parent.type === Syntax.AssignmentExpression && parent.left === node
|
||||
&& parent.operator === '=')
|
||||
|| (parent.type === Syntax.ForInStatement && parent.left === node)) {
|
||||
assignedGlobals[node.name] = true;
|
||||
}
|
||||
},
|
||||
leave: function (node, parent) {
|
||||
currentScope = scoper.release(node) || currentScope;
|
||||
}
|
||||
// Underscore is not available in this package.
|
||||
globalScope.implicit.variables.forEach(function (variable) {
|
||||
assignedGlobals[variable.name] = true;
|
||||
});
|
||||
|
||||
return assignedGlobals;
|
||||
|
||||
@@ -14,10 +14,7 @@ Npm.depends({
|
||||
// This code was originally written against the unreleased 1.1 branch. We can
|
||||
// probably switch to a built NPM version when it gets released.
|
||||
esprima: "https://github.com/ariya/esprima/tarball/2a41dbf0ddadade0b09a9a7cc9a0c8df9c434018",
|
||||
estraverse: "1.1.2-1",
|
||||
// Fork to add ignoreEval option.
|
||||
// https://github.com/Constellation/escope/pull/18
|
||||
escope: "https://github.com/meteor/escope/tarball/fef31f03797be5718080f811f9ca6e5297c90d2a"
|
||||
escope: "1.0.0"
|
||||
});
|
||||
|
||||
// This package may not depend on ANY other Meteor packages, even in the test
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Package.describe({
|
||||
summary: "The dynamic stylesheet language."
|
||||
summary: "The dynamic stylesheet language"
|
||||
});
|
||||
|
||||
Package._transitional_registerBuildPlugin({
|
||||
|
||||
11
packages/livedata/.npm/package/npm-shrinkwrap.json
generated
11
packages/livedata/.npm/package/npm-shrinkwrap.json
generated
@@ -1,18 +1,23 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"sockjs": {
|
||||
"version": "0.3.7",
|
||||
"version": "0.3.8",
|
||||
"dependencies": {
|
||||
"node-uuid": {
|
||||
"version": "1.3.3"
|
||||
},
|
||||
"faye-websocket": {
|
||||
"version": "0.4.4"
|
||||
"version": "0.7.0",
|
||||
"dependencies": {
|
||||
"websocket-driver": {
|
||||
"version": "0.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"websocket": {
|
||||
"version": "1.0.7"
|
||||
"version": "1.0.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ MethodInvocation = function (options) {
|
||||
// reruns subscriptions
|
||||
this._setUserId = options.setUserId || function () {};
|
||||
|
||||
// used for associating the connection with a login token so that the
|
||||
// connection can be closed if the token is no longer valid
|
||||
this._setLoginToken = options._setLoginToken || function () {};
|
||||
|
||||
// Scratch data scoped to this connection (livedata_connection on the
|
||||
// client, livedata_session on the server). This is only used
|
||||
// internally, but we should have real and documented API for this
|
||||
@@ -48,6 +52,13 @@ _.extend(MethodInvocation.prototype, {
|
||||
throw new Error("Can't call setUserId in a method after calling unblock");
|
||||
self.userId = userId;
|
||||
self._setUserId(userId);
|
||||
},
|
||||
_setLoginToken: function (token) {
|
||||
this._setLoginToken(token);
|
||||
this._sessionData.loginToken = token;
|
||||
},
|
||||
_getLoginToken: function (token) {
|
||||
return this._sessionData.loginToken;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@ _.extend(SessionCollectionView.prototype, {
|
||||
/* Session */
|
||||
/******************************************************************************/
|
||||
|
||||
var Session = function (server, version) {
|
||||
var Session = function (server, version, socket) {
|
||||
var self = this;
|
||||
self.id = Random.id();
|
||||
|
||||
@@ -220,18 +220,11 @@ var Session = function (server, version) {
|
||||
self.version = version;
|
||||
|
||||
self.initialized = false;
|
||||
self.socket = null;
|
||||
self.last_connect_time = 0;
|
||||
self.last_detach_time = +(new Date);
|
||||
self.socket = socket;
|
||||
|
||||
self.in_queue = [];
|
||||
self.inQueue = [];
|
||||
self.blocked = false;
|
||||
self.worker_running = false;
|
||||
|
||||
self.out_queue = [];
|
||||
|
||||
// id of invocation => {result or error, when}
|
||||
self.result_cache = {};
|
||||
self.workerRunning = false;
|
||||
|
||||
// Sub objects for active subscriptions
|
||||
self._namedSubs = {};
|
||||
@@ -257,6 +250,13 @@ var Session = function (server, version) {
|
||||
// when we are rerunning subscriptions, any ready messages
|
||||
// we want to buffer up for when we are done rerunning subscriptions
|
||||
self._pendingReady = [];
|
||||
|
||||
socket.send(stringifyDDP({msg: 'connected',
|
||||
session: self.id}));
|
||||
// On initial connect, spin up all the universal publishers.
|
||||
Fiber(function () {
|
||||
self.startUniversalSubs();
|
||||
}).run();
|
||||
};
|
||||
|
||||
_.extend(Session.prototype, {
|
||||
@@ -340,32 +340,7 @@ _.extend(Session.prototype, {
|
||||
var view = self.getCollectionView(collectionName);
|
||||
view.changed(subscriptionHandle, id, fields);
|
||||
},
|
||||
// Connect a new socket to this session, displacing (and closing)
|
||||
// any socket that was previously connected
|
||||
connect: function (socket) {
|
||||
var self = this;
|
||||
if (self.socket) {
|
||||
self.socket.close();
|
||||
self.detach(self.socket);
|
||||
}
|
||||
|
||||
self.socket = socket;
|
||||
self.last_connect_time = +(new Date);
|
||||
_.each(self.out_queue, function (msg) {
|
||||
if (Meteor._printSentDDP)
|
||||
Meteor._debug("Sent DDP", stringifyDDP(msg));
|
||||
self.socket.send(stringifyDDP(msg));
|
||||
});
|
||||
self.out_queue = [];
|
||||
|
||||
// On initial connect, spin up all the universal publishers.
|
||||
if (!self.initialized) {
|
||||
self.initialized = true;
|
||||
Fiber(function () {
|
||||
self.startUniversalSubs();
|
||||
}).run();
|
||||
}
|
||||
},
|
||||
|
||||
startUniversalSubs: function () {
|
||||
var self = this;
|
||||
@@ -378,70 +353,33 @@ _.extend(Session.prototype, {
|
||||
});
|
||||
},
|
||||
|
||||
// If 'socket' is the socket currently connected to this session,
|
||||
// detach it (the session will then have no socket -- it will
|
||||
// continue running and queue up its messages.) If 'socket' isn't
|
||||
// the currently connected socket, just clean up the pointer that
|
||||
// may have led us to believe otherwise.
|
||||
detach: function (socket) {
|
||||
var self = this;
|
||||
if (socket === self.socket) {
|
||||
self.socket = null;
|
||||
self.last_detach_time = +(new Date);
|
||||
}
|
||||
if (socket.meteor_session === self)
|
||||
socket.meteor_session = null;
|
||||
},
|
||||
|
||||
// Should be called periodically to prune the method invocation
|
||||
// replay cache.
|
||||
cleanup: function () {
|
||||
var self = this;
|
||||
// Only prune if we're connected, and we've been connected for at
|
||||
// least five minutes. That seems like enough time for the client
|
||||
// to finish its reconnection. Then, keep five minutes of
|
||||
// history. That seems like enough time for the client to receive
|
||||
// our responses, or else for us to notice that the connection is
|
||||
// gone.
|
||||
var now = +(new Date);
|
||||
if (!(self.socket && (now - self.last_connect_time) > 5 * 60 * 1000))
|
||||
return; // not connected, or not connected long enough
|
||||
|
||||
var kill = [];
|
||||
_.each(self.result_cache, function (info, id) {
|
||||
if (now - info.when > 5 * 60 * 1000)
|
||||
kill.push(id);
|
||||
});
|
||||
_.each(kill, function (id) {
|
||||
delete self.result_cache[id];
|
||||
});
|
||||
},
|
||||
|
||||
// Destroy this session. Stop all processing and tear everything
|
||||
// down. If a socket was attached, close it.
|
||||
destroy: function () {
|
||||
var self = this;
|
||||
if (self.socket) {
|
||||
self.socket.close();
|
||||
self.detach(self.socket);
|
||||
self.socket._meteorSession = null;
|
||||
}
|
||||
self._deactivateAllSubscriptions();
|
||||
Meteor.defer(function () {
|
||||
// stop callbacks can yield, so we defer this on destroy.
|
||||
// see also _closeAllForTokens and its desire to destroy things in a loop.
|
||||
self._deactivateAllSubscriptions();
|
||||
});
|
||||
// Drop the merge box data immediately.
|
||||
self.collectionViews = {};
|
||||
self.in_queue = [];
|
||||
self.out_queue = [];
|
||||
self.inQueue = null;
|
||||
},
|
||||
|
||||
// Send a message (queueing it if no socket is connected right now.)
|
||||
// Send a message (doing nothing if no socket is connected right now.)
|
||||
// It should be a JSON object (it will be stringified.)
|
||||
send: function (msg) {
|
||||
var self = this;
|
||||
if (Meteor._printSentDDP)
|
||||
Meteor._debug("Sent DDP", stringifyDDP(msg));
|
||||
if (self.socket)
|
||||
if (self.socket) {
|
||||
if (Meteor._printSentDDP)
|
||||
Meteor._debug("Sent DDP", stringifyDDP(msg));
|
||||
self.socket.send(stringifyDDP(msg));
|
||||
else
|
||||
self.out_queue.push(msg);
|
||||
}
|
||||
},
|
||||
|
||||
// Send a connection error.
|
||||
@@ -468,20 +406,20 @@ _.extend(Session.prototype, {
|
||||
// way, but it's the easiest thing that's correct. (unsub needs to
|
||||
// be ordered against sub, methods need to be ordered against each
|
||||
// other.)
|
||||
processMessage: function (msg_in, socket) {
|
||||
processMessage: function (msg_in) {
|
||||
var self = this;
|
||||
if (socket !== self.socket)
|
||||
if (!self.inQueue) // we have been destroyed.
|
||||
return;
|
||||
|
||||
self.in_queue.push(msg_in);
|
||||
if (self.worker_running)
|
||||
self.inQueue.push(msg_in);
|
||||
if (self.workerRunning)
|
||||
return;
|
||||
self.worker_running = true;
|
||||
self.workerRunning = true;
|
||||
|
||||
var processNext = function () {
|
||||
var msg = self.in_queue.shift();
|
||||
var msg = self.inQueue && self.inQueue.shift();
|
||||
if (!msg) {
|
||||
self.worker_running = false;
|
||||
self.workerRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -569,18 +507,6 @@ _.extend(Session.prototype, {
|
||||
msg: 'updated', methods: [msg.id]});
|
||||
});
|
||||
|
||||
// check for a replayed method (this is important during
|
||||
// reconnect)
|
||||
if (_.has(self.result_cache, msg.id)) {
|
||||
// found -- just resend whatever we sent last time
|
||||
var payload = _.clone(self.result_cache[msg.id]);
|
||||
delete payload.when;
|
||||
self.send(
|
||||
_.extend({msg: 'result', id: msg.id}, payload));
|
||||
fence.arm();
|
||||
return;
|
||||
}
|
||||
|
||||
// find the handler
|
||||
var handler = self.server.method_handlers[msg.method];
|
||||
if (!handler) {
|
||||
@@ -595,9 +521,15 @@ _.extend(Session.prototype, {
|
||||
self._setUserId(userId);
|
||||
};
|
||||
|
||||
var setLoginToken = function (newToken) {
|
||||
self._setLoginToken(newToken);
|
||||
};
|
||||
|
||||
var invocation = new MethodInvocation({
|
||||
isSimulation: false,
|
||||
userId: self.userId, setUserId: setUserId,
|
||||
userId: self.userId,
|
||||
setUserId: setUserId,
|
||||
_setLoginToken: setLoginToken,
|
||||
unblock: unblock,
|
||||
sessionData: self.sessionData
|
||||
});
|
||||
@@ -622,7 +554,6 @@ _.extend(Session.prototype, {
|
||||
var payload =
|
||||
exception ? {error: exception} : (result !== undefined ?
|
||||
{result: result} : {});
|
||||
self.result_cache[msg.id] = _.extend({when: +(new Date)}, payload);
|
||||
self.send(_.extend({msg: 'result', id: msg.id}, payload));
|
||||
}
|
||||
},
|
||||
@@ -652,6 +583,19 @@ _.extend(Session.prototype, {
|
||||
});
|
||||
},
|
||||
|
||||
// XXX This mixes accounts concerns (login tokens) into livedata, which is not
|
||||
// ideal. Eventually we'll have an API that allows accounts to keep track of
|
||||
// which connections are associated with tokens and close them when necessary,
|
||||
// rather than the current state of things where accounts tells livedata which
|
||||
// connections are associated with which tokens, and when to close connections
|
||||
// associated with a given token.
|
||||
_setLoginToken: function (newToken) {
|
||||
var self = this;
|
||||
var oldToken = self.sessionData.loginToken;
|
||||
self.sessionData.loginToken = newToken;
|
||||
self.server._loginTokenChanged(self, newToken, oldToken);
|
||||
},
|
||||
|
||||
// Sets the current user id in all appropriate contexts and reruns
|
||||
// all subscriptions
|
||||
_setUserId: function(userId) {
|
||||
@@ -714,12 +658,6 @@ _.extend(Session.prototype, {
|
||||
self._pendingReady = [];
|
||||
}
|
||||
});
|
||||
|
||||
// XXX figure out the login token that was just used, and set up an observe
|
||||
// on the user doc so that deleting the user or the login token disconnects
|
||||
// the session. For now, if you want to make sure that your deleted users
|
||||
// don't have any continuing sessions, you can restart the server, but we
|
||||
// should make it automatic.
|
||||
},
|
||||
|
||||
_startSubscription: function (handler, subId, params, name) {
|
||||
@@ -895,6 +833,12 @@ _.extend(Subscription.prototype, {
|
||||
cur._publishCursor(self);
|
||||
});
|
||||
self.ready();
|
||||
} else if (res) {
|
||||
// truthy values other than cursors or arrays are probably a
|
||||
// user mistake (possible returning a Mongo document via, say,
|
||||
// `coll.findOne()`).
|
||||
self.error(new Error("Publish function can only return a Cursor or "
|
||||
+ "an array of Cursors"));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1027,11 +971,23 @@ Server = function () {
|
||||
|
||||
self.sessions = {}; // map from id to session
|
||||
|
||||
// Keeps track of the open connections associated with particular login
|
||||
// tokens. Used for logging out all a user's open connections, expiring login
|
||||
// tokens, etc.
|
||||
// XXX This mixes accounts concerns (login tokens) into livedata, which is not
|
||||
// ideal. Eventually we'll have an API that allows accounts to keep track of
|
||||
// which connections are associated with tokens and close them when necessary,
|
||||
// rather than the current state of things where accounts tells livedata which
|
||||
// connections are associated with which tokens, and when to close connections
|
||||
// associated with a given token.
|
||||
self.sessionsByLoginToken = {};
|
||||
|
||||
|
||||
self.stream_server = new StreamServer;
|
||||
|
||||
self.stream_server.register(function (socket) {
|
||||
// socket implements the SockJSConnection interface
|
||||
socket.meteor_session = null;
|
||||
socket._meteorSession = null;
|
||||
|
||||
var sendError = function (reason, offendingMessage) {
|
||||
var msg = {msg: 'error', reason: reason};
|
||||
@@ -1057,7 +1013,7 @@ Server = function () {
|
||||
}
|
||||
|
||||
if (msg.msg === 'connect') {
|
||||
if (socket.meteor_session) {
|
||||
if (socket._meteorSession) {
|
||||
sendError("Already connected", msg);
|
||||
return;
|
||||
}
|
||||
@@ -1065,44 +1021,26 @@ Server = function () {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!socket.meteor_session) {
|
||||
if (!socket._meteorSession) {
|
||||
sendError('Must connect first', msg);
|
||||
return;
|
||||
}
|
||||
socket.meteor_session.processMessage(msg, socket);
|
||||
socket._meteorSession.processMessage(msg);
|
||||
} catch (e) {
|
||||
// XXX print stack nicely
|
||||
Meteor._debug("Internal exception while processing message", msg,
|
||||
e.stack);
|
||||
e.message, e.stack);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('close', function () {
|
||||
if (socket.meteor_session)
|
||||
socket.meteor_session.detach(socket);
|
||||
});
|
||||
});
|
||||
|
||||
// Every minute, clean up sessions that have been abandoned for a
|
||||
// minute. Also run result cache cleanup.
|
||||
// XXX at scale, we'll want to have a separate timer for each
|
||||
// session, and stagger them
|
||||
// XXX when we get resume working again, we might keep sessions
|
||||
// open longer (but stop running their diffs!)
|
||||
Meteor.setInterval(function () {
|
||||
var now = +(new Date);
|
||||
var destroyedIds = [];
|
||||
_.each(self.sessions, function (s, id) {
|
||||
s.cleanup();
|
||||
if (!s.socket && (now - s.last_detach_time) > 60 * 1000) {
|
||||
s.destroy();
|
||||
destroyedIds.push(id);
|
||||
if (socket._meteorSession) {
|
||||
Fiber(function () {
|
||||
self._destroySession(socket._meteorSession);
|
||||
}).run();
|
||||
}
|
||||
});
|
||||
_.each(destroyedIds, function (id) {
|
||||
delete self.sessions[id];
|
||||
});
|
||||
}, 1 * 60 * 1000);
|
||||
});
|
||||
};
|
||||
|
||||
_.extend(Server.prototype, {
|
||||
@@ -1110,19 +1048,13 @@ _.extend(Server.prototype, {
|
||||
_handleConnect: function (socket, msg) {
|
||||
var self = this;
|
||||
// In the future, handle session resumption: something like:
|
||||
// socket.meteor_session = self.sessions[msg.session]
|
||||
// socket._meteorSession = self.sessions[msg.session]
|
||||
var version = calculateVersion(msg.support, SUPPORTED_DDP_VERSIONS);
|
||||
|
||||
if (msg.version === version) {
|
||||
// Creating a new session
|
||||
socket.meteor_session = new Session(self, version);
|
||||
self.sessions[socket.meteor_session.id] = socket.meteor_session;
|
||||
|
||||
|
||||
socket.send(stringifyDDP({msg: 'connected',
|
||||
session: socket.meteor_session.id}));
|
||||
// will kick off previous connection, if any
|
||||
socket.meteor_session.connect(socket);
|
||||
socket._meteorSession = new Session(self, version, socket);
|
||||
self.sessions[socket._meteorSession.id] = socket._meteorSession;
|
||||
} else if (!msg.version) {
|
||||
// connect message without a version. This means an old (pre-pre1)
|
||||
// client is trying to connect. If we just disconnect the
|
||||
@@ -1216,6 +1148,21 @@ _.extend(Server.prototype, {
|
||||
}
|
||||
},
|
||||
|
||||
_destroySession: function (session) {
|
||||
var self = this;
|
||||
delete self.sessions[session.id];
|
||||
if (session.sessionData.loginToken) {
|
||||
self.sessionsByLoginToken[session.sessionData.loginToken] = _.without(
|
||||
self.sessionsByLoginToken[session.sessionData.loginToken],
|
||||
session.id
|
||||
);
|
||||
if (_.isEmpty(self.sessionsByLoginToken[session.sessionData.loginToken])) {
|
||||
delete self.sessionsByLoginToken[session.sessionData.loginToken];
|
||||
}
|
||||
}
|
||||
session.destroy();
|
||||
},
|
||||
|
||||
methods: function (methods) {
|
||||
var self = this;
|
||||
_.each(methods, function (func, name) {
|
||||
@@ -1270,17 +1217,27 @@ _.extend(Server.prototype, {
|
||||
var setUserId = function() {
|
||||
throw new Error("Can't call setUserId on a server initiated method call");
|
||||
};
|
||||
var setLoginToken = function () {
|
||||
// XXX is this correct?
|
||||
throw new Error("Can't call _setLoginToken on a server " +
|
||||
"initiated method call");
|
||||
};
|
||||
var currentInvocation = DDP._CurrentInvocation.get();
|
||||
if (currentInvocation) {
|
||||
userId = currentInvocation.userId;
|
||||
setUserId = function(userId) {
|
||||
currentInvocation.setUserId(userId);
|
||||
};
|
||||
setLoginToken = function (newToken) {
|
||||
currentInvocation._setLoginToken(newToken);
|
||||
};
|
||||
}
|
||||
|
||||
var invocation = new MethodInvocation({
|
||||
isSimulation: false,
|
||||
userId: userId, setUserId: setUserId,
|
||||
userId: userId,
|
||||
setUserId: setUserId,
|
||||
_setLoginToken: setLoginToken,
|
||||
sessionData: self.sessionData
|
||||
});
|
||||
try {
|
||||
@@ -1305,6 +1262,42 @@ _.extend(Server.prototype, {
|
||||
if (exception)
|
||||
throw exception;
|
||||
return result;
|
||||
},
|
||||
|
||||
_loginTokenChanged: function (session, newToken, oldToken) {
|
||||
var self = this;
|
||||
if (oldToken) {
|
||||
// Remove the session from the list of open sessions for the old token.
|
||||
self.sessionsByLoginToken[oldToken] = _.without(
|
||||
self.sessionsByLoginToken[oldToken],
|
||||
session.id
|
||||
);
|
||||
if (_.isEmpty(self.sessionsByLoginToken[oldToken]))
|
||||
delete self.sessionsByLoginToken[oldToken];
|
||||
}
|
||||
if (newToken) {
|
||||
if (! _.has(self.sessionsByLoginToken, newToken))
|
||||
self.sessionsByLoginToken[newToken] = [];
|
||||
self.sessionsByLoginToken[newToken].push(session.id);
|
||||
}
|
||||
},
|
||||
|
||||
// Close all open sessions associated with any of the tokens in
|
||||
// `tokens`.
|
||||
_closeAllForTokens: function (tokens) {
|
||||
var self = this;
|
||||
_.each(tokens, function (token) {
|
||||
if (_.has(self.sessionsByLoginToken, token)) {
|
||||
// _destroySession modifies sessionsByLoginToken, so we clone it.
|
||||
_.each(EJSON.clone(self.sessionsByLoginToken[token]), function (sessionId) {
|
||||
// Destroy session and remove from self.sessions.
|
||||
var session = self.sessions[sessionId];
|
||||
if (session) {
|
||||
self._destroySession(session);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -10,9 +10,13 @@ Meteor.methods({
|
||||
check(arguments, [Match.Any]);
|
||||
return arguments[0];
|
||||
},
|
||||
exception: function (where, intended) {
|
||||
exception: function (where, options) {
|
||||
check(where, String);
|
||||
check(intended, Match.Optional(Boolean));
|
||||
check(options, Match.Optional({
|
||||
intended: Match.Optional(Boolean),
|
||||
throwThroughFuture: Match.Optional(Boolean)
|
||||
}));
|
||||
options = options || {};
|
||||
var shouldThrow =
|
||||
(Meteor.isServer && where === "server") ||
|
||||
(Meteor.isClient && where === "client") ||
|
||||
@@ -20,11 +24,20 @@ Meteor.methods({
|
||||
|
||||
if (shouldThrow) {
|
||||
var e;
|
||||
if (intended)
|
||||
if (options.intended)
|
||||
e = new Meteor.Error(999, "Client-visible test exception");
|
||||
else
|
||||
e = new Error("Test method throwing an exception");
|
||||
e.expected = true;
|
||||
|
||||
// We used to improperly serialize errors that were thrown through a
|
||||
// future first.
|
||||
if (Meteor.isServer && options.throwThroughFuture) {
|
||||
var Future = Npm.require('fibers/future');
|
||||
var f = new Future;
|
||||
f['throw'](e);
|
||||
e = f.wait();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -124,6 +124,8 @@ testAsyncMulti("livedata - basic method invocation", [
|
||||
echoTest({$date: 30}), // literal
|
||||
echoTest({$literal: {$date: 30}}),
|
||||
echoTest(12),
|
||||
echoTest(Infinity),
|
||||
echoTest(-Infinity),
|
||||
|
||||
function (test, expect) {
|
||||
if (Meteor.isServer)
|
||||
@@ -214,7 +216,17 @@ testAsyncMulti("livedata - basic method invocation", [
|
||||
if (Meteor.isServer) {
|
||||
var threw = false;
|
||||
try {
|
||||
Meteor.call("exception", "both", true);
|
||||
Meteor.call("exception", "both", {intended: true});
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
test.equal(e.error, 999);
|
||||
test.equal(e.reason, "Client-visible test exception");
|
||||
}
|
||||
test.isTrue(threw);
|
||||
threw = false;
|
||||
try {
|
||||
Meteor.call("exception", "both", {intended: true,
|
||||
throwThroughFuture: true});
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
test.equal(e.error, 999);
|
||||
@@ -225,12 +237,18 @@ testAsyncMulti("livedata - basic method invocation", [
|
||||
|
||||
if (Meteor.isClient) {
|
||||
test.equal(
|
||||
Meteor.call("exception", "both", true,
|
||||
Meteor.call("exception", "both", {intended: true},
|
||||
expect(failure(test, 999,
|
||||
"Client-visible test exception"))),
|
||||
undefined);
|
||||
test.equal(
|
||||
Meteor.call("exception", "server", true,
|
||||
Meteor.call("exception", "server", {intended: true},
|
||||
expect(failure(test, 999,
|
||||
"Client-visible test exception"))),
|
||||
undefined);
|
||||
test.equal(
|
||||
Meteor.call("exception", "server", {intended: true,
|
||||
throwThroughFuture: true},
|
||||
expect(failure(test, 999,
|
||||
"Client-visible test exception"))),
|
||||
undefined);
|
||||
|
||||
@@ -3,8 +3,7 @@ Package.describe({
|
||||
internal: true
|
||||
});
|
||||
|
||||
Npm.depends({sockjs: "0.3.7",
|
||||
websocket: "1.0.7"});
|
||||
Npm.depends({sockjs: "0.3.8", websocket: "1.0.8"});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use(['check', 'random', 'ejson', 'json', 'underscore', 'deps', 'logging'],
|
||||
|
||||
@@ -142,8 +142,9 @@ _.extend(LivedataTest.ClientStream.prototype, {
|
||||
|
||||
self._clearConnectionTimer();
|
||||
if (self.currentConnection) {
|
||||
self.currentConnection.close();
|
||||
var conn = self.currentConnection;
|
||||
self.currentConnection = null;
|
||||
conn.close();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -38,7 +38,10 @@ StreamServer = function () {
|
||||
// combining CPU-heavy processing with SockJS termination (eg a proxy which
|
||||
// converts to Unix sockets) but for now, raise the delay.
|
||||
disconnect_delay: 60 * 1000,
|
||||
jsessionid: false
|
||||
// Set the USE_JSESSIONID environment variable to enable setting the
|
||||
// JSESSIONID cookie. This is useful for setting up proxies with
|
||||
// session affinity.
|
||||
jsessionid: !!process.env.USE_JSESSIONID
|
||||
};
|
||||
|
||||
// If you know your server environment (eg, proxies) will prevent websockets
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"cli-color": {
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.3",
|
||||
"dependencies": {
|
||||
"es5-ext": {
|
||||
"version": "0.9.2"
|
||||
|
||||
@@ -4,7 +4,7 @@ Package.describe({
|
||||
});
|
||||
|
||||
Npm.depends({
|
||||
"cli-color": "0.2.2"
|
||||
"cli-color": "0.2.3"
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
|
||||
@@ -3,7 +3,7 @@ Package.describe({
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use(['livedata', 'underscore', 'spark', 'templating'], 'client');
|
||||
api.use(['livedata', 'mongo-livedata', 'underscore', 'spark', 'templating'], 'client');
|
||||
|
||||
api.add_files([
|
||||
'madewith.css',
|
||||
|
||||
@@ -79,3 +79,13 @@ Meteor.Error = Meteor.makeErrorType(
|
||||
else
|
||||
self.message = '[' + self.error + ']';
|
||||
});
|
||||
|
||||
// Meteor.Error is basically data and is sent over DDP, so you should be able to
|
||||
// properly EJSON-clone it. This is especially important because if a
|
||||
// Meteor.Error is thrown through a Future, the error, reason, and details
|
||||
// properties become non-enumerable so a standard Object clone won't preserve
|
||||
// them and they will be lost from DDP.
|
||||
Meteor.Error.prototype.clone = function () {
|
||||
var self = this;
|
||||
return new Meteor.Error(self.error, self.reason, self.details);
|
||||
};
|
||||
|
||||
@@ -84,7 +84,7 @@ _.extend(Meteor, {
|
||||
var logErr = function (err) {
|
||||
if (err)
|
||||
return Meteor._debug("Exception in callback of async function",
|
||||
err ? err.stack : err);
|
||||
err.stack ? err.stack : err);
|
||||
};
|
||||
|
||||
// Pop off optional args that are undefined
|
||||
|
||||
18
packages/minifiers/.npm/package/npm-shrinkwrap.json
generated
18
packages/minifiers/.npm/package/npm-shrinkwrap.json
generated
@@ -1,29 +1,24 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"clean-css": {
|
||||
"version": "1.0.11",
|
||||
"version": "1.1.2",
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "1.2.0",
|
||||
"dependencies": {
|
||||
"keypress": {
|
||||
"version": "0.1.0"
|
||||
}
|
||||
}
|
||||
"version": "2.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uglify-js": {
|
||||
"from": "https://github.com/mishoo/UglifyJS2/tarball/b1febde3e9be32b9d88918ed733efc3796e3f143",
|
||||
"from": "https://github.com/meteor/UglifyJS2/tarball/bb0a762d12d2ecd058b9d7b57f16b4c289378d9c",
|
||||
"dependencies": {
|
||||
"async": {
|
||||
"version": "0.2.9"
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.1.26",
|
||||
"version": "0.1.30",
|
||||
"dependencies": {
|
||||
"amdefine": {
|
||||
"version": "0.0.5"
|
||||
"version": "0.0.8"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -34,6 +29,9 @@
|
||||
"version": "0.0.2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uglify-to-browserify": {
|
||||
"version": "1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ Package.describe({
|
||||
});
|
||||
|
||||
Npm.depends({
|
||||
"clean-css": "1.0.11",
|
||||
// We depend on this commit, which has not been released yet.
|
||||
"uglify-js": "https://github.com/mishoo/UglifyJS2/tarball/b1febde3e9be32b9d88918ed733efc3796e3f143"
|
||||
"clean-css": "1.1.2",
|
||||
// Fork of 2.4.0 fixing https://github.com/mishoo/UglifyJS2/pull/308
|
||||
"uglify-js": "https://github.com/meteor/UglifyJS2/tarball/bb0a762d12d2ecd058b9d7b57f16b4c289378d9c"
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
|
||||
@@ -46,7 +46,7 @@ It just hasn't been looked at/thought about yet.
|
||||
upsert combined with $-operators might work, but hasn't actually been
|
||||
looked at or tested.
|
||||
|
||||
In general, the API needs tests, espectially update. (On the other
|
||||
In general, the API needs tests, especially update. (On the other
|
||||
hand, the underlying selector and mutator code is quite well tested.)
|
||||
|
||||
## OTHER STUFF ##
|
||||
|
||||
@@ -87,12 +87,18 @@ LocalCollection.Cursor = function (collection, selector, options) {
|
||||
if (LocalCollection._selectorIsId(selector)) {
|
||||
// stash for fast path
|
||||
self.selector_id = LocalCollection._idStringify(selector);
|
||||
self.selector_f = LocalCollection._compileSelector(selector);
|
||||
self.selector_f = LocalCollection._compileSelector(selector, self);
|
||||
self.sort_f = undefined;
|
||||
} else {
|
||||
// MongoDB throws different errors on different branching operators
|
||||
// containing $near
|
||||
if (isGeoQuerySpecial(selector))
|
||||
throw new Error("$near can't be inside $or/$and/$nor/$not");
|
||||
|
||||
self.selector_id = undefined;
|
||||
self.selector_f = LocalCollection._compileSelector(selector);
|
||||
self.sort_f = options.sort ? LocalCollection._compileSort(options.sort) : null;
|
||||
self.selector_f = LocalCollection._compileSelector(selector, self);
|
||||
self.sort_f = (isGeoQuery(selector) || options.sort) ?
|
||||
LocalCollection._compileSort(options.sort || [], self) : null;
|
||||
}
|
||||
self.skip = options.skip;
|
||||
self.limit = options.limit;
|
||||
@@ -140,9 +146,8 @@ LocalCollection.prototype.findOne = function (selector, options) {
|
||||
return this.find(selector, options).fetch()[0];
|
||||
};
|
||||
|
||||
LocalCollection.Cursor.prototype.forEach = function (callback) {
|
||||
LocalCollection.Cursor.prototype.forEach = function (callback, thisArg) {
|
||||
var self = this;
|
||||
var doc;
|
||||
|
||||
if (self.db_objects === null)
|
||||
self.db_objects = self._getRawObjects(true);
|
||||
@@ -155,12 +160,13 @@ LocalCollection.Cursor.prototype.forEach = function (callback) {
|
||||
movedBefore: true});
|
||||
|
||||
while (self.cursor_pos < self.db_objects.length) {
|
||||
var elt = EJSON.clone(self.db_objects[self.cursor_pos++]);
|
||||
var elt = EJSON.clone(self.db_objects[self.cursor_pos]);
|
||||
if (self.projection_f)
|
||||
elt = self.projection_f(elt);
|
||||
if (self._transform)
|
||||
elt = self._transform(elt);
|
||||
callback(elt);
|
||||
callback.call(thisArg, elt, self.cursor_pos, self);
|
||||
++self.cursor_pos;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -169,11 +175,11 @@ LocalCollection.Cursor.prototype.getTransform = function () {
|
||||
return self._transform;
|
||||
};
|
||||
|
||||
LocalCollection.Cursor.prototype.map = function (callback) {
|
||||
LocalCollection.Cursor.prototype.map = function (callback, thisArg) {
|
||||
var self = this;
|
||||
var res = [];
|
||||
self.forEach(function (doc) {
|
||||
res.push(callback(doc));
|
||||
self.forEach(function (doc, index) {
|
||||
res.push(callback.call(thisArg, doc, index, self));
|
||||
});
|
||||
return res;
|
||||
};
|
||||
@@ -473,9 +479,13 @@ LocalCollection.prototype.insert = function (doc, callback) {
|
||||
LocalCollection._recomputeResults(self.queries[qid]);
|
||||
});
|
||||
self._observeQueue.drain();
|
||||
// Defer in case the callback returns on a future; gives the caller time to
|
||||
// wait on the future.
|
||||
if (callback) Meteor.defer(function () { callback(null, doc._id); });
|
||||
|
||||
// Defer because the caller likely doesn't expect the callback to be run
|
||||
// immediately.
|
||||
if (callback)
|
||||
Meteor.defer(function () {
|
||||
callback(null, doc._id);
|
||||
});
|
||||
return doc._id;
|
||||
};
|
||||
|
||||
@@ -484,7 +494,7 @@ LocalCollection.prototype.remove = function (selector, callback) {
|
||||
var remove = [];
|
||||
|
||||
var queriesToRecompute = [];
|
||||
var selector_f = LocalCollection._compileSelector(selector);
|
||||
var selector_f = LocalCollection._compileSelector(selector, self);
|
||||
|
||||
// Avoid O(n) for "remove a single doc by ID".
|
||||
var specificIds = LocalCollection._idsMatchedBySelector(selector);
|
||||
@@ -533,9 +543,12 @@ LocalCollection.prototype.remove = function (selector, callback) {
|
||||
LocalCollection._recomputeResults(query);
|
||||
});
|
||||
self._observeQueue.drain();
|
||||
// Defer in case the callback returns on a future; gives the caller time to
|
||||
// wait on the future.
|
||||
if (callback) Meteor.defer(callback);
|
||||
var result = remove.length;
|
||||
if (callback)
|
||||
Meteor.defer(function () {
|
||||
callback(null, result);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
// XXX atomicity: if multi is true, and one modification fails, do
|
||||
@@ -548,10 +561,7 @@ LocalCollection.prototype.update = function (selector, mod, options, callback) {
|
||||
}
|
||||
if (!options) options = {};
|
||||
|
||||
if (options.upsert)
|
||||
throw new Error("upsert not yet implemented");
|
||||
|
||||
var selector_f = LocalCollection._compileSelector(selector);
|
||||
var selector_f = LocalCollection._compileSelector(selector, self);
|
||||
|
||||
// Save the original results of any query that we might need to
|
||||
// _recomputeResults on, because _modifyAndNotify will mutate the objects in
|
||||
@@ -565,12 +575,15 @@ LocalCollection.prototype.update = function (selector, mod, options, callback) {
|
||||
});
|
||||
var recomputeQids = {};
|
||||
|
||||
var updateCount = 0;
|
||||
|
||||
for (var id in self.docs) {
|
||||
var doc = self.docs[id];
|
||||
if (selector_f(doc)) {
|
||||
// XXX Should we save the original even if mod ends up being a no-op?
|
||||
self._saveOriginal(id, doc);
|
||||
self._modifyAndNotify(doc, mod, recomputeQids);
|
||||
++updateCount;
|
||||
if (!options.multi)
|
||||
break;
|
||||
}
|
||||
@@ -583,9 +596,54 @@ LocalCollection.prototype.update = function (selector, mod, options, callback) {
|
||||
qidToOriginalResults[qid]);
|
||||
});
|
||||
self._observeQueue.drain();
|
||||
// Defer in case the callback returns on a future; gives the caller time to
|
||||
// wait on the future.
|
||||
if (callback) Meteor.defer(callback);
|
||||
|
||||
// If we are doing an upsert, and we didn't modify any documents yet, then
|
||||
// it's time to do an insert. Figure out what document we are inserting, and
|
||||
// generate an id for it.
|
||||
var insertedId;
|
||||
if (updateCount === 0 && options.upsert) {
|
||||
var newDoc = LocalCollection._removeDollarOperators(selector);
|
||||
LocalCollection._modify(newDoc, mod, true);
|
||||
if (! newDoc._id && options.insertedId)
|
||||
newDoc._id = options.insertedId;
|
||||
insertedId = self.insert(newDoc);
|
||||
updateCount = 1;
|
||||
}
|
||||
|
||||
// Return the number of affected documents, or in the upsert case, an object
|
||||
// containing the number of affected docs and the id of the doc that was
|
||||
// inserted, if any.
|
||||
var result;
|
||||
if (options._returnObject) {
|
||||
result = {
|
||||
numberAffected: updateCount
|
||||
};
|
||||
if (insertedId !== undefined)
|
||||
result.insertedId = insertedId;
|
||||
} else {
|
||||
result = updateCount;
|
||||
}
|
||||
|
||||
if (callback)
|
||||
Meteor.defer(function () {
|
||||
callback(null, result);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
// A convenience wrapper on update. LocalCollection.upsert(sel, mod) is
|
||||
// equivalent to LocalCollection.update(sel, mod, { upsert: true, _returnObject:
|
||||
// true }).
|
||||
LocalCollection.prototype.upsert = function (selector, mod, options, callback) {
|
||||
var self = this;
|
||||
if (! callback && typeof options === "function") {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
return self.update(selector, mod, _.extend({}, options, {
|
||||
upsert: true,
|
||||
_returnObject: true
|
||||
}), callback);
|
||||
};
|
||||
|
||||
LocalCollection.prototype._modifyAndNotify = function (
|
||||
@@ -623,7 +681,7 @@ LocalCollection.prototype._modifyAndNotify = function (
|
||||
// in the output. So it's safe to skip recompute if neither before or
|
||||
// after are true.)
|
||||
if (before || after)
|
||||
recomputeQids[qid] = true;
|
||||
recomputeQids[qid] = true;
|
||||
} else if (before && !after) {
|
||||
LocalCollection._removeFromResults(query, doc);
|
||||
} else if (!before && after) {
|
||||
@@ -1033,119 +1091,113 @@ LocalCollection._compileProjection = function (fields) {
|
||||
if (!_.isObject(fields))
|
||||
throw MinimongoError("fields option must be an object");
|
||||
|
||||
// Check passed projection fields' keys:
|
||||
// If you have two rules such as 'foo.bar' and 'foo.bar.baz', then the
|
||||
// result becomes ambiguous. If that happens, there is a probability you are
|
||||
// doing something wrong, framework should notify you about such mistake
|
||||
// earlier on cursor compilation step than later during runtime.
|
||||
// Note, that real mongo doesn't do anything about it and the later rule
|
||||
// appears in projection project, more priority it takes.
|
||||
//
|
||||
// Example, assume following in mongo shell:
|
||||
// > db.coll.insert({ a: { b: 23, c: 44 } })
|
||||
// > db.coll.find({}, { 'a': 1, 'a.b': 1 })
|
||||
// { "_id" : ObjectId("520bfe456024608e8ef24af3"), "a" : { "b" : 23 } }
|
||||
// > db.coll.find({}, { 'a.b': 1, 'a': 1 })
|
||||
// { "_id" : ObjectId("520bfe456024608e8ef24af3"), "a" : { "b" : 23, "c" : 44 } }
|
||||
//
|
||||
// Note, how second time the return set of keys is different.
|
||||
var keyPaths = _.keys(fields);
|
||||
_.each(keyPaths, function (keyPath) {
|
||||
_.each(keyPaths, function (anotherKeyPath) {
|
||||
var idx = keyPath.indexOf(anotherKeyPath);
|
||||
// check if one key is path-prefix of another (like "abra" and
|
||||
// "abra.cadabra", but not "abra" and "abrab.ra")
|
||||
if (keyPath !== anotherKeyPath && !idx && keyPath[anotherKeyPath.length] === '.')
|
||||
throw MinimongoError("both " + keyPath + " and " + anotherKeyPath +
|
||||
" found in fields option, using both of them may trigger " +
|
||||
"unexpected behavior. Did you mean to use only one of them?");
|
||||
});
|
||||
});
|
||||
|
||||
if (_.any(_.values(fields), function (x) {
|
||||
return _.indexOf([1, 0, true, false], x) === -1; }))
|
||||
throw MinimongoError("Projection values should be one of 1, 0, true, or false");
|
||||
|
||||
var _idProjection = _.isUndefined(fields._id) ? true : fields._id;
|
||||
delete fields._id;
|
||||
// Find the non-_id keys (_id is handled specially because it is included unless
|
||||
// explicitly excluded). Sort the keys, so that our code to detect overlaps
|
||||
// like 'foo' and 'foo.bar' can assume that 'foo' comes first.
|
||||
var fieldsKeys = _.reject(_.keys(fields).sort(), function (key) { return key === '_id'; });
|
||||
var including = null; // Unknown
|
||||
var projectionRules = [];
|
||||
var projectionRulesTree = {}; // Tree represented as nested objects
|
||||
|
||||
_.each(fields, function (rule, keyPath) {
|
||||
rule = !!rule;
|
||||
_.each(fieldsKeys, function (keyPath) {
|
||||
var rule = !!fields[keyPath];
|
||||
if (including === null)
|
||||
including = rule;
|
||||
if (including !== rule)
|
||||
// This error message is copies from MongoDB shell
|
||||
throw MinimongoError("You cannot currently mix including and excluding fields.");
|
||||
projectionRules.push(keyPath.split('.'));
|
||||
var treePos = projectionRulesTree;
|
||||
keyPath = keyPath.split('.');
|
||||
|
||||
_.each(keyPath.slice(0, -1), function (key, idx) {
|
||||
if (!_.has(treePos, key))
|
||||
treePos[key] = {};
|
||||
else if (_.isBoolean(treePos[key])) {
|
||||
// Check passed projection fields' keys: If you have two rules such as
|
||||
// 'foo.bar' and 'foo.bar.baz', then the result becomes ambiguous. If
|
||||
// that happens, there is a probability you are doing something wrong,
|
||||
// framework should notify you about such mistake earlier on cursor
|
||||
// compilation step than later during runtime. Note, that real mongo
|
||||
// doesn't do anything about it and the later rule appears in projection
|
||||
// project, more priority it takes.
|
||||
//
|
||||
// Example, assume following in mongo shell:
|
||||
// > db.coll.insert({ a: { b: 23, c: 44 } })
|
||||
// > db.coll.find({}, { 'a': 1, 'a.b': 1 })
|
||||
// { "_id" : ObjectId("520bfe456024608e8ef24af3"), "a" : { "b" : 23 } }
|
||||
// > db.coll.find({}, { 'a.b': 1, 'a': 1 })
|
||||
// { "_id" : ObjectId("520bfe456024608e8ef24af3"), "a" : { "b" : 23, "c" : 44 } }
|
||||
//
|
||||
// Note, how second time the return set of keys is different.
|
||||
|
||||
var currentPath = keyPath.join('.');
|
||||
var anotherPath = keyPath.slice(0, idx + 1).join('.');
|
||||
throw MinimongoError("both " + currentPath + " and " + anotherPath +
|
||||
" found in fields option, using both of them may trigger " +
|
||||
"unexpected behavior. Did you mean to use only one of them?");
|
||||
}
|
||||
|
||||
treePos = treePos[key];
|
||||
});
|
||||
|
||||
treePos[_.last(keyPath)] = including;
|
||||
});
|
||||
|
||||
// XXX do these functions share too much in common?
|
||||
if (including)
|
||||
return function (doc) {
|
||||
var result = {};
|
||||
// returns transformed doc according to ruleTree
|
||||
var transform = function (doc, ruleTree) {
|
||||
// Special case for "sets"
|
||||
if (_.isArray(doc))
|
||||
return _.map(doc, function (subdoc) { return transform(subdoc, ruleTree); });
|
||||
|
||||
_.each(projectionRules, function (keyPath) {
|
||||
var target = result;
|
||||
var docTarget = doc;
|
||||
for (var i = 0; i < keyPath.length - 1; i++) {
|
||||
var key = keyPath[i];
|
||||
// This block simulates MongoDB behavior for different edge-cases when
|
||||
// object on certain path wasn't found or array found instead of an
|
||||
// object, or vice-versa.
|
||||
if (!_.has(target, key)) {
|
||||
if (_.isArray(docTarget[key])) {
|
||||
target[key] = [];
|
||||
docTarget = undefined;
|
||||
break;
|
||||
} else if (_.isObject(docTarget[key]))
|
||||
target[key] = {};
|
||||
else {
|
||||
docTarget = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
var res = including ? {} : EJSON.clone(doc);
|
||||
_.each(ruleTree, function (rule, key) {
|
||||
if (!_.has(doc, key))
|
||||
return;
|
||||
if (_.isObject(rule)) {
|
||||
// For sub-objects/subsets we branch
|
||||
if (_.isObject(doc[key]))
|
||||
res[key] = transform(doc[key], rule);
|
||||
// Otherwise we don't even touch this subfield
|
||||
} else if (including)
|
||||
res[key] = doc[key];
|
||||
else
|
||||
delete res[key];
|
||||
});
|
||||
|
||||
target = target[key];
|
||||
docTarget = docTarget[key];
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
if (keyPath.length > 0 && docTarget && _.has(docTarget, _.last(keyPath)))
|
||||
target[_.last(keyPath)] = docTarget[_.last(keyPath)];
|
||||
});
|
||||
return function (obj) {
|
||||
var res = transform(obj, projectionRulesTree);
|
||||
|
||||
if (_idProjection && _.has(doc, '_id'))
|
||||
result._id = doc._id;
|
||||
|
||||
return result;
|
||||
};
|
||||
else
|
||||
return function (doc) {
|
||||
// XXX Deep copy on this level might be a slowing factor,
|
||||
// In fact we need it only in case of nested excluded fields.
|
||||
var result = EJSON.clone(doc);
|
||||
|
||||
_.each(projectionRules, function (keyPath) {
|
||||
var target = result;
|
||||
var docTarget = doc;
|
||||
for (var i = 0; i < keyPath.length - 1; i++) {
|
||||
var key = keyPath[i];
|
||||
if (!_.has(target, key)) {
|
||||
break;
|
||||
}
|
||||
|
||||
target = target[key];
|
||||
docTarget = docTarget[key];
|
||||
}
|
||||
|
||||
if (keyPath.length > 0)
|
||||
delete target[_.last(keyPath)];
|
||||
});
|
||||
|
||||
if (!_idProjection)
|
||||
delete result._id;
|
||||
|
||||
return result;
|
||||
};
|
||||
if (_idProjection && _.has(obj, '_id'))
|
||||
res._id = obj._id;
|
||||
if (!_idProjection && _.has(res, '_id'))
|
||||
delete res._id;
|
||||
return res;
|
||||
};
|
||||
};
|
||||
|
||||
// Searches $near operator in the selector recursively
|
||||
// (including all $or/$and/$nor/$not branches)
|
||||
var isGeoQuery = function (selector) {
|
||||
return _.any(selector, function (val, key) {
|
||||
// Note: _.isObject matches objects and arrays
|
||||
return key === "$near" || (_.isObject(val) && isGeoQuery(val));
|
||||
});
|
||||
};
|
||||
|
||||
// Checks if $near appears under some $or/$and/$nor/$not branch
|
||||
var isGeoQuerySpecial = function (selector) {
|
||||
return _.any(selector, function (val, key) {
|
||||
if (_.contains(['$or', '$and', '$nor', '$not'], key))
|
||||
return isGeoQuery(val);
|
||||
// Note: _.isObject matches objects and arrays
|
||||
return _.isObject(val) && isGeoQuerySpecial(val);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -66,7 +66,8 @@ var log_callbacks = function (operations) {
|
||||
// XXX test shared structure in all MM entrypoints
|
||||
Tinytest.add("minimongo - basics", function (test) {
|
||||
var c = new LocalCollection(),
|
||||
fluffyKitten_id;
|
||||
fluffyKitten_id,
|
||||
count;
|
||||
|
||||
fluffyKitten_id = c.insert({type: "kitten", name: "fluffy"});
|
||||
c.insert({type: "kitten", name: "snookums"});
|
||||
@@ -87,7 +88,8 @@ Tinytest.add("minimongo - basics", function (test) {
|
||||
test.length(c.find({type: "kitten"}).fetch(), 2);
|
||||
test.length(c.find({type: "cryptographer"}).fetch(), 2);
|
||||
|
||||
c.update({name: "snookums"}, {$set: {type: "cryptographer"}});
|
||||
count = c.update({name: "snookums"}, {$set: {type: "cryptographer"}});
|
||||
test.equal(count, 1);
|
||||
test.equal(c.find().count(), 4);
|
||||
test.equal(c.find({type: "kitten"}).count(), 1);
|
||||
test.equal(c.find({type: "cryptographer"}).count(), 3);
|
||||
@@ -102,10 +104,12 @@ Tinytest.add("minimongo - basics", function (test) {
|
||||
c.remove({_id: null});
|
||||
c.remove({_id: false});
|
||||
c.remove({_id: undefined});
|
||||
c.remove();
|
||||
count = c.remove();
|
||||
test.equal(count, 0);
|
||||
test.equal(c.find().count(), 4);
|
||||
|
||||
c.remove({});
|
||||
count = c.remove({});
|
||||
test.equal(count, 4);
|
||||
test.equal(c.find().count(), 0);
|
||||
|
||||
c.insert({_id: 1, name: "strawberry", tags: ["fruit", "red", "squishy"]});
|
||||
@@ -180,16 +184,25 @@ Tinytest.add("minimongo - cursors", function (test) {
|
||||
|
||||
// forEach
|
||||
var count = 0;
|
||||
q.forEach(function (obj) {
|
||||
var context = {};
|
||||
q.forEach(function (obj, i, cursor) {
|
||||
test.equal(obj.i, count++);
|
||||
});
|
||||
test.equal(obj.i, i);
|
||||
test.isTrue(context === this);
|
||||
test.isTrue(cursor === q);
|
||||
}, context);
|
||||
test.equal(count, 20);
|
||||
// everything empty
|
||||
test.length(q.fetch(), 0);
|
||||
q.rewind();
|
||||
|
||||
// map
|
||||
res = q.map(function (obj) { return obj.i * 2; });
|
||||
res = q.map(function (obj, i, cursor) {
|
||||
test.equal(obj.i, i);
|
||||
test.isTrue(context === this);
|
||||
test.isTrue(cursor === q);
|
||||
return obj.i * 2;
|
||||
}, context);
|
||||
test.length(res, 20);
|
||||
for (var i = 0; i < 20; i++)
|
||||
test.equal(res[i], i * 2);
|
||||
@@ -427,6 +440,9 @@ Tinytest.add("minimongo - selector_compiler", function (test) {
|
||||
nomatch({a: {$ne: 1}}, {a: [1, 2]});
|
||||
nomatch({a: {$ne: 2}}, {a: [1, 2]});
|
||||
match({a: {$ne: 3}}, {a: [1, 2]});
|
||||
nomatch({'a.b': {$ne: 1}}, {a: [{b: 1}, {b: 2}]});
|
||||
nomatch({'a.b': {$ne: 2}}, {a: [{b: 1}, {b: 2}]});
|
||||
match({'a.b': {$ne: 3}}, {a: [{b: 1}, {b: 2}]});
|
||||
|
||||
nomatch({a: {$ne: {x: 1}}}, {a: {x: 1}});
|
||||
match({a: {$ne: {x: 1}}}, {a: {x: 2}});
|
||||
@@ -456,7 +472,9 @@ Tinytest.add("minimongo - selector_compiler", function (test) {
|
||||
nomatch({a: {$nin: [1, 2, 3]}}, {a: [2]}); // tested against mongodb
|
||||
nomatch({a: {$nin: [{x: 1}, {x: 2}, {x: 3}]}}, {a: [{x: 2}]});
|
||||
nomatch({a: {$nin: [1, 2, 3]}}, {a: [4, 2]});
|
||||
nomatch({'a.b': {$nin: [1, 2, 3]}}, {a: [{b:4}, {b:2}]});
|
||||
match({a: {$nin: [1, 2, 3]}}, {a: [4]});
|
||||
match({'a.b': {$nin: [1, 2, 3]}}, {a: [{b:4}]});
|
||||
|
||||
// $size
|
||||
match({a: {$size: 0}}, {a: []});
|
||||
@@ -564,7 +582,9 @@ Tinytest.add("minimongo - selector_compiler", function (test) {
|
||||
match({x: {$not: {$lt: 10, $gt: 7}}}, {x: 6});
|
||||
|
||||
match({x: {$not: {$gt: 7}}}, {x: [2, 3, 4]});
|
||||
match({'x.y': {$not: {$gt: 7}}}, {x: [{y:2}, {y:3}, {y:4}]});
|
||||
nomatch({x: {$not: {$gt: 7}}}, {x: [2, 3, 4, 10]});
|
||||
nomatch({'x.y': {$not: {$gt: 7}}}, {x: [{y:2}, {y:3}, {y:4}, {y:10}]});
|
||||
|
||||
match({x: {$not: /a/}}, {x: "dog"});
|
||||
nomatch({x: {$not: /a/}}, {x: "cat"});
|
||||
@@ -898,7 +918,7 @@ Tinytest.add("minimongo - projection_compiler", function (test) {
|
||||
var testProjection = function (projection, tests) {
|
||||
var projection_f = LocalCollection._compileProjection(projection);
|
||||
var equalNonStrict = function (a, b, desc) {
|
||||
test.equal(EJSON.stringify(a), EJSON.stringify(b), desc);
|
||||
test.isTrue(_.isEqual(a, b), desc);
|
||||
};
|
||||
|
||||
_.each(tests, function (testCase) {
|
||||
@@ -983,6 +1003,11 @@ Tinytest.add("minimongo - projection_compiler", function (test) {
|
||||
[ { a: { b: 42 } }, { a: { b: 42 } }, "Can't have ambiguous rules (one is prefix of another)" ]
|
||||
]);
|
||||
});
|
||||
test.throws(function () {
|
||||
testProjection({ 'a.b.c': 1, 'a.b': 1, 'a': 1 }, [
|
||||
[ { a: { b: 42 } }, { a: { b: 42 } }, "Can't have ambiguous rules (one is prefix of another)" ]
|
||||
]);
|
||||
});
|
||||
|
||||
test.throws(function () {
|
||||
testProjection("some string", [
|
||||
@@ -1069,6 +1094,63 @@ Tinytest.add("minimongo - fetch with fields", function (test) {
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add("minimongo - fetch with projection, subarrays", function (test) {
|
||||
// Apparently projection of type 'foo.bar.x' for
|
||||
// { foo: [ { bar: { x: 42 } }, { bar: { x: 3 } } ] }
|
||||
// should return exactly this object. More precisely, arrays are considered as
|
||||
// sets and are queried separately and then merged back to result set
|
||||
var c = new LocalCollection();
|
||||
|
||||
// Insert a test object with two set fields
|
||||
c.insert({
|
||||
setA: [{
|
||||
fieldA: 42,
|
||||
fieldB: 33
|
||||
}, {
|
||||
fieldA: "the good",
|
||||
fieldB: "the bad",
|
||||
fieldC: "the ugly"
|
||||
}],
|
||||
setB: [{
|
||||
anotherA: { },
|
||||
anotherB: "meh"
|
||||
}, {
|
||||
anotherA: 1234,
|
||||
anotherB: 431
|
||||
}]
|
||||
});
|
||||
|
||||
var equalNonStrict = function (a, b, desc) {
|
||||
test.isTrue(_.isEqual(a, b), desc);
|
||||
};
|
||||
|
||||
var testForProjection = function (projection, expected) {
|
||||
var fetched = c.find({}, { fields: projection }).fetch()[0];
|
||||
equalNonStrict(fetched, expected, "failed sub-set projection: " +
|
||||
JSON.stringify(projection));
|
||||
};
|
||||
|
||||
testForProjection({ 'setA.fieldA': 1, 'setB.anotherB': 1, _id: 0 },
|
||||
{
|
||||
setA: [{ fieldA: 42 }, { fieldA: "the good" }],
|
||||
setB: [{ anotherB: "meh" }, { anotherB: 431 }]
|
||||
});
|
||||
|
||||
testForProjection({ 'setA.fieldA': 0, 'setB.anotherA': 0, _id: 0 },
|
||||
{
|
||||
setA: [{fieldB:33}, {fieldB:"the bad",fieldC:"the ugly"}],
|
||||
setB: [{ anotherB: "meh" }, { anotherB: 431 }]
|
||||
});
|
||||
|
||||
c.remove({});
|
||||
c.insert({a:[[{b:1,c:2},{b:2,c:4}],{b:3,c:5},[{b:4, c:9}]]});
|
||||
|
||||
testForProjection({ 'a.b': 1, _id: 0 },
|
||||
{a: [ [ { b: 1 }, { b: 2 } ], { b: 3 }, [ { b: 4 } ] ] });
|
||||
testForProjection({ 'a.b': 0, _id: 0 },
|
||||
{a: [ [ { c: 2 }, { c: 4 } ], { c: 5 }, [ { c: 9 } ] ] });
|
||||
});
|
||||
|
||||
Tinytest.add("minimongo - observe ordered with projection", function (test) {
|
||||
// These tests are copy-paste from "minimongo -observe ordered",
|
||||
// slightly modified to test projection
|
||||
@@ -1495,6 +1577,28 @@ Tinytest.add("minimongo - modify", function (test) {
|
||||
modify({a: [1]}, {$push: {a: [2]}}, {a: [1, [2]]});
|
||||
modify({a: []}, {$push: {'a.1': 99}}, {a: [null, [99]]}); // tested
|
||||
modify({a: {}}, {$push: {'a.x': 99}}, {a: {x: [99]}});
|
||||
modify({}, {$push: {a: {$each: [1, 2, 3]}}},
|
||||
{a: [1, 2, 3]});
|
||||
modify({a: []}, {$push: {a: {$each: [1, 2, 3]}}},
|
||||
{a: [1, 2, 3]});
|
||||
modify({a: [true]}, {$push: {a: {$each: [1, 2, 3]}}},
|
||||
{a: [true, 1, 2, 3]});
|
||||
// No positive numbers for $slice
|
||||
exception({}, {$push: {a: {$each: [], $slice: 5}}});
|
||||
modify({a: [true]}, {$push: {a: {$each: [1, 2, 3], $slice: -2}}},
|
||||
{a: [2, 3]});
|
||||
modify({a: [false, true]}, {$push: {a: {$each: [1], $slice: -2}}},
|
||||
{a: [true, 1]});
|
||||
modify(
|
||||
{a: [{x: 3}, {x: 1}]},
|
||||
{$push: {a: {
|
||||
$each: [{x: 4}, {x: 2}],
|
||||
$slice: -2,
|
||||
$sort: {x: 1}
|
||||
}}},
|
||||
{a: [{x: 3}, {x: 4}]});
|
||||
modify({}, {$push: {a: {$each: [1, 2, 3], $slice: 0}}}, {a: []});
|
||||
modify({a: [1, 2]}, {$push: {a: {$each: [1, 2, 3], $slice: 0}}}, {a: []});
|
||||
|
||||
// $pushAll
|
||||
modify({}, {$pushAll: {a: [1]}}, {a: [1]});
|
||||
@@ -1596,11 +1700,8 @@ Tinytest.add("minimongo - modify", function (test) {
|
||||
modify({a: {b: 12}}, {$rename: {'a.b': 'x'}}, {a: {}, x: 12}); // tested
|
||||
modify({a: {b: 12}}, {$rename: {'a.b': 'q.r'}}, {a: {}, q: {r: 12}});
|
||||
modify({a: {b: 12}}, {$rename: {'a.b': 'q.2.r'}}, {a: {}, q: {2: {r: 12}}});
|
||||
// Opera weirdly reorders the output. But what it does tends to be close
|
||||
// enough.
|
||||
modify({a: {b: 12}, q: {}}, {$rename: {'a.b': 'q.2.r'}},
|
||||
(typeof opera === 'undefined' ? {a: {}, q: {2: {r: 12}}}
|
||||
: {q: {2: {r: 12}}, a: {}}));
|
||||
{a: {}, q: {2: {r: 12}}});
|
||||
exception({a: {b: 12}, q: []}, {$rename: {'a.b': 'q.2'}}); // tested
|
||||
exception({a: {b: 12}, q: []}, {$rename: {'a.b': 'q.2.r'}}); // tested
|
||||
test.expect_fail();
|
||||
@@ -1934,7 +2035,8 @@ Tinytest.add("minimongo - diff", function (test) {
|
||||
|
||||
Tinytest.add("minimongo - saveOriginals", function (test) {
|
||||
// set up some data
|
||||
var c = new LocalCollection();
|
||||
var c = new LocalCollection(),
|
||||
count;
|
||||
c.insert({_id: 'foo', x: 'untouched'});
|
||||
c.insert({_id: 'bar', x: 'updateme'});
|
||||
c.insert({_id: 'baz', x: 'updateme'});
|
||||
@@ -1945,9 +2047,12 @@ Tinytest.add("minimongo - saveOriginals", function (test) {
|
||||
c.saveOriginals();
|
||||
c.insert({_id: "hooray", z: 'insertme'});
|
||||
c.remove({y: 'removeme'});
|
||||
c.update({x: 'updateme'}, {$set: {z: 5}}, {multi: true});
|
||||
count = c.update({x: 'updateme'}, {$set: {z: 5}}, {multi: true});
|
||||
c.update('bar', {$set: {k: 7}}); // update same doc twice
|
||||
|
||||
// Verify returned count is correct
|
||||
test.equal(count, 2);
|
||||
|
||||
// Verify the originals.
|
||||
var originals = c.retrieveOriginals();
|
||||
var affected = ['bar', 'baz', 'quux', 'whoa', 'hooray'];
|
||||
@@ -2180,3 +2285,98 @@ Tinytest.add("minimongo - count on cursor with limit", function(test){
|
||||
c.stop();
|
||||
|
||||
});
|
||||
|
||||
Tinytest.add("minimongo - $near operator tests", function (test) {
|
||||
var coll = new LocalCollection();
|
||||
coll.insert({ rest: { loc: [2, 3] } });
|
||||
coll.insert({ rest: { loc: [-3, 3] } });
|
||||
coll.insert({ rest: { loc: [5, 5] } });
|
||||
|
||||
test.equal(coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 30 } }).count(), 3);
|
||||
test.equal(coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 4 } }).count(), 1);
|
||||
var points = coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 6 } }).fetch();
|
||||
_.each(points, function (point, i, points) {
|
||||
test.isTrue(!i || distance([0, 0], point.rest.loc) >= distance([0, 0], points[i - 1].rest.loc));
|
||||
});
|
||||
|
||||
function distance(a, b) {
|
||||
var x = a[0] - b[0];
|
||||
var y = a[1] - b[1];
|
||||
return Math.sqrt(x * x + y * y);
|
||||
}
|
||||
|
||||
// GeoJSON tests
|
||||
coll = new LocalCollection();
|
||||
var data = [{ "category" : "BURGLARY", "descript" : "BURGLARY OF STORE, FORCIBLE ENTRY", "address" : "100 Block of 10TH ST", "location" : { "type" : "Point", "coordinates" : [ -122.415449723856, 37.7749518087273 ] } },
|
||||
{ "category" : "WEAPON LAWS", "descript" : "POSS OF PROHIBITED WEAPON", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415386041221, 37.7747879744156 ] } },
|
||||
{ "category" : "LARCENY/THEFT", "descript" : "GRAND THEFT OF PROPERTY", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.41538270191, 37.774683628213 ] } },
|
||||
{ "category" : "LARCENY/THEFT", "descript" : "PETTY THEFT FROM LOCKED AUTO", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415396041221, 37.7747879744156 ] } },
|
||||
{ "category" : "OTHER OFFENSES", "descript" : "POSSESSION OF BURGLARY TOOLS", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415386041221, 37.7747879734156 ] } }
|
||||
];
|
||||
|
||||
_.each(data, function (x, i) { coll.insert(_.extend(x, { x: i })); });
|
||||
|
||||
var close15 = coll.find({ location: { $near: {
|
||||
$geometry: { type: "Point",
|
||||
coordinates: [-122.4154282, 37.7746115] },
|
||||
$maxDistance: 15 } } }).fetch();
|
||||
test.length(close15, 1);
|
||||
test.equal(close15[0].descript, "GRAND THEFT OF PROPERTY");
|
||||
|
||||
var close20 = coll.find({ location: { $near: {
|
||||
$geometry: { type: "Point",
|
||||
coordinates: [-122.4154282, 37.7746115] },
|
||||
$maxDistance: 20 } } }).fetch();
|
||||
test.length(close20, 4);
|
||||
test.equal(close20[0].descript, "GRAND THEFT OF PROPERTY");
|
||||
test.equal(close20[1].descript, "PETTY THEFT FROM LOCKED AUTO");
|
||||
test.equal(close20[2].descript, "POSSESSION OF BURGLARY TOOLS");
|
||||
test.equal(close20[3].descript, "POSS OF PROHIBITED WEAPON");
|
||||
|
||||
// Any combinations of $near with $or/$and/$nor/$not should throw an error
|
||||
test.throws(function () {
|
||||
coll.find({ location: {
|
||||
$not: {
|
||||
$near: {
|
||||
$geometry: {
|
||||
type: "Point",
|
||||
coordinates: [-122.4154282, 37.7746115]
|
||||
}, $maxDistance: 20 } } } });
|
||||
});
|
||||
test.throws(function () {
|
||||
coll.find({
|
||||
$and: [ { location: { $near: { $geometry: { type: "Point", coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 20 }}},
|
||||
{ x: 0 }]
|
||||
});
|
||||
});
|
||||
test.throws(function () {
|
||||
coll.find({
|
||||
$or: [ { location: { $near: { $geometry: { type: "Point", coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 20 }}},
|
||||
{ x: 0 }]
|
||||
});
|
||||
});
|
||||
test.throws(function () {
|
||||
coll.find({
|
||||
$nor: [ { location: { $near: { $geometry: { type: "Point", coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 1 }}},
|
||||
{ x: 0 }]
|
||||
});
|
||||
});
|
||||
test.throws(function () {
|
||||
coll.find({
|
||||
$and: [{
|
||||
$and: [{
|
||||
location: {
|
||||
$near: {
|
||||
$geometry: {
|
||||
type: "Point",
|
||||
coordinates: [-122.4154282, 37.7746115]
|
||||
},
|
||||
$maxDistance: 1
|
||||
}
|
||||
}
|
||||
}]
|
||||
}]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user