<commit_before>var quotes = function(dbot) {
    var quotes = dbot.db.quoteArrs;
    var addStack = [];
    var rmAllowed = true;

    var commands = {
        '~q': function(data, params) {
            var q = data.message.valMatch(/^~q ([\d\w\s-]*)/, 2);
            if(q) {
                q[1] = q[1].trim();
                key = q[1].toLowerCase();
                if(quotes.hasOwnProperty(key)) {
                    dbot.say(data.channel, q[1] + ': ' + quotes[key].random());
                } else {
                    dbot.say(data.channel, 'Nobody loves ' + q[1]);
                }
            }
        },

        '~qsearch': function(data, params) {
            if(params[2] === undefined) {
                dbot.say(data.channel, 'Next time provide a search parameter. Commence incineration.');
            } else {
                params[1].trim();
                key = params[1].toLowerCase();
                if(!quotes.hasOwnProperty(key)) {
                    dbot.say(data.channel, 'That category has no quotes in it. Commence incineration.');
                } else {
                    var matches = [];

                    quotes[key].each(function(quote) {
                        if(quote.indexOf(params[2]) != -1) {
                            matches.push(quote);
                        }
                    }.bind(this));

                    if(matches.length == 0) {
                        dbot.say(data.channel, 'No results found.');
                    } else {
                        dbot.say(data.channel, params[1] + ' (' + params[2] + '): ' + matches.random() + ' [' + matches.length + ' results]');
                    }
                }
            }
        },

        '~rmlast': function(data, params) {
            if(rmAllowed == true || data.user == dbot.admin) {
                var q = data.message.valMatch(/^~rmlast ([\d\w\s-]*)/, 2);
                if(q) {
                    q[1] = q[1].trim()
                    key = q[1].toLowerCase();
                    if(quotes.hasOwnProperty(q[1])) {
                        if(!dbot.db.locks.include(q[1]) || data.user == dbot.admin) {
                            var quote = quotes[key].pop();
                            if(quotes[key] === undefined) {
                                delete quotes[key];
                            }
                            rmAllowed = false;
                            dbot.say(data.channel, '\'' + quote + '\' removed from ' + q[1]);
                        } else {
                            dbot.say(data.channel, q[1] + ' is locked. Commence incineration.');
                        }
                    } else {
                        dbot.say(data.channel, 'No quotes exist under ' + q[1]);
                    }
                } else {
                    var last = addStack.pop();
                    if(last) {
                        if(!dbot.db.locks.include(last)) {
                            quotes[last].pop();
                            rmAllowed = false;
                            dbot.say(data.channel, 'Last quote removed from ' + last + '.');
                        } else {
                            dbot.say(data.channel, last + ' is locked. Commence incineration.');
                        }
                    } else {
                        dbot.say(data.channel, 'No quotes were added recently.');
                    }
                }
            } else {
                dbot.say(data.channel, 'No spamming that shit. Try again in a few minutes...');
            }
        },

        '~rm': function(data, params) {
            if(rmAllowed == true || data.user == dbot.admin) {
                var q = data.message.valMatch(/^~rm ([\d\w\s-]*) (.+)$/, 3);
                if(q) {
                    if(quotes.hasOwnProperty(q[1])) {
                        if(!dbot.db.locks.include(q[1])) {
                            var index = quotes[q[1]].indexOf(q[2]);
                            if(index != -1) {
                                quotes[q[1]].splice(index, 1);
                                rmAllowed = false;
                                dbot.say(data.channel, '\'' + q[2] + '\' removed from ' + q[1]);
                            } else {
                                dbot.say(data.channel, '\'' + q[2] + '\' doesn\'t exist under user \'' + q[1] + '\'.');
                            }
                        } else {
                            dbot.say(data.channel, q[1] + ' is locked. Initiate incineration.');
                        }
                    } else {
                        dbot.say(data.channel, 'No quotes exist under ' + q[1]);
                    }
                } else {
                    dbot.say(data.channel, 'Invalid syntax. Initiate incineration.');
                }
            } else {
                dbot.say(data.channel, 'No spamming that shit. Try again in a few minutes...');
            }
        },

        '~qcount': function(data, params) {
            var q = data.message.valMatch(/^~qcount ([\d\w\s-]*)/, 2);
            if(q) {
                q[1] = q[1].trim();
                key = q[1].toLowerCase();
                if(quotes.hasOwnProperty(key)) {
                    dbot.say(data.channel, q[1] + ' has ' + quotes[key].length + ' quotes.');
                } else {
                    dbot.say(data.channel, 'No quotes under ' + q[1]);
                }
            } else { // Give total quote count
                var totalQuoteCount = 0;
                for(var category in quotes) {
                    console.log('adding ' + category.length);
                    totalQuoteCount += category.length;
                }
                dbot.say(data.channel, 'There are ' + totalQuoteCount + ' quotes in total.');
            }
        },

        '~qadd': function(data, params) {
            var q = data.message.valMatch(/^~qadd ([\d\w\s-]*)=(.+)$/, 3);
            if(q) {
                key = q[1].toLowerCase();
                if(!Object.isArray(quotes[key])) {
                    quotes[key] = [];
                } else {
                    if (q[2] in quotes[key]) {
                        dbot.say(data.channel, 'Quote already in DB. Initiate incineration.');
                        return;
                    }
                }
                quotes[key].push(q[2]);
                addStack.push(q[1]);
                rmAllowed = true;
                dbot.say(data.channel, 'Quote saved in \'' + q[1] + '\' (' + quotes[key].length + ')');
            } else {
                dbot.say(data.channel, 'Invalid syntax. Initiate incineration.');
            }
        },

        '~qset': function(data, params) {
            var q = data.message.valMatch(/^~qset ([\d\w\s-]*)=(.+)$/, 3);
            if(q) {
                q[1] = q[1].trim();
                key = q[1].toLowerCase();
                if(!quotes.hasOwnProperty(key) || (quotes.hasOwnProperty(key) &&
                        quotes[key].length == 1)) {
                    quotes[key] = [q[2]];
                    dbot.say(data.channel, 'Quote saved as ' + q[1]);
                } else {
                    dbot.say(data.channel, 'No replacing arrays, you whore.');
                }
            }
        },

        '~rq': function(data, params) {
            var rQuote = Object.keys(quotes).random();
            dbot.say(data.channel, rQuote + ': ' + quotes[rQuote].random());
        },

        '~d': function(data, params) {
            dbot.say(data.channel,  data.user + ': ' + dbot.db.quoteArrs['depressionbot'].random());
        },

        '~link': function(data, params) {
            if(params[1] === undefined || !quotes.hasOwnProperty(params[1].toLowerCase())) {
                dbot.say(data.channel, 'Syntax error. Commence incineration.');
            } else {
                dbot.say(data.channel, 'Link to "'+params[1]+'" - http://nc.no.de:443/quotes/'+params[1]);
            }
        }
    };

    return {
        'onLoad': function() {
            dbot.timers.addTimer(1000 * 60 * 3, function() {
                rmAllowed = true;
            });
            return commands;
        },

        // For automatic quote retrieval
        'listener': function(data, params) {
            if(data.user == 'reality') {
                var once = data.message.valMatch(/^I ([\d\w\s,'-]* once)/, 2);
            } else {
                var once = data.message.valMatch(/^reality ([\d\w\s,'-]* once)/, 2);
            }

            if(once) {
                if((dbot.db.bans.hasOwnProperty('~qadd') &&
                dbot.db.bans['~qadd'].include(data.user)) ||
                dbot.db.bans['*'].include(data.user)) {
                    dbot.say(data.channel, data.user + ' is banned from using this command. Commence incineration.');
                } else {
                    dbot.db.quoteArrs['realityonce'].push('reality ' + once[1] + '.');
                    addStack.push('realityonce');
                    rmAllowed = true;
                    dbot.instance.say(data.channel, '\'reality ' + once[1] + '.\' saved.');
                }
            }
        },

        'on': 'PRIVMSG'
    };
};

exports.fetch = function(dbot) {
    return quotes(dbot);
};
<commit_msg>delete category when rmlast makes empty quote category<commit_after>var quotes = function(dbot) {
    var quotes = dbot.db.quoteArrs;
    var addStack = [];
    var rmAllowed = true;

    var commands = {
        '~q': function(data, params) {
            var q = data.message.valMatch(/^~q ([\d\w\s-]*)/, 2);
            if(q) {
                q[1] = q[1].trim();
                key = q[1].toLowerCase();
                if(quotes.hasOwnProperty(key)) {
                    dbot.say(data.channel, q[1] + ': ' + quotes[key].random());
                } else {
                    dbot.say(data.channel, 'Nobody loves ' + q[1]);
                }
            }
        },

        '~qsearch': function(data, params) {
            if(params[2] === undefined) {
                dbot.say(data.channel, 'Next time provide a search parameter. Commence incineration.');
            } else {
                params[1].trim();
                key = params[1].toLowerCase();
                if(!quotes.hasOwnProperty(key)) {
                    dbot.say(data.channel, 'That category has no quotes in it. Commence incineration.');
                } else {
                    var matches = [];

                    quotes[key].each(function(quote) {
                        if(quote.indexOf(params[2]) != -1) {
                            matches.push(quote);
                        }
                    }.bind(this));

                    if(matches.length == 0) {
                        dbot.say(data.channel, 'No results found.');
                    } else {
                        dbot.say(data.channel, params[1] + ' (' + params[2] + '): ' + matches.random() + ' [' + matches.length + ' results]');
                    }
                }
            }
        },

        '~rmlast': function(data, params) {
            if(rmAllowed == true || data.user == dbot.admin) {
                var q = data.message.valMatch(/^~rmlast ([\d\w\s-]*)/, 2);
                if(q) {
                    q[1] = q[1].trim()
                    key = q[1].toLowerCase();
                    if(quotes.hasOwnProperty(q[1])) {
                        if(!dbot.db.locks.include(q[1]) || data.user == dbot.admin) {
                            var quote = quotes[key].pop();
                            if(quotes[key].length) {
                                delete quotes[key];
                            }
                            rmAllowed = false;
                            dbot.say(data.channel, '\'' + quote + '\' removed from ' + q[1]);
                        } else {
                            dbot.say(data.channel, q[1] + ' is locked. Commence incineration.');
                        }
                    } else {
                        dbot.say(data.channel, 'No quotes exist under ' + q[1]);
                    }
                } else {
                    var last = addStack.pop();
                    if(last) {
                        if(!dbot.db.locks.include(last)) {
                            quotes[last].pop();
                            rmAllowed = false;
                            dbot.say(data.channel, 'Last quote removed from ' + last + '.');
                        } else {
                            dbot.say(data.channel, last + ' is locked. Commence incineration.');
                        }
                    } else {
                        dbot.say(data.channel, 'No quotes were added recently.');
                    }
                }
            } else {
                dbot.say(data.channel, 'No spamming that shit. Try again in a few minutes...');
            }
        },

        '~rm': function(data, params) {
            if(rmAllowed == true || data.user == dbot.admin) {
                var q = data.message.valMatch(/^~rm ([\d\w\s-]*) (.+)$/, 3);
                if(q) {
                    if(quotes.hasOwnProperty(q[1])) {
                        if(!dbot.db.locks.include(q[1])) {
                            var index = quotes[q[1]].indexOf(q[2]);
                            if(index != -1) {
                                quotes[q[1]].splice(index, 1);
                                rmAllowed = false;
                                dbot.say(data.channel, '\'' + q[2] + '\' removed from ' + q[1]);
                            } else {
                                dbot.say(data.channel, '\'' + q[2] + '\' doesn\'t exist under user \'' + q[1] + '\'.');
                            }
                        } else {
                            dbot.say(data.channel, q[1] + ' is locked. Initiate incineration.');
                        }
                    } else {
                        dbot.say(data.channel, 'No quotes exist under ' + q[1]);
                    }
                } else {
                    dbot.say(data.channel, 'Invalid syntax. Initiate incineration.');
                }
            } else {
                dbot.say(data.channel, 'No spamming that shit. Try again in a few minutes...');
            }
        },

        '~qcount': function(data, params) {
            var q = data.message.valMatch(/^~qcount ([\d\w\s-]*)/, 2);
            if(q) {
                q[1] = q[1].trim();
                key = q[1].toLowerCase();
                if(quotes.hasOwnProperty(key)) {
                    dbot.say(data.channel, q[1] + ' has ' + quotes[key].length + ' quotes.');
                } else {
                    dbot.say(data.channel, 'No quotes under ' + q[1]);
                }
            } else { // Give total quote count
                var totalQuoteCount = 0;
                for(var category in quotes) {
                    console.log('adding ' + category.length);
                    totalQuoteCount += category.length;
                }
                dbot.say(data.channel, 'There are ' + totalQuoteCount + ' quotes in total.');
            }
        },

        '~qadd': function(data, params) {
            var q = data.message.valMatch(/^~qadd ([\d\w\s-]*)=(.+)$/, 3);
            if(q) {
                key = q[1].toLowerCase();
                if(!Object.isArray(quotes[key])) {
                    quotes[key] = [];
                } else {
                    if (q[2] in quotes[key]) {
                        dbot.say(data.channel, 'Quote already in DB. Initiate incineration.');
                        return;
                    }
                }
                quotes[key].push(q[2]);
                addStack.push(q[1]);
                rmAllowed = true;
                dbot.say(data.channel, 'Quote saved in \'' + q[1] + '\' (' + quotes[key].length + ')');
            } else {
                dbot.say(data.channel, 'Invalid syntax. Initiate incineration.');
            }
        },

        '~qset': function(data, params) {
            var q = data.message.valMatch(/^~qset ([\d\w\s-]*)=(.+)$/, 3);
            if(q) {
                q[1] = q[1].trim();
                key = q[1].toLowerCase();
                if(!quotes.hasOwnProperty(key) || (quotes.hasOwnProperty(key) &&
                        quotes[key].length == 1)) {
                    quotes[key] = [q[2]];
                    dbot.say(data.channel, 'Quote saved as ' + q[1]);
                } else {
                    dbot.say(data.channel, 'No replacing arrays, you whore.');
                }
            }
        },

        '~rq': function(data, params) {
            var rQuote = Object.keys(quotes).random();
            dbot.say(data.channel, rQuote + ': ' + quotes[rQuote].random());
        },

        '~d': function(data, params) {
            dbot.say(data.channel,  data.user + ': ' + dbot.db.quoteArrs['depressionbot'].random());
        },

        '~link': function(data, params) {
            if(params[1] === undefined || !quotes.hasOwnProperty(params[1].toLowerCase())) {
                dbot.say(data.channel, 'Syntax error. Commence incineration.');
            } else {
                dbot.say(data.channel, 'Link to "'+params[1]+'" - http://nc.no.de:443/quotes/'+params[1]);
            }
        }
    };

    return {
        'onLoad': function() {
            dbot.timers.addTimer(1000 * 60 * 3, function() {
                rmAllowed = true;
            });
            return commands;
        },

        // For automatic quote retrieval
        'listener': function(data, params) {
            if(data.user == 'reality') {
                var once = data.message.valMatch(/^I ([\d\w\s,'-]* once)/, 2);
            } else {
                var once = data.message.valMatch(/^reality ([\d\w\s,'-]* once)/, 2);
            }

            if(once) {
                if((dbot.db.bans.hasOwnProperty('~qadd') &&
                dbot.db.bans['~qadd'].include(data.user)) ||
                dbot.db.bans['*'].include(data.user)) {
                    dbot.say(data.channel, data.user + ' is banned from using this command. Commence incineration.');
                } else {
                    dbot.db.quoteArrs['realityonce'].push('reality ' + once[1] + '.');
                    addStack.push('realityonce');
                    rmAllowed = true;
                    dbot.instance.say(data.channel, '\'reality ' + once[1] + '.\' saved.');
                }
            }
        },

        'on': 'PRIVMSG'
    };
};

exports.fetch = function(dbot) {
    return quotes(dbot);
};
<|endoftext|>


<commit_before>// Streaming logs
var pushstream = new PushStream({
  host: window.location.hostname,
  port: 443,
  useSSL: true,
  modes: "eventsource",
  urlPrefixEventsource: "/__/sub",
  channelsByArgument: true,
  channelsArgument: "id"
});
pushstream.onmessage = function(data,id,channel) {
  // console.log(data);

  // Check that this channel matches the active channel
  if(document.getElementById('active-channel') && document.getElementById('active-channel').value == data.channel) {
    var html = data.html;
    var autoScroll = (window.innerHeight + window.scrollY) >= document.body.offsetHeight;
    var line = document.createElement('div');
    line.innerHTML = html;
    document.getElementById('log-lines').appendChild(line.childNodes[0]);
    // Auto-scroll if the window is already scrolled to the bottom
    if(autoScroll) {
      window.scrollTo(0,document.body.scrollHeight);
    }
    if(window.check_alert) {
      check_alert(data);
    }
  } else {
    channel_activity(data.channel, data.type);
  }
}
pushstream.addChannel('chat');
pushstream.connect();

// Channel unread indicators
function channel_activity(channel, type) {
  var tab = document.querySelector('li.channel[data-channel="'+channel+'"]');
  if(tab && type == 'message') {
    tab.classList.add('activity');
  }

  var channels = Cookies.getJSON('unread');
  if(channels == null) {
    channels = {};
  }
  if(typeof channels[channel] == 'undefined' || channels[channel] == 'read') {
    channels[channel] = 'unread';
    Cookies.set('unread', channels);
  }
}

function channel_unread(channel) {
  var channels = Cookies.getJSON('unread');
  return channels && channels[channel] == 'unread';
}

function channel_read(channel) {
  var channels = Cookies.getJSON('unread');
  if(channels) {
    channels[channel] = 'read';
  }
  Cookies.set('unread', channels);
  var tab = document.querySelector('li.channel[data-channel="'+channel+'"]');
  if(tab) {
    tab.classList.remove('activity');
  }
}<commit_msg>fix activity cookie<commit_after>// Streaming logs
var pushstream = new PushStream({
  host: window.location.hostname,
  port: 443,
  useSSL: true,
  modes: "eventsource",
  urlPrefixEventsource: "/__/sub",
  channelsByArgument: true,
  channelsArgument: "id"
});
pushstream.onmessage = function(data,id,channel) {
  // console.log(data);

  // Check that this channel matches the active channel
  if(document.getElementById('active-channel') && document.getElementById('active-channel').value == data.channel) {
    var html = data.html;
    var autoScroll = (window.innerHeight + window.scrollY) >= document.body.offsetHeight;
    var line = document.createElement('div');
    line.innerHTML = html;
    document.getElementById('log-lines').appendChild(line.childNodes[0]);
    // Auto-scroll if the window is already scrolled to the bottom
    if(autoScroll) {
      window.scrollTo(0,document.body.scrollHeight);
    }
    if(window.check_alert) {
      check_alert(data);
    }
  } else {
    channel_activity(data.channel, data.type);
  }
}
pushstream.addChannel('chat');
pushstream.connect();

// Channel unread indicators
function channel_activity(channel, type) {
  var tab = document.querySelector('li.channel[data-channel="'+channel+'"]');
  if(tab && type == 'message') {
    tab.classList.add('activity');
  }

  var channels = Cookies.getJSON('unread');
  if(channels == "undefined") {
    channels = {};
  }
  if(typeof channels[channel] == 'undefined' || channels[channel] == 'read') {
    channels[channel] = 'unread';
    Cookies.set('unread', channels);
  }
}

function channel_unread(channel) {
  var channels = Cookies.getJSON('unread');
  return channels && channels[channel] == 'unread';
}

function channel_read(channel) {
  var channels = Cookies.getJSON('unread');
  if(channels) {
    channels[channel] = 'read';
  }
  Cookies.set('unread', channels);
  var tab = document.querySelector('li.channel[data-channel="'+channel+'"]');
  if(tab) {
    tab.classList.remove('activity');
  }
}<|endoftext|>


<commit_before>/**
 * Created by Stein-Otto Svorstøl on 26.08.15.
 * Logic of requests regarding crowds
 */

var Crowds = require('../models/crowd.js');

var userController = require('../controllers/userController.js');

module.exports = {
    create: function(req, res){
        var crowd = req.body;
        delete crowd._id; // Can get a value from clients
        if (crowd.members.indexOf(crowd.owner) === -1 ) crowd.members.push(crowd.owner);
        if (!isValidCrowdObject(crowd)) return res.sendStatus(422);
        Crowds.findWithName(crowd.name, function(result){
            if (!result.length === 0) return res.status(409).send('Crowd name already in use.');
            Crowds.insertCrowd(crowd, function(insertData){
                res.json(insertData);
            });
        });
    },

    addMember: function(req, res){
        var username = req.body.username;
        var crowdId = req.params.crowdId;
        Crowds.addMember(crowdId, username, function(result){
            if(result === 404) return res.sendStatus(404);
            res.status(200).send('Member added.');
        });
    },

    removeMember: function(req, res){
        var crowdId = req.params.crowdId,
            username = req.body.username;
        Crowds.removeMember(crowdId, username, function(result){
            if(result === 404) return res.sendStatus(404);
            res.status(200).send('Member removed.');
        });
    },

    getAll: function(req, res){
        Crowds.getAll(function(result){
            var toReturn = [];
            result.forEach(function(doc, index){
                buildCrowdObject(doc, function(obj){
                    toReturn.push(obj);
                    if (index+1 === result.length ) res.json({crowds: toReturn}); // Done
                });
            });
        });
    },

    get: function(req, res){
        var crowdId = req.params.crowdId;
        Crowds.getCrowdWithId(crowdId, function(result){
            if (result === 404) return res.status(404).send('Crowd not found.');
            buildCrowdObject(result, function(obj){
                res.json(obj);
            });
        });
    }
};

function buildCrowdObject(crowdDoc, callback){
    var membersAsUserObjects = [];
    crowdDoc.members.forEach(function(memberName, index){
        userController.getUserByUsername(memberName, function(obj){ // Get that username as user object
            membersAsUserObjects.push(obj); // Push to list
            if(index + 1 === crowdDoc.members.length) { // If've checked all the membersAsUserObjects
                crowdDoc.members = membersAsUserObjects; // Set membersAsUserObjects list of object
                return callback(crowdDoc); // And return crowdDoc to callback
            }
        })
    });
}

function isValidCrowdObject(crowd){
    if (typeof crowd.owner === 'string'
        && typeof crowd.members === 'object'
        && typeof crowd.name === 'string'
        && Object.keys(crowd).length === 3) return true;
    return false;
}
<commit_msg>Implement move of crowd-member to add/remove from PUT body to URI<commit_after>/**
 * Created by Stein-Otto Svorstøl on 26.08.15.
 * Logic of requests regarding crowds
 */

var Crowds = require('../models/crowd.js');

var userController = require('../controllers/userController.js');

module.exports = {
    create: function(req, res){
        var crowd = req.body;
        delete crowd._id; // Can get a value from clients
        if (crowd.members.indexOf(crowd.owner) === -1 ) crowd.members.push(crowd.owner);
        if (!isValidCrowdObject(crowd)) return res.sendStatus(422);
        Crowds.findWithName(crowd.name, function(result){
            if (!result.length === 0) return res.status(409).send('Crowd name already in use.');
            Crowds.insertCrowd(crowd, function(insertData){
                res.json(insertData);
            });
        });
    },

    addMember: function(req, res){
        var username = req.params.username;
        var crowdId = req.params.crowdId;
        Crowds.addMember(crowdId, username, function(result){
            if(result === 404) return res.sendStatus(404);
            res.status(200).send('Member added.');
        });
    },

    removeMember: function(req, res){
        var crowdId = req.params.crowdId,
            username = req.params.username;
        Crowds.removeMember(crowdId, username, function(result){
            if(result === 404) return res.sendStatus(404);
            res.status(200).send('Member removed.');
        });
    },

    getAll: function(req, res){
        Crowds.getAll(function(result){
            var toReturn = [];
            result.forEach(function(doc, index){
                buildCrowdObject(doc, function(obj){
                    toReturn.push(obj);
                    if (index+1 === result.length ) res.json({crowds: toReturn}); // Done
                });
            });
        });
    },

    get: function(req, res){
        var crowdId = req.params.crowdId;
        Crowds.getCrowdWithId(crowdId, function(result){
            if (result === 404) return res.status(404).send('Crowd not found.');
            buildCrowdObject(result, function(obj){
                res.json(obj);
            });
        });
    }
};

function buildCrowdObject(crowdDoc, callback){
    var membersAsUserObjects = [];
    crowdDoc.members.forEach(function(memberName, index){
        userController.getUserByUsername(memberName, function(obj){ // Get that username as user object
            membersAsUserObjects.push(obj); // Push to list
            if(index + 1 === crowdDoc.members.length) { // If've checked all the membersAsUserObjects
                crowdDoc.members = membersAsUserObjects; // Set membersAsUserObjects list of object
                return callback(crowdDoc); // And return crowdDoc to callback
            }
        })
    });
}

function isValidCrowdObject(crowd){
    if (typeof crowd.owner === 'string'
        && typeof crowd.members === 'object'
        && typeof crowd.name === 'string'
        && Object.keys(crowd).length === 3) return true;
    return false;
}
<|endoftext|>


<commit_before>/**
  Copyright 2018 LNFWebsite
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at
      http://www.apache.org/licenses/LICENSE-2.0
  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
**/

// * This function highlights playlist elements

function highlight(i, which, persist) {
  $("tr:nth-child(" + i + ")").attr("id", "new-" + which);
  if (!persist) {
    $("tr." + which).removeClass(which);
  }
  $("#new-" + which).addClass(which);
  $("#new-" + which).removeAttr("id");
}

// * This function adds video elements to the playlist

function addVideoToList(name, time, spot, smooth) {
  //name = decodeURIComponent(name); TESTING

  if (smooth) {
    smooth = " class=\"animated flipInX\"";
  }
  else {
    smooth = "";
  }

  //let trElement = "
" + name + "
<button class=\"tableButton removeButton\" onclick=\"buttonRemoveVideo(this);\" title=\"Remove\"></button>" +
  //"<button class=\"tableButton playButton\" onclick=\"buttonPlayVideo(this);\" title=\"Play\"></button>
	" + time + "
";

  let trElement = "
" + name +
  "
" +
  "" +
  "" +
  "" +
  "
	" + time + "
";

  if ($("#videosTable > tr").length > 0) {
    if (spot > 1) {
      $("#videosTable > tr").eq(spot-2).after(trElement);
    }
    else {
      $(trElement).insertBefore("#videosTable > tr:first");
    }
  }
  else {
    $("#videosTable").append(trElement);
  }
}

// * This function removes video elements from the playlist

function removeVideoFromList(index, smooth) {
  if (smooth === true) {
    $("tr:nth-child(" + index + ")").fadeOut(function() {
      $(this).remove();
    });
  }
  else {
    $("tr:nth-child(" + index + ")").remove();
  }
}

// * This function restores the video list highlighting as applicable
// * It does not use stored values, but rather the state of the playlist

function restoreHighlight(which) {
  if (which === videoIteration) {
    highlight(which, "selected", false);
  }
  if (videos[which][2] === baseAutoplayVideoId) {
    highlight(which, "radio", false);
  }
  if (videoErrorIds.indexOf(videos[which][2]) > -1) {
    highlight(which, "videoError", true);
  }
}

// * This function refreshes the playlist viewer with videos in the videos array
// * It's primary use is for playlist shuffling

function refreshVideoList() {
  for (let i = 1; i < videos.length; i++) {
    removeVideoFromList(i, false);
    let printTime = msConversion(videos[i][1] * 1000);
    addVideoToList(videos[i][0], printTime, i, false);
    restoreHighlight(i);
  }
}

// * These functions are called when the play/remove video buttons in the playlist viewer are clicked

let PlaylistButtons = function() {
  this.play = function(element) {
    let index = $(".playButton").index(element);
    actionPlayVideo(index);
  }
  this.remove = function(element) {
    let index = $(".removeButton").index(element) + 1;
    actionRemoveVideo(index);
  }
  this.autoplay = function(element) {
    let index = $(".autoplayButton").index(element) + 1;
    console.log("here: " + index);
    playlistAutoplay = true;
    addAutoplayVideo(index, 'reset');
    playlistFeatures.toggleSelected(playlistAutoplay, ".fa-rss");
  }
}
let playlistButtons = new PlaylistButtons;

// * This function makes the entire playlist viewer sortable by dragging&dropping
// * It calls the actionMoveVideo function above

function makeSortable() {
  $( "#videosTable" ).sortable({
    update: function(event, ui) {
      actionMoveVideo(oldIndex + 1, ui.item.index() + 1);
      setPlaylist();
      videoPreviews();
    },
    start: function(event, ui) {
      oldIndex = ui.item.index();
    },
    cancel: "span"
  });
}

// * This object is for the settings available in the playlist manipulation footer
// * Those buttons call their corresponding functionality here

let PlaylistFeatures = function() {
  let self = this;
  this.toggleSelected = function(doit, which) {
    if (doit) {
      $(which).addClass("selected");
    }
    else {
      $(which).removeClass("selected");
    }
  }
  this.playNext = function() {
    sendStation("playlistfeaturesplaynext");
    playlistPlayNext = (playlistPlayNext ? false : true);
    self.toggleSelected(playlistPlayNext, ".fa-arrow-circle-right");
  }
  this.repeat = function() {
    sendStation("playlistfeaturesrepeat");
    playlistRepeat = (playlistRepeat ? false : true);
    videoPreviews();
    self.toggleSelected(playlistRepeat, ".fa-redo-alt");
  }
  this.shuffle = function() {
    sendStation("playlistfeaturesshuffle");
    playlistShuffle = (playlistShuffle ? false : true);
    shufflePlaylist();
    self.toggleSelected(playlistShuffle, ".fa-random");
  }
  this.autoplay = function() {
    playlistAutoplay = (playlistAutoplay ? false : true);
    if (playlistAutoplay === false) {
      autoplayVideos = [];
      autoplayVideoIteration = 0;
      baseAutoplayVideoId = false;
      autoplayList = false;
      autoplayLoading = false;
      $("tr").removeClass("radio");
    }
    else {
      addAutoplayVideo();
    }
    self.toggleSelected(playlistAutoplay, ".fa-rss");
  }
}
let playlistFeatures = new PlaylistFeatures;

// * This function loads the previous and next video button's data in the playlist manipulation footer
// * It is called whenever anything changes in the playlist or the currently playing video changes

function videoPreviews() {
  if (!isMobile) {
    function addData(which, iteration) {
      $("#" + which + "Video .videoName").text(decodeURIComponent(videos[iteration][0]));
      $("#" + which + "Video .videoTime").text(msConversion(videos[iteration][1] * 1000));
      $("#" + which + "Video .videoImage").attr("src", "https://i.ytimg.com/vi/" + videos[iteration][2] + "/default.jpg");
    }
    function changeOpacity(which, amount) {
      $("#" + which + "Video .videoName, #" + which + "Video .videoImage, #" + which + "Video .videoTime").css("opacity", amount);
    }
    function greyOut(which, color) {
      $("#" + which + "Video").css("background-color", color);
      if (color === "white") {
        color = "black";
      }
      else {
        color = "inherit";
      }
      $("#" + which + "Video .videoImageContainer").css("background", color);
    }

    if (changeIteration(1) <= videoCounter) {
      changeOpacity("next", "1");
      greyOut("next", "white");
      addData("next", changeIteration(1));
    }
    else {
      changeOpacity("next", "0");
      greyOut("next", "grey");
    }

    if (changeIteration(-1) > 0 || (playlistRepeat && videoIteration == 1)) {
      changeOpacity("previous", "1");
      greyOut("previous", "white");
      addData("previous", (playlistRepeat && videoIteration == 1 ? changeIteration(-2) + 1 : changeIteration(-1)));
    }
    else {
      changeOpacity("previous", "0");
      greyOut("previous", "grey");
    }
  }
}
<commit_msg>switch to using escape, fingers crossed<commit_after>/**
  Copyright 2018 LNFWebsite
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at
      http://www.apache.org/licenses/LICENSE-2.0
  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
**/

// * This function highlights playlist elements

function highlight(i, which, persist) {
  $("tr:nth-child(" + i + ")").attr("id", "new-" + which);
  if (!persist) {
    $("tr." + which).removeClass(which);
  }
  $("#new-" + which).addClass(which);
  $("#new-" + which).removeAttr("id");
}

// * This function adds video elements to the playlist

function addVideoToList(name, time, spot, smooth) {
  //name = decodeURIComponent(name); TESTING
  name = escape(name);

  if (smooth) {
    smooth = " class=\"animated flipInX\"";
  }
  else {
    smooth = "";
  }

  //let trElement = "
" + name + "
<button class=\"tableButton removeButton\" onclick=\"buttonRemoveVideo(this);\" title=\"Remove\"></button>" +
  //"<button class=\"tableButton playButton\" onclick=\"buttonPlayVideo(this);\" title=\"Play\"></button>
	" + time + "
";

  let trElement = "
" + name +
  "
" +
  "" +
  "" +
  "" +
  "
	" + time + "
";

  if ($("#videosTable > tr").length > 0) {
    if (spot > 1) {
      $("#videosTable > tr").eq(spot-2).after(trElement);
    }
    else {
      $(trElement).insertBefore("#videosTable > tr:first");
    }
  }
  else {
    $("#videosTable").append(trElement);
  }
}

// * This function removes video elements from the playlist

function removeVideoFromList(index, smooth) {
  if (smooth === true) {
    $("tr:nth-child(" + index + ")").fadeOut(function() {
      $(this).remove();
    });
  }
  else {
    $("tr:nth-child(" + index + ")").remove();
  }
}

// * This function restores the video list highlighting as applicable
// * It does not use stored values, but rather the state of the playlist

function restoreHighlight(which) {
  if (which === videoIteration) {
    highlight(which, "selected", false);
  }
  if (videos[which][2] === baseAutoplayVideoId) {
    highlight(which, "radio", false);
  }
  if (videoErrorIds.indexOf(videos[which][2]) > -1) {
    highlight(which, "videoError", true);
  }
}

// * This function refreshes the playlist viewer with videos in the videos array
// * It's primary use is for playlist shuffling

function refreshVideoList() {
  for (let i = 1; i < videos.length; i++) {
    removeVideoFromList(i, false);
    let printTime = msConversion(videos[i][1] * 1000);
    addVideoToList(videos[i][0], printTime, i, false);
    restoreHighlight(i);
  }
}

// * These functions are called when the play/remove video buttons in the playlist viewer are clicked

let PlaylistButtons = function() {
  this.play = function(element) {
    let index = $(".playButton").index(element);
    actionPlayVideo(index);
  }
  this.remove = function(element) {
    let index = $(".removeButton").index(element) + 1;
    actionRemoveVideo(index);
  }
  this.autoplay = function(element) {
    let index = $(".autoplayButton").index(element) + 1;
    console.log("here: " + index);
    playlistAutoplay = true;
    addAutoplayVideo(index, 'reset');
    playlistFeatures.toggleSelected(playlistAutoplay, ".fa-rss");
  }
}
let playlistButtons = new PlaylistButtons;

// * This function makes the entire playlist viewer sortable by dragging&dropping
// * It calls the actionMoveVideo function above

function makeSortable() {
  $( "#videosTable" ).sortable({
    update: function(event, ui) {
      actionMoveVideo(oldIndex + 1, ui.item.index() + 1);
      setPlaylist();
      videoPreviews();
    },
    start: function(event, ui) {
      oldIndex = ui.item.index();
    },
    cancel: "span"
  });
}

// * This object is for the settings available in the playlist manipulation footer
// * Those buttons call their corresponding functionality here

let PlaylistFeatures = function() {
  let self = this;
  this.toggleSelected = function(doit, which) {
    if (doit) {
      $(which).addClass("selected");
    }
    else {
      $(which).removeClass("selected");
    }
  }
  this.playNext = function() {
    sendStation("playlistfeaturesplaynext");
    playlistPlayNext = (playlistPlayNext ? false : true);
    self.toggleSelected(playlistPlayNext, ".fa-arrow-circle-right");
  }
  this.repeat = function() {
    sendStation("playlistfeaturesrepeat");
    playlistRepeat = (playlistRepeat ? false : true);
    videoPreviews();
    self.toggleSelected(playlistRepeat, ".fa-redo-alt");
  }
  this.shuffle = function() {
    sendStation("playlistfeaturesshuffle");
    playlistShuffle = (playlistShuffle ? false : true);
    shufflePlaylist();
    self.toggleSelected(playlistShuffle, ".fa-random");
  }
  this.autoplay = function() {
    playlistAutoplay = (playlistAutoplay ? false : true);
    if (playlistAutoplay === false) {
      autoplayVideos = [];
      autoplayVideoIteration = 0;
      baseAutoplayVideoId = false;
      autoplayList = false;
      autoplayLoading = false;
      $("tr").removeClass("radio");
    }
    else {
      addAutoplayVideo();
    }
    self.toggleSelected(playlistAutoplay, ".fa-rss");
  }
}
let playlistFeatures = new PlaylistFeatures;

// * This function loads the previous and next video button's data in the playlist manipulation footer
// * It is called whenever anything changes in the playlist or the currently playing video changes

function videoPreviews() {
  if (!isMobile) {
    function addData(which, iteration) {
      $("#" + which + "Video .videoName").text(decodeURIComponent(videos[iteration][0]));
      $("#" + which + "Video .videoTime").text(msConversion(videos[iteration][1] * 1000));
      $("#" + which + "Video .videoImage").attr("src", "https://i.ytimg.com/vi/" + videos[iteration][2] + "/default.jpg");
    }
    function changeOpacity(which, amount) {
      $("#" + which + "Video .videoName, #" + which + "Video .videoImage, #" + which + "Video .videoTime").css("opacity", amount);
    }
    function greyOut(which, color) {
      $("#" + which + "Video").css("background-color", color);
      if (color === "white") {
        color = "black";
      }
      else {
        color = "inherit";
      }
      $("#" + which + "Video .videoImageContainer").css("background", color);
    }

    if (changeIteration(1) <= videoCounter) {
      changeOpacity("next", "1");
      greyOut("next", "white");
      addData("next", changeIteration(1));
    }
    else {
      changeOpacity("next", "0");
      greyOut("next", "grey");
    }

    if (changeIteration(-1) > 0 || (playlistRepeat && videoIteration == 1)) {
      changeOpacity("previous", "1");
      greyOut("previous", "white");
      addData("previous", (playlistRepeat && videoIteration == 1 ? changeIteration(-2) + 1 : changeIteration(-1)));
    }
    else {
      changeOpacity("previous", "0");
      greyOut("previous", "grey");
    }
  }
}
<|endoftext|>


<commit_before>/*
 * $Id$
 * (c) Copyright 2002-2016 by authors of the Tiki Wiki CMS Groupware Project
 *
 * All Rights Reserved. See copyright.txt for details and a complete list of authors.
 * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
 *
 * Ckeditor styles definition for Tiki 6
 */

CKEDITOR.stylesSet.add('tikistyles',
	[
	 {name:'Normal',element:'p'},
	 {name:'Title Bar',element:'div',attributes:{'class':'titlebar'}},
	 {name:'Simple Box',element:'div',attributes:{'class':'simplebox'}},
	 {name:'Code',element:'div',attributes:{'class':'code'}}
	]);

<commit_msg>Add useful info in source<commit_after>/*
 * $Id$
 * (c) Copyright 2002-2016 by authors of the Tiki Wiki CMS Groupware Project
 *
 * All Rights Reserved. See copyright.txt for details and a complete list of authors.
 * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
 *
 * Ckeditor styles definition for Tiki 6
 */

/* If your changes here do not make any change in the wysiwyg editor, the reason is, ckeditor stores things in your browser cache.
 * Good news: most users can see your modifications, it's just you.
 * During the time when you wish to try changes here, uncomment the following line. You may need to reload a page with an open editor the first time.
 * Then comment it again because it kills ckeditor performance.
 */
/*
  CKEDITOR.timestamp= +new Date;
*/
CKEDITOR.stylesSet.add('tikistyles',
	[
	 {name:'Normal',element:'p'},
	 {name:'Title Bar',element:'div',attributes:{'class':'titlebar'}},
	 {name:'Simple Box',element:'div',attributes:{'class':'simplebox'}},
	 {name:'Code',element:'div',attributes:{'class':'code'}}
	]);

<|endoftext|>


<commit_before>/**
 * @module gulp/tasks/build/images
 */

// Gulp
const gulp = __require( 'gulp' );

// Files
const size = __require( 'gulp-size' );

// Images
const imagemin = __require( 'gulp-imagemin' );

// Task config
export const config = {
	size: {
		title: 'Images:',
	},
};

// Task files.
export const files = {
	source: 'assets/source/images/*',
	dest:   'assets/dist/images',
};

/**
 * Gulp callback for `build/images` task.
 *
 * @return {Function}
 */
export const callback = () =>
	gulp.src( files.source )
		.pipe( imagemin() )
		.pipe( gulp.dest( files.dest ) )
		.pipe( size( config.size ) );

// Register the task.
gulp.task( 'build/images', callback );
<commit_msg>Fix file size report for `build/images` task<commit_after>/**
 * @module gulp/tasks/build/images
 */

// Gulp
const gulp = __require( 'gulp' );

// Files
const size = __require( 'gulp-size' );

// Images
const imagemin = __require( 'gulp-imagemin' );

// Task config
export const config = {
	size: {
		title: 'Images:',
	},
};

// Task files.
export const files = {
	source: 'assets/source/images/*',
	dest:   'assets/dist/images',
};

/**
 * Gulp callback for `build/images` task.
 *
 * @return {Function}
 */
export const callback = () =>
	gulp.src( files.source )
		.pipe( imagemin() )
		.pipe( size( config.size ) )
		.pipe( gulp.dest( files.dest ) );

// Register the task.
gulp.task( 'build/images', callback );
<|endoftext|>


<commit_before>/**
 * This module adds GDPR consentManagement support to prebid.js.  It interacts with
 * supported CMPs (Consent Management Platforms) to grab the user's consent information
 * and make it available for any GDPR supported adapters to read/pass this information to
 * their system.
 */
import * as utils from 'src/utils';
import { config } from 'src/config';
import { gdprDataHandler } from 'src/adaptermanager';
import includes from 'core-js/library/fn/array/includes';
import strIncludes from 'core-js/library/fn/string/includes';

const DEFAULT_CMP = 'iab';
const DEFAULT_CONSENT_TIMEOUT = 10000;
const DEFAULT_ALLOW_AUCTION_WO_CONSENT = true;

export let userCMP;
export let consentTimeout;
export let allowAuction;

let consentData;

// add new CMPs here, with their dedicated lookup function
const cmpCallMap = {
  'iab': lookupIabConsent
};

/**
 * This function handles interacting with an IAB compliant CMP to obtain the consent information of the user.
 * Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function
 * based on the appropriate result.
 * @param {function(string)} cmpSuccess acts as a success callback when CMP returns a value; pass along consentObject (string) from CMP
 * @param {function(string)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string)
 * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function)
 */
function lookupIabConsent(cmpSuccess, cmpError, hookConfig) {
  function handleCmpResponseCallbacks() {
    const cmpResponse = {};

    function afterEach() {
      if (cmpResponse.getConsentData && cmpResponse.getVendorConsents) {
        cmpSuccess(cmpResponse, hookConfig);
      }
    }

    return {
      consentDataCallback: function(consentResponse) {
        cmpResponse.getConsentData = consentResponse;
        afterEach();
      },
      vendorConsentsCallback: function(consentResponse) {
        cmpResponse.getVendorConsents = consentResponse;
        afterEach();
      }
    }
  }

  let callbackHandler = handleCmpResponseCallbacks();
  let cmpCallbacks = {};
  let cmpFunction;

  // to collect the consent information from the user, we perform two calls to the CMP in parallel:
  // first to collect the user's consent choices represented in an encoded string (via getConsentData)
  // second to collect the user's full unparsed consent information (via getVendorConsents)

  // the following code also determines where the CMP is located and uses the proper workflow to communicate with it:
  // check to see if CMP is found on the same window level as prebid and call it directly if so
  // check to see if prebid is in a safeframe (with CMP support)
  // else assume prebid may be inside an iframe and use the IAB CMP locator code to see if CMP's located in a higher parent window. this works in cross domain iframes
  // if the CMP is not found, the iframe function will call the cmpError exit callback to abort the rest of the CMP workflow
  try {
    cmpFunction = window.__cmp || utils.getWindowTop().__cmp;
  } catch (e) {}

  if (utils.isFn(cmpFunction)) {
    cmpFunction('getConsentData', null, callbackHandler.consentDataCallback);
    cmpFunction('getVendorConsents', null, callbackHandler.vendorConsentsCallback);
  } else if (inASafeFrame() && typeof window.$sf.ext.cmp === 'function') {
    callCmpWhileInSafeFrame('getConsentData', callbackHandler.consentDataCallback);
    callCmpWhileInSafeFrame('getVendorConsents', callbackHandler.vendorConsentsCallback);
  } else {
    // find the CMP frame
    let f = window;
    let cmpFrame;
    while (!cmpFrame) {
      try {
        if (f.frames['__cmpLocator']) cmpFrame = f;
      } catch (e) {}
      if (f === window.top) break;
      f = f.parent;
    }

    callCmpWhileInIframe('getConsentData', cmpFrame, callbackHandler.consentDataCallback);
    callCmpWhileInIframe('getVendorConsents', cmpFrame, callbackHandler.vendorConsentsCallback);
  }

  function inASafeFrame() {
    return !!(window.$sf && window.$sf.ext);
  }

  function callCmpWhileInSafeFrame(commandName, callback) {
    function sfCallback(msgName, data) {
      if (msgName === 'cmpReturn') {
        let responseObj = (commandName === 'getConsentData') ? data.vendorConsentData : data.vendorConsents;
        callback(responseObj);
      }
    }

    // find sizes from adUnits object
    let adUnits = hookConfig.adUnits;
    let width = 1;
    let height = 1;
    if (Array.isArray(adUnits) && adUnits.length > 0) {
      let sizes = utils.getAdUnitSizes(adUnits[0]);
      width = sizes[0][0];
      height = sizes[0][1];
    }

    window.$sf.ext.register(width, height, sfCallback);
    window.$sf.ext.cmp(commandName);
  }

  function callCmpWhileInIframe(commandName, cmpFrame, moduleCallback) {
    /* Setup up a __cmp function to do the postMessage and stash the callback.
      This function behaves (from the caller's perspective identicially to the in-frame __cmp call */
    window.__cmp = function(cmd, arg, callback) {
      if (!cmpFrame) {
        removePostMessageListener();

        let errmsg = 'CMP not found';
        return cmpError(errmsg, hookConfig);
      }
      let callId = Math.random() + '';
      let msg = {__cmpCall: {
        command: cmd,
        parameter: arg,
        callId: callId
      }};
      cmpCallbacks[callId] = callback;
      cmpFrame.postMessage(msg, '*');
    }

    /** when we get the return message, call the stashed callback */
    window.addEventListener('message', readPostMessageResponse, false);

    // call CMP
    window.__cmp(commandName, null, cmpIframeCallback);

    function readPostMessageResponse(event) {
      let json = (typeof event.data === 'string' && strIncludes(event.data, 'cmpReturn')) ? JSON.parse(event.data) : event.data;
      if (json.__cmpReturn && json.__cmpReturn.callId) {
        let i = json.__cmpReturn;
        // TODO - clean up this logic (move listeners?); we have duplicate messages responses because 2 eventlisteners are active from the 2 cmp requests running in parallel
        if (typeof cmpCallbacks[i.callId] !== 'undefined') {
          cmpCallbacks[i.callId](i.returnValue, i.success);
          delete cmpCallbacks[i.callId];
        }
      }
    }

    function removePostMessageListener() {
      window.removeEventListener('message', readPostMessageResponse, false);
    }

    function cmpIframeCallback(consentObject) {
      removePostMessageListener();
      moduleCallback(consentObject);
    }
  }
}

/**
 * If consentManagement module is enabled (ie included in setConfig), this hook function will attempt to fetch the
 * user's encoded consent string from the supported CMP.  Once obtained, the module will store this
 * data as part of a gdprConsent object which gets transferred to adaptermanager's gdprDataHandler object.
 * This information is later added into the bidRequest object for any supported adapters to read/pass along to their system.
 * @param {object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids.
 * @param {function} fn required; The next function in the chain, used by hook.js
 */
export function requestBidsHook(reqBidsConfigObj, fn) {
  // preserves all module related variables for the current auction instance (used primiarily for concurrent auctions)
  const hookConfig = {
    context: this,
    args: arguments,
    nextFn: fn,
    adUnits: reqBidsConfigObj.adUnits || $$PREBID_GLOBAL$$.adUnits,
    bidsBackHandler: reqBidsConfigObj.bidsBackHandler,
    haveExited: false,
    timer: null
  };

  // in case we already have consent (eg during bid refresh)
  if (consentData) {
    return exitModule(null, hookConfig);
  }

  if (!includes(Object.keys(cmpCallMap), userCMP)) {
    utils.logWarn(`CMP framework (${userCMP}) is not a supported framework.  Aborting consentManagement module and resuming auction.`);
    return hookConfig.nextFn.apply(hookConfig.context, hookConfig.args);
  }

  cmpCallMap[userCMP].call(this, processCmpData, cmpFailed, hookConfig);

  // only let this code run if module is still active (ie if the callbacks used by CMPs haven't already finished)
  if (!hookConfig.haveExited) {
    if (consentTimeout === 0) {
      processCmpData(undefined, hookConfig);
    } else {
      hookConfig.timer = setTimeout(cmpTimedOut.bind(null, hookConfig), consentTimeout);
    }
  }
}

/**
 * This function checks the consent data provided by CMP to ensure it's in an expected state.
 * If it's bad, we exit the module depending on config settings.
 * If it's good, then we store the value and exits the module.
 * @param {object} consentObject required; object returned by CMP that contains user's consent choices
 * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function)
 */
function processCmpData(consentObject, hookConfig) {
  if (
    !utils.isPlainObject(consentObject) ||
    (!utils.isPlainObject(consentObject.getVendorConsents) || Object.keys(consentObject.getVendorConsents).length === 0) ||
    (!utils.isPlainObject(consentObject.getConsentData) || Object.keys(consentObject.getConsentData).length === 0)) {
    cmpFailed(`CMP returned unexpected value during lookup process; returned value was (${consentObject}).`, hookConfig);
  } else {
    clearTimeout(hookConfig.timer);
    storeConsentData(consentObject);

    exitModule(null, hookConfig);
  }
}

/**
 * General timeout callback when interacting with CMP takes too long.
 */
function cmpTimedOut(hookConfig) {
  cmpFailed('CMP workflow exceeded timeout threshold.', hookConfig);
}

/**
 * This function contains the controlled steps to perform when there's a problem with CMP.
 * @param {string} errMsg required; should be a short descriptive message for why the failure/issue happened.
 * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function)
*/
function cmpFailed(errMsg, hookConfig) {
  clearTimeout(hookConfig.timer);

  // still set the consentData to undefined when there is a problem as per config options
  if (allowAuction) {
    storeConsentData(undefined);
  }
  exitModule(errMsg, hookConfig);
}

/**
 * Stores CMP data locally in module and then invokes gdprDataHandler.setConsentData() to make information available in adaptermanger.js for later in the auction
 * @param {object} cmpConsentObject required; an object representing user's consent choices (can be undefined in certain use-cases for this function only)
 */
function storeConsentData(cmpConsentObject) {
  consentData = {
    consentString: (cmpConsentObject) ? cmpConsentObject.getConsentData.consentData : undefined,
    vendorData: (cmpConsentObject) ? cmpConsentObject.getVendorConsents : undefined,
    gdprApplies: (cmpConsentObject) ? cmpConsentObject.getConsentData.gdprApplies : undefined
  };
  gdprDataHandler.setConsentData(consentData);
}

/**
 * This function handles the exit logic for the module.
 * There are several paths in the module's logic to call this function and we only allow 1 of the 3 potential exits to happen before suppressing others.
 *
 * We prevent multiple exits to avoid conflicting messages in the console depending on certain scenarios.
 * One scenario could be auction was canceled due to timeout with CMP being reached.
 * While the timeout is the accepted exit and runs first, the CMP's callback still tries to process the user's data (which normally leads to a good exit).
 * In this case, the good exit will be suppressed since we already decided to cancel the auction.
 *
 * Three exit paths are:
 * 1. good exit where auction runs (CMP data is processed normally).
 * 2. bad exit but auction still continues (warning message is logged, CMP data is undefined and still passed along).
 * 3. bad exit with auction canceled (error message is logged).
 * @param {string} errMsg optional; only to be used when there was a 'bad' exit.  String is a descriptive message for the failure/issue encountered.
 * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function)
 */
function exitModule(errMsg, hookConfig) {
  if (hookConfig.haveExited === false) {
    hookConfig.haveExited = true;

    let context = hookConfig.context;
    let args = hookConfig.args;
    let nextFn = hookConfig.nextFn;

    if (errMsg) {
      if (allowAuction) {
        utils.logWarn(errMsg + ' Resuming auction without consent data as per consentManagement config.');
        nextFn.apply(context, args);
      } else {
        utils.logError(errMsg + ' Canceling auction as per consentManagement config.');
        if (typeof hookConfig.bidsBackHandler === 'function') {
          hookConfig.bidsBackHandler();
        } else {
          utils.logError('Error executing bidsBackHandler');
        }
      }
    } else {
      nextFn.apply(context, args);
    }
  }
}

/**
 * Simply resets the module's consentData variable back to undefined, mainly for testing purposes
 */
export function resetConsentData() {
  consentData = undefined;
  gdprDataHandler.setConsentData(null);
}

/**
 * A configuration function that initializes some module variables, as well as add a hook into the requestBids function
 * @param {object} config required; consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean)
 */
export function setConfig(config) {
  if (utils.isStr(config.cmpApi)) {
    userCMP = config.cmpApi;
  } else {
    userCMP = DEFAULT_CMP;
    utils.logInfo(`consentManagement config did not specify cmp.  Using system default setting (${DEFAULT_CMP}).`);
  }

  if (utils.isNumber(config.timeout)) {
    consentTimeout = config.timeout;
  } else {
    consentTimeout = DEFAULT_CONSENT_TIMEOUT;
    utils.logInfo(`consentManagement config did not specify timeout.  Using system default setting (${DEFAULT_CONSENT_TIMEOUT}).`);
  }

  if (typeof config.allowAuctionWithoutConsent === 'boolean') {
    allowAuction = config.allowAuctionWithoutConsent;
  } else {
    allowAuction = DEFAULT_ALLOW_AUCTION_WO_CONSENT;
    utils.logInfo(`consentManagement config did not specify allowAuctionWithoutConsent.  Using system default setting (${DEFAULT_ALLOW_AUCTION_WO_CONSENT}).`);
  }

  $$PREBID_GLOBAL$$.requestBids.addHook(requestBidsHook, 50);
}
config.getConfig('consentManagement', config => setConfig(config.consentManagement));
<commit_msg>update consentManagement error logic/handling (#2723)<commit_after>/**
 * This module adds GDPR consentManagement support to prebid.js.  It interacts with
 * supported CMPs (Consent Management Platforms) to grab the user's consent information
 * and make it available for any GDPR supported adapters to read/pass this information to
 * their system.
 */
import * as utils from 'src/utils';
import { config } from 'src/config';
import { gdprDataHandler } from 'src/adaptermanager';
import includes from 'core-js/library/fn/array/includes';
import strIncludes from 'core-js/library/fn/string/includes';

const DEFAULT_CMP = 'iab';
const DEFAULT_CONSENT_TIMEOUT = 10000;
const DEFAULT_ALLOW_AUCTION_WO_CONSENT = true;

export let userCMP;
export let consentTimeout;
export let allowAuction;

let consentData;

// add new CMPs here, with their dedicated lookup function
const cmpCallMap = {
  'iab': lookupIabConsent
};

/**
 * This function handles interacting with an IAB compliant CMP to obtain the consent information of the user.
 * Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function
 * based on the appropriate result.
 * @param {function(string)} cmpSuccess acts as a success callback when CMP returns a value; pass along consentObject (string) from CMP
 * @param {function(string)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string)
 * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function)
 */
function lookupIabConsent(cmpSuccess, cmpError, hookConfig) {
  function handleCmpResponseCallbacks() {
    const cmpResponse = {};

    function afterEach() {
      if (cmpResponse.getConsentData && cmpResponse.getVendorConsents) {
        cmpSuccess(cmpResponse, hookConfig);
      }
    }

    return {
      consentDataCallback: function(consentResponse) {
        cmpResponse.getConsentData = consentResponse;
        afterEach();
      },
      vendorConsentsCallback: function(consentResponse) {
        cmpResponse.getVendorConsents = consentResponse;
        afterEach();
      }
    }
  }

  let callbackHandler = handleCmpResponseCallbacks();
  let cmpCallbacks = {};
  let cmpFunction;

  // to collect the consent information from the user, we perform two calls to the CMP in parallel:
  // first to collect the user's consent choices represented in an encoded string (via getConsentData)
  // second to collect the user's full unparsed consent information (via getVendorConsents)

  // the following code also determines where the CMP is located and uses the proper workflow to communicate with it:
  // check to see if CMP is found on the same window level as prebid and call it directly if so
  // check to see if prebid is in a safeframe (with CMP support)
  // else assume prebid may be inside an iframe and use the IAB CMP locator code to see if CMP's located in a higher parent window. this works in cross domain iframes
  // if the CMP is not found, the iframe function will call the cmpError exit callback to abort the rest of the CMP workflow
  try {
    cmpFunction = window.__cmp || utils.getWindowTop().__cmp;
  } catch (e) {}

  if (utils.isFn(cmpFunction)) {
    cmpFunction('getConsentData', null, callbackHandler.consentDataCallback);
    cmpFunction('getVendorConsents', null, callbackHandler.vendorConsentsCallback);
  } else if (inASafeFrame() && typeof window.$sf.ext.cmp === 'function') {
    callCmpWhileInSafeFrame('getConsentData', callbackHandler.consentDataCallback);
    callCmpWhileInSafeFrame('getVendorConsents', callbackHandler.vendorConsentsCallback);
  } else {
    // find the CMP frame
    let f = window;
    let cmpFrame;
    while (!cmpFrame) {
      try {
        if (f.frames['__cmpLocator']) cmpFrame = f;
      } catch (e) {}
      if (f === window.top) break;
      f = f.parent;
    }

    callCmpWhileInIframe('getConsentData', cmpFrame, callbackHandler.consentDataCallback);
    callCmpWhileInIframe('getVendorConsents', cmpFrame, callbackHandler.vendorConsentsCallback);
  }

  function inASafeFrame() {
    return !!(window.$sf && window.$sf.ext);
  }

  function callCmpWhileInSafeFrame(commandName, callback) {
    function sfCallback(msgName, data) {
      if (msgName === 'cmpReturn') {
        let responseObj = (commandName === 'getConsentData') ? data.vendorConsentData : data.vendorConsents;
        callback(responseObj);
      }
    }

    // find sizes from adUnits object
    let adUnits = hookConfig.adUnits;
    let width = 1;
    let height = 1;
    if (Array.isArray(adUnits) && adUnits.length > 0) {
      let sizes = utils.getAdUnitSizes(adUnits[0]);
      width = sizes[0][0];
      height = sizes[0][1];
    }

    window.$sf.ext.register(width, height, sfCallback);
    window.$sf.ext.cmp(commandName);
  }

  function callCmpWhileInIframe(commandName, cmpFrame, moduleCallback) {
    /* Setup up a __cmp function to do the postMessage and stash the callback.
      This function behaves (from the caller's perspective identicially to the in-frame __cmp call */
    window.__cmp = function(cmd, arg, callback) {
      if (!cmpFrame) {
        removePostMessageListener();

        let errmsg = 'CMP not found';
        return cmpError(errmsg, hookConfig);
      }
      let callId = Math.random() + '';
      let msg = {__cmpCall: {
        command: cmd,
        parameter: arg,
        callId: callId
      }};
      cmpCallbacks[callId] = callback;
      cmpFrame.postMessage(msg, '*');
    }

    /** when we get the return message, call the stashed callback */
    window.addEventListener('message', readPostMessageResponse, false);

    // call CMP
    window.__cmp(commandName, null, cmpIframeCallback);

    function readPostMessageResponse(event) {
      let json = (typeof event.data === 'string' && strIncludes(event.data, 'cmpReturn')) ? JSON.parse(event.data) : event.data;
      if (json.__cmpReturn && json.__cmpReturn.callId) {
        let i = json.__cmpReturn;
        // TODO - clean up this logic (move listeners?); we have duplicate messages responses because 2 eventlisteners are active from the 2 cmp requests running in parallel
        if (typeof cmpCallbacks[i.callId] !== 'undefined') {
          cmpCallbacks[i.callId](i.returnValue, i.success);
          delete cmpCallbacks[i.callId];
        }
      }
    }

    function removePostMessageListener() {
      window.removeEventListener('message', readPostMessageResponse, false);
    }

    function cmpIframeCallback(consentObject) {
      removePostMessageListener();
      moduleCallback(consentObject);
    }
  }
}

/**
 * If consentManagement module is enabled (ie included in setConfig), this hook function will attempt to fetch the
 * user's encoded consent string from the supported CMP.  Once obtained, the module will store this
 * data as part of a gdprConsent object which gets transferred to adaptermanager's gdprDataHandler object.
 * This information is later added into the bidRequest object for any supported adapters to read/pass along to their system.
 * @param {object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids.
 * @param {function} fn required; The next function in the chain, used by hook.js
 */
export function requestBidsHook(reqBidsConfigObj, fn) {
  // preserves all module related variables for the current auction instance (used primiarily for concurrent auctions)
  const hookConfig = {
    context: this,
    args: arguments,
    nextFn: fn,
    adUnits: reqBidsConfigObj.adUnits || $$PREBID_GLOBAL$$.adUnits,
    bidsBackHandler: reqBidsConfigObj.bidsBackHandler,
    haveExited: false,
    timer: null
  };

  // in case we already have consent (eg during bid refresh)
  if (consentData) {
    return exitModule(null, hookConfig);
  }

  if (!includes(Object.keys(cmpCallMap), userCMP)) {
    utils.logWarn(`CMP framework (${userCMP}) is not a supported framework.  Aborting consentManagement module and resuming auction.`);
    return hookConfig.nextFn.apply(hookConfig.context, hookConfig.args);
  }

  cmpCallMap[userCMP].call(this, processCmpData, cmpFailed, hookConfig);

  // only let this code run if module is still active (ie if the callbacks used by CMPs haven't already finished)
  if (!hookConfig.haveExited) {
    if (consentTimeout === 0) {
      processCmpData(undefined, hookConfig);
    } else {
      hookConfig.timer = setTimeout(cmpTimedOut.bind(null, hookConfig), consentTimeout);
    }
  }
}

/**
 * This function checks the consent data provided by CMP to ensure it's in an expected state.
 * If it's bad, we exit the module depending on config settings.
 * If it's good, then we store the value and exits the module.
 * @param {object} consentObject required; object returned by CMP that contains user's consent choices
 * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function)
 */
function processCmpData(consentObject, hookConfig) {
  let gdprApplies = consentObject && consentObject.getConsentData && consentObject.getConsentData.gdprApplies;
  if (
    (typeof gdprApplies !== 'boolean') ||
    (gdprApplies === true &&
      !(utils.isStr(consentObject.getConsentData.consentData) &&
        utils.isPlainObject(consentObject.getVendorConsents) &&
        Object.keys(consentObject.getVendorConsents).length > 1
      )
    )
  ) {
    cmpFailed(`CMP returned unexpected value during lookup process.`, hookConfig, consentObject);
  } else {
    clearTimeout(hookConfig.timer);
    storeConsentData(consentObject);

    exitModule(null, hookConfig);
  }
}

/**
 * General timeout callback when interacting with CMP takes too long.
 */
function cmpTimedOut(hookConfig) {
  cmpFailed('CMP workflow exceeded timeout threshold.', hookConfig);
}

/**
 * This function contains the controlled steps to perform when there's a problem with CMP.
 * @param {string} errMsg required; should be a short descriptive message for why the failure/issue happened.
 * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function)
 * @param {object} extraArgs contains additional data that's passed along in the error/warning messages for easier debugging
*/
function cmpFailed(errMsg, hookConfig, extraArgs) {
  clearTimeout(hookConfig.timer);

  // still set the consentData to undefined when there is a problem as per config options
  if (allowAuction) {
    storeConsentData(undefined);
  }
  exitModule(errMsg, hookConfig, extraArgs);
}

/**
 * Stores CMP data locally in module and then invokes gdprDataHandler.setConsentData() to make information available in adaptermanger.js for later in the auction
 * @param {object} cmpConsentObject required; an object representing user's consent choices (can be undefined in certain use-cases for this function only)
 */
function storeConsentData(cmpConsentObject) {
  consentData = {
    consentString: (cmpConsentObject) ? cmpConsentObject.getConsentData.consentData : undefined,
    vendorData: (cmpConsentObject) ? cmpConsentObject.getVendorConsents : undefined,
    gdprApplies: (cmpConsentObject) ? cmpConsentObject.getConsentData.gdprApplies : undefined
  };
  gdprDataHandler.setConsentData(consentData);
}

/**
 * This function handles the exit logic for the module.
 * There are several paths in the module's logic to call this function and we only allow 1 of the 3 potential exits to happen before suppressing others.
 *
 * We prevent multiple exits to avoid conflicting messages in the console depending on certain scenarios.
 * One scenario could be auction was canceled due to timeout with CMP being reached.
 * While the timeout is the accepted exit and runs first, the CMP's callback still tries to process the user's data (which normally leads to a good exit).
 * In this case, the good exit will be suppressed since we already decided to cancel the auction.
 *
 * Three exit paths are:
 * 1. good exit where auction runs (CMP data is processed normally).
 * 2. bad exit but auction still continues (warning message is logged, CMP data is undefined and still passed along).
 * 3. bad exit with auction canceled (error message is logged).
 * @param {string} errMsg optional; only to be used when there was a 'bad' exit.  String is a descriptive message for the failure/issue encountered.
 * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function)
 * @param {object} extraArgs contains additional data that's passed along in the error/warning messages for easier debugging
 */
function exitModule(errMsg, hookConfig, extraArgs) {
  if (hookConfig.haveExited === false) {
    hookConfig.haveExited = true;

    let context = hookConfig.context;
    let args = hookConfig.args;
    let nextFn = hookConfig.nextFn;

    if (errMsg) {
      if (allowAuction) {
        utils.logWarn(errMsg + ' Resuming auction without consent data as per consentManagement config.', extraArgs);
        nextFn.apply(context, args);
      } else {
        utils.logError(errMsg + ' Canceling auction as per consentManagement config.', extraArgs);
        if (typeof hookConfig.bidsBackHandler === 'function') {
          hookConfig.bidsBackHandler();
        } else {
          utils.logError('Error executing bidsBackHandler');
        }
      }
    } else {
      nextFn.apply(context, args);
    }
  }
}

/**
 * Simply resets the module's consentData variable back to undefined, mainly for testing purposes
 */
export function resetConsentData() {
  consentData = undefined;
  gdprDataHandler.setConsentData(null);
}

/**
 * A configuration function that initializes some module variables, as well as add a hook into the requestBids function
 * @param {object} config required; consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean)
 */
export function setConfig(config) {
  if (utils.isStr(config.cmpApi)) {
    userCMP = config.cmpApi;
  } else {
    userCMP = DEFAULT_CMP;
    utils.logInfo(`consentManagement config did not specify cmp.  Using system default setting (${DEFAULT_CMP}).`);
  }

  if (utils.isNumber(config.timeout)) {
    consentTimeout = config.timeout;
  } else {
    consentTimeout = DEFAULT_CONSENT_TIMEOUT;
    utils.logInfo(`consentManagement config did not specify timeout.  Using system default setting (${DEFAULT_CONSENT_TIMEOUT}).`);
  }

  if (typeof config.allowAuctionWithoutConsent === 'boolean') {
    allowAuction = config.allowAuctionWithoutConsent;
  } else {
    allowAuction = DEFAULT_ALLOW_AUCTION_WO_CONSENT;
    utils.logInfo(`consentManagement config did not specify allowAuctionWithoutConsent.  Using system default setting (${DEFAULT_ALLOW_AUCTION_WO_CONSENT}).`);
  }

  $$PREBID_GLOBAL$$.requestBids.addHook(requestBidsHook, 50);
}
config.getConfig('consentManagement', config => setConfig(config.consentManagement));
<|endoftext|>