;(function(Agent){

  var patchViewChanges = _.debounce(function(view, prop, action, difference, oldValue) {
    Agent.lazyWorker.push({
      context: Agent,
      args: [view],
      callback: function(view) {
        Agent.sendAppComponentReport('view:change', {
          cid: view.cid,
          data: Agent.serializeView(view)
        });
      }
    });
  }, 200);

  // @private
  Agent.patchBackboneView = function(BackboneView) {
    debug.log('Backbone.View detected');

    Agent.patchBackboneComponent(BackboneView, function(view) { // on new instance
      Agent.lazyWorker.push({
        context: Agent,
        args: [view],
        callback: function(view) {
          // registra il nuovo componente dell'app
          var data = Agent.serializeView(view)
          Agent.registerAppComponent('View', view, data);
          Agent.sendAppComponentReport('view:change', {
            data: data,
            cid: view.cid
          });
        }
      });

      // Patcha i metodi del componente dell'app
      Agent.patchAppComponentTrigger(view, 'view');

      Agent.onDefined(view, 'ui', function() {
        Agent.onChange(view.ui, _.partial(patchViewChanges, view));
        patchViewChanges(view);
      });

      Agent.onDefined(view, '_events', function() {
        Agent.onChange(view._events, _.partial(patchViewChanges, view));
        patchViewChanges(view);
      });
    });
  };

}(Agent));

;(function(Agent){

  var patchViewChanges = _.debounce(function(view, prop, action, difference, oldValue) {
    Agent.lazyWorker.push({
      context: Agent,
      args: [view],
      callback: function(view) {
        Agent.sendAppComponentReport('view:change', {
          cid: view.cid,
          data: Agent.serializeView(view)
        });
      }
    });
  }, 200);

  // @private
  Agent.patchBackboneView = function(BackboneView) {
    debug.log('Backbone.View detected');

    Agent.patchBackboneComponent(BackboneView, function(view) { // on new instance
      Agent.lazyWorker.push({
        context: Agent,
        args: [view],
        callback: function(view) {
          // registra il nuovo componente dell'app
          var data = Agent.serializeView(view)
          Agent.registerAppComponent('View', view, data);
          Agent.sendAppComponentReport('view:change', {
            data: data,
            cid: view.cid
          });
        }
      });

      // set class name if we can
      if (view._requirePath) {
        view._className = _.last(view._requirePath.split('/'));
      }

      // Patcha i metodi del componente dell'app
      Agent.patchAppComponentTrigger(view, 'view');

      Agent.onDefined(view, 'ui', function() {
        Agent.onChange(view.ui, _.partial(patchViewChanges, view));
        patchViewChanges(view);
      });

      Agent.onDefined(view, '_events', function() {
        Agent.onChange(view._events, _.partial(patchViewChanges, view));
        patchViewChanges(view);
      });
    });
  };

}(Agent));

Add class names to views when a require path is available


/**
 * @constructor
 * @param {boolean} webfontSupport
 * @param {boolean} webKitFallbackBug
 */
webfont.BrowserInfo = function (webfontSupport, webKitFallbackBug, webKitMetricsBug) {
  this.webfontSupport_ = webfontSupport;
  this.webKitFallbackBug_ = webKitFallbackBug;
  this.webKitMetricsBug_ = webKitMetricsBug;
};

/**
 * @return {boolean}
 */
webfont.BrowserInfo.prototype.hasWebFontSupport = function () {
  return this.webfontSupport_;
};

/**
 * @return {boolean}
 */
webfont.BrowserInfo.prototype.hasWebKitFallbackBug = function () {
  return this.webKitFallbackBug_;
};

/**
 * @return {boolean}
 */
webfont.BrowserInfo.prototype.hasWebKitMetricsBug = function () {
  return this.webKitMetricsBug_;
};

/**
 * @constructor
 * @param {boolean} webfontSupport
 * @param {boolean} webKitFallbackBug
 * @param {boolean} webKitMetricsBug
 */
webfont.BrowserInfo = function (webfontSupport, webKitFallbackBug, webKitMetricsBug) {
  this.webfontSupport_ = webfontSupport;
  this.webKitFallbackBug_ = webKitFallbackBug;
  this.webKitMetricsBug_ = webKitMetricsBug;
};

/**
 * Returns true if the browser supports web fonts.
 *
 * @return {boolean}
 */
webfont.BrowserInfo.prototype.hasWebFontSupport = function () {
  return this.webfontSupport_;
};

/**
 * Returns true if the browser has the WebKit fallback bug.
 *
 * The bug causes the normal CSS font stack to be ignored while
 * loading web fonts. Instead it picks the generic font family
 * (or the default generic font family) of the first instance
 * the web font is mentioned in CSS. It switches to this font
 * immediately while loading web font, causing two changes in
 * font to occur (compared to other browsers which only change
 * font once the web font has loaded.)
 *
 * The bug has been fixed and is only happens in WebKit versions
 * below 536.11. Even though it is fixed we still have a large
 * percentage of users on older WebKit versions, mostly on mobile
 * platforms.
 *
 * Also see: https://bugs.webkit.org/show_bug.cgi?id=76684
 *
 * @return {boolean}
 */
webfont.BrowserInfo.prototype.hasWebKitFallbackBug = function () {
  return this.webKitFallbackBug_;
};

/**
 * Returns true if the browser has the WebKit metrics bug
 *
 * The metrics bug causes WebKit to change the height of a font
 * while loading a web font. Other browsers do not modify
 * the width or height of the fallback font while a web font is
 * loading. This caused our width and height check to be incorrect,
 * triggering a false positive.
 *
 * Also see: https://bugs.webkit.org/show_bug.cgi?id=110977
 *
 * @return {boolean}
 */
webfont.BrowserInfo.prototype.hasWebKitMetricsBug = function () {
  return this.webKitMetricsBug_;
};

Add information about bugs to BrowserInfo.


define(['module', 'jquery', 'underscore', 'lib/sync', 'dataseed/views/dataset'], function(module, $, _, sync, DatasetEmbedView) {
    'use strict';

    // Override Backbone sync
    Backbone.sync = sync;

    // Get base URL
    var config = module.config(),
        base_url = (_.isUndefined(config['BASE_URL'])) ? 'http://dataseedapp.com' : config['BASE_URL'];

    // Setup external API access
    var _ajax = $.ajax;
    $.extend({
        ajax: function(options) {
            options['url'] = base_url + options['url'];
            return _ajax.call(this, options);
        }
    });
});

define(['module', 'jquery', 'underscore', 'lib/sync', 'dataseed/views/dataset'], function(module, $, _, sync, DatasetEmbedView) {
    'use strict';

    // Override Backbone sync
    Backbone.sync = sync;

    // Get base URL
    var config = module.config(),
        base_url = (_.isUndefined(config['BASE_URL'])) ? 'https://getdataseed.com' : config['BASE_URL'];

    // Setup external API access
    var _ajax = $.ajax;
    $.extend({
        ajax: function(options) {
            options['url'] = base_url + options['url'];
            return _ajax.call(this, options);
        }
    });
});

Update to new dataseed URL


'use strict';

var path            = require('path')
  , common          = require('path2/common')
  , defaultReadFile = require('fs2/read-file')
  , cssAid          = require('css-aid')

  , dirname = path.dirname, resolve = path.resolve;

module.exports = function (indexPath, readFile) {
	var rootPath, indexDir;
	if (!readFile) readFile = defaultReadFile;
	indexPath = resolve(indexPath);
	indexDir = dirname(indexPath);
	return readFile(indexPath, 'utf8')(function (content) {
		var filenames = content.trim().split('\n').filter(Boolean).map(function (name) {
			return resolve(indexDir, name);
		});
		rootPath = common.apply(null, filenames);
		return filenames;
	}).map(function (filename) {
		return readFile(filename, 'utf8')(function (content) {
			return {
				filename: filename.slice(rootPath.length + 1),
				content: content
			};
		});
	})(function (data) {
		return cssAid(data.reduce(function (content, data) {
			return content + '/* ' + data.filename + ' */\n\n' + data.content + '\n';
		}, ''));
	});
};

'use strict';

var path            = require('path')
  , common          = require('path2/common')
  , defaultReadFile = require('fs2/read-file')
  , cssAid          = require('css-aid')

  , dirname = path.dirname, resolve = path.resolve;

module.exports = function (indexPath, readFile) {
	var rootPath, indexDir;
	if (!readFile) readFile = defaultReadFile;
	indexPath = resolve(indexPath);
	indexDir = dirname(indexPath);
	return readFile(indexPath, 'utf8')(function (content) {
		var filenames = content.trim().split('\n').filter(Boolean).map(function (name) {
			return resolve(indexDir, name);
		});
		rootPath = common.apply(null, filenames);
		return filenames;
	}).map(function (filename) {
		return readFile(filename, 'utf8')(function (content) {
			return {
				filename: filename.slice(rootPath.length + 1),
				content: content
			};
		});
	})(function (data) {
		return cssAid(data.reduce(function (content, data) {
			return content + '/* ' + data.filename + ' */\n\n' + data.content + '\n';
		}, '').slice(0, -1));
	});
};

Trim last added new line


import won from "../../app/service/won.js";

import { complain } from "./uc-complain.js";
import { handleComplaint } from "./uc-handle-complaint.js";
import { customUseCase } from "./uc-custom.js";

import { details, mergeInEmptyDraft } from "../detail-definitions.js";

export const otherGroup = {
  identifier: "othergroup",
  label: "More...",
  icon: undefined,
  useCases: {
    complain: complain,
    handleComplaint: handleComplaint,
    customUseCase: customUseCase,
    groupChat: {
      identifier: "groupChat",
      label: "New Groupchat Post",
      draft: {
        ...mergeInEmptyDraft({
          content: {
            facets: [
              { "@id": "#groupFacet", "@type": won.WON.GroupFacet },
              { "@id": "#holdableFacet", "@type": won.WON.HoldableFacet },
            ],
            defaultFacet: { "@id": "#groupFacet", "@type": won.WON.GroupFacet },
          },
        }),
      },
      details: details,
      seeksDetails: details,
    },
  },
};

import won from "../../app/service/won.js";

import { complain } from "./uc-complain.js";
import { handleComplaint } from "./uc-handle-complaint.js";
import { customUseCase } from "./uc-custom.js";

import { details, mergeInEmptyDraft } from "../detail-definitions.js";

export const otherGroup = {
  identifier: "othergroup",
  label: "More...",
  icon: undefined,
  useCases: {
    complain: complain,
    handleComplaint: handleComplaint,
    customUseCase: customUseCase,
    groupChat: {
      identifier: "groupChat",
      label: "New Groupchat Post",
      draft: {
        ...mergeInEmptyDraft({
          content: {
            facets: {
              "#groupFacet": won.WON.GroupFacet,
              "#holdableFacet": won.WON.HoldableFacet,
            },
            defaultFacet: { "#groupFacet": won.WON.GroupFacet },
          },
        }),
      },
      details: details,
      seeksDetails: details,
    },
  },
};

Change structure of facets and defaultFacet within the useCase-groupChat draft


import { addParameters, configure } from '@storybook/angular';

function loadStories() {
  // Import design stories
  require('../src/design');

  // automatically import all files ending in *.stories.ts
  const req = require.context('../src/app', true, /.stories.ts$/);
  req.keys().forEach(filename => req(filename));
}

addParameters({
  options: {
    storySort: (a, b) => a[1].id.localeCompare(b[1].id)
  }
});

configure(loadStories, module);

import { addParameters, configure } from '@storybook/angular';

function loadStories() {
  // Import design stories
  require('../src/design');

  // automatically import all files ending in *.stories.ts
  const req = require.context('../src/app', true, /.stories.ts$/);
  req.keys().forEach(filename => req(filename));
}

const defaultRoot = 'Components';
const guidelinesRoot = 'Guidelines';

