<commit_before>var FilesystemCollection = require('../core/FilesystemCollection');
var PageNode = require('./PageNode');
var _ = require('lodash');
var npath = require('path');

var _super = FilesystemCollection.prototype;

module.exports = FilesystemCollection.extend({

  'filetype': 'page',
  'extension': 'txt',

  'comparator': 'sortablePath',

  'model': PageNode,

  'initialize': function () {

    var self = this;
    _super.initialize.apply(self, arguments);

    self.aliasIndex = {};
    self.bindTo(self, 'add change:alias', 'indexAlias');
  },

  'indexAlias': function (model) {

    var self = this;
    var alias = model.attributes.alias;
    if (typeof alias === 'string') alias = [alias];

    alias && alias.forEach(function (url) {
      self.aliasIndex[url] = model.attributes.url;
    });
  },

  'defaultPages': function (urls) {

    var self = this;
    var count = 0;

    _.each(urls, function (url) {

      var page =  self.get(url);
      if (!page) {

        count++;

        var path = npath.join(self.options.frameworkDir, 'lib/pages' + url + '/error.txt');
        // self.logger.pending(self.namespace, 'Loading default 404 Not Found page @ ' + path);
        page = new self.model(null, self.options);

        page.fetch(path).done(function () {

          var models = _.flatten(_.toArray(arguments));
          self.add(models);
        });
      }
    });

    return count;
  },

  'ensureErrorPages': function () {

    var self = this;
    var started = new Date();
    var defaultCount = self.defaultPages(['/404', '/error']);
    defaultCount && self.logger.since(self.namespace, 'Loaded ' + defaultCount + ' default page(s)', started);
  },

  'filter': function (test, options) {

    var self = this;
    options || (options = {});

    self.assert('function', test, 'Cannot filter collection without a function to test each models attributes with');

    var result = _super.filter.call(self, function (model) {

      if (!options.hidden && model.isHidden()) return false;

      return test(model);
    });

    options.comparator = options.sortBy;
    options = _.merge({}, self.options, options);

    return new self.constructor(result, options);
  },

  'getUrlForAlias': function (url) {

    var self = this;
    return self.aliasIndex[url];
  },

  'childrenOf': function (parentUrl, options) {

    var self = this;
    options || (options = {});

    self.assert('defined string', parentUrl, 'Invalid parentUrl');

    var parent = self.get(parentUrl);
    self.assert('object', parent, 'No parent page exists with url ' + parentUrl);

    return self.filter(function (model) {

      var url = model.attributes.url;
      if (parentUrl == url) return false;

      var slashesInParentUrl = parentUrl.split('/').length - 1;
      if (parentUrl === '/') slashesInParentUrl = 0;
      var slashesInChildUrl = url.split('/').length - 1;

      if (slashesInChildUrl > (slashesInParentUrl + 1)) return false;

      return url.indexOf(parentUrl) === 0;
    }, options);
  }
});<commit_msg>sorting<commit_after>var FilesystemCollection = require('../core/FilesystemCollection');
var PageNode = require('./PageNode');
var _ = require('lodash');
var npath = require('path');

var _super = FilesystemCollection.prototype;

module.exports = FilesystemCollection.extend({

  'filetype': 'page',
  'extension': 'txt',

  'comparator': 'sortablePath',

  'model': PageNode,

  'initialize': function () {

    var self = this;
    _super.initialize.apply(self, arguments);

    self.aliasIndex = {};
    self.bindTo(self, 'add change:alias', 'indexAlias');
  },

  'indexAlias': function (model) {

    var self = this;
    var alias = model.attributes.alias;
    if (typeof alias === 'string') alias = [alias];

    alias && alias.forEach(function (url) {
      self.aliasIndex[url] = model.attributes.url;
    });
  },

  'defaultPages': function (urls) {

    var self = this;
    var count = 0;

    _.each(urls, function (url) {

      var page =  self.get(url);
      if (!page) {

        count++;

        var path = npath.join(self.options.frameworkDir, 'lib/pages' + url + '/error.txt');
        // self.logger.pending(self.namespace, 'Loading default 404 Not Found page @ ' + path);
        page = new self.model(null, self.options);

        page.fetch(path).done(function () {

          var models = _.flatten(_.toArray(arguments));
          self.add(models);
        });
      }
    });

    return count;
  },

  'ensureErrorPages': function () {

    var self = this;
    var started = new Date();
    var defaultCount = self.defaultPages(['/404', '/error']);
    defaultCount && self.logger.since(self.namespace, 'Loaded ' + defaultCount + ' default page(s)', started);
  },

  'filter': function (test, options) {

    var self = this;
    options || (options = {});

    self.assert('function', test, 'Cannot filter collection without a function to test each models attributes with');

    var result = _super.filter.call(self, function (model) {

      if (!options.hidden && model.isHidden()) return false;

      return test(model);
    });

    options.comparator = options.sortBy || self.comparator;
    options = _.merge({}, self.options, options);

    if (options.order === 'DESC' || options.reverse) {
      var comparator = options.comparator;
      options.comparator = function (a, b) {
        if (a.attributes[comparator] > b.attributes[comparator]) return -1;
        if (b.attributes[comparator] > a.attributes[comparator]) return 1;
        return 0;
      };
    }

    return new self.constructor(result, options);
  },

  'getUrlForAlias': function (url) {

    var self = this;
    return self.aliasIndex[url];
  },

  'childrenOf': function (parentUrl, options) {

    var self = this;
    options || (options = {});

    self.assert('defined string', parentUrl, 'Invalid parentUrl');

    var parent = self.get(parentUrl);
    self.assert('object', parent, 'No parent page exists with url ' + parentUrl);

    return self.filter(function (model) {

      var url = model.attributes.url;
      if (parentUrl == url) return false;

      var slashesInParentUrl = parentUrl.split('/').length - 1;
      if (parentUrl === '/') slashesInParentUrl = 0;
      var slashesInChildUrl = url.split('/').length - 1;

      if (slashesInChildUrl > (slashesInParentUrl + 1)) return false;

      return url.indexOf(parentUrl) === 0;
    }, options);
  }
});<|endoftext|>


<commit_before>// Copyright 2014 Open vStorage NV
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*global define, window */
define([
    'plugins/router', 'plugins/dialog', 'jqp/pnotify',
    'ovs/shared', 'ovs/generic',
    'jqp/timeago'
], function(router, dialog, $, shared, generic) {
    "use strict";
    var mode, childRouter;
    mode = router.activeInstruction().params[0];
    shared.routing.buildSiteRoutes(mode);
    childRouter = router.createChildRouter()
                        .makeRelative({
                            moduleId: 'viewmodels/site',
                            route: ':mode',
                            fromParent: true
                        })
                        .map(shared.routing.siteRoutes)
                        .buildNavigationModel();
    childRouter.guardRoute = function(instance, instruction) {
        var state, metadata;
        if (instance !== undefined && instance.hasOwnProperty('guard')) {
            if (instance.guard.authenticated === true) {
                if (instance.shared.authentication.validate()) {
                    window.localStorage.removeItem('state');
                    return true;
                }
                window.localStorage.setItem('referrer', instruction.fragment);
                state = window.localStorage.getItem('state');
                if (state === null && instance.shared.authentication.metadata.mode === 'remote') {
                    metadata = instance.shared.authentication.metadata;
                    state = generic.getTimestamp() + '_' + Math.random().toString().substr(2, 10);
                    window.localStorage.setItem('state', state);
                    return metadata.authorize_uri +
                        '?response_type=code' +
                        '&client_id=' + encodeURIComponent(metadata.client_id) +
                        '&redirect_uri=' + encodeURIComponent('https://' + metadata.ip + '/api/oauth2/redirect/') +
                        '&state=' + encodeURIComponent(state) +
                        '&scope=' + encodeURIComponent(metadata.scope);
                }
                return instruction.params[0] + '/login';
            }
        }
        window.localStorage.removeItem('state');
        return true;
    };
    childRouter.mapUnknownRoutes('../404');

    return {
        shared: shared,
        router: childRouter,
        activate: function(mode) {
            var self = this;
            // Config
            self.shared.mode(mode);

            // Notifications
            $.pnotify.defaults.history = false;
            $.pnotify.defaults.styling = "bootstrap";

            // Fetch main API metadata
            $.ajax('/api/?timestamp=' + (new Date().getTime()), {
                    type: 'GET',
                    contentType: 'application/json',
                    timeout: 5000,
                    headers: { Accept: 'application/json' }
                })
                .done(function(metadata) {
                    shared.nodes = metadata.storagerouter_ips;
                    shared.identification(metadata.identification);
                    window.localStorage.setItem('nodes', JSON.stringify(shared.nodes));
                });
        }
    };
});
<commit_msg>Backport PR#27<commit_after>// Copyright 2014 Open vStorage NV
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*global define, window */
define([
    'plugins/router', 'plugins/dialog', 'jqp/pnotify',
    'ovs/shared', 'ovs/generic',
    'jqp/timeago'
], function(router, dialog, $, shared, generic) {
    "use strict";
    var mode, childRouter;
    mode = router.activeInstruction().params[0];
    shared.routing.buildSiteRoutes(mode);
    childRouter = router.createChildRouter()
                        .makeRelative({
                            moduleId: 'viewmodels/site',
                            route: ':mode',
                            fromParent: true
                        })
                        .map(shared.routing.siteRoutes)
                        .buildNavigationModel();
    childRouter.guardRoute = function(instance, instruction) {
        var state, metadata;
        if (instance !== undefined && instance.hasOwnProperty('guard')) {
            if (instance.guard.authenticated === true) {
                if (instance.shared.authentication.validate()) {
                    window.localStorage.removeItem('state');
                    return true;
                }
                window.localStorage.setItem('referrer', instruction.fragment);
                state = window.localStorage.getItem('state');
                if (state === null && instance.shared.authentication.metadata.mode === 'remote') {
                    metadata = instance.shared.authentication.metadata;
                    state = generic.getTimestamp() + '_' + Math.random().toString().substr(2, 10);
                    window.localStorage.setItem('state', state);
                    return metadata.authorize_uri +
                        '?response_type=code' +
                        '&client_id=' + encodeURIComponent(metadata.client_id) +
                        '&redirect_uri=' + encodeURIComponent('https://' + window.location.host + '/api/oauth2/redirect/') +
                        '&state=' + encodeURIComponent(state) +
                        '&scope=' + encodeURIComponent(metadata.scope);
                }
                return instruction.params[0] + '/login';
            }
        }
        window.localStorage.removeItem('state');
        return true;
    };
    childRouter.mapUnknownRoutes('../404');

    return {
        shared: shared,
        router: childRouter,
        activate: function(mode) {
            var self = this;
            // Config
            self.shared.mode(mode);

            // Notifications
            $.pnotify.defaults.history = false;
            $.pnotify.defaults.styling = "bootstrap";

            // Fetch main API metadata
            $.ajax('/api/?timestamp=' + (new Date().getTime()), {
                    type: 'GET',
                    contentType: 'application/json',
                    timeout: 5000,
                    headers: { Accept: 'application/json' }
                })
                .done(function(metadata) {
                    shared.nodes = metadata.storagerouter_ips;
                    shared.identification(metadata.identification);
                    window.localStorage.setItem('nodes', JSON.stringify(shared.nodes));
                });
        }
    };
});
<|endoftext|>


