ページ

2009年8月31日月曜日

NSHTTPCookieStorage相当のクラスを自前で実装する (6)クッキー送出のロジックを改良

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

前回までは URL に適合するクッキーの検索は、格納してある全てのクッキーを対象に行っていた。これではクッキーの数が増えた場合に効率が悪いので少し改良を加える。複雑なことはやらず、できるだけ単純にしてみよう。

具体的にはドメインをキーとしたハッシュ(辞書)を作り、その下に実際のクッキー配列を格納するようにする。

(イメージ)

NSMutableDictionary
|
|--key:".xcatsan.com"
| value: NSMutableDictionary
| |-- key:"/xxx", value={path="/", name="xxx", ..}
| |-- key:"/some/yyy", value={path="/some", name="yyy", ..}
| :
|
|--key:".co-co-adays.com"
| value: NSMutableDictionary
| |-- key:"/aaa", value={path="/", name="aaa", ..}
| |-- key:"/some/bbb", value={path="/some", name="bbb", ..}
| :


ドメイン名をキーとして取得できる値は NSMutableDictionary とする。path+name をキーとして実際のクッキーをその値として登録する。これによって domain+path+name でユニークが保たれる。


なおドメインは後方一致もありなので、単純なキー検索だけでは漏れが生じてしまう。
この為、検索時にはドメインの完全一致に加えて、後方一致も加える。

(イメージ)
www.japan.xcatsan.com のクッキーを検索する。

(1) www.japan.xcatsan.com をキーにして検索
(2) .japan.xcatsan.com をキーにして検索
(3) .xcatsan.com をキーにして検索

上記でヒットする配列群を前回までのロジックでチェックする。

(続く)

2009年8月30日日曜日

SimpleCap on Snow Leopard

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

自宅の Macに Snow Leopard をインストール。
SimpleCapの挙動を確認したところ大丈夫そう。
ちょっとホッとした。

Xcode は 3.2 に上がったようだ。

2009年8月29日土曜日

NSHTTPCookieStorage相当のクラスを自前で実装する (5)クッキー送出ではより詳細な方を返す

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

クッキー送出の処理で、同じドメイン内で同じ名前を持つクッキーがある場合はより詳細な方を返す。

例)pathが "/acme" と "/acme/ammo" で同じ nameの Cookieが存在する場合、
 a) リクエストURLのパスが "/acme/ammo/test.html" の場合、後者を返す
 b) リクエストURLのパスが "/acme/parts/some.html" の場合、前者を返す


前回のコードをこれに合わせて書き換えてみる。

XCHTTPCookieStorage.m
- (NSArray *)cookiesForURL:(NSURL *)URL
{
// NSMutableArray* return_cookies = [NSMutableArray array];
NSMutableDictionary* cookie_dicts = [NSMutableDictionary dictionary];
:
NSHTTPCookie* stored_cookie;
if (stored_cookie = [cookie_dicts valueForKey:cookie_name]) {
if ([cookie_path length] <= [[stored_cookie path] length]) {
continue;
}
}
// [return_cookies addObject:cookie];
[cookie_dicts setObject:cookie forKey:cookie_name];
}

// return return_cookies;
return [cookie_dicts allValues];
}


該当するクッキーを格納するクラスを、NSMutableArrayをやめて NSMuableDictionaryに変更する。キーにクッキーの名前を使うことで同一のドメイン内のクッキーの名前はユニークになる。同一の名前が存在する場合は、詳細な方=すなわちパスが長い方を選ぶようにした。

2009年8月28日金曜日

NSHTTPCookieStorage相当のクラスを自前で実装する (4)クッキー送出に成功

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

どうにかこうにかクッキーの送出に成功した。

Googleのログイン画面でID,パスワードを入力すると、


無事にログイン状態となった。


ハマったのは前回紹介したリダイレクト時のクッキーの扱いと、謎のクラッシュ。後者はいろいろ手を入れている間に直ってしまった。もしかすると NSLog() で表示しているオブジェクトに問題があったのかもしれない(NSLog()を一通り消した後、改善していたので)。

サンプル:CookieStorage-2.zip


以下、コード解説。

まず XCHTTPCookieStorageを使う側、WebResourceLoadDelegate のメソッドから。
AppController.m