function sortFn(a, b) {
  const aRep = a[1];
  const bRep = b[1];

  // Rename kind to contain global root
  if (!aRep.kind.includes('|')) {
    aRep.kind = `${defaultRoot}|${aRep.kind}`;
  }

  if (!bRep.kind.includes('|')) {
    bRep.kind = `${defaultRoot}|${bRep.kind}`;
  }

  if (
    aRep.kind.includes(guidelinesRoot) &&
    !bRep.kind.includes(guidelinesRoot)
  ) {
    return -1;
  }

  return aRep.id.localeCompare(bRep.id);
}

addParameters({ options: { storySort: sortFn } });

configure(loadStories, module);

Improve sorting of storybook pages


/* =============================================================
 * tabby.js v1.0
 * Simple, mobile-first toggle tabs.
 * Script by Chris Ferdinandi - http://gomakethings.com
 * Licensed under WTFPL - http://www.wtfpl.net
 * ============================================================= */

$(function () {
    $('.tabs a').click(function(e) {
        e.preventDefault(); // Prevent default link behavior.
        var tabID = $(this).attr('href'); // Pull the href value as the tabID.

        $(this).addClass('active').parent().addClass('active'); // Add the .active class to the link and it's parent li (if one exists).
        $(this).siblings().removeClass('active'); // Remove the .active class from sibling tab navigation elements.
        $(this).parent('li').siblings().removeClass('active').children().removeClass('active'); // Remove the .active class from sibling li elements and their links.
        $(tabID).addClass('active'); // Add the .active class to the div with the tab content.
        $(tabID).siblings().removeClass('active'); // Remove the .active class from other tab content divs.
    });
});

/* =============================================================
 * js-accessibility.js v1.0.0
 * Adds .js class to <body> for progressive enhancement.
 * Script by Chris Ferdinandi - http://gomakethings.com
 * Licensed under WTFPL - http://www.wtfpl.net
 * ============================================================= */

$(function () {
    $('body').addClass('js'); // On page load, add the .js class to the <body> element.
});

/* =============================================================
 * tabby.js
 * Simple, mobile-first toggle tabs.
 * Script by Chris Ferdinandi - http://gomakethings.com
 * Licensed under WTFPL - http://www.wtfpl.net
 * ============================================================= */

(function($) {
    $(function () {
        $('.tabs a').click(function(e) {
            e.preventDefault(); // Prevent default link behavior.
            var tabID = $(this).attr('href'); // Pull the href value as the tabID.

            $(this).addClass('active').parent().addClass('active'); // Add the .active class to the link and it's parent li (if one exists).
            $(this).siblings().removeClass('active'); // Remove the .active class from sibling tab navigation elements.
            $(this).parent('li').siblings().removeClass('active').children().removeClass('active'); // Remove the .active class from sibling li elements and their links.
            $(tabID).addClass('active'); // Add the .active class to the div with the tab content.
            $(tabID).siblings().removeClass('active'); // Remove the .active class from other tab content divs.
        });
    });
})(jQuery);

/* =============================================================
 * js-accessibility.js
 * Adds .js class to <body> for progressive enhancement.
 * Script by Chris Ferdinandi - http://gomakethings.com
 * Licensed under WTFPL - http://www.wtfpl.net
 * ============================================================= */

(function($) {
    $(function () {
        $('body').addClass('js'); // On page load, add the .js class to the <body> element.
    });
})(jQuery);

Set jQuery to noConflict mode for better compatability


/**
 * KarmaConfig
 * @description Configure the karma test runner.
 */

var config = require("./Config"),
	javascriptSources = config.JavaScripts.paths.devDir + "/**/*.js";

module.exports = function (karmaConfig) {
	"use strict";

	karmaConfig.set({
		basePath: "../",

		singleRun: true,
		colors: true,
		captureTimeout: 7000,
		logLevel: config.LOG_INFO,

		frameworks: ["jasmine", "requirejs"],
		plugins: ["karma-jasmine", "karma-requirejs", "karma-coverage", "karma-chrome-launcher", "karma-firefox-launcher", "karma-safari-launcher", "karma-phantomjs-launcher"],
		reporters: ["progress", "coverage"],

		preprocessors: {
			javascriptSources: "coverage",
		},

		coverageReporter: {
			reporters: [{
				type: "text-summary",
				dir: config.karma.coverageDir
			}, {
				type: "html",
				dir: config.karma.coverageDir
			}]
		},

		// List of files to load in the browser
		files: [{
			pattern: javascriptSources,
			included: false
		}, {
			pattern: config.karma.testsDir + "/Specs/**/*.js",
			included: false
		}, {
			pattern: config.karma.testsDir + "/SpecList.js",
			included: false
		}, config.karma.testsDir + "/Test-App.js"]
	});
};

/**
 * KarmaConfig
 * @description Configure the karma test runner.
 */

var config = require("./Config"),
	javascriptSources = config.JavaScripts.paths.devDir + "/**/*.js";

module.exports = function (karmaConfig) {
	"use strict";

	karmaConfig.set({
		basePath: "../",

		singleRun: true,
		colors: true,
		captureTimeout: 12000,
		logLevel: config.LOG_INFO,
		autoWatch: false,

		frameworks: ["jasmine", "requirejs"],
		plugins: ["karma-jasmine", "karma-requirejs", "karma-coverage", "karma-chrome-launcher", "karma-firefox-launcher", "karma-safari-launcher", "karma-phantomjs-launcher"],
		reporters: ["progress", "coverage"],

		preprocessors: {
			"**/Resources/Public/Javascripts/Sources/**/*.js": "coverage",
		},

		coverageReporter: {
			reporters: [{
				type: "text-summary",
				dir: config.karma.coverageDir
			}, {
				type: "html",
				dir: config.karma.coverageDir
			}]
		},

		// List of files to load in the browser "Resources/Public/Javascripts/Tests
		files: [{
			pattern: "Resources/Public/Javascripts/Sources/**/*.js",
			included: false
		}, {
			pattern: "Resources/Public/Javascripts/Tests/Specs/**/*.js",
			included: false
		}, {
			pattern: "Resources/Public/Javascripts/Tests/SpecList.js",
			included: false
		}, "Resources/Public/Javascripts/Tests/Test-App.js"]
	});
};

Set hard coded paths and increase the 'captureTimeout' for karma tests


'use strict';

let experiments = {};

Object.freeze(experiments);

module.exports = experiments;

'use strict';
const symbol = require('../utilities/symbol');

let experiments = {
  MODULE_UNIFICATION: symbol('module-unification'),
};

Object.freeze(experiments);

module.exports = experiments;

Revert "Disable in-progress experiments for beta."


const path = require('path');

function searchAssets(
  assets = [],
  extensions = []
) {
  return Object.keys(assets).map(
    name => {
      var nameCleaned = name;
      if (/\.(css\?).*$/.test(name)) {
        nameCleaned = name.substr(0, name.lastIndexOf('?')); // ignore hash on file like style.css?7ec000f0d0d347
      }
      extensions.indexOf(path.extname(nameCleaned)) >= 0 && { name, asset: assets[name] }
    }
  ).filter(a => a);
}

function searchFiles(
  modules = {},
  extensions = [],
  getter = a => a
) {
  return Object.keys(modules).map((name) => {
    const file = getter(modules[name]);

    if (!file) {
      return null;
    }

    return extensions.indexOf(path.extname(file)) >= 0 && file;
  }).filter(a => a);
}

module.exports = {
  assets: searchAssets,
  files: searchFiles
};

const path = require('path');

function searchAssets(
  assets = [],
  extensions = []
) {
  return Object.keys(assets).map(
    name => {
      var nameCleaned = name;
      if (/\.(css\?).*$/.test(name)) {
        nameCleaned = name.substr(0, name.lastIndexOf('?')); // ignore hash on file like style.css?7ec000f0d0d347
      }
      return extensions.indexOf(path.extname(nameCleaned)) >= 0 && { name, asset: assets[name] }
    }
  ).filter(a => a);
}

function searchFiles(
  modules = {},
  extensions = [],
  getter = a => a
) {
  return Object.keys(modules).map((name) => {
    const file = getter(modules[name]);

    if (!file) {
      return null;
    }

    return extensions.indexOf(path.extname(file)) >= 0 && file;
  }).filter(a => a);
}

module.exports = {
  assets: searchAssets,
  files: searchFiles
};

Fix CI build, return value so test passes.


import Prop from 'props/prop';
import canvas from 'canvas';

const coords = {
  northEast: {
    x: 1,
    y: -1,
  },
  southEast: {
    x: 1,
    y: 1,
  },
  southWest: {
    x: -1,
    y: 1,
  },
  northWest: {
    x: -1,
    y: -1,
  },
};

export default class Ball extends Prop {
  constructor() {
    const width = 10;
    const height = 10;
    const x = (canvas.width / 2) - (width / 2);
    const y = (canvas.height / 2) - (height / 2);

    super(x, y, width, height);
  }

  isOutOfBounds() {
    // This will be moved into a collision detection lib at some point.
    return (
      this.positionY === canvas.height ||
      this.positionY === 0 ||
      this.positionX === canvas.width ||
      this.positionX === 0
    );
  }

  fire() {
    const direction = coords.northWest;

    const move = setInterval(() => {
      this.move(direction.x, direction.y);
      if (this.isOutOfBounds()) {
        console.log('Stopping ball, out of bounds');
        clearInterval(move);
      }
    }, 10);
  }
}

import Prop from 'props/prop';
import canvas from 'canvas';

const coords = {
  northEast: {
    x: 1,
    y: -1,
  },
  southEast: {
    x: 1,
    y: 1,
  },
  southWest: {
    x: -1,
    y: 1,
  },
  northWest: {
    x: -1,
    y: -1,
  },
};

export default class Ball extends Prop {
  constructor() {
    const width = 10;
    const height = 10;
    const x = (canvas.width / 2) - (width / 2);
    const y = (canvas.height / 2) - (height / 2);

    super(x, y, width, height);
  }

  isOutOfBounds() {
    // This will be moved into a collision detection lib at some point.
    return (
      this.positionY === canvas.height ||
      this.positionY === 0 ||
      this.positionX === canvas.width ||
      this.positionX === 0
    );
  }

  fire() {
    const direction = coords.northWest;
    const move = () => {
      this.move(direction.x, direction.y);

      if (!this.isOutOfBounds()) {
        return requestAnimationFrame(move);
      }
    };

    requestAnimationFrame(move);
  }
}

Replace clearInterval with RequestAnimationFrame for performance


class Timing
{
    /**
     * debounces functions invoked by events
     *
     * @param {function} func       function to be invoked
     * @param {number} wait         time to wait before func is invoked
     * @returns {function(...[*]=)}
     */
    static debounce (func, wait = 80)
    {
        let timeoutId = null;

        return (...args) =>
        {
            const later = () =>
            {
                timeoutId = null;
                func(...args);
            };

            clearTimeout(timeoutId);

            timeoutId = window.setTimeout(later, wait);
        };
    }
}

export default Timing;

/**
 * Debounces functions that are frequently invoked
 *
 * @param {function} func       function to be invoked
 * @param {number} delay         time to wait before func is invoked
 * @returns {function(...[*]=)}
 */
export function debounce (func, delay = 80)
{
    let timeoutId = null;

    return (...args) =>
    {
        const later = () =>
        {
            timeoutId = null;
            func(...args);
        };

        window.clearTimeout(timeoutId);

        timeoutId = window.setTimeout(later, delay);
    };
}

Refactor and properly export debounce function


'use strict';

var express = require('express');
var fs = require('fs');
var path = require('path');

var app = express();

var avatar = require('avatar-generator')({
  order: 'background face clothes head hair eye mouth'.split(' '),
  images: path.join(__dirname, 'node_modules/avatar-generator/img'),
  convert: 'convert-image'
});

avatar('test@example.com', 'male', 200)
  .write('./test.jpg', function (err) {
    if (err) {
      console.log(err);
    }
  });

app.get('/:id', function(req, res) {
  res.header("Content-Type", "image/jpeg");
  fs.readFile('test.jpg', function (err,data) {
    if (err) {
      console.log(err);
      return;
    }
    res.send(data);
  });
});

app.listen(3000);

'use strict';

var express = require('express');
var fs = require('fs');
var path = require('path');

var app = express();

var avatar = require('avatar-generator')({
  order: 'background face clothes head hair eye mouth'.split(' '),
  images: path.join(__dirname, 'node_modules/avatar-generator/img'),
  convert: 'convert-image'
});

var avatarGenerator = new Promise(function(resolve) {
  avatar('test@example.com', 'male', 200)
  .write('./test.jpg', function (err) {
    if (err) {
      console.log(err);
    }
  });
  resolve();
});

app.get('/:id', function(req, res) {
  res.header("Content-Type", "image/jpeg");
  avatarGenerator.then(function() {
    fs.readFile('test.jpg', function (err,data) {
      if (err) {
        console.log(err);
        return;
      }
      res.send(data);
    });
  });
});

app.listen(3000);

Use the promise to asynchronous call the avatar generator


'use strict';

const hapi = require('hapi');
const oauth = require('./lib/oauth.js');

const server = new hapi.Server();
server.connection({
    host: 'localhost',
    port: 3000
});

server.route({
    method: 'GET',
    path: '/fitbit-authorization',
    handler: function (request, reply) {
        // Handle implicit grant flow
        if (request.query.access_token === undefined) {
            console.log('No accesss token detected.');
            console.log('Sending user through Fitbit authorization.');

            reply.redirect(oauth.getImplicitAccessToken(config.fitbit));
        } else if (request.query.access_token !== undefined) {
            let accessToken = request.query.access_token;
            console.log(`Access token found: ${accessToken}`);
        }
    }
});

// Start server
server.start((error) => {
    if (error) {
        throw error;
    }

    console.log(`Server running at: ${server.info.uri}`);
});

'use strict';

const hapi = require('hapi');
const oauth = require('./lib/oauth.js');
const config = require('./config.js');

const server = new hapi.Server();
server.connection({
    host: 'localhost',
    port: 3000
});

server.route({
    method: 'GET',
    path: '/fitbit-authorization',
    handler: function (request, reply) {
        // Handle implicit grant flow
        if (request.query.access_token === undefined) {
            console.log('No accesss token detected.');
            console.log('Sending user through Fitbit authorization.');

            let authorizationUrl = oauth.getFitbitImplicitAccessToken(config.appCredentials.dev.fitbit);
            reply('Redirecting to Fitbit authorization...').redirect(authorizationUrl);
        } else if (request.query.access_token !== undefined) {
            let accessToken = request.query.access_token;
            console.log(`Access token found: ${accessToken}`);
        }
    }
});

server.route({
    method: 'GET',
    path: '/slack-authorization',
    handler: function (request, reply) {

    }
})

// Start server
server.start((error) => {
    if (error) {
        throw error;
    }

    console.log(`Server running at: ${server.info.uri}`);
});

Add route for Slack authorization


#!/usr/bin/env node

(function() {
  'use strict';

  var die = function(msg) {
    console.error(msg);
    process.exit(1);
  };

  var ccs = require('closure-compiler-service');
  var fs = require('fs');

  // write output to console if called from command line
  if (process.argv.length === 3) {
    var filename = process.argv[2];

    var js_code = fs.readFile(filename, function(err, buf) {
      if (err) { die(err); }

      ccs.compile(buf, function(errs, code) {
        if (errs) { die(errs); }
        console.log(code);
      });
    });
  }
})();

#!/usr/bin/env node

(function() {
  'use strict';

  var ccs = require('closure-compiler-service');
  var fs  = require('fs');
  var pkg = require('./package.json');

  var exit = function(msg) {
    console.log(msg);
    process.exit(0);
  }

  var die = function(msg) {
    console.error(msg);
    process.exit(1);
  };

  // write output to console if called from command line
  if (process.argv.length === 3) {
    var arg = process.argv[2];

    if (arg === '-v' || arg === '--version') {
      exit(pkg.name + ' ' + pkg.version);
    }

    if (arg === '-h' || arg === '--help') {
      exit('See ' + pkg.homepage);
    }

    var js_code = fs.readFile(arg, function(err, buf) {
      if (err) { die(err); }

      ccs.compile(buf, function(errs, code) {
        if (errs) { die(errs); }
        console.log(code);
      });
    });
  }
})();

Add --help and --version arguments


#! /usr/bin/env node
/* Copyright (c) 2018 Looker Data Sciences, Inc. See https://github.com/looker-open-source/look-at-me-sideways/blob/master/LICENSE.txt */
const minimist = require('minimist')
const lams = require('./index.js')
const fromEntries = require('fromentries')
const cliArgs = fromEntries( // ponyfill for Object.fromEntries
	Object.entries(
		minimist(
			process.argv.slice(
				process.argv[0]=='lams'
					? 1 // e.g. lams --bla
					: 2 // e.g. node index.js --bla
			),
			{
				alias:{
					source: ["input", "i"]
				}
			}
		)
	)
	//Convert kebab-case and snake_case to camelCase
	.map(([k,v])=>[k.replace(/[-_][a-zA-Z-0-9]/g,s=>s.toUpperCase()),v])
)

!async function() {
	await lams(cliArgs)
}()

#! /usr/bin/env node
/* Copyright (c) 2018 Looker Data Sciences, Inc. See https://github.com/looker-open-source/look-at-me-sideways/blob/master/LICENSE.txt */
const minimist = require('minimist')
const lams = require('./index.js')
const fromEntries = require('fromentries')
const cliArgs = fromEntries( // ponyfill for Object.fromEntries
	Object.entries(
		minimist(
			process.argv.slice(
				process.argv[0]=='lams'
					? 1 // e.g. lams --bla
					: 2 // e.g. node index.js --bla
			),
			{
				alias:{
					source: ["input", "i"]
				}
			}
		)
	)
	//Convert kebab-case and snake_case to camelCase
	.map(([k,v])=>[k.replace(/[-_][a-zA-Z-0-9]/g,s=>s.slice(1).toUpperCase()),v])
)

!async function() {
	await lams(cliArgs)
}()

Fix kebab-to-camel conversion for CLI arguments


#!/usr/bin/env node
'use strict';

var pkg = require('./package.json');
var iconmartGlyphiconsCrawler = require('./');
var argv = process.argv.slice(2);

function help() {
  console.log([
    '',
      '  ' + pkg.description,
    '',
    '  Example',
    '    iconmart-glyphicons-crawler ',
    ''
  ].join('\n'));
}

if (argv.indexOf('--help') !== -1) {
  help();
  return;
}

if (argv.indexOf('--version') !== -1) {
  console.log(pkg.version);
  return;
}

iconmartGlyphiconsCrawler(argv[0], function(){

});

#!/usr/bin/env node
'use strict';

var pkg = require('./package.json');
var iconmartGlyphiconsCrawler = require('./');
var argv = process.argv.slice(2);

function help() {
  console.log([
    '',
      '  ' + pkg.description,
    '',
    '  Example',
    '    iconmart-glyphicons-crawler ',
    ''
  ].join('\n'));
}

if (argv.indexOf('--help') !== -1) {
  help();
  return;
}

if (argv.indexOf('--version') !== -1) {
  console.log(pkg.version);
  return;
}

iconmartGlyphiconsCrawler.run(argv[0], function(){

});

Adjust CLI script to use .run() method


"use strict";

module.exports = {
  defaults: {
    html: "<html>\n  <head>\n    <title>Page Title</title>\n  </head>\n  <body>\n    <!-- Put your page markup here -->\n  </body>\n</html>",
    css: "",
    javascript: ""
  }
}

"use strict";

module.exports = {
  defaults: {
    html: "<!doctype html>\n<html>\n  <head>\n    <title>Page Title</title>\n  </head>\n  <body>\n    <!-- Put your page markup here -->\n  </body>\n</html>",
    css: "",
    javascript: ""
  }
}

Add a doctype to the default HTML


import React from 'react';
import PropTypes from 'prop-types';

export default class SearchBox extends React.PureComponent {
  static propTypes = {
    value: PropTypes.string.isRequired,
    onValueChanged: PropTypes.func
  }

  static defaultProps = {
    value: ''
  }

  handleBlur = () => {
    this.input.focus();
  }

  handleChange = (event) => {
    const { onValueChanged } = this.props;
    if (onValueChanged) onValueChanged(event.target.value);
  }

  render = () => {
    return (
      <div className='search-field'>
        <input
          type='text'
          value={this.props.value}
          ref={(input) => { this.input = input; }}
          autoFocus={true}
          onBlur={this.handleBlur}
          onChange={this.handleChange} />
      </div>
    );
  }
}

import React from 'react';
import PropTypes from 'prop-types';

export default class SearchBox extends React.PureComponent {
  static propTypes = {
    value: PropTypes.string.isRequired,
    onValueChanged: PropTypes.func
  }

  static defaultProps = {
    value: ''
  }

  componentDidMount = () => {
    const index = this.props.value.length;
    this.input.setSelectionRange(index, index);
  }

  handleBlur = () => {
    this.input.focus();
  }

  handleChange = (event) => {
    const { onValueChanged } = this.props;
    if (onValueChanged) onValueChanged(event.target.value);
  }

  render = () => {
    return (
      <div className='search-field'>
        <input
          type='text'
          value={this.props.value}
          ref={(input) => { this.input = input; }}
          autoFocus={true}
          onBlur={this.handleBlur}
          onChange={this.handleChange} />
      </div>
    );
  }
}

Fix caret position after first launch


// These URL paths will be transformed to CN mirrors.
var mirrors = {
  "//developers.google.com"           : "//developers.google.cn",
  "//firebase.google.com"             : "//firebase.google.cn",
  "//developer.android.com"           : "//developer.android.google.cn",
  "//angular.io"                      : "//angular.cn",
  "//maps.google.com"                 : "//maps.google.cn",
  "google.com/maps"                   : "google.cn/maps",
  "//translate.google.com"            : "//translate.google.cn",
  "//codelabs.developers.google.com"  : "//code-labs.cn",
  "//code-labs.io"                    : "//code-labs.cn"
}

// These URL paths are not available on CN mirrors, therefore won't be transformed.
var whitelist = [
  "//developers.google.com/groups",
  "//developers.google.com/events",
  "//firebase.google.com/support/contact/",
]

function mirrorUrl(url) {
  // Check for whitelisting.
  for (var key in whitelist) {
    if (url.includes(whitelist[key])) {
      return url;
    }
  }

  // Check for mapping.
  for (var key in mirrors) {
    if (url.includes(key)) {
      url = url.replace(key, mirrors[key]);
      break;
    }
  }
  return url;
}

// These URL paths will be transformed to CN mirrors.
var mirrors = {
  "//developers.google.com"           : "//developers.google.cn",
  "//firebase.google.com"             : "//firebase.google.cn",
  "//developer.android.com"           : "//developer.android.google.cn",
  "//angular.io"                      : "//angular.cn",
  "//maps.google.com"                 : "//maps.google.cn",
  "//ditu.google.com"                 : "//ditu.google.cn",
  "google.com/maps"                   : "google.cn/maps",
  "//translate.google.com"            : "//translate.google.cn",
  "//codelabs.developers.google.com"  : "//code-labs.cn",
  "//code-labs.io"                    : "//code-labs.cn"
}

// These URL paths are not available on CN mirrors, therefore won't be transformed.
var whitelist = [
  "//developers.google.com/groups",
  "//developers.google.com/events",
  "//firebase.google.com/support/contact/",
]

function mirrorUrl(url) {
  // Check for whitelisting.
  for (var key in whitelist) {
    if (url.includes(whitelist[key])) {
      return url;
    }
  }

  // Check for mapping.
  for (var key in mirrors) {
    if (url.includes(key)) {
      url = url.replace(key, mirrors[key]);
      break;
    }
  }
  return url;
}

Add "ditu.google.com" for google maps


import React from 'react'
import AltContainer from 'alt-container'

import Header from './components/Header'
import Date from './components/Date'
import PoemContainer from './containers/PoemContainer'
import TaskContainer from './containers/TaskContainer'

import DateStore from './stores/DateStore'
import DateActions from './actions/DateActions'

