В Хроме и Хромиуме уже 2.5 года существует баг отсутствия кроссдоменного доступа к другому фрейму из контекстного скрипта (юзерскрипта). То, что нормально работает в скрипте обычной страницы, например, межсайтовая передача данных с помощью postMessage и что без проблем работает в других браузерах, в Хроме иногда считается «ограничением безопасности», но на самом деле это обычный и признанный баг, отмеченный с 4-й версии.
Известно, что подобные проблемы решаются в расширениях Хрома, когда им пропишут права доступа, но суть вопроса в том, что дополнительных прав доступа для таких рядовых задач не требуется, нужен всего лишь обход бага в одном браузере. И тогда мы одним файлом можем записать расширение, работающее во всех браузерах, поддерживающих юзерскрипты. Пример такой задачи, не решаемой обычными скриптами — получение данных о числе «лайков» из кнопки Google Plus (без специальной авторизации в Google Apps и без серверных технологий). Для такой и подобных задач, в которых «вендор» не предоставляет API, нужен (необходим) юзерскрипт, и он не обязан быть индивидуальным для каждого браузера, как этого требуют расширения.
Пример получения числа «лайков» из кнопки Google+ в юзерскрипте, работающий во всех браузерах (кроме IE), имеется в HabrAjax. Там значение «лайков» читается внутри фрейма внедрённым скриптом и выносится наружу для более компактного отобращения прямо на поверхности кнопки. Боковая сноска для 4-5-значного числа шириной около 50 пикс. скрывается стилями, чем сильно экономится место на кнопку. Ниже — скриншот.
Как получают данные из кроссдоменного фрейма в остальных браузерах
Данные получают достаточно просто, используя метод postMessage, поддерживаемый в Firefox 3+, IE8+, Safari4+, Opera 9.5+, Chrome1+. Для более старых браузеров пользуются хаками типа Iframe hash, Iframe name. В окне-приёмнике ставят слушатель события "message":
window.addEventListener("message", /*Function*/receiveMessage, false);
В окне- (или фрейме-) источнике выполняют отправку текстового сообщения:
(target_window).postMessage(/*Srting*/data, /*Srting*/domainTarget);
Контекст источника должен указывать на целевое окно, а во втором аргументе рекомендуется указывать явно в целях безопасности домен приёмника, а не писать строку, содержащую звёздочку (в смысле, «любой домен»). Тут-то в Хроме в контекстных скриптах всплывает баг — браузер должен обеспечить понимание объекта (целевое окно).postMessage, и в обычных скриптах в нём так и происходит. Для юзерскриптов у Хрома возникает баг неопределённости объекта (целевое окно).postMessage, если окно содержит другой домен — иной, чем источник сообщения. Поэтому к достаточно стройной схеме передачи сообщения требуется «костыль».
Дополнение процедуры кроссдоменной передачи для Хрома
На стороне приёмника — никаких дополнений, потому что проблема не в нём. В источнике используем загрузку скрипта в окружение window.
Во внедрённом в чужой фрейм скрипте читаем, ожидая появления, нужные нам данные.
if(gPlusFrame){
/**
* check occurrence of third-party event with growing interval
* @constructor
* @param{Number} t start period of check
* @param{Number} i number of checks
* @param{Number} m multiplier of period increment
* @param{Function} checkOccur event handler
* @param{Function} check event condition
*/
var Tout = function(h){
var th = this;
(function(){
if((h.dat = h.check() )) //data place
h.occur();
else
if(h.i-- >0)
th.ww = setTimeout(arguments.callee, (h.t *= h.m) );
})();
};
new Tout({t:320, i:6, m: 1.6
,occur: function(){
var id = location.hash.match(/(?|#|&)id=([^&]+)/) //frame id [or name]
, w = win;
id = id && id.length && id[2];
var s = w.JSON && w.JSON.stringify && w.JSON.stringify( //must supported earlier
{likes: this.dat.innerHTML, frme: id}) //data format
, pHost = (function(a){ //host extract from parameter (#|&)parent
if(!a.match(/^https?:///)) return'';
var b = document.createElement('a');
b.href = a;
b.pathname = b.search = b.hash ='';
return b.href.replace(//??#?$/,'')
})( decodeURIComponent( (w.location.href.match(/.*(?|#|&)parent=([^&]+)/) ||[])[2] ||'') );
try{
//'s'.wcl(s)
if(!isChrome || w.parent && w.parent.postMessage){
s && w.parent.postMessage(s, pHost); //all browsers except Chrome
//wcl('postpost')
}else if(s)
winEval(function(args){
var w = window
, p1 = arguments[0]
, p2 = arguments[1];
if(w.postMessage && p1 && w != w.parent){
function wpm(){
w.parent.postMessage(p1, p2); //msg with a glance Chrome bug
}
w.document.all ? w.setTimeout(wpm, 0) : wpm();
}
}, [s, pHost]);
}catch(er){wcl(er)}
}
,check: function(){
return document && document.querySelector('#aggregateCount');
}
});
}
И при обнаружении требуемого данного (число лайков) запускается часть приведённого кода, начинающаяся с «try{». Для всех браузеров, кроме Хрома, выполняется создание события postMessage() в одну строчку. Для Хрома выполняется обход бага, описанный в функции winEval().
/**
* evaluate script in window scope
* @param{Function} fs function or string is body of function
* @param{String|Array} s string or array of strings for arguments
* @param{Boolean} noOnce not delete script after exec
*/
var winEval = function(fs, s, noOnce){ //exec function/text in other scope
s = (s ||[]) instanceof Array? s ||[] : [s]; //wrap by array
var fs2 = typeof fs=='function'
? (fs +'').replace(/(^s*functions*([^)]*)s*{s*|s*}s*$)/g,'') //clean wrapper
: fs
, as ='';
for(var i =0, sL =s.length; i < sL; i++) //sequential array
as += (i?',':'') +"'"+ s[i].replace(/'/g,"\'").replace(/(rn|r|n)/g,"\n") +"'";
fs = '(function(){'+ fs2 +'}).apply(window,['+ as +']);';
//'fs'.wcl(fs, fs2)
var d = document
, scr = d.createElement('script');
scr.setAttribute('type','application/javascript');
scr.textContent = fs;
var dPlace = d.body || d.getElementsByTagName('head') && d.getElementsByTagName('head')[0];
dPlace.appendChild(scr);
if(!noOnce) dPlace.removeChild(scr);
};
Вот и все премудрости обхода бага. Как видно, требуется дополнительных 30-40 строчек. Есть одно утешение, что эти строчки представляют собой функции, которые могут пригодиться и в других местах юзерскрипта.
Если потребуется транспорт данных в другую сторону и на ней тоже потребуется юзерскрипт (если нет возможности написать обычный скрипт на странице), точно такое же дополнение будет нужно и для второго домена. Логично использовать ту же самую процедуру и тот же самый скрипт, добавив в мета-директиву его второй домен.
Требования к формату фреймов или скриптов
Метод postMessage требует знания домена и пути к целевому окну. Поэтому в юзерскрипт требуется передать каким-то образом эти 2 параметра. Они должны быть указаны или явно (например, parent), или быть взяты из доступного окружения. Так как скрипт запускается в кроссдоменном фрейме (или окне) и не имеет (для Хрома) возможности доступа к другим окнам, на практике указывают домен в URL фрейма (так сделано, например, в кнопке Google+). Поэтому предположим, что имя домена целевого окна записано в URl в формате /.*(?|#|&)parent=([^&]+)/ (это — рег. выражение, которое распознаёт параметр. Например, виджет будет вызван в фрейме с URL:
some-widget-site.com/abcd.php?a=1&b=2&parent=http://some-my-site.com/some-path.
Или то же самое, но будет добавлен якорь:
some-widget-site.com/abcd.php?a=1&b=2#parent=http://some-my-site.com/some-path.
Если нет такой возможности задать домен цели, скрипт должен быть переписан так, чтобы задать домен явно или другим путём (как вариант — принять юзерскриптом имя домена через тот же postMessage из верхнего окна). В дальнейшем предполагается, что домен указан в параметре parent. Так, в частности, сделано в кнопке Google+.
Обратим внимание, что этот сложный механизм будет работать во всех браузерах, но он требует больше ресурсов, т.к. неявно выполняется eval(), поэтому загрузку скрипта в [window scope] следует делать только там, где необходимо, а именно, в браузере Chrome, до тех пор, пока будет существовать баг 20773. Поэтому в рабочем скрипте сделано ветвление с проверкой доступности postMessage в кроссдоменном фрейме и выполнение обхода бага, если метод недоступен.
В скрипте имеется пара полезных функций, которые бывают нужны в других местах юзерскрипта и могли бы быть взяты как библиотечные:
1) загрузка программ в окружение [window scope] ([global_scope]);
2) слежение за появлением независимых асинхронных данных с помощью замедляющегося таймера.
Вторая процедура — не лучшее решение, но иногда нет иной возможности узнать о наступлении независимых от своего скрипта условий, как в случае проверки результатов работы стороннего виджета. К такому случаю как раз относится чтение числа «лайков» в фрейме Google+.
PS. Админы, переименуйте блог GreaseMonkey, пожалуйста, наконец-то, в «Юзерскрипты».
Автор: spmbt