tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

WallpaperFeed.sys.mjs (12025B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
      4 
      5 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      6 
      7 const lazy = {};
      8 ChromeUtils.defineESModuleGetters(lazy, {
      9  BasePromiseWorker: "resource://gre/modules/PromiseWorker.sys.mjs",
     10  RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
     11  Utils: "resource://services-settings/Utils.sys.mjs",
     12 });
     13 
     14 import {
     15  actionTypes as at,
     16  actionCreators as ac,
     17 } from "resource://newtab/common/Actions.mjs";
     18 
     19 const PREF_WALLPAPERS_ENABLED =
     20  "browser.newtabpage.activity-stream.newtabWallpapers.enabled";
     21 
     22 const PREF_WALLPAPERS_HIGHLIGHT_SEEN_COUNTER =
     23  "browser.newtabpage.activity-stream.newtabWallpapers.highlightSeenCounter";
     24 
     25 const WALLPAPER_REMOTE_SETTINGS_COLLECTION_V2 = "newtab-wallpapers-v2";
     26 
     27 const PREF_WALLPAPERS_CUSTOM_WALLPAPER_ENABLED =
     28  "browser.newtabpage.activity-stream.newtabWallpapers.customWallpaper.enabled";
     29 
     30 const PREF_WALLPAPERS_CUSTOM_WALLPAPER_UUID =
     31  "browser.newtabpage.activity-stream.newtabWallpapers.customWallpaper.uuid";
     32 
     33 const PREF_SELECTED_WALLPAPER =
     34  "browser.newtabpage.activity-stream.newtabWallpapers.wallpaper";
     35 
     36 const RS_FALLBACK_BASE_URL =
     37  "https://firefox-settings-attachments.cdn.mozilla.net/";
     38 
     39 export class WallpaperFeed {
     40  #customBackgroundObjectURL = null;
     41 
     42  // @backward-compat { version 148 } This newtab train-hop compatibility
     43  // shim can be removed once Firefox 148 makes it to the release channel.
     44  #usesProtocolHandler =
     45    Services.vc.compare(AppConstants.MOZ_APP_VERSION, "148.0a1") >= 0;
     46 
     47  constructor() {
     48    this.loaded = false;
     49    this.wallpaperClient = null;
     50    this._onSync = this.onSync.bind(this);
     51  }
     52 
     53  // Constructs a moz-newtab-wallpaper:// URI for the given wallpaper UUID.
     54  getWallpaperURL(uuid) {
     55    return `moz-newtab-wallpaper://${uuid}`;
     56  }
     57 
     58  /**
     59   * This thin wrapper around global.fetch makes it easier for us to write
     60   * automated tests that simulate responses from this fetch.
     61   */
     62  fetch(...args) {
     63    return fetch(...args);
     64  }
     65 
     66  /**
     67   * This thin wrapper around lazy.RemoteSettings makes it easier for us to write
     68   * automated tests that simulate responses from this fetch.
     69   */
     70  RemoteSettings(...args) {
     71    return lazy.RemoteSettings(...args);
     72  }
     73 
     74  /**
     75   * This thin wrapper around lazy.BasePromiseWorker makes it easier for us to write
     76   * automated tests
     77   */
     78  BasePromiseWorker(...args) {
     79    return new lazy.BasePromiseWorker(...args);
     80  }
     81 
     82  async wallpaperSetup(isStartup = false) {
     83    const wallpapersEnabled = Services.prefs.getBoolPref(
     84      PREF_WALLPAPERS_ENABLED
     85    );
     86 
     87    if (wallpapersEnabled) {
     88      if (!this.wallpaperClient) {
     89        // getting collection
     90        this.wallpaperClient = this.RemoteSettings(
     91          WALLPAPER_REMOTE_SETTINGS_COLLECTION_V2
     92        );
     93      }
     94 
     95      this.wallpaperClient.on("sync", this._onSync);
     96      this.updateWallpapers(isStartup);
     97    }
     98  }
     99 
    100  async wallpaperTeardown() {
    101    if (this._onSync) {
    102      this.wallpaperClient?.off("sync", this._onSync);
    103    }
    104    this.loaded = false;
    105    this.wallpaperClient = null;
    106  }
    107 
    108  async onSync() {
    109    this.wallpaperTeardown();
    110    await this.wallpaperSetup(false /* isStartup */);
    111  }
    112 
    113  async updateWallpapers(isStartup = false) {
    114    // @backward-compat { version 148 } This newtab train-hop compatibility
    115    // shim can be removed once Firefox 148 makes it to the release channel.
    116    if (!this.#usesProtocolHandler) {
    117      if (this.#customBackgroundObjectURL) {
    118        URL.revokeObjectURL(this.#customBackgroundObjectURL);
    119        this.#customBackgroundObjectURL = null;
    120      }
    121    }
    122 
    123    let uuid = Services.prefs.getStringPref(
    124      PREF_WALLPAPERS_CUSTOM_WALLPAPER_UUID,
    125      ""
    126    );
    127 
    128    const selectedWallpaper = Services.prefs.getStringPref(
    129      PREF_SELECTED_WALLPAPER,
    130      ""
    131    );
    132 
    133    if (uuid && selectedWallpaper === "custom") {
    134      // @backward-compat { version 148 } This newtab train-hop compatibility
    135      // shim can be removed once Firefox 148 makes it to the release channel.
    136      if (this.#usesProtocolHandler) {
    137        const wallpaperURI = this.getWallpaperURL(uuid);
    138 
    139        this.store.dispatch(
    140          ac.BroadcastToContent({
    141            type: at.WALLPAPERS_CUSTOM_SET,
    142            data: wallpaperURI,
    143          })
    144        );
    145      } else {
    146        const wallpaperDir = PathUtils.join(PathUtils.profileDir, "wallpaper");
    147        const filePath = PathUtils.join(wallpaperDir, uuid);
    148 
    149        try {
    150          let testFile = await IOUtils.getFile(filePath);
    151 
    152          if (!testFile) {
    153            throw new Error("File does not exist");
    154          }
    155 
    156          let imageFile = await File.createFromNsIFile(testFile);
    157          this.#customBackgroundObjectURL = URL.createObjectURL(imageFile);
    158 
    159          this.store.dispatch(
    160            ac.BroadcastToContent({
    161              type: at.WALLPAPERS_CUSTOM_SET,
    162              data: this.#customBackgroundObjectURL,
    163            })
    164          );
    165        } catch (error) {
    166          console.warn(`Wallpaper file not found: ${error.message}`);
    167          Services.prefs.clearUserPref(PREF_WALLPAPERS_CUSTOM_WALLPAPER_UUID);
    168          return;
    169        }
    170      }
    171    } else {
    172      this.store.dispatch(
    173        ac.BroadcastToContent({
    174          type: at.WALLPAPERS_CUSTOM_SET,
    175          data: null,
    176        })
    177      );
    178    }
    179 
    180    // retrieving all records in collection
    181    const records = await this.wallpaperClient.get();
    182    if (!records?.length) {
    183      return;
    184    }
    185 
    186    const customWallpaperEnabled = Services.prefs.getBoolPref(
    187      PREF_WALLPAPERS_CUSTOM_WALLPAPER_ENABLED
    188    );
    189 
    190    let baseAttachmentURL = RS_FALLBACK_BASE_URL;
    191    try {
    192      baseAttachmentURL = await lazy.Utils.baseAttachmentsURL();
    193    } catch (error) {
    194      console.error(
    195        `Error fetching remote settings base url from CDN. Falling back to ${RS_FALLBACK_BASE_URL}`,
    196        error
    197      );
    198    }
    199 
    200    const wallpapers = [
    201      ...records.map(record => {
    202        return {
    203          ...record,
    204          ...(record.attachment
    205            ? {
    206                wallpaperUrl: `${baseAttachmentURL}${record.attachment.location}`,
    207              }
    208            : {}),
    209          background_position: record.background_position || "center",
    210          category: record.category || "",
    211          order: record.order || 0,
    212        };
    213      }),
    214    ];
    215 
    216    const categories = [
    217      ...new Set(
    218        wallpapers.map(wallpaper => wallpaper.category).filter(Boolean)
    219      ),
    220      ...(customWallpaperEnabled ? ["custom-wallpaper"] : []), // Conditionally add custom wallpaper input
    221    ];
    222 
    223    this.store.dispatch(
    224      ac.BroadcastToContent({
    225        type: at.WALLPAPERS_SET,
    226        data: wallpapers,
    227        meta: {
    228          isStartup,
    229        },
    230      })
    231    );
    232 
    233    this.store.dispatch(
    234      ac.BroadcastToContent({
    235        type: at.WALLPAPERS_CATEGORY_SET,
    236        data: categories,
    237        meta: {
    238          isStartup,
    239        },
    240      })
    241    );
    242  }
    243 
    244  initHighlightCounter() {
    245    let counter = Services.prefs.getIntPref(
    246      PREF_WALLPAPERS_HIGHLIGHT_SEEN_COUNTER
    247    );
    248 
    249    this.store.dispatch(
    250      ac.AlsoToPreloaded({
    251        type: at.WALLPAPERS_FEATURE_HIGHLIGHT_COUNTER_INCREMENT,
    252        data: {
    253          value: counter,
    254        },
    255      })
    256    );
    257  }
    258 
    259  wallpaperSeenEvent() {
    260    let counter = Services.prefs.getIntPref(
    261      PREF_WALLPAPERS_HIGHLIGHT_SEEN_COUNTER
    262    );
    263 
    264    const newCount = counter + 1;
    265 
    266    this.store.dispatch(
    267      ac.OnlyToMain({
    268        type: at.SET_PREF,
    269        data: {
    270          name: "newtabWallpapers.highlightSeenCounter",
    271          value: newCount,
    272        },
    273      })
    274    );
    275 
    276    this.store.dispatch(
    277      ac.AlsoToPreloaded({
    278        type: at.WALLPAPERS_FEATURE_HIGHLIGHT_COUNTER_INCREMENT,
    279        data: {
    280          value: newCount,
    281        },
    282      })
    283    );
    284  }
    285 
    286  async wallpaperUpload(file) {
    287    try {
    288      const customWallpaperThemeWorker = this.BasePromiseWorker(
    289        "resource://newtab/lib/Wallpapers/WallpaperTheme.worker.mjs",
    290        { type: "module" }
    291      );
    292      const wallpaperTheme = await customWallpaperThemeWorker.post(
    293        "calculateTheme",
    294        [file]
    295      );
    296      customWallpaperThemeWorker.terminate();
    297      const wallpaperDir = PathUtils.join(PathUtils.profileDir, "wallpaper");
    298 
    299      // create wallpaper directory if it does not exist
    300      await IOUtils.makeDirectory(wallpaperDir, { ignoreExisting: true });
    301 
    302      let uuid = Services.uuid.generateUUID().toString().slice(1, -1);
    303      Services.prefs.setStringPref(PREF_WALLPAPERS_CUSTOM_WALLPAPER_UUID, uuid);
    304 
    305      const filePath = PathUtils.join(wallpaperDir, uuid);
    306 
    307      // convert to Uint8Array for IOUtils
    308      const arrayBuffer = await file.arrayBuffer();
    309      const uint8Array = new Uint8Array(arrayBuffer);
    310 
    311      await IOUtils.write(filePath, uint8Array, { tmpPath: `${filePath}.tmp` });
    312 
    313      // @backward-compat { version 148 } This newtab train-hop compatibility
    314      // shim can be removed once Firefox 148 makes it to the release channel.
    315      if (this.#usesProtocolHandler) {
    316        const wallpaperURI = this.getWallpaperURL(uuid);
    317 
    318        this.store.dispatch(
    319          ac.BroadcastToContent({
    320            type: at.WALLPAPERS_CUSTOM_SET,
    321            data: wallpaperURI,
    322          })
    323        );
    324      } else {
    325        if (this.#customBackgroundObjectURL) {
    326          URL.revokeObjectURL(this.#customBackgroundObjectURL);
    327          this.#customBackgroundObjectURL = null;
    328        }
    329 
    330        this.#customBackgroundObjectURL = URL.createObjectURL(file);
    331 
    332        this.store.dispatch(
    333          ac.BroadcastToContent({
    334            type: at.WALLPAPERS_CUSTOM_SET,
    335            data: this.#customBackgroundObjectURL,
    336          })
    337        );
    338      }
    339 
    340      this.store.dispatch(
    341        ac.SetPref("newtabWallpapers.customWallpaper.theme", wallpaperTheme)
    342      );
    343 
    344      return filePath;
    345    } catch (error) {
    346      console.error("Error saving wallpaper:", error);
    347      return null;
    348    }
    349  }
    350 
    351  async removeCustomWallpaper() {
    352    try {
    353      let uuid = Services.prefs.getStringPref(
    354        PREF_WALLPAPERS_CUSTOM_WALLPAPER_UUID,
    355        ""
    356      );
    357 
    358      if (!uuid) {
    359        return;
    360      }
    361 
    362      const wallpaperDir = PathUtils.join(PathUtils.profileDir, "wallpaper");
    363      const filePath = PathUtils.join(wallpaperDir, uuid);
    364 
    365      await IOUtils.remove(filePath, { ignoreAbsent: true });
    366 
    367      Services.prefs.clearUserPref(PREF_WALLPAPERS_CUSTOM_WALLPAPER_UUID);
    368 
    369      this.store.dispatch(
    370        ac.BroadcastToContent({
    371          type: at.WALLPAPERS_CUSTOM_SET,
    372          data: null,
    373        })
    374      );
    375    } catch (error) {
    376      console.error("Failed to remove custom wallpaper:", error);
    377    }
    378  }
    379 
    380  async onAction(action) {
    381    switch (action.type) {
    382      case at.INIT:
    383        await this.wallpaperSetup(true /* isStartup */);
    384        this.initHighlightCounter();
    385        break;
    386      case at.UNINIT:
    387        break;
    388      case at.SYSTEM_TICK:
    389        break;
    390      case at.PREF_CHANGED:
    391        if (
    392          action.data.name ===
    393            "newtabWallpapers.newtabWallpapers.customColor.enabled" ||
    394          action.data.name === "newtabWallpapers.customWallpaper.enabled" ||
    395          action.data.name === "newtabWallpapers.enabled"
    396        ) {
    397          this.wallpaperTeardown();
    398          await this.wallpaperSetup(false /* isStartup */);
    399        }
    400        if (action.data.name === "newtabWallpapers.highlightSeenCounter") {
    401          // Reset redux highlight counter to pref
    402          this.initHighlightCounter();
    403        }
    404        break;
    405      case at.WALLPAPERS_SET:
    406        break;
    407      case at.WALLPAPERS_FEATURE_HIGHLIGHT_SEEN:
    408        this.wallpaperSeenEvent();
    409        break;
    410      case at.WALLPAPER_UPLOAD:
    411        this.wallpaperUpload(action.data.file);
    412        break;
    413      case at.WALLPAPER_REMOVE_UPLOAD:
    414        await this.removeCustomWallpaper();
    415        break;
    416    }
    417  }
    418 }