<commit_before>'use strict';

const fs = require('fs-extra');
const dns = require('dns');
const url = require('url');
const path = require('path');
const execa = require('execa');
const template = require('lodash/template');

const Promise = require('bluebird');
const NginxConfFile = require('nginx-conf').NginxConfFile;

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

class NginxExtension extends cli.Extension {
    setup(cmd, argv) {
        // ghost setup --local, skip
        if (argv.local) {
            return;
        }

        cmd.addStage('nginx', this.setupNginx.bind(this), null, 'Nginx');
        cmd.addStage('ssl', this.setupSSL.bind(this), 'nginx', 'SSL');
        cmd.addStage('ssl-renew', this.setupRenew.bind(this), 'ssl', 'automatic SSL renewal');
    }

    setupNginx(argv, ctx, task) {
        if (!this.isSupported()) {
            this.ui.log('Nginx is not installed. Skipping Nginx setup.', 'yellow');
            return task.skip();
        }

        let parsedUrl = url.parse(ctx.instance.config.get('url'));

        if (parsedUrl.port) {
            this.ui.log('Your url contains a port. Skipping Nginx setup.', 'yellow');
            return task.skip();
        }

        if (parsedUrl.pathname !== '/') {
            this.ui.log('The Nginx service does not support subdirectory configurations yet. Skipping Nginx setup.', 'yellow');
            return task.skip();
        }

        if (fs.existsSync(`/etc/nginx/sites-available/${parsedUrl.hostname}.conf`)) {
            this.ui.log('Nginx configuration already found for this url. Skipping Nginx setup.', 'yellow');
            return task.skip();
        }

        return Promise.fromNode((cb) => NginxConfFile.createFromSource('', cb)).then((conf) => {
            conf.nginx._add('server');

            let http = conf.nginx.server;

            http._add('listen', '80');
            http._add('listen', '[::]:80');
            http._add('server_name', parsedUrl.hostname);

            let rootPath = path.resolve(ctx.instance.dir, 'system', 'nginx-root');
            fs.ensureDirSync(rootPath);
            http._add('root', rootPath);

            http._add('location', '/');
            this._addProxyBlock(http.location, ctx.instance.config.get('server.port'));

            http._add('client_max_body_size', '50m');

            let confFile = `${parsedUrl.hostname}.conf`;

            return ctx.instance.template(
                conf.toString(),
                'nginx config',
                confFile,
                '/etc/nginx/sites-available'
            ).then((generated) => {
                if (!generated) {
                    this.ui.log('Nginx config not generated', 'yellow');
                    return;
                }

                ctx.instance.cliConfig.set('extension.nginx', true).save();

                return this.ui.sudo(`ln -sf /etc/nginx/sites-available/${confFile} /etc/nginx/sites-enabled/${confFile}`)
                    .then(() => this.restartNginx());
            });
        });
    }

    setupSSL(argv, ctx, task) {
        if (ctx.instance.cliConfig.get('extension.ssl', false)) {
            this.ui.log('SSL has already been set up, skipping', 'yellow');
            return task.skip();
        }

        if (!argv.prompt && !argv.sslemail) {
            this.ui.log('SSL email must be provided via the --sslemail option, skipping SSL setup', 'yellow');
            return task.skip();
        }

        let parsedUrl = url.parse(ctx.instance.config.get('url'));

        let confFile = `${parsedUrl.hostname}.conf`;
        let nginxConfPath = path.join(ctx.instance.dir, 'system', 'files', confFile);

        if (!fs.existsSync(nginxConfPath)) {
            if (ctx.single) {
                this.ui.log('Nginx config file does not exist, skipping SSL setup', 'yellow');
            }

            return task.skip();
        }

        let rootPath = path.resolve(ctx.instance.dir, 'system', 'nginx-root');
        const letsencrypt = require('./letsencrypt');

        return this.ui.listr([{
            title: 'Checking DNS resolution',
            task: (ctx) => {
                return Promise.fromNode(cb => dns.lookup(parsedUrl.hostname, {family: 4}, cb)).catch((error) => {
                    if (error.code !== 'ENOTFOUND') {
                        // Some other error
                        return Promise.reject(error);
                    }

                    // DNS entry has not populated yet, log an error and skip rest of the
                    // ssl configuration
                    let text = [
                        'Uh-oh! It looks like your domain isn\'t set up correctly yet.',
                        'Because of this, SSL setup won\'t work correctly. Once you\'ve set up your domain',
                        'and pointed it at this server\'s IP, try running `ghost setup ssl` again.'
                    ];

                    this.ui.log(text.join(' '), 'yellow');
                    ctx.dnsfail = true;
                });
            }
        }, {
            title: 'Preparing Nginx for Let\'s Encrypt SSL certificate creation',
            skip: (ctx) => ctx.dnsfail,
            task: (ctx) => {
                let promise;

                if (argv.sslemail) {
                    promise = Promise.resolve(argv.sslemail);
                } else {
                    promise = this.ui.prompt({
                        name: 'email',
                        type: 'input',
                        message: 'Enter your email (used for Let\'s Encrypt notifications)',
                        validate: value => Boolean(value) || 'You must supply an email'
                    }).then(answer => { argv.sslemail = answer.email; });
                }

                return promise.then(() => {
                    return Promise.fromCallback((cb) => NginxConfFile.create(nginxConfPath, cb)).then((conf) => {
                        ctx.ssl = {};

                        ctx.ssl.conf = conf;
                        ctx.ssl.http = conf.nginx.server;

                        // Don't add well-known block if it already exists
                        if (ctx.ssl.http.location.length === 1) {
                            ctx.ssl.http._add('location', '~ /.well-known');
                            ctx.ssl.http.location[1]._add('allow', 'all');
                        }
                    });
                });
            }
        }, {
            title: 'Restarting Nginx',
            skip: (ctx) => ctx.dnsfail,
            task: () => this.restartNginx()
        }, {
            title: 'Getting SSL Certificate',
            skip: (ctx) => ctx.dnsfail,
            task: () => {
                return letsencrypt(ctx.instance, argv.sslemail, argv.sslstaging).catch((error) => {
                    if (!(error instanceof cli.errors.ProcessError)) {
                        return Promise.reject(error);
                    }

                    // Ensure ~/.well-known location gets cleaned up
                    ctx.ssl.http._remove('location', 1);
                    return Promise.reject(error);
                });
            }
        }, {
            title: 'Generating Encryption Key (may take a few minutes)',
            skip: (ctx) => ctx.dnsfail,
            task: (ctx) => {
                ctx.ssl.dhparamOutFile = path.join(ctx.instance.dir, 'system', 'files', 'dhparam.pem');
                return execa.shell(`openssl dhparam -out ${ctx.ssl.dhparamOutFile} 2048`)
                    .catch((error) => Promise.reject(new cli.errors.ProcessError(error)));
            }
        }, {
            title: 'Writing SSL parameters',
            skip: (ctx) => ctx.dnsfail,
            task: (ctx) => {
                let sslParamsTemplate = template(fs.readFileSync(path.join(__dirname, 'ssl-params.conf.template'), 'utf8'));
                return ctx.instance.template(sslParamsTemplate({
                    dhparam: ctx.ssl.dhparamOutFile
                }), 'ssl parameters', 'ssl-params.conf');
            }
        }, {
            title: 'Updating Nginx with SSL config',
            skip: (ctx) => ctx.dnsfail,
            task: (ctx) => {
                // add ssl server block
                ctx.ssl.conf.nginx._add('server');
                let https = ctx.ssl.conf.nginx.server[1];

                // add listen directives
                https._add('listen', '443 ssl http2');
                https._add('listen', '[::]:443 ssl http2');
                https._add('server_name', parsedUrl.hostname);

                let letsencryptPath = path.join(ctx.instance.dir, 'system', 'letsencrypt');

                // add ssl cert directives
                https._add('ssl_certificate', path.join(letsencryptPath, 'fullchain.pem'));
                https._add('ssl_certificate_key', path.join(letsencryptPath, 'privkey.pem'));
                // add ssl-params snippet
                https._add('include', path.join(ctx.instance.dir, 'system', 'files', 'ssl-params.conf'));
                // add root directive
                https._add('root', rootPath);

                https._add('location', '/');
                this._addProxyBlock(https.location, ctx.instance.config.get('server.port'));

                https._add('client_max_body_size', '50m');

                https._add('location', '~ /.well-known');
                https.location[1]._add('allow', 'all');

                ctx.instance.cliConfig.set('extension.ssl', true)
                    .set('extension.sslemail', argv.sslemail).save();
            }
        }, {
            title: 'Restarting Nginx',
            skip: (ctx) => ctx.dnsfail,
            task: () => this.restartNginx()
        }], false);
    }

    setupRenew(argv, ctx) {
        return this._cron((cron) => {
            // Ensure any crontab with the same instance name is removed
            cron.remove({comment: ctx.instance.name});
            let cmd = `cd ${ctx.instance.dir} && ${process.argv.slice(0, 2).join(' ')} ssl-renew`;
            cron.create(cmd, '@monthly', ctx.instance.name);
        });
    }

    _addProxyBlock(location, port) {
        location._add('proxy_set_header', 'X-Forwarded-For $proxy_add_x_forwarded_for');
        location._add('proxy_set_header', 'X-Forwarded-Proto $scheme');
        location._add('proxy_set_header', 'X-Real-IP $remote_addr');
        location._add('proxy_set_header', 'Host $http_host');
        location._add('proxy_pass', `http://127.0.0.1:${port}`);
    }

    uninstall(instance) {
        if (!instance.cliConfig.get('extension.nginx', false)) {
            return;
        }

        let parsedUrl = url.parse(instance.config.get('url'));
        let confFile = `${parsedUrl.hostname}.conf`;

        let promises = [
            this._cron(cron => cron.remove({comment: instance.name}))
        ];

        if (fs.existsSync(`/etc/nginx/sites-available/${confFile}`)) {
            promises.push(
                this.ui.sudo(`rm /etc/nginx/sites-available/${confFile}`).then(() => {
                    return this.ui.sudo(`rm /etc/nginx/sites-enabled/${confFile}`);
                }).catch(
                    () => Promise.reject(new cli.errors.SystemError('Nginx config file link could not be removed, you will need to do this manually.'))
                )
            );
        }

        return Promise.all(promises).then(() => this.restartNginx());
    }

    _cron(fn) {
        const crontab = require('crontab');

        return Promise.fromCallback(cb => crontab.load(cb)).then((cron) => {
            fn(cron);
            return Promise.fromCallback(cb => cron.save(cb));
        });
    }

    restartNginx() {
        return this.ui.sudo('service nginx restart')
            .catch((error) => Promise.reject(new cli.errors.ProcessError(error)));
    }

    isSupported() {
        try {
            execa.shellSync('dpkg -l | grep nginx', {stdio: 'ignore'});
            return true;
        } catch (e) {
            return false;
        }
    }
}