- (NSURLRequest *)webView:(WebView *)sender
resource:(id)identifier
willSendRequest:(NSURLRequest *)request
redirectResponse:(NSURLResponse *)redirectResponse
fromDataSource:(WebDataSource *)dataSource
{
[(NSMutableURLRequest*)request setHTTPShouldHandleCookies:NO];

if (redirectResponse) {
[self _setCookiesWithResponse:(NSHTTPURLResponse*)redirectResponse];
}

// NSLog(@"%@", [request URL]);

NSArray* cookies =
[[XCHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:[request URL]];

if ([cookies count] > 0) {
NSDictionary* cookie_fields =
[NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
[(NSMutableURLRequest*)request setAllHTTPHeaderFields:cookie_fields];
}

return request;
}


リダイレクト時には redirectResponse が非nilになるので、サーバから受け取ったクッキーを _setCookiesWithResponse: を使い保存する。

URLに適合するクッキーが存在する場合は、-[NSMutableURLRequest setAllHTTPHeaderFields:]を使い、リクエストヘッダに "Cookie"ヘッダを追加する。

レスポンスは _setCookiesWithResponse: を読んでクッキーを保存するだけ。
- (void)webView:(WebView *)sender
resource:(id)identifier
didReceiveResponse:(NSURLResponse *)response
fromDataSource:(WebDataSource *)dataSource
{
[self _setCookiesWithResponse:(NSHTTPURLResponse*)response];
}


_setCookiesWithResponse: の実装はこう。
- (void)_setCookiesWithResponse:(NSHTTPURLResponse*)response
{
NSDictionary* headers = [(NSHTTPURLResponse*)response allHeaderFields];
NSURL* URL = [response URL];
NSArray* cookies =
[NSHTTPCookie cookiesWithResponseHeaderFields:headers forURL:URL];

if ([cookies count] > 0) {
[[XCHTTPCookieStorage sharedHTTPCookieStorage] setCookies:cookies
forURL:URL
mainDocumentURL:URL];
}
}



次は XCHTTPCookieStorage の実装〜URLに合ったクッキーを返す処理。
※以前紹介したクッキーの送出ルールはまだ完全に実装できていない。

XCHTTPCookieStorage.m
- (NSArray *)cookiesForURL:(NSURL *)URL
{
NSMutableArray* return_cookies = [NSMutableArray array];
NSString* url_host = [URL host];
NSString* url_path = [URL path];
NSDate* date = [NSDate date];
BOOL is_secure = [[URL scheme] isEqualToString:@"https"];

for (NSHTTPCookie* cookie in [_cookies allValues]) {

NSString* cookie_path = [cookie path];
NSString* cookie_domain = [cookie domain];
NSDate* cookie_expires_date = [cookie expiresDate];

// secure
if ([cookie isSecure] && !is_secure) {
continue;
}

// expires
if (cookie_expires_date &&
[date compare:cookie_expires_date] == NSOrderedDescending) {
continue;
}

// domain
//*TODO* cookie_domain: .www.google.com
if ([cookie_domain hasPrefix:@"."]) {
if (![url_host hasSuffix:cookie_domain]) {
continue;
}
} else if (![url_host isEqualToString:cookie_domain]) {
continue;
}

// path
if (![cookie_path isEqualToString:@"/"]) {
if (![url_path isEqualToString:cookie_path]) {
if (![url_path hasPrefix:[cookie_path stringByAppendingString:@"/"]]) {
continue;
}
}
}
[return_cookies addObject:cookie];
}
return return_cookies;
}


格納されているクッキーを一つ一つ、セキュリティ、有効期限、ドメイン、パス、とチェックしていく。
#この単純な実装ではクッキーが多くなると時間がかかるので工夫が必要だな。

- - - -
とりあえずクッキーの受け取りと送出の基本動作ができるようになった。この後はクッキーの仕様に合った実装を行って完成度を上げていき、最後に永続化処理を入れて完成させる。

2009年8月27日木曜日

リダイレクト時のクッキー受け取り

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

クッキーの自前ハンドリングを行っていてどうも受け取れていないクッキーが存在する。調べていると次のことが分かった。

1. 通常のレスポンス(200)の場合は、WebResourceLoadDelegateのメソッド が呼び出され、引数で渡される response からサーバが送出したクッキーを取り出すことができる。

2. リダイレクト(301等)の場合は、webView:resource:didReceiveResponse:fromDataSource: が呼ばれない。クッキーは webView:resource:willSendRequest:redirectResponse:fromDataSource: で渡される redirectResponse から取得する。


Googleのログインなどではリダイレクト時にもクッキーを送出しているのでこれも拾ってやる必要がある。

- - - -
クッキー送出の実装は苦戦中。上記問題が片付いたが、Googleへログインすると何故か強制終了してしまう...

Debugger() was called!

The Debugger has exited due to signal 2 (SIGINT).The Debugger has exited due to signal 2 (SIGINT).

2009年8月26日水曜日

NSHTTPCookieStorage相当のクラスを自前で実装する (3)クッキーの送出

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

今回は受け取ったクッキーを送出する処理を加える。

前回のサンプルは受け取りだけしか行わないので、そのまま google.cmのログイン処理を行っても警告が出てログインできない。


今回のゴールはクッキーを無事送出してログインできるようにすること。


[Studying HTTP] HTTP Cookiesを参考に送出のルールをまとめてみた。

Cookie送出ルール
1. expires(有効期限)< 現在日時

2. domain がリクエストURLのホスト名と後方一致する
  (例)domainが ".xcatsan.com" の場合、"www.xcatsan.com" はOK

3. path がリクエストURLのパスと前方一致する(ただしパスの区切りを考慮)
  (例)pathが "/foo" の場合、"/foo/bar.html" は OK
     pathが "/foo" の場合、"/foooo/bar.html" は NG (*)

4. secure が TRUEの場合、HTTPSのみ OK

5. 同じ domain 内で、同じ name の Cookieが存在する場合は、
  より詳細な方を返す。
  (例)pathが "/acme" と "/acme/ammo" で同じ nameの Cookieが存在する場合、
     a) リクエストURLのパスが "/acme/ammo/test.html" の場合、後者を返す
     b) リクエストURLのパスが "/acme/parts/some.html" の場合、前者を返す


上記をざっくりと実装してみよう。

2009年8月25日火曜日

NSHTTPCookieStorage相当のクラスを自前で実装する (2)クッキーの受け取り

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

最初から保存やら同期やらすべてを実装すると大変なので、まずはメモリ上だけでクッキーのやりとりを行う実装から始めよう。当面はラフな実装で動作確認を先行して行い、徐々に精度を高めていくやり方で進める。

今回はクッキーの受け取り処理を実装する。


(1) クライアント側の用意

まず WebView の WebResourceLoadDelegate として AppController を指定し、必要なデリゲートメソッドを実装する。

リクエスト処理では、setHTTPShouldHandleCookies を使い、デフォルトの NSHTTPCookieStorage の使用を中止する。
AppController.m

// WebResourceLoadDelegate
- (NSURLRequest *)webView:(WebView *)sender
resource:(id)identifier
willSendRequest:(NSURLRequest *)request
redirectResponse:(NSURLResponse *)redirectResponse
fromDataSource:(WebDataSource *)dataSource
{
[(NSMutableURLRequest*)request setHTTPShouldHandleCookies:NO];
return request;
}


続いてレスポンス処理では、受け取ったヘッダ情報を XCHTTPCookieStorageへ渡す。
- (void)webView:(WebView *)sender
resource:(id)identifier
didReceiveResponse:(NSURLResponse *)response
fromDataSource:(WebDataSource *)dataSource
{
NSDictionary* headers = [(NSHTTPURLResponse*)response allHeaderFields];
NSURL* URL = [response URL];

NSArray* cookies =
[NSHTTPCookie cookiesWithResponseHeaderFields:headers forURL:URL];

[[XCHTTPCookieStorage sharedHTTPCookieStorage] setCookies:cookies
forURL:URL
mainDocumentURL:URL];
}



(2) XCHTTPCookieStorage の実装

XCHTTPCookieStorage.m

初期化。
- (id)init
{
if (self = [super init]) {
_cookie_accept_policy = NSHTTPCookieAcceptPolicyAlways;
_cookies = [[NSMutableDictionary alloc] init];
}
return self;
}


インスタンス取得(シングルトン)。
+ (XCHTTPCookieStorage *)sharedHTTPCookieStorage
{
static XCHTTPCookieStorage* _cookie_storage = nil;

if (!_cookie_storage) {
_cookie_storage = [[XCHTTPCookieStorage alloc] init];
}
return _cookie_storage;
}


