<commit_before>'use strict';
var _ = require('underscore');
var async = require('async');
var labeledLogger = require('./logger');

function Player(options) {
    options = options || {};

    _.bindAll.apply(_, [this].concat(_.functions(this)));
    this.config         = options.config            || require('./config').getConfig();
    this.logger         = options.logger            || labeledLogger('core');
    this.playedQueue    = options.playedQueue       || [];
    this.queue          = options.queue             || [];
    this.plugins        = options.plugins           || {};
    this.backends       = options.backends          || {};
    this.songsPreparing = options.songsPreparing    || {};
    this.volume         = options.volume            || 1;
    this.songEndTimeout = options.songEndTimeout    || null;
}

// call hook function in all modules
// if any hooks return a truthy value, it is an error and we abort
// be very careful with calling hooks from within a hook, infinite loops are possible
Player.prototype.callHooks = function(hook, argv) {
    // _.find() used instead of _.each() because we want to break out as soon
    // as a hook returns a truthy value (used to indicate an error, e.g. in form
    // of a string)
    var err = null;

    this.logger.silly('callHooks(' + hook + ',', argv, ')');

    _.find(this.plugins, function(plugin) {
        if (plugin[hook]) {
            err = plugin[hook].apply(null, argv);
            return err;
        }
    });

    return err;
};

// returns number of hook functions attached to given hook
Player.prototype.numHooks = function(hook) {
    var cnt = 0;

    _.find(this.plugins, function(plugin) {
        if (plugin[hook]) {
            cnt++;
        }
    });

    return cnt;
};

Player.prototype.endOfSong = function() {
    var np = this.queue[0];

    this.logger.info('end of song ' + np.songID);
    this.callHooks('onSongEnd', [np]);

    this.playedQueue.push(this.queue[0]);
    this.playedQueue = _.last(this.playedQueue, this.config.playedQueueSize);

    this.playbackPosition = null;
    this.playbackStart = null;
    this.queue[0] = null;
    this.songEndTimeout = null;
    this.onQueueModify();
};

// start or resume playback of now playing song.
// if pos is undefined, playback continues (or starts from 0 if !playbackPosition)
Player.prototype.startPlayback = function(pos) {
    var np = this.queue[0];
    if (!np) {
        this.logger.verbose('startPlayback called, but hit end of queue');
        return;
    }

    if (pos) {
        this.logger.info('playing song: ' + np.songID + ', from pos: ' + pos);
    } else {
        this.logger.info('playing song: ' + np.songID);
    }

    var oldPlaybackStart = this.playbackStart;
    this.playbackStart = new Date().getTime(); // song is playing while this is truthy

    // where did the song start playing from at playbackStart?
    if (!_.isUndefined(pos)) {
        this.playbackPosition = pos;
    } else if (!this.playbackPosition) {
        this.playbackPosition = 0;
    }

    if (oldPlaybackStart) {
        this.callHooks('onSongSeek', [np]);
    } else {
        this.callHooks('onSongChange', [np]);
    }

    var durationLeft = parseInt(np.duration) - this.playbackPosition + this.config.songDelayMs;
    if (this.songEndTimeout) {
        this.logger.debug('songEndTimeout was cleared');
        clearTimeout(this.songEndTimeout);
        this.songEndTimeout = null;
    }
    this.songEndTimeout = setTimeout(this.endOfSong, durationLeft);
};

Player.prototype.pausePlayback = function() {
    // update position
    this.playbackPosition += new Date().getTime() - this.playbackStart;
    this.playbackStart = null;

    clearTimeout(this.songEndTimeout);
    this.songEndTimeout = null;
    this.callHooks('onSongPause', [this.nowPlaying]);
};

// TODO: proper song object with constructor?
Player.prototype.setPrepareTimeout = function(song) {
    if (song.prepareTimeout) {
        clearTimeout(song.prepareTimeout);
    }

    song.prepareTimeout = setTimeout(_.bind(function() {
        this.logger.info('prepare timeout for song: ' + song.songID + ', removing');
        song.cancelPrepare('prepare timeout');
        song.prepareTimeout = null;
    }, this), this.config.songPrepareTimeout);

    Object.defineProperty(song, 'prepareTimeout', {
        enumerable: false,
        writable: true
    });
};

Player.prototype.prepareError = function(song, err) {
    // remove all instances of this song
    for (var i = this.queue.length - 1; i >= 0; i--) {
        if (this.queue[i].songID === song.songID &&
            this.queue[i].backendName === song.backendName) {
            if (!song.beingDeleted) {
                this.logger.error('preparing song failed! (' + err + '), removing from queue: ' +
                        song.songID);
                this.removeFromQueue(i);
            }
        }
    }

    this.callHooks('onSongPrepareError', [song, err]);
};

Player.prototype.prepareProgCallback = function(song, dataSize, done, asyncCallback) {
    /* progress callback
     * when this is called, new song data has been flushed to disk */

    // start playback if it hasn't been started yet
    if (this.queue[0] &&
            this.queue[0].backendName === song.backendName &&
            this.queue[0].songID === song.songID &&
            !this.playbackStart && dataSize) {
        this.startPlayback();
    }

    if (done) {
        // mark song as prepared
        this.callHooks('onSongPrepared', [song]);

        // done preparing, can't cancel anymore
        delete(song.cancelPrepare);
        delete(this.songsPreparing[song.backendName][song.songID]);

        // clear prepare timeout
        clearTimeout(song.prepareTimeout);
        song.prepareTimeout = null;

        asyncCallback();
    } else {
        // reset prepare timeout
        this.setPrepareTimeout(song);
    }

    // tell plugins that new data is available for this song, and
    // whether the song is now fully written to disk or not.
    this.callHooks('onPrepareProgress', [song, dataSize, done]);
};

Player.prototype.prepareErrCallback = function(song, err, asyncCallback) {
    /* error callback */

    // don't let anything run cancelPrepare anymore
    delete(song.cancelPrepare);

    // clear prepare timeout
    clearTimeout(song.prepareTimeout);
    song.prepareTimeout = null;

    // abort preparing more songs; current song will be deleted ->
    // onQueueModified is called -> song preparation is triggered again
    asyncCallback(true);

    // TODO: investigate this, should probably be above asyncCallback
    this.prepareError(song, err);
    delete(this.songsPreparing[song.backendName][song.songID]);
};

// TODO: get rid of the callback hell, use promises?
Player.prototype.prepareSong = function(song, asyncCallback) {
    if (!song) {
        this.logger.warn('prepareSong() without song');
        asyncCallback(true);
        return;
    }
    if (!this.backends[song.backendName]) {
        this.prepareError(song, 'prepareSong() with unknown backend ' + song.backendName);
        asyncCallback(true);
        return;
    }

    if (this.backends[song.backendName].isPrepared(song)) {
        // start playback if it hasn't been started yet
        if (this.queue[0] &&
                this.queue[0].backendName === song.backendName &&
                this.queue[0].songID === song.songID &&
                !this.playbackStart) {
            this.startPlayback();
        }

        // song is already prepared, ok to prepare more songs
        asyncCallback();
    } else if (this.songsPreparing[song.backendName][song.songID]) {
        // this song is already preparing, so don't yet prepare next song
        asyncCallback(true);
    } else {
        // song is not prepared and not currently preparing: let backend prepare it
        this.logger.debug('DEBUG: prepareSong() ' + song.songID);
        this.songsPreparing[song.backendName][song.songID] = song;

        song.cancelPrepare = this.backends[song.backendName].prepareSong(
            song,
            _.partial(this.prepareProgCallback, _, _, _, asyncCallback),
            _.partial(this.prepareErrCallback, _, _, asyncCallback)
        );

        this.setPrepareTimeout(song);
    }
};

// prepare now playing and queued songs for playback
Player.prototype.prepareSongs = function() {
    async.series([
        _.bind(function(callback) {
            // prepare now-playing song if it exists and if not prepared
            if (this.queue[0]) {
                this.prepareSong(this.queue[0], callback);
            } else {
                callback(true);
            }
        }, this),
        _.bind(function(callback) {
            // prepare next song in queue if it exists and if not prepared
            if (this.queue[1]) {
                this.prepareSong(this.queue[1], callback);
            } else {
                callback(true);
            }
        }, this)
    ]);
};

// to be called whenever the queue has been modified
// this function will:
// - play back the first song in the queue if no song is playing
// - call prepareSongs()
Player.prototype.onQueueModify = function() {
    this.callHooks('preQueueModify', [this.queue]);

    // set next song as now playing
    if (!this.queue[0]) {
        this.queue.shift();
    }

    if (!this.queue.length) {
        // if the queue is now empty, do nothing
        this.callHooks('onEndOfQueue');
        this.logger.info('end of queue, waiting for more songs');
    } else {
        // else prepare songs
        this.prepareSongs();
    }
    this.callHooks('postQueueModify', [this.queue]);
};

// find song from queue
Player.prototype.searchQueue = function(backendName, songID) {
    for (var i = 0; i < this.queue.length; i++) {
        if (this.queue[i].songID === songID &&
               this.queue[i].backendName === backendName) {
            return this.queue[i];
        }
    }

    return null;
};

// make a search query to backends
Player.prototype.searchBackends = function(query, callback) {
    var resultCnt = 0;
    var allResults = {};

    _.each(this.backends, function(backend) {
        backend.search(query, _.bind(function(results) {
            resultCnt++;

            // make a temporary copy of songlist, clear songlist, check
            // each song and add them again if they are ok
            var tempSongs = _.clone(results.songs);
            allResults[backend.name] = results;
            allResults[backend.name].songs = {};

            _.each(tempSongs, function(song) {
                var err = this.callHooks('preAddSearchResult', [song]);
                if (!err) {
                    allResults[backend.name].songs[song.songID] = song;
                } else {
                    this.logger.error('preAddSearchResult hook error: ' + err);
                }
            }, this);

            // got results from all services?
            if (resultCnt >= Object.keys(this.backends).length) {
                callback(allResults);
            }
        }, this), _.bind(function(err) {
            resultCnt++;
            this.logger.error('error while searching ' + backend.name + ': ' + err);

            // got results from all services?
            if (resultCnt >= Object.keys(this.backends).length) {
                callback(allResults);
            }
        }, this));
    }, this);
};

// get rid of song in queue
// cnt can be left out for deleting only one song
Player.prototype.removeFromQueue = function(pos, cnt, onlyRemove) {
    var retval = [];
    if (!cnt) {
        cnt = 1;
    }
    pos = Math.max(0, parseInt(pos));

    if (!onlyRemove) {
        this.callHooks('preSongsRemoved', [pos, cnt]);
    }

    // remove songs from queue
    if (pos + cnt > 0) {
        if (this.queue.length) {
            // stop preparing songs we are about to remove
            // we want to limit this to this.queue.length if cnt is very large
            for (var i = 0; i < Math.min(this.queue.length, pos + cnt); i++) {
                var song = this.queue[i];

                // signal prepareError function not to run removeFromQueue again
                // TODO: try getting rid of this ugly hack (beingDeleted)...
                // TODO: more non enumerable properties, especially plugins?
                Object.defineProperty(song, 'beingDeleted', {
                    enumerable: false,
                    writable: true
                });

                song.beingDeleted = true;
                if (song.cancelPrepare) {
                    song.cancelPrepare('song deleted');
                    delete(song.cancelPrepare);
                }
            }

            retval = this.queue.splice(pos, cnt);

            if (pos === 0) {
                // now playing was deleted
                this.playbackPosition = null;
                this.playbackStart = null;
                clearTimeout(this.songEndTimeout);
                this.songEndTimeout = null;
            }
        }
    }

    if (!onlyRemove) {
        this.onQueueModify();
        this.callHooks('postSongsRemoved', [pos, cnt]);
    }

    return retval;
};

Player.prototype.moveInQueue = function(from, to, cnt) {
    if (!cnt) {
        cnt = 1;
    }

    this.callHooks('preSongsMoved', [from, to, cnt]);

    var songs = this.removeFromQueue(from, cnt, true);
    Array.prototype.splice.apply(this.queue, [to, 0].concat(songs));

    this.callHooks('sortQueue');
    this.onQueueModify();
    this.callHooks('postSongsMoved', [songs, from, to, cnt]);
};

// add songs to the queue, at optional position
Player.prototype.addToQueue = function(songs, pos) {
    if (!pos) {
        pos = this.queue.length;
    }
    if (pos < 0) {
        pos = 1;
    }
    pos = Math.min(pos, this.queue.length);

    this.callHooks('preSongsQueued', [songs, pos]);
    _.each(songs, function(song) {
        // check that required fields are provided
        if (!song.title || !song.songID || !song.backendName || !song.duration) {
            this.logger.info('required song fields not provided: ' + song.songID);
            return;
            //return 'required song fields not provided'; // TODO: this ain't gonna work
        }

        var err = this.callHooks('preSongQueued', [song]);
        if (err) {
            this.logger.error('not adding song to queue: ' + err);
        } else {
            song.timeAdded = new Date().getTime();

            this.queue.splice(pos++, 0, song);
            this.logger.info('added song to queue: ' + song.songID);
            this.callHooks('postSongQueued', [song]);
        }
    }, this);

    this.callHooks('sortQueue');
    this.onQueueModify();
    this.callHooks('postSongsQueued', [songs, pos]);
};

Player.prototype.shuffleQueue = function() {
    // don't change now playing
    var temp = this.queue.shift();
    this.queue = _.shuffle(this.queue);
    this.queue.unshift(temp);

    this.callHooks('onQueueShuffled', [this.queue]);
    this.onQueueModify();
};

// cnt can be negative to go back or zero to restart current song
Player.prototype.skipSongs = function(cnt) {
    this.npIsPlaying = false;

    // TODO: this could be replaced with a splice?
    for (var i = 0; i < Math.abs(cnt); i++) {
        if (cnt > 0) {
            if (this.queue[0]) {
                this.playedQueue.push(this.queue[0]);
            }

            this.queue.shift();
        } else if (cnt < 0) {
            if (this.playedQueue.length) {
                this.queue.unshift(this.playedQueue.pop());
            }
        }

        // ran out of songs while skipping, stop
        if (!this.queue[0]) {
            break;
        }
    }

    this.playedQueue = _.last(this.playedQueue, this.config.playedQueueSize);

    this.playbackPosition = null;
    this.playbackStart = null;
    clearTimeout(this.songEndTimeout);
    this.songEndTimeout = null;
    this.onQueueModify();
};

// TODO: userID does not belong into core...?
Player.prototype.setVolume = function(newVol, userID) {
    newVol = Math.min(1, Math.max(0, newVol));
    this.volume = newVol;
    this.callHooks('onVolumeChange', [newVol, userID]);
};

module.exports = Player;
<commit_msg>check range before moving<commit_after>'use strict';
var _ = require('underscore');
var async = require('async');
var labeledLogger = require('./logger');

function Player(options) {
    options = options || {};

    _.bindAll.apply(_, [this].concat(_.functions(this)));
    this.config         = options.config            || require('./config').getConfig();
    this.logger         = options.logger            || labeledLogger('core');
    this.playedQueue    = options.playedQueue       || [];
    this.queue          = options.queue             || [];
    this.plugins        = options.plugins           || {};
    this.backends       = options.backends          || {};
    this.songsPreparing = options.songsPreparing    || {};
    this.volume         = options.volume            || 1;
    this.songEndTimeout = options.songEndTimeout    || null;
}

// call hook function in all modules
// if any hooks return a truthy value, it is an error and we abort
// be very careful with calling hooks from within a hook, infinite loops are possible
Player.prototype.callHooks = function(hook, argv) {
    // _.find() used instead of _.each() because we want to break out as soon
    // as a hook returns a truthy value (used to indicate an error, e.g. in form
    // of a string)
    var err = null;

    this.logger.silly('callHooks(' + hook + ',', argv, ')');

    _.find(this.plugins, function(plugin) {
        if (plugin[hook]) {
            err = plugin[hook].apply(null, argv);
            return err;
        }
    });

    return err;
};

// returns number of hook functions attached to given hook
Player.prototype.numHooks = function(hook) {
    var cnt = 0;

    _.find(this.plugins, function(plugin) {
        if (plugin[hook]) {
            cnt++;
        }
    });

    return cnt;
};

Player.prototype.endOfSong = function() {
    var np = this.queue[0];

    this.logger.info('end of song ' + np.songID);
    this.callHooks('onSongEnd', [np]);

    this.playedQueue.push(this.queue[0]);
    this.playedQueue = _.last(this.playedQueue, this.config.playedQueueSize);

    this.playbackPosition = null;
    this.playbackStart = null;
    this.queue[0] = null;
    this.songEndTimeout = null;
    this.onQueueModify();
};

// start or resume playback of now playing song.
// if pos is undefined, playback continues (or starts from 0 if !playbackPosition)
Player.prototype.startPlayback = function(pos) {
    var np = this.queue[0];
    if (!np) {
        this.logger.verbose('startPlayback called, but hit end of queue');
        return;
    }

    if (pos) {
        this.logger.info('playing song: ' + np.songID + ', from pos: ' + pos);
    } else {
        this.logger.info('playing song: ' + np.songID);
    }

    var oldPlaybackStart = this.playbackStart;
    this.playbackStart = new Date().getTime(); // song is playing while this is truthy

    // where did the song start playing from at playbackStart?
    if (!_.isUndefined(pos)) {
        this.playbackPosition = pos;
    } else if (!this.playbackPosition) {
        this.playbackPosition = 0;
    }

    if (oldPlaybackStart) {
        this.callHooks('onSongSeek', [np]);
    } else {
        this.callHooks('onSongChange', [np]);
    }

    var durationLeft = parseInt(np.duration) - this.playbackPosition + this.config.songDelayMs;
    if (this.songEndTimeout) {
        this.logger.debug('songEndTimeout was cleared');
        clearTimeout(this.songEndTimeout);
        this.songEndTimeout = null;
    }
    this.songEndTimeout = setTimeout(this.endOfSong, durationLeft);
};

Player.prototype.pausePlayback = function() {
    // update position
    this.playbackPosition += new Date().getTime() - this.playbackStart;
    this.playbackStart = null;

    clearTimeout(this.songEndTimeout);
    this.songEndTimeout = null;
    this.callHooks('onSongPause', [this.nowPlaying]);
};

// TODO: proper song object with constructor?
Player.prototype.setPrepareTimeout = function(song) {
    if (song.prepareTimeout) {
        clearTimeout(song.prepareTimeout);
    }

    song.prepareTimeout = setTimeout(_.bind(function() {
        this.logger.info('prepare timeout for song: ' + song.songID + ', removing');
        song.cancelPrepare('prepare timeout');
        song.prepareTimeout = null;
    }, this), this.config.songPrepareTimeout);

    Object.defineProperty(song, 'prepareTimeout', {
        enumerable: false,
        writable: true
    });
};

Player.prototype.prepareError = function(song, err) {
    // remove all instances of this song
    for (var i = this.queue.length - 1; i >= 0; i--) {
        if (this.queue[i].songID === song.songID &&
            this.queue[i].backendName === song.backendName) {
            if (!song.beingDeleted) {
                this.logger.error('preparing song failed! (' + err + '), removing from queue: ' +
                        song.songID);
                this.removeFromQueue(i);
            }
        }
    }

    this.callHooks('onSongPrepareError', [song, err]);
};

Player.prototype.prepareProgCallback = function(song, dataSize, done, asyncCallback) {
    /* progress callback
     * when this is called, new song data has been flushed to disk */

    // start playback if it hasn't been started yet
    if (this.queue[0] &&
            this.queue[0].backendName === song.backendName &&
            this.queue[0].songID === song.songID &&
            !this.playbackStart && dataSize) {
        this.startPlayback();
    }

    if (done) {
        // mark song as prepared
        this.callHooks('onSongPrepared', [song]);

        // done preparing, can't cancel anymore
        delete(song.cancelPrepare);
        delete(this.songsPreparing[song.backendName][song.songID]);

        // clear prepare timeout
        clearTimeout(song.prepareTimeout);
        song.prepareTimeout = null;

        asyncCallback();
    } else {
        // reset prepare timeout
        this.setPrepareTimeout(song);
    }

    // tell plugins that new data is available for this song, and
    // whether the song is now fully written to disk or not.
    this.callHooks('onPrepareProgress', [song, dataSize, done]);
};

Player.prototype.prepareErrCallback = function(song, err, asyncCallback) {
    /* error callback */

    // don't let anything run cancelPrepare anymore
    delete(song.cancelPrepare);

    // clear prepare timeout
    clearTimeout(song.prepareTimeout);
    song.prepareTimeout = null;

    // abort preparing more songs; current song will be deleted ->
    // onQueueModified is called -> song preparation is triggered again
    asyncCallback(true);

    // TODO: investigate this, should probably be above asyncCallback
    this.prepareError(song, err);
    delete(this.songsPreparing[song.backendName][song.songID]);
};

// TODO: get rid of the callback hell, use promises?
Player.prototype.prepareSong = function(song, asyncCallback) {
    if (!song) {
        this.logger.warn('prepareSong() without song');
        asyncCallback(true);
        return;
    }
    if (!this.backends[song.backendName]) {
        this.prepareError(song, 'prepareSong() with unknown backend ' + song.backendName);
        asyncCallback(true);
        return;
    }

    if (this.backends[song.backendName].isPrepared(song)) {
        // start playback if it hasn't been started yet
        if (this.queue[0] &&
                this.queue[0].backendName === song.backendName &&
                this.queue[0].songID === song.songID &&
                !this.playbackStart) {
            this.startPlayback();
        }

        // song is already prepared, ok to prepare more songs
        asyncCallback();
    } else if (this.songsPreparing[song.backendName][song.songID]) {
        // this song is already preparing, so don't yet prepare next song
        asyncCallback(true);
    } else {
        // song is not prepared and not currently preparing: let backend prepare it
        this.logger.debug('DEBUG: prepareSong() ' + song.songID);
        this.songsPreparing[song.backendName][song.songID] = song;

        song.cancelPrepare = this.backends[song.backendName].prepareSong(
            song,
            _.partial(this.prepareProgCallback, _, _, _, asyncCallback),
            _.partial(this.prepareErrCallback, _, _, asyncCallback)
        );

        this.setPrepareTimeout(song);
    }
};

// prepare now playing and queued songs for playback
Player.prototype.prepareSongs = function() {
    async.series([
        _.bind(function(callback) {
            // prepare now-playing song if it exists and if not prepared
            if (this.queue[0]) {
                this.prepareSong(this.queue[0], callback);
            } else {
                callback(true);
            }
        }, this),
        _.bind(function(callback) {
            // prepare next song in queue if it exists and if not prepared
            if (this.queue[1]) {
                this.prepareSong(this.queue[1], callback);
            } else {
                callback(true);
            }
        }, this)
    ]);
};

// to be called whenever the queue has been modified
// this function will:
// - play back the first song in the queue if no song is playing
// - call prepareSongs()
Player.prototype.onQueueModify = function() {
    this.callHooks('preQueueModify', [this.queue]);

    // set next song as now playing
    if (!this.queue[0]) {
        this.queue.shift();
    }

    if (!this.queue.length) {
        // if the queue is now empty, do nothing
        this.callHooks('onEndOfQueue');
        this.logger.info('end of queue, waiting for more songs');
    } else {
        // else prepare songs
        this.prepareSongs();
    }
    this.callHooks('postQueueModify', [this.queue]);
};

// find song from queue
Player.prototype.searchQueue = function(backendName, songID) {
    for (var i = 0; i < this.queue.length; i++) {
        if (this.queue[i].songID === songID &&
               this.queue[i].backendName === backendName) {
            return this.queue[i];
        }
    }

    return null;
};

// make a search query to backends
Player.prototype.searchBackends = function(query, callback) {
    var resultCnt = 0;
    var allResults = {};

    _.each(this.backends, function(backend) {
        backend.search(query, _.bind(function(results) {
            resultCnt++;

            // make a temporary copy of songlist, clear songlist, check
            // each song and add them again if they are ok
            var tempSongs = _.clone(results.songs);
            allResults[backend.name] = results;
            allResults[backend.name].songs = {};

            _.each(tempSongs, function(song) {
                var err = this.callHooks('preAddSearchResult', [song]);
                if (!err) {
                    allResults[backend.name].songs[song.songID] = song;
                } else {
                    this.logger.error('preAddSearchResult hook error: ' + err);
                }
            }, this);

            // got results from all services?
            if (resultCnt >= Object.keys(this.backends).length) {
                callback(allResults);
            }
        }, this), _.bind(function(err) {
            resultCnt++;
            this.logger.error('error while searching ' + backend.name + ': ' + err);

            // got results from all services?
            if (resultCnt >= Object.keys(this.backends).length) {
                callback(allResults);
            }
        }, this));
    }, this);
};

// get rid of song in queue
// cnt can be left out for deleting only one song
Player.prototype.removeFromQueue = function(pos, cnt, onlyRemove) {
    var retval = [];
    if (!cnt) {
        cnt = 1;
    }
    pos = Math.max(0, parseInt(pos));

    if (!onlyRemove) {
        this.callHooks('preSongsRemoved', [pos, cnt]);
    }

    // remove songs from queue
    if (pos + cnt > 0) {
        if (this.queue.length) {
            // stop preparing songs we are about to remove
            // we want to limit this to this.queue.length if cnt is very large
            for (var i = 0; i < Math.min(this.queue.length, pos + cnt); i++) {
                var song = this.queue[i];

                // signal prepareError function not to run removeFromQueue again
                // TODO: try getting rid of this ugly hack (beingDeleted)...
                // TODO: more non enumerable properties, especially plugins?
                Object.defineProperty(song, 'beingDeleted', {
                    enumerable: false,
                    writable: true
                });

                song.beingDeleted = true;
                if (song.cancelPrepare) {
                    song.cancelPrepare('song deleted');
                    delete(song.cancelPrepare);
                }
            }

            retval = this.queue.splice(pos, cnt);

            if (pos === 0) {
                // now playing was deleted
                this.playbackPosition = null;
                this.playbackStart = null;
                clearTimeout(this.songEndTimeout);
                this.songEndTimeout = null;
            }
        }
    }

    if (!onlyRemove) {
        this.onQueueModify();
        this.callHooks('postSongsRemoved', [pos, cnt]);
    }

    return retval;
};

Player.prototype.moveInQueue = function(from, to, cnt) {
    if (!cnt || cnt < 1) {
        cnt = 1;
    }
    if (from < 0 || from + cnt >= this.queue.length || to + cnt >= this.queue.length) {
        return null;
    }

    this.callHooks('preSongsMoved', [from, to, cnt]);

    var songs = this.removeFromQueue(from, cnt, true);
    Array.prototype.splice.apply(this.queue, [to, 0].concat(songs));

    this.callHooks('sortQueue');
    this.onQueueModify();
    this.callHooks('postSongsMoved', [songs, from, to, cnt]);

    return songs;
};

// add songs to the queue, at optional position
Player.prototype.addToQueue = function(songs, pos) {
    if (!pos) {
        pos = this.queue.length;
    }
    if (pos < 0) {
        pos = 1;
    }
    pos = Math.min(pos, this.queue.length);

    this.callHooks('preSongsQueued', [songs, pos]);
    _.each(songs, function(song) {
        // check that required fields are provided
        if (!song.title || !song.songID || !song.backendName || !song.duration) {
            this.logger.info('required song fields not provided: ' + song.songID);
            return;
            //return 'required song fields not provided'; // TODO: this ain't gonna work
        }

        var err = this.callHooks('preSongQueued', [song]);
        if (err) {
            this.logger.error('not adding song to queue: ' + err);
        } else {
            song.timeAdded = new Date().getTime();

            this.queue.splice(pos++, 0, song);
            this.logger.info('added song to queue: ' + song.songID);
            this.callHooks('postSongQueued', [song]);
        }
    }, this);

    this.callHooks('sortQueue');
    this.onQueueModify();
    this.callHooks('postSongsQueued', [songs, pos]);
};

Player.prototype.shuffleQueue = function() {
    // don't change now playing
    var temp = this.queue.shift();
    this.queue = _.shuffle(this.queue);
    this.queue.unshift(temp);

    this.callHooks('onQueueShuffled', [this.queue]);
    this.onQueueModify();
};

// cnt can be negative to go back or zero to restart current song
Player.prototype.skipSongs = function(cnt) {
    this.npIsPlaying = false;

    // TODO: this could be replaced with a splice?
    for (var i = 0; i < Math.abs(cnt); i++) {
        if (cnt > 0) {
            if (this.queue[0]) {
                this.playedQueue.push(this.queue[0]);
            }

            this.queue.shift();
        } else if (cnt < 0) {
            if (this.playedQueue.length) {
                this.queue.unshift(this.playedQueue.pop());
            }
        }

        // ran out of songs while skipping, stop
        if (!this.queue[0]) {
            break;
        }
    }

    this.playedQueue = _.last(this.playedQueue, this.config.playedQueueSize);

    this.playbackPosition = null;
    this.playbackStart = null;
    clearTimeout(this.songEndTimeout);
    this.songEndTimeout = null;
    this.onQueueModify();
};

// TODO: userID does not belong into core...?
Player.prototype.setVolume = function(newVol, userID) {
    newVol = Math.min(1, Math.max(0, newVol));
    this.volume = newVol;
    this.callHooks('onVolumeChange', [newVol, userID]);
};

module.exports = Player;
<|endoftext|>


<commit_before>/* jshint node:true */
'use strict';

var gulp = require('gulp');

var $ = require('gulp-load-plugins')();
var browserSync = require('browser-sync');
var del = require('del');

// Paths
var appPath = 'app';
var distPath = 'dist';

var paths = {
  app: {
    img: appPath + '/img/**/*',
    css: appPath + '/css/**/*.css',
    js: appPath + '/js/**/*.js',
    fonts: appPath + '/fonts/*.{eot,svg,ttf,woff}',
    icons: appPath + '/icons/*',
    html: appPath + '/**/*.{html,htm}'
  },
  dist: {
    fonts: distPath + '/fonts',
    icons: distPath + '/icons',
    img: distPath + '/img'
  },
};

// Options
var options = {
  autoprefixer: {
    browsers: [
      '> 2%',
      'last 2 versions',
      'Firefox ESR',
      'ie >= 9'
    ],
    cascade: false
  },
  htmlmin: {
    removeComments: true,
    collapseWhitespace: true
  },
  imagemin: {
    progressive: true,
    svgoPlugins: [
      {removeViewBox: false}
    ],
    use: []
  }
};

// linters

  // js
  gulp.task('lintjs', function() {
    return gulp.src(paths.app.js)
      .pipe($.jshint())
      .pipe($.jshint.reporter('jshint-stylish'));
  });

  // css
  gulp.task('lintcss', function() {
    return gulp.src(paths.app.css)
      .pipe($.csslint())
      .pipe($.csslint.reporter());
  });

// clean

  gulp.task('clean', function () {
    del([
      distPath + '/*'
    ]);
  });

// compile

  gulp.task('compile', function () {
    var assets = $.useref.assets();
    return gulp.src(paths.app.html)
      .pipe(assets)
      .pipe($.if('*.js', $.uglify()))
      .pipe($.if('*.css', $.autoprefixer(options.autoprefixer)))
      .pipe($.if('*.css', $.csso()))
      .pipe($.rev())
      .pipe(assets.restore())
      .pipe($.useref())
      .pipe($.revReplace())
      .pipe($.if('*.{html, htm}', $.htmlmin(options.htmlmin)))
      .pipe(gulp.dest(distPath));
  });

  gulp.task('img', function () {
      return gulp.src(paths.app.img)
          .pipe($.imagemin(options.imagemin))
          .pipe(gulp.dest(paths.dist.img));
  });

  gulp.task('fonts', function () {
    return gulp.src(paths.app.fonts)
      .pipe($.flatten())
      .pipe(gulp.dest(paths.dist.fonts));
  });

  gulp.task('icons', function () {
    return gulp.src(paths.app.icons)
      .pipe(gulp.dest(paths.dist.icons));
  });

  gulp.task('extras', function () {
    return gulp.src(appPath + '/browserconfig.xml')
      .pipe(gulp.dest(distPath));
  });

// Browser sync

  gulp.task('browser-sync', function() {
    return browserSync({
      server: {
        baseDir: distPath,
        index: 'index.html'
      },
      browser: 'chrome'
    });
  });

  gulp.task('browser-sync-dev', function() {
    return browserSync({
      server: {
        baseDir: appPath,
        index: 'index.html'
      },
      browser: 'chrome'
    });
  });

// Main gulp tasks

  // builds all files and runs from dist directory
  gulp.task('default', ['lintjs', 'compile', 'img', 'fonts', 'icons',
                        'extras', 'browser-sync']);

  // skips building phase and runs from dist directory
  gulp.task('run', ['browser-sync']);

  // runs from app directory
  gulp.task('dev', ['browser-sync-dev'], function() {
    // watch for JS changes
    gulp.watch(paths.app.js, ['lintjs', browserSync.reload]);

    // watch for CSS changes
    gulp.watch(paths.app.css, browserSync.reload);

    // watch for HTML changes
    gulp.watch(paths.app.html, browserSync.reload);
  });
<commit_msg>gulpfile.js: removed unused task<commit_after>/* jshint node:true */
'use strict';

var gulp = require('gulp');

var $ = require('gulp-load-plugins')();
var browserSync = require('browser-sync');
var del = require('del');

// Paths
var appPath = 'app';
var distPath = 'dist';

var paths = {
  app: {
    img: appPath + '/img/**/*',
    css: appPath + '/css/**/*.css',
    js: appPath + '/js/**/*.js',
    fonts: appPath + '/fonts/*.{eot,svg,ttf,woff}',
    icons: appPath + '/icons/*',
    html: appPath + '/**/*.{html,htm}'
  },
  dist: {
    fonts: distPath + '/fonts',
    icons: distPath + '/icons',
    img: distPath + '/img'
  },
};

// Options
var options = {
  autoprefixer: {
    browsers: [
      '> 2%',
      'last 2 versions',
      'Firefox ESR',
      'ie >= 9'
    ],
    cascade: false
  },
  htmlmin: {
    removeComments: true,
    collapseWhitespace: true
  },
  imagemin: {
    progressive: true,
    svgoPlugins: [
      {removeViewBox: false}
    ],
    use: []
  }
};

// linters

  // js
  gulp.task('lintjs', function() {
    return gulp.src(paths.app.js)
      .pipe($.jshint())
      .pipe($.jshint.reporter('jshint-stylish'));
  });

  // css
  gulp.task('lintcss', function() {
    return gulp.src(paths.app.css)
      .pipe($.csslint())
      .pipe($.csslint.reporter());
  });

// clean

  gulp.task('clean', function () {
    del([
      distPath + '/*'
    ]);
  });