module.exports = NginxExtension;
<commit_msg>fix(ssl): ensure well-known block actually gets created<commit_after>'use strict';

const fs = require('fs-extra');
const dns = require('dns');
const url = require('url');
const path = require('path');
const execa = require('execa');
const template = require('lodash/template');

const Promise = require('bluebird');
const NginxConfFile = require('nginx-conf').NginxConfFile;

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

class NginxExtension extends cli.Extension {
    setup(cmd, argv) {
        // ghost setup --local, skip
        if (argv.local) {
            return;
        }

        cmd.addStage('nginx', this.setupNginx.bind(this), null, 'Nginx');
        cmd.addStage('ssl', this.setupSSL.bind(this), 'nginx', 'SSL');
        cmd.addStage('ssl-renew', this.setupRenew.bind(this), 'ssl', 'automatic SSL renewal');
    }

    setupNginx(argv, ctx, task) {
        if (!this.isSupported()) {
            this.ui.log('Nginx is not installed. Skipping Nginx setup.', 'yellow');
            return task.skip();
        }

        let parsedUrl = url.parse(ctx.instance.config.get('url'));

        if (parsedUrl.port) {
            this.ui.log('Your url contains a port. Skipping Nginx setup.', 'yellow');
            return task.skip();
        }

        if (parsedUrl.pathname !== '/') {
            this.ui.log('The Nginx service does not support subdirectory configurations yet. Skipping Nginx setup.', 'yellow');
            return task.skip();
        }

        if (fs.existsSync(`/etc/nginx/sites-available/${parsedUrl.hostname}.conf`)) {
            this.ui.log('Nginx configuration already found for this url. Skipping Nginx setup.', 'yellow');
            return task.skip();
        }

        return Promise.fromNode((cb) => NginxConfFile.createFromSource('', cb)).then((conf) => {
            conf.nginx._add('server');

            let http = conf.nginx.server;

            http._add('listen', '80');
            http._add('listen', '[::]:80');
            http._add('server_name', parsedUrl.hostname);

            let rootPath = path.resolve(ctx.instance.dir, 'system', 'nginx-root');
            fs.ensureDirSync(rootPath);
            http._add('root', rootPath);

            http._add('location', '/');
            this._addProxyBlock(http.location, ctx.instance.config.get('server.port'));

            http._add('client_max_body_size', '50m');

            let confFile = `${parsedUrl.hostname}.conf`;

            return ctx.instance.template(
                conf.toString(),
                'nginx config',
                confFile,
                '/etc/nginx/sites-available'
            ).then((generated) => {
                if (!generated) {
                    this.ui.log('Nginx config not generated', 'yellow');
                    return;
                }

                ctx.instance.cliConfig.set('extension.nginx', true).save();

                return this.ui.sudo(`ln -sf /etc/nginx/sites-available/${confFile} /etc/nginx/sites-enabled/${confFile}`)
                    .then(() => this.restartNginx());
            });
        });
    }

    setupSSL(argv, ctx, task) {
        if (ctx.instance.cliConfig.get('extension.ssl', false)) {
            this.ui.log('SSL has already been set up, skipping', 'yellow');
            return task.skip();
        }

        if (!argv.prompt && !argv.sslemail) {
            this.ui.log('SSL email must be provided via the --sslemail option, skipping SSL setup', 'yellow');
            return task.skip();
        }

        let parsedUrl = url.parse(ctx.instance.config.get('url'));

        let confFile = `${parsedUrl.hostname}.conf`;
        let nginxConfPath = path.join(ctx.instance.dir, 'system', 'files', confFile);

        if (!fs.existsSync(nginxConfPath)) {
            if (ctx.single) {
                this.ui.log('Nginx config file does not exist, skipping SSL setup', 'yellow');
            }

            return task.skip();
        }

        let rootPath = path.resolve(ctx.instance.dir, 'system', 'nginx-root');
        const letsencrypt = require('./letsencrypt');

        return this.ui.listr([{
            title: 'Checking DNS resolution',
            task: (ctx) => {
                return Promise.fromNode(cb => dns.lookup(parsedUrl.hostname, {family: 4}, cb)).catch((error) => {
                    if (error.code !== 'ENOTFOUND') {
                        // Some other error
                        return Promise.reject(error);
                    }

                    // DNS entry has not populated yet, log an error and skip rest of the
                    // ssl configuration
                    let text = [
                        'Uh-oh! It looks like your domain isn\'t set up correctly yet.',
                        'Because of this, SSL setup won\'t work correctly. Once you\'ve set up your domain',
                        'and pointed it at this server\'s IP, try running `ghost setup ssl` again.'
                    ];

                    this.ui.log(text.join(' '), 'yellow');
                    ctx.dnsfail = true;
                });
            }
        }, {
            title: 'Preparing Nginx for Let\'s Encrypt SSL certificate creation',
            skip: (ctx) => ctx.dnsfail,
            task: (ctx) => {
                let promise;

                if (argv.sslemail) {
                    promise = Promise.resolve(argv.sslemail);
                } else {
                    promise = this.ui.prompt({
                        name: 'email',
                        type: 'input',
                        message: 'Enter your email (used for Let\'s Encrypt notifications)',
                        validate: value => Boolean(value) || 'You must supply an email'
                    }).then(answer => { argv.sslemail = answer.email; });
                }

                return promise.then(() => {
                    return Promise.fromCallback((cb) => NginxConfFile.create(nginxConfPath, cb)).then((conf) => {
                        ctx.ssl = {};

                        ctx.ssl.conf = conf;
                        ctx.ssl.http = conf.nginx.server;

                        let location = ctx.ssl.http.location;

                        // Don't add well-known block if it already exists
                        if (!Array.isArray(location) || location.length === 1) {
                            ctx.ssl.http._add('location', '~ /.well-known');
                            ctx.ssl.http.location[1]._add('allow', 'all');
                        }
                    });
                });
            }
        }, {
            title: 'Restarting Nginx',
            skip: (ctx) => ctx.dnsfail,
            task: () => this.restartNginx()
        }, {
            title: 'Getting SSL Certificate',
            skip: (ctx) => ctx.dnsfail,
            task: () => {
                return letsencrypt(ctx.instance, argv.sslemail, argv.sslstaging).catch((error) => {
                    if (!(error instanceof cli.errors.ProcessError)) {
                        return Promise.reject(error);
                    }

                    // Ensure ~/.well-known location gets cleaned up
                    ctx.ssl.http._remove('location', 1);
                    return Promise.reject(error);
                });
            }
        }, {
            title: 'Generating Encryption Key (may take a few minutes)',
            skip: (ctx) => ctx.dnsfail,
            task: (ctx) => {
                ctx.ssl.dhparamOutFile = path.join(ctx.instance.dir, 'system', 'files', 'dhparam.pem');
                return execa.shell(`openssl dhparam -out ${ctx.ssl.dhparamOutFile} 2048`)
                    .catch((error) => Promise.reject(new cli.errors.ProcessError(error)));
            }
        }, {
            title: 'Writing SSL parameters',
            skip: (ctx) => ctx.dnsfail,
            task: (ctx) => {
                let sslParamsTemplate = template(fs.readFileSync(path.join(__dirname, 'ssl-params.conf.template'), 'utf8'));
                return ctx.instance.template(sslParamsTemplate({
                    dhparam: ctx.ssl.dhparamOutFile
                }), 'ssl parameters', 'ssl-params.conf');
            }
        }, {
            title: 'Updating Nginx with SSL config',
            skip: (ctx) => ctx.dnsfail,
            task: (ctx) => {
                // add ssl server block
                ctx.ssl.conf.nginx._add('server');
                let https = ctx.ssl.conf.nginx.server[1];

                // add listen directives
                https._add('listen', '443 ssl http2');
                https._add('listen', '[::]:443 ssl http2');
                https._add('server_name', parsedUrl.hostname);

                let letsencryptPath = path.join(ctx.instance.dir, 'system', 'letsencrypt');

                // add ssl cert directives
                https._add('ssl_certificate', path.join(letsencryptPath, 'fullchain.pem'));
                https._add('ssl_certificate_key', path.join(letsencryptPath, 'privkey.pem'));
                // add ssl-params snippet
                https._add('include', path.join(ctx.instance.dir, 'system', 'files', 'ssl-params.conf'));
                // add root directive
                https._add('root', rootPath);

                https._add('location', '/');
                this._addProxyBlock(https.location, ctx.instance.config.get('server.port'));

                https._add('client_max_body_size', '50m');

                https._add('location', '~ /.well-known');
                https.location[1]._add('allow', 'all');

                ctx.instance.cliConfig.set('extension.ssl', true)
                    .set('extension.sslemail', argv.sslemail).save();
            }
        }, {
            title: 'Restarting Nginx',
            skip: (ctx) => ctx.dnsfail,
            task: () => this.restartNginx()
        }], false);
    }

    setupRenew(argv, ctx) {
        return this._cron((cron) => {
            // Ensure any crontab with the same instance name is removed
            cron.remove({comment: ctx.instance.name});
            let cmd = `cd ${ctx.instance.dir} && ${process.argv.slice(0, 2).join(' ')} ssl-renew`;
            cron.create(cmd, '@monthly', ctx.instance.name);
        });
    }

    _addProxyBlock(location, port) {
        location._add('proxy_set_header', 'X-Forwarded-For $proxy_add_x_forwarded_for');
        location._add('proxy_set_header', 'X-Forwarded-Proto $scheme');
        location._add('proxy_set_header', 'X-Real-IP $remote_addr');
        location._add('proxy_set_header', 'Host $http_host');
        location._add('proxy_pass', `http://127.0.0.1:${port}`);
    }

    uninstall(instance) {
        if (!instance.cliConfig.get('extension.nginx', false)) {
            return;
        }

        let parsedUrl = url.parse(instance.config.get('url'));
        let confFile = `${parsedUrl.hostname}.conf`;

        let promises = [
            this._cron(cron => cron.remove({comment: instance.name}))
        ];

        if (fs.existsSync(`/etc/nginx/sites-available/${confFile}`)) {
            promises.push(
                this.ui.sudo(`rm /etc/nginx/sites-available/${confFile}`).then(() => {
                    return this.ui.sudo(`rm /etc/nginx/sites-enabled/${confFile}`);
                }).catch(
                    () => Promise.reject(new cli.errors.SystemError('Nginx config file link could not be removed, you will need to do this manually.'))
                )
            );
        }

        return Promise.all(promises).then(() => this.restartNginx());
    }

    _cron(fn) {
        const crontab = require('crontab');

        return Promise.fromCallback(cb => crontab.load(cb)).then((cron) => {
            fn(cron);
            return Promise.fromCallback(cb => cron.save(cb));
        });
    }

    restartNginx() {
        return this.ui.sudo('service nginx restart')
            .catch((error) => Promise.reject(new cli.errors.ProcessError(error)));
    }

    isSupported() {
        try {
            execa.shellSync('dpkg -l | grep nginx', {stdio: 'ignore'});
            return true;
        } catch (e) {
            return false;
        }
    }
}

