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.
Files changed (120) hide show
  1. data/LICENSE.txt +66 -0
  2. data/README.txt +64 -0
  3. data/bin/soks-create-wiki.rb +193 -0
  4. data/contrib/diff/lcs.rb +1105 -0
  5. data/contrib/diff/lcs/array.rb +21 -0
  6. data/contrib/diff/lcs/block.rb +51 -0
  7. data/contrib/diff/lcs/callbacks.rb +322 -0
  8. data/contrib/diff/lcs/change.rb +169 -0
  9. data/contrib/diff/lcs/hunk.rb +257 -0
  10. data/contrib/diff/lcs/ldiff.rb +226 -0
  11. data/contrib/diff/lcs/string.rb +19 -0
  12. data/contrib/diff_licence.txt +76 -0
  13. data/contrib/easyprompt.rb +58 -0
  14. data/contrib/easyprompt_licence.txt +504 -0
  15. data/contrib/redcloth-3.0.3.rb +1113 -0
  16. data/contrib/redcloth_license.txt +27 -0
  17. data/lib/authenticators.rb +121 -0
  18. data/lib/helpers/counter-helpers.rb +132 -0
  19. data/lib/helpers/default-helpers.rb +416 -0
  20. data/lib/helpers/mail2wiki-helper.rb +105 -0
  21. data/lib/helpers/maintenance-helpers.rb +149 -0
  22. data/lib/helpers/rss2wiki-helper.rb +47 -0
  23. data/lib/helpers/wiki2html.rb +60 -0
  24. data/lib/soks-model.rb +271 -0
  25. data/lib/soks-servlet.rb +177 -0
  26. data/lib/soks-storage.rb +187 -0
  27. data/lib/soks-upgrade-0.0.2.rb +70 -0
  28. data/lib/soks-utils.rb +327 -0
  29. data/lib/soks-view.rb +399 -0
  30. data/lib/soks.rb +27 -0
  31. data/rakefile +109 -0
  32. data/templates/default/attachment/favicon.ico +0 -0
  33. data/templates/default/attachment/logo.jpg +0 -0
  34. data/templates/default/attachment/logo.png +0 -0
  35. data/templates/default/attachment/logo.tiff +0 -0
  36. data/templates/default/attachment/newpage.js +41 -0
  37. data/templates/default/attachment/print_stylesheet.css +2 -0
  38. data/templates/default/attachment/robots.txt +6 -0
  39. data/templates/default/attachment/rss.png +0 -0
  40. data/templates/default/attachment/stylesheet.css +219 -0
  41. data/templates/default/banned_titles.txt +67 -0
  42. data/templates/default/caches/readme.txt +1 -0
  43. data/templates/default/content/Api%20for%20classes%20to%20modify%20the%20wiki.textile +30 -0
  44. data/templates/default/content/Author.textile +16 -0
  45. data/templates/default/content/Automatic%20Summaries.textile +40 -0
  46. data/templates/default/content/Automatic%20counters.textile +22 -0
  47. data/templates/default/content/Automatic%20exporters.textile +23 -0
  48. data/templates/default/content/Automatic%20importers.textile +59 -0
  49. data/templates/default/content/Automatic%20linking.textile +7 -0
  50. data/templates/default/content/Automatic%20maintenance%20helpers.textile +39 -0
  51. data/templates/default/content/Bug%3A%20Competing%20edits.textile +22 -0
  52. data/templates/default/content/Bug%3A%20Does%20not%20make%20use%20of%20if%2Dmodified%2Dsince%20r.textile +3 -0
  53. data/templates/default/content/Bug%3A%20Email%20adresses%20in%20page%20titles%20cause%20incorrec.textile +3 -0
  54. data/templates/default/content/Bug%3A%20GEM%20limits%20title%20lengths.textile +3 -0
  55. data/templates/default/content/Bug%3A%20Memory%20leak.textile +13 -0
  56. data/templates/default/content/Bug%3A%20Page%2Einserted%5Finto%20is%20never%20purged.textile +17 -0
  57. data/templates/default/content/Bug%3A%20Pages%20that%20link%20here%20may%20not%20appear%20on%20r.textile +13 -0
  58. data/templates/default/content/Bug%3A%20Textile%20mishandles%20paragraphs.textile +37 -0
  59. data/templates/default/content/Bug%3A%20Unanticipated%20Rollbacks.textile +23 -0
  60. data/templates/default/content/Bug%3A%20notextile%20does%20not%20prevent%20page%20inserts.textile +3 -0
  61. data/templates/default/content/Home%20Page.textile +22 -0
  62. data/templates/default/content/How%20to%20administrate%20this%20wiki.textile +57 -0
  63. data/templates/default/content/How%20to%20change%20the%20way%20this%20wiki%20looks.textile +32 -0
  64. data/templates/default/content/How%20to%20export%20a%20site%20from%20this%20wiki.textile +82 -0
  65. data/templates/default/content/How%20to%20get%20the%20latest%20Soks%20from%20cvs.textile +45 -0
  66. data/templates/default/content/How%20to%20hack%20soks.textile +66 -0
  67. data/templates/default/content/How%20to%20import%20a%20site%20from%20instiki.textile +15 -0
  68. data/templates/default/content/How%20to%20import%20data.textile +41 -0
  69. data/templates/default/content/How%20to%20install%20Soks.textile +33 -0
  70. data/templates/default/content/How%20to%20password%20protect%20your%20wiki.textile +53 -0
  71. data/templates/default/content/How%20to%20re%2Dbuild%20the%20page%20cache.textile +71 -0
  72. data/templates/default/content/How%20to%20report%20a%20bug.textile +9 -0
  73. data/templates/default/content/How%20to%20upgrade%20soks.textile +32 -0
  74. data/templates/default/content/How%20to%20use%20the%20Automatic%20Helper%20classes.textile +12 -0
  75. data/templates/default/content/How%20to%20use%20this%20wiki.textile +30 -0
  76. data/templates/default/content/List%20of%20changes.textile +10 -0
  77. data/templates/default/content/News%3A%20Version%201%2D0%2D0%20released.textile +19 -0
  78. data/templates/default/content/News%3A%20Version%201%2D0%2D1%20released.textile +12 -0
  79. data/templates/default/content/Pages%20to%20include%20in%20the%20distribution.textile +55 -0
  80. data/templates/default/content/Per%20Wiki%20Templates.textile +37 -0
  81. data/templates/default/content/Picture%20of%20a%20pair%20of%20soks.textile +1 -0
  82. data/templates/default/content/Planned%20Features.textile +74 -0
  83. data/templates/default/content/README.textile +64 -0
  84. data/templates/default/content/RSS%20feed.textile +9 -0
  85. data/templates/default/content/Recent%20changes%20to%20this%20site.textile +352 -0
  86. data/templates/default/content/SOKS%20features.textile +19 -0
  87. data/templates/default/content/Sidebar%20Page.textile +6 -0
  88. data/templates/default/content/Site%20Index.textile +241 -0
  89. data/templates/default/content/Soks%27s%20Licence.textile +66 -0
  90. data/templates/default/content/Tag%3A%20Include%20this%20page%20in%20the%20distribution.textile +6 -0
  91. data/templates/default/start.rb +90 -0
  92. data/templates/default/version.txt +1 -0
  93. data/templates/default/views/Page_content.rhtml +1 -0
  94. data/templates/default/views/Page_edit.rhtml +79 -0
  95. data/templates/default/views/Page_find.rhtml +35 -0
  96. data/templates/default/views/Page_linksfromrss.rhtml +24 -0
  97. data/templates/default/views/Page_listrss.rhtml +46 -0
  98. data/templates/default/views/Page_meta.rhtml +44 -0
  99. data/templates/default/views/Page_print.rhtml +6 -0
  100. data/templates/default/views/Page_revision.rhtml +39 -0
  101. data/templates/default/views/Page_revisions.rhtml +36 -0
  102. data/templates/default/views/Page_rss.rhtml +57 -0
  103. data/templates/default/views/Page_view.rhtml +8 -0
  104. data/templates/default/views/UploadPage_edit.rhtml +63 -0
  105. data/templates/default/views/frame.rhtml +63 -0
  106. data/templates/default/views/messages.yaml +7 -0
  107. data/test/html/2006Mar.html +66 -0
  108. data/test/html/poignant.html +36 -0
  109. data/test/html/poignant.textile +36 -0
  110. data/test/mock-objects.rb +69 -0
  111. data/test/test_counter-helper.rb +162 -0
  112. data/test/test_soks-helper-maintenance.rb +106 -0
  113. data/test/test_soks-helpers.rb +145 -0
  114. data/test/test_soks-model.rb +144 -0
  115. data/test/test_soks-servlet.rb +240 -0
  116. data/test/test_soks-storage.rb +108 -0
  117. data/test/test_soks-utils.rb +226 -0
  118. data/test/test_soks-view.rb +193 -0
  119. data/test/test_soks.rb +9 -0
  120. metadata +182 -0
@@ -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
@@ -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
@@ -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