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.
- data/LICENSE +674 -0
- data/README +102 -0
- data/Rakefile +67 -0
- data/access.log +1 -0
- data/bin/microengine +0 -0
- data/content/403/en.body +2 -0
- data/content/403/en.header +1 -0
- data/content/403/layout +1 -0
- data/content/403/ru.body +2 -0
- data/content/403/ru.header +1 -0
- data/content/404/en.body +5 -0
- data/content/404/en.header +1 -0
- data/content/404/layout +1 -0
- data/content/404/ru.body +5 -0
- data/content/404/ru.header +1 -0
- data/content/WARNING +2 -0
- data/content/en.body +2 -0
- data/content/en.header +1 -0
- data/content/layout +1 -0
- data/content/ru.body +2 -0
- data/content/ru.header +1 -0
- data/layouts/WARNING +2 -0
- data/layouts/default/en.yaml +7 -0
- data/layouts/default/layout.rhtml +18 -0
- data/layouts/default/ru.yaml +7 -0
- data/lib/microengine/admin.rb +268 -0
- data/lib/microengine/admin_exception.rb +31 -0
- data/lib/microengine/admin_page.rb +199 -0
- data/lib/microengine/assambler.rb +192 -0
- data/lib/microengine/dispatcher.rb +125 -0
- data/lib/microengine/file_cache.rb +62 -0
- data/lib/microengine/html/deleter.rhtml +11 -0
- data/lib/microengine/html/editor.rhtml +34 -0
- data/lib/microengine/html/en.yaml +33 -0
- data/lib/microengine/html/error.rhtml +6 -0
- data/lib/microengine/html/refresher.rhtml +7 -0
- data/lib/microengine/html/ru.yaml +33 -0
- data/lib/microengine/html/untranslater.rhtml +11 -0
- data/lib/microengine/http.rb +213 -0
- data/lib/microengine/initializer.rb +93 -0
- data/lib/microengine/memory_cache.rb +53 -0
- data/password +20 -0
- data/public/microengine.fcgi +5 -0
- data/public/styles/handheld.css +0 -0
- data/public/styles/print.css +3 -0
- data/public/styles/screen.css +18 -0
- metadata +106 -0
data/README
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
= MicroEngine - fast and minimalistic site engine
|
2
|
+
|
3
|
+
MicroEngine is a fast, simple and minimalistic site engine. It be created for
|
4
|
+
simple sites, which only share information (maybe in some languages) and
|
5
|
+
has web interface to edit, create and delete pages.
|
6
|
+
|
7
|
+
MicroEngine has a good support for i18n. User languages will be detected
|
8
|
+
automatically. Search engines will be storage page on all languages.
|
9
|
+
|
10
|
+
MicroEngine didn't use database. Information is storage in files and may be
|
11
|
+
cached in memory on engine loading.
|
12
|
+
|
13
|
+
It's ideal for simple homepages.
|
14
|
+
|
15
|
+
== Install
|
16
|
+
|
17
|
+
=== Depends
|
18
|
+
MicroEngine used only FastCGI, so your web server must be support it.
|
19
|
+
Apache must has mod_rewrite and mod_fastcgi modules.
|
20
|
+
|
21
|
+
=== Gems
|
22
|
+
|
23
|
+
# gem install microengine
|
24
|
+
$ cd /SITE/DIR/
|
25
|
+
$ microengine ./
|
26
|
+
$ ./password YOUR_PASSWORD
|
27
|
+
|
28
|
+
=== All-include package
|
29
|
+
It is good choise for hosting where you hasn't root password or access to gem.
|
30
|
+
Just download archive with all libraries and unpack it to neccesary dir.
|
31
|
+
|
32
|
+
$ tar -xf microengine-*.tar.bz2
|
33
|
+
$ mv microengine-* /SITE/DIR/
|
34
|
+
$ cd /SITE/DIR/
|
35
|
+
$ ./password YOUR_PASSWORD
|
36
|
+
|
37
|
+
=== Apache example config
|
38
|
+
After install MicroEngine files you must add this information to Apache config
|
39
|
+
file. Note, if .htaccess isn't support, you must copy content of
|
40
|
+
/PATH/TO/MICROENGINE/ROOT/public/.htaccess in <VirtualHost *> tag.
|
41
|
+
|
42
|
+
NameVirtualHost *
|
43
|
+
<VirtualHost *>
|
44
|
+
ServerName YOUR_SITE.COM
|
45
|
+
DocumentRoot /PATH/TO/MICROENGINE/ROOT/public/
|
46
|
+
</VirtualHost>
|
47
|
+
|
48
|
+
=== Security
|
49
|
+
Note that anyone can catch you admin password in Internet. So change http://
|
50
|
+
to https:// in admin links to use SSL (if you web server support it).
|
51
|
+
|
52
|
+
== Layouts
|
53
|
+
You must always open www.YOUR_SITE.COM/admin/refresh/ after any
|
54
|
+
changing of layout to refresh cache.
|
55
|
+
|
56
|
+
Layout contain design and other HTML code, which was repeated in pages.
|
57
|
+
Page contain header and body section, which will be put in layout.
|
58
|
+
|
59
|
+
Layout is saved in /layout/LAYOUT_NAME/ dir. This dir must contain layout.rhtml
|
60
|
+
file and YAML files for translations with name like LANG.yaml (LANG is a
|
61
|
+
language code). All translation files must contain language.code and
|
62
|
+
language.title values.
|
63
|
+
|
64
|
+
layout.rhtml is a simple ERB file with HTML. You may use Ruby code in <% and %>
|
65
|
+
tags. To insert some variable value use <%= variable %> .
|
66
|
+
See http://en.wikipedia.org/wiki/ERuby for details.
|
67
|
+
|
68
|
+
There are some predefined variables in ERB:
|
69
|
+
* header - Will be contain page header. Must be located in <head> tag.
|
70
|
+
* body - Will be contain page body. Must be located in <body> tag.
|
71
|
+
* languages - Will be contain HTML list with available page translations.
|
72
|
+
* translation - Hash with layout translation form YAML files in layout dir.
|
73
|
+
* path - path to current page.
|
74
|
+
|
75
|
+
== Usage
|
76
|
+
=== Web interface
|
77
|
+
To edit or create page open URL www.YOUR_SITE.COM/admin/edit/PATH .
|
78
|
+
You must change PATH to path for you page. For example, if you want to create
|
79
|
+
page www.example.com/films/best/ open www.example.com/admin/edit/films/best/
|
80
|
+
|
81
|
+
To delete translation open www.YOUR_SITE.COM/admin/untranslate/PATH?lang=LANG .
|
82
|
+
LANG must be changed to language code of deleting translation. For example,
|
83
|
+
if you want to delete French translation of www.example.com/resume/ page
|
84
|
+
input www.example.com/admin/untranslate/resume/?lang=fr
|
85
|
+
|
86
|
+
To delete page (and all translations) open www.YOUR_SITE.COM/admin/delete/PATH .
|
87
|
+
|
88
|
+
=== Manually edit
|
89
|
+
You must always open www.YOUR_SITE.COM/admin/refresh/ after any file changing to
|
90
|
+
refresh cache.
|
91
|
+
|
92
|
+
You can also edit site manually. Site content saved in /content/ dir.
|
93
|
+
'layout' file contain layout name. There are files LANG.body and LANG.header
|
94
|
+
(LANG is a language code) for all translations exists with body and header,
|
95
|
+
respectively.
|
96
|
+
|
97
|
+
== License
|
98
|
+
Microengine is licensed under the GNU General Public License version 3.
|
99
|
+
You can read it in LICENSE file or in http://www.gnu.org/licenses/gpl.html .
|
100
|
+
|
101
|
+
== Author
|
102
|
+
Andrey "A.I." Sitnik <andrey@sitnik.ru>
|
data/Rakefile
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'rake/contrib/rubyforgepublisher'
|
5
|
+
|
6
|
+
PKG_NAME = "microengine"
|
7
|
+
PKG_VERSION = "0.0.1"
|
8
|
+
|
9
|
+
RUBY_FORGE_PROJECT = "microengine"
|
10
|
+
RUBY_FORGE_USER = "iskin"
|
11
|
+
|
12
|
+
PKG_FILES = FileList[
|
13
|
+
"bin/*",
|
14
|
+
"content/**/*",
|
15
|
+
"layouts/**/*",
|
16
|
+
"lib/microengine/**/*",
|
17
|
+
"public/**/*",
|
18
|
+
"access.log",
|
19
|
+
"LICENSE",
|
20
|
+
"password",
|
21
|
+
"Rakefile",
|
22
|
+
"README"]
|
23
|
+
|
24
|
+
# RDoc
|
25
|
+
Rake::RDocTask.new do |rd|
|
26
|
+
rd.main = "README"
|
27
|
+
rd.rdoc_files.include("README", "lib/microengine/*.rb")
|
28
|
+
rd.title = "MicroEngine"
|
29
|
+
rd.options << "--all"
|
30
|
+
end
|
31
|
+
|
32
|
+
# Gem
|
33
|
+
spec = Gem::Specification.new do |s|
|
34
|
+
s.platform = Gem::Platform::RUBY
|
35
|
+
s.name = PKG_NAME
|
36
|
+
s.version = PKG_VERSION
|
37
|
+
s.summary = "MicroEngine is a fast, simple and minimalistic site engine."
|
38
|
+
s.description = <<-EOF
|
39
|
+
MicroEngine is a fast, simple and minimalistic site engine.
|
40
|
+
It contain good i18n support and web interface to edit, create and delete pages.
|
41
|
+
It be created for simple sites, which only share information
|
42
|
+
(maybe in some languages) like homepages.
|
43
|
+
EOF
|
44
|
+
|
45
|
+
s.add_dependency 'fastcgi'
|
46
|
+
|
47
|
+
s.files = PKG_FILES
|
48
|
+
s.require_path = 'lib'
|
49
|
+
s.bindir = "bin"
|
50
|
+
s.executables = ["microengine"]
|
51
|
+
s.default_executable = "microengine"
|
52
|
+
|
53
|
+
s.author = 'Andrey "A.I." Sitnik'
|
54
|
+
s.email = "andrey@sitnik.ru"
|
55
|
+
s.homepage = "http://microengine.rubyforge.org"
|
56
|
+
s.rubyforge_project = RUBY_FORGE_PROJECT
|
57
|
+
end
|
58
|
+
|
59
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
60
|
+
pkg.gem_spec = spec
|
61
|
+
end
|
62
|
+
|
63
|
+
# All-include package
|
64
|
+
Rake::PackageTask.new(PKG_NAME, PKG_VERSION) do |pkg|
|
65
|
+
pkg.need_tar_bz2 = true
|
66
|
+
pkg.package_files = PKG_FILES
|
67
|
+
end
|
data/access.log
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
E, [2008-01-26T20:17:16.989961 #10063] ERROR -- : /home/ai/Разработка/microengine/public/../lib/../access.log
|
data/bin/microengine
ADDED
File without changes
|
data/content/403/en.body
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
<title>403 - Access denied</title>
|
data/content/403/layout
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
default
|
data/content/403/ru.body
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
<title>403 - Доступ запрещён</title>
|
data/content/404/en.body
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
<title>404 - This page isn't exist</title>
|
data/content/404/layout
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
default
|
data/content/404/ru.body
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
<title>404 - Такой страницы нет</title>
|
data/content/WARNING
ADDED
data/content/en.body
ADDED
data/content/en.header
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
<title>It's work!</title>
|
data/content/layout
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
default
|
data/content/ru.body
ADDED
data/content/ru.header
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
<title>Работает!</title>
|
data/layouts/WARNING
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
2
|
+
<html xmlns="http://www.w3.org/1999/xhtml" lang="<%= translation['language']['code'] %>" xml:lang="<%= translation['language']['code'] %>">
|
3
|
+
<head>
|
4
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
5
|
+
<meta http-equiv="Content-Language" content="<%= translation['language']['code'] %>" />
|
6
|
+
<link rel="stylesheet" type="text/css" media="screen" href="/styles/screen.css" />
|
7
|
+
<link rel="stylesheet" type="text/css" media="print" href="/styles/print.css" />
|
8
|
+
<link rel="stylesheet" type="text/css" media="handheld" href="/styles/handheld.css" />
|
9
|
+
<%= header %>
|
10
|
+
</head>
|
11
|
+
<body>
|
12
|
+
<%= body %>
|
13
|
+
<%= languages %>
|
14
|
+
<div id="admin">
|
15
|
+
<a href="/admin/edit<%= path %>"><%= translation['edit'] %></a>
|
16
|
+
</div>
|
17
|
+
</body>
|
18
|
+
</html>
|
@@ -0,0 +1,268 @@
|
|
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
|
+
require 'admin_page'
|
20
|
+
require 'admin_exception'
|
21
|
+
require 'fileutils'
|
22
|
+
|
23
|
+
module Microengine
|
24
|
+
# Change content and create/delete pages by web interface
|
25
|
+
class Admin
|
26
|
+
|
27
|
+
attr_writer :logger
|
28
|
+
attr_writer :shadow
|
29
|
+
attr_accessor :cache
|
30
|
+
attr_accessor :config
|
31
|
+
attr_accessor :dispatcher
|
32
|
+
attr_accessor :assambler
|
33
|
+
|
34
|
+
def initialize
|
35
|
+
@blocked = []
|
36
|
+
begin
|
37
|
+
@access_logger = Logger.new(MICROENGINE_ROOT + '/access.log', 7, 1048576)
|
38
|
+
@access_logger.level = Logger::WARN
|
39
|
+
rescue
|
40
|
+
@access_logger = Logger.new(STDERR)
|
41
|
+
@access_logger.warn "Can't access /access.log . Ensure, that it exists and is chmod 0666."
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Check HTTP request for admin actions
|
46
|
+
def dispatch(http)
|
47
|
+
if http.url_start? '/admin/'
|
48
|
+
if http.url_start? '/admin/do/'
|
49
|
+
command, path = http.url[10..-1].split('/', 2)
|
50
|
+
|
51
|
+
if not http.post['password'].nil?
|
52
|
+
password = http.post['password']
|
53
|
+
elsif not http.cookie['password'].nil?
|
54
|
+
password = http.cookie['password']
|
55
|
+
else
|
56
|
+
@dispatcher.not_found http
|
57
|
+
return true
|
58
|
+
end
|
59
|
+
|
60
|
+
if not blocked? http.ip and right? password
|
61
|
+
if http.cookie['password'].nil?
|
62
|
+
http.set_cookie 'password', password
|
63
|
+
end
|
64
|
+
|
65
|
+
begin
|
66
|
+
case command
|
67
|
+
when 'save':
|
68
|
+
save http, path
|
69
|
+
when 'delete':
|
70
|
+
delete http, path
|
71
|
+
when 'untranslate':
|
72
|
+
untranslate http, path
|
73
|
+
when 'refresh':
|
74
|
+
refresh_cache
|
75
|
+
http.redirect '/'
|
76
|
+
else
|
77
|
+
dispatcher.not_found http
|
78
|
+
end
|
79
|
+
rescue AdminException => e
|
80
|
+
AdminPage.new(http, path, self).error(e.message)
|
81
|
+
end
|
82
|
+
else
|
83
|
+
#TODO delete cookie
|
84
|
+
block http.ip, 5
|
85
|
+
@access_logger.warn 'Wrong password from ' + http.ip
|
86
|
+
http.status = '403 Forbidden'
|
87
|
+
http << @cache.get('/403/', dispatcher.language(http, @cache.langs['/403/']))
|
88
|
+
end
|
89
|
+
else
|
90
|
+
command, path = http.url[7..-1].split('/', 2)
|
91
|
+
|
92
|
+
case command
|
93
|
+
when 'edit':
|
94
|
+
AdminPage.new(http, path, self).editor
|
95
|
+
when 'untranslate':
|
96
|
+
AdminPage.new(http, path, self).untranslater
|
97
|
+
when 'delete':
|
98
|
+
AdminPage.new(http, path, self).deleter
|
99
|
+
when 'refresh':
|
100
|
+
AdminPage.new(http, path, self).refresher
|
101
|
+
else
|
102
|
+
dispatcher.not_found http
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
true
|
107
|
+
|
108
|
+
else
|
109
|
+
false
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
# Save page content
|
116
|
+
def save(http, path)
|
117
|
+
lang = http.post['lang']
|
118
|
+
if lang.nil?
|
119
|
+
raise AdminException, 'no_lang'
|
120
|
+
end
|
121
|
+
if http.post['body'].nil? or '' == http.post['body']
|
122
|
+
raise AdminException, 'no_body'
|
123
|
+
end
|
124
|
+
if http.post['header'].nil?
|
125
|
+
raise AdminException, 'no_header'
|
126
|
+
end
|
127
|
+
if 'admin/' == path[0..5]
|
128
|
+
raise AdminException, 'admin_subdir'
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
begin
|
133
|
+
FileUtils.makedirs MICROENGINE_ROOT + '/content/' + path
|
134
|
+
{'body' => "#{lang}.body", 'header' => "#{lang}.header", 'layout' => 'layout'}.each do |post, file|
|
135
|
+
filename = "/content/#{path}#{file}"
|
136
|
+
file = File.new MICROENGINE_ROOT + filename, 'w'
|
137
|
+
file.write http.post[post]
|
138
|
+
file.close
|
139
|
+
end
|
140
|
+
rescue
|
141
|
+
raise AdminException, 'cant_write'
|
142
|
+
end
|
143
|
+
|
144
|
+
refresh_cache
|
145
|
+
http.redirect '/' + path
|
146
|
+
end
|
147
|
+
|
148
|
+
# Delete page translation
|
149
|
+
def untranslate(http, path)
|
150
|
+
lang = http.post['lang']
|
151
|
+
if lang.nil?
|
152
|
+
raise AdminException, 'no_lang'
|
153
|
+
end
|
154
|
+
|
155
|
+
['body', 'header'].each do |type|
|
156
|
+
filename = "/content/#{path}#{lang}.#{type}"
|
157
|
+
file = MICROENGINE_ROOT + filename
|
158
|
+
if not File.exists? file
|
159
|
+
http.redirect '/' + path
|
160
|
+
end
|
161
|
+
|
162
|
+
begin
|
163
|
+
File.delete file
|
164
|
+
rescue
|
165
|
+
raise AdminException, 'cant_write'
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
refresh_cache
|
170
|
+
http.redirect '/' + path
|
171
|
+
end
|
172
|
+
|
173
|
+
# Delete page
|
174
|
+
def delete(http, path)
|
175
|
+
if ['404/', '403/', ''].include? path
|
176
|
+
raise AdminException, 'delete_system'
|
177
|
+
end
|
178
|
+
|
179
|
+
dir = MICROENGINE_ROOT + '/content/' + path
|
180
|
+
if not File.exists? dir + 'layout'
|
181
|
+
http.redirect '/'
|
182
|
+
return
|
183
|
+
end
|
184
|
+
|
185
|
+
Dir.foreach(dir) do |file|
|
186
|
+
if File.directory? dir + file
|
187
|
+
next
|
188
|
+
elsif file =~ /^(layout|[^\.]+\.(body|header))$/
|
189
|
+
begin
|
190
|
+
File.delete dir + file
|
191
|
+
rescue
|
192
|
+
raise AdminException, 'cant_write'
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Delete empty dirs
|
198
|
+
dir = path
|
199
|
+
while true
|
200
|
+
fullpath = MICROENGINE_ROOT + '/content/' + dir
|
201
|
+
if 2 == Dir.entries(fullpath).length
|
202
|
+
begin
|
203
|
+
Dir.rmdir fullpath
|
204
|
+
rescue
|
205
|
+
break
|
206
|
+
end
|
207
|
+
else
|
208
|
+
break
|
209
|
+
end
|
210
|
+
index = dir.rindex('/', -2)
|
211
|
+
if index.nil?
|
212
|
+
break
|
213
|
+
end
|
214
|
+
dir = dir[0..index]
|
215
|
+
end
|
216
|
+
|
217
|
+
refresh_cache
|
218
|
+
http.redirect '/'
|
219
|
+
end
|
220
|
+
|
221
|
+
# Check admin assword
|
222
|
+
def right?(password)
|
223
|
+
hash = SHA1::sha1(password + @shadow['salt']).to_s
|
224
|
+
if hash == @shadow['hash']
|
225
|
+
true
|
226
|
+
else
|
227
|
+
false
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Block IP for bruteforce defence
|
232
|
+
def block(ip, seconds)
|
233
|
+
@blocked.push [ip, Time.now + seconds]
|
234
|
+
end
|
235
|
+
|
236
|
+
# Check is IP blocked
|
237
|
+
def blocked?(ip)
|
238
|
+
now = Time.now
|
239
|
+
# Delete old
|
240
|
+
@blocked.each do |blocked|
|
241
|
+
if blocked[1] < now
|
242
|
+
@blocked.delete blocked
|
243
|
+
else
|
244
|
+
break
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# Check
|
249
|
+
@blocked.each do |blocked|
|
250
|
+
if blocked[0] == ip
|
251
|
+
return true
|
252
|
+
end
|
253
|
+
end
|
254
|
+
false
|
255
|
+
end
|
256
|
+
|
257
|
+
# Recreate cache
|
258
|
+
def refresh_cache
|
259
|
+
begin
|
260
|
+
@assambler.refresh
|
261
|
+
@cache.refresh
|
262
|
+
rescue Exception => e
|
263
|
+
AdminPage.new(http, self).error('cant_cache')
|
264
|
+
raise "Cache wasn't refreshed. " + e.message
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|