Использование технологии Microsoft Active Accessibility для доступа к содержимому браузера

в 10:08, , рубрики: accessibility, c++, MSAA, Блог компании Инфопульс Украина, браузеры, Программирование, метки:

Давайте придумаем решение вот такой-вот простенькой задачи.
Имеется: браузер (IE, Chrome или Firefox), уже запущенный пользователем.
Требуется: написать программу, которая получит URL, который в данный момент введён в адресной строке.

Давайте подумаем, каким образом эту простенькую задачу решить НЕ получится:

1. FindWindow + GetWindowText

Почему не получится

Первая идея — найти окно браузера, в нём дочернее окно адресной строки и взять URL оттуда. Практика показывает, что отдельное дочернее окно для адресной строки имеет только IE. FF и Chrome кросплатформенны, поэтому предпочитают весь свой контент отрисовывать самостоятельно.

2. Браузерное расширение, которое отдаст URL нашей программе (например, через запрос к localhost)

Почему не получится

Можно. Но во-первых, для трёх браузеров нужно будет написать 3 разных расширения, а во-вторых, для FF и Chrome мы будем вынуждены распространять его только через их магазины расширений. Писать программу, работоспособность которой зависит от того, зачешется ли сегодня левая пятка модератора — нет уж, увольте.

3. Давайте напишем сниффер и посмотрим что там пользователь открывал

Почему не получится

А давайте! Но что дальше? Даже если из потока трафика мы выделим данные, полученные именно браузером и расшифруем HTTP-протокол, мы всё-равно не узнаем именно текущий URL (ссылок в потоке будет много). Кроме того, сразу идут в сад HTTPS-соединения, HTTP/2, ссылки на локально открытые файлы, ссылки на внутренние страницы (типа chrome://settings) и т.д.

4. Давайте воспользуемся Remote Debugging Protocol ну или каким-нибудь Selenium-ом

Почему не получится

Не подходит из-за ограничения условий исходной задачи: браузер уже запущен, мы не можем запустить новый подконтрольный экземпляр, нам нужно взаимодействовать с уже имеющимся.

5. Может быть, хуки?

Почему не получится

Ну, внедриться-то мы в браузер сможем. А на что вешать хуки? Для IE всё ясно — SetWindowText для окна адресной строки (но с ним и более простой способ №1 проходил). А в FF и Chrome у нас нет каких-то чётко определённых объектов и интерфейсов, на которые мы можем завязаться. Можно что-то сделать с конкретной версией браузера, но универсального решения не получится.

6. Скриншот окна браузера, определение положения адресной строки, распознавание текста с картинки!

Почему не получится

Уже как-то начинает смахивать на отчаяние, правда? Прикинем все варианты цветовых схем ОС, разрешений, масштабов, учтём наличие в браузере плагинов, цветовых схем, нестандартного расположения элементов, right-to-left языковых локалей ну и закончим случаем, когда окно адресной строки слишком узкое, чтобы вместить URL полностью.

7. Ваш вариант
А напишите в комментариях, какие ещё решения вам приходят в голову и мы подумаем, получится или нет.

А теперь один из правильных ответов: мы воспользуемся уже старенькой, но весьма стабильной и поддерживаемой всеми браузерами во всех ОС с Win95 до Win10 технологией Microsoft Active Accessibility, которая даст нам возможность не только получить текущий URL (при чём одинаковым образом для всех браузеров), но и вообще дать доступ ко всему контенту браузера — от самого родительского окна с его заголовком, меню, тулбаром, вкладками и до содержимого открытой веб-страницы вплоть до самого последнего её элемента.

Введение

Microsoft Active Accessibility (MSAA) придумали аж в 1997-ом году и сделали её для того, чтобы стало возможным писать экранные лупы, приложения для чтения текста с экрана и создания прочих программ, улучшающим взаимодействие с компьютером людей с ограниченными физическими возможностями (проблемы со зрением, слухом и т.д.). Поддержка технологии в IE появилась давно, в FF и Chrome тоже была добавлена чуть позже. С выходом Vista появилось улучшение — Windows Automation API, однако и старый добрый MSAA никуда не делся, отлично работает с последними ОС и браузерами.

Код

В общем, ничего сложного в коде нет. Входной точкой для нас будет родительское окно браузера, которое можно получить по его ClassID:

FindWindow(L"IEFrame", NULL); // IE
FindWindow(L"MozillaWindowClass", NULL); // Firefox
FindWindow(L"Chrome_WidgetWin_1", NULL); // Chrome. Этот код может сработать, но вообще-то документация (http://www.chromium.org/developers/design-documents/accessibility) рекомендует перебирать все окна, класс которых начинающиеся с "Chrome", на случай, если им взбредёт в голову изменить название класса. Из практики можно добавить, что перебирать нужно окна с таким class name и непустым заголовком.

Дальше нужно у этого окна получить указатель на COM-интерфейс IAccessible

::AccessibleObjectFromWindow(hWndChrome, OBJID_CLIENT, IID_IAccessible, (void**)(&pAccMain));

Да, перед этим не забудьте:

  • Подключить заголовочный файл #include «oleacc.h»
  • Прилинковать Oleacc.lib
  • Инициализировать COM вызовом функции ::CoInitialize(NULL);
    Это очень важно не забыть! Без этого у вас что-то может начать работать, но в непредвиденные моменты вы получите странные ошибки. Также возможна ситуация, когда никаких ошибок не будет, но вы просто не получите часть данных. В общем, очень подлая и совершенное не поддающаяся отладке ошибка.

Итак, у нас есть указатель на IAccessible. Что это такое? Это корневой узел дерева, описывающего весь браузер — окно, заголовок, меню, тулбары, адресную строку, контент страницы, статусбар. Как бы это всё увидеть в наглядном виде? Нет ничего проще! Microsoft для этого предоставляет утилиту inspect.exe (поставляется с Windows SDK, у меня лежит в папке C:Program Files (x86)Windows Kits8.0binx64). Разработчики Хромиума рекомендуют утилиту aViewer.

Давайте посмотрим, как выглядят деревья доступных элементов браузеров:
IE
Использование технологии Microsoft Active Accessibility для доступа к содержимому браузера - 1

Chrome
Использование технологии Microsoft Active Accessibility для доступа к содержимому браузера - 2

Firefox
Использование технологии Microsoft Active Accessibility для доступа к содержимому браузера - 3

Как мы видим, адресная строка доступна через интерфейс IAccessible во всех браузерах. Названия элементов, положение в дереве в разных браузерах разное, но в общем-то для доступа к адресной строке нам нужно только пара функций: возможность получения имени и значения текущего элемента и возможность получения детей текущего элемента дерева.

И то и другое пишется просто, вот финальный код, получающий текущий URL для Chrome.

#include "stdafx.h"
#include <string>
#include <iostream>
#include "windows.h"
#include "oleacc.h"
#include "atlbase.h"

std::wstring GetName(IAccessible *pAcc)
{
	CComBSTR bstrName;
	if (!pAcc || FAILED(pAcc->get_accName(CComVariant((int)CHILDID_SELF), &bstrName)) || !bstrName.m_str)
		return L"";
	
	return bstrName.m_str;
}

HRESULT WalkTreeWithAccessibleChildren(CComPtr<IAccessible> pAcc)
{
	long childCount = 0;
	long returnCount = 0;

	HRESULT hr = pAcc->get_accChildCount(&childCount);

	if (childCount == 0)
		return S_OK;

	CComVariant* pArray = new CComVariant[childCount];
	hr = ::AccessibleChildren(pAcc, 0L, childCount, pArray, &returnCount);
	if (FAILED(hr))
		return hr;

	for (int x = 0; x < returnCount; x++)
	{
		CComVariant vtChild = pArray[x];
		if (vtChild.vt != VT_DISPATCH)
			continue;
		
		CComPtr<IDispatch> pDisp = vtChild.pdispVal;
		CComQIPtr<IAccessible> pAccChild = pDisp;
		if (!pAccChild)
			continue;

		std::wstring name = GetName(pAccChild).data();
		if (name.find(L"Адресная строка и строка поиска") != -1)
		{
			CComBSTR bstrValue;
			if (SUCCEEDED(pAccChild->get_accValue(CComVariant((int)CHILDID_SELF), &bstrValue)) && bstrValue.m_str)
				std::wcout << std::wstring(bstrValue.m_str).c_str();

			return S_FALSE;
		}

		if (WalkTreeWithAccessibleChildren(pAccChild) == S_FALSE)
			return S_FALSE;
	}

	delete[] pArray;
	return S_OK;
}

HWND hWndChrome = NULL;

BOOL CALLBACK FindChromeWindowProc(HWND hwnd, LPARAM lParam)
{
	wchar_t className[100];
	if (GetClassName(hwnd, className, 100) == 0 || wcscmp(className, L"Chrome_WidgetWin_1") != 0)
		return TRUE;

	wchar_t title[1000];
	if (GetWindowText(hwnd, title, 1000) == 0 || wcslen(title) == 0)
		return TRUE;
	
	hWndChrome = hwnd;
	return FALSE;
}


int _tmain(int argc, _TCHAR* argv[])
{
	::CoInitialize(NULL);
	EnumWindows(FindChromeWindowProc, 0);

	if (hWndChrome == NULL)
		return 0;

	CComPtr<IAccessible> pAccMain;
	HRESULT hr = ::AccessibleObjectFromWindow(hWndChrome, 1, IID_IAccessible, (void**)(&pAccMain)); // 1 - захардкоженный идентификатор ловушки

	CComPtr<IAccessible> pAccMain2;
	::AccessibleObjectFromWindow(hWndChrome, OBJID_CLIENT, IID_IAccessible, (void**)(&pAccMain2));

	WalkTreeWithAccessibleChildren(pAccMain2);

	return 0;
}

Результат работы:

Использование технологии Microsoft Active Accessibility для доступа к содержимому браузера - 4

Для остальных браузеров всё аналогично.

Мелкий нюанс

Технология MSAA в Chrome по-умолчанию отключена. Это связано с архитектурой Хрома: его разделение на процессы приводит к тому, что ни в каком одном процессе нет информации обо всём дереве элементов, необходимых MSAA. Разработчики Хрома не дураки и предусмотрели включение сбора этой информации и её кеширование в главном процессе. Но поскольку это всё несколько ресурсозатратно, а технология MSAA нужна относительно небольшому количеству людей — они её по-умолчанию выключили. Включить её можно двумя способами:

  • Ручной: пойти в Хроме по ссылке chrome://accessibility и включить
  • Программный: Хром создаёт специальную «ловушку», которой можно послать сообщение о том, что в системе присутствует приложение, использующее MSAA. Отправить в эту ловушку сообщение можно вот так:
    CComPtr<IAccessible> pAccMain;
    HRESULT hr = ::AccessibleObjectFromWindow(hwnd, 1, IID_IAccessible, (void**)(&pAccMain)); // hwnd - главное окно Хрома, 1 - захардкоженный идентификатор ловушки
    

Автор: tangro

Источник

Поделиться

* - обязательные к заполнению поля