module.exports = NginxExtension;
<|endoftext|>


<commit_before>const _ = require('lodash');
const { convertRestQueryParams, buildQuery } = require('strapi-utils');

module.exports = ({ model, modelKey }) => {
  return {
    find(params, populate) {
      const withRelated =
        populate ||
        model.associations
          .filter(ast => ast.autoPopulate !== false)
          .map(ast => ast.alias);

      const filters = convertRestQueryParams(params);

      return model
        .query(buildQuery({ model, filters }))
        .fetchAll({ withRelated });
    },

    findOne(params, populate) {
      const withRelated =
        populate ||
        model.associations
          .filter(ast => ast.autoPopulate !== false)
          .map(ast => ast.alias);

      return model
        .forge({
          [model.primaryKey]: params[model.primaryKey] || params.id,
        })
        .fetch({
          withRelated,
        });
    },

    count(params = {}) {
      const { where } = convertRestQueryParams(params);

      return model.query(buildQuery({ model, filters: { where } })).count();
    },

    async create(values) {
      const relations = _.pick(
        values,
        model.associations.map(ast => ast.alias)
      );

      const data = _.omitBy(values, excludeExernalValues(model));

      const runCreate = async trx => {
        // Create entry with no-relational data.
        const entry = await model.forge(data).save(null, { transacting: trx });
        await createGroups(entry, values, { model, transacting: trx });
        return entry;
      };

      const db = strapi.connections[model.connection];
      const entry = await db.transaction(trx => runCreate(trx));

      return model.updateRelations({ id: entry.id, values: relations });
    },

    async update(params, values) {
      // Extract values related to relational data.
      const relations = _.pick(
        values,
        model.associations.map(ast => ast.alias)
      );

      const data = _.omitBy(values, excludeExernalValues(model));

      const runUpdate = async trx => {
        const entry = await model
          .forge(params)
          .save(data, { transacting: trx });
        await updateGroups(entry, values, { model, transacting: trx });
      };

      const db = strapi.connections[model.connection];
      await db.transaction(trx => runUpdate(trx));

      return model.updateRelations(
        Object.assign(params, { values: relations })
      );

      // Create relational data and return the entry.
    },

    async delete(params) {
      const values = {};
      model.associations.map(association => {
        switch (association.nature) {
          case 'oneWay':
          case 'oneToOne':
          case 'manyToOne':
          case 'oneToManyMorph':
            values[association.alias] = null;
            break;
          case 'oneToMany':
          case 'manyToMany':
          case 'manyToManyMorph':
            values[association.alias] = [];
            break;
          default:
        }
      });

      await model.updateRelations({ ...params, values });
      return model.forge(params).destroy();
    },

    search(params, populate) {
      // Convert `params` object to filters compatible with Bookshelf.
      const filters = strapi.utils.models.convertParams(modelKey, params);

      // Select field to populate.
      const withRelated =
        populate ||
        model.associations
          .filter(ast => ast.autoPopulate !== false)
          .map(ast => ast.alias);

      return model
        .query(qb => {
          buildSearchQuery(qb, model, params);

          if (filters.sort) {
            qb.orderBy(filters.sort.key, filters.sort.order);
          }

          if (filters.start) {
            qb.offset(_.toNumber(filters.start));
          }

          if (filters.limit) {
            qb.limit(_.toNumber(filters.limit));
          }
        })
        .fetchAll({
          withRelated,
        });
    },

    countSearch(params) {
      return model.query(qb => buildSearchQuery(qb, model, params)).count();
    },
  };
};

/**
 * util to build search query
 * @param {*} qb
 * @param {*} model
 * @param {*} params
 */
const buildSearchQuery = (qb, model, params) => {
  const query = (params._q || '').replace(/[^a-zA-Z0-9.-\s]+/g, '');

  const associations = model.associations.map(x => x.alias);

  const searchText = Object.keys(model._attributes)
    .filter(
      attribute =>
        attribute !== model.primaryKey && !associations.includes(attribute)
    )
    .filter(attribute =>
      ['string', 'text'].includes(model._attributes[attribute].type)
    );

  const searchInt = Object.keys(model._attributes)
    .filter(
      attribute =>
        attribute !== model.primaryKey && !associations.includes(attribute)
    )
    .filter(attribute =>
      ['integer', 'decimal', 'float'].includes(
        model._attributes[attribute].type
      )
    );

  const searchBool = Object.keys(model._attributes)
    .filter(
      attribute =>
        attribute !== model.primaryKey && !associations.includes(attribute)
    )
    .filter(attribute =>
      ['boolean'].includes(model._attributes[attribute].type)
    );

  if (!_.isNaN(_.toNumber(query))) {
    searchInt.forEach(attribute => {
      qb.orWhereRaw(attribute, _.toNumber(query));
    });
  }

  if (query === 'true' || query === 'false') {
    searchBool.forEach(attribute => {
      qb.orWhereRaw(attribute, _.toNumber(query === 'true'));
    });
  }

  // Search in columns with text using index.
  switch (model.client) {
    case 'mysql':
      qb.orWhereRaw(
        `MATCH(${searchText.join(',')}) AGAINST(? IN BOOLEAN MODE)`,
        `*${query}*`
      );
      break;
    case 'pg': {
      const searchQuery = searchText.map(attribute =>
        _.toLower(attribute) === attribute
          ? `to_tsvector(${attribute})`
          : `to_tsvector('${attribute}')`
      );

      qb.orWhereRaw(`${searchQuery.join(' || ')} @@ to_tsquery(?)`, query);
      break;
    }
  }
};

/**
 *
 * @param {Object} entry - the entity the groups are linked ot
 * @param {Object} values - Input values
 * @param {Object} options
 * @param {Object} options.model - the ref model
 * @param {Object} options.transacting - transaction option
 */
async function createGroups(entry, values, { model, transacting }) {
  const groupKeys = Object.keys(model.attributes).filter(key => {
    return model.attributes[key].type === 'group';
  });

  for (let key of groupKeys) {
    const attr = model.attributes[key];
    const { group, required = true, repeatable = true } = attr;

    const joinModel = entry[key].joinModel;
    const { foreignKey } = joinModel;
    const groupModel = strapi.groups[group];

    const createGroupAndLink = async ({ value, order }) => {
      return groupModel
        .forge()
        .save(value, { transacting })
        .then(group => {
          return joinModel.forge().save(
            {
              [foreignKey]: entry.id,
              slice_type: groupModel.collectionName,
              slice_id: group.id,
              field: key,
              order,
            },
            { transacting }
          );
        });
    };

    if (required === true && !_.has(values, key)) {
      const err = new Error(`Group ${key} is required`);
      err.status = 400;
      throw err;
    }

    if (!_.has(values, key)) continue;

    const groupValue = values[key];

    if (repeatable === true) {
      validateRepeatableInput(groupValue, { key, ...attr });
      await Promise.all(
        groupValue.map((value, idx) =>
          createGroupAndLink({ value, order: idx + 1 })
        )
      );
    } else {
      validateNonRepeatableInput(groupValue, { key, ...attr });
      await createGroupAndLink({ value: groupValue, order: 1 });
    }
  }
}

async function updateGroups(entry, values, { model, transacting }) {
  const groupKeys = Object.keys(model.attributes).filter(key => {
    return model.attributes[key].type === 'group';
  });

  for (let key of groupKeys) {
    // if key isn't present then don't change the current group data
    if (!_.has(values, key)) continue;

    const attr = model.attributes[key];
    const { group, repeatable = true } = attr;

    const joinModel = entry[key].joinModel;
    const { foreignKey } = joinModel;
    const groupModel = strapi.groups[group];

    const groupValue = values[key];

    const updateOreateGroupAndLink = async ({ value, order }) => {
      // check if value has an id then update else create
      if (_.has(value, groupModel.primaryKey)) {
        return groupModel
          .forge(value)
          .save(value, { transacting, patch: true, require: false })
          .then(group => {
            return joinModel
              .forge()
              .query({
                where: {
                  [foreignKey]: entry.id,
                  slice_type: groupModel.collectionName,
                  slice_id: group.id,
                  field: key,
                },
              })
              .save(
                {
                  order,
                },
                { transacting, patch: true, require: false }
              );
          });
      } else {
        return groupModel
          .forge()
          .save(value, { transacting })
          .then(group => {
            return joinModel.forge().save(
              {
                [foreignKey]: entry.id,
                slice_type: groupModel.collectionName,
                slice_id: group.id,
                field: key,
                order,
              },
              { transacting }
            );
          });
      }
    };

    if (repeatable === true) {
      validateRepeatableInput(groupValue, { key, ...attr });

      // TODO: delete the ones not kept and their links
      const idsToKeep = groupValue
        .filter(el => _.has(el, groupModel.primaryKey))
        .map(el => el[groupModel.primaryKey]);

      const idsToDelete = await joinModel
        .forge()
        .query(qb => {
          qb.where(foreignKey, entry.id)
            .andWhere('field', key)
            .whereNotIn('slice_id', idsToKeep);
        })
        .fetchAll({ transacting })
        .map(el => el.get('slice_id'));

      if (idsToDelete > 0) {
        await joinModel
          .forge()
          .query(qb => qb.whereIn('slice_id', idsToDelete))
          .destroy({ transacting, require: false });

        await groupModel
          .forge()
          .query(qb => qb.whereIn(groupModel.primaryKey, idsToDelete))
          .destroy({ transacting, require: false });
      }

      await Promise.all(
        groupValue.map((value, idx) => {
          return updateOreateGroupAndLink({ value, order: idx + 1 });
        })
      );
    } else {
      validateNonRepeatableInput(groupValue, { key, ...attr });

      // TODO: if the value has an id => update
      // else delete and create

      // await updateOreateGroupAndLink({ value: groupValue, order: 1 });
    }
  }
  return;
}

function validateRepeatableInput(value, { key, min, max }) {
  if (!Array.isArray(value)) {
    const err = new Error(`Group ${key} is repetable. Expected an array`);
    err.status = 400;
    throw err;
  }

  if (min && value.length < min) {
    const err = new Error(`Group ${key} must contain at least ${min} items`);
    err.status = 400;
    throw err;
  }
  if (max && value.length > max) {
    const err = new Error(`Group ${key} must contain at most ${max} items`);
    err.status = 400;
    throw err;
  }
}

function validateNonRepeatableInput(value, { key, required }) {
  if (typeof value !== 'object') {
    const err = new Error(`Group ${key} should be an object`);
    err.status = 400;
    throw err;
  }

  if (required === true && value === null) {
    const err = new Error(`Group ${key} is required`);
    err.status = 400;
    throw err;
  }
}

function excludeExernalValues(model) {
  // TODO: allow timestamps update someday

  const assocKeys = model.associations.map(ast => ast.alias);
  const groupKeys = Object.keys(model.attributes).filter(key => {
    return model.attributes[key].type === 'group';
  });

  const excludedKeys = assocKeys.concat(groupKeys);
  return (value, key) => excludedKeys.includes(key);
}
<commit_msg>Update single and clean delete old groups<commit_after>const _ = require('lodash');
const { convertRestQueryParams, buildQuery } = require('strapi-utils');

