<commit_before>/**
 * Experience Sampling event page.
 *
 * This background page handles the various events for registering participants
 * and showing new surveys in response to API events.
 *
 * Participants must fill out both a consent form and a startup survey (with
 * demographics) before they can begin to answer real survey questions.
 */

var cesp = {};  // namespace variable

cesp.readyForSurveys = false;
cesp.operatingSystem = "";

// Settings.
cesp.SERVER_URL = 'https://chrome-experience-sampling.appspot.com';
cesp.SUBMIT_SURVEY_ACTION = '/_ah/api/cesp/v1/submitsurvey';
cesp.XHR_TIMEOUT = 4000;
cesp.NOTIFICATION_TITLE = 'New Chrome survey available!';
cesp.NOTIFICATION_BODY = 'Your feedback makes Chrome better.';
cesp.NOTIFICATION_BUTTON = 'Take survey!';
cesp.ICON_FILE = 'icon.png';
cesp.NOTIFICATION_DEFAULT_TIMEOUT = 10;  // minutes
cesp.NOTIFICATION_TAG = 'chromeSurvey';
cesp.ALARM_NAME = 'notificationTimeout';

// SETUP

/**
 * Sets up basic state for the extension. Called when extension is installed.
 */
function setupState() {
  chrome.storage.local.set({'pending_responses': []});
  chrome.runtime.getPlatformInfo(function(platformInfo) {
    cesp.operatingSystem = platformInfo.os;
  });
  // Set the count of surveys shown to 0, and reset it each day.
  chrome.storage.local.set({cesp.SURVEYS_SHOWN_TODAY: 0});
  chrome.alarms.create(cesp.SURVEY_THROTTLE_RESET_ALARM,
      {delayInMinutes: 5, periodInMinutes: 1440});
}

/**
 * Retrieves the registration status from Local Storage.
 */
function getConsentStatus() {
  chrome.storage.local.get(constants.CONSENT_KEY, maybeShowConsentForm);
}

/**
 * Checks whether consent has been granted yet; if not, opens the consent form.
 * @param {object} consentLookup Object containing consent status (or empty).
 */
function maybeShowConsentForm(consentLookup) {
  if (!consentLookup || consentLookup[constants.CONSENT_KEY] == null ||
      consentLookup[constants.CONSENT_KEY] == constants.CONSENT_PENDING) {
    chrome.storage.onChanged.addListener(storageUpdated);
    chrome.tabs.create({'url': chrome.extension.getURL('consent.html')});
  } else if (consentLookup[constants.CONSENT_KEY] ==
             constants.CONSENT_REJECTED) {
    chrome.management.uninstallSelf();
  } else if (consentLookup[constants.CONSENT_KEY] ==
             constants.CONSENT_GRANTED) {
    // Someone might have filled out the consent form previously but not
    // filled out the setup survey. Check to see if that's the case.
    chrome.storage.local.get(constants.SETUP_KEY, maybeShowSetupSurvey);
  }
}

/**
 * Checks whether the setup survey has been completed yet. If it has been, we
 * are now ready to start showing surveys. If not, we need to listen for
 * when it's completed.
 * @param {object} setupLookup Object containing setup survey status (or empty).
 */
function maybeShowSetupSurvey(setupLookup) {
  if (!setupLookup || setupLookup[constants.SETUP_KEY] == null ||
      setupLookup[constants.SETUP_KEY] == constants.SETUP_PENDING) {
    chrome.tabs.create({'url': chrome.extension.getURL('surveys/setup.html')});
  } else if (setupLookup[constants.SETUP_KEY] == constants.SETUP_COMPLETED) {
    cesp.readyForSurveys = true;
  }
}

/**
 * Listens for the setup survey submission. When that happens, signals that
 * the experience sampling is now ready to begin.
 * @param {object} changes The changed portions of the database.
 * @param {string} areaName The name of the storage area.
 */
function storageUpdated(changes, areaName) {
  if (changes && changes[constants.SETUP_KEY] &&
      changes[constants.SETUP_KEY].newValue == constants.SETUP_COMPLETED) {
    cesp.readyForSurveys = true;
  }
}

// Performs consent and registration checks on startup and install.
chrome.runtime.onInstalled.addListener(getConsentStatus);
chrome.runtime.onStartup.addListener(getConsentStatus);
chrome.runtime.onInstalled.addListener(setupState);

// SURVEY HANDLING

/**
 * Clears our existing notification(s).
 */
function clearNotifications(unused) {
  chrome.notifications.clear(cesp.NOTIFICATION_TAG, function(unused) {});
  chrome.alarms.clearAll();
}

/**
 * Creates a new notification to prompt the participant to take an experience
 * sampling survey.
 * @param {object} element The browser element of interest.
 * @param {object} decision The decision the participant made.
 */
function showSurveyNotification(element, decision) {
  if (!cesp.readyForSurveys) return;

  chrome.storage.local.get(cesp.SURVEYS_SHOWN_TODAY, function(items) {
    if (items[cesp.SURVEYS_SHOWN_TODAY] >= cesp.MAX_SURVEYS_PER_DAY) {
      return;
    }

    clearNotifications();

    var timePromptShown = new Date();
    var clickHandler = function(unused) {
      var timePromptClicked = new Date();
      loadSurvey(element, decision, timePromptShown, timePromptClicked);
      clearNotifications();
    };

    var opt = {
      type: 'basic',
      iconUrl: cesp.ICON_FILE,
      title: cesp.NOTIFICATION_TITLE,
      message: cesp.NOTIFICATION_BODY,
      eventTime: Date.now(),
      buttons: [{title: cesp.NOTIFICATION_BUTTON}]
    };
    chrome.notifications.create(
        cesp.NOTIFICATION_TAG,
        opt,
        function(id) {
          chrome.alarms.create(
              cesp.ALARM_NAME,
              {delayInMinutes: cesp.NOTIFICATION_DEFAULT_TIMEOUT});
        });
    chrome.notifications.onClicked.addListener(clickHandler);
    chrome.notifications.onButtonClicked.addListener(clickHandler);

    chrome.storage.local.set({
      cesp.SURVEYS_SHOWN_TODAY: items[cesp.SURVEYS_SHOWN_TODAY] + 1
    });
  }
}

/**
 * Creates a new tab with the experience sampling survey page.
 * @param {object} element The browser element of interest.
 * @param {object} decision The decision the participant made.
 * @param {object} timePromptShown Date object of when the survey prompt
 *     notification was shown to the participant.
 * @param {object} timePromptClicked Date object of when the participant
 *     clicked the survey prompt notification.
 */
function loadSurvey(element, decision, timePromptShown, timePromptClicked) {
  if (!cesp.readyForSurveys) return;

  var surveyLocations = {
    SSL: 'ssl.html',
    EXAMPLE: 'survey-example.html'
  };
  var surveyURL;
  var eventType = constants.FindEventType(element['name']);
  switch (eventType) {
    case constants.EventType.SSL:
      surveyURL = surveyLocations.SSL;
      break;
    case constants.EventType.UNKNOWN:
      surveyURL = surveyLocations.EXAMPLE;
      console.log('Unknown event type: ' + element['name']);
      break;
  }
  chrome.tabs.create(
      {'url': chrome.extension.getURL('surveys/' + surveyURL)},
      function() { console.log('Opened survey.'); });
}

// Trigger the new survey prompt when the participant makes a decision about an
// experience sampling element.
chrome.experienceSamplingPrivate.onDecision.addListener(showSurveyNotification);
// Clear the notification state when the survey times out.
chrome.alarms.onAlarm.addListener(clearNotifications);

/**
 * A survey response (question and answer).
 * @constructor
 * @param {string} question The question being answered.
 * @param {string} answer The answer to that question.
 */
function Response(question, answer) {
  this.question = question;
  this.answer = answer;
}

/**
 * A completed survey.
 * @constructor
 * @param {string} type The type of survey.
 * @param {int} participantId The participant ID.
 * @param {Date} dateTaken The date and time when the survey was taken.
 * @param {Array.Response} responses An array of Response objects.
*/
function Survey(type, participantId, dateTaken, responses) {
  this.type = type;
  this.participantId = participantId;
  this.dateTaken = dateTaken;
  this.responses = responses;
}

/**
 * Sends a survey to the CESP backend via XHR.
 * @param {Survey} survey The completed survey to send to the backend.
 * @param {function(string)} successCallback A function to call on receiving a
 *     successful response (HTTP 204). It should look like
 *     "function(response) {...};" where "response" is the text of the response
 *     (if there is any).
 * @param {function(!number=)} errorCallback A function to call on receiving an
 *     error from the server, or on timing out. It should look like
 *     "function(status) {...};" where "status" is an HTTP status code integer,
 *     if there is one. For a timeout, there is no status.
 */
function sendSurvey(survey, successCallback, errorCallback) {
  var url = cesp.SERVER_URL + cesp.SUBMIT_SURVEY_ACTION;
  var method = "POST";
  var dateTaken = survey.dateTaken.toISOString();
  // Get rid of timezone "Z" on end of ISO String for AppEngine compatibility.
  if (dateTaken.slice(-1) === "Z") {
    dateTaken = dateTaken.slice(0, -1);
  }
  var data = {
    "date_taken": dateTaken,
    "participant_id": survey.participantId,
    "responses": [],
    "survey_type": survey.type
  };
  for (var i = 0; i < survey.responses.length; i++) {
    data.responses.push(survey.responses[i]);
  }
  var xhr = new XMLHttpRequest();
  function onLoadHandler(event) {
    if (xhr.readyState === 4) {
      if (xhr.status === 204) {
        successCallback(xhr.response);
      } else {
        errorCallback(xhr.status);
      }
    }
  }
  function onErrorHandler(event) {
    errorCallback(xhr.status);
  }
  function onTimeoutHandler(event) {
    errorCallback();
  }
  xhr.open(method, url, true);
  xhr.setRequestHeader('Content-Type', 'application/JSON');
  xhr.timeout = cesp.XHR_TIMEOUT;
  xhr.onload = onLoadHandler;
  xhr.onerror = onErrorHandler;
  xhr.ontimeout = onTimeoutHandler;
  xhr.send(JSON.stringify(data));
}
<commit_msg>Add missing MAX_SURVEYS_PER_DAY constant<commit_after>/**
 * Experience Sampling event page.
 *
 * This background page handles the various events for registering participants
 * and showing new surveys in response to API events.
 *
 * Participants must fill out both a consent form and a startup survey (with
 * demographics) before they can begin to answer real survey questions.
 */

var cesp = {};  // namespace variable

cesp.readyForSurveys = false;
cesp.operatingSystem = "";

// Settings.
cesp.SERVER_URL = 'https://chrome-experience-sampling.appspot.com';
cesp.SUBMIT_SURVEY_ACTION = '/_ah/api/cesp/v1/submitsurvey';
cesp.XHR_TIMEOUT = 4000;
cesp.NOTIFICATION_TITLE = 'New Chrome survey available!';
cesp.NOTIFICATION_BODY = 'Your feedback makes Chrome better.';
cesp.NOTIFICATION_BUTTON = 'Take survey!';
cesp.MAX_SURVEYS_PER_DAY = 10;  // TODO: Is this a sane number per day?
cesp.ICON_FILE = 'icon.png';
cesp.NOTIFICATION_DEFAULT_TIMEOUT = 10;  // minutes
cesp.NOTIFICATION_TAG = 'chromeSurvey';
cesp.ALARM_NAME = 'notificationTimeout';

// SETUP

/**
 * Sets up basic state for the extension. Called when extension is installed.
 */
function setupState() {
  chrome.storage.local.set({'pending_responses': []});
  chrome.runtime.getPlatformInfo(function(platformInfo) {
    cesp.operatingSystem = platformInfo.os;
  });
  // Set the count of surveys shown to 0, and reset it each day.
  chrome.storage.local.set({cesp.SURVEYS_SHOWN_TODAY: 0});
  chrome.alarms.create(cesp.SURVEY_THROTTLE_RESET_ALARM,
      {delayInMinutes: 5, periodInMinutes: 1440});
}

/**
 * Retrieves the registration status from Local Storage.
 */
function getConsentStatus() {
  chrome.storage.local.get(constants.CONSENT_KEY, maybeShowConsentForm);
}

/**
 * Checks whether consent has been granted yet; if not, opens the consent form.
 * @param {object} consentLookup Object containing consent status (or empty).
 */
function maybeShowConsentForm(consentLookup) {
  if (!consentLookup || consentLookup[constants.CONSENT_KEY] == null ||
      consentLookup[constants.CONSENT_KEY] == constants.CONSENT_PENDING) {
    chrome.storage.onChanged.addListener(storageUpdated);
    chrome.tabs.create({'url': chrome.extension.getURL('consent.html')});
  } else if (consentLookup[constants.CONSENT_KEY] ==
             constants.CONSENT_REJECTED) {
    chrome.management.uninstallSelf();
  } else if (consentLookup[constants.CONSENT_KEY] ==
             constants.CONSENT_GRANTED) {
    // Someone might have filled out the consent form previously but not
    // filled out the setup survey. Check to see if that's the case.
    chrome.storage.local.get(constants.SETUP_KEY, maybeShowSetupSurvey);
  }
}

/**
 * Checks whether the setup survey has been completed yet. If it has been, we
 * are now ready to start showing surveys. If not, we need to listen for
 * when it's completed.
 * @param {object} setupLookup Object containing setup survey status (or empty).
 */
function maybeShowSetupSurvey(setupLookup) {
  if (!setupLookup || setupLookup[constants.SETUP_KEY] == null ||
      setupLookup[constants.SETUP_KEY] == constants.SETUP_PENDING) {
    chrome.tabs.create({'url': chrome.extension.getURL('surveys/setup.html')});
  } else if (setupLookup[constants.SETUP_KEY] == constants.SETUP_COMPLETED) {
    cesp.readyForSurveys = true;
  }
}

/**
 * Listens for the setup survey submission. When that happens, signals that
 * the experience sampling is now ready to begin.
 * @param {object} changes The changed portions of the database.
 * @param {string} areaName The name of the storage area.
 */
function storageUpdated(changes, areaName) {
  if (changes && changes[constants.SETUP_KEY] &&
      changes[constants.SETUP_KEY].newValue == constants.SETUP_COMPLETED) {
    cesp.readyForSurveys = true;
  }
}

// Performs consent and registration checks on startup and install.
chrome.runtime.onInstalled.addListener(getConsentStatus);
chrome.runtime.onStartup.addListener(getConsentStatus);
chrome.runtime.onInstalled.addListener(setupState);

// SURVEY HANDLING

/**
 * Clears our existing notification(s).
 */
function clearNotifications(unused) {
  chrome.notifications.clear(cesp.NOTIFICATION_TAG, function(unused) {});
  chrome.alarms.clearAll();
}

/**
 * Creates a new notification to prompt the participant to take an experience
 * sampling survey.
 * @param {object} element The browser element of interest.
 * @param {object} decision The decision the participant made.
 */
function showSurveyNotification(element, decision) {
  if (!cesp.readyForSurveys) return;

  chrome.storage.local.get(cesp.SURVEYS_SHOWN_TODAY, function(items) {
    if (items[cesp.SURVEYS_SHOWN_TODAY] >= cesp.MAX_SURVEYS_PER_DAY) {
      return;
    }

    clearNotifications();

    var timePromptShown = new Date();
    var clickHandler = function(unused) {
      var timePromptClicked = new Date();
      loadSurvey(element, decision, timePromptShown, timePromptClicked);
      clearNotifications();
    };

    var opt = {
      type: 'basic',
      iconUrl: cesp.ICON_FILE,
      title: cesp.NOTIFICATION_TITLE,
      message: cesp.NOTIFICATION_BODY,
      eventTime: Date.now(),
      buttons: [{title: cesp.NOTIFICATION_BUTTON}]
    };
    chrome.notifications.create(
        cesp.NOTIFICATION_TAG,
        opt,
        function(id) {
          chrome.alarms.create(
              cesp.ALARM_NAME,
              {delayInMinutes: cesp.NOTIFICATION_DEFAULT_TIMEOUT});
        });
    chrome.notifications.onClicked.addListener(clickHandler);
    chrome.notifications.onButtonClicked.addListener(clickHandler);

    chrome.storage.local.set({
      cesp.SURVEYS_SHOWN_TODAY: items[cesp.SURVEYS_SHOWN_TODAY] + 1
    });
  }
}

/**
 * Creates a new tab with the experience sampling survey page.
 * @param {object} element The browser element of interest.
 * @param {object} decision The decision the participant made.
 * @param {object} timePromptShown Date object of when the survey prompt
 *     notification was shown to the participant.
 * @param {object} timePromptClicked Date object of when the participant
 *     clicked the survey prompt notification.
 */
function loadSurvey(element, decision, timePromptShown, timePromptClicked) {
  if (!cesp.readyForSurveys) return;

  var surveyLocations = {
    SSL: 'ssl.html',
    EXAMPLE: 'survey-example.html'
  };
  var surveyURL;
  var eventType = constants.FindEventType(element['name']);
  switch (eventType) {
    case constants.EventType.SSL:
      surveyURL = surveyLocations.SSL;
      break;
    case constants.EventType.UNKNOWN:
      surveyURL = surveyLocations.EXAMPLE;
      console.log('Unknown event type: ' + element['name']);
      break;
  }
  chrome.tabs.create(
      {'url': chrome.extension.getURL('surveys/' + surveyURL)},
      function() { console.log('Opened survey.'); });
}

// Trigger the new survey prompt when the participant makes a decision about an
// experience sampling element.
chrome.experienceSamplingPrivate.onDecision.addListener(showSurveyNotification);
// Clear the notification state when the survey times out.
chrome.alarms.onAlarm.addListener(clearNotifications);

/**
 * A survey response (question and answer).
 * @constructor
 * @param {string} question The question being answered.
 * @param {string} answer The answer to that question.
 */
function Response(question, answer) {
  this.question = question;
  this.answer = answer;
}

/**
 * A completed survey.
 * @constructor
 * @param {string} type The type of survey.
 * @param {int} participantId The participant ID.
 * @param {Date} dateTaken The date and time when the survey was taken.
 * @param {Array.Response} responses An array of Response objects.
*/
function Survey(type, participantId, dateTaken, responses) {
  this.type = type;
  this.participantId = participantId;
  this.dateTaken = dateTaken;
  this.responses = responses;
}

/**
 * Sends a survey to the CESP backend via XHR.
 * @param {Survey} survey The completed survey to send to the backend.
 * @param {function(string)} successCallback A function to call on receiving a
 *     successful response (HTTP 204). It should look like
 *     "function(response) {...};" where "response" is the text of the response
 *     (if there is any).
 * @param {function(!number=)} errorCallback A function to call on receiving an
 *     error from the server, or on timing out. It should look like
 *     "function(status) {...};" where "status" is an HTTP status code integer,
 *     if there is one. For a timeout, there is no status.
 */
function sendSurvey(survey, successCallback, errorCallback) {
  var url = cesp.SERVER_URL + cesp.SUBMIT_SURVEY_ACTION;
  var method = "POST";
  var dateTaken = survey.dateTaken.toISOString();
  // Get rid of timezone "Z" on end of ISO String for AppEngine compatibility.
  if (dateTaken.slice(-1) === "Z") {
    dateTaken = dateTaken.slice(0, -1);
  }
  var data = {
    "date_taken": dateTaken,
    "participant_id": survey.participantId,
    "responses": [],
    "survey_type": survey.type
  };
  for (var i = 0; i < survey.responses.length; i++) {
    data.responses.push(survey.responses[i]);
  }
  var xhr = new XMLHttpRequest();
  function onLoadHandler(event) {
    if (xhr.readyState === 4) {
      if (xhr.status === 204) {
        successCallback(xhr.response);
      } else {
        errorCallback(xhr.status);
      }
    }
  }
  function onErrorHandler(event) {
    errorCallback(xhr.status);
  }
  function onTimeoutHandler(event) {
    errorCallback();
  }
  xhr.open(method, url, true);
  xhr.setRequestHeader('Content-Type', 'application/JSON');
  xhr.timeout = cesp.XHR_TIMEOUT;
  xhr.onload = onLoadHandler;
  xhr.onerror = onErrorHandler;
  xhr.ontimeout = onTimeoutHandler;
  xhr.send(JSON.stringify(data));
}
<|endoftext|>


<commit_before>// Copyright 2019 Google LLC
//
// 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.

/**
 * @fileoverview Background script to handle retrieving information from active
 * tab for reporting and alerting purposes.
 */

goog.module('suspiciousSiteReporter.Background');

const ClientRequest = goog.require('proto.suspiciousSiteReporter.extension.ChromeExtensionClientRequest');
const ReferrerChainEntry = goog.require('proto.suspiciousSiteReporter.extension.ReferrerChainEntry');
const alerts = goog.require('suspiciousSiteReporter.alerts');

/**
 * The API URL used for submitting reports.
 * @define {string} This may be redefined at build time to point at the
 *     prod endpoint.
 */
const API_URL = goog.define('API_URL', 'https://test-safebrowsing.google.com');

/**
 * Converts a data URL to a Uint8Array representation of the screenshot.
 * @param {string} dataUrl URL representation of an image with the following
 *     format: data:[<mediatype>][;base64],<data>
 * @return {!Uint8Array} A Uin8Array representation of the image.
 */
const getIntArrayFromDataUrl = dataUrl => {
  // Separate out the mime component, which comes after the data: prefix and
  // before ;base64. e.g., image/png for screenshot data.
  const mimeString =
      dataUrl.split(',')[0].split(':')[1].split(';')[0];
  // Since the data URL is retrieved from chrome.tabs.captureVisibleTab with
  // the format set to png, verify that the mimeString matches.
  if (mimeString !== 'image/png') {
    throw Error('Screenshot is in incorrect format');
  }
  // Convert base 64 to raw binary data held in a string. The data segment
  // comes after the comma in a data URL.
  const byteString = atob(dataUrl.split(',')[1]);
  // Write the bytes of the string to an ArrayBuffer.
  const intArray =
      new Uint8Array(new ArrayBuffer(byteString.length));
  byteString.split('').forEach((c, i) => {
    intArray[i] = c.charCodeAt(0);
  });
  return intArray;
};

/**
 * Converts a referrer from the extension API into a proto-compatible version.
 * @param {!chrome.safeBrowsingPrivate.ReferrerChainEntry} entry Referrer chain
 *     entry object retrieved from the extension API.
 * @return {!ReferrerChainEntry} A parsed ReferrerChainEntry.
 */
const parseReferrerEntry = entry => {
  const parsedEntry = new ReferrerChainEntry()
                          .setUrl(entry.url)
                          .setMainFrameUrl(entry.mainFrameUrl)
                          .setType(ReferrerChainEntry.URLType[entry.urlType]);
  if (entry.ipAddresses) {
    entry.ipAddresses.forEach((ipAddress) => {
      parsedEntry.addIpAddresses(ipAddress);
    });
  }
  parsedEntry.setReferrerUrl(entry.referrerUrl);
  parsedEntry.setReferrerMainFrameUrl(entry.referrerMainFrameUrl);
  parsedEntry.setIsRetargeting(entry.isRetargeting);
  parsedEntry.setNavigationTimeMsec(entry.navigationTimeMs);
  if (entry.serverRedirectChain) {
    entry.serverRedirectChain.forEach((serverRedirect) => {
      const redirect =
          new ReferrerChainEntry.ServerRedirect().setUrl(serverRedirect.url);
      parsedEntry.addServerRedirectChain(redirect);
    });
  }
  parsedEntry.setNavigationInitiation(
      ReferrerChainEntry.NavigationInitiation[entry.navigationInitiation]);
  parsedEntry.setMaybeLaunchedByExternalApplication(
      entry.maybeLaunchedByExternalApp);
  return parsedEntry;
};

class Background {
  constructor() {
    /** {?Array<string>} A list of suspicious signals on a site.  */
    this.alerts = undefined;
  }

  /**
   * Fetches number of alerts and sets icon badge.
   * @param {!Tab} tab A Chrome Tab instance.
   * @private
   */
  async getAlertBadge_(tab) {
    const alertList = await alerts.computeAlerts(tab.url);
    this.alerts = alertList;

    /**
     * Sets the color of the flag icon.
     * @param {!string} color The color to set the flag. Will be one of orange,
     *     green, or gray.
     * @param {!Tab} tab A Chrome Tab instance.
     */
    const setFlagIconColor = (color, tab) => {
      chrome.browserAction.setIcon({
        path: {
          '16': `images/${color}flag16.png`,
          '48': `images/${color}flag48.png`,
          '128': `images/${color}flag128.png`,
        },
        tabId: tab.id,
      });
    };

    if (alertList.length === 0) {
      setFlagIconColor('green', tab);
      return;
    }
    // To reduce noise, show a gray flag if the only signal is the not top
    // site signal. This works because the top site signal is most useful when
    // a website is also flagged for other reasons.
    if (alertList.length === 1 &&
        alertList.includes(alerts.ALERT_MESSAGES['notTopSite'])) {
      console.log('gray');
      setFlagIconColor('gray', tab);
    } else {
      chrome.browserAction.setBadgeText({
        text: alertList.length.toString(),
        tabId: tab.id,
      });
      setFlagIconColor('orange', tab);
    }
  }

  /**
   * Submits the report to Safe Browsing.
   * @param {?ClientRequest} data The data to send.
   * @param {string} domain The target domain for submitting the report request.
   * @private
   */
  submitReport_(data, domain) {
    const path = '/safebrowsing/clientreport/crx-report';
    const xhr = new XMLHttpRequest();
    xhr.open('POST', domain.concat(path), true);
    xhr.send(data.serialize());
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          chrome.runtime.sendMessage({'success': true});
        } else {
          chrome.runtime.sendMessage({'error': true});
        }
      }
    };
  }

  /**
   * Listens for a report message from the popup.
   * @param {!Object<string, *>} request Request object.
   * @param {!MessageSender} sender Message sender information.
   * @param {!Function} sendResponse function.
   * @private
   */
  onMessageReceived_(request, sender, sendResponse) {
    if (request['report']) {
      let report = new ClientRequest().setUrl(String(request['url']));
      if (request['screenshotUrl']) {
        report.setScreenshot(
            getIntArrayFromDataUrl(String(request['screenshotUrl'])));
      }
      if (request['domContent']) {
        report.setDom(String(request['domContent']));
      }
      const referrerChain =
          /** @type {!Array<!chrome.safeBrowsingPrivate.ReferrerChainEntry>} */
          (request['referrer']);
      if (referrerChain) {
        referrerChain.forEach((entry) => {
          report.addReferrerChain(parseReferrerEntry(entry));
        });
      }
      this.submitReport_(report, API_URL);
    }
  }

  /**
   * Initializes the extension background page.
   */
  init() {
    chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
      const requestReceived = /** @type {!Object<string, *>} */ (request);
      this.onMessageReceived_(requestReceived, sender, sendResponse);
    });

    chrome.extension.onConnect.addListener((port) => {
      port.onMessage.addListener((message) => {
        port.postMessage({siteInfo: this.alerts});
      });
    });

    alerts.setTopSitesList();

    /**
     * Sets the browser action and icon for a tab.
     * @param {!Tab} tab A Chrome Tab instance.
     */
    const setBrowserActionAndIcon = (tab) => {
      if (!tab.url.startsWith('http')) {
        chrome.browserAction.disable(tab.id);
      } else {
        chrome.browserAction.enable(tab.id);
        this.getAlertBadge_(tab);
      }
    };
    chrome.tabs.query({}, (tabs) => {
      tabs.forEach((tab) => {
        setBrowserActionAndIcon(tab);
      });
    });
    chrome.tabs.onActivated.addListener((activeTabInfo) => {
      chrome.tabs.get(activeTabInfo.tabId, (tab) => {
        setBrowserActionAndIcon(tab);
      });
    });
    chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
      setBrowserActionAndIcon(tab);
    });
  }
}

