microengine 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/LICENSE +674 -0
  2. data/README +102 -0
  3. data/Rakefile +67 -0
  4. data/access.log +1 -0
  5. data/bin/microengine +0 -0
  6. data/content/403/en.body +2 -0
  7. data/content/403/en.header +1 -0
  8. data/content/403/layout +1 -0
  9. data/content/403/ru.body +2 -0
  10. data/content/403/ru.header +1 -0
  11. data/content/404/en.body +5 -0
  12. data/content/404/en.header +1 -0
  13. data/content/404/layout +1 -0
  14. data/content/404/ru.body +5 -0
  15. data/content/404/ru.header +1 -0
  16. data/content/WARNING +2 -0
  17. data/content/en.body +2 -0
  18. data/content/en.header +1 -0
  19. data/content/layout +1 -0
  20. data/content/ru.body +2 -0
  21. data/content/ru.header +1 -0
  22. data/layouts/WARNING +2 -0
  23. data/layouts/default/en.yaml +7 -0
  24. data/layouts/default/layout.rhtml +18 -0
  25. data/layouts/default/ru.yaml +7 -0
  26. data/lib/microengine/admin.rb +268 -0
  27. data/lib/microengine/admin_exception.rb +31 -0
  28. data/lib/microengine/admin_page.rb +199 -0
  29. data/lib/microengine/assambler.rb +192 -0
  30. data/lib/microengine/dispatcher.rb +125 -0
  31. data/lib/microengine/file_cache.rb +62 -0
  32. data/lib/microengine/html/deleter.rhtml +11 -0
  33. data/lib/microengine/html/editor.rhtml +34 -0
  34. data/lib/microengine/html/en.yaml +33 -0
  35. data/lib/microengine/html/error.rhtml +6 -0
  36. data/lib/microengine/html/refresher.rhtml +7 -0
  37. data/lib/microengine/html/ru.yaml +33 -0
  38. data/lib/microengine/html/untranslater.rhtml +11 -0
  39. data/lib/microengine/http.rb +213 -0
  40. data/lib/microengine/initializer.rb +93 -0
  41. data/lib/microengine/memory_cache.rb +53 -0
  42. data/password +20 -0
  43. data/public/microengine.fcgi +5 -0
  44. data/public/styles/handheld.css +0 -0
  45. data/public/styles/print.css +3 -0
  46. data/public/styles/screen.css +18 -0
  47. metadata +106 -0