module.exports = ({ model, modelKey }) => {
  return {
    find(params, populate) {
      const withRelated =
        populate ||
        model.associations
          .filter(ast => ast.autoPopulate !== false)
          .map(ast => ast.alias);

      const filters = convertRestQueryParams(params);

      return model
        .query(buildQuery({ model, filters }))
        .fetchAll({ withRelated });
    },

    findOne(params, populate) {
      const withRelated =
        populate ||
        model.associations
          .filter(ast => ast.autoPopulate !== false)
          .map(ast => ast.alias);

      return model
        .forge({
          [model.primaryKey]: params[model.primaryKey] || params.id,
        })
        .fetch({
          withRelated,
        });
    },

    count(params = {}) {
      const { where } = convertRestQueryParams(params);

      return model.query(buildQuery({ model, filters: { where } })).count();
    },

    async create(values) {
      const relations = _.pick(
        values,
        model.associations.map(ast => ast.alias)
      );

      const data = _.omitBy(values, excludeExernalValues(model));

      const runCreate = async trx => {
        // Create entry with no-relational data.
        const entry = await model.forge(data).save(null, { transacting: trx });
        await createGroups(entry, values, { model, transacting: trx });
        return entry;
      };

      const db = strapi.connections[model.connection];
      const entry = await db.transaction(trx => runCreate(trx));

      return model.updateRelations({ id: entry.id, values: relations });
    },

    async update(params, values) {
      // Extract values related to relational data.
      const relations = _.pick(
        values,
        model.associations.map(ast => ast.alias)
      );

      const data = _.omitBy(values, excludeExernalValues(model));

      const runUpdate = async trx => {
        const entry = await model
          .forge(params)
          .save(data, { transacting: trx });
        await updateGroups(entry, values, { model, transacting: trx });
      };

      const db = strapi.connections[model.connection];
      await db.transaction(trx => runUpdate(trx));

      return model.updateRelations(
        Object.assign(params, { values: relations })
      );

      // Create relational data and return the entry.
    },

    async delete(params) {
      const values = {};
      model.associations.map(association => {
        switch (association.nature) {
          case 'oneWay':
          case 'oneToOne':
          case 'manyToOne':
          case 'oneToManyMorph':
            values[association.alias] = null;
            break;
          case 'oneToMany':
          case 'manyToMany':
          case 'manyToManyMorph':
            values[association.alias] = [];
            break;
          default:
        }
      });

      await model.updateRelations({ ...params, values });
      return model.forge(params).destroy();
    },

    search(params, populate) {
      // Convert `params` object to filters compatible with Bookshelf.
      const filters = strapi.utils.models.convertParams(modelKey, params);

      // Select field to populate.
      const withRelated =
        populate ||
        model.associations
          .filter(ast => ast.autoPopulate !== false)
          .map(ast => ast.alias);

      return model
        .query(qb => {
          buildSearchQuery(qb, model, params);

          if (filters.sort) {
            qb.orderBy(filters.sort.key, filters.sort.order);
          }

          if (filters.start) {
            qb.offset(_.toNumber(filters.start));
          }

          if (filters.limit) {
            qb.limit(_.toNumber(filters.limit));
          }
        })
        .fetchAll({
          withRelated,
        });
    },

    countSearch(params) {
      return model.query(qb => buildSearchQuery(qb, model, params)).count();
    },
  };
};

/**
 * util to build search query
 * @param {*} qb
 * @param {*} model
 * @param {*} params
 */
const buildSearchQuery = (qb, model, params) => {
  const query = (params._q || '').replace(/[^a-zA-Z0-9.-\s]+/g, '');

  const associations = model.associations.map(x => x.alias);

  const searchText = Object.keys(model._attributes)
    .filter(
      attribute =>
        attribute !== model.primaryKey && !associations.includes(attribute)
    )
    .filter(attribute =>
      ['string', 'text'].includes(model._attributes[attribute].type)
    );

  const searchInt = Object.keys(model._attributes)
    .filter(
      attribute =>
        attribute !== model.primaryKey && !associations.includes(attribute)
    )
    .filter(attribute =>
      ['integer', 'decimal', 'float'].includes(
        model._attributes[attribute].type
      )
    );

  const searchBool = Object.keys(model._attributes)
    .filter(
      attribute =>
        attribute !== model.primaryKey && !associations.includes(attribute)
    )
    .filter(attribute =>
      ['boolean'].includes(model._attributes[attribute].type)
    );

  if (!_.isNaN(_.toNumber(query))) {
    searchInt.forEach(attribute => {
      qb.orWhereRaw(attribute, _.toNumber(query));
    });
  }

  if (query === 'true' || query === 'false') {
    searchBool.forEach(attribute => {
      qb.orWhereRaw(attribute, _.toNumber(query === 'true'));
    });
  }

  // Search in columns with text using index.
  switch (model.client) {
    case 'mysql':
      qb.orWhereRaw(
        `MATCH(${searchText.join(',')}) AGAINST(? IN BOOLEAN MODE)`,
        `*${query}*`
      );
      break;
    case 'pg': {
      const searchQuery = searchText.map(attribute =>
        _.toLower(attribute) === attribute
          ? `to_tsvector(${attribute})`
          : `to_tsvector('${attribute}')`
      );

      qb.orWhereRaw(`${searchQuery.join(' || ')} @@ to_tsquery(?)`, query);
      break;
    }
  }
};

/**
 *
 * @param {Object} entry - the entity the groups are linked ot
 * @param {Object} values - Input values
 * @param {Object} options
 * @param {Object} options.model - the ref model
 * @param {Object} options.transacting - transaction option
 */
async function createGroups(entry, values, { model, transacting }) {
  const groupKeys = Object.keys(model.attributes).filter(key => {
    return model.attributes[key].type === 'group';
  });

  for (let key of groupKeys) {
    const attr = model.attributes[key];
    const { group, required = true, repeatable = true } = attr;

    const joinModel = entry[key].joinModel;
    const { foreignKey } = joinModel;
    const groupModel = strapi.groups[group];

    const createGroupAndLink = async ({ value, order }) => {
      return groupModel
        .forge()
        .save(value, { transacting })
        .then(group => {
          return joinModel.forge().save(
            {
              [foreignKey]: entry.id,
              slice_type: groupModel.collectionName,
              slice_id: group.id,
              field: key,
              order,
            },
            { transacting }
          );
        });
    };

    if (required === true && !_.has(values, key)) {
      const err = new Error(`Group ${key} is required`);
      err.status = 400;
      throw err;
    }

    if (!_.has(values, key)) continue;

    const groupValue = values[key];

    if (repeatable === true) {
      validateRepeatableInput(groupValue, { key, ...attr });
      await Promise.all(
        groupValue.map((value, idx) =>
          createGroupAndLink({ value, order: idx + 1 })
        )
      );
    } else {
      validateNonRepeatableInput(groupValue, { key, ...attr });
      await createGroupAndLink({ value: groupValue, order: 1 });
    }
  }
}

async function updateGroups(entry, values, { model, transacting }) {
  const groupKeys = Object.keys(model.attributes).filter(key => {
    return model.attributes[key].type === 'group';
  });

  for (let key of groupKeys) {
    // if key isn't present then don't change the current group data
    if (!_.has(values, key)) continue;

    const attr = model.attributes[key];
    const { group, repeatable = true } = attr;

    const joinModel = entry[key].joinModel;
    const { foreignKey } = joinModel;
    const groupModel = strapi.groups[group];

    const groupValue = values[key];

    const updateOreateGroupAndLink = async ({ value, order }) => {
      // check if value has an id then update else create
      if (_.has(value, groupModel.primaryKey)) {
        return groupModel
          .forge(value)
          .save(value, { transacting, patch: true, require: false })
          .then(group => {
            return joinModel
              .forge()
              .query({
                where: {
                  [foreignKey]: entry.id,
                  slice_type: groupModel.collectionName,
                  slice_id: group.id,
                  field: key,
                },
              })
              .save(
                {
                  order,
                },
                { transacting, patch: true, require: false }
              );
          });
      } else {
        return groupModel
          .forge()
          .save(value, { transacting })
          .then(group => {
            return joinModel.forge().save(
              {
                [foreignKey]: entry.id,
                slice_type: groupModel.collectionName,
                slice_id: group.id,
                field: key,
                order,
              },
              { transacting }
            );
          });
      }
    };

    if (repeatable === true) {
      validateRepeatableInput(groupValue, { key, ...attr });

      await deleteOldGroups(entry, groupValue, {
        key,
        joinModel,
        groupModel,
        transacting,
      });

      await Promise.all(
        groupValue.map((value, idx) => {
          return updateOreateGroupAndLink({ value, order: idx + 1 });
        })
      );
    } else {
      validateNonRepeatableInput(groupValue, { key, ...attr });

      await deleteOldGroups(entry, groupValue, {
        key,
        joinModel,
        groupModel,
        transacting,
      });

      await updateOreateGroupAndLink({ value: groupValue, order: 1 });
    }
  }
  return;
}

async function deleteOldGroups(
  entry,
  groupValue,
  { key, joinModel, groupModel, transacting }
) {
  const groupArr = Array.isArray(groupValue) ? groupValue : [groupValue];

  const idsToKeep = groupArr
    .filter(el => _.has(el, groupModel.primaryKey))
    .map(el => el[groupModel.primaryKey]);

  const allIds = await joinModel
    .forge()
    .query(qb => {
      qb.where(joinModel.foreignKey, entry.id).andWhere('field', key);
    })
    .fetchAll({ transacting })
    .map(el => el.get('slice_id'));

  // verify the provided ids are realted to this entity.
  idsToKeep.forEach(id => {
    if (!allIds.includes(id)) {
      const err = new Error(
        `Some of the provided groups in ${key} are not related to the entity`
      );
      err.status = 400;
      throw err;
    }
  });

  const idsToDelete = _.difference(allIds, idsToKeep);
  if (idsToDelete > 0) {
    await joinModel
      .forge()
      .query(qb => qb.whereIn('slice_id', idsToDelete))
      .destroy({ transacting, require: false });

    await groupModel
      .forge()
      .query(qb => qb.whereIn(groupModel.primaryKey, idsToDelete))
      .destroy({ transacting, require: false });
  }
}

function validateRepeatableInput(value, { key, min, max }) {
  if (!Array.isArray(value)) {
    const err = new Error(`Group ${key} is repetable. Expected an array`);
    err.status = 400;
    throw err;
  }

  if (min && value.length < min) {
    const err = new Error(`Group ${key} must contain at least ${min} items`);
    err.status = 400;
    throw err;
  }
  if (max && value.length > max) {
    const err = new Error(`Group ${key} must contain at most ${max} items`);
    err.status = 400;
    throw err;
  }
}

