Inject a JavaScript to predefined pages and use page action - google-chrome-extension

Well I have a list of domains (about 10) which my chrome extension is going to interact with.
As I studied the chrome extensions documentation this needs to use content_scripts
I have included these lines in the manifest.json
"content_scripts": [ {
"all_frames": true,
"js": [ "js/main.js" ],
"matches": [ "http://domain1.com/*",
"http://domain2.com/*",
"http://domain3.com/*",
"http://domain4.com/*",
"http://domain5.com/*",
"http://domain6.com/*",
"http://domain7.com/*",
"http://domain8.com/*",
"http://domain9.com/*",
"http://domain10.com/*"
],
"run_at": "document_start"
}],
This means that during loading every page that the url matches the defined url's in the manifest file, then the main.js will be injected to the page. Am I right? yes.
So I want to do some UI when the script is injected through page action
I included these lines to the manifest:
"page_action": {
"default_icon": "images/pa.png",
"default_title": "This in one of that 10 domains, that is why I showed up!"
},
It seems that it is not enough. and I have to manually trigger the page action.
but where ?
I realized that for this purpose I would need a background.html file.
but Why I can not include the trigger at the same main.js file?
answer:
However, content scripts have some limitations. They **cannot**:
- Use chrome.* APIs (except for parts of chrome.extension)
- Use variables or functions defined by their extension's pages
- Use variables or functions defined by web pages or by other content scripts
So included it in the manifest:
"background_page": "background.html"
and this is the content:
<html>
<head>
<script>
function check (tab_id , data , tab){
//test just one domain to be simple
if (tab.url.indexOf('domain1.com') > -1){
chrome.pageAction.show(tab_id);
};
};
chrome.tabs.onUpdated.addListener(check);
</script>
</head>
</html>
Fair enough until here,
What I want and I don't know is how to add the ability of toggle on/off the extension.
User clicks on the page action icon -> the icon changes and turns off/on (the main.js would act different)

Instead of adding the content script through the manifest, you can also use the chrome.tabs.onUpdated in conjunction with chrome.tabs.executeScript:
// Example:
var url_pattern = /^http:\/\/(domain1|domain2|domain3|etc)\//i;
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
if (url_pattern.test(tab.url)) {
if (changeInfo.status === 'complete') { // Or 'loading'
chrome.tabs.executeScript(tabId, {'file':'main.js'});
chrome.pageAction.show(tabId);
}
} else {
chrome.pageAction.hide(tabId);
}
});
Do not forget to check for the value changeInfo.status, because otherwise, the content script will be executed twice.
In one of these if-statements, you can incorporate a check whether the extension is active or not, and act upon it:
if (changeInfo.status === 'complete' && am_I_active_questionmark) ...
Side not: Instead of using background_page, you can also use "background": {"scripts":["bg.js"]}, and place the background script in bg.js.

Related

Chrome extension content scripts not running on certain sites

