In this article, I will describe how to reuse the code, as well as various tricks that are specific to userjs.
Other articles in the series:
Note.The examples are then purposely simplified, brazenly cluttering up the global namespace, and do not care about security. These issues are covered separately.
Code reuse: we organize loading of scripts
The order of loading userjs in Opera version 10 is determined by the order in which files are issued by the operating system, i.e. generally undefined. It depends on the OS, on the FS driver and the FS itself, as well as on the weather on Mars. Starting with the 10th version of Opera downloads files in alphabetical order, it is already easier, but still not convenient enough.
')
The lack of a guaranteed boot order hinders the normal reuse of the code. Of course, this is not a problem if functions are needed only in event handlers (at this moment all scripts are loaded), but often they are also needed when loading userjs itself. And in this case, each script is forced to pull all the necessary functionality with them and reinvent the wheel.
Instead of the immediate execution of the function, add the function to the array:
if (! ('scripts' in window)) scripts = []; // Array may not be if the script is loaded first.
scripts.push (function () {/ * bla-bla-bla * /});
And the launch of the code will be handled by a special
loader.js script:
if (! ('scripts' in window)) scripts = [];
for (var i = 0; i <scripts.length; ++ i) scripts [i] ();
scripts = {push: function (f) {f (); }};
Pay attention to the last line: scripts loaded after
loader.js will call a new function
push , which will not put the function into an array, but execute it immediately.
As long as the change did not give us anything, the functions are still put in an array in an arbitrary order. However now, having on hands an array of functions entirely, we can operate this order.
For example, you can use a list of dependencies:
scripts.push ({
name: 'example',
requires: ['utils', 'xpath'],
init: function () {/ * bla-bla-bla * /}
});
It should be noted that when loading a script there is no way to know that this script is loaded last. It can be argued that all scripts are already loaded with a signal handler, for example
DOMContentLoaded , but the handler is no longer available advanced features provided by Opera to user scripts, such as
opera.addEventListener ,
defineMagicVariable and others. Therefore, dependency analysis should be performed immediately when adding each new function and the function is executed as soon as all dependencies are satisfied. And in the page event handler, you can already identify functions with unmet dependencies.
In this sense, the output and distribution of the tenth version will help simplify the code - you can add a script with the lexicographically last name, which will be guaranteed to be loaded last and will perform the calculation of dependencies and the launch of all functions.
Data exchange between scripts
As stated in the first article, the global namespace should not be used. But how then can you use the objects of one script from another (and why would you otherwise need to ensure the order of their loading)?
Method one is to simply try to avoid conflicts with scripts on the page using a separate namespace:
unique_script_name.js unique_script_name = {
doIt: function () {...},
dontDoIt: function () {...},
};
some_other_script.js unique_script_name.doIt ();
The second method uses the modified
loader.js , but the data will be completely inaccessible to the scripts from the page:
loader.js :
(function () {
var shared = {};
if (! ('scripts' in window)) scripts = [];
for (var i = 0; i <scripts.length; ++ i) scripts [i] (shared);
scripts = {push: function (f) {f (shared); }};
}) ();
Now each script receives a
shared object as an argument to the
init function. This object is unreachable through the
window object (unless of course any userjs makes it so foolishly).
if (! ('scripts' in window)) scripts = [];
scripts.push (function (shared) {
shared.very_useful_script = {
doIt: function () {...},
};
});
Configuring Scripts
Many userjs require pre-configuration. Typically, the setting is to open the script and edit the values ​​of some variables. But I would like to provide for this a normal user interface. Creating an interface in the browser is not a problem, but where to save the configuration so that the script can get it, while preferably at no extra cost?
If the setting is local for the page, then everything is simple: save the settings in cookies. But for global settings it is not suitable.
One option is to save the configuration to the global repository or files using the tricks described below, but they require loading either extra frames, or a plug-in, or Java on each page. There is a cheaper option - save the configuration as userjs. This can be done either using LiveConnect, or a Java applet (both versions require Java), or simply asking the user to save the data: // link in the scripts folder.
Sample configuration script:
if (! ('scripts' in window)) scripts = [];
scripts.push (function (shared) {
shared.configuration = {
example_script: {timeout: 10; },
another_script: {password: '123'},
};
});
The lightning-fast configuration of the Opera itself is loaded without using any tricks. With the help of
loader.js, you can guarantee the loading of the configuration before running other scripts.
XDM - Cross-domain messaging
XDM is a way to exchange information between documents, which can even be from different domains. Consists of two components:
- The Window.postMessage (str) function to send a string to another window. To send a message to a frame, use iframe.contentWindow.postMessage (str)
- The message event raised when a message is received. The data field of the event object contains a message string, and the source field contains a link to the sending window.
window.addEventListener ('message', function (ev) {
alert ('got message:' + ev.data);
ev.source.postMessage ('got it');
}, true);
iframe.contentWindow.postMessage ('test');
Attention! Naive use of XDM is dangerous. Scripts on the page can receive a link to the iframe and send messages to it, thus obtaining important information from shared storage or performing cross-domain requests. Methods of protection will be described in the following article.
Shared Data Warehouse
Compared to the GreaseMonkey, Opera userjs scripts are devoid of useful
GM_getValue ,
GM_setValue ,
GM_deleteValue functions that allow you to save data in common for all pages of storage. But their functionality can be emulated by using the fact that:
- userjs are executed even on pages that are not loaded due to an error;
- Opera supports message transfer between frames (XDM - Cross-domain messaging);
- Unable to load the page with the address " 0.0.0.0 ".
This trick is called “Opera 0.0.0.0 hack”, although in fact the address can be any other. It is simply not safe to use the correct address. So, the script consists of two parts, one of which is executed on the page with the address “
0.0.0.0 ”, the other on all the others. The first part subscribes to XDM messages, the second part creates a hidden
iframe on the page with the address “
0.0.0.0 ”, sends it messages with commands (get / set / delete) and gets the results. The data itself is stored in cookies page "
0.0.0.0 ".
if (location.href == 'http://0.0.0.0/') {
document.addEventListener ('message', function (evt) {
var a = msg.data.split ('|');
switch (a [0]) {
case 'get': evt.source.postMessage (getCookie (a [1]));
case 'set': setCookie (a [1]);
case 'del': delCookie (a [1]);
}
}, true);
} else {
document.addEventListener ('message', function (evt) {
alert ('got value:' + evt.data);
});
document.addEventListener ('DOMContentLoaded', function () {
var iframe = document.createElement ('iframe');
iframe.style.display = 'none';
iframe.onload = function () {
iframe.contentWindow.postMessage ('set | name | value');
iframe.contentWindow.postMessage ('get | name');
}
iframe.src = 'http://0.0.0.0/';
document.documentElement.appendChild (iframe);
}, true);
}
Cross-domain XMLHttpRequest
You can create a lot of useful scripts if you allow
XMLHttpRequest to make requests to other domains. This spell check, and translation, and auto-completion, and a lot more then. But here, Opera didn’t take pity on users either - userjs have the same “same origin” restriction as the scripts on the page.
However, using the same XDM as for shared storage, you can run
XMLHttpRequest in the context of another domain. I will not give an example, it will be similar to the previous one, but instead of calling
getCookie there will be a call to
XMLHttpRequest .
Trick. In order not to download the contents of the domain in the frame in vain, you can upload "-xmlhttprequest.domain.ru" instead of "domain.ru", the domain name is incorrect, so that the domain cannot even have such a subdomain. Then in the context of the frame you need to run
document.domain = "domain.ru";
and then you can use
XMLHttpRequest .
LiveConnect
LiveConnect is a way to call Java code from JavaScipt. First appeared in Netscape and works in Opera. The
java global object provides access to Java packages and classes.
Code example:
// Find out if the file exists.
var file_exists = (new java.io.File (path)). exists ();
With Java you can do a lot: get access to files (including writing), work with the clipboard, use sockets, etc. Details can be found in the Java documentation.
But by default, all this, of course, is forbidden, otherwise the very first visit to the unreliable site would have ended miserably.
Permissions are written in the Java policy file. The path to it can be found in the "opera: config # Java | SecurityPolicy" setting. I recommend not to modify the existing file, but to copy it and register it in the settings.
Official policy file documentation is on the Sun website.
Example of recording permissions:
grant codeBase "http://some.host.ru/" {
// Allow ALL.
// permission java.security.AllPermission;
// Allow full access to files in the "~ / .opera / userjs / data" folder.
// $ {/} substitutes the system path delimiter: “/” on Unix, “\” on Windows.
// “-” at the end of the path means “all folders and files inside recursively”.
// permission java.io.FilePermission "$ {user.home} $ {/}. opera $ {/} userjs $ {/} data / -", "read, write, delete";
// Allow access to the clipboard.
// permission java.awt.AWTPermission "accessClipboard";
};
Attention! Setting permissions for any path will allow performing not only your userjs scripts, but also scripts from the page. Although it is unlikely that someone will use LiveConnect on their website in the hope of this, it’s better to be paranoid in terms of security. The way to solve this problem is given in the next article.