В Хроме и Хромиуме уже 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":
В окне- (или фрейме-) источнике выполняют отправку текстового сообщения:
Контекст источника должен указывать на целевое окно, а во втором аргументе рекомендуется указывать явно в целях безопасности домен приёмника, а не писать строку, содержащую звёздочку (в смысле, «любой домен»). Тут-то в Хроме в контекстных скриптах всплывает баг — браузер должен обеспечить понимание объекта (целевое окно).postMessage, и в обычных скриптах в нём так и происходит. Для юзерскриптов у Хрома возникает баг неопределённости объекта (целевое окно).postMessage, если окно содержит другой домен — иной, чем источник сообщения. Поэтому к достаточно стройной схеме передачи сообщения требуется «костыль».
На стороне приёмника — никаких дополнений, потому что проблема не в нём. В источнике используем загрузку скрипта в окружение window.
Во внедрённом в чужой фрейм скрипте читаем, ожидая появления, нужные нам данные.
И при обнаружении требуемого данного (число лайков) запускается часть приведённого кода, начинающаяся с «try{». Для всех браузеров, кроме Хрома, выполняется создание события postMessage() в одну строчку. Для Хрома выполняется обход бага, описанный в функции winEval().
Вот и все премудрости обхода бага. Как видно, требуется дополнительных 30-40 строчек. Есть одно утешение, что эти строчки представляют собой функции, которые могут пригодиться и в других местах юзерскрипта.
Если потребуется транспорт данных в другую сторону и на ней тоже потребуется юзерскрипт (если нет возможности написать обычный скрипт на странице), точно такое же дополнение будет нужно и для второго домена. Логично использовать ту же самую процедуру и тот же самый скрипт, добавив в мета-директиву его второй домен.
Метод postMessage требует знания домена и пути к целевому окну. Поэтому в юзерскрипт требуется передать каким-то образом эти 2 параметра. Они должны быть указаны или явно (например, parent), или быть взяты из доступного окружения. Так как скрипт запускается в кроссдоменном фрейме (или окне) и не имеет (для Хрома) возможности доступа к другим окнам, на практике указывают домен в URL фрейма (так сделано, например, в кнопке Google+). Поэтому предположим, что имя домена целевого окна записано в URl в формате /.*(\?|#|&)parent=([^&]+)/ (это — рег. выражение, которое распознаёт параметр. Например, виджет будет вызван в фрейме с URL:
http://some-widget-site.com/abcd.php?a=1&b=2&parent=http://some-my-site.com/some-path.
Или то же самое, но будет добавлен якорь:
http://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, пожалуйста, наконец-то, в «Юзерскрипты».
Известно, что подобные проблемы решаются в расширениях Хрома, когда им пропишут права доступа, но суть вопроса в том, что дополнительных прав доступа для таких рядовых задач не требуется, нужен всего лишь обход бага в одном браузере. И тогда мы одним файлом можем записать расширение, работающее во всех браузерах, поддерживающих юзерскрипты. Пример такой задачи, не решаемой обычными скриптами — получение данных о числе «лайков» из кнопки 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*function\s*\([^\)]*\)\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(/(\r\n|\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:
http://some-widget-site.com/abcd.php?a=1&b=2&parent=http://some-my-site.com/some-path.
Или то же самое, но будет добавлен якорь:
http://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, пожалуйста, наконец-то, в «Юзерскрипты».