渡されたクッキーを内部処理する(保存する)。
- (void)setCookies:(NSArray *)cookies forURL:(NSURL *)URL mainDocumentURL:(NSURL *)mainDocumentURL
{
for (NSHTTPCookie *cookie in cookies) {
[self setCookie:cookie];
}
}


_keyForCookie: で生成される文字列をキーとして、NSMutableDictionaryへ格納する。
- (void)setCookie:(NSHTTPCookie *)cookie
{
[_cookies setValue:cookie forKey:[self _keyForCookie:cookie]];
}



_keyForCookie: はドメイン名、パス、名前を '/'区切りで連結したものを返す。この辺りは以前参考にした mySTEP の方法を借用した。
- (NSString*)_keyForCookie:(NSHTTPCookie*)cookie
{
return [NSString stringWithFormat:@"%@/%@/%@",
[cookie domain], [cookie path], [cookie name]];
}



実行時に NSDictionary に格納しているクッキーが見られるように「print cookies」ボタンを追加し、-[AppController printCookies:] へ接続する。


- (IBAction)printCookies:(id)sender
{
NSLog(@"%@", [[XCHTTPCookieStorage sharedHTTPCookieStorage] cookies]);
}



実行してみよう。
google.comを開く。


この時のクッキーは次の通り。既に1つ受け取っている。


ページの右上にある「ログイン」リンクを押す。


クッキーを見ると、1つ増えている。



サンプル:
CookieStorage-1.zip

- - - -
粗い実装だがまずは受け取りができるようなった。次はクッキー送出に取りかかってみる。

2009年8月24日月曜日

Cocoaでサンプルコードを書く時のひな形の作り方

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

サンプルプログラムのひな形の作り方の紹介。

初めて使うクラスの検証やちょっとした動作確認するときにこのひな形があると便利。

全部で 3step
1. 新規プロジェクト作成
2. クラス作成
3. InterfaceBuilderでインスタンス作成



1. 新規プロジェクト作成

Xcodeを立ち上げて新規プロジェクトを作成する。

"Cocoa Application" を選ぶ。

ここでは Sample という名前でプロジェクトを保存する。

プロジェクトが用意できた。



2. クラス作成

検証用コードを記述するクラスを1つ作成する。メニューから「新規ファイル...」を選ぶ。

"Objective-C class" を選択する。

名前は "AppController" とする。


AppController.m と AppController.h ファイルが作成される。

AppController.m を開き、メソッド awakeFromNib を実装する。Nib (Xib)ファイルが読み込まれた後、このメソッドが呼び出される。検証コードはたいていここへ記述する。



3. InterfaceBuilderでインスタンス作成

"MainMenu.xib" ファイルをダブルクリックする。



InterfaceBuilderが起動する。



Library で Object を選択し MainMenu.xib へ配置する。

配置された状態はこのようになる。

Inspector の "Object Identity"タブを開き、Classに "AppController" と入れる。

MainMenu.xib 上も表記が "App Controller" に変わる。

こうしておくとアプリ起動時に MainMenu.xib が読み込まれ AppControllerが自動的にインスタンス化される。



これで一通り終わり。ビルドして実行してみよう。


空のウィンドウが立ち上がる(それだけ)。


この後は非GUIの検証なら必要なコードを書いて結果を NSLog()を使ってデバッガコンソールへ吐き出すだけ。もし GUIが絡む検証なら InterfaceBuilderで GUIを設定し、AppControllerにアウトレットを追加したりする。

(おしまい)

- - - -
当ブログでサンプルプログラムを作るときはいつもこれを繰り返し行っている。
(コピーの使いまわしや自動化する方法もあるが、ボケ防止で毎回手でやってます...)

2009年8月23日日曜日

SimpleCap 隠し機能(Cmd+S で縮小保存)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

バージョン1.0には見合わせた機能にSimpleViewerの縮小保存がある。
実はリリース後に気がついたのだが、保存ボタンは取り除いたもののショートカットを無効にするのを忘れていた。

その事に気がついてから使い始めてみると結構便利なことがわかった。そこで現時点では隠し機能になってしまうが紹介する。次バージョンでは採用する予定。

まず SimpleViewerで画像を縮小表示する。いい大きさになったら Command+S を押す。


するとその時点での縮小率を使い新しいファイルを作成する。元のファイル名に縮小率をつけたものになる。

2009年8月22日土曜日

NSHTTPCookie の同値性チェック -[isEqual:]

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

クッキーを管理する上では2つのクッキーが同じものかどうかを判断する(=同値性をチェックする)必要がある。

以前参考にしたソースコードでは domain + path + name の3つで判断していた。これを採用するにしても、本来同値性チェックの責務は NSHTTPCookie 側で持った方が良いと考えられるのでサブクラスを作りそのロジックを実装しよう。と、思ったが NSHTTPCookie はその辺り考えて作ってあるのでは?という考えが浮かんできた。そこでコードを書いて簡単に NSHTTPCookie の同値性を調べてみることにする。

まずは domain + path + name の組み合わせで判断されているかどうか。

domain + path + name が同じクッキーを作成して -[isEqual:] を使って同値性をチェックする。

 NSDictionary* properties1 =
[NSDictionary dictionaryWithObjectsAndKeys:
@"/" , NSHTTPCookiePath,
@".xcatsan.com" , NSHTTPCookieDomain,
@"sample" , NSHTTPCookieName,
@"hello" , NSHTTPCookieValue,
nil];

NSDictionary* properties2 =
[NSDictionary dictionaryWithObjectsAndKeys:
@"/" , NSHTTPCookiePath,
@".xcatsan.com" , NSHTTPCookieDomain,
@"sample" , NSHTTPCookieName,
@"hello" , NSHTTPCookieValue,
nil];

NSHTTPCookie* cookie1 = [NSHTTPCookie cookieWithProperties:properties1];
NSHTTPCookie* cookie2 = [NSHTTPCookie cookieWithProperties:properties2];

NSLog(@"cookie1: %@", cookie1);
NSLog(@"cookie2: %@", cookie2);

if ([cookie1 isEqual:cookie2]) {
NSLog(@"[cookie1 isEqual:cookie2]: YES");
} else {
NSLog(@"[cookie1 isEqual:cookie2]: NO");
}


結果は YES。良さそう。
2009-08-22 12:59:38.286 CookieStorage[9346:10b] cookie1: 
2009-08-22 12:59:38.293 CookieStorage[9346:10b] cookie2:
2009-08-22 12:59:38.297 CookieStorage[9346:10b] [cookie1 isEqual:cookie2]: YES


次に cookie2のパスを /some に変えてみる。

結果は NO。
2009-08-22 13:02:21.825 CookieStorage[9382:10b] cookie1: 
2009-08-22 13:02:21.831 CookieStorage[9382:10b] cookie2:
2009-08-22 13:02:21.837 CookieStorage[9382:10b] [cookie1 isEqual:cookie2]: NO


その他、domain, name を変えても同様に NO。これは良さそうだ。

が、value を変えると不一致となった。
[Session started at 2009-08-21 13:06:29 +0900.]
2009-08-22 13:06:31.789 CookieStorage[9497:10b] cookie1:
2009-08-22 13:06:31.795 CookieStorage[9497:10b] cookie2:
2009-08-22 13:06:31.796 CookieStorage[9497:10b] [cookie1 isEqual:cookie2]: NO


うーん、駄目か。
その他のエントリも念のため調べてみた。
同値性条件に入る値
NSHTTPCookieComment
NSHTTPCookieCommentURL
NSHTTPCookieDomain
NSHTTPCookieName
NSHTTPCookiePath
NSHTTPCookieSecure
NSHTTPCookieValue
NSHTTPCookieVersion

同値性条件に入らない値
NSHTTPCookieDiscard
NSHTTPCookieExpires
NSHTTPCookieMaximumAge
NSHTTPCookieOriginURL
NSHTTPCookiePort

※上記はラフに確認したので不正確なところもあるかもしれない。うまく設定できなかった値もあった。

- - - - -
本来の同値性と言う意味からは NSHTTPCookieの isEqualの動作はこれで良いのだろう。が、domain + path + name で同一視したい今回の要件では使えない。やはり自前で判断する必要がある。サブクラス化するかどうか。

2009年8月21日金曜日

NSHTTPCookieStorage相当のクラスを自前で実装する (1)スタート

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

それではいよいよ NSHTTPCookieStorage 相当のクラスを自前で実装していく。

仕様はこんな感じ。

クラス名:XCHTTPCookieStorage

メソッド:NSHTTPCookieStorage準拠

+ (XCHTTPCookieStorage *)sharedHTTPCookieStorage;
- (NSArray *)cookies;
- (void)setCookie:(NSHTTPCookie *)cookie;
- (void)deleteCookie:(NSHTTPCookie *)cookie;
- (NSArray *)cookiesForURL:(NSURL *)URL;
- (void)setCookies:(NSArray *)cookies forURL:(NSURL *)URL mainDocumentURL:(NSURL *)mainDocumentURL;
- (NSHTTPCookieAcceptPolicy)cookieAcceptPolicy;
- (void)setCookieAcceptPolicy:(NSHTTPCookieAcceptPolicy)cookieAcceptPolicy;


永続化方法:plistファイル

複数アプリでの共有:不可
=> ファイル名はアプリ毎に違ったものにする

- - - -
クラス実装にあたって動作確認用のプロジェクトを作った。



サンプル:CookiStorage-0.zip

今は簡易ブラウズ機能だけだが、これからここへクッキー管理のクラスを実装し動作確認を取っていく。

2009年8月20日木曜日

NSHTTPCookieStorage.m のソースコードを読む

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

前回の続き。今回は NSHTTPCookieStorage.m のソースコードを読む。

ソースコードは Apple提供のものではなく mySTEP(QuantumSTEP)提供のヤツ。

NSHTTPCookieStorage.h

NSHTTPCookieStorage.m


まずはヘッダファイルから。

NSHTTPCookieStorage.h

#import 

@class NSMutableArray, NSMutableDictionary, NSDistributedLock, NSDate, NSTimer;

typedef enum _NSHTTPCookieAcceptPolicy
{
NSHTTPCookieAcceptPolicyAlways=0,
:
} NSHTTPCookieAcceptPolicy;
:
@interface NSHTTPCookieStorage : NSObject
{
NSMutableDictionary *_cookies;
NSDistributedLock *_lock;
NSDate *_lastModification;
NSHTTPCookieAcceptPolicy _cookieAcceptPolicy;
NSTimer *_timer;
BOOL _touched;
}
: 以下、メソッド定義が続く
:


メンバ変数は名前からおおよその用途がわかる。_lock はファイル書き出しで使うのだろう。_lastModification はクッキー情報全体の最終更新日時で、書き出し時の判断で使うのだろうか。NSTimer も使われているが、非同期書き出し(定期書き出し)にでも使うのかもしれない。_touched は変更の有無か。


次は NSHTTPCookieStorage.m。上から読んでいこう。


#define COOKIE_DATABASE @"~/Library/Cookies/Cookies.plist" // Array of Dictionaries
#define COOKIE_DATABASE_LOCK COOKIE_DATABASE@".lock"

まず定数定義。べた書きで Cookies.plist と ロックファイルの名前が定義されている。


クラスメソッド。一旦作成したインスタンスを使い回すシングルトン。
+ (NSHTTPCookieStorage *) sharedHTTPCookieStorage
{
static NSHTTPCookieStorage *_shared;
if(!_shared)
_shared=[[self alloc] init];
return _shared;
}



次は _sync。これはメモリ内のデータをファイルへ書き出す処理を司っているようだ。
- (void)_sync

中身を見る前に他のメソッドでの使われ方を先にみることにしよう。
後回しにして次へ行く。


次は全クッキー情報の取得。初回存在しない場合は _sync を呼び出している。多分、ファイルから読み出すのだろう。
- (NSArray *) cookies;
{
if(!_cookies)
[self _sync];
return [_cookies allValues];
}



続いては指定 URLにマッチするクッキーの取得。
- (NSArray *) cookiesForURL:(NSURL *) url

長いのでソースはオリジナルを見てもらうとして、ポイントだけ解説。

クッキーを1件づつ処理する。
while((cookie=[c nextObject]))
:
:
(1) if([domain hasPrefix:@"."])
{
(1') if(![[url host] hasSuffix:domain])
continue; // does not match suffix
}
(2) else if(![[url host] isEqualToString:domain])
continue; // does not match exact domain
path=[cookie path];
(3) if(![path isEqualToString:@"/"] && ![[url path] isEqualToString:path])
continue; // neither "all paths" nor specific path
(4) if([cookie isSecure] && ![[url scheme] hasSuffix:@"s"])
continue; // not requested by secure protocol
(5) if((portList=[cookie portList]) && ![portList containsObject:[url port]])
continue; // no match with port list
[r addObject:cookie]; // exact match
}
return r;


