刘光辉
9 小时以前 0dfe84494048ce27ba8449831782128412d3eb13
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import type { ObjectDirective } from 'vue';
 
interface PasswordInputState {
  activate: () => void;
  input: HTMLInputElement;
  unlock: () => void;
}
 
interface DirectiveState {
  inputStates: Map<HTMLInputElement, PasswordInputState>;
  observer: MutationObserver;
}
 
const directiveStates = new WeakMap<HTMLElement, DirectiveState>();
 
function disableFormAutocomplete(root: HTMLElement) {
  if (root instanceof HTMLFormElement) root.autocomplete = 'off';
  root.querySelectorAll('form').forEach((form) => (form.autocomplete = 'off'));
}
 
function protectInput(input: HTMLInputElement, state: DirectiveState) {
  if (state.inputStates.has(input)) return;
 
  input.autocomplete = 'off';
  input.name = `credential-${Date.now()}-${Math.random().toString(36).slice(2)}`;
  input.readOnly = true;
  input.type = 'text';
  input.style.setProperty('-webkit-text-security', 'disc');
  Object.assign(input.dataset, { '1pIgnore': 'true', lpignore: 'true' });
 
  const activate = () => {
    input.type = 'password';
    input.readOnly = false;
    input.style.removeProperty('-webkit-text-security');
    input.removeEventListener('pointerdown', unlock);
    input.removeEventListener('keydown', activate);
    input.removeEventListener('beforeinput', activate);
    input.removeEventListener('paste', activate);
  };
 
  // Keep the field disguised as text while it receives focus; password managers commonly fill on focus.
  const unlock = () => {
    input.readOnly = false;
    input.removeEventListener('pointerdown', unlock);
  };
 
  input.addEventListener('pointerdown', unlock);
  input.addEventListener('keydown', activate);
  input.addEventListener('beforeinput', activate);
  input.addEventListener('paste', activate);
  state.inputStates.set(input, { activate, input, unlock });
}
 
function protectPasswordInputs(root: HTMLElement, state: DirectiveState) {
  disableFormAutocomplete(root);
  if (root instanceof HTMLInputElement && root.type === 'password') protectInput(root, state);
  root.querySelectorAll<HTMLInputElement>('input[type="password"]').forEach((input) => protectInput(input, state));
}
 
/** Keep password managers from identifying and filling password fields before the user interacts with them. */
export const vDisablePasswordAutofill: ObjectDirective<HTMLElement, boolean | undefined> = {
  mounted(el, binding) {
    if (binding.value === false) return;
 
    const state: DirectiveState = {
      inputStates: new Map(),
      observer: new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
          mutation.addedNodes.forEach((node) => {
            if (node instanceof HTMLElement) protectPasswordInputs(node, state);
          });
        });
      }),
    };
    protectPasswordInputs(el, state);
    state.observer.observe(el, { childList: true, subtree: true });
    directiveStates.set(el, state);
  },
  unmounted(el) {
    const state = directiveStates.get(el);
    if (!state) return;
 
    state.observer.disconnect();
    state.inputStates.forEach(({ activate, input, unlock }) => {
      input.removeEventListener('pointerdown', unlock);
      input.removeEventListener('keydown', activate);
      input.removeEventListener('beforeinput', activate);
      input.removeEventListener('paste', activate);
    });
    directiveStates.delete(el);
  },
};