Получение URL текущей вкладки в популярных браузерах

Вероятно, многие сталкивались с задачей получения URL текущей вкладки из браузера. Целью подобного интереса может быть как шпионаж за пользователем, так и персональный учет времени проведенный за теми или иными сайтами для анализа собственной продуктивности. Передо мной недавно встала подобная задача, которую я практически успешно решил(почему “практически” вы узнаете далее).

В статье могут использоваться классы из С++ Qt фреймворка, их имя начинается с литеры ‘Q’. Вы легко можете заменить их на что-то привычное для вас, я же использую Qt для своего удобства. Т.к. он применяются тут только в контексте вспомогательных классов это не помешает вам уловить суть методов.

Итак, способы получения URL из браузера я бы разделил на 2 группы: анализ файлов браузера(ФБ) и системные вызовы(СВ). Анализ файлов браузера – означает, что для получения URL необходимо использовать некие данные из файлов, которые принадлежат тому или иному браузеру(например файл истории). Этот метод является почти(об этом далее) кроссплатформенным. Также, необходимо понимать, что обновление файла операция не моментальная и может происходить с некоторой задержкой, а значит требуемая нами информация может быть не найдена в момент, когда мы решим анализировать его. Правда, на моей практике задержки более чем в секунду я не встречал, поэтому можно считать этот способ надежным. Системные вызовы – подразумевают использование каких-то ОС специфичных вызовов для получения URL. Этот метод применим не ко всем браузерам, и не является кроссплатформенным. Так же этот метод является наиболее быстрым и надежным, поэтому при возможности стоит отдавать предпочтение именно ему. Большинство методов используют следующую идиому: Найти имя текущей вкладки –> Найти URL соответствующий этому имени. В описания алгоритмов поиска подразумевается, что имя уже найдено вами(если оно необходимо). Поэтому, если вы не знаете как находить имя обратитесь к Приложению, там описано получения имени вкладки для каждого случая.

Приведенные в статье фрагменты кода были выдраны из моего проекта и не были скомпилированы, в том виде, в котором они тут представлен. Это значит, что там могут содержатся синтаксические ошибки. Будьте внимательны и сообщайте мне о найденных ошибках, пожалуйста.

Приведу таблицу соответствия метода получения URL из различных популярных браузеров в наиболее популярных ОС.

Браузер

ОС

Opera

Firefox

Chrome

Internet Explorer

Safari

Windows

ФБ (global_history.dat)

ФБ (sessionstore.js)

СВ (Получение URL прямиком из окна)

СВ (Получение URL прямиком из окна)

ФБ (History.plist)

Linux

ФБ (global_history.dat)

ФБ (sessionstore.js)

ФБ

(Current Session)

-

-

Mac OS X

СВ
(Apple script)

СВ
(Apple script)

СВ
(Apple script)

-

СВ
(Apple script)

Теперь, давайте шаг за шагом разберем каждый метод для каждой ОС, представим его плюсы и минусы, а также различные подводные камни. И начну я с моего любимого браузера:

Opera

Пожалуй самый простой способ добывания URL по ФБ методу предоставляет нам Opera. Формат её файла истории прост и прямолинеен. Формат файла является текстовым, закодированным в UTF-8. Файл состоит из блоков следующего вида:

format

Таким образом, мы можем использовать имеющееся у нас имя таба и считать соответствующий ему URL, который находится непосредственно за именем и отделен символом перевода строки ‘\n’.

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

Есть некоторая особенность в том, как Opera представляет имя сайта, который является текущим в браузере:

  • Если имя сайта слишком длинное(> 125 символов), тогда Opera не представляет его целиком в имени окна, а показывает лишь часть и ставит “…”(троеточие) на конце строки. В этом случае необходимо обрезать “…” и искать уже часть имени сайта, а не его целиком. В файле истории же, Opera сохраняет имя как оно есть, без каких либо модификаций.
  • Opera добавляет “- Opera” в конце имени каждого сайта, в имени окна. Необходимо всегда обрезать это окончание, т.к. оно не содержится в файле истории.

Анализ файла истории является единственным вариантом нахождения Opera URL в  Linux и Windows.

Private tab url не будет отражена в истории, а значит нет возможности получить URL текущего таба, если он является private

Следующая таблица содержит пути, которые ведут к global_history.dat на разных операционных системах. Подразумевается установка браузера по умолчанию, безо всяких модификаций:

ОС

Размещение

Windows

%APPDATA%/Opera/Opera

Linux

~/.opera

Mac OS X

~/Library/Opera

 

Реализация на C++:

QString GetUrlByTitle(const QString& strPathToData, 
    const QString& strTitle)
{
    QString strTitleCopy = strTitle;
    strTitleCopy.replace(" - Opera", "");

    QString strPathToHistory = strPathToData + "/global_history.dat";
    QFile File(strPathToHistory);
    if(!File.open(QIODevice::ReadOnly | QIODevice::Text))
    {
        return QString();
    }

    int TitleSize = strTitleCopy.count();
    QTextStream Stream(&File);
    Stream.setCodec("UTF-8");
    const int OperLineLimit = 125;

    if (strTitleCopy.endsWith("...") && TitleSize > OperLineLimit)
    {
        //Отрезаем многоточие
        strTitleCopy.chop(3);
        while(!Stream.atEnd())
        {
            QString strLine = Stream.readLine();
            if(strLine.count() > OperLineLimit &&
                strLine.startsWith(strTitleCopy))
            {
                strLine = Stream.readLine();
                return strLine;
            }
        }
    }
    else
    {
        while(!Stream.atEnd())
        {
            QString strLine = Stream.readLine();
            if(strLine == strTitleCopy)
            {
                strLine = Stream.readLine();
                return strLine;
            }
        }
    }
    return QString();
}

 


Теперь рассмотрим СВ способ получения текущей URL в Opera. Мне известен только один способ получения URL средствами ОС, и этот способ работает только в Mac OS X, т.к. использует поставляемое вместе с Mac OS X решение – Apple Script.

На Apple Script решение простое и прямолинейное:

tell application "Opera"
	return URL of front document as string
end tell

Firefox

Разберем формат файла sessionstore.js, хранящего информацию о сессии открытой в Firefox или же последней открытой сессии, если Firefox закрыт. Нет смысла разбирать формат файла полностью, а он несколько запутаннее чем его аналог в Opera. Нам нужно получить URL, и, следовательно, разберем лишь часть, которая необходима для получения оного.

Важно знать, что формат файла sessionstore.js в  Firefox версии 3.5 и выше отличается от формата файла своих “предков”. Различаются они не сильно, хотя эти отличия весьма существенны при анализе. При дальнейшем повествовании я буду ссылаться на формат файла, который был до Firefox 3.5 как старый формат, формат, который был представлен в Firefox 3.5 и остается таким по сей день(на день написания публичным релизом является Firefox 5.0) будет именоваться не иначе как новый формат.

Для определения версии Firefox, стоит воспользоваться полем LastVersion в файле compatibility.ini.

Т.к. Firefox хранит много информации, которая не относится к нашей цели, непосредственно, я представлю формат той строки, которую нам необходимо вычленить из этого файла в процессе поиска заветного URL. Текст в круглых скобках, является поясняющим и не является частью формата:

Новый формат:

new

Формат файла является текстовым, закодированным в UTF-8.

Пример:

"url":"http://www.yandex.ru/","title":"Яндекс"

 

Старый формат:

old

Формат файла является текстовым, закодированным в ASCII. При этом, символы unicode записываются в каноническом формате: \uXXXX. Поэтому, прежде чем искать заголовок сайта, в файле старого формата, необходимо привести все символы unicode из заголовка к каноническому формату.

Пример:

url:"http://www.yandex.ru/", title:"\u042F\u043D\u0434\u0435\u043A\u0441"

 

Таким образом, по заголовку сайта мы находим позицию, относительно которой необходимо начать поиск влево и первый же встретившийся нам URL, будет искомым.

Позицию заголовка стоит искать вместе с кавычками, дабы избежать ситуации, когда заголовок который мы ищем является также подзаголовком другого заголовка; помимо того, что является заголовком сайта, URL которого мы ищем.

Особенность заголовка в Firefox: Firefox добавляет строку “ – Mozilla Firefox” в конец имени окна. Следовательно, его необходимо отрезать при анализе, т.к. это окончание не содержится в файле сессии.

Анализ файла сессии является единственным вариантом(известным мне) нахождения Firefox URL во всех 3-х ОС: Linux, Windows и Mac OS X.

Следующая таблица содержит пути, которые ведут к sessionstore.js и compatibility.ini на разных операционных системах. Подразумевается установка браузера по умолчанию, безо всяких модификаций:

ОС

Размещение

Windows

%APPDATA%/Mozilla/Firefox/Profiles/<gibberish_name>/

Linux

~/.mozilla/firefox/<gibberish_name>/

Mac OS X

~/Library/Application Support/Firefox/Profiles/<gibberish_name>/

 

Где <gibberish_name>, есть некое уникальное имя, которое генерируется Firefox. Судя по всему, это имя отражает профиль определенного пользователя. Соответственно, если их несколько, то необходимо выбирать нужный. Выбор профиля, выходит за рамки данной статьи и остается заданием для читателя, т.к. я не сталкивался с подобной проблемой.

Реализация на C++:

Введем несколько вспомогательных функций; для определения версии Firefox:

enum Versions{e30, e35};
Versions DetermineFirefoxVersion(const QString& strPathToData)
{
    QString strCompatabilityPath = strPathToData;
    strCompatabilityPath += "/compatibility.ini";
    QFile File(strCompatabilityPath);
    if (File.open(QIODevice::ReadOnly | QIODevice::Text))
    {
        QTextStream Stream(&File);
        Stream.setCodec("UTF-8");
        QString strLine = Stream.readAll();
        if (strLine.indexOf("LastVersion=3.0") != -1)
            return e30;	
    }
    return e35;
}

и для замены канонически записанных unicode символов на их непосредственные значения:

void ReplaceUnicodeCodesWithCharacters(QString& strSource)
{
    QRegExp RegExp("\\u([0-9A-F]{4})");
    QStringList List;
    int CodePosition = 0;
    while((CodePosition = RegExp.indexIn(strSource, CodePosition)) != -1) 
    {
        if (!List.contains(RegExp.cap(1)))
            List << RegExp.cap(1);
        CodePosition += RegExp.matchedLength();
    }

    foreach(QString strEntry, List)
    {
        bool Ok = false;
        uint CodePoint = strEntry.toUInt(&Ok, 16); 
        if(Ok)
        {
            QString strTmp = QChar(CodePoint);
            strSource.replace("\\u" + strEntry, strTmp);
        }
        else
        {
            //Уупс, что-то не то у нас в строке
        }
    }
    strSource.replace("\\xAB", "«");
    strSource.replace("\\xBB", "»");
    strSource.replace("\\xA0", " ");
}

 

Теперь приведем основной код:

QString GetUrlByTitle(const QString& strTitle,
    const QString& strPathToData)
{
    QString strWindowTitle = strTitle;
    strWindowTitle.replace(" - Mozilla Firefox", "");
    strWindowTitle.replace("\"", "\\\"");
    //Воссоздаем формат принятый в sessionstore.js из нашего заголовка
    QString strTitleEntry = "\"title\":\"" + strWindowTitle + "\"";

    QString strPathToStore = strPathToData + "/sessionstore.js";
    QFile File(strPathToStore);
    if (!File.open(QIODevice::ReadOnly | QIODevice::Text))
    {
        return QString();
    }
    QTextStream Stream(&File);
    Stream.setCodec("UTF-8");
    QString strContent = Stream.readAll();

    Versions Version = DetermineFirefoxVersion();
    if (Version == e30)
    {
        strTitleEntry = " title:\"" + strWindowTitle + "\"";
        ReplaceUnicodeCodesWithCharacters(strContent);
    }
    QRegExp RegExp("\"url\":\"([^{]*)\"," + strTitleEntry);
    RegExp.setMinimal(true);
    if(RegExp.indexIn(strContent) != -1)
        return RegExp.cap(1).trimmed();

    /*
         Если не удалось найти посредством RegExp, 
         будем искать "по старинке"
    */
    int TitlePosition = strContent.indexOf(strTitleEntry);
    if (TitlePosition != -1)
    {
        QString strUrlPattern = "\"url\":\"";
        //'"'(кавычка) и ','(запятая)
        int RedundantSymbols = 2;
        if (Version == e30)
        {
            strUrlPattern = "url:\"";
            //Учитываем дополнительный пробел
            RedundantSymbols++;
        }
        QString strLeftPart = strContent.left(TitlePosition);

        int UrlPosition = strLeftPart.lastIndexOf(strUrlPattern); 
        int UrlSize = TitlePosition - UrlPosition - RedundantSymbols;
        QString strResult;
        if(UrlSize > 0)
            strResult = strLeftPart.mid(UrlPosition + strUrlPattern.size(),
                UrlSize);
        return strResult; 
    }
    return QString();
}

Internet Explorer

Честно говоря, я даже не искал, как можно получить URL в IE посредством ФБ, т.к. IE присутствует только в ОС Windows, а в этой системе имеется простой и надежный способ получения URL прямо из окна Internet Explorer.

