Pull to refresh

UserJS. Часть 2: Трюки

Reading time 7 min
Views 3.5K
В этой статье я опишу способ переиспользования кода, а также различные трюки, специфичные для userjs.

Другие статьи серии:

Примечание.
Примеры далее нарочно упрощены, нагло захламляют глобальное пространство имен и не заботятся о безопасности. Эти вопросы освещаются отдельно.

Переиспользование кода: упорядочиваем загрузку скриптов


Порядок загрузки userjs в Opera версии до 10 определяется порядком выдачи имен файлов операционной системой, т.е. в общем случае неопределен. Зависит от ОС, от драйвера ФС и самой ФС, а также погоды на Марсе. Начиная с 10-ой версии Opera загружает файлы в алфавитном порядке, уже проще, но все ещё недостаточно удобно.

Отсутствие гарантированного порядка загрузки мешает нормальному переиспользованию кода. Конечно, это не проблема, если функции необходимы только в обработчиках событий (в этот момент загружены уже все скрипты), но часто они нужны и при загрузке самого userjs. А в этом случае каждый скрипт вынужден тянуть всю нужную функциональность с собой и заново изобретать велосипед.

Вместо немедленного выполнения функции добавим функцию в массив:
if (! ('scripts' in window)) scripts = []; // Массива может и не быть, если скрипт загружается первым.
scripts.push(function() { /* bla-bla-bla */ });


А запуском кода будет заниматься специальный скрипт loader.js:
if (! ('scripts' in window)) scripts = [];
for (var i = 0; i < scripts.length; ++i) scripts[i]();
scripts = { push: function(f) { f(); } };

Обратите внимание на последнюю строку: скрипты, загружающиеся после loader.js, вызовут новую функцию push, которая не положит функцию в массив, а выполнит её немедленно.

Пока изменение нам ничего не дало — функции по-прежнему кладутся с массив в произвольном порядке. Однако теперь, имея на руках массив функций целиком, мы можем этим порядком управлять.

Например, можно использовать список зависимостей:
scripts.push({
  name: 'example',
  requires: ['utils', 'xpath'],
  init: function() { /* bla-bla-bla */}
});


Нужно заметить, что при загрузке скрипта нет способа узнать, что этот скрипт загружается последним. Можно утверждать, что все скрипты уже загружены с обработчике сигнала, например DOMContentLoaded, но в обработчике уже недоступны расширенные возможности, предоставляемые Opera пользовательским скриптам, например opera.addEventListener, defineMagicVariable и другие. Поэтому анализ зависимостей нужно производить немедленно при добавлении каждой новой функции и функция выполняется как только все зависимости удовлетворены. А в обработчике события загрузки страницы можно уже выявить функции с неудовлетворенными зависимостями.

В этом смысле выход и распространение десятой версии поможет упростить код — можно будет добавить скрипт с лексикографически последним именем, который будет гарантированно загружаться последним и будет выполнять просчет зависимостей и запуск всех функций.

Обмен данными между скриптами


Как было сказано в первой статье, не следует использовать глобальное пространство имен. Но как же тогда использовать объекты одного скрипта из другого (а зачем иначе нужно обеспечивать порядок их загрузки)?

Способ первый — просто постараться избежать конфликтов со скриптами на странице, используя отдельное пространство имен:

unique_script_name.js
unique_script_name = {
  doIt: function() { ... },
  dontDoIt: function() { ... },
};


some_other_script.js
unique_script_name.doIt();


Второй способ использует модифицированный loader.js, зато данные будут совершенно недоступны скриптам со страницы:

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); } };
})();


Теперь каждый скрипт получает объект shared как аргумент функции init. Этот объект недостижим через объект window (если конечно какой-нибудь userjs не сделает его таковым по глупости).

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


Конфигурирование скриптов


Многие userjs требуют предварительной настройки. Обычно настройка заключается в открытии скрипта и правке значений некоторых переменных. Но хотелось бы предоставить для этого нормальный пользовательский интерфейс. Создать интерфейс в браузере не проблема, но куда сохранить конфигурацию, так чтобы скрипт мог её получить, при этом желательно без особых затрат?

Если настройка локальна для страницы, то все просто: сохраняйте настройки в cookies. Но для глобальных настроек это не подходит.

Один из вариантов — сохранять конфигурацию в глобальное хранилище или файлы с использованием трюков, описанных ниже, но они требуют загрузки либо дополнительных фреймов, либо плагина, либо Java на каждой странице. Есть вариант дешевле — сохранять конфигурацию как userjs. Сделать это можно либо с помощью LiveConnect, либо Java-апплета (оба варианта требуют Java), либо же просто попросив пользователя сохранить data://-ссылку в папку скриптов.

Пример скрипта конфигурации:
if (! ('scripts' in window)) scripts = [];
scripts.push(function(shared) {
  shared.configuration = {
    example_script: { timeout: 10; },
    another_script: { password: '123' },
  };
});


Загружается конфигурация молниеносно самой же Opera без использования каких-либо трюков. С помощью loader.js можно гарантировать загрузку конфигурации до запуска остальных скриптов.

XDM — Cross-domain messaging


XDM — это способ обмена информацией между документами, которые могут быть даже с разных доменов. Состоит из двух компонент:
  • Функция Window.postMessage(str) для отправки строки другому окну. Для отправки сообщения фрейму используйте iframe.contentWindow.postMessage(str)
  • Событие message, вызываемое при получении сообщения. Поле data объекта события содержит строку сообщения, а поле source содержит ссылку на окно-отправитель.


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


Внимание! Наивное использование XDM опасно. Скрипты на странице могут получить ссылку на iframe и отправлять ему сообщения, таким образом получая важную информацию из разделяемого хранилища или выполняя междоменные запросы. Способы защиты будут описаны в следующей статье.

Разделяемое хранилище данных

По сравнению со скриптами GreaseMonkey, Opera userjs лишены полезных функций GM_getValue,GM_setValue, GM_deleteValue, которые позволяют сохранять данные в общем для всех страниц хранилище. Но их функциональность можно эмулировать, воспользовавшись тем, что:
  • userjs выполняются даже на страницах, не загруженных из-за ошибки;
  • Opera поддерживает пересылку сообщений между фреймами (XDM — Cross-domain messaging);
  • невозможно загрузить страницу с адресом «0.0.0.0».

Этот трюк называется «Opera 0.0.0.0 hack», хотя на самом деле адрес может быть и любым другим. Просто небезопасно использовать корректный адрес. Итак, скрипт состоит из двух частей, одна из них выполняется на странице с адресом «0.0.0.0», другая — на всех остальных. Первая часть подписывается на XDM-сообщения, вторая — создает на странице скрытый iframe с адресом «0.0.0.0», отправляет ему сообщения с командами (get/set/delete) и получает результаты. Сами данные хранятся в cookies страницы «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

Очень много полезных скриптов можно создать, если разрешить XMLHttpRequest делать запросы на другие домены. Это и проверка орфографии, и перевод, и автодополнение, и много ещё чего. Но и здесь Opera нисколько не сжалилась над пользователями — userjs имеют то же ограничение «same origin», что и скрипты на странице.

Однако, воспользовавшись все тем же XDM, что и для разделяемого хранилища, можно выполнять XMLHttpRequest в контексте другого домена. Пример приводить не буду, он будет похож на предыдущий, только вместо вызова getCookie будет вызов XMLHttpRequest.

Трюк. Чтобы зря не загружать содержимое домена во фрейме, можно вместо «domain.ru» загружать "-xmlhttprequest.domain.ru", имя домена некорректное специально, чтобы у домена даже случайно не могло быть такого субдомена. Затем в контексте фрейма нужно выполнить
document.domain = "domain.ru";

и после этого можно использовать XMLHttpRequest.

LiveConnect


LiveConnect — это способ вызова Java-кода из JavaScipt. Впервые появился в Netscape и работает в Opera. Глобальный объект java предоставляет доступ к пакетам и классам Java.

Пример кода:
// Узнать, существует ли файл.
var file_exists = (new java.io.File(path)).exists();


С помощью Java можно сделать много: получить доступ к файлам (в том числе на запись), работать с буфером обмена, использовать сокеты и т.д. Подробности можно узнать в документации по Java.

Но по умолчанию все это, конечно же, запрещено, иначе первый же визит на неблагонадежный сайт окончился бы плачевно.
Разрешения записаны в файле политики Java. Путь к нему можно узнать из настройки «opera:config#Java|SecurityPolicy». Рекомендую не модифицировать существующий файл, а скопировать его и прописать в настроках.

Официальная документация по файлу политики есть на сайте Sun.

Пример записи разрешений:

grant codeBase "http://some.host.ru/" {
  // Разрешить ВСЕ.
  //permission java.security.AllPermission;

  // Разрешить полный доступ к файлам в папке «~/.opera/userjs/data».
  // ${/} подставляет системный разделитель пути: «/» на Unix, «\» на Windows.
  // «-» в конце пути означает «все папки и файлы внутри рекурсивно».
  //permission java.io.FilePermission "${user.home}${/}.opera${/}userjs${/}data/-", "read,write,delete";

  // Разрешить доступ к буферу обмена.
  //permission java.awt.AWTPermission "accessClipboard";
};


Внимание! Установка разрешений для какого-либо пути позволит выполнять разрешенные действия не только вашим userjs скриптам, но и скриптам со страницы. Хотя и маловероятно, что кто-то будет использовать LiveConnect на своем сайте в надежде на это, но в плане безопасности лучше быть параноиком. Способ решения этой проблемы приведен в следующей статье.
Tags:
Hubs:
+10
Comments 2
Comments Comments 2

Articles