function validateNonRepeatableInput(value, { key, required }) {
  if (typeof value !== 'object') {
    const err = new Error(`Group ${key} should be an object`);
    err.status = 400;
    throw err;
  }

  if (required === true && value === null) {
    const err = new Error(`Group ${key} is required`);
    err.status = 400;
    throw err;
  }
}

function excludeExernalValues(model) {
  // TODO: allow timestamps update someday

  const assocKeys = model.associations.map(ast => ast.alias);
  const groupKeys = Object.keys(model.attributes).filter(key => {
    return model.attributes[key].type === 'group';
  });

  const excludedKeys = assocKeys.concat(groupKeys);
  return (value, key) => excludedKeys.includes(key);
}
<|endoftext|>


<commit_before>var url = require('url');
var UserModel = require('../models/user').model;
var jwt = require('jwt-simple');

exports.isAuthenticated = function (req, res, next) {
    var token = exports.retrieveToken(req);

    if (token) {
        try {
            var decoded = jwt.decode(token, 'UBEAT_TOKEN_SECRET');

            if (decoded.exp <= Date.now()) {
                return res.status(401).send({
                    errorCode: 'ACCESS_DENIED',
                    message: 'Access token is expired'
                }).end();
            }

            UserModel.findOne({ '_id': decoded.iss }, function (err, user) {
                if (!err) {
                    if (user) {
                        req.user = user;
                        return next()
                    } else {
                        return res.status(401).send({
                            errorCode: 'ACCESS_DENIED',
                            message: 'User associated with token was not found'
                        }).end();
                    }
                }
            });
        } catch (err) {
            return res.status(401).send({
                errorCode: 'ACCESS_DENIED',
                message: 'Error retrieving user associated with token'
            }).end();
        }

    } else {
        return res.status(401).send({
            errorCode: 'ACCESS_DENIED',
            message: 'Access token is missing'
        }).end();
    }
};

exports.retrieveToken = function (req) {
    var parsed_url = url.parse(req.url, true);

    return (req.body && req.body.access_token) ||
        parsed_url.query.access_token ||
        req.headers['authorization'];
};
<commit_msg>* Remove unnecessary calls to end()<commit_after>var url = require('url');
var UserModel = require('../models/user').model;
var jwt = require('jwt-simple');

exports.isAuthenticated = function (req, res, next) {
    var token = exports.retrieveToken(req);

    if (token) {
        try {
            var decoded = jwt.decode(token, 'UBEAT_TOKEN_SECRET');

            if (decoded.exp <= Date.now()) {
                return res.status(401).send({
                    errorCode: 'ACCESS_DENIED',
                    message: 'Access token is expired'
                });
            }

            UserModel.findOne({ '_id': decoded.iss }, function (err, user) {
                if (!err) {
                    if (user) {
                        req.user = user;
                        return next()
                    } else {
                        return res.status(401).send({
                            errorCode: 'ACCESS_DENIED',
                            message: 'User associated with token was not found'
                        });
                    }
                }
            });
        } catch (err) {
            return res.status(401).send({
                errorCode: 'ACCESS_DENIED',
                message: 'Error retrieving user associated with token'
            });
        }

    } else {
        return res.status(401).send({
            errorCode: 'ACCESS_DENIED',
            message: 'Access token is missing'
        });
    }
};

exports.retrieveToken = function (req) {
    var parsed_url = url.parse(req.url, true);

    return (req.body && req.body.access_token) ||
        parsed_url.query.access_token ||
        req.headers['authorization'];
};
<|endoftext|>


<commit_before>'use strict'

var Base = require('vigour-js/lib/base')

module.exports = function (load, event, client) {
  console.log('\n\n\nFIRE SUBS!'.blue.bold.inverse, client.key.magenta.inverse, this._path, load)
  // if (!client.connection) {
  //   console.log('---> no client yet!  the stuff that needs fixing (make subs part of clients)'.red.bold)
  // }
  var adapter = this.getAdapter()
  if (!adapter) {
    return
  }

  // console.log('letsgo')

  var eventorigin = this.parseEvent(event, adapter)

  if (eventorigin &&
    client.key !== eventorigin &&
    client.connection &&
    client !== event.client &&
      (client.connection && client.connection.origin.upstream && !client.connection.origin.upstream._input)
  ) {
    for (var i in load) {
      let data = load[i].data
      if (load[i].origin._input instanceof Base) {
        console.log('recusrce!')
        recursive(load[i].origin, event, adapter, client, data)
      } else {
        if (data === void 0 && load[i].origin) {
          console.log('ok normal? send empty object? or cancel', load[i].origin._path)
        }
        client.send(load[i].origin, adapter.parent, data, event)
      }
    }
  } else {
    console.log('blocked upstream or own event'.red.inverse)
    // console.log('load is:'.bold, load)
  }
}

function recursive (obs, event, adapter, client, data) {
  if (obs._input instanceof Base) {
    recursive(obs._input, event, adapter, client, data)
  }
  if (!obs._input && data === null) {
    // console.log('referenced was here is now removed we need the path!!!'.red)
    // console.log(obs._input, obs._path && obs._path.join('.'))
  }
  console.log('recusrce! send', obs, obs._input)

  client.send(obs, adapter.parent, obs._input, event)
}
<commit_msg>fixed reference test<commit_after>'use strict'

var Base = require('vigour-js/lib/base')

module.exports = function (load, event, client) {
  console.log('\n\n\nFIRE SUBS!'.blue.bold.inverse, client.key.magenta.inverse, this._path, load)
  // if (!client.connection) {
  //   console.log('---> no client yet!  the stuff that needs fixing (make subs part of clients)'.red.bold)
  // }
  var adapter = this.getAdapter()
  if (!adapter) {
    return
  }

  // console.log('letsgo')

  var eventorigin = this.parseEvent(event, adapter)

  if (eventorigin &&
    client.key !== eventorigin &&
    client.connection &&
    client !== event.client &&
      (client.connection && client.connection.origin.upstream && !client.connection.origin.upstream._input)
  ) {
    for (var i in load) {
      let data = load[i].data
      if (load[i].origin._input instanceof Base) {
        console.log('recusrce!')
        recursive(load[i].origin, event, adapter, client, data)
      } else {
        if (data === void 0 && load[i].origin) {
          console.log('ok normal? send empty object? or cancel', load[i].origin._path)
        }
        client.send(load[i].origin, adapter.parent, data, event)
      }
    }
  } else {
    console.log('blocked upstream or own event'.red.inverse)
    // console.log('load is:'.bold, load)
  }
}

function recursive (obs, event, adapter, client, data) {
  if (obs._input instanceof Base) {
    recursive(obs._input, event, adapter, client, data)
  }
  if (!obs._input && data === null) {
    // console.log('referenced was here is now removed we need the path!!!'.red)
    // console.log(obs._input, obs._path && obs._path.join('.'))
  }
  console.log('recusrce! send', obs, obs._input)
  // have to send the referenced thing as well just to be sure!
  if (obs._input && obs._input._input === null) {
    // client.send(obs, adapter.parent, void 0, event)
  } else {
    client.send(obs, adapter.parent, obs._input, event)
  }
}
<|endoftext|>


