summaryrefslogtreecommitdiff
path: root/raveos-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/modules/injection.js
blob: 7d5bdd1866c3f305d1211589303b5aeb68c8f043 (plain)
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
// SPDX-FileCopyrightText: 2023 Deminder <tremminder@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
import { logDebug } from './util.js';

/**
 * The InjectionTracker helps revert object property injections 'out of order'
 * by keeping track of the injection history.
 * This is useful when overriding the same method from mulitple extensions,
 * for instance, `Main.panel.statusArea.quickSettings._addItems`.
 *
 * The InjectionManager does not work when multiple extensions override the same method.
 * Its `restoreMethod` only restores correctly if it is called in reverse order to `overrideMethod`.
 *
 */
export class InjectionTracker {
  localInjections = [];

  /**
   * Inject a property into an object.
   *
   * To revert the injection call `injection.clear()`.
   *
   * @param {object} obj the target object
   * @param {string} prop the property name
   * @param {any} value the new property value
   * @param {boolean} isPropertyDescriptor whether the value is a property descriptor
   *
   * @returns {object} an injection object
   * provding `injection.original` and `injection.previous` property values
   */
  injectProperty(obj, prop, value, isPropertyDescriptor) {
    let propertyDescriptor = Object.getOwnPropertyDescriptor(obj, prop);
    if (!propertyDescriptor) {
      if (prop in obj) {
        propertyDescriptor = {
          value: obj[prop],
          writable: true,
          configurable: true,
        };
      } else {
        throw new Error(
          `Injection target object does not have a '${prop}' property!`
        );
      }
    }
    // Keep history; mutated by each injection
    const histories = obj.__injectionHistories ?? {};
    const history = histories[prop] ?? [];
    // Push old property descriptor to history
    history.push(propertyDescriptor);
    histories[prop] = history;
    obj.__injectionHistories = histories;

    const injectionId = history.length;
    logDebug('[new] injectionid', injectionId);

    // Override value
    Object.defineProperty(
      obj,
      prop,
      isPropertyDescriptor
        ? value
        : { value, writable: false, configurable: true }
    );

    const localInjectionId = this.localInjections.length;
    const injection = createInjection(obj, prop, history, injectionId, () => {
      // Remove from local injections
      this.localInjections[localInjectionId] = undefined;
      const pruneIndex =
        this.localInjections.length -
        1 -
        [...this.localInjections].reverse().findIndex(inj => inj !== undefined);
      if (pruneIndex === this.localInjections.length) {
        logDebug('[local-cleanclear]');
        this.localInjections = [];
      } else {
        logDebug('[local-nocleanclear]');
        this.localInjections = this.localInjections.slice(0, pruneIndex);
      }
    });
    this.localInjections.push(injection);
    return injection;
  }

  /**
   * Clear all injections made by this instance.
   */
  clearAll() {
    this.localInjections
      .filter(inj => inj !== undefined)
      .forEach(inj => inj.clear());
  }
}

function createInjection(obj, prop, history, injectionId, clearHook) {
  let reverted = false;
  const readDescriptorValue = descriptor =>
    'get' in descriptor ? descriptor.get.call(obj) : descriptor.value;
  return {
    /**
     * Read the original property value before any injections.
     */
    get original() {
      // Using h instead history to remain valid after `clear()`
      const h = obj.__injectionHistories?.[prop];
      return readDescriptorValue(
        h?.length ? h[0] : Object.getOwnPropertyDescriptor(obj, prop)
      );
    },

    /**
     * Read the previous property value from the injection history.
     *
     * Valid after `clear()` as long as no new injection occurs.
     */
    get previous() {
      return history.length
        ? readDescriptorValue(
            injectionId > history.length
              ? Object.getOwnPropertyDescriptor(obj, prop)
              : popPropertyDescriptorFromHistory(
                  // previous history
                  history.slice(0, injectionId)
                )
          )
        : this.original;
    },

    /**
     * Clear the injection from the injection history.
     */
    clear() {
      if (!reverted) {
        reverted = true;
        clearHook();

        // Remove from global injections
        if (injectionId >= history.length) {
          logDebug(
            '[remclear] injectionID',
            injectionId,
            'historylen',
            history.length
          );
          // Restore property of obj
          Object.defineProperty(
            obj,
            prop,
            popPropertyDescriptorFromHistory(history)
          );
          // Cleanup empty history
          if (history.length === 0) {
            logDebug('[cleanclear]');
            delete obj.__injectionHistories[prop];

            // Cleanup empty history store
            if (Object.keys(obj.__injectionHistories).length === 0) {
              delete obj.__injectionHistories;
            }
          } else {
            logDebug('[nocleanclear] history.length', history.length);
          }
        } else {
          logDebug(
            '[setclear] injectionID',
            injectionId,
            'historylen',
            history.length
          );
          // Clear injection from history
          history[injectionId] = undefined;
        }
      }
    },

    /**
     * Count of previous injections. Is 1 if there is only this injection.
     */
    get count() {
      return history.slice(0, injectionId).filter(i => i !== undefined).length;
    },
  };
}

function popPropertyDescriptorFromHistory(history) {
  let propertyDescriptor;
  do {
    propertyDescriptor = history.pop();
  } while (history.length && propertyDescriptor === undefined);
  return propertyDescriptor;
}