├── demokeys.json
├── src
├── utils
│ ├── empty-function.js
│ ├── clone.js
│ ├── load-polyfills.coffee
│ ├── throw.coffee
│ ├── cookies.coffee
│ ├── eventbus.js
│ ├── decode-hash.coffee
│ ├── queue.coffee
│ ├── location-origin.coffee
│ ├── ui
│ │ ├── captcha.html
│ │ ├── platform.html
│ │ ├── display-banner.coffee
│ │ └── platform.css
│ ├── entity.coffee
│ ├── get-key.coffee
│ ├── pool.coffee
│ ├── promises.coffee
│ ├── utils.js
│ ├── dom-events.js
│ ├── console-shim.coffee
│ ├── set-style.coffee
│ ├── query-string-encoder.coffee
│ ├── logger.coffee
│ ├── find-url.coffee
│ ├── raven.coffee
│ ├── script-loader.coffee
│ ├── dom-walker.coffee
│ ├── lodash.js
│ ├── get-current-script.js
│ ├── config-normalizer.coffee
│ ├── domready.js
│ ├── uuid.js
│ ├── is-mobile.coffee
│ ├── base64.js
│ └── leaky-bucket.js
├── client
│ ├── flag
│ │ └── index.coffee
│ ├── track
│ │ ├── autotrack.coffee
│ │ └── index.coffee
│ ├── querystring
│ │ └── index.coffee
│ ├── embeds
│ │ ├── sandbox.coffee
│ │ ├── index.js
│ │ ├── strategies
│ │ │ └── js.coffee
│ │ └── deployment.coffee
│ ├── config-check.coffee
│ ├── traits
│ │ └── index.coffee
│ ├── index.coffee
│ ├── sharer
│ │ └── index.coffee
│ ├── parse-opts.coffee
│ ├── current-user.coffee
│ ├── api.coffee
│ ├── initialize-platform.js
│ ├── script-tag-config.coffee
│ ├── channel.coffee
│ ├── current-config.coffee
│ └── auth.coffee
├── flux
│ ├── constants
│ │ └── RemoteConstants.js
│ ├── dispatcher
│ │ └── RemoteDispatcher.js
│ ├── stores
│ │ ├── ClientConfigStore.js
│ │ ├── RemoteUserStore.js
│ │ ├── RemoteConfigStore.js
│ │ └── RemoteHeaderStore.js
│ └── actions
│ │ └── RemoteActions.js
├── remote
│ ├── services
│ │ ├── tumblr.coffee
│ │ ├── twitter.coffee
│ │ ├── soundcloud.coffee
│ │ ├── list.coffee
│ │ ├── generic-service.coffee
│ │ ├── angellist.coffee
│ │ ├── google.coffee
│ │ ├── linkedin.coffee
│ │ ├── github.coffee
│ │ ├── instagram.coffee
│ │ ├── admin.coffee
│ │ ├── track.coffee
│ │ ├── hull.coffee
│ │ └── facebook.coffee
│ ├── wrapped-request.js
│ ├── channel.coffee
│ ├── services.coffee
│ └── gateway.coffee
├── polyfills
│ ├── assign.js
│ └── xhr-xdr.js
├── hull-remote.coffee
├── hull.coffee
└── styles
│ └── style.scss
├── .gitignore
├── .travis.yml
├── .babelrc
├── scripts
└── sentry_release.sh
├── .github
├── issue_template.md
└── pull_request_template.md
├── app
├── app.example.js
└── index.html
├── manifest.json
├── .eslintrc
├── .editorconfig
├── circle.yml
├── test_utils
└── preprocessor.js
├── __tests__
└── utils
│ ├── analytics-id.coffee
│ └── trait-test.js
├── LICENSE
├── __mocks__
└── superagent.js
├── README.md
├── package.json
├── webpack.config.js
├── config.js
└── gulpfile.js
/demokeys.json:
--------------------------------------------------------------------------------
1 | {
2 | "appId": "54dba72a8bca245d19000001",
3 | "orgUrl": "https://ships-demos.hullbeta.io"
4 | }
5 |
--------------------------------------------------------------------------------
/src/utils/empty-function.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function emptyFunction() {}
4 |
5 | module.exports = emptyFunction;
6 |
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .env.sh
3 | /lib
4 | app/*html
5 | app/app.js
6 | bower_components
7 | node_modules
8 | npm-debug.log
9 | .idea
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 0.10
4 | before_script:
5 | - npm install -g gulp
6 | branches:
7 | only:
8 | - master
9 |
--------------------------------------------------------------------------------
/src/utils/clone.js:
--------------------------------------------------------------------------------
1 | module.exports = function(obj){
2 | if (obj === undefined){ return obj; }
3 | return JSON.parse(JSON.stringify(obj))
4 | }
5 |
--------------------------------------------------------------------------------
/src/client/flag/index.coffee:
--------------------------------------------------------------------------------
1 | module.exports = (api)->
2 | (id)->
3 | api
4 | provider: "hull"
5 | path: [id, 'flag'].join('/')
6 | ,'post'
7 |
--------------------------------------------------------------------------------
/src/utils/load-polyfills.coffee:
--------------------------------------------------------------------------------
1 | require "../polyfills/xhr-xdr" #TODO : Test if we can remove this now it's in Polyfill service
2 | require "../utils/console-shim"
3 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-0"],
3 | "plugins": ["transform-es3-member-expression-literals", "babel-plugin-transform-es3-property-literals"]
4 | }
5 |
--------------------------------------------------------------------------------
/src/utils/throw.coffee:
--------------------------------------------------------------------------------
1 | logger = require './logger'
2 | Raven = require './raven'
3 |
4 | module.exports = (err)->
5 | Raven.captureException(err)
6 | logger.error err.message, err.stack
7 |
--------------------------------------------------------------------------------
/src/utils/cookies.coffee:
--------------------------------------------------------------------------------
1 | Cookies = require 'cookies-js'
2 | COOKIES_ENABLED = '_ce'
3 | module.exports =
4 | set: Cookies.set
5 | get: Cookies.get
6 | remove: Cookies.expire
7 | enabled: -> Cookies._areEnabled()
8 |
9 |
--------------------------------------------------------------------------------
/src/utils/eventbus.js:
--------------------------------------------------------------------------------
1 | import EventEmitter from 'eventemitter2';
2 | var emitter = new EventEmitter({
3 | wildcard: true,
4 | maxListeners: 200,
5 | newListener: false,
6 | delimiter: '.'
7 | });
8 | module.exports = emitter;
9 |
--------------------------------------------------------------------------------
/scripts/sentry_release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | if [ -n "$SENTRY_URL" ]; then
3 | curl -v $SENTRY_URL -X POST -H 'Content-Type: application/json' -d '{"version":"'"$CIRCLE_SHA1"'", "ref":"'"$CIRCLE_SHA1"'"}'
4 | else
5 | echo "SENTRY_URL is not set"
6 | fi
7 |
--------------------------------------------------------------------------------
/src/utils/decode-hash.coffee:
--------------------------------------------------------------------------------
1 | Base64 = require '../utils/base64'
2 | module.exports = ()->
3 | try
4 | h = document.location.hash.replace('#', '')
5 | hash = JSON.parse(Base64.decode(h)) if !!h
6 | catch e
7 | hash = null
8 | return hash
9 |
--------------------------------------------------------------------------------
/.github/issue_template.md:
--------------------------------------------------------------------------------
1 | ## Expected Behavior
2 |
3 |
4 | ## Actual Behavior
5 |
6 |
7 | ## Steps to Reproduce the Problem
8 |
9 | 1.
10 | 1.
11 | 1.
12 |
13 | ## Specifications
14 |
15 | - Version:
16 | - Platform:
17 | - Subsystem:
18 |
--------------------------------------------------------------------------------
/src/utils/queue.coffee:
--------------------------------------------------------------------------------
1 | Queue = ()->
2 | _open = false
3 | _cbs = []
4 | run: (cb)->
5 | if _open
6 | cb()
7 | else
8 | _cbs.push cb
9 | flush: ()->
10 | _open = true
11 | cb() for cb in _cbs
12 | module.exports = Queue
13 |
--------------------------------------------------------------------------------
/app/app.example.js:
--------------------------------------------------------------------------------
1 | Hull.init({
2 | platformId: 'YOUR_PLATFORM_ID',
3 | orgUrl: 'http://your.app.domain.tld',
4 | // jsUrl: 'http://your.local.dev/'
5 | // name: "name your app",
6 | debug: true
7 | });
8 |
9 | Hull.ready(function(hull, me, platform, org){
10 | })
11 |
--------------------------------------------------------------------------------
/src/utils/location-origin.coffee:
--------------------------------------------------------------------------------
1 |
2 | module.exports = ->
3 | return window.location.origin if window.location.origin
4 | port = ''
5 | port = ':' + window.location.port if window.location.port
6 | origin = window.location.protocol + "//" + window.location.hostname + port
7 | origin
8 |
--------------------------------------------------------------------------------
/src/utils/ui/captcha.html:
--------------------------------------------------------------------------------
1 |
2 |
A lot of users have been created with your IP!
3 |
Please prove us that you are a Human.
4 |
5 |
6 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "displayName": "Hull.js",
3 | "index": "dist/index.html",
4 | "version": "0.10.0",
5 | "index": "index.html",
6 | "demoKeys":{
7 | "appId": "52fb86bedea4dfd8de000003",
8 | "orgUrl": "https://super.hullbeta.io"
9 | },
10 | "settings": []
11 | }
12 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "env": {
4 | "browser": true,
5 | "node": true
6 | },
7 | "rules": {
8 | "quotes": [2, "single"],
9 | "key-spacing" : [0],
10 | "no-multi-spaces": [0],
11 | "no-mixed-requires": [0],
12 | "no-underscore-dangle": [0]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/flux/constants/RemoteConstants.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | UPDATE_CLIENT_CONFIG:'UPDATE_CLIENT_CONFIG',
3 | UPDATE_REMOTE_CONFIG:'UPDATE_REMOTE_CONFIG',
4 | UPDATE_SERVICES:'UPDATE_SERVICES',
5 |
6 | SET_HEADER:'SET_HEADER',
7 |
8 | LOGOUT_USER:'LOGOUT_USER',
9 | UPDATE_USER:'UPDATE_USER'
10 | };
11 |
--------------------------------------------------------------------------------
/src/remote/services/tumblr.coffee:
--------------------------------------------------------------------------------
1 | GenericService = require './generic-service'
2 |
3 | class TumblrService extends GenericService
4 | name : 'twitter'
5 | path: 'tumblr/v2'
6 | constructor: (config, gateway)->
7 | super(config,gateway)
8 | @request = @wrappedRequest
9 |
10 | module.exports = TumblrService
11 |
--------------------------------------------------------------------------------
/src/flux/dispatcher/RemoteDispatcher.js:
--------------------------------------------------------------------------------
1 | var flux = require('flux');
2 | var assign = require('../../polyfills/assign');
3 |
4 |
5 | var RemoteDispatcher = assign(new flux.Dispatcher(), {
6 | handleAction: function(action) {
7 | this.dispatch({action:action});
8 | },
9 | });
10 |
11 | module.exports = RemoteDispatcher;
12 |
--------------------------------------------------------------------------------
/src/remote/services/twitter.coffee:
--------------------------------------------------------------------------------
1 | GenericService = require './generic-service'
2 |
3 | class TwitterService extends GenericService
4 | name : 'twitter'
5 | path: 'twitter/1.1'
6 | constructor: (config, gateway)->
7 | super(config,gateway)
8 | @request = @wrappedRequest
9 |
10 | module.exports = TwitterService
11 |
--------------------------------------------------------------------------------
/src/remote/services/soundcloud.coffee:
--------------------------------------------------------------------------------
1 | GenericService = require './generic-service'
2 |
3 | class SoundCloudService extends GenericService
4 | name : 'soundcloud'
5 | path: 'soundcloud'
6 | constructor: (config, gateway)->
7 | super(config,gateway)
8 | @request = @wrappedRequest
9 |
10 | module.exports = SoundCloudService
11 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | ; EditorConfig helps developers define and maintain consistent
2 | ; coding styles between different editors and IDEs
3 | ; editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | indent_style = space
9 | indent_size = 2
10 | end_of_line = lf
11 | charset = utf-8
12 | trim_trailing_whitespace = true
13 | insert_final_newline = true
14 |
--------------------------------------------------------------------------------
/src/client/track/autotrack.coffee:
--------------------------------------------------------------------------------
1 | Base64 = require '../../utils/base64'
2 |
3 | module.exports = (headers, emitter)->
4 | hullTrack = headers['Hull-Track']
5 | if hullTrack
6 | try
7 | [eventName, trackParams] = JSON.parse(Base64.decode(hullTrack))
8 | emitter.emit(eventName, trackParams)
9 | catch error
10 | false
11 |
--------------------------------------------------------------------------------
/src/utils/entity.coffee:
--------------------------------------------------------------------------------
1 | Base64 = require './base64.js'
2 |
3 | module.exports =
4 | decode: (input)->
5 | unless /^~[a-z0-9_\-\+\/\=]+$/i.test(input)
6 | throw "'#{input}' cannot be decoded because it has not been correctly encoded"
7 |
8 | Base64.decodeURL(input.substr(1))
9 |
10 | encode: (input)->
11 | "~#{Base64.encodeURL(input)}"
12 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | build:
4 | docker:
5 | - image: circleci/node:8.12
6 | working_directory: ~/repo
7 | steps:
8 | - checkout
9 | - run: yarn install
10 | - run: ./node_modules/.bin/gulp deploy:release
11 | - run: ./scripts/sentry_release.sh
12 | branches:
13 | only:
14 | - release/0.10.0
15 |
--------------------------------------------------------------------------------
/src/utils/ui/platform.html:
--------------------------------------------------------------------------------
1 |
2 |
Congratulations! Hull is ready.
3 |
Your snippet is correctly installed and configured
This banner is only visible to you.
4 |
Close Window
5 |
6 |
--------------------------------------------------------------------------------
/src/utils/get-key.coffee:
--------------------------------------------------------------------------------
1 | clone = require './clone'
2 | _ = require './lodash'
3 |
4 | module.exports = (hash, key)->
5 | return hash unless key and _.isObject(hash)
6 | _.each key.split('.'), (k)->
7 | return hash if hash == undefined
8 | if _.includes(_.keys(hash), k)
9 | hash = hash[k]
10 | else
11 | hash = undefined
12 |
13 | clone(hash)
14 |
--------------------------------------------------------------------------------
/src/utils/pool.coffee:
--------------------------------------------------------------------------------
1 | _pool = _pool || {}
2 | createPool = (name)->
3 | _pool[name] ?= []
4 | (args...)-> _pool[name].push args
5 | deletePool = (name)->
6 | delete _pool[name]
7 | run = (name, obj)->
8 | obj[name](data...) for data in _pool[name]
9 | deletePool(name)
10 |
11 | module.exports={
12 | create:createPool,
13 | delete:deletePool,
14 | run:run
15 | }
16 |
--------------------------------------------------------------------------------
/src/polyfills/assign.js:
--------------------------------------------------------------------------------
1 | Object.assign = Object.assign || function assign(target, source) {
2 | for (var index = 1, key; index in arguments; ++index) {
3 | source = arguments[index];
4 |
5 | for (key in source) {
6 | if (Object.prototype.hasOwnProperty.call(source, key)) {
7 | target[key] = source[key];
8 | }
9 | }
10 | }
11 | return target;
12 | };
13 | module.exports = Object.assign
14 |
--------------------------------------------------------------------------------
/src/utils/promises.coffee:
--------------------------------------------------------------------------------
1 | Promise = require 'bluebird/js/browser/bluebird.core'
2 |
3 | Promise.config({
4 | warnings: false,
5 | longStackTraces: true,
6 | cancellation: true,
7 | monitoring: true
8 | })
9 |
10 | Promise.deferred = Promise.deferred || ->
11 | dfd = {}
12 | dfd.promise = new Promise (resolve, reject)=>
13 | dfd.resolve = resolve
14 | dfd.reject = reject
15 | dfd
16 |
17 | module.exports = Promise
18 |
--------------------------------------------------------------------------------
/test_utils/preprocessor.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | /* global require, module */
3 |
4 | var CoffeeScript = require("coffee-script");
5 | var Babel = require("../node_modules/babel-jest");
6 |
7 | module.exports = {
8 | process: function(src, path) {
9 | if (CoffeeScript.helpers.isCoffee(path)) {
10 | return CoffeeScript.compile(src, {"bare": true});
11 | } else {
12 | return Babel.process(src, path);
13 | }
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/src/utils/utils.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | cookies : require("cookies-js"),
3 | eventemitter2 : require("eventemitter2"),
4 | assign : require("../polyfills/assign"),
5 | _ : require("./lodash"),
6 | isMobile : require("./is-mobile"),
7 | uuid : require("./uuid"),
8 | domready : require("./domready"),
9 | Promise : require("./promises"),
10 | superagent : require("superagent")
11 | };
12 |
--------------------------------------------------------------------------------
/src/utils/dom-events.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | export function addEvent(node, eventName, handler) {
4 | if (node.attachEvent) {
5 | node.attachEvent(`on${eventName}`, handler);
6 | } else {
7 | node.addEventListener(eventName, handler, false);
8 | }
9 | }
10 |
11 | export function removeEvent(node, eventName, handler) {
12 | if (node.detachEvent) {
13 | node.detachEvent(`on${eventName}`, handler);
14 | } else {
15 | node.removeEventListener(eventName, handler, false);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/remote/services/list.coffee:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | admin : require './admin'
3 | track : require './track'
4 | hull : require './hull'
5 | angellist : require './angellist'
6 | facebook : require './facebook'
7 | github : require './github'
8 | google : require './google'
9 | instagram : require './instagram'
10 | linkedin : require './linkedin'
11 | soundcloud : require './soundcloud'
12 | tumblr : require './tumblr'
13 | twitter : require './twitter'
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/console-shim.coffee:
--------------------------------------------------------------------------------
1 | methods = ["assert", "clear", "count", "debug", "dir", "dirxml", "error", "exception", "group", "groupCollapsed", "groupEnd", "info", "log", "markTimeline", "profile", "profileEnd", "markTimeline", "table", "time", "timeEnd", "timeStamp", "trace", "warn"]
2 |
3 | unless window.console and console.log
4 | (->
5 | noop = ()->
6 | console = window.console = {}
7 | console[method] = noop for method in methods
8 | )()
9 | else if typeof console.log=='object'
10 | log = console.log
11 | console.log = (args...)-> log(args)
12 |
--------------------------------------------------------------------------------
/src/utils/set-style.coffee:
--------------------------------------------------------------------------------
1 | _ = require '../utils/lodash'
2 |
3 |
4 | getDocHeight = (doc)->
5 | return unless doc
6 | body = doc.body
7 | html = doc.documentElement
8 | Math.max( body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);
9 |
10 | setDimension = (el, dim, val)->
11 | if val?
12 | val = "#{val}px" if /[0-9]+$/.test(val.toString())
13 | el.style[dim] = val
14 |
15 | module.exports = (el,style) ->
16 | return unless _.isObject(style)
17 | _.map style, (value,key)-> setDimension(el,key,value)
18 |
--------------------------------------------------------------------------------
/src/utils/query-string-encoder.coffee:
--------------------------------------------------------------------------------
1 | Qs = require('qs');
2 |
3 | keywords =
4 | true: true
5 | false: false
6 | null: null
7 | undefined: undefined
8 |
9 | decoder = (value) ->
10 | return parseFloat(value) if (/^(\d+|\d*\.\d+)$/.test(value))
11 | return keywords[value] if (value of keywords)
12 | return value
13 |
14 |
15 | module.exports =
16 | encode : (data)->
17 | Qs.stringify(data)
18 | decode : (search)->
19 | return Qs.parse(
20 | window.location.search.slice(1),
21 | decoder: decoder
22 | ) unless search
23 | Qs.parse(search)
24 |
--------------------------------------------------------------------------------
/src/remote/services/generic-service.coffee:
--------------------------------------------------------------------------------
1 | RemoteConfigStore = require '../../flux/stores/RemoteConfigStore'
2 | getWrappedRequest = require '../wrapped-request'
3 | jsonp = require 'browser-jsonp'
4 |
5 | class GenericService
6 | name : null
7 | path : null
8 | constructor : (config,gateway)->
9 | @config = config
10 | @gateway = gateway
11 | @wrappedRequest = getWrappedRequest({name:@name,path:@path},gateway)
12 |
13 | getSettings: (provider)-> RemoteConfigStore.getAuth(@name||provider)
14 |
15 | request_jsonp : (request)=> jsonp(request)
16 |
17 | module.exports = GenericService
18 |
--------------------------------------------------------------------------------
/__tests__/utils/analytics-id.coffee:
--------------------------------------------------------------------------------
1 | jest
2 | .dontMock "../../src/utils/analytics-id"
3 | .dontMock "../../src/utils/uuid"
4 | .dontMock "../../src/utils/cookies"
5 | .dontMock "cookies-js"
6 |
7 | describe "analyticsJS IDs", ()->
8 | analyticsId = require "../../src/utils/analytics-id"
9 | it "Returns the same browser ID when called twice", ()->
10 | browserId1 = analyticsId.getBrowserId()
11 | browserId2 = analyticsId.getBrowserId()
12 | expect(browserId1).toBe browserId2
13 |
14 | it "Returns the same session ID when called twice", ()->
15 | browserId1 = analyticsId.getSessionId()
16 | browserId2 = analyticsId.getSessionId()
17 | expect(browserId1).toBe browserId2
18 |
--------------------------------------------------------------------------------
/src/utils/logger.coffee:
--------------------------------------------------------------------------------
1 | enabled = false
2 | verbose = false
3 |
4 | log = (args...)->
5 | # TODO :
6 | # Dont use spread if we want IE8: It doesnt' treat console.log as a real function hence can't do console.log.apply.
7 | # Duh...
8 | console.log(args...)
9 |
10 | error = (args...)->
11 | # TODO :
12 | # Dont use spread if we want IE8: It doesnt' treat console.log as a real function hence can't do console.log.apply.
13 | # Duh...
14 | console.error(args...)
15 |
16 | module.exports = {
17 | enabled: ()->
18 | return !!enabled
19 | init: (debug)->
20 | enabled = !!debug.enabled
21 | verbose = !!debug.verbose
22 | log: (args...)->
23 | log(args...) if enabled
24 | info: (args...)->
25 | log(args...) if enabled
26 | verbose: (args...)->
27 | log(args...) if enabled and verbose
28 | warn: (args...)->
29 | console.warn(args...)
30 | error: error
31 | }
32 |
--------------------------------------------------------------------------------
/src/utils/find-url.coffee:
--------------------------------------------------------------------------------
1 | _ = require './lodash'
2 | domWalker = require './dom-walker'
3 |
4 | getDOMUrl = (sourceNode)->
5 | return unless domWalker.isNode(sourceNode)
6 | body = document.body
7 | node = sourceNode
8 | while node and node != body
9 | # Find link on self
10 | # or find first link from siblings
11 | link = domWalker.getLinkValue(node)
12 | unless link
13 | matchingNode = _.find(domWalker.getSiblings(node), domWalker.getLinkValue)
14 | link = domWalker.getLinkValue matchingNode
15 |
16 | # If we found something, return.
17 | return link if link?
18 |
19 | # Else, walk up
20 | node = node.parentNode
21 |
22 | getPageUrl = ()->
23 | window.location.href
24 |
25 | getOGUrl = ()->
26 | domWalker.getMetaValue('og:url')
27 |
28 | module.exports = (node)->
29 | u = getDOMUrl(node) || getOGUrl() || getPageUrl()
30 | u.replace(/[#\/]*$/, '')
31 |
32 |
--------------------------------------------------------------------------------
/src/remote/services/angellist.coffee:
--------------------------------------------------------------------------------
1 | assign = require '../../polyfills/assign'
2 | GenericService = require './generic-service'
3 |
4 | class AngelListService extends GenericService
5 | name : 'angellist'
6 | path : 'angellist'
7 | constructor: (config, gateway)->
8 |
9 | request: (request, callback, errback)->
10 | {method, path, params} = request
11 | method = method.toLowerCase()
12 | return errback('Unable to perform non-GET requests on AngelList') unless method=='get'
13 |
14 | token = @getSettings()
15 | path = path.substring(1) if (path[0] == "/")
16 |
17 | params =
18 | url : "https://api.linkedin.com/v1/#{path}"
19 | data : assign({}, params, {access_token : @token.access_token})
20 | error: (err)-> errback(err.url)
21 | success: (response)->
22 | callback
23 | body: response.data
24 | provider: @name
25 | @request_jsonp(params)
26 |
27 |
28 | module.exports = AngelListService
29 |
--------------------------------------------------------------------------------
/src/utils/raven.coffee:
--------------------------------------------------------------------------------
1 | script = require "./script-loader"
2 | _ = require './lodash'
3 | pending = []
4 | userContext = {}
5 |
6 |
7 |
8 | module.exports =
9 | init: (dsn, context)->
10 | if dsn && !window.Raven
11 | script(src: 'https://cdn.ravenjs.com/2.1.1/raven.min.js').then ->
12 | window.Raven.config(dsn, {
13 | release: REVISION,
14 | whitelistUrls: [/hull/]
15 | }).install()
16 | window.Raven.setExtraContext(context)
17 | window.Raven.setUserContext(userContext)
18 | _.map pending.splice(0), (e)->
19 | window.Raven.captureException(e.err, e.ctx)
20 |
21 | setUserContext: (ctx)->
22 | if window.Raven && window.Raven.setUserContext
23 | window.Raven.setUserContext(ctx)
24 | else
25 | userContext = ctx
26 |
27 | captureException: (err, ctx)->
28 | if window.Raven && window.Raven.captureException
29 | window.Raven.captureException(err, ctx)
30 | else
31 | pending.push({ err: err, ctx: ctx })
32 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Description of the change
2 |
3 | > Description here
4 |
5 | ## Type of change
6 | - [ ] Bug fix (non-breaking change that fixes an issue)
7 | - [ ] New feature (non-breaking change that adds functionality)
8 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
9 |
10 | ## Related issues
11 |
12 | > Fix [#1]()
13 |
14 | ## Checklists
15 |
16 | ### Development
17 |
18 | - [ ] Lint rules pass locally
19 | - [ ] The code changed/added as part of this pull request has been covered with tests
20 | - [ ] All tests related to the changed code pass in development
21 |
22 | ### Code review
23 |
24 | - [ ] This pull request has a descriptive title and information useful to a reviewer. There may be a screenshot or screencast attached
25 | - [ ] "Ready for review" label attached to the PR and reviewers mentioned in a comment
26 | - [ ] Changes have been reviewed by at least one other engineer
27 | - [ ] Issue from task tracker has a link to this pull request
28 |
--------------------------------------------------------------------------------
/src/remote/services/google.coffee:
--------------------------------------------------------------------------------
1 | assign = require '../../polyfills/assign'
2 | GenericService = require './generic-service'
3 |
4 | class GoogleService extends GenericService
5 | name : 'google'
6 | path: 'google'
7 |
8 | constructor: (config, gateway)-> super(config,gateway)
9 |
10 | request : (request,callback,errback)=>
11 | token = @getSettings().credentials?.token
12 | return errback('No Google+ User') unless token?
13 |
14 | {method, path, params} = request
15 | method = method.toLowerCase()
16 | return errback('Unable to perform non-GET requests on Google+') unless method=='get'
17 | path = path.substring(1) if (path[0] == "/")
18 |
19 | params =
20 | url : "https://www.googleapis.com/plus/v1/#{path}"
21 | data : assign({}, params, {access_token : token})
22 | error: (err)-> errback(err.url)
23 | success: (response)=>
24 | callback
25 | provider: @name
26 | body: response
27 | @request_jsonp(params)
28 |
29 | module.exports = GoogleService
30 |
--------------------------------------------------------------------------------
/src/remote/services/linkedin.coffee:
--------------------------------------------------------------------------------
1 | assign = require '../../polyfills/assign'
2 | GenericService = require './generic-service'
3 |
4 | class LinkedInService extends GenericService
5 | name : 'linkedin'
6 | path: 'linkedin'
7 |
8 | constructor: (config, gateway)-> super(config,gateway)
9 |
10 | request : (request,callback,errback)=>
11 | token = @getSettings().credentials?.token
12 | return errback('No Likedin User') unless token?
13 |
14 | {method, path, params} = request
15 | method = method.toLowerCase()
16 | return errback('Unable to perform non-GET requests on Likedin') unless method=='get'
17 | path = path.substring(1) if (path[0] == "/")
18 |
19 | params =
20 | url : "https://api.linkedin.com/v1/#{path}"
21 | data : assign({}, params, {oauth2_access_token : token})
22 | error: (err)-> errback(err.url)
23 | success: (response)=>
24 | callback
25 | provider: @name
26 | body: response
27 | @request_jsonp(params)
28 |
29 | module.exports = LinkedInService
30 |
--------------------------------------------------------------------------------
/src/utils/ui/display-banner.coffee:
--------------------------------------------------------------------------------
1 | _ = require '../../utils/lodash'
2 | platform = require 'html!./platform.html'
3 | captcha = require 'html!./captcha.html'
4 |
5 | platformCss = require 'raw!inline-style!./platform.css'
6 |
7 | banners =
8 | platform:
9 | html: platform,
10 | css: platformCss
11 | captcha:
12 | html: captcha
13 | css: platformCss
14 |
15 | module.exports = (banner, root = document.body)->
16 | node = document.createElement('div');
17 | html = banners[banner]?.html
18 | css = banners[banner]?.css
19 | node.innerHTML = html
20 |
21 | for selector, style of css
22 | elements = node.querySelectorAll(selector)
23 | element.setAttribute('style', style) for element in elements
24 |
25 | a = node.getElementsByTagName('a')
26 | _.each a, (element)->
27 | intent = element.getAttribute('data-hull-intent')
28 | element.addEventListener 'click',(e)->
29 | e.stopPropagation()
30 | e.preventDefault()
31 | Hull.emit(intent)
32 |
33 | root.appendChild(node) if root
34 |
35 | node
36 |
--------------------------------------------------------------------------------
/src/remote/services/github.coffee:
--------------------------------------------------------------------------------
1 | GenericService = require './generic-service'
2 | superagent = require 'superagent'
3 | QSEncoder = require '../../utils/query-string-encoder'
4 |
5 | class GithubService extends GenericService
6 | name : 'github'
7 | path : 'github'
8 |
9 | constructor: (config, gateway)-> super(config,gateway)
10 |
11 | request : (request,callback,errback)=>
12 | token = @getSettings().credentials?.token
13 |
14 | {method, path, params} = request
15 | method = method.toUpperCase()
16 | path = path.substring(1) if (path[0] == "/")
17 |
18 | url = "https://api.github.com/#{path}"
19 |
20 | s = superagent(method, url)
21 | if (method=='GET' and params?) then s.query(QSEncoder.encode(params)) else s.send(params)
22 | s.set('Authorization', "token #{token}") if token
23 | s.end (err, response)->
24 | return errback(response.error.message) if response.error
25 | callback
26 | provider: @name
27 | headers: response.headers
28 | body: response.body
29 |
30 | module.exports = GithubService
31 |
--------------------------------------------------------------------------------
/src/client/querystring/index.coffee:
--------------------------------------------------------------------------------
1 | _ = require '../../utils/lodash'
2 | qs = require '../../utils/query-string-encoder'
3 |
4 | pick = (prefix, obj) -> _.reduce obj, (m, v, k) ->
5 | m[k.replace(prefix,'')] = v if k.indexOf(prefix) == 0
6 | m
7 | , {}
8 |
9 | class QueryString
10 | constructor : (traits, track, alias, currentUser)->
11 | @alias = alias
12 | @currentUser = currentUser
13 | @traits = traits
14 | @track = track
15 | @parse()
16 |
17 | getCurrentUserId: -> @currentUser.get('id')
18 |
19 | parse: () =>
20 | q = qs.decode()
21 | return unless _.size(q)
22 |
23 | { hjs_email, hjs_uid, hjs_event, hjs_aid } = q
24 |
25 | @alias(hjs_aid) if hjs_aid
26 |
27 | traits = pick('hjs_trait_', q)
28 | @traits(traits) if _.size(traits)
29 |
30 | @traits({ email: { operation: "setIfNull", value: hjs_email} }) if hjs_email
31 |
32 | attrs = pick('hjs_attr_', q)
33 | @traits(attrs) if _.size(attrs)
34 |
35 | props = pick('hjs_prop_', q)
36 | @tracker.track(hjs_event, props) if hjs_event
37 |
38 |
39 | module.exports = QueryString
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 hull
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/flux/stores/ClientConfigStore.js:
--------------------------------------------------------------------------------
1 | var EventEmitter = require('events').EventEmitter;
2 | var assign = require('../../polyfills/assign');
3 | var RemoteDispatcher = require('../dispatcher/RemoteDispatcher');
4 | var RemoteConstants = require('../constants/RemoteConstants');
5 |
6 | var CHANGE_EVENT = 'change';
7 |
8 | var state = {};
9 |
10 | var ClientConfigStore = assign({}, EventEmitter.prototype, {
11 | emitChange : function() {this.emit(CHANGE_EVENT); },
12 | addChangeListener : function(callback) {this.on(CHANGE_EVENT, callback); },
13 | removeChangeListener: function(callback) {this.removeListener(CHANGE_EVENT, callback); },
14 | getState : function() {return state;},
15 |
16 | dispatcherIndex: RemoteDispatcher.register(function(payload) {
17 | var action = payload.action;
18 | var text;
19 |
20 | switch(action.actionType) {
21 | case RemoteConstants.UPDATE_CLIENT_CONFIG:
22 | state = action.config
23 | ClientConfigStore.emitChange();
24 | break;
25 | }
26 | return true;
27 | })
28 |
29 | });
30 |
31 | module.exports = ClientConfigStore;
32 |
--------------------------------------------------------------------------------
/src/remote/services/instagram.coffee:
--------------------------------------------------------------------------------
1 | assign = require '../../polyfills/assign'
2 | GenericService = require './generic-service'
3 |
4 | class InstagramService extends GenericService
5 | name : 'instagram'
6 | path: 'instagram'
7 |
8 | constructor: (config, gateway)-> super(config,gateway)
9 |
10 | request : (request,callback,errback)=>
11 | config = @getSettings()
12 | return errback('No Likedin User') unless token?
13 |
14 | {method, path, params} = request
15 | method = method.toLowerCase()
16 | path = path.substring(1) if (path[0] == "/")
17 | if method != 'get'
18 | return @wrappedRequest(request, callback, errback)
19 | else
20 | params =
21 | url : "https://api.instagram.com/v1/#{path}"
22 | data : assign({}, params, config)
23 | error: (err)-> errback(err.url)
24 | success: (response)->
25 | callback
26 | provider: @name
27 | body: response.data
28 | headers:
29 | pagination: response.pagination
30 | meta: response.meta
31 | @request_jsonp(params)
32 |
33 | module.exports = InstagramService
34 |
--------------------------------------------------------------------------------
/src/utils/script-loader.coffee:
--------------------------------------------------------------------------------
1 | Promise = require './promises'
2 | _ = require './lodash'
3 | logger = require './logger'
4 |
5 | module.exports = (opts={})->
6 | doc = opts.document || window.document
7 | sc = document.createElement "script"
8 | new Promise (resolve, reject)->
9 | # Reject loading polyfills after 10 seconds,
10 |
11 | errorTimeout = setTimeout ()->
12 | error = new Error("Error loading #{opts.src}.\nConnectivity issue?")
13 | logger.error(error, error.message, error.stack)
14 | reject(error);
15 | , 60000
16 |
17 | if opts.attributes
18 | _.map opts.attributes, (value, key)-> sc.setAttribute(key, value)
19 |
20 | sc.id = opts.id if opts.id
21 |
22 | sc.src = opts.src
23 | done = false
24 |
25 | # http://stackoverflow.com/questions/4845762/onload-handler-for-script-tag-in-internet-explorer
26 | sc.onload = sc.onreadystatechange = () ->
27 | if !done && (!@readyState || @readyState == "loaded" || @readyState == "complete")
28 | clearTimeout(errorTimeout)
29 | done = true;
30 | resolve(sc)
31 | # Handle memory leak in IE
32 | sc.onload = sc.onreadystatechange = null;
33 |
34 | doc.getElementsByTagName('head')[0]?.appendChild(sc)
35 |
--------------------------------------------------------------------------------
/src/utils/dom-walker.coffee:
--------------------------------------------------------------------------------
1 | _ = require './lodash'
2 |
3 | # Returns Link in either data-hull-link='...' or a href='...'
4 | getLinkValue = (node)->
5 | return unless node?
6 | node.dataset?.hullLink || (node.nodeName=='A' && node.href)
7 |
8 | # Returns a Meta Tag
9 | getMetaValue = (name)->
10 | metas = document.getElementsByTagName('meta');
11 | meta = _.find metas, (meta)-> meta.getAttribute('property')==name || meta.getAttribute('name')==name
12 | return unless meta?
13 | meta.content
14 |
15 | # Returns true if it is a DOM node
16 | # http://stackoverflow.com/questions/384286/javascript-isdom-how-do-you-check-if-a-javascript-object-is-a-dom-object
17 | isNode = (o)->
18 | if typeof Node == "object" then o instanceof Node else o and typeof o == "object" and typeof o.nodeType == "number" and typeof o.nodeName=="string"
19 |
20 | getChildren = (n, skipMe)->
21 | r = []
22 | while n and n = n.nextSibling
23 | r.push n if n.nodeType == 1 and n != skipMe
24 | r
25 |
26 | getSiblings = (n) ->
27 | getChildren(n.parentNode.firstChild, n)
28 |
29 | module.exports = {
30 | getSiblings : getSiblings
31 | getChildren : getChildren
32 | getMetaValue : getMetaValue
33 | getLinkValue : getLinkValue
34 | isNode : isNode
35 | }
36 |
--------------------------------------------------------------------------------
/src/utils/lodash.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /* global require */
3 |
4 | module.exports = {
5 | compact: require('lodash/compact'),
6 | uniq: require('lodash/uniq'),
7 | size: require('lodash/size'),
8 | includes: require('lodash/includes'),
9 | reject: require('lodash/reject'),
10 | toArray: require('lodash/toArray'),
11 | defaults: require('lodash/defaults'),
12 | isElement: require('lodash/isElement'),
13 | each: require('lodash/each'),
14 | partition: require('lodash/partition'),
15 | every: require('lodash/every'),
16 | indexOf: require('lodash/indexOf'),
17 | isEmpty: require('lodash/isEmpty'),
18 | throttle: require('lodash/throttle'),
19 | isArray: require('lodash/isArray'),
20 | isEqual: require('lodash/isEqual'),
21 | isFunction: require('lodash/isFunction'),
22 | isObject: require('lodash/isObject'),
23 | isString: require('lodash/isString'),
24 | isUndefined: require('lodash/isObject'),
25 | keys: require('lodash/keys'),
26 | map: require('lodash/map'),
27 | find: require('lodash/find'),
28 | filter: require('lodash/filter'),
29 | values: require('lodash/values'),
30 | omit: require('lodash/omit'),
31 | pick: require('lodash/pick'),
32 | reduce: require('lodash/reduce'),
33 | some: require('lodash/some'),
34 | fromPairs: require('lodash/fromPairs')
35 | };
36 |
--------------------------------------------------------------------------------
/src/remote/wrapped-request.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import _ from "../utils/lodash";
4 | import assign from "../polyfills/assign";
5 |
6 | var wrappedRequest = function(service, gateway, middlewares=[]){
7 | return function query(request, callback, errback){
8 | if(!callback){callback = function(){};}
9 | if(!errback){errback = function(){};}
10 |
11 | var path = request.path;
12 |
13 | if (path[0] === "/"){
14 | path = path.substring(1);
15 | }
16 | path = (service.name!=="hull")? "services/#{service.path}/#{path}":path;
17 |
18 | if (service.name !== "hull"){
19 | request.params = (request.method.toLowerCase()==="delete")?JSON.stringify(request.params||{}):request.params;
20 | }
21 |
22 | var req = assign({}, request, {path});
23 | var handle = gateway.handle(req);
24 | _.each(middlewares, function(m){
25 | handle = handle.then(m, m);
26 | });
27 |
28 | handle = handle.then((response)=>{
29 | response = assign({}, response, { provider: service.name });
30 |
31 | if (response.status >= 200 && response.status < 300) {
32 | return response;
33 | } else {
34 | throw response.body || response;
35 | }
36 | })
37 |
38 | handle.then(callback, errback);
39 |
40 | return handle;
41 | };
42 | };
43 |
44 | module.exports = wrappedRequest;
45 |
--------------------------------------------------------------------------------
/src/utils/ui/platform.css:
--------------------------------------------------------------------------------
1 | .hull-banner-container {
2 | position: fixed;
3 | top: 0;
4 | left: 0;
5 | right: 0;
6 | padding: 30px 0;
7 | background: white;
8 | -webkit-font-smoothing: antialiased;
9 | text-align: center;
10 | }
11 |
12 | .hull-captcha-banner-container {
13 | position: fixed;
14 | top: 0;
15 | left: 0;
16 | right: 0;
17 | height: 100%;
18 | padding: 30px 0;
19 | background: white;
20 | background: rgba(255,255,255,.95)
21 | -webkit-font-smoothing: antialiased;
22 | text-align: center;
23 | }
24 |
25 | .hull-banner-title{
26 | color:#666;
27 | margin:0 0 5px 0;
28 | font-size: 24px;
29 | font-weight: normal;
30 | font-family:'Helvetica Neue',Helvetica,arial,sans-serif;
31 | }
32 |
33 | .hull-banner-text{
34 | font-size: 14px;
35 | margin:0 0 10px 0;
36 | color:#838383;
37 | font-weight: normal;
38 | font-family:'Helvetica Neue',Helvetica,arial,sans-serif;
39 | }
40 |
41 | .hull-banner-button{
42 | -webkit-border-radius: 3px;
43 | -moz-border-radius: 3px;
44 | -ms-border-radius: 3px;
45 | -o-border-radius: 3px;
46 | border-radius: 3px;
47 | margin:0;
48 | background: white;
49 | color:#33ADEC;
50 | border:1px solid #33ADEC;
51 | padding:10px 30px;
52 | font-size: 14px;
53 | line-height: 50px;
54 | font-weight: normal;
55 | }
56 |
57 | .hull-captcha-container {
58 | width: 304px;
59 | margin: 30px auto 0 auto;
60 | }
61 |
--------------------------------------------------------------------------------
/src/utils/get-current-script.js:
--------------------------------------------------------------------------------
1 | import _ from './lodash';
2 |
3 | const URL_REGEX = /(data:text\/javascript(?:;[^,]+)?,.+?|(?:|blob:)(?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/
4 |
5 | function getErrorStack(){
6 | let e = new Error();
7 | let stack = e.stack
8 | if(typeof stack !== 'string' || !stack){
9 | try{
10 | throw e
11 | } catch(err){
12 | stack = err.stack
13 | }
14 | }
15 | return stack
16 | }
17 | function getUrlFromStack(stack=''){
18 | let lines = stack.split('\n');
19 | let urls = _.reduce(lines, function(u, line){
20 | let t = line.match(URL_REGEX)
21 | if(t) {
22 | u.push(t[1])
23 | }
24 | return u
25 | },[]);
26 | // Eliminate Hull.js from the stack, get next closest URL
27 | return _.uniq(urls)[1]
28 | }
29 |
30 | function getCurrentScriptFromUrl(url, scripts){
31 | var i, script = null;
32 |
33 | if (typeof url === 'string' && url) {
34 | for (i = scripts.length; i--; ) {
35 | if (scripts[i].src === url) {
36 | script = scripts[i];
37 | break;
38 | }
39 | }
40 | }
41 | return script;
42 | }
43 |
44 | function getCurrentScript(){
45 | let scripts = document.getElementsByTagName('script');
46 | let stack = getErrorStack();
47 | let url = getUrlFromStack(stack);
48 | return getCurrentScriptFromUrl(url, scripts);
49 | }
50 |
51 | module.exports = getCurrentScript
52 |
--------------------------------------------------------------------------------
/src/remote/services/admin.coffee:
--------------------------------------------------------------------------------
1 | superagent = require 'superagent'
2 | GenericService = require './generic-service'
3 | QSEncoder = require '../../utils/query-string-encoder'
4 | logger = require '../../utils/logger'
5 | RemoteConfigStore = require '../../flux/stores/RemoteConfigStore'
6 |
7 | class HullAdminService extends GenericService
8 | name : 'hull'
9 |
10 | constructor: (config, gateway)->
11 |
12 | request: (request, callback, errback)->
13 | {method, path, params, organization} = request
14 | throw new Error('No organization defined') unless organization?
15 |
16 | method = method.toUpperCase()
17 |
18 | path = path.substring(1) if (path[0] == "/")
19 | top_domain = document.location.host.split('.')
20 | top_domain.shift()
21 | top_domain = top_domain.join('.')
22 | protocol = document.location.protocol
23 | url = "#{protocol}//#{organization}.#{top_domain}/api/v1/#{path}"
24 |
25 | headers = {}
26 | token = RemoteConfigStore.getHullToken()
27 |
28 | headers['AccessToken'] = token if token
29 |
30 | s = superagent(method, url).set(headers)
31 |
32 | if (method=='GET' and params?) then s.query(QSEncoder.encode(params)) else s.send(params)
33 | s.end (err, response = {})->
34 | return errback(response.body, response.error) if err || response.error
35 | callback
36 | provider: 'admin'
37 | body: response.body
38 |
39 | module.exports = HullAdminService
40 |
--------------------------------------------------------------------------------
/src/utils/config-normalizer.coffee:
--------------------------------------------------------------------------------
1 | _ = require '../utils/lodash'
2 | clone = require '../utils/clone'
3 | cookies = require '../utils/cookies'
4 | qs = require '../utils/query-string-encoder'
5 |
6 | flattenSettings = (settings, name)->
7 | nameArray = name.split('_')
8 | nameArray.pop() if nameArray.length > 1
9 | [nameArray.join('_'), settings[name] || {}]
10 |
11 | applyUserCredentials = (config, creds={})->
12 | _.each creds, (c, k)->
13 | return unless _.keys(c).length
14 | config?[k] ?= {} #Never happens except for `hull`
15 | config?[k].credentials = c
16 | config
17 |
18 | sortServicesByType = (settings, types)->
19 | ret = _.map types, (names, type)->
20 | typeSettings = _.fromPairs _.map(names, (name)->flattenSettings(settings,name))
21 | [type, typeSettings]
22 | _.fromPairs ret
23 |
24 | module.exports = (_config={})->
25 | config = clone(_config)
26 | config.debug ?= false
27 |
28 | # This exists because we have 3 endpoints where Credentials and Settings are scattered:
29 | # - remote.html config.settings
30 | # - remote.html config.data.credentials
31 | # - /app/settings
32 | services = sortServicesByType config.services.settings, config.services.types
33 | services.auth ?= {}
34 | services.auth = applyUserCredentials services.auth, config.data.credentials
35 | config.services = services
36 |
37 | config.cookiesEnabled = cookies.enabled()
38 |
39 | config.queryParams = qs.decode() || {}
40 |
41 | config
42 |
--------------------------------------------------------------------------------
/src/client/embeds/sandbox.coffee:
--------------------------------------------------------------------------------
1 | assign = require '../../polyfills/assign'
2 | _ = require '../../utils/lodash'
3 | findUrl = require '../../utils/find-url'
4 | clone = require '../../utils/clone'
5 | throwErr = require '../../utils/throw'
6 |
7 | class Sandbox
8 | constructor : (deployment)->
9 | @ship = deployment.ship
10 | @shipClassName = ".ship-#{@ship.id}"
11 | @deployment = deployment
12 |
13 | @callbacks = []
14 | @_scopes = []
15 |
16 | @hull = assign({}, window.Hull, {
17 | getDocument : @getDocument
18 | getShipClassName : @getShipClassName
19 | track : @track
20 | share : @share
21 | });
22 |
23 | ###*
24 | * Performs a track that has the `ship_id` field set correctly
25 | * @param {[type]} name [description]
26 | * @param {[type]} event={} [description]
27 | * @return {[type]} [description]
28 | ###
29 | track : (name, event={})=>
30 | event.ship_id = @ship.id
31 | Hull.track(name, event)
32 |
33 | share: (opts={}, event={})=>
34 | opts.params = assign({}, opts.params, { ship_id: @ship.id })
35 | Hull.share(opts, event)
36 |
37 |
38 | setDocument : (doc)->
39 | return unless doc?
40 | @_document = doc
41 |
42 | getDocument : ()=> @_document
43 |
44 | getShipClassName : => @shipClassName
45 |
46 | get : ()-> @hull
47 |
48 | destroy: ()=>
49 | @_scopes = []
50 |
51 | module.exports = Sandbox
52 |
--------------------------------------------------------------------------------
/__tests__/utils/trait-test.js:
--------------------------------------------------------------------------------
1 | jest.dontMock("../../src/client/traits");
2 | var Trait = require("../../src/client/traits");
3 |
4 | function makeTrait(key, value) {
5 | var api = jest.genMockFn();
6 | var trait = Trait(api)('foo', value);
7 | return [api, trait];
8 | }
9 |
10 | describe("Traits", function() {
11 |
12 |
13 | describe("with no initial value", function() {
14 |
15 | var api, trait;
16 | beforeEach(() => [api, trait] = makeTrait('foo'));
17 |
18 |
19 | it("should not call api in initialization", ()=>
20 | expect(api).not.toBeCalled()
21 | );
22 |
23 | describe("and an operation after", function() {
24 |
25 |
26 | var methods = [['inc', 1], ['set', 'yeah'], ['dec', 2]];
27 |
28 | methods.map(function([op, val]) {
29 |
30 | it("works with " + op, function() {
31 | trait[op](val);
32 | expect(api).toBeCalledWith('me/traits', 'put', {
33 | name: 'foo',
34 | operation: op,
35 | value: val
36 | });
37 | });
38 |
39 | });
40 |
41 | });
42 |
43 | });
44 |
45 |
46 | describe("with an initial value", function() {
47 |
48 | var api, trait;
49 | beforeEach(() => [api, trait] = makeTrait('foo', 'bar'));
50 |
51 | it("records the value with set on initialization", function() {
52 | expect(api).toBeCalledWith('me/traits', 'put', {
53 | name: 'foo',
54 | operation: 'set',
55 | value: 'bar'
56 | });
57 | });
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/src/remote/channel.coffee:
--------------------------------------------------------------------------------
1 | Promise = require '../utils/promises'
2 | xdm = require '../utils/xdm'
3 |
4 | catchAll = (res)-> res
5 |
6 | class Channel
7 | constructor: (config, services)->
8 | @_ready = {}
9 | @promise = new Promise (resolve, reject)=>
10 | @_ready.resolve = resolve
11 | @_ready.reject = reject
12 |
13 | @rpc = null;
14 |
15 |
16 | try
17 | local = services.getMethods()
18 | @rpc = new xdm.Rpc
19 | acl: config.appDomains
20 | ,
21 | remote:
22 | ready : {}
23 | message : {}
24 | userUpdate : {}
25 | configUpdate : {}
26 | getClientConfig : {}
27 | track : {}
28 | show : {}
29 | hide : {}
30 | local: local
31 |
32 | # Send config to client
33 | @rpc.ready(config)
34 | @rpc.getClientConfig(@_ready.resolve)
35 |
36 | catch e
37 | try
38 | domains = (config.appDomains || []).toString()
39 | whitelisted = domains.replace(/\(\:\[0-9\]\+\)\?\$/g,'').replace(/\^\(https\?\:\/\/\)\?/g,'').replace(/\\/g,'')
40 | e = new Error("#{e.message}, You should whitelist this domain. The following domains are authorized : \n#{whitelisted}");
41 | @_ready.reject(e)
42 | @rpc = new xdm.Rpc({}, local: {}, remote: { loadError: {} })
43 | @rpc.loadError e.message
44 | catch e
45 | throw new Error("Unable to establish communication between Hull Remote and your page. #{e.message}")
46 |
47 | module.exports = Channel
48 |
--------------------------------------------------------------------------------
/__mocks__/superagent.js:
--------------------------------------------------------------------------------
1 | /* global jest */
2 |
3 | var superagent = jest.genMockFunction().mockReturnThis();
4 |
5 | var Response = jest.genMockFunction().mockImplementation(function() {
6 | this.status = 200;
7 | this.ok = true;
8 | });
9 |
10 | Response.prototype.get = jest.genMockFunction();
11 | Response.prototype.toError = jest.genMockFunction();
12 |
13 | var Request = jest.genMockFunction().mockImplementation(function(method, url) {
14 | this.method = method;
15 | this.url = url;
16 | });
17 |
18 | Request.prototype.accept = jest.genMockFunction().mockReturnThis();
19 | Request.prototype.set = jest.genMockFunction().mockReturnThis();
20 | Request.prototype.send = jest.genMockFunction().mockReturnThis();
21 | Request.prototype.field = jest.genMockFunction().mockReturnThis();
22 | Request.prototype.query = jest.genMockFunction().mockReturnThis();
23 |
24 | Request.prototype.end = jest.genMockFunction().mockImplementation(function(callback) {
25 | if (superagent.mockDelay) {
26 | this.delayTimer = setTimeout(callback, 0, superagent.mockError, superagent.mockResponse);
27 |
28 | return;
29 | }
30 |
31 | callback(superagent.mockError, superagent.mockResponse);
32 | });
33 |
34 | Request.prototype.abort = jest.genMockFunction().mockImplementation(function() {
35 | this.aborted = true;
36 |
37 | if (this.delayTimer) {
38 | clearTimeout(this.delayTimer);
39 | }
40 | });
41 |
42 | superagent.Request = Request;
43 | superagent.Response = Response;
44 |
45 | superagent.mockResponse = new Response();
46 | superagent.mockError = null;
47 | superagent.mockDelay = false;
48 |
49 | module.exports = superagent;
50 |
--------------------------------------------------------------------------------
/src/utils/domready.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * domready (c) Dustin Diaz 2012 - License MIT
3 | */
4 | !function (name, definition) {
5 | if (typeof module != 'undefined') module.exports = definition()
6 | else if (typeof define == 'function' && typeof define.amd == 'object') define(definition)
7 | else this[name] = definition()
8 | }('domready', function (ready, scope) {
9 |
10 | var fns = [], fn, f = false
11 | , doc = scope || document
12 | , testEl = doc.documentElement
13 | , hack = testEl.doScroll
14 | , domContentLoaded = 'DOMContentLoaded'
15 | , addEventListener = 'addEventListener'
16 | , onreadystatechange = 'onreadystatechange'
17 | , readyState = 'readyState'
18 | , loadedRgx = hack ? /^loaded|^c/ : /^loaded|^i|^c/
19 | , loaded = loadedRgx.test(doc[readyState])
20 |
21 | function flush(f) {
22 | loaded = 1
23 | while (f = fns.shift()) f()
24 | }
25 |
26 | doc[addEventListener] && doc[addEventListener](domContentLoaded, fn = function () {
27 | doc.removeEventListener(domContentLoaded, fn, f)
28 | flush()
29 | }, f)
30 |
31 |
32 | hack && doc.attachEvent(onreadystatechange, fn = function () {
33 | if (/^c/.test(doc[readyState])) {
34 | doc.detachEvent(onreadystatechange, fn)
35 | flush()
36 | }
37 | })
38 |
39 | return (ready = hack ?
40 | function (fn) {
41 | self != top ?
42 | loaded ? fn() : fns.push(fn) :
43 | function () {
44 | try {
45 | testEl.doScroll('left')
46 | } catch (e) {
47 | return setTimeout(function() { ready(fn) }, 50)
48 | }
49 | fn()
50 | }()
51 | } :
52 | function (fn) {
53 | loaded ? fn() : fns.push(fn)
54 | })
55 | })
56 |
--------------------------------------------------------------------------------
/src/flux/actions/RemoteActions.js:
--------------------------------------------------------------------------------
1 | var RemoteDispatcher= require('../dispatcher/RemoteDispatcher');
2 | var RemoteConstants = require('../constants/RemoteConstants');
3 | var RemoteUserStore = require('../stores/RemoteUserStore');
4 |
5 | var RemoteSettingsActions = {
6 | updateClientConfig: function(config){
7 | RemoteDispatcher.handleAction({
8 | actionType: RemoteConstants.UPDATE_CLIENT_CONFIG,
9 | config: config
10 | });
11 | },
12 | updateRemoteConfig: function(config, options={}){
13 | RemoteDispatcher.handleAction({
14 | actionType: RemoteConstants.UPDATE_REMOTE_CONFIG,
15 | config: config,
16 | options:options
17 | });
18 | },
19 | updateServices: function(services, options={}){
20 | RemoteDispatcher.handleAction({
21 | actionType: RemoteConstants.UPDATE_SERVICES,
22 | services: services,
23 | options:options
24 | });
25 | },
26 | logoutUser: function(options={}){
27 | RemoteDispatcher.handleAction({
28 | actionType: RemoteConstants.LOGOUT_USER,
29 | options: options
30 | });
31 | },
32 | updateUser: function(user, options={}){
33 | RemoteDispatcher.handleAction({
34 | actionType: RemoteConstants.UPDATE_USER,
35 | user:user,
36 | options:options
37 | });
38 | },
39 | updateUserIfMe: function(data,options={}){
40 | // We don't know if it's a Me object. for now it's just a bundle of data from the API.
41 | if(data.body && RemoteUserStore.isSameId(data.body.id)){
42 | RemoteDispatcher.handleAction({
43 | actionType: RemoteConstants.UPDATE_USER,
44 | user:data.body,
45 | options:options
46 | });
47 | }
48 | }
49 | };
50 |
51 | module.exports = RemoteSettingsActions;
52 |
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Hull JS Library
10 |
17 |
38 |
39 |
40 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/flux/stores/RemoteUserStore.js:
--------------------------------------------------------------------------------
1 | var EventEmitter = require('events').EventEmitter;
2 | var assign = require('../../polyfills/assign');
3 | var RemoteDispatcher = require('../dispatcher/RemoteDispatcher');
4 | var RemoteConstants = require('../constants/RemoteConstants');
5 |
6 | var CHANGE_EVENT = 'change';
7 |
8 | var state = {
9 | user:null,
10 | };
11 |
12 | var isSameId = function(value){
13 | return value === (state.user && state.user.id);
14 | }
15 |
16 | var isUpToDate = function(user){
17 | if(!user && !state.user){return true}
18 | return user && state.user && user.updated_at===state.user.updated_at
19 | }
20 |
21 | var RemoteUserStore = assign({}, EventEmitter.prototype, {
22 | emitChange : function(changeEvent) {this.emit(CHANGE_EVENT, changeEvent); },
23 | addChangeListener : function(callback) {this.on(CHANGE_EVENT, callback); },
24 | removeChangeListener: function(callback) {this.removeListener(CHANGE_EVENT, callback); },
25 | getState : function() {return state;},
26 | isSameId : isSameId,
27 | isUpToDate : isUpToDate,
28 |
29 | dispatcherIndex: RemoteDispatcher.register(function(payload) {
30 | var action = payload.action;
31 | var text;
32 |
33 | switch(action.actionType) {
34 | case RemoteConstants.LOGOUT_USER:
35 | state.user = null
36 | if(!action.options.silent===true){
37 | RemoteUserStore.emitChange(action.actionType);
38 | }
39 | break;
40 |
41 | case RemoteConstants.UPDATE_USER:
42 | if(!isUpToDate(action.user)){
43 | state.user = action.user
44 | if(!action.options.silent===true){
45 | RemoteUserStore.emitChange(action.actionType);
46 | }
47 | }
48 | break;
49 | }
50 | return true;
51 | })
52 |
53 | });
54 |
55 | module.exports = RemoteUserStore;
56 |
--------------------------------------------------------------------------------
/src/client/config-check.coffee:
--------------------------------------------------------------------------------
1 | Promise = require '../utils/promises'
2 |
3 | # Wraps config failure
4 | onConfigFailure = (err)->
5 | throw err
6 |
7 |
8 | # Parse the tracked events configuration and standardize it.
9 | formatTrackConfig = (config={})->
10 | switch (Object.prototype.toString.call(config).match(/^\[object (.*)\]$/)[1])
11 | when "Object"
12 | if config.only?
13 | config = { only: (m.toString() for m in config.only) }
14 | else if config.ignore?
15 | config = { ignore: (m.toString() for m in config.ignore) }
16 | else
17 | config
18 | when "RegExp"
19 | config = { only: config.toString() }
20 | when "Array"
21 | config = { only: (m.toString() for m in config) }
22 | # Setup initial referrer
23 | config.referrer = document.referrer if document?.referrer
24 | config
25 |
26 |
27 | module.exports = (config)->
28 | config.track = formatTrackConfig(config.track)
29 | promise = new Promise (resolve, reject)->
30 | msg = "You need to pass some keys to Hull to start it: "
31 | readMore = "Read more about this here : http://www.hull.io/docs/references/hull_js/#hull-init-params-cb-errb"
32 | # Fail right now if we don't have the required setup
33 | if config.orgUrl and config.appId
34 | # Auto add protocol if we dont have one of http://, https://, //
35 | reject(new Error(" You specified orgUrl as #{config.orgUrl}. We do not support protocol-relative URLs in organization URLs yet.")) if config.orgUrl.match(/^\/\//)
36 | config.orgUrl ="https://#{config.orgUrl}" unless config.orgUrl.match(/^http[s]?:\/\//)
37 | resolve()
38 | else
39 | reject(new Error "#{msg} We couldn't find `orgUrl` in the config object you passed to `Hull.init`\n #{readMore}") unless config.orgUrl
40 | reject(new Error "#{msg} We couldn't find `platformId` in the config object you passed to `Hull.init`\n #{readMore}") unless config.appId
41 | promise.then(null, onConfigFailure)
42 | promise
43 |
--------------------------------------------------------------------------------
/src/utils/uuid.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | /*global module*/
3 |
4 | // Private array of chars to use
5 | var CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split("");
6 |
7 | var uuid = function(len, radix) {
8 | var chars = CHARS, uu = [], i;
9 | radix = radix || chars.length;
10 |
11 | if (len) {
12 | // Compact form
13 | for (i = 0; i < len; i++){
14 | uu[i] = chars[0 | Math.random() * radix];
15 | }
16 | } else {
17 | // rfc4122, version 4 form
18 | var r;
19 |
20 | // rfc4122 requires these characters
21 | uu[8] = uu[13] = uu[18] = uu[23] = "-";
22 | uu[14] = "4";
23 |
24 | // Fill in random data. At i==19 set the high bits of clock sequence as
25 | // per rfc4122, sec. 4.1.5
26 | for (i = 0; i < 36; i++) {
27 | if (!uu[i]) {
28 | r = 0 | Math.random() * 16;
29 | uu[i] = chars[(i === 19) ? (r & 0x3) | 0x8 : r];
30 | }
31 | }
32 | }
33 |
34 | return uu.join("");
35 | };
36 |
37 | // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance
38 | // by minimizing calls to random()
39 | var uuidFast = function() {
40 | var chars = CHARS, uu = new Array(36), rnd = 0, r;
41 | for (var i = 0; i < 36; i++) {
42 | if (i === 8 || i === 13 || i === 18 || i === 23) {
43 | uu[i] = "-";
44 | } else if (i === 14) {
45 | uu[i] = "4";
46 | } else {
47 | if (rnd <= 0x02) {
48 | rnd = 0x2000000 + (Math.random() * 0x1000000) | 0;
49 | }
50 | r = rnd & 0xf;
51 | rnd = rnd >> 4;
52 | uu[i] = chars[(i === 19) ? (r & 0x3) | 0x8 : r];
53 | }
54 | }
55 | return uu.join("");
56 | };
57 |
58 | // A more compact, but less performant, RFC4122v4 solution:
59 | var uuidCompact = function() {
60 | return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
61 | var r = Math.random() * 16 | 0,
62 | v = c === "x" ? r : (r & 0x3 | 0x8);
63 | return v.toString(16);
64 | });
65 | };
66 |
67 | module.exports = {uuid, uuidFast, uuidCompact};
68 |
--------------------------------------------------------------------------------
/src/client/traits/index.coffee:
--------------------------------------------------------------------------------
1 | _ = require '../../utils/lodash'
2 | assign = require '../../polyfills/assign'
3 |
4 | class Trait
5 | constructor: (api, name, value)->
6 | @_api = api
7 | @name = name
8 | if value?
9 | # If value is an object, then it has to be compatible
10 | if _.isObject(value) && _.includes(Object.keys(value), 'value') && _.includes(['inc', 'dec', 'set', 'increment', 'decrement', 'setIfNull'], value.operation)
11 | # new Trait('a_number', {value: 20, operation: 'dec'});
12 | @raw assign({},value,{name:@name})
13 | else
14 | # new Trait('a_number', 20);
15 | @set value
16 |
17 | inc: (step = 1)->
18 | @raw({ name: @name, operation: 'inc', value: step })
19 |
20 | increment: (step = 1)->
21 | @raw({ name: @name, operation: 'inc', value: step })
22 |
23 | dec: (step = 1)->
24 | @raw({ name: @name, operation: 'dec', value: step })
25 |
26 | decrement: (step = 1)->
27 | @raw({ name: @name, operation: 'dec', value: step })
28 |
29 | set: (value)->
30 | @raw({ name: @name, operation: 'set', value: value })
31 |
32 | setIfNull: (value)->
33 | @raw({ name: @name, operation: 'setIfNull', value: value })
34 |
35 | raw: (payload)->
36 | @_api.message('me/traits', 'put', payload)
37 |
38 |
39 | module.exports = (api)->
40 | (name, value)->
41 | # We've been passed a Hash, iterate on it to create a trait for each.
42 | if _.isObject(name)
43 | # Hull.traits({
44 | # 'a_number' : {value: 20, operation: 'dec'},
45 | # 'a_number_2': {value: 20, operation: 'dec'},
46 | # 'a_number_3': 20
47 | # });
48 | _.map name, (value, key)-> new Trait(api, key, value)
49 | Hull.emit('hull.traits',name)
50 | Hull.emit('hull.identify',name)
51 | else
52 | # Hull.traits('a_number', {value: 20, operation: 'dec'});
53 | # Hull.traits('a_number', 20);
54 | traits = {}
55 | traits[name] = value
56 | Hull.emit('hull.traits',traits)
57 | new Trait(api, name, value)
58 |
--------------------------------------------------------------------------------
/src/remote/services/track.coffee:
--------------------------------------------------------------------------------
1 | LeakyBucket = require '../../utils/leaky-bucket'
2 | assign = require '../../polyfills/assign'
3 | EventBus = require '../../utils/eventbus'
4 | RemoteUserStore = require '../../flux/stores/RemoteUserStore'
5 | GenericService = require './generic-service'
6 | Base64 = require '../../utils/base64'
7 |
8 |
9 | StructuredEventProps = ['category', 'action', 'label', 'property', 'value']
10 | MarketingProps = ['campaign', 'source', 'medium', 'term', 'content']
11 | TopLevelProps = ['hull_ship_id'].concat(StructuredEventProps)
12 |
13 | Identity = (o)-> o
14 |
15 | class HullTrackService extends GenericService
16 | name : 'hull'
17 |
18 | constructor: (config, gateway)->
19 | super(config, gateway)
20 |
21 | @_request = @wrappedRequest
22 |
23 | if (config.trackRateLimit && config.trackRateLimit.capacity)
24 | @rateLimitter = new LeakyBucket(config.trackRateLimit)
25 | else
26 | @rateLimitter = false
27 |
28 | RemoteUserStore.addChangeListener (change)=>
29 | currentUser = RemoteUserStore.getState().user
30 | currentUserId = currentUser?.id
31 |
32 |
33 | request: (opts, callback, errback) =>
34 | if @rateLimitter
35 | @rateLimitter.throttle().then =>
36 | @_sendRequest(opts, callback, errback)
37 | else
38 | @_sendRequest(opts, callback, errback)
39 |
40 | _sendRequest: (opts, callback, errback) =>
41 | { params, path } = opts
42 |
43 | event = path
44 |
45 | EventBus.emit('remote.tracked', { event, params: params.payload });
46 |
47 | @_request({
48 | path: 't',
49 | method: 'post',
50 | params: {
51 | event: event,
52 | properties: params.payload,
53 | url: params.url,
54 | referer: params.referer || ""
55 | },
56 | nocallback: true
57 | }).then (response)=>
58 | response.provider = 'track'
59 | response
60 | .then (callback || Identity), (errback || Identity)
61 |
62 |
63 | module.exports = HullTrackService
64 |
--------------------------------------------------------------------------------
/src/utils/is-mobile.coffee:
--------------------------------------------------------------------------------
1 | isMobile = ->
2 | # http://stackoverflow.com/questions/11381673/javascript-solution-to-detect-mobile-browser
3 | n = navigator.userAgent||navigator.vendor||window.opera
4 | !! /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(n)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(n.substr(0,4));
5 |
6 | module.exports = isMobile
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Hull.js [  ](https://circleci.com/gh/hull/hull-js)
2 |
3 | # Building the library
4 | Checkout
5 |
6 | git clone git@github.com:hull/hull-js.git
7 |
8 | First, install gulp
9 |
10 | sudo npm install -g gulp
11 |
12 | Then switch to hull-js dir
13 |
14 | cd hull-js
15 | npm install
16 | npm run build
17 |
18 | The last command will start a static HTTP server (port `3001`) that serves the files located in the root folder.
19 |
20 | ## Developing
21 |
22 | A boilerplate app is located in the `app` folder. Here's how to use it:
23 |
24 | ```
25 | cp app/app.example.js app/app.js
26 | gulp
27 | ```
28 |
29 | Gulp will automatically start a Webpack server with live reloading.
30 | When it is done, you can point your browser to [http://localhost:3001](http://localhost:3001)
31 |
32 | __Note__: You must enter some keys in `app/app.js`. Find them by creating an Organization and a Platform at [https://dashboard.hullapp.io](https://dashboard.hullapp.io).
33 |
34 | # Main `gulp` tasks
35 |
36 | * `build`: Builds and executes the tests
37 | * `server` (default): `dist` + starts a live reloading server for development
38 |
39 | # Releasing
40 |
41 | * We use continuous integration.
42 |
43 | * Checkout `master`
44 | * `git flow release start 'YOUR_RELEASE_VERSION_NAME'`
45 | * Merge your changes
46 | * Bump `YOUR_RELEASE_VERSION_NAME` in `bower.json` and `package.json`
47 | * Write Changelog
48 | * Commit changes
49 | * `git flow release finish 'YOUR_RELEASE_VERSION_NAME'`
50 |
51 | # Contributing
52 | You're encouraged to submit pull requests,
53 | propose features and [discuss issues](http://github.com/hull/hull.js/issues).
54 |
55 | If you want to submit code:
56 |
57 | * Fork the project
58 | * Write tests for your new feature or a test that reproduces a bug
59 | * Implement your feature or make a bug fix
60 | * Commit, push and make a pull request. Bonus points for topic branches.
61 |
62 | # License
63 | MIT License. See LICENSE for details.
64 |
65 | # Copyright
66 | Copyright (c) 2015 Hull, Inc.
67 |
--------------------------------------------------------------------------------
/src/client/index.coffee:
--------------------------------------------------------------------------------
1 | _ = require '../utils/lodash'
2 | clone = require '../utils/clone'
3 | EventBus = require '../utils/eventbus'
4 | Entity = require '../utils/entity'
5 | findUrl = require '../utils/find-url'
6 | logger = require '../utils/logger'
7 |
8 | Api = require './api'
9 | Auth = require './auth'
10 | Flag = require './flag/index'
11 | Tracker = require './track/index'
12 | Traits = require './traits/index'
13 | Sharer = require './sharer/index'
14 | QueryString = require './querystring/index'
15 |
16 | utils = require '../utils/utils'
17 |
18 |
19 | class Client
20 | constructor: (channel, currentUser, currentConfig)->
21 |
22 | @currentConfig = currentConfig
23 |
24 | api = new Api(channel, currentUser, currentConfig)
25 | alias = (id) -> api.message({ path: "/me/alias" }, "post", { anonymous_id: id })
26 | auth = new Auth(api, currentUser, currentConfig)
27 | tracker = new Tracker(api)
28 |
29 | sharer = new Sharer(currentConfig);
30 | flag = new Flag(api)
31 | traits = new Traits(api)
32 | qs = new QueryString(traits, tracker.track, alias)
33 |
34 |
35 | if @currentConfig.get('debug.enabled')
36 | EventBus.on 'hull.**', (args...)->
37 | logger.log("--HULL EVENT--[#{@event}]--", args...);
38 |
39 | # Creating the complete hull object we'll send back to the API
40 | @hull =
41 | config : @currentConfig.get
42 | utils : utils
43 | api : api.message
44 | currentUser : currentUser.get
45 | entity : Entity
46 | signup : auth.signup
47 | logout : auth.logout
48 | login : auth.login
49 | resetPassword : auth.resetPassword
50 | confirmEmail : auth.confirmEmail
51 | linkIdentity : auth.linkIdentity
52 | unlinkIdentity : auth.unlinkIdentity
53 | track : tracker.track
54 | trackForm : tracker.trackForm
55 | alias : alias
56 | flag : flag
57 | identify : traits
58 | traits : traits
59 | trait : traits
60 | share : sharer.share
61 | findUrl : findUrl
62 | parseQueryString : qs.parse
63 |
64 | # Return an object that will be digested by Hull main file and
65 | # has everything
66 |
67 | @hull
68 |
69 | module.exports = Client
70 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hull-js",
3 | "version": "0.10.0",
4 | "main": "lib/hull.js",
5 | "dependencies": {
6 | "babel-runtime": "^6.6.1",
7 | "bluebird": "^3.3.5",
8 | "browser-jsonp": "^1.1.4",
9 | "cookies-js": "^1.2.2",
10 | "currentscript": "^1.1.0",
11 | "eventemitter2": "^1.0.3",
12 | "flux": "^2.1.1",
13 | "git-rev-sync": "^1.5.1",
14 | "html-loader": "^0.4.3",
15 | "inline-style-loader": "0.0.5",
16 | "lodash": "^4.11.1",
17 | "merge-stream": "^1.0.0",
18 | "putainde-localstorage": "^4.0.0",
19 | "qs": "^6.1.0",
20 | "raw-loader": "^0.5.1",
21 | "superagent": "^1.8.3"
22 | },
23 | "devDependencies": {
24 | "babel-core": "^6.7.7",
25 | "babel-eslint": "^6.0.3",
26 | "babel-jest": "^12.0.2",
27 | "babel-loader": "^6.2.4",
28 | "babel-plugin-lodash": "^3.1.4",
29 | "babel-plugin-transform-es3-member-expression-literals": "^6.5.0",
30 | "babel-plugin-transform-es3-property-literals": "^6.5.0",
31 | "babel-preset-es2015": "^6.6.0",
32 | "babel-preset-stage-0": "^6.5.0",
33 | "coffee-loader": "^0.7.2",
34 | "coffee-script": "^1.10.0",
35 | "concurrent-transform": "^1.0.0",
36 | "del": "^2.2.0",
37 | "eslint": "^2.8.0",
38 | "gulp": "^3.9.1",
39 | "gulp-awspublish": "^3.0.2",
40 | "gulp-gh-pages": "^0.5.4",
41 | "gulp-invalidate-cloudfront": "hull/gulp-invalidate-cloudfront",
42 | "gulp-notify": "^2.2.0",
43 | "gulp-rename": "^1.2.2",
44 | "gulp-util": "^3.0.7",
45 | "harmonize": "^1.4.4",
46 | "jest-cli": "^12.0.2",
47 | "moment": "^2.13.0",
48 | "ngrok": "^2.1.7",
49 | "node-notifier": "^4.5.0",
50 | "object-assign": "^4.0.1",
51 | "run-sequence": "^1.1.5",
52 | "stats-webpack-plugin": "0.3.1",
53 | "webpack": "^1.13.0",
54 | "webpack-dev-server": "^1.14.1"
55 | },
56 | "jest": {
57 | "testDirectoryName": "__tests__",
58 | "scriptPreprocessor": "/test_utils/preprocessor.js",
59 | "testFileExtensions": [
60 | "coffee",
61 | "js"
62 | ],
63 | "testPathIgnorePatterns": [
64 | "node_modules"
65 | ],
66 | "unmockedModulePathPatterns": [
67 | "/node_modules/object-assign",
68 | "/node_modules/lodash",
69 | "/src/utils/lodash"
70 | ],
71 | "moduleFileExtensions": [
72 | "js",
73 | "json",
74 | "coffee"
75 | ]
76 | },
77 | "scripts": {
78 | "test": "jest",
79 | "build": "gulp build",
80 | "start": "gulp"
81 | },
82 | "engines": {
83 | "node": "8.x"
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/client/embeds/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /*global require, module*/
3 |
4 | import _ from '../../utils/lodash';
5 | import throwErr from '../../utils/throw';
6 | import logger from '../../utils/logger';
7 | import Promise from '../../utils/promises';
8 | import getCurrentScript from '../../utils/get-current-script';
9 | import currentScript from 'currentscript'
10 | import Deployment from './deployment';
11 |
12 | let _initialized = false;
13 | let _context = {};
14 |
15 | module.exports = {
16 | initialize: function(context) {
17 | _initialized = true;
18 | _context = context;
19 | },
20 |
21 | embed: function(deployments, opts, callback, errback) {
22 | if (!deployments || !deployments.length) { return; }
23 | if (opts && opts.reset !== false) { Deployment.resetDeployments()}
24 |
25 | let embeds = [];
26 | let promises = [];
27 |
28 | logger.log('Embedding Deployments', deployments)
29 | for (let d = 0, l = deployments.length; d < l; d++) {
30 | let dpl = new Deployment(deployments[d], _context);
31 | promises.push(dpl.embed({ refresh: true }));
32 | }
33 |
34 | Promise.all(promises).then((deployments)=>{
35 | logger.log('Deployments resolved', deployments)
36 | if (_.isFunction(callback)){
37 | callback(_.map(deployments, 'value'))
38 | }
39 | },errback).catch(throwErr);
40 | },
41 |
42 | onEmbed: function() {
43 | let args = Array.prototype.slice.call(arguments);
44 | let callback;
45 |
46 | while (args.length>0 && !_.isFunction(callback)){
47 | callback = args.shift();
48 | }
49 |
50 | if (!_.isFunction(callback)) return false;
51 |
52 | let currentScript = document.currentScript || getCurrentScript() || document._currentScript;
53 |
54 |
55 | logger.verbose('currentScript', currentScript);
56 |
57 | // detectedScript is null on Chrome. Use this to use either the polyfill or the native implementation.
58 | // Fallback to script detection (how will this work ?)
59 |
60 | // Detect JS embed mode first.
61 | let shipId = currentScript.getAttribute('data-hull-ship-script');
62 | let deployments = Deployment.getDeployments(shipId);
63 | // In JS-mode, callbacks are passed down to the DeploymentStrategy,
64 | // In theother modes, they are ignored because we retreive the callbacks from the import document.
65 | if(deployments && deployments.length){
66 | for (var i = deployments.length - 1; i >= 0; i--) {
67 | if(deployments[i]){
68 | deployments[i].onEmbed(callback)
69 | }
70 | };
71 | }
72 | }
73 | };
74 |
--------------------------------------------------------------------------------
/src/client/sharer/index.coffee:
--------------------------------------------------------------------------------
1 | _ = require '../../utils/lodash'
2 | findUrl = require '../../utils/find-url'
3 | assign = require '../../polyfills/assign'
4 | domWalker = require '../../utils/dom-walker'
5 | Promise = require '../../utils/promises'
6 | qs = require '../../utils/query-string-encoder'
7 | EventBus = require '../../utils/eventbus'
8 |
9 | popup = (location, opts={}, params={})->
10 | return unless location?
11 | share = window.open("#{location}?#{qs.encode(params)}", 'hull_share', "location=0,status=0,width=#{opts.width},height=#{opts.height}")
12 | new Promise (resolve, reject)->
13 | interval = setInterval ->
14 | try
15 | if !share? || share.closed
16 | window.clearInterval(interval)
17 | resolve({})
18 | catch e
19 | reject(e)
20 | , 500
21 |
22 |
23 | class Sharer
24 |
25 | constructor: (currentConfig)->
26 | @currentConfig = currentConfig
27 |
28 | share: (opts, event={})=>
29 | if !_.isObject(opts)
30 | throw new Error("You need to pass an options hash to the share method. You passed: #{JSON.stringify(opts)}")
31 | else if !opts.provider?
32 | throw new Error("You need specify the provider on which to share. You passed: #{opts.provider}")
33 | else if !_.isObject(opts.params)
34 | opts.params = {}
35 | # throw new Error("You need specify some parameters to pass the provider. You passed: #{opts.params}")
36 |
37 | opts.params ||= {}
38 | # If the Sharing URL is not specified, then walk up the DOM to find some URL to share.
39 | # If No url is specified, will walk up to window.location.href.
40 | #
41 | # Lookup Order
42 | # 1. Passed-in url
43 | # 2. Find url from Click Targt
44 | # 3. Ship container node
45 |
46 | opts.params.url = opts.params.url || opts.params.href
47 |
48 | if (!opts.params.url)
49 | opts.params.url = findUrl(event.target)
50 | opts.params.title = opts.params.title || domWalker.getMetaValue('og:title') || document.title
51 |
52 | params = assign({
53 | platform_id: @currentConfig.get('appId')
54 | }, opts.params)
55 |
56 | provider = opts.provider
57 |
58 | popupUrl = @currentConfig.get('orgUrl') + "/api/v1/intent/share/" + opts.provider
59 | sharePromise = popup(popupUrl, { width: 550, height: 420 }, params)
60 |
61 | sharePromise.then (response)=>
62 | data = assign({ url: opts.params.url }, params, { provider })
63 | EventBus.emit("hull.#{opts.provider}.share", data)
64 | assign({}, data, { response })
65 | , (err)-> throw err
66 |
67 |
68 | module.exports = Sharer
69 |
--------------------------------------------------------------------------------
/src/client/track/index.coffee:
--------------------------------------------------------------------------------
1 | _ = require '../../utils/lodash'
2 | EventBus = require '../../utils/eventbus'
3 | assign = require '../../polyfills/assign'
4 |
5 | bnd = if window.addEventListener then 'addEventListener' else 'attachEvent';
6 | unbnd = if window.removeEventListener then 'removeEventListener' else 'detachEvent';
7 | prefix = if (bnd != 'addEventListener') then 'on' else '';
8 |
9 | listen = (el, type, fn, capture) => el[bnd](prefix + type, fn, capture || false);
10 |
11 | class Tracker
12 | constructor : (api, currentUser)->
13 | @api = api
14 | @currentUser = currentUser
15 | @setupTracking()
16 |
17 | getCurrentUserId: -> @currentUser.get('id')
18 |
19 | setupTracking: ->
20 | return if @setup
21 |
22 | @setup = true
23 |
24 | self = this
25 |
26 | EventBus.on 'hull.*.share', (res)->
27 | self.track(this.event, res)
28 |
29 | EventBus.on 'hull.user.create', (me)->
30 | providers = _.map me.identities, 'provider'
31 | self.track 'hull.user.create', { providers: providers, main_identity: me.main_identity }
32 |
33 | EventBus.on 'hull.user.update', (me)->
34 | self.track 'hull.user.update', {}
35 |
36 | EventBus.on 'hull.user.login', (me, provider)->
37 | providers = _.map me.identities, 'provider'
38 | provider = provider || me.main_identity
39 | self.track 'hull.user.login', { provider: provider, providers: providers, main_identity: me.main_identity }
40 |
41 | EventBus.on 'hull.user.logout', ()->
42 | self.track('hull.user.logout')
43 |
44 | trackForm: (forms, ev, properties) =>
45 | return false unless !!forms
46 | forms = [forms] if _.isElement(forms)
47 | _.map forms, (form) =>
48 | return console.log("Not an HTML element", form) unless _.isElement(form)
49 | trackSubmit = (e) =>
50 | e.preventDefault();
51 | evt = if _.isFunction(ev) then ev(form) else ev
52 | props = if _.isFunction(properties) then properties(form) else properties
53 |
54 | _isSubmitted = false
55 | submit = =>
56 | return if _isSubmitted
57 | _isSubmitted = true
58 | form.submit()
59 |
60 | setTimeout submit
61 | , 1000
62 |
63 | @track(evt, props).then submit, submit
64 | $ = (window.jQuery || window.Zepto)
65 | if $ then $(form).submit(trackSubmit) else listen(form, 'submit', trackSubmit)
66 | true
67 |
68 | track: (event, payload, success, failure)=>
69 | @api.message
70 | provider: 'track'
71 | path: event
72 | , 'post', { payload: payload, url: document.location.href, referer: document.referrer }
73 |
74 | module.exports = Tracker
75 |
--------------------------------------------------------------------------------
/src/flux/stores/RemoteConfigStore.js:
--------------------------------------------------------------------------------
1 | var EventEmitter = require('events').EventEmitter;
2 | var _ = require('../../utils/lodash');
3 | var assign = require('../../polyfills/assign');
4 | var RemoteDispatcher = require('../dispatcher/RemoteDispatcher');
5 | var RemoteConstants = require('../constants/RemoteConstants');
6 |
7 | var CHANGE_EVENT = 'change';
8 |
9 | var state = {};
10 |
11 | var RemoteConfigStore = assign({}, EventEmitter.prototype, {
12 | emitChange : function(changeEvent) {this.emit(CHANGE_EVENT, changeEvent); },
13 | addChangeListener : function(callback) {this.on(CHANGE_EVENT, callback); },
14 | removeChangeListener: function(callback) {this.removeListener(CHANGE_EVENT, callback); },
15 | getAuth : function(provider) {
16 | if(state && state.services && state.services.auth && state.services.auth[provider]){
17 | return state.services.auth[provider]
18 | }
19 | return undefined
20 | },
21 | getHullToken : function(){ return state.access_token; },
22 | getState : function(){ return state; },
23 |
24 | dispatcherIndex: RemoteDispatcher.register(function(payload) {
25 | var action = payload.action;
26 | var text;
27 |
28 | switch(action.actionType) {
29 | case RemoteConstants.UPDATE_REMOTE_CONFIG:
30 | state = action.config
31 | if(!action.options.silent===true){
32 | RemoteConfigStore.emitChange(action.actionType);
33 | }
34 | break;
35 |
36 | case RemoteConstants.UPDATE_SERVICES:
37 | state.services = action.services;
38 | if(!action.options.silent===true){
39 | RemoteConfigStore.emitChange(action.actionType);
40 | }
41 | break;
42 |
43 | case RemoteConstants.UPDATE_USER:
44 | state.access_token = action.user.access_token
45 | if(!action.options.silent===true){
46 | RemoteConfigStore.emitChange(action.actionType);
47 | }
48 | break;
49 |
50 | case RemoteConstants.LOGOUT_USER:
51 | delete state.access_token
52 | if(state.services && state.services.auth){
53 | _.map(state.services, function(services, type){
54 | _.map(services, function(service, key){
55 | delete service.credentials
56 | // Since Hull Storage doesnt have the same format as the others :(
57 | if(type==='storage'){ delete service.params }
58 | });
59 | });
60 | }
61 | if(!action.options.silent===true){
62 | RemoteConfigStore.emitChange(action.actionType);
63 | }
64 | break;
65 |
66 | }
67 | return true;
68 | })
69 |
70 | });
71 |
72 | module.exports = RemoteConfigStore;
73 |
--------------------------------------------------------------------------------
/src/client/parse-opts.coffee:
--------------------------------------------------------------------------------
1 | _ = require '../utils/lodash'
2 | assign = require '../polyfills/assign'
3 |
4 | ###
5 | # Parses the parameters for an API call. At this point, they can have two forms
6 | # * [String, ...] where the String is an uri. The request will be made to the default provider
7 | # * [Object, ...] where the Object describes more completely the request. It must provide a "path" key, can provide a "provider" key as well as some default parameters in the "params" key
8 | # In the second form, the optional params can be overridden through parameters at data.api calls
9 | #
10 | # The normalized form is the first one.
11 | #
12 | # @param {Array} the parameters for the API calls
13 | # @return {Array} The normalized form of parameters
14 | ###
15 |
16 | defaultProvider='hull'
17 |
18 | _stringDescription = (desc)->
19 | [desc, defaultProvider, {}]
20 | _objectDescription = (desc)->
21 | path = desc.path
22 | organization = desc.organization if desc.organization?
23 | provider = desc.provider || defaultProvider
24 | params = desc.params || {}
25 | [path, provider, params, organization]
26 |
27 |
28 | parse = (argsArray)->
29 | description = argsArray.shift()
30 | [path, provider, params] = _stringDescription(description) if _.isString(description)
31 | [path, provider, params, organization] = _objectDescription(description) if _.isObject(description)
32 |
33 | throw """
34 | Hull cannot find the Path (URI) you want to send your request to.
35 | Could you check your API Call parameters ? Here's what we have received :
36 | ${JSON.stringify(path)}
37 | """ unless path
38 |
39 | path = path.substring(1) if path[0] == "/"
40 |
41 | ret = []
42 | ret.push(params) if params?
43 | ret = ret.concat(argsArray)
44 |
45 | callback = errback = null
46 | params = {}
47 |
48 | # Parses the params into the correct format to pass to the channel
49 | # Allows flexible signature
50 | # Expects :
51 | # {params}, method, callback, errback
52 | while (next = ret.shift())
53 | type = typeof next
54 | if type == 'string' && !method
55 | method = next.toLowerCase()
56 | else if (type == 'function' && (!callback || !errback))
57 | if !callback
58 | callback = next
59 | else if (!errback)
60 | errback = next
61 | else if (type == 'object')
62 | params = assign(params, next)
63 | else
64 | throw new TypeError("Invalid argument passed to Hull.api(): " + next)
65 |
66 | method ?= 'get'
67 |
68 | callback = callback || ->
69 | errback = errback || ->
70 |
71 | {
72 | opts: { provider, path, method, params, organization },
73 | callback: callback
74 | errback : errback
75 | }
76 |
77 |
78 | module.exports = parse
79 |
--------------------------------------------------------------------------------
/src/client/current-user.coffee:
--------------------------------------------------------------------------------
1 | _ = require '../utils/lodash'
2 | EventBus = require '../utils/eventbus'
3 | cookies = require '../utils/cookies'
4 | Base64 = require '../utils/base64'
5 | clone = require '../utils/clone'
6 | getKey = require '../utils/get-key'
7 | Raven = require '../utils/raven'
8 |
9 |
10 | # Fix for http://www.hull.io/docs/users/backend on browsers where 3rd party cookies disabled
11 | fixCookies = (userSig)->
12 | try
13 | if window.jQuery && _.isFunction(window.jQuery.fn.ajaxSend)
14 | window.jQuery(document).ajaxSend (e, xhr, opts)->
15 | if userSig && !opts.crossDomain
16 | xhr.setRequestHeader('Hull-User-Sig', userSig)
17 | catch e
18 |
19 |
20 | setUserContext = (user)->
21 | try
22 | if user && user.id
23 | Raven.setUserContext({
24 | id: user.id,
25 | email: user.email
26 | })
27 | catch e
28 |
29 |
30 |
31 | class CurrentUser
32 |
33 | constructor: ->
34 | @clear()
35 |
36 | get: (key) =>
37 | # Ensure logged out user gives Null, not False
38 | return null if !key and !@me
39 | getKey(@me, key)
40 |
41 | clear: ()=>
42 | @me = null
43 |
44 | hasIdentity : (identity)=>
45 | return false unless identity?
46 | identities = @me?.identities
47 | identity = identity.toLowerCase()
48 | return false unless identities and identity
49 | _.some identities, (i) -> i.provider.toLowerCase()==identity
50 |
51 | # We force create and emit a user.
52 | setLoginStatus : (me, provider)=>
53 | @me = me
54 | EventBus.emit('hull.user.create', me) unless me?.stats?.sign_in_count?
55 | EventBus.emit('hull.user.login', me, provider)
56 |
57 | init : (me)=>
58 | # Init is silent
59 | @me = me
60 | setUserContext(me)
61 | me
62 |
63 | set : (me) =>
64 | prevUpdatedAt = @me?.updated_at
65 | prevId = @me?.id
66 |
67 | # Silently update now
68 | @me = me
69 | setUserContext(me)
70 |
71 | # User was updated. Emit Update
72 | if prevUpdatedAt != me?.updated_at
73 | EventBus.emit('hull.user.update', me)
74 | if me?.id
75 | # We have a user
76 | # User changed. Do the full update.
77 | if prevId != me.id
78 | @setLoginStatus(me)
79 | else
80 | # We have no user anymore
81 | # Emit logout event
82 | if prevId
83 | EventBus.emit('hull.user.logout')
84 |
85 | me
86 |
87 | setCookies : (headers={}, appId)->
88 | cookieName = "hull_#{appId}"
89 | if headers && headers['Hull-User-Id'] && headers['Hull-User-Sig']
90 | val = Base64.encode(JSON.stringify(headers))
91 | @signature = val
92 | fixCookies(val)
93 | cookies.set(cookieName, val, path: "/")
94 | else
95 | @signature = false
96 | cookies.remove(cookieName, path: "/")
97 |
98 |
99 | module.exports = CurrentUser
100 |
--------------------------------------------------------------------------------
/src/flux/stores/RemoteHeaderStore.js:
--------------------------------------------------------------------------------
1 | var EventEmitter = require('events').EventEmitter;
2 | var assign = require('../../polyfills/assign');
3 | var RemoteDispatcher = require('../dispatcher/RemoteDispatcher');
4 | var RemoteConstants = require('../constants/RemoteConstants');
5 | var RemoteUserStore = require('../stores/RemoteUserStore');
6 |
7 | var CHANGE_EVENT = 'change';
8 | var ACCESS_TOKEN_HEADER = 'Hull-Access-Token';
9 | var USER_ID_HEADER = 'Hull-User-Id';
10 | var APP_ID_HEADER = 'Hull-App-Id';
11 |
12 | var state = {
13 | headers: {
14 | 'Accept':'application/json',
15 | 'Content-Type':'application/json'
16 | }
17 | };
18 | function getHeader(header) {
19 | return state.headers[header];
20 | }
21 | function setHeader(header,value) {
22 | state.headers[header] = value;
23 | }
24 | function destroyHeader(header) {
25 | delete state.headers[header];
26 | }
27 |
28 | var RemoteHeaderStore = assign({}, EventEmitter.prototype, {
29 | emitChange : function(change) {this.emit(CHANGE_EVENT, change); },
30 | addChangeListener : function(callback) {this.on(CHANGE_EVENT, callback); },
31 | removeChangeListener: function(callback) {this.removeListener(CHANGE_EVENT, callback); },
32 | getState : function() {return state.headers;},
33 | getHeader : function(h) {return state.headers[h];},
34 |
35 | dispatcherIndex: RemoteDispatcher.register(function(payload) {
36 | var action = payload.action;
37 | var text;
38 | switch(action.actionType) {
39 |
40 | case RemoteConstants.UPDATE_REMOTE_CONFIG:
41 | if(action){
42 | if(action.config && action.config.appId){
43 | setHeader(APP_ID_HEADER, action.config.appId);
44 | }
45 | }
46 | break;
47 |
48 | case RemoteConstants.UPDATE_SETTINGS:
49 | if(action && action.services && action.services.auth && action.services.auth.hull && action.services.auth.hull.credentials){
50 | setHeader(ACCESS_TOKEN_HEADER, action.services.auth.hull.credentials.access_token);
51 | }
52 | break;
53 |
54 | case RemoteConstants.UPDATE_USER:
55 | if(action.user.id != getHeader(USER_ID_HEADER) || action.user.access_token != getHeader(ACCESS_TOKEN_HEADER)){
56 | if (action.user && action.user.access_token){
57 | setHeader(USER_ID_HEADER, action.user.id)
58 | setHeader(ACCESS_TOKEN_HEADER,action.user.access_token);
59 | } else {
60 | destroyHeader(ACCESS_TOKEN_HEADER);
61 | destroyHeader(USER_ID_HEADER);
62 | }
63 | RemoteHeaderStore.emitChange(action.actionType);
64 | }
65 | break;
66 |
67 | case RemoteConstants.LOGOUT_USER:
68 | destroyHeader(ACCESS_TOKEN_HEADER);
69 | destroyHeader(USER_ID_HEADER);
70 | if(!action.options.silent===true){
71 | RemoteHeaderStore.emitChange(action.actionType);
72 | }
73 | break;
74 |
75 | }
76 | return true;
77 | })
78 |
79 | });
80 |
81 | module.exports = RemoteHeaderStore;
82 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var _ = require('lodash');
2 | var path = require('path');
3 | var webpack = require('webpack');
4 | var config = require('./config');
5 | var StatsPlugin = require('stats-webpack-plugin');
6 | var assign = require('object-assign');
7 |
8 | var devOutput = _.extend({},config.output,{publicPath: config.previewUrl+config.assetsFolder+'/'});
9 |
10 | if(config.hotReload){
11 | var devEntry = _.reduce(config.entry,function(entries,v,k){
12 | entries[k] = ['webpack-dev-server/client?'+config.previewUrl, 'webpack/hot/dev-server', v];
13 | return entries;
14 | },{});
15 | var devPlugins = [new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin()]
16 | } else {
17 | var devEntry = config.entry
18 | var devPlugins = [new webpack.NoErrorsPlugin()]
19 | }
20 |
21 | debugOutput = assign({},config.output,{filename: '[name].debug.js',});
22 |
23 | var uglifyPlugin = new webpack.optimize.UglifyJsPlugin({
24 | comments: false,
25 | minimize:true,
26 | ascii_only:true,
27 | quote_keys:true,
28 | sourceMap: true,
29 | beautify: false,
30 | compress: {
31 | warnings: false,
32 | drop_console: false,
33 | drop_debugger:false,
34 | dead_code:true,
35 | // comparisons: true,
36 | // conditionals:true
37 | join_vars:true
38 | }
39 | })
40 |
41 | module.exports = {
42 | development:{
43 | browser: {
44 | name : 'browser',
45 | devtool : '#source-map',
46 | devServer: true,
47 | entry : devEntry,
48 | output : devOutput,
49 | resolve : {
50 | root: [path.join(__dirname, "bower_components")],
51 | extensions: config.extensions
52 | },
53 | module : {loaders: config.loaders},
54 | plugins: config.plugins.concat([
55 | new webpack.DefinePlugin({'process.env': {'NODE_ENV': JSON.stringify('development') } })
56 | ]).concat(devPlugins)
57 | }
58 | },
59 | production:{
60 | browser: {
61 | name : 'browser',
62 | devtool : '#source-map',
63 | entry : config.entry,
64 | output : config.output,
65 | resolve : {
66 | root: [path.join(__dirname, "bower_components")],
67 | extensions: config.extensions
68 | },
69 | module : {loaders: config.loaders},
70 | plugins : config.plugins.concat([
71 | new webpack.DefinePlugin({'process.env': {'NODE_ENV': JSON.stringify('production') } }),
72 | uglifyPlugin,
73 | new webpack.optimize.DedupePlugin(),
74 | new StatsPlugin(path.join(config.outputFolder, 'stats.json'), { chunkModules: true, profile: true })
75 | ])
76 | }
77 | },
78 | debug:{
79 | browser: {
80 | name : 'browser',
81 | devtool : '#source-map',
82 | entry : config.entry,
83 | output : debugOutput,
84 | resolve : {
85 | root: [path.join(__dirname, "bower_components")],
86 | extensions: config.extensions
87 | },
88 | module : {loaders: config.loaders},
89 | plugins : config.plugins.concat([
90 | new webpack.DefinePlugin({'process.env': {'NODE_ENV': JSON.stringify('production') } }),
91 | new webpack.optimize.DedupePlugin()
92 | ])
93 | }
94 | },
95 | }
96 |
--------------------------------------------------------------------------------
/src/client/api.coffee:
--------------------------------------------------------------------------------
1 | Promise = require '../utils/promises'
2 | EventBus = require '../utils/eventbus'
3 | logger = require '../utils/logger'
4 | parseOpts = require './parse-opts'
5 | channel = require './channel'
6 |
7 | class Api
8 | constructor : (channel, currentUser, currentConfig)->
9 | @channel = channel
10 | @currentConfig = currentConfig
11 | @currentUser = currentUser
12 |
13 | EventBus.on 'hull.settings.update', (settings)-> currentConfig.setRemote(settings,'settings')
14 |
15 | data = @currentConfig.getRemote('data')
16 | authScope = if data.headers?['Hull-Auth-Scope'] then data.headers['Hull-Auth-Scope'].split(":")[0] else ''
17 |
18 | @currentUser.setCookies(data.headers, currentConfig.get('appId')) if (data.headers?)
19 |
20 | @_message = (params, userSuccessCallback, userErrorCallback)=>
21 |
22 | ###
23 | # Sends the message described by @params to xdm
24 | # @param {Object} contains the provider, uri and parameters for the message
25 | # @param {Function} optional a success callback
26 | # @param {Function} optional an error callback
27 | # @return {Promise}
28 | ###
29 | message: ()=>
30 | return logger.error("Hull Api is not initialized yet. You should run your app from the callback of Hull.ready()") unless @channel.rpc
31 | {opts, callback, errback} = parseOpts(Array.prototype.slice.call(arguments))
32 |
33 | new Promise (resolve, reject)=>
34 | onSuccess = (res={})=>
35 | # intercept calls and update current user
36 | @updateCurrentUser(res).then () =>
37 | @updateCurrentUserCookies(res.headers, res.provider)
38 | callback(res.body)
39 | resolve(res.body)
40 |
41 | onError = (response, error)=>
42 | errback(response.message, error)
43 | reject(response.message, error)
44 |
45 | @channel.rpc.message(opts, onSuccess, onError)
46 |
47 | refreshUser: ()=>
48 | new Promise (resolve, reject)=>
49 | onSuccess = (response={})=>
50 | @currentUser.set(response.me)
51 | @currentConfig.setRemote(response.services, 'services')
52 | @updateCurrentUserCookies(response.headers, 'hull')
53 | resolve(response.me)
54 |
55 | onError = (err={})=>
56 | reject(err)
57 |
58 | @channel.rpc.refreshUser(onSuccess, onError)
59 |
60 | updateCurrentUser: (response) =>
61 | header = response.headers?['Hull-User-Id']
62 |
63 | p = Promise.resolve()
64 | return p unless header?
65 |
66 | if response.body?.id == header
67 | # Set currentUser to the response body if it contains the current user.
68 | @currentUser.set(response.body)
69 | else
70 | # Fetch the currentUser from the server if the header does not match with
71 | # the currentUser id.
72 | u = @currentUser.get()
73 | p = @refreshUser() if !u || u.id != header
74 |
75 | return p
76 |
77 | _updateCurrentUser: (user={}, headers={})=>
78 | header = headers?['Hull-User-Id']
79 | @currentUser.set(user) if header && user?.id == header
80 |
81 | updateCurrentUserCookies: (headers, provider)=>
82 | @currentUser.setCookies(headers, @currentConfig.get('appId')) if (headers? and provider == 'hull')
83 |
84 | module.exports = Api
85 |
--------------------------------------------------------------------------------
/src/hull-remote.coffee:
--------------------------------------------------------------------------------
1 | require './utils/load-polyfills'
2 |
3 | Raven = require './utils/raven'
4 | EventBus = require './utils/eventbus'
5 | logger = require './utils/logger'
6 | configNormalize = require './utils/config-normalizer'
7 | locationOrigin = require './utils/location-origin'
8 | qs = require './utils/query-string-encoder'
9 | Services = require './remote/services'
10 | Gateway = require './remote/gateway'
11 | Channel = require './remote/channel'
12 |
13 | ClientConfigStore = require './flux/stores/ClientConfigStore'
14 | RemoteHeaderStore = require './flux/stores/RemoteHeaderStore'
15 | RemoteUserStore = require './flux/stores/RemoteUserStore'
16 | RemoteConfigStore = require './flux/stores/RemoteConfigStore'
17 |
18 | RemoteActions = require './flux/actions/RemoteActions'
19 | RemoteConstants = require './flux/constants/RemoteConstants'
20 |
21 |
22 |
23 | captureException = (err, ctx)->
24 | Raven.captureException(err, ctx)
25 |
26 | hull = undefined
27 |
28 |
29 | Hull = (remoteConfig)->
30 | return hull if hull
31 | config = configNormalize(remoteConfig)
32 |
33 | Raven.init(config.queryParams.ravenDsn, {
34 | runtime: 'hull-remote',
35 | orgUrl: locationOrigin(),
36 | appId: config.appId
37 | })
38 |
39 | # The access token stuff is a Safari hack:
40 | # Safari doesn't send response tokens for remote exchange
41 | RemoteActions.updateRemoteConfig(config)
42 |
43 | hull = {config}
44 | gateway = new Gateway(config)
45 | services = new Services(config, gateway)
46 | channel = new Channel(config, services)
47 |
48 | ClientConfigStore.addChangeListener (change)=>
49 | logger.init((ClientConfigStore.getState()||{}).debug)
50 |
51 | RemoteConfigStore.addChangeListener (change)=>
52 | # Notify client whenever settings change
53 | switch change
54 | when RemoteConstants.UPDATE_REMOTE_CONFIG, RemoteConstants.UPDATE_SETTINGS
55 | channel.rpc.configUpdate(RemoteConfigStore.getState())
56 | break
57 |
58 | RemoteUserStore.addChangeListener (change)=>
59 | # Notify client whenever user changes
60 | switch change
61 | when RemoteConstants.UPDATE_USER, RemoteConstants.CLEAR_USER
62 | channel.rpc.userUpdate(RemoteUserStore.getState().user)
63 | break
64 |
65 | request = services.services.hull.request
66 |
67 | hideOnClick = (e)-> channel.rpc.hide()
68 |
69 | subscribeToEvents = (clientConfig)->
70 |
71 | if document.addEventListener
72 | document.addEventListener('click', hideOnClick)
73 | else if document.attachEvent
74 | document.attachEvent('onclick', hideOnClick)
75 |
76 | EventBus.on 'remote.iframe.show', ()-> channel.rpc.show()
77 | EventBus.on 'remote.iframe.hide', ()-> channel.rpc.hide()
78 |
79 | EventBus.on 'remote.track', (payload)->
80 | services.services.track.request({params:payload.params, path:payload.event})
81 |
82 | EventBus.on 'remote.tracked', (payload)->
83 | channel.rpc.track(payload)
84 |
85 | clientConfig
86 |
87 |
88 | channel.promise
89 | .then(subscribeToEvents)
90 | .then (clientConfig)->
91 | RemoteActions.updateClientConfig(clientConfig)
92 | .catch (err)->
93 | captureException(err)
94 | console.error("Could not initialize Hull: #{err.message}")
95 |
96 | Hull.version = VERSION
97 | module.exports = Hull
98 |
--------------------------------------------------------------------------------
/src/client/initialize-platform.js:
--------------------------------------------------------------------------------
1 | import emptyFunction from '../utils/empty-function';
2 | import request from 'superagent';
3 |
4 | function initializeShopifyPlatform(context, currentConfig, hull) {
5 | const { customerId, accessToken, callbackUrl } = currentConfig.get();
6 |
7 | try {
8 | const { pathname, search, hash } = document.location;
9 | const { domain, domain_redirect, ssl_enabled, alias_cart_enabled } = (context.app && context.app.settings) || {};
10 |
11 | if (domain_redirect && domain && pathname.match(/^\/account\/login/) && domain !== document.location.host) {
12 |
13 | const protocol = ssl_enabled ? 'https:' : 'http:';
14 | document.location.href = [
15 | protocol,
16 | '//',
17 | domain,
18 | pathname,
19 | search,
20 | hash
21 | ].join('');
22 | }
23 | } catch (err) {}
24 |
25 | const currentUser = hull.currentUser();
26 |
27 | if (!customerId && currentUser) {
28 | hull.api('services/shopify/login', { return_to: document.location.href }).then(function(r) {
29 | // If the platform has multipass enabled and we are NOT inside the customizer
30 | // we can log the customer in without knowing his password.
31 |
32 | const { inits_count } = currentConfig.identifySession() || {};
33 |
34 | if (r.auth === 'multipass' && inits_count < 2) {
35 | if (!(callbackUrl || '').match('__hull_proxy__')) {
36 | let l = 'https://' + document.location.host + '/account/login/multipass/' + r.token;
37 | window.location = l;
38 | }
39 | } else {
40 | hull.logout();
41 | }
42 | });
43 | } else if (/^[0-9]+$/.test(customerId) && (!accessToken || !currentUser)) {
44 | hull.api('services/shopify/customers/' + customerId, 'put');
45 | }
46 |
47 | if (customerId) {
48 | Hull.on('hull.user.logout', function() {
49 | document.location = '/account/logout';
50 | });
51 | }
52 |
53 | function aliasCart(cart = {}) {
54 | if (cart && cart.token) {
55 | const CART_ALIAS_KEY = 'cartAliasToken';
56 | const aliasToken = [cart.token, customerId].join('-');
57 | if (aliasToken !== currentConfig.storageGet(CART_ALIAS_KEY)) {
58 | hull.api('services/shopify/cart', { cart }, 'post').then(function() {
59 | currentConfig.storageSet(CART_ALIAS_KEY, aliasToken);
60 | });
61 | }
62 | }
63 | }
64 |
65 | // if (window.HullShopify && window.HullShopify.cart && window.HullShopify.cart.token) {
66 | // aliasCart(window.HullShopify.cart);
67 | // } else if (window.HullShopify && window.HullShopify.template) {
68 | // let browser = {};
69 | // try {
70 | // browser = currentConfig.identifyBrowser();
71 | // } catch (err) {
72 | // browser = {}
73 | // }
74 | // request.post('/cart.js')
75 | // .send({})
76 | // .set('Accept', 'application/json')
77 | // .end((err, res={}) => {
78 | // aliasCart(res.body);
79 | // })
80 | // }
81 | }
82 |
83 | function getPlatformInitializer(platform) {
84 | if (platform.type === 'platforms/shopify_shop') {
85 | return initializeShopifyPlatform;
86 | } else {
87 | return emptyFunction;
88 | }
89 | }
90 |
91 | function initializePlatform(context, currentConfig, hull) {
92 | const initializer = getPlatformInitializer(context.app);
93 | return initializer(context, currentConfig, hull);
94 | }
95 |
96 | module.exports = initializePlatform;
97 |
--------------------------------------------------------------------------------
/src/client/script-tag-config.coffee:
--------------------------------------------------------------------------------
1 | _ = require '../utils/lodash'
2 | logger = require '../utils/logger'
3 |
4 | dasherize = (str)->
5 | str.replace /[A-Z]/, (c) -> "-" + c.toLowerCase()
6 |
7 | origin = window.location.origin || window.location.protocol + "//" + window.location.hostname
8 | if window.location.port
9 | origin += ':' + window.location.port
10 |
11 | httpsRegex = /^https:|^\/\//
12 |
13 | valid =
14 | regex: (reg)-> (val, key)->
15 | check = reg.test(val)
16 | if !check
17 | logger.error("[Hull.init] invalid format for '#{key}'. Value '#{val}' should match #{reg.toString()}")
18 | check
19 |
20 | https: (val, key)->
21 | check = httpsRegex.test(val)
22 | return true if check
23 | logger.warn("[Hull.init] #{key} should be loaded via https. Current value is ", val)
24 | true
25 |
26 |
27 | transform =
28 | url: (url, key)->
29 | if url && url.length > 0
30 | a = document.createElement('a')
31 | a.href = url
32 | a.href
33 |
34 | bool: (str, key)->
35 | switch str
36 | when 'true', true then true
37 | when 'false', false then false
38 | else undefined
39 |
40 | initParams = {
41 | appId: {
42 | validation: valid.regex(/^[a-f0-9]{24}$/i)
43 | required: true
44 | altKeys: ['shipId', 'platformId']
45 | },
46 | orgUrl: {
47 | validation: valid.https
48 | required: true
49 | },
50 | jsUrl: {
51 | transform: transform.url
52 | validation: valid.https
53 | altKeys: ['src']
54 | },
55 | callbackUrl: {
56 | transform: transform.url
57 | validation: valid.regex(new RegExp("^" + origin + "/"))
58 | },
59 | debug : { default: false, transform: transform.bool },
60 | verbose : { default: false, transform: transform.bool },
61 | embed : { default: true, transform: transform.bool },
62 | autoStart : { default: true, transform: transform.bool },
63 | accessToken : { default: null },
64 | customerId : { default: null },
65 | anonymousId : { default: null },
66 | sessionId : { default: null },
67 | ravenDsn : { default: null }
68 | }
69 |
70 | getAttribute = (el, k)->
71 | el.getAttribute("data-" + dasherize(k)) || el.getAttribute("data-" + k) || el.getAttribute(dasherize(k)) || el.getAttribute(k)
72 |
73 | getParamValue = (el, param, key)->
74 | keys = [key].concat(param.altKeys || [])
75 | transform = param.transform || (o)-> o
76 | value = _.reduce(keys, (ret, k)->
77 | if ret == null
78 | value = transform(getAttribute(el, k), key)
79 | valid = value? && (!param.validation || param.validation(value, k, el))
80 | ret = value if valid
81 | ret
82 | , null)
83 |
84 | if value? then value else param.default
85 |
86 | module.exports = ->
87 | hull_js_sdk = document.getElementById('hull-js-sdk') || document.querySelector('[org-url][platform-id][data-org-url][data-platform-id]')
88 |
89 | return unless hull_js_sdk
90 |
91 | # Map known config values to Hull init
92 | out = _.reduce initParams, (config, param, key)->
93 | return unless config
94 | value = getParamValue(hull_js_sdk, param, key)
95 | if value?
96 | config[key] = value
97 | else if param.required
98 | config = false
99 | config
100 | , {}
101 | if out
102 | logger.error("[Hull.init] jsUrl NEEDS be loaded via https if orgUrl is https ") if httpsRegex.test(out.orgUrl) and not httpsRegex.test(out.jsUrl)
103 | out
104 |
--------------------------------------------------------------------------------
/src/client/embeds/strategies/js.coffee:
--------------------------------------------------------------------------------
1 | Promise = require '../../../utils/promises'
2 | _ = require '../../../utils/lodash'
3 | setStyle = require '../../../utils/set-style'
4 | logger = require '../../../utils/logger'
5 | throwErr = require '../../../utils/throw'
6 | scriptLoader = require '../../../utils/script-loader'
7 | Sandbox = require '../sandbox'
8 |
9 | scripts = {}
10 |
11 | getScript = (deployment)-> scripts[deployment.ship.id]
12 | setScript = (deployment, sc) -> scripts[deployment.ship.id] = sc
13 |
14 | class JSDeploymentStrategy
15 | scopeStyles : false
16 |
17 | constructor : (deployment)->
18 | @deployment = deployment
19 | @sandbox = new Sandbox(deployment)
20 | @ready = {}
21 | @insertions = []
22 | @ready.promise = new Promise (resolve, reject)=>
23 | @ready.resolve = resolve
24 | @ready.reject = reject
25 |
26 | addShipClasses : (el)->
27 | el.className = el.className + " ship-#{@deployment.ship.id} ship-deployment-#{@deployment.id}"
28 | el.setAttribute('data-hull-deployment', @deployment.id)
29 | el.setAttribute('data-hull-ship', @deployment.ship.id)
30 |
31 | ###*
32 | * Insert an element at the right position relative to a target.
33 | * @param {Node} el The element to insert
34 | * @param {Node} target The target where to insert the content
35 | * @return {Node} el
36 | ###
37 | insert: (el, target)->
38 | @addShipClasses(el)
39 | setStyle(el, {width:@deployment.settings._width || '100%', height:@deployment.settings.height})
40 | switch @deployment.settings._placement
41 | when 'before' then target.parentNode.insertBefore(el, target)
42 | when 'after' then target.parentNode.insertBefore(el, target.nextSibling)
43 | when 'top' then target.insertBefore(el, target.firstChild)
44 | when 'replace'
45 | if target.nodeName == 'IMG'
46 | target.parentNode.replaceChild(el, target)
47 | else
48 | target.removeChild(target.firstChild) while target.firstChild
49 | target.appendChild(el)
50 | else
51 | # Embed at append
52 | target.appendChild(el)
53 | el
54 |
55 | destroy: ()=>
56 | _.map @insertions, (insertion)-> insertion.el?.parentNode?.removeChild(insertion.el)
57 | @insertions = []
58 |
59 | ###*
60 | * Embeds a Script in the page
61 | * @return {promise} a promise for the onLoad event
62 | ###
63 | embed : (targets)->
64 | @elements = []
65 |
66 | for target in targets
67 | el = document.createElement('div')
68 | @addInsertion(@insert(el, target))
69 |
70 |
71 | sc = document.querySelector("[data-hull-ship-script=\"#{@deployment.ship.id}\"]");
72 | if !getScript(@deployment)
73 | setScript(@deployment, true)
74 | attributes =
75 | 'data-hull-deployment' : @deployment.id
76 | 'data-hull-ship-script' : @deployment.ship.id
77 |
78 | scriptLoader({src:@deployment.ship.index, attributes})
79 | .then (args...)=>
80 | @ready.resolve(args...)
81 | .catch (err)=>
82 | @ready.reject(err)
83 | throwErr(err)
84 | else
85 | new Promise (resolve, reject)=>
86 | @ready.resolve()
87 | resolve()
88 |
89 | addInsertion : (el)=>
90 | @insertions.push el
91 |
92 | onEmbed: (callback)=>
93 | if callback
94 | @ready.promise.then ()=>
95 | _.map @insertions, (insertion)=>
96 | callback(insertion, @deployment.getPublicData(), @sandbox.hull)
97 | .catch throwErr
98 |
99 | module.exports = JSDeploymentStrategy
100 |
--------------------------------------------------------------------------------
/src/client/channel.coffee:
--------------------------------------------------------------------------------
1 | xdm = require '../utils/xdm'
2 | domready = require '../utils/domready'
3 | _ = require '../utils/lodash'
4 | EventBus = require '../utils/eventbus'
5 | logger = require '../utils/logger'
6 | Promise = require '../utils/promises'
7 |
8 | hiddenFrameStyle =
9 | top: '-20px'
10 | left: '-20px'
11 | bottom: 'auto'
12 | right: 'auto'
13 | width: '1px'
14 | height: '1px'
15 | display: 'block'
16 | position: 'fixed'
17 | zIndex: undefined
18 | overflow: 'hidden'
19 |
20 | shownFrameStyle =
21 | top: '0px'
22 | left: '0px'
23 | right: '0px'
24 | bottom: '0px'
25 | width: '100%'
26 | height: '100%'
27 | display: 'block'
28 | position: 'fixed'
29 | zIndex: 10000
30 | overflow: 'auto'
31 |
32 | rpcFrameInitStyle =
33 | tabIndex: -1
34 | height: "0"
35 | width: "1px"
36 | style:
37 | position: 'fixed'
38 | width: "1px"
39 | height: "1px"
40 | top: '-20px'
41 | left: '-20px'
42 | overflow: 'hidden'
43 |
44 | class Channel
45 |
46 | constructor : (currentUser, currentConfig)->
47 | @allowRetry = true
48 | @retryCount = 0
49 | @currentConfig = currentConfig
50 | @currentUser = currentUser
51 | @timeout = null
52 | @rpc = null
53 | @_ready = {}
54 | @promise = new Promise (resolve, reject)=>
55 | @_ready.resolve = resolve
56 | @_ready.reject = reject
57 | domready(@startRpc)
58 |
59 | startRpc: =>
60 | @retryCount += 1
61 | @timeout = setTimeout(@loadingFailed, @retryCount * 10000)
62 | @rpc = new xdm.Rpc
63 | remote : @currentConfig.getRemoteUrl()
64 | container : document.body
65 | channel : [@currentConfig.get('appId'), @retryCount].join("-")
66 | props : rpcFrameInitStyle
67 | ,
68 | remote :
69 | message : {},
70 | ready : {},
71 | refreshUser : {},
72 | resetIdentify : {}
73 | local:
74 | message : @onMessage
75 | ready : @ready
76 | loadError : @loadError
77 | userUpdate : @userUpdate
78 | configUpdate : @configUpdate
79 | show : @showIframe
80 | hide : @hideIframe
81 | track : @emitTrackEvent
82 | getClientConfig : @getClientConfig
83 | @rpc
84 |
85 | loadError: (err)=>
86 | window.clearTimeout(@timeout)
87 | @allowRetry = false
88 | @_ready.reject new Error(err)
89 |
90 | loadingFailed: (err) =>
91 | @rpc && @rpc.destroy()
92 | if @allowRetry && @retryCount < 4
93 | @startRpc()
94 | else
95 | @_ready.reject(new Error('Remote loading has failed. Please check "orgUrl" and "appId" in your configuration. This may also be about connectivity.'))
96 |
97 | onMessage : (e)->
98 | if e.error then @_ready.reject(e.error) else logger.log("RPC Message", arguments)
99 |
100 | ready : (remoteConfig) =>
101 | try
102 | window.clearTimeout(@timeout)
103 | @currentConfig.initRemote(remoteConfig)
104 | @_ready.resolve @
105 | catch err
106 | @_ready.reject(err)
107 |
108 | getClientConfig : () => @currentConfig.get()
109 | showIframe : () => @applyFrameStyle(shownFrameStyle)
110 | hideIframe : () => @applyFrameStyle(hiddenFrameStyle)
111 | emitTrackEvent : (args...) => EventBus.emit('hull.track',args...)
112 | configUpdate : (config)=> @currentConfig.setRemote(config)
113 | userUpdate : (me)=> @currentUser.set(me)
114 | applyFrameStyle : (styles)=>
115 | _.map styles, (v,k)=> @rpc.iframe.style[k] = v
116 |
117 |
118 | module.exports = Channel
119 |
--------------------------------------------------------------------------------
/src/remote/services.coffee:
--------------------------------------------------------------------------------
1 | _ = require '../utils/lodash'
2 | logger = require '../utils/logger'
3 | Promise = require '../utils/promises'
4 | assign = require '../polyfills/assign'
5 |
6 | ServiceList = require './services/list'
7 |
8 | cookies = require '../utils/cookies'
9 |
10 | RemoteHeaderStore = require '../flux/stores/RemoteHeaderStore'
11 | RemoteUserStore = require '../flux/stores/RemoteUserStore'
12 | RemoteConfigStore = require '../flux/stores/RemoteConfigStore'
13 |
14 | RemoteActions = require '../flux/actions/RemoteActions'
15 | RemoteConstants = require '../flux/constants/RemoteConstants'
16 |
17 | handleSpecialRoutes = (request)->
18 | switch request.path
19 | when '/api/v1/logout'
20 | request.nocallback = true
21 | RemoteActions.logoutUser()
22 | break
23 |
24 | request
25 |
26 | maybeUpdateUser = (response)->
27 | # Pass every return call to the API to maybe update the User if the api call was a '/me' call
28 | # We do this because Me can have aliases such as the user's ID.
29 | RemoteActions.updateUserIfMe(response)
30 | response
31 |
32 | class Services
33 | constructor : (remoteConfig, gateway)->
34 | RemoteActions.updateUser(remoteConfig.data.me) if (remoteConfig.data.me)
35 | @gateway = gateway
36 | gateway.before(handleSpecialRoutes)
37 | gateway.after(maybeUpdateUser)
38 |
39 | @services = _.reduce ServiceList, (memo,Service,key)->
40 | memo[key] = new Service(remoteConfig,gateway)
41 | return memo
42 | , {}
43 |
44 | RemoteHeaderStore.addChangeListener (change)=>
45 | switch change
46 | when RemoteConstants.UPDATE_USER
47 | # Auto-Fetch the user everytime the Hull-User-Id header changes
48 | userHeaderId = RemoteHeaderStore.getHeader('Hull-User-Id')
49 | user = RemoteUserStore.getState().user
50 | @onRefreshUser() if userHeaderId != user?.id
51 | break
52 |
53 | getMethods : ()->
54 | {
55 | ready : @onReady
56 | message : @onMessage
57 | refreshUser : @onRefreshUser,
58 | resetIdentify: @onResetIdentify
59 | }
60 |
61 | onResetIdentify: (xdmCallback, xdmErrback)=>
62 | cookies.remove('_bid', path: '/', domain: document.location.host)
63 | cookies.remove('_sid', path: '/', domain: document.location.host)
64 | @gateway.resetIdentify()
65 |
66 | onReady : (req, xdmCallback, xdmErrback) ->
67 |
68 | onRefreshUser : (xdmCallback, xdmErrback)=>
69 | xdmCallback ?=->
70 | xdmErrback ?=->
71 | # Those calls skip the middleware queue to prevent request loops
72 | me = @services.hull.request({path:'me',nocallback:true})
73 | settings = @services.hull.request({path:'app/settings',nocallback:true})
74 |
75 | onSuccess = (res)=>
76 | # Refreshing the User results in us setting everything up again
77 | me = res[0].body
78 | services = res[1].body
79 | RemoteActions.updateUser(me,{silent:true})
80 | RemoteActions.updateServices(services,{silent:true})
81 | # Do not send the data back. We're just refreshing stuff.
82 | # Data will come back through even handlers on Flux Stores
83 | xdmCallback({me,services, headers: res[0].headers})
84 | undefined
85 |
86 | onError = (res={})->
87 | xdmErrback(res.response || res)
88 | error = new Error(res?.response?.message || res?.response || res)
89 | logger.error 'Failed refreshing User', error.message, error.stack
90 | throw error
91 |
92 | Promise.all([me, settings]).then onSuccess, onError
93 |
94 | undefined
95 |
96 | handleAdminCall: (request, callback, errback)=>
97 |
98 | onMessage : (request, xdmCallback, xdmErrback)=>
99 | xdmCallback ?=->
100 | xdmErrback ?=->
101 | throw new Error("Path not recognized #{JSON.stringify(request, null, 2)}") unless request.path
102 | service = @services[request.provider]
103 | if _.isFunction service.request
104 | service.request request, xdmCallback, xdmErrback
105 | return undefined
106 | else
107 | xdmErrback request
108 |
109 | undefined
110 |
111 | module.exports = Services
112 |
--------------------------------------------------------------------------------
/src/remote/services/hull.coffee:
--------------------------------------------------------------------------------
1 | Promise = require '../../utils/promises'
2 | _ = require '../../utils/lodash'
3 | EventBus = require '../../utils/eventbus'
4 | Base64 = require '../../utils/base64'
5 | getWrappedRequest = require '../wrapped-request'
6 | scriptLoader = require '../../utils/script-loader';
7 | assign = require '../../polyfills/assign';
8 | displayBanner = require '../../utils/ui/display-banner'
9 | { addEvent, removeEvent } = require '../../utils/dom-events'
10 |
11 | RECAPTCHA_ONLOAD = '___recaptcha___'
12 | RECAPTCHA_SRC = "https://www.google.com/recaptcha/api.js?onload=#{RECAPTCHA_ONLOAD}&render=explicit"
13 |
14 | _loadRecaptchaScriptPromise = null;
15 | loadRecaptchaScript = ->
16 | if (_loadRecaptchaScriptPromise?)
17 | return _loadRecaptchaScriptPromise
18 |
19 | _loadRecaptchaScriptPromise = new Promise (resolve) ->
20 | window[RECAPTCHA_ONLOAD] = resolve
21 | scriptLoader({ src: RECAPTCHA_SRC });
22 | return null
23 |
24 | return _loadRecaptchaScriptPromise;
25 |
26 | class TrackEventMatcher
27 | #TODO Make this clearer.
28 | constructor: (config)->
29 | if config == false || config?.ignore?
30 | @mode = 'ignore'
31 | @_initMatchers(config.ignore)
32 | else if !config?
33 | @mode = 'match_all'
34 | else
35 | @mode = 'match'
36 | @_initMatchers(config?.only || config)
37 |
38 | _initMatchers: (m)->
39 | m = [m] if _.isString(m)
40 | @matchers = _.map _.compact(m), (c)->
41 | _c = c.toString()
42 | if /^\/.*\/$/.test(_c)
43 | new RegExp(_c.slice(1,-1))
44 | else
45 | _c
46 |
47 | isTracked: (event)->
48 | return false unless event?
49 | return true if @mode == 'match_all'
50 | ret = _.some _.map @matchers, (m)->
51 | if _.isFunction(m.test)
52 | m.test(event)
53 | else
54 | m == event
55 | if @mode == 'ignore'
56 | return !ret
57 | else
58 | return ret
59 |
60 | # Ajax Response Middlewares
61 | trackResponse = (response={})=>
62 | if track = response.headers?['Hull-Track']
63 | try
64 | [eventName, payload] = JSON.parse(Base64.decode(track))
65 | EventBus.emit('remote.track', { event:eventName, params: { payload, url: document.referrer } })
66 | catch error
67 | # Don't throw an error here but report what happened.
68 | "Invalid Tracking header : ${JSON.stringify(errror,null,2)}"
69 | return response
70 |
71 | stopEventPropagation = (e) ->
72 | e.stopPropagation()
73 |
74 | removeBanner = (banner) ->
75 | removeEvent(banner, 'click', stopEventPropagation)
76 | banner.remove()
77 |
78 | class HullService
79 | constructor: (config, gateway)->
80 | @config = config
81 | middlewares = [@ensureIsHuman, trackResponse]
82 |
83 | @request = getWrappedRequest({ name:'hull', path: null }, gateway, middlewares)
84 | @trackMatcher = new TrackEventMatcher(config.track)
85 |
86 | ensureIsHuman: (response) =>
87 | return response if (!response.body?)
88 |
89 | { sitekey, stoken } = response.body;
90 | return response if (response.status != 429 || !sitekey? || !stoken?)
91 |
92 | banner = @_getCaptchaBanner()
93 | performRequest = @request
94 | return new Promise (resolve, reject) ->
95 | loadRecaptchaScript().then ->
96 | grecaptcha.render banner.querySelector('.hull-captcha-container'),
97 | sitekey: sitekey,
98 | stoken: stoken,
99 | callback: (captchaResponse) ->
100 | EventBus.emit('remote.iframe.hide');
101 | removeBanner(banner)
102 |
103 | requestWithCaptcha = assign {}, response.request,
104 | headers: assign {}, response.request.headers, { 'X-Captcha-Response': captchaResponse }
105 | performRequest(requestWithCaptcha).then(resolve, reject)
106 |
107 | document.body.appendChild(banner)
108 |
109 | EventBus.emit('remote.iframe.show');
110 |
111 | _getCaptchaBanner: ->
112 | removeBanner(@_banner) if @_banner
113 |
114 | @_banner = displayBanner('captcha', false)
115 | addEvent(@_banner, 'click', stopEventPropagation)
116 |
117 | @_banner
118 |
119 | module.exports = HullService
120 |
--------------------------------------------------------------------------------
/src/utils/base64.js:
--------------------------------------------------------------------------------
1 | // https://code.google.com/p/stringencoders/source/browse/trunk/javascript/base64.js?r=230
2 |
3 | const PADCHAR = '=';
4 | const ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
5 |
6 | function makeDOMException() {
7 | // sadly in FF,Safari,Chrome you can't make a DOMException
8 | var e, tmp;
9 |
10 | try {
11 | return new DOMException(DOMException.INVALID_CHARACTER_ERR);
12 | } catch (tmp) {
13 | // not available, just passback a duck-typed equiv
14 | // https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Error
15 | // https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Error/prototype
16 | var ex = new Error("DOM Exception 5");
17 |
18 | // ex.number and ex.description is IE-specific.
19 | ex.code = ex.number = 5;
20 | ex.name = ex.description = "INVALID_CHARACTER_ERR";
21 |
22 | // Safari/Chrome output format
23 | ex.toString = function() { return 'Error: ' + ex.name + ': ' + ex.message; };
24 | return ex;
25 | }
26 | }
27 |
28 | function getbyte64(s,i) {
29 | // This is oddly fast, except on Chrome/V8.
30 | // Minimal or no improvement in performance by using a
31 | // object with properties mapping chars to value (eg. 'A': 0)
32 | var idx = ALPHA.indexOf(s.charAt(i));
33 | if (idx === -1) {
34 | throw makeDOMException();
35 | }
36 | return idx;
37 | }
38 |
39 | function getbyte(s,i) {
40 | var x = s.charCodeAt(i);
41 | if (x > 255) {
42 | throw makeDOMException();
43 | }
44 | return x;
45 | }
46 |
47 | function encode(s) {
48 | if (arguments.length !== 1) {
49 | throw new SyntaxError("Not enough arguments");
50 | }
51 | var padchar = PADCHAR;
52 | var alpha = ALPHA;
53 |
54 | var i, b10;
55 | var x = [];
56 |
57 | // convert to string
58 | s = '' + s;
59 |
60 | var imax = s.length - s.length % 3;
61 |
62 | if (s.length === 0) {
63 | return s;
64 | }
65 | for (i = 0; i < imax; i += 3) {
66 | b10 = (getbyte(s,i) << 16) | (getbyte(s,i+1) << 8) | getbyte(s,i+2);
67 | x.push(alpha.charAt(b10 >> 18));
68 | x.push(alpha.charAt((b10 >> 12) & 0x3F));
69 | x.push(alpha.charAt((b10 >> 6) & 0x3f));
70 | x.push(alpha.charAt(b10 & 0x3f));
71 | }
72 | switch (s.length - imax) {
73 | case 1:
74 | b10 = getbyte(s,i) << 16;
75 | x.push(alpha.charAt(b10 >> 18) + alpha.charAt((b10 >> 12) & 0x3F) +
76 | padchar + padchar);
77 | break;
78 | case 2:
79 | b10 = (getbyte(s,i) << 16) | (getbyte(s,i+1) << 8);
80 | x.push(alpha.charAt(b10 >> 18) + alpha.charAt((b10 >> 12) & 0x3F) +
81 | alpha.charAt((b10 >> 6) & 0x3f) + padchar);
82 | break;
83 | }
84 | return x.join('');
85 | }
86 |
87 | function decode(s) {
88 | // convert to string
89 | s = '' + s;
90 | var pads, i, b10;
91 | var imax = s.length
92 | if (imax === 0) {
93 | return s;
94 | }
95 |
96 | if (imax % 4 !== 0) {
97 | throw makeDOMException();
98 | }
99 |
100 | pads = 0
101 | if (s.charAt(imax - 1) === PADCHAR) {
102 | pads = 1;
103 | if (s.charAt(imax - 2) === PADCHAR) {
104 | pads = 2;
105 | }
106 | // either way, we want to ignore this last block
107 | imax -= 4;
108 | }
109 |
110 | var x = [];
111 | for (i = 0; i < imax; i += 4) {
112 | b10 = (getbyte64(s,i) << 18) | (getbyte64(s,i+1) << 12) |
113 | (getbyte64(s,i+2) << 6) | getbyte64(s,i+3);
114 | x.push(String.fromCharCode(b10 >> 16, (b10 >> 8) & 0xff, b10 & 0xff));
115 | }
116 |
117 | switch (pads) {
118 | case 1:
119 | b10 = (getbyte64(s,i) << 18) | (getbyte64(s,i+1) << 12) | (getbyte64(s,i+2) << 6);
120 | x.push(String.fromCharCode(b10 >> 16, (b10 >> 8) & 0xff));
121 | break;
122 | case 2:
123 | b10 = (getbyte64(s,i) << 18) | (getbyte64(s,i+1) << 12);
124 | x.push(String.fromCharCode(b10 >> 16));
125 | break;
126 | }
127 | return x.join('');
128 | }
129 |
130 | function encodeURL(input) {
131 | return encodeURIComponent(encode(input).replace(/\+/g, '-').replace(/\//g, '_'));
132 | }
133 |
134 | function decodeURL(input) {
135 | return decode(decodeURIComponent(input).replace(/-/g, '+').replace(/_/g, '/'));
136 | }
137 |
138 | module.exports = { encode, decode, encodeURL, decodeURL };
139 |
--------------------------------------------------------------------------------
/src/remote/services/facebook.coffee:
--------------------------------------------------------------------------------
1 | assign = require '../../polyfills/assign'
2 | Promise = require '../../utils/promises'
3 | _ = require '../../utils/lodash'
4 | EventBus = require '../../utils/eventbus'
5 | clone = require '../../utils/clone'
6 | GenericService = require './generic-service'
7 |
8 | FB_EVENTS = ["auth.authResponseChanged", "auth.statusChange", "auth.login", "auth.logout", "comment.create", "comment.remove", "edge.create", "edge.remove", "message.send", "xfbml.render"]
9 |
10 | class FacebookService extends GenericService
11 | name : 'facebook'
12 | path : 'facebook'
13 | constructor: (config, gateway)->
14 | super(config, gateway)
15 | settings = @getSettings()
16 | @loadFbSdk(settings) if settings?.appId
17 |
18 | request : (args...)=>
19 | @ensureLoggedIn().then ()=> @performRequest(args...)
20 |
21 | ensureLoggedIn : ()=>
22 | self = this
23 | new Promise (resolve, reject)=>
24 | args = Array.prototype.slice.call(arguments)
25 | if @fbUser?.status=='connected'
26 | resolve()
27 | else
28 | FB.getLoginStatus (res)=>
29 | self.updateFBUserStatus(res)
30 | if res.status=='connected' then resolve() else reject()
31 | , true
32 | # , true ~ Maybe we dont need a roundtrip each time.
33 |
34 | performRequest: (request, callback, errback) =>
35 | path = request.path
36 | isUICall = (path=='ui' and request.params?.method?)
37 |
38 | fbErrback = (msg, res)->
39 | res.time = new Date()
40 | errback(res)
41 |
42 | fbCallback = @fbRequestCallback(request, {isUICall, path}, callback, errback)
43 |
44 | if path == 'fql.query'
45 | FB.api({ method: 'fql.query', query: request.params.query },fbCallback)
46 |
47 | else if isUICall
48 | params = clone(request.params)
49 |
50 | @showIframe()
51 | trackParams = { ui_request_id: utils?.uuid?() || (new Date()).getTime() }
52 | @track {event:"facebook.#{path}.open", params:assign({}, request.params, trackParams)}
53 |
54 | setTimeout ()=>
55 | FB.ui params, (response)=>
56 | event = "facebook.#{path}."
57 | event += if !response || response.error_code then "error" else "success"
58 | @track {event, params:assign({}, response, trackParams)}
59 | fbCallback(response)
60 | , 100
61 |
62 | else
63 | FB.api path, request.method, request.params, @fbRequestCallback(request, {isUICall:false, path}, callback, fbErrback)
64 |
65 |
66 | showIframe : ->
67 | EventBus.emit('remote.iframe.show') if @fb
68 |
69 | hideIframe : ->
70 | EventBus.emit('remote.iframe.hide') if @fb
71 |
72 | track : (args...)->
73 | EventBus.emit('remote.track', args...)
74 |
75 | fbRequestCallback : (request, opts={}, callback, errback) =>
76 | (response) =>
77 | @fbUiCallback(request,response,opts.path) if opts.isUICall
78 | if !response or response?.error
79 | errorMsg = if (response) then "[Facebook Error] #{response.error.type} : #{response.error.message}" else "[Facebook Error] Unknown error"
80 | return errback(errorMsg, { response, request })
81 | callback({ body: response, provider: 'facebook' })
82 |
83 | fbUiCallback : (req, res, path)=>
84 | @hideIframe()
85 | opts = { path: path, method: 'post', params: res }
86 | @wrappedRequest(opts)
87 |
88 | updateFBUserStatus: (res)=>
89 | @fbUser = res
90 |
91 | subscribeToFBEvents : ()=>
92 | return unless FB?.Event?
93 | self = this
94 | _.map FB_EVENTS, (event)=>
95 | FB.Event.subscribe event, (args...)=>
96 | if event.indexOf('auth.')>-1
97 | self.updateFBUserStatus(args...)
98 | EventBus.emit("fb.#{event}", args...)
99 |
100 |
101 | loadFbSdk: (config)->
102 | new Promise (resolve, reject)=>
103 | config = _.omit(assign({},config,{status:true}), 'credentials')
104 | fb = document.createElement 'script'
105 | fb.type = 'text/javascript'
106 | fb.async = true
107 | fb.src = "https://connect.facebook.net/en_US/all.js"
108 | (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(fb);
109 | window.fbAsyncInit = ()=>
110 | @fb = true
111 | FB.init config
112 | FB.getLoginStatus @updateFBUserStatus
113 | @subscribeToFBEvents()
114 | resolve({})
115 |
116 |
117 | module.exports = FacebookService
118 |
--------------------------------------------------------------------------------
/src/client/embeds/deployment.coffee:
--------------------------------------------------------------------------------
1 | assign = require '../../polyfills/assign'
2 | _ = require '../../utils/lodash'
3 | clone = require '../../utils/clone'
4 | throwErr = require '../../utils/throw'
5 | logger = require '../../utils/logger'
6 | Promise = require '../../utils/promises'
7 |
8 | # RawDeploymentStrategy = require './strategies/raw'
9 | # ScopeDeploymentStrategy = require './strategies/scope'
10 | # IframeDeploymentStrategy = require './strategies/iframe'
11 | JSDeploymentStrategy = require './strategies/js'
12 |
13 | deploymentRegistry = {}
14 | shipRegistry = {}
15 |
16 | resetDeployments = () ->
17 | deployment.destroy() for own key, deployment of deploymentRegistry
18 | shipRegistry = {}
19 | deploymentRegistry = {}
20 |
21 | getDeployment = (id)-> deploymentRegistry[id]
22 | getDeployments = (id)-> _.values(shipRegistry[id])
23 |
24 | class Deployment
25 | @resetDeployments: resetDeployments
26 | @getDeployment : getDeployment
27 | @getDeployments : getDeployments
28 |
29 | constructor: (dpl, context)->
30 | return deploymentRegistry[dpl.id] if deploymentRegistry[dpl.id]
31 | deploymentRegistry[dpl.id] = @
32 |
33 | @id = dpl.id
34 | @name = dpl.ship.name
35 |
36 | @organization = assign({}, context.org)
37 | @platform = dpl.platform
38 | @ship = dpl.ship
39 |
40 | shipRegistry[dpl.ship.id] ||= {}
41 | shipRegistry[dpl.ship.id][dpl.id] = @
42 |
43 |
44 | # onUpdate is used to attach handlers for ship.update events emitted from Hull's dashboard
45 | @onUpdate = dpl.onUpdate
46 |
47 | @settings = dpl.settings
48 | # "_selector" : ".ship", //CSS3 Selector on which to embed the ship(s)
49 | # "_multi": true, //Wether to embed on the first matching element or all
50 | # "_placement" : "before"|"after"|"append"|"top"|"replace", //Position relative to selector
51 | # "_sandbox" : true //Wether to sandbox the platform : true
52 | # "_width" : "100%", //Dimensions to give the containing element. Passed as-is as style tag
53 | # "_height" : "50px", //Dimensions to give the containing element. Passed as-is as style tag
54 |
55 | ###*
56 | * Fetches all targets specified in a deployment
57 | * @param {object} opts options object. opts.refresh = true|false // Force Refresh
58 | * @return {Nodes Array} A memoized array of Nodes matching the Query Selector (this.targets)
59 | ###
60 | getTargets : (opts={})->
61 | return @targets if @targets and !opts.refresh
62 | return [] unless @settings._selector
63 |
64 | targets = if @settings._multi
65 | document.querySelectorAll(@settings._selector)
66 | else
67 | target = document.querySelector(@settings._selector)
68 | if target then [target] else []
69 |
70 | logger.info("No deployment targets for selector #{@settings._selector}", @) unless targets.length
71 | targets
72 |
73 | getPublicData: ()=>
74 | assign(clone({
75 | ship : @ship
76 | organization : @organization
77 | platform : @platform
78 | settings : @settings
79 | }), {
80 | onUpdate: (@onUpdate || ->)
81 | })
82 |
83 | getDeploymentStrategy : ()=>
84 | return @deploymentStrategy if @deploymentStrategy?
85 |
86 | # DS = if @ship.index.match(/\.js$/)
87 | # JSDeploymentStrategy
88 | # else if @settings._sandbox == 'raw'
89 | # RawDeploymentStrategy
90 | # else if !!@settings._sandbox
91 | # IframeDeploymentStrategy
92 | # else
93 | # ScopeDeploymentStrategy
94 |
95 | # @deploymentStrategy = new DS(@)
96 | @deploymentStrategy = new JSDeploymentStrategy(@)
97 | @deploymentStrategy
98 |
99 | embed : (opts={})=>
100 | # If we're refreshing, rebuild the target list
101 | @targets = @getTargets(opts)
102 | ds = @getDeploymentStrategy()
103 | if @targets.length
104 | ds.embed(@targets, opts).then ()=>
105 | @onEmbed()
106 | ,throwErr
107 | .catch throwErr
108 | else
109 | Promise.resolve()
110 |
111 | boot: ()=>
112 | @getDeploymentStrategy().boot() if @targets?.length
113 |
114 | onEmbed : (callback)=>
115 | @getDeploymentStrategy().onEmbed(callback) if @targets?.length
116 |
117 | destroy: ()=>
118 | if @targets.length
119 | @getDeploymentStrategy().destroy()
120 | @deploymentStrategy = null
121 |
122 | module.exports = Deployment
123 |
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var fs = require('fs');
4 | var _ = require('lodash');
5 | var webpack = require('webpack');
6 | var path = require('path');
7 | var pkg = require('./package.json');
8 | var moment = require('moment');
9 | var git = require('git-rev-sync');
10 |
11 | // DO NOT CHANGE FOLDERS
12 | // WIHTOUT UPDATING PACKAGE.JSON TOO.
13 | var sourceFolder = 'src';
14 | var outputFolder = 'lib';
15 | var assetsFolder = '';
16 | var serverPort = process.env.PORT||3001;
17 | var previewUrl = 'http://localhost:'+serverPort;
18 |
19 | var hotReload = false;
20 |
21 | // DO NOT CHANGE SHIP ENTRY
22 | // WITHOUT UPDATING PACKAGE.JSON TOO
23 | // THESE ARE THE JS FILES USED AS ENTRY POINTS TO COMPILE YOUR APP
24 | var entry = {
25 | hull: './'+sourceFolder+'/hull.coffee'
26 | }
27 |
28 | // ADDITIONAL FILES TO BE COPIED BY GULP
29 | function gulpDest(out){
30 | return path.join(outputFolder,assetsFolder,out);
31 | }
32 |
33 | var files = {
34 | 'app/**/*' : outputFolder,
35 | 'src/vendors/**/*' : gulpDest('vendors/'),
36 | 'src/images/**/*' : gulpDest('images/'),
37 | 'src/*.html' : outputFolder
38 | }
39 |
40 | var libName = pkg.name;
41 | var displayName = 'Hull.js';
42 |
43 | var getAWSConfig = function(){
44 | return {
45 | gzip: { ext:".gz" },
46 | config : {
47 | "params":{
48 | "Bucket": process.env.AWS_BUCKET,
49 | },
50 | "region": process.env.AWS_REGION,
51 | "accessKeyId":process.env.AWS_KEY,
52 | "secretAccessKey":process.env.AWS_SECRET,
53 | },
54 | cloudfront:{
55 | credentials:{
56 | "accessKeyId":process.env.AWS_KEY,
57 | "secretAccessKey":process.env.AWS_SECRET,
58 | },
59 | distributionId:process.env.CLOUDFRONT_DISTRIBUTION_ID,
60 | region:"us-east-1"
61 | },
62 | publish:{
63 | options:{
64 | simulate: false,
65 | },
66 | headers: {
67 | "Cache-Control": "max-age=3600, no-transform, public"
68 | }
69 | }
70 | };
71 | };
72 |
73 | // ------------------------------------------------
74 | // ------------------------------------------------
75 | // NO NEED TO TOUCH ANYTHING BELOW THIS
76 | // ------------------------------------------------
77 | // ------------------------------------------------
78 |
79 | var outputPath = path.join(__dirname, outputFolder);
80 |
81 | var output = {
82 | path: path.join(outputPath,assetsFolder),
83 | pathinfo: true,
84 | filename: '[name].js',
85 | chunkFileName: '[name].chunk.js',
86 | libraryTarget: 'umd',
87 | library: displayName,
88 | publicPath: '/'+assetsFolder+'/'
89 | }
90 |
91 | var extensions = ['', '.js', '.css', '.scss', '.coffee'];
92 | var modulesDirectories = ['node_modules', 'bower_components', 'src/vendor'];
93 |
94 | var sassIncludePaths = modulesDirectories.map(function(include){
95 | return ('includePaths[]='+path.resolve(__dirname, include))
96 | }).join('&');
97 |
98 |
99 | // https://github.com/webpack/react-starter/blob/master/make-webpack-config.js
100 | // 'imports?define=>false': Yeah, we're going big and disabling AMD completely. F**k it.
101 | // This is because webpack strips the `this` context when requiring those, while they expect it.
102 | // Basically, this fixes all of our problems with badly constructed AMD modules.
103 | // Among which: vex, datepicker
104 | var loaders = [
105 | {test: /\.json$/, loader: 'json' },
106 | {test: /\.coffee$/, loader: 'coffee'},
107 | {test: /\.js$/, loader: 'babel', exclude: /node_modules|bower_components/},
108 | {test: /\.jpe?g$|\.gif$|\.png$/, loader: 'file' },
109 | {test: /\.svg$|\.woff$|\.ttf$|\.wav$|\.mp3$/, loader: 'file' },
110 | ];
111 |
112 | var REVISION = process.env.CIRCLE_SHA1 || git.short();
113 |
114 | // We remove the 'dist' from the filenames for demo and index.html in package.json
115 | // Package.json expects our files to be addressable from the same repo
116 | // We put them in `dist` to have a clean structure but then we need to build them in the right place
117 | var plugins = [
118 | new webpack.DefinePlugin({
119 | 'VERSION' : JSON.stringify(pkg.version),
120 | 'REVISION' : JSON.stringify(REVISION),
121 | 'BUILD_DATE' : JSON.stringify(moment().format('MMMM, DD, YYYY, HH:mm:ss')),
122 | 'PUBLIC_PATH': JSON.stringify(output.publicPath)
123 | }),
124 | new webpack.ResolverPlugin(
125 | new webpack.ResolverPlugin.DirectoryDescriptionFilePlugin("bower.json", ["main"])
126 | ),
127 | new webpack.optimize.OccurenceOrderPlugin()
128 | ]
129 |
130 | var externals = {}
131 |
132 | module.exports = {
133 | hotReload : hotReload,
134 | aws : getAWSConfig(),
135 | libName : libName,
136 | displayName : displayName,
137 | version : pkg.version,
138 |
139 | files : files,
140 |
141 | outputFolder : outputFolder,
142 | assetsFolder : assetsFolder,
143 | serverPort : serverPort,
144 | previewUrl : previewUrl,
145 |
146 | entry : entry,
147 | output : output,
148 | extensions : extensions,
149 | modulesDirectories : modulesDirectories,
150 | plugins : plugins,
151 | loaders : loaders,
152 | externals : externals,
153 |
154 | pkg : pkg
155 | }
156 |
--------------------------------------------------------------------------------
/src/remote/gateway.coffee:
--------------------------------------------------------------------------------
1 | assign = require '../polyfills/assign'
2 | Promise = require '../utils/promises'
3 | _ = require '../utils/lodash'
4 | cookies = require '../utils/cookies'
5 | Base64 = require '../utils/base64'
6 | logger = require '../utils/logger'
7 | EventBus = require '../utils/eventbus'
8 | clone = require '../utils/clone'
9 | RemoteHeaderStore = require '../flux/stores/RemoteHeaderStore'
10 | QSEncoder = require '../utils/query-string-encoder'
11 |
12 | # require 'whatwg-fetch' #Polyfill for Global -> Not ready from primetime
13 | superagent = require 'superagent'
14 | API_PATH = '/api/v1/'
15 | API_PATH_REGEXP = /^\/?api\/v1\//
16 | RESPONSE_HEADERS = ['Hull-Auth-Scope', 'Hull-Track', 'Hull-User-Id', 'Hull-User-Sig', 'X-Hits-Count', 'Link']
17 |
18 | TRACKING_PATHS = ["/api/v1/t", "/api/v1/me/traits", "/api/v1/me/alias"];
19 |
20 | normalizePath = (path) ->
21 | return path.replace(API_PATH_REGEXP, API_PATH) if API_PATH_REGEXP.test(path)
22 | path = path.substring(1) if path[0] == '/'
23 | API_PATH + path
24 |
25 | batchable = (threshold, callback) ->
26 | timeout = null
27 | args = []
28 |
29 | ->
30 | args.push Array::slice.call(arguments)
31 | clearTimeout timeout
32 | delayed = =>
33 | callback.call(@, args)
34 | timeout = null
35 | args = []
36 | timeout = setTimeout delayed, threshold
37 |
38 | reduceHeaders = (headers)->
39 | _headers = _.reduce headers, (memo, value, key) ->
40 | memo[key.toLowerCase()] = value
41 | memo
42 | , {}
43 | _.reduce RESPONSE_HEADERS, (memo, name) ->
44 | value = _headers[name.toLowerCase()]
45 | memo[name] = value if value?
46 | memo
47 | , {}
48 |
49 | formatBatchParams = (requests) ->
50 | params = { sequential: true }
51 | params.ops = for request in requests
52 | r = request[0]
53 | c = {method: r.method, url: r.path}
54 | c.params = r.params if r.params?
55 | c.headers = r.headers if r.headers?
56 | c
57 |
58 | params
59 |
60 | resolveResponse = (request, response={}, resolve, reject)->
61 | headers = reduceHeaders(response.headers)
62 |
63 | h = {
64 | body: response.body,
65 | status: response.status,
66 | headers: headers,
67 | request: request,
68 | }
69 |
70 | return resolve(h)
71 |
72 | class Gateway
73 |
74 | constructor: (config={}) ->
75 | { batching, appId, identify, location } = config
76 | @apiEndpoint = config.apiEndpoint
77 | @trackingEndpoint = config.trackingEndpoint
78 | @identify = identify
79 | @location = location || {}
80 | @options = _.defaults({}, batching, { min:1, max:1, delay:2 })
81 | @queue = batchable @options.delay, (requests) -> @flush(requests)
82 |
83 | identifyBrowserAndSession: ->
84 | ident = {}
85 | ident['Hull-Bid'] = @identify.browser
86 | ident['Hull-Sid'] = @identify.session
87 | if @location
88 | ident['X-Track-Url'] = @location.url
89 | ident['X-Track-Referer'] = @location.referer
90 | ident
91 |
92 | resetIdentify: ->
93 | @identify = {}
94 |
95 | fetch : (options={}) =>
96 | {method, headers, path, params} = options
97 |
98 | method = (method||'get').toUpperCase()
99 |
100 | headers = assign(@identifyBrowserAndSession(), RemoteHeaderStore.getState(), headers)
101 | if @trackingEndpoint && TRACKING_PATHS.includes(path)
102 | endpoint = [@trackingEndpoint, path].join('')
103 | else if @apiEndpoint
104 | endpoint = [@apiEndpoint, path].join('')
105 | else
106 | endpoint = path
107 |
108 | #TODO Check SuperAgent under IE8 and below
109 | s = superagent(method, endpoint).set(headers)
110 |
111 | if params? and method=='GET' then s.query(QSEncoder.encode(params)) else s.send(params)
112 |
113 | new Promise (resolve, reject)->
114 | logger.verbose(">", method, path, params, headers)
115 |
116 | s.end (err, response={})=>
117 | logger.verbose("<", method, path, response)
118 | h = {body:response.body, headers: response.headers, status: response.status}
119 | # if (response.ok) then resolve(h) else reject(h)
120 | resolve(h)
121 |
122 | flush: (requests) ->
123 | if requests.length <= @options.min
124 | while requests.length
125 | [request, resolve, reject] = requests.pop()
126 | @handleOne(request, resolve, reject)
127 | else
128 | @handleMany(requests.splice(0, @options.max))
129 | @flush(requests) if requests.length
130 |
131 | after_middlewares : []
132 | before_middlewares : []
133 |
134 | # Connect-like middleware syntax
135 | after : (cb)=>
136 | return unless (cb? and _.isFunction(cb))
137 | @after_middlewares.push(cb)
138 | cb
139 |
140 | before : (cb)=>
141 | return unless (cb? and _.isFunction(cb))
142 | @before_middlewares.push(cb)
143 | cb
144 |
145 | handle: (request) ->
146 | request.path = normalizePath request.path
147 |
148 | promise = new Promise (resolve, reject)=>
149 | _.each @before_middlewares, (middleware)-> middleware(request)
150 | @queue(request, resolve, reject)
151 |
152 |
153 | unless request.nocallback
154 | _.each @after_middlewares, (middleware)->
155 | promise = promise.then middleware
156 |
157 | promise.catch (err)->
158 | throw err
159 |
160 | # Single request posting
161 | handleOne: (request, resolve, reject) ->
162 | success = (response)->
163 | resolveResponse(request, response, resolve, reject)
164 |
165 | error = (error)->
166 | response = error.body
167 | reject({response, request, headers: {}})
168 |
169 | @fetch(request).then(success, error)
170 |
171 | # Batching API posting
172 | handleMany: (requests) ->
173 | params = formatBatchParams(requests)
174 |
175 | success = (responses)->
176 | resolveResponse(requests[i], response, requests[i][1], requests[i][2]) for response, i in responses.body.results
177 | undefined #Don't forget this to keep the promise chain alive
178 |
179 | error = (response)->
180 | request[1].reject({response:response, headers: {}, request: request}) for request in requests
181 |
182 | @fetch({method: 'post', path: '/api/v1/batch', params }).then(success, error)
183 |
184 | module.exports = Gateway
185 |
--------------------------------------------------------------------------------
/src/client/current-config.coffee:
--------------------------------------------------------------------------------
1 | localstorage = require('putainde-localstorage')
2 | _ = require '../utils/lodash'
3 | EventBus = require '../utils/eventbus'
4 | clone = require '../utils/clone'
5 | throwErr = require '../utils/throw'
6 | Base64 = require '../utils/base64'
7 | assign = require '../polyfills/assign'
8 | Promise = require '../utils/promises'
9 | getKey = require '../utils/get-key'
10 |
11 | getReferralContext = ->
12 | {
13 | initial_url: document.location.href,
14 | initial_referrer: document.referrer || '$direct',
15 | initial_referring_domain: extractDomainFromUrl(document.referrer) || '$direct',
16 | initial_utm_tags: extractUtmTags(),
17 | first_seen_at: new Date().getTime()
18 | }
19 |
20 | getRemoteUrl = (config, identifiers)->
21 | url = "#{config.orgUrl}/api/v1/#{config.appId}/remote.html?v=#{VERSION}"
22 | url += "&url=#{encodeURIComponent(document.location.href)}"
23 | url += "&r=#{encodeURIComponent(document.referrer)}"
24 | url += "&js=#{config.jsUrl}" if config.jsUrl
25 | url += "&uid=#{config.uid}" if config.uid
26 | url += "&debug_remote=true" if config.debugRemote
27 | url += "&access_token=#{config.accessToken}" if config.accessToken?
28 | url += "&user_hash=#{config.userHash}" if config.userHash != undefined
29 | url += "&_bid=#{identifiers.bid}" if identifiers?.bid
30 | url += "&_sid=#{identifiers.sid}" if identifiers?.sid
31 | url += "&ravenDsn=#{config.ravenDsn}" if config.ravenDsn
32 | url
33 |
34 | # Parse the tracked events configuration and standardize it.
35 | formatTrackConfig = (config={})->
36 | switch (Object.prototype.toString.call(config).match(/^\[object (.*)\]$/)[1])
37 | when "Object"
38 | if config.only?
39 | config = { only: (m.toString() for m in config.only) }
40 | else if config.ignore?
41 | config = { ignore: (m.toString() for m in config.ignore) }
42 | else
43 | config
44 | when "RegExp"
45 | config = { only: config.toString() }
46 | when "Array"
47 | config = { only: (m.toString() for m in config) }
48 | # Setup initial referrer
49 | config.referrer = document.referrer if document?.referrer
50 | config
51 |
52 | checkConfig = (config)->
53 | config = clone(config)
54 | config.track = formatTrackConfig(config.track)
55 | promise = new Promise (resolve, reject)=>
56 | msg = "You need to pass some keys to Hull to start it: "
57 | readMore = "Read more about this here : http://www.hull.io/docs/references/hull_js/#hull-init-params-cb-errb"
58 | # Fail right now if we don't have the required setup
59 | if config.orgUrl and config.appId
60 | # Auto add protocol if we dont have one of http://, https://, //
61 | reject(new Error(" You specified orgUrl as #{config.orgUrl}. We do not support protocol-relative URLs in organization URLs yet.")) if config.orgUrl.match(/^\/\//)
62 | config.orgUrl ="https://#{config.orgUrl}" unless config.orgUrl.match(/^http[s]?:\/\//)
63 | resolve(config)
64 | else
65 | reject(new Error "#{msg} We couldn't find `orgUrl` in the config object you passed to `Hull.init`\n #{readMore}") unless config.orgUrl
66 | reject(new Error "#{msg} We couldn't find `platformId` in the config object you passed to `Hull.init`\n #{readMore}") unless config.appId
67 | promise.then(null, throwErr)
68 | promise
69 |
70 | extractDomainFromUrl = (url)->
71 | return unless url && url.length
72 | u = url.match(/^(https?:\/\/)?([\da-z\.-]+)([\/\w \.-]*)*\/?/)
73 | u[2] if u
74 |
75 | extractUtmTags = ->
76 | _.reduce document.location.search.slice(1).split('&'), (tags, t)->
77 | [k,v] = t.split('=', 2)
78 | tags[k.replace(/^utm_/, '')] = v if /^utm_/.test(k)
79 | tags
80 | , {}
81 |
82 | class CurrentConfig
83 |
84 | constructor: ()->
85 |
86 | init: (config)->
87 | # # Init is silent
88 | # @_clientConfig = config
89 | @_remoteConfig = {}
90 | @_clientConfig = {}
91 |
92 |
93 | checkConfig(config).then (config)=>
94 | org = extractDomainFromUrl(config.orgUrl)
95 | ns = ['hull'].concat(org.split('.')).join('_')
96 | @storage = localstorage.create({ namespace: ns })
97 | @_clientConfig = config
98 | @
99 | , throwErr
100 |
101 | initRemote: (cfg={})->
102 | @identifyBrowser(cfg.identify.browser, count: true)
103 | @identifySession(cfg.identify.session, count: true)
104 | @_remoteConfig = cfg
105 |
106 | set: (config, key)->
107 | if key? then @_clientConfig[key] = config else @_clientConfig = config
108 |
109 | setSettings: ()->
110 |
111 | setRemote: (hash, key)->
112 | if(key)
113 | previousConfig = @_remoteConfig[key]
114 | @_remoteConfig[key] = assign({},@_remoteConfig[key],hash)
115 | else
116 | previousConfig = @_remoteConfig
117 | @_remoteConfig = assign({}, @_remoteConfig, hash)
118 | @onUpdate() unless _.isEqual(previousConfig, hash)
119 |
120 |
121 | get: (key)=>
122 | hash = clone(@_clientConfig);
123 | hash.services = clone(@_remoteConfig.services);
124 | getKey(hash, key)
125 |
126 | getRemote: (key)->
127 | getKey(@_remoteConfig, key)
128 |
129 | getRemoteUrl: ()=>
130 | browser = @identifyBrowser(@_clientConfig.anonymousId || @_clientConfig.browserId)
131 | session = @identifySession(@_clientConfig.sessionId)
132 | getRemoteUrl(@_clientConfig, { bid: browser.id, sid: session.id })
133 |
134 | identifySession: (id, options={})=>
135 | session = @identify('session', id, assign({}, options, expires: 60 * 1000 * 30))
136 | @_clientConfig.sessionId = session.id
137 | session
138 |
139 | identifyBrowser: (id, options={})=>
140 | browser = @identify('browser', id, options)
141 | @_clientConfig.browserId = browser.id
142 | @_clientConfig.anonymousId = browser.id
143 | browser
144 |
145 | storageSet: (key, value)=>
146 | try
147 | @storage.set(key, value)
148 | catch e
149 | null
150 |
151 | storageGet: (key)=>
152 | try
153 | @storage.get(key)
154 | catch e
155 | null
156 |
157 | resetIdentify: ->
158 | try @storage.clear()
159 |
160 | identify: (key, id, options={})=>
161 | ident = @storageGet(key)
162 | now = new Date().getTime()
163 | if options.expires
164 | # Auto expire after 30 minutes
165 | if !ident || ident.expires_at < now
166 | ident = getReferralContext()
167 | ident.expires_at = now + options.expires
168 | else
169 | ident ?= getReferralContext()
170 | ident.id = id if id?
171 |
172 | if options.count
173 | ident.inits_count = (ident.inits_count || 0) + 1
174 |
175 | @storageSet(key, ident)
176 |
177 | ident
178 |
179 | onUpdate : () =>
180 | EventBus.emit('hull.config.update', @get())
181 |
182 |
183 | module.exports = CurrentConfig
184 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /*global require*/
3 | var _ = require('lodash');
4 | var del = require('del');
5 | var path = require('path');
6 | var runSequence = require('run-sequence');
7 |
8 | var gulp = require('gulp');
9 | var harmonize = require('harmonize');
10 | var awspublish = require('gulp-awspublish');
11 | var rename = require('gulp-rename');
12 | var parallelize = require('concurrent-transform');
13 | var gutil = require('gulp-util');
14 | var deploy = require('gulp-gh-pages');
15 | var cloudfront = require('gulp-invalidate-cloudfront');
16 | var notifier = require('node-notifier');
17 | var merge = require('merge-stream');
18 |
19 | var ngrok = require('ngrok');
20 | var webpack = require('webpack');
21 | var WebpackDevServer = require('webpack-dev-server');
22 |
23 |
24 | // Get our Config.
25 | var config = require('./config');
26 | var webpackConfig = require('./webpack.config');
27 |
28 | harmonize();
29 |
30 | var notify = function(message){
31 | notifier.notify({title: config.displayName+" Gulp",message:message});
32 | };
33 |
34 |
35 | // Setup a Ngrok server
36 | var ngrokServe = function(subdomain){
37 | var options = { port: config.serverPort };
38 | var env = process.env;
39 | if (env.NGROK_AUTHTOKEN) {
40 | options.authtoken = env.NGROK_AUTHTOKEN;
41 |
42 | if(env.NGROK_SUBDOMAIN || subdomain){
43 | options.subdomain = (env.NGROK_SUBDOMAIN || subdomain).replace(/-/g,'');
44 | }
45 | }
46 | ngrok.connect(options, function (error, url) {
47 | if (error) {
48 | throw new gutil.PluginError('ship:server', error);
49 | }
50 |
51 | url = url.replace('https', 'http');
52 | notify({message:"Ngrok Started on "+url});
53 |
54 | gutil.log('[ship:server]', url);
55 | });
56 | }
57 |
58 |
59 | gulp.task('default', ['server']);
60 | gulp.task('serve', ['server']);
61 | gulp.task('clean', function(callback) { del(['./'+config.outputFolder+'/**/*']).then(function(){callback()}); });
62 | gulp.task('server', function(callback) { runSequence('clean', 'copy-files:watch', 'webpack:server', callback); });
63 | gulp.task('build', function(callback) { runSequence('clean', 'copy-files', 'webpack:build', callback); });
64 | gulp.task('deploy', function(callback) { runSequence('build', 'publish:sha', callback); });
65 | gulp.task('deploy:release', function(callback) { runSequence('build', 'publish:release', callback); });
66 |
67 |
68 | var notify = function(message){
69 | notifier.notify({title: config.displayName+" Gulp", message: message});
70 | };
71 | var handleError = function(err, taskName){
72 | if(err){
73 | notify(taskName+" Error: "+ err);
74 | throw new gutil.PluginError("webpack:build", err);
75 | }
76 | };
77 | // Copy static files from the source to the destination
78 | var copyFiles = function(callback){
79 | _.map(config.files, function(dest, src){
80 | gulp.src(src).pipe(gulp.dest(dest));
81 | });
82 | notify("Vendors Updated");
83 | if (_.isFunction(callback)){
84 | callback();
85 | }
86 | };
87 |
88 | gulp.task("copy-files", copyFiles);
89 |
90 | gulp.task("copy-files:watch", function(){
91 | copyFiles();
92 | gulp.watch(_.keys(config.files), copyFiles);
93 | });
94 |
95 | //Production Build.
96 | //Minified, clean code. No demo keys inside.
97 | //demo.html WILL NOT WORK with this build.
98 | //
99 | //Webpack handles CSS/SCSS, JS, and HTML files.
100 | var afterBuild = function(err, stats){
101 | if (!!err) {throw new gutil.PluginError("webpack:build", err); }
102 |
103 | var jsonStats = stats.toJson();
104 |
105 | if (jsonStats.errors.length > 0) {
106 | return new gutil.PluginError("webpack:build", JSON.stringify(jsonStats.errors));
107 | }
108 |
109 | if (jsonStats.warnings.length > 0) {
110 | new gutil.PluginError("webpack:build", JSON.stringify(jsonStats.warnings));
111 | }
112 |
113 |
114 | gutil.log("[webpack:build]", stats.toString({colors: true}));
115 | notify("App Built");
116 | };
117 | gulp.task("webpack:build", function(callback) {
118 | // Then, use Webpack to bundle all JS and html files to the destination folder
119 | notify("Building App");
120 | webpack(_.values(webpackConfig.production), function(err, stats) {
121 | afterBuild(err, stats);
122 | webpack(_.values(webpackConfig.debug), function(err, stats){
123 | afterBuild(err, stats);
124 | callback();
125 | });
126 | });
127 | });
128 |
129 | // Dev Build
130 | // Create the webpack compiler here for caching and performance.
131 | var devCompiler = webpack(webpackConfig.development.browser);
132 |
133 | // Build a Dev version of the project. Launched once on startup so we can have eveything copied.
134 | gulp.task("webpack:build:dev", function(callback) {
135 | // run webpack with Dev profile.
136 | // Embeds the Hull config keys, and the necessary stuff to make demo.html work
137 | devCompiler.run(function(err, stats) {
138 | if (err){
139 | throw new gutil.PluginError("webpack:build:dev", err);
140 | }
141 |
142 | var jsonStats = stats.toJson();
143 |
144 | if(jsonStats.errors.length > 0){
145 | return new gutil.PluginError("webpack:build:dev", JSON.stringify(jsonStats.errors));
146 | }
147 |
148 | if(jsonStats.warnings.length > 0){
149 | new gutil.PluginError("webpack:build:dev", JSON.stringify(jsonStats.warnings));
150 | }
151 |
152 | gutil.log("[webpack:build:dev]", stats.toString({colors: true}));
153 | notify({message: "Webpack Updated"});
154 | callback();
155 | });
156 | });
157 |
158 | // Launch webpack dev server.
159 | gulp.task("webpack:server", function() {
160 | var taskName = "webpack:server";
161 | new WebpackDevServer(devCompiler, {
162 | disableHostCheck: true,
163 | contentBase: config.outputFolder,
164 | publicPath: config.assetsFolder+"/",
165 | hot: config.hotReload,
166 | stats: {colors: true }
167 | }).listen(config.serverPort, function(err) {
168 | handleError(err, taskName);
169 | // Dump the preview URL in the console, and open Chrome when launched for convenience.
170 | notify({message: "Dev Server Started"});
171 | var url = webpackConfig.development.browser.output.publicPath+"webpack-dev-server/";
172 | ngrokServe(config.libName)
173 | gutil.log("["+taskName+"]", url);
174 | });
175 | });
176 |
177 | var publish = function(versions){
178 | var aws = config.aws
179 | var publisher = awspublish.create(aws.config);
180 | var files = path.join(config.outputFolder, config.assetsFolder, "*")
181 | var streams = [];
182 | var cloudfrontInvalidations = []
183 | for (var i = 0; i < versions.length; i++) {
184 | var version = versions[i];
185 | if(version){
186 | console.log('Deploying to ',version);
187 | var plain = gulp.src(files)
188 | .pipe(rename(function(p){
189 | p.dirname += '/'+version;
190 | console.log('Publishing '+path.join(p.dirname,p.basename+p.extname))
191 | }))
192 | var gzip = gulp.src(files)
193 | .pipe(rename(function(p){
194 | p.dirname += '/'+version;
195 | console.log('Publishing '+path.join(p.dirname,p.basename+p.extname))
196 | }))
197 | .pipe(awspublish.gzip(aws.gzip))
198 | cloudfrontInvalidations.push('/'+version+'/*');
199 | streams.push(plain);
200 | streams.push(gzip);
201 | }
202 | };
203 |
204 | var invalidationBatch = {
205 | CallerReference: new Date().toString(),
206 | Paths:{
207 | Quantity:cloudfrontInvalidations.length,
208 | Items:cloudfrontInvalidations
209 | }
210 | }
211 | return merge.apply(merge,streams)
212 | .pipe(parallelize(publisher.publish(aws.publish.headers,aws.publish.options)))
213 | .pipe(publisher.cache())
214 | .pipe(cloudfront(invalidationBatch, aws.cloudfront))
215 | .pipe(awspublish.reporter())
216 | }
217 |
218 | // Deploys to S3
219 | gulp.task('publish:sha',function(){
220 | var SHA1 = process.env.CIRCLE_SHA1;
221 | if( !SHA1 ){ return; }
222 | return publish([SHA1]);
223 | });
224 |
225 | gulp.task('publish:release',function(){
226 | var SHA1 = process.env.CIRCLE_SHA1;
227 | var RELEASE = config.pkg.version;
228 | if( !SHA1 || !RELEASE ){ return; }
229 | return publish([SHA1,RELEASE]);
230 | });
231 |
--------------------------------------------------------------------------------
/src/hull.coffee:
--------------------------------------------------------------------------------
1 | # # This file is responsible for defining window.Hull
2 | # # and providing pooled methods to the user while
3 | # # Hull is actually loading.
4 |
5 | require './utils/load-polyfills'
6 | assign = require './polyfills/assign'
7 | Promise = require './utils/promises'
8 | _ = require './utils/lodash'
9 | logger = require './utils/logger'
10 | Raven = require './utils/raven'
11 | Client = require './client'
12 | CurrentUser = require './client/current-user'
13 | CurrentConfig = require './client/current-config'
14 | Channel = require './client/channel'
15 | Api = require './client/api'
16 |
17 | EventBus = require './utils/eventbus'
18 | Pool = require './utils/pool'
19 | HullRemote = require './hull-remote'
20 | embeds = require './client/embeds'
21 | scriptTagConfig = require './client/script-tag-config'
22 | initializePlatform = require './client/initialize-platform'
23 | decodeHash = require './utils/decode-hash'
24 | displayBanner = require './utils/ui/display-banner'
25 |
26 | ###*
27 | * Wraps the success callback
28 | *
29 | * Extends the global object
30 | * Reinjects events in the live app from the pool
31 | * Replays the track events
32 | * @param {function} userSuccessCallback the function that will be called if everything went well
33 | * @param {object} _hull an partial build of the Hull object
34 | * @param {object} data config data coming from the Remote
35 | * @return {object} the Hull object
36 | ###
37 | onInitSuccess = (userSuccessCallback, hull, data)->
38 | userSuccessCallback = userSuccessCallback || ->
39 | {me, app, org} = data
40 |
41 |
42 | if app.track_page_inits
43 | hull.track('hull.app.init')
44 |
45 | embeds.initialize({ org });
46 | hull.embed = embeds.embed
47 | hull.onEmbed = embeds.onEmbed
48 |
49 | # We're on the client.
50 | delete hull.initRemote
51 |
52 | # Prune init queue
53 | Pool.run('identify', hull)
54 | Pool.run('alias', hull)
55 | Pool.run('track', hull)
56 | Pool.run('traits', hull)
57 | Pool.run('trackForm', hull)
58 |
59 | # Execute Hull.init callback
60 | ready.resolve {hull, me, app, org}
61 |
62 | EventBus.emit('hull.ready', hull, me, app, org)
63 | EventBus.emit('hull.init', hull, me, app, org)
64 | logger.log("Hull.js version \"#{hull.version}\" started")
65 |
66 | # Do Hull.embed(platform.deployments) automatically
67 | if hull.config().embed != false
68 | if _.isArray(app?.deployments) and app.deployments.length>0
69 | embeds.embed(app.deployments,{},onEmbedComplete, onEmbedError)
70 | else
71 | onEmbedComplete()
72 |
73 | hash = decodeHash()
74 | snippet = hash?.hull?.snippet
75 | if snippet
76 | config = hull.config()
77 | origin = snippet.origin
78 | platformOk = snippet.platformId == config.appId
79 |
80 | snippetOrgUrl = snippet.orgUrl.replace(/^http:/,'https:')
81 | orgUrl = config.orgUrl.replace(/^http:/,'https:')
82 | orgOk = snippetOrgUrl == orgUrl
83 |
84 | check = snippet.check
85 |
86 | window.location.hash=""
87 | if(orgOk && platformOk)
88 | opener.postMessage({ result: check }, origin);
89 | EventBus.once 'hull.snippet.success', ()->
90 | opener.postMessage({ result: check }, origin);
91 | window.close()
92 | displayBanner('platform')
93 | else
94 | response = { code:'invalid', orgUrl: orgUrl, platformId: config.appId }
95 | opener.postMessage({ error: btoa(JSON.stringify(response)) }, origin);
96 | # Everything went well, call the init callback
97 | userSuccessCallback(hull, me, app, org)
98 |
99 | hull
100 |
101 | # Wraps init failure
102 | onInitFailure = (err)-> throw err
103 |
104 | onEmbedComplete = ()->
105 | EventBus.emit('hull.ships.ready');
106 | logger.log("Hull Embeds Completed successfully")
107 |
108 | onEmbedError = (err...)->
109 | logger.error("Failed embedding Ships", err...)
110 |
111 | parseHash = ()->
112 | return if window.location.href.match('//.+\.hullapp\.io/.+/remote.html') # prevent this when in remote.html
113 |
114 | hash = decodeHash()
115 | if hash? && hash.hasOwnProperty('success')
116 | window.location.hash = ''
117 |
118 | if window?.opener?.Hull? and window?.opener?.__hull_login_status__
119 | window.opener.__hull_login_status__(hash)
120 | window.close()
121 |
122 | parseHash()
123 |
124 | captureException = (err, ctx)->
125 | Raven.captureException(err, ctx)
126 |
127 | ###*
128 | * Main Hull Entry Point
129 | *
130 | * Will only be executed once.
131 | * @param {[type]} config={} [description]
132 | * @param {[type]} userSuccessCallback [description]
133 | * @param {[type]} userFailureCallback [description]
134 | * @return {[type]} [description]
135 | ###
136 | init = (config={}, userSuccessCallback, userFailureCallback)->
137 |
138 | if !!hull._initialized
139 | throw new Error('Hull.init can be called only once')
140 | return
141 |
142 | config.version = VERSION
143 | # Process this as a single object we send to Remote side (cleaner)
144 | config.debug = if config.debug then { enabled: true, verbose: config.verbose } else { }
145 | logger.init(config.debug)
146 | config.appId = config.appId || config.platformId || config.shipId
147 | delete config.platformId if config.platformId?
148 | delete config.shipId if config.shipId?
149 |
150 | missing = []
151 | missing.push "orgUrl" unless config.orgUrl?
152 | missing.push "platformId" unless config.appId?
153 | httpsRegex = /^https:|^\/\//
154 | throw new Error("[Hull.init] jsUrl NEEDS be loaded via https if orgUrl is https") if config.jsUrl and not httpsRegex.test(config.jsUrl) and httpsRegex.test(config.jsUrl)
155 | throw new Error("[Hull.init] You forgot to pass #{missing.join(',')} needed to initialize hull properly") if missing.length
156 |
157 | hull._initialized = true
158 |
159 | client = {}
160 | channel = {}
161 |
162 | currentUser = new CurrentUser()
163 | currentConfig = new CurrentConfig()
164 |
165 | Raven.init(config.ravenDsn, {
166 | runtime: 'hull-js',
167 | orgUrl: config.orgUrl,
168 | appId: config.appId || config.platformId
169 | })
170 |
171 | throwErr = (err)->
172 | # Something was wrong while initializing
173 | logger.error(err.stack)
174 | userFailureCallback = userFailureCallback || ->
175 | userFailureCallback(err)
176 | ready.reject(err)
177 | Raven.captureException(err)
178 |
179 | # Ensure we have everything we need before starting Hull
180 | currentConfig.init(config).then (currentConfig)=>
181 | # Create the communication channel with Remote
182 | channel = new Channel(currentUser, currentConfig)
183 | channel.promise
184 | , throwErr
185 | .then (channel)=>
186 | # Create the Hull client that stores the API, Auth, Sharing and Tracking objects.
187 | client = new Client(channel, currentUser, currentConfig)
188 | , onInitFailure
189 | , throwErr
190 | .then (hullClient)=>
191 | # Initialize
192 | client.hull = assign(hull,client.hull)
193 | data = currentConfig.getRemote('data')
194 | currentUser.init(data?.me)
195 | initializePlatform(data, currentConfig, client.hull)
196 | onInitSuccess(userSuccessCallback, client.hull, data)
197 | , throwErr
198 | .catch throwErr
199 |
200 |
201 |
202 | ready = {}
203 | ready.promise = new Promise (resolve, reject)=>
204 | ready.reject = reject
205 | ready.resolve = resolve
206 |
207 | ready.promise.catch (err)-> throw new Error('Hull.ready callback error', err)
208 |
209 | hullReady = (callback, errback)->
210 | callback = callback || ->
211 | errback = errback || ->
212 | ready.promise.then (res)->
213 | callback(res.hull, res.me, res.app, res.org)
214 | res
215 | , errback
216 | .catch (err)-> logger.error err.message, err.stack
217 |
218 | shimmedMethod = (method)->
219 | logger.log("Hull.#{method} is only useful when Ships are sandboxed. This method does nothing here")
220 | false
221 |
222 | hull =
223 | _initialized : false
224 | initRemote : HullRemote
225 | init : init
226 | ready : hullReady
227 | version : VERSION
228 | revision : REVISION
229 | trackForm : Pool.create('trackForm')
230 | track : Pool.create('track')
231 | traits : Pool.create('traits')
232 | identify : Pool.create('identify')
233 | alias : Pool.create('alias')
234 | captureException: captureException
235 |
236 | # Assign EventBus methods to Hull
237 | eeMethods = ['on', 'onAny', 'offAny', 'once', 'many', 'off', 'emit']
238 | _.map eeMethods, (m)->
239 | hull[m] = (args...) -> EventBus[m](args...)
240 |
241 | unless window.Hull?
242 | autoStartConfig = scriptTagConfig()
243 | if autoStartConfig && autoStartConfig.autoStart
244 | if !hull._initialized
245 | autoStartConfig && autoStartConfig.autoStart && init(autoStartConfig)
246 |
247 | window.Hull = hull
248 | setTimeout ->
249 | window.hullAsyncInit(hull) if window?.hullAsyncInit and _.isFunction(window.hullAsyncInit)
250 | , 0
251 | else
252 | logger.error "Hull Snippet found more than once (or you already have a global variable named window.Hull). Either way, we can't launch Hull more than once. We only use the first one in the page"
253 |
254 | module.exports = hull
255 |
--------------------------------------------------------------------------------
/src/styles/style.scss:
--------------------------------------------------------------------------------
1 | @mixin reset(){
2 | div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, div.form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary, time, mark, audio, video, button, textarea, input, input[type]{
3 | alignment-adjust:auto;
4 | alignment-baseline:baseline;
5 | animation-play-state:running;
6 | animation:none 0 ease 0 1 normal;
7 | appearance:normal;
8 | azimuth:center;
9 | backface-visibility:visible;
10 | background-color:transparent;
11 | background-image:none;
12 | background:none 0 0 auto repeat scroll padding-box transparent;
13 | baseline-shift:baseline;
14 | binding:none;
15 | bleed:6pt;
16 | bookmark-label:content();
17 | bookmark-level:none;
18 | bookmark-state:open;
19 | bookmark-target:none;
20 | border-radius:0;
21 | border:0 none transparent;
22 | bottom:auto;
23 | box-align:stretch;
24 | box-decoration-break:slice;
25 | box-direction:normal;
26 | box-flex-group:1;
27 | box-flex:0.0;
28 | box-lines:single;
29 | box-ordinal-group:1;
30 | box-orient:inline-axis;
31 | box-pack:start;
32 | box-shadow:none;
33 | box-sizing:content-box;
34 | break-after:auto;
35 | break-before:auto;
36 | break-inside:auto;
37 | caption-side:top;
38 | clear:none;
39 | clip:auto;
40 | color-profile:auto;
41 | color:inherit;
42 | column-count:auto;
43 | column-fill:balance;
44 | column-gap:normal;
45 | column-rule:medium medium #1f1f1f;
46 | column-span:1;
47 | column-width:auto;
48 | columns:auto auto;
49 | content:normal;
50 | counter-increment:none;
51 | counter-reset:none;
52 | crop:auto;
53 | cursor:auto;
54 | direction:ltr;
55 | display:inline;
56 | dominant-baseline:auto;
57 | drop-initial-after-adjust:text-after-edge;
58 | drop-initial-after-align:baseline;
59 | drop-initial-before-adjust:text-before-edge;
60 | drop-initial-before-align:caps-height;
61 | drop-initial-size:auto;
62 | drop-initial-value:initial;
63 | elevation:level;
64 | empty-cells:show;
65 | filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);
66 | fit-position:0 0;
67 | fit:fill;
68 | float-offset:0 0;
69 | float:none;
70 | font-family:"Helvetica Neue", Helvetica, Arial, sans-serif;
71 | font-size-adjust:none;
72 | font-size:100%;
73 | font-stretch:normal;
74 | font-style:normal;
75 | font-variant:normal;
76 | font-weight:400;
77 | font:normal normal 100% "Helvetica Neue", Helvetica, Arial, sans-serif;
78 | grid-columns:none;
79 | grid-rows:none;
80 | hanging-punctuation:none;
81 | height:auto;
82 | hyphenate-after:auto;
83 | hyphenate-before:auto;
84 | hyphenate-character:auto;
85 | hyphenate-lines:no-limit;
86 | hyphenate-resource:none;
87 | hyphens:manual;
88 | icon:auto;
89 | image-orientation:auto;
90 | image-rendering:auto;
91 | image-resolution:normal;
92 | inline-box-align:last;
93 | left:auto;
94 | letter-spacing:normal;
95 | line-height:inherit;
96 | line-stacking:inline-line-height exclude-ruby consider-shifts;
97 | list-style:disc outside none;
98 | margin:0;
99 | marks:none;
100 | marquee-direction:forward;
101 | marquee-loop:1;
102 | marquee-play-count:1;
103 | marquee-speed:normal;
104 | marquee-style:scroll;
105 | max-height:none;
106 | max-width:none;
107 | min-height:0;
108 | min-width:0;
109 | move-to:normal;
110 | nav-down:auto;
111 | nav-index:auto;
112 | nav-left:auto;
113 | nav-right:auto;
114 | nav-up:auto;
115 | opacity:1;
116 | orphans:2;
117 | outline-offset:0;
118 | outline:invert none medium;
119 | overflow-style:auto;
120 | overflow:visible;
121 | padding:0;
122 | page-break-after:auto;
123 | page-break-before:auto;
124 | page-break-inside:auto;
125 | page-policy:start;
126 | page:auto;
127 | perspective-origin:50% 50%;
128 | perspective:none;
129 | position:static;
130 | presentation-level:0;
131 | punctuation-trim:none;
132 | quotes:none;
133 | rendering-intent:auto;
134 | resize:none;
135 | right:auto;
136 | rotation-point:50% 50%;
137 | rotation:0;
138 | ruby-align:auto;
139 | ruby-overhang:none;
140 | ruby-position:before;
141 | ruby-span:none;
142 | size:auto;
143 | string-set:none;
144 | table-layout:auto;
145 | text-align-last:start;
146 | text-align:start;
147 | text-decoration:none;
148 | text-emphasis:none;
149 | text-height:auto;
150 | text-indent:0;
151 | text-justify:auto;
152 | text-outline:none;
153 | text-shadow:none;
154 | text-transform:none;
155 | text-wrap:normal;
156 | top:auto;
157 | transform-origin:50% 50% 0;
158 | transform-style:flat;
159 | transform:none;
160 | transition:all 0 ease 0;
161 | unicode-bidi:normal;
162 | vertical-align:baseline;
163 | white-space-collapse:collapse;
164 | white-space:normal;
165 | widows:2;
166 | width:auto;
167 | word-break:normal;
168 | word-spacing:normal;
169 | word-wrap:normal;
170 | z-index:auto;
171 | }
172 |
173 | address,
174 | article,
175 | aside,
176 | blockquote,
177 | canvas,
178 | center,
179 | dd,
180 | details,
181 | dir,
182 | div,
183 | div.form,
184 | dl,
185 | dt,
186 | fieldset,
187 | figcaption,
188 | figure,
189 | footer,
190 | form,
191 | frame,
192 | frameset,
193 | h1,
194 | h2,
195 | h3,
196 | h4,
197 | h5,
198 | h6,
199 | header,
200 | hgroup,
201 | hr,
202 | menu,
203 | menu,
204 | nav,
205 | noframes,
206 | ol,
207 | p,
208 | pre,
209 | section,
210 | summary,
211 | ul {
212 | display: block;
213 | }
214 |
215 | li {
216 | display: list-item;
217 | }
218 |
219 | table {
220 | display: table;
221 | }
222 |
223 | tr {
224 | display: table-row;
225 | }
226 |
227 | thead {
228 | display: table-header-group;
229 | }
230 |
231 | tbody {
232 | display: table-row-group;
233 | }
234 |
235 | tfoot {
236 | display: table-footer-group;
237 | }
238 |
239 | col {
240 | display: table-column;
241 | }
242 |
243 | colgroup {
244 | display: table-column-group;
245 | }
246 |
247 | td,
248 | th {
249 | display: table-cell;
250 | }
251 |
252 | caption {
253 | display: table-caption;
254 | }
255 |
256 | input,
257 | select {
258 | display: inline-block;
259 | }
260 |
261 | b,
262 | strong {
263 | font-weight: bold;
264 | }
265 |
266 | textarea,
267 | input {
268 | cursor: text;
269 | }
270 |
271 | textarea::-webkit-input-placeholder,
272 | input::-webkit-input-placeholder {
273 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
274 | font-size-adjust: none;
275 | font-size: 100%;
276 | font-style: normal;
277 | letter-spacing: normal;
278 | font-stretch: normal;
279 | font-variant: normal;
280 | font-weight: normal;
281 | font: normal normal 100% "Helvetica Neue", Helvetica, Arial, sans-serif;
282 | text-align: left;
283 | text-align-last: start;
284 | text-decoration: none;
285 | text-emphasis: none;
286 | text-height: auto;
287 | text-indent: 0;
288 | text-justify: auto;
289 | text-outline: none;
290 | text-shadow: none;
291 | text-transform: none;
292 | text-wrap: normal;
293 | background-color: inherit;
294 | color: inherit;
295 | }
296 |
297 | textarea::-moz-placeholder,
298 | input::-moz-placeholder {
299 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
300 | font-size-adjust: none;
301 | font-size: 100%;
302 | font-style: normal;
303 | letter-spacing: normal;
304 | font-stretch: normal;
305 | font-variant: normal;
306 | font-weight: normal;
307 | font: normal normal 100% "Helvetica Neue", Helvetica, Arial, sans-serif;
308 | text-align: left;
309 | text-align-last: start;
310 | text-decoration: none;
311 | text-emphasis: none;
312 | text-height: auto;
313 | text-indent: 0;
314 | text-justify: auto;
315 | text-outline: none;
316 | text-shadow: none;
317 | text-transform: none;
318 | text-wrap: normal;
319 | background-color: inherit;
320 | color: inherit;
321 | }
322 |
323 | textarea:-ms-input-placeholder,
324 | input:-ms-input-placeholder {
325 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
326 | font-size-adjust: none;
327 | font-size: 100%;
328 | font-style: normal;
329 | letter-spacing: normal;
330 | font-stretch: normal;
331 | font-variant: normal;
332 | font-weight: normal;
333 | font: normal normal 100% "Helvetica Neue", Helvetica, Arial, sans-serif;
334 | text-align: left;
335 | text-align-last: start;
336 | text-decoration: none;
337 | text-emphasis: none;
338 | text-height: auto;
339 | text-indent: 0;
340 | text-justify: auto;
341 | text-outline: none;
342 | text-shadow: none;
343 | text-transform: none;
344 | text-wrap: normal;
345 | background-color: inherit;
346 | color: inherit;
347 | }
348 |
349 | input[type=checkbox],
350 | input[type=radio] {
351 | cursor: default;
352 | }
353 |
354 | a,
355 | a *,
356 | a span,
357 | button,
358 | button *,
359 | button span,
360 | input[type=submit],
361 | input[type=reset] {
362 | cursor: pointer;
363 | }
364 |
365 | a:active
366 | a:hover,
367 | a:link,
368 | a:visited {
369 | color: inherit;
370 | background: transparent;
371 | text-shadow: none;
372 | }
373 |
374 | button::-moz-focus-inner {
375 | border: 0;
376 | padding: 0;
377 | }
378 | }
379 |
380 | .hull-reset.hull-reset{
381 | @include reset();
382 | }
383 |
--------------------------------------------------------------------------------
/src/polyfills/xhr-xdr.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | xhr-xdr-adapter
4 |
5 | Use XDomainRequest on IE 8 and IE 9 for cross-origin requests only.
6 | Default to standard XMLHttpRequest otherwise.
7 |
8 | Include this file on IE 8 & 9 before making cross-origin ajax requests
9 | using libraries or frameworks such as jQuery or AngularJS. This will allow
10 | you to do things like AJAX GET templates/assets from a CDN at run time using
11 | the standard XMLHttpRequest API on IE 8/9, or do simple cross-domain POSTs.
12 |
13 | But it doesn't get around the basic limitations of IE 8 & 9's XDomainRequest:
14 | * No authentication or cookies can be sent
15 | * POST or GET only
16 | * No custom headers can be sent
17 | * text/plain contentType only
18 |
19 |
20 | The MIT License (MIT)
21 |
22 | Copyright (c) 2014 Intuit Inc.
23 |
24 | Permission is hereby granted, free of charge, to any person obtaining a copy
25 | of this software and associated documentation files (the "Software"), to deal
26 | in the Software without restriction, including without limitation the rights
27 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
28 | copies of the Software, and to permit persons to whom the Software is
29 | furnished to do so, subject to the following conditions:
30 |
31 | The above copyright notice and this permission notice shall be included in all
32 | copies or substantial portions of the Software.
33 |
34 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
35 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
36 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
37 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
38 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
39 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
40 | SOFTWARE.
41 | */
42 |
43 | (function () {
44 | "use strict";
45 | // Ignore everything below if not on IE 8 or IE 9.
46 | if (!window.XDomainRequest) { // typeof XDomainRequest is 'object' in IE 8, 'function' in IE 9
47 | return;
48 | }
49 | if ('withCredentials' in new window.XMLHttpRequest()) {
50 | return;
51 | }
52 | if (window.XMLHttpRequest.supportsXDR === true) {
53 | // already set up
54 | return;
55 | }
56 |
57 | var OriginalXMLHttpRequest = window.XMLHttpRequest;
58 | var urlRegEx = /^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/;
59 | var httpRegEx = /^https?:\/\//i;
60 | var getOrPostRegEx = /^get|post$/i;
61 | var sameSchemeRegEx = new RegExp('^' + location.protocol, 'i');
62 |
63 | // Determine whether XDomainRequest should be used for a given request
64 | var useXDR = function (method, url, async) {
65 | var remoteUrl = url;
66 | var baseHref;
67 | var myLocationParts;
68 | var remoteLocationParts;
69 | var crossDomain;
70 |
71 | try {
72 | // account for the possibility of a setting, which could make a URL that looks relative actually be cross-domain
73 | if ((remoteUrl && remoteUrl.indexOf("://") < 0) && document.getElementsByTagName('base').length > 0) {
74 | baseHref = document.getElementsByTagName('base')[0].href;
75 | if (baseHref) {
76 | remoteUrl = baseHref + remoteUrl;
77 | }
78 | }
79 |
80 | myLocationParts = urlRegEx.exec(location.href);
81 | remoteLocationParts = urlRegEx.exec(remoteUrl);
82 | crossDomain = (myLocationParts[2].toLowerCase() !== remoteLocationParts[2].toLowerCase());
83 |
84 | // XDomainRequest can only be used for async get/post requests across same scheme, which must be http: or https:
85 | return crossDomain && async && getOrPostRegEx.test(method) && httpRegEx.test(remoteUrl) && sameSchemeRegEx.test(remoteUrl);
86 | }
87 | catch (ex) {
88 | return false;
89 | }
90 | };
91 |
92 | window.XMLHttpRequest = function () {
93 | var self = this;
94 | this._setReadyState = function (readyState) {
95 | if (self.readyState !== readyState) {
96 | self.readyState = readyState;
97 | if (typeof self.onreadystatechange === "function") {
98 | self.onreadystatechange();
99 | }
100 | self._runCallbacks('readystatechange');
101 | }
102 | };
103 | this._callbacks = {};
104 | this.readyState = 0;
105 | this.responseText = "";
106 | this.status = 0;
107 | this.statusText = "";
108 | this.withCredentials = false;
109 | };
110 |
111 | window.XMLHttpRequest.prototype._runCallbacks = function(event, args) {
112 | var self = this;
113 | var callbacks = (self._callbacks[event] || []).slice();
114 | var cb = callbacks.pop();
115 | while (cb) {
116 | cb.apply(self, args);
117 | cb = callbacks.pop();
118 | }
119 | };
120 |
121 | window.XMLHttpRequest.prototype.addEventListener = function(event, callback) {
122 | var self = this;
123 | this._callbacks[event] = this._callbacks[event] || [];
124 | this._callbacks[event].push(callback);
125 | };
126 |
127 | window.XMLHttpRequest.prototype.open = function (method, url, async) {
128 | var self = this;
129 | var request;
130 |
131 | if (useXDR(method, url, async)) {
132 |
133 | // Use XDR
134 | request = new XDomainRequest();
135 | request._xdr = true;
136 | request.onerror = function () {
137 | self.status = 400;
138 | self.statusText = "Error";
139 | self._setReadyState(4);
140 | if (self.onerror) {
141 | self.onerror();
142 | self._runCallbacks('error');
143 | }
144 | };
145 | request.ontimeout = function () {
146 | self.status = 408;
147 | self.statusText = "Timeout";
148 | self._setReadyState(2);
149 | if (self.ontimeout) {
150 | self.ontimeout();
151 | }
152 | self._runCallbacks('timeout');
153 | };
154 | request.onload = function () {
155 | self.responseText = request.responseText;
156 | self.status = 200;
157 | self.statusText = "OK";
158 | self._setReadyState(4);
159 | if (self.onload) {
160 | self.onload();
161 | }
162 | self._runCallbacks('load', [].slice.call(arguments));
163 | };
164 | request.onprogress = function () {
165 | if (self.onprogress) {
166 | self.onprogress.apply(self, arguments);
167 | }
168 | self._runCallbacks('progress', [].slice.call(arguments));
169 | };
170 | }
171 |
172 | else {
173 |
174 | // Use standard XHR
175 | request = new OriginalXMLHttpRequest();
176 | request.withCredentials = this.withCredentials;
177 | request.onreadystatechange = function () {
178 | if (request.readyState === 4) {
179 | try {
180 | self.status = request.status;
181 | self.statusText = request.statusText;
182 | self.responseText = request.responseText;
183 | self.responseXML = request.responseXML;
184 | } catch (e) {
185 |
186 | }
187 | }
188 | self._setReadyState(request.readyState);
189 | };
190 | request.onabort = function () {
191 | if (self.onabort) {
192 | self.onabort.apply(self, arguments);
193 | }
194 | };
195 | request.onerror = function () {
196 | if (self.onerror) {
197 | self.onerror.apply(self, arguments);
198 | }
199 | };
200 | request.onload = function () {
201 | if (self.onload) {
202 | self.onload.apply(self, arguments);
203 | }
204 | };
205 | request.onloadend = function () {
206 | if (self.onloadend) {
207 | self.onloadend.apply(self, arguments);
208 | }
209 | };
210 | request.onloadstart = function () {
211 | if (self.onloadstart) {
212 | self.onloadstart.apply(self, arguments);
213 | }
214 | };
215 | request.onprogress = function () {
216 | if (self.onprogress) {
217 | self.onprogress.apply(self, arguments);
218 | }
219 | };
220 | }
221 |
222 | this._request = request;
223 | request.open.apply(request, arguments);
224 | this._setReadyState(1);
225 | };
226 |
227 | window.XMLHttpRequest.prototype.abort = function () {
228 | this._request.abort();
229 | this._setReadyState(0);
230 | this.onreadystatechange = null;
231 | };
232 |
233 | window.XMLHttpRequest.prototype.send = function (body) {
234 | var self = this;
235 | this._request.withCredentials = this.withCredentials;
236 |
237 | if (this._request._xdr) {
238 | setTimeout(function () {
239 | self._request.send(body);
240 | }, 0);
241 | }
242 | else {
243 | this._request.send(body);
244 | }
245 |
246 | if (this._request.readyState === 4) {
247 | // when async==false the browser is blocked until the transfer is complete and readyState becomes 4
248 | // onreadystatechange should not get called in this case
249 | this.status = this._request.status;
250 | this.statusText = this._request.statusText;
251 | this.responseText = this._request.responseText;
252 | this.readyState = this._request.readyState;
253 | }
254 | else {
255 | this._setReadyState(2);
256 | }
257 | };
258 |
259 | window.XMLHttpRequest.prototype.setRequestHeader = function () {
260 | if (this._request.setRequestHeader) {
261 | this._request.setRequestHeader.apply(this._request, arguments);
262 | }
263 | };
264 |
265 | window.XMLHttpRequest.prototype.getAllResponseHeaders = function () {
266 | if (this._request.getAllResponseHeaders) {
267 | return this._request.getAllResponseHeaders();
268 | }
269 | else {
270 | return ("Content-Length: " + this.responseText.length +
271 | "\r\nContent-Type:" + this._request.contentType);
272 | }
273 | };
274 |
275 | window.XMLHttpRequest.prototype.getResponseHeader = function (header) {
276 | if (this._request.getResponseHeader) {
277 | return this._request.getResponseHeader.apply(this._request, arguments);
278 | }
279 | if (typeof header !== "string") {
280 | return;
281 | }
282 | header = header.toLowerCase();
283 | if (header === "content-type") {
284 | return this._request.contentType;
285 | }
286 | else if (header === "content-length") {
287 | return this.responseText.length;
288 | }
289 | };
290 |
291 | window.XMLHttpRequest.supportsXDR = true;
292 | })();
293 |
--------------------------------------------------------------------------------
/src/client/auth.coffee:
--------------------------------------------------------------------------------
1 | assign = require '../polyfills/assign'
2 | Promise = require '../utils/promises'
3 | _ = require '../utils/lodash'
4 | logger = require '../utils/logger'
5 | EventBus = require '../utils/eventbus'
6 | isMobile = require '../utils/is-mobile'
7 |
8 | getNoUserPromise = ()->
9 | Promise.reject({
10 | reason: 'no_current_user',
11 | message: 'User must be logged in to perform this action'
12 | })
13 |
14 | getUser = ()->
15 | user = Hull.currentUser()
16 | return (!!user && user.id?)
17 |
18 | parseParams = (argsArray)->
19 |
20 | opts = {}
21 |
22 | while (next = argsArray.shift())
23 | if _.isString next
24 | if !login
25 | login = next
26 | else
27 | password = next
28 | else if _.isFunction next
29 | if !callback
30 | callback = next
31 | else
32 | errback = next
33 | else if _.isObject(next) and !_.isEmpty(next)
34 | if !login and !password
35 | opts = next
36 | else
37 | opts.params = assign(next, opts.params||{})
38 |
39 | if login
40 | opts = if password then assign(opts, {login, password}) else assign(opts, {provider:login})
41 |
42 | opts.params = opts.params || {}
43 | callback = callback || ->
44 | errback = errback || ->
45 |
46 | opts.params.display ||= 'touch' if isMobile()
47 |
48 | # Setup defaults for Popup login for Facebook on Desktop
49 |
50 | # # Redirect login by default. if on Mobile.
51 | # # Setup 'display' to be 'touch' for Facebook Login if on Mobile
52 | if opts.provider == 'facebook'
53 | opts.params.display ||= if opts.strategy == 'redirect' then 'page' else 'popup'
54 |
55 | # TODO : OK to ignore `params` in this email login scenario ?
56 | delete opts.params if opts.password?
57 |
58 | {
59 | options: opts
60 | callback : callback
61 | errback : errback
62 | }
63 |
64 | postForm = (path, method='post', params={}) ->
65 | form = document.createElement("form")
66 | form.setAttribute("method", method)
67 | form.setAttribute("action", path)
68 |
69 | for key of params
70 | if params.hasOwnProperty key
71 | hiddenField = document.createElement("input")
72 | hiddenField.setAttribute("type", "hidden")
73 | hiddenField.setAttribute("name", key)
74 | hiddenField.setAttribute("value", params[key])
75 | form.appendChild(hiddenField)
76 | document.body.appendChild(form)
77 | form.submit()
78 |
79 | class Auth
80 | constructor: (api, currentUser, currentConfig)->
81 | @api = api
82 | @currentUser = currentUser
83 | @currentConfig = currentConfig
84 | @_popupInterval = null
85 | @_authenticating = null
86 | @authServices = _.keys(@currentConfig.get('services.auth'))
87 |
88 | isAuthenticating : -> @_authenticating?
89 |
90 | # Generates the complete URL to be reached to validate login
91 | generateAuthUrl : (opts={})->
92 | @createAuthCallback()
93 | params = opts.params || {}
94 | params.app_id = @currentConfig.get('appId')
95 | # The following is here for backward compatibility. Must be removed at first sight next time
96 | params.callback_url = opts.redirect_url || params.callback_url || @currentConfig.get('callback_url') || @currentConfig.get('callbackUrl') || document.location.toString()
97 | params.auth_referer = document.location.toString()
98 | params.version = @currentConfig.get('version')
99 | params._bid = @currentConfig.identifyBrowser().id
100 | params._sid = @currentConfig.identifySession().id
101 | querystring = _.map params,(v,k) ->
102 | encodeURIComponent(k)+'='+encodeURIComponent(v)
103 | .join('&')
104 | "#{@currentConfig.get('orgUrl')}/auth/#{opts.provider}?#{querystring}"
105 |
106 | createAuthCallback: =>
107 | window.__hull_login_status__ = (hash) =>
108 | window.__hull_login_status__ = null
109 | @onAuthComplete(hash)
110 |
111 | popupAuthWindow: (path, opts={})->
112 |
113 | # Handle smaller Facebook popup windows
114 | [width, height] = if opts.provider == 'facebook' and opts.params.display == 'popup' then [500, 400] else [1030, 550]
115 |
116 | openerString = "location=0,status=0,width=#{width},height=#{height}"
117 |
118 | w = window.open(path, "_auth",openerString)
119 |
120 | # Support for cordova events
121 | if window.device?.cordova
122 | w?.addEventListener 'loadstart', (event)->
123 | hash = try JSON.parse(Base64.decode(event.url.split('#')[1]))
124 | if hash
125 | window.__hull_login_status__(hash)
126 | w.close()
127 |
128 | # 30 seconds after creating popup, reject promise if still active.
129 |
130 | setTimeout ()=>
131 | @onAuthComplete({ success: false, error: { reason: 'timeout', message: 'Timeout for login (after 30 seconds), User never finished the auth' } })
132 | , 90000
133 |
134 | # Reject Promise if window has been closed
135 | @_popupInterval = w? && setInterval =>
136 | @onAuthComplete({ success: false, error: { reason: 'window_closed', message: 'User closed the window before finishing. He might have canceled' } }) if w?.closed
137 | , 200
138 |
139 | onAuthComplete : (hash)=>
140 | return unless @_authenticating
141 | if hash.success
142 | @_authenticating.resolve({})
143 | else
144 | error = new Error("Login failed : #{hash?.error?.reason}")
145 | error[k] = v for k, v of hash.error
146 | @_authenticating.reject(error)
147 |
148 | @_authenticating = null
149 | clearInterval(@_popupInterval)
150 | @_popupInterval = null
151 |
152 | undefined
153 |
154 | loginWithProvider : (opts)=>
155 | isAuthenticating = @isAuthenticating()
156 | return isAuthenticating if isAuthenticating
157 |
158 | @_authenticating = {}
159 | promise = new Promise (resolve, reject)=>
160 | @_authenticating.resolve = resolve
161 | @_authenticating.reject = reject
162 |
163 | unless ~(_.indexOf(@authServices, opts.provider))
164 | @_authenticating.reject
165 | message: "No authentication service #{opts.provider} configured for the app"
166 | reason: 'no_such_service'
167 | return promise
168 |
169 | @_authenticating.provider = opts.provider.toLowerCase()
170 |
171 | authUrl = @generateAuthUrl(opts)
172 |
173 | if opts.strategy == 'redirect'
174 | # Don't do an early return to not break promise chain
175 | window.location.href = authUrl
176 | else
177 | # Classic Popup Strategy
178 | @popupAuthWindow(authUrl, opts)
179 |
180 | promise
181 |
182 | login : () =>
183 | if @isAuthenticating()
184 | # Return promise even if login is in progress.
185 | msg = "Login in progress. Use `Hull.on('hull.user.login', callback)` to call `callback` when done."
186 | logger.info msg
187 | return Promise.reject {error: {reason:'in_progress', message: 'Login already in progress'}}
188 |
189 | # Handle Legacy Format,
190 | # Ensure New Format: Hash signature
191 | # Preprocess Options
192 | # Opts format is now : {login:"", password:"", params:{}} or {provider:"", params:{}}
193 | {options, callback, errback} = parseParams(Array.prototype.slice.call(arguments))
194 |
195 | if !(options?.provider? || options.login? || options.access_token?)
196 | # UserName+Password
197 | # Hull.login({login:'abcd@ef.com', password:'passwd', strategy:'redirect|popup', redirect:'...'})
198 | unless options.login? and options.password?
199 | msg = 'Seems like something is wrong in your Hull.login() call, We need a login and password fields to login. Read up here: http://www.hull.io/docs/references/hull_js/#user-signup-and-login'
200 | logger.warn msg
201 | return Promise.reject({error:{ reason:'missing_parameters', message:'Empty login or password' }})
202 |
203 |
204 |
205 | if options.provider?
206 | # Social Login
207 | if options.access_token?
208 | # Hull.login({provider:'facebook', access_token:'xxxx'})
209 | provider = assign({}, options)
210 | delete provider.provider
211 | op = {}
212 | op[options.provider] = provider
213 | promise = @api.message('users', 'post', op)
214 | else
215 | # Hull.login({provider:'facebook', strategy:'redirect|popup', redirect:'...'})
216 | promise = @loginWithProvider(options)
217 |
218 | else
219 |
220 | # Email Login
221 | # Hull.login({login:'user@host.com', password:'xxxx'})
222 | # Hull.login({access_token:'xxxxx'})
223 |
224 | promise = @api.message('users/login', 'post', _.pick(options, 'login', 'password', 'access_token'))
225 |
226 | if options.strategy == 'redirect' || !@currentConfig.getRemote('cookiesEnabled')
227 | postUrl = @currentConfig.get('orgUrl')+'/api/v1/users/login'
228 | options.redirect_url ||= window.location.href
229 | promise = promise.then -> postForm(postUrl, 'post', options)
230 |
231 | @completeLoginPromiseChain(promise, callback, errback)
232 |
233 | logout: (options={}, callback, errback) =>
234 | @currentConfig.resetIdentify()
235 | @api.channel.rpc.resetIdentify()
236 | promise = @api.message('logout')
237 |
238 | if options.strategy == 'redirect' || !@currentConfig.getRemote('cookiesEnabled')
239 | redirect_url = options.redirect_url || document.location.href
240 | # Add this param to make sure safari actually redirects to the logoutUrl
241 | b = new Date().getTime()
242 | logoutUrl = @currentConfig.get('orgUrl') + '/api/v1/logout?b=' + b + '&redirect_url=' + encodeURIComponent(redirect_url)
243 | promise.then -> document.location = logoutUrl
244 |
245 | @completeLoginPromiseChain(promise,callback,errback)
246 |
247 |
248 | resetPassword : (email=@currentUser.get('email'), callback, errback) =>
249 | promise = @api.message('/users/request_password_reset', 'post', {email})
250 | @completeLoginPromiseChain(promise,callback,errback)
251 |
252 | confirmEmail : (email=@currentUser.get('email'), callback, errback) =>
253 | promise = @api.message('/users/request_confirmation_email', 'post', {email})
254 | @completeLoginPromiseChain(promise,callback,errback)
255 |
256 | signup : (attrs, callback, errback) =>
257 | promise = @api.message('users', 'POST', attrs)
258 | if !@currentConfig.getRemote('cookiesEnabled')
259 | postUrl = @currentConfig.get('orgUrl')+'/api/v1/users/login'
260 | promise = promise.then (user)->
261 | params = {
262 | redirect_url: window.location.href,
263 | access_token: user.access_token
264 | }
265 | postForm(postUrl, 'post', params)
266 | @completeLoginPromiseChain(promise, callback, errback)
267 |
268 | ###*
269 | * link an Identity to a Hull User
270 | * @param {options} An options Hash
271 | * @param {callback} Success callback
272 | * @param {errback} error callback
273 | * @return {Promise} A promise
274 | ###
275 | linkIdentity : ()=>
276 | return getNoUserPromise() unless getUser()
277 | {options, callback, errback} = parseParams(Array.prototype.slice.call(arguments))
278 | options.params.mode = 'connect'
279 | @login(options, callback, errback)
280 |
281 | ###*
282 | * unlink an Identity from a Hull User
283 | * @param {options} An options Hash
284 | * @param {callback} Success callback
285 | * @param {errback} error callback
286 | * @return {Promise} A promise
287 | *
288 | ###
289 | unlinkIdentity : ()=>
290 | return getNoUserPromise() unless getUser()
291 | {options, callback, errback} = parseParams(Array.prototype.slice.call(arguments))
292 | promise = @api.message("me/identities/#{options.provider}", 'delete')
293 | @completeLoginPromiseChain(promise,callback,errback)
294 |
295 | completeLoginPromiseChain: (promise, callback,errback)=>
296 | if promise && promise.then
297 | callback = callback || ->
298 | errback = errback || ->
299 |
300 | p = promise.then @api.refreshUser, @emitLoginFailure
301 | p.then callback, errback
302 | p
303 |
304 | emitLoginFailure : (err)->
305 | EventBus.emit("hull.user.fail", err)
306 | err
307 | throw err
308 |
309 | module.exports = Auth
310 |
--------------------------------------------------------------------------------
/src/utils/leaky-bucket.js:
--------------------------------------------------------------------------------
1 | // Origin: https://github.com/linaGirl/leaky-bucket
2 | // Version: 3.0.4
3 | // License: MIT
4 |
5 |
6 | class LeakyBucket {
7 |
8 |
9 | /**
10 | * Sets up the leaky bucket. The bucket is designed so that it can
11 | * burst by the capacity it is given. after that items can be queued
12 | * until a timeout of n seonds is reached.
13 | *
14 | * example: throttle 10 actions per minute that have each a cost of 1, reject
15 | * everything theat is overflowing. there will no more than 10 items queued
16 | * at any time
17 | * capacity: 10
18 | * interval: 60
19 | * timeout: 60
20 | *
21 | * example: throttle 100 actions per minute that have a cost of 1, reject
22 | * items that have to wait more thatn 2 minutes. there will be no more thatn
23 | * 200 items queued at any time. of those 200 items 100 will be bursted within
24 | * a minute, the rest will be executed evenly spread over a mintue.
25 | * capacity: 100
26 | * interval: 60
27 | * timeout: 120
28 | *
29 | * @param {number} capacity the capacity the bucket has per interval
30 | * @param {number} timeout the total time items are allowed to wait for execution
31 | * @param {number} interval the interval for the capacity in seconds
32 | */
33 | constructor({
34 | capacity = 60,
35 | timeout,
36 | interval = 60000,
37 | } = {}) {
38 | // set the timeout to the interval if not set, so that the bucket overflows as soon
39 | // the capacity is reached
40 | if (isNaN(timeout)) timeout = interval;
41 |
42 | // queue containing all items to execute
43 | this.queue = [];
44 |
45 | // the value f all items currently enqueued
46 | this.totalCost = 0;
47 |
48 | // the capacity, which can be used at this moment
49 | // to execute items
50 | this.currentCapacity = capacity;
51 |
52 | // time when the last refill occured
53 | this.lastRefill = null;
54 |
55 |
56 | this.setCapacity(capacity);
57 | this.setTimeout(timeout);
58 | this.setInterval(interval);
59 | }
60 |
61 |
62 |
63 |
64 | /**
65 | * dthe throttle method is used to throttle things. it is async and will resolve either
66 | * immediatelly, if there is space in the bucket, than can be bursted, or it will wait
67 | * until there is enough capacity left to execute the item with the given cost. if the
68 | * bucket is overflowing, and the item cannot be executed within the timeout of the bucket,
69 | * the call will be rejected with an error.
70 | *
71 | * @param {number} cost=1 the cost of the item to be throttled. is the cost is unknown,
72 | * the cost can be payed after execution using the pay method.
73 | * defaults to 1.
74 | * @param {boolean} append = true set to false if the item needs ot be added to the
75 | * beginning of the queue
76 | * @param {boolean} isPause = false defines if the element is a pause elemtn, if yes, it
77 | * will not be cleaned off of the queue when checking
78 | * for overflowing elements
79 | * @returns {promise} resolves when the item can be executed, rejects if the item cannot
80 | * be executed in time
81 | */
82 | throttle(cost = 1, append = true, isPause = false) {
83 | const maxCurrentCapacity = this.getCurrentMaxCapacity();
84 |
85 | // if items are added at the beginning, the excess items will be remove
86 | // later on
87 | if (append && this.totalCost + cost > maxCurrentCapacity) {
88 | throw new Error(`Cannot throttle item, bucket is overflowing: the maximum capacity is ${maxCurrentCapacity}, the current total capacity is ${this.totalCost}!`);
89 | }
90 |
91 | return new Promise((resolve, reject) => {
92 | const item = {
93 | resolve,
94 | reject,
95 | cost,
96 | isPause,
97 | };
98 |
99 | this.totalCost += cost;
100 |
101 | if (append) {
102 | this.queue.push(item);
103 | } else {
104 | this.queue.unshift(item);
105 | this.cleanQueue();
106 | }
107 |
108 |
109 | this.startTimer();
110 | });
111 | }
112 |
113 |
114 |
115 | /**
116 | * either executes directly when enough capacity is present or delays the
117 | * execution until enough capacity is available.
118 | *
119 | * @private
120 | */
121 | startTimer() {
122 | if (!this.timer && this.queue.length > 0) {
123 | const item = this.getFirstItem();
124 |
125 | this.refill();
126 |
127 | if (this.currentCapacity >= item.cost) {
128 | item.resolve();
129 |
130 | // remove the item from the queue
131 | this.shiftQueue();
132 |
133 | // pay it's cost
134 | this.pay(item.cost);
135 |
136 | // go to the next item
137 | this.startTimer();
138 | } else {
139 | const requiredDelta = item.cost + (this.currentCapacity * -1);
140 | const timeToDelta = requiredDelta / this.refillRate * 1000;
141 |
142 | // wait until the next item can be handled
143 | this.timer = setTimeout(() => {
144 | this.timer = 0;
145 | this.startTimer();
146 | }, timeToDelta);
147 | }
148 | }
149 | }
150 |
151 |
152 | /**
153 | * removes the first item in the queue, resolves the promise that indicated
154 | * that the bucket is empty and no more items are waiting
155 | *
156 | * @private
157 | */
158 | shiftQueue() {
159 | this.queue.shift();
160 |
161 | if (this.queue.length === 0 && this.emptyPromiseResolver) {
162 | this.emptyPromiseResolver();
163 | }
164 | }
165 |
166 |
167 |
168 | /**
169 | * is resolved as soon as the bucket is empty. is basically an event
170 | * that is emitted
171 | */
172 | isEmpty() {
173 | if (!this.emptyPromiseResolver) {
174 | this.emptyPromise = new Promise((resolve) => {
175 | this.emptyPromiseResolver = () => {
176 | this.emptyPromiseResolver = null;
177 | this.emptyPromise = null;
178 | resolve();
179 | };
180 | });
181 | }
182 |
183 | return this.emptyPromise;
184 | }
185 |
186 |
187 |
188 |
189 | /**
190 | * ends the bucket. The bucket may be recycled after this call
191 | */
192 | end() {
193 | this.stopTimer();
194 | this.clear();
195 | }
196 |
197 |
198 |
199 | /**
200 | * removes all items from the queue, does not stop the timer though
201 | *
202 | * @privae
203 | */
204 | clear() {
205 | this.queue = [];
206 | }
207 |
208 |
209 |
210 | /**
211 | * can be used to pay costs for items where the cost is clear after exection
212 | * this will devcrease the current capacity availabe on the bucket.
213 | *
214 | * @param {number} cost the ost to pay
215 | */
216 | pay(cost) {
217 |
218 | // reduce the current capacity, so that bursts
219 | // as calculated correctly
220 | this.currentCapacity -= cost;
221 |
222 | // keep track of the total cost for the bucket
223 | // so that we know when we're overflowing
224 | this.totalCost -= cost;
225 |
226 | // store the date the leky bucket was starting to leak
227 | // so that it can be refilled correctly
228 | if (this.lastRefill === null) {
229 | this.lastRefill = Date.now();
230 | }
231 | }
232 |
233 |
234 |
235 | /**
236 | * stops the running times
237 | *
238 | * @private
239 | */
240 | stopTimer() {
241 | if (this.timer) {
242 | clearTimeout(this.timer);
243 | this.timer = null;
244 | }
245 | }
246 |
247 |
248 |
249 | /**
250 | * refills the bucket with capacity which has become available since the
251 | * last refill. starts to refill after a call has started using capacity
252 | *
253 | * @private
254 | */
255 | refill() {
256 |
257 | // don't do refills, if we're already full
258 | if (this.currentCapacity < this.capacity) {
259 |
260 | // refill the currently avilable capacity
261 | const refillAmount = ((Date.now() - this.lastRefill) / 1000) * this.refillRate;
262 | this.currentCapacity += refillAmount;
263 |
264 | // make sure, that no more capacity is added than is the maximum
265 | if (this.currentCapacity >= this.capacity) {
266 | this.currentCapacity = this.capacity;
267 | this.lastRefill = null;
268 | } else {
269 | // date of last refill, ued for the next refill
270 | this.lastRefill = Date.now();
271 | }
272 | }
273 | }
274 |
275 |
276 |
277 | /**
278 | * gets the currenlty avilable max capacity, respecintg
279 | * the capacity that is already used in the moment
280 | *
281 | * @private
282 | */
283 | getCurrentMaxCapacity() {
284 | this.refill();
285 | return this.maxCapacity - (this.capacity - this.currentCapacity);
286 | }
287 |
288 |
289 |
290 | /**
291 | * removes all items that cannot be executed in time due to items
292 | * that were added in front of them in the queue (mostly pause items)
293 | *
294 | * @private
295 | */
296 | cleanQueue() {
297 | const maxCapacity = this.getCurrentMaxCapacity();
298 | let currentCapacity = 0;
299 |
300 | // find the first item, that goes over the thoretical maximal
301 | // capacity that is available
302 | const index = this.queue.findIndex((item) => {
303 | currentCapacity += item.cost;
304 | return currentCapacity > maxCapacity;
305 | });
306 |
307 |
308 | // reject all items that cannot be enqueued
309 | if (index >= 0) {
310 | this.queue.splice(index).forEach((item) => {
311 | if (!item.isPause) {
312 | item.reject(new Error(`Cannot throttle item because an item was added in front of it which caused the queue to overflow!`));
313 | this.totalCost -= item.cost;
314 | }
315 | });
316 | }
317 | }
318 |
319 |
320 |
321 | /**
322 | * returns the first item from the queue
323 | *
324 | * @private
325 | */
326 | getFirstItem() {
327 | if (this.queue.length > 0) {
328 | return this.queue[0];
329 | } else {
330 | return null;
331 | }
332 | }
333 |
334 |
335 |
336 | /**
337 | * pasue the bucket for the given cost. means that an item is added in the
338 | * front of the queue with the cost passed to this method
339 | *
340 | * @param {number} cost the cost to pasue by
341 | */
342 | pauseByCost(cost) {
343 | this.stopTimer();
344 | this.throttle(cost, false, true);
345 | }
346 |
347 |
348 | /**
349 | * pause the bucket for n seconds. means that an item with the cost for one
350 | * second is added at the beginning of the queue
351 | *
352 | * @param {number} seconds the number of seconds to pause the bucket by
353 | */
354 | pause(seconds = 1) {
355 | this.drain();
356 | this.stopTimer();
357 | const cost = this.refillRate * seconds;
358 | this.pauseByCost(cost);
359 | }
360 |
361 |
362 |
363 | /**
364 | * drains the bucket, so that nothing can be exuted at the moment
365 | *
366 | * @private
367 | */
368 | drain() {
369 | this.currentCapacity = 0;
370 | this.lastRefill = Date.now();
371 | }
372 |
373 |
374 |
375 | /**
376 | * set the timeout value for the bucket. this is the amount of time no item
377 | * may longer wait for.
378 | *
379 | * @param {number} timeout in seonds
380 | */
381 | setTimeout(timeout) {
382 | this.timeout = timeout;
383 | this.updateVariables();
384 | return this;
385 | }
386 |
387 |
388 | /**
389 | * set the interval within whch the capacity can be used
390 | *
391 | * @param {number} interval in seonds
392 | */
393 | setInterval(interval) {
394 | this.interval = interval;
395 | this.updateVariables();
396 | return this;
397 | }
398 |
399 |
400 | /**
401 | * set the capacity of the bucket. this si the capacity that can be used per interval
402 | *
403 | * @param {number} capacity
404 | */
405 | setCapacity(capacity) {
406 | this.capacity = capacity;
407 | this.updateVariables();
408 | return this;
409 | }
410 |
411 |
412 |
413 | /**
414 | * claculates the values of some frequently used variables on the bucket
415 | *
416 | * @private
417 | */
418 | updateVariables() {
419 | // take one as default for each variable since this method may be called
420 | // before every variable was set
421 | this.maxCapacity = ((this.timeout || 1) / (this.interval || 1)) * (this.capacity || 1);
422 |
423 | // the rate, at which the leaky bucket is filled per second
424 | this.refillRate = (this.capacity || 1) / (this.interval || 1);
425 |
426 | }
427 | }
428 |
429 | module.exports = LeakyBucket;
--------------------------------------------------------------------------------