Enable Password Manager on MyTax Hasil

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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();
})();