const App = () => {
  return (

      <AltContainer stores={{ store: DateStore }} >

        <Date actions={DateActions} />
      </AltContainer>

              <TaskContainer />

              <PoemContainer />

  )
}

export default App

Add task and poem container's width


var textFrame = null;

// using the proxy pattern http://docs.jquery.com/Types#Proxy%5FPattern to override the addText function

(function () {
    var proxied = window.addText;
    window.addText = function (text) {
        if (textFrame == null) {
            // We can access the original addText function like this. This is required so we can add Frame.htm
            // to the output window in the first place!
            return proxied(text);
        }
        else {
            textFrame.append(text);
            textFrame.animate({ scrollTop: textFrame.height() }, 0);
        }
    };
})();

function beginUsingTextFrame() {
    textFrame = $("#divText");
    setFrameHeight();
    $("body").css("overflow", "hidden");
    $(window).resize(function() {
        setFrameHeight();
    });
}

function setFrameHeight() {
    textFrame.height($(window).height() - textFrame.offset().top - 6);
}

var textFrame = null;

function beginUsingTextFrame() {
    textFrame = $("#divText");
    setFrameHeight();
    $("body").css("overflow", "hidden");

    $(window).resize(function() {
        setFrameHeight();
    });

    window.addText = function(text) {
        textFrame.append(text);
        scrollToEnd();
    }

    window.scrollToEnd = function () {
        textFrame.scrollTop(textFrame.attr("scrollHeight"));
    }
}

function setFrameHeight() {
    textFrame.height($(window).height() - textFrame.offset().top - 6);
}

Tidy up JS and fix scrolling bug


// CARTO account name
cartoUser = 'chekpeds';

// CARTO table names lookup
export const cartoTables = {
  nyc_borough: 'nyc_borough',
  nyc_city_council: 'nyc_city_council',
  nyc_community_board: 'nyc_community_board',
  nyc_neighborhood: 'nyc_neighborhood',
  nyc_nypd_precinct: 'nyc_nypd_precinct',
  nyc_zip_code: 'nyc_zip_code',
  nyc_crashes: 'export2016_07'
};

// basemap for Leaflet
export const basemapURL =
  'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png';

// CARTO account name
export const cartoUser = 'chekpeds';

// CARTO table names lookup
export const cartoTables = {
  nyc_borough: 'nyc_borough',
  nyc_city_council: 'nyc_city_council',
  nyc_community_board: 'nyc_community_board',
  nyc_neighborhood: 'nyc_neighborhood',
  nyc_nypd_precinct: 'nyc_nypd_precinct',
  nyc_zip_code: 'nyc_zip_code',
  nyc_crashes: 'export2016_07'
};

// basemap for Leaflet
export const basemapURL =
  'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png';

Fix typo in app config for cartoUser


import {
  dbGetAllFeedback,
  dbGetFeedback,
  dbCreateFeedback,
  dbUpdateFeedback,
  dbDelFeedback,
} from '../models/feedback';

export const getAllFeedback = (request, reply) => dbGetAllFeedback().then(reply);
export const getFeedback = (request, reply) => dbGetFeedback(request.params.feedbackId).then(reply);
export const createFeedback = (request, reply) => dbCreateFeedback(request.payload).then(reply);

// TODO: make sure user "owns" feedback (i.e. either expertId or teacherId matches userId)
export const updateFeedback = (request, reply) => dbUpdateFeedback(request.params).then(reply);
export const delFeedback = (request, reply) => dbDelFeedback(request.params.feedbackId).then(reply);

import Boom from 'boom';
import {
  dbGetAllFeedback,
  dbGetFeedback,
  dbCreateFeedback,
  dbUpdateFeedback,
  dbDelFeedback,
} from '../models/feedback';

export const getAllFeedback = (request, reply) => dbGetAllFeedback().then(reply);
export const getFeedback = (request, reply) => dbGetFeedback(request.params.feedbackId).then(reply);
export const createFeedback = async (request, reply) => {
  try {
    await dbCreateFeedback({
      text: request.payload.text,
      creatorType: 'teacher', // TODO: for now only teachers supported
      email: request.pre.user.email, // TODO: anonymous feedback?
    });

    reply({
      message: 'Thank you for sending feedback to XPRT.',
    });
  } catch (e) {
    reply(Boom.badImplementation(e));
  }
};

// TODO: make sure user "owns" feedback (i.e. either expertId or teacherId matches userId)
export const updateFeedback = (request, reply) => dbUpdateFeedback(request.params).then(reply);
export const delFeedback = (request, reply) => dbDelFeedback(request.params.feedbackId).then(reply);

Add necessary fields to createFeedback()


describe( 'NOrgCtrl', function() {
  describe( 'N-Org Editing Control', function() {
    var NOrgCtrl, $location, $scope;

    beforeEach( module( 'nOrg' ) );

    beforeEach( inject( function( $controller, _$location_, $rootScope ) {
      $location = _$location_;
      $scope = $rootScope.$new();
      NOrgCtrl = $controller( 'NOrgCtrl', { $location: $location, $scope: $scope });
    }));

    it( 'should pass a dummy test', inject( function() {
      expect( NOrgCtrl ).toBeTruthy();
    }));
  });
});

describe( 'N-Org editing control', function() {
  var NOrgCtrl, $location, $scope;

  beforeEach( module( 'nOrg' ) );

  beforeEach( inject( function( $controller, _$location_, $rootScope ) {
    $location = _$location_;
    $scope = $rootScope.$new();
    NOrgCtrl = $controller( 'NOrgCtrl', { $location: $location, $scope: $scope });
  }));

  it( 'should exist', inject( function() {
    expect( NOrgCtrl ).toBeTruthy();
  }));

  it( 'should provide N-Org nodes', inject( function() {
    expect( $scope.projects.length ).toBeTruthy();
  }));

  // node children
  it( 'nodes may have children ', inject( function() {
    expect( $scope.projects.filter(function(node) {
      return node.children;
    }).length).toBeTruthy();
  }));
  it( 'nodes may not have children ', inject( function() {
    expect( $scope.projects.filter(function(node) {
      return ! node.children;
    }).length).toBeTruthy();
  }));

  // node ids
  it( 'generates an element id for nodes', inject( function() {
    $scope.projects.forEach($scope.sanitizeNode);
    expect( $scope.projects.filter(function(node) {
      return node.id;
    }).length).toEqual($scope.projects.length);
  }));
  it( 'generates valid, CSS select-able ids for nodes', inject( function() {
    $scope.projects.forEach($scope.sanitizeNode);
    expect( $scope.projects.filter(function(node) {
      return (/[<@\.>]/).test(node.id);
    }).length).toBeFalsy();
  }));

  // node headers
  it( 'generates a list of headers to display for nodes', inject( function() {
    $scope.projects.forEach($scope.sanitizeNode);
    expect( $scope.projects.filter(function(node) {
      return typeof node.header_keys != "undefined";
    }).length).toEqual($scope.projects.length);
  }));
  it( 'nodes may have visible headers', inject( function() {
    $scope.projects.forEach($scope.sanitizeNode);
    expect( $scope.projects.filter(function(node) {
      return node.header_keys.length;
    }).length).toBeTruthy();
  }));
  it( 'nodes may not have visible headers', inject( function() {
    $scope.projects.forEach($scope.sanitizeNode);
    expect( $scope.projects.filter(function(node) {
      return ! node.header_keys.length;
    }).length).toBeTruthy();
  }));
});

Add test coverage for existing code


YUI.add("model-list-tweets", function(Y) {
    "use strict";

    var tristis = Y.namespace("Tristis"),
        models  = Y.namespace("Tristis.Models"),

        Tweets;

    Tweets = Y.Base.create("tweets", Y.LazyModelList, [
        Y.namespace("Extensions").ModelListMore
    ], {
        sync : function(action, options, done) {
            var args = {},
                last;

            if(action === "more") {
                last = this.item(this.size() - 1);

                if(last) {
                    args.since_id = last.id_str;
                }
            }

            args = Y.merge(
                { count : 50 },
                this.get("config") || {},
                args
            );

            tristis.twitter.get(this.get("api"), args, done);
        },

        // Sort tweets by date
        comparator : function(model) {
            return Date.parse(model.created_at);
        },

        // Sort tweets by newest first
        _compare : function(a, b) {
            return b - a;
        }
    }, {
        ATTRS : {
            api     : null,
            config  : null
        }
    });

    models.Tweets = Tweets;

}, "@VERSION@", {
    requires : [
        // YUI
        "base-build",
        "lazy-model-list",

        // Extensions
        "extension-model-list-more"
    ]
});

YUI.add("model-list-tweets", function(Y) {
    "use strict";

    var tristis = Y.namespace("Tristis"),
        models  = Y.namespace("Tristis.Models"),

        Tweets;

    Tweets = Y.Base.create("tweets", Y.LazyModelList, [
        Y.namespace("Extensions").ModelListMore
    ], {
        sync : function(action, options, done) {
            var args = {};

            if(action === "more") {
                if(this.size()) {
                    args.since_id = this.item(0).id_str;
                }
            }

            args = Y.merge(
                { count : 50 },
                this.get("config") || {},
                args
            );

            tristis.twitter.get(this.get("api"), args, done);
        },

        // Sort tweets by date
        comparator : function(model) {
            return Date.parse(model.created_at);
        },

        // Sort tweets by newest first
        _compare : function(a, b) {
            return b - a;
        }
    }, {
        ATTRS : {
            api     : null,
            config  : null
        }
    });

    models.Tweets = Tweets;

}, "@VERSION@", {
    requires : [
        // YUI
        "base-build",
        "lazy-model-list",

        // Extensions
        "extension-model-list-more"
    ]
});

Fix since_id when calling Tweets.more()


import EventManager from '../helpers/event-manager';

const createObjectUrl = (src) => {
  const blob = new Blob([src], { type: 'text/javascript' });
  const url = URL.createObjectURL(blob);
  return url;
};

class Thread extends EventManager {
  constructor() {
    super(null);
    this.currentWorker = null;
    this.jobId = null;
    this.active = false;
  }

  setActive(bool) {
    this.active = bool;
  }

  startProcessing() {
    this.setActive(true);
    this.triggerStartProcessing();
  }

  doneProcessing() {
    this.setActive(false);
    this.triggerDoneProcessing(this);
  }

  process(operation, bool) {
    this.startProcessing();
    let worker;
    if (bool || operation.id === this.jobId) {
      worker = this.currentWorker;
    } else {
      worker = this.spawnWorker(operation.src);
      this.jobId = operation.id;
    }

    worker.onmessage = ({ data }) => {
      this.triggerData(operation, data);
      this.doneProcessing();
    };

    worker.onerror = (e) => {
      this.triggerDataError(operation, e);
      this.doneProcessing();
    };

    worker.postMessage(operation.data);
  }

  spawnWorker(src) {
    if (this.currentWorker) {
      this.currentWorker.terminate();
    }

    const url = createObjectUrl(src);
    const worker = new Worker(url);

    return this.currentWorker = worker;
  }
}

export default Thread;

import EventManager from '../helpers/event-manager';

const createObjectUrl = (src) => {
  const blob = new Blob([src], { type: 'text/javascript' });
  const url = URL.createObjectURL(blob);
  return url;
};

class Thread extends EventManager {
  constructor() {
    super(null);
    this.currentWorker = null;
    this.jobId = null;
    this.active = false;
  }

  setActive(bool) {
    this.active = bool;
  }

  startProcessing() {
    this.setActive(true);
    this.triggerStartProcessing();
  }

  doneProcessing() {
    this.setActive(false);
    this.triggerDoneProcessing(this);
  }

  process(operation, bool) {
    this.startProcessing();
    let worker;
    if (bool || operation.id === this.jobId) {
      worker = this.currentWorker;
    } else {
      worker = this.spawnWorker(operation.src);
      this.jobId = operation.id;
    }

    worker.onmessage = ({ data }) => {
      this.triggerData(operation, data);
      this.doneProcessing();
    };

    worker.onerror = (e) => {
      this.triggerDataError(operation, e);
      this.doneProcessing();
    };

    worker.postMessage(operation.data);
    operation.data = null;
  }

