Soks 0.0.7 → 1.0.0
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 +2 -0
- data/README.txt +3 -2
- data/TODO.txt +31 -0
- data/bin/soks-create-wiki.rb +0 -1
- data/lib/authenticators.rb +30 -4
- data/lib/helpers/counter-helpers.rb +132 -0
- data/lib/helpers/default-helpers.rb +170 -169
- data/lib/helpers/mail2wiki-helper.rb +18 -22
- data/lib/helpers/maintenance-helpers.rb +149 -0
- data/lib/helpers/rss2wiki-helper.rb +7 -8
- data/lib/soks-model.rb +82 -54
- data/lib/soks-servlet.rb +126 -108
- data/lib/soks-storage.rb +74 -11
- data/lib/soks-utils.rb +77 -3
- data/lib/soks-view.rb +169 -103
- data/lib/soks.rb +5 -23
- data/templates/default/attachment/newpage.js +4 -13
- data/templates/default/attachment/print_stylesheet.css +2 -7
- data/templates/default/caches/readme.txt +1 -0
- data/templates/default/content/Api%20for%20classes%20to%20modify%20the%20wiki.textile +2 -0
- data/templates/default/content/Author.textile +4 -1
- data/templates/default/content/Automatic%20Summaries.textile +16 -53
- data/templates/default/content/Automatic%20linking%20between%20pages.textile +3 -3
- data/templates/default/content/{bug%3A%20competing%20edits.textile → Bug%3A%20Competing%20edits.textile} +9 -0
- data/templates/default/content/Bug%3A%20Does%20not%20make%20use%20of%20if%2Dmodified%2Dsince%20r.textile +2 -0
- data/templates/default/content/Bug%3A%20E%2Dmail%20addresses%20with%20hyphens%20not%20recognised.textile +17 -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 -1
- data/templates/default/content/Bug%3A%20Memory%20leak.textile +13 -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 +4 -0
- data/templates/default/content/Bug%3A%20Unanticipated%20Rollbacks.textile +2 -0
- data/templates/default/content/Bug%3A%20notextile%20does%20not%20prevent%20page%20inserts.textile +2 -0
- data/templates/default/content/Home%20Page.textile +3 -1
- data/templates/default/content/How%20to%20administrate%20this%20wiki.textile +23 -13
- data/templates/default/content/How%20to%20change%20the%20way%20this%20wiki%20looks.textile +3 -1
- data/templates/default/content/How%20to%20export%20a%20site%20from%20this%20wiki.textile +22 -0
- data/templates/default/content/How%20to%20get%20the%20latest%20Soks%20from%20cvs.textile +2 -0
- data/templates/default/content/How%20to%20hack%20soks.textile +2 -0
- data/templates/default/content/How%20to%20import%20a%20site%20from%20instiki.textile +2 -0
- data/templates/default/content/{How%20to%20import%20data%20to%20this%20wiki.textile → How%20to%20import%20data.textile} +3 -7
- data/templates/default/content/How%20to%20install%20Soks.textile +2 -0
- data/templates/default/content/How%20to%20password%20protect%20your%20wiki.textile +21 -11
- data/templates/default/content/How%20to%20report%20a%20bug.textile +2 -1
- data/templates/default/content/How%20to%20upgrade%20soks.textile +22 -0
- data/templates/default/content/How%20to%20use%20the%20keyboard%20shortcuts.textile +2 -2
- data/templates/default/content/How%20to%20use%20this%20wiki.textile +3 -1
- data/templates/default/content/List%20of%20changes.textile +84 -118
- data/templates/default/content/News%3A%20Version%201%2D0%2D0%20released.textile +19 -0
- data/templates/default/content/Pages%20to%20include%20in%20the%20distribution.textile +51 -0
- data/templates/default/content/Per%20Wiki%20Templates.textile +2 -0
- data/templates/default/content/Planned%20Features.textile +30 -9
- data/templates/default/content/README.textile +3 -2
- data/templates/default/content/RSS%20feed.textile +1 -1
- data/templates/default/content/Recent%20changes%20to%20this%20site.textile +283 -0
- data/templates/default/content/SOKS%20features.textile +3 -0
- data/templates/default/content/Site%20Index.textile +202 -0
- data/templates/default/content/Soks%20Licence.textile +2 -0
- data/templates/default/content/Tag%3A%20Include%20this%20page%20in%20the%20distribution.textile +6 -0
- data/templates/default/start.rb +67 -123
- data/templates/default/version.txt +1 -1
- data/templates/default/views/Page_edit.rhtml +7 -7
- data/templates/default/views/{Page_search_results.rhtml → Page_find.rhtml} +9 -3
- 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 +1 -1
- data/templates/default/views/Page_revision.rhtml +39 -0
- data/templates/default/views/Page_revisions.rhtml +13 -5
- data/templates/default/views/Page_rss.rhtml +8 -8
- data/templates/default/views/Page_view.rhtml +3 -3
- data/templates/default/views/UploadPage_edit.rhtml +8 -8
- data/templates/default/views/frame.rhtml +8 -8
- data/templates/default/views/messages.yaml +1 -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/stress_url_calls.rb +33 -0
- data/test/stress_urls.txt +68 -0
- data/test/test_counter-helper.rb +158 -0
- data/test/test_soks-helper-maintenance.rb +106 -0
- data/test/test_soks-helpers.rb +104 -0
- data/test/test_soks-model.rb +144 -0
- data/test/test_soks-servlet.rb +231 -0
- data/test/test_soks-storage.rb +70 -31
- data/test/test_soks-utils.rb +112 -13
- data/test/test_soks-view.rb +141 -3
- metadata +38 -27
- data/templates/default/content/A%20page%20with%20an%20umlaut%20%F6%20in%20its%20title.textile +0 -1
- data/templates/default/content/All%20News.textile +0 -26
- data/templates/default/content/Bil%20Kleb.textile +0 -1
- data/templates/default/content/Bil.textile +0 -1
- data/templates/default/content/Bill%20Wood.textile +0 -3
- data/templates/default/content/Bug%3A%20RSS%20feed%20does%20not%20validate.textile +0 -10
- data/templates/default/content/Bug%3A%20Type%20a%20title%20here.textile +0 -31
- data/templates/default/content/Instructions%20and%20Howtos.textile +0 -21
- data/templates/default/content/Latest%20News.textile +0 -26
- data/templates/default/content/New%20Recent%20Changes%20class.textile +0 -68
- data/templates/default/content/New%20page%20templates%20or%20categories%20code.textile +0 -68
- data/templates/default/content/News%3A%20Version%200%2E0%2E6%20Released.textile +0 -13
- data/templates/default/content/Recent%20Blog%20Entries.textile +0 -5
- data/templates/default/content/Recent%20Changes%20to%20This%20Site.textile +0 -286
- data/templates/default/content/Ruby.textile +0 -9
- data/templates/default/content/Skorgu.textile +0 -3
- data/templates/default/content/ctrl%2Dn.textile +0 -1
- data/templates/default/content/let%20me%20know.textile +0 -1
- data/templates/default/content/sandbox.textile +0 -20
- data/templates/default/content/tamc.textile +0 -1
- data/templates/default/content/tamc2.textile +0 -1
|
@@ -3,7 +3,7 @@ require 'net/imap'
|
|
|
3
3
|
# From Dave Burt on comp.lang.ruby
|
|
4
4
|
class String
|
|
5
5
|
def from_quoted_printable
|
|
6
|
-
self.gsub(/\r\n/, "\n").unpack("M").first
|
|
6
|
+
self.gsub(/\r\n/, "\n").gsub(/=(?![\dA-F]{2})/,'=3D').unpack("M").first
|
|
7
7
|
end
|
|
8
8
|
end
|
|
9
9
|
|
|
@@ -14,20 +14,16 @@ class Message
|
|
|
14
14
|
def initialize( imap, message_id )
|
|
15
15
|
@imap, @message_id = imap, message_id
|
|
16
16
|
envelope = @imap.fetch( @message_id, 'ENVELOPE' ).first.attr['ENVELOPE']
|
|
17
|
-
@subject = envelope['subject']
|
|
18
|
-
@sender_name = envelope['from'].first['name']
|
|
17
|
+
@subject = envelope['subject'].gsub(/^(Fw|Re):?/i,'').strip
|
|
18
|
+
@sender_name = envelope['from'].first['name'].gsub(/@/,' at ')
|
|
19
19
|
@date = envelope['date']
|
|
20
|
-
@sender_email = envelope['from'].first['mailbox'] + '
|
|
20
|
+
@sender_email = envelope['from'].first['mailbox'] + ' at ' + envelope['from'].first['host']
|
|
21
|
+
@sender_name = @sender_email unless @sender_name && @sender_name.size > 1
|
|
21
22
|
@text = plain_text_content_from_message( message_id )
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def plain_text_content_from_message( id )
|
|
25
|
-
|
|
26
|
-
text
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def textile
|
|
30
|
-
text.gsub(/[\*-]{2,}/,'').gsub(/([^\n])\n([^\n])/,'\1\2').gsub(/^[ \t]+/,'')
|
|
26
|
+
@imap.fetch( id, 'BODY[1]' ).first.attr['BODY[1]'].from_quoted_printable
|
|
31
27
|
end
|
|
32
28
|
|
|
33
29
|
end
|
|
@@ -38,31 +34,29 @@ class Mail2WikiHelper
|
|
|
38
34
|
:server => 'imap.hermes.cam.ac.uk',
|
|
39
35
|
:username => 'tamc2',
|
|
40
36
|
:password => 'missing_a_password',
|
|
41
|
-
:mailbox => '
|
|
42
|
-
:
|
|
37
|
+
:mailbox => 'test',
|
|
38
|
+
:check_event => :hour,
|
|
43
39
|
:subject_regexp => /.*/,
|
|
40
|
+
:keyword => 'PutInWiki'
|
|
44
41
|
}
|
|
45
42
|
|
|
46
43
|
def initialize( wiki, settings = {} )
|
|
47
44
|
@settings = DEFAULT_SETTINGS.merge( settings )
|
|
48
45
|
@wiki = wiki
|
|
49
46
|
check_mailbox
|
|
50
|
-
|
|
51
|
-
loop do
|
|
52
|
-
sleep @settings[:check_period ]
|
|
53
|
-
check_mailbox
|
|
54
|
-
end
|
|
55
|
-
end
|
|
47
|
+
@wiki.watch_for(@settings[:check_event]) { check_mailbox }
|
|
56
48
|
end
|
|
57
49
|
|
|
58
50
|
private
|
|
59
51
|
|
|
60
52
|
def check_mailbox
|
|
53
|
+
$LOG.info "Checking #{@settings[:mailbox]} on #{@settings[:server]}"
|
|
61
54
|
login
|
|
62
55
|
select_mailbox
|
|
63
56
|
new_messages_for_wiki do |message_id|
|
|
64
57
|
this_message = Message.new( @imap, message_id )
|
|
65
58
|
if this_message.subject =~ @settings[:subject_regexp]
|
|
59
|
+
$LOG.info "Adding '#{this_message.subject}' to wiki"
|
|
66
60
|
add_message_to_wiki( this_message )
|
|
67
61
|
mark_as_added( message_id )
|
|
68
62
|
end
|
|
@@ -78,8 +72,10 @@ class Mail2WikiHelper
|
|
|
78
72
|
text = current_page.textile
|
|
79
73
|
end
|
|
80
74
|
text << "\n\n"
|
|
81
|
-
text << "#{message.date} from #{message.sender_name} #{message.sender_email}\n"
|
|
82
|
-
text <<
|
|
75
|
+
text << "*Copied from Email on #{message.date} from #{message.sender_name} (#{message.sender_email})*\n\n"
|
|
76
|
+
text << "<pre>\n"
|
|
77
|
+
text << message.text
|
|
78
|
+
text << "\n</pre>\n"
|
|
83
79
|
@wiki.revise(message.subject, text, message.sender_name )
|
|
84
80
|
end
|
|
85
81
|
|
|
@@ -98,11 +94,11 @@ class Mail2WikiHelper
|
|
|
98
94
|
end
|
|
99
95
|
|
|
100
96
|
def new_messages_for_wiki
|
|
101
|
-
@imap.search(
|
|
97
|
+
@imap.search("UNKEYWORD #{@settings[:keyword]}").each { |id| yield id }
|
|
102
98
|
end
|
|
103
99
|
|
|
104
100
|
def mark_as_added( id )
|
|
105
|
-
|
|
101
|
+
@imap.store( id, '+FLAGS', [@settings[:keyword]] )
|
|
106
102
|
end
|
|
107
103
|
|
|
108
104
|
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, maximum_revisions = 20 )
|
|
33
|
+
@wiki = wiki
|
|
34
|
+
@age_to_wipe_at = age_to_wipe_at
|
|
35
|
+
@maximum_revisions = maximum_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
|
|
@@ -7,19 +7,18 @@ class RSS2WikiHelper
|
|
|
7
7
|
DEFAULT_SETTINGS = {
|
|
8
8
|
:url => 'http://localhost:8000/rss/recent%20changes%20to%20this%20site',
|
|
9
9
|
:pagename => nil, # If nil, uses channel title,
|
|
10
|
-
:
|
|
10
|
+
:update_on_event => :hour,
|
|
11
11
|
:author => 'AutomaticRSS2Wiki',
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
def initialize( wiki, settings = {} )
|
|
15
15
|
@settings = DEFAULT_SETTINGS.merge( settings )
|
|
16
16
|
@wiki = wiki
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
end
|
|
17
|
+
update_rss
|
|
18
|
+
update_wiki
|
|
19
|
+
@wiki.watch_for(@settings[:update_on_event]) do
|
|
20
|
+
update_rss
|
|
21
|
+
update_wiki
|
|
23
22
|
end
|
|
24
23
|
end
|
|
25
24
|
|
|
@@ -36,7 +35,7 @@ class RSS2WikiHelper
|
|
|
36
35
|
end
|
|
37
36
|
|
|
38
37
|
def update_rss
|
|
39
|
-
$
|
|
38
|
+
$LOG.info "Updating feed"
|
|
40
39
|
open(@settings[:url]) do |http|
|
|
41
40
|
@rss = RSS::Parser.parse( http.read , false)
|
|
42
41
|
end
|
data/lib/soks-model.rb
CHANGED
|
@@ -2,8 +2,8 @@ require 'thread'
|
|
|
2
2
|
|
|
3
3
|
# Revision stores changes as a diff against the more recent content version
|
|
4
4
|
class Revision
|
|
5
|
-
attr_reader :number,
|
|
6
|
-
attr_reader
|
|
5
|
+
attr_reader :number, :author, :page
|
|
6
|
+
attr_reader :changes, :created_on
|
|
7
7
|
alias :revised_on :created_on
|
|
8
8
|
|
|
9
9
|
def initialize( page, number, changes, author, created_on = Time.now )
|
|
@@ -13,7 +13,7 @@ class Revision
|
|
|
13
13
|
# Recreates the content of the page AFTER this revision had been made.
|
|
14
14
|
# Done by recursively applying diffs to more recent versions.
|
|
15
15
|
def content
|
|
16
|
-
|
|
16
|
+
following_revision ? following_revision.previous_content : page.content
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
# Recreateds the content of the page BEFORE this revision had been made
|
|
@@ -21,8 +21,14 @@ class Revision
|
|
|
21
21
|
content.split("\n").unpatch!(@changes).join("\n")
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
26
32
|
|
|
27
33
|
def method_missing( symbol, *args )
|
|
28
34
|
raise(ArgumentError, "Revision does not respond to #{symbol}", caller) unless @page && @page.respond_to?( symbol )
|
|
@@ -41,10 +47,9 @@ class Page
|
|
|
41
47
|
|
|
42
48
|
# Returns an empty version of itself.
|
|
43
49
|
def self.empty( name )
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
empty = self.new( name )
|
|
51
|
+
empty.revise( $MESSAGES[:Type_what_you_want_here_and_click_save], "NoOne" )
|
|
46
52
|
class << empty
|
|
47
|
-
def textile; "[[#{$MESSAGES[:Create]} #{name} => /edit/#{name} ]]" end
|
|
48
53
|
def empty?; true; end
|
|
49
54
|
end
|
|
50
55
|
empty
|
|
@@ -60,15 +65,9 @@ class Page
|
|
|
60
65
|
# Revises the content of this page, creating a new revision class that stores the changes
|
|
61
66
|
def revise( new_content, author )
|
|
62
67
|
return nil if new_content == @content
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# @revisions[-1] = Revision.new( self, @revisions.length - 1, changes , author )
|
|
67
|
-
#else
|
|
68
|
-
changes = changes_between( @content, new_content )
|
|
69
|
-
return nil if changes.empty?
|
|
70
|
-
@revisions << Revision.new( self, @revisions.length, changes , author )
|
|
71
|
-
#end
|
|
68
|
+
changes = new_content.changes_from @content
|
|
69
|
+
return nil if changes.empty?
|
|
70
|
+
@revisions << Revision.new( self, @revisions.length, changes , author )
|
|
72
71
|
@content = new_content
|
|
73
72
|
@revisions.last
|
|
74
73
|
end
|
|
@@ -76,13 +75,13 @@ class Page
|
|
|
76
75
|
# Returns the content of this page to that of a previous version
|
|
77
76
|
def rollback( number, author )
|
|
78
77
|
revise( ( number < 0 ) ? $MESSAGES[:page_deleted] : @revisions[ number ].content, author )
|
|
79
|
-
end
|
|
78
|
+
end
|
|
80
79
|
|
|
81
80
|
def revision( number ) @revisions[ number ] end
|
|
82
81
|
|
|
83
82
|
def deleted?
|
|
84
83
|
( content =~ /^#{$MESSAGES[:page_deleted]}/i ) ||
|
|
85
|
-
( content =~ /^#{$MESSAGES[:content_moved_to]} /i )
|
|
84
|
+
( content =~ /^#{$MESSAGES[:content_moved_to]} /i ) ? true : false
|
|
86
85
|
end
|
|
87
86
|
|
|
88
87
|
def empty?; @revisions.empty? end
|
|
@@ -96,10 +95,10 @@ class Page
|
|
|
96
95
|
def is_inserted_into( page )
|
|
97
96
|
@links_lock.synchronize { @inserted_into << page unless @inserted_into.include? page }
|
|
98
97
|
end
|
|
99
|
-
|
|
100
|
-
def textile( content = @content ) content end
|
|
101
98
|
|
|
102
99
|
def name_for_index; name.downcase end
|
|
100
|
+
|
|
101
|
+
# Refactored changes_between into the String class in soks-utils
|
|
103
102
|
|
|
104
103
|
# Any unhandled calls are passed onto the latest revision (e.g. author, creation time etc)
|
|
105
104
|
def method_missing( symbol, *args )
|
|
@@ -109,20 +108,10 @@ class Page
|
|
|
109
108
|
raise ArgumentError,"Page does not respond to #{symbol}", caller
|
|
110
109
|
end
|
|
111
110
|
end
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def
|
|
116
|
-
old_content.split("\n").diff( new_content.split("\n") ).map { |changeset| changeset.map { |change| change.to_a } }
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# This is no longer used, because I've found it confuses people.
|
|
120
|
-
def the_same_author_recently_revised_this_page( author )
|
|
121
|
-
return false if empty?
|
|
122
|
-
return false unless author == self.author
|
|
123
|
-
((Time.now - self.revised_on) < (60*5) ) # 5 Minutes
|
|
124
|
-
end
|
|
125
|
-
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class EmptyPage < Page
|
|
114
|
+
def empty?; true; end
|
|
126
115
|
end
|
|
127
116
|
|
|
128
117
|
#Serves as a marker, so ImagePage and AttachmentPage can re-use the same view templates
|
|
@@ -130,43 +119,50 @@ class UploadPage < Page
|
|
|
130
119
|
end
|
|
131
120
|
|
|
132
121
|
class ImagePage < UploadPage
|
|
133
|
-
def textile( content = @content )
|
|
134
|
-
deleted? ? content : "!#{$SETTINGS[:url]}#{content.strip}!:#{$SETTINGS[:url]}/view/#{@name.url_encode}"
|
|
135
|
-
end
|
|
136
|
-
|
|
137
122
|
def name_for_index; @name[10..-1].strip.downcase end
|
|
138
123
|
end
|
|
139
124
|
|
|
140
125
|
class AttachmentPage < UploadPage
|
|
141
|
-
def textile( content = @content )
|
|
142
|
-
deleted? ? content : %Q{[[ #{name} => #{$SETTINGS[:url]}#{content} ]]\n}
|
|
143
|
-
end
|
|
144
126
|
def name_for_index; @name[ 9..-1].strip.downcase end
|
|
145
127
|
end
|
|
146
128
|
|
|
147
129
|
# This class has turned into a behmoth, need to refactor.
|
|
148
130
|
class Wiki
|
|
149
131
|
include WikiFlatFileStore
|
|
132
|
+
include WikiCacheStore
|
|
150
133
|
include Enumerable
|
|
151
134
|
include Notify # Will notify any watchers if underlying files change
|
|
135
|
+
|
|
136
|
+
attr_accessor :check_files_every
|
|
152
137
|
|
|
153
138
|
PAGE_CLASSES = [
|
|
154
139
|
[ /^picture of/i, ImagePage ],
|
|
155
140
|
[ /^attached/i, AttachmentPage ],
|
|
156
141
|
[ /.*/, Page ]
|
|
157
142
|
]
|
|
143
|
+
|
|
144
|
+
CACHE_NAME = 'pages'
|
|
158
145
|
|
|
159
|
-
def initialize(
|
|
160
|
-
@
|
|
161
|
-
@
|
|
146
|
+
def initialize( content_folder, cache_folder = nil )
|
|
147
|
+
@cache_folder = cache_folder
|
|
148
|
+
@folder = content_folder
|
|
149
|
+
@pages = load_cache(CACHE_NAME) || {}
|
|
162
150
|
@shutting_down = false
|
|
151
|
+
@check_files_every = nil
|
|
152
|
+
watch_for(:start) { start }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def start
|
|
163
156
|
load_all_pages
|
|
164
157
|
start_watching_files
|
|
158
|
+
setup_periodic_notifications
|
|
165
159
|
end
|
|
166
160
|
|
|
167
161
|
def shutdown
|
|
162
|
+
notify :shutdown
|
|
168
163
|
sleep(1) until event_queue.empty?
|
|
169
164
|
@shutting_down = true # Stop further modifications
|
|
165
|
+
save_cache(CACHE_NAME, @pages)
|
|
170
166
|
end
|
|
171
167
|
|
|
172
168
|
def page( name )
|
|
@@ -178,7 +174,7 @@ class Wiki
|
|
|
178
174
|
end
|
|
179
175
|
|
|
180
176
|
def exists?( name )
|
|
181
|
-
@pages.include?( name.downcase ) && !
|
|
177
|
+
@pages.include?( name.downcase ) && !page_named( name ).deleted?
|
|
182
178
|
end
|
|
183
179
|
|
|
184
180
|
def revise( pagename, content, author )
|
|
@@ -187,22 +183,43 @@ class Wiki
|
|
|
187
183
|
mutate( pagename ) { |page| page.revise( content, author ) }
|
|
188
184
|
end
|
|
189
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
|
+
|
|
190
193
|
def rollback( pagename, number, author )
|
|
191
194
|
raise "Sorry! Shutting down..." if @shutting_down
|
|
192
195
|
check_disk_for_updated_page pagename
|
|
193
196
|
mutate( pagename ) { |page| page.rollback( number, author ) }
|
|
194
197
|
end
|
|
195
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
|
+
|
|
196
212
|
private
|
|
197
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
|
+
|
|
198
220
|
def start_watching_files
|
|
199
|
-
return unless
|
|
200
|
-
|
|
201
|
-
loop do
|
|
202
|
-
sleep $SETTINGS[:check_files_every]
|
|
203
|
-
load_all_pages
|
|
204
|
-
end
|
|
205
|
-
end.priority = -5
|
|
221
|
+
return unless check_files_every
|
|
222
|
+
watch_for(check_files_every) { load_all_pages }
|
|
206
223
|
end
|
|
207
224
|
|
|
208
225
|
def new_page( name, initializer = :new )
|
|
@@ -216,8 +233,19 @@ class Wiki
|
|
|
216
233
|
page = page_named( pagename ) || new_page( pagename )
|
|
217
234
|
revision = nil
|
|
218
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
|
|
219
243
|
revision, dont_save = yield page
|
|
220
|
-
|
|
244
|
+
# Save page if required
|
|
245
|
+
if revision && dont_save != :dont_save
|
|
246
|
+
save page
|
|
247
|
+
add_page_to_index( page )
|
|
248
|
+
end
|
|
221
249
|
end
|
|
222
250
|
if revision
|
|
223
251
|
notify :page_revised, page, revision
|