Repository: 4commerce-technologies-AG/meteor Branch: release-1.3.4.1-universal Commit: ffdadacb6138 Files: 2037 Total size: 12.0 MB Directory structure: gitextract_hcijcecp/ ├── .arcconfig ├── .eslintignore ├── .github/ │ ├── ISSUE_TEMPLATE.md │ └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .mailmap ├── .reviewboardrc ├── .travis.yml ├── Contributing.md ├── History.md ├── IssueTriage.md ├── LICENSE.txt ├── LICENSES/ │ ├── Apache.txt │ ├── Artistic.txt │ ├── BSD.txt │ ├── CDDL.txt │ ├── DWTFYWT.txt │ ├── Eclipse.txt │ ├── ISC.txt │ ├── IntelHAXM.txt │ ├── MIT.txt │ ├── MongoDB.txt │ ├── Node.txt │ ├── PublicDomain.txt │ ├── andris9.txt │ ├── browserify.txt │ ├── commonmark.txt │ ├── heapdump.txt │ ├── ieee754.txt │ ├── jsdoc.txt │ ├── libuv.txt │ ├── nan.txt │ ├── through2.txt │ ├── tough-cookie.txt │ └── unorm.txt ├── README.md ├── Roadmap.md ├── circle.yml ├── examples/ │ ├── .gitignore │ ├── other/ │ │ ├── benchmark/ │ │ │ └── .meteor/ │ │ │ └── release │ │ ├── client-info/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── client-info.html │ │ │ └── client-info.js │ │ ├── controllers-demo/ │ │ │ └── .meteor/ │ │ │ └── release │ │ ├── defer-in-inactive-tab/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ └── packages │ │ │ ├── README.md │ │ │ ├── test.html │ │ │ └── test.js │ │ ├── domrange-grid/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── domrange-grid.css │ │ │ ├── domrange-grid.html │ │ │ └── domrange-grid.js │ │ ├── login-demo/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── login-demo.css │ │ │ ├── login-demo.html │ │ │ └── login-demo.js │ │ ├── parties/ │ │ │ ├── .meteor/ │ │ │ │ ├── .finished-upgraders │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ ├── platforms │ │ │ │ ├── release │ │ │ │ └── versions │ │ │ ├── client/ │ │ │ │ ├── client.js │ │ │ │ ├── parties.css │ │ │ │ └── parties.html │ │ │ ├── model.js │ │ │ └── server/ │ │ │ └── server.js │ │ ├── quiescence/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── quiescence.html │ │ │ └── quiescence.js │ │ ├── template-demo/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── client/ │ │ │ │ ├── d3.v2.js │ │ │ │ ├── template-demo.css │ │ │ │ ├── template-demo.html │ │ │ │ └── template-demo.js │ │ │ └── model.js │ │ └── wordplay/ │ │ ├── .meteor/ │ │ │ ├── .finished-upgraders │ │ │ ├── .gitignore │ │ │ ├── packages │ │ │ ├── platforms │ │ │ ├── release │ │ │ └── versions │ │ ├── TODO │ │ ├── client/ │ │ │ ├── wordplay.css │ │ │ ├── wordplay.html │ │ │ └── wordplay.js │ │ ├── model.js │ │ ├── private/ │ │ │ └── enable2k.txt │ │ └── server/ │ │ ├── game.js │ │ └── make-boggle-dict.js.noload │ └── unfinished/ │ ├── accounts-ui-viewer/ │ │ ├── .meteor/ │ │ │ ├── .gitignore │ │ │ ├── packages │ │ │ └── release │ │ ├── accounts-ui-viewer.html │ │ ├── accounts-ui-viewer.js │ │ └── accounts-ui-viewer.less │ ├── atoms/ │ │ ├── .meteor/ │ │ │ ├── .gitignore │ │ │ ├── packages │ │ │ └── release │ │ ├── atoms.css │ │ ├── atoms.html │ │ └── atoms.js │ ├── azrael/ │ │ ├── .meteor/ │ │ │ ├── .gitignore │ │ │ ├── packages │ │ │ └── release │ │ ├── client/ │ │ │ ├── azrael.css │ │ │ ├── azrael.html │ │ │ └── azrael.js │ │ └── model.js │ ├── benchmark/ │ │ ├── .meteor/ │ │ │ ├── .gitignore │ │ │ ├── packages │ │ │ └── release │ │ ├── benchmark.css │ │ ├── benchmark.html │ │ ├── benchmark.js │ │ ├── run-local.sh │ │ └── scenarios/ │ │ ├── README.md │ │ ├── default.json │ │ ├── fast.json │ │ ├── nodata.json │ │ ├── scale10.json │ │ ├── scale100.json │ │ ├── scale20.json │ │ ├── scale40.json │ │ └── scale50.json │ ├── blaze-test/ │ │ ├── .meteor/ │ │ │ ├── .gitignore │ │ │ ├── packages │ │ │ └── release │ │ └── client/ │ │ ├── blaze-test.css │ │ ├── blaze-test.html │ │ └── blaze-test.js │ ├── chat-benchmark/ │ │ ├── .meteor/ │ │ │ ├── .gitignore │ │ │ ├── packages │ │ │ └── release │ │ ├── benchmark.css │ │ ├── benchmark.html │ │ ├── benchmark.js │ │ ├── run-local.sh │ │ └── scenarios/ │ │ ├── README.md │ │ └── default.json │ ├── coffeeless/ │ │ ├── .meteor/ │ │ │ ├── .gitignore │ │ │ ├── packages │ │ │ └── release │ │ ├── client/ │ │ │ ├── coffeeless.coffee │ │ │ ├── coffeeless.html │ │ │ └── coffeeless.less │ │ └── model.coffee │ ├── controls/ │ │ ├── .meteor/ │ │ │ ├── .gitignore │ │ │ ├── packages │ │ │ └── release │ │ ├── client/ │ │ │ └── controls.js │ │ ├── controls.css │ │ └── controls.html │ ├── jsparse-docs/ │ │ ├── .meteor/ │ │ │ ├── .gitignore │ │ │ ├── packages │ │ │ └── release │ │ ├── jsparse-docs.css │ │ ├── jsparse-docs.html │ │ └── jsparse-docs.js │ ├── leaderboard-remote/ │ │ ├── .meteor/ │ │ │ ├── .gitignore │ │ │ ├── packages │ │ │ └── release │ │ └── client/ │ │ ├── leaderboard-remote.css │ │ ├── leaderboard-remote.html │ │ └── leaderboard-remote.js │ ├── movers/ │ │ ├── .meteor/ │ │ │ ├── .gitignore │ │ │ ├── packages │ │ │ └── release │ │ ├── client/ │ │ │ └── jquery-ui-sortable.js │ │ ├── movers.html │ │ ├── movers.js │ │ └── movers.less │ ├── parse-inspector/ │ │ ├── .meteor/ │ │ │ ├── .gitignore │ │ │ ├── packages │ │ │ └── release │ │ ├── parse-inspector.css │ │ ├── parse-inspector.html │ │ └── parse-inspector.js │ ├── python-ddp-client/ │ │ ├── README │ │ ├── ddp-client.py │ │ └── test_input │ ├── reorderable-list/ │ │ ├── .meteor/ │ │ │ ├── .gitignore │ │ │ ├── packages │ │ │ └── release │ │ ├── client/ │ │ │ ├── jquery-ui-sortable.js │ │ │ ├── shark.css │ │ │ ├── shark.html │ │ │ └── shark.js │ │ └── lib/ │ │ └── items.js │ ├── todos-backbone/ │ │ ├── .meteor/ │ │ │ ├── .gitignore │ │ │ ├── packages │ │ │ └── release │ │ ├── body.html │ │ ├── client/ │ │ │ └── todos.js │ │ ├── common.js │ │ └── todos.css │ └── todos-underscore/ │ ├── .meteor/ │ │ ├── .gitignore │ │ ├── packages │ │ └── release │ ├── body.html │ ├── client/ │ │ └── client.js │ ├── common.js │ ├── main.css │ ├── reset.css │ └── server/ │ └── bootstrap.js ├── meteor ├── meteor.bat ├── packages/ │ ├── .gitignore │ ├── accounts-base/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── accounts_client.js │ │ ├── accounts_common.js │ │ ├── accounts_rate_limit.js │ │ ├── accounts_reconnect_tests.js │ │ ├── accounts_server.js │ │ ├── accounts_tests.js │ │ ├── accounts_url_tests.js │ │ ├── client_main.js │ │ ├── client_tests.js │ │ ├── localstorage_token.js │ │ ├── package.js │ │ ├── server_main.js │ │ ├── server_tests.js │ │ ├── url_client.js │ │ └── url_server.js │ ├── accounts-facebook/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── facebook.js │ │ ├── facebook_login_button.css │ │ └── package.js │ ├── accounts-github/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── github.js │ │ ├── github_login_button.css │ │ └── package.js │ ├── accounts-google/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── google.js │ │ ├── google_login_button.css │ │ └── package.js │ ├── accounts-meetup/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── meetup.js │ │ ├── meetup_login_button.css │ │ └── package.js │ ├── accounts-meteor-developer/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── meteor-developer-login-button.css │ │ ├── meteor-developer.js │ │ └── package.js │ ├── accounts-oauth/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── oauth_client.js │ │ ├── oauth_common.js │ │ ├── oauth_server.js │ │ ├── oauth_tests.js │ │ └── package.js │ ├── accounts-password/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── email_templates.js │ │ ├── email_tests.js │ │ ├── email_tests_setup.js │ │ ├── package.js │ │ ├── password_client.js │ │ ├── password_server.js │ │ ├── password_tests.js │ │ └── password_tests_setup.js │ ├── accounts-twitter/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.js │ │ ├── twitter.js │ │ └── twitter_login_button.css │ ├── accounts-ui/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── login_buttons.less │ │ └── package.js │ ├── accounts-ui-unstyled/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── accounts_ui.js │ │ ├── accounts_ui_tests.js │ │ ├── login_buttons.html │ │ ├── login_buttons.import.less │ │ ├── login_buttons.js │ │ ├── login_buttons_dialogs.html │ │ ├── login_buttons_dialogs.js │ │ ├── login_buttons_dropdown.html │ │ ├── login_buttons_dropdown.js │ │ ├── login_buttons_session.js │ │ ├── login_buttons_single.html │ │ ├── login_buttons_single.js │ │ └── package.js │ ├── accounts-weibo/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.js │ │ ├── weibo.js │ │ └── weibo_login_button.css │ ├── allow-deny/ │ │ ├── README.md │ │ ├── allow-deny-tests.js │ │ ├── allow-deny.js │ │ └── package.js │ ├── appcache/ │ │ ├── .gitignore │ │ ├── QA.md │ │ ├── README.md │ │ ├── appcache-client.js │ │ ├── appcache-server.js │ │ ├── appcache_tests-client.js │ │ ├── appcache_tests-server.js │ │ └── package.js │ ├── audit-argument-checks/ │ │ ├── .gitignore │ │ ├── README.md │ │ └── package.js │ ├── autopublish/ │ │ ├── .gitignore │ │ ├── README.md │ │ └── package.js │ ├── autoupdate/ │ │ ├── .gitignore │ │ ├── QA.md │ │ ├── README.md │ │ ├── autoupdate_client.js │ │ ├── autoupdate_cordova.js │ │ ├── autoupdate_server.js │ │ └── package.js │ ├── babel-compiler/ │ │ ├── .npm/ │ │ │ └── package/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── babel-compiler.js │ │ ├── babel.js │ │ └── package.js │ ├── babel-runtime/ │ │ ├── .npm/ │ │ │ └── package/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── babel-runtime.js │ │ └── package.js │ ├── base64/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── base64.js │ │ ├── base64_test.js │ │ └── package.js │ ├── binary-heap/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── binary-heap-tests.js │ │ ├── max-heap.js │ │ ├── min-heap.js │ │ ├── min-max-heap.js │ │ └── package.js │ ├── blaze/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── attrs.js │ │ ├── backcompat.js │ │ ├── builtins.js │ │ ├── dombackend.js │ │ ├── domrange.js │ │ ├── events.js │ │ ├── exceptions.js │ │ ├── lookup.js │ │ ├── materializer.js │ │ ├── microscore.js │ │ ├── package.js │ │ ├── preamble.js │ │ ├── render_tests.js │ │ ├── template.js │ │ ├── view.js │ │ └── view_tests.js │ ├── blaze-html-templates/ │ │ ├── README.md │ │ └── package.js │ ├── blaze-tools/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.js │ │ ├── preamble.js │ │ ├── tojs.js │ │ ├── token_tests.js │ │ └── tokens.js │ ├── boilerplate-generator/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── boilerplate-generator.js │ │ ├── boilerplate_web.browser.html │ │ ├── boilerplate_web.cordova.html │ │ └── package.js │ ├── browser-policy/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── browser-policy-test.js │ │ └── package.js │ ├── browser-policy-common/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── browser-policy-common.js │ │ └── package.js │ ├── browser-policy-content/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── browser-policy-content.js │ │ └── package.js │ ├── browser-policy-framing/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── browser-policy-framing.js │ │ └── package.js │ ├── caching-compiler/ │ │ ├── .npm/ │ │ │ └── package/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── caching-compiler.js │ │ ├── multi-file-caching-compiler.js │ │ └── package.js │ ├── caching-html-compiler/ │ │ ├── README.md │ │ ├── caching-html-compiler.js │ │ └── package.js │ ├── callback-hook/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── hook.js │ │ ├── hook_tests.js │ │ └── package.js │ ├── check/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── isPlainObject.js │ │ ├── match.js │ │ ├── match_test.js │ │ └── package.js │ ├── code-prettify/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── lang-apollo.js │ │ ├── lang-clj.js │ │ ├── lang-css.js │ │ ├── lang-go.js │ │ ├── lang-hs.js │ │ ├── lang-lisp.js │ │ ├── lang-lua.js │ │ ├── lang-ml.js │ │ ├── lang-n.js │ │ ├── lang-proto.js │ │ ├── lang-scala.js │ │ ├── lang-sql.js │ │ ├── lang-tex.js │ │ ├── lang-vhdl.js │ │ ├── lang-wiki.js │ │ ├── lang-xq.js │ │ ├── lang-yaml.js │ │ ├── package.js │ │ ├── prettify.css │ │ ├── prettify.js │ │ └── styles/ │ │ ├── desert.css │ │ └── sunburst.css │ ├── coffeescript/ │ │ ├── .gitignore │ │ ├── .npm/ │ │ │ └── plugin/ │ │ │ └── compileCoffeescript/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── package.js │ │ ├── plugin/ │ │ │ └── compile-coffeescript.js │ │ └── tests/ │ │ ├── bare_test_setup.coffee │ │ ├── bare_tests.js │ │ ├── coffeescript_strict_tests.coffee │ │ ├── coffeescript_test_setup.js │ │ ├── coffeescript_tests.coffee │ │ ├── coffeescript_tests.js │ │ ├── es2015_module.js │ │ ├── litcoffeescript_tests.coffee.md │ │ └── litcoffeescript_tests.litcoffee │ ├── coffeescript-test-helper/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── exporting.coffee │ │ └── package.js │ ├── constraint-solver/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── benchmark-tests.js │ │ ├── catalog-cache-tests.js │ │ ├── catalog-cache.js │ │ ├── catalog-loader.js │ │ ├── constraint-solver-input.js │ │ ├── constraint-solver-tests.js │ │ ├── constraint-solver.js │ │ ├── datatypes-tests.js │ │ ├── datatypes.js │ │ ├── gem-test-data.js │ │ ├── input-tests.js │ │ ├── package.js │ │ ├── slow-test-data.js │ │ ├── solver.js │ │ ├── stack-overflow-bug-test-data.js │ │ ├── version-pricer-tests.js │ │ └── version-pricer.js │ ├── crosswalk/ │ │ └── package.js │ ├── ddp/ │ │ ├── .gitignore │ │ ├── DDP.md │ │ ├── README.md │ │ └── package.js │ ├── ddp-client/ │ │ ├── .npm/ │ │ │ └── package/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── client_convenience.js │ │ ├── id_map.js │ │ ├── livedata_common.js │ │ ├── livedata_connection.js │ │ ├── livedata_connection_tests.js │ │ ├── livedata_test_service.js │ │ ├── livedata_tests.js │ │ ├── namespace.js │ │ ├── package.js │ │ ├── random_stream.js │ │ ├── random_stream_tests.js │ │ ├── sockjs-0.3.4.js │ │ ├── stream_client_common.js │ │ ├── stream_client_nodejs.js │ │ ├── stream_client_sockjs.js │ │ ├── stream_client_tests.js │ │ ├── stream_tests.js │ │ └── stub_stream.js │ ├── ddp-common/ │ │ ├── heartbeat.js │ │ ├── method_invocation.js │ │ ├── namespace.js │ │ ├── package.js │ │ ├── random_stream.js │ │ └── utils.js │ ├── ddp-rate-limiter/ │ │ ├── README.md │ │ ├── ddp-rate-limiter-test-service.js │ │ ├── ddp-rate-limiter-tests-common.js │ │ ├── ddp-rate-limiter-tests.js │ │ ├── ddp-rate-limiter.js │ │ └── package.js │ ├── ddp-server/ │ │ ├── .npm/ │ │ │ └── package/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── crossbar.js │ │ ├── crossbar_tests.js │ │ ├── livedata_server.js │ │ ├── livedata_server_tests.js │ │ ├── package.js │ │ ├── server_convenience.js │ │ ├── session_view_tests.js │ │ ├── stream_server.js │ │ └── writefence.js │ ├── deprecated/ │ │ ├── README │ │ ├── amplify/ │ │ │ ├── .gitignore │ │ │ ├── amplify.js │ │ │ └── package.js │ │ ├── backbone/ │ │ │ ├── .gitignore │ │ │ ├── backbone.js │ │ │ └── package.js │ │ ├── bootstrap/ │ │ │ ├── .gitignore │ │ │ ├── css/ │ │ │ │ ├── bootstrap-override.css │ │ │ │ ├── bootstrap-responsive.css │ │ │ │ └── bootstrap.css │ │ │ ├── js/ │ │ │ │ └── bootstrap.js │ │ │ └── package.js │ │ └── d3/ │ │ ├── .gitignore │ │ ├── d3.v3.js │ │ └── package.js │ ├── deps/ │ │ ├── .gitignore │ │ ├── README.md │ │ └── package.js │ ├── diff-sequence/ │ │ ├── diff.js │ │ ├── package.js │ │ └── tests.js │ ├── disable-oplog/ │ │ ├── .gitignore │ │ ├── README.md │ │ └── package.js │ ├── ecmascript/ │ │ ├── README.md │ │ ├── bare-test-file.js │ │ ├── bare-test.js │ │ ├── ecmascript.js │ │ ├── package.js │ │ ├── plugin.js │ │ ├── runtime-tests.js │ │ └── transpilation-tests.js │ ├── ecmascript-runtime/ │ │ ├── .npm/ │ │ │ └── package/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── package.js │ │ ├── runtime-tests.js │ │ └── runtime.js │ ├── ejson/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── custom_models_for_tests.js │ │ ├── ejson.js │ │ ├── ejson_test.js │ │ ├── package.js │ │ └── stringify.js │ ├── email/ │ │ ├── .gitignore │ │ ├── .npm/ │ │ │ └── package/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── email.js │ │ ├── email_tests.js │ │ └── package.js │ ├── es5-shim/ │ │ ├── .npm/ │ │ │ └── package/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── client.js │ │ ├── console.js │ │ ├── export_globals.js │ │ ├── import_globals.js │ │ ├── package.js │ │ └── server.js │ ├── facebook/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── facebook_client.js │ │ ├── facebook_configure.html │ │ ├── facebook_configure.js │ │ ├── facebook_server.js │ │ └── package.js │ ├── facts/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── facts.html │ │ ├── facts.js │ │ └── package.js │ ├── fastclick/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── fastclick.js │ │ ├── package.js │ │ ├── post.js │ │ └── pre.js │ ├── force-ssl/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── force_ssl_common.js │ │ ├── force_ssl_server.js │ │ └── package.js │ ├── geojson-utils/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── geojson-utils.js │ │ ├── geojson-utils.tests.js │ │ ├── main.js │ │ └── package.js │ ├── github/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── github_client.js │ │ ├── github_configure.html │ │ ├── github_configure.js │ │ ├── github_server.js │ │ └── package.js │ ├── google/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── google_client.js │ │ ├── google_configure.html │ │ ├── google_configure.js │ │ ├── google_server.js │ │ └── package.js │ ├── handlebars/ │ │ ├── .gitignore │ │ ├── README.md │ │ └── package.js │ ├── hot-code-push/ │ │ ├── README.md │ │ ├── hot-code-push-tests.js │ │ ├── hot-code-push.js │ │ └── package.js │ ├── html-tools/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── charref.js │ │ ├── charref_tests.js │ │ ├── package.js │ │ ├── parse.js │ │ ├── parse_tests.js │ │ ├── scanner.js │ │ ├── templatetag.js │ │ ├── tokenize.js │ │ ├── tokenize_tests.js │ │ └── utils.js │ ├── htmljs/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── html.js │ │ ├── htmljs_test.js │ │ ├── package.js │ │ ├── preamble.js │ │ └── visitors.js │ ├── http/ │ │ ├── .gitignore │ │ ├── .npm/ │ │ │ └── package/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── deprecated.js │ │ ├── httpcall_client.js │ │ ├── httpcall_common.js │ │ ├── httpcall_server.js │ │ ├── httpcall_tests.js │ │ ├── package.js │ │ ├── test_responder.js │ │ └── test_static.serveme │ ├── id-map/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── id-map.js │ │ └── package.js │ ├── insecure/ │ │ ├── .gitignore │ │ ├── README.md │ │ └── package.js │ ├── jquery/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── jquery.js │ │ ├── main.js │ │ └── package.js │ ├── jquery-waypoints/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.js │ │ └── waypoints.coffee │ ├── jshint/ │ │ ├── .npm/ │ │ │ └── plugin/ │ │ │ └── lintJshint/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── package.js │ │ └── plugin/ │ │ └── lint-jshint.js │ ├── jsparse/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── lexer.js │ │ ├── package.js │ │ ├── parser.js │ │ ├── parser_tests.js │ │ ├── parserlib.js │ │ └── stringify.js │ ├── launch-screen/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── default-behavior.js │ │ ├── mobile-launch-screen.js │ │ └── package.js │ ├── less/ │ │ ├── .gitignore │ │ ├── .npm/ │ │ │ └── plugin/ │ │ │ └── compileLessBatch/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── less_tests.js │ │ ├── package.js │ │ ├── plugin/ │ │ │ └── compile-less.js │ │ └── tests/ │ │ ├── dir/ │ │ │ ├── in-dir.import.less │ │ │ ├── in-dir2.import.less │ │ │ ├── root.less │ │ │ └── subdir/ │ │ │ └── in-subdir.import.less │ │ ├── imports/ │ │ │ └── not-included.less │ │ ├── top.import.less │ │ ├── top2.less │ │ └── top3.import.less │ ├── livedata/ │ │ ├── .gitignore │ │ ├── DDP.md │ │ ├── README.md │ │ └── package.js │ ├── localstorage/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── localstorage.js │ │ ├── localstorage_tests.js │ │ └── package.js │ ├── logging/ │ │ ├── .gitignore │ │ ├── .npm/ │ │ │ └── package/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── logging.js │ │ ├── logging_cordova.js │ │ ├── logging_test.js │ │ └── package.js │ ├── logic-solver/ │ │ ├── README.md │ │ ├── logic.js │ │ ├── logic_tests.js │ │ ├── minisat.js │ │ ├── minisat_wrapper.js │ │ ├── optimize.js │ │ ├── package.js │ │ └── types.js │ ├── markdown/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── license.txt │ │ ├── package.js │ │ ├── showdown.js │ │ └── template-integration.js │ ├── meetup/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── meetup_client.js │ │ ├── meetup_configure.html │ │ ├── meetup_configure.js │ │ ├── meetup_server.js │ │ └── package.js │ ├── meteor/ │ │ ├── .gitignore │ │ ├── .npm/ │ │ │ └── package/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── bare_test_setup.js │ │ ├── bare_tests.js │ │ ├── browser_environment_test.js │ │ ├── client_environment.js │ │ ├── client_environment_test.js │ │ ├── cordova_environment.js │ │ ├── cordova_environment_test.js │ │ ├── debug.js │ │ ├── debug_test.js │ │ ├── dynamics_browser.js │ │ ├── dynamics_nodejs.js │ │ ├── dynamics_test.js │ │ ├── errors.js │ │ ├── fiber_helpers.js │ │ ├── fiber_helpers_test.js │ │ ├── fiber_stubs_client.js │ │ ├── flush-buffers-on-exit-in-windows.js │ │ ├── global.js │ │ ├── helpers.js │ │ ├── helpers_test.js │ │ ├── package.js │ │ ├── plugin/ │ │ │ └── basic-file-types.js │ │ ├── server_environment.js │ │ ├── server_environment_test.js │ │ ├── setimmediate.js │ │ ├── startup_client.js │ │ ├── startup_server.js │ │ ├── string_utils.js │ │ ├── test_environment.js │ │ ├── timers.js │ │ ├── timers_tests.js │ │ ├── url_common.js │ │ ├── url_server.js │ │ ├── url_tests.js │ │ └── wrapasync_test.js │ ├── meteor-base/ │ │ ├── README.md │ │ └── package.js │ ├── meteor-developer/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── meteor_developer_client.js │ │ ├── meteor_developer_common.js │ │ ├── meteor_developer_configure.html │ │ ├── meteor_developer_configure.js │ │ ├── meteor_developer_server.js │ │ └── package.js │ ├── meteor-platform/ │ │ ├── .gitignore │ │ ├── README.md │ │ └── package.js │ ├── meteor-tool/ │ │ ├── .gitignore │ │ ├── README.md │ │ └── package.js │ ├── meyerweb-reset/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.js │ │ └── reset.css │ ├── minifier-css/ │ │ ├── .gitignore │ │ ├── .npm/ │ │ │ └── package/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── minification.js │ │ ├── minifier-tests.js │ │ ├── minifier.js │ │ ├── package.js │ │ └── urlrewriting-tests.js │ ├── minifier-js/ │ │ ├── .gitignore │ │ ├── .npm/ │ │ │ └── package/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── beautify-tests.js │ │ ├── minifier.js │ │ └── package.js │ ├── minimongo/ │ │ ├── .gitignore │ │ ├── NOTES │ │ ├── README.md │ │ ├── diff.js │ │ ├── helpers.js │ │ ├── id_map.js │ │ ├── minimongo.js │ │ ├── minimongo_server_tests.js │ │ ├── minimongo_tests.js │ │ ├── modify.js │ │ ├── objectid.js │ │ ├── observe.js │ │ ├── package.js │ │ ├── projection.js │ │ ├── selector.js │ │ ├── selector_modifier.js │ │ ├── selector_projection.js │ │ ├── sort.js │ │ ├── sorter_projection.js │ │ ├── wrap_transform.js │ │ └── wrap_transform_tests.js │ ├── mobile-experience/ │ │ ├── README.md │ │ └── package.js │ ├── mobile-status-bar/ │ │ ├── .gitignore │ │ ├── README.md │ │ └── package.js │ ├── modules/ │ │ ├── .npm/ │ │ │ └── package/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── buffer.js │ │ ├── client.js │ │ ├── css.js │ │ ├── install-packages.js │ │ ├── package.js │ │ ├── process.js │ │ ├── server.js │ │ └── stubs.js │ ├── modules-runtime/ │ │ ├── .npm/ │ │ │ └── package/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── modules-runtime-tests.js │ │ ├── modules-runtime.js │ │ └── package.js │ ├── mongo/ │ │ ├── .gitignore │ │ ├── .npm/ │ │ │ └── package/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── allow_tests.js │ │ ├── collection.js │ │ ├── collection_tests.js │ │ ├── doc_fetcher.js │ │ ├── doc_fetcher_tests.js │ │ ├── local_collection_driver.js │ │ ├── mongo_driver.js │ │ ├── mongo_livedata_tests.js │ │ ├── observe_changes_tests.js │ │ ├── observe_multiplex.js │ │ ├── oplog_observe_driver.js │ │ ├── oplog_tailing.js │ │ ├── oplog_tests.js │ │ ├── package.js │ │ ├── polling_observe_driver.js │ │ └── remote_collection_driver.js │ ├── mongo-id/ │ │ ├── id.js │ │ └── package.js │ ├── mongo-livedata/ │ │ ├── .gitignore │ │ ├── README.md │ │ └── package.js │ ├── non-core/ │ │ ├── README │ │ ├── jquery-history/ │ │ │ ├── .gitignore │ │ │ ├── history.adapter.jquery.js │ │ │ ├── history.html4.js │ │ │ ├── history.js │ │ │ └── package.js │ │ ├── jquery-layout/ │ │ │ ├── .gitignore │ │ │ ├── jquery.layout.js │ │ │ └── package.js │ │ ├── npm-bcrypt/ │ │ │ ├── .gitignore │ │ │ ├── .npm/ │ │ │ │ └── package/ │ │ │ │ ├── .gitignore │ │ │ │ ├── README │ │ │ │ └── npm-shrinkwrap.json │ │ │ ├── .versions │ │ │ ├── package.js │ │ │ └── wrapper.js │ │ └── npm-node-aes-gcm/ │ │ ├── .gitignore │ │ ├── .npm/ │ │ │ └── package/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── .versions │ │ ├── README.md │ │ ├── package.js │ │ └── wrapper.js │ ├── npm-mongo/ │ │ ├── .npm/ │ │ │ └── package/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── package.js │ │ └── wrapper.js │ ├── oauth/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── deprecated.js │ │ ├── end_of_popup_response.html │ │ ├── end_of_popup_response.js │ │ ├── end_of_redirect_response.html │ │ ├── end_of_redirect_response.js │ │ ├── oauth_browser.js │ │ ├── oauth_client.js │ │ ├── oauth_common.js │ │ ├── oauth_cordova.js │ │ ├── oauth_server.js │ │ ├── oauth_tests.js │ │ ├── package.js │ │ └── pending_credentials.js │ ├── oauth-encryption/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── encrypt.js │ │ ├── encrypt_tests.js │ │ └── package.js │ ├── oauth1/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── oauth1_binding.js │ │ ├── oauth1_pending_request_tokens.js │ │ ├── oauth1_server.js │ │ ├── oauth1_tests.js │ │ └── package.js │ ├── oauth2/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── oauth2_server.js │ │ ├── oauth2_tests.js │ │ └── package.js │ ├── observe-sequence/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── observe_sequence.js │ │ ├── observe_sequence_tests.js │ │ └── package.js │ ├── ordered-dict/ │ │ ├── .gitignore │ │ ├── ordered_dict.js │ │ └── package.js │ ├── package-stats-opt-out/ │ │ ├── .gitignore │ │ ├── README.md │ │ └── package.js │ ├── package-version-parser/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package-version-parser-tests.js │ │ ├── package-version-parser.js │ │ ├── package.js │ │ └── semver410.js │ ├── preserve-inputs/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── deprecated.js │ │ └── package.js │ ├── promise/ │ │ ├── .npm/ │ │ │ └── package/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── client.js │ │ ├── common.js │ │ ├── package.js │ │ ├── promise-tests.js │ │ └── server.js │ ├── random/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── deprecated.js │ │ ├── package.js │ │ ├── random.js │ │ └── random_tests.js │ ├── rate-limit/ │ │ ├── README.md │ │ ├── package.js │ │ ├── rate-limit-tests.js │ │ └── rate-limit.js │ ├── reactive-dict/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── migration.js │ │ ├── package.js │ │ ├── reactive-dict-tests.js │ │ └── reactive-dict.js │ ├── reactive-var/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.js │ │ └── reactive-var.js │ ├── reload/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── deprecated.js │ │ ├── package.js │ │ ├── reload.js │ │ └── reload_tests.js │ ├── reload-safetybelt/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.js │ │ ├── reload-safety-belt-tests.js │ │ ├── reload-safety-belt.js │ │ └── safetybelt.js │ ├── retry/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.js │ │ └── retry.js │ ├── routepolicy/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.js │ │ ├── routepolicy.js │ │ └── routepolicy_tests.js │ ├── service-configuration/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.js │ │ ├── service_configuration_common.js │ │ └── service_configuration_server.js │ ├── session/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.js │ │ ├── session.js │ │ └── session_tests.js │ ├── sha/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.js │ │ └── sha256.js │ ├── showdown/ │ │ ├── .gitignore │ │ ├── README.md │ │ └── package.js │ ├── spacebars/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.js │ │ └── spacebars-runtime.js │ ├── spacebars-compiler/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── codegen.js │ │ ├── compile_tests.js │ │ ├── compiler.js │ │ ├── compiler_output_tests.coffee │ │ ├── optimizer.js │ │ ├── package.js │ │ ├── react.js │ │ ├── spacebars_tests.js │ │ └── templatetag.js │ ├── spacebars-tests/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── assets/ │ │ │ ├── markdown_basic.html │ │ │ ├── markdown_each1.html │ │ │ ├── markdown_each2.html │ │ │ ├── markdown_if1.html │ │ │ └── markdown_if2.html │ │ ├── old_templates.js │ │ ├── old_templates_tests.js │ │ ├── package.js │ │ ├── template_tests.html │ │ ├── template_tests.js │ │ ├── template_tests_server.js │ │ ├── templating_tests.html │ │ └── templating_tests.js │ ├── spiderable/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.js │ │ ├── phantom_script.js │ │ ├── spiderable.html │ │ ├── spiderable.js │ │ ├── spiderable_client.js │ │ ├── spiderable_client_tests.js │ │ ├── spiderable_server.js │ │ └── spiderable_server_tests.js │ ├── srp/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── biginteger.js │ │ ├── package.js │ │ ├── srp.js │ │ └── srp_tests.js │ ├── standard-app-packages/ │ │ ├── .gitignore │ │ ├── README.md │ │ └── package.js │ ├── standard-minifier-css/ │ │ ├── .npm/ │ │ │ └── plugin/ │ │ │ ├── minifyStd/ │ │ │ │ ├── .gitignore │ │ │ │ ├── README │ │ │ │ └── npm-shrinkwrap.json │ │ │ └── minifyStdCSS/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── package.js │ │ └── plugin/ │ │ └── minify-css.js │ ├── standard-minifier-js/ │ │ ├── .npm/ │ │ │ └── plugin/ │ │ │ └── minifyStd/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── package.js │ │ └── plugin/ │ │ └── minify-js.js │ ├── standard-minifiers/ │ │ ├── README.md │ │ └── package.js │ ├── startup/ │ │ ├── .gitignore │ │ ├── README.md │ │ └── package.js │ ├── static-html/ │ │ ├── README.md │ │ ├── package.js │ │ └── static-html.js │ ├── stylus/ │ │ ├── .gitignore │ │ ├── .npm/ │ │ │ └── plugin/ │ │ │ ├── compileStylus/ │ │ │ │ ├── .gitignore │ │ │ │ ├── README │ │ │ │ └── npm-shrinkwrap.json │ │ │ └── compileStylusBatch/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── package.js │ │ ├── plugin/ │ │ │ └── compile-stylus.js │ │ ├── stylus_tests.html │ │ ├── stylus_tests.import.styl │ │ ├── stylus_tests.js │ │ └── stylus_tests.styl │ ├── templating/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── dynamic.html │ │ ├── dynamic.js │ │ ├── dynamic_tests.html │ │ ├── dynamic_tests.js │ │ ├── package.js │ │ ├── plugin/ │ │ │ └── compile-templates.js │ │ └── templating.js │ ├── templating-tools/ │ │ ├── README.md │ │ ├── code-generation.js │ │ ├── compile-tags-with-spacebars.js │ │ ├── html-scanner-tests.js │ │ ├── html-scanner.js │ │ ├── package.js │ │ ├── templating-tools.js │ │ └── throw-compile-error.js │ ├── test-helpers/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── async_multi.js │ │ ├── callback_logger.js │ │ ├── canonicalize_html.js │ │ ├── connection.js │ │ ├── current_style.js │ │ ├── domutils.js │ │ ├── event_simulation.js │ │ ├── package.js │ │ ├── render_div.js │ │ ├── seeded_random.js │ │ ├── seeded_random_test.js │ │ ├── try_all_permutations.js │ │ └── try_all_permutations_test.js │ ├── test-in-browser/ │ │ ├── .gitignore │ │ ├── autoupdate.js │ │ ├── diff_match_patch_uncompressed.js │ │ ├── driver.css │ │ ├── driver.html │ │ ├── driver.js │ │ └── package.js │ ├── test-in-console/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── driver.js │ │ ├── package.js │ │ ├── reporter.js │ │ ├── run.sh │ │ └── runner.js │ ├── test-server-tests-in-console-once/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.js │ │ └── server.js │ ├── tinytest/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── model.js │ │ ├── package.js │ │ ├── tinytest.js │ │ ├── tinytest_client.js │ │ └── tinytest_server.js │ ├── tinytest-harness/ │ │ ├── README.md │ │ └── package.js │ ├── tracker/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── deprecated.js │ │ ├── package.js │ │ ├── tracker.js │ │ └── tracker_tests.js │ ├── twitter/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.js │ │ ├── twitter_client.js │ │ ├── twitter_configure.html │ │ ├── twitter_configure.js │ │ └── twitter_server.js │ ├── ui/ │ │ ├── .gitignore │ │ ├── README.md │ │ └── package.js │ ├── underscore/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.js │ │ ├── post.js │ │ ├── pre.js │ │ └── underscore.js │ ├── underscore-tests/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── each_test.js │ │ └── package.js │ ├── url/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.js │ │ ├── url_client.js │ │ ├── url_common.js │ │ └── url_server.js │ ├── webapp/ │ │ ├── .gitignore │ │ ├── .npm/ │ │ │ └── package/ │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── README.md │ │ ├── package.js │ │ ├── webapp_client.js │ │ ├── webapp_client_tests.js │ │ ├── webapp_cordova.js │ │ ├── webapp_server.js │ │ └── webapp_tests.js │ ├── webapp-hashing/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.js │ │ └── webapp-hashing.js │ ├── weibo/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.js │ │ ├── weibo_client.js │ │ ├── weibo_configure.html │ │ ├── weibo_configure.js │ │ └── weibo_server.js │ └── xmlbuilder/ │ ├── .gitignore │ ├── .npm/ │ │ └── package/ │ │ ├── .gitignore │ │ ├── README │ │ └── npm-shrinkwrap.json │ ├── README.md │ ├── package.js │ └── xmlbuilder.js ├── scripts/ │ ├── admin/ │ │ ├── banners-oldcore.json │ │ ├── banners.json │ │ ├── bump-all-version-numbers.js │ │ ├── check-package-dependencies.rb │ │ ├── copy-bootstrap-tarballs-from-jenkins.sh │ │ ├── copy-dev-bundle-from-jenkins.sh │ │ ├── copy-windows-installer-from-jenkins.sh │ │ ├── eslint/ │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── eslint.sh │ │ │ └── package.json │ │ ├── find-author-github.sh │ │ ├── find-new-npm-versions.sh │ │ ├── git-hooks/ │ │ │ └── pre-commit │ │ ├── install-from-bootstrap.sh │ │ ├── jsdoc/ │ │ │ ├── .gitignore │ │ │ ├── docdata-jsdoc-template/ │ │ │ │ └── publish.js │ │ │ ├── jsdoc-conf.json │ │ │ ├── jsdoc.sh │ │ │ └── package.json │ │ ├── launch-meteor │ │ ├── manifest.json │ │ ├── meteor-release-experimental.json │ │ ├── meteor-release-official.json │ │ ├── old-banner.txt │ │ ├── publish-meteor-tool-on-all-platforms.sh │ │ ├── publish-meteor-tool-on-arch.sh │ │ ├── publish-meteor-tool.bat │ │ └── test-packages-with-mongo-versions.rb │ ├── benchmarks/ │ │ └── initial-start-time.sh │ ├── build-dev-bundle-common.sh │ ├── build-mongo-for-dev-bundle.sh │ ├── build-node-for-dev-bundle.sh │ ├── ci.sh │ ├── dev-bundle-server-package.js │ ├── dev-bundle-tool-package.js │ ├── doctool.js │ ├── doctool.md │ ├── doctool.md.md │ ├── generate-dev-bundle.ps1 │ ├── generate-dev-bundle.sh │ ├── node.sh │ ├── npm.cmd │ └── windows/ │ ├── .gitignore │ ├── README │ ├── build-installer.ps1 │ ├── check-dev-bundle.ps1 │ ├── download-dev-bundle.ps1 │ └── installer/ │ ├── .gitignore │ ├── MeteorSetup.sln │ ├── WiXBalExtension/ │ │ ├── .gitignore │ │ ├── BalExtensionExt.sln │ │ ├── LICENSE.TXT │ │ ├── build.bat │ │ ├── inc/ │ │ │ ├── .gitignore │ │ │ ├── Version.proj │ │ │ ├── WixDistribution.cs │ │ │ ├── WixDistribution.h │ │ │ └── wix.rc │ │ ├── wixext/ │ │ │ ├── BalCompiler.cs │ │ │ ├── BalExtension.cs │ │ │ ├── BalPreprocessorExtension.cs │ │ │ ├── Properties/ │ │ │ │ └── AssemblyInfo.cs │ │ │ ├── WixBalExtensionExt.csproj │ │ │ ├── Xsd/ │ │ │ │ └── bal.xsd │ │ │ └── data/ │ │ │ ├── messages.xml │ │ │ └── tables.xml │ │ ├── wixlib/ │ │ │ ├── BalExtension.wixproj │ │ │ ├── BalExtension.wxs │ │ │ ├── wixstdba.wxs │ │ │ ├── wixstdba_platform.wxi │ │ │ └── wixstdba_x86.wxs │ │ └── wixstdba/ │ │ ├── JSON.cpp │ │ ├── JSON.h │ │ ├── JSONValue.cpp │ │ ├── JSONValue.h │ │ ├── Resources/ │ │ │ ├── Hyperlink2Theme.xml │ │ │ ├── HyperlinkTheme.wxl │ │ │ ├── HyperlinkTheme.xml │ │ │ ├── LoremIpsumLicense.rtf │ │ │ ├── RtfTheme.wxl │ │ │ └── RtfTheme.xml │ │ ├── WixStandardBootstrapperApplication.cpp │ │ ├── precomp.h │ │ ├── resource.h │ │ ├── wixstdba.cpp │ │ ├── wixstdba.def │ │ ├── wixstdba.rc │ │ ├── wixstdba.vcxproj │ │ └── wixstdba.vcxproj.filters │ ├── WiXHelper/ │ │ ├── .gitignore │ │ ├── CustomAction.cpp │ │ ├── CustomAction.def │ │ ├── WiXHelper.vcxproj │ │ ├── WiXHelper.vcxproj.filters │ │ ├── stdafx.cpp │ │ ├── stdafx.h │ │ └── targetver.h │ ├── WiXInstaller/ │ │ ├── .gitignore │ │ ├── Configuration.wxi │ │ ├── MSIPackage.wixproj │ │ ├── Meteor_Bundle.wxs │ │ ├── Meteor_Product.wxs │ │ ├── Resources/ │ │ │ ├── License.htm │ │ │ ├── License.rtf │ │ │ ├── Theme_Meteor.wxl │ │ │ ├── Theme_Meteor.xml │ │ │ └── dummy.file │ │ └── SetupPackage.wixproj │ ├── WiXSDK/ │ │ ├── inc/ │ │ │ ├── BalBaseBootstrapperApplication.h │ │ │ ├── IBootstrapperApplication.h │ │ │ ├── IBootstrapperBAFunction.h │ │ │ ├── IBootstrapperEngine.h │ │ │ ├── aclutil.h │ │ │ ├── apuputil.h │ │ │ ├── atomutil.h │ │ │ ├── balcondition.h │ │ │ ├── balinfo.h │ │ │ ├── balretry.h │ │ │ ├── balutil.h │ │ │ ├── buffutil.h │ │ │ ├── cabcutil.h │ │ │ ├── cabutil.h │ │ │ ├── certutil.h │ │ │ ├── conutil.h │ │ │ ├── cryputil.h │ │ │ ├── custommsierrors.h │ │ │ ├── dictutil.h │ │ │ ├── dirutil.h │ │ │ ├── dutil.h │ │ │ ├── eseutil.h │ │ │ ├── fileutil.h │ │ │ ├── gdiputil.h │ │ │ ├── iis7util.h │ │ │ ├── inetutil.h │ │ │ ├── iniutil.h │ │ │ ├── jsonutil.h │ │ │ ├── locutil.h │ │ │ ├── logutil.h │ │ │ ├── memutil.h │ │ │ ├── metautil.h │ │ │ ├── osutil.h │ │ │ ├── pathutil.h │ │ │ ├── perfutil.h │ │ │ ├── polcutil.h │ │ │ ├── procutil.h │ │ │ ├── regutil.h │ │ │ ├── resrutil.h │ │ │ ├── reswutil.h │ │ │ ├── rexutil.h │ │ │ ├── rmutil.h │ │ │ ├── rssutil.h │ │ │ ├── sceutil.h │ │ │ ├── shelutil.h │ │ │ ├── sqlutil.h │ │ │ ├── srputil.h │ │ │ ├── strutil.h │ │ │ ├── svcutil.h │ │ │ ├── thmutil.h │ │ │ ├── timeutil.h │ │ │ ├── uriutil.h │ │ │ ├── userutil.h │ │ │ ├── wcalog.h │ │ │ ├── wcautil.h │ │ │ ├── wcawow64.h │ │ │ ├── wcawrapquery.h │ │ │ ├── wiutil.h │ │ │ ├── wuautil.h │ │ │ └── xmlutil.h │ │ ├── vs2010/ │ │ │ └── lib/ │ │ │ └── x86/ │ │ │ ├── balutil.lib │ │ │ ├── deputil.lib │ │ │ ├── dutil.lib │ │ │ └── wcautil.lib │ │ ├── vs2012/ │ │ │ └── lib/ │ │ │ └── x86/ │ │ │ ├── balutil.lib │ │ │ ├── deputil.lib │ │ │ ├── dutil.lib │ │ │ └── wcautil.lib │ │ └── vs2013/ │ │ └── lib/ │ │ └── x86/ │ │ ├── balutil.lib │ │ ├── deputil.lib │ │ ├── dutil.lib │ │ └── wcautil.lib │ └── build.bat └── tools/ ├── PERFORMANCE.md ├── README.md ├── cli/ │ ├── README.md │ ├── commands-aliases.js │ ├── commands-cordova.js │ ├── commands-packages-query.js │ ├── commands-packages.js │ ├── commands.js │ ├── dev-bundle-bin-commands.js │ ├── dev-bundle-bin-helpers.js │ ├── example-repositories.js │ ├── flush-buffers-on-exit-in-windows.js │ ├── help.txt │ └── main.js ├── console/ │ ├── README.md │ ├── console.js │ └── progress.js ├── cordova/ │ ├── README.md │ ├── builder.js │ ├── index.js │ ├── project.js │ ├── protect-string-proto.js │ ├── run-targets.js │ └── runner.js ├── fs/ │ ├── README.md │ ├── files.js │ ├── mini-files.js │ ├── safe-pathwatcher.js │ └── watch.js ├── index.js ├── inspector.js ├── isobuild/ │ ├── README.md │ ├── build-plugin.js │ ├── builder.js │ ├── bundler.js │ ├── compiler-deprecated-compile-step.js │ ├── compiler-plugin.js │ ├── compiler.js │ ├── css-modules.js │ ├── import-scanner.js │ ├── isopack-cache.js │ ├── isopack.js │ ├── js-analyze.js │ ├── linker.js │ ├── linter-plugin.js │ ├── meteor-npm-userconfig │ ├── meteor-npm.js │ ├── minifier-plugin.js │ ├── npm-discards.js │ ├── package-api.js │ ├── package-source.js │ ├── resolver.js │ ├── source-arch.js │ └── test-files.js ├── meteor-services/ │ ├── README.md │ ├── auth-client.js │ ├── auth.js │ ├── config.js │ ├── deploy.js │ ├── service-connection.js │ └── stats.js ├── packaging/ │ ├── README.md │ ├── catalog/ │ │ ├── catalog-local.js │ │ ├── catalog-remote.js │ │ ├── catalog-utils.js │ │ └── catalog.js │ ├── package-client.js │ ├── package-map.js │ ├── package-version-parser.js │ ├── release.js │ ├── tropohouse.js │ ├── updater.js │ └── warehouse.js ├── project-context.js ├── runners/ │ ├── README.md │ ├── run-all.js │ ├── run-app.js │ ├── run-log.js │ ├── run-mongo.js │ ├── run-proxy.js │ ├── run-selenium.js │ └── run-updater.js ├── shell-client.js ├── static-assets/ │ ├── README.md │ ├── server/ │ │ ├── boot-utils.js │ │ ├── boot.js │ │ ├── mini-files.js │ │ ├── npm-rebuild.js │ │ ├── npm-require.js │ │ ├── server-json.js │ │ └── shell-server.js │ ├── skel/ │ │ ├── .gitignore │ │ ├── .meteor/ │ │ │ ├── .gitignore │ │ │ ├── packages │ │ │ └── platforms │ │ ├── client/ │ │ │ ├── main.css │ │ │ ├── main.html │ │ │ └── main.js │ │ ├── package.json │ │ └── server/ │ │ └── main.js │ └── skel-pack/ │ ├── README.md │ ├── package.js │ ├── ~fs-name~-tests.js │ └── ~fs-name~.js ├── tests/ │ ├── apps/ │ │ ├── app-prints-pid/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ └── print.js │ │ ├── app-throws-error/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── packages/ │ │ │ │ └── throwing-package/ │ │ │ │ ├── package.js │ │ │ │ └── thrower.js │ │ │ └── throw.js │ │ ├── app-using-stylus/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── body.html │ │ │ ├── client/ │ │ │ │ ├── app-export.import.styl │ │ │ │ └── app.styl │ │ │ ├── main.js │ │ │ └── packages/ │ │ │ └── my-package/ │ │ │ ├── package-export.styl │ │ │ ├── package-file.main.styl │ │ │ ├── package-local-export.styl │ │ │ └── package.js │ │ ├── app-with-atmosphere-package/ │ │ │ └── .meteor/ │ │ │ ├── .gitignore │ │ │ ├── packages │ │ │ ├── release │ │ │ └── versions │ │ ├── app-with-unimported-lazy-file/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── .id │ │ │ │ ├── packages │ │ │ │ ├── platforms │ │ │ │ ├── release │ │ │ │ └── versions │ │ │ ├── imports/ │ │ │ │ └── file.js │ │ │ └── main.js │ │ ├── build-errors/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── foo.awesome │ │ │ └── packages/ │ │ │ └── with-colon-plugin/ │ │ │ ├── .gitignore │ │ │ ├── package.js │ │ │ └── plugin.js │ │ ├── build-plugin-throws-error/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── file.extension │ │ │ └── packages/ │ │ │ └── build-plugin/ │ │ │ ├── README.md │ │ │ ├── build-plugin.js │ │ │ └── package.js │ │ ├── caching-coffee/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── f1.coffee │ │ │ ├── f2.coffee │ │ │ ├── f3.coffee │ │ │ └── packages/ │ │ │ └── local-pack/ │ │ │ ├── p.coffee │ │ │ └── package.js │ │ ├── caching-less/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── imports/ │ │ │ │ └── dotdot.less │ │ │ ├── output.js │ │ │ ├── packages/ │ │ │ │ └── local-pack/ │ │ │ │ ├── p.less │ │ │ │ └── package.js │ │ │ ├── subdir/ │ │ │ │ ├── foo.import.less │ │ │ │ └── nested-root.less │ │ │ └── top.less │ │ ├── caching-stylus/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── imports/ │ │ │ │ └── dotdot.styl │ │ │ ├── output.js │ │ │ ├── packages/ │ │ │ │ └── local-pack/ │ │ │ │ ├── p.styl │ │ │ │ └── package.js │ │ │ ├── subdir/ │ │ │ │ ├── foo.import.styl │ │ │ │ └── nested-root.styl │ │ │ └── top.styl │ │ ├── circular-deps/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── packages/ │ │ │ │ ├── first/ │ │ │ │ │ └── package.js │ │ │ │ ├── first-imply/ │ │ │ │ │ └── package.js │ │ │ │ ├── first-unordered/ │ │ │ │ │ └── package.js │ │ │ │ ├── first-weak/ │ │ │ │ │ └── package.js │ │ │ │ ├── second/ │ │ │ │ │ └── package.js │ │ │ │ ├── second-imply/ │ │ │ │ │ └── package.js │ │ │ │ ├── second-unordered/ │ │ │ │ │ └── package.js │ │ │ │ └── second-weak/ │ │ │ │ └── package.js │ │ │ └── server/ │ │ │ └── exit123.js │ │ ├── compiler-plugin-add-asset/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── foo.printme │ │ │ ├── packages/ │ │ │ │ └── local-plugin/ │ │ │ │ ├── package.js │ │ │ │ └── plugin.js │ │ │ └── use-asset.js │ │ ├── compiler-plugin-asset-and-source/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ └── packages/ │ │ │ └── asset-and-source/ │ │ │ ├── asset-and-source.js │ │ │ └── package.js │ │ ├── compiler-plugin-static-html/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── .id │ │ │ │ ├── packages │ │ │ │ ├── platforms │ │ │ │ ├── release │ │ │ │ └── versions │ │ │ └── static.html │ │ ├── compiler-plugin-static-html-error/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── .id │ │ │ │ ├── packages │ │ │ │ ├── platforms │ │ │ │ ├── release │ │ │ │ └── versions │ │ │ └── static.html │ │ ├── compiler-plugin-throws-on-instantiate/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── foo.printme │ │ │ └── packages/ │ │ │ └── local-plugin/ │ │ │ ├── package.js │ │ │ └── plugin.js │ │ ├── css-injection-test/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── css-injection-test.js │ │ │ └── packages/ │ │ │ └── my-package/ │ │ │ ├── foo.css │ │ │ └── package.js │ │ ├── custom-minifier/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── body.html │ │ │ ├── code.js │ │ │ ├── packages/ │ │ │ │ └── custom-minifier/ │ │ │ │ ├── package.js │ │ │ │ └── plugin/ │ │ │ │ └── minify.js │ │ │ └── styles.css │ │ ├── ddp-heartbeat/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ └── server/ │ │ │ └── heartbeat_test.js │ │ ├── debug-only-test/ │ │ │ ├── .meteor/ │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ └── packages/ │ │ │ └── debug-only-prod-only/ │ │ │ └── package.js │ │ ├── dev-bundle-bin-commands/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ └── package.json │ │ ├── duplicate-compiler-extensions/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── bar.printme │ │ │ ├── foo.printme │ │ │ └── packages/ │ │ │ ├── another-local-plugin/ │ │ │ │ ├── package.js │ │ │ │ └── plugin.js │ │ │ └── local-plugin/ │ │ │ ├── package.js │ │ │ └── plugin.js │ │ ├── empty/ │ │ │ └── .meteor/ │ │ │ ├── .gitignore │ │ │ ├── packages │ │ │ └── release │ │ ├── failover-test/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ └── server/ │ │ │ └── failover-test.js │ │ ├── hot-code-push-test/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── hot-code-push-test.html │ │ │ ├── hot-code-push-test.js │ │ │ └── packages/ │ │ │ └── my-package/ │ │ │ ├── foo.js │ │ │ └── package.js │ │ ├── lint-on-publish/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── .id │ │ │ │ ├── packages │ │ │ │ ├── platforms │ │ │ │ └── release │ │ │ ├── a.js │ │ │ └── packages/ │ │ │ ├── dep-package/ │ │ │ │ ├── dep-package.js │ │ │ │ └── package.js │ │ │ └── my-package/ │ │ │ ├── .versions │ │ │ ├── my-package.js │ │ │ └── package.js │ │ ├── linting-app/ │ │ │ ├── .jshintrc │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── a.html │ │ │ ├── client/ │ │ │ │ └── client.js │ │ │ ├── packages/ │ │ │ │ └── my-package/ │ │ │ │ ├── .jshintrc │ │ │ │ ├── .versions │ │ │ │ ├── package-client.js │ │ │ │ ├── package-server.js │ │ │ │ └── package.js │ │ │ └── server/ │ │ │ └── server.js │ │ ├── local-compiler-plugin/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── bar.printme │ │ │ ├── foo.printme │ │ │ └── packages/ │ │ │ └── local-plugin/ │ │ │ ├── package.js │ │ │ └── plugin.js │ │ ├── minification-css-splitting/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── body.html │ │ │ ├── client/ │ │ │ │ └── lots-of-styles.main.styl │ │ │ └── code.js │ │ ├── minifier-plugin-bad-extension/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ └── packages/ │ │ │ └── local-plugin/ │ │ │ ├── package.js │ │ │ └── plugin.js │ │ ├── minifier-plugin-multiple-minifiers-for-js/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ └── packages/ │ │ │ ├── local-plugin/ │ │ │ │ ├── package.js │ │ │ │ └── plugin.js │ │ │ └── local-plugin-2/ │ │ │ ├── package.js │ │ │ └── plugin.js │ │ ├── mobile-platforms/ │ │ │ └── .meteor/ │ │ │ ├── .gitignore │ │ │ ├── packages │ │ │ ├── platforms │ │ │ └── release │ │ ├── modules/ │ │ │ ├── .gitignore │ │ │ ├── .meteor/ │ │ │ │ ├── .finished-upgraders │ │ │ │ ├── .gitignore │ │ │ │ ├── .id │ │ │ │ ├── packages │ │ │ │ ├── platforms │ │ │ │ ├── release │ │ │ │ └── versions │ │ │ ├── README.md │ │ │ ├── client/ │ │ │ │ ├── compatibility/ │ │ │ │ │ └── bare.js │ │ │ │ └── only.js │ │ │ ├── eager-coffee.coffee │ │ │ ├── eager-jsx.jsx │ │ │ ├── eager.css │ │ │ ├── imports/ │ │ │ │ ├── dir/ │ │ │ │ │ ├── index.js │ │ │ │ │ └── package.json │ │ │ │ ├── lazy.css │ │ │ │ ├── lazy.html │ │ │ │ ├── lazy1.js │ │ │ │ ├── lazy2.js │ │ │ │ ├── plain.es5.js │ │ │ │ ├── return.js │ │ │ │ └── shared.js │ │ │ ├── lazy-css.html │ │ │ ├── lib/ │ │ │ │ ├── a/ │ │ │ │ │ ├── index.js │ │ │ │ │ └── package.json │ │ │ │ ├── b/ │ │ │ │ │ └── package.json │ │ │ │ └── index.js │ │ │ ├── package.json │ │ │ ├── packages/ │ │ │ │ ├── client-only-ecmascript/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── client.js │ │ │ │ │ ├── imported.js │ │ │ │ │ ├── package.js │ │ │ │ │ └── server.js │ │ │ │ ├── modules-test-package/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── client.js │ │ │ │ │ ├── common.js │ │ │ │ │ ├── css/ │ │ │ │ │ │ ├── imported.css │ │ │ │ │ │ └── not-imported.css │ │ │ │ │ ├── illegal.html │ │ │ │ │ ├── os-stub.js │ │ │ │ │ ├── package.js │ │ │ │ │ └── server.js │ │ │ │ └── modules-test-plugin/ │ │ │ │ ├── .babelrc │ │ │ │ ├── README.md │ │ │ │ ├── array.arson │ │ │ │ ├── modules-test-plugin.js │ │ │ │ ├── one.arson │ │ │ │ ├── oyez-transform.js │ │ │ │ ├── package.js │ │ │ │ └── plugin.js │ │ │ ├── server/ │ │ │ │ └── only.js │ │ │ └── tests.js │ │ ├── npmtest/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ └── packages/ │ │ │ └── npm-test/ │ │ │ ├── npmtest.js │ │ │ └── package.js │ │ ├── once/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ └── once.js │ │ ├── package-stats-tests/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── package-stats-tests.js │ │ │ └── packages/ │ │ │ ├── local-package/ │ │ │ │ ├── blah.js │ │ │ │ └── package.js │ │ │ └── package-stats-opt-out/ │ │ │ └── package.js │ │ ├── package-tests/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── identifier │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── mobile-config.js │ │ │ └── packages/ │ │ │ ├── contains-camera-cordova-plugin/ │ │ │ │ └── package.js │ │ │ ├── contains-cordova-plugin/ │ │ │ │ ├── package.js │ │ │ │ ├── package2.js │ │ │ │ └── package3.js │ │ │ ├── contains-old-cordova-plugin/ │ │ │ │ └── package.js │ │ │ ├── contains-plugin/ │ │ │ │ ├── .gitignore │ │ │ │ ├── .npm/ │ │ │ │ │ └── plugin/ │ │ │ │ │ └── compileCoffeescript/ │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── README │ │ │ │ │ └── npm-shrinkwrap.json │ │ │ │ ├── package.js │ │ │ │ ├── package2.js │ │ │ │ └── plugin/ │ │ │ │ └── plugin.js │ │ │ ├── debug-only/ │ │ │ │ ├── debug-only.js │ │ │ │ └── package.js │ │ │ ├── depends-on-plugin/ │ │ │ │ ├── .gitignore │ │ │ │ ├── foo.js │ │ │ │ └── package.js │ │ │ ├── empty-cordova-plugin/ │ │ │ │ ├── package.js │ │ │ │ └── plugin/ │ │ │ │ ├── plugin.xml │ │ │ │ ├── src/ │ │ │ │ │ └── android/ │ │ │ │ │ ├── Empty.java │ │ │ │ │ └── Empty_changed.java │ │ │ │ └── www/ │ │ │ │ └── Empty.js │ │ │ ├── no-description/ │ │ │ │ └── package.js │ │ │ ├── prod-only/ │ │ │ │ ├── package.js │ │ │ │ └── prod-only.js │ │ │ ├── say-something/ │ │ │ │ ├── .gitignore │ │ │ │ ├── foo.js │ │ │ │ └── package.js │ │ │ ├── say-something-client-targets/ │ │ │ │ ├── .gitignore │ │ │ │ ├── all-clients.js │ │ │ │ ├── browser-client.js │ │ │ │ ├── cordova-client.js │ │ │ │ ├── package.js │ │ │ │ └── server.js │ │ │ ├── with-add-files/ │ │ │ │ ├── package.js │ │ │ │ └── with-add-files.js │ │ │ └── with-main-module/ │ │ │ ├── package.js │ │ │ └── with-main-module.js │ │ ├── packageless/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── .id │ │ │ │ ├── packages │ │ │ │ ├── platforms │ │ │ │ └── release │ │ │ └── packageless.js │ │ ├── simple-app/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ └── simple.html │ │ ├── standard-app/ │ │ │ └── .meteor/ │ │ │ ├── .gitignore │ │ │ ├── packages │ │ │ └── release │ │ └── uses-published-package-with-inactive-source/ │ │ ├── .meteor/ │ │ │ ├── .gitignore │ │ │ ├── packages │ │ │ └── release │ │ └── packages/ │ │ └── local-plugin/ │ │ └── package.js │ ├── autoupdate.js │ ├── boot-tests.js │ ├── build-errors.js │ ├── built-packages/ │ │ └── has-colons.tgz │ ├── bundle.js │ ├── colon-converter-tests.js │ ├── command-line.js │ ├── compiler-plugins.js │ ├── constraint-solver.js │ ├── cordova-builds.js │ ├── cordova-hcp.js │ ├── cordova-platforms.js │ ├── cordova-plugins.js │ ├── cordova-run.js │ ├── create.js │ ├── custom-minifier.js │ ├── ddp-heartbeat.js │ ├── dev-bundle-bin-commands.js │ ├── fake-mongod/ │ │ ├── fake-mongod │ │ ├── fake-mongod.bat │ │ └── fake-mongod.js │ ├── galaxy.js │ ├── help.js │ ├── hot-code-push.js │ ├── js-analyze.js │ ├── linter-plugins.js │ ├── login.js │ ├── meteor-script-link-tests.js │ ├── minifier-bad-plugins.js │ ├── modules.js │ ├── mongo.js │ ├── npm.js │ ├── old/ │ │ ├── app-with-package/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── .id │ │ │ │ └── packages │ │ │ └── packages/ │ │ │ └── test-package/ │ │ │ ├── dummy.js │ │ │ └── package.js │ │ ├── app-with-private/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── .id │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ ├── packages/ │ │ │ │ └── test-package/ │ │ │ │ ├── .gitignore │ │ │ │ ├── package.js │ │ │ │ ├── plugin.js │ │ │ │ ├── test-package.js │ │ │ │ ├── test-package.txt │ │ │ │ └── test.notregistered │ │ │ ├── private/ │ │ │ │ ├── nested/ │ │ │ │ │ └── test.txt │ │ │ │ └── test.txt │ │ │ └── test.js │ │ ├── app-with-public/ │ │ │ ├── .meteor/ │ │ │ │ ├── .gitignore │ │ │ │ ├── .id │ │ │ │ ├── packages │ │ │ │ └── release │ │ │ └── public/ │ │ │ ├── nested/ │ │ │ │ └── nested.txt │ │ │ └── test.txt │ │ ├── cli-test.sh │ │ ├── empty-app/ │ │ │ └── .meteor/ │ │ │ ├── .gitignore │ │ │ ├── .id │ │ │ └── packages │ │ ├── test-bundler-assets.js │ │ ├── test-bundler-npm.js │ │ ├── test-bundler-options.js │ │ └── test-watch.js │ ├── old.js │ ├── organizations.js │ ├── package-tests.js │ ├── packages/ │ │ ├── fake-accounts-base/ │ │ │ ├── fake-accounts-base-tests.js │ │ │ ├── fake-accounts-base.js │ │ │ └── package.js │ │ ├── has-colons.tgz │ │ ├── package-for-show/ │ │ │ ├── README.md │ │ │ ├── package-completely-empty.js │ │ │ ├── package-customizable.js │ │ │ ├── package-rc-version.js │ │ │ ├── package-with-deps.js │ │ │ ├── package-with-exports.js │ │ │ ├── package-with-git.js │ │ │ ├── package-with-implies.js │ │ │ └── package.js │ │ ├── package-of-two-versions/ │ │ │ ├── package-version.js │ │ │ └── package.js │ │ ├── package-with-colons/ │ │ │ └── package.js │ │ └── package-with-npm/ │ │ └── package.js │ ├── parse-stack-test.js │ ├── releases.js │ ├── run.js │ ├── source-maps.js │ ├── standard-minification.js │ ├── static-html.js │ ├── stylus-cross-packages.js │ ├── tarball.js │ ├── test-modes.js │ ├── utils-tests.js │ ├── version-parser.js │ └── wipe-all-packages.js ├── tool-env/ │ ├── README.md │ ├── cleanup.js │ ├── install-babel.js │ ├── install-git-hooks.js │ ├── install-runtime.js │ ├── isopackets.js │ ├── profile-require.js │ ├── profile.js │ └── source-map-retriever-stack.js ├── tool-testing/ │ ├── README.md │ ├── galaxy-utils.js │ ├── phantom/ │ │ └── open-url.js │ ├── selftest.js │ └── test-utils.js ├── upgraders.js └── utils/ ├── archinfo.js ├── buildmessage.js ├── buildmessage.md ├── colon-converter.js ├── fiber-helpers.js ├── func-utils.js ├── http-helpers.js ├── mongo-exit-codes.js ├── parse-stack.js ├── processes.js └── utils.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .arcconfig ================================================ { "project_id" : "Meteor framework", "conduit_uri" : "https://phabricator.meteor.io/", "immutable_history" : false } ================================================ FILE: .eslintignore ================================================ android_bundle/ dev_bundle/ docs/ examples/ packages/ scripts/ tools/ !tools/*.js !tools/isobuild/*.js !tools/catalog/*.js !tools/packaging/*.js !tools/cli/*.js !tools/runners/*.js !tools/tool-env/*.js !tools/fs/*.js # Below, files that have yet to be converted to match the linter tools/archinfo.js tools/auth-client.js tools/auth.js tools/buildmessage.js tools/cleanup.js tools/colon-converter.js tools/config.js tools/console.js tools/deploy.js tools/fiber-helpers.js tools/fs/files.js tools/fs/mini-files.js tools/func-utils.js tools/http-helpers.js tools/inspector.js tools/index.js tools/mongo-exit-codes.js tools/processes.js tools/progress.js tools/project-context.js tools/runners/run-log.js tools/fs/safe-pathwatcher.js tools/selftest.js tools/service-connection.js tools/shell-client.js tools/stats.js tools/test-utils.js tools/upgraders.js tools/utils/utils.js tools/fs/watch.js tools/catalog/catalog-local.js tools/catalog/catalog-remote.js tools/catalog/catalog.js tools/catalog/catalog-utils.js tools/cli/commands-cordova.js tools/cli/commands-packages-query.js tools/cli/commands-packages.js tools/cli/commands.js tools/cli/main.js tools/tool-env/flush-buffers-on-exit-in-windows.js tools/tool-env/install-babel.js tools/tool-env/isopackets.js tools/tool-env/profile-require.js tools/tool-env/profile.js tools/runners/run-all.js tools/runners/run-app.js tools/runners/run-mongo.js tools/runners/run-proxy.js tools/runners/run-selenium.js tools/runners/run-updater.js tools/packaging/package-client.js tools/packaging/package-map.js tools/packaging/package-version-parser.js tools/packaging/release.js tools/packaging/tropohouse.js tools/packaging/updater.js tools/packaging/warehouse.js tools/isobuild/build-plugin.js tools/isobuild/builder.js tools/isobuild/bundler.js tools/isobuild/compiler-deprecated-compile-step.js tools/isobuild/compiler-plugin.js tools/isobuild/compiler.js tools/isobuild/import-scanner.js tools/isobuild/isopack-cache.js tools/isobuild/isopack.js tools/isobuild/js-analyze.js tools/isobuild/linker.js tools/isobuild/linter-plugin.js tools/isobuild/meteor-npm.js tools/isobuild/npm-discards.js tools/isobuild/package-api.js tools/isobuild/package-source.js tools/isobuild/source-arch.js ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ ## README and "Good to know" section Please make sure that you carefully have read the [README](https://github.com/4commerce-technologies-AG/meteor) and especially had a look to its "Good to know" section before filing a new issue. ## Guide and tutorial issues Some of the already known issues may also help as a guide or tutorial. Please check out our [_guide flagged issues_](https://github.com/4commerce-technologies-AG/meteor/issues?q=+label%3Aflag%3Aguide+) if your question may already be answered. Thanks for supporting this project ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. Note that we are unlikely to accept pull requests that add features without prior discussion. The best way to propose a feature is to open an issue first and discuss your ideas there before implementing them. ================================================ FILE: .gitignore ================================================ .DS_Store /.meteor *~ /dev_bundle /dev_bundle*.tar.gz /android_bundle /android_bundle*.tar.gz /node_*.tar.gz /mongo_*.tar.gz /dist \#*\# .\#* .idea *.iml *.sublime-project *.sublime-workspace TAGS *.log *.out npm-debug.log universe .babel-cache mongo-test-output ================================================ FILE: .mailmap ================================================ # The presence of this file makes it easier to find GitHub usernames for # History.md. # # This is a git dotfile that affects the output of 'git shortlog'. eg, run: # git shortlog -s release/METEOR@1.0.1..HEAD # to get a sorted list of all committers to revisions in HEAD but not # in 1.0.1. To get the list including email addresses (useful for input # to the script below) include --email as well. # # For any emails that show up in the shortlog that aren't in one of # these lists, figure out their GitHub username and add them. # # A command-line way to get the GitHub username for an author: # scripts/admin/find-author-github.sh 'User Name ' # (Note that this script always outputs GITHUB so you should manually # check to see if they are an MDG employee!) GITHUB: 0a- GITHUB: adnissen GITHUB: aldeed GITHUB: AlexeyMK GITHUB: andylash GITHUB: ansman GITHUB: anstarovoyt GITHUB: apendua GITHUB: awatson1978 GITHUB: awwx GITHUB: babenzele GITHUB: benweissmann GITHUB: bwhitty GITHUB: Cangit GITHUB: chrisbridgett GITHUB: christianbundy GITHUB: cmather GITHUB: codeinthehole GITHUB: colllin GITHUB: cryptoquick GITHUB: d4nyll GITHUB: dandv GITHUB: DanielDent GITHUB: DanielDent GITHUB: DanielDornhardt GITHUB: davegonzalez GITHUB: DenisGorbachev GITHUB: DenisGorbachev GITHUB: ducdigital GITHUB: duckspeaker GITHUB: ecwyne GITHUB: emgee3 GITHUB: EOT GITHUB: fay-jai GITHUB: felixrabe GITHUB: FooBarWidget GITHUB: FredericoC GITHUB: Gaelan GITHUB: graemian GITHUB: gsuess GITHUB: hwillson GITHUB: icellan GITHUB: ImtiazMajeed GITHUB: jacott GITHUB: jakozaur GITHUB: JamesLefrere GITHUB: jbruni GITHUB: jfhamlin GITHUB: jperl GITHUB: kentonv GITHUB: kevinchiu GITHUB: knownasilya GITHUB: LyuGGang GITHUB: marcandre GITHUB: mart-jansink GITHUB: matteodem GITHUB: Maxhodges GITHUB: MaximDubrovin GITHUB: meawoppl GITHUB: meonkeys GITHUB: michaelbishop GITHUB: mitar GITHUB: mitar GITHUB: mizzao GITHUB: mquandalle GITHUB: mquandalle GITHUB: murillo128 GITHUB: musically-ut GITHUB: nathan-muir GITHUB: Neftedollar GITHUB: netanelgilad GITHUB: ogourment GITHUB: ograycode GITHUB: omeid GITHUB: OyoKooN GITHUB: paulswartz GITHUB: pcjpcj2 GITHUB: Pent GITHUB: physiocoder GITHUB: PooMaster GITHUB: prapicault GITHUB: prapicault GITHUB: Primigenus GITHUB: pscanf GITHUB: queso GITHUB: rbabayoff GITHUB: rcy GITHUB: rdickert GITHUB: restebanez GITHUB: rgoomar GITHUB: rgould GITHUB: RichardLitt GITHUB: richguan GITHUB: rick-golden-healthagen GITHUB: rissem GITHUB: rjakobsson GITHUB: RobertLowe GITHUB: romanzolotarev GITHUB: rosh93 GITHUB: ryw GITHUB: rzymek GITHUB: sdarnell GITHUB: Siilwyn GITHUB: smallhelm GITHUB: subhog GITHUB: svda GITHUB: Tarang GITHUB: tbjers GITHUB: thatneat GITHUB: timhaines GITHUB: timoabend GITHUB: tmeasday GITHUB: TomFreudenberg GITHUB: trusktr GITHUB: twhy GITHUB: Urigo GITHUB: waitingkuo GITHUB: wulfmeister GITHUB: yauh GITHUB: yeputons GITHUB: zol METEOR: arbesfeld METEOR: avital METEOR: benjamn METEOR: benjamn METEOR: debergalis METEOR: dgreensp METEOR: ekatek METEOR: estark37 METEOR: estark37 METEOR: glasser METEOR: glasser METEOR: gschmidt METEOR: justinsb METEOR: karayu METEOR: mariapacana METEOR: multilinear METEOR: n1mmy METEOR: sixolet METEOR: Slava METEOR: Slava METEOR: stubailo METEOR: stubailo METEOR: stubailo METEOR: stubailo METEOR: yyx990803 ================================================ FILE: .reviewboardrc ================================================ REVIEWBOARD_URL = 'https://rbcommons.com/s/meteor/' REPOSITORY = 'Meteor framework' ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - "0.10" cache: directories: - "dev_bundle" - ".meteor" - ".babel-cache" install: ./meteor --get-ready script: TEST_PACKAGES_EXCLUDE="less" ./packages/test-in-console/run.sh sudo: false ================================================ FILE: Contributing.md ================================================ # Contributing to Meteor universal fork We are happy to have your help for building and running the Meteor universal fork. Here's how you can help us with tests, bug reports and new code.
### README and "Good to know" section Please make sure that you carefully have read the [README](https://github.com/4commerce-technologies-AG/meteor) and especially had a look to its "Good to know" section.
### Guide and tutorial issues Some of the already known issues may also help as a guide or tutorial. Please check out our [_guide flagged issues_](https://github.com/4commerce-technologies-AG/meteor/issues?q=+label%3Aflag%3Aguide+) if your question may already be answered.
### Reporting a bug in Meteor universal fork We welcome bug reports. If you've found a new bug in running Meteor universal fork please file a report in [our issue tracker](https://github.com/4commerce-technologies-AG/meteor/issues). Before you file your issue, please look twice to see if it has already been reported or solved. If so, comment, up-vote or +1 the existing, even closed, issue to show that it's affecting multiple people.

# Contributing to official Meteor project Please check contribution for Meteor at the official project and [read about their rules](https://github.com/meteor/meteor/blob/devel/Contributing.md) ================================================ FILE: History.md ================================================ ## v.NEXT ## v1.3.4.1 * Increased the default HTTP timeout for requests made by the `meteor` command-line tool to 60 seconds (previously 30), and [disabled the timeout completely for Galaxy deploys](https://forums.meteor.com/t/1-3-4-breaks-galaxy-deployment-etimedout/25383/). * Minor bug fixes: [#7281](https://github.com/meteor/meteor/pull/7281) [#7276](https://github.com/meteor/meteor/pull/7276) ## v1.3.4 * The version of `npm` used by `meteor npm` and when installing `Npm.depends` dependencies of Meteor packages has been upgraded from 2.15.1 to **3.9.6**, which should lead to much flatter node_modules dependency trees. * The `meteor-babel` npm package has been upgraded to 0.11.6, and is now installed using `npm@3.9.6`, fixing bugs arising from Windows path limits, such as [#7247](https://github.com/meteor/meteor/issues/7247). * The `reify` npm package has been upgraded to 0.3.4, fixing [#7250](https://github.com/meteor/meteor/issues/7250). * Thanks to caching improvements for the `files.{stat,lstat,readdir,realpath}` methods and `PackageSource#_findSources`, development server restart times are no longer proportional to the number of files in `node_modules` directories. [#7253](https://github.com/meteor/meteor/issues/7253) [#7008](https://github.com/meteor/meteor/issues/7008) * When installed via `InstallMeteor.exe` on Windows, Meteor can now be easily uninstalled through the "Programs and Features" control panel. * HTTP requests made by the `meteor` command-line tool now have a timeout of 30 seconds, which can be adjusted by the `$TIMEOUT_SCALE_FACTOR` environment variable. [#7143](https://github.com/meteor/meteor/pull/7143) * The `request` npm dependency of the `http` package has been upgraded from 2.53.0 to 2.72.0. * The `--headless` option is now supported by `meteor test` and `meteor test-packages`, in addition to `meteor self-test`. [#7245](https://github.com/meteor/meteor/pull/7245) * Miscellaneous fixed bugs: [#7255](https://github.com/meteor/meteor/pull/7255) [#7239](https://github.com/meteor/meteor/pull/7239) ## v1.3.3.1 * Fixed bugs: [#7226](https://github.com/meteor/meteor/pull/7226) [#7181](https://github.com/meteor/meteor/pull/7181) [#7221](https://github.com/meteor/meteor/pull/7221) [#7215](https://github.com/meteor/meteor/pull/7215) [#7217](https://github.com/meteor/meteor/pull/7217) * The `node-aes-gcm` npm package used by `oauth-encryption` has been upgraded to 0.1.5. [#7217](https://github.com/meteor/meteor/issues/7217) * The `reify` module compiler has been upgraded to 0.3.3. * The `meteor-babel` package has been upgraded to 0.11.4. * The `pathwatcher` npm package has been upgraded to 6.7.0. * In CoffeeScript files with raw JavaScript enclosed by backticks, the compiled JS will no longer contain `require` calls inserted by Babel. [#7226](https://github.com/meteor/meteor/issues/7226) * Code related to the Velocity testing system has been removed. [#7235](https://github.com/meteor/meteor/pull/7235) * Allow smtps:// in MAIL_URL [#7043](https://github.com/meteor/meteor/pull/7043) * Adds `Accounts.onLogout()` a hook directly analogous to `Accounts.onLogin()`. [PR #6889](https://github.com/meteor/meteor/pull/6889) ## v1.3.3 * Node has been upgraded from 0.10.43 to 0.10.45. * `npm` has been upgraded from 2.14.22 to 2.15.1. * The `fibers` package has been upgraded to 1.0.13. * The `meteor-babel` package has been upgraded to 0.10.9. * The `meteor-promise` package has been upgraded to 0.7.1, a breaking change for code that uses `Promise.denodeify`, `Promise.nodeify`, `Function.prototype.async`, or `Function.prototype.asyncApply`, since those APIs have been removed. * Meteor packages with binary npm dependencies are now automatically rebuilt using `npm rebuild` whenever the version of Node or V8 changes, making it much simpler to use Meteor with different versions of Node. 5dc51d39ecc9e8e342884f3b4f8a489f734b4352 * `*.min.js` files are no longer minified during the build process. [PR #6986](https://github.com/meteor/meteor/pull/6986) [Issue #5363](https://github.com/meteor/meteor/issues/5363) * You can now pick where the `.meteor/local` directory is created by setting the `METEOR_LOCAL_DIR` environment variable. This lets you run multiple instances of the same Meteor app. [PR #6760](https://github.com/meteor/meteor/pull/6760) [Issue #6532](https://github.com/meteor/meteor/issues/6532) * Allow using authType in Facebook login [PR #5694](https://github.com/meteor/meteor/pull/5694) * Adds flush() method to Tracker to force recomputation [PR #4710](https://github.com/meteor/meteor/pull/4710) * Adds `defineMutationMethods` option (default: true) to `new Mongo.Collection` to override default behavior that sets up mutation methods (/collection/[insert|update...]) [PR #5778](https://github.com/meteor/meteor/pull/5778) * Allow overridding the default warehouse url by specifying `METEOR_WAREHOUSE_URLBASE` [PR #7054](https://github.com/meteor/meteor/pull/7054) * Allow `_id` in `$setOnInsert` in Minimongo: https://github.com/meteor/meteor/pull/7066 * Added support for `$eq` to Minimongo: https://github.com/meteor/meteor/pull/4235 * Insert a `Date` header into emails by default: https://github.com/meteor/meteor/pull/6916/files * `meteor test` now supports setting the bind address using `--port IP:PORT` the same as `meteor run` [PR #6964](https://github.com/meteor/meteor/pull/6964) [Issue #6961](https://github.com/meteor/meteor/issues/6961) * `Meteor.apply` now takes a `noRetry` option to opt-out of automatically retrying non-idempotent methods on connection blips: [PR #6180](https://github.com/meteor/meteor/pull/6180) * DDP callbacks are now batched on the client side. This means that after a DDP message arrives, the local DDP client will batch changes for a minimum of 5ms (configurable via `bufferedWritesInterval`) and a maximum of 500ms (configurable via `bufferedWritesMaxAge`) before calling any callbacks (such as cursor observe callbacks). * PhantomJS is no longer included in the Meteor dev bundle (#6905). If you previously relied on PhantomJS for local testing, the `spiderable` package, Velocity tests, or testing Meteor from a checkout, you should now install PhantomJS yourself, by running the following commmand: `meteor npm install -g phantomjs-prebuilt` * The `babel-compiler` package now looks for `.babelrc` files and `package.json` files with a "babel" section. If found, these files may contribute additional Babel transforms that run before the usual `babel-preset-meteor` set of transforms. In other words, if you don't like the way `babel-preset-meteor` handles a particular kind of syntax, you can add your preferred transform plugins to the "presets" or "plugins" section of your `.babelrc` or `package.json` file. #6351 * When `BabelCompiler` cannot resolve a Babel plugin or preset package in `.babelrc` or `package.json`, it now merely warns instead of crashing. #7179 * Compiler plugins can now import npm packages that are visible to their input files using `inputFile.require(id)`. b16e8d50194b37d3511889b316345f31d689b020 * `import` statements in application modules now declare normal variables for the symbols that are imported, making it significantly easier to inspect imported variables when debugging in the browser console or in `meteor shell`. * `import` statements in application modules are no longer restricted to the top level, and may now appear inside conditional statements (e.g. `if (Meteor.isServer) { import ... }`) or in nested scopes. * `import` statements now work as expected in `meteor shell`. #6271 * Commands installed in `dev_bundle/lib/node_modules/.bin` (such as `node-gyp` and `node-pre-gyp`) are now available to scripts run by `meteor npm`. e95dfe410e1b43e8131bc2df9d2c29decdd1eaf6 * When building an application using `meteor build`, "devDependencies" listed in `package.json` are no longer copied into the bundle. #6750 * Packages tested with `meteor test-packages` now have access to local `node_modules` directories installed in the parent application or in the package directory itself. #6827 * You no longer need to specify `DEPLOY_HOSTNAME=galaxy.meteor.com` to run `meteor deploy` (and similar commands) against Galaxy. The AWS us-east-1 Galaxy is now the default for `DEPLOY_HOSTNAME`. If your app's DNS points to another Galaxy region, `meteor deploy` will detect that automatically as well. #7055 * The `coffeescript` plugin now passes raw JavaScript code enclosed by back-ticks to `BabelCompiler`, enabling all ECMAScript features (including `import` and `export`) within CoffeeScript. #6000 #6691 * The `coffeescript` package now implies the same runtime environment as `ecmascript` (`ecmascript-runtime`, `babel-runtime`, and `promise`, but not `modules`). #7184 * When Meteor packages install `npm` dependencies, the `process.env.NPM_CONFIG_REGISTRY` environment variable is now respected. #7162 * `files.rename` now always executes synchronously. 9856d1d418a4d19c0adf22ec9a92f7ce81a23b05 * "Bare" files contained by `client/compatibility/` directories or added with `api.addFiles(path, ..., { bare: true })` are no longer compiled by Babel. https://github.com/meteor/meteor/pull/7033#issuecomment-225126778 * Miscellaneous fixed bugs: #6877 #6843 #6881 ## v1.3.2.4 > Meteor 1.3.2.4 was published because publishing 1.3.2.3 failed in an unrecoverable way. Meteor 1.3.2.4 contains no additional changes beyond the changes in 1.3.2.3. ## v1.3.2.3 * Reverted accidental changes included in 1.3.2.1 and 1.3.2.2 that improved DDP performance by batching updates, but broke some packages that relied on private methods of the DDP client Connection class. See https://github.com/meteor/meteor/pull/5680 for more details. These changes will be reinstated in 1.3.3. ## v1.3.2.2 * Fixed bugs #6819 and #6831. ## v1.3.2.1 * Fixed faulty comparison of `.sourcePath` and `.targetPath` properties of files scanned by the `ImportScanner`, which caused problems for apps using the `tap:i18n` package. 6e792a7cf25847b8cd5d5664a0ff45c9fffd9e57 ## v1.3.2 * The `meteor/meteor` repository now includes a `Roadmap.md` file: https://github.com/meteor/meteor/blob/devel/Roadmap.md * Running `npm install` in `bundle/programs/server` when deploying an app also rebuilds any binary npm dependencies, fixing #6537. Set METEOR_SKIP_NPM_REBUILD=1 to disable this behavior if necessary. * Non-.js(on) files in `node_modules` (such as `.less` and `.scss`) are now processed by compiler plugins and may be imported by JS. #6037 * The `jquery` package can now be completely removed from any app (#6563), and uses `/node_modules/jquery` if available (#6626). * Source maps are once again generated for all bundled JS files, even if they are merely identity mappings, so that the files appear distinct in the browser, and stack traces make more sense. #6639 * All application files in `imports` directories are now considered lazy, regardless of whether the app is using the `modules` package. This could be a breaking change for 1.3.2 apps that do not use `modules` or `ecmascript` but contain `imports` directories. Workaround: move files out of `imports`, or rename `imports` to something else. * The `npm-bcrypt` package has been upgraded to use the latest version (0.8.5) of the `bcrypt` npm package. * Compiler plugins can call `addJavaScript({ path })` multiple times with different paths for the same source file, and `module.id` will reflect this `path` instead of the source path, if they are different. #6806 * Fixed bugs: https://github.com/meteor/meteor/milestones/Release%201.3.2 * Fixed unintended change to `Match.Optional` which caused it to behave the same as the new `Match.Maybe` and incorrectly matching `null` where it previously would not have allowed it. #6735 ## v1.3.1 * Long isopacket node_modules paths have been shortened, fixing upgrade problems on Windows. #6609 * Version 1.3.1 of Meteor can now publish packages for earlier versions of Meteor, provided those packages do not rely on modules. #6484 #6618 * The meteor-babel npm package used by babel-compiler has been upgraded to version 0.8.4. c8d12aed4e725217efbe86fa35de5d5e56d73c83 * The `meteor node` and `meteor npm` commands now return the same exit codes as their child processes. #6673 #6675 * Missing module warnings are no longer printed for Meteor packages, or for `require` calls when `require` is not a free variable, fixing https://github.com/practicalmeteor/meteor-mocha/issues/19. * Cordova iOS builds are no longer built by Meteor, but merely prepared for building. 88d43a0f16a484a5716050cb7de8066b126c7b28 * Compiler plugin errors were formerly silenced for files not explicitly added in package.js. Now those errors are reported when/if the files are imported by the ImportScanner. be986fd70926c9dd8eff6d8866205f236c8562c4 ## v1.3 ### ES2015/Modules * Enable ES2015 and CommonJS modules in Meteor apps and packages, on both client and server. Also let you install modules in apps and package by running `npm install`. See: https://github.com/meteor/meteor/blob/master/packages/modules/README.md * Enable ES2015 generators and ES2016 async/await in the `ecmascript` package. * Inherit static getters and setters in subclasses, when using the `ecmascript` package. #5624 * Report full file paths on compiler errors when using the `ecmascript` package. #5551 * Now possible to `import` or `require` files with a `.json` file extension. #5810 * `process.env.NODE_ENV` is now defined on both client and server as either `development` or `production`, which also determines the boolean flags `Meteor.isDevelopment` and `Meteor.isProduction`. * Absolute identifiers for app modules no longer have the `/app/` prefix, and absolute identifiers for Meteor packages now have the prefix `/node_modules/meteor/` instead of just `/node_modules/`, meaning you should `import {Blaze} from "meteor/blaze"` instead of `from "blaze"`. * Package variables imported by application code are once again exposed globally, allowing them to be accessed from the browser console or from `meteor shell`. #5868 * Fixed global variable assignment analysis during linking. #5870 #5819 * Changes to files in node_modules will now trigger a restart of the development server, just like any other file changes. #5815 * The meteor package now exports a `global` variable (a la Node) that provides a reliable reference to the global object for all Meteor code. * Packages in local node_modules directories now take precedence over Meteor packages of the same name. #5933 * Upgraded `babel-compiler` to Babel 6, with the following set of plugins: https://github.com/meteor/babel-preset-meteor/blob/master/index.js * Lazy CSS modules may now be imported by JS: 12c946ee651a93725f243f790c7919de3d445a19 * Packages in the top-level node_modules directory of an app can now be imported by Meteor packages: c631d3ac35f5ca418b93c454f521989855b8ec72 * Added support for wildcard import and export statements. #5872 #5897 * Client-side stubs for built-in Node modules are now provided automatically if the `meteor-node-stubs` npm package is installed. #6056 * Imported file extensions are now optional for file types handled by compiler plugins. #6151 * Upgraded Babel packages to ~6.5.0: 292824da3f8449afd1cd39fcd71acd415c809c0f Note: .babelrc files are now ignored (#6016), but may be reenabled (#6351). * Polyfills now provided for `process.nextTick` and `process.platform`. #6167 #6198 #6055 efe53de492da6df785f1cbef2799d1d2b492a939 * The `meteor test-app` command is now `meteor test [--full-app]`: ab5ab15768136d55c76d51072e746d80b45ec181 * New apps now include a `package.json` file. c51b8cf7ffd8e7c9ca93768a2df93e4b552c199c * `require.resolve` is now supported. https://github.com/benjamn/install/commit/ff6b25d6b5511d8a92930da41db73b93eb1d6cf8 * JSX now enabled in `.js` files processed by the `ecmascript` compiler plugin. #6151 * On the server, modules contained within `node_modules` directories are now loaded using the native Node `require` function. #6398 * All ` ================================================ FILE: examples/unfinished/todos-backbone/client/todos.js ================================================ // An example Backbone application contributed by // [Jérôme Gravel-Niquet](http://jgn.me/). This demo uses a simple // [LocalStorage adapter](backbone-localstorage.html) // to persist Backbone models within your browser. // Load the application once the DOM is ready, using `jQuery.ready`: $(function(){ // ask for all the todos in my cache Meteor.subscribe('todos'); // helper functions function all () { return Todos.find(); }; function all_done () { return Todos.find({done: true}); }; function all_remaining () { return Todos.find({done: false}); }; function nextOrder () { var todos = Todos.find({}, {sort: {order: -1}, limit: 1}); return todos[0] ? todos[0].order + 1 : 1; }; // Todo Item View // -------------- // The DOM element for a todo item... window.TodoView = Backbone.View.extend({ //... is a list tag. tagName: "li", // Cache the template function for a single item. template: _.template($('#item-template').html()), // The DOM events specific to an item. events: { "click .check" : "toggleDone", "dblclick div.todo-text" : "edit", "click span.todo-destroy" : "clear", "keypress .todo-input" : "updateOnEnter" }, // Re-render the contents of the todo item. render: function() { $(this.el).html(this.template(this.model)); this.setText(); return this; }, // To avoid XSS (not that it would be harmful in this particular app), // we use `jQuery.text` to set the contents of the todo item. setText: function() { this.$('.todo-text').text(this.model.text); this.input = this.$('.todo-input'); this.input.bind('blur', _.bind(this.close, this)).val(this.model.text); }, // Toggle the `"done"` state of the object. toggleDone: function() { Todos.update(this.model._id, {$set: {done: !this.model.done}}); }, // Switch this view into `"editing"` mode, displaying the input field. edit: function() { $(this.el).addClass("editing"); this.input.focus(); }, // Close the `"editing"` mode, saving changes to the todo. // findLive callback will update this view. close: function() { Todos.update(this.model._id, {$set: {text: this.input.val()}}); $(this.el).removeClass("editing"); }, // If you hit `enter`, we're through editing the item. updateOnEnter: function(e) { if (e.keyCode == 13) this.close(); }, // Remove this view from the DOM. remove: function() { $(this.el).remove(); }, // destroy the todo object. the findLive callback will g/c this view. clear: function() { Todos.remove(this.model._id); } }); // The Application // --------------- // Our overall **AppView** is the top-level piece of UI. window.AppView = Backbone.View.extend({ // Instead of generating a new element, bind to the existing skeleton of // the App already present in the HTML. el: $("#todoapp"), // Our template for the line of statistics at the bottom of the app. statsTemplate: _.template($('#stats-template').html()), // Delegated events for creating new items, and clearing done ones. events: { "keypress #new-todo": "createOnEnter", "keyup #new-todo": "showTooltip", "click .todo-clear a": "clearCompleted" }, todos: [], // At initialization we bind to the relevant events on the `Todos` // collection, when items are added or changed. Kick things off by // loading any preexisting todos that might be saved in *localStorage*. initialize: function() { var self = this; this.input = this.$("#new-todo"); // spin up the live query. ignore the return value since we never // stop the query. Todos.findLive({}, { added: function (obj, before_idx) { // add a view node to the DOM var view = new TodoView({model: obj}); self.todos.splice(before_idx, 0, view); self.$("#todo-list").append(view.render().el); self.render(); }, removed: function (obj, at_idx) { // remove the view node from the DOM var view = self.todos.splice(at_idx, 1); view[0].remove(); self.render(); }, changed: function (obj, at_idx) { // update obj in existing view and rerender self.todos[at_idx].model = obj; self.todos[at_idx].render(); self.render(); }, moved: function (old_idx, new_idx) { // unimplemented -- items don't ever move }, sort: {'order': 1} }); }, // Re-rendering the App just means refreshing the statistics -- the rest // of the app doesn't change. render: function() { console.log("RENDER", all().length, all_done().length, all_remaining().length); this.$('#todo-stats').html(this.statsTemplate({ total: all().length, done: all_done().length, remaining: all_remaining().length })); }, // If you hit return in the main input field, and there is text to save, // create new **Todo** model. createOnEnter: function(e) { var text = this.input.val(); if (!text || e.keyCode != 13) return; Todos.insert({text: text, done: false, order: nextOrder()}); this.input.val(''); }, // Clear all done todo items, destroying their models. clearCompleted: function() { _.each(all_done(), function (todo) { Todos.remove(todo._id); }); return false; }, // Lazily show the tooltip that tells you to press `enter` to save // a new todo item, after one second. showTooltip: function(e) { var tooltip = this.$(".ui-tooltip-top"); var val = this.input.val(); tooltip.fadeOut(); if (this.tooltipTimeout) clearTimeout(this.tooltipTimeout); if (val == '' || val == this.input.attr('placeholder')) return; var show = function(){ tooltip.show().fadeIn(); }; this.tooltipTimeout = _.delay(show, 1000); } }); // Finally, we kick things off by creating the **App**. window.App = new AppView; }); ================================================ FILE: examples/unfinished/todos-backbone/common.js ================================================ Todos = new Mongo.Collection("todos"); //Todos.schema({text: String, done: Boolean, order: Number}); if (Meteor.isServer) { Meteor.publish('todos', function () { return Todos.find(); }); } ================================================ FILE: examples/unfinished/todos-backbone/todos.css ================================================ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } a img { border: none; } html { background: #eeeeee; } body { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.4em; background: #eeeeee; color: #333333; } #todoapp { width: 480px; margin: 0 auto 40px; background: white; padding: 20px; -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 5px 6px 0; -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 5px 6px 0; -o-box-shadow: rgba(0, 0, 0, 0.2) 0 5px 6px 0; box-shadow: rgba(0, 0, 0, 0.2) 0 5px 6px 0; } #todoapp h1 { font-size: 36px; font-weight: bold; text-align: center; padding: 20px 0 30px 0; line-height: 1; } #create-todo { position: relative; } #create-todo input { width: 466px; font-size: 24px; font-family: inherit; line-height: 1.4em; border: 0; outline: none; padding: 6px; border: 1px solid #999999; -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; -o-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; } #create-todo input::-webkit-input-placeholder { font-style: italic; } #create-todo span { position: absolute; z-index: 999; width: 170px; left: 50%; margin-left: -85px; } #todo-list { margin-top: 10px; } #todo-list li { padding: 12px 20px 11px 0; position: relative; font-size: 24px; line-height: 1.1em; border-bottom: 1px solid #cccccc; } #todo-list li:after { content: "\0020"; display: block; height: 0; clear: both; overflow: hidden; visibility: hidden; } #todo-list li.editing { padding: 0; border-bottom: 0; } #todo-list .editing .display, #todo-list .edit { display: none; } #todo-list .editing .edit { display: block; } #todo-list .editing input { width: 444px; font-size: 24px; font-family: inherit; margin: 0; line-height: 1.6em; border: 0; outline: none; padding: 10px 7px 0px 27px; border: 1px solid #999999; -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; -o-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; } #todo-list .check { position: relative; top: 9px; margin: 0 10px 0 7px; float: left; } #todo-list .done .todo-text { text-decoration: line-through; color: #777777; } #todo-list .todo-destroy { position: absolute; right: 5px; top: 14px; display: none; cursor: pointer; width: 20px; height: 20px; background: url(destroy.png) no-repeat 0 0; } #todo-list li:hover .todo-destroy { display: block; } #todo-list .todo-destroy:hover { background-position: 0 -20px; } #todo-stats { *zoom: 1; margin-top: 10px; color: #777777; } #todo-stats:after { content: "\0020"; display: block; height: 0; clear: both; overflow: hidden; visibility: hidden; } #todo-stats .todo-count { float: left; } #todo-stats .todo-count .number { font-weight: bold; color: #333333; } #todo-stats .todo-clear { float: right; } #todo-stats .todo-clear a { color: #777777; font-size: 12px; } #todo-stats .todo-clear a:visited { color: #777777; } #todo-stats .todo-clear a:hover { color: #336699; } #instructions { width: 520px; margin: 10px auto; color: #777777; text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0; text-align: center; } #instructions a { color: #336699; } #credits { width: 520px; margin: 30px auto; color: #999; text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0; text-align: center; } #credits a { color: #888; } /* * François 'cahnory' Germain */ .ui-tooltip, .ui-tooltip-top, .ui-tooltip-right, .ui-tooltip-bottom, .ui-tooltip-left { color:#ffffff; cursor:normal; display:-moz-inline-stack; display:inline-block; font-size:12px; font-family:arial; padding:.5em 1em; position:relative; text-align:center; text-shadow:0 -1px 1px #111111; -webkit-border-top-left-radius:4px ; -webkit-border-top-right-radius:4px ; -webkit-border-bottom-right-radius:4px ; -webkit-border-bottom-left-radius:4px ; -khtml-border-top-left-radius:4px ; -khtml-border-top-right-radius:4px ; -khtml-border-bottom-right-radius:4px ; -khtml-border-bottom-left-radius:4px ; -moz-border-radius-topleft:4px ; -moz-border-radius-topright:4px ; -moz-border-radius-bottomright:4px ; -moz-border-radius-bottomleft:4px ; border-top-left-radius:4px ; border-top-right-radius:4px ; border-bottom-right-radius:4px ; border-bottom-left-radius:4px ; -o-box-shadow:0 1px 2px #000000, inset 0 0 0 1px #222222, inset 0 2px #666666, inset 0 -2px 2px #444444; -moz-box-shadow:0 1px 2px #000000, inset 0 0 0 1px #222222, inset 0 2px #666666, inset 0 -2px 2px #444444; -khtml-box-shadow:0 1px 2px #000000, inset 0 0 0 1px #222222, inset 0 2px #666666, inset 0 -2px 2px #444444; -webkit-box-shadow:0 1px 2px #000000, inset 0 0 0 1px #222222, inset 0 2px #666666, inset 0 -2px 2px #444444; box-shadow:0 1px 2px #000000, inset 0 0 0 1px #222222, inset 0 2px #666666, inset 0 -2px 2px #444444; background-color:#3b3b3b; background-image:-moz-linear-gradient(top,#555555,#222222); background-image:-webkit-gradient(linear,left top,left bottom,color-stop(0,#555555),color-stop(1,#222222)); filter:progid:DXImageTransform.Microsoft.gradient(startColorStr=#555555,EndColorStr=#222222); -ms-filter:progid:DXImageTransform.Microsoft.gradient(startColorStr=#555555,EndColorStr=#222222); } .ui-tooltip:after, .ui-tooltip-top:after, .ui-tooltip-right:after, .ui-tooltip-bottom:after, .ui-tooltip-left:after { content:"\25B8"; display:block; font-size:2em; height:0; line-height:0; position:absolute; } .ui-tooltip:after, .ui-tooltip-bottom:after { color:#2a2a2a; bottom:0; left:1px; text-align:center; text-shadow:1px 0 2px #000000; -o-transform:rotate(90deg); -moz-transform:rotate(90deg); -khtml-transform:rotate(90deg); -webkit-transform:rotate(90deg); width:100%; } .ui-tooltip-top:after { bottom:auto; color:#4f4f4f; left:-2px; top:0; text-align:center; text-shadow:none; -o-transform:rotate(-90deg); -moz-transform:rotate(-90deg); -khtml-transform:rotate(-90deg); -webkit-transform:rotate(-90deg); width:100%; } .ui-tooltip-right:after { color:#222222; right:-0.375em; top:50%; margin-top:-.05em; text-shadow:0 1px 2px #000000; -o-transform:rotate(0); -moz-transform:rotate(0); -khtml-transform:rotate(0); -webkit-transform:rotate(0); } .ui-tooltip-left:after { color:#222222; left:-0.375em; top:50%; margin-top:.1em; text-shadow:0 -1px 2px #000000; -o-transform:rotate(180deg); -moz-transform:rotate(180deg); -khtml-transform:rotate(180deg); -webkit-transform:rotate(180deg); } ================================================ FILE: examples/unfinished/todos-underscore/.meteor/.gitignore ================================================ local ================================================ FILE: examples/unfinished/todos-underscore/.meteor/packages ================================================ # Meteor packages used by this project, one per line. # # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. jquery jquery-layout jquery-history standard-app-packages ================================================ FILE: examples/unfinished/todos-underscore/.meteor/release ================================================ 0.6.0 ================================================ FILE: examples/unfinished/todos-underscore/body.html ================================================ Todos

    To get started, create a new todo list in the left sidebar by typing its name in the text box. Select a list by clicking on its name, and rename by double clicking. The active list appears in the main window pane. You can do the usual here: add items, check them off as completed, and destroy items. You can also tag items with one or more tags, by clicking the blue Add new tag button to the right. All your in-use tags appear at the top. You can filter the list items by selecting a tag, or click the leftmost button to return to the full list.

    Inspired by Backbone's Todo Demo, with credit to Jérôme Gravel-Niquet.

    ================================================ FILE: examples/unfinished/todos-underscore/client/client.js ================================================ // quick jquery extension to bind text inputs to blur and RET. $.fn.onBlurOrEnter = function (callback) { this.bind('blur', callback); this.bind('keypress', function (evt) { if (evt.keyCode === 13 && $(this).val()) callback.call(this, evt); }); }; // everything else happens after DOM is ready $(function () { $('body').layout({north__minSize: 50, spacing_open: 10, north__fxSettings: { direction: "vertical" }}); // cache the template function for a single item. var item_template = _.template($('#item-template').html()); // this render function could be replaced with a handlebars // template. underscore template isn't safe for user-entered data // like the item text (XSS). function renderItem (obj) { // generate template for todo var elt = $(item_template(obj)); // set text through jquery for XSS protection elt.find('.todo-text').text(obj.text); // clicking the checkbox toggles done state elt.find('.check').click(function () { Todos.update(obj._id, {$set: {done: !obj.done}}); }); // clicking destroy button removes the item elt.find('.destroy').click(function () { Todos.remove(obj._id); }); // wire up tag destruction links elt.find('.tag .remove').click(function () { var tag = $(this).attr('name'); $(this).parent().fadeOut(500, function () { Todos.update(obj._id, {$pull: {tags: tag}}); }); }); // wire up add tag elt.find('.addtag').click(function () { $(this).hide(); elt.find('.edittag').show(); elt.find('.edittag input').focus(); }); // wire up edit tag elt.find('.edittag input').onBlurOrEnter(function () { elt.find('.edittag').hide(); elt.find('.addtag').show(); if ($(this).val() !== '') Todos.update(obj._id, {$addToSet: {tags: $(this).val()}}); }); // doubleclick on todo text brings up the editor elt.find('.todo-text').dblclick(function () { elt.addClass('editing'); var input = elt.find('.todo-input'); input.val(obj.text); input.focus(); input.select(); input.onBlurOrEnter(function () { elt.removeClass('editing'); if ($(this).val() !== '') Todos.update(obj._id, {$set: {text: elt.find('.todo-input').val()}}); }); }); return elt[0]; }; // construct new todo from text box $('#new-todo').bind('keypress', function (evt) { var list_id = Session.get('list_id'); var tag = Session.get('tag_filter'); // prevent creation of a new todo if nothing is selected if (!list_id) return; var text = $('#new-todo').val(); if (evt.keyCode === 13 && text) { var obj = {text: text, list_id: list_id, done: false, timestamp: (new Date()).getTime()}; if (tag) obj.tags = [tag]; Todos.insert(obj); $('#new-todo').val(''); } }); var current_list_stop; function setCurrentList (list_id) { Session.set('list_id', list_id); $('#items-view').show(); // kill current findLive render if (current_list_stop) current_list_stop.stop(); var query = {list_id: list_id}; if (Session.get('tag_filter')) query.tags = Session.get('tag_filter') // render individual todo list, stash kill function current_list_stop = Meteor.ui.renderList(Todos, $('#item-list'), { selector: query, sort: {timestamp: 1}, render: renderItem, events: {} }); }; // render list of lists in the left sidebar. Meteor.ui.renderList(Lists, $('#lists'), { sort: {name: 1}, template: $('#list-template'), events: { 'click': function (evt) { window.History.pushState({list_id: this._id}, "Todos: " + this.name, "/" + this._id); }, 'dblclick': function (evt) { var list_elt = $(evt.currentTarget); var input = list_elt.find('.list-name-input'); list_elt.addClass('editing'); input.val(this.name); input.focus(); input.select(); var _id = this._id; input.onBlurOrEnter(function () { list_elt.removeClass('editing'); if (input.val() !== '') Lists.update(_id, {$set: {name: input.val()}}); }); } } }); // construct new todo list from text box $('#new-list').bind('keypress', function (evt) { var text = $('#new-list').val(); if (evt.keyCode === 13 && text) { var list = Lists.insert({name: text}); $('#new-list').val(''); window.History.pushState({list_id: list._id}, "Todos: " + list.name, "/" + list._id); } }); // tags and filters // the tag filter bar is easy to generate using a simple // renderList() against a minimongo query. since minimongo doesn't // support aggregate queries, construct a local collection to serve // the same purpose, and drive the renderList() off of it. var LocalTags = new Mongo.Collection; (function () { function updateLocalTags() { var real = _(Todos.find()).chain().pluck('tags').compact().flatten().uniq().value(); real.unshift(null); // XXX fake tag var computed = _(LocalTags.find()).pluck('tag'); _.each(_.difference(real, computed), function (new_tag) { LocalTags.insert({tag: new_tag}); }); _.each(_.difference(computed, real), function (dead_tag) { LocalTags.remove({tag: dead_tag}); }); }; Todos.findLive({}, { added: function (obj, before_idx) { _.defer(updateLocalTags); }, removed: function (id, at_idx) { _.defer(updateLocalTags); }, changed: function (obj, at_idx) { _.defer(updateLocalTags); }, }); })(); // findLive() against the computed tag table. since we also want a // show-all button, arrange for the computed table to always include // a null placeholder tag, and for the template to render that as // "Show all". always begin the user session with a null filter. Session.set('tag_filter', null); Meteor.ui.renderList(LocalTags, $('#tag-filter'), { sort: {tag: 1}, template: $('#tag-filter-template'), events: { 'click': function (evt) { if (Session.equals('tag_filter', this.tag)) Session.set('tag_filter', null); else Session.set('tag_filter', this.tag); setCurrentList(Session.get('list_id')); } } }); // load list on statechange (which we drive from several places). window.History.Adapter.bind(window, 'statechange', function () { var state = window.History.getState(); var list = Lists.find(state.data.list_id); setCurrentList(list._id); }); // subscribe to all available todo lists. once the inital load // completes, navigate to the list specified by URL, if any. Meteor.subscribe('lists', function () { var initial_list_id = window.location.pathname.split('/')[1]; var list; if (initial_list_id) { list = Lists.find(initial_list_id); } else { var lists = Lists.find({}, {sort: {name: 1}, limit: 1}); list = lists[0]; } if (list) { window.History.replaceState({list_id: list._id}, "Todos: " + list.name, "/" + list._id); // replaceState doesn't always trigger statechange on reload. if // you last reloaded the same page and the state is the same, it // won't fire. so call this here. double calling is not great, but // OK. setCurrentList(list._id); } }); // subscribe to all the items in each list. no need for a callback // here: todo items are never queried using collection.find(). Meteor.subscribe('todos'); }); ================================================ FILE: examples/unfinished/todos-underscore/common.js ================================================ Lists = new Mongo.Collection("lists"); Todos = new Mongo.Collection("todos"); /* Schema support coming soon! Lists.schema({text: String}); Todos.schema({text: String, done: Boolean, tags: [String]}); */ if (Meteor.isServer) { Meteor.publish('lists', function () { return Lists.find(); }); Meteor.publish('todos', function () { return Todos.find(); }); } ================================================ FILE: examples/unfinished/todos-underscore/main.css ================================================ body { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.4em; background: #eeeeee; color: #333333; } .ui-layout-north { background: #dddddd; } #tag-filter { margin: 8px; } #items-view { display: none; margin: 10px; } #new-todo { width: 466px; font-size: 24px; font-family: inherit; line-height: 1.4em; border: 0; outline: none; padding: 6px; border: 1px solid #999999; margin-left: 75px; } .ui-layout-west { padding: 10px; border-right: solid 1px #cccccc; } .ui-layout-south { border-top: solid 1px black; padding: 10px; background: #cccccc; } #help p { margin: 8px; } .ui-layout-center { overflow: auto; } #lists .list { margin: 2px; font-weight: bold; } #lists .list-name .empty { font-size: 0.9em; font-style: italic; } #lists .editing .display, #lists .edit { display: none; } #lists .editing .edit { display: block; } #lists .editing input { font-family: inherit; margin: 0; line-height: 1.6em; border: 0; outline: none; padding: 10px 7px 0px 27px; border: 1px solid #999999; } #lists .selected { background-color: lightblue; } /* todo items */ #item-list { margin-top: 10px; } #item-list li { margin: 12px; font-size: 24px; line-height: 1.1em; border-bottom: 1px solid #cccccc; height: 50px; } #item-list li:after { content: "\0020"; display: block; height: 0; clear: both; overflow: hidden; visibility: hidden; } #item-list .destroy { float: left; width: 20px; height: 20px; cursor: pointer; margin-top: 12px; margin-left: 5px; } #item-list li:hover .destroy { background: url('/destroy.png') no-repeat 0 0; } #item-list li .destroy:hover { background-position: 0 -20px; } #item-list .display { float: left; margin: 9px; } #item-list .check { float: left; margin: 9px; } #item-list .edit { float: left; } #item-list .todo-text { float: left; } #item-list li.editing { padding: 0; } #item-list .editing .display, #item-list .edit { display: none; } #item-list .editing .edit { display: block; } #item-list .editing input { width: 444px; font-size: 24px; font-family: inherit; margin-left: 38px; line-height: 1.6em; border: 0; outline: none; border: 1px solid #999999; } #item-list .done .todo-text { text-decoration: line-through; color: #777777; } #item-list .item-tags { float: right; } /* tags */ .tag { float: left; color: black; background: #aaaaaa; font-size: 16px; font-weight: bold; cursor: pointer; border: 1px solid black; border-radius: 2px; -webkit-border-radius: 2px; -moz-border-radius: 2px; padding: 1px 3px 1px 3px; margin: 4px; } .tag.addtag { background: lightblue; border: 1px dashed black; } .tag.edittag { display: none; } .tag.selected { background: lightblue; } .tag .name { float: left; } .tag .remove { margin-top: 5px; margin-left: 5px; float: left; width: 16px; height: 16px; background-image: url("/close_16.png"); } ================================================ FILE: examples/unfinished/todos-underscore/reset.css ================================================ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } a img { border: none; } ================================================ FILE: examples/unfinished/todos-underscore/server/bootstrap.js ================================================ // if the database is empty on server start, create some sample data. Meteor.startup(function () { if (Lists.find().length === 0) { var list1 = Lists.insert({name: 'Things to do'}); Todos.insert({list_id: list1._id, text: 'Write Meteor app', tags: ['fun']}); Todos.insert({list_id: list1._id, text: 'Drink beer', tags: ['fun', 'yum']}); var list2 = Lists.insert({name: 'Places to see'}); Todos.insert({list_id: list2._id, text: 'San Francisco', tags: ['yum']}); Todos.insert({list_id: list2._id, text: 'Paris', tags: ['fun']}); Todos.insert({list_id: list2._id, text: 'Tokyo'}); var list3 = Lists.insert({name: 'People to meet'}); Todos.insert({list_id: list3._id, text: 'All the cool kids'}); } }); ================================================ FILE: meteor ================================================ #!/usr/bin/env bash BUNDLE_VERSION=0.6.15 # OS Check. Put here because here is where we download the precompiled # bundles that are arch specific. UNAME=$(uname) if [ "$UNAME" != "Linux" -a "$UNAME" != "Darwin" ] ; then if [ "$UNAME" != "FreeBSD" -a "$UNAME" != "OpenBSD" -a "$UNAME" != "NetBSD" ] ; then echo "Sorry, this OS is not supported." exit 1 fi fi # The METEOR_UNIVERSAL_FLAG will save the indicator how to handle unofficially # support environments. For armvXl boards we are support pre built binaries from # bintray. For all other systems we check, that there are system binries available # for node and mongo. If METEOR_UNIVERSAL_FLAG is not set, then this runs as same # as official meteor installer and starter METEOR_UNIVERSAL_FLAG= if [ "$UNAME" = "Darwin" ] ; then if [ "i386" != "$(uname -p)" -o "1" != "$(sysctl -n hw.cpu64bit_capable 2>/dev/null || echo 0)" ] ; then # Can't just test uname -m = x86_64, because Snow Leopard can # return other values. echo "Only 64-bit Intel processors are supported at this time." exit 1 fi ARCH="x86_64" elif [ "$UNAME" = "Linux" ] ; then ARCH="$(uname -m)" if [ "$ARCH" != "i686" -a "$ARCH" != "x86_64" ] ; then if [ "$ARCH" != "armv6l" -a "$ARCH" != "armv7l" ] ; then # set flag that we are in universal system environment support mode METEOR_UNIVERSAL_FLAG="env" else # set flag that we are in unofficial ARM support mode METEOR_UNIVERSAL_FLAG="arm" fi fi elif [ "$UNAME" = "FreeBSD" -o "$UNAME" = "OpenBSD" -o "$UNAME" = "NetBSD" ] ; then ARCH="$(uname -m)" if [ "$ARCH" != "i686" -a "$ARCH" != "x86_64" -a "$ARCH" != "amd64" ] ; then # set flag that we are in universal system environment support mode METEOR_UNIVERSAL_FLAG="env" else # set flag that we are in unofficial xBSD support mode METEOR_UNIVERSAL_FLAG="bsd" fi fi PLATFORM="${UNAME}_${ARCH}" # Find the script dir, following symlinks. Note that symlink can be relative or # absolute. Too bad 'readlink -f' and 'realpath' (the command-line program) are # not portable. We don't stress about infinite loops or bad links, because the # OS has already resolved this symlink chain once in order to actually run the # shell script. ORIG_DIR="$(pwd)" SCRIPT="$0" while true; do # The symlink might be relative, so we have to actually cd to the right place # each time in order to resolve it. cd "$(dirname "$SCRIPT")" if [ ! -L "$(basename "$SCRIPT")" ]; then SCRIPT_DIR="$(pwd -P)" break fi SCRIPT="$(readlink "$(basename "$SCRIPT")")" done cd "$ORIG_DIR" function install_dev_bundle { set -e trap "echo Failed to install dependency kit." EXIT TARBALL="dev_bundle_${PLATFORM}_${BUNDLE_VERSION}.tar.gz" BUNDLE_TMPDIR="$SCRIPT_DIR/dev_bundle.xxx" rm -rf "$BUNDLE_TMPDIR" mkdir "$BUNDLE_TMPDIR" # duplicated in scripts/windows/download-dev-bundle.ps1: DEV_BUNDLE_URL_ROOT="https://d3sqy0vbqsdhku.cloudfront.net/" # If you set $USE_TEST_DEV_BUNDLE_SERVER then we will download # dev bundles copied by copy-dev-bundle-from-jenkins.sh without --prod. # It still only does this if the version number has changed # (setting it won't cause it to automatically delete a prod dev bundle). if [ -n "$USE_TEST_DEV_BUNDLE_SERVER" ] ; then DEV_BUNDLE_URL_ROOT="https://s3.amazonaws.com/com.meteor.static/test/" fi # test for unofficial supported architecture based dev_bundles if [ -n "$METEOR_UNIVERSAL_FLAG" ] ; then # Use bintray release downloads DEV_BUNDLE_URL_ROOT="https://dl.bintray.com/4commerce-technologies-ag/meteor-universal/${METEOR_UNIVERSAL_FLAG}_dev_bundles/" fi # test if we can download the dev_bundle tarball function __check_tarball_download() { CHECK_TARBALL=$(curl --head --location --silent "$DEV_BUNDLE_URL_ROOT$TARBALL" --output /dev/null --write-out '%{http_code}') # Write down a message instead doing a false download if [ "$CHECK_TARBALL" != "200" ] ; then if [ -n "$METEOR_UNIVERSAL_FLAG" ] ; then # We have identified that there is no pre built dev_bundle available # and we do not provide yet downloadable pre builts for that platform echo "" echo "You are trying to run Meteor on yet not official supported platform: ${PLATFORM}" echo "There is currently no pre-built dev_bundle available for your system." echo "Check https://github.com/4commerce-technologies-AG/meteor to get" echo "the information how you may generate your own dev_bundle using" echo "scripts/generate-dev-bundle.sh" echo "" fi echo "Unable to download: $DEV_BUNDLE_URL_ROOT$TARBALL" echo "Pre-built tarball is not available!" echo "" exit 1 fi } if [ -f "$SCRIPT_DIR/$TARBALL" ] ; then echo "Skipping download and installing kit from $SCRIPT_DIR/$TARBALL" >&2 tar -xzf "$SCRIPT_DIR/$TARBALL" -C "$BUNDLE_TMPDIR" elif [ -n "$SAVE_DEV_BUNDLE_TARBALL" ] ; then # URL duplicated in tools/server/target.sh.in __check_tarball_download curl --location -# "$DEV_BUNDLE_URL_ROOT$TARBALL" >"$SCRIPT_DIR/$TARBALL" tar -xzf "$SCRIPT_DIR/$TARBALL" -C "$BUNDLE_TMPDIR" else __check_tarball_download curl --location -# "$DEV_BUNDLE_URL_ROOT$TARBALL" | tar -xzf - -C "$BUNDLE_TMPDIR" fi # In case that we have to use the universal system environment, make sure # that at least the necessary linked binaries are available and linked to # the correct system binaries. If detect a linked binary just set the link # again to the installed system one after unpacking if [ -L "$BUNDLE_TMPDIR/bin/node" ] ; then # test for system installed binaries if [ -z "$(which node 2>/dev/null)" -o -z "$(which npm 2>/dev/null)" ] ; then echo "Meteor officially supports only Linux i686 and x86_64 and OSX architecture for now." echo "Meteor universal unofficially supports arm, bsd and universal architectures as well." echo "To use the universal system environment make sure that the necessary binaries are pre-installed." echo "Please make sure that node (compatible to $NODE_VERSION) and npm is installed." echo -e "\tnode version:" $(which node 2>/dev/null) echo -e "\tnpm version:" $(which npm 2>/dev/null) exit 1 fi # remove the link from dev_bundle tgz and reset new to existing binary rm -f "$BUNDLE_TMPDIR/bin/node" rm -f "$BUNDLE_TMPDIR/bin/npm" ln -s "$(which node)" "$BUNDLE_TMPDIR/bin/node" ln -s "$(which npm)" "$BUNDLE_TMPDIR/bin/npm" fi if [ -L "$BUNDLE_TMPDIR/mongodb/bin/mongod" ] ; then # test for system installed binaries if [ -z "$(which mongo 2>/dev/null)" -o -z "$(which mongod 2>/dev/null)" ] ; then echo "Meteor officially supports only Linux i686 and x86_64 and OSX architecture for now." echo "Meteor universal unofficially supports arm, bsd and universal architectures as well." echo "To use the universal system environment make sure that the necessary binaries are pre-installed." echo "Please make sure that mongo and mongod is installed." echo -e "\tmongo version:" $(which mongo 2>/dev/null) echo -e "\tmongod version:" $(which mongod 2>/dev/null) exit 1 fi # remove the link from dev_bundle tgz and reset new to existing binary rm -f "$BUNDLE_TMPDIR/mongodb/bin/mongo" rm -f "$BUNDLE_TMPDIR/mongodb/bin/mongod" ln -s "$(which mongo 2>/dev/null)" "$BUNDLE_TMPDIR/mongodb/bin/mongo" ln -s "$(which mongod 2>/dev/null)" "$BUNDLE_TMPDIR/mongodb/bin/mongod" fi test -x "${BUNDLE_TMPDIR}/bin/node" # bomb out if it didn't work, eg no net # Delete old dev bundle and rename the new one on top of it. rm -rf "$SCRIPT_DIR/dev_bundle" mv "$BUNDLE_TMPDIR" "$SCRIPT_DIR/dev_bundle" echo "Installed dependency kit v${BUNDLE_VERSION} in dev_bundle." >&2 echo >&2 trap - EXIT set +e } if [ -d "$SCRIPT_DIR/.git" ] || [ -f "$SCRIPT_DIR/.git" ]; then # In a checkout. if [ ! -d "$SCRIPT_DIR/dev_bundle" ] ; then echo "It's the first time you've run Meteor from a git checkout." >&2 echo "I will download a kit containing all of Meteor's dependencies." >&2 install_dev_bundle elif [ ! -f "$SCRIPT_DIR/dev_bundle/.bundle_version.txt" ] || grep -qvx "$BUNDLE_VERSION" "$SCRIPT_DIR/dev_bundle/.bundle_version.txt" ; then echo "Your dependency kit is out of date. I will download the new one." >&2 install_dev_bundle fi export BABEL_CACHE_DIR="$SCRIPT_DIR/.babel-cache" fi DEV_BUNDLE="$SCRIPT_DIR/dev_bundle" METEOR="$SCRIPT_DIR/tools/index.js" # Bump our file descriptor ulimit as high as it will go. This is a # temporary workaround for dependancy watching holding open too many # files: https://app.asana.com/0/364581412985/472479912325 if [ "$(ulimit -n)" != "unlimited" ] ; then ulimit -n 16384 > /dev/null 2>&1 || \ ulimit -n 8192 > /dev/null 2>&1 || \ ulimit -n 4096 > /dev/null 2>&1 || \ ulimit -n 2048 > /dev/null 2>&1 || \ ulimit -n 1024 > /dev/null 2>&1 || \ ulimit -n 512 > /dev/null 2>&1 fi # set ENV VAR and include non-core packages per default if $PACKAGE_DIRS # is not set already if [ -z "$PACKAGE_DIRS" ] && [ -d "$SCRIPT_DIR/packages/non-core" ] ; then export PACKAGE_DIRS=$SCRIPT_DIR/packages/non-core fi # We used to set $NODE_PATH here to include the node_modules from the dev # bundle, but now we just get them from the symlink at tools/node_modules. This # is better because node_modules directories found via the ancestor walk from # the script take precedence over $NODE_PATH; it used to be that users would # screw up their meteor installs by have a ~/node_modules exec "$DEV_BUNDLE/bin/node" ${TOOL_NODE_FLAGS} "$METEOR" "$@" ================================================ FILE: meteor.bat ================================================ @echo off SETLOCAL rem only if we are running from a checkout IF EXIST "%~dp0\.git" ( rem verify that we have 7zip in the path 7z.exe --help > nul IF errorlevel 1 ( REM For some reason, without quotes this line causes an error echo "Please install 7z.exe (7-Zip) and put it into your PATH" exit /b 1 ) rem if dev_bundle is not present, get it IF NOT EXIST "%~dp0\dev_bundle" ( REM need `< con` so that we can run this file from Node REM (See http://stackoverflow.com/questions/9155289/calling-powershell-from-nodejs) PowerShell.exe -executionpolicy ByPass -file "%~dp0\scripts\windows\download-dev-bundle.ps1" < con ) rem if dev_bundle is the wrong version, remove it and get a new one PowerShell.exe -executionpolicy ByPass -file "%~dp0\scripts\windows\check-dev-bundle.ps1" < con IF errorlevel 1 ( rmdir /s /q "%~dp0\dev_bundle" IF EXIST "%~dp0\dev_bundle" ( echo Couldn't delete old dependency kit. Please try again. exit /b 1 ) PowerShell.exe -executionpolicy ByPass -file "%~dp0\scripts\windows\download-dev-bundle.ps1" < con ) rem Only set this when we're in a checkout. When running from a release, rem this is correctly set in the top-level `meteor.bat` file SET METEOR_INSTALLATION=%~dp0 ) SET NODE_PATH=%~dp0\dev_bundle\lib\node_modules SET BABEL_CACHE_DIR=%~dp0\.babel-cache "%~dp0\dev_bundle\bin\node.exe" "%~dp0\tools\index.js" %* ENDLOCAL EXIT /b %ERRORLEVEL% ================================================ FILE: packages/.gitignore ================================================ .meteor */.meteor ================================================ FILE: packages/accounts-base/.gitignore ================================================ .build* ================================================ FILE: packages/accounts-base/README.md ================================================ # accounts-base Meteor's user account system. This package implements the basic functions necessary for user accounts and lets other packages register login services. Some of these services are in the following packages: - `accounts-password` - `accounts-facebook` - `accounts-google` - `accounts-github` - `accounts-twitter` - `accounts-meetup` - `accounts-weibo` There are also login services available in community packages. For more information, see the [Meteor docs](http://docs.meteor.com/#accounts_api) and the Meteor Accounts [project page](https://www.meteor.com/accounts). ================================================ FILE: packages/accounts-base/accounts_client.js ================================================ import {AccountsCommon} from "./accounts_common.js"; /** * @summary Constructor for the `Accounts` object on the client. * @locus Client * @class AccountsClient * @extends AccountsCommon * @instancename accountsClient * @param {Object} options an object with fields: * @param {Object} options.connection Optional DDP connection to reuse. * @param {String} options.ddpUrl Optional URL for creating a new DDP connection. */ export class AccountsClient extends AccountsCommon { constructor(options) { super(options); this._loggingIn = false; this._loggingInDeps = new Tracker.Dependency; this._loginServicesHandle = this.connection.subscribe("meteor.loginServiceConfiguration"); this._pageLoadLoginCallbacks = []; this._pageLoadLoginAttemptInfo = null; // Defined in url_client.js. this._initUrlMatching(); // Defined in localstorage_token.js. this._initLocalStorage(); } /// /// CURRENT USER /// // @override userId() { return this.connection.userId(); } // This is mostly just called within this file, but Meteor.loginWithPassword // also uses it to make loggingIn() be true during the beginPasswordExchange // method call too. _setLoggingIn(x) { if (this._loggingIn !== x) { this._loggingIn = x; this._loggingInDeps.changed(); } } /** * @summary True if a login method (such as `Meteor.loginWithPassword`, `Meteor.loginWithFacebook`, or `Accounts.createUser`) is currently in progress. A reactive data source. * @locus Client */ loggingIn() { this._loggingInDeps.depend(); return this._loggingIn; } /** * @summary Log the user out. * @locus Client * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. */ logout(callback) { var self = this; self.connection.apply('logout', [], { wait: true }, function (error, result) { if (error) { callback && callback(error); } else { self.makeClientLoggedOut(); callback && callback(); } }); } /** * @summary Log out other clients logged in as the current user, but does not log out the client that calls this function. * @locus Client * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. */ logoutOtherClients(callback) { var self = this; // We need to make two method calls: one to replace our current token, // and another to remove all tokens except the current one. We want to // call these two methods one after the other, without any other // methods running between them. For example, we don't want `logout` // to be called in between our two method calls (otherwise the second // method call would return an error). Another example: we don't want // logout to be called before the callback for `getNewToken`; // otherwise we would momentarily log the user out and then write a // new token to localStorage. // // To accomplish this, we make both calls as wait methods, and queue // them one after the other, without spinning off the event loop in // between. Even though we queue `removeOtherTokens` before // `getNewToken`, we won't actually send the `removeOtherTokens` call // until the `getNewToken` callback has finished running, because they // are both wait methods. self.connection.apply( 'getNewToken', [], { wait: true }, function (err, result) { if (! err) { self._storeLoginToken( self.userId(), result.token, result.tokenExpires ); } } ); self.connection.apply( 'removeOtherTokens', [], { wait: true }, function (err) { callback && callback(err); } ); } }; var Ap = AccountsClient.prototype; /** * @summary True if a login method (such as `Meteor.loginWithPassword`, `Meteor.loginWithFacebook`, or `Accounts.createUser`) is currently in progress. A reactive data source. * @locus Client * @importFromPackage meteor */ Meteor.loggingIn = function () { return Accounts.loggingIn(); }; /// /// LOGIN METHODS /// // Call a login method on the server. // // A login method is a method which on success calls `this.setUserId(id)` and // `Accounts._setLoginToken` on the server and returns an object with fields // 'id' (containing the user id), 'token' (containing a resume token), and // optionally `tokenExpires`. // // This function takes care of: // - Updating the Meteor.loggingIn() reactive data source // - Calling the method in 'wait' mode // - On success, saving the resume token to localStorage // - On success, calling Accounts.connection.setUserId() // - Setting up an onReconnect handler which logs in with // the resume token // // Options: // - methodName: The method to call (default 'login') // - methodArguments: The arguments for the method // - validateResult: If provided, will be called with the result of the // method. If it throws, the client will not be logged in (and // its error will be passed to the callback). // - userCallback: Will be called with no arguments once the user is fully // logged in, or with the error on error. // Ap.callLoginMethod = function (options) { var self = this; options = _.extend({ methodName: 'login', methodArguments: [{}], _suppressLoggingIn: false }, options); // Set defaults for callback arguments to no-op functions; make sure we // override falsey values too. _.each(['validateResult', 'userCallback'], function (f) { if (!options[f]) options[f] = function () {}; }); // Prepare callbacks: user provided and onLogin/onLoginFailure hooks. var loginCallbacks = _.once(function (error) { if (!error) { self._onLoginHook.each(function (callback) { callback(); return true; }); } else { self._onLoginFailureHook.each(function (callback) { callback(); return true; }); } options.userCallback.apply(this, arguments); }); var reconnected = false; // We want to set up onReconnect as soon as we get a result token back from // the server, without having to wait for subscriptions to rerun. This is // because if we disconnect and reconnect between getting the result and // getting the results of subscription rerun, we WILL NOT re-send this // method (because we never re-send methods whose results we've received) // but we WILL call loggedInAndDataReadyCallback at "reconnect quiesce" // time. This will lead to makeClientLoggedIn(result.id) even though we // haven't actually sent a login method! // // But by making sure that we send this "resume" login in that case (and // calling makeClientLoggedOut if it fails), we'll end up with an accurate // client-side userId. (It's important that livedata_connection guarantees // that the "reconnect quiesce"-time call to loggedInAndDataReadyCallback // will occur before the callback from the resume login call.) var onResultReceived = function (err, result) { if (err || !result || !result.token) { // Leave onReconnect alone if there was an error, so that if the user was // already logged in they will still get logged in on reconnect. // See issue #4970. } else { self.connection.onReconnect = function () { reconnected = true; // If our token was updated in storage, use the latest one. var storedToken = self._storedLoginToken(); if (storedToken) { result = { token: storedToken, tokenExpires: self._storedLoginTokenExpires() }; } if (! result.tokenExpires) result.tokenExpires = self._tokenExpiration(new Date()); if (self._tokenExpiresSoon(result.tokenExpires)) { self.makeClientLoggedOut(); } else { self.callLoginMethod({ methodArguments: [{resume: result.token}], // Reconnect quiescence ensures that the user doesn't see an // intermediate state before the login method finishes. So we don't // need to show a logging-in animation. _suppressLoggingIn: true, userCallback: function (error) { var storedTokenNow = self._storedLoginToken(); if (error) { // If we had a login error AND the current stored token is the // one that we tried to log in with, then declare ourselves // logged out. If there's a token in storage but it's not the // token that we tried to log in with, we don't know anything // about whether that token is valid or not, so do nothing. The // periodic localStorage poll will decide if we are logged in or // out with this token, if it hasn't already. Of course, even // with this check, another tab could insert a new valid token // immediately before we clear localStorage here, which would // lead to both tabs being logged out, but by checking the token // in storage right now we hope to make that unlikely to happen. // // If there is no token in storage right now, we don't have to // do anything; whatever code removed the token from storage was // responsible for calling `makeClientLoggedOut()`, or the // periodic localStorage poll will call `makeClientLoggedOut` // eventually if another tab wiped the token from storage. if (storedTokenNow && storedTokenNow === result.token) { self.makeClientLoggedOut(); } } // Possibly a weird callback to call, but better than nothing if // there is a reconnect between "login result received" and "data // ready". loginCallbacks(error); }}); } }; } }; // This callback is called once the local cache of the current-user // subscription (and all subscriptions, in fact) are guaranteed to be up to // date. var loggedInAndDataReadyCallback = function (error, result) { // If the login method returns its result but the connection is lost // before the data is in the local cache, it'll set an onReconnect (see // above). The onReconnect will try to log in using the token, and *it* // will call userCallback via its own version of this // loggedInAndDataReadyCallback. So we don't have to do anything here. if (reconnected) return; // Note that we need to call this even if _suppressLoggingIn is true, // because it could be matching a _setLoggingIn(true) from a // half-completed pre-reconnect login method. self._setLoggingIn(false); if (error || !result) { error = error || new Error( "No result from call to " + options.methodName); loginCallbacks(error); return; } try { options.validateResult(result); } catch (e) { loginCallbacks(e); return; } // Make the client logged in. (The user data should already be loaded!) self.makeClientLoggedIn(result.id, result.token, result.tokenExpires); loginCallbacks(); }; if (!options._suppressLoggingIn) self._setLoggingIn(true); self.connection.apply( options.methodName, options.methodArguments, {wait: true, onResultReceived: onResultReceived}, loggedInAndDataReadyCallback); }; Ap.makeClientLoggedOut = function () { // Ensure client was successfully logged in before running logout hooks. if (this.connection._userId) { this._onLogoutHook.each(function (callback) { callback(); return true; }); } this._unstoreLoginToken(); this.connection.setUserId(null); this.connection.onReconnect = null; }; Ap.makeClientLoggedIn = function (userId, token, tokenExpires) { this._storeLoginToken(userId, token, tokenExpires); this.connection.setUserId(userId); }; /** * @summary Log the user out. * @locus Client * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. * @importFromPackage meteor */ Meteor.logout = function (callback) { return Accounts.logout(callback); }; /** * @summary Log out other clients logged in as the current user, but does not log out the client that calls this function. * @locus Client * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. * @importFromPackage meteor */ Meteor.logoutOtherClients = function (callback) { return Accounts.logoutOtherClients(callback); }; /// /// LOGIN SERVICES /// // A reactive function returning whether the loginServiceConfiguration // subscription is ready. Used by accounts-ui to hide the login button // until we have all the configuration loaded // Ap.loginServicesConfigured = function () { return this._loginServicesHandle.ready(); }; // Some login services such as the redirect login flow or the resume // login handler can log the user in at page load time. The // Meteor.loginWithX functions have a callback argument, but the // callback function instance won't be in memory any longer if the // page was reloaded. The `onPageLoadLogin` function allows a // callback to be registered for the case where the login was // initiated in a previous VM, and we now have the result of the login // attempt in a new VM. // Register a callback to be called if we have information about a // login attempt at page load time. Call the callback immediately if // we already have the page load login attempt info, otherwise stash // the callback to be called if and when we do get the attempt info. // Ap.onPageLoadLogin = function (f) { if (this._pageLoadLoginAttemptInfo) { f(this._pageLoadLoginAttemptInfo); } else { this._pageLoadLoginCallbacks.push(f); } }; // Receive the information about the login attempt at page load time. // Call registered callbacks, and also record the info in case // someone's callback hasn't been registered yet. // Ap._pageLoadLogin = function (attemptInfo) { if (this._pageLoadLoginAttemptInfo) { Meteor._debug("Ignoring unexpected duplicate page load login attempt info"); return; } _.each(this._pageLoadLoginCallbacks, function (callback) { callback(attemptInfo); }); this._pageLoadLoginCallbacks = []; this._pageLoadLoginAttemptInfo = attemptInfo; }; /// /// HANDLEBARS HELPERS /// // If our app has a Blaze, register the {{currentUser}} and {{loggingIn}} // global helpers. if (Package.blaze) { /** * @global * @name currentUser * @isHelper true * @summary Calls [Meteor.user()](#meteor_user). Use `{{#if currentUser}}` to check whether the user is logged in. */ Package.blaze.Blaze.Template.registerHelper('currentUser', function () { return Meteor.user(); }); /** * @global * @name loggingIn * @isHelper true * @summary Calls [Meteor.loggingIn()](#meteor_loggingin). */ Package.blaze.Blaze.Template.registerHelper('loggingIn', function () { return Meteor.loggingIn(); }); } ================================================ FILE: packages/accounts-base/accounts_common.js ================================================ /** * @summary Super-constructor for AccountsClient and AccountsServer. * @locus Anywhere * @class AccountsCommon * @instancename accountsClientOrServer * @param options {Object} an object with fields: * - connection {Object} Optional DDP connection to reuse. * - ddpUrl {String} Optional URL for creating a new DDP connection. */ export class AccountsCommon { constructor(options) { // Currently this is read directly by packages like accounts-password // and accounts-ui-unstyled. this._options = {}; // Note that setting this.connection = null causes this.users to be a // LocalCollection, which is not what we want. this.connection = undefined; this._initConnection(options || {}); // There is an allow call in accounts_server.js that restricts writes to // this collection. this.users = new Mongo.Collection("users", { _preventAutopublish: true, connection: this.connection }); // Callback exceptions are printed with Meteor._debug and ignored. this._onLoginHook = new Hook({ bindEnvironment: false, debugPrintExceptions: "onLogin callback" }); this._onLoginFailureHook = new Hook({ bindEnvironment: false, debugPrintExceptions: "onLoginFailure callback" }); this._onLogoutHook = new Hook({ bindEnvironment: false, debugPrintExceptions: "onLogout callback" }); } /** * @summary Get the current user id, or `null` if no user is logged in. A reactive data source. * @locus Anywhere but publish functions */ userId() { throw new Error("userId method not implemented"); } /** * @summary Get the current user record, or `null` if no user is logged in. A reactive data source. * @locus Anywhere but publish functions */ user() { var userId = this.userId(); return userId ? this.users.findOne(userId) : null; } // Set up config for the accounts system. Call this on both the client // and the server. // // Note that this method gets overridden on AccountsServer.prototype, but // the overriding method calls the overridden method. // // XXX we should add some enforcement that this is called on both the // client and the server. Otherwise, a user can // 'forbidClientAccountCreation' only on the client and while it looks // like their app is secure, the server will still accept createUser // calls. https://github.com/meteor/meteor/issues/828 // // @param options {Object} an object with fields: // - sendVerificationEmail {Boolean} // Send email address verification emails to new users created from // client signups. // - forbidClientAccountCreation {Boolean} // Do not allow clients to create accounts directly. // - restrictCreationByEmailDomain {Function or String} // Require created users to have an email matching the function or // having the string as domain. // - loginExpirationInDays {Number} // Number of days since login until a user is logged out (login token // expires). /** * @summary Set global accounts options. * @locus Anywhere * @param {Object} options * @param {Boolean} options.sendVerificationEmail New users with an email address will receive an address verification email. * @param {Boolean} options.forbidClientAccountCreation Calls to [`createUser`](#accounts_createuser) from the client will be rejected. In addition, if you are using [accounts-ui](#accountsui), the "Create account" link will not be available. * @param {String | Function} options.restrictCreationByEmailDomain If set to a string, only allows new users if the domain part of their email address matches the string. If set to a function, only allows new users if the function returns true. The function is passed the full email address of the proposed new user. Works with password-based sign-in and external services that expose email addresses (Google, Facebook, GitHub). All existing users still can log in after enabling this option. Example: `Accounts.config({ restrictCreationByEmailDomain: 'school.edu' })`. * @param {Number} options.loginExpirationInDays The number of days from when a user logs in until their token expires and they are logged out. Defaults to 90. Set to `null` to disable login expiration. * @param {String} options.oauthSecretKey When using the `oauth-encryption` package, the 16 byte key using to encrypt sensitive account credentials in the database, encoded in base64. This option may only be specifed on the server. See packages/oauth-encryption/README.md for details. */ config(options) { var self = this; // We don't want users to accidentally only call Accounts.config on the // client, where some of the options will have partial effects (eg removing // the "create account" button from accounts-ui if forbidClientAccountCreation // is set, or redirecting Google login to a specific-domain page) without // having their full effects. if (Meteor.isServer) { __meteor_runtime_config__.accountsConfigCalled = true; } else if (!__meteor_runtime_config__.accountsConfigCalled) { // XXX would be nice to "crash" the client and replace the UI with an error // message, but there's no trivial way to do this. Meteor._debug("Accounts.config was called on the client but not on the " + "server; some configuration options may not take effect."); } // We need to validate the oauthSecretKey option at the time // Accounts.config is called. We also deliberately don't store the // oauthSecretKey in Accounts._options. if (_.has(options, "oauthSecretKey")) { if (Meteor.isClient) throw new Error("The oauthSecretKey option may only be specified on the server"); if (! Package["oauth-encryption"]) throw new Error("The oauth-encryption package must be loaded to set oauthSecretKey"); Package["oauth-encryption"].OAuthEncryption.loadKey(options.oauthSecretKey); options = _.omit(options, "oauthSecretKey"); } // validate option keys var VALID_KEYS = ["sendVerificationEmail", "forbidClientAccountCreation", "restrictCreationByEmailDomain", "loginExpirationInDays"]; _.each(_.keys(options), function (key) { if (!_.contains(VALID_KEYS, key)) { throw new Error("Accounts.config: Invalid key: " + key); } }); // set values in Accounts._options _.each(VALID_KEYS, function (key) { if (key in options) { if (key in self._options) { throw new Error("Can't set `" + key + "` more than once"); } self._options[key] = options[key]; } }); } /** * @summary Register a callback to be called after a login attempt succeeds. * @locus Anywhere * @param {Function} func The callback to be called when login is successful. */ onLogin(func) { return this._onLoginHook.register(func); } /** * @summary Register a callback to be called after a login attempt fails. * @locus Anywhere * @param {Function} func The callback to be called after the login has failed. */ onLoginFailure(func) { return this._onLoginFailureHook.register(func); } /** * @summary Register a callback to be called after a logout attempt succeeds. * @locus Anywhere * @param {Function} func The callback to be called when logout is successful. */ onLogout(func) { return this._onLogoutHook.register(func); } _initConnection(options) { if (! Meteor.isClient) { return; } // The connection used by the Accounts system. This is the connection // that will get logged in by Meteor.login(), and this is the // connection whose login state will be reflected by Meteor.userId(). // // It would be much preferable for this to be in accounts_client.js, // but it has to be here because it's needed to create the // Meteor.users collection. if (options.connection) { this.connection = options.connection; } else if (options.ddpUrl) { this.connection = DDP.connect(options.ddpUrl); } else if (typeof __meteor_runtime_config__ !== "undefined" && __meteor_runtime_config__.ACCOUNTS_CONNECTION_URL) { // Temporary, internal hook to allow the server to point the client // to a different authentication server. This is for a very // particular use case that comes up when implementing a oauth // server. Unsupported and may go away at any point in time. // // We will eventually provide a general way to use account-base // against any DDP connection, not just one special one. this.connection = DDP.connect(__meteor_runtime_config__.ACCOUNTS_CONNECTION_URL); } else { this.connection = Meteor.connection; } } _getTokenLifetimeMs() { return (this._options.loginExpirationInDays || DEFAULT_LOGIN_EXPIRATION_DAYS) * 24 * 60 * 60 * 1000; } _tokenExpiration(when) { // We pass when through the Date constructor for backwards compatibility; // `when` used to be a number. return new Date((new Date(when)).getTime() + this._getTokenLifetimeMs()); } _tokenExpiresSoon(when) { var minLifetimeMs = .1 * this._getTokenLifetimeMs(); var minLifetimeCapMs = MIN_TOKEN_LIFETIME_CAP_SECS * 1000; if (minLifetimeMs > minLifetimeCapMs) minLifetimeMs = minLifetimeCapMs; return new Date() > (new Date(when) - minLifetimeMs); } } var Ap = AccountsCommon.prototype; // Note that Accounts is defined separately in accounts_client.js and // accounts_server.js. /** * @summary Get the current user id, or `null` if no user is logged in. A reactive data source. * @locus Anywhere but publish functions * @importFromPackage meteor */ Meteor.userId = function () { return Accounts.userId(); }; /** * @summary Get the current user record, or `null` if no user is logged in. A reactive data source. * @locus Anywhere but publish functions * @importFromPackage meteor */ Meteor.user = function () { return Accounts.user(); }; // how long (in days) until a login token expires var DEFAULT_LOGIN_EXPIRATION_DAYS = 90; // Clients don't try to auto-login with a token that is going to expire within // .1 * DEFAULT_LOGIN_EXPIRATION_DAYS, capped at MIN_TOKEN_LIFETIME_CAP_SECS. // Tries to avoid abrupt disconnects from expiring tokens. var MIN_TOKEN_LIFETIME_CAP_SECS = 3600; // one hour // how often (in milliseconds) we check for expired tokens EXPIRE_TOKENS_INTERVAL_MS = 600 * 1000; // 10 minutes // how long we wait before logging out clients when Meteor.logoutOtherClients is // called CONNECTION_CLOSE_DELAY_MS = 10 * 1000; // loginServiceConfiguration and ConfigError are maintained for backwards compatibility Meteor.startup(function () { var ServiceConfiguration = Package['service-configuration'].ServiceConfiguration; Ap.loginServiceConfiguration = ServiceConfiguration.configurations; Ap.ConfigError = ServiceConfiguration.ConfigError; }); // Thrown when the user cancels the login process (eg, closes an oauth // popup, declines retina scan, etc) var lceName = 'Accounts.LoginCancelledError'; Ap.LoginCancelledError = Meteor.makeErrorType( lceName, function (description) { this.message = description; } ); Ap.LoginCancelledError.prototype.name = lceName; // This is used to transmit specific subclass errors over the wire. We should // come up with a more generic way to do this (eg, with some sort of symbolic // error code rather than a number). Ap.LoginCancelledError.numericError = 0x8acdc2f; ================================================ FILE: packages/accounts-base/accounts_rate_limit.js ================================================ import {AccountsCommon} from "./accounts_common.js"; var Ap = AccountsCommon.prototype; var defaultRateLimiterRuleId; // Removes default rate limiting rule Ap.removeDefaultRateLimit = function () { const resp = DDPRateLimiter.removeRule(defaultRateLimiterRuleId); defaultRateLimiterRuleId = null; return resp; }; // Add a default rule of limiting logins, creating new users and password reset // to 5 times every 10 seconds per connection. Ap.addDefaultRateLimit = function () { if (!defaultRateLimiterRuleId) { defaultRateLimiterRuleId = DDPRateLimiter.addRule({ userId: null, clientAddress: null, type: 'method', name: function (name) { return _.contains(['login', 'createUser', 'resetPassword', 'forgotPassword'], name); }, connectionId: function (connectionId) { return true; } }, 5, 10000); } }; Ap.addDefaultRateLimit(); ================================================ FILE: packages/accounts-base/accounts_reconnect_tests.js ================================================ if (Meteor.isServer) { Meteor.methods({ getConnectionUserId: function() { return this.userId; } }); } if (Meteor.isClient) { Tinytest.addAsync('accounts - reconnect auto-login', function(test, done) { var username1 = 'testuser1-' + Random.id(); var username2 = 'testuser2-' + Random.id(); var password1 = 'password1-' + Random.id(); var password2 = 'password2-' + Random.id(); var timeoutHandle; var onLoginStopper; loginAsUser1(); function loginAsUser1() { Accounts.createUser({ username: username1, password: password1 }, onUser1LoggedIn); } function onUser1LoggedIn(err) { test.isUndefined(err, 'Unexpected error logging in as user1'); Accounts.createUser({ username: username2, password: password2 }, onUser2LoggedIn); } function onUser2LoggedIn(err) { test.isUndefined(err, 'Unexpected error logging in as user2'); onLoginStopper = Accounts.onLogin(onUser2LoggedInAfterReconnect); Meteor.disconnect(); Meteor.reconnect(); } function onUser2LoggedInAfterReconnect() { onLoginStopper.stop(); Meteor.loginWithPassword('non-existent-user', 'or-wrong-password', onFailedLogin); } function onFailedLogin(err) { test.instanceOf(err, Meteor.Error, 'No Meteor.Error on login failure'); onLoginStopper = Accounts.onLogin(onUser2LoggedInAfterReconnectAfterFailedLogin); Meteor.disconnect(); Meteor.reconnect(); timeoutHandle = Meteor.setTimeout(failTest, 1000); } function failTest() { onLoginStopper.stop(); test.fail('Issue #4970 has occured.'); Meteor.call('getConnectionUserId', checkFinalState); } function onUser2LoggedInAfterReconnectAfterFailedLogin() { onLoginStopper.stop(); Meteor.clearTimeout(timeoutHandle); Meteor.call('getConnectionUserId', checkFinalState); } function checkFinalState(err, connectionUserId) { test.isUndefined(err, 'Unexpected error calling getConnectionUserId'); test.equal(connectionUserId, Meteor.userId(), 'userId is different on client and server'); done(); } }); } ================================================ FILE: packages/accounts-base/accounts_server.js ================================================ var crypto = Npm.require('crypto'); import {AccountsCommon} from "./accounts_common.js"; /** * @summary Constructor for the `Accounts` namespace on the server. * @locus Server * @class AccountsServer * @extends AccountsCommon * @instancename accountsServer * @param {Object} server A server object such as `Meteor.server`. */ export class AccountsServer extends AccountsCommon { // Note that this constructor is less likely to be instantiated multiple // times than the `AccountsClient` constructor, because a single server // can provide only one set of methods. constructor(server) { super(); this._server = server || Meteor.server; // Set up the server's methods, as if by calling Meteor.methods. this._initServerMethods(); this._initAccountDataHooks(); // If autopublish is on, publish these user fields. Login service // packages (eg accounts-google) add to these by calling // addAutopublishFields. Notably, this isn't implemented with multiple // publishes since DDP only merges only across top-level fields, not // subfields (such as 'services.facebook.accessToken') this._autopublishFields = { loggedInUser: ['profile', 'username', 'emails'], otherUsers: ['profile', 'username'] }; this._initServerPublications(); // connectionId -> {connection, loginToken} this._accountData = {}; // connection id -> observe handle for the login token that this connection is // currently associated with, or a number. The number indicates that we are in // the process of setting up the observe (using a number instead of a single // sentinel allows multiple attempts to set up the observe to identify which // one was theirs). this._userObservesForConnections = {}; this._nextUserObserveNumber = 1; // for the number described above. // list of all registered handlers. this._loginHandlers = []; setupUsersCollection(this.users); setupDefaultLoginHandlers(this); setExpireTokensInterval(this); this._validateLoginHook = new Hook({ bindEnvironment: false }); this._validateNewUserHooks = [ defaultValidateNewUserHook.bind(this) ]; this._deleteSavedTokensForAllUsersOnStartup(); this._skipCaseInsensitiveChecksForTest = {}; } /// /// CURRENT USER /// // @override of "abstract" non-implementation in accounts_common.js userId() { // This function only works if called inside a method. In theory, it // could also be called from publish statements, since they also // have a userId associated with them. However, given that publish // functions aren't reactive, using any of the infomation from // Meteor.user() in a publish function will always use the value // from when the function first runs. This is likely not what the // user expects. The way to make this work in a publish is to do // Meteor.find(this.userId).observe and recompute when the user // record changes. var currentInvocation = DDP._CurrentInvocation.get(); if (!currentInvocation) throw new Error("Meteor.userId can only be invoked in method calls. Use this.userId in publish functions."); return currentInvocation.userId; } /// /// LOGIN HOOKS /// /** * @summary Validate login attempts. * @locus Server * @param {Function} func Called whenever a login is attempted (either successful or unsuccessful). A login can be aborted by returning a falsy value or throwing an exception. */ validateLoginAttempt(func) { // Exceptions inside the hook callback are passed up to us. return this._validateLoginHook.register(func); } /** * @summary Set restrictions on new user creation. * @locus Server * @param {Function} func Called whenever a new user is created. Takes the new user object, and returns true to allow the creation or false to abort. */ validateNewUser(func) { this._validateNewUserHooks.push(func); } /// /// CREATE USER HOOKS /// /** * @summary Customize new user creation. * @locus Server * @param {Function} func Called whenever a new user is created. Return the new user object, or throw an `Error` to abort the creation. */ onCreateUser(func) { if (this._onCreateUserHook) { throw new Error("Can only call onCreateUser once"); } this._onCreateUserHook = func; } }; var Ap = AccountsServer.prototype; // Give each login hook callback a fresh cloned copy of the attempt // object, but don't clone the connection. // function cloneAttemptWithConnection(connection, attempt) { var clonedAttempt = EJSON.clone(attempt); clonedAttempt.connection = connection; return clonedAttempt; } Ap._validateLogin = function (connection, attempt) { this._validateLoginHook.each(function (callback) { var ret; try { ret = callback(cloneAttemptWithConnection(connection, attempt)); } catch (e) { attempt.allowed = false; // XXX this means the last thrown error overrides previous error // messages. Maybe this is surprising to users and we should make // overriding errors more explicit. (see // https://github.com/meteor/meteor/issues/1960) attempt.error = e; return true; } if (! ret) { attempt.allowed = false; // don't override a specific error provided by a previous // validator or the initial attempt (eg "incorrect password"). if (!attempt.error) attempt.error = new Meteor.Error(403, "Login forbidden"); } return true; }); }; Ap._successfulLogin = function (connection, attempt) { this._onLoginHook.each(function (callback) { callback(cloneAttemptWithConnection(connection, attempt)); return true; }); }; Ap._failedLogin = function (connection, attempt) { this._onLoginFailureHook.each(function (callback) { callback(cloneAttemptWithConnection(connection, attempt)); return true; }); }; Ap._successfulLogout = function () { this._onLogoutHook.each(function (callback) { callback(); return true; }); }; /// /// LOGIN METHODS /// // Login methods return to the client an object containing these // fields when the user was logged in successfully: // // id: userId // token: * // tokenExpires: * // // tokenExpires is optional and intends to provide a hint to the // client as to when the token will expire. If not provided, the // client will call Accounts._tokenExpiration, passing it the date // that it received the token. // // The login method will throw an error back to the client if the user // failed to log in. // // // Login handlers and service specific login methods such as // `createUser` internally return a `result` object containing these // fields: // // type: // optional string; the service name, overrides the handler // default if present. // // error: // exception; if the user is not allowed to login, the reason why. // // userId: // string; the user id of the user attempting to login (if // known), required for an allowed login. // // options: // optional object merged into the result returned by the login // method; used by HAMK from SRP. // // stampedLoginToken: // optional object with `token` and `when` indicating the login // token is already present in the database, returned by the // "resume" login handler. // // For convenience, login methods can also throw an exception, which // is converted into an {error} result. However, if the id of the // user attempting the login is known, a {userId, error} result should // be returned instead since the user id is not captured when an // exception is thrown. // // This internal `result` object is automatically converted into the // public {id, token, tokenExpires} object returned to the client. // Try a login method, converting thrown exceptions into an {error} // result. The `type` argument is a default, inserted into the result // object if not explicitly returned. // var tryLoginMethod = function (type, fn) { var result; try { result = fn(); } catch (e) { result = {error: e}; } if (result && !result.type && type) result.type = type; return result; }; // Log in a user on a connection. // // We use the method invocation to set the user id on the connection, // not the connection object directly. setUserId is tied to methods to // enforce clear ordering of method application (using wait methods on // the client, and a no setUserId after unblock restriction on the // server) // // The `stampedLoginToken` parameter is optional. When present, it // indicates that the login token has already been inserted into the // database and doesn't need to be inserted again. (It's used by the // "resume" login handler). Ap._loginUser = function (methodInvocation, userId, stampedLoginToken) { var self = this; if (! stampedLoginToken) { stampedLoginToken = self._generateStampedLoginToken(); self._insertLoginToken(userId, stampedLoginToken); } // This order (and the avoidance of yields) is important to make // sure that when publish functions are rerun, they see a // consistent view of the world: the userId is set and matches // the login token on the connection (not that there is // currently a public API for reading the login token on a // connection). Meteor._noYieldsAllowed(function () { self._setLoginToken( userId, methodInvocation.connection, self._hashLoginToken(stampedLoginToken.token) ); }); methodInvocation.setUserId(userId); return { id: userId, token: stampedLoginToken.token, tokenExpires: self._tokenExpiration(stampedLoginToken.when) }; }; // After a login method has completed, call the login hooks. Note // that `attemptLogin` is called for *all* login attempts, even ones // which aren't successful (such as an invalid password, etc). // // If the login is allowed and isn't aborted by a validate login hook // callback, log in the user. // Ap._attemptLogin = function ( methodInvocation, methodName, methodArgs, result ) { if (!result) throw new Error("result is required"); // XXX A programming error in a login handler can lead to this occuring, and // then we don't call onLogin or onLoginFailure callbacks. Should // tryLoginMethod catch this case and turn it into an error? if (!result.userId && !result.error) throw new Error("A login method must specify a userId or an error"); var user; if (result.userId) user = this.users.findOne(result.userId); var attempt = { type: result.type || "unknown", allowed: !! (result.userId && !result.error), methodName: methodName, methodArguments: _.toArray(methodArgs) }; if (result.error) attempt.error = result.error; if (user) attempt.user = user; // _validateLogin may mutate `attempt` by adding an error and changing allowed // to false, but that's the only change it can make (and the user's callbacks // only get a clone of `attempt`). this._validateLogin(methodInvocation.connection, attempt); if (attempt.allowed) { var ret = _.extend( this._loginUser( methodInvocation, result.userId, result.stampedLoginToken ), result.options || {} ); this._successfulLogin(methodInvocation.connection, attempt); return ret; } else { this._failedLogin(methodInvocation.connection, attempt); throw attempt.error; } }; // All service specific login methods should go through this function. // Ensure that thrown exceptions are caught and that login hook // callbacks are still called. // Ap._loginMethod = function ( methodInvocation, methodName, methodArgs, type, fn ) { return this._attemptLogin( methodInvocation, methodName, methodArgs, tryLoginMethod(type, fn) ); }; // Report a login attempt failed outside the context of a normal login // method. This is for use in the case where there is a multi-step login // procedure (eg SRP based password login). If a method early in the // chain fails, it should call this function to report a failure. There // is no corresponding method for a successful login; methods that can // succeed at logging a user in should always be actual login methods // (using either Accounts._loginMethod or Accounts.registerLoginHandler). Ap._reportLoginFailure = function ( methodInvocation, methodName, methodArgs, result ) { var attempt = { type: result.type || "unknown", allowed: false, error: result.error, methodName: methodName, methodArguments: _.toArray(methodArgs) }; if (result.userId) { attempt.user = this.users.findOne(result.userId); } this._validateLogin(methodInvocation.connection, attempt); this._failedLogin(methodInvocation.connection, attempt); // _validateLogin may mutate attempt to set a new error message. Return // the modified version. return attempt; }; /// /// LOGIN HANDLERS /// // The main entry point for auth packages to hook in to login. // // A login handler is a login method which can return `undefined` to // indicate that the login request is not handled by this handler. // // @param name {String} Optional. The service name, used by default // if a specific service name isn't returned in the result. // // @param handler {Function} A function that receives an options object // (as passed as an argument to the `login` method) and returns one of: // - `undefined`, meaning don't handle; // - a login method result object Ap.registerLoginHandler = function (name, handler) { if (! handler) { handler = name; name = null; } this._loginHandlers.push({ name: name, handler: handler }); }; // Checks a user's credentials against all the registered login // handlers, and returns a login token if the credentials are valid. It // is like the login method, except that it doesn't set the logged-in // user on the connection. Throws a Meteor.Error if logging in fails, // including the case where none of the login handlers handled the login // request. Otherwise, returns {id: userId, token: *, tokenExpires: *}. // // For example, if you want to login with a plaintext password, `options` could be // { user: { username: }, password: }, or // { user: { email: }, password: }. // Try all of the registered login handlers until one of them doesn't // return `undefined`, meaning it handled this call to `login`. Return // that return value. Ap._runLoginHandlers = function (methodInvocation, options) { for (var i = 0; i < this._loginHandlers.length; ++i) { var handler = this._loginHandlers[i]; var result = tryLoginMethod( handler.name, function () { return handler.handler.call(methodInvocation, options); } ); if (result) { return result; } if (result !== undefined) { throw new Meteor.Error(400, "A login handler should return a result or undefined"); } } return { type: null, error: new Meteor.Error(400, "Unrecognized options for login request") }; }; // Deletes the given loginToken from the database. // // For new-style hashed token, this will cause all connections // associated with the token to be closed. // // Any connections associated with old-style unhashed tokens will be // in the process of becoming associated with hashed tokens and then // they'll get closed. Ap.destroyToken = function (userId, loginToken) { this.users.update(userId, { $pull: { "services.resume.loginTokens": { $or: [ { hashedToken: loginToken }, { token: loginToken } ] } } }); }; Ap._initServerMethods = function () { // The methods created in this function need to be created here so that // this variable is available in their scope. var accounts = this; // This object will be populated with methods and then passed to // accounts._server.methods further below. var methods = {}; // @returns {Object|null} // If successful, returns {token: reconnectToken, id: userId} // If unsuccessful (for example, if the user closed the oauth login popup), // throws an error describing the reason methods.login = function (options) { var self = this; // Login handlers should really also check whatever field they look at in // options, but we don't enforce it. check(options, Object); var result = accounts._runLoginHandlers(self, options); return accounts._attemptLogin(self, "login", arguments, result); }; methods.logout = function () { var token = accounts._getLoginToken(this.connection.id); accounts._setLoginToken(this.userId, this.connection, null); if (token && this.userId) accounts.destroyToken(this.userId, token); this.setUserId(null); accounts._successfulLogout(); }; // Delete all the current user's tokens and close all open connections logged // in as this user. Returns a fresh new login token that this client can // use. Tests set Accounts._noConnectionCloseDelayForTest to delete tokens // immediately instead of using a delay. // // XXX COMPAT WITH 0.7.2 // This single `logoutOtherClients` method has been replaced with two // methods, one that you call to get a new token, and another that you // call to remove all tokens except your own. The new design allows // clients to know when other clients have actually been logged // out. (The `logoutOtherClients` method guarantees the caller that // the other clients will be logged out at some point, but makes no // guarantees about when.) This method is left in for backwards // compatibility, especially since application code might be calling // this method directly. // // @returns {Object} Object with token and tokenExpires keys. methods.logoutOtherClients = function () { var self = this; var user = accounts.users.findOne(self.userId, { fields: { "services.resume.loginTokens": true } }); if (user) { // Save the current tokens in the database to be deleted in // CONNECTION_CLOSE_DELAY_MS ms. This gives other connections in the // caller's browser time to find the fresh token in localStorage. We save // the tokens in the database in case we crash before actually deleting // them. var tokens = user.services.resume.loginTokens; var newToken = accounts._generateStampedLoginToken(); var userId = self.userId; accounts.users.update(userId, { $set: { "services.resume.loginTokensToDelete": tokens, "services.resume.haveLoginTokensToDelete": true }, $push: { "services.resume.loginTokens": accounts._hashStampedToken(newToken) } }); Meteor.setTimeout(function () { // The observe on Meteor.users will take care of closing the connections // associated with `tokens`. accounts._deleteSavedTokensForUser(userId, tokens); }, accounts._noConnectionCloseDelayForTest ? 0 : CONNECTION_CLOSE_DELAY_MS); // We do not set the login token on this connection, but instead the // observe closes the connection and the client will reconnect with the // new token. return { token: newToken.token, tokenExpires: accounts._tokenExpiration(newToken.when) }; } else { throw new Meteor.Error("You are not logged in."); } }; // Generates a new login token with the same expiration as the // connection's current token and saves it to the database. Associates // the connection with this new token and returns it. Throws an error // if called on a connection that isn't logged in. // // @returns Object // If successful, returns { token: , id: , // tokenExpires: }. methods.getNewToken = function () { var self = this; var user = accounts.users.findOne(self.userId, { fields: { "services.resume.loginTokens": 1 } }); if (! self.userId || ! user) { throw new Meteor.Error("You are not logged in."); } // Be careful not to generate a new token that has a later // expiration than the curren token. Otherwise, a bad guy with a // stolen token could use this method to stop his stolen token from // ever expiring. var currentHashedToken = accounts._getLoginToken(self.connection.id); var currentStampedToken = _.find( user.services.resume.loginTokens, function (stampedToken) { return stampedToken.hashedToken === currentHashedToken; } ); if (! currentStampedToken) { // safety belt: this should never happen throw new Meteor.Error("Invalid login token"); } var newStampedToken = accounts._generateStampedLoginToken(); newStampedToken.when = currentStampedToken.when; accounts._insertLoginToken(self.userId, newStampedToken); return accounts._loginUser(self, self.userId, newStampedToken); }; // Removes all tokens except the token associated with the current // connection. Throws an error if the connection is not logged // in. Returns nothing on success. methods.removeOtherTokens = function () { var self = this; if (! self.userId) { throw new Meteor.Error("You are not logged in."); } var currentToken = accounts._getLoginToken(self.connection.id); accounts.users.update(self.userId, { $pull: { "services.resume.loginTokens": { hashedToken: { $ne: currentToken } } } }); }; // Allow a one-time configuration for a login service. Modifications // to this collection are also allowed in insecure mode. methods.configureLoginService = function (options) { check(options, Match.ObjectIncluding({service: String})); // Don't let random users configure a service we haven't added yet (so // that when we do later add it, it's set up with their configuration // instead of ours). // XXX if service configuration is oauth-specific then this code should // be in accounts-oauth; if it's not then the registry should be // in this package if (!(accounts.oauth && _.contains(accounts.oauth.serviceNames(), options.service))) { throw new Meteor.Error(403, "Service unknown"); } var ServiceConfiguration = Package['service-configuration'].ServiceConfiguration; if (ServiceConfiguration.configurations.findOne({service: options.service})) throw new Meteor.Error(403, "Service " + options.service + " already configured"); if (_.has(options, "secret") && usingOAuthEncryption()) options.secret = OAuthEncryption.seal(options.secret); ServiceConfiguration.configurations.insert(options); }; accounts._server.methods(methods); }; Ap._initAccountDataHooks = function () { var accounts = this; accounts._server.onConnection(function (connection) { accounts._accountData[connection.id] = { connection: connection }; connection.onClose(function () { accounts._removeTokenFromConnection(connection.id); delete accounts._accountData[connection.id]; }); }); }; Ap._initServerPublications = function () { var accounts = this; // Publish all login service configuration fields other than secret. accounts._server.publish("meteor.loginServiceConfiguration", function () { var ServiceConfiguration = Package['service-configuration'].ServiceConfiguration; return ServiceConfiguration.configurations.find({}, {fields: {secret: 0}}); }, {is_auto: true}); // not techincally autopublish, but stops the warning. // Publish the current user's record to the client. accounts._server.publish(null, function () { if (this.userId) { return accounts.users.find({ _id: this.userId }, { fields: { profile: 1, username: 1, emails: 1 } }); } else { return null; } }, /*suppress autopublish warning*/{is_auto: true}); // Use Meteor.startup to give other packages a chance to call // addAutopublishFields. Package.autopublish && Meteor.startup(function () { // ['profile', 'username'] -> {profile: 1, username: 1} var toFieldSelector = function (fields) { return _.object(_.map(fields, function (field) { return [field, 1]; })); }; accounts._server.publish(null, function () { if (this.userId) { return accounts.users.find({ _id: this.userId }, { fields: toFieldSelector(accounts._autopublishFields.loggedInUser) }); } else { return null; } }, /*suppress autopublish warning*/{is_auto: true}); // XXX this publish is neither dedup-able nor is it optimized by our special // treatment of queries on a specific _id. Therefore this will have O(n^2) // run-time performance every time a user document is changed (eg someone // logging in). If this is a problem, we can instead write a manual publish // function which filters out fields based on 'this.userId'. accounts._server.publish(null, function () { var selector = this.userId ? { _id: { $ne: this.userId } } : {}; return accounts.users.find(selector, { fields: toFieldSelector(accounts._autopublishFields.otherUsers) }); }, /*suppress autopublish warning*/{is_auto: true}); }); }; // Add to the list of fields or subfields to be automatically // published if autopublish is on. Must be called from top-level // code (ie, before Meteor.startup hooks run). // // @param opts {Object} with: // - forLoggedInUser {Array} Array of fields published to the logged-in user // - forOtherUsers {Array} Array of fields published to users that aren't logged in Ap.addAutopublishFields = function (opts) { this._autopublishFields.loggedInUser.push.apply( this._autopublishFields.loggedInUser, opts.forLoggedInUser); this._autopublishFields.otherUsers.push.apply( this._autopublishFields.otherUsers, opts.forOtherUsers); }; /// /// ACCOUNT DATA /// // HACK: This is used by 'meteor-accounts' to get the loginToken for a // connection. Maybe there should be a public way to do that. Ap._getAccountData = function (connectionId, field) { var data = this._accountData[connectionId]; return data && data[field]; }; Ap._setAccountData = function (connectionId, field, value) { var data = this._accountData[connectionId]; // safety belt. shouldn't happen. accountData is set in onConnection, // we don't have a connectionId until it is set. if (!data) return; if (value === undefined) delete data[field]; else data[field] = value; }; /// /// RECONNECT TOKENS /// /// support reconnecting using a meteor login token Ap._hashLoginToken = function (loginToken) { var hash = crypto.createHash('sha256'); hash.update(loginToken); return hash.digest('base64'); }; // {token, when} => {hashedToken, when} Ap._hashStampedToken = function (stampedToken) { return _.extend(_.omit(stampedToken, 'token'), { hashedToken: this._hashLoginToken(stampedToken.token) }); }; // Using $addToSet avoids getting an index error if another client // logging in simultaneously has already inserted the new hashed // token. Ap._insertHashedLoginToken = function (userId, hashedToken, query) { query = query ? _.clone(query) : {}; query._id = userId; this.users.update(query, { $addToSet: { "services.resume.loginTokens": hashedToken } }); }; // Exported for tests. Ap._insertLoginToken = function (userId, stampedToken, query) { this._insertHashedLoginToken( userId, this._hashStampedToken(stampedToken), query ); }; Ap._clearAllLoginTokens = function (userId) { this.users.update(userId, { $set: { 'services.resume.loginTokens': [] } }); }; // test hook Ap._getUserObserve = function (connectionId) { return this._userObservesForConnections[connectionId]; }; // Clean up this connection's association with the token: that is, stop // the observe that we started when we associated the connection with // this token. Ap._removeTokenFromConnection = function (connectionId) { if (_.has(this._userObservesForConnections, connectionId)) { var observe = this._userObservesForConnections[connectionId]; if (typeof observe === 'number') { // We're in the process of setting up an observe for this connection. We // can't clean up that observe yet, but if we delete the placeholder for // this connection, then the observe will get cleaned up as soon as it has // been set up. delete this._userObservesForConnections[connectionId]; } else { delete this._userObservesForConnections[connectionId]; observe.stop(); } } }; Ap._getLoginToken = function (connectionId) { return this._getAccountData(connectionId, 'loginToken'); }; // newToken is a hashed token. Ap._setLoginToken = function (userId, connection, newToken) { var self = this; self._removeTokenFromConnection(connection.id); self._setAccountData(connection.id, 'loginToken', newToken); if (newToken) { // Set up an observe for this token. If the token goes away, we need // to close the connection. We defer the observe because there's // no need for it to be on the critical path for login; we just need // to ensure that the connection will get closed at some point if // the token gets deleted. // // Initially, we set the observe for this connection to a number; this // signifies to other code (which might run while we yield) that we are in // the process of setting up an observe for this connection. Once the // observe is ready to go, we replace the number with the real observe // handle (unless the placeholder has been deleted or replaced by a // different placehold number, signifying that the connection was closed // already -- in this case we just clean up the observe that we started). var myObserveNumber = ++self._nextUserObserveNumber; self._userObservesForConnections[connection.id] = myObserveNumber; Meteor.defer(function () { // If something else happened on this connection in the meantime (it got // closed, or another call to _setLoginToken happened), just do // nothing. We don't need to start an observe for an old connection or old // token. if (self._userObservesForConnections[connection.id] !== myObserveNumber) { return; } var foundMatchingUser; // Because we upgrade unhashed login tokens to hashed tokens at // login time, sessions will only be logged in with a hashed // token. Thus we only need to observe hashed tokens here. var observe = self.users.find({ _id: userId, 'services.resume.loginTokens.hashedToken': newToken }, { fields: { _id: 1 } }).observeChanges({ added: function () { foundMatchingUser = true; }, removed: function () { connection.close(); // The onClose callback for the connection takes care of // cleaning up the observe handle and any other state we have // lying around. } }); // If the user ran another login or logout command we were waiting for the // defer or added to fire (ie, another call to _setLoginToken occurred), // then we let the later one win (start an observe, etc) and just stop our // observe now. // // Similarly, if the connection was already closed, then the onClose // callback would have called _removeTokenFromConnection and there won't // be an entry in _userObservesForConnections. We can stop the observe. if (self._userObservesForConnections[connection.id] !== myObserveNumber) { observe.stop(); return; } self._userObservesForConnections[connection.id] = observe; if (! foundMatchingUser) { // We've set up an observe on the user associated with `newToken`, // so if the new token is removed from the database, we'll close // the connection. But the token might have already been deleted // before we set up the observe, which wouldn't have closed the // connection because the observe wasn't running yet. connection.close(); } }); } }; function setupDefaultLoginHandlers(accounts) { accounts.registerLoginHandler("resume", function (options) { return defaultResumeLoginHandler.call(this, accounts, options); }); } // Login handler for resume tokens. function defaultResumeLoginHandler(accounts, options) { if (!options.resume) return undefined; check(options.resume, String); var hashedToken = accounts._hashLoginToken(options.resume); // First look for just the new-style hashed login token, to avoid // sending the unhashed token to the database in a query if we don't // need to. var user = accounts.users.findOne( {"services.resume.loginTokens.hashedToken": hashedToken}); if (! user) { // If we didn't find the hashed login token, try also looking for // the old-style unhashed token. But we need to look for either // the old-style token OR the new-style token, because another // client connection logging in simultaneously might have already // converted the token. user = accounts.users.findOne({ $or: [ {"services.resume.loginTokens.hashedToken": hashedToken}, {"services.resume.loginTokens.token": options.resume} ] }); } if (! user) return { error: new Meteor.Error(403, "You've been logged out by the server. Please log in again.") }; // Find the token, which will either be an object with fields // {hashedToken, when} for a hashed token or {token, when} for an // unhashed token. var oldUnhashedStyleToken; var token = _.find(user.services.resume.loginTokens, function (token) { return token.hashedToken === hashedToken; }); if (token) { oldUnhashedStyleToken = false; } else { token = _.find(user.services.resume.loginTokens, function (token) { return token.token === options.resume; }); oldUnhashedStyleToken = true; } var tokenExpires = accounts._tokenExpiration(token.when); if (new Date() >= tokenExpires) return { userId: user._id, error: new Meteor.Error(403, "Your session has expired. Please log in again.") }; // Update to a hashed token when an unhashed token is encountered. if (oldUnhashedStyleToken) { // Only add the new hashed token if the old unhashed token still // exists (this avoids resurrecting the token if it was deleted // after we read it). Using $addToSet avoids getting an index // error if another client logging in simultaneously has already // inserted the new hashed token. accounts.users.update( { _id: user._id, "services.resume.loginTokens.token": options.resume }, {$addToSet: { "services.resume.loginTokens": { "hashedToken": hashedToken, "when": token.when } }} ); // Remove the old token *after* adding the new, since otherwise // another client trying to login between our removing the old and // adding the new wouldn't find a token to login with. accounts.users.update(user._id, { $pull: { "services.resume.loginTokens": { "token": options.resume } } }); } return { userId: user._id, stampedLoginToken: { token: options.resume, when: token.when } }; } // (Also used by Meteor Accounts server and tests). // Ap._generateStampedLoginToken = function () { return { token: Random.secret(), when: new Date }; }; /// /// TOKEN EXPIRATION /// // Deletes expired tokens from the database and closes all open connections // associated with these tokens. // // Exported for tests. Also, the arguments are only used by // tests. oldestValidDate is simulate expiring tokens without waiting // for them to actually expire. userId is used by tests to only expire // tokens for the test user. Ap._expireTokens = function (oldestValidDate, userId) { var tokenLifetimeMs = this._getTokenLifetimeMs(); // when calling from a test with extra arguments, you must specify both! if ((oldestValidDate && !userId) || (!oldestValidDate && userId)) { throw new Error("Bad test. Must specify both oldestValidDate and userId."); } oldestValidDate = oldestValidDate || (new Date(new Date() - tokenLifetimeMs)); var userFilter = userId ? {_id: userId} : {}; // Backwards compatible with older versions of meteor that stored login token // timestamps as numbers. this.users.update(_.extend(userFilter, { $or: [ { "services.resume.loginTokens.when": { $lt: oldestValidDate } }, { "services.resume.loginTokens.when": { $lt: +oldestValidDate } } ] }), { $pull: { "services.resume.loginTokens": { $or: [ { when: { $lt: oldestValidDate } }, { when: { $lt: +oldestValidDate } } ] } } }, { multi: true }); // The observe on Meteor.users will take care of closing connections for // expired tokens. }; // @override from accounts_common.js Ap.config = function (options) { // Call the overridden implementation of the method. var superResult = AccountsCommon.prototype.config.apply(this, arguments); // If the user set loginExpirationInDays to null, then we need to clear the // timer that periodically expires tokens. if (_.has(this._options, "loginExpirationInDays") && this._options.loginExpirationInDays === null && this.expireTokenInterval) { Meteor.clearInterval(this.expireTokenInterval); this.expireTokenInterval = null; } return superResult; }; function setExpireTokensInterval(accounts) { accounts.expireTokenInterval = Meteor.setInterval(function () { accounts._expireTokens(); }, EXPIRE_TOKENS_INTERVAL_MS); } /// /// OAuth Encryption Support /// var OAuthEncryption = Package["oauth-encryption"] && Package["oauth-encryption"].OAuthEncryption; function usingOAuthEncryption() { return OAuthEncryption && OAuthEncryption.keyIsLoaded(); } // OAuth service data is temporarily stored in the pending credentials // collection during the oauth authentication process. Sensitive data // such as access tokens are encrypted without the user id because // we don't know the user id yet. We re-encrypt these fields with the // user id included when storing the service data permanently in // the users collection. // function pinEncryptedFieldsToUser(serviceData, userId) { _.each(_.keys(serviceData), function (key) { var value = serviceData[key]; if (OAuthEncryption && OAuthEncryption.isSealed(value)) value = OAuthEncryption.seal(OAuthEncryption.open(value), userId); serviceData[key] = value; }); } // Encrypt unencrypted login service secrets when oauth-encryption is // added. // // XXX For the oauthSecretKey to be available here at startup, the // developer must call Accounts.config({oauthSecretKey: ...}) at load // time, instead of in a Meteor.startup block, because the startup // block in the app code will run after this accounts-base startup // block. Perhaps we need a post-startup callback? Meteor.startup(function () { if (! usingOAuthEncryption()) { return; } var ServiceConfiguration = Package['service-configuration'].ServiceConfiguration; ServiceConfiguration.configurations.find({ $and: [{ secret: { $exists: true } }, { "secret.algorithm": { $exists: false } }] }).forEach(function (config) { ServiceConfiguration.configurations.update(config._id, { $set: { secret: OAuthEncryption.seal(config.secret) } }); }); }); // XXX see comment on Accounts.createUser in passwords_server about adding a // second "server options" argument. function defaultCreateUserHook(options, user) { if (options.profile) user.profile = options.profile; return user; } // Called by accounts-password Ap.insertUserDoc = function (options, user) { // - clone user document, to protect from modification // - add createdAt timestamp // - prepare an _id, so that you can modify other collections (eg // create a first task for every new user) // // XXX If the onCreateUser or validateNewUser hooks fail, we might // end up having modified some other collection // inappropriately. The solution is probably to have onCreateUser // accept two callbacks - one that gets called before inserting // the user document (in which you can modify its contents), and // one that gets called after (in which you should change other // collections) user = _.extend({ createdAt: new Date(), _id: Random.id() }, user); if (user.services) { _.each(user.services, function (serviceData) { pinEncryptedFieldsToUser(serviceData, user._id); }); } var fullUser; if (this._onCreateUserHook) { fullUser = this._onCreateUserHook(options, user); // This is *not* part of the API. We need this because we can't isolate // the global server environment between tests, meaning we can't test // both having a create user hook set and not having one set. if (fullUser === 'TEST DEFAULT HOOK') fullUser = defaultCreateUserHook(options, user); } else { fullUser = defaultCreateUserHook(options, user); } _.each(this._validateNewUserHooks, function (hook) { if (! hook(fullUser)) throw new Meteor.Error(403, "User validation failed"); }); var userId; try { userId = this.users.insert(fullUser); } catch (e) { // XXX string parsing sucks, maybe // https://jira.mongodb.org/browse/SERVER-3069 will get fixed one day if (e.name !== 'MongoError') throw e; if (e.code !== 11000) throw e; if (e.err.indexOf('emails.address') !== -1) throw new Meteor.Error(403, "Email already exists."); if (e.err.indexOf('username') !== -1) throw new Meteor.Error(403, "Username already exists."); // XXX better error reporting for services.facebook.id duplicate, etc throw e; } return userId; }; // Helper function: returns false if email does not match company domain from // the configuration. Ap._testEmailDomain = function (email) { var domain = this._options.restrictCreationByEmailDomain; return !domain || (_.isFunction(domain) && domain(email)) || (_.isString(domain) && (new RegExp('@' + Meteor._escapeRegExp(domain) + '$', 'i')).test(email)); }; // Validate new user's email or Google/Facebook/GitHub account's email function defaultValidateNewUserHook(user) { var self = this; var domain = self._options.restrictCreationByEmailDomain; if (!domain) return true; var emailIsGood = false; if (!_.isEmpty(user.emails)) { emailIsGood = _.any(user.emails, function (email) { return self._testEmailDomain(email.address); }); } else if (!_.isEmpty(user.services)) { // Find any email of any service and check it emailIsGood = _.any(user.services, function (service) { return service.email && self._testEmailDomain(service.email); }); } if (emailIsGood) return true; if (_.isString(domain)) throw new Meteor.Error(403, "@" + domain + " email required"); else throw new Meteor.Error(403, "Email doesn't match the criteria."); } /// /// MANAGING USER OBJECTS /// // Updates or creates a user after we authenticate with a 3rd party. // // @param serviceName {String} Service name (eg, twitter). // @param serviceData {Object} Data to store in the user's record // under services[serviceName]. Must include an "id" field // which is a unique identifier for the user in the service. // @param options {Object, optional} Other options to pass to insertUserDoc // (eg, profile) // @returns {Object} Object with token and id keys, like the result // of the "login" method. // Ap.updateOrCreateUserFromExternalService = function ( serviceName, serviceData, options ) { options = _.clone(options || {}); if (serviceName === "password" || serviceName === "resume") throw new Error( "Can't use updateOrCreateUserFromExternalService with internal service " + serviceName); if (!_.has(serviceData, 'id')) throw new Error( "Service data for service " + serviceName + " must include id"); // Look for a user with the appropriate service user id. var selector = {}; var serviceIdKey = "services." + serviceName + ".id"; // XXX Temporary special case for Twitter. (Issue #629) // The serviceData.id will be a string representation of an integer. // We want it to match either a stored string or int representation. // This is to cater to earlier versions of Meteor storing twitter // user IDs in number form, and recent versions storing them as strings. // This can be removed once migration technology is in place, and twitter // users stored with integer IDs have been migrated to string IDs. if (serviceName === "twitter" && !isNaN(serviceData.id)) { selector["$or"] = [{},{}]; selector["$or"][0][serviceIdKey] = serviceData.id; selector["$or"][1][serviceIdKey] = parseInt(serviceData.id, 10); } else { selector[serviceIdKey] = serviceData.id; } var user = this.users.findOne(selector); if (user) { pinEncryptedFieldsToUser(serviceData, user._id); // We *don't* process options (eg, profile) for update, but we do replace // the serviceData (eg, so that we keep an unexpired access token and // don't cache old email addresses in serviceData.email). // XXX provide an onUpdateUser hook which would let apps update // the profile too var setAttrs = {}; _.each(serviceData, function (value, key) { setAttrs["services." + serviceName + "." + key] = value; }); // XXX Maybe we should re-use the selector above and notice if the update // touches nothing? this.users.update(user._id, { $set: setAttrs }); return { type: serviceName, userId: user._id }; } else { // Create a new user with the service data. Pass other options through to // insertUserDoc. user = {services: {}}; user.services[serviceName] = serviceData; return { type: serviceName, userId: this.insertUserDoc(options, user) }; } }; function setupUsersCollection(users) { /// /// RESTRICTING WRITES TO USER OBJECTS /// users.allow({ // clients can modify the profile field of their own document, and // nothing else. update: function (userId, user, fields, modifier) { // make sure it is our record if (user._id !== userId) return false; // user can only modify the 'profile' field. sets to multiple // sub-keys (eg profile.foo and profile.bar) are merged into entry // in the fields list. if (fields.length !== 1 || fields[0] !== 'profile') return false; return true; }, fetch: ['_id'] // we only look at _id. }); /// DEFAULT INDEXES ON USERS users._ensureIndex('username', {unique: 1, sparse: 1}); users._ensureIndex('emails.address', {unique: 1, sparse: 1}); users._ensureIndex('services.resume.loginTokens.hashedToken', {unique: 1, sparse: 1}); users._ensureIndex('services.resume.loginTokens.token', {unique: 1, sparse: 1}); // For taking care of logoutOtherClients calls that crashed before the // tokens were deleted. users._ensureIndex('services.resume.haveLoginTokensToDelete', { sparse: 1 }); // For expiring login tokens users._ensureIndex("services.resume.loginTokens.when", { sparse: 1 }); } /// /// CLEAN UP FOR `logoutOtherClients` /// Ap._deleteSavedTokensForUser = function (userId, tokensToDelete) { if (tokensToDelete) { this.users.update(userId, { $unset: { "services.resume.haveLoginTokensToDelete": 1, "services.resume.loginTokensToDelete": 1 }, $pullAll: { "services.resume.loginTokens": tokensToDelete } }); } }; Ap._deleteSavedTokensForAllUsersOnStartup = function () { var self = this; // If we find users who have saved tokens to delete on startup, delete // them now. It's possible that the server could have crashed and come // back up before new tokens are found in localStorage, but this // shouldn't happen very often. We shouldn't put a delay here because // that would give a lot of power to an attacker with a stolen login // token and the ability to crash the server. Meteor.startup(function () { self.users.find({ "services.resume.haveLoginTokensToDelete": true }, { "services.resume.loginTokensToDelete": 1 }).forEach(function (user) { self._deleteSavedTokensForUser( user._id, user.services.resume.loginTokensToDelete ); }); }); }; ================================================ FILE: packages/accounts-base/accounts_tests.js ================================================ Meteor.methods({ getCurrentLoginToken: function () { return Accounts._getLoginToken(this.connection.id); } }); // XXX it'd be cool to also test that the right thing happens if options // *are* validated, but Accounts._options is global state which makes this hard // (impossible?) Tinytest.add('accounts - config validates keys', function (test) { test.throws(function () { Accounts.config({foo: "bar"}); }); }); var idsInValidateNewUser = {}; Accounts.validateNewUser(function (user) { idsInValidateNewUser[user._id] = true; return true; }); Tinytest.add('accounts - validateNewUser gets passed user with _id', function (test) { var newUserId = Accounts.updateOrCreateUserFromExternalService('foobook', {id: Random.id()}).userId; test.isTrue(newUserId in idsInValidateNewUser); }); Tinytest.add('accounts - updateOrCreateUserFromExternalService - Facebook', function (test) { var facebookId = Random.id(); // create an account with facebook var uid1 = Accounts.updateOrCreateUserFromExternalService( 'facebook', {id: facebookId, monkey: 42}, {profile: {foo: 1}}).id; var users = Meteor.users.find({"services.facebook.id": facebookId}).fetch(); test.length(users, 1); test.equal(users[0].profile.foo, 1); test.equal(users[0].services.facebook.monkey, 42); // create again with the same id, see that we get the same user. // it should update services.facebook but not profile. var uid2 = Accounts.updateOrCreateUserFromExternalService( 'facebook', {id: facebookId, llama: 50}, {profile: {foo: 1000, bar: 2}}).id; test.equal(uid1, uid2); users = Meteor.users.find({"services.facebook.id": facebookId}).fetch(); test.length(users, 1); test.equal(users[0].profile.foo, 1); test.equal(users[0].profile.bar, undefined); test.equal(users[0].services.facebook.llama, 50); // make sure we *don't* lose values not passed this call to // updateOrCreateUserFromExternalService test.equal(users[0].services.facebook.monkey, 42); // cleanup Meteor.users.remove(uid1); }); Tinytest.add('accounts - updateOrCreateUserFromExternalService - Weibo', function (test) { var weiboId1 = Random.id(); var weiboId2 = Random.id(); // users that have different service ids get different users var uid1 = Accounts.updateOrCreateUserFromExternalService( 'weibo', {id: weiboId1}, {profile: {foo: 1}}).id; var uid2 = Accounts.updateOrCreateUserFromExternalService( 'weibo', {id: weiboId2}, {profile: {bar: 2}}).id; test.equal(Meteor.users.find({"services.weibo.id": {$in: [weiboId1, weiboId2]}}).count(), 2); test.equal(Meteor.users.findOne({"services.weibo.id": weiboId1}).profile.foo, 1); test.equal(Meteor.users.findOne({"services.weibo.id": weiboId1}).emails, undefined); test.equal(Meteor.users.findOne({"services.weibo.id": weiboId2}).profile.bar, 2); test.equal(Meteor.users.findOne({"services.weibo.id": weiboId2}).emails, undefined); // cleanup Meteor.users.remove(uid1); Meteor.users.remove(uid2); }); Tinytest.add('accounts - updateOrCreateUserFromExternalService - Twitter', function (test) { var twitterIdOld = parseInt(Random.hexString(4), 16); var twitterIdNew = ''+twitterIdOld; // create an account with twitter using the old ID format of integer var uid1 = Accounts.updateOrCreateUserFromExternalService( 'twitter', {id: twitterIdOld, monkey: 42}, {profile: {foo: 1}}).id; var users = Meteor.users.find({"services.twitter.id": twitterIdOld}).fetch(); test.length(users, 1); test.equal(users[0].profile.foo, 1); test.equal(users[0].services.twitter.monkey, 42); // Update the account with the new ID format of string // test that the existing user is found, and that the ID // gets updated to a string value var uid2 = Accounts.updateOrCreateUserFromExternalService( 'twitter', {id: twitterIdNew, monkey: 42}, {profile: {foo: 1}}).id; test.equal(uid1, uid2); users = Meteor.users.find({"services.twitter.id": twitterIdNew}).fetch(); test.length(users, 1); // cleanup Meteor.users.remove(uid1); }); Tinytest.add('accounts - insertUserDoc username', function (test) { var userIn = { username: Random.id() }; // user does not already exist. create a user object with fields set. var userId = Accounts.insertUserDoc( {profile: {name: 'Foo Bar'}}, userIn ); var userOut = Meteor.users.findOne(userId); test.equal(typeof userOut.createdAt, 'object'); test.equal(userOut.profile.name, 'Foo Bar'); test.equal(userOut.username, userIn.username); // run the hook again. now the user exists, so it throws an error. test.throws(function () { Accounts.insertUserDoc( {profile: {name: 'Foo Bar'}}, userIn ); }, 'Username already exists.'); // cleanup Meteor.users.remove(userId); }); Tinytest.add('accounts - insertUserDoc email', function (test) { var email1 = Random.id(); var email2 = Random.id(); var email3 = Random.id(); var userIn = { emails: [{address: email1, verified: false}, {address: email2, verified: true}] }; // user does not already exist. create a user object with fields set. var userId = Accounts.insertUserDoc( {profile: {name: 'Foo Bar'}}, userIn ); var userOut = Meteor.users.findOne(userId); test.equal(typeof userOut.createdAt, 'object'); test.equal(userOut.profile.name, 'Foo Bar'); test.equal(userOut.emails, userIn.emails); // run the hook again with the exact same emails. // run the hook again. now the user exists, so it throws an error. test.throws(function () { Accounts.insertUserDoc( {profile: {name: 'Foo Bar'}}, userIn ); }, 'Email already exists.'); // now with only one of them. test.throws(function () { Accounts.insertUserDoc( {}, {emails: [{address: email1}]} ); }, 'Email already exists.'); test.throws(function () { Accounts.insertUserDoc( {}, {emails: [{address: email2}]} ); }, 'Email already exists.'); // a third email works. var userId3 = Accounts.insertUserDoc( {}, {emails: [{address: email3}]} ); var user3 = Meteor.users.findOne(userId3); test.equal(typeof user3.createdAt, 'object'); // cleanup Meteor.users.remove(userId); Meteor.users.remove(userId3); }); // More token expiration tests are in accounts-password Tinytest.addAsync('accounts - expire numeric token', function (test, onComplete) { var userIn = { username: Random.id() }; var userId = Accounts.insertUserDoc({ profile: { name: 'Foo Bar' } }, userIn); var date = new Date(new Date() - 5000); Meteor.users.update(userId, { $set: { "services.resume.loginTokens": [{ hashedToken: Random.id(), when: date }, { hashedToken: Random.id(), when: +date }] } }); var observe = Meteor.users.find(userId).observe({ changed: function (newUser) { if (newUser.services && newUser.services.resume && _.isEmpty(newUser.services.resume.loginTokens)) { observe.stop(); onComplete(); } } }); Accounts._expireTokens(new Date(), userId); }); // Login tokens used to be stored unhashed in the database. We want // to make sure users can still login after upgrading. var insertUnhashedLoginToken = function (userId, stampedToken) { Meteor.users.update( userId, {$push: {'services.resume.loginTokens': stampedToken}} ); }; Tinytest.addAsync('accounts - login token', function (test, onComplete) { // Test that we can login when the database contains a leftover // old style unhashed login token. var userId1 = Accounts.insertUserDoc({}, {username: Random.id()}); var stampedToken = Accounts._generateStampedLoginToken(); insertUnhashedLoginToken(userId1, stampedToken); var connection = DDP.connect(Meteor.absoluteUrl()); connection.call('login', {resume: stampedToken.token}); connection.disconnect(); // Steal the unhashed token from the database and use it to login. // This is a sanity check so that when we *can't* login with a // stolen *hashed* token, we know it's not a problem with the test. var userId2 = Accounts.insertUserDoc({}, {username: Random.id()}); insertUnhashedLoginToken(userId2, Accounts._generateStampedLoginToken()); var stolenToken = Meteor.users.findOne(userId2).services.resume.loginTokens[0].token; test.isTrue(stolenToken); connection = DDP.connect(Meteor.absoluteUrl()); connection.call('login', {resume: stolenToken}); connection.disconnect(); // Now do the same thing, this time with a stolen hashed token. var userId3 = Accounts.insertUserDoc({}, {username: Random.id()}); Accounts._insertLoginToken(userId3, Accounts._generateStampedLoginToken()); stolenToken = Meteor.users.findOne(userId3).services.resume.loginTokens[0].hashedToken; test.isTrue(stolenToken); connection = DDP.connect(Meteor.absoluteUrl()); // evil plan foiled test.throws( function () { connection.call('login', {resume: stolenToken}); }, /You\'ve been logged out by the server/ ); connection.disconnect(); // Old style unhashed tokens are replaced by hashed tokens when // encountered. This means that after someone logins once, the // old unhashed token is no longer available to be stolen. var userId4 = Accounts.insertUserDoc({}, {username: Random.id()}); var stampedToken = Accounts._generateStampedLoginToken(); insertUnhashedLoginToken(userId4, stampedToken); connection = DDP.connect(Meteor.absoluteUrl()); connection.call('login', {resume: stampedToken.token}); connection.disconnect(); // The token is no longer available to be stolen. stolenToken = Meteor.users.findOne(userId4).services.resume.loginTokens[0].token; test.isFalse(stolenToken); // After the upgrade, the client can still login with their original // unhashed login token. connection = DDP.connect(Meteor.absoluteUrl()); connection.call('login', {resume: stampedToken.token}); connection.disconnect(); onComplete(); }); Tinytest.addAsync( 'accounts - connection data cleaned up', function (test, onComplete) { makeTestConnection( test, function (clientConn, serverConn) { // onClose callbacks are called in order, so we run after the // close callback in accounts. serverConn.onClose(function () { test.isFalse(Accounts._getAccountData(serverConn.id, 'connection')); onComplete(); }); test.isTrue(Accounts._getAccountData(serverConn.id, 'connection')); serverConn.close(); }, onComplete ); } ); Tinytest.add( 'accounts - get new token', function (test) { // Test that the `getNewToken` method returns us a valid token, with // the same expiration as our original token. var userId = Accounts.insertUserDoc({}, { username: Random.id() }); var stampedToken = Accounts._generateStampedLoginToken(); Accounts._insertLoginToken(userId, stampedToken); var conn = DDP.connect(Meteor.absoluteUrl()); conn.call('login', { resume: stampedToken.token }); test.equal(conn.call('getCurrentLoginToken'), Accounts._hashLoginToken(stampedToken.token)); var newTokenResult = conn.call('getNewToken'); test.equal(newTokenResult.tokenExpires, Accounts._tokenExpiration(stampedToken.when)); test.equal(conn.call('getCurrentLoginToken'), Accounts._hashLoginToken(newTokenResult.token)); conn.disconnect(); // A second connection should be able to log in with the new token // we got. var secondConn = DDP.connect(Meteor.absoluteUrl()); secondConn.call('login', { resume: newTokenResult.token }); secondConn.disconnect(); } ); Tinytest.addAsync( 'accounts - remove other tokens', function (test, onComplete) { // Test that the `removeOtherTokens` method removes all tokens other // than the caller's token, thereby logging out and closing other // connections. var userId = Accounts.insertUserDoc({}, { username: Random.id() }); var stampedTokens = []; var conns = []; _.times(2, function (i) { stampedTokens.push(Accounts._generateStampedLoginToken()); Accounts._insertLoginToken(userId, stampedTokens[i]); var conn = DDP.connect(Meteor.absoluteUrl()); conn.call('login', { resume: stampedTokens[i].token }); test.equal(conn.call('getCurrentLoginToken'), Accounts._hashLoginToken(stampedTokens[i].token)); conns.push(conn); }); conns[0].call('removeOtherTokens'); simplePoll( function () { var tokens = _.map(conns, function (conn) { return conn.call('getCurrentLoginToken'); }); return ! tokens[1] && tokens[0] === Accounts._hashLoginToken(stampedTokens[0].token); }, function () { // success _.each(conns, function (conn) { conn.disconnect(); }); onComplete(); }, function () { // timed out throw new Error("accounts - remove other tokens timed out"); } ); } ); Tinytest.add( 'accounts - hook callbacks can access Meteor.userId()', function (test) { var userId = Accounts.insertUserDoc({}, { username: Random.id() }); var stampedToken = Accounts._generateStampedLoginToken(); Accounts._insertLoginToken(userId, stampedToken); var validateStopper = Accounts.validateLoginAttempt(function(attempt) { test.equal(Meteor.userId(), validateAttemptExpectedUserId, "validateLoginAttempt"); return true; }); var onLoginStopper = Accounts.onLogin(function(attempt) { test.equal(Meteor.userId(), onLoginExpectedUserId, "onLogin"); }); var onLoginFailureStopper = Accounts.onLoginFailure(function(attempt) { test.equal(Meteor.userId(), onLoginFailureExpectedUserId, "onLoginFailure"); }); var conn = DDP.connect(Meteor.absoluteUrl()); // On a new connection, Meteor.userId() should be null until logged in. var validateAttemptExpectedUserId = null; var onLoginExpectedUserId = userId; conn.call('login', { resume: stampedToken.token }); // Now that the user is logged in on the connection, Meteor.userId() should // return that user. validateAttemptExpectedUserId = userId; conn.call('login', { resume: stampedToken.token }); // Trigger onLoginFailure callbacks var onLoginFailureExpectedUserId = userId; test.throws(function() { conn.call('login', { resume: "bogus" }) }, '403'); conn.disconnect(); validateStopper.stop(); onLoginStopper.stop(); onLoginFailureStopper.stop(); } ); ================================================ FILE: packages/accounts-base/accounts_url_tests.js ================================================ import {AccountsTest} from "meteor/accounts-base"; Tinytest.add("accounts - parse urls for accounts-password", function (test) { var actions = ["reset-password", "verify-email", "enroll-account"]; // make sure the callback was called the right number of times var actionsParsed = []; _.each(actions, function (hashPart) { var fakeToken = "asdf"; var hashTokenOnly = "#/" + hashPart + "/" + fakeToken; AccountsTest.attemptToMatchHash(hashTokenOnly, function (token, action) { test.equal(token, fakeToken); test.equal(action, hashPart); // XXX COMPAT WITH 0.9.3 if (hashPart === "reset-password") { test.equal(Accounts._resetPasswordToken, fakeToken); } else if (hashPart === "verify-email") { test.equal(Accounts._verifyEmailToken, fakeToken); } else if (hashPart === "enroll-account") { test.equal(Accounts._enrollAccountToken, fakeToken); } // Reset variables for the next test Accounts._resetPasswordToken = null; Accounts._verifyEmailToken = null; Accounts._enrollAccountToken = null; actionsParsed.push(action); }); }); // make sure each action is called once, in order test.equal(actionsParsed, actions); }); ================================================ FILE: packages/accounts-base/client_main.js ================================================ import {AccountsClient} from "./accounts_client.js"; import {AccountsTest} from "./url_client.js"; import "./localstorage_token.js"; /** * @namespace Accounts * @summary The namespace for all client-side accounts-related methods. */ Accounts = new AccountsClient(); /** * @summary A [Mongo.Collection](#collections) containing user documents. * @locus Anywhere * @type {Mongo.Collection} * @importFromPackage meteor */ Meteor.users = Accounts.users; export { // Since this file is the main module for the client version of the // accounts-base package, properties of non-entry-point modules need to // be re-exported in order to be accessible to modules that import the // accounts-base package. AccountsClient, AccountsTest, }; ================================================ FILE: packages/accounts-base/client_tests.js ================================================ import "./accounts_url_tests.js"; import "./accounts_reconnect_tests.js"; ================================================ FILE: packages/accounts-base/localstorage_token.js ================================================ import {AccountsClient} from "./accounts_client.js"; var Ap = AccountsClient.prototype; // This file deals with storing a login token and user id in the // browser's localStorage facility. It polls local storage every few // seconds to synchronize login state between multiple tabs in the same // browser. // Login with a Meteor access token. This is the only public function // here. Meteor.loginWithToken = function (token, callback) { return Accounts.loginWithToken(token, callback); }; Ap.loginWithToken = function (token, callback) { this.callLoginMethod({ methodArguments: [{ resume: token }], userCallback: callback }); }; // Semi-internal API. Call this function to re-enable auto login after // if it was disabled at startup. Ap._enableAutoLogin = function () { this._autoLoginEnabled = true; this._pollStoredLoginToken(); }; /// /// STORING /// // Call this from the top level of the test file for any test that does // logging in and out, to protect multiple tabs running the same tests // simultaneously from interfering with each others' localStorage. Ap._isolateLoginTokenForTest = function () { this.LOGIN_TOKEN_KEY = this.LOGIN_TOKEN_KEY + Random.id(); this.USER_ID_KEY = this.USER_ID_KEY + Random.id(); }; Ap._storeLoginToken = function (userId, token, tokenExpires) { Meteor._localStorage.setItem(this.USER_ID_KEY, userId); Meteor._localStorage.setItem(this.LOGIN_TOKEN_KEY, token); if (! tokenExpires) tokenExpires = this._tokenExpiration(new Date()); Meteor._localStorage.setItem(this.LOGIN_TOKEN_EXPIRES_KEY, tokenExpires); // to ensure that the localstorage poller doesn't end up trying to // connect a second time this._lastLoginTokenWhenPolled = token; }; Ap._unstoreLoginToken = function () { Meteor._localStorage.removeItem(this.USER_ID_KEY); Meteor._localStorage.removeItem(this.LOGIN_TOKEN_KEY); Meteor._localStorage.removeItem(this.LOGIN_TOKEN_EXPIRES_KEY); // to ensure that the localstorage poller doesn't end up trying to // connect a second time this._lastLoginTokenWhenPolled = null; }; // This is private, but it is exported for now because it is used by a // test in accounts-password. // Ap._storedLoginToken = function () { return Meteor._localStorage.getItem(this.LOGIN_TOKEN_KEY); }; Ap._storedLoginTokenExpires = function () { return Meteor._localStorage.getItem(this.LOGIN_TOKEN_EXPIRES_KEY); }; Ap._storedUserId = function () { return Meteor._localStorage.getItem(this.USER_ID_KEY); }; Ap._unstoreLoginTokenIfExpiresSoon = function () { var tokenExpires = this._storedLoginTokenExpires(); if (tokenExpires && this._tokenExpiresSoon(new Date(tokenExpires))) { this._unstoreLoginToken(); } }; /// /// AUTO-LOGIN /// Ap._initLocalStorage = function () { var self = this; // Key names to use in localStorage self.LOGIN_TOKEN_KEY = "Meteor.loginToken"; self.LOGIN_TOKEN_EXPIRES_KEY = "Meteor.loginTokenExpires"; self.USER_ID_KEY = "Meteor.userId"; var rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX; if (rootUrlPathPrefix || this.connection !== Meteor.connection) { // We want to keep using the same keys for existing apps that do not // set a custom ROOT_URL_PATH_PREFIX, so that most users will not have // to log in again after an app updates to a version of Meteor that // contains this code, but it's generally preferable to namespace the // keys so that connections from distinct apps to distinct DDP URLs // will be distinct in Meteor._localStorage. var namespace = ":" + this.connection._stream.rawUrl; if (rootUrlPathPrefix) { namespace += ":" + rootUrlPathPrefix; } self.LOGIN_TOKEN_KEY += namespace; self.LOGIN_TOKEN_EXPIRES_KEY += namespace; self.USER_ID_KEY += namespace; } if (self._autoLoginEnabled) { // Immediately try to log in via local storage, so that any DDP // messages are sent after we have established our user account self._unstoreLoginTokenIfExpiresSoon(); var token = self._storedLoginToken(); if (token) { // On startup, optimistically present us as logged in while the // request is in flight. This reduces page flicker on startup. var userId = self._storedUserId(); userId && self.connection.setUserId(userId); self.loginWithToken(token, function (err) { if (err) { Meteor._debug("Error logging in with token: " + err); self.makeClientLoggedOut(); } self._pageLoadLogin({ type: "resume", allowed: !err, error: err, methodName: "login", // XXX This is duplicate code with loginWithToken, but // loginWithToken can also be called at other times besides // page load. methodArguments: [{resume: token}] }); }); } } // Poll local storage every 3 seconds to login if someone logged in in // another tab self._lastLoginTokenWhenPolled = token; if (self._pollIntervalTimer) { // Unlikely that _initLocalStorage will be called more than once for // the same AccountsClient instance, but just in case... clearInterval(self._pollIntervalTimer); } self._pollIntervalTimer = setInterval(function () { self._pollStoredLoginToken(); }, 3000); }; Ap._pollStoredLoginToken = function () { var self = this; if (! self._autoLoginEnabled) { return; } var currentLoginToken = self._storedLoginToken(); // != instead of !== just to make sure undefined and null are treated the same if (self._lastLoginTokenWhenPolled != currentLoginToken) { if (currentLoginToken) { self.loginWithToken(currentLoginToken, function (err) { if (err) { self.makeClientLoggedOut(); } }); } else { self.logout(); } } self._lastLoginTokenWhenPolled = currentLoginToken; }; ================================================ FILE: packages/accounts-base/package.js ================================================ Package.describe({ summary: "A user account system", version: "1.2.8" }); Package.onUse(function (api) { api.use('underscore', ['client', 'server']); api.use('ecmascript', ['client', 'server']); api.use('ddp-rate-limiter'); api.use('localstorage', 'client'); api.use('tracker', 'client'); api.use('check', 'server'); api.use('random', ['client', 'server']); api.use('ejson', 'server'); api.use('callback-hook', ['client', 'server']); // use unordered to work around a circular dependency // (service-configuration needs Accounts.connection) api.use('service-configuration', ['client', 'server'], { unordered: true }); // needed for getting the currently logged-in user api.use('ddp', ['client', 'server']); // need this because of the Meteor.users collection but in the future // we'd probably want to abstract this away api.use('mongo', ['client', 'server']); // If the 'blaze' package is loaded, we'll define some helpers like // {{currentUser}}. If not, no biggie. api.use('blaze', 'client', {weak: true}); // Allow us to detect 'autopublish', and publish some Meteor.users fields if // it's loaded. api.use('autopublish', 'server', {weak: true}); api.use('oauth-encryption', 'server', {weak: true}); // Though this "Accounts" symbol is the only official Package export for // the accounts-base package, modules that import accounts-base will // have access to anything added to the exports object of the main // module, including AccountsClient and AccountsServer (those symbols // just won't be automatically imported as "global" variables). api.export('Accounts'); // These main modules import all the other modules that comprise the // accounts-base package, and define exports that will be accessible to // modules that import the accounts-base package. api.mainModule('server_main.js', 'server'); api.mainModule('client_main.js', 'client'); }); Package.onTest(function (api) { api.use([ 'accounts-base', 'ecmascript', 'tinytest', 'random', 'test-helpers', 'oauth-encryption', 'underscore', 'ddp', 'accounts-password' ]); api.mainModule('server_tests.js', 'server'); api.mainModule('client_tests.js', 'client'); }); ================================================ FILE: packages/accounts-base/server_main.js ================================================ import {AccountsServer} from "./accounts_server.js"; import "./accounts_rate_limit.js"; import "./url_server.js"; /** * @namespace Accounts * @summary The namespace for all server-side accounts-related methods. */ Accounts = new AccountsServer(Meteor.server); // Users table. Don't use the normal autopublish, since we want to hide // some fields. Code to autopublish this is in accounts_server.js. // XXX Allow users to configure this collection name. /** * @summary A [Mongo.Collection](#collections) containing user documents. * @locus Anywhere * @type {Mongo.Collection} * @importFromPackage meteor */ Meteor.users = Accounts.users; export { // Since this file is the main module for the server version of the // accounts-base package, properties of non-entry-point modules need to // be re-exported in order to be accessible to modules that import the // accounts-base package. AccountsServer }; ================================================ FILE: packages/accounts-base/server_tests.js ================================================ import "./accounts_tests.js"; import "./accounts_reconnect_tests.js"; ================================================ FILE: packages/accounts-base/url_client.js ================================================ import {AccountsClient} from "./accounts_client.js"; var Ap = AccountsClient.prototype; // All of the special hash URLs we support for accounts interactions var accountsPaths = ["reset-password", "verify-email", "enroll-account"]; var savedHash = window.location.hash; Ap._initUrlMatching = function () { // By default, allow the autologin process to happen. this._autoLoginEnabled = true; // We only support one callback per URL. this._accountsCallbacks = {}; // Try to match the saved value of window.location.hash. this._attemptToMatchHash(); }; // Separate out this functionality for testing Ap._attemptToMatchHash = function () { attemptToMatchHash(this, savedHash, defaultSuccessHandler); }; // Note that both arguments are optional and are currently only passed by // accounts_url_tests.js. function attemptToMatchHash(accounts, hash, success) { _.each(accountsPaths, function (urlPart) { var token; var tokenRegex = new RegExp("^\\#\\/" + urlPart + "\\/(.*)$"); var match = hash.match(tokenRegex); if (match) { token = match[1]; // XXX COMPAT WITH 0.9.3 if (urlPart === "reset-password") { accounts._resetPasswordToken = token; } else if (urlPart === "verify-email") { accounts._verifyEmailToken = token; } else if (urlPart === "enroll-account") { accounts._enrollAccountToken = token; } } else { return; } // If no handlers match the hash, then maybe it's meant to be consumed // by some entirely different code, so we only clear it the first time // a handler successfully matches. Note that later handlers reuse the // savedHash, so clearing window.location.hash here will not interfere // with their needs. window.location.hash = ""; // Do some stuff with the token we matched success.call(accounts, token, urlPart); }); } function defaultSuccessHandler(token, urlPart) { var self = this; // put login in a suspended state to wait for the interaction to finish self._autoLoginEnabled = false; // wait for other packages to register callbacks Meteor.startup(function () { // if a callback has been registered for this kind of token, call it if (self._accountsCallbacks[urlPart]) { self._accountsCallbacks[urlPart](token, function () { self._enableAutoLogin(); }); } }); } // Export for testing export var AccountsTest = { attemptToMatchHash: function (hash, success) { return attemptToMatchHash(Accounts, hash, success); } }; // XXX these should be moved to accounts-password eventually. Right now // this is prevented by the need to set autoLoginEnabled=false, but in // some bright future we won't need to do that anymore. /** * @summary Register a function to call when a reset password link is clicked * in an email sent by * [`Accounts.sendResetPasswordEmail`](#accounts_sendresetpasswordemail). * This function should be called in top-level code, not inside * `Meteor.startup()`. * @memberof! Accounts * @name onResetPasswordLink * @param {Function} callback The function to call. It is given two arguments: * * 1. `token`: A password reset token that can be passed to * [`Accounts.resetPassword`](#accounts_resetpassword). * 2. `done`: A function to call when the password reset UI flow is complete. The normal * login process is suspended until this function is called, so that the * password for user A can be reset even if user B was logged in. * @locus Client */ Ap.onResetPasswordLink = function (callback) { if (this._accountsCallbacks["reset-password"]) { Meteor._debug("Accounts.onResetPasswordLink was called more than once. " + "Only one callback added will be executed."); } this._accountsCallbacks["reset-password"] = callback; }; /** * @summary Register a function to call when an email verification link is * clicked in an email sent by * [`Accounts.sendVerificationEmail`](#accounts_sendverificationemail). * This function should be called in top-level code, not inside * `Meteor.startup()`. * @memberof! Accounts * @name onEmailVerificationLink * @param {Function} callback The function to call. It is given two arguments: * * 1. `token`: An email verification token that can be passed to * [`Accounts.verifyEmail`](#accounts_verifyemail). * 2. `done`: A function to call when the email verification UI flow is complete. * The normal login process is suspended until this function is called, so * that the user can be notified that they are verifying their email before * being logged in. * @locus Client */ Ap.onEmailVerificationLink = function (callback) { if (this._accountsCallbacks["verify-email"]) { Meteor._debug("Accounts.onEmailVerificationLink was called more than once. " + "Only one callback added will be executed."); } this._accountsCallbacks["verify-email"] = callback; }; /** * @summary Register a function to call when an account enrollment link is * clicked in an email sent by * [`Accounts.sendEnrollmentEmail`](#accounts_sendenrollmentemail). * This function should be called in top-level code, not inside * `Meteor.startup()`. * @memberof! Accounts * @name onEnrollmentLink * @param {Function} callback The function to call. It is given two arguments: * * 1. `token`: A password reset token that can be passed to * [`Accounts.resetPassword`](#accounts_resetpassword) to give the newly * enrolled account a password. * 2. `done`: A function to call when the enrollment UI flow is complete. * The normal login process is suspended until this function is called, so that * user A can be enrolled even if user B was logged in. * @locus Client */ Ap.onEnrollmentLink = function (callback) { if (this._accountsCallbacks["enroll-account"]) { Meteor._debug("Accounts.onEnrollmentLink was called more than once. " + "Only one callback added will be executed."); } this._accountsCallbacks["enroll-account"] = callback; }; ================================================ FILE: packages/accounts-base/url_server.js ================================================ import {AccountsServer} from "./accounts_server.js"; // XXX These should probably not actually be public? AccountsServer.prototype.urls = { resetPassword: function (token) { return Meteor.absoluteUrl('#/reset-password/' + token); }, verifyEmail: function (token) { return Meteor.absoluteUrl('#/verify-email/' + token); }, enrollAccount: function (token) { return Meteor.absoluteUrl('#/enroll-account/' + token); } }; ================================================ FILE: packages/accounts-facebook/.gitignore ================================================ .build* ================================================ FILE: packages/accounts-facebook/README.md ================================================ # accounts-facebook A login service for Facebook. See the [project page](https://www.meteor.com/accounts) on Meteor Accounts for more details. ================================================ FILE: packages/accounts-facebook/facebook.js ================================================ Accounts.oauth.registerService('facebook'); if (Meteor.isClient) { Meteor.loginWithFacebook = function(options, callback) { // support a callback without options if (! callback && typeof options === "function") { callback = options; options = null; } var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); Facebook.requestCredential(options, credentialRequestCompleteCallback); }; } else { Accounts.addAutopublishFields({ // publish all fields including access token, which can legitimately // be used from the client (if transmitted over ssl or on // localhost). https://developers.facebook.com/docs/concepts/login/access-tokens-and-types/, // "Sharing of Access Tokens" forLoggedInUser: ['services.facebook'], forOtherUsers: [ // https://www.facebook.com/help/167709519956542 'services.facebook.id', 'services.facebook.username', 'services.facebook.gender' ] }); } ================================================ FILE: packages/accounts-facebook/facebook_login_button.css ================================================ #login-buttons-image-facebook { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAq0lEQVQ4jWP8//8/AyWAhYGBgcEmauYZBgYGYxL1nj2yLN2ECcohVTNcDwsxKlXlhRm6yzwZRAS5GRgYGBhsombC5ZhwaUIGyJrRAVEuwGYzSS7AB/C64MiydKx8ZJfgNeDN+68MDAwIL8D4RLsgIHsJis0wPjKgOAyoE4hcnGwMGkpiBBUbacvA2TfuvaKiC759/3X23NUnOPMDtgTEwMBwloGBgYGR0uwMAGOPLJS9mkQHAAAAAElFTkSuQmCC); } ================================================ FILE: packages/accounts-facebook/package.js ================================================ Package.describe({ summary: "Login service for Facebook accounts", version: "1.0.10" }); Package.onUse(function(api) { api.use('accounts-base', ['client', 'server']); // Export Accounts (etc) to packages using this one. api.imply('accounts-base', ['client', 'server']); api.use('accounts-oauth', ['client', 'server']); api.use('facebook', ['client', 'server']); api.addFiles('facebook_login_button.css', 'client'); api.addFiles("facebook.js"); }); ================================================ FILE: packages/accounts-github/.gitignore ================================================ .build* ================================================ FILE: packages/accounts-github/README.md ================================================ # accounts-github A login service for GitHub. See the [project page](https://www.meteor.com/accounts) on Meteor Accounts for more details. ================================================ FILE: packages/accounts-github/github.js ================================================ Accounts.oauth.registerService('github'); if (Meteor.isClient) { Meteor.loginWithGithub = function(options, callback) { // support a callback without options if (! callback && typeof options === "function") { callback = options; options = null; } var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); Github.requestCredential(options, credentialRequestCompleteCallback); }; } else { Accounts.addAutopublishFields({ // not sure whether the github api can be used from the browser, // thus not sure if we should be sending access tokens; but we do it // for all other oauth2 providers, and it may come in handy. forLoggedInUser: ['services.github'], forOtherUsers: ['services.github.username'] }); } ================================================ FILE: packages/accounts-github/github_login_button.css ================================================ #login-buttons-image-github { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wJGBYxHYxl31wAAAHpSURBVDjLpZI/aFNRFMZ/973bJqGRPopV4qNq/+SpTYnWRhCKilShg9BGcHOM+GfQoZuLk4iLgw4qZNBaHLuIdBNHl7Ta1qdNFI3SihnaNG1MpH3vuiQYQnwZvHCG893zffc751z4z6PX5T5gA1DAKnAaOAQEgAfAVeCpl+CeCrlRuEC6maO4h0A1wl4tPAHMqNUthvrDdHYY7A3t4rDVjeO6rBU2FaABM1WCrBNoi48Mi+nH9yj+KtPibAKwJXfQ5vcRG7soUnYmWEuQgAEIYBv4cGpoILI0Z4tyYYPegS6UguyijZQ6J45GSNmZHzUcJYD2ii2Ajv7efZ8WZ6ZwXFj79hXpayW4O0SL1Nl/8jzZlZ9dQLFS70pgvZKIyGD0yvu5eRmMnrk1PjI81ir1qBACTdPevXj95mVuNX8XKDQc/+T334bZZ104cvzYw2s3J3qAL5WXSsDbf61NNMBu+wOBs+VSyQ84Nfhg028ZGx3/qyy0lC7lgi7lghBitoon03lvB8l0/k7Wnk+8mny0cyXzEcfZxgwfZPTyRMHsOzAFXE9YhtNQIJnOx4FpJXT1eSkn2g0frqMoFrfoCXcqlCOAGwnLuO/l4JymcWl5uRxzXUKghBAiZ5r+WaV4lrCM555zqO+x2d0ftGmpiA/0k70AAAAASUVORK5CYII=); } ================================================ FILE: packages/accounts-github/package.js ================================================ Package.describe({ summary: "Login service for Github accounts", version: "1.0.10" }); Package.onUse(function(api) { api.use('accounts-base', ['client', 'server']); // Export Accounts (etc) to packages using this one. api.imply('accounts-base', ['client', 'server']); api.use('accounts-oauth', ['client', 'server']); api.use('github', ['client', 'server']); api.addFiles('github_login_button.css', 'client'); api.addFiles("github.js"); }); ================================================ FILE: packages/accounts-google/.gitignore ================================================ .build* ================================================ FILE: packages/accounts-google/README.md ================================================ # accounts-google A login service for Google. See the [project page](https://www.meteor.com/accounts) on Meteor Accounts for more details. ================================================ FILE: packages/accounts-google/google.js ================================================ Accounts.oauth.registerService('google'); if (Meteor.isClient) { Meteor.loginWithGoogle = function(options, callback) { // support a callback without options if (! callback && typeof options === "function") { callback = options; options = null; } // Use Google's domain-specific login page if we want to restrict creation to // a particular email domain. (Don't use it if restrictCreationByEmailDomain // is a function.) Note that all this does is change Google's UI --- // accounts-base/accounts_server.js still checks server-side that the server // has the proper email address after the OAuth conversation. if (typeof Accounts._options.restrictCreationByEmailDomain === 'string') { options = _.extend({}, options || {}); options.loginUrlParameters = _.extend({}, options.loginUrlParameters || {}); options.loginUrlParameters.hd = Accounts._options.restrictCreationByEmailDomain; } var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); Google.requestCredential(options, credentialRequestCompleteCallback); }; } else { Accounts.addAutopublishFields({ forLoggedInUser: _.map( // publish access token since it can be used from the client (if // transmitted over ssl or on // localhost). https://developers.google.com/accounts/docs/OAuth2UserAgent // refresh token probably shouldn't be sent down. Google.whitelistedFields.concat(['accessToken', 'expiresAt']), // don't publish refresh token function (subfield) { return 'services.google.' + subfield; }), forOtherUsers: _.map( // even with autopublish, no legitimate web app should be // publishing all users' emails _.without(Google.whitelistedFields, 'email', 'verified_email'), function (subfield) { return 'services.google.' + subfield; }) }); } ================================================ FILE: packages/accounts-google/google_login_button.css ================================================ #login-buttons-image-google { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAADCklEQVQ4jSXSy2ucVRjA4d97zvdNJpPJbTJJE9rYaCINShZtRCFIA1bbLryBUlyoLQjqVl12W7UbN4qb1gtuYhFRRBCDBITaesFbbI3RFBLSptEY05l0ZjLfnMvrov/Bs3gAcF71x6VVHTk+o8nDH+hrH89rUK9Z9Yaen57S3wVtGaMBNGC0IegWKIDxTtVaOHVugZVmH3HX3Zz+4l+W1xvkOjuZfPsspY4CNkZELEgEIJKwYlBjEwjec/mfCMVuorVs76R8+P0KYMmP30U2dT8eIZqAR2ipRcWjEYxGSCRhV08e04oYMoxYLi97EI9YCJ0FHBYbIVGDlUBLwRlLIuYW6chEmQt/rJO09RJjhjEJEYvJYGNhkbUhw43OXtIWDFRq9G87nAaSK6sVRm8r8fzRMWbOX2Xx7ypd7ZET03sQhDOz73DqSJOrd+7HSo4QIu0Nx/4rOzx+cRXZ9+z7+uqJ+3hiepxK3fHZT2tMjXYzOtzL6dmznPzhLexgN0QlxAAYxAlqUqRmkf5j59RlNQ6MFHhgcpCTTx8EUb5e+plD7x4jjg1ANCAgrRQAdR7xKXjBlGyLYi7PxaUmb8z8xcpGHVXLHaXdjI0egKyJiQYTEhSPREVIEUBNC+Mqm+xpz3j0njLPHB2nsh1QgeG+IS48dYbD5YNoo0ZUAbVEuTUoKuBSZOarX/WhyQn6eg2+usDWf0s0tq8zNPYk+WI/Lnge++hlvlyfQ3NdECzGRWKwEEA0qNY251n69kV6+Y0kbaCZoebG2X3oU7pKoyxuXOPe945zs9DCeosGIXoBDyaLdf6ce4Hbk+/Y299ksKtAuaeNsiyw8c1LKIZ95b0MdgxA5giixACpTxEPSau6QdFfI5/2cLPmEW+JAQrtJUJzDXF1dkwHzVodJMX4HFEcQQMaFdPeM0Jb/4PUtzzaLKAhRyJFwo6lbegRNFfk819muV5dR4JBQoQdQ2xFiDmSNDHiaptamR9Gq5cQ18AledrGDpOfeI5Lq8u88smbhMRisoSAgAYghdfn5H/JkHuRZ1owLAAAAABJRU5ErkJggg==); } ================================================ FILE: packages/accounts-google/package.js ================================================ Package.describe({ summary: "Login service for Google accounts", version: "1.0.10" }); Package.onUse(function(api) { api.use(['underscore', 'random']); api.use('accounts-base', ['client', 'server']); // Export Accounts (etc) to packages using this one. api.imply('accounts-base', ['client', 'server']); api.use('accounts-oauth', ['client', 'server']); api.use('google', ['client', 'server']); api.addFiles('google_login_button.css', 'client'); api.addFiles("google.js"); }); ================================================ FILE: packages/accounts-meetup/.gitignore ================================================ .build* ================================================ FILE: packages/accounts-meetup/README.md ================================================ # accounts-meetup A login service for Meetup. See the [project page](https://www.meteor.com/accounts) on Meteor Accounts for more details. ================================================ FILE: packages/accounts-meetup/meetup.js ================================================ Accounts.oauth.registerService('meetup'); if (Meteor.isClient) { Meteor.loginWithMeetup = function(options, callback) { // support a callback without options if (! callback && typeof options === "function") { callback = options; options = null; } var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); Meetup.requestCredential(options, credentialRequestCompleteCallback); }; } else { Accounts.addAutopublishFields({ // publish all fields including access token, which can legitimately // be used from the client (if transmitted over ssl or on // localhost). http://www.meetup.com/meetup_api/auth/#oauth2implicit forLoggedInUser: ['services.meetup'], forOtherUsers: ['services.meetup.id'] }); } ================================================ FILE: packages/accounts-meetup/meetup_login_button.css ================================================ #login-buttons-image-meetup.login-image { top: 8px; height: 11px; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAYAAAB24g05AAAEJGlDQ1BJQ0MgUHJvZmlsZQAAOBGFVd9v21QUPolvUqQWPyBYR4eKxa9VU1u5GxqtxgZJk6XtShal6dgqJOQ6N4mpGwfb6baqT3uBNwb8AUDZAw9IPCENBmJ72fbAtElThyqqSUh76MQPISbtBVXhu3ZiJ1PEXPX6yznfOec7517bRD1fabWaGVWIlquunc8klZOnFpSeTYrSs9RLA9Sr6U4tkcvNEi7BFffO6+EdigjL7ZHu/k72I796i9zRiSJPwG4VHX0Z+AxRzNRrtksUvwf7+Gm3BtzzHPDTNgQCqwKXfZwSeNHHJz1OIT8JjtAq6xWtCLwGPLzYZi+3YV8DGMiT4VVuG7oiZpGzrZJhcs/hL49xtzH/Dy6bdfTsXYNY+5yluWO4D4neK/ZUvok/17X0HPBLsF+vuUlhfwX4j/rSfAJ4H1H0qZJ9dN7nR19frRTeBt4Fe9FwpwtN+2p1MXscGLHR9SXrmMgjONd1ZxKzpBeA71b4tNhj6JGoyFNp4GHgwUp9qplfmnFW5oTdy7NamcwCI49kv6fN5IAHgD+0rbyoBc3SOjczohbyS1drbq6pQdqumllRC/0ymTtej8gpbbuVwpQfyw66dqEZyxZKxtHpJn+tZnpnEdrYBbueF9qQn93S7HQGGHnYP7w6L+YGHNtd1FJitqPAR+hERCNOFi1i1alKO6RQnjKUxL1GNjwlMsiEhcPLYTEiT9ISbN15OY/jx4SMshe9LaJRpTvHr3C/ybFYP1PZAfwfYrPsMBtnE6SwN9ib7AhLwTrBDgUKcm06FSrTfSj187xPdVQWOk5Q8vxAfSiIUc7Z7xr6zY/+hpqwSyv0I0/QMTRb7RMgBxNodTfSPqdraz/sDjzKBrv4zu2+a2t0/HHzjd2Lbcc2sG7GtsL42K+xLfxtUgI7YHqKlqHK8HbCCXgjHT1cAdMlDetv4FnQ2lLasaOl6vmB0CMmwT/IPszSueHQqv6i/qluqF+oF9TfO2qEGTumJH0qfSv9KH0nfS/9TIp0Wboi/SRdlb6RLgU5u++9nyXYe69fYRPdil1o1WufNSdTTsp75BfllPy8/LI8G7AUuV8ek6fkvfDsCfbNDP0dvRh0CrNqTbV7LfEEGDQPJQadBtfGVMWEq3QWWdufk6ZSNsjG2PQjp3ZcnOWWing6noonSInvi0/Ex+IzAreevPhe+CawpgP1/pMTMDo64G0sTCXIM+KdOnFWRfQKdJvQzV1+Bt8OokmrdtY2yhVX2a+qrykJfMq4Ml3VR4cVzTQVz+UoNne4vcKLoyS+gyKO6EHe+75Fdt0Mbe5bRIf/wjvrVmhbqBN97RD1vxrahvBOfOYzoosH9bq94uejSOQGkVM6sN/7HelL4t10t9F4gPdVzydEOx83Gv+uNxo7XyL/FtFl8z9ZAHF4bBsrEwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAiFJREFUKBV9Ur+PEkEU/t7sLO7yUyB6whmBEPWUxCiaaGFtNDH+D+ZabWxo7GxI7hL/BctrjbXdxYqLV1gIHAq5AIdHCMguByw741vUxMqXzExm5v343vc9+ljdfaOOe69Dnods4ryKRiICRNAAiFdwQvPOb+s7kRa+T8K2MUzGdqR7+OVhYu8dYoDy2W0cBPzHOAmxn3cBMK2XlTvyYuGKvvz8BdTSIxgGSAjOxC5/zAiZEKaJ1dmcgSgGwv+M1o7G4G1e8uXVZ0/91L0yw1TAavU7TBhryDAlJicn6LQ7uFkqQRgCgltBOLz282uflQzFuGdp4OdipYfDIQ0GAxSLRfT7ffYL46jVgmBUt+LcJNvYcdBu1HH72nVY6SRJSQY0w3tbreK43YbN5OQLBTQbDSRTKeRyOcQTcXza30eUYR8e1PD123dktrexYUhIzTyTdQ4wBC25hXw2gylXefTksXamDm3d2MJ4MkG320On08Ho9BSlu2XaKOQxb7YgfaUCZvCqUvH9lUdKaXJdV9u2xZxKYmNiiekw0e31dKNeV+UH94MYeTZfaOlPXROLJWJKswQGKwGk4gloViJQXiuegaCGkCgmU7CymyI6X64C3Wk2k3JYO5jZe+/hOFPSLJHgauuh4cqaByhAEJxBouAvETJp4bhmOJ3GKJdZyiOtdhapSNOMW1yQfBaTheb9X2MV/r4ZplRIROQP21YjK/ThF08c3oOAma3kAAAAAElFTkSuQmCC); } ================================================ FILE: packages/accounts-meetup/package.js ================================================ Package.describe({ summary: "Login service for Meetup accounts", version: "1.0.10" }); Package.onUse(function(api) { api.use('accounts-base', ['client', 'server']); // Export Accounts (etc) to packages using this one. api.imply('accounts-base', ['client', 'server']); api.use('accounts-oauth', ['client', 'server']); api.use('meetup', ['client', 'server']); api.addFiles('meetup_login_button.css', 'client'); api.addFiles("meetup.js"); }); ================================================ FILE: packages/accounts-meteor-developer/.gitignore ================================================ .build* ================================================ FILE: packages/accounts-meteor-developer/README.md ================================================ # accounts-meteor-developer A login service for Meteor developer accounts. See the project page on [Meteor Accounts](https://www.meteor.com/accounts) and Meteor [Developer Accounts](https://www.meteor.com/services/developer-accounts) for more details. ================================================ FILE: packages/accounts-meteor-developer/meteor-developer-login-button.css ================================================ #login-buttons-image-meteor-developer { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAEJGlDQ1BJQ0MgUHJvZmlsZQAAOBGFVd9v21QUPolvUqQWPyBYR4eKxa9VU1u5GxqtxgZJk6XtShal6dgqJOQ6N4mpGwfb6baqT3uBNwb8AUDZAw9IPCENBmJ72fbAtElThyqqSUh76MQPISbtBVXhu3ZiJ1PEXPX6yznfOec7517bRD1fabWaGVWIlquunc8klZOnFpSeTYrSs9RLA9Sr6U4tkcvNEi7BFffO6+EdigjL7ZHu/k72I796i9zRiSJPwG4VHX0Z+AxRzNRrtksUvwf7+Gm3BtzzHPDTNgQCqwKXfZwSeNHHJz1OIT8JjtAq6xWtCLwGPLzYZi+3YV8DGMiT4VVuG7oiZpGzrZJhcs/hL49xtzH/Dy6bdfTsXYNY+5yluWO4D4neK/ZUvok/17X0HPBLsF+vuUlhfwX4j/rSfAJ4H1H0qZJ9dN7nR19frRTeBt4Fe9FwpwtN+2p1MXscGLHR9SXrmMgjONd1ZxKzpBeA71b4tNhj6JGoyFNp4GHgwUp9qplfmnFW5oTdy7NamcwCI49kv6fN5IAHgD+0rbyoBc3SOjczohbyS1drbq6pQdqumllRC/0ymTtej8gpbbuVwpQfyw66dqEZyxZKxtHpJn+tZnpnEdrYBbueF9qQn93S7HQGGHnYP7w6L+YGHNtd1FJitqPAR+hERCNOFi1i1alKO6RQnjKUxL1GNjwlMsiEhcPLYTEiT9ISbN15OY/jx4SMshe9LaJRpTvHr3C/ybFYP1PZAfwfYrPsMBtnE6SwN9ib7AhLwTrBDgUKcm06FSrTfSj187xPdVQWOk5Q8vxAfSiIUc7Z7xr6zY/+hpqwSyv0I0/QMTRb7RMgBxNodTfSPqdraz/sDjzKBrv4zu2+a2t0/HHzjd2Lbcc2sG7GtsL42K+xLfxtUgI7YHqKlqHK8HbCCXgjHT1cAdMlDetv4FnQ2lLasaOl6vmB0CMmwT/IPszSueHQqv6i/qluqF+oF9TfO2qEGTumJH0qfSv9KH0nfS/9TIp0Wboi/SRdlb6RLgU5u++9nyXYe69fYRPdil1o1WufNSdTTsp75BfllPy8/LI8G7AUuV8ek6fkvfDsCfbNDP0dvRh0CrNqTbV7LfEEGDQPJQadBtfGVMWEq3QWWdufk6ZSNsjG2PQjp3ZcnOWWing6noonSInvi0/Ex+IzAreevPhe+CawpgP1/pMTMDo64G0sTCXIM+KdOnFWRfQKdJvQzV1+Bt8OokmrdtY2yhVX2a+qrykJfMq4Ml3VR4cVzTQVz+UoNne4vcKLoyS+gyKO6EHe+75Fdt0Mbe5bRIf/wjvrVmhbqBN97RD1vxrahvBOfOYzoosH9bq94uejSOQGkVM6sN/7HelL4t10t9F4gPdVzydEOx83Gv+uNxo7XyL/FtFl8z9ZAHF4bBsrEwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAg5JREFUOBFdU7uuqVEQHr/7/RIhJMLZUVGrdAoP4AEUXkC1T6dSKZQ6TyAkEvtUFGqVRKGlkRCFwm27zZkZ/rV/eyXWXP75Zr6ZNQAA8PdP0zTxBYNB9c3j8Sj9V/x7ApPJJIGhYEhkoVDAer0uutfrRbPZ/JaI4k1IB9xuN1itVrher1QA4H6/w/l8hsViAcfjETKZDBAjOBwOcLlcJIYvjcFOp1M+7HY7keFwWMC9Xg+SySSs12sBUHXgeOOxMPh0OkGxWIRAIADz+RxmsxnUajUolUoSy8z42Gw2KcY6MyX2AJFIRHpqt9vcCm63WxyPx0hsxOZrMpm89e3z+dBisTx9DocDiYUYo9FIgVihOYhNfWOr1UJihLlcTiV7JQE0PhFX43O73UQ+Hg+RfNFQke1utytJqGWkJ9dgv99DPB7n1uD78i1Sv7hPwoppt9vlBZbLpdgyA9KQpi4Zy+WyVFAlSdEZcPXBYID5fF5i/X6/SJ4shkLPpZlOp4JdrVZYrVZxOByqXP1+/wmggvrgZWN5iMwilUrh178v7HQ6mM1mxffx5wM3m40kaTab4kskEiJ5ARnHvSCzeBlKptNp0RuNhiT4/PspNrdrACMvg3ygZcFoNIqxWExehenx7nNi3otKpSK66v1ZnX0/fyb+o0hfL5++HxyjA/WCBtxPAoNT0XS5XGrR1PYZiv4Hdhyfo4t1ww8AAAAASUVORK5CYII=); } ================================================ FILE: packages/accounts-meteor-developer/meteor-developer.js ================================================ Accounts.oauth.registerService("meteor-developer"); if (Meteor.isClient) { Meteor.loginWithMeteorDeveloperAccount = function (options, callback) { // support a callback without options if (! callback && typeof options === "function") { callback = options; options = null; } var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); MeteorDeveloperAccounts.requestCredential(options, credentialRequestCompleteCallback); }; } else { Accounts.addAutopublishFields({ // publish all fields including access token, which can legitimately be used // from the client (if transmitted over ssl or on localhost). forLoggedInUser: ['services.meteor-developer'], forOtherUsers: [ 'services.meteor-developer.username', 'services.meteor-developer.profile', 'services.meteor-developer.id' ] }); } ================================================ FILE: packages/accounts-meteor-developer/package.js ================================================ Package.describe({ summary: "Login service for Meteor developer accounts", version: "1.0.10" }); Package.onUse(function (api) { api.use(['underscore', 'random']); api.use('accounts-base', ['client', 'server']); // Export Accounts (etc) to packages using this one. api.imply('accounts-base', ['client', 'server']); api.use('accounts-oauth', ['client', 'server']); api.use('meteor-developer', ['client', 'server']); api.addFiles("meteor-developer.js"); api.addFiles("meteor-developer-login-button.css", "client"); }); ================================================ FILE: packages/accounts-oauth/.gitignore ================================================ .build* ================================================ FILE: packages/accounts-oauth/README.md ================================================ # accounts-oauth Common functionality for OAuth-based login services. See the [project page](https://www.meteor.com/accounts) on Meteor Accounts for more details. ================================================ FILE: packages/accounts-oauth/oauth_client.js ================================================ // Documentation for Meteor.loginWithExternalService /** * @name loginWith * @memberOf Meteor * @function * @summary Log the user in using an external service. * @locus Client * @param {Object} [options] * @param {String[]} options.requestPermissions A list of permissions to request from the user. * @param {Boolean} options.requestOfflineToken If true, asks the user for permission to act on their behalf when offline. This stores an additional offline token in the `services` field of the user document. Currently only supported with Google. * @param {Object} options.loginUrlParameters Provide additional parameters to the authentication uri. Currently only supported with Google {@url https://developers.google.com/identity/protocols/OpenIDConnect#authenticationuriparameters}. * @param {String} options.loginHint An email address that the external service will use to pre-fill the login prompt. Currently only supported with Meteor developer accounts and Google accounts. If used with Google, the Google User ID can also be passed. * @param {String} options.loginStyle Login style ("popup" or "redirect", defaults to the login service configuration). The "popup" style opens the login page in a separate popup window, which is generally preferred because the Meteor application doesn't need to be reloaded. The "redirect" style redirects the Meteor application's window to the login page, and the login service provider redirects back to the Meteor application which is then reloaded. The "redirect" style can be used in situations where a popup window can't be opened, such as in a mobile UIWebView. The "redirect" style however relies on session storage which isn't available in Safari private mode, so the "popup" style will be forced if session storage can't be used. * @param {String} options.redirectUrl If using "redirect" login style, the user will be returned to this URL after authorisation has been completed. * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. The callback cannot be called if you are using the "redirect" `loginStyle`, because the app will have reloaded in the meantime; try using [client-side login hooks](#accounts_onlogin) instead. * @importFromPackage meteor */ // Allow server to specify a specify subclass of errors. We should come // up with a more generic way to do this! var convertError = function (err) { if (err && err instanceof Meteor.Error && err.error === Accounts.LoginCancelledError.numericError) return new Accounts.LoginCancelledError(err.reason); else return err; }; // For the redirect login flow, the final step is that we're // redirected back to the application. The credentialToken for this // login attempt is stored in the reload migration data, and the // credentialSecret for a successful login is stored in session // storage. Meteor.startup(function () { var oauth = OAuth.getDataAfterRedirect(); if (! oauth) return; // We'll only have the credentialSecret if the login completed // successfully. However we still call the login method anyway to // retrieve the error if the login was unsuccessful. var methodName = 'login'; var methodArguments = [{oauth: _.pick(oauth, 'credentialToken', 'credentialSecret')}]; Accounts.callLoginMethod({ methodArguments: methodArguments, userCallback: function (err) { // The redirect login flow is complete. Construct an // `attemptInfo` object with the login result, and report back // to the code which initiated the login attempt // (e.g. accounts-ui, when that package is being used). err = convertError(err); Accounts._pageLoadLogin({ type: oauth.loginService, allowed: !err, error: err, methodName: methodName, methodArguments: methodArguments }); } }); }); // Send an OAuth login method to the server. If the user authorized // access in the popup this should log the user in, otherwise // nothing should happen. Accounts.oauth.tryLoginAfterPopupClosed = function(credentialToken, callback) { var credentialSecret = OAuth._retrieveCredentialSecret(credentialToken) || null; Accounts.callLoginMethod({ methodArguments: [{oauth: { credentialToken: credentialToken, credentialSecret: credentialSecret }}], userCallback: callback && function (err) { callback(convertError(err)); }}); }; Accounts.oauth.credentialRequestCompleteHandler = function(callback) { return function (credentialTokenOrError) { if(credentialTokenOrError && credentialTokenOrError instanceof Error) { callback && callback(credentialTokenOrError); } else { Accounts.oauth.tryLoginAfterPopupClosed(credentialTokenOrError, callback); } }; }; ================================================ FILE: packages/accounts-oauth/oauth_common.js ================================================ Accounts.oauth = {}; var services = {}; // Helper for registering OAuth based accounts packages. // On the server, adds an index to the user collection. Accounts.oauth.registerService = function (name) { if (_.has(services, name)) throw new Error("Duplicate service: " + name); services[name] = true; if (Meteor.server) { // Accounts.updateOrCreateUserFromExternalService does a lookup by this id, // so this should be a unique index. You might want to add indexes for other // fields returned by your service (eg services.github.login) but you can do // that in your app. Meteor.users._ensureIndex('services.' + name + '.id', {unique: 1, sparse: 1}); } }; // Removes a previously registered service. // This will disable logging in with this service, and serviceNames() will not // contain it. // It's worth noting that already logged in users will remain logged in unless // you manually expire their sessions. Accounts.oauth.unregisterService = function (name) { if (!_.has(services, name)) throw new Error("Service not found: " + name); delete services[name]; }; Accounts.oauth.serviceNames = function () { return _.keys(services); }; ================================================ FILE: packages/accounts-oauth/oauth_server.js ================================================ // Listen to calls to `login` with an oauth option set. This is where // users actually get logged in to meteor via oauth. Accounts.registerLoginHandler(function (options) { if (!options.oauth) return undefined; // don't handle check(options.oauth, { credentialToken: String, // When an error occurs while retrieving the access token, we store // the error in the pending credentials table, with a secret of // null. The client can call the login method with a secret of null // to retrieve the error. credentialSecret: Match.OneOf(null, String) }); var result = OAuth.retrieveCredential(options.oauth.credentialToken, options.oauth.credentialSecret); if (!result) { // OAuth credentialToken is not recognized, which could be either // because the popup was closed by the user before completion, or // some sort of error where the oauth provider didn't talk to our // server correctly and closed the popup somehow. // // We assume it was user canceled and report it as such, using a // numeric code that the client recognizes (XXX this will get // replaced by a symbolic error code at some point // https://trello.com/c/kMkw800Z/53-official-ddp-specification). This // will mask failures where things are misconfigured such that the // server doesn't see the request but does close the window. This // seems unlikely. // // XXX we want `type` to be the service name such as "facebook" return { type: "oauth", error: new Meteor.Error( Accounts.LoginCancelledError.numericError, "No matching login attempt found") }; } if (result instanceof Error) // We tried to login, but there was a fatal error. Report it back // to the user. throw result; else { if (!_.contains(Accounts.oauth.serviceNames(), result.serviceName)) { // serviceName was not found in the registered services list. // This could happen because the service never registered itself or // unregisterService was called on it. return { type: "oauth", error: new Meteor.Error( Accounts.LoginCancelledError.numericError, "No registered oauth service found for: " + result.serviceName) }; } return Accounts.updateOrCreateUserFromExternalService(result.serviceName, result.serviceData, result.options); } }); ================================================ FILE: packages/accounts-oauth/oauth_tests.js ================================================ // XXX Add a test to ensure that successful logins call Accounts.updateOrCreateUserFromExternalService // XXX Add a test to ensure that a missing or failed loginResult is handled correctly ================================================ FILE: packages/accounts-oauth/package.js ================================================ Package.describe({ summary: "Common code for OAuth-based login services", version: "1.1.13" }); Package.onUse(function (api) { api.use('underscore', ['client', 'server']); api.use('random', ['client', 'server']); api.use('check', ['client', 'server']); api.use('webapp', 'server'); api.use('accounts-base', ['client', 'server']); // Export Accounts (etc) to packages using this one. api.imply('accounts-base', ['client', 'server']); api.use('oauth'); api.addFiles('oauth_common.js'); api.addFiles('oauth_client.js', 'client'); api.addFiles('oauth_server.js', 'server'); }); Package.onTest(function (api) { api.addFiles("oauth_tests.js", 'server'); }); ================================================ FILE: packages/accounts-password/.gitignore ================================================ .build* ================================================ FILE: packages/accounts-password/README.md ================================================ # accounts-password A login service that enables secure password-based login. See the [project page](https://www.meteor.com/accounts) on Meteor Accounts for more details. ================================================ FILE: packages/accounts-password/email_templates.js ================================================ function greet(welcomeMsg) { return function(user, url) { var greeting = (user.profile && user.profile.name) ? ("Hello " + user.profile.name + ",") : "Hello,"; return `${greeting} ${welcomeMsg}, simply click the link below. ${url} Thanks. `; }; } /** * @summary Options to customize emails sent from the Accounts system. * @locus Server * @importFromPackage accounts-base */ Accounts.emailTemplates = { from: "Meteor Accounts ", siteName: Meteor.absoluteUrl().replace(/^https?:\/\//, '').replace(/\/$/, ''), resetPassword: { subject: function(user) { return "How to reset your password on " + Accounts.emailTemplates.siteName; }, text: function(user, url) { var greeting = (user.profile && user.profile.name) ? ("Hello " + user.profile.name + ",") : "Hello,"; return `${greeting} To reset your password, simply click the link below. ${url} Thanks. `; } }, verifyEmail: { subject: function(user) { return "How to verify email address on " + Accounts.emailTemplates.siteName; }, text: greet("To verify your account email") }, enrollAccount: { subject: function(user) { return "An account has been created for you on " + Accounts.emailTemplates.siteName; }, text: greet("To start using the service") } }; ================================================ FILE: packages/accounts-password/email_tests.js ================================================ var resetPasswordToken; var verifyEmailToken; var enrollAccountToken; Accounts._isolateLoginTokenForTest(); if (Meteor.isServer) { Accounts.removeDefaultRateLimit(); } testAsyncMulti("accounts emails - reset password flow", [ function (test, expect) { this.randomSuffix = Random.id(); this.email = "Ada-intercept@example.com" + this.randomSuffix; // Create the user with another email and add the tested for email later, // so we can test whether forgotPassword respects the passed in email Accounts.createUser({email: "another@example.com" + this.randomSuffix, password: 'foobar'}, expect((error) => { test.equal(error, undefined); Meteor.call("addEmailForTestAndVerify", this.email); })); }, function (test, expect) { Accounts.forgotPassword({email: this.email}, expect((error) => { test.equal(error, undefined); })); }, function (test, expect) { Accounts.connection.call( "getInterceptedEmails", this.email, expect((error, result) => { test.equal(error, undefined); test.notEqual(result, undefined); test.equal(result.length, 2); // the first is the email verification var options = result[1]; var re = new RegExp(Meteor.absoluteUrl() + "#/reset-password/(\\S*)"); var match = options.text.match(re); test.isTrue(match); resetPasswordToken = match[1]; test.isTrue(options.html.match(re)); test.equal(options.from, 'test@meteor.com'); test.equal(options.headers['My-Custom-Header'], 'Cool'); })); }, function (test, expect) { Accounts.resetPassword(resetPasswordToken, "newPassword", expect((error) => { test.isFalse(error); })); }, function (test, expect) { Meteor.logout(expect((error) => { test.equal(error, undefined); test.equal(Meteor.user(), null); })); }, function (test, expect) { Meteor.loginWithPassword( {email: this.email}, "newPassword", expect((error) => { test.isFalse(error); })); }, function (test, expect) { Meteor.logout(expect((error) => { test.equal(error, undefined); test.equal(Meteor.user(), null); })); } ]); testAsyncMulti(`accounts emails - \ reset password flow with case insensitive email`, [ function (test, expect) { this.randomSuffix = Random.id(); this.email = "Ada-intercept@example.com" + this.randomSuffix; // Create the user with another email and add the tested for email later, // so we can test whether forgotPassword respects the passed in email Accounts.createUser({email: "another@example.com" + this.randomSuffix, password: 'foobar'}, expect((error) => { test.equal(error, undefined); Meteor.call("addEmailForTestAndVerify", this.email); })); }, function (test, expect) { Accounts.forgotPassword({email: "ada-intercept@example.com" + this.randomSuffix}, expect((error) => { test.equal(error, undefined); })); }, function (test, expect) { Accounts.connection.call( "getInterceptedEmails", this.email, expect((error, result) => { test.equal(error, undefined); test.notEqual(result, undefined); test.equal(result.length, 2); // the first is the email verification var options = result[1]; var re = new RegExp(Meteor.absoluteUrl() + "#/reset-password/(\\S*)"); var match = options.text.match(re); test.isTrue(match); resetPasswordToken = match[1]; test.isTrue(options.html.match(re)); test.equal(options.from, 'test@meteor.com'); test.equal(options.headers['My-Custom-Header'], 'Cool'); })); }, function (test, expect) { Accounts.resetPassword(resetPasswordToken, "newPassword", expect((error) => { test.isFalse(error); })); }, function (test, expect) { Meteor.logout(expect((error) => { test.equal(error, undefined); test.equal(Meteor.user(), null); })); }, function (test, expect) { Meteor.loginWithPassword( {email: this.email}, "newPassword", expect((error) => { test.isFalse(error); })); }, function (test, expect) { Meteor.logout(expect((error) => { test.equal(error, undefined); test.equal(Meteor.user(), null); })); } ]); var getVerifyEmailToken = function (email, test, expect) { Accounts.connection.call( "getInterceptedEmails", email, expect((error, result) => { test.equal(error, undefined); test.notEqual(result, undefined); test.equal(result.length, 1); var options = result[0]; var re = new RegExp(Meteor.absoluteUrl() + "#/verify-email/(\\S*)"); var match = options.text.match(re); test.isTrue(match); verifyEmailToken = match[1]; test.isTrue(options.html.match(re)); test.equal(options.from, 'test@meteor.com'); test.equal(options.headers['My-Custom-Header'], 'Cool'); })); }; var loggedIn = function (test, expect) { return expect((error) => { test.equal(error, undefined); test.isTrue(Meteor.user()); }); }; testAsyncMulti("accounts emails - verify email flow", [ function (test, expect) { this.email = Random.id() + "-intercept@example.com"; this.anotherEmail = Random.id() + "-intercept@example.com"; Accounts.createUser( {email: this.email, password: 'foobar'}, loggedIn(test, expect)); }, function (test, expect) { test.equal(Meteor.user().emails.length, 1); test.equal(Meteor.user().emails[0].address, this.email); test.isFalse(Meteor.user().emails[0].verified); // We should NOT be publishing things like verification tokens! test.isFalse(_.has(Meteor.user(), 'services')); }, function (test, expect) { getVerifyEmailToken(this.email, test, expect); }, function (test, expect) { // Log out, to test that verifyEmail logs us back in. Meteor.logout(expect((error) => { test.equal(error, undefined); test.equal(Meteor.user(), null); })); }, function (test, expect) { Accounts.verifyEmail(verifyEmailToken, loggedIn(test, expect)); }, function (test, expect) { test.equal(Meteor.user().emails.length, 1); test.equal(Meteor.user().emails[0].address, this.email); test.isTrue(Meteor.user().emails[0].verified); }, function (test, expect) { Accounts.connection.call( "addEmailForTestAndVerify", this.anotherEmail, expect((error, result) => { test.isFalse(error); test.equal(Meteor.user().emails.length, 2); test.equal(Meteor.user().emails[1].address, this.anotherEmail); test.isFalse(Meteor.user().emails[1].verified); })); }, function (test, expect) { getVerifyEmailToken(this.anotherEmail, test, expect); }, function (test, expect) { // Log out, to test that verifyEmail logs us back in. (And if we don't // do that, waitUntilLoggedIn won't be able to prevent race conditions.) Meteor.logout(expect((error) => { test.equal(error, undefined); test.equal(Meteor.user(), null); })); }, function (test, expect) { Accounts.verifyEmail(verifyEmailToken, loggedIn(test, expect)); }, function (test, expect) { test.equal(Meteor.user().emails[1].address, this.anotherEmail); test.isTrue(Meteor.user().emails[1].verified); }, function (test, expect) { Meteor.logout(expect((error) => { test.equal(error, undefined); test.equal(Meteor.user(), null); })); } ]); var getEnrollAccountToken = function (email, test, expect) { Accounts.connection.call( "getInterceptedEmails", email, expect((error, result) => { test.equal(error, undefined); test.notEqual(result, undefined); test.equal(result.length, 1); var options = result[0]; var re = new RegExp(Meteor.absoluteUrl() + "#/enroll-account/(\\S*)") var match = options.text.match(re); test.isTrue(match); enrollAccountToken = match[1]; test.isTrue(options.html.match(re)); test.equal(options.from, 'test@meteor.com'); test.equal(options.headers['My-Custom-Header'], 'Cool'); })); }; testAsyncMulti("accounts emails - enroll account flow", [ function (test, expect) { this.email = Random.id() + "-intercept@example.com"; Accounts.connection.call("createUserOnServer", this.email, expect((error, result) => { test.isFalse(error); var user = result; test.equal(user.emails.length, 1); test.equal(user.emails[0].address, this.email); test.isFalse(user.emails[0].verified); })); }, function (test, expect) { getEnrollAccountToken(this.email, test, expect); }, function (test, expect) { Accounts.resetPassword(enrollAccountToken, 'password', loggedIn(test, expect)); }, function (test, expect) { test.equal(Meteor.user().emails.length, 1); test.equal(Meteor.user().emails[0].address, this.email); test.isTrue(Meteor.user().emails[0].verified); }, function (test, expect) { Meteor.logout(expect((error) => { test.equal(error, undefined); test.equal(Meteor.user(), null); })); }, function (test, expect) { Meteor.loginWithPassword({email: this.email}, 'password', loggedIn(test ,expect)); }, function (test, expect) { test.equal(Meteor.user().emails.length, 1); test.equal(Meteor.user().emails[0].address, this.email); test.isTrue(Meteor.user().emails[0].verified); }, function (test, expect) { Meteor.logout(expect((error) => { test.equal(error, undefined); test.equal(Meteor.user(), null); })); } ]); ================================================ FILE: packages/accounts-password/email_tests_setup.js ================================================ // // a mechanism to intercept emails sent to addressing including // the string "intercept", storing them in an array that can then // be retrieved using the getInterceptedEmails method // var interceptedEmails = {}; // (email address) -> (array of options) // add html email templates that just contain the url Accounts.emailTemplates.resetPassword.html = Accounts.emailTemplates.enrollAccount.html = Accounts.emailTemplates.verifyEmail.html = function (user, url) { return url; }; // override the from address Accounts.emailTemplates.resetPassword.from = Accounts.emailTemplates.enrollAccount.from = Accounts.emailTemplates.verifyEmail.from = function (user) { return 'test@meteor.com'; }; // add a custom header to check against Accounts.emailTemplates.headers = { 'My-Custom-Header' : 'Cool' }; EmailTest.hookSend(function (options) { var to = options.to; if (!to || to.indexOf('intercept') === -1) { return true; // go ahead and send } else { if (!interceptedEmails[to]) interceptedEmails[to] = []; interceptedEmails[to].push(options); return false; // skip sending } }); Meteor.methods({ getInterceptedEmails: function (email) { check(email, String); return interceptedEmails[email]; }, addEmailForTestAndVerify: function (email) { check(email, String); Meteor.users.update( {_id: this.userId}, {$push: {emails: {address: email, verified: false}}}); Accounts.sendVerificationEmail(this.userId, email); }, createUserOnServer: function (email) { check(email, String); var userId = Accounts.createUser({email: email}); Accounts.sendEnrollmentEmail(userId); return Meteor.users.findOne(userId); } }); ================================================ FILE: packages/accounts-password/package.js ================================================ Package.describe({ summary: "Password support for accounts", version: "1.1.11" }); Package.onUse(function(api) { api.use('npm-bcrypt@0.8.6_1'); api.use([ 'accounts-base', 'srp', 'sha', 'ejson', 'ddp' ], ['client', 'server']); // Export Accounts (etc) to packages using this one. api.imply('accounts-base', ['client', 'server']); api.use('email', ['server']); api.use('random', ['server']); api.use('check'); api.use('underscore'); api.use('ecmascript'); api.addFiles('email_templates.js', 'server'); api.addFiles('password_server.js', 'server'); api.addFiles('password_client.js', 'client'); }); Package.onTest(function(api) { api.use(['accounts-password', 'tinytest', 'test-helpers', 'tracker', 'accounts-base', 'random', 'email', 'underscore', 'check', 'ddp', 'ecmascript']); api.addFiles('password_tests_setup.js', 'server'); api.addFiles('password_tests.js', ['client', 'server']); api.addFiles('email_tests_setup.js', 'server'); api.addFiles('email_tests.js', 'client'); }); ================================================ FILE: packages/accounts-password/password_client.js ================================================ // Attempt to log in with a password. // // @param selector {String|Object} One of the following: // - {username: (username)} // - {email: (email)} // - a string which may be a username or email, depending on whether // it contains "@". // @param password {String} // @param callback {Function(error|undefined)} /** * @summary Log the user in with a password. * @locus Client * @param {Object | String} user * Either a string interpreted as a username or an email; or an object with a * single key: `email`, `username` or `id`. Username or email match in a case * insensitive manner. * @param {String} password The user's password. * @param {Function} [callback] Optional callback. * Called with no arguments on success, or with a single `Error` argument * on failure. * @importFromPackage meteor */ Meteor.loginWithPassword = function (selector, password, callback) { if (typeof selector === 'string') if (selector.indexOf('@') === -1) selector = {username: selector}; else selector = {email: selector}; Accounts.callLoginMethod({ methodArguments: [{ user: selector, password: Accounts._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. // 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 && callback(error); } else { callback && callback(); } } }); }; Accounts._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 && 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: Accounts._hashPassword(options.plaintextPassword) }], userCallback: callback }); } }; // Attempt to log in as a new user. /** * @summary Create a new user. * @locus Anywhere * @param {Object} options * @param {String} options.username A unique name for this user. * @param {String} options.email The user's email address. * @param {String} options.password The user's password. This is __not__ sent in plain text over the wire. * @param {Object} options.profile The user's profile, typically including the `name` field. * @param {Function} [callback] Client only, optional callback. Called with no arguments on success, or with a single `Error` argument on failure. * @importFromPackage accounts-base */ Accounts.createUser = function (options, callback) { options = _.clone(options); // we'll be modifying options if (typeof options.password !== 'string') throw new Error("options.password must be a string"); if (!options.password) { callback(new Meteor.Error(400, "Password may not be empty")); return; } // Replace password with the hashed password. options.password = Accounts._hashPassword(options.password); Accounts.callLoginMethod({ methodName: 'createUser', methodArguments: [options], userCallback: callback }); }; // Change password. Must be logged in. // // @param oldPassword {String|null} By default servers no longer allow // changing password without the old password, but they could so we // support passing no password to the server and letting it decide. // @param newPassword {String} // @param callback {Function(error|undefined)} /** * @summary Change the current user's password. Must be logged in. * @locus Client * @param {String} oldPassword The user's current password. This is __not__ sent in plain text over the wire. * @param {String} newPassword A new password for the user. This is __not__ sent in plain text over the wire. * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. * @importFromPackage accounts-base */ Accounts.changePassword = function (oldPassword, newPassword, callback) { if (!Meteor.user()) { callback && callback(new Error("Must be logged in to change password.")); return; } check(newPassword, String); if (!newPassword) { callback(new Meteor.Error(400, "Password may not be empty")); return; } Accounts.connection.apply( 'changePassword', [oldPassword ? Accounts._hashPassword(oldPassword) : null, Accounts._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 && callback(err); } else { // 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 // their password // // @param options {Object} // - email: (email) // @param callback (optional) {Function(error|undefined)} /** * @summary Request a forgot password email. * @locus Client * @param {Object} options * @param {String} options.email The email address to send a password reset link. * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. * @importFromPackage accounts-base */ Accounts.forgotPassword = function(options, callback) { if (!options.email) throw new Error("Must pass options.email"); Accounts.connection.call("forgotPassword", options, callback); }; // Resets a password based on a token originally created by // Accounts.forgotPassword, and then logs in the matching user. // // @param token {String} // @param newPassword {String} // @param callback (optional) {Function(error|undefined)} /** * @summary Reset the password for a user using a token received in email. Logs the user in afterwards. * @locus Client * @param {String} token The token retrieved from the reset password URL. * @param {String} newPassword A new password for the user. This is __not__ sent in plain text over the wire. * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. * @importFromPackage accounts-base */ Accounts.resetPassword = function(token, newPassword, callback) { check(token, String); check(newPassword, String); if (!newPassword) { callback(new Meteor.Error(400, "Password may not be empty")); return; } Accounts.callLoginMethod({ methodName: 'resetPassword', methodArguments: [token, Accounts._hashPassword(newPassword)], userCallback: callback}); }; // Verifies a user's email address based on a token originally // created by Accounts.sendVerificationEmail // // @param token {String} // @param callback (optional) {Function(error|undefined)} /** * @summary Marks the user's email address as verified. Logs the user in afterwards. * @locus Client * @param {String} token The token retrieved from the verification URL. * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. * @importFromPackage accounts-base */ Accounts.verifyEmail = function(token, callback) { if (!token) throw new Error("Need to pass token"); Accounts.callLoginMethod({ methodName: 'verifyEmail', methodArguments: [token], userCallback: callback}); }; ================================================ FILE: packages/accounts-password/password_server.js ================================================ /// BCRYPT var bcrypt = NpmModuleBcrypt; 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 /// Accounts._findUserByQuery = function (query) { var user = null; if (query.id) { user = Meteor.users.findOne({ _id: query.id }); } else { var fieldName; var fieldValue; if (query.username) { fieldName = 'username'; fieldValue = query.username; } else if (query.email) { fieldName = 'emails.address'; fieldValue = query.email; } else { throw new Error("shouldn't happen (validation missed something)"); } var selector = {}; selector[fieldName] = fieldValue; user = Meteor.users.findOne(selector); // If user is not found, try a case insensitive lookup if (!user) { selector = selectorForFastCaseInsensitiveLookup(fieldName, fieldValue); var candidateUsers = Meteor.users.find(selector).fetch(); // No match if multiple candidates are found if (candidateUsers.length === 1) { user = candidateUsers[0]; } } } return user; }; /** * @summary Finds the user with the specified username. * First tries to match username case sensitively; if that fails, it * tries case insensitively; but if more than one user matches the case * insensitive search, it returns null. * @locus Server * @param {String} username The username to look for * @returns {Object} A user if found, else null * @importFromPackage accounts-base */ Accounts.findUserByUsername = function (username) { return Accounts._findUserByQuery({ username: username }); }; /** * @summary Finds the user with the specified email. * First tries to match email case sensitively; if that fails, it * tries case insensitively; but if more than one user matches the case * insensitive search, it returns null. * @locus Server * @param {String} email The email address to look for * @returns {Object} A user if found, else null * @importFromPackage accounts-base */ Accounts.findUserByEmail = function (email) { return Accounts._findUserByQuery({ email: email }); }; // Generates a MongoDB selector that can be used to perform a fast case // insensitive lookup for the given fieldName and string. Since MongoDB does // not support case insensitive indexes, and case insensitive regex queries // are slow, we construct a set of prefix selectors for all permutations of // the first 4 characters ourselves. We first attempt to matching against // these, and because 'prefix expression' regex queries do use indexes (see // http://docs.mongodb.org/v2.6/reference/operator/query/regex/#index-use), // this has been found to greatly improve performance (from 1200ms to 5ms in a // test with 1.000.000 users). var selectorForFastCaseInsensitiveLookup = function (fieldName, string) { // Performance seems to improve up to 4 prefix characters var prefix = string.substring(0, Math.min(string.length, 4)); var orClause = _.map(generateCasePermutationsForString(prefix), function (prefixPermutation) { var selector = {}; selector[fieldName] = new RegExp('^' + Meteor._escapeRegExp(prefixPermutation)); return selector; }); var caseInsensitiveClause = {}; caseInsensitiveClause[fieldName] = new RegExp('^' + Meteor._escapeRegExp(string) + '$', 'i') return {$and: [{$or: orClause}, caseInsensitiveClause]}; } // Generates permutations of all case variations of a given string. var generateCasePermutationsForString = function (string) { var permutations = ['']; for (var i = 0; i < string.length; i++) { var ch = string.charAt(i); permutations = _.flatten(_.map(permutations, function (prefix) { var lowerCaseChar = ch.toLowerCase(); var upperCaseChar = ch.toUpperCase(); // Don't add unneccesary permutations when ch is not a letter if (lowerCaseChar === upperCaseChar) { return [prefix + ch]; } else { return [prefix + lowerCaseChar, prefix + upperCaseChar]; } })); } return permutations; } var checkForCaseInsensitiveDuplicates = function (fieldName, displayName, fieldValue, ownUserId) { // Some tests need the ability to add users with the same case insensitive // value, hence the _skipCaseInsensitiveChecksForTest check var skipCheck = _.has(Accounts._skipCaseInsensitiveChecksForTest, fieldValue); if (fieldValue && !skipCheck) { var matchedUsers = Meteor.users.find( selectorForFastCaseInsensitiveLookup(fieldName, fieldValue)).fetch(); if (matchedUsers.length > 0 && // If we don't have a userId yet, any match we find is a duplicate (!ownUserId || // Otherwise, check to see if there are multiple matches or a match // that is not us (matchedUsers.length > 1 || matchedUsers[0]._id !== ownUserId))) { throw new Meteor.Error(403, displayName + " already exists."); } } }; // XXX maybe this belongs in the check package var NonEmptyString = Match.Where(function (x) { check(x, String); return x.length > 0; }); var userQueryValidator = Match.Where(function (user) { check(user, { id: Match.Optional(NonEmptyString), username: Match.Optional(NonEmptyString), email: Match.Optional(NonEmptyString) }); if (_.keys(user).length !== 1) throw new Match.Error("User property must have exactly one field"); return true; }); 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. // Accounts.registerLoginHandler("password", function (options) { if (! options.password || options.srp) return undefined; // don't handle check(options, { user: userQueryValidator, password: passwordValidator }); var user = Accounts._findUserByQuery(options.user); if (!user) throw new Meteor.Error(403, "User not found"); 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) { 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) { 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 = Accounts._findUserByQuery(options.user); if (!user) throw new Meteor.Error(403, "User not found"); // 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}; }); /// /// CHANGING /// /** * @summary Change a user's username. Use this instead of updating the * database directly. The operation will fail if there is an existing user * with a username only differing in case. * @locus Server * @param {String} userId The ID of the user to update. * @param {String} newUsername A new username for the user. * @importFromPackage accounts-base */ Accounts.setUsername = function (userId, newUsername) { check(userId, NonEmptyString); check(newUsername, NonEmptyString); var user = Meteor.users.findOne(userId); if (!user) throw new Meteor.Error(403, "User not found"); var oldUsername = user.username; // Perform a case insensitive check for duplicates before update checkForCaseInsensitiveDuplicates('username', 'Username', newUsername, user._id); Meteor.users.update({_id: user._id}, {$set: {username: newUsername}}); // Perform another check after update, in case a matching user has been // inserted in the meantime try { checkForCaseInsensitiveDuplicates('username', 'Username', newUsername, user._id); } catch (ex) { // Undo update if the check fails Meteor.users.update({_id: user._id}, {$set: {username: oldUsername}}); throw ex; } }; // Let the user change their own password if they know the old // 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"); var user = Meteor.users.findOne(this.userId); if (!user) throw new Meteor.Error(403, "User not found"); 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 })); } 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 // be tricky, so we'll settle for just replacing all tokens other than // the one for the current connection. var currentToken = Accounts._getLoginToken(this.connection.id); Meteor.users.update( { _id: this.userId }, { $set: { 'services.password.bcrypt': hashed }, $pull: { 'services.resume.loginTokens': { hashedToken: { $ne: currentToken } } }, $unset: { 'services.password.reset': 1 } } ); return {passwordChanged: true}; }}); // Force change the users password. /** * @summary Forcibly change the password for a user. * @locus Server * @param {String} userId The id of the user to update. * @param {String} newPassword A new password for the user. * @param {Object} [options] * @param {Object} options.logout Logout all current connections with this userId (default: true) * @importFromPackage accounts-base */ Accounts.setPassword = function (userId, newPlaintextPassword, options) { options = _.extend({logout: true}, options); var user = Meteor.users.findOne(userId); if (!user) throw new Meteor.Error(403, "User not found"); var update = { $unset: { 'services.password.srp': 1, // XXX COMPAT WITH 0.8.1.3 'services.password.reset': 1 }, $set: {'services.password.bcrypt': hashPassword(newPlaintextPassword)} }; if (options.logout) { update.$unset['services.resume.loginTokens'] = 1; } Meteor.users.update({_id: user._id}, update); }; /// /// RESETTING VIA EMAIL /// // Method called by a user to request a password reset email. This is // the start of the reset process. Meteor.methods({forgotPassword: function (options) { check(options, {email: String}); var user = Accounts.findUserByEmail(options.email); if (!user) throw new Meteor.Error(403, "User not found"); const emails = _.pluck(user.emails || [], 'address'); const caseSensitiveEmail = _.find(emails, email => { return email.toLowerCase() === options.email.toLowerCase(); }); Accounts.sendResetPasswordEmail(user._id, caseSensitiveEmail); }}); // send the user an email with a link that when opened allows the user // to set a new password, without the old password. /** * @summary Send an email with a link the user can use to reset their password. * @locus Server * @param {String} userId The id of the user to send email to. * @param {String} [email] Optional. Which address of the user's to send the email to. This address must be in the user's `emails` list. Defaults to the first email in the list. * @importFromPackage accounts-base */ Accounts.sendResetPasswordEmail = function (userId, email) { // Make sure the user exists, and email is one of their addresses. var user = Meteor.users.findOne(userId); if (!user) throw new Error("Can't find user"); // pick the first email if we weren't passed an email. if (!email && user.emails && user.emails[0]) email = user.emails[0].address; // make sure we have a valid 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": tokenRecord }}); // before passing to template, update user object with new token Meteor._ensure(user, 'services', 'password').reset = tokenRecord; var resetPasswordUrl = Accounts.urls.resetPassword(token); var options = { to: email, from: Accounts.emailTemplates.resetPassword.from ? Accounts.emailTemplates.resetPassword.from(user) : Accounts.emailTemplates.from, subject: Accounts.emailTemplates.resetPassword.subject(user) }; if (typeof Accounts.emailTemplates.resetPassword.text === 'function') { options.text = Accounts.emailTemplates.resetPassword.text(user, resetPasswordUrl); } if (typeof Accounts.emailTemplates.resetPassword.html === 'function') options.html = Accounts.emailTemplates.resetPassword.html(user, resetPasswordUrl); if (typeof Accounts.emailTemplates.headers === 'object') { options.headers = Accounts.emailTemplates.headers; } Email.send(options); }; // send the user an email informing them that their account was created, with // a link that when opened both marks their email as verified and forces them // to choose their password. The email must be one of the addresses in the // user's emails field, or undefined to pick the first email automatically. // // This is not called automatically. It must be called manually if you // want to use enrollment emails. /** * @summary Send an email with a link the user can use to set their initial password. * @locus Server * @param {String} userId The id of the user to send email to. * @param {String} [email] Optional. Which address of the user's to send the email to. This address must be in the user's `emails` list. Defaults to the first email in the list. * @importFromPackage accounts-base */ Accounts.sendEnrollmentEmail = function (userId, email) { // XXX refactor! This is basically identical to sendResetPasswordEmail. // Make sure the user exists, and email is in their addresses. var user = Meteor.users.findOne(userId); if (!user) throw new Error("Can't find user"); // pick the first email if we weren't passed an email. if (!email && user.emails && user.emails[0]) email = user.emails[0].address; // make sure we have a valid 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": tokenRecord }}); // before passing to template, update user object with new token Meteor._ensure(user, 'services', 'password').reset = tokenRecord; var enrollAccountUrl = Accounts.urls.enrollAccount(token); var options = { to: email, from: Accounts.emailTemplates.enrollAccount.from ? Accounts.emailTemplates.enrollAccount.from(user) : Accounts.emailTemplates.from, subject: Accounts.emailTemplates.enrollAccount.subject(user) }; if (typeof Accounts.emailTemplates.enrollAccount.text === 'function') { options.text = Accounts.emailTemplates.enrollAccount.text(user, enrollAccountUrl); } if (typeof Accounts.emailTemplates.enrollAccount.html === 'function') options.html = Accounts.emailTemplates.enrollAccount.html(user, enrollAccountUrl); if (typeof Accounts.emailTemplates.headers === 'object') { options.headers = Accounts.emailTemplates.headers; } Email.send(options); }; // Take token from sendResetPasswordEmail or sendEnrollmentEmail, change // the users password, and log them in. Meteor.methods({resetPassword: function (token, newPassword) { var self = this; return Accounts._loginMethod( self, "resetPassword", arguments, "password", function () { check(token, String); check(newPassword, passwordValidator); var user = Meteor.users.findOne({ "services.password.reset.token": token}); if (!user) throw new Meteor.Error(403, "Token expired"); var email = user.services.password.reset.email; if (!_.include(_.pluck(user.emails || [], 'address'), email)) return { userId: user._id, 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 // of having a bad token set if things fail. var oldToken = Accounts._getLoginToken(self.connection.id); Accounts._setLoginToken(user._id, self.connection, null); var resetToOldToken = function () { Accounts._setLoginToken(user._id, self.connection, oldToken); }; try { // Update the user record by: // - 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( { _id: user._id, 'emails.address': email, 'services.password.reset.token': token }, {$set: {'services.password.bcrypt': hashed, 'emails.$.verified': true}, $unset: {'services.password.reset': 1, 'services.password.srp': 1}}); if (affectedRecords !== 1) return { userId: user._id, error: new Meteor.Error(403, "Invalid email") }; } catch (err) { resetToOldToken(); throw err; } // Replace all valid login tokens with new ones (changing // password should invalidate existing sessions). Accounts._clearAllLoginTokens(user._id); return {userId: user._id}; } ); }}); /// /// EMAIL VERIFICATION /// // send the user an email with a link that when opened marks that // address as verified /** * @summary Send an email with a link the user can use verify their email address. * @locus Server * @param {String} userId The id of the user to send email to. * @param {String} [email] Optional. Which address of the user's to send the email to. This address must be in the user's `emails` list. Defaults to the first unverified email in the list. * @importFromPackage accounts-base */ Accounts.sendVerificationEmail = function (userId, address) { // XXX Also generate a link using which someone can delete this // account if they own said address but weren't those who created // this account. // Make sure the user exists, and address is one of their addresses. var user = Meteor.users.findOne(userId); if (!user) throw new Error("Can't find user"); // pick the first unverified address if we weren't passed an address. if (!address) { var email = _.find(user.emails || [], function (e) { return !e.verified; }); address = (email || {}).address; if (!address) { throw new Error("That user has no unverified email addresses."); } } // make sure we have a valid address if (!address || !_.contains(_.pluck(user.emails || [], 'address'), address)) throw new Error("No such email address for user."); var tokenRecord = { token: Random.secret(), address: address, when: new Date()}; Meteor.users.update( {_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 = { to: address, from: Accounts.emailTemplates.verifyEmail.from ? Accounts.emailTemplates.verifyEmail.from(user) : Accounts.emailTemplates.from, subject: Accounts.emailTemplates.verifyEmail.subject(user) }; if (typeof Accounts.emailTemplates.verifyEmail.text === 'function') { options.text = Accounts.emailTemplates.verifyEmail.text(user, verifyEmailUrl); } if (typeof Accounts.emailTemplates.verifyEmail.html === 'function') options.html = Accounts.emailTemplates.verifyEmail.html(user, verifyEmailUrl); if (typeof Accounts.emailTemplates.headers === 'object') { options.headers = Accounts.emailTemplates.headers; } Email.send(options); }; // Take token from sendVerificationEmail, mark the email as verified, // and log them in. Meteor.methods({verifyEmail: function (token) { var self = this; return Accounts._loginMethod( self, "verifyEmail", arguments, "password", function () { check(token, String); var user = Meteor.users.findOne( {'services.email.verificationTokens.token': token}); if (!user) throw new Meteor.Error(403, "Verify email link expired"); var tokenRecord = _.find(user.services.email.verificationTokens, function (t) { return t.token == token; }); if (!tokenRecord) return { userId: user._id, error: new Meteor.Error(403, "Verify email link expired") }; var emailsRecord = _.find(user.emails, function (e) { return e.address == tokenRecord.address; }); if (!emailsRecord) return { userId: user._id, error: new Meteor.Error(403, "Verify email link is for unknown address") }; // By including the address in the query, we can use 'emails.$' in the // modifier to get a reference to the specific object in the emails // array. See // http://www.mongodb.org/display/DOCS/Updating/#Updating-The%24positionaloperator) // http://www.mongodb.org/display/DOCS/Updating#Updating-%24pull Meteor.users.update( {_id: user._id, 'emails.address': tokenRecord.address}, {$set: {'emails.$.verified': true}, $pull: {'services.email.verificationTokens': {address: tokenRecord.address}}}); return {userId: user._id}; } ); }}); /** * @summary Add an email address for a user. Use this instead of directly * updating the database. The operation will fail if there is a different user * with an email only differing in case. If the specified user has an existing * email only differing in case however, we replace it. * @locus Server * @param {String} userId The ID of the user to update. * @param {String} newEmail A new email address for the user. * @param {Boolean} [verified] Optional - whether the new email address should * be marked as verified. Defaults to false. * @importFromPackage accounts-base */ Accounts.addEmail = function (userId, newEmail, verified) { check(userId, NonEmptyString); check(newEmail, NonEmptyString); check(verified, Match.Optional(Boolean)); if (_.isUndefined(verified)) { verified = false; } var user = Meteor.users.findOne(userId); if (!user) throw new Meteor.Error(403, "User not found"); // Allow users to change their own email to a version with a different case // We don't have to call checkForCaseInsensitiveDuplicates to do a case // insensitive check across all emails in the database here because: (1) if // there is no case-insensitive duplicate between this user and other users, // then we are OK and (2) if this would create a conflict with other users // then there would already be a case-insensitive duplicate and we can't fix // that in this code anyway. var caseInsensitiveRegExp = new RegExp('^' + Meteor._escapeRegExp(newEmail) + '$', 'i'); var didUpdateOwnEmail = _.any(user.emails, function(email, index) { if (caseInsensitiveRegExp.test(email.address)) { Meteor.users.update({ _id: user._id, 'emails.address': email.address }, {$set: { 'emails.$.address': newEmail, 'emails.$.verified': verified }}); return true; } return false; }); // In the other updates below, we have to do another call to // checkForCaseInsensitiveDuplicates to make sure that no conflicting values // were added to the database in the meantime. We don't have to do this for // the case where the user is updating their email address to one that is the // same as before, but only different because of capitalization. Read the // big comment above to understand why. if (didUpdateOwnEmail) { return; } // Perform a case insensitive check for duplicates before update checkForCaseInsensitiveDuplicates('emails.address', 'Email', newEmail, user._id); Meteor.users.update({ _id: user._id }, { $addToSet: { emails: { address: newEmail, verified: verified } } }); // Perform another check after update, in case a matching user has been // inserted in the meantime try { checkForCaseInsensitiveDuplicates('emails.address', 'Email', newEmail, user._id); } catch (ex) { // Undo update if the check fails Meteor.users.update({_id: user._id}, {$pull: {emails: {address: newEmail}}}); throw ex; } } /** * @summary Remove an email address for a user. Use this instead of updating * the database directly. * @locus Server * @param {String} userId The ID of the user to update. * @param {String} email The email address to remove. * @importFromPackage accounts-base */ Accounts.removeEmail = function (userId, email) { check(userId, NonEmptyString); check(email, NonEmptyString); var user = Meteor.users.findOne(userId); if (!user) throw new Meteor.Error(403, "User not found"); Meteor.users.update({_id: user._id}, {$pull: {emails: {address: email}}}); } /// /// CREATING USERS /// // Shared createUser function called from the createUser method, both // if originates in client or server code. Calls user provided hooks, // does the actual user insertion. // // returns the user id var createUser = function (options) { // Unknown keys allowed, because a onCreateUserHook can take arbitrary // options. check(options, Match.ObjectIncluding({ username: Match.Optional(String), email: Match.Optional(String), password: Match.Optional(passwordValidator) })); var username = options.username; var email = options.email; if (!username && !email) throw new Meteor.Error(400, "Need to set a username or email"); var user = {services: {}}; if (options.password) { var hashed = hashPassword(options.password); user.services.password = { bcrypt: hashed }; } if (username) user.username = username; if (email) user.emails = [{address: email, verified: false}]; // Perform a case insensitive check before insert checkForCaseInsensitiveDuplicates('username', 'Username', username); checkForCaseInsensitiveDuplicates('emails.address', 'Email', email); var userId = Accounts.insertUserDoc(options, user); // Perform another check after insert, in case a matching user has been // inserted in the meantime try { checkForCaseInsensitiveDuplicates('username', 'Username', username, userId); checkForCaseInsensitiveDuplicates('emails.address', 'Email', email, userId); } catch (ex) { // Remove inserted user if the check fails Meteor.users.remove(userId); throw ex; } return userId; }; // method for create user. Requests come from the client. Meteor.methods({createUser: function (options) { var self = this; return Accounts._loginMethod( self, "createUser", arguments, "password", function () { // createUser() above does more checking. check(options, Object); if (Accounts._options.forbidClientAccountCreation) return { error: new Meteor.Error(403, "Signups forbidden") }; // Create user. result contains id and token. var userId = createUser(options); // safety belt. createUser is supposed to throw on error. send 500 error // instead of sending a verification email with empty userid. if (! userId) throw new Error("createUser failed to insert new user"); // If `Accounts._options.sendVerificationEmail` is set, register // a token to verify the user's primary email, and send it to // that address. if (options.email && Accounts._options.sendVerificationEmail) Accounts.sendVerificationEmail(userId, options.email); // client gets logged in as the new user afterwards. return {userId: userId}; } ); }}); // Create user directly on the server. // // Unlike the client version, this does not log you in as this user // after creation. // // returns userId or throws an error if it can't create // // XXX add another argument ("server options") that gets sent to onCreateUser, // which is always empty when called from the createUser method? eg, "admin: // true", which we want to prevent the client from setting, but which a custom // method calling Accounts.createUser could set? // Accounts.createUser = function (options, callback) { options = _.clone(options); // XXX allow an optional callback? if (callback) { throw new Error("Accounts.createUser with callback not supported on the server yet."); } return createUser(options); }; /// /// PASSWORD-SPECIFIC INDEXES ON USERS /// Meteor.users._ensureIndex('services.email.verificationTokens.token', {unique: 1, sparse: 1}); Meteor.users._ensureIndex('services.password.reset.token', {unique: 1, sparse: 1}); ================================================ FILE: packages/accounts-password/password_tests.js ================================================ Accounts._noConnectionCloseDelayForTest = true; if (Meteor.isServer) { Accounts.removeDefaultRateLimit(); Meteor.methods({ getResetToken: function () { var token = Meteor.users.findOne(this.userId).services.password.reset; return token; }, addSkipCaseInsensitiveChecksForTest: function (value) { Accounts._skipCaseInsensitiveChecksForTest[value] = true; }, removeSkipCaseInsensitiveChecksForTest: function (value) { delete Accounts._skipCaseInsensitiveChecksForTest[value]; }, countUsersOnServer: function (query) { return Meteor.users.find(query).count(); } }); } if (Meteor.isClient) (function () { // XXX note, only one test can do login/logout things at once! for // now, that is this test. Accounts._isolateLoginTokenForTest(); var addSkipCaseInsensitiveChecksForTest = function (value, test, expect) { Meteor.call('addSkipCaseInsensitiveChecksForTest', value); }; var removeSkipCaseInsensitiveChecksForTest = function (value, test, expect) { Meteor.call('removeSkipCaseInsensitiveChecksForTest', value); }; var createUserStep = function (test, expect) { // Hack because Tinytest does not clean the database between tests/runs this.randomSuffix = Random.id(10); this.username = 'AdaLovelace' + this.randomSuffix; this.email = "Ada-intercept@lovelace.com" + this.randomSuffix; this.password = 'password'; Accounts.createUser( {username: this.username, email: this.email, password: this.password}, loggedInAs(this.username, test, expect)); }; var logoutStep = function (test, expect) { Meteor.logout(expect(function (error) { if (error) { test.fail(error.message); } test.equal(Meteor.user(), null); })); }; var loggedInAs = function (someUsername, test, expect) { return expect(function (error) { if (error) { test.fail(error.message); } test.equal(Meteor.userId() && Meteor.user().username, someUsername); }); }; var loggedInUserHasEmail = function (someEmail, test, expect) { return expect(function (error) { if (error) { test.fail(error.message); } var user = Meteor.user(); test.isTrue(user && _.some(user.emails, function(email) { return email.address === someEmail; })); }); }; var expectError = function (expectedError, test, expect) { return expect(function (actualError) { test.equal(actualError && actualError.error, expectedError.error); test.equal(actualError && actualError.reason, expectedError.reason); }); }; var expectUserNotFound = function (test, expect) { return expectError(new Meteor.Error(403, "User not found"), test, expect); }; var waitForLoggedOutStep = function (test, expect) { pollUntil(expect, function () { return Meteor.userId() === null; }, 10 * 1000, 100); }; var invalidateLoginsStep = function (test, expect) { Meteor.call("testInvalidateLogins", 'fail', expect(function (error) { if (error) { test.fail(error.message); } })); }; var hideActualLoginErrorStep = function (test, expect) { Meteor.call("testInvalidateLogins", 'hide', expect(function (error) { if (error) { test.fail(error.message); } })); }; var validateLoginsStep = function (test, expect) { Meteor.call("testInvalidateLogins", false, expect(function (error) { if (error) { test.fail(error.message); } })); }; testAsyncMulti("passwords - basic login with password", [ function (test, expect) { // setup this.username = Random.id(); this.email = Random.id() + '-intercept@example.com'; this.password = 'password'; Accounts.createUser( {username: this.username, email: this.email, password: this.password}, loggedInAs(this.username, test, expect)); }, function (test, expect) { test.notEqual(Meteor.userId(), null); }, logoutStep, function (test, expect) { Meteor.loginWithPassword(this.username, this.password, loggedInAs(this.username, test, expect)); }, logoutStep, // This next step tests reactive contexts which are reactive on // Meteor.user(). function (test, expect) { // Set up a reactive context that only refreshes when Meteor.user() is // invalidated. var loaded = false; var handle = Tracker.autorun(function () { if (Meteor.user() && Meteor.user().emails) loaded = true; }); // At the beginning, we're not logged in. test.isFalse(loaded); Meteor.loginWithPassword(this.username, this.password, expect(function (error) { test.equal(error, undefined); test.notEqual(Meteor.userId(), null); // By the time of the login callback, the user should be loaded. test.isTrue(Meteor.user().emails); // Flushing should get us the rerun as well. Tracker.flush(); test.isTrue(loaded); handle.stop(); })); }, logoutStep, function (test, expect) { Meteor.loginWithPassword({username: this.username}, this.password, loggedInAs(this.username, test, expect)); }, logoutStep, function (test, expect) { Meteor.loginWithPassword(this.email, this.password, loggedInAs(this.username, test, expect)); }, logoutStep, function (test, expect) { Meteor.loginWithPassword({email: this.email}, this.password, loggedInAs(this.username, test, expect)); }, logoutStep ]); testAsyncMulti("passwords - plain text passwords", [ function (test, expect) { // setup this.username = Random.id(); this.email = Random.id() + '-intercept@example.com'; this.password = 'password'; // create user with raw password (no API, need to invoke callLoginMethod // directly) Accounts.callLoginMethod({ methodName: 'createUser', methodArguments: [{username: this.username, password: this.password}], userCallback: loggedInAs(this.username, test, expect) }); }, logoutStep, // check can login normally with this password. function(test, expect) { Meteor.loginWithPassword({username: this.username}, this.password, loggedInAs(this.username, test, expect)); }, logoutStep, // plain text password. no API for this, have to invoke callLoginMethod // directly. function (test, expect) { Accounts.callLoginMethod({ // wrong password methodArguments: [{user: {username: this.username}, password: 'wrong'}], userCallback: expect(function (error) { test.isTrue(error); test.isFalse(Meteor.user()); })}); }, function (test, expect) { Accounts.callLoginMethod({ // right password methodArguments: [{user: {username: this.username}, password: this.password}], userCallback: loggedInAs(this.username, test, expect) }); }, logoutStep ]); testAsyncMulti("passwords - logging in with case insensitive username", [ createUserStep, logoutStep, // We should be able to log in with the username in lower case function (test, expect) { Meteor.loginWithPassword( { username: "adalovelace" + this.randomSuffix }, this.password, loggedInAs(this.username, test, expect)); } ]); testAsyncMulti("passwords - logging in with case insensitive username " + "with non-ASCII characters", [ function (test, expect) { // Hack because Tinytest does not clean the database between tests/runs this.randomSuffix = Random.id(10); this.username = 'ÁdaLØvela😈e' + this.randomSuffix; this.password = 'password'; Accounts.createUser( {username: this.username, email: this.email, password: this.password}, loggedInAs(this.username, test, expect)); }, logoutStep, // We should be able to log in with the username in lower case function (test, expect) { Meteor.loginWithPassword( { username: "ádaløvela😈e" + this.randomSuffix }, this.password, loggedInAs(this.username, test, expect)); } ]); testAsyncMulti("passwords - logging in with case insensitive username " + "should escape regex special characters", [ createUserStep, logoutStep, // We shouldn't be able to log in with a regex expression for the username function (test, expect) { Meteor.loginWithPassword( { username: ".+" + this.randomSuffix }, this.password, expectUserNotFound(test, expect)); } ]); testAsyncMulti("passwords - logging in with case insensitive username " + "should require a match of the full string", [ createUserStep, logoutStep, // We shouldn't be able to log in with a partial match for the username function (test, expect) { Meteor.loginWithPassword( { username: "lovelace" + this.randomSuffix }, this.password, expectUserNotFound(test, expect)); } ]); testAsyncMulti("passwords - logging in with case insensitive username when " + "there are multiple matches", [ createUserStep, logoutStep, function (test, expect) { this.otherUsername = 'Adalovelace' + this.randomSuffix; addSkipCaseInsensitiveChecksForTest(this.otherUsername, test, expect); }, // Create another user with a username that only differs in case function (test, expect) { Accounts.createUser( { username: this.otherUsername, password: this.password }, loggedInAs(this.otherUsername, test, expect)); }, function (test, expect) { removeSkipCaseInsensitiveChecksForTest(this.otherUsername, test, expect); }, // We shouldn't be able to log in with the username in lower case function (test, expect) { Meteor.loginWithPassword( { username: "adalovelace" + this.randomSuffix }, this.password, expectUserNotFound(test, expect)); }, // We should still be able to log in with the username in original case function (test, expect) { Meteor.loginWithPassword( { username: this.username }, this.password, loggedInAs(this.username, test, expect)); } ]); testAsyncMulti("passwords - creating users with the same case insensitive " + "username", [ createUserStep, logoutStep, // Attempting to create another user with a username that only differs in // case should fail function (test, expect) { this.newUsername = 'adalovelace' + this.randomSuffix; Accounts.createUser( { username: this.newUsername, password: this.password }, expectError( new Meteor.Error(403, "Username already exists."), test, expect)); }, // Make sure the new user has not been inserted function (test, expect) { Meteor.call('countUsersOnServer', { username: this.newUsername }, expect(function (error, result) { test.equal(result, 0); })); } ]); testAsyncMulti("passwords - logging in with case insensitive email", [ createUserStep, logoutStep, // We should be able to log in with the email in lower case function (test, expect) { Meteor.loginWithPassword( { email: "ada-intercept@lovelace.com" + this.randomSuffix }, this.password, loggedInAs(this.username, test, expect)); } ]); testAsyncMulti("passwords - logging in with case insensitive email should " + "escape regex special characters", [ createUserStep, logoutStep, // We shouldn't be able to log in with a regex expression for the email function (test, expect) { Meteor.loginWithPassword( { email: ".+" + this.randomSuffix }, this.password, expectUserNotFound(test, expect)); } ]); testAsyncMulti("passwords - logging in with case insensitive email should " + "require a match of the full string", [ createUserStep, logoutStep, // We shouldn't be able to log in with a partial match for the email function (test, expect) { Meteor.loginWithPassword( { email: "com" + this.randomSuffix }, this.password, expectUserNotFound(test, expect)); } ]); testAsyncMulti("passwords - logging in with case insensitive email when " + "there are multiple matches", [ createUserStep, logoutStep, function (test, expect) { this.otherUsername = 'AdaLovelace' + Random.id(10); this.otherEmail = "ADA-intercept@lovelace.com" + this.randomSuffix; addSkipCaseInsensitiveChecksForTest(this.otherEmail, test, expect); }, // Create another user with an email that only differs in case function (test, expect) { Accounts.createUser( { username: this.otherUsername, email: this.otherEmail, password: this.password }, loggedInAs(this.otherUsername, test, expect)); }, function (test, expect) { removeSkipCaseInsensitiveChecksForTest(this.otherUsername, test, expect); }, logoutStep, // We shouldn't be able to log in with the email in lower case function (test, expect) { Meteor.loginWithPassword( { email: "ada-intercept@lovelace.com" + this.randomSuffix }, this.password, expectUserNotFound(test, expect)); }, // We should still be able to log in with the email in original case function (test, expect) { Meteor.loginWithPassword( { email: this.email }, this.password, loggedInAs(this.username, test, expect)); } ]); testAsyncMulti("passwords - creating users with the same case insensitive " + "email", [ createUserStep, logoutStep, // Attempting to create another user with an email that only differs in // case should fail function (test, expect) { this.newEmail = "ada-intercept@lovelace.com" + this.randomSuffix; Accounts.createUser( { email: this.newEmail, password: this.password }, expectError( new Meteor.Error(403, "Email already exists."), test, expect)); }, // Make sure the new user has not been inserted function (test, expect) { Meteor.call('countUsersOnServer', { 'emails.address': this.newEmail }, expect (function (error, result) { test.equal(result, 0); }) ); } ]); testAsyncMulti("passwords - changing passwords", [ function (test, expect) { // setup this.username = Random.id(); this.email = Random.id() + '-intercept@example.com'; this.password = 'password'; this.password2 = 'password2'; Accounts.createUser( { username: this.username, email: this.email, password: this.password }, loggedInAs(this.username, test, expect)); }, // Send a password reset email so that we can test that password // reset tokens get deleted on password change. function (test, expect) { Meteor.call("forgotPassword", { email: this.email }, expect(function (error) { test.isFalse(error); })); }, function (test, expect) { var self = this; Meteor.call("getResetToken", expect(function (err, token) { test.isFalse(err); test.isTrue(token); self.token = token; })); }, // change password with bad old password. we stay logged in. function (test, expect) { var self = this; Accounts.changePassword('wrong', 'doesntmatter', expect(function (error) { test.isTrue(error); test.equal(Meteor.user().username, self.username); })); }, // change password with good old password. function (test, expect) { Accounts.changePassword(this.password, this.password2, loggedInAs(this.username, test, expect)); }, function (test, expect) { Meteor.call("getResetToken", expect(function (err, token) { test.isFalse(err); test.isFalse(token); })); }, logoutStep, // old password, failed login function (test, expect) { Meteor.loginWithPassword(this.email, this.password, expect(function (error) { test.isTrue(error); test.isFalse(Meteor.user()); })); }, // new password, success function (test, expect) { Meteor.loginWithPassword(this.email, this.password2, loggedInAs(this.username, test, expect)); }, logoutStep ]); testAsyncMulti("passwords - changing password logs out other clients", [ function (test, expect) { this.username = Random.id(); this.email = Random.id() + '-intercept@example.com'; this.password = 'password'; this.password2 = 'password2'; Accounts.createUser( { username: this.username, email: this.email, password: this.password }, loggedInAs(this.username, test, expect)); }, // Log in a second connection as this user. function (test, expect) { var self = this; self.secondConn = DDP.connect(Meteor.absoluteUrl()); self.secondConn.call('login', { user: { username: self.username }, password: self.password }, expect(function (err, result) { test.isFalse(err); self.secondConn.setUserId(result.id); test.isTrue(self.secondConn.userId()); self.secondConn.onReconnect = function () { self.secondConn.apply( 'login', [{ resume: result.token }], { wait: true }, function (err, result) { self.secondConn.setUserId(result && result.id || null); } ); }; })); }, function (test, expect) { var self = this; Accounts.changePassword(self.password, self.password2, expect(function (err) { test.isFalse(err); })); }, // Now that we've changed the password, wait until the second // connection gets logged out. function (test, expect) { var self = this; pollUntil(expect, function () { return self.secondConn.userId() === null; }, 10 * 1000, 100); } ]); testAsyncMulti("passwords - new user hooks", [ function (test, expect) { // setup this.username = Random.id(); this.email = Random.id() + '-intercept@example.com'; this.password = 'password'; }, // test Accounts.validateNewUser function(test, expect) { Accounts.createUser( {username: this.username, password: this.password, // should fail the new user validators profile: {invalid: true}}, expect(function (error) { test.equal(error.error, 403); test.equal(error.reason, "User validation failed"); })); }, logoutStep, function(test, expect) { Accounts.createUser( {username: this.username, password: this.password, // should fail the new user validator with a special // exception profile: {invalidAndThrowException: true}}, expect(function (error) { test.equal( error.reason, "An exception thrown within Accounts.validateNewUser"); })); }, // test Accounts.onCreateUser function(test, expect) { Accounts.createUser( {username: this.username, password: this.password, testOnCreateUserHook: true}, loggedInAs(this.username, test, expect)); }, function(test, expect) { test.equal(Meteor.user().profile.touchedByOnCreateUser, true); }, logoutStep ]); testAsyncMulti("passwords - Meteor.user()", [ function (test, expect) { // setup this.username = Random.id(); this.password = 'password'; Accounts.createUser( {username: this.username, password: this.password, testOnCreateUserHook: true}, loggedInAs(this.username, test, expect)); }, // test Meteor.user(). This test properly belongs in // accounts-base/accounts_tests.js, but this is where the tests that // actually log in are. function(test, expect) { var self = this; var clientUser = Meteor.user(); Accounts.connection.call('testMeteorUser', expect(function (err, result) { test.equal(result._id, clientUser._id); test.equal(result.username, clientUser.username); test.equal(result.username, self.username); test.equal(result.profile.touchedByOnCreateUser, true); test.equal(err, undefined); })); }, function(test, expect) { // Test that even with no published fields, we still have a document. Accounts.connection.call('clearUsernameAndProfile', expect(function() { test.isTrue(Meteor.userId()); var user = Meteor.user(); test.equal(user, {_id: Meteor.userId()}); })); }, logoutStep, function(test, expect) { var clientUser = Meteor.user(); test.equal(clientUser, null); test.equal(Meteor.userId(), null); Accounts.connection.call('testMeteorUser', expect(function (err, result) { test.equal(err, undefined); test.equal(result, null); })); } ]); testAsyncMulti("passwords - allow rules", [ // create a second user to have an id for in a later test function (test, expect) { this.otherUsername = Random.id(); Accounts.createUser( {username: this.otherUsername, password: 'dontcare', testOnCreateUserHook: true}, loggedInAs(this.otherUsername, test, expect)); }, function (test, expect) { this.otherUserId = Meteor.userId(); }, function (test, expect) { // real setup this.username = Random.id(); this.password = 'password'; Accounts.createUser( {username: this.username, password: this.password, testOnCreateUserHook: true}, loggedInAs(this.username, test, expect)); }, // test the default Meteor.users allow rule. This test properly belongs in // accounts-base/accounts_tests.js, but this is where the tests that // actually log in are. function(test, expect) { this.userId = Meteor.userId(); test.notEqual(this.userId, null); test.notEqual(this.userId, this.otherUserId); // Can't update fields other than profile. Meteor.users.update( this.userId, {$set: {disallowed: true, 'profile.updated': 42}}, expect(function (err) { test.isTrue(err); test.equal(err.error, 403); test.isFalse(_.has(Meteor.user(), 'disallowed')); test.isFalse(_.has(Meteor.user().profile, 'updated')); })); }, function(test, expect) { // Can't update another user. Meteor.users.update( this.otherUserId, {$set: {'profile.updated': 42}}, expect(function (err) { test.isTrue(err); test.equal(err.error, 403); })); }, function(test, expect) { // Can't update using a non-ID selector. (This one is thrown client-side.) test.throws(function () { Meteor.users.update( {username: this.username}, {$set: {'profile.updated': 42}}); }); test.isFalse(_.has(Meteor.user().profile, 'updated')); }, function(test, expect) { // Can update own profile using ID. Meteor.users.update( this.userId, {$set: {'profile.updated': 42}}, expect(function (err) { test.isFalse(err); test.equal(42, Meteor.user().profile.updated); })); }, logoutStep ]); testAsyncMulti("passwords - tokens", [ function (test, expect) { // setup this.username = Random.id(); this.password = 'password'; Accounts.createUser( {username: this.username, password: this.password}, loggedInAs(this.username, test, expect)); }, function (test, expect) { // we can't login with an invalid token var expectLoginError = expect(function (err) { test.isTrue(err); }); Meteor.loginWithToken('invalid', expectLoginError); }, function (test, expect) { // we can login with a valid token var expectLoginOK = expect(function (err) { test.isFalse(err); }); Meteor.loginWithToken(Accounts._storedLoginToken(), expectLoginOK); }, function (test, expect) { // test logging out invalidates our token var expectLoginError = expect(function (err) { test.isTrue(err); }); var token = Accounts._storedLoginToken(); test.isTrue(token); Meteor.logout(function () { Meteor.loginWithToken(token, expectLoginError); }); }, function (test, expect) { var self = this; // Test that login tokens get expired. We should get logged out when a // token expires, and not be able to log in again with the same token. var expectNoError = expect(function (err) { test.isFalse(err); }); Meteor.loginWithPassword(this.username, this.password, function (error) { self.token = Accounts._storedLoginToken(); test.isTrue(self.token); expectNoError(error); Accounts.connection.call("expireTokens"); }); }, waitForLoggedOutStep, function (test, expect) { var token = Accounts._storedLoginToken(); test.isFalse(token); }, function (test, expect) { // Test that once expireTokens is finished, we can't login again with our // previous token. Meteor.loginWithToken(this.token, expect(function (err, result) { test.isTrue(err); test.equal(Meteor.userId(), null); })); }, logoutStep, function (test, expect) { var self = this; // Test that Meteor.logoutOtherClients logs out a second // authentcated connection while leaving Accounts.connection // logged in. var secondConn = DDP.connect(Meteor.absoluteUrl()); var token; var expectSecondConnLoggedOut = expect(function (err, result) { test.isTrue(err); }); var expectAccountsConnLoggedIn = expect(function (err, result) { test.isFalse(err); }); var expectSecondConnLoggedIn = expect(function (err, result) { test.equal(result.token, token); test.isFalse(err); Meteor.logoutOtherClients(function (err) { test.isFalse(err); secondConn.call('login', { resume: token }, expectSecondConnLoggedOut); Accounts.connection.call('login', { resume: Accounts._storedLoginToken() }, expectAccountsConnLoggedIn); }); }); Meteor.loginWithPassword( self.username, self.password, expect(function (err) { test.isFalse(err); token = Accounts._storedLoginToken(); test.isTrue(token); secondConn.call('login', { resume: token }, expectSecondConnLoggedIn); }) ); }, logoutStep, // The tests below this point are for the deprecated // `logoutOtherClients` method. function (test, expect) { var self = this; // Test that Meteor.logoutOtherClients logs out a second authenticated // connection while leaving Accounts.connection logged in. var token; self.secondConn = DDP.connect(Meteor.absoluteUrl()); var expectLoginError = expect(function (err) { test.isTrue(err); }); var expectValidToken = expect(function (err, result) { test.isFalse(err); test.isTrue(result); self.tokenFromLogoutOthers = result.token; }); var expectSecondConnLoggedIn = expect(function (err, result) { test.equal(result.token, token); test.isFalse(err); // This test will fail if an unrelated reconnect triggers before the // connection is logged out. In general our tests aren't resilient to // mid-test reconnects. self.secondConn.onReconnect = function () { self.secondConn.call("login", { resume: token }, expectLoginError); }; Accounts.connection.call("logoutOtherClients", expectValidToken); }); Meteor.loginWithPassword(this.username, this.password, expect(function (err) { test.isFalse(err); token = Accounts._storedLoginToken(); self.beforeLogoutOthersToken = token; test.isTrue(token); self.secondConn.call("login", { resume: token }, expectSecondConnLoggedIn); })); }, // Test that logoutOtherClients logged out Accounts.connection and that the // previous token is no longer valid. waitForLoggedOutStep, function (test, expect) { var self = this; var token = Accounts._storedLoginToken(); test.isFalse(token); this.secondConn.close(); Meteor.loginWithToken( self.beforeLogoutOthersToken, expect(function (err) { test.isTrue(err); test.isFalse(Meteor.userId()); }) ); }, // Test that logoutOtherClients returned a new token that we can use to // log in. function (test, expect) { var self = this; Meteor.loginWithToken( self.tokenFromLogoutOthers, expect(function (err) { test.isFalse(err); test.isTrue(Meteor.userId()); }) ); }, logoutStep, function (test, expect) { var self = this; // Test that deleting a user logs out that user's connections. Meteor.loginWithPassword(this.username, this.password, expect(function (err) { test.isFalse(err); Accounts.connection.call("removeUser", self.username); })); }, waitForLoggedOutStep ]); testAsyncMulti("passwords - validateLoginAttempt", [ function (test, expect) { this.username = Random.id(); this.password = "password"; Accounts.createUser( {username: this.username, password: this.password}, loggedInAs(this.username, test, expect)); }, logoutStep, invalidateLoginsStep, function (test, expect) { Meteor.loginWithPassword( this.username, this.password, expect(function (error) { test.isTrue(error); test.equal(error.reason, "Login forbidden"); }) ); }, validateLoginsStep, function (test, expect) { Meteor.loginWithPassword( "no such user", "some password", expect(function (error) { test.isTrue(error); test.equal(error.reason, 'User not found'); }) ); }, hideActualLoginErrorStep, function (test, expect) { Meteor.loginWithPassword( "no such user", "some password", expect(function (error) { test.isTrue(error); test.equal(error.reason, 'hide actual error'); }) ); }, validateLoginsStep ]); testAsyncMulti("passwords - server onLogin hook", [ function (test, expect) { Meteor.call("testCaptureLogins", expect(function (error) { test.isFalse(error); })); }, function (test, expect) { this.username = Random.id(); this.password = "password"; Accounts.createUser( {username: this.username, password: this.password}, loggedInAs(this.username, test, expect)); }, function (test, expect) { var self = this; Meteor.call("testFetchCapturedLogins", expect(function (error, logins) { test.isFalse(error); test.equal(logins.length, 1); var login = logins[0]; test.isTrue(login.successful); var attempt = login.attempt; test.equal(attempt.type, "password"); test.isTrue(attempt.allowed); test.equal(attempt.methodArguments[0].username, self.username); })); } ]); testAsyncMulti("passwords - client onLogin hook", [ function (test, expect) { var self = this; this.username = Random.id(); this.password = "password"; this.attempt = false; this.onLogin = Accounts.onLogin(function (attempt) { self.attempt = true; }); Accounts.createUser( {username: this.username, password: this.password}, loggedInAs(this.username, test, expect)); }, function (test, expect) { this.onLogin.stop(); test.isTrue(this.attempt); expect(function () {})(); } ]); testAsyncMulti("passwords - server onLogout hook", [ function (test, expect) { Meteor.call("testCaptureLogouts", expect(function (error) { test.isFalse(error); })); }, function (test, expect) { this.username = Random.id(); this.password = "password"; Accounts.createUser( {username: this.username, password: this.password}, loggedInAs(this.username, test, expect)); }, logoutStep, function (test, expect) { var self = this; Meteor.call("testFetchCapturedLogouts", expect(function (error, logouts) { test.isFalse(error); test.equal(logouts.length, 1); var logout = logouts[0]; test.isTrue(logout.successful); })); } ]); testAsyncMulti("passwords - client onLogout hook", [ function (test, expect) { var self = this; this.username = Random.id(); this.password = "password"; this.attempt = false; this.onLogout = Accounts.onLogout(function () { self.logoutSuccess = true; }); Accounts.createUser( {username: this.username, password: this.password}, loggedInAs(this.username, test, expect)); }, logoutStep, function (test, expect) { test.isTrue(this.logoutSuccess); expect(function() {})(); } ]); testAsyncMulti("passwords - server onLoginFailure hook", [ function (test, expect) { this.username = Random.id(); this.password = "password"; Accounts.createUser( {username: this.username, password: this.password}, loggedInAs(this.username, test, expect)); }, logoutStep, function (test, expect) { Meteor.call("testCaptureLogins", expect(function (error) { test.isFalse(error); })); }, function (test, expect) { Meteor.loginWithPassword(this.username, "incorrect", expect(function (error) { test.isTrue(error); })); }, function (test, expect) { Meteor.call("testFetchCapturedLogins", expect(function (error, logins) { test.isFalse(error); test.equal(logins.length, 1); var login = logins[0]; test.isFalse(login.successful); var attempt = login.attempt; test.equal(attempt.type, "password"); test.isFalse(attempt.allowed); test.equal(attempt.error.reason, "Incorrect password"); })); }, function (test, expect) { Meteor.call("testCaptureLogins", expect(function (error) { test.isFalse(error); })); }, function (test, expect) { Meteor.loginWithPassword("no such user", "incorrect", expect(function (error) { test.isTrue(error); })); }, function (test, expect) { Meteor.call("testFetchCapturedLogins", expect(function (error, logins) { test.isFalse(error); test.equal(logins.length, 1); var login = logins[0]; test.isFalse(login.successful); var attempt = login.attempt; test.equal(attempt.type, "password"); test.isFalse(attempt.allowed); test.equal(attempt.error.reason, "User not found"); })); } ]); testAsyncMulti("passwords - client onLoginFailure hook", [ function (test, expect) { var self = this; this.username = Random.id(); this.password = "password"; this.attempt = false; this.onLoginFailure = Accounts.onLoginFailure(function () { self.attempt = true; }) Accounts.createUser( {username: this.username, password: this.password}, loggedInAs(this.username, test, expect)); }, logoutStep, function (test, expect) { Meteor.call("testCaptureLogins", expect(function (error) { test.isFalse(error); })); }, function (test, expect) { Meteor.loginWithPassword(this.username, "incorrect", expect(function (error) { test.isTrue(error); })); }, function (test, expect) { this.onLoginFailure.stop(); test.isTrue(this.attempt); expect(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 ]); }) (); if (Meteor.isServer) (function () { Tinytest.add( 'passwords - setup more than one onCreateUserHook', function (test) { test.throws(function() { Accounts.onCreateUser(function () {}); }); }); Tinytest.add( 'passwords - createUser hooks', function (test) { var username = Random.id(); test.throws(function () { // should fail the new user validators Accounts.createUser({username: username, profile: {invalid: true}}); }); var userId = Accounts.createUser({username: username, testOnCreateUserHook: true}); test.isTrue(userId); var user = Meteor.users.findOne(userId); test.equal(user.profile.touchedByOnCreateUser, true); }); Tinytest.add( 'passwords - setPassword', function (test) { var username = Random.id(); var email = username + '-intercept@example.com'; var userId = Accounts.createUser({username: username, email: email}); var user = Meteor.users.findOne(userId); // no services yet. test.equal(user.services.password, undefined); // set a new password. Accounts.setPassword(userId, 'new password'); user = Meteor.users.findOne(userId); var oldSaltedHash = user.services.password.bcrypt; test.isTrue(oldSaltedHash); // Send a reset password email (setting a reset token) and insert a login // token. Accounts.sendResetPasswordEmail(userId, email); Accounts._insertLoginToken(userId, Accounts._generateStampedLoginToken()); test.isTrue(Meteor.users.findOne(userId).services.password.reset); test.isTrue(Meteor.users.findOne(userId).services.resume.loginTokens); // reset with the same password, see we get a different salted hash Accounts.setPassword(userId, 'new password', {logout: false}); user = Meteor.users.findOne(userId); var newSaltedHash = user.services.password.bcrypt; test.isTrue(newSaltedHash); test.notEqual(oldSaltedHash, newSaltedHash); // No more reset token. test.isFalse(Meteor.users.findOne(userId).services.password.reset); // But loginTokens are still here since we did logout: false. test.isTrue(Meteor.users.findOne(userId).services.resume.loginTokens); // reset again, see that the login tokens are gone. Accounts.setPassword(userId, 'new password'); user = Meteor.users.findOne(userId); var newerSaltedHash = user.services.password.bcrypt; test.isTrue(newerSaltedHash); test.notEqual(oldSaltedHash, newerSaltedHash); test.notEqual(newSaltedHash, newerSaltedHash); // No more tokens. test.isFalse(Meteor.users.findOne(userId).services.password.reset); test.isFalse(Meteor.users.findOne(userId).services.resume.loginTokens); // cleanup Meteor.users.remove(userId); }); // This test properly belongs in accounts-base/accounts_tests.js, but // this is where the tests that actually log in are. Tinytest.add('accounts - user() out of context', function (test) { // basic server context, no method. test.throws(function () { Meteor.user(); }); }); // XXX would be nice to test // Accounts.config({forbidClientAccountCreation: true}) Tinytest.addAsync( 'passwords - login token observes get cleaned up', function (test, onComplete) { var username = Random.id(); Accounts.createUser({ username: username, password: 'password' }); makeTestConnection( test, function (clientConn, serverConn) { serverConn.onClose(function () { test.isFalse(Accounts._getUserObserve(serverConn.id)); onComplete(); }); var result = clientConn.call('login', { user: {username: username}, password: 'password' }); test.isTrue(result); var token = Accounts._getAccountData(serverConn.id, 'loginToken'); test.isTrue(token); // We poll here, instead of just checking `_getUserObserve` // once, because the login method defers the creation of the // observe, and setting up the observe yields, so we could end // up here before the observe has been set up. simplePoll( function () { return !! Accounts._getUserObserve(serverConn.id); }, function () { test.isTrue(Accounts._getUserObserve(serverConn.id)); clientConn.disconnect(); }, function () { test.fail("timed out waiting for user observe for connection " + serverConn.id); onComplete(); } ); }, onComplete ); } ); Tinytest.add( 'passwords - reset password doesn\t work if email changed after email sent', function (test) { var username = Random.id(); var email = username + '-intercept@example.com'; var userId = Accounts.createUser({ username: username, email: email, password: "old-password" }); var user = Meteor.users.findOne(userId); Accounts.sendResetPasswordEmail(userId, email); var resetPasswordEmailOptions = Meteor.call("getInterceptedEmails", email)[0]; var re = new RegExp(Meteor.absoluteUrl() + "#/reset-password/(\\S*)"); var match = resetPasswordEmailOptions.text.match(re); test.isTrue(match); var resetPasswordToken = match[1]; var newEmail = Random.id() + '-new@example.com'; Meteor.users.update(userId, {$set: {"emails.0.address": newEmail}}); test.throws(function () { Meteor.call("resetPassword", resetPasswordToken, "new-password"); }, /Token has invalid email address/); test.throws(function () { Meteor.call("login", {user: {username: username}, password: "new-password"}); }, /Incorrect password/); }); // We should be able to change the username Tinytest.add("passwords - change username", function (test) { var username = Random.id(); var userId = Accounts.createUser({ username: username }); test.isTrue(userId); var newUsername = Random.id(); Accounts.setUsername(userId, newUsername); test.equal(Accounts._findUserByQuery({id: userId}).username, newUsername); // Test findUserByUsername as well while we're here test.equal(Accounts.findUserByUsername(newUsername)._id, userId); }); Tinytest.add("passwords - change username to a new one only differing " + "in case", function (test) { var username = Random.id() + "user"; var userId = Accounts.createUser({ username: username.toUpperCase() }); test.isTrue(userId); var newUsername = username.toLowerCase(); Accounts.setUsername(userId, newUsername); test.equal(Accounts._findUserByQuery({id: userId}).username, newUsername); }); // We should not be able to change the username to one that only // differs in case from an existing one Tinytest.add("passwords - change username should fail when there are " + "existing users with a username only differing in case", function (test) { var username = Random.id() + "user"; var usernameUpper = username.toUpperCase(); var userId1 = Accounts.createUser({ username: username }); var user2OriginalUsername = Random.id(); var userId2 = Accounts.createUser({ username: user2OriginalUsername }); test.isTrue(userId1); test.isTrue(userId2); test.throws(function () { Accounts.setUsername(userId2, usernameUpper); }, /Username already exists/); test.equal(Accounts._findUserByQuery({id: userId2}).username, user2OriginalUsername); }); Tinytest.add("passwords - add email", function (test) { var origEmail = Random.id() + "@turing.com"; var userId = Accounts.createUser({ email: origEmail }); var newEmail = Random.id() + "@turing.com"; Accounts.addEmail(userId, newEmail); var thirdEmail = Random.id() + "@turing.com"; Accounts.addEmail(userId, thirdEmail, true); test.equal(Accounts._findUserByQuery({id: userId}).emails, [ { address: origEmail, verified: false }, { address: newEmail, verified: false }, { address: thirdEmail, verified: true } ]); // Test findUserByEmail as well while we're here test.equal(Accounts.findUserByEmail(origEmail)._id, userId); }); Tinytest.add("passwords - add email when the user has an existing email " + "only differing in case", function (test) { var origEmail = Random.id() + "@turing.com"; var userId = Accounts.createUser({ email: origEmail }); var newEmail = Random.id() + "@turing.com"; Accounts.addEmail(userId, newEmail); var thirdEmail = origEmail.toUpperCase(); Accounts.addEmail(userId, thirdEmail, true); test.equal(Accounts._findUserByQuery({id: userId}).emails, [ { address: thirdEmail, verified: true }, { address: newEmail, verified: false } ]); }); Tinytest.add("passwords - add email should fail when there is an existing " + "user with an email only differing in case", function (test) { var user1Email = Random.id() + "@turing.com"; var userId1 = Accounts.createUser({ email: user1Email }); var user2Email = Random.id() + "@turing.com"; var userId2 = Accounts.createUser({ email: user2Email }); var dupEmail = user1Email.toUpperCase(); test.throws(function () { Accounts.addEmail(userId2, dupEmail); }, /Email already exists/); test.equal(Accounts._findUserByQuery({id: userId1}).emails, [ { address: user1Email, verified: false } ]); test.equal(Accounts._findUserByQuery({id: userId2}).emails, [ { address: user2Email, verified: false } ]); }); Tinytest.add("passwords - remove email", function (test) { var origEmail = Random.id() + "@turing.com"; var userId = Accounts.createUser({ email: origEmail }); var newEmail = Random.id() + "@turing.com"; Accounts.addEmail(userId, newEmail); var thirdEmail = Random.id() + "@turing.com"; Accounts.addEmail(userId, thirdEmail, true); test.equal(Accounts._findUserByQuery({id: userId}).emails, [ { address: origEmail, verified: false }, { address: newEmail, verified: false }, { address: thirdEmail, verified: true } ]); Accounts.removeEmail(userId, newEmail); test.equal(Accounts._findUserByQuery({id: userId}).emails, [ { address: origEmail, verified: false }, { address: thirdEmail, verified: true } ]); Accounts.removeEmail(userId, origEmail); test.equal(Accounts._findUserByQuery({id: userId}).emails, [ { address: thirdEmail, verified: true } ]); }); }) (); ================================================ FILE: packages/accounts-password/password_tests_setup.js ================================================ Accounts.validateNewUser(function (user) { if (user.profile && user.profile.invalidAndThrowException) throw new Meteor.Error(403, "An exception thrown within Accounts.validateNewUser"); return !(user.profile && user.profile.invalid); }); Accounts.onCreateUser(function (options, user) { if (options.testOnCreateUserHook) { user.profile = user.profile || {}; user.profile.touchedByOnCreateUser = true; return user; } else { return 'TEST DEFAULT HOOK'; } }); // connection id -> action var invalidateLogins = {}; Meteor.methods({ testInvalidateLogins: function (action) { if (action) invalidateLogins[this.connection.id] = action; else delete invalidateLogins[this.connection.id]; } }); Accounts.validateLoginAttempt(function (attempt) { var action = attempt && attempt.connection && invalidateLogins[attempt.connection.id]; if (! action) return true; else if (action === 'fail') return false; else if (action === 'hide') throw new Meteor.Error(403, 'hide actual error'); else throw new Error('unknown action: ' + action); }); // connection id -> [{successful: boolean, attempt: object}] var capturedLogins = {}; Meteor.methods({ testCaptureLogins: function () { capturedLogins[this.connection.id] = []; }, testCaptureLogouts: function() { capturedLogouts = []; }, testFetchCapturedLogins: function () { if (capturedLogins[this.connection.id]) { var logins = capturedLogins[this.connection.id]; delete capturedLogins[this.connection.id]; return logins; } else return []; }, testFetchCapturedLogouts: function() { return capturedLogouts; } }); Accounts.onLogin(function (attempt) { if (!attempt.connection) // if login method called from the server return; if (capturedLogins[attempt.connection.id]) capturedLogins[attempt.connection.id].push({ successful: true, attempt: _.omit(attempt, 'connection') }); }); Accounts.onLoginFailure(function (attempt) { if (!attempt.connection) // if login method called from the server return; if (capturedLogins[attempt.connection.id]) { capturedLogins[attempt.connection.id].push({ successful: false, attempt: _.omit(attempt, 'connection') }); } }); var capturedLogouts = []; Accounts.onLogout(function() { capturedLogouts.push({ successful: true }); }); // Because this is global state that affects every client, we can't turn // it on and off during the tests. Doing so would mean two simultaneous // test runs could collide with each other. // // We should probably have some sort of server-isolation between // multiple test runs. Perhaps a separate server instance per run. This // problem isn't unique to this test, there are other places in the code // where we do various hacky things to work around the lack of // server-side isolation. // // For now, we just test the one configuration state. You can comment // out each configuration option and see that the tests fail. Accounts.config({ sendVerificationEmail: true }); Meteor.methods({ testMeteorUser: function () { return Meteor.user(); }, clearUsernameAndProfile: function () { if (!this.userId) throw new Error("Not logged in!"); Meteor.users.update(this.userId, {$unset: {profile: 1, username: 1}}); }, expireTokens: function () { Accounts._expireTokens(new Date(), this.userId); }, removeUser: function (username) { Meteor.users.remove({ "username": username }); } }); // 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"); } }); ================================================ FILE: packages/accounts-twitter/.gitignore ================================================ .build* ================================================ FILE: packages/accounts-twitter/README.md ================================================ # accounts-twitter A login service for Twitter. See the [project page](https://www.meteor.com/accounts) on Meteor Accounts for more details. ================================================ FILE: packages/accounts-twitter/package.js ================================================ Package.describe({ summary: "Login service for Twitter accounts", version: "1.0.10" }); Package.onUse(function(api) { api.use('underscore', ['server']); api.use('accounts-base', ['client', 'server']); // Export Accounts (etc) to packages using this one. api.imply('accounts-base', ['client', 'server']); api.use('accounts-oauth', ['client', 'server']); api.use('twitter', ['client', 'server']); api.use('http', ['client', 'server']); api.addFiles('twitter_login_button.css', 'client'); api.addFiles("twitter.js"); }); ================================================ FILE: packages/accounts-twitter/twitter.js ================================================ Accounts.oauth.registerService('twitter'); if (Meteor.isClient) { Meteor.loginWithTwitter = function(options, callback) { // support a callback without options if (! callback && typeof options === "function") { callback = options; options = null; } var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); Twitter.requestCredential(options, credentialRequestCompleteCallback); }; } else { var autopublishedFields = _.map( // don't send access token. https://dev.twitter.com/discussions/5025 Twitter.whitelistedFields.concat(['id', 'screenName']), function (subfield) { return 'services.twitter.' + subfield; }); Accounts.addAutopublishFields({ forLoggedInUser: autopublishedFields, forOtherUsers: autopublishedFields }); } ================================================ FILE: packages/accounts-twitter/twitter_login_button.css ================================================ #login-buttons-image-twitter { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAByklEQVQ4jaVTz0sbQRh92V10l006GaKJCtEtmqMYU0Qpwqb4B6zgXdT0WEr7B0ih4MGLP05CUWMvHkQwglhvGhsvKmJOBhTUQjWU2slilKarrAfdZROTQ8m7fPMx33szb75vXKZpohpwVbEBCNaCMUYopXppAWOMxDNsOPf3H1WIeDoSURYYYwQAKKW6y7KgLe2vam11KyMRZcEpEP6SOkwbUgc4ATAKUF8YW2fXhZejvaHPsc7gvH2DnCfQGEtdxrd/5NRJteUDpVTf+5kLp2WlA6JsCyZv9ChplPKdTfJZkYWhEF3bvnV3fb36NZSY3dP6Q/5V4hFvIAaKPckE8W5pLBIQdwHAthBdPtpJuhpeAwDu74DrP4/R1/Ts4cwBWg/gN+DowoSqTBPezAMAeAHw+suSw4Q7schFApF6af19a+2yLVIB7xR+0Zk75yCveu82FMnMViKHCXcSa3PPVBJAX5BszL2SP2kNwvdy5M1e+S2AogME4HFYPibPpxKZC03nRAp/M+Dx2UWDzTXfpttrx72ikCoVtrrAAwgdXBk9iazxxtpskfhs1O86aHXXpAEcA7ivJGDBDcDnyAsA2FMsi1KB/0bVv/EBBBSY9mZ7PAsAAAAASUVORK5CYII=); } ================================================ FILE: packages/accounts-ui/.gitignore ================================================ .build* ================================================ FILE: packages/accounts-ui/README.md ================================================ # accounts-ui A turn-key user interface for Meteor Accounts. To add Accounts and a set of login controls to an application, add the `accounts-ui` package and at least one login provider package: `accounts-password`, `accounts-facebook`, `accounts-github`, `accounts-google`, `accounts-twitter`, or `accounts-weibo`. Then simply add the `{{> loginButtons}}` helper to an HTML file. See the Meteor Accounts [project page](https://www.meteor.com/accounts) for more info. ## Details Adding the `{{> loginButtons}}` helper to an HTML file will place a login widget on the page. If there is only one provider configured and it is an external service, this will add a login/logout button. If you use `accounts-password` or use multiple external login services, this will add a "Sign in" link which opens a dropdown menu with login options. If you plan to position the login dropdown in the right edge of the screen, use `{{> loginButtons align="right"}}` in order to get the dropdown to lay itself out without expanding off the edge of the screen. To configure the behavior of `{{> loginButtons}}`, use [`Accounts.ui.config`](http://docs.meteor.com/#accounts_ui_config). `accounts-ui` also includes modal popup dialogs to handle links from [`sendResetPasswordEmail`](http://docs.meteor.com/#accounts_sendresetpasswordemail), [`sendVerificationEmail`](http://docs.meteor.com/#accounts_sendverificationemail), and [`sendEnrollmentEmail`](http://docs.meteor.com/#accounts_sendenrollmentemail). These do not have to be manually placed in HTML: they are automatically activated when the URLs are loaded. See the Meteor Accounts [project page](https://www.meteor.com/accounts) for more info. ================================================ FILE: packages/accounts-ui/login_buttons.less ================================================ @import "{accounts-ui-unstyled}/login_buttons.import.less"; ================================================ FILE: packages/accounts-ui/package.js ================================================ Package.describe({ summary: "Simple templates to add login widgets to an app", version: "1.1.9" }); Package.onUse(function (api) { // Export Accounts (etc) to packages using this one. api.imply('accounts-base', ['client', 'server']); api.use('accounts-ui-unstyled', 'client'); api.use('less', 'client'); api.addFiles(['login_buttons.less'], 'client'); }); ================================================ FILE: packages/accounts-ui-unstyled/.gitignore ================================================ .build* ================================================ FILE: packages/accounts-ui-unstyled/README.md ================================================ # accounts-ui-unstyled A version of `accounts-ui` without the CSS, so that you can add your own styling. See the [`accounts-ui` README](https://atmospherejs.com/meteor/accounts-ui) and the Meteor Accounts [project page](https://www.meteor.com/accounts) for details. ================================================ FILE: packages/accounts-ui-unstyled/accounts_ui.js ================================================ /** * @summary Accounts UI * @namespace * @memberOf Accounts * @importFromPackage accounts-base */ Accounts.ui = {}; Accounts.ui._options = { requestPermissions: {}, requestOfflineToken: {}, forceApprovalPrompt: {} }; // XXX refactor duplicated code in this function /** * @summary Configure the behavior of [`{{> loginButtons}}`](#accountsui). * @locus Client * @param {Object} options * @param {Object} options.requestPermissions Which [permissions](#requestpermissions) to request from the user for each external service. * @param {Object} options.requestOfflineToken To ask the user for permission to act on their behalf when offline, map the relevant external service to `true`. Currently only supported with Google. See [Meteor.loginWithExternalService](#meteor_loginwithexternalservice) for more details. * @param {Object} options.forceApprovalPrompt If true, forces the user to approve the app's permissions, even if previously approved. Currently only supported with Google. * @param {String} options.passwordSignupFields Which fields to display in the user creation form. One of '`USERNAME_AND_EMAIL`', '`USERNAME_AND_OPTIONAL_EMAIL`', '`USERNAME_ONLY`', or '`EMAIL_ONLY`' (default). * @importFromPackage accounts-base */ Accounts.ui.config = function(options) { // validate options keys var VALID_KEYS = ['passwordSignupFields', 'requestPermissions', 'requestOfflineToken', 'forceApprovalPrompt']; _.each(_.keys(options), function (key) { if (!_.contains(VALID_KEYS, key)) throw new Error("Accounts.ui.config: Invalid key: " + key); }); // deal with `passwordSignupFields` if (options.passwordSignupFields) { if (_.contains([ "USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY", "EMAIL_ONLY" ], options.passwordSignupFields)) { if (Accounts.ui._options.passwordSignupFields) throw new Error("Accounts.ui.config: Can't set `passwordSignupFields` more than once"); else Accounts.ui._options.passwordSignupFields = options.passwordSignupFields; } else { throw new Error("Accounts.ui.config: Invalid option for `passwordSignupFields`: " + options.passwordSignupFields); } } // deal with `requestPermissions` if (options.requestPermissions) { _.each(options.requestPermissions, function (scope, service) { if (Accounts.ui._options.requestPermissions[service]) { throw new Error("Accounts.ui.config: Can't set `requestPermissions` more than once for " + service); } else if (!(scope instanceof Array)) { throw new Error("Accounts.ui.config: Value for `requestPermissions` must be an array"); } else { Accounts.ui._options.requestPermissions[service] = scope; } }); } // deal with `requestOfflineToken` if (options.requestOfflineToken) { _.each(options.requestOfflineToken, function (value, service) { if (service !== 'google') throw new Error("Accounts.ui.config: `requestOfflineToken` only supported for Google login at the moment."); if (Accounts.ui._options.requestOfflineToken[service]) { throw new Error("Accounts.ui.config: Can't set `requestOfflineToken` more than once for " + service); } else { Accounts.ui._options.requestOfflineToken[service] = value; } }); } // deal with `forceApprovalPrompt` if (options.forceApprovalPrompt) { _.each(options.forceApprovalPrompt, function (value, service) { if (service !== 'google') throw new Error("Accounts.ui.config: `forceApprovalPrompt` only supported for Google login at the moment."); if (Accounts.ui._options.forceApprovalPrompt[service]) { throw new Error("Accounts.ui.config: Can't set `forceApprovalPrompt` more than once for " + service); } else { Accounts.ui._options.forceApprovalPrompt[service] = value; } }); } }; passwordSignupFields = function () { return Accounts.ui._options.passwordSignupFields || "EMAIL_ONLY"; }; ================================================ FILE: packages/accounts-ui-unstyled/accounts_ui_tests.js ================================================ // XXX Most of the testing of accounts-ui is done manually, across // multiple browsers using examples/unfinished/accounts-ui-helper. We // should *definitely* automate this, but Tinytest is generally not // the right abstraction to use for this. // XXX it'd be cool to also test that the right thing happens if options // *are* validated, but Accounts.ui._options is global state which makes this hard // (impossible?) Tinytest.add('accounts-ui - config validates keys', function (test) { test.throws(function () { Accounts.ui.config({foo: "bar"}); }); test.throws(function () { Accounts.ui.config({passwordSignupFields: "not a valid option"}); }); test.throws(function () { Accounts.ui.config({requestPermissions: {facebook: "not an array"}}); }); test.throws(function () { Accounts.ui.config({forceApprovalPrompt: {facebook: "only google"}}); }); }); ================================================ FILE: packages/accounts-ui-unstyled/login_buttons.html ================================================ ================================================ FILE: packages/accounts-ui-unstyled/login_buttons.import.less ================================================ //////////////////// MIXINS // Minimal, well-documented, general-purpose CSS mixins. // (Some are same as Bootstrap.) ////////// Box-Sizing: Border-Box // Setting `box-sizing: border-box` on an element causes the CSS // layout algorithm to interpret `width` and `height` declarations // as referring to the size of the border box (outside the border), // not the content box as usual (inside the padding). // // This is especially useful for stretching a form element to the // width of its container even if the form element has arbitrary // padding and borders, which can be done using `width: 100%`. // // Browser support is IE 8+ and all modern browsers, with the caveat // that `-moz-box-sizing` in Firefox is considered to have some // buggy or non-compliant behavior. For example, min/max-width/height // may not interact correctly. See // https://bugzilla.mozilla.org/show_bug.cgi?id=243412. .box-sizing-by-border () { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } ////////// Box-Shadow .box-shadow (...) { box-shadow: @arguments; -webkit-box-shadow: @arguments; // For Android } ////////// Unselectable .unselectable () { -webkit-user-select: none; // Chrome/Safari -moz-user-select: none; // Firefox -ms-user-select: none; // IE10+ // These delarations not implemented in browsers yet: -o-user-select: none; user-select: none; // In IE <= 9 and Opera, need unselectable="on" in the HTML. } //////////////////// LOGIN BUTTONS @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%); @login-buttons-color-active-border: darken(@login-buttons-color-active, 10%); @login-buttons-config-color: darken(#f53, 10%); @login-buttons-config-color-border: darken(@login-buttons-config-color, 10%); @login-buttons-config-color-active: lighten(@login-buttons-config-color, 10%); @login-buttons-config-color-active-border: darken(@login-buttons-config-color-active, 10%); #login-buttons { display: inline-block; margin-right: 0.2px; // Fixes display on IE8: http://www.compsoft.co.uk/Blog/2009/11/inline-block-not-quite-inline-blocking.html // This seems to keep the height of the line from // being sensitive to the presence of the unicode down arrow, // which otherwise bumps the baseline down by 1px. line-height: 1; .login-button { position: relative; // so that we can position the image absolutely within the button } .login-buttons-with-only-one-button { display: inline-block; .login-button { display: inline-block; } .login-text-and-button { display: inline-block; } } .login-display-name { display: inline-block; padding-right: 2px; line-height: 1.5; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } .loading { line-height: 1; background-image: url(data:image/gif;base64,R0lGODlhEAALAPQAAP///wAAANra2tDQ0Orq6gYGBgAAAC4uLoKCgmBgYLq6uiIiIkpKSoqKimRkZL6+viYmJgQEBE5OTubm5tjY2PT09Dg4ONzc3PLy8ra2tqCgoMrKyu7u7gAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCwAAACwAAAAAEAALAAAFLSAgjmRpnqSgCuLKAq5AEIM4zDVw03ve27ifDgfkEYe04kDIDC5zrtYKRa2WQgAh+QQJCwAAACwAAAAAEAALAAAFJGBhGAVgnqhpHIeRvsDawqns0qeN5+y967tYLyicBYE7EYkYAgAh+QQJCwAAACwAAAAAEAALAAAFNiAgjothLOOIJAkiGgxjpGKiKMkbz7SN6zIawJcDwIK9W/HISxGBzdHTuBNOmcJVCyoUlk7CEAAh+QQJCwAAACwAAAAAEAALAAAFNSAgjqQIRRFUAo3jNGIkSdHqPI8Tz3V55zuaDacDyIQ+YrBH+hWPzJFzOQQaeavWi7oqnVIhACH5BAkLAAAALAAAAAAQAAsAAAUyICCOZGme1rJY5kRRk7hI0mJSVUXJtF3iOl7tltsBZsNfUegjAY3I5sgFY55KqdX1GgIAIfkECQsAAAAsAAAAABAACwAABTcgII5kaZ4kcV2EqLJipmnZhWGXaOOitm2aXQ4g7P2Ct2ER4AMul00kj5g0Al8tADY2y6C+4FIIACH5BAkLAAAALAAAAAAQAAsAAAUvICCOZGme5ERRk6iy7qpyHCVStA3gNa/7txxwlwv2isSacYUc+l4tADQGQ1mvpBAAIfkECQsAAAAsAAAAABAACwAABS8gII5kaZ7kRFGTqLLuqnIcJVK0DeA1r/u3HHCXC/aKxJpxhRz6Xi0ANAZDWa+kEAA7AAAAAAAAAAAA); width: 16px; background-position: center center; background-repeat: no-repeat; } } #login-buttons .login-button, .accounts-dialog .login-button { cursor: pointer; .unselectable(); padding: 4px 8px; font-size: 80%; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; line-height: 1.5; text-align: center; color: #fff; background: @login-buttons-color; border: 1px solid @login-buttons-color-border; border-radius: 4px; &:hover { background: @login-buttons-color-active; } &:active { background: @login-buttons-color-active; .box-shadow(0 2px 3px 0 rgba(0, 0, 0, 0.2) inset); } &.login-button-disabled, &.login-button-disabled:active { color: #ddd; background: #aaa; border: 1px solid lighten(#aaa, 10%); .box-shadow(none); } } // precendence of this selector is significant .accounts-dialog * { // A base for our dialog CSS, to reset browser styles and protect against // the app's CSS. Dialogs include the dropdown, config modals, and the // reset password modal. We can't completely isolate the dialogs from // the app's CSS, and that isn't the goal because the app can style them. // This rule is a compromise that should take precedence over some very // broad rules but be overridden by more specific ones. // Add more declarations here if they help the dialogs look good // out-of-the-box in more apps. padding: 0; margin: 0; line-height: inherit; color: inherit; font: inherit; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } .accounts-dialog .login-button { width: auto; margin-bottom: 4px; } #login-buttons { .login-buttons-padding { display: inline-block; width: 30px; } .login-display-name { margin-right: 4px; } .configure-button { background: @login-buttons-config-color; border-color: @login-buttons-config-color-border; &:active, &:hover { background: @login-buttons-config-color-active; border-color: @login-buttons-config-color-active-border; } } .login-image { display: inline-block; position: absolute; left: 6px; top: 6px; width: 16px; height: 16px; } .text-besides-image { margin-left: 18px; } .no-services { color: red; } .login-link-and-dropdown-list { position: relative; } .login-close-text { float: left; position: relative; padding-bottom: 8px; } .login-text-and-button .loading, .login-link-and-dropdown-list .loading { display: inline-block; } &.login-buttons-dropdown-align-left #login-dropdown-list .loading { float: right; } &.login-buttons-dropdown-align-right #login-dropdown-list .loading { float: left; } .login-close-text-clear { clear: both; } .or { text-align: center; } .hline { text-decoration: line-through; color: lightgrey; } .or-text { font-weight: bold; } #signup-link { float: right; } #forgot-password-link { float: left; } #back-to-login-link { float: right; } } #login-buttons a, .accounts-dialog a { cursor: pointer; text-decoration: underline; } #login-buttons.login-buttons-dropdown-align-right .login-close-text { float: right; } @meteor-accounts-base-padding: 8px; @meteor-accounts-dialog-border-width: 1px; .accounts-dialog { border: @meteor-accounts-dialog-border-width solid #ccc; z-index: 1000; background: white; border-radius: 4px; padding: 8px 12px; margin: -8px -12px 0 -12px; width: @login-buttons-accounts-dialog-width; .box-shadow(0 0 3px 0 rgba(0, 0, 0, 0.2)); // Labels and links inherit app's font with this line commented out: //font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; color: #333; // XXX Make the dropdown and dialogs look good without a top-level // line-height: 1.6. For now, we apply it to everything except // the "Close" link, which we want to have the same line-height // as the "Sign in" link. & > * { line-height: 1.6; } & > .login-close-text { line-height: inherit; font-size: inherit; font-family: inherit; } label, .title { font-size: 80%; margin-top: 7px; margin-bottom: -2px; } label { // Bootstrap sets labels as 'display: block;'. Undo that. display: inline; } input[type=text], input[type=email], input[type=password] { // Be pixel-accurate in IE 8+ regardless of our borders and // paddings, at the expense of IE 7. // Any heights or widths applied to this element will set the // size of the border box (including padding and borders) // instead of the content box. This makes it possible to // do width 100%. .box-sizing-by-border(); width: 100%; // A fix purely for the "meteor add bootstrap" experience. // Bootstrap sets "height: 20px" on form fields, which is too // small when applied to the border box. People have complained // that Bootstrap takes this approach for the sake of IE 7: // https://github.com/twitter/bootstrap/issues/2935 // Our work-around is to override Bootstrap's rule (with higher // precedence). &[type] { height: auto; } } .login-button-form-submit { margin-top: 8px; } .message { font-size: 80%; margin-top: 8px; line-height: 1.3; } .error-message { color: red; } .info-message { color: green; } .additional-link { font-size: 75%; } .accounts-close { position: absolute; top: 0; right: 5px; font-size: 20px; font-weight: bold; line-height: 20px; text-decoration: none; color: #000; opacity: 0.4; &:hover { opacity: 0.8; } } #login-buttons-cancel-reset-password { float: right; } #login-buttons-cancel-enroll-account { float: right; } } #login-dropdown-list { position: absolute; // The top-left of the border-box of the dropdown is absolutely // positioned within its container, so we need to compensate // for the border. The padding is already compensated for by // negative margins on the dropdown. // XXX We could use negative margins to compensate for the // border too. top: -@meteor-accounts-dialog-border-width; left: -@meteor-accounts-dialog-border-width; } #login-buttons.login-buttons-dropdown-align-right #login-dropdown-list { left: auto; right: -@meteor-accounts-dialog-border-width; } #login-buttons-message-dialog .message { /* we intentionally want it bigger on this dialog since it's the only thing displayed */ font-size: 100%; } .accounts-centered-dialog { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; z-index: 1001; position: fixed; left: 50%; margin-left: -(@login-buttons-accounts-dialog-width + @meteor-accounts-base-padding) / 2; top: 50%; margin-top: -40px; /* = approximately -height/2, though height can change */ } @configure-login-service-dialog-width: 530px; #configure-login-service-dialog { width: @configure-login-service-dialog-width; margin-left: -(@configure-login-service-dialog-width + @meteor-accounts-base-padding) / 2; margin-top: -300px; /* = approximately -height/2, though height can change */ table { width: 100%; } input[type=text] { width: 100%; font-family: "Courier New", Courier, monospace; } ol { margin-top: 10px; margin-bottom: 10px; li { margin-left: 30px; } } .configuration_labels { width: 30%; } .configuration_inputs { width: 70%; } .new-section { margin-top: 10px; } .url { font-family: "Courier New", Courier, monospace; } } #configure-login-service-dialog-save-configuration { float: right; } .configure-login-service-dismiss-button { float: left; } #just-verified-dismiss-button, #messages-dialog-dismiss-button { margin-top: 8px; } .hide-background { position: fixed; left: 0; top: 0; width: 100%; height: 100%; z-index: 999; /* XXX consider replacing with DXImageTransform */ background-color: rgb(0.2, 0.2, 0.2); /* fallback for IE7-8 */ background-color: rgba(0, 0, 0, 0.7); } #login-buttons, .accounts-dialog { input[type=text], input[type=email], input[type=password] { padding: 4px; border: 1px solid #aaa; border-radius: 3px; line-height: 1; } } ================================================ FILE: packages/accounts-ui-unstyled/login_buttons.js ================================================ // for convenience var loginButtonsSession = Accounts._loginButtonsSession; // shared between dropdown and single mode Template.loginButtons.events({ 'click #login-buttons-logout': function() { Meteor.logout(function () { loginButtonsSession.closeDropdown(); }); } }); Template.registerHelper('loginButtons', function () { throw new Error("Use {{> loginButtons}} instead of {{loginButtons}}"); }); // // helpers // displayName = function () { var user = Meteor.user(); if (!user) return ''; if (user.profile && user.profile.name) return user.profile.name; if (user.username) return user.username; if (user.emails && user.emails[0] && user.emails[0].address) return user.emails[0].address; return ''; }; // returns an array of the login services used by this app. each // element of the array is an object (eg {name: 'facebook'}), since // that makes it useful in combination with handlebars {{#each}}. // // don't cache the output of this function: if called during startup (before // oauth packages load) it might not include them all. // // NOTE: It is very important to have this return password last // because of the way we render the different providers in // login_buttons_dropdown.html getLoginServices = function () { var self = this; // First look for OAuth services. var services = Package['accounts-oauth'] ? Accounts.oauth.serviceNames() : []; // Be equally kind to all login services. This also preserves // backwards-compatibility. (But maybe order should be // configurable?) services.sort(); // Add password, if it's there; it must come last. if (hasPasswordService()) services.push('password'); return _.map(services, function(name) { return {name: name}; }); }; hasPasswordService = function () { return !!Package['accounts-password']; }; dropdown = function () { return hasPasswordService() || getLoginServices().length > 1; }; // XXX improve these. should this be in accounts-password instead? // // XXX these will become configurable, and will be validated on // the server as well. validateUsername = function (username) { if (username.length >= 3) { return true; } else { loginButtonsSession.errorMessage("Username must be at least 3 characters long"); return false; } }; validateEmail = function (email) { if (passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL" && email === '') return true; if (email.indexOf('@') !== -1) { return true; } else { loginButtonsSession.errorMessage("Invalid email"); return false; } }; validatePassword = function (password) { if (password.length >= 6) { return true; } else { loginButtonsSession.errorMessage("Password must be at least 6 characters long"); return false; } }; // // loginButtonLoggedOut template // Template._loginButtonsLoggedOut.helpers({ dropdown: dropdown, services: getLoginServices, singleService: function () { var services = getLoginServices(); if (services.length !== 1) throw new Error( "Shouldn't be rendering this template with more than one configured service"); return services[0]; }, configurationLoaded: function () { return Accounts.loginServicesConfigured(); } }); // // loginButtonsLoggedIn template // // decide whether we should show a dropdown rather than a row of // buttons Template._loginButtonsLoggedIn.helpers({ dropdown: dropdown }); // // loginButtonsLoggedInSingleLogoutButton template // Template._loginButtonsLoggedInSingleLogoutButton.helpers({ displayName: displayName }); // // loginButtonsMessage template // Template._loginButtonsMessages.helpers({ errorMessage: function () { return loginButtonsSession.get('errorMessage'); } }); Template._loginButtonsMessages.helpers({ infoMessage: function () { return loginButtonsSession.get('infoMessage'); } }); // // loginButtonsLoggingInPadding template // Template._loginButtonsLoggingInPadding.helpers({ dropdown: dropdown }); ================================================ FILE: packages/accounts-ui-unstyled/login_buttons_dialogs.html ================================================ {{> _resetPasswordDialog}} {{> _justResetPasswordDialog}} {{> _enrollAccountDialog}} {{> _justVerifiedEmailDialog}} {{> _configureLoginServiceDialog}} {{> _configureLoginOnDesktopDialog}} {{! if we're not showing a dropdown, we need some other place to show messages }} {{> _loginButtonsMessagesDialog}} ================================================ FILE: packages/accounts-ui-unstyled/login_buttons_dialogs.js ================================================ // for convenience var loginButtonsSession = Accounts._loginButtonsSession; // since we don't want to pass around the callback that we get from our event // handlers, we just make it a variable for the whole file var doneCallback; Accounts.onResetPasswordLink(function (token, done) { loginButtonsSession.set("resetPasswordToken", token); doneCallback = done; }); Accounts.onEnrollmentLink(function (token, done) { loginButtonsSession.set("enrollAccountToken", token); doneCallback = done; }); Accounts.onEmailVerificationLink(function (token, done) { Accounts.verifyEmail(token, function (error) { if (! error) { loginButtonsSession.set('justVerifiedEmail', true); } done(); // XXX show something if there was an error. }); }); // // resetPasswordDialog template // Template._resetPasswordDialog.events({ 'click #login-buttons-reset-password-button': function () { resetPassword(); }, 'keypress #reset-password-new-password': function (event) { if (event.keyCode === 13) resetPassword(); }, 'click #login-buttons-cancel-reset-password': function () { loginButtonsSession.set('resetPasswordToken', null); if (doneCallback) doneCallback(); } }); var resetPassword = function () { loginButtonsSession.resetMessages(); var newPassword = document.getElementById('reset-password-new-password').value; if (!validatePassword(newPassword)) return; Accounts.resetPassword( loginButtonsSession.get('resetPasswordToken'), newPassword, function (error) { if (error) { loginButtonsSession.errorMessage(error.reason || "Unknown error"); } else { loginButtonsSession.set('resetPasswordToken', null); loginButtonsSession.set('justResetPassword', true); if (doneCallback) doneCallback(); } }); }; Template._resetPasswordDialog.helpers({ inResetPasswordFlow: function () { return loginButtonsSession.get('resetPasswordToken'); } }); // // justResetPasswordDialog template // Template._justResetPasswordDialog.events({ 'click #just-verified-dismiss-button': function () { loginButtonsSession.set('justResetPassword', false); } }); Template._justResetPasswordDialog.helpers({ visible: function () { return loginButtonsSession.get('justResetPassword'); }, displayName: displayName }); // // enrollAccountDialog template // Template._enrollAccountDialog.events({ 'click #login-buttons-enroll-account-button': function () { enrollAccount(); }, 'keypress #enroll-account-password': function (event) { if (event.keyCode === 13) enrollAccount(); }, 'click #login-buttons-cancel-enroll-account': function () { loginButtonsSession.set('enrollAccountToken', null); if (doneCallback) doneCallback(); } }); var enrollAccount = function () { loginButtonsSession.resetMessages(); var password = document.getElementById('enroll-account-password').value; if (!validatePassword(password)) return; Accounts.resetPassword( loginButtonsSession.get('enrollAccountToken'), password, function (error) { if (error) { loginButtonsSession.errorMessage(error.reason || "Unknown error"); } else { loginButtonsSession.set('enrollAccountToken', null); if (doneCallback) doneCallback(); } }); }; Template._enrollAccountDialog.helpers({ inEnrollAccountFlow: function () { return loginButtonsSession.get('enrollAccountToken'); } }); // // justVerifiedEmailDialog template // Template._justVerifiedEmailDialog.events({ 'click #just-verified-dismiss-button': function () { loginButtonsSession.set('justVerifiedEmail', false); } }); Template._justVerifiedEmailDialog.helpers({ visible: function () { return loginButtonsSession.get('justVerifiedEmail'); }, displayName: displayName }); // // loginButtonsMessagesDialog template // Template._loginButtonsMessagesDialog.events({ 'click #messages-dialog-dismiss-button': function () { loginButtonsSession.resetMessages(); } }); Template._loginButtonsMessagesDialog.helpers({ visible: function () { var hasMessage = loginButtonsSession.get('infoMessage') || loginButtonsSession.get('errorMessage'); return !dropdown() && hasMessage; } }); // // configureLoginServiceDialog template // Template._configureLoginServiceDialog.events({ 'click .configure-login-service-dismiss-button': function () { loginButtonsSession.set('configureLoginServiceDialogVisible', false); }, 'click #configure-login-service-dialog-save-configuration': function () { if (loginButtonsSession.get('configureLoginServiceDialogVisible') && ! loginButtonsSession.get('configureLoginServiceDialogSaveDisabled')) { // Prepare the configuration document for this login service var serviceName = loginButtonsSession.get('configureLoginServiceDialogServiceName'); var configuration = { service: serviceName }; // Fetch the value of each input field _.each(configurationFields(), function(field) { configuration[field.property] = document.getElementById( 'configure-login-service-dialog-' + field.property).value .replace(/^\s*|\s*$/g, ""); // trim() doesnt work on IE8; }); configuration.loginStyle = $('#configure-login-service-dialog input[name="loginStyle"]:checked') .val(); // Configure this login service Accounts.connection.call( "configureLoginService", configuration, function (error, result) { if (error) Meteor._debug("Error configuring login service " + serviceName, error); else loginButtonsSession.set('configureLoginServiceDialogVisible', false); }); } }, // IE8 doesn't support the 'input' event, so we'll run this on the keyup as // well. (Keeping the 'input' event means that this also fires when you use // the mouse to change the contents of the field, eg 'Cut' menu item.) 'input, keyup input': function (event) { // if the event fired on one of the configuration input fields, // check whether we should enable the 'save configuration' button if (event.target.id.indexOf('configure-login-service-dialog') === 0) updateSaveDisabled(); } }); // check whether the 'save configuration' button should be enabled. // this is a really strange way to implement this and a Forms // Abstraction would make all of this reactive, and simpler. var updateSaveDisabled = function () { var anyFieldEmpty = _.any(configurationFields(), function(field) { return document.getElementById( 'configure-login-service-dialog-' + field.property).value === ''; }); loginButtonsSession.set('configureLoginServiceDialogSaveDisabled', anyFieldEmpty); }; // Returns the appropriate template for this login service. This // template should be defined in the service's package var configureLoginServiceDialogTemplateForService = function () { var serviceName = loginButtonsSession.get('configureLoginServiceDialogServiceName'); // XXX Service providers should be able to specify their configuration // template name. return Template['configureLoginServiceDialogFor' + (serviceName === 'meteor-developer' ? 'MeteorDeveloper' : capitalize(serviceName))]; }; var configurationFields = function () { var template = configureLoginServiceDialogTemplateForService(); return template.fields(); }; Template._configureLoginServiceDialog.helpers({ configurationFields: function () { return configurationFields(); }, visible: function () { return loginButtonsSession.get('configureLoginServiceDialogVisible'); }, configurationSteps: function () { // renders the appropriate template return configureLoginServiceDialogTemplateForService(); }, saveDisabled: function () { return loginButtonsSession.get('configureLoginServiceDialogSaveDisabled'); } }); // XXX from http://epeli.github.com/underscore.string/lib/underscore.string.js var capitalize = function(str){ str = str == null ? '' : String(str); return str.charAt(0).toUpperCase() + str.slice(1); }; Template._configureLoginOnDesktopDialog.helpers({ visible: function () { return loginButtonsSession.get('configureOnDesktopVisible'); } }); Template._configureLoginOnDesktopDialog.events({ 'click #configure-on-desktop-dismiss-button': function () { loginButtonsSession.set('configureOnDesktopVisible', false); } }); ================================================ FILE: packages/accounts-ui-unstyled/login_buttons_dropdown.html ================================================ ================================================ FILE: packages/accounts-ui-unstyled/login_buttons_dropdown.js ================================================ // for convenience var loginButtonsSession = Accounts._loginButtonsSession; // events shared between loginButtonsLoggedOutDropdown and // loginButtonsLoggedInDropdown Template.loginButtons.events({ 'click #login-name-link, click #login-sign-in-link': function () { loginButtonsSession.set('dropdownVisible', true); }, 'click .login-close-text': function () { loginButtonsSession.closeDropdown(); } }); // // loginButtonsLoggedInDropdown template and related // Template._loginButtonsLoggedInDropdown.events({ 'click #login-buttons-open-change-password': function() { loginButtonsSession.resetMessages(); loginButtonsSession.set('inChangePasswordFlow', true); } }); Template._loginButtonsLoggedInDropdown.helpers({ displayName: displayName, inChangePasswordFlow: function () { return loginButtonsSession.get('inChangePasswordFlow'); }, inMessageOnlyFlow: function () { return loginButtonsSession.get('inMessageOnlyFlow'); }, dropdownVisible: function () { return loginButtonsSession.get('dropdownVisible'); } }); Template._loginButtonsLoggedInDropdownActions.helpers({ allowChangingPassword: function () { // it would be more correct to check whether the user has a password set, // but in order to do that we'd have to send more data down to the client, // and it'd be preferable not to send down the entire service.password document. // // instead we use the heuristic: if the user has a username or email set. var user = Meteor.user(); return user.username || (user.emails && user.emails[0] && user.emails[0].address); } }); // // loginButtonsLoggedOutDropdown template and related // Template._loginButtonsLoggedOutDropdown.events({ 'click #login-buttons-password': function () { loginOrSignup(); }, 'keypress #forgot-password-email': function (event) { if (event.keyCode === 13) forgotPassword(); }, 'click #login-buttons-forgot-password': function () { forgotPassword(); }, 'click #signup-link': function () { loginButtonsSession.resetMessages(); // store values of fields before swtiching to the signup form var username = trimmedElementValueById('login-username'); var email = trimmedElementValueById('login-email'); var usernameOrEmail = trimmedElementValueById('login-username-or-email'); // notably not trimmed. a password could (?) start or end with a space var password = elementValueById('login-password'); loginButtonsSession.set('inSignupFlow', true); loginButtonsSession.set('inForgotPasswordFlow', false); // force the ui to update so that we have the approprate fields to fill in Tracker.flush(); // update new fields with appropriate defaults if (username !== null) document.getElementById('login-username').value = username; else if (email !== null) document.getElementById('login-email').value = email; else if (usernameOrEmail !== null) if (usernameOrEmail.indexOf('@') === -1) document.getElementById('login-username').value = usernameOrEmail; else document.getElementById('login-email').value = usernameOrEmail; if (password !== null) document.getElementById('login-password').value = password; // Force redrawing the `login-dropdown-list` element because of // a bizarre Chrome bug in which part of the DIV is not redrawn // in case you had tried to unsuccessfully log in before // switching to the signup form. // // Found tip on how to force a redraw on // http://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes/3485654#3485654 var redraw = document.getElementById('login-dropdown-list'); redraw.style.display = 'none'; redraw.offsetHeight; // it seems that this line does nothing but is necessary for the redraw to work redraw.style.display = 'block'; }, 'click #forgot-password-link': function () { loginButtonsSession.resetMessages(); // store values of fields before swtiching to the signup form var email = trimmedElementValueById('login-email'); var usernameOrEmail = trimmedElementValueById('login-username-or-email'); loginButtonsSession.set('inSignupFlow', false); loginButtonsSession.set('inForgotPasswordFlow', true); // force the ui to update so that we have the approprate fields to fill in Tracker.flush(); // update new fields with appropriate defaults if (email !== null) document.getElementById('forgot-password-email').value = email; else if (usernameOrEmail !== null) if (usernameOrEmail.indexOf('@') !== -1) document.getElementById('forgot-password-email').value = usernameOrEmail; }, 'click #back-to-login-link': function () { loginButtonsSession.resetMessages(); var username = trimmedElementValueById('login-username'); var email = trimmedElementValueById('login-email') || trimmedElementValueById('forgot-password-email'); // Ughh. Standardize on names? // notably not trimmed. a password could (?) start or end with a space var password = elementValueById('login-password'); loginButtonsSession.set('inSignupFlow', false); loginButtonsSession.set('inForgotPasswordFlow', false); // force the ui to update so that we have the approprate fields to fill in Tracker.flush(); if (document.getElementById('login-username') && username !== null) document.getElementById('login-username').value = username; if (document.getElementById('login-email') && email !== null) document.getElementById('login-email').value = email; var usernameOrEmailInput = document.getElementById('login-username-or-email'); if (usernameOrEmailInput) { if (email !== null) usernameOrEmailInput.value = email; if (username !== null) usernameOrEmailInput.value = username; } if (password !== null) document.getElementById('login-password').value = password; }, 'keypress #login-username, keypress #login-email, keypress #login-username-or-email, keypress #login-password, keypress #login-password-again': function (event) { if (event.keyCode === 13) loginOrSignup(); } }); Template._loginButtonsLoggedOutDropdown.helpers({ // additional classes that can be helpful in styling the dropdown additionalClasses: function () { if (!hasPasswordService()) { return false; } else { if (loginButtonsSession.get('inSignupFlow')) { return 'login-form-create-account'; } else if (loginButtonsSession.get('inForgotPasswordFlow')) { return 'login-form-forgot-password'; } else { return 'login-form-sign-in'; } } }, dropdownVisible: function () { return loginButtonsSession.get('dropdownVisible'); }, hasPasswordService: hasPasswordService }); // return all login services, with password last Template._loginButtonsLoggedOutAllServices.helpers({ services: getLoginServices, isPasswordService: function () { return this.name === 'password'; }, hasOtherServices: function () { return getLoginServices().length > 1; }, hasPasswordService: hasPasswordService }); Template._loginButtonsLoggedOutPasswordService.helpers({ fields: function () { var loginFields = [ {fieldName: 'username-or-email', fieldLabel: 'Username or Email', visible: function () { return _.contains( ["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL"], passwordSignupFields()); }}, {fieldName: 'username', fieldLabel: 'Username', visible: function () { return passwordSignupFields() === "USERNAME_ONLY"; }}, {fieldName: 'email', fieldLabel: 'Email', inputType: 'email', visible: function () { return passwordSignupFields() === "EMAIL_ONLY"; }}, {fieldName: 'password', fieldLabel: 'Password', inputType: 'password', visible: function () { return true; }} ]; var signupFields = [ {fieldName: 'username', fieldLabel: 'Username', visible: function () { return _.contains( ["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"], passwordSignupFields()); }}, {fieldName: 'email', fieldLabel: 'Email', inputType: 'email', visible: function () { return _.contains( ["USERNAME_AND_EMAIL", "EMAIL_ONLY"], passwordSignupFields()); }}, {fieldName: 'email', fieldLabel: 'Email (optional)', inputType: 'email', visible: function () { return passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL"; }}, {fieldName: 'password', fieldLabel: 'Password', inputType: 'password', visible: function () { return true; }}, {fieldName: 'password-again', fieldLabel: 'Password (again)', inputType: 'password', visible: function () { // No need to make users double-enter their password if // they'll necessarily have an email set, since they can use // the "forgot password" flow. return _.contains( ["USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"], passwordSignupFields()); }} ]; return loginButtonsSession.get('inSignupFlow') ? signupFields : loginFields; }, inForgotPasswordFlow: function () { return loginButtonsSession.get('inForgotPasswordFlow'); }, inLoginFlow: function () { return !loginButtonsSession.get('inSignupFlow') && !loginButtonsSession.get('inForgotPasswordFlow'); }, inSignupFlow: function () { return loginButtonsSession.get('inSignupFlow'); }, showCreateAccountLink: function () { return !Accounts._options.forbidClientAccountCreation; }, showForgotPasswordLink: function () { return _.contains( ["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "EMAIL_ONLY"], passwordSignupFields()); } }); Template._loginButtonsFormField.helpers({ inputType: function () { return this.inputType || "text"; } }); // // loginButtonsChangePassword template // Template._loginButtonsChangePassword.events({ 'keypress #login-old-password, keypress #login-password, keypress #login-password-again': function (event) { if (event.keyCode === 13) changePassword(); }, 'click #login-buttons-do-change-password': function () { changePassword(); } }); Template._loginButtonsChangePassword.helpers({ fields: function () { return [ {fieldName: 'old-password', fieldLabel: 'Current Password', inputType: 'password', visible: function () { return true; }}, {fieldName: 'password', fieldLabel: 'New Password', inputType: 'password', visible: function () { return true; }}, {fieldName: 'password-again', fieldLabel: 'New Password (again)', inputType: 'password', visible: function () { // No need to make users double-enter their password if // they'll necessarily have an email set, since they can use // the "forgot password" flow. return _.contains( ["USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"], passwordSignupFields()); }} ]; } }); // // helpers // var elementValueById = function(id) { var element = document.getElementById(id); if (!element) return null; else return element.value; }; var trimmedElementValueById = function(id) { var element = document.getElementById(id); if (!element) return null; else return element.value.replace(/^\s*|\s*$/g, ""); // trim() doesn't work on IE8; }; var loginOrSignup = function () { if (loginButtonsSession.get('inSignupFlow')) signup(); else login(); }; var login = function () { loginButtonsSession.resetMessages(); var username = trimmedElementValueById('login-username'); var email = trimmedElementValueById('login-email'); var usernameOrEmail = trimmedElementValueById('login-username-or-email'); // notably not trimmed. a password could (?) start or end with a space var password = elementValueById('login-password'); var loginSelector; if (username !== null) { if (!validateUsername(username)) return; else loginSelector = {username: username}; } else if (email !== null) { if (!validateEmail(email)) return; else loginSelector = {email: email}; } else if (usernameOrEmail !== null) { // XXX not sure how we should validate this. but this seems good enough (for now), // since an email must have at least 3 characters anyways if (!validateUsername(usernameOrEmail)) return; else loginSelector = usernameOrEmail; } else { throw new Error("Unexpected -- no element to use as a login user selector"); } Meteor.loginWithPassword(loginSelector, password, function (error, result) { if (error) { loginButtonsSession.errorMessage(error.reason || "Unknown error"); } else { loginButtonsSession.closeDropdown(); } }); }; var signup = function () { loginButtonsSession.resetMessages(); var options = {}; // to be passed to Accounts.createUser var username = trimmedElementValueById('login-username'); if (username !== null) { if (!validateUsername(username)) return; else options.username = username; } var email = trimmedElementValueById('login-email'); if (email !== null) { if (!validateEmail(email)) return; else options.email = email; } // notably not trimmed. a password could (?) start or end with a space var password = elementValueById('login-password'); if (!validatePassword(password)) return; else options.password = password; if (!matchPasswordAgainIfPresent()) return; Accounts.createUser(options, function (error) { if (error) { loginButtonsSession.errorMessage(error.reason || "Unknown error"); } else { loginButtonsSession.closeDropdown(); } }); }; var forgotPassword = function () { loginButtonsSession.resetMessages(); var email = trimmedElementValueById("forgot-password-email"); if (email.indexOf('@') !== -1) { Accounts.forgotPassword({email: email}, function (error) { if (error) loginButtonsSession.errorMessage(error.reason || "Unknown error"); else loginButtonsSession.infoMessage("Email sent"); }); } else { loginButtonsSession.errorMessage("Invalid email"); } }; var changePassword = function () { loginButtonsSession.resetMessages(); // notably not trimmed. a password could (?) start or end with a space var oldPassword = elementValueById('login-old-password'); // notably not trimmed. a password could (?) start or end with a space var password = elementValueById('login-password'); if (!validatePassword(password)) return; if (!matchPasswordAgainIfPresent()) return; Accounts.changePassword(oldPassword, password, function (error) { if (error) { loginButtonsSession.errorMessage(error.reason || "Unknown error"); } else { loginButtonsSession.set('inChangePasswordFlow', false); loginButtonsSession.set('inMessageOnlyFlow', true); loginButtonsSession.infoMessage("Password changed"); } }); }; var matchPasswordAgainIfPresent = function () { // notably not trimmed. a password could (?) start or end with a space var passwordAgain = elementValueById('login-password-again'); if (passwordAgain !== null) { // notably not trimmed. a password could (?) start or end with a space var password = elementValueById('login-password'); if (password !== passwordAgain) { loginButtonsSession.errorMessage("Passwords don't match"); return false; } } return true; }; ================================================ FILE: packages/accounts-ui-unstyled/login_buttons_session.js ================================================ var VALID_KEYS = [ 'dropdownVisible', // XXX consider replacing these with one key that has an enum for values. 'inSignupFlow', 'inForgotPasswordFlow', 'inChangePasswordFlow', 'inMessageOnlyFlow', 'errorMessage', 'infoMessage', // dialogs with messages (info and error) 'resetPasswordToken', 'enrollAccountToken', 'justVerifiedEmail', 'justResetPassword', 'configureLoginServiceDialogVisible', 'configureLoginServiceDialogServiceName', 'configureLoginServiceDialogSaveDisabled', 'configureOnDesktopVisible' ]; var validateKey = function (key) { if (!_.contains(VALID_KEYS, key)) throw new Error("Invalid key in loginButtonsSession: " + key); }; var KEY_PREFIX = "Meteor.loginButtons."; // XXX This should probably be package scope rather than exported // (there was even a comment to that effect here from before we had // namespacing) but accounts-ui-viewer uses it, so leave it as is for // now Accounts._loginButtonsSession = { set: function(key, value) { validateKey(key); if (_.contains(['errorMessage', 'infoMessage'], key)) throw new Error("Don't set errorMessage or infoMessage directly. Instead, use errorMessage() or infoMessage()."); this._set(key, value); }, _set: function(key, value) { Session.set(KEY_PREFIX + key, value); }, get: function(key) { validateKey(key); return Session.get(KEY_PREFIX + key); }, closeDropdown: function () { this.set('inSignupFlow', false); this.set('inForgotPasswordFlow', false); this.set('inChangePasswordFlow', false); this.set('inMessageOnlyFlow', false); this.set('dropdownVisible', false); this.resetMessages(); }, infoMessage: function(message) { this._set("errorMessage", null); this._set("infoMessage", message); this.ensureMessageVisible(); }, errorMessage: function(message) { this._set("errorMessage", message); this._set("infoMessage", null); this.ensureMessageVisible(); }, // is there a visible dialog that shows messages (info and error) isMessageDialogVisible: function () { return this.get('resetPasswordToken') || this.get('enrollAccountToken') || this.get('justVerifiedEmail'); }, // ensure that somethings displaying a message (info or error) is // visible. if a dialog with messages is open, do nothing; // otherwise open the dropdown. // // notably this doesn't matter when only displaying a single login // button since then we have an explicit message dialog // (_loginButtonsMessageDialog), and dropdownVisible is ignored in // this case. ensureMessageVisible: function () { if (!this.isMessageDialogVisible()) this.set("dropdownVisible", true); }, resetMessages: function () { this._set("errorMessage", null); this._set("infoMessage", null); }, configureService: function (name) { if (Meteor.isCordova) { this.set('configureOnDesktopVisible', true); } else { this.set('configureLoginServiceDialogVisible', true); this.set('configureLoginServiceDialogServiceName', name); this.set('configureLoginServiceDialogSaveDisabled', true); } } }; ================================================ FILE: packages/accounts-ui-unstyled/login_buttons_single.html ================================================ ================================================ FILE: packages/accounts-ui-unstyled/login_buttons_single.js ================================================ // for convenience var loginButtonsSession = Accounts._loginButtonsSession; var loginResultCallback = function (serviceName, err) { if (!err) { loginButtonsSession.closeDropdown(); } else if (err instanceof Accounts.LoginCancelledError) { // do nothing } else if (err instanceof ServiceConfiguration.ConfigError) { loginButtonsSession.configureService(serviceName); } else { loginButtonsSession.errorMessage(err.reason || "Unknown error"); } }; // In the login redirect flow, we'll have the result of the login // attempt at page load time when we're redirected back to the // application. Register a callback to update the UI (i.e. to close // the dialog on a successful login or display the error on a failed // login). // Accounts.onPageLoadLogin(function (attemptInfo) { // Ignore if we have a left over login attempt for a service that is no longer registered. if (_.contains(_.pluck(getLoginServices(), "name"), attemptInfo.type)) loginResultCallback(attemptInfo.type, attemptInfo.error); }); Template._loginButtonsLoggedOutSingleLoginButton.events({ 'click .login-button': function () { var serviceName = this.name; loginButtonsSession.resetMessages(); // XXX Service providers should be able to specify their // `Meteor.loginWithX` method name. var loginWithService = Meteor["loginWith" + (serviceName === 'meteor-developer' ? 'MeteorDeveloperAccount' : capitalize(serviceName))]; var options = {}; // use default scope unless specified if (Accounts.ui._options.requestPermissions[serviceName]) options.requestPermissions = Accounts.ui._options.requestPermissions[serviceName]; if (Accounts.ui._options.requestOfflineToken[serviceName]) options.requestOfflineToken = Accounts.ui._options.requestOfflineToken[serviceName]; if (Accounts.ui._options.forceApprovalPrompt[serviceName]) options.forceApprovalPrompt = Accounts.ui._options.forceApprovalPrompt[serviceName]; loginWithService(options, function (err) { loginResultCallback(serviceName, err); }); } }); Template._loginButtonsLoggedOutSingleLoginButton.helpers({ configured: function () { return !!ServiceConfiguration.configurations.findOne({service: this.name}); }, capitalizedName: function () { if (this.name === 'github') // XXX we should allow service packages to set their capitalized name return 'GitHub'; else if (this.name === 'meteor-developer') return 'Meteor'; else return capitalize(this.name); } }); // XXX from http://epeli.github.com/underscore.string/lib/underscore.string.js var capitalize = function(str){ str = str == null ? '' : String(str); return str.charAt(0).toUpperCase() + str.slice(1); }; ================================================ FILE: packages/accounts-ui-unstyled/package.js ================================================ Package.describe({ summary: "Unstyled version of login widgets", version: "1.1.12" }); Package.onUse(function (api) { api.use(['tracker', 'service-configuration', 'accounts-base', 'underscore', 'templating', 'session', 'jquery'], 'client'); // Export Accounts (etc) to packages using this one. api.imply('accounts-base', ['client', 'server']); // Allow us to call Accounts.oauth.serviceNames, if there are any OAuth // services. api.use('accounts-oauth', {weak: true}); // Allow us to directly test if accounts-password (which doesn't use // Accounts.oauth.registerService) exists. api.use('accounts-password', {weak: true}); api.addFiles([ 'accounts_ui.js', 'login_buttons.html', 'login_buttons_single.html', 'login_buttons_dropdown.html', 'login_buttons_dialogs.html', 'login_buttons_session.js', 'login_buttons.js', 'login_buttons_single.js', 'login_buttons_dropdown.js', 'login_buttons_dialogs.js'], 'client'); // The less source defining the default style for accounts-ui. Just adding // this package doesn't actually apply these styles; they need to be // `@import`ed from some non-import less file. The accounts-ui package does // that for you, or you can do it in your app. api.use('less'); api.addFiles('login_buttons.import.less'); }); Package.onTest(function (api) { api.use('accounts-ui-unstyled'); api.use('tinytest'); api.addFiles('accounts_ui_tests.js', 'client'); }); ================================================ FILE: packages/accounts-weibo/.gitignore ================================================ .build* ================================================ FILE: packages/accounts-weibo/README.md ================================================ # accounts-weibo A login service for Weibo. See the [project page](https://www.meteor.com/accounts) on Meteor Accounts for more details. ================================================ FILE: packages/accounts-weibo/package.js ================================================ Package.describe({ summary: "Login service for Sina Weibo accounts", version: "1.0.10" }); Package.onUse(function(api) { api.use('accounts-base', ['client', 'server']); // Export Accounts (etc) to packages using this one. api.imply('accounts-base', ['client', 'server']); api.use('accounts-oauth', ['client', 'server']); api.use('weibo', ['client', 'server']); api.addFiles('weibo_login_button.css', 'client'); api.addFiles("weibo.js"); }); ================================================ FILE: packages/accounts-weibo/weibo.js ================================================ Accounts.oauth.registerService('weibo'); if (Meteor.isClient) { Meteor.loginWithWeibo = function(options, callback) { // support a callback without options if (! callback && typeof options === "function") { callback = options; options = null; } var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); Weibo.requestCredential(options, credentialRequestCompleteCallback); }; } else { Accounts.addAutopublishFields({ // publish all fields including access token, which can legitimately // be used from the client (if transmitted over ssl or on localhost) forLoggedInUser: ['services.weibo'], forOtherUsers: ['services.weibo.screenName'] }); } ================================================ FILE: packages/accounts-weibo/weibo_login_button.css ================================================ #login-buttons-image-weibo { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAKySURBVDhPY2AgEpR5sjf/nS/6//UkoX+XJltuCvVxkcOp9cyZM1w/r13TuXvmDD9MkYIwg7qrNrubnzFb6J5intPHqrnvCnIwyKIYsmrVKuaFYWEFW2Sk79zX0f6/REHhKFABC0zRsky+rXMSeZdKCTLIHqgUvLAknW8L3IAQDw/RFlbWnQ801P+DNN8D4n0qyk94GRiEjTg5Lbz4+YOCdbhjVmTxbZwex7PUW58t8O1Ukf9gA2IDAoRPWFudfayt9f+mpsb/6yrK/28qKf4/ISf7YZu83K07QMNe6On9nyWusMtVm813azH/UWctZo/vc8TABjB3CApufAzSqKjw/7apyf+nMdH/XxUX/X+RnfX/qY/3/5tqqv/vq6v936KsfB2onltaiEHGx5AteFep4EmGUEHB1Adamv9v6er8fztp0v//79////nr1/+3X778B4N///5/O3jw/0N39//nlBQ/louLd4MMAWImcPhsU1G6DfLvt717wepnz537X0FB4T8fL+//AH///2/evgWL/7l///9dE+P/b4AWTZSWXg/UzAj2/w2gs59mZYEV7d+//z8rE9N/JUXF/w62tiD//a+urIS4BAgeA712Cxg2F40M36alpXGBDTgmI/3hdUU5WEFjff3/wvx8MNvcxARsQE1VFUQ30Et37Oz+P1RV+b/J0nIjUATigmgBvtzH5mb//9++/f/mkyf/A4KC/nv7+oI1W1hb/3/1+fP//9+//39ekP//CVDzTlnZxxtnz1ZBSUDeDAyZh7W13nybOeP/7W1b/09rbf2/FhgWHy9c+P912bL/D11d/l+WEP8/SUR4Ox8DA6pmmEkpHh4ya0JCim4lJGx7kZp8821CwrN7Hh4Pr7m6nDoSET61PjDQichsA3T7//+s/16/5gXSkIAa1AAAh8dhOVd5xHAAAAAASUVORK5CYII=); } ================================================ FILE: packages/allow-deny/README.md ================================================ ================================================ FILE: packages/allow-deny/allow-deny-tests.js ================================================ // Currently in 'mongo' package ================================================ FILE: packages/allow-deny/allow-deny.js ================================================ /// /// Remote methods and access control. /// // Restrict default mutators on collection. allow() and deny() take the // same options: // // options.insert {Function(userId, doc)} // return true to allow/deny adding this document // // options.update {Function(userId, docs, fields, modifier)} // return true to allow/deny updating these documents. // `fields` is passed as an array of fields that are to be modified // // options.remove {Function(userId, docs)} // return true to allow/deny removing these documents // // options.fetch {Array} // Fields to fetch for these validators. If any call to allow or deny // does not have this option then all fields are loaded. // // allow and deny can be called multiple times. The validators are // evaluated as follows: // - If neither deny() nor allow() has been called on the collection, // then the request is allowed if and only if the "insecure" smart // package is in use. // - Otherwise, if any deny() function returns true, the request is denied. // - Otherwise, if any allow() function returns true, the request is allowed. // - Otherwise, the request is denied. // // Meteor may call your deny() and allow() functions in any order, and may not // call all of them if it is able to make a decision without calling them all // (so don't include side effects). AllowDeny = { CollectionPrototype: {} }; // In the `mongo` package, we will extend Mongo.Collection.prototype with these // methods const CollectionPrototype = AllowDeny.CollectionPrototype; /** * @summary Allow users to write directly to this collection from client code, subject to limitations you define. * @locus Server * @method allow * @memberOf Mongo.Collection * @instance * @param {Object} options * @param {Function} options.insert,update,remove Functions that look at a proposed modification to the database and return true if it should be allowed. * @param {String[]} options.fetch Optional performance enhancement. Limits the fields that will be fetched from the database for inspection by your `update` and `remove` functions. * @param {Function} options.transform Overrides `transform` on the [`Collection`](#collections). Pass `null` to disable transformation. */ CollectionPrototype.allow = function(options) { addValidator(this, 'allow', options); }; /** * @summary Override `allow` rules. * @locus Server * @method deny * @memberOf Mongo.Collection * @instance * @param {Object} options * @param {Function} options.insert,update,remove Functions that look at a proposed modification to the database and return true if it should be denied, even if an [allow](#allow) rule says otherwise. * @param {String[]} options.fetch Optional performance enhancement. Limits the fields that will be fetched from the database for inspection by your `update` and `remove` functions. * @param {Function} options.transform Overrides `transform` on the [`Collection`](#collections). Pass `null` to disable transformation. */ CollectionPrototype.deny = function(options) { addValidator(this, 'deny', options); }; CollectionPrototype._defineMutationMethods = function(options) { const self = this; options = options || {}; // set to true once we call any allow or deny methods. If true, use // allow/deny semantics. If false, use insecure mode semantics. self._restricted = false; // Insecure mode (default to allowing writes). Defaults to 'undefined' which // means insecure iff the insecure package is loaded. This property can be // overriden by tests or packages wishing to change insecure mode behavior of // their collections. self._insecure = undefined; self._validators = { insert: {allow: [], deny: []}, update: {allow: [], deny: []}, remove: {allow: [], deny: []}, upsert: {allow: [], deny: []}, // dummy arrays; can't set these! fetch: [], fetchAllFields: false }; if (!self._name) return; // anonymous collection // XXX Think about method namespacing. Maybe methods should be // "Meteor:Mongo:insert/NAME"? self._prefix = '/' + self._name + '/'; // Mutation Methods // Minimongo on the server gets no stubs; instead, by default // it wait()s until its result is ready, yielding. // This matches the behavior of macromongo on the server better. // XXX see #MeteorServerNull if (self._connection && (self._connection === Meteor.server || Meteor.isClient)) { const m = {}; _.each(['insert', 'update', 'remove'], function (method) { const methodName = self._prefix + method; if (options.useExisting) { const handlerPropName = Meteor.isClient ? '_methodHandlers' : 'method_handlers'; // Do not try to create additional methods if this has already been called. // (Otherwise the .methods() call below will throw an error.) if (self._connection[handlerPropName] && typeof self._connection[handlerPropName][methodName] === 'function') return; } m[methodName] = function (/* ... */) { // All the methods do their own validation, instead of using check(). check(arguments, [Match.Any]); const args = _.toArray(arguments); try { // For an insert, if the client didn't specify an _id, generate one // now; because this uses DDP.randomStream, it will be consistent with // what the client generated. We generate it now rather than later so // that if (eg) an allow/deny rule does an insert to the same // collection (not that it really should), the generated _id will // still be the first use of the stream and will be consistent. // // However, we don't actually stick the _id onto the document yet, // because we want allow/deny rules to be able to differentiate // between arbitrary client-specified _id fields and merely // client-controlled-via-randomSeed fields. let generatedId = null; if (method === "insert" && !_.has(args[0], '_id')) { generatedId = self._makeNewID(); } if (this.isSimulation) { // In a client simulation, you can do any mutation (even with a // complex selector). if (generatedId !== null) args[0]._id = generatedId; return self._collection[method].apply( self._collection, args); } // This is the server receiving a method call from the client. // We don't allow arbitrary selectors in mutations from the client: only // single-ID selectors. if (method !== 'insert') throwIfSelectorIsNotId(args[0], method); if (self._restricted) { // short circuit if there is no way it will pass. if (self._validators[method].allow.length === 0) { throw new Meteor.Error( 403, "Access denied. No allow validators set on restricted " + "collection for method '" + method + "'."); } const validatedMethodName = '_validated' + method.charAt(0).toUpperCase() + method.slice(1); args.unshift(this.userId); method === 'insert' && args.push(generatedId); return self[validatedMethodName].apply(self, args); } else if (self._isInsecure()) { if (generatedId !== null) args[0]._id = generatedId; // In insecure mode, allow any mutation (with a simple selector). // XXX This is kind of bogus. Instead of blindly passing whatever // we get from the network to this function, we should actually // know the correct arguments for the function and pass just // them. For example, if you have an extraneous extra null // argument and this is Mongo on the server, the .wrapAsync'd // functions like update will get confused and pass the // "fut.resolver()" in the wrong slot, where _update will never // invoke it. Bam, broken DDP connection. Probably should just // take this whole method and write it three times, invoking // helpers for the common code. return self._collection[method].apply(self._collection, args); } else { // In secure mode, if we haven't called allow or deny, then nothing // is permitted. throw new Meteor.Error(403, "Access denied"); } } catch (e) { if (e.name === 'MongoError' || e.name === 'MinimongoError') { throw new Meteor.Error(409, e.toString()); } else { throw e; } } }; }); self._connection.methods(m); } }; CollectionPrototype._updateFetch = function (fields) { const self = this; if (!self._validators.fetchAllFields) { if (fields) { self._validators.fetch = _.union(self._validators.fetch, fields); } else { self._validators.fetchAllFields = true; // clear fetch just to make sure we don't accidentally read it self._validators.fetch = null; } } }; CollectionPrototype._isInsecure = function () { const self = this; if (self._insecure === undefined) return !!Package.insecure; return self._insecure; }; CollectionPrototype._validatedInsert = function (userId, doc, generatedId) { const self = this; // call user validators. // Any deny returns true means denied. if (_.any(self._validators.insert.deny, function(validator) { return validator(userId, docToValidate(validator, doc, generatedId)); })) { throw new Meteor.Error(403, "Access denied"); } // Any allow returns true means proceed. Throw error if they all fail. if (_.all(self._validators.insert.allow, function(validator) { return !validator(userId, docToValidate(validator, doc, generatedId)); })) { throw new Meteor.Error(403, "Access denied"); } // If we generated an ID above, insert it now: after the validation, but // before actually inserting. if (generatedId !== null) doc._id = generatedId; self._collection.insert.call(self._collection, doc); }; // Simulate a mongo `update` operation while validating that the access // control rules set by calls to `allow/deny` are satisfied. If all // pass, rewrite the mongo operation to use $in to set the list of // document ids to change ##ValidatedChange CollectionPrototype._validatedUpdate = function( userId, selector, mutator, options) { const self = this; check(mutator, Object); options = _.clone(options) || {}; if (!LocalCollection._selectorIsIdPerhapsAsObject(selector)) throw new Error("validated update should be of a single ID"); // We don't support upserts because they don't fit nicely into allow/deny // rules. if (options.upsert) throw new Meteor.Error(403, "Access denied. Upserts not " + "allowed in a restricted collection."); const noReplaceError = "Access denied. In a restricted collection you can only" + " update documents, not replace them. Use a Mongo update operator, such " + "as '$set'."; // compute modified fields const fields = []; if (_.isEmpty(mutator)) { throw new Meteor.Error(403, noReplaceError); } _.each(mutator, function (params, op) { if (op.charAt(0) !== '$') { throw new Meteor.Error(403, noReplaceError); } else if (!_.has(ALLOWED_UPDATE_OPERATIONS, op)) { throw new Meteor.Error( 403, "Access denied. Operator " + op + " not allowed in a restricted collection."); } else { _.each(_.keys(params), function (field) { // treat dotted fields as if they are replacing their // top-level part if (field.indexOf('.') !== -1) field = field.substring(0, field.indexOf('.')); // record the field we are trying to change if (!_.contains(fields, field)) fields.push(field); }); } }); const findOptions = {transform: null}; if (!self._validators.fetchAllFields) { findOptions.fields = {}; _.each(self._validators.fetch, function(fieldName) { findOptions.fields[fieldName] = 1; }); } const doc = self._collection.findOne(selector, findOptions); if (!doc) // none satisfied! return 0; // call user validators. // Any deny returns true means denied. if (_.any(self._validators.update.deny, function(validator) { const factoriedDoc = transformDoc(validator, doc); return validator(userId, factoriedDoc, fields, mutator); })) { throw new Meteor.Error(403, "Access denied"); } // Any allow returns true means proceed. Throw error if they all fail. if (_.all(self._validators.update.allow, function(validator) { const factoriedDoc = transformDoc(validator, doc); return !validator(userId, factoriedDoc, fields, mutator); })) { throw new Meteor.Error(403, "Access denied"); } options._forbidReplace = true; // Back when we supported arbitrary client-provided selectors, we actually // rewrote the selector to include an _id clause before passing to Mongo to // avoid races, but since selector is guaranteed to already just be an ID, we // don't have to any more. return self._collection.update.call( self._collection, selector, mutator, options); }; // Only allow these operations in validated updates. Specifically // whitelist operations, rather than blacklist, so new complex // operations that are added aren't automatically allowed. A complex // operation is one that does more than just modify its target // field. For now this contains all update operations except '$rename'. // http://docs.mongodb.org/manual/reference/operators/#update const ALLOWED_UPDATE_OPERATIONS = { $inc:1, $set:1, $unset:1, $addToSet:1, $pop:1, $pullAll:1, $pull:1, $pushAll:1, $push:1, $bit:1 }; // Simulate a mongo `remove` operation while validating access control // rules. See #ValidatedChange CollectionPrototype._validatedRemove = function(userId, selector) { const self = this; const findOptions = {transform: null}; if (!self._validators.fetchAllFields) { findOptions.fields = {}; _.each(self._validators.fetch, function(fieldName) { findOptions.fields[fieldName] = 1; }); } const doc = self._collection.findOne(selector, findOptions); if (!doc) return 0; // call user validators. // Any deny returns true means denied. if (_.any(self._validators.remove.deny, function(validator) { return validator(userId, transformDoc(validator, doc)); })) { throw new Meteor.Error(403, "Access denied"); } // Any allow returns true means proceed. Throw error if they all fail. if (_.all(self._validators.remove.allow, function(validator) { return !validator(userId, transformDoc(validator, doc)); })) { throw new Meteor.Error(403, "Access denied"); } // Back when we supported arbitrary client-provided selectors, we actually // rewrote the selector to {_id: {$in: [ids that we found]}} before passing to // Mongo to avoid races, but since selector is guaranteed to already just be // an ID, we don't have to any more. return self._collection.remove.call(self._collection, selector); }; CollectionPrototype._callMutatorMethod = function _callMutatorMethod(name, args, callback) { if (Meteor.isClient && !callback && !alreadyInSimulation()) { // Client can't block, so it can't report errors by exception, // only by callback. If they forget the callback, give them a // default one that logs the error, so they aren't totally // baffled if their writes don't work because their database is // down. // Don't give a default callback in simulation, because inside stubs we // want to return the results from the local collection immediately and // not force a callback. callback = function (err) { if (err) Meteor._debug(name + " failed: " + (err.reason || err.stack)); }; } // For two out of three mutator methods, the first argument is a selector const firstArgIsSelector = name === "update" || name === "remove"; if (firstArgIsSelector && !alreadyInSimulation()) { // If we're about to actually send an RPC, we should throw an error if // this is a non-ID selector, because the mutation methods only allow // single-ID selectors. (If we don't throw here, we'll see flicker.) throwIfSelectorIsNotId(args[0], name); } const mutatorMethodName = this._prefix + name; return this._connection.apply( mutatorMethodName, args, { returnStubValue: true }, callback); } function transformDoc(validator, doc) { if (validator.transform) return validator.transform(doc); return doc; } function docToValidate(validator, doc, generatedId) { let ret = doc; if (validator.transform) { ret = EJSON.clone(doc); // If you set a server-side transform on your collection, then you don't get // to tell the difference between "client specified the ID" and "server // generated the ID", because transforms expect to get _id. If you want to // do that check, you can do it with a specific // `C.allow({insert: f, transform: null})` validator. if (generatedId !== null) { ret._id = generatedId; } ret = validator.transform(ret); } return ret; } function addValidator(collection, allowOrDeny, options) { // validate keys const VALID_KEYS = ['insert', 'update', 'remove', 'fetch', 'transform']; _.each(_.keys(options), function (key) { if (!_.contains(VALID_KEYS, key)) throw new Error(allowOrDeny + ": Invalid key: " + key); }); collection._restricted = true; _.each(['insert', 'update', 'remove'], function (name) { if (options.hasOwnProperty(name)) { if (!(options[name] instanceof Function)) { throw new Error(allowOrDeny + ": Value for `" + name + "` must be a function"); } // If the transform is specified at all (including as 'null') in this // call, then take that; otherwise, take the transform from the // collection. if (options.transform === undefined) { options[name].transform = collection._transform; // already wrapped } else { options[name].transform = LocalCollection.wrapTransform( options.transform); } collection._validators[name][allowOrDeny].push(options[name]); } }); // Only update the fetch fields if we're passed things that affect // fetching. This way allow({}) and allow({insert: f}) don't result in // setting fetchAllFields if (options.update || options.remove || options.fetch) { if (options.fetch && !(options.fetch instanceof Array)) { throw new Error(allowOrDeny + ": Value for `fetch` must be an array"); } collection._updateFetch(options.fetch); } } function throwIfSelectorIsNotId(selector, methodName) { if (!LocalCollection._selectorIsIdPerhapsAsObject(selector)) { throw new Meteor.Error( 403, "Not permitted. Untrusted code may only " + methodName + " documents by ID."); } }; // Determine if we are in a DDP method simulation function alreadyInSimulation() { const enclosing = DDP._CurrentInvocation.get(); return enclosing && enclosing.isSimulation; } ================================================ FILE: packages/allow-deny/package.js ================================================ Package.describe({ name: 'allow-deny', version: '1.0.5', // Brief, one-line summary of the package. summary: 'Implements functionality for allow/deny and client-side db operations', // URL to the Git repository containing the source code for this package. git: 'https://github.com/meteor/meteor', // By default, Meteor will default to using README.md for documentation. // To avoid submitting documentation, set this field to null. documentation: null }); Package.onUse(function(api) { api.use([ 'ecmascript', 'underscore', 'minimongo', // Just for LocalCollection.wrapTransform :[ 'check', 'ejson', 'ddp', ]); api.addFiles('allow-deny.js'); api.export('AllowDeny'); }); Package.onTest(function(api) { api.use('ecmascript'); api.use('tinytest'); api.use('allow-deny'); api.addFiles('allow-deny-tests.js'); }); ================================================ FILE: packages/appcache/.gitignore ================================================ .build* ================================================ FILE: packages/appcache/QA.md ================================================ # QA Notes ## Viewing the app cache Chrome: Navigate to chrome://appcache-internals/ Firefox: Open Tools / Advanced / Network. The section reading "The following websites are allowed to store data for offline use" will show the amount of data in the app cache ("1.2 MB"). If this number is 0 the app is permitted to use the app cache but the app cache is currently turned off. ## Setup Create a simple static app and add the appcache package. static.html: ```` some static content ```` ## App is cached offline Run Meteor, load the app in the browser, stop Meteor. Reload the page in the browser and observe the content is still visible. ## Hot code reload still works Run Meteor, open the app in the browser. Make a change to static.html. Observe the change appear in the web page. Note that it is normal when using the app cache for the page reload to be delayed a bit while the browser fetches the changed code in the background. Without app cache: (page goes blank) -> (browser fetches) -> (page renders) With app cache: (browser fetches) -> (page goes blank) -> (page renders) ## Enabling / disabling the appcache turns the app cache on / off Run Meteor, open the app in the browser. Disable your browser in the appcache config. For example, if you're using Chrome: ```` if (Meteor.isServer) { Meteor.AppCache.config({ chrome: false }); } ```` Observe following the hot code reload the app is no longer cached. Enable your browser again: ```` if (Meteor.isServer) { Meteor.AppCache.config({ chrome: true }); } ```` Observe following the hot code reload the app is cached again. ## Removing the appcache package turns off app caching Start Meteor, open the app in the browser. Stop Meteor, remove the appcache package, remove or comment out the call to Meteor.AppCache.config in static.js, start Meteor again. Wait for the browser to reestablish its livedata connection. Observe following the hot code reload that the app is no longer cached. ================================================ FILE: packages/appcache/README.md ================================================ # appcache The `appcache` package, part of [Webapp](https://www.meteor.com/webapp), stores the static parts of a Meteor application (the client side Javascript, HTML, CSS, and images) in the browser's [application cache](https://en.wikipedia.org/wiki/AppCache). ## Using Appcache To enable caching simply add the `appcache` package to your project. * Once a user has visited a Meteor application for the first time and the application has been cached, on subsequent visits the web page loads faster because the browser can load the application out of the cache without contacting the server first. * Hot code pushes are loaded by the browser in the background while the app continues to run. Once the new code has been fully loaded the browser is able to switch over to the new code quickly. * The application cache allows the application to be loaded even when the browser doesn't have an Internet connection, and so enables using the app offline. (Note however that the `appcache` package by itself doesn't make *data* available offline: in an application loaded offline, a Meteor Collection will appear to be empty in the client until the Internet becomes available and the browser is able to establish a DDP connection). To turn AppCache off for specific browsers use: Meteor.AppCache.config({ chrome: false, firefox: false }); The supported browsers that can be enabled or disabled include, but are not limited to, `android`, `chrome`, `chromium`, `chromeMobileIOS`, `firefox`, `ie`, `mobileSafari` and `safari`. Browsers limit the amount of data they will put in the application cache, which can vary due to factors such as how much disk space is free. Unfortunately if your application goes over the limit rather than disabling the application cache altogether and running the application online, the browser will instead fail that particular *update* of the cache, leaving your users running old code. Thus it's best to keep the size of the cache below 5MB. The `appcache` package will print a warning on the Meteor server console if the total size of the resources being cached is over 5MB. If you have files too large to fit in the cache you can disable caching by URL prefix. For example, Meteor.AppCache.config({onlineOnly: ['/online/']}); causes files in your `public/online` directory to not be cached, and so they will only be available online. You can then move your large files into that directory and refer to them at the new URL: If you'd prefer not to move your files, you can use the file names themselves as the URL prefix: Meteor.AppCache.config({ onlineOnly: [ '/bigimage.jpg', '/largedata.json' ] }); though keep in mind that since the exclusion is by prefix (this is a limitation of the application cache manifest), excluding `/largedata.json` will also exclude such URLs as `/largedata.json.orig` and `/largedata.json/file1`. For more information about how Meteor interacts with the application cache, see the [AppCache page](https://github.com/meteor/meteor/wiki/AppCache) in the Meteor wiki. ================================================ FILE: packages/appcache/appcache-client.js ================================================ if (window.applicationCache) { var appCacheStatuses = [ 'uncached', 'idle', 'checking', 'downloading', 'updateready', 'obsolete' ]; var updatingAppcache = false; var reloadRetry = null; var appcacheUpdated = false; Reload._onMigrate('appcache', function (retry) { if (appcacheUpdated) return [true]; // An uncached application (one that does not have a manifest) cannot // be updated. if (window.applicationCache.status === window.applicationCache.UNCACHED) return [true]; if (!updatingAppcache) { try { window.applicationCache.update(); } catch (e) { Meteor._debug('applicationCache update error', e); // There's no point in delaying the reload if we can't update the cache. return [true]; } updatingAppcache = true; } // Delay migration until the app cache has been updated. reloadRetry = retry; return false; }); // If we're migrating and the app cache is now up to date, signal that // we're now ready to migrate. var cacheIsNowUpToDate = function () { if (!updatingAppcache) return; appcacheUpdated = true; reloadRetry(); }; window.applicationCache.addEventListener('updateready', cacheIsNowUpToDate, false); window.applicationCache.addEventListener('noupdate', cacheIsNowUpToDate, false); // We'll get the obsolete event on a 404 fetching the app.manifest: // we had previously been running with an app cache, but the app // cache has now been disabled or the appcache package removed. // Reload to get the new non-cached code. window.applicationCache.addEventListener('obsolete', (function () { if (reloadRetry) { cacheIsNowUpToDate(); } else { appcacheUpdated = true; Reload._reload(); } }), false); } // if window.applicationCache ================================================ FILE: packages/appcache/appcache-server.js ================================================ var crypto = Npm.require('crypto'); var fs = Npm.require('fs'); var path = Npm.require('path'); var _disableSizeCheck = false; Meteor.AppCache = { config: function (options) { _.each(options, function (value, option) { if (option === 'browsers') { disabledBrowsers = {}; _.each(value, function (browser) { disabledBrowsers[browser] = false; }); } else if (option === 'onlineOnly') { _.each(value, function (urlPrefix) { RoutePolicy.declare(urlPrefix, 'static-online'); }); } // option to suppress warnings for tests. else if (option === '_disableSizeCheck') { _disableSizeCheck = value; } else if (value === false) { disabledBrowsers[option] = true; } else if (value === true) { disabledBrowsers[option] = false; } else { throw new Error('Invalid AppCache config option: ' + option); } }); } }; var disabledBrowsers = {}; var browserDisabled = function (request) { return disabledBrowsers[request.browser.name]; }; WebApp.addHtmlAttributeHook(function (request) { if (browserDisabled(request)) return null; else return { manifest: "/app.manifest" }; }); WebApp.connectHandlers.use(function (req, res, next) { if (req.url !== '/app.manifest') { return next(); } // Browsers will get confused if we unconditionally serve the // manifest and then disable the app cache for that browser. If // the app cache had previously been enabled for a browser, it // will continue to fetch the manifest as long as it's available, // even if we now are not including the manifest attribute in the // app HTML. (Firefox for example will continue to display "this // website is asking to store data on your computer for offline // use"). Returning a 404 gets the browser to really turn off the // app cache. if (browserDisabled(WebApp.categorizeRequest(req))) { res.writeHead(404); res.end(); return; } var manifest = "CACHE MANIFEST\n\n"; // After the browser has downloaded the app files from the server and // has populated the browser's application cache, the browser will // *only* connect to the server and reload the application if the // *contents* of the app manifest file has changed. // // So to ensure that the client updates if client resources change, // include a hash of client resources in the manifest. manifest += "# " + WebApp.clientHash() + "\n"; // When using the autoupdate package, also include // AUTOUPDATE_VERSION. Otherwise the client will get into an // infinite loop of reloads when the browser doesn't fetch the new // app HTML which contains the new version, and autoupdate will // reload again trying to get the new code. if (Package.autoupdate) { var version = Package.autoupdate.Autoupdate.autoupdateVersion; if (version !== WebApp.clientHash()) manifest += "# " + version + "\n"; } manifest += "\n"; manifest += "CACHE:" + "\n"; manifest += "/" + "\n"; _.each(WebApp.clientPrograms[WebApp.defaultArch].manifest, function (resource) { if (resource.where === 'client' && ! RoutePolicy.classify(resource.url)) { manifest += resource.url; // If the resource is not already cacheable (has a query // parameter, presumably with a hash or version of some sort), // put a version with a hash in the cache. // // Avoid putting a non-cacheable asset into the cache, otherwise // the user can't modify the asset until the cache headers // expire. if (!resource.cacheable) manifest += "?" + resource.hash; manifest += "\n"; } }); manifest += "\n"; manifest += "FALLBACK:\n"; manifest += "/ /" + "\n"; // Add a fallback entry for each uncacheable asset we added above. // // This means requests for the bare url (/image.png instead of // /image.png?hash) will work offline. Online, however, the browser // will send a request to the server. Users can remove this extra // request to the server and have the asset served from cache by // specifying the full URL with hash in their code (manually, with // some sort of URL rewriting helper) _.each(WebApp.clientPrograms[WebApp.defaultArch].manifest, function (resource) { if (resource.where === 'client' && ! RoutePolicy.classify(resource.url) && !resource.cacheable) { manifest += resource.url + " " + resource.url + "?" + resource.hash + "\n"; } }); manifest += "\n"; manifest += "NETWORK:\n"; // TODO adding the manifest file to NETWORK should be unnecessary? // Want more testing to be sure. manifest += "/app.manifest" + "\n"; _.each( [].concat( RoutePolicy.urlPrefixesFor('network'), RoutePolicy.urlPrefixesFor('static-online') ), function (urlPrefix) { manifest += urlPrefix + "\n"; } ); manifest += "*" + "\n"; // content length needs to be based on bytes var body = new Buffer(manifest); res.setHeader('Content-Type', 'text/cache-manifest'); res.setHeader('Content-Length', body.length); return res.end(body); }); var sizeCheck = function () { var totalSize = 0; _.each(WebApp.clientPrograms[WebApp.defaultArch].manifest, function (resource) { if (resource.where === 'client' && ! RoutePolicy.classify(resource.url)) { totalSize += resource.size; } }); if (totalSize > 5 * 1024 * 1024) { Meteor._debug( "** You are using the appcache package but the total size of the\n" + "** cached resources is " + (totalSize / 1024 / 1024).toFixed(1) + "MB.\n" + "**\n" + "** This is over the recommended maximum of 5 MB and may break your\n" + "** app in some browsers! See http://docs.meteor.com/#appcache\n" + "** for more information and fixes.\n" ); } }; // Run the size check after user code has had a chance to run. That way, // the size check can take into account files that the user does not // want cached. Otherwise, the size check warning will still print even // if the user excludes their large files with // `Meteor.AppCache.config({onlineOnly: files})`. Meteor.startup(function () { if (! _disableSizeCheck) sizeCheck(); }); ================================================ FILE: packages/appcache/appcache_tests-client.js ================================================ var manifestUrl = '/app.manifest'; var appcacheTest = function (name, cb) { Tinytest.addAsync('appcache - ' + name, function (test, next) { HTTP.get(manifestUrl, function (err, res) { if (err) { test.fail(err); } else { cb(test, res); } next(); }); }); }; // Verify that the code status of the HTTP response is "OK" appcacheTest('presence', function (test, manifest) { test.equal(manifest.statusCode, 200, 'manifest not served'); }); // Verify the content-type HTTP header appcacheTest('content type', function (test, manifest) { test.equal(manifest.headers['content-type'], 'text/cache-manifest'); }); // Verify that each section header is only set once. appcacheTest('sections uniqueness', function (test, manifest) { var content = manifest.content; var mandatorySectionHeaders = ['CACHE:', 'NETWORK:', 'FALLBACK:']; var optionalSectionHeaders = ['SETTINGS']; _.each(_.union(mandatorySectionHeaders, optionalSectionHeaders), function (sectionHeader) { var globalSearch = new RegExp(sectionHeader, "g"); var matches = content.match(globalSearch) || []; test.isTrue(matches.length <= 1, sectionHeader + ' is set twice'); if (_.contains(mandatorySectionHeaders, sectionHeader)) { test.isTrue(matches.length == 1, sectionHeader + ' is not set'); } }); }); // Verify the content of the header and of each section of the manifest using // regular expressions. Regular expressions matches malformed URIs but that's // not what we're trying to catch here (the user is free to add its own content // in the manifest -- even malformed). appcacheTest('sections validity', function (test, manifest) { var lines = manifest.content.split('\n'); var i = 0; var currentRegex = null, line = null; var nextLine = function () { return lines[i++]; }; var eof = function () { return i >= lines.length; }; var nextLineMatches = function (expected, n) { n = n || 1; _.times(n, function () { var testFunc = _.isRegExp(expected) ? 'matches' : 'equal'; test[testFunc](nextLine(), expected); }); }; // Verify header validity nextLineMatches('CACHE MANIFEST'); nextLineMatches(''); nextLineMatches(/^# [a-z0-9]+$/i, 2); // Verify body validity while (! eof()) { line = nextLine(); // There are three distinct sections: 'CACHE', 'FALLBACK', and 'NETWORK'. // A section start with its name suffixed by a colon. When we read a new // section header, we update the currentRegex expression for the next lines // of the section. // XXX There is also a 'SETTINGS' section, not used by this package. If this // section is used, the test will fail. if (line === 'CACHE:' || line === 'NETWORK:') currentRegex = /^\S+$/; else if (line === 'FALLBACK:') currentRegex = /^\S+ \S+$/; // Blank lines and lines starting with a `#` (comments) are valid else if (line == '' || line.match(/^#.+/)) continue; // Outside sections, only blanks lines and comments are valid else if (currentRegex === null) test.fail('Invalid line ' + i + ': ' + line); // Inside a section, a star is a valid expression else if (line === '*') continue; // If it is not a blank line, not a comment, and not a header it must // match the current section format else test.matches(line, currentRegex, 'line ' + i); } }); // Verify that resources declared on the server with the `onlineOnly` parameter // are present in the network section of the manifest. The `appcache` package // also automatically add the manifest (`app.manifest`) add the star symbol to // this list and therefore we also check the presence of these two elements. appcacheTest('network section content', function (test, manifest) { var shouldBePresentInNetworkSection = [ "/app.manifest", "/online/", "/bigimage.jpg", "/largedata.json", "*" ]; var lines = manifest.content.split('\n'); var startNetworkSection = lines.indexOf('NETWORK:'); // We search the end of the 'NETWORK:' section by looking at the beginning // of any potential other section. By default we set this value to // `lines.length - 1` which is the index of the last line. var otherSections = ['CACHE:', 'FALLBACK:', 'SETTINGS']; var endNetworkSection = _.reduce(otherSections, function (min, sectionName) { var position = lines.indexOf(sectionName); return position > startNetworkSection && position < min ? position : min; }, lines.length - 1); // We remove the first line because it's the 'NETWORK:' header line. var networkLines = lines.slice(startNetworkSection + 1, endNetworkSection); _.each(shouldBePresentInNetworkSection, function (item) { test.include(networkLines, item); }); }); ================================================ FILE: packages/appcache/appcache_tests-server.js ================================================ // For our testing purpose, we don't want the cache manifest to be // active, all we want is the cache manifest to be manually request-able // so we can read it and verify its validity from the client. This is // because caching the test files would make tests non deterministic // depending on the state of the browser cache. To do that we disable // the "manifest" attribute of the tag. This runs after appcache // registers its hook, so this hook overrides the return value of the // real hook. We point to a non-existent file to clear the appcache in // case there was previously a site running with appcache on // localhost:3000. WebApp.addHtmlAttributeHook(function (request) { return { manifest: "/no-such-file" }; }); // Let's add some resources in the 'NETWORK' section Meteor.AppCache.config({ onlineOnly: [ '/online/', '/bigimage.jpg', '/largedata.json' ], _disableSizeCheck: true // don't print warnings }); ================================================ FILE: packages/appcache/package.js ================================================ Package.describe({ summary: "Enable the application cache in the browser", version: "1.0.11" }); Package.onUse(function (api) { api.use('webapp', 'server'); api.use('reload', 'client'); api.use('routepolicy', 'server'); api.use('underscore', 'server'); api.use('autoupdate', 'server', {weak: true}); api.addFiles('appcache-client.js', 'client'); api.addFiles('appcache-server.js', 'server'); }); Package.onTest(function (api) { api.use('tinytest'); api.use('appcache'); api.use('http', 'client'); api.use('underscore', 'client'); api.use('webapp', 'server'); api.addFiles('appcache_tests-server.js', 'server'); api.addFiles('appcache_tests-client.js', 'client'); }); ================================================ FILE: packages/audit-argument-checks/.gitignore ================================================ .build* ================================================ FILE: packages/audit-argument-checks/README.md ================================================ # Audit-Argument-Checks This is an internal Meteor package. ================================================ FILE: packages/audit-argument-checks/package.js ================================================ Package.describe({ summary: "Try to detect inadequate input sanitization", version: '1.0.7' }); // This package is empty; its presence is detected by livedata. ================================================ FILE: packages/autopublish/.gitignore ================================================ .build* ================================================ FILE: packages/autopublish/README.md ================================================ # autopublish Publish all server collections to the client. This package is useful for prototyping an app without worrying about which clients have access to certain data, but should be removed as soon as the app needs to restrict which data is seen by the client. The `autopublish` package is automatically added to every Meteor app by `meteor create`. ================================================ FILE: packages/autopublish/package.js ================================================ Package.describe({ summary: "(For prototyping only) Publish the entire database to all clients", version: '1.0.7' }); // This package is empty; its presence is detected by several other packages // (such as ddp-server and mongo) which check for the presence of // Package.autopublish. ================================================ FILE: packages/autoupdate/.gitignore ================================================ .build* ================================================ FILE: packages/autoupdate/QA.md ================================================ # QA Notes ## Hot Code Push Reload Run the leaderboard example, and click on one of the names. Make a change to the leaderboard.html file, see the client reload, and see that the name is still selected. ## AUTOUPDATE_VERSION Set the `AUTOUPDATE_VERSION` environment variable when running the application: $ AUTOUPDATE_VERSION=abc meteor Now when you make an HTML change, it won't appear in the client automatically. (Note the leader list flickers when the server subscription restarts, but that's not a window reload). Conversely, you can force a client reload (even without making any client code changes) by restarting the server with a new value for `AUTOUPDATE_VERSION`. ## No Client Reload on Server-only Change Revert previous changes and run the example without setting AUTOUPDATE_VERSION. Note that it might look like the browser is reloading because the page content in the leaderboard example will flicker when the server restarts because the example is using autopublish, but that the window won't actually be reloading. In the browser console, assign a variable such as `a = true` so that you can easily verify that the client hasn't reloaded. In the leaderboard example directory, create the `server` directory and add `foo.js`. See in the browser console that `a` is still defined, indicating the browser hasn't reloaded. ## Test with the appcache Add the appcache package: $ meteor add appcache And do the above tests again. Note that if 1) AUTOUPDATE_VERSION is set so the client doesn't automatically reload, 2) you make a client change, and 3) you manually reload the browser page, you usually *won't* see the updated HTML the *first* time you reload (unless the browser happened to check the app cache manifest between steps 2 and 3). This is normal browser app cache behavior: the browser populates the app cache in the background, so it doesn't wait for new files to download before displaying the web page. ## Autoupdate.newClientAvailable Undo previous changes made, such as by using `git checkout .` Reload the client, which will cause the browser to stop using the app cache. It's hard to see the `newClientAvailable` reactive variable when the client automatically reloads. Remove the `hot-code-push` package so you can see the variable without having the client also reload. $ meteor remove meteor-base $ meteor add meteor webapp ddp autoupdate Add to leaderboard.js: Template.leaderboard.helpers({ available: function () { return Autoupdate.newClientAvailable().toString(); } }); And add `{{available}}` to the leaderboard template in leaderboard.html. Initially you'll see `false`, and then when you make a change to the leaderboard HTML you'll see the variable change to `true`. (You won't see the new HTML on the client because you disabled reload). Amusingly, you can undo the addition you made to the HTML and the "new client available" variable will go back to `false` (you now don't have client code available on the server different than what's running in the browser), because by default the client version is based on a hash of the client files. ## DDP Version Negotiation Failure A quick way to test DDP version negotiation failure is to force the client to use the wrong DDP version. At the top of livedata_connection.js: var Connection = function (url, options) { var self = this; + options.supportedDDPVersions = ['abc']; You will see the client reload (in the hope that new client code will be available that can successfully negotiation the DDP version). Each reload takes longer than the one before, using an exponential backoff. If you remove the `options.supportedDDPVersions` line and allow the client to connect (or manually reload the browser page so you don't have to wait), this will reset the exponential backoff counter. You can verify the counter was reset by adding the line back in a second time, and you'll see the reload cycle start over again with first reloading quickly, and then again taking longer between tries. ================================================ FILE: packages/autoupdate/README.md ================================================ # autoupdate This package is the heart of Meteor's Hot Code Push functionality. It has a client component and a server component component. The client component uses a DDP API provided by the server to subscribe to the version ID of the most recent build of the app's client. When it sees that a new version is available, it uses the [reload](https://atmospherejs.com/meteor/reload) package (if included in the app) to gracefully save the app's state and reload it in place. `autoupdate` is part of the [Webapp](https://www.meteor.com/webapp) project. ================================================ FILE: packages/autoupdate/autoupdate_client.js ================================================ // Subscribe to the `meteor_autoupdate_clientVersions` collection, // which contains the set of acceptable client versions. // // A "hard code push" occurs when the running client version is not in // the set of acceptable client versions (or the server updates the // collection, there is a published client version marked `current` and // the running client version is no longer in the set). // // When the `reload` package is loaded, a hard code push causes // the browser to reload, so that it will load the latest client // version from the server. // // A "soft code push" represents the situation when the running client // version is in the set of acceptable versions, but there is a newer // version available on the server. // // `Autoupdate.newClientAvailable` is a reactive data source which // becomes `true` if there is a new version of the client is available on // the server. // // This package doesn't implement a soft code reload process itself, // but `newClientAvailable` could be used for example to display a // "click to reload" link to the user. // The client version of the client code currently running in the // browser. var autoupdateVersion = __meteor_runtime_config__.autoupdateVersion || "unknown"; var autoupdateVersionRefreshable = __meteor_runtime_config__.autoupdateVersionRefreshable || "unknown"; // The collection of acceptable client versions. ClientVersions = new Mongo.Collection("meteor_autoupdate_clientVersions"); Autoupdate = {}; Autoupdate.newClientAvailable = function () { return !! ClientVersions.findOne({ _id: "version", version: {$ne: autoupdateVersion} }) || !! ClientVersions.findOne({ _id: "version-refreshable", version: {$ne: autoupdateVersionRefreshable} }); }; Autoupdate._ClientVersions = ClientVersions; // Used by a self-test var knownToSupportCssOnLoad = false; var retry = new Retry({ // Unlike the stream reconnect use of Retry, which we want to be instant // in normal operation, this is a wacky failure. We don't want to retry // right away, we can start slowly. // // A better way than timeconstants here might be to use the knowledge // of when we reconnect to help trigger these retries. Typically, the // server fixing code will result in a restart and reconnect, but // potentially the subscription could have a transient error. minCount: 0, // don't do any immediate retries baseTimeout: 30*1000 // start with 30s }); var failures = 0; Autoupdate._retrySubscription = function () { Meteor.subscribe("meteor_autoupdate_clientVersions", { onError: function (error) { Meteor._debug("autoupdate subscription failed:", error); failures++; retry.retryLater(failures, function () { // Just retry making the subscription, don't reload the whole // page. While reloading would catch more cases (for example, // the server went back a version and is now doing old-style hot // code push), it would also be more prone to reload loops, // which look really bad to the user. Just retrying the // subscription over DDP means it is at least possible to fix by // updating the server. Autoupdate._retrySubscription(); }); }, onReady: function () { if (Package.reload) { var checkNewVersionDocument = function (doc) { var self = this; if (doc._id === 'version-refreshable' && doc.version !== autoupdateVersionRefreshable) { autoupdateVersionRefreshable = doc.version; // Switch out old css links for the new css links. Inspired by: // https://github.com/guard/guard-livereload/blob/master/js/livereload.js#L710 var newCss = (doc.assets && doc.assets.allCss) || []; var oldLinks = []; _.each(document.getElementsByTagName('link'), function (link) { if (link.className === '__meteor-css__') { oldLinks.push(link); } }); var waitUntilCssLoads = function (link, callback) { var executeCallback = _.once(callback); link.onload = function () { knownToSupportCssOnLoad = true; executeCallback(); }; if (! knownToSupportCssOnLoad) { var id = Meteor.setInterval(function () { if (link.sheet) { executeCallback(); Meteor.clearInterval(id); } }, 50); } }; var removeOldLinks = _.after(newCss.length, function () { _.each(oldLinks, function (oldLink) { oldLink.parentNode.removeChild(oldLink); }); }); var attachStylesheetLink = function (newLink) { document.getElementsByTagName("head").item(0).appendChild(newLink); waitUntilCssLoads(newLink, function () { Meteor.setTimeout(removeOldLinks, 200); }); }; if (newCss.length !== 0) { _.each(newCss, function (css) { var newLink = document.createElement("link"); newLink.setAttribute("rel", "stylesheet"); newLink.setAttribute("type", "text/css"); newLink.setAttribute("class", "__meteor-css__"); newLink.setAttribute("href", Meteor._relativeToSiteRootUrl(css.url)); attachStylesheetLink(newLink); }); } else { removeOldLinks(); } } else if (doc._id === 'version' && doc.version !== autoupdateVersion) { handle && handle.stop(); if (Package.reload) { Package.reload.Reload._reload(); } } }; var handle = ClientVersions.find().observe({ added: checkNewVersionDocument, changed: checkNewVersionDocument }); } } }); }; Autoupdate._retrySubscription(); ================================================ FILE: packages/autoupdate/autoupdate_cordova.js ================================================ var autoupdateVersionCordova = __meteor_runtime_config__.autoupdateVersionCordova || "unknown"; // The collection of acceptable client versions. ClientVersions = new Mongo.Collection("meteor_autoupdate_clientVersions"); Autoupdate = {}; Autoupdate.newClientAvailable = function() { return !! ClientVersions.findOne({ _id: 'version-cordova', version: {$ne: autoupdateVersionCordova} }); }; var retry = new Retry({ // Unlike the stream reconnect use of Retry, which we want to be instant // in normal operation, this is a wacky failure. We don't want to retry // right away, we can start slowly. // // A better way than timeconstants here might be to use the knowledge // of when we reconnect to help trigger these retries. Typically, the // server fixing code will result in a restart and reconnect, but // potentially the subscription could have a transient error. minCount: 0, // don't do any immediate retries baseTimeout: 30*1000 // start with 30s }); var failures = 0; Autoupdate._retrySubscription = function() { var appId = __meteor_runtime_config__.appId; Meteor.subscribe("meteor_autoupdate_clientVersions", appId, { onError: function(error) { console.log("autoupdate subscription failed:", error); failures++; retry.retryLater(failures, function() { // Just retry making the subscription, don't reload the whole // page. While reloading would catch more cases (for example, // the server went back a version and is now doing old-style hot // code push), it would also be more prone to reload loops, // which look really bad to the user. Just retrying the // subscription over DDP means it is at least possible to fix by // updating the server. Autoupdate._retrySubscription(); }); }, onReady: function() { if (Package.reload) { var checkNewVersionDocument = function(doc) { var self = this; if (doc.version !== autoupdateVersionCordova) { newVersionAvailable(); } }; var handle = ClientVersions.find({_id: 'version-cordova'}).observe({ added: checkNewVersionDocument, changed: checkNewVersionDocument }); } } }); }; Meteor.startup(function() { WebAppLocalServer.onNewVersionReady(function() { if (Package.reload) { Package.reload.Reload._reload(); } }); Autoupdate._retrySubscription(); }); var newVersionAvailable = function() { WebAppLocalServer.checkForUpdates(); } ================================================ FILE: packages/autoupdate/autoupdate_server.js ================================================ // Publish the current client versions to the client. When a client // sees the subscription change and that there is a new version of the // client available on the server, it can reload. // // By default there are two current client versions. The refreshable client // version is identified by a hash of the client resources seen by the browser // that are refreshable, such as CSS, while the non refreshable client version // is identified by a hash of the rest of the client assets // (the HTML, code, and static files in the `public` directory). // // If the environment variable `AUTOUPDATE_VERSION` is set it will be // used as the client id instead. You can use this to control when // the client reloads. For example, if you want to only force a // reload on major changes, you can use a custom AUTOUPDATE_VERSION // which you only change when something worth pushing to clients // immediately happens. // // The server publishes a `meteor_autoupdate_clientVersions` // collection. There are two documents in this collection, a document // with _id 'version' which represents the non refreshable client assets, // and a document with _id 'version-refreshable' which represents the // refreshable client assets. Each document has a 'version' field // which is equivalent to the hash of the relevant assets. The refreshable // document also contains a list of the refreshable assets, so that the client // can swap in the new assets without forcing a page refresh. Clients can // observe changes on these documents to detect when there is a new // version available. // // In this implementation only two documents are present in the collection // the current refreshable client version and the current nonRefreshable client // version. Developers can easily experiment with different versioning and // updating models by forking this package. var Future = Npm.require("fibers/future"); Autoupdate = {}; // The collection of acceptable client versions. ClientVersions = new Mongo.Collection("meteor_autoupdate_clientVersions", { connection: null }); // The client hash includes __meteor_runtime_config__, so wait until // all packages have loaded and have had a chance to populate the // runtime config before using the client hash as our default auto // update version id. // Note: Tests allow people to override Autoupdate.autoupdateVersion before // startup. Autoupdate.autoupdateVersion = null; Autoupdate.autoupdateVersionRefreshable = null; Autoupdate.autoupdateVersionCordova = null; Autoupdate.appId = __meteor_runtime_config__.appId = process.env.APP_ID; var syncQueue = new Meteor._SynchronousQueue(); // updateVersions can only be called after the server has fully loaded. var updateVersions = function (shouldReloadClientProgram) { // Step 1: load the current client program on the server and update the // hash values in __meteor_runtime_config__. if (shouldReloadClientProgram) { WebAppInternals.reloadClientPrograms(); } // If we just re-read the client program, or if we don't have an autoupdate // version, calculate it. if (shouldReloadClientProgram || Autoupdate.autoupdateVersion === null) { Autoupdate.autoupdateVersion = process.env.AUTOUPDATE_VERSION || WebApp.calculateClientHashNonRefreshable(); } // If we just recalculated it OR if it was set by (eg) test-in-browser, // ensure it ends up in __meteor_runtime_config__. __meteor_runtime_config__.autoupdateVersion = Autoupdate.autoupdateVersion; Autoupdate.autoupdateVersionRefreshable = __meteor_runtime_config__.autoupdateVersionRefreshable = process.env.AUTOUPDATE_VERSION || WebApp.calculateClientHashRefreshable(); Autoupdate.autoupdateVersionCordova = __meteor_runtime_config__.autoupdateVersionCordova = process.env.AUTOUPDATE_VERSION || WebApp.calculateClientHashCordova(); // Step 2: form the new client boilerplate which contains the updated // assets and __meteor_runtime_config__. if (shouldReloadClientProgram) { WebAppInternals.generateBoilerplate(); } // XXX COMPAT WITH 0.8.3 if (! ClientVersions.findOne({current: true})) { // To ensure apps with version of Meteor prior to 0.9.0 (in // which the structure of documents in `ClientVersions` was // different) also reload. ClientVersions.insert({current: true}); } if (! ClientVersions.findOne({_id: "version"})) { ClientVersions.insert({ _id: "version", version: Autoupdate.autoupdateVersion }); } else { ClientVersions.update("version", { $set: { version: Autoupdate.autoupdateVersion }}); } if (! ClientVersions.findOne({_id: "version-cordova"})) { ClientVersions.insert({ _id: "version-cordova", version: Autoupdate.autoupdateVersionCordova, refreshable: false }); } else { ClientVersions.update("version-cordova", { $set: { version: Autoupdate.autoupdateVersionCordova }}); } // Use `onListening` here because we need to use // `WebAppInternals.refreshableAssets`, which is only set after // `WebApp.generateBoilerplate` is called by `main` in webapp. WebApp.onListening(function () { if (! ClientVersions.findOne({_id: "version-refreshable"})) { ClientVersions.insert({ _id: "version-refreshable", version: Autoupdate.autoupdateVersionRefreshable, assets: WebAppInternals.refreshableAssets }); } else { ClientVersions.update("version-refreshable", { $set: { version: Autoupdate.autoupdateVersionRefreshable, assets: WebAppInternals.refreshableAssets }}); } }); }; Meteor.publish( "meteor_autoupdate_clientVersions", function (appId) { // `null` happens when a client doesn't have an appId and passes // `undefined` to `Meteor.subscribe`. `undefined` is translated to // `null` as JSON doesn't have `undefined. check(appId, Match.OneOf(String, undefined, null)); // Don't notify clients using wrong appId such as mobile apps built with a // different server but pointing at the same local url if (Autoupdate.appId && appId && Autoupdate.appId !== appId) return []; return ClientVersions.find(); }, {is_auto: true} ); Meteor.startup(function () { updateVersions(false); }); var fut = new Future(); // We only want 'refresh' to trigger 'updateVersions' AFTER onListen, // so we add a queued task that waits for onListen before 'refresh' can queue // tasks. Note that the `onListening` callbacks do not fire until after // Meteor.startup, so there is no concern that the 'updateVersions' calls from // 'refresh' will overlap with the `updateVersions` call from Meteor.startup. syncQueue.queueTask(function () { fut.wait(); }); WebApp.onListening(function () { fut.return(); }); var enqueueVersionsRefresh = function () { syncQueue.queueTask(function () { updateVersions(true); }); }; // Listen for the special {refresh: 'client'} message, which signals that a // client asset has changed. process.on('message', Meteor.bindEnvironment(function (m) { if (m && m.refresh === 'client') { enqueueVersionsRefresh(); } }, "handling client refresh message")); // Another way to tell the process to refresh: send SIGHUP signal process.on('SIGHUP', Meteor.bindEnvironment(function () { enqueueVersionsRefresh(); }, "handling SIGHUP signal for refresh")); ================================================ FILE: packages/autoupdate/package.js ================================================ Package.describe({ summary: "Update the client when new client code is available", version: '1.2.10' }); Package.onUse(function (api) { api.use([ 'webapp', 'check' ], 'server'); api.use([ 'tracker', 'retry' ], 'client'); api.use([ 'ddp', 'mongo', 'underscore' ], ['client', 'server']); api.use(['http', 'random'], 'web.cordova'); api.addFiles('autoupdate_server.js', 'server'); api.addFiles('autoupdate_client.js', 'web.browser'); api.addFiles('autoupdate_cordova.js', 'web.cordova'); api.export('Autoupdate'); }); ================================================ FILE: packages/babel-compiler/.npm/package/.gitignore ================================================ node_modules ================================================ FILE: packages/babel-compiler/.npm/package/README ================================================ This directory and the files immediately inside it are automatically generated when you change this package's NPM dependencies. Commit the files in this directory (npm-shrinkwrap.json, .gitignore, and this README) to source control so that others run the same versions of sub-dependencies. You should NOT check in the node_modules directory that Meteor automatically creates; if you are using git, the .gitignore file tells git to ignore it. ================================================ FILE: packages/babel-compiler/.npm/package/npm-shrinkwrap.json ================================================ { "dependencies": { "acorn": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.2.0.tgz", "from": "acorn@>=3.2.0 <3.3.0" }, "amdefine": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz", "from": "amdefine@>=0.0.4" }, "ansi-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz", "from": "ansi-regex@>=2.0.0 <3.0.0" }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", "from": "ansi-styles@>=2.2.1 <3.0.0" }, "ast-types": { "version": "0.8.16", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.8.16.tgz", "from": "ast-types@>=0.8.16 <0.9.0" }, "babel-code-frame": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.8.0.tgz", "from": "babel-code-frame@>=6.8.0 <7.0.0" }, "babel-core": { "version": "6.9.1", "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.9.1.tgz", "from": "babel-core@>=6.9.1 <6.10.0" }, "babel-generator": { "version": "6.10.2", "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.10.2.tgz", "from": "babel-generator@>=6.9.0 <7.0.0" }, "babel-helper-builder-react-jsx": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.9.0.tgz", "from": "babel-helper-builder-react-jsx@>=6.8.0 <7.0.0" }, "babel-helper-call-delegate": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.8.0.tgz", "from": "babel-helper-call-delegate@>=6.8.0 <7.0.0" }, "babel-helper-define-map": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.9.0.tgz", "from": "babel-helper-define-map@>=6.9.0 <7.0.0" }, "babel-helper-function-name": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.8.0.tgz", "from": "babel-helper-function-name@>=6.8.0 <7.0.0" }, "babel-helper-get-function-arity": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.8.0.tgz", "from": "babel-helper-get-function-arity@>=6.8.0 <7.0.0" }, "babel-helper-hoist-variables": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.8.0.tgz", "from": "babel-helper-hoist-variables@>=6.8.0 <7.0.0" }, "babel-helper-optimise-call-expression": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.8.0.tgz", "from": "babel-helper-optimise-call-expression@>=6.8.0 <7.0.0" }, "babel-helper-regex": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.9.0.tgz", "from": "babel-helper-regex@>=6.8.0 <7.0.0" }, "babel-helper-replace-supers": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.8.0.tgz", "from": "babel-helper-replace-supers@>=6.8.0 <7.0.0" }, "babel-helpers": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.8.0.tgz", "from": "babel-helpers@>=6.8.0 <7.0.0" }, "babel-messages": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.8.0.tgz", "from": "babel-messages@>=6.8.0 <7.0.0" }, "babel-plugin-check-es2015-constants": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.8.0.tgz", "from": "babel-plugin-check-es2015-constants@>=6.8.0 <7.0.0" }, "babel-plugin-syntax-async-functions": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.8.0.tgz", "from": "babel-plugin-syntax-async-functions@>=6.8.0 <7.0.0" }, "babel-plugin-syntax-async-generators": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.8.0.tgz", "from": "babel-plugin-syntax-async-generators@>=6.8.0 <7.0.0" }, "babel-plugin-syntax-flow": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.8.0.tgz", "from": "babel-plugin-syntax-flow@>=6.8.0 <7.0.0" }, "babel-plugin-syntax-jsx": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.8.0.tgz", "from": "babel-plugin-syntax-jsx@>=6.3.13 <7.0.0" }, "babel-plugin-syntax-object-rest-spread": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.8.0.tgz", "from": "babel-plugin-syntax-object-rest-spread@>=6.8.0 <7.0.0" }, "babel-plugin-syntax-trailing-function-commas": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.8.0.tgz", "from": "babel-plugin-syntax-trailing-function-commas@>=6.8.0 <7.0.0" }, "babel-plugin-transform-es2015-arrow-functions": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.8.0.tgz", "from": "babel-plugin-transform-es2015-arrow-functions@>=6.8.0 <7.0.0" }, "babel-plugin-transform-es2015-block-scoped-functions": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.8.0.tgz", "from": "babel-plugin-transform-es2015-block-scoped-functions@>=6.8.0 <7.0.0" }, "babel-plugin-transform-es2015-block-scoping": { "version": "6.10.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.10.1.tgz", "from": "babel-plugin-transform-es2015-block-scoping@>=6.10.1 <7.0.0" }, "babel-plugin-transform-es2015-classes": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.9.0.tgz", "from": "babel-plugin-transform-es2015-classes@>=6.9.0 <7.0.0" }, "babel-plugin-transform-es2015-computed-properties": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.8.0.tgz", "from": "babel-plugin-transform-es2015-computed-properties@>=6.8.0 <7.0.0" }, "babel-plugin-transform-es2015-destructuring": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.9.0.tgz", "from": "babel-plugin-transform-es2015-destructuring@>=6.9.0 <7.0.0" }, "babel-plugin-transform-es2015-for-of": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.8.0.tgz", "from": "babel-plugin-transform-es2015-for-of@>=6.8.0 <7.0.0" }, "babel-plugin-transform-es2015-function-name": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.9.0.tgz", "from": "babel-plugin-transform-es2015-function-name@>=6.9.0 <7.0.0" }, "babel-plugin-transform-es2015-literals": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.8.0.tgz", "from": "babel-plugin-transform-es2015-literals@>=6.8.0 <7.0.0" }, "babel-plugin-transform-es2015-modules-commonjs": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.8.0.tgz", "from": "babel-plugin-transform-es2015-modules-commonjs@>=6.8.0 <6.9.0" }, "babel-plugin-transform-es2015-object-super": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.8.0.tgz", "from": "babel-plugin-transform-es2015-object-super@>=6.8.0 <7.0.0" }, "babel-plugin-transform-es2015-parameters": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.9.0.tgz", "from": "babel-plugin-transform-es2015-parameters@>=6.9.0 <7.0.0" }, "babel-plugin-transform-es2015-shorthand-properties": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.8.0.tgz", "from": "babel-plugin-transform-es2015-shorthand-properties@>=6.8.0 <7.0.0" }, "babel-plugin-transform-es2015-spread": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.8.0.tgz", "from": "babel-plugin-transform-es2015-spread@>=6.8.0 <7.0.0" }, "babel-plugin-transform-es2015-sticky-regex": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.8.0.tgz", "from": "babel-plugin-transform-es2015-sticky-regex@>=6.8.0 <7.0.0" }, "babel-plugin-transform-es2015-template-literals": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.8.0.tgz", "from": "babel-plugin-transform-es2015-template-literals@>=6.8.0 <7.0.0" }, "babel-plugin-transform-es2015-typeof-symbol": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.8.0.tgz", "from": "babel-plugin-transform-es2015-typeof-symbol@>=6.8.0 <7.0.0" }, "babel-plugin-transform-es2015-unicode-regex": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.8.0.tgz", "from": "babel-plugin-transform-es2015-unicode-regex@>=6.8.0 <7.0.0" }, "babel-plugin-transform-es3-member-expression-literals": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es3-member-expression-literals/-/babel-plugin-transform-es3-member-expression-literals-6.8.0.tgz", "from": "babel-plugin-transform-es3-member-expression-literals@>=6.8.0 <7.0.0" }, "babel-plugin-transform-es3-property-literals": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es3-property-literals/-/babel-plugin-transform-es3-property-literals-6.8.0.tgz", "from": "babel-plugin-transform-es3-property-literals@>=6.8.0 <7.0.0" }, "babel-plugin-transform-flow-strip-types": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.8.0.tgz", "from": "babel-plugin-transform-flow-strip-types@>=6.8.0 <7.0.0" }, "babel-plugin-transform-object-rest-spread": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.8.0.tgz", "from": "babel-plugin-transform-object-rest-spread@>=6.8.0 <7.0.0" }, "babel-plugin-transform-react-display-name": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.8.0.tgz", "from": "babel-plugin-transform-react-display-name@>=6.3.13 <7.0.0" }, "babel-plugin-transform-react-jsx": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.8.0.tgz", "from": "babel-plugin-transform-react-jsx@>=6.3.13 <7.0.0" }, "babel-plugin-transform-react-jsx-source": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.9.0.tgz", "from": "babel-plugin-transform-react-jsx-source@>=6.3.13 <7.0.0" }, "babel-plugin-transform-regenerator": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.9.0.tgz", "from": "babel-plugin-transform-regenerator@>=6.9.0 <7.0.0" }, "babel-plugin-transform-runtime": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.9.0.tgz", "from": "babel-plugin-transform-runtime@>=6.9.0 <6.10.0" }, "babel-plugin-transform-strict-mode": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.8.0.tgz", "from": "babel-plugin-transform-strict-mode@>=6.8.0 <7.0.0" }, "babel-preset-meteor": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/babel-preset-meteor/-/babel-preset-meteor-6.11.0.tgz", "from": "babel-preset-meteor@>=6.11.0 <6.12.0" }, "babel-preset-react": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.5.0.tgz", "from": "babel-preset-react@>=6.5.0 <6.6.0" }, "babel-register": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.9.0.tgz", "from": "babel-register@>=6.9.0 <7.0.0" }, "babel-runtime": { "version": "6.9.2", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.9.2.tgz", "from": "babel-runtime@>=6.9.2 <6.10.0" }, "babel-template": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.9.0.tgz", "from": "babel-template@>=6.9.0 <6.10.0" }, "babel-traverse": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.9.0.tgz", "from": "babel-traverse@>=6.9.0 <6.10.0" }, "babel-types": { "version": "6.10.2", "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.10.2.tgz", "from": "babel-types@>=6.10.0 <6.11.0" }, "babylon": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.8.1.tgz", "from": "babylon@>=6.8.1 <6.9.0" }, "balanced-match": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz", "from": "balanced-match@>=0.4.1 <0.5.0" }, "brace-expansion": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.5.tgz", "from": "brace-expansion@>=1.0.0 <2.0.0" }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "from": "chalk@>=1.1.0 <2.0.0" }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "from": "concat-map@0.0.1" }, "convert-source-map": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.2.0.tgz", "from": "convert-source-map@>=1.2.0 <1.3.0" }, "core-js": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.0.tgz", "from": "core-js@>=2.4.0 <3.0.0" }, "debug": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", "from": "debug@>=2.1.1 <3.0.0" }, "detect-indent": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-3.0.1.tgz", "from": "detect-indent@>=3.0.1 <4.0.0" }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "from": "escape-string-regexp@>=1.0.2 <2.0.0" }, "esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", "from": "esutils@>=2.0.2 <3.0.0" }, "get-stdin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", "from": "get-stdin@>=4.0.1 <5.0.0" }, "globals": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/globals/-/globals-8.18.0.tgz", "from": "globals@>=8.3.0 <9.0.0" }, "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "from": "has-ansi@>=2.0.0 <3.0.0" }, "home-or-tmp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-1.0.0.tgz", "from": "home-or-tmp@>=1.0.0 <2.0.0" }, "invariant": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.1.tgz", "from": "invariant@>=2.2.0 <3.0.0" }, "is-finite": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.1.tgz", "from": "is-finite@>=1.0.0 <2.0.0" }, "js-tokens": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-1.0.3.tgz", "from": "js-tokens@>=1.0.2 <2.0.0" }, "jsesc": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "from": "jsesc@>=0.5.0 <0.6.0" }, "json5": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json5/-/json5-0.4.0.tgz", "from": "json5@>=0.4.0 <0.5.0" }, "lodash": { "version": "4.13.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.13.1.tgz", "from": "lodash@>=4.13.1 <4.14.0" }, "loose-envify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.2.0.tgz", "from": "loose-envify@>=1.0.0 <2.0.0" }, "magic-string": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.15.1.tgz", "from": "magic-string@>=0.15.0 <0.16.0" }, "meteor-babel": { "version": "0.11.6", "resolved": "https://registry.npmjs.org/meteor-babel/-/meteor-babel-0.11.6.tgz", "from": "meteor-babel@0.11.6" }, "meteor-babel-helpers": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/meteor-babel-helpers/-/meteor-babel-helpers-0.0.3.tgz", "from": "meteor-babel-helpers@0.0.3" }, "minimatch": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", "from": "minimatch@>=2.0.3 <3.0.0" }, "minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "from": "minimist@>=1.1.0 <2.0.0" }, "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "from": "mkdirp@>=0.5.1 <0.6.0", "dependencies": { "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "from": "minimist@0.0.8" } } }, "ms": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", "from": "ms@0.7.1" }, "number-is-nan": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.0.tgz", "from": "number-is-nan@>=1.0.0 <2.0.0" }, "os-tmpdir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.1.tgz", "from": "os-tmpdir@>=1.0.1 <2.0.0" }, "path-exists": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-1.0.0.tgz", "from": "path-exists@>=1.0.0 <2.0.0" }, "path-is-absolute": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz", "from": "path-is-absolute@>=1.0.0 <2.0.0" }, "private": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/private/-/private-0.1.6.tgz", "from": "private@>=0.1.6 <0.2.0" }, "regenerate": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.1.tgz", "from": "regenerate@>=1.2.1 <2.0.0" }, "regenerator-runtime": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.9.5.tgz", "from": "regenerator-runtime@>=0.9.5 <0.10.0" }, "regexpu-core": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", "from": "regexpu-core@>=1.0.0 <2.0.0" }, "regjsgen": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", "from": "regjsgen@>=0.2.0 <0.3.0" }, "regjsparser": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", "from": "regjsparser@>=0.1.4 <0.2.0" }, "reify": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/reify/-/reify-0.3.4.tgz", "from": "reify@>=0.3.4 <0.4.0" }, "repeating": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/repeating/-/repeating-1.1.3.tgz", "from": "repeating@>=1.1.0 <2.0.0" }, "shebang-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", "from": "shebang-regex@>=1.0.0 <2.0.0" }, "slash": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", "from": "slash@>=1.0.0 <2.0.0" }, "source-map": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", "from": "source-map@>=0.5.0 <0.6.0" }, "source-map-support": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.2.10.tgz", "from": "source-map-support@>=0.2.10 <0.3.0", "dependencies": { "source-map": { "version": "0.1.32", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.32.tgz", "from": "source-map@0.1.32" } } }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "from": "strip-ansi@>=3.0.0 <4.0.0" }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "from": "supports-color@>=2.0.0 <3.0.0" }, "to-fast-properties": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.2.tgz", "from": "to-fast-properties@>=1.0.1 <2.0.0" }, "user-home": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", "from": "user-home@>=1.1.1 <2.0.0" }, "vlq": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.1.tgz", "from": "vlq@>=0.2.1 <0.3.0" } } } ================================================ FILE: packages/babel-compiler/README.md ================================================ [Babel](http://babeljs.io/) is a parser and transpiler for ECMAScript 2015 syntax and beyond, which enables some upcoming JavaScript syntax features to be used in today's browsers and runtimes. Meteor's Babel support consists of the following core packages: * `babel-compiler` - Exposes the [Babel API](https://babeljs.io/docs/usage/api/) on the symbol `Babel`. For example, `Babel.compile(source, options)`. * `babel-runtime` - Meteor versions of the external helpers used by Babel-generated code. Meteor's core packages must run on IE 8 without polyfills, so these helpers cannot assume the existence of `Object.defineProperty`, `Object.freeze`, and so on. ### Babel API The `babel-compiler` package exports the `Babel` symbol, which exposes functionality provided by the [`meteor-babel`](https://www.npmjs.com/package/meteor-babel) NPM package, which is in turn implmented using the [`babel-core`](https://www.npmjs.com/package/babel-core) NPM package. Note that you can only use the `babel-compiler` package on the server. Example: ```js var babelOptions = Babel.getDefaultOptions(); // Modify the default options, if necessary: babelOptions.whitelist = [ "es6.blockScoping", // For `let` "es6.arrowFunctions" // For `=>` ]; var result = Babel.compile( "let square = (x) => x*x;", babelOptions ); // result.code will be something like // "var square = function (x) {\n return x * x;\n};" ``` Use `Babel.compile(source)` to transpile code using a set of default options that work well for Meteor code. ### `.babelrc` configuration files Like other Babel-compiled projects, a Meteor project that uses the `ecmascript` package can specify custom Babel plugins and presets (which are just groups of plugins) in JSON files named `.babelrc`. For example, to enable the Babel [transform](https://www.npmjs.com/package/babel-plugin-transform-class-properties) that supports [class properties](https://github.com/jeffmo/es-class-fields-and-static-properties), you should 1. run `meteor npm install --save-dev babel-plugin-transform-class-properties` 2. put the following in a `.babelrc` file in the root of your project: ```js { "plugins": ["transform-class-properties"] } ``` If you want to include all Stage 1 transforms (including the class properties plugin), you could use a preset instead: ```sh meteor npm install --save-dev babel-preset-stage-1 ``` and then (in your `.babelrc` file): ```js { "presets": ["stage-1"] } ``` Note that you should never need to include the `es2015` or `react` transforms, as that functionality is already provided by the default `babel-preset-meteor` preset. Any plugins and transforms that you list in your `.babelrc` file will be included after `babel-preset-meteor`. To be considered by the `babel-compiler` package, `.babelrc` files must be contained within your root application directory. ### Resources: * [API docs](https://babeljs.io/docs/usage/api/) * [List of transformers](https://babeljs.io/docs/usage/transformers/) ================================================ FILE: packages/babel-compiler/babel-compiler.js ================================================ /** * A compiler that can be instantiated with features and used inside * Plugin.registerCompiler * @param {Object} extraFeatures The same object that getDefaultOptions takes */ BabelCompiler = function BabelCompiler(extraFeatures) { this.extraFeatures = extraFeatures; this._babelrcCache = null; this._babelrcWarnings = Object.create(null); }; var BCp = BabelCompiler.prototype; var excludedFileExtensionPattern = /\.es5\.js$/i; var hasOwn = Object.prototype.hasOwnProperty; BCp.processFilesForTarget = function (inputFiles) { // Reset this cache for each batch processed. this._babelrcCache = null; inputFiles.forEach(function (inputFile) { var toBeAdded = this.processOneFileForTarget(inputFile); if (toBeAdded) { inputFile.addJavaScript(toBeAdded); } }, this); }; // Returns an object suitable for passing to inputFile.addJavaScript, or // null to indicate there was an error, and nothing should be added. BCp.processOneFileForTarget = function (inputFile, source) { this._babelrcCache = this._babelrcCache || Object.create(null); if (typeof source !== "string") { // Other compiler plugins can call processOneFileForTarget with a // source string that's different from inputFile.getContentsAsString() // if they've already done some processing. source = inputFile.getContentsAsString(); } var packageName = inputFile.getPackageName(); var inputFilePath = inputFile.getPathInPackage(); var outputFilePath = inputFilePath; var fileOptions = inputFile.getFileOptions(); var toBeAdded = { sourcePath: inputFilePath, path: outputFilePath, data: source, hash: inputFile.getSourceHash(), sourceMap: null, bare: !! fileOptions.bare }; var cacheDeps = { sourceHash: toBeAdded.hash }; // If you need to exclude a specific file within a package from Babel // compilation, pass the { transpile: false } options to api.addFiles // when you add that file. if (fileOptions.transpile !== false && // Bare files should not be transpiled by Babel, because they do not // have access to CommonJS APIs like `require`, `module`, `exports`. ! toBeAdded.bare && // If you need to exclude a specific file within an app from Babel // compilation, give it the following file extension: .es5.js ! excludedFileExtensionPattern.test(inputFilePath)) { var targetCouldBeInternetExplorer8 = inputFile.getArch() === "web.browser"; var extraFeatures = Object.assign({}, this.extraFeatures); if (! extraFeatures.hasOwnProperty("jscript")) { // Perform some additional transformations to improve compatibility // in older browsers (e.g. wrapping named function expressions, per // http://kiro.me/blog/nfe_dilemma.html). extraFeatures.jscript = targetCouldBeInternetExplorer8; } if (inputFile.isPackageFile()) { // When compiling package files, handle import/export syntax using // the official Babel plugin, so that package authors won't publish // code that relies on module.import and module.export, because such // code would fail on Meteor versions before 1.3.3. When compiling // application files, however, it's fine to rely on module.import // and module.export, and the developer experience will be much // better for it: faster compilation, real variables, import // statements inside conditional statements, etc. // // TODO Remove this once we are confident enough developers have // updated to a version of Meteor that supports module.import and // module.export. extraFeatures.legacyModules = true; } var babelOptions = Babel.getDefaultOptions(extraFeatures); this.inferExtraBabelOptions(inputFile, babelOptions, cacheDeps); babelOptions.sourceMap = true; babelOptions.filename = babelOptions.sourceFileName = packageName ? "/packages/" + packageName + "/" + inputFilePath : "/" + inputFilePath; babelOptions.sourceMapTarget = babelOptions.filename + ".map"; try { var result = profile('Babel.compile', function () { return Babel.compile(source, babelOptions, cacheDeps); }); } catch (e) { if (e.loc) { inputFile.error({ message: e.message, line: e.loc.line, column: e.loc.column, }); return null; } throw e; } toBeAdded.data = result.code; toBeAdded.hash = result.hash; toBeAdded.sourceMap = result.map; } return toBeAdded; }; BCp.setDiskCacheDirectory = function (cacheDir) { Babel.setCacheDir(cacheDir); }; function profile(name, func) { if (typeof Profile !== 'undefined') { return Profile.time(name, func); } else { return func(); } }; BCp.inferExtraBabelOptions = function (inputFile, babelOptions, cacheDeps) { if (! inputFile.require || ! inputFile.findControlFile || ! inputFile.readAndWatchFile) { return false; } return ( // If a .babelrc exists, it takes precedence over package.json. this._inferFromBabelRc(inputFile, babelOptions, cacheDeps) || this._inferFromPackageJson(inputFile, babelOptions, cacheDeps) ); }; BCp._inferFromBabelRc = function (inputFile, babelOptions, cacheDeps) { var babelrcPath = inputFile.findControlFile(".babelrc"); if (babelrcPath) { if (! hasOwn.call(this._babelrcCache, babelrcPath)) { this._babelrcCache[babelrcPath] = JSON.parse(inputFile.readAndWatchFile(babelrcPath)); } return this._inferHelper( inputFile, babelOptions, babelrcPath, this._babelrcCache[babelrcPath], cacheDeps ); } }; BCp._inferFromPackageJson = function (inputFile, babelOptions, cacheDeps) { var pkgJsonPath = inputFile.findControlFile("package.json"); if (pkgJsonPath) { if (! hasOwn.call(this._babelrcCache, pkgJsonPath)) { this._babelrcCache[pkgJsonPath] = JSON.parse( inputFile.readAndWatchFile(pkgJsonPath) ).babel || null; } return this._inferHelper( inputFile, babelOptions, pkgJsonPath, this._babelrcCache[pkgJsonPath], cacheDeps ); } }; BCp._inferHelper = function ( inputFile, babelOptions, controlFilePath, babelrc, cacheDeps ) { if (! babelrc) { return false; } var compiler = this; var inferredPresets = []; var result; function infer(listName, prefix) { var list = babelrc[listName]; if (! Array.isArray(list) || list.length === 0) { return; } function req(id) { try { return reqMightThrow(id); } catch (e) { if (e.code !== "MODULE_NOT_FOUND") { throw e; } if (! hasOwn.call(compiler._babelrcWarnings, id)) { compiler._babelrcWarnings[id] = controlFilePath; console.error( "Warning: unable to resolve " + JSON.stringify(id) + " in " + listName + " of " + controlFilePath ); } return null; } } function reqMightThrow(id) { var isTopLevel = "./".indexOf(id.charAt(0)) < 0; var presetOrPlugin; var presetOrPluginMeta; if (isTopLevel) { try { // If the identifier is top-level, try to prefix it with // "babel-plugin-" or "babel-preset-". presetOrPlugin = inputFile.require(prefix + id); presetOrPluginMeta = inputFile.require( packageNameFromTopLevelModuleId(prefix + id) + '/package.json'); } catch (e) { if (e.code !== "MODULE_NOT_FOUND") { throw e; } // Fall back to requiring the plugin as-is if the prefix failed. presetOrPlugin = inputFile.require(id); presetOrPluginMeta = inputFile.require( packageNameFromTopLevelModuleId(id) + '/package.json'); } } else { // If the identifier is not top-level, but relative or absolute, // then it will be required as-is, so that you can implement your // own Babel plugins locally, rather than always using plugins // installed from npm. presetOrPlugin = inputFile.require(id, controlFilePath); // Note that inputFile.readAndWatchFileWithHash converts module // identifers to OS-specific paths if necessary. var absId = inputFile.resolve(id, controlFilePath); var info = inputFile.readAndWatchFileWithHash(absId); presetOrPluginMeta = { name: absId, version: info.hash }; } return { name: presetOrPluginMeta.name, version: presetOrPluginMeta.version, module: presetOrPlugin.__esModule ? presetOrPlugin.default : presetOrPlugin }; } var filtered = []; list.forEach(function (item, i) { if (typeof item === "string") { result = req(item); if (! result) return; item = result.module; cacheDeps[result.name] = result.version; } else if (Array.isArray(item) && typeof item[0] === "string") { item = item.slice(); // defensive copy result = req(item[0]); if (! result) return; item[0] = result.module; cacheDeps[result.name] = result.version; } // else, an object { presets: [], plugins: [] } from meteorBabel, whose // version is used for the cache hash internally. filtered.push(item); }); if (listName === "plugins") { // Turn any additional plugins into their own preset, so that they // can come before babel-preset-meteor. inferredPresets.push({ plugins: filtered }); } else if (listName === "presets") { inferredPresets.push.apply(inferredPresets, filtered); } } infer("presets", "babel-preset-"); infer("plugins", "babel-plugin-"); if (inferredPresets.length > 0) { babelOptions.presets.push.apply( babelOptions.presets, inferredPresets ); return true; } return false; }; // 'react-hot-loader/babel' => 'react-hot-loader' function packageNameFromTopLevelModuleId(id) { return id.split("/", 1)[0]; } ================================================ FILE: packages/babel-compiler/babel.js ================================================ var meteorBabel = Npm.require('meteor-babel'); /** * Returns a new object containing default options appropriate for */ function getDefaultOptions(extraFeatures) { // See https://github.com/meteor/babel/blob/master/options.js for more // information about what the default options are. var options = meteorBabel.getDefaultOptions(extraFeatures); // The sourceMap option should probably be removed from the default // options returned by meteorBabel.getDefaultOptions. delete options.sourceMap; return options; } Babel = { getDefaultOptions: getDefaultOptions, // Deprecated, now a no-op. validateExtraFeatures: Function.prototype, compile: function (source, options) { options = options || getDefaultOptions(); return meteorBabel.compile(source, options); }, setCacheDir: function (cacheDir) { meteorBabel.setCacheDir(cacheDir); } }; ================================================ FILE: packages/babel-compiler/package.js ================================================ Package.describe({ name: "babel-compiler", summary: "Parser/transpiler for ECMAScript 2015+ syntax", // Tracks the npm version below. Use wrap numbers to increment // without incrementing the npm version. Hmm-- Apparently this // isn't possible because you can't publish a non-recommended // release with package versions that don't have a pre-release // identifier at the end (eg, -dev) version: '6.8.3' }); Npm.depends({ 'meteor-babel': '0.11.6', }); Package.onUse(function (api) { api.use('ecmascript-runtime'); api.addFiles([ 'babel.js', 'babel-compiler.js' ], 'server'); api.export('Babel', 'server'); api.export('BabelCompiler', 'server'); }); ================================================ FILE: packages/babel-runtime/.npm/package/.gitignore ================================================ node_modules ================================================ FILE: packages/babel-runtime/.npm/package/README ================================================ This directory and the files immediately inside it are automatically generated when you change this package's NPM dependencies. Commit the files in this directory (npm-shrinkwrap.json, .gitignore, and this README) to source control so that others run the same versions of sub-dependencies. You should NOT check in the node_modules directory that Meteor automatically creates; if you are using git, the .gitignore file tells git to ignore it. ================================================ FILE: packages/babel-runtime/.npm/package/npm-shrinkwrap.json ================================================ { "dependencies": { "meteor-babel-helpers": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/meteor-babel-helpers/-/meteor-babel-helpers-0.0.3.tgz", "from": "https://registry.npmjs.org/meteor-babel-helpers/-/meteor-babel-helpers-0.0.3.tgz" }, "regenerator-runtime": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.9.5.tgz", "from": "regenerator-runtime@0.9.5" } } } ================================================ FILE: packages/babel-runtime/README.md ================================================ # babel-runtime Meteor maintains a version of the runtime helpers needed by Babel-transpiled code. In most cases, the code is copied from Babel's helper implementations, though we have also made some changes. Benefits of maintaining our own package include: * IE 8 support. Babel's helpers target IE 9 and do not work in IE 8, but generally IE 8 support can be achieved with only minor changes. * Backwards-compatibility. When the Babel compiler changes, the helpers sometimes change. Our Babel package can keep old helpers for back-compat. (If we change over to publishing original ES6 code in packages instead of transpiled code, this becomes less important.) * Client-side code size. We've made the helpers file as small as possible. ## Helpers Helpers needed for each transform **as of [Babel v5.6.15](https://github.com/babel/babel/tree/a1a46882fddc596a47e0e29017c5440ab6d7d9c0/src/babel/transformation/transformers)**: * es3.propertyLiterals: None * es3.memberExpressionLiterals: None * es6.arrowFunctions: None * es6.templateLiterals * `taggedTemplateLiteralLoose` * es6.classes * `inherits` * `classCallCheck` * `createClass` (only for getter/setters) * Excluded because only for decorator support(2): `createDecoratedClass`, `defineDecoratedPropertyDescriptor` * es6.constants: None * es6.blockScoping: None * Excluded because only for spec mode(1): `temporalUndefined`, `temporalAssertDefined` * es6.properties.shorthand: None * es6.properties.computed: None * Excluded because only for non-loose mode(1): `defineProperty` * es6.parameters: None * es6.spread * `bind` (for `new A(...b)`) * es6.forOf: None * es7.objectRestSpread * `_extends` * Everything in es6.destructuring * es6.destructuring * `objectWithoutProperties` * `objectDestructuringEmpty` * es7.trailingFunctionCommas: None * flow: None Footnotes: 1. A transform can be run in "loose," normal, or "spec" mode, with "loose" providing the fastest, most lightweight, and usually most browser-compatible transpilation, while "spec" mode tries extra hard to be spec-compliant at the expense of those things. We've found that "loose" mode is the best mode for production code for every transform we've looked at. 2. Decorators are still a Stage 1 proposal and are only implemented in Babel as an experiment. ================================================ FILE: packages/babel-runtime/babel-runtime.js ================================================ var hasOwn = Object.prototype.hasOwnProperty; var S = typeof Symbol === "function" ? Symbol : {}; var iteratorSymbol = S.iterator || "@@iterator"; meteorBabelHelpers = require("meteor-babel-helpers"); var BabelRuntime = { // es6.templateLiterals // Constructs the object passed to the tag function in a tagged // template literal. taggedTemplateLiteralLoose: function (strings, raw) { // Babel's own version of this calls Object.freeze on `strings` and // `strings.raw`, but it doesn't seem worth the compatibility and // performance concerns. If you're writing code against this helper, // don't add properties to these objects. strings.raw = raw; return strings; }, // es6.classes // Checks that a class constructor is being called with `new`, and throws // an error if it is not. classCallCheck: function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }, // es6.classes inherits: function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } if (superClass) { if (Object.create) { // All but IE 8 subClass.prototype = Object.create(superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); } else { // IE 8 path. Slightly worse for modern browsers, because `constructor` // is enumerable and shows up in the inspector unnecessarily. // It's not an "own" property of any instance though. // // For correctness when writing code, // don't enumerate all the own-and-inherited properties of an instance // of a class and expect not to find `constructor` (but who does that?). var F = function () { this.constructor = subClass; }; F.prototype = superClass.prototype; subClass.prototype = new F(); } // For modern browsers, this would be `subClass.__proto__ = superClass`, // but IE <=10 don't support `__proto__`, and in this case the difference // would be detectable; code that works in modern browsers could easily // fail on IE 8 if we ever used the `__proto__` trick. // // There's no perfect way to make static methods inherited if they are // assigned after declaration of the classes. The best we can do is // to copy them. In other words, when you write `class Foo // extends Bar`, we copy the static methods from Bar onto Foo, but future // ones are not copied. // // For correctness when writing code, don't add static methods to a class // after you subclass it. // The ecmascript-runtime package provides adequate polyfills for // all of these Object.* functions (and Array#forEach), and anyone // using babel-runtime is almost certainly using it because of the // ecmascript package, which also implies ecmascript-runtime. Object.getOwnPropertyNames(superClass).forEach(function (k) { // This property descriptor dance preserves getter/setter behavior // in browsers that support accessor properties (all except // IE8). In IE8, the superClass can't have accessor properties // anyway, so this code is still safe. var descriptor = Object.getOwnPropertyDescriptor(superClass, k); if (descriptor && typeof descriptor === "object") { if (Object.getOwnPropertyDescriptor(subClass, k)) { // If subClass already has a property by this name, then it // would not be inherited, so it should not be copied. This // notably excludes properties like .prototype and .name. return; } Object.defineProperty(subClass, k, descriptor); } }); } }, createClass: (function () { var hasDefineProperty = false; try { // IE 8 has a broken Object.defineProperty, so feature-test by // trying to call it. Object.defineProperty({}, 'x', {}); hasDefineProperty = true; } catch (e) {} function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (! hasDefineProperty) { // e.g. `class Foo { get bar() {} }`. If you try to use getters and // setters in IE 8, you will get a big nasty error, with or without // Babel. I don't know of any other syntax features besides getters // and setters that will trigger this error. throw new Error( "Your browser does not support this type of class property. " + "For example, Internet Explorer 8 does not support getters and " + "setters."); } if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(), "typeof": function (obj) { return obj && obj.constructor === Symbol ? "symbol" : typeof obj; }, possibleConstructorReturn: function (self, call) { if (! self) { throw new ReferenceError( "this hasn't been initialised - super() hasn't been called" ); } var callType = typeof call; if (call && callType === "function" || callType === "object") { return call; } return self; }, interopRequireDefault: function (obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }, interopRequireWildcard: function (obj) { if (obj && obj.__esModule) { return obj; } var newObj = {}; if (obj != null) { for (var key in obj) { if (hasOwn.call(obj, key)) { newObj[key] = obj[key]; } } } newObj["default"] = obj; return newObj; }, interopExportWildcard: function (obj, defaults) { var newObj = defaults({}, obj); delete newObj["default"]; return newObj; }, defaults: function (obj, defaults) { Object.getOwnPropertyNames(defaults).forEach(function (key) { var desc = Object.getOwnPropertyDescriptor(defaults, key); if (desc && desc.configurable && typeof obj[key] === "undefined") { Object.defineProperty(obj, key, desc); } }); return obj; }, // es7.objectRestSpread and react (JSX) "extends": Object.assign || (function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (hasOwn.call(source, key)) { target[key] = source[key]; } } } return target; }), // es6.destructuring objectWithoutProperties: function (obj, keys) { var target = {}; outer: for (var i in obj) { if (! hasOwn.call(obj, i)) continue; for (var j = 0; j < keys.length; j++) { if (keys[j] === i) continue outer; } target[i] = obj[i]; } return target; }, // es6.destructuring objectDestructuringEmpty: function (obj) { if (obj == null) throw new TypeError("Cannot destructure undefined"); }, // es6.spread bind: Function.prototype.bind || (function () { var isCallable = function (value) { return typeof value === 'function'; }; var $Object = Object; var to_string = Object.prototype.toString; var array_slice = Array.prototype.slice; var array_concat = Array.prototype.concat; var array_push = Array.prototype.push; var max = Math.max; var Empty = function Empty() {}; // Copied from es5-shim.js (3ac7942). See original for more comments. return function bind(that) { var target = this; if (!isCallable(target)) { throw new TypeError('Function.prototype.bind called on incompatible ' + target); } var args = array_slice.call(arguments, 1); var bound; var binder = function () { if (this instanceof bound) { var result = target.apply( this, array_concat.call(args, array_slice.call(arguments)) ); if ($Object(result) === result) { return result; } return this; } else { return target.apply( that, array_concat.call(args, array_slice.call(arguments)) ); } }; var boundLength = max(0, target.length - args.length); var boundArgs = []; for (var i = 0; i < boundLength; i++) { array_push.call(boundArgs, '$' + i); } // Create a Function from source code so that it has the right `.length`. // Probably not important for Babel. This code violates CSPs that ban // `eval`, but the browsers that need this polyfill don't have CSP! bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this, arguments); }')(binder); if (target.prototype) { Empty.prototype = target.prototype; bound.prototype = new Empty(); Empty.prototype = null; } return bound; }; })(), toConsumableArray: function (arr) { if (Array.isArray(arr)) { for (var i = arr.length - 1, arr2 = Array(i + 1); i >= 0; --i) { arr2[i] = arr[i]; } return arr2; } return Array.from(arr); }, toArray: function (arr) { return Array.isArray(arr) ? arr : Array.from(arr); }, slicedToArray: function (iterable, limit) { if (Array.isArray(iterable)) { return iterable; } if (iterable) { var it = iterable[iteratorSymbol](); var result = []; var info; if (typeof limit !== "number") { limit = Infinity; } while (result.length < limit && ! (info = it.next()).done) { result.push(info.value); } return result; } throw new TypeError( "Invalid attempt to destructure non-iterable instance" ); }, slice: Array.prototype.slice }; // Use meteorInstall to install all of the above helper functions within // node_modules/babel-runtime/helpers. Object.keys(BabelRuntime).forEach(function (helperName) { var helpers = {}; helpers[helperName + ".js"] = function (require, exports, module) { module.exports = BabelRuntime[helperName]; }; meteorInstall({ node_modules: { "babel-runtime": { helpers: helpers } } }); }); // Use meteorInstall to install the regenerator runtime at // node_modules/babel-runtime/regenerator. meteorInstall({ node_modules: { "babel-runtime": { "regenerator.js": function (r, e, module) { // Note that we use the require function provided to the // babel-runtime.js file, not the one named 'r' above. var runtime = require("regenerator-runtime"); // If Promise.asyncApply is defined, use it to wrap calls to // runtime.async so that the entire async function will run in its // own Fiber, not just the code that comes after the first await. if (typeof Promise === "function" && typeof Promise.asyncApply === "function") { var realAsync = runtime.async; runtime.async = function () { return Promise.asyncApply(realAsync, runtime, arguments); }; } module.exports = runtime; } } } }); ================================================ FILE: packages/babel-runtime/package.js ================================================ Package.describe({ name: "babel-runtime", summary: "Runtime support for output of Babel transpiler", version: '0.1.9_1', documentation: 'README.md' }); Npm.depends({ "regenerator-runtime": "0.9.5", "meteor-babel-helpers": "0.0.3" }); Package.onUse(function (api) { // If the es5-shim package is installed, make sure it loads before // babel-runtime, since babel-runtime uses some ES5 APIs like // Object.defineProperties that are buggy in older browsers. api.use("es5-shim", { weak: true }); api.use("modules"); api.use("promise"); // Needed by Regenerator. api.addFiles("babel-runtime.js"); api.export("meteorBabelHelpers"); }); ================================================ FILE: packages/base64/.gitignore ================================================ .build* ================================================ FILE: packages/base64/README.md ================================================ This is an internal Meteor package. ================================================ FILE: packages/base64/base64.js ================================================ // Base 64 encoding var BASE_64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; var BASE_64_VALS = {}; for (var i = 0; i < BASE_64_CHARS.length; i++) { BASE_64_VALS[BASE_64_CHARS.charAt(i)] = i; }; Base64 = {}; Base64.encode = function (array) { if (typeof array === "string") { var str = array; array = Base64.newBinary(str.length); for (var i = 0; i < str.length; i++) { var ch = str.charCodeAt(i); if (ch > 0xFF) { throw new Error( "Not ascii. Base64.encode can only take ascii strings."); } array[i] = ch; } } var answer = []; var a = null; var b = null; var c = null; var d = null; for (var i = 0; i < array.length; i++) { switch (i % 3) { case 0: a = (array[i] >> 2) & 0x3F; b = (array[i] & 0x03) << 4; break; case 1: b = b | (array[i] >> 4) & 0xF; c = (array[i] & 0xF) << 2; break; case 2: c = c | (array[i] >> 6) & 0x03; d = array[i] & 0x3F; answer.push(getChar(a)); answer.push(getChar(b)); answer.push(getChar(c)); answer.push(getChar(d)); a = null; b = null; c = null; d = null; break; } } if (a != null) { answer.push(getChar(a)); answer.push(getChar(b)); if (c == null) answer.push('='); else answer.push(getChar(c)); if (d == null) answer.push('='); } return answer.join(""); }; var getChar = function (val) { return BASE_64_CHARS.charAt(val); }; var getVal = function (ch) { if (ch === '=') { return -1; } return BASE_64_VALS[ch]; }; // XXX This is a weird place for this to live, but it's used both by // this package and 'ejson', and we can't put it in 'ejson' without // introducing a circular dependency. It should probably be in its own // package or as a helper in a package that both 'base64' and 'ejson' // use. Base64.newBinary = function (len) { if (typeof Uint8Array === 'undefined' || typeof ArrayBuffer === 'undefined') { var ret = []; for (var i = 0; i < len; i++) { ret.push(0); } ret.$Uint8ArrayPolyfill = true; return ret; } return new Uint8Array(new ArrayBuffer(len)); }; Base64.decode = function (str) { var len = Math.floor((str.length*3)/4); if (str.charAt(str.length - 1) == '=') { len--; if (str.charAt(str.length - 2) == '=') len--; } var arr = Base64.newBinary(len); var one = null; var two = null; var three = null; var j = 0; for (var i = 0; i < str.length; i++) { var c = str.charAt(i); var v = getVal(c); switch (i % 4) { case 0: if (v < 0) throw new Error('invalid base64 string'); one = v << 2; break; case 1: if (v < 0) throw new Error('invalid base64 string'); one = one | (v >> 4); arr[j++] = one; two = (v & 0x0F) << 4; break; case 2: if (v >= 0) { two = two | (v >> 2); arr[j++] = two; three = (v & 0x03) << 6; } break; case 3: if (v >= 0) { arr[j++] = three | v; } break; } } return arr; }; ================================================ FILE: packages/base64/base64_test.js ================================================ var asciiToArray = function (str) { var arr = Base64.newBinary(str.length); for (var i = 0; i < str.length; i++) { var c = str.charCodeAt(i); if (c > 0xFF) { throw new Error("Not ascii"); } arr[i] = c; } return arr; }; var arrayToAscii = function (arr) { var res = []; for (var i = 0; i < arr.length; i++) { res.push(String.fromCharCode(arr[i])); } return res.join(""); }; Tinytest.add("base64 - testing the test", function (test) { test.equal(arrayToAscii(asciiToArray("The quick brown fox jumps over the lazy dog")), "The quick brown fox jumps over the lazy dog"); }); Tinytest.add("base64 - empty", function (test) { test.equal(Base64.encode(EJSON.newBinary(0)), ""); test.equal(Base64.decode(""), EJSON.newBinary(0)); }); Tinytest.add("base64 - wikipedia examples", function (test) { var tests = [ {txt: "pleasure.", res: "cGxlYXN1cmUu"}, {txt: "leasure.", res: "bGVhc3VyZS4="}, {txt: "easure.", res: "ZWFzdXJlLg=="}, {txt: "asure.", res: "YXN1cmUu"}, {txt: "sure.", res: "c3VyZS4="} ]; _.each(tests, function(t) { test.equal(Base64.encode(asciiToArray(t.txt)), t.res); test.equal(arrayToAscii(Base64.decode(t.res)), t.txt); }); }); Tinytest.add("base64 - non-text examples", function (test) { var tests = [ {array: [0, 0, 0], b64: "AAAA"}, {array: [0, 0, 1], b64: "AAAB"} ]; _.each(tests, function(t) { test.equal(Base64.encode(t.array), t.b64); var expectedAsBinary = EJSON.newBinary(t.array.length); _.each(t.array, function (val, i) { expectedAsBinary[i] = val; }); test.equal(Base64.decode(t.b64), expectedAsBinary); }); }); ================================================ FILE: packages/base64/package.js ================================================ Package.describe({ summary: "Base64 encoding and decoding", version: '1.0.9' }); Package.onUse(function (api) { api.export('Base64'); api.addFiles('base64.js', ['client', 'server']); }); Package.onTest(function (api) { api.use('base64', ['client', 'server']); api.use(['tinytest', 'underscore', 'ejson']); api.addFiles('base64_test.js', ['client', 'server']); }); ================================================ FILE: packages/binary-heap/.gitignore ================================================ .build* ================================================ FILE: packages/binary-heap/README.md ================================================ This is an internal Meteor package. ================================================ FILE: packages/binary-heap/binary-heap-tests.js ================================================ Tinytest.add("binary-heap - simple max-heap tests", function (test) { var h = new MaxHeap(function (a, b) { return a-b; }); h.set("a", 1); h.set("b", 233); h.set("c", -122); h.set("d", 0); h.set("e", 0); test.equal(h.size(), 5); test.equal(h.maxElementId(), "b"); test.equal(h.get("b"), 233); h.remove("b"); test.equal(h.size(), 4); test.equal(h.maxElementId(), "a"); h.set("e", 44); test.equal(h.maxElementId(), "e"); test.equal(h.get("b"), null); test.isTrue(h.has("a")); test.isFalse(h.has("dd")); h.clear(); test.isFalse(h.has("a")); test.equal(h.size(), 0); test.equal(h.setDefault("a", 12345), 12345); test.equal(h.setDefault("a", 55555), 12345); test.equal(h.size(), 1); test.equal(h.maxElementId(), "a"); }); Tinytest.add("binary-heap - big test for max-heap", function (test) { var positiveNumbers = _.shuffle(_.range(1, 41)); var negativeNumbers = _.shuffle(_.range(-1, -41, -1)); var allNumbers = negativeNumbers.concat(positiveNumbers); var heap = new MaxHeap(function (a, b) { return a-b; }); var output = []; _.each(allNumbers, function (n) { heap.set(n, n); }); _.times(positiveNumbers.length + negativeNumbers.length, function () { var maxId = heap.maxElementId(); output.push(heap.get(maxId)); heap.remove(maxId); }); allNumbers.sort(function (a, b) { return b-a; }); test.equal(output, allNumbers); }); Tinytest.add("binary-heap - min-max heap tests", function (test) { var h = new MinMaxHeap(function (a, b) { return a-b; }); h.set("a", 1); h.set("b", 233); h.set("c", -122); h.set("d", 0); h.set("e", 0); test.equal(h.size(), 5); test.equal(h.maxElementId(), "b"); test.equal(h.get("b"), 233); test.equal(h.minElementId(), "c"); h.remove("b"); test.equal(h.size(), 4); test.equal(h.minElementId(), "c"); h.set("e", -123); test.equal(h.minElementId(), "e"); test.equal(h.get("b"), null); test.isTrue(h.has("a")); test.isFalse(h.has("dd")); h.clear(); test.isFalse(h.has("a")); test.equal(h.size(), 0); test.equal(h.setDefault("a", 12345), 12345); test.equal(h.setDefault("a", 55555), 12345); test.equal(h.size(), 1); test.equal(h.maxElementId(), "a"); test.equal(h.minElementId(), "a"); }); Tinytest.add("binary-heap - big test for min-max-heap", function (test) { var N = 500; var positiveNumbers = _.shuffle(_.range(1, N + 1)); var negativeNumbers = _.shuffle(_.range(-1, -N - 1, -1)); var allNumbers = positiveNumbers.concat(negativeNumbers); var heap = new MinMaxHeap(function (a, b) { return a-b; }); var output = []; var initialSets = _.clone(allNumbers); _.each(allNumbers, function (n) { heap.set(n, n); heap._selfCheck(); heap._minHeap._selfCheck(); }); allNumbers = _.shuffle(allNumbers); var secondarySets = _.clone(allNumbers); _.each(allNumbers, function (n) { heap.set(-n, n); heap._selfCheck(); heap._minHeap._selfCheck(); }); _.times(positiveNumbers.length + negativeNumbers.length, function () { var minId = heap.minElementId(); output.push(heap.get(minId)); heap.remove(minId); heap._selfCheck(); heap._minHeap._selfCheck(); }); test.equal(heap.size(), 0); allNumbers.sort(function (a, b) { return a-b; }); var initialTestText = "initial sets: " + initialSets.toString() + "; secondary sets: " + secondarySets.toString(); test.equal(output, allNumbers, initialTestText); _.each(initialSets, function (n) { heap.set(n, n); }) _.each(secondarySets, function (n) { heap.set(-n, n); }); allNumbers.sort(function (a, b) { return b-a; }); output = []; _.times(positiveNumbers.length + negativeNumbers.length, function () { var maxId = heap.maxElementId(); output.push(heap.get(maxId)); heap.remove(maxId); heap._selfCheck(); heap._minHeap._selfCheck(); }); test.equal(output, allNumbers, initialTestText); }); ================================================ FILE: packages/binary-heap/max-heap.js ================================================ // Constructor of Heap // - comparator - Function - given two items returns a number // - options: // - initData - Array - Optional - the initial data in a format: // Object: // - id - String - unique id of the item // - value - Any - the data value // each value is retained // - IdMap - Constructor - Optional - custom IdMap class to store id->index // mappings internally. Standard IdMap is used by default. MaxHeap = function (comparator, options) { if (! _.isFunction(comparator)) throw new Error('Passed comparator is invalid, should be a comparison function'); var self = this; // a C-style comparator that is given two values and returns a number, // negative if the first value is less than the second, positive if the second // value is greater than the first and zero if they are equal. self._comparator = comparator; options = _.defaults(options || {}, { IdMap: IdMap }); // _heapIdx maps an id to an index in the Heap array the corresponding value // is located on. self._heapIdx = new options.IdMap; // The Heap data-structure implemented as a 0-based contiguous array where // every item on index idx is a node in a complete binary tree. Every node can // have children on indexes idx*2+1 and idx*2+2, except for the leaves. Every // node has a parent on index (idx-1)/2; self._heap = []; // If the initial array is passed, we can build the heap in linear time // complexity (O(N)) compared to linearithmic time complexity (O(nlogn)) if // we push elements one by one. if (_.isArray(options.initData)) self._initFromData(options.initData); }; _.extend(MaxHeap.prototype, { // Builds a new heap in-place in linear time based on passed data _initFromData: function (data) { var self = this; self._heap = _.map(data, function (o) { return { id: o.id, value: o.value }; }); _.each(data, function (o, i) { self._heapIdx.set(o.id, i); }); if (! data.length) return; // start from the first non-leaf - the parent of the last leaf for (var i = parentIdx(data.length - 1); i >= 0; i--) self._downHeap(i); }, _downHeap: function (idx) { var self = this; while (leftChildIdx(idx) < self.size()) { var left = leftChildIdx(idx); var right = rightChildIdx(idx); var largest = idx; if (left < self.size()) { largest = self._maxIndex(largest, left); } if (right < self.size()) { largest = self._maxIndex(largest, right); } if (largest === idx) break; self._swap(largest, idx); idx = largest; } }, _upHeap: function (idx) { var self = this; while (idx > 0) { var parent = parentIdx(idx); if (self._maxIndex(parent, idx) === idx) { self._swap(parent, idx) idx = parent; } else { break; } } }, _maxIndex: function (idxA, idxB) { var self = this; var valueA = self._get(idxA); var valueB = self._get(idxB); return self._comparator(valueA, valueB) >= 0 ? idxA : idxB; }, // Internal: gets raw data object placed on idxth place in heap _get: function (idx) { var self = this; return self._heap[idx].value; }, _swap: function (idxA, idxB) { var self = this; var recA = self._heap[idxA]; var recB = self._heap[idxB]; self._heapIdx.set(recA.id, idxB); self._heapIdx.set(recB.id, idxA); self._heap[idxA] = recB; self._heap[idxB] = recA; }, get: function (id) { var self = this; if (! self.has(id)) return null; return self._get(self._heapIdx.get(id)); }, set: function (id, value) { var self = this; if (self.has(id)) { if (self.get(id) === value) return; var idx = self._heapIdx.get(id); self._heap[idx].value = value; // Fix the new value's position // Either bubble new value up if it is greater than its parent self._upHeap(idx); // or bubble it down if it is smaller than one of its children self._downHeap(idx); } else { self._heapIdx.set(id, self._heap.length); self._heap.push({ id: id, value: value }); self._upHeap(self._heap.length - 1); } }, remove: function (id) { var self = this; if (self.has(id)) { var last = self._heap.length - 1; var idx = self._heapIdx.get(id); if (idx !== last) { self._swap(idx, last); self._heap.pop(); self._heapIdx.remove(id); // Fix the swapped value's position self._upHeap(idx); self._downHeap(idx); } else { self._heap.pop(); self._heapIdx.remove(id); } } }, has: function (id) { var self = this; return self._heapIdx.has(id); }, empty: function () { var self = this; return !self.size(); }, clear: function () { var self = this; self._heap = []; self._heapIdx.clear(); }, // iterate over values in no particular order forEach: function (iterator) { var self = this; _.each(self._heap, function (obj) { return iterator(obj.value, obj.id); }); }, size: function () { var self = this; return self._heap.length; }, setDefault: function (id, def) { var self = this; if (self.has(id)) return self.get(id); self.set(id, def); return def; }, clone: function () { var self = this; var clone = new MaxHeap(self._comparator, self._heap); return clone; }, maxElementId: function () { var self = this; return self.size() ? self._heap[0].id : null; }, _selfCheck: function () { var self = this; for (var i = 1; i < self._heap.length; i++) if (self._maxIndex(parentIdx(i), i) !== parentIdx(i)) throw new Error("An item with id " + self._heap[i].id + " has a parent younger than it: " + self._heap[parentIdx(i)].id); } }); function leftChildIdx (i) { return i * 2 + 1; } function rightChildIdx (i) { return i * 2 + 2; } function parentIdx (i) { return (i - 1) >> 1; } ================================================ FILE: packages/binary-heap/min-heap.js ================================================ MinHeap = function (comparator, options) { var self = this; MaxHeap.call(self, function (a, b) { return -comparator(a, b); }, options); }; Meteor._inherits(MinHeap, MaxHeap); _.extend(MinHeap.prototype, { maxElementId: function () { throw new Error("Cannot call maxElementId on MinHeap"); }, minElementId: function () { var self = this; return MaxHeap.prototype.maxElementId.call(self); } }); ================================================ FILE: packages/binary-heap/min-max-heap.js ================================================ // This implementation of Min/Max-Heap is just a subclass of Max-Heap // with a Min-Heap as an encapsulated property. // // Most of the operations are just proxy methods to call the same method on both // heaps. // // This implementation takes 2*N memory but is fairly simple to write and // understand. And the constant factor of a simple Heap is usually smaller // compared to other two-way priority queues like Min/Max Heaps // (http://www.cs.otago.ac.nz/staffpriv/mike/Papers/MinMaxHeaps/MinMaxHeaps.pdf) // and Interval Heaps // (http://www.cise.ufl.edu/~sahni/dsaac/enrich/c13/double.htm) MinMaxHeap = function (comparator, options) { var self = this; MaxHeap.call(self, comparator, options); self._minHeap = new MinHeap(comparator, options); }; Meteor._inherits(MinMaxHeap, MaxHeap); _.extend(MinMaxHeap.prototype, { set: function (id, value) { var self = this; MaxHeap.prototype.set.apply(self, arguments); self._minHeap.set(id, value); }, remove: function (id) { var self = this; MaxHeap.prototype.remove.apply(self, arguments); self._minHeap.remove(id); }, clear: function () { var self = this; MaxHeap.prototype.clear.apply(self, arguments); self._minHeap.clear(); }, setDefault: function (id, def) { var self = this; MaxHeap.prototype.setDefault.apply(self, arguments); return self._minHeap.setDefault(id, def); }, clone: function () { var self = this; var clone = new MinMaxHeap(self._comparator, self._heap); return clone; }, minElementId: function () { var self = this; return self._minHeap.minElementId(); } }); ================================================ FILE: packages/binary-heap/package.js ================================================ Package.describe({ summary: "Binary Heap datastructure implementation", version: '1.0.9' }); Package.onUse(function (api) { api.export('MaxHeap'); api.export('MinHeap'); api.export('MinMaxHeap'); api.use(['underscore', 'id-map']); api.addFiles(['max-heap.js', 'min-heap.js', 'min-max-heap.js']); }); Package.onTest(function (api) { api.use([ 'tinytest', 'underscore', 'binary-heap' ]); api.addFiles('binary-heap-tests.js'); }); ================================================ FILE: packages/blaze/.gitignore ================================================ .build* ================================================ FILE: packages/blaze/README.md ================================================ # Blaze Blaze is a powerful library for creating user interfaces by writing reactive HTML templates. Compared to using a combination of traditional templates and jQuery, Blaze eliminates the need for all the "update logic" in your app that listens for data changes and manipulates the DOM. Instead, familiar template directives like `{{#if}}` and `{{#each}}` integrate with [Tracker's](https://meteor.com/tracker) "transparent reactivity" and [Minimongo's](https://meteor.com/mini-databases) database cursors so that the DOM updates automatically. Read more on the Blaze [project page](http://www.meteor.com/blaze). ## Details Blaze has two major parts: * A template compiler that compiles template files into JavaScript code that runs against the Blaze runtime library. Moreover, Blaze provides a compiler toolchain (think LLVM) that can be used to support arbitrary template syntaxes. The flagship template syntax is Spacebars, a variant of Handlebars, but a community alternative based on Jade is already in use by many apps. * A reactive DOM engine that builds and manages the DOM at runtime, invoked via templates or directly from the app, which features reactively updating regions, lists, and attributes; event delegation; and many callbacks and hooks to aid the app developer. Blaze is sometimes compared to frameworks like React, Angular, Ember, Polymer, Knockout, and others by virtue of its advanced templating system. What sets Blaze apart is a relentless focus on the developer experience, using templating, transparent reactivity, and interoperability with existing libraries to create a gentle learning curve while enabling you to build world-class apps. ## Examples Here are two Spacebars templates from an example app called "Leaderboard" which displays a sorted list of top players and their scores: ```html ``` The template tags `{{name}}` and `{{score}}` refer to properties of the data context (the current player), while `players` and `selected` refer to helper functions. Helper functions and event handlers are defined in JavaScript: ```javascript Template.leaderboard.helpers({ players: function () { // Perform a reactive database query against minimongo return Players.find({}, { sort: { score: -1, name: 1 } }); } }); Template.player.events({ 'click': function () { // click on a player to select it Session.set("selectedPlayer", this._id); } }); Template.player.helpers({ selected: function () { return Session.equals("selectedPlayer", this._id) ? "selected" : ''; } }); ``` No additional UI code is necessary to ensure that the list of players stays up-to-date, or that the "selected" class is added and removed from the LI elements as appropriate when the user clicks on a player. Thanks to a powerful template language, it doesn't take much ceremony to write a loop, include another template, or bind an attribute (or part of an attribute). And thanks to Tracker's transparent reactivity, there's no ceremony around depending on reactive data sources like the database or Session; it just happens when you read the value, and when the value changes, the DOM will be updated in a fine-grained way. # Principles ## Gentle Learning Curve To get started with Blaze, you don't have to learn a lot of concepts or terminology. As web developers, we are already students of HTML, CSS, and JavaScript, which are complex technologies described in thick books. Blaze lets you apply your existing knowledge in exciting new ways without having to read another book first. Many factors go into making Blaze easy to pick up and use, including the other principles below. In general, we prefer APIs that lead to simple and obvious-looking application code, and we recognize that developers have limited time and energy to learn new and unfamiliar terms and syntaxes. It may sound obvious to "keep it simple" and prioritize the developer experience when creating a system for reactive HTML, but it's also challenging, and we think it's not done often enough! We use feedback from the Meteor community to ensure that Blaze's features stay simple, understandable, and useful. ## Transparent Reactivity Under the hood, Blaze uses the [Tracker](https://meteor.com/tracker) library to automatically keep track of when to recalculate each template helper. If a helper reads a value from the client-side database, for example, the helper will automatically be recalculated when the value changes. What this means for the developer is simple. You don't have to explicitly declare when to update the DOM, or even perform any explicit "data-binding." You don't have to know how Tracker works, or even exactly what "reactivity" means, to benefit. The result is less thinking and less typing than other approaches. ## Clean Templates Blaze embraces popular template syntaxes such as Handlebars and Jade which are clean, readable, and familiar to developers coming from other frameworks. A good template language should clearly distinguish the special "template directives" (often enclosed in curly braces) from the HTML, and it should not obscure the structure of the resulting HTML. These properties make templating an easy concept to learn after static HTML (or alongside it), and make templates easy to read, easy to style with CSS, and easy to relate to the DOM. In contrast, some newer frameworks try to remake templates as just HTML (Angular, Polymer) or replace them with just JavaScript (React). These approaches tend to obscure either the structure of the template, or what is a real DOM element and what is not, or both. In addition, since templates are generally precompiled anyway as a best practice, it's really not important that raw template source code be browser-parsable. Meanwhile, the developer experience of reading, writing, and maintaining templates is hugely important. ## Plugin Interoperability Web developers often share snippets of HTML, JavaScript, and CSS, or publish them as libraries, widgets, or jQuery plugins. They want to embed videos, maps, and other third-party content. Blaze doesn't assume it owns the whole DOM, and it tries to make as few assumptions as possible about the DOM outside of its updates. It hooks into jQuery's clean-up routines to prevent memory leaks, and it preserves classes, attributes, and styles added to elements by jQuery or any third-party library. While it's certainly possible for Blaze and jQuery to step on each other's toes if you aren't careful, there are established patterns for keeping the peace, and Meteor users rightfully expect to be able to use the various widgets and enhancements cooked up by the broader web community in their apps. # Comparisons to other libraries Compared to Backbone and other libraries that simply re-render templates, Blaze does much less re-rendering and doesn't suffer from the dreaded "nested view" problem, which is when two templates can't be updated independently of each other because one is nested inside the other. In addition, Blaze automatically determines when re-rendering must occur, using Tracker. Compared to Ember, Blaze offers finer-grained, automatic DOM updates. Because Blaze uses Tracker's transparent reactivity, you don't have to perform explicit "data-binding" to get data into your template, or declare the data dependencies of each template helper. Compared to Angular and Polymer, Blaze has a gentler learning curve, simpler concepts, and nicer template syntax that cleanly separates template directives and HTML. Also, Blaze is targeted at today's browsers and not designed around a hypothetical "browser of the future." Compared to React, Blaze emphasizes HTML templates rather than JavaScript component classes. Templates are more approachable than JavaScript code and easier to read, write, and style with CSS. Instead of using Tracker, React relies on a combination of explicit "setState" calls and data-model diffing in order to achieve efficient rendering. # Future Work ### Components Blaze will get better patterns for creating reusable UI components. Templates already serve as reusable components, to a point. Improvements will focus on: * Argument-handling * Local reactive state * "Methods" that are callable from other components and have side effects, versus the current "helpers" which are called from the template language and are "pure" * Scoping and the lookup chain * Inheritance and configuration ### Forms Most applications have a lot of forms, where input fields and other widgets are used to enter data, which must then be validated and turned into database changes. Server-side frameworks like Rails and Django have well-honed patterns for this, but client-side frameworks are typically more lacking, perhaps because they are more estranged from the database. Meteor developers have already found ways and built packages to deal with forms and validation, but we think there's a great opportunity to make this part of the core, out-of-the-box Meteor experience. ### Mobile and Animation Blaze will cater to the needs of the mobile web, including enhanced performance and patterns for touch and other mobile interaction. We'll also improve the ease with which developers can integrate animated transitions into their apps. ### JavaScript Expressions in Templates We plan to support JavaScript expressions in templates. This will make templates more expressive, and it will further shorten application code by eliminating the need for a certain class of one-line helpers. The usual argument against allowing JavaScript expressions in a template language is one of "separation of concerns" -- separating business logic from presentation, so that the business logic may be better organized, maintained, and tested independently. Meanwhile, even "logicless" template languages often include some concessions in the form of microsyntax for filtering, querying, and transforming data before using it. This special syntax (and its extension mechanisms) must then be learned. While keeping business logic out of templates is indeed good policy, there is a large class of "presentation logic" that is not really separable from the concerns of templates and HTML, such as the code to calculate styles and classes to apply to HTML elements or to massage data records into a better form for templating purposes. In many cases where this code is short, it may be more convenient or more readable to embed the code in the template, and it's certainly better than evolving the template syntax in a direction that diverges from JavaScript. Because templates are already precompiled to JavaScript code, there is nothing fundamentally difficult or inelegant about allowing a large subset of JavaScript to be used within templates (see e.g. the project Ractive.js). ### Other Template Enhancements Source maps for debugging templates. Imagine seeing your template code in the browser's debugger! Pretty slick. True lexical scoping. Better support for pluggable template syntax (e.g. Jade-like templates). There is already a Jade package in use, but we should learn from it and clarify the abstraction boundary that authors of template syntaxes are programming against. ### Pluggable Backends (don't require jQuery) While Blaze currently requires jQuery, it is architected to run against other "DOM backends" using a common adaptor interface. You should be able to use Zepto, or some very small shim if browser compatibility is not a big deal for your application for some reason. At the moment, no such adaptors besides the jQuery one have been written. The Blaze team experimented with dropping jQuery and talking directly to "modern browsers," but it turns out there is about 5-10K of code at the heart of jQuery that you can't throw out even if you don't care about old browsers or supporting jQuery's app-facing API, which is required just to bring browsers up to the modest expectations of web developers. ### Better Stand-alone Support Blaze will get better support for using it outside of Meteor, such as regular stand-alone builds. # Resources * [Templates API](http://docs.meteor.com/#templates_api) * [Blaze API](http://docs.meteor.com/#blaze) * [Spacebars syntax](https://github.com/meteor/meteor/blob/devel/packages/spacebars/README.md) # Packages * blaze * blaze-tools * html-tools * htmljs * spacebars * spacebars-compiler ================================================ FILE: packages/blaze/attrs.js ================================================ var jsUrlsAllowed = false; Blaze._allowJavascriptUrls = function () { jsUrlsAllowed = true; }; Blaze._javascriptUrlsAllowed = function () { return jsUrlsAllowed; }; // An AttributeHandler object is responsible for updating a particular attribute // of a particular element. AttributeHandler subclasses implement // browser-specific logic for dealing with particular attributes across // different browsers. // // To define a new type of AttributeHandler, use // `var FooHandler = AttributeHandler.extend({ update: function ... })` // where the `update` function takes arguments `(element, oldValue, value)`. // The `element` argument is always the same between calls to `update` on // the same instance. `oldValue` and `value` are each either `null` or // a Unicode string of the type that might be passed to the value argument // of `setAttribute` (i.e. not an HTML string with character references). // When an AttributeHandler is installed, an initial call to `update` is // always made with `oldValue = null`. The `update` method can access // `this.name` if the AttributeHandler class is a generic one that applies // to multiple attribute names. // // AttributeHandlers can store custom properties on `this`, as long as they // don't use the names `element`, `name`, `value`, and `oldValue`. // // AttributeHandlers can't influence how attributes appear in rendered HTML, // only how they are updated after materialization as DOM. AttributeHandler = function (name, value) { this.name = name; this.value = value; }; Blaze._AttributeHandler = AttributeHandler; AttributeHandler.prototype.update = function (element, oldValue, value) { if (value === null) { if (oldValue !== null) element.removeAttribute(this.name); } else { element.setAttribute(this.name, value); } }; AttributeHandler.extend = function (options) { var curType = this; var subType = function AttributeHandlerSubtype(/*arguments*/) { AttributeHandler.apply(this, arguments); }; subType.prototype = new curType; subType.extend = curType.extend; if (options) _.extend(subType.prototype, options); return subType; }; /// Apply the diff between the attributes of "oldValue" and "value" to "element." // // Each subclass must implement a parseValue method which takes a string // as an input and returns a dict of attributes. The keys of the dict // are unique identifiers (ie. css properties in the case of styles), and the // values are the entire attribute which will be injected into the element. // // Extended below to support classes, SVG elements and styles. var DiffingAttributeHandler = AttributeHandler.extend({ update: function (element, oldValue, value) { if (!this.getCurrentValue || !this.setValue || !this.parseValue) throw new Error("Missing methods in subclass of 'DiffingAttributeHandler'"); var oldAttrsMap = oldValue ? this.parseValue(oldValue) : {}; var newAttrsMap = value ? this.parseValue(value) : {}; // the current attributes on the element, which we will mutate. var attrString = this.getCurrentValue(element); var attrsMap = attrString ? this.parseValue(attrString) : {}; _.each(_.keys(oldAttrsMap), function (t) { if (! (t in newAttrsMap)) delete attrsMap[t]; }); _.each(_.keys(newAttrsMap), function (t) { attrsMap[t] = newAttrsMap[t]; }); this.setValue(element, _.values(attrsMap).join(' ')); } }); var ClassHandler = DiffingAttributeHandler.extend({ // @param rawValue {String} getCurrentValue: function (element) { return element.className; }, setValue: function (element, className) { element.className = className; }, parseValue: function (attrString) { var tokens = {}; _.each(attrString.split(' '), function(token) { if (token) tokens[token] = token; }); return tokens; } }); var SVGClassHandler = ClassHandler.extend({ getCurrentValue: function (element) { return element.className.baseVal; }, setValue: function (element, className) { element.setAttribute('class', className); } }); var StyleHandler = DiffingAttributeHandler.extend({ getCurrentValue: function (element) { return element.getAttribute('style'); }, setValue: function (element, style) { if (style === '') { element.removeAttribute('style'); } else { element.setAttribute('style', style); } }, // Parse a string to produce a map from property to attribute string. // // Example: // "color:red; foo:12px" produces a token {color: "color:red", foo:"foo:12px"} parseValue: function (attrString) { var tokens = {}; // Regex for parsing a css attribute declaration, taken from css-parse: // https://github.com/reworkcss/css-parse/blob/7cef3658d0bba872cde05a85339034b187cb3397/index.js#L219 var regex = /(\*?[-#\/\*\\\w]+(?:\[[0-9a-z_-]+\])?)\s*:\s*(?:\'(?:\\\'|.)*?\'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+[;\s]*/g; var match = regex.exec(attrString); while (match) { // match[0] = entire matching string // match[1] = css property // Prefix the token to prevent conflicts with existing properties. // XXX No `String.trim` on Safari 4. Swap out $.trim if we want to // remove strong dep on jquery. tokens[' ' + match[1]] = match[0].trim ? match[0].trim() : $.trim(match[0]); match = regex.exec(attrString); } return tokens; } }); var BooleanHandler = AttributeHandler.extend({ update: function (element, oldValue, value) { var name = this.name; if (value == null) { if (oldValue != null) element[name] = false; } else { element[name] = true; } } }); var DOMPropertyHandler = AttributeHandler.extend({ update: function (element, oldValue, value) { var name = this.name; if (value !== element[name]) element[name] = value; } }); // attributes of the type 'xlink:something' should be set using // the correct namespace in order to work var XlinkHandler = AttributeHandler.extend({ update: function(element, oldValue, value) { var NS = 'http://www.w3.org/1999/xlink'; if (value === null) { if (oldValue !== null) element.removeAttributeNS(NS, this.name); } else { element.setAttributeNS(NS, this.name, this.value); } } }); // cross-browser version of `instanceof SVGElement` var isSVGElement = function (elem) { return 'ownerSVGElement' in elem; }; var isUrlAttribute = function (tagName, attrName) { // Compiled from http://www.w3.org/TR/REC-html40/index/attributes.html // and // http://www.w3.org/html/wg/drafts/html/master/index.html#attributes-1 var urlAttrs = { FORM: ['action'], BODY: ['background'], BLOCKQUOTE: ['cite'], Q: ['cite'], DEL: ['cite'], INS: ['cite'], OBJECT: ['classid', 'codebase', 'data', 'usemap'], APPLET: ['codebase'], A: ['href'], AREA: ['href'], LINK: ['href'], BASE: ['href'], IMG: ['longdesc', 'src', 'usemap'], FRAME: ['longdesc', 'src'], IFRAME: ['longdesc', 'src'], HEAD: ['profile'], SCRIPT: ['src'], INPUT: ['src', 'usemap', 'formaction'], BUTTON: ['formaction'], BASE: ['href'], MENUITEM: ['icon'], HTML: ['manifest'], VIDEO: ['poster'] }; if (attrName === 'itemid') { return true; } var urlAttrNames = urlAttrs[tagName] || []; return _.contains(urlAttrNames, attrName); }; // To get the protocol for a URL, we let the browser normalize it for // us, by setting it as the href for an anchor tag and then reading out // the 'protocol' property. if (Meteor.isClient) { var anchorForNormalization = document.createElement('A'); } var getUrlProtocol = function (url) { if (Meteor.isClient) { anchorForNormalization.href = url; return (anchorForNormalization.protocol || "").toLowerCase(); } else { throw new Error('getUrlProtocol not implemented on the server'); } }; // UrlHandler is an attribute handler for all HTML attributes that take // URL values. It disallows javascript: URLs, unless // Blaze._allowJavascriptUrls() has been called. To detect javascript: // urls, we set the attribute on a dummy anchor element and then read // out the 'protocol' property of the attribute. var origUpdate = AttributeHandler.prototype.update; var UrlHandler = AttributeHandler.extend({ update: function (element, oldValue, value) { var self = this; var args = arguments; if (Blaze._javascriptUrlsAllowed()) { origUpdate.apply(self, args); } else { var isJavascriptProtocol = (getUrlProtocol(value) === "javascript:"); var isVBScriptProtocol = (getUrlProtocol(value) === "vbscript:"); if (isJavascriptProtocol || isVBScriptProtocol) { Blaze._warn("URLs that use the 'javascript:' or 'vbscript:' protocol are not " + "allowed in URL attribute values. " + "Call Blaze._allowJavascriptUrls() " + "to enable them."); origUpdate.apply(self, [element, oldValue, null]); } else { origUpdate.apply(self, args); } } } }); // XXX make it possible for users to register attribute handlers! makeAttributeHandler = function (elem, name, value) { // generally, use setAttribute but certain attributes need to be set // by directly setting a JavaScript property on the DOM element. if (name === 'class') { if (isSVGElement(elem)) { return new SVGClassHandler(name, value); } else { return new ClassHandler(name, value); } } else if (name === 'style') { return new StyleHandler(name, value); } else if ((elem.tagName === 'OPTION' && name === 'selected') || (elem.tagName === 'INPUT' && name === 'checked')) { return new BooleanHandler(name, value); } else if ((elem.tagName === 'TEXTAREA' || elem.tagName === 'INPUT') && name === 'value') { // internally, TEXTAREAs tracks their value in the 'value' // attribute just like INPUTs. return new DOMPropertyHandler(name, value); } else if (name.substring(0,6) === 'xlink:') { return new XlinkHandler(name.substring(6), value); } else if (isUrlAttribute(elem.tagName, name)) { return new UrlHandler(name, value); } else { return new AttributeHandler(name, value); } // XXX will need one for 'style' on IE, though modern browsers // seem to handle setAttribute ok. }; ElementAttributesUpdater = function (elem) { this.elem = elem; this.handlers = {}; }; // Update attributes on `elem` to the dictionary `attrs`, whose // values are strings. ElementAttributesUpdater.prototype.update = function(newAttrs) { var elem = this.elem; var handlers = this.handlers; for (var k in handlers) { if (! _.has(newAttrs, k)) { // remove attributes (and handlers) for attribute names // that don't exist as keys of `newAttrs` and so won't // be visited when traversing it. (Attributes that // exist in the `newAttrs` object but are `null` // are handled later.) var handler = handlers[k]; var oldValue = handler.value; handler.value = null; handler.update(elem, oldValue, null); delete handlers[k]; } } for (var k in newAttrs) { var handler = null; var oldValue; var value = newAttrs[k]; if (! _.has(handlers, k)) { if (value !== null) { // make new handler handler = makeAttributeHandler(elem, k, value); handlers[k] = handler; oldValue = null; } } else { handler = handlers[k]; oldValue = handler.value; } if (oldValue !== value) { handler.value = value; handler.update(elem, oldValue, value); if (value === null) delete handlers[k]; } } }; ================================================ FILE: packages/blaze/backcompat.js ================================================ UI = Blaze; Blaze.ReactiveVar = ReactiveVar; UI._templateInstance = Blaze.Template.instance; Handlebars = {}; Handlebars.registerHelper = Blaze.registerHelper; Handlebars._escape = Blaze._escape; // Return these from {{...}} helpers to achieve the same as returning // strings from {{{...}}} helpers Handlebars.SafeString = function(string) { this.string = string; }; Handlebars.SafeString.prototype.toString = function() { return this.string.toString(); }; ================================================ FILE: packages/blaze/builtins.js ================================================ Blaze._calculateCondition = function (cond) { if (cond instanceof Array && cond.length === 0) cond = false; return !! cond; }; /** * @summary Constructs a View that renders content with a data context. * @locus Client * @param {Object|Function} data An object to use as the data context, or a function returning such an object. If a function is provided, it will be reactively re-run. * @param {Function} contentFunc A Function that returns [*renderable content*](#renderable_content). */ Blaze.With = function (data, contentFunc) { var view = Blaze.View('with', contentFunc); view.dataVar = new ReactiveVar; view.onViewCreated(function () { if (typeof data === 'function') { // `data` is a reactive function view.autorun(function () { view.dataVar.set(data()); }, view.parentView, 'setData'); } else { view.dataVar.set(data); } }); return view; }; /** * Attaches bindings to the instantiated view. * @param {Object} bindings A dictionary of bindings, each binding name * corresponds to a value or a function that will be reactively re-run. * @param {View} view The target. */ Blaze._attachBindingsToView = function (bindings, view) { view.onViewCreated(function () { _.each(bindings, function (binding, name) { view._scopeBindings[name] = new ReactiveVar; if (typeof binding === 'function') { view.autorun(function () { view._scopeBindings[name].set(binding()); }, view.parentView); } else { view._scopeBindings[name].set(binding); } }); }); }; /** * @summary Constructs a View setting the local lexical scope in the block. * @param {Function} bindings Dictionary mapping names of bindings to * values or computations to reactively re-run. * @param {Function} contentFunc A Function that returns [*renderable content*](#renderable_content). */ Blaze.Let = function (bindings, contentFunc) { var view = Blaze.View('let', contentFunc); Blaze._attachBindingsToView(bindings, view); return view; }; /** * @summary Constructs a View that renders content conditionally. * @locus Client * @param {Function} conditionFunc A function to reactively re-run. Whether the result is truthy or falsy determines whether `contentFunc` or `elseFunc` is shown. An empty array is considered falsy. * @param {Function} contentFunc A Function that returns [*renderable content*](#renderable_content). * @param {Function} [elseFunc] Optional. A Function that returns [*renderable content*](#renderable_content). If no `elseFunc` is supplied, no content is shown in the "else" case. */ Blaze.If = function (conditionFunc, contentFunc, elseFunc, _not) { var conditionVar = new ReactiveVar; var view = Blaze.View(_not ? 'unless' : 'if', function () { return conditionVar.get() ? contentFunc() : (elseFunc ? elseFunc() : null); }); view.__conditionVar = conditionVar; view.onViewCreated(function () { this.autorun(function () { var cond = Blaze._calculateCondition(conditionFunc()); conditionVar.set(_not ? (! cond) : cond); }, this.parentView, 'condition'); }); return view; }; /** * @summary An inverted [`Blaze.If`](#blaze_if). * @locus Client * @param {Function} conditionFunc A function to reactively re-run. If the result is falsy, `contentFunc` is shown, otherwise `elseFunc` is shown. An empty array is considered falsy. * @param {Function} contentFunc A Function that returns [*renderable content*](#renderable_content). * @param {Function} [elseFunc] Optional. A Function that returns [*renderable content*](#renderable_content). If no `elseFunc` is supplied, no content is shown in the "else" case. */ Blaze.Unless = function (conditionFunc, contentFunc, elseFunc) { return Blaze.If(conditionFunc, contentFunc, elseFunc, true /*_not*/); }; /** * @summary Constructs a View that renders `contentFunc` for each item in a sequence. * @locus Client * @param {Function} argFunc A function to reactively re-run. The function can * return one of two options: * * 1. An object with two fields: '_variable' and '_sequence'. Each iterates over * '_sequence', it may be a Cursor, an array, null, or undefined. Inside the * Each body you will be able to get the current item from the sequence using * the name specified in the '_variable' field. * * 2. Just a sequence (Cursor, array, null, or undefined) not wrapped into an * object. Inside the Each body, the current item will be set as the data * context. * @param {Function} contentFunc A Function that returns [*renderable * content*](#renderable_content). * @param {Function} [elseFunc] A Function that returns [*renderable * content*](#renderable_content) to display in the case when there are no items * in the sequence. */ Blaze.Each = function (argFunc, contentFunc, elseFunc) { var eachView = Blaze.View('each', function () { var subviews = this.initialSubviews; this.initialSubviews = null; if (this._isCreatedForExpansion) { this.expandedValueDep = new Tracker.Dependency; this.expandedValueDep.depend(); } return subviews; }); eachView.initialSubviews = []; eachView.numItems = 0; eachView.inElseMode = false; eachView.stopHandle = null; eachView.contentFunc = contentFunc; eachView.elseFunc = elseFunc; eachView.argVar = new ReactiveVar; eachView.variableName = null; // update the @index value in the scope of all subviews in the range var updateIndices = function (from, to) { if (to === undefined) { to = eachView.numItems - 1; } for (var i = from; i <= to; i++) { var view = eachView._domrange.members[i].view; view._scopeBindings['@index'].set(i); } }; eachView.onViewCreated(function () { // We evaluate argFunc in an autorun to make sure // Blaze.currentView is always set when it runs (rather than // passing argFunc straight to ObserveSequence). eachView.autorun(function () { // argFunc can return either a sequence as is or a wrapper object with a // _sequence and _variable fields set. var arg = argFunc(); if (_.isObject(arg) && _.has(arg, '_sequence')) { eachView.variableName = arg._variable || null; arg = arg._sequence; } eachView.argVar.set(arg); }, eachView.parentView, 'collection'); eachView.stopHandle = ObserveSequence.observe(function () { return eachView.argVar.get(); }, { addedAt: function (id, item, index) { Tracker.nonreactive(function () { var newItemView; if (eachView.variableName) { // new-style #each (as in {{#each item in items}}) // doesn't create a new data context newItemView = Blaze.View('item', eachView.contentFunc); } else { newItemView = Blaze.With(item, eachView.contentFunc); } eachView.numItems++; var bindings = {}; bindings['@index'] = index; if (eachView.variableName) { bindings[eachView.variableName] = item; } Blaze._attachBindingsToView(bindings, newItemView); if (eachView.expandedValueDep) { eachView.expandedValueDep.changed(); } else if (eachView._domrange) { if (eachView.inElseMode) { eachView._domrange.removeMember(0); eachView.inElseMode = false; } var range = Blaze._materializeView(newItemView, eachView); eachView._domrange.addMember(range, index); updateIndices(index); } else { eachView.initialSubviews.splice(index, 0, newItemView); } }); }, removedAt: function (id, item, index) { Tracker.nonreactive(function () { eachView.numItems--; if (eachView.expandedValueDep) { eachView.expandedValueDep.changed(); } else if (eachView._domrange) { eachView._domrange.removeMember(index); updateIndices(index); if (eachView.elseFunc && eachView.numItems === 0) { eachView.inElseMode = true; eachView._domrange.addMember( Blaze._materializeView( Blaze.View('each_else',eachView.elseFunc), eachView), 0); } } else { eachView.initialSubviews.splice(index, 1); } }); }, changedAt: function (id, newItem, oldItem, index) { Tracker.nonreactive(function () { if (eachView.expandedValueDep) { eachView.expandedValueDep.changed(); } else { var itemView; if (eachView._domrange) { itemView = eachView._domrange.getMember(index).view; } else { itemView = eachView.initialSubviews[index]; } if (eachView.variableName) { itemView._scopeBindings[eachView.variableName].set(newItem); } else { itemView.dataVar.set(newItem); } } }); }, movedTo: function (id, item, fromIndex, toIndex) { Tracker.nonreactive(function () { if (eachView.expandedValueDep) { eachView.expandedValueDep.changed(); } else if (eachView._domrange) { eachView._domrange.moveMember(fromIndex, toIndex); updateIndices( Math.min(fromIndex, toIndex), Math.max(fromIndex, toIndex)); } else { var subviews = eachView.initialSubviews; var itemView = subviews[fromIndex]; subviews.splice(fromIndex, 1); subviews.splice(toIndex, 0, itemView); } }); } }); if (eachView.elseFunc && eachView.numItems === 0) { eachView.inElseMode = true; eachView.initialSubviews[0] = Blaze.View('each_else', eachView.elseFunc); } }); eachView.onViewDestroyed(function () { if (eachView.stopHandle) eachView.stopHandle.stop(); }); return eachView; }; Blaze._TemplateWith = function (arg, contentFunc) { var w; var argFunc = arg; if (typeof arg !== 'function') { argFunc = function () { return arg; }; } // This is a little messy. When we compile `{{> Template.contentBlock}}`, we // wrap it in Blaze._InOuterTemplateScope in order to skip the intermediate // parent Views in the current template. However, when there's an argument // (`{{> Template.contentBlock arg}}`), the argument needs to be evaluated // in the original scope. There's no good order to nest // Blaze._InOuterTemplateScope and Spacebars.TemplateWith to achieve this, // so we wrap argFunc to run it in the "original parentView" of the // Blaze._InOuterTemplateScope. // // To make this better, reconsider _InOuterTemplateScope as a primitive. // Longer term, evaluate expressions in the proper lexical scope. var wrappedArgFunc = function () { var viewToEvaluateArg = null; if (w.parentView && w.parentView.name === 'InOuterTemplateScope') { viewToEvaluateArg = w.parentView.originalParentView; } if (viewToEvaluateArg) { return Blaze._withCurrentView(viewToEvaluateArg, argFunc); } else { return argFunc(); } }; var wrappedContentFunc = function () { var content = contentFunc.call(this); // Since we are generating the Blaze._TemplateWith view for the // user, set the flag on the child view. If `content` is a template, // construct the View so that we can set the flag. if (content instanceof Blaze.Template) { content = content.constructView(); } if (content instanceof Blaze.View) { content._hasGeneratedParent = true; } return content; }; w = Blaze.With(wrappedArgFunc, wrappedContentFunc); w.__isTemplateWith = true; return w; }; Blaze._InOuterTemplateScope = function (templateView, contentFunc) { var view = Blaze.View('InOuterTemplateScope', contentFunc); var parentView = templateView.parentView; // Hack so that if you call `{{> foo bar}}` and it expands into // `{{#with bar}}{{> foo}}{{/with}}`, and then `foo` is a template // that inserts `{{> Template.contentBlock}}`, the data context for // `Template.contentBlock` is not `bar` but the one enclosing that. if (parentView.__isTemplateWith) parentView = parentView.parentView; view.onViewCreated(function () { this.originalParentView = this.parentView; this.parentView = parentView; this.__childDoesntStartNewLexicalScope = true; }); return view; }; // XXX COMPAT WITH 0.9.0 Blaze.InOuterTemplateScope = Blaze._InOuterTemplateScope; ================================================ FILE: packages/blaze/dombackend.js ================================================ var DOMBackend = {}; Blaze._DOMBackend = DOMBackend; var $jq = (typeof jQuery !== 'undefined' ? jQuery : (typeof Package !== 'undefined' ? Package.jquery && Package.jquery.jQuery : null)); if (! $jq) throw new Error("jQuery not found"); DOMBackend._$jq = $jq; DOMBackend.parseHTML = function (html) { // Return an array of nodes. // // jQuery does fancy stuff like creating an appropriate // container element and setting innerHTML on it, as well // as working around various IE quirks. return $jq.parseHTML(html) || []; }; DOMBackend.Events = { // `selector` is non-null. `type` is one type (but // may be in backend-specific form, e.g. have namespaces). // Order fired must be order bound. delegateEvents: function (elem, type, selector, handler) { $jq(elem).on(type, selector, handler); }, undelegateEvents: function (elem, type, handler) { $jq(elem).off(type, '**', handler); }, bindEventCapturer: function (elem, type, selector, handler) { var $elem = $jq(elem); var wrapper = function (event) { event = $jq.event.fix(event); event.currentTarget = event.target; // Note: It might improve jQuery interop if we called into jQuery // here somehow. Since we don't use jQuery to dispatch the event, // we don't fire any of jQuery's event hooks or anything. However, // since jQuery can't bind capturing handlers, it's not clear // where we would hook in. Internal jQuery functions like `dispatch` // are too high-level. var $target = $jq(event.currentTarget); if ($target.is($elem.find(selector))) handler.call(elem, event); }; handler._meteorui_wrapper = wrapper; type = DOMBackend.Events.parseEventType(type); // add *capturing* event listener elem.addEventListener(type, wrapper, true); }, unbindEventCapturer: function (elem, type, handler) { type = DOMBackend.Events.parseEventType(type); elem.removeEventListener(type, handler._meteorui_wrapper, true); }, parseEventType: function (type) { // strip off namespaces var dotLoc = type.indexOf('.'); if (dotLoc >= 0) return type.slice(0, dotLoc); return type; } }; ///// Removal detection and interoperability. // For an explanation of this technique, see: // http://bugs.jquery.com/ticket/12213#comment:23 . // // In short, an element is considered "removed" when jQuery // cleans up its *private* userdata on the element, // which we can detect using a custom event with a teardown // hook. var NOOP = function () {}; // Circular doubly-linked list var TeardownCallback = function (func) { this.next = this; this.prev = this; this.func = func; }; // Insert newElt before oldElt in the circular list TeardownCallback.prototype.linkBefore = function(oldElt) { this.prev = oldElt.prev; this.next = oldElt; oldElt.prev.next = this; oldElt.prev = this; }; TeardownCallback.prototype.unlink = function () { this.prev.next = this.next; this.next.prev = this.prev; }; TeardownCallback.prototype.go = function () { var func = this.func; func && func(); }; TeardownCallback.prototype.stop = TeardownCallback.prototype.unlink; DOMBackend.Teardown = { _JQUERY_EVENT_NAME: 'blaze_teardown_watcher', _CB_PROP: '$blaze_teardown_callbacks', // Registers a callback function to be called when the given element or // one of its ancestors is removed from the DOM via the backend library. // The callback function is called at most once, and it receives the element // in question as an argument. onElementTeardown: function (elem, func) { var elt = new TeardownCallback(func); var propName = DOMBackend.Teardown._CB_PROP; if (! elem[propName]) { // create an empty node that is never unlinked elem[propName] = new TeardownCallback; // Set up the event, only the first time. $jq(elem).on(DOMBackend.Teardown._JQUERY_EVENT_NAME, NOOP); } elt.linkBefore(elem[propName]); return elt; // so caller can call stop() }, // Recursively call all teardown hooks, in the backend and registered // through DOMBackend.onElementTeardown. tearDownElement: function (elem) { var elems = []; // Array.prototype.slice.call doesn't work when given a NodeList in // IE8 ("JScript object expected"). var nodeList = elem.getElementsByTagName('*'); for (var i = 0; i < nodeList.length; i++) { elems.push(nodeList[i]); } elems.push(elem); $jq.cleanData(elems); } }; $jq.event.special[DOMBackend.Teardown._JQUERY_EVENT_NAME] = { setup: function () { // This "setup" callback is important even though it is empty! // Without it, jQuery will call addEventListener, which is a // performance hit, especially with Chrome's async stack trace // feature enabled. }, teardown: function() { var elem = this; var callbacks = elem[DOMBackend.Teardown._CB_PROP]; if (callbacks) { var elt = callbacks.next; while (elt !== callbacks) { elt.go(); elt = elt.next; } callbacks.go(); elem[DOMBackend.Teardown._CB_PROP] = null; } } }; // Must use jQuery semantics for `context`, not // querySelectorAll's. In other words, all the parts // of `selector` must be found under `context`. DOMBackend.findBySelector = function (selector, context) { return $jq(selector, context); }; ================================================ FILE: packages/blaze/domrange.js ================================================ // A constant empty array (frozen if the JS engine supports it). var _emptyArray = Object.freeze ? Object.freeze([]) : []; // `[new] Blaze._DOMRange([nodeAndRangeArray])` // // A DOMRange consists of an array of consecutive nodes and DOMRanges, // which may be replaced at any time with a new array. If the DOMRange // has been attached to the DOM at some location, then updating // the array will cause the DOM to be updated at that location. Blaze._DOMRange = function (nodeAndRangeArray) { if (! (this instanceof DOMRange)) // called without `new` return new DOMRange(nodeAndRangeArray); var members = (nodeAndRangeArray || _emptyArray); if (! (members && (typeof members.length) === 'number')) throw new Error("Expected array"); for (var i = 0; i < members.length; i++) this._memberIn(members[i]); this.members = members; this.emptyRangePlaceholder = null; this.attached = false; this.parentElement = null; this.parentRange = null; this.attachedCallbacks = _emptyArray; }; var DOMRange = Blaze._DOMRange; // In IE 8, don't use empty text nodes as placeholders // in empty DOMRanges, use comment nodes instead. Using // empty text nodes in modern browsers is great because // it doesn't clutter the web inspector. In IE 8, however, // it seems to lead in some roundabout way to the OAuth // pop-up crashing the browser completely. In the past, // we didn't use empty text nodes on IE 8 because they // don't accept JS properties, so just use the same logic // even though we don't need to set properties on the // placeholder anymore. DOMRange._USE_COMMENT_PLACEHOLDERS = (function () { var result = false; var textNode = document.createTextNode(""); try { textNode.someProp = true; } catch (e) { // IE 8 result = true; } return result; })(); // static methods DOMRange._insert = function (rangeOrNode, parentElement, nextNode, _isMove) { var m = rangeOrNode; if (m instanceof DOMRange) { m.attach(parentElement, nextNode, _isMove); } else { if (_isMove) DOMRange._moveNodeWithHooks(m, parentElement, nextNode); else DOMRange._insertNodeWithHooks(m, parentElement, nextNode); } }; DOMRange._remove = function (rangeOrNode) { var m = rangeOrNode; if (m instanceof DOMRange) { m.detach(); } else { DOMRange._removeNodeWithHooks(m); } }; DOMRange._removeNodeWithHooks = function (n) { if (! n.parentNode) return; if (n.nodeType === 1 && n.parentNode._uihooks && n.parentNode._uihooks.removeElement) { n.parentNode._uihooks.removeElement(n); } else { n.parentNode.removeChild(n); } }; DOMRange._insertNodeWithHooks = function (n, parent, next) { // `|| null` because IE throws an error if 'next' is undefined next = next || null; if (n.nodeType === 1 && parent._uihooks && parent._uihooks.insertElement) { parent._uihooks.insertElement(n, next); } else { parent.insertBefore(n, next); } }; DOMRange._moveNodeWithHooks = function (n, parent, next) { if (n.parentNode !== parent) return; // `|| null` because IE throws an error if 'next' is undefined next = next || null; if (n.nodeType === 1 && parent._uihooks && parent._uihooks.moveElement) { parent._uihooks.moveElement(n, next); } else { parent.insertBefore(n, next); } }; DOMRange.forElement = function (elem) { if (elem.nodeType !== 1) throw new Error("Expected element, found: " + elem); var range = null; while (elem && ! range) { range = (elem.$blaze_range || null); if (! range) elem = elem.parentNode; } return range; }; DOMRange.prototype.attach = function (parentElement, nextNode, _isMove, _isReplace) { // This method is called to insert the DOMRange into the DOM for // the first time, but it's also used internally when // updating the DOM. // // If _isMove is true, move this attached range to a different // location under the same parentElement. if (_isMove || _isReplace) { if (! (this.parentElement === parentElement && this.attached)) throw new Error("Can only move or replace an attached DOMRange, and only under the same parent element"); } var members = this.members; if (members.length) { this.emptyRangePlaceholder = null; for (var i = 0; i < members.length; i++) { DOMRange._insert(members[i], parentElement, nextNode, _isMove); } } else { var placeholder = ( DOMRange._USE_COMMENT_PLACEHOLDERS ? document.createComment("") : document.createTextNode("")); this.emptyRangePlaceholder = placeholder; parentElement.insertBefore(placeholder, nextNode || null); } this.attached = true; this.parentElement = parentElement; if (! (_isMove || _isReplace)) { for(var i = 0; i < this.attachedCallbacks.length; i++) { var obj = this.attachedCallbacks[i]; obj.attached && obj.attached(this, parentElement); } } }; DOMRange.prototype.setMembers = function (newNodeAndRangeArray) { var newMembers = newNodeAndRangeArray; if (! (newMembers && (typeof newMembers.length) === 'number')) throw new Error("Expected array"); var oldMembers = this.members; for (var i = 0; i < oldMembers.length; i++) this._memberOut(oldMembers[i]); for (var i = 0; i < newMembers.length; i++) this._memberIn(newMembers[i]); if (! this.attached) { this.members = newMembers; } else { // don't do anything if we're going from empty to empty if (newMembers.length || oldMembers.length) { // detach the old members and insert the new members var nextNode = this.lastNode().nextSibling; var parentElement = this.parentElement; // Use detach/attach, but don't fire attached/detached hooks this.detach(true /*_isReplace*/); this.members = newMembers; this.attach(parentElement, nextNode, false, true /*_isReplace*/); } } }; DOMRange.prototype.firstNode = function () { if (! this.attached) throw new Error("Must be attached"); if (! this.members.length) return this.emptyRangePlaceholder; var m = this.members[0]; return (m instanceof DOMRange) ? m.firstNode() : m; }; DOMRange.prototype.lastNode = function () { if (! this.attached) throw new Error("Must be attached"); if (! this.members.length) return this.emptyRangePlaceholder; var m = this.members[this.members.length - 1]; return (m instanceof DOMRange) ? m.lastNode() : m; }; DOMRange.prototype.detach = function (_isReplace) { if (! this.attached) throw new Error("Must be attached"); var oldParentElement = this.parentElement; var members = this.members; if (members.length) { for (var i = 0; i < members.length; i++) { DOMRange._remove(members[i]); } } else { var placeholder = this.emptyRangePlaceholder; this.parentElement.removeChild(placeholder); this.emptyRangePlaceholder = null; } if (! _isReplace) { this.attached = false; this.parentElement = null; for(var i = 0; i < this.attachedCallbacks.length; i++) { var obj = this.attachedCallbacks[i]; obj.detached && obj.detached(this, oldParentElement); } } }; DOMRange.prototype.addMember = function (newMember, atIndex, _isMove) { var members = this.members; if (! (atIndex >= 0 && atIndex <= members.length)) throw new Error("Bad index in range.addMember: " + atIndex); if (! _isMove) this._memberIn(newMember); if (! this.attached) { // currently detached; just updated members members.splice(atIndex, 0, newMember); } else if (members.length === 0) { // empty; use the empty-to-nonempty handling of setMembers this.setMembers([newMember]); } else { var nextNode; if (atIndex === members.length) { // insert at end nextNode = this.lastNode().nextSibling; } else { var m = members[atIndex]; nextNode = (m instanceof DOMRange) ? m.firstNode() : m; } members.splice(atIndex, 0, newMember); DOMRange._insert(newMember, this.parentElement, nextNode, _isMove); } }; DOMRange.prototype.removeMember = function (atIndex, _isMove) { var members = this.members; if (! (atIndex >= 0 && atIndex < members.length)) throw new Error("Bad index in range.removeMember: " + atIndex); if (_isMove) { members.splice(atIndex, 1); } else { var oldMember = members[atIndex]; this._memberOut(oldMember); if (members.length === 1) { // becoming empty; use the logic in setMembers this.setMembers(_emptyArray); } else { members.splice(atIndex, 1); if (this.attached) DOMRange._remove(oldMember); } } }; DOMRange.prototype.moveMember = function (oldIndex, newIndex) { var member = this.members[oldIndex]; this.removeMember(oldIndex, true /*_isMove*/); this.addMember(member, newIndex, true /*_isMove*/); }; DOMRange.prototype.getMember = function (atIndex) { var members = this.members; if (! (atIndex >= 0 && atIndex < members.length)) throw new Error("Bad index in range.getMember: " + atIndex); return this.members[atIndex]; }; DOMRange.prototype._memberIn = function (m) { if (m instanceof DOMRange) m.parentRange = this; else if (m.nodeType === 1) // DOM Element m.$blaze_range = this; }; DOMRange._destroy = function (m, _skipNodes) { if (m instanceof DOMRange) { if (m.view) Blaze._destroyView(m.view, _skipNodes); } else if ((! _skipNodes) && m.nodeType === 1) { // DOM Element if (m.$blaze_range) { Blaze._destroyNode(m); m.$blaze_range = null; } } }; DOMRange.prototype._memberOut = DOMRange._destroy; // Tear down, but don't remove, the members. Used when chunks // of DOM are being torn down or replaced. DOMRange.prototype.destroyMembers = function (_skipNodes) { var members = this.members; for (var i = 0; i < members.length; i++) this._memberOut(members[i], _skipNodes); }; DOMRange.prototype.destroy = function (_skipNodes) { DOMRange._destroy(this, _skipNodes); }; DOMRange.prototype.containsElement = function (elem) { if (! this.attached) throw new Error("Must be attached"); // An element is contained in this DOMRange if it's possible to // reach it by walking parent pointers, first through the DOM and // then parentRange pointers. In other words, the element or some // ancestor of it is at our level of the DOM (a child of our // parentElement), and this element is one of our members or // is a member of a descendant Range. // First check that elem is a descendant of this.parentElement, // according to the DOM. if (! Blaze._elementContains(this.parentElement, elem)) return false; // If elem is not an immediate child of this.parentElement, // walk up to its ancestor that is. while (elem.parentNode !== this.parentElement) elem = elem.parentNode; var range = elem.$blaze_range; while (range && range !== this) range = range.parentRange; return range === this; }; DOMRange.prototype.containsRange = function (range) { if (! this.attached) throw new Error("Must be attached"); if (! range.attached) return false; // A DOMRange is contained in this DOMRange if it's possible // to reach this range by following parent pointers. If the // DOMRange has the same parentElement, then it should be // a member, or a member of a member etc. Otherwise, we must // contain its parentElement. if (range.parentElement !== this.parentElement) return this.containsElement(range.parentElement); if (range === this) return false; // don't contain self while (range && range !== this) range = range.parentRange; return range === this; }; DOMRange.prototype.onAttached = function (attached) { this.onAttachedDetached({ attached: attached }); }; // callbacks are `attached(range, element)` and // `detached(range, element)`, and they may // access the `callbacks` object in `this`. // The arguments to `detached` are the same // range and element that were passed to `attached`. DOMRange.prototype.onAttachedDetached = function (callbacks) { if (this.attachedCallbacks === _emptyArray) this.attachedCallbacks = []; this.attachedCallbacks.push(callbacks); }; DOMRange.prototype.$ = function (selector) { var self = this; var parentNode = this.parentElement; if (! parentNode) throw new Error("Can't select in removed DomRange"); // Strategy: Find all selector matches under parentNode, // then filter out the ones that aren't in this DomRange // using `DOMRange#containsElement`. This is // asymptotically slow in the presence of O(N) sibling // content that is under parentNode but not in our range, // so if performance is an issue, the selector should be // run on a child element. // Since jQuery can't run selectors on a DocumentFragment, // we don't expect findBySelector to work. if (parentNode.nodeType === 11 /* DocumentFragment */) throw new Error("Can't use $ on an offscreen range"); var results = Blaze._DOMBackend.findBySelector(selector, parentNode); // We don't assume `results` has jQuery API; a plain array // should do just as well. However, if we do have a jQuery // array, we want to end up with one also, so we use // `.filter`. // Function that selects only elements that are actually // in this DomRange, rather than simply descending from // `parentNode`. var filterFunc = function (elem) { // handle jQuery's arguments to filter, where the node // is in `this` and the index is the first argument. if (typeof elem === 'number') elem = this; return self.containsElement(elem); }; if (! results.filter) { // not a jQuery array, and not a browser with // Array.prototype.filter (e.g. IE <9) var newResults = []; for (var i = 0; i < results.length; i++) { var x = results[i]; if (filterFunc(x)) newResults.push(x); } results = newResults; } else { // `results.filter` is either jQuery's or ECMAScript's `filter` results = results.filter(filterFunc); } return results; }; // Returns true if element a contains node b and is not node b. // // The restriction that `a` be an element (not a document fragment, // say) is based on what's easy to implement cross-browser. Blaze._elementContains = function (a, b) { if (a.nodeType !== 1) // ELEMENT return false; if (a === b) return false; if (a.compareDocumentPosition) { return a.compareDocumentPosition(b) & 0x10; } else { // Should be only old IE and maybe other old browsers here. // Modern Safari has both functions but seems to get contains() wrong. // IE can't handle b being a text node. We work around this // by doing a direct parent test now. b = b.parentNode; if (! (b && b.nodeType === 1)) // ELEMENT return false; if (a === b) return true; return a.contains(b); } }; ================================================ FILE: packages/blaze/events.js ================================================ var EventSupport = Blaze._EventSupport = {}; var DOMBackend = Blaze._DOMBackend; // List of events to always delegate, never capture. // Since jQuery fakes bubbling for certain events in // certain browsers (like `submit`), we don't want to // get in its way. // // We could list all known bubbling // events here to avoid creating speculative capturers // for them, but it would only be an optimization. var eventsToDelegate = EventSupport.eventsToDelegate = { blur: 1, change: 1, click: 1, focus: 1, focusin: 1, focusout: 1, reset: 1, submit: 1 }; var EVENT_MODE = EventSupport.EVENT_MODE = { TBD: 0, BUBBLING: 1, CAPTURING: 2 }; var NEXT_HANDLERREC_ID = 1; var HandlerRec = function (elem, type, selector, handler, recipient) { this.elem = elem; this.type = type; this.selector = selector; this.handler = handler; this.recipient = recipient; this.id = (NEXT_HANDLERREC_ID++); this.mode = EVENT_MODE.TBD; // It's important that delegatedHandler be a different // instance for each handlerRecord, because its identity // is used to remove it. // // It's also important that the closure have access to // `this` when it is not called with it set. this.delegatedHandler = (function (h) { return function (evt) { if ((! h.selector) && evt.currentTarget !== evt.target) // no selector means only fire on target return; return h.handler.apply(h.recipient, arguments); }; })(this); // WHY CAPTURE AND DELEGATE: jQuery can't delegate // non-bubbling events, because // event capture doesn't work in IE 8. However, there // are all sorts of new-fangled non-bubbling events // like "play" and "touchenter". We delegate these // events using capture in all browsers except IE 8. // IE 8 doesn't support these events anyway. var tryCapturing = elem.addEventListener && (! _.has(eventsToDelegate, DOMBackend.Events.parseEventType(type))); if (tryCapturing) { this.capturingHandler = (function (h) { return function (evt) { if (h.mode === EVENT_MODE.TBD) { // must be first time we're called. if (evt.bubbles) { // this type of event bubbles, so don't // get called again. h.mode = EVENT_MODE.BUBBLING; DOMBackend.Events.unbindEventCapturer( h.elem, h.type, h.capturingHandler); return; } else { // this type of event doesn't bubble, // so unbind the delegation, preventing // it from ever firing. h.mode = EVENT_MODE.CAPTURING; DOMBackend.Events.undelegateEvents( h.elem, h.type, h.delegatedHandler); } } h.delegatedHandler(evt); }; })(this); } else { this.mode = EVENT_MODE.BUBBLING; } }; EventSupport.HandlerRec = HandlerRec; HandlerRec.prototype.bind = function () { // `this.mode` may be EVENT_MODE_TBD, in which case we bind both. in // this case, 'capturingHandler' is in charge of detecting the // correct mode and turning off one or the other handlers. if (this.mode !== EVENT_MODE.BUBBLING) { DOMBackend.Events.bindEventCapturer( this.elem, this.type, this.selector || '*', this.capturingHandler); } if (this.mode !== EVENT_MODE.CAPTURING) DOMBackend.Events.delegateEvents( this.elem, this.type, this.selector || '*', this.delegatedHandler); }; HandlerRec.prototype.unbind = function () { if (this.mode !== EVENT_MODE.BUBBLING) DOMBackend.Events.unbindEventCapturer(this.elem, this.type, this.capturingHandler); if (this.mode !== EVENT_MODE.CAPTURING) DOMBackend.Events.undelegateEvents(this.elem, this.type, this.delegatedHandler); }; EventSupport.listen = function (element, events, selector, handler, recipient, getParentRecipient) { // Prevent this method from being JITed by Safari. Due to a // presumed JIT bug in Safari -- observed in Version 7.0.6 // (9537.78.2) -- this method may crash the Safari render process if // it is JITed. // Repro: https://github.com/dgreensp/public/tree/master/safari-crash try { element = element; } finally {} var eventTypes = []; events.replace(/[^ /]+/g, function (e) { eventTypes.push(e); }); var newHandlerRecs = []; for (var i = 0, N = eventTypes.length; i < N; i++) { var type = eventTypes[i]; var eventDict = element.$blaze_events; if (! eventDict) eventDict = (element.$blaze_events = {}); var info = eventDict[type]; if (! info) { info = eventDict[type] = {}; info.handlers = []; } var handlerList = info.handlers; var handlerRec = new HandlerRec( element, type, selector, handler, recipient); newHandlerRecs.push(handlerRec); handlerRec.bind(); handlerList.push(handlerRec); // Move handlers of enclosing ranges to end, by unbinding and rebinding // them. In jQuery (or other DOMBackend) this causes them to fire // later when the backend dispatches event handlers. if (getParentRecipient) { for (var r = getParentRecipient(recipient); r; r = getParentRecipient(r)) { // r is an enclosing range (recipient) for (var j = 0, Nj = handlerList.length; j < Nj; j++) { var h = handlerList[j]; if (h.recipient === r) { h.unbind(); h.bind(); handlerList.splice(j, 1); // remove handlerList[j] handlerList.push(h); j--; // account for removed handler Nj--; // don't visit appended handlers } } } } } return { // closes over just `element` and `newHandlerRecs` stop: function () { var eventDict = element.$blaze_events; if (! eventDict) return; // newHandlerRecs has only one item unless you specify multiple // event types. If this code is slow, it's because we have to // iterate over handlerList here. Clearing a whole handlerList // via stop() methods is O(N^2) in the number of handlers on // an element. for (var i = 0; i < newHandlerRecs.length; i++) { var handlerToRemove = newHandlerRecs[i]; var info = eventDict[handlerToRemove.type]; if (! info) continue; var handlerList = info.handlers; for (var j = handlerList.length - 1; j >= 0; j--) { if (handlerList[j] === handlerToRemove) { handlerToRemove.unbind(); handlerList.splice(j, 1); // remove handlerList[j] } } } newHandlerRecs.length = 0; } }; }; ================================================ FILE: packages/blaze/exceptions.js ================================================ var debugFunc; // We call into user code in many places, and it's nice to catch exceptions // propagated from user code immediately so that the whole system doesn't just // break. Catching exceptions is easy; reporting them is hard. This helper // reports exceptions. // // Usage: // // ``` // try { // // ... someStuff ... // } catch (e) { // reportUIException(e); // } // ``` // // An optional second argument overrides the default message. // Set this to `true` to cause `reportException` to throw // the next exception rather than reporting it. This is // useful in unit tests that test error messages. Blaze._throwNextException = false; Blaze._reportException = function (e, msg) { if (Blaze._throwNextException) { Blaze._throwNextException = false; throw e; } if (! debugFunc) // adapted from Tracker debugFunc = function () { return (typeof Meteor !== "undefined" ? Meteor._debug : ((typeof console !== "undefined") && console.log ? console.log : function () {})); }; // In Chrome, `e.stack` is a multiline string that starts with the message // and contains a stack trace. Furthermore, `console.log` makes it clickable. // `console.log` supplies the space between the two arguments. debugFunc()(msg || 'Exception caught in template:', e.stack || e.message || e); }; Blaze._wrapCatchingExceptions = function (f, where) { if (typeof f !== 'function') return f; return function () { try { return f.apply(this, arguments); } catch (e) { Blaze._reportException(e, 'Exception in ' + where + ':'); } }; }; ================================================ FILE: packages/blaze/lookup.js ================================================ Blaze._globalHelpers = {}; // Documented as Template.registerHelper. // This definition also provides back-compat for `UI.registerHelper`. Blaze.registerHelper = function (name, func) { Blaze._globalHelpers[name] = func; }; // Also documented as Template.deregisterHelper Blaze.deregisterHelper = function(name) { delete Blaze._globalHelpers[name]; } var bindIfIsFunction = function (x, target) { if (typeof x !== 'function') return x; return _.bind(x, target); }; // If `x` is a function, binds the value of `this` for that function // to the current data context. var bindDataContext = function (x) { if (typeof x === 'function') { return function () { var data = Blaze.getData(); if (data == null) data = {}; return x.apply(data, arguments); }; } return x; }; Blaze._OLDSTYLE_HELPER = {}; Blaze._getTemplateHelper = function (template, name, tmplInstanceFunc) { // XXX COMPAT WITH 0.9.3 var isKnownOldStyleHelper = false; if (template.__helpers.has(name)) { var helper = template.__helpers.get(name); if (helper === Blaze._OLDSTYLE_HELPER) { isKnownOldStyleHelper = true; } else if (helper != null) { return wrapHelper(bindDataContext(helper), tmplInstanceFunc); } else { return null; } } // old-style helper if (name in template) { // Only warn once per helper if (! isKnownOldStyleHelper) { template.__helpers.set(name, Blaze._OLDSTYLE_HELPER); if (! template._NOWARN_OLDSTYLE_HELPERS) { Blaze._warn('Assigning helper with `' + template.viewName + '.' + name + ' = ...` is deprecated. Use `' + template.viewName + '.helpers(...)` instead.'); } } if (template[name] != null) { return wrapHelper(bindDataContext(template[name]), tmplInstanceFunc); } } return null; }; var wrapHelper = function (f, templateFunc) { if (typeof f !== "function") { return f; } return function () { var self = this; var args = arguments; return Blaze.Template._withTemplateInstanceFunc(templateFunc, function () { return Blaze._wrapCatchingExceptions(f, 'template helper').apply(self, args); }); }; }; Blaze._lexicalBindingLookup = function (view, name) { var currentView = view; var blockHelpersStack = []; // walk up the views stopping at a Spacebars.include or Template view that // doesn't have an InOuterTemplateScope view as a parent do { // skip block helpers views // if we found the binding on the scope, return it if (_.has(currentView._scopeBindings, name)) { var bindingReactiveVar = currentView._scopeBindings[name]; return function () { return bindingReactiveVar.get(); }; } } while (! (currentView.__startsNewLexicalScope && ! (currentView.parentView && currentView.parentView.__childDoesntStartNewLexicalScope)) && (currentView = currentView.parentView)); return null; }; // templateInstance argument is provided to be available for possible // alternative implementations of this function by 3rd party packages. Blaze._getTemplate = function (name, templateInstance) { if ((name in Blaze.Template) && (Blaze.Template[name] instanceof Blaze.Template)) { return Blaze.Template[name]; } return null; }; Blaze._getGlobalHelper = function (name, templateInstance) { if (Blaze._globalHelpers[name] != null) { return wrapHelper(bindDataContext(Blaze._globalHelpers[name]), templateInstance); } return null; }; // Looks up a name, like "foo" or "..", as a helper of the // current template; the name of a template; a global helper; // or a property of the data context. Called on the View of // a template (i.e. a View with a `.template` property, // where the helpers are). Used for the first name in a // "path" in a template tag, like "foo" in `{{foo.bar}}` or // ".." in `{{frobulate ../blah}}`. // // Returns a function, a non-function value, or null. If // a function is found, it is bound appropriately. // // NOTE: This function must not establish any reactive // dependencies itself. If there is any reactivity in the // value, lookup should return a function. Blaze.View.prototype.lookup = function (name, _options) { var template = this.template; var lookupTemplate = _options && _options.template; var helper; var binding; var boundTmplInstance; var foundTemplate; if (this.templateInstance) { boundTmplInstance = _.bind(this.templateInstance, this); } // 0. looking up the parent data context with the special "../" syntax if (/^\./.test(name)) { // starts with a dot. must be a series of dots which maps to an // ancestor of the appropriate height. if (!/^(\.)+$/.test(name)) throw new Error("id starting with dot must be a series of dots"); return Blaze._parentData(name.length - 1, true /*_functionWrapped*/); } // 1. look up a helper on the current template if (template && ((helper = Blaze._getTemplateHelper(template, name, boundTmplInstance)) != null)) { return helper; } // 2. look up a binding by traversing the lexical view hierarchy inside the // current template if (template && (binding = Blaze._lexicalBindingLookup(Blaze.currentView, name)) != null) { return binding; } // 3. look up a template by name if (lookupTemplate && ((foundTemplate = Blaze._getTemplate(name, boundTmplInstance)) != null)) { return foundTemplate; } // 4. look up a global helper if ((helper = Blaze._getGlobalHelper(name, boundTmplInstance)) != null) { return helper; } // 5. look up in a data context return function () { var isCalledAsFunction = (arguments.length > 0); var data = Blaze.getData(); var x = data && data[name]; if (! x) { if (lookupTemplate) { throw new Error("No such template: " + name); } else if (isCalledAsFunction) { throw new Error("No such function: " + name); } else if (name.charAt(0) === '@' && ((x === null) || (x === undefined))) { // Throw an error if the user tries to use a `@directive` // that doesn't exist. We don't implement all directives // from Handlebars, so there's a potential for confusion // if we fail silently. On the other hand, we want to // throw late in case some app or package wants to provide // a missing directive. throw new Error("Unsupported directive: " + name); } } if (! data) { return null; } if (typeof x !== 'function') { if (isCalledAsFunction) { throw new Error("Can't call non-function: " + x); } return x; } return x.apply(data, arguments); }; }; // Implement Spacebars' {{../..}}. // @param height {Number} The number of '..'s Blaze._parentData = function (height, _functionWrapped) { // If height is null or undefined, we default to 1, the first parent. if (height == null) { height = 1; } var theWith = Blaze.getView('with'); for (var i = 0; (i < height) && theWith; i++) { theWith = Blaze.getView(theWith, 'with'); } if (! theWith) return null; if (_functionWrapped) return function () { return theWith.dataVar.get(); }; return theWith.dataVar.get(); }; Blaze.View.prototype.lookupTemplate = function (name) { return this.lookup(name, {template:true}); }; ================================================ FILE: packages/blaze/materializer.js ================================================ // Turns HTMLjs into DOM nodes and DOMRanges. // // - `htmljs`: the value to materialize, which may be any of the htmljs // types (Tag, CharRef, Comment, Raw, array, string, boolean, number, // null, or undefined) or a View or Template (which will be used to // construct a View). // - `intoArray`: the array of DOM nodes and DOMRanges to push the output // into (required) // - `parentView`: the View we are materializing content for (optional) // - `_existingWorkStack`: optional argument, only used for recursive // calls when there is some other _materializeDOM on the call stack. // If _materializeDOM called your function and passed in a workStack, // pass it back when you call _materializeDOM (such as from a workStack // task). // // Returns `intoArray`, which is especially useful if you pass in `[]`. Blaze._materializeDOM = function (htmljs, intoArray, parentView, _existingWorkStack) { // In order to use fewer stack frames, materializeDOMInner can push // tasks onto `workStack`, and they will be popped off // and run, last first, after materializeDOMInner returns. The // reason we use a stack instead of a queue is so that we recurse // depth-first, doing newer tasks first. var workStack = (_existingWorkStack || []); materializeDOMInner(htmljs, intoArray, parentView, workStack); if (! _existingWorkStack) { // We created the work stack, so we are responsible for finishing // the work. Call each "task" function, starting with the top // of the stack. while (workStack.length) { // Note that running task() may push new items onto workStack. var task = workStack.pop(); task(); } } return intoArray; }; var materializeDOMInner = function (htmljs, intoArray, parentView, workStack) { if (htmljs == null) { // null or undefined return; } switch (typeof htmljs) { case 'string': case 'boolean': case 'number': intoArray.push(document.createTextNode(String(htmljs))); return; case 'object': if (htmljs.htmljsType) { switch (htmljs.htmljsType) { case HTML.Tag.htmljsType: intoArray.push(materializeTag(htmljs, parentView, workStack)); return; case HTML.CharRef.htmljsType: intoArray.push(document.createTextNode(htmljs.str)); return; case HTML.Comment.htmljsType: intoArray.push(document.createComment(htmljs.sanitizedValue)); return; case HTML.Raw.htmljsType: // Get an array of DOM nodes by using the browser's HTML parser // (like innerHTML). var nodes = Blaze._DOMBackend.parseHTML(htmljs.value); for (var i = 0; i < nodes.length; i++) intoArray.push(nodes[i]); return; } } else if (HTML.isArray(htmljs)) { for (var i = htmljs.length-1; i >= 0; i--) { workStack.push(_.bind(Blaze._materializeDOM, null, htmljs[i], intoArray, parentView, workStack)); } return; } else { if (htmljs instanceof Blaze.Template) { htmljs = htmljs.constructView(); // fall through to Blaze.View case below } if (htmljs instanceof Blaze.View) { Blaze._materializeView(htmljs, parentView, workStack, intoArray); return; } } } throw new Error("Unexpected object in htmljs: " + htmljs); }; var materializeTag = function (tag, parentView, workStack) { var tagName = tag.tagName; var elem; if ((HTML.isKnownSVGElement(tagName) || isSVGAnchor(tag)) && document.createElementNS) { // inline SVG elem = document.createElementNS('http://www.w3.org/2000/svg', tagName); } else { // normal elements elem = document.createElement(tagName); } var rawAttrs = tag.attrs; var children = tag.children; if (tagName === 'textarea' && tag.children.length && ! (rawAttrs && ('value' in rawAttrs))) { // Provide very limited support for TEXTAREA tags with children // rather than a "value" attribute. // Reactivity in the form of Views nested in the tag's children // won't work. Compilers should compile textarea contents into // the "value" attribute of the tag, wrapped in a function if there // is reactivity. if (typeof rawAttrs === 'function' || HTML.isArray(rawAttrs)) { throw new Error("Can't have reactive children of TEXTAREA node; " + "use the 'value' attribute instead."); } rawAttrs = _.extend({}, rawAttrs || null); rawAttrs.value = Blaze._expand(children, parentView); children = []; } if (rawAttrs) { var attrUpdater = new ElementAttributesUpdater(elem); var updateAttributes = function () { var expandedAttrs = Blaze._expandAttributes(rawAttrs, parentView); var flattenedAttrs = HTML.flattenAttributes(expandedAttrs); var stringAttrs = {}; for (var attrName in flattenedAttrs) { stringAttrs[attrName] = Blaze._toText(flattenedAttrs[attrName], parentView, HTML.TEXTMODE.STRING); } attrUpdater.update(stringAttrs); }; var updaterComputation; if (parentView) { updaterComputation = parentView.autorun(updateAttributes, undefined, 'updater'); } else { updaterComputation = Tracker.nonreactive(function () { return Tracker.autorun(function () { Tracker._withCurrentView(parentView, updateAttributes); }); }); } Blaze._DOMBackend.Teardown.onElementTeardown(elem, function attrTeardown() { updaterComputation.stop(); }); } if (children.length) { var childNodesAndRanges = []; // push this function first so that it's done last workStack.push(function () { for (var i = 0; i < childNodesAndRanges.length; i++) { var x = childNodesAndRanges[i]; if (x instanceof Blaze._DOMRange) x.attach(elem); else elem.appendChild(x); } }); // now push the task that calculates childNodesAndRanges workStack.push(_.bind(Blaze._materializeDOM, null, children, childNodesAndRanges, parentView, workStack)); } return elem; }; var isSVGAnchor = function (node) { // We generally aren't able to detect SVG elements because // if "A" were in our list of known svg element names, then all // nodes would be created using // `document.createElementNS`. But in the special case of , we can at least detect that attribute and // create an SVG tag in that case. // // However, we still have a general problem of knowing when to // use document.createElementNS and when to use // document.createElement; for example, font tags will always // be created as SVG elements which can cause other // problems. #1977 return (node.tagName === "a" && node.attrs && node.attrs["xlink:href"] !== undefined); }; ================================================ FILE: packages/blaze/microscore.js ================================================ // Microscore is a partial polyfill for Underscore. It implements // a subset of Underscore functions, and for some functions it // implements a subset of the full functionality. // // Code written against Microscore should just work with Underscore. // The reverse is not true, because Microscore doesn't support // all features of every function. A list of known differences // between Underscore and Microscore is given with each function. // // This file should be curated to keep it small, so that it doesn't // grow into Underscore. // // In the future, we'll figure out something better, like package // slices and dead code elimination. if (typeof _ !== 'undefined') throw new Error("If you have Underscore, don't use Microscore"); _ = {}; var hasOwnProperty = Object.prototype.hasOwnProperty; var objectToString = Object.prototype.toString; // Doesn't support more than two arguments (more than one "source" // object). _.extend = function (tgt, src) { for (var k in src) { if (hasOwnProperty.call(src, k)) tgt[k] = src[k]; } return tgt; }; _.has = function (obj, key) { return hasOwnProperty.call(obj, key); }; // Returns a copy of `array` with falsy elements removed. _.compact = function (array) { var result = []; for (var i = 0; i < array.length; i++) { var item = array[i]; if (item) result.push(item); } return result; }; // Returns whether `array` contains an element that is // `=== item`. _.contains = function (array, item) { for (var i = 0; i < array.length; i++) { if (array[i] === item) return true; } return false; }; // Returns `array` filtered to exclude elements that are // `=== item`. Similar to `_.without`. _.without = function (array, item) { var result = []; for (var i = 0; i < array.length; i++) { var x = array[i]; if (x !== item) result.push(x); } return result; }; // Assembles an array by calling `func(oldElement, index)` // on each element of `array`. Assumes argument is an array. _.map = function (array, func) { var result = new Array(array.length); for (var i = 0; i < array.length; i++) { result[i] = func(array[i], i); } return result; }; // Given an array: Calls `func(element, index)` on each element of // `array`. // // Given an object: Calls `func(value, key)` on each key/value of // `obj`. // // Only REAL arrays are treated as arrays. No Arguments objects, jQuery // objects, etc. This may be relaxed to the standard Meteor approach // if it is too constraining. // // Doesn't accept `null` as first argument. Doesn't delegate to built-in // `forEach` (which is generally not faster anyway because it calls // across the C/JS boundary). Doesn't mess with JavaScript's built-in // behavior if keys are added or removed during iteration (i.e. may // or may not visit them). _.each = function (arrayOrObject, func) { if (objectToString.call(arrayOrObject) === '[object Array]') { var array = arrayOrObject; for (var i = 0; i < array.length; i++) { func(array[i], i); } } else { var obj = arrayOrObject; for (var key in obj) { if (_.has(obj, key)) func(obj[key], key); } } }; _.bind = function (f, context) { return function () { return f.apply(target, context); }; }; ================================================ FILE: packages/blaze/package.js ================================================ Package.describe({ summary: "Meteor Reactive Templating library", version: '2.1.8' }); Package.onUse(function (api) { api.export(['Blaze', 'UI', 'Handlebars']); api.use('jquery'); // should be a weak dep, by having multiple "DOM backends" api.use('tracker'); api.use('check'); api.use('underscore'); // only the subset in microscore.js api.use('htmljs'); api.imply('htmljs'); api.use('observe-sequence'); api.use('reactive-var'); api.addFiles([ 'preamble.js' ]); // client-only files api.addFiles([ 'dombackend.js', 'domrange.js', 'events.js', 'attrs.js', 'materializer.js' ], 'client'); // client and server api.addFiles([ 'exceptions.js', 'view.js', 'builtins.js', 'lookup.js', 'template.js', 'backcompat.js' ]); }); Package.onTest(function (api) { api.use('tinytest'); api.use('jquery'); // strong dependency, for testing jQuery backend api.use('blaze'); api.use('test-helpers'); api.use('underscore'); api.use('blaze-tools'); // for BlazeTools.toJS api.use('html-tools'); api.use('reactive-var'); api.use('tracker'); api.use('templating'); api.addFiles('view_tests.js'); api.addFiles('render_tests.js', 'client'); }); ================================================ FILE: packages/blaze/preamble.js ================================================ /** * @namespace Blaze * @summary The namespace for all Blaze-related methods and classes. */ Blaze = {}; // Utility to HTML-escape a string. Included for legacy reasons. Blaze._escape = (function() { var escape_map = { "<": "<", ">": ">", '"': """, "'": "'", "`": "`", /* IE allows backtick-delimited attributes?? */ "&": "&" }; var escape_one = function(c) { return escape_map[c]; }; return function (x) { return x.replace(/[&<>"'`]/g, escape_one); }; })(); Blaze._warn = function (msg) { msg = 'Warning: ' + msg; if ((typeof console !== 'undefined') && console.warn) { console.warn(msg); } }; ================================================ FILE: packages/blaze/render_tests.js ================================================ var toCode = BlazeTools.toJS; var P = HTML.P; var CharRef = HTML.CharRef; var DIV = HTML.DIV; var Comment = HTML.Comment; var BR = HTML.BR; var A = HTML.A; var UL = HTML.UL; var LI = HTML.LI; var SPAN = HTML.SPAN; var HR = HTML.HR; var TEXTAREA = HTML.TEXTAREA; var INPUT = HTML.INPUT; var materialize = function (content, parent) { var func = content; if (typeof content !== 'function') { func = function () { return content; }; } Blaze.render(func, parent); }; var toHTML = Blaze.toHTML; Tinytest.add("blaze - render - basic", function (test) { var run = function (input, expectedInnerHTML, expectedHTML, expectedCode) { var div = document.createElement("DIV"); materialize(input, div); test.equal(canonicalizeHtml(div.innerHTML), expectedInnerHTML); test.equal(toHTML(input), expectedHTML); if (typeof expectedCode !== 'undefined') test.equal(toCode(input), expectedCode); }; run(P('Hello'), '

    Hello

    ', '

    Hello

    ', 'HTML.P("Hello")'); run([], '', '', '[]'); run([null, null], '', '', '[null, null]'); // Test crazy character references // `𝕫` is "Mathematical double-struck small z" a.k.a. "open-face z" run(P(CharRef({html: '𝕫', str: '\ud835\udd6b'})), '

    \ud835\udd6b

    ', '

    𝕫

    ', 'HTML.P(HTML.CharRef({html: "𝕫", str: "\\ud835\\udd6b"}))'); run(P({id: CharRef({html: '𝕫', str: '\ud835\udd6b'})}, 'Hello'), '

    Hello

    ', '

    Hello

    ', 'HTML.P({id: HTML.CharRef({html: "𝕫", str: "\\ud835\\udd6b"})}, "Hello")'); run(P({id: [CharRef({html: '𝕫', str: '\ud835\udd6b'}), '!']}, 'Hello'), '

    Hello

    ', '

    Hello

    ', 'HTML.P({id: [HTML.CharRef({html: "𝕫", str: "\\ud835\\udd6b"}), "!"]}, "Hello")'); // Test comments run(DIV(Comment('Test')), '
    ', // our innerHTML-canonicalization function kills comment contents '
    ', 'HTML.DIV(HTML.Comment("Test"))'); // Test arrays run([P('Hello'), P('World')], '

    Hello

    World

    ', '

    Hello

    World

    ', '[HTML.P("Hello"), HTML.P("World")]'); // Test slightly more complicated structure run(DIV({'class': 'foo'}, UL(LI(P(A({href: '#one'}, 'One'))), LI(P('Two', BR(), 'Three')))), '
    ', '
    ', 'HTML.DIV({"class": "foo"}, HTML.UL(HTML.LI(HTML.P(HTML.A({href: "#one"}, "One"))), HTML.LI(HTML.P("Two", HTML.BR(), "Three"))))'); // Test nully attributes run(BR({x: null, y: [[], []], a: [['']]}), '
    ', '
    ', 'HTML.BR({a: [[""]]})'); run(BR({ x: function () { return Blaze.View(function () { return Blaze.View(function () { return []; }); }); }, a: function () { return Blaze.View(function () { return Blaze.View(function () { return ''; }); }); }}), '
    ', '
    '); }); // test that we correctly update the 'value' property on input fields // rather than the 'value' attribute. the 'value' attribute only sets // the initial value. Tinytest.add("blaze - render - input - value", function (test) { var R = ReactiveVar("hello"); var div = document.createElement("DIV"); materialize(INPUT({value: function () { return R.get(); }}), div); var inputEl = div.querySelector('input'); test.equal(inputEl.value, "hello"); inputEl.value = "goodbye"; R.set("hola"); Tracker.flush(); test.equal(inputEl.value, "hola"); }); // test that we correctly update the 'checked' property rather than // the 'checked' attribute on input fields of type 'checkbox'. the // 'checked' attribute only sets the initial value. Tinytest.add("blaze - render - input - checked", function (test) { var R = ReactiveVar(null); var div = document.createElement("DIV"); materialize(INPUT({type: "checkbox", checked: function () { return R.get(); }}), div); var inputEl = div.querySelector('input'); test.equal(inputEl.checked, false); inputEl.checked = true; R.set("checked"); Tracker.flush(); R.set(null); Tracker.flush(); test.equal(inputEl.checked, false); }); Tinytest.add("blaze - render - textarea", function (test) { var run = function (optNode, text, html, code) { if (typeof optNode === 'string') { // called with args (text, html, code) code = html; html = text; text = optNode; optNode = null; } var div = document.createElement("DIV"); var node = TEXTAREA({value: optNode || text}); materialize(node, div); var value = div.querySelector('textarea').value; value = value.replace(/\r\n/g, "\n"); // IE8 substitutes \n with \r\n test.equal(value, text); test.equal(toHTML(node), html); if (typeof code === 'string') test.equal(toCode(node), code); }; run('Hello', '', 'HTML.TEXTAREA({value: "Hello"})'); run('\nHello', '', 'HTML.TEXTAREA({value: "\\nHello"})'); run('', '', 'HTML.TEXTAREA({value: ""})'); run(CharRef({html: '&', str: '&'}), '&', '', 'HTML.TEXTAREA({value: HTML.CharRef({html: "&", str: "&"})})'); run(function () { return ['a', Blaze.View(function () { return 'b'; }), 'c']; }, 'abc', ''); // test that reactivity of textarea "value" attribute works... (function () { var R = ReactiveVar('one'); var div = document.createElement("DIV"); var node = TEXTAREA({value: function () { return Blaze.View(function () { return R.get(); }); }}); materialize(node, div); var textarea = div.querySelector('textarea'); test.equal(textarea.value, 'one'); R.set('two'); Tracker.flush(); test.equal(textarea.value, 'two'); })(); // ... while "content" reactivity simply doesn't update // (but doesn't throw either) (function () { var R = ReactiveVar('one'); var div = document.createElement("DIV"); var node = TEXTAREA([Blaze.View(function () { return R.get(); })]); materialize(node, div); var textarea = div.querySelector('textarea'); test.equal(textarea.value, 'one'); R.set('two'); Tracker.flush({_throwFirstError: true}); test.equal(textarea.value, 'one'); })(); }); Tinytest.add("blaze - render - view isolation", function (test) { // Reactively change a text node (function () { var R = ReactiveVar('Hello'); var test1 = function () { return P(Blaze.View(function () { return R.get(); })); }; test.equal(toHTML(test1()), '

    Hello

    '); var div = document.createElement("DIV"); materialize(test1, div); test.equal(canonicalizeHtml(div.innerHTML), "

    Hello

    "); R.set('World'); Tracker.flush(); test.equal(canonicalizeHtml(div.innerHTML), "

    World

    "); })(); // Reactively change an array of text nodes (function () { var R = ReactiveVar(['Hello', ' World']); var test1 = function () { return P(Blaze.View(function () { return R.get(); })); }; test.equal(toHTML(test1()), '

    Hello World

    '); var div = document.createElement("DIV"); materialize(test1, div); test.equal(canonicalizeHtml(div.innerHTML), "

    Hello World

    "); R.set(['Goodbye', ' World']); Tracker.flush(); test.equal(canonicalizeHtml(div.innerHTML), "

    Goodbye World

    "); })(); }); // IE strips malformed styles like "bar::d" from the `style` // attribute. We detect this to adjust expectations for the StyleHandler // test below. var malformedStylesAllowed = function () { var div = document.createElement("div"); div.setAttribute("style", "bar::d;"); return (div.getAttribute("style") === "bar::d;"); }; Tinytest.add("blaze - render - view GC", function (test) { // test that removing parent element removes listeners and stops autoruns. (function () { var R = ReactiveVar('Hello'); var test1 = P(Blaze.View(function () { return R.get(); })); var div = document.createElement("DIV"); materialize(test1, div); test.equal(canonicalizeHtml(div.innerHTML), "

    Hello

    "); R.set('World'); Tracker.flush(); test.equal(canonicalizeHtml(div.innerHTML), "

    World

    "); test.equal(R._numListeners(), 1); $(div).remove(); test.equal(R._numListeners(), 0); R.set('Steve'); Tracker.flush(); // should not have changed: test.equal(canonicalizeHtml(div.innerHTML), "

    World

    "); })(); }); Tinytest.add("blaze - render - reactive attributes", function (test) { (function () { var R = ReactiveVar({'class': ['david gre', CharRef({html: 'ë', str: '\u00eb'}), 'nspan'], id: 'foo'}); var spanFunc = function () { return SPAN(HTML.Attrs( function () { return R.get(); })); }; test.equal(Blaze.toHTML(spanFunc()), ''); test.equal(R._numListeners(), 0); var div = document.createElement("DIV"); Blaze.render(spanFunc, div); test.equal(canonicalizeHtml(div.innerHTML), ''); test.equal(R._numListeners(), 1); var span = div.firstChild; test.equal(span.nodeName, 'SPAN'); span.className += ' blah'; // change the element's class outside of Blaze. this simulates what a jQuery could do R.set({'class': 'david smith', id: 'bar'}); Tracker.flush(); test.equal(canonicalizeHtml(div.innerHTML), ''); test.equal(R._numListeners(), 1); R.set({}); Tracker.flush(); test.equal(canonicalizeHtml(div.innerHTML), ''); test.equal(R._numListeners(), 1); $(div).remove(); test.equal(R._numListeners(), 0); })(); // Test styles. (function () { // Test the case where there is a semicolon in the css attribute. var R = ReactiveVar({'style': 'foo: "a;aa"; bar: b;', id: 'foo'}); var spanFunc = function () { return SPAN(HTML.Attrs(function () { return R.get(); })); }; test.equal(Blaze.toHTML(spanFunc()), ''); test.equal(R._numListeners(), 0); var div = document.createElement("DIV"); Blaze.render(spanFunc, div); test.equal(canonicalizeHtml(div.innerHTML), ''); test.equal(R._numListeners(), 1); var span = div.firstChild; test.equal(span.nodeName, 'SPAN'); span.setAttribute('style', span.getAttribute('style') + '; jquery-style: hidden'); R.set({'style': 'foo: "a;zz;aa";', id: 'bar'}); Tracker.flush(); test.equal(canonicalizeHtml(div.innerHTML, true), ''); test.equal(R._numListeners(), 1); R.set({}); Tracker.flush(); test.equal(canonicalizeHtml(div.innerHTML), ''); test.equal(R._numListeners(), 1); $(div).remove(); test.equal(R._numListeners(), 0); })(); // Test that identical styles are successfully overwritten. (function () { var R = ReactiveVar({'style': 'foo: a;'}); var spanFunc = function () { return SPAN(HTML.Attrs(function () { return R.get(); })); }; var div = document.createElement("DIV"); document.body.appendChild(div); Blaze.render(spanFunc, div); test.equal(canonicalizeHtml(div.innerHTML), ''); var span = div.firstChild; test.equal(span.nodeName, 'SPAN'); span.setAttribute("style", 'foo: b;'); test.equal(canonicalizeHtml(div.innerHTML), ''); R.set({'style': 'foo: c;'}); Tracker.flush(); test.equal(canonicalizeHtml(div.innerHTML), ''); // test malformed styles - different expectations in IE (which // strips malformed styles) from other browsers R.set({'style': 'foo: a; bar::d;:e; baz: c;'}); Tracker.flush(); test.equal(canonicalizeHtml(div.innerHTML), malformedStylesAllowed() ? '' : ''); // Test strange styles R.set({'style': ' foo: c; constructor: a; __proto__: b;'}); Tracker.flush(); test.equal(canonicalizeHtml(div.innerHTML), ''); R.set({}); Tracker.flush(); test.equal(canonicalizeHtml(div.innerHTML), ''); R.set({'style': 'foo: bar;'}); Tracker.flush(); test.equal(canonicalizeHtml(div.innerHTML), ''); })(); // Test `null`, `undefined`, and `[]` attributes (function () { var R = ReactiveVar({id: 'foo', aaa: null, bbb: undefined, ccc: [], ddd: [null], eee: [undefined], fff: [[]], ggg: ['x', ['y', ['z']]]}); var spanFunc = function () { return SPAN(HTML.Attrs( function () { return R.get(); })); }; test.equal(Blaze.toHTML(spanFunc()), ''); test.equal(toCode(SPAN(R.get())), 'HTML.SPAN({id: "foo", ggg: ["x", ["y", ["z"]]]})'); var div = document.createElement("DIV"); Blaze.render(spanFunc, div); var span = div.firstChild; test.equal(span.nodeName, 'SPAN'); test.equal(canonicalizeHtml(div.innerHTML), ''); R.set({id: 'foo', ggg: [[], [], []]}); Tracker.flush(); test.equal(canonicalizeHtml(div.innerHTML), ''); R.set({id: 'foo', ggg: null}); Tracker.flush(); test.equal(canonicalizeHtml(div.innerHTML), ''); R.set({id: 'foo', ggg: ''}); Tracker.flush(); test.equal(canonicalizeHtml(div.innerHTML), ''); $(div).remove(); test.equal(R._numListeners(), 0); })(); }); Tinytest.add("blaze - render - templates and views", function (test) { (function () { var counter = 1; var buf = []; var myTemplate = Blaze.Template( 'myTemplate', function () { return [String(this.number), (this.number < 3 ? makeView() : HR())]; }); myTemplate.constructView = function (number) { var view = Template.prototype.constructView.call(this); view.number = number; return view; }; myTemplate.created = function () { test.isFalse(Tracker.active); var view = this.view; var parent = Blaze.getView(view, 'myTemplate'); if (parent) { buf.push('parent of ' + view.number + ' is ' + parent.number); } buf.push('created ' + Template.currentData()); }; myTemplate.onRendered(function () { test.isFalse(Tracker.active); var nodeDescr = function (node) { if (node.nodeType === 8) // comment return ''; if (node.nodeType === 3) // text return node.nodeValue; return node.nodeName; }; var view = this.view; var start = view.firstNode(); var end = view.lastNode(); // skip marker nodes while (start !== end && ! nodeDescr(start)) start = start.nextSibling; while (end !== start && ! nodeDescr(end)) end = end.previousSibling; buf.push('dom-' + Template.currentData() + ' is ' + nodeDescr(start) +'..' + nodeDescr(end)); }); myTemplate.onDestroyed(function () { test.isFalse(Tracker.active); buf.push('destroyed ' + Template.currentData()); }); var makeView = function () { var number = counter++; return Blaze.With(number, function () { return myTemplate.constructView(number); }); }; var div = document.createElement("DIV"); Blaze.render(makeView, div); buf.push('---flush---'); Tracker.flush(); test.equal(buf, ['created 1', 'parent of 2 is 1', 'created 2', 'parent of 3 is 2', 'created 3', '---flush---', // (proper order for these has not be thought out:) 'dom-3 is 3..HR', 'dom-2 is 2..HR', 'dom-1 is 1..HR']); test.equal(canonicalizeHtml(div.innerHTML), '123
    '); buf.length = 0; $(div).remove(); buf.sort(); test.equal(buf, ['destroyed 1', 'destroyed 2', 'destroyed 3']); // Now use toHTML. Should still get most of the callbacks (not `rendered`). buf.length = 0; counter = 1; var html = Blaze.toHTML(makeView()); test.equal(buf, ['created 1', 'parent of 2 is 1', 'created 2', 'parent of 3 is 2', 'created 3', 'destroyed 3', 'destroyed 2', 'destroyed 1']); test.equal(html, '123
    '); })(); }); Tinytest.add("blaze - render - findAll", function (test) { var found = null; var $found = null; var myTemplate = new Template( 'findAllTest', function() { return DIV([P('first'), P('second')]); }); myTemplate.rendered = function() { found = this.findAll('p'); $found = this.$('p'); }; var div = document.createElement("DIV"); Blaze.render(myTemplate, div); Tracker.flush(); test.equal(_.isArray(found), true); test.equal(_.isArray($found), false); test.equal(found.length, 2); test.equal($found.length, 2); }); Tinytest.add("blaze - render - reactive attributes 2", function (test) { var R1 = ReactiveVar(['foo']); var R2 = ReactiveVar(['bar']); var spanFunc = function () { return SPAN(HTML.Attrs( { blah: function () { return R1.get(); } }, function () { return { blah: R2.get() }; })); }; var div = document.createElement("DIV"); Blaze.render(spanFunc, div); var check = function (expected) { test.equal(Blaze.toHTML(spanFunc()), expected); test.equal(canonicalizeHtml(div.innerHTML), expected); }; check(''); test.equal(R1._numListeners(), 1); test.equal(R2._numListeners(), 1); R2.set([[]]); Tracker.flush(); // We combine `['foo']` with what evaluates to `[[[]]]`, which is nully. check(''); R2.set([['']]); Tracker.flush(); // We combine `['foo']` with what evaluates to `[[['']]]`, which is non-nully. check(''); R2.set(null); Tracker.flush(); // We combine `['foo']` with `[null]`, which is nully. check(''); R1.set([[], []]); Tracker.flush(); // We combine two nully values. check(''); R1.set([[], ['foo']]); Tracker.flush(); check(''); // clean up $(div).remove(); test.equal(R1._numListeners(), 0); test.equal(R2._numListeners(), 0); }); Tinytest.add("blaze - render - SVG", function (test) { if (! document.createElementNS) { // IE 8 return; } var fillColor = ReactiveVar('red'); var classes = ReactiveVar('one two'); var content = DIV({'class': 'container'}, HTML.SVG( {width: 100, height: 100}, HTML.CIRCLE({cx: 50, cy: 50, r: 40, stroke: 'black', 'stroke-width': 3, 'class': function () { return classes.get(); }, fill: function () { return fillColor.get(); }}))); var div = document.createElement("DIV"); materialize(content, div); var circle = div.querySelector('.container > svg > circle'); test.equal(circle.getAttribute('fill'), 'red'); test.equal(circle.className.baseVal, 'one two'); fillColor.set('green'); classes.set('two three'); Tracker.flush(); test.equal(circle.getAttribute('fill'), 'green'); test.equal(circle.className.baseVal, 'two three'); test.equal(circle.nodeName, 'circle'); test.equal(circle.namespaceURI, "http://www.w3.org/2000/svg"); test.equal(circle.parentNode.namespaceURI, "http://www.w3.org/2000/svg"); }); Tinytest.add("ui - attributes", function (test) { var SPAN = HTML.SPAN; var amp = HTML.CharRef({html: '&', str: '&'}); test.equal(HTML.toHTML(SPAN({title: ['M', amp, 'Ms']}, 'M', amp, 'M candies')), 'M&M candies'); }); ================================================ FILE: packages/blaze/template.js ================================================ // [new] Blaze.Template([viewName], renderFunction) // // `Blaze.Template` is the class of templates, like `Template.foo` in // Meteor, which is `instanceof Template`. // // `viewKind` is a string that looks like "Template.foo" for templates // defined by the compiler. /** * @class * @summary Constructor for a Template, which is used to construct Views with particular name and content. * @locus Client * @param {String} [viewName] Optional. A name for Views constructed by this Template. See [`view.name`](#view_name). * @param {Function} renderFunction A function that returns [*renderable content*](#renderable_content). This function is used as the `renderFunction` for Views constructed by this Template. */ Blaze.Template = function (viewName, renderFunction) { if (! (this instanceof Blaze.Template)) // called without `new` return new Blaze.Template(viewName, renderFunction); if (typeof viewName === 'function') { // omitted "viewName" argument renderFunction = viewName; viewName = ''; } if (typeof viewName !== 'string') throw new Error("viewName must be a String (or omitted)"); if (typeof renderFunction !== 'function') throw new Error("renderFunction must be a function"); this.viewName = viewName; this.renderFunction = renderFunction; this.__helpers = new HelperMap; this.__eventMaps = []; this._callbacks = { created: [], rendered: [], destroyed: [] }; }; var Template = Blaze.Template; var HelperMap = function () {}; HelperMap.prototype.get = function (name) { return this[' '+name]; }; HelperMap.prototype.set = function (name, helper) { this[' '+name] = helper; }; HelperMap.prototype.has = function (name) { return (' '+name) in this; }; /** * @summary Returns true if `value` is a template object like `Template.myTemplate`. * @locus Client * @param {Any} value The value to test. */ Blaze.isTemplate = function (t) { return (t instanceof Blaze.Template); }; /** * @name onCreated * @instance * @memberOf Template * @summary Register a function to be called when an instance of this template is created. * @param {Function} callback A function to be added as a callback. * @locus Client * @importFromPackage templating */ Template.prototype.onCreated = function (cb) { this._callbacks.created.push(cb); }; /** * @name onRendered * @instance * @memberOf Template * @summary Register a function to be called when an instance of this template is inserted into the DOM. * @param {Function} callback A function to be added as a callback. * @locus Client * @importFromPackage templating */ Template.prototype.onRendered = function (cb) { this._callbacks.rendered.push(cb); }; /** * @name onDestroyed * @instance * @memberOf Template * @summary Register a function to be called when an instance of this template is removed from the DOM and destroyed. * @param {Function} callback A function to be added as a callback. * @locus Client * @importFromPackage templating */ Template.prototype.onDestroyed = function (cb) { this._callbacks.destroyed.push(cb); }; Template.prototype._getCallbacks = function (which) { var self = this; var callbacks = self[which] ? [self[which]] : []; // Fire all callbacks added with the new API (Template.onRendered()) // as well as the old-style callback (e.g. Template.rendered) for // backwards-compatibility. callbacks = callbacks.concat(self._callbacks[which]); return callbacks; }; var fireCallbacks = function (callbacks, template) { Template._withTemplateInstanceFunc( function () { return template; }, function () { for (var i = 0, N = callbacks.length; i < N; i++) { callbacks[i].call(template); } }); }; Template.prototype.constructView = function (contentFunc, elseFunc) { var self = this; var view = Blaze.View(self.viewName, self.renderFunction); view.template = self; view.templateContentBlock = ( contentFunc ? new Template('(contentBlock)', contentFunc) : null); view.templateElseBlock = ( elseFunc ? new Template('(elseBlock)', elseFunc) : null); if (self.__eventMaps || typeof self.events === 'object') { view._onViewRendered(function () { if (view.renderCount !== 1) return; if (! self.__eventMaps.length && typeof self.events === "object") { // Provide limited back-compat support for `.events = {...}` // syntax. Pass `template.events` to the original `.events(...)` // function. This code must run only once per template, in // order to not bind the handlers more than once, which is // ensured by the fact that we only do this when `__eventMaps` // is falsy, and we cause it to be set now. Template.prototype.events.call(self, self.events); } _.each(self.__eventMaps, function (m) { Blaze._addEventMap(view, m, view); }); }); } view._templateInstance = new Blaze.TemplateInstance(view); view.templateInstance = function () { // Update data, firstNode, and lastNode, and return the TemplateInstance // object. var inst = view._templateInstance; /** * @instance * @memberOf Blaze.TemplateInstance * @name data * @summary The data context of this instance's latest invocation. * @locus Client */ inst.data = Blaze.getData(view); if (view._domrange && !view.isDestroyed) { inst.firstNode = view._domrange.firstNode(); inst.lastNode = view._domrange.lastNode(); } else { // on 'created' or 'destroyed' callbacks we don't have a DomRange inst.firstNode = null; inst.lastNode = null; } return inst; }; /** * @name created * @instance * @memberOf Template * @summary Provide a callback when an instance of a template is created. * @locus Client * @deprecated in 1.1 */ // To avoid situations when new callbacks are added in between view // instantiation and event being fired, decide on all callbacks to fire // immediately and then fire them on the event. var createdCallbacks = self._getCallbacks('created'); view.onViewCreated(function () { fireCallbacks(createdCallbacks, view.templateInstance()); }); /** * @name rendered * @instance * @memberOf Template * @summary Provide a callback when an instance of a template is rendered. * @locus Client * @deprecated in 1.1 */ var renderedCallbacks = self._getCallbacks('rendered'); view.onViewReady(function () { fireCallbacks(renderedCallbacks, view.templateInstance()); }); /** * @name destroyed * @instance * @memberOf Template * @summary Provide a callback when an instance of a template is destroyed. * @locus Client * @deprecated in 1.1 */ var destroyedCallbacks = self._getCallbacks('destroyed'); view.onViewDestroyed(function () { fireCallbacks(destroyedCallbacks, view.templateInstance()); }); return view; }; /** * @class * @summary The class for template instances * @param {Blaze.View} view * @instanceName template */ Blaze.TemplateInstance = function (view) { if (! (this instanceof Blaze.TemplateInstance)) // called without `new` return new Blaze.TemplateInstance(view); if (! (view instanceof Blaze.View)) throw new Error("View required"); view._templateInstance = this; /** * @name view * @memberOf Blaze.TemplateInstance * @instance * @summary The [View](#blaze_view) object for this invocation of the template. * @locus Client * @type {Blaze.View} */ this.view = view; this.data = null; /** * @name firstNode * @memberOf Blaze.TemplateInstance * @instance * @summary The first top-level DOM node in this template instance. * @locus Client * @type {DOMNode} */ this.firstNode = null; /** * @name lastNode * @memberOf Blaze.TemplateInstance * @instance * @summary The last top-level DOM node in this template instance. * @locus Client * @type {DOMNode} */ this.lastNode = null; // This dependency is used to identify state transitions in // _subscriptionHandles which could cause the result of // TemplateInstance#subscriptionsReady to change. Basically this is triggered // whenever a new subscription handle is added or when a subscription handle // is removed and they are not ready. this._allSubsReadyDep = new Tracker.Dependency(); this._allSubsReady = false; this._subscriptionHandles = {}; }; /** * @summary Find all elements matching `selector` in this template instance, and return them as a JQuery object. * @locus Client * @param {String} selector The CSS selector to match, scoped to the template contents. * @returns {DOMNode[]} */ Blaze.TemplateInstance.prototype.$ = function (selector) { var view = this.view; if (! view._domrange) throw new Error("Can't use $ on template instance with no DOM"); return view._domrange.$(selector); }; /** * @summary Find all elements matching `selector` in this template instance. * @locus Client * @param {String} selector The CSS selector to match, scoped to the template contents. * @returns {DOMElement[]} */ Blaze.TemplateInstance.prototype.findAll = function (selector) { return Array.prototype.slice.call(this.$(selector)); }; /** * @summary Find one element matching `selector` in this template instance. * @locus Client * @param {String} selector The CSS selector to match, scoped to the template contents. * @returns {DOMElement} */ Blaze.TemplateInstance.prototype.find = function (selector) { var result = this.$(selector); return result[0] || null; }; /** * @summary A version of [Tracker.autorun](#tracker_autorun) that is stopped when the template is destroyed. * @locus Client * @param {Function} runFunc The function to run. It receives one argument: a Tracker.Computation object. */ Blaze.TemplateInstance.prototype.autorun = function (f) { return this.view.autorun(f); }; /** * @summary A version of [Meteor.subscribe](#meteor_subscribe) that is stopped * when the template is destroyed. * @return {SubscriptionHandle} The subscription handle to the newly made * subscription. Call `handle.stop()` to manually stop the subscription, or * `handle.ready()` to find out if this particular subscription has loaded all * of its inital data. * @locus Client * @param {String} name Name of the subscription. Matches the name of the * server's `publish()` call. * @param {Any} [arg1,arg2...] Optional arguments passed to publisher function * on server. * @param {Function|Object} [options] If a function is passed instead of an * object, it is interpreted as an `onReady` callback. * @param {Function} [options.onReady] Passed to [`Meteor.subscribe`](#meteor_subscribe). * @param {Function} [options.onStop] Passed to [`Meteor.subscribe`](#meteor_subscribe). * @param {DDP.Connection} [options.connection] The connection on which to make the * subscription. */ Blaze.TemplateInstance.prototype.subscribe = function (/* arguments */) { var self = this; var subHandles = self._subscriptionHandles; var args = _.toArray(arguments); // Duplicate logic from Meteor.subscribe var options = {}; if (args.length) { var lastParam = _.last(args); // Match pattern to check if the last arg is an options argument var lastParamOptionsPattern = { onReady: Match.Optional(Function), // XXX COMPAT WITH 1.0.3.1 onError used to exist, but now we use // onStop with an error callback instead. onError: Match.Optional(Function), onStop: Match.Optional(Function), connection: Match.Optional(Match.Any) }; if (_.isFunction(lastParam)) { options.onReady = args.pop(); } else if (lastParam && ! _.isEmpty(lastParam) && Match.test(lastParam, lastParamOptionsPattern)) { options = args.pop(); } } var subHandle; var oldStopped = options.onStop; options.onStop = function (error) { // When the subscription is stopped, remove it from the set of tracked // subscriptions to avoid this list growing without bound delete subHandles[subHandle.subscriptionId]; // Removing a subscription can only change the result of subscriptionsReady // if we are not ready (that subscription could be the one blocking us being // ready). if (! self._allSubsReady) { self._allSubsReadyDep.changed(); } if (oldStopped) { oldStopped(error); } }; var connection = options.connection; var callbacks = _.pick(options, ["onReady", "onError", "onStop"]); // The callbacks are passed as the last item in the arguments array passed to // View#subscribe args.push(callbacks); // View#subscribe takes the connection as one of the options in the last // argument subHandle = self.view.subscribe.call(self.view, args, { connection: connection }); if (! _.has(subHandles, subHandle.subscriptionId)) { subHandles[subHandle.subscriptionId] = subHandle; // Adding a new subscription will always cause us to transition from ready // to not ready, but if we are already not ready then this can't make us // ready. if (self._allSubsReady) { self._allSubsReadyDep.changed(); } } return subHandle; }; /** * @summary A reactive function that returns true when all of the subscriptions * called with [this.subscribe](#TemplateInstance-subscribe) are ready. * @return {Boolean} True if all subscriptions on this template instance are * ready. */ Blaze.TemplateInstance.prototype.subscriptionsReady = function () { this._allSubsReadyDep.depend(); this._allSubsReady = _.all(this._subscriptionHandles, function (handle) { return handle.ready(); }); return this._allSubsReady; }; /** * @summary Specify template helpers available to this template. * @locus Client * @param {Object} helpers Dictionary of helper functions by name. * @importFromPackage templating */ Template.prototype.helpers = function (dict) { if (! _.isObject(dict)) { throw new Error("Helpers dictionary has to be an object"); } for (var k in dict) this.__helpers.set(k, dict[k]); }; // Kind of like Blaze.currentView but for the template instance. // This is a function, not a value -- so that not all helpers // are implicitly dependent on the current template instance's `data` property, // which would make them dependenct on the data context of the template // inclusion. Template._currentTemplateInstanceFunc = null; Template._withTemplateInstanceFunc = function (templateInstanceFunc, func) { if (typeof func !== 'function') throw new Error("Expected function, got: " + func); var oldTmplInstanceFunc = Template._currentTemplateInstanceFunc; try { Template._currentTemplateInstanceFunc = templateInstanceFunc; return func(); } finally { Template._currentTemplateInstanceFunc = oldTmplInstanceFunc; } }; /** * @summary Specify event handlers for this template. * @locus Client * @param {EventMap} eventMap Event handlers to associate with this template. * @importFromPackage templating */ Template.prototype.events = function (eventMap) { if (! _.isObject(eventMap)) { throw new Error("Event map has to be an object"); } var template = this; var eventMap2 = {}; for (var k in eventMap) { eventMap2[k] = (function (k, v) { return function (event/*, ...*/) { var view = this; // passed by EventAugmenter var data = Blaze.getData(event.currentTarget); if (data == null) data = {}; var args = Array.prototype.slice.call(arguments); var tmplInstanceFunc = _.bind(view.templateInstance, view); args.splice(1, 0, tmplInstanceFunc()); return Template._withTemplateInstanceFunc(tmplInstanceFunc, function () { return v.apply(data, args); }); }; })(k, eventMap[k]); } template.__eventMaps.push(eventMap2); }; /** * @function * @name instance * @memberOf Template * @summary The [template instance](#template_inst) corresponding to the current template helper, event handler, callback, or autorun. If there isn't one, `null`. * @locus Client * @returns {Blaze.TemplateInstance} * @importFromPackage templating */ Template.instance = function () { return Template._currentTemplateInstanceFunc && Template._currentTemplateInstanceFunc(); }; // Note: Template.currentData() is documented to take zero arguments, // while Blaze.getData takes up to one. /** * @summary * * - Inside an `onCreated`, `onRendered`, or `onDestroyed` callback, returns * the data context of the template. * - Inside an event handler, returns the data context of the template on which * this event handler was defined. * - Inside a helper, returns the data context of the DOM node where the helper * was used. * * Establishes a reactive dependency on the result. * @locus Client * @function * @importFromPackage templating */ Template.currentData = Blaze.getData; /** * @summary Accesses other data contexts that enclose the current data context. * @locus Client * @function * @param {Integer} [numLevels] The number of levels beyond the current data context to look. Defaults to 1. * @importFromPackage templating */ Template.parentData = Blaze._parentData; /** * @summary Defines a [helper function](#template_helpers) which can be used from all templates. * @locus Client * @function * @param {String} name The name of the helper function you are defining. * @param {Function} function The helper function itself. * @importFromPackage templating */ Template.registerHelper = Blaze.registerHelper; /** * @summary Removes a global [helper function](#template_helpers). * @locus Client * @function * @param {String} name The name of the helper function you are defining. * @importFromPackage templating */ Template.deregisterHelper = Blaze.deregisterHelper; ================================================ FILE: packages/blaze/view.js ================================================ /// [new] Blaze.View([name], renderMethod) /// /// Blaze.View is the building block of reactive DOM. Views have /// the following features: /// /// * lifecycle callbacks - Views are created, rendered, and destroyed, /// and callbacks can be registered to fire when these things happen. /// /// * parent pointer - A View points to its parentView, which is the /// View that caused it to be rendered. These pointers form a /// hierarchy or tree of Views. /// /// * render() method - A View's render() method specifies the DOM /// (or HTML) content of the View. If the method establishes /// reactive dependencies, it may be re-run. /// /// * a DOMRange - If a View is rendered to DOM, its position and /// extent in the DOM are tracked using a DOMRange object. /// /// When a View is constructed by calling Blaze.View, the View is /// not yet considered "created." It doesn't have a parentView yet, /// and no logic has been run to initialize the View. All real /// work is deferred until at least creation time, when the onViewCreated /// callbacks are fired, which happens when the View is "used" in /// some way that requires it to be rendered. /// /// ...more lifecycle stuff /// /// `name` is an optional string tag identifying the View. The only /// time it's used is when looking in the View tree for a View of a /// particular name; for example, data contexts are stored on Views /// of name "with". Names are also useful when debugging, so in /// general it's good for functions that create Views to set the name. /// Views associated with templates have names of the form "Template.foo". /** * @class * @summary Constructor for a View, which represents a reactive region of DOM. * @locus Client * @param {String} [name] Optional. A name for this type of View. See [`view.name`](#view_name). * @param {Function} renderFunction A function that returns [*renderable content*](#renderable_content). In this function, `this` is bound to the View. */ Blaze.View = function (name, render) { if (! (this instanceof Blaze.View)) // called without `new` return new Blaze.View(name, render); if (typeof name === 'function') { // omitted "name" argument render = name; name = ''; } this.name = name; this._render = render; this._callbacks = { created: null, rendered: null, destroyed: null }; // Setting all properties here is good for readability, // and also may help Chrome optimize the code by keeping // the View object from changing shape too much. this.isCreated = false; this._isCreatedForExpansion = false; this.isRendered = false; this._isAttached = false; this.isDestroyed = false; this._isInRender = false; this.parentView = null; this._domrange = null; // This flag is normally set to false except for the cases when view's parent // was generated as part of expanding some syntactic sugar expressions or // methods. // Ex.: Blaze.renderWithData is an equivalent to creating a view with regular // Blaze.render and wrapping it into {{#with data}}{{/with}} view. Since the // users don't know anything about these generated parent views, Blaze needs // this information to be available on views to make smarter decisions. For // example: removing the generated parent view with the view on Blaze.remove. this._hasGeneratedParent = false; // Bindings accessible to children views (via view.lookup('name')) within the // closest template view. this._scopeBindings = {}; this.renderCount = 0; }; Blaze.View.prototype._render = function () { return null; }; Blaze.View.prototype.onViewCreated = function (cb) { this._callbacks.created = this._callbacks.created || []; this._callbacks.created.push(cb); }; Blaze.View.prototype._onViewRendered = function (cb) { this._callbacks.rendered = this._callbacks.rendered || []; this._callbacks.rendered.push(cb); }; Blaze.View.prototype.onViewReady = function (cb) { var self = this; var fire = function () { Tracker.afterFlush(function () { if (! self.isDestroyed) { Blaze._withCurrentView(self, function () { cb.call(self); }); } }); }; self._onViewRendered(function onViewRendered() { if (self.isDestroyed) return; if (! self._domrange.attached) self._domrange.onAttached(fire); else fire(); }); }; Blaze.View.prototype.onViewDestroyed = function (cb) { this._callbacks.destroyed = this._callbacks.destroyed || []; this._callbacks.destroyed.push(cb); }; Blaze.View.prototype.removeViewDestroyedListener = function (cb) { var destroyed = this._callbacks.destroyed; if (! destroyed) return; var index = _.lastIndexOf(destroyed, cb); if (index !== -1) { // XXX You'd think the right thing to do would be splice, but _fireCallbacks // gets sad if you remove callbacks while iterating over the list. Should // change this to use callback-hook or EventEmitter or something else that // properly supports removal. destroyed[index] = null; } }; /// View#autorun(func) /// /// Sets up a Tracker autorun that is "scoped" to this View in two /// important ways: 1) Blaze.currentView is automatically set /// on every re-run, and 2) the autorun is stopped when the /// View is destroyed. As with Tracker.autorun, the first run of /// the function is immediate, and a Computation object that can /// be used to stop the autorun is returned. /// /// View#autorun is meant to be called from View callbacks like /// onViewCreated, or from outside the rendering process. It may not /// be called before the onViewCreated callbacks are fired (too early), /// or from a render() method (too confusing). /// /// Typically, autoruns that update the state /// of the View (as in Blaze.With) should be started from an onViewCreated /// callback. Autoruns that update the DOM should be started /// from either onViewCreated (guarded against the absence of /// view._domrange), or onViewReady. Blaze.View.prototype.autorun = function (f, _inViewScope, displayName) { var self = this; // The restrictions on when View#autorun can be called are in order // to avoid bad patterns, like creating a Blaze.View and immediately // calling autorun on it. A freshly created View is not ready to // have logic run on it; it doesn't have a parentView, for example. // It's when the View is materialized or expanded that the onViewCreated // handlers are fired and the View starts up. // // Letting the render() method call `this.autorun()` is problematic // because of re-render. The best we can do is to stop the old // autorun and start a new one for each render, but that's a pattern // we try to avoid internally because it leads to helpers being // called extra times, in the case where the autorun causes the // view to re-render (and thus the autorun to be torn down and a // new one established). // // We could lift these restrictions in various ways. One interesting // idea is to allow you to call `view.autorun` after instantiating // `view`, and automatically wrap it in `view.onViewCreated`, deferring // the autorun so that it starts at an appropriate time. However, // then we can't return the Computation object to the caller, because // it doesn't exist yet. if (! self.isCreated) { throw new Error("View#autorun must be called from the created callback at the earliest"); } if (this._isInRender) { throw new Error("Can't call View#autorun from inside render(); try calling it from the created or rendered callback"); } if (Tracker.active) { throw new Error("Can't call View#autorun from a Tracker Computation; try calling it from the created or rendered callback"); } var templateInstanceFunc = Blaze.Template._currentTemplateInstanceFunc; var func = function viewAutorun(c) { return Blaze._withCurrentView(_inViewScope || self, function () { return Blaze.Template._withTemplateInstanceFunc( templateInstanceFunc, function () { return f.call(self, c); }); }); }; // Give the autorun function a better name for debugging and profiling. // The `displayName` property is not part of the spec but browsers like Chrome // and Firefox prefer it in debuggers over the name function was declared by. func.displayName = (self.name || 'anonymous') + ':' + (displayName || 'anonymous'); var comp = Tracker.autorun(func); var stopComputation = function () { comp.stop(); }; self.onViewDestroyed(stopComputation); comp.onStop(function () { self.removeViewDestroyedListener(stopComputation); }); return comp; }; Blaze.View.prototype._errorIfShouldntCallSubscribe = function () { var self = this; if (! self.isCreated) { throw new Error("View#subscribe must be called from the created callback at the earliest"); } if (self._isInRender) { throw new Error("Can't call View#subscribe from inside render(); try calling it from the created or rendered callback"); } if (self.isDestroyed) { throw new Error("Can't call View#subscribe from inside the destroyed callback, try calling it inside created or rendered."); } }; /** * Just like Blaze.View#autorun, but with Meteor.subscribe instead of * Tracker.autorun. Stop the subscription when the view is destroyed. * @return {SubscriptionHandle} A handle to the subscription so that you can * see if it is ready, or stop it manually */ Blaze.View.prototype.subscribe = function (args, options) { var self = this; options = options || {}; self._errorIfShouldntCallSubscribe(); var subHandle; if (options.connection) { subHandle = options.connection.subscribe.apply(options.connection, args); } else { subHandle = Meteor.subscribe.apply(Meteor, args); } self.onViewDestroyed(function () { subHandle.stop(); }); return subHandle; }; Blaze.View.prototype.firstNode = function () { if (! this._isAttached) throw new Error("View must be attached before accessing its DOM"); return this._domrange.firstNode(); }; Blaze.View.prototype.lastNode = function () { if (! this._isAttached) throw new Error("View must be attached before accessing its DOM"); return this._domrange.lastNode(); }; Blaze._fireCallbacks = function (view, which) { Blaze._withCurrentView(view, function () { Tracker.nonreactive(function fireCallbacks() { var cbs = view._callbacks[which]; for (var i = 0, N = (cbs && cbs.length); i < N; i++) cbs[i] && cbs[i].call(view); }); }); }; Blaze._createView = function (view, parentView, forExpansion) { if (view.isCreated) throw new Error("Can't render the same View twice"); view.parentView = (parentView || null); view.isCreated = true; if (forExpansion) view._isCreatedForExpansion = true; Blaze._fireCallbacks(view, 'created'); }; var doFirstRender = function (view, initialContent) { var domrange = new Blaze._DOMRange(initialContent); view._domrange = domrange; domrange.view = view; view.isRendered = true; Blaze._fireCallbacks(view, 'rendered'); var teardownHook = null; domrange.onAttached(function attached(range, element) { view._isAttached = true; teardownHook = Blaze._DOMBackend.Teardown.onElementTeardown( element, function teardown() { Blaze._destroyView(view, true /* _skipNodes */); }); }); // tear down the teardown hook view.onViewDestroyed(function () { teardownHook && teardownHook.stop(); teardownHook = null; }); return domrange; }; // Take an uncreated View `view` and create and render it to DOM, // setting up the autorun that updates the View. Returns a new // DOMRange, which has been associated with the View. // // The private arguments `_workStack` and `_intoArray` are passed in // by Blaze._materializeDOM and are only present for recursive calls // (when there is some other _materializeView on the stack). If // provided, then we avoid the mutual recursion of calling back into // Blaze._materializeDOM so that deep View hierarchies don't blow the // stack. Instead, we push tasks onto workStack for the initial // rendering and subsequent setup of the View, and they are done after // we return. When there is a _workStack, we do not return the new // DOMRange, but instead push it into _intoArray from a _workStack // task. Blaze._materializeView = function (view, parentView, _workStack, _intoArray) { Blaze._createView(view, parentView); var domrange; var lastHtmljs; // We don't expect to be called in a Computation, but just in case, // wrap in Tracker.nonreactive. Tracker.nonreactive(function () { view.autorun(function doRender(c) { // `view.autorun` sets the current view. view.renderCount++; view._isInRender = true; // Any dependencies that should invalidate this Computation come // from this line: var htmljs = view._render(); view._isInRender = false; if (! c.firstRun) { Tracker.nonreactive(function doMaterialize() { // re-render var rangesAndNodes = Blaze._materializeDOM(htmljs, [], view); if (! Blaze._isContentEqual(lastHtmljs, htmljs)) { domrange.setMembers(rangesAndNodes); Blaze._fireCallbacks(view, 'rendered'); } }); } lastHtmljs = htmljs; // Causes any nested views to stop immediately, not when we call // `setMembers` the next time around the autorun. Otherwise, // helpers in the DOM tree to be replaced might be scheduled // to re-run before we have a chance to stop them. Tracker.onInvalidate(function () { if (domrange) { domrange.destroyMembers(); } }); }, undefined, 'materialize'); // first render. lastHtmljs is the first htmljs. var initialContents; if (! _workStack) { initialContents = Blaze._materializeDOM(lastHtmljs, [], view); domrange = doFirstRender(view, initialContents); initialContents = null; // help GC because we close over this scope a lot } else { // We're being called from Blaze._materializeDOM, so to avoid // recursion and save stack space, provide a description of the // work to be done instead of doing it. Tasks pushed onto // _workStack will be done in LIFO order after we return. // The work will still be done within a Tracker.nonreactive, // because it will be done by some call to Blaze._materializeDOM // (which is always called in a Tracker.nonreactive). initialContents = []; // push this function first so that it happens last _workStack.push(function () { domrange = doFirstRender(view, initialContents); initialContents = null; // help GC because of all the closures here _intoArray.push(domrange); }); // now push the task that calculates initialContents _workStack.push(_.bind(Blaze._materializeDOM, null, lastHtmljs, initialContents, view, _workStack)); } }); if (! _workStack) { return domrange; } else { return null; } }; // Expands a View to HTMLjs, calling `render` recursively on all // Views and evaluating any dynamic attributes. Calls the `created` // callback, but not the `materialized` or `rendered` callbacks. // Destroys the view immediately, unless called in a Tracker Computation, // in which case the view will be destroyed when the Computation is // invalidated. If called in a Tracker Computation, the result is a // reactive string; that is, the Computation will be invalidated // if any changes are made to the view or subviews that might affect // the HTML. Blaze._expandView = function (view, parentView) { Blaze._createView(view, parentView, true /*forExpansion*/); view._isInRender = true; var htmljs = Blaze._withCurrentView(view, function () { return view._render(); }); view._isInRender = false; var result = Blaze._expand(htmljs, view); if (Tracker.active) { Tracker.onInvalidate(function () { Blaze._destroyView(view); }); } else { Blaze._destroyView(view); } return result; }; // Options: `parentView` Blaze._HTMLJSExpander = HTML.TransformingVisitor.extend(); Blaze._HTMLJSExpander.def({ visitObject: function (x) { if (x instanceof Blaze.Template) x = x.constructView(); if (x instanceof Blaze.View) return Blaze._expandView(x, this.parentView); // this will throw an error; other objects are not allowed! return HTML.TransformingVisitor.prototype.visitObject.call(this, x); }, visitAttributes: function (attrs) { // expand dynamic attributes if (typeof attrs === 'function') attrs = Blaze._withCurrentView(this.parentView, attrs); // call super (e.g. for case where `attrs` is an array) return HTML.TransformingVisitor.prototype.visitAttributes.call(this, attrs); }, visitAttribute: function (name, value, tag) { // expand attribute values that are functions. Any attribute value // that contains Views must be wrapped in a function. if (typeof value === 'function') value = Blaze._withCurrentView(this.parentView, value); return HTML.TransformingVisitor.prototype.visitAttribute.call( this, name, value, tag); } }); // Return Blaze.currentView, but only if it is being rendered // (i.e. we are in its render() method). var currentViewIfRendering = function () { var view = Blaze.currentView; return (view && view._isInRender) ? view : null; }; Blaze._expand = function (htmljs, parentView) { parentView = parentView || currentViewIfRendering(); return (new Blaze._HTMLJSExpander( {parentView: parentView})).visit(htmljs); }; Blaze._expandAttributes = function (attrs, parentView) { parentView = parentView || currentViewIfRendering(); return (new Blaze._HTMLJSExpander( {parentView: parentView})).visitAttributes(attrs); }; Blaze._destroyView = function (view, _skipNodes) { if (view.isDestroyed) return; view.isDestroyed = true; Blaze._fireCallbacks(view, 'destroyed'); // Destroy views and elements recursively. If _skipNodes, // only recurse up to views, not elements, for the case where // the backend (jQuery) is recursing over the elements already. if (view._domrange) view._domrange.destroyMembers(_skipNodes); }; Blaze._destroyNode = function (node) { if (node.nodeType === 1) Blaze._DOMBackend.Teardown.tearDownElement(node); }; // Are the HTMLjs entities `a` and `b` the same? We could be // more elaborate here but the point is to catch the most basic // cases. Blaze._isContentEqual = function (a, b) { if (a instanceof HTML.Raw) { return (b instanceof HTML.Raw) && (a.value === b.value); } else if (a == null) { return (b == null); } else { return (a === b) && ((typeof a === 'number') || (typeof a === 'boolean') || (typeof a === 'string')); } }; /** * @summary The View corresponding to the current template helper, event handler, callback, or autorun. If there isn't one, `null`. * @locus Client * @type {Blaze.View} */ Blaze.currentView = null; Blaze._withCurrentView = function (view, func) { var oldView = Blaze.currentView; try { Blaze.currentView = view; return func(); } finally { Blaze.currentView = oldView; } }; // Blaze.render publicly takes a View or a Template. // Privately, it takes any HTMLJS (extended with Views and Templates) // except null or undefined, or a function that returns any extended // HTMLJS. var checkRenderContent = function (content) { if (content === null) throw new Error("Can't render null"); if (typeof content === 'undefined') throw new Error("Can't render undefined"); if ((content instanceof Blaze.View) || (content instanceof Blaze.Template) || (typeof content === 'function')) return; try { // Throw if content doesn't look like HTMLJS at the top level // (i.e. verify that this is an HTML.Tag, or an array, // or a primitive, etc.) (new HTML.Visitor).visit(content); } catch (e) { // Make error message suitable for public API throw new Error("Expected Template or View"); } }; // For Blaze.render and Blaze.toHTML, take content and // wrap it in a View, unless it's a single View or // Template already. var contentAsView = function (content) { checkRenderContent(content); if (content instanceof Blaze.Template) { return content.constructView(); } else if (content instanceof Blaze.View) { return content; } else { var func = content; if (typeof func !== 'function') { func = function () { return content; }; } return Blaze.View('render', func); } }; // For Blaze.renderWithData and Blaze.toHTMLWithData, wrap content // in a function, if necessary, so it can be a content arg to // a Blaze.With. var contentAsFunc = function (content) { checkRenderContent(content); if (typeof content !== 'function') { return function () { return content; }; } else { return content; } }; /** * @summary Renders a template or View to DOM nodes and inserts it into the DOM, returning a rendered [View](#blaze_view) which can be passed to [`Blaze.remove`](#blaze_remove). * @locus Client * @param {Template|Blaze.View} templateOrView The template (e.g. `Template.myTemplate`) or View object to render. If a template, a View object is [constructed](#template_constructview). If a View, it must be an unrendered View, which becomes a rendered View and is returned. * @param {DOMNode} parentNode The node that will be the parent of the rendered template. It must be an Element node. * @param {DOMNode} [nextNode] Optional. If provided, must be a child of parentNode; the template will be inserted before this node. If not provided, the template will be inserted as the last child of parentNode. * @param {Blaze.View} [parentView] Optional. If provided, it will be set as the rendered View's [`parentView`](#view_parentview). */ Blaze.render = function (content, parentElement, nextNode, parentView) { if (! parentElement) { Blaze._warn("Blaze.render without a parent element is deprecated. " + "You must specify where to insert the rendered content."); } if (nextNode instanceof Blaze.View) { // handle omitted nextNode parentView = nextNode; nextNode = null; } // parentElement must be a DOM node. in particular, can't be the // result of a call to `$`. Can't check if `parentElement instanceof // Node` since 'Node' is undefined in IE8. if (parentElement && typeof parentElement.nodeType !== 'number') throw new Error("'parentElement' must be a DOM node"); if (nextNode && typeof nextNode.nodeType !== 'number') // 'nextNode' is optional throw new Error("'nextNode' must be a DOM node"); parentView = parentView || currentViewIfRendering(); var view = contentAsView(content); Blaze._materializeView(view, parentView); if (parentElement) { view._domrange.attach(parentElement, nextNode); } return view; }; Blaze.insert = function (view, parentElement, nextNode) { Blaze._warn("Blaze.insert has been deprecated. Specify where to insert the " + "rendered content in the call to Blaze.render."); if (! (view && (view._domrange instanceof Blaze._DOMRange))) throw new Error("Expected template rendered with Blaze.render"); view._domrange.attach(parentElement, nextNode); }; /** * @summary Renders a template or View to DOM nodes with a data context. Otherwise identical to `Blaze.render`. * @locus Client * @param {Template|Blaze.View} templateOrView The template (e.g. `Template.myTemplate`) or View object to render. * @param {Object|Function} data The data context to use, or a function returning a data context. If a function is provided, it will be reactively re-run. * @param {DOMNode} parentNode The node that will be the parent of the rendered template. It must be an Element node. * @param {DOMNode} [nextNode] Optional. If provided, must be a child of parentNode; the template will be inserted before this node. If not provided, the template will be inserted as the last child of parentNode. * @param {Blaze.View} [parentView] Optional. If provided, it will be set as the rendered View's [`parentView`](#view_parentview). */ Blaze.renderWithData = function (content, data, parentElement, nextNode, parentView) { // We defer the handling of optional arguments to Blaze.render. At this point, // `nextNode` may actually be `parentView`. return Blaze.render(Blaze._TemplateWith(data, contentAsFunc(content)), parentElement, nextNode, parentView); }; /** * @summary Removes a rendered View from the DOM, stopping all reactive updates and event listeners on it. Also destroys the Blaze.Template instance associated with the view. * @locus Client * @param {Blaze.View} renderedView The return value from `Blaze.render` or `Blaze.renderWithData`, or the `view` property of a Blaze.Template instance. Calling `Blaze.remove(Template.instance().view)` from within a template event handler will destroy the view as well as that template and trigger the template's `onDestroyed` handlers. */ Blaze.remove = function (view) { if (! (view && (view._domrange instanceof Blaze._DOMRange))) throw new Error("Expected template rendered with Blaze.render"); while (view) { if (! view.isDestroyed) { var range = view._domrange; if (range.attached && ! range.parentRange) range.detach(); range.destroy(); } view = view._hasGeneratedParent && view.parentView; } }; /** * @summary Renders a template or View to a string of HTML. * @locus Client * @param {Template|Blaze.View} templateOrView The template (e.g. `Template.myTemplate`) or View object from which to generate HTML. */ Blaze.toHTML = function (content, parentView) { parentView = parentView || currentViewIfRendering(); return HTML.toHTML(Blaze._expandView(contentAsView(content), parentView)); }; /** * @summary Renders a template or View to HTML with a data context. Otherwise identical to `Blaze.toHTML`. * @locus Client * @param {Template|Blaze.View} templateOrView The template (e.g. `Template.myTemplate`) or View object from which to generate HTML. * @param {Object|Function} data The data context to use, or a function returning a data context. */ Blaze.toHTMLWithData = function (content, data, parentView) { parentView = parentView || currentViewIfRendering(); return HTML.toHTML(Blaze._expandView(Blaze._TemplateWith( data, contentAsFunc(content)), parentView)); }; Blaze._toText = function (htmljs, parentView, textMode) { if (typeof htmljs === 'function') throw new Error("Blaze._toText doesn't take a function, just HTMLjs"); if ((parentView != null) && ! (parentView instanceof Blaze.View)) { // omitted parentView argument textMode = parentView; parentView = null; } parentView = parentView || currentViewIfRendering(); if (! textMode) throw new Error("textMode required"); if (! (textMode === HTML.TEXTMODE.STRING || textMode === HTML.TEXTMODE.RCDATA || textMode === HTML.TEXTMODE.ATTRIBUTE)) throw new Error("Unknown textMode: " + textMode); return HTML.toText(Blaze._expand(htmljs, parentView), textMode); }; /** * @summary Returns the current data context, or the data context that was used when rendering a particular DOM element or View from a Meteor template. * @locus Client * @param {DOMElement|Blaze.View} [elementOrView] Optional. An element that was rendered by a Meteor, or a View. */ Blaze.getData = function (elementOrView) { var theWith; if (! elementOrView) { theWith = Blaze.getView('with'); } else if (elementOrView instanceof Blaze.View) { var view = elementOrView; theWith = (view.name === 'with' ? view : Blaze.getView(view, 'with')); } else if (typeof elementOrView.nodeType === 'number') { if (elementOrView.nodeType !== 1) throw new Error("Expected DOM element"); theWith = Blaze.getView(elementOrView, 'with'); } else { throw new Error("Expected DOM element or View"); } return theWith ? theWith.dataVar.get() : null; }; // For back-compat Blaze.getElementData = function (element) { Blaze._warn("Blaze.getElementData has been deprecated. Use " + "Blaze.getData(element) instead."); if (element.nodeType !== 1) throw new Error("Expected DOM element"); return Blaze.getData(element); }; // Both arguments are optional. /** * @summary Gets either the current View, or the View enclosing the given DOM element. * @locus Client * @param {DOMElement} [element] Optional. If specified, the View enclosing `element` is returned. */ Blaze.getView = function (elementOrView, _viewName) { var viewName = _viewName; if ((typeof elementOrView) === 'string') { // omitted elementOrView; viewName present viewName = elementOrView; elementOrView = null; } // We could eventually shorten the code by folding the logic // from the other methods into this method. if (! elementOrView) { return Blaze._getCurrentView(viewName); } else if (elementOrView instanceof Blaze.View) { return Blaze._getParentView(elementOrView, viewName); } else if (typeof elementOrView.nodeType === 'number') { return Blaze._getElementView(elementOrView, viewName); } else { throw new Error("Expected DOM element or View"); } }; // Gets the current view or its nearest ancestor of name // `name`. Blaze._getCurrentView = function (name) { var view = Blaze.currentView; // Better to fail in cases where it doesn't make sense // to use Blaze._getCurrentView(). There will be a current // view anywhere it does. You can check Blaze.currentView // if you want to know whether there is one or not. if (! view) throw new Error("There is no current view"); if (name) { while (view && view.name !== name) view = view.parentView; return view || null; } else { // Blaze._getCurrentView() with no arguments just returns // Blaze.currentView. return view; } }; Blaze._getParentView = function (view, name) { var v = view.parentView; if (name) { while (v && v.name !== name) v = v.parentView; } return v || null; }; Blaze._getElementView = function (elem, name) { var range = Blaze._DOMRange.forElement(elem); var view = null; while (range && ! view) { view = (range.view || null); if (! view) { if (range.parentRange) range = range.parentRange; else range = Blaze._DOMRange.forElement(range.parentElement); } } if (name) { while (view && view.name !== name) view = view.parentView; return view || null; } else { return view; } }; Blaze._addEventMap = function (view, eventMap, thisInHandler) { thisInHandler = (thisInHandler || null); var handles = []; if (! view._domrange) throw new Error("View must have a DOMRange"); view._domrange.onAttached(function attached_eventMaps(range, element) { _.each(eventMap, function (handler, spec) { var clauses = spec.split(/,\s+/); // iterate over clauses of spec, e.g. ['click .foo', 'click .bar'] _.each(clauses, function (clause) { var parts = clause.split(/\s+/); if (parts.length === 0) return; var newEvents = parts.shift(); var selector = parts.join(' '); handles.push(Blaze._EventSupport.listen( element, newEvents, selector, function (evt) { if (! range.containsElement(evt.currentTarget)) return null; var handlerThis = thisInHandler || this; var handlerArgs = arguments; return Blaze._withCurrentView(view, function () { return handler.apply(handlerThis, handlerArgs); }); }, range, function (r) { return r.parentRange; })); }); }); }); view.onViewDestroyed(function () { _.each(handles, function (h) { h.stop(); }); handles.length = 0; }); }; ================================================ FILE: packages/blaze/view_tests.js ================================================ if (Meteor.isClient) { Tinytest.add("blaze - view - callbacks", function (test) { var R = ReactiveVar('foo'); var buf = ''; var v = Blaze.View(function () { return R.get(); }); v.onViewCreated(function () { buf += 'c' + v.renderCount; }); v._onViewRendered(function () { buf += 'r' + v.renderCount; }); v.onViewReady(function () { buf += 'y' + v.renderCount; }); v.onViewDestroyed(function () { buf += 'd' + v.renderCount; }); test.equal(buf, ''); var div = document.createElement("DIV"); test.isFalse(v.isRendered); test.isFalse(v._isAttached); test.equal(canonicalizeHtml(div.innerHTML), ""); test.throws(function () { v.firstNode(); }, /View must be attached/); test.throws(function () { v.lastNode(); }, /View must be attached/); Blaze.render(v, div); test.equal(buf, 'c0r1'); test.equal(typeof (v.firstNode().nodeType), "number"); test.equal(typeof (v.lastNode().nodeType), "number"); test.isTrue(v.isRendered); test.isTrue(v._isAttached); test.equal(buf, 'c0r1'); test.equal(canonicalizeHtml(div.innerHTML), "foo"); Tracker.flush(); test.equal(buf, 'c0r1y1'); R.set("bar"); Tracker.flush(); test.equal(buf, 'c0r1y1r2y2'); test.equal(canonicalizeHtml(div.innerHTML), "bar"); Blaze.remove(v); test.equal(buf, 'c0r1y1r2y2d2'); test.equal(canonicalizeHtml(div.innerHTML), ""); buf = ""; R.set("baz"); Tracker.flush(); test.equal(buf, ""); }); } ================================================ FILE: packages/blaze-html-templates/README.md ================================================ # blaze-html-templates A meta-package that includes everything you need to compile and run Meteor templates with Spacebars and Blaze. For more details, see the documentation of the component packages: - [templating](https://atmospherejs.com/meteor/templating): compiles `.html` files - [blaze](https://atmospherejs.com/meteor/blaze): the runtime library - [spacebars](https://atmospherejs.com/meteor/spacebars): the templating language ================================================ FILE: packages/blaze-html-templates/package.js ================================================ Package.describe({ name: 'blaze-html-templates', version: '1.0.4', // Brief, one-line summary of the package. summary: 'Compile HTML templates into reactive UI with Meteor Blaze', // By default, Meteor will default to using README.md for documentation. // To avoid submitting documentation, set this field to null. documentation: 'README.md' }); Package.onUse(function(api) { api.imply([ // A library for reactive user interfaces 'blaze', // The following packages are basically empty shells that just exist to // satisfy code checking for the existence of a package. Rest assured that // they are not adding any bloat to your bundle. 'ui', // XXX COMPAT WITH PACKAGES BUILT FOR 0.9.0. 'spacebars', // XXX COMPAT WITH PACKAGES BUILT FOR 0.9.0 // Compile .html files into Blaze reactive views 'templating' ]); }); ================================================ FILE: packages/blaze-tools/.gitignore ================================================ .build* ================================================ FILE: packages/blaze-tools/README.md ================================================ # blaze-tools Compile-time utilities that are likely to be useful to any package that compiles templates for Blaze. ================================================ FILE: packages/blaze-tools/package.js ================================================ Package.describe({ summary: "Compile-time tools for Blaze", version: '1.0.9' }); Package.onUse(function (api) { api.export('BlazeTools'); api.use('htmljs'); api.use('underscore'); api.addFiles(['preamble.js', 'tokens.js', 'tojs.js']); }); Package.onTest(function (api) { api.use('blaze-tools'); api.use('tinytest'); api.use('underscore'); api.use('html-tools'); api.addFiles(['token_tests.js']); }); ================================================ FILE: packages/blaze-tools/preamble.js ================================================ BlazeTools = {}; ================================================ FILE: packages/blaze-tools/tojs.js ================================================ BlazeTools.EmitCode = function (value) { if (! (this instanceof BlazeTools.EmitCode)) // called without `new` return new BlazeTools.EmitCode(value); if (typeof value !== 'string') throw new Error('BlazeTools.EmitCode must be constructed with a string'); this.value = value; }; BlazeTools.EmitCode.prototype.toJS = function (visitor) { return this.value; }; // Turns any JSONable value into a JavaScript literal. toJSLiteral = function (obj) { // See for `\u2028\u2029`. // Also escape Unicode surrogates. return (JSON.stringify(obj) .replace(/[\u2028\u2029\ud800-\udfff]/g, function (c) { return '\\u' + ('000' + c.charCodeAt(0).toString(16)).slice(-4); })); }; BlazeTools.toJSLiteral = toJSLiteral; var jsReservedWordSet = (function (set) { _.each("abstract else instanceof super boolean enum int switch break export interface synchronized byte extends let this case false long throw catch final native throws char finally new transient class float null true const for package try continue function private typeof debugger goto protected var default if public void delete implements return volatile do import short while double in static with".split(' '), function (w) { set[w] = 1; }); return set; })({}); toObjectLiteralKey = function (k) { if (/^[a-zA-Z$_][a-zA-Z$0-9_]*$/.test(k) && jsReservedWordSet[k] !== 1) return k; return toJSLiteral(k); }; BlazeTools.toObjectLiteralKey = toObjectLiteralKey; var hasToJS = function (x) { return x.toJS && (typeof (x.toJS) === 'function'); }; ToJSVisitor = HTML.Visitor.extend(); ToJSVisitor.def({ visitNull: function (nullOrUndefined) { return 'null'; }, visitPrimitive: function (stringBooleanOrNumber) { return toJSLiteral(stringBooleanOrNumber); }, visitArray: function (array) { var parts = []; for (var i = 0; i < array.length; i++) parts.push(this.visit(array[i])); return '[' + parts.join(', ') + ']'; }, visitTag: function (tag) { return this.generateCall(tag.tagName, tag.attrs, tag.children); }, visitComment: function (comment) { return this.generateCall('HTML.Comment', null, [comment.value]); }, visitCharRef: function (charRef) { return this.generateCall('HTML.CharRef', {html: charRef.html, str: charRef.str}); }, visitRaw: function (raw) { return this.generateCall('HTML.Raw', null, [raw.value]); }, visitObject: function (x) { if (hasToJS(x)) { return x.toJS(this); } throw new Error("Unexpected object in HTMLjs in toJS: " + x); }, generateCall: function (name, attrs, children) { var tagSymbol; if (name.indexOf('.') >= 0) { tagSymbol = name; } else if (HTML.isTagEnsured(name)) { tagSymbol = 'HTML.' + HTML.getSymbolName(name); } else { tagSymbol = 'HTML.getTag(' + toJSLiteral(name) + ')'; } var attrsArray = null; if (attrs) { attrsArray = []; var needsHTMLAttrs = false; if (HTML.isArray(attrs)) { var attrsArray = []; for (var i = 0; i < attrs.length; i++) { var a = attrs[i]; if (hasToJS(a)) { attrsArray.push(a.toJS(this)); needsHTMLAttrs = true; } else { var attrsObjStr = this.generateAttrsDictionary(attrs[i]); if (attrsObjStr !== null) attrsArray.push(attrsObjStr); } } } else if (hasToJS(attrs)) { attrsArray.push(attrs.toJS(this)); needsHTMLAttrs = true; } else { attrsArray.push(this.generateAttrsDictionary(attrs)); } } var attrsStr = null; if (attrsArray && attrsArray.length) { if (attrsArray.length === 1 && ! needsHTMLAttrs) { attrsStr = attrsArray[0]; } else { attrsStr = 'HTML.Attrs(' + attrsArray.join(', ') + ')'; } } var argStrs = []; if (attrsStr !== null) argStrs.push(attrsStr); if (children) { for (var i = 0; i < children.length; i++) argStrs.push(this.visit(children[i])); } return tagSymbol + '(' + argStrs.join(', ') + ')'; }, generateAttrsDictionary: function (attrsDict) { if (attrsDict.toJS && (typeof (attrsDict.toJS) === 'function')) { // not an attrs dictionary, but something else! Like a template tag. return attrsDict.toJS(this); } var kvStrs = []; for (var k in attrsDict) { if (! HTML.isNully(attrsDict[k])) kvStrs.push(toObjectLiteralKey(k) + ': ' + this.visit(attrsDict[k])); } if (kvStrs.length) return '{' + kvStrs.join(', ') + '}'; return null; } }); BlazeTools.ToJSVisitor = ToJSVisitor; BlazeTools.toJS = function (content) { return (new ToJSVisitor).visit(content); }; ================================================ FILE: packages/blaze-tools/token_tests.js ================================================ Tinytest.add("blaze-tools - token parsers", function (test) { var run = function (func, input, expected) { var scanner = new HTMLTools.Scanner('z' + input); // make sure the parse function respects `scanner.pos` scanner.pos = 1; var result = func(scanner); if (expected === null) { test.equal(scanner.pos, 1); test.equal(result, null); } else { test.isTrue(scanner.isEOF()); test.equal(result, expected); } }; var runValue = function (func, input, expectedValue) { var expected; if (expectedValue === null) expected = null; else expected = { text: input, value: expectedValue }; run(func, input, expected); }; var parseNumber = BlazeTools.parseNumber; var parseIdentifierName = BlazeTools.parseIdentifierName; var parseExtendedIdentifierName = BlazeTools.parseExtendedIdentifierName; var parseStringLiteral = BlazeTools.parseStringLiteral; runValue(parseNumber, "0", 0); runValue(parseNumber, "-0", 0); runValue(parseNumber, "-", null); runValue(parseNumber, ".a", null); runValue(parseNumber, ".1", 0.1); runValue(parseNumber, "1.", 1); runValue(parseNumber, "1.1", 1.1); runValue(parseNumber, "0x", null); runValue(parseNumber, "0xa", 10); runValue(parseNumber, "-0xa", -10); runValue(parseNumber, "1e+1", 10); _.each([parseIdentifierName, parseExtendedIdentifierName], function (f) { run(f, "a", "a"); run(f, "true", "true"); run(f, "null", "null"); run(f, "if", "if"); run(f, "1", null); run(f, "1a", null); run(f, "+a", null); run(f, "a1", "a1"); run(f, "a1a", "a1a"); run(f, "_a8f_f8d88_", "_a8f_f8d88_"); }); run(parseIdentifierName, "@index", null); run(parseExtendedIdentifierName, "@index", "@index"); run(parseExtendedIdentifierName, "@something", "@something"); run(parseExtendedIdentifierName, "@", null); runValue(parseStringLiteral, '"a"', 'a'); runValue(parseStringLiteral, '"\'"', "'"); runValue(parseStringLiteral, '\'"\'', '"'); runValue(parseStringLiteral, '"a\\\nb"', 'ab'); // line continuation runValue(parseStringLiteral, '"a\u0062c"', 'abc'); // Note: IE 8 doesn't correctly parse '\v' in JavaScript. runValue(parseStringLiteral, '"\\0\\b\\f\\n\\r\\t\\v"', '\0\b\f\n\r\t\u000b'); runValue(parseStringLiteral, '"\\x41"', 'A'); runValue(parseStringLiteral, '"\\\\"', '\\'); runValue(parseStringLiteral, '"\\\""', '\"'); runValue(parseStringLiteral, '"\\\'"', '\''); runValue(parseStringLiteral, "'\\\\'", '\\'); runValue(parseStringLiteral, "'\\\"'", '\"'); runValue(parseStringLiteral, "'\\\''", '\''); test.throws(function () { run(parseStringLiteral, "'this is my string"); }, /Unterminated string literal/); }); ================================================ FILE: packages/blaze-tools/tokens.js ================================================ // Adapted from source code of http://xregexp.com/plugins/#unicode var unicodeCategories = { Ll: "0061-007A00B500DF-00F600F8-00FF01010103010501070109010B010D010F01110113011501170119011B011D011F01210123012501270129012B012D012F01310133013501370138013A013C013E014001420144014601480149014B014D014F01510153015501570159015B015D015F01610163016501670169016B016D016F0171017301750177017A017C017E-0180018301850188018C018D019201950199-019B019E01A101A301A501A801AA01AB01AD01B001B401B601B901BA01BD-01BF01C601C901CC01CE01D001D201D401D601D801DA01DC01DD01DF01E101E301E501E701E901EB01ED01EF01F001F301F501F901FB01FD01FF02010203020502070209020B020D020F02110213021502170219021B021D021F02210223022502270229022B022D022F02310233-0239023C023F0240024202470249024B024D024F-02930295-02AF037103730377037B-037D039003AC-03CE03D003D103D5-03D703D903DB03DD03DF03E103E303E503E703E903EB03ED03EF-03F303F503F803FB03FC0430-045F04610463046504670469046B046D046F04710473047504770479047B047D047F0481048B048D048F04910493049504970499049B049D049F04A104A304A504A704A904AB04AD04AF04B104B304B504B704B904BB04BD04BF04C204C404C604C804CA04CC04CE04CF04D104D304D504D704D904DB04DD04DF04E104E304E504E704E904EB04ED04EF04F104F304F504F704F904FB04FD04FF05010503050505070509050B050D050F05110513051505170519051B051D051F05210523052505270561-05871D00-1D2B1D6B-1D771D79-1D9A1E011E031E051E071E091E0B1E0D1E0F1E111E131E151E171E191E1B1E1D1E1F1E211E231E251E271E291E2B1E2D1E2F1E311E331E351E371E391E3B1E3D1E3F1E411E431E451E471E491E4B1E4D1E4F1E511E531E551E571E591E5B1E5D1E5F1E611E631E651E671E691E6B1E6D1E6F1E711E731E751E771E791E7B1E7D1E7F1E811E831E851E871E891E8B1E8D1E8F1E911E931E95-1E9D1E9F1EA11EA31EA51EA71EA91EAB1EAD1EAF1EB11EB31EB51EB71EB91EBB1EBD1EBF1EC11EC31EC51EC71EC91ECB1ECD1ECF1ED11ED31ED51ED71ED91EDB1EDD1EDF1EE11EE31EE51EE71EE91EEB1EED1EEF1EF11EF31EF51EF71EF91EFB1EFD1EFF-1F071F10-1F151F20-1F271F30-1F371F40-1F451F50-1F571F60-1F671F70-1F7D1F80-1F871F90-1F971FA0-1FA71FB0-1FB41FB61FB71FBE1FC2-1FC41FC61FC71FD0-1FD31FD61FD71FE0-1FE71FF2-1FF41FF61FF7210A210E210F2113212F21342139213C213D2146-2149214E21842C30-2C5E2C612C652C662C682C6A2C6C2C712C732C742C76-2C7B2C812C832C852C872C892C8B2C8D2C8F2C912C932C952C972C992C9B2C9D2C9F2CA12CA32CA52CA72CA92CAB2CAD2CAF2CB12CB32CB52CB72CB92CBB2CBD2CBF2CC12CC32CC52CC72CC92CCB2CCD2CCF2CD12CD32CD52CD72CD92CDB2CDD2CDF2CE12CE32CE42CEC2CEE2CF32D00-2D252D272D2DA641A643A645A647A649A64BA64DA64FA651A653A655A657A659A65BA65DA65FA661A663A665A667A669A66BA66DA681A683A685A687A689A68BA68DA68FA691A693A695A697A723A725A727A729A72BA72DA72F-A731A733A735A737A739A73BA73DA73FA741A743A745A747A749A74BA74DA74FA751A753A755A757A759A75BA75DA75FA761A763A765A767A769A76BA76DA76FA771-A778A77AA77CA77FA781A783A785A787A78CA78EA791A793A7A1A7A3A7A5A7A7A7A9A7FAFB00-FB06FB13-FB17FF41-FF5A", Lm: "02B0-02C102C6-02D102E0-02E402EC02EE0374037A0559064006E506E607F407F507FA081A0824082809710E460EC610FC17D718431AA71C78-1C7D1D2C-1D6A1D781D9B-1DBF2071207F2090-209C2C7C2C7D2D6F2E2F30053031-3035303B309D309E30FC-30FEA015A4F8-A4FDA60CA67FA717-A71FA770A788A7F8A7F9A9CFAA70AADDAAF3AAF4FF70FF9EFF9F", Lo: "00AA00BA01BB01C0-01C3029405D0-05EA05F0-05F20620-063F0641-064A066E066F0671-06D306D506EE06EF06FA-06FC06FF07100712-072F074D-07A507B107CA-07EA0800-08150840-085808A008A2-08AC0904-0939093D09500958-09610972-09770979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A390A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE00AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B830B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-0CB30CB5-0CB90CBD0CDE0CE00CE10CF10CF20D05-0D0C0D0E-0D100D12-0D3A0D3D0D4E0D600D610D7A-0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E450E810E820E840E870E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC40EDC-0EDF0F000F40-0F470F49-0F6C0F88-0F8C1000-102A103F1050-1055105A-105D106110651066106E-10701075-1081108E10D0-10FA10FD-1248124A-124D1250-12561258125A-125D1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-13151318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-17311740-17511760-176C176E-17701780-17B317DC1820-18421844-18771880-18A818AA18B0-18F51900-191C1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541B05-1B331B45-1B4B1B83-1BA01BAE1BAF1BBA-1BE51C00-1C231C4D-1C4F1C5A-1C771CE9-1CEC1CEE-1CF11CF51CF62135-21382D30-2D672D80-2D962DA0-2DA62DA8-2DAE2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE3006303C3041-3096309F30A1-30FA30FF3105-312D3131-318E31A0-31BA31F0-31FF3400-4DB54E00-9FCCA000-A014A016-A48CA4D0-A4F7A500-A60BA610-A61FA62AA62BA66EA6A0-A6E5A7FB-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2AA00-AA28AA40-AA42AA44-AA4BAA60-AA6FAA71-AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADBAADCAAE0-AAEAAAF2AB01-AB06AB09-AB0EAB11-AB16AB20-AB26AB28-AB2EABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA6DFA70-FAD9FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3DFD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF66-FF6FFF71-FF9DFFA0-FFBEFFC2-FFC7FFCA-FFCFFFD2-FFD7FFDA-FFDC", Lt: "01C501C801CB01F21F88-1F8F1F98-1F9F1FA8-1FAF1FBC1FCC1FFC", Lu: "0041-005A00C0-00D600D8-00DE01000102010401060108010A010C010E01100112011401160118011A011C011E01200122012401260128012A012C012E01300132013401360139013B013D013F0141014301450147014A014C014E01500152015401560158015A015C015E01600162016401660168016A016C016E017001720174017601780179017B017D018101820184018601870189-018B018E-0191019301940196-0198019C019D019F01A001A201A401A601A701A901AC01AE01AF01B1-01B301B501B701B801BC01C401C701CA01CD01CF01D101D301D501D701D901DB01DE01E001E201E401E601E801EA01EC01EE01F101F401F6-01F801FA01FC01FE02000202020402060208020A020C020E02100212021402160218021A021C021E02200222022402260228022A022C022E02300232023A023B023D023E02410243-02460248024A024C024E03700372037603860388-038A038C038E038F0391-03A103A3-03AB03CF03D2-03D403D803DA03DC03DE03E003E203E403E603E803EA03EC03EE03F403F703F903FA03FD-042F04600462046404660468046A046C046E04700472047404760478047A047C047E0480048A048C048E04900492049404960498049A049C049E04A004A204A404A604A804AA04AC04AE04B004B204B404B604B804BA04BC04BE04C004C104C304C504C704C904CB04CD04D004D204D404D604D804DA04DC04DE04E004E204E404E604E804EA04EC04EE04F004F204F404F604F804FA04FC04FE05000502050405060508050A050C050E05100512051405160518051A051C051E05200522052405260531-055610A0-10C510C710CD1E001E021E041E061E081E0A1E0C1E0E1E101E121E141E161E181E1A1E1C1E1E1E201E221E241E261E281E2A1E2C1E2E1E301E321E341E361E381E3A1E3C1E3E1E401E421E441E461E481E4A1E4C1E4E1E501E521E541E561E581E5A1E5C1E5E1E601E621E641E661E681E6A1E6C1E6E1E701E721E741E761E781E7A1E7C1E7E1E801E821E841E861E881E8A1E8C1E8E1E901E921E941E9E1EA01EA21EA41EA61EA81EAA1EAC1EAE1EB01EB21EB41EB61EB81EBA1EBC1EBE1EC01EC21EC41EC61EC81ECA1ECC1ECE1ED01ED21ED41ED61ED81EDA1EDC1EDE1EE01EE21EE41EE61EE81EEA1EEC1EEE1EF01EF21EF41EF61EF81EFA1EFC1EFE1F08-1F0F1F18-1F1D1F28-1F2F1F38-1F3F1F48-1F4D1F591F5B1F5D1F5F1F68-1F6F1FB8-1FBB1FC8-1FCB1FD8-1FDB1FE8-1FEC1FF8-1FFB21022107210B-210D2110-211221152119-211D212421262128212A-212D2130-2133213E213F214521832C00-2C2E2C602C62-2C642C672C692C6B2C6D-2C702C722C752C7E-2C802C822C842C862C882C8A2C8C2C8E2C902C922C942C962C982C9A2C9C2C9E2CA02CA22CA42CA62CA82CAA2CAC2CAE2CB02CB22CB42CB62CB82CBA2CBC2CBE2CC02CC22CC42CC62CC82CCA2CCC2CCE2CD02CD22CD42CD62CD82CDA2CDC2CDE2CE02CE22CEB2CED2CF2A640A642A644A646A648A64AA64CA64EA650A652A654A656A658A65AA65CA65EA660A662A664A666A668A66AA66CA680A682A684A686A688A68AA68CA68EA690A692A694A696A722A724A726A728A72AA72CA72EA732A734A736A738A73AA73CA73EA740A742A744A746A748A74AA74CA74EA750A752A754A756A758A75AA75CA75EA760A762A764A766A768A76AA76CA76EA779A77BA77DA77EA780A782A784A786A78BA78DA790A792A7A0A7A2A7A4A7A6A7A8A7AAFF21-FF3A", Mc: "0903093B093E-09400949-094C094E094F0982098309BE-09C009C709C809CB09CC09D70A030A3E-0A400A830ABE-0AC00AC90ACB0ACC0B020B030B3E0B400B470B480B4B0B4C0B570BBE0BBF0BC10BC20BC6-0BC80BCA-0BCC0BD70C01-0C030C41-0C440C820C830CBE0CC0-0CC40CC70CC80CCA0CCB0CD50CD60D020D030D3E-0D400D46-0D480D4A-0D4C0D570D820D830DCF-0DD10DD8-0DDF0DF20DF30F3E0F3F0F7F102B102C10311038103B103C105610571062-10641067-106D108310841087-108C108F109A-109C17B617BE-17C517C717C81923-19261929-192B193019311933-193819B0-19C019C819C91A19-1A1B1A551A571A611A631A641A6D-1A721B041B351B3B1B3D-1B411B431B441B821BA11BA61BA71BAA1BAC1BAD1BE71BEA-1BEC1BEE1BF21BF31C24-1C2B1C341C351CE11CF21CF3302E302FA823A824A827A880A881A8B4-A8C3A952A953A983A9B4A9B5A9BAA9BBA9BD-A9C0AA2FAA30AA33AA34AA4DAA7BAAEBAAEEAAEFAAF5ABE3ABE4ABE6ABE7ABE9ABEAABEC", Mn: "0300-036F0483-04870591-05BD05BF05C105C205C405C505C70610-061A064B-065F067006D6-06DC06DF-06E406E706E806EA-06ED07110730-074A07A6-07B007EB-07F30816-0819081B-08230825-08270829-082D0859-085B08E4-08FE0900-0902093A093C0941-0948094D0951-095709620963098109BC09C1-09C409CD09E209E30A010A020A3C0A410A420A470A480A4B-0A4D0A510A700A710A750A810A820ABC0AC1-0AC50AC70AC80ACD0AE20AE30B010B3C0B3F0B41-0B440B4D0B560B620B630B820BC00BCD0C3E-0C400C46-0C480C4A-0C4D0C550C560C620C630CBC0CBF0CC60CCC0CCD0CE20CE30D41-0D440D4D0D620D630DCA0DD2-0DD40DD60E310E34-0E3A0E47-0E4E0EB10EB4-0EB90EBB0EBC0EC8-0ECD0F180F190F350F370F390F71-0F7E0F80-0F840F860F870F8D-0F970F99-0FBC0FC6102D-10301032-10371039103A103D103E10581059105E-10601071-1074108210851086108D109D135D-135F1712-17141732-1734175217531772177317B417B517B7-17BD17C617C9-17D317DD180B-180D18A91920-19221927192819321939-193B1A171A181A561A58-1A5E1A601A621A65-1A6C1A73-1A7C1A7F1B00-1B031B341B36-1B3A1B3C1B421B6B-1B731B801B811BA2-1BA51BA81BA91BAB1BE61BE81BE91BED1BEF-1BF11C2C-1C331C361C371CD0-1CD21CD4-1CE01CE2-1CE81CED1CF41DC0-1DE61DFC-1DFF20D0-20DC20E120E5-20F02CEF-2CF12D7F2DE0-2DFF302A-302D3099309AA66FA674-A67DA69FA6F0A6F1A802A806A80BA825A826A8C4A8E0-A8F1A926-A92DA947-A951A980-A982A9B3A9B6-A9B9A9BCAA29-AA2EAA31AA32AA35AA36AA43AA4CAAB0AAB2-AAB4AAB7AAB8AABEAABFAAC1AAECAAEDAAF6ABE5ABE8ABEDFB1EFE00-FE0FFE20-FE26", Nd: "0030-00390660-066906F0-06F907C0-07C90966-096F09E6-09EF0A66-0A6F0AE6-0AEF0B66-0B6F0BE6-0BEF0C66-0C6F0CE6-0CEF0D66-0D6F0E50-0E590ED0-0ED90F20-0F291040-10491090-109917E0-17E91810-18191946-194F19D0-19D91A80-1A891A90-1A991B50-1B591BB0-1BB91C40-1C491C50-1C59A620-A629A8D0-A8D9A900-A909A9D0-A9D9AA50-AA59ABF0-ABF9FF10-FF19", Nl: "16EE-16F02160-21822185-218830073021-30293038-303AA6E6-A6EF", Pc: "005F203F20402054FE33FE34FE4D-FE4FFF3F" }; var unicodeClass = function (abbrev) { return '[' + unicodeCategories[abbrev].replace(/[0-9A-F]{4}/ig, "\\u$&") + ']'; }; // See ECMA-262 spec, 3rd edition, Section 7.6 // Match one or more characters that can start an identifier. // This is IdentifierStart+. var rIdentifierPrefix = new RegExp( "^([a-zA-Z$_]+|\\\\u[0-9a-fA-F]{4}|" + [unicodeClass('Lu'), unicodeClass('Ll'), unicodeClass('Lt'), unicodeClass('Lm'), unicodeClass('Lo'), unicodeClass('Nl')].join('|') + ")+"); // Match one or more characters that can continue an identifier. // This is (IdentifierPart and not IdentifierStart)+. // To match a full identifier, match rIdentifierPrefix, then // match rIdentifierMiddle followed by rIdentifierPrefix until they both fail. var rIdentifierMiddle = new RegExp( "^([0-9]|" + [unicodeClass('Mn'), unicodeClass('Mc'), unicodeClass('Nd'), unicodeClass('Pc')].join('|') + ")+"); // See ECMA-262 spec, 3rd edition, Section 7.8.3 var rHexLiteral = /^0[xX][0-9a-fA-F]+(?!\w)/; var rDecLiteral = /^(((0|[1-9][0-9]*)(\.[0-9]*)?)|\.[0-9]+)([Ee][+-]?[0-9]+)?(?!\w)/; // Section 7.8.4 var rStringQuote = /^["']/; // Match one or more characters besides quotes, backslashes, or line ends var rStringMiddle = /^(?=.)[^"'\\]+?((?!.)|(?=["'\\]))/; // Match one escape sequence, including the backslash. var rEscapeSequence = /^\\(['"\\bfnrtv]|0(?![0-9])|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|(?=.)[^ux0-9])/; // Match one ES5 line continuation var rLineContinuation = /^\\(\r\n|[\u000A\u000D\u2028\u2029])/; BlazeTools.parseNumber = function (scanner) { var startPos = scanner.pos; var isNegative = false; if (scanner.peek() === '-') { scanner.pos++; isNegative = true; } // Note that we allow `"-0xa"`, unlike `Number(...)`. var rest = scanner.rest(); var match = rDecLiteral.exec(rest) || rHexLiteral.exec(rest); if (! match) { scanner.pos = startPos; return null; } var matchText = match[0]; scanner.pos += matchText.length; var text = (isNegative ? '-' : '') + matchText; var value = Number(matchText); value = (isNegative ? -value : value); return { text: text, value: value }; }; BlazeTools.parseIdentifierName = function (scanner) { var startPos = scanner.pos; var rest = scanner.rest(); var match = rIdentifierPrefix.exec(rest); if (! match) return null; scanner.pos += match[0].length; rest = scanner.rest(); var foundMore = true; while (foundMore) { foundMore = false; match = rIdentifierMiddle.exec(rest); if (match) { foundMore = true; scanner.pos += match[0].length; rest = scanner.rest(); } match = rIdentifierPrefix.exec(rest); if (match) { foundMore = true; scanner.pos += match[0].length; rest = scanner.rest(); } } return scanner.input.substring(startPos, scanner.pos); }; BlazeTools.parseExtendedIdentifierName = function (scanner) { // parse an identifier name optionally preceded by '@' if (scanner.peek() === '@') { scanner.pos++; var afterAt = BlazeTools.parseIdentifierName(scanner); if (afterAt) { return '@' + afterAt; } else { scanner.pos--; return null; } } else { return BlazeTools.parseIdentifierName(scanner); } }; BlazeTools.parseStringLiteral = function (scanner) { var startPos = scanner.pos; var rest = scanner.rest(); var match = rStringQuote.exec(rest); if (! match) return null; var quote = match[0]; scanner.pos++; rest = scanner.rest(); var jsonLiteral = '"'; while (match) { match = rStringMiddle.exec(rest); if (match) { jsonLiteral += match[0]; } else { match = rEscapeSequence.exec(rest); if (match) { var esc = match[0]; // Convert all string escapes to JSON-compatible string escapes, so we // can use JSON.parse for some of the work. JSON strings are not the // same as JS strings. They don't support `\0`, `\v`, `\'`, or hex // escapes. if (esc === '\\0') jsonLiteral += '\\u0000'; else if (esc === '\\v') // Note: IE 8 doesn't correctly parse '\v' in JavaScript. jsonLiteral += '\\u000b'; else if (esc.charAt(1) === 'x') jsonLiteral += '\\u00' + esc.slice(2); else if (esc === '\\\'') jsonLiteral += "'"; else jsonLiteral += esc; } else { match = rLineContinuation.exec(rest); if (! match) { match = rStringQuote.exec(rest); if (match) { var c = match[0]; if (c !== quote) { if (c === '"') jsonLiteral += '\\'; jsonLiteral += c; } } } } } if (match) { scanner.pos += match[0].length; rest = scanner.rest(); if (match[0] === quote) break; } } if (! match || match[0] !== quote) scanner.fatal("Unterminated string literal"); jsonLiteral += '"'; var text = scanner.input.substring(startPos, scanner.pos); var value = JSON.parse(jsonLiteral); return { text: text, value: value }; }; ================================================ FILE: packages/boilerplate-generator/.gitignore ================================================ .build* ================================================ FILE: packages/boilerplate-generator/README.md ================================================ This is an internal Meteor package. ================================================ FILE: packages/boilerplate-generator/boilerplate-generator.js ================================================ var fs = Npm.require('fs'); var path = Npm.require('path'); // Copied from webapp_server var readUtf8FileSync = function (filename) { return Meteor.wrapAsync(fs.readFile)(filename, 'utf8'); }; Boilerplate = function (arch, manifest, options) { var self = this; options = options || {}; self.template = _getTemplate(arch); self.baseData = null; self.func = null; self._generateBoilerplateFromManifestAndSource( manifest, self.template, options ); }; // The 'extraData' argument can be used to extend 'self.baseData'. Its // purpose is to allow you to specify data that you might not know at // the time that you construct the Boilerplate object. (e.g. it is used // by 'webapp' to specify data that is only known at request-time). Boilerplate.prototype.toHTML = function (extraData) { var self = this; if (! self.baseData || ! self.func) throw new Error('Boilerplate did not instantiate correctly.'); return "\n" + Blaze.toHTML(Blaze.With(_.extend(self.baseData, extraData), self.func)); }; // XXX Exported to allow client-side only changes to rebuild the boilerplate // without requiring a full server restart. // Produces an HTML string with given manifest and boilerplateSource. // Optionally takes urlMapper in case urls from manifest need to be prefixed // or rewritten. // Optionally takes pathMapper for resolving relative file system paths. // Optionally allows to override fields of the data context. Boilerplate.prototype._generateBoilerplateFromManifestAndSource = function (manifest, boilerplateSource, options) { var self = this; // map to the identity by default var urlMapper = options.urlMapper || _.identity; var pathMapper = options.pathMapper || _.identity; var boilerplateBaseData = { css: [], js: [], head: '', body: '', meteorManifest: JSON.stringify(manifest) }; // allow the caller to extend the default base data _.extend(boilerplateBaseData, options.baseDataExtension); _.each(manifest, function (item) { var urlPath = urlMapper(item.url); var itemObj = { url: urlPath }; if (options.inline) { itemObj.scriptContent = readUtf8FileSync( pathMapper(item.path)); itemObj.inline = true; } if (item.type === 'css' && item.where === 'client') { boilerplateBaseData.css.push(itemObj); } if (item.type === 'js' && item.where === 'client') { boilerplateBaseData.js.push(itemObj); } if (item.type === 'head') { boilerplateBaseData.head = readUtf8FileSync(pathMapper(item.path)); } if (item.type === 'body') { boilerplateBaseData.body = readUtf8FileSync(pathMapper(item.path)); } }); var boilerplateRenderCode = SpacebarsCompiler.compile( boilerplateSource, { isBody: true }); // Note that we are actually depending on eval's local environment capture // so that UI and HTML are visible to the eval'd code. // XXX the template we are evaluating relies on the fact that UI is globally // available. global.UI = UI; self.func = eval(boilerplateRenderCode); self.baseData = boilerplateBaseData; }; var _getTemplate = _.memoize(function (arch) { var filename = 'boilerplate_' + arch + '.html'; return Assets.getText(filename); }); ================================================ FILE: packages/boilerplate-generator/boilerplate_web.browser.html ================================================ {{#each css}} {{/each}} {{{head}}} {{{dynamicHead}}} {{{body}}} {{{dynamicBody}}} {{#if inlineScriptsAllowed}} {{else}} {{/if}} {{#each js}} {{/each}} {{#each additionalStaticJs}} {{#if ../inlineScriptsAllowed}} {{else}} {{/if}} {{/each}} ================================================ FILE: packages/boilerplate-generator/boilerplate_web.cordova.html ================================================ {{! We are explicitly not using bundledJsCssUrlRewriteHook: in cordova we serve assets up directly from disk, so rewriting the URL does not make sense }} {{#each css}} {{/each}} {{#each js}} {{/each}} {{#each additionalStaticJs}} {{#if ../inlineScriptsAllowed}} {{else}} {{/if}} {{/each}} {{{head}}} {{{body}}} ================================================ FILE: packages/boilerplate-generator/package.js ================================================ Package.describe({ summary: "Generates the boilerplate html from program's manifest", version: '1.0.9' }); Package.onUse(function (api) { api.use(['underscore', 'spacebars-compiler', 'spacebars', 'htmljs', 'ui'], 'server'); api.addFiles(['boilerplate-generator.js'], 'server'); api.export(['Boilerplate'], 'server'); // These are spacebars templates, but we process them manually with the // spacebars compiler rather than letting the 'templating' package (which // isn't fully supported on the server yet) handle it. That also means that // they don't contain the outer "