воскресенье, 31 мая 2015 г.

vk.com: как запостить картинку на стену

Время не стоит на месте. Это неважно, пользуешься ли ты социальными сетями. Они всё равно существуют рядом с тобой, и отмахнуться от этого факта удаётся далеко не всегда.

Предположим, что у нас есть некий веб-сайт, например, картинная галерея. Требуется предоставить посетителю этой картинной галереи возможность помещать картинки себе на стену в vk.com, сделав этот процесс максимально прозрачным.

С точки зрения вконтакта эта процедура состоит из следующих этапов:
  1. авторизация в их сети и запрос разрешения на действие от имени пользователя;
  2. некая подготовительная работа, завершающаяся получением url, на который следует отправить изображение;
  3. закачка изображения на предоставленный вконтактом ресурс, адрес которого получен в п.2;
  4. сохранение изображения вконтакте;
  5. формирование сообщения на стену, содержащего ссылку на сохраненное изображение.

К счастью, вконтакт предоставляет некий 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&param2=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

Комментариев нет:

Отправить комментарий