exports = Background;
<commit_msg>Update background.js<commit_after>// Copyright 2019 Google LLC
//
// 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.

/**
 * @fileoverview Background script to handle retrieving information from active
 * tab for reporting and alerting purposes.
 */

goog.module('suspiciousSiteReporter.Background');

const ClientRequest = goog.require('proto.suspiciousSiteReporter.extension.ChromeExtensionClientRequest');
const ReferrerChainEntry = goog.require('proto.suspiciousSiteReporter.extension.ReferrerChainEntry');
const alerts = goog.require('suspiciousSiteReporter.alerts');

/**
 * The API URL used for submitting reports.
 * @define {string} This may be redefined at build time to point at the
 *     prod endpoint.
 */
const API_URL = goog.define('API_URL', 'https://test-safebrowsing.google.com');

/**
 * Converts a data URL to a Uint8Array representation of the screenshot.
 * @param {string} dataUrl URL representation of an image with the following
 *     format: data:[<mediatype>][;base64],<data>
 * @return {!Uint8Array} A Uin8Array representation of the image.
 */
const getIntArrayFromDataUrl = dataUrl => {
  // Separate out the mime component, which comes after the data: prefix and
  // before ;base64. e.g., image/png for screenshot data.
  const mimeString =
      dataUrl.split(',')[0].split(':')[1].split(';')[0];
  // Since the data URL is retrieved from chrome.tabs.captureVisibleTab with
  // the format set to png, verify that the mimeString matches.
  if (mimeString !== 'image/png') {
    throw Error('Screenshot is in incorrect format');
  }
  // Convert base 64 to raw binary data held in a string. The data segment
  // comes after the comma in a data URL.
  const byteString = atob(dataUrl.split(',')[1]);
  // Write the bytes of the string to an ArrayBuffer.
  const intArray =
      new Uint8Array(new ArrayBuffer(byteString.length));
  byteString.split('').forEach((c, i) => {
    intArray[i] = c.charCodeAt(0);
  });
  return intArray;
};

/**
 * Converts a referrer from the extension API into a proto-compatible version.
 * @param {!chrome.safeBrowsingPrivate.ReferrerChainEntry} entry Referrer chain
 *     entry object retrieved from the extension API.
 * @return {!ReferrerChainEntry} A parsed ReferrerChainEntry.
 */
const parseReferrerEntry = entry => {
  const parsedEntry = new ReferrerChainEntry()
                          .setUrl(entry.url)
                          .setMainFrameUrl(entry.mainFrameUrl)
                          .setType(ReferrerChainEntry.URLType[entry.urlType]);
  if (entry.ipAddresses) {
    entry.ipAddresses.forEach((ipAddress) => {
      parsedEntry.addIpAddresses(ipAddress);
    });
  }
  parsedEntry.setReferrerUrl(entry.referrerUrl);
  parsedEntry.setReferrerMainFrameUrl(entry.referrerMainFrameUrl);
  parsedEntry.setIsRetargeting(entry.isRetargeting);
  parsedEntry.setNavigationTimeMsec(entry.navigationTimeMs);
  if (entry.serverRedirectChain) {
    entry.serverRedirectChain.forEach((serverRedirect) => {
      const redirect =
          new ReferrerChainEntry.ServerRedirect().setUrl(serverRedirect.url);
      parsedEntry.addServerRedirectChain(redirect);
    });
  }
  parsedEntry.setNavigationInitiation(
      ReferrerChainEntry.NavigationInitiation[entry.navigationInitiation]);
  parsedEntry.setMaybeLaunchedByExternalApplication(
      entry.maybeLaunchedByExternalApp);
  return parsedEntry;
};

class Background {
  constructor() {
    /** {?Array<string>} A list of suspicious signals on a site.  */
    this.alerts = undefined;
  }

  /**
   * Fetches number of alerts and sets icon badge.
   * @param {!Tab} tab A Chrome Tab instance.
   * @private
   */
  async getAlertBadge_(tab) {
    const alertList = await alerts.computeAlerts(tab.url);
    this.alerts = alertList;

    /**
     * Sets the color of the flag icon.
     * @param {!string} color The color to set the flag. Will be one of orange,
     *     green, or gray.
     * @param {!Tab} tab A Chrome Tab instance.
     */
    const setFlagIconColor = (color, tab) => {
      chrome.browserAction.setIcon({
        path: {
          '16': `images/${color}flag16.png`,
          '48': `images/${color}flag48.png`,
          '128': `images/${color}flag128.png`,
        },
        tabId: tab.id,
      });
    };

    if (alertList.length === 0) {
      setFlagIconColor('green', tab);
      return;
    }
    // To reduce noise, show a gray flag if the only signal is the not top
    // site signal. This works because the top site signal is most useful when
    // a website is also flagged for other reasons.
    if (alertList.length === 1 &&
        alertList.includes(alerts.ALERT_MESSAGES['notTopSite'])) {
      setFlagIconColor('gray', tab);
    } else {
      chrome.browserAction.setBadgeText({
        text: alertList.length.toString(),
        tabId: tab.id,
      });
      setFlagIconColor('orange', tab);
    }
  }