Правда, есть небольшой нюанс: метод получения в различных версиях отличается, так, на данный момент можно разделить версии IE на два класса: старые(IE 6.0 и младше) и новые(IE 7.0 и старше). Различия между версиями заключаются в разной иерархии окон, которую необходимо пройти до заветного окна содержащего текст искомого URL.

На момент написание статьи последней мажорной релизной версией является 9.0

 

Так, для старых версий IE, иерархия окон(по именам их классов) выглядит следующим образом:

     Для новых версий иерархия выглядит несколько иначе:

old new

URL находится в последнем окне в иерархии, классом которого является Edit. Для получения текста URL воспользуемся сообщением WM_GETTEXT, посланным последнему окну в иерархии. Результатом будет искомый URL.

C++ реализация:

Начнем со вспомогательного метода:

QString GetWindowText(HWND Window)
{
    int Length = ::SendMessage(Window, WM_GETTEXTLENGTH, 0, 0);
    if (Length == 0)
        return QString();
    //для \0 символа
    Length += 1;
    QScopedArrayPointer<WCHAR> spBuffer(new WCHAR[Length]);
    ::SendMessage(Window, WM_GETTEXT, Length,
        reinterpret_cast<LPARAM>(spBuffer.data()));
    return QString::fromWCharArray(spBuffer.data());
}


и закончим основной реализацией:

HWND ActiveWindow = ::GetForegroundWindow();
HWND WorkerHandle = ::FindWindowEx(ActiveWindow, NULL, L"WorkerW", NULL);
if(!WorkerHandle)
    WorkerHandle = ::FindWindowEx(m_ActiveWindow, NULL, L"WorkerA", NULL);
if(!WorkerHandle)
{
    return QString();
}
HWND RebarHandle = ::FindWindowEx(WorkerHandle, NULL, L"ReBarWindow32", 
    NULL);		
HWND ComboBoxExHandle = ::FindWindowEx(RebarHandle, NULL, L"ComboBoxEx32", 
    NULL);

HWND AdressBandRootHandle = NULL;
if(!ComboBoxExHandle)
{
    AdressBandRootHandle = ::FindWindowEx(RebarHandle, NULL, 
        L"Address Band Root", NULL);
}
HWND ComboBoxHandle = NULL;
if(ComboBoxExHandle)
        ComboBoxHandle = ::FindWindowEx(ComboBoxExHandle, NULL,
            L"ComboBox", NULL);

HWND EditHandle = NULL;
//IE 7.0 и выше...
if (!ComboBoxHandle) 
{
    EditHandle = ::FindWindowEx(AdressBandRootHandle, NULL, L"Edit", 
        NULL);
}
//...или IE 6.0
else
{
    EditHandle = ::FindWindowEx(ComboBoxHandle, NULL, L"Edit", NULL);
}
return GetWindowText(EditHandle);

Google Chrome

Поиск URL по методу ФБ мы будем проводить в файле Current Tabs. Формат этого файла ясен не полностью, но нам необходима лишь его часть, чтобы по названию сайта найти URL. Итак, приступим: формат файла является бинарным, URL  и имена сайтов хранятся закодированными различными методами(ASCII и UTF16-LE соответственно).

Формат записи, которая которая содержит необходимую нам информацию, следующий:

format

Для нахождения искомого URL необходимо выполнить следующий манипуляции  над файлом Current Session:

  1. Найти позицию заголовка сайта в файле(P1). 
  2. Произвести поиск в левую сторону, от P1 до позиции первого встреченного протокола(http, ftp, и т.д.). Это будет P2
  3. Отрезать все, что слева от P2 и справа от P1. В дальнейшем рассматривается только интервал [P2, P1)
  4. Удалить все нули в последовательности
  5. Отрезать от конца полученной последовательности такое количество байт, которое необходимо для представления длины заголовка. Например, для заголовка длиной 120 символов необходимо отрезать 1 байт, а для заголовка длиной 300 символов необходимо отрезать 2 байта.
  6. Полученная последовательность является искомым URL

Необходимо помнить, что имеющийся у вас заголовок сайта может быть частью более длинного заголовка принадлежащего другому URL. Поэтому при поиске необходимо это учитывать. Чтобы убедится, что найденный нами заголовок именно тот, что мы ищем нужно найти значение поля “Длина заголовка сайта” и сравнить с длиной имеющегося заголовка, если они совпадают, тогда мы нашил то, что искали.