<commit_before>'use strict';

/*
* When adding modules to the namespace, ensure that they are added in alphabetical order according to module name.
*/

// MODULES //

var setReadOnly = require( '@stdlib/utils/define-read-only-property' );

// MAIN //

/**
* Top-level namespace.
*
* @namespace pareto1
*/
var pareto = {};

/**
* @name cdf
* @memberof pareto1
* @readonly
* @type {Function}
* @see {@link module:@stdlib/math/base/dist/pareto-type1/cdf}
*/
setReadOnly( pareto, 'cdf', require( '@stdlib/math/base/dist/pareto-type1/cdf' ) );

/**
* @name entropy
* @memberof pareto1
* @readonly
* @type {Function}
* @see {@link module:@stdlib/math/base/dist/pareto-type1/entropy}
*/
setReadOnly( pareto, 'entropy', require( '@stdlib/math/base/dist/pareto-type1/entropy' ) );

/**
* @name kurtosis
* @memberof pareto1
* @readonly
* @type {Function}
* @see {@link module:@stdlib/math/base/dist/pareto-type1/kurtosis}
*/
setReadOnly( pareto, 'kurtosis', require( '@stdlib/math/base/dist/pareto-type1/kurtosis' ) );

/**
* @name mean
* @memberof pareto1
* @readonly
* @type {Function}
* @see {@link module:@stdlib/math/base/dist/pareto-type1/mean}
*/
setReadOnly( pareto, 'mean', require( '@stdlib/math/base/dist/pareto-type1/mean' ) );

/**
* @name median
* @memberof pareto1
* @readonly
* @type {Function}
* @see {@link module:@stdlib/math/base/dist/pareto-type1/median}
*/
setReadOnly( pareto, 'median', require( '@stdlib/math/base/dist/pareto-type1/median' ) );

/**
* @name mode
* @memberof pareto1
* @readonly
* @type {Function}
* @see {@link module:@stdlib/math/base/dist/pareto-type1/mode}
*/
setReadOnly( pareto, 'mode', require( '@stdlib/math/base/dist/pareto-type1/mode' ) );

/**
* @name pdf
* @memberof pareto1
* @readonly
* @type {Function}
* @see {@link module:@stdlib/math/base/dist/pareto-type1/pdf}
*/
setReadOnly( pareto, 'pdf', require( '@stdlib/math/base/dist/pareto-type1/pdf' ) );

/**
* @name quantile
* @memberof pareto1
* @readonly
* @type {Function}
* @see {@link module:@stdlib/math/base/dist/pareto-type1/quantile}
*/
setReadOnly( pareto, 'quantile', require( '@stdlib/math/base/dist/pareto-type1/quantile' ) );

/**
* @name skewness
* @memberof pareto1
* @readonly
* @type {Function}
* @see {@link module:@stdlib/math/base/dist/pareto-type1/skewness}
*/
setReadOnly( pareto, 'skewness', require( '@stdlib/math/base/dist/pareto-type1/skewness' ) );

/**
* @name variance
* @memberof pareto1
* @readonly
* @type {Function}
* @see {@link module:@stdlib/math/base/dist/pareto-type1/variance}
*/
setReadOnly( pareto, 'variance', require( '@stdlib/math/base/dist/pareto-type1/variance' ) );

// EXPORTS //

module.exports = pareto;
<commit_msg>Add to Pareto namespace<commit_after>'use strict';

/*
* When adding modules to the namespace, ensure that they are added in alphabetical order according to module name.
*/

// MODULES //

var setReadOnly = require( '@stdlib/utils/define-read-only-property' );

// MAIN //

/**
* Top-level namespace.
*
* @namespace pareto1
*/
var pareto = {};

/**
* @name cdf
* @memberof pareto1
* @readonly
* @type {Function}
* @see {@link module:@stdlib/math/base/dist/pareto-type1/cdf}
*/
setReadOnly( pareto, 'cdf', require( '@stdlib/math/base/dist/pareto-type1/cdf' ) );

/**
* @name entropy
* @memberof pareto1
* @readonly
* @type {Function}
* @see {@link module:@stdlib/math/base/dist/pareto-type1/entropy}
*/
setReadOnly( pareto, 'entropy', require( '@stdlib/math/base/dist/pareto-type1/entropy' ) );

/**
* @name kurtosis
* @memberof pareto1
* @readonly
* @type {Function}
* @see {@link module:@stdlib/math/base/dist/pareto-type1/kurtosis}
*/
setReadOnly( pareto, 'kurtosis', require( '@stdlib/math/base/dist/pareto-type1/kurtosis' ) );

/**
* @name mean
* @memberof pareto1
* @readonly
* @type {Function}
* @see {@link module:@stdlib/math/base/dist/pareto-type1/mean}
*/
setReadOnly( pareto, 'mean', require( '@stdlib/math/base/dist/pareto-type1/mean' ) );

/**
* @name median
* @memberof pareto1
* @readonly
* @type {Function}
* @see {@link module:@stdlib/math/base/dist/pareto-type1/median}
*/
setReadOnly( pareto, 'median', require( '@stdlib/math/base/dist/pareto-type1/median' ) );

/**
* @name mode
* @memberof pareto1
* @readonly
* @type {Function}
* @see {@link module:@stdlib/math/base/dist/pareto-type1/mode}
*/
setReadOnly( pareto, 'mode', require( '@stdlib/math/base/dist/pareto-type1/mode' ) );

/**
* @name Pareto1
* @memberof pareto1
* @readonly
* @type {Function}
* @see {@link module:@stdlib/math/base/dist/pareto-type1/ctor}
*/
setReadOnly( pareto, 'Pareto1', require( '@stdlib/math/base/dist/pareto-type1/ctor' ) );

/**
* @name pdf
* @memberof pareto1
* @readonly
* @type {Function}
* @see {@link module:@stdlib/math/base/dist/pareto-type1/pdf}
*/
setReadOnly( pareto, 'pdf', require( '@stdlib/math/base/dist/pareto-type1/pdf' ) );

/**
* @name quantile
* @memberof pareto1
* @readonly
* @type {Function}
* @see {@link module:@stdlib/math/base/dist/pareto-type1/quantile}
*/
setReadOnly( pareto, 'quantile', require( '@stdlib/math/base/dist/pareto-type1/quantile' ) );

/**
* @name skewness
* @memberof pareto1
* @readonly
* @type {Function}
* @see {@link module:@stdlib/math/base/dist/pareto-type1/skewness}
*/
setReadOnly( pareto, 'skewness', require( '@stdlib/math/base/dist/pareto-type1/skewness' ) );

/**
* @name variance
* @memberof pareto1
* @readonly
* @type {Function}
* @see {@link module:@stdlib/math/base/dist/pareto-type1/variance}
*/
setReadOnly( pareto, 'variance', require( '@stdlib/math/base/dist/pareto-type1/variance' ) );

// EXPORTS //

module.exports = pareto;
<|endoftext|>


<commit_before>'use strict';
var domv = require('domv');

//var StateAnchor = require('../StateAnchor');
var EditablePlainText = require('./editing/EditablePlainText');
var EditableTextEnumeration = require('./editing/EditableTextEnumeration');
var EditableBooleanText = require('./editing/EditableBooleanText');
var CompanyLogo = require('./CompanyLogo');
var CompanyShortMetaTable = require('./CompanyShortMetaTable');

require('static-reference')('./style/CompanyGridItem.less');

