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,105 @@
1
+ require 'net/imap'
2
+
3
+ # From Dave Burt on comp.lang.ruby
4
+ class String
5
+ def from_quoted_printable
6
+ self.gsub(/\r\n/, "\n").gsub(/=(?![\dA-F]{2})/,'=3D').unpack("M").first
7
+ end
8
+ end
9
+
10
+ class Message
11
+
12
+ attr_reader :message_id, :subject, :sender_name, :sender_email, :date, :text
13
+
14
+ def initialize( imap, message_id )
15
+ @imap, @message_id = imap, message_id
16
+ envelope = @imap.fetch( @message_id, 'ENVELOPE' ).first.attr['ENVELOPE']
17
+ @subject = envelope['subject'].gsub(/^(Fw|Re):?/i,'').strip
18
+ @sender_name = envelope['from'].first['name']
19
+ @date = envelope['date']
20
+ @sender_email = envelope['from'].first['mailbox'] + ' at ' + envelope['from'].first['host']
21
+ @sender_name = @sender_email unless @sender_name && @sender_name.size > 1
22
+ @sender_name.gsub!(/@/,' at ')
23
+ @text = plain_text_content_from_message( message_id )
24
+ end
25
+
26
+ def plain_text_content_from_message( id )
27
+ @imap.fetch( id, 'BODY[1]' ).first.attr['BODY[1]'].from_quoted_printable
28
+ end
29
+
30
+ end
31
+
32
+ class Mail2WikiHelper
33
+
34
+ DEFAULT_SETTINGS = {
35
+ :server => 'imap.hermes.cam.ac.uk',
36
+ :username => 'tamc2',
37
+ :password => 'missing_a_password',
38
+ :mailbox => 'test',
39
+ :check_event => :hour,
40
+ :subject_regexp => /.*/,
41
+ :keyword => 'PutInWiki'
42
+ }
43
+
44
+ def initialize( wiki, settings = {} )
45
+ @settings = DEFAULT_SETTINGS.merge( settings )
46
+ @wiki = wiki
47
+ check_mailbox
48
+ @wiki.watch_for(@settings[:check_event]) { check_mailbox }
49
+ end
50
+
51
+ private
52
+
53
+ def check_mailbox
54
+ $LOG.info "Checking #{@settings[:mailbox]} on #{@settings[:server]}"
55
+ login
56
+ select_mailbox
57
+ new_messages_for_wiki do |message_id|
58
+ this_message = Message.new( @imap, message_id )
59
+ if this_message.subject =~ @settings[:subject_regexp]
60
+ $LOG.info "Adding '#{this_message.subject}' to wiki"
61
+ add_message_to_wiki( this_message )
62
+ mark_as_added( message_id )
63
+ end
64
+ end
65
+ logout
66
+ end
67
+
68
+ def add_message_to_wiki( message )
69
+ current_page = @wiki.page( message.subject )
70
+ if current_page.empty?
71
+ text = "h1. #{message.subject}"
72
+ else
73
+ text = current_page.textile
74
+ end
75
+ text << "\n\n"
76
+ text << "*Copied from Email on #{message.date} from #{message.sender_name} (#{message.sender_email})*\n\n"
77
+ text << "<pre>\n"
78
+ text << message.text
79
+ text << "\n</pre>\n"
80
+ @wiki.revise(message.subject, text, message.sender_name )
81
+ end
82
+
83
+ def login
84
+ @imap = Net::IMAP.new(@settings[:server])
85
+ @imap.login( @settings[:username], @settings[:password] )
86
+ end
87
+
88
+ def logout
89
+ @imap.logout
90
+ @imap.disconnect
91
+ end
92
+
93
+ def select_mailbox
94
+ @imap.select @settings[:mailbox]
95
+ end
96
+
97
+ def new_messages_for_wiki
98
+ @imap.search("UNKEYWORD #{@settings[:keyword]}").each { |id| yield id }
99
+ end
100
+
101
+ def mark_as_added( id )
102
+ @imap.store( id, '+FLAGS', [@settings[:keyword]] )
103
+ end
104
+
105
+ end
@@ -0,0 +1,149 @@
1
+ class DeleteOldPagesHelper
2
+
3
+ # Default wakes up each day at midnight and wipes all deleted pages more than 100 days old
4
+ def initialize( wiki, event_to_check_on = :day, age_to_wipe_at = 60*60*24*100 )
5
+ @wiki = wiki
6
+ @age_to_wipe_at = age_to_wipe_at
7
+ @wiki.watch_for(event_to_check_on) { check_and_delete_pages }
8
+ end
9
+
10
+ private
11
+
12
+ def check_and_delete_pages
13
+ @wiki.each(false) do |name, page|
14
+ next unless page.deleted?
15
+ next unless old_enough_to_wipe?( page )
16
+ $LOG.warn "Permanently wiping #{name} from wiki AND from disk"
17
+ @wiki.wipe_from_disk( name )
18
+ end
19
+ end
20
+
21
+ def old_enough_to_wipe?( page )
22
+ (Time.now - page.revised_on) > @age_to_wipe_at
23
+ end
24
+
25
+ end
26
+
27
+ class DeleteOldRevisionsHelper
28
+
29
+ AUTHOR = 'Automatic Revision Remover'
30
+
31
+ # Default wakes up each day at midnight and wipes all revisions more than 100 days old if there are more than 20 revisions in the page
32
+ def initialize( wiki, event_to_check_on = :day, age_to_wipe_at = 60*60*24*365, minimum_revisions = 20 )
33
+ @wiki = wiki
34
+ @age_to_wipe_at = age_to_wipe_at
35
+ @maximum_revisions = minimum_revisions
36
+ @wiki.watch_for(event_to_check_on) { check_and_delete_revisions }
37
+ end
38
+
39
+ private
40
+
41
+ def check_and_delete_revisions
42
+ @wiki.each do |name, page|
43
+ next unless page.revisions.size > @maximum_revisions
44
+ next unless old_enough_to_delete?( page.revisions.first )
45
+ delete_old_revisions_from( page )
46
+ end
47
+ end
48
+
49
+ def delete_old_revisions_from( page )
50
+ page.content_lock.synchronize do
51
+ delete_revisions_at_or_below = page.revisions.size - @maximum_revisions - 1
52
+ delete_revisions_at_or_below -= 1 while not old_enough_to_delete? page.revisions[delete_revisions_at_or_below]
53
+
54
+ new_revisions = []
55
+ new_revisions << Revision.new( page,
56
+ new_revisions.length,
57
+ page.revisions[delete_revisions_at_or_below].content.changes_from(""),
58
+ AUTHOR,
59
+ page.revisions[delete_revisions_at_or_below].created_on )
60
+
61
+ page.revisions[ (delete_revisions_at_or_below+1)..page.revisions.size].each do |revision|
62
+ new_revisions << Revision.new( page,
63
+ new_revisions.length,
64
+ revision.changes,
65
+ revision.author,
66
+ revision.created_on )
67
+ end
68
+ page.revisions = new_revisions
69
+ @wiki.save_all_revisions( page )
70
+ end
71
+ end
72
+
73
+ def old_enough_to_delete?( revision )
74
+ (Time.now - revision.created_on) > @age_to_wipe_at
75
+ end
76
+ end
77
+
78
+ class MergeOldRevisionsHelper
79
+
80
+ # Default wakes up each hour and merges all revisions more than 24 hours old, by the same author, and that are created within an hour of each other
81
+ def initialize( wiki, event_to_check_on = :day, minimum_age_to_merge = 60*60*24*365, maximum_time_between_revisions_for_merge = 60*60 )
82
+ @wiki = wiki
83
+ @minimum_age_to_merge = minimum_age_to_merge
84
+ @maximum_time_between_revisions_for_merge = maximum_time_between_revisions_for_merge
85
+ @wiki.watch_for(event_to_check_on) { check_for_pages_to_merge }
86
+ end
87
+
88
+ def check_for_pages_to_merge
89
+ @wiki.each do |name, page|
90
+ page.content_lock.synchronize do
91
+ check_revisions_to_merge_on page
92
+ end
93
+ end
94
+ end
95
+
96
+ def check_revisions_to_merge_on( page )
97
+ return if page.empty?
98
+ change_made = false
99
+ new_revisions = []
100
+ next_revision = page.revisions.first
101
+ while next_revision
102
+ ending_revision = next_revision
103
+
104
+ while can_merge?( next_revision, ending_revision.following_revision )
105
+ ending_revision = ending_revision.following_revision
106
+ end
107
+
108
+ changes = if ending_revision == next_revision
109
+ next_revision.changes
110
+ else
111
+ change_made = true
112
+ ending_revision.content.changes_from( next_revision.previous_content )
113
+ end
114
+
115
+ new_revisions << Revision.new( page,
116
+ new_revisions.length,
117
+ changes,
118
+ ending_revision.author,
119
+ ending_revision.created_on )
120
+
121
+ next_revision = ending_revision.following_revision
122
+ end
123
+ if change_made
124
+ page.revisions = new_revisions
125
+ @wiki.save_all_revisions( page )
126
+ end
127
+ end
128
+
129
+ def can_merge?( revision_a, revision_b )
130
+ return false unless revision_a && revision_b
131
+ return false unless same_author?( revision_a, revision_b )
132
+ return false unless revised_at_a_similar_time?( revision_a, revision_b )
133
+ return false unless not_to_recent?( revision_a )
134
+ return false unless not_to_recent?( revision_b )
135
+ true
136
+ end
137
+
138
+ def same_author?( a, b )
139
+ a.author == b.author
140
+ end
141
+
142
+ def revised_at_a_similar_time?( a, b )
143
+ (a.revised_on - b.revised_on).abs < @maximum_time_between_revisions_for_merge
144
+ end
145
+
146
+ def not_to_recent?( a_revision )
147
+ (Time.now - a_revision.revised_on) > @minimum_age_to_merge
148
+ end
149
+ end
@@ -0,0 +1,47 @@
1
+ require 'rss/1.0'
2
+ require 'rss/2.0'
3
+ require 'open-uri'
4
+
5
+ class RSS2WikiHelper
6
+
7
+ DEFAULT_SETTINGS = {
8
+ :url => 'http://localhost:8000/rss/recent%20changes%20to%20this%20site',
9
+ :pagename => nil, # If nil, uses channel title,
10
+ :update_on_event => :hour,
11
+ :author => 'AutomaticRSS2Wiki',
12
+ }
13
+
14
+ def initialize( wiki, settings = {} )
15
+ @settings = DEFAULT_SETTINGS.merge( settings )
16
+ @wiki = wiki
17
+ update_rss
18
+ update_wiki
19
+ @wiki.watch_for(@settings[:update_on_event]) do
20
+ update_rss
21
+ update_wiki
22
+ end
23
+ end
24
+
25
+ def update_wiki
26
+ @wiki.revise( @settings[:pagename] || @rss.channel.title, render, @rss.items.first.respond_to?('author') ? @rss.items.first.author : "AutomaticRSS" )
27
+ end
28
+
29
+ def render
30
+ content = "h1. #{@rss.channel.title}\n\n"
31
+ @rss.items.each do |item|
32
+ content << "# [[ #{escape(item.title)} => #{item.link} ]]\n"
33
+ end
34
+ content
35
+ end
36
+
37
+ def update_rss
38
+ $LOG.info "Updating feed"
39
+ open(@settings[:url]) do |http|
40
+ @rss = RSS::Parser.parse( http.read , false)
41
+ end
42
+ end
43
+
44
+ def escape( string )
45
+ string.tr('[]=>','')
46
+ end
47
+ end
@@ -0,0 +1,60 @@
1
+ class Wiki2Html
2
+
3
+ DEFAULT_SETTINGS = {
4
+ :views_to_copy => ['view','meta','rss'],
5
+ :extension => '.html',
6
+ :destination_dir => '/Users/tamc2/Sites',
7
+ :destination_url => 'http://localhost/~tamc2'
8
+ }
9
+
10
+ def initialize( wiki, view, settings = {} )
11
+ @settings = DEFAULT_SETTINGS.merge( settings )
12
+ @wiki, @view = wiki, view
13
+ @wiki.watch_for( :page_created, :page_deleted ) { |event, page| new_page( page ) }
14
+ @wiki.watch_for( :page_revised ) { |event, page| page_revised( page ) }
15
+ update_all_pages
16
+ end
17
+
18
+ def new_page( page )
19
+ copy_all_views( page.name )
20
+ titleregex = Regexp.new( page.name, Regexp::IGNORECASE )
21
+ @wiki.each { |name, linkedpage |
22
+ page_revised( linkedpage ) if linkedpage.textile =~ titleregex
23
+ }
24
+ end
25
+
26
+ def page_revised( page )
27
+ copy_all_views( page.name )
28
+ page.inserted_into.each { |including_page| copy_all_views( including_page.name ) }
29
+ end
30
+
31
+ def update_all_pages
32
+ @wiki.each { |pagename, page| copy_all_views( pagename) }
33
+ end
34
+
35
+ def copy_all_views( pagename )
36
+ @settings[:views_to_copy].each { |view| copy_view( pagename, view ) }
37
+ end
38
+
39
+ def copy_view( pagename, view )
40
+ $stderr.puts "Copying #{pagename} #{view}"
41
+ html = @view.view( pagename, view )
42
+ update_links html
43
+ destination = "#{@settings[:destination_dir]}/#{view}/#{pagename}#{@settings[:extension]}".downcase
44
+ File.mkpath(File.dirname(destination)) unless File.exists?(File.dirname(destination))
45
+ File.open( destination,'w') {|file| file.puts html}
46
+ end
47
+
48
+ def update_links( html )
49
+ old_link = /['"]#{$SETTINGS[:url]}\/(.*?)\/(.*?)(\.\w*)?['"]/
50
+ html.gsub!(old_link) do |match|
51
+ if ($1.downcase == 'attachment' || @settings[:views_to_copy].include?( $1.downcase ))
52
+ "'#{@settings[:destination_url]}\/#{$1.downcase}\/#{$2.downcase}#{$3 || @settings[:extension]}'"
53
+ else
54
+ match
55
+ end
56
+ end
57
+ html
58
+ end
59
+
60
+ end
@@ -0,0 +1,271 @@
1
+ require 'thread'
2
+
3
+ # Revision stores changes as a diff against the more recent content version
4
+ class Revision
5
+ attr_reader :number, :author, :page
6
+ attr_reader :changes, :created_on
7
+ alias :revised_on :created_on
8
+
9
+ def initialize( page, number, changes, author, created_on = Time.now )
10
+ @page, @number, @changes, @author, @created_on = page, number, changes, author, created_on
11
+ end
12
+
13
+ # Recreates the content of the page AFTER this revision had been made.
14
+ # Done by recursively applying diffs to more recent versions.
15
+ def content
16
+ following_revision ? following_revision.previous_content : page.content
17
+ end
18
+
19
+ # Recreateds the content of the page BEFORE this revision had been made
20
+ def previous_content
21
+ content.split("\n").unpatch!(@changes).join("\n")
22
+ end
23
+
24
+ def previous_revision
25
+ return nil if number == 0
26
+ page.revision( number - 1 )
27
+ end
28
+
29
+ def following_revision
30
+ page.revision( number + 1 )
31
+ end
32
+
33
+ def method_missing( symbol, *args )
34
+ raise(ArgumentError, "Revision does not respond to #{symbol}", caller) unless @page && @page.respond_to?( symbol )
35
+ @page.send symbol, *args
36
+ end
37
+
38
+ # To allow the contents to be dumped in a class independent way.
39
+ # Must match the order of the variables used in initialize
40
+ def to_a() [@number, @changes, @author, @created_on] end
41
+ end
42
+
43
+ class Page
44
+ attr_accessor :content_lock, :name, :content, :revisions
45
+ attr_accessor :links_lock, :links_from, :links_to, :inserted_into
46
+ alias :to_s :name
47
+
48
+ # Returns an empty version of itself.
49
+ def self.empty( name )
50
+ empty = self.new( name )
51
+ empty.revise( $MESSAGES[:Type_what_you_want_here_and_click_save], "NoOne" )
52
+ class << empty
53
+ def empty?; true; end
54
+ end
55
+ empty
56
+ end
57
+
58
+ def initialize( name )
59
+ @content_lock, @links_lock = Mutex.new, Mutex.new
60
+ @name, @content, @revisions = name, "", []
61
+ @links_from, @links_to = [], []
62
+ @inserted_into = []
63
+ end
64
+
65
+ # Revises the content of this page, creating a new revision class that stores the changes
66
+ def revise( new_content, author )
67
+ return nil if new_content == @content
68
+ changes = new_content.changes_from @content
69
+ return nil if changes.empty?
70
+ @revisions << Revision.new( self, @revisions.length, changes , author )
71
+ @content = new_content
72
+ @revisions.last
73
+ end
74
+
75
+ # Returns the content of this page to that of a previous version
76
+ def rollback( number, author )
77
+ revise( ( number < 0 ) ? $MESSAGES[:page_deleted] : @revisions[ number ].content, author )
78
+ end
79
+
80
+ def revision( number ) @revisions[ number ] end
81
+
82
+ def deleted?
83
+ ( content =~ /^#{$MESSAGES[:page_deleted]}/i ) ||
84
+ ( content =~ /^#{$MESSAGES[:content_moved_to]} /i ) ? true : false
85
+ end
86
+
87
+ def empty?; @revisions.empty? end
88
+
89
+ def <=>( otherpage ) self.score <=> otherpage.score end
90
+
91
+ def score; @links_from.size + @links_to.size end
92
+
93
+ def created_on; @revisions.first.created_on end
94
+
95
+ def is_inserted_into( page )
96
+ @links_lock.synchronize { @inserted_into << page unless @inserted_into.include? page }
97
+ end
98
+
99
+ def name_for_index; name.downcase end
100
+
101
+ # Refactored changes_between into the String class in soks-utils
102
+
103
+ # Any unhandled calls are passed onto the latest revision (e.g. author, creation time etc)
104
+ def method_missing( symbol, *args )
105
+ if @revisions.last && @revisions.last.respond_to?(symbol)
106
+ @revisions.last.send symbol, *args
107
+ else
108
+ raise ArgumentError,"Page does not respond to #{symbol}", caller
109
+ end
110
+ end
111
+ end
112
+
113
+ class EmptyPage < Page
114
+ def empty?; true; end
115
+ end
116
+
117
+ #Serves as a marker, so ImagePage and AttachmentPage can re-use the same view templates
118
+ class UploadPage < Page
119
+ end
120
+
121
+ class ImagePage < UploadPage
122
+ def name_for_index; @name[10..-1].strip.downcase end
123
+ end
124
+
125
+ class AttachmentPage < UploadPage
126
+ def name_for_index; @name[ 9..-1].strip.downcase end
127
+ end
128
+
129
+ # This class has turned into a behmoth, need to refactor.
130
+ class Wiki
131
+ include WikiFlatFileStore
132
+ include WikiCacheStore
133
+ include Enumerable
134
+ include Notify # Will notify any watchers if underlying files change
135
+
136
+ attr_accessor :check_files_every
137
+
138
+ PAGE_CLASSES = [
139
+ [ /^picture of/i, ImagePage ],
140
+ [ /^attached/i, AttachmentPage ],
141
+ [ /.*/, Page ]
142
+ ]
143
+
144
+ CACHE_NAME = 'pages'
145
+
146
+ def initialize( content_folder, cache_folder = nil )
147
+ @cache_folder = cache_folder
148
+ @folder = content_folder
149
+ @pages = load_cache(CACHE_NAME) || {}
150
+ @shutting_down = false
151
+ @check_files_every = nil
152
+ watch_for(:start) { start }
153
+ end
154
+
155
+ def start
156
+ load_all_pages
157
+ start_watching_files
158
+ setup_periodic_notifications
159
+ end
160
+
161
+ def shutdown
162
+ notify :shutdown
163
+ sleep(1) until event_queue.empty?
164
+ @shutting_down = true # Stop further modifications
165
+ save_cache(CACHE_NAME, @pages)
166
+ end
167
+
168
+ def page( name )
169
+ page_named( name )|| new_page( name, :empty )
170
+ end
171
+
172
+ def each( exclude_deleted = true )
173
+ @pages.each { |name, page| yield [name, page] unless exclude_deleted && page.deleted? }
174
+ end
175
+
176
+ def exists?( name )
177
+ @pages.include?( name.downcase ) && !page_named( name ).deleted?
178
+ end
179
+
180
+ def revise( pagename, content, author )
181
+ raise "Sorry! Shutting down..." if @shutting_down
182
+ check_disk_for_updated_page pagename
183
+ mutate( pagename ) { |page| page.revise( content, author ) }
184
+ end
185
+
186
+ def move( old_pagename, new_pagename, author )
187
+ old_content = page(old_pagename).content
188
+ revise( old_pagename, "#{$MESSAGES[:content_moved_to]} [[#{new_pagename}]]", author )
189
+ revise( new_pagename, "#{$MESSAGES[:content_moved_from]} [[#{old_pagename}]]", author )
190
+ revise( new_pagename, old_content, author )
191
+ end
192
+
193
+ def rollback( pagename, number, author )
194
+ raise "Sorry! Shutting down..." if @shutting_down
195
+ check_disk_for_updated_page pagename
196
+ mutate( pagename ) { |page| page.rollback( number, author ) }
197
+ end
198
+
199
+ def delete( pagename, author )
200
+ revise( pagename, $MESSAGES[:page_deleted], author )
201
+ end
202
+
203
+ def wipe_from_disk( pagename )
204
+ page = page_named( pagename )
205
+ raise "Page not deleted!" unless page.deleted?
206
+ page.content_lock.synchronize do
207
+ delete_files_for_page( pagename.downcase )
208
+ @pages.delete( pagename.downcase )
209
+ end
210
+ end
211
+
212
+ private
213
+
214
+ def setup_periodic_notifications
215
+ PeriodicNotification.new( :year, :month, :day, :hour, :min ) do |period|
216
+ notify period unless @shutting_down
217
+ end
218
+ end
219
+
220
+ def start_watching_files
221
+ return unless check_files_every
222
+ watch_for(check_files_every) { load_all_pages }
223
+ end
224
+
225
+ def new_page( name, initializer = :new )
226
+ PAGE_CLASSES.each do |regex,klass|
227
+ return klass.send( initializer, name) if name =~ regex
228
+ end
229
+ end
230
+
231
+ def mutate( pagename )
232
+ didexist = exists? pagename
233
+ page = page_named( pagename ) || new_page( pagename )
234
+ revision = nil
235
+ page.content_lock.synchronize do
236
+ # Check if the capitalisation of the page has changed
237
+ unless page.name == pagename
238
+ move_files_for_page( page.name, pagename )
239
+ page.name = pagename
240
+ notify :page_title_recapitalized, page
241
+ end
242
+ # Yield to the mutator block
243
+ revision, dont_save = yield page
244
+ # Save page if required
245
+ if revision && dont_save != :dont_save
246
+ save page
247
+ add_page_to_index( page )
248
+ end
249
+ end
250
+ if revision
251
+ notify :page_revised, page, revision
252
+ if page.deleted?
253
+ notify :page_deleted, page, revision
254
+ elsif !didexist
255
+ notify :page_created, page, revision
256
+ end
257
+ end
258
+ end
259
+
260
+ def add_page_to_index( page )
261
+ @pages[ page.name.downcase ] = page
262
+ page
263
+ end
264
+
265
+ def page_named( pagename )
266
+ @pages[ pagename.downcase ]
267
+ end
268
+
269
+ end
270
+
271
+