  /**
   * Submits the report to Safe Browsing.
   * @param {?ClientRequest} data The data to send.
   * @param {string} domain The target domain for submitting the report request.
   * @private
   */
  submitReport_(data, domain) {
    const path = '/safebrowsing/clientreport/crx-report';
    const xhr = new XMLHttpRequest();
    xhr.open('POST', domain.concat(path), true);
    xhr.send(data.serialize());
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          chrome.runtime.sendMessage({'success': true});
        } else {
          chrome.runtime.sendMessage({'error': true});
        }
      }
    };
  }

  /**
   * Listens for a report message from the popup.
   * @param {!Object<string, *>} request Request object.
   * @param {!MessageSender} sender Message sender information.
   * @param {!Function} sendResponse function.
   * @private
   */
  onMessageReceived_(request, sender, sendResponse) {
    if (request['report']) {
      let report = new ClientRequest().setUrl(String(request['url']));
      if (request['screenshotUrl']) {
        report.setScreenshot(
            getIntArrayFromDataUrl(String(request['screenshotUrl'])));
      }
      if (request['domContent']) {
        report.setDom(String(request['domContent']));
      }
      const referrerChain =
          /** @type {!Array<!chrome.safeBrowsingPrivate.ReferrerChainEntry>} */
          (request['referrer']);
      if (referrerChain) {
        referrerChain.forEach((entry) => {
          report.addReferrerChain(parseReferrerEntry(entry));
        });
      }
      this.submitReport_(report, API_URL);
    }
  }

  /**
   * Initializes the extension background page.
   */
  init() {
    chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
      const requestReceived = /** @type {!Object<string, *>} */ (request);
      this.onMessageReceived_(requestReceived, sender, sendResponse);
    });

    chrome.extension.onConnect.addListener((port) => {
      port.onMessage.addListener((message) => {
        port.postMessage({siteInfo: this.alerts});
      });
    });

    alerts.setTopSitesList();

    /**
     * Sets the browser action and icon for a tab.
     * @param {!Tab} tab A Chrome Tab instance.
     */
    const setBrowserActionAndIcon = (tab) => {
      if (!tab.url.startsWith('http')) {
        chrome.browserAction.disable(tab.id);
      } else {
        chrome.browserAction.enable(tab.id);
        this.getAlertBadge_(tab);
      }
    };
    chrome.tabs.query({}, (tabs) => {
      tabs.forEach((tab) => {
        setBrowserActionAndIcon(tab);
      });
    });
    chrome.tabs.onActivated.addListener((activeTabInfo) => {
      chrome.tabs.get(activeTabInfo.tabId, (tab) => {
        setBrowserActionAndIcon(tab);
      });
    });
    chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
      setBrowserActionAndIcon(tab);
    });
  }
}

exports = Background;
<|endoftext|>


<commit_before>/**
 * Experience Sampling event page.
 *
 * This background page handles the various events for registering participants
 * and showing new surveys in response to API events.
 *
 * Participants must fill out both a consent form and a startup survey (with
 * demographics) before they can begin to answer real survey questions.
 */

var cesp = {};  // namespace variable

cesp.readyForSurveys = false;
cesp.operatingSystem = "";

// Settings.
cesp.SERVER_URL = 'https://chrome-experience-sampling.appspot.com';
cesp.SUBMIT_SURVEY_ACTION = '/_ah/api/cesp/v1/submitsurvey';
cesp.XHR_TIMEOUT = 4000;
cesp.NOTIFICATION_TITLE = 'New Chrome survey available!';
cesp.NOTIFICATION_BODY = 'Your feedback makes Chrome better.';
cesp.NOTIFICATION_BUTTON = 'Take survey!';
cesp.ICON_FILE = 'icon.png';
cesp.NOTIFICATION_DEFAULT_TIMEOUT = 10;  // minutes
cesp.NOTIFICATION_TAG = 'chromeSurvey';
cesp.ALARM_NAME = 'notificationTimeout';

// SETUP

/**
 * Sets up basic state for the extension. Called when extension is installed.
 */
function setupState() {
  chrome.storage.local.set({'pending_responses': []});
  chrome.runtime.getPlatformInfo(function(platformInfo) {
    cesp.operatingSystem = platformInfo.os;
  });
}

/**
 * Retrieves the registration status from Local Storage.
 */
function getConsentStatus() {
  chrome.storage.local.get(constants.CONSENT_KEY, maybeShowConsentForm);
}

/**
 * Checks whether consent has been granted yet; if not, opens the consent form.
 * @param {object} consentLookup Object containing consent status (or empty).
 */
function maybeShowConsentForm(consentLookup) {
  if (!consentLookup || consentLookup[constants.CONSENT_KEY] == null ||
      consentLookup[constants.CONSENT_KEY] == constants.CONSENT_PENDING) {
    chrome.storage.onChanged.addListener(storageUpdated);
    chrome.tabs.create({'url': chrome.extension.getURL('consent.html')});
  } else if (consentLookup[constants.CONSENT_KEY] ==
             constants.CONSENT_REJECTED) {
    chrome.management.uninstallSelf();
  } else if (consentLookup[constants.CONSENT_KEY] ==
             constants.CONSENT_GRANTED) {
    // Someone might have filled out the consent form previously but not
    // filled out the setup survey. Check to see if that's the case.
    chrome.storage.local.get(constants.SETUP_KEY, maybeShowSetupSurvey);
  }
}

/**
 * Checks whether the setup survey has been completed yet. If it has been, we
 * are now ready to start showing surveys. If not, we need to listen for
 * when it's completed.
 * @param {object} setupLookup Object containing setup survey status (or empty).
 */
function maybeShowSetupSurvey(setupLookup) {
  if (!setupLookup || setupLookup[constants.SETUP_KEY] == null ||
      setupLookup[constants.SETUP_KEY] == constants.SETUP_PENDING) {
    chrome.tabs.create({'url': chrome.extension.getURL('surveys/setup.html')});
  } else if (setupLookup[constants.SETUP_KEY] == constants.SETUP_COMPLETED) {
    cesp.readyForSurveys = true;
  }
}

/**
 * Listens for the setup survey submission. When that happens, signals that
 * the experience sampling is now ready to begin.
 * @param {object} changes The changed portions of the database.
 * @param {string} areaName The name of the storage area.
 */
function storageUpdated(changes, areaName) {
  if (changes && changes[constants.SETUP_KEY] &&
      changes[constants.SETUP_KEY].newValue == constants.SETUP_COMPLETED) {
    cesp.readyForSurveys = true;
  }
}

// Performs consent and registration checks on startup and install.
chrome.runtime.onInstalled.addListener(getConsentStatus);
chrome.runtime.onStartup.addListener(getConsentStatus);
chrome.runtime.onInstalled.addListener(setupState);

// SURVEY HANDLING

/**
 * Clears our existing notification(s).
 */
function clearNotifications(unused) {
  chrome.notifications.clear(cesp.NOTIFICATION_TAG, function(unused) {});
  chrome.alarms.clearAll();
}

/**
 * Creates a new notification to prompt the participant to take an experience
 * sampling survey.
 * @param {object} element The browser element of interest.
 * @param {object} decision The decision the participant made.
 */
function showSurveyNotification(element, decision) {
  if (!cesp.readyForSurveys) return;
  clearNotifications();

  var timePromptShown = new Date();
  var clickHandler = function(unused) {
    var timePromptClicked = new Date();
    loadSurvey(element, decision, timePromptShown, timePromptClicked);
    clearNotifications();
  };

  var opt = {
    type: 'basic',
    iconUrl: cesp.ICON_FILE,
    title: cesp.NOTIFICATION_TITLE,
    message: cesp.NOTIFICATION_BODY,
    eventTime: Date.now(),
    buttons: [{title: cesp.NOTIFICATION_BUTTON}]
  };
  chrome.notifications.create(
      cesp.NOTIFICATION_TAG,
      opt,
      function(id) {
        chrome.alarms.create(
            cesp.ALARM_NAME,
            {delayInMinutes: cesp.NOTIFICATION_DEFAULT_TIMEOUT});
      });
  chrome.notifications.onClicked.addListener(clickHandler);
  chrome.notifications.onButtonClicked.addListener(clickHandler);
}

/**
 * Creates a new tab with the experience sampling survey page.
 * @param {object} element The browser element of interest.
 * @param {object} decision The decision the participant made.
 * @param {object} timePromptShown Date object of when the survey prompt
 *     notification was shown to the participant.
 * @param {object} timePromptClicked Date object of when the participant
 *     clicked the survey prompt notification.
 */
function loadSurvey(element, decision, timePromptShown, timePromptClicked) {
  if (!cesp.readyForSurveys) return;

  var surveyLocations = {
    SSL: 'ssl.html',
    EXAMPLE: 'survey-example.html'
  };
  var surveyURL;
  var eventType = constants.FindEventType(element['name']);
  switch (eventType) {
    case constants.EventType.SSL:
      surveyURL = surveyLocations.SSL;
      break;
    case constants.EventType.UNKNOWN:
      surveyURL = surveyLocations.EXAMPLE;
      console.log('Unknown event type: ' + element['name']);
      break;
  }
  chrome.tabs.create(
      {'url': chrome.extension.getURL('surveys/' + surveyURL)},
      function() { console.log('Opened survey.'); });
}

// Trigger the new survey prompt when the participant makes a decision about an
// experience sampling element.
chrome.experienceSamplingPrivate.onDecision.addListener(showSurveyNotification);
// Clear the notification state when the survey times out.
chrome.alarms.onAlarm.addListener(clearNotifications);

/**
 * A survey response (question and answer).
 * @constructor
 * @param {string} question The question being answered.
 * @param {string} answer The answer to that question.
 */
function Response(question, answer) {
  this.question = question;
  this.answer = answer;
}

/**
 * A completed survey.
 * @constructor
 * @param {string} type The type of survey.
 * @param {int} participantId The participant ID.
 * @param {Date} dateTaken The date and time when the survey was taken.
 * @param {Array.Response} responses An array of Response objects.
*/
function Survey(type, participantId, dateTaken, responses) {
  this.type = type;
  this.participantId = participantId;
  this.dateTaken = dateTaken;
  this.responses = responses;
}

/**
 * Sends a survey to the CESP backend via XHR.
 * @param {Survey} survey The completed survey to send to the backend.
 * @param {function(string)} successCallback A function to call on receiving a
 *     successful response (HTTP 204). It should look like
 *     "function(response) {...};" where "response" is the text of the response
 *     (if there is any).
 * @param {function(!number=)} errorCallback A function to call on receiving an
 *     error from the server, or on timing out. It should look like
 *     "function(status) {...};" where "status" is an HTTP status code integer,
 *     if there is one. For a timeout, there is no status.
 */
function sendSurvey(survey, successCallback, errorCallback) {
  var url = cesp.SERVER_URL + cesp.SUBMIT_SURVEY_ACTION;
  var method = "POST";
  var dateTaken = survey.dateTaken.toISOString();
  // Get rid of timezone "Z" on end of ISO String for AppEngine compatibility.
  if (dateTaken.slice(-1) === "Z") {
    dateTaken = dateTaken.slice(0, -1);
  }
  var data = {
    "date_taken": dateTaken,
    "participant_id": survey.participantId,
    "responses": [],
    "survey_type": survey.type
  };
  for (var i = 0; i < survey.responses.length; i++) {
    data.responses.push(survey.responses[i]);
  }
  var xhr = new XMLHttpRequest();
  function onLoadHandler(event) {
    if (xhr.readyState === 4) {
      if (xhr.status === 204) {
        successCallback(xhr.response);
      } else {
        errorCallback(xhr.status);
      }
    }
  }
  function onErrorHandler(event) {
    errorCallback(xhr.status);
  }
  function onTimeoutHandler(event) {
    errorCallback();
  }
  xhr.open(method, url, true);
  xhr.setRequestHeader('Content-Type', 'application/JSON');
  xhr.timeout = cesp.XHR_TIMEOUT;
  xhr.onload = onLoadHandler;
  xhr.onerror = onErrorHandler;
  xhr.ontimeout = onTimeoutHandler;
  xhr.send(JSON.stringify(data));
}
<commit_msg>Change setupState function to take a details object, and then only do the initial setup if it's an actual installation event.<commit_after>/**
 * Experience Sampling event page.
 *
 * This background page handles the various events for registering participants
 * and showing new surveys in response to API events.
 *
 * Participants must fill out both a consent form and a startup survey (with
 * demographics) before they can begin to answer real survey questions.
 */

var cesp = {};  // namespace variable

cesp.readyForSurveys = false;
cesp.operatingSystem = "";

// Settings.
cesp.SERVER_URL = 'https://chrome-experience-sampling.appspot.com';
cesp.SUBMIT_SURVEY_ACTION = '/_ah/api/cesp/v1/submitsurvey';
cesp.XHR_TIMEOUT = 4000;
cesp.NOTIFICATION_TITLE = 'New Chrome survey available!';
cesp.NOTIFICATION_BODY = 'Your feedback makes Chrome better.';
cesp.NOTIFICATION_BUTTON = 'Take survey!';
cesp.ICON_FILE = 'icon.png';
cesp.NOTIFICATION_DEFAULT_TIMEOUT = 10;  // minutes
cesp.NOTIFICATION_TAG = 'chromeSurvey';
cesp.ALARM_NAME = 'notificationTimeout';

// SETUP

/**
 * Sets up basic state for the extension. Called when extension is installed.
 */
function setupState(details) {
  if (details.reason === 'install') {
    chrome.storage.local.set({'pending_responses': []});
    chrome.runtime.getPlatformInfo(function(platformInfo) {
      cesp.operatingSystem = platformInfo.os;
    });
  }
}

/**
 * Retrieves the registration status from Local Storage.
 */
function getConsentStatus() {
  chrome.storage.local.get(constants.CONSENT_KEY, maybeShowConsentForm);
}

/**
 * Checks whether consent has been granted yet; if not, opens the consent form.
 * @param {object} consentLookup Object containing consent status (or empty).
 */
function maybeShowConsentForm(consentLookup) {
  if (!consentLookup || consentLookup[constants.CONSENT_KEY] == null ||
      consentLookup[constants.CONSENT_KEY] == constants.CONSENT_PENDING) {
    chrome.storage.onChanged.addListener(storageUpdated);
    chrome.tabs.create({'url': chrome.extension.getURL('consent.html')});
  } else if (consentLookup[constants.CONSENT_KEY] ==
             constants.CONSENT_REJECTED) {
    chrome.management.uninstallSelf();
  } else if (consentLookup[constants.CONSENT_KEY] ==
             constants.CONSENT_GRANTED) {
    // Someone might have filled out the consent form previously but not
    // filled out the setup survey. Check to see if that's the case.
    chrome.storage.local.get(constants.SETUP_KEY, maybeShowSetupSurvey);
  }
}

/**
 * Checks whether the setup survey has been completed yet. If it has been, we
 * are now ready to start showing surveys. If not, we need to listen for
 * when it's completed.
 * @param {object} setupLookup Object containing setup survey status (or empty).
 */
function maybeShowSetupSurvey(setupLookup) {
  if (!setupLookup || setupLookup[constants.SETUP_KEY] == null ||
      setupLookup[constants.SETUP_KEY] == constants.SETUP_PENDING) {
    chrome.tabs.create({'url': chrome.extension.getURL('surveys/setup.html')});
  } else if (setupLookup[constants.SETUP_KEY] == constants.SETUP_COMPLETED) {
    cesp.readyForSurveys = true;
  }
}

/**
 * Listens for the setup survey submission. When that happens, signals that
 * the experience sampling is now ready to begin.
 * @param {object} changes The changed portions of the database.
 * @param {string} areaName The name of the storage area.
 */
function storageUpdated(changes, areaName) {
  if (changes && changes[constants.SETUP_KEY] &&
      changes[constants.SETUP_KEY].newValue == constants.SETUP_COMPLETED) {
    cesp.readyForSurveys = true;
  }
}

// Performs consent and registration checks on startup and install.
chrome.runtime.onInstalled.addListener(getConsentStatus);
chrome.runtime.onStartup.addListener(getConsentStatus);
chrome.runtime.onInstalled.addListener(setupState);

// SURVEY HANDLING

/**
 * Clears our existing notification(s).
 */
function clearNotifications(unused) {
  chrome.notifications.clear(cesp.NOTIFICATION_TAG, function(unused) {});
  chrome.alarms.clearAll();
}

/**
 * Creates a new notification to prompt the participant to take an experience
 * sampling survey.
 * @param {object} element The browser element of interest.
 * @param {object} decision The decision the participant made.
 */
function showSurveyNotification(element, decision) {
  if (!cesp.readyForSurveys) return;
  clearNotifications();

  var timePromptShown = new Date();
  var clickHandler = function(unused) {
    var timePromptClicked = new Date();
    loadSurvey(element, decision, timePromptShown, timePromptClicked);
    clearNotifications();
  };

  var opt = {
    type: 'basic',
    iconUrl: cesp.ICON_FILE,
    title: cesp.NOTIFICATION_TITLE,
    message: cesp.NOTIFICATION_BODY,
    eventTime: Date.now(),
    buttons: [{title: cesp.NOTIFICATION_BUTTON}]
  };
  chrome.notifications.create(
      cesp.NOTIFICATION_TAG,
      opt,
      function(id) {
        chrome.alarms.create(
            cesp.ALARM_NAME,
            {delayInMinutes: cesp.NOTIFICATION_DEFAULT_TIMEOUT});
      });
  chrome.notifications.onClicked.addListener(clickHandler);
  chrome.notifications.onButtonClicked.addListener(clickHandler);
}

/**
 * Creates a new tab with the experience sampling survey page.
 * @param {object} element The browser element of interest.
 * @param {object} decision The decision the participant made.
 * @param {object} timePromptShown Date object of when the survey prompt
 *     notification was shown to the participant.
 * @param {object} timePromptClicked Date object of when the participant
 *     clicked the survey prompt notification.
 */
function loadSurvey(element, decision, timePromptShown, timePromptClicked) {
  if (!cesp.readyForSurveys) return;

  var surveyLocations = {
    SSL: 'ssl.html',
    EXAMPLE: 'survey-example.html'
  };
  var surveyURL;
  var eventType = constants.FindEventType(element['name']);
  switch (eventType) {
    case constants.EventType.SSL:
      surveyURL = surveyLocations.SSL;
      break;
    case constants.EventType.UNKNOWN:
      surveyURL = surveyLocations.EXAMPLE;
      console.log('Unknown event type: ' + element['name']);
      break;
  }
  chrome.tabs.create(
      {'url': chrome.extension.getURL('surveys/' + surveyURL)},
      function() { console.log('Opened survey.'); });
}

// Trigger the new survey prompt when the participant makes a decision about an
// experience sampling element.
chrome.experienceSamplingPrivate.onDecision.addListener(showSurveyNotification);
// Clear the notification state when the survey times out.
chrome.alarms.onAlarm.addListener(clearNotifications);

/**
 * A survey response (question and answer).
 * @constructor
 * @param {string} question The question being answered.
 * @param {string} answer The answer to that question.
 */
function Response(question, answer) {
  this.question = question;
  this.answer = answer;
}

/**
 * A completed survey.
 * @constructor
 * @param {string} type The type of survey.
 * @param {int} participantId The participant ID.
 * @param {Date} dateTaken The date and time when the survey was taken.
 * @param {Array.Response} responses An array of Response objects.
*/
function Survey(type, participantId, dateTaken, responses) {
  this.type = type;
  this.participantId = participantId;
  this.dateTaken = dateTaken;
  this.responses = responses;
}

/**
 * Sends a survey to the CESP backend via XHR.
 * @param {Survey} survey The completed survey to send to the backend.
 * @param {function(string)} successCallback A function to call on receiving a
 *     successful response (HTTP 204). It should look like
 *     "function(response) {...};" where "response" is the text of the response
 *     (if there is any).
 * @param {function(!number=)} errorCallback A function to call on receiving an
 *     error from the server, or on timing out. It should look like
 *     "function(status) {...};" where "status" is an HTTP status code integer,
 *     if there is one. For a timeout, there is no status.
 */
function sendSurvey(survey, successCallback, errorCallback) {
  var url = cesp.SERVER_URL + cesp.SUBMIT_SURVEY_ACTION;
  var method = "POST";
  var dateTaken = survey.dateTaken.toISOString();
  // Get rid of timezone "Z" on end of ISO String for AppEngine compatibility.
  if (dateTaken.slice(-1) === "Z") {
    dateTaken = dateTaken.slice(0, -1);
  }
  var data = {
    "date_taken": dateTaken,
    "participant_id": survey.participantId,
    "responses": [],
    "survey_type": survey.type
  };
  for (var i = 0; i < survey.responses.length; i++) {
    data.responses.push(survey.responses[i]);
  }
  var xhr = new XMLHttpRequest();
  function onLoadHandler(event) {
    if (xhr.readyState === 4) {
      if (xhr.status === 204) {
        successCallback(xhr.response);
      } else {
        errorCallback(xhr.status);
      }
    }
  }
  function onErrorHandler(event) {
    errorCallback(xhr.status);
  }
  function onTimeoutHandler(event) {
    errorCallback();
  }
  xhr.open(method, url, true);
  xhr.setRequestHeader('Content-Type', 'application/JSON');
  xhr.timeout = cesp.XHR_TIMEOUT;
  xhr.onload = onLoadHandler;
  xhr.onerror = onErrorHandler;
  xhr.ontimeout = onTimeoutHandler;
  xhr.send(JSON.stringify(data));
}
<|endoftext|>


<commit_before><commit_msg>Build new dispatcher<commit_after><|endoftext|>


<commit_before><commit_msg>Added documentation to methods in draw example<commit_after><|endoftext|>


<commit_before>
import Entity from './entity';

const RG = require('./rg');
RG.Component = require('./component');
RG.Brain = require('./brain.virtual');
RG.Brain.Player = require('./brain.player');
RG.Inv = require('./inv');
RG.Spell = require('./spell');
RG.Time = require('./time');
const Mixin = require('./mixin');

RG.Actor = {};

const ACTOR_NO_ACTION = Object.freeze(() => {});
const EMPTY_ARGS = Object.freeze({});
const SPEED_COEFF = RG.BASE_SPEED * RG.ACTION_DUR;