function CompanyGridItem(node, router, company)
{
        domv.Component.call(this, node, 'li');

        if (this.isCreationConstructor(node))
        {
                this.cls('CompanyGridItem');

                var div = this.shorthand('div');
                var h2 = this.shorthand('h2');

                this.attr('data-id', company._id);

                this.appendChild(
                        company.logoURL ? new CompanyLogo(this.document, company) : null,
                        h2('title',
                                this.titleLink = new EditablePlainText(
                                        this.document,
                                        'a',
                                        false,
                                        company.name + ''
                                ).attr({href: company.homepage ? company.homepage + '' : null})
                        ),
                        this.payoff = new EditablePlainText(this.document, 'p', true, (company.payoff || '') + '').cls('payoff'),
                        this.sectors = new EditableTextEnumeration(this.document, company.sectors).cls('sectors'),
                        this.meta = new CompanyShortMetaTable(this.document, router, company),
                        div('bottomButtons',
                                this.visible = new EditableBooleanText(this.document, company.visible, '✗ hidden', '✓ visible').cls('visible')
                        )
                );

                if (company.editableByCurrentUser)
                {
                        this.cls('canEdit');
                }
                else
                {
                        this.visible.style.display = 'none';
                }
        }
        else
        {
                this.assertHasClass('CompanyGridItem');
                this.titleLink = this.assertSelector('> .title > a', EditablePlainText);
                this.payoff = this.assertSelector('> .payoff', EditablePlainText);
                this.sectors = this.assertSelector('> .sectors', EditableTextEnumeration);
                this.meta = this.assertSelector('> .CompanyShortMetaTable', CompanyShortMetaTable, router);
                this.visible = this.assertSelector('> .bottomButtons > .visible', EditableBooleanText);
        }
}

module.exports = CompanyGridItem;
require('inherits')(CompanyGridItem, domv.Component);

// (not using get/set for this because only a subset of company is stored here)
CompanyGridItem.prototype.setValues = function(company)
{
        //todo logoUrl

        if ('homepage' in company)
        {
                this.titleLink.attr('href', company.homepage);
        }

        if ('name' in company)
        {
                this.titleLink.value = company.name;
        }

        if ('payoff' in company)
        {
                this.payoff.value = company.payoff;
        }

        if ('sectors' in company)
        {
                this.sectors.value = company.sectors;
        }

        if ('visible' in company)
        {
                this.visible.value = company.visible;
        }

        this.meta.setValues(company);
};

CompanyGridItem.prototype.getValues = function(since)
{
        var values = this.meta.getValues(since);
        // todo logoUrl & homepage

        if (this.titleLink.isChangedByUserSince(since))
        {
                values.name = this.titleLink.value;
        }

        if (this.payoff.isChangedByUserSince(since))
        {
                values.payoff = this.payoff.value;
        }

        if (this.sectors.isChangedByUserSince(since))
        {
                values.sectors = this.sectors.value;
        }

        if (this.visible.isChangedByUserSince(since))
        {
                values.visible = this.visible.value;
        }

        return Object.keys(values).length ? values : null;
};

Object.defineProperty(CompanyGridItem.prototype, 'id', {
        get: function()
        {
                return this.getAttr('data-id');
        }
});

Object.defineProperty(CompanyGridItem.prototype, 'canEdit', {
        get: function()
        {
                return this.hasClass('canEdit');
        },
        set: function(value)
        {
                return this.toggleClass('canEdit', value);
        }
});

Object.defineProperty(CompanyGridItem.prototype, 'editing', {
        configurable: true,
        get: function()
        {
                return this.hasClass('editing');
        },
        set: function(value)
        {
                value = !!value;
                this.toggleClass('editing', value);
                this.titleLink.editing = value;
                this.payoff.editing = value;
                this.meta.editing = value;
                this.sectors.editing = value;
                this.visible.editing = value;
        }
});
<commit_msg>rel nofollow for company links<commit_after>'use strict';
var domv = require('domv');

//var StateAnchor = require('../StateAnchor');
var EditablePlainText = require('./editing/EditablePlainText');
var EditableTextEnumeration = require('./editing/EditableTextEnumeration');
var EditableBooleanText = require('./editing/EditableBooleanText');
var CompanyLogo = require('./CompanyLogo');
var CompanyShortMetaTable = require('./CompanyShortMetaTable');

require('static-reference')('./style/CompanyGridItem.less');

function CompanyGridItem(node, router, company)
{
        domv.Component.call(this, node, 'li');

        if (this.isCreationConstructor(node))
        {
                this.cls('CompanyGridItem');

                var div = this.shorthand('div');
                var h2 = this.shorthand('h2');

                this.attr('data-id', company._id);

                this.appendChild(
                        company.logoURL ? new CompanyLogo(this.document, company) : null,
                        h2('title',
                                this.titleLink = new EditablePlainText(
                                        this.document,
                                        'a',
                                        false,
                                        company.name + ''
                                ).attr({
                                        href: company.homepage ? company.homepage + '' : null,
                                        rel: 'nofollow'
                                })
                        ),
                        this.payoff = new EditablePlainText(this.document, 'p', true, (company.payoff || '') + '').cls('payoff'),
                        this.sectors = new EditableTextEnumeration(this.document, company.sectors).cls('sectors'),
                        this.meta = new CompanyShortMetaTable(this.document, router, company),
                        div('bottomButtons',
                                this.visible = new EditableBooleanText(this.document, company.visible, '✗ hidden', '✓ visible').cls('visible')
                        )
                );

                if (company.editableByCurrentUser)
                {
                        this.cls('canEdit');
                }
                else
                {
                        this.visible.style.display = 'none';
                }
        }
        else
        {
                this.assertHasClass('CompanyGridItem');
                this.titleLink = this.assertSelector('> .title > a', EditablePlainText);
                this.payoff = this.assertSelector('> .payoff', EditablePlainText);
                this.sectors = this.assertSelector('> .sectors', EditableTextEnumeration);
                this.meta = this.assertSelector('> .CompanyShortMetaTable', CompanyShortMetaTable, router);
                this.visible = this.assertSelector('> .bottomButtons > .visible', EditableBooleanText);
        }
}

module.exports = CompanyGridItem;
require('inherits')(CompanyGridItem, domv.Component);

// (not using get/set for this because only a subset of company is stored here)
CompanyGridItem.prototype.setValues = function(company)
{
        //todo logoUrl

        if ('homepage' in company)
        {
                this.titleLink.attr('href', company.homepage);
        }

        if ('name' in company)
        {
                this.titleLink.value = company.name;
        }

        if ('payoff' in company)
        {
                this.payoff.value = company.payoff;
        }

        if ('sectors' in company)
        {
                this.sectors.value = company.sectors;
        }

        if ('visible' in company)
        {
                this.visible.value = company.visible;
        }

        this.meta.setValues(company);
};

CompanyGridItem.prototype.getValues = function(since)
{
        var values = this.meta.getValues(since);
        // todo logoUrl & homepage

        if (this.titleLink.isChangedByUserSince(since))
        {
                values.name = this.titleLink.value;
        }

        if (this.payoff.isChangedByUserSince(since))
        {
                values.payoff = this.payoff.value;
        }

        if (this.sectors.isChangedByUserSince(since))
        {
                values.sectors = this.sectors.value;
        }

        if (this.visible.isChangedByUserSince(since))
        {
                values.visible = this.visible.value;
        }

        return Object.keys(values).length ? values : null;
};

Object.defineProperty(CompanyGridItem.prototype, 'id', {
        get: function()
        {
                return this.getAttr('data-id');
        }
});

Object.defineProperty(CompanyGridItem.prototype, 'canEdit', {
        get: function()
        {
                return this.hasClass('canEdit');
        },
        set: function(value)
        {
                return this.toggleClass('canEdit', value);
        }
});

Object.defineProperty(CompanyGridItem.prototype, 'editing', {
        configurable: true,
        get: function()
        {
                return this.hasClass('editing');
        },
        set: function(value)
        {
                value = !!value;
                this.toggleClass('editing', value);
                this.titleLink.editing = value;
                this.payoff.editing = value;
                this.meta.editing = value;
                this.sectors.editing = value;
                this.visible.editing = value;
        }
});
<|endoftext|>


<commit_before>'use strict';

const Fs = require('fs');
const Os = require('os');
const Path = require('path');

const Code = require('code');
const Content = require('content');
const Form = require('multi-part');
const Hapi = require('hapi');
const Lab = require('lab');

const Blaine = require('../lib/');

const lab = exports.lab = Lab.script();

