📜 ⬆️ ⬇️

The story of one Google Chrome extension

One fine day, a couple of hours before the end of the work, the task comes to me: “We need to write a browser application, which should send the data from the page to the client’s website by clicking the user. What application and browser is completely your choice ... ".

After a bit of thinking, I came up with the google chrome extension option:


')
When my thoughts subsided a little, the first thing I did was to introduce the Google Chrome extension into the search for the harb. Having seen an extensive version of articles on this topic, I with peace of mind went home completely confident that after reading them in the morning, by the end of the working day the matter would be ' in the hat ' (how wrong I was then). After reading a couple of them, I had a general idea of ​​how this works, but this turned out to be not enough for the realization of my ideas. Well, let's get started ...

Open google chrome, enter chrome: // extensions , tick Developer mode, click the Load unpacked extension button, select the folder and click Ok.



In the beginning was the word manifest. Below you can see the contents of this file (manifest.json is a mandatory name of the manifest file)

manifest.json
{ "manifest_version": 2, "name": "My application", //     "version": "0.9", "icons": { "16": "./16x16.png", "32": "./32x32.png", "48": "./48x48.png", "128": "./128x128.png" }, "permissions": [ "tabs", "http://*/*", "https://*/*" ], "background" : { "page": "background.html" }, "content_scripts":[{ "matches": [ "http://*/*", "https://*/*" ], "js": [ "script_in_content.js" ] }], "browser_action": { "default_title": "Application", "default_icon" : "./16x16.png" // "default_popup": "login.html" //   html- ,       ,    JS   html  } } 


manifest_version - currently 2 is required.
version - the version of your extension, may contain only numbers and `.` (those. '2.1.12', '0.59', etc.)
icons is a list of all the icons that will be displayed in the browser in various places (16 - in the address bar, 48 - in the list of all extensions, etc.)
permissions - an array with permissions is listed here, I only needed tabs.http and https needed for ajax exchange with any sites, and also so that script_in_content.js could exchange data with the background page - background.html.
background is the name of the background page. The background page is an important element, although for some applications it is not necessary. Why is it needed a little later?
content_scripts - here it says that the file script_in_content.js will be automatically loaded for the page opened in the tab. The page should be open from http: // * / * sites, all sites with http, but not https, although you could specify them.
browser_action - there are 2 display icons for the extension: browser_action and page_action

page_action says that the extension is individual for each tab, that is, the icon will be displayed in the address bar. This icon can be hidden / displayed using JS depending on the circumstances.

On the contrary, browser_action is not considered individual and is displayed not in the address bar, but in the panel for extensions. This icon can not be hidden on JS (but you can block it), it is displayed constantly. Browser_action has one advantage over page_action , on top of browser_action icon you can write a couple of beautiful characters (I only have 4).



I chose browser_action , as I need to work not with one site, but with several. And yes, drawing beautiful characters on the icon.
Here is what Google says about this:

Do not have a few pages.
Do not use this page. Use browser actions instead.


And so, what will our application do; I will say right away, the application that will be described later is a small part of what was done for the client. When the manager visits the site hantim.ru to view information about the contract / vacancy, the application parses the html code of the page and finds the information (vacancy, city, etc.). When you click on the extension icon - a login form is displayed where the manager enters his data, and then he can add the selected job / contract to his profile on the corporate website.

Now how it all works. Google provides us with this picture:


1) Inspected window is what we opened in the tab, content scripts is our script_in_content.js, it has full access to the DOM page.
2) Background page is the heart of the application, in our case this is background.html.
3) DevTools page is what will be displayed when you click on the extension icon (login.html or find.html in our case).

The only thing that confuses me in this picture is the connection between the DevTools page and the Inspected window . I did not find a solution to transfer data from one area to another. But if you set the Background Page as an intermediary, and transfer the data through it, then everything will work.

And so, it's time for the code. Let's start with the invisible side.

background.html
 <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <script type="text/javascript" src="lib.js"></script> <script type="text/javascript" src="bg.js"></script> </head> <body></body> </html> 


I hope this questions should not arise. One note: "background.html - downloads only once for the entire time of the browser, and when it starts." Here you can see that we load 2 js files (lib.js is a set of functions, bg.js is the 'head' of the application).

bg.js
 /** * OnLoad function * * @return void */ window.onload = function(){ // tmp storage window.bg = new bgObj(); // some variables !!! important window.bg.api_site_host = 'http://katran.by'; // get all graber hosts: !!!once!!! new Ajax({ url: window.bg.api_site_host+'/regexp.php', response: 'json', async: false, onComplete: function(data){ if(data && data.status && (data.status === 'ok')) window.bg.grabber_hosts = data.data; } }).send(); // set handler to tabs chrome.tabs.onActivated.addListener(function(info) { window.bg.onActivated(info); }); // set handler to tabs: need for seng objects chrome.extension.onConnect.addListener(function(port){ port.onMessage.addListener(factory); }); // set handler to extention on icon click chrome.browserAction.onClicked.addListener(function(tab) { window.bg.onClicked(tab); }); // set handler to tabs chrome.tabs.onUpdated.addListener(function(id, info, tab) { // if tab load if (info && info.status && (info.status.toLowerCase() === 'complete')){ // if user open empty tab or ftp protocol and etc. if(!id || !tab || !tab.url || (tab.url.indexOf('http:') == -1)) return 0; // save tab info if need window.bg.push(tab); // connect with new tab, and save object var port = chrome.tabs.connect(id); window.bg.tabs[id].port_info = port; // run function in popup.html chrome.tabs.executeScript(id, {code:"initialization()"}); // send id, hosts and others information into popup.js window.bg.tabs[id].port_info.postMessage({method:'setTabId', data:id}); window.bg.tabs[id].port_info.postMessage({method:'setHosts', data:window.bg.grabber_hosts}); window.bg.tabs[id].port_info.postMessage({method:'run'}); // if user is logged into application set find.html popup if(window.bg.user.id) chrome.browserAction.setPopup({popup: "find.html"}); }; }); window.bg.onAppReady(); }; /** * Functino will be called when popup.js send some data by port interface * * @return void */ function factory(obj){ if(obj && obj.method){ if(obj.data) window.bg[obj.method](obj.data); else window.bg[obj.method](); } } /** * Popup object * * @version 2013-10-11 * @return Object */ window.bgObj = function(){ }; /** * Pablic methods */ window.bgObj.prototype = { /** * some internal params */ tabs: {}, user: {}, popup_dom: {}, active_tab: {}, grabber_hosts: {}, done_urls: [], /** * init() function */ onAppReady: function() { // if user not logged into application set login.html popup chrome.browserAction.setPopup({popup: "login.html"}); }, /** * Function add tab into $tabs object, if need */ push: function(tab) { if(tab.id && (tab.id != 0)){ if(!this.tabs[tab.id]) this.tabs[tab.id] = {tab_obj:tab}; } }, /** * Function will be called from popup.js */ mustParsed: function(data) { if(this.tabs[data.tab_id]){ var id = data.tab_id; this.tabs[id].must_parsed = data.find; // run parser in popup.js, if need if(this.tabs[id].must_parsed && (this.tabs[id].must_parsed === true)) this.tabs[id].port_info.postMessage({method:'parsePage'}); } }, /** * Function will be called from popup.js */ matchesCount: function(data) { if(data.tab_id && this.tabs[data.tab_id]){ var id = data.tab_id; this.tabs[id].matches = data.matches; this.tabs[id].matches_count = this.tabs[id].matches.length+''; if(this.tabs[id].matches_count && this.tabs[id].matches_count != '0'){ chrome.browserAction.setBadgeText({text: this.tabs[id].matches_count}); return 0; } } // show default text chrome.browserAction.setBadgeText({text:''}); }, /** * Function will be called when user change active tab */ onActivated: function(info) { // set active tab this.active_tab = info; var data = {}; data.matches = []; if(info.tabId){ data.tab_id = info.tabId; if(!this.tabs[data.tab_id]) this.tabs[data.tab_id] = {}; if(!this.tabs[data.tab_id].matches) this.tabs[data.tab_id].matches = []; data.matches = this.tabs[data.tab_id].matches; } // set actual count of matches for current tab this.matchesCount(data); // if user is logged into application set find.html popup if(this.user.id) chrome.browserAction.setPopup({popup: "find.html"}); }, /** * Function will be called when user click on extension icon */ onClicked: function(tab) { alert(' .     .'); return 0; }, /** * Function will be called from login.js */ loginUser: function(user_data) { var self = this; var json_data = false; // get all graber hosts: !!!once!!! new Ajax({ url: window.bg.api_site_host+'/login.php?user='+encodeURIComponent(JSON.stringify(user_data)), method: 'post', response: 'json', async: false, onComplete: function(data){ if(data && data.status){ // if login - ok if(data.status === 'ok') self.user = data.data; json_data = data; } } }).send(); // return value for login.js return json_data; }, /** * Function will be called from login.js and others places */ setPopup: function(popup_file) { chrome.browserAction.setPopup({tabId: this.active_tab.tabId, popup: popup_file}); }, /** * Function will be called from find.js and others places */ getMatches: function() { // init if need if(!this.tabs[this.active_tab.tabId]) this.tabs[this.active_tab.tabId] = {}; if(!this.tabs[this.active_tab.tabId].matches) this.tabs[this.active_tab.tabId].matches = []; // if user alredy send this url - remove for(var i = 0, cnt = this.tabs[this.active_tab.tabId].matches.length; i < cnt; i++){ for(var j = 0, len = this.done_urls.length; j < len; j++){ if(this.tabs[this.active_tab.tabId].matches[i].url === this.done_urls[j]){ this.tabs[this.active_tab.tabId].matches[i].url = ''; break; } } } return this.tabs[this.active_tab.tabId].matches; }, /** * Function will be called from find.js and others places */ addUrlToGrabber: function(url) { // if $url == '' - already used if(json_data.status && (json_data.status === 'ok')){ var matches = this.tabs[this.active_tab.tabId].matches; for(var i = 0, cnt = matches.length; i < cnt; i++){ if(matches[i].url && (matches[i].url === url)) matches[i].url = ''; this.done_urls.push(url); } } // return value for login.js return json_data; }, /** * Empty method */ empty: function() { } } 


First of all, we wait for window.onload , then send a request to katran.by (get json data, from which site and which RegExp we give the necessary data), then hang handler's on the browser tabs (for this we specified in the manifest permissions ~ tabs).

  chrome.tabs.onActivated.addListener(function(info) { window.bg.onActivated(info); }); 


onActivated - occurs when the user has moved to a new tab (by clicking or by alt + tab).

  chrome.tabs.onUpdated.addListener(function(id, info, tab) { ..... }); 


onUpdated - occurs when the page is full (not only the DOM loaded, but all the images) loaded in the tab.

  chrome.browserAction.onClicked.addListener(function(tab) { window.bg.onClicked(tab); }); 


onClicked - occurs when the user clicks on the application icon. A small note, if during the click the default_popup is set, then the onClicked handler will not start. default_popup is an html page that will be displayed after clicking on the extension icon. default_popup can be set in the manifest, as well as using chrome.browserAction.setPopup ({popup: “find.html”}); or chrome.pageAction.setPopup ({popup: "find.html"});

  chrome.extension.onConnect.addListener(function(port){ port.onMessage.addListener(factory); }); 


This dark magic construction is needed to receive data sent from script_in_content.js using port .
Data processing is engaged in factory (obj)

 function factory(obj){ if(obj && obj.method){ if(obj.data) window.bg[obj.method](obj.data); else window.bg[obj.method](); } } 


What happens when a user loads a tab, and the following happens:



There are 2 ways to transfer data from background.html to script_in_content.js :
  1. hrome.tabs.executeScript (integer tabId, InjectDetails details, function callback) - one thing, but you can only transfer data as a string (not an object, not an array)
  2. hrome.tabs.sendMessage (integer tabId, any message, function responseCallback) - so you can transfer anything, though you will need additional settings


And so, we sent the data to script_in_content.js, then it is time to review its code.

script_in_content.js
 // set handler to tabs: need for seng objects to backgroung.js chrome.extension.onConnect.addListener(function(port){ port.onMessage.addListener(factory); }); /** * Function remove spaces in begin and end of string * * @version 2012-11-05 * @param string str * @return string */ function trim(str) { return String(str).replace(/^\s+|\s+$/g, ''); } /** * Functino will be called from background.js * * @return void */ function initialization(){ window.popup = new popupObj(); } /** * Functino will be called when background.js send some data by port interface * * @return void */ function factory(obj){ if(obj && obj.method){ if(obj.data) window.popup[obj.method](obj.data); else window.popup[obj.method](); } } /** * Popup object * * @version 2013-10-11 * @return Object */ window.popupObj = function(){ }; /** * Pablic methods */ window.popupObj.prototype = { /** * some internal params */ available_hosts: [], total_host: null, matches: [], tab_id: null, port: null, cars: [], /** * Function will be called from bg.js */ setHosts: function(hosts) { this.available_hosts = hosts; }, /** * Function will be called from bg.js */ setTabId: function(id) { this.tab_id = id; }, /** * Function check total host */ run: function() { // get total host if(document.location.host && (document.location.host != '')) this.total_host = document.location.host; else if(document.location.hostname && (document.location.hostname != '')) this.total_host = document.location.hostname; if(!this.total_host || (this.total_host === '')) return 0; var find = false; // if total host in array $available_hosts - parse page for finde cars for (host in this.available_hosts) { if(this.total_host.indexOf(host) != -1){ this.total_host = host; find = true; break; } }; // create connection to backgroung.html and send request this.port = chrome.extension.connect(); this.port.postMessage({method:'mustParsed', data:{tab_id:this.tab_id, find:find}}); }, /** * Function will be called from bg.js * Parse page */ parsePage: function() { // reset variable before parse this.matches = []; if(!this.available_hosts[this.total_host]) return 0; var html = window.document.body.innerHTML; var reg_exp = this.available_hosts[this.total_host]; var matches = {}; var match = []; var find = false; for(var i = 0, len = reg_exp.length; i < len; i++) { var exp = new RegExp(reg_exp[i].reg_exp, reg_exp[i].flag); match = exp.exec(html); if(match && match.length && reg_exp[i].index){ matches[reg_exp[i].field] = trim(match[reg_exp[i].index]); find = true; } else if(match && match.length){ matches[reg_exp[i].field] = match; find = true; } } // this url will be send to site if(find === true){ matches.url = document.location.href; this.matches.push(matches); } // send count of matches this.port.postMessage({method:'matchesCount', data:{tab_id:this.tab_id, matches: this.matches}}); } } 


The first thing that catches your eye is receiving data from background.html, as you can see, it is the same as in bg.js:

 chrome.extension.onConnect.addListener(function(port){ port.onMessage.addListener(factory); }); 


As you remember, earlier in bg.js we started initialization() , setTabId() , setHosts() and run() . Of most interest is window.popup.run (). There, the domain name of the server of the open page is checked, and if this name matches the list of sites that interest us (data from which must be transferred to the corporate resource) - find = true; and send the request window.bg.mustParsed(obj) to bg.js.

  /** * Function will be called from script_in_content.js */ mustParsed: function(data) { if(this.tabs[data.tab_id]){ var id = data.tab_id; this.tabs[id].must_parsed = data.find; // run parser in popup.js, if need if(this.tabs[id].must_parsed && (this.tabs[id].must_parsed === true)) this.tabs[id].port_info.postMessage({method:'parsePage'}); } } 


If a domain match was found, then start the parsePage() page in script_in_content.js .

  /** * Function will be called from bg.js * Parse page */ parsePage: function() { // reset variable before parse this.matches = []; if(!this.available_hosts[this.total_host]) return 0; var html = window.document.body.innerHTML; var reg_exp = this.available_hosts[this.total_host]; var matches = {}; var match = []; var find = false; for(var i = 0, len = reg_exp.length; i < len; i++) { var exp = new RegExp(reg_exp[i].reg_exp, reg_exp[i].flag); match = exp.exec(html); if(match && match.length && reg_exp[i].index){ matches[reg_exp[i].field] = trim(match[reg_exp[i].index]); find = true; } else if(match && match.length){ matches[reg_exp[i].field] = match; find = true; } } // this url will be send to site if(find === true){ matches.url = document.location.href; this.matches.push(matches); } // send count of matches this.port.postMessage({method:'matchesCount', data:{tab_id:this.tab_id, matches: this.matches}}); } 


If the script found something on the page, then all that it found adds to the array, adds the current url of the page to it and sends it back to bg.js , saying: “Look what I found ...”. In response to this, bg.js analyzes the input data, and if RegExp has found something, it writes the number of matches (1, 2, etc.) chrome.browserAction.setBadgeText({text: this.tabs[id].matches_count}); .

This is like all the highlights of the bg.js bundle and script_in_content.js .
Now let's talk about popup . When a user clicks on an application icon, the login.html form is displayed.
The manager enters his data from the corporate site, clicks Login, and then the following happens:

login.html
 <!DOCTYPE html> <html> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <script type="text/javascript" src="login.js"></script> <link type="text/css" rel="stylesheet" href="login.css"> <title>Grabber popup</title> </head> <body> <div class="body"> <div class="emptyLogin"> <div id="error_message"> </div> <form name="login_form" action="" method="get" id="popup_login_form"> <table> <tbody> <tr> <td align="right"> E-mail:</td> <td><input type="text" name="login" value="" tabindex="1"></td> </tr> <tr> <td align="right">:</td> <td><input type="password" name="pass" value="" tabindex="2"></td> </tr> <tr> <td colspan="2" align="center"><input type="submit" value="Login" class="button"></td> </tr> </tbody> </table> </form> </div> <div id="loader"><img src="ajax-loader.gif" title="Loding" alt="Loading"></div> </div> </body> </html> 


login.js
 /** * OnLoad function * * @return void */ window.onload = function(){ // set some events handlers document.getElementById('popup_login_form').onsubmit = function(obj){ // fade popup document.getElementById('loader').style.display = 'block'; document.getElementById('error_message').innerHTML = ' '; if(obj.target.elements && obj.target.elements.length && (obj.target.elements.length === 3)){ var data = {}; data.login = obj.target.elements[0].value; data.pass = obj.target.elements[1].value; setTimeout(function(){ var bg_wnd = chrome.extension.getBackgroundPage(); var result = bg_wnd.bg.loginUser(data); if(result && result.status && (result.status === 'error')) document.getElementById('error_message').innerHTML = result.mess; else{ // set new popup html code and close popup window bg_wnd.bg.setPopup('find.html'); window.close(); } // hide fade on popup document.getElementById('loader').style.display = 'none'; }, 500); } return false; }; } 


The task of login.js is to hang onsubmit on the form, and send the login / password to background.html (bg.js) ,
This is done using the following construction (as you will see, we can directly call the methods of the bg.js object):

  var bg_wnd = chrome.extension.getBackgroundPage(); var result = bg_wnd.bg.loginUser(data); 


bg_wnd.bg.loginUser(data) sends data to the server, if everything is fine, popup login.html is replaced by find.html ,
and user data is stored in a variable. Change popup happens as follows:

  /** * Function will be called from login.js and others places */ setPopup: function(popup_file) { chrome.browserAction.setPopup({tabId: this.active_tab.tabId, popup: popup_file}); }, 


A small note, if the user opened the popup login.html, put the cursor in the 'Your E-mail:' field and presses TAB (the first time) in the hope of passing to the password, then it will be disappointed, the focus will not change. This bug is still relevant.

So, it remains quite a bit.
After we have successfully logged in, we change the popup to find.html .

find.html
 <!DOCTYPE html> <html> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <script type="text/javascript" src="find.js"></script> <link type="text/css" rel="stylesheet" href="find.css"> <title>Grabber</title> </head> <body> <div class="body"> <div class="carsRows" id="popup_cars_rows"> <h3 style="text-align: center; margin: 5px 0;">  </h3> <form name="cars_form" action="" method="get" id="popup_cars_form"> <table id="popup_cars_table"> <thead> <tr> <th class="make"></th> <th class="info"></th> <th class="addBtn"> </th> </tr> </thead> <tbody> </tbody> </table> </form> </div> <div class="carsRows" id="popup_cars_rows_none" style="display: none;"> <h3 style="text-align: center; margin: 5px 0;">    </h3> </div> <div id="loader"><img src="ajax-loader.gif" title="Loding" alt="Loading"></div> </div> </body> </html> 


find.js
 /** * OnLoad function * * @return void */ window.onload = function(){ // set new popup html code and close popup window window.bg_wnd = chrome.extension.getBackgroundPage(); var rows = window.bg_wnd.bg.getMatches(); // function render popup renderPopup(rows); } /** * Function set cars into html * * @param array $rows * @return void */ function renderPopup(rows) { if(rows.length === 0){ document.getElementById('popup_cars_rows').style.display = 'none'; document.getElementById('popup_cars_rows_none').style.display = 'block'; return 0; } else{ document.getElementById('popup_cars_rows').style.display = 'block'; document.getElementById('popup_cars_rows_none').style.display = 'none'; } for (var i = 0, cnt = rows.length; i < cnt; i++) renderRow(rows[i]); } /** * Function set cars into html * * @param object $row * @return void */ function renderRow(row) { var tbl = document.getElementById('popup_cars_table').children[1]; // add divided row var td = tbl.insertRow(-1).insertCell(-1); td.setAttribute('colspan', '3'); td.innerHTML = '<hr style="border: 1px solid #909090; width: 75%">'; var tr = tbl.insertRow(-1); var td1 = tr.insertCell(-1); var td2 = tr.insertCell(-1); var td3 = tr.insertCell(-1); var vacancy = []; var city = []; var hash = { vacancy: '', city: '', } var table_row = []; for(key in row){ if(hash[key]){ if(key == 'vacancy') vacancy.push(row[key]); if(key == 'city') city.push(row[key]); } } td1.innerHTML = vacancy.join(' ');; td2.innerHTML = city.join(' '); td3.innerHTML = (row.url === '')?'<b><em></em></b>':'<input type="button" value="" name="cars[]" class="button"><input type="hidden" value="'+row.url+'" name="url[]">'; td3.children[0].addEventListener('click', function(){addToGrabber(event)}, false); } function addToGrabber(e) { // hide fade on popup document.getElementById('loader').getElementsByTagName('img')[0].style.marginTop = (window.innerHeight/2-10)+'px'; document.getElementById('loader').style.display = 'block'; if(e && e.srcElement){ var url = e.srcElement.parentNode.children[1].value; setTimeout(function(){ var result = window.bg_wnd.bg.addUrlToGrabber(url); e.srcElement.parentNode.innerHTML = '<b><em></em></b>'; // hide fade on popup document.getElementById('loader').style.display = 'none'; }, 500); } } 


As soon as find.html is loaded, find.js comes into play . His task is to ask bg.js: 'What do you have on the current page' - and display what bg.js gave.

 /** * OnLoad function * * @return void */ window.onload = function(){ // set new popup html code and close popup window window.bg_wnd = chrome.extension.getBackgroundPage(); var rows = window.bg_wnd.bg.getMatches(); // function render popup renderPopup(rows); } 


It looks like a turnkey solution.



With the 'Add' button, I think you will understand how it works. Lastly, I want to say how this whole thing is being debugged.
background.html - to see how the bg.js and lib.js scripts work , you need to click on the background.html link on the chrome: // extensions page.

script_in_content.js - it is executed in the context of the page, so you can safely inspect the page and watch the console with the output of errors in it.
login.html and find.html - in order to display the Developer Tools, you need to click on the application icon and select the page inspection by right-clicking the mouse.



Ps. All JavaScript should be in js files, if you paste it into html - chrome will swear.
Also a couple of links:
to documentation: manifest.json , Chrome's API
on github.com: source code

Pss. From the words x256 corrected the introduction.

Source: https://habr.com/ru/post/198778/


All Articles