  spawnWorker(src) {
    if (this.currentWorker) {
      this.currentWorker.terminate();
    }

    const url = createObjectUrl(src);
    const worker = new Worker(url);

    return this.currentWorker = worker;
  }
}

export default Thread;

Fix to help garbage-collector work more rapidly


import { Immutable } from 'nuclear-js';
import { getters as entityGetters } from '../entity';

export const currentView = [
  'currentView',
];

export const views = [
  entityGetters.entityMap,
  entities => entities.filter(entity => entity.domain === 'group' &&
                                        entity.attributes.view),
];

function addToMap(map, entities, groupEntity) {
  groupEntity.attributes.entity_id.forEach(entityId => {
    if (map.has(entityId)) return;

    const entity = entities.get(entityId);

    if (!entity || entity.attributes.hidden) return;

    map.set(entityId, entity);

    if (entity.domain === 'group') {
      addToMap(map, entities, entity);
    }
  });
}

export const currentViewEntities = [
  entityGetters.entityMap,
  currentView,
  (entities, view) => {
    let viewEntity;

    if (view) {
      viewEntity = entities.get(view);
    }

    if (!viewEntity) {
      return entities.filter(entity => !entity.attributes.hidden);
    }

    return (new Immutable.Map()).withMutations(map => {
      addToMap(map, entities, viewEntity);
    });
  },
];

import { Immutable } from 'nuclear-js';
import { getters as entityGetters } from '../entity';

const DEFAULT_VIEW_ENTITY_ID = 'group.default_view';

export const currentView = [
  'currentView',
];

export const views = [
  entityGetters.entityMap,
  entities => entities.filter(entity => entity.domain === 'group' &&
                                        entity.attributes.view &&
                                        entity.entityId !== DEFAULT_VIEW_ENTITY_ID),
];

function addToMap(map, entities, groupEntity) {
  groupEntity.attributes.entity_id.forEach(entityId => {
    if (map.has(entityId)) return;

    const entity = entities.get(entityId);

    if (!entity || entity.attributes.hidden) return;

    map.set(entityId, entity);

    if (entity.domain === 'group') {
      addToMap(map, entities, entity);
    }
  });
}

export const currentViewEntities = [
  entityGetters.entityMap,
  currentView,
  (entities, view) => {
    let viewEntity;

    if (view) {
      viewEntity = entities.get(view);
    } else {
      // will be undefined if entity does not exist
      viewEntity = entities.get(DEFAULT_VIEW_ENTITY_ID);
    }

    if (!viewEntity) {
      return entities.filter(entity => !entity.attributes.hidden);
    }

    return (new Immutable.Map()).withMutations(map => {
      addToMap(map, entities, viewEntity);
    });
  },
];

Allow overwriting ALL view with own view


'use strict';

var randu = require( '@stdlib/math/base/random/randu' );
var quantile = require( './../lib' );

var mu;
var p;
var y;
var i;

for ( i = 0; i < 10; i++ ) {
	p = (randu() * 2.0) - 0.5;
	mu = (randu() * 10) - 5;
	y = quantile( p, mu );
	console.log( 'p: %d, µ: %d, Q(p;µ): %d', p, mu, y );
}

'use strict';

var randu = require( '@stdlib/math/base/random/randu' );
var quantile = require( './../lib' );

var mu;
var p;
var y;
var i;

for ( i = 0; i < 10; i++ ) {
	p = randu();
	mu = ( randu()*10.0 ) - 5.0;
	y = quantile( p, mu );
	console.log( 'p: %d, µ: %d, Q(p;µ): %d', p, mu, y );
}

Fix example and add decimal points


'use strict';

const _ = require('lodash');
const Promise = require('bluebird');
const assert = require('hoek').assert;

class Deployer {
  constructor(namespace, kubectl) {
    this.namespace = namespace;
    this.kubectl = kubectl;
  }

  deployResource(path, options) {
    let requirements = Promise.resolve();
    const resource = this.namespace.getResource(path);

    assert(resource, "Resource not defined");

    const dependencies = _.get(resource.value, ["metadata", "annotations", "x-truder.net/dependencies"]);
    if (dependencies) {
      requirements = Promise.map(dependencies.split(','), dep => {
        return this.deployResource(dep, options);
      });
    }

    return requirements.then(() => {
      if (options.rollingUpdate && resource.type === 'replicationcontrollers') {
        return this.kubectl.rollingUpdate(resource.namespace, options.rollingUpdate, resource.value);
      }

      return this.kubectl.deploy(resource.namespace, resource.kind, resource.name, resource.value);
    }).then(() => {
      if (options.restart && resource.type === 'replicationcontrollers') {
        return this.kubectl.deleteByLabels(resource.name, 'pod', resource.value.spec.selector);
      }
    });
  }

  deploy(include, options) {
    let resources;

    if (_.isEmpty(include)) {
      resources = _.keys(this.namespace.resources);
    } else {
      resources = _.isArray(include) ? include : [include];
    }

    return Promise.mapSeries(resources, path => this.deployResource(path, options));
  }
}

module.exports = Deployer;

'use strict';

const _ = require('lodash');
const Promise = require('bluebird');
const assert = require('hoek').assert;

class Deployer {
  constructor(namespace, kubectl) {
    this.namespace = namespace;
    this.kubectl = kubectl;
  }

  deployResource(path, options) {
    let requirements = Promise.resolve();
    const resource = this.namespace.getResource(path);

    assert(resource, "Resource not defined");

    const dependencies = _.get(resource.value, ["metadata", "annotations", "x-truder.net/dependencies"]);
    if (dependencies) {
      requirements = Promise.map(dependencies.split(','), dep => {
        return this.deployResource(dep, options);
      });
    }

    return requirements.then(() => {
      if (options.rollingUpdate && resource.type === 'replicationcontrollers') {
        return this.kubectl.rollingUpdate(resource.namespace, options.rollingUpdate, resource.value);
      }

      return this.kubectl.deploy(resource.namespace, resource.kind, resource.name, resource.value);
    }).then(() => {
      if (options.restart && resource.type === 'replicationcontrollers') {
        return this.kubectl.deleteByLabels(resource.name, 'pod', resource.value.spec.selector);
      }
    });
  }

  deploy(include, options) {
    let resources;

    if (_.isEmpty(include)) {
      resources = this.namespace.resources;
    } else {
      resources = _.isArray(include) ? include : [include];
    }

    return Promise.mapSeries(resources, path => this.deployResource(path, options));
  }
}

module.exports = Deployer;

Fix deployer not finding resources


var BlockDevice = require( 'blockdevice' )
var Disk = require( 'disk' )
var ExFAT = require( '..' )
var assert = require( 'assert' )

var log = console.log.bind( console )
var inspect = function( value ) {
  return require( 'util' ).inspect( value, {
    colors: true,
    // depth: null,
  })
}

suite( 'ExFAT', function() {

  var device = null
  var disk = null
  var partition = null
  var volume = null

  suiteSetup( 'Create BlockDevice', function() {
    device = new BlockDevice({
      // path: BlockDevice.getPath( 2 ),
      path: __dirname + '/data/usb-thumb-exfat.bin',
      mode: 'r',
      blockSize: 512,
    })
  })

  suiteSetup( 'Open Disk', function( done ) {
    disk = new Disk( device )
    disk.open( done )
  })

  test( 'Instance ExFAT Volume', function() {

    log( inspect( disk ) )
    log( '' )

    var part = disk.mbr.partitions[0]
    partition = device.partition({
      firstLBA: part.firstLBA,
      lastLBA: part.firstLBA + part.sectors
    })

    volume = new ExFAT.Volume( partition )

    volume.mount( function( error ) {
      if( error ) console.trace( error.stack )
      log( inspect( volume ) )
      log( '' )
    })

  })

  teardown( 'Close Disk', function( done ) {
    disk.close( done )
  })

})

var BlockDevice = require( 'blockdevice' )
var Disk = require( 'disk' )
var ExFAT = require( '..' )
var assert = require( 'assert' )

var log = console.log.bind( console )
var inspect = function( value ) {
  return require( 'util' ).inspect( value, {
    colors: true,
    // depth: null,
  })
}

suite( 'ExFAT', function() {

  var device = null
  var disk = null
  var partition = null
  var volume = null

  suiteSetup( 'Create BlockDevice', function() {
    device = new BlockDevice({
      path: __dirname + '/data/usb-thumb-exfat.bin',
      mode: 'r',
      blockSize: 512,
    })
  })

  suiteSetup( 'Open Disk', function( done ) {
    disk = new Disk( device )
    disk.open( done )
  })

  test( 'Instance ExFAT Volume', function( done ) {

    log( inspect( disk ) )
    log( '' )

    var part = disk.mbr.partitions[0]
    partition = device.partition({
      firstLBA: part.firstLBA,
      lastLBA: part.firstLBA + part.sectors
    })

    volume = new ExFAT.Volume( partition )

    volume.mount( function( error ) {
      log( inspect( volume ) )
      log( '' )
      done( error )
    })

  })

  teardown( 'Close Disk', function( done ) {
    disk.close( done )
  })

})

Update test: Add missing test callback


expect = require('chai').expect,
redis = require('redis'),
redisClient = redis.createClient(),
RateLimiter = require('../ratelimiter'),
limits = require('./data');

fitbit = new RateLimiter("fitbit", limits[0], redisClient);
jawbone = new RateLimiter("jawbone", limits[1], redisClient);

blockedRequests = [],
allowedRequests = [];

// These are the callbacks for block and allow request
block = function(uid, limits) {
	blockedRequests.push(limits);
};

allow = function(uid) {
	allowedRequests.push(uid);
};

var singleLimitTests = require('./lib/singlelimit'),
	multiLimitTests = require('./lib/multilimit'),
	expireTests = require('./lib/expire');

describe("Single limit tests", function() {
	singleLimitTests();
})

describe("Multi limit tests", function() {
	multiLimitTests();
});

describe("Expire tests", function() {
	expireTests();
});

expect = require('chai').expect,
redis = require('redis'),
redisClient = redis.createClient(),
RateLimiter = require('../ratelimiter'),
limits = require('./data');

// Initialize two different rate limiters with different limits
fitbit = new RateLimiter("fitbit", limits[0], redisClient);
jawbone = new RateLimiter("jawbone", limits[1], redisClient);

// Allowed and blocked requests will be stored in these arrays
blockedRequests = [],
allowedRequests = [];

// These are the callbacks for block and allow request
block = function(uid, limits) {
	blockedRequests.push(limits);
};

allow = function(uid) {
	allowedRequests.push(uid);
};

// Run all the tests
var singleLimitTests = require('./lib/singlelimit'),
	multiLimitTests = require('./lib/multilimit'),
	expireTests = require('./lib/expire');

describe("Single limit tests", function() {
	singleLimitTests();
})

describe("Multi limit tests", function() {
	multiLimitTests();
});

describe("Expire tests", function() {
	expireTests();
});

Add comments for test launcher


import { BasicStrategy } from 'passport-http';
import passport from 'passport';
import async from 'async';
import jwt from 'jsonwebtoken';

import { userModel } from '../models/index';
import { secret } from '../config/config';

import {
  ExtractJwt,
  Strategy
} from 'passport-jwt';

passport.use(new BasicStrategy(
  function(username, password, callback) {

    userModel.findOne({ name: username }, function (err, user) {

      if (err) {
				return callback(err);
			}

      if (!user) {
        return callback(null, { err: {usernameErr: 'There is not an account for this username' } });
			}

      user.arePasswordsMatching(password, function(err, isMatch) {

        if (err) {
          return callback(err);
        }

        if (!isMatch) {
          return callback(null, { err: { passwordErr: 'Invalid password' } });
        }

        let payload = {
          'iss': 'users-microservice',
          'userId': user._id,
          'userName': user.name,
          'userEmail': user.email
        };

        const token = jwt.sign(payload, secret)

        return callback(null, { token });
      });
    });
  }
));

let opts = {};

let cookieExtractor = function(req) {
  let token = null;

  if (req && req.cookies){
    token = req.cookies['jwt'];
  }

  return token;
};