lab.experiment('blaine', () => {

    let server;

    lab.before((done) => {

        server = new Hapi.Server();
        server.connection();

        const plugin = {
            register: Blaine,
            options: {
                whitelist: ['png']
            }
        };

        const main = {
            config: {
                handler: (request, reply) => reply(request.payload),
                payload: {
                    output: 'data',
                    parse: false
                }
            },
            method: '*',
            path: '/main'
        };

        const ignore = {
            config: {
                handler: (request, reply) => reply(),
                payload: {
                    output: 'stream',
                    parse: true
                }
            },
            method: '*',
            path: '/ignore'
        };

        server.register(plugin, (err) => {

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

            server.route([main, ignore]);
            done();
        });
    });

    lab.test('should return control to the server if the route parses or does not handle in-memory request payload', (done) => {

        server.inject({ method: 'POST', url: '/ignore' }, (response) => {

            Code.expect(response.statusCode).to.equal(200);
            Code.expect(response.headers['content-validation']).to.equal('success');
            Code.expect(response.headers['content-type']).to.not.exist();
            done();
        });
    });

    lab.test('should return control to the server if the payload does not contain any file', (done) => {

        const form = new Form();
        form.append('foo', 'bar');

        server.inject({ headers: form.getHeaders(), method: 'POST', payload: form.get(), url: '/main' }, (response) => {

            Code.expect(response.statusCode).to.equal(200);
            Code.expect(response.headers['content-validation']).to.equal('success');
            Code.expect(Content.type(response.headers['content-type']).mime).to.equal('application/octet-stream');
            done();
        });
    });

    lab.test('should return error if the payload cannot be parsed', (done) => {

        const png = Path.join(Os.tmpdir(), 'foo.png');
        Fs.createWriteStream(png).end(new Buffer([0x89, 0x50]));

        const form = new Form();
        form.append('file', Fs.createReadStream(png));

        server.inject({ headers: { 'Content-Type': 'application/json' }, method: 'POST', payload: form.get(), url: '/main' }, (response) => {

            Code.expect(response.statusCode).to.equal(415);
            Code.expect(response.headers['content-validation']).to.not.exist();
            Code.expect(Content.type(response.headers['content-type']).mime).to.equal('application/json');
            done();
        });
    });

    lab.test('should return control to the server if all files the in payload are allowed', (done) => {

        const png = Path.join(Os.tmpdir(), 'foo.png');
        Fs.createWriteStream(png).end(new Buffer('89504e47', 'hex'));

        const form = new Form();
        form.append('file1', Fs.createReadStream(png));
        form.append('file2', Fs.createReadStream(png));
        form.append('foo', 'bar');

        server.inject({ headers: form.getHeaders(), method: 'POST', payload: form.get(), url: '/main' }, (response) => {

            Code.expect(response.statusCode).to.equal(200);
            Code.expect(response.headers['content-validation']).to.equal('success');
            Code.expect(Content.type(response.headers['content-type']).mime).to.equal('application/octet-stream');
            done();
        });
    });
});
<commit_msg>Replace deprecated Buffer constructor<commit_after>'use strict';

const Fs = require('fs');
const Os = require('os');
const Path = require('path');

const Code = require('code');
const Content = require('content');
const Form = require('multi-part');
const Hapi = require('hapi');
const Lab = require('lab');

const Blaine = require('../lib/');

const lab = exports.lab = Lab.script();

lab.experiment('blaine', () => {

    let server;

    lab.before((done) => {

        server = new Hapi.Server();
        server.connection();

        const plugin = {
            register: Blaine,
            options: {
                whitelist: ['png']
            }
        };

        const main = {
            config: {
                handler: (request, reply) => reply(request.payload),
                payload: {
                    output: 'data',
                    parse: false
                }
            },
            method: '*',
            path: '/main'
        };

        const ignore = {
            config: {
                handler: (request, reply) => reply(),
                payload: {
                    output: 'stream',
                    parse: true
                }
            },
            method: '*',
            path: '/ignore'
        };

        server.register(plugin, (err) => {

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

            server.route([main, ignore]);
            done();
        });
    });

    lab.test('should return control to the server if the route parses or does not handle in-memory request payload', (done) => {

        server.inject({ method: 'POST', url: '/ignore' }, (response) => {

            Code.expect(response.statusCode).to.equal(200);
            Code.expect(response.headers['content-validation']).to.equal('success');
            Code.expect(response.headers['content-type']).to.not.exist();
            done();
        });
    });

    lab.test('should return control to the server if the payload does not contain any file', (done) => {

        const form = new Form();
        form.append('foo', 'bar');

        server.inject({ headers: form.getHeaders(), method: 'POST', payload: form.get(), url: '/main' }, (response) => {

            Code.expect(response.statusCode).to.equal(200);
            Code.expect(response.headers['content-validation']).to.equal('success');
            Code.expect(Content.type(response.headers['content-type']).mime).to.equal('application/octet-stream');
            done();
        });
    });

    lab.test('should return error if the payload cannot be parsed', (done) => {

        const png = Path.join(Os.tmpdir(), 'foo.png');
        Fs.createWriteStream(png).end(Buffer.from('89504e47', 'hex'));

        const form = new Form();
        form.append('file', Fs.createReadStream(png));

        server.inject({ headers: { 'Content-Type': 'application/json' }, method: 'POST', payload: form.get(), url: '/main' }, (response) => {

            Code.expect(response.statusCode).to.equal(415);
            Code.expect(response.headers['content-validation']).to.not.exist();
            Code.expect(Content.type(response.headers['content-type']).mime).to.equal('application/json');
            done();
        });
    });

    lab.test('should return control to the server if all files the in payload are allowed', (done) => {

        const png = Path.join(Os.tmpdir(), 'foo.png');
        Fs.createWriteStream(png).end(Buffer.from('89504e47', 'hex'));

        const form = new Form();
        form.append('file1', Fs.createReadStream(png));
        form.append('file2', Fs.createReadStream(png));
        form.append('foo', 'bar');

        server.inject({ headers: form.getHeaders(), method: 'POST', payload: form.get(), url: '/main' }, (response) => {

            Code.expect(response.statusCode).to.equal(200);
            Code.expect(response.headers['content-validation']).to.equal('success');
            Code.expect(Content.type(response.headers['content-type']).mime).to.equal('application/octet-stream');
            done();
        });
    });
});
<|endoftext|>


<commit_before>/*jsl:declare require*/
/*jsl:declare process*/
/*jsl:declare console*/
/*jsl:declare module*/
/*jsl:declare JSON*/
function appRouter(opts) {
	var debug = require('debug')('kotn-base');
	var router = require('express').Router();
	var Promise = require('bluebird');
	var crypto = require('crypto');
	var url = require('url');
	var bodyParser = require('body-parser');
	var shopifyAPI = require('shopify-node-api');
	var installPath = opts.path;
	var hostName = opts.hostName;
	var appName = opts.name;
	var apiKey = opts.apiKey;
	var apiSecret = opts.apiSecret;

	function makeNonce() {
		var data = Math.floor(Math.random() * 1000) + new Date().getTime() * 1000;
		return crypto.createHash('md5').update(data.toFixed()).digest('hex');
	}

	function makeFinishURL(req) {
		var assumeHeroku = process.env.USE_PORT == 'N';
		var finishURL = url.format({
			protocol: assumeHeroku ? 'https:' : req.protocol,
			hostname: req.hostname,
			port: assumeHeroku ? '' : process.env.PORT,
			pathname: installPath + '/finish_auth'
		});
		debug('making finish url: ' + finishURL);
		return finishURL;
	}

	var session = opts.sessionHandler || require('./shopify-ui-session')();

	router.use(function(req, res, next){
		res.locals.ctx = res.locals.ctx || {};
		res.locals.ctx.apiKey = apiKey;
		next();
	});

	router.use(bodyParser.json({
		verify: function(req, res, buf, encoding) {
			var shopHMAC = req.get('x-shopify-hmac-sha256');
			if (!shopHMAC) return;
			if (req.get('x-kotn-webhook-verified')) throw 'Unexpected webhook verified header';
			var digest = crypto.createHmac('SHA256', apiSecret).update(buf).digest('base64');
			debug('checking digest: ' + digest.slice(0,5) + ' expect ' + req.get('x-shopify-hmac-sha256').slice(0,5) +' '+encoding);
			if (digest == req.get('x-shopify-hmac-sha256')) {
				req.headers['x-kotn-webhook-verified'] = '200';
			}else{
				debug(JSON.stringify(req.headers, null, ' '));
			}
		}
	})); // to support JSON-encoded bodies
	router.use(bodyParser.urlencoded({ // to support URL-encoded bodies
		extended: true
	}));

	router.get('/api', function(req, res) {
		debug('at ' + appName + ' api: ' + req.originalUrl);
		res.send('API Running');
	});
	router.get('/', session, function(req, res) {
		var indexURL = url.parse(req.originalUrl, true, true);
		if (!indexURL.pathname.endsWith('/')) {
			indexURL.pathname = indexURL.pathname + '/';
			res.redirect(url.format(indexURL)); // keep query and hash
			return;
		}
		if (req.session.shopConfig && req.session.shopConfig.shop) {
			processAuth(req.session.shopConfig.shop, req, res);
			return;
		}

		res.render('pages' + installPath + '/index', res.locals.ctx, function(err, html){
			if(err){
				console.error(err);
				res.redirect(installPath +'/preferences');
				return;
			}
			res.send(html);
		});
	});

	function processAuth(shop, req, res) {
		req.session.shopConfig = {
			shop: shop, // MYSHOP.myshopify.com
			shopify_api_key: apiKey,
			shopify_shared_secret: apiSecret,
			shopify_scope: opts.appScope || 'read_products,write_products',
			redirect_uri: makeFinishURL(req),
			nonce: makeNonce(),
			verbose: false
		};
		var Shopify = new shopifyAPI(req.session.shopConfig);
		var auth_url = Shopify.buildAuthURL();
		res.redirect(auth_url);
	}
	router.get('/auth', session, function(req, response) {
		if (req.query.shop) {
			req.headers['x-frame-options'] = 'ALLOW-FROM https://'+ req.query.shop +'.myshopify.com';

			var shop = req.query.shop.split('.')[0];
			var Shopify = new shopifyAPI({
				shop: shop, // MYSHOP.myshopify.com
				shopify_api_key: apiKey,
				shopify_shared_secret: apiSecret,
				verbose: false
			});
			if (Shopify.is_valid_signature(req.query, true)) {
				console.log('processing valid signature for ' + shop);
				processAuth(shop, req, response);
				return;
			}
			processAuth(shop, req, response);
			return;
		}
		response.render('pages' + installPath + '/auth', response.locals.ctx);
	});
	router.post('/auth', session, function(req, response) {
		var shop = req.body.shop;
		if (!shop) {
			response.redirect(installPath + '/auth');
			return;
		}
		processAuth(shop, req, response);
	});
	router.get('/finish_auth', session, function(req, res) {
		var Shopify = new shopifyAPI(req.session.shopConfig), // You need to pass in your config here
			query_params = req.query;
		Shopify.exchange_temporary_token(query_params, function(err, data) {
			if (err) {
				console.error(err);
				res.status(500).send(err);
				return;
			}
			req.session.shopConfig = Shopify.config;
			var token = data.access_token;
			req.db.func('get_shop_token', [appName, Shopify.config.shop, token], 1).
			then(function(result) {
				debug('exists token: ' + result.token.slice(0, 5) + ' is ' + (result.isnew ? 'new' : 'existing'));
				if (result.isnew) {
					return new Promise(function(resolve, reject) {
						var uninstallHook = {
							webhook: {
								topic: 'app/uninstalled',
								format: 'json',
								address: 'https://' + hostName + installPath + '/uninstall'
							}
						};
						Shopify.post('/admin/webhooks.json', uninstallHook, function(err, data) {
							if (err) {
								reject(err);
								return;
							}
							console.log('installed webhook: ' + data.webhook.id);
							resolve(Shopify.config);
						});
					}).then(function(config) {
						if (opts.onInstall) return opts.onInstall(req.db, config);
						return null;
					});
				} else {
					debug('token already installed for ' + Shopify.config.shop);
					return null;
				}
			}).
			then(function() {
				res.redirect(installPath + '/preferences');
			}, function(err) {
				res.status(500).send(err);
			});
		});
	});

	function getShopConfig(req, nosession, refreshConfig, defaults) {
		//console.log(JSON.stringify(req.headers));
		return new Promise(function(resolve, reject) {
			var config = (!nosession && req.session.shopConfig);
			var shop = req.query.shop || ('200' == req.get('x-kotn-webhook-verified') && req.get('x-shopify-shop-domain')) || (!nosession && req.session.shop);
			if (shop) shop = shop.split('.')[0];
			if(!shop && req.session && req.session.shopConfig) shop = req.session.shopConfig.shop;

			if ((shop && config && config.shop == shop) || (config && !shop)) {
				debug('have session config');
				if(!refreshConfig){
					resolve(config);
					return;
				}
			}
			if (!shop) {
				reject(new Error('invalid request'));
				return;
			}
			if(nosession && !(('200' == req.get('x-kotn-webhook-verified')) || validateRequest(req))){
				throw new Error('Missing required authentication');  // session requests need validation from auth token
			}
			lookupShopConfig(req.db, appName, shop, defaults).then(function(freshConfig) {
				if(!nosession) req.session.shopConfig = freshConfig;
				resolve(freshConfig);
			}, function(err) {
				console.error(err || new Error('no installed shop'));
				reject(err || new Error('no installed shop'));
			});
		});
	}

	function lookupShopConfig(db, appName, shopName, defaults) {
		return db.one('select id, token, is_subscribed, subscription_ends from app_installs where app = $1 and shop = $2 and uninstalled is null', [appName, shopName]).
		then(function(row) {
			debug('found shop token ' + row.token.slice(0, 5) + '...');
			var config = Object.assign({
				app_id: row.id,
				shop: shopName, // MYSHOP.myshopify.com
				shopify_api_key: apiKey,
				shopify_shared_secret: apiSecret,
				access_token: row.token,
				verbose: false,
				is_subscribed: row.is_subscribed,
				subscription_ends : row.subscription_ends
			}, defaults || {});
			return config;
		});
	}

	function validateRequest(req){
		var ts = parseInt(req.query.timestamp,10);
		var d = new Date().getTime()/1000;
		if(Math.abs(d-ts)  > 120){ // 2 minutes
			console.info('stale request');
			return false;
		}

		var keys = Object.keys(req.query);
		keys.sort();
		var parts = [];
		keys.forEach((k)=>{
			switch(k){
			case 'hmac':
				break;
			default:
				parts.push(k+'='+ req.query[k]);
			}
		});

		var digest = crypto.createHmac('sha256', apiSecret).update(parts.join('&')).digest('hex');
		return digest == req.query.hmac;

	}

	function validSession(req, res, next){
		var config = req.session && req.session.shopConfig;
		var applyConfigToSession = (config)=>{
			res.locals.shopConfig = config;
			req.session.shopConfig = config;
			session(req, res, next);
			return null;
		};
		var authenticatedRequest = validateRequest(req);
		var shop = req.query.shop || ('200' == req.get('x-kotn-webhook-verified') && req.get('x-shopify-shop-domain'));
		if(shop) shop = shop.split('.')[0];
		if(authenticatedRequest){
			// have auth info so use it
			console.log('auth in request');
			if(config && config.shop == shop) return applyConfigToSession(config);
			return lookupShopConfig(req.db, appName, shop).then(applyConfigToSession);
		}
		// not freshly validated.
		// use the existing session config

		if(!config){
			console.log('invalid prefs access');
			res.redirect(installPath + '/auth');
			return;
		}
		console.log('already valid prefs access');

		applyConfigToSession(config);

	}

	router.getCustHash = (function() {
		function generateHashSecret(shop) {
			if(!opts.inboundHashSeed) throw 'Missing inboundHashSeed from config';
			var hash = crypto.createHash('sha1');
			hash.update(shop);
			hash.update(opts.inboundHashSeed);
			hash.update(apiSecret);
			return hash.digest('hex');
		}

		function generateHash(shop, id){
			var hasher = crypto.createHash('sha1');
			hasher.update(id);
			hasher.update(generateHashSecret(shop));
			return hasher.digest('hex');
		}

		function check(shop, custId, hash) {
			var check = generateHash(shop, custId);
			// console.log('compare : '+ hash +" vs "+ check);
			return hash === check;
		}
		return {
			getSecret: generateHashSecret,
			makeHash: generateHash,
			checkHash: check
		};
	})();

	router.get('/preferences', session, validSession, function(req, res) {
		var config = res.locals.shopConfig;
		res.render('pages' + installPath + '/prefs', Object.assign({is_subscribed: config.is_subscribed, subscription_ends:config.subscription_ends}, res.locals.ctx));
	});

	router.post('/uninstall', function(req, res) {
		if ('200' != req.get('x-kotn-webhook-verified')) {
			console.error('invalid signature for uninstall');
			res.status(403).end();
			return;
		}
		var shop = req.get('x-shopify-shop-domain');
		if (!shop) {
			console.error('missing shop header for uninstall');
			res.status(400).send('missing shop');
			return;
		}
		debug('valid remove request for ' + shop);
		shop = shop.split('.')[0];
		req.db.func('uninstall_shop_token', [appName, shop], 1).
		then(function(result) {
			if (opts.onUninstall) {
				return opts.onUninstall(req.db, shop).then(function() {
					return result;
				});
			} else return result;
		}).then(function(result) {
			if (result || result.uninstall_shop_token) {
				console.log(shop + ' uninstalled ' + result.uninstall_shop_token);
			}
			res.setHeader('Content-Type', 'application/json');
			res.send(JSON.stringify({
				status: 'uninstalled'
			}));
		});
	});

	router.getShopConfig = getShopConfig;
	router.lookupShopConfig = lookupShopConfig;
	router.validSession = validSession;
	return router;
}
module.exports = appRouter;<commit_msg>debug lines on hmac access test<commit_after>/*jsl:declare require*/
/*jsl:declare process*/
/*jsl:declare console*/
/*jsl:declare module*/
/*jsl:declare JSON*/
function appRouter(opts) {
	var debug = require('debug')('kotn-base');
	var router = require('express').Router();
	var Promise = require('bluebird');
	var crypto = require('crypto');
	var url = require('url');
	var bodyParser = require('body-parser');
	var shopifyAPI = require('shopify-node-api');
	var installPath = opts.path;
	var hostName = opts.hostName;
	var appName = opts.name;
	var apiKey = opts.apiKey;
	var apiSecret = opts.apiSecret;

	function makeNonce() {
		var data = Math.floor(Math.random() * 1000) + new Date().getTime() * 1000;
		return crypto.createHash('md5').update(data.toFixed()).digest('hex');
	}

	function makeFinishURL(req) {
		var assumeHeroku = process.env.USE_PORT == 'N';
		var finishURL = url.format({
			protocol: assumeHeroku ? 'https:' : req.protocol,
			hostname: req.hostname,
			port: assumeHeroku ? '' : process.env.PORT,
			pathname: installPath + '/finish_auth'
		});
		debug('making finish url: ' + finishURL);
		return finishURL;
	}

	var session = opts.sessionHandler || require('./shopify-ui-session')();

	router.use(function(req, res, next){
		res.locals.ctx = res.locals.ctx || {};
		res.locals.ctx.apiKey = apiKey;
		next();
	});

	router.use(bodyParser.json({
		verify: function(req, res, buf, encoding) {
			var shopHMAC = req.get('x-shopify-hmac-sha256');
			if (!shopHMAC) return;
			if (req.get('x-kotn-webhook-verified')) throw 'Unexpected webhook verified header';
			var digest = crypto.createHmac('SHA256', apiSecret).update(buf).digest('base64');
			debug('checking digest: ' + digest.slice(0,5) + ' expect ' + req.get('x-shopify-hmac-sha256').slice(0,5) +' '+encoding);
			if (digest == req.get('x-shopify-hmac-sha256')) {
				req.headers['x-kotn-webhook-verified'] = '200';
			}else{
				debug(JSON.stringify(req.headers, null, ' '));
			}
		}
	})); // to support JSON-encoded bodies
	router.use(bodyParser.urlencoded({ // to support URL-encoded bodies
		extended: true
	}));

	router.get('/api', function(req, res) {
		debug('at ' + appName + ' api: ' + req.originalUrl);
		res.send('API Running');
	});
	router.get('/', session, function(req, res) {
		var indexURL = url.parse(req.originalUrl, true, true);
		if (!indexURL.pathname.endsWith('/')) {
			indexURL.pathname = indexURL.pathname + '/';
			res.redirect(url.format(indexURL)); // keep query and hash
			return;
		}
		if (req.session.shopConfig && req.session.shopConfig.shop) {
			processAuth(req.session.shopConfig.shop, req, res);
			return;
		}

		res.render('pages' + installPath + '/index', res.locals.ctx, function(err, html){
			if(err){
				console.error(err);
				res.redirect(installPath +'/preferences');
				return;
			}
			res.send(html);
		});
	});

	function processAuth(shop, req, res) {
		req.session.shopConfig = {
			shop: shop, // MYSHOP.myshopify.com
			shopify_api_key: apiKey,
			shopify_shared_secret: apiSecret,
			shopify_scope: opts.appScope || 'read_products,write_products',
			redirect_uri: makeFinishURL(req),
			nonce: makeNonce(),
			verbose: false
		};
		var Shopify = new shopifyAPI(req.session.shopConfig);
		var auth_url = Shopify.buildAuthURL();
		res.redirect(auth_url);
	}
	router.get('/auth', session, function(req, response) {
		if (req.query.shop) {
			req.headers['x-frame-options'] = 'ALLOW-FROM https://'+ req.query.shop +'.myshopify.com';

			var shop = req.query.shop.split('.')[0];
			var Shopify = new shopifyAPI({
				shop: shop, // MYSHOP.myshopify.com
				shopify_api_key: apiKey,
				shopify_shared_secret: apiSecret,
				verbose: false
			});
			if (Shopify.is_valid_signature(req.query, true)) {
				console.log('processing valid signature for ' + shop);
				processAuth(shop, req, response);
				return;
			}
			processAuth(shop, req, response);
			return;
		}
		response.render('pages' + installPath + '/auth', response.locals.ctx);
	});
	router.post('/auth', session, function(req, response) {
		var shop = req.body.shop;
		if (!shop) {
			response.redirect(installPath + '/auth');
			return;
		}
		processAuth(shop, req, response);
	});
	router.get('/finish_auth', session, function(req, res) {
		var Shopify = new shopifyAPI(req.session.shopConfig), // You need to pass in your config here
			query_params = req.query;
		Shopify.exchange_temporary_token(query_params, function(err, data) {
			if (err) {
				console.error(err);
				res.status(500).send(err);
				return;
			}
			req.session.shopConfig = Shopify.config;
			var token = data.access_token;
			req.db.func('get_shop_token', [appName, Shopify.config.shop, token], 1).
			then(function(result) {
				debug('exists token: ' + result.token.slice(0, 5) + ' is ' + (result.isnew ? 'new' : 'existing'));
				if (result.isnew) {
					return new Promise(function(resolve, reject) {
						var uninstallHook = {
							webhook: {
								topic: 'app/uninstalled',
								format: 'json',
								address: 'https://' + hostName + installPath + '/uninstall'
							}
						};
						Shopify.post('/admin/webhooks.json', uninstallHook, function(err, data) {
							if (err) {
								reject(err);
								return;
							}
							console.log('installed webhook: ' + data.webhook.id);
							resolve(Shopify.config);
						});
					}).then(function(config) {
						if (opts.onInstall) return opts.onInstall(req.db, config);
						return null;
					});
				} else {
					debug('token already installed for ' + Shopify.config.shop);
					return null;
				}
			}).
			then(function() {
				res.redirect(installPath + '/preferences');
			}, function(err) {
				res.status(500).send(err);
			});
		});
	});

	function getShopConfig(req, nosession, refreshConfig, defaults) {
		//console.log(JSON.stringify(req.headers));
		return new Promise(function(resolve, reject) {
			var config = (!nosession && req.session.shopConfig);
			var shop = req.query.shop || ('200' == req.get('x-kotn-webhook-verified') && req.get('x-shopify-shop-domain')) || (!nosession && req.session.shop);
			if (shop) shop = shop.split('.')[0];
			if(!shop && req.session && req.session.shopConfig) shop = req.session.shopConfig.shop;

			if ((shop && config && config.shop == shop) || (config && !shop)) {
				debug('have session config');
				if(!refreshConfig){
					resolve(config);
					return;
				}
			}
			if (!shop) {
				reject(new Error('invalid request'));
				return;
			}
			if(nosession && !(('200' == req.get('x-kotn-webhook-verified')) || validateRequest(req))){
				throw new Error('Missing required authentication');  // session requests need validation from auth token
			}
			lookupShopConfig(req.db, appName, shop, defaults).then(function(freshConfig) {
				if(!nosession) req.session.shopConfig = freshConfig;
				resolve(freshConfig);
			}, function(err) {
				console.error(err || new Error('no installed shop'));
				reject(err || new Error('no installed shop'));
			});
		});
	}

	function lookupShopConfig(db, appName, shopName, defaults) {
		return db.one('select id, token, is_subscribed, subscription_ends from app_installs where app = $1 and shop = $2 and uninstalled is null', [appName, shopName]).
		then(function(row) {
			debug('found shop token ' + row.token.slice(0, 5) + '...');
			var config = Object.assign({
				app_id: row.id,
				shop: shopName, // MYSHOP.myshopify.com
				shopify_api_key: apiKey,
				shopify_shared_secret: apiSecret,
				access_token: row.token,
				verbose: false,
				is_subscribed: row.is_subscribed,
				subscription_ends : row.subscription_ends
			}, defaults || {});
			return config;
		});
	}

	function validateRequest(req){
		var ts = parseInt(req.query.timestamp,10);
		var d = new Date().getTime()/1000;
		if(Math.abs(d-ts)  > 120){ // 2 minutes
			console.info('stale request');
			return false;
		}

		var keys = Object.keys(req.query);
		keys.sort();
		var parts = [];
		var haveHmac = false;
		keys.forEach((k)=>{
			switch(k){
			case 'hmac':
				haveHmac = true;
				break;
			default:
				parts.push(k+'='+ req.query[k]);
			}
		});

		var digest = crypto.createHmac('sha256', apiSecret).update(parts.join('&')).digest('hex');
		var digestMatch = digest == req.query.hmac;
		if(!digestMatch) console.error('invalid digest '+( haveHmac ? 'with' : 'without') +' hmac from '+parts.join('&'));
		return digestMatch;

	}

	function validSession(req, res, next){
		var config = req.session && req.session.shopConfig;
		var applyConfigToSession = (config)=>{
			res.locals.shopConfig = config;
			req.session.shopConfig = config;
			session(req, res, next);
			return null;
		};
		var authenticatedRequest = validateRequest(req);
		var shop = req.query.shop || ('200' == req.get('x-kotn-webhook-verified') && req.get('x-shopify-shop-domain'));
		if(shop) shop = shop.split('.')[0];
		if(authenticatedRequest){
			// have auth info so use it
			console.log('auth in request');
			if(config && config.shop == shop) return applyConfigToSession(config);
			return lookupShopConfig(req.db, appName, shop).then(applyConfigToSession);
		}
		// not freshly validated.
		// use the existing session config

		if(!config){
			console.log('invalid prefs access');
			res.redirect(installPath + '/auth');
			return;
		}
		console.log('already valid prefs access');

		applyConfigToSession(config);

	}

	router.getCustHash = (function() {
		function generateHashSecret(shop) {
			if(!opts.inboundHashSeed) throw 'Missing inboundHashSeed from config';
			var hash = crypto.createHash('sha1');
			hash.update(shop);
			hash.update(opts.inboundHashSeed);
			hash.update(apiSecret);
			return hash.digest('hex');
		}

		function generateHash(shop, id){
			var hasher = crypto.createHash('sha1');
			hasher.update(id);
			hasher.update(generateHashSecret(shop));
			return hasher.digest('hex');
		}

		function check(shop, custId, hash) {
			var check = generateHash(shop, custId);
			// console.log('compare : '+ hash +" vs "+ check);
			return hash === check;
		}
		return {
			getSecret: generateHashSecret,
			makeHash: generateHash,
			checkHash: check
		};
	})();

	router.get('/preferences', session, validSession, function(req, res) {
		var config = res.locals.shopConfig;
		res.render('pages' + installPath + '/prefs', Object.assign({is_subscribed: config.is_subscribed, subscription_ends:config.subscription_ends}, res.locals.ctx));
	});

	router.post('/uninstall', function(req, res) {
		if ('200' != req.get('x-kotn-webhook-verified')) {
			console.error('invalid signature for uninstall');
			res.status(403).end();
			return;
		}
		var shop = req.get('x-shopify-shop-domain');
		if (!shop) {
			console.error('missing shop header for uninstall');
			res.status(400).send('missing shop');
			return;
		}
		debug('valid remove request for ' + shop);
		shop = shop.split('.')[0];
		req.db.func('uninstall_shop_token', [appName, shop], 1).
		then(function(result) {
			if (opts.onUninstall) {
				return opts.onUninstall(req.db, shop).then(function() {
					return result;
				});
			} else return result;
		}).then(function(result) {
			if (result || result.uninstall_shop_token) {
				console.log(shop + ' uninstalled ' + result.uninstall_shop_token);
			}
			res.setHeader('Content-Type', 'application/json');
			res.send(JSON.stringify({
				status: 'uninstalled'
			}));
		});
	});

	router.getShopConfig = getShopConfig;
	router.lookupShopConfig = lookupShopConfig;
	router.validSession = validSession;
	return router;
}
module.exports = appRouter;<|endoftext|>


<commit_before>import Component from '@glimmer/component';
import extractAudioMetadata from '../utils/extract-audio-metadata';
import prettifyFileName from '../utils/prettify-file-name';
import {
    IMAGE_EXTENSIONS,
    IMAGE_MIME_TYPES
} from 'ghost-admin/components/gh-image-uploader';
import {TrackedObject} from 'tracked-built-ins';
import {action} from '@ember/object';
import {bind} from '@ember/runloop';
import {guidFor} from '@ember/object/internals';
import {isBlank} from '@ember/utils';
import {inject as service} from '@ember/service';
import {set} from '@ember/object';
import {task} from 'ember-concurrency-decorators';
import {tracked} from '@glimmer/tracking';
export const AUDIO_EXTENSIONS = ['mp3', 'wav', 'ogg', 'm4a'];
export const AUDIO_MIME_TYPES = ['audio/mp3', 'audio/mpeg', 'audio/ogg', 'audio/wav', 'audio/mp4'];

const PLACEHOLDERS = ['summer', 'mountains', 'ufo-attack'];

/* Payload
{
    src: 'https://ghostsite.com/media/...',
    title: '...',
    duration: 60,
    mimeType: 'audio/mp3',
    thumbnailSrc: 'https://ghostsite.com/content/media/...'
}
*/