class BaseActor extends Mixin.Locatable(Mixin.Typed(Entity)) {

    constructor(name) { // {{{2
        super({propType: RG.TYPE_ACTOR, type: null});
        this._name = name;
        this.add(new RG.Component.Action());
    }

    isPlayer() {return false;}
    isEnemy() {return false;}
    addEnemy() {/* No implementation here */}

    setName(name) {this._name = name;}
    getName() {return this._name;}

    getBrain() {return this._brain;}

    setBrain(brain) {
        this._brain = brain;
        this._brain.setActor(this);
    }

    getSpeed() {
        return RG.BASE_SPEED;
    }

    getEquipProtection() {return 0;}

    /* Returns the next action for this actor.*/
    nextAction(obj) {
        // Use actor brain to determine the action
        const cb = this._brain.decideNextAction(obj);
        let action = null;

        if (cb !== null) {
            const duration = Math.round(SPEED_COEFF / this.getSpeed());
            action = new RG.Time.Action(duration, cb, EMPTY_ARGS);
        }
        else {
            action = new RG.Time.Action(0, ACTOR_NO_ACTION, EMPTY_ARGS);
        }

        if (this._brain.hasOwnProperty('energy')) {
            action.energy = this._brain.energy;
        }
        action.actor = this;
        return action;
    }

    /* Serializes the virtual actor. */
    toJSON() {
        let levelID = null;
        if (this.getLevel()) {
            levelID = this.getLevel().getID();
        }
        const obj = {
            id: this.getID(),
            name: this.getName(),
            type: this.getType(),
            levelID,
            brain: this._brain.toJSON(),
            new: 'Base'
        };

        obj.components = RG.Component.compsToJSON(this);

        if (obj.type === null) {
            RG.err('Actor.Virtual', 'toJSON',
                `Type null for ${JSON.stringify(obj)}`);
        }

        return obj;
    }

}
RG.Actor.Base = BaseActor;

/* Virtual actor can be used to spawn more entities or for AI-like effects
 * inside a level. */
class VirtualActor extends BaseActor {

    constructor(name) { // {{{2
        super(name);
        this._brain = new RG.Brain.Virtual(this);
    }

}
RG.Actor.Virtual = VirtualActor;

/* Object representing a game actor who takes actions.  */
class RGActorRogue extends BaseActor {
    constructor(name) { // {{{2
        super(name);

        this._brain = new RG.Brain.Rogue(this);
        this._brain.getMemory().addEnemyType('player');

        this._invEq = new RG.Inv.Inventory(this);
        this._maxWeight = 10.0;

        // Components for this entity
        this.add(new RG.Component.Experience());
        this.add(new RG.Component.Combat());
        this.add(new RG.Component.Stats());
        this.add(new RG.Component.Health(50));
        this.add(new RG.Component.Corporeal());

        const perception = new RG.Component.Perception();
        perception.setFOVRange(RG.NPC_FOV_RANGE);
        this.add(new RG.Component.Perception());
    }

    /* Returns true if actor is a player.*/
    isPlayer() {
        return this.has('Player') || this.has('PlayerControlled');
    }

    getFOVRange() {
        let range = this.get('Perception').getFOVRange();
        if (this.has('EagleEye')) {range += 2;}
        return range;
    }

    setFOVRange(range) {
        this.get('Perception').setFOVRange(range);
    }

    //---------------------------------
    // Brain-related methods
    //---------------------------------

    addEnemy(actor) {this._brain.addEnemy(actor);}
    addFriend(actor) {this._brain.addFriend(actor);}

    isEnemy(actor) {
        return this._brain.getMemory().isEnemy(actor);
    }

    isFriend(actor) {
        return this._brain.getMemory().isFriend(actor);
    }

    //---------------------------------
    // Equipment related methods
    //---------------------------------

    getInvEq() { return this._invEq; }

    /* Returns weapon that is wielded by the actor.*/
    getWeapon() {return this._invEq.getWeapon();}

    /* Returns weapon that is wielded by the actor.*/
    getMissileWeapon() {
        return this._invEq.getMissileWeapon();
    }

    /* Returns missile equipped by the player.*/
    getMissile() {
        return this._invEq.getEquipment().getItem('missile');
    }

    getEquipAttack() {
        let att = this._invEq.getEquipment().getAttack();
        if (this.has('Skills')) {
            att += this.get('Skills').getLevel('Melee');
        }
        return att;
    }

    getEquipDefense() {
        let def = this._invEq.getEquipment().getDefense();
        if (this.has('Skills')) {
            def += this.get('Skills').getLevel('Shields');
        }
        return def;
    }

    getEquipProtection() {
        return this._invEq.getEquipment().getProtection();
    }

    getShieldDefense() {
        const shield = this._invEq.getEquipment().getEquipped('shield');
        let bonus = 0;
        if (shield) {
            bonus = shield.getDefense();
            if (this.has('Skills')) {
                bonus += this.get('Skills').getLevel('Shields');
            }
        }
        return bonus;
    }

    setActorClass(classObj) {
        this._actorClass = classObj;
    }

    getActorClass() {
        return this._actorClass;
    }

    setBook(book) {
        this._spellbook = book;
    }

    getBook() {
        return this._spellbook;
    }

    /* Returns carrying capacity of the actor.*/
    getMaxWeight() {
        const statStr = this.get('Stats').getStrength();
        const eqStr = this._invEq.getEquipment().getStrength();
        return 2 * statStr + 2 * eqStr + this._maxWeight;
    }

    /* Marks actor as player. Cannot unset player.*/
    setIsPlayer(isPlayer) {
        if (isPlayer) {
            this._brain = new RG.Brain.Player(this);
            if (!this.has('StatsMods')) {
                this.add(new RG.Component.StatsMods());
            }
            if (!this.has('CombatMods')) {
                this.add(new RG.Component.CombatMods());
            }
            this.add(new RG.Component.Player());
            this.add(new RG.Component.SpellPower());
        }
        else {
            RG.err('Actor.Rogue', 'setIsPlayer',
                'Actor cannot be changed from player to mob.');
        }
    }

    /* Used when controlling other actors with the "real player" actor .*/
    setPlayerCtrl(isPlayer) {
        if (isPlayer) {
            this.add(new RG.Component.PlayerControlled());
            this._actualBrain = this._brain;
            this._brain = new RG.Brain.Player(this);
            if (!this.has('StatsMods')) {
                this.add(new RG.Component.StatsMods());
            }
            if (!this.has('CombatMods')) {
                this.add(new RG.Component.CombatMods());
            }
            this.add(new RG.Component.Possessed());
        }
        else {
            this.remove('PlayerControlled');
            this.remove('StatsMods');
            this.remove('CombatMods');
            this.remove('Possessed');
            this._brain = this._actualBrain;
            delete this._actualBrain;
        }
    }

    /* Returns the cell where this actor is located at.*/
    getCell() {
        const x = this.getX();
        const y = this.getY();
        const level = this.getLevel();
        if (level) {
            return level.getMap().getCell(x, y);
        }
        return null;
    }

    isInLevel(level) {
        if (this.getLevel()) {
            return this.getLevel().getID() === level.getID();
        }
        return false;
    }

    toJSON() {
        let levelID = null;
        if (this.getLevel()) {
            levelID = this.getLevel().getID();
        }
        const obj = {
            id: this.getID(),
            name: this.getName(),
            type: this.getType(),
            x: this.getX(),
            y: this.getY(),
            fovRange: this.getFOVRange(),
            levelID,
            inventory: this.getInvEq().getInventory().toJSON(),
            equipment: this.getInvEq().getEquipment().toJSON(),
            brain: this._brain.toJSON(),
            new: 'Rogue'
        };

        obj.components = RG.Component.compsToJSON(this);

        if (obj.type === null) {
            RG.err('Actor.Rogue', 'toJSON',
                `Type null for ${JSON.stringify(obj)}`);
        }

        if (this._spellbook) {
            obj.spellbook = this._spellbook.toJSON();
        }
        if (this.has('Player')) {
            obj.isPlayer = true;
        }
        if (this._actualBrain) {
            obj.brain = this._actualBrain.toJSON();
        }

        return obj;
    }

    //---------------------------------
    // Combat-related methods
    //---------------------------------

    getAttack() {
        let attack = this.get('Combat').getAttack();
        attack += this.getEquipAttack();
        attack += this._addFromCompList('CombatMods', 'getAttack');
        attack += Math.floor(this.getAccuracy() / 2);
        return attack;
    }

    getDefense() {
        let defense = this.get('Combat').getDefense();
        defense += this.getEquipDefense();
        defense += this._addFromCompList('CombatMods', 'getDefense');
        defense += Math.floor(this.getAgility() / 2);
        return defense;
    }

    getProtection() {
        let protection = this.get('Combat').getProtection();
        protection += this.getEquipProtection();
        protection += this._addFromCompList('CombatMods', 'getProtection');
        return protection;
    }

    getDamage() {
        let damage = this.get('Combat').rollDamage();
        const weapon = this.getWeapon();
        if (weapon) {
            damage = RG.getItemDamage(weapon);
        }
        const strength = this.getStrength();
        damage += RG.strengthToDamage(strength);
        damage += this._addFromCompList('CombatMods', 'getDamage');
        return damage;

    }

    getCombatBonus(funcName) {
        return this._addFromCompList('CombatMods', funcName);
    }

    _addFromCompList(compType, func) {
        const compList = this.getList(compType);
        if (compList.length > 0) {
            return compList.reduce((acc, val) => {
                return acc + val[func]();
            }, 0);
        }
        return 0;
    }

    //-------------------------------------------------------------
    // Stats-related methods (these take eq and boosts into account
    //-------------------------------------------------------------

    getAccuracy() {
        let acc = this.get('Stats').getAccuracy();
        acc += this.getInvEq().getEquipment().getAccuracy();
        acc += this._addFromCompList('StatsMods', 'getAccuracy');
        return acc;
    }

    getAgility() {
        let agi = this.get('Stats').getAgility();
        agi += this.getInvEq().getEquipment().getAgility();
        agi += this._addFromCompList('StatsMods', 'getAgility');
        return agi;
    }

    getStrength() {
        let str = this.get('Stats').getStrength();
        str += this.getInvEq().getEquipment().getStrength();
        str += this._addFromCompList('StatsMods', 'getStrength');
        return str;
    }

    getWillpower() {
        let wil = this.get('Stats').getWillpower();
        wil += this.getInvEq().getEquipment().getWillpower();
        wil += this._addFromCompList('StatsMods', 'getWillpower');
        return wil;
    }

    getSpeed() {
        let speed = this.get('Stats').getSpeed();
        speed += this.getInvEq().getEquipment().getSpeed();
        speed += this._addFromCompList('StatsMods', 'getSpeed');
        return speed;
    }

    getPerception() {
        let per = this.get('Stats').getPerception();
        per += this.getInvEq().getEquipment().getPerception();
        per += this._addFromCompList('StatsMods', 'getPerception');
        return per;
    }

    getMagic() {
        let mag = this.get('Stats').getMagic();
        mag += this.getInvEq().getEquipment().getMagic();
        mag += this._addFromCompList('StatsMods', 'getMagic');
        return mag;
    }

    /* Returns bonuses applied to given stat. */
    getStatBonus(funcName) {
        return this._addFromCompList('StatsMods', funcName);
    }
}

RG.Actor.Rogue = RGActorRogue;

module.exports = RG.Actor;
<commit_msg>Moved isPlayer() to base class. Fixed adding of perception component.<commit_after>
import Entity from './entity';

const RG = require('./rg');
RG.Component = require('./component');
RG.Brain = require('./brain.virtual');
RG.Brain.Player = require('./brain.player');
RG.Inv = require('./inv');
RG.Spell = require('./spell');
RG.Time = require('./time');
const Mixin = require('./mixin');

RG.Actor = {};

const ACTOR_NO_ACTION = Object.freeze(() => {});
const EMPTY_ARGS = Object.freeze({});
const SPEED_COEFF = RG.BASE_SPEED * RG.ACTION_DUR;

class BaseActor extends Mixin.Locatable(Mixin.Typed(Entity)) {

    constructor(name) { // {{{2
        super({propType: RG.TYPE_ACTOR, type: null});
        this._name = name;
        this.add(new RG.Component.Action());
    }

    /* Returns true if actor is a player.*/
    isPlayer() {
        return this.has('Player') || this.has('PlayerControlled');
    }

    isEnemy() {return false;}
    addEnemy() {/* No implementation here */}

    setName(name) {this._name = name;}
    getName() {return this._name;}

    getBrain() {return this._brain;}

    setBrain(brain) {
        this._brain = brain;
        this._brain.setActor(this);
    }

    getSpeed() {
        return RG.BASE_SPEED;
    }

    getEquipProtection() {return 0;}

    /* Returns the next action for this actor.*/
    nextAction(obj) {
        // Use actor brain to determine the action
        const cb = this._brain.decideNextAction(obj);
        let action = null;

        if (cb !== null) {
            const duration = Math.round(SPEED_COEFF / this.getSpeed());
            action = new RG.Time.Action(duration, cb, EMPTY_ARGS);
        }
        else {
            action = new RG.Time.Action(0, ACTOR_NO_ACTION, EMPTY_ARGS);
        }

        if (this._brain.hasOwnProperty('energy')) {
            action.energy = this._brain.energy;
        }
        action.actor = this;
        return action;
    }

    /* Serializes the virtual actor. */
    toJSON() {
        let levelID = null;
        if (this.getLevel()) {
            levelID = this.getLevel().getID();
        }
        const obj = {
            id: this.getID(),
            name: this.getName(),
            type: this.getType(),
            levelID,
            brain: this._brain.toJSON(),
            new: 'Base'
        };

        obj.components = RG.Component.compsToJSON(this);

        if (obj.type === null) {
            RG.err('Actor.Virtual', 'toJSON',
                `Type null for ${JSON.stringify(obj)}`);
        }

        return obj;
    }

}
RG.Actor.Base = BaseActor;

/* Virtual actor can be used to spawn more entities or for AI-like effects
 * inside a level. */
class VirtualActor extends BaseActor {

    constructor(name) { // {{{2
        super(name);
        this._brain = new RG.Brain.Virtual(this);
    }

}
RG.Actor.Virtual = VirtualActor;

/* Object representing a game actor who takes actions.  */
class RGActorRogue extends BaseActor {
    constructor(name) { // {{{2
        super(name);

        this._brain = new RG.Brain.Rogue(this);
        this._brain.getMemory().addEnemyType('player');

        this._invEq = new RG.Inv.Inventory(this);
        this._maxWeight = 10.0;

        // Components for this entity
        this.add(new RG.Component.Experience());
        this.add(new RG.Component.Combat());
        this.add(new RG.Component.Stats());
        this.add(new RG.Component.Health(50));
        this.add(new RG.Component.Corporeal());

        const perception = new RG.Component.Perception();
        perception.setFOVRange(RG.NPC_FOV_RANGE);
        this.add(perception);
    }

    getFOVRange() {
        let range = this.get('Perception').getFOVRange();
        if (this.has('EagleEye')) {range += 2;}
        return range;
    }

    setFOVRange(range) {
        this.get('Perception').setFOVRange(range);
    }

    //---------------------------------
    // Brain-related methods
    //---------------------------------

    addEnemy(actor) {this._brain.addEnemy(actor);}
    addFriend(actor) {this._brain.addFriend(actor);}

    isEnemy(actor) {
        return this._brain.getMemory().isEnemy(actor);
    }

    isFriend(actor) {
        return this._brain.getMemory().isFriend(actor);
    }

    //---------------------------------
    // Equipment related methods
    //---------------------------------

    getInvEq() { return this._invEq; }

    /* Returns weapon that is wielded by the actor.*/
    getWeapon() {return this._invEq.getWeapon();}

    /* Returns weapon that is wielded by the actor.*/
    getMissileWeapon() {
        return this._invEq.getMissileWeapon();
    }

    /* Returns missile equipped by the player.*/
    getMissile() {
        return this._invEq.getEquipment().getItem('missile');
    }

    getEquipAttack() {
        let att = this._invEq.getEquipment().getAttack();
        if (this.has('Skills')) {
            att += this.get('Skills').getLevel('Melee');
        }
        return att;
    }

    getEquipDefense() {
        let def = this._invEq.getEquipment().getDefense();
        if (this.has('Skills')) {
            def += this.get('Skills').getLevel('Shields');
        }
        return def;
    }

    getEquipProtection() {
        return this._invEq.getEquipment().getProtection();
    }

    getShieldDefense() {
        const shield = this._invEq.getEquipment().getEquipped('shield');
        let bonus = 0;
        if (shield) {
            bonus = shield.getDefense();
            if (this.has('Skills')) {
                bonus += this.get('Skills').getLevel('Shields');
            }
        }
        return bonus;
    }

    setActorClass(classObj) {
        this._actorClass = classObj;
    }

    getActorClass() {
        return this._actorClass;
    }

    setBook(book) {
        this._spellbook = book;
    }

    getBook() {
        return this._spellbook;
    }

    /* Returns carrying capacity of the actor.*/
    getMaxWeight() {
        const statStr = this.get('Stats').getStrength();
        const eqStr = this._invEq.getEquipment().getStrength();
        return 2 * statStr + 2 * eqStr + this._maxWeight;
    }

    /* Marks actor as player. Cannot unset player.*/
    setIsPlayer(isPlayer) {
        if (isPlayer) {
            this._brain = new RG.Brain.Player(this);
            if (!this.has('StatsMods')) {
                this.add(new RG.Component.StatsMods());
            }
            if (!this.has('CombatMods')) {
                this.add(new RG.Component.CombatMods());
            }
            this.add(new RG.Component.Player());
            this.add(new RG.Component.SpellPower());
        }
        else {
            RG.err('Actor.Rogue', 'setIsPlayer',
                'Actor cannot be changed from player to mob.');
        }
    }

    /* Used when controlling other actors with the "real player" actor .*/
    setPlayerCtrl(isPlayer) {
        if (isPlayer) {
            this.add(new RG.Component.PlayerControlled());
            this._actualBrain = this._brain;
            this._brain = new RG.Brain.Player(this);
            if (!this.has('StatsMods')) {
                this.add(new RG.Component.StatsMods());
            }
            if (!this.has('CombatMods')) {
                this.add(new RG.Component.CombatMods());
            }
            this.add(new RG.Component.Possessed());
        }
        else {
            this.remove('PlayerControlled');
            this.remove('StatsMods');
            this.remove('CombatMods');
            this.remove('Possessed');
            this._brain = this._actualBrain;
            delete this._actualBrain;
        }
    }

    /* Returns the cell where this actor is located at.*/
    getCell() {
        const x = this.getX();
        const y = this.getY();
        const level = this.getLevel();
        if (level) {
            return level.getMap().getCell(x, y);
        }
        return null;
    }

    isInLevel(level) {
        if (this.getLevel()) {
            return this.getLevel().getID() === level.getID();
        }
        return false;
    }

    toJSON() {
        let levelID = null;
        if (this.getLevel()) {
            levelID = this.getLevel().getID();
        }
        const obj = {
            id: this.getID(),
            name: this.getName(),
            type: this.getType(),
            x: this.getX(),
            y: this.getY(),
            fovRange: this.getFOVRange(),
            levelID,
            inventory: this.getInvEq().getInventory().toJSON(),
            equipment: this.getInvEq().getEquipment().toJSON(),
            brain: this._brain.toJSON(),
            new: 'Rogue'
        };

        obj.components = RG.Component.compsToJSON(this);

        if (obj.type === null) {
            RG.err('Actor.Rogue', 'toJSON',
                `Type null for ${JSON.stringify(obj)}`);
        }

        if (this._spellbook) {
            obj.spellbook = this._spellbook.toJSON();
        }
        if (this.has('Player')) {
            obj.isPlayer = true;
        }
        if (this._actualBrain) {
            obj.brain = this._actualBrain.toJSON();
        }

        return obj;
    }

    //---------------------------------
    // Combat-related methods
    //---------------------------------

    getAttack() {
        let attack = this.get('Combat').getAttack();
        attack += this.getEquipAttack();
        attack += this._addFromCompList('CombatMods', 'getAttack');
        attack += Math.floor(this.getAccuracy() / 2);
        return attack;
    }

    getDefense() {
        let defense = this.get('Combat').getDefense();
        defense += this.getEquipDefense();
        defense += this._addFromCompList('CombatMods', 'getDefense');
        defense += Math.floor(this.getAgility() / 2);
        return defense;
    }

    getProtection() {
        let protection = this.get('Combat').getProtection();
        protection += this.getEquipProtection();
        protection += this._addFromCompList('CombatMods', 'getProtection');
        return protection;
    }

    getDamage() {
        let damage = this.get('Combat').rollDamage();
        const weapon = this.getWeapon();
        if (weapon) {
            damage = RG.getItemDamage(weapon);
        }
        const strength = this.getStrength();
        damage += RG.strengthToDamage(strength);
        damage += this._addFromCompList('CombatMods', 'getDamage');
        return damage;

    }

    getCombatBonus(funcName) {
        return this._addFromCompList('CombatMods', funcName);
    }

    _addFromCompList(compType, func) {
        const compList = this.getList(compType);
        if (compList.length > 0) {
            return compList.reduce((acc, val) => {
                return acc + val[func]();
            }, 0);
        }
        return 0;
    }

    //-------------------------------------------------------------
    // Stats-related methods (these take eq and boosts into account
    //-------------------------------------------------------------

    getAccuracy() {
        let acc = this.get('Stats').getAccuracy();
        acc += this.getInvEq().getEquipment().getAccuracy();
        acc += this._addFromCompList('StatsMods', 'getAccuracy');
        return acc;
    }

    getAgility() {
        let agi = this.get('Stats').getAgility();
        agi += this.getInvEq().getEquipment().getAgility();
        agi += this._addFromCompList('StatsMods', 'getAgility');
        return agi;
    }

    getStrength() {
        let str = this.get('Stats').getStrength();
        str += this.getInvEq().getEquipment().getStrength();
        str += this._addFromCompList('StatsMods', 'getStrength');
        return str;
    }

