~harmless/security.txt

d8c85c94b88d8f0802aa9c059e0b48226f640183 — Travis Paul 1 year, 8 months ago 1adbdde
Basic functionality working across Firefox, Chrome, Opera, Edge
M Gruntfile.js => Gruntfile.js +2 -1
@@ 61,6 61,7 @@ module.exports = function(grunt) {
  });

  grunt.registerTask('manifests', 'Combine manifest.json files', function () {
    const {merge} = require('lodash/object');
    function rmNotes(obj) {
      const notesRegExp = new RegExp('^_notes_');
      Object.keys(obj).forEach((key) => {


@@ 87,7 88,7 @@ module.exports = function(grunt) {
      const manifestCopy = Object.assign({}, manifest);
      const filePath = `build/${browser}/manifest.json`;
      rmNotes(browserManifest);
      Object.assign(manifestCopy, browserManifest);
      merge(manifestCopy, browserManifest);
      grunt.file.write(filePath, JSON.stringify(manifestCopy, null, 2));
      grunt.log.ok(filePath);
    });

M package.json => package.json +1 -0
@@ 18,6 18,7 @@
    "grunt-contrib-jshint": "^2.1.0",
    "grunt-mkdir": "^1.0.0",
    "load-grunt-tasks": "^5.1.0",
    "lodash": "^4.17.15",
    "webextension-polyfill": "^0.6.0"
  }
}

M src/_locales/en/messages.json => src/_locales/en/messages.json +15 -19
@@ 7,22 7,6 @@
    "message": "Preferences",
    "description" : "Title text of options page"
  },
  "display_mode": {
    "message": "Display mode: ",
    "description" : "Label text for display mode selection drop-down"
  },
  "display_mode_INLINE": {
    "message": "In the same window",
    "description" : "Description for INLINE display mode"
  },
  "display_mode_NEW_TAB": {
    "message": "In a new tab",
    "description" : "Description for NEW_TAB display mode"
  },
  "display_mode_NEW_WINDOW": {
    "message": "In a new window",
    "description" : "Description for NEW_WINDOW display mode"
  },
  "check_humanstxt": {
    "message": "Also check for a humans.txt file: ",
    "description" : "Label text for humans.txt option"


@@ 79,6 63,14 @@
    "message": "Reset all preferences to default values",
    "description" : "Button text to set all options to the default"
  },
  "found_nothing": {
    "message": "No security.txt or humans.txt found.",
    "description" : "pageAction title when neither humans.txt or security.txt is found."
  },
  "not_found_security_txt": {
    "message": "No security.txt found",
    "description" : "pageAction title when only security.txt is not found and humans.txt check is disabled."
  },
  "found_security_txt": {
    "message": "Found security.txt",
    "description" : "pageAction title when only security.txt is found."


@@ 99,9 91,13 @@
    "message": "Release Notes",
    "description" : "Page title for release notes page shown on extension update"
  },
  "copy_clipboard": {
    "message": "Copy to clipboard",
    "description" : "Text for button to copy text to clipboard."
  "copy_security_clipboard": {
    "message": "Copy security.txt to clipboard",
    "description" : "Text for button to copy security.txt text to clipboard."
  },
  "copy_humans_clipboard": {
    "message": "Copy humans.txt to clipboard",
    "description" : "Text for button to copy humans.txt text to clipboard."
  },
  "copied_humans_to_clipboard": {
    "message": "Copied humans.txt!",

M src/css/popup.css => src/css/popup.css +7 -7
@@ 53,18 53,18 @@ body {
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
}
#securitytxt, #humanstxt{
#securityTxt, #humansTxt{
  display: none;
  border-top: 1px solid #bbb;
  padding:5px;
}
#securitytab:checked ~ #securitytxt,
#humanstab:checked ~ #humanstxt {
#securityTab:checked ~ #securityTxt,
#humansTab:checked ~ #humansTxt {
  display: block;
}

textarea {
  padding-top: 25px;
  padding-top: 5px;
  border:none;
  width:100%;
  min-height:300px;


@@ 78,10 78,10 @@ textarea:focus {
}

button {
  opacity: 0.45;
  opacity: 0.8;
  position:absolute;
  right:20px;
  top:55px;
  top:15px;
  background:rgba(225,225,225,0.8);
  cursor: pointer;
}


@@ 90,4 90,4 @@ button:hover {
  opacity: 1;
  background:rgba(225,225,225,0.9);
  cursor: pointer;
}
}
\ No newline at end of file

M src/js/background.js => src/js/background.js +100 -51
@@ 1,3 1,5 @@
/* globals i18n */

const httpRegExp = new RegExp('^http');

function isPlainText(result) {


@@ 8,89 10,136 @@ function isPlainText(result) {
}

function fetchError(error) {
  console.log('fetch() error:', error);
  console.log('fetch() fail:', error);
}

function tabsOnUpdated(tabId, changeInfo, tab) {
  if (changeInfo.url && httpRegExp.test(changeInfo.url)) {
function fetchFiles(tabId, tab) {
  const tabURL = new URL(tab.url);
  const host = `${tabURL.protocol}//${tabURL.host}`;

    const tabURL = new URL(tab.url);
    const host = `${tabURL.protocol}//${tabURL.host}`;
  const finalResults = {
    now: Date.now(),
    host: tabURL.host,
    security: false,
    humans: false
  };

    fetch(`${host}/.well-known/security.txt`).then((result) => {
  let humansTxtCheck = true;

      console.log('/.well-known/security.txt', result.status, result.headers.get('Content-Type'));
  fetch(`${host}/.well-known/security.txt`).then((result) => {

      if (!result.ok || !isPlainText(result)) {
    console.log('/.well-known/security.txt', result.status,
      result.headers.get('Content-Type'));

        return fetch(`${host}/security.txt`).then((result) => {
    if (!result.ok || !isPlainText(result)) {

          console.log('/security.txt', result.status, result.headers.get('Content-Type'));
      return fetch(`${host}/security.txt`).then((result) => {

          if (!result.ok || !isPlainText(result)) {
            return console.log({
              type: 'security.txt',
              found: false
            });
          }
        console.log('/security.txt', result.status,
          result.headers.get('Content-Type'));

        if (result.ok && isPlainText(result)) {
          result.text().then((text) => {
            console.log({
              type: 'security.txt',
            finalResults.security = {
              path: `${host}/security.txt`,
              found: true,
              content: text
            });
              text
            };
          });
        }
      }, fetchError);
    }

        }, fetchError);
      }

      result.text().then((text) => {
        console.log({
          type: 'security.txt',
          path: `${host}/.well-known/security.txt`,
          found: true,
          content: text
        });
      });
    return result.text().then((text) => {
      finalResults.security = {
        path: `${host}/.well-known/security.txt`,
        text
      };
    });

    }, fetchError);
  }, fetchError).finally(() => {

    browser.storage.local.get('check_humanstxt').then((result) => {

      if (result && result.hasOwnProperty('check_humanstxt') && result.check_humanstxt === 'OFF') {
        console.log('humans.txt check disabled');
        return;
        humansTxtCheck = false;
        return console.log('humans.txt check disabled');
      }

      fetch(`${host}/humans.txt`).then((result) => {
      return fetch(`${host}/humans.txt`).then((result) => {

        console.log('/humans.txt', result.status, result.headers.get('Content-Type'));
        console.log('/humans.txt', result.status,
          result.headers.get('Content-Type'));

        if (!result.ok || !isPlainText(result)) {
          return console.log({
            type: 'humans.txt',
            found: false
        if (result.ok && isPlainText(result)) {
          return result.text().then((text) => {
            finalResults.humans = {
              path: `${host}/humans.txt`,
              text
            };
          });
        }
      }, fetchError);

        result.text().then((text) => {
          console.log({
            type: 'humans.txt',
            path: `${host}/humans.txt`,
            found: true,
            content: text
          });
    }, fetchError).finally(() => {
      console.log('finalResults', finalResults);

      let title = i18n((humansTxtCheck) ?
        'found_nothing' : 'not_found_security_txt');

      if (finalResults.security && !finalResults.humans) {
        title = i18n('found_security_txt');
      } else if (!finalResults.security && finalResults.humans) {
        title = i18n('found_humans_txt');
      } else if (finalResults.security && finalResults.humans) {
        title = i18n('found_security_and_humans_txt');
      }
      
      // XXX Need to change icons for Chrome/Edge bug with pageAction.hide()
      // interesting enough, the bug doesn't impact Opera
      if (finalResults.security || finalResults.humans) {
        localStorage.setItem(finalResults.host, JSON.stringify(finalResults));
        browser.pageAction.show(tabId);
        browser.pageAction.setPopup({
          tabId: tabId,
          popup: 'popup.html#' + finalResults.host
        });
      } else {
        localStorage.removeItem(finalResults.host);
        browser.pageAction.hide(tabId);
        browser.pageAction.setPopup({
          tabId: tabId,
          // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction/setPopup
          // If an empty string ("") is passed here, the popup is disabled, and 
          // the extension will receive pageAction.onClicked events.
          popup: ''
        });
      }

      }, fetchError);
    }, (error) => {
      console.error('ERROR: browser.storage.local.get(check_humanstxt)', error);
      browser.pageAction.setTitle({
        tabId,
        title
      });
    });
  });
}

function tabsOnUpdated(tabId, changeInfo, tab) {
  if (changeInfo.url && httpRegExp.test(changeInfo.url)) {
    fetchFiles(tabId, tab);
  }
}

function runtimeOnInstalled(details) {

  // process each existing tab that's open when the extension is installed
  browser.tabs.query({}).then((tabs) => {
    tabs.forEach((tab) => {
      if (httpRegExp.test(tab.url)) {
        fetchFiles(tab.id, {url: tab.url});
      }
    });
  });

  switch (details.reason) {
    case 'install': 
      browser.tabs.create({

A src/js/popup.js => src/js/popup.js +67 -0
@@ 0,0 1,67 @@
/* globals i18n, i18nHydrate */

window.addEventListener('DOMContentLoaded', () => {
  const host = document.location.hash.replace('#', '');

  const securityTxt = document.querySelector('#securityTxt > textarea');
  const securityTab = document.querySelector('#securityTabLabel');
  const securityInput = document.querySelector('#securityTab');

  const humansTxt = document.querySelector('#humansTxt > textarea');
  const humansTab = document.querySelector('#humansTabLabel');
  const humansInput = document.querySelector('#humansTab');

  const clipboardSecurity = document.querySelector('#securityTxt button');
  const clipboardHumans = document.querySelector('#humansTxt button');
  const msgTimeout = 3000;

  let hostJSON = localStorage.getItem(host);
  let hostData;

  i18nHydrate();

  if (hostJSON) {
    try {
      hostData = JSON.parse(hostJSON);
    } catch (e) {
      console.error(host, hostJSON, e);
      return;
    }
  }

  if (hostData.security) {
    securityTxt.value = hostData.security.text;
  } else {
    securityTab.style.display = 'none';
  }

  if (hostData.humans) {
    humansTxt.value = hostData.humans.text;
  } else {
    humansTab.style.display = 'none';
  }

  if (hostData.humans && !hostData.security) {
    securityInput.checked = false;
    humansInput.checked = true;
  }

  clipboardSecurity.addEventListener('click', () => {
    securityTxt.select();
    document.execCommand('copy');
    clipboardSecurity.innerText = i18n('copied_security_to_clipboard');
    setTimeout(() => {
      clipboardSecurity.innerText = i18n('copy_security_clipboard');
    }, msgTimeout);
  });

  clipboardHumans.addEventListener('click', () => {
    humansTxt.select();
    document.execCommand('copy');
    clipboardHumans.innerText = i18n('copied_humans_to_clipboard');
    setTimeout(() => {
      clipboardHumans.innerText = i18n('copy_humans_clipboard');
    }, msgTimeout);
  });

});
\ No newline at end of file

M src/options.html => src/options.html +0 -8
@@ 7,14 7,6 @@
  </head>
  <body>
    <form>
      <div class="browser-style">
        <label for="display_mode" data-i18n></label>
        <select id="display_mode" class="browser-style" data-i18n>
          <option value="INLINE"></option>
          <option value="NEW_TAB"></option>
          <option value="NEW_WINDOW"></option>
        </select>
      </div>
      <div>
        <label for="check_humanstxt" data-i18n></label>
        <select id="check_humanstxt" class="browser-style" data-i18n>

M src/popup.html => src/popup.html +8 -9
@@ 3,20 3,19 @@
  <head>
    <meta charset="utf-8"/>
    <link rel="stylesheet" href="css/popup.css"/>
    <link rel="stylesheet" href="chrome://browser/content/extension.css"/>
  </head>
  <body>
    <div class="tabs">
      <input type="radio" id="securitytab" name="tabs" checked>
      <label id="securitytablabel" for="securitytab">security.txt</label>
      <input type="radio" id="humanstab" name="tabs">
      <label id="humanstablabel" for="humanstab">humans.txt</label>
      <div id="securitytxt">
        <button type="button" data-i18n="copy_clipboard"></button>
      <input type="radio" id="securityTab" name="tabs" checked>
      <label id="securityTabLabel" for="securityTab">security.txt</label>
      <input type="radio" id="humansTab" name="tabs">
      <label id="humansTabLabel" for="humansTab">humans.txt</label>
      <div id="securityTxt">
        <button type="button" data-i18n="copy_security_clipboard"></button>
        <textarea spellcheck="false"></textarea>
      </div>
      <div id="humanstxt">
        <button type="button" data-i18n="copy_clipboard"></button>
      <div id="humansTxt">
        <button type="button" data-i18n="copy_humans_clipboard"></button>
        <textarea spellcheck="false"></textarea>
      </div>
    </div>

M yarn.lock => yarn.lock +1 -1
@@ 1090,7 1090,7 @@ locate-path@^3.0.0:
    p-locate "^3.0.0"
    path-exists "^3.0.0"

lodash@^4.17.14, lodash@~4.17.10, lodash@~4.17.11, lodash@~4.17.5:
lodash@^4.17.14, lodash@^4.17.15, lodash@~4.17.10, lodash@~4.17.11, lodash@~4.17.5:
  version "4.17.15"
  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
  integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==