// TODO: query file size limit from config and forbid uploads before they start

export default class KoenigCardAudioComponent extends Component {
    @service ajax;
    @service ghostPaths;

    @tracked files;
    @tracked thumbnailFiles;
    @tracked isDraggedOver = false;
    @tracked previewThumbnailSrc;

    // previewPayload stores all of the data collected until upload completes
    // at which point it will be saved to the real payload and the preview deleted
    @tracked previewPayload = new TrackedObject({});

    audioExtensions = AUDIO_EXTENSIONS;
    audioMimeTypes = AUDIO_MIME_TYPES;
    imageExtensions = IMAGE_EXTENSIONS;
    imageMimeTypes = IMAGE_MIME_TYPES;
    placeholder = PLACEHOLDERS[Math.floor(Math.random() * PLACEHOLDERS.length)]

    payloadAudioAttrs = ['src', 'title', 'duration', 'mimeType', 'thumbnailSrc'];

    get isEmpty() {
        return isBlank(this.args.payload.src);
    }

    get isIncomplete() {
        const {src} = this.args.payload;
        return isBlank(src);
    }

    get toolbar() {
        if (this.args.isEditing) {
            return false;
        }

        return {
            items: [{
                buttonClass: 'fw4 flex items-center white',
                icon: 'koenig/kg-edit',
                iconClass: 'fill-white',
                title: 'Edit',
                text: '',
                action: bind(this, this.args.editCard)
            }]
        };
    }

    get totalDuration() {
        let duration = this.args.payload.duration || this.previewPayload.duration;
        return this.getFormattedDuration(duration);
    }

    constructor() {
        super(...arguments);
        this.args.registerComponent(this);

        const payloadDefaults = {
            loop: false
        };

        Object.entries(payloadDefaults).forEach(([key, value]) => {
            if (this.args.payload[key] === undefined) {
                this.updatePayloadAttr(key, value);
            }
        });
    }

    @action
    didInsert(element) {
        // required for snippet rects to be calculated - editor reaches in to component,
        // expecting a non-Glimmer component with a .element property
        this.element = element;

        const {triggerBrowse, src, files} = this.args.payload;

        // don't persist editor-only payload attrs
        delete this.args.payload.triggerBrowse;
        delete this.args.payload.files;

        // the editor will add a triggerBrowse payload attr when inserting from
        // the card menu to save an extra click needed to open the file dialog
        if (triggerBrowse && !src && !files) {
            this.triggerAudioFileDialog();
        }

        // payload.files will be present if we have an externally set audio that
        // should be uploaded. Typically from a paste or drag/drop
        if (files) {
            this.files = files;
        }
    }

    @action
    registerAudioFileInput(input) {
        this._audioFileInput = input;
    }

    @action
    registerAudioThumbnailFileInput(input) {
        this._audioThumbnailFileInput = input;
    }

    @action
    triggerAudioFileDialog(event) {
        if (this._audioFileInput) {
            return this._audioFileInput.click();
        }

        const target = event?.target || this.element;

        const cardElem = target.closest('.__mobiledoc-card');
        const fileInput = cardElem?.querySelector('input[type="file"]');

        if (fileInput) {
            fileInput.click();
        }
    }

    @action
    triggerThumbnailFileDialog(event) {
        if (this._audioThumbnailFileInput) {
            return this._audioThumbnailFileInput.click();
        }

        const target = event?.target || this.element;

        const cardElem = target.closest('.__mobiledoc-card');
        const fileInput = cardElem?.querySelector('input[type="file"]');

        if (fileInput) {
            fileInput.click();
        }
    }

    @action
    deleteThumbnail() {
        this.updatePayloadAttr('thumbnailSrc', null);
    }

    @task
    *extractAudioMetadataTask(file) {
        return yield extractAudioMetadata(file);
    }

    @action
    async audioUploadStarted(files) {
        // extract metadata into temporary payload whilst audio is uploading
        const file = files[0];
        if (file) {
            // use a task here so we can wait for it later if the upload is quicker
            const metadata = await this.extractAudioMetadataTask.perform(file);

            this.previewPayload.duration = metadata?.duration;
            this.previewPayload.mimeType = metadata?.mimeType;
        }
    }

    getFormattedDuration(duration = 200) {
        const minutes = Math.floor(duration / 60);
        const seconds = Math.floor(duration - (minutes * 60));
        const formattedDuration = `${minutes}:${seconds}`;
        return formattedDuration;
    }

    @action
    async audioUploadCompleted([audio]) {
        this.previewPayload.src = audio.url;
        this.previewPayload.title = prettifyFileName(audio.fileName);

        // upload can complete before metadata is extracted when running locally
        await this.extractAudioMetadataTask.last;

        // save preview payload attrs into actual payload and create undo snapshot
        this.args.editor.run(() => {
            this.payloadAudioAttrs.forEach((attr) => {
                this.updatePayloadAttr(attr, this.previewPayload[attr]);
            });
        });

        // reset preview so we're back to rendering saved data
        this.previewPayload = new TrackedObject({});
    }

    @action
    async audioThumbnailUploadCompleted([thumb]) {
        const thumbnailGuid = Date.now().valueOf();
        this.previewPayload.thumbnailSrc = `${thumb.url}?v=${thumbnailGuid}`;

        // save preview payload attr into actual payload
        this.args.editor.run(() => {
            this.updatePayloadAttr('thumbnailSrc', this.previewPayload.thumbnailSrc);
        });

        // reset preview so we're back to rendering saved data
        this.previewPayload = new TrackedObject({});
    }

    @action
    audioUploadFailed() {
        // reset all attrs, creating an undo snapshot
        this.args.editor.run(() => {
            this.payloadAudioAttrs.forEach((attr) => {
                this.updatePayloadAttr(attr, null);
            });
        });
    }

    @action
    setAudioTitle(content) {
        this.updatePayloadAttr('title', content);
    }

    @action
    audioThumbnailUploadFailed() {
        this.previewPayload.thumbnailSrc = null;
        this.args.editor.run(() => {
            this.updatePayloadAttr('thumbnailSrc', this.previewPayload.thumbnailSrc);
        });
    }

    @task
    *uploadThumbnailFromBlobTask(audioUrl, fileBlob) {
        const formData = new FormData();
        formData.append('file', fileBlob, `media-thumbnail-${guidFor(this)}.jpg`);
        formData.append('url', audioUrl);

        const url = `${this.ghostPaths.apiRoot}/media/thumbnail/upload/`;

        const response = yield this.ajax.put(url, {
            data: formData,
            processData: false,
            contentType: false,
            dataType: 'json'
        });

        return response.media[0].url;
    }

    @action
    toggleLoop() {
        this.updatePayloadAttr('loop', !this.args.payload.loop);
    }

    @action
    updatePayloadAttr(attr, value) {
        const {payload} = this.args;

        set(payload, attr, value);

        // update the mobiledoc and stay in edit mode
        this.args.saveCard(payload, false);
    }

    @action
    dragOver(event) {
        if (!event.dataTransfer) {
            return;
        }

        event.stopPropagation();
        event.preventDefault();

        this.isDraggedOver = true;
    }