@@ -0,0 +1,31 @@
1
+ =begin
2
+ Logic to edit site from web interface.
3
+ Copyright (C) 2008 Andrey "A.I." Sitnik <andrey@sitnik.ru>
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU General Public License as published by
7
+ the Free Software Foundation, either version 3 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU General Public License for more details.
14
+
15
+ You should have received a copy of the GNU General Public License
16
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ =end
18
+
19
+ module Microengine
20
+ # Exception in admin page ot action.
21
+ # Contain message and code to translate message in error page.
22
+ class AdminException < Exception
23
+
24
+ attr_reader :code
25
+
26
+ def initialize(msg = nil, code = nil)
27
+ super(msg)
28
+ @code = code
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,199 @@
1
+ =begin
2
+ Logic to edit site from web interface.
3
+ Copyright (C) 2008 Andrey "A.I." Sitnik <andrey@sitnik.ru>
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU General Public License as published by
7
+ the Free Software Foundation, either version 3 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU General Public License for more details.
14
+
15
+ You should have received a copy of the GNU General Public License
16
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ =end
18
+
19
+ module Microengine
20
+ # Admin interface
21
+ class AdminPage
22
+
23
+ def initialize(http, path, admin)
24
+ @http = http
25
+ @path = '/' + path
26
+ @admin = admin
27
+ @show_password = http.cookie['password'].nil?
28
+
29
+ #TODO Layout for /admin/refresh/
30
+ if not @http.post['layout'].nil?
31
+ @layout = @admin.config['default_layout']
32
+ elsif File.exists? MICROENGINE_ROOT + "/content#{@path}layout"
33
+ begin
34
+ @layout = IO.read MICROENGINE_ROOT + "/content#{@path}layout"
35
+ rescue
36
+ @layout = @admin.config['default_layout']
37
+ end
38
+ else
39
+ @layout = @admin.config['default_layout']
40
+ end
41
+ @layout_langs = []
42
+ Dir.glob(MICROENGINE_ROOT + "/layouts/#{@layout}/*.yaml") do |lang|
43
+ @layout_langs.push File.basename(lang, '.yaml')
44
+ end
45
+
46
+ @lang = language
47
+ load_translation
48
+ end
49
+
50
+ # Show interface to edit page
51
+ def editor
52
+ if '/admin/' == @path[0..6]
53
+ error 'admin_subdir'
54
+ return
55
+ end
56
+
57
+ if not File.exists? MICROENGINE_ROOT + "/layouts/#{@layout}/#{@lang}.yaml"
58
+ @old_lang = @lang
59
+ @lang = @admin.dispatcher.language(@http, @layout_langs)
60
+ error 'untranslated_layout'
61
+ return
62
+ end
63
+
64
+ @layouts = []
65
+ @untranslated_layouts = {}
66
+ if @admin.cache.langs[@path].nil?
67
+ langs = [@lang]
68
+ else
69
+ langs = @admin.cache.langs[@path] + [@lang]
70
+ end
71
+ Dir.glob(MICROENGINE_ROOT + '/layouts/*/layout.rhtml') do |file|
72
+ layout = file.split('/')[-2]
73
+ @layouts.push layout
74
+ layout_langs = []
75
+ Dir.glob(MICROENGINE_ROOT + "/layouts/#{layout}/*.yaml") do |lang|
76
+ layout_langs.push File.basename(lang, '.yaml')
77
+ end
78
+ untranslated = langs - layout_langs
79
+ if not untranslated.empty?
80
+ @untranslated_layouts[layout] = untranslated
81
+ end
82
+ end
83
+
84
+ if @http.post['body'].nil?
85
+ begin
86
+ @body = IO.read MICROENGINE_ROOT + "/content#{@path}#{@lang}.body"
87
+ rescue
88
+ @body = ''
89
+ end
90
+ else
91
+ @body = @http.post['body']
92
+ end
93
+
94
+ if @http.post['header'].nil?
95
+ begin
96
+ @header = IO.read MICROENGINE_ROOT + "/content#{@path}#{@lang}.header"
97
+ rescue
98
+ @header = ''
99
+ end
100
+ else
101
+ @header = @http.post['header']
102
+ end
103
+
104
+ display 'editor'
105
+ end
106
+
107
+ # Show interface to delete page
108
+ def deleter
109
+ if '/admin/' == @path[0..6]
110
+ error 'admin_subdir'
111
+ return
112
+ end
113
+ if @admin.cache.langs[@path].nil?
114
+ @admin.dispatcher.not_found @http
115
+ return
116
+ end
117
+ if ['/404/', '/403/', '/'].include? @path
118
+ error 'delete_system'
119
+ return
120
+ end
121
+ @body = IO.read MICROENGINE_ROOT + "/content#{@path}#{@lang}.body"
122
+ display 'deleter'
123
+ end
124
+
125
+ # Show interface to delete translation
126
+ def untranslater
127
+ if '/admin/' == @path[0..6]
128
+ error 'admin_subdir'
129
+ return
130
+ end
131
+ if @admin.cache.langs[@path].nil?
132
+ @admin.dispatcher.not_found @http
133
+ return
134
+ end
135
+
136
+ @body = IO.read MICROENGINE_ROOT + "/content#{@path}#{@lang}.body"
137
+ display 'untranslater'
138
+ end
139
+
140
+ # Show interface to input password for refresh cache
141
+ def refresher
142
+ display 'refresher'
143
+ end
144
+
145
+ # Error
146
+ def error(code)
147
+ @code = code
148
+ display 'error'
149
+ end
150
+
151
+ private
152
+
153
+ # Return page language to admin action
154
+ def language
155
+ if @http.get['lang'].nil?
156
+ langs = @admin.cache.langs[@path]
157
+ if langs.nil?
158
+ langs = @layout_langs
159
+ end
160
+ @admin.dispatcher.language(@http, langs)
161
+ else
162
+ @http.get['lang']
163
+ end
164
+ end
165
+
166
+ def load_translation
167
+ @admin_langs = []
168
+ Dir.glob(MICROENGINE_ROOT + '/lib/microengine/html/*.yaml') do |file|
169
+ @admin_langs.push File.basename(file, '.yaml')
170
+ end
171
+
172
+ if @admin_langs.include? @lang
173
+ lang = @lang
174
+ else
175
+ lang = @admin.dispatcher.language(@http, @admin_langs)
176
+ end
177
+
178
+ @translation = YAML::load_file MICROENGINE_ROOT + "/lib/microengine/html/#{lang}.yaml"
179
+ end
180
+
181
+ # Display admin HTML in page layout
182
+ def display(view)
183
+ header = "<title>#{@translation[view]['title']}</title>"
184
+ rhtml = IO.read MICROENGINE_ROOT + "/lib/microengine/html/#{view}.rhtml"
185
+ body = ERB.new(rhtml).result(binding)
186
+
187
+ if 'editor' == view
188
+ langs = @layout_langs
189
+ elsif 'error' == view
190
+ langs = []
191
+ else
192
+ langs = @admin.cache.langs[@path]
193
+ end
194
+
195
+ @http << @admin.assambler.assamble(@path, @layout, @lang, header, body, langs)
196
+ end
197
+
198
+ end
199
+ end
@@ -0,0 +1,192 @@
1
+ =begin
2
+ Create XHTML pages from layouts and content.
3
+ Copyright (C) 2008 Andrey "A.I." Sitnik <andrey@sitnik.ru>
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU General Public License as published by
7
+ the Free Software Foundation, either version 3 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU General Public License for more details.
14
+
15
+ You should have received a copy of the GNU General Public License
16
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ =end
18
+
19
+ require 'erb'
20
+ require 'yaml'
21
+ require 'fileutils'
22
+
23
+ module Microengine
24
+ # Create XHTML pages from layouts and content
25
+ class Assambler
26
+
27
+ attr_writer :config
28
+ attr_writer :logger
29
+
30
+ # Create XHTML pages from layouts and content
31
+ def refresh
32
+ @layouts = {}
33
+
34
+ begin
35
+ if not File.directory? MICROENGINE_ROOT + '/cache/'
36
+ FileUtils.mkdir MICROENGINE_ROOT + '/cache/'
37
+ end
38
+ FileUtils.rm_r Dir.glob(MICROENGINE_ROOT + '/cache/*')
39
+ rescue
40
+ raise "Can't delete and create /cache/ dir. Ensure that it is chmod 0666."
41
+ end
42
+
43
+ cache_dir '/'
44
+ end
45
+
46
+ # Cache content for some lang in path
47
+ def cache_file(path, lang, langs)
48
+ file_path = MICROENGINE_ROOT + '/content' + path
49
+
50
+ begin
51
+ layout = IO.read(file_path + 'layout').strip
52
+ rescue
53
+ raise "Can't read layout file in /content#{path}. Please ensure that #{path}layout exists and is chmod 644"
54
+ end
55
+ begin
56
+ header = IO.read(file_path + lang + '.header')
57
+ rescue
58
+ raise "Can't read #{lang}.header file in /content#{path}. Please ensure that #{path}#{lang}.header exists and is chmod 644"
59
+ end
60
+ begin
61
+ body = IO.read(file_path + lang + '.body')
62
+ rescue
63
+ raise "Can't read #{lang}.body file in /content#{path}. Please ensure that #{path}#{lang}.body exists and is chmod 644"
64
+ end
65
+
66
+ begin
67
+ content = assamble path, layout, lang, header, body, langs
68
+ rescue Exception => e
69
+ raise "Error in /content#{path}: " + e.message
70
+ end
71
+
72
+ begin
73
+ io = File.new(MICROENGINE_ROOT + "/cache#{path}content.#{lang}.html", 'w')
74
+ io.write content
75
+ io.close
76
+ rescue
77
+ raise "Can't write to /cache#{path}content.#{lang}.html . Ensure that /cache/ dir and all files inside is chmod 0666."
78
+ end
79
+ end
80
+
81
+ # Create XHTML page by layout and content
82
+ def assamble(path, layout_name, lang, header, body, langs)
83
+ layout = get_layout layout_name
84
+ if layout[lang].nil?
85
+ raise "There isn't translation in '#{lang}' for #{layout_name}"
86
+ end
87
+ translation = layout[lang]
88
+ languages = languages_html langs, lang, layout
89
+ layout[:layout].result(binding)
90
+ end
91
+
92
+ private
93
+
94
+ # Create languages list HTML
95
+ def languages_html(langs, selected, translations)
96
+ html = '<ul id="languages">'
97
+ langs.sort.each do |lang|
98
+ if lang == selected
99
+ html += '<li><span class="selected">' + translations[lang]['language']['title'] + '</span></li>';
100
+ else
101
+ html += '<li><a href="?lang=' + lang + '">' + translations[lang]['language']['title'] + '</a></li>'
102
+ end
103
+ end
104
+ html += '</ul>'
105
+ end
106
+
107
+ # Cache directory and it's subdirectory of content
108
+ def cache_dir(dir)
109
+ path = MICROENGINE_ROOT + '/content' + dir
110
+ cache = MICROENGINE_ROOT + '/cache' + dir
111
+ if not File.directory? cache
112
+ begin
113
+ Dir.mkdir cache
114
+ rescue
115
+ raise "Can't create dir #{cache} . Please ensure that cache dir is chmod 0777."
116
+ end
117
+ end
118
+
119
+ langs = []
120
+ Dir.foreach path do |entry|
121
+ if '.' == entry[0..0]
122
+ next
123
+ elsif File.directory? path + entry + '/'
124
+ begin
125
+ cache_dir dir + entry + '/'
126
+ rescue
127
+ @logger.error e.message
128
+ end
129
+ elsif '.body' == entry[-5..-1]
130
+ langs.push entry[0..entry.index('.')-1].downcase
131
+ end
132
+ end
133
+
134
+ langs.each do |lang|
135
+ begin
136
+ cache_file dir, lang, langs
137
+ rescue Exception => e
138
+ @logger.error e.message
139
+ end
140
+ end
141
+ end
142
+
143
+ # Get hash with layouts XML object
144
+ def get_layout(name)
145
+ if @layouts[name].nil?
146
+ @layouts[name] = load_layout name
147
+ end
148
+ return @layouts[name]
149
+ end
150
+
151
+ # Load layout from XML file and create translated versions
152
+ def load_layout(name)
153
+ # Check name and layout
154
+ name = name.sub(/(^|\/)\.\.?/, '')
155
+ dir = MICROENGINE_ROOT + '/layouts/' + name + '/';
156
+ if not File.directory?(dir) or not File.exists? dir + 'layout.rhtml'
157
+ raise "Can't find layout #{name} ."
158
+ end
159
+
160
+ # Load layout
161
+ layout = {
162
+ :layout => ERB::new(IO.read(dir + 'layout.rhtml'))
163
+ }
164
+
165
+ # Load translation
166
+ Dir.glob(dir + '**.yaml') do |file|
167
+ lang = File.basename file, '.yaml'
168
+ if 'layout' == lang
169
+ next
170
+ end
171
+ layout[lang] = load_translation(file)
172
+ end
173
+ layout
174
+ end
175
+
176
+ # Load translation
177
+ def load_translation(file)
178
+ translation = YAML::load_file(file)
179
+ lang = File.basename(file, '.yaml')
180
+
181
+ # Restore require fields if necessary
182
+ if translation['language'].nil? or translation['language']['title'].nil?
183
+ translation['language'] = {'code' => lang, 'title' => lang}
184
+ else
185
+ if lang != translation['language']['code']
186
+ translation['language']['code'] = lang
187
+ end
188
+ end
189
+ translation
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,125 @@
1
+ =begin
2
+ FastCGI dispatcher to return XHTML pages by HTTP request.
3
+ Copyright (C) 2008 Andrey "A.I." Sitnik <andrey@sitnik.ru>
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU General Public License as published by
7
+ the Free Software Foundation, either version 3 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU General Public License for more details.
14
+
15
+ You should have received a copy of the GNU General Public License
16
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ =end
18
+
19
+ require 'fcgi'
20
+ require 'admin'
21
+ require 'http'
22
+
23
+ module Microengine
24
+ # Use FastCGI to return XHTML by HTTP request
25
+ class Dispatcher
26
+
27
+ attr_writer :config
28
+ attr_writer :logger
29
+ attr_writer :spiders
30
+ attr_writer :cache
31
+ attr_writer :admin
32
+
33
+ # Start FastCGI
34
+ def run
35
+ FCGI.each do |request|
36
+ http = HTTP.new(request)
37
+
38
+ if @admin.dispatch(http)
39
+ request.finish
40
+ next
41
+ else
42
+ langs = @cache.langs[http.url]
43
+ if langs.nil?
44
+ not_found http
45
+ request.finish
46
+ next
47
+ end
48
+
49
+ if spider? http.agent
50
+ # Search engine spiders (another i18n logic)
51
+ if http.get['lang'].nil?
52
+ if langs.include? http.get['lang']
53
+ http << @cache.get(http.url, http.get['lang'])
54
+ request.finish
55
+ next
56
+ end
57
+ end
58
+ http.redirect http.url + '?lang=' + language(http, langs)
59
+ else
60
+ # Human visitor
61
+ if http.get['lang']
62
+ http.set_cookie 'lang', http.get['lang'], 30
63
+ if not langs.include? http.get['lang']
64
+ http.redirect http.url
65
+ request.finish
66
+ next
67
+ end
68
+ end
69
+ http << @cache.get(http.url, language(http, langs))
70
+ end
71
+ request.finish
72
+ end
73
+ end
74
+ end
75
+
76
+ # Forward to 404 error
77
+ def not_found(http)
78
+ http.status = '404 Not Found'
79
+ http << @cache.get('/404/', language(http, @cache.langs['/404/']))
80
+ http.finish
81
+ end
82
+
83
+ # Select best language for user
84
+ def language(http, available)
85
+ # Language selected manually
86
+ if not http.get['lang'].nil?
87
+ if available.include? http.get['lang']
88
+ return http.get['lang']
89
+ end
90
+ elsif http.cookie['lang']
91
+ if available.include? http.cookie['lang']
92
+ return http.cookie['lang']
93
+ end
94
+ end
95
+
96
+ # Select user language
97
+ http.langs.each do |lang|
98
+ lang.downcase!
99
+ if available.include? lang
100
+ return lang
101
+ end
102
+ end
103
+
104
+ # Select any language
105
+ if available.include? @config['default_language']
106
+ @config['default_language']
107
+ else
108
+ available.first
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ # Check HTTP_USER_AGENT value for search engine spiders
115
+ def spider?(agent)
116
+ @spiders.each do |spider|
117
+ if agent.include? spider
118
+ return true
119
+ end
120
+ end
121
+ false
122
+ end
123
+
124
+ end
125
+ end