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
|
@@ -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
|
data/lib/soks-model.rb
ADDED
|
@@ -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
|
+
|