Как и другие браузеры, Google Chrome добавляет свою уникальную строку к заголовку сайта в заголовке окна. Это строка “ - Google Chrome”  и её необходимо отрезать перед началом поиска.

К сожалению, этот метод не является 100% надежным, т.к. по невыясненным мною причинам Google Chrome сохраняет не все посещенные URL в этом файле. Я встречал как минимум 1 сайт с таким поведением, это был официальный сайт Apple Украина, но сейчас я не могу его найти, по непонятным причинам Улыбка Именно поэтому я упоминал в начале статьи, что задачу мне “почти” удалось решить.

Еще одним минусом этого метода является его неработоспособность в ОС Windows, т.к. Google Chrome не дает открыть файл Current Tabs на чтение. Видимо этот файл открыт в Хроме с флагом OF_SHARE_EXCLUSIVE.

Таким образом, этот способ является рабочим только в Mac OS X и Linux, при этом это единственный способ добычи URL из Google Chrome для семейства ОC Linux.

Следующая таблица содержит пути, которые ведут к  файлу Current Session на разных операционных системах. Подразумевается установка браузера по умолчанию, безо всяких модификаций:

ОС

Размещение

Windows

%LOCALAPPDATA%/Google/Chrome/User Data/Default

Linux

~/.config/google-chrome/Default

Mac OS X

~/Library/Application Support/Google/Chrome/Default

 

Реализация на C++:

Для начала приведем вспомогательный метод:

int FindTitleIndex(const QByteArray& FileContent,
    const QString& strTitle,
    int StartPosition/* = 0*/)
{
    QTextCodec* pCodec = QTextCodec::codecForName("UTF-16LE");
    QByteArray Utf16Title = pCodec->fromUnicode(strTitle);
    //Удаляем BOM(специфично для Qt)
    Utf16Title.remove(0, 2);
    int TitleIndex = FileContent.indexOf(Utf16Title, StartPosition);
    /* 
        Определяем, является ли найденная позиция искомой, или
        это позиция подстроки другого заголовка.
    */
    if (TitleIndex != -1)
    {
        QByteArray TruncatedContent = FileContent.left(TitleIndex);
        int UrlStart = TruncatedContent.lastIndexOf("http"); 
        if (UrlStart != -1)
        {
            QByteArray LengthBytes;
            if (strTitle.size()/256)
            {
                unsigned short TitleSize =strTitle.size();
                char *pTitleSize = reinterpret_cast<char*>(&TitleSize);
                LengthBytes = QByteArray(pTitleSize, sizeof(TitleSize));
            }
            else
            {
                char TitleSize = static_cast<char>(strTitle.size());
                LengthBytes = QByteArray(&TitleSize, sizeof(char));
            }
            /*
                 Ищем байты представляющие длину заголовка между 
                 заголовком и url. Продолжаем поиск заголовка если эти
                 байты не найдены
            */
            if (TruncatedContent.indexOf(LengthBytes, UrlStart) == -1)
                return FindTitleIndex(FileContent,
                    strTitle, TitleIndex + strTitle.size());
        }
    }
    return TitleIndex;
}

Основной код:

QString GetUrlByTitle(const QString& strTitle,
    const QString& strPathToData)
{
    QString strTitleCopy = strTitle;
    strTitleCopy.replace(" - Google Chrome", "");

    QString strPathToHistory = strPathToData + "/Current Session";
    QFile File(strPathToHistory);
    if (!File.open(QIODevice::ReadOnly))
    {
        return QString();
    }
    QByteArray FileContent = File.readAll();

    int TitleStart = FindTitleIndex(FileContent, strTitleCopy);
    if (TitleStart != -1)
    {
        QByteArray TruncatedContent = FileContent.left(TitleStart);
        int UrlStart = TruncatedContent.lastIndexOf("http"); 
        TruncatedContent.remove(0, UrlStart);
        TruncatedContent.replace('\0', "");
        int LengthBytes = strTitleCopy.size()/256 + 1; 
        TruncatedContent.chop(LengthBytes); 
        return TruncatedContent;
    }
    else
    {
        //Заголовок не найден, увы
    }
    return QString();
}

 

 


Теперь рассмотрим СВ метод получения URL в Google Chrome и начнем, пожалуй, с ОС Windows:

Windows

Тут решение достаточно простое, хотя и различается для старых и новых версий Chrome. Решение заключается в нахождении окна ввода определенного класса, и получения его содержимого. Так же как и в IE, только без иерархических заморочек, т.к. окно ввода является прямым наследником основного окна. В старых версиях класс окна именовался “Chrome_AutocompleteEditView”, в новых же версиях он называется “Chrome_OmniboxView”. Мне сложно сказать, когда произошел этот переход, т.к. я обнаружил новый класс окна ввода URL в мажорной версии 13, которая на момент написания является последней. Для получения текста URL воспользуемся сообщением WM_GETTEXT, посланному окну найденному ранее.

Теперь тоже самое, только языком C++:

HWND ActiveWindow = ::GetForegroundWindow();
HWND EditHandle = ::FindWindowEx(ActiveWindow, 0, L"Chrome_OmniboxView",
    NULL);
if (!EditHandle)
{
     //Старые версии
     EditHandle = ::FindWindowEx(m_ActiveWindow, 0,
         L"Chrome_AutocompleteEditView", NULL);
     if (!EditHandle)
     {
            return QString();
     }
}
//Воспользуемся функцией приведенной в секции про IE
return GetWindowText(EditHandle);

Mac OS X

Здесь все как обычно, воспользуемся языком Apple Script для получения URL:

tell application "Google Chrome"
    set theDate to Url of active tab of first window
    return theDate
end tell

Safary

В рамках нахождения URL по методу ФБ разберем формат файла History.plist. Формат файла является бинарным, назначение многих полей(как обычно) мне не известно, хотя для нашей задачи это не принципиально.

Если я не ошибаюсь, то в старых версиях Mac OS X формат plist файлов был гораздо проще; это был текстовый XML файл. Я не рассматриваю данный вариант в статье т.к. у меня нет живого примере и я считаю, что старыми версиями Mac OS X можно пренебречь

Итак, разберем формат блока который представляет для нас интерес:

format

При разборе этого блока мы сразу же мы сталкиваемся с “гениальным” алгоритмом хранения заголовка сайта в файле истории, который вышел из кузниц компании Apple, а именно: если заголовок содержит только символы ASCII(при этом только символы значения которых < 0x7F), тогда он хранится  как есть. Если же  заголовок содержит символы значения которых превышают 0x7F тогда заголовок хранится в UTF-16BE. Поэтому будьте аккуратны при поиске заголовка в файле: прежде чем искать его, необходимо определить как должен быть закодирован заголовок для удачного поиска.

Вы думали это все? нет, “гениальность” простирается и дальше: если заголовок сайта не превышает 14 символов, тогда размер поля “Неопознанные символы” будет равен 1 байту. Ежели заголовок сайта превысит это загадочное число, тогда “Неопознанные символы” будут занимать уже 3 байта. Не хочу никого обвинять, не понимая мотивов, но когда я разбирал формат этого файла я постоянно находился в состоянии легкого недоумения. Таким образом, алгоритм поиска нужного URL будет выглядеть следующим образом:

  1. Определяем наличие “не православных”(> 0x7E) символов в заголовке сайта. Если они обнаружены, тогда конвертируем его в UTF-16BE.
  2. Ищем заголовок в файле. Для исключения возможности, что мы можем зацепить заголовок который является надмножеством нашего мы используем оконечный символ “[” при поиске. Таким образом мы будем искать строку “<Заголовок сайта>[”. При этом символ “[” имеет hex значение 5B. Позиция P1
  3. Отрезаем все, что справа от P1, больше нас эта информация не интересует.
  4. Ищем первое вхождение протокола(http, ftp etc.) с конца последовательности. Это будет P2
  5. Отрезаем все, что слева от P2
  6. Вычисляем размер заголовка сайта в символах.
  7. Отрезаем 1 байта от конца последовательности, если длина заголовка не превышает 14, в противном случае отрезаем 3 байта .
  8. Полученная последовательность является искомым URL.

Вышеописанный метод является единственным способом добычи URL из Safari в ОС Windows, известным мне.

Вероятно, plist файл можно гораздо проще разобрать с помощью Cocoa и де-сериализации посредством NSArray. Я не пробовал этого сделать, поэтому утверждать не берусь.

Следующая таблица содержит пути, которые ведут к файлу History.plist на разных операционных системах. Подразумевается установка браузера по умолчанию, безо всяких модификаций:

ОС

Размещение

Windows

%APPDATA%/Apple Computer/Safari

Linux

-

Mac OS X

~/Library/Safari

 

 

 

Реализация на C++:

Для начала приведем код вспомогательных методов:

int FindTitleIndex(const QByteArray& FileContent,
    const QString& strTitle, 
    bool IsAscii /*= true*/)
{
    if (!IsAscii)
    {
        QTextCodec* pCodec = QTextCodec::codecForName("UTF-16BE");
        QByteArray Utf16Title = pCodec->fromUnicode(strTitle);
        //Удаляем BOM(специфично для Qt)
        Utf16Title.remove(0, 2);
        Utf16Title.append('[');
        return FileContent.indexOf(Utf16Title);
    }
    return FileContent.indexOf(strTitle);
}
bool IsAscii(const QString& strSource)
{
    const ushort* pData = strSource.utf16();
    auto IsNonAsciiChar = [](ushort Symbol)
    {
        return Symbol > 0x007E;
    };
    auto FoundIt = std::find_if(pData, pData + strSource.size(), 
        IsNonAsciiChar);
    return FoundIt  == (pData + strSource.size());
}

И код основного метода:

QString GetUrlByTitle(const QString& strTitle,
    const QString& strPathToData) 
{
    QString strPathToHistory = strPathToData + "/History.plist";
    QFile File(strPathToHistory);
    if (!File.open(QIODevice::ReadOnly))
    {
        return QString();
    }
    QByteArray FileContent = File.readAll();

    int TitleStart = FindTitleIndex(FileContent, strTitle, 
        IsAscii(strTitle));
    if (TitleStart != -1)
    {
        QByteArray TruncatedContent = FileContent.left(TitleStart);
        int UrlStart = TruncatedContent.lastIndexOf("http"); 
        TruncatedContent.remove(0, UrlStart);
        const int SafariMagicSize = 14;
        int GibberishSymbols = 3;
        if (strTitle.size() <= SafariMagicSize)
            GibberishSymbols = 1;
        TruncatedContent.chop(GibberishSymbols); 
        return TruncatedContent;
    }
    return QString();
}

 


Метод СВ, вновь, работает только для Mac OS X и вновь это AppleScript:

tell application "Safari"
    set theDate to URL of the document of window 1
    return theDate
end tell

Вместо вывода:

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

Методы системных вызовов большей частью были найдены на просторах интернета, хотя и могут быть с легкостью “переизобретены” самостоятельно. За исключением AppleScript в Opera. Тут я пасую, я не знаю как можно вывести этот скрипт самостоятельно; не иначе разработчики Opera когда-то помогли. На момент написания статьи финальными версиями браузеров являлись:

Opera: 11.50
Intenet Explorer: 9.0
Google Chrome: 13.0
Mozilla Firefox: 6.0
Safari: 5.1

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

Удачной вам разработки!

Ну и обещанное дополнение:

Дополнение: Получение заголовка активного окна в различных ОС

Т.к. в разных ОС понятие “активного окна” разнится, дальше будем считать, что активное окно этот то окно с которым пользователь работает в данный момент.

Windows

Тут все просто, даже комментировать не буду:

HWND ActiveWindow = ::GetForegroundWindow();
while(!ActiveWindow) 
{
    ActiveWindow = ::GetForegroundWindow();
}
int Length = ::SendMessage(Window, WM_GETTEXTLENGTH, 0, 0);
//для \0 символа
Length += 1;
std::vector<wchar_t> Buffer(Length);
::SendMessage(Window, WM_GETTEXT, Length, 
    reinterpret_cast<LPARAM>(&Buffer[0]));

Mac OS X

Решение на Mac OS X немного сложнее и состоит из двух частей, сначала получим bundle id активного окна, для этого обратимся за помощью к Objective-C++ и Cocoa:

NSAutoreleasePool* Pool = [[NSAutoreleasePool alloc] init];
NSDictionary* ActiveAppInfo = [[NSWorkspace sharedWorkspace]
    activeApplication];
QString strId = QString::fromUtf([[ActiveAppInfo objectForKey:
    @"NSApplicationBundleIdentifier"] UTF8String]);
[Pool release];

Дальше, применяем раннее полученный id в AppleScript:

tell application id "<подставляем_ранее_полученный_id>"
    try
        if the (count of windows) is not 0 then
            set window_name to name of front window as Unicode text
        end if
        return window_name
    end try
end tell

Q: Почему бы сразу не получить имя активного приложения и уже по нему находить заголовок окна?
A: Потомучто, не все пишут приложения правильно и имя приложения может отличатся от имени bundle. Идентификация по bundle id более надежна, хотя и не является панацеей.

Есть, еще один нюанс, касаемо Firefox. В старых версиях заголовок окна не всегда возвращается. Для того, чтобы появилась возможность считывания заголовка окна необходимо исполнить AppleScript который вызовет ошибку в Firefox. После этого можно смело читать заголовок, до перезапуска Firefox. Вот скрипт, который я использовал для старых версий Firefox:

tell application "Firefox"
    try
        get document of window -1
    end try
    get name of front window as Unicode text
end tell

Linux

А вот тут нас ждет сюрприз Улыбка Все ниженаписанное будет работать только с оконными менеджерами и приложениями, которые подчиняются EWMH спецификации, с остальными приложениями этот код работать не будет(например xcalc)! Я вообще не знаю, что с ними будет работать. Хорошей новостью является то, что KDE и GNOME разрабатываются с оглядкой на эту спецификацию, а значит в большинстве случаев этот код должен вам помочь.

Прежде всего заведем глобальную переменную, которая будет олицетворять текущий Display:

g_pDisplay = ::XOpenDisplay(NULL);

Не забудьте освободить его, когда закончите с ним работать:

::XCloseDisplay(g_pDisplay);

Теперь напишем код получения свойства окна:

QByteArray GetProperty(Window xWindow, 
    Atom XaPropertyType, 
    const char* strPropertyName)
{
    Atom XaPropertyName = ::XInternAtom(g_pDisplay, strPropertyName, False);
    if (XaPropertyName == None)
    {
        //Обрабатайте ошибку соответствующе
    }
    Atom XaActualType = None;
    int ActualFormat = 0;
    unsigned long ItemsNumber = 0;
    unsigned long RemainingBytes = 0;
    unsigned char* pProperty = NULL;
    const long c_MaxPropertySize = 4096;
    /*
         c_MaxPropertySize/4 объяснение может быть найдено в мане по 
         XGetWindowProperty
    */
    int Result = ::XGetWindowProperty(g_pDisplay, xWindow, XaPropertyName,
        0, c_MaxPropertySize / 4, False,
        XaPropertyType, &XaActualType, &ActualFormat,
        &ItemsNumber, &RemainingBytes, &pProperty);
    if (Result != Success) 
    {
        //не получается получить свойство
    }
    if (XaActualType != XaPropertyType) 
    {
        
        //Тип свойства не верен
        ::XFree(pProperty);
        return QByteArray();
    }

    //ActualFormat содержит размер, в битах, каждого элемента в pProperty
    size_t PropertySizeBytes = (ActualFormat / 8) * ItemsNumber;
    QByteArray Property(reinterpret_cast<char*>(pProperty), 
        PropertySizeBytes);
    ::XFree(pProperty);
    return Property;
}

C помощью этого, напишем код получения активного окна:

Window KActivityCollectorX11::GetActiveWindow()
{
    Window ActiveWindow = None;
    
    QByteArray Property = _GetProperty(::XDefaultRootWindow(g_pDisplay),
        XA_WINDOW, "_NET_ACTIVE_WINDOW");
    if (!Property.isEmpty())
    {
        ActiveWindow = *(reinterpret_cast<Window*>(Property.data()));
    }
    else
    {
        //обработайте ошибку
    }
    return ActiveWindow;
}

Наконец, имея активное окно получаем его заголовок:

QString KActivityCollectorX11::GetWindowTitle()
{
    Window ActiveWindow = GetActiveWindow();
    if (ActiveWindow == None)
    {
        return QString();
    }

    QByteArray NetWmName = GetProperty(ActiveWindow, 
        ::XInternAtom(g_pDisplay, "UTF8_STRING", False),
        "_NET_WM_NAME");
    
    QString strTitle;
    if (!NetWmName.isEmpty())
    {
        strTitle = QString::fromUtf8(NetWmName.data());
    }
    else//Окно не подчиняющееся новой спецификации
    {
        QByteArray WmName = GetProperty(m_ActiveWindow, XA_STRING, 
            "WM_NAME");
        if (!WmName.isEmpty())
        {
            gsize OutputSize = 0;
            gchar* strTitleIntUtf8 = ::g_locale_to_utf8(WmName.data(), -1,
                NULL, &OutputSize, NULL);
            if (strTitleIntUtf8)
            {
                strTitle = QString::fromUtf8(strTitleIntUtf8);
                ::g_free(strTitleIntUtf8);
            }
            else
            {
                //обрабатываем ошибку
            }
        }
    }
    return strTitle;
}