gf-Soks 1.0.4
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 +66 -0
- data/README.txt +64 -0
- data/bin/soks-create-wiki.rb +193 -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/easyprompt.rb +58 -0
- data/contrib/easyprompt_licence.txt +504 -0
- data/contrib/redcloth-3.0.3.rb +1113 -0
- data/contrib/redcloth_license.txt +27 -0
- data/lib/authenticators.rb +121 -0
- data/lib/helpers/counter-helpers.rb +132 -0
- data/lib/helpers/default-helpers.rb +416 -0
- data/lib/helpers/mail2wiki-helper.rb +105 -0
- data/lib/helpers/maintenance-helpers.rb +149 -0
- data/lib/helpers/rss2wiki-helper.rb +47 -0
- data/lib/helpers/wiki2html.rb +60 -0
- data/lib/soks-model.rb +271 -0
- data/lib/soks-servlet.rb +177 -0
- data/lib/soks-storage.rb +187 -0
- data/lib/soks-upgrade-0.0.2.rb +70 -0
- data/lib/soks-utils.rb +327 -0
- data/lib/soks-view.rb +399 -0
- data/lib/soks.rb +27 -0
- data/rakefile +109 -0
- data/templates/default/attachment/favicon.ico +0 -0
- data/templates/default/attachment/logo.jpg +0 -0
- data/templates/default/attachment/logo.png +0 -0
- data/templates/default/attachment/logo.tiff +0 -0
- data/templates/default/attachment/newpage.js +41 -0
- data/templates/default/attachment/print_stylesheet.css +2 -0
- data/templates/default/attachment/robots.txt +6 -0
- data/templates/default/attachment/rss.png +0 -0
- data/templates/default/attachment/stylesheet.css +219 -0
- data/templates/default/banned_titles.txt +67 -0
- data/templates/default/caches/readme.txt +1 -0
- data/templates/default/content/Api%20for%20classes%20to%20modify%20the%20wiki.textile +30 -0
- data/templates/default/content/Author.textile +16 -0
- data/templates/default/content/Automatic%20Summaries.textile +40 -0
- data/templates/default/content/Automatic%20counters.textile +22 -0
- data/templates/default/content/Automatic%20exporters.textile +23 -0
- data/templates/default/content/Automatic%20importers.textile +59 -0
- data/templates/default/content/Automatic%20linking.textile +7 -0
- data/templates/default/content/Automatic%20maintenance%20helpers.textile +39 -0
- data/templates/default/content/Bug%3A%20Competing%20edits.textile +22 -0
- data/templates/default/content/Bug%3A%20Does%20not%20make%20use%20of%20if%2Dmodified%2Dsince%20r.textile +3 -0
- data/templates/default/content/Bug%3A%20Email%20adresses%20in%20page%20titles%20cause%20incorrec.textile +3 -0
- data/templates/default/content/Bug%3A%20GEM%20limits%20title%20lengths.textile +3 -0
- data/templates/default/content/Bug%3A%20Memory%20leak.textile +13 -0
- data/templates/default/content/Bug%3A%20Page%2Einserted%5Finto%20is%20never%20purged.textile +17 -0
- data/templates/default/content/Bug%3A%20Pages%20that%20link%20here%20may%20not%20appear%20on%20r.textile +13 -0
- data/templates/default/content/Bug%3A%20Textile%20mishandles%20paragraphs.textile +37 -0
- data/templates/default/content/Bug%3A%20Unanticipated%20Rollbacks.textile +23 -0
- data/templates/default/content/Bug%3A%20notextile%20does%20not%20prevent%20page%20inserts.textile +3 -0
- data/templates/default/content/Home%20Page.textile +22 -0
- data/templates/default/content/How%20to%20administrate%20this%20wiki.textile +57 -0
- data/templates/default/content/How%20to%20change%20the%20way%20this%20wiki%20looks.textile +32 -0
- data/templates/default/content/How%20to%20export%20a%20site%20from%20this%20wiki.textile +82 -0
- data/templates/default/content/How%20to%20get%20the%20latest%20Soks%20from%20cvs.textile +45 -0
- data/templates/default/content/How%20to%20hack%20soks.textile +66 -0
- data/templates/default/content/How%20to%20import%20a%20site%20from%20instiki.textile +15 -0
- data/templates/default/content/How%20to%20import%20data.textile +41 -0
- data/templates/default/content/How%20to%20install%20Soks.textile +33 -0
- data/templates/default/content/How%20to%20password%20protect%20your%20wiki.textile +53 -0
- data/templates/default/content/How%20to%20re%2Dbuild%20the%20page%20cache.textile +71 -0
- data/templates/default/content/How%20to%20report%20a%20bug.textile +9 -0
- data/templates/default/content/How%20to%20upgrade%20soks.textile +32 -0
- data/templates/default/content/How%20to%20use%20the%20Automatic%20Helper%20classes.textile +12 -0
- data/templates/default/content/How%20to%20use%20this%20wiki.textile +30 -0
- data/templates/default/content/List%20of%20changes.textile +10 -0
- data/templates/default/content/News%3A%20Version%201%2D0%2D0%20released.textile +19 -0
- data/templates/default/content/News%3A%20Version%201%2D0%2D1%20released.textile +12 -0
- data/templates/default/content/Pages%20to%20include%20in%20the%20distribution.textile +55 -0
- data/templates/default/content/Per%20Wiki%20Templates.textile +37 -0
- data/templates/default/content/Picture%20of%20a%20pair%20of%20soks.textile +1 -0
- data/templates/default/content/Planned%20Features.textile +74 -0
- data/templates/default/content/README.textile +64 -0
- data/templates/default/content/RSS%20feed.textile +9 -0
- data/templates/default/content/Recent%20changes%20to%20this%20site.textile +352 -0
- data/templates/default/content/SOKS%20features.textile +19 -0
- data/templates/default/content/Sidebar%20Page.textile +6 -0
- data/templates/default/content/Site%20Index.textile +241 -0
- data/templates/default/content/Soks%27s%20Licence.textile +66 -0
- data/templates/default/content/Tag%3A%20Include%20this%20page%20in%20the%20distribution.textile +6 -0
- data/templates/default/start.rb +90 -0
- data/templates/default/version.txt +1 -0
- data/templates/default/views/Page_content.rhtml +1 -0
- data/templates/default/views/Page_edit.rhtml +79 -0
- data/templates/default/views/Page_find.rhtml +35 -0
- data/templates/default/views/Page_linksfromrss.rhtml +24 -0
- data/templates/default/views/Page_listrss.rhtml +46 -0
- data/templates/default/views/Page_meta.rhtml +44 -0
- data/templates/default/views/Page_print.rhtml +6 -0
- data/templates/default/views/Page_revision.rhtml +39 -0
- data/templates/default/views/Page_revisions.rhtml +36 -0
- data/templates/default/views/Page_rss.rhtml +57 -0
- data/templates/default/views/Page_view.rhtml +8 -0
- data/templates/default/views/UploadPage_edit.rhtml +63 -0
- data/templates/default/views/frame.rhtml +63 -0
- data/templates/default/views/messages.yaml +7 -0
- data/test/html/2006Mar.html +66 -0
- data/test/html/poignant.html +36 -0
- data/test/html/poignant.textile +36 -0
- data/test/mock-objects.rb +69 -0
- data/test/test_counter-helper.rb +162 -0
- data/test/test_soks-helper-maintenance.rb +106 -0
- data/test/test_soks-helpers.rb +145 -0
- data/test/test_soks-model.rb +144 -0
- data/test/test_soks-servlet.rb +240 -0
- data/test/test_soks-storage.rb +108 -0
- data/test/test_soks-utils.rb +226 -0
- data/test/test_soks-view.rb +193 -0
- data/test/test_soks.rb +9 -0
- metadata +182 -0
data/lib/soks-servlet.rb
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/local/bin/ruby
|
|
2
|
+
require 'authenticators'
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
class ServletSettings
|
|
6
|
+
|
|
7
|
+
attr_accessor :view_controller, :wiki
|
|
8
|
+
attr_accessor :home_page
|
|
9
|
+
attr_accessor :default_view
|
|
10
|
+
attr_accessor :upload_directory
|
|
11
|
+
attr_accessor :authenticators
|
|
12
|
+
attr_accessor :static_file_directories
|
|
13
|
+
attr_accessor :force_no_cache
|
|
14
|
+
attr_accessor :content_types
|
|
15
|
+
attr_accessor :wiki_directory
|
|
16
|
+
|
|
17
|
+
def initialize( wiki, view_controller )
|
|
18
|
+
@wiki, @view_controller = wiki, view_controller
|
|
19
|
+
@home_page = 'Home Page'
|
|
20
|
+
@default_view = 'view'
|
|
21
|
+
@authenticators = []
|
|
22
|
+
@static_file_directories = {}
|
|
23
|
+
@content_types = {}
|
|
24
|
+
@force_no_cache = false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def url
|
|
28
|
+
@view_controller.root_url
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def page_name_for_url_name( url_name )
|
|
32
|
+
@view_controller.page_name_for_url_name( url_name )
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def url_name_for_page_name( page_name )
|
|
36
|
+
@view_controller.url_name_for_page_name( page_name )
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class WikiServlet < WEBrick::HTTPServlet::AbstractServlet
|
|
41
|
+
|
|
42
|
+
attr_accessor :server, :settings
|
|
43
|
+
|
|
44
|
+
def initialize( server, servlet_settings )
|
|
45
|
+
@server, @settings = server, servlet_settings
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def service( request, response )
|
|
49
|
+
case request.path_info
|
|
50
|
+
|
|
51
|
+
# Pass some requests directly to static files
|
|
52
|
+
when '/robots.txt', '/favicon.ico'; serveStaticFile( request, response, request.path[1..-1], 'Attachment' )
|
|
53
|
+
|
|
54
|
+
# If request of the form /verb/pagename then do it
|
|
55
|
+
when /\/(\w+?)\/(.+)/; wiki_service( request, response, $1.capitalize, $2 )
|
|
56
|
+
|
|
57
|
+
# If request of the form /pagename then redirect to /view/pagename
|
|
58
|
+
when /\/(.+)/ ; response.set_redirect( WEBrick::HTTPStatus::Found, "#{settings.url}/#{settings.default_view}/#{$1}" )
|
|
59
|
+
|
|
60
|
+
# If request of the form / then redirect to /view/home%20page
|
|
61
|
+
when "/" ; redirect( response, settings.home_page, settings.default_view )
|
|
62
|
+
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def wiki_service( request, response, verb, url_name )
|
|
67
|
+
authenticate request, response
|
|
68
|
+
make_username_valid( request )
|
|
69
|
+
if settings.static_file_directories.include? verb
|
|
70
|
+
serveStaticFile( request, response, url_name, verb )
|
|
71
|
+
elsif self.respond_to?( "do#{verb}" )
|
|
72
|
+
self.send( "do#{verb}", request, response,settings.page_name_for_url_name(url_name), request.user )
|
|
73
|
+
set_cache_settings(response)
|
|
74
|
+
else
|
|
75
|
+
renderView( request, response, settings.page_name_for_url_name(url_name), verb, request.user )
|
|
76
|
+
set_cache_settings(response)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def authenticate( request, response )
|
|
81
|
+
settings.authenticators.each do |path_regex,authenticator|
|
|
82
|
+
if request.path_info.downcase =~ path_regex
|
|
83
|
+
authenticator.authenticate( request, response )
|
|
84
|
+
break
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# A special redirect to allow WikiLink style urls
|
|
90
|
+
# Not sure if used by anyone, so may delete
|
|
91
|
+
def doWiki( request, response, pagename, person )
|
|
92
|
+
redirect( response, pagename.gsub(/([a-z])([A-Z])/,'\1 \2') )
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# This passes any requests for static files onto a FileHandler
|
|
96
|
+
def serveStaticFile( request, response, url_name, view )
|
|
97
|
+
request.script_name = view
|
|
98
|
+
request.path_info = "/#{url_name}"
|
|
99
|
+
WEBrick::HTTPServlet::FileHandler.get_instance(@server, settings.static_file_directories[view], true).service(request, response)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# This passes any rendering of the page onto the view class
|
|
103
|
+
def renderView( request, response, pagename, view, person )
|
|
104
|
+
response.body = view_controller.render( pagename, view, person, request.query )
|
|
105
|
+
response['Content-Type'] = settings.content_types[view] || 'text/html'
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# All the following methods change the wiki, then redirect
|
|
109
|
+
|
|
110
|
+
def doSave( request, response, pagename, person )
|
|
111
|
+
pagename = move_page_as_required( request, response, pagename, person )
|
|
112
|
+
content = request.query["content"].to_s.gsub(/\r\n/,"\n")
|
|
113
|
+
wiki.revise( pagename, content, person ) if content
|
|
114
|
+
redirect( response, pagename )
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def doRollback( request, response, pagename, person )
|
|
118
|
+
if request.query['revision']
|
|
119
|
+
wiki.rollback( pagename, request.query['revision'].to_i, person )
|
|
120
|
+
end
|
|
121
|
+
redirect( response, pagename )
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def doDelete( request, response, pagename, person )
|
|
125
|
+
wiki.delete( pagename, person )
|
|
126
|
+
redirect( response, pagename )
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def doUpload( request, response, pagename, person )
|
|
130
|
+
pagename = move_page_as_required( request, response, pagename, person )
|
|
131
|
+
unless request.query['file'] == ""
|
|
132
|
+
filename = upload_file_data( request.query['file'] )
|
|
133
|
+
wiki.revise( pagename, filename, person )
|
|
134
|
+
end
|
|
135
|
+
redirect( response, pagename )
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
def redirect( response, pagename, verb = settings.default_view )
|
|
141
|
+
response.set_redirect( WEBrick::HTTPStatus::Found, "#{settings.url}/#{verb}/#{settings.url_name_for_page_name(pagename)}" )
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Moves a page if there is a newtitle in the query
|
|
145
|
+
# If the original page had 'type a title' in its tile, then it is assumed to be a template
|
|
146
|
+
# and therefore is not moved.
|
|
147
|
+
def move_page_as_required( request, response, pagename, person )
|
|
148
|
+
new_pagename = "#{request.query["titleprefix"]}#{request.query["newtitle"]}"
|
|
149
|
+
return new_pagename if pagename =~ /#{$MESSAGES[:Type_a_title_here]}/io
|
|
150
|
+
return pagename if new_pagename == pagename
|
|
151
|
+
wiki.move( pagename, new_pagename, person )
|
|
152
|
+
new_pagename
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def upload_file_data( upload_data, destination = settings.upload_directory )
|
|
156
|
+
return "Uploads prohibited" unless destination
|
|
157
|
+
path = settings.static_file_directories[ destination ]
|
|
158
|
+
filename = File.unique_filename( path , upload_data.filename )
|
|
159
|
+
File.open( File.join( path, filename ), 'wb' ) { |file| upload_data.list.each { |data| file << data } }
|
|
160
|
+
"/#{destination}/#{filename}"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Make sure the username doesn't start with Automatic
|
|
164
|
+
def make_username_valid( request )
|
|
165
|
+
request.user = "User: #{request.user}" if request.user =~ /^Automatic/i
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def set_cache_settings(response)
|
|
169
|
+
return unless @settings.force_no_cache
|
|
170
|
+
response['Cache-control'] ||= 'no-cache'
|
|
171
|
+
response['Pragma'] ||= 'no-cache'
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def wiki; @settings.wiki end
|
|
175
|
+
def view_controller; @settings.view_controller end
|
|
176
|
+
|
|
177
|
+
end
|
data/lib/soks-storage.rb
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
module WikiCacheStore
|
|
2
|
+
|
|
3
|
+
CACHE_EXTENSION = ".marshal"
|
|
4
|
+
|
|
5
|
+
def load_cache( cache_name )
|
|
6
|
+
return nil unless @cache_folder
|
|
7
|
+
|
|
8
|
+
cache = nil
|
|
9
|
+
|
|
10
|
+
File.open( cache_filename_for( cache_name ) ) do |f|
|
|
11
|
+
cache = Marshal.load(f)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
File.delete( cache_filename_for( cache_name ) )
|
|
15
|
+
|
|
16
|
+
$LOG.info "Loaded #{cache_name} cache"
|
|
17
|
+
|
|
18
|
+
return cache
|
|
19
|
+
|
|
20
|
+
rescue ArgumentError
|
|
21
|
+
$LOG.warn "#{cache_name} cache corrupt (bad characters in file)"
|
|
22
|
+
return nil
|
|
23
|
+
|
|
24
|
+
rescue EOFError
|
|
25
|
+
$LOG.warn "#{cache_name} cache corrupt (unexpected end of file)"
|
|
26
|
+
return nil
|
|
27
|
+
|
|
28
|
+
rescue Errno::ENOENT
|
|
29
|
+
$LOG.warn "#{cache_name} cache not found"
|
|
30
|
+
return nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def save_cache( cache_name, cache_object )
|
|
34
|
+
return nil unless @cache_folder
|
|
35
|
+
File.open( cache_filename_for( cache_name ), 'w' ) do |f|
|
|
36
|
+
f.puts Marshal.dump(cache_object)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def cache_filename_for( name )
|
|
41
|
+
File.join( @cache_folder, "#{name}#{CACHE_EXTENSION}")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
module WikiFlatFileStore
|
|
47
|
+
|
|
48
|
+
CONTENT_EXTENSION = '.textile'
|
|
49
|
+
REVISIONS_EXTENSION = '.yaml'
|
|
50
|
+
DEFAULT_AUTHOR = 'the import script'
|
|
51
|
+
|
|
52
|
+
def load_all_pages
|
|
53
|
+
move_files_if_names_are_not_url_encoded
|
|
54
|
+
pages_on_disk = Dir[ File.join( @folder, "*#{CONTENT_EXTENSION}" ) ].map { |filename| page_name_for( filename )}
|
|
55
|
+
pages_in_memory = @pages.values.map { |page| page && page.name }
|
|
56
|
+
( pages_in_memory.compact | pages_on_disk ).each do |pagename|
|
|
57
|
+
if check_disk_for_updated_page( pagename, true ) == :file_does_not_exist
|
|
58
|
+
revise( pagename, $MESSAGES[:page_deleted], DEFAULT_AUTHOR )
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def save( page )
|
|
64
|
+
save_content( page )
|
|
65
|
+
save_last_revision( page )
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def delete_files_for_page( page_name )
|
|
69
|
+
File.delete( filename_for_content( page_name ), filename_for_revisions( page_name ) )
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def move_files_for_page( old_page_name, new_page_name )
|
|
73
|
+
File.rename( filename_for_content( old_page_name ), filename_for_content( new_page_name ) )
|
|
74
|
+
File.rename( filename_for_revisions( old_page_name ), filename_for_revisions( new_page_name ) )
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def check_disk_for_updated_page( pagename, force = false )
|
|
78
|
+
return unless force || self.check_files_every # We don't care about file changes
|
|
79
|
+
filename = filename_for_content( pagename )
|
|
80
|
+
return :file_does_not_exist unless File.exists?( filename ) # File doesn't exist on disk
|
|
81
|
+
return load_page( filename ) unless page_named( pagename )# File is new on the disk, but not yet in memory
|
|
82
|
+
return load_page( filename ) if content_newer_than_revisions?( page_named(pagename) ) # File is newer on disk
|
|
83
|
+
return nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def load_page( filename )
|
|
87
|
+
mutate( page_name_for( filename ) ) do |page|
|
|
88
|
+
disk_content = load_content( page )
|
|
89
|
+
return nil if disk_content == page.content # No change, disk is the same as memory
|
|
90
|
+
|
|
91
|
+
# We now know that the content on disk is different from that in memory
|
|
92
|
+
|
|
93
|
+
page.revisions = load_revisions( page ) if page.revisions.empty? # Load revisions from disk if none known
|
|
94
|
+
# assumes disk revisions are ALWAYS up to date with memory?
|
|
95
|
+
|
|
96
|
+
# We now know what the page content and the page revisions should be. But not if the revisions are up to date
|
|
97
|
+
if content_newer_than_revisions?( page ) # The textile file has been modified, but the array file has not been updated to match
|
|
98
|
+
page.content = reconstruct_content_from_revisions( page.revisions )
|
|
99
|
+
page.revise( disk_content, DEFAULT_AUTHOR )
|
|
100
|
+
save_last_revision( page )
|
|
101
|
+
else # The textile file and the array file are in sync.
|
|
102
|
+
page.content = disk_content
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
add_page_to_index( page )
|
|
106
|
+
[ page.revisions.last, :dont_save ]
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def load_content( page )
|
|
111
|
+
IO.readlines( filename_for_content( page.name ) ).join
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def load_revisions( page )
|
|
115
|
+
return [] unless File.exists?( filename_for_revisions( page.name ) )
|
|
116
|
+
revisions = []
|
|
117
|
+
begin
|
|
118
|
+
File.open( filename_for_revisions( page.name ) ) { |file|
|
|
119
|
+
YAML.each_document( file ) { |array|
|
|
120
|
+
next unless array.is_a? Array
|
|
121
|
+
next unless array.size == 4
|
|
122
|
+
next unless array[0].is_a? Integer
|
|
123
|
+
revisions[ array[0] ] = Revision.new( page, *array ) }
|
|
124
|
+
}
|
|
125
|
+
rescue
|
|
126
|
+
$LOG.error "Error loading revisions with #{$!.to_s} in file #{page.name}"
|
|
127
|
+
end
|
|
128
|
+
revisions.each_with_index { |r,i| $LOG.error "#{page.name} missing revision #{i}" unless r }
|
|
129
|
+
revisions
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def content_newer_than_revisions?( page )
|
|
133
|
+
return true if page.empty?
|
|
134
|
+
File.ctime(filename_for_content( page.name )) > File.ctime(filename_for_revisions(page.name))
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def reconstruct_content_from_revisions( revisions )
|
|
138
|
+
content = []
|
|
139
|
+
revisions.each { |revision| content = Diff::LCS.patch( content, revision.changes, :patch ) }
|
|
140
|
+
content.join("\n")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def move_files_if_names_are_not_url_encoded
|
|
144
|
+
Dir[ File.join( @folder, "*#{CONTENT_EXTENSION}" ) ].each do |filename|
|
|
145
|
+
basename = File.basename( filename, '.*')
|
|
146
|
+
next if basename.url_decode.url_encode == basename # All ok, so no worry
|
|
147
|
+
new_name = File.join( File.dirname(filename), File.unique_filename( File.dirname(filename), basename.url_decode.url_encode + File.extname( filename) ) )
|
|
148
|
+
File.rename(filename, new_name )
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def save_content( page )
|
|
153
|
+
File.open(filename_for_content( page.name ), 'w' ) { |file| file.puts page.content }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Appends the last revision onto the yaml file
|
|
157
|
+
def save_last_revision( page )
|
|
158
|
+
$LOG.info "Saving revisions for #{page.name}"
|
|
159
|
+
File.open(filename_for_revisions( page.name ), 'a' ) do |file|
|
|
160
|
+
YAML.dump( page.revisions.last.to_a, file )
|
|
161
|
+
file.puts # Needed to ensure that documents are separated
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def save_all_revisions( page )
|
|
166
|
+
$LOG.warn "Saving all revisions for #{page.name}"
|
|
167
|
+
File.open(filename_for_revisions( page.name ), 'w' ) do |file|
|
|
168
|
+
page.revisions.each do |revision|
|
|
169
|
+
YAML.dump( revision.to_a, file )
|
|
170
|
+
file.puts # Needed to ensure that documents are separated
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def page_name_for( filename )
|
|
176
|
+
File.basename( filename, '.*').url_decode
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def filename_for_content( pagename )
|
|
180
|
+
File.join( @folder, "#{pagename.url_encode}#{CONTENT_EXTENSION}" )
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def filename_for_revisions( pagename )
|
|
184
|
+
File.join( @folder, "#{pagename.url_encode}#{REVISIONS_EXTENSION}" )
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
require 'soks-utils'
|
|
2
|
+
require 'yaml'
|
|
3
|
+
|
|
4
|
+
# This is the definition of Revision from v-0-0-2.
|
|
5
|
+
# Much smaller than the current definition. sigh.
|
|
6
|
+
class Revision
|
|
7
|
+
attr_reader :number, :changes, :created_at, :author
|
|
8
|
+
|
|
9
|
+
def initialize( number, changes, author )
|
|
10
|
+
@number, @changes, @author = number, changes, author
|
|
11
|
+
@created_at = Time.now
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def content( page )
|
|
15
|
+
page.revision( @number + 1 ) ? page.revision( @number + 1 ).previous_content( page ) : page.content
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def previous_content( page )
|
|
19
|
+
content( page ).split("\n").unpatch!(@changes).join("\n")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class SoksUpgrade
|
|
24
|
+
|
|
25
|
+
def load_old_revisions( filename )
|
|
26
|
+
File.open( filename ) { |file| return Marshal.load( file ) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def save_new_revisions( old_filename, revisions )
|
|
30
|
+
File.open(new_filename_for_old( old_filename ), 'w' ) do |file|
|
|
31
|
+
revisions.each do |revision|
|
|
32
|
+
YAML.dump( [revision.number, revision.changes, revision.author, revision.created_at ] , file )
|
|
33
|
+
file.puts
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def new_filename_for_old( old_filename )
|
|
39
|
+
basename = File.basename( old_filename, '.*')
|
|
40
|
+
new_extension = '.yaml'
|
|
41
|
+
File.join( File.dirname(old_filename), basename ) + new_extension
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def upgrade_revisions( directory )
|
|
45
|
+
search = File.join( directory,'content', "*.marshal" )
|
|
46
|
+
Dir[ search ].each do |filename|
|
|
47
|
+
puts "Upgrading #{filename}"
|
|
48
|
+
save_new_revisions( filename, load_old_revisions( filename ))
|
|
49
|
+
File.delete filename
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def upgrade_textile( filename )
|
|
54
|
+
textile = IO.readlines( filename ).join
|
|
55
|
+
textile.gsub!(/\[\[\s*(.*?)\s*(|:\s*(.*?)\s*)\]\]/) do |m|
|
|
56
|
+
title, page = $1, $3
|
|
57
|
+
page ? "[[ #{title} => #{page} ]]" : "[[ #{title} ]]"
|
|
58
|
+
end
|
|
59
|
+
File.open( filename, 'w' ) { |f| f.puts textile }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def upgrade_content( directory )
|
|
63
|
+
search = File.join( directory,'content', "*.textile" )
|
|
64
|
+
Dir[ search ].each do |filename|
|
|
65
|
+
puts "Upgrading #{filename}"
|
|
66
|
+
upgrade_textile( filename )
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
end
|
data/lib/soks-utils.rb
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# This is a bit like observable, but for events
|
|
2
|
+
module Notify
|
|
3
|
+
|
|
4
|
+
# Will notify in a separate low priority thread
|
|
5
|
+
def watch_for( *events , &action_block )
|
|
6
|
+
self.event_queue.watch_for( events, action_block )
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Will notify in the running high priority thread (ie will block response to user)
|
|
10
|
+
def watch_attentively_for( *events, &action_block )
|
|
11
|
+
self.event_queue.watch_attentively_for( events, action_block )
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def notify( event, *messages)
|
|
15
|
+
raise "Sorry! Shutting down..." if @shutting_down
|
|
16
|
+
self.event_queue.event( event, messages )
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def event_queue
|
|
20
|
+
@event_queue ||= EventQueue.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class EventQueue
|
|
26
|
+
|
|
27
|
+
def initialize
|
|
28
|
+
@queue = Queue.new
|
|
29
|
+
start_thread
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def event( event, messages )
|
|
33
|
+
# $LOG.warn "#{event}, #{messages}"
|
|
34
|
+
check_thread_ok
|
|
35
|
+
@queue.enq [ event, messages ]
|
|
36
|
+
$LOG.warn "Notification queue backlog of #{@queue.size}" if @queue.size > 100
|
|
37
|
+
notify_attentive_watchers( event, *messages )
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Will call the action_block lazily
|
|
41
|
+
def watch_for( events , action_block )
|
|
42
|
+
events.each { |event| watchers_for(event) << action_block }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Will call the action_block imediately
|
|
46
|
+
def watch_attentively_for( events, action_block )
|
|
47
|
+
events.each { |event| attentive_watchers_for(event) << action_block }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def empty?
|
|
51
|
+
@queue.empty? && !@notifying_flag
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def check_thread_ok
|
|
57
|
+
start_thread unless @thread && @thread.alive?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def start_thread
|
|
61
|
+
@thread = Thread.new do
|
|
62
|
+
loop do
|
|
63
|
+
check_for_events
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
@thread.priority = -1
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def check_for_events
|
|
70
|
+
event, messages = @queue.deq
|
|
71
|
+
notify( event, *messages )
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def notify( event, *messages)
|
|
75
|
+
@notifying_flag = true
|
|
76
|
+
watchers_for( event ).each { |action_block|
|
|
77
|
+
begin
|
|
78
|
+
action_block.call(event, *messages)
|
|
79
|
+
rescue StandardError => err
|
|
80
|
+
$LOG.warn "ERROR #{err}: #{event} - #{messages.join(' ')}"
|
|
81
|
+
err.backtrace.each { |s| $stderr.puts s }
|
|
82
|
+
end
|
|
83
|
+
}
|
|
84
|
+
@notifying_flag = false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def watchers_for( event )
|
|
88
|
+
watchers[ event ] ||= []
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def watchers
|
|
92
|
+
@watchers ||= {}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def notify_attentive_watchers( event, *messages )
|
|
96
|
+
attentive_watchers_for( event ).each { |action_block|
|
|
97
|
+
begin
|
|
98
|
+
action_block.call(event, *messages)
|
|
99
|
+
rescue StandardError => err
|
|
100
|
+
$stderr.puts "ERROR #{err}: #{event} - #{messages.join(' ')}"
|
|
101
|
+
err.backtrace.each { |s| $stderr.puts s }
|
|
102
|
+
end
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def attentive_watchers_for( event )
|
|
107
|
+
attentive_watchers[ event ] ||= []
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def attentive_watchers
|
|
111
|
+
@attentive_watchers ||= {}
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
class PeriodicNotification
|
|
116
|
+
|
|
117
|
+
def initialize( *notify_about, &block)
|
|
118
|
+
@block = block
|
|
119
|
+
notify_about.each do |period|
|
|
120
|
+
start_thread( period )
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def start_thread( period )
|
|
127
|
+
Thread.new( period ) do |period|
|
|
128
|
+
while true
|
|
129
|
+
sleep seconds_to_next_period( period )
|
|
130
|
+
@block.call( period )
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def seconds_to_next_period( period )
|
|
136
|
+
Time.now.next( period ) - Time.now
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
class String
|
|
141
|
+
# Return the left bit of a string e.g. "String".left(2) => "St"
|
|
142
|
+
def left( length ) self.slice( 0, length ) end
|
|
143
|
+
|
|
144
|
+
# Encode the string so it can be used in urls (code coppied from CGI)
|
|
145
|
+
def url_encode
|
|
146
|
+
self.gsub(/([^a-zA-Z0-9]+)/n) do
|
|
147
|
+
'%' + $1.unpack('H2' * $1.size).join('%').upcase
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Decode a string url encoded so it can be used in urls (code coppied from CGI)
|
|
152
|
+
def url_decode
|
|
153
|
+
self.gsub(/((?:%[0-9a-fA-F]{2})+)/n) do
|
|
154
|
+
[$1.delete('%')].pack('H*')
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Return the first n lines of the string
|
|
159
|
+
def first_lines( lines = 1 )
|
|
160
|
+
self.split("\n")[0,lines].join("\n")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def close_unmatched_html
|
|
164
|
+
start_tags = self.scan(/<(\w+)[^>\/]*?(?!\/)>/)
|
|
165
|
+
end_tags = self.scan(/<\/(\w+)[^>\/]*?>/)
|
|
166
|
+
return self if start_tags.size == end_tags.size
|
|
167
|
+
missing_tags = start_tags - end_tags
|
|
168
|
+
text = self.dup
|
|
169
|
+
missing_tags.each do |tag|
|
|
170
|
+
text << "</#{tag[0]}>"
|
|
171
|
+
end
|
|
172
|
+
text
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Removes punctuation and spaces that can cause problems with page names
|
|
176
|
+
#def to_valid_pagename
|
|
177
|
+
# self.tr('\\\[]?{}#&^`<>/','').strip
|
|
178
|
+
#end
|
|
179
|
+
|
|
180
|
+
#Returns the changes between the lines of this string and another
|
|
181
|
+
def changes_from( other_string )
|
|
182
|
+
other_string.split("\n").diff( self.split("\n") ).map { |changeset| changeset.map { |change| change.to_a } }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
class FiniteUniqueList
|
|
188
|
+
include Enumerable
|
|
189
|
+
|
|
190
|
+
attr_accessor :max_size
|
|
191
|
+
|
|
192
|
+
def initialize( max_size = nil, reverse = false, sort_by = nil )
|
|
193
|
+
@max_size = max_size
|
|
194
|
+
@list = Array.new
|
|
195
|
+
@sort_by = sort_by
|
|
196
|
+
@reverse = reverse
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def add( item )
|
|
200
|
+
remove( item )
|
|
201
|
+
@list << item
|
|
202
|
+
sort_items
|
|
203
|
+
remove_excess_items
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def remove( item )
|
|
207
|
+
@list.delete( item )
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def each
|
|
211
|
+
if @reverse
|
|
212
|
+
@list.reverse_each { |item| yield item }
|
|
213
|
+
else
|
|
214
|
+
@list.each { |item| yield item }
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def empty?; @list.empty? end
|
|
219
|
+
|
|
220
|
+
def include?( item )
|
|
221
|
+
@list.include?( item )
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private
|
|
225
|
+
|
|
226
|
+
def remove_excess_items
|
|
227
|
+
return unless @max_size
|
|
228
|
+
while @list.size > @max_size
|
|
229
|
+
@list.shift
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def sort_items
|
|
234
|
+
return unless @sort_by
|
|
235
|
+
@list = @list.sort_by { |item| item.send( @sort_by ) }
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Kindly written by Bil Kleb
|
|
240
|
+
class Numeric
|
|
241
|
+
# similar to distance_of_time_in_words as found in
|
|
242
|
+
# actionpack-1.1.0/lib/action_view/helpers/date_helper.rb
|
|
243
|
+
def to_time_units
|
|
244
|
+
seconds = self.round
|
|
245
|
+
case seconds
|
|
246
|
+
when 0: "fraction of a second"
|
|
247
|
+
when 1: "second"
|
|
248
|
+
when 2..45: "#{seconds} seconds"
|
|
249
|
+
when 46..90: "minute"
|
|
250
|
+
when 91..(60*45): "#{(seconds.to_f/60.0).round} minutes"
|
|
251
|
+
when (60*45)..(60*90): "hour"
|
|
252
|
+
when (60*90)..(60*60*22): "#{(seconds.to_f/60.0/60.0).round} hours"
|
|
253
|
+
when (60*60*22)..(60*60*36): "day"
|
|
254
|
+
when (60*60*36)..(60*60*24*26): "#{(seconds.to_f/60.0/60.0/24.0).round} days"
|
|
255
|
+
when (60*60*24*26)..(60*60*24*45): "month"
|
|
256
|
+
when (60*60*24*45)..(60*60*24*30*11): "#{(seconds.to_f/60.0/60.0/24.0/30.0).round} months"
|
|
257
|
+
when (60*60*24*30*11)..(60*60*24*500): "year"
|
|
258
|
+
else "#{(seconds.to_f/60.0/60.0/24.0/365.0).round} years"
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
class Time
|
|
264
|
+
|
|
265
|
+
# Returns 'yesterday', 'tomorrow' for date relative to now
|
|
266
|
+
def relative_day
|
|
267
|
+
# Days difference
|
|
268
|
+
case self.days_from( Time.now )
|
|
269
|
+
when -7..-2 ; strftime('Last %A')
|
|
270
|
+
when -1 ; "Yesterday"
|
|
271
|
+
when 0 ; "Today"
|
|
272
|
+
when 1 ; "Tomorrow"
|
|
273
|
+
when 2..7 ; strftime('%A')
|
|
274
|
+
else ; strftime( (Time.now.year == self.year) ? '%d %b' :'%d %b %Y')
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def days_from( other_time )
|
|
279
|
+
((Time.local(self.year, self.month, self.day)-Time.local(other_time.year, other_time.month, other_time.day))/(24*60*60)).round
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Checks whether two times are on the same day
|
|
283
|
+
def same_day?( other_time )
|
|
284
|
+
return false unless other_time.year == self.year
|
|
285
|
+
return false unless other_time.yday == self.yday
|
|
286
|
+
return true
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Returns the Time at the next occurance of the period
|
|
290
|
+
def next( period )
|
|
291
|
+
case period
|
|
292
|
+
when :sec, :second
|
|
293
|
+
next_time = self + 1
|
|
294
|
+
Time.local( next_time.year, next_time.month, next_time.day, next_time.hour, next_time.min, next_time.sec )
|
|
295
|
+
when :min, :minute
|
|
296
|
+
next_time = self + 60
|
|
297
|
+
Time.local( next_time.year, next_time.month, next_time.day, next_time.hour, next_time.min )
|
|
298
|
+
when :hour
|
|
299
|
+
next_time = self + ( 60*60 )
|
|
300
|
+
Time.local( next_time.year, next_time.month, next_time.day, next_time.hour)
|
|
301
|
+
when :day
|
|
302
|
+
next_time = self + ( 60*60*24 )
|
|
303
|
+
Time.local( next_time.year, next_time.month, next_time.day)
|
|
304
|
+
when :mon, :month
|
|
305
|
+
next_time = self + ( 60*60*24*(32-self.day) )
|
|
306
|
+
Time.local( next_time.year, next_time.month)
|
|
307
|
+
when :year
|
|
308
|
+
next_time = self + ( 60*60*24*(367-self.yday) )
|
|
309
|
+
Time.local( next_time.year )
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
class File
|
|
316
|
+
|
|
317
|
+
def self.unique_filename( path, filename )
|
|
318
|
+
filename.tr!('\\','/') if filename =~ /^[A-Z]:\\/ # File.basename requires / rather than \ which can be a problem with file uploads
|
|
319
|
+
filename = File.basename( filename ) # Drop all the extra directory information
|
|
320
|
+
filename.gsub!(/[^A-Za-z0-9._%]/,'') # Drop anything but the basic stuff we trust
|
|
321
|
+
return filename unless exist?( join( path, filename ) )# Leave as is, if doesn't exist
|
|
322
|
+
name, counter, extension = basename( filename, '.*'), 1, extname( filename )
|
|
323
|
+
counter += 1 while exist?( join( path, "#{name}#{counter}#{extension}" ) )
|
|
324
|
+
return "#{name}#{counter}#{extension}"
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
end
|