microengine 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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