    @action
    dragLeave(event) {
        event.preventDefault();
        this.isDraggedOver = false;
    }

    @action
    drop(event) {
        event.preventDefault();
        event.stopPropagation();

        this.isDraggedOver = false;

        if (event.dataTransfer.files) {
            if (!this.args.payload.src) {
                this.files = [event.dataTransfer.files[0]];
            } else {
                this.thumbnailFiles = [event.dataTransfer.files[0]];
            }
        }
    }
}
<commit_msg>Added support for additional wav MIME types<commit_after>import Component from '@glimmer/component';
import extractAudioMetadata from '../utils/extract-audio-metadata';
import prettifyFileName from '../utils/prettify-file-name';
import {
    IMAGE_EXTENSIONS,
    IMAGE_MIME_TYPES
} from 'ghost-admin/components/gh-image-uploader';
import {TrackedObject} from 'tracked-built-ins';
import {action} from '@ember/object';
import {bind} from '@ember/runloop';
import {guidFor} from '@ember/object/internals';
import {isBlank} from '@ember/utils';
import {inject as service} from '@ember/service';
import {set} from '@ember/object';
import {task} from 'ember-concurrency-decorators';
import {tracked} from '@glimmer/tracking';
export const AUDIO_EXTENSIONS = ['mp3', 'wav', 'ogg', 'm4a'];
export const AUDIO_MIME_TYPES = ['audio/mp3', 'audio/mpeg', 'audio/ogg', 'audio/wav', 'audio/vnd.wav', 'audio/wave', 'audio/x-wav', 'audio/mp4'];

const PLACEHOLDERS = ['summer', 'mountains', 'ufo-attack'];

/* Payload
{
    src: 'https://ghostsite.com/media/...',
    title: '...',
    duration: 60,
    mimeType: 'audio/mp3',
    thumbnailSrc: 'https://ghostsite.com/content/media/...'
}
*/

// TODO: query file size limit from config and forbid uploads before they start

export default class KoenigCardAudioComponent extends Component {
    @service ajax;
    @service ghostPaths;

    @tracked files;
    @tracked thumbnailFiles;
    @tracked isDraggedOver = false;
    @tracked previewThumbnailSrc;

    // previewPayload stores all of the data collected until upload completes
    // at which point it will be saved to the real payload and the preview deleted
    @tracked previewPayload = new TrackedObject({});

    audioExtensions = AUDIO_EXTENSIONS;
    audioMimeTypes = AUDIO_MIME_TYPES;
    imageExtensions = IMAGE_EXTENSIONS;
    imageMimeTypes = IMAGE_MIME_TYPES;
    placeholder = PLACEHOLDERS[Math.floor(Math.random() * PLACEHOLDERS.length)]

    payloadAudioAttrs = ['src', 'title', 'duration', 'mimeType', 'thumbnailSrc'];

    get isEmpty() {
        return isBlank(this.args.payload.src);
    }

    get isIncomplete() {
        const {src} = this.args.payload;
        return isBlank(src);
    }

    get toolbar() {
        if (this.args.isEditing) {
            return false;
        }

        return {
            items: [{
                buttonClass: 'fw4 flex items-center white',
                icon: 'koenig/kg-edit',
                iconClass: 'fill-white',
                title: 'Edit',
                text: '',
                action: bind(this, this.args.editCard)
            }]
        };
    }

    get totalDuration() {
        let duration = this.args.payload.duration || this.previewPayload.duration;
        return this.getFormattedDuration(duration);
    }

    constructor() {
        super(...arguments);
        this.args.registerComponent(this);

        const payloadDefaults = {
            loop: false
        };

        Object.entries(payloadDefaults).forEach(([key, value]) => {
            if (this.args.payload[key] === undefined) {
                this.updatePayloadAttr(key, value);
            }
        });
    }

    @action
    didInsert(element) {
        // required for snippet rects to be calculated - editor reaches in to component,
        // expecting a non-Glimmer component with a .element property
        this.element = element;

        const {triggerBrowse, src, files} = this.args.payload;

        // don't persist editor-only payload attrs
        delete this.args.payload.triggerBrowse;
        delete this.args.payload.files;

        // the editor will add a triggerBrowse payload attr when inserting from
        // the card menu to save an extra click needed to open the file dialog
        if (triggerBrowse && !src && !files) {
            this.triggerAudioFileDialog();
        }

        // payload.files will be present if we have an externally set audio that
        // should be uploaded. Typically from a paste or drag/drop
        if (files) {
            this.files = files;
        }
    }

    @action
    registerAudioFileInput(input) {
        this._audioFileInput = input;
    }

    @action
    registerAudioThumbnailFileInput(input) {
        this._audioThumbnailFileInput = input;
    }

    @action
    triggerAudioFileDialog(event) {
        if (this._audioFileInput) {
            return this._audioFileInput.click();
        }

        const target = event?.target || this.element;

        const cardElem = target.closest('.__mobiledoc-card');
        const fileInput = cardElem?.querySelector('input[type="file"]');

        if (fileInput) {
            fileInput.click();
        }
    }

    @action
    triggerThumbnailFileDialog(event) {
        if (this._audioThumbnailFileInput) {
            return this._audioThumbnailFileInput.click();
        }

        const target = event?.target || this.element;

        const cardElem = target.closest('.__mobiledoc-card');
        const fileInput = cardElem?.querySelector('input[type="file"]');

        if (fileInput) {
            fileInput.click();
        }
    }

    @action
    deleteThumbnail() {
        this.updatePayloadAttr('thumbnailSrc', null);
    }

    @task
    *extractAudioMetadataTask(file) {
        return yield extractAudioMetadata(file);
    }

    @action
    async audioUploadStarted(files) {
        // extract metadata into temporary payload whilst audio is uploading
        const file = files[0];
        if (file) {
            // use a task here so we can wait for it later if the upload is quicker
            const metadata = await this.extractAudioMetadataTask.perform(file);

            this.previewPayload.duration = metadata?.duration;
            this.previewPayload.mimeType = metadata?.mimeType;
        }
    }

    getFormattedDuration(duration = 200) {
        const minutes = Math.floor(duration / 60);
        const seconds = Math.floor(duration - (minutes * 60));
        const formattedDuration = `${minutes}:${seconds}`;
        return formattedDuration;
    }

    @action
    async audioUploadCompleted([audio]) {
        this.previewPayload.src = audio.url;
        this.previewPayload.title = prettifyFileName(audio.fileName);

        // upload can complete before metadata is extracted when running locally
        await this.extractAudioMetadataTask.last;

        // save preview payload attrs into actual payload and create undo snapshot
        this.args.editor.run(() => {
            this.payloadAudioAttrs.forEach((attr) => {
                this.updatePayloadAttr(attr, this.previewPayload[attr]);
            });
        });

        // reset preview so we're back to rendering saved data
        this.previewPayload = new TrackedObject({});
    }

    @action
    async audioThumbnailUploadCompleted([thumb]) {
        const thumbnailGuid = Date.now().valueOf();
        this.previewPayload.thumbnailSrc = `${thumb.url}?v=${thumbnailGuid}`;

        // save preview payload attr into actual payload
        this.args.editor.run(() => {
            this.updatePayloadAttr('thumbnailSrc', this.previewPayload.thumbnailSrc);
        });

        // reset preview so we're back to rendering saved data
        this.previewPayload = new TrackedObject({});
    }

    @action
    audioUploadFailed() {
        // reset all attrs, creating an undo snapshot
        this.args.editor.run(() => {
            this.payloadAudioAttrs.forEach((attr) => {
                this.updatePayloadAttr(attr, null);
            });
        });
    }

    @action
    setAudioTitle(content) {
        this.updatePayloadAttr('title', content);
    }

    @action
    audioThumbnailUploadFailed() {
        this.previewPayload.thumbnailSrc = null;
        this.args.editor.run(() => {
            this.updatePayloadAttr('thumbnailSrc', this.previewPayload.thumbnailSrc);
        });
    }

    @task
    *uploadThumbnailFromBlobTask(audioUrl, fileBlob) {
        const formData = new FormData();
        formData.append('file', fileBlob, `media-thumbnail-${guidFor(this)}.jpg`);
        formData.append('url', audioUrl);

        const url = `${this.ghostPaths.apiRoot}/media/thumbnail/upload/`;

        const response = yield this.ajax.put(url, {
            data: formData,
            processData: false,
            contentType: false,
            dataType: 'json'
        });

        return response.media[0].url;
    }

    @action
    toggleLoop() {
        this.updatePayloadAttr('loop', !this.args.payload.loop);
    }

    @action
    updatePayloadAttr(attr, value) {
        const {payload} = this.args;

        set(payload, attr, value);

        // update the mobiledoc and stay in edit mode
        this.args.saveCard(payload, false);
    }

    @action
    dragOver(event) {
        if (!event.dataTransfer) {
            return;
        }

        event.stopPropagation();
        event.preventDefault();

        this.isDraggedOver = true;
    }

    @action
    dragLeave(event) {
        event.preventDefault();
        this.isDraggedOver = false;
    }

    @action
    drop(event) {
        event.preventDefault();
        event.stopPropagation();

        this.isDraggedOver = false;

        if (event.dataTransfer.files) {
            if (!this.args.payload.src) {
                this.files = [event.dataTransfer.files[0]];
            } else {
                this.thumbnailFiles = [event.dataTransfer.files[0]];
            }
        }
    }
}
<|endoftext|>