Logo

dev-resources.site

for different kinds of informations.

Avoiding a "Host Permission" Review Delay When Publishing a Chrome Extension

Published at
12/3/2024
Categories
chrome
chromium
extension
Author
alexmacarthur
Categories
3 categories in total
chrome
open
chromium
open
extension
open
Author
13 person written this
alexmacarthur
open
Avoiding a "Host Permission" Review Delay When Publishing a Chrome Extension

I just wrapped up a Chrome Extension that allows you to convert and download any AVIF or WebP image as a more useful JPEG, PNG, or GIF (it aims to solve one of the greatest pains on the internet). The extension's very simple, but I ran into an interesting slowdown getting it finished up and submitted for review.

Under the "Permission Justification" section of the submission form, the following banner was shown after uploading my ZIP file:

"Delay publishing" is rather ambiguous, which led me to assume it'd be forever before it'd finally get reviewed. I wasn't up for that, so I did some digging and found a way to circumvent the issue by structuring my extension a bit differently. Hopefully, it can help speed up someone else's process too.

The Initial Structural Problem

This warning was triggered by the first version of my manifest.json file – specifically my usage of content_scripts:

Here's how it looked:

{
    "manifest_version": 3,
    "name": "PicPerf's Image Saver",
    "version": "1.1",
    "description": "Convert and save images in different formats.",
    "permissions": ["contextMenus", "downloads"],
    "background": {
        "service_worker": "background.js"
    },
    "content_scripts": [
        {
            "matches": ["<all_urls>"],
            "js": ["content.js"]
        }
    ], 
    "icons": {
        "16": "images/icon-16.png",
        "48": "images/icon-48.png",
        "128": "images/icon-128.png"
    },  
    "action": {}
}

Enter fullscreen mode Exit fullscreen mode

The content_scripts section of the file specifies code that can run in the context of a loaded web page. Any scripts injected here can read, modify, and share details about what the user's viewing. That sounds inherently risky, and for good reason. And even risker, the <all_urls> match meant content.js would be able to run on any page. No restrictions.

My extension does legitimately need to access this kind of stuff. It'd performs a little bit of DOM work to indicate a conversion is being performed, and it's also necessary for triggering a download when the work is finished. (There may be a way to offload some of this work to that background.js file referenced above, but I haven't done deep exploration into those possibilities yet).

All of this content.js work was triggered by an event published from my background.js file:

chrome.contextMenus.onClicked.addListener((info, tab) => {
  const formatId = info.menuItemId.replace("convert-to-", "");
  const format = FORMATS.find((f) => f.id === formatId);

  chrome.tabs.sendMessage(tab.id, {
    type: "CONVERT_IMAGE",
    imageUrl: info.srcUrl,
    format: format,
  });
});
Enter fullscreen mode Exit fullscreen mode

All of this was functioning fine, so I was eager to figure out a workaround.

Granting Access On-Demand

Thankfully, it didn't take long. I was able to find an alternative approach using Chrome's "activeTab" and "scripting" permissions, which would grant access to the page only when the extension is explicitly invoked. This way, all the work I needed to do would only ever happen in response to a user's action, and only on the current tab. It's a bit safer, and it'd mean I could bypass that extra review time.

First up, I added a couple more permissions and removed the content_scripts property from my manifest.json file.

{
    "manifest_version": 3,
    "name": "PicPerf's Image Saver",
    "version": "1.1",
    "description": "Convert and save images in different formats.",
- "permissions": ["contextMenus", "downloads"],
+ "permissions": ["contextMenus", "downloads", "activeTab", "scripting"],
    "background": {
        "service_worker": "background.js"
    },
- "content_scripts": [
- {
- "matches": ["<all_urls>"],
- "js": ["content.js"]
- }
- ], 
    "icons": {
        "16": "images/icon-16.png",
        "48": "images/icon-48.png",
        "128": "images/icon-128.png"
    },  
    "action": {}
}

Enter fullscreen mode Exit fullscreen mode

Next, I adjusted that background.js bit to use Chrome's Scripting API. Rather than strictly publishing a message to a content script that's already listening, it'd first execute that script, and then publish the message.

It's a bit contrived for ease of explanation, but this is how it unfolded:

// background.js

chrome.contextMenus.onClicked.addListener((info, tab) => {
  const formatId = info.menuItemId.replace("convert-to-", "");
  const format = FORMATS.find((f) => f.id === formatId);

  // First, execute the client-side script.
  chrome.scripting
    .executeScript({
      target: { tabId: tab.id },
      files: ["content.js"],
    })
    .then(() => {
      // Then, publish the message like before.
      chrome.tabs.sendMessage(tab.id, {
        type: "CONVERT_IMAGE",
        imageUrl: info.srcUrl,
        format: format,
      });
    },
    (error) => {
      console.error(error);
    }
  );
});
Enter fullscreen mode Exit fullscreen mode

At first attempted, everything continued to work fine. Until I started to repeatedly save images on the same page.

Avoiding Repeat Execution

With this setup, content.js was executing every time my context menu item was clicked. That meant my event listener would become repeatedly reregistered, causing more callbacks to trigger unnecessarily.

For my needs, the fix was simple enough: only execute the script when I know it hadn't been done before. Otherwise, publish that message like normal.

// background.js

globalThis._PP_EXECUTED_ON_TABS = new Set();

function publishMessage(tabId, srcUrl, format) {
  chrome.tabs.sendMessage(tabId, {
    type: "CONVERT_IMAGE",
    imageUrl: srcUrl,
    format: format,
  });
}

chrome.contextMenus.onClicked.addListener((info, tab) => {
  const formatId = info.menuItemId.replace("convert-to-", "");
  const format = FORMATS.find((f) => f.id === formatId);

  // It's already been executed on this tab! Bow out early.
  if (globalThis._PP_EXECUTED_ON_TABS.has(tab.id)) {
    publishMessage(tab.id, info.srcUrl, format);

    return;
  }

  globalThis._PP_EXECUTED_ON_TABS.add(tab.id);

  chrome.scripting
    .executeScript({
      target: { tabId: tab.id },
      files: ["content.js"],
    })
    .then(() => {
      publishMessage(tab.id, info.srcUrl, format);
    },
    (error) => {
      console.error(error);
    }
  );
});
Enter fullscreen mode Exit fullscreen mode

No change to my content.js file was needed at all, by the way. It just listened for an event from Chrome like before:

// content.js

chrome.runtime.onMessage.addListener((message) => {
  if (message.type === "CONVERT_IMAGE") {
    // Do stuff.
  }
});
Enter fullscreen mode Exit fullscreen mode

Works great, and all parties (especially me) were satisfied. ✅

I'll Be Back

I am still so sorely unfamiliar with the extension writing process, as well as the APIs and conventions Chrome provides with it. So, while it's such a little thing, understanding how some of these pieces fit together was very satisfying. I'm eager to iterate on this extension and be back to build more tools in the future.

The PicPerf Image Saver is live, by the way. Install it and send me your feedback!

chrome Article's
30 articles in total
Favicon
Test your website in different Timezone
Favicon
Chrome starts to look cute now with its new Chrome MV
Favicon
The latest version of Chrome browser enable the solidWorks 3D ActiveX control
Favicon
Deploy Puppeteer As a Serverless API: Solutions Compared
Favicon
🎄 Instant Local Translation with Chrome 🌐
Favicon
8 AI-Powered Chrome Extensions to Save Hours of Manual Work 🧙‍♂️
Favicon
🔅 Adjust Page Brightness: Take Control of Your Screen's Luminosity
Favicon
The Best Chrome Heart Jeans for a Luxe Casual Look
Favicon
FREE: Password Generator ⚡ PRO (extension/add-on) + [source code]
Favicon
Chrome OS Guide to go from Zero to DevOps Hero in a nutshell
Favicon
Avoiding a "Host Permission" Review Delay When Publishing a Chrome Extension
Favicon
How to disconnect WebSocket from Chrome Developer tool
Favicon
How to Choose the Best Free VPN Chrome Extension for Your Needs
Favicon
Chrome Hearts Shirts: A Fusion of Luxury and Streetwear
Favicon
JSON Viewer Plus: The Last JSON Extension You'll Ever Need
Favicon
9 AI-Powered Chrome Extensions to Save Hours of Manual Work 🧙‍♂️🔥
Favicon
How to Run Google Chrome without CORS Error
Favicon
From WHOIS to SSL: How DNS Checker Pro Unveils the Hidden Details of Any Website
Favicon
How to Publish a Chrome Extension
Favicon
How to Launch Google Chrome Without CORS Protection on macOS
Favicon
Exclusive Chrome Hearts Ring: Unique Design for Bold Style
Favicon
Live Captions!
Favicon
Capture Browser XHR/Fetch API Response Automatically into JSON Files
Favicon
Learn Spanish Chrome extension
Favicon
17 Lesser Known Chrome Extensions You Wish You Knew Sooner 🤩⚡
Favicon
Resolving Issues from Unexpected Changes to `container-type: inline-size` in Chrome 129
Favicon
20 Best Chrome Extensions for Developers in 2024
Favicon
Going beyond the classic console.log
Favicon
How to create a Chrome extension
Favicon
Top 10 must have Chrome extension for webdevelopers

Featured ones: