ruby-web 1.1.1
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/ChangeLog +474 -0
- data/INSTALL.txt +9 -0
- data/InstalledFiles +180 -0
- data/LICENSE.txt +74 -0
- data/Rakefile +529 -0
- data/TODO +65 -0
- data/doc/additional.xml +149 -0
- data/doc/core.xml +652 -0
- data/doc/credits/index.xml +52 -0
- data/doc/credits/php.contributors.xml +118 -0
- data/doc/credits/php.language-snippets.ent +622 -0
- data/doc/install/index.xml +136 -0
- data/doc/install/mac/index.xml +21 -0
- data/doc/install/ruby-web.install.rb.instructions.xml +7 -0
- data/doc/install/unix/index.xml +46 -0
- data/doc/install/win/apache1.xml +166 -0
- data/doc/install/win/apache2.xml +141 -0
- data/doc/install/win/iis.xml +162 -0
- data/doc/install/win/index.xml +24 -0
- data/doc/install/win/installer.xml +31 -0
- data/doc/install/win/manual.xml +43 -0
- data/doc/manual.xml +69 -0
- data/doc/old/apache_cgi.txt +23 -0
- data/doc/old/fastcgi.txt +23 -0
- data/doc/old/mod_ruby.txt +21 -0
- data/doc/old/snippets.rdoc +183 -0
- data/doc/old/webrick.txt +23 -0
- data/doc/old/windows_cgi.txt +9 -0
- data/doc/tutorial.xml +14 -0
- data/doc/xsl/manual-multi.xsl +10 -0
- data/doc/xsl/manual-pdf.xsl +6 -0
- data/doc/xsl/manual-single.xsl +6 -0
- data/doc/xsl/manual.css +22 -0
- data/install.rb +1022 -0
- data/lib/formatter.rb +314 -0
- data/lib/html-parser.rb +429 -0
- data/lib/htmlrepair.rb +113 -0
- data/lib/htmlsplit.rb +842 -0
- data/lib/sgml-parser.rb +332 -0
- data/lib/web.rb +68 -0
- data/lib/web/assertinclude.rb +129 -0
- data/lib/web/config.rb +50 -0
- data/lib/web/connection.rb +1070 -0
- data/lib/web/convenience.rb +154 -0
- data/lib/web/formreader.rb +318 -0
- data/lib/web/htmlparser/html-parser.rb +429 -0
- data/lib/web/htmlparser/sgml-parser.rb +332 -0
- data/lib/web/htmltools/element.rb +296 -0
- data/lib/web/htmltools/stparser.rb +276 -0
- data/lib/web/htmltools/tags.rb +286 -0
- data/lib/web/htmltools/tree.rb +139 -0
- data/lib/web/htmltools/xmltree.rb +160 -0
- data/lib/web/htmltools/xpath.rb +71 -0
- data/lib/web/info.rb +63 -0
- data/lib/web/load.rb +210 -0
- data/lib/web/mime.rb +87 -0
- data/lib/web/phprb.rb +340 -0
- data/lib/web/resources/test/cookie.rb +33 -0
- data/lib/web/resources/test/counter.rb +20 -0
- data/lib/web/resources/test/multipart.rb +14 -0
- data/lib/web/resources/test/redirect.rb +8 -0
- data/lib/web/resources/test/stock.rb +33 -0
- data/lib/web/sapi/apache.rb +129 -0
- data/lib/web/sapi/fastcgi.rb +22 -0
- data/lib/web/sapi/install/apache.rb +180 -0
- data/lib/web/sapi/install/iis.rb +93 -0
- data/lib/web/sapi/install/macosx.rb +90 -0
- data/lib/web/sapi/webrick.rb +86 -0
- data/lib/web/session.rb +83 -0
- data/lib/web/shim/cgi.rb +129 -0
- data/lib/web/shim/rails.rb +175 -0
- data/lib/web/stringio.rb +78 -0
- data/lib/web/strscanparser.rb +24 -0
- data/lib/web/tagparser.rb +96 -0
- data/lib/web/testing.rb +666 -0
- data/lib/web/traceoutput.rb +75 -0
- data/lib/web/unit.rb +56 -0
- data/lib/web/upload.rb +59 -0
- data/lib/web/validate.rb +52 -0
- data/lib/web/wiki.rb +557 -0
- data/lib/web/wiki/linker.rb +72 -0
- data/lib/web/wiki/page.rb +201 -0
- data/lib/webunit.rb +27 -0
- data/lib/webunit/assert.rb +152 -0
- data/lib/webunit/converter.rb +154 -0
- data/lib/webunit/cookie.rb +118 -0
- data/lib/webunit/domwalker.rb +185 -0
- data/lib/webunit/exception.rb +14 -0
- data/lib/webunit/form.rb +116 -0
- data/lib/webunit/frame.rb +37 -0
- data/lib/webunit/htmlelem.rb +122 -0
- data/lib/webunit/image.rb +26 -0
- data/lib/webunit/jscript.rb +31 -0
- data/lib/webunit/link.rb +33 -0
- data/lib/webunit/params.rb +321 -0
- data/lib/webunit/parser.rb +229 -0
- data/lib/webunit/response.rb +464 -0
- data/lib/webunit/runtest.rb +41 -0
- data/lib/webunit/table.rb +148 -0
- data/lib/webunit/testcase.rb +45 -0
- data/lib/webunit/ui/cui/testrunner.rb +50 -0
- data/lib/webunit/utils.rb +68 -0
- data/lib/webunit/webunit.rb +28 -0
- data/test/dev/action.rb +83 -0
- data/test/dev/forms.rb +104 -0
- data/test/dev/forms2.rb +104 -0
- data/test/dev/parser.rb +17 -0
- data/test/dev/scripts/dump.rb +24 -0
- data/test/dev/scripts/makedist.rb +62 -0
- data/test/dev/scripts/uri.rb +41 -0
- data/test/dev/scripts/uri/common.rb +432 -0
- data/test/dev/scripts/uri/ftp.rb +149 -0
- data/test/dev/scripts/uri/generic.rb +1106 -0
- data/test/dev/scripts/uri/http.rb +76 -0
- data/test/dev/scripts/uri/https.rb +26 -0
- data/test/dev/scripts/uri/ldap.rb +238 -0
- data/test/dev/scripts/uri/mailto.rb +260 -0
- data/test/dev/scripts/urireg.rb +174 -0
- data/test/dev/simpledispatcher.rb +156 -0
- data/test/dev/test.action.rb +146 -0
- data/test/dev/test.formreader.rb +463 -0
- data/test/dev/test.simpledispatcher.rb +186 -0
- data/test/dev/webunit/conv/digit-0.rb +21 -0
- data/test/dev/webunit/conv/digit-1.rb +17 -0
- data/test/dev/webunit/conv/digit.rb +23 -0
- data/test/dev/webunit/conv/test_digit-0.rb +16 -0
- data/test/dev/webunit/conv/test_digit-1.rb +19 -0
- data/test/dev/webunit/conv/test_digit.rb +26 -0
- data/test/dev/webunit/conv/test_digit_view-0.rb +76 -0
- data/test/dev/webunit/conv/test_digit_view-1.rb +102 -0
- data/test/dev/webunit/conv/test_digit_view.rb +134 -0
- data/test/installation/htdocs/cgi_test.rb +296 -0
- data/test/installation/htdocs/test_install.rb +4 -0
- data/test/installation/runwebtest.rb +5 -0
- data/test/installation/test_cookie.rb +128 -0
- data/test/installation/test_form.rb +47 -0
- data/test/installation/test_multipart.rb +51 -0
- data/test/installation/test_request.rb +24 -0
- data/test/installation/test_response.rb +35 -0
- data/test/unit/htdocs/cookie.rb +32 -0
- data/test/unit/htdocs/multipart.rb +28 -0
- data/test/unit/htdocs/redirect.rb +12 -0
- data/test/unit/htdocs/simple.rb +13 -0
- data/test/unit/htdocs/stock.rb +33 -0
- data/test/unit/test_assert.rb +162 -0
- data/test/unit/test_cookie.rb +114 -0
- data/test/unit/test_domwalker.rb +77 -0
- data/test/unit/test_form.rb +42 -0
- data/test/unit/test_frame.rb +40 -0
- data/test/unit/test_htmlelem.rb +74 -0
- data/test/unit/test_image.rb +45 -0
- data/test/unit/test_jscript.rb +57 -0
- data/test/unit/test_link.rb +85 -0
- data/test/unit/test_multipart.rb +51 -0
- data/test/unit/test_params.rb +210 -0
- data/test/unit/test_parser.rb +53 -0
- data/test/unit/test_response.rb +150 -0
- data/test/unit/test_table.rb +70 -0
- data/test/unit/test_utils.rb +106 -0
- data/test/unit/test_webunit.rb +28 -0
- data/test/web/mod_ruby_stub.rb +39 -0
- data/test/web/test.assertinclude.rb +109 -0
- data/test/web/test.buffer.rb +182 -0
- data/test/web/test.code.loader.rb +78 -0
- data/test/web/test.config.rb +31 -0
- data/test/web/test.error.handling.rb +91 -0
- data/test/web/test.formreader-2.0.rb +352 -0
- data/test/web/test.load.rb +125 -0
- data/test/web/test.mime-type.rb +23 -0
- data/test/web/test.narf.cgi.rb +106 -0
- data/test/web/test.phprb.rb +239 -0
- data/test/web/test.request.rb +368 -0
- data/test/web/test.response.rb +637 -0
- data/test/web/test.ruby-web.rb +10 -0
- data/test/web/test.session.rb +50 -0
- data/test/web/test.shim.cgi.rb +96 -0
- data/test/web/test.tagparser.rb +65 -0
- data/test/web/test.template2.rb +297 -0
- data/test/web/test.testing2.rb +318 -0
- data/test/web/test.upload.rb +45 -0
- data/test/web/test.validate.rb +46 -0
- data/test/web/test.web.test.rb +495 -0
- data/test/wiki/test.history.rb +297 -0
- data/test/wiki/test.illustration_page.rb +287 -0
- data/test/wiki/test.linker.rb +197 -0
- data/test/wiki/test.tarpit.rb +56 -0
- data/test/wiki/test.wiki.rb +300 -0
- data/test/wikitestroot/admin.rb +7 -0
- data/test/wikitestroot/wiki.rb +6 -0
- metadata +234 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# TODO: trace output is woefully broken
|
|
2
|
+
|
|
3
|
+
# TODO: formerly on Web::close:
|
|
4
|
+
# if Web::display_trace
|
|
5
|
+
# trace_output
|
|
6
|
+
# end
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
module Web
|
|
10
|
+
# Web::display_trace controls the display of trace variables
|
|
11
|
+
@@display_trace = false
|
|
12
|
+
class << self
|
|
13
|
+
attr_accessor :display_trace
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
TRACE_STYLESHEET=<<-EOF
|
|
17
|
+
<style type="text/css">
|
|
18
|
+
span.tracecontent { background-color:white; color:black;font: 10pt verdana, arial; }
|
|
19
|
+
span.tracecontent table { font: 10pt verdana, arial; cellspacing:0; cellpadding:0; margin-bottom:25}
|
|
20
|
+
span.tracecontent tr.subhead { background-color:cccccc;}
|
|
21
|
+
span.tracecontent th { padding:0,3,0,3 }
|
|
22
|
+
span.tracecontent th.alt { background-color:black; color:white; padding:3,3,2,3; }
|
|
23
|
+
span.tracecontent td { padding:0,3,0,3 }
|
|
24
|
+
span.tracecontent tr.alt { background-color:eeeeee }
|
|
25
|
+
span.tracecontent h1 { font: 24pt verdana, arial; margin:0,0,0,0}
|
|
26
|
+
span.tracecontent h2 { font: 18pt verdana, arial; margin:0,0,0,0}
|
|
27
|
+
span.tracecontent h3 { font: 12pt verdana, arial; margin:0,0,0,0}
|
|
28
|
+
span.tracecontent th a { color:darkblue; font: 8pt verdana, arial; }
|
|
29
|
+
span.tracecontent a { color:darkblue;text-decoration:none }
|
|
30
|
+
span.tracecontent a:hover { color:darkblue;text-decoration:underline; }
|
|
31
|
+
span.tracecontent div.outer { width:90%; margin:15,15,15,15}
|
|
32
|
+
span.tracecontent table.viewmenu td { background-color:006699; color:white; padding:0,5,0,5; }
|
|
33
|
+
span.tracecontent table.viewmenu td.end { padding:0,0,0,0; }
|
|
34
|
+
span.tracecontent table.viewmenu a {color:white; font: 8pt verdana, arial; }
|
|
35
|
+
span.tracecontent table.viewmenu a:hover {color:white; font: 8pt verdana, arial; }
|
|
36
|
+
span.tracecontent a.tinylink {color:darkblue; font: 8pt verdana, arial;text-decoration:underline;}
|
|
37
|
+
span.tracecontent a.link {color:darkblue; text-decoration:underline;}
|
|
38
|
+
span.tracecontent div.buffer {padding-top:7; padding-bottom:17;}
|
|
39
|
+
span.tracecontent .small { font: 8pt verdana, arial }
|
|
40
|
+
span.tracecontent table td { padding-right:20 }
|
|
41
|
+
span.tracecontent table td.nopad { padding-right:5 }
|
|
42
|
+
</style>
|
|
43
|
+
EOF
|
|
44
|
+
|
|
45
|
+
class Connection
|
|
46
|
+
def Connection.trace_output_template
|
|
47
|
+
template = <<-EOF
|
|
48
|
+
<div>
|
|
49
|
+
<h1>Request Details</h1><br>
|
|
50
|
+
<table cellspacing="0" cellpadding="0" border="1" style="width:100%;border-collapse:collapse;">
|
|
51
|
+
<tr>
|
|
52
|
+
<th class="alt" align="Left" colspan=2><h3><b>Request Parameters</b></h3></th></tr>
|
|
53
|
+
<narf:foreach from=parameters item=parameter>
|
|
54
|
+
<tr><th width=150>{$parameter.key}</th><td>{$parameter.value}</td></tr>
|
|
55
|
+
</narf:foreach>
|
|
56
|
+
</table>
|
|
57
|
+
<br>
|
|
58
|
+
<table cellspacing="0" cellpadding="0" border="1" style="width:100%;border-collapse:collapse;">
|
|
59
|
+
<tr><th class="alt" align="Left" colspan=2><h3><b>Cookies</b></h3></th></tr>
|
|
60
|
+
<narf:foreach from=cookies item=cookie>
|
|
61
|
+
<tr><th width=150>{$cookie.key}</th><td>{$cookie.value}</td></tr>
|
|
62
|
+
</narf:foreach>
|
|
63
|
+
</table>
|
|
64
|
+
<br>
|
|
65
|
+
<table cellspacing="0" cellpadding="0" border="1" style="width:100%;border-collapse:collapse;">
|
|
66
|
+
<tr><th class="alt" align="Left" colspan=2><h3><b>Session</b></h3></th></tr>
|
|
67
|
+
<narf:foreach from=session item=sessionitem>
|
|
68
|
+
<tr><th width=150>{$sessionitem.key}</th><td>{$sessionitem.value}</td></tr>
|
|
69
|
+
</table>
|
|
70
|
+
</div>
|
|
71
|
+
EOF
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
data/lib/web/unit.rb
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require 'web'
|
|
2
|
+
require 'webunit/webunit'
|
|
3
|
+
|
|
4
|
+
module Web
|
|
5
|
+
module Unit
|
|
6
|
+
include WebUnit
|
|
7
|
+
include WebUnit::Utils
|
|
8
|
+
attr_reader :response
|
|
9
|
+
|
|
10
|
+
def setup_webunit
|
|
11
|
+
@urlbase = ENV['URLBASE'] ? ENV['URLBASE'] : 'http://localhost/'
|
|
12
|
+
@urlbase = orthop_url( @urlbase )
|
|
13
|
+
@urlbase << '/' unless @urlbase =~ %r!/$!
|
|
14
|
+
$stderr.puts "URLBASE is #{@urlbase}" if $DEBUG
|
|
15
|
+
$URLBASE = @urlbase
|
|
16
|
+
WebUnit::Response::reset
|
|
17
|
+
@cookies_kept = WebUnit::Cookies::instance.cookies
|
|
18
|
+
Web::cookie::clear
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def update_cookies
|
|
22
|
+
Web::cookie = Hash.new
|
|
23
|
+
Cookies.instance.cookies.each do |k, v|
|
|
24
|
+
Web::cookie[k.first] = v['value']
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def reverse_update_cookies
|
|
29
|
+
Cookies::clear
|
|
30
|
+
Web::cookie.each do |k,v|
|
|
31
|
+
Cookies::add( k, v )
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def do_request( url, params={} )
|
|
36
|
+
reverse_update_cookies
|
|
37
|
+
url = @urlbase + url unless (url.index(@urlbase) == 0)
|
|
38
|
+
@response = Response::get( url )
|
|
39
|
+
update_cookies
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def do_submit( params={} )
|
|
43
|
+
reverse_update_cookies
|
|
44
|
+
form = @response.form
|
|
45
|
+
params.each do |k,v|
|
|
46
|
+
form.params[k].value = v
|
|
47
|
+
end
|
|
48
|
+
@response = form.submit
|
|
49
|
+
update_cookies
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def assert_title( expected, document=@response )
|
|
53
|
+
assert_equal( expected, document.title )
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
data/lib/web/upload.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require 'tempfile'
|
|
2
|
+
|
|
3
|
+
module Web
|
|
4
|
+
# == Purpose
|
|
5
|
+
# This class delegates to the uploaded Tempfile, and adds
|
|
6
|
+
# content type and original filename attributes.
|
|
7
|
+
#
|
|
8
|
+
# If you are testing a multipart/form, use this class
|
|
9
|
+
# to pretend you uploaded a file:
|
|
10
|
+
#
|
|
11
|
+
# "uploaded_file" => Upload.new(anIO,
|
|
12
|
+
# "image/png",
|
|
13
|
+
# "my_favorite_image.png" )
|
|
14
|
+
#
|
|
15
|
+
# See Web::Testing for more information.
|
|
16
|
+
#
|
|
17
|
+
class Upload
|
|
18
|
+
attr_reader :content_type, :original_filename
|
|
19
|
+
|
|
20
|
+
def initialize( tempfile, content_type, original_filename )
|
|
21
|
+
if (tempfile.is_a? String)
|
|
22
|
+
contents = File.open( tempfile, "r" ) { |f| f.read }
|
|
23
|
+
tempfile = Tempfile.new("Web")
|
|
24
|
+
tempfile.binmode
|
|
25
|
+
tempfile.write contents
|
|
26
|
+
tempfile.rewind
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@content_type = content_type
|
|
30
|
+
@original_filename = original_filename
|
|
31
|
+
@tempfile = tempfile
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def local_path
|
|
35
|
+
@tempfile.path
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# use this method to move an upload somewhere you care about
|
|
39
|
+
def save( filename )
|
|
40
|
+
File.open( filename, "w" ) { |f|
|
|
41
|
+
f.binmode
|
|
42
|
+
f.write( self.read )
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# how do I get the contents of an upload?
|
|
47
|
+
def read
|
|
48
|
+
@tempfile.rewind
|
|
49
|
+
@tempfile.read
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# how do I get in IO object from an upload?
|
|
53
|
+
def open
|
|
54
|
+
@tempfile.rewind
|
|
55
|
+
yield @tempfile
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
end
|
data/lib/web/validate.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module Web
|
|
2
|
+
def Web::rule( params = {} )
|
|
3
|
+
Web::ruleset ||= Array.new
|
|
4
|
+
Web::ruleset.push Rule.new( params )
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def Web::validate
|
|
8
|
+
problems = Array.new
|
|
9
|
+
Web::ruleset.each do |r|
|
|
10
|
+
unless Web[r.field] =~ r.regexp
|
|
11
|
+
problems.push r
|
|
12
|
+
raise Web::FatalValidationError.new(r) if (r.malformed == :fatal)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
problems
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Connection
|
|
19
|
+
attr_accessor :ruleset
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class Web::FatalValidationError < Error
|
|
23
|
+
attr_reader :rule
|
|
24
|
+
def initialize( rule )
|
|
25
|
+
@rule = rule
|
|
26
|
+
super( rule.message )
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class Rule
|
|
31
|
+
attr_reader :field, :regexp, :message, :malformed
|
|
32
|
+
def initialize( params = {} )
|
|
33
|
+
[:field, :regexp].each do |k|
|
|
34
|
+
unless params.has_key? k
|
|
35
|
+
raise ArgumentError.new( "Web::rule is missing required parameter (:#{k})" )
|
|
36
|
+
else
|
|
37
|
+
self.instance_variable_set("@#{k}".intern, params[k] )
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
[:message, :malformed ].each do |k|
|
|
42
|
+
if params.has_key? k
|
|
43
|
+
self.instance_variable_set("@#{k}".intern, params[k] )
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
@field = @field.to_s
|
|
48
|
+
@malformed ||= :continue
|
|
49
|
+
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/web/wiki.rb
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
require 'web'
|
|
2
|
+
require 'web/wiki/linker'
|
|
3
|
+
require 'web/wiki/page'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
require 'ftools'
|
|
6
|
+
require 'strscan'
|
|
7
|
+
require 'ipaddr'
|
|
8
|
+
|
|
9
|
+
module Web
|
|
10
|
+
# == Purpose
|
|
11
|
+
# This wiki exists to:
|
|
12
|
+
# * Serve http://www.narf-lib.org
|
|
13
|
+
# * Test the utility of features in the NARF toolkit, and serve
|
|
14
|
+
# as an example of how to build a decent-sized application
|
|
15
|
+
# with NARF
|
|
16
|
+
# * Be a nice, useful wiki; aka UseMod with HTMLArea integration
|
|
17
|
+
# and image uploads
|
|
18
|
+
#
|
|
19
|
+
# == Usage
|
|
20
|
+
# This is the quickest way to embed the narf wiki:
|
|
21
|
+
#
|
|
22
|
+
# #!/usr/bin/env ruby
|
|
23
|
+
# require 'web/wiki'
|
|
24
|
+
# Web::process{
|
|
25
|
+
# Web::Wiki::handle_request()
|
|
26
|
+
# }
|
|
27
|
+
#
|
|
28
|
+
# You will enjoy your wiki more, however, by fiddling with
|
|
29
|
+
# a few config settings. Here what a production wiki.rb might
|
|
30
|
+
# look like:
|
|
31
|
+
#
|
|
32
|
+
# #!/usr/bin/env ruby
|
|
33
|
+
# require 'web/wiki'
|
|
34
|
+
#
|
|
35
|
+
# Web::process{
|
|
36
|
+
# # this line allows me to overload the default templates
|
|
37
|
+
# Web::config[:load_path] << "../"
|
|
38
|
+
#
|
|
39
|
+
# # here I set a number of config variables.
|
|
40
|
+
# Web::Wiki::set_pref( :store_url => "/pages/",
|
|
41
|
+
# :store_dir => "../pages/",
|
|
42
|
+
# :home_page => "NarfRocks",
|
|
43
|
+
# :resourceurl => "/resources" )
|
|
44
|
+
#
|
|
45
|
+
# # finally, I handle the request
|
|
46
|
+
# Web::Wiki.handle_request
|
|
47
|
+
# }
|
|
48
|
+
#
|
|
49
|
+
# The default templates for the NARF wiki are located in
|
|
50
|
+
#
|
|
51
|
+
# site_lib/web/wiki/resources/
|
|
52
|
+
#
|
|
53
|
+
# Append your template dir to the :load_path and
|
|
54
|
+
# only worry about overriding the templates you care about.
|
|
55
|
+
# Chances are, you'll care about these two templates:
|
|
56
|
+
#
|
|
57
|
+
# template.html:: the overall template
|
|
58
|
+
# home.html:: the template for the homepage
|
|
59
|
+
#
|
|
60
|
+
# The following preferences can make a difference for
|
|
61
|
+
# your wiki:
|
|
62
|
+
#
|
|
63
|
+
# store_dir::
|
|
64
|
+
# Web::Wiki uses YAML to save pages. By default,
|
|
65
|
+
# these pages will be saved in "./pages/".
|
|
66
|
+
# store_url::
|
|
67
|
+
# If you make your store files publicly accessible,
|
|
68
|
+
# Web::Wiki can let the webserver handle any the download
|
|
69
|
+
# of any upload page assets.
|
|
70
|
+
# home_page::
|
|
71
|
+
# The default home page for the wiki is HomePage. Set the
|
|
72
|
+
# HomePage to something meaningful in your wiki.
|
|
73
|
+
# resourceurl::
|
|
74
|
+
# By default, the NARF wiki will download various resource files
|
|
75
|
+
# from sitelib/web/wiki/resources. This makes installation easy,
|
|
76
|
+
# but a single visit to the edit page will incur the hit of
|
|
77
|
+
# executing ruby for every single image. Make these files
|
|
78
|
+
# accessible online and tell NARF where to find them, and
|
|
79
|
+
# things should run more quickly.
|
|
80
|
+
#
|
|
81
|
+
# == Credits
|
|
82
|
+
# Web::Wiki must extend its thanks to these very, very useful projects:
|
|
83
|
+
#
|
|
84
|
+
# YAML:: http://yaml4r.sourceforge.net/
|
|
85
|
+
# HTMLArea:: http://sourceforge.net/projects/itools-htmlarea/
|
|
86
|
+
#
|
|
87
|
+
class Wiki
|
|
88
|
+
|
|
89
|
+
# It seems nicer to define the class
|
|
90
|
+
# methods on an object and
|
|
91
|
+
# delegate a few class methods
|
|
92
|
+
def Wiki.method_missing( symbol, *args )
|
|
93
|
+
if [:handle_request,
|
|
94
|
+
:store_dir,
|
|
95
|
+
:store_url,
|
|
96
|
+
:store_basedir,
|
|
97
|
+
:more_news,
|
|
98
|
+
:page_list,
|
|
99
|
+
:open_permissions ].include? symbol
|
|
100
|
+
wiki.send symbol, *args
|
|
101
|
+
else
|
|
102
|
+
super( symbol, args )
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
@@wiki = nil
|
|
107
|
+
def Wiki.wiki
|
|
108
|
+
@@wiki ||= Wiki.new
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def Wiki.wipe #:nodoc:
|
|
112
|
+
@@wiki = nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
@@preferences = nil
|
|
116
|
+
def Wiki.preferences
|
|
117
|
+
@@preferences ||= {"baseurl" => Web::script_name,
|
|
118
|
+
"resourceurl" => "{$baseurl}/resources",
|
|
119
|
+
"store_dir" => "./pages/",
|
|
120
|
+
"tarpit_dir" => "./tarpit/",
|
|
121
|
+
"resource_dir" => File.join( File.dirname( __FILE__ ),
|
|
122
|
+
"wiki",
|
|
123
|
+
"resources" ),
|
|
124
|
+
"vandals" => "vandals.txt",
|
|
125
|
+
"home_page" => "HomePage",
|
|
126
|
+
"home_template" => "home.html",
|
|
127
|
+
"allowed_actions" => Wiki::Request.actions.keys
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def Wiki.pref( key )
|
|
132
|
+
key = key.to_s unless (key.kind_of? String)
|
|
133
|
+
pref = Wiki.preferences[key]
|
|
134
|
+
while (pref =~ /\{\$([^\}]*)\}/)
|
|
135
|
+
variable = $1
|
|
136
|
+
pref.gsub!( /\{\$#{variable}\}/, Wiki.pref( variable.strip ) || "")
|
|
137
|
+
end
|
|
138
|
+
pref
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def Wiki.set_pref( new_prefs={} )
|
|
142
|
+
new_prefs.each{ |key,value|
|
|
143
|
+
key = key.to_s unless (key.kind_of? String)
|
|
144
|
+
Wiki.preferences[key] = value
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
#-------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
module Store #:nodoc:
|
|
150
|
+
|
|
151
|
+
def move_asset( from, filename )
|
|
152
|
+
historical = File.expand_path(filename)
|
|
153
|
+
i=0
|
|
154
|
+
while( File.exists? historical )
|
|
155
|
+
i += 1
|
|
156
|
+
historical = File.dirname( historical ) + "/\#" + i.to_s + "." + File.basename( historical ).gsub( /^\#\d*\./, "" )
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
File.move( filename, historical ) if File.exists? filename
|
|
160
|
+
|
|
161
|
+
if (from.is_a? Web::Upload)
|
|
162
|
+
from.save(filename)
|
|
163
|
+
else
|
|
164
|
+
File.move( from, filename )
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def page_list
|
|
169
|
+
Dir[store_dir + "/*.yaml"].entries.collect { |e|
|
|
170
|
+
File.basename( e, ".yaml" ).gsub( /-slash-/, "/" )
|
|
171
|
+
}
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def store( name )
|
|
175
|
+
File.join( store_dir, name.gsub(/\//, "-slash-") + ".yaml" )
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def load_page( name = Web["page.name"] )
|
|
179
|
+
if name.size == 0
|
|
180
|
+
if (Web.path_info)
|
|
181
|
+
name = Web.path_info.gsub( Regexp.new(Web.script_name), "" ).gsub(/^\/|\.html$/,"")
|
|
182
|
+
end
|
|
183
|
+
if name.size == 0
|
|
184
|
+
name = Web::Wiki::pref( :home_page )
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
page = ""
|
|
189
|
+
page_file = store( name )
|
|
190
|
+
if File.exists? page_file
|
|
191
|
+
File.open( page_file, "r" ) { |f|
|
|
192
|
+
page = YAML.load( f )
|
|
193
|
+
}
|
|
194
|
+
else
|
|
195
|
+
page = Web::Wiki::Page.new( name )
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
{/\\r/ => "\r",
|
|
199
|
+
/\\n/ => "\n",
|
|
200
|
+
/\\"/ => "\"",
|
|
201
|
+
/\\'/ => "'", }.each{ |find, replace|
|
|
202
|
+
page.content.gsub!( find, replace )
|
|
203
|
+
}
|
|
204
|
+
page
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def news
|
|
208
|
+
more_news[0..4]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def more_news
|
|
212
|
+
if File.exists?(store_dir + "/more_news.yaml")
|
|
213
|
+
contents = File.open( store_dir + "/more_news.yaml" ) { |f| f.read }
|
|
214
|
+
unless contents.empty?
|
|
215
|
+
YAML.load(contents)
|
|
216
|
+
else
|
|
217
|
+
[ ]
|
|
218
|
+
end
|
|
219
|
+
else
|
|
220
|
+
[ ]
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def recent
|
|
225
|
+
if File.exists?(store_dir + "/recent.yaml")
|
|
226
|
+
contents = File.open( store_dir + "/recent.yaml" ) { |f| f.read }
|
|
227
|
+
unless contents.empty?
|
|
228
|
+
YAML.load(contents)
|
|
229
|
+
else
|
|
230
|
+
[ ]
|
|
231
|
+
end
|
|
232
|
+
else
|
|
233
|
+
[ ]
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def save( page )
|
|
238
|
+
# save a slim version of the page for recent changes
|
|
239
|
+
rcpage = page.clone
|
|
240
|
+
rcpage.content = ""
|
|
241
|
+
rcpage.history = [ ]
|
|
242
|
+
|
|
243
|
+
rc = more_news
|
|
244
|
+
rc = rc.unshift(rcpage) unless rcpage.comment.empty?
|
|
245
|
+
rc.pop if rc.size > Page::max_revisions
|
|
246
|
+
yaml = rc.to_yaml
|
|
247
|
+
File.open( store_dir + "/more_news.yaml", "w" ) { |f|
|
|
248
|
+
f.write(yaml)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
rc = recent
|
|
252
|
+
rc = rc.unshift(rcpage)
|
|
253
|
+
|
|
254
|
+
recent_page_names = [ ]
|
|
255
|
+
rc.delete_if do |p|
|
|
256
|
+
if recent_page_names.include? p.name
|
|
257
|
+
true
|
|
258
|
+
else
|
|
259
|
+
recent_page_names.push p.name
|
|
260
|
+
false
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
rc.pop if rc.size > Page::max_revisions
|
|
264
|
+
yaml = rc.to_yaml
|
|
265
|
+
File.open( store_dir + "/recent.yaml", "w" ) do |f|
|
|
266
|
+
f.write(yaml)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
comment = page.comment
|
|
270
|
+
# clear out comment now that we've saved more news
|
|
271
|
+
page.comment = ""
|
|
272
|
+
yaml = page.to_yaml
|
|
273
|
+
File.open( store( page.name ), "w" ) { |f|
|
|
274
|
+
f.write(yaml)
|
|
275
|
+
}
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def store_basedir
|
|
280
|
+
if ( vandal? )
|
|
281
|
+
Wiki.pref( "tarpit_dir" )
|
|
282
|
+
else
|
|
283
|
+
Wiki.pref( "store_dir" )
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def store_dir
|
|
288
|
+
unless (File.exists? store_dirname)
|
|
289
|
+
File.makedirs( store_dirname)
|
|
290
|
+
end
|
|
291
|
+
store_dirname
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def store_url
|
|
295
|
+
Wiki::pref( :store_url ) || Wiki::pref( :store_dir )
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def store_dirname
|
|
299
|
+
unless ( store_basedir =~ /^(\w:)?\// )
|
|
300
|
+
File.expand_path( Dir.pwd + "/" + self.store_basedir )
|
|
301
|
+
else
|
|
302
|
+
store_basedir
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def vandal?
|
|
307
|
+
vandal = false
|
|
308
|
+
vandals.each do |pattern|
|
|
309
|
+
if pattern =~ ENV["REMOTE_ADDR"]
|
|
310
|
+
vandal = true
|
|
311
|
+
break
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
vandal
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def vandals
|
|
318
|
+
@vandals ||= if File.exists?( Wiki.pref("vandals") || "" )
|
|
319
|
+
File.open( Wiki.pref("vandals") ) { |f|
|
|
320
|
+
f.to_a
|
|
321
|
+
}.collect{ |line|
|
|
322
|
+
/^#{line.chomp.strip.gsub(/"/, '')}/
|
|
323
|
+
}
|
|
324
|
+
else
|
|
325
|
+
[ ]
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def open_permissions
|
|
330
|
+
require 'find'
|
|
331
|
+
Find.find( store_dir ) { |name|
|
|
332
|
+
begin
|
|
333
|
+
if File.directory? name
|
|
334
|
+
File.chmod( 0777, name )
|
|
335
|
+
else
|
|
336
|
+
File.chmod( 0666, name )
|
|
337
|
+
end
|
|
338
|
+
rescue Exception
|
|
339
|
+
# this should be more specific to access errors
|
|
340
|
+
end
|
|
341
|
+
}
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
include Wiki::Store
|
|
347
|
+
|
|
348
|
+
#-------------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
def handle_request
|
|
351
|
+
Request.new( self ).handle_request
|
|
352
|
+
#unless (Wiki.pref("added_exit_handler"))
|
|
353
|
+
#
|
|
354
|
+
# Wiki.set_pref("added_exit_handler" => true )
|
|
355
|
+
#end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
class Request # :nodoc:
|
|
359
|
+
|
|
360
|
+
attr_accessor :template, :content_template, :wiki
|
|
361
|
+
|
|
362
|
+
def initialize( wiki )
|
|
363
|
+
@page = @handled = false
|
|
364
|
+
@wiki = wiki
|
|
365
|
+
@template = if (File.basename(Web.script_name || '') == "admin.rb")
|
|
366
|
+
"admin.html"
|
|
367
|
+
else
|
|
368
|
+
"template.html"
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
attr_writer :page
|
|
373
|
+
def page
|
|
374
|
+
unless @page
|
|
375
|
+
@page = self.wiki.load_page
|
|
376
|
+
unless (Web["revision"] == nil || Web["revision"].empty? || Web["revision"].to_s == "0")
|
|
377
|
+
history = @page.history
|
|
378
|
+
revision = history.find{ |p|
|
|
379
|
+
Web["revision"].to_i == p.revision
|
|
380
|
+
}
|
|
381
|
+
revision.history = history
|
|
382
|
+
@page = revision || @page
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
@page
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
attr_writer :vars
|
|
389
|
+
def vars
|
|
390
|
+
@vars ||= {"page" => self.page,
|
|
391
|
+
"wiki" => self.wiki,
|
|
392
|
+
"action" => Web["action"],
|
|
393
|
+
"baseurl" => Web::Wiki::pref(:baseurl),
|
|
394
|
+
"resourceurl" => Web::Wiki::pref(:resourceurl),
|
|
395
|
+
}
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def handle_request
|
|
399
|
+
cmd = Web["action"]
|
|
400
|
+
cmd = "default" if cmd.empty? || cmd == "view_revision"
|
|
401
|
+
cmd = "download_resource" if Web::path_info =~ /^\/resources\//
|
|
402
|
+
cmd = "default" unless Web::Wiki::pref("allowed_actions").include?(cmd)
|
|
403
|
+
|
|
404
|
+
Request.actions[cmd].call(self)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
unless @handled
|
|
408
|
+
Web::config[:load_path] << Web::Wiki::pref("resource_dir")
|
|
409
|
+
self.vars["content_template"] = self.content_template
|
|
410
|
+
Web::local.merge!( self.vars )
|
|
411
|
+
Web::load( self.template ) if self.template
|
|
412
|
+
@handled = true
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
@@actions = {}
|
|
417
|
+
def Request.actions
|
|
418
|
+
@@actions
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def Request.action( name, &action )
|
|
422
|
+
Action.new name, &action
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class Action #:nodoc:
|
|
427
|
+
attr_accessor :name
|
|
428
|
+
def initialize( name, &action )
|
|
429
|
+
@name = name
|
|
430
|
+
@action = action
|
|
431
|
+
Request.actions[name] = self
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def call( request )
|
|
435
|
+
if (self.name == "default")
|
|
436
|
+
request.content_template = request.page.template
|
|
437
|
+
else
|
|
438
|
+
request.content_template = self.name + ".html"
|
|
439
|
+
end
|
|
440
|
+
@action.call(request) if @action
|
|
441
|
+
if ( [ "illustration.html",
|
|
442
|
+
"images.html" ].include? request.content_template )
|
|
443
|
+
request.template = request.content_template
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
[ "default", "page_history", "images", "more_news" ].each{ |boring_action|
|
|
449
|
+
action( boring_action )
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
#action( "more_news_rss_0.91" ) {|r|
|
|
453
|
+
# r.template = "more_news_rss_0.91.html"
|
|
454
|
+
#}
|
|
455
|
+
|
|
456
|
+
action( "edit" ) { |r|
|
|
457
|
+
r.page.comment = "";
|
|
458
|
+
r.vars["options"] = {
|
|
459
|
+
"content_editor" => case Web::env["http_user_agent"]
|
|
460
|
+
when /MSIE.*Windows/
|
|
461
|
+
"ie-content_editor.html"
|
|
462
|
+
when /Mozilla.*(?!MSIE).*Windows/
|
|
463
|
+
"ekit-content_editor.html"
|
|
464
|
+
else
|
|
465
|
+
"default-content_editor.html"
|
|
466
|
+
end,
|
|
467
|
+
"page_types" => ["Normal", "Illustration"],
|
|
468
|
+
"align" => ["left", "center", "right"],
|
|
469
|
+
"valign" => ["top", "middle", "bottom"],
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
def download_file( basedir, requested_asset )
|
|
474
|
+
self.template = nil
|
|
475
|
+
self.content_template = nil
|
|
476
|
+
|
|
477
|
+
basedir = File.expand_path( basedir )
|
|
478
|
+
requested_asset = File.expand_path( File.join(basedir,
|
|
479
|
+
requested_asset.gsub( /\\/, "/") ) )
|
|
480
|
+
# security check on the requested_asset --
|
|
481
|
+
# it must be underneath the basedir
|
|
482
|
+
if ( requested_asset.index( basedir ) == 0 && \
|
|
483
|
+
File.exists?( requested_asset ) )
|
|
484
|
+
# deliver the file
|
|
485
|
+
Web.content_type = Web.get_mime_type( requested_asset )
|
|
486
|
+
Web.write File.open(requested_asset, "r" ) { |f|
|
|
487
|
+
f.read
|
|
488
|
+
}
|
|
489
|
+
Web.flush
|
|
490
|
+
else
|
|
491
|
+
Web.status = "404";
|
|
492
|
+
Web.write "404 File Not Found"
|
|
493
|
+
Web.flush
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
action( "download" ) { |r|
|
|
498
|
+
r.download_file( r.page.dir,Web["asset"] )
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
action("download_resource") { |r|
|
|
502
|
+
r.download_file( Wiki::pref(:resource_dir),
|
|
503
|
+
Web::path_info.gsub( /^\/resources/, '' ) )
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
["select_illustration", "insert_download"].each{ |images_target|
|
|
509
|
+
action(images_target) { |r|
|
|
510
|
+
r.template = "images.html"
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
action( "Upload" ) { |r|
|
|
515
|
+
r.content_template = "images.html"
|
|
516
|
+
r.vars["action"] = Web["calling_action"]
|
|
517
|
+
Dir.mkdir r.page.dir unless (File.exists? r.page.dir)
|
|
518
|
+
r.wiki.move_asset( Web["upload"],
|
|
519
|
+
File.join( r.page.dir,
|
|
520
|
+
File.basename(Web["upload"] \
|
|
521
|
+
.original_filename \
|
|
522
|
+
.gsub( /\\/, "/") ) ) )
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
action( "asset_history" ) { |r|
|
|
526
|
+
r.vars["asset"] = { "name" => Web["asset"],
|
|
527
|
+
"history" => r.page.historical_assets[Web["asset"]] }
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
action( "Save" ) { |r|
|
|
531
|
+
# patrick's (seemed simpler at the moment)
|
|
532
|
+
# comment out if you want to try the
|
|
533
|
+
# simple dispatcher code below
|
|
534
|
+
r.page.set_by_request
|
|
535
|
+
r.wiki.save( r.page )
|
|
536
|
+
|
|
537
|
+
# redirect back to default
|
|
538
|
+
Web::request["action"] = ["default"]
|
|
539
|
+
Web::clear()
|
|
540
|
+
r.handle_request
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
action( "delete_asset" ) { |r|
|
|
546
|
+
r.content_template = "images.html"
|
|
547
|
+
r.vars["action"] = Web["calling_action"]
|
|
548
|
+
r.wiki.move_asset( File.join( r.page.dir, File.basename( Web["asset"] ) ),
|
|
549
|
+
File.join( r.page.dir, File.basename( "\#deleted." + Web["asset"] ) ) )
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
Wiki = Web::Wiki
|