ruwiki 0.9.0
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/Readme.rubygems +86 -0
- data/Readme.tarfile +65 -0
- data/bin/ruwiki +58 -0
- data/bin/ruwiki.cgi +87 -0
- data/bin/ruwiki_convert +56 -0
- data/bin/ruwiki_service.rb +82 -0
- data/bin/ruwiki_servlet +53 -0
- data/contrib/enscript-token.rb +55 -0
- data/contrib/rublog_integrator.rb +68 -0
- data/data/Default/ProjectIndex.ruwiki +49 -0
- data/data/Ruwiki/Antispam.ruwiki +65 -0
- data/data/Ruwiki/BugTracking.ruwiki +33 -0
- data/data/Ruwiki/ChangeLog.ruwiki +102 -0
- data/data/Ruwiki/Configuring_Ruwiki.ruwiki +151 -0
- data/data/Ruwiki/Extending_Ruwiki.ruwiki +317 -0
- data/data/Ruwiki/LicenseAndAuthorInfo.ruwiki +30 -0
- data/data/Ruwiki/ProjectIndex.ruwiki +84 -0
- data/data/Ruwiki/Roadmap.ruwiki +225 -0
- data/data/Ruwiki/RuwikiTemplatingLibrary.ruwiki +156 -0
- data/data/Ruwiki/RuwikiUtilities.ruwiki +157 -0
- data/data/Ruwiki/SandBox.ruwiki +9 -0
- data/data/Ruwiki/To_Do.ruwiki +51 -0
- data/data/Ruwiki/TroubleShooting.ruwiki +33 -0
- data/data/Ruwiki/WikiFeatures.ruwiki +17 -0
- data/data/Ruwiki/WikiMarkup.ruwiki +261 -0
- data/data/Tutorial/AddingPages.ruwiki +16 -0
- data/data/Tutorial/AddingProjects.ruwiki +16 -0
- data/data/Tutorial/ProjectIndex.ruwiki +11 -0
- data/data/Tutorial/SandBox.ruwiki +9 -0
- data/data/agents.banned +60 -0
- data/data/agents.readonly +321 -0
- data/data/hostip.banned +30 -0
- data/data/hostip.readonly +28 -0
- data/lib/ruwiki.rb +622 -0
- data/lib/ruwiki/auth.rb +56 -0
- data/lib/ruwiki/auth/gforge.rb +73 -0
- data/lib/ruwiki/backend.rb +318 -0
- data/lib/ruwiki/backend/flatfiles.rb +217 -0
- data/lib/ruwiki/config.rb +244 -0
- data/lib/ruwiki/exportable.rb +192 -0
- data/lib/ruwiki/handler.rb +342 -0
- data/lib/ruwiki/lang/de.rb +339 -0
- data/lib/ruwiki/lang/en.rb +334 -0
- data/lib/ruwiki/lang/es.rb +339 -0
- data/lib/ruwiki/page.rb +262 -0
- data/lib/ruwiki/servlet.rb +38 -0
- data/lib/ruwiki/template.rb +553 -0
- data/lib/ruwiki/utils.rb +24 -0
- data/lib/ruwiki/utils/command.rb +102 -0
- data/lib/ruwiki/utils/converter.rb +297 -0
- data/lib/ruwiki/utils/manager.rb +639 -0
- data/lib/ruwiki/utils/servletrunner.rb +295 -0
- data/lib/ruwiki/wiki.rb +147 -0
- data/lib/ruwiki/wiki/tokens.rb +136 -0
- data/lib/ruwiki/wiki/tokens/00default.rb +211 -0
- data/lib/ruwiki/wiki/tokens/01wikilinks.rb +166 -0
- data/lib/ruwiki/wiki/tokens/02actions.rb +63 -0
- data/lib/ruwiki/wiki/tokens/abbreviations.rb +40 -0
- data/lib/ruwiki/wiki/tokens/calendar.rb +147 -0
- data/lib/ruwiki/wiki/tokens/headings.rb +43 -0
- data/lib/ruwiki/wiki/tokens/lists.rb +112 -0
- data/lib/ruwiki/wiki/tokens/rubylists.rb +48 -0
- data/ruwiki.conf +22 -0
- data/ruwiki.pkg +0 -0
- data/templates/default/body.tmpl +19 -0
- data/templates/default/content.tmpl +7 -0
- data/templates/default/controls.tmpl +23 -0
- data/templates/default/edit.tmpl +27 -0
- data/templates/default/error.tmpl +14 -0
- data/templates/default/footer.tmpl +23 -0
- data/templates/default/ruwiki.css +297 -0
- data/templates/default/save.tmpl +8 -0
- data/templates/sidebar/body.tmpl +19 -0
- data/templates/sidebar/content.tmpl +8 -0
- data/templates/sidebar/controls.tmpl +8 -0
- data/templates/sidebar/edit.tmpl +27 -0
- data/templates/sidebar/error.tmpl +13 -0
- data/templates/sidebar/footer.tmpl +22 -0
- data/templates/sidebar/ruwiki.css +347 -0
- data/templates/sidebar/save.tmpl +10 -0
- data/templates/simple/body.tmpl +13 -0
- data/templates/simple/content.tmpl +7 -0
- data/templates/simple/controls.tmpl +8 -0
- data/templates/simple/edit.tmpl +25 -0
- data/templates/simple/error.tmpl +10 -0
- data/templates/simple/footer.tmpl +10 -0
- data/templates/simple/ruwiki.css +192 -0
- data/templates/simple/save.tmpl +8 -0
- data/tests/harness.rb +52 -0
- data/tests/tc_backend_flatfile.rb +103 -0
- data/tests/tc_bugs.rb +74 -0
- data/tests/tc_exportable.rb +64 -0
- data/tests/tc_template.rb +145 -0
- data/tests/tc_tokens.rb +335 -0
- data/tests/testall.rb +20 -0
- metadata +182 -0
data/lib/ruwiki/auth.rb
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# Ruwiki
|
|
3
|
+
# Copyright � 2002 - 2004, Digikata and HaloStatue
|
|
4
|
+
# Alan Chen (alan@digikata.com)
|
|
5
|
+
# Austin Ziegler (ruwiki@halostatue.ca)
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the same terms as Ruby.
|
|
8
|
+
#
|
|
9
|
+
# $Id: auth.rb,v 1.2 2004/11/22 04:53:40 austin Exp $
|
|
10
|
+
#++
|
|
11
|
+
class Ruwiki::Auth
|
|
12
|
+
class << self
|
|
13
|
+
def [](name)
|
|
14
|
+
@delegate ||= {}
|
|
15
|
+
|
|
16
|
+
if @delegate.has_key?(name)
|
|
17
|
+
@delegate[name]
|
|
18
|
+
else
|
|
19
|
+
require "ruwiki/auth/#{name}"
|
|
20
|
+
@delegate[name] = Ruwiki::Auth.const_get(name.capitalize)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class Token
|
|
26
|
+
def initialize(name = nil, groups = [], permissions = {})
|
|
27
|
+
@user_name = name
|
|
28
|
+
@groups = groups
|
|
29
|
+
@permissions = permissions
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def found?
|
|
33
|
+
not @user_name.nil?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def name
|
|
37
|
+
@user_name
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def member?(unix_group_name)
|
|
41
|
+
@groups.include?(unix_group_name)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def groups
|
|
45
|
+
@groups
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def allowed?(action)
|
|
49
|
+
@permission[action]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def permissions
|
|
53
|
+
@permissions
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# Ruwiki
|
|
3
|
+
# Copyright � 2002 - 2004, Digikata and HaloStatue
|
|
4
|
+
# Alan Chen (alan@digikata.com)
|
|
5
|
+
# Austin Ziegler (ruwiki@halostatue.ca)
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the same terms as Ruby.
|
|
8
|
+
#
|
|
9
|
+
# $Id: gforge.rb,v 1.3 2004/11/28 23:28:15 austin Exp $
|
|
10
|
+
#++
|
|
11
|
+
|
|
12
|
+
begin
|
|
13
|
+
require 'gforge_auth'
|
|
14
|
+
rescue LoadError
|
|
15
|
+
class GForgeAuthenticator
|
|
16
|
+
class AuthenticationResult
|
|
17
|
+
def initialize(name = nil, groups = [])
|
|
18
|
+
@user_name = name
|
|
19
|
+
@groups = groups
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def found?
|
|
23
|
+
not @user_name.nil?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def user_name
|
|
27
|
+
raise "No session associated with the given key was found" unless found?
|
|
28
|
+
@user_name
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def member?(unix_group_name)
|
|
32
|
+
raise "No session associated with the given key was found" unless found?
|
|
33
|
+
@groups.include?(unix_group_name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def groups
|
|
37
|
+
raise "No session associated with the given key was found" unless found?
|
|
38
|
+
@groups
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.authenticate(sessionkey, options = {})
|
|
43
|
+
sql = %Q(SELECT user_name FROM users u, user_session us WHERE us.session_hash = '#{sessionkey}' AND us.user_id = u.user_id;)
|
|
44
|
+
res = %x{psql -q -t -U #{options['user']} #{options['pass']} -c \"#{sql}\"}
|
|
45
|
+
rows = res.split(/\n/)
|
|
46
|
+
return AuthenticationResult.new if rows.size != 1
|
|
47
|
+
|
|
48
|
+
user_name = rows[0].strip
|
|
49
|
+
sql = %Q(SELECT unix_group_name FROM groups g, users u, user_group ug WHERE u.user_name = '#{user_name}' AND ug.user_id = u.user_id AND g.group_id = ug.group_id)
|
|
50
|
+
|
|
51
|
+
res = %x(psql -q -t -U #{options['user']} #{options['pass']} -c \"#{sql}\")
|
|
52
|
+
groups = []
|
|
53
|
+
res.split(/\n/).each {|row| groups << row.strip }
|
|
54
|
+
AuthenticationResult.new(user_name, groups)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class Ruwiki::Auth::Gforge < Ruwiki::Auth
|
|
60
|
+
def self.authenticate(request, response, options = {})
|
|
61
|
+
options['user'] = options['user'].gsub!(%r{^(\w+)}, '\1')
|
|
62
|
+
options['pass'] = options['pass'].gsub!(%r{^(\w+)}, '\1')
|
|
63
|
+
session_key = request.cookies['session_ser'].value[0].split(%r{-\*-})[-1]
|
|
64
|
+
token = GForgeAuthenticator.authenticate(session_key, options)
|
|
65
|
+
token = Ruwiki::Auth::Token.new(token.user_name, token.groups)
|
|
66
|
+
token.permissions.default = true if token.found?
|
|
67
|
+
rescue
|
|
68
|
+
token = Ruwiki::Auth::Token.new
|
|
69
|
+
token.permissions.default = false
|
|
70
|
+
ensure
|
|
71
|
+
return token
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# Ruwiki
|
|
3
|
+
# Copyright � 2002 - 2004, Digikata and HaloStatue
|
|
4
|
+
# Alan Chen (alan@digikata.com)
|
|
5
|
+
# Austin Ziegler (ruwiki@halostatue.ca)
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the same terms as Ruby.
|
|
8
|
+
#
|
|
9
|
+
# $Id: backend.rb,v 1.24 2004/11/28 02:26:57 austin Exp $
|
|
10
|
+
#++
|
|
11
|
+
|
|
12
|
+
begin
|
|
13
|
+
if defined?(Gem::Cache)
|
|
14
|
+
require_gem 'diff-lcs', '~> 1.1.2'
|
|
15
|
+
else
|
|
16
|
+
require 'diff/lcs'
|
|
17
|
+
end
|
|
18
|
+
rescue LoadError => ex
|
|
19
|
+
$stderr.puts ex.message
|
|
20
|
+
raise
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class Ruwiki
|
|
24
|
+
# The list of known backends.
|
|
25
|
+
KNOWN_BACKENDS = %w(flatfiles)
|
|
26
|
+
|
|
27
|
+
# The Ruwiki backend delegator. Ruwiki will always instantiate a version
|
|
28
|
+
# of this class which delegates the actual method execution to the Backend
|
|
29
|
+
# class. Error handling is handled by capturing (and possibly forwarding)
|
|
30
|
+
# exceptions raised by the delegate class.
|
|
31
|
+
class BackendDelegator
|
|
32
|
+
def initialize(ruwiki, backend)
|
|
33
|
+
@message = ruwiki.config.message
|
|
34
|
+
@time_format = ruwiki.config.time_format || "%H:%M:%S"
|
|
35
|
+
@date_format = ruwiki.config.date_format || "%Y.%m.%d"
|
|
36
|
+
@datetime_format = ruwiki.config.datetime_format || "#{@date_format} #{@time_format}"
|
|
37
|
+
options = ruwiki.config.storage_options
|
|
38
|
+
options['default-page'] = ruwiki.config.default_page
|
|
39
|
+
|
|
40
|
+
unless Ruwiki::KNOWN_BACKENDS.include?(backend)
|
|
41
|
+
raise RuntimeError, @message[:backend_unknown] % [backend]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
beconst = backend.capitalize
|
|
45
|
+
|
|
46
|
+
require "ruwiki/backend/#{backend}"
|
|
47
|
+
|
|
48
|
+
beoptions = options[backend]
|
|
49
|
+
@delegate = Ruwiki::Backend.const_get(beconst).new(beoptions)
|
|
50
|
+
rescue Ruwiki::Backend::BackendError => ex
|
|
51
|
+
if ex.kind_of?(Array)
|
|
52
|
+
raise Ruwiki::Backend::BackendError.new(nil), @message[ex.reason[0]] % ex.reason[1]
|
|
53
|
+
else
|
|
54
|
+
raise
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Retrieve the specified topic and project page. Calls Backend#load
|
|
59
|
+
# after verifying that the project exists.
|
|
60
|
+
def retrieve(topic, project = 'Default')
|
|
61
|
+
unless page_exists?(topic, project)
|
|
62
|
+
exported = Ruwiki::Page::NULL_PAGE.dup
|
|
63
|
+
exported['properties'] = {
|
|
64
|
+
'title' => topic,
|
|
65
|
+
'topic' => topic,
|
|
66
|
+
'project' => project,
|
|
67
|
+
'create-date' => Time.now,
|
|
68
|
+
'edit-date' => Time.now,
|
|
69
|
+
'editable' => true,
|
|
70
|
+
'indexable' => true,
|
|
71
|
+
'entropy' => 0.0,
|
|
72
|
+
'html-headers' => [],
|
|
73
|
+
'version' => 0
|
|
74
|
+
}
|
|
75
|
+
exported['page'] = {
|
|
76
|
+
'header' => nil,
|
|
77
|
+
'footer' => nil
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if project_exists?(project)
|
|
81
|
+
exported['page']['content'] = ""
|
|
82
|
+
else
|
|
83
|
+
exported['page']['content'] = @message[:project_does_not_exist] % [project]
|
|
84
|
+
end
|
|
85
|
+
return exported
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
return @delegate.load(topic, project)
|
|
89
|
+
rescue Ruwiki::Backend::InvalidFormatError => ex
|
|
90
|
+
raise Ruwiki::Backend::BackendError.new(nil), @message[:page_not_in_backend_format] % [project, topic, @delegate.class]
|
|
91
|
+
rescue Errno::EACCES => ex
|
|
92
|
+
raise Ruwiki::Backend::BackendError.new(ex), @message[:no_access_to_read_topic] % [project, topic]
|
|
93
|
+
rescue Exception => ex
|
|
94
|
+
mm = [project, topic, %Q~#{ex}<br />\n#{ex.backtrace.join('<br />\n')}~]
|
|
95
|
+
raise Ruwiki::Backend::BackendError.new(ex), @message[:cannot_retrieve_topic] % mm
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Stores the specified topic and project page.
|
|
99
|
+
def store(page)
|
|
100
|
+
@delegate.store(page)
|
|
101
|
+
|
|
102
|
+
# update change page
|
|
103
|
+
begin
|
|
104
|
+
recent_changes = nil
|
|
105
|
+
if (page.topic == 'RecentChanges')
|
|
106
|
+
recent_changes = page.dup
|
|
107
|
+
else
|
|
108
|
+
recent_changes = Page.new(retrieve('RecentChanges', page.project))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
changeline = "\n; #{page.editor_ip} (#{Time.now.strftime(@datetime_format)}), #{page.topic} : #{page.edit_comment}"
|
|
112
|
+
|
|
113
|
+
# add changeline to top of page
|
|
114
|
+
recent_changes.content = changeline + (recent_changes.content || "")
|
|
115
|
+
@delegate.store(recent_changes)
|
|
116
|
+
rescue Exception => ex
|
|
117
|
+
raise "Couldn't save RecentChanges\n#{ex.backtrace}"
|
|
118
|
+
end
|
|
119
|
+
rescue Errno::EACCES => ex
|
|
120
|
+
raise Ruwiki::Backend::BackendError.new(ex), @message[:no_access_to_store_topic] % [page.project, page.topic]
|
|
121
|
+
rescue Exception => ex
|
|
122
|
+
mm = [page.project, page.topic, %Q~#{ex}<br />\n#{ex.backtrace.join('<br />\n')}~]
|
|
123
|
+
raise Ruwiki::Backend::BackendError.new(ex), @message[:cannot_store_topic] % mm
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Destroys the specified topic and project page.
|
|
127
|
+
def destroy(page)
|
|
128
|
+
@delegate.destroy(page)
|
|
129
|
+
rescue Errno::EACCES => ex
|
|
130
|
+
raise Ruwiki::Backend::BackendError.new(ex), @message[:no_access_to_destroy_topic] % [page.project, page.topic]
|
|
131
|
+
rescue Exception => ex
|
|
132
|
+
mm = [page.project, page.topic, %Q~#{ex}<br />\n#{ex.backtrace.join('<br />\n')}~]
|
|
133
|
+
raise Ruwiki::Backend::BackendError.new(ex), @message[:cannot_destroy_topic] % mm
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Releases the lock on the page.
|
|
137
|
+
def release_lock(page, address = 'UNKNOWN')
|
|
138
|
+
time = Time.now.to_i
|
|
139
|
+
@delegate.release_lock(page, time, address)
|
|
140
|
+
rescue Ruwiki::Backend::BackendError
|
|
141
|
+
raise Ruwiki::Backend::BackendError.new(nil), @message[:cannot_release_lock] % [page.project, page.topic]
|
|
142
|
+
rescue Errno::EACCES, Exception => ex
|
|
143
|
+
mm = [page.project, page.topic, %Q~#{ex}<br />\n#{ex.backtrace.join('<br />\n')}~]
|
|
144
|
+
raise Ruwiki::Backend::BackendError.new(ex), @message[:error_releasing_lock] % mm
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Attempts to obtain a lock on the page. The lock
|
|
148
|
+
def obtain_lock(page, address = 'UNKNOWN', timeout = 600)
|
|
149
|
+
time = Time.now.to_i
|
|
150
|
+
expire = time + timeout
|
|
151
|
+
@delegate.obtain_lock(page, time, expire, address)
|
|
152
|
+
rescue Ruwiki::Backend::BackendError
|
|
153
|
+
raise Ruwiki::Backend::BackendError.new(nil), @message[:cannot_obtain_lock] % [page.project, page.topic]
|
|
154
|
+
rescue Errno::EACCES, Exception => ex
|
|
155
|
+
mm = [page.project, page.topic, %Q~#{ex}<br />\n#{ex.backtrace.join('<br />\n')}~]
|
|
156
|
+
raise Ruwiki::Backend::BackendError.new(ex), @message[:error_creating_lock] % mm
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Checks to see if the project exists.
|
|
160
|
+
def project_exists?(project)
|
|
161
|
+
@delegate.project_exists?(project)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Checks to see if the page exists.
|
|
165
|
+
def page_exists?(topic, project = 'Default')
|
|
166
|
+
@delegate.page_exists?(topic, project)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Attempts to create the project.
|
|
170
|
+
def create_project(project)
|
|
171
|
+
@delegate.create_project(project)
|
|
172
|
+
rescue Ruwiki::Backend::ProjectExists => ex
|
|
173
|
+
raise Ruwiki::Backend::BackendError.new(ex), @message[:project_already_exists] % [project]
|
|
174
|
+
rescue Errno::EACCES => ex
|
|
175
|
+
raise Ruwiki::Backend::BackendError.new(ex), @message[:no_access_to_create_project] % [project]
|
|
176
|
+
rescue Exception => ex
|
|
177
|
+
mm = [project, %Q~#{ex}<br />\n#{ex.backtrace.join('<br />\n')}~]
|
|
178
|
+
raise Ruwiki::Backend::BackendError.new(ex), @message[:cannot_create_project] % mm
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Attempts to destroy the project.
|
|
182
|
+
def destroy_project(project)
|
|
183
|
+
@delegate.destroy_project(project)
|
|
184
|
+
rescue Errno::EACCES => ex
|
|
185
|
+
raise Ruwiki::Backend::BackendError.new(ex), @message[:no_access_to_destroy_project] % [project]
|
|
186
|
+
rescue Exception => ex
|
|
187
|
+
mm = [project, %Q~#{ex}<br />\n#{ex.backtrace.join('<br />\n')}~]
|
|
188
|
+
raise Ruwiki::Backend::BackendError.new(ex), @message[:cannot_destroy_project] % mm
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def search_all_projects(searchstr)
|
|
192
|
+
if @delegate.respond_to?(:search_all_projects)
|
|
193
|
+
@delegate.search_all_projects(searchstr)
|
|
194
|
+
else
|
|
195
|
+
search_all_projects_default(searchstr)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Attempts to search all projects. This is the default
|
|
200
|
+
# search_all_projects used unless the delegate implements
|
|
201
|
+
# a specialized search_all_projects.
|
|
202
|
+
def search_all_projects_default(searchstr)
|
|
203
|
+
hits = {}
|
|
204
|
+
list_projects.each do |project|
|
|
205
|
+
lhits = search_project(project, searchstr)
|
|
206
|
+
# Transform the keys from project local to global links.
|
|
207
|
+
lhits.each { |key, val| hits["#{project}::#{key}"] = val }
|
|
208
|
+
end
|
|
209
|
+
hits
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Attempts to search a project
|
|
213
|
+
def search_project(project, searchstr)
|
|
214
|
+
#TODO: Validate searchstr is a safe regexp?
|
|
215
|
+
@delegate.search_project(project, searchstr)
|
|
216
|
+
rescue Exception => ex
|
|
217
|
+
mm = [project, searchstr, ex.class, %Q~#{ex}<br />\n#{ex.backtrace.join('<br />\n')}~]
|
|
218
|
+
raise Ruwiki::Backend::BackendError.new(ex), @message[:search_project_fail] % mm
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Return an array of projects
|
|
222
|
+
def list_projects
|
|
223
|
+
@delegate.list_projects
|
|
224
|
+
rescue Errno::EACCES => ex
|
|
225
|
+
raise Ruwiki::Backend::BackendError.new(ex), @message[:no_access_list_projects]
|
|
226
|
+
rescue Exception => ex
|
|
227
|
+
mm = ['', %Q~#{ex}<br />\n#{ex.backtrace.join('<br />\n')}~]
|
|
228
|
+
raise Ruwiki::Backend::BackendError.new(ex), @message[:cannot_list_projects] % mm
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Return an array of projects
|
|
232
|
+
def list_topics(projname)
|
|
233
|
+
@delegate.list_topics(projname)
|
|
234
|
+
rescue Errno::EACCES => ex
|
|
235
|
+
raise Ruwiki::Backend::BackendError.new(ex), @message[:no_access_list_topics] % [projname]
|
|
236
|
+
rescue Exception => ex
|
|
237
|
+
mm = [projname, ex.message]
|
|
238
|
+
raise Ruwiki::Backend::BackendError.new(ex), @message[:cannot_list_topics] % mm
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# The Ruwiki backend abstract class and factory.
|
|
243
|
+
class Backend
|
|
244
|
+
class ProjectExists < RuntimeError #:nodoc:
|
|
245
|
+
end
|
|
246
|
+
class InvalidFormatError < RuntimeError #:nodoc:
|
|
247
|
+
end
|
|
248
|
+
class BackendError < RuntimeError #:nodoc:
|
|
249
|
+
attr_reader :reason
|
|
250
|
+
|
|
251
|
+
def initialize(reason, *args)
|
|
252
|
+
if @reason.respond_to?(:message)
|
|
253
|
+
@reason = reason.message
|
|
254
|
+
else
|
|
255
|
+
@reason = reason
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
def initialize(storage_options)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
private
|
|
263
|
+
NL_RE = %r{\n} #:nodoc:
|
|
264
|
+
|
|
265
|
+
def map_diffset(diffset)
|
|
266
|
+
diffset.map do |hunk|
|
|
267
|
+
if hunk.kind_of?(Array)
|
|
268
|
+
hunk.map { |change| change.to_a }
|
|
269
|
+
else
|
|
270
|
+
hunk.to_a
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Creates the current diff object. This is made from two
|
|
276
|
+
# Ruwiki::Page#export hashes.
|
|
277
|
+
def make_diff(oldpage, newpage)
|
|
278
|
+
oldpage = oldpage.export if oldpage.kind_of?(Ruwiki::Page)
|
|
279
|
+
newpage = newpage.export if newpage.kind_of?(Ruwiki::Page)
|
|
280
|
+
|
|
281
|
+
diff = Hash.new
|
|
282
|
+
|
|
283
|
+
newpage.keys.sort.each do |sect|
|
|
284
|
+
newpage[sect].keys.sort.each do |item|
|
|
285
|
+
oldval = oldpage[sect][item]
|
|
286
|
+
newval = newpage[sect][item]
|
|
287
|
+
|
|
288
|
+
case [sect, item]
|
|
289
|
+
when ['properties', 'html-headers']
|
|
290
|
+
# Protect against NoMethodError.
|
|
291
|
+
oldval ||= []
|
|
292
|
+
newval ||= []
|
|
293
|
+
val = Diff::LCS.sdiff(oldval, newval, Diff::LCS::ContextDiffCallbacks)
|
|
294
|
+
when ['ruwiki', 'content-version'], ['properties', 'version'],
|
|
295
|
+
['properties', 'entropy']
|
|
296
|
+
val = Diff::LCS.sdiff([oldval], [newval], Diff::LCS::ContextDiffCallbacks)
|
|
297
|
+
when ['properties', 'create-date'], ['properties', 'edit-date']
|
|
298
|
+
val = Diff::LCS.sdiff([oldval.to_i], [newval.to_i], Diff::LCS::ContextDiffCallbacks)
|
|
299
|
+
else
|
|
300
|
+
# Protect against NoMethodError.
|
|
301
|
+
val = Diff::LCS.sdiff(oldval.to_s.split(NL_RE), newval.to_s.split(NL_RE), Diff::LCS::ContextDiffCallbacks)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
(diff[sect] ||= {})[item] = map_diffset(val) unless val.nil? or val.empty?
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
diff_hash = {
|
|
309
|
+
'old_version' => oldpage['properties']['version'],
|
|
310
|
+
'new_version' => newpage['properties']['version'],
|
|
311
|
+
'edit-date' => newpage['properties']['edit-date'].to_i,
|
|
312
|
+
'editor-ip' => newpage['properties']['editor-ip'],
|
|
313
|
+
'editor' => newpage['properties']['editor'],
|
|
314
|
+
'diff' => diff
|
|
315
|
+
}
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# Ruwiki
|
|
3
|
+
# Copyright � 2002 - 2004, Digikata and HaloStatue
|
|
4
|
+
# Alan Chen (alan@digikata.com)
|
|
5
|
+
# Austin Ziegler (ruwiki@halostatue.ca)
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the same terms as Ruby.
|
|
8
|
+
#
|
|
9
|
+
# $Id: flatfiles.rb,v 1.25 2004/11/26 12:18:47 austin Exp $
|
|
10
|
+
#++
|
|
11
|
+
require 'ruwiki/exportable'
|
|
12
|
+
|
|
13
|
+
# Stores Ruwiki pages as flatfiles.
|
|
14
|
+
class Ruwiki::Backend::Flatfiles < Ruwiki::Backend
|
|
15
|
+
# Initializes the Flatfiles backend. The known options for the Flatfiles
|
|
16
|
+
# backend are documented below.
|
|
17
|
+
#
|
|
18
|
+
# data-path:: The directory in which the wiki files will be found. By
|
|
19
|
+
# default, this is "./data/"
|
|
20
|
+
# extension:: The extension of the wiki files. By default, this is
|
|
21
|
+
# +nil+ in the backend.
|
|
22
|
+
# format:: The format of the files in the backend. By default,
|
|
23
|
+
# this is 'exportable', a tagged data format produced by
|
|
24
|
+
# Ruwiki::Exportable; alternative formats are 'yaml'
|
|
25
|
+
# (::YAML.dump) and 'marshal' (::Marshal.dump).
|
|
26
|
+
# default-page:: The default page for a project. By default, this is
|
|
27
|
+
# ProjectIndex. This is provided only so that the backend
|
|
28
|
+
# can make reasonable guesses.
|
|
29
|
+
def initialize(options)
|
|
30
|
+
@data_path = options['data-path'] || File.join(".", "data")
|
|
31
|
+
@extension = options['extension']
|
|
32
|
+
@format = case options['format']
|
|
33
|
+
when 'exportable', nil
|
|
34
|
+
Ruwiki::Exportable
|
|
35
|
+
when 'yaml'
|
|
36
|
+
::YAML
|
|
37
|
+
when 'marshal'
|
|
38
|
+
::Marshal
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if @extension.nil?
|
|
42
|
+
@extension_re = /$/
|
|
43
|
+
else
|
|
44
|
+
@extension_re = /\.#{@extension}$/
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
@default_page = options['default-page'] || "ProjectIndex"
|
|
48
|
+
if not (File.exists?(@data_path) and File.directory?(@data_path))
|
|
49
|
+
raise Ruwiki::Backend::BackendError.new([:flatfiles_no_data_directory, [@data_path]])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
super
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Destroys the topic page.
|
|
56
|
+
def destroy(page)
|
|
57
|
+
pf = page_file(page.topic, page.project)
|
|
58
|
+
File.unlink(pf) if File.exists?(pf)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Checks to see if the project exists.
|
|
62
|
+
def project_exists?(project)
|
|
63
|
+
pd = project_directory(project)
|
|
64
|
+
File.exists?(pd) and File.directory?(pd)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Checks to see if the page exists.
|
|
68
|
+
def page_exists?(topic, project = 'Default')
|
|
69
|
+
pf = page_file(topic, project)
|
|
70
|
+
project_exists?(project) and File.exists?(pf)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Tries to create the project.
|
|
74
|
+
def create_project(project)
|
|
75
|
+
pd = project_directory(project)
|
|
76
|
+
raise Ruwiki::Backend::ProjectExists if File.exists?(pd)
|
|
77
|
+
Dir.mkdir(pd)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Tries to destroy the project.
|
|
81
|
+
def destroy_project(project)
|
|
82
|
+
pd = project_directory(project)
|
|
83
|
+
Dir.rmdir(pd) if File.exists?(pd) and File.directory?(pd)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# String search all topic names and content in a project and
|
|
87
|
+
# return a hash of topic hits.
|
|
88
|
+
def search_project(project, searchstr)
|
|
89
|
+
re_search = Regexp.new(searchstr, Regexp::IGNORECASE)
|
|
90
|
+
|
|
91
|
+
hits = Hash.new { |hh, kk| hh[kk] = 0 }
|
|
92
|
+
topic_list = list_topics(project)
|
|
93
|
+
|
|
94
|
+
return hits if topic_list.empty?
|
|
95
|
+
|
|
96
|
+
# search topic content
|
|
97
|
+
topic_list.each do |topic|
|
|
98
|
+
# search name
|
|
99
|
+
hits[topic] += topic.scan(re_search).size
|
|
100
|
+
|
|
101
|
+
# check content
|
|
102
|
+
page = load(topic, project) rescue Ruwiki::Page::NULL_PAGE
|
|
103
|
+
page['page'].each_value do |item|
|
|
104
|
+
item = item.join("") if item.kind_of?(Array)
|
|
105
|
+
item ||= ""
|
|
106
|
+
hits[topic] += item.scan(re_search).size
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
hits
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def lock_okay?(page, time, address = 'UNKNOWN')
|
|
114
|
+
lockokay = false
|
|
115
|
+
lockfile = "#{page_file(page.topic, page.project)}.lock"
|
|
116
|
+
|
|
117
|
+
if File.exists?(lockfile)
|
|
118
|
+
data = File.read(lockfile).split(%r{!})
|
|
119
|
+
# If the lock belongs to this address, we don't care how old it is.
|
|
120
|
+
# Thus, release it.
|
|
121
|
+
lock_okay ||= (data[0].chomp == address)
|
|
122
|
+
# If the lock is older than 10 minutes, release it.
|
|
123
|
+
lock_okay ||= (data[1].to_i < time)
|
|
124
|
+
else
|
|
125
|
+
lockokay = true
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Attempts to obtain a lock on the topic page. This must return the lock
|
|
130
|
+
def obtain_lock(page, time, expire, address = 'UNKNOWN')
|
|
131
|
+
lock = "#{address}!#{expire}"
|
|
132
|
+
|
|
133
|
+
if lock_okay?(page, time, address)
|
|
134
|
+
File.open("#{page_file(page.topic, page.project)}.lock", 'wb') { |lfh| lfh.puts lock }
|
|
135
|
+
else
|
|
136
|
+
raise Ruwiki::Backend::BackendError.new(nil)
|
|
137
|
+
end
|
|
138
|
+
lock
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Releases the lock on the topic page.
|
|
142
|
+
def release_lock(page, time, address = 'UNKNOWN')
|
|
143
|
+
time = Time.now.to_i
|
|
144
|
+
lockfile = "#{page_file(page.topic, page.project)}.lock"
|
|
145
|
+
|
|
146
|
+
if lock_okay?(page, time, address)
|
|
147
|
+
File.unlink(lockfile) if File.exists?(lockfile)
|
|
148
|
+
else
|
|
149
|
+
raise Ruwiki::Backend::BackendError.new(nil)
|
|
150
|
+
end
|
|
151
|
+
true
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# list projects found in data path
|
|
155
|
+
def list_projects
|
|
156
|
+
Dir[File.join(@data_path, "*")].select do |dd|
|
|
157
|
+
File.directory?(dd) and File.exist?(page_file(@default_page, File.basename(dd)))
|
|
158
|
+
end.map { |dd| File.basename(dd) }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# list topics found in data path
|
|
162
|
+
def list_topics(project)
|
|
163
|
+
pd = project_directory(project)
|
|
164
|
+
raise Ruwiki::Backend::BackendError.new(:no_project) unless File.exist?(pd)
|
|
165
|
+
|
|
166
|
+
Dir[File.join(pd, "*")].select do |ff|
|
|
167
|
+
ff !~ /\.rdiff$/ and ff !~ /\.lock$/ and File.file?(ff) and ff =~ @extension_re
|
|
168
|
+
end.map { |ff| File.basename(ff).sub(@extension_re, "") }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def project_directory(project) # :nodoc:
|
|
172
|
+
File.join(@data_path, project)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def page_file(topic, project = 'Default') # :nodoc:
|
|
176
|
+
if @extension.nil?
|
|
177
|
+
File.join(project_directory(project), topic)
|
|
178
|
+
else
|
|
179
|
+
File.join(project_directory(project), "#{topic}.#{@extension}")
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def make_rdiff(page_file, new_page)
|
|
184
|
+
diff_file = "#{page_file}.rdiff"
|
|
185
|
+
|
|
186
|
+
old_page = self.class.load(pf) rescue Ruwiki::Page::NULL_PAGE
|
|
187
|
+
|
|
188
|
+
diffs = []
|
|
189
|
+
File.open(diff_file, 'rb') { |ff| diffs = Marshal.load(ff) } if File.exists?(diff_file)
|
|
190
|
+
diffs << make_diff(old_page, new_page)
|
|
191
|
+
changes = Marshal.dump(diffs)
|
|
192
|
+
|
|
193
|
+
File.open(diff_file, 'wb') { |ff| ff << changes }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Provides a HEADER marker.
|
|
197
|
+
# Loads the topic page from disk.
|
|
198
|
+
def load(topic, project)
|
|
199
|
+
data = nil
|
|
200
|
+
File.open(page_file(topic, project), 'rb') { |ff| data = ff.read }
|
|
201
|
+
|
|
202
|
+
Ruwiki::Page::NULL_PAGE.merge(@format.load(data))
|
|
203
|
+
rescue Ruwiki::Exportable::InvalidFormatError, TypeError, ArgumentError
|
|
204
|
+
raise Ruwiki::Backend::InvalidFormatError
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Saves the topic page -- and its difference with the previous version
|
|
208
|
+
# -- to disk.
|
|
209
|
+
def store(page)
|
|
210
|
+
pagefile = page_file(page.topic, page.project)
|
|
211
|
+
export = page.export
|
|
212
|
+
newpage = @format.dump(export)
|
|
213
|
+
make_rdiff(pagefile, export)
|
|
214
|
+
|
|
215
|
+
File.open(pagefile, 'wb') { |ff| ff.puts newpage }
|
|
216
|
+
end
|
|
217
|
+
end
|