opts.jwtFromRequest = ExtractJwt.fromExtractors([cookieExtractor]);
opts.secretOrKey = secret;

passport.use(new Strategy(opts,
  function(jwt_payload, callback) {
    const { userId } = jwt_payload;

    userModel.findById(userId, function(err, user) {

      if (err) {
          return callback(err, false);
      }

      if (user) {
          return callback(null, user);
      } else {
          return callback(null, { err: 'There is not a user for this token' });
      }
    });
  }
));

export const authenticatedWithToken = passport.authenticate('jwt', { session : false });
export const authenticatedWithBasic = passport.authenticate('basic', { session : false });

import fetch from 'node-fetch';

import { userModel } from '../models/index';

export const authenticatedWithToken = (req, res, next) => {
  let csrf;
  let jwt;

  if (req && req.cookies && req.headers){
    csrf = req.headers.csrf;
    jwt = req.cookies.jwt;

    const opts = {
      headers: {
        csrf,
        jwt
      }
    };

    fetch('http://localhost:3002/signcheck', opts)
      .then(res => {
        if (res.status === 401) {
          return null;
        }

        return res.json();
      })
      .then(user => {
        if (!user) {
          return next();
        } else {
          return userModel.findOne({name: user.name})
            .then(user => {
              req.user = user;

              return next();
          });
        }
      })
  }
};

Change the authentication process by using the users micro service


var assert = require('chai').assert;
var HashService = require('../');
var BCryptHash = require('../lib/BCryptHash');

describe('HashService', function () {
  it('Should properly export', function () {
    assert.isObject(HashService);
    assert.isFunction(HashService.create);
    assert.isFunction(HashService.BCryptHash);
  });

  it('Should properly create bcrypt instance', function () {
    assert.instanceOf(HashService.create('bcrypt'), BCryptHash);
  });

  it('Should properly throw Error if type is unrecognized', function () {
    assert.throw(function () {
      HashService.create('NOT_EXISTS');
    }, Error);
  });

  it('Should properly hash bcrypt', function () {
    var hasher = HashService.create('bcrypt');
    var hash = hasher.hashSync('MY_PASSWORD');

    assert.ok(hasher.compareSync('MY_PASSWORD', hash));
    assert.notOk(hasher.compareSync('WRONG_PASSWORD', hash));
  });
});

var assert = require('chai').assert;
var HashService = require('../');
var BCryptHash = HashService.BCryptHash;

describe('HashService', function () {
  it('Should properly export', function () {
    assert.isObject(HashService);
    assert.isFunction(HashService.create);
    assert.isFunction(HashService.BCryptHash);
  });

  it('Should properly create bcrypt instance', function () {
    assert.instanceOf(HashService.create('bcrypt'), BCryptHash);
  });

  it('Should properly throw Error if type is unrecognized', function () {
    assert.throw(function () {
      HashService.create('NOT_EXISTS');
    }, Error);
  });

  it('Should properly hash bcrypt', function () {
    var hasher = HashService.create('bcrypt');
    var hash = hasher.hashSync('MY_PASSWORD');

    assert.ok(hasher.compareSync('MY_PASSWORD', hash));
    assert.notOk(hasher.compareSync('WRONG_PASSWORD', hash));
  });
});

Change require to link from service


(function (global) {
    'use strict';

    var app = global.app = global.app || {};

    app.PushFactory = (function () {
        var create = function (sender, recipients) {
            var filter;

            if (Array.isArray(recipients) && recipients.length > 0) {
                // filter on the userId field in each device
                filter = {
                    "User.Id": {
                        "$in": recipients
                    }
                };
            }
            // custom data object for Android and iOS
            var customData = {
                "dateCreated": new Date()
            };

            var pushTitle = "Backend Services Push Sample";
            var pushMessage = sender + ": " + "Hello, push notifications!";

            // constructing the payload for the notification specifically for each supported mobile platform
            // following the structure from here: http://docs.telerik.com/platform/backend-services/features/push-notifications/structure
            var androidPayload = {
                "data": {
                    "title": pushTitle,
                    "message": pushMessage,
                    "customData": customData
                }
            };

            var iosPayload = {
                "aps": {
                    "alert": pushMessage,
                    "badge": 1,
                    "sound": "default"
                },
                "customData": customData
            };

            var wpPayload = {
                "Toast": {
                    "Title": pushTitle,
                    "Message": pushMessage
                }
            };

            var notificationObject = {
                "Filter": JSON.stringify(filter),
                "Android": androidPayload,
                "IOS": iosPayload,
                "WindowsPhone": wpPayload
            };

            return notificationObject;
        }

        return {
            create: create
        }
    })();
}(window));

(function (global) {
    'use strict';

    var app = global.app = global.app || {};

    app.PushFactory = (function () {
        var create = function (sender, recipients) {
            var filter;

            if (Array.isArray(recipients) && recipients.length > 0) {
                // filter on the userId field in each device
                filter = {
                    "UserId": {
                        "$in": recipients
                    }
                };
            }
            // custom data object for Android and iOS
            var customData = {
                "dateCreated": new Date()
            };

            var pushTitle = "Backend Services Push Sample";
            var pushMessage = sender + ": " + "Hello, push notifications!";

            // constructing the payload for the notification specifically for each supported mobile platform
            // following the structure from here: http://docs.telerik.com/platform/backend-services/features/push-notifications/structure
            var androidPayload = {
                "data": {
                    "title": pushTitle,
                    "message": pushMessage,
                    "customData": customData
                }
            };

            var iosPayload = {
                "aps": {
                    "alert": pushMessage,
                    "badge": 1,
                    "sound": "default"
                },
                "customData": customData
            };

            var wpPayload = {
                "Toast": {
                    "Title": pushTitle,
                    "Message": pushMessage
                }
            };

            var notificationObject = {
                "Filter": JSON.stringify(filter),
                "Android": androidPayload,
                "IOS": iosPayload,
                "WindowsPhone": wpPayload
            };

            return notificationObject;
        }

        return {
            create: create
        }
    })();
}(window));

Implement the strict filter to use UserId to match the Limit permissions for push


const AutoLaunch = require('auto-launch');
const {remote} = require('electron');

function shouldQuitApp(cmd) {
  if (process.platform !== 'win32') {
    return false;
  }
  return ['--squirrel-install', '--squirrel-updated', '--squirrel-uninstall', '--squirrel-obsolete'].includes(cmd);
}

async function setupAutoLaunch(cmd) {
  const appLauncher = new AutoLaunch({
    name: remote.app.getName(),
    isHidden: true
  });
  if (cmd === '--squirrel-uninstall') {
      // If we're uninstalling, make sure we also delete our auto launch registry key
    return appLauncher.disable();
  } else if (cmd === '--squirrel-install' || cmd === '--squirrel-updated') {
      // If we're updating and already have an registry entry for auto launch, make sure to update the path
    const enabled = await appLauncher.isEnabled();
    if (enabled) {
      return appLauncher.enable();
    }
  }
  return async () => true;
}

function squirrelStartup() {
  if (process.platform === 'win32') {
    const cmd = process.argv[1];
    setupAutoLaunch(cmd).then(() => {
      require('electron-squirrel-startup'); // eslint-disable-line global-require
    });
    return shouldQuitApp(cmd);
  }
  return false;
}

module.exports = squirrelStartup;

const AutoLaunch = require('auto-launch');
const {app} = require('electron');

function shouldQuitApp(cmd) {
  if (process.platform !== 'win32') {
    return false;
  }
  return ['--squirrel-install', '--squirrel-updated', '--squirrel-uninstall', '--squirrel-obsolete'].includes(cmd);
}

async function setupAutoLaunch(cmd) {
  const appLauncher = new AutoLaunch({
    name: app.getName(),
    isHidden: true
  });
  if (cmd === '--squirrel-uninstall') {
      // If we're uninstalling, make sure we also delete our auto launch registry key
    return appLauncher.disable();
  } else if (cmd === '--squirrel-install' || cmd === '--squirrel-updated') {
      // If we're updating and already have an registry entry for auto launch, make sure to update the path
    const enabled = await appLauncher.isEnabled();
    if (enabled) {
      return appLauncher.enable();
    }
  }
  return async () => true;
}

function squirrelStartup() {
  if (process.platform === 'win32') {
    const cmd = process.argv[1];
    setupAutoLaunch(cmd).then(() => {
      require('electron-squirrel-startup'); // eslint-disable-line global-require
    });
    return shouldQuitApp(cmd);
  }
  return false;
}

module.exports = squirrelStartup;

Fix wrong require for main process


/*

HoC that provides a simple mutation that expects a single JSON object in return

Example usage:

export default withMutation({
  name: 'getEmbedlyData',
  args: {url: 'String'},
})(EmbedlyURL);

*/

import { graphql } from 'react-apollo';
import gql from 'graphql-tag';

export default function withMutation({name, args}) {

  const args1 = _.map(args, (type, name) => `$${name}: ${type}`); // e.g. $url: String
  const args2 = _.map(args, (type, name) => `${name}: $${name}`); // e.g. $url: url

  return graphql(gql`
    mutation ${name}(${args1}) {
      ${name}(${args2})
    }
  `, {
    props: ({ownProps, mutate}) => ({
      [name]: (vars) => {
        return mutate({
          variables: vars,
        });
      }
    }),
  });
}

/*

HoC that provides a simple mutation that expects a single JSON object in return

Example usage:

export default withMutation({
  name: 'getEmbedlyData',
  args: {url: 'String'},
})(EmbedlyURL);

*/

import { graphql } from 'react-apollo';
import gql from 'graphql-tag';

export default function withMutation({name, args}) {

  let mutation;

  if (args) {
    const args1 = _.map(args, (type, name) => `$${name}: ${type}`); // e.g. $url: String
    const args2 = _.map(args, (type, name) => `${name}: $${name}`); // e.g. $url: url
    mutation = `
      mutation ${name}(${args1}) {
        ${name}(${args2})
      }
    `
  } else {
    mutation = `
      mutation ${name} {
        ${name}
      }
    `
  }

  return graphql(gql`${mutation}`, {
    props: ({ownProps, mutate}) => ({
      [name]: (vars) => {
        return mutate({
          variables: vars,
        });
      }
    }),
  });
}

Make mutation hoc accept mutations without arguments


/**
 * @license
 * Copyright (c) 2020 The Polymer Project Authors. All rights reserved. This
 * code may only be used under the BSD style license found at
 * http://polymer.github.io/LICENSE.txt The complete set of authors may be
 * found at http://polymer.github.io/AUTHORS.txt The complete set of
 * contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt Code
 * distributed by Google as part of the polymer project is also subject to an
 * additional IP rights grant found at http://polymer.github.io/PATENTS.txt
 */

'use strict';

const compilerPackage = require('google-closure-compiler');
const gulp = require('gulp');
const sourcemaps = require('gulp-sourcemaps');

const closureCompiler = compilerPackage.gulp();

gulp.task('default', () => {
  return gulp.src('./src/**/*.js', {base: './'})
    .pipe(sourcemaps.init())
    .pipe(closureCompiler({
      compilation_level: 'ADVANCED',
      warning_level: 'VERBOSE',
      language_in: 'ECMASCRIPT6_STRICT',
      language_out: 'ECMASCRIPT5_STRICT',
      dependency_mode: 'STRICT',
      entry_point: ['/src/formdata-event'],
      js_output_file: 'formdata-event.min.js',
      output_wrapper: '(function(){\n%output%\n}).call(self);',
      assume_function_wrapper: true,
      rewrite_polyfills: false,
    }))
    .pipe(sourcemaps.write('/'))
    .pipe(gulp.dest('./'));
});

/**
 * @license
 * Copyright (c) 2020 The Polymer Project Authors. All rights reserved. This
 * code may only be used under the BSD style license found at
 * http://polymer.github.io/LICENSE.txt The complete set of authors may be
 * found at http://polymer.github.io/AUTHORS.txt The complete set of
 * contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt Code
 * distributed by Google as part of the polymer project is also subject to an
 * additional IP rights grant found at http://polymer.github.io/PATENTS.txt
 */