(1) ドメイン名チェック
クッキーのドメイン名が .(ドット)で始まる場合、ホスト名のサフィックスチェックを行う。

URL:host=www.some.site.com
Cookie:domain=.site.com ならOK(次のチェックへ)

(2) ホスト名チェック
URLのホスト名とクッキーのドメイン名が一致するかをチェックする。

URL:host=www.some.site.com
Cookie:domain=www.some.site.com ならOK(次のチェックへ)

(3) パスチェック
URLのパスとクッキーのパスが一致するかをチェックする。
クッキーのパスが /(ルート)ならこのチェックはパスして次へ。

URL:path=/foo/bar
Cookie:path=/foo/bar ならOK(次のチェックへ)

#ん?Cookieの仕様上 Cookie:path=/foo は駄目だったか?

(4) セキュアチェック
クッキーがセキュア指定されている場合は HTTPSの時のみクッキー送出を許す。
コードでは [url scheme] の最後が s で終わっているかどうかでチェックしている。

(5) ポートチェック
クッキーにポート指定がある場合は、URLのポートがそれに一致するかチェックする。
#ポートチェックが必要なのは知らなかった。

(1)〜(5)すべてのチェックがパスできたら該当するクッキーを配列へ加え、最後にそれを返す。



次は削除。domainとpath、nameを連結した文字列をキーとして該当するクッキーを削除している。なるほど domain+path+nameをキーとしているのか。
- (void) deleteCookie:(NSHTTPCookie *) cookie
{
[_cookies removeObjectForKey:[NSString stringWithFormat:@"%@/%@/%@", [cookie domain], [cookie path], [cookie name]]];
before we did sync?
_touched=YES;
}



次は登録。プライバシーポリシーをチェックしたのち、domain+path+name をキーにして格納する。初回は _cookies がnilなので _syncでファイルから読み込む。最後に _touchedを立てておく。
- (void) setCookie:(NSHTTPCookie *) cookie
{
NSString *key;
if(_cookieAcceptPolicy == NSHTTPCookieAcceptPolicyNever)
return;
key=[NSString stringWithFormat:@"%@/%@/%@", [cookie domain], [cookie path], [cookie name]];
if(!_cookies)
[self _sync];
[_cookies setObject:cookie forKey:key];
_touched=YES;
}


次は配列で登録。配列をループにして setCookie:を繰り返し呼び出す。よく見ると mainURL が使われていない。
- (void) setCookies:(NSArray *) cookies forURL:(NSURL *) url mainDocumentURL:(NSURL *) mainURL
{
NSEnumerator *c;
NSHTTPCookie *cookie;
if(_cookieAcceptPolicy == NSHTTPCookieAcceptPolicyNever)
return;
c=[_cookies objectEnumerator];
while((cookie=[c nextObject]))
{
// [cookie setMainDocumentURL:mainURL]
[self setCookie:cookie];
}
}



ポリシーのゲッター。
- (NSHTTPCookieAcceptPolicy) cookieAcceptPolicy
 { return _cookieAcceptPolicy; }


ポリシーのセッター。_touchedを立てている。
- (void) setCookieAcceptPolicy:(NSHTTPCookieAcceptPolicy) policy
{
if(_cookieAcceptPolicy != policy)
{
_cookieAcceptPolicy=policy;
_touched=YES;
}
}



イニシャライザ。NSDistributedLock でロックを用意した後、30秒毎に _sync を呼び出すタイマーを設定している。なるほど非同期で定期書き出しを行っているのか。
- (id) init
{
if((self=[super init]))
{
_lock=[[NSDistributedLock alloc] initWithPath:COOKIE_DATABASE_LOCK];
_timer=[[NSTimer scheduledTimerWithTimeInterval:30 target:self selector:@selector(_sync) userInfo:nil repeats:YES] retain];
}
return self;
}


最後に dealloc。
- (void) dealloc
{
[_timer invalidate];
[_timer release];
if(_touched)
[self _sync]; // final sync
[_lock release];
[_cookies release];
[_lastModification release];
[super dealloc];
}



さて飛ばした _sync を見てみる。これまでの状況から、(1)初回ファイルからの読み込み (2)30秒毎のファイル書き出し、の2つの処理を行っているようだ。

こちらも長いのでポイントだけ。
- (void) _sync

最初にロック獲得を試みる。ロックがとれない間は 1秒スリープを繰り返す。むう。タイムアウト(もしくは回数制限)が無いので下手すると延々とここにとどまるのだろうか。
 while(![_lock tryLock])
sleep(1);



次にクッキーをファイルから読み込むかどうかの判断。
 if(!_cookies || [[[[NSFileManager defaultManager] fileAttributesAtPath:COOKIE_DATABASE traverseLink:YES] fileModificationDate] compare:_lastModification] == NSOrderedDescending)

初回(_cookies==nil)もしくは、インスタンス変数で保持している最終更新日時よりもファイルの更新日時が新しい場合に、これ以降の処理(ファイル読み込み)を行う。
後者のチェックは複数のアプリでこのインスタンスが使われることを想定したものだろう。

ファイルの読み込み処理。クラスメソッド一発。エラーが起きたらどうなるのやら。。
NSArray *a=[NSArray arrayWithContentsOfFile:COOKIE_DATABASE]; // read all properties


読み込んだ内容を NSHTTPCookie のインスタンスへ詰め替える。
NSEnumerator *e=[a objectEnumerator];
NSDictionary *props;
if(!_cookies)
_cookies=[[NSMutableDictionary alloc] initWithCapacity:100];
while((props=[e nextObject]))
{ // merge changes
NSString *key=[NSString stringWithFormat:@"%@/%@/%@", [props objectForKey:NSHTTPCookieDomain], [props objectForKey:NSHTTPCookiePath], [props objectForKey:NSHTTPCookieName]];
NSHTTPCookie *cookie=[_cookies objectForKey:key];
:
:

作成した NSHTTPCookie は domain+path+name をキーにして NSMutableDictionary へ格納していく。


続いて書き出し。
 if(_touched || !_lastModification)

最初に変更があった(_touched==YES)かとうかをチェックする。また1度も書き出しが行われていない(_lastModification==nil)場合も書き出すことにする。

書き出し処理。
    while((cookie=[e nextObject]))
[a addObject:[cookie properties]]; // just save the properties
[a writeToFile:COOKIE_DATABASE atomically:YES]; // should we write a binary property list???
_lastModification=[[[[NSFileManager defaultManager] fileAttributesAtPath:COOKIE_DATABASE traverseLink:YES] fileModificationDate] retain]; // modified
}



