UserJS. Part 2: Tricks

    In this article, I will describe how to reuse code, as well as various userjs-specific tricks.

    Other articles in the series:

    Note.
    The examples are further deliberately simplified, brazenly cluttering the global namespace and do not care about security. These issues are covered separately.

    Code reuse: streamline script loading


    The order of loading userjs in Opera versions up to 10 is determined by the order in which the operating system displays the file names, i.e. generally undefined. It depends on the OS, the FS driver and the FS itself, as well as the weather on Mars. Starting with the 10th version, Opera uploads files in alphabetical order, already easier, but still not convenient enough.

    The lack of a guaranteed boot order interferes with normal code reuse. Of course, this is not a problem if functions are necessary only in event handlers (at this moment all scripts are already 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 it and reinvent the wheel.

    Instead of immediately executing the function, add the function to the array:
    if (! ('scripts' in window)) scripts = []; // There may not be an array if the script is loaded first.
    scripts.push (function () {/ * bla-bla-bla * /});
    


    A special script loader.js will be involved in the code launch :
    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 the new push function , which will not put the function into an array, but will execute it immediately.

    So far, the change has given us nothing - the functions are still placed with the array in random order. However, now that we have the entire array of functions on hand, we can control this order.

    For example, you can use the 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 find out 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 advanced features provided by Opera to user scripts, for example opera.addEventListener , defineMagicVariable and others , are no longer available in the handler . Therefore, the analysis of dependencies should be done immediately when each new function is added and the function is executed as soon as all the dependencies are satisfied. And in the page load event handler, you can already identify functions with unsatisfied dependencies.

    In this sense, the release 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 launch of all functions.

    Exchange data between scripts


    As mentioned in the first article, you should not use the global namespace. But how then to use the objects of one script from another (and why else do you need to ensure the order of their loading)?

    The first way 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 a modified loader.js , but the data will be completely inaccessible to 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 unattainable through the window object (unless of course some userjs makes it so stupid).

    if (! ('scripts' in window)) scripts = [];
    scripts.push (function (shared) {
      shared.very_useful_script = {
        doIt: function () {...},
      };
    });
    


    Script configuration


    Many userjs require pre-configuration. Typically, the setup 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 without any extra cost?

    If the setting is local to the page, then everything is simple: save the settings in cookies. But for global settings this 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 additional frames, or a plugin, or Java on each page. There is a cheaper option - save the configuration as userjs. You can do this either using LiveConnect, or a Java applet (both options require Java), or simply by asking the user to save data: // - a link to the script folder.

    Example configuration script:
    if (! ('scripts' in window)) scripts = [];
    scripts.push (function (shared) {
      shared.configuration = {
        example_script: {timeout: 10; },
        another_script: {password: '123'},
      };
    });
    


    The configuration is loaded lightning fast with Opera itself without using any tricks. Using loader.js, you can guarantee that the configuration is loaded before the rest of the scripts are run.

    XDM - Cross-domain messaging


    XDM is a way of exchanging information between documents, which can even be from different domains. Consists of two components:
    • 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 sender window.


    window.addEventListener ('message', function (ev) {
      alert ('got message:' + ev.data);
      ev.source.postMessage ('got it');
    }, true);
    iframe.contentWindow.postMessage ('test');
    


    Attention! The naive use of XDM is dangerous. The scripts on the page can get a link to the iframe and send messages to it, thus receiving important information from shared storage or performing cross-domain requests. Security methods will be described in the next article.

    Shared Data Warehouse

    Compared to GreaseMonkey scripts, Opera userjs lack the useful functions GM_getValue , GM_setValue , GM_deleteValue , which allow saving data in a common storage for all pages. But their functionality can be emulated, taking advantage of the fact that:
    • userjs run even on pages not loaded due to an error;
    • Opera supports message forwarding between frames (XDM - Cross-domain messaging);
    • It is not possible 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’s just not safe to use the correct address. So, the script consists of two parts, one of them is executed on the page with the address " 0.0.0.0 ", the other - on all the rest. The first part subscribes to XDM messages, the second - creates a hidden iframe on the page with the address " 0.0.0.0 ", sends him messages with commands (get / set / delete) and receives the results. The data itself is stored in the cookies of the " 0.0.0.0 " page .
    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

    A lot of useful scripts can be created if XMLHttpRequest is allowed to make requests to other domains. This is a spellcheck, and translation, and auto-completion, and much more. But here Opera did not take pity on users at all - userjs have the same “same origin” restriction as the scripts on the page.

    However, using the same XDM as for shared storage, you can execute XMLHttpRequest in the context of another domain. I will not give an example, it will be similar to the previous one, only instead of calling getCookie there will be an XMLHttpRequest call .

    Trick.In order not to waste the content of the domain in the frame, instead of “domain.ru” you can load “-xmlhttprequest.domain.ru”, the domain name is specially incorrect so that the domain could not even have such a subdomain by accident. Then in the context of the frame you need to execute
    document.domain = "domain.ru";
    

    and after that you can use XMLHttpRequest .

    Liveconnect


    LiveConnect is a way to invoke Java code from JavaScipt. First appeared in Netscape and works in Opera. The global java 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: access files (including for writing), work with the clipboard, use sockets, etc. See the Java documentation for more details.

    But by default, all this, of course, is forbidden, otherwise the first visit to an unreliable site would end in failure.
    Permissions are written to the Java policy file. You can find the path to it from the “opera: config # Java | SecurityPolicy” setting. I recommend not to modify the existing file, but copy it and register it in the settings.

    The official documentation for the policy file is on the Sun website.

    Permission entry example:

    grant codeBase "http://some.host.ru/" {
      // Allow ALL.
      // permission java.security.AllPermission;
      // Allow full access to the files in the folder “~ / .opera / userjs / data”.
      // $ {/} substitutes the system path separator: “/” 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 you to perform permitted actions not only for your userjs scripts, but also scripts from the page. Although it is unlikely that anyone will use LiveConnect on their site 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.

    Also popular now: