mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'release-0.8.2'
This commit is contained in:
11
.mailmap
11
.mailmap
@@ -12,6 +12,7 @@ GITHUB: aldeed <eric@dairystatedesigns.com>
|
||||
GITHUB: AlexeyMK <alexey@alexeymk.com>
|
||||
GITHUB: apendua <apendua@gmail.com>
|
||||
GITHUB: arbesfeld <arbesfeld@gmail.com>
|
||||
GITHUB: Cangit <fredricendrerud@gmail.com>
|
||||
GITHUB: DenisGorbachev <Denis.Gorbachev@faster-than-wind.ru>
|
||||
GITHUB: EOT <eot@gmx.at>
|
||||
GITHUB: FooBarWidget <honglilai@gmail.com>
|
||||
@@ -20,15 +21,21 @@ GITHUB: OyoKooN <nathan@sxnlabs.com>
|
||||
GITHUB: RobertLowe <robert@iblargz.com>
|
||||
GITHUB: ansman <nicklas@ansman.se>
|
||||
GITHUB: awwx <andrew.wilcox@gmail.com>
|
||||
GITHUB: babenzele <tim.p.phillips@gmail.com>
|
||||
GITHUB: cmather <mather.chris@gmail.com>
|
||||
GITHUB: codeinthehole <david.winterbottom@gmail.com>
|
||||
GITHUB: dandv <ddascalescu+github@gmail.com>
|
||||
GITHUB: davegonzalez <gonzalez.dalex@gmail.com>
|
||||
GITHUB: ducdigital <duc@ducdigital.com>
|
||||
GITHUB: emgee3 <hello@gravitronic.com>
|
||||
GITHUB: felixrabe <felix@rabe.io>
|
||||
GITHUB: FredericoC <frederico.carvalho@3stack.com.au>
|
||||
GITHUB: icellan <icellan@icellan.com>
|
||||
GITHUB: jacott <geoffjacobsen@gmail.com>
|
||||
GITHUB: jfhamlin <jfhamlin@gmail.com>
|
||||
GITHUB: jbruni <contato@jbruni.com.br>
|
||||
GITHUB: justinsb <justin@fathomdb.com>
|
||||
GITHUB: kentonv <temporal@gmail.com>
|
||||
GITHUB: marcandre <github@marc-andre.ca>
|
||||
GITHUB: mart-jansink <m.jansink@gmail.com>
|
||||
GITHUB: meawoppl <meawoppl@gmail.com>
|
||||
@@ -47,7 +54,10 @@ GITHUB: rgould <rwgould@gmail.com>
|
||||
GITHUB: ryw <ry@rywalker.com>
|
||||
GITHUB: rzymek <rzymek@gmail.com>
|
||||
GITHUB: sdarnell <stephen@darnell.plus.com>
|
||||
GITHUB: subhog <hubert@orlikarnia.com>
|
||||
GITHUB: tbjers <torgny@xorcode.com>
|
||||
GITHUB: timhaines <tmhaines@gmail.com>
|
||||
GITHUB: tmeasday <tom@thesnail.org>
|
||||
GITHUB: yeputons <egor.suvorov@gmail.com>
|
||||
GITHUB: zol <zol@percolatestudio.com>
|
||||
|
||||
@@ -65,4 +75,3 @@ METEOR: sixolet <naomi@meteor.com>
|
||||
METEOR: Slava <slava@meteor.com>
|
||||
METEOR: stubailo <sashko@mit.edu>
|
||||
METEOR: ekatek <ekate@meteor.com>
|
||||
|
||||
|
||||
197
History.md
197
History.md
@@ -1,6 +1,192 @@
|
||||
## v.NEXT
|
||||
|
||||
|
||||
## v0.8.2
|
||||
|
||||
#### Meteor Accounts
|
||||
|
||||
* Switch `accounts-password` to use bcrypt to store passwords on the
|
||||
server. (Previous versions of Meteor used a protocol called SRP.)
|
||||
Users will be transparently transitioned when they log in. This
|
||||
transition is one-way, so you cannot downgrade a production app once
|
||||
you upgrade to 0.8.2. If you are maintaining an authenticating DDP
|
||||
client:
|
||||
- Clients that use the plaintext password login handler (i.e. call
|
||||
the `login` method with argument `{ password: <plaintext
|
||||
password> }`) will continue to work, but users will not be
|
||||
transitioned from SRP to bcrypt when logging in with this login
|
||||
handler.
|
||||
- Clients that use SRP will no longer work. These clients should
|
||||
instead directly call the `login` method, as in
|
||||
`Meteor.loginWithPassword`. The argument to the `login` method
|
||||
can be either:
|
||||
- `{ password: <plaintext password> }`, or
|
||||
- `{ password: { digest: <password hash>, algorithm: "sha-256" } }`,
|
||||
where the password hash is the hex-encoded SHA256 hash of the
|
||||
plaintext password.
|
||||
|
||||
* Show the display name of the currently logged-in user after following
|
||||
an email verification link or a password reset link in `accounts-ui`.
|
||||
|
||||
* Add a `userEmail` option to `Meteor.loginWithMeteorDeveloperAccount`
|
||||
to pre-fill the user's email address in the OAuth popup.
|
||||
|
||||
* Ensure that the user object has updated token information before
|
||||
it is passed to email template functions. #2210
|
||||
|
||||
* Export the function that serves the HTTP response at the end of an
|
||||
OAuth flow as `OAuth._endOfLoginResponse`. This function can be
|
||||
overridden to make the OAuth popup flow work in certain mobile
|
||||
environments where `window.opener` is not supported.
|
||||
|
||||
* Remove support for OAuth redirect URLs with a `redirect` query
|
||||
parameter. This OAuth flow was never documented and never fully
|
||||
worked.
|
||||
|
||||
|
||||
#### Blaze
|
||||
|
||||
* Blaze now tracks individual CSS rules in `style` attributes and won't
|
||||
overwrite changes to them made by other JavaScript libraries.
|
||||
|
||||
* Add {{> UI.dynamic}} to make it easier to dynamically render a
|
||||
template with a data context.
|
||||
|
||||
* Add `UI._templateInstance()` for accessing the current template
|
||||
instance from within a block helper.
|
||||
|
||||
* Add `UI._parentData(n)` for accessing parent data contexts from
|
||||
within a block helper.
|
||||
|
||||
* Add preliminary API for registering hooks to run when Blaze intends to
|
||||
insert, move, or remove DOM elements. For example, you can use these
|
||||
hooks to animate nodes as they are inserted, moved, or removed. To use
|
||||
them, you can set the `_uihooks` property on a container DOM
|
||||
element. `_uihooks` is an object that can have any subset of the
|
||||
following three properties:
|
||||
|
||||
- `insertElement: function (node, next)`: called when Blaze intends
|
||||
to insert the DOM element `node` before the element `next`
|
||||
- `moveElement: function (node, next)`: called when Blaze intends to
|
||||
move the DOM element `node` before the element `next`
|
||||
- `removeElement: function (node)`: called when Blaze intends to
|
||||
remove the DOM element `node`
|
||||
|
||||
Note that when you set one of these functions on a container
|
||||
element, Blaze will not do the actual operation; it's your
|
||||
responsibility to actually insert, move, or remove the node (by
|
||||
calling `$(node).remove()`, for example).
|
||||
|
||||
* The `findAll` method on template instances now returns a vanilla
|
||||
array, not a jQuery object. The `$` method continues to
|
||||
return a jQuery object. #2039
|
||||
|
||||
* Fix a Blaze memory leak by cleaning up event handlers when a template
|
||||
instance is destroyed. #1997
|
||||
|
||||
* Fix a bug where helpers used by {{#with}} were still re-running when
|
||||
their reactive data sources changed after they had been removed from
|
||||
the DOM.
|
||||
|
||||
* Stop not updating form controls if they're focused. If a field is
|
||||
edited by one user while another user is focused on it, it will just
|
||||
lose its value but maintain its focus. #1965
|
||||
|
||||
* Add `_nestInCurrentComputation` option to `UI.render`, fixing a bug in
|
||||
{{#each}} when an item is added inside a computation that subsequently
|
||||
gets invalidated. #2156
|
||||
|
||||
* Fix bug where "=" was not allowed in helper arguments. #2157
|
||||
|
||||
* Fix bug when a template tag immediately follows a Spacebars block
|
||||
comment. #2175
|
||||
|
||||
|
||||
#### Command-line tool
|
||||
|
||||
* Add --directory flag to `meteor bundle`. Setting this flag outputs a
|
||||
directory rather than a tarball.
|
||||
|
||||
* Speed up updates of NPM modules by upgrading Node to include our fix for
|
||||
https://github.com/npm/npm/issues/3265 instead of passing `--force` to
|
||||
`npm install`.
|
||||
|
||||
* Always rebuild on changes to npm-shrinkwrap.json files. #1648
|
||||
|
||||
* Fix uninformative error message when deploying to long hostnames. #1208
|
||||
|
||||
* Increase a buffer size to avoid failing when running MongoDB due to a
|
||||
large number of processes running on the machine, and fix the error
|
||||
message when the failure does occur. #2158
|
||||
|
||||
* Clarify a `meteor mongo` error message when using the MONGO_URL
|
||||
environment variable. #1256
|
||||
|
||||
|
||||
#### Testing
|
||||
|
||||
* Run server tests from multiple clients serially instead of in
|
||||
parallel. This allows testing features that modify global server
|
||||
state. #2088
|
||||
|
||||
|
||||
#### Security
|
||||
|
||||
* Add Content-Type headers on JavaScript and CSS resources.
|
||||
|
||||
* Add `X-Content-Type-Options: nosniff` header to
|
||||
`browser-policy-content`'s default policy. If you are using
|
||||
`browser-policy-content` and you don't want your app to send this
|
||||
header, then call `BrowserPolicy.content.allowContentTypeSniffing()`.
|
||||
|
||||
* Use `Meteor.absoluteUrl()` to compute the redirect URL in the `force-ssl`
|
||||
package (instead of the host header).
|
||||
|
||||
|
||||
#### Miscellaneous
|
||||
|
||||
* Allow `check` to work on the server outside of a Fiber. #2136
|
||||
|
||||
* EJSON custom type conversion functions should not be permitted to yield. #2136
|
||||
|
||||
* The legacy polling observe driver handles errors communicating with MongoDB
|
||||
better and no longer gets "stuck" in some circumstances.
|
||||
|
||||
* Automatically rewind cursors before calls to `fetch`, `forEach`, or `map`. On
|
||||
the client, don't cache the return value of `cursor.count()` (consistently
|
||||
with the server behavior). `cursor.rewind()` is now a no-op. #2114
|
||||
|
||||
* Remove an obsolete hack in reporting line numbers for LESS errors. #2216
|
||||
|
||||
* Avoid exceptions when accessing localStorage in certain Internet
|
||||
Explorer configurations. #1291, #1688.
|
||||
|
||||
* Make `handle.ready()` reactively stop, where `handle` is a
|
||||
subscription handle.
|
||||
|
||||
* Fix an error message from `audit-argument-checks` after login.
|
||||
|
||||
* Make the DDP server send an error if the client sends a connect
|
||||
message with a missing or malformed `support` field. #2125
|
||||
|
||||
* Fix missing `jquery` dependency in the `amplify` package. #2113
|
||||
|
||||
* Ban inserting EJSON custom types as documents. #2095
|
||||
|
||||
* Fix incorrect URL rewrites in stylesheets. #2106
|
||||
|
||||
* Upgraded dependencies:
|
||||
- node: 0.10.28 (from 0.10.26)
|
||||
- uglify-js: 2.4.13 (from 2.4.7)
|
||||
- sockjs server: 0.3.9 (from 0.3.8)
|
||||
- websocket-driver: 0.3.4 (from 0.3.2)
|
||||
- stylus: 0.46.3 (from 0.42.3)
|
||||
|
||||
Patches contributed by GitHub users awwx, babenzele, Cangit, dandv,
|
||||
ducdigital, emgee3, felixrabe, FredericoC, jbruni, kentonv, mizzao,
|
||||
mquandalle, subhog, tbjers, tmeasday.
|
||||
|
||||
|
||||
## v.0.8.1.3
|
||||
|
||||
* Fix a security issue in the `spiderable` package. `spiderable` now
|
||||
@@ -19,7 +205,8 @@
|
||||
|
||||
* Add missing `underscore` dependency in the `oauth-encryption` package. #2165
|
||||
|
||||
* Fix minification bug that caused some apps to fail to render in IE8. #2037.
|
||||
* Work around IE8 bug that caused some apps to fail to render when
|
||||
minified. #2037.
|
||||
|
||||
|
||||
## v.0.8.1.2
|
||||
@@ -99,6 +286,8 @@
|
||||
|
||||
* Clean up autoruns when calling `UI.toHTML`.
|
||||
|
||||
* Properly clean up event listeners when removing templates.
|
||||
|
||||
* Add support for `{{!-- block comments --}}` in Spacebars. Block comments may
|
||||
contain `}}`, so they are more useful than `{{! normal comments}}` for
|
||||
commenting out sections of Spacebars templates.
|
||||
@@ -123,6 +312,12 @@
|
||||
get one with `DDP.randomStream`.
|
||||
https://trello.com/c/moiiS2rP/57-pattern-for-creating-multiple-database-records-from-a-method
|
||||
|
||||
* The document passed to the `insert` callback of `allow` and `deny` now only
|
||||
has a `_id` field if the client explicitly specified one; this allows you to
|
||||
use `allow`/`deny` rules to prevent clients from specifying their own
|
||||
`_id`. As an exception, `allow`/`deny` rules with a `transform` always have an
|
||||
`_id`.
|
||||
|
||||
* DDP now has an implementation of bidirectional heartbeats which is consistent
|
||||
across SockJS and websocket transports. This enables connection keepalive and
|
||||
allows servers and clients to more consistently and efficiently detect
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.8.1.3
|
||||
0.8.2
|
||||
|
||||
@@ -962,6 +962,10 @@ The available callbacks are:
|
||||
{{#dtdd "insert(userId, doc)"}}
|
||||
The user `userId` wants to insert the document `doc` into the
|
||||
collection. Return `true` if this should be allowed.
|
||||
|
||||
`doc` will contain the `_id` field if one was explicitly set by the client, or
|
||||
if there is an active `transform`. You can use this to prevent users from
|
||||
specifying arbitrary `_id` fields.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "update(userId, doc, fieldNames, modifier)"}}
|
||||
@@ -1113,12 +1117,6 @@ Unlike the other functions, `count` registers a dependency only on the
|
||||
number of matching documents. (Updates that just change or reorder the
|
||||
documents in the result set will not trigger a recomputation.)
|
||||
|
||||
{{> api_box cursor_rewind}}
|
||||
|
||||
The `forEach`, `map`, or `fetch` methods can only be called once on a
|
||||
cursor. To access the data in a cursor more than once, use `rewind` to
|
||||
reset the cursor.
|
||||
|
||||
{{> api_box cursor_observe}}
|
||||
|
||||
Establishes a *live query* that invokes callbacks when the result of
|
||||
@@ -2150,11 +2148,11 @@ in the DOM.
|
||||
|
||||
{{> api_box template_findAll}}
|
||||
|
||||
Returns a [jQuery object](http://api.jquery.com/Types/#jQuery) of DOM elements
|
||||
matching `selector`. This object is similar to an array but has other methods
|
||||
defined by the jQuery library.
|
||||
`this.findAll` returns an array of DOM elements matching `selector`.
|
||||
|
||||
You can also call this function as `this.$(selector)`.
|
||||
`this.$` returns a [jQuery object](http://api.jquery.com/Types/#jQuery) of
|
||||
those same elements. jQuery objects are similar to arrays, with
|
||||
additional methods defined by the jQuery library.
|
||||
|
||||
The template instance serves as the document root for the selector. Only
|
||||
elements inside the template and its sub-templates can match parts of
|
||||
@@ -3165,8 +3163,3 @@ code can read `data.txt` by running:
|
||||
{{/each}}
|
||||
</dl>
|
||||
</template>
|
||||
|
||||
|
||||
<template name="api_section">
|
||||
<h2 id="{{id}}"><a href="#{{id}}" class="selflink"><span>{{name}}</span></a></h2>
|
||||
</template>
|
||||
|
||||
@@ -404,7 +404,7 @@ Template.api.method_invocation_connection = {
|
||||
|
||||
Template.api.error = {
|
||||
id: "meteor_error",
|
||||
name: "new Meteor.Error(error, reason, details)",
|
||||
name: "new Meteor.Error(error [, reason] [, details])",
|
||||
locus: "Anywhere",
|
||||
descr: ["This class represents a symbolic error thrown by a method."],
|
||||
args: [
|
||||
@@ -779,14 +779,6 @@ Template.api.cursor_map = {
|
||||
]
|
||||
};
|
||||
|
||||
Template.api.cursor_rewind = {
|
||||
id: "rewind",
|
||||
name: "<em>cursor</em>.rewind()",
|
||||
locus: "Anywhere",
|
||||
descr: ["Resets the query cursor."],
|
||||
args: [ ]
|
||||
};
|
||||
|
||||
Template.api.cursor_observe = {
|
||||
id: "observe",
|
||||
name: "<em>cursor</em>.observe(callbacks)",
|
||||
@@ -1133,6 +1125,11 @@ Template.api.loginWithExternalService = {
|
||||
name: "forceApprovalPrompt",
|
||||
type: "Boolean",
|
||||
descr: "If true, forces the user to approve the app's permissions, even if previously approved. Currently only supported with Google."
|
||||
},
|
||||
{
|
||||
name: "userEmail",
|
||||
type: "String",
|
||||
descr: "An email address that the external service will use to pre-fill the login prompt. Currently only supported with Meteor developer accounts."
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -1805,7 +1802,7 @@ Template.api.template_helpers = {
|
||||
|
||||
Template.api.template_findAll = {
|
||||
id: "template_findAll",
|
||||
name: "<em>this</em>.findAll(selector)",
|
||||
name: "<em>this</em>.findAll(selector) and <em>this</em>.$(selector)",
|
||||
locus: "Client",
|
||||
descr: ["Find all elements matching `selector` in this template instance."],
|
||||
args: [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
{{#markdown}}
|
||||
|
||||
{{#api_section "commandline"}}Command line{{/api_section}}
|
||||
<h1 id="commandline">Command line</h1>
|
||||
|
||||
<!-- XXX some intro text? -->
|
||||
|
||||
@@ -55,6 +55,15 @@ then you'll need to make sure the DNS for that domain is configured to
|
||||
point at `origin.meteor.com`.
|
||||
|
||||
|
||||
The first time you deploy an app you'll be prompted for an email address —
|
||||
follow the link in your email to finish setting up your account.
|
||||
|
||||
|
||||
Once you have your account you can log in and log out from the command line,
|
||||
check your username with `meteor whoami`, and run `meteor authorized` to give
|
||||
other Meteor developers permissions to deploy your app and access its database
|
||||
and logs.
|
||||
|
||||
|
||||
You can deploy in debug mode by passing `--debug`. This
|
||||
will leave your source code readable by your favorite in-browser
|
||||
@@ -67,24 +76,6 @@ the `--delete` option along with the site.
|
||||
|
||||
|
||||
|
||||
To add an administrative password to your deployment, include
|
||||
the `--password` option. Meteor will prompt
|
||||
for a password. Once set, any future `meteor deploy` to
|
||||
the same domain will require that you provide the password. The same
|
||||
password protects access to `meteor mongo`
|
||||
and `meteor logs`. You can change the password by
|
||||
running `meteor deploy --password` again,
|
||||
which will first prompt for the current password, then for a new
|
||||
password.
|
||||
|
||||
|
||||
{{#warning}}
|
||||
Password protection only applies to administrative actions with the
|
||||
Meteor command. It does not prevent access to your deployed
|
||||
website. Also, this all is a temporary hack until we have
|
||||
full-featured Meteor accounts.
|
||||
{{/warning}}
|
||||
|
||||
{{#warning}}
|
||||
If you use a domain name other than `meteor.com`
|
||||
you must ensure that the name resolves
|
||||
|
||||
@@ -140,9 +140,8 @@ functions, available under the `Template` namespace. It's
|
||||
a really convenient way to ship HTML templates to the client.
|
||||
See the [templates](#livehtmltemplates) section for more.
|
||||
|
||||
Lastly, the Meteor server will serve any files under the `public`
|
||||
directory, just like in a Rails or Django project. This is the place
|
||||
for images, `favicon.ico`, `robots.txt`, and anything else.
|
||||
Lastly, the Meteor server will serve any files under the `public` directory.
|
||||
This is the place for images, `favicon.ico`, `robots.txt`, and anything else.
|
||||
|
||||
It is best to write your application in such a way that it is
|
||||
insensitive to the order in which files are loaded, for example by
|
||||
@@ -448,7 +447,7 @@ extension. In the file, make a `<template>` tag and give it a
|
||||
will precompile the template, ship it down to the client, and make it
|
||||
available as on the global `Template` object.
|
||||
|
||||
When you app is loaded, it automatically renders the special template called
|
||||
When your app is loaded, it automatically renders the special template called
|
||||
`<body>`, which is written using the `<body>` element instead of a
|
||||
`<template>`. You insert a template inside another template by using the
|
||||
`{{dstache}}> inclusion}}` operator.
|
||||
@@ -741,7 +740,7 @@ To get started, run
|
||||
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.10 and a
|
||||
MongoDB server. (The current release of Meteor has been tested with Node
|
||||
0.10.26; older versions contain a serious bug that can cause production servers
|
||||
0.10.28; older versions contain a serious bug that can cause production servers
|
||||
to stall.) 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
|
||||
|
||||
@@ -181,7 +181,6 @@ var toc = [
|
||||
{instance: "cursor", name: "map"},
|
||||
{instance: "cursor", name: "fetch"},
|
||||
{instance: "cursor", name: "count"},
|
||||
{instance: "cursor", name: "rewind"},
|
||||
{instance: "cursor", name: "observe"},
|
||||
{instance: "cursor", name: "observeChanges", id: "observe_changes"}
|
||||
],
|
||||
@@ -250,6 +249,7 @@ var toc = [
|
||||
],
|
||||
{name: "Template instances", id: "template_inst"}, [
|
||||
{instance: "this", name: "findAll", id: "template_findAll"},
|
||||
{instance: "this", name: "$", id: "template_findAll"},
|
||||
{instance: "this", name: "find", id: "template_find"},
|
||||
{instance: "this", name: "firstNode", id: "template_firstNode"},
|
||||
{instance: "this", name: "lastNode", id: "template_lastNode"},
|
||||
|
||||
@@ -169,6 +169,12 @@ sites can frame your site, while
|
||||
`BrowserPolicy.content.allowFrameOrigin` allows you to control which
|
||||
sites can be loaded inside frames on your site.
|
||||
|
||||
Adding `browser-policy-content` to your app also tells certain
|
||||
browsers to avoid sniffing content types away from the declared type
|
||||
(for example, interpreting a text file as JavaScript), using the
|
||||
[X-Content-Type-Options](http://msdn.microsoft.com/en-us/library/ie/gg622941(v=vs.85).aspx)
|
||||
header. To re-enable content sniffing, you can call
|
||||
`BrowserPolicy.content.allowContentTypeSniffing()`.
|
||||
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// While galaxy apps are on their own special meteor releases, override
|
||||
// Meteor.release here.
|
||||
if (Meteor.isClient) {
|
||||
Meteor.release = Meteor.release ? "0.8.1.3" : undefined;
|
||||
Meteor.release = Meteor.release ? "0.8.2" : undefined;
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.8.1.3
|
||||
0.8.2
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.8.1.3
|
||||
0.8.2
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.8.1.3
|
||||
0.8.2
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.8.1.3
|
||||
0.8.2
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.8.1.3
|
||||
0.8.2
|
||||
|
||||
2
meteor
2
meteor
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
BUNDLE_VERSION=0.3.34
|
||||
BUNDLE_VERSION=0.3.37
|
||||
|
||||
# OS Check. Put here because here is where we download the precompiled
|
||||
# bundles that are arch specific.
|
||||
|
||||
@@ -544,7 +544,7 @@ Meteor.methods({
|
||||
/// ACCOUNT DATA
|
||||
///
|
||||
|
||||
// connectionId -> {connection, loginToken, srpChallenge}
|
||||
// connectionId -> {connection, loginToken}
|
||||
var accountData = {};
|
||||
|
||||
// HACK: This is used by 'meteor-accounts' to get the loginToken for a
|
||||
|
||||
@@ -10,7 +10,7 @@ if (Meteor.isClient) {
|
||||
|
||||
var credentialRequestCompleteCallback =
|
||||
Accounts.oauth.credentialRequestCompleteHandler(callback);
|
||||
MeteorDeveloperAccounts.requestCredential(credentialRequestCompleteCallback);
|
||||
MeteorDeveloperAccounts.requestCredential(options, credentialRequestCompleteCallback);
|
||||
};
|
||||
} else {
|
||||
Accounts.addAutopublishFields({
|
||||
|
||||
@@ -7,6 +7,7 @@ Package.on_use(function(api) {
|
||||
// Export Accounts (etc) to packages using this one.
|
||||
api.imply('accounts-base', ['client', 'server']);
|
||||
api.use('srp', ['client', 'server']);
|
||||
api.use('sha', ['client', 'server']);
|
||||
api.use('email', ['server']);
|
||||
api.use('random', ['server']);
|
||||
api.use('check', ['server']);
|
||||
|
||||
@@ -8,41 +8,80 @@
|
||||
// @param password {String}
|
||||
// @param callback {Function(error|undefined)}
|
||||
Meteor.loginWithPassword = function (selector, password, callback) {
|
||||
var srp = new SRP.Client(password);
|
||||
var request = srp.startExchange();
|
||||
|
||||
if (typeof selector === 'string')
|
||||
if (selector.indexOf('@') === -1)
|
||||
selector = {username: selector};
|
||||
else
|
||||
selector = {email: selector};
|
||||
|
||||
request.user = selector;
|
||||
Accounts.callLoginMethod({
|
||||
methodArguments: [{
|
||||
user: selector,
|
||||
password: hashPassword(password)
|
||||
}],
|
||||
userCallback: function (error, result) {
|
||||
if (error && error.error === 400 &&
|
||||
error.reason === 'old password format') {
|
||||
// The "reason" string should match the error thrown in the
|
||||
// password login handler in password_server.js.
|
||||
|
||||
// Normally, we only set Meteor.loggingIn() to true within
|
||||
// Accounts.callLoginMethod, but we'd also like it to be true during the
|
||||
// password exchange. So we set it to true here, and clear it on error; in
|
||||
// the non-error case, it gets cleared by callLoginMethod.
|
||||
Accounts._setLoggingIn(true);
|
||||
Accounts.connection.apply(
|
||||
'beginPasswordExchange', [request], function (error, result) {
|
||||
if (error || !result) {
|
||||
Accounts._setLoggingIn(false);
|
||||
error = error ||
|
||||
new Error("No result from call to beginPasswordExchange");
|
||||
callback && callback(error);
|
||||
return;
|
||||
// XXX COMPAT WITH 0.8.1.3
|
||||
// If this user's last login was with a previous version of
|
||||
// Meteor that used SRP, then the server throws this error to
|
||||
// indicate that we should try again. The error includes the
|
||||
// user's SRP identity. We provide a value derived from the
|
||||
// identity and the password to prove to the server that we know
|
||||
// the password without requiring a full SRP flow, as well as
|
||||
// SHA256(password), which the server bcrypts and stores in
|
||||
// place of the old SRP information for this user.
|
||||
srpUpgradePath({
|
||||
upgradeError: error,
|
||||
userSelector: selector,
|
||||
plaintextPassword: password
|
||||
}, callback);
|
||||
}
|
||||
else if (error) {
|
||||
callback(error);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var response = srp.respondToChallenge(result);
|
||||
Accounts.callLoginMethod({
|
||||
methodArguments: [{srp: response}],
|
||||
validateResult: function (result) {
|
||||
if (!srp.verifyConfirmation({HAMK: result.HAMK}))
|
||||
throw new Error("Server is cheating!");
|
||||
},
|
||||
userCallback: callback});
|
||||
var hashPassword = function (password) {
|
||||
return {
|
||||
digest: SHA256(password),
|
||||
algorithm: "sha-256"
|
||||
};
|
||||
};
|
||||
|
||||
// XXX COMPAT WITH 0.8.1.3
|
||||
// The server requested an upgrade from the old SRP password format,
|
||||
// so supply the needed SRP identity to login. Options:
|
||||
// - upgradeError: the error object that the server returned to tell
|
||||
// us to upgrade from SRP to bcrypt.
|
||||
// - userSelector: selector to retrieve the user object
|
||||
// - plaintextPassword: the password as a string
|
||||
var srpUpgradePath = function (options, callback) {
|
||||
var details;
|
||||
try {
|
||||
details = EJSON.parse(options.upgradeError.details);
|
||||
} catch (e) {}
|
||||
if (!(details && details.format === 'srp')) {
|
||||
callback(new Meteor.Error(400,
|
||||
"Password is old. Please reset your " +
|
||||
"password."));
|
||||
} else {
|
||||
Accounts.callLoginMethod({
|
||||
methodArguments: [{
|
||||
user: options.userSelector,
|
||||
srp: SHA256(details.identity + ":" + options.plaintextPassword),
|
||||
password: hashPassword(options.plaintextPassword)
|
||||
}],
|
||||
userCallback: callback
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -52,10 +91,9 @@ Accounts.createUser = function (options, callback) {
|
||||
|
||||
if (!options.password)
|
||||
throw new Error("Must set options.password");
|
||||
var verifier = SRP.generateVerifier(options.password);
|
||||
// strip old password, replacing with the verifier object
|
||||
delete options.password;
|
||||
options.srp = verifier;
|
||||
|
||||
// Replace password with the hashed password.
|
||||
options.password = hashPassword(options.password);
|
||||
|
||||
Accounts.callLoginMethod({
|
||||
methodName: 'createUser',
|
||||
@@ -79,49 +117,39 @@ Accounts.changePassword = function (oldPassword, newPassword, callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
var verifier = SRP.generateVerifier(newPassword);
|
||||
|
||||
if (!oldPassword) {
|
||||
Accounts.connection.apply(
|
||||
'changePassword', [{srp: verifier}], function (error, result) {
|
||||
if (error || !result) {
|
||||
callback && callback(
|
||||
error || new Error("No result from changePassword."));
|
||||
} else {
|
||||
callback && callback();
|
||||
}
|
||||
});
|
||||
} else { // oldPassword
|
||||
var srp = new SRP.Client(oldPassword);
|
||||
var request = srp.startExchange();
|
||||
request.user = {id: Meteor.user()._id};
|
||||
Accounts.connection.apply(
|
||||
'beginPasswordExchange', [request], function (error, result) {
|
||||
if (error || !result) {
|
||||
callback && callback(
|
||||
error || new Error("No result from call to beginPasswordExchange"));
|
||||
return;
|
||||
}
|
||||
|
||||
var response = srp.respondToChallenge(result);
|
||||
response.srp = verifier;
|
||||
Accounts.connection.apply(
|
||||
'changePassword', [response],function (error, result) {
|
||||
if (error || !result) {
|
||||
callback && callback(
|
||||
error || new Error("No result from changePassword."));
|
||||
Accounts.connection.apply(
|
||||
'changePassword',
|
||||
[oldPassword ? hashPassword(oldPassword) : null, hashPassword(newPassword)],
|
||||
function (error, result) {
|
||||
if (error || !result) {
|
||||
if (error && error.error === 400 &&
|
||||
error.reason === 'old password format') {
|
||||
// XXX COMPAT WITH 0.8.1.3
|
||||
// The server is telling us to upgrade from SRP to bcrypt, as
|
||||
// in Meteor.loginWithPassword.
|
||||
srpUpgradePath({
|
||||
upgradeError: error,
|
||||
userSelector: { id: Meteor.userId() },
|
||||
plaintextPassword: oldPassword
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
} else {
|
||||
if (!srp.verifyConfirmation(result)) {
|
||||
// Monkey business!
|
||||
callback &&
|
||||
callback(new Error("Old password verification failed."));
|
||||
} else {
|
||||
callback && callback();
|
||||
}
|
||||
// Now that we've successfully migrated from srp to
|
||||
// bcrypt, try changing the password again.
|
||||
Accounts.changePassword(oldPassword, newPassword, callback);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// A normal error, not an error telling us to upgrade to bcrypt
|
||||
callback && callback(
|
||||
error || new Error("No result from changePassword."));
|
||||
}
|
||||
} else {
|
||||
callback && callback();
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Sends an email to a user with a link that can be used to reset
|
||||
@@ -148,10 +176,9 @@ Accounts.resetPassword = function(token, newPassword, callback) {
|
||||
if (!newPassword)
|
||||
throw new Error("Need to pass newPassword");
|
||||
|
||||
var verifier = SRP.generateVerifier(newPassword);
|
||||
Accounts.callLoginMethod({
|
||||
methodName: 'resetPassword',
|
||||
methodArguments: [token, verifier],
|
||||
methodArguments: [token, hashPassword(newPassword)],
|
||||
userCallback: callback});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,78 @@
|
||||
/// BCRYPT
|
||||
|
||||
var bcrypt = Npm.require('bcrypt');
|
||||
var bcryptHash = Meteor._wrapAsync(bcrypt.hash);
|
||||
var bcryptCompare = Meteor._wrapAsync(bcrypt.compare);
|
||||
|
||||
// User records have a 'services.password.bcrypt' field on them to hold
|
||||
// their hashed passwords (unless they have a 'services.password.srp'
|
||||
// field, in which case they will be upgraded to bcrypt the next time
|
||||
// they log in).
|
||||
//
|
||||
// When the client sends a password to the server, it can either be a
|
||||
// string (the plaintext password) or an object with keys 'digest' and
|
||||
// 'algorithm' (must be "sha-256" for now). The Meteor client always sends
|
||||
// password objects { digest: *, algorithm: "sha-256" }, but DDP clients
|
||||
// that don't have access to SHA can just send plaintext passwords as
|
||||
// strings.
|
||||
//
|
||||
// When the server receives a plaintext password as a string, it always
|
||||
// hashes it with SHA256 before passing it into bcrypt. When the server
|
||||
// receives a password as an object, it asserts that the algorithm is
|
||||
// "sha-256" and then passes the digest to bcrypt.
|
||||
|
||||
|
||||
Accounts._bcryptRounds = 10;
|
||||
|
||||
// Given a 'password' from the client, extract the string that we should
|
||||
// bcrypt. 'password' can be one of:
|
||||
// - String (the plaintext password)
|
||||
// - Object with 'digest' and 'algorithm' keys. 'algorithm' must be "sha-256".
|
||||
//
|
||||
var getPasswordString = function (password) {
|
||||
if (typeof password === "string") {
|
||||
password = SHA256(password);
|
||||
} else { // 'password' is an object
|
||||
if (password.algorithm !== "sha-256") {
|
||||
throw new Error("Invalid password hash algorithm. " +
|
||||
"Only 'sha-256' is allowed.");
|
||||
}
|
||||
password = password.digest;
|
||||
}
|
||||
return password;
|
||||
};
|
||||
|
||||
// Use bcrypt to hash the password for storage in the database.
|
||||
// `password` can be a string (in which case it will be run through
|
||||
// SHA256 before bcrypt) or an object with properties `digest` and
|
||||
// `algorithm` (in which case we bcrypt `password.digest`).
|
||||
//
|
||||
var hashPassword = function (password) {
|
||||
password = getPasswordString(password);
|
||||
return bcryptHash(password, Accounts._bcryptRounds);
|
||||
};
|
||||
|
||||
// Check whether the provided password matches the bcrypt'ed password in
|
||||
// the database user record. `password` can be a string (in which case
|
||||
// it will be run through SHA256 before bcrypt) or an object with
|
||||
// properties `digest` and `algorithm` (in which case we bcrypt
|
||||
// `password.digest`).
|
||||
//
|
||||
Accounts._checkPassword = function (user, password) {
|
||||
var result = {
|
||||
userId: user._id
|
||||
};
|
||||
|
||||
password = getPasswordString(password);
|
||||
|
||||
if (! bcryptCompare(password, user.services.password.bcrypt)) {
|
||||
result.error = new Meteor.Error(403, "Incorrect password");
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
var checkPassword = Accounts._checkPassword;
|
||||
|
||||
///
|
||||
/// LOGIN
|
||||
///
|
||||
@@ -16,6 +91,16 @@ var selectorFromUserQuery = function (user) {
|
||||
throw new Error("shouldn't happen (validation missed something)");
|
||||
};
|
||||
|
||||
var findUserFromUserQuery = function (user) {
|
||||
var selector = selectorFromUserQuery(user);
|
||||
|
||||
var user = Meteor.users.findOne(selector);
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "User not found");
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
// XXX maybe this belongs in the check package
|
||||
var NonEmptyString = Match.Where(function (x) {
|
||||
check(x, String);
|
||||
@@ -33,134 +118,133 @@ var userQueryValidator = Match.Where(function (user) {
|
||||
return true;
|
||||
});
|
||||
|
||||
// Step 1 of SRP password exchange. This puts an `M` value in the
|
||||
// session data for this connection. If a client later sends the same
|
||||
// `M` value to a method on this connection, it proves they know the
|
||||
// password for this user. We can then prove we know the password to
|
||||
// them by sending our `HAMK` value.
|
||||
var passwordValidator = Match.OneOf(
|
||||
String,
|
||||
{ digest: String, algorithm: String }
|
||||
);
|
||||
|
||||
// Handler to login with a password.
|
||||
//
|
||||
// The Meteor client sets options.password to an object with keys
|
||||
// 'digest' (set to SHA256(password)) and 'algorithm' ("sha-256").
|
||||
//
|
||||
// For other DDP clients which don't have access to SHA, the handler
|
||||
// also accepts the plaintext password in options.password as a string.
|
||||
//
|
||||
// (It might be nice if servers could turn the plaintext password
|
||||
// option off. Or maybe it should be opt-in, not opt-out?
|
||||
// Accounts.config option?)
|
||||
//
|
||||
// Note that neither password option is secure without SSL.
|
||||
//
|
||||
// @param request {Object} with fields:
|
||||
// user: either {username: (username)}, {email: (email)}, or {id: (userId)}
|
||||
// A: hex encoded int. the client's public key for this exchange
|
||||
// @returns {Object} with fields:
|
||||
// identity: random string ID
|
||||
// salt: random string ID
|
||||
// B: hex encoded int. server's public key for this exchange
|
||||
Meteor.methods({beginPasswordExchange: function (request) {
|
||||
var self = this;
|
||||
try {
|
||||
check(request, {
|
||||
user: userQueryValidator,
|
||||
A: String
|
||||
});
|
||||
var selector = selectorFromUserQuery(request.user);
|
||||
|
||||
var user = Meteor.users.findOne(selector);
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "User not found");
|
||||
|
||||
if (!user.services || !user.services.password ||
|
||||
!user.services.password.srp)
|
||||
throw new Meteor.Error(403, "User has no password set");
|
||||
|
||||
var verifier = user.services.password.srp;
|
||||
var srp = new SRP.Server(verifier);
|
||||
var challenge = srp.issueChallenge({A: request.A});
|
||||
|
||||
} catch (err) {
|
||||
// Report login failure if the method fails, so that login hooks are
|
||||
// called. If the method succeeds, login hooks will be called when
|
||||
// the second step method ('login') is called. If a user calls
|
||||
// 'beginPasswordExchange' but then never calls the second step
|
||||
// 'login' method, no login hook will fire.
|
||||
// The validate login hooks can mutate the exception to be thrown.
|
||||
var attempt = Accounts._reportLoginFailure(self, 'beginPasswordExchange', arguments, {
|
||||
type: 'password',
|
||||
error: err,
|
||||
userId: user && user._id
|
||||
});
|
||||
throw attempt.error;
|
||||
}
|
||||
|
||||
// Save results so we can verify them later.
|
||||
Accounts._setAccountData(this.connection.id, 'srpChallenge',
|
||||
{ userId: user._id, M: srp.M, HAMK: srp.HAMK }
|
||||
);
|
||||
return challenge;
|
||||
}});
|
||||
|
||||
// Handler to login with password via SRP. Checks the `M` value set by
|
||||
// beginPasswordExchange.
|
||||
Accounts.registerLoginHandler("password", function (options) {
|
||||
if (!options.srp)
|
||||
return undefined; // don't handle
|
||||
check(options.srp, {M: String});
|
||||
|
||||
// we're always called from within a 'login' method, so this should
|
||||
// be safe.
|
||||
var currentInvocation = DDP._CurrentInvocation.get();
|
||||
var serialized = Accounts._getAccountData(currentInvocation.connection.id, 'srpChallenge');
|
||||
if (!serialized || serialized.M !== options.srp.M)
|
||||
return {
|
||||
userId: serialized && serialized.userId,
|
||||
error: new Meteor.Error(403, "Incorrect password")
|
||||
};
|
||||
// Only can use challenges once.
|
||||
Accounts._setAccountData(currentInvocation.connection.id, 'srpChallenge', undefined);
|
||||
|
||||
var userId = serialized.userId;
|
||||
var user = Meteor.users.findOne(userId);
|
||||
// Was the user deleted since the start of this challenge?
|
||||
if (!user)
|
||||
return {
|
||||
userId: userId,
|
||||
error: new Meteor.Error(403, "User not found")
|
||||
};
|
||||
|
||||
return {
|
||||
userId: userId,
|
||||
options: {HAMK: serialized.HAMK}
|
||||
};
|
||||
});
|
||||
|
||||
// Handler to login with plaintext password.
|
||||
//
|
||||
// The meteor client doesn't use this, it is for other DDP clients who
|
||||
// haven't implemented SRP. Since it sends the password in plaintext
|
||||
// over the wire, it should only be run over SSL!
|
||||
//
|
||||
// Also, it might be nice if servers could turn this off. Or maybe it
|
||||
// should be opt-in, not opt-out? Accounts.config option?
|
||||
Accounts.registerLoginHandler("password", function (options) {
|
||||
if (!options.password || !options.user)
|
||||
if (! options.password || options.srp)
|
||||
return undefined; // don't handle
|
||||
|
||||
check(options, {user: userQueryValidator, password: String});
|
||||
check(options, {
|
||||
user: userQueryValidator,
|
||||
password: passwordValidator
|
||||
});
|
||||
|
||||
var selector = selectorFromUserQuery(options.user);
|
||||
var user = Meteor.users.findOne(selector);
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "User not found");
|
||||
|
||||
var user = findUserFromUserQuery(options.user);
|
||||
|
||||
if (!user.services || !user.services.password ||
|
||||
!user.services.password.srp)
|
||||
return {
|
||||
userId: user._id,
|
||||
error: new Meteor.Error(403, "User has no password set")
|
||||
};
|
||||
!(user.services.password.bcrypt || user.services.password.srp))
|
||||
throw new Meteor.Error(403, "User has no password set");
|
||||
|
||||
// Just check the verifier output when the same identity and salt
|
||||
// are passed. Don't bother with a full exchange.
|
||||
var verifier = user.services.password.srp;
|
||||
var newVerifier = SRP.generateVerifier(options.password, {
|
||||
identity: verifier.identity, salt: verifier.salt});
|
||||
if (!user.services.password.bcrypt) {
|
||||
if (typeof options.password === "string") {
|
||||
// The client has presented a plaintext password, and the user is
|
||||
// not upgraded to bcrypt yet. We don't attempt to tell the client
|
||||
// to upgrade to bcrypt, because it might be a standalone DDP
|
||||
// client doesn't know how to do such a thing.
|
||||
var verifier = user.services.password.srp;
|
||||
var newVerifier = SRP.generateVerifier(options.password, {
|
||||
identity: verifier.identity, salt: verifier.salt});
|
||||
|
||||
if (verifier.verifier !== newVerifier.verifier)
|
||||
if (verifier.verifier !== newVerifier.verifier) {
|
||||
return {
|
||||
userId: user._id,
|
||||
error: new Meteor.Error(403, "Incorrect password")
|
||||
};
|
||||
}
|
||||
|
||||
return {userId: user._id};
|
||||
} else {
|
||||
// Tell the client to use the SRP upgrade process.
|
||||
throw new Meteor.Error(400, "old password format", EJSON.stringify({
|
||||
format: 'srp',
|
||||
identity: user.services.password.srp.identity
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return checkPassword(
|
||||
user,
|
||||
options.password
|
||||
);
|
||||
});
|
||||
|
||||
// Handler to login using the SRP upgrade path. To use this login
|
||||
// handler, the client must provide:
|
||||
// - srp: H(identity + ":" + password)
|
||||
// - password: a string or an object with properties 'digest' and 'algorithm'
|
||||
//
|
||||
// We use `options.srp` to verify that the client knows the correct
|
||||
// password without doing a full SRP flow. Once we've checked that, we
|
||||
// upgrade the user to bcrypt and remove the SRP information from the
|
||||
// user document.
|
||||
//
|
||||
// The client ends up using this login handler after trying the normal
|
||||
// login handler (above), which throws an error telling the client to
|
||||
// try the SRP upgrade path.
|
||||
//
|
||||
// XXX COMPAT WITH 0.8.1.3
|
||||
Accounts.registerLoginHandler("password", function (options) {
|
||||
if (!options.srp || !options.password)
|
||||
return undefined; // don't handle
|
||||
|
||||
check(options, {
|
||||
user: userQueryValidator,
|
||||
srp: String,
|
||||
password: passwordValidator
|
||||
});
|
||||
|
||||
var user = findUserFromUserQuery(options.user);
|
||||
|
||||
// Check to see if another simultaneous login has already upgraded
|
||||
// the user record to bcrypt.
|
||||
if (user.services && user.services.password && user.services.password.bcrypt)
|
||||
return checkPassword(user, options.password);
|
||||
|
||||
if (!(user.services && user.services.password && user.services.password.srp))
|
||||
throw new Meteor.Error(403, "User has no password set");
|
||||
|
||||
var v1 = user.services.password.srp.verifier;
|
||||
var v2 = SRP.generateVerifier(
|
||||
null,
|
||||
{
|
||||
hashedIdentityAndPassword: options.srp,
|
||||
salt: user.services.password.srp.salt
|
||||
}
|
||||
).verifier;
|
||||
if (v1 !== v2)
|
||||
return {
|
||||
userId: user._id,
|
||||
error: new Meteor.Error(403, "Incorrect password")
|
||||
};
|
||||
|
||||
// Upgrade to bcrypt on successful login.
|
||||
var salted = hashPassword(options.password);
|
||||
Meteor.users.update(
|
||||
user._id,
|
||||
{
|
||||
$unset: { 'services.password.srp': 1 },
|
||||
$set: { 'services.password.bcrypt': salted }
|
||||
}
|
||||
);
|
||||
|
||||
return {userId: user._id};
|
||||
});
|
||||
|
||||
@@ -170,34 +254,47 @@ Accounts.registerLoginHandler("password", function (options) {
|
||||
///
|
||||
|
||||
// Let the user change their own password if they know the old
|
||||
// password. Checks the `M` value set by beginPasswordExchange.
|
||||
Meteor.methods({changePassword: function (options) {
|
||||
// password. `oldPassword` and `newPassword` should be objects with keys
|
||||
// `digest` and `algorithm` (representing the SHA256 of the password).
|
||||
//
|
||||
// XXX COMPAT WITH 0.8.1.3
|
||||
// Like the login method, if the user hasn't been upgraded from SRP to
|
||||
// bcrypt yet, then this method will throw an 'old password format'
|
||||
// error. The client should call the SRP upgrade login handler and then
|
||||
// retry this method again.
|
||||
//
|
||||
// UNLIKE the login method, there is no way to avoid getting SRP upgrade
|
||||
// errors thrown. The reasoning for this is that clients using this
|
||||
// method directly will need to be updated anyway because we no longer
|
||||
// support the SRP flow that they would have been doing to use this
|
||||
// method previously.
|
||||
Meteor.methods({changePassword: function (oldPassword, newPassword) {
|
||||
check(oldPassword, passwordValidator);
|
||||
check(newPassword, passwordValidator);
|
||||
|
||||
if (!this.userId)
|
||||
throw new Meteor.Error(401, "Must be logged in");
|
||||
check(options, {
|
||||
// If options.M is set, it means we went through a challenge with the old
|
||||
// password. For now, we don't allow changePassword without knowing the old
|
||||
// password.
|
||||
M: String,
|
||||
srp: Match.Optional(SRP.matchVerifier),
|
||||
password: Match.Optional(String)
|
||||
});
|
||||
|
||||
var serialized = Accounts._getAccountData(this.connection.id, 'srpChallenge');
|
||||
if (!serialized || serialized.M !== options.M)
|
||||
throw new Meteor.Error(403, "Incorrect password");
|
||||
if (serialized.userId !== this.userId)
|
||||
// No monkey business!
|
||||
throw new Meteor.Error(403, "Incorrect password");
|
||||
// Only can use challenges once.
|
||||
Accounts._setAccountData(this.connection.id, 'srpChallenge', undefined);
|
||||
var user = Meteor.users.findOne(this.userId);
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "User not found");
|
||||
|
||||
var verifier = options.srp;
|
||||
if (!verifier && options.password) {
|
||||
verifier = SRP.generateVerifier(options.password);
|
||||
if (!user.services || !user.services.password ||
|
||||
(!user.services.password.bcrypt && !user.services.password.srp))
|
||||
throw new Meteor.Error(403, "User has no password set");
|
||||
|
||||
if (! user.services.password.bcrypt) {
|
||||
throw new Meteor.Error(400, "old password format", EJSON.stringify({
|
||||
format: 'srp',
|
||||
identity: user.services.password.srp.identity
|
||||
}));
|
||||
}
|
||||
if (!verifier)
|
||||
throw new Meteor.Error(400, "Invalid verifier");
|
||||
|
||||
var result = checkPassword(user, oldPassword);
|
||||
if (result.error)
|
||||
throw result.error;
|
||||
|
||||
var hashed = hashPassword(newPassword);
|
||||
|
||||
// It would be better if this removed ALL existing tokens and replaced
|
||||
// the token for the current connection with a new one, but that would
|
||||
@@ -207,29 +304,28 @@ Meteor.methods({changePassword: function (options) {
|
||||
Meteor.users.update(
|
||||
{ _id: this.userId },
|
||||
{
|
||||
$set: { 'services.password.srp': verifier },
|
||||
$set: { 'services.password.bcrypt': hashed },
|
||||
$pull: {
|
||||
'services.resume.loginTokens': { hashedToken: { $ne: currentToken } }
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
var ret = {passwordChanged: true};
|
||||
if (serialized)
|
||||
ret.HAMK = serialized.HAMK;
|
||||
return ret;
|
||||
return {passwordChanged: true};
|
||||
}});
|
||||
|
||||
|
||||
// Force change the users password.
|
||||
Accounts.setPassword = function (userId, newPassword) {
|
||||
Accounts.setPassword = function (userId, newPlaintextPassword) {
|
||||
var user = Meteor.users.findOne(userId);
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "User not found");
|
||||
var newVerifier = SRP.generateVerifier(newPassword);
|
||||
|
||||
Meteor.users.update({_id: user._id}, {
|
||||
$set: {'services.password.srp': newVerifier}});
|
||||
Meteor.users.update(
|
||||
{_id: user._id},
|
||||
{ $unset: {'services.password.srp': 1}, // XXX COMPAT WITH 0.8.1.3
|
||||
$set: {'services.password.bcrypt': hashPassword(newPlaintextPassword)} }
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -266,13 +362,16 @@ Accounts.sendResetPasswordEmail = function (userId, email) {
|
||||
|
||||
var token = Random.secret();
|
||||
var when = new Date();
|
||||
var tokenRecord = {
|
||||
token: token,
|
||||
email: email,
|
||||
when: when
|
||||
};
|
||||
Meteor.users.update(userId, {$set: {
|
||||
"services.password.reset": {
|
||||
token: token,
|
||||
email: email,
|
||||
when: when
|
||||
}
|
||||
"services.password.reset": tokenRecord
|
||||
}});
|
||||
// before passing to template, update user object with new token
|
||||
user.services.password.reset = tokenRecord;
|
||||
|
||||
var resetPasswordUrl = Accounts.urls.resetPassword(token);
|
||||
|
||||
@@ -312,17 +411,21 @@ Accounts.sendEnrollmentEmail = function (userId, email) {
|
||||
if (!email || !_.contains(_.pluck(user.emails || [], 'address'), email))
|
||||
throw new Error("No such email for user.");
|
||||
|
||||
|
||||
var token = Random.secret();
|
||||
var when = new Date();
|
||||
var tokenRecord = {
|
||||
token: token,
|
||||
email: email,
|
||||
when: when
|
||||
};
|
||||
Meteor.users.update(userId, {$set: {
|
||||
"services.password.reset": {
|
||||
token: token,
|
||||
email: email,
|
||||
when: when
|
||||
}
|
||||
"services.password.reset": tokenRecord
|
||||
}});
|
||||
|
||||
// before passing to template, update user object with new token
|
||||
Meteor._ensure(user, "services", "password");
|
||||
user.services.password.reset = tokenRecord;
|
||||
|
||||
var enrollAccountUrl = Accounts.urls.enrollAccount(token);
|
||||
|
||||
var options = {
|
||||
@@ -342,7 +445,7 @@ Accounts.sendEnrollmentEmail = function (userId, email) {
|
||||
|
||||
// Take token from sendResetPasswordEmail or sendEnrollmentEmail, change
|
||||
// the users password, and log them in.
|
||||
Meteor.methods({resetPassword: function (token, newVerifier) {
|
||||
Meteor.methods({resetPassword: function (token, newPassword) {
|
||||
var self = this;
|
||||
return Accounts._loginMethod(
|
||||
self,
|
||||
@@ -351,10 +454,10 @@ Meteor.methods({resetPassword: function (token, newVerifier) {
|
||||
"password",
|
||||
function () {
|
||||
check(token, String);
|
||||
check(newVerifier, SRP.matchVerifier);
|
||||
check(newPassword, passwordValidator);
|
||||
|
||||
var user = Meteor.users.findOne({
|
||||
"services.password.reset.token": ""+token});
|
||||
"services.password.reset.token": token});
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "Token expired");
|
||||
var email = user.services.password.reset.email;
|
||||
@@ -364,6 +467,8 @@ Meteor.methods({resetPassword: function (token, newVerifier) {
|
||||
error: new Meteor.Error(403, "Token has invalid email address")
|
||||
};
|
||||
|
||||
var hashed = hashPassword(newPassword);
|
||||
|
||||
// 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
|
||||
@@ -376,7 +481,7 @@ Meteor.methods({resetPassword: function (token, newVerifier) {
|
||||
|
||||
try {
|
||||
// Update the user record by:
|
||||
// - Changing the password verifier to the new one
|
||||
// - Changing the password to the new one
|
||||
// - Forgetting about the reset token that was just used
|
||||
// - Verifying their email, since they got the password reset via email.
|
||||
var affectedRecords = Meteor.users.update(
|
||||
@@ -385,9 +490,10 @@ Meteor.methods({resetPassword: function (token, newVerifier) {
|
||||
'emails.address': email,
|
||||
'services.password.reset.token': token
|
||||
},
|
||||
{$set: {'services.password.srp': newVerifier,
|
||||
{$set: {'services.password.bcrypt': hashed,
|
||||
'emails.$.verified': true},
|
||||
$unset: {'services.password.reset': 1}});
|
||||
$unset: {'services.password.reset': 1,
|
||||
'services.password.srp': 1}});
|
||||
if (affectedRecords !== 1)
|
||||
return {
|
||||
userId: user._id,
|
||||
@@ -443,6 +549,13 @@ Accounts.sendVerificationEmail = function (userId, address) {
|
||||
{_id: userId},
|
||||
{$push: {'services.email.verificationTokens': tokenRecord}});
|
||||
|
||||
// before passing to template, update user object with new token
|
||||
Meteor._ensure(user, "services", "email");
|
||||
if (! user.services.email.verificationTokens) {
|
||||
user.services.email.verificationTokens = [];
|
||||
}
|
||||
user.services.email.verificationTokens.push(tokenRecord);
|
||||
|
||||
var verifyEmailUrl = Accounts.urls.verifyEmail(tokenRecord.token);
|
||||
|
||||
var options = {
|
||||
@@ -528,8 +641,7 @@ var createUser = function (options) {
|
||||
check(options, Match.ObjectIncluding({
|
||||
username: Match.Optional(String),
|
||||
email: Match.Optional(String),
|
||||
password: Match.Optional(String),
|
||||
srp: Match.Optional(SRP.matchVerifier)
|
||||
password: Match.Optional(passwordValidator)
|
||||
}));
|
||||
|
||||
var username = options.username;
|
||||
@@ -537,18 +649,12 @@ var createUser = function (options) {
|
||||
if (!username && !email)
|
||||
throw new Meteor.Error(400, "Need to set a username or email");
|
||||
|
||||
// Raw password. The meteor client doesn't send this, but a DDP
|
||||
// client that didn't implement SRP could send this. This should
|
||||
// only be done over SSL.
|
||||
var user = {services: {}};
|
||||
if (options.password) {
|
||||
if (options.srp)
|
||||
throw new Meteor.Error(400, "Don't pass both password and srp in options");
|
||||
options.srp = SRP.generateVerifier(options.password);
|
||||
var hashed = hashPassword(options.password);
|
||||
user.services.password = { bcrypt: hashed };
|
||||
}
|
||||
|
||||
var user = {services: {}};
|
||||
if (options.srp)
|
||||
user.services.password = {srp: options.srp}; // XXX validate verifier
|
||||
if (username)
|
||||
user.username = username;
|
||||
if (email)
|
||||
|
||||
@@ -732,6 +732,86 @@ if (Meteor.isClient) (function () {
|
||||
}
|
||||
]);
|
||||
|
||||
testAsyncMulti("passwords - srp to bcrypt upgrade", [
|
||||
logoutStep,
|
||||
// Create user with old SRP credentials in the database.
|
||||
function (test, expect) {
|
||||
var self = this;
|
||||
Meteor.call("testCreateSRPUser", expect(function (error, result) {
|
||||
test.isFalse(error);
|
||||
self.username = result;
|
||||
}));
|
||||
},
|
||||
// We are able to login with the old style credentials in the database.
|
||||
function (test, expect) {
|
||||
Meteor.loginWithPassword(this.username, 'abcdef', expect(function (error) {
|
||||
test.isFalse(error);
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
Meteor.call("testSRPUpgrade", this.username, expect(function (error) {
|
||||
test.isFalse(error);
|
||||
}));
|
||||
},
|
||||
logoutStep,
|
||||
// After the upgrade to bcrypt we're still able to login.
|
||||
function (test, expect) {
|
||||
Meteor.loginWithPassword(this.username, 'abcdef', expect(function (error) {
|
||||
test.isFalse(error);
|
||||
}));
|
||||
},
|
||||
logoutStep,
|
||||
function (test, expect) {
|
||||
Meteor.call("removeUser", this.username, expect(function (error) {
|
||||
test.isFalse(error);
|
||||
}));
|
||||
}
|
||||
]);
|
||||
|
||||
testAsyncMulti("passwords - srp to bcrypt upgrade via password change", [
|
||||
logoutStep,
|
||||
// Create user with old SRP credentials in the database.
|
||||
function (test, expect) {
|
||||
var self = this;
|
||||
Meteor.call("testCreateSRPUser", expect(function (error, result) {
|
||||
test.isFalse(error);
|
||||
self.username = result;
|
||||
}));
|
||||
},
|
||||
// Log in with the plaintext password handler, which should NOT upgrade us to bcrypt.
|
||||
function (test, expect) {
|
||||
Accounts.callLoginMethod({
|
||||
methodName: "login",
|
||||
methodArguments: [ { user: { username: this.username }, password: "abcdef" } ],
|
||||
userCallback: expect(function (err) {
|
||||
test.isFalse(err);
|
||||
})
|
||||
});
|
||||
},
|
||||
function (test, expect) {
|
||||
Meteor.call("testNoSRPUpgrade", this.username, expect(function (error) {
|
||||
test.isFalse(error);
|
||||
}));
|
||||
},
|
||||
// Changing our password should upgrade us to bcrypt.
|
||||
function (test, expect) {
|
||||
Accounts.changePassword("abcdef", "abcdefg", expect(function (error) {
|
||||
test.isFalse(error);
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
Meteor.call("testSRPUpgrade", this.username, expect(function (error) {
|
||||
test.isFalse(error);
|
||||
}));
|
||||
},
|
||||
// And after the upgrade we should be able to change our password again.
|
||||
function (test, expect) {
|
||||
Accounts.changePassword("abcdefg", "abcdef", expect(function (error) {
|
||||
test.isFalse(error);
|
||||
}));
|
||||
},
|
||||
logoutStep
|
||||
]);
|
||||
}) ();
|
||||
|
||||
|
||||
@@ -778,16 +858,15 @@ if (Meteor.isServer) (function () {
|
||||
// set a new password.
|
||||
Accounts.setPassword(userId, 'new password');
|
||||
user = Meteor.users.findOne(userId);
|
||||
var oldVerifier = user.services.password.srp;
|
||||
test.isTrue(user.services.password.srp);
|
||||
var oldSaltedHash = user.services.password.bcrypt;
|
||||
test.isTrue(oldSaltedHash);
|
||||
|
||||
// reset with the same password, see we get a different verifier
|
||||
// reset with the same password, see we get a different salted hash
|
||||
Accounts.setPassword(userId, 'new password');
|
||||
user = Meteor.users.findOne(userId);
|
||||
var newVerifier = user.services.password.srp;
|
||||
test.notEqual(oldVerifier.salt, newVerifier.salt);
|
||||
test.notEqual(oldVerifier.identity, newVerifier.identity);
|
||||
test.notEqual(oldVerifier.verifier, newVerifier.verifier);
|
||||
var newSaltedHash = user.services.password.bcrypt;
|
||||
test.isTrue(newSaltedHash);
|
||||
test.notEqual(oldSaltedHash, newSaltedHash);
|
||||
|
||||
// cleanup
|
||||
Meteor.users.remove(userId);
|
||||
|
||||
@@ -115,3 +115,39 @@ Meteor.methods({
|
||||
Meteor.users.remove({ "username": username });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Create a user that had previously logged in with SRP.
|
||||
|
||||
Meteor.methods({
|
||||
testCreateSRPUser: function () {
|
||||
var username = Random.id();
|
||||
Meteor.users.remove({username: username});
|
||||
var userId = Accounts.createUser({username: username});
|
||||
Meteor.users.update(
|
||||
userId,
|
||||
{ '$set': { 'services.password.srp': {
|
||||
"identity" : "iPNrshUEcpOSO5fRDu7o4RRDc9OJBCGGljYpcXCuyg9",
|
||||
"salt" : "Dk3lFggdEtcHU3aKm6Odx7sdcaIrMskQxBbqtBtFzt6",
|
||||
"verifier" : "2e8bce266b1357edf6952cc56d979db19f699ced97edfb2854b95972f820b0c7006c1a18e98aad40edf3fe111b87c52ef7dd06b320ce452d01376df2d560fdc4d8e74f7a97bca1f67b3cfaef34dee34dd6c76571c247d762624dc166dab5499da06bc9358528efa75bf74e2e7f5a80d09e60acf8856069ae5cfb080f2239ee76"
|
||||
} } }
|
||||
);
|
||||
return username;
|
||||
},
|
||||
|
||||
testSRPUpgrade: function (username) {
|
||||
var user = Meteor.users.findOne({username: username});
|
||||
if (user.services && user.services.password && user.services.password.srp)
|
||||
throw new Error("srp wasn't removed");
|
||||
if (!(user.services && user.services.password && user.services.password.bcrypt))
|
||||
throw new Error("bcrypt wasn't added");
|
||||
},
|
||||
|
||||
testNoSRPUpgrade: function (username) {
|
||||
var user = Meteor.users.findOne({username: username});
|
||||
if (user.services && user.services.password && user.services.password.bcrypt)
|
||||
throw new Error("bcrypt was added");
|
||||
if (user.services && user.services.password && ! user.services.password.srp)
|
||||
throw new Error("srp was removed");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<body>
|
||||
{{> _resetPasswordDialog}}
|
||||
{{> _justResetPasswordDialog}}
|
||||
{{> _enrollAccountDialog}}
|
||||
{{> _justVerifiedEmailDialog}}
|
||||
{{> _configureLoginServiceDialog}}
|
||||
@@ -32,6 +33,16 @@
|
||||
{{/if}}
|
||||
</template>
|
||||
|
||||
<template name="_justResetPasswordDialog">
|
||||
{{#if visible}}
|
||||
<div class="accounts-dialog accounts-centered-dialog">
|
||||
Password reset.
|
||||
You are now logged in as {{displayName}}.
|
||||
<div class="login-button" id="just-verified-dismiss-button">Dismiss</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
|
||||
<template name="_enrollAccountDialog">
|
||||
{{#if inEnrollAccountFlow}}
|
||||
<div class="hide-background"></div>
|
||||
@@ -59,7 +70,8 @@
|
||||
<template name="_justVerifiedEmailDialog">
|
||||
{{#if visible}}
|
||||
<div class="accounts-dialog accounts-centered-dialog">
|
||||
Email verified
|
||||
Email verified.
|
||||
You are now logged in as {{displayName}}.
|
||||
<div class="login-button" id="just-verified-dismiss-button">Dismiss</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
@@ -114,5 +126,3 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ var loginButtonsSession = Accounts._loginButtonsSession;
|
||||
|
||||
//
|
||||
// populate the session so that the appropriate dialogs are
|
||||
// displayed by reading variables set by accounts-urls, which parses
|
||||
// special URLs. since accounts-ui depends on accounts-urls, we are
|
||||
// displayed by reading variables set by accounts-base, which parses
|
||||
// special URLs. since accounts-ui depends on accounts-base, we are
|
||||
// guaranteed to have these set at this point.
|
||||
//
|
||||
|
||||
@@ -63,6 +63,7 @@ var resetPassword = function () {
|
||||
loginButtonsSession.errorMessage(error.reason || "Unknown error");
|
||||
} else {
|
||||
loginButtonsSession.set('resetPasswordToken', null);
|
||||
loginButtonsSession.set('justResetPassword', true);
|
||||
Accounts._enableAutoLogin();
|
||||
}
|
||||
});
|
||||
@@ -72,6 +73,23 @@ Template._resetPasswordDialog.inResetPasswordFlow = function () {
|
||||
return loginButtonsSession.get('resetPasswordToken');
|
||||
};
|
||||
|
||||
//
|
||||
// justResetPasswordDialog template
|
||||
//
|
||||
|
||||
Template._justResetPasswordDialog.events({
|
||||
'click #just-verified-dismiss-button': function () {
|
||||
loginButtonsSession.set('justResetPassword', false);
|
||||
}
|
||||
});
|
||||
|
||||
Template._justResetPasswordDialog.visible = function () {
|
||||
return loginButtonsSession.get('justResetPassword');
|
||||
};
|
||||
|
||||
Template._justResetPasswordDialog.displayName = displayName;
|
||||
|
||||
|
||||
|
||||
//
|
||||
// enrollAccountDialog template
|
||||
@@ -128,6 +146,8 @@ Template._justVerifiedEmailDialog.visible = function () {
|
||||
return loginButtonsSession.get('justVerifiedEmail');
|
||||
};
|
||||
|
||||
Template._justVerifiedEmailDialog.displayName = displayName;
|
||||
|
||||
|
||||
//
|
||||
// loginButtonsMessagesDialog template
|
||||
|
||||
@@ -14,6 +14,7 @@ var VALID_KEYS = [
|
||||
'resetPasswordToken',
|
||||
'enrollAccountToken',
|
||||
'justVerifiedEmail',
|
||||
'justResetPassword',
|
||||
|
||||
'configureLoginServiceDialogVisible',
|
||||
'configureLoginServiceDialogServiceName',
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
//////////////////// LOGIN BUTTONS
|
||||
|
||||
@login-buttons-accounts-dialog-width: 198px;
|
||||
@login-buttons-accounts-dialog-width: 250px;
|
||||
@login-buttons-color: #596595;
|
||||
@login-buttons-color-border: darken(@login-buttons-color, 10%);
|
||||
@login-buttons-color-active: lighten(@login-buttons-color, 10%);
|
||||
@@ -381,7 +381,7 @@
|
||||
}
|
||||
|
||||
#just-verified-dismiss-button, #messages-dialog-dismiss-button {
|
||||
margin-top: 4px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.hide-background {
|
||||
|
||||
@@ -3,5 +3,6 @@ Package.describe({
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use('jquery', 'client');
|
||||
api.add_files('amplify.js', 'client');
|
||||
});
|
||||
|
||||
@@ -19,9 +19,29 @@ WebApp.connectHandlers.use(function (req, res, next) {
|
||||
BrowserPolicy.framing._constructXFrameOptions();
|
||||
var csp = BrowserPolicy.content &&
|
||||
BrowserPolicy.content._constructCsp();
|
||||
if (xFrameOptions)
|
||||
if (xFrameOptions) {
|
||||
res.setHeader("X-Frame-Options", xFrameOptions);
|
||||
if (csp)
|
||||
}
|
||||
if (csp) {
|
||||
res.setHeader("Content-Security-Policy", csp);
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// We use `rawConnectHandlers` to set X-Content-Type-Options on all
|
||||
// requests, including static files.
|
||||
// XXX We should probably use `rawConnectHandlers` for X-Frame-Options
|
||||
// and Content-Security-Policy too, but let's make sure that doesn't
|
||||
// break anything first (e.g. the OAuth popup flow won't work well with
|
||||
// a CSP that disallows inline scripts).
|
||||
WebApp.rawConnectHandlers.use(function (req, res, next) {
|
||||
if (BrowserPolicy._runningTest())
|
||||
return next();
|
||||
|
||||
var contentTypeOptions = BrowserPolicy.content &&
|
||||
BrowserPolicy.content._xContentTypeOptions();
|
||||
if (contentTypeOptions) {
|
||||
res.setHeader("X-Content-Type-Options", contentTypeOptions);
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// 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).
|
||||
// go to any origin). Browsers will also be told not to sniff content types
|
||||
// away from declared content types (X-Content-Type-Options: nosniff).
|
||||
//
|
||||
// Apps should call BrowserPolicy.content.disallowInlineScripts() if they are
|
||||
// not using any inline script tags and are willing to accept an extra round
|
||||
@@ -32,6 +33,8 @@
|
||||
// allowAllContentSameOrigin()
|
||||
// disallowAllContent()
|
||||
//
|
||||
// You can allow content type sniffing by calling
|
||||
// `BrowserPolicy.content.allowContentTypeSniffing()`.
|
||||
|
||||
var cspSrcs;
|
||||
var cachedCsp; // Avoid constructing the header out of cspSrcs when possible.
|
||||
@@ -44,6 +47,9 @@ var keywords = {
|
||||
none: "'none'"
|
||||
};
|
||||
|
||||
// If false, we set the X-Content-Type-Options header to 'nosniff'.
|
||||
var contentSniffingAllowed = false;
|
||||
|
||||
BrowserPolicy.content = {};
|
||||
|
||||
var parseCsp = function (csp) {
|
||||
@@ -126,6 +132,7 @@ var setDefaultPolicy = function () {
|
||||
"connect-src *; " +
|
||||
"img-src data: 'self'; " +
|
||||
"style-src 'self' 'unsafe-inline';");
|
||||
contentSniffingAllowed = false;
|
||||
};
|
||||
|
||||
var setWebAppInlineScripts = function (value) {
|
||||
@@ -134,6 +141,9 @@ var setWebAppInlineScripts = function (value) {
|
||||
};
|
||||
|
||||
_.extend(BrowserPolicy.content, {
|
||||
allowContentTypeSniffing: function () {
|
||||
contentSniffingAllowed = true;
|
||||
},
|
||||
// Exported for tests and browser-policy-common.
|
||||
_constructCsp: function () {
|
||||
if (! cspSrcs || _.isEmpty(cspSrcs))
|
||||
@@ -220,6 +230,12 @@ _.extend(BrowserPolicy.content, {
|
||||
"default-src": []
|
||||
};
|
||||
setWebAppInlineScripts(false);
|
||||
},
|
||||
|
||||
_xContentTypeOptions: function () {
|
||||
if (! contentSniffingAllowed) {
|
||||
return "nosniff";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -151,3 +151,10 @@ Tinytest.add("browser-policy - x-frame-options", function (test) {
|
||||
BrowserPolicy.framing.restrictToOrigin("bar.com");
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add("browser-policy - X-Content-Type-Options", function (test) {
|
||||
BrowserPolicy.content._reset();
|
||||
test.equal(BrowserPolicy.content._xContentTypeOptions(), "nosniff");
|
||||
BrowserPolicy.content.allowContentTypeSniffing();
|
||||
test.equal(BrowserPolicy.content._xContentTypeOptions(), undefined);
|
||||
});
|
||||
|
||||
@@ -7,7 +7,14 @@ var currentArgumentChecker = new Meteor.EnvironmentVariable;
|
||||
|
||||
check = function (value, pattern) {
|
||||
// Record that check got called, if somebody cared.
|
||||
var argChecker = currentArgumentChecker.get();
|
||||
//
|
||||
// We use getOrNullIfOutsideFiber so that it's OK to call check()
|
||||
// from non-Fiber server contexts; the downside is that if you forget to
|
||||
// bindEnvironment on some random callback in your method/publisher,
|
||||
// it might not find the argumentChecker and you'll get an error about
|
||||
// not checking an argument that it looks like you're checking (instead
|
||||
// of just getting a "Node code must run in a Fiber" error).
|
||||
var argChecker = currentArgumentChecker.getOrNullIfOutsideFiber();
|
||||
if (argChecker)
|
||||
argChecker.checking(value);
|
||||
try {
|
||||
|
||||
@@ -257,3 +257,30 @@ Tinytest.add("check - Match error path", function (test) {
|
||||
match({ "return": 0 }, { "return": String }, "[\"return\"]");
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/meteor/meteor/issues/2136
|
||||
Meteor.isServer && Tinytest.addAsync("check - non-fiber check works", function (test, onComplete) {
|
||||
var Fiber = Npm.require('fibers');
|
||||
|
||||
// We can only call test.isTrue inside normal Meteor Fibery code, so give us a
|
||||
// bindEnvironment way to get back.
|
||||
var report = Meteor.bindEnvironment(function (success) {
|
||||
test.isTrue(success);
|
||||
onComplete();
|
||||
});
|
||||
|
||||
// Get out of a fiber with process.nextTick and ensure that we can still use
|
||||
// check.
|
||||
process.nextTick(function () {
|
||||
var success = true;
|
||||
if (Fiber.current)
|
||||
success = false;
|
||||
if (success) {
|
||||
try {
|
||||
check(true, Boolean);
|
||||
} catch (e) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
report(success);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,17 +44,21 @@ var _throwOrLog = function (from, e) {
|
||||
}
|
||||
};
|
||||
|
||||
// Like `Meteor._noYieldsAllowed(function () { f(comp); })` but shorter,
|
||||
// and doesn't clutter the stack with an extra frame on the client,
|
||||
// where `_noYieldsAllowed` is a no-op. `f` may be a computation
|
||||
// function or an onInvalidate callback.
|
||||
var callWithNoYieldsAllowed = function (f, comp) {
|
||||
// Takes a function `f`, and wraps it in a `Meteor._noYieldsAllowed`
|
||||
// block if we are running on the server. On the client, returns the
|
||||
// original function (since `Meteor._noYieldsAllowed` is a
|
||||
// no-op). This has the benefit of not adding an unnecessary stack
|
||||
// frame on the client.
|
||||
var withNoYieldsAllowed = function (f) {
|
||||
if ((typeof Meteor === 'undefined') || Meteor.isClient) {
|
||||
f(comp);
|
||||
return f;
|
||||
} else {
|
||||
Meteor._noYieldsAllowed(function () {
|
||||
f(comp);
|
||||
});
|
||||
return function () {
|
||||
var args = arguments;
|
||||
Meteor._noYieldsAllowed(function () {
|
||||
f.apply(null, args);
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -140,7 +144,7 @@ _assign(Deps.Computation.prototype, {
|
||||
|
||||
if (self.invalidated) {
|
||||
Deps.nonreactive(function () {
|
||||
callWithNoYieldsAllowed(f, self);
|
||||
withNoYieldsAllowed(f)(self);
|
||||
});
|
||||
} else {
|
||||
self._onInvalidateCallbacks.push(f);
|
||||
@@ -164,7 +168,7 @@ _assign(Deps.Computation.prototype, {
|
||||
// self.invalidated === true.
|
||||
for(var i = 0, f; f = self._onInvalidateCallbacks[i]; i++) {
|
||||
Deps.nonreactive(function () {
|
||||
callWithNoYieldsAllowed(f, self);
|
||||
withNoYieldsAllowed(f)(self);
|
||||
});
|
||||
}
|
||||
self._onInvalidateCallbacks = [];
|
||||
@@ -188,7 +192,7 @@ _assign(Deps.Computation.prototype, {
|
||||
var previousInCompute = inCompute;
|
||||
inCompute = true;
|
||||
try {
|
||||
callWithNoYieldsAllowed(self._func, self);
|
||||
withNoYieldsAllowed(self._func)(self);
|
||||
} finally {
|
||||
setCurrentComputation(previous);
|
||||
inCompute = false;
|
||||
|
||||
@@ -6,7 +6,6 @@ Package.describe({
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use('underscore');
|
||||
api.export('Deps');
|
||||
api.add_files('deps.js');
|
||||
api.add_files('deprecated.js');
|
||||
|
||||
@@ -110,14 +110,19 @@ var builtinConverters = [
|
||||
return EJSON._isCustomType(obj);
|
||||
},
|
||||
toJSONValue: function (obj) {
|
||||
return {$type: obj.typeName(), $value: obj.toJSONValue()};
|
||||
var jsonValue = Meteor._noYieldsAllowed(function () {
|
||||
return obj.toJSONValue();
|
||||
});
|
||||
return {$type: obj.typeName(), $value: jsonValue};
|
||||
},
|
||||
fromJSONValue: function (obj) {
|
||||
var typeName = obj.$type;
|
||||
if (!_.has(customTypes, typeName))
|
||||
throw new Error("Custom EJSON type " + typeName + " is not defined");
|
||||
var converter = customTypes[typeName];
|
||||
return converter(obj.$value);
|
||||
return Meteor._noYieldsAllowed(function () {
|
||||
return converter(obj.$value);
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -71,9 +71,6 @@ EmailTest.restoreOutputStream = function () {
|
||||
var devModeSend = function (mc) {
|
||||
var devmode_mail_id = next_devmode_mail_id++;
|
||||
|
||||
// Make sure we use whatever stream was set at the time of the Email.send
|
||||
// call even in the 'end' callback, in case there are multiple concurrent
|
||||
// test runs.
|
||||
var stream = output_stream;
|
||||
|
||||
// This approach does not prevent other writers to stdout from interleaving.
|
||||
|
||||
@@ -15,8 +15,6 @@ Tinytest.add("email - dev mode smoke test", function (test) {
|
||||
text: "This is the body\nof the message\nFrom us.",
|
||||
headers: {'X-Meteor-Test': 'a custom header'}
|
||||
});
|
||||
// Note that we use the local "stream" here rather than Email._output_stream
|
||||
// in case a concurrent test run mutates Email._output_stream too.
|
||||
// XXX brittle if mailcomposer changes header order, etc
|
||||
test.equal(stream.getContentsAsString("utf8"),
|
||||
"====== BEGIN MAIL #0 ======\n" +
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
var url = Npm.require("url");
|
||||
|
||||
// Unfortunately we can't use a connect middleware here since
|
||||
// sockjs installs itself prior to all existing listeners
|
||||
// (meaning prior to any connect middlewares) so we need to take
|
||||
@@ -39,10 +41,7 @@ httpServer.addListener('request', function (req, res) {
|
||||
if (!isLocal && !isSsl) {
|
||||
// connection is not cool. send a 302 redirect!
|
||||
|
||||
// if we don't have a host header, there's not a lot we can do. We
|
||||
// don't know how to redirect them.
|
||||
// XXX can we do better here?
|
||||
var host = req.headers.host || 'no-host-header';
|
||||
var host = url.parse(Meteor.absoluteUrl()).hostname;
|
||||
|
||||
// strip off the port number. If we went to a URL with a custom
|
||||
// port, we don't know what the custom SSL port is anyway.
|
||||
|
||||
@@ -6,6 +6,7 @@ Package.describe({
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use('htmljs');
|
||||
api.imply('htmljs');
|
||||
|
||||
api.export('HTMLTools');
|
||||
|
||||
|
||||
@@ -208,24 +208,24 @@ var getChars = makeRegexMatcher(/^[^&<\u0000][^&<\u0000{]*/);
|
||||
getHTMLToken = HTMLTools.Parse.getHTMLToken = function (scanner, dataMode) {
|
||||
var result = null;
|
||||
if (scanner.getSpecialTag) {
|
||||
var lastPos = -1;
|
||||
// Try to parse a "special tag" by calling out to the provided
|
||||
// `getSpecialTag` function. If the function returns `null` but
|
||||
// consumes characters, it must have parsed a comment or something,
|
||||
// so we loop and try it again. If it ever returns `null` without
|
||||
// consumes characters, it must have parsed a comment, so we return null
|
||||
// and allow the lexer to continue. If it ever returns `null` without
|
||||
// consuming anything, that means it didn't see anything interesting
|
||||
// so we look for a normal token. If it returns a truthy value,
|
||||
// the value must be an object. We wrap it in a Special token.
|
||||
while ((! result) && scanner.pos > lastPos) {
|
||||
lastPos = scanner.pos;
|
||||
result = scanner.getSpecialTag(
|
||||
scanner,
|
||||
(dataMode === 'rcdata' ? TEMPLATE_TAG_POSITION.IN_RCDATA :
|
||||
(dataMode === 'rawtext' ? TEMPLATE_TAG_POSITION.IN_RAWTEXT :
|
||||
TEMPLATE_TAG_POSITION.ELEMENT)));
|
||||
}
|
||||
var lastPos = scanner.pos;
|
||||
result = scanner.getSpecialTag(
|
||||
scanner,
|
||||
(dataMode === 'rcdata' ? TEMPLATE_TAG_POSITION.IN_RCDATA :
|
||||
(dataMode === 'rawtext' ? TEMPLATE_TAG_POSITION.IN_RAWTEXT :
|
||||
TEMPLATE_TAG_POSITION.ELEMENT)));
|
||||
|
||||
if (result)
|
||||
return { t: 'Special', v: result };
|
||||
else if (scanner.pos > lastPos)
|
||||
return null;
|
||||
}
|
||||
|
||||
var chars = getChars(scanner);
|
||||
|
||||
@@ -89,12 +89,16 @@ testAsyncMulti("httpcall - errors", [
|
||||
test.isFalse(result);
|
||||
test.isFalse(error.response);
|
||||
};
|
||||
HTTP.call("GET", "http://asfd.asfd/", expect(unknownServerCallback));
|
||||
|
||||
// 0.0.0.0 is an illegal IP address, and thus should always give an error.
|
||||
// If your ISP is intercepting DNS misses and serving ads, an obviously
|
||||
// invalid URL (http://asdf.asdf) might produce an HTTP response.
|
||||
HTTP.call("GET", "http://0.0.0.0/", expect(unknownServerCallback));
|
||||
|
||||
if (Meteor.isServer) {
|
||||
// test sync version
|
||||
try {
|
||||
var unknownServerResult = HTTP.call("GET", "http://asfd.asfd/");
|
||||
var unknownServerResult = HTTP.call("GET", "http://0.0.0.0/");
|
||||
unknownServerCallback(undefined, unknownServerResult);
|
||||
} catch (e) {
|
||||
unknownServerCallback(e, e.response);
|
||||
|
||||
@@ -44,7 +44,7 @@ Plugin.registerSourceHandler("less", function (compileStep) {
|
||||
compileStep.error({
|
||||
message: "Less compiler error: " + e.message,
|
||||
sourcePath: e.filename || compileStep.inputPath,
|
||||
line: e.line - 1, // dunno why, but it matches
|
||||
line: e.line,
|
||||
column: e.column + 1
|
||||
});
|
||||
return;
|
||||
|
||||
12
packages/livedata/.npm/package/npm-shrinkwrap.json
generated
12
packages/livedata/.npm/package/npm-shrinkwrap.json
generated
@@ -4,23 +4,15 @@
|
||||
"version": "0.7.2",
|
||||
"dependencies": {
|
||||
"websocket-driver": {
|
||||
"version": "0.3.2"
|
||||
"version": "0.3.4"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sockjs": {
|
||||
"version": "0.3.8",
|
||||
"version": "0.3.9",
|
||||
"dependencies": {
|
||||
"node-uuid": {
|
||||
"version": "1.3.3"
|
||||
},
|
||||
"faye-websocket": {
|
||||
"version": "0.7.0",
|
||||
"dependencies": {
|
||||
"websocket-driver": {
|
||||
"version": "0.3.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ var Connection = function (url, options) {
|
||||
|
||||
// Reactive userId.
|
||||
self._userId = null;
|
||||
self._userIdDeps = (typeof Deps !== "undefined") && new Deps.Dependency;
|
||||
self._userIdDeps = new Deps.Dependency;
|
||||
|
||||
// Block auto-reload while we're waiting for method responses.
|
||||
if (Meteor.isClient && Package.reload && !options.reloadWithOutstanding) {
|
||||
@@ -522,9 +522,18 @@ _.extend(Connection.prototype, {
|
||||
params: EJSON.clone(params),
|
||||
inactive: false,
|
||||
ready: false,
|
||||
readyDeps: (typeof Deps !== "undefined") && new Deps.Dependency,
|
||||
readyDeps: new Deps.Dependency,
|
||||
readyCallback: callbacks.onReady,
|
||||
errorCallback: callbacks.onError
|
||||
errorCallback: callbacks.onError,
|
||||
connection: self,
|
||||
remove: function() {
|
||||
delete this.connection._subscriptions[this.id];
|
||||
this.ready && this.readyDeps.changed();
|
||||
},
|
||||
stop: function() {
|
||||
this.connection._send({msg: 'unsub', id: id});
|
||||
this.remove();
|
||||
}
|
||||
};
|
||||
self._send({msg: 'sub', id: id, name: name, params: params});
|
||||
}
|
||||
@@ -534,15 +543,15 @@ _.extend(Connection.prototype, {
|
||||
stop: function () {
|
||||
if (!_.has(self._subscriptions, id))
|
||||
return;
|
||||
self._send({msg: 'unsub', id: id});
|
||||
delete self._subscriptions[id];
|
||||
|
||||
self._subscriptions[id].stop();
|
||||
},
|
||||
ready: function () {
|
||||
// return false if we've unsubscribed.
|
||||
if (!_.has(self._subscriptions, id))
|
||||
return false;
|
||||
var record = self._subscriptions[id];
|
||||
record.readyDeps && record.readyDeps.depend();
|
||||
record.readyDeps.depend();
|
||||
return record.ready;
|
||||
}
|
||||
};
|
||||
@@ -891,8 +900,7 @@ _.extend(Connection.prototype, {
|
||||
// but it doesn't seem worth it yet to have a special API for
|
||||
// subscriptions to preserve after unit tests.
|
||||
if (sub.name !== 'meteor_autoupdate_clientVersions') {
|
||||
self._send({msg: 'unsub', id: id});
|
||||
delete self._subscriptions[id];
|
||||
self._subscriptions[id].stop();
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -1297,7 +1305,7 @@ _.extend(Connection.prototype, {
|
||||
return;
|
||||
subRecord.readyCallback && subRecord.readyCallback();
|
||||
subRecord.ready = true;
|
||||
subRecord.readyDeps && subRecord.readyDeps.changed();
|
||||
subRecord.readyDeps.changed();
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -1353,7 +1361,7 @@ _.extend(Connection.prototype, {
|
||||
if (!_.has(self._subscriptions, msg.id))
|
||||
return;
|
||||
var errorCallback = self._subscriptions[msg.id].errorCallback;
|
||||
delete self._subscriptions[msg.id];
|
||||
self._subscriptions[msg.id].remove();
|
||||
if (errorCallback && msg.error) {
|
||||
errorCallback(new Meteor.Error(
|
||||
msg.error.error, msg.error.reason, msg.error.details));
|
||||
|
||||
@@ -122,13 +122,14 @@ Tinytest.add("livedata stub - subscribe", function (test) {
|
||||
test.isTrue(callback_fired);
|
||||
Deps.flush();
|
||||
test.isTrue(reactivelyReady);
|
||||
autorunHandle.stop();
|
||||
|
||||
// Unsubscribe.
|
||||
sub.stop();
|
||||
test.length(stream.sent, 1);
|
||||
message = JSON.parse(stream.sent.shift());
|
||||
test.equal(message, {msg: 'unsub', id: id});
|
||||
Deps.flush();
|
||||
test.isFalse(reactivelyReady);
|
||||
|
||||
// Resubscribe.
|
||||
conn.subscribe('my_data');
|
||||
@@ -161,13 +162,18 @@ Tinytest.add("livedata stub - reactive subscribe", function (test) {
|
||||
};
|
||||
|
||||
// Subscribe to some subs.
|
||||
var stopperHandle;
|
||||
var stopperHandle, completerHandle;
|
||||
var autorunHandle = Deps.autorun(function () {
|
||||
conn.subscribe("foo", rFoo.get(), onReady(rFoo.get()));
|
||||
conn.subscribe("bar", rBar.get(), onReady(rBar.get()));
|
||||
conn.subscribe("completer", onReady("completer"));
|
||||
completerHandle = conn.subscribe("completer", onReady("completer"));
|
||||
stopperHandle = conn.subscribe("stopper", onReady("stopper"));
|
||||
});
|
||||
|
||||
var completerReady;
|
||||
var readyAutorunHandle = Deps.autorun(function() {
|
||||
completerReady = completerHandle.ready();
|
||||
});
|
||||
|
||||
// Check sub messages. (Assume they are sent in the order executed.)
|
||||
test.length(stream.sent, 4);
|
||||
@@ -193,11 +199,15 @@ Tinytest.add("livedata stub - reactive subscribe", function (test) {
|
||||
|
||||
// Haven't hit onReady yet.
|
||||
test.equal(onReadyCount, {});
|
||||
Deps.flush();
|
||||
test.isFalse(completerReady);
|
||||
|
||||
// "completer" gets ready now. its callback should fire.
|
||||
stream.receive({msg: 'ready', 'subs': [idCompleter]});
|
||||
test.equal(onReadyCount, {completer: 1});
|
||||
test.length(stream.sent, 0);
|
||||
Deps.flush();
|
||||
test.isTrue(completerReady);
|
||||
|
||||
// Stop 'stopper'.
|
||||
stopperHandle.stop();
|
||||
@@ -206,12 +216,15 @@ Tinytest.add("livedata stub - reactive subscribe", function (test) {
|
||||
test.equal(message, {msg: 'unsub', id: idStopper});
|
||||
|
||||
test.equal(onReadyCount, {completer: 1});
|
||||
Deps.flush();
|
||||
test.isTrue(completerReady);
|
||||
|
||||
// Change the foo subscription and flush. We should sub to the new foo
|
||||
// subscription, re-sub to the stopper subscription, and then unsub from the old
|
||||
// foo subscription. The bar subscription should be unaffected. The completer
|
||||
// subscription should *NOT* call its new onReady callback, because we only
|
||||
// call at most one onReady for a given reactively-saved subscription.
|
||||
// The completerHandle should have been reestablished to the ready handle.
|
||||
rFoo.set("foo2");
|
||||
Deps.flush();
|
||||
test.length(stream.sent, 3);
|
||||
@@ -230,6 +243,7 @@ Tinytest.add("livedata stub - reactive subscribe", function (test) {
|
||||
test.equal(message, {msg: 'unsub', id: idFoo1});
|
||||
|
||||
test.equal(onReadyCount, {completer: 1});
|
||||
test.isTrue(completerReady);
|
||||
|
||||
// Ready the stopper and bar subs. Completing stopper should call only the
|
||||
// onReady from the new subscription because they were separate subscriptions
|
||||
@@ -244,6 +258,8 @@ Tinytest.add("livedata stub - reactive subscribe", function (test) {
|
||||
// time.
|
||||
autorunHandle.stop();
|
||||
Deps.flush();
|
||||
test.isFalse(completerReady);
|
||||
readyAutorunHandle.stop();
|
||||
|
||||
test.length(stream.sent, 4);
|
||||
// The order of unsubs here is not important.
|
||||
@@ -257,6 +273,86 @@ Tinytest.add("livedata stub - reactive subscribe", function (test) {
|
||||
test.equal(actualIds, expectedIds);
|
||||
});
|
||||
|
||||
Tinytest.add("livedata stub - reactive subscribe handle correct", function (test) {
|
||||
var stream = new StubStream();
|
||||
var conn = newConnection(stream);
|
||||
|
||||
startAndConnect(test, stream);
|
||||
|
||||
var rFoo = new ReactiveVar('foo1');
|
||||
|
||||
// Subscribe to some subs.
|
||||
var fooHandle, fooReady;
|
||||
var autorunHandle = Deps.autorun(function () {
|
||||
fooHandle = conn.subscribe("foo", rFoo.get());
|
||||
Deps.autorun(function() {
|
||||
fooReady = fooHandle.ready();
|
||||
});
|
||||
});
|
||||
|
||||
var message = JSON.parse(stream.sent.shift());
|
||||
var idFoo1 = message.id;
|
||||
delete message.id;
|
||||
test.equal(message, {msg: 'sub', name: 'foo', params: ['foo1']});
|
||||
|
||||
// Not ready yet
|
||||
Deps.flush();
|
||||
test.isFalse(fooHandle.ready());
|
||||
test.isFalse(fooReady);
|
||||
|
||||
// change the argument to foo. This will make a new handle, which isn't ready
|
||||
// the ready autorun should invalidate, reading the new false value, and
|
||||
// setting up a new dep which goes true soon
|
||||
rFoo.set("foo2");
|
||||
Deps.flush();
|
||||
test.length(stream.sent, 2);
|
||||
|
||||
message = JSON.parse(stream.sent.shift());
|
||||
var idFoo2 = message.id;
|
||||
delete message.id;
|
||||
test.equal(message, {msg: 'sub', name: 'foo', params: ['foo2']});
|
||||
|
||||
message = JSON.parse(stream.sent.shift());
|
||||
test.equal(message, {msg: 'unsub', id: idFoo1});
|
||||
|
||||
Deps.flush();
|
||||
test.isFalse(fooHandle.ready());
|
||||
test.isFalse(fooReady);
|
||||
|
||||
// "foo" gets ready now. The handle should be ready and the autorun rerun
|
||||
stream.receive({msg: 'ready', 'subs': [idFoo2]});
|
||||
test.length(stream.sent, 0);
|
||||
Deps.flush();
|
||||
test.isTrue(fooHandle.ready());
|
||||
test.isTrue(fooReady);
|
||||
|
||||
// change the argument to foo. This will make a new handle, which isn't ready
|
||||
// the ready autorun should invalidate, making fooReady false too
|
||||
rFoo.set("foo3");
|
||||
Deps.flush();
|
||||
test.length(stream.sent, 2);
|
||||
|
||||
message = JSON.parse(stream.sent.shift());
|
||||
var idFoo3 = message.id;
|
||||
delete message.id;
|
||||
test.equal(message, {msg: 'sub', name: 'foo', params: ['foo3']});
|
||||
|
||||
message = JSON.parse(stream.sent.shift());
|
||||
test.equal(message, {msg: 'unsub', id: idFoo2});
|
||||
|
||||
Deps.flush();
|
||||
test.isFalse(fooHandle.ready());
|
||||
test.isFalse(fooReady);
|
||||
|
||||
// "foo" gets ready again
|
||||
stream.receive({msg: 'ready', 'subs': [idFoo3]});
|
||||
test.length(stream.sent, 0);
|
||||
Deps.flush();
|
||||
test.isTrue(fooHandle.ready());
|
||||
test.isTrue(fooReady);
|
||||
|
||||
autorunHandle.stop();
|
||||
});
|
||||
|
||||
Tinytest.add("livedata stub - this", function (test) {
|
||||
var stream = new StubStream();
|
||||
|
||||
@@ -403,13 +403,16 @@ _.extend(Session.prototype, {
|
||||
});
|
||||
},
|
||||
|
||||
// Destroy this session. Stop all processing and tear everything
|
||||
// down. If a socket was attached, close it.
|
||||
destroy: function () {
|
||||
// Destroy this session and unregister it at the server.
|
||||
close: function () {
|
||||
var self = this;
|
||||
|
||||
// Destroy this session, even if it's not registered at the
|
||||
// server. Stop all processing and tear everything down. If a socket
|
||||
// was attached, close it.
|
||||
|
||||
// Already destroyed.
|
||||
if (!self.inQueue)
|
||||
if (! self.inQueue)
|
||||
return;
|
||||
|
||||
if (self.heartbeat) {
|
||||
@@ -430,7 +433,7 @@ _.extend(Session.prototype, {
|
||||
"livedata", "sessions", -1);
|
||||
|
||||
Meteor.defer(function () {
|
||||
// stop callbacks can yield, so we defer this on destroy.
|
||||
// stop callbacks can yield, so we defer this on close.
|
||||
// sub._isDeactivated() detects that we set inQueue to null and
|
||||
// treats it as semi-deactivated (it will ignore incoming callbacks, etc).
|
||||
self._deactivateAllSubscriptions();
|
||||
@@ -441,19 +444,9 @@ _.extend(Session.prototype, {
|
||||
callback();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// Destroy this session and unregister it at the server.
|
||||
close: function () {
|
||||
var self = this;
|
||||
|
||||
// Unconditionally destroy this session, even if it's not
|
||||
// registered at the server.
|
||||
self.destroy();
|
||||
|
||||
// Unregister the session. This will also call `destroy`, but
|
||||
// that's OK because `destroy` is idempotent.
|
||||
self.server._closeSession(self);
|
||||
// Unregister the session.
|
||||
self.server._removeSession(self);
|
||||
},
|
||||
|
||||
// Send a message (doing nothing if no socket is connected right now.)
|
||||
@@ -920,6 +913,9 @@ _.extend(Subscription.prototype, {
|
||||
try {
|
||||
var res = maybeAuditArgumentChecks(
|
||||
self._handler, self, EJSON.clone(self._params),
|
||||
// It's OK that this would look weird for universal subscriptions,
|
||||
// because they have no arguments so there can never be an
|
||||
// audit-argument-checks failure.
|
||||
"publisher '" + self._name + "'");
|
||||
} catch (e) {
|
||||
self.error(e);
|
||||
@@ -1035,7 +1031,8 @@ _.extend(Subscription.prototype, {
|
||||
_recreate: function () {
|
||||
var self = this;
|
||||
return new Subscription(
|
||||
self._session, self._handler, self._subscriptionId, self._params);
|
||||
self._session, self._handler, self._subscriptionId, self._params,
|
||||
self._name);
|
||||
},
|
||||
|
||||
error: function (error) {
|
||||
@@ -1217,37 +1214,40 @@ _.extend(Server.prototype, {
|
||||
|
||||
_handleConnect: function (socket, msg) {
|
||||
var self = this;
|
||||
|
||||
// The connect message must specify a version and an array of supported
|
||||
// versions, and it must claim to support what it is proposing.
|
||||
if (!(typeof (msg.version) === 'string' &&
|
||||
_.isArray(msg.support) &&
|
||||
_.all(msg.support, _.isString) &&
|
||||
_.contains(msg.support, msg.version))) {
|
||||
socket.send(stringifyDDP({msg: 'failed',
|
||||
version: SUPPORTED_DDP_VERSIONS[0]}));
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// In the future, handle session resumption: something like:
|
||||
// socket._meteorSession = self.sessions[msg.session]
|
||||
var version = calculateVersion(msg.support, SUPPORTED_DDP_VERSIONS);
|
||||
|
||||
if (msg.version === version) {
|
||||
// Creating a new session
|
||||
socket._meteorSession = new Session(self, version, socket, self.options);
|
||||
self.sessions[socket._meteorSession.id] = socket._meteorSession;
|
||||
self.onConnectionHook.each(function (callback) {
|
||||
if (socket._meteorSession)
|
||||
callback(socket._meteorSession.connectionHandle);
|
||||
return true;
|
||||
});
|
||||
} 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
|
||||
// connection, they'll retry right away. Instead, just pause for a
|
||||
// bit (randomly distributed so as to avoid synchronized swarms)
|
||||
// and hold the connection open.
|
||||
var timeout = 1000 * (30 + Random.fraction() * 60);
|
||||
// drop all future data coming over this connection on the
|
||||
// floor. We don't want to confuse things.
|
||||
socket.removeAllListeners('data');
|
||||
Meteor.setTimeout(function () {
|
||||
socket.send(stringifyDDP({msg: 'failed', version: version}));
|
||||
socket.close();
|
||||
}, timeout);
|
||||
} else {
|
||||
if (msg.version !== version) {
|
||||
// The best version to use (according to the client's stated preferences)
|
||||
// is not the one the client is trying to use. Inform them about the best
|
||||
// version to use.
|
||||
socket.send(stringifyDDP({msg: 'failed', version: version}));
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Yay, version matches! Create a new session.
|
||||
socket._meteorSession = new Session(self, version, socket, self.options);
|
||||
self.sessions[socket._meteorSession.id] = socket._meteorSession;
|
||||
self.onConnectionHook.each(function (callback) {
|
||||
if (socket._meteorSession)
|
||||
callback(socket._meteorSession.connectionHandle);
|
||||
return true;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Register a publish handler function.
|
||||
@@ -1323,11 +1323,10 @@ _.extend(Server.prototype, {
|
||||
}
|
||||
},
|
||||
|
||||
_closeSession: function (session) {
|
||||
_removeSession: function (session) {
|
||||
var self = this;
|
||||
if (self.sessions[session.id]) {
|
||||
delete self.sessions[session.id];
|
||||
session.destroy();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -7,7 +7,11 @@ Package.describe({
|
||||
// because it's the same library used as a server in sockjs, and it's easiest to
|
||||
// deal with a single websocket implementation. (Plus, its maintainer is easy
|
||||
// to work with on pull requests.)
|
||||
Npm.depends({sockjs: "0.3.8", "faye-websocket": "0.7.2"});
|
||||
//
|
||||
// (By listing faye-websocket first, it's more likely that npm deduplication
|
||||
// will prevent a second copy of faye-websocket from being installed inside
|
||||
// sockjs.)
|
||||
Npm.depends({"faye-websocket": "0.7.2", sockjs: "0.3.9"});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use(['check', 'random', 'ejson', 'json', 'underscore', 'deps',
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
// Meteor._localStorage is not an ideal name, but we can change it later.
|
||||
|
||||
if (window.localStorage) {
|
||||
// Let's test to make sure that localStorage actually works. For example, in
|
||||
// Safari with private browsing on, window.localStorage exists but actually
|
||||
// trying to use it throws.
|
||||
// Let's test to make sure that localStorage actually works. For example, in
|
||||
// Safari with private browsing on, window.localStorage exists but actually
|
||||
// trying to use it throws.
|
||||
// Accessing window.localStorage can also immediately throw an error in IE (#1291).
|
||||
|
||||
var key = '_localstorage_test_' + Random.id();
|
||||
var retrieved;
|
||||
try {
|
||||
var key = '_localstorage_test_' + Random.id();
|
||||
var retrieved;
|
||||
try {
|
||||
if (window.localStorage) {
|
||||
window.localStorage.setItem(key, key);
|
||||
retrieved = window.localStorage.getItem(key);
|
||||
window.localStorage.removeItem(key);
|
||||
} catch (e) {
|
||||
// ... ignore
|
||||
}
|
||||
if (key === retrieved) {
|
||||
Meteor._localStorage = {
|
||||
getItem: function (key) {
|
||||
return window.localStorage.getItem(key);
|
||||
},
|
||||
setItem: function (key, value) {
|
||||
window.localStorage.setItem(key, value);
|
||||
},
|
||||
removeItem: function (key) {
|
||||
window.localStorage.removeItem(key);
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
// ... ignore
|
||||
}
|
||||
if (key === retrieved) {
|
||||
Meteor._localStorage = {
|
||||
getItem: function (key) {
|
||||
return window.localStorage.getItem(key);
|
||||
},
|
||||
setItem: function (key, value) {
|
||||
window.localStorage.setItem(key, value);
|
||||
},
|
||||
removeItem: function (key) {
|
||||
window.localStorage.removeItem(key);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!Meteor._localStorage) {
|
||||
|
||||
@@ -4,7 +4,13 @@ MeteorDeveloperAccounts = {};
|
||||
// @param credentialRequestCompleteCallback {Function} Callback function to call on
|
||||
// completion. Takes one argument, credentialToken on success, or Error on
|
||||
// error.
|
||||
var requestCredential = function (credentialRequestCompleteCallback) {
|
||||
var requestCredential = function (options, credentialRequestCompleteCallback) {
|
||||
// support a callback without options
|
||||
if (! credentialRequestCompleteCallback && typeof options === "function") {
|
||||
credentialRequestCompleteCallback = options;
|
||||
options = null;
|
||||
}
|
||||
|
||||
var config = ServiceConfiguration.configurations.findOne({
|
||||
service: 'meteor-developer'
|
||||
});
|
||||
@@ -20,8 +26,12 @@ var requestCredential = function (credentialRequestCompleteCallback) {
|
||||
METEOR_DEVELOPER_URL + "/oauth2/authorize?" +
|
||||
"state=" + credentialToken +
|
||||
"&response_type=code&" +
|
||||
"client_id=" + config.clientId +
|
||||
"&redirect_uri=" + Meteor.absoluteUrl("_oauth/meteor-developer?close");
|
||||
"client_id=" + config.clientId;
|
||||
|
||||
if (options && options.userEmail)
|
||||
loginUrl += '&user_email=' + encodeURIComponent(options.userEmail);
|
||||
|
||||
loginUrl += "&redirect_uri=" + Meteor.absoluteUrl("_oauth/meteor-developer?close");
|
||||
|
||||
OAuth.showPopup(
|
||||
loginUrl,
|
||||
|
||||
@@ -12,6 +12,10 @@ _.extend(Meteor.EnvironmentVariable.prototype, {
|
||||
return currentValues[this.slot];
|
||||
},
|
||||
|
||||
getOrNullIfOutsideFiber: function () {
|
||||
return this.get();
|
||||
},
|
||||
|
||||
withValue: function (value, func) {
|
||||
var saved = currentValues[this.slot];
|
||||
try {
|
||||
|
||||
@@ -24,6 +24,25 @@ _.extend(Meteor.EnvironmentVariable.prototype, {
|
||||
Fiber.current._meteor_dynamics[this.slot];
|
||||
},
|
||||
|
||||
// Most Meteor code ought to run inside a fiber, and the
|
||||
// _nodeCodeMustBeInFiber assertion helps you remember to include appropriate
|
||||
// bindEnvironment calls (which will get you the *right value* for your
|
||||
// environment variables, on the server).
|
||||
//
|
||||
// In some very special cases, it's more important to run Meteor code on the
|
||||
// server in non-Fiber contexts rather than to strongly enforce the safeguard
|
||||
// against forgetting to use bindEnvironment. For example, using `check` in
|
||||
// some top-level constructs like connect handlers without needing unnecessary
|
||||
// Fibers on every request is more important that possibly failing to find the
|
||||
// correct argumentChecker. So this function is just like get(), but it
|
||||
// returns null rather than throwing when called from outside a Fiber. (On the
|
||||
// client, it is identical to get().)
|
||||
getOrNullIfOutsideFiber: function () {
|
||||
if (!Fiber.current)
|
||||
return null;
|
||||
return this.get();
|
||||
},
|
||||
|
||||
withValue: function (value, func) {
|
||||
Meteor._nodeCodeMustBeInFiber();
|
||||
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
// http://davidshariff.com/blog/javascript-inheritance-patterns/
|
||||
var inherits = function (child, parent) {
|
||||
var tmp = function () {};
|
||||
tmp.prototype = parent.prototype;
|
||||
child.prototype = new tmp;
|
||||
child.prototype.constructor = child;
|
||||
};
|
||||
|
||||
// Makes an error subclass which properly contains a stack trace in most
|
||||
// environments. constructor can set fields on `this` (and should probably set
|
||||
// `message`, which is what gets displayed at the top of a stack trace).
|
||||
@@ -34,7 +26,7 @@ Meteor.makeErrorType = function (name, constructor) {
|
||||
return self;
|
||||
};
|
||||
|
||||
inherits(errorClass, Error);
|
||||
Meteor._inherits(errorClass, Error);
|
||||
|
||||
return errorClass;
|
||||
};
|
||||
|
||||
@@ -115,7 +115,7 @@ _.extend(Meteor, {
|
||||
|
||||
// Sets child's prototype to a new object whose prototype is parent's
|
||||
// prototype. Used as:
|
||||
// Meteor._inherit(ClassB, ClassA).
|
||||
// Meteor._inherits(ClassB, ClassA).
|
||||
// _.extend(ClassB.prototype, { ... })
|
||||
// Inspired by CoffeeScript's `extend` and Google Closure's `goog.inherits`.
|
||||
_inherits: function (Child, Parent) {
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
}
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "2.4.7",
|
||||
"version": "2.4.13",
|
||||
"dependencies": {
|
||||
"async": {
|
||||
"version": "0.2.9"
|
||||
"version": "0.2.10"
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.1.31",
|
||||
"version": "0.1.33",
|
||||
"dependencies": {
|
||||
"amdefine": {
|
||||
"version": "0.1.0"
|
||||
@@ -39,7 +39,7 @@
|
||||
}
|
||||
},
|
||||
"uglify-to-browserify": {
|
||||
"version": "1.0.1"
|
||||
"version": "1.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,17 @@ CssTools = {
|
||||
_.each(ast.stylesheet.rules, function(rule, ruleIndex) {
|
||||
var basePath = path.dirname(rule.position.source);
|
||||
|
||||
// Set the correct basePath based on how the linked asset will be served.
|
||||
// XXX This is wrong. We are coupling the information about how files will
|
||||
// be served by the web server to the information how they were stored
|
||||
// originally on the filesystem in the project structure. Ideally, there
|
||||
// should be some module that tells us precisely how each asset will be
|
||||
// served but for now we are just assuming that everything that comes from
|
||||
// a folder starting with "/packages/" is served on the same path as
|
||||
// it was on the filesystem and everything else is served on root "/".
|
||||
if (! basePath.match(/^\/?packages\//i))
|
||||
basePath = "/";
|
||||
|
||||
_.each(rule.declarations, function(declaration, declarationIndex) {
|
||||
var parts, resource, absolutePath, quotes, oldCssUrl, newCssUrl;
|
||||
var value = declaration.value;
|
||||
|
||||
@@ -4,7 +4,7 @@ Package.describe({
|
||||
});
|
||||
|
||||
Npm.depends({
|
||||
"uglify-js": "2.4.7",
|
||||
"uglify-js": "2.4.13",
|
||||
"css-parse": "https://github.com/reworkcss/css-parse/tarball/aa7e23285375ca621dd20250bac0266c6d8683a5",
|
||||
"css-stringify": "https://github.com/reworkcss/css-stringify/tarball/a7fe6de82e055d41d1c5923ec2ccef06f2a45efa"
|
||||
});
|
||||
|
||||
@@ -4,8 +4,7 @@ Tinytest.add("minifiers - url rewriting when merging", function (test) {
|
||||
return "body { color: green; background: top center url(" + backgroundPath + ") black, bottom center url(" + backgroundPath + "); }"
|
||||
};
|
||||
|
||||
var filename = 'dir/subdir/style.css';
|
||||
var parseOptions = { source: filename, position: true };
|
||||
var parseOptions = { source: null, position: true };
|
||||
|
||||
var t = function(relativeUrl, absoluteUrl, desc) {
|
||||
var ast1 = CssTools.parseCss(stylesheet(relativeUrl), parseOptions);
|
||||
@@ -15,17 +14,30 @@ Tinytest.add("minifiers - url rewriting when merging", function (test) {
|
||||
test.equal(CssTools.stringifyCss(ast1), CssTools.stringifyCss(ast2), desc);
|
||||
};
|
||||
|
||||
t('../image.png', 'dir/image.png', 'parent directory');
|
||||
t('./../image.png', 'dir/image.png', 'parent directory');
|
||||
t('../subdir2/image.png', 'dir/subdir2/image.png', 'cousin directory');
|
||||
parseOptions.source = 'packages/nameOfPackage/style.css';
|
||||
t('../image.png', 'packages/image.png', 'parent directory');
|
||||
t('./../image.png', 'packages/image.png', 'parent directory');
|
||||
t('../nameOfPackage2/image.png', 'packages/nameOfPackage2/image.png', 'cousin directory');
|
||||
t('../../image.png', 'image.png', 'grand parent directory');
|
||||
t('./image.png', 'dir/subdir/image.png', 'current directory');
|
||||
t('./child/image.png', 'dir/subdir/child/image.png', 'child directory');
|
||||
t('child/image.png', 'dir/subdir/child/image.png', 'child directory');
|
||||
t('./image.png', 'packages/nameOfPackage/image.png', 'current directory');
|
||||
t('./child/image.png', 'packages/nameOfPackage/child/image.png', 'child directory');
|
||||
t('child/image.png', 'packages/nameOfPackage/child/image.png', 'child directory');
|
||||
t('/image.png', '/image.png', 'absolute url');
|
||||
t('"/image.png"', '"/image.png"', 'double quoted url');
|
||||
t("'/image.png'", "'/image.png'", 'single quoted url');
|
||||
t('"./../image.png"', '"packages/image.png"', 'quoted parent directory');
|
||||
t('http://i.imgur.com/fBcdJIh.gif', 'http://i.imgur.com/fBcdJIh.gif', 'complete URL');
|
||||
t('"http://i.imgur.com/fBcdJIh.gif"', '"http://i.imgur.com/fBcdJIh.gif"', 'complete quoted URL');
|
||||
t('data:image/png;base64,iVBORw0K=', 'data:image/png;base64,iVBORw0K=', 'data URI');
|
||||
t('http://', 'http://', 'malformed URL');
|
||||
|
||||
parseOptions.source = 'application/client/dir/other-style.css';
|
||||
t('./image.png', '/image.png', 'base path is root');
|
||||
t('./child/image.png', '/child/image.png', 'child directory from root');
|
||||
t('child/image.png', '/child/image.png', 'child directory from root');
|
||||
t('/image.png', '/image.png', 'absolute url');
|
||||
t('"/image.png"', '"/image.png"', 'double quoted url');
|
||||
t("'/image.png'", "'/image.png'", 'single quoted url');
|
||||
t('"./../image.png"', '"dir/image.png"', 'quoted parent directory');
|
||||
t('http://i.imgur.com/fBcdJIh.gif', 'http://i.imgur.com/fBcdJIh.gif', 'complete URL');
|
||||
t('"http://i.imgur.com/fBcdJIh.gif"', '"http://i.imgur.com/fBcdJIh.gif"', 'complete quoted URL');
|
||||
t('data:image/png;base64,iVBORw0K=', 'data:image/png;base64,iVBORw0K=', 'data URI');
|
||||
|
||||
@@ -8,7 +8,7 @@ isArray = function (x) {
|
||||
// XXX maybe this should be EJSON.isObject, though EJSON doesn't know about
|
||||
// RegExp
|
||||
// XXX note that _type(undefined) === 3!!!!
|
||||
isPlainObject = function (x) {
|
||||
isPlainObject = LocalCollection._isPlainObject = function (x) {
|
||||
return x && LocalCollection._f._type(x) === 3;
|
||||
};
|
||||
|
||||
|
||||
@@ -113,20 +113,16 @@ LocalCollection.Cursor = function (collection, selector, options) {
|
||||
|
||||
self._transform = LocalCollection.wrapTransform(options.transform);
|
||||
|
||||
// db_objects is an array of the objects that match the cursor. (It's always
|
||||
// an array, never an IdMap: LocalCollection.Cursor is always ordered.)
|
||||
self.db_objects = null;
|
||||
self.cursor_pos = 0;
|
||||
|
||||
// by default, queries register w/ Deps when it is available.
|
||||
if (typeof Deps !== "undefined")
|
||||
self.reactive = (options.reactive === undefined) ? true : options.reactive;
|
||||
};
|
||||
|
||||
// Since we don't actually have a "nextObject" interface, there's really no
|
||||
// reason to have a "rewind" interface. All it did was make multiple calls
|
||||
// to fetch/map/forEach return nothing the second time.
|
||||
// XXX COMPAT WITH 0.8.1
|
||||
LocalCollection.Cursor.prototype.rewind = function () {
|
||||
var self = this;
|
||||
self.db_objects = null;
|
||||
self.cursor_pos = 0;
|
||||
};
|
||||
|
||||
LocalCollection.prototype.findOne = function (selector, options) {
|
||||
@@ -150,25 +146,52 @@ LocalCollection.prototype.findOne = function (selector, options) {
|
||||
LocalCollection.Cursor.prototype.forEach = function (callback, thisArg) {
|
||||
var self = this;
|
||||
|
||||
if (self.db_objects === null)
|
||||
self.db_objects = self._getRawObjects({ordered: true});
|
||||
var docs;
|
||||
var needsClone = true;
|
||||
if (self.reactive && Deps.active) {
|
||||
// Ensure that we invalidate the current computation if the result of this
|
||||
// query changes. We also piggy-back on top of the query done by
|
||||
// observeChanges so we don't need to do another query.
|
||||
var computation = Deps.currentComputation;
|
||||
var invalidate = function () {
|
||||
computation.invalidate();
|
||||
};
|
||||
var initial = true;
|
||||
docs = [];
|
||||
// observeChanges will stop() when this computation is invalidated
|
||||
self.observeChanges({
|
||||
added: function (id, fields) {
|
||||
if (initial) {
|
||||
fields._id = id;
|
||||
docs.push(fields);
|
||||
} else {
|
||||
invalidate();
|
||||
}
|
||||
},
|
||||
changed: invalidate,
|
||||
removed: invalidate,
|
||||
movedBefore: invalidate
|
||||
});
|
||||
initial = false;
|
||||
needsClone = false; // observeChanges gives us cloned docs
|
||||
} else {
|
||||
docs = self._getRawObjects({ordered: true});
|
||||
}
|
||||
|
||||
if (self.reactive)
|
||||
self._depend({
|
||||
addedBefore: true,
|
||||
removed: true,
|
||||
changed: true,
|
||||
movedBefore: true});
|
||||
|
||||
while (self.cursor_pos < self.db_objects.length) {
|
||||
var elt = EJSON.clone(self.db_objects[self.cursor_pos]);
|
||||
if (self.projectionFn)
|
||||
_.each(docs, function (elt, i) {
|
||||
if (self.projectionFn) {
|
||||
elt = self.projectionFn(elt);
|
||||
} else if (needsClone) {
|
||||
// projection functions always clone the pieces they use, and
|
||||
// observeChanges callbacks got a cloned document, but otherwise we have
|
||||
// to do it here.
|
||||
elt = EJSON.clone(elt);
|
||||
}
|
||||
|
||||
if (self._transform)
|
||||
elt = self._transform(elt);
|
||||
callback.call(thisArg, elt, self.cursor_pos, self);
|
||||
++self.cursor_pos;
|
||||
}
|
||||
callback.call(thisArg, elt, i, self);
|
||||
});
|
||||
};
|
||||
|
||||
LocalCollection.Cursor.prototype.getTransform = function () {
|
||||
@@ -196,14 +219,34 @@ LocalCollection.Cursor.prototype.fetch = function () {
|
||||
LocalCollection.Cursor.prototype.count = function () {
|
||||
var self = this;
|
||||
|
||||
if (self.reactive)
|
||||
self._depend({added: true, removed: true},
|
||||
true /* allow the observe to be unordered */);
|
||||
if (self.reactive && Deps.active) {
|
||||
// Ensure that we invalidate the current computation if the result of this
|
||||
// query changes. We also piggy-back on top of the query done by
|
||||
// observeChanges so we don't need to do another query.
|
||||
var computation = Deps.currentComputation;
|
||||
var invalidate = function () {
|
||||
computation.invalidate();
|
||||
};
|
||||
var initial = true;
|
||||
var count = 0;
|
||||
// observeChanges will stop() when this computation is invalidated
|
||||
self.observeChanges({
|
||||
// we have to use addedBefore rather than added, because observeChanges in
|
||||
// unordered (added) mode doesn't support skip/limit
|
||||
addedBefore: function () {
|
||||
if (initial) {
|
||||
count++;
|
||||
} else {
|
||||
invalidate();
|
||||
}
|
||||
},
|
||||
removed: invalidate
|
||||
});
|
||||
initial = false;
|
||||
return count;
|
||||
}
|
||||
|
||||
if (self.db_objects === null)
|
||||
self.db_objects = self._getRawObjects({ordered: true});
|
||||
|
||||
return self.db_objects.length;
|
||||
return self._getRawObjects({ordered: true}).length;
|
||||
};
|
||||
|
||||
LocalCollection.Cursor.prototype._publishCursor = function (sub) {
|
||||
@@ -277,7 +320,7 @@ _.extend(LocalCollection.Cursor.prototype, {
|
||||
// unordered observe. eg, update's EJSON.clone, and the "there are several"
|
||||
// comment in _modifyAndNotify
|
||||
// XXX allow skip/limit with unordered observe
|
||||
if (!options._allow_unordered && !ordered && (self.skip || self.limit))
|
||||
if (!ordered && (self.skip || self.limit))
|
||||
throw new Error("must use ordered observe with skip or limit");
|
||||
|
||||
if (self.fields && (self.fields._id === 0 || self.fields._id === false))
|
||||
@@ -472,31 +515,6 @@ LocalCollection.Cursor.prototype._getRawObjects = function (options) {
|
||||
return results.slice(idx_start, idx_end);
|
||||
};
|
||||
|
||||
// XXX Maybe we need a version of observe that just calls a callback if
|
||||
// anything changed.
|
||||
LocalCollection.Cursor.prototype._depend = function (changers, _allow_unordered) {
|
||||
var self = this;
|
||||
|
||||
if (Deps.active) {
|
||||
var v = new Deps.Dependency;
|
||||
v.depend();
|
||||
var notifyChange = _.bind(v.changed, v);
|
||||
|
||||
var options = {
|
||||
_suppress_initial: true,
|
||||
_allow_unordered: _allow_unordered
|
||||
};
|
||||
_.each(['added', 'changed', 'removed', 'addedBefore', 'movedBefore'],
|
||||
function (fnName) {
|
||||
if (changers[fnName])
|
||||
options[fnName] = notifyChange;
|
||||
});
|
||||
|
||||
// observeChanges will stop() when this computation is invalidated
|
||||
self.observeChanges(options);
|
||||
}
|
||||
};
|
||||
|
||||
// XXX enforce rule that field names can't start with '$' or contain '.'
|
||||
// (real mongodb does in fact enforce this)
|
||||
// XXX possibly enforce that 'undefined' does not appear (we assume
|
||||
|
||||
@@ -176,11 +176,11 @@ Tinytest.add("minimongo - cursors", function (test) {
|
||||
// fetch
|
||||
res = q.fetch();
|
||||
test.length(res, 20);
|
||||
for (var i = 0; i < 20; i++)
|
||||
for (var i = 0; i < 20; i++) {
|
||||
test.equal(res[i].i, i);
|
||||
// everything empty
|
||||
test.length(q.fetch(), 0);
|
||||
q.rewind();
|
||||
}
|
||||
// call it again, it still works
|
||||
test.length(q.fetch(), 20);
|
||||
|
||||
// forEach
|
||||
var count = 0;
|
||||
@@ -192,9 +192,8 @@ Tinytest.add("minimongo - cursors", function (test) {
|
||||
test.isTrue(cursor === q);
|
||||
}, context);
|
||||
test.equal(count, 20);
|
||||
// everything empty
|
||||
test.length(q.fetch(), 0);
|
||||
q.rewind();
|
||||
// call it again, it still works
|
||||
test.length(q.fetch(), 20);
|
||||
|
||||
// map
|
||||
res = q.map(function (obj, i, cursor) {
|
||||
@@ -206,8 +205,8 @@ Tinytest.add("minimongo - cursors", function (test) {
|
||||
test.length(res, 20);
|
||||
for (var i = 0; i < 20; i++)
|
||||
test.equal(res[i], i * 2);
|
||||
// everything empty
|
||||
test.length(q.fetch(), 0);
|
||||
// call it again, it still works
|
||||
test.length(q.fetch(), 20);
|
||||
|
||||
// findOne (and no rewind first)
|
||||
test.equal(c.findOne({i: 0}).i, 0);
|
||||
@@ -2920,7 +2919,26 @@ Tinytest.add("minimongo - count on cursor with limit", function(test){
|
||||
test.equal(count, 3);
|
||||
|
||||
c.stop();
|
||||
});
|
||||
|
||||
Tinytest.add("minimongo - reactive count with cached cursor", function (test) {
|
||||
var coll = new LocalCollection;
|
||||
var cursor = coll.find({});
|
||||
var firstAutorunCount, secondAutorunCount;
|
||||
Deps.autorun(function(){
|
||||
firstAutorunCount = cursor.count();
|
||||
});
|
||||
Deps.autorun(function(){
|
||||
secondAutorunCount = coll.find({}).count();
|
||||
});
|
||||
test.equal(firstAutorunCount, 0);
|
||||
test.equal(secondAutorunCount, 0);
|
||||
coll.insert({i: 1});
|
||||
coll.insert({i: 2});
|
||||
coll.insert({i: 3});
|
||||
Deps.flush();
|
||||
test.equal(firstAutorunCount, 3);
|
||||
test.equal(secondAutorunCount, 3);
|
||||
});
|
||||
|
||||
Tinytest.add("minimongo - $near operator tests", function (test) {
|
||||
|
||||
@@ -837,8 +837,7 @@ if (Meteor.isServer) {
|
||||
|
||||
Tinytest.add("collection - global insecure", function (test) {
|
||||
// note: This test alters the global insecure status, by sneakily hacking
|
||||
// the global Package object! This may collide with itself if run multiple
|
||||
// times (but is better than the old test which had the same problem)
|
||||
// the global Package object!
|
||||
var insecurePackage = Package.insecure;
|
||||
|
||||
Package.insecure = {};
|
||||
|
||||
@@ -15,12 +15,11 @@ var Future = Npm.require(path.join('fibers', 'future'));
|
||||
MongoInternals = {};
|
||||
MongoTest = {};
|
||||
|
||||
// This is used to add or remove EJSON from the beginning of everything nested
|
||||
// inside an EJSON custom type. It should only be called on pure JSON!
|
||||
var replaceNames = function (filter, thing) {
|
||||
if (typeof thing === "object") {
|
||||
// XXX This condition should match our `looksLikeArray` condition in
|
||||
// underscore. (A Buffer might not be the only thing that should be
|
||||
// treated as an array.)
|
||||
if (_.isArray(thing) || thing instanceof Buffer) {
|
||||
if (_.isArray(thing)) {
|
||||
return _.map(thing, _.bind(replaceNames, null, filter));
|
||||
}
|
||||
var ret = {};
|
||||
@@ -51,7 +50,8 @@ var replaceMongoAtomWithMeteor = function (document) {
|
||||
if (document instanceof MongoDB.ObjectID) {
|
||||
return new Meteor.Collection.ObjectID(document.toHexString());
|
||||
}
|
||||
if (document["EJSON$type"] && document["EJSON$value"]) {
|
||||
if (document["EJSON$type"] && document["EJSON$value"]
|
||||
&& _.size(document) === 2) {
|
||||
return EJSON.fromJSONValue(replaceNames(unmakeMongoLegal, document));
|
||||
}
|
||||
if (document instanceof MongoDB.Timestamp) {
|
||||
@@ -303,13 +303,25 @@ var bindEnvironmentForWrite = function (callback) {
|
||||
MongoConnection.prototype._insert = function (collection_name, document,
|
||||
callback) {
|
||||
var self = this;
|
||||
|
||||
var sendError = function (e) {
|
||||
if (callback)
|
||||
return callback(e);
|
||||
throw e;
|
||||
};
|
||||
|
||||
if (collection_name === "___meteor_failure_test_collection") {
|
||||
var e = new Error("Failure test");
|
||||
e.expected = true;
|
||||
if (callback)
|
||||
return callback(e);
|
||||
else
|
||||
throw e;
|
||||
sendError(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(LocalCollection._isPlainObject(document) &&
|
||||
!EJSON._isCustomType(document))) {
|
||||
sendError(new Error(
|
||||
"Only documents (plain objects) may be inserted into MongoDB"));
|
||||
return;
|
||||
}
|
||||
|
||||
var write = self._maybeBeginWrite();
|
||||
@@ -708,7 +720,7 @@ Cursor = function (mongo, cursorDescription) {
|
||||
self._synchronousCursor = null;
|
||||
};
|
||||
|
||||
_.each(['forEach', 'map', 'rewind', 'fetch', 'count'], function (method) {
|
||||
_.each(['forEach', 'map', 'fetch', 'count'], function (method) {
|
||||
Cursor.prototype[method] = function () {
|
||||
var self = this;
|
||||
|
||||
@@ -731,6 +743,13 @@ _.each(['forEach', 'map', 'rewind', 'fetch', 'count'], function (method) {
|
||||
};
|
||||
});
|
||||
|
||||
// Since we don't actually have a "nextObject" interface, there's really no
|
||||
// reason to have a "rewind" interface. All it did was make multiple calls
|
||||
// to fetch/map/forEach return nothing the second time.
|
||||
// XXX COMPAT WITH 0.8.1
|
||||
Cursor.prototype.rewind = function () {
|
||||
};
|
||||
|
||||
Cursor.prototype.getTransform = function () {
|
||||
return this._cursorDescription.options.transform;
|
||||
};
|
||||
@@ -862,6 +881,9 @@ _.extend(SynchronousCursor.prototype, {
|
||||
forEach: function (callback, thisArg) {
|
||||
var self = this;
|
||||
|
||||
// Get back to the beginning.
|
||||
self._rewind();
|
||||
|
||||
// We implement the loop ourself instead of using self._dbCursor.each,
|
||||
// because "each" will call its callback outside of a fiber which makes it
|
||||
// much more complex to make this function synchronous.
|
||||
@@ -883,7 +905,7 @@ _.extend(SynchronousCursor.prototype, {
|
||||
return res;
|
||||
},
|
||||
|
||||
rewind: function () {
|
||||
_rewind: function () {
|
||||
var self = this;
|
||||
|
||||
// known to be synchronous
|
||||
|
||||
@@ -24,6 +24,10 @@ if (Meteor.isServer) {
|
||||
Meteor.publish('c-' + name, function () {
|
||||
return c.find();
|
||||
});
|
||||
},
|
||||
dropInsecureCollection: function(name) {
|
||||
var c = COLLECTIONS[name];
|
||||
c._dropCollection();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -330,7 +334,6 @@ Tinytest.addAsync("mongo-livedata - basics, " + idGeneration, function (test, on
|
||||
}, context);
|
||||
test.equal(total, 14);
|
||||
|
||||
cur.rewind();
|
||||
index = 0;
|
||||
test.equal(cur.map(function (doc, i, cursor) {
|
||||
// XXX we could theoretically make map run its iterations in parallel or
|
||||
@@ -1448,21 +1451,28 @@ testAsyncMulti('mongo-livedata - document with a custom type, ' + idGeneration,
|
||||
Meteor.subscribe('c-' + this.collectionName, expect());
|
||||
}
|
||||
}, function (test, expect) {
|
||||
var coll = new Meteor.Collection(this.collectionName, collectionOptions);
|
||||
var self = this;
|
||||
self.coll = new Meteor.Collection(this.collectionName, collectionOptions);
|
||||
var docId;
|
||||
// Dog is implemented at the top of the file, outside of the idGeneration
|
||||
// loop (so that we only call EJSON.addType once).
|
||||
var d = new Dog("reginald", "purple");
|
||||
coll.insert({d: d}, expect(function (err, id) {
|
||||
self.coll.insert({d: d}, expect(function (err, id) {
|
||||
test.isFalse(err);
|
||||
test.isTrue(id);
|
||||
docId = id;
|
||||
var cursor = coll.find();
|
||||
var cursor = self.coll.find();
|
||||
test.equal(cursor.count(), 1);
|
||||
var inColl = coll.findOne();
|
||||
var inColl = self.coll.findOne();
|
||||
test.isTrue(inColl);
|
||||
inColl && test.equal(inColl.d.speak(), "woof");
|
||||
}));
|
||||
}, function (test, expect) {
|
||||
var self = this;
|
||||
self.coll.insert(new Dog("rover", "orange"), expect(function (err, id) {
|
||||
test.isTrue(err);
|
||||
test.isFalse(id);
|
||||
}));
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -2331,14 +2341,21 @@ _.each( ['STRING', 'MONGO'], function (idGeneration) {
|
||||
testAsyncMulti('mongo-livedata - consistent _id generation ' + name + ', ' + repetitions + ' repetitions on ' + collectionCount + ' collections, idGeneration=' + idGeneration, [ function (test, expect) {
|
||||
var collectionOptions = { idGeneration: idGeneration };
|
||||
|
||||
var cleanups = this.cleanups = [];
|
||||
this.collections = _.times(collectionCount, function () {
|
||||
var collectionName = "consistentid_" + Random.id();
|
||||
if (Meteor.isClient) {
|
||||
Meteor.call('createInsecureCollection', collectionName, collectionOptions);
|
||||
Meteor.subscribe('c-' + collectionName, expect());
|
||||
cleanups.push(function (expect) { Meteor.call('dropInsecureCollection', collectionName, expect(function () {})); });
|
||||
}
|
||||
|
||||
return (COLLECTIONS[collectionName] = new Meteor.Collection(collectionName, collectionOptions));
|
||||
var collection = new Meteor.Collection(collectionName, collectionOptions);
|
||||
if (Meteor.isServer) {
|
||||
cleanups.push(function () { collection._dropCollection(); });
|
||||
}
|
||||
COLLECTIONS[collectionName] = collection;
|
||||
return collection;
|
||||
});
|
||||
}, function (test, expect) {
|
||||
// now run the actual test
|
||||
@@ -2347,6 +2364,11 @@ _.each( ['STRING', 'MONGO'], function (idGeneration) {
|
||||
fn(test, expect, this.collections[j], i);
|
||||
}
|
||||
}
|
||||
}, function (test, expect) {
|
||||
// Run any registered cleanup functions (e.g. to drop collections)
|
||||
_.each(this.cleanups, function(cleanup) {
|
||||
cleanup(expect);
|
||||
});
|
||||
}]);
|
||||
|
||||
});
|
||||
@@ -2954,12 +2976,3 @@ testAsyncMulti("mongo-livedata - undefined find options", [
|
||||
test.equal(result, self.doc);
|
||||
}
|
||||
]);
|
||||
|
||||
// We're not sure if this should be supported, but it was broken in
|
||||
// 0.8.1 and we decided to make a quick
|
||||
// fix. https://github.com/meteor/meteor/issues/2095
|
||||
Meteor.isServer && Tinytest.add("mongo-livedata - insert and retrieve EJSON user-defined type as document", function (test) {
|
||||
var coll = new Meteor.Collection(Random.id());
|
||||
coll.insert(new Meteor.Collection.ObjectID());
|
||||
coll.find({}).fetch();
|
||||
});
|
||||
|
||||
@@ -126,10 +126,11 @@ _.extend(PollingObserveDriver.prototype, {
|
||||
--self._pollsScheduledButNotStarted;
|
||||
|
||||
var first = false;
|
||||
if (!self._results) {
|
||||
var oldResults = self._results;
|
||||
if (!oldResults) {
|
||||
first = true;
|
||||
// XXX maybe use OrderedDict instead?
|
||||
self._results = self._ordered ? [] : new LocalCollection._IdMap;
|
||||
oldResults = self._ordered ? [] : new LocalCollection._IdMap;
|
||||
}
|
||||
|
||||
self._testOnlyPollCallback && self._testOnlyPollCallback();
|
||||
@@ -138,25 +139,34 @@ _.extend(PollingObserveDriver.prototype, {
|
||||
var writesForCycle = self._pendingWrites;
|
||||
self._pendingWrites = [];
|
||||
|
||||
// Get the new query results. (These calls can yield.)
|
||||
if (!first)
|
||||
self._synchronousCursor.rewind();
|
||||
var newResults = self._synchronousCursor.getRawObjects(self._ordered);
|
||||
var oldResults = self._results;
|
||||
// Get the new query results. (This yields.)
|
||||
try {
|
||||
var newResults = self._synchronousCursor.getRawObjects(self._ordered);
|
||||
} catch (e) {
|
||||
// getRawObjects can throw if we're having trouble talking to the
|
||||
// database. That's fine --- we will repoll later anyway. But we should
|
||||
// make sure not to lose track of this cycle's writes.
|
||||
Array.prototype.push.apply(self._pendingWrites, writesForCycle);
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Run diffs. (This can yield too.)
|
||||
// Run diffs.
|
||||
if (!self._stopped) {
|
||||
LocalCollection._diffQueryChanges(
|
||||
self._ordered, oldResults, newResults, self._multiplexer);
|
||||
}
|
||||
|
||||
// Replace self._results atomically.
|
||||
self._results = newResults;
|
||||
|
||||
// Signals the multiplexer to call all initial adds.
|
||||
// Signals the multiplexer to allow all observeChanges calls that share this
|
||||
// multiplexer to return. (This happens asynchronously, via the
|
||||
// multiplexer's queue.)
|
||||
if (first)
|
||||
self._multiplexer.ready();
|
||||
|
||||
// Replace self._results atomically. (This assignment is what makes `first`
|
||||
// stay through on the next cycle, so we've waited until after we've
|
||||
// committed to ready-ing the multiplexer.)
|
||||
self._results = newResults;
|
||||
|
||||
// Once the ObserveMultiplexer has processed everything we've done in this
|
||||
// round, mark all the writes which existed before this call as
|
||||
// commmitted. (If new writes have shown up in the meantime, there'll
|
||||
|
||||
@@ -106,18 +106,13 @@ middleware = function (req, res, next) {
|
||||
}
|
||||
}
|
||||
|
||||
// XXX the following is actually wrong. if someone wants to
|
||||
// redirect rather than close once we are done with the OAuth
|
||||
// flow, as supported by
|
||||
// Oauth_renderOauthResults, this will still
|
||||
// close the popup instead. Once we fully support the redirect
|
||||
// flow (by supporting that in places such as
|
||||
// packages/facebook/facebook_client.js) we should revisit this.
|
||||
//
|
||||
// close the popup. because nobody likes them just hanging
|
||||
// there. when someone sees this multiple times they might
|
||||
// think to check server logs (we hope?)
|
||||
closePopup(res);
|
||||
OAuth._endOfLoginResponse(res, {
|
||||
query: req.query,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -149,67 +144,114 @@ var ensureConfigured = function(serviceName) {
|
||||
}
|
||||
};
|
||||
|
||||
var isSafe = function (value) {
|
||||
// This matches strings generated by `Random.secret` and
|
||||
// `Random.id`.
|
||||
return typeof value === "string" &&
|
||||
/^[a-zA-Z0-9\-_]+$/.test(value);
|
||||
};
|
||||
|
||||
// Internal: used by the oauth1 and oauth2 packages
|
||||
OAuth._renderOauthResults = function(res, query, credentialSecret) {
|
||||
// We support ?close and ?redirect=URL. Any other query should just
|
||||
// serve a blank page. For tests, we support the
|
||||
// We expect the ?close parameter to be present, in which case we
|
||||
// close the popup at the end of the OAuth flow. Any other query
|
||||
// string should just serve a blank page. For tests, we support the
|
||||
// `only_credential_secret_for_test` parameter, which just returns the
|
||||
// credential secret without any surrounding HTML. (The test needs to
|
||||
// be able to easily grab the secret and use it to log in.)
|
||||
//
|
||||
// XXX only_credential_secret_for_test could be useful for other
|
||||
// things beside tests, like command-line clients. We should give it a
|
||||
// real name and serve the credential secret in JSON.
|
||||
if (query.only_credential_secret_for_test) {
|
||||
res.writeHead(200, {'Content-Type': 'text/html'});
|
||||
res.end(credentialSecret, 'utf-8');
|
||||
} else if (query.error) {
|
||||
Log.warn("Error in OAuth Server: " + query.error);
|
||||
closePopup(res);
|
||||
} else if ('close' in query) { // check with 'in' because we don't set a value
|
||||
closePopup(res, query.state, credentialSecret);
|
||||
} else if (query.redirect) {
|
||||
// Only redirect to URLs on the same domain as this app.
|
||||
// XXX No code in core uses this code path right now.
|
||||
// XXX In order for the redirect flow to be fully supported, we'd
|
||||
// have to communicate the credentialSecret back to the app somehow.
|
||||
var redirectHostname = url.parse(query.redirect).hostname;
|
||||
var appHostname = url.parse(Meteor.absoluteUrl()).hostname;
|
||||
if (appHostname === redirectHostname) {
|
||||
// We rely on node to make sure the header is really only a single header
|
||||
// (not, for example, a url with a newline and then another header).
|
||||
res.writeHead(302, {'Location': query.redirect});
|
||||
} else {
|
||||
res.writeHead(400);
|
||||
}
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(200, {'Content-Type': 'text/html'});
|
||||
res.end('', 'utf-8');
|
||||
var details = { query: query };
|
||||
if (query.error) {
|
||||
details.error = query.error;
|
||||
} else {
|
||||
var token = query.state;
|
||||
var secret = credentialSecret;
|
||||
if (token && secret &&
|
||||
isSafe(token) && isSafe(secret)) {
|
||||
details.credentials = { token: token, secret: secret};
|
||||
} else {
|
||||
details.error = "invalid_credential_token_or_secret";
|
||||
}
|
||||
}
|
||||
|
||||
OAuth._endOfLoginResponse(res, details);
|
||||
}
|
||||
};
|
||||
|
||||
var closePopup = function(res, state, credentialSecret) {
|
||||
|
||||
var isSafe = function (value) {
|
||||
// This matches strings generated by `Random.secret` and
|
||||
// `Random.id`.
|
||||
return typeof value === "string" &&
|
||||
/^[a-zA-Z0-9\-_]+$/.test(value);
|
||||
};
|
||||
// Writes an HTTP response to the popup window at the end of an OAuth
|
||||
// login flow. At this point, if the user has successfully authenticated
|
||||
// to the OAuth server and authorized this app, we communicate the
|
||||
// credentialToken and credentialSecret to the main window. The main
|
||||
// window must provide both these values to the DDP `login` method to
|
||||
// authenticate its DDP connection. After communicating these vaues to
|
||||
// the main window, we close the popup.
|
||||
//
|
||||
// We export this function so that developers can override this
|
||||
// behavior, which is particularly useful in, for example, some mobile
|
||||
// environments where popups and/or `window.opener` don't work. For
|
||||
// example, an app could override `OAuth._endOfLoginResponse` to put the
|
||||
// credential token and credential secret in the popup URL for the main
|
||||
// window to read them there instead of using `window.opener`. If you
|
||||
// override this function, you take responsibility for writing to the
|
||||
// request and calling `res.end()` to complete the request.
|
||||
//
|
||||
// Arguments:
|
||||
// - res: the HTTP response object
|
||||
// - details:
|
||||
// - query: the query string on the HTTP request
|
||||
// - credentials: { token: *, secret: * }. If present, this field
|
||||
// indicates that the login was successful. Return these values
|
||||
// to the client, who can use them to log in over DDP. If
|
||||
// present, the values have been checked against a limited
|
||||
// character set and are safe to include in HTML.
|
||||
// - error: if present, a string or Error indicating an error that
|
||||
// occurred during the login. This can come from the client and
|
||||
// so shouldn't be trusted for security decisions or included in
|
||||
// the response without sanitizing it first. Only one of `error`
|
||||
// or `credentials` should be set.
|
||||
OAuth._endOfLoginResponse = function(res, details) {
|
||||
|
||||
res.writeHead(200, {'Content-Type': 'text/html'});
|
||||
// If we have a credentialSecret, report it back to the parent window, with
|
||||
// the corresponding state (which we sanitize because it came from a
|
||||
// query parameter). The parent window uses the state and credential secret
|
||||
// to log in over DDP.
|
||||
var setCredentialSecret = '';
|
||||
if (state && credentialSecret && isSafe(state) && isSafe(credentialSecret)) {
|
||||
setCredentialSecret = 'window.opener && ' +
|
||||
'window.opener.Package.oauth.OAuth._handleCredentialSecret(' +
|
||||
JSON.stringify(state) + ', ' + JSON.stringify(credentialSecret) + ');';
|
||||
|
||||
var content = function (setCredentialSecret) {
|
||||
return '<html><head><script>' +
|
||||
setCredentialSecret +
|
||||
'window.close()</script></head></html>';
|
||||
};
|
||||
|
||||
if (details.error) {
|
||||
Log.warn("Error in OAuth Server: " +
|
||||
(details.error instanceof Error ?
|
||||
details.error.message : details.error));
|
||||
res.end(content(""), 'utf-8');
|
||||
return;
|
||||
}
|
||||
|
||||
if ("close" in details.query) {
|
||||
// If we have a credentialSecret, report it back to the parent
|
||||
// window, with the corresponding credentialToken. The parent window
|
||||
// uses the credentialToken and credentialSecret to log in over DDP.
|
||||
var setCredentialSecret = '';
|
||||
if (details.credentials.token && details.credentials.secret) {
|
||||
setCredentialSecret = 'var credentialToken = ' +
|
||||
JSON.stringify(details.credentials.token) + ';' +
|
||||
'var credentialSecret = ' +
|
||||
JSON.stringify(details.credentials.secret) + ';' +
|
||||
'window.opener && ' +
|
||||
'window.opener.Package.oauth.OAuth._handleCredentialSecret(' +
|
||||
'credentialToken, credentialSecret);';
|
||||
}
|
||||
res.end(content(setCredentialSecret), "utf-8");
|
||||
} else {
|
||||
res.end("", "utf-8");
|
||||
}
|
||||
var content =
|
||||
'<html><head><script>' +
|
||||
setCredentialSecret +
|
||||
'window.close()</script></head></html>';
|
||||
res.end(content, 'utf-8');
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ Package.describe({
|
||||
Package.on_use(function (api) {
|
||||
api.use('deps');
|
||||
api.use('minimongo'); // for idStringify
|
||||
api.use('underscore');
|
||||
api.use('random');
|
||||
api.export('ObserveSequence');
|
||||
api.add_files(['observe_sequence.js']);
|
||||
});
|
||||
|
||||
1
packages/sha/.gitignore
vendored
Normal file
1
packages/sha/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.build*
|
||||
9
packages/sha/package.js
Normal file
9
packages/sha/package.js
Normal file
@@ -0,0 +1,9 @@
|
||||
Package.describe({
|
||||
summary: "SHA256 implementation",
|
||||
internal: true
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.export('SHA256');
|
||||
api.add_files(['sha256.js'], ['client', 'server']);
|
||||
});
|
||||
@@ -1,7 +1,5 @@
|
||||
/// METEOR WRAPPER
|
||||
//
|
||||
// XXX this should get packaged and moved into the Meteor.crypto
|
||||
// namespace, along with other hash functions.
|
||||
SHA256 = (function () {
|
||||
|
||||
|
||||
@@ -14,18 +12,18 @@ SHA256 = (function () {
|
||||
* Original code by Angel Marin, Paul Johnston.
|
||||
*
|
||||
**/
|
||||
|
||||
|
||||
function SHA256(s){
|
||||
|
||||
|
||||
var chrsz = 8;
|
||||
var hexcase = 0;
|
||||
|
||||
|
||||
function safe_add (x, y) {
|
||||
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
|
||||
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
|
||||
return (msw << 16) | (lsw & 0xFFFF);
|
||||
}
|
||||
|
||||
|
||||
function S (X, n) { return ( X >>> n ) | (X << (32 - n)); }
|
||||
function R (X, n) { return ( X >>> n ); }
|
||||
function Ch(x, y, z) { return ((x & y) ^ ((~x) & z)); }
|
||||
@@ -34,17 +32,17 @@ function SHA256(s){
|
||||
function Sigma1256(x) { return (S(x, 6) ^ S(x, 11) ^ S(x, 25)); }
|
||||
function Gamma0256(x) { return (S(x, 7) ^ S(x, 18) ^ R(x, 3)); }
|
||||
function Gamma1256(x) { return (S(x, 17) ^ S(x, 19) ^ R(x, 10)); }
|
||||
|
||||
|
||||
function core_sha256 (m, l) {
|
||||
var K = new Array(0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5, 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, 0xE49B69C1, 0xEFBE4786, 0xFC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA, 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x6CA6351, 0x14292967, 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85, 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070, 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3, 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2);
|
||||
var HASH = new Array(0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19);
|
||||
var W = new Array(64);
|
||||
var a, b, c, d, e, f, g, h, i, j;
|
||||
var T1, T2;
|
||||
|
||||
|
||||
m[l >> 5] |= 0x80 << (24 - l % 32);
|
||||
m[((l + 64 >> 9) << 4) + 15] = l;
|
||||
|
||||
|
||||
for ( var i = 0; i<m.length; i+=16 ) {
|
||||
a = HASH[0];
|
||||
b = HASH[1];
|
||||
@@ -54,14 +52,14 @@ function SHA256(s){
|
||||
f = HASH[5];
|
||||
g = HASH[6];
|
||||
h = HASH[7];
|
||||
|
||||
|
||||
for ( var j = 0; j<64; j++) {
|
||||
if (j < 16) W[j] = m[j + i];
|
||||
else W[j] = safe_add(safe_add(safe_add(Gamma1256(W[j - 2]), W[j - 7]), Gamma0256(W[j - 15])), W[j - 16]);
|
||||
|
||||
|
||||
T1 = safe_add(safe_add(safe_add(safe_add(h, Sigma1256(e)), Ch(e, f, g)), K[j]), W[j]);
|
||||
T2 = safe_add(Sigma0256(a), Maj(a, b, c));
|
||||
|
||||
|
||||
h = g;
|
||||
g = f;
|
||||
f = e;
|
||||
@@ -71,7 +69,7 @@ function SHA256(s){
|
||||
b = a;
|
||||
a = safe_add(T1, T2);
|
||||
}
|
||||
|
||||
|
||||
HASH[0] = safe_add(a, HASH[0]);
|
||||
HASH[1] = safe_add(b, HASH[1]);
|
||||
HASH[2] = safe_add(c, HASH[2]);
|
||||
@@ -83,7 +81,7 @@ function SHA256(s){
|
||||
}
|
||||
return HASH;
|
||||
}
|
||||
|
||||
|
||||
function str2binb (str) {
|
||||
var bin = Array();
|
||||
var mask = (1 << chrsz) - 1;
|
||||
@@ -92,7 +90,7 @@ function SHA256(s){
|
||||
}
|
||||
return bin;
|
||||
}
|
||||
|
||||
|
||||
function Utf8Encode(string) {
|
||||
// METEOR change:
|
||||
// The webtoolkit.info version of this code added this
|
||||
@@ -102,11 +100,11 @@ function SHA256(s){
|
||||
//
|
||||
// string = string.replace(/\r\n/g,"\n");
|
||||
var utftext = "";
|
||||
|
||||
|
||||
for (var n = 0; n < string.length; n++) {
|
||||
|
||||
|
||||
var c = string.charCodeAt(n);
|
||||
|
||||
|
||||
if (c < 128) {
|
||||
utftext += String.fromCharCode(c);
|
||||
}
|
||||
@@ -119,12 +117,12 @@ function SHA256(s){
|
||||
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
|
||||
utftext += String.fromCharCode((c & 63) | 128);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
return utftext;
|
||||
}
|
||||
|
||||
|
||||
function binb2hex (binarray) {
|
||||
var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
|
||||
var str = "";
|
||||
@@ -134,10 +132,10 @@ function SHA256(s){
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
|
||||
s = Utf8Encode(s);
|
||||
return binb2hex(core_sha256(str2binb(s), s.length * chrsz));
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// METEOR WRAPPER
|
||||
1
packages/spacebars-common/.gitignore
vendored
Normal file
1
packages/spacebars-common/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.build*
|
||||
9
packages/spacebars-common/package.js
Normal file
9
packages/spacebars-common/package.js
Normal file
@@ -0,0 +1,9 @@
|
||||
Package.describe({
|
||||
summary: "Common code for spacebars and spacebars-compiler",
|
||||
internal: true
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.export('Spacebars');
|
||||
api.add_files('spacebars.js');
|
||||
});
|
||||
1
packages/spacebars-common/spacebars.js
Normal file
1
packages/spacebars-common/spacebars.js
Normal file
@@ -0,0 +1 @@
|
||||
Spacebars = {};
|
||||
@@ -125,6 +125,20 @@ Tinytest.add("spacebars - compiler output", function (test) {
|
||||
}));
|
||||
});
|
||||
|
||||
run("{{!-- --}}{{#if cond}}aaa{{!\n}}{{else}}{{!}}bbb{{!-- --}}{{/if}}{{!}}",
|
||||
function() {
|
||||
var self = this;
|
||||
return UI.If(function () {
|
||||
return Spacebars.call(self.lookup("cond"));
|
||||
}, UI.block(function() {
|
||||
var self = this;
|
||||
return "aaa";
|
||||
}), UI.block(function() {
|
||||
var self = this;
|
||||
return "bbb";
|
||||
}));
|
||||
});
|
||||
|
||||
run("{{> foo bar}}",
|
||||
function() {
|
||||
var self = this;
|
||||
|
||||
@@ -3,8 +3,8 @@ Package.describe({
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use('spacebars');
|
||||
api.imply('spacebars');
|
||||
api.use('spacebars-common');
|
||||
api.imply('spacebars-common');
|
||||
|
||||
// we attach stuff to the global symbol `HTML`, exported
|
||||
// by `htmljs` via `html-tools`, so we both use and effectively
|
||||
@@ -13,7 +13,6 @@ Package.on_use(function (api) {
|
||||
api.imply('html-tools');
|
||||
|
||||
api.use('underscore');
|
||||
api.use('ui');
|
||||
api.use('minifiers', ['server']);
|
||||
api.add_files(['tokens.js', 'tojs.js', 'templatetag.js',
|
||||
'spacebars-compiler.js']);
|
||||
|
||||
@@ -163,10 +163,22 @@ var builtInBlockHelpers = {
|
||||
'each': 'UI.Each'
|
||||
};
|
||||
|
||||
// These must be prefixed with `UI.` when you use them in a template.
|
||||
var builtInLexicals = {
|
||||
// Some `UI.*` paths are special in that they generate code that
|
||||
// doesn't folow the normal lookup rules for dotted symbols. The
|
||||
// following names must be prefixed with `UI.` when you use them in a
|
||||
// template.
|
||||
var builtInUIPaths = {
|
||||
// `template` is a local variable defined in the generated render
|
||||
// function for the template in which `UI.contentBlock` (or
|
||||
// `UI.elseBlock`) is invoked. `template` is a reference to the
|
||||
// template itself.
|
||||
'contentBlock': 'template.__content',
|
||||
'elseBlock': 'template.__elseContent'
|
||||
'elseBlock': 'template.__elseContent',
|
||||
|
||||
// `Template` is the global template namespace. If you define a
|
||||
// template named `foo` in Spacebars, it gets defined as
|
||||
// `Template.foo` in JavaScript.
|
||||
'dynamic': 'Template.__dynamic'
|
||||
};
|
||||
|
||||
// A "reserved name" can't be used as a <template> name. This
|
||||
@@ -288,11 +300,11 @@ var codeGenPath = function (path, opts) {
|
||||
// inclusion or as a block helper, in addition to supporting
|
||||
// `{{> UI.contentBlock}}`.
|
||||
if (path.length >= 2 &&
|
||||
path[0] === 'UI' && builtInLexicals.hasOwnProperty(path[1])) {
|
||||
path[0] === 'UI' && builtInUIPaths.hasOwnProperty(path[1])) {
|
||||
if (path.length > 2)
|
||||
throw new Error("Unexpected dotted path beginning with " +
|
||||
path[0] + '.' + path[1]);
|
||||
return builtInLexicals[path[1]];
|
||||
return builtInUIPaths[path[1]];
|
||||
}
|
||||
|
||||
var args = [toJSLiteral(path[0])];
|
||||
|
||||
@@ -102,6 +102,8 @@ Tinytest.add("spacebars - stache tags", function (test) {
|
||||
|
||||
run('{{foo.[]/[]}}', {type: 'DOUBLE', path: ['foo', '', ''],
|
||||
args: []});
|
||||
run('{{x foo.[=]}}', {type: 'DOUBLE', path: ['x'],
|
||||
args: [['PATH', ['foo', '=']]]});
|
||||
run('{{[].foo}}', "Path can't start with empty string");
|
||||
|
||||
run('{{foo null}}', {type: 'DOUBLE', path: ['foo'],
|
||||
@@ -159,6 +161,9 @@ Tinytest.add("spacebars - stache tags", function (test) {
|
||||
run('{{./this}}', "Can only use");
|
||||
run('{{../this}}', "Can only use");
|
||||
|
||||
run('{{foo "="}}', {type: 'DOUBLE', path: ['foo'],
|
||||
args: [['STRING', '=']]});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ TemplateTag.parse = function (scannerOrString) {
|
||||
// Result is either the keyword matched, or null
|
||||
// if we're not at a keyword argument position.
|
||||
var scanArgKeyword = function () {
|
||||
var match = /^([^\{\}\(\)\>#=\s]+)\s*=\s*/.exec(scanner.rest());
|
||||
var match = /^([^\{\}\(\)\>#=\s"'\[\]]+)\s*=\s*/.exec(scanner.rest());
|
||||
if (match) {
|
||||
scanner.pos += match[0].length;
|
||||
return match[1];
|
||||
|
||||
49
packages/spacebars-tests/assets/markdown_basic.html
Normal file
49
packages/spacebars-tests/assets/markdown_basic.html
Normal file
@@ -0,0 +1,49 @@
|
||||
<p><i>hi</i>
|
||||
/each}}</p>
|
||||
|
||||
<p><b><i>hi</i></b>
|
||||
<b>/each}}</b></p>
|
||||
|
||||
<ul>
|
||||
<li><i>hi</i></li>
|
||||
<li><p>/each}}</p></li>
|
||||
<li><p><b><i>hi</i></b></p></li>
|
||||
<li><b>/each}}</b></li>
|
||||
</ul>
|
||||
|
||||
<p>some paragraph to fix showdown's four space parsing below.</p>
|
||||
|
||||
<pre><code><i>hi</i>
|
||||
/each}}
|
||||
|
||||
<b><i>hi</i></b>
|
||||
<b>/each}}</b>
|
||||
</code></pre>
|
||||
|
||||
<p>&gt</p>
|
||||
|
||||
<ul>
|
||||
<li>&gt</li>
|
||||
</ul>
|
||||
|
||||
<p><code>&gt</code></p>
|
||||
|
||||
<pre><code>&gt
|
||||
</code></pre>
|
||||
|
||||
<p>></p>
|
||||
|
||||
<ul>
|
||||
<li>></li>
|
||||
</ul>
|
||||
|
||||
<p><code>&gt;</code></p>
|
||||
|
||||
<pre><code>&gt;
|
||||
</code></pre>
|
||||
|
||||
<p><code><i>hi</i></code>
|
||||
<code>/each}}</code></p>
|
||||
|
||||
<p><code><b><i>hi</i></b></code>
|
||||
<code><b>/each}}</code></p>
|
||||
15
packages/spacebars-tests/assets/markdown_each1.html
Normal file
15
packages/spacebars-tests/assets/markdown_each1.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<p><b></b></p>
|
||||
|
||||
<ul>
|
||||
<li></li>
|
||||
<li><b></b></li>
|
||||
</ul>
|
||||
|
||||
<p>some paragraph to fix showdown's four space parsing below.</p>
|
||||
|
||||
<pre><code><b></b>
|
||||
</code></pre>
|
||||
|
||||
<p>``</p>
|
||||
|
||||
<p><code><b></b></code></p>
|
||||
19
packages/spacebars-tests/assets/markdown_each2.html
Normal file
19
packages/spacebars-tests/assets/markdown_each2.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<p>item</p>
|
||||
|
||||
<p><b>item</b></p>
|
||||
|
||||
<ul>
|
||||
<li><p>item</p></li>
|
||||
<li><p><b>item</b></p></li>
|
||||
</ul>
|
||||
|
||||
<p>some paragraph to fix showdown's four space parsing below.</p>
|
||||
|
||||
<pre><code>item
|
||||
|
||||
<b>item</b>
|
||||
</code></pre>
|
||||
|
||||
<p><code>item</code></p>
|
||||
|
||||
<p><code><b>item</b></code></p>
|
||||
19
packages/spacebars-tests/assets/markdown_if1.html
Normal file
19
packages/spacebars-tests/assets/markdown_if1.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<p>false</p>
|
||||
|
||||
<p><b>false</b></p>
|
||||
|
||||
<ul>
|
||||
<li><p>false</p></li>
|
||||
<li><p><b>false</b></p></li>
|
||||
</ul>
|
||||
|
||||
<p>some paragraph to fix showdown's four space parsing below.</p>
|
||||
|
||||
<pre><code>false
|
||||
|
||||
<b>false</b>
|
||||
</code></pre>
|
||||
|
||||
<p><code>false</code></p>
|
||||
|
||||
<p><code><b>false</b></code></p>
|
||||
19
packages/spacebars-tests/assets/markdown_if2.html
Normal file
19
packages/spacebars-tests/assets/markdown_if2.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<p>true</p>
|
||||
|
||||
<p><b>true</b></p>
|
||||
|
||||
<ul>
|
||||
<li><p>true</p></li>
|
||||
<li><p><b>true</b></p></li>
|
||||
</ul>
|
||||
|
||||
<p>some paragraph to fix showdown's four space parsing below.</p>
|
||||
|
||||
<pre><code>true
|
||||
|
||||
<b>true</b>
|
||||
</code></pre>
|
||||
|
||||
<p><code>true</code></p>
|
||||
|
||||
<p><code><b>true</b></code></p>
|
||||
@@ -18,4 +18,14 @@ Package.on_test(function (api) {
|
||||
'template_tests.html',
|
||||
'template_tests.js'
|
||||
], 'client');
|
||||
|
||||
api.add_files('template_tests_server.js', 'server');
|
||||
|
||||
api.add_files([
|
||||
'assets/markdown_basic.html',
|
||||
'assets/markdown_if1.html',
|
||||
'assets/markdown_if2.html',
|
||||
'assets/markdown_each1.html',
|
||||
'assets/markdown_each2.html'
|
||||
], 'server', { isAsset: true });
|
||||
});
|
||||
|
||||
@@ -664,6 +664,34 @@ Hi there!
|
||||
<a href="#bad-url" id="spacebars_test_event_returns_false_link">click me</a>
|
||||
</template>
|
||||
|
||||
<template name="spacebars_test_event_selectors1">
|
||||
<div>{{> spacebars_test_event_selectors2}}</div>
|
||||
</template>
|
||||
|
||||
<template name="spacebars_test_event_selectors2">
|
||||
<p class="p1">Not it</p>
|
||||
<div><p class="p2">It</p></div>
|
||||
</template>
|
||||
|
||||
<template name="spacebars_test_event_selectors_capturing1">
|
||||
<div>{{> spacebars_test_event_selectors_capturing2}}</div>
|
||||
</template>
|
||||
|
||||
<template name="spacebars_test_event_selectors_capturing2">
|
||||
<video class='video1'>
|
||||
<source id='mp4'
|
||||
src="http://media.w3.org/2010/05/sintel/trailer.mp4"
|
||||
type='video/mp4'>
|
||||
</video>
|
||||
<div>
|
||||
<video class='video2'>
|
||||
<source id='mp4'
|
||||
src="http://media.w3.org/2010/05/sintel/trailer.mp4"
|
||||
type='video/mp4'>
|
||||
</video>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="spacebars_test_tables1">
|
||||
<table><tr><td>Foo</td></tr></table>
|
||||
</template>
|
||||
@@ -726,3 +754,71 @@ Hi there!
|
||||
<img src="{{foo}}" />
|
||||
<input value="{{foo}}" />
|
||||
</template>
|
||||
|
||||
<template name="spacebars_test_event_handler_cleanup">
|
||||
{{#if foo}}
|
||||
{{>spacebars_test_event_handler_cleanup_sub}}
|
||||
{{/if}}
|
||||
</template>
|
||||
|
||||
<template name="spacebars_test_event_handler_cleanup_sub">
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<template name="spacebars_test_each_with_autorun_insert">
|
||||
{{#each items}}
|
||||
{{name}}
|
||||
{{/each}}
|
||||
</template>
|
||||
|
||||
<template name="spacebars_test_ui_hooks">
|
||||
<div class="test-ui-hooks">
|
||||
{{#each items}}
|
||||
<div class="item">{{_id}}</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="spacebars_test_ui_hooks_nested">
|
||||
{{#if foo}}
|
||||
{{> spacebars_test_ui_hooks_nested_sub}}
|
||||
{{/if}}
|
||||
</template>
|
||||
|
||||
<template name="spacebars_test_ui_hooks_nested_sub">
|
||||
<div>
|
||||
{{#with true}}
|
||||
<p>hello</p>
|
||||
{{/with}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="spacebars_test_template_instance_helper">
|
||||
{{#with true}}{{foo}}{{/with}}
|
||||
</template>
|
||||
|
||||
<template name="spacebars_test_with_cleanup">
|
||||
<div class="test-with-cleanup">
|
||||
{{#with foo}}
|
||||
{{this}}
|
||||
{{/with}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="spacebars_test_template_parent_data_helper">
|
||||
{{#with "parent"}}
|
||||
{{> spacebars_test_template_parent_data_helper_child}}
|
||||
{{/with}}
|
||||
</template>
|
||||
|
||||
<template name="spacebars_test_template_parent_data_helper_child">
|
||||
{{#each a}}
|
||||
{{#with b}}
|
||||
{{#if c}}
|
||||
{{#with "d"}}
|
||||
{{foo}}
|
||||
{{/with}}
|
||||
{{/if}}
|
||||
{{/with}}
|
||||
{{/each}}
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
var renderToDiv = function (comp) {
|
||||
var div = document.createElement("DIV");
|
||||
UI.materialize(comp, div);
|
||||
return div;
|
||||
};
|
||||
|
||||
var divRendersTo = function (test, div, html) {
|
||||
Deps.flush({_throwFirstError: true});
|
||||
var actual = canonicalizeHtml(div.innerHTML);
|
||||
@@ -767,30 +761,40 @@ Tinytest.add('spacebars - templates - textarea each', function (test) {
|
||||
|
||||
// Ensure that one can call `Meteor.defer` within a rendered callback
|
||||
// triggered by a document insertion that happend in a method stub.
|
||||
//
|
||||
// Why do we have this test? Because you generally can't call
|
||||
// `Meteor.defer` inside a method stub (see
|
||||
// packages/meteor/timers.js). This test verifies that rendered
|
||||
// callbacks don't fire synchronously as part of a method stub.
|
||||
testAsyncMulti('spacebars - template - defer in rendered callbacks', [function (test, expect) {
|
||||
var tmpl = Template.spacebars_template_test_defer_in_rendered;
|
||||
var coll = new Meteor.Collection("test-defer-in-rendered--client-only");
|
||||
var coll = new Meteor.Collection(null);
|
||||
|
||||
Meteor.methods({
|
||||
spacebarsTestInsertEmptyObject: function () {
|
||||
// cause a new instance of `subtmpl` to be placed in the
|
||||
// DOM. verify that it's not fired directly within a method
|
||||
// stub, in which `Meteor.defer` is not allowed.
|
||||
coll.insert({});
|
||||
}
|
||||
});
|
||||
|
||||
tmpl.items = function () {
|
||||
return coll.find();
|
||||
};
|
||||
|
||||
var subtmpl = Template.spacebars_template_test_defer_in_rendered_subtemplate;
|
||||
|
||||
subtmpl.rendered = expect(function () {
|
||||
// will throw if called in a method stub
|
||||
Meteor.defer(function () {
|
||||
});
|
||||
Meteor.defer(function () {});
|
||||
});
|
||||
|
||||
var div = renderToDiv(tmpl);
|
||||
|
||||
// `coll` is not defined on the server so we'll get an error. We
|
||||
// can't make this a client-only collection since then we won't be
|
||||
// running in a stub and the error won't fire.
|
||||
Meteor._suppress_log(1);
|
||||
// cause a new instance of `subtmpl` to be placed in the DOM. verify
|
||||
// that it's not fired directly within a method stub, in which
|
||||
// `Meteor.defer` is not allowed.
|
||||
coll.insert({});
|
||||
// not defined on the server, but it's fine since the stub does
|
||||
// the relevant work
|
||||
Meteor.call("spacebarsTestInsertEmptyObject");
|
||||
}]);
|
||||
|
||||
testAsyncMulti('spacebars - template - rendered template is DOM in rendered callbacks', [
|
||||
@@ -955,176 +959,76 @@ Tinytest.add('spacebars - templates - constant #each argument', function (test)
|
||||
'foo bar 2');
|
||||
});
|
||||
|
||||
// extract a multi-line string from a comment within a function.
|
||||
// @param f {Function} eg function () { /* [[[...content...]]] */ }
|
||||
// @returns {String} eg "content"
|
||||
var textFromFunction = function(f) {
|
||||
var str = f.toString().match(/\[\[\[([\S\s]*)\]\]\]/m)[1];
|
||||
// remove line number comments added by linker
|
||||
str = str.replace(/[ ]*\/\/ \d+$/gm, '');
|
||||
return str;
|
||||
};
|
||||
|
||||
Tinytest.add('spacebars - templates - #markdown - basic', function (test) {
|
||||
Tinytest.addAsync('spacebars - templates - #markdown - basic', function (test, onComplete) {
|
||||
var tmpl = Template.spacebars_template_test_markdown_basic;
|
||||
tmpl.obj = {snippet: "<i>hi</i>"};
|
||||
tmpl.hi = function () {
|
||||
return this.snippet;
|
||||
};
|
||||
var div = renderToDiv(tmpl);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), canonicalizeHtml(textFromFunction(function () { /*
|
||||
[[[<p><i>hi</i>
|
||||
/each}}</p>
|
||||
|
||||
<p><b><i>hi</i></b>
|
||||
<b>/each}}</b></p>
|
||||
|
||||
<ul>
|
||||
<li><i>hi</i></li>
|
||||
<li><p>/each}}</p></li>
|
||||
<li><p><b><i>hi</i></b></p></li>
|
||||
<li><b>/each}}</b></li>
|
||||
</ul>
|
||||
|
||||
<p>some paragraph to fix showdown's four space parsing below.</p>
|
||||
|
||||
<pre><code><i>hi</i>
|
||||
/each}}
|
||||
|
||||
<b><i>hi</i></b>
|
||||
<b>/each}}</b>
|
||||
</code></pre>
|
||||
|
||||
<p>&gt</p>
|
||||
|
||||
<ul>
|
||||
<li>&gt</li>
|
||||
</ul>
|
||||
|
||||
<p><code>&gt</code></p>
|
||||
|
||||
<pre><code>&gt
|
||||
</code></pre>
|
||||
|
||||
<p>></p>
|
||||
|
||||
<ul>
|
||||
<li>></li>
|
||||
</ul>
|
||||
|
||||
<p><code>&gt;</code></p>
|
||||
|
||||
<pre><code>&gt;
|
||||
</code></pre>
|
||||
|
||||
<p><code><i>hi</i></code>
|
||||
<code>/each}}</code></p>
|
||||
|
||||
<p><code><b><i>hi</i></b></code>
|
||||
<code><b>/each}}</code></p>]]] */
|
||||
})));
|
||||
Meteor.call("getAsset", "markdown_basic.html", function (err, html) {
|
||||
test.isFalse(err);
|
||||
test.equal(canonicalizeHtml(div.innerHTML),
|
||||
canonicalizeHtml(html));
|
||||
onComplete();
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add('spacebars - templates - #markdown - if', function (test) {
|
||||
var tmpl = Template.spacebars_template_test_markdown_if;
|
||||
var R = new ReactiveVar(false);
|
||||
tmpl.cond = function () { return R.get(); };
|
||||
testAsyncMulti('spacebars - templates - #markdown - if', [
|
||||
function (test, expect) {
|
||||
var self = this;
|
||||
Meteor.call("getAsset", "markdown_if1.html", expect(function (err, html) {
|
||||
test.isFalse(err);
|
||||
self.html1 = html;
|
||||
}));
|
||||
Meteor.call("getAsset", "markdown_if2.html", expect(function (err, html) {
|
||||
test.isFalse(err);
|
||||
self.html2 = html;
|
||||
}));
|
||||
},
|
||||
|
||||
var div = renderToDiv(tmpl);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), canonicalizeHtml(textFromFunction(function () { /*
|
||||
[[[<p>false</p>
|
||||
function (test, expect) {
|
||||
var self = this;
|
||||
var tmpl = Template.spacebars_template_test_markdown_if;
|
||||
var R = new ReactiveVar(false);
|
||||
tmpl.cond = function () { return R.get(); };
|
||||
|
||||
<p><b>false</b></p>
|
||||
var div = renderToDiv(tmpl);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), canonicalizeHtml(self.html1));
|
||||
R.set(true);
|
||||
Deps.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), canonicalizeHtml(self.html2));
|
||||
}
|
||||
]);
|
||||
|
||||
<ul>
|
||||
<li><p>false</p></li>
|
||||
<li><p><b>false</b></p></li>
|
||||
</ul>
|
||||
testAsyncMulti('spacebars - templates - #markdown - each', [
|
||||
function (test, expect) {
|
||||
var self = this;
|
||||
Meteor.call("getAsset", "markdown_each1.html", expect(function (err, html) {
|
||||
test.isFalse(err);
|
||||
self.html1 = html;
|
||||
}));
|
||||
Meteor.call("getAsset", "markdown_each2.html", expect(function (err, html) {
|
||||
test.isFalse(err);
|
||||
self.html2 = html;
|
||||
}));
|
||||
},
|
||||
|
||||
<p>some paragraph to fix showdown's four space parsing below.</p>
|
||||
function (test, expect) {
|
||||
var self = this;
|
||||
var tmpl = Template.spacebars_template_test_markdown_each;
|
||||
var R = new ReactiveVar([]);
|
||||
tmpl.seq = function () { return R.get(); };
|
||||
|
||||
<pre><code>false
|
||||
var div = renderToDiv(tmpl);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), canonicalizeHtml(self.html1));
|
||||
|
||||
<b>false</b>
|
||||
</code></pre>
|
||||
|
||||
<p><code>false</code></p>
|
||||
|
||||
<p><code><b>false</b></code></p>]]] */
|
||||
})));
|
||||
R.set(true);
|
||||
Deps.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), canonicalizeHtml(textFromFunction(function () { /*
|
||||
[[[<p>true</p>
|
||||
|
||||
<p><b>true</b></p>
|
||||
|
||||
<ul>
|
||||
<li><p>true</p></li>
|
||||
<li><p><b>true</b></p></li>
|
||||
</ul>
|
||||
|
||||
<p>some paragraph to fix showdown's four space parsing below.</p>
|
||||
|
||||
<pre><code>true
|
||||
|
||||
<b>true</b>
|
||||
</code></pre>
|
||||
|
||||
<p><code>true</code></p>
|
||||
|
||||
<p><code><b>true</b></code></p>]]] */
|
||||
})));
|
||||
});
|
||||
|
||||
Tinytest.add('spacebars - templates - #markdown - each', function (test) {
|
||||
var tmpl = Template.spacebars_template_test_markdown_each;
|
||||
var R = new ReactiveVar([]);
|
||||
tmpl.seq = function () { return R.get(); };
|
||||
|
||||
var div = renderToDiv(tmpl);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), canonicalizeHtml(textFromFunction(function () { /*
|
||||
[[[<p><b></b></p>
|
||||
|
||||
<ul>
|
||||
<li></li>
|
||||
<li><b></b></li>
|
||||
</ul>
|
||||
|
||||
<p>some paragraph to fix showdown's four space parsing below.</p>
|
||||
|
||||
<pre><code><b></b>
|
||||
</code></pre>
|
||||
|
||||
<p>``</p>
|
||||
|
||||
<p><code><b></b></code></p>]]] */
|
||||
})));
|
||||
|
||||
R.set(["item"]);
|
||||
Deps.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), canonicalizeHtml(textFromFunction(function () { /*
|
||||
[[[<p>item</p>
|
||||
|
||||
<p><b>item</b></p>
|
||||
|
||||
<ul>
|
||||
<li><p>item</p></li>
|
||||
<li><p><b>item</b></p></li>
|
||||
</ul>
|
||||
|
||||
<p>some paragraph to fix showdown's four space parsing below.</p>
|
||||
|
||||
<pre><code>item
|
||||
|
||||
<b>item</b>
|
||||
</code></pre>
|
||||
|
||||
<p><code>item</code></p>
|
||||
|
||||
<p><code><b>item</b></code></p>]]] */
|
||||
})));
|
||||
});
|
||||
R.set(["item"]);
|
||||
Deps.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), canonicalizeHtml(self.html2));
|
||||
}
|
||||
]);
|
||||
|
||||
Tinytest.add('spacebars - templates - #markdown - inclusion', function (test) {
|
||||
var tmpl = Template.spacebars_template_test_markdown_inclusion;
|
||||
@@ -1440,22 +1344,15 @@ _.each(['textarea', 'text', 'password', 'submit', 'button',
|
||||
test.equal(DomUtils.getElementValue(input), "This is a fridge");
|
||||
|
||||
if (canFocus) {
|
||||
// ...unless focused
|
||||
// ...if focused, it still updates but focus isn't lost.
|
||||
focusElement(input);
|
||||
DomUtils.setElementValue(input, "something else");
|
||||
R.set({x:"frog"});
|
||||
|
||||
Deps.flush();
|
||||
test.equal(DomUtils.getElementValue(input), "This is a fridge");
|
||||
|
||||
// blurring and re-setting works
|
||||
blurElement(input);
|
||||
Deps.flush();
|
||||
test.equal(DomUtils.getElementValue(input), "This is a fridge");
|
||||
test.equal(DomUtils.getElementValue(input), "This is a frog");
|
||||
test.equal(document.activeElement, input);
|
||||
}
|
||||
R.set({x:"new frog"});
|
||||
Deps.flush();
|
||||
|
||||
test.equal(DomUtils.getElementValue(input), "This is a new frog");
|
||||
|
||||
// Setting a value (similar to user typing) should prevent value from being
|
||||
// reverted if the div is re-rendered but the rendered value (ie, R) does
|
||||
@@ -1833,6 +1730,60 @@ Tinytest.add(
|
||||
}
|
||||
);
|
||||
|
||||
// Make sure that if you bind an event on "div p", for example,
|
||||
// both the div and the p need to be in the template. jQuery's
|
||||
// `$(elem).find(...)` works this way, but the browser's
|
||||
// querySelector doesn't.
|
||||
Tinytest.add(
|
||||
"spacebars - template - event map selector scope",
|
||||
function (test) {
|
||||
var tmpl = Template.spacebars_test_event_selectors1;
|
||||
var tmpl2 = Template.spacebars_test_event_selectors2;
|
||||
var buf = [];
|
||||
tmpl2.events({
|
||||
'click div p': function (evt) { buf.push(evt.currentTarget.className); }
|
||||
});
|
||||
|
||||
var div = renderToDiv(tmpl);
|
||||
document.body.appendChild(div);
|
||||
test.equal(buf.join(), '');
|
||||
clickIt(div.querySelector('.p1'));
|
||||
test.equal(buf.join(), '');
|
||||
clickIt(div.querySelector('.p2'));
|
||||
test.equal(buf.join(), 'p2');
|
||||
document.body.removeChild(div);
|
||||
}
|
||||
);
|
||||
|
||||
if (document.addEventListener) {
|
||||
// see note about non-bubbling events in the "capuring events"
|
||||
// templating test for why we use the VIDEO tag. (It would be
|
||||
// nice to get rid of the network dependency, though.)
|
||||
// We skip this test in IE 8.
|
||||
Tinytest.add(
|
||||
"spacebars - template - event map selector scope (capturing)",
|
||||
function (test) {
|
||||
var tmpl = Template.spacebars_test_event_selectors_capturing1;
|
||||
var tmpl2 = Template.spacebars_test_event_selectors_capturing2;
|
||||
var buf = [];
|
||||
tmpl2.events({
|
||||
'play div video': function (evt) { buf.push(evt.currentTarget.className); }
|
||||
});
|
||||
|
||||
var div = renderToDiv(tmpl);
|
||||
document.body.appendChild(div);
|
||||
test.equal(buf.join(), '');
|
||||
simulateEvent(div.querySelector(".video1"),
|
||||
"play", {}, {bubbles: false});
|
||||
test.equal(buf.join(), '');
|
||||
simulateEvent(div.querySelector(".video2"),
|
||||
"play", {}, {bubbles: false});
|
||||
test.equal(buf.join(), 'video2');
|
||||
document.body.removeChild(div);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Tinytest.add("spacebars - template - tables", function (test) {
|
||||
var tmpl1 = Template.spacebars_test_tables1;
|
||||
|
||||
@@ -2011,3 +1962,297 @@ Tinytest.add(
|
||||
checkAttrs(" javascript:alert(1)", false);
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add(
|
||||
"spacebars - template - event handlers get cleaned up with template is removed",
|
||||
function (test) {
|
||||
var tmpl = Template.spacebars_test_event_handler_cleanup;
|
||||
var subtmpl = Template.spacebars_test_event_handler_cleanup_sub;
|
||||
|
||||
var rv = new ReactiveVar(true);
|
||||
tmpl.foo = function () {
|
||||
return rv.get();
|
||||
};
|
||||
|
||||
subtmpl.events({
|
||||
"click/mouseover": function () { }
|
||||
});
|
||||
|
||||
var div = renderToDiv(tmpl);
|
||||
|
||||
test.equal(div.$_uievents["click"].handlers.length, 1);
|
||||
test.equal(div.$_uievents["mouseover"].handlers.length, 1);
|
||||
|
||||
rv.set(false);
|
||||
Deps.flush();
|
||||
|
||||
test.equal(div.$_uievents["click"].handlers.length, 0);
|
||||
test.equal(div.$_uievents["mouseover"].handlers.length, 0);
|
||||
}
|
||||
);
|
||||
|
||||
// https://github.com/meteor/meteor/issues/2156
|
||||
Tinytest.add(
|
||||
"spacebars - template - each with inserts inside autorun",
|
||||
function (test) {
|
||||
var tmpl = Template.spacebars_test_each_with_autorun_insert;
|
||||
var coll = new Meteor.Collection(null);
|
||||
var rv = new ReactiveVar;
|
||||
|
||||
tmpl.items = function () {
|
||||
return coll.find();
|
||||
};
|
||||
|
||||
var div = renderToDiv(tmpl);
|
||||
|
||||
Deps.autorun(function () {
|
||||
if (rv.get()) {
|
||||
coll.insert({ name: rv.get() });
|
||||
}
|
||||
});
|
||||
|
||||
rv.set("foo1");
|
||||
Deps.flush();
|
||||
var firstId = coll.findOne()._id;
|
||||
|
||||
rv.set("foo2");
|
||||
Deps.flush();
|
||||
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "foo1 foo2");
|
||||
|
||||
coll.update(firstId, { $set: { name: "foo3" } });
|
||||
Deps.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "foo3 foo2");
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add(
|
||||
"spacebars - ui hooks",
|
||||
function (test) {
|
||||
var tmpl = Template.spacebars_test_ui_hooks;
|
||||
var rv = new ReactiveVar([]);
|
||||
tmpl.items = function () {
|
||||
return rv.get();
|
||||
};
|
||||
|
||||
var div = renderToDiv(tmpl);
|
||||
|
||||
var hooks = [];
|
||||
var container = div.querySelector(".test-ui-hooks");
|
||||
|
||||
// Before we attach the ui hooks, put two items in the DOM.
|
||||
var origVal = [{ _id: 'foo1' }, { _id: 'foo2' }];
|
||||
rv.set(origVal);
|
||||
Deps.flush();
|
||||
|
||||
container._uihooks = {
|
||||
insertElement: function (n, next) {
|
||||
hooks.push("insert");
|
||||
|
||||
// check that the element hasn't actually been added yet
|
||||
test.isTrue(n.parentNode.nodeType === 11 /*DOCUMENT_FRAGMENT_NODE*/);
|
||||
test.isFalse(n.parentNode.parentNode);
|
||||
},
|
||||
removeElement: function (n) {
|
||||
hooks.push("remove");
|
||||
// check that the element hasn't actually been removed yet
|
||||
test.isTrue(n.parentNode === container);
|
||||
},
|
||||
moveElement: function (n, next) {
|
||||
hooks.push("move");
|
||||
// check that the element hasn't actually been moved yet
|
||||
test.isFalse(n.nextNode === next);
|
||||
}
|
||||
};
|
||||
|
||||
var testDomUnchanged = function () {
|
||||
var items = div.querySelectorAll(".item");
|
||||
test.equal(items.length, 2);
|
||||
test.equal(canonicalizeHtml(items[0].innerHTML), "foo1");
|
||||
test.equal(canonicalizeHtml(items[1].innerHTML), "foo2");
|
||||
};
|
||||
|
||||
var newVal = _.clone(origVal);
|
||||
newVal.push({ _id: 'foo3' });
|
||||
rv.set(newVal);
|
||||
Deps.flush();
|
||||
test.equal(hooks, ['insert']);
|
||||
testDomUnchanged();
|
||||
|
||||
newVal.reverse();
|
||||
rv.set(newVal);
|
||||
Deps.flush();
|
||||
test.equal(hooks, ['insert', 'move']);
|
||||
testDomUnchanged();
|
||||
|
||||
newVal = [origVal[0]];
|
||||
rv.set(newVal);
|
||||
Deps.flush();
|
||||
test.equal(hooks, ['insert', 'move', 'remove']);
|
||||
testDomUnchanged();
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add(
|
||||
"spacebars - ui hooks - nested domranges",
|
||||
function (test) {
|
||||
var tmpl = Template.spacebars_test_ui_hooks_nested;
|
||||
var rv = new ReactiveVar(true);
|
||||
|
||||
tmpl.foo = function () {
|
||||
return rv.get();
|
||||
};
|
||||
|
||||
var subtmpl = Template.spacebars_test_ui_hooks_nested_sub;
|
||||
var uiHookCalled = false;
|
||||
subtmpl.rendered = function () {
|
||||
this.firstNode.parentNode._uihooks = {
|
||||
removeElement: function (node) {
|
||||
uiHookCalled = true;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
var div = renderToDiv(tmpl);
|
||||
document.body.appendChild(div);
|
||||
Deps.flush();
|
||||
|
||||
var htmlBeforeRemove = canonicalizeHtml(div.innerHTML);
|
||||
rv.set(false);
|
||||
Deps.flush();
|
||||
test.isTrue(uiHookCalled);
|
||||
var htmlAfterRemove = canonicalizeHtml(div.innerHTML);
|
||||
test.equal(htmlBeforeRemove, htmlAfterRemove);
|
||||
document.body.removeChild(div);
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add(
|
||||
"spacebars - access template instance from helper",
|
||||
function (test) {
|
||||
// Set a property on the template instance; check that it's still
|
||||
// there from a helper.
|
||||
|
||||
var tmpl = Template.spacebars_test_template_instance_helper;
|
||||
var value = Random.id();
|
||||
var instanceFromHelper;
|
||||
|
||||
tmpl.created = function () {
|
||||
this.value = value;
|
||||
};
|
||||
tmpl.foo = function () {
|
||||
instanceFromHelper = UI._templateInstance();
|
||||
};
|
||||
|
||||
var div = renderToDiv(tmpl);
|
||||
test.equal(instanceFromHelper.value, value);
|
||||
}
|
||||
);
|
||||
|
||||
// XXX This is for traversing empty text nodes and should be removed
|
||||
// on blaze-refactor.
|
||||
var getSiblingText = function (node, siblingNum) {
|
||||
var sibling = node;
|
||||
for (var i = 0; i < siblingNum; i++) {
|
||||
if (sibling)
|
||||
sibling = sibling.nextSibling;
|
||||
}
|
||||
return $(sibling).text();
|
||||
};
|
||||
|
||||
Tinytest.add(
|
||||
"spacebars - access template instance from helper, " +
|
||||
"template instance is kept up-to-date",
|
||||
function (test) {
|
||||
var tmpl = Template.spacebars_test_template_instance_helper;
|
||||
var rv = new ReactiveVar("");
|
||||
var instanceFromHelper;
|
||||
|
||||
tmpl.foo = function () {
|
||||
instanceFromHelper = UI._templateInstance();
|
||||
return rv.get();
|
||||
};
|
||||
|
||||
var div = renderToDiv(tmpl);
|
||||
rv.set("first");
|
||||
Deps.flush();
|
||||
// `nextSibling` because the first node is an empty text node.
|
||||
test.equal(getSiblingText(instanceFromHelper.firstNode, 4),
|
||||
"first");
|
||||
|
||||
rv.set("second");
|
||||
Deps.flush();
|
||||
test.equal(getSiblingText(instanceFromHelper.firstNode, 4),
|
||||
"second");
|
||||
|
||||
// UI._templateInstance() should throw when called from not within a
|
||||
// helper.
|
||||
test.throws(function () {
|
||||
UI._templateInstance();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add(
|
||||
"spacebars - {{#with}} autorun is cleaned up",
|
||||
function (test) {
|
||||
var tmpl = Template.spacebars_test_with_cleanup;
|
||||
var rv = new ReactiveVar("");
|
||||
var helperCalled = false;
|
||||
tmpl.foo = function () {
|
||||
helperCalled = true;
|
||||
return rv.get();
|
||||
};
|
||||
|
||||
var div = renderToDiv(tmpl);
|
||||
rv.set("first");
|
||||
Deps.flush();
|
||||
test.equal(helperCalled, true);
|
||||
|
||||
helperCalled = false;
|
||||
$(div).find(".test-with-cleanup").remove();
|
||||
|
||||
rv.set("second");
|
||||
Deps.flush();
|
||||
test.equal(helperCalled, false);
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add(
|
||||
"spacebars - access parent data contexts from helper",
|
||||
function (test) {
|
||||
var childTmpl = Template.spacebars_test_template_parent_data_helper_child;
|
||||
var parentTmpl = Template.spacebars_test_template_parent_data_helper;
|
||||
var rv = new ReactiveVar(0);
|
||||
|
||||
childTmpl.a = ["a"];
|
||||
childTmpl.b = new ReactiveVar("b");
|
||||
childTmpl.c = ["c"];
|
||||
|
||||
childTmpl.foo = function () {
|
||||
var data = UI._parentData(rv.get());
|
||||
return data.get === undefined ? data : data.get();
|
||||
};
|
||||
|
||||
var div = renderToDiv(parentTmpl);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "d");
|
||||
|
||||
rv.set(1);
|
||||
Deps.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "b");
|
||||
|
||||
// Test UI._parentData() reactivity
|
||||
|
||||
childTmpl.b.set("bNew");
|
||||
Deps.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "bNew");
|
||||
|
||||
rv.set(2);
|
||||
Deps.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "a");
|
||||
|
||||
rv.set(3);
|
||||
Deps.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "parent");
|
||||
}
|
||||
);
|
||||
|
||||
7
packages/spacebars-tests/template_tests_server.js
Normal file
7
packages/spacebars-tests/template_tests_server.js
Normal file
@@ -0,0 +1,7 @@
|
||||
var path = Npm.require("path");
|
||||
|
||||
Meteor.methods({
|
||||
getAsset: function (filename) {
|
||||
return Assets.getText(path.join("assets", filename));
|
||||
}
|
||||
});
|
||||
26
packages/spacebars/dynamic.html
Normal file
26
packages/spacebars/dynamic.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!-- Expects the data context to have a `template` property (the name of
|
||||
the template to render) and an optional `data` property. If the `data`
|
||||
property is not specified, then the parent data context will be used
|
||||
instead. Uses the __dynamicWithDataContext template below to actually
|
||||
render the template. -->
|
||||
<template name="__dynamic">
|
||||
{{checkContext}}
|
||||
{{#if dataContextPresent}}
|
||||
{{> __dynamicWithDataContext}}
|
||||
{{else}}
|
||||
{{! if there was no explicit 'data' argument, use the parent context}}
|
||||
{{> __dynamicWithDataContext template=template data=..}}
|
||||
{{/if}}
|
||||
</template>
|
||||
|
||||
<!-- Expects the data context to have a `template` property (the name of
|
||||
the template to render) and a `data` property, which can be falsey. -->
|
||||
<template name="__dynamicWithDataContext">
|
||||
{{#with chooseTemplate template}}
|
||||
{{#with ../data}} {{! original 'dataContext' argument to __dynamic}}
|
||||
{{> ..}} {{! return value from chooseTemplate(template) }}
|
||||
{{else}} {{! if the 'dataContext' argument was falsey }}
|
||||
{{> .. ../data}} {{! return value from chooseTemplate(template) }}
|
||||
{{/with}}
|
||||
{{/with}}
|
||||
</template>
|
||||
21
packages/spacebars/dynamic.js
Normal file
21
packages/spacebars/dynamic.js
Normal file
@@ -0,0 +1,21 @@
|
||||
Template.__dynamicWithDataContext.chooseTemplate = function (name) {
|
||||
return Template[name] || null;
|
||||
};
|
||||
|
||||
Template.__dynamic.dataContextPresent = function () {
|
||||
return _.has(this, "data");
|
||||
};
|
||||
|
||||
Template.__dynamic.checkContext = function () {
|
||||
if (! _.has(this, "template")) {
|
||||
throw new Error("Must specify name in the 'template' argument " +
|
||||
"to {{> UI.dynamic}}.");
|
||||
}
|
||||
|
||||
_.each(this, function (v, k) {
|
||||
if (k !== "template" && k !== "data") {
|
||||
throw new Error("Invalid argument to {{> UI.dynamic}}: " +
|
||||
k);
|
||||
}
|
||||
});
|
||||
};
|
||||
45
packages/spacebars/dynamic_tests.html
Normal file
45
packages/spacebars/dynamic_tests.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<template name="ui_dynamic_test">
|
||||
{{> UI.dynamic template=templateName data=templateData}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_test_no_data">
|
||||
{{> UI.dynamic template=templateName}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_test_inherited_data">
|
||||
{{#with context}}
|
||||
{{> UI.dynamic template=templateName}}
|
||||
{{else}}
|
||||
{{> UI.dynamic template=templateName}}
|
||||
{{/with}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_test_sub">
|
||||
test{{foo}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_test_falsey_inner_context">
|
||||
{{#with foo="bar"}}
|
||||
{{> UI.dynamic template=templateName data=context}}
|
||||
{{/with}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_test_bad_args0">
|
||||
{{> UI.dynamic}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_test_bad_args1">
|
||||
{{> UI.dynamic foo="bar"}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_test_bad_args2">
|
||||
{{> UI.dynamic template="ui_dynamic_test_sub" foo="bar"}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_test_falsey_context">
|
||||
{{> UI.dynamic template="ui_dynamic_test_falsey_context_sub"}}
|
||||
</template>
|
||||
|
||||
<template name="ui_dynamic_test_falsey_context_sub">
|
||||
{{foo}}
|
||||
</template>
|
||||
146
packages/spacebars/dynamic_tests.js
Normal file
146
packages/spacebars/dynamic_tests.js
Normal file
@@ -0,0 +1,146 @@
|
||||
Tinytest.add(
|
||||
"ui-dynamic-template - render template dynamically", function (test, expect) {
|
||||
var tmpl = Template.ui_dynamic_test;
|
||||
|
||||
var nameVar = new ReactiveVar;
|
||||
var dataVar = new ReactiveVar;
|
||||
tmpl.templateName = function () {
|
||||
return nameVar.get();
|
||||
};
|
||||
tmpl.templateData = function () {
|
||||
return dataVar.get();
|
||||
};
|
||||
|
||||
// No template chosen
|
||||
var div = renderToDiv(tmpl);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "");
|
||||
|
||||
// Choose the "ui-dynamic-test-sub" template, with no data context
|
||||
// passed in.
|
||||
nameVar.set("ui_dynamic_test_sub");
|
||||
Deps.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "test");
|
||||
|
||||
// Set a data context.
|
||||
dataVar.set({ foo: "bar" });
|
||||
Deps.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "testbar");
|
||||
});
|
||||
|
||||
// Same test as above, but the {{> UI.dynamic}} inclusion has no
|
||||
// `dataContext` argument.
|
||||
Tinytest.add(
|
||||
"ui-dynamic-template - render template dynamically, no data context",
|
||||
function (test, expect) {
|
||||
var tmpl = Template.ui_dynamic_test_no_data;
|
||||
|
||||
var nameVar = new ReactiveVar;
|
||||
tmpl.templateName = function () {
|
||||
return nameVar.get();
|
||||
};
|
||||
|
||||
var div = renderToDiv(tmpl);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "");
|
||||
|
||||
nameVar.set("ui_dynamic_test_sub");
|
||||
Deps.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "test");
|
||||
});
|
||||
|
||||
|
||||
Tinytest.add(
|
||||
"ui-dynamic-template - render template " +
|
||||
"dynamically, data context gets inherited",
|
||||
function (test, expect) {
|
||||
var tmpl = Template.ui_dynamic_test_inherited_data;
|
||||
|
||||
var nameVar = new ReactiveVar();
|
||||
var dataVar = new ReactiveVar();
|
||||
tmpl.templateName = function () {
|
||||
return nameVar.get();
|
||||
};
|
||||
tmpl.context = function () {
|
||||
return dataVar.get();
|
||||
};
|
||||
|
||||
var div = renderToDiv(tmpl);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "");
|
||||
|
||||
nameVar.set("ui_dynamic_test_sub");
|
||||
Deps.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "test");
|
||||
|
||||
// Set the top-level template's data context; this should be
|
||||
// inherited by the dynamically-chosen template, since the {{>
|
||||
// UI.dynamic}} inclusion didn't include a data argument.
|
||||
dataVar.set({ foo: "bar" });
|
||||
Deps.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "testbar");
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add(
|
||||
"ui-dynamic-template - render template " +
|
||||
"dynamically, data context does not get inherited if " +
|
||||
"falsey context is passed in",
|
||||
function (test, expect) {
|
||||
var tmpl = Template.ui_dynamic_test_falsey_inner_context;
|
||||
|
||||
var nameVar = new ReactiveVar();
|
||||
var dataVar = new ReactiveVar();
|
||||
tmpl.templateName = function () {
|
||||
return nameVar.get();
|
||||
};
|
||||
tmpl.context = function () {
|
||||
return dataVar.get();
|
||||
};
|
||||
|
||||
var div = renderToDiv(tmpl);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "");
|
||||
|
||||
nameVar.set("ui_dynamic_test_sub");
|
||||
Deps.flush();
|
||||
// Even though the data context is falsey, we DON'T expect the
|
||||
// subtemplate to inherit the data context from the parent template.
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "test");
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add(
|
||||
"ui-dynamic-template - render template " +
|
||||
"dynamically, bad arguments",
|
||||
function (test, expect) {
|
||||
var tmplPrefix = "ui_dynamic_test_bad_args";
|
||||
var errors = [
|
||||
"Must specify 'template' as an argument",
|
||||
"Must specify 'template' as an argument",
|
||||
"Invalid argument to {{> UI.dynamic}}"
|
||||
];
|
||||
|
||||
for (var i = 0; i < 3; i++) {
|
||||
var tmpl = Template[tmplPrefix + i];
|
||||
test.throws(function () {
|
||||
var div = renderToDiv(tmpl);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add(
|
||||
"ui-dynamic-template - render template " +
|
||||
"dynamically, falsey context",
|
||||
function (test, expect) {
|
||||
var tmpl = Template.ui_dynamic_test_falsey_context;
|
||||
var subtmpl = Template.ui_dynamic_test_falsey_context_sub;
|
||||
|
||||
var subtmplContext;
|
||||
subtmpl.foo = function () {
|
||||
subtmplContext = this;
|
||||
};
|
||||
var div = renderToDiv(tmpl);
|
||||
|
||||
// Because `this` can only be an object, Blaze normalizes falsey
|
||||
// data contexts to {}.
|
||||
test.equal(subtmplContext, {});
|
||||
}
|
||||
);
|
||||
@@ -12,9 +12,18 @@ Package.describe({
|
||||
// Additional tests are in `spacebars-tests`.
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.export('Spacebars');
|
||||
api.use('spacebars-common');
|
||||
api.imply('spacebars-common');
|
||||
|
||||
api.use('htmljs');
|
||||
api.use('ui');
|
||||
api.use('templating');
|
||||
api.add_files(['spacebars-runtime.js']);
|
||||
api.add_files(['dynamic.html', 'dynamic.js'], 'client');
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
api.use(["spacebars", "tinytest", "test-helpers"]);
|
||||
api.use("templating", "client");
|
||||
api.add_files(["dynamic_tests.html", "dynamic_tests.js"], "client");
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
Spacebars = {};
|
||||
|
||||
// * `templateOrFunction` - template (component) or function returning a template
|
||||
// or null
|
||||
Spacebars.include = function (templateOrFunction, contentBlock, elseContentBlock) {
|
||||
@@ -222,13 +220,18 @@ Spacebars.With = function (argFunc, contentBlock, elseContentBlock) {
|
||||
return UI.If(this.v, UI.With(this.v, contentBlock), elseContentBlock);
|
||||
},
|
||||
materialized: (function () {
|
||||
var f = function () {
|
||||
var f = function (range) {
|
||||
var self = this;
|
||||
if (Deps.active) {
|
||||
Deps.onInvalidate(function () {
|
||||
self.v.stop();
|
||||
});
|
||||
}
|
||||
if (range) {
|
||||
range.removed = function () {
|
||||
self.v.stop();
|
||||
};
|
||||
}
|
||||
};
|
||||
f.isWith = true;
|
||||
return f;
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
// XXX COMPAT WITH 0.8.1.3
|
||||
// This package is replaced by the use of bcrypt in accounts-password,
|
||||
// but we are leaving in some of the code to allow existing user
|
||||
// databases to be upgraded from SRP to bcrypt.
|
||||
|
||||
Package.describe({
|
||||
summary: "Library for Secure Remote Password (SRP) exchanges",
|
||||
internal: true
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use(['random', 'check'], ['client', 'server']);
|
||||
api.use(['random', 'check', 'sha'], ['client', 'server']);
|
||||
api.use('underscore');
|
||||
api.export('SRP');
|
||||
api.add_files(['biginteger.js', 'sha256.js', 'srp.js'],
|
||||
api.add_files(['biginteger.js', 'srp.js'],
|
||||
['client', 'server']);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
SRP = {};
|
||||
// This package contains just enough of the original SRP code to
|
||||
// support the backwards-compatibility upgrade path.
|
||||
//
|
||||
// An SRP (and possibly also accounts-srp) package should eventually be
|
||||
// available in Atmosphere so that users can continue to use SRP if they
|
||||
// want to.
|
||||
|
||||
/////// PUBLIC CLIENT
|
||||
SRP = {};
|
||||
|
||||
/**
|
||||
* Generate a new SRP verifier. Password is the plaintext password.
|
||||
@@ -8,6 +13,7 @@ SRP = {};
|
||||
* options is optional and can include:
|
||||
* - identity: String. The SRP username to user. Mostly this is passed
|
||||
* in for testing. Random UUID if not provided.
|
||||
* - hashedIdentityAndPassword: combined identity and password, already hashed, for the SRP to bcrypt upgrade path.
|
||||
* - salt: String. A salt to use. Mostly this is passed in for
|
||||
* testing. Random UUID if not provided.
|
||||
* - SRP parameters (see _defaults and paramsFromOptions below)
|
||||
@@ -15,14 +21,19 @@ SRP = {};
|
||||
SRP.generateVerifier = function (password, options) {
|
||||
var params = paramsFromOptions(options);
|
||||
|
||||
var identity = (options && options.identity) || Random.secret();
|
||||
var salt = (options && options.salt) || Random.secret();
|
||||
|
||||
var x = params.hash(salt + params.hash(identity + ":" + password));
|
||||
var identity;
|
||||
var hashedIdentityAndPassword = options && options.hashedIdentityAndPassword;
|
||||
if (!hashedIdentityAndPassword) {
|
||||
identity = (options && options.identity) || Random.secret();
|
||||
hashedIdentityAndPassword = params.hash(identity + ":" + password);
|
||||
}
|
||||
|
||||
var x = params.hash(salt + hashedIdentityAndPassword);
|
||||
var xi = new BigInteger(x, 16);
|
||||
var v = params.g.modPow(xi, params.N);
|
||||
|
||||
|
||||
return {
|
||||
identity: identity,
|
||||
salt: salt,
|
||||
@@ -38,249 +49,6 @@ SRP.matchVerifier = {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Generate a new SRP client object. Password is the plaintext password.
|
||||
*
|
||||
* options is optional and can include:
|
||||
* - a: client's private ephemeral value. String or
|
||||
* BigInteger. Normally, this is picked randomly, but it can be
|
||||
* passed in for testing.
|
||||
* - SRP parameters (see _defaults and paramsFromOptions below)
|
||||
*/
|
||||
SRP.Client = function (password, options) {
|
||||
var self = this;
|
||||
self.params = paramsFromOptions(options);
|
||||
self.password = password;
|
||||
|
||||
// shorthand
|
||||
var N = self.params.N;
|
||||
var g = self.params.g;
|
||||
|
||||
// construct public and private keys.
|
||||
var a, A;
|
||||
if (options && options.a) {
|
||||
if (typeof options.a === "string")
|
||||
a = new BigInteger(options.a, 16);
|
||||
else if (options.a instanceof BigInteger)
|
||||
a = options.a;
|
||||
else
|
||||
throw new Error("Invalid parameter: a");
|
||||
|
||||
A = g.modPow(a, N);
|
||||
|
||||
if (A.mod(N) === 0)
|
||||
throw new Error("Invalid parameter: a: A mod N == 0.");
|
||||
|
||||
} else {
|
||||
while (!A || A.mod(N) === 0) {
|
||||
a = randInt();
|
||||
A = g.modPow(a, N);
|
||||
}
|
||||
}
|
||||
|
||||
self.a = a;
|
||||
self.A = A;
|
||||
self.Astr = A.toString(16);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Initiate an SRP exchange.
|
||||
*
|
||||
* returns { A: 'client public ephemeral key. hex encoded integer.' }
|
||||
*/
|
||||
SRP.Client.prototype.startExchange = function () {
|
||||
var self = this;
|
||||
|
||||
return {
|
||||
A: self.Astr
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Respond to the server's challenge with a proof of password.
|
||||
*
|
||||
* challenge is an object with
|
||||
* - B: server public ephemeral key. hex encoded integer.
|
||||
* - identity: user's identity (SRP username).
|
||||
* - salt: user's salt.
|
||||
*
|
||||
* returns { M: 'client proof of password. hex encoded integer.' }
|
||||
* throws an error if it got an invalid challenge.
|
||||
*/
|
||||
SRP.Client.prototype.respondToChallenge = function (challenge) {
|
||||
var self = this;
|
||||
|
||||
// shorthand
|
||||
var N = self.params.N;
|
||||
var g = self.params.g;
|
||||
var k = self.params.k;
|
||||
var H = self.params.hash;
|
||||
|
||||
// XXX check for missing / bad parameters.
|
||||
self.identity = challenge.identity;
|
||||
self.salt = challenge.salt;
|
||||
self.Bstr = challenge.B;
|
||||
self.B = new BigInteger(self.Bstr, 16);
|
||||
|
||||
if (self.B.mod(N) === 0)
|
||||
throw new Error("Server sent invalid key: B mod N == 0.");
|
||||
|
||||
var u = new BigInteger(H(self.Astr + self.Bstr), 16);
|
||||
var x = new BigInteger(
|
||||
H(self.salt + H(self.identity + ":" + self.password)), 16);
|
||||
|
||||
var kgx = k.multiply(g.modPow(x, N));
|
||||
var aux = self.a.add(u.multiply(x));
|
||||
var S = self.B.subtract(kgx).modPow(aux, N);
|
||||
var M = H(self.Astr + self.Bstr + S.toString(16));
|
||||
var HAMK = H(self.Astr + M + S.toString(16));
|
||||
|
||||
self.S = S;
|
||||
self.HAMK = HAMK;
|
||||
|
||||
return {
|
||||
M: M
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Verify server's confirmation message.
|
||||
*
|
||||
* confirmation is an object with
|
||||
* - HAMK: server's proof of password.
|
||||
*
|
||||
* returns true or false.
|
||||
*/
|
||||
SRP.Client.prototype.verifyConfirmation = function (confirmation) {
|
||||
var self = this;
|
||||
|
||||
return (self.HAMK && (confirmation.HAMK === self.HAMK));
|
||||
};
|
||||
|
||||
|
||||
|
||||
/////// PUBLIC SERVER
|
||||
|
||||
|
||||
/**
|
||||
* Generate a new SRP server object. Password is the plaintext password.
|
||||
*
|
||||
* options is optional and can include:
|
||||
* - b: server's private ephemeral value. String or
|
||||
* BigInteger. Normally, this is picked randomly, but it can be
|
||||
* passed in for testing.
|
||||
* - SRP parameters (see _defaults and paramsFromOptions below)
|
||||
*/
|
||||
SRP.Server = function (verifier, options) {
|
||||
var self = this;
|
||||
self.params = paramsFromOptions(options);
|
||||
self.verifier = verifier;
|
||||
|
||||
// shorthand
|
||||
var N = self.params.N;
|
||||
var g = self.params.g;
|
||||
var k = self.params.k;
|
||||
var v = new BigInteger(self.verifier.verifier, 16);
|
||||
|
||||
// construct public and private keys.
|
||||
var b, B;
|
||||
if (options && options.b) {
|
||||
if (typeof options.b === "string")
|
||||
b = new BigInteger(options.b, 16);
|
||||
else if (options.b instanceof BigInteger)
|
||||
b = options.b;
|
||||
else
|
||||
throw new Error("Invalid parameter: b");
|
||||
|
||||
B = k.multiply(v).add(g.modPow(b, N)).mod(N);
|
||||
|
||||
if (B.mod(N) === 0)
|
||||
throw new Error("Invalid parameter: b: B mod N == 0.");
|
||||
|
||||
} else {
|
||||
while (!B || B.mod(N) === 0) {
|
||||
b = randInt();
|
||||
B = k.multiply(v).add(g.modPow(b, N)).mod(N);
|
||||
}
|
||||
}
|
||||
|
||||
self.b = b;
|
||||
self.B = B;
|
||||
self.Bstr = B.toString(16);
|
||||
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Issue a challenge to the client.
|
||||
*
|
||||
* Takes a request from the client containing:
|
||||
* - A: hex encoded int.
|
||||
*
|
||||
* Returns a challenge with:
|
||||
* - B: server public ephemeral key. hex encoded integer.
|
||||
* - identity: user's identity (SRP username).
|
||||
* - salt: user's salt.
|
||||
*
|
||||
* Throws an error if issued a bad request.
|
||||
*/
|
||||
SRP.Server.prototype.issueChallenge = function (request) {
|
||||
var self = this;
|
||||
|
||||
// XXX check for missing / bad parameters.
|
||||
self.Astr = request.A;
|
||||
self.A = new BigInteger(self.Astr, 16);
|
||||
|
||||
if (self.A.mod(self.params.N) === 0)
|
||||
throw new Error("Client sent invalid key: A mod N == 0.");
|
||||
|
||||
// shorthand
|
||||
var N = self.params.N;
|
||||
var H = self.params.hash;
|
||||
|
||||
// Compute M and HAMK in advance. Don't send to client yet.
|
||||
var u = new BigInteger(H(self.Astr + self.Bstr), 16);
|
||||
var v = new BigInteger(self.verifier.verifier, 16);
|
||||
var avu = self.A.multiply(v.modPow(u, N));
|
||||
self.S = avu.modPow(self.b, N);
|
||||
self.M = H(self.Astr + self.Bstr + self.S.toString(16));
|
||||
self.HAMK = H(self.Astr + self.M + self.S.toString(16));
|
||||
|
||||
return {
|
||||
identity: self.verifier.identity,
|
||||
salt: self.verifier.salt,
|
||||
B: self.Bstr
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Verify a response from the client and return confirmation.
|
||||
*
|
||||
* Takes a challenge response from the client containing:
|
||||
* - M: client proof of password. hex encoded int.
|
||||
*
|
||||
* Returns a confirmation if the client's proof is good:
|
||||
* - HAMK: server proof of password. hex encoded integer.
|
||||
* OR null if the client's proof doesn't match.
|
||||
*/
|
||||
SRP.Server.prototype.verifyResponse = function (response) {
|
||||
var self = this;
|
||||
|
||||
if (response.M !== self.M)
|
||||
return null;
|
||||
|
||||
return {
|
||||
HAMK: self.HAMK
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
/////// INTERNAL
|
||||
|
||||
/**
|
||||
* Default parameter values for SRP.
|
||||
*
|
||||
@@ -331,8 +99,3 @@ var paramsFromOptions = function (options) {
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
|
||||
var randInt = function () {
|
||||
return new BigInteger(Random.hexString(36), 16);
|
||||
};
|
||||
|
||||
@@ -1,38 +1,6 @@
|
||||
Tinytest.add("srp - good exchange", function(test) {
|
||||
var password = 'hi there!';
|
||||
var verifier = SRP.generateVerifier(password);
|
||||
|
||||
var C = new SRP.Client(password);
|
||||
var S = new SRP.Server(verifier);
|
||||
|
||||
var request = C.startExchange();
|
||||
var challenge = S.issueChallenge(request);
|
||||
var response = C.respondToChallenge(challenge);
|
||||
var confirmation = S.verifyResponse(response);
|
||||
|
||||
test.isTrue(confirmation);
|
||||
test.isTrue(C.verifyConfirmation(confirmation));
|
||||
|
||||
});
|
||||
|
||||
Tinytest.add("srp - bad exchange", function(test) {
|
||||
var verifier = SRP.generateVerifier('one password');
|
||||
|
||||
var C = new SRP.Client('another password');
|
||||
var S = new SRP.Server(verifier);
|
||||
|
||||
var request = C.startExchange();
|
||||
var challenge = S.issueChallenge(request);
|
||||
var response = C.respondToChallenge(challenge);
|
||||
var confirmation = S.verifyResponse(response);
|
||||
|
||||
test.isFalse(confirmation);
|
||||
});
|
||||
|
||||
|
||||
Tinytest.add("srp - fixed values", function(test) {
|
||||
// Test exact values during the exchange. We have to be very careful
|
||||
// about changing the SRP code, because changes could render
|
||||
// Test exact values outputted by `generateVerifier`. We have to be very
|
||||
// careful about changing the SRP code, because changes could render
|
||||
// people's existing user database unusable. This test is
|
||||
// intentionally brittle to catch change that could affect the
|
||||
// validity of user passwords.
|
||||
@@ -45,71 +13,7 @@ Tinytest.add("srp - fixed values", function(test) {
|
||||
|
||||
var verifier = SRP.generateVerifier(
|
||||
password, {identity: identity, salt: salt});
|
||||
|
||||
var C = new SRP.Client(password, {a: a});
|
||||
var S = new SRP.Server(verifier, {b: b});
|
||||
|
||||
var request = C.startExchange();
|
||||
test.equal(request.A, "8a75aa61471a92d4c3b5d53698c910af5ef013c42799876c40612d1d5e0dc41d01f669bc022fadcd8a704030483401a1b86b8670191bd9dfb1fb506dd11c688b2f08e9946756263954db2040c1df1894af7af5f839c9215bb445268439157e65e8f100469d575d5d0458e19e8bd4dd4ea2c0b30b1b3f4f39264de4ec596e0bb7");
|
||||
|
||||
var challenge = S.issueChallenge(request);
|
||||
test.equal(challenge.B, "77ab0a40ef428aa2fa2bc257c905f352c7f75fbcfdb8761393c9dc0f730bbb0270ba9f837545b410c955c3f761494b329ad23c6efdec7e63509e538c2f68a3526e072550a11dac46017718362205e0c698b5bed67d6ff475aa92c191ca169f865c81a1a577373c449b98df720c7b7ff50536f9919d781e698025fd7164932ba7");
|
||||
|
||||
var response = C.respondToChallenge(challenge);
|
||||
test.equal(response.M, "8705d31bb61497279adf44eef6c167dcb7e03aa7a42102c1ea7e73025fbd4cd9");
|
||||
|
||||
var confirmation = S.verifyResponse(response);
|
||||
test.equal(confirmation.HAMK, "07a0f200392fa9a084db7acc2021fbc174bfb36956b46835cc12506b68b27bba");
|
||||
|
||||
test.isTrue(C.verifyConfirmation(confirmation));
|
||||
});
|
||||
|
||||
|
||||
Tinytest.add("srp - options", function(test) {
|
||||
// test that all options are respected.
|
||||
//
|
||||
// Note, all test strings here should be hex, because the 'hash'
|
||||
// function needs to output numbers.
|
||||
|
||||
var baseOptions = {
|
||||
hash: function (x) { return x; },
|
||||
N: 'b',
|
||||
g: '2',
|
||||
k: '1'
|
||||
};
|
||||
var verifierOptions = _.extend({
|
||||
identity: 'a',
|
||||
salt: 'b'
|
||||
}, baseOptions);
|
||||
var clientOptions = _.extend({
|
||||
a: "2"
|
||||
}, baseOptions);
|
||||
var serverOptions = _.extend({
|
||||
b: "2"
|
||||
}, baseOptions);
|
||||
|
||||
var verifier = SRP.generateVerifier('c', verifierOptions);;
|
||||
|
||||
test.equal(verifier.identity, 'a');
|
||||
test.equal(verifier.salt, 'b');
|
||||
test.equal(verifier.verifier, '3');
|
||||
|
||||
var C = new SRP.Client('c', clientOptions);
|
||||
var S = new SRP.Server(verifier, serverOptions);
|
||||
|
||||
var request = C.startExchange();
|
||||
test.equal(request.A, '4');
|
||||
|
||||
var challenge = S.issueChallenge(request);
|
||||
test.equal(challenge.identity, 'a');
|
||||
test.equal(challenge.salt, 'b');
|
||||
test.equal(challenge.B, '7');
|
||||
|
||||
var response = C.respondToChallenge(challenge);
|
||||
test.equal(response.M, '471');
|
||||
|
||||
var confirmation = S.verifyResponse(response);
|
||||
test.isTrue(confirmation);
|
||||
test.equal(confirmation.HAMK, '44711');
|
||||
|
||||
test.equal(verifier.identity, identity);
|
||||
test.equal(verifier.salt, salt);
|
||||
test.equal(verifier.verifier, "56778b720d20b2e306f04e47180fb94335b88a6052808483acb0e85612606f9f1d8d5a3c6b85e0c7bfec7f08c07bdfbd0d40b032f517871dd8afd045b0f24e2edc05ccdc47b19f35d2eb9f7670521a38c1b358fcee63f052a1aedbb1282d3b92c7a554f8523f3379c2fbc6885be8227fbd426ad6960c3839809f8c94d80a6c51");
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
}
|
||||
},
|
||||
"stylus": {
|
||||
"version": "0.42.3",
|
||||
"version": "0.46.3",
|
||||
"dependencies": {
|
||||
"css-parse": {
|
||||
"version": "1.7.0"
|
||||
@@ -32,16 +32,24 @@
|
||||
"version": "0.3.5"
|
||||
},
|
||||
"debug": {
|
||||
"version": "0.7.4"
|
||||
"version": "1.0.1",
|
||||
"dependencies": {
|
||||
"ms": {
|
||||
"version": "0.6.2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sax": {
|
||||
"version": "0.5.8"
|
||||
},
|
||||
"glob": {
|
||||
"version": "3.2.9",
|
||||
"version": "3.2.11",
|
||||
"dependencies": {
|
||||
"inherits": {
|
||||
"version": "2.0.1"
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "0.2.14",
|
||||
"version": "0.3.0",
|
||||
"dependencies": {
|
||||
"lru-cache": {
|
||||
"version": "2.5.0"
|
||||
@@ -50,9 +58,6 @@
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ Package._transitional_registerBuildPlugin({
|
||||
sources: [
|
||||
'plugin/compile-stylus.js'
|
||||
],
|
||||
npmDependencies: { stylus: "0.42.3", nib: "1.0.2" }
|
||||
npmDependencies: { stylus: "0.46.3", nib: "1.0.2" }
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
|
||||
@@ -31,12 +31,15 @@ canonicalizeHtml = function(html) {
|
||||
attrs = attrs.replace(/\s+/g, ' ');
|
||||
// quote unquoted attribute values, as in `type=checkbox`. This
|
||||
// will do the wrong thing if there's an `=` in an attribute value.
|
||||
attrs = attrs.replace(/(\w)=([^" >/]+)/g, '$1="$2"');
|
||||
// for the purpose of splitting attributes in a string like
|
||||
// 'a="b" c="d"', assume they are separated by a single space
|
||||
// and values are double-quoted, but allow for spaces inside
|
||||
// the quotes. Split on space following quote.
|
||||
var attrList = attrs.replace(/" /g, '"\u0000').split('\u0000');
|
||||
attrs = attrs.replace(/(\w)=([^'" >/]+)/g, '$1="$2"');
|
||||
|
||||
// for the purpose of splitting attributes in a string like 'a="b"
|
||||
// c="d"', assume they are separated by a single space and values
|
||||
// are double- or single-quoted, but allow for spaces inside the
|
||||
// quotes. Split on space following quote.
|
||||
var attrList = attrs.replace(/(\w)='([^']*)' /g, "$1='$2'\u0000");
|
||||
attrList = attrList.replace(/(\w)="([^"]*)" /g, '$1="$2"\u0000');
|
||||
attrList = attrList.split("\u0000");
|
||||
// put attributes in alphabetical order
|
||||
attrList.sort();
|
||||
|
||||
@@ -59,11 +62,33 @@ canonicalizeHtml = function(html) {
|
||||
if (key === 'sizset')
|
||||
continue;
|
||||
var value = a[1];
|
||||
value = value.replace(/["'`]/g, '"');
|
||||
// this check is probably made unreachable by a regex above
|
||||
// that quotes unquoted attribute values
|
||||
if (value.charAt(0) !== '"')
|
||||
value = '"'+value+'"';
|
||||
|
||||
// make sure the attribute is doubled-quoted
|
||||
if (value.charAt(0) === '"') {
|
||||
// Do nothing
|
||||
} else {
|
||||
if (value.charAt(0) !== "'") {
|
||||
// attribute is unquoted. should be unreachable because of
|
||||
// regex above.
|
||||
value = '"' + value + '"';
|
||||
} else {
|
||||
// attribute is single-quoted. make it double-quoted.
|
||||
value = value.replace(/\"/g, """);
|
||||
}
|
||||
value = value.replace(/["'`]/g, '"');
|
||||
}
|
||||
|
||||
// Encode quotes and double quotes in the attribute.
|
||||
var attr = value.slice(1, -1);
|
||||
attr = attr.replace(/\"/g, """);
|
||||
attr = attr.replace(/\'/g, """);
|
||||
value = '"' + attr + '"';
|
||||
|
||||
// Ensure that styles do not end with a semicolon.
|
||||
if (key === 'style') {
|
||||
value = value.replace(/;\"$/, '"');
|
||||
}
|
||||
|
||||
tagContents.push(key+'='+value);
|
||||
}
|
||||
return '<'+tagContents.join(' ')+'>';
|
||||
|
||||
@@ -20,6 +20,7 @@ Package.on_use(function (api) {
|
||||
'pollUntil', 'try_all_permutations',
|
||||
'SeededRandom', 'ReactiveVar', 'clickElement', 'blurElement',
|
||||
'focusElement', 'simulateEvent', 'getStyleProperty', 'canonicalizeHtml',
|
||||
'renderToDiv',
|
||||
'withCallbackLogger', 'testAsyncMulti', 'simplePoll',
|
||||
'makeTestConnection', 'DomUtils'], {testOnly: true});
|
||||
|
||||
@@ -28,6 +29,7 @@ Package.on_use(function (api) {
|
||||
api.add_files('event_simulation.js');
|
||||
api.add_files('seeded_random.js');
|
||||
api.add_files('canonicalize_html.js');
|
||||
api.add_files('render_div.js');
|
||||
api.add_files('current_style.js');
|
||||
api.add_files('reactivevar.js');
|
||||
api.add_files('callback_logger.js');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user