書き出し後ににロックを解除する。
 [_lock unlock];


最後に変更があった場合は NSHTTPCookieStorageCookiesChangedNotification を投げる。
 if(changed)
[[NSNotificationCenter defaultCenter] postNotificationName:NSHTTPCookieStorageCookiesChangedNotification object:self];


#って、コードを見ると changed を設定してるところが無いぞ。

- - - -
怪しいところがいくつもありこのままでは使えないな。だが参考になった。

2009年8月19日水曜日

NSHTTPCookieStorage による Cookies.plist 保存を調べる

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

さて Webkit でのクッキーのハンドリング方法が大体わかったので、クッキーを処理するクラスの設計に入ろう。

設計にあたっては気になる点が2つある。
一つは、Foudation には既に NSHTTPCookieStorage が存在するので、新しいクラスは互換性を持たせるかどうか。

ここで互換性と言っているのは、NSHTTPCookieStorage と同じメソッドを実装するくらいの意味。これは採用・不採用共に強い理由が見当たらないのでとりあえず互換性を持たせる方向でいこう。ただし今のところ NSHTTPCookieStorage のサブクラス化はしない。


もう一つは保存(永続化)方法をどうするか。Safari(すなわち NSHTTPCookieStorage)では plist へ保存していた。plist へ保存するにしてもどういうタイミングでどんな方法で行うのか(plist書き出し自体は NSArray 等のメソッドで簡単にできる)。


そんなこともあり Safariの Cookies.plist を調べているうちに次のことがわかった。

(1) テンポラリファイルを作成し、書き出し後に Cookies.plist を置き換えている

Cookie を扱うサイトを表示させると同時に、Cookiesフォルダを見てみると一瞬テンポラリファイルが作成されているのが分かった。恐らく書き出し後に Cookies.plist へ切り替えている。


(2) 一定間隔で書き出しが行われている

フォルダを眺めていると Cookieを扱うサイトを表示する毎に保存するのではなく、数十秒から1分程度の間隔で書き出しが行われている(ように見えた)。


なるほど。plistという簡単なファイルを使っていることもあり、ファイルの破損防止や負荷軽減の為にこんな工夫をしているのか。なお SQLiteならもっと無頓着にできそうだ(この辺りの管理は SQLiteがやってくれるので)。



NSHTTPCookieStorage の保存方法について情報が無いかネットで調べてみる。

するとソースコードがあった。

mySTEP - NSHTTPCookieStorage.m


とは言っても Apple提供の Foudation のそれではなく、QuantumSTEP のソースコード(正確には mySTEPのコード)。

QuantumSTEP は携帯端末向けの GNUStep をベースとしたアプリケーションプラットフォーム。
QuantumSTEP




紹介記事)マイコミジャーナル OSX ハッキング!
出なけりゃ作る!? Cocoa風味の"サブサブノート"

おお、こんなプラットフォームがあったとは知らなかった。

iPhone SDKとの比較もある。
QuantumSTEP vs iPhoneSDK


----
次回はソースコードを読み解いてみよう。

2009年8月18日火曜日

Safari のクッキーはどう保存されているのか?

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

Safari は NSHTTPCookieStorage を使っていると思われるが、cookie は、どこに、どういう形式で保存されているのか?

探してみると、ホーム下のライブラリ配下に Cookies というフォルダがあり、そこに Cookies.plist というファイルが存在した。これか。




SQLite と予想していたのだが、実態は plist だったのか。

開いてみよう。


中には Dictionary の配列が大量に格納されており、item1 から連番が振られている。

Dictionaryの中身。Cookieのエントリが格納されている。



NSDictionary を格納した配列(NSArray)をそのまま plist形式で吐き出したようになっている。

2009年8月17日月曜日

WPSU(14) - WebKitでクッキーを自前でハンドリングする #4 NSHTTPCookieを調べる

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

#タイトルが長くなってきた

さてクッキーを自前でハンドリングするにしても用意されているクラスが使えればそれにこしたことが無い。Foudation.framework には幸いなことに NSHTTPCookie クラスが用意されている。

NSHTTPCookie Class Reference

これが使えそうかどうか調査してみよう。

リファレンスを眺めると3つのインスタンス作成メソッドが用意されていた。


このうち、+[NSHTTPCookie cookiesWithResponseHeaderFields:forURL:] を試してみる。

cookiesWithResponseHeaderFields:forURL:

デリゲートメソッドに仕掛ける。
サーバから受け取ったレスポンス内のヘッダ情報を渡してみた。
AppController.m

- (void)webView:(WebView *)sender
resource:(id)identifier
didReceiveResponse:(NSURLResponse *)response
fromDataSource:(WebDataSource *)dataSource
{
NSDictionary* headers = [(NSHTTPURLResponse*)response allHeaderFields];
NSURL* url = [response URL];

NSArray* cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:headers forURL:url];
NSLog(@"---------------");
NSLog(@"%@", url);
NSLog(@"%@", cookies);
}



実行してみよう。Googleへ行き、非ログイン状態で「ログイン」リンクを押してみた。


するとサーバからクッキーが送られてきた。先ほどのメソッドでうまく取り込めているようだ。
2009-08-17 21:14:58.569 WebPageScreenshotUtility[2160:10b] ---------------
2009-08-17 21:14:58.621 WebPageScreenshotUtility[2160:10b] https://www.google.com/accounts/Login?hl=ja&continue=http://www.google.co.jp/
2009-08-17 21:14:58.640 WebPageScreenshotUtility[2160:10b] (
<NSHTTPCookie
version:0
name:@"GoogleAccountsLocale_session"
value:@"ja"
expiresDate:@"(null)"
created:@"272175298.569016"
sessionOnly:TRUE
domain:@"www.google.com"
path:@"/accounts"
secure:FALSE
comment:@"(null)"
commentURL:@"(null)"
portList:(null)>,
<NSHTTPCookie
version:0
name:@"GALX"
value:@"fo7Y_pW13-M"
expiresDate:@"(null)"
created:@"272175298.569091"
sessionOnly:TRUE
domain:@"www.google.com"
path:@"/accounts"
secure:TRUE
comment:@"(null)"
commentURL:@"(null)"
portList:(null)>
)

※読みやすい様に改行を入れてある

- - - - -
おお、これは便利。ヘッダ内のCookieの解析はこれが使えそうだ。

2009年8月16日日曜日

