Categories
open all | close allTags
フォーム | モデル | スキンエンジン | 国際化 | Flash | デュアル・コア | タグ | Migration | 認証 | ドキュメント | Subversion | rake | JustPosted | パソコン | テスト | 名称 | Aptana | アクセス制御 | RESTful | CSRFSearch
インストーラの起動条件
インストーラは一応できたのですが,起動条件がまだちゃんと定まっていません。パスワードなしで動き,DBやメンバー追加などをしてしまうので,初期状態のとき以外は起動しないようにかなり気をつける必要があります。起動を許可する条件と,その条件をセットする方法およびタイミング,起動条件を解除する方法およびタイミング,条件が整わないときに起動させない方法の四つを決めないといけません。それからインストーラでどこまで設定させるかという問題もあります。当初はDBへの接続設定を入れるつもりだったのですが,DBの設定ファイルがないとRailsのアプリケーションが起動しません。ごまかして起動することは可能なのですが,設定ファイルの更新後に,元に戻す方法が難しい。単純にファイルを書き換えるだけではFastCGI環境のようにロードしたまま実行するものではうまくいきません。どこかで現在のプロセスを殺して再起動させないといけませんが,そこまでをインストーラに盛り込むのは不可能です。
別案として,インストーラ専用のアプリケーションを作り,そこから設定させるというのも考えたのですが,手間ばかりかかってしまい,インストーラが作業を軽減させてくれているのか分からなくなってしまいます。そこまでやるのなら,アプリケーション本体もインストーラがサーバーから持ってきてインストールするといったブラウザのインストールなどでよく見られる方法を取るのもありかなあとは思うのですが,今度はSubversionからデータを取ってくるという,新たな壁にぶつかります。svnコマンドを呼び出すことで,できそうな雰囲気もありますが,これは後の課題ということで。
さて,話を戻してインストーラの条件ですが,起動許可条件としては,特定のファイルが存在すること,DB内に必要なテーブルが存在していないこと,あるいは存在していて,テーブル内に特定のレコードがあること,を考えています。偶然に条件が整ってしまう可能性がほとんどない(ファイルあるいはレコードの存在を条件にしている),進入されて実行される可能性が低い(ファイルとDBの両方をクラックされてしまうとしたら,そもそもインストーラうんぬんとは関係なく侵入者はなんでもできてしまう)というのが理由です。
条件の設定方法は,まずファイルは初期コピー時に含まれます。DBのフィールドはマイグレーションで設定します。初期状態ではフィールドの代わりにテーブルがないことが条件として使われます。
これで起動するとマイグレーションを実行し,言語設定を行い,サイト・オーナー設定を行って完了です。終了時に,ファイルを消去し,DBのフィールドを削除して手続き完了です。
起動条件の判定は,一応インストーラのメイン・メソッド内に入れるつもりです。それがプロセスの再起動なしで一番簡単にできるので。
【追記】プラグインのインストールでSubversionを呼び出すところはsystem関数に"svn export ..."を実行させていた。
備忘メモ:ping送信管理部分の実装
未来の記事を投稿したときなどにもちゃんと公開時にpingを打ったり,mixiの日記を投稿したりするようにするための実装についてのメモ。専用のテーブルpingsにはフィールドとして,公開日時,アイテムのID,コールバックするプラグインのID,オプショナル・データ(プラグインが自由に使える)を持つ。
アイテムがパブリッシュされるときに,プラグインがreserve_pingメソッドを呼ぶと,このテーブルに予約される。その場でpingを発行するときも,必ずこのインタフェースを通す。
アイテムが公開状態になる(公開時刻に達する)と,このテーブルのデータを元にプラグインにevent_send_pingが送られ(非同期に実行できたりするとうれしいけど,それは難しいかな?),テーブルのデータは消去される。
アイテムがパブリッシュから別の状態に変わったときもテーブルのデータを消す。
これでプラグイン側は予約して受け取るというインタフェースを実装するだけで済むはず。
インストーラを作成
改造ついでに寄り道してインストーラを作成しています。インストーラとはいっても実際にするのは言語設定と「Site Owner」の設定くらいですが。DBにテーブルを作るところはインストーラではなくrakeを使わざるを得ないので,その後に最初に走らせるプログラムということになります。気持ちとしてはウィザード風のユーザ・インタフェースにしたかったのですが,そういったデザイン・テンプレートが見つからなかったので管理画面のものを代用しています。
管理画面の抽象化と権限チェックの強化
あまり成果が上がっていない話ですが,引き続き管理画面の抽象化を考えています。どういうことかというと,表示する内容(メニューの文字やリンクなど)と,表示を制御する部分(CSS,HTMLのタグなど)を分け,さらにそれらを表示ロジックと分離させたいのです。ここまでできると,管理画面がほとんど着せ替えに近い格好で変えられ,さらに管理画面の機能強化も容易になります。
前述のようにテーブルについては既に完成し,かなりのロジック軽減につながっているのですが,メニュー表示などに広げるかどうかが悩みどころ。もちろん分離は可能なのですが,ロジック側や表示側の手間がかなりかかってしまい,どうもメリットがあるのかないのか微妙なところ。今は使わない方向で動いています。
もう一つ考え中なのが,管理画面での権限チェックを忘れない方法。
管理画面ではアクションごとに権限のチェックが必要ですが,権限のチェックをしなくても動いてしまうため,セキュリティ・ホールを作ってしまうおそれがあります。そこで権限のチェックをしていないときに動かなくするか,メッセージを表示するような仕組みを入れておきたいと思っています。今,これを書きながら一つアイディアが浮かんだので試してみます。
結果:AdminControllerにinitializeメソッドを作り,そこで@right_checkedというインスタンス変数をfalseで初期化します。権限チェックのメソッドで,これをtrueに変えます。ビューを実行するときにこの変数を見て,falseだとエラーメッセージを出すようにしました。
スキン周りに関する仕様のまとめ
Nucleusのスキン周りについてTwitterでいろんな話が出ていたので,Foodyn CMSにおける仕様と実装についてまとめておきます。Foodynはスキン(他のCMSだとテンプレートといったほうがいいのかな?)の処理系(エンジン)を複数載せられますが,ここでは標準組み込みであるNucleus互換のスキン・エンジンについてだけまとめます。
まず,現状のNucleusのスキンには分かりやすさなどにおいていくつか問題があります。
①DBに保存されているスキンと,ファイル・ベースでインクルードする部分があり,編集画面もそれぞれ別になる
②さまざまなスキンタイプが分かりにくい
③スキンとテンプレートの関係が分かりにくい。テンプレートの中にはアイテムの表示に関係するもの,カテゴリーの表示に関係するもの,さらには日付表示に関係するものや,画像表示に関係するものなどが混在しており,どこで何が使われているのか理解するのが難しい
④特別なページを作る機能が少ない
Foodynではこれらについて次のような改善策を取ります。
①Nucleusのスペシャルスキンパーツをインクルードする機能を標準で持ちます(Nucleusでファイルをインクルードするのに使うparsedincludeを拡張)。基本的にDBベースの編集だけで済むようになります。
②「レイアウト」スキンを使うと,各スキンに固有な部分(例えばアイテムページにおける<%item%>スキン変数)はデフォルトのものが利用でき,何も作らなくて構わない(もちろんデフォルトで不満がある場合は作っても構わない)
③テンプレートにデフォルト機能があるので,意識してテンプレートを使わなくてもよくなります。例えば<%categorylist%>だけで動くようになります。また,インライン・テンプレート機能を使うことで,スキンの中にテンプレートを埋め込めます。
また,スキンに対してメインのテンプレートとアイテムスキン用のテンプレートという二つのテンプレートを属性として持つようにします(未実装)。標準ではスキン名に「/index」「/item」をつけたものがテンプレート名になります。これらはスキンのデフォルトテンプレートとして働きます(ない場合はシステムのデフォルトが使われます)。
これらのスキンに付属するテンプレートはスキン編集画面から直接パート(例えばカテゴリ表示関係)ごとに呼び出して編集できるようにします(未実装)。
④ブログに付属しないアイテムを作ることができ,このアイテム・ページに自由に名前を付けることによって,さまざまなページが作れます。
トップ・ページの機能があり,トップページだけ専用のスキンパートを利用できます。
表出力の抽象化
管理画面の基本は表です。ブログやアイテムの一覧はもちろん,フォームもラベル部分と入力部分で表を使って表示することが多いと思います。そこで,管理画面の出力内容と表示を分離するため,表出力を抽象化してみました。
まず,出力内容はコントローラで次のような形で設定します。
def index
@blogs = @member.blogs
@table.header=[la('Blog Name'),la('Add Item'),la('Edit Item'),la('Comments'),\
la('Tags/Categories'),la('Settings'),('Delete Blog')]
@table.widths=[177]
@table.data = []
@blogs.each do |blog|
row = []
row << h(blog.bname)
row << context_link(new_admin_blog_item_url(@memberid, blog),blog,"commit", "page_add","add item")
row << context_link(admin_blog_items_url(@memberid, blog), blog, "commit", "page_edit","edit items")
row << context_link(admin_blog_comments_url(@memberid, blog),blog,"commentedit", "comments","comments")
row << context_link(admin_blog_categories_url(@memberid, blog),blog,"commentedit", "tag_red","categories")
row << context_link(edit_admin_blog_url(@memberid, blog),blog,"editsettings", "application_edit","settings")
row << context_link(admin_blog_url(@memberid, blog), blog,"deleteblog", "folder_delete","delete blog", \
:confirm=>'OK?', :method=>'delete')
@table.data << row
end
render
endここで@tableが表を抽象化したTable型のオブジェクトで,@table.headerに表頭,@table.widthsに列幅指定(あれば),@table.dataに表のボディ部分が入ります。
一方,表示側は
def show_table table
temp = <<HEAD
<div class="table">
<img src="/admin/img/bg-th-left.gif" width="8" height="7" alt="" class="left" />
<img src="/admin/img/bg-th-right.gif" width="7" height="7" alt="" class="right" />
HEAD
table.controller self
temp += table.html do |h|
h.options({:class=>'listing', :cellpadding=>"0", :cellspacing=>"0"})
h.head({:first=>'first', :last=>'last'})
h.body({:first=>'first style1', :last=>'last', :cycle=>[nil, 'bg']})
end
temp += '</div>'
endここでパラメータで与えられたtableが先ほどのTable型のオブジェクト。これに対して,table.htmlというメソッドで表の表示内容を指定しています。管理画面のすべての表に対して,このshow_table一つで対応できます。後はコンテンツ側を変えるだけ。
これで表部分はいいのですが,問題は表に入らないような要素がある場合。例えば,管理画面のトップページである,今の画面には新規ブログを作るボタンがあります。そこの部分は抽象化していないので,ビューに素のHTMLを書くことになってしまいました。今後はこういった要素の抽象化をするのか,あるいはこういった要素を排除していくのか考えていったほうがよさそうです。
クロージャを使うメソッドをかっこよくする
ツリー型カテゴリーの表示など,ロジックが必要な表示ルーチンで,ロジックと表示そのものの部分を分けるためにクロージャを使う話は以前に書きました。そのときのロジック側(Blogモデル)はこうなってました。
def category_tree
tempbuffer = ""
level = 0
self.categories.inject(nil) do |last_right, cat|
# cat is under the last leaf
if !last_right || (last_right > cat.cleft)
level += 1
tempbuffer += yield(:level_up, level)
#
elsif (last_right + 2 <= cat.cleft)
tempbuffer += yield(:item_close, level)
(last_right+2..cat.cleft).each do |i|
level -= 1
tempbuffer += yield(:level_down, level)
end
else
tempbuffer += yield(:item_close, level)
end
tempbuffer += yield(:item, level, cat)
last_right = cat.cright
end
if (level > 1)
tempbuffer += yield(:level_down, level)
end
(level..1).each do |i|
tempbuffer += yield(:item_close, level)
end
tempbuffer += yield(:level_down, level)
return tempbuffer
end
一方,呼び出し側(パーサ内のparse_categorylist)は
def parse_categorylist params
blog = params[2] ? Blog.find_by_bname(params[2]) : @controller.blog
template = _template(params[1])
bd = blog.basic_data
data = Hash.new
blog.category_tree do |type, level, cat|
data['level'] = level.to_s
case type
when :level_up
fill(template['CATLIST_HEADER'], bd.merge(data))
when :level_down
fill(template['CATLIST_FOOTER'], bd.merge(data))
when :item
data['catlink'] = url_for(:controller=>'categories', :catid=>cat.catid, :action=>'show')
fill(template['CATLIST_LISTITEM'], data.merge(cat.attrs))
when :item_close
fill(template['CATLIST_LISTITEM_END'], bd.merge(data))
end
end
end
これで実用上は不自由ないのですが,どうも呼び出し側があまりかっこよくない。case文で場合分けというのが「ださい」感じがします。yieldで呼び出されるたびにこのcase文を通るのが効率的にももう一つ。ここの部分を
blog.category_tree do |cat|
cat.level_up { |level|
data['level'] = level.to_s
fill(template['CATLIST_HEADER'], bd.merge(data))
}
cat.level_down { fill(template['CATLIST_FOOTER'], bd.merge(data)) }
cat.item { |cat|
data['catlink'] = url_for(:controller=>'categories', :catid=>cat.catid, :action=>'show')
fill(template['CATLIST_LISTITEM'], data.merge(cat.attrs))
}
cat.item_close { fill(template['CATLIST_LISTITEM_END'], bd.merge(data)) }
end
と書けたら,かなり「Railsっぽい」感じになります。で,やってみました。
» Read more
インライン・テンプレート
インライン・テンプレートの機能,動くようになったのですが,今の「ネストなしパーサー」だとインラインでテンプレート書くときに<%%>が使えないので<::>に書きなおす必要があります。やはりこれだと面倒なので,ネスト付きパーサーに切り替えることを画策中です。
ただ,単純に切り替えただけではさすがにエラーが…。ちゃんとテストしていないメソッドなので当然ですが。しばらくバグ取りに時間がかかりそうです。
それと,このネスト付きパーサーを考えたときは,ネストの内側も全部解釈実行することを前提にしていたのですが,考えてみたら内側は別のパーサー(例えばスキン変数の内側はテンプレートのパーサーが扱うべき)で実行するのが普通なので,そのあたりの処理も変えないといけません。例えばifの解釈もネストできるのですが,そこはオーバースペックでしょう。
インライン・テンプレートとヘッダー変数を実装
仕様として書いたからには実装しないと,ということでNucleus互換表示エンジンにヘッダー・スキン変数の機能とインライン・テンプレートの機能を実装しました。ヘッダーの方はごくごく簡単で,コントローラにヘッダを保持するハッシュを用意しておき,プラグインから読み書きできるようにして,スキン変数で書き出すだけです。文字列でなくハッシュにしたのは,同じものが既にないかどうか調べられるようにするため。インライン・テンプレートはスキン変数のパラメータに{}で挟まれた部分があると,そこをJSON(実際にはYAMLのライブラリを使っていますが)として解釈するもの。まだちゃんとデバッグはしていませんが,たぶんそんなに問題はないでしょう。
Nucleusの開発合宿に参加しました
Nucleusの開発合宿に行ってきました(詳しくはこちら)。とはいえ二泊三日のうち中日の日帰りという一部の参加でした。Foodyn CMSについてはこのブログにいろいろ書いてはいますが,見ていない人も多いだろうし,情報がまとまっていないので,この機会にどういうものだか理解してもらおうと,時間を取ってもらってプレゼンさせてもらいました。前の記事のプレゼンはそのための資料です。
午前中に1回と,見逃した人用に夕食後に1回,計1時間近く使わせていただきました。ありがとうございました。これが何かのきっかけになればと思います(Sourceforge.netのDevelopersは一人増えました。^^)。
また,ページスイッチの部分を実装しました。一つのページの中に複数の切り替え用スイッチ(例えば一つはメインブログ,もう一つはサブブログの切り替え用)を置けます。実装は案外苦労しましたが,割と面白い機能になったかと思います。