Dajbych.net


Jak vytvářet doplňky pro Internet Explorer, díl 2

, 9 minut čtení

ie9 logo

V pokračování článku o vytváření doplňků pro IE si ukážeme, jak přistupovat ke cookies prohlížeče a nenutit uživatele přihlašovat se zvlášť na webové stránky a zvlášť do doplňku prohlížeče. Déle jak lze posílat HTTP požadavky, odesílat data na server a přijímat odpovědi, a to asynchronně bez blokování vláken. A také jak lze manipulovat s obsahem webové stránky, která je v prohlížeči zrovna otevřená, pomocí API jádra IE.

Čtení cookies

K většině úkonů na dnešním webu je potřeba znát identifikátor uživatele.

Akce doplňku prohlížeče spojené s profilem uživatele vyžadují, aby byl uživatel do svého profilu přihlášen. Identifikátor uživatele je pak uložen v cookie prohlížeče. Nejjednodušší způsob, jak ho získat, je přečtení souboru, do kterého Internet Explorer cookie ukládá. Jeden záznam v něm vypadá nějak takto:

webauthtoken
w7tkNt...Q%3D%3D
moje.domena.cz/
1536
1882420992
30229098
2616903031
30155471
*

Algoritmus si nejprve vyžádá cookie soubor požadované domény a poté v něm hledá token, který server převádí na identifikátor uživatele. Hodnota tokenu je uložena pod klíčem webauthtoken.

// read the token
String^ tokenValue;
String^ cookies = Path::Combine(Environment::GetFolderPath(Environment::SpecialFolder::ApplicationData), L"Microsoft\\Windows\\Cookies");
for each (String^ file in Directory::GetFiles(cookies, L"*@www.contoso[*.txt", SearchOption::TopDirectoryOnly)) {
    bool value = false;
    String^ cookieValue = nullptr;
    for each (String^ line in File::ReadLines(file)) {
        if (value) {
            tokenValue = line;
            break;
        } else if (line == _T("webauthtoken")) {
            value = true;
        }
    }
}

V závislosti na jednoznačnosti názvu domény druhého řádu je potřeba kontrolovat správný název domény u samotné hodnoty. Název souboru totiž neobsahuje doménu prvního řádu. Pokud je název domény rozumný, většinou není problém. V některých případech může být ale název souboru celkem divoký. Je proto důležité podívat se do složky jak vlastně IE soubor s cookie vaší domény vlastně pojmenovává.

Token server obdrží spolu s požadavkem a převede ho na identifikátor uživatele. Například pokud server obdrží token v POST datech a používá Windows Live ID, vypadá zpracování takto:

string wlid;
var token = context.Request.Form["webauthtoken"];
var wll = new WindowsLiveLogin(true);
var user = wll.ProcessToken(token);
if (user != null) {
    wlid = user.Id;
} else {
    ...
    return;
}

Úplně odlišná situace nastává, pokud je vaším cílem přečíst cookies právě načtené stránky. K tomu je k dispozici patřičné API:

void STDMETHODCALLTYPE CMyButton::OnDocumentComplete(IDispatch *pDisp, VARIANT *pvarURL) {
    HRESULT hr = S_OK;

    // Query for the IWebBrowser2 interface.
    CComQIPtr<IWebBrowser2> spTempWebBrowser = pDisp;

    // Is this event associated with the top-level browser?
    if (spTempWebBrowser && m_spWebBrowser &&
        m_spWebBrowser.IsEqualObject(spTempWebBrowser)) {
        
        // Get the current document object from browser...
        CComPtr<IDispatch> spDispDoc;
        hr = m_spWebBrowser->get_Document(&spDispDoc);
        if (SUCCEEDED(hr)) {
            
            // ...and query for an HTML document.
            CComQIPtr<IHTMLDocument2> spHTMLDoc = spDispDoc;
            if (spHTMLDoc != NULL) {

                BSTR cookie;
                HRESULT hr = spHTMLDoc->get_cookie(&cookie);
                MessageBox(NULL, cookie, L"Cookie", 0);

            }
        }
    }
}

