Предположим, что у нас есть некий веб-сайт, например, картинная галерея. Требуется предоставить посетителю этой картинной галереи возможность помещать картинки себе на стену в vk.com, сделав этот процесс максимально прозрачным.
С точки зрения вконтакта эта процедура состоит из следующих этапов:
- авторизация в их сети и запрос разрешения на действие от имени пользователя;
- некая подготовительная работа, завершающаяся получением url, на который следует отправить изображение;
- закачка изображения на предоставленный вконтактом ресурс, адрес которого получен в п.2;
- сохранение изображения вконтакте;
- формирование сообщения на стену, содержащего ссылку на сохраненное изображение.
К счастью, вконтакт предоставляет некий API, позволяющий почти все эти действия производить прямо из скриптов, выполняющихся на стороне клиента. Почти все. Кроме одного. Кроме пункта 3. И в этом заключается большая интрига.
Разберём эту схему подробнее. Прежде всего необходимо провести подготовительную работу. Не каждое приложение, умеющее работать с сокетами, может взаимодействовать с вконтактом. Требуется его предварительная регистрация в этой социальной сети. Делается это вот тут. Результатом этой регистрации будет некое число (или строка?), apiId (пусть будет 121212), которое потребуется в п.1.
Сам процесс авторизации выглядит достаточно просто. В заголовок нашей веб-страницы добавляется скрипт VK API:
<script src="//vk.com/js/api/openapi.js" type="text/javascript"></script>Далее вызывается функция, использующая apiId, полученный при регистрации приложения:
VK.init({apiId: "121212"});
После этого производится собственно авторизация:
VK.Auth.login( function() { // действия в случае успешной авторизации }, 5);Следует обратить внимание на цифру 5. Это на самом деле 1+4, или, проще говоря, установленный нулевой и второй флаги запрашиваемых прав доступа.
На этом пункт 1 можно считать выполненным.
Второй пункт продолжает действия в случае успешной авторизации. Он выглядит так:
VK.Api.call( "photos.getWallUploadServer", {}, function(data) { // объект data содержит либо свойство error, описывающее ошибку, // либо свойство response, содержащее адрес ресурса, на который // следует отправлять изображение для сохранения его вконтакте: // data.response["upload_url"] } );Как видим, по окончании этого этапа у нас на руках оказывается некий длинный url примерно такого вида: http://c12345.vk.com/?param1=11111¶m2=22222. Дальше на этот адрес следует сделать POST-запрос с параметром по имени photo, содеращим файл изображения.
Тут начинается проблема. На стороне клиента инструментарий, позволяющий межсайтовый скриптинг, достаточно ограничен. Можно воспользоваться jsonp - но он не позволяет POST-запросы. Можно создать дочернее окно с веб-формой, заполнить её и сделать сабмит - но как в эту форму передать двоичное содержимое файла, которого нет у клиента? Можно, наконец, попросить наш веб-сервер передать за нас нужный файл - но в документации сказано: запрос на получение адреса для загрузки файла и post-запрос с файлом должны осуществляться с одного ip-адреса (и к тому же, по слухам, вконтакт не любит много запросов с одного адреса).
Остаётся единственная альтернатива: использовать flash, точнее, flex-приложение (потому что последнее - бесплатно). Флэш тоже имеет свои представления о безопасности и загрузке файлов, но, к счастью, вконтакте позаботился об этом, и сайты c12345.vk.com предоставляют вполне либеральный файл crossdomain.xml:
<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd"> <cross-domain-policy> <site-control permitted-cross-domain-policies="master-only"/> <allow-http-request-headers-from domain="*" headers="*"/> <allow-access-from domain="*" to-ports="80"/> <allow-access-from domain="*" to-ports="443"/> </cross-domain-policy>Сам флэш-ролик на странице мы вольны поместить с атрибутом allowScriptAccess="always", и, таким образом, обеспечить двусторонний обмен скриптов ролика со скриптами из вмещающей страницы:
<object id="mySwf" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" allowScriptAccess="always"> <embed name="mySwf" src="vkUpload.swf" width="500" height="500" allowScriptAccess="always" > </object>
Что касается внутренности флэш-ролика, то она может быть, например, такой:
<?xml version="1.0" encoding="utf-8"?> <s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:mx="library://ns.adobe.com/flex/mx" xmlns:s="library://ns.adobe.com/flex/spark" width="500" height="500" creationComplete="init()" > <mx:Image id="myImage" width="100%" height="100%"/> <fx:Script> <![CDATA[ import flash.net.URLRequest; import flash.net.URLLoader; import flash.display.Loader; import flash.events.Event; import flash.events.IOErrorEvent; import flash.events.SecurityErrorEvent; import flash.external.*; import mx.graphics.codec.PNGEncoder; internal var imageDownloader : Loader = new Loader(), imageByteArray : ByteArray = null, imageUploadUrl : String = "", imageUploader : URLLoader = new URLLoader(), uploadResponse : String = ""; public function init() : void { Security.allowDomain("*"); ExternalInterface.addCallback("processDataF", processData); imageDownloader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, imageDownloaderErrorHandler); imageDownloader.contentLoaderInfo.addEventListener(Event.COMPLETE, imageDownloaderCompleteHandler); imageUploader.addEventListener(IOErrorEvent.IO_ERROR, imageUploaderErrorHandler); imageUploader.addEventListener(Event.COMPLETE, imageUploaderCompleteHandler); imageUploader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, imageUploaderSecurityErrorHandler); imageUploader.dataFormat = URLLoaderDataFormat.TEXT; } public function processData (fileSourceUrl : String, uploadUrl : String) : void { // сохраняем адрес выгрузки изображения imageUploadUrl = uploadUrl; // получаем изображение var request:URLRequest = new URLRequest(fileSourceUrl); try { imageDownloader.load(request); } catch (error:*) { processError("Загрузка изображения", error.message); } } private function processError (step : String, message : String) : void { ExternalInterface.call("ReturnFromFlexWithError", step + "\n" + message); } private function processComplete (message : String) : void { ExternalInterface.call("ReturnFromFlexWithSuccess", encodeURIComponent(message)); } private function imageDownloaderErrorHandler (e : IOErrorEvent) : void { processError("Процесс загрузки изображения", e.text); } private function imageDownloaderCompleteHandler (e : Event) : void { try { var bitmap : Bitmap = Bitmap(imageDownloader.content); myImage.source = bitmap; var png:PNGEncoder = new PNGEncoder(); imageByteArray = png.encode(bitmap.bitmapData); } catch (error:*) { processError("Окончание процесса загрузки изображения", error.message); return; } if (!imageByteArray) { processError("Получение данных для выгрузки изображения", "Массив пуст"); return; } // выгружаем изображение var postData : ByteArray = new ByteArray(); postData.writeMultiByte('--TESTTESTTEST\r\n', 'utf-8'); postData.writeMultiByte('Content-Disposition: form-data; name="photo"; filename="photo.png"\r\n', 'utf-8'); postData.writeMultiByte('Content-Type: application/octet-stream\r\n', 'utf-8'); postData.writeMultiByte('Content-Transfer-Encoding: binary\r\n', 'utf-8'); postData.writeMultiByte('\r\n', 'utf-8'); postData.writeBytes(imageByteArray); postData.writeMultiByte('\r\n', 'utf-8'); postData.writeMultiByte('--TESTTESTTEST--\r\n', 'utf-8'); var request : URLRequest = new URLRequest(imageUploadUrl); request.method = URLRequestMethod.POST; request.requestHeaders.push( new URLRequestHeader( 'Content-Type', 'multipart/form-data; boundary=TESTTESTTEST' ) ); request.data = postData; try { imageUploader.load(request); } catch (error:*) { processError("Начало процесса выгрузки изображения", error.message); return; } } private function imageUploaderErrorHandler (e : IOErrorEvent) : void { processError("Процесс выгрузки изображения, ввод-вывод", e.text); } private function imageUploaderSecurityErrorHandler (e : SecurityErrorEvent) : void { processError("Процесс выгрузки изображения, безопасность", e.text); } private function imageUploaderCompleteHandler (e : Event) : void { processComplete(String(imageUploader.data)); } ]]> </fx:Script> </s:Application>Схема взаимодействия этого ролика со скриптом вмещающей страницы проста. Скрипт со страницы вызывает метод ролика:
document["mySwf"].processDataF(АдресИзображенияДляЗагрузки, АдресСервераВконтакте);Этот метод по результатам своего труда либо при ошибке передает управление в функцию:
function ReturnFromFlexWithError(data) { alert("Ошибка при работе через Flex!\n" + data); }либо - при успехе - в функцию:
function ReturnFromFlexWithSuccess(data) { var jsonData = JSON.parse(decodeURIComponent(data)); // действия в случае успешной загрузки изображения // особую ценность представляют jsonData.photo, jsonData.server, jsonData.hash }Тут стоит упомянуть забавную тонкость. Если не uri-кодировать строку, возвращаемую роликом в основной скрипт, то браузер выдает ошибку синтаксиса. Поэтому приходится передавать закодированную строку, и декодировать её на стороне javascript-а.
Вернемся к нашей схеме. Получив на предыдущем этапе значения photo, server и hash мы можем сохранить загруженное фото:
VK.Api.call( "photos.saveWallPhoto", {photo: jsonData.photo, server: jsonData.server, hash: jsonData.hash}, function(data) { // объект data содержит либо свойство error, описывающее ошибку, // либо свойство response, содержащее идентификатор сохраненного изображения // data.response[0].id } );
Осталось выполнить последний пункт: разместить на стене изображение, содержащее фото. Это делается командой:
VK.api( "wall.post", {message: "текст сообщения", attachments: data.response[0].id}, function (data) { // объект data содержит либо свойство error, описывающее ошибку, // либо свойство response, содержащее результат операции } );
Как видим, основными трудностями, которые нам здесь пришлось обойти, является запрет кроссдоменного скриптинга и помещение в POST-запрос информации, которую сервер бы трактовал как двоичный файл.
А это - в качестве иллюстрации вышеизложенного:
http://это-очередной-унылый-домен-из-кириллической-зоны.рф/web/js/z150525-00.xml