    getWillpower() {
        let wil = this.get('Stats').getWillpower();
        wil += this.getInvEq().getEquipment().getWillpower();
        wil += this._addFromCompList('StatsMods', 'getWillpower');
        return wil;
    }

    getSpeed() {
        let speed = this.get('Stats').getSpeed();
        speed += this.getInvEq().getEquipment().getSpeed();
        speed += this._addFromCompList('StatsMods', 'getSpeed');
        return speed;
    }

    getPerception() {
        let per = this.get('Stats').getPerception();
        per += this.getInvEq().getEquipment().getPerception();
        per += this._addFromCompList('StatsMods', 'getPerception');
        return per;
    }

    getMagic() {
        let mag = this.get('Stats').getMagic();
        mag += this.getInvEq().getEquipment().getMagic();
        mag += this._addFromCompList('StatsMods', 'getMagic');
        return mag;
    }

    /* Returns bonuses applied to given stat. */
    getStatBonus(funcName) {
        return this._addFromCompList('StatsMods', funcName);
    }
}

RG.Actor.Rogue = RGActorRogue;

module.exports = RG.Actor;
<|endoftext|>


<commit_before>import { Observable } from 'rxjs/Observable'
import { of } from 'rxjs/observable/of'
import { from } from 'rxjs/observable/from'
import { _catch } from 'rxjs/operator/catch'
import { concatMap } from 'rxjs/operator/concatMap'
import { map } from 'rxjs/operator/map'
import { filter } from 'rxjs/operator/filter'

const { Collection } = require('./ast.js')
const HorizonSocket = require('./socket.js')
const { log, logError, enableLogging } = require('./logging.js')
const { authEndpoint, TokenStorage, clearAuthTokens } = require('./auth')

const defaultHost = typeof window !== 'undefined' && window.location &&
        `${window.location.host}` || 'localhost:8181'
const defaultSecure = typeof window !== 'undefined' && window.location &&
        window.location.protocol === 'https:' || false

function Horizon({
  host = defaultHost,
  secure = defaultSecure,
  path = 'horizon',
  lazyWrites = false,
  authType = 'unauthenticated',
} = {}) {
  // If we're in a redirection from OAuth, store the auth token for
  // this user in localStorage.
  const tokenStorage = new TokenStorage({ authType, path })
  tokenStorage.setAuthFromQueryParams()

  const socket = new HorizonSocket(
    host, secure, path, ::tokenStorage.handshake)

  // Store whatever token we get back from the server when we get a
  // handshake response
  socket.handshake.subscribe({
    next(handshake) {
      tokenStorage.set(handshake.token)
    },
    error(err) {
      if (/JsonWebTokenError/.test(err.message)) {
        console.error('Horizon: clearing token storage since auth failed')
        tokenStorage.remove()
      }
    },
  })

  // This is the object returned by the Horizon function. It's a
  // function so we can construct a collection simply by calling it
  // like horizon('my_collection')
  function horizon(name) {
    return new Collection(sendRequest, name, lazyWrites)
  }

  horizon.currentUser = () => new UserDataTerm(horizon, socket.handshake)

  horizon.disconnect = () => {
    socket.complete()
  }

  // Dummy subscription to force it to connect to the
  // server. Optionally provide an error handling function if the
  // socket experiences an error.
  // Note: Users of the Observable interface shouldn't need this
  horizon.connect = (
    onError = err => { console.error(`Received an error: ${err}`) }
  ) => {
    socket.subscribe(
      () => {},
      onError
    )
  }

  // Either subscribe to status updates, or return an observable with
  // the current status and all subsequent status changes.
  horizon.status = subscribeOrObservable(socket.status)

  // Convenience method for finding out when disconnected
  horizon.onDisconnected = subscribeOrObservable(
    socket.status::filter(x => x.type === 'disconnected'))

  // Convenience method for finding out when ready
  horizon.onReady = subscribeOrObservable(
    socket.status::filter(x => x.type === 'ready'))

  // Convenience method for finding out when an error occurs
  horizon.onSocketError = subscribeOrObservable(
    socket.status::filter(x => x.type === 'error'))

  horizon.utensils = {
    sendRequest,
    tokenStorage,
  }
  Object.freeze(horizon.utensils)

  horizon._authMethods = null
  horizon._horizonPath = path
  horizon.authEndpoint = authEndpoint
  horizon.hasAuthToken = ::tokenStorage.hasAuthToken

  return horizon

  // Sends a horizon protocol request to the server, and pulls the data
  // portion of the response out.
  function sendRequest(type, options) {
    // Both remove and removeAll use the type 'remove' in the protocol
    const normalizedType = type === 'removeAll' ? 'remove' : type
    return socket
      .makeRequest({ type: normalizedType, options }) // send the raw request
      ::concatMap(resp => {
        // unroll arrays being returned
        if (resp.data) {
          return Observable::from(resp.data)
        } else {
          // Still need to emit a document even if we have no new data
          return Observable::from([ { state: resp.state, type: resp.type } ])
        }
      })
      ::_catch(e => Observable.create(subscriber => {
        subscriber.error(e)
      })) // on error, strip error message
  }
}

function subscribeOrObservable(observable) {
  return (...args) => {
    if (args.length > 0) {
      return observable.subscribe(...args)
    } else {
      return observable
    }
  }
}

class UserDataTerm {
  constructor(hz, baseObservable) {
    this._hz = hz
    this._baseObservable = baseObservable::map(handshake => handshake.id)
  }

  _query(userId) {
    return this._hz('users').find(userId)
  }

  fetch() {
    return this._baseObservable::concatMap(userId => {
      if (userId === null) {
        return Observable::of({})
      } else {
        return this._query(userId).fetch()
      }
    })
  }

  watch(...args) {
    return this._baseObservable::concatMap(userId => {
      if (userId === null) {
        return Observable::of({})
      } else {
        return this._query(userId).watch(...args)
      }
    })
  }
}

Horizon.log = log
Horizon.logError = logError
Horizon.enableLogging = enableLogging
Horizon.Socket = HorizonSocket
Horizon.clearAuthTokens = clearAuthTokens

module.exports = Horizon
<commit_msg>Don't store unauthenticated tokens (#516)<commit_after>import { Observable } from 'rxjs/Observable'
import { of } from 'rxjs/observable/of'
import { from } from 'rxjs/observable/from'
import { _catch } from 'rxjs/operator/catch'
import { concatMap } from 'rxjs/operator/concatMap'
import { map } from 'rxjs/operator/map'
import { filter } from 'rxjs/operator/filter'

const { Collection } = require('./ast.js')
const HorizonSocket = require('./socket.js')
const { log, logError, enableLogging } = require('./logging.js')
const { authEndpoint, TokenStorage, clearAuthTokens } = require('./auth')

const defaultHost = typeof window !== 'undefined' && window.location &&
        `${window.location.host}` || 'localhost:8181'
const defaultSecure = typeof window !== 'undefined' && window.location &&
        window.location.protocol === 'https:' || false

function Horizon({
  host = defaultHost,
  secure = defaultSecure,
  path = 'horizon',
  lazyWrites = false,
  authType = 'unauthenticated',
} = {}) {
  // If we're in a redirection from OAuth, store the auth token for
  // this user in localStorage.
  const tokenStorage = new TokenStorage({ authType, path })
  tokenStorage.setAuthFromQueryParams()

  const socket = new HorizonSocket(
    host, secure, path, ::tokenStorage.handshake)

  // Store whatever token we get back from the server when we get a
  // handshake response
  socket.handshake.subscribe({
    next(handshake) {
      if (authType !== 'unauthenticated') {
        tokenStorage.set(handshake.token)
      }
    },
    error(err) {
      if (/JsonWebTokenError/.test(err.message)) {
        console.error('Horizon: clearing token storage since auth failed')
        tokenStorage.remove()
      }
    },
  })

  // This is the object returned by the Horizon function. It's a
  // function so we can construct a collection simply by calling it
  // like horizon('my_collection')
  function horizon(name) {
    return new Collection(sendRequest, name, lazyWrites)
  }

  horizon.currentUser = () => new UserDataTerm(horizon, socket.handshake)

  horizon.disconnect = () => {
    socket.complete()
  }

  // Dummy subscription to force it to connect to the
  // server. Optionally provide an error handling function if the
  // socket experiences an error.
  // Note: Users of the Observable interface shouldn't need this
  horizon.connect = (
    onError = err => { console.error(`Received an error: ${err}`) }
  ) => {
    socket.subscribe(
      () => {},
      onError
    )
  }

  // Either subscribe to status updates, or return an observable with
  // the current status and all subsequent status changes.
  horizon.status = subscribeOrObservable(socket.status)

  // Convenience method for finding out when disconnected
  horizon.onDisconnected = subscribeOrObservable(
    socket.status::filter(x => x.type === 'disconnected'))

  // Convenience method for finding out when ready
  horizon.onReady = subscribeOrObservable(
    socket.status::filter(x => x.type === 'ready'))

  // Convenience method for finding out when an error occurs
  horizon.onSocketError = subscribeOrObservable(
    socket.status::filter(x => x.type === 'error'))

  horizon.utensils = {
    sendRequest,
    tokenStorage,
  }
  Object.freeze(horizon.utensils)

  horizon._authMethods = null
  horizon._horizonPath = path
  horizon.authEndpoint = authEndpoint
  horizon.hasAuthToken = ::tokenStorage.hasAuthToken

  return horizon

  // Sends a horizon protocol request to the server, and pulls the data
  // portion of the response out.
  function sendRequest(type, options) {
    // Both remove and removeAll use the type 'remove' in the protocol
    const normalizedType = type === 'removeAll' ? 'remove' : type
    return socket
      .makeRequest({ type: normalizedType, options }) // send the raw request
      ::concatMap(resp => {
        // unroll arrays being returned
        if (resp.data) {
          return Observable::from(resp.data)
        } else {
          // Still need to emit a document even if we have no new data
          return Observable::from([ { state: resp.state, type: resp.type } ])
        }
      })
      ::_catch(e => Observable.create(subscriber => {
        subscriber.error(e)
      })) // on error, strip error message
  }
}

function subscribeOrObservable(observable) {
  return (...args) => {
    if (args.length > 0) {
      return observable.subscribe(...args)
    } else {
      return observable
    }
  }
}

class UserDataTerm {
  constructor(hz, baseObservable) {
    this._hz = hz
    this._baseObservable = baseObservable::map(handshake => handshake.id)
  }

  _query(userId) {
    return this._hz('users').find(userId)
  }

  fetch() {
    return this._baseObservable::concatMap(userId => {
      if (userId === null) {
        return Observable::of({})
      } else {
        return this._query(userId).fetch()
      }
    })
  }

  watch(...args) {
    return this._baseObservable::concatMap(userId => {
      if (userId === null) {
        return Observable::of({})
      } else {
        return this._query(userId).watch(...args)
      }
    })
  }
}

Horizon.log = log
Horizon.logError = logError
Horizon.enableLogging = enableLogging
Horizon.Socket = HorizonSocket
Horizon.clearAuthTokens = clearAuthTokens

module.exports = Horizon
<|endoftext|>