Má to ovšem zcela zásadní nevýhodu. Spočívá v tom, že stránka z domény, jejíž cookies chceme přečíst, musí být zrovna načtená. Doplněk prohlížeče na stránkách www.contoso.com by tedy znal profil uživatele až v momentě, kdy by uživatel na stránky zavítal. Do té doby by doplněk nefungoval. Proto je vhodné za účelem získání identifikátoru uživatele přečíst konkrétní cookie soubor.

Pokud doplněk nenajde ani cookie v souboru, pak uživatel není na stránce www.contoso.com přihlášen. V tom případě stačí jeden MessageBox, který uživatel vyzve, aby se na stránky přihlásil. Uživatel většinou rozumí tomu, co se po něm chce a na stránku se přihlásit umí. Ve srovnání s vlastním přihlašovacím formulářem doplňku pak není stresován pro něj dosud neznámým uživatelským rozhraním. A hlavní výhoda je, že se ten formulář vůbec nemusí programovat.

Ukázkový skript ke stažení: Doplňky pro IE – cookies

Komunikace se serverem

Komunikace se serverem může být pro doplněk prohlížeče stejně důležitá, jako pro webovou stránku. V tomto díle si ukážeme, jak jednoduchým způsobem s využitím C++/CLI poslat požadavek serveru a jak obdržet odpověď. Zároveň si vysvětlíme, jak to udělat asynchronně a neblokovat tak vlákna.

Odesílání dat na server je nejjednodušší realizovat přes .NET, protože stačí kód rozdělit do tří metod, aby byl asynchronní. Nejprve je potřeba zaraferencovat knihovnu Systém.Web, která HTTP komunikaci zajišťuje. Knihoven je potřeba referencovat co nejméně. V případě objektu pomocníka prohlížeče jejich načítání do paměti zdržuje jeho spouštění.

První metoda, která se volá vždy, když je potřeba na server nějaká data poslat, má na starosti otevření streamu požadavku. Pro pohodlné zpracování dat jsem zvolil simulaci HTML formuláře.

void MyNetClass::Exec() {
    try {
        // prepare web request
        HttpWebRequest^ request = static_cast<HttpWebRequest^>(
          WebRequest::Create(L"http://www.contoso.com/api.ashx"));
        request->Method = L"POST";
        request->ContentType = L"application/x-www-form-urlencoded";
        request->BeginGetRequestStream(gcnew AsyncCallback(GetRequestStreamCallback), request);
        
    } catch (System::Exception^ ex) {
        pin_ptr<const wchar_t> mgs = PtrToStringChars(ex->Message);
        MessageBox(NULL, mgs, _T("IE Toolbar Button"), 0);
    }
}

Text požadavku obsahuje jednotlivé hodnoty. V tomto případě webauthtoken k identifikaci uživatele a action k identifikaci toho, co že to má vlastně server s požadavkem dělat. Metoda UrlEncode upraví řetězec pro použití v textu požadavku. U parametru action není použita, protože předpokládám, že bude nabývat jen hodnot definovaných v rámci organizace.

Nejprve se vytvoří řetězec data, který se následně vepíše do streamu. To je v pořádku jen pro kratší řetězce (a přehledné ukázky zdrojových kódů). Odesílání většího objemu dat (například obsahu souboru) tímto způsobem už ale v pořádku není. Tam je potřeba vepisovat jednotlivé kusy dat po kouskách přímo do streamu.

void MyNetClass::GetRequestStreamCallback(IAsyncResult^ asynchronousResult) {
    try {
        HttpWebRequest^ request = static_cast<HttpWebRequest^>(asynchronousResult->AsyncState);
        Stream^ stream = request->EndGetRequestStream(asynchronousResult);

        // prepare data
        String^ data = L"webauthtoken=" + HttpUtility::UrlEncode(tokenValue) + L"&action= ActionName";
        array<unsigned char>^ post = Encoding::ASCII->GetBytes(data);
        stream->Write(post, 0, post->Length);
        stream->Close();

        // send the request
        request->BeginGetResponse(gcnew AsyncCallback(GetResponseCallback), request);

    } catch (System::Exception^ ex) {
        pin_ptr<const wchar_t> mgs = PtrToStringChars(ex->Message);
        MessageBox(NULL, mgs, _T("IE Toolbar Button"), 0);
    }
}

Poslední metoda má na starosti zpracování odpovědi serveru. Mám ve zvyku vrátit řetězec OK, pokud vše proběhlo bez chyby. V opačném případě vrátím přímo text chyby. Pokud odpověď serveru obsahuje data ke zpracování, je na místě zjišťování chyby z HTTP status kódu. Tady už ale záleží především na vašich zvyklostech.

void MyNetClass::GetResponseCallback(IAsyncResult^ asynchronousResult) {
    try {

        // read the response
        HttpWebRequest^ request = static_cast<HttpWebRequest^>(asynchronousResult->AsyncState);
        HttpWebResponse^ response = static_cast<HttpWebResponse^>(request->EndGetResponse(asynchronousResult));
        Stream^ stream = response->GetResponseStream();
        StreamReader^ reader = gcnew StreamReader(stream);
        String^ answer = reader->ReadToEnd();

        if (answer != L"OK") throw gcnew Exception(answer);

        stream->Close();
        reader->Close();
        response->Close();

    } catch (System::Exception^ ex) {
        pin_ptr<const wchar_t> mgs = PtrToStringChars(ex->Message);
        MessageBox(NULL, mgs, _T("IE Toolbar Button"), 0);
    }
}

Je možné však využít celou řadu dalších možností. Například webové služby, komponenty Managed Extensibility Frameworku, připojení do SQL Serveru, zařízení připojená do PC, zkrátka celý .NET Framework.

Ukázkový skript ke stažení: Doplňky pro IE – komunikace

Manipulace s DOM

Na závěr si ukážeme, jak přistupovat k samotné webové stránce, manipulovat s její strukturou a měnit kaskádové styly. Protože se jedná přímo o API jádra IE, ke kterému lze přistupovat jen pomocí C++, managed kód nám tentokrát nemůže nijak pomoci.

Nejprve si ukážeme, jak docílit toho, aby po načtení stránky zmizely všechny obrázky. K tomu je potřeba vytvořit objekt pomocníka prohlížeče. Opět se jmenuje CMyButton, ale jen proto, aby byly ukázky zdrojových kódů kompatibilní napříč celým seriálem. Pokud není třída zaregistrována zároveň jako rozšíření prohlížeče, nemá s tlačítkem nic společného.

void STDMETHODCALLTYPE CMyButton::OnDocumentComplete(IDispatch *pDisp, VARIANT *pvarURL) {
    HRESULT hr = S_OK;

    // Query for the IWebBrowser2 interface.
    CComQIPtr<IWebBrowser2> spTempWebBrowser = pDisp;

    // Is this event associated with the top-level browser?
    if (spTempWebBrowser && m_spWebBrowser &&
        m_spWebBrowser.IsEqualObject(spTempWebBrowser)) {
        
        // Get the current document object from browser...
        CComPtr<IDispatch> spDispDoc;
        hr = m_spWebBrowser->get_Document(&spDispDoc);
        if (SUCCEEDED(hr)) {
            
            // ...and query for an HTML document.
            CComQIPtr<IHTMLDocument2> spHTMLDoc = spDispDoc;
            if (spHTMLDoc != NULL) {
                
                // Finally, remove the images.
                RemoveImages(spHTMLDoc);
            }
        }
    }
}

