Enable Password Manager on MyTax Hasil

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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();
})();