'use strict';

const compilerPackage = require('google-closure-compiler');
const gulp = require('gulp');
const sourcemaps = require('gulp-sourcemaps');

const closureCompiler = compilerPackage.gulp();

gulp.task('default', () => {
  return gulp.src('./src/**/*.js', {base: './'})
    .pipe(sourcemaps.init())
    .pipe(closureCompiler({
      compilation_level: 'ADVANCED',
      warning_level: 'VERBOSE',
      language_in: 'ECMASCRIPT_2019',
      language_out: 'ECMASCRIPT5_STRICT',
      dependency_mode: 'STRICT',
      entry_point: ['/src/formdata-event'],
      js_output_file: 'formdata-event.min.js',
      output_wrapper: '(function(){\n%output%\n}).call(self);',
      assume_function_wrapper: true,
      rewrite_polyfills: false,
    }))
    .pipe(sourcemaps.write('/'))
    .pipe(gulp.dest('./'));
});

Update Closure LANGUAGE_IN flag to match tsc's target.


var fs = require('fs');
var specs = JSON.parse(fs.readFileSync('tests/end2end/specs.json'));

var browser_capabilities = JSON.parse(process.env.SELENIUM_BROWSER_CAPABILITIES);
browser_capabilities['name'] = 'GlobaLeaks-E2E';
browser_capabilities['tunnel-identifier'] = process.env.TRAVIS_JOB_NUMBER;
browser_capabilities['build'] = process.env.TRAVIS_BUILD_ID;
browser_capabilities['tags'] = [process.env.TRAVIS_BRANCH];

exports.config = {
  framework: 'jasmine',

  baseUrl: 'http://localhost:9000/',

  troubleshoot: false,
  directConnect: false,

  sauceUser: process.env.SAUCE_USERNAME,
  sauceKey: process.env.SAUCE_ACCESS_KEY,
  capabilities: browser_capabilities,

  params: {
    'testFileDownload': false,
    'verifyFileDownload': false,
    'tmpDir': '/tmp/globaleaks-download',
  },

  specs: specs,

  jasmineNodeOpts: {
    isVerbose: true,
    includeStackTrace: true,
    defaultTimeoutInterval: 360000
  }
};

var fs = require('fs');
var specs = JSON.parse(fs.readFileSync('tests/end2end/specs.json'));

var browser_capabilities = JSON.parse(process.env.SELENIUM_BROWSER_CAPABILITIES);
browser_capabilities['name'] = 'GlobaLeaks-E2E';
browser_capabilities['tunnel-identifier'] = process.env.TRAVIS_JOB_NUMBER;
browser_capabilities['build'] = process.env.TRAVIS_BUILD_NUMBER;
browser_capabilities['tags'] = [process.env.TRAVIS_BRANCH];

exports.config = {
  framework: 'jasmine',

  baseUrl: 'http://localhost:9000/',

  troubleshoot: false,
  directConnect: false,

  sauceUser: process.env.SAUCE_USERNAME,
  sauceKey: process.env.SAUCE_ACCESS_KEY,
  capabilities: browser_capabilities,

  params: {
    'testFileDownload': false,
    'verifyFileDownload': false,
    'tmpDir': '/tmp/globaleaks-download',
  },

  specs: specs,

  jasmineNodeOpts: {
    isVerbose: true,
    includeStackTrace: true,
    defaultTimeoutInterval: 360000
  }
};

Revert "Replace TRAVIS_BUILD_NUMBER with TRAVIS_BUILD_ID"


const winston = require('winston');
const { Writable } = require('stream');
const globalLogger = require('../../logger');

const getSyncLogger = (logStream) => {
	const logger = winston.createLogger({
		transports: [
			new winston.transports.Stream({
				stream: globalLogger,
			}),
		],
	});
	if (logStream && logStream instanceof Writable) {
		logger.add(new winston.transports.Stream({
			stream: logStream,
		}));
	}
	return logger;
};

module.exports = getSyncLogger;

const winston = require('winston');
const { Writable } = require('stream');
const globalLogger = require('../../logger');

const getSyncLogger = (logStream) => {
	const logger = winston.createLogger({
		levels: winston.config.syslog.levels,
		transports: [
			new winston.transports.Stream({
				stream: globalLogger,
			}),
		],
	});
	if (logStream && logStream instanceof Writable) {
		logger.add(new winston.transports.Stream({
			stream: logStream,
		}));
	}
	return logger;
};

module.exports = getSyncLogger;

Align with global log levels


import client from '@/store/api/client'

export default {
  getAssetTypes (callback) {
    client.get('/api/data/asset-types', callback)
  },

  getAssetType (assetTypeId, callback) {
    client.get(`/api/data/entity-types/${assetTypeId}`, callback)
  },

  newAssetType (assetType, callback) {
    const data = {
      name: assetType.name,
      task_types: assetType.task_types
    }
    return client.ppost('/api/data/entity-types', data)
  },

  updateAssetType (assetType, callback) {
    const data = {
      name: assetType.name,
      task_types: assetType.task_types
    }
    return client.pput(`/api/data/entity-types/${assetType.id}`, data)
  },

  deleteAssetType (assetType, callback) {
    return client.pdel(`/api/data/entity-types/${assetType.id}`)
  }
}

import client from '@/store/api/client'

export default {
  getAssetTypes (callback) {
    client.get('/api/data/asset-types?relations=true', callback)
  },

  getAssetType (assetTypeId, callback) {
    client.get(`/api/data/entity-types/${assetTypeId}`, callback)
  },

  newAssetType (assetType, callback) {
    const data = {
      name: assetType.name,
      task_types: assetType.task_types
    }
    return client.ppost('/api/data/entity-types', data)
  },

  updateAssetType (assetType, callback) {
    const data = {
      name: assetType.name,
      task_types: assetType.task_types
    }
    return client.pput(`/api/data/entity-types/${assetType.id}`, data)
  },

  deleteAssetType (assetType, callback) {
    return client.pdel(`/api/data/entity-types/${assetType.id}`)
  }
}

Fix the way asset types are retrieved


// Karma configuration file, see link for more information
// https://karma-runner.github.io/0.13/config/configuration-file.html
process.env.CHROME_BIN = require('puppeteer').executablePath()

module.exports = function (config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine', '@angular/cli'],
    plugins: [
      require('karma-jasmine'),
      require('karma-chrome-launcher'),
      require('karma-jasmine-html-reporter'),
      require('karma-coverage-istanbul-reporter'),
      require('@angular/cli/plugins/karma')
    ],
    client:{
      clearContext: false // leave Jasmine Spec Runner output visible in browser
    },
    coverageIstanbulReporter: {
      reports: [ 'html', 'lcovonly' ],
      fixWebpackSourcePaths: true
    },
    angularCli: {
      environment: 'dev'
    },
    reporters: ['progress', 'kjhtml'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['ChromeHeadless'],
    // browsers: ['ChromeHeadlessNoSandbox'],
    // customLaunchers: {
    //   ChromeHeadlessNoSandbox: {
    //     base: 'Chrome',
    //     flags: [
    //       '--disable-translate',
    //       '--headless',
    //       '--disable-gpu',
    //       '--disable-extensions',
    //       '--no-sandbox',
    //       '--disable-setuid-sandbox',
    //       '--remote-debugging-port=9222']
    //   }
    //},
    singleRun: true
  });
};

// Karma configuration file, see link for more information
// https://karma-runner.github.io/0.13/config/configuration-file.html
process.env.CHROME_BIN = require('puppeteer').executablePath()

module.exports = function (config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine', '@angular/cli'],
    plugins: [
      require('karma-jasmine'),
      require('karma-chrome-launcher'),
      require('karma-jasmine-html-reporter'),
      require('karma-coverage-istanbul-reporter'),
      require('@angular/cli/plugins/karma')
    ],
    client:{
      clearContext: false // leave Jasmine Spec Runner output visible in browser
    },
    coverageIstanbulReporter: {
      reports: [ 'html', 'lcovonly' ],
      fixWebpackSourcePaths: true
    },
    angularCli: {
      environment: 'dev'
    },
    reporters: ['progress', 'kjhtml'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['ChromeHeadlessNoSandbox'],
    customLaunchers: {
       ChromeHeadlessNoSandbox: {
         base: 'ChromeHeadless',
         flags: [
    //       '--disable-translate',
    //       '--headless',
    //       '--disable-gpu',
    //       '--disable-extensions',
           '--no-sandbox',
    //       '--disable-setuid-sandbox',
    //       '--remote-debugging-port=9222'
         ]
       }
    },
    singleRun: true
  });
};

Revert to puppeteer (keeping chromium install for dependencies)


'use strict';
var grunt = require('grunt');

/*
    ======== A Handy Little Nodeunit Reference ========
    https://github.com/caolan/nodeunit

    Test methods:
    test.expect(numAssertions)
    test.done()
    Test assertions:
    test.ok(value, [message])
    test.equal(actual, expected, [message])
    test.notEqual(actual, expected, [message])
    test.deepEqual(actual, expected, [message])
    test.notDeepEqual(actual, expected, [message])
    test.strictEqual(actual, expected, [message])
    test.notStrictEqual(actual, expected, [message])
    test.throws(block, [error], [message])
    test.doesNotThrow(block, [error], [message])
    test.ifError(value)
*/

exports.asset_pipeline = {
    setUp: function(done) {
        // setup here if necessary
        done();
    },
    concat: function(test) {

        test.expect(2);
        var all = fs.readFileSync('build/artifacts/concat/custom.js',{encoding:'utf8'});

        // var foo ='bar'; var lorem = 'ipsum'; foo = 'the unexpected';
        eval(all);

        test.equal( lorem, 'ipsum');
        test.equal( foo, 'the unexpected');

        test.done();
    }
};

'use strict';
var grunt = require('grunt');

/*
    ======== A Handy Little Nodeunit Reference ========
    https://github.com/caolan/nodeunit

    Test methods:
    test.expect(numAssertions)
    test.done()
    Test assertions:
    test.ok(value, [message])
    test.equal(actual, expected, [message])
    test.notEqual(actual, expected, [message])
    test.deepEqual(actual, expected, [message])
    test.notDeepEqual(actual, expected, [message])
    test.strictEqual(actual, expected, [message])
    test.notStrictEqual(actual, expected, [message])
    test.throws(block, [error], [message])
    test.doesNotThrow(block, [error], [message])
    test.ifError(value)
*/

exports.asset_pipeline = {
    setUp: function(done) {
        // setup here if necessary
        done();
    },
    concat: function(test) {

        test.expect(2);
        var all = fs.readFileSync('build/asset_pipeline/concat/custom.js',{encoding:'utf8'});

        // var foo ='bar'; var lorem = 'ipsum'; foo = 'the unexpected';
        eval(all);

        test.equal( lorem, 'ipsum');
        test.equal( foo, 'the unexpected');

        test.done();
    }
};

Test should verify internal dir not output dir


window.PusherClient = function(window) {

  var MISSING_PUSHER_ERROR_SEEN = false;
  var exports = {};

  exports.subscribeToPusherChanel = function(channelName, callbackFn, pusherUrl) {
    var client;
    if (pusherUrl == null) {
      pusherUrl = 'https://pusher.tokenly.com';
    }

    if (window.Faye == null) {
      console.error('Pusher client not defined');
      if (!MISSING_PUSHER_ERROR_SEEN) {
        alert('Unable to load the live data feed.  Please reload this page.');
        MISSING_PUSHER_ERROR_SEEN = true;
        window.Bugsnag && Bugsnag.notify("Missing Pusher Reference", "window.Faye was not a valid object");
      }
      return null;
    }

    client = new window.Faye.Client(pusherUrl + "/public");
    client.subscribe("/" + channelName, function(data) {
      callbackFn(data);
    });

    return client;
  };

  exports.closePusherChanel = function(client) {
    if (client != null) {
      client.disconnect();
    }
  };

  return exports;

}(this);

