2 # Copyright (C) 2001-2013
3 # Alex Schroeder <alex@gnu.org>
4 # Copyleft 2008 Brian Curry <http://www.raiazome.com>
5 # ... including lots of patches from the UseModWiki site
6 # Copyright (C) 2001, 2002 various authors
7 # ... which was based on UseModWiki version 0.92 (April 21, 2001)
8 # Copyright (C) 2000, 2001 Clifford A. Adams
9 # <caadams@frontiernet.net> or <usemod@usemod.com>
10 # ... which was based on the GPLed AtisWiki 0.3
11 # Copyright (C) 1998 Markus Denker <marcus@ira.uka.de>
12 # ... which was based on the LGPLed CVWiki CVS-patches
13 # Copyright (C) 1997 Peter Merel
14 # ... and The Original WikiWikiWeb
15 # Copyright (C) 1996, 1997 Ward Cunningham <ward@c2.com>
16 # (code reused with permission)
18 # This program is free software: you can redistribute it and/or modify it under
19 # the terms of the GNU General Public License as published by the Free Software
20 # Foundation, either version 3 of the License, or (at your option) any later
23 # This program is distributed in the hope that it will be useful, but WITHOUT
24 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
25 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
27 # You should have received a copy of the GNU General Public License along with
28 # this program. If not, see <http://www.gnu.org/licenses/>.
32 use utf8
; # in case anybody ever addes UTF8 characters to the source
34 use CGI
::Carp
qw(fatalsToBrowser);
35 use File
::Glob
':glob';
36 local $| = 1; # Do not buffer output (localized for mod_perl)
39 use vars
qw($RssLicense $RssCacheHours @RcDays $TempDir $LockDir $DataDir
40 $KeepDir $PageDir $RcOldFile $IndexFile $BannedContent $NoEditFile $BannedHosts
41 $ConfigFile $FullUrl $SiteName $HomePage $LogoUrl $RcDefault $RssDir
42 $IndentLimit $RecentTop $RecentLink $EditAllowed $UseDiff $KeepDays $KeepMajor
43 $EmbedWiki $BracketText $UseConfig $UseLookup $AdminPass $EditPass $NetworkFile
44 $BracketWiki $FreeLinks $WikiLinks $SummaryHours $FreeLinkPattern $RCName
45 $RunCGI $ShowEdits $LinkPattern $RssExclude $InterLinkPattern $MaxPost $UseGrep
46 $UrlPattern $UrlProtocols $ImageExtensions $InterSitePattern $FS $CookieName
47 $SiteBase $StyleSheet $NotFoundPg $FooterNote $NewText $EditNote $UserGotoBar
48 $VisitorFile $RcFile %Smilies %SpecialDays $InterWikiMoniker $SiteDescription
49 $RssImageUrl $ReadMe $RssRights $BannedCanRead $SurgeProtection $TopLinkBar
50 $LanguageLimit $SurgeProtectionTime $SurgeProtectionViews $DeletedPage
51 %Languages $InterMap $ValidatorLink %LockOnCreation $RssStyleSheet
52 %CookieParameters @UserGotoBarPages $NewComment $HtmlHeaders $StyleSheetPage
53 $ConfigPage $ScriptName $CommentsPrefix @UploadTypes $AllNetworkFiles
54 $UsePathInfo $UploadAllowed $LastUpdate $PageCluster %PlainTextPages
55 $RssInterwikiTranslate $UseCache $Counter $ModuleDir $FullUrlPattern
56 $SummaryDefaultLength $FreeInterLinkPattern %InvisibleCookieParameters
57 %AdminPages $UseQuestionmark $JournalLimit $LockExpiration $RssStrip
58 %LockExpires @IndexOptions @Debugging $DocumentHeader %HtmlEnvironmentContainers
59 @MyAdminCode @MyFooters @MyInitVariables @MyMacros @MyMaintenance @MyRules
60 $Favicon @EditorGotoBarPages $EditorGotoBar @ExploreGotoBarPages $ExploreGotoBar);
63 use vars qw(%Page %InterSite %IndexHash %Translate %OldCookie $FootnoteNumber
64 $OpenPageName @IndexList $Message $q $Now %RecentVisitors @HtmlStack
65 @HtmlAttrStack $ReplaceForm %MyInc $CollectingJournal $bol $WikiDescription
66 $PrintedHeader %Locks $Fragment @Blocks @Flags $Today @KnownLocks
67 $ModulesDescription %Action %RuleOrder %Includes %RssInterwikiTranslate);
69 # Can be set outside the script: $DataDir, $UseConfig, $ConfigFile, $ModuleDir,
70 # $ConfigPage, $AdminPass, $EditPass, $ScriptName, $FullUrl, $RunCGI.
72 # 1 = load config file in the data directory
73 $UseConfig = 1 unless defined $UseConfig;
76 $DataDir = $ENV{WikiDataDir} if $UseConfig and not $DataDir;
77 $DataDir = '/tmp/oddmuse' unless $DataDir; # FIXME: /var/opt/oddmuse/wiki ?
78 $ConfigPage = '' unless $ConfigPage; # config page
80 # 1 = Run script as CGI instead of loading as module
81 $RunCGI = 1 unless defined $RunCGI;
83 # 1 = allow page views using wiki.pl/PageName
86 # -1 = disabled, 0 = 10s; 1 = partial HTML cache; 2 = HTTP/1.1 caching
89 $SiteName = 'Wiki'; # Name of site (used for titles)
90 $HomePage = 'HomePage'; # Home page
91 $CookieName = 'Wiki'; # Name for this wiki (for multi-wiki sites)
93 $SiteBase = ''; # Full URL for <BASE> header
94 $MaxPost = 1024 * 210; # Maximum 210K posts (about 200K for pages)
95 $StyleSheet = ''; # URL for CSS stylesheet (like '/wiki.css')
96 $StyleSheetPage = 'css'; # Page for CSS sheet
97 $LogoUrl = ''; # URL for site logo ('' for no logo)
98 $Favicon = ''; # URL for favicon ('' for no icon)
99 $NotFoundPg = ''; # Page for not-found links ('' for blank pg)
101 $NewText = "This page is empty.\n"; # New page text
102 $NewComment = "Add your comment here.\n"; # New comment text
104 $EditAllowed = 1; # 0 = no, 1 = yes, 2 = comments pages only, 3 = comments only
105 $AdminPass = '' unless defined $AdminPass; # Whitespace separated passwords.
106 $EditPass = '' unless defined $EditPass; # Whitespace separated passwords.
108 $BannedHosts = 'BannedHosts'; # Page for banned hosts
109 $BannedCanRead = 1; # 1 = banned cannot edit, 0 = banned cannot read
110 $BannedContent = 'BannedContent'; # Page for banned content (usually for link-ban)
111 $WikiLinks = 1; # 1 = LinkPattern is a link
112 $FreeLinks = 1; # 1 = [[some text]] is a link
113 $UseQuestionmark = 1; # 1 = append questionmark to links to nonexisting pages
114 $BracketText = 1; # 1 = [URL desc] uses a description for the URL
115 $BracketWiki = 1; # 1 = [WikiLink desc] uses a desc for the local link
116 $NetworkFile = 1; # 1 = file: is a valid protocol for URLs
117 $AllNetworkFiles = 0; # 1 = file:///foo is allowed -- the default allows only file://foo
118 $InterMap = 'InterMap'; # name of the intermap page, '' = disable
119 $RssInterwikiTranslate = 'RssInterwikiTranslate'; # name of RSS interwiki translation page, '' = disable
120 $ENV{PATH} = '/bin:/usr/bin'; # Path used to find 'diff' and 'grep'
121 $UseDiff = 1; # 1 = use diff
122 $UseGrep = 1; # 1 = use grep to speed up searches
123 $SurgeProtection = 1; # 1 = protect against leeches
124 $SurgeProtectionTime = 20; # Size of the protected window in seconds
125 $SurgeProtectionViews = 10; # How many page views to allow in this window
126 $DeletedPage = 'DeletedPage'; # Pages starting with this can be deleted
127 $RCName = 'RecentChanges'; # Name of changes page
128 @RcDays = qw(1 3 7 30 90); # Days for links on RecentChanges
129 $RcDefault = 30; # Default number of RecentChanges days
130 $KeepDays = 14; # Days to keep old revisions
131 $KeepMajor = 1; # 1 = keep at least one major rev when expiring pages
132 $SummaryHours = 4; # Hours to offer the old subject when editing a page
133 $SummaryDefaultLength = 150; # Length of default text for summary (0 to disable)
134 $ShowEdits = 0; # 1 = major and show minor edits in recent changes
135 $UseLookup = 1; # 1 = lookup host names instead of using only IP numbers
136 $RecentTop = 1; # 1 = most recent entries at the top of the list
137 $RecentLink = 1; # 1 = link to usernames
138 $PageCluster = ''; # name of cluster page, eg. 'Cluster' to enable
139 $InterWikiMoniker = ''; # InterWiki prefix for this wiki for RSS
140 $SiteDescription = ''; # RSS Description of this wiki
141 $RssStrip = '^\d\d\d\d-\d\d-\d\d_'; # Regexp to strip from feed item titles
142 $RssImageUrl = $LogoUrl; # URL to image to associate with your RSS feed
143 $RssRights = ''; # Copyright notice for RSS, usually an URL to the appropriate text
144 $RssExclude = 'RssExclude'; # name of the page that lists pages to be excluded from the feed
145 $RssCacheHours = 1; # How many hours to cache remote RSS files
146 $RssStyleSheet = ''; # External style sheet for RSS files
147 $UploadAllowed = 0; # 1 = yes, 0 = administrators only
148 @UploadTypes = ('image/jpeg', # MIME types allowed, all allowed if empty list
150 'image/vnd.microsoft.icon');
151 $EmbedWiki = 0; # 1 = no headers/footers
152 $FooterNote = ''; # HTML for bottom of every page
153 $EditNote = ''; # HTML notice above buttons on edit page
154 $TopLinkBar = 1; # Pick and choose what gets displayed on top
155 # Summary of useful values.
156 # +-------+---------+---------+------+
157 # | Value | EditBar | GotoBar | Motd |
158 # |-------+---------+---------+------|
159 # | 0 | No | No | No |
160 # | 1 | No | Yes | Yes |
161 # | 2 | Yes | No | Yes |
162 # | 3 | Yes | Yes | Yes |
163 # +-------+---------+---------+------+
164 @UserGotoBarPages = (); # List of pagenames
165 $UserGotoBar = ''; # HTML added to end of goto bar
166 @EditorGotoBarPages = (); # List of pagenames available only to editors
167 $EditorGotoBar = ''; # HTML added to end of editor goto bar
168 @ExploreGotoBarPages = (); # List of pagenames available only to editors
169 $ExploreGotoBar = ''; # HTML added to end of editor goto bar
170 $ValidatorLink = 0; # 1 = Link to the W3C HTML validator service
171 $CommentsPrefix = ''; # prefix for comment pages, eg. 'Comments_on_' to enable
172 $HtmlHeaders = ''; # Additional stuff to put in the HTML <head> section
173 $IndentLimit = 20; # Maximum depth of nested lists
174 $LanguageLimit = 3; # Number of matches req. for each language
175 $JournalLimit = 200; # how many pages can be collected in one go?
176 $DocumentHeader = qq(<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.0 Strict//EN")
177 . qq( "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n)
178 . qq(<html xmlns
="http://www.w3.org/1999/xhtml">);
179 # Checkboxes at the end of the index.
180 @IndexOptions = (['pages', T
('Include normal pages'), 1, \
&AllPagesList
]);
181 # Display short comments below the GotoBar for special days
182 # Example: %SpecialDays = ('1-1' => 'New Year', '1-2' => 'Next Day');
184 # Replace regular expressions with inlined images
185 # Example: %Smilies = (":-?D(?=\\W)" => '/pics/grin.png');
187 # Detect page languages when saving edits
188 # Example: %Languages = ('de' => '\b(der|die|das|und|oder)\b');
190 @KnownLocks = qw(main diff index merge visitors); # locks to remove
191 $LockExpiration = 60; # How long before expirable locks are expired
192 %LockExpires = (diff
=>1, index=>1, merge
=>1, visitors
=>1); # locks to expire after some time
193 %CookieParameters = (username
=>'', pwd
=>'', homepage
=>'', theme
=>'', css
=>'', msg
=>'',
194 lang
=>'', toplinkbar
=>$TopLinkBar, embed
=>$EmbedWiki, );
195 %InvisibleCookieParameters = (msg
=>1, pwd
=>1,);
196 %Action = (rc
=> \
&BrowseRc
, rollback
=> \
&DoRollback
,
197 browse
=> \
&BrowseResolvedPage
, maintain
=> \
&DoMaintain
,
198 random
=> \
&DoRandom
, pagelock
=> \
&DoPageLock
,
199 history
=> \
&DoHistory
, editlock
=> \
&DoEditLock
,
200 edit
=> \
&DoEdit
, version
=> \
&DoShowVersion
,
201 download
=> \
&DoDownload
, rss
=> \
&DoRss
,
202 unlock
=> \
&DoUnlock
, password
=> \
&DoPassword
,
203 index => \
&DoIndex
, admin
=> \
&DoAdminPage
,
204 clear
=> \
&DoClearCache
, debug
=> \
&DoDebug
,
205 contrib
=> \
&DoContributors
, more
=> \
&DoJournal
);
206 @MyRules = (\
&LinkRules
, \
&ListRule
); # don't set this variable, add to it!
207 %RuleOrder = (\
&LinkRules
=> 0, \
&ListRule
=> 0);
209 # The 'main' program, called at the end of this script file (aka. as handler)
213 if (not $BannedCanRead and UserIsBanned
() and not UserIsEditor
()) {
214 ReportError
(T
('Reading not allowed: user, ip, or network is blocked.'), '403 FORBIDDEN',
215 0, $q->p(ScriptLink
('action=password', T
('Login'), 'password')));
220 sub ReportError
{ # fatal!
221 my ($errmsg, $status, $log, @html) = @_;
222 InitRequest
(); # make sure we can report errors before InitRequest
223 print GetHttpHeader
('text/html', 'nocache', $status), GetHtmlHeader
(T
('Error')),
224 $q->start_div({class=>"error"}), $q->h1(QuoteHtml
($errmsg)), @html, $q->end_div,
225 $q->end_html, "\n\n"; # newlines for FCGI because of exit()
226 WriteStringToFile
("$TempDir/error", '<body>' . $q->h1("$status $errmsg") . $q->Dump) if $log;
227 map { ReleaseLockDir
($_); } keys %Locks;
232 binmode(STDOUT
, ':utf8'); # this is where the HTML gets printed
233 binmode(STDERR
, ':utf8'); # just in case somebody prints debug info to stderr
235 $FS = "\x1e"; # The FS character is the RECORD SEPARATOR control char in ASCII
236 $Message = ''; # Warnings and non-fatal errors.
237 InitLinkPatterns
(); # Link pattern can be changed in config files
238 InitModules
(); # Modules come first so that users can change module variables in config
239 InitConfig
(); # Config comes as early as possible; remember $q is not available here
240 InitRequest
(); # get $q with $MaxPost; set these in the config file
241 InitCookie
(); # After InitRequest, because $q is used
242 InitVariables
(); # After config, to change variables, after InitCookie for GetParam
246 if ($UseConfig and $ModuleDir and -d
$ModuleDir) {
247 foreach my $lib (bsd_glob
("$ModuleDir/*.p[ml]")) {
248 do $lib unless $MyInc{$lib};
249 $MyInc{$lib} = 1; # Cannot use %INC in mod_perl settings
250 $Message .= CGI
::p
("$lib: $@") if $@
; # no $q exists, yet
256 if ($UseConfig and $ConfigFile and not $INC{$ConfigFile} and -f
$ConfigFile) {
257 do $ConfigFile; # these options must be set in a wrapper script or via the environment
258 $Message .= CGI
::p
("$ConfigFile: $@") if $@
; # remember, no $q exists, yet
260 if ($ConfigPage) { # $FS and $MaxPost must be set in config file!
261 my ($status, $data) = ReadFile
(GetPageFile
(FreeToNormal
($ConfigPage)));
262 my %data = ParseData
($data); # before InitVariables so GetPageContent won't work
263 eval $data{text
} if $data{text
};
264 $Message .= CGI
::p
("$ConfigPage: $@") if $@
;
269 utf8
::decode
($DataDir); # just in case, eg. "WikiDataDir=/tmp/Zürich♥ perl wiki.pl"
270 $PageDir = "$DataDir/page"; # Stores page data
271 $KeepDir = "$DataDir/keep"; # Stores kept (old) page data
272 $TempDir = "$DataDir/temp"; # Temporary files and locks
273 $LockDir = "$TempDir/lock"; # DB is locked if this exists
274 $NoEditFile = "$DataDir/noedit"; # Indicates that the site is read-only
275 $RcFile = "$DataDir/rc.log"; # New RecentChanges logfile
276 $RcOldFile = "$DataDir/oldrc.log"; # Old RecentChanges logfile
277 $IndexFile = "$DataDir/pageidx"; # List of all pages
278 $VisitorFile = "$DataDir/visitors.log"; # List of recent visitors
279 $RssDir = "$DataDir/rss"; # For rss feed cache
280 $ReadMe = "$DataDir/README"; # file with default content for the HomePage
281 # Config file with Perl code to execute
282 $ConfigFile = "$DataDir/config" unless $ConfigFile;
283 # For extensions (ending in .pm or .pl)
284 $ModuleDir = "$DataDir/modules" unless $ModuleDir;
287 sub InitRequest
{ # set up $q
288 $CGI::POST_MAX
= $MaxPost;
289 $q = new CGI
unless $q;
292 sub InitVariables
{ # Init global session variables for mod_perl!
293 $WikiDescription = $q->p($q->a({-href
=>'http://www.oddmuse.org/'}, 'Oddmuse'),
294 $Counter++ > 0 ? Ts
('%s calls', $Counter) : '');
295 $WikiDescription .= $ModulesDescription if $ModulesDescription;
296 $PrintedHeader = 0; # Error messages don't print headers unless necessary
297 $ReplaceForm = 0; # Only admins may search and replace
298 $ScriptName = $q->url() unless defined $ScriptName; # URL used in links
299 $FullUrl = $ScriptName unless $FullUrl; # URL used in forms
304 %RecentVisitors = ();
305 $OpenPageName = ''; # Currently open page
306 my $add_space = $CommentsPrefix =~ /[ \t_]$/;
307 map { $$_ = FreeToNormal
($$_); } # convert spaces to underscores on all configurable pagenames
308 (\
$HomePage, \
$RCName, \
$BannedHosts, \
$InterMap, \
$StyleSheetPage, \
$CommentsPrefix,
309 \
$ConfigPage, \
$NotFoundPg, \
$RssInterwikiTranslate, \
$BannedContent, \
$RssExclude, );
310 $CommentsPrefix .= '_' if $add_space;
311 @UserGotoBarPages = ($HomePage) unless @UserGotoBarPages;
312 @ExploreGotoBarPages = ($RCName) unless @ExploreGotoBarPages;
313 my @pages = sort($BannedHosts, $StyleSheetPage, $ConfigPage, $InterMap,
314 $RssInterwikiTranslate, $BannedContent);
315 %AdminPages = map { $_ => 1} @pages, $RssExclude unless %AdminPages;
316 %LockOnCreation = map { $_ => 1} @pages unless %LockOnCreation;
317 %PlainTextPages = ($BannedHosts => 1, $BannedContent => 1,
318 $StyleSheetPage => 1, $ConfigPage => 1) unless %PlainTextPages;
319 delete $PlainTextPages{''}; # $ConfigPage and others might be empty.
320 CreateDir
($DataDir); # Create directory if it doesn't exist
321 $Now = time; # Reset in case script is persistent
322 my $ts = (stat($IndexFile))[9]; # always stat for multiple server processes
323 ReInit
() if not $ts or $LastUpdate != $ts; # reinit if another process changed files (requires $DataDir)
325 unshift(@MyRules, \
&MyRules
) if defined(&MyRules
) && (not @MyRules or $MyRules[0] != \
&MyRules
);
326 @MyRules = sort {$RuleOrder{$a} <=> $RuleOrder{$b}} @MyRules; # default is 0
327 ReportError
(Ts
('Cannot create %s', $DataDir) . ": $!", '500 INTERNAL SERVER ERROR')
329 foreach my $sub (@MyInitVariables) {
331 $Message .= $q->p($@
) if $@
;
335 sub ReInit
{ # init everything we need if we want to link to stuff
336 my $id = shift; # when saving a page, what to do depends on the page being saved
337 AllPagesList
() if not $id;
338 InterInit
() if $InterMap and (not $id or $id eq $InterMap);
339 %RssInterwikiTranslate = () if not $id or $id eq $RssInterwikiTranslate; # special since rarely used
343 undef $q->{'.cookies'}; # Clear cache if it exists (for SpeedyCGI)
344 my $cookie = $q->cookie($CookieName);
345 %OldCookie = split(/$FS/o, UrlDecode
($cookie));
346 my %provided = map { $_ => 1 } $q->param;
347 for my $key (keys %OldCookie) {
348 SetParam
($key, $OldCookie{$key}) unless $provided{$key};
354 sub CookieUsernameFix
{
355 # Only valid usernames get stored in the new cookie.
356 my $name = GetParam
('username', '');
357 $q->delete('username');
360 } elsif ($WikiLinks && !$FreeLinks && !($name =~ /^$LinkPattern$/o)) {
361 $Message .= $q->p(Ts
('Invalid UserName %s: not saved.', $name));
362 } elsif ($FreeLinks && (!($name =~ /^$FreeLinkPattern$/o))) {
363 $Message .= $q->p(Ts
('Invalid UserName %s: not saved.', $name));
364 } elsif (length($name) > 50) { # Too long
365 $Message .= $q->p(T
('UserName must be 50 characters or less: not saved'));
367 SetParam
('username', $name);
371 sub CookieRollbackFix
{
372 my @rollback = grep(/rollback-(\d+)/, $q->param);
373 if (@rollback and $rollback[0] =~ /(\d+)/) {
375 $q->delete('action');
376 SetParam
('action', 'rollback');
381 my ($name, $default) = @_;
382 utf8
::encode
($name); # turn to byte string
383 my $result = $q->param($name);
384 $result = $default unless defined($result);
385 return QuoteHtml
($result); # you need to unquote anything that can have <tags>
389 my ($name, $val) = @_;
390 $q->param($name, $val);
393 sub InitLinkPatterns
{
394 my ($WikiWord, $QDelim);
395 $QDelim = '(?:"")?'; # Optional quote delimiter (removed from the output)
396 $WikiWord = '[A-Z]+[a-z\x{0080}-\x{fffd}]+[A-Z][A-Za-z\x{0080}-\x{fffd}]*'; # exclude noncharacters FFFE and FFFF
397 $LinkPattern = "($WikiWord)$QDelim";
398 $FreeLinkPattern = "([-,.()'%&?;<> _1-9A-Za-z\x{0080}-\x{fffd}]|[-,.()'%&?;<> _0-9A-Za-z\x{0080}-\x{fffd}][-,.()'%&?;<> _0-9A-Za-z\x{0080}-\x{fffd}]+)"; # disallow "0" and must match HTML and plain text (ie. > and >)
399 # Intersites must start with uppercase letter to avoid confusion with URLs.
400 $InterSitePattern = '[A-Z\x{0080}-\x{fffd}]+[A-Za-z\x{0080}-\x{fffd}]+';
401 $InterLinkPattern = "($InterSitePattern:[-a-zA-Z0-9\x{0080}-\x{fffd}_=!?#\$\@~`\%&*+\\/:;.,]*[-a-zA-Z0-9\x{0080}-\x{fffd}_=#\$\@~`\%&*+\\/])$QDelim";
402 $FreeInterLinkPattern = "($InterSitePattern:[-a-zA-Z0-9\x{0080}-\x{fffd}_=!?#\$\@~`\%&*+\\/:;.,()' ]+)"; # plus space and other characters, and no restrictions on the end of the pattern
403 $UrlProtocols = 'http|https|ftp|afs|news|nntp|mid|cid|mailto|wais|prospero|telnet|gopher|irc|feed';
404 $UrlProtocols .= '|file' if $NetworkFile;
405 my $UrlChars = '[-a-zA-Z0-9/@=+$_~*.,;:?!\'"()&#%]'; # see RFC 2396
406 my $EndChars = '[-a-zA-Z0-9/@=+$_~*]'; # no punctuation at the end of the url.
407 $UrlPattern = "((?:$UrlProtocols):$UrlChars+$EndChars)";
408 $FullUrlPattern="((?:$UrlProtocols):$UrlChars+)"; # when used in square brackets
409 $ImageExtensions = '(gif|jpg|png|bmp|jpeg|svg)';
414 return 0 unless defined($block); # "0" must print
415 return 1 if $block eq ''; # '' is the result of a dirty rule
420 sub Dirty
{ # arg 1 is the raw text; the real output must be printed instead
421 if ($Fragment ne '') {
422 $Fragment =~ s
|<p
>\s
*</p
>||g
; # clean up extra paragraphs (see end of ApplyRules)
424 push(@Blocks, $Fragment);
427 push(@Blocks, (shift));
433 # locallinks: apply rules that create links depending on local config (incl. interlink!)
434 my ($text, $locallinks, $withanchors, $revision, @tags) = @_; # $revision is used for images
435 $text =~ s/\r\n/\n/g; # DOS to Unix
436 $text =~ s/\n+$//g; # No trailing paragraphs
437 $text =~ s/^\n+//g; # No leading paragraphs
438 return unless $text ne ''; # allow the text '0'
439 local $Fragment = ''; # the clean HTML fragment not yet on @Blocks
440 local @Blocks=(); # the list of cached HTML blocks
441 local @Flags=(); # a list for each block, 1 = dirty, 0 = clean
442 Clean
(join('', map { AddHtmlEnvironment
($_) } @tags));
443 if ($OpenPageName and $PlainTextPages{$OpenPageName}) { # there should be no $PlainTextPages{''}
444 Clean
(CloseHtmlEnvironments
() . $q->pre($text));
445 } elsif (my ($type) = TextIsFile
($text)) {
446 Clean
(CloseHtmlEnvironments
() . $q->p(T
('This page contains an uploaded file:'))
447 . $q->p(GetDownloadLink
($OpenPageName, (substr($type, 0, 6) eq 'image/'), $revision)));
449 my $smileyregex = join "|", keys %Smilies;
450 $smileyregex = qr/(?=$smileyregex)/;
454 # Block level elements should eat trailing empty lines to prevent empty p elements.
455 if ($bol && m/\G(\s*\n)+/cg) {
456 Clean
(CloseHtmlEnvironments
() . AddHtmlEnvironment
('p'));
457 } elsif ($bol && m/\G(\<include(\s+(text|with-anchors))?\s+"(.*)"\>[ \t]*\n?)/cgi) {
458 # <include "uri..."> includes the text of the given URI verbatim
459 Clean
(CloseHtmlEnvironments
());
461 my ($oldpos, $old_, $type, $uri) = ((pos), $_, $3, UnquoteHtml
($4)); # remember, page content is quoted!
462 if ($uri =~ /^($UrlProtocols):/o) {
463 if ($type eq 'text') {
464 print $q->pre({class=>"include $uri"}, QuoteHtml
(GetRaw
($uri)));
465 } else { # never use local links for remote pages, with a starting tag
466 print $q->start_div({class=>"include $uri"});
467 ApplyRules
(QuoteHtml
(GetRaw
($uri)), 0, ($type eq 'with-anchors'), undef, 'p');
471 $Includes{$OpenPageName} = 1;
472 local $OpenPageName = FreeToNormal
($uri);
473 if ($type eq 'text') {
474 print $q->pre({class=>"include $OpenPageName"}, QuoteHtml
(GetPageContent
($OpenPageName)));
475 } elsif (not $Includes{$OpenPageName}) { # with a starting tag, watch out for recursion
476 print $q->start_div({class=>"include $OpenPageName"});
477 ApplyRules
(QuoteHtml
(GetPageContent
($OpenPageName)), $locallinks, $withanchors, undef, 'p');
479 delete $Includes{$OpenPageName};
481 print $q->p({-class=>'error'}, $q->strong(Ts
('Recursive include of %s!', $OpenPageName)));
484 Clean
(AddHtmlEnvironment
('p')); # if dirty block is looked at later, this will disappear
485 ($_, pos) = ($old_, $oldpos); # restore \G (assignment order matters!)
486 } elsif ($bol && m/\G(\<journal(\s+(\d*))?(\s+"(.*?)")?(\s+(reverse|past|future))?(\s+search\s+(.*))?\>[ \t]*\n?)/cgi) {
487 # <journal 10 "regexp"> includes 10 pages matching regexp
488 Clean
(CloseHtmlEnvironments
());
490 my ($oldpos, $old_) = (pos, $_); # remember these because of the call to PrintJournal()
491 PrintJournal
($3, $5, $7, 0, $9); # no offset
492 Clean
(AddHtmlEnvironment
('p')); # if dirty block is looked at later, this will disappear
493 ($_, pos) = ($old_, $oldpos); # restore \G (assignment order matters!)
494 } elsif ($bol && m/\G(\<rss(\s+(\d*))?\s+(.*?)\>[ \t]*\n?)/cgis) {
495 # <rss "uri..."> stores the parsed RSS of the given URI
496 Clean
(CloseHtmlEnvironments
());
498 my ($oldpos, $old_) = (pos, $_); # remember these because of the call to RSS()
499 print RSS
($3 ?
$3 : 15, split(/\s+/, UnquoteHtml
($4)));
500 Clean
(AddHtmlEnvironment
('p')); # if dirty block is looked at later, this will disappear
501 ($_, pos) = ($old_, $oldpos); # restore \G (assignment order matters!)
502 } elsif (/\G(<search (.*?)>)/cgis) {
504 Clean
(CloseHtmlEnvironments
());
506 my ($oldpos, $old_) = (pos, $_);
507 print $q->start_div({-class=>'search'});
508 SearchTitleAndBody
($2, \
&PrintSearchResult
, SearchRegexp
($2));
510 Clean
(AddHtmlEnvironment
('p')); # if dirty block is looked at later, this will disappear
511 ($_, pos) = ($old_, $oldpos); # restore \G (assignment order matters!)
512 } elsif ($bol && m/\G(<<<<<<< )/cg) {
513 my ($str, $count, $limit, $oldpos) = ($1, 0, 100, pos);
514 while (m/\G(.*\n)/cg and $count++ < $limit) {
516 last if (substr($1, 0, 29) eq '>>>>>>> ');
518 if ($count >= $limit) {
519 pos = $oldpos; # reset because we did not find a match
520 Clean
('<<<<<<< ');
522 Clean
(CloseHtmlEnvironments
() . $q->pre({-class=>'conflict'}, $str) . AddHtmlEnvironment
('p'));
524 } elsif ($bol and m/\G#REDIRECT/cg) {
526 } elsif (%Smilies && m/\G$smileyregex/cog && Clean
(SmileyReplace
())) {
527 } elsif (Clean
(RunMyRules
($locallinks, $withanchors))) {
528 } elsif (m/\G\s*\n(\s*\n)+/cg) { # paragraphs: at least two newlines
529 Clean
(CloseHtmlEnvironments
() . AddHtmlEnvironment
('p')); # another one like this further up
530 } elsif (m/\G&([A-Za-z]+|#[0-9]+|#x[A-Za-f0-9]+);/cg) { # entity references
532 } elsif (m/\G\s+/cg) {
534 } elsif (m/\G([A-Za-z\x{0080}-\x{fffd}]+([ \t]+[a-z\x{0080}-\x{fffd}]+)*[ \t]+)/cg
535 or m/\G([A-Za-z\x{0080}-\x{fffd}]+)/cg or m/\G(\S)/cg) {
536 Clean
($1); # multiple words but do not match http://foo
540 $bol = (substr($_,pos()-1,1) eq "\n");
543 pos = length $_; # notify module functions we've completed rule handling
544 Clean
(CloseHtmlEnvironments
()); # last block -- close it, cache it
545 if ($Fragment ne '') {
546 $Fragment =~ s
|<p
>\s
*</p
>||g
; # clean up extra paragraphs (see end Dirty())
548 push(@Blocks, $Fragment);
551 # this can be stored in the page cache -- see PrintCache
552 return (join($FS, @Blocks), join($FS, @Flags));
556 if ($bol && m/\G(\s*\n)*(\*+)[ \t]+/cg
557 or InElement
('li') && m/\G(\s*\n)+(\*+)[ \t]+/cg) {
558 return CloseHtmlEnvironmentUntil
('li')
559 . OpenHtmlEnvironment
('ul',length($2)) . AddHtmlEnvironment
('li');
565 my ($locallinks, $withanchors) = @_;
567 and ($BracketText && m/\G(\[$InterLinkPattern\s+([^\]]+?)\])/cog
568 or $BracketText && m/\G(\[\[$FreeInterLinkPattern\|([^\]]+?)\]\])/cog
569 or m/\G(\[$InterLinkPattern\])/cog or m/\G(\[\[\[$FreeInterLinkPattern\]\]\])/cog
570 or m/\G($InterLinkPattern)/cog or m/\G(\[\[$FreeInterLinkPattern\]\])/cog)) {
571 # [InterWiki:FooBar text] or [InterWiki:FooBar] or
572 # InterWiki:FooBar or [[InterWiki:foo bar|text]] or
573 # [[InterWiki:foo bar]] or [[[InterWiki:foo bar]]]-- Interlinks
574 # can change when the intermap changes (local config, therefore
575 # depend on $locallinks). The intermap is only read if
576 # necessary, so if this not an interlink, we have to backtrack a
578 my $bracket = (substr($1, 0, 1) eq '[') # but \[\[$FreeInterLinkPattern\]\] it not bracket!
579 && !((substr($1, 0, 2) eq '[[') && (substr($1, 2, 1) ne '[') && index($1, '|') < 0);
580 my $quote = (substr($1, 0, 2) eq '[[');
581 my ($oldmatch, $output) = ($1, GetInterLink
($2, $3, $bracket, $quote)); # $3 may be empty
582 if ($oldmatch eq $output) { # no interlink
583 my ($site, $rest) = split(/:/, $oldmatch, 2);
585 pos = (pos) - length($rest) - 1; # skip site, but reparse rest
588 print $output; # this is an interlink
590 } elsif ($BracketText && m/\G(\[$FullUrlPattern[|[:space:]]([^\]]+?)\])/cog
591 or $BracketText && m/\G(\[\[$FullUrlPattern[|[:space:]]([^\]]+?)\]\])/cog
592 or m/\G(\[$FullUrlPattern\])/cog or m/\G($UrlPattern)/cog) {
593 # [URL text] makes [text] link to URL, [URL] makes footnotes [1]
594 my ($str, $url, $text, $bracket, $rest) = ($1, $2, $3, (substr($1, 0, 1) eq '['), '');
595 if ($url =~ /(<|>|&)$/) { # remove trailing partial named entitites and add them as
596 $rest = $1; # back again at the end as trailing text.
597 $url =~ s/&(lt|gt|amp)$//;
599 if ($bracket and not defined $text) { # [URL] is dirty because the number may change
601 print GetUrl
($url, $text, $bracket), $rest;
603 Clean
(GetUrl
($url, $text, $bracket, not $bracket) . $rest); # $text may be empty, no images in brackets
605 } elsif ($WikiLinks && m/\G!$LinkPattern/cog) {
606 Clean
($1); # ! gets eaten
607 } elsif ($WikiLinks && $locallinks
608 && ($BracketWiki && m/\G(\[$LinkPattern\s+([^\]]+?)\])/cog
609 or m/\G(\[$LinkPattern\])/cog or m/\G($LinkPattern)/cog)) {
610 # [LocalPage text], [LocalPage], LocalPage
612 my $bracket = (substr($1, 0, 1) eq '[' and not $3);
613 print GetPageOrEditLink
($2, $3, $bracket);
614 } elsif ($locallinks && $FreeLinks && (m/\G(\[\[image:$FreeLinkPattern\]\])/cog
615 or m/\G(\[\[image:$FreeLinkPattern\|([^]|]+)\]\])/cog)) {
616 # [[image:Free Link]], [[image:Free Link|alt text]]
618 print GetDownloadLink
(FreeToNormal
($2), 1, undef, UnquoteHtml
($3));
619 } elsif ($FreeLinks && $locallinks
620 && ($BracketWiki && m/\G(\[\[$FreeLinkPattern\|([^\]]+)\]\])/cog
621 or m/\G(\[\[\[$FreeLinkPattern\]\]\])/cog
622 or m/\G(\[\[$FreeLinkPattern\]\])/cog)) {
623 # [[Free Link|text]], [[[Free Link]]], [[Free Link]]
625 my $bracket = (substr($1, 0, 3) eq '[[[');
626 print GetPageOrEditLink
($2, $3, $bracket, 1); # $3 may be empty
628 return undef; # nothing matched
630 return ''; # one of the dirty rules matched (and they all are)
633 sub SetHtmlEnvironmentContainer
{
634 my ($html_tag, $html_tag_attr) = @_;
635 $HtmlEnvironmentContainers{$html_tag} = defined $html_tag_attr ?
(
636 $HtmlEnvironmentContainers{$html_tag} ?
'|'.$HtmlEnvironmentContainers{$html_tag} : '').
640 # A stashed attribute is an attribute that is meant for internal use
641 # by the Wiki engine i.e., they don't make their way in to final HTML.
642 # You can use stashed attributes for storing additional paramaters
643 # along with a HTML tag.
645 # As of this writing, only the Org markup engine uses stashed
646 # attributes. The value that is stashed is the leading indentation of
647 # an list item. (NOTE: in case of Emacs Org-mode markup the *relative*
648 # indentation between the list items signifies the depth of a list
649 # item i.e., an item that (i) begins at the same column as an earlier
650 # item is a sibling (ii) is more indented starts a sub-list (iii) that
651 # is less indented continues a super-list.)
653 # IMPLEMENTATION NOTE: To distinguish stashed attributes from regular
654 # user-provided attributes, they are enclosed between `%%' markers.
656 sub StashAttributeValue
{ # See note above
657 my ($html_tag_attr, $attr, $value) = @_;
658 return $html_tag_attr unless ($attr and $value);
659 return "$html_tag_attr %$attr% = \"$value\"";
662 sub GetStashedAttributeValue
{ # See note above
663 my ($html_tag_attr, $attr) = @_;
664 if ($html_tag_attr =~ /(^|\s+)%$attr%\s*=\s*"(.*?)"(\s+|$)/) {
670 sub ClearStashedAttributes
{ # See note above
671 my ($html_tag_attr) = @_;
672 $html_tag_attr =~ s/(^|\s+)%.*?%\s*=\s*".*?"(?=($|\s))//g;
673 return $html_tag_attr;
677 my ($html_tag, $html_tag_attr) = @_;
678 if ($html_tag eq 'list') {
679 $html_tag = GetStashedAttributeValue
($html_tag_attr, 'type') eq
680 'numbered' ?
'ol' : 'ul';
682 $html_tag_attr = ClearStashedAttributes
($html_tag_attr);
683 return $html_tag_attr ?
"<$html_tag $html_tag_attr>" : "<$html_tag>";
687 my ($html_tag, $html_tag_attr) = @_;
688 if ($html_tag eq 'list') {
689 $html_tag = GetStashedAttributeValue
($html_tag_attr, 'type') eq
690 'numbered' ?
'ol' : 'ul';
692 return "</$html_tag>";
695 sub InElement
{ # is $html_tag in @HtmlStack?
696 my ($html_tag, $html_tag_attr) = @_;
698 foreach my $html_tag_current (@HtmlStack) {
699 return 1 if $html_tag_current eq $html_tag and
700 ($html_tag_attr ?
$HtmlAttrStack[$i] =~ m/$html_tag_attr/ : 1);
705 sub AddOrCloseHtmlEnvironment
{ # add $html_tag, if not already added; close, otherwise
706 my ($html_tag, $html_tag_attr) = @_;
707 return InElement
($html_tag, '^'.$html_tag_attr.'$')
708 ? CloseHtmlEnvironment
($html_tag, '^'.$html_tag_attr.'$')
709 : AddHtmlEnvironment
($html_tag, $html_tag_attr);
712 sub AddHtmlEnvironment
{ # add a new $html_tag
713 my ($html_tag, $html_tag_attr) = @_;
714 $html_tag_attr = '' if not defined $html_tag_attr;
715 if ($html_tag and not (@HtmlStack and $HtmlStack[0] eq $html_tag and
716 ($html_tag_attr ?
$HtmlAttrStack[0] =~ m/$html_tag_attr/ : 1))) {
717 unshift(@HtmlStack, $html_tag);
718 unshift(@HtmlAttrStack, $html_tag_attr);
719 return OpenHtmlTag
($html_tag, $html_tag_attr);
720 } return ''; # always return something
723 sub OpenHtmlEnvironment
{ # close the previous $html_tag and open a new one
724 my ($html_tag, $depth, $html_tag_attr) = @_;
725 my ($html, $found, @stack) = ('', 0); # always return something
726 while (@HtmlStack and $found < $depth) { # determine new stack
727 my $tag = pop(@HtmlStack);
728 $found++ if $tag eq $html_tag; # this ignores that ul and ol can be equivalent for nesting purposes
729 unshift(@stack, $tag);
731 unshift(@stack, pop(@HtmlStack)) if @HtmlStack and $found < $depth; # nested sublist coming up, keep list item
732 @HtmlStack = @stack if not $found; # if starting a new list
733 $html .= CloseHtmlEnvironments
(); # close remaining elements (or all elements if a new list)
734 @HtmlStack = @stack if $found; # if not starting a new list
735 $depth = $IndentLimit if $depth > $IndentLimit; # requested depth 0 makes no sense
736 $html_tag_attr = qq/class="$html_tag_attr"/ # backwards-compatibility hack: classically, the third argument to this function was a single CSS class, rather than string of HTML tag attributes as in the second argument to the "AddHtmlEnvironment" function. To allow both sorts, we conditionally change this string to 'class="$html_tag_attr"' when this string is a single CSS class.
737 if $html_tag_attr && $html_tag_attr !~ m/^\s*.+?\s*=\s*('|").+\1/;
738 splice(@HtmlAttrStack, 0, @HtmlAttrStack - @HtmlStack); # truncate to size of @HtmlStack
739 foreach ($found..$depth-1) {
740 unshift(@HtmlStack, $html_tag);
741 unshift(@HtmlAttrStack, $html_tag_attr);
742 $html .= OpenHtmlTag
($html_tag, $html_tag_attr);
747 sub CloseHtmlEnvironments
{ # close all -- remember to use AddHtmlEnvironment('p') if required!
748 return CloseHtmlEnvironmentUntil
() if pos($_) == length($_); # close all HTML environments if we're are at the end of this page
751 defined $HtmlEnvironmentContainers{$HtmlStack[0]} and # avoid closing block level elements
752 ($HtmlEnvironmentContainers{$HtmlStack[0]} ?
$HtmlAttrStack[0] =~
753 m/$HtmlEnvironmentContainers{$HtmlStack[0]}/ : 1) and return $html;
754 $html .= CloseHtmlTag
(shift(@HtmlStack), shift(@HtmlAttrStack));
758 sub CloseHtmlEnvironment
{ # close environments up to and including $html_tag
759 my $html = CloseHtmlEnvironmentUntil
(@_) if @_ and InElement
(@_);
760 if (@HtmlStack and (not(@_) or defined $html)) {
761 return $html.CloseHtmlTag
(shift(@HtmlStack), shift(@HtmlAttrStack));
762 } return $html or ''; # always return something
765 sub CloseHtmlEnvironmentUntil
{ # close environments up to but not including $html_tag
766 my ($html_tag, $html_tag_attr) = @_;
768 while (@HtmlStack && (pos($_) == length($_) || # while there is an HTML tag-stack and we are at the end of this page or...
769 !($html_tag ?
$HtmlStack[0] eq $html_tag && # the top tag is not the desired tag and...
770 ($html_tag_attr ?
$HtmlAttrStack[0] =~ # its attributes do not match,
771 m/$html_tag_attr/ : 1) : ''))) { # then...
772 # shift off the top tag and append it to our HTML string.
773 $html .= CloseHtmlTag
(shift(@HtmlStack), shift(@HtmlAttrStack));
778 foreach my $regexp (keys %Smilies) {
779 if (m/\G($regexp)/cg) {
780 return $q->img({-src
=>$Smilies{$regexp}, -alt
=>UnquoteHtml
($1), -class=>'smiley'});
786 my ($locallinks, $withanchors) = @_;
787 foreach my $sub (@MyRules) {
788 my $result = &$sub($locallinks, $withanchors);
789 SetParam
('msg', $@
) if $@
;
790 return $result if defined($result);
797 foreach my $macro (@MyMacros) { &$macro };
801 sub PrintWikiToHTML
{
802 my ($markup, $is_saving_cache, $revision, $is_locked) = @_;
803 my ($blocks, $flags);
805 $markup =~ s/$FS//go if $markup; # Remove separators (paranoia)
806 $markup = QuoteHtml
($markup);
807 ($blocks, $flags) = ApplyRules
($markup, 1, $is_saving_cache, $revision, 'p');
808 if ($is_saving_cache and not $revision and $Page{revision
} # don't save revision 0 pages
809 and $Page{blocks
} ne $blocks and $Page{flags
} ne $flags) {
810 $Page{blocks
} = $blocks;
811 $Page{flags
} = $flags;
812 if ($is_locked or RequestLockDir
('main')) { # not fatal!
814 ReleaseLock
() unless $is_locked;
820 return unless UserIsAdminOrError
();
821 RequestLockOrError
();
822 print GetHeader
('', T
('Clear Cache')), $q->start_div({-class=>'content clear'}),
823 $q->p(T
('Main lock obtained.')), '<p>';
824 foreach my $id (AllPagesList
()) {
826 delete $Page{blocks
};
828 delete $Page{languages
};
829 $Page{languages
} = GetLanguages
($Page{blocks
}) unless TextIsFile
($Page{blocks
});
831 print $q->br(), GetPageLink
($id);
833 print '</p>', $q->p(T
('Main lock released.')), $q->end_div();
834 utime time, time, $IndexFile; # touch index file
841 $html =~ s/&/&/g;
844 $html =~ s/[\x00-\x08\x0b\x0c\x0e-\x1f]/ /g; # legal xml: #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
852 $html =~ s/&/&/g;
859 return '' unless $str;
860 utf8
::encode
($str); # turn to byte string
861 my @letters = split(//, $str);
862 my %safe = map {$_ => 1} ('a' .. 'z', 'A' .. 'Z', '0' .. '9', '-', '_', '.', '!', '~', '*', "'", '(', ')', '#');
863 foreach my $letter (@letters) {
864 $letter = sprintf("%%%02x", ord($letter)) unless $safe{$letter};
866 return join('', @letters);
871 $str =~ s/%([0-9a-f][0-9a-f])/chr(hex($1))/ge;
872 utf8
::decode
($str); # make internal string
878 $re =~ s/([\\\[\]\$()^.])/\\$1/g;
884 return unless eval { require LWP
::UserAgent
; };
885 my $ua = LWP
::UserAgent
->new;
886 my $response = $ua->get($uri);
887 return $response->decoded_content if $response->is_success;
891 print GetHeader
(undef, T
('Journal'));
892 print $q->start_div({-class=>'content'});
893 PrintJournal
(map { GetParam
($_, ''); } qw(num regexp mode offset search));
898 sub JournalSort
{ $b cmp $a }
901 return if $CollectingJournal; # avoid infinite loops
902 local $CollectingJournal = 1;
903 my ($num, $regexp, $mode, $offset, $search) = @_;
904 $regexp = '^\d\d\d\d-\d\d-\d\d' unless $regexp;
905 $num = 10 unless $num;
906 $offset = 0 unless $offset;
907 # FIXME: Should pass filtered list of pages to SearchTitleAndBody to save time?
908 my @pages = sort JournalSort
(grep(/$regexp/, $search ? SearchTitleAndBody
($search) : AllPagesList
()));
909 if ($mode eq 'reverse' or $mode eq 'future') {
910 @pages = reverse @pages;
912 $b = defined($Today) ?
$Today : CalcDay
($Now);
913 if ($mode eq 'future') {
914 for (my $i = 0; $i < @pages; $i++) {
916 if (JournalSort
() == -1) {
917 @pages = @pages[$i..$#pages];
921 } elsif ($mode eq 'past') {
922 for (my $i = 0; $i < @pages; $i++) {
924 if (JournalSort
() == 1) {
925 @pages = @pages[$i..$#pages];
930 return unless $pages[$offset];
931 print $q->start_div({-class=>'journal'});
932 my $next = $offset + PrintAllPages
(1, 1, $num, @pages[$offset .. $#pages]);
934 print $q->p({-class=>'more'}, ScriptLink
("action=more;num=$num;regexp=$regexp;search=$search;mode=$mode;offset=$next", T
('More...'), 'more')) if $pages[$next];
938 my ($links, $comments, $num, @pages) = @_;
939 my $lang = GetParam
('lang', 0);
941 for my $id (@pages) {
942 last if $n >= $JournalLimit and not UserIsAdmin
() or $num and $n >= $num;
943 $i++; # pages looked at
944 local ($OpenPageName, %Page); # this is local!
946 my @languages = split(/,/, $Page{languages
});
947 next if $lang and @languages and not grep(/$lang/, @languages);
948 next if PageMarkedForDeletion
();
949 next if substr($Page{text
}, 0, 10) eq '#REDIRECT ';
950 print $q->start_div({-class=>'page'}),
951 $q->h1($links ? GetPageLink
($id)
952 : $q->a({-name
=>$id}, UrlEncode
(FreeToNormal
($id))));
954 if ($comments and $id !~ /^$CommentsPrefix/o) {
955 print $q->p({-class=>'comment'},
956 GetPageLink
($CommentsPrefix . $id,
957 T
('Comments on this page')));
960 $n++; # pages actually printed
966 return if $CollectingJournal; # avoid infinite loops when using full=1
967 local $CollectingJournal = 1;
968 my $maxitems = shift;
971 if (not eval { require XML
::RSS
; }) {
973 return $q->div({-class=>'rss'}, $q->p({-class=>'error'}, $q->strong(T
('XML::RSS is not available on this system.')), $err));
975 # All strings that are concatenated with strings returned by the RSS
976 # feed must be decoded. Without this decoding, 'diff' and 'history'
977 # translations will be double encoded when printing the result.
978 my $tDiff = T
('diff');
979 my $tHistory = T
('history');
980 my $wikins = 'http://purl.org/rss/1.0/modules/wiki/';
981 my $rdfns = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
982 @uris = map { s/^"?(.*?)"?$/$1/; $_; } @uris; # strip quotes of uris
983 my ($str, %data) = GetRss
(@uris);
984 foreach my $uri (keys %data) {
985 my $data = $data{$uri};
987 $str .= $q->p({-class=>'error'}, $q->strong(Ts
('%s returned no data, or LWP::UserAgent is not available.',
988 $q->a({-href
=>$uri}, $uri))));
990 my $rss = new XML
::RSS
;
991 eval { local $SIG{__DIE__
}; $rss->parse($data); };
993 $str .= $q->p({-class=>'error'}, $q->strong(Ts
('RSS parsing failed for %s', $q->a({-href
=>$uri}, $uri)) . ': ' . $@
));
997 RssInterwikiTranslateInit
(); # not needed anywhere else thus init only now and not in ReInit
998 $interwiki = $rss->{channel
}->{$wikins}->{interwiki
};
999 $interwiki =~ s/^\s+//; # when RDF is used, sometimes whitespace remains,
1000 $interwiki =~ s/\s+$//; # which breaks the test for an existing $interwiki below
1002 $interwiki = $rss->{channel
}->{$rdfns}->{value
};
1004 $interwiki = $RssInterwikiTranslate{$interwiki} if $RssInterwikiTranslate{$interwiki};
1005 $interwiki = $RssInterwikiTranslate{$uri} unless $interwiki;
1008 $str .= $q->p({-class=>'error'}, $q->strong(Ts
('No items found in %s.', $q->a({-href
=>$uri}, $uri))))
1009 unless @
{$rss->{items
}};
1010 foreach my $i (@
{$rss->{items
}}) {
1012 my $date = $i->{dc
}->{date
};
1013 if (not $date and $i->{pubDate
}) {
1014 $date = $i->{pubDate
};
1015 my %mon = (Jan
=>1, Feb
=>2, Mar
=>3, Apr
=>4, May
=>5, Jun
=>6,
1016 Jul
=>7, Aug
=>8, Sep
=>9, Oct
=>10, Nov
=>11, Dec
=>12);
1017 $date =~ s
/^(?:[A-Z][a-z][a-z], )?(\d\d?) ([A-Z][a-z][a-z]) (\d\d(?:\d\d)?)/ # pubDate uses RFC 822
1018 sprintf('%04d-%02d-%02d', ($3 < 100 ?
1900 + $3 : $3), $mon{$2}, $1)/e
;
1020 $date = sprintf("%03d", $num--) unless $date; # for RSS 0.91 feeds without date, descending
1021 my $title = $i->{title
};
1022 my $description = $i->{description
};
1023 if (not $title and $description) { # title may be missing in RSS 2.00
1024 $title = $description;
1027 $title = $i->{link} if not $title and $i->{link}; # if description and title are missing
1028 $line .= ' (' . $q->a({-href
=>$i->{$wikins}->{diff
}}, $tDiff) . ')'
1029 if $i->{$wikins}->{diff
};
1030 $line .= ' (' . $q->a({-href
=>$i->{$wikins}->{history
}}, $tHistory) . ')'
1031 if $i->{$wikins}->{history
};
1034 $line .= ' ' . $q->a({-href
=>$i->{link}, -title
=>$date},
1035 ($interwiki ?
$interwiki . ':' : '') . $title);
1037 $line .= ' ' . $title;
1040 my $contributor = $i->{dc
}->{contributor
};
1041 $contributor = $i->{$wikins}->{username
} unless $contributor;
1042 $contributor =~ s/^\s+//;
1043 $contributor =~ s/\s+$//;
1044 $contributor = $i->{$rdfns}->{value
} unless $contributor;
1045 $line .= $q->span({-class=>'contributor'}, $q->span(T
(' . . . . ')) . $contributor) if $contributor;
1047 if ($description =~ /</) {
1048 $line .= $q->div({-class=>'description'}, $description);
1050 $line .= $q->span({class=>'dash'}, ' – ') . $q->strong({-class=>'description'}, $description);
1053 while ($lines{$date}) {
1055 } # make sure this is unique
1056 $lines{$date} = $line;
1061 my @lines = sort { $b cmp $a } keys %lines;
1062 @lines = @lines[0..$maxitems-1] if $maxitems and $#lines > $maxitems;
1064 foreach my $key (@lines) {
1065 my $line = $lines{$key};
1066 if ($key =~ /(\d\d\d\d(?:-\d?\d)?(?:-\d?\d)?)(?:[T ](\d?\d:\d\d))?/) {
1067 my ($day, $time) = ($1, $2);
1068 if ($day ne $date) {
1069 $str .= '</ul>' if $date; # close ul except for the first time where no open ul exists
1071 $str .= $q->p($q->strong($day)) . '<ul>';
1073 $line = $q->span({-class=>'time'}, $time . ' UTC ') . $line if $time;
1074 } elsif (not $date) {
1075 $str .= '<ul>'; # if the feed doesn't have any dates we need to start the list anyhow
1076 $date = $Now; # to ensure the list starts only once
1078 $str .= $q->li($line);
1080 $str .= '</ul>' if $date;
1081 return $q->div({-class=>'rss'}, $str);
1085 my %todo = map {$_, GetRssFile
($_)} @_;
1088 if (GetParam
('cache', $UseCache) > 0) {
1089 foreach my $uri (keys %todo) { # read cached rss files if possible
1090 if ($Now - (stat($todo{$uri}))[9] < $RssCacheHours * 3600) {
1091 $data{$uri} = ReadFile
($todo{$uri});
1092 delete($todo{$uri}); # no need to fetch them below
1096 my @need_cache = keys %todo;
1097 if (keys %todo > 1) { # try parallel access if available
1098 eval { # see code example in LWP::Parallel, not LWP::Parallel::UserAgent (no callbacks here)
1099 require LWP
::Parallel
::UserAgent
;
1100 my $pua = LWP
::Parallel
::UserAgent
->new();
1101 foreach my $uri (keys %todo) {
1102 if (my $res = $pua->register(HTTP
::Request
->new('GET', $uri))) {
1103 $str .= $res->error_as_HTML;
1106 %todo = (); # because the uris in the response may have changed due to redirects
1107 my $entries = $pua->wait();
1108 foreach (keys %$entries) {
1109 my $uri = $entries->{$_}->request->uri;
1110 $data{$uri} = $entries->{$_}->response->decoded_content;
1114 foreach my $uri (keys %todo) { # default operation: synchronous fetching
1115 $data{$uri} = GetRaw
($uri);
1117 if (GetParam
('cache', $UseCache) > 0) {
1119 foreach my $uri (@need_cache) {
1120 my $data = $data{$uri};
1121 # possibly a Latin-1 file without encoding attribute will cause a problem?
1122 $data =~ s/encoding="[^"]*"/encoding="UTF-8"/; # content was converted
1123 WriteStringToFile
(GetRssFile
($uri), $data) if $data;
1130 return $RssDir . '/' . UrlEncode
(shift);
1133 sub RssInterwikiTranslateInit
{
1134 return unless $RssInterwikiTranslate;
1135 %RssInterwikiTranslate = ();
1136 foreach (split(/\n/, GetPageContent
($RssInterwikiTranslate))) {
1137 if (/^ ([^ ]+)[ \t]+([^ ]+)$/) {
1138 $RssInterwikiTranslate{$1} = $2;
1143 sub GetInterSiteUrl
{
1144 my ($site, $page, $quote) = @_;
1145 return unless $page;
1146 $page = join('/', map { UrlEncode
($_) } split(/\
//, $page)) if $quote; # Foo:bar+baz is not quoted, [[Foo:bar baz]] is.
1147 my $url = $InterSite{$site} or return;
1148 $url =~ s/\%s/$page/g or $url .= $page;
1152 sub BracketLink
{ # brackets can be removed via CSS
1153 return $q->span($q->span({class=>'bracket'}, '[') . (shift) . $q->span({class=>'bracket'}, ']'));
1157 my ($id, $text, $bracket, $quote) = @_;
1158 my ($site, $page) = split(/:/, $id, 2);
1159 $page =~ s/&/&/g; # Unquote common URL HTML
1160 my $url = GetInterSiteUrl
($site, $page, $quote);
1161 my $class = 'inter ' . $site;
1162 if ($text && $bracket && !$url) {
1163 return "[$id $text]";
1164 } elsif ($bracket && !$url) {
1168 } elsif ($bracket && !$text) {
1169 $text = BracketLink
(++$FootnoteNumber);
1170 $class .= ' number';
1172 $text = $q->span({-class=>'site'}, $site)
1173 . $q->span({-class=>'separator'}, ':')
1174 . $q->span({-class=>'page'}, $page);
1175 } elsif ($bracket) { # and $text is set
1176 $class .= ' outside';
1178 return $q->a({-href
=>$url, -class=>$class}, $text);
1183 foreach (split(/\n/, GetPageContent
($InterMap))) {
1184 if (/^ ($InterSitePattern)[ \t]+([^ ]+)$/) {
1185 $InterSite{$1} = $2;
1191 my ($url, $text, $bracket, $images) = @_;
1192 $url =~ /^($UrlProtocols)/;
1193 my $class = "url $1";
1194 if ($NetworkFile && $url =~ m
|^file
:///| && !$AllNetworkFiles
1195 or !$NetworkFile && $url =~ m
|^file
:|) {
1196 # Only do remote file:// links. No file:///c|/windows.
1198 } elsif ($bracket and not defined $text) {
1199 $text = BracketLink
(++$FootnoteNumber);
1200 $class .= ' number';
1201 } elsif (not defined $text) {
1203 } elsif ($bracket) { # and $text is set
1204 $class .= ' outside';
1206 $url = UnquoteHtml
($url); # links should be unquoted again
1207 if ($images && $url =~ /^(http:|https:|ftp:).+\.$ImageExtensions$/i) {
1208 return $q->img({-src
=>$url, -alt
=>$url, -class=>$class});
1210 return $q->a({-href
=>$url, -class=>$class}, $text);
1214 sub GetPageOrEditLink
{ # use GetPageLink and GetEditLink if you know the result!
1215 my ($id, $text, $bracket, $free) = @_;
1216 $id = FreeToNormal
($id);
1217 my ($class, $resolved, $title, $exists) = ResolveId
($id);
1218 if (!$text && $resolved && $bracket) {
1219 $text = BracketLink
(++$FootnoteNumber);
1220 $class .= ' number';
1221 $title = NormalToFree
($id);
1223 my $link = $text||NormalToFree
($id);
1224 if ($resolved) { # anchors don't exist as pages, therefore do not use $exists
1225 return ScriptLink
(UrlEncode
($resolved), $link, $class, undef, $title);
1226 } else { # reproduce markup if $UseQuestionmark
1227 return GetEditLink
($id, UnquoteHtml
($bracket ?
"[$link]" : $link)) if not $UseQuestionmark;
1228 $link = QuoteHtml
($id) . GetEditLink
($id, '?');
1229 $link .= ($free ?
'|' : ' ') . $text if $text and $text ne $id;
1230 $link = "[[$link]]" if $free;
1231 $link = "[$link]" if $bracket or not $free and $text;
1236 sub GetPageLink
{ # use if you want to force a link to local pages, whether it exists or not
1237 my ($id, $name, $class, $accesskey) = @_;
1238 $id = FreeToNormal
($id);
1239 $name = $id unless $name;
1240 $class .= ' ' if $class;
1241 return ScriptLink
(UrlEncode
($id), NormalToFree
($name), $class . 'local',
1242 undef, undef, $accesskey);
1245 sub GetEditLink
{ # shortcut
1246 my ($id, $name, $upload, $accesskey) = @_;
1247 $id = FreeToNormal
($id);
1248 my $action = 'action=edit;id=' . UrlEncode
($id);
1249 $action .= ';upload=1' if $upload;
1250 return ScriptLink
($action, NormalToFree
($name), 'edit', undef, T
('Click to edit this page'), $accesskey);
1253 sub GetRawLink
{ # shortcut
1254 my ($id, $name, $accesskey) = @_;
1255 $id = FreeToNormal
($id);
1256 my $action = 'raw=1;id=' . UrlEncode
($id);
1257 return ScriptLink
($action, NormalToFree
($name), 'raw', undef, T
('View raw text of this page'), $accesskey);
1262 if ($action =~ /^($UrlProtocols)\%3a/ or $action =~ /^\%2f/) { # nearlinks and other URLs
1263 $action =~ s/%([0-9a-f][0-9a-f])/chr(hex($1))/ge; # undo urlencode
1265 } elsif ($UsePathInfo and index($action, '=') == -1) {
1266 $action = $ScriptName . '/' . $action;
1268 $action = $ScriptName . '?' . $action;
1270 return $action unless wantarray;
1271 return ($action, index($action, '=') != -1);
1275 my ($action, $text, $class, $name, $title, $accesskey) = @_;
1276 my ($url, $nofollow) = ScriptUrl
($action);
1278 $params{-href
} = $url;
1279 $params{'-rel'} = 'nofollow' if $nofollow;
1280 $params{'-class'} = $class if $class;
1281 $params{'-name'} = $name if $name;
1282 $params{'-title'} = $title if $title;
1283 $params{'-accesskey'} = $accesskey if $accesskey;
1284 return $q->a(\
%params, $text);
1287 sub GetDownloadLink
{
1288 my ($id, $image, $revision, $alt) = @_;
1289 $alt = NormalToFree
($id) unless $alt;
1290 # if the page does not exist
1291 return '[[' . ($image ?
'image' : 'download') . ':'
1292 . ($UseQuestionmark ? QuoteHtml
($id) . GetEditLink
($id, '?', 1)
1293 : GetEditLink
($id, $id, 1)) . ']]'
1294 unless $IndexHash{$id};
1297 $action = "action=download;id=" . UrlEncode
($id) . ";revision=$revision";
1298 } elsif ($UsePathInfo) {
1299 $action = "download/" . UrlEncode
($id);
1301 $action = "action=download;id=" . UrlEncode
($id);
1304 if ($UsePathInfo and not $revision) {
1305 $action = $ScriptName . '/' . $action;
1307 $action = $ScriptName . '?' . $action;
1309 return $action if $image == 2;
1310 my $result = $q->img({-src
=>$action, -alt
=>UnquoteHtml
($alt), -class=>'upload'});
1311 $result = ScriptLink
(UrlEncode
($id), $result, 'image')
1312 unless $id eq $OpenPageName;
1315 return ScriptLink
($action, $alt, 'upload');
1319 sub PrintCache
{ # Use after OpenPage!
1320 my @blocks = split($FS,$Page{blocks
});
1321 my @flags = split($FS,$Page{flags
});
1322 $FootnoteNumber = 0;
1323 foreach my $block (@blocks) {
1324 if (shift(@flags)) {
1325 ApplyRules
($block, 1, 1); # local links, anchors, current revision, no start tag
1332 sub PrintPageHtml
{ # print an open page
1333 return unless GetParam
('page', 1);
1334 if ($Page{blocks
} && $Page{flags
} && GetParam
('cache', $UseCache) > 0) {
1337 PrintWikiToHTML
($Page{text
}, 1); # save cache, current revision, no main lock
1341 sub PrintPageDiff
{ # print diff for open page
1342 my $diff = GetParam
('diff', 0);
1343 if ($UseDiff && $diff) {
1344 PrintHtmlDiff
($diff);
1345 print $q->hr() if GetParam
('page', 1);
1350 my ($id, $limit, $error) = @_;
1354 open(STDOUT
, '>', \
$diff) or die "Can't open memory file: $!";
1355 binmode(STDOUT
); # works whether STDOUT already has the UTF8 layer or not
1356 binmode(STDOUT
, ":utf8");
1358 utf8
::decode
($diff);
1359 return $error if $limit and length($diff) > $limit;
1360 open(STDOUT
, '>', \
$page) or die "Can't open memory file: $!";
1361 binmode(STDOUT
); # works whether STDOUT already has the UTF8 layer or not
1362 binmode(STDOUT
, ":utf8");
1364 utf8
::decode
($page);
1365 return $diff . $q->p($error) if $limit and length($diff . $page) > $limit;
1366 return $diff . $page;
1371 return $Translate{$text} if $Translate{$text};
1376 my ($text, $string) = @_;
1378 $text =~ s/\%s/$string/ if defined($string);
1385 $text =~ s/\%([1-9])/$_[$1]/ge;
1390 my $id = UnquoteHtml
(GetParam
('id', GetParam
('title', ''))); # id=x or title=x -> x
1392 my @keywords = $q->keywords;
1393 foreach my $keyword (@keywords) {
1394 utf8
::decode
($keyword);
1396 $id = join('_', @keywords) unless $id; # script?p+q -> p_q
1399 my $path = $q->path_info;
1400 utf8
::decode
($path);
1401 my @path = split(/\//, $path);
1402 $id = pop(@path) unless $id; # script/p/q -> q
1403 foreach my $p (@path) {
1404 SetParam
($p, 1); # script/p/q -> p=1
1410 sub DoBrowseRequest
{
1411 # We can use the error message as the HTTP error code
1412 ReportError
(Ts
('CGI Internal error: %s',$q->cgi_error), $q->cgi_error) if $q->cgi_error;
1413 print $q->header(-status
=>'304 NOT MODIFIED') and return if PageFresh
(); # return value is ignored
1415 my $action = lc(GetParam
('action', '')); # script?action=foo;id=bar
1416 $action = 'download' if GetParam
('download', '') and not $action; # script/download/id
1417 my $search = GetParam
('search', '');
1418 if ($Action{$action}) {
1419 &{$Action{$action}}($id);
1420 } elsif ($action and defined &MyActions
) {
1421 eval { local $SIG{__DIE__
}; MyActions
(); };
1423 ReportError
(Ts
('Invalid action parameter %s', $action), '501 NOT IMPLEMENTED');
1424 } elsif ($search ne '') { # allow search for "0"
1425 SetParam
('action', 'search'); # fake it
1427 } elsif (GetParam
('title', '') and not GetParam
('Cancel', '')) {
1428 DoPost
(GetParam
('title', ''));
1430 BrowseResolvedPage
($id||$HomePage); # default action!
1434 sub ValidId
{ # hack alert: returns error message if invalid, and unfortunately the empty string if valid!
1435 my $id = FreeToNormal
(shift);
1436 return T
('Page name is missing') unless $id;
1437 return Ts
('Page name is too long: %s', $id) if length($id) > 120;
1438 return Ts
('Invalid Page %s (must not end with .db)', $id) if $id =~ m
|\
.db
$|;
1439 return Ts
('Invalid Page %s (must not end with .lck)', $id) if $id =~ m
|\
.lck
$|;
1440 return Ts
('Invalid Page %s', $id) if $FreeLinks ?
$id !~ m
|^$FreeLinkPattern$| : $id !~ m
|^$LinkPattern$|;
1445 my $error = ValidId
($id);
1446 ReportError
($error, '400 BAD REQUEST') if $error;
1450 sub ResolveId
{ # return css class, resolved id, title (eg. for popups), exist-or-not
1452 return ('local', $id, '', 1) if $IndexHash{$id};
1453 return ('', '', '', '');
1456 sub BrowseResolvedPage
{
1457 my $id = FreeToNormal
(shift);
1458 my ($class, $resolved, $title, $exists) = ResolveId
($id);
1459 if ($class && $class eq 'near' && not GetParam
('rcclusteronly', 0)) { # nearlink (is url)
1460 print $q->redirect({-uri
=>$resolved});
1461 } elsif ($class && $class eq 'alias') { # an anchor was found instead of a page
1462 ReBrowsePage
($resolved);
1463 } elsif (not $resolved and $NotFoundPg and $id !~ /^$CommentsPrefix/o) { # custom page-not-found message
1464 BrowsePage
($NotFoundPg);
1465 } elsif ($resolved) { # an existing page was found
1466 BrowsePage
($resolved, GetParam
('raw', 0));
1467 } else { # new page!
1468 BrowsePage
($id, GetParam
('raw', 0), undef, '404 NOT FOUND') if ValidIdOrDie
($id);
1473 my ($id, $raw, $comment, $status) = @_;
1475 my ($text, $revision, $summary) = GetTextRevision
(GetParam
('revision', ''));
1476 $text = $NewText unless $revision or $Page{revision
}; # new text for new pages
1477 # handle a single-level redirect
1478 my $oldId = GetParam
('oldid', '');
1479 if ((substr($text, 0, 10) eq '#REDIRECT ')) {
1481 $Message .= $q->p(T
('Too many redirections'));
1482 } elsif ($revision) {
1483 $Message .= $q->p(T
('No redirection for old revisions'));
1484 } elsif (($FreeLinks and $text =~ /^\#REDIRECT\s+\[\[$FreeLinkPattern\]\]/)
1485 or ($WikiLinks and $text =~ /^\#REDIRECT\s+$LinkPattern/)) {
1486 return ReBrowsePage
(FreeToNormal
($1), $id);
1488 $Message .= $q->p(T
('Invalid link pattern for #REDIRECT'));
1491 # shortcut if we only need the raw text: no caching, no diffs, no html.
1493 print GetHttpHeader
('text/plain', $Page{ts
}, $IndexHash{$id} ?
undef : '404 NOT FOUND');
1495 print $Page{ts
} . " # Do not delete this line when editing!\n";
1501 my $msg = GetParam
('msg', '');
1502 $Message .= $q->p($msg) if $msg; # show message if the page is shown
1503 SetParam
('msg', '');
1504 print GetHeader
($id, NormalToFree
($id), $oldId, undef, $status,
1505 GetFooterLinks
($id, $revision));
1506 my $showDiff = GetParam
('diff', 0);
1507 if ($UseDiff && $showDiff) {
1508 PrintHtmlDiff
($showDiff, GetParam
('diffrevision', $revision), $revision, $text, $summary);
1511 PrintPageContent
($text, $revision, $comment);
1512 SetParam
('rcclusteronly', $id) if FreeToNormal
(GetCluster
($text)) eq $id; # automatically filter by cluster
1514 PrintFooter
($id, $revision, $comment);
1518 my ($id, $oldId) = map { UrlEncode
($_); } @_; # encode before printing URL
1519 if ($oldId) { # Target of #REDIRECT (loop breaking)
1520 print GetRedirectPage
("action=browse;oldid=$oldId;id=$id", $id);
1522 print GetRedirectPage
($id, $id);
1526 sub GetRedirectPage
{
1527 my ($action, $name) = @_;
1529 if (GetParam
('raw', 0)) {
1530 $html = GetHttpHeader
('text/plain');
1531 $html .= Ts
('Please go on to %s.', $action); # no redirect
1534 if ($UsePathInfo and $action !~ /=/) {
1535 $url = $ScriptName . '/' . $action;
1537 $url = $ScriptName . '?' . $action;
1539 my $nameLink = $q->a({-href
=>$url}, $name);
1540 my %headers = (-uri
=>$url);
1541 my $cookie = Cookie
();
1543 $headers{-cookie
} = $cookie;
1545 return $q->redirect(%headers);
1549 my @pages = AllPagesList
();
1550 ReBrowsePage
($pages[int(rand($#pages + 1))]);
1553 sub PageFresh
{ # pages can depend on other pages (ie. last update), admin status, and css
1554 return 1 if $q->http('HTTP_IF_NONE_MATCH') and GetParam
('cache', $UseCache) >= 2
1555 and $q->http('HTTP_IF_NONE_MATCH') eq PageEtag
();
1559 my ($changed, $visible, %params) = CookieData
();
1560 return UrlEncode
(join($FS, $LastUpdate||$Now, sort(values %params))); # no CTL in field values
1563 sub FileFresh
{ # old files are never stale, current files are stale when the page was modified
1564 return 1 if $q->http('HTTP_IF_NONE_MATCH') and GetParam
('cache', $UseCache) >= 2
1565 and (GetParam
('revision', 0) or $q->http('HTTP_IF_NONE_MATCH') eq $Page{ts
});
1570 if (GetParam
('raw', 0)) {
1571 print GetHttpHeader
('text/plain');
1574 PrintRcHtml
($id || $RCName, 1);
1578 sub GetRcLines
{ # starttime, hash of seen pages to use as a second return value
1579 my $starttime = shift || GetParam
('from', 0) ||
1580 $Now - GetParam
('days', $RcDefault) * 86400; # 24*60*60
1581 my $filterOnly = GetParam
('rcfilteronly', '');
1582 # these variables apply accross logfiles
1583 my %match = $filterOnly ?
map { $_ => 1 } SearchTitleAndBody
($filterOnly) : ();
1586 # check the first timestamp in the default file, maybe read old log file
1587 open(F
, '<:utf8', $RcFile);
1589 my ($ts) = split(/$FS/o, $line); # the first timestamp in the regular rc file
1590 if (not $ts or $ts > $starttime) { # we need to read the old rc file, too
1591 push(@result, GetRcLinesFor
($RcOldFile, $starttime,\
%match, \
%following));
1593 push(@result, GetRcLinesFor
($RcFile, $starttime, \
%match, \
%following));
1594 # GetRcLinesFor is trying to save memory space, but some operations
1595 # can only happen once we have all the data.
1596 return LatestChanges
(StripRollbacks
(@result));
1600 my $all = GetParam
('all', 0);
1603 for (my $i = $#result; $i >= 0; $i--) {
1604 my $id = $result[$i][1];
1606 $result[$i][9] = 1 unless $seen{$id}; # mark latest edit
1608 splice(@result, $i, 1) if $seen{$id}; # remove older edits
1612 my $to = GetParam
('upto', 0);
1614 for (my $i = 0; $i < $#result; $i++) {
1615 if ($result[$i][0] > $to) {
1616 splice(@result, $i);
1621 return reverse @result;
1624 sub StripRollbacks
{
1626 if (not (GetParam
('all', 0) or GetParam
('rollback', 0))) { # strip rollbacks
1628 for (my $i = $#result; $i >= 0; $i--) {
1629 # some fields have a different meaning if looking at rollbacks
1630 my $ts = $result[$i][0];
1631 my $id = $result[$i][1];
1632 my $target_ts = $result[$i][2];
1633 my $target_id = $result[$i][3];
1634 if ($id eq '[[rollback]]') {
1636 $rollback{$target_id} = $target_ts; # single page rollback
1637 splice(@result, $i, 1); # strip marker
1640 while ($ts > $target_ts and $i > 0) {
1641 $i--; # quickly skip all these lines
1642 $ts = $result[$i][0];
1644 splice(@result, $i + 1, $end - $i);
1645 $i++; # compensate $i-- in for loop
1647 } elsif ($rollback{$id} and $ts > $rollback{$id}) {
1648 splice(@result, $i, 1); # strip rolled back single pages
1651 } else { # just strip the marker left by DoRollback()
1652 for (my $i = $#result; $i >= 0; $i--) {
1653 splice(@result, $i, 1) if $result[$i][1] eq '[[rollback]]'; # id
1661 my $starttime = shift;
1662 my %match = %{$_[0]}; # deref
1663 my %following = %{$_[1]}; # deref
1665 my $showminoredit = GetParam
('showedit', $ShowEdits); # show minor edits
1666 my $all = GetParam
('all', 0);
1667 my ($idOnly, $userOnly, $hostOnly, $clusterOnly, $filterOnly, $match, $lang,
1668 $followup) = map { UnquoteHtml
(GetParam
($_, '')); }
1669 qw(rcidonly rcuseronly rchostonly
1670 rcclusteronly rcfilteronly match lang followup);
1671 # parsing and filtering
1673 open(F
, '<:utf8', $file) or return ();
1674 while (my $line = <F
>) {
1676 my ($ts, $id, $minor, $summary, $host, $username, $revision,
1677 $languages, $cluster) = split(/$FS/o, $line);
1678 next if $ts < $starttime;
1679 $following{$id} = $ts if $followup and $followup eq $username;
1680 next if $followup and (not $following{$id} or $ts <= $following{$id});
1681 next if $idOnly and $idOnly ne $id;
1682 next if $filterOnly and not $match{$id};
1683 next if ($userOnly and $userOnly ne $username);
1684 next if $minor == 1 and !$showminoredit; # skip minor edits (if [[rollback]] this is bogus)
1685 next if !$minor and $showminoredit == 2; # skip major edits
1686 next if $match and $id !~ /$match/i;
1687 next if $hostOnly and $host !~ /$hostOnly/i;
1688 my @languages = split(/,/, $languages);
1689 next if $lang and @languages and not grep(/$lang/, @languages);
1691 ($cluster, $summary) = ($1, $2) if $summary =~ /^\[\[$FreeLinkPattern\]\] ?: *(.*)/
1692 or $summary =~ /^$LinkPattern ?: *(.*)/o;
1693 next if ($clusterOnly and $clusterOnly ne $cluster);
1694 $cluster = '' if $clusterOnly; # don't show cluster if $clusterOnly eq $cluster
1695 if ($all < 2 and not $clusterOnly and $cluster) {
1696 $summary = "$id: $summary"; # print the cluster instead of the page
1703 $following{$id} = $ts if $followup and $followup eq $username;
1704 push(@result, [$ts, $id, $minor, $summary, $host, $username, $revision,
1705 \
@languages, $cluster]);
1710 sub ProcessRcLines
{
1711 my ($printDailyTear, $printRCLine) = @_; # code references
1714 for my $line (GetRcLines
()) {
1715 my ($ts, $id, $minor, $summary, $host, $username, $revision, $languageref,
1716 $cluster, $last) = @
$line;
1717 if ($date ne CalcDay
($ts)) {
1718 $date = CalcDay
($ts);
1719 &$printDailyTear($date);
1721 &$printRCLine($id, $ts, $host, $username, $summary, $minor, $revision,
1722 $languageref, $cluster, $last);
1727 my ($from, $upto, $html) = (GetParam
('from', 0), GetParam
('upto', 0), '');
1729 $html .= $q->h2(Ts
('Updates since %s', TimeToText
(GetParam
('from', 0))) . ' '
1730 . ($upto ? Ts
('up to %s', TimeToText
($upto)) : ''));
1732 $html .= $q->h2((GetParam
('days', $RcDefault) != 1)
1733 ? Ts
('Updates in the last %s days',
1734 GetParam
('days', $RcDefault))
1735 : Ts
('Updates in the last %s day',
1736 GetParam
('days', $RcDefault)))
1738 my $days = GetParam
('days', $RcDefault);
1739 my $all = GetParam
('all', 0);
1740 my $edits = GetParam
('showedit', 0);
1741 my $rollback = GetParam
('rollback', 0);
1743 my ($idOnly, $userOnly, $hostOnly, $clusterOnly, $filterOnly,
1744 $match, $lang, $followup) =
1746 my $val = GetParam
($_, '');
1747 $html .= $q->p($q->b('(' . Ts
('for %s only', $val) . ')')) if $val;
1748 $action .= ";$_=$val" if $val; # remember these parameters later!
1750 } qw(rcidonly rcuseronly rchostonly rcclusteronly rcfilteronly
1751 match lang followup);
1752 my $rss = "action=rss$action;days=$days;all=$all;showedit=$edits";
1754 $action = GetPageParameters
('browse', $clusterOnly) . $action;
1756 $action = "action=rc$action";
1760 push(@menu, ScriptLink
("$action;days=$days;all=0;showedit=$edits",
1761 T
('List latest change per page only')));
1763 push(@menu, ScriptLink
("$action;days=$days;all=1;showedit=$edits",
1764 T
('List all changes')));
1766 push(@menu, ScriptLink
("$action;days=$days;all=0;rollback=0;"
1767 . "showedit=$edits", T
('Skip rollbacks')));
1769 push(@menu, ScriptLink
("$action;days=$days;all=0;rollback=1;"
1770 . "showedit=$edits", T
('Include rollbacks')));
1774 push(@menu, ScriptLink
("$action;days=$days;all=$all;showedit=0",
1775 T
('List only major changes')));
1777 push(@menu, ScriptLink
("$action;days=$days;all=$all;showedit=1",
1778 T
('Include minor changes')));
1781 $q->p((map { ScriptLink
("$action;days=$_;all=$all;showedit=$edits",
1782 ($_ != 1) ? Ts
('%s days', $_) : Ts
('%s days', $_));
1783 } @RcDays), $q->br(), @menu, $q->br(),
1784 ScriptLink
($action . ';from=' . ($LastUpdate + 1)
1785 . ";all=$all;showedit=$edits", T
('List later changes')),
1786 ScriptLink
($rss, T
('RSS'), 'rss nopages nodiff'),
1787 ScriptLink
("$rss;full=1", T
('RSS with pages'), 'rss pages nodiff'),
1788 ScriptLink
("$rss;full=1;diff=1", T
('RSS with pages and diff'),
1792 sub GetScriptUrlWithRcParameters
{
1793 my $url = "$ScriptName?action=rss";
1794 foreach my $param (qw(from upto days all showedit rollback rcidonly rcuseronly
1795 rchostonly rcclusteronly rcfilteronly match lang
1796 followup page diff full)) {
1797 my $val = GetParam
($param, undef);
1798 $url .= ";$param=$val" if defined $val;
1804 my $form = $q->strong(T
('Filters'));
1805 $form .= $q->input({-type
=>'hidden', -name
=>'action', -value
=>'rc'});
1806 $form .= $q->input({-type
=>'hidden', -name
=>'all', -value
=>1})
1807 if (GetParam
('all', 0));
1808 $form .= $q->input({-type
=>'hidden', -name
=>'showedit', -value
=>1})
1809 if (GetParam
('showedit', 0));
1810 $form .= $q->input({-type
=>'hidden', -name
=>'days',
1811 -value
=>GetParam
('days', $RcDefault)})
1812 if (GetParam
('days', $RcDefault) != $RcDefault);
1814 foreach my $h (['match' => T
('Title:')],
1815 ['rcfilteronly' => T
('Title and Body:')],
1816 ['rcuseronly' => T
('Username:')], ['rchostonly' => T
('Host:')],
1817 ['followup' => T
('Follow up to:')]) {
1818 $table .= $q->Tr($q->td($q->label({-for=>$h->[0]}, $h->[1])),
1819 $q->td($q->textfield(-name
=>$h->[0], -id
=>$h->[0],
1822 $table .= $q->Tr($q->td($q->label({-for=>'rclang'}, T
('Language:')))
1823 . $q->td($q->textfield(-name
=>'lang', -id
=>'rclang',
1825 -default=>GetParam
('lang', ''))))
1827 return GetFormStart
(undef, 'get', 'filter') . $q->p($form) . $q->table($table)
1828 . $q->p($q->submit('dofilter', T
('Go!'))) . $q->endform;
1832 my ($html, $inlist) = ('', 0);
1833 # Optimize param fetches and translations out of main loop
1834 my $all = GetParam
('all', 0);
1835 my $admin = UserIsAdmin
();
1836 my $rollback_was_possible = 0;
1837 my $printDailyTear = sub {
1843 $html .= $q->p($q->strong($date));
1849 my $printRCLine = sub {
1850 my($id, $ts, $host, $username, $summary, $minor, $revision,
1851 $languages, $cluster, $last) = @_;
1852 my $all_revision = $last ?
undef : $revision; # no revision for the last one
1853 $host = QuoteHtml
($host);
1854 my $author = GetAuthorLink
($host, $username);
1855 my $sum = $summary ?
$q->span({class=>'dash'}, ' – ')
1856 . $q->strong(QuoteHtml
($summary)) : '';
1857 my $edit = $minor ?
$q->em({class=>'type'}, T
('(minor)')) : '';
1858 my $lang = @
{$languages}
1859 ?
$q->span({class=>'lang'}, '[' . join(', ', @
{$languages}) . ']') : '';
1860 my ($pagelink, $history, $diff, $rollback) = ('', '', '', '');
1862 $pagelink = GetOldPageLink
('browse', $id, $all_revision, $id, $cluster);
1863 my $rollback_is_possible = RollbackPossible
($ts);
1864 if ($admin and ($rollback_is_possible or $rollback_was_possible)) {
1865 $rollback = $q->submit("rollback-$ts", T
('rollback'));
1866 $rollback_was_possible = $rollback_is_possible;
1868 $rollback_was_possible = 0;
1870 } elsif ($cluster) {
1871 $pagelink = GetOldPageLink
('browse', $id, $revision, $id, $cluster);
1873 $pagelink = GetPageLink
($id, $cluster);
1874 $history = '(' . GetHistoryLink
($id, T
('history')) . ')';
1876 if ($cluster and $PageCluster) {
1877 $diff .= GetPageLink
($PageCluster) . ':';
1878 } elsif ($UseDiff and GetParam
('diffrclink', 1)) {
1879 if ($revision == 1) {
1880 $diff .= '(' . $q->span({-class=>'new'}, T
('new')) . ')';
1882 $diff .= '(' . ScriptLinkDiff
(2, $id, T
('diff'), '', $all_revision) .')';
1884 $diff .= '(' . ScriptLinkDiff
($minor ?
2 : 1, $id, T
('diff'), '') . ')';
1887 $html .= $q->li($q->span({-class=>'time'}, CalcTime
($ts)), $diff, $history,
1888 $rollback, $pagelink, T
(' . . . . '), $author, $sum, $lang,
1891 ProcessRcLines
($printDailyTear, $printRCLine);
1892 $html .= '</ul>' if $inlist;
1893 my $to = GetParam
('from', $Now - GetParam
('days', $RcDefault) * 86400);
1894 my $from = $to - GetParam
('days', $RcDefault) * 86400;
1895 my $more = "action=rc;from=$from;upto=$to";
1896 foreach (qw(all showedit rollback rcidonly rcuseronly rchostonly
1897 rcclusteronly rcfilteronly match lang followup)) {
1898 my $val = GetParam
($_, '');
1899 $more .= ";$_=$val" if $val;
1901 $html .= $q->p({-class=>'more'}, ScriptLink
($more, T
('More...'), 'more'));
1902 return GetFormStart
(undef, 'get', 'rc') . $html . $q->endform;
1905 sub PrintRcHtml
{ # to append RC to existing page, or action=rc directly
1906 my ($id, $standalone) = @_;
1907 my $rc = ($id eq $RCName or $id eq T
($RCName) or T
($id) eq $RCName);
1908 print GetHeader
('', $rc ? NormalToFree
($id) : Ts
('All changes for %s', NormalToFree
($id)), undef, undef, undef, GetFooterLinks
($id))
1910 if ($standalone or $rc or GetParam
('rcclusteronly', '')) {
1911 print $q->start_div({-class=>'rc'});
1912 print $q->hr() unless $standalone or GetParam
('embed', $EmbedWiki);
1913 print RcHeader
() . RcHtml
() . GetFilterForm
() . $q->end_div();
1915 PrintFooter
($id) if $standalone;
1919 my ($name, $value) = @_;
1921 $value =~ s/\n+/\n /;
1922 return $value ?
$name . ': ' . $value . "\n" : '';
1925 sub RcTextRevision
{
1926 my($id, $ts, $host, $username, $summary, $minor, $revision,
1927 $languages, $cluster, $last) = @_;
1928 my $link = $ScriptName
1929 . (GetParam
('all', 0) && ! $last
1930 ?
'?' . GetPageParameters
('browse', $id, $revision, $cluster, $last)
1931 : ($UsePathInfo ?
'/' : '?') . UrlEncode
($id));
1932 print "\n", RcTextItem
('title', NormalToFree
($id)),
1933 RcTextItem
('description', $summary),
1934 RcTextItem
('generator', $username
1935 ?
$username . ' ' . Ts
('from %s', $host) : $host),
1936 RcTextItem
('language', join(', ', @
{$languages})), RcTextItem
('link', $link),
1937 RcTextItem
('last-modified', TimeToW3
($ts)),
1938 RcTextItem
('revision', $revision);
1941 sub PrintRcText
{ # print text rss header and call ProcessRcLines
1942 local $RecentLink = 0;
1943 print RcTextItem
('title', $SiteName),
1944 RcTextItem
('description', $SiteDescription), RcTextItem
('link', $ScriptName),
1945 RcTextItem
('generator', 'Oddmuse'), RcTextItem
('rights', $RssRights);
1946 ProcessRcLines
(sub {}, \
&RcTextRevision
);
1950 my $date = TimeToRFC822
($LastUpdate);
1952 if (GetParam
("exclude", 1)) {
1953 foreach (split(/\n/, GetPageContent
($RssExclude))) {
1954 if (/^ ([^ ]+)[ \t]*$/) { # only read lines with one word after one space
1959 my $rss = qq{<?xml version
="1.0" encoding
="UTF-8"?
>\n};
1960 if ($RssStyleSheet =~ /\.(xslt?|xml)$/) {
1961 $rss .= qq{<?xml
-stylesheet type
="text/xml" href
="$RssStyleSheet" ?
>\n};
1962 } elsif ($RssStyleSheet) {
1963 $rss .= qq{<?xml
-stylesheet type
="text/css" href
="$RssStyleSheet" ?
>\n};
1965 $rss .= qq{<rss version
="2.0"
1966 xmlns
:wiki
="http://purl.org/rss/1.0/modules/wiki/"
1967 xmlns
:dc
="http://purl.org/dc/elements/1.1/"
1968 xmlns
:cc
="http://web.resource.org/cc/"
1969 xmlns
:atom
="http://www.w3.org/2005/Atom">
1971 <docs
>http
://blogs
.law
.harvard
.edu
/tech/rss
</docs
>
1973 my $title = QuoteHtml
($SiteName) . ': '
1974 . GetParam
('title', QuoteHtml
(NormalToFree
($HomePage)));
1975 $rss .= "<title>$title</title>\n";
1976 $rss .= "<link>" . ScriptUrl
($HomePage) . "</link>\n";
1977 $rss .= qq{<atom
:link href
="} . GetScriptUrlWithRcParameters()
1978 . qq{" rel
="self" type
="application/rss+xml" />\n};
1979 $rss .= "<description>" . QuoteHtml
($SiteDescription) . "</description>\n"
1980 if $SiteDescription;
1981 $rss .= "<pubDate>$date</pubDate>\n";
1982 $rss .= "<lastBuildDate>$date</lastBuildDate>\n";
1983 $rss .= "<generator>Oddmuse</generator>\n";
1984 $rss .= "<copyright>$RssRights</copyright>\n" if $RssRights;
1985 $rss .= join('', map {"<cc:license>" . QuoteHtml
($_) . "</cc:license>\n"}
1986 (ref $RssLicense eq 'ARRAY' ? @
$RssLicense : $RssLicense))
1988 $rss .= "<wiki:interwiki>$InterWikiMoniker</wiki:interwiki>\n"
1989 if $InterWikiMoniker;
1991 $rss .= "<image>\n";
1992 $rss .= "<url>$RssImageUrl</url>\n";
1993 $rss .= "<title>$title</title>\n"; # the same as the channel
1994 $rss .= "<link>$ScriptName</link>\n"; # the same as the channel
1995 $rss .= "</image>\n";
1997 my $limit = GetParam
("rsslimit", 15); # Only take the first 15 entries
1999 ProcessRcLines
(sub {}, sub {
2001 return if $excluded{$id}
2002 or ($limit ne 'all' and $count++ >= $limit);
2003 $rss .= "\n" . RssItem
($id, @_);
2005 $rss .= "</channel>\n</rss>\n";
2010 my ($id, $ts, $host, $username, $summary, $minor, $revision,
2011 $languages, $cluster, $last) = @_;
2012 my $name = ItemName
($id);
2013 $summary = PageHtml
($id, 50*1024, T
('This page is too big to send over RSS.'))
2014 if (GetParam
('full', 0)); # full page means summary is not shown
2015 my $date = TimeToRFC822
($ts);
2016 $username = QuoteHtml
($username);
2017 $username = $host unless $username;
2018 my $rss = "<item>\n";
2019 $rss .= "<title>$name</title>\n";
2020 my $link = ScriptUrl
(GetParam
('all', $cluster)
2021 ? GetPageParameters
('browse', $id, $revision, $cluster, $last)
2023 $rss .= "<link>$link</link>\n<guid>$link</guid>\n";
2024 $rss .= "<description>" . QuoteHtml
($summary) . "</description>\n" if $summary;
2025 $rss .= "<pubDate>" . $date . "</pubDate>\n";
2026 $rss .= "<comments>" . ScriptUrl
($CommentsPrefix . UrlEncode
($id))
2027 . "</comments>\n" if $CommentsPrefix and $id !~ /^$CommentsPrefix/o
;
2028 $rss .= "<dc:contributor>" . $username . "</dc:contributor>\n" if $username;
2029 $rss .= "<wiki:status>" . (1 == $revision ?
'new' : 'updated')
2030 . "</wiki:status>\n";
2031 $rss .= "<wiki:importance>" . ($minor ?
'minor' : 'major')
2032 . "</wiki:importance>\n";
2033 $rss .= "<wiki:version>" . $revision . "</wiki:version>\n";
2034 $rss .= "<wiki:history>" . ScriptUrl
("action=history;id=" . UrlEncode
($id))
2035 . "</wiki:history>\n";
2036 $rss .= "<wiki:diff>" . ScriptUrl
("action=browse;diff=1;id=" . UrlEncode
($id))
2037 . "</wiki:diff>\n" if $UseDiff and GetParam
('diffrclink', 1);
2038 return $rss . "</item>\n";
2042 print GetHttpHeader
('application/xml');
2050 if (GetParam
('raw', 0)) {
2051 print GetHttpHeader
('text/plain'),
2052 RcTextItem
('title', Ts
('History of %s', NormalToFree
($OpenPageName))),
2053 RcTextItem
('date', TimeToText
($Now)),
2054 RcTextItem
('link', ScriptUrl
("action=history;id=$OpenPageName;raw=1")),
2055 RcTextItem
('generator', 'Oddmuse');
2057 my @languages = split(/,/, $Page{languages
});
2058 RcTextRevision
($id, $Page{ts
}, $Page{host
}, $Page{username
}, $Page{summary
},
2059 $Page{minor
}, $Page{revision
}, \
@languages, undef, 1);
2060 foreach my $revision (GetKeepRevisions
($OpenPageName)) {
2061 my %keep = GetKeptRevision
($revision);
2062 @languages = split(/,/, $keep{languages
});
2063 RcTextRevision
($id, $keep{ts
}, $keep{host
}, $keep{username
},
2064 $keep{summary
}, $keep{minor
}, $keep{revision
}, \
@languages);
2067 print GetHeader
('', Ts
('History of %s', NormalToFree
($id)),
2068 undef, undef, undef, GetFooterLinks
($id, 'history'));
2070 my $rollback = UserCanEdit
($id, 0) && (GetParam
('username', '')
2072 my $date = CalcDay
($Page{ts
});
2073 my @html = (GetHistoryLine
($id, \
%Page, $row++, $rollback, $date, 1));
2074 foreach my $revision (GetKeepRevisions
($OpenPageName)) {
2075 my %keep = GetKeptRevision
($revision);
2076 my $new = CalcDay
($keep{ts
});
2077 push(@html, GetHistoryLine
($id, \
%keep, $row++, $rollback,
2078 $new, $new ne $date));
2081 @html = (GetFormStart
(undef, 'get', 'history'),
2082 $q->p($q->submit({-name
=>T
('Compare')}),
2083 # don't use $q->hidden here!
2084 $q->input({-type
=>'hidden',-name
=>'action',-value
=>'browse'}),
2085 $q->input({-type
=>'hidden', -name
=>'diff', -value
=>'1'}),
2086 $q->input({-type
=>'hidden', -name
=>'id', -value
=>$id})),
2087 $q->table({-class=>'history'}, @html),
2088 $q->p($q->submit({-name
=>T
('Compare')})),
2089 $q->end_form()) if $UseDiff;
2090 push(@html, $q->p(ScriptLink
('title=' . UrlEncode
($id) . ';text='
2091 . UrlEncode
($DeletedPage) . ';summary='
2092 . UrlEncode
(T
('Deleted')),
2093 T
('Mark this page for deletion'))))
2094 if $KeepDays and $rollback and $Page{revision
};
2095 print $q->div({-class=>'content history'}, @html);
2096 PrintFooter
($id, 'history');
2100 sub GetHistoryLine
{
2101 my ($id, $dataref, $row, $rollback, $date, $newday) = @_;
2102 my %data = %$dataref;
2103 my $revision = $data{revision
};
2104 return $q->p(T
('No other revisions available')) unless $revision;
2105 my $html = CalcTime
($data{ts
});
2106 if (0 == $row) { # current revision
2107 $html .= ' (' . T
('current') . ')' if $rollback;
2108 $html .= ' ' . GetPageLink
($id, Ts
('Revision %s', $revision));
2110 $html .= ' ' . $q->submit("rollback-$data{ts}", T
('rollback')) if $rollback;
2111 $html .= ' ' . GetOldPageLink
('browse', $id, $revision,
2112 Ts
('Revision %s', $revision));
2114 my $host = $data{host
};
2115 $host = $data{ip
} unless $host;
2116 $html .= T
(' . . . . ') . GetAuthorLink
($host, $data{username
});
2117 $html .= $q->span({class=>'dash'}, ' – ')
2118 . $q->strong(QuoteHtml
($data{summary
})) if $data{summary
};
2119 $html .= ' ' . $q->em({class=>'type'}, T
('(minor)')) . ' ' if $data{minor
};
2121 my %attr1 = (-type
=>'radio', -name
=>'diffrevision', -value
=>$revision);
2122 $attr1{-checked
} = 'checked' if 1==$row;
2123 my %attr2 = (-type
=>'radio', -name
=>'revision', -value
=> $row ?
$revision : '');
2124 $attr2{-checked
} = 'checked' if 0==$row; # first row is special
2125 $html = $q->Tr($q->td($q->input(\
%attr1)), $q->td($q->input(\
%attr2)),
2127 $html = $q->Tr($q->td({-colspan
=>3}, $q->strong($date))) . $html if $newday;
2130 $html = $q->strong($date) . $q->br() . $html if $newday;
2135 sub DoContributors
{
2137 SetParam
('rcidonly', $id);
2139 print GetHeader
('', Ts
('Contributors to %s', NormalToFree
($id || $SiteName)));
2141 for my $line (GetRcLines
(1)) {
2142 my ($ts, $pagename, $minor, $summary, $host, $username) = @
$line;
2143 $contrib{$username}++ if $username;
2145 print $q->div({-class=>'content contrib'},
2146 $q->p(map { GetPageLink
($_) } sort(keys %contrib)));
2150 sub RollbackPossible
{
2151 my $ts = shift; # there can be no rollback to the most recent change(s) made (1s resolution!)
2152 return $ts != $LastUpdate && ($Now - $ts) < $KeepDays * 86400; # 24*60*60
2157 my $to = GetParam
('to', 0);
2158 ReportError
(T
('Missing target for rollback.'), '400 BAD REQUEST') unless $to;
2159 ReportError
(T
('Target for rollback is too far back.'), '400 BAD REQUEST') unless $page or RollbackPossible
($to);
2160 ReportError
(T
('A username is required for ordinary users.'), '403 FORBIDDEN') unless GetParam
('username', '') or UserIsEditor
();
2162 if (not $page) { # cannot just use list length because of ('')
2163 return unless UserIsAdminOrError
(); # only admins can do mass changes
2164 SetParam
('showedit', 1); # make GetRcLines return minor edits as well
2165 SetParam
('all', 1); # prevent LatestChanges from interfering
2166 SetParam
('rollback', 1); # prevent StripRollbacks from interfering
2167 my %ids = map { my ($ts, $id) = @
$_; $id => 1; } # make unique via hash
2168 GetRcLines
($Now - $KeepDays * 86400); # 24*60*60
2173 RequestLockOrError
();
2174 print GetHeader
('', T
('Rolling back changes'),
2175 undef, undef, undef, GetFooterLinks
($page)),
2176 $q->start_div({-class=>'content rollback'}), $q->start_p();
2177 foreach my $id (@ids) {
2179 my ($text, $minor, $ts) = GetTextAtTime
($to);
2180 if ($Page{text
} eq $text) {
2181 print T
("The two revisions are the same."), $q->br() if $page; # no message when doing mass revert
2182 } elsif (!UserCanEdit
($id, 1)) {
2183 print Ts
('Editing not allowed for %s.', $id), $q->br();
2184 } elsif (not UserIsEditor
() and my $rule = BannedContent
($text)) {
2185 print Ts
('Rollback of %s would restore banned content.', $id), $rule, $q->br();
2187 Save
($id, $text, Ts
('Rollback to %s', TimeToText
($to)), $minor, ($Page{ip
} ne $ENV{REMOTE_ADDR
}));
2188 print Ts
('%s rolled back', GetPageLink
($id)), ($ts ?
' ' . Ts
('to %s', TimeToText
($to)) : ''), $q->br();
2191 WriteRcLog
('[[rollback]]', $page, $to); # leave marker
2192 print $q->end_p() . $q->end_div();
2198 my ($id, @rest) = @_;
2200 push(@menu, ScriptLink
('action=index',
2201 T
('Index of all pages'), 'index'))
2203 push(@menu, ScriptLink
('action=version',
2204 T
('Wiki Version'), 'version'))
2205 if $Action{version
};
2206 push(@menu, ScriptLink
('action=unlock',
2207 T
('Unlock Wiki'), 'unlock'))
2209 push(@menu, ScriptLink
('action=password',
2210 T
('Password'), 'password'))
2211 if $Action{password
};
2212 push(@menu, ScriptLink
('action=maintain',
2213 T
('Run maintenance'), 'maintain'))
2214 if $Action{maintain
};
2215 if (UserIsAdmin
()) {
2216 push(@menu, ScriptLink
('action=clear',
2217 T
('Clear Cache'), 'clear'))
2219 if ($Action{editlock
}) {
2220 if (-f
"$DataDir/noedit") {
2221 push(@menu, ScriptLink
('action=editlock;set=0',
2222 T
('Unlock site'), 'editlock 0'));
2224 push(@menu, ScriptLink
('action=editlock;set=1',
2225 T
('Lock site'), 'editlock 1'));
2228 if ($id and $Action{pagelock
}) {
2229 my $title = NormalToFree
($id);
2230 if (-f GetLockedPageFile
($id)) {
2231 push(@menu, ScriptLink
('action=pagelock;set=0;id='
2233 Ts
('Unlock %s', $title),
2236 push(@menu, ScriptLink
('action=pagelock;set=1;id='
2238 Ts
('Lock %s', $title),
2243 foreach my $sub (@MyAdminCode) {
2244 &$sub($id, \
@menu, \
@rest);
2245 $Message .= $q->p($@
) if $@
; # since this happens before GetHeader is called, the message will be shown
2247 print GetHeader
('', T
('Administration')),
2248 $q->div({-class=>'content admin'}, $q->p(T
('Actions:')), $q->ul($q->li(\
@menu)),
2249 $q->p(T
('Important pages:')) . $q->ul(map { $q->li(GetPageOrEditLink
($_, NormalToFree
($_))) if $_;
2250 } sort keys %AdminPages),
2251 $q->p(Ts
('To mark a page for deletion, put <strong>%s</strong> on the first line.',
2252 $DeletedPage)), @rest);
2256 sub GetPageParameters
{
2257 my ($action, $id, $revision, $cluster, $last) = @_;
2258 $id = FreeToNormal
($id);
2259 my $link = "action=$action;id=" . UrlEncode
($id);
2260 $link .= ";revision=$revision" if $revision and not $last;
2261 $link .= ';rcclusteronly=' . UrlEncode
($cluster) if $cluster;
2265 sub GetOldPageLink
{
2266 my ($action, $id, $revision, $name, $cluster, $last) = @_;
2267 return ScriptLink
(GetPageParameters
($action, $id, $revision, $cluster, $last),
2268 NormalToFree
($name), 'revision');
2272 my ($text, $class, $name, $title) = @_;
2273 my $id = UrlEncode
(QuoteRegexp
('"' . $text . '"'));
2274 $name = UrlEncode
($name);
2275 $text = NormalToFree
($text);
2276 $id =~ s/_/+/g; # Search for url-escaped spaces
2277 return ScriptLink
('search=' . $id, $text, $class, $name, $title);
2280 sub ScriptLinkDiff
{
2281 my ($diff, $id, $text, $new, $old) = @_;
2282 my $action = 'action=browse;diff=' . $diff . ';id=' . UrlEncode
($id);
2283 $action .= ";diffrevision=$old" if ($old and $old ne '');
2284 $action .= ";revision=$new" if ($new and $new ne '');
2285 return ScriptLink
($action, $text, 'diff');
2289 my ($host, $username) = @_;
2290 $username = FreeToNormal
($username);
2291 my $name = NormalToFree
($username);
2292 if (ValidId
($username) ne '') { # ValidId() returns error string
2293 $username = ''; # Just pretend it isn't there.
2295 if ($username and $RecentLink) {
2296 return ScriptLink
(UrlEncode
($username), $name, 'author', undef, Ts
('from %s', $host));
2297 } elsif ($username) {
2298 return $q->span({-class=>'author'}, $name) . ' ' . Ts
('from %s', $host);
2303 sub GetHistoryLink
{
2304 my ($id, $text) = @_;
2305 my $action = 'action=history;id=' . UrlEncode
(FreeToNormal
($id));
2306 return ScriptLink
($action, $text, 'history');
2310 my ($id, $text) = @_;
2311 return ScriptLink
('action=rc;all=1;from=1;showedit=1;rcidonly=' . UrlEncode
(FreeToNormal
($id)), $text, 'rc');
2315 my ($id, $title) = @_;
2316 return $q->h1(GetSearchLink
($id, '', '', T
('Click to search for references to this page')))
2319 return $q->h1($title);
2323 return if (!$LogoUrl);
2324 my $alt = T
('[Home]');
2325 my $url = $IndexHash{$LogoUrl} ? GetDownloadLink
($LogoUrl, 2) : $LogoUrl;
2326 return ScriptLink
(UrlEncode
($HomePage),
2327 $q->img({-src
=>$url, -alt
=>$alt, -class=>'logo'}), 'logo');
2331 my $day = CalcDay
($Now);
2332 my $month_and_day = substr($day, 5);
2333 my $message = $SpecialDays{$month_and_day};
2335 return '' unless $message;
2336 return $q->div({-class=>'motd'},
2337 $q->span({-class=>'specialdays'}, $message))
2341 my ($id, $title, $oldId, $nocache, $status, $editLinks) = @_;
2342 my $embed = GetParam
('embed', $EmbedWiki);
2343 my $result = GetHttpHeader
('text/html', $nocache, $status);
2345 $Message .= $q->p('(' . Ts
('redirected from %s', GetEditLink
($oldId, $oldId)) . ')');
2347 $result .= GetHtmlHeader
(Ts
('%s: ', $SiteName) . UnWiki
($title), $id);
2349 $result .= $q->div({-class=>'header'}, $q->div({-class=>'message'}, $Message)) if $Message;
2352 $result .= $q->start_div({-class=>'header'});
2353 $result .= GetLogoUrl
();
2355 $result .= GetLoginLinks
($id);
2357 my $topLinkBar = GetParam
('toplinkbar', $TopLinkBar);
2358 my $gotoBar = $topLinkBar & 1; # bit0
2359 my $editBar = $topLinkBar & 2; # bit1
2361 $result .= GetGotoBar
($id) if $gotoBar;
2362 $result .= GetExploreGotoBar
() if $gotoBar;
2363 $result .= GetEditorGotoBar
() if $gotoBar;
2364 $result .= GetMotd
() if $topLinkBar;
2365 $result .= $editLinks if $editBar && UserIsEditor
();
2367 $result .= $q->div({-class=>'message'}, $Message) if $Message;
2368 $result .= GetTitle
($id, $title);
2369 return $result . $q->end_div() . $q->start_div({-class=>'wrapper'});
2373 return if $PrintedHeader;
2375 my ($type, $ts, $status, $encoding) = @_; # $ts is undef, a ts, or 'nocache'
2376 $q->charset($type =~ m!^(text/|application/xml)! ?
'utf-8' : ''); # text/plain, text/html, application/xml: UTF-8
2377 my %headers = (-cache_control
=>($UseCache < 0 ?
'no-cache' : 'max-age=10'));
2378 $headers{-etag
} = $ts || PageEtag
() if GetParam
('cache', $UseCache) >= 2;
2379 $headers{'-last-modified'} = TimeToRFC822
($ts) if $ts and $ts ne 'nocache'; # RFC 2616 section 13.3.4
2380 $headers{-type
} = GetParam
('mime-type', $type);
2381 $headers{-status
} = $status if $status;
2382 $headers{-Content_Encoding
} = $encoding if $encoding;
2383 my $cookie = Cookie
();
2384 $headers{-cookie
} = $cookie if $cookie;
2385 if ($q->request_method() eq 'HEAD') {
2386 print $q->header(%headers), "\n\n"; # add newlines for FCGI because of exit()
2387 exit; # total shortcut -- HEAD never expects anything other than the header!
2389 return $q->header(%headers);
2393 my ($changed, $visible, %params);
2394 foreach my $key (keys %CookieParameters) {
2395 my $default = $CookieParameters{$key};
2396 my $value = GetParam
($key, $default);
2397 $params{$key} = $value if $value ne $default;
2398 # The cookie is considered to have changed under the following
2399 # condition: If the value was already set, and the new value is
2400 # not the same as the old value, or if there was no old value, and
2401 # the new value is not the default.
2402 my $change = (defined $OldCookie{$key} ?
($value ne $OldCookie{$key}) : ($value ne $default));
2403 $visible = 1 if $change and not $InvisibleCookieParameters{$key};
2404 $changed = 1 if $change; # note if any parameter changed and needs storing
2406 return $changed, $visible, %params;
2410 my ($changed, $visible, %params) = CookieData
(); # params are URL encoded
2412 my $cookie = join(UrlEncode
($FS), %params); # no CTL in field values
2413 my $result = $q->cookie(-name
=>$CookieName, -value
=>$cookie,
2415 $Message .= $q->p(T
('Cookie: ') . $CookieName . ', '
2416 . join(', ', map {$_ . '=' . $params{$_}}
2417 keys(%params))) if $visible;
2423 sub GetHtmlHeader
{ # always HTML!
2424 my ($title, $id) = @_;
2425 my $base = $SiteBase ?
$q->base({-href
=>$SiteBase}) : '';
2426 $base .= '<link rel="alternate" type="application/wiki" title="'
2427 . T
('Edit this page') . '" href="'
2428 . ScriptUrl
('action=edit;id=' . UrlEncode
(GetId
())) . '" />' if $id;
2429 return $DocumentHeader
2430 . $q->head($q->title($title) . $base
2431 . GetCss
() . GetRobots
() . GetFeeds
() . GetFavicon
() . $HtmlHeaders
2432 . '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />')
2433 . '<body class="' . GetParam
('theme', $ScriptName) . '">';
2436 sub GetRobots
{ # NOINDEX for non-browse pages.
2437 if (GetParam
('action', 'browse') eq 'browse'
2438 and not GetParam
('revision', '')) {
2439 return '<meta name="robots" content="INDEX,FOLLOW" />';
2441 return '<meta name="robots" content="NOINDEX,FOLLOW" />';
2445 sub GetFeeds
{ # default for $HtmlHeaders
2446 my $html = '<link rel="alternate" type="application/rss+xml" title="'
2447 . QuoteHtml
($SiteName) . '" href="' . $ScriptName . '?action=rss" />';
2448 my $id = GetId
(); # runs during Init, not during DoBrowseRequest
2449 $html .= '<link rel="alternate" type="application/rss+xml" title="'
2450 . QuoteHtml
("$SiteName: $id") . '" href="' . $ScriptName
2451 . '?action=rss;rcidonly=' . UrlEncode
(FreeToNormal
($id)) . '" />' if $id;
2452 my $username = GetParam
('username', '');
2453 $html .= '<link rel="alternate" type="application/rss+xml" '
2454 . 'title="Follow-ups for ' . NormalToFree
($username) . '" '
2455 . 'href="' . ScriptUrl
('action=rss;followup=' . UrlEncode
($username))
2456 . '" />' if $username;
2460 sub GetCss
{ # prevent javascript injection
2461 my @css = map { s/\".*//; $_; } split(/\s+/, GetParam
('css', ''));
2462 push (@css, $StyleSheet) if $StyleSheet and not @css;
2463 push (@css, "$ScriptName?action=browse;id=" . UrlEncode
($StyleSheetPage) . ";raw=1;mime-type=text/css")
2464 if $IndexHash{$StyleSheetPage} and not @css;
2465 push (@css, 'http://www.oddmuse.org/default.css') unless @css;
2466 return join('', map { qq(<link type
="text/css" rel
="stylesheet" href
="$_" />) } @css);
2471 my $url = $IndexHash{$Favicon} ? GetDownloadLink
($Favicon, 2) : $Favicon;
2472 return qq(<link rel
="shortcut icon" href
="$url" />);
2478 sub PrintPageContent
{
2479 my ($text, $revision, $comment) = @_;
2480 print $q->start_div({-class=>'content browse'});
2481 if ($revision eq '' and $Page{blocks
} and GetParam
('cache', $UseCache) > 0) {
2484 my $savecache = ($Page{revision
} > 0 and $revision eq ''); # new page not cached
2485 PrintWikiToHTML
($text, $savecache, $revision); # unlocked, with anchors, unlocked
2488 print $q->start_div({-class=>'preview'}), $q->hr();
2489 print $q->h2(T
('Preview:'));
2490 # no caching, current revision, unlocked
2491 PrintWikiToHTML
(AddComment
('', $comment));
2492 print $q->hr(), $q->h2(T
('Preview only, not yet saved')), $q->end_div();
2493 } print $q->end_div();
2497 my ($id, $rev, $comment) = @_;
2498 if (GetParam
('embed', $EmbedWiki)) {
2499 print $q->end_html, "\n";
2502 print GetCommentForm
($id, $rev, $comment),
2503 $q->start_div({-class=>'wrapper close'}), $q->end_div(), $q->end_div(),
2504 $q->start_div({-class=>'footer'}), $q->hr(), GetGotoBar
($id),
2505 GetFooterLinks
($id, $rev), GetFooterTimestamp
($id, $rev);
2506 if ($DataDir =~ m
|/tmp/|) {
2507 print $q->p($q->strong(T
('Warning') . ': ')
2508 . Ts
('Database is stored in temporary directory %s', $DataDir));
2510 print T
($FooterNote) if $FooterNote;
2511 print $q->p(GetValidatorLink
()) if GetParam
('validate', $ValidatorLink);
2512 print $q->p(Ts
('%s seconds', (time - $Now))) if GetParam
('timing',0);
2513 print $q->end_div();
2514 PrintMyContent
($id) if defined(&PrintMyContent
);
2515 foreach my $sub (@MyFooters) {
2518 print $q->end_html, "\n";
2521 sub GetFooterTimestamp
{
2522 my ($id, $rev) = @_;
2523 if ($id and $rev ne 'history' and $rev ne 'edit' and $Page{revision
}) {
2524 my @elements = ($q->br(), ($rev eq '' ? T
('Last edited') : T
('Edited')), TimeToText
($Page{ts
}),
2525 Ts
('by %s', GetAuthorLink
($Page{host
}, $Page{username
})));
2526 push(@elements, ScriptLinkDiff
(2, $id, T
('(diff)'), $rev)) if $UseDiff and $Page{revision
} > 1;
2527 return $q->span({-class=>'time'}, @elements);
2534 return '' unless ($Action{admin
} and GetParam
('action', '') ne 'admin');
2536 my $action = 'action=admin';
2537 $action .= ';id=' . UrlEncode
($id) if $id;
2538 return ScriptLink
($action, T
('Administration'), 'admin');
2545 my $adminLink = GetAdminLink
($id) if UserIsAdmin
();
2546 push(@elements, $adminLink) if $adminLink;
2548 push(@elements, ScriptLink
('action=password', T
('Password'), 'password'))
2549 if $Action{password
};
2551 return @elements ?
$q->span({-class=>'login bar'}, $q->br(), @elements) : '';
2554 sub GetFooterLinks
{
2555 my ($id, $rev) = @_;
2557 if ($id and $rev ne 'history' and $rev ne 'edit') {
2558 if ($CommentsPrefix) {
2559 if ($id =~ /^$CommentsPrefix(.*)/o) {
2560 push(@elements, GetPageLink
($1, undef, 'original', T
('a')));
2562 push(@elements, GetPageLink
($CommentsPrefix . $id, undef, 'comment', T
('c')));
2565 if (UserCanEdit
($id, 0)) {
2566 if ($rev) { # showing old revision
2567 push(@elements, GetOldPageLink
('edit', $id, $rev, Ts
('Edit revision %s of this page', $rev)));
2568 } else { # showing current revision
2569 push(@elements, GetEditLink
($id, T
('Edit this page'), undef, T
('e')));
2571 } else { # no permission or generated page
2572 push(@elements, ScriptLink
('action=password', T
('This page is read-only'), 'password'));
2575 push(@elements, GetHistoryLink
($id, T
('View other revisions'))) if $Action{history
} and $id and $rev ne 'history';
2576 push(@elements, GetPageLink
($id, T
('View current revision')),
2577 GetRCLink
($id, T
('View all changes'))) if $Action{history
} and $rev ne '';
2578 push(@elements, ScriptLink
("action=contrib;id=" . UrlEncode
($id), T
('View contributors'), 'contrib'))
2579 if $Action{contrib
} and $id and $rev eq 'history';
2581 push(@elements, GetRawLink
($id, T
('Raw'), T
('r')));
2583 return @elements ?
$q->span({-class=>'edit bar'}, $q->br(), @elements) : '';
2586 sub GetCommentForm
{
2587 my ($id, $rev, $comment) = @_;
2588 if ($CommentsPrefix ne '' and $id and $rev ne 'history' and $rev ne 'edit'
2589 and $id =~ /^$CommentsPrefix/o and UserCanEdit
($id, 0, 1)) {
2590 return $q->div({-class=>'comment'}, GetFormStart
(undef, undef, 'comment'), # protected by questionasker
2591 $q->p(GetHiddenValue
('title', $id),
2592 GetTextArea
('aftertext', $comment ?
$comment : $NewComment, 10)), $EditNote,
2593 $q->p($q->span({-class=>'username'},
2594 $q->label({-for=>'username'}, T
('Username:')), ' ',
2595 $q->textfield(-name
=>'username', -id
=>'username',
2596 -default=>GetParam
('username', ''),
2597 -override
=>1, -size
=>20, -maxlength
=>50)),
2598 $q->span({-class=>'homepage'},
2599 $q->label({-for=>'homepage'}, T
('Homepage URL:')), ' ',
2600 $q->textfield(-name
=>'homepage', -id
=>'homepage',
2601 -default=>GetParam
('homepage', ''),
2602 -override
=>1, -size
=>40, -maxlength
=>100))),
2603 $q->p($q->submit(-name
=>'Save', -accesskey
=>T
('s'), -value
=>T
('Save')), ' ',
2604 $q->submit(-name
=>'Preview', -accesskey
=>T
('p'), -value
=>T
('Preview'))),
2611 my ($ignore, $method, $class) = @_;
2614 return $q->start_multipart_form(-method
=>$method, -action
=>$FullUrl,
2615 -accept_charset
=>'utf-8', -class=>$class);
2619 my $form = # $q->label({-for=>'search'}, T('Search:')) . ' '
2621 . $q->textfield(-name
=>'search', -id
=>'search', -size
=>15, -placeholder
=>"Search",
2622 -accesskey
=>T
('f')) . ' ';
2624 $form .= $q->label({-for=>'replace'}, T
('Replace:')) . ' '
2625 . $q->textfield(-name
=>'replace', -id
=>'replace', -size
=>20) . ' '
2626 . $q->checkbox(-name
=>'delete', -label
=>T
('Delete')) . ' ';
2629 $form .= $q->label({-for=>'searchlang'}, T
('Language:')) . ' '
2630 . $q->textfield(-name
=>'lang', -id
=>'searchlang', -size
=>10,
2631 -default=>GetParam
('lang', '')) . ' ';
2633 return GetFormStart
(undef, 'get', 'search')
2634 . $q->p($form . $q->submit('dosearch', T
('Go!'))) . $q->endform;
2637 sub GetValidatorLink
{
2638 return $q->a({-href
=> 'http://validator.w3.org/check/referer'}, T
('Validate HTML'))
2639 . ' ' . $q->a({-href
=>'http://jigsaw.w3.org/css-validator/check/referer'}, T
('Validate CSS'));
2642 sub GetGotoBar
{ # ignore $id parameter
2643 return $q->span({-class=>'gotobar bar'}, $q->br(), (map { GetPageLink
($_) }
2644 @UserGotoBarPages), $UserGotoBar);
2647 sub GetEditorGotoBar
{
2648 return !UserIsEditor
() ?
'' :
2649 $q->span({-class=>'editorgotobar bar'}, $q->br(),
2650 (map { GetPageLink
($_) } @EditorGotoBarPages), $EditorGotoBar);
2653 sub GetExploreGotoBar
{
2654 return $q->span({-class=>'explorebar bar'}, $q->br(), (map { GetPageLink
($_) }
2655 @ExploreGotoBarPages), GetSearchForm
());
2659 my ($type, $old, $new, $text, $summary) = @_;
2660 my $intro = T
('Last edit');
2661 my $diff = GetCacheDiff
($type == 1 ?
'major' : 'minor');
2662 # compute old revision if cache is disabled or no cached diff is available
2663 if (not $old and (not $diff or GetParam
('cache', $UseCache) < 1)) {
2665 $old = $Page{lastmajor
} - 1;
2666 ($text, $new, $summary) = GetTextRevision
($Page{lastmajor
}, 1)
2667 unless $new or $Page{lastmajor
} == $Page{revision
};
2671 $old = $Page{revision
} - 1;
2674 $summary = $Page{summary
} if not $summary and not $new;
2675 $summary = $q->p({-class=>'summary'}, T
('Summary:') . ' ' . $summary) if $summary;
2676 if ($old > 0) { # generate diff if the computed old revision makes sense
2677 $diff = GetKeptDiff
($text, $old);
2678 $intro = Tss
('Difference between revision %1 and %2', $old,
2679 $new ? Ts
('revision %s', $new) : T
('current revision'));
2680 } elsif ($type == 1 and $Page{lastmajor
} != $Page{revision
}) {
2681 $intro = Ts
('Last major edit (%s)', ScriptLinkDiff
(1, $OpenPageName, T
('later minor edits'),
2682 undef, $Page{lastmajor
}||1));
2684 $diff =~ s!<p><strong>(.*?)</strong></p>!'<p><strong>' . T($1) . '</strong></p>'!ge;
2685 $diff = T
('No diff available.') unless $diff;
2686 print $q->div({-class=>'diff'}, $q->p($q->b($intro)), $summary, $diff);
2691 my $diff = $Page{"diff-$type"};
2692 $diff = $Page{"diff-minor"} if ($diff eq '1'); # if major eq minor diff
2697 my ($new, $revision) = @_;
2698 $revision = 1 unless $revision;
2699 my ($old, $rev) = GetTextRevision
($revision, 1);
2700 return '' unless $rev;
2701 return T
("The two revisions are the same.") if $old eq $new;
2702 return GetDiff
($old, $new, $rev);
2705 sub DoDiff
{ # Actualy call the diff program
2706 CreateDir
($TempDir);
2707 my $oldName = "$TempDir/old";
2708 my $newName = "$TempDir/new";
2709 RequestLockDir
('diff') or return '';
2710 WriteStringToFile
($oldName, $_[0]);
2711 WriteStringToFile
($newName, $_[1]);
2712 my $diff_out = `diff $oldName $newName`;
2713 utf8
::decode
($diff_out); # needs decoding
2714 $diff_out =~ s/\\ No newline.*\n//g; # Get rid of common complaint.
2715 ReleaseLockDir
('diff');
2716 # No need to unlink temp files--next diff will just overwrite.
2721 my ($old, $new, $revision) = @_;
2722 my $old_is_file = (TextIsFile
($old))[0] || '';
2723 my $old_is_image = ($old_is_file =~ /^image\//);
2724 my $new_is_file = TextIsFile
($new);
2725 if ($old_is_file or $new_is_file) {
2726 return $q->p($q->strong(T
('Old revision:')))
2727 . $q->div({-class=>'old'}, # don't pring new revision, because that's the one that gets shown!
2728 $q->p($old_is_file ? GetDownloadLink
($OpenPageName, $old_is_image, $revision) : $old))
2730 $old =~ s/[\r\n]+/\n/g;
2731 $new =~ s/[\r\n]+/\n/g;
2732 return ImproveDiff
(DoDiff
($old, $new));
2735 sub ImproveDiff
{ # NO NEED TO BE called within a diff lock
2736 my $diff = QuoteHtml
(shift);
2738 my @hunks = split (/^(\d+,?\d*[adc]\d+,?\d*\n)/m, $diff);
2739 my $result = shift (@hunks); # intro
2740 while ($#hunks > 0) # at least one header and a real hunk
2742 my $header = shift (@hunks);
2743 $header =~ s
|^(\d
+.*c
.*)|<p
><strong
>Changed
:</strong></p>| # T('Changed:')
2744 or $header =~ s
|^(\d
+.*d
.*)|<p
><strong
>Deleted
:</strong></p>| # T('Deleted:')
2745 or $header =~ s
|^(\d
+.*a
.*)|<p
><strong
>Added
:</strong></p>|; # T('Added:')
2747 my $chunk = shift (@hunks);
2748 my ($old, $new) = split (/\n---\n/, $chunk, 2);
2749 if ($old and $new) {
2750 ($old, $new) = DiffMarkWords
($old, $new);
2751 $result .= "$old<p><strong>to</strong></p>\n$new"; # T('to')
2753 if (substr($chunk,0,2) eq '&g') {
2754 $result .= DiffAddPrefix
(DiffStripPrefix
($chunk), '> ', 'new');
2756 $result .= DiffAddPrefix
(DiffStripPrefix
($chunk), '< ', 'old');
2764 my ($old, $new) = map { DiffStripPrefix
($_) } @_;
2765 my @diffs = grep(/^\d/, split(/\n/, DoDiff
(join("\n",split(/\s+|\b/,$old)) . "\n",
2766 join("\n",split(/\s+|\b/,$new)) . "\n")));
2767 foreach my $diff (reverse @diffs) { # so that new html tags don't confuse word counts
2768 my ($start1,$end1,$type,$start2,$end2) = $diff =~ /^(\d+),?(\d*)([adc])(\d+),?(\d*)$/mg;
2769 if ($type eq 'd' or $type eq 'c') {
2770 $end1 = $start1 unless $end1;
2771 $old = DiffHtmlMarkWords
($old,$start1,$end1);
2773 if ($type eq 'a' or $type eq 'c') {
2774 $end2 = $start2 unless $end2;
2775 $new = DiffHtmlMarkWords
($new,$start2,$end2);
2778 return (DiffAddPrefix
($old, '< ', 'old'),
2779 DiffAddPrefix
($new, '> ', 'new'));
2782 sub DiffHtmlMarkWords
{
2783 my ($text,$start,$end) = @_;
2784 my @fragments = split(/(\s+|\b)/, $text);
2785 splice(@fragments, 2 * ($start - 1), 0, '<strong class="changes">');
2786 splice(@fragments, 2 * $end, 0, '</strong>');
2787 my $result = join('', @fragments);
2788 $result =~ s!&<(/?)strong([^>]*)>(amp|[gl]t);!<$1strong$2>&$3;!g;
2789 $result =~ s!&(amp|[gl]t)<(/?)strong([^>]*)>;!&$1;<$2strong$3>!g;
2793 sub DiffStripPrefix
{
2795 $str =~ s/^&[lg]t; //gm;
2800 my ($str, $prefix, $class) = @_;
2801 my @lines = split(/\n/,$str);
2802 for my $line (@lines) {
2803 $line = $prefix . $line;
2805 return $q->div({-class=>$class},$q->p(join($q->br(), @lines)));
2808 sub ParseData
{ # called a lot during search, so it was optimized
2809 my $data = shift; # by eliminating non-trivial regular expressions
2811 my $end = index($data, ': ');
2812 my $key = substr($data, 0, $end);
2813 my $start = $end += 2; # skip ': '
2814 while ($end = index($data, "\n", $end) + 1) { # include \n
2815 next if substr($data, $end, 1) eq "\t"; # continue after \n\t
2816 $result{$key} = substr($data, $start, $end - $start - 1); # strip last \n
2817 $start = index($data, ': ', $end); # starting at $end begins the new key
2818 last if $start == -1;
2819 $key = substr($data, $end, $start - $end);
2820 $end = $start += 2; # skip ': '
2822 $result{$key} .= substr($data, $end, -1); # strip last \n
2823 foreach (keys %result) {
2824 $result{$_} =~ s/\n\t/\n/g;
2829 sub OpenPage
{ # Sets global variables
2831 if ($OpenPageName eq $id) {
2834 if ($IndexHash{$id}) {
2835 %Page = ParseData
(ReadFileOrDie
(GetPageFile
($id)));
2839 $Page{revision
} = 0;
2840 if ($id eq $HomePage
2841 and (open(F
, '<:utf8', $ReadMe)
2842 or open(F
, '<:utf8', 'README'))) {
2846 } elsif ($CommentsPrefix and $id =~ /^$CommentsPrefix(.*)/o) { # do nothing
2849 $OpenPageName = $id;
2852 sub GetTextAtTime
{ # call with opened page, return $minor if all pages between now and $ts are minor!
2854 my $minor = $Page{minor
};
2855 return ($Page{text
}, $minor, 0) if $Page{ts
} <= $ts; # current page is old enough
2856 return ($DeletedPage, $minor, 0) if $Page{revision
} == 1 and $Page{ts
} > $ts; # created after $ts
2857 my %keep = (); # info may be needed after the loop
2858 foreach my $revision (GetKeepRevisions
($OpenPageName)) {
2859 %keep = GetKeptRevision
($revision);
2860 $minor = 0 if not $keep{minor
} and $keep{ts
} >= $ts; # ignore keep{minor} if keep{ts} is too old
2861 return ($keep{text
}, $minor, 0) if $keep{ts
} <= $ts;
2863 return ($DeletedPage, $minor, 0) if $keep{revision
} == 1; # then the page was created after $ts!
2864 return ($keep{text
}, $minor, $keep{ts
}); # the oldest revision available is not old enough
2867 sub GetTextRevision
{
2868 my ($revision, $quiet) = @_;
2869 $revision =~ s/\D//g; # Remove non-numeric chars
2870 return ($Page{text
}, $revision, $Page{summary
}) unless $revision and $revision ne $Page{revision
};
2871 my %keep = GetKeptRevision
($revision);
2873 $Message .= $q->p(Ts
('Revision %s not available', $revision)
2874 . ' (' . T
('showing current revision instead') . ')') unless $quiet;
2875 return ($Page{text
}, '', '');
2877 $Message .= $q->p(Ts
('Showing revision %s', $revision)) unless $quiet;
2878 return ($keep{text
}, $revision, $keep{summary
});
2881 sub GetPageContent
{
2883 if ($IndexHash{$id}) {
2884 my %data = ParseData
(ReadFileOrDie
(GetPageFile
($id)));
2890 sub GetKeptRevision
{ # Call after OpenPage
2891 my ($status, $data) = ReadFile
(GetKeepFile
($OpenPageName, (shift)));
2892 return () unless $status;
2893 return ParseData
($data);
2898 return $PageDir . '/' . GetPageDirectory
($id) . "/$id.pg";
2902 my ($id, $revision) = @_; die "No revision for $id" unless $revision; #FIXME
2903 return $KeepDir . '/' . GetPageDirectory
($id) . "/$id/$revision.kp";
2907 my $id = shift; die 'No id' unless $id; #FIXME
2908 return $KeepDir . '/' . GetPageDirectory
($id) . '/' . $id;
2912 return bsd_glob
(GetKeepDir
(shift) . '/*.kp'); # files such as 1.kp, 2.kp, etc.
2915 sub GetKeepRevisions
{
2916 return sort {$b <=> $a} map { m/([0-9]+)\.kp$/; $1; } GetKeepFiles
(shift);
2919 sub GetPageDirectory
{
2921 if ($id =~ /^([a-zA-Z])/) {
2927 # Always call SavePage within a lock.
2928 sub SavePage
{ # updating the cache will not change timestamp and revision!
2929 ReportError
(T
('Cannot save a nameless page.'), '400 BAD REQUEST', 1) unless $OpenPageName;
2930 ReportError
(T
('Cannot save a page without revision.'), '400 BAD REQUEST', 1) unless $Page{revision
};
2931 CreatePageDir
($PageDir, $OpenPageName);
2932 WriteStringToFile
(GetPageFile
($OpenPageName), EncodePage
(%Page));
2936 return if ($Page{revision
} < 1); # Don't keep 'empty' revision
2937 delete $Page{blocks
}; # delete some info from the page
2938 delete $Page{flags
};
2939 delete $Page{'diff-major'};
2940 delete $Page{'diff-minor'};
2941 $Page{'keep-ts'} = $Now; # expire only $KeepDays from $Now!
2942 CreateKeepDir
($KeepDir, $OpenPageName);
2943 WriteStringToFile
(GetKeepFile
($OpenPageName, $Page{revision
}), EncodePage
(%Page));
2949 $result .= (shift @data) . ': ' . EscapeNewlines
(shift @data) . "\n" while (@data);
2953 sub EscapeNewlines
{
2954 $_[0] =~ s/\n/\n\t/g; # modify original instead of copying
2958 sub ExpireKeepFiles
{ # call with opened page
2959 return unless $KeepDays;
2960 my $expirets = $Now - ($KeepDays * 86400); # 24*60*60
2961 foreach my $revision (GetKeepRevisions
($OpenPageName)) {
2962 my %keep = GetKeptRevision
($revision);
2963 next if $keep{'keep-ts'} >= $expirets;
2964 next if $KeepMajor and $keep{revision
} == $Page{lastmajor
};
2965 unlink GetKeepFile
($OpenPageName, $revision);
2971 utf8
::encode
($file); # filenames are bytes!
2972 if (open(IN
, '<:utf8', $file)) {
2973 local $/ = undef; # Read complete files
2983 my ($status, $data);
2984 ($status, $data) = ReadFile
($file);
2986 ReportError
(Ts
('Cannot open %s', $file) . ": $!", '500 INTERNAL SERVER ERROR');
2991 sub WriteStringToFile
{
2992 my ($file, $string) = @_;
2993 utf8
::encode
($file);
2994 open(OUT
, '>:encoding(UTF-8)', $file)
2995 or ReportError
(Ts
('Cannot write %s', $file) . ": $!", '500 INTERNAL SERVER ERROR');
3000 sub AppendStringToFile
{
3001 my ($file, $string) = @_;
3002 utf8
::encode
($file);
3003 open(OUT
, '>>:encoding(UTF-8)', $file)
3004 or ReportError
(Ts
('Cannot write %s', $file) . ": $!", '500 INTERNAL SERVER ERROR');
3011 utf8
::encode
($newdir);
3012 return if -d
$newdir;
3013 mkdir($newdir, 0775)
3014 or ReportError
(Ts
('Cannot create %s', $newdir) . ": $!", '500 INTERNAL SERVER ERROR');
3018 my ($dir, $id) = @_;
3020 CreateDir
($dir . '/' . GetPageDirectory
($id));
3024 my ($dir, $id) = @_;
3025 CreatePageDir
($dir, $id);
3026 CreateDir
($dir . '/' . GetPageDirectory
($id) . '/' . $id);
3029 sub GetLockedPageFile
{
3031 return $PageDir . '/' . GetPageDirectory
($id) . "/$id.lck";
3034 sub RequestLockDir
{
3035 my ($name, $tries, $wait, $error, $retried) = @_;
3036 $tries = 4 unless $tries;
3037 $wait = 2 unless $wait;
3038 CreateDir
($TempDir);
3039 my $lock = $LockDir . $name;
3041 while (mkdir($lock, 0555) == 0) {
3042 if ($n++ >= $tries) {
3043 my $ts = (stat($lock))[9];
3044 if ($Now - $ts > $LockExpiration and $LockExpires{$name}
3046 ReleaseLockDir
($name); # try to expire lock (no checking)
3047 return 1 if RequestLockDir
($name, undef, undef, undef, 1);
3049 return 0 unless $error;
3050 ReportError
(Ts
('Could not get %s lock', $name) . ": $!. "
3051 . Ts
('The lock was created %s.', CalcTimeSince
($Now - $ts))
3052 . ($retried ?
' ' . T
('Maybe the user running this script is no longer allowed to remove the lock directory?') : ''),
3053 '503 SERVICE UNAVAILABLE');
3061 sub ReleaseLockDir
{
3062 my $name = shift; # We don't check whether we succeeded.
3063 rmdir($LockDir . $name); # Before fixing, make sure we only call this
3064 delete $Locks{$name}; # when we know the lock exists.
3067 sub RequestLockOrError
{
3068 # 10 tries, 3 second wait, die on error
3069 return RequestLockDir
('main', 10, 3, 1);
3073 ReleaseLockDir
('main');
3076 sub ForceReleaseLock
{
3077 my $pattern = shift;
3079 foreach my $name (bsd_glob
$pattern) {
3080 # First try to obtain lock (in case of normal edit lock)
3081 $forced = 1 if !RequestLockDir
($name, 5, 3, 0);
3082 ReleaseLockDir
($name); # Release the lock, even if we didn't get it.
3089 print GetHeader
('', T
('Unlock Wiki'), undef, 'nocache');
3090 print $q->p(T
('This operation may take several seconds...'));
3091 for my $lock (@KnownLocks) {
3092 if (ForceReleaseLock
($lock)) {
3093 $message .= $q->p(Ts
('Forced unlock of %s lock.', $lock));
3099 print $q->p(T
('No unlock required.'));
3105 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime(shift);
3106 return sprintf('%4d-%02d-%02d', $year+1900, $mon+1, $mday);
3110 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime(shift);
3111 return sprintf('%02d:%02d UTC', $hour, $min);
3116 if ($total >= 7200) {
3117 return Ts
('%s hours ago',int($total/3600));
3118 } elsif ($total >= 3600) {
3119 return T
('1 hour ago');
3120 } elsif ($total >= 120) {
3121 return Ts
('%s minutes ago',int($total/60));
3122 } elsif ($total >= 60) {
3123 return T
('1 minute ago');
3124 } elsif ($total >= 2) {
3125 return Ts
('%s seconds ago',int($total));
3126 } elsif ($total == 1) {
3127 return T
('1 second ago');
3129 return T
('just now');
3135 return CalcDay
($t) . ' ' . CalcTime
($t);
3138 sub TimeToW3
{ # Complete date plus hours and minutes: YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00)
3139 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime(shift); # use special UTC designator ("Z")
3140 return sprintf('%4d-%02d-%02dT%02d:%02dZ', $year+1900, $mon+1, $mday, $hour, $min);
3144 my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime(shift); # Sat, 07 Sep 2002 00:00:01 GMT
3145 return sprintf("%s, %02d %s %04d %02d:%02d:%02d GMT", qw(Sun Mon Tue Wed Thu Fri Sat)[$wday], $mday,
3146 qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)[$mon], $year+1900, $hour, $min, $sec);
3149 sub GetHiddenValue
{
3150 my ($name, $value) = @_;
3151 $q->param($name, $value);
3152 return $q->input({-type
=>"hidden", -name
=>$name, -value
=>$value});
3155 sub GetRemoteHost
{ # when testing, these variables are undefined.
3156 my $rhost = $ENV{REMOTE_HOST
}; # tests are written to avoid -w warnings.
3157 if (not $rhost and $UseLookup and $ENV{REMOTE_ADDR
}) {
3158 # Catch errors (including bad input) without aborting the script
3159 eval 'use Socket; my $iaddr = inet_aton($ENV{REMOTE_ADDR});'
3160 . '$rhost = gethostbyaddr($iaddr, AF_INET) if $iaddr;';
3163 $rhost = $ENV{REMOTE_ADDR
};
3168 sub FreeToNormal
{ # trim all spaces and convert them to underlines
3170 return '' unless $id;
3172 if (index($id, '_') > -1) { # Quick check for any space/underscores
3177 return UnquoteHtml
($id);
3181 my $id = shift; # id
3182 return NormalToFree
($id) unless GetParam
('short', 1) and $RssStrip;
3183 my $comment = $id =~ s/^($CommentsPrefix)//o; # strip first so that ^ works
3184 $id =~ s/^$RssStrip//o;
3185 $id = $CommentsPrefix . $id if $comment;
3186 return NormalToFree
($id);
3189 sub NormalToFree
{ # returns HTML quoted title with spaces
3192 return QuoteHtml
($title);
3197 return $str unless $WikiLinks and $str =~ /^$LinkPattern$/;
3198 $str =~ s/([[:lower:]])([[:upper:]])/$1 $2/g;
3203 my ($id, $newText, $preview) = @_;
3205 my $upload = GetParam
('upload', undef);
3206 if (!UserCanEdit
($id, 1)) {
3207 my $rule = UserIsBanned
();
3209 ReportError
(T
('Edit Denied'), '403 FORBIDDEN', undef,
3210 $q->p(T
('Editing not allowed: user, ip, or network is blocked.')),
3211 $q->p(T
('Contact the wiki administrator for more information.')),
3212 $q->p(Ts
('The rule %s matched for you.', $rule) . ' '
3213 . Ts
('See %s for more information.', GetPageLink
($BannedHosts))));
3215 ReportError
(T
('Edit Denied'), '403 FORBIDDEN', undef,
3216 $q->p(Ts
('Editing not allowed: %s is read-only.', NormalToFree
($id))));
3218 } elsif ($upload and not $UploadAllowed and not UserIsAdmin
()) {
3219 ReportError
(T
('Only administrators can upload files.'), '403 FORBIDDEN');
3222 my ($text, $revision) = GetTextRevision
(GetParam
('revision', ''), 1); # maybe revision reset!
3223 my $oldText = $preview ?
$newText : $text;
3224 my $isFile = TextIsFile
($oldText);
3225 $upload = $isFile if not defined $upload;
3226 if ($upload and not $UploadAllowed and not UserIsAdmin
()) {
3227 ReportError
(T
('Only administrators can upload files.'), '403 FORBIDDEN');
3229 if ($upload) { # shortcut lots of code
3232 } elsif ($isFile and not $upload) {
3236 if ($revision and not $upload) {
3237 $header = Ts
('Editing revision %s of', $revision) . ' ' . NormalToFree
($id);
3239 $header = Ts
('Editing %s', NormalToFree
($id));
3241 print GetHeader
('', $header, undef, undef, undef, GetFooterLinks
($id, 'edit')),
3242 $q->start_div({-class=>'content edit'});
3243 if ($preview and not $upload) {
3244 print $q->start_div({-class=>'preview'});
3245 print $q->h2(T
('Preview:'));
3246 PrintWikiToHTML
($oldText); # no caching, current revision, unlocked
3247 print $q->hr(), $q->h2(T
('Preview only, not yet saved')), $q->end_div();
3250 print $q->strong(Ts
('Editing old revision %s.', $revision) . ' '
3251 . T
('Saving this page will replace the latest revision with this text.'))
3253 print GetEditForm
($id, $upload, $oldText, $revision), $q->end_div();
3254 PrintFooter
($id, 'edit');
3258 my ($page_name, $upload, $oldText, $revision) = @_;
3259 my $html = GetFormStart
(undef, undef, $upload ?
'edit upload' : 'edit text') # protected by questionasker
3260 .$q->p(GetHiddenValue
("title", $page_name), ($revision ? GetHiddenValue
('revision', $revision) : ''),
3261 GetHiddenValue
('oldtime', $Page{ts
}), ($upload ? GetUpload
() : GetTextArea
('text', $oldText)));
3262 my $summary = UnquoteHtml
(GetParam
('summary', ''))
3263 || ($Now - $Page{ts
} < ($SummaryHours * 3600) ?
$Page{summary
} : '');
3264 $html .= $q->p(T
('Summary:').$q->br().GetTextArea
('summary', $summary, 2))
3265 .$q->p($q->checkbox(-name
=>'recent_edit', -checked
=>(GetParam
('recent_edit', '') eq 'on'),
3266 -label
=>T
('This change is a minor edit.')));
3267 $html .= T
($EditNote) if $EditNote; # Allow translation
3268 my $username = GetParam
('username', '');
3269 $html .= $q->p($q->label({-for=>'username'}, T
('Username:')).' '
3270 .$q->textfield(-name
=>'username', -id
=>'username', -default=>$username,
3271 -override
=>1, -size
=>20, -maxlength
=>50))
3272 .$q->p($q->submit(-name
=>'Save', -accesskey
=>T
('s'), -value
=>T
('Save')),
3273 ($upload ?
'' : ' ' . $q->submit(-name
=>'Preview', -accesskey
=>T
('p'), -value
=>T
('Preview'))).
3274 ' '.$q->submit(-name
=>'Cancel', -value
=>T
('Cancel')));
3276 $html .= $q->p(ScriptLink
('action=edit;upload=0;id='.UrlEncode
($page_name), T
('Replace this file with text'), 'upload'));
3278 elsif ($UploadAllowed or UserIsAdmin
()) {
3279 $html .= $q->p(ScriptLink
('action=edit;upload=1;id='.UrlEncode
($page_name), T
('Replace this text with a file'), 'upload'));
3281 $html .= $q->endform();
3286 my ($name, $text, $rows) = @_;
3287 return $q->textarea(-id
=>$name, -name
=>$name, -default=>$text, -rows
=>$rows||25, -columns
=>78, -override
=>1);
3291 return T
('File to upload: ') . $q->filefield(-name
=>'file', -size
=>50, -maxlength
=>100);
3296 OpenPage
($id) if ValidIdOrDie
($id);
3297 print $q->header(-status
=>'304 NOT MODIFIED') and return if FileFresh
(); # FileFresh needs an OpenPage!
3298 my ($text, $revision) = GetTextRevision
(GetParam
('revision', '')); # maybe revision reset!
3300 if (my ($type, $encoding) = TextIsFile
($text)) {
3301 my ($data) = $text =~ /^[^\n]*\n(.*)/s;
3302 my %allowed = map {$_ => 1} @UploadTypes;
3303 ReportError
(Ts
('Files of type %s are not allowed.', $type), '415 UNSUPPORTED MEDIA TYPE')
3304 if @UploadTypes and not $allowed{$type};
3305 print GetHttpHeader
($type, $ts, undef, $encoding);
3306 require MIME
::Base64
;
3307 binmode(STDOUT
, ":pop:raw"); # need to pop utf8 for Windows users!?
3308 print MIME
::Base64
::decode
($data);
3310 print GetHttpHeader
('text/plain', $ts);
3316 print GetHeader
('',T
('Password')), $q->start_div({-class=>'content password'});
3317 print $q->p(T
('Your password is saved in a cookie, if you have cookies enabled. Cookies may get lost if you connect from another machine, from another account, or using another software.'));
3318 if (UserIsAdmin
()) {
3319 print $q->p(T
('You are currently an administrator on this site.'));
3320 } elsif (UserIsEditor
()) {
3321 print $q->p(T
('You are currently an editor on this site.'));
3323 print $q->p(T
('You are a normal user on this site.'));
3324 if ($AdminPass or $EditPass) {
3325 print $q->p(T
('Your password does not match any of the administrator or editor passwords.'));
3328 if ($AdminPass or $EditPass) {
3329 print GetFormStart
(undef, undef, 'password'),
3330 $q->p(GetHiddenValue
('action', 'password'), T
('Password:'), ' ',
3331 $q->password_field(-name
=>'pwd', -size
=>20, -maxlength
=>50),
3332 $q->submit(-name
=>'Save', -accesskey
=>T
('s'), -value
=>T
('Save'))), $q->endform;
3334 print $q->p(T
('This site does not use admin or editor passwords.'));
3336 print $q->end_div();
3340 sub UserIsEditorOrError
{
3342 or ReportError
(T
('This operation is restricted to site editors only...'), '403 FORBIDDEN');
3346 sub UserIsAdminOrError
{
3348 or ReportError
(T
('This operation is restricted to administrators only...'), '403 FORBIDDEN');
3353 my ($id, $editing, $comment) = @_;
3354 return 0 if $id eq 'SampleUndefinedPage' or $id eq T
('SampleUndefinedPage')
3355 or $id eq 'Sample_Undefined_Page' or $id eq T
('Sample_Undefined_Page');
3356 return 1 if UserIsAdmin
();
3357 return 0 if $id ne '' and -f GetLockedPageFile
($id);
3358 return 0 if $LockOnCreation{$id} and not -f GetPageFile
($id); # new page
3359 return 1 if UserIsEditor
();
3360 return 0 if !$EditAllowed or -f
$NoEditFile;
3361 return 0 if $editing and UserIsBanned
(); # this call is more expensive
3362 return 0 if $EditAllowed >= 2 and (not $CommentsPrefix or $id !~ /^$CommentsPrefix/o);
3363 return 1 if $EditAllowed >= 3 and ($comment or (GetParam
('aftertext', '') and not GetParam
('text', '')));
3364 return 0 if $EditAllowed >= 3;
3369 return 0 if GetParam
('action', '') eq 'password'; # login is always ok
3371 $ip = $ENV{'REMOTE_ADDR'};
3372 $host = GetRemoteHost
();
3373 foreach (split(/\n/, GetPageContent
($BannedHosts))) {
3374 if (/^\s*([^#]\S+)/) { # all lines except empty lines and comments, trim whitespace
3376 return $regexp if ($ip =~ /$regexp/i);
3377 return $regexp if ($host =~ /$regexp/i);
3384 return 0 if $AdminPass eq '';
3385 my $pwd = GetParam
('pwd', '');
3386 foreach (split(/\s+/, $AdminPass)) {
3387 return 1 if $pwd eq $_;
3393 return 1 if UserIsAdmin
(); # Admin includes editor
3394 return 0 if $EditPass eq '';
3395 my $pwd = GetParam
('pwd', ''); # Used for both passwords
3396 foreach (split(/\s+/, $EditPass)) {
3397 return 1 if $pwd eq $_;
3404 my @urls = $str =~ /$FullUrlPattern/go;
3405 foreach (split(/\n/, GetPageContent
($BannedContent))) {
3406 next unless m/^\s*([^#]+?)\s*(#\s*(\d\d\d\d-\d\d-\d\d\s*)?(.*))?$/;
3407 my ($regexp, $comment, $re) = ($1, $4, undef);
3408 foreach my $url (@urls) {
3409 eval { $re = qr/$regexp/i; };
3410 if (defined($re) && $url =~ $re) {
3411 return Tss
('Rule "%1" matched "%2" on this page.', $regexp, $url) . ' '
3412 . ($comment ? Ts
('Reason: %s.', $comment) : T
('Reason unknown.')) . ' '
3413 . Ts
('See %s for more information.', GetPageLink
($BannedContent));
3421 my $raw = GetParam
('raw', 0);
3422 my $match = GetParam
('match', '');
3424 my @menu = ($q->label({-for=>'indexmatch'}, T
('Filter:')) . ' '
3425 . $q->textfield(-name
=>'match', -id
=>'indexmatch', -size
=>20));
3426 foreach my $data (@IndexOptions) {
3427 my ($option, $text, $default, $sub) = @
$data;
3428 my $value = GetParam
($option, $default); # HTML checkbox warning!
3429 $value = 0 if GetParam
('manual', 0) and $value ne 'on';
3430 push(@pages, &$sub) if $value;
3431 push(@menu, $q->checkbox(-name
=>$option, -checked
=>$value, -label
=>$text));
3433 @pages = grep /$match/i, @pages if $match;
3434 @pages = sort @pages;
3436 print GetHttpHeader
('text/plain'); # and ignore @menu
3438 print GetHeader
('', T
('Index of all pages'));
3439 push(@menu, GetHiddenValue
('manual', 1) . $q->submit(-value
=>T
('Go!')));
3440 push(@menu, $q->b(Ts
('(for %s)', GetParam
('lang', '')))) if GetParam
('lang', '');
3441 print $q->start_div({-class=>'content index'}),
3442 GetFormStart
(undef, 'get', 'index'), GetHiddenValue
('action', 'index'),
3443 $q->p(join($q->br(), @menu)), $q->end_form(),
3444 $q->h2(Ts
('%s pages found.', ($#pages + 1))), $q->start_p();
3449 print $q->end_p(), $q->end_div() unless $raw;
3450 PrintFooter
() unless $raw;
3455 my $lang = GetParam
('lang', 0);
3458 my @languages = split(/,/, $Page{languages
});
3459 next if (@languages and not grep(/$lang/, @languages));
3461 if (GetParam
('raw', 0)) {
3462 if (GetParam
('search', '') and GetParam
('context',1)) {
3463 print "title: $id\n\n"; # for near links without full search
3468 print GetPageOrEditLink
($id, NormalToFree
($id)), $q->br();
3473 my $refresh = GetParam
('refresh', 0);
3474 return @IndexList if @IndexList and not $refresh;
3475 SetParam
('refresh', 0) if $refresh;
3476 if (not $refresh and -f
$IndexFile) {
3477 my ($status, $rawIndex) = ReadFile
($IndexFile); # not fatal
3479 %IndexHash = split(/\s+/, $rawIndex);
3480 @IndexList = sort(keys %IndexHash);
3483 # If open fails just refresh the index
3487 # If file exists and cannot be changed, error!
3488 my $locked = RequestLockDir
('index', undef, undef, -f
$IndexFile);
3489 foreach (bsd_glob
("$PageDir/*/*.pg"), bsd_glob
("$PageDir/*/.*.pg")) {
3490 next unless m
|/.*/(.+)\
.pg
$|;
3493 push(@IndexList, $id);
3494 $IndexHash{$id} = 1;
3496 WriteStringToFile
($IndexFile, join(' ', %IndexHash)) if $locked;
3497 ReleaseLockDir
('index') if $locked;
3503 return DoIndex
() if $string eq '';
3504 eval { qr/$string/ }
3505 or $@
and ReportError
(Ts
('Malformed regular expression in %s', $string),
3507 my $replacement = GetParam
('replace',undef);
3508 my $raw = GetParam
('raw','');
3510 if ($replacement or GetParam
('delete', 0)) {
3511 return unless UserIsAdminOrError
();
3512 print GetHeader
('', Ts
('Replaced: %s', $string . " → " . $replacement)),
3513 $q->start_div({-class=>'content replacement'});
3514 @results = Replace
($string,$replacement);
3515 foreach (@results) {
3516 PrintSearchResult
($_, SearchRegexp
($replacement||$string));
3520 print GetHttpHeader
('text/plain');
3521 print RcTextItem
('title', Ts
('Search for: %s', $string)), RcTextItem
('date', TimeToText
($Now)),
3522 RcTextItem
('link', $q->url(-path_info
=>1, -query
=>1)), "\n" if GetParam
('context', 1);
3524 print GetHeader
('', Ts
('Search for: %s', $string)), $q->start_div({-class=>'content search'});
3525 $ReplaceForm = UserIsAdmin
();
3526 print $q->p({-class=>'links'}, SearchMenu
($string));
3528 @results = SearchTitleAndBody
($string, \
&PrintSearchResult
, SearchRegexp
($string));
3530 print SearchResultCount
($#results + 1), $q->end_div() unless $raw;
3531 PrintFooter
() unless $raw;
3535 return ScriptLink
('action=rc;rcfilteronly=' . UrlEncode
(shift),
3536 T
('View changes for these pages'));
3539 sub SearchResultCount
{ $q->p({-class=>'result'}, Ts
('%s pages found.', (shift))); }
3541 sub PageIsUploadedFile
{
3543 return undef if $OpenPageName eq $id;
3544 if ($IndexHash{$id}) {
3545 my $file = GetPageFile
($id);
3546 utf8
::encode
($file); # filenames are bytes!
3547 open(FILE
, '<:utf8', $file)
3548 or ReportError
(Ts
('Cannot open %s', $file) . ": $!", '500 INTERNAL SERVER ERROR');
3549 while (defined($_ = <FILE
>) and $_ !~ /^text: /) {
3550 } # read lines until we get to the text key
3552 return TextIsFile
(substr($_,6)); # pass "#FILE image/png\n" to the test
3556 sub SearchTitleAndBody
{ # expects search string to be HTML quoted and will unquote it
3557 my ($string, $func, @args) = @_;
3558 $string = UnquoteHtml
($string);
3560 my $lang = GetParam
('lang', '');
3561 foreach my $id (GrepFiltered
($string, AllPagesList
())) {
3562 my $name = NormalToFree
($id);
3563 my ($text) = PageIsUploadedFile
($id); # set to mime-type if this is an uploaded file
3564 if (not $text) { # not uploaded file, therefore allow searching of page body
3565 local ($OpenPageName, %Page); # this is local!
3566 OpenPage
($id); # this opens a page twice if it is not uploaded, but that's ok
3568 my @languages = split(/,/, $Page{languages
});
3569 next if (@languages and not grep(/$lang/, @languages));
3571 $text = $Page{text
};
3573 if (SearchString
($string, $name . "\n" . $text)) { # the real search code
3575 &$func($id, @args) if $func;
3581 sub GrepFiltered
{ # grep is so much faster!!
3582 my ($string, @pages) = @_;
3583 my $regexp = SearchRegexp
($string);
3584 return @pages unless GetParam
('grep', $UseGrep) and $regexp;
3585 my @result = grep(/$regexp/i, @pages);
3586 my %found = map {$_ => 1} @result;
3587 $regexp =~ s/\\n(\)*)$/\$$1/g; # sometimes \n can be replaced with $
3588 $regexp =~ s/([?+{|()])/\\$1/g; # basic regular expressions from man grep
3589 # if we know of any remaining grep incompatibilities we should
3590 # return @pages here!
3591 $regexp = quotemeta($regexp);
3592 open(F
, '-|:encoding(UTF-8)', "grep -rli $regexp '$PageDir' 2>/dev/null");
3594 push(@result, $1) if m/.*\/(.*)\
.pg
/ and not $found{$1};
3597 return sort @result;
3601 my ($string, $data) = @_;
3602 my @strings = grep /./, $string =~ /\"([^\"]+)\"|(\S+)/g; # skip null entries
3603 foreach my $str (@strings) {
3604 return 0 unless ($data =~ /$str/i);
3610 my $regexp = join '|', map { index($_,'|') == -1 ?
$_ : "($_)" }
3611 grep /./, shift =~ /\"([^\"]+)\"|(\S+)/g; # this acts as OR
3612 $regexp =~ s/\\s/[[:space:]]/g;
3616 sub PrintSearchResult
{
3617 my ($name, $regex) = @_;
3618 return PrintPage
($name) if not GetParam
('context',1);
3619 my $raw = GetParam
('raw', 0);
3620 OpenPage
($name); # should be open already, just making sure!
3621 my $text = $Page{text
};
3622 my ($type) = TextIsFile
($text); # MIME type if an uploaded file
3624 # get the page, filter it, remove all tags
3625 $text =~ s/$FS//go; # Remove separators (paranoia)
3626 $text =~ s/[\s]+/ /g; # Shrink whitespace
3627 $text =~ s/([-_=\\*\\.]){10,}/$1$1$1$1$1/g ; # e.g. shrink "----------"
3628 $entry{title
} = $name;
3629 $entry{description
} = $type || SearchExtract
(QuoteHtml
($text), $regex);
3630 $entry{size
} = int((length($text)/1024)+1) . 'K';
3631 $entry{'last-modified'} = TimeToText
($Page{ts
});
3632 $entry{username
} = $Page{username
};
3633 $entry{host
} = $Page{host
};
3634 PrintSearchResultEntry
(\
%entry, $regex);
3637 sub PrintSearchResultEntry
{
3638 my %entry = %{(shift)}; # get value from reference
3640 if (GetParam
('raw', 0)) {
3641 $entry{generator
} = $entry{username
} . ' ' if $entry{username
};
3642 $entry{generator
} .= Ts
('from %s', $entry{host
}) if $entry{host
};
3643 foreach my $key (qw(title description size last-modified generator username host)) {
3644 print RcTextItem
($key, $entry{$key});
3646 print RcTextItem
('link', "$ScriptName?$entry{title}"), "\n";
3648 my $author = GetAuthorLink
($entry{host
}, $entry{username
});
3649 $author = $entry{generator
} unless $author;
3650 my $id = $entry{title
};
3651 my ($class, $resolved, $title, $exists) = ResolveId
($id);
3652 my $text = NormalToFree
($id);
3653 my $result = $q->span({-class=>'result'}, ScriptLink
(UrlEncode
($resolved), $text, $class, undef, $title));
3654 my $description = $entry{description
};
3655 $description = $q->br() . SearchHighlight
($description, $regex) if $description;
3656 my $info = $entry{size
};
3657 $info .= ' - ' if $info;
3658 $info .= T
('last updated') . ' ' . $entry{'last-modified'} if $entry{'last-modified'};
3659 $info .= ' ' . T
('by') . ' ' . $author if $author;
3660 $info = $q->br() . $q->span({-class=>'info'}, $info) if $info;
3661 print $q->p($result, $description, $info);
3665 sub SearchHighlight
{
3666 my ($data, $regex) = @_;
3667 $data =~ s/($regex)/<strong>$1<\/strong
>/gi
;
3672 my ($data, $string) = @_;
3673 my ($snippetlen, $maxsnippets) = (100, 4) ; # these seem nice.
3674 # show a snippet from the beginning of the document
3675 my $j = index($data, ' ', $snippetlen); # end on word boundary
3676 my $t = substr($data, 0, $j);
3677 my $result = $t . ' . . .';
3678 $data = substr($data, $j); # to avoid rematching
3680 while ($jsnippet < $maxsnippets && $data =~ m/($string)/i) {
3682 if (($j = index($data, $1)) > -1 ) {
3683 # get substr containing (start of) match, ending on word boundaries
3684 my $start = index($data, ' ', $j-($snippetlen/2));
3685 $start = 0 if ($start == -1);
3686 my $end = index($data, ' ', $j+($snippetlen/2));
3687 $end = length($data ) if ($end == -1);
3688 $t = substr($data, $start, $end-$start);
3689 $result .= $t . ' . . .';
3690 # truncate text to avoid rematching the same string.
3691 $data = substr($data, $end);
3698 my ($from, $to) = @_;
3699 my $lang = GetParam
('lang', '');
3701 RequestLockOrError
(); # fatal
3702 foreach my $id (AllPagesList
()) {
3705 my @languages = split(/,/, $Page{languages
});
3706 next if (@languages and not grep(/$lang/, @languages));
3709 if (eval "s{$from}{$to}gi") { # allows use of backreferences
3710 push (@result, $id);
3711 Save
($id, $_, $from . ' -> ' . $to, 1,
3712 ($Page{ip
} ne $ENV{REMOTE_ADDR
}));
3720 my $id = FreeToNormal
(shift);
3722 ReportError
(Ts
('Editing not allowed for %s.', $id), '403 FORBIDDEN') unless UserCanEdit
($id, 1);
3723 # Lock before getting old page to prevent races
3724 RequestLockOrError
(); # fatal
3726 my $old = $Page{text
};
3727 my $string = UnquoteHtml
(GetParam
('text', undef));
3728 $string =~ s/(\r|$FS)//go;
3729 my ($type) = TextIsFile
($string); # MIME type if an uploaded file
3730 my $filename = GetParam
('file', undef);
3731 if (($filename or $type) and not $UploadAllowed and not UserIsAdmin
()) {
3732 ReportError
(T
('Only administrators can upload files.'), '403 FORBIDDEN');
3734 my $comment = UnquoteHtml
(GetParam
('aftertext', undef));
3735 $comment =~ s/(\r|$FS)//go;
3736 if (defined($comment) and (not $comment or $comment eq $NewComment)) {
3740 if ($filename) { # upload file
3741 my $file = $q->upload('file');
3742 if (not $file and $q->cgi_error) {
3743 ReportError
(Ts
('Transfer Error: %s', $q->cgi_error), '500 INTERNAL SERVER ERROR');
3745 ReportError
(T
('Browser reports no file info.'), '500 INTERNAL SERVER ERROR')
3746 unless $q->uploadInfo($filename);
3747 $type = $q->uploadInfo($filename)->{'Content-Type'};
3748 ReportError
(T
('Browser reports no file type.'), '415 UNSUPPORTED MEDIA TYPE') unless $type;
3749 local $/ = undef; # Read complete files
3750 my $content = <$file>; # Apparently we cannot count on <$file> to always work within the eval!?
3751 my $encoding = 'gzip' if substr($content,0,2) eq "\x1f\x8b";
3752 eval { require MIME
::Base64
; $_ = MIME
::Base64
::encode
($content) };
3753 $string = "#FILE $type $encoding\n" . $_;
3754 } else { # ordinary text edit
3755 $string = AddComment
($old, $comment) if $comment;
3756 $string = substr($string, length($DeletedPage)) # undelete pages when adding a comment
3757 if $comment and substr($string, 0, length($DeletedPage)) eq $DeletedPage; # no regexp!
3758 $string .= "\n" if ($string !~ /\n$/); # add trailing newline
3759 $string = RunMyMacros
($string); # run macros on text pages only
3761 my %allowed = map {$_ => 1} @UploadTypes;
3762 ReportError
(Ts
('Files of type %s are not allowed.', $type), '415 UNSUPPORTED MEDIA TYPE')
3763 if @UploadTypes and $type and not $allowed{$type};
3765 my $summary = GetSummary
();
3766 if (not UserIsEditor
()) {
3767 my $rule = BannedContent
($string) || BannedContent
($summary);
3768 ReportError
(T
('Edit Denied'), '403 FORBIDDEN', undef, $q->p(T
('The page contains banned text.')),
3769 $q->p(T
('Contact the wiki administrator for more information.')), $q->p($rule)) if $rule;
3771 # rebrowse if no changes
3772 my $oldrev = $Page{revision
};
3773 if (GetParam
('Preview', '')) { # Preview button was used
3776 BrowsePage
($id, 0, RunMyMacros
($comment)); # show macros in preview
3778 DoEdit
($id, $string, 1);
3781 } elsif ($old eq $string) {
3782 ReleaseLock
(); # No changes -- just show the same page again
3783 return ReBrowsePage
($id);
3784 } elsif ($oldrev == 0 and ($string eq $NewText or $string eq "\n")) {
3785 ReportError
(T
('No changes to be saved.'), '400 BAD REQUEST'); # don't fake page creation because of webdav
3788 if ($oldrev) { # the first author (no old revision) is not considered to be "new"
3789 # prefer usernames for potential new author detection
3790 $newAuthor = 1 if not $Page{username
} or $Page{username
} ne GetParam
('username', '');
3791 $newAuthor = 1 if not $ENV{REMOTE_ADDR
} or not $Page{ip
} or $ENV{REMOTE_ADDR
} ne $Page{ip
};
3793 my $oldtime = $Page{ts
};
3794 my $myoldtime = GetParam
('oldtime', ''); # maybe empty!
3795 # Handle raw edits with the meta info on the first line
3796 if (GetParam
('raw', 0) == 2 and $string =~ /^([0-9]+).*\n((.*\n)*.*)/) {
3800 my $generalwarning = 0;
3801 if ($newAuthor and $oldtime ne $myoldtime and not $comment) {
3803 my ($ancestor) = GetTextAtTime
($myoldtime);
3804 if ($ancestor and $old ne $ancestor) {
3805 my $new = MergeRevisions
($string, $ancestor, $old);
3808 if ($new =~ /^<<<<<<</m and $new =~ /^>>>>>>>/m) {
3809 SetParam
('msg', Ts
('This page was changed by somebody else %s.',
3810 CalcTimeSince
($Now - $Page{ts
}))
3811 . ' ' . T
('The changes conflict. Please check the page again.'));
3812 } # else no conflict
3814 $generalwarning = 1;
3815 } # else merge revision didn't work
3816 } # else nobody changed the page in the mean time (same text)
3818 $generalwarning = 1;
3819 } # no way to be sure since myoldtime is missing
3820 } # same author or nobody changed the page in the mean time (same timestamp)
3821 if ($generalwarning and ($Now - $Page{ts
}) < 600) {
3822 SetParam
('msg', Ts
('This page was changed by somebody else %s.',
3823 CalcTimeSince
($Now - $Page{ts
}))
3824 . ' ' . T
('Please check whether you overwrote those changes.'));
3826 Save
($id, $string, $summary, (GetParam
('recent_edit', '') eq 'on'), $filename);
3832 my $text = GetParam
('aftertext', '') || ($Page{revision
} > 0 ?
'' : GetParam
('text', ''));
3833 if ($SummaryDefaultLength and length($text) > $SummaryDefaultLength) {
3834 $text = substr($text, 0, $SummaryDefaultLength);
3835 $text =~ s/\s*\S*$/ . . ./;
3837 my $summary = GetParam
('summary', '') || $text; # not GetParam('summary', $text) work because '' is defined
3838 $summary =~ s/$FS|[\r\n]+/ /go; # remove linebreaks and separator characters
3839 $summary =~ s/\[$FullUrlPattern\s+(.*?)\]/$2/go; # fix common annoyance when copying text to summary
3840 $summary =~ s/\[$FullUrlPattern\]//go;
3841 $summary =~ s/\[\[$FreeLinkPattern\]\]/$1/go;
3842 return UnquoteHtml
($summary);
3846 my ($string, $comment) = @_;
3847 $comment =~ s/\r//g; # Remove "\r"-s (0x0d) from the string
3848 $comment =~ s/\s+$//g; # Remove whitespace at the end
3849 return $string unless $comment;
3850 my $isCommentPage = $CommentsPrefix and (GetParam
('title') =~ /^$CommentsPrefix/o);
3851 return $string . $comment
3852 unless $isCommentPage; # Regular page.
3853 if ($comment ne $NewComment) {
3854 # Page is a comments page. Store username, homepage, timestamp
3855 # along with the comment.
3856 my $author = GetParam
('username', T
('Anonymous'));
3857 my $homepage = GetParam
('homepage', '');
3858 $homepage = 'http://' . $homepage
3859 if $homepage and not substr($homepage,0,7) eq 'http://';
3860 $author = "[$homepage $author]" if $homepage;
3861 $string .= "\n----\n\n" if $string and $string ne "\n";
3862 $string .= $comment . "\n\n"
3863 . '-- ' . $author . ' ' . TimeToText
($Now) . "\n\n";
3868 sub Save
{ # call within lock, with opened page
3869 my ($id, $new, $summary, $minor, $upload) = @_;
3870 my $user = GetParam
('username', '');
3871 my $host = GetRemoteHost
();
3872 my $revision = $Page{revision
} + 1;
3873 my $old = $Page{text
};
3874 my $olddiff = $Page{'diff-major'} == '1' ?
$Page{'diff-minor'} : $Page{'diff-major'};
3875 if ($revision == 1 and -e
$IndexFile and not unlink($IndexFile)) { # regenerate index on next request
3876 SetParam
('msg', Ts
('Cannot delete the index file %s.', $IndexFile)
3877 . ' ' . T
('Please check the directory permissions.')
3878 . ' ' . T
('Your changes were not saved.'));
3883 SaveKeepFile
(); # deletes blocks, flags, diff-major, and diff-minor, and sets keep-ts
3886 $Page{lastmajor
} = $revision unless $minor;
3887 $Page{revision
} = $revision;
3888 $Page{summary
} = $summary;
3889 $Page{username
} = $user;
3890 $Page{ip
} = $ENV{REMOTE_ADDR
};
3891 $Page{host
} = $host;
3892 $Page{minor
} = $minor;
3894 if ($UseDiff and $UseCache > 1 and $revision > 1 and not $upload and not TextIsFile
($old)) {
3895 UpdateDiffs
($old, $new, $olddiff); # sets diff-major and diff-minor
3898 $languages = GetLanguages
($new) unless $upload;
3899 $Page{languages
} = $languages;
3901 if ($revision == 1 and $LockOnCreation{$id}) {
3902 WriteStringToFile
(GetLockedPageFile
($id), 'LockOnCreation');
3904 WriteRcLog
($id, $summary, $minor, $revision, $user, $host, $languages, GetCluster
($new));
3905 if ($revision == 1) {
3906 $IndexHash{$id} = 1;
3907 @IndexList = sort(keys %IndexHash);
3908 WriteStringToFile
($IndexFile, join(' ', %IndexHash));
3912 sub TouchIndexFile
{
3914 utime $ts, $ts, $IndexFile;
3915 $LastUpdate = $Now = $ts;
3921 for my $lang (sort keys %Languages) {
3922 my @matches = $text =~ /$Languages{$lang}/ig;
3923 push(@result, $lang) if $#matches >= $LanguageLimit;
3925 return join(',', @result);
3930 return '' unless $PageCluster;
3931 return $1 if ($WikiLinks && /^$LinkPattern\n/o)
3932 or ($FreeLinks && /^\[\[$FreeLinkPattern\]\]\n/o);
3935 sub MergeRevisions
{ # merge change from file2 to file3 into file1
3936 my ($file1, $file2, $file3) = @_;
3937 my ($name1, $name2, $name3) = ("$TempDir/file1", "$TempDir/file2", "$TempDir/file3");
3938 CreateDir
($TempDir);
3939 RequestLockDir
('merge') or return T
('Could not get a lock to merge!');
3940 WriteStringToFile
($name1, $file1);
3941 WriteStringToFile
($name2, $file2);
3942 WriteStringToFile
($name3, $file3);
3943 my ($you,$ancestor,$other) = (T
('you'), T
('ancestor'), T
('other'));
3944 my $output = `diff3 -m -L "$you" -L "$ancestor" -L "$other" $name1 $name2 $name3`;
3945 ReleaseLockDir
('merge'); # don't unlink temp files--next merge will just overwrite.
3949 # Note: all diff and recent-list operations should be done within locks.
3951 my ($id, $summary, $minor, $revision, $username, $host, $languages, $cluster) = @_;
3952 my $rc_line = join($FS, $Now, $id, $minor, $summary, $host,
3953 $username, $revision, $languages, $cluster);
3954 AppendStringToFile
($RcFile, $rc_line . "\n");
3957 sub UpdateDiffs
{ # this could be optimized, but isn't frequent enough
3958 my ($old, $new, $olddiff) = @_;
3959 $Page{'diff-minor'} = GetDiff
($old, $new); # create new diff-minor
3960 # 1 is a special value for GetCacheDiff telling it to use diff-minor
3961 $Page{'diff-major'} = $Page{lastmajor
} == $Page{revision
} ?
1 : $olddiff;
3965 print GetHeader
('', T
('Run Maintenance')), $q->start_div({-class=>'content maintain'});
3966 my $fname = "$DataDir/maintain";
3967 if (!UserIsAdmin
()) {
3968 if ((-f
$fname) && ((-M
$fname) < 0.5)) {
3969 print $q->p(T
('Maintenance not done.') . ' ' . T
('(Maintenance can only be done once every 12 hours.)')
3970 . ' ', T
('Remove the "maintain" file or wait.')), $q->end_div();
3975 print '<p>', T
('Expiring keep files and deleting pages marked for deletion');
3976 # Expire all keep files
3977 foreach my $name (AllPagesList
()) {
3978 print $q->br(), GetPageLink
($name);
3980 my $delete = PageDeletable
();
3982 my $status = DeletePage
($OpenPageName);
3983 print ' ' . ($status ? T
('not deleted: ') . $status : T
('deleted'));
3989 RequestLockOrError
();
3990 print $q->p(T
('Main lock obtained.'));
3991 print $q->p(Ts
('Moving part of the %s log file.', $RCName));
3992 # Determine the number of days to go back
3995 $days = $_ if $_ > $days;
3997 my $starttime = $Now - $days * 86400; # 24*60*60
3998 # Read the current file
3999 my ($status, $data) = ReadFile
($RcFile);
4001 print $q->p($q->strong(Ts
('Could not open %s log file', $RCName) . ':') . ' '. $RcFile),
4002 $q->p(T
('Error was') . ':'), $q->pre($!), $q->p(T
('Note: This error is normal if no changes have been made.'));
4004 # Move the old stuff from rc to temp
4005 my @rc = split(/\n/, $data);
4007 for ($i = 0; $i < @rc ; $i++) {
4008 my ($ts) = split(/$FS/o, $rc[$i]);
4009 last if ($ts >= $starttime);
4011 print $q->p(Ts
('Moving %s log entries.', $i));
4013 my @temp = splice(@rc, 0, $i);
4014 # Write new files, and backups
4015 AppendStringToFile
($RcOldFile, join("\n",@temp) . "\n");
4016 WriteStringToFile
($RcFile . '.old', $data);
4017 WriteStringToFile
($RcFile, @rc ?
join("\n",@rc) . "\n" : '');
4019 if (opendir(DIR
, $RssDir)) { # cleanup if they should expire anyway
4020 foreach (readdir(DIR
)) {
4021 unlink "$RssDir/$_" if $Now - (stat($_))[9] > $RssCacheHours * 3600;
4025 foreach my $sub (@MyMaintenance) {
4028 WriteStringToFile
($fname, 'Maintenance done at ' . TimeToText
($Now));
4030 print $q->p(T
('Main lock released.')), $q->end_div();
4035 return unless $KeepDays;
4036 my $expirets = $Now - ($KeepDays * 86400); # 24*60*60
4037 return 0 unless $Page{ts
} < $expirets;
4038 return PageMarkedForDeletion
();
4041 sub PageMarkedForDeletion
{
4042 return 1 if $Page{text
} =~ /^\s*$/; # only whitespace is also to be deleted
4043 return $DeletedPage && substr($Page{text
}, 0, length($DeletedPage)) eq $DeletedPage; # no regexp!
4046 sub DeletePage
{ # Delete must be done inside locks.
4049 foreach my $name (GetPageFile
($id), GetKeepFiles
($id), GetKeepDir
($id), GetLockedPageFile
($id), $IndexFile) {
4050 unlink $name if -f
$name;
4051 rmdir $name if -d
$name;
4054 delete $IndexHash{$id};
4055 @IndexList = sort(keys %IndexHash);
4056 return ''; # no error
4060 return unless UserIsAdminOrError
();
4061 print GetHeader
('', T
('Set or Remove global edit lock'));
4062 my $fname = "$NoEditFile";
4063 if (GetParam
("set", 1)) {
4064 WriteStringToFile
($fname, 'editing locked.');
4068 utime time, time, $IndexFile; # touch index file
4069 print $q->p(-f
$fname ? T
('Edit lock created.') : T
('Edit lock removed.'));
4074 return unless UserIsAdminOrError
();
4075 print GetHeader
('', T
('Set or Remove page edit lock'));
4076 my $id = GetParam
('id', '');
4077 my $fname = GetLockedPageFile
($id) if ValidIdOrDie
($id);
4078 if (GetParam
('set', 1)) {
4079 WriteStringToFile
($fname, 'editing locked.');
4083 utime time, time, $IndexFile; # touch index file
4084 print $q->p(-f
$fname ? Ts
('Lock for %s created.', GetPageLink
($id))
4085 : Ts
('Lock for %s removed.', GetPageLink
($id)));
4090 print GetHeader
('', T
('Displaying Wiki Version')), $q->start_div({-class=>'content version'});
4091 print $WikiDescription, $q->p($q->server_software()),
4092 $q->p(sprintf('Perl v%vd', $^V
)),
4093 $q->p($ENV{MOD_PERL
} ?
$ENV{MOD_PERL
} : "no mod_perl"), $q->p('CGI: ', $CGI::VERSION
),
4094 $q->p('LWP::UserAgent ', eval { local $SIG{__DIE__
}; require LWP
::UserAgent
; $LWP::UserAgent
::VERSION
; }),
4095 $q->p('XML::RSS: ', eval { local $SIG{__DIE__
}; require XML
::RSS
; $XML::RSS
::VERSION
; }),
4096 $q->p('XML::Parser: ', eval { local $SIG{__DIE__
}; $XML::Parser
::VERSION
; });
4097 print $q->p('diff: ' . (`diff --version` || $!)), $q->p('diff3: ' . (`diff3 --version` || $!)) if $UseDiff;
4098 print $q->p('grep: ' . (`grep --version` || $!)) if $UseGrep;
4099 print $q->end_div();
4104 print GetHeader
('', T
('Debugging Information')),
4105 $q->start_div({-class=>'content debug'});
4106 foreach my $sub (@Debugging) { &$sub }
4107 print $q->end_div();
4111 sub DoSurgeProtection
{
4112 return unless $SurgeProtection;
4113 my $name = GetParam
('username','');
4114 $name = $ENV{'REMOTE_ADDR'} if not $name and $SurgeProtection;
4115 return unless $name;
4116 ReadRecentVisitors
();
4117 AddRecentVisitor
($name);
4118 if (RequestLockDir
('visitors')) { # not fatal
4119 WriteRecentVisitors
();
4120 ReleaseLockDir
('visitors');
4121 if (DelayRequired
($name)) {
4122 ReportError
(Ts
('Too many connections by %s',$name)
4123 . ': ' . Tss
('Please do not fetch more than %1 pages in %2 seconds.',
4124 $SurgeProtectionViews, $SurgeProtectionTime),
4125 '503 SERVICE UNAVAILABLE');
4127 } elsif (GetParam
('action', '') ne 'unlock') {
4128 ReportError
(Ts
('Could not get %s lock', 'visitors') . ': ' . Ts
('Check whether the web server can create the directory %s and whether it can create files in it.', $TempDir), '503 SERVICE UNAVAILABLE');
4134 my @entries = @
{$RecentVisitors{$name}};
4135 my $ts = $entries[$SurgeProtectionViews];
4136 return ($Now - $ts) < $SurgeProtectionTime;
4139 sub AddRecentVisitor
{
4141 my $value = $RecentVisitors{$name};
4142 my @entries = ($Now);
4143 push(@entries, @
{$value}) if $value;
4144 $RecentVisitors{$name} = \
@entries;
4147 sub ReadRecentVisitors
{
4148 my ($status, $data) = ReadFile
($VisitorFile);
4149 %RecentVisitors = ();
4150 return unless $status;
4151 foreach (split(/\n/,$data)) {
4152 my @entries = split /$FS/o;
4153 my $name = shift(@entries);
4154 $RecentVisitors{$name} = \
@entries if $name;
4158 sub WriteRecentVisitors
{
4160 my $limit = $Now - $SurgeProtectionTime;
4161 foreach my $name (keys %RecentVisitors) {
4162 my @entries = @
{$RecentVisitors{$name}};
4163 if ($entries[0] >= $limit) { # if the most recent one is too old, do not keep
4164 $data .= join($FS, $name, @entries[0 .. $SurgeProtectionViews - 1]) . "\n";
4167 WriteStringToFile
($VisitorFile, $data);
4170 sub TextIsFile
{ $_[0] =~ /^#FILE (\S+) ?(\S+)?\n/ }
4172 DoWikiRequest
() if $RunCGI and not exists $ENV{MOD_PERL
}; # Do everything.
4173 1; # In case we are loaded from elsewhere