Soks 0.0.2
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.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
|