WPSU(14) - WebKitでクッキーを自前でハンドリングする #3 設計

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

クッキーを自前処理する上での流れがおおまかつかめた。

WPSUでは、クッキーの取得・保存を行うクラスを作ってクッキーの管理はこれにすべてまかせてしまおう。

イメージはこんな感じ。

2009年8月15日土曜日

WPSU(13) - WebKitでクッキーを自前でハンドリングする #2 レスポンスを見る

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

こんどはレスポンスを見てみる。

-[WebResourceLoadDelegate webView:resource:didReceiveResponse:fromDataSource:]を実装し、レスポンスヘッダを書き出してみる。

WeController.m

- (void)webView:(WebView *)sender
resource:(id)identifier
didReceiveResponse:(NSURLResponse *)response
fromDataSource:(WebDataSource *)dataSource
{
NSLog(@"%@", [(NSHTTPURLResponse*)response allHeaderFields]);
}



Googleへアクセスしログアウトしたところ下記のヘッダが得られた。
2009-08-15 12:36:15.842 WebPageScreenshotUtility[10622:10b] {
"Cache-Control" = "private, max-age=0";
"Content-Encoding" = gzip;
"Content-Length" = 282;
"Content-Type" = "text/html; charset=UTF-8";
Date = "Fri, 15 Aug 2009 03:36:15 GMT";
Expires = "Fri, 15 Aug 2009 03:36:15 GMT";
Server = "GFE/2.0";
"Set-Cookie" = "GoogleAccountsLocale_session=ja, SID=EXPIRED;Domain=.google.co.jp;Path=/;Expires=Mon, 01-Jan-1990 00:00:00 GMT, HSID=EXPIRED;Domain=.google.co.jp;Path=/;Expires=Mon, 01-Jan-1990 00:00:00 GMT, SSID=EXPIRED;Domain=.google.co.jp;Path=/;Expires=Mon, 01-Jan-1990 00:00:00 GMT";
"X-Content-Type-Options" = nosniff;
}


"Set-Cookie" が得られた。

- - - - - -

WebView を使った場合、2つのデリゲートでそれぞれクッキーの送出と保存を行えば良さそうだ。

2009年8月14日金曜日

WPSU(12) - WebKitでクッキーを自前でハンドリングする #1 リクエストを調べる

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

クッキーをWebKitアプリ間で共有しないようにするには、クッキーの処理を自前でやる必要があることがわかった。WSBUでは共有したくないのでクッキー処理を実装することにする。

普通なら「まずクッキーの仕様を調べて、、」といくところだが大雑把な内容は理解しているつもりなので実装を進めながら確認を取っていこう。


まずは WebKit でクッキーを自前処理する方法を調べる。

WebView の WebResourceLoadDelegate に先日作成した WebController を指定する。ここにデリゲートメソッド webView:resource:willSendRequest:redirectResponse:fromDataSource: を実装してみた。

WebController.m

- (NSURLRequest *)webView:(WebView *)sender
resource:(id)identifier
willSendRequest:(NSURLRequest *)request
redirectResponse:(NSURLResponse *)redirectResponse
fromDataSource:(WebDataSource *)dataSource
{
NSLog(@"%@", [request allHTTPHeaderFields]);
return request;
}


調査目的でリクエストヘッダの内容をデバッガコンソールへ出力させている。

google.com へアクセスしてみよう。

デバッガコンソール(抜粋)
[Session started at 2009-08-14 12:14:01 +0900.]
2009-08-14 12:14:05.494 WebPageScreenshotUtility[10382:10b] {
Accept = "application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5";
"User-Agent" = "Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_8; ja-jp) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/3.2.3 Safari/525.28.3";
}
2009-08-14 12:14:07.097 WebPageScreenshotUtility[10382:10b] {
Accept = "application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5";
"Accept-Encoding" = "gzip, deflate";
"Accept-Language" = "ja-jp";
"User-Agent" = "Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_8; ja-jp) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/3.2.3 Safari/525.28.3";
}
2009-08-14 12:14:07.505 WebPageScreenshotUtility[10382:10b] {
Referer = "http://www.google.co.jp/";
"User-Agent" = "Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_8; ja-jp) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/3.2.3 Safari/525.28.3";
}


見たところクッキーヘッダが無い。

クッキーが送出されていないと思いきや、google.comを見るとログイン状態になっている。



どうもこのメソッドが呼ばれるタイミングではまだクッキー情報が付加されていないようだ。

試しに setHTTPShouldHandleCookies: を使い、クッキー処理を停止させてみる。
 [(NSMutableURLRequest*)request setHTTPShouldHandleCookies:NO];


すると今度は google.com へアクセスしてもログイン状態にはなっていない。なるほど。

2009年8月13日木曜日

WebKit でクッキーを共有しない方法を探す

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

クッキー共有問題(勝手に命名)に困っている人はやはりいるようで、いくつかのサイトで取り上げられている。

Cocoabuilder - Overriding NSHTTPCookieStorage for WebView

自前の NSHTTPCookieStorageクラスを用意し、poseAtClass でシステムの用意するクラスを置き換える、なんて方法が紹介されている(うまくいっていないようだが)。HTTPヘッダを自前で処理する案も出ている様子。



まったり開発情報Blog - CocoaアプリでCookieを独自に管理する

-[NSURLReuqest setHTTPShouldHandleCookies:]で Cookieのハンドリングを無効にして、自前で Cookieを管理する案が紹介されている。

まったり開発情報Blog - CocoaアプリでCookieを独自に管理する(※上の続き)

自前クラスの実装に成功したとのこと。WebView での自前Cookie管理の方法も紹介されている。WebViewの WebResourceLoadDelegate を使う(これは Cocoabuilderの記事でも紹介されていた)。



overwrite/exchange NSHTTPCookieStorage
これは未解決。


- - - -
うーむ。NSHTTPCookieStorageを使わず、自前でヘッダ処理をする方法しかなさそうだ。

2009年8月12日水曜日

WebKit を使ったアプリケーションはクッキーを共有する

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

WPSU を作っていて気になったことがある。
WPSU を使って Googleを開くとログインしたはずが無いのに、ログイン状態で表示される。


(右上にメールアドレスが記載)


普段 Safari でこのアカウントを使っているのだが、
もしやクッキーは WebKitを使ったアプリケーションで共有されている?

Safariを開きログイン状態であることを確認した後、試しに WPSUでログアウトしてみると Safariの方でもログアウト状態になっていた。おおなんてこった。これってまずくないのか?