Protože načtení dokumentu může proběhnout i v rámci a typ dokumentu vůbec nemusí být HTML či XHTML, provede se nejprve několik kontrol. Následně se zavolá metoda RemoveImages, která se postará o samotné skrytí obrázků.

void CMyButton::RemoveImages(IHTMLDocument2* pDocument) {
    CComPtr<IHTMLElementCollection> spImages;

    // Get the collection of images from the DOM.
    HRESULT hr = pDocument->get_images(&spImages);
    if (hr == S_OK && spImages != NULL) {
        
        // Get the number of images in the collection.
        long cImages = 0;
        hr = spImages->get_length(&cImages);
        if (hr == S_OK && cImages > 0) {
            for (int i = 0; i < cImages; i++) {
                CComVariant svarItemIndex(i);
                CComVariant svarEmpty;
                CComPtr<IDispatch> spdispImage;

                // Get the image out of the collection by index.
                hr = spImages->item(svarItemIndex, svarEmpty, &spdispImage);
                if (hr == S_OK && spdispImage != NULL) {
                    // First, query for the generic HTML element interface...
                    CComQIPtr<IHTMLElement> spElement = spdispImage;

                    if (spElement) {
                        // ...then ask for the style interface.
                        CComPtr<IHTMLStyle> spStyle;
                        hr = spElement->get_style(&spStyle);

                        // Set display="none" to hide the image.
                        if (hr == S_OK && spStyle != NULL) {
                            static const CComBSTR sbstrNone(L"none");
                            spStyle->put_display(sbstrNone);
                        }
                    }
                }
            }
        }
    }
}

Document object model, mnohem častěji však zkráceně DOM, je struktura, která popisuje webovou stránku. HTML či XHTML je nejprve převedeno na DOM. Ten se následně převádí na display tree, na kterém jsou aplikovány veškeré kaskádové styly. Teprve ten se pak renderuje. DOM je reprezentován rozhraním IHTMLDocument2.

Rozhraní HTML dokumentu zpřístupňuje metodu get_images, která vrací všechny obrázky. Přes ně se poté iteruje, přičemž se jednotlivé položky kolekce převádí na rozhraní IHTMLElement. K CSS není nutné přistupovat přes atributy elementu a pracovat s CSS v textovém režimu. Místo toho je možné použít rozhraní IHTMLStyle, které umožňuje nastavit přímo jednotlivé vlastnosti.

Rozhraní IHTMLDocument2 má metody na vrácení aktivního elementu, vytvoření nového a otevření okna. Poskytuje události na stisknutí klávesy, tlačítka myši, označení textu, najetí myši na element a mnoho dalšího. Je k dispozici plus mínus vše, co je dostupné z JavaScriptu. Podle toho, jak se API s novějšími verzemi IE rozšiřuje, je možné dívat se na DOM přes nová rozhraní. IE9 tak má už IHTMLDocument7.

A kolik že těch rozhraní, které Internet Explorer nabízí, vlastně je? Devátá verze jich má přes pět set. Necelá stovka přibyla s devátou verzí kvůli SVG. Ale například Canvas jich má jen sedm, XmlHttpRequest pět. Naprostá většina rozhraní je však pro HTML. Celý jejich seznam i s dokumentací je samozřejmě k dispozici na MSDN Library.

Ukázkový skript ke stažení: Doplňky pro IE – manipulace s DOM

Závěr

Pokud vás někdy napadla funkce, kterou by stálo za to mít v prohlížeči, ale programovat kvůli ní celý prohlížeč nemá cenu, je rozšíření prohlížeče nejsnazší způsob, jak si něco odzkoušet. Internet Explorer totiž nenabízí jen okno, do kterého se vykreslují webové stránky (i když i to je samo o sobě obrovským přínosem), ale poskytuje platformu pro zpracování webových stránek.

Článek byl sepsán pro Zdroják.