Soks 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.txt +60 -0
- data/README.txt +65 -0
- data/bin/soks-create-wiki.rb +41 -0
- data/contrib/diff/lcs.rb +1105 -0
- data/contrib/diff/lcs/array.rb +21 -0
- data/contrib/diff/lcs/block.rb +51 -0
- data/contrib/diff/lcs/callbacks.rb +322 -0
- data/contrib/diff/lcs/change.rb +169 -0
- data/contrib/diff/lcs/hunk.rb +257 -0
- data/contrib/diff/lcs/ldiff.rb +226 -0
- data/contrib/diff/lcs/string.rb +19 -0
- data/contrib/diff_licence.txt +76 -0
- data/contrib/redcloth-2.0.11.rb +894 -0
- data/contrib/redcloth-3.0.1.rb +1019 -0
- data/contrib/redcloth_license.txt +27 -0
- data/lib/authenticators.rb +79 -0
- data/lib/soks-helpers.rb +321 -0
- data/lib/soks-model.rb +208 -0
- data/lib/soks-servlet.rb +125 -0
- data/lib/soks-utils.rb +80 -0
- data/lib/soks-view.rb +424 -0
- data/lib/soks.rb +19 -0
- data/template/attachment/logo.png +0 -0
- data/template/attachment/stylesheet.css +63 -0
- data/template/content/How%20to%20export%20a%20site%20from%20this%20wiki.textile +5 -0
- data/template/content/How%20to%20hack%20soks.textile +60 -0
- data/template/content/How%20to%20import%20a%20site%20from%20instiki.textile +13 -0
- data/template/content/Improving%20the%20style%20of%20this%20wiki.textile +30 -0
- data/template/content/Picture%20of%20a%20pair%20of%20soks.textile +1 -0
- data/template/content/Pointers%20on%20adjusting%20the%20settings.textile +39 -0
- data/template/content/Pointers%20on%20how%20to%20use%20this%20wiki.textile +21 -0
- data/template/content/Recent%20Changes%20to%20This%20Site.textile +203 -0
- data/template/content/Soks%20Licence.textile +64 -0
- data/template/content/home%20page.textile +18 -0
- data/template/start.rb +74 -0
- data/template/views/AttachmentPage_edit.rhtml +36 -0
- data/template/views/ImagePage_edit.rhtml +36 -0
- data/template/views/Page_content.rhtml +1 -0
- data/template/views/Page_edit.rhtml +34 -0
- data/template/views/Page_print.rhtml +5 -0
- data/template/views/Page_revisions.rhtml +18 -0
- data/template/views/Page_rss.rhtml +34 -0
- data/template/views/Page_search_results.rhtml +19 -0
- data/template/views/Page_view.rhtml +3 -0
- data/template/views/frame.rhtml +34 -0
- metadata +88 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
Redcloth License
|
2
|
+
|
3
|
+
Redistribution and use in source and binary forms, with or without
|
4
|
+
modification, are permitted provided that the following conditions are met:
|
5
|
+
|
6
|
+
* Redistributions of source code must retain the above copyright notice,
|
7
|
+
this list of conditions and the following disclaimer.
|
8
|
+
|
9
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
11
|
+
and/or other materials provided with the distribution.
|
12
|
+
|
13
|
+
* Neither the name Textile nor the names of its contributors may be used to
|
14
|
+
endorse or promote products derived from this software without specific
|
15
|
+
prior written permission.
|
16
|
+
|
17
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
18
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
19
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
20
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
21
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
22
|
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
23
|
+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
24
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
25
|
+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
26
|
+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
27
|
+
POSSIBILITY OF SUCH DAMAGE.
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'webrick/config'
|
2
|
+
require 'webrick/httpstatus'
|
3
|
+
require 'webrick/httpauth/authenticator'
|
4
|
+
require 'base64'
|
5
|
+
|
6
|
+
module WEBrick
|
7
|
+
module HTTPAuth
|
8
|
+
class NotAuthentication
|
9
|
+
|
10
|
+
include WEBrick::HTTPAuth::Authenticator
|
11
|
+
|
12
|
+
AuthScheme = "Basic"
|
13
|
+
|
14
|
+
def initialize( realm = "editing" )
|
15
|
+
config = { :UserDB => "nodb" , :Realm => realm }
|
16
|
+
check_init(config)
|
17
|
+
@config = Config::BasicAuth.dup.update(config)
|
18
|
+
end
|
19
|
+
|
20
|
+
def authenticate(req, res)
|
21
|
+
unless basic_credentials = check_scheme(req)
|
22
|
+
challenge(req, res)
|
23
|
+
end
|
24
|
+
userid, password = decode64(basic_credentials).split(":", 2)
|
25
|
+
if userid.empty?
|
26
|
+
error("user id was not given.")
|
27
|
+
challenge(req, res)
|
28
|
+
end
|
29
|
+
info("%s: authentication succeeded.", userid)
|
30
|
+
req.user = userid
|
31
|
+
end
|
32
|
+
|
33
|
+
def challenge(req, res)
|
34
|
+
res[@response_field] = "#{@auth_scheme} realm=\"#{@realm}\""
|
35
|
+
raise @auth_exception
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class OnePasswordAuthentication
|
40
|
+
include Authenticator
|
41
|
+
|
42
|
+
AuthScheme = "Basic"
|
43
|
+
|
44
|
+
attr_reader :realm, :userdb, :logger
|
45
|
+
|
46
|
+
def initialize( password = "", realm = "editing" )
|
47
|
+
config = { :UserDB => "nodb" , :Realm => realm }
|
48
|
+
check_init(config)
|
49
|
+
@config = Config::BasicAuth.dup.update(config)
|
50
|
+
@password = password
|
51
|
+
end
|
52
|
+
|
53
|
+
def authenticate(req, res)
|
54
|
+
unless basic_credentials = check_scheme(req)
|
55
|
+
challenge(req, res)
|
56
|
+
end
|
57
|
+
userid, password = decode64(basic_credentials).split(":", 2)
|
58
|
+
password ||= ""
|
59
|
+
if userid.empty?
|
60
|
+
error("user id was not given.")
|
61
|
+
challenge(req, res)
|
62
|
+
end
|
63
|
+
|
64
|
+
if password != @password
|
65
|
+
error("%s: password unmatch.", userid)
|
66
|
+
challenge(req, res)
|
67
|
+
end
|
68
|
+
info("%s: authentication succeeded.", userid)
|
69
|
+
req.user = userid
|
70
|
+
end
|
71
|
+
|
72
|
+
def challenge(req, res)
|
73
|
+
res[@response_field] = "#{@auth_scheme} realm=\"#{@realm}\""
|
74
|
+
raise @auth_exception
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
data/lib/soks-helpers.rb
ADDED
@@ -0,0 +1,321 @@
|
|
1
|
+
require 'soks'
|
2
|
+
require 'net/smtp'
|
3
|
+
|
4
|
+
class AutomaticUpdateCrossLinks
|
5
|
+
|
6
|
+
def initialize( wiki )
|
7
|
+
@wiki = wiki
|
8
|
+
@wiki.watch_for( :page_created ) { |event, page| new_page( page ) }
|
9
|
+
@wiki.watch_for( :page_deleted ) { |event, page| delete_page( page ) }
|
10
|
+
@wiki.watch_for( :page_revised ) { |event, page| page_revised( page ) }
|
11
|
+
update_all_pages
|
12
|
+
end
|
13
|
+
|
14
|
+
def new_page( page )
|
15
|
+
@wiki.rollingmatch[ page.name ] = page
|
16
|
+
titleregex = Regexp.new( page.name, Regexp::IGNORECASE )
|
17
|
+
@wiki.each { |name, linkedpage |
|
18
|
+
unless name[0,10] == 'Site Index'
|
19
|
+
@wiki.refresh_redcloth( linkedpage ) if linkedpage.textile =~ titleregex
|
20
|
+
end
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def delete_page( page )
|
25
|
+
@wiki.rollingmatch.delete( page.name )
|
26
|
+
page.links_to.each { |linkedpage| @wiki.refresh_redcloth( linkedpage ) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def page_revised( page )
|
30
|
+
page.inserted_into.each { |including_page|
|
31
|
+
@wiki.refresh_redcloth( including_page )
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def update_all_pages
|
36
|
+
@wiki.each { |pagename, page| @wiki.rollingmatch[ page.name ] = page }
|
37
|
+
@wiki.each { |pagename, page| @wiki.refresh_redcloth( page ) }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class AutomaticRecentChanges
|
42
|
+
|
43
|
+
IGNORED_AUTHORS = /^Automatic/
|
44
|
+
|
45
|
+
def initialize( wiki, lines = 200, pagename = "Recent Changes to This Site", author = "AutomaticRecentChanges" )
|
46
|
+
@wiki, @lines, @pagename, @author = wiki, lines, pagename, author
|
47
|
+
@wiki.watch_for( :page_revised ) { |event, page, revision| revised( page, revision ) }
|
48
|
+
end
|
49
|
+
|
50
|
+
def revised( page, revision )
|
51
|
+
unless ( revision.author =~ IGNORED_AUTHORS )
|
52
|
+
add_recent_change( page, revision )
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_recent_change( page, revision )
|
57
|
+
changes_content = "On #{revision.created_at.strftime('%Y %b %d')}, #{revision.author} made these changes to [[#{page}]]:\n"
|
58
|
+
revision.changes.each do |change_group| change_group.each do |change|
|
59
|
+
next if change[2] == ""
|
60
|
+
changes_content << case change[0]
|
61
|
+
# Note, +/- reversed because diffs stored as diff from CURRENT version
|
62
|
+
when "-" ; "| #{change[1]} | -<notextile>#{change[2]}</notextile>- |\n"
|
63
|
+
when "+" ; "| #{change[1]} | <notextile>#{change[2]}</notextile> |\n"
|
64
|
+
end
|
65
|
+
end end
|
66
|
+
changes_content << "\n"
|
67
|
+
changes_content << @wiki.page( @pagename ).content.first_lines( @lines ) unless @wiki.page( @pagename ).empty?
|
68
|
+
@wiki.revise( @pagename , changes_content, @author)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
class AutomaticOnePageIndex
|
73
|
+
|
74
|
+
def initialize( wiki, pagename = "Site Index", author = "AutomaticIndex" )
|
75
|
+
@wiki, @pagename, @author = wiki, pagename, author
|
76
|
+
@wiki.watch_for( :page_created, :page_deleted ) { update_index }
|
77
|
+
update_index
|
78
|
+
end
|
79
|
+
|
80
|
+
def update_index
|
81
|
+
index = @wiki.sort_by{ |name, page| page.name_for_index.capitalize }.map{ |name, page| "* [[ #{page} ]]\n" }
|
82
|
+
@wiki.revise( @pagename, "h1. Index\n\n#{index}", @author )
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class AutomaticMultiPageIndex < AutomaticOnePageIndex
|
87
|
+
|
88
|
+
def update_index
|
89
|
+
buckets = separate_pages_by_first_character
|
90
|
+
link_bar = link_bar_from_buckets( buckets )
|
91
|
+
buckets.each do |firstcharacter, pages|
|
92
|
+
index = pages.sort_by{ |page| page.name_for_index.capitalize }.map{ |page| "* [[ #{page} ]]\n" }
|
93
|
+
@wiki.revise( "#{@pagename} #{firstcharacter}", "h1. Site Index #{firstcharacter}\n\n#{link_bar}\n\n#{index}", @author )
|
94
|
+
end
|
95
|
+
@wiki.revise( @pagename, "h1. Site Index\n\n#{link_bar}", @author )
|
96
|
+
end
|
97
|
+
|
98
|
+
def separate_pages_by_first_character
|
99
|
+
buckets = Hash.new
|
100
|
+
@wiki.each do |name, page|
|
101
|
+
( buckets[ page.name_for_index[0,1].upcase ] ||= [] ) << page
|
102
|
+
end
|
103
|
+
buckets
|
104
|
+
end
|
105
|
+
|
106
|
+
def link_bar_from_buckets( buckets )
|
107
|
+
buckets.keys.sort.map { |letter| "[[ #{letter} : #{@pagename} #{letter} ]]\n" }.join(' ')
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class FiniteUniqueList
|
112
|
+
include Enumerable
|
113
|
+
|
114
|
+
attr_accessor :max_size
|
115
|
+
|
116
|
+
def initialize( max_size = nil )
|
117
|
+
@max_size = max_size
|
118
|
+
@list = Array.new
|
119
|
+
end
|
120
|
+
|
121
|
+
def add( item )
|
122
|
+
remove( item )
|
123
|
+
@list << item
|
124
|
+
remove_excess_items
|
125
|
+
end
|
126
|
+
|
127
|
+
def remove( item )
|
128
|
+
@list.delete( item )
|
129
|
+
end
|
130
|
+
|
131
|
+
def each
|
132
|
+
@list.reverse_each { |item| yield item }
|
133
|
+
end
|
134
|
+
|
135
|
+
def empty?; @list.empty? end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def remove_excess_items
|
140
|
+
if @max_size
|
141
|
+
while @list.size > @max_size
|
142
|
+
@list.shift
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
class AutomaticSummary
|
149
|
+
|
150
|
+
DEFAULT_SETTINGS = {
|
151
|
+
:regexp_for_pages => /.*/,
|
152
|
+
:max_pages_to_show => nil,
|
153
|
+
:pagename => 'Summary',
|
154
|
+
:author => 'AutomaticSummary',
|
155
|
+
:lines_to_include => nil,
|
156
|
+
:only_new_pages => false,
|
157
|
+
}
|
158
|
+
|
159
|
+
attr_reader :settings, :pages_in_summary
|
160
|
+
|
161
|
+
def initialize( wiki, settings = {} )
|
162
|
+
@wiki = wiki
|
163
|
+
@settings = DEFAULT_SETTINGS.merge( settings )
|
164
|
+
@pages_in_summary = FiniteUniqueList.new( @settings[:max_pages_to_show] )
|
165
|
+
scan_for_pages_to_summarise
|
166
|
+
render_summary
|
167
|
+
@wiki.watch_for( @settings[:only_new_pages] ? :page_created : :page_revised ) { |event, page| check_whether_to_add_page( page ) }
|
168
|
+
@wiki.watch_for( :page_deleted ) { |event, page| check_whether_to_remove_page( page ) }
|
169
|
+
end
|
170
|
+
|
171
|
+
def check_whether_to_add_page( page )
|
172
|
+
return if page.name == @settings[:pagename]
|
173
|
+
if page.name=~ @settings[:regexp_for_pages]
|
174
|
+
@pages_in_summary.add( page )
|
175
|
+
render_summary
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def check_whether_to_remove_page( page )
|
180
|
+
render_summary if @pages_in_summary.remove( page )
|
181
|
+
end
|
182
|
+
|
183
|
+
def scan_for_pages_to_summarise
|
184
|
+
@wiki.select do |name,page|
|
185
|
+
name=~ @settings[:regexp_for_pages]
|
186
|
+
end.sort_by do |name, page|
|
187
|
+
(@settings[:only_new_pages] ? page.revision[0] : page).created_at
|
188
|
+
end.each do |name, page|
|
189
|
+
@pages_in_summary.add page
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def render_summary
|
194
|
+
if @pages_in_summary.empty?
|
195
|
+
@wiki.revise( @settings[:pagename], "No pages found to summarise" , @settings[:author] )
|
196
|
+
else
|
197
|
+
content = ""
|
198
|
+
@pages_in_summary.each do |page|
|
199
|
+
content << "<div class='subpage'>\n\n"
|
200
|
+
content << render_page( page )
|
201
|
+
content << "\n\np(more). [[(more) : #{page.name}]]\n\n"
|
202
|
+
content << "</div>\n"
|
203
|
+
end
|
204
|
+
@wiki.revise( @settings[:pagename], content , @settings[:author] )
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def render_page( page )
|
209
|
+
if @settings[:lines_to_include]
|
210
|
+
page.textile.first_lines( @settings[:lines_to_include] )
|
211
|
+
else
|
212
|
+
page.textile
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
class AutomaticCalendar
|
218
|
+
|
219
|
+
attr_reader :month_pagename, :day_pagename
|
220
|
+
|
221
|
+
def initialize( wiki, month_pagename = '%Y %b', day_pagename = '%Y %b %d', author = "AutomaticCalendar" )
|
222
|
+
@wiki, @month_pagename, @day_pagename, @author = wiki, month_pagename, day_pagename, author
|
223
|
+
Thread.new do
|
224
|
+
loop do
|
225
|
+
Time.now.month.upto( Time.now.month+12 ) { |m| render_month( m ) }
|
226
|
+
sleep(60*60*24)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def render_month( month )
|
232
|
+
@wiki.revise( month_pagename( month ), calendar_for( month ) , @author ) unless @wiki.exists?( month_pagename( month ) )
|
233
|
+
end
|
234
|
+
|
235
|
+
def calendar_for( month )
|
236
|
+
content = "<div class='calendar'>\n"
|
237
|
+
content << "|_. Su |_. Mo |_. Tu |_. We |_. Th |_. Fr |_. Sa |\n"
|
238
|
+
1.upto( time_for( month, 1 ).wday ) { content << "| . " }
|
239
|
+
day = nil
|
240
|
+
1.upto( 31 ) do |day_no|
|
241
|
+
day = time_for( month, day_no )
|
242
|
+
break if day.month > month
|
243
|
+
content << "| [[ #{day_no} : #{day_pagename( day )} ]] "
|
244
|
+
content << "|\n" if day.wday == 6
|
245
|
+
end
|
246
|
+
day.wday.upto( 5 ) { content << "| . " }
|
247
|
+
content << "|\n"
|
248
|
+
content << "\n\n< #{month_pagename( month-1 )} #{month_pagename( month+1 )} >\n"
|
249
|
+
end
|
250
|
+
|
251
|
+
def time_for( month, day = 1 )
|
252
|
+
year = Time.now.year
|
253
|
+
if month > 12
|
254
|
+
year +=1
|
255
|
+
month -= 12
|
256
|
+
elsif month < 1
|
257
|
+
year -= 1
|
258
|
+
month = month + 12
|
259
|
+
end
|
260
|
+
if day > 31
|
261
|
+
month += 1
|
262
|
+
day -= 31
|
263
|
+
end
|
264
|
+
Time.local( year, month, day, 8, 0 )
|
265
|
+
end
|
266
|
+
|
267
|
+
def month_pagename( month = Time.now.month ) time_for( month ).strftime(@month_pagename) end
|
268
|
+
|
269
|
+
def day_pagename( date = Time.now ) date.strftime(@day_pagename) end
|
270
|
+
end
|
271
|
+
|
272
|
+
class AutomaticUpcomingEvents
|
273
|
+
|
274
|
+
def initialize( wiki, calendar, days_passed = 0, days_future = 7, pagename = 'Upcoming Events', author = "AutomaticUpcomingEvents" )
|
275
|
+
@wiki, @calendar, @days_passed, @days__future, @pagename, @author = wiki, calendar, days_passed, days_future, pagename, author
|
276
|
+
@wiki.watch_for( :page_revised ) { |event, page| page_revised( page ) }
|
277
|
+
Thread.new do
|
278
|
+
loop do
|
279
|
+
render_upcoming_events
|
280
|
+
sleep(60*60)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def page_revised( page )
|
286
|
+
render_upcoming_events if page.name =~ /^\d\d\d\d ... \d\d/
|
287
|
+
end
|
288
|
+
|
289
|
+
def render_upcoming_events
|
290
|
+
content = "<div class='upcomingevents'>\n"
|
291
|
+
Time.now.day.upto( Time.now.day+7 ) do |day|
|
292
|
+
time = @calendar.time_for( Time.now.month, day )
|
293
|
+
content << "| [[ #{relative_day( time ) } : #{@calendar.day_pagename( time )} ]] |"
|
294
|
+
content << (@wiki.exists?( @calendar.day_pagename(time) ) ? render_event( @calendar.day_pagename( time ) ) : " |\n")
|
295
|
+
end
|
296
|
+
content << "\n\np(more). [[(more) : #{@calendar.month_pagename}]]\n\n"
|
297
|
+
content << "</div>\n"
|
298
|
+
@wiki.revise( "Upcoming Events", content , "AutomaticCalendar" )
|
299
|
+
end
|
300
|
+
|
301
|
+
def relative_day( date )
|
302
|
+
case ( (date - @calendar.time_for( Time.now.month, Time.now.day ) ) / ( 60 * 60 * 24 ) ) # Days difference
|
303
|
+
when -7..-2 ; date.strftime('Last %A')
|
304
|
+
when -1 ; "Yesterday"
|
305
|
+
when 0 ; "Today"
|
306
|
+
when 1 ; "Tomorrow"
|
307
|
+
when 2..7 ; date.strftime('%A')
|
308
|
+
else ; date.strftime('%A %d')
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
def render_event( name )
|
313
|
+
page = @wiki.page( name )
|
314
|
+
headings = page.textile.select { |line| line =~ /^h\d\./ }
|
315
|
+
headings = headings.map { |heading| heading.to_s[4..-1].strip }
|
316
|
+
headings = [ page.textile[/.*?\n/] ] if headings.empty?
|
317
|
+
content = "[[ #{headings.shift} : #{page.name} ]] |\n"
|
318
|
+
headings.each { |heading| content << "| | [[ #{heading} : #{page.name} ]] |\n" }
|
319
|
+
content
|
320
|
+
end
|
321
|
+
end
|
data/lib/soks-model.rb
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
# Revision stores changes as a diff against the more recent content version
|
2
|
+
class Revision
|
3
|
+
attr_reader :number, :changes, :created_at, :author
|
4
|
+
|
5
|
+
def initialize( number, changes, author )
|
6
|
+
@number, @changes, @author = number, changes, author
|
7
|
+
@created_at = Time.now
|
8
|
+
end
|
9
|
+
|
10
|
+
# Recreates the content of the page when this revision was created, by recursively applying diffs
|
11
|
+
# to more recent versions.
|
12
|
+
def content( page )
|
13
|
+
$stderr.puts "#{number} #{page}"
|
14
|
+
page.revision( @number + 1 ) ? page.revision( @number + 1 ).previous_content( page ) : page.content
|
15
|
+
end
|
16
|
+
|
17
|
+
def previous_content( page )
|
18
|
+
content( page ).split("\n").unpatch!(@changes).join("\n")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Page
|
23
|
+
attr_reader :name, :content, :revisions
|
24
|
+
attr_accessor :links_from, :links_to, :inserted_into
|
25
|
+
|
26
|
+
# Returns an empty version of itself.
|
27
|
+
def self.empty( name )
|
28
|
+
empty = self.new( name, "Type what you want here and click save", "NoOne" )
|
29
|
+
class << empty
|
30
|
+
def textile; "[[Create #{name} (#{self.class}) : /edit/#{name} ]]" end
|
31
|
+
def empty?; true; end
|
32
|
+
end
|
33
|
+
empty
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize( name, content, author )
|
37
|
+
@name, @content, @revisions = name, "", []
|
38
|
+
@links_from, @links_to = [], []
|
39
|
+
@inserted_into, @watchers = [], []
|
40
|
+
revise content, author
|
41
|
+
end
|
42
|
+
|
43
|
+
alias :textile :content
|
44
|
+
alias :name_for_index :name
|
45
|
+
alias :to_s :name
|
46
|
+
|
47
|
+
# Revises the content of this page, creating a new revision class that stores the changes
|
48
|
+
def revise( content, author )
|
49
|
+
changes = @content.split("\n").diff( content.split("\n") ).map { |changeset| changeset.map { |change| change.to_a } }
|
50
|
+
unless changes.empty?
|
51
|
+
@revisions << Revision.new( @revisions.length, changes, author )
|
52
|
+
@content = content
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns the content of this page to that of a previous version
|
57
|
+
def rollback( number, author )
|
58
|
+
if number < 0
|
59
|
+
revise( "page deleted", author )
|
60
|
+
else
|
61
|
+
revise( @revisions[ number ].content( self ), author )
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def revision( number ) @revisions[ number ] end
|
66
|
+
|
67
|
+
def deleted?
|
68
|
+
( content.strip.downcase == 'page deleted' ) ||
|
69
|
+
( content =~ /^content moved to /i )
|
70
|
+
end
|
71
|
+
|
72
|
+
def empty?; @revisions.empty? end
|
73
|
+
|
74
|
+
def score; @links_from.size + @links_to.size + @watchers.size end
|
75
|
+
|
76
|
+
def <=>( otherpage ) self.score <=> otherpage.score end
|
77
|
+
|
78
|
+
def watch( person ) ( @watchers << person ).uniq! end
|
79
|
+
def watching?( person ) @watchers.include? person end
|
80
|
+
def unwatch( person ) @watchers.delete( person ) end
|
81
|
+
|
82
|
+
def is_inserted_into( page ) (@inserted_into << page).uniq! end
|
83
|
+
|
84
|
+
# Any unhandled calls are passed onto the latest revision (e.g. author, creation time etc)
|
85
|
+
def method_missing(method_symbol, *args)
|
86
|
+
@revisions.last.send( method_symbol, *args )
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
#Serves as a marker, so ImagePage and AttachmentPage can re-use the same view templates
|
91
|
+
class UploadPage < Page
|
92
|
+
end
|
93
|
+
|
94
|
+
class ImagePage < UploadPage
|
95
|
+
def textile()
|
96
|
+
deleted? ? content : "!#{content}(#{@name})!:/#{@name.url_encode}\n"
|
97
|
+
end
|
98
|
+
|
99
|
+
def name_for_index; @name[ 10..-1].strip end
|
100
|
+
end
|
101
|
+
|
102
|
+
class AttachmentPage < UploadPage
|
103
|
+
def textile()
|
104
|
+
deleted? ? content : %Q{"#{@name}":#{content}\n}
|
105
|
+
end
|
106
|
+
def name_for_index; @name[ 9..-1].strip end
|
107
|
+
end
|
108
|
+
|
109
|
+
class Wiki
|
110
|
+
include Enumerable
|
111
|
+
|
112
|
+
def initialize( folder )
|
113
|
+
@folder = folder
|
114
|
+
@pages = Hash.new
|
115
|
+
@page_classes = [
|
116
|
+
[ /^picture of/i, ImagePage ],
|
117
|
+
[ /^attached/i, AttachmentPage ],
|
118
|
+
[ /.*/, Page ]
|
119
|
+
]
|
120
|
+
load_all_pages_in( folder )
|
121
|
+
end
|
122
|
+
|
123
|
+
def page( name )
|
124
|
+
@pages[ name.downcase ] || class_for( name ).empty( name )
|
125
|
+
end
|
126
|
+
|
127
|
+
def each
|
128
|
+
@pages.each { |name, page| yield [name, page] unless page.deleted? }
|
129
|
+
end
|
130
|
+
|
131
|
+
def exists?( name )
|
132
|
+
@pages.include?( name.downcase ) && !page( name.downcase ).deleted?
|
133
|
+
end
|
134
|
+
|
135
|
+
def revise( pagename, content, author )
|
136
|
+
mutate( pagename, content, author ) { |page| page.revise( content, author ) }
|
137
|
+
end
|
138
|
+
|
139
|
+
def rollback( pagename, number, author )
|
140
|
+
mutate( pagename ) { |page| page.rollback( number, author ) }
|
141
|
+
end
|
142
|
+
|
143
|
+
def watch( pagename, person )
|
144
|
+
mutate( pagename ) { |page| page.watch( person ) }
|
145
|
+
end
|
146
|
+
|
147
|
+
def unwatch( pagename, person )
|
148
|
+
mutate( pagename ) { |page| page.unwatch( person ) }
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def mutate( pagename, content = nil, author = nil )
|
154
|
+
page = @pages[ pagename.downcase ]
|
155
|
+
if page
|
156
|
+
yield page
|
157
|
+
save_page page
|
158
|
+
elsif content && author
|
159
|
+
@pages[ pagename.downcase ] = page = class_for( pagename ).new( pagename, content, author)
|
160
|
+
save_page page
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def load_all_pages_in( folder )
|
165
|
+
Dir.foreach( folder ) do |filename|
|
166
|
+
$stderr.puts "Loading #{filename}"
|
167
|
+
load_page( filename ) if filename =~ /\.textile$/
|
168
|
+
end
|
169
|
+
$stderr.puts "All loaded"
|
170
|
+
end
|
171
|
+
|
172
|
+
def load_page( filename )
|
173
|
+
if File.exists? File.join( @folder, filename )
|
174
|
+
page_name = File.basename( filename, '.*').url_decode
|
175
|
+
content = IO.readlines( File.join( @folder, filename ) ).join
|
176
|
+
revisions = load_revisions_for( filename )
|
177
|
+
|
178
|
+
page = class_for( page_name ).new( page_name, content, 'imported' )
|
179
|
+
page.revisions.replace( revisions ) if revisions
|
180
|
+
@pages[ page.name.downcase ] = page
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def load_revisions_for( page_filename )
|
185
|
+
revision_filename = "#{File.basename( page_filename, '.*')}.marshal"
|
186
|
+
if File.exists? File.join( @folder, revision_filename )
|
187
|
+
File.open( File.join( @folder, revision_filename ) ) do |file|
|
188
|
+
return Marshal.load( file )
|
189
|
+
end
|
190
|
+
end
|
191
|
+
return nil
|
192
|
+
end
|
193
|
+
|
194
|
+
def save_page( page )
|
195
|
+
File.open( "#{filename_for( page )}.textile", 'w' ) { |file| file.puts page.content }
|
196
|
+
File.open( "#{filename_for( page )}.marshal", 'w' ) { |file| Marshal.dump( page.revisions, file ) }
|
197
|
+
end
|
198
|
+
|
199
|
+
def filename_for( page )
|
200
|
+
File.join( @folder, "#{page.name.url_encode}" )
|
201
|
+
end
|
202
|
+
|
203
|
+
def class_for( name )
|
204
|
+
@page_classes.each do |regex,klass|
|
205
|
+
return klass if name =~ regex
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|