WebKitにはクッキーを扱うクラスは見つからなかった。Foundation.framework を調べると NSHTTPCookieStorage というクラスが見つかった。キャッシュの時と同様 Foundation.framework で管理されているようだ。

ADC - NSHTTPCookieStorage Class Reference

Overview には、はっきりと "a singleton object (shared instance)" と書かれている。



サンプルを作って動作を見てみる。

サンプル:CookieStudy.zip


-[NSHTTPCookieStorage cookies]の結果をデバッガコンソールへ吐き出してみよう。
コードはこんな感じ。

@implementation AppController

- (void)awakeFromNib
{
NSHTTPCookieStorage* cookie_storage =
[NSHTTPCookieStorage sharedHTTPCookieStorage];
NSArray* cookies = [cookie_storage cookies];

NSLog(@"%@", cookies);
}

@end


実行するとでるわでるわ、すべてのクッキーが表示された。


表示内容はこんな感じ。
<NSHTTPCookie version:0 name:@"__utmz" value:@"1981754.1243.1.1.utmcsr=google.co.jp/"
expiresDate:@"2010-01-13 00:40:03 +0900"
created:@"2692303.4789"
sessionOnly:FALSE
domain:@".deep.csi.com"
path:@"/"
secure:FALSE
comment:@"(null)"
commentURL:@"(null)"
portList:(null)>

※一部値を改変

- - - -
悪意のある WebKitアプリケーションがクッキー情報を好きにできるってことか。
と思ったりもしたが、
この件に限らず悪意のあるプログラムを走らせたら何でもできるという点で特別問題視する事でもないか。

ただ異なるブラウザ間でログイン状態が共有されるのはあまり気持ちのよい状況ではないな。

2009年8月11日火曜日

Cocoaで RTFDを表示させてみる

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

RTFDファイルを表示させるサンプルを組んでみよう。

Xcodeで新規プロジェクトを作成した後、表示させたい RTFDファイルをテキストエディット.appで作成する。


知らなかったのだが、表組やリストなども作成できる。リンクの作り方も分かった。

Xcode内でもきちんと表示される。



InterfaceBuilderを立ち上げ、NSScrollViewをウィンドウへ貼付ける。この中の Custom View を NSTextViewに変える。



AppController クラスを追加し、先ほどの NSTextViewをアウトレットに接続しておく。

AppContorller.h

@interface AppController : NSObject {

IBOutlet NSTextView* _text_view;
}

@end



実装は sample.rtfd を NSTextViewで読み込むだけ。前回調べたメソッドを使っている。

AppContorller.m
@implementation AppController

- (void)awakeFromNib
{
NSString* path = [[NSBundle mainBundle] pathForResource:@"sample" ofType:@"rtfd"];
NSLog(@"%@", path);

[_text_view readRTFDFromFile:path];
}
@end



実行してみよう。




出た。


面白い事にテキストの編集だけでなく、フォントの色やサイズなどの表示属性も変えることができる。


表組も編集できた。



なお表示のみで編集させたくない場合は - [NSTextView setEditable:] に NOを渡してやる。

 [_text_view setEditable:NO];


NSTextView Class Reference - setEditable:


サンプル:RTFDsample.zip

- - - -
Cocoaで RTFDがかなり簡単に扱えることがわかった。文字装飾や画像のあるちょっとした文章をユーザに見せたい場合にこれは使える(しかもたった数行で)。

2009年8月10日月曜日

RTFD ( Rich Text Format Directory) のcocoaでの扱いを調べる

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

Credits.rtdf を用意すると About box に画像付きの装飾文字が簡単に表示できた。このことから cocoaでは RTFD が簡単に扱えそうだ。どうやっているのだろうか。興味が出て来てたので ADCリファレンス内で RTF(D)に関する情報を集めてみた。

Text Attachment Programing Topics for Cocoa: Adding Attachments to Text

これによると NSText が RTFDの読み書きを行うメソッドをもっているとのこと。

これか。
NSText Class Reference - readRTFDFromFromFile:




NSText は NSView のサブクラスだからひょっとしてウィンドウへ貼付けて RTFDを読み込ませれば簡単に表示できるのだろうか?

- - - -
残念ながら今試す環境がないので実装はまた明日。

2009年8月9日日曜日

SimpleCap - AboutパネルをHTMLやRTFを使ってカスタマイズする

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

MacOSXのアプリケーションには「...について」というメニューがあり、これを選ぶとAboutパネルが表示される。
SimpleCapの例:


このクレジット表示をカスタマイズしよう。といっても大げさなものは不要でやりたいのはホームページへのハイパーリンクを追加するだけ。

どんな方法があるのか調べてみたところ、HTMLやRTFを使う方法と、カスタムビュー(パネル)を使う方法があるようだ。

ADC - Technical Note TN2179
Cocoaアプリケーションの「About」パネル作成


試しに Credits.html を作ってみた。


するとAboutパネルの一部にこのHTMLが表示されるようになった。リンクもきちんと働く。



ここでふと疑問。このHTMLはローカライズできるのだろうか。
試してみよう。ローカリゼーションを追加し EnglishとJapaneseの2つを用意した。Japaneseには「ホームページ」と日本語を表示するようにした。


実行すると Japaneseのローカリゼーションが利用された。ただ文字化けしている。


エンコーディングを UTF-16に変える。


文字化けが直った。


フォントが明朝体っぽい。fontタグを指定してみる。

<font face="Verdana">


ゴシックになった。HTMLでいろいろと表現を指定できるようだ。



次は RTF (Rich Text Format)を試してみる。標準のテキストエディット.app を使いフォントの種類とサイズを変えてみた。



忠実に表示された。



今度は画像を入れてみよう。画像を入れるとRTFでは駄目で RTFDを使うことになる。ファイル名は "Credits.rtfd" とした。


実行してみよう。ちゃんと出た。


リンクはどうか。


リンクも OK。




画像はともかく RTF でも HTML でも目的のことはできそうだ。RTFの場合、OSX標準のテキストエディット.appでWYSIWYGで編集できるのがメリットか。ただテキストエディット.appでハイパーリンクの作り方がわからなかったので(今回は Safariからドラッグ&ドロップした)、方法がなければリンクの扱いがちょっと面倒。(方法はありそうだが)。

なお RTFDについては zariganitosh さんが興味深い調査を紹介していた。

ザリガニが見ていた...。
添付書類付きリッチテキスト(.rtfd)の実態

なるほど。

- - - -
今のところ凝った事はいらないので今回は HTMLを採用することにした。