// compile

  gulp.task('compile', function () {
    var assets = $.useref.assets();
    return gulp.src(paths.app.html)
      .pipe(assets)
      .pipe($.if('*.js', $.uglify()))
      .pipe($.if('*.css', $.autoprefixer(options.autoprefixer)))
      .pipe($.if('*.css', $.csso()))
      .pipe($.rev())
      .pipe(assets.restore())
      .pipe($.useref())
      .pipe($.revReplace())
      .pipe($.if('*.{html, htm}', $.htmlmin(options.htmlmin)))
      .pipe(gulp.dest(distPath));
  });

  gulp.task('img', function () {
      return gulp.src(paths.app.img)
          .pipe($.imagemin(options.imagemin))
          .pipe(gulp.dest(paths.dist.img));
  });

  gulp.task('fonts', function () {
    return gulp.src(paths.app.fonts)
      .pipe($.flatten())
      .pipe(gulp.dest(paths.dist.fonts));
  });

  gulp.task('icons', function () {
    return gulp.src(paths.app.icons)
      .pipe(gulp.dest(paths.dist.icons));
  });

// Browser sync

  gulp.task('browser-sync', function() {
    return browserSync({
      server: {
        baseDir: distPath,
        index: 'index.html'
      },
      browser: 'chrome'
    });
  });

  gulp.task('browser-sync-dev', function() {
    return browserSync({
      server: {
        baseDir: appPath,
        index: 'index.html'
      },
      browser: 'chrome'
    });
  });

// Main gulp tasks

  // builds all files and runs from dist directory
  gulp.task('default', ['lintjs', 'compile', 'img', 'fonts', 'icons',
                        'browser-sync']);

  // skips building phase and runs from dist directory
  gulp.task('run', ['browser-sync']);

  // runs from app directory
  gulp.task('dev', ['browser-sync-dev'], function() {
    // watch for JS changes
    gulp.watch(paths.app.js, ['lintjs', browserSync.reload]);

    // watch for CSS changes
    gulp.watch(paths.app.css, browserSync.reload);

    // watch for HTML changes
    gulp.watch(paths.app.html, browserSync.reload);
  });
<|endoftext|>


<commit_before>/*
//1 get user profile
//2 get rit of beebop error
//3 click more buttons
//4 write json for hypo
*/

'use strict';

const _ = require('lodash');
const Script = require('smooch-bot').Script;
const scriptRules = require('./script.json');

var myDate = new Date();
var groet = '';

/* hour is before noon */
if ( myDate.getHours() < 8 )
{
  groet = "Goeiemorgen 🌝.  Bedankt voor je bezoek maar op dit moment is Independer echter gesloten. 🕘 Uiteraard kun je met onze IndyBot verder praten maar er is helaas niemand die jouw vraag specifiek kan beantwoorden. Je kan je vraag ook doormailen 📩 naar info@independer. Dan komt het altijd goed.";
}
else
if ( myDate.getHours() >= 8 && myDate.getHours() <=12 )
{
  groet = "Goeiemorgen 🌝 ";
}
else  /* Hour is from noon to 5pm (actually to 5:59 pm) */
if ( myDate.getHours() >= 12 && myDate.getHours() <= 17 )
{
groet = "Goedendag 🌞";
}
else  /* the hour is after 5pm, so it is between 6pm and midnight */
if ( myDate.getHours() > 17&& myDate.getHours() <= 20 )
{
groet = "Goedenavond 🌙";
}
else
if (myDate.getHours() > 20  || myDate.getHours() <= 24 )
{
  groet = "Goedenavond 🌙  Bedankt voor je bezoek maar op dit moment is Independer echter gesloten. 🕘 Uiteraard kun je met onze IndyBot verder praten maar er is helaas niemand die jouw vraag specifiek kan beantwoorden. Je kan je vraag ook doormailen 📩 naar info@independer. Dan komt het altijd goed.";
}
else  /* the hour is not between 0 and 24, so something is wrong */
{
groet = "Welkom. ";
}

function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}

module.exports = new Script({

 processing: {
prompt: (bot) => bot.say('![](http://www.bieg.nl/beeld/speechbubble.gif)'),
 receive: () => 'processing'
 },

start: {
    receive: (bot,message) => {
        const opening = message.text.trim().toUpperCase();

        return bot.say(`${groet}... Wat voor soort hypotheek zoek je? `)
        .then(() => bot.say(`![](http://www.bieg.nl/beeld/woningen.jpg)`))
        .then(() => bot.say(`%[Starters Hypotheek](postback:hypotheektype_starter)`))
        .then(() => bot.say (`%[Nieuwe hypotheek](postback:hypotheektype_nieuw) `))
        .then(() => bot.say (`%[Hypotheek oversluiten](postback:hypotheektype_oversluiten)`))
        .then(() => 'selecteerHypotheek');
    }
},

speak: {
          receive: (bot, message) => {

              const upperText = message.text.trim().toUpperCase();

              function updateSilent() {
                  switch (upperText) {
                      case "CONNECT ME":
                          return bot.setProp("silent", true);
                      case "DISCONNECT":
                          return bot.setProp("silent", false);
                      default:
                          return Promise.resolve();
                  }
              }

              function getSilent() {
                  return bot.getProp("silent");
              }

              function processMessage(isSilent) {
                  if (isSilent) {
                      return Promise.resolve("speak");
                  }

                  /*  CREATE EXTRA JSON FILE FOR HYPO AND INCLUDE IT */
                  if (!_.has(scriptRules, upperText)) {
                      return bot.say('![](http://www.bieg.nl/beeld/speechbubble.gif)').then(() => 'speak');
                  }

                  var response = scriptRules[upperText];
                  var lines = response.split('\n');

                  var p = Promise.resolve();
                  _.each(lines, function(line) {
                      line = line.trim();
                      p = p.then(function() {
                          console.log(line);
                          return wait(50).then(function() {
                              return bot.say(line);
                          });
                      });
                  });

                  return p.then(() => 'speak');
              }

              return updateSilent()
                  .then(getSilent)
                  .then(processMessage);
          }
},

selecteerHypotheek: {
    receive: (bot, message) => {
      switch(message.text) {
      case 'Hoi':
            return bot.say(`${groet} waar ben je naar op zoek? %[Starters hypotheek](postback:hypotheektype_starter) %[Nieuwe hypotheek](postback:hypotheektype_nieuw) %[Hypotheek oversluiten](postback:hypotheektype_oversluiten)`)
            .then(() => 'askName')
          break;
      case 'Starters Hypotheek':
          return bot.say(`Wat voor type woning zoek je? `)
          .then(() => bot.say(`%[🏬 Appartement](postback:hypotheekkeuze_appartement) %[🏠 Huis](postback:hypotheekkeuze_huis) %[📭 Vakantiewoning](postback:hypotheekkeuze_vakantiewoning)`))
          .then(() => 'woningType')
              break;
        case 'Nieuwe hypotheek':
          return bot.say(`😞 Helaas biedt Independer momenteel alleen Starters een hypotheek aan.`)
          .then(() => bot.say(`Via onderstaande link kun je de beste hypotheekadviseur voor jou vinden. %[💼 Zoek Hypotheek Adviseur](https://www.independer.nl/hypotheekadviseur/jelocatie.aspx)`))
            .then(() => 'finish')
          break;
        case 'Hypotheek oversluiten':
          return bot.say(`😟 Het spijt me maar op dit moment biedt Independer alleen  Starters een hypotheek.`)
          .then(()=> bot.say(`Als het allemaal wel zo ver is, wil je dan een update ontvangen? %[Ja, dat wil ik wel](postback:update_ja) %[Nee, dat hoeft niet](postback:update_nee)`))
            .then(() => 'updateOntvangen')
          break;
        default:
          return bot.say(`![](http://www.bieg.nl/beeld/speechbubble.gif)`)
            .then(() => 'processing')
          break;
      }
    }
},

updateOntvangen: {
    receive: (bot, message) => {
      switch(message.text) {
        case 'Ja, dat wil ik wel':
            return bot.say(`👍 Leuk, dan houd ik  je op de hoogte zodra er weer nieuws is.`)
            .then(() => 'update_ja');
          break;
          case 'Nee, dat hoeft niet':
              receive: () => 'bye'
              break;
          default:
            receive => 'processing'
            break;
      }
    }
},

update_ja: {
  prompt: (bot) => bot.say('Wat is je email adres?'),
      receive: (bot, message) => {
          const email=message.text;
          return bot.setProp('email', email)
              .then(()  => bot.say(`Ok - ✉  dan hou ik je via ${email} op de hoogte.`))
              .then(()  =>'lastCheck')
    }
},
update_nee: {
  receive: () => 'bye'
},

hypotheekStarter: {
    receive: () => 'askName'
},

hypotheektype_nieuw: {
    prompt: (bot) => bot.say('Independer biedt momenteel alleen voor Starters een hypotheek. Onderstaande link bied je meer informatie %[Hypotheek Adviseur](https://www.independer.nl/hypotheekadviseur/intro.aspx)'),
    receive: () => 'processing'
},

woningType: {
  receive: (bot, message) => {
    switch(message.text) {
        case '🏬 Appartement':
          return bot.say(`Nice!`)
          .then(() => 'vervolgVragen')
          break;
      case '🏠 Huis':
          return bot.say(`Leuk`)
          .then(() => 'vervolgVragen')
          break;
    case '📭 Vakantiewoning':
          return bot.say(`Gezellig`)
          .then(() => 'vervolgVragen')
          break;
      default:
        receive => 'processing'
        break;
        }
    }
},

vervolgVragen: {
  prompt: (bot) => bot.say(`Hoe heet je eigelijk? 😋`),
          receive: (bot, message) => {
              const Name = message.text;
              return bot.setProp('Name', Name)
                  .then(() => bot.say(`Hoi ${Name}. 📋 Ik heb nog wat vragen voor je om verder te kunnen.`))
                  .then(() => 'processing')
            }
},

lastCheck: {
    prompt: (bot) => bot.say(' Is er nog iets waar ik  je bij kan helpen?  🔶  %[Ik zoek meer informatie](postback:meerInfo) %[Nee hoor](postback:bye)'),
          receive: (bot, message) => {
            switch(message.text) {
              case 'Nee hoor':
                  receive: ()  => 'bye'
                  break;
             case 'Ik zoek meer informatie':
                  receive: () => 'speak'
                  break;
            case 'Nee':
                  receive: () => 'bye'
                  break;
            default:
              return bot.say('![](http://www.bieg.nl/beeld/speechbubble.gif)')
                .then(() => 'processing')
              break;
            }
        }
},

meerInfo: {
  prompt: (bot) => bot.say('bestand'),
   receive: () => 'processing'
},

bye: {
    prompt: (bot) => bot.say('Fijn je gesproken te hebben. Bedankt voor je tijd ⏲'),
    receive: ()  => 'finish'
},

// error: {
// prompt: (bot) => bot.say('Sorry - kun je dat nog eens zeggen?  Er ging iets mis...'),
// receive: () => 'pri'
// },

finish: {
receive: () => 'finish'
}
});
<commit_msg>lhkl<commit_after>/*
//1 get user profile
//2 get rit of beebop error
//3 click more buttons
//4 write json for hypo
*/

'use strict';

const _ = require('lodash');
const Script = require('smooch-bot').Script;
const scriptRules = require('./script.json');

var myDate = new Date();
var groet = '';

/* hour is before noon */
if ( myDate.getHours() < 8 )
{
  groet = "Goeiemorgen 🌝.  Bedankt voor je bezoek maar op dit moment is Independer echter gesloten. 🕘 Uiteraard kun je met onze IndyBot verder praten maar er is helaas niemand die jouw vraag specifiek kan beantwoorden. Je kan je vraag ook doormailen 📩 naar info@independer. Dan komt het altijd goed.";
}
else
if ( myDate.getHours() >= 8 && myDate.getHours() <=12 )
{
  groet = "Goeiemorgen 🌝 ";
}
else  /* Hour is from noon to 5pm (actually to 5:59 pm) */
if ( myDate.getHours() >= 12 && myDate.getHours() <= 17 )
{
groet = "Goedendag 🌞";
}
else  /* the hour is after 5pm, so it is between 6pm and midnight */
if ( myDate.getHours() > 17&& myDate.getHours() <= 20 )
{
groet = "Goedenavond 🌙";
}
else
if (myDate.getHours() > 20  || myDate.getHours() <= 24 )
{
  groet = "Goedenavond 🌙  Bedankt voor je bezoek maar op dit moment is Independer echter gesloten. 🕘 Uiteraard kun je met onze IndyBot verder praten maar er is helaas niemand die jouw vraag specifiek kan beantwoorden. Je kan je vraag ook doormailen 📩 naar info@independer. Dan komt het altijd goed.";
}
else  /* the hour is not between 0 and 24, so something is wrong */
{
groet = "Welkom. ";
}

function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}

module.exports = new Script({

 processing: {
prompt: (bot) => bot.say('![](http://www.bieg.nl/beeld/speechbubble.gif)'),
 receive: () => 'processing'
 },

start: {
    receive: (bot,message) => {
        const opening = message.text.trim().toUpperCase();

        return bot.say(`${groet}... Wat voor soort hypotheek zoek je? `)
        .then(() => bot.say(`![](http://www.bieg.nl/beeld/woningen.jpg)`))
        .then(() => bot.say(`%[Starters Hypotheek](postback:hypotheektype_starter)`))
        .then(() => bot.say (`%[Nieuwe hypotheek](postback:hypotheektype_nieuw) `))
        .then(() => bot.say (`%[Hypotheek oversluiten](postback:hypotheektype_oversluiten)`))
        .then(() => 'selecteerHypotheek');
    }
},

.then(
                      => function updateUser() {
                        givenName: 'New',
                        surname: 'Name'
                      })

speak: {
          receive: (bot, message) => {

              const upperText = message.text.trim().toUpperCase();

              function updateSilent() {
                  switch (upperText) {
                      case "CONNECT ME":
                          return bot.setProp("silent", true);
                      case "DISCONNECT":
                          return bot.setProp("silent", false);
                      default:
                          return Promise.resolve();
                  }
              }

              function getSilent() {
                  return bot.getProp("silent");
              }

              function processMessage(isSilent) {
                  if (isSilent) {
                      return Promise.resolve("speak");
                  }

                  /*  CREATE EXTRA JSON FILE FOR HYPO AND INCLUDE IT */
                  if (!_.has(scriptRules, upperText)) {
                      return bot.say('![](http://www.bieg.nl/beeld/speechbubble.gif)').then(() => 'speak');
                  }

                  var response = scriptRules[upperText];
                  var lines = response.split('\n');

                  var p = Promise.resolve();
                  _.each(lines, function(line) {
                      line = line.trim();
                      p = p.then(function() {
                          console.log(line);
                          return wait(50).then(function() {
                              return bot.say(line);
                          });
                      });
                  });

                  return p.then(() => 'speak');
              }

              return updateSilent()
                  .then(getSilent)
                  .then(processMessage);
          }
},

selecteerHypotheek: {
    receive: (bot, message) => {
      switch(message.text) {
      case 'Hoi':
            return bot.say(`${groet} waar ben je naar op zoek? %[Starters hypotheek](postback:hypotheektype_starter) %[Nieuwe hypotheek](postback:hypotheektype_nieuw) %[Hypotheek oversluiten](postback:hypotheektype_oversluiten)`)
            .then(() => 'askName')
          break;
      case 'Starters Hypotheek':
          return bot.say(`Wat voor type woning zoek je? `)
          .then(() => bot.say(`%[🏬 Appartement](postback:hypotheekkeuze_appartement) %[🏠 Huis](postback:hypotheekkeuze_huis) %[📭 Vakantiewoning](postback:hypotheekkeuze_vakantiewoning)`))
          .then(() => 'woningType')
              break;
        case 'Nieuwe hypotheek':
          return bot.say(`😞 Helaas biedt Independer momenteel alleen Starters een hypotheek aan.`)
          .then(() => bot.say(`Via onderstaande link kun je de beste hypotheekadviseur voor jou vinden. %[💼 Zoek Hypotheek Adviseur](https://www.independer.nl/hypotheekadviseur/jelocatie.aspx)`))
            .then(() => 'finish')
          break;
        case 'Hypotheek oversluiten':
          return bot.say(`😟 Het spijt me maar op dit moment biedt Independer alleen  Starters een hypotheek.`)
          .then(()=> bot.say(`Als het allemaal wel zo ver is, wil je dan een update ontvangen? %[Ja, dat wil ik wel](postback:update_ja) %[Nee, dat hoeft niet](postback:update_nee)`))
            .then(() => 'updateOntvangen')
          break;
        default:
          return bot.say(`![](http://www.bieg.nl/beeld/speechbubble.gif)`)
            .then(() => 'processing')
          break;
      }
    }
},

updateOntvangen: {
    receive: (bot, message) => {
      switch(message.text) {
        case 'Ja, dat wil ik wel':
            return bot.say(`👍 Leuk, dan houd ik  je op de hoogte zodra er weer nieuws is.`)
            .then(() => 'update_ja');
          break;
          case 'Nee, dat hoeft niet':
              receive: () => 'bye'
              break;
          default:
            receive => 'processing'
            break;
      }
    }
},

update_ja: {
  prompt: (bot) => bot.say('Wat is je email adres?'),
      receive: (bot, message) => {
          const email=message.text;
          return bot.setProp('email', email)
              .then(()  => bot.say(`Ok - ✉  dan hou ik je via ${email} op de hoogte.`))
              .then(()  =>'lastCheck')
    }
},
update_nee: {
  receive: () => 'bye'
},

hypotheekStarter: {
    receive: () => 'askName'
},

hypotheektype_nieuw: {
    prompt: (bot) => bot.say('Independer biedt momenteel alleen voor Starters een hypotheek. Onderstaande link bied je meer informatie %[Hypotheek Adviseur](https://www.independer.nl/hypotheekadviseur/intro.aspx)'),
    receive: () => 'processing'
},

woningType: {
  receive: (bot, message) => {
    switch(message.text) {
        case '🏬 Appartement':
          return bot.say(`Nice!`)
          .then(() => 'vervolgVragen')
          break;
      case '🏠 Huis':
          return bot.say(`Leuk`)
          .then(() => 'vervolgVragen')
          break;
    case '📭 Vakantiewoning':
          return bot.say(`Gezellig`)
          .then(() => 'vervolgVragen')
          break;
      default:
        receive => 'processing'
        break;
        }
    }
},

vervolgVragen: {
  prompt: (bot) => bot.say(`Hoe heet je eigelijk? 😋`),
          receive: (bot, message) => {
              const Name = message.text;
              return bot.setProp('Name', Name)
                  .then(() => bot.say(`Hoi ${Name}. 📋 Ik heb nog wat vragen voor je om verder te kunnen.`))
                  .then(() => 'processing')            }
},

lastCheck: {
    prompt: (bot) => bot.say(' Is er nog iets waar ik  je bij kan helpen?  🔶  %[Ik zoek meer informatie](postback:meerInfo) %[Nee hoor](postback:bye)'),
          receive: (bot, message) => {
            switch(message.text) {
              case 'Nee hoor':
                  receive: ()  => 'bye'
                  break;
             case 'Ik zoek meer informatie':
                  receive: () => 'speak'
                  break;
            case 'Nee':
                  receive: () => 'bye'
                  break;
            default:
              return bot.say('![](http://www.bieg.nl/beeld/speechbubble.gif)')
                .then(() => 'processing')
              break;
            }
        }
},

meerInfo: {
  prompt: (bot) => bot.say('bestand'),
   receive: () => 'processing'
},

bye: {
    prompt: (bot) => bot.say('Fijn je gesproken te hebben. Bedankt voor je tijd ⏲'),
    receive: ()  => 'finish'
},

// error: {
// prompt: (bot) => bot.say('Sorry - kun je dat nog eens zeggen?  Er ging iets mis...'),
// receive: () => 'pri'
// },

finish: {
receive: () => 'finish'
}
});
<|endoftext|>


<commit_before>module.exports = function (shipit) {
  require('shipit-deploy')(shipit);

  shipit.initConfig({
    default: {
      workspace: '/tmp/<%= appName %>',
      deployTo: '~/<%= appName %>',
      repositoryUrl: '<%= repositoryUrl %>',
      ignores: ['.git', 'tests', '.gitignore', 'devops', 'client/tests', 'node_modules', 'client/node_modules'],
      rsync: ['--del'],
      keepReleases: 3,
      key: '~/.ssh/id_rsa',
      shallowClone: true,
    },
    staging: {
      branch: 'master',
      servers: 'www-data@<%= stagingIpAddress %>',
    },
    prod: {
      branch: 'master',
      servers: 'www-data@<%= prodIpAddress %>',
    },
  });

  const yarn = '/usr/local/lib/npm/bin/yarn';

  const npmBackInstall = function () {
      shipit.log('INSTALL BACKEND NODE_MODULES');
    return shipit.remote(`cd ${shipit.releasePath} && ${yarn} install`);
  };

  const npmFrontInstall = function () {
      shipit.log('INSTALLING FRONTEND NODE_MODULES');
    return shipit.remote(`cd ${shipit.releasePath}/client && ${yarn} install`);
  };

  const npmFrontCompile = function () {
    shipit.log('START COMPILING FRONTEND CODE');
    return shipit.remote(`cd ${shipit.releasePath} && npm run build:client &> /dev/null`);
  }

  const runMigrations = function () {
    shipit.log('RUNNNING MIGRATIONS');
    return shipit.remote(`cd ${shipit.releasePath} && npm run migrate:up`);
  }

  const restartServer = function () {
    shipit.log('RESTARTING SERVER');
    return shipit.remote(`cd ${shipit.releasePath} && npm run start:prod`);
  }

  shipit.on('updated', function() {
    return shipit.start('install');
  });

  // blTask will block other tasks during its execution (synchronous)
  shipit.blTask('install', function() {
    return npmBackInstall()
      .then(npmFrontInstall)
      .then(npmFrontCompile)
      .then(runMigrations)
      .then(restartServer)
      .then(() => {
        shipit.log('Deployment successfull!\n');
      })
      .catch(function (err) {
        shipit.log(`Deployment failed: ${err}`);
      });
  });
};
<commit_msg>Update deploy script<commit_after>module.exports = function (shipit) {
  require('shipit-deploy')(shipit);

  shipit.initConfig({
    default: {
      workspace: '/tmp/<%= appName %>',
      deployTo: '~/<%= appName %>',
      repositoryUrl: '<%= repositoryUrl %>',
      ignores: ['.git', 'tests', '.gitignore', 'devops', 'client/tests', 'node_modules', 'client/node_modules'],
      rsync: ['--del'],
      keepReleases: 3,
      key: '~/.ssh/id_rsa',
      shallowClone: true,
    },
    staging: {
      branch: 'master',
      servers: 'www-data@<%= stagingIpAddress %>',
    },
    prod: {
      branch: 'master',
      servers: 'www-data@<%= prodIpAddress %>',
    },
  });

  const yarn = '/usr/local/lib/npm/bin/yarn';

  const installDependencies = function () {
      shipit.log('INSTALL NODE_MODULES');
    return shipit.remote(`cd ${shipit.releasePath} && ${yarn} install`);
  };

  const npmFrontCompile = function () {
    shipit.log('START COMPILING FRONTEND CODE');
    return shipit.remote(`cd ${shipit.releasePath} && npm run build:client &> /dev/null`);
  }

  const runMigrations = function () {
    shipit.log('RUNNNING MIGRATIONS');
    return shipit.remote(`cd ${shipit.releasePath} && npm run migrate:up`);
  }

  const restartServer = function () {
    shipit.log('RESTARTING SERVER');
    return shipit.remote(`cd ${shipit.releasePath} && npm run start:prod`);
  }

  shipit.on('updated', function() {
    return shipit.start('install');
  });

  // blTask will block other tasks during its execution (synchronous)
  shipit.blTask('install', function() {
    return installDependencies()
      .then(npmFrontCompile)
      .then(runMigrations)
      .then(restartServer)
      .then(() => {
        shipit.log('Deployment successfull!\n');
      })
      .catch(function (err) {
        shipit.log(`Deployment failed: ${err}`);
      });
  });
};
<|endoftext|>


<commit_before>describe('Service: sessionStorage', function () {
  var sessionStorage;

  beforeEach(module('sessionStorage'));

  beforeEach(inject(function(_sessionStorage_){
    sessionStorage = _sessionStorage_;
  }));

  describe('#setItem', function(){
    it('should save an json encoded object under the given key in the window.sessionStorage', function(){
      sessionStorage.setItem('key', 'value');
      expect(window.sessionStorage.getItem('key')).toBe('"value"');

      sessionStorage.setItem('key2', {ololo: true});
      expect(window.sessionStorage.getItem('key2')).toBe('{"ololo":true}');
    });
  })

});<commit_msg>test(sessionStorage)<commit_after>describe('Service: sessionStorage', function () {
  var sessionStorage;

  beforeEach(module('sessionStorage'));

  beforeEach(inject(function(_sessionStorage_){
    sessionStorage = _sessionStorage_;
  }));

  describe('#setItem', function(){
    it('should save an json encoded object under the given key in the window.sessionStorage', function(){
      sessionStorage.setItem('key', 'value');
      expect(window.sessionStorage.getItem('key')).toBe('"value"');

      sessionStorage.setItem('key2', {ololo: true});
      expect(window.sessionStorage.getItem('key2')).toBe('{"ololo":true}');
    });
  });

  describe('#getItem', function(){
    it('should return an object for a given key', function(){
      sessionStorage.setItem('key', {ololo: true});
      expect(sessionStorage.getItem('key')).toEqual({ololo: true});
    })
  });

  describe('#removeItem', function(){
    it('should delete object from storage', function(){
      sessionStorage.setItem('key', {ololo: true});
      expect(sessionStorage.getItem('key')).toEqual({ololo: true});
      sessionStorage.removeItem('key');
      expect(sessionStorage.getItem('key')).toEqual(null);
    })
  });

});<|endoftext|>


<commit_before>/*!
 * ${copyright}
 */

/* global ActiveXObject:false */

// Provides control sap.m.PDFViewer.
sap.ui.define([
		"jquery.sap.global",
		"./library",
		"sap/ui/core/Control",
		"sap/ui/Device",
		"sap/m/PDFViewerRenderManager"
	],
	function (jQuery, library, Control, Device, PDFViewerRenderManager) {
		"use strict";

		var aAllowedMimeTypes = Object.freeze([
			"application/pdf",
			"application/x-google-chrome-pdf"
		]);

		function isSupportedMimeType(sMimeType) {
			var iFoundIndex = aAllowedMimeTypes.indexOf(sMimeType);
			return iFoundIndex > -1;
		}

		/**
		 * Definition of PDFViewer control
		 *
		 * @param {string} [sId] id for the new control, generated automatically if no id is given
		 * @param {object} [mSettings] initial settings for the new control
		 *
		 * @class
		 * Allow displaying of pdf files and provides useful API.
		 * @extends sap.ui.core.Control
		 *
		 * @author SAP SE
		 * @version ${version}
		 *
		 * @constructor
		 * @public
		 * @alias sap.m.PDFViewer
		 * @ui5-metamodel This control/element also will be described in the UI5 (legacy) designtime metamodel
		 */
		var PDFViewer = Control.extend("sap.m.PDFViewer",
			/** @lends sap.m.PDFViewer.prototype */
			{
				metadata: {
					library: "sap.m",
					properties: {
						/**
						 * Defines the height of the PDF viewer control, respective to the height of
						 * the parent container. Can be set to a percent, pixel, or em value.
						 */
						height : {type : "sap.ui.core.CSSSize", group : "Dimension", defaultValue : "100%"},
						/**
						 * Defines the width of the PDF viewer control, respective to the width of the
						 * parent container. Can be set to a percent, pixel, or em value.
						 */
						width : {type : "sap.ui.core.CSSSize", group : "Dimension", defaultValue : "100%"},
						/**
						 * Specifies the path to the PDF file to display. Can be set to a relative or
						 * an absolute path.
						 */
						source: {type: "sap.ui.core.URI", group: "Misc", defaultValue: null},
						/**
						 * A custom error message that is displayed when the PDF file cannot be loaded.
						 * the PDF file.
						 */
						errorMessage: {type: "string", group: "Misc", defaultValue: null},
						/**
						 * A custom text that is displayed instead of the loaded PDF frame when an error
						 * occurs.
						 */
						errorPlaceholderMessage: {type: "string", group: "Misc", defaultValue: null},
						/**
						 * A custom title for the PDF viewer popup dialog. Works only if the PDF viewer
						 * is set to open in a popup dialog.
						 */
						popupHeaderTitle: {type: "string", group: "Misc", defaultValue: null}
					},
					aggregations: {
						/**
						 * A custom control that can be used instead of the error message specified by the
						 * errorPlaceholderMessage property.
						 */
						errorPlaceholder: {type : "sap.ui.core.Control", multiple : false},
						/**
						 * A multiple aggregation for buttons that can be added to the footer of the popup
						 * dialog. Works only if the PDF viewer is set to open in a popup dialog.
						 */
						popupButtons: {type: "sap.m.Button", multiple: true, singularName: "popupButton"}
					},
					events: {
						/**
						 * This event is fired when a PDF file is loaded. If the PDF is loaded in smaller chunks,
						 * this event is fired as often as defined by the browser's plugin. This may happen after
						 * a couple chunks are processed.
						 */
						loaded: {},
						/**
						 * This event is fired when there is an error loading the PDF file.
						 */
						error: {},
						/**
						 * This event is fired when the PDF viewer control cannot check the loaded content. For
						 * example, the default configuration of the Mozilla Firefox browser may not allow checking
						 * the loaded content. This may also happen when the source PDF file is stored in a different
						 * domain.
						 */
						sourceValidationFailed: {}
					}
				}
			});

		/**
		 * @returns {boolean}
		 * @private
		 */
		PDFViewer._isPdfPluginEnabled = function () {
			var bIsEnabled = true;
			if (Device.browser.firefox) {
				// https://bugzilla.mozilla.org/show_bug.cgi?id=1293406
				// mimeType is missing for firefox even though it is enabled
				return bIsEnabled;
			}

			if (Device.browser.internet_explorer) {
				// hacky code how to recognize that pdf plugin is installed and enabled
				try {
					new ActiveXObject("AcroPDF.PDF");
				} catch (e) {
					bIsEnabled = false;
				}

				return bIsEnabled;
			}

			var aMimeTypes = navigator.mimeTypes;
			bIsEnabled = aAllowedMimeTypes.some(function (sAllowedMimeType) {
				var oMimeTypeItem = aMimeTypes.namedItem(sAllowedMimeType);
				return oMimeTypeItem !== null;
			});

			return bIsEnabled;
		};

		/**
		 * Lifecycle method
		 *
		 * @private
		 */
		PDFViewer.prototype.init = function () {
			// helper object that holds the references of nested objects
			this._objectsRegister = {};

			// state variable that shows the state of popup (rendering of pdf in popup requires it)
			this._bIsPopupOpen = false;

			this._initPopupControl();
			this._initPlaceholderLinkControl();
			this._initPlaceholderMessagePageControl();

			this._initControlState();
		};

		/**
		 * Setup state variables to default state
		 *
		 * @private
		 */
		PDFViewer.prototype._initControlState = function () {
			// state property that control if the embedded pdf should or should not rendered.
			this._bRenderPdfContent = true;

			// detect that beforeunload was fired (IE only)
			this._bOnBeforeUnloadFired = false;
		};

		/**
		 * Lifecycle method
		 *
		 * @private
		 */
		PDFViewer.prototype.onAfterRendering = function () {
			var fnInitIframeElement = function () {
				// cant use attachBrowserEvent because it attach event to component root node (this.$())
				// load event does not bubble so it has to be bind directly to iframe element
				var oIframeElement = this._getIframeDOMElement();
				var oIframeContentWindow = jQuery(oIframeElement.get(0).contentWindow);

				if (Device.browser.internet_explorer) {
					// being special does not mean useful
					// https://connect.microsoft.com/IE/feedback/details/809377/ie-11-load-event-doesnt-fired-for-pdf-in-iframe

					// onerror does not works on IE. Therefore readyonstatechange and unload events are used for error detection.
					// When invalid response is received (404, etc.), readystatechange is not fired but unload is.
					// When valid response is received, then readystatechange and 'complete' state of target's element is received.
					oIframeContentWindow.on("beforeunload", this._onBeforeUnloadListener.bind(this));
					oIframeContentWindow.on("readystatechange", this._onReadyStateChangeListener.bind(this));

					// some error codes load html file and fires loadEvent
					oIframeElement.on("load", this._onLoadIEListener.bind(this));
				} else {
					// normal browsers supports load events as specification said
					oIframeElement.on("load", this._onLoadListener.bind(this));
				}
				oIframeElement.on("error", this._onErrorListener.bind(this));

				var sParametrizedSource = this.getSource();
				var iCrossPosition = this.getSource().indexOf("#");
				if (iCrossPosition > -1) {
					sParametrizedSource = sParametrizedSource.substr(0, iCrossPosition);
				}
				sParametrizedSource += "#view=FitH";
				if (jQuery.sap.validateUrl(sParametrizedSource)) {
					oIframeElement.attr("src", encodeURI(sParametrizedSource));
				} else {
					this._fireErrorEvent();
				}
			}.bind(this);

			try {
				this.setBusy(true);
				fnInitIframeElement();
			} catch (error) {
				this.setBusy(false);
			}
		};

		/**
		 * @private
		 */
		PDFViewer.prototype._fireErrorEvent = function () {
			this.setBusy(false);
			this._bRenderPdfContent = false;
			// calls controls invalidate because the error state should be render.
			// It is controlled by the state variable called _bRenderPdfContent
			// The main invalidate set the state of the control to the default and tries to load and render pdf
			Control.prototype.invalidate.call(this);
			this.fireEvent("error", {}, true);
		};

		/**
		 * @private
		 */
		PDFViewer.prototype._fireLoadedEvent = function () {
			this._bRenderPdfContent = true;
			this.setBusy(false);
			this.fireEvent("loaded");
		};

		/**
		 * @param oEvent
		 * @private
		 */
		PDFViewer.prototype._onLoadListener = function (oEvent) {
			try {
				var oTarget = jQuery(oEvent.target),
					bContinue = true;
				// Firefox
				// https://bugzilla.mozilla.org/show_bug.cgi?id=911444
				// because of the embedded pdf plugin in firefox it is not possible to check contentType of the iframe document
				// if the content is pdf. If the content is not a pdf and it is from the same origin, it can be accessed.
				// Other browsers allow access to the mimeType of the iframe's document if the content is from the same origin.
				var sCurrentContentType = "application/pdf";
				try {
					// browsers render pdf in iframe as html page with embed tag
					var aEmbeds = oTarget[0].contentWindow.document.embeds;
					bContinue = aEmbeds.length === 1;
					if (bContinue) {
						sCurrentContentType = aEmbeds[0].attributes.getNamedItem("type").value;
					}
				} catch (error) {
					// even though the sourceValidationFailed event is fired, the default behaviour is to continue.
					// when preventDefault is on event object is called, the rendering ends up with error
					bContinue = this.fireEvent("sourceValidationFailed", {}, true);
				}

				if (bContinue && isSupportedMimeType(sCurrentContentType)) {
					this._fireLoadedEvent();
				} else {
					this._fireErrorEvent();
				}
			} catch (error) {
				jQuery.sap.log.fatal(false, "Fatal error during the handling of load event happened.");
				jQuery.sap.log.fatal(false, error.message);
			}
		};

		/**
		 * @private
		 */
		PDFViewer.prototype._onErrorListener = function () {
			this._fireErrorEvent();
		};

		/**
		 * @private
		 */
		PDFViewer.prototype._onReadyStateChangeListener = function (oEvent) {
			var INTERACTIVE_READY_STATE = "interactive";
			var COMPLETE_READY_STATE = "complete";

			switch (oEvent.target.readyState) {
				case INTERACTIVE_READY_STATE: // IE11 only fires interactive
				case COMPLETE_READY_STATE:
					this._fireLoadedEvent();
					break;
			}
		};

		/**
		 * @private
		 */
		PDFViewer.prototype._onBeforeUnloadListener = function () {
			// IE problems
			// when invalid response is received (404), beforeunload is fired twice
			if (this._bOnBeforeUnloadFired) {
				this._fireErrorEvent();
				return;
			}

			this._bOnBeforeUnloadFired = true;
		};

		/**
		 * @param oEvent
		 * @private
		 */
		PDFViewer.prototype._onLoadIEListener = function (oEvent) {
			var sCurrentContentType = oEvent.currentTarget.contentWindow.document.mimeType;
			if (!isSupportedMimeType(sCurrentContentType)) {
				this._fireErrorEvent();
			}
		};

		/**
		 * @param oEvent
		 * @private
		 */
		PDFViewer.prototype._onAfterPopupClose = function (oEvent) {
			var oPopup = this._objectsRegister.getPopup();
			// content has to be cleared from dom
			oPopup.removeAllContent();
			this._bIsPopupOpen = false;
		};

		/**
		 * @returns {boolean}
		 * @private
		 */
		PDFViewer.prototype._shouldRenderPdfContent = function () {
			return PDFViewer._isPdfPluginEnabled() && this._bRenderPdfContent && this.getSource() !== null;
		};

		/**
		 * @returns {boolean}
		 * @private
		 */
		PDFViewer.prototype._isSourceValidToDisplay = function () {
			var sSource = this.getSource();
			return sSource !== null && sSource !== "" && typeof sSource !== "undefined";
		};

		/**
		 * @param oOrigin
		 */
		PDFViewer.prototype.invalidate = function (oOrigin) {
			this._initControlState();
			Control.prototype.invalidate.call(this, oOrigin);
		};

		/**
		 * Opens up the dialog with pdf viewer inside it
		 *
		 * @public
		 */
		PDFViewer.prototype.open = function () {
			if (!this._isSourceValidToDisplay()) {
				jQuery.sap.assert(false, "The pdf can not be opened with given source. Given source: " + this.getSource());
				return;
			}

			if (this._isEmbeddedModeAllowed()) {
				this._openOnDesktop();
			} else {
				this._openOnMobile();
			}
		};

		/**
		 * Handles opening on desktop devices
		 * @private
		 */
		PDFViewer.prototype._openOnDesktop = function () {
			var oPopup = this._objectsRegister.getPopup();

			if (this._bIsPopupOpen) {
				return;
			}

			this._initControlState();
			this._preparePopup(oPopup);
			oPopup.addContent(this);

			this._bIsPopupOpen = true;
			oPopup.open();
		};

		/**
		 * Handles opening on mobile/tablet devices
		 * @private
		 */
		PDFViewer.prototype._openOnMobile = function () {
			var oWindow = window.open(this.getSource());
			oWindow.focus();
		};

		/**
		 * Gets the iframe element from rendered DOM
		 * @returns {*} jQuery object of iframe
		 * @private
		 */
		PDFViewer.prototype._getIframeDOMElement = function () {
			var oIframeElement = this.$().find("iframe");
			if (oIframeElement.length === 0) {
				throw Error("Underlying iframe was not found in DOM.");
			}
			if (oIframeElement.length > 1) {
				jQuery.sap.log.fatal("Initialization of iframe fails. Reason: the control somehow renders multiple iframes");
			}
			return oIframeElement;
		};

		/**
		 * @private
		 */
		PDFViewer.prototype._isEmbeddedModeAllowed = function () {
			return Device.system.desktop;
		};

		/**
		 * @returns {jQuery.sap.util.ResourceBundle}
		 * @private
		 */
		PDFViewer.prototype._getLibraryResourceBundle = function () {
			return sap.ui.getCore().getLibraryResourceBundle("sap.m");
		};

		/**
		 * @returns {string}
		 * @private
		 */
		PDFViewer.prototype._getMessagePageErrorMessage = function () {
			return this.getErrorPlaceholderMessage() ? this.getErrorPlaceholderMessage() :
				this._getLibraryResourceBundle().getText("PDF_VIEWER_PLACEHOLDER_ERROR_TEXT");
		};

		/**
		 * @returns {string}
		 * @private
		 */
		PDFViewer.prototype._getRenderWidth = function () {
			return this._bIsPopupOpen ? '100%' : this.getWidth();
		};

		/**
		 * @returns {string}
		 * @private
		 */
		PDFViewer.prototype._getRenderHeight = function () {
			return this._bIsPopupOpen ? '100%' : this.getHeight();
		};

		/**
		 * Lifecycle method
		 * @private
		 */
		PDFViewer.prototype.exit = function () {
			jQuery.each(this._objectsRegister, function (iIndex, fnGetObject) {
				fnGetObject().destroy();
			});
		};

		PDFViewerRenderManager.extendPdfViewer(PDFViewer);

		return PDFViewer;
	}, /* bExport= */ true);
<commit_msg>[INTERNAL][FIX]: sap.m.PDFViewer: Fix asynchronous IE error handling.<commit_after>/*!
 * ${copyright}
 */

/* global ActiveXObject:false */

// Provides control sap.m.PDFViewer.
sap.ui.define([
		"jquery.sap.global",
		"./library",
		"sap/ui/core/Control",
		"sap/ui/Device",
		"sap/m/PDFViewerRenderManager"
	],
	function (jQuery, library, Control, Device, PDFViewerRenderManager) {
		"use strict";

		var aAllowedMimeTypes = Object.freeze([
			"application/pdf",
			"application/x-google-chrome-pdf"
		]);

		function isSupportedMimeType(sMimeType) {
			var iFoundIndex = aAllowedMimeTypes.indexOf(sMimeType);
			return iFoundIndex > -1;
		}

		/**
		 * Definition of PDFViewer control
		 *
		 * @param {string} [sId] id for the new control, generated automatically if no id is given
		 * @param {object} [mSettings] initial settings for the new control
		 *
		 * @class
		 * Allow displaying of pdf files and provides useful API.
		 * @extends sap.ui.core.Control
		 *
		 * @author SAP SE
		 * @version ${version}
		 *
		 * @constructor
		 * @public
		 * @alias sap.m.PDFViewer
		 * @ui5-metamodel This control/element also will be described in the UI5 (legacy) designtime metamodel
		 */
		var PDFViewer = Control.extend("sap.m.PDFViewer",
			/** @lends sap.m.PDFViewer.prototype */
			{
				metadata: {
					library: "sap.m",
					properties: {
						/**
						 * Defines the height of the PDF viewer control, respective to the height of
						 * the parent container. Can be set to a percent, pixel, or em value.
						 */
						height : {type : "sap.ui.core.CSSSize", group : "Dimension", defaultValue : "100%"},
						/**
						 * Defines the width of the PDF viewer control, respective to the width of the
						 * parent container. Can be set to a percent, pixel, or em value.
						 */
						width : {type : "sap.ui.core.CSSSize", group : "Dimension", defaultValue : "100%"},
						/**
						 * Specifies the path to the PDF file to display. Can be set to a relative or
						 * an absolute path.
						 */
						source: {type: "sap.ui.core.URI", group: "Misc", defaultValue: null},
						/**
						 * A custom error message that is displayed when the PDF file cannot be loaded.
						 * the PDF file.
						 */
						errorMessage: {type: "string", group: "Misc", defaultValue: null},
						/**
						 * A custom text that is displayed instead of the loaded PDF frame when an error
						 * occurs.
						 */
						errorPlaceholderMessage: {type: "string", group: "Misc", defaultValue: null},
						/**
						 * A custom title for the PDF viewer popup dialog. Works only if the PDF viewer
						 * is set to open in a popup dialog.
						 */
						popupHeaderTitle: {type: "string", group: "Misc", defaultValue: null}
					},
					aggregations: {
						/**
						 * A custom control that can be used instead of the error message specified by the
						 * errorPlaceholderMessage property.
						 */
						errorPlaceholder: {type : "sap.ui.core.Control", multiple : false},
						/**
						 * A multiple aggregation for buttons that can be added to the footer of the popup
						 * dialog. Works only if the PDF viewer is set to open in a popup dialog.
						 */
						popupButtons: {type: "sap.m.Button", multiple: true, singularName: "popupButton"}
					},
					events: {
						/**
						 * This event is fired when a PDF file is loaded. If the PDF is loaded in smaller chunks,
						 * this event is fired as often as defined by the browser's plugin. This may happen after
						 * a couple chunks are processed.
						 */
						loaded: {},
						/**
						 * This event is fired when there is an error loading the PDF file.
						 */
						error: {},
						/**
						 * This event is fired when the PDF viewer control cannot check the loaded content. For
						 * example, the default configuration of the Mozilla Firefox browser may not allow checking
						 * the loaded content. This may also happen when the source PDF file is stored in a different
						 * domain.
						 */
						sourceValidationFailed: {}
					}
				}
			});

		/**
		 * @returns {boolean}
		 * @private
		 */
		PDFViewer._isPdfPluginEnabled = function () {
			var bIsEnabled = true;
			if (Device.browser.firefox) {
				// https://bugzilla.mozilla.org/show_bug.cgi?id=1293406
				// mimeType is missing for firefox even though it is enabled
				return bIsEnabled;
			}

			if (Device.browser.internet_explorer) {
				// hacky code how to recognize that pdf plugin is installed and enabled
				try {
					new ActiveXObject("AcroPDF.PDF");
				} catch (e) {
					bIsEnabled = false;
				}

				return bIsEnabled;
			}

			var aMimeTypes = navigator.mimeTypes;
			bIsEnabled = aAllowedMimeTypes.some(function (sAllowedMimeType) {
				var oMimeTypeItem = aMimeTypes.namedItem(sAllowedMimeType);
				return oMimeTypeItem !== null;
			});

			return bIsEnabled;
		};

		/**
		 * Lifecycle method
		 *
		 * @private
		 */
		PDFViewer.prototype.init = function () {
			// helper object that holds the references of nested objects
			this._objectsRegister = {};

			// state variable that shows the state of popup (rendering of pdf in popup requires it)
			this._bIsPopupOpen = false;

			this._initPopupControl();
			this._initPlaceholderLinkControl();
			this._initPlaceholderMessagePageControl();

			this._initControlState();
		};

		/**
		 * Setup state variables to default state
		 *
		 * @private
		 */
		PDFViewer.prototype._initControlState = function () {
			// state property that control if the embedded pdf should or should not rendered.
			this._bRenderPdfContent = true;

			// detect that beforeunload was fired (IE only)
			this._bOnBeforeUnloadFired = false;
		};

		/**
		 * Lifecycle method
		 *
		 * @private
		 */
		PDFViewer.prototype.onAfterRendering = function () {
			var fnInitIframeElement = function () {
				// cant use attachBrowserEvent because it attach event to component root node (this.$())
				// load event does not bubble so it has to be bind directly to iframe element
				var oIframeElement = this._getIframeDOMElement();
				var oIframeContentWindow = jQuery(oIframeElement.get(0).contentWindow);

				if (Device.browser.internet_explorer) {
					// being special does not mean useful
					// https://connect.microsoft.com/IE/feedback/details/809377/ie-11-load-event-doesnt-fired-for-pdf-in-iframe

					// onerror does not works on IE. Therefore readyonstatechange and unload events are used for error detection.
					// When invalid response is received (404, etc.), readystatechange is not fired but unload is.
					// When valid response is received, then readystatechange and 'complete' state of target's element is received.
					oIframeContentWindow.on("beforeunload", this._onBeforeUnloadListener.bind(this));
					oIframeContentWindow.on("readystatechange", this._onReadyStateChangeListener.bind(this));

					// some error codes load html file and fires loadEvent
					oIframeElement.on("load", this._onLoadIEListener.bind(this));
				} else {
					// normal browsers supports load events as specification said
					oIframeElement.on("load", this._onLoadListener.bind(this));
				}
				oIframeElement.on("error", this._onErrorListener.bind(this));

				var sParametrizedSource = this.getSource();
				var iCrossPosition = this.getSource().indexOf("#");
				if (iCrossPosition > -1) {
					sParametrizedSource = sParametrizedSource.substr(0, iCrossPosition);
				}
				sParametrizedSource += "#view=FitH";
				if (jQuery.sap.validateUrl(sParametrizedSource)) {
					oIframeElement.attr("src", encodeURI(sParametrizedSource));
				} else {
					this._fireErrorEvent();
				}
			}.bind(this);

			try {
				this.setBusy(true);
				fnInitIframeElement();
			} catch (error) {
				this.setBusy(false);
			}
		};

		/**
		 * @private
		 */
		PDFViewer.prototype._fireErrorEvent = function () {
			this.setBusy(false);
			this._bRenderPdfContent = false;
			// calls controls invalidate because the error state should be render.
			// It is controlled by the state variable called _bRenderPdfContent
			// The main invalidate set the state of the control to the default and tries to load and render pdf
			Control.prototype.invalidate.call(this);
			this.fireEvent("error", {}, true);
		};

		/**
		 * @private
		 */
		PDFViewer.prototype._fireLoadedEvent = function () {
			this._bRenderPdfContent = true;
			this.setBusy(false);
			this.fireEvent("loaded");
		};

		/**
		 * @param oEvent
		 * @private
		 */
		PDFViewer.prototype._onLoadListener = function (oEvent) {
			try {
				var oTarget = jQuery(oEvent.target),
					bContinue = true;
				// Firefox
				// https://bugzilla.mozilla.org/show_bug.cgi?id=911444
				// because of the embedded pdf plugin in firefox it is not possible to check contentType of the iframe document
				// if the content is pdf. If the content is not a pdf and it is from the same origin, it can be accessed.
				// Other browsers allow access to the mimeType of the iframe's document if the content is from the same origin.
				var sCurrentContentType = "application/pdf";
				try {
					// browsers render pdf in iframe as html page with embed tag
					var aEmbeds = oTarget[0].contentWindow.document.embeds;
					bContinue = aEmbeds.length === 1;
					if (bContinue) {
						sCurrentContentType = aEmbeds[0].attributes.getNamedItem("type").value;
					}
				} catch (error) {
					// even though the sourceValidationFailed event is fired, the default behaviour is to continue.
					// when preventDefault is on event object is called, the rendering ends up with error
					bContinue = this.fireEvent("sourceValidationFailed", {}, true);
				}

				if (bContinue && isSupportedMimeType(sCurrentContentType)) {
					this._fireLoadedEvent();
				} else {
					this._fireErrorEvent();
				}
			} catch (error) {
				jQuery.sap.log.fatal(false, "Fatal error during the handling of load event happened.");
				jQuery.sap.log.fatal(false, error.message);
			}
		};

		/**
		 * @private
		 */
		PDFViewer.prototype._onErrorListener = function () {
			this._fireErrorEvent();
		};

		/**
		 * @private
		 */
		PDFViewer.prototype._onReadyStateChangeListener = function (oEvent) {
			var INTERACTIVE_READY_STATE = "interactive";
			var COMPLETE_READY_STATE = "complete";

			switch (oEvent.target.readyState) {
				case INTERACTIVE_READY_STATE: // IE11 only fires interactive
				case COMPLETE_READY_STATE:
					this._fireLoadedEvent();
					break;
			}
		};

		/**
		 * @private
		 */
		PDFViewer.prototype._onBeforeUnloadListener = function () {
			// IE problems
			// when invalid response is received (404), beforeunload is fired twice
			if (this._bOnBeforeUnloadFired) {
				this._fireErrorEvent();
				return;
			}

			this._bOnBeforeUnloadFired = true;
		};

		/**
		 * @param oEvent
		 * @private
		 */
		PDFViewer.prototype._onLoadIEListener = function (oEvent) {
			try {
				// because of asynchronity of events, IE sometimes fires load event even after it unloads the content.
				// The contentWindow does not exists in these moments. On the other hand, the error state is already handled
				// by onBeforeUnloadListener, so we only need catch for catching the error and then return.
				// The problem is not with null reference. The access of the contentWindow sometimes fires 'access denied' error
				// which is not detectable otherwise.
				var sCurrentContentType = oEvent.currentTarget.contentWindow.document.mimeType;
			} catch (err) {
				return;
			}

			if (!isSupportedMimeType(sCurrentContentType)) {
				this._fireErrorEvent();
			}
		};

		/**
		 * @param oEvent
		 * @private
		 */
		PDFViewer.prototype._onAfterPopupClose = function (oEvent) {
			var oPopup = this._objectsRegister.getPopup();
			// content has to be cleared from dom
			oPopup.removeAllContent();
			this._bIsPopupOpen = false;
		};

		/**
		 * @returns {boolean}
		 * @private
		 */
		PDFViewer.prototype._shouldRenderPdfContent = function () {
			return PDFViewer._isPdfPluginEnabled() && this._bRenderPdfContent && this.getSource() !== null;
		};

		/**
		 * @returns {boolean}
		 * @private
		 */
		PDFViewer.prototype._isSourceValidToDisplay = function () {
			var sSource = this.getSource();
			return sSource !== null && sSource !== "" && typeof sSource !== "undefined";
		};

		/**
		 * @param oOrigin
		 */
		PDFViewer.prototype.invalidate = function (oOrigin) {
			this._initControlState();
			Control.prototype.invalidate.call(this, oOrigin);
		};

		/**
		 * Opens up the dialog with pdf viewer inside it
		 *
		 * @public
		 */
		PDFViewer.prototype.open = function () {
			if (!this._isSourceValidToDisplay()) {
				jQuery.sap.assert(false, "The pdf can not be opened with given source. Given source: " + this.getSource());
				return;
			}

			if (this._isEmbeddedModeAllowed()) {
				this._openOnDesktop();
			} else {
				this._openOnMobile();
			}
		};

		/**
		 * Handles opening on desktop devices
		 * @private
		 */
		PDFViewer.prototype._openOnDesktop = function () {
			var oPopup = this._objectsRegister.getPopup();

			if (this._bIsPopupOpen) {
				return;
			}

			this._initControlState();
			this._preparePopup(oPopup);
			oPopup.addContent(this);

			this._bIsPopupOpen = true;
			oPopup.open();
		};

		/**
		 * Handles opening on mobile/tablet devices
		 * @private
		 */
		PDFViewer.prototype._openOnMobile = function () {
			var oWindow = window.open(this.getSource());
			oWindow.focus();
		};

		/**
		 * Gets the iframe element from rendered DOM
		 * @returns {*} jQuery object of iframe
		 * @private
		 */
		PDFViewer.prototype._getIframeDOMElement = function () {
			var oIframeElement = this.$().find("iframe");
			if (oIframeElement.length === 0) {
				throw Error("Underlying iframe was not found in DOM.");
			}
			if (oIframeElement.length > 1) {
				jQuery.sap.log.fatal("Initialization of iframe fails. Reason: the control somehow renders multiple iframes");
			}
			return oIframeElement;
		};

		/**
		 * @private
		 */
		PDFViewer.prototype._isEmbeddedModeAllowed = function () {
			return Device.system.desktop;
		};

		/**
		 * @returns {jQuery.sap.util.ResourceBundle}
		 * @private
		 */
		PDFViewer.prototype._getLibraryResourceBundle = function () {
			return sap.ui.getCore().getLibraryResourceBundle("sap.m");
		};

		/**
		 * @returns {string}
		 * @private
		 */
		PDFViewer.prototype._getMessagePageErrorMessage = function () {
			return this.getErrorPlaceholderMessage() ? this.getErrorPlaceholderMessage() :
				this._getLibraryResourceBundle().getText("PDF_VIEWER_PLACEHOLDER_ERROR_TEXT");
		};

		/**
		 * @returns {string}
		 * @private
		 */
		PDFViewer.prototype._getRenderWidth = function () {
			return this._bIsPopupOpen ? '100%' : this.getWidth();
		};

		/**
		 * @returns {string}
		 * @private
		 */
		PDFViewer.prototype._getRenderHeight = function () {
			return this._bIsPopupOpen ? '100%' : this.getHeight();
		};

		/**
		 * Lifecycle method
		 * @private
		 */
		PDFViewer.prototype.exit = function () {
			jQuery.each(this._objectsRegister, function (iIndex, fnGetObject) {
				fnGetObject().destroy();
			});
		};

		PDFViewerRenderManager.extendPdfViewer(PDFViewer);

		return PDFViewer;
	}, /* bExport= */ true);
<|endoftext|>


<commit_before>const td              = require('testdouble');
const Promise         = require('rsvp').Promise;
const expect          = require('../../../../helpers/expect');
const Device          = require('../../../../../lib/objects/device');

describe('Android List Device Task', function() {
  afterEach(function() {
    td.reset();
  });

  it('lints out emulators, ignoring non iOS devices', function() {
    td.replace('../../../../../lib/utils/spawn', function(cmd, args) {
      let deviceList = `List of Devices Attached \nuuid  device usb:337641472X product:jfltevl model:SGH_I337M device:jfltecan transport_id:1 \n`;

      return Promise.resolve(deviceList);
    });

    let list = require('../../../../../lib/targets/android/tasks/list-devices');

    return list().then(function(found) {
      expect(found).to.deep.equal([new Device({
        name: 'SGH_I337M',
        uuid: 'uuid',
        platform: 'android',
        deviceType: 'device'
      })]);
    });
  });
});
<commit_msg>fix(list-devices-test): stub sdk path<commit_after>const td              = require('testdouble');
const Promise         = require('rsvp').Promise;
const expect          = require('../../../../helpers/expect');
const Device          = require('../../../../../lib/objects/device');

describe('Android List Device Task', function() {
  afterEach(function() {
    td.reset();
  });

  it('lints out emulators, ignoring non iOS devices', function() {
    td.replace('../../../../../lib/utils/spawn', function(cmd, args) {
      let deviceList = `List of Devices Attached \nuuid  device usb:337641472X product:jfltevl model:SGH_I337M device:jfltecan transport_id:1 \n`;
      return Promise.resolve(deviceList);
    });

    td.replace('../../../../../lib/targets/android/utils/sdk-paths', function() {
      return {
        adb: 'fakeAdb'
      }
    });

    let list = require('../../../../../lib/targets/android/tasks/list-devices');

    return list().then(function(found) {
      expect(found).to.deep.equal([new Device({
        name: 'SGH_I337M',
        uuid: 'uuid',
        platform: 'android',
        deviceType: 'device'
      })]);
    });
  });
});
<|endoftext|>