window.PusherClient = function(window) {

  var MISSING_PUSHER_ERROR_SEEN = false;
  var exports = {};

  exports.subscribeToPusherChanel = function(channelName, callbackFn, pusherUrl) {
    var client;
    if (pusherUrl == null) {
      pusherUrl = 'https://pusher.tokenly.com';
    }

    if (window.Faye == null) {
      console.error('Pusher client not defined');
      if (!MISSING_PUSHER_ERROR_SEEN) {
        alert('Unable to load the live data feed.  Please reload this page.');
        MISSING_PUSHER_ERROR_SEEN = true;
        window.Bugsnag && Bugsnag.notify("Missing Pusher Reference", "window.Faye was not a valid object");
      }
      return null;
    }

    // remove leading slash if any
    if (channelName.indexOf('/') === 0) {
      channelName = channelName.substr(1)
    }

    client = new window.Faye.Client(pusherUrl + "/public");
    client.subscribe("/" + channelName, function(data) {
      callbackFn(data);
    });

    return client;
  };

  exports.closePusherChanel = function(client) {
    if (client != null) {
      client.disconnect();
    }
  };

  return exports;

}(this);

Remove leading slash to prevent duplicate slashes


describe("Zotero.QuickCopy", function() {
	var quickCopyPref = Zotero.Prefs.get("export.quickCopy.setting");
	quickCopyPref = JSON.stringify(Zotero.QuickCopy.unserializeSetting(quickCopyPref));

	// TODO: These should set site-specific prefs and test the actual response against it,
	// but that will need to wait for 5.0. For now, just make sure they don't fail.
	describe("#getFormatFromURL()", function () {
		it("should handle a domain", function () {
			assert.deepEqual(Zotero.QuickCopy.getFormatFromURL('http://foo.com/'), quickCopyPref);
		})

		it("should handle a domain and path", function () {
			assert.deepEqual(Zotero.QuickCopy.getFormatFromURL('http://foo.com/bar'), quickCopyPref);
		})

		it("should handle a local host", function () {
			assert.deepEqual(Zotero.QuickCopy.getFormatFromURL('http://foo/'), quickCopyPref);
		})

		it("should handle an about: URL", function () {
			assert.deepEqual(Zotero.QuickCopy.getFormatFromURL('about:blank'), quickCopyPref);
		})

		it("should handle a chrome URL", function () {
			assert.deepEqual(Zotero.QuickCopy.getFormatFromURL('chrome://zotero/content/tab.xul'), quickCopyPref);
		})
	})
})

describe("Zotero.QuickCopy", function() {
	var quickCopyPref = Zotero.Prefs.get("export.quickCopy.setting");
	quickCopyPref = JSON.stringify(Zotero.QuickCopy.unserializeSetting(quickCopyPref));

	// TODO: These should set site-specific prefs and test the actual response against it,
	// but that will need to wait for 5.0. For now, just make sure they don't fail.
	describe("#getFormatFromURL()", function () {
		it("should handle an HTTP URL", function () {
			assert.deepEqual(Zotero.QuickCopy.getFormatFromURL('http://foo.com/'), quickCopyPref);
		})

		it("should handle an HTTPS URL", function () {
			assert.deepEqual(Zotero.QuickCopy.getFormatFromURL('https://foo.com/'), quickCopyPref);
		})

		it("should handle a domain and path", function () {
			assert.deepEqual(Zotero.QuickCopy.getFormatFromURL('http://foo.com/bar'), quickCopyPref);
		})

		it("should handle a local host", function () {
			assert.deepEqual(Zotero.QuickCopy.getFormatFromURL('http://foo/'), quickCopyPref);
		})

		it("should handle an about: URL", function () {
			assert.deepEqual(Zotero.QuickCopy.getFormatFromURL('about:blank'), quickCopyPref);
		})

		it("should handle a chrome URL", function () {
			assert.deepEqual(Zotero.QuickCopy.getFormatFromURL('chrome://zotero/content/tab.xul'), quickCopyPref);
		})
	})
})

Add a test to make sure HTTPS URLs aren't failing for Quick Copy


/**
 * Consider:
 *
 *    .a {
 *      margin: 0 5px 0 0;
 *    }
 *
 *    .a {
 *      margin: 0;
 *    }
 *
 * Would be converted to:
 *
 *    html[dir='ltr'] .a {
 *      margin: 0 5px 0 0;
 *    }
 *
 *    html[dir='rtl'] .a {
 *      margin: 0 0 0 5px;
 *    }
 *
 *    .a {
 *       margin: 0;
 *    }
 *
 * As the "html[dir] .a" will have more specificity,
 * "margin: 0" will be ignored. That's why we must
 * convert the following properties *always*, regardless
 * of the value.
 *
 *
 */
module.exports = [
    'background',
    'background-position',
    'background-position-x',
    'border',
    'border-color',
    'border-radius',
    'border-style',
    'border-width',
    'border-top',
    'border-right',
    'border-bottom',
    'border-left',
    'box-shadow',
    'margin',
    'padding',
    'text-shadow',
    'text-align',
    'transform',
    'transform-origin'
].join('|');

/**
 * Consider:
 *
 *    .a {
 *      margin: 0 5px 0 0;
 *    }
 *
 *    .a {
 *      margin: 0;
 *    }
 *
 * Would be converted to:
 *
 *    html[dir='ltr'] .a {
 *      margin: 0 5px 0 0;
 *    }
 *
 *    html[dir='rtl'] .a {
 *      margin: 0 0 0 5px;
 *    }
 *
 *    .a {
 *       margin: 0;
 *    }
 *
 * As the "html[dir] .a" will have more specificity,
 * "margin: 0" will be ignored. That's why we must
 * convert the following properties *always*, regardless
 * of the value.
 *
 *
 */
module.exports = [
    'background',
    'background-position',
    'background-position-x',
    'border',
    'border-color',
    'border-radius',
    'border-style',
    'border-width',
    'border-top',
    'border-right',
    'border-bottom',
    'border-left',
    'box-shadow',
    'margin',
    'padding',
    'text-shadow',
    'text-align',
    'transform',
    'transform-origin',
    'transition'
].join('|');

Add `transition` to always convert list


jQuery(function($) {
	$(document).on('change', '.js-save-on-change', function() {
        var $field = $(this);
        var name = $field.attr('name');
        var value = $field.val();

        $field.closest('.setting').trigger('changed', [name, value]);
    });

    $(document).on('changed', '.setting', function(event, name, value) {
        var $setting = $(this);
        var old = $setting.data('old');

        $setting.removeClass('saved').addClass('saving');

        if (old !== value) {
            FluxBB.ajax('POST', 'admin/settings/' + name, {
                value: value
            }).success(function(data) {
                $setting.data('old', value);
                $setting.addClass('saved');
            }).always(function() {
                $setting.removeClass('saving');
            });
        }
    });
});

jQuery(function($) {
    $('.setting .js-save-on-change').each(function () {
        var $field = $(this);
        $field.closest('.setting').data('old', $field.val());
    });

	$(document).on('change', '.js-save-on-change', function() {
        var $field = $(this);
        var name = $field.attr('name');
        var value = $field.val();

        $field.closest('.setting').trigger('changed', [name, value]);
    });

    $(document).on('changed', '.setting', function(event, name, value) {
        var $setting = $(this);
        var old = $setting.data('old');

        $setting.removeClass('saved').addClass('saving');

        if (old !== value) {
            FluxBB.ajax('POST', 'admin/settings/' + name, {
                value: value
            }).success(function(data) {
                $setting.data('old', value);
                $setting.addClass('saved');
            }).always(function() {
                $setting.removeClass('saving');
            });
        }
    });
});

Set data-old attribute automatically via JavaScript.


import { mapValues } from 'lodash-es';
import { isPlainObject } from 'lodash-es';
import { wrap } from 'lodash-es';

const FROM_HIGHCHARTS_PROVIDER = 'getHighcharts';
const FROM_CHART_PROVIDER = 'getChart';
const FROM_AXIS_PROVIDER = 'getAxis';
const FROM_SERIES_PROVIDER = 'getSeries';

export const PROVIDED_PROPS = [
  FROM_HIGHCHARTS_PROVIDER,
  FROM_CHART_PROVIDER,
  FROM_AXIS_PROVIDER,
  FROM_SERIES_PROVIDER
];

function cleanConfig(config) {
  // omit provided props
  return Object.keys(config)
    .filter(key => PROVIDED_PROPS.indexOf(key) < 0)
    .reduce((object, key) => {
      object[key] = config[key];
      return object;
    }, {});
}

function deepCleanConfig (config) {
  const cleanedRoot = cleanConfig(config);
  return mapValues(cleanedRoot, prop => {
    if (isPlainObject(prop) === false) return prop;

    return deepCleanConfig(prop);
  });
}

export default function removeProvidedProps (func) {
  return wrap(func, function (origFunc, config, ...rest) {
    const cleanedConfig = deepCleanConfig(config);
    return origFunc(cleanedConfig, ...rest);
  });
}

import { mapValues } from 'lodash-es';
import { isPlainObject } from 'lodash-es';

const FROM_HIGHCHARTS_PROVIDER = 'getHighcharts';
const FROM_CHART_PROVIDER = 'getChart';
const FROM_AXIS_PROVIDER = 'getAxis';
const FROM_SERIES_PROVIDER = 'getSeries';

export const PROVIDED_PROPS = [
  FROM_HIGHCHARTS_PROVIDER,
  FROM_CHART_PROVIDER,
  FROM_AXIS_PROVIDER,
  FROM_SERIES_PROVIDER
];

function cleanConfig(config) {
  // omit provided props
  return Object.keys(config)
    .filter(key => PROVIDED_PROPS.indexOf(key) < 0)
    .reduce((object, key) => {
      object[key] = config[key];
      return object;
    }, {});
}

function deepCleanConfig (config) {
  const cleanedRoot = cleanConfig(config);
  return mapValues(cleanedRoot, prop => {
    if (isPlainObject(prop) === false) return prop;

    return deepCleanConfig(prop);
  });
}

export default function removeProvidedProps (func) {
  return (config, ...rest) => {
    const cleanedConfig = deepCleanConfig(config);
    return func(cleanedConfig, ...rest);
  };
}

Replace lodash-es/wrap with arrow function


module.exports = {
  'parser': 'babel-eslint',
  'plugins': [
    'react'
  ],
  'extends': [
    'plugin:react/recommended'
  ],
  'rules': {
    'react/jsx-no-bind': 1,
    'react/prop-types': 1
  }
};

module.exports = {
  'parser': 'babel-eslint',
  'plugins': [
    'react'
  ],
  'extends': [
    'plugin:react/recommended'
  ],
  'rules': {
    'react/jsx-no-bind': 1,
    'react/prop-types': 1,
    'react/prefer-stateless-function': 1,
    'react/prefer-es6-class': [1, 'never']
  }
};

Add react rules to force function when we can otherwise use createClass


var fs = require('fs');
var path = require('path');
var sass = require('node-sass');

module.exports = function(grunt) {
  function writeFile(path, contents) {
    if(grunt.file.exists(path)) {
      grunt.file.delete(path);
    }

    grunt.file.write(path, contents);
  }

  grunt.registerTask('sass', function() {
    console.log(sass.info());

    var inFile = path.resolve('scss/app.scss');
    var outFile = path.resolve('dist/app.min.css');

    var result = sass.renderSync({
      file: inFile,
      outFile: outFile,
      outputStyle: 'compressed',
      sourceMap: false
    });

    writeFile(outFile, result.css);
  });
};

var fs = require('fs');
var path = require('path');
var sass = require('node-sass');

module.exports = function(grunt) {
  function writeFile(path, contents) {
    if(grunt.file.exists(path)) {
      grunt.file.delete(path);
    }

    grunt.file.write(path, contents);
  }

  grunt.registerTask('sass', function() {
    grunt.log.writeln(sass.info());

    var inFile = path.resolve('scss/app.scss');
    var outFile = path.resolve('dist/app.min.css');

    var result = sass.renderSync({
      file: inFile,
      outFile: outFile,
      outputStyle: 'compressed',
      sourceMap: false
    });

    writeFile(outFile, result.css);
  });
};

Replace console log with Grunt's logging function.