<commit_before>setLabels = function () {
  // name Session.prototype.send
  var originalSend = MeteorX.Session.prototype.send;
  MeteorX.Session.prototype.send = function kadira_Session_send () {
    var args = Array.prototype.slice.call(arguments);
    return originalSend.apply(this, args);
  }

  // name mongodb.Connection.createDataHandler
  var mongodb = MongoInternals.NpmModule;
  var originalCreateDataHandler = mongodb.Connection.createDataHandler;
  mongodb.Connection.createDataHandler = function () {
    var args = Array.prototype.slice.call(arguments);
    var originalHandler = originalCreateDataHandler.apply(this, args);
    return function kadira_MongoDB_dataHandler () {
      var args = Array.prototype.slice.call(arguments);
      return originalHandler.apply(this, args);
    }
  }

  // name Multiplexer initial adds
  var originalSendAdds = MeteorX.Multiplexer.prototype._sendAdds;
  MeteorX.Multiplexer.prototype._sendAdds = function kadira_Multiplexer_sendAdds () {
    var args = Array.prototype.slice.call(arguments);
    return originalSendAdds.apply(this, args);
  }

  // name MongoConnection insert
  var originalMongoInsert = MeteorX.MongoConnection.prototype._insert;
  MeteorX.MongoConnection.prototype._insert = function kadira_MongoConnection_insert () {
    var args = Array.prototype.slice.call(arguments);
    return originalMongoInsert.apply(this, args);
  }

  // name MongoConnection update
  var originalMongoUpdate = MeteorX.MongoConnection.prototype._update;
  MeteorX.MongoConnection.prototype._update = function kadira_MongoConnection_update () {
    var args = Array.prototype.slice.call(arguments);
    return originalMongoUpdate.apply(this, args);
  }

  // name MongoConnection remove
  var originalMongoRemove = MeteorX.MongoConnection.prototype._remove;
  MeteorX.MongoConnection.prototype._remove = function kadira_MongoConnection_remove () {
    var args = Array.prototype.slice.call(arguments);
    return originalMongoRemove.apply(this, args);
  }

  // name Pubsub added
  var originalPubsubAdded = MeteorX.Session.prototype.sendAdded;
  MeteorX.Session.prototype.sendAdded = function kadira_Session_sendAdded () {
    var args = Array.prototype.slice.call(arguments);
    return originalPubsubAdded.apply(this, args);
  }

  // name Pubsub changed
  var originalPubsubChanged = MeteorX.Session.prototype.sendChanged;
  MeteorX.Session.prototype.sendChanged = function kadira_Session_sendChanged () {
    var args = Array.prototype.slice.call(arguments);
    return originalPubsubChanged.apply(this, args);
  }

  // name Pubsub removed
  var originalPubsubRemoved = MeteorX.Session.prototype.sendRemoved;
  MeteorX.Session.prototype.sendRemoved = function kadira_Session_sendRemoved () {
    var args = Array.prototype.slice.call(arguments);
    return originalPubsubRemoved.apply(this, args);
  }

  // name MongoCursor methods
  var cursorProto = MeteorX.MongoCursor.prototype;
  var originalCursorForEach =  cursorProto.forEach;
  cursorProto.forEach = function kadira_Cursor_forEach () {
    var args = Array.prototype.slice.call(arguments);
    return originalCursorForEach.apply(this, args);
  }

  var originalCursorMap =  cursorProto.map;
  cursorProto.map = function kadira_Cursor_map () {
    var args = Array.prototype.slice.call(arguments);
    return originalCursorMap.apply(this, args);
  }

  var originalCursorFetch =  cursorProto.fetch;
  cursorProto.fetch = function kadira_Cursor_fetch () {
    var args = Array.prototype.slice.call(arguments);
    return originalCursorFetch.apply(this, args);
  }

  var originalCursorCount =  cursorProto.count;
  cursorProto.count = function kadira_Cursor_count () {
    var args = Array.prototype.slice.call(arguments);
    return originalCursorCount.apply(this, args);
  }

  var originalCursorObserveChanges =  cursorProto.observeChanges;
  cursorProto.observeChanges = function kadira_Cursor_observeChanges () {
    var args = Array.prototype.slice.call(arguments);
    return originalCursorObserveChanges.apply(this, args);
  }

  var originalCursorObserve =  cursorProto.observe;
  cursorProto.observe = function kadira_Cursor_observe () {
    var args = Array.prototype.slice.call(arguments);
    return originalCursorObserve.apply(this, args);
  }

  var originalCursorRewind =  cursorProto.rewind;
  cursorProto.rewind = function kadira_Cursor_rewind () {
    var args = Array.prototype.slice.call(arguments);
    return originalCursorRewind.apply(this, args);
  }
}
<commit_msg>Refactor - remove unnecessary variable<commit_after>setLabels = function () {
  // name Session.prototype.send
  var originalSend = MeteorX.Session.prototype.send;
  MeteorX.Session.prototype.send = function kadira_Session_send () {
    var args = Array.prototype.slice.call(arguments);
    return originalSend.apply(this, args);
  }

  // name mongodb.Connection.createDataHandler
  var mongodb = MongoInternals.NpmModule;
  var originalCreateDataHandler = mongodb.Connection.createDataHandler;
  mongodb.Connection.createDataHandler = function () {
    var args = Array.prototype.slice.call(arguments);
    var originalHandler = originalCreateDataHandler.apply(this, args);
    return function kadira_MongoDB_dataHandler () {
      var args = Array.prototype.slice.call(arguments);
      return originalHandler.apply(this, args);
    }
  }

  // name Multiplexer initial adds
  var originalSendAdds = MeteorX.Multiplexer.prototype._sendAdds;
  MeteorX.Multiplexer.prototype._sendAdds = function kadira_Multiplexer_sendAdds () {
    var args = Array.prototype.slice.call(arguments);
    return originalSendAdds.apply(this, args);
  }

  // name MongoConnection insert
  var originalMongoInsert = MeteorX.MongoConnection.prototype._insert;
  MeteorX.MongoConnection.prototype._insert = function kadira_MongoConnection_insert () {
    var args = Array.prototype.slice.call(arguments);
    return originalMongoInsert.apply(this, args);
  }

  // name MongoConnection update
  var originalMongoUpdate = MeteorX.MongoConnection.prototype._update;
  MeteorX.MongoConnection.prototype._update = function kadira_MongoConnection_update () {
    var args = Array.prototype.slice.call(arguments);
    return originalMongoUpdate.apply(this, args);
  }

  // name MongoConnection remove
  var originalMongoRemove = MeteorX.MongoConnection.prototype._remove;
  MeteorX.MongoConnection.prototype._remove = function kadira_MongoConnection_remove () {
    var args = Array.prototype.slice.call(arguments);
    return originalMongoRemove.apply(this, args);
  }

  // name Pubsub added
  var originalPubsubAdded = MeteorX.Session.prototype.sendAdded;
  MeteorX.Session.prototype.sendAdded = function kadira_Session_sendAdded () {
    var args = Array.prototype.slice.call(arguments);
    return originalPubsubAdded.apply(this, args);
  }

  // name Pubsub changed
  var originalPubsubChanged = MeteorX.Session.prototype.sendChanged;
  MeteorX.Session.prototype.sendChanged = function kadira_Session_sendChanged () {
    var args = Array.prototype.slice.call(arguments);
    return originalPubsubChanged.apply(this, args);
  }

  // name Pubsub removed
  var originalPubsubRemoved = MeteorX.Session.prototype.sendRemoved;
  MeteorX.Session.prototype.sendRemoved = function kadira_Session_sendRemoved () {
    var args = Array.prototype.slice.call(arguments);
    return originalPubsubRemoved.apply(this, args);
  }

  // name MongoCursor forEach
  var originalCursorForEach = MeteorX.MongoCursor.prototype.forEach;
  MeteorX.MongoCursor.prototype.forEach = function kadira_Cursor_forEach () {
    var args = Array.prototype.slice.call(arguments);
    return originalCursorForEach.apply(this, args);
  }

  // name MongoCursor map
  var originalCursorMap = MeteorX.MongoCursor.prototype.map;
  MeteorX.MongoCursor.prototype.map = function kadira_Cursor_map () {
    var args = Array.prototype.slice.call(arguments);
    return originalCursorMap.apply(this, args);
  }

  // name MongoCursor fetch
  var originalCursorFetch = MeteorX.MongoCursor.prototype.fetch;
  MeteorX.MongoCursor.prototype.fetch = function kadira_Cursor_fetch () {
    var args = Array.prototype.slice.call(arguments);
    return originalCursorFetch.apply(this, args);
  }

  // name MongoCursor count
  var originalCursorCount = MeteorX.MongoCursor.prototype.count;
  MeteorX.MongoCursor.prototype.count = function kadira_Cursor_count () {
    var args = Array.prototype.slice.call(arguments);
    return originalCursorCount.apply(this, args);
  }

  // name MongoCursor observeChanges
  var originalCursorObserveChanges = MeteorX.MongoCursor.prototype.observeChanges;
  MeteorX.MongoCursor.prototype.observeChanges = function kadira_Cursor_observeChanges () {
    var args = Array.prototype.slice.call(arguments);
    return originalCursorObserveChanges.apply(this, args);
  }

  // name MongoCursor observe
  var originalCursorObserve = MeteorX.MongoCursor.prototype.observe;
  MeteorX.MongoCursor.prototype.observe = function kadira_Cursor_observe () {
    var args = Array.prototype.slice.call(arguments);
    return originalCursorObserve.apply(this, args);
  }

  // name MongoCursor rewind
  var originalCursorRewind = MeteorX.MongoCursor.prototype.rewind;
  MeteorX.MongoCursor.prototype.rewind = function kadira_Cursor_rewind () {
    var args = Array.prototype.slice.call(arguments);
    return originalCursorRewind.apply(this, args);
  }
}
<|endoftext|>


<commit_before>// Provides a backend that plays songs with AV and mp3.js.
// Expects aurora.js dependency as a module argument.
var Backend = (function (AV) {

  // Make the object we will export as our singleton sound-playing backend thingy.
  var backend = {
    // This tracks if we have been started
    ready: false,
    // This holds the callbacks to call after we boot up
    readyCallbacks: [],
    // This gets filled in with an IpfsNode instance that stores and retrieves
    // files.
    ipfsnode: null,
    // This gets filled in with the communication channel to the player
    // frontend.
    ipc: null,
    // This holds the one AV player that is allowed to make noise. Not to be
    // confused with the Player, which is the frontend,
    globalPlayer: null,
    // This determines if we should play the first globalPlayer we manage to
    // make, as soon as we can.
    playNow: false,
    // This holds a song database
    database: {
      // This holds all the records in an array
      allSongs: [],
      // This maps from URL string to index in array
      byUrl: {}
    }
  }

  /**
   * Download and parse some JSON, and return a promise.
   */
  backend.getJSON = function (url) {
    return new Promise(function (resolve, reject) {
      var xhr = new XMLHttpRequest()
      xhr.open("GET", url)
      xhr.overrideMimeType("text/json")
      xhr.onload = function () {
        // This function gets the XHR as this, and fires when the XHR is
        // done, one way or the other.

        console.log("Got " + url + " with " + xhr.statusText)

        // Grab the status
        var status = this.status
        if (status >= 200 && status < 300) {
          // Status code is in the success range
          var parsed
          try {
            // Parse the JSON
            parsed = JSON.parse(xhr.responseText)
          } catch(err) {
            // We can't parse this JSON
            reject(err)
          }

          // OK we parsed it!
          resolve(parsed)
        } else {
          // Something else happened (server returned error)
          // We're upposed to reject with Error objects
          reject(Error("XHR refused: " + xhr.statusText))
        }
      }
      xhr.onerror = function () {
        // Something happened and the request errored out
        reject(Error("XHR error: " + xhr.statusText))
      }

      // Kick off the request.
      console.log("Getting " + url)
      xhr.send()
    })
  }

  /**
   * Given a Song object with title, album, artist, and url, add the song to the
   * metadata database if it doesn't already exist.
   */
  backend.loadSong = function(song) {
    // We ID songs uniquely by URL for now
    if (!backend.database.byUrl.hasOwnProperty(song.url)) {
      // Keep the new song
      backend.database.allSongs.push(song)
      // Record where it is in the database
      backend.database.byUrl[song.url] = backend.database.allSongs.length - 1
    }
  }

  /**
   * Download song records as JSON from the given URL and keep them in the local
   * database. URL may be ipfs:.
   * Calls the callback with an error, or null if everything works.
   */
  backend.loadSongs = function (url, callback) {
    var parts = url.split(':')
    if (parts[0] == 'ipfs') {
      // This is an ipfs URL.
      backend.ipfsnode.ipfs.files.cat(parts[1], (err, fileData) => {
        if (err) {
          return callback(err)
        }

        // We got something back. Parse it as JSON
        var parsed = JSON.parse(fileData.toString('utf-8'))

        for(var i = 0; i < parsed.length; i++) {
          // Add each item to the songs database if necessary
          backend.loadSong(parsed[i])
        }

        callback(null)

      })
    } else {
      backend.getJSON(url).then(function (songs) {
        // Add all the songs we downloaded.
        console.log('Downloaded songs', songs)

        for(var i = 0; i < songs.length; i++) {
          // Add each item to the songs database if necessary
          backend.loadSong(songs[i])
        }

        callback(null)

      }).catch(err => callback(err))
    }
  }

  /**
   * Save the song database to IPFS as JSON. Call the callback with an error if
   * it fails, or null and the IPFS hash of the saved database if it succeeds.
   */
  backend.saveSongs = function(callback) {
    console.log('Saving database of %d songs', backend.database.allSongs.length)

    // Add it to IPFS
    var ipfs = backend.ipfsnode.ipfs
    ipfs.files.add(backend.ipfsnode.Buffer.from(JSON.stringify(backend.database.allSongs)), (err, returned) => {
      if (err) {
        // IPFS didn't like it, so complain
        callback(err)
      }

      console.log('Database saved to IPFS hash:', returned[0].hash)

      // Call the callback with no error and the hash
      callback(null, returned[0].hash)

    })
  }

  /**
   * Search for songs containing the given string in their metadata. Returns a
   * function that, when called with a page number, calls a callback with that
   * page of the results as an array of Song objects.
   */
  backend.findSongs = function (query) {
    // This will keep a persistent array of page arrays, until nobody wants the
    // results anymore.
    var pages = []

    // This is our iteration index in allSongs
    var index = 0

    // This is our page size
    var pageSize = 50

    return function (page, pageHandler) {
      console.log('Finding page %d', page)

      while(index < backend.database.allSongs.length && page >= pages.length) {
        // Make a new page
        var newPage = []

        console.log('Generating page %d', pages.length)

        while (index < backend.database.allSongs.length && newPage.length < pageSize) {
          // We should search more songs

          // TODO: break up this loop so we don't scan all songs and make
          // everyone wait, but in a way that lets us cancel the operation
          // somehow.

          // How about this one
          var candidate = backend.database.allSongs[index]
          index++

          // Evaluate a match. For now, one field has to contain the query.
          // Later we can try lunr.js or something.
          var lowerQuery = query.toLowerCase()
          if (candidate.title.toLowerCase().includes(lowerQuery) ||
            candidate.artist.toLowerCase().includes(lowerQuery) ||
            candidate.album.toLowerCase().includes(lowerQuery)) {

            // This song is a match
            newPage.push(candidate)

          }
        }
        pages.push(newPage)
      }

      if(page < pages.length) {
        // We already know what's on this page. Send it back.
        pageHandler(pages[page])
      } else {
        // We couldn't generate that page. Send an empty page.
        pageHandler([])
      }

    }
  }

  /**
   * Add a song to the local metadata database with the metadata decoded from
   * the given file. Adds the file to IPFS to get its hash. Calls the callback
   * with null and the song object, if it turns out to be a readable song. Calls
   * the callback with an error if there is an error.
   */
  backend.addSong = function (fileData, callback) {

    try {
      // Put the whole thing in a try-catch in case Aurora explodes

      // Load metadata and make sure it's audio
      var asset = AV.Asset.fromBuffer(fileData)

      asset.on('error', (err) => {
        // Report decoding errors
        callback(err)
      })

      asset.get('metadata', (metadata) => {
        // We got metadata, so it must be real audio
        console.log('Metadata', metadata)

        // Add it to IPFS
        var ipfs = backend.ipfsnode.ipfs
        ipfs.files.add(backend.ipfsnode.Buffer.from(fileData), (err, returned) => {
          if (err) {
            // IPFS didn't like it, so complain
            callback(err)
          }

          console.log('IPFS hash:', returned[0].hash)

          // Craft a song object
          var song = {
            title: metadata.title,
            album: metadata.album,
            artist: metadata.artist,
            url: 'ipfs:' + returned[0].hash
          }

          // Add it to the database if it's not there already
          backend.loadSong(song)

          // Call the callback with no error and the song
          callback(null, song)

        })

      })
    } catch (err) {
      callback(err)
    }

  }

  /**
   * Start up the backend, using the given IpfsNode singleton to store and
   * retrieve files, and the given IPC-style EventEmitter to communicate with
   * the Player frontend (with .send() and .on()).
   */
  backend.start = function (ipfsnode, ipc) {
    backend.ipfsnode = ipfsnode
    backend.ipc = ipc

    backend.ipc.on('player-upload', (event, fileData) => {
      // Handle an ArrayBuffer of file data to upload.

      console.log('Uploading file...')

      backend.addSong(fileData, (err, song) => {
        if (err) {
          // Report errors, but make sure to send them back so the next song in
          // the queue can be tried
          event.sender.send('player-uploaded', err)
        }

        // Say we uploaded the song
        event.sender.send('player-uploaded', null, song)

      })

    })

    backend.ipc.on('player-search', (event, query, pageNumber) => {
      // Handle a search query

      // First, see if it's an IPFS hash
      var hashRegex = /(\/?ipfs\/|\/?ipns\/)?(Qm[A-HJ-NP-Za-km-z1-9]{44,45})/

      var found = query.match(hashRegex)

      if(found) {
        // This is an IPFS hash. Extract just the hash part
        // 0 is whole regex, 1 is IPFS/IPNS, 2 is actual hash
        var hash = found[2]

        console.log('Searching IPFS for', hash, found)

        // Use our wrapper to get the whoile file
        backend.ipfsnode.ipfs.files.cat(hash, (err, fileData) => {
          if (err) {
            throw err
          }

          backend.addSong(fileData, (err, song) => {
            if (err) {
              // It's not a song actually
              throw err
            }

            // It's a single song and we successfully loaded it

            // Give the user just this song to look at
            event.sender.send('player-songs', [song])

          })
        })
      } else {
        // It's not an IPFS hash for a song, so maybe it's a full text search

        console.log('Looking for songs matching', query)

        // Do a search and get the first page
        // TODO: reuse the pager
        var pager = backend.findSongs(query)

        pager(pageNumber, (results) => {
          console.log('Got %d search results', results.length)
          event.sender.send('player-songs', results)
        })
      }

    })

    // We can get hints that URLS will be played soon and that we should go
    // looking for their content.
    backend.ipc.on('player-hint', (event, url) => {

      var ipfs = backend.ipfsnode.ipfs
      ipfs.files.cat(url.split(':')[1], (err, data) => {
        if (err) {
          throw err
        }

        console.log('IPFS downloaded file for hint: %s', url)

        // Discard the data for now

      })

    })

    // Actually play URLs
    backend.ipc.on('player-url', (event, url, playNow) => {
      // Play the given URL. Ought to be an ipfs:<hash> URL.

      console.log('Loading: %s', url)

      if(backend.globalPlayer !== null) {
        // If there's already a One True Player, get rid of it. Only one player
        // will be allowed to replace it, when an IPFS cat finally finishes.
        backend.globalPlayer.stop();
        backend.globalPlayer = null;
      }

      // Remember whether we want to play as soon as we can or not, but allow
      // that to be changed if we get a play or pause event while the data for
      // the song is being downloaded. TODO: this weird edge case goes away if
      // we write an IpfsSource for aurora.js
      backend.playNow = playNow

      // Get the track data from IPFS
      var ipfs = backend.ipfsnode.ipfs
      ipfs.files.cat(url.split(':')[1], (err, fileData) => {
        if (err) {
          throw err
        }

        if(backend.globalPlayer !== null) {
          // Someone else has beaten us to becoming the One True Player.
          // We have our data, but they ought to play instead.
          return
        }

        console.log('Playing: %s', url)

        // Make an asset from the buffer
        var asset = AV.Asset.fromBuffer(fileData)

        asset.on('format', (format) => {
          console.log('Format decoded: ' + format)
        })

        asset.on('duration', (duration) => {
          console.log('Duration decoded: %d', duration)
          // Inform the UI of the song duration
          event.sender.send('player-duration', duration)
        })

        asset.on('decodeStart', () => {
          console.log('Audio decode started')
        })

        // Make a new Player for the asset.
        var player = new AV.Player(asset)

        // Become the One True Player
        backend.globalPlayer = player;

        player.on('error', (err) => {
          console.log('Player Error: ' + err)
          throw err
        })

        player.on('progress', (msecs) => {
          // Inform the UI of the playback progress
          event.sender.send('player-progress', msecs)
        })

        player.on('end', () => {
          // We're done!
          console.log('player is done')
          event.sender.send('player-ended')
        })

        if(backend.playNow) {
          // We want to play the song as soon as we can
          console.log('Will play now')
          player.play()
        } else {
          // Just preload
          player.preload()
        }
      })
    })

    // Handle requests to pause the music
    backend.ipc.on('player-pause', (event) => {
      if(backend.globalPlayer !== null) {
        console.log('Pause global player')
        backend.globalPlayer.pause()
      } else {
        console.log('No global player; do not play when ready')
        backend.playNow = false
      }
    })

    // And to play it again
    backend.ipc.on('player-play', (event) => {
      if(backend.globalPlayer !== null) {
        console.log('Play global player')
        backend.globalPlayer.play()
      } else {
        console.log('No global player; play when ready')
        backend.playNow = true
      }
    })

    // Now import/export stuff

    // Import song metadata from the given URL
    backend.ipc.on('player-import', (event, url) => {
      // Use the song loader method
      backend.loadSongs(url, (err) => {
        if (err) {
          throw err
        }

        event.sender.send('player-imported')

      })
    })

    // Export all song metadata to a URL and send it back in the player-exported
    // event.
    backend.ipc.on('player-export', (event, url) => {
      backend.saveSongs((err, hash) => {
        if (err) {
          // Complain about any errors
          throw err
        }

        console.log('Exported successfully')

        // Reply with the hash of the database
        event.sender.send('player-exported', 'ipfs:' + hash)
      })
    })

    console.log('Backend ready')

    // Mark the backend ready and tell everyone who has been waiting
    backend.ready = true
    while (backend.readyCallbacks.length > 0) {
      callback = backend.readyCallbacks.pop()
      callback()
    }
  }

  /**
   * Call the given callback after the backend is ready to process IPC events.
   */
  backend.onReady = function (callback) {
    if (backend.ready) {
      // Ready now
      callback()
    } else {
      // Call when ready
      backend.readyCallbacks.push(callback);
    }
  }

  // Return the completed module object
  return backend
}(AV))
<commit_msg>Route all loading through one function<commit_after>// Provides a backend that plays songs with AV and mp3.js.
// Expects aurora.js dependency as a module argument.
var Backend = (function (AV) {

  // Make the object we will export as our singleton sound-playing backend thingy.
  var backend = {
    // This tracks if we have been started
    ready: false,
    // This holds the callbacks to call after we boot up
    readyCallbacks: [],
    // This gets filled in with an IpfsNode instance that stores and retrieves
    // files.
    ipfsnode: null,
    // This gets filled in with the communication channel to the player
    // frontend.
    ipc: null,
    // This holds the one AV player that is allowed to make noise. Not to be
    // confused with the Player, which is the frontend,
    globalPlayer: null,
    // This determines if we should play the first globalPlayer we manage to
    // make, as soon as we can.
    playNow: false,
    // This holds a song database
    database: {
      // This holds all the records in an array
      allSongs: [],
      // This maps from URL string to index in array
      byUrl: {}
    }
  }

  /**
   * Load the given URL as an ArrayBuffer or UInt8Array or similar.
   * Supports ipfs: and ipfs+A256GCM: with # separating hash from key.
   * Also supports anything Fetch supports.
   */
  backend.loadUrl = function(url, callback) {
    var parts = url.split(':')
    if (parts[0] == 'ipfs') {
      // This is an ipfs URL.
      backend.ipfsnode.ipfs.files.cat(parts[1], callback)
    } else if (parts[0] == 'ipfs+A256GCM') {
      // It is encrypted

      var urlParts = parts[1].split('#')
      var ipfsHash = urlParts[0]
      var key = urlParts[1]

      backend.loadUrl('ipfs:' + ipfsHash, async (err, data) => {
        // Go download the encrypted blob
        if (err) {
          return callback(err)
        }

        // Decrypt the data and send it to our callback
        var decrypted
        try {
          decrypted = await EasyCrypto.decrypt(data, key)
        } catch (err) {
          return callback(err)
        }
        callback(null, decrypted)
      })

    } else {
      // It is probably something Fetch can do
      fetch(new Request(url)).then((response) => {
        return response.arrayBuffer()
      }).catch((err) => {
        callback(err)
      }).then((buffer) => {
        callback(null, buffer)
      })
    }
  }

  /**
   * Given a Song object with title, album, artist, and url, add the song to the
   * metadata database if it doesn't already exist.
   */
  backend.loadSong = function(song) {
    // We ID songs uniquely by URL for now
    if (!backend.database.byUrl.hasOwnProperty(song.url)) {
      // Keep the new song
      backend.database.allSongs.push(song)
      // Record where it is in the database
      backend.database.byUrl[song.url] = backend.database.allSongs.length - 1
    }
  }

  /**
   * Download song records as JSON from the given URL and keep them in the local
   * database. URL may be ipfs:.
   * Calls the callback with an error, or null if everything works.
   */
  backend.loadSongs = function (url, callback) {
    backend.loadUrl(url, (err, data) => {
      // Download the URL where the song data is
      if (err) {
        return callback(err)
      }

      // We got something back. Parse it as UTF-8 JSON
      var parsed = JSON.parse(new TextDecoder('utf-8').decode(fileData))

      for(var i = 0; i < parsed.length; i++) {
        // Add each item to the songs database if necessary
        backend.loadSong(parsed[i])
      }

      callback(null)

    })
  }

  /**
   * Save the song database to IPFS as JSON. Call the callback with an error if
   * it fails, or null and the IPFS hash of the saved database if it succeeds.
   */
  backend.saveSongs = function(callback) {
    console.log('Saving database of %d songs', backend.database.allSongs.length)

    // Add it to IPFS
    var ipfs = backend.ipfsnode.ipfs
    ipfs.files.add(backend.ipfsnode.Buffer.from(JSON.stringify(backend.database.allSongs)), (err, returned) => {
      if (err) {
        // IPFS didn't like it, so complain
        callback(err)
      }

      console.log('Database saved to IPFS hash:', returned[0].hash)

      // Call the callback with no error and the hash
      callback(null, returned[0].hash)

    })
  }

  /**
   * Search for songs containing the given string in their metadata. Returns a
   * function that, when called with a page number, calls a callback with that
   * page of the results as an array of Song objects.
   */
  backend.findSongs = function (query) {
    // This will keep a persistent array of page arrays, until nobody wants the
    // results anymore.
    var pages = []

    // This is our iteration index in allSongs
    var index = 0

    // This is our page size
    var pageSize = 50

    return function (page, pageHandler) {
      console.log('Finding page %d', page)

      while(index < backend.database.allSongs.length && page >= pages.length) {
        // Make a new page
        var newPage = []

        console.log('Generating page %d', pages.length)

        while (index < backend.database.allSongs.length && newPage.length < pageSize) {
          // We should search more songs

          // TODO: break up this loop so we don't scan all songs and make
          // everyone wait, but in a way that lets us cancel the operation
          // somehow.

          // How about this one
          var candidate = backend.database.allSongs[index]
          index++

          // Evaluate a match. For now, one field has to contain the query.
          // Later we can try lunr.js or something.
          var lowerQuery = query.toLowerCase()
          if (candidate.title.toLowerCase().includes(lowerQuery) ||
            candidate.artist.toLowerCase().includes(lowerQuery) ||
            candidate.album.toLowerCase().includes(lowerQuery)) {

            // This song is a match
            newPage.push(candidate)

          }
        }
        pages.push(newPage)
      }

      if(page < pages.length) {
        // We already know what's on this page. Send it back.
        pageHandler(pages[page])
      } else {
        // We couldn't generate that page. Send an empty page.
        pageHandler([])
      }

    }
  }

  /**
   * Add a song to the local metadata database with the metadata decoded from
   * the given file. Adds the file to IPFS to get its hash. Calls the callback
   * with null and the song object, if it turns out to be a readable song. Calls
   * the callback with an error if there is an error.
   */
  backend.addSong = function (fileData, callback) {

    try {
      // Put the whole thing in a try-catch in case Aurora explodes

      // Load metadata and make sure it's audio
      var asset = AV.Asset.fromBuffer(fileData)

      asset.on('error', (err) => {
        // Report decoding errors
        callback(err)
      })

      asset.get('metadata', (metadata) => {
        // We got metadata, so it must be real audio
        console.log('Metadata', metadata)

        // Add it to IPFS
        var ipfs = backend.ipfsnode.ipfs
        ipfs.files.add(backend.ipfsnode.Buffer.from(fileData), (err, returned) => {
          if (err) {
            // IPFS didn't like it, so complain
            callback(err)
          }

          console.log('IPFS hash:', returned[0].hash)

          // Craft a song object
          var song = {
            title: metadata.title,
            album: metadata.album,
            artist: metadata.artist,
            url: 'ipfs:' + returned[0].hash
          }

          // Add it to the database if it's not there already
          backend.loadSong(song)

          // Call the callback with no error and the song
          callback(null, song)

        })

      })
    } catch (err) {
      callback(err)
    }

  }

  /**
   * Start up the backend, using the given IpfsNode singleton to store and
   * retrieve files, and the given IPC-style EventEmitter to communicate with
   * the Player frontend (with .send() and .on()).
   */
  backend.start = function (ipfsnode, ipc) {
    backend.ipfsnode = ipfsnode
    backend.ipc = ipc

    backend.ipc.on('player-upload', (event, fileData) => {
      // Handle an ArrayBuffer of file data to upload.

      console.log('Uploading file...')

      backend.addSong(fileData, (err, song) => {
        if (err) {
          // Report errors, but make sure to send them back so the next song in
          // the queue can be tried
          event.sender.send('player-uploaded', err)
        }

        // Say we uploaded the song
        event.sender.send('player-uploaded', null, song)

      })

    })

    backend.ipc.on('player-search', (event, query, pageNumber) => {
      // Handle a search query

      // First, see if it's an IPFS hash
      var hashRegex = /(\/?ipfs\/|\/?ipns\/)?(Qm[A-HJ-NP-Za-km-z1-9]{44,45})/

      var found = query.match(hashRegex)

      if(found) {
        // This is an IPFS hash. Extract just the hash part
        // 0 is whole regex, 1 is IPFS/IPNS, 2 is actual hash
        var hash = found[2]

        console.log('Searching IPFS for', hash, found)

        // Use our wrapper to get the whoile file
        backend.ipfsnode.ipfs.files.cat(hash, (err, fileData) => {
          if (err) {
            throw err
          }

          backend.addSong(fileData, (err, song) => {
            if (err) {
              // It's not a song actually
              throw err
            }

            // It's a single song and we successfully loaded it

            // Give the user just this song to look at
            event.sender.send('player-songs', [song])

          })
        })
      } else {
        // It's not an IPFS hash for a song, so maybe it's a full text search

        console.log('Looking for songs matching', query)

        // Do a search and get the first page
        // TODO: reuse the pager
        var pager = backend.findSongs(query)

        pager(pageNumber, (results) => {
          console.log('Got %d search results', results.length)
          event.sender.send('player-songs', results)
        })
      }

    })

    // We can get hints that URLS will be played soon and that we should go
    // looking for their content.
    backend.ipc.on('player-hint', (event, url) => {

      backend.loadUrl(url, (err, data) => {
        if (err) {
          throw err
        }

        console.log('Downloaded file for hint: %s', url)

        // Discard the data for now

      })

    })

    // Actually play URLs
    backend.ipc.on('player-url', (event, url, playNow) => {
      // Play the given URL. Ought to be an ipfs:<hash> URL.

      console.log('Loading: %s', url)

      if(backend.globalPlayer !== null) {
        // If there's already a One True Player, get rid of it. Only one player
        // will be allowed to replace it, when an IPFS cat finally finishes.
        backend.globalPlayer.stop();
        backend.globalPlayer = null;
      }

      // Remember whether we want to play as soon as we can or not, but allow
      // that to be changed if we get a play or pause event while the data for
      // the song is being downloaded. TODO: this weird edge case goes away if
      // we write an IpfsSource for aurora.js
      backend.playNow = playNow

      // Get the track data
      backend.loadUrl(url, (err, fileData) => {
        if (err) {
          throw err
        }

        if(backend.globalPlayer !== null) {
          // Someone else has beaten us to becoming the One True Player.
          // We have our data, but they ought to play instead.
          return
        }

        console.log('Playing: %s', url)

        // Make an asset from the buffer
        var asset = AV.Asset.fromBuffer(fileData)

        asset.on('format', (format) => {
          console.log('Format decoded: ' + format)
        })

        asset.on('duration', (duration) => {
          console.log('Duration decoded: %d', duration)
          // Inform the UI of the song duration
          event.sender.send('player-duration', duration)
        })

        asset.on('decodeStart', () => {
          console.log('Audio decode started')
        })

        // Make a new Player for the asset.
        var player = new AV.Player(asset)

        // Become the One True Player
        backend.globalPlayer = player;

        player.on('error', (err) => {
          console.log('Player Error: ' + err)
          throw err
        })

        player.on('progress', (msecs) => {
          // Inform the UI of the playback progress
          event.sender.send('player-progress', msecs)
        })

        player.on('end', () => {
          // We're done!
          console.log('player is done')
          event.sender.send('player-ended')
        })

        if(backend.playNow) {
          // We want to play the song as soon as we can
          console.log('Will play now')
          player.play()
        } else {
          // Just preload
          player.preload()
        }
      })
    })

    // Handle requests to pause the music
    backend.ipc.on('player-pause', (event) => {
      if(backend.globalPlayer !== null) {
        console.log('Pause global player')
        backend.globalPlayer.pause()
      } else {
        console.log('No global player; do not play when ready')
        backend.playNow = false
      }
    })

    // And to play it again
    backend.ipc.on('player-play', (event) => {
      if(backend.globalPlayer !== null) {
        console.log('Play global player')
        backend.globalPlayer.play()
      } else {
        console.log('No global player; play when ready')
        backend.playNow = true
      }
    })

    // Now import/export stuff

    // Import song metadata from the given URL
    backend.ipc.on('player-import', (event, url) => {
      // Use the song loader method
      backend.loadSongs(url, (err) => {
        if (err) {
          throw err
        }

        event.sender.send('player-imported')

      })
    })

    // Export all song metadata to a URL and send it back in the player-exported
    // event.
    backend.ipc.on('player-export', (event, url) => {
      backend.saveSongs((err, hash) => {
        if (err) {
          // Complain about any errors
          throw err
        }

        console.log('Exported successfully')

        // Reply with the hash of the database
        event.sender.send('player-exported', 'ipfs:' + hash)
      })
    })

    console.log('Backend ready')

    // Mark the backend ready and tell everyone who has been waiting
    backend.ready = true
    while (backend.readyCallbacks.length > 0) {
      callback = backend.readyCallbacks.pop()
      callback()
    }
  }

  /**
   * Call the given callback after the backend is ready to process IPC events.
   */
  backend.onReady = function (callback) {
    if (backend.ready) {
      // Ready now
      callback()
    } else {
      // Call when ready
      backend.readyCallbacks.push(callback);
    }
  }

  // Return the completed module object
  return backend
}(AV))
<|endoftext|>