I've been attempting to write a very simple Chrome extension (manifest v3) to automatically close those annoying tabs zoom leaves open after you join a meeting.
So far I have been able to get most pages to automatically close with my extension but it simply refuses to run on certain domains, including the one I actually need it to run on: https://company-name-here.zoom.us/. I ultimately would like to set the content script matchers to just zoom but for now I have expanded it to all sites in an effort to reduce sources of error.
It is not working no matter how I attempt to load the page, be it by clicking the redirect url on a google calendar event, reloading the page manually after it has already been opened, and even manually typing out the url and hitting enter. The zoom home page suffers from the same problem but other sites such as stack overflow show the "Content script loaded" console log and close in 5 seconds as I would expect.
Please find the entire source for the extension below:
manifest.json
{
"manifest_version": 3,
"name": "Zoom Auto Closer",
"version": "1.0",
"background": {
"service_worker": "src/background.js"
},
"content_scripts": [{
"run_at": "document_start",
"matches": ["<all_urls>"],
"js": ["src/content.js"]
}]
}
src/content.js
const closeDelay = 5_000;
const closeCurrentTab = () => chrome.runtime.sendMessage('close-tab');
const main = () => {
console.log('Content script loaded');
setTimeout(closeCurrentTab, closeDelay);
};
main();
src/background.js
const closeTab = tabId => chrome.tabs.remove(tabId);
const onMessage = (message, sender) => {
console.log('Received a message:', message);
switch (message) {
case 'close-tab': return closeTab(sender.tab.id);
}
}
const main = () => {
console.log('Service worker registered');
chrome.runtime.onMessage.addListener(onMessage);
}
main();
The issue might be with the usage of <all_urls>.
Google says on the matching patterns docs:
The special pattern <all_urls> matches any URL that starts with a
permitted scheme.
And the permitted schemes are http:, https:, and file:.
I am not too familiar with Zoom, but this article suggests that zoom uses the protocol zoommtg: to launch the the desktop program, so this falls outside of what <all_urls> covers.
Edit:
Now I see that you stated that the urls start with https:// so that might invalidate what I suggested. Still might be worth trying "*://*.zoom.us/*" instead of <all_urls>.
You could try using "*://*.zoom.us/*" instead. If that doesn't work you could try ditching the content script and handling everything in the background service worker.
In the background service worker, you could add a listener for chrome.tabs.onUpdated and check the url value to see if it matches the url for a Zoom tab and close it from there. You would also need to use the Alarms API for the delay.
This method wouldn't be as efficient because it is called on every tab update (not really worth worrying about), but it is a possible workaround if you can't get the content script method to work.

chrome.runtime.onInstalled SOMETIMES not defined in event page

When running in test (reloading an unpacked extension), about 1 out of 5 times my event page's chrome.runtime object does not (yet) have the 'onInstalled' property.
// Cannot read property 'addListener' of undefined
chrome.runtime.onInstalled.addListener(...)
Feels like a race condition on startup within the extension container?
When the error throws, chrome.runtime only has the following:
{OnInstalledReason, OnRestartRequiredReason, PlatformArch,
PlatformNaclArch, PlatformOs, RequestUpdateCheckStatus, connect, sendMessage}
Try moving the event listener to a background script if they are in a content script. Content scripts have limited access to Chrome API. You can define it in the manifest. Then, if needed, you can send it from the background to a content script.
{
"manifest_version": 2,
"name": "someName",
"version": "0.0",
"description": ":D",
"content_scripts": [
{
"matches": ["https://*/*", "http://*/*"],
"js": ["content.js"]
}
],
"background":{
"scripts":["background.js"]
}
}
According to the linked Issue 601559, this was a bug in Chrome that was fixed in Chrome 51 (May 2016).

Content Script pattern patch confusion

I'm trying to write my first chrome extension and I can't get the content script loading correctly. I would like it to load for only the home page of You Tube (ie, https://www.youtube.com/); however, I would not like it to load for any other page, for example, after a user searches (ie, https://www.youtube.com/results?search_query=programming). Here is what I have:
"content_scripts": [
{
"matches": ["*://*.youtube.com/*"],
"exclude_matches": ["*://*.youtube.com/"],
"js": ["jquery.js", "content.js"]
}
]
Using the above code, content.js doesn't load at all; however, if I take out the "exclude_matches", the content script loads on https://www.youtube.com/.
Currently your manifest includes all of the youtube pages except the main page.
The following will include only the main page:
"content_scripts": [
{
"matches": ["*://www.youtube.com/"],
"js": ["jquery.js", "content.js"]
}
]
However Youtube uses history API navigation which means that if the user first opened a video page and then navigated to the main page your content script won't be injected automatically. You will need to use chrome.webNavigation.onHistoryStateUpdated event handler with url filters:
chrome.webNavigation.onHistoryStateUpdated.addListener(
function(details) {
var tabId = details.tabId;
chrome.tabs.executeScript(tabId, {file: "jquery.js", runAt: "document_start"}, function() {
chrome.tabs.executeScript(tabId, {file: "content.js", runAt: "document_start"});
});
},
{
url: [
{urlEquals: "https://www.youtube.com/"}
]
}
);
And you'll probably need a handler to remove the effect of your content scripts when user navigates from the main page. This can be implemented as a pagehide listener in the content script or using (another) onHistoryStateUpdated listener.
Alternatively you can have your scripts on all of the youtube and then check whether current url is of the home page in the content script. This might be useful in case script injection with onHistoryStateUpdated happens too late and you see a delay between navigation and subsequent applying of content scripts.

Modify Iframe of external site from a specific parent - Chrome Extension

Working on a Youtube extension and will like to bring some of it into Facebook,
I'm able to modify the Youtube iframe inside Facebook posts, but the issue is that it's modify it in every site and not only on Facebook.
So I would like to know how can I set the specific parent window please?
I hope there is a way to set it simply in the manifest file,
otherwise I can just use JS to check for location.href as in Facebook it returns:
https://s-static.ak.facebook.com/common/referer_frame.php
Currently in my manifest file:
"content_scripts": [
{
"matches": [
"*://*.facebook.com/*",
"*://*.youtube.com/embed/*"
],
"css": ["styles/facebook.css"],
"all_frames": true
}
]
You can easily find the domain of the parent frame via location.ancestorOrigins, even across different domains. E.g, use the following manifest file:
"content_scripts": [{
"matches": [
"*://*.youtube.com/embed/*"
],
"js": ["js/facebook.js"],
"all_frames": true
}],
"web_accessible_resources": ["styles/facebook.css"],
and the following JS:
// Note: parentOrigin could be `undefined` in the top-level frame.
var parentOrigin = location.ancestorOrigins[0];
if (parentOrigin === 'https://facebook.com' ||
parentOrigin === 'http://facebook.com') {
var style = document.createElement('link');
style.rel = 'stylesheet';
// NOTE: This only works because the file is declared at the
// web_accessible_resources list in manifest.json
style.href = chrome.runtime.getURL('styles/facebook.css');
(document.head || document.documentElement).appendChild(style);
}
If the YouTube video is embedded via the Iframe API, you could also try to insert the style in the frame by matching the URL. E.g., without any JavaScript, the style can be loaded in the YouTube frame using:
"content_scripts": [{
"matches": [
"*://*.youtube.com/embed/*origin=https://facebook.com/*",
"*://*.youtube.com/embed/*origin=http://facebook.com/*"
],
"css": ["styles/facebook.css"],
"all_frames": true
}]
If you inject a content script both into the iframe and the parent frame, you can (using the background page as a message "router") ask the outer script.
Of use: content scripts can learn their place in the frame hierarchy.
So, the logic would be:
Youtube content script checks its hierarchy, obtaining its "index" on the tree, and computes the index of its parent.
CS messages the background with the index of its parent, requesting a check.
Background page gets the tab ID from the message, and messages all frames in the target tab with the request and parent index.
All content scripts that receive the message check their index. If it matches the parent frame, the content script checks its URL and reports back.
The background page routes the answer back to the original content script.
Tell me if you need help with any of those steps.

Chrome extension that focuses items in elements panel

I am trying to develop a chrome extension that among other things, will be able to focus an element in the elements panel of the chrome devtools.
I have been pulling my hair out trying to get this to work today but have had no luck so far.
I think part of the key to cracking what I need is here
Here are the main differences between the eval() and
chrome.tabs.executeScript() methods:
The eval() method does not use an isolated world for the code being evaluated, so the JavaScript state of the inspected window is
accessible to the code. Use this method when access to the JavaScript
state of the inspected page is required.
The execution context of the code being evaluated includes the Developer Tools console API. For example, the code can use inspect()
and $0.
The evaluated code may return a value that is passed to the extension callback. The returned value has to be a valid JSON object
(it may contain only primitive JavaScript types and acyclic references
to other JSON objects). Please observe extra care while processing the
data received from the inspected page — the execution context is
essentially controlled by the inspected page; a malicious page may
affect the data being returned to the extension.
But I cannot find the correct place to send the message to or execute the command in order for this to work I am just repeatedly told the following:
Error in event handler for 'undefined': $ is not defined
ReferenceError: $ is not defined
at Object.ftDev.processMsg (chrome-extension://ffhninlgmdgkjlibihgejadgekgchcmd/ftDev.js:39:31)
at chrome-extension://ffhninlgmdgkjlibihgejadgekgchcmd/ftDev.js:16:7
at chrome.Event.dispatchToListener (event_bindings:387:21)
at chrome.Event.dispatch_ (event_bindings:373:27)
at chrome.Event.dispatch (event_bindings:393:17)
at miscellaneous_bindings:166:35
at chrome.Event.dispatchToListener (event_bindings:387:21)
at chrome.Event.dispatch_ (event_bindings:373:27)
at chrome.Event.dispatch (event_bindings:393:17)
at Object.chromeHidden.Port.dispatchOnMessage (miscellaneous_bindings:254:22) event_bindings:377
chrome.Event.dispatch_
Ideally I would like to use the inspect() method of the chrome console not the $() method.
manifest.json
{
"name": "XXXXX Ad and Spotlight Debugger",
"version": "0.1",
"manifest_version": 2,
"description": "A tool to help you identify and debug XXXXXX ads and spotlights in Chrome",
"devtools_page": "ftDev.html",
"browser_action": {
"default_icon": "icon.png",
"default_popup": "popup.html",
"default_title": "XXXXXX Debug Tool"
},
"background": {
"persistent": false,
"page": "background.html",
"js": ["background.js"]
},
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["getFTContent.js"],
"all_frames": true
}],
"permissions": ["tabs","cookies","<all_urls>","devtools"]
}
Then there is similar code in the popup.js, background.js and devtools.js file that essentially boils down to this:
processMsg: function(request, sender, sendResponse) {
switch(request.type) {
case "inspect":
$(request.msg);
sendResponse(request.msg + "successfully inspected");
break;
default:
break;
}
} /*other cases removed for sake of brevity*/
Which when executed results in the error above. I am sure that I am trying to execute the command in the wrong context but I can't figure out how to apply it. In the popup.js file I have also tried executing the $ method as below
chrome.tabs.executeScript(tabId, {code: 'function(){$("#htmlID");}'}, function(){});
Any ideas help would be amazing I can supply more of my code if you think it's necessary but I think this pretty much sums up the problem.
Ok - so I had a look around at the font changer thing and it still wasn't quite what I was looking for in the end but then I had a Eureka moment when I was looking over this page for about the 15th time and realised that I had somehow missed the most important part on the page (at least in order to do what I wanted) which was this method
chrome.devtools.inspectedWindow.eval("string to evaluate", callBack)
It is noted that isn't necessarily a good idea for security reasons as it it doesn't run the code in the isolated world.
Anyway - if I run this code from my devtools' page js-code with the following
chrome.devtools.inspectedWindow.eval("inspect(*id_of_the_div_i_want_inspect*)")
Then it will select this item in the elements page of the devtools... it also made me extremely happy!
:D
I don't know if anyone else will ever need/want this but it too me a long(ish) time to figure it out so I hope it helps other people in future.
You can easily highlight DOM elements in any tab, where your content script is injected. As an example, look at Font Selector extension.
As the source code is available (and thoroughly explained) I'll not post it here. The effect you'll see is that after clicking the browser action button every DOM element under mouse cursor becomes highlighed with red border.
If you want to send some info about selected/highlighted element from the tab into your background page, you can do it via messaging (you have already used it, as I see).

Resources