| 1 | package MENTA; |
|---|
| 2 | use strict; |
|---|
| 3 | use warnings; |
|---|
| 4 | use utf8; |
|---|
| 5 | use CGI::ExceptionManager; |
|---|
| 6 | |
|---|
| 7 | our $VERSION = '0.05'; |
|---|
| 8 | our $REQ; |
|---|
| 9 | our $CONFIG; |
|---|
| 10 | our $REQUIRED; |
|---|
| 11 | our $MOBILEAGENTRE; |
|---|
| 12 | our $CARRIER; |
|---|
| 13 | our $STASH; |
|---|
| 14 | our $PLUGIN_LOADED; |
|---|
| 15 | BEGIN { |
|---|
| 16 | $REQUIRED = {}; |
|---|
| 17 | } |
|---|
| 18 | |
|---|
| 19 | sub import { |
|---|
| 20 | strict->import; |
|---|
| 21 | warnings->import; |
|---|
| 22 | utf8->import; |
|---|
| 23 | } |
|---|
| 24 | |
|---|
| 25 | package main; # ここ以下の関数はすべてコントローラで呼ぶことができます |
|---|
| 26 | |
|---|
| 27 | sub config () { $MENTA::CONFIG } |
|---|
| 28 | |
|---|
| 29 | sub run_menta { |
|---|
| 30 | my $config = shift @_; |
|---|
| 31 | |
|---|
| 32 | local $MENTA::CONFIG = $config; |
|---|
| 33 | local $MENTA::REQ; |
|---|
| 34 | local $MENTA::STASH; |
|---|
| 35 | |
|---|
| 36 | CGI::ExceptionManager->run( |
|---|
| 37 | callback => sub { |
|---|
| 38 | dispatch() |
|---|
| 39 | }, |
|---|
| 40 | powered_by => '<strong>MENTA</strong>, Web Application Framework.', |
|---|
| 41 | (config->{menta}->{fatals_to_browser} ? () : (renderer => sub { "INTERNAL SERVER ERROR!" x 100 })) |
|---|
| 42 | ); |
|---|
| 43 | } |
|---|
| 44 | |
|---|
| 45 | sub dispatch { |
|---|
| 46 | my $path = $ENV{PATH_INFO} || '/'; |
|---|
| 47 | $path =~ s!^/+!!g; |
|---|
| 48 | if ($path =~ /^[a-z0-9_]*$/) { |
|---|
| 49 | $path ||= 'index'; |
|---|
| 50 | my $cdir = controller_dir(); |
|---|
| 51 | my $controller = "${cdir}/${path}.pl"; |
|---|
| 52 | my $controller_mt = controller_dir() . "/${path}.mt"; |
|---|
| 53 | if (-f $controller) { |
|---|
| 54 | my $meth = "do_$path"; |
|---|
| 55 | package main; |
|---|
| 56 | do $controller; |
|---|
| 57 | if (my $e = $@) { |
|---|
| 58 | if (ref $e) { |
|---|
| 59 | warn "KTKR"; |
|---|
| 60 | return; |
|---|
| 61 | } else { |
|---|
| 62 | die $e; |
|---|
| 63 | } |
|---|
| 64 | } |
|---|
| 65 | die $@ if $@; |
|---|
| 66 | if (my $code = main->can($meth)) { |
|---|
| 67 | $code->(); |
|---|
| 68 | die "なにも出力してません"; |
|---|
| 69 | } else { |
|---|
| 70 | die "「${path}」というモードは存在しません!${controller} の中に ${meth} が定義されていないようです"; |
|---|
| 71 | } |
|---|
| 72 | } elsif (-f $controller_mt) { |
|---|
| 73 | my $out = __render_partial("${path}.mt", controller_dir()); |
|---|
| 74 | utf8::encode($out); |
|---|
| 75 | print "Content-Type: text/html; charset=utf-8\r\n"; |
|---|
| 76 | print "\r\n"; |
|---|
| 77 | print $out; |
|---|
| 78 | } else { |
|---|
| 79 | die "「${path}」というモードは存在しません。コントローラファイルもありません(${controller})。テンプレートファイルもありません(${controller_mt})"; |
|---|
| 80 | } |
|---|
| 81 | } elsif ($path ne 'menta.cgi' && -f "app/$path" && $path =~ /^static\//) { |
|---|
| 82 | $path = "app/$path"; |
|---|
| 83 | if (open my $fh, '<', $path) { |
|---|
| 84 | binmode $fh; |
|---|
| 85 | binmode STDOUT; |
|---|
| 86 | printf "Content-Type: %s\r\n\r\n", guess_mime_type($path); |
|---|
| 87 | print do { local $/; <$fh> }; |
|---|
| 88 | close $fh; |
|---|
| 89 | } else { |
|---|
| 90 | die "ファイルが開きません"; |
|---|
| 91 | } |
|---|
| 92 | } elsif ($path =~ /^(?:crossdomain\.xml|favicon\.ico|robots\.txt)$/) { |
|---|
| 93 | print "status: 404\r\ncontent-type: text/plain\r\n\r\n"; |
|---|
| 94 | } else { |
|---|
| 95 | die "${path} を処理する方法がわかりません"; |
|---|
| 96 | } |
|---|
| 97 | } |
|---|
| 98 | |
|---|
| 99 | sub escape_html { |
|---|
| 100 | local $_ = shift; |
|---|
| 101 | return $_ unless $_; |
|---|
| 102 | s/&/&/g; |
|---|
| 103 | s/>/>/g; |
|---|
| 104 | s/</</g; |
|---|
| 105 | s/"/"/g; |
|---|
| 106 | s/'/'/g; |
|---|
| 107 | return $_; |
|---|
| 108 | } |
|---|
| 109 | |
|---|
| 110 | sub unescape_html { |
|---|
| 111 | local $_ = shift; |
|---|
| 112 | return $_ unless $_; |
|---|
| 113 | s/>/>/g; |
|---|
| 114 | s/</</g; |
|---|
| 115 | s/"/"/g; |
|---|
| 116 | s/'/'/g; |
|---|
| 117 | s/&/&/g; |
|---|
| 118 | return $_; |
|---|
| 119 | } |
|---|
| 120 | |
|---|
| 121 | sub guess_mime_type { |
|---|
| 122 | my $ext = shift; |
|---|
| 123 | $ext =~ s/.+\.(.+)$/$1/; |
|---|
| 124 | |
|---|
| 125 | # TODO should be moved to other. |
|---|
| 126 | my $mime_map = { |
|---|
| 127 | css => 'text/css', |
|---|
| 128 | js => 'application/javascript', |
|---|
| 129 | jpg => 'image/jpeg', |
|---|
| 130 | gif => 'image/gif', |
|---|
| 131 | png => 'image/png', |
|---|
| 132 | txt => 'text/plain', |
|---|
| 133 | }; |
|---|
| 134 | $mime_map->{$ext} || 'application/octet-stream'; |
|---|
| 135 | } |
|---|
| 136 | |
|---|
| 137 | sub cache_dir { |
|---|
| 138 | config->{menta}->{cache_dir} || 'cache' |
|---|
| 139 | } |
|---|
| 140 | |
|---|
| 141 | sub tmpl_dir { |
|---|
| 142 | config->{menta}->{cache_dir} || 'app/tmpl/' |
|---|
| 143 | } |
|---|
| 144 | |
|---|
| 145 | sub controller_dir { |
|---|
| 146 | config->{menta}->{controller_dir} || 'app/controller/' |
|---|
| 147 | } |
|---|
| 148 | |
|---|
| 149 | sub data_dir { |
|---|
| 150 | config->{menta}->{data_dir} || 'app/data/' |
|---|
| 151 | } |
|---|
| 152 | |
|---|
| 153 | sub static_dir { |
|---|
| 154 | config->{menta}->{static_dir} || 'app/static/' |
|---|
| 155 | } |
|---|
| 156 | |
|---|
| 157 | # TODO: ディレクトリトラバーサル対策 |
|---|
| 158 | sub __render_partial { |
|---|
| 159 | my ($tmpl, $tmpldir, @params) = @_; |
|---|
| 160 | my $conf = config()->{menta}; |
|---|
| 161 | my $cachedir = cache_dir(); |
|---|
| 162 | mkdir $cachedir unless -d $cachedir; |
|---|
| 163 | my $cachefname = "$cachedir/$tmpl"; |
|---|
| 164 | my $tmplfname = "$tmpldir/$tmpl"; |
|---|
| 165 | my $use_cache = sub { |
|---|
| 166 | my @orig = stat $tmplfname or return; |
|---|
| 167 | my @cached = stat $cachefname or return; |
|---|
| 168 | return $orig[9] < $cached[9]; |
|---|
| 169 | }->(); |
|---|
| 170 | my $out; |
|---|
| 171 | if ($use_cache) { |
|---|
| 172 | my $tmplcode = do $cachefname; |
|---|
| 173 | die $@ if $@; |
|---|
| 174 | die "テンプレートキャッシュを読み込めませんでした: ${tmplfname}" unless $tmplcode; |
|---|
| 175 | $out = $tmplcode->(@params); |
|---|
| 176 | } else { |
|---|
| 177 | die "「${tmplfname}」という名前のテンプレートファイルは見つかりません" unless -f $tmplfname; |
|---|
| 178 | require_once('MENTA/Template.pm'); |
|---|
| 179 | my $tmplsrc = read_file($tmplfname); |
|---|
| 180 | my $mt = MENTA::Template->new; |
|---|
| 181 | $mt->parse($tmplsrc); |
|---|
| 182 | $mt->build(); |
|---|
| 183 | my $src = $mt->code(); |
|---|
| 184 | my $tmplcode = eval $src; |
|---|
| 185 | die $@ if $@; |
|---|
| 186 | $out = $tmplcode->(@params); |
|---|
| 187 | write_file($cachefname, "package main; use utf8;\n${src}"); |
|---|
| 188 | } |
|---|
| 189 | $out; |
|---|
| 190 | } |
|---|
| 191 | sub render_partial { |
|---|
| 192 | my ($tmpl, @params) = @_; |
|---|
| 193 | __render_partial($tmpl, tmpl_dir(), @params); |
|---|
| 194 | } |
|---|
| 195 | |
|---|
| 196 | sub detach() { CGI::ExceptionManager::detach(@_) } |
|---|
| 197 | |
|---|
| 198 | sub render { |
|---|
| 199 | my ($tmpl, @params) = @_; |
|---|
| 200 | my $out = render_partial($tmpl, @params); |
|---|
| 201 | utf8::encode($out); |
|---|
| 202 | print "Content-Type: text/html; charset=utf-8\r\n"; |
|---|
| 203 | print "\r\n"; |
|---|
| 204 | print $out; |
|---|
| 205 | |
|---|
| 206 | detach; |
|---|
| 207 | } |
|---|
| 208 | |
|---|
| 209 | sub redirect { |
|---|
| 210 | my ($location, ) = @_; |
|---|
| 211 | print "Status: 302\r\n"; |
|---|
| 212 | print "Location: $location\r\n"; |
|---|
| 213 | print "\r\n"; |
|---|
| 214 | |
|---|
| 215 | detach; |
|---|
| 216 | } |
|---|
| 217 | |
|---|
| 218 | sub finalize { |
|---|
| 219 | my $str = shift; |
|---|
| 220 | my $content_type = shift || 'text/html; charset=utf-8'; |
|---|
| 221 | |
|---|
| 222 | print "Content-Type: $content_type\r\n"; |
|---|
| 223 | print "\r\n"; |
|---|
| 224 | print $str; |
|---|
| 225 | |
|---|
| 226 | detach; |
|---|
| 227 | } |
|---|
| 228 | |
|---|
| 229 | sub read_file { |
|---|
| 230 | my $fname = shift; |
|---|
| 231 | open my $fh, '<:utf8', $fname or die "${fname} を読み込み用に開けません: $!"; |
|---|
| 232 | my $s = do { local $/; join '', <$fh> }; |
|---|
| 233 | close $fh; |
|---|
| 234 | $s; |
|---|
| 235 | } |
|---|
| 236 | |
|---|
| 237 | sub write_file { |
|---|
| 238 | my ($fname, $stuff) = @_; |
|---|
| 239 | open my $fh, '>:utf8', $fname or die "${fname} を書き込み用に開けません: $!"; |
|---|
| 240 | print $fh $stuff; |
|---|
| 241 | close $fh; |
|---|
| 242 | } |
|---|
| 243 | |
|---|
| 244 | sub param { |
|---|
| 245 | my $key = shift; |
|---|
| 246 | |
|---|
| 247 | unless (defined $MENTA::REQ) { |
|---|
| 248 | require_once('CGI/Simple.pm'); |
|---|
| 249 | $CGI::Simple::PARAM_UTF8++; |
|---|
| 250 | $MENTA::REQ = CGI::Simple->new(); |
|---|
| 251 | } |
|---|
| 252 | |
|---|
| 253 | $MENTA::REQ->param($key); |
|---|
| 254 | } |
|---|
| 255 | |
|---|
| 256 | sub upload { |
|---|
| 257 | unless (defined $MENTA::REQ) { |
|---|
| 258 | require_once('CGI/Simple.pm'); |
|---|
| 259 | $CGI::Simple::PARAM_UTF8++; |
|---|
| 260 | $MENTA::REQ = CGI::Simple->new(); |
|---|
| 261 | } |
|---|
| 262 | $MENTA::REQ->upload(@_); |
|---|
| 263 | } |
|---|
| 264 | |
|---|
| 265 | sub require_once { |
|---|
| 266 | my $path = shift; |
|---|
| 267 | return if $MENTA::REQUIRED->{$path}; |
|---|
| 268 | require $path; |
|---|
| 269 | $MENTA::REQUIRED->{$path} = 1; |
|---|
| 270 | } |
|---|
| 271 | |
|---|
| 272 | sub load_plugin { |
|---|
| 273 | my $plugin = shift; |
|---|
| 274 | return if $MENTA::PLUGIN_LOADED->{$plugin}; |
|---|
| 275 | my $path = "plugins/${plugin}.pl"; |
|---|
| 276 | require $path; |
|---|
| 277 | $MENTA::PLUGIN_LOADED->{$plugin}++; |
|---|
| 278 | my $package = __menta_extract_package($path) || ''; |
|---|
| 279 | no strict 'refs'; |
|---|
| 280 | for ( |
|---|
| 281 | grep { /$plugin/o } |
|---|
| 282 | grep { defined &{"${package}::$_"} } |
|---|
| 283 | keys %{"${package}::"} |
|---|
| 284 | ) { |
|---|
| 285 | *{"main::$_"} = *{"${package}::$_"} |
|---|
| 286 | } |
|---|
| 287 | } |
|---|
| 288 | |
|---|
| 289 | sub __menta_extract_package { |
|---|
| 290 | my $modulefile = shift; |
|---|
| 291 | open my $fh, '<', $modulefile or die "$modulefile を開けません: $!"; |
|---|
| 292 | my $in_pod = 0; |
|---|
| 293 | while (<$fh>) { |
|---|
| 294 | $in_pod = 1 if m/^=\w/; |
|---|
| 295 | $in_pod = 0 if /^=cut/; |
|---|
| 296 | next if ( $in_pod || /^=cut/ ); # skip pod text |
|---|
| 297 | next if /^\s*\#/; |
|---|
| 298 | |
|---|
| 299 | /^\s*package\s+(.*?)\s*;/ and return $1; |
|---|
| 300 | } |
|---|
| 301 | return; |
|---|
| 302 | } |
|---|
| 303 | |
|---|
| 304 | sub is_post_request () { |
|---|
| 305 | my $method = $ENV{REQUEST_METHOD}; |
|---|
| 306 | return $method eq 'POST'; |
|---|
| 307 | } |
|---|
| 308 | |
|---|
| 309 | # TODO: CGI にはこのための環境変数ってなかったっけ? |
|---|
| 310 | sub docroot () { $ENV{SCRIPT_NAME} || '' } |
|---|
| 311 | |
|---|
| 312 | sub uri_for { |
|---|
| 313 | my ($path, $query) = @_; |
|---|
| 314 | my @q; |
|---|
| 315 | while (my ($key, $val) = each %$query) { |
|---|
| 316 | $val = join '', map { /^[a-zA-Z0-9_.!~*'()-]$/ ? $_ : '%' . uc(unpack('H2', $_)) } split //, $val; |
|---|
| 317 | push @q, "${key}=${val}"; |
|---|
| 318 | } |
|---|
| 319 | docroot . '/' . $path . (scalar @q ? '?' . join('&', @q) : ''); |
|---|
| 320 | } |
|---|
| 321 | |
|---|
| 322 | sub static_file_path { |
|---|
| 323 | my $path = shift; |
|---|
| 324 | docroot . '/static/' . $path; |
|---|
| 325 | } |
|---|
| 326 | |
|---|
| 327 | 1; |
|---|