Enable Password Manager on MyTax Hasil

Enables Password Manager autofill on mytax.hasil.gov.my

// ==UserScript==
// @name         Enable Password Manager on MyTax Hasil
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Enables Password Manager autofill on mytax.hasil.gov.my
// @author       kazcfz
// @match        https://*.hasil.gov.my/*
// @grant        none
// @run-at       document-start
// @icon         https://upload.wikimedia.org/wikipedia/commons/4/4e/LHDN_logo.png
// @license      MIT
// ==/UserScript==

/* eslint-disable no-multi-spaces */

(function () {
  'use strict';

  // --- Constants ---
  const FORM_SELECTOR = "form[data-parsley-validate]";       // Main login form
  const PWD_ID = "gpm-password";                             // Temporary hidden password input ID
  const PERSISTENT_ID = "gpm-persistent-password";           // Hidden password field that persists across form reloads
  const ID_SELECT_SELECTOR = "select[ng-model='idType']";    // Dropdown for ID type
  const ID_SELECT_VALUE = "1";                               // 1 (Identification Card No.) || 2 (Passport No.) || 3 (Army No.) || 4 (Police No.)

  /**
   * Utility: Create a hidden <input> element that can be used
   * for tricking Google Password Manager into storing credentials.
   */
  function createHiddenInput({ type, id, name, autocomplete }) {
    const el = document.createElement("input");
    el.type = type;
    el.id = id;
    if (name) el.name = name;
    if (autocomplete) el.autocomplete = autocomplete;
    el.setAttribute(
      "style",
      "position:absolute;left:-9999px;top:0;width:1px;height:1px;opacity:0;pointer-events:none;border:0;margin:0;padding:0;"
    );
    return el;
  }

  /**
   * Ensure that a persistent hidden password field exists in <body>.
   * This field survives between form reloads and holds the last known password.
   */
  function ensurePersistentPasswordField() {
    const existing = document.getElementById(PERSISTENT_ID);
    if (existing) return existing;

    const create = () => {
      const el = createHiddenInput({ type: "password", id: PERSISTENT_ID });
      document.body.appendChild(el);
      return el;
    };

    if (document.body) {
      return create();
    } else {
      // Wait for <body> to appear if it doesn’t exist yet
      const observer = new MutationObserver(() => {
        if (document.body) {
          create();
          observer.disconnect();
        }
      });
      observer.observe(document.documentElement, { childList: true, subtree: true });
      return null;
    }
  }

  /**
   * Add hidden password fields to the form so Google Password Manager can recognize it.
   * Syncs input into the persistent password field whenever it changes.
   */
  function addCredentialFields() {
    const form = document.querySelector(FORM_SELECTOR);
    if (!form) return false;

    // Prevent duplicate fields
    if (form.querySelector(`#${PWD_ID}`)) return true;

    const password = createHiddenInput({
      type: "password",
      id: PWD_ID,
      name: "password",
      autocomplete: "current-password"
    });

    // Sync hidden field with persistent field
    password.addEventListener("input", () => {
      const persistent = ensurePersistentPasswordField();
      if (persistent) persistent.value = password.value;
    });

    form.appendChild(password);
    return true;
  }

  /**
   * Auto-select "Identification Card No." as the ID type if not already selected.
   */
  function autoSelectIdType() {
    const select = document.querySelector(ID_SELECT_SELECTOR);
    if (!select) return false;

    if (select.value !== ID_SELECT_VALUE) {
      select.value = ID_SELECT_VALUE;
      select.dispatchEvent(new Event("change", { bubbles: true }));
      select.dispatchEvent(new Event("input", { bubbles: true }));
    }
    return true;
  }

  /**
   * Restore the password from the persistent hidden field back into the actual
   * password input field (Angular-bound) if it exists and is empty.
   */
  function restorePassword() {
    const realPwd = document.querySelector("input[type='password'][ng-model='idPP']");
    if (!realPwd) return false;

    const persistent = document.getElementById(PERSISTENT_ID);
    if (persistent && persistent.value && !realPwd.value) {
      realPwd.focus();
      realPwd.value = persistent.value;

      // Trigger Angular change detection
      realPwd.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
      realPwd.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));

      realPwd.blur();

      return true;
    }
    return false;
  }

  /**
   * Clear the persistent password field when the form is submitted,
   * ensuring credentials aren’t kept in memory unnecessarily.
   */
  function clearPersistentOnSubmit() {
    const form = document.querySelector(FORM_SELECTOR);
    if (!form) return;

    form.addEventListener("submit", () => {
      const persistent = document.getElementById(PERSISTENT_ID);
      if (persistent) persistent.remove();
    }, { once: true });
  }

  /**
   * Safe wrapper for creating a MutationObserver on a node.
   * Returns the observer instance or null if creation failed.
   */
  function safeObserve(targetNode, callback) {
    try {
      const mo = new MutationObserver(callback);
      mo.observe(targetNode, { childList: true, subtree: true });
      return mo;
    } catch {
      return null;
    }
  }

  /**
   * Initialization routine:
   * - Ensures hidden fields exist
   * - Sets default ID type
   * - Restores password if possible
   * - Watches DOM changes to reapply these steps as needed
   */
  function init() {
    ensurePersistentPasswordField();
    addCredentialFields();
    autoSelectIdType();
    restorePassword();
    clearPersistentOnSubmit();

    // Observe DOM mutations for dynamic content
    let mo = safeObserve(document.documentElement, () => {
      addCredentialFields();
      autoSelectIdType();
      restorePassword();
      clearPersistentOnSubmit();
    });

    // Fallback if observer creation fails
    if (!mo) {
      const retryInterval = setInterval(() => {
        addCredentialFields();
        autoSelectIdType();
        restorePassword();
        clearPersistentOnSubmit();
        if (document.body) {
          mo = safeObserve(document.body, () => {
            addCredentialFields();
            autoSelectIdType();
            restorePassword();
            clearPersistentOnSubmit();
          });
          if (mo) clearInterval(retryInterval);
        }
      }, 300);
      setTimeout(() => clearInterval(retryInterval), 15000);
    }

    // Ensure everything runs again once DOM is fully loaded
    window.addEventListener("DOMContentLoaded", () => {
      ensurePersistentPasswordField();
      addCredentialFields();
      autoSelectIdType();
      restorePassword();
      clearPersistentOnSubmit();
    }, { once: true });
  }

  // --- Entry point ---
  init();
})();