воскресенье, 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

среда, 6 мая 2015 г.

ШТРИХ-ФР-К: одна маленькая особенность

Если при печати чека из 1С драйвер фискального регистратора вдруг заявляет "При печати чека произошла ошибка. Чек не напечатан на фискальном регистраторе. Дополнительное описание: A3h, Некорректное состояние ЭКЛЗ", не надо ему так уж прямо безоговорочно верить. Лучше поверить программе "Тест драйвера", которая показывает, что всё нормально, и проводит чек без всяких ошибок.

Я перетряхнул все кабели, обновил версию драйвера с 4.9 на 4.12 и обратно, но дело оказалось вовсе не в этом. В таких случаях надо включить ведение логов драйвера (это делается либо в программе "Тест драйвера" - кнопка "Настройка свойств" - кнопка "Дополнительные параметры" - галочка "Вести лог", либо в самой 1С в свойствах подключаемого оборудования - в зависимости от того, какая программа глючит), и тогда будет видно различие логов "больного" чека:
...
[06.05.2015 12:39:38.365] [ info]: TFiscalPrinter ------------------------------------------------------------
[06.05.2015 12:39:38.365] [debug]: TFiscalPrinter Команда: 8Dh, Открыть чек
[06.05.2015 12:39:38.365] [ info]: TFiscalPrinter ------------------------------------------------------------
...
[06.05.2015 12:39:38.928] [debug]: Tole1Cst OpenCheck(CheckNumber: 972; SessionNumber: 309): True
[06.05.2015 12:39:38.928] [debug]: Tole1Cst RetValue:True
[06.05.2015 12:39:38.928] [debug]: Tole1Cst Invoke: 0x00000000 (Операция успешно завершена)
...
[06.05.2015 12:39:39.271] [ info]: TFiscalPrinter ------------------------------------------------------------
[06.05.2015 12:39:39.271] [debug]: TFiscalPrinter Команда: 85h, Закрытие чека
[06.05.2015 12:39:39.271] [ info]: TFiscalPrinter ------------------------------------------------------------
[06.05.2015 12:39:39.271] [debug]: TPrinterProtocol -> 05
[06.05.2015 12:39:39.271] [debug]: TPrinterProtocol <- 15
[06.05.2015 12:39:39.271] [debug]: TPrinterProtocol -> ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ
[06.05.2015 12:39:39.271] [debug]: TPrinterProtocol -> 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[06.05.2015 12:39:39.271] [debug]: TPrinterProtocol -> 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[06.05.2015 12:39:39.271] [debug]: TPrinterProtocol -> 00 00 00 00 00 00 00 00 00 00 00 00 00 68
[06.05.2015 12:39:39.271] [debug]: TPrinterProtocol <- 06
[06.05.2015 12:39:39.334] [debug]: TPrinterProtocol <- 02
[06.05.2015 12:39:39.334] [debug]: TPrinterProtocol <- 02
[06.05.2015 12:39:39.334] [debug]: TPrinterProtocol <- 85 A3 24
[06.05.2015 12:39:39.334] [debug]: TPrinterProtocol -> 06
[06.05.2015 12:39:39.334] [error]: TFiscalPrinter Ошибка: (163, A3h) Некорректное состояние ЭКЛЗ
[06.05.2015 12:39:39.334] [error]: TFiscalPrinter Ошибка: 163 Некорректное состояние ЭКЛЗ
...

и "здорового":
...
[06.05.2015 12:58:23.819] [ info]: TFiscalPrinter ------------------------------------------------------------
[06.05.2015 12:58:23.819] [debug]: TFiscalPrinter Команда: 8Dh, Открыть чек
[06.05.2015 12:58:23.819] [ info]: TFiscalPrinter ------------------------------------------------------------
...
[06.05.2015 12:58:24.772] [ info]: TFiscalPrinter ------------------------------------------------------------
[06.05.2015 12:58:24.772] [debug]: TFiscalPrinter Команда: 80h, Продажа
[06.05.2015 12:58:24.772] [ info]: TFiscalPrinter ------------------------------------------------------------
..
[06.05.2015 12:58:24.866] [debug]: Tole1Cst PrintFiscalString: True
[06.05.2015 12:58:24.866] [debug]: Tole1Cst RetValue:True
[06.05.2015 12:58:24.866] [debug]: Tole1Cst Invoke: 0x00000000 (Операция успешно завершена)
...
[06.05.2015 12:58:25.209] [ info]: TFiscalPrinter ------------------------------------------------------------
[06.05.2015 12:58:25.209] [debug]: TFiscalPrinter Команда: 85h, Закрытие чека
[06.05.2015 12:58:25.209] [ info]: TFiscalPrinter ------------------------------------------------------------
[06.05.2015 12:58:25.209] [debug]: TPrinterProtocol -> 05
[06.05.2015 12:58:25.209] [debug]: TPrinterProtocol <- 15
[06.05.2015 12:58:25.209] [debug]: TPrinterProtocol -> ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ ХХ
[06.05.2015 12:58:25.209] [debug]: TPrinterProtocol -> 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[06.05.2015 12:58:25.209] [debug]: TPrinterProtocol -> 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[06.05.2015 12:58:25.209] [debug]: TPrinterProtocol -> 00 00 00 00 00 00 00 00 00 00 00 00 00 9A
[06.05.2015 12:58:25.209] [debug]: TPrinterProtocol <- 06
[06.05.2015 12:58:25.288] [debug]: TPrinterProtocol <- 02
[06.05.2015 12:58:25.288] [debug]: TPrinterProtocol <- 08
[06.05.2015 12:58:25.288] [debug]: TPrinterProtocol <- 85 00 01 00 00 00 00 00 8C
[06.05.2015 12:58:25.288] [debug]: TPrinterProtocol -> 06
...
[06.05.2015 12:58:27.991] [debug]: Tole1Cst CloseCheck: True
[06.05.2015 12:58:27.991] [debug]: Tole1Cst RetValue:True
[06.05.2015 12:58:27.991] [debug]: Tole1Cst Invoke: 0x00000000 (Операция успешно завершена)
...

Как видим, если между открытием чека и закрытием не было операции "Продажа" (или, например, как говорят опытные люди, если в незакрытом чеке продажа была отсторнирована), то в момент закрытия чека его открытая электронная копия на ЭКЛЗ отсутствует, и возникает вышеуказанная ошибка A3h.