pirosikick's diary

君のハートにunshift

Titanium MobileでTwitterOAuth時のエラーと解決方法

最近、Titanium Mobileにはまってます。とりあえず下記記事を参考にTwitterクライアントを開発中です。

連載:Titanium Mobileで作る! iPhone/Androidアプリ|gihyo.jp … 技術評論社

OAuth認証で結構四苦八苦したので、備忘録&情報シェアのために書きなぐります。


Parseエラー

4回目の記事でOAuthの実装が書かれているんだが、いざTwitterのログイン画面がポップアップ(Ti.UI.webview)で表示されると5秒くらいで落ちてしまう。。その時に吐き出されるエラーが下記。

[ERROR] Error Domain=com.google.GDataXML Code=-1 "The operation couldn’t be completed. (com.google.GDataXML error -1.)". in -[TiDOMDocumentProxy parseString:] (TiDOMDocumentProxy.m:50)
[ERROR] The application has crashed with an unhandled exception. Stack trace:
0 CoreFoundation 0x0232b58c __exceptionPreprocess + 156
1 libobjc.A.dylib 0x0247f313 objc_exception_throw + 44
2 ShizuTwi 0x0009c2b4 -[TiProxy throwException:subreason:location:] + 478
3 ShizuTwi 0x0010119c -[TiDOMDocumentProxy parseString:] + 572
~ 以下略 ~

oauth_adapter.js

4回目の記事では、oauth_adapter.jsを使って実装している。

oauth-adapter -


OAuth Adapter for Appcelerator Titanium - Google Project Hosting

oauth_adapter.jsでは、ポップアップでOAuthの認証画面を表示して、ログイン後に表示されるPINコードを読み込んでそれを元にAccessTokenを取得してくれる。

どうやってPINコードを読み込んでいるか?

WebViewが読み込まれると、oauth_adapter.jsのauthorizeUICallbackという関数が呼ばれる。

authorizeUICallbackでは、

  1. 表示されているログインページのソースをTi.XML.parseStringでパースしてDOMに変換
  2. getElementsByTagName('div')でdiv要素を全て取り出す
  3. idが「oauth_pin」であるdiv要素を探し、その要素の中身をPINとする。
    // looks for the PIN everytime the user clicks on the WebView to authorize the APP
    // currently works with TWITTER
    var authorizeUICallback = function(e)
    {
        Ti.API.debug('authorizeUILoaded');

       // 1. 表示されているログインページのソースをTi.XML.parseStringでパースしてDOMに変換
        var xmlDocument = Ti.XML.parseString(e.source.html);

        // 2. getElementsByTagName('div')でdiv要素を全て取り出す
        var nodeList = xmlDocument.getElementsByTagName('div');

        for (var i = 0; i < nodeList.length; i++)
        {
            var node = nodeList.item(i);
            var id = node.attributes.getNamedItem('id');

            // 3. idが「oauth_pin」であるdiv要素を探し、その要素の中身をPINとする。
            if (id && id.nodeValue == 'oauth_pin')
            {
                pin = node.text;

                if (receivePinCallback) setTimeout(receivePinCallback, 100);

                id = null;
                node = null;

                destroyAuthorizeUI();

                break;
            }
        }

        nodeList = null;
        xmlDocument = null;

    };

原因

そのエラーが発生する前に下記のようなログが大量に吐き出される。

Entity: line 18: parser error : Opening and ending tag mismatch: link line 7 and head

^
Entity: line 44: parser error : Opening and ending tag mismatch: img line 42 and h3

^

DOMパースでエラーが発生しているみたい。。


解決

エラーで検索したりしたけども結局いい解決方法が見つからず、しょうがないのでoauth_adapter.jsを修正しました。修正内容としては、DOMにパースするのではなくevalJSを使ってPINを取得するように変更しました。

evalJSとは

evalJSとは、Ti.UI.webviewの関数で、webViewで表示されているページでJavascriptを実行しその結果を文字列で返す関数です。例えば、下記のようにするとの中身をログに出力します。

Ti.API.debug(webview.evalJS('window.document.body.innerHTML'));
querySelectorでPINを取得

TwitterのOAuthの画面で、PINが表示される部分の構造は下記のようになっています。

<div id="oauth_pin">
<p>
<span id="code-desc">Next, return to <アプリケーション名> and enter this PIN to complete the authorization process:</span>
<kbd aria-labelledby="code-desc"><code><ここにPINコードが表示される></code></kbd>
</p>
</div>

なので、e.sourceが表示されているログインページのWebViewだとすると、

var res = e.source.evalJS('window.document.querySelector(\'kbd[aria-labelledby="code-desc"] > code\').innerHTML');

↑でPINコードを取得することができる。


authorizeUICallbackを修正

下記のように修正しましたー。

    var authorizeUICallback = function(e)
    {
        Ti.API.debug('authorizeUILoaded');

        if (!e || !e.source) {
            return;
        }

        var res = e.source.evalJS('window.document.querySelector(\'kbd[aria-labelledby="code-desc"] > code\').innerHTML');

        if (res) {

            pin = res;

            if (receivePinCallback) setTimeout(receivePinCallback, 100);

            destroyAuthorizeUI();
        }
    };