oddmuse-2.2.6.pl 153 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043
  1. #! /usr/bin/perl
  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)
  17. # This program is free software: you can redistribute it and/or modify it under
  18. # the terms of the GNU General Public License as published by the Free Software
  19. # Foundation, either version 3 of the License, or (at your option) any later
  20. # version.
  21. #
  22. # This program is distributed in the hope that it will be useful, but WITHOUT
  23. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  24. # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  25. #
  26. # You should have received a copy of the GNU General Public License along with
  27. # this program. If not, see <http://www.gnu.org/licenses/>.
  28. package OddMuse;
  29. use strict;
  30. use CGI;
  31. use CGI::Carp qw(fatalsToBrowser);
  32. use File::Glob ':glob';
  33. local $| = 1; # Do not buffer output (localized for mod_perl)
  34. # Options:
  35. use vars qw($RssLicense $RssCacheHours @RcDays $TempDir $LockDir $DataDir
  36. $KeepDir $PageDir $RcOldFile $IndexFile $BannedContent $NoEditFile $BannedHosts
  37. $ConfigFile $FullUrl $SiteName $HomePage $LogoUrl $RcDefault $RssDir
  38. $IndentLimit $RecentTop $RecentLink $EditAllowed $UseDiff $KeepDays $KeepMajor
  39. $EmbedWiki $BracketText $UseConfig $UseLookup $AdminPass $EditPass
  40. $PassHashFunction $PassSalt $NetworkFile
  41. $BracketWiki $FreeLinks $WikiLinks $SummaryHours $FreeLinkPattern $RCName
  42. $RunCGI $ShowEdits $LinkPattern $RssExclude $InterLinkPattern $MaxPost $UseGrep
  43. $UrlPattern $UrlProtocols $ImageExtensions $InterSitePattern $FS $CookieName
  44. $SiteBase $StyleSheet $NotFoundPg $FooterNote $NewText $EditNote $UserGotoBar
  45. $VisitorFile $RcFile %Smilies %SpecialDays $InterWikiMoniker $SiteDescription
  46. $RssImageUrl $ReadMe $RssRights $BannedCanRead $SurgeProtection $TopLinkBar
  47. $LanguageLimit $SurgeProtectionTime $SurgeProtectionViews $DeletedPage
  48. %Languages $InterMap $ValidatorLink %LockOnCreation $RssStyleSheet
  49. %CookieParameters @UserGotoBarPages $NewComment $HtmlHeaders $StyleSheetPage
  50. $ConfigPage $ScriptName $CommentsPrefix $CommentsPattern @UploadTypes $AllNetworkFiles
  51. $UsePathInfo $UploadAllowed $LastUpdate $PageCluster %PlainTextPages
  52. $RssInterwikiTranslate $UseCache $Counter $ModuleDir $FullUrlPattern
  53. $SummaryDefaultLength $FreeInterLinkPattern %InvisibleCookieParameters
  54. %AdminPages $UseQuestionmark $JournalLimit $LockExpiration $RssStrip
  55. %LockExpires @IndexOptions @Debugging $DocumentHeader %HtmlEnvironmentContainers
  56. @MyAdminCode @MyFooters @MyInitVariables @MyMacros @MyMaintenance @MyRules);
  57. # Internal variables:
  58. use vars qw(%Page %InterSite %IndexHash %Translate %OldCookie $FootnoteNumber
  59. $OpenPageName @IndexList $Message $q $Now %RecentVisitors @HtmlStack
  60. @HtmlAttrStack $ReplaceForm %MyInc $CollectingJournal $bol $WikiDescription
  61. $PrintedHeader %Locks $Fragment @Blocks @Flags $Today @KnownLocks
  62. $ModulesDescription %Action %RuleOrder %Includes %RssInterwikiTranslate);
  63. # Can be set outside the script: $DataDir, $UseConfig, $ConfigFile, $ModuleDir,
  64. # $ConfigPage, $AdminPass, $EditPass, $ScriptName, $FullUrl, $RunCGI.
  65. # 1 = load config file in the data directory
  66. $UseConfig = 1 unless defined $UseConfig;
  67. # Main wiki directory
  68. $DataDir = $ENV{WikiDataDir} if $UseConfig and not $DataDir;
  69. $DataDir = '/tmp/oddmuse' unless $DataDir; # FIXME: /var/opt/oddmuse/wiki ?
  70. $ConfigPage = '' unless $ConfigPage; # config page
  71. # 1 = Run script as CGI instead of loading as module
  72. $RunCGI = 1 unless defined $RunCGI;
  73. # 1 = allow page views using wiki.pl/PageName
  74. $UsePathInfo = 1;
  75. # -1 = disabled, 0 = 10s; 1 = partial HTML cache; 2 = HTTP/1.1 caching
  76. $UseCache = 2;
  77. $SiteName = 'Wiki'; # Name of site (used for titles)
  78. $HomePage = 'HomePage'; # Home page
  79. $CookieName = 'Wiki'; # Name for this wiki (for multi-wiki sites)
  80. $SiteBase = ''; # Full URL for <BASE> header
  81. $MaxPost = 1024 * 210; # Maximum 210K posts (about 200K for pages)
  82. $StyleSheet = ''; # URL for CSS stylesheet (like '/wiki.css')
  83. $StyleSheetPage = 'css'; # Page for CSS sheet
  84. $LogoUrl = ''; # URL for site logo ('' for no logo)
  85. $NotFoundPg = ''; # Page for not-found links ('' for blank pg)
  86. $NewText = T('This page is empty.') . "\n"; # New page text
  87. $NewComment = T('Add your comment here.') . "\n"; # New comment text
  88. $EditAllowed = 1; # 0 = no, 1 = yes, 2 = comments pages only, 3 = comments only
  89. $AdminPass = '' unless defined $AdminPass; # Whitespace separated passwords.
  90. $EditPass = '' unless defined $EditPass; # Whitespace separated passwords.
  91. $PassHashFunction = '' unless defined $PassHashFunction; # Name of the function to create hashes
  92. $PassSalt = '' unless defined $PassSalt; # Salt will be added to any password before hashing
  93. $BannedHosts = 'BannedHosts'; # Page for banned hosts
  94. $BannedCanRead = 1; # 1 = banned cannot edit, 0 = banned cannot read
  95. $BannedContent = 'BannedContent'; # Page for banned content (usually for link-ban)
  96. $WikiLinks = 1; # 1 = LinkPattern is a link
  97. $FreeLinks = 1; # 1 = [[some text]] is a link
  98. $UseQuestionmark = 1; # 1 = append questionmark to links to nonexisting pages
  99. $BracketText = 1; # 1 = [URL desc] uses a description for the URL
  100. $BracketWiki = 1; # 1 = [WikiLink desc] uses a desc for the local link
  101. $NetworkFile = 1; # 1 = file: is a valid protocol for URLs
  102. $AllNetworkFiles = 0; # 1 = file:///foo is allowed -- the default allows only file://foo
  103. $InterMap = 'InterMap'; # name of the intermap page, '' = disable
  104. $RssInterwikiTranslate = 'RssInterwikiTranslate'; # name of RSS interwiki translation page, '' = disable
  105. $ENV{PATH} = '/bin:/usr/bin'; # Path used to find 'diff' and 'grep'
  106. $UseDiff = 1; # 1 = use diff
  107. $UseGrep = 1; # 1 = use grep to speed up searches
  108. $SurgeProtection = 1; # 1 = protect against leeches
  109. $SurgeProtectionTime = 20; # Size of the protected window in seconds
  110. $SurgeProtectionViews = 10; # How many page views to allow in this window
  111. $DeletedPage = 'DeletedPage'; # Pages starting with this can be deleted
  112. $RCName = 'RecentChanges'; # Name of changes page
  113. @RcDays = qw(1 3 7 30 90); # Days for links on RecentChanges
  114. $RcDefault = 30; # Default number of RecentChanges days
  115. $KeepDays = 14; # Days to keep old revisions
  116. $KeepMajor = 1; # 1 = keep at least one major rev when expiring pages
  117. $SummaryHours = 4; # Hours to offer the old subject when editing a page
  118. $SummaryDefaultLength = 150; # Length of default text for summary (0 to disable)
  119. $ShowEdits = 0; # 1 = major and show minor edits in recent changes
  120. $UseLookup = 1; # 1 = lookup host names instead of using only IP numbers
  121. $RecentTop = 1; # 1 = most recent entries at the top of the list
  122. $RecentLink = 1; # 1 = link to usernames
  123. $PageCluster = ''; # name of cluster page, eg. 'Cluster' to enable
  124. $InterWikiMoniker = ''; # InterWiki prefix for this wiki for RSS
  125. $SiteDescription = ''; # RSS Description of this wiki
  126. $RssStrip = '^\d\d\d\d-\d\d-\d\d_'; # Regexp to strip from feed item titles
  127. $RssImageUrl = $LogoUrl; # URL to image to associate with your RSS feed
  128. $RssRights = ''; # Copyright notice for RSS, usually an URL to the appropriate text
  129. $RssExclude = 'RssExclude'; # name of the page that lists pages to be excluded from the feed
  130. $RssCacheHours = 1; # How many hours to cache remote RSS files
  131. $RssStyleSheet = ''; # External style sheet for RSS files
  132. $UploadAllowed = 0; # 1 = yes, 0 = administrators only
  133. @UploadTypes = ('image/jpeg', 'image/png'); # MIME types allowed, all allowed if empty list
  134. $EmbedWiki = 0; # 1 = no headers/footers
  135. $FooterNote = ''; # HTML for bottom of every page
  136. $EditNote = ''; # HTML notice above buttons on edit page
  137. $TopLinkBar = 1; # 1 = add a goto bar at the top of the page
  138. @UserGotoBarPages = (); # List of pagenames
  139. $UserGotoBar = ''; # HTML added to end of goto bar
  140. $ValidatorLink = 0; # 1 = Link to the W3C HTML validator service
  141. $CommentsPrefix = ''; # prefix for comment pages, eg. 'Comments_on_' to enable
  142. $CommentsPattern = undef; # regex used to match comment pages
  143. $HtmlHeaders = ''; # Additional stuff to put in the HTML <head> section
  144. $IndentLimit = 20; # Maximum depth of nested lists
  145. $LanguageLimit = 3; # Number of matches req. for each language
  146. $JournalLimit = 200; # how many pages can be collected in one go?
  147. $DocumentHeader = qq(<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN")
  148. . qq( "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n)
  149. . qq(<html xmlns="http://www.w3.org/1999/xhtml">);
  150. # Checkboxes at the end of the index.
  151. @IndexOptions = ();
  152. # Display short comments below the GotoBar for special days
  153. # Example: %SpecialDays = ('1-1' => 'New Year', '1-2' => 'Next Day');
  154. %SpecialDays = ();
  155. # Replace regular expressions with inlined images
  156. # Example: %Smilies = (":-?D(?=\\W)" => '/pics/grin.png');
  157. %Smilies = ();
  158. # Detect page languages when saving edits
  159. # Example: %Languages = ('de' => '\b(der|die|das|und|oder)\b');
  160. %Languages = ();
  161. @KnownLocks = qw(main diff index merge visitors); # locks to remove
  162. $LockExpiration = 60; # How long before expirable locks are expired
  163. %LockExpires = (diff=>1, index=>1, merge=>1, visitors=>1); # locks to expire after some time
  164. %CookieParameters = (username=>'', pwd=>'', homepage=>'', theme=>'', css=>'', msg=>'',
  165. lang=>'', toplinkbar=>$TopLinkBar, embed=>$EmbedWiki, );
  166. %InvisibleCookieParameters = (msg=>1, pwd=>1,);
  167. %Action = (rc => \&BrowseRc, rollback => \&DoRollback,
  168. browse => \&BrowseResolvedPage, maintain => \&DoMaintain,
  169. random => \&DoRandom, pagelock => \&DoPageLock,
  170. history => \&DoHistory, editlock => \&DoEditLock,
  171. edit => \&DoEdit, version => \&DoShowVersion,
  172. download => \&DoDownload, rss => \&DoRss,
  173. unlock => \&DoUnlock, password => \&DoPassword,
  174. index => \&DoIndex, admin => \&DoAdminPage,
  175. clear => \&DoClearCache, debug => \&DoDebug,
  176. contrib => \&DoContributors, more => \&DoJournal);
  177. @MyRules = (\&LinkRules, \&ListRule); # don't set this variable, add to it!
  178. %RuleOrder = (\&LinkRules => 0, \&ListRule => 0);
  179. # The 'main' program, called at the end of this script file (aka. as handler)
  180. sub DoWikiRequest {
  181. Init();
  182. DoSurgeProtection();
  183. if (not $BannedCanRead and UserIsBanned() and not UserIsEditor()) {
  184. ReportError(T('Reading not allowed: user, ip, or network is blocked.'), '403 FORBIDDEN',
  185. 0, $q->p(ScriptLink('action=password', T('Login'), 'password')));
  186. }
  187. DoBrowseRequest();
  188. }
  189. sub ReportError { # fatal!
  190. my ($errmsg, $status, $log, @html) = @_;
  191. InitRequest(); # make sure we can report errors before InitRequest
  192. print GetHttpHeader('text/html', 'nocache', $status), GetHtmlHeader(T('Error')),
  193. $q->start_div({class=>"error"}), $q->h1(QuoteHtml($errmsg)), @html, $q->end_div,
  194. $q->end_html, "\n\n"; # newlines for FCGI because of exit()
  195. WriteStringToFile("$TempDir/error", '<body>' . $q->h1("$status $errmsg") . $q->Dump) if $log;
  196. map { ReleaseLockDir($_); } keys %Locks;
  197. exit (2);
  198. }
  199. sub Init {
  200. binmode(STDOUT, ':utf8');
  201. InitDirConfig();
  202. $FS = "\x1e"; # The FS character is the RECORD SEPARATOR control char in ASCII
  203. $Message = ''; # Warnings and non-fatal errors.
  204. InitLinkPatterns(); # Link pattern can be changed in config files
  205. InitModules(); # Modules come first so that users can change module variables in config
  206. InitConfig(); # Config comes as early as possible; remember $q is not available here
  207. InitRequest(); # get $q with $MaxPost; set these in the config file
  208. InitCookie(); # After InitRequest, because $q is used
  209. InitVariables(); # After config, to change variables, after InitCookie for GetParam
  210. }
  211. sub InitModules {
  212. if ($UseConfig and $ModuleDir and -d $ModuleDir) {
  213. foreach my $lib (bsd_glob("$ModuleDir/*.p[ml]")) {
  214. do $lib unless $MyInc{$lib};
  215. $MyInc{$lib} = 1; # Cannot use %INC in mod_perl settings
  216. $Message .= CGI::p("$lib: $@") if $@; # no $q exists, yet
  217. }
  218. }
  219. }
  220. sub InitConfig {
  221. if ($UseConfig and $ConfigFile and not $INC{$ConfigFile} and -f $ConfigFile) {
  222. do $ConfigFile; # these options must be set in a wrapper script or via the environment
  223. $Message .= CGI::p("$ConfigFile: $@") if $@; # remember, no $q exists, yet
  224. }
  225. if ($ConfigPage) { # $FS and $MaxPost must be set in config file!
  226. my ($status, $data) = ReadFile(GetPageFile(FreeToNormal($ConfigPage)));
  227. my %data = ParseData($data); # before InitVariables so GetPageContent won't work
  228. eval $data{text} if $data{text};
  229. $Message .= CGI::p("$ConfigPage: $@") if $@;
  230. }
  231. }
  232. sub InitDirConfig {
  233. utf8::decode($DataDir); # just in case, eg. "WikiDataDir=/tmp/Zürich♥ perl wiki.pl"
  234. $PageDir = "$DataDir/page"; # Stores page data
  235. $KeepDir = "$DataDir/keep"; # Stores kept (old) page data
  236. $TempDir = "$DataDir/temp"; # Temporary files and locks
  237. $LockDir = "$TempDir/lock"; # DB is locked if this exists
  238. $NoEditFile = "$DataDir/noedit"; # Indicates that the site is read-only
  239. $RcFile = "$DataDir/rc.log"; # New RecentChanges logfile
  240. $RcOldFile = "$DataDir/oldrc.log"; # Old RecentChanges logfile
  241. $IndexFile = "$DataDir/pageidx"; # List of all pages
  242. $VisitorFile = "$DataDir/visitors.log"; # List of recent visitors
  243. $RssDir = "$DataDir/rss"; # For rss feed cache
  244. $ReadMe = "$DataDir/README"; # file with default content for the HomePage
  245. # Config file with Perl code to execute
  246. $ConfigFile = "$DataDir/config" unless $ConfigFile;
  247. # For extensions (ending in .pm or .pl)
  248. $ModuleDir = "$DataDir/modules" unless $ModuleDir;
  249. }
  250. sub InitRequest { # set up $q
  251. $CGI::POST_MAX = $MaxPost;
  252. $q = new CGI unless $q;
  253. }
  254. sub InitVariables { # Init global session variables for mod_perl!
  255. $WikiDescription = $q->p($q->a({-href=>'http://www.oddmuse.org/'}, 'Oddmuse'),
  256. $Counter++ > 0 ? Ts('%s calls', $Counter) : '');
  257. $WikiDescription .= $ModulesDescription if $ModulesDescription;
  258. $PrintedHeader = 0; # Error messages don't print headers unless necessary
  259. $ReplaceForm = 0; # Only admins may search and replace
  260. $ScriptName = $q->url() unless defined $ScriptName; # URL used in links
  261. $FullUrl = $ScriptName unless $FullUrl; # URL used in forms
  262. %Locks = ();
  263. @Blocks = ();
  264. @Flags = ();
  265. $Fragment = '';
  266. %RecentVisitors = ();
  267. $OpenPageName = ''; # Currently open page
  268. my $add_space = $CommentsPrefix =~ /[ \t_]$/;
  269. map { $$_ = FreeToNormal($$_); } # convert spaces to underscores on all configurable pagenames
  270. (\$HomePage, \$RCName, \$BannedHosts, \$InterMap, \$StyleSheetPage, \$CommentsPrefix,
  271. \$ConfigPage, \$NotFoundPg, \$RssInterwikiTranslate, \$BannedContent, \$RssExclude, );
  272. $CommentsPrefix .= '_' if $add_space;
  273. $CommentsPattern = "^$CommentsPrefix(.*)"
  274. unless defined $CommentsPattern or not $CommentsPrefix;
  275. @UserGotoBarPages = ($HomePage, $RCName) unless @UserGotoBarPages;
  276. my @pages = sort($BannedHosts, $StyleSheetPage, $ConfigPage, $InterMap,
  277. $RssInterwikiTranslate, $BannedContent);
  278. %AdminPages = map { $_ => 1} @pages, $RssExclude unless %AdminPages;
  279. %LockOnCreation = map { $_ => 1} @pages unless %LockOnCreation;
  280. %PlainTextPages = ($BannedHosts => 1, $BannedContent => 1,
  281. $StyleSheetPage => 1, $ConfigPage => 1) unless %PlainTextPages;
  282. delete $PlainTextPages{''}; # $ConfigPage and others might be empty.
  283. CreateDir($DataDir); # Create directory if it doesn't exist
  284. $Now = time; # Reset in case script is persistent
  285. my $ts = (stat($IndexFile))[9]; # always stat for multiple server processes
  286. ReInit() if not $ts or $LastUpdate != $ts; # reinit if another process changed files (requires $DataDir)
  287. $LastUpdate = $ts;
  288. unshift(@MyRules, \&MyRules) if defined(&MyRules) && (not @MyRules or $MyRules[0] != \&MyRules);
  289. @MyRules = sort {$RuleOrder{$a} <=> $RuleOrder{$b}} @MyRules; # default is 0
  290. ReportError(Ts('Cannot create %s', $DataDir) . ": $!", '500 INTERNAL SERVER ERROR')
  291. unless -d $DataDir;
  292. @IndexOptions = (['pages', T('Include normal pages'), 1, \&AllPagesList]);
  293. foreach my $sub (@MyInitVariables) {
  294. my $result = &$sub;
  295. $Message .= $q->p($@) if $@;
  296. }
  297. }
  298. sub ReInit { # init everything we need if we want to link to stuff
  299. my $id = shift; # when saving a page, what to do depends on the page being saved
  300. AllPagesList() if not $id;
  301. InterInit() if $InterMap and (not $id or $id eq $InterMap);
  302. %RssInterwikiTranslate = () if not $id or $id eq $RssInterwikiTranslate; # special since rarely used
  303. }
  304. sub InitCookie {
  305. undef $q->{'.cookies'}; # Clear cache if it exists (for SpeedyCGI)
  306. my $cookie = $q->cookie($CookieName);
  307. utf8::decode($cookie); # make sure it's decoded as UTF-8
  308. %OldCookie = split(/$FS/o, UrlDecode($cookie));
  309. my %provided = map { utf8::decode($_); $_ => 1 } $q->param;
  310. for my $key (keys %OldCookie) {
  311. SetParam($key, $OldCookie{$key}) unless $provided{$key};
  312. }
  313. CookieUsernameFix();
  314. CookieRollbackFix();
  315. }
  316. sub CookieUsernameFix {
  317. # Only valid usernames get stored in the new cookie.
  318. my $name = GetParam('username', '');
  319. $q->delete('username');
  320. if (!$name) {
  321. # do nothing
  322. } elsif ($WikiLinks && !$FreeLinks && !($name =~ /^$LinkPattern$/o)) {
  323. $Message .= $q->p(Ts('Invalid UserName %s: not saved.', $name));
  324. } elsif ($FreeLinks && (!($name =~ /^$FreeLinkPattern$/o))) {
  325. $Message .= $q->p(Ts('Invalid UserName %s: not saved.', $name));
  326. } elsif (length($name) > 50) { # Too long
  327. $Message .= $q->p(T('UserName must be 50 characters or less: not saved'));
  328. } else {
  329. SetParam('username', $name);
  330. }
  331. }
  332. sub CookieRollbackFix {
  333. my @rollback = grep(/rollback-(\d+)/, $q->param);
  334. if (@rollback and $rollback[0] =~ /(\d+)/) {
  335. SetParam('to', $1);
  336. $q->delete('action');
  337. SetParam('action', 'rollback');
  338. }
  339. }
  340. sub GetParam {
  341. my ($name, $default) = @_;
  342. utf8::encode($name); # may fail
  343. my $result = $q->param($name);
  344. $result = $default unless defined($result);
  345. utf8::decode($result); # may fail
  346. return QuoteHtml($result); # you need to unquote anything that can have <tags>
  347. }
  348. sub SetParam {
  349. my ($name, $val) = @_;
  350. $q->param($name, $val);
  351. }
  352. sub InitLinkPatterns {
  353. my ($WikiWord, $QDelim);
  354. $QDelim = '(?:"")?'; # Optional quote delimiter (removed from the output)
  355. $WikiWord = '[A-Z]+[a-z\x{0080}-\x{fffd}]+[A-Z][A-Za-z\x{0080}-\x{fffd}]*'; # exclude noncharacters FFFE and FFFF
  356. $LinkPattern = "($WikiWord)$QDelim";
  357. $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 &gt;)
  358. # Intersites must start with uppercase letter to avoid confusion with URLs.
  359. $InterSitePattern = '[A-Z\x{0080}-\x{fffd}]+[A-Za-z\x{0080}-\x{fffd}]+';
  360. $InterLinkPattern = "($InterSitePattern:[-a-zA-Z0-9\x{0080}-\x{fffd}_=!?#\$\@~`\%&*+\\/:;.,]*[-a-zA-Z0-9\x{0080}-\x{fffd}_=#\$\@~`\%&*+\\/])$QDelim";
  361. $FreeInterLinkPattern = "($InterSitePattern:[-a-zA-Z0-9\x{0080}-\x{fffd}_=!?#\$\@~`\%&*+\\/:;.,()' ]+)"; # plus space and other characters, and no restrictions on the end of the pattern
  362. $UrlProtocols = 'http|https|ftp|afs|news|nntp|mid|cid|mailto|wais|prospero|telnet|gopher|irc|feed';
  363. $UrlProtocols .= '|file' if $NetworkFile;
  364. my $UrlChars = '[-a-zA-Z0-9/@=+$_~*.,;:?!\'"()&#%]'; # see RFC 2396
  365. my $EndChars = '[-a-zA-Z0-9/@=+$_~*]'; # no punctuation at the end of the url.
  366. $UrlPattern = "((?:$UrlProtocols):$UrlChars+$EndChars)";
  367. $FullUrlPattern="((?:$UrlProtocols):$UrlChars+)"; # when used in square brackets
  368. $ImageExtensions = '(gif|jpg|png|bmp|jpeg|svg)';
  369. }
  370. sub Clean {
  371. my $block = shift;
  372. return 0 unless defined($block); # "0" must print
  373. return 1 if $block eq ''; # '' is the result of a dirty rule
  374. $Fragment .= $block;
  375. return 1;
  376. }
  377. sub Dirty { # arg 1 is the raw text; the real output must be printed instead
  378. if ($Fragment ne '') {
  379. $Fragment =~ s|<p>\s*</p>||g; # clean up extra paragraphs (see end of ApplyRules)
  380. print $Fragment;
  381. push(@Blocks, $Fragment);
  382. push(@Flags, 0);
  383. }
  384. push(@Blocks, (shift));
  385. push(@Flags, 1);
  386. $Fragment = '';
  387. }
  388. sub ApplyRules {
  389. # locallinks: apply rules that create links depending on local config (incl. interlink!)
  390. my ($text, $locallinks, $withanchors, $revision, @tags) = @_; # $revision is used for images
  391. $text =~ s/\r\n/\n/g; # DOS to Unix
  392. $text =~ s/\n+$//g; # No trailing paragraphs
  393. return unless $text ne ''; # allow the text '0'
  394. local $Fragment = ''; # the clean HTML fragment not yet on @Blocks
  395. local @Blocks=(); # the list of cached HTML blocks
  396. local @Flags=(); # a list for each block, 1 = dirty, 0 = clean
  397. Clean(join('', map { AddHtmlEnvironment($_) } @tags));
  398. if ($OpenPageName and $PlainTextPages{$OpenPageName}) { # there should be no $PlainTextPages{''}
  399. Clean(CloseHtmlEnvironments() . $q->pre($text));
  400. } elsif (my ($type) = TextIsFile($text)) {
  401. Clean(CloseHtmlEnvironments() . $q->p(T('This page contains an uploaded file:'))
  402. . $q->p(GetDownloadLink($OpenPageName, (substr($type, 0, 6) eq 'image/'), $revision)));
  403. } else {
  404. my $smileyregex = join "|", keys %Smilies;
  405. $smileyregex = qr/(?=$smileyregex)/;
  406. local $_ = $text;
  407. local $bol = 1;
  408. while (1) {
  409. # Block level elements should eat trailing empty lines to prevent empty p elements.
  410. if ($bol && m/\G(\s*\n)+/cg) {
  411. Clean(CloseHtmlEnvironments() . AddHtmlEnvironment('p'));
  412. } elsif ($bol && m/\G(\&lt;include(\s+(text|with-anchors))?\s+"(.*)"\&gt;[ \t]*\n?)/cgi) {
  413. # <include "uri..."> includes the text of the given URI verbatim
  414. Clean(CloseHtmlEnvironments());
  415. Dirty($1);
  416. my ($oldpos, $old_, $type, $uri) = ((pos), $_, $3, UnquoteHtml($4)); # remember, page content is quoted!
  417. if ($uri =~ /^($UrlProtocols):/o) {
  418. if ($type eq 'text') {
  419. print $q->pre({class=>"include $uri"}, QuoteHtml(GetRaw($uri)));
  420. } else { # never use local links for remote pages, with a starting tag
  421. print $q->start_div({class=>"include $uri"});
  422. ApplyRules(QuoteHtml(GetRaw($uri)), 0, ($type eq 'with-anchors'), undef, 'p');
  423. print $q->end_div();
  424. }
  425. } else {
  426. $Includes{$OpenPageName} = 1;
  427. local $OpenPageName = FreeToNormal($uri);
  428. if ($type eq 'text') {
  429. print $q->pre({class=>"include $OpenPageName"}, QuoteHtml(GetPageContent($OpenPageName)));
  430. } elsif (not $Includes{$OpenPageName}) { # with a starting tag, watch out for recursion
  431. print $q->start_div({class=>"include $OpenPageName"});
  432. ApplyRules(QuoteHtml(GetPageContent($OpenPageName)), $locallinks, $withanchors, undef, 'p');
  433. print $q->end_div();
  434. delete $Includes{$OpenPageName};
  435. } else {
  436. print $q->p({-class=>'error'}, $q->strong(Ts('Recursive include of %s!', $OpenPageName)));
  437. }
  438. }
  439. Clean(AddHtmlEnvironment('p')); # if dirty block is looked at later, this will disappear
  440. ($_, pos) = ($old_, $oldpos); # restore \G (assignment order matters!)
  441. } elsif ($bol && m/\G(\&lt;journal(\s+(\d*)(,(\d*))?)?(\s+"(.*?)")?(\s+(reverse|past|future))?(\s+search\s+(.*))?\&gt;[ \t]*\n?)/cgi) {
  442. # <journal 10 "regexp"> includes 10 pages matching regexp
  443. Clean(CloseHtmlEnvironments());
  444. Dirty($1);
  445. my ($oldpos, $old_) = (pos, $_); # remember these because of the call to PrintJournal()
  446. PrintJournal($3, $5, $7, $9, 0, $11); # no offset
  447. Clean(AddHtmlEnvironment('p')); # if dirty block is looked at later, this will disappear
  448. ($_, pos) = ($old_, $oldpos); # restore \G (assignment order matters!)
  449. } elsif ($bol && m/\G(\&lt;rss(\s+(\d*))?\s+(.*?)\&gt;[ \t]*\n?)/cgis) {
  450. # <rss "uri..."> stores the parsed RSS of the given URI
  451. Clean(CloseHtmlEnvironments());
  452. Dirty($1);
  453. my ($oldpos, $old_) = (pos, $_); # remember these because of the call to RSS()
  454. print RSS($3 ? $3 : 15, split(/\s+/, UnquoteHtml($4)));
  455. Clean(AddHtmlEnvironment('p')); # if dirty block is looked at later, this will disappear
  456. ($_, pos) = ($old_, $oldpos); # restore \G (assignment order matters!)
  457. } elsif (/\G(&lt;search (.*?)&gt;)/cgis) {
  458. # <search regexp>
  459. Clean(CloseHtmlEnvironments());
  460. Dirty($1);
  461. my ($oldpos, $old_) = (pos, $_);
  462. print $q->start_div({-class=>'search'});
  463. SearchTitleAndBody($2, \&PrintSearchResult, SearchRegexp($2));
  464. print $q->end_div;
  465. Clean(AddHtmlEnvironment('p')); # if dirty block is looked at later, this will disappear
  466. ($_, pos) = ($old_, $oldpos); # restore \G (assignment order matters!)
  467. } elsif ($bol && m/\G(&lt;&lt;&lt;&lt;&lt;&lt;&lt; )/cg) {
  468. my ($str, $count, $limit, $oldpos) = ($1, 0, 100, pos);
  469. while (m/\G(.*\n)/cg and $count++ < $limit) {
  470. $str .= $1;
  471. last if (substr($1, 0, 29) eq '&gt;&gt;&gt;&gt;&gt;&gt;&gt; ');
  472. }
  473. if ($count >= $limit) {
  474. pos = $oldpos; # reset because we did not find a match
  475. Clean('&lt;&lt;&lt;&lt;&lt;&lt;&lt; ');
  476. } else {
  477. Clean(CloseHtmlEnvironments() . $q->pre({-class=>'conflict'}, $str) . AddHtmlEnvironment('p'));
  478. }
  479. } elsif ($bol and m/\G#REDIRECT/cg) {
  480. Clean('#REDIRECT');
  481. } elsif (%Smilies && m/\G$smileyregex/cog && Clean(SmileyReplace())) {
  482. } elsif (Clean(RunMyRules($locallinks, $withanchors))) {
  483. } elsif (m/\G\s*\n(\s*\n)+/cg) { # paragraphs: at least two newlines
  484. Clean(CloseHtmlEnvironments() . AddHtmlEnvironment('p')); # another one like this further up
  485. } elsif (m/\G&amp;([A-Za-z]+|#[0-9]+|#x[A-Za-f0-9]+);/cg) { # entity references
  486. Clean("&$1;");
  487. } elsif (m/\G\s+/cg) {
  488. Clean(' ');
  489. } elsif (m/\G([A-Za-z\x{0080}-\x{fffd}]+([ \t]+[a-z\x{0080}-\x{fffd}]+)*[ \t]+)/cg
  490. or m/\G([A-Za-z\x{0080}-\x{fffd}]+)/cg or m/\G(\S)/cg) {
  491. Clean($1); # multiple words but do not match http://foo
  492. } else {
  493. last;
  494. }
  495. $bol = (substr($_,pos()-1,1) eq "\n");
  496. }
  497. }
  498. pos = length $_; # notify module functions we've completed rule handling
  499. Clean(CloseHtmlEnvironments()); # last block -- close it, cache it
  500. if ($Fragment ne '') {
  501. $Fragment =~ s|<p>\s*</p>||g; # clean up extra paragraphs (see end Dirty())
  502. print $Fragment;
  503. push(@Blocks, $Fragment);
  504. push(@Flags, 0);
  505. }
  506. # this can be stored in the page cache -- see PrintCache
  507. return (join($FS, @Blocks), join($FS, @Flags));
  508. }
  509. sub ListRule {
  510. if ($bol && m/\G(\s*\n)*(\*+)[ \t]+/cg
  511. or InElement('li') && m/\G(\s*\n)+(\*+)[ \t]+/cg) {
  512. return CloseHtmlEnvironmentUntil('li')
  513. . OpenHtmlEnvironment('ul',length($2)) . AddHtmlEnvironment('li');
  514. }
  515. return undef;
  516. }
  517. sub LinkRules {
  518. my ($locallinks, $withanchors) = @_;
  519. if ($locallinks
  520. and ($BracketText && m/\G(\[$InterLinkPattern\s+([^\]]+?)\])/cog
  521. or $BracketText && m/\G(\[\[$FreeInterLinkPattern\|([^\]]+?)\]\])/cog
  522. or m/\G(\[$InterLinkPattern\])/cog or m/\G(\[\[\[$FreeInterLinkPattern\]\]\])/cog
  523. or m/\G($InterLinkPattern)/cog or m/\G(\[\[$FreeInterLinkPattern\]\])/cog)) {
  524. # [InterWiki:FooBar text] or [InterWiki:FooBar] or
  525. # InterWiki:FooBar or [[InterWiki:foo bar|text]] or
  526. # [[InterWiki:foo bar]] or [[[InterWiki:foo bar]]]-- Interlinks
  527. # can change when the intermap changes (local config, therefore
  528. # depend on $locallinks). The intermap is only read if
  529. # necessary, so if this not an interlink, we have to backtrack a
  530. # bit.
  531. my $bracket = (substr($1, 0, 1) eq '[') # but \[\[$FreeInterLinkPattern\]\] it not bracket!
  532. && !((substr($1, 0, 2) eq '[[') && (substr($1, 2, 1) ne '[') && index($1, '|') < 0);
  533. my $quote = (substr($1, 0, 2) eq '[[');
  534. my ($oldmatch, $output) = ($1, GetInterLink($2, $3, $bracket, $quote)); # $3 may be empty
  535. if ($oldmatch eq $output) { # no interlink
  536. my ($site, $rest) = split(/:/, $oldmatch, 2);
  537. Clean($site);
  538. pos = (pos) - length($rest) - 1; # skip site, but reparse rest
  539. } else {
  540. Dirty($oldmatch);
  541. print $output; # this is an interlink
  542. }
  543. } elsif ($BracketText && m/\G(\[$FullUrlPattern[|[:space:]]([^\]]+?)\])/cog
  544. or $BracketText && m/\G(\[\[$FullUrlPattern[|[:space:]]([^\]]+?)\]\])/cog
  545. or m/\G(\[$FullUrlPattern\])/cog or m/\G($UrlPattern)/cog) {
  546. # [URL text] makes [text] link to URL, [URL] makes footnotes [1]
  547. my ($str, $url, $text, $bracket, $rest) = ($1, $2, $3, (substr($1, 0, 1) eq '['), '');
  548. if ($url =~ /(&lt|&gt|&amp)$/) { # remove trailing partial named entitites and add them as
  549. $rest = $1; # back again at the end as trailing text.
  550. $url =~ s/&(lt|gt|amp)$//;
  551. }
  552. if ($bracket and not defined $text) { # [URL] is dirty because the number may change
  553. Dirty($str);
  554. print GetUrl($url, $text, $bracket), $rest;
  555. } else {
  556. Clean(GetUrl($url, $text, $bracket, not $bracket) . $rest); # $text may be empty, no images in brackets
  557. }
  558. } elsif ($WikiLinks && m/\G!$LinkPattern/cog) {
  559. Clean($1); # ! gets eaten
  560. } elsif ($WikiLinks && $locallinks
  561. && ($BracketWiki && m/\G(\[$LinkPattern\s+([^\]]+?)\])/cog
  562. or m/\G(\[$LinkPattern\])/cog or m/\G($LinkPattern)/cog)) {
  563. # [LocalPage text], [LocalPage], LocalPage
  564. Dirty($1);
  565. my $bracket = (substr($1, 0, 1) eq '[' and not $3);
  566. print GetPageOrEditLink($2, $3, $bracket);
  567. } elsif ($locallinks && $FreeLinks && (m/\G(\[\[image:$FreeLinkPattern\]\])/cog
  568. or m/\G(\[\[image:$FreeLinkPattern\|([^]|]+)\]\])/cog)) {
  569. # [[image:Free Link]], [[image:Free Link|alt text]]
  570. Dirty($1);
  571. print GetDownloadLink(FreeToNormal($2), 1, undef, UnquoteHtml($3));
  572. } elsif ($FreeLinks && $locallinks
  573. && ($BracketWiki && m/\G(\[\[$FreeLinkPattern\|([^\]]+)\]\])/cog
  574. or m/\G(\[\[\[$FreeLinkPattern\]\]\])/cog
  575. or m/\G(\[\[$FreeLinkPattern\]\])/cog)) {
  576. # [[Free Link|text]], [[[Free Link]]], [[Free Link]]
  577. Dirty($1);
  578. my $bracket = (substr($1, 0, 3) eq '[[[');
  579. print GetPageOrEditLink($2, $3, $bracket, 1); # $3 may be empty
  580. } else {
  581. return undef; # nothing matched
  582. }
  583. return ''; # one of the dirty rules matched (and they all are)
  584. }
  585. sub SetHtmlEnvironmentContainer {
  586. my ($html_tag, $html_tag_attr) = @_;
  587. $HtmlEnvironmentContainers{$html_tag} = defined $html_tag_attr ? (
  588. $HtmlEnvironmentContainers{$html_tag} ? '|'.$HtmlEnvironmentContainers{$html_tag} : '').
  589. $html_tag_attr : '';
  590. }
  591. sub InElement { # is $html_tag in @HtmlStack?
  592. my ($html_tag, $html_tag_attr) = @_;
  593. my $i = 0;
  594. foreach my $html_tag_current (@HtmlStack) {
  595. return 1 if $html_tag_current eq $html_tag and
  596. ($html_tag_attr ? $HtmlAttrStack[$i] =~ m/$html_tag_attr/ : 1);
  597. $i++;
  598. } return '';
  599. }
  600. sub AddOrCloseHtmlEnvironment { # add $html_tag, if not already added; close, otherwise
  601. my ($html_tag, $html_tag_attr) = @_;
  602. return InElement ($html_tag, '^'.$html_tag_attr.'$')
  603. ? CloseHtmlEnvironment($html_tag, '^'.$html_tag_attr.'$')
  604. : AddHtmlEnvironment ($html_tag, $html_tag_attr);
  605. }
  606. sub AddHtmlEnvironment { # add a new $html_tag
  607. my ($html_tag, $html_tag_attr) = @_;
  608. $html_tag_attr = '' if not defined $html_tag_attr;
  609. if ($html_tag and not (@HtmlStack and $HtmlStack[0] eq $html_tag and
  610. ($html_tag_attr ? $HtmlAttrStack[0] =~ m/$html_tag_attr/ : 1))) {
  611. unshift(@HtmlStack, $html_tag);
  612. unshift(@HtmlAttrStack, $html_tag_attr);
  613. return '<'.$html_tag.($html_tag_attr ? ' '.$html_tag_attr : '').'>';
  614. } return ''; # always return something
  615. }
  616. sub OpenHtmlEnvironment { # close the previous $html_tag and open a new one
  617. my ($html_tag, $depth, $html_tag_attr) = @_;
  618. my ($html, $found, @stack) = ('', 0); # always return something
  619. while (@HtmlStack and $found < $depth) { # determine new stack
  620. my $tag = pop(@HtmlStack);
  621. $found++ if $tag eq $html_tag; # this ignores that ul and ol can be equivalent for nesting purposes
  622. unshift(@stack, $tag);
  623. }
  624. unshift(@stack, pop(@HtmlStack)) if @HtmlStack and $found < $depth; # nested sublist coming up, keep list item
  625. @HtmlStack = @stack if not $found; # if starting a new list
  626. $html .= CloseHtmlEnvironments(); # close remaining elements (or all elements if a new list)
  627. @HtmlStack = @stack if $found; # if not starting a new list
  628. $depth = $IndentLimit if $depth > $IndentLimit; # requested depth 0 makes no sense
  629. $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.
  630. if $html_tag_attr && $html_tag_attr !~ m/^\s*[[:alpha:]]@@+\s*=\s*('|").+\1/;
  631. splice(@HtmlAttrStack, 0, @HtmlAttrStack - @HtmlStack); # truncate to size of @HtmlStack
  632. foreach ($found..$depth-1) {
  633. unshift(@HtmlStack, $html_tag);
  634. unshift(@HtmlAttrStack, $html_tag_attr);
  635. $html .= $html_tag_attr ? "<$html_tag $html_tag_attr>" : "<$html_tag>";
  636. }
  637. return $html;
  638. }
  639. sub CloseHtmlEnvironments { # close all -- remember to use AddHtmlEnvironment('p') if required!
  640. return CloseHtmlEnvironmentUntil() if pos($_) == length($_); # close all HTML environments if we're are at the end of this page
  641. my $html = '';
  642. while (@HtmlStack) {
  643. defined $HtmlEnvironmentContainers{$HtmlStack[0]} and # avoid closing block level elements
  644. ($HtmlEnvironmentContainers{$HtmlStack[0]} ? $HtmlAttrStack[0] =~
  645. m/$HtmlEnvironmentContainers{$HtmlStack[0]}/ : 1) and return $html;
  646. shift(@HtmlAttrStack);
  647. $html .= '</'.shift(@HtmlStack).'>';
  648. } return $html;
  649. }
  650. sub CloseHtmlEnvironment { # close environments up to and including $html_tag
  651. my $html = CloseHtmlEnvironmentUntil(@_) if @_ and InElement(@_);
  652. if (@HtmlStack and (not(@_) or defined $html)) {
  653. shift(@HtmlAttrStack);
  654. return $html.'</'.shift(@HtmlStack).'>';
  655. } return $html or ''; # always return something
  656. }
  657. sub CloseHtmlEnvironmentUntil { # close environments up to but not including $html_tag
  658. my ($html_tag, $html_tag_attr) = @_;
  659. my $html = '';
  660. while (@HtmlStack && (pos($_) == length($_) || # while there is an HTML tag-stack and we are at the end of this page or...
  661. !($html_tag ? $HtmlStack[0] eq $html_tag && # the top tag is not the desired tag and...
  662. ($html_tag_attr ? $HtmlAttrStack[0] =~ # its attributes do not match,
  663. m/$html_tag_attr/ : 1) : ''))) { # then...
  664. shift(@HtmlAttrStack); # shift off the top tag and
  665. $html .= '</'.shift(@HtmlStack).'>'; # append it to our HTML string.
  666. } return $html;
  667. }
  668. sub SmileyReplace {
  669. foreach my $regexp (keys %Smilies) {
  670. if (m/\G($regexp)/cg) {
  671. return $q->img({-src=>$Smilies{$regexp}, -alt=>UnquoteHtml($1), -class=>'smiley'});
  672. }
  673. }
  674. }
  675. sub RunMyRules {
  676. my ($locallinks, $withanchors) = @_;
  677. foreach my $sub (@MyRules) {
  678. my $result = &$sub($locallinks, $withanchors);
  679. SetParam('msg', $@) if $@;
  680. return $result if defined($result);
  681. }
  682. return undef;
  683. }
  684. sub RunMyMacros {
  685. $_ = shift;
  686. foreach my $macro (@MyMacros) { &$macro };
  687. return $_;
  688. }
  689. sub PrintWikiToHTML {
  690. my ($markup, $is_saving_cache, $revision, $is_locked) = @_;
  691. my ($blocks, $flags);
  692. $FootnoteNumber = 0;
  693. $markup =~ s/$FS//go if $markup; # Remove separators (paranoia)
  694. $markup = QuoteHtml($markup);
  695. ($blocks, $flags) = ApplyRules($markup, 1, $is_saving_cache, $revision, 'p');
  696. if ($is_saving_cache and not $revision and $Page{revision} # don't save revision 0 pages
  697. and $Page{blocks} ne $blocks and $Page{flags} ne $flags) {
  698. $Page{blocks} = $blocks;
  699. $Page{flags} = $flags;
  700. if ($is_locked or RequestLockDir('main')) { # not fatal!
  701. SavePage();
  702. ReleaseLock() unless $is_locked;
  703. }
  704. }
  705. }
  706. sub DoClearCache {
  707. return unless UserIsAdminOrError();
  708. RequestLockOrError();
  709. print GetHeader('', T('Clear Cache')), $q->start_div({-class=>'content clear'}),
  710. $q->p(T('Main lock obtained.')), '<p>';
  711. foreach my $id (AllPagesList()) {
  712. OpenPage($id);
  713. delete $Page{blocks};
  714. delete $Page{flags};
  715. delete $Page{languages};
  716. $Page{languages} = GetLanguages($Page{blocks}) unless TextIsFile($Page{blocks});
  717. SavePage();
  718. print $q->br(), GetPageLink($id);
  719. }
  720. print '</p>', $q->p(T('Main lock released.')), $q->end_div();
  721. utime time, time, $IndexFile; # touch index file
  722. ReleaseLock();
  723. PrintFooter();
  724. }
  725. sub QuoteHtml {
  726. my $html = shift;
  727. $html =~ s/&/&amp;/g;
  728. $html =~ s/</&lt;/g;
  729. $html =~ s/>/&gt;/g;
  730. $html =~ s/[\x00-\x08\x0b\x0c\x0e-\x1f]/ /g; # legal xml: #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
  731. return $html;
  732. }
  733. sub UnquoteHtml {
  734. my $html = shift;
  735. $html =~ s/&lt;/</g;
  736. $html =~ s/&gt;/>/g;
  737. $html =~ s/&amp;/&/g;
  738. $html =~ s/%26/&/g;
  739. return $html;
  740. }
  741. sub UrlEncode {
  742. my $str = shift;
  743. return '' unless $str;
  744. utf8::encode($str); # turn to byte string
  745. my @letters = split(//, $str);
  746. my %safe = map {$_ => 1} ('a' .. 'z', 'A' .. 'Z', '0' .. '9', '-', '_', '.', '!', '~', '*', "'", '(', ')', '#');
  747. foreach my $letter (@letters) {
  748. $letter = sprintf("%%%02x", ord($letter)) unless $safe{$letter};
  749. }
  750. return join('', @letters);
  751. }
  752. sub UrlDecode {
  753. my $str = shift;
  754. $str =~ s/%([0-9a-f][0-9a-f])/chr(hex($1))/ge;
  755. utf8::decode($str); # make internal string
  756. return $str;
  757. }
  758. sub QuoteRegexp {
  759. my $re = shift;
  760. $re =~ s/([\\\[\]\$()^.])/\\$1/g;
  761. return $re;
  762. }
  763. sub GetRaw {
  764. my $uri = shift;
  765. return unless eval { require LWP::UserAgent; };
  766. my $ua = LWP::UserAgent->new;
  767. my $response = $ua->get($uri);
  768. return $response->decoded_content if $response->is_success;
  769. }
  770. sub DoJournal {
  771. print GetHeader(undef, T('Journal'));
  772. print $q->start_div({-class=>'content'});
  773. PrintJournal(map { GetParam($_, ''); } qw(num num regexp mode offset search));
  774. print $q->end_div();
  775. PrintFooter();
  776. }
  777. sub JournalSort { $b cmp $a }
  778. sub PrintJournal {
  779. return if $CollectingJournal; # avoid infinite loops
  780. local $CollectingJournal = 1;
  781. my ($num, $numMore, $regexp, $mode, $offset, $search) = @_;
  782. $regexp = '^\d\d\d\d-\d\d-\d\d' unless $regexp;
  783. $num = 10 unless $num;
  784. $numMore = $num unless $numMore;
  785. $offset = 0 unless $offset;
  786. # FIXME: Should pass filtered list of pages to SearchTitleAndBody to save time?
  787. my @pages = sort JournalSort (grep(/$regexp/, $search ? SearchTitleAndBody($search) : AllPagesList()));
  788. if ($mode eq 'reverse' or $mode eq 'future') {
  789. @pages = reverse @pages;
  790. }
  791. $b = defined($Today) ? $Today : CalcDay($Now);
  792. if ($mode eq 'future') {
  793. for (my $i = 0; $i < @pages; $i++) {
  794. $a = $pages[$i];
  795. if (JournalSort() == -1) {
  796. @pages = @pages[$i..$#pages];
  797. last;
  798. }
  799. }
  800. } elsif ($mode eq 'past') {
  801. for (my $i = 0; $i < @pages; $i++) {
  802. $a = $pages[$i];
  803. if (JournalSort() == 1) {
  804. @pages = @pages[$i..$#pages];
  805. last;
  806. }
  807. }
  808. }
  809. return unless $pages[$offset];
  810. print $q->start_div({-class=>'journal'});
  811. my $next = $offset + PrintAllPages(1, 1, $num, @pages[$offset .. $#pages]);
  812. print $q->end_div();
  813. $regexp = UrlEncode($regexp);
  814. $search = UrlEncode($search);
  815. print $q->p({-class=>'more'}, ScriptLink("action=more;num=$numMore;regexp=$regexp;search=$search;mode=$mode;offset=$next", T('More...'), 'more')) if $pages[$next];
  816. }
  817. sub PrintAllPages {
  818. my ($links, $comments, $num, @pages) = @_;
  819. my $lang = GetParam('lang', 0);
  820. my ($i, $n) = 0;
  821. for my $id (@pages) {
  822. last if $n >= $JournalLimit and not UserIsAdmin() or $num and $n >= $num;
  823. $i++; # pages looked at
  824. local ($OpenPageName, %Page); # this is local!
  825. OpenPage($id);
  826. my @languages = split(/,/, $Page{languages});
  827. next if $lang and @languages and not grep(/$lang/, @languages);
  828. next if PageMarkedForDeletion();
  829. next if substr($Page{text}, 0, 10) eq '#REDIRECT ';
  830. print $q->start_div({-class=>'page'}),
  831. $q->h1($links ? GetPageLink($id)
  832. : $q->a({-name=>$id}, UrlEncode(FreeToNormal($id))));
  833. PrintPageHtml();
  834. PrintPageCommentsLink($id, $comments);
  835. print $q->end_div();
  836. $n++; # pages actually printed
  837. }
  838. return $i;
  839. }
  840. sub PrintPageCommentsLink {
  841. my ($id, $comments) = @_;
  842. if ($comments and $CommentsPattern and $id !~ /$CommentsPattern/o) {
  843. print $q->p({-class=>'comment'},
  844. GetPageLink($CommentsPrefix . $id, T('Comments on this page')));
  845. }
  846. }
  847. sub RSS {
  848. return if $CollectingJournal; # avoid infinite loops when using full=1
  849. local $CollectingJournal = 1;
  850. my $maxitems = shift;
  851. my @uris = @_;
  852. my %lines;
  853. if (not eval { require XML::RSS; }) {
  854. my $err = $@;
  855. return $q->div({-class=>'rss'}, $q->p({-class=>'error'}, $q->strong(T('XML::RSS is not available on this system.')), $err));
  856. }
  857. # All strings that are concatenated with strings returned by the RSS
  858. # feed must be decoded. Without this decoding, 'diff' and 'history'
  859. # translations will be double encoded when printing the result.
  860. my $tDiff = T('diff');
  861. my $tHistory = T('history');
  862. my $wikins = 'http://purl.org/rss/1.0/modules/wiki/';
  863. my $rdfns = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
  864. @uris = map { s/^"?(.*?)"?$/$1/; $_; } @uris; # strip quotes of uris
  865. my ($str, %data) = GetRss(@uris);
  866. foreach my $uri (keys %data) {
  867. my $data = $data{$uri};
  868. if (not $data) {
  869. $str .= $q->p({-class=>'error'}, $q->strong(Ts('%s returned no data, or LWP::UserAgent is not available.',
  870. $q->a({-href=>$uri}, $uri))));
  871. } else {
  872. my $rss = new XML::RSS;
  873. eval { local $SIG{__DIE__}; $rss->parse($data); };
  874. if ($@) {
  875. $str .= $q->p({-class=>'error'}, $q->strong(Ts('RSS parsing failed for %s', $q->a({-href=>$uri}, $uri)) . ': ' . $@));
  876. } else {
  877. my $interwiki;
  878. if (@uris > 1) {
  879. RssInterwikiTranslateInit(); # not needed anywhere else thus init only now and not in ReInit
  880. $interwiki = $rss->{channel}->{$wikins}->{interwiki};
  881. $interwiki =~ s/^\s+//; # when RDF is used, sometimes whitespace remains,
  882. $interwiki =~ s/\s+$//; # which breaks the test for an existing $interwiki below
  883. if (!$interwiki) {
  884. $interwiki = $rss->{channel}->{$rdfns}->{value};
  885. }
  886. $interwiki = $RssInterwikiTranslate{$interwiki} if $RssInterwikiTranslate{$interwiki};
  887. $interwiki = $RssInterwikiTranslate{$uri} unless $interwiki;
  888. }
  889. my $num = 999;
  890. $str .= $q->p({-class=>'error'}, $q->strong(Ts('No items found in %s.', $q->a({-href=>$uri}, $uri))))
  891. unless @{$rss->{items}};
  892. foreach my $i (@{$rss->{items}}) {
  893. my $line;
  894. my $date = $i->{dc}->{date};
  895. if (not $date and $i->{pubDate}) {
  896. $date = $i->{pubDate};
  897. my %mon = (Jan=>1, Feb=>2, Mar=>3, Apr=>4, May=>5, Jun=>6,
  898. Jul=>7, Aug=>8, Sep=>9, Oct=>10, Nov=>11, Dec=>12);
  899. $date =~ s/^(?:[A-Z][a-z][a-z], )?(\d\d?) ([A-Z][a-z][a-z]) (\d\d(?:\d\d)?)/ # pubDate uses RFC 822
  900. sprintf('%04d-%02d-%02d', ($3 < 100 ? 1900 + $3 : $3), $mon{$2}, $1)/e;
  901. }
  902. $date = sprintf("%03d", $num--) unless $date; # for RSS 0.91 feeds without date, descending
  903. my $title = $i->{title};
  904. my $description = $i->{description};
  905. if (not $title and $description) { # title may be missing in RSS 2.00
  906. $title = $description;
  907. $description = '';
  908. }
  909. $title = $i->{link} if not $title and $i->{link}; # if description and title are missing
  910. $line .= ' (' . $q->a({-href=>$i->{$wikins}->{diff}}, $tDiff) . ')'
  911. if $i->{$wikins}->{diff};
  912. $line .= ' (' . $q->a({-href=>$i->{$wikins}->{history}}, $tHistory) . ')'
  913. if $i->{$wikins}->{history};
  914. if ($title) {
  915. if ($i->{link}) {
  916. $line .= ' ' . $q->a({-href=>$i->{link}, -title=>$date},
  917. ($interwiki ? $interwiki . ':' : '') . $title);
  918. } else {
  919. $line .= ' ' . $title;
  920. }
  921. }
  922. my $contributor = $i->{dc}->{contributor};
  923. $contributor = $i->{$wikins}->{username} unless $contributor;
  924. $contributor =~ s/^\s+//;
  925. $contributor =~ s/\s+$//;
  926. $contributor = $i->{$rdfns}->{value} unless $contributor;
  927. $line .= $q->span({-class=>'contributor'}, $q->span(T(' . . . . ')) . $contributor) if $contributor;
  928. if ($description) {
  929. if ($description =~ /</) {
  930. $line .= $q->div({-class=>'description'}, $description);
  931. } else {
  932. $line .= $q->span({class=>'dash'}, ' &#8211; ') . $q->strong({-class=>'description'}, $description);
  933. }
  934. }
  935. while ($lines{$date}) {
  936. $date .= ' ';
  937. } # make sure this is unique
  938. $lines{$date} = $line;
  939. }
  940. }
  941. }
  942. }
  943. my @lines = sort { $b cmp $a } keys %lines;
  944. @lines = @lines[0..$maxitems-1] if $maxitems and $#lines > $maxitems;
  945. my $date = '';
  946. foreach my $key (@lines) {
  947. my $line = $lines{$key};
  948. if ($key =~ /(\d\d\d\d(?:-\d?\d)?(?:-\d?\d)?)(?:[T ](\d?\d:\d\d))?/) {
  949. my ($day, $time) = ($1, $2);
  950. if ($day ne $date) {
  951. $str .= '</ul>' if $date; # close ul except for the first time where no open ul exists
  952. $date = $day;
  953. $str .= $q->p($q->strong($day)) . '<ul>';
  954. }
  955. $line = $q->span({-class=>'time'}, $time . ' UTC ') . $line if $time;
  956. } elsif (not $date) {
  957. $str .= '<ul>'; # if the feed doesn't have any dates we need to start the list anyhow
  958. $date = $Now; # to ensure the list starts only once
  959. }
  960. $str .= $q->li($line);
  961. }
  962. $str .= '</ul>' if $date;
  963. return $q->div({-class=>'rss'}, $str);
  964. }
  965. sub GetRss {
  966. my %todo = map {$_, GetRssFile($_)} @_;
  967. my %data = ();
  968. my $str = '';
  969. if (GetParam('cache', $UseCache) > 0) {
  970. foreach my $uri (keys %todo) { # read cached rss files if possible
  971. if ($Now - (stat($todo{$uri}))[9] < $RssCacheHours * 3600) {
  972. $data{$uri} = ReadFile($todo{$uri});
  973. delete($todo{$uri}); # no need to fetch them below
  974. }
  975. }
  976. }
  977. my @need_cache = keys %todo;
  978. if (keys %todo > 1) { # try parallel access if available
  979. eval { # see code example in LWP::Parallel, not LWP::Parallel::UserAgent (no callbacks here)
  980. require LWP::Parallel::UserAgent;
  981. my $pua = LWP::Parallel::UserAgent->new();
  982. foreach my $uri (keys %todo) {
  983. if (my $res = $pua->register(HTTP::Request->new('GET', $uri))) {
  984. $str .= $res->error_as_HTML;
  985. }
  986. }
  987. %todo = (); # because the uris in the response may have changed due to redirects
  988. my $entries = $pua->wait();
  989. foreach (keys %$entries) {
  990. my $uri = $entries->{$_}->request->uri;
  991. $data{$uri} = $entries->{$_}->response->decoded_content;
  992. }
  993. }
  994. }
  995. foreach my $uri (keys %todo) { # default operation: synchronous fetching
  996. $data{$uri} = GetRaw($uri);
  997. }
  998. if (GetParam('cache', $UseCache) > 0) {
  999. CreateDir($RssDir);
  1000. foreach my $uri (@need_cache) {
  1001. my $data = $data{$uri};
  1002. # possibly a Latin-1 file without encoding attribute will cause a problem?
  1003. $data =~ s/encoding="[^"]*"/encoding="UTF-8"/; # content was converted
  1004. WriteStringToFile(GetRssFile($uri), $data) if $data;
  1005. }
  1006. }
  1007. return $str, %data;
  1008. }
  1009. sub GetRssFile {
  1010. return $RssDir . '/' . UrlEncode(shift);
  1011. }
  1012. sub RssInterwikiTranslateInit {
  1013. return unless $RssInterwikiTranslate;
  1014. %RssInterwikiTranslate = ();
  1015. foreach (split(/\n/, GetPageContent($RssInterwikiTranslate))) {
  1016. if (/^ ([^ ]+)[ \t]+([^ ]+)$/) {
  1017. $RssInterwikiTranslate{$1} = $2;
  1018. }
  1019. }
  1020. }
  1021. sub GetInterSiteUrl {
  1022. my ($site, $page, $quote) = @_;
  1023. return unless $page;
  1024. $page = join('/', map { UrlEncode($_) } split(/\//, $page)) if $quote; # Foo:bar+baz is not quoted, [[Foo:bar baz]] is.
  1025. my $url = $InterSite{$site} or return;
  1026. $url =~ s/\%s/$page/g or $url .= $page;
  1027. return $url;
  1028. }
  1029. sub BracketLink { # brackets can be removed via CSS
  1030. return $q->span($q->span({class=>'bracket'}, '[') . (shift) . $q->span({class=>'bracket'}, ']'));
  1031. }
  1032. sub GetInterLink {
  1033. my ($id, $text, $bracket, $quote) = @_;
  1034. my ($site, $page) = split(/:/, $id, 2);
  1035. $page =~ s/&amp;/&/g; # Unquote common URL HTML
  1036. my $url = GetInterSiteUrl($site, $page, $quote);
  1037. my $class = 'inter ' . $site;
  1038. if ($text && $bracket && !$url) {
  1039. return "[$id $text]";
  1040. } elsif ($bracket && !$url) {
  1041. return "[$id]";
  1042. } elsif (!$url) {
  1043. return $id;
  1044. } elsif ($bracket && !$text) {
  1045. $text = BracketLink(++$FootnoteNumber);
  1046. $class .= ' number';
  1047. } elsif (!$text) {
  1048. $text = $q->span({-class=>'site'}, $site)
  1049. . $q->span({-class=>'separator'}, ':')
  1050. . $q->span({-class=>'page'}, $page);
  1051. } elsif ($bracket) { # and $text is set
  1052. $class .= ' outside';
  1053. }
  1054. return $q->a({-href=>$url, -class=>$class}, $text);
  1055. }
  1056. sub InterInit {
  1057. %InterSite = ();
  1058. foreach (split(/\n/, GetPageContent($InterMap))) {
  1059. if (/^ ($InterSitePattern)[ \t]+([^ ]+)$/) {
  1060. $InterSite{$1} = $2;
  1061. }
  1062. }
  1063. }
  1064. sub GetUrl {
  1065. my ($url, $text, $bracket, $images) = @_;
  1066. $url =~ /^($UrlProtocols)/;
  1067. my $class = "url $1";
  1068. if ($NetworkFile && $url =~ m|^file:///| && !$AllNetworkFiles
  1069. or !$NetworkFile && $url =~ m|^file:|) {
  1070. # Only do remote file:// links. No file:///c|/windows.
  1071. return $url;
  1072. } elsif ($bracket and not defined $text) {
  1073. $text = BracketLink(++$FootnoteNumber);
  1074. $class .= ' number';
  1075. } elsif (not defined $text) {
  1076. $text = $url;
  1077. } elsif ($bracket) { # and $text is set
  1078. $class .= ' outside';
  1079. }
  1080. $url = UnquoteHtml($url); # links should be unquoted again
  1081. if ($images && $url =~ /^(http:|https:|ftp:).+\.$ImageExtensions$/i) {
  1082. return $q->img({-src=>$url, -alt=>$url, -class=>$class});
  1083. } else {
  1084. return $q->a({-href=>$url, -class=>$class}, $text);
  1085. }
  1086. }
  1087. sub GetPageOrEditLink { # use GetPageLink and GetEditLink if you know the result!
  1088. my ($id, $text, $bracket, $free) = @_;
  1089. $id = FreeToNormal($id);
  1090. my ($class, $resolved, $title, $exists) = ResolveId($id);
  1091. if (!$text && $resolved && $bracket) {
  1092. $text = BracketLink(++$FootnoteNumber);
  1093. $class .= ' number';
  1094. $title = NormalToFree($id);
  1095. }
  1096. my $link = $text||NormalToFree($id);
  1097. if ($resolved) { # anchors don't exist as pages, therefore do not use $exists
  1098. return ScriptLink(UrlEncode($resolved), $link, $class, undef, $title);
  1099. } else { # reproduce markup if $UseQuestionmark
  1100. return GetEditLink($id, UnquoteHtml($bracket ? "[$link]" : $link)) if not $UseQuestionmark;
  1101. $link = QuoteHtml($id) . GetEditLink($id, '?');
  1102. $link .= ($free ? '|' : ' ') . $text if $text and $text ne $id;
  1103. $link = "[[$link]]" if $free;
  1104. $link = "[$link]" if $bracket or not $free and $text;
  1105. return $link;
  1106. }
  1107. }
  1108. sub GetPageLink { # use if you want to force a link to local pages, whether it exists or not
  1109. my ($id, $name, $class, $accesskey) = @_;
  1110. $id = FreeToNormal($id);
  1111. $name = $id unless $name;
  1112. $class .= ' ' if $class;
  1113. return ScriptLink(UrlEncode($id), NormalToFree($name), $class . 'local',
  1114. undef, undef, $accesskey);
  1115. }
  1116. sub GetEditLink { # shortcut
  1117. my ($id, $name, $upload, $accesskey) = @_;
  1118. $id = FreeToNormal($id);
  1119. my $action = 'action=edit;id=' . UrlEncode($id);
  1120. $action .= ';upload=1' if $upload;
  1121. return ScriptLink($action, NormalToFree($name), 'edit', undef, T('Click to edit this page'), $accesskey);
  1122. }
  1123. sub ScriptUrl {
  1124. my $action = shift;
  1125. if ($action =~ /^($UrlProtocols)\%3a/ or $action =~ /^\%2f/) { # nearlinks and other URLs
  1126. $action =~ s/%([0-9a-f][0-9a-f])/chr(hex($1))/ge; # undo urlencode
  1127. # do nothing
  1128. } elsif ($UsePathInfo and index($action, '=') == -1) {
  1129. $action = $ScriptName . '/' . $action;
  1130. } else {
  1131. $action = $ScriptName . '?' . $action;
  1132. }
  1133. return $action unless wantarray;
  1134. return ($action, index($action, '=') != -1);
  1135. }
  1136. sub ScriptLink {
  1137. my ($action, $text, $class, $name, $title, $accesskey) = @_;
  1138. my ($url, $nofollow) = ScriptUrl($action);
  1139. my %params;
  1140. $params{-href} = $url;
  1141. $params{'-rel'} = 'nofollow' if $nofollow;
  1142. $params{'-class'} = $class if $class;
  1143. $params{'-name'} = $name if $name;
  1144. $params{'-title'} = $title if $title;
  1145. $params{'-accesskey'} = $accesskey if $accesskey;
  1146. return $q->a(\%params, $text);
  1147. }
  1148. sub GetDownloadLink {
  1149. my ($id, $image, $revision, $alt) = @_;
  1150. $alt = NormalToFree($id) unless $alt;
  1151. # if the page does not exist
  1152. return '[[' . ($image ? 'image' : 'download') . ':'
  1153. . ($UseQuestionmark ? QuoteHtml($id) . GetEditLink($id, '?', 1)
  1154. : GetEditLink($id, $id, 1)) . ']]'
  1155. unless $IndexHash{$id};
  1156. my $action;
  1157. if ($revision) {
  1158. $action = "action=download;id=" . UrlEncode($id) . ";revision=$revision";
  1159. } elsif ($UsePathInfo) {
  1160. $action = "download/" . UrlEncode($id);
  1161. } else {
  1162. $action = "action=download;id=" . UrlEncode($id);
  1163. }
  1164. if ($image) {
  1165. if ($UsePathInfo and not $revision) {
  1166. $action = $ScriptName . '/' . $action;
  1167. } else {
  1168. $action = $ScriptName . '?' . $action;
  1169. }
  1170. return $action if $image == 2;
  1171. my $result = $q->img({-src=>$action, -alt=>UnquoteHtml($alt), -class=>'upload'});
  1172. $result = ScriptLink(UrlEncode($id), $result, 'image')
  1173. unless $id eq $OpenPageName;
  1174. return $result;
  1175. } else {
  1176. return ScriptLink($action, $alt, 'upload');
  1177. }
  1178. }
  1179. sub PrintCache { # Use after OpenPage!
  1180. my @blocks = split($FS,$Page{blocks});
  1181. my @flags = split($FS,$Page{flags});
  1182. $FootnoteNumber = 0;
  1183. foreach my $block (@blocks) {
  1184. if (shift(@flags)) {
  1185. ApplyRules($block, 1, 1); # local links, anchors, current revision, no start tag
  1186. } else {
  1187. print $block;
  1188. }
  1189. }
  1190. }
  1191. sub PrintPageHtml { # print an open page
  1192. return unless GetParam('page', 1);
  1193. if ($Page{blocks} && $Page{flags} && GetParam('cache', $UseCache) > 0) {
  1194. PrintCache();
  1195. } else {
  1196. PrintWikiToHTML($Page{text}, 1); # save cache, current revision, no main lock
  1197. }
  1198. }
  1199. sub PrintPageDiff { # print diff for open page
  1200. my $diff = GetParam('diff', 0);
  1201. if ($UseDiff && $diff) {
  1202. PrintHtmlDiff($diff);
  1203. print $q->hr() if GetParam('page', 1);
  1204. }
  1205. }
  1206. sub PageHtml {
  1207. my ($id, $limit, $error) = @_;
  1208. my ($diff, $page);
  1209. local *STDOUT;
  1210. OpenPage($id);
  1211. open(STDOUT, '>', \$diff) or die "Can't open memory file: $!";
  1212. binmode(STDOUT, ":utf8");
  1213. PrintPageDiff();
  1214. utf8::decode($diff);
  1215. return $error if $limit and length($diff) > $limit;
  1216. open(STDOUT, '>', \$page) or die "Can't open memory file: $!";
  1217. binmode(STDOUT, ":utf8");
  1218. PrintPageHtml();
  1219. utf8::decode($page);
  1220. return $diff . $q->p($error) if $limit and length($diff . $page) > $limit;
  1221. return $diff . $page;
  1222. }
  1223. sub T {
  1224. my $text = shift;
  1225. return $Translate{$text} if $Translate{$text};
  1226. return $text;
  1227. }
  1228. sub Ts {
  1229. my ($text, $string) = @_;
  1230. $text = T($text);
  1231. $text =~ s/\%s/$string/ if defined($string);
  1232. return $text;
  1233. }
  1234. sub Tss {
  1235. my $text = $_[0];
  1236. $text = T($text);
  1237. $text =~ s/\%([1-9])/$_[$1]/ge;
  1238. return $text;
  1239. }
  1240. sub GetId {
  1241. my $id = UnquoteHtml(GetParam('id', GetParam('title', ''))); # id=x or title=x -> x
  1242. if (not $id) {
  1243. my @keywords = $q->keywords;
  1244. foreach my $keyword (@keywords) {
  1245. utf8::decode($keyword);
  1246. }
  1247. $id = join('_', @keywords) unless $id; # script?p+q -> p_q
  1248. }
  1249. if ($UsePathInfo) {
  1250. my $path = $q->path_info;
  1251. utf8::decode($path);
  1252. my @path = split(/\//, $path);
  1253. $id = pop(@path) unless $id; # script/p/q -> q
  1254. foreach my $p (@path) {
  1255. SetParam($p, 1); # script/p/q -> p=1
  1256. }
  1257. }
  1258. return $id;
  1259. }
  1260. sub DoBrowseRequest {
  1261. # We can use the error message as the HTTP error code
  1262. ReportError(Ts('CGI Internal error: %s',$q->cgi_error), $q->cgi_error) if $q->cgi_error;
  1263. print $q->header(-status=>'304 NOT MODIFIED') and return if PageFresh(); # return value is ignored
  1264. my $id = GetId();
  1265. my $action = lc(GetParam('action', '')); # script?action=foo;id=bar
  1266. $action = 'download' if GetParam('download', '') and not $action; # script/download/id
  1267. my $search = GetParam('search', '');
  1268. if ($Action{$action}) {
  1269. &{$Action{$action}}($id);
  1270. } elsif ($action and defined &MyActions) {
  1271. eval { local $SIG{__DIE__}; MyActions(); };
  1272. } elsif ($action) {
  1273. ReportError(Ts('Invalid action parameter %s', $action), '501 NOT IMPLEMENTED');
  1274. } elsif ($search ne '') { # allow search for "0"
  1275. SetParam('action', 'search'); # fake it
  1276. DoSearch($search);
  1277. } elsif (GetParam('title', '') and not GetParam('Cancel', '')) {
  1278. DoPost(GetParam('title', ''));
  1279. } else {
  1280. BrowseResolvedPage($id||$HomePage); # default action!
  1281. }
  1282. }
  1283. sub ValidId { # hack alert: returns error message if invalid, and unfortunately the empty string if valid!
  1284. my $id = FreeToNormal(shift);
  1285. return T('Page name is missing') unless $id;
  1286. return Ts('Page name is too long: %s', $id) if length($id) > 120;
  1287. return Ts('Invalid Page %s (must not end with .db)', $id) if $id =~ m|\.db$|;
  1288. return Ts('Invalid Page %s (must not end with .lck)', $id) if $id =~ m|\.lck$|;
  1289. return Ts('Invalid Page %s', $id) if $FreeLinks ? $id !~ m|^$FreeLinkPattern$| : $id !~ m|^$LinkPattern$|;
  1290. }
  1291. sub ValidIdOrDie {
  1292. my $id = shift;
  1293. my $error = ValidId($id);
  1294. ReportError($error, '400 BAD REQUEST') if $error;
  1295. return 1;
  1296. }
  1297. sub ResolveId { # return css class, resolved id, title (eg. for popups), exist-or-not
  1298. my $id = shift;
  1299. return ('local', $id, '', 1) if $IndexHash{$id};
  1300. return ('', '', '', '');
  1301. }
  1302. sub BrowseResolvedPage {
  1303. my $id = FreeToNormal(shift);
  1304. my ($class, $resolved, $title, $exists) = ResolveId($id);
  1305. if ($class && $class eq 'near' && not GetParam('rcclusteronly', 0)) { # nearlink (is url)
  1306. print $q->redirect({-uri=>$resolved});
  1307. } elsif ($class && $class eq 'alias') { # an anchor was found instead of a page
  1308. ReBrowsePage($resolved);
  1309. } elsif (not $resolved and $NotFoundPg and $id !~ /$CommentsPattern/o) { # custom page-not-found message
  1310. BrowsePage($NotFoundPg);
  1311. } elsif ($resolved) { # an existing page was found
  1312. BrowsePage($resolved, GetParam('raw', 0));
  1313. } else { # new page!
  1314. BrowsePage($id, GetParam('raw', 0), undef, '404 NOT FOUND') if ValidIdOrDie($id);
  1315. }
  1316. }
  1317. sub BrowsePage {
  1318. my ($id, $raw, $comment, $status) = @_;
  1319. OpenPage($id);
  1320. my ($text, $revision, $summary) = GetTextRevision(GetParam('revision', ''));
  1321. $text = $NewText unless $revision or $Page{revision}; # new text for new pages
  1322. # handle a single-level redirect
  1323. my $oldId = GetParam('oldid', '');
  1324. if ((substr($text, 0, 10) eq '#REDIRECT ')) {
  1325. if ($oldId) {
  1326. $Message .= $q->p(T('Too many redirections'));
  1327. } elsif ($revision) {
  1328. $Message .= $q->p(T('No redirection for old revisions'));
  1329. } elsif (($FreeLinks and $text =~ /^\#REDIRECT\s+\[\[$FreeLinkPattern\]\]/)
  1330. or ($WikiLinks and $text =~ /^\#REDIRECT\s+$LinkPattern/)) {
  1331. return ReBrowsePage(FreeToNormal($1), $id);
  1332. } else {
  1333. $Message .= $q->p(T('Invalid link pattern for #REDIRECT'));
  1334. }
  1335. }
  1336. # shortcut if we only need the raw text: no caching, no diffs, no html.
  1337. if ($raw) {
  1338. print GetHttpHeader('text/plain', $Page{ts}, $IndexHash{$id} ? undef : '404 NOT FOUND');
  1339. if ($raw == 2) {
  1340. print $Page{ts} . " # Do not delete this line when editing!\n";
  1341. }
  1342. print $text;
  1343. return;
  1344. }
  1345. # normal page view
  1346. my $msg = GetParam('msg', '');
  1347. $Message .= $q->p($msg) if $msg; # show message if the page is shown
  1348. SetParam('msg', '');
  1349. print GetHeader($id, NormalToFree($id), $oldId, undef, $status);
  1350. my $showDiff = GetParam('diff', 0);
  1351. if ($UseDiff && $showDiff) {
  1352. PrintHtmlDiff($showDiff, GetParam('diffrevision', $revision), $revision, $text, $summary);
  1353. print $q->hr();
  1354. }
  1355. PrintPageContent($text, $revision, $comment);
  1356. SetParam('rcclusteronly', $id) if FreeToNormal(GetCluster($text)) eq $id; # automatically filter by cluster
  1357. PrintRcHtml($id);
  1358. PrintFooter($id, $revision, $comment);
  1359. }
  1360. sub ReBrowsePage {
  1361. my ($id, $oldId) = map { UrlEncode($_); } @_; # encode before printing URL
  1362. if ($oldId) { # Target of #REDIRECT (loop breaking)
  1363. print GetRedirectPage("action=browse;oldid=$oldId;id=$id", $id);
  1364. } else {
  1365. print GetRedirectPage($id, $id);
  1366. }
  1367. }
  1368. sub GetRedirectPage {
  1369. my ($action, $name) = @_;
  1370. my ($url, $html);
  1371. if (GetParam('raw', 0)) {
  1372. $html = GetHttpHeader('text/plain');
  1373. $html .= Ts('Please go on to %s.', $action); # no redirect
  1374. return $html;
  1375. }
  1376. if ($UsePathInfo and $action !~ /=/) {
  1377. $url = $ScriptName . '/' . $action;
  1378. } else {
  1379. $url = $ScriptName . '?' . $action;
  1380. }
  1381. my $nameLink = $q->a({-href=>$url}, $name);
  1382. my %headers = (-uri=>$url);
  1383. my $cookie = Cookie();
  1384. if ($cookie) {
  1385. $headers{-cookie} = $cookie;
  1386. }
  1387. return $q->redirect(%headers);
  1388. }
  1389. sub DoRandom {
  1390. my @pages = AllPagesList();
  1391. ReBrowsePage($pages[int(rand($#pages + 1))]);
  1392. }
  1393. sub PageFresh { # pages can depend on other pages (ie. last update), admin status, and css
  1394. return 1 if $q->http('HTTP_IF_NONE_MATCH') and GetParam('cache', $UseCache) >= 2
  1395. and $q->http('HTTP_IF_NONE_MATCH') eq PageEtag();
  1396. }
  1397. sub PageEtag {
  1398. my ($changed, $visible, %params) = CookieData();
  1399. return UrlEncode(join($FS, $LastUpdate, sort(values %params))); # no CTL in field values
  1400. }
  1401. sub FileFresh { # old files are never stale, current files are stale when the page was modified
  1402. return 1 if $q->http('HTTP_IF_NONE_MATCH') and GetParam('cache', $UseCache) >= 2
  1403. and (GetParam('revision', 0) or $q->http('HTTP_IF_NONE_MATCH') eq $Page{ts});
  1404. }
  1405. sub BrowseRc {
  1406. my $id = shift;
  1407. if (GetParam('raw', 0)) {
  1408. print GetHttpHeader('text/plain');
  1409. PrintRcText();
  1410. } else {
  1411. PrintRcHtml($id || $RCName, 1);
  1412. }
  1413. }
  1414. sub GetRcLines { # starttime, hash of seen pages to use as a second return value
  1415. my $starttime = shift || GetParam('from', 0) ||
  1416. $Now - GetParam('days', $RcDefault) * 86400; # 24*60*60
  1417. my $filterOnly = GetParam('rcfilteronly', '');
  1418. # these variables apply accross logfiles
  1419. my %match = $filterOnly ? map { $_ => 1 } SearchTitleAndBody($filterOnly) : ();
  1420. my %following = ();
  1421. my @result = ();
  1422. # check the first timestamp in the default file, maybe read old log file
  1423. open(F, '<:utf8', $RcFile);
  1424. my $line = <F>;
  1425. my ($ts) = split(/$FS/o, $line); # the first timestamp in the regular rc file
  1426. if (not $ts or $ts > $starttime) { # we need to read the old rc file, too
  1427. push(@result, GetRcLinesFor($RcOldFile, $starttime,\%match, \%following));
  1428. }
  1429. push(@result, GetRcLinesFor($RcFile, $starttime, \%match, \%following));
  1430. # GetRcLinesFor is trying to save memory space, but some operations
  1431. # can only happen once we have all the data.
  1432. return LatestChanges(StripRollbacks(@result));
  1433. }
  1434. sub LatestChanges {
  1435. my $all = GetParam('all', 0);
  1436. my @result = @_;
  1437. my %seen = ();
  1438. for (my $i = $#result; $i >= 0; $i--) {
  1439. my $id = $result[$i][1];
  1440. if ($all) {
  1441. $result[$i][9] = 1 unless $seen{$id}; # mark latest edit
  1442. } else {
  1443. splice(@result, $i, 1) if $seen{$id}; # remove older edits
  1444. }
  1445. $seen{$id} = 1;
  1446. }
  1447. my $to = GetParam('upto', 0);
  1448. if ($to) {
  1449. for (my $i = 0; $i < $#result; $i++) {
  1450. if ($result[$i][0] > $to) {
  1451. splice(@result, $i);
  1452. last;
  1453. }
  1454. }
  1455. }
  1456. return reverse @result;
  1457. }
  1458. sub StripRollbacks {
  1459. my @result = @_;
  1460. if (not (GetParam('all', 0) or GetParam('rollback', 0))) { # strip rollbacks
  1461. my (%rollback);
  1462. for (my $i = $#result; $i >= 0; $i--) {
  1463. # some fields have a different meaning if looking at rollbacks
  1464. my $ts = $result[$i][0];
  1465. my $id = $result[$i][1];
  1466. my $target_ts = $result[$i][2];
  1467. my $target_id = $result[$i][3];
  1468. if ($id eq '[[rollback]]') {
  1469. if ($target_id) {
  1470. $rollback{$target_id} = $target_ts; # single page rollback
  1471. splice(@result, $i, 1); # strip marker
  1472. } else {
  1473. my $end = $i;
  1474. while ($ts > $target_ts and $i > 0) {
  1475. $i--; # quickly skip all these lines
  1476. $ts = $result[$i][0];
  1477. }
  1478. splice(@result, $i + 1, $end - $i);
  1479. $i++; # compensate $i-- in for loop
  1480. }
  1481. } elsif ($rollback{$id} and $ts > $rollback{$id}) {
  1482. splice(@result, $i, 1); # strip rolled back single pages
  1483. }
  1484. }
  1485. } else { # just strip the marker left by DoRollback()
  1486. for (my $i = $#result; $i >= 0; $i--) {
  1487. splice(@result, $i, 1) if $result[$i][1] eq '[[rollback]]'; # id
  1488. }
  1489. }
  1490. return @result;
  1491. }
  1492. sub GetRcLinesFor {
  1493. my $file = shift;
  1494. my $starttime = shift;
  1495. my %match = %{$_[0]}; # deref
  1496. my %following = %{$_[1]}; # deref
  1497. # parameters
  1498. my $showminoredit = GetParam('showedit', $ShowEdits); # show minor edits
  1499. my $all = GetParam('all', 0);
  1500. my ($idOnly, $userOnly, $hostOnly, $clusterOnly, $filterOnly, $match, $lang,
  1501. $followup) = map { UnquoteHtml(GetParam($_, '')); }
  1502. qw(rcidonly rcuseronly rchostonly
  1503. rcclusteronly rcfilteronly match lang followup);
  1504. # parsing and filtering
  1505. my @result = ();
  1506. open(F, '<:utf8', $file) or return ();
  1507. while (my $line = <F>) {
  1508. chomp($line);
  1509. my ($ts, $id, $minor, $summary, $host, $username, $revision,
  1510. $languages, $cluster) = split(/$FS/o, $line);
  1511. next if $ts < $starttime;
  1512. $following{$id} = $ts if $followup and $followup eq $username;
  1513. next if $followup and (not $following{$id} or $ts <= $following{$id});
  1514. next if $idOnly and $idOnly ne $id;
  1515. next if $filterOnly and not $match{$id};
  1516. next if ($userOnly and $userOnly ne $username);
  1517. next if $minor == 1 and !$showminoredit; # skip minor edits (if [[rollback]] this is bogus)
  1518. next if !$minor and $showminoredit == 2; # skip major edits
  1519. next if $match and $id !~ /$match/i;
  1520. next if $hostOnly and $host !~ /$hostOnly/i;
  1521. my @languages = split(/,/, $languages);
  1522. next if $lang and @languages and not grep(/$lang/, @languages);
  1523. if ($PageCluster) {
  1524. ($cluster, $summary) = ($1, $2) if $summary =~ /^\[\[$FreeLinkPattern\]\] ?: *(.*)/
  1525. or $summary =~ /^$LinkPattern ?: *(.*)/o;
  1526. next if ($clusterOnly and $clusterOnly ne $cluster);
  1527. $cluster = '' if $clusterOnly; # don't show cluster if $clusterOnly eq $cluster
  1528. if ($all < 2 and not $clusterOnly and $cluster) {
  1529. $summary = "$id: $summary"; # print the cluster instead of the page
  1530. $id = $cluster;
  1531. $revision = '';
  1532. }
  1533. } else {
  1534. $cluster = '';
  1535. }
  1536. $following{$id} = $ts if $followup and $followup eq $username;
  1537. push(@result, [$ts, $id, $minor, $summary, $host, $username, $revision,
  1538. \@languages, $cluster]);
  1539. }
  1540. return @result;
  1541. }
  1542. sub ProcessRcLines {
  1543. my ($printDailyTear, $printRCLine) = @_; # code references
  1544. # needed for output
  1545. my $date = '';
  1546. for my $line (GetRcLines()) {
  1547. my ($ts, $id, $minor, $summary, $host, $username, $revision, $languageref,
  1548. $cluster, $last) = @$line;
  1549. if ($date ne CalcDay($ts)) {
  1550. $date = CalcDay($ts);
  1551. &$printDailyTear($date);
  1552. }
  1553. &$printRCLine($id, $ts, $host, $username, $summary, $minor, $revision,
  1554. $languageref, $cluster, $last);
  1555. }
  1556. }
  1557. sub RcHeader {
  1558. my ($from, $upto, $html) = (GetParam('from', 0), GetParam('upto', 0), '');
  1559. if ($from) {
  1560. $html .= $q->h2(Ts('Updates since %s', TimeToText(GetParam('from', 0))) . ' '
  1561. . ($upto ? Ts('up to %s', TimeToText($upto)) : ''));
  1562. } else {
  1563. $html .= $q->h2((GetParam('days', $RcDefault) != 1)
  1564. ? Ts('Updates in the last %s days',
  1565. GetParam('days', $RcDefault))
  1566. : Ts('Updates in the last %s day',
  1567. GetParam('days', $RcDefault)))
  1568. }
  1569. my $days = GetParam('days', $RcDefault);
  1570. my $all = GetParam('all', 0);
  1571. my $edits = GetParam('showedit', 0);
  1572. my $rollback = GetParam('rollback', 0);
  1573. my $action = '';
  1574. my ($idOnly, $userOnly, $hostOnly, $clusterOnly, $filterOnly,
  1575. $match, $lang, $followup) =
  1576. map {
  1577. my $val = GetParam($_, '');
  1578. $html .= $q->p($q->b('(' . Ts('for %s only', $val) . ')')) if $val;
  1579. $action .= ";$_=$val" if $val; # remember these parameters later!
  1580. $val;
  1581. } qw(rcidonly rcuseronly rchostonly rcclusteronly rcfilteronly
  1582. match lang followup);
  1583. my $rss = "action=rss$action;days=$days;all=$all;showedit=$edits";
  1584. if ($clusterOnly) {
  1585. $action = GetPageParameters('browse', $clusterOnly) . $action;
  1586. } else {
  1587. $action = "action=rc$action";
  1588. }
  1589. my @menu;
  1590. if ($all) {
  1591. push(@menu, ScriptLink("$action;days=$days;all=0;showedit=$edits",
  1592. T('List latest change per page only')));
  1593. } else {
  1594. push(@menu, ScriptLink("$action;days=$days;all=1;showedit=$edits",
  1595. T('List all changes')));
  1596. if ($rollback) {
  1597. push(@menu, ScriptLink("$action;days=$days;all=0;rollback=0;"
  1598. . "showedit=$edits", T('Skip rollbacks')));
  1599. } else {
  1600. push(@menu, ScriptLink("$action;days=$days;all=0;rollback=1;"
  1601. . "showedit=$edits", T('Include rollbacks')));
  1602. }
  1603. }
  1604. if ($edits) {
  1605. push(@menu, ScriptLink("$action;days=$days;all=$all;showedit=0",
  1606. T('List only major changes')));
  1607. } else {
  1608. push(@menu, ScriptLink("$action;days=$days;all=$all;showedit=1",
  1609. T('Include minor changes')));
  1610. }
  1611. return $html .
  1612. $q->p((map { ScriptLink("$action;days=$_;all=$all;showedit=$edits",
  1613. ($_ != 1) ? Ts('%s days', $_) : Ts('%s days', $_));
  1614. } @RcDays), $q->br(), @menu, $q->br(),
  1615. ScriptLink($action . ';from=' . ($LastUpdate + 1)
  1616. . ";all=$all;showedit=$edits", T('List later changes')),
  1617. ScriptLink($rss, T('RSS'), 'rss nopages nodiff'),
  1618. ScriptLink("$rss;full=1", T('RSS with pages'), 'rss pages nodiff'),
  1619. ScriptLink("$rss;full=1;diff=1", T('RSS with pages and diff'),
  1620. 'rss pages diff'));
  1621. }
  1622. sub GetScriptUrlWithRcParameters {
  1623. my $url = "$ScriptName?action=rss";
  1624. foreach my $param (qw(from upto days all showedit rollback rcidonly rcuseronly
  1625. rchostonly rcclusteronly rcfilteronly match lang
  1626. followup page diff full)) {
  1627. my $val = GetParam($param, undef);
  1628. $url .= ";$param=$val" if defined $val;
  1629. }
  1630. return $url;
  1631. }
  1632. sub GetFilterForm {
  1633. my $form = $q->strong(T('Filters'));
  1634. $form .= $q->input({-type=>'hidden', -name=>'action', -value=>'rc'});
  1635. $form .= $q->input({-type=>'hidden', -name=>'all', -value=>1})
  1636. if (GetParam('all', 0));
  1637. $form .= $q->input({-type=>'hidden', -name=>'showedit', -value=>1})
  1638. if (GetParam('showedit', 0));
  1639. $form .= $q->input({-type=>'hidden', -name=>'days',
  1640. -value=>GetParam('days', $RcDefault)})
  1641. if (GetParam('days', $RcDefault) != $RcDefault);
  1642. my $table = '';
  1643. foreach my $h (['match' => T('Title:')],
  1644. ['rcfilteronly' => T('Title and Body:')],
  1645. ['rcuseronly' => T('Username:')], ['rchostonly' => T('Host:')],
  1646. ['followup' => T('Follow up to:')]) {
  1647. $table .= $q->Tr($q->td($q->label({-for=>$h->[0]}, $h->[1])),
  1648. $q->td($q->textfield(-name=>$h->[0], -id=>$h->[0],
  1649. -size=>20)));
  1650. }
  1651. $table .= $q->Tr($q->td($q->label({-for=>'rclang'}, T('Language:')))
  1652. . $q->td($q->textfield(-name=>'lang', -id=>'rclang',
  1653. -size=>10,
  1654. -default=>GetParam('lang', ''))))
  1655. if %Languages;
  1656. return GetFormStart(undef, 'get', 'filter') . $q->p($form) . $q->table($table)
  1657. . $q->p($q->submit('dofilter', T('Go!'))) . $q->end_form;
  1658. }
  1659. sub RcHtml {
  1660. my ($html, $inlist) = ('', 0);
  1661. # Optimize param fetches and translations out of main loop
  1662. my $all = GetParam('all', 0);
  1663. my $admin = UserIsAdmin();
  1664. my $rollback_was_possible = 0;
  1665. my $printDailyTear = sub {
  1666. my $date = shift;
  1667. if ($inlist) {
  1668. $html .= '</ul>';
  1669. $inlist = 0;
  1670. }
  1671. $html .= $q->p($q->strong($date));
  1672. if (!$inlist) {
  1673. $html .= '<ul>';
  1674. $inlist = 1;
  1675. }
  1676. };
  1677. my $printRCLine = sub {
  1678. my($id, $ts, $host, $username, $summary, $minor, $revision,
  1679. $languages, $cluster, $last) = @_;
  1680. my $all_revision = $last ? undef : $revision; # no revision for the last one
  1681. $host = QuoteHtml($host);
  1682. my $author = GetAuthorLink($host, $username);
  1683. my $sum = $summary ? $q->span({class=>'dash'}, ' &#8211; ')
  1684. . $q->strong(QuoteHtml($summary)) : '';
  1685. my $edit = $minor ? $q->em({class=>'type'}, T('(minor)')) : '';
  1686. my $lang = @{$languages}
  1687. ? $q->span({class=>'lang'}, '[' . join(', ', @{$languages}) . ']') : '';
  1688. my ($pagelink, $history, $diff, $rollback) = ('', '', '', '');
  1689. if ($all) {
  1690. $pagelink = GetOldPageLink('browse', $id, $all_revision, $id, $cluster);
  1691. my $rollback_is_possible = RollbackPossible($ts);
  1692. if ($admin and ($rollback_is_possible or $rollback_was_possible)) {
  1693. $rollback = $q->submit("rollback-$ts", T('rollback'));
  1694. $rollback_was_possible = $rollback_is_possible;
  1695. } else {
  1696. $rollback_was_possible = 0;
  1697. }
  1698. } elsif ($cluster) {
  1699. $pagelink = GetOldPageLink('browse', $id, $revision, $id, $cluster);
  1700. } else {
  1701. $pagelink = GetPageLink($id, $cluster);
  1702. $history = '(' . GetHistoryLink($id, T('history')) . ')';
  1703. }
  1704. if ($cluster and $PageCluster) {
  1705. $diff .= GetPageLink($PageCluster) . ':';
  1706. } elsif ($UseDiff and GetParam('diffrclink', 1)) {
  1707. if ($revision == 1) {
  1708. $diff .= '(' . $q->span({-class=>'new'}, T('new')) . ')';
  1709. } elsif ($all) {
  1710. $diff .= '(' . ScriptLinkDiff(2, $id, T('diff'), '', $all_revision) .')';
  1711. } else {
  1712. $diff .= '(' . ScriptLinkDiff($minor ? 2 : 1, $id, T('diff'), '') . ')';
  1713. }
  1714. }
  1715. $html .= $q->li($q->span({-class=>'time'}, CalcTime($ts)), $diff, $history,
  1716. $rollback, $pagelink, T(' . . . . '), $author, $sum, $lang,
  1717. $edit);
  1718. };
  1719. ProcessRcLines($printDailyTear, $printRCLine);
  1720. $html .= '</ul>' if $inlist;
  1721. my $to = GetParam('from', $Now - GetParam('days', $RcDefault) * 86400);
  1722. my $from = $to - GetParam('days', $RcDefault) * 86400;
  1723. my $more = "action=rc;from=$from;upto=$to";
  1724. foreach (qw(all showedit rollback rcidonly rcuseronly rchostonly
  1725. rcclusteronly rcfilteronly match lang followup)) {
  1726. my $val = GetParam($_, '');
  1727. $more .= ";$_=$val" if $val;
  1728. }
  1729. $html .= $q->p({-class=>'more'}, ScriptLink($more, T('More...'), 'more'));
  1730. return GetFormStart(undef, 'get', 'rc') . $html . $q->end_form;
  1731. }
  1732. sub PrintRcHtml { # to append RC to existing page, or action=rc directly
  1733. my ($id, $standalone) = @_;
  1734. my $rc = ($id eq $RCName or $id eq T($RCName) or T($id) eq $RCName);
  1735. print GetHeader('', $rc ? NormalToFree($id) : Ts('All changes for %s', NormalToFree($id)))
  1736. if $standalone;
  1737. if ($standalone or $rc or GetParam('rcclusteronly', '')) {
  1738. print $q->start_div({-class=>'rc'});
  1739. print $q->hr() unless $standalone or GetParam('embed', $EmbedWiki);
  1740. print RcHeader() . RcHtml() . GetFilterForm() . $q->end_div();
  1741. }
  1742. PrintFooter($id) if $standalone;
  1743. }
  1744. sub RcTextItem {
  1745. my ($name, $value) = @_;
  1746. $value =~ s/\n+$//;
  1747. $value =~ s/\n+/\n /;
  1748. return $value ? $name . ': ' . $value . "\n" : '';
  1749. }
  1750. sub RcTextRevision {
  1751. my($id, $ts, $host, $username, $summary, $minor, $revision,
  1752. $languages, $cluster, $last) = @_;
  1753. my $link = $ScriptName
  1754. . (GetParam('all', 0) && ! $last
  1755. ? '?' . GetPageParameters('browse', $id, $revision, $cluster, $last)
  1756. : ($UsePathInfo ? '/' : '?') . UrlEncode($id));
  1757. print "\n", RcTextItem('title', NormalToFree($id)),
  1758. RcTextItem('description', $summary),
  1759. RcTextItem('generator', GetAuthor($host, $username)),
  1760. RcTextItem('language', join(', ', @{$languages})), RcTextItem('link', $link),
  1761. RcTextItem('last-modified', TimeToW3($ts)),
  1762. RcTextItem('revision', $revision);
  1763. }
  1764. sub PrintRcText { # print text rss header and call ProcessRcLines
  1765. local $RecentLink = 0;
  1766. print RcTextItem('title', $SiteName),
  1767. RcTextItem('description', $SiteDescription), RcTextItem('link', $ScriptName),
  1768. RcTextItem('generator', 'Oddmuse'), RcTextItem('rights', $RssRights);
  1769. ProcessRcLines(sub {}, \&RcTextRevision);
  1770. }
  1771. sub GetRcRss {
  1772. my $date = TimeToRFC822($LastUpdate);
  1773. my %excluded = ();
  1774. if (GetParam("exclude", 1)) {
  1775. foreach (split(/\n/, GetPageContent($RssExclude))) {
  1776. if (/^ ([^ ]+)[ \t]*$/) { # only read lines with one word after one space
  1777. $excluded{$1} = 1;
  1778. }
  1779. }
  1780. }
  1781. my $rss = qq{<?xml version="1.0" encoding="UTF-8"?>\n};
  1782. if ($RssStyleSheet =~ /\.(xslt?|xml)$/) {
  1783. $rss .= qq{<?xml-stylesheet type="text/xml" href="$RssStyleSheet" ?>\n};
  1784. } elsif ($RssStyleSheet) {
  1785. $rss .= qq{<?xml-stylesheet type="text/css" href="$RssStyleSheet" ?>\n};
  1786. }
  1787. $rss .= qq{<rss version="2.0"
  1788. xmlns:wiki="http://purl.org/rss/1.0/modules/wiki/"
  1789. xmlns:dc="http://purl.org/dc/elements/1.1/"
  1790. xmlns:cc="http://web.resource.org/cc/"
  1791. xmlns:atom="http://www.w3.org/2005/Atom">
  1792. <channel>
  1793. <docs>http://blogs.law.harvard.edu/tech/rss</docs>
  1794. };
  1795. my $title = QuoteHtml($SiteName) . ': '
  1796. . GetParam('title', QuoteHtml(NormalToFree($HomePage)));
  1797. $rss .= "<title>$title</title>\n";
  1798. $rss .= "<link>" . ScriptUrl($HomePage) . "</link>\n";
  1799. $rss .= qq{<atom:link href="} . GetScriptUrlWithRcParameters()
  1800. . qq{" rel="self" type="application/rss+xml" />\n};
  1801. $rss .= "<description>" . QuoteHtml($SiteDescription) . "</description>\n"
  1802. if $SiteDescription;
  1803. $rss .= "<pubDate>$date</pubDate>\n";
  1804. $rss .= "<lastBuildDate>$date</lastBuildDate>\n";
  1805. $rss .= "<generator>Oddmuse</generator>\n";
  1806. $rss .= "<copyright>$RssRights</copyright>\n" if $RssRights;
  1807. $rss .= join('', map {"<cc:license>" . QuoteHtml($_) . "</cc:license>\n"}
  1808. (ref $RssLicense eq 'ARRAY' ? @$RssLicense : $RssLicense))
  1809. if $RssLicense;
  1810. $rss .= "<wiki:interwiki>$InterWikiMoniker</wiki:interwiki>\n"
  1811. if $InterWikiMoniker;
  1812. if ($RssImageUrl) {
  1813. $rss .= "<image>\n";
  1814. $rss .= "<url>$RssImageUrl</url>\n";
  1815. $rss .= "<title>$title</title>\n"; # the same as the channel
  1816. $rss .= "<link>$ScriptName</link>\n"; # the same as the channel
  1817. $rss .= "</image>\n";
  1818. }
  1819. my $limit = GetParam("rsslimit", 15); # Only take the first 15 entries
  1820. my $count = 0;
  1821. ProcessRcLines(sub {}, sub {
  1822. my $id = shift;
  1823. return if $excluded{$id}
  1824. or ($limit ne 'all' and $count++ >= $limit);
  1825. $rss .= "\n" . RssItem($id, @_);
  1826. });
  1827. $rss .= "</channel>\n</rss>\n";
  1828. return $rss;
  1829. }
  1830. sub RssItem {
  1831. my ($id, $ts, $host, $username, $summary, $minor, $revision,
  1832. $languages, $cluster, $last) = @_;
  1833. my $name = ItemName($id);
  1834. $summary = PageHtml($id, 50*1024, T('This page is too big to send over RSS.'))
  1835. if (GetParam('full', 0)); # full page means summary is not shown
  1836. my $date = TimeToRFC822($ts);
  1837. $username = QuoteHtml($username);
  1838. $username = $host unless $username;
  1839. my $rss = "<item>\n";
  1840. $rss .= "<title>$name</title>\n";
  1841. my $link = ScriptUrl(GetParam('all', $cluster)
  1842. ? GetPageParameters('browse', $id, $revision, $cluster, $last)
  1843. : UrlEncode($id));
  1844. $rss .= "<link>$link</link>\n<guid>$link</guid>\n";
  1845. $rss .= "<description>" . QuoteHtml($summary) . "</description>\n" if $summary;
  1846. $rss .= "<pubDate>" . $date . "</pubDate>\n";
  1847. $rss .= "<comments>" . ScriptUrl($CommentsPrefix . UrlEncode($id))
  1848. . "</comments>\n" if $CommentsPattern and $id !~ /$CommentsPattern/o;
  1849. $rss .= "<dc:contributor>" . $username . "</dc:contributor>\n" if $username;
  1850. $rss .= "<wiki:status>" . (1 == $revision ? 'new' : 'updated')
  1851. . "</wiki:status>\n";
  1852. $rss .= "<wiki:importance>" . ($minor ? 'minor' : 'major')
  1853. . "</wiki:importance>\n";
  1854. $rss .= "<wiki:version>" . $revision . "</wiki:version>\n";
  1855. $rss .= "<wiki:history>" . ScriptUrl("action=history;id=" . UrlEncode($id))
  1856. . "</wiki:history>\n";
  1857. $rss .= "<wiki:diff>" . ScriptUrl("action=browse;diff=1;id=" . UrlEncode($id))
  1858. . "</wiki:diff>\n" if $UseDiff and GetParam('diffrclink', 1);
  1859. return $rss . "</item>\n";
  1860. }
  1861. sub DoRss {
  1862. print GetHttpHeader('application/xml');
  1863. print GetRcRss();
  1864. }
  1865. sub DoHistory {
  1866. my $id = shift;
  1867. ValidIdOrDie($id);
  1868. OpenPage($id);
  1869. if (GetParam('raw', 0)) {
  1870. print GetHttpHeader('text/plain'),
  1871. RcTextItem('title', Ts('History of %s', NormalToFree($OpenPageName))),
  1872. RcTextItem('date', TimeToText($Now)),
  1873. RcTextItem('link', ScriptUrl("action=history;id=$OpenPageName;raw=1")),
  1874. RcTextItem('generator', 'Oddmuse');
  1875. SetParam('all', 1);
  1876. my @languages = split(/,/, $Page{languages});
  1877. RcTextRevision($id, $Page{ts}, $Page{host}, $Page{username}, $Page{summary},
  1878. $Page{minor}, $Page{revision}, \@languages, undef, 1);
  1879. foreach my $revision (GetKeepRevisions($OpenPageName)) {
  1880. my %keep = GetKeptRevision($revision);
  1881. @languages = split(/,/, $keep{languages});
  1882. RcTextRevision($id, $keep{ts}, $keep{host}, $keep{username},
  1883. $keep{summary}, $keep{minor}, $keep{revision}, \@languages);
  1884. }
  1885. } else {
  1886. print GetHeader('', Ts('History of %s', NormalToFree($id)));
  1887. my $row = 0;
  1888. my $rollback = UserCanEdit($id, 0) && (GetParam('username', '')
  1889. or UserIsEditor());
  1890. my $date = CalcDay($Page{ts});
  1891. my @html = (GetHistoryLine($id, \%Page, $row++, $rollback, $date, 1));
  1892. foreach my $revision (GetKeepRevisions($OpenPageName)) {
  1893. my %keep = GetKeptRevision($revision);
  1894. my $new = CalcDay($keep{ts});
  1895. push(@html, GetHistoryLine($id, \%keep, $row++, $rollback,
  1896. $new, $new ne $date));
  1897. $date = $new;
  1898. }
  1899. @html = (GetFormStart(undef, 'get', 'history'),
  1900. $q->p($q->submit({-name=>T('Compare')}),
  1901. # don't use $q->hidden here!
  1902. $q->input({-type=>'hidden',-name=>'action',-value=>'browse'}),
  1903. $q->input({-type=>'hidden', -name=>'diff', -value=>'1'}),
  1904. $q->input({-type=>'hidden', -name=>'id', -value=>$id})),
  1905. $q->table({-class=>'history'}, @html),
  1906. $q->p($q->submit({-name=>T('Compare')})),
  1907. $q->end_form()) if $UseDiff;
  1908. push(@html, $q->p(ScriptLink('title=' . UrlEncode($id) . ';text='
  1909. . UrlEncode($DeletedPage) . ';summary='
  1910. . UrlEncode(T('Deleted')),
  1911. T('Mark this page for deletion'))))
  1912. if $KeepDays and $rollback and $Page{revision};
  1913. print $q->div({-class=>'content history'}, @html);
  1914. PrintFooter($id, 'history');
  1915. }
  1916. }
  1917. sub GetHistoryLine {
  1918. my ($id, $dataref, $row, $rollback, $date, $newday) = @_;
  1919. my %data = %$dataref;
  1920. my $revision = $data{revision};
  1921. return $q->p(T('No other revisions available')) unless $revision;
  1922. my $html = CalcTime($data{ts});
  1923. if (0 == $row) { # current revision
  1924. $html .= ' (' . T('current') . ')' if $rollback;
  1925. $html .= ' ' . GetPageLink($id, Ts('Revision %s', $revision));
  1926. } else {
  1927. $html .= ' ' . $q->submit("rollback-$data{ts}", T('rollback')) if $rollback;
  1928. $html .= ' ' . GetOldPageLink('browse', $id, $revision,
  1929. Ts('Revision %s', $revision));
  1930. }
  1931. my $host = $data{host};
  1932. $host = $data{ip} unless $host;
  1933. $html .= T(' . . . . ') . GetAuthorLink($host, $data{username});
  1934. $html .= $q->span({class=>'dash'}, ' &#8211; ')
  1935. . $q->strong(QuoteHtml($data{summary})) if $data{summary};
  1936. $html .= ' ' . $q->em({class=>'type'}, T('(minor)')) . ' ' if $data{minor};
  1937. if ($UseDiff) {
  1938. my %attr1 = (-type=>'radio', -name=>'diffrevision', -value=>$revision);
  1939. $attr1{-checked} = 'checked' if 1==$row;
  1940. my %attr2 = (-type=>'radio', -name=>'revision', -value=> $row ? $revision : '');
  1941. $attr2{-checked} = 'checked' if 0==$row; # first row is special
  1942. $html = $q->Tr($q->td($q->input(\%attr1)), $q->td($q->input(\%attr2)),
  1943. $q->td($html));
  1944. $html = $q->Tr($q->td({-colspan=>3}, $q->strong($date))) . $html if $newday;
  1945. } else {
  1946. $html .= $q->br();
  1947. $html = $q->strong($date) . $q->br() . $html if $newday;
  1948. }
  1949. return $html;
  1950. }
  1951. sub DoContributors {
  1952. my $id = shift;
  1953. SetParam('rcidonly', $id);
  1954. SetParam('all', 1);
  1955. print GetHeader('', Ts('Contributors to %s', NormalToFree($id || $SiteName)));
  1956. my %contrib = ();
  1957. for my $line (GetRcLines(1)) {
  1958. my ($ts, $pagename, $minor, $summary, $host, $username) = @$line;
  1959. $contrib{$username}++ if $username;
  1960. }
  1961. print $q->div({-class=>'content contrib'},
  1962. $q->p(map { GetPageLink($_) } sort(keys %contrib)));
  1963. PrintFooter();
  1964. }
  1965. sub RollbackPossible {
  1966. my $ts = shift; # there can be no rollback to the most recent change(s) made (1s resolution!)
  1967. return $ts != $LastUpdate && ($Now - $ts) < $KeepDays * 86400; # 24*60*60
  1968. }
  1969. sub DoRollback {
  1970. my $page = shift;
  1971. my $to = GetParam('to', 0);
  1972. ReportError(T('Missing target for rollback.'), '400 BAD REQUEST') unless $to;
  1973. ReportError(T('Target for rollback is too far back.'), '400 BAD REQUEST') unless $page or RollbackPossible($to);
  1974. ReportError(T('A username is required for ordinary users.'), '403 FORBIDDEN') unless GetParam('username', '') or UserIsEditor();
  1975. my @ids = ();
  1976. if (not $page) { # cannot just use list length because of ('')
  1977. return unless UserIsAdminOrError(); # only admins can do mass changes
  1978. SetParam('showedit', 1); # make GetRcLines return minor edits as well
  1979. SetParam('all', 1); # prevent LatestChanges from interfering
  1980. SetParam('rollback', 1); # prevent StripRollbacks from interfering
  1981. my %ids = map { my ($ts, $id) = @$_; $id => 1; } # make unique via hash
  1982. GetRcLines($Now - $KeepDays * 86400); # 24*60*60
  1983. @ids = keys %ids;
  1984. } else {
  1985. @ids = ($page);
  1986. }
  1987. RequestLockOrError();
  1988. print GetHeader('', T('Rolling back changes')),
  1989. $q->start_div({-class=>'content rollback'}), $q->start_p();
  1990. foreach my $id (@ids) {
  1991. OpenPage($id);
  1992. my ($text, $minor, $ts) = GetTextAtTime($to);
  1993. if ($Page{text} eq $text) {
  1994. print T("The two revisions are the same."), $q->br() if $page; # no message when doing mass revert
  1995. } elsif (!UserCanEdit($id, 1)) {
  1996. print Ts('Editing not allowed for %s.', $id), $q->br();
  1997. } elsif (not UserIsEditor() and my $rule = BannedContent($text)) {
  1998. print Ts('Rollback of %s would restore banned content.', $id), $rule, $q->br();
  1999. } else {
  2000. Save($id, $text, Ts('Rollback to %s', TimeToText($to)), $minor, ($Page{ip} ne $ENV{REMOTE_ADDR}));
  2001. print Ts('%s rolled back', GetPageLink($id)), ($ts ? ' ' . Ts('to %s', TimeToText($to)) : ''), $q->br();
  2002. }
  2003. }
  2004. WriteRcLog('[[rollback]]', $page, $to); # leave marker
  2005. print $q->end_p() . $q->end_div();
  2006. ReleaseLock();
  2007. PrintFooter($page);
  2008. }
  2009. sub DoAdminPage {
  2010. my ($id, @rest) = @_;
  2011. my @menu = ();
  2012. push(@menu, ScriptLink('action=index',
  2013. T('Index of all pages'), 'index'))
  2014. if $Action{index};
  2015. push(@menu, ScriptLink('action=version',
  2016. T('Wiki Version'), 'version'))
  2017. if $Action{version};
  2018. push(@menu, ScriptLink('action=unlock',
  2019. T('Unlock Wiki'), 'unlock'))
  2020. if $Action{unlock};
  2021. push(@menu, ScriptLink('action=password',
  2022. T('Password'), 'password'))
  2023. if $Action{password};
  2024. push(@menu, ScriptLink('action=maintain',
  2025. T('Run maintenance'), 'maintain'))
  2026. if $Action{maintain};
  2027. if (UserIsAdmin()) {
  2028. push(@menu, ScriptLink('action=clear',
  2029. T('Clear Cache'), 'clear'))
  2030. if $Action{clear};
  2031. if ($Action{editlock}) {
  2032. if (-f "$DataDir/noedit") {
  2033. push(@menu, ScriptLink('action=editlock;set=0',
  2034. T('Unlock site'), 'editlock 0'));
  2035. } else {
  2036. push(@menu, ScriptLink('action=editlock;set=1',
  2037. T('Lock site'), 'editlock 1'));
  2038. }
  2039. }
  2040. if ($id and $Action{pagelock}) {
  2041. my $title = NormalToFree($id);
  2042. if (-f GetLockedPageFile($id)) {
  2043. push(@menu, ScriptLink('action=pagelock;set=0;id='
  2044. . UrlEncode($id),
  2045. Ts('Unlock %s', $title),
  2046. 'pagelock 0'));
  2047. } else {
  2048. push(@menu, ScriptLink('action=pagelock;set=1;id='
  2049. . UrlEncode($id),
  2050. Ts('Lock %s', $title),
  2051. 'pagelock 1'));
  2052. }
  2053. }
  2054. }
  2055. foreach my $sub (@MyAdminCode) {
  2056. &$sub($id, \@menu, \@rest);
  2057. $Message .= $q->p($@) if $@; # since this happens before GetHeader is called, the message will be shown
  2058. }
  2059. print GetHeader('', T('Administration')),
  2060. $q->div({-class=>'content admin'}, $q->p(T('Actions:')), $q->ul($q->li(\@menu)),
  2061. $q->p(T('Important pages:')) . $q->ul(map { $q->li(GetPageOrEditLink($_, NormalToFree($_))) if $_;
  2062. } sort keys %AdminPages),
  2063. $q->p(Ts('To mark a page for deletion, put <strong>%s</strong> on the first line.',
  2064. $DeletedPage)), @rest);
  2065. PrintFooter();
  2066. }
  2067. sub GetPageParameters {
  2068. my ($action, $id, $revision, $cluster, $last) = @_;
  2069. $id = FreeToNormal($id);
  2070. my $link = "action=$action;id=" . UrlEncode($id);
  2071. $link .= ";revision=$revision" if $revision and not $last;
  2072. $link .= ';rcclusteronly=' . UrlEncode($cluster) if $cluster;
  2073. return $link;
  2074. }
  2075. sub GetOldPageLink {
  2076. my ($action, $id, $revision, $name, $cluster, $last) = @_;
  2077. return ScriptLink(GetPageParameters($action, $id, $revision, $cluster, $last),
  2078. NormalToFree($name), 'revision');
  2079. }
  2080. sub GetSearchLink {
  2081. my ($text, $class, $name, $title) = @_;
  2082. my $id = UrlEncode(QuoteRegexp('"' . $text . '"'));
  2083. $name = UrlEncode($name);
  2084. $text = NormalToFree($text);
  2085. $id =~ s/_/+/g; # Search for url-escaped spaces
  2086. return ScriptLink('search=' . $id, $text, $class, $name, $title);
  2087. }
  2088. sub ScriptLinkDiff {
  2089. my ($diff, $id, $text, $new, $old) = @_;
  2090. my $action = 'action=browse;diff=' . $diff . ';id=' . UrlEncode($id);
  2091. $action .= ";diffrevision=$old" if ($old and $old ne '');
  2092. $action .= ";revision=$new" if ($new and $new ne '');
  2093. return ScriptLink($action, $text, 'diff');
  2094. }
  2095. sub GetAuthor {
  2096. my ($host, $username) = @_;
  2097. return $username . ' ' . Ts('from %s', $host) if $username and $host;
  2098. return $username if $username;
  2099. return T($host); # could be 'Anonymous'
  2100. }
  2101. sub GetAuthorLink {
  2102. my ($host, $username) = @_;
  2103. $username = FreeToNormal($username);
  2104. my $name = NormalToFree($username);
  2105. if (ValidId($username) ne '') { # ValidId() returns error string
  2106. $username = ''; # Just pretend it isn't there.
  2107. }
  2108. if ($username and $RecentLink) {
  2109. return ScriptLink(UrlEncode($username), $name, 'author', undef, $host);
  2110. } elsif ($username) {
  2111. return $q->span({-class=>'author'}, $name);
  2112. }
  2113. return T($host); # could be 'Anonymous'
  2114. }
  2115. sub GetHistoryLink {
  2116. my ($id, $text) = @_;
  2117. my $action = 'action=history;id=' . UrlEncode(FreeToNormal($id));
  2118. return ScriptLink($action, $text, 'history');
  2119. }
  2120. sub GetRCLink {
  2121. my ($id, $text) = @_;
  2122. return ScriptLink('action=rc;all=1;from=1;showedit=1;rcidonly=' . UrlEncode(FreeToNormal($id)), $text, 'rc');
  2123. }
  2124. sub GetHeader {
  2125. my ($id, $title, $oldId, $nocache, $status) = @_;
  2126. my $embed = GetParam('embed', $EmbedWiki);
  2127. my $alt = T('[Home]');
  2128. my $result = GetHttpHeader('text/html', $nocache, $status);
  2129. if ($oldId) {
  2130. $Message .= $q->p('(' . Ts('redirected from %s', GetEditLink($oldId, $oldId)) . ')');
  2131. }
  2132. $result .= GetHtmlHeader(Ts('%s: ', $SiteName) . UnWiki($title), $id);
  2133. if ($embed) {
  2134. $result .= $q->div({-class=>'header'}, $q->div({-class=>'message'}, $Message)) if $Message;
  2135. return $result;
  2136. }
  2137. $result .= $q->start_div({-class=>'header'});
  2138. if (not $embed and $LogoUrl) {
  2139. my $url = $IndexHash{$LogoUrl} ? GetDownloadLink($LogoUrl, 2) : $LogoUrl;
  2140. $result .= ScriptLink(UrlEncode($HomePage),
  2141. $q->img({-src=>$url, -alt=>$alt, -class=>'logo'}), 'logo');
  2142. }
  2143. if (GetParam('toplinkbar', $TopLinkBar)) {
  2144. $result .= GetGotoBar($id);
  2145. if (%SpecialDays) {
  2146. my ($sec, $min, $hour, $mday, $mon, $year) = gmtime($Now);
  2147. if ($SpecialDays{($mon + 1) . '-' . $mday}) {
  2148. $result .= $q->br() . $q->span({-class=>'specialdays'},
  2149. $SpecialDays{($mon + 1) . '-' . $mday});
  2150. }
  2151. }
  2152. }
  2153. $result .= $q->div({-class=>'message'}, $Message) if $Message;
  2154. if ($id ne '') {
  2155. $result .= $q->h1(GetSearchLink($id, '', '', T('Click to search for references to this page')));
  2156. } else {
  2157. $result .= $q->h1($title);
  2158. }
  2159. return $result . $q->end_div() . $q->start_div({-class=>'wrapper'});
  2160. }
  2161. sub GetHttpHeader {
  2162. return if $PrintedHeader;
  2163. $PrintedHeader = 1;
  2164. my ($type, $ts, $status, $encoding) = @_; # $ts is undef, a ts, or 'nocache'
  2165. $q->charset($type =~ m!^(text/|application/xml)! ? 'utf-8' : ''); # text/plain, text/html, application/xml: UTF-8
  2166. my %headers = (-cache_control=>($UseCache < 0 ? 'no-cache' : 'max-age=10'));
  2167. $headers{-etag} = $ts || PageEtag() if GetParam('cache', $UseCache) >= 2;
  2168. $headers{'-last-modified'} = TimeToRFC822($ts) if $ts and $ts ne 'nocache'; # RFC 2616 section 13.3.4
  2169. $headers{-type} = GetParam('mime-type', $type);
  2170. $headers{-status} = $status if $status;
  2171. $headers{-Content_Encoding} = $encoding if $encoding;
  2172. my $cookie = Cookie();
  2173. $headers{-cookie} = $cookie if $cookie;
  2174. if ($q->request_method() eq 'HEAD') {
  2175. print $q->header(%headers), "\n\n"; # add newlines for FCGI because of exit()
  2176. exit; # total shortcut -- HEAD never expects anything other than the header!
  2177. }
  2178. return $q->header(%headers);
  2179. }
  2180. sub CookieData {
  2181. my ($changed, $visible, %params);
  2182. foreach my $key (keys %CookieParameters) {
  2183. my $default = $CookieParameters{$key};
  2184. my $value = GetParam($key, $default);
  2185. $params{$key} = $value if $value ne $default;
  2186. # The cookie is considered to have changed under the following
  2187. # condition: If the value was already set, and the new value is
  2188. # not the same as the old value, or if there was no old value, and
  2189. # the new value is not the default.
  2190. my $change = (defined $OldCookie{$key} ? ($value ne $OldCookie{$key}) : ($value ne $default));
  2191. $visible = 1 if $change and not $InvisibleCookieParameters{$key};
  2192. $changed = 1 if $change; # note if any parameter changed and needs storing
  2193. }
  2194. return $changed, $visible, %params;
  2195. }
  2196. sub Cookie {
  2197. my ($changed, $visible, %params) = CookieData(); # params are URL encoded
  2198. if ($changed) {
  2199. my $cookie = join(UrlEncode($FS), %params); # no CTL in field values
  2200. utf8::encode($cookie); # prevent casting to Latin 1
  2201. my $result = $q->cookie(-name=>$CookieName, -value=>$cookie,
  2202. -expires=>'+2y');
  2203. $Message .= $q->p(T('Cookie: ') . $CookieName . ', '
  2204. . join(', ', map {$_ . '=' . $params{$_}}
  2205. keys(%params))) if $visible;
  2206. return $result;
  2207. }
  2208. return '';
  2209. }
  2210. sub GetHtmlHeader { # always HTML!
  2211. my ($title, $id) = @_;
  2212. my $base = $SiteBase ? $q->base({-href=>$SiteBase}) : '';
  2213. $base .= '<link rel="alternate" type="application/wiki" title="'
  2214. . T('Edit this page') . '" href="'
  2215. . ScriptUrl('action=edit;id=' . UrlEncode(GetId())) . '" />' if $id;
  2216. return $DocumentHeader
  2217. . $q->head($q->title($title) . $base
  2218. . GetCss() . GetRobots() . GetFeeds() . $HtmlHeaders
  2219. . '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />')
  2220. . '<body class="' . GetParam('theme', $ScriptName) . '">';
  2221. }
  2222. sub GetRobots { # NOINDEX for non-browse pages.
  2223. if (GetParam('action', 'browse') eq 'browse'
  2224. and not GetParam('revision', '')) {
  2225. return '<meta name="robots" content="INDEX,FOLLOW" />';
  2226. } else {
  2227. return '<meta name="robots" content="NOINDEX,FOLLOW" />';
  2228. }
  2229. }
  2230. sub GetFeeds { # default for $HtmlHeaders
  2231. my $html = '<link rel="alternate" type="application/rss+xml" title="'
  2232. . QuoteHtml($SiteName) . '" href="' . $ScriptName . '?action=rss" />';
  2233. my $id = GetId(); # runs during Init, not during DoBrowseRequest
  2234. $html .= '<link rel="alternate" type="application/rss+xml" title="'
  2235. . QuoteHtml("$SiteName: $id") . '" href="' . $ScriptName
  2236. . '?action=rss;rcidonly=' . UrlEncode(FreeToNormal($id)) . '" />' if $id;
  2237. my $username = GetParam('username', '');
  2238. $html .= '<link rel="alternate" type="application/rss+xml" '
  2239. . 'title="Follow-ups for ' . NormalToFree($username) . '" '
  2240. . 'href="' . ScriptUrl('action=rss;followup=' . UrlEncode($username))
  2241. . '" />' if $username;
  2242. return $html;
  2243. }
  2244. sub GetCss { # prevent javascript injection
  2245. my @css = map { s/\".*//; $_; } split(/\s+/, GetParam('css', ''));
  2246. push (@css, $StyleSheet) if $StyleSheet and not @css;
  2247. push (@css, "$ScriptName?action=browse;id=" . UrlEncode($StyleSheetPage) . ";raw=1;mime-type=text/css")
  2248. if $IndexHash{$StyleSheetPage} and not @css;
  2249. push (@css, 'http://www.oddmuse.org/default.css') unless @css;
  2250. return join('', map { qq(<link type="text/css" rel="stylesheet" href="$_" />) } @css);
  2251. }
  2252. sub PrintPageContent {
  2253. my ($text, $revision, $comment) = @_;
  2254. print $q->start_div({-class=>'content browse'});
  2255. if ($revision eq '' and $Page{blocks} and GetParam('cache', $UseCache) > 0) {
  2256. PrintCache();
  2257. } else {
  2258. my $savecache = ($Page{revision} > 0 and $revision eq ''); # new page not cached
  2259. PrintWikiToHTML($text, $savecache, $revision); # unlocked, with anchors, unlocked
  2260. }
  2261. if ($comment) {
  2262. print $q->start_div({-class=>'preview'}), $q->hr();
  2263. print $q->h2(T('Preview:'));
  2264. # no caching, current revision, unlocked
  2265. PrintWikiToHTML(AddComment('', $comment));
  2266. print $q->hr(), $q->h2(T('Preview only, not yet saved')), $q->end_div();
  2267. } print $q->end_div();
  2268. }
  2269. sub PrintFooter {
  2270. my ($id, $rev, $comment) = @_;
  2271. if (GetParam('embed', $EmbedWiki)) {
  2272. print $q->end_html, "\n";
  2273. return;
  2274. }
  2275. print GetCommentForm($id, $rev, $comment),
  2276. $q->start_div({-class=>'wrapper close'}), $q->end_div(), $q->end_div(),
  2277. $q->start_div({-class=>'footer'}), $q->hr(), GetGotoBar($id),
  2278. GetFooterLinks($id, $rev), GetFooterTimestamp($id, $rev), GetSearchForm();
  2279. if ($DataDir =~ m|/tmp/|) {
  2280. print $q->p($q->strong(T('Warning') . ': ')
  2281. . Ts('Database is stored in temporary directory %s', $DataDir));
  2282. }
  2283. print T($FooterNote) if $FooterNote;
  2284. print $q->p(GetValidatorLink()) if GetParam('validate', $ValidatorLink);
  2285. print $q->p(Ts('%s seconds', (time - $Now))) if GetParam('timing',0);
  2286. print $q->end_div();
  2287. PrintMyContent($id) if defined(&PrintMyContent);
  2288. foreach my $sub (@MyFooters) {
  2289. print &$sub(@_);
  2290. }
  2291. print $q->end_html, "\n";
  2292. }
  2293. sub GetFooterTimestamp {
  2294. my ($id, $rev) = @_;
  2295. if ($id and $rev ne 'history' and $rev ne 'edit' and $Page{revision}) {
  2296. my @elements = ($q->br(), ($rev eq '' ? T('Last edited') : T('Edited')), TimeToText($Page{ts}),
  2297. Ts('by %s', GetAuthorLink($Page{host}, $Page{username})));
  2298. push(@elements, ScriptLinkDiff(2, $id, T('(diff)'), $rev)) if $UseDiff and $Page{revision} > 1;
  2299. return $q->span({-class=>'time'}, @elements);
  2300. }
  2301. return '';
  2302. }
  2303. sub GetFooterLinks {
  2304. my ($id, $rev) = @_;
  2305. my @elements;
  2306. if ($id and $rev ne 'history' and $rev ne 'edit') {
  2307. if ($CommentsPattern) {
  2308. if ($id =~ /$CommentsPattern/o) {
  2309. push(@elements, GetPageLink($1, undef, 'original', T('a')));
  2310. } else {
  2311. push(@elements, GetPageLink($CommentsPrefix . $id, undef, 'comment', T('c')));
  2312. }
  2313. }
  2314. if (UserCanEdit($id, 0)) {
  2315. if ($rev) { # showing old revision
  2316. push(@elements, GetOldPageLink('edit', $id, $rev, Ts('Edit revision %s of this page', $rev)));
  2317. } else { # showing current revision
  2318. push(@elements, GetEditLink($id, T('Edit this page'), undef, T('e')));
  2319. }
  2320. } else { # no permission or generated page
  2321. push(@elements, ScriptLink('action=password', T('This page is read-only'), 'password'));
  2322. }
  2323. }
  2324. push(@elements, GetHistoryLink($id, T('View other revisions'))) if $Action{history} and $id and $rev ne 'history';
  2325. push(@elements, GetPageLink($id, T('View current revision')),
  2326. GetRCLink($id, T('View all changes'))) if $Action{history} and $rev ne '';
  2327. push(@elements, ScriptLink("action=contrib;id=" . UrlEncode($id), T('View contributors'), 'contrib'))
  2328. if $Action{contrib} and $id and $rev eq 'history';
  2329. if ($Action{admin} and GetParam('action', '') ne 'admin') {
  2330. my $action = 'action=admin';
  2331. $action .= ';id=' . UrlEncode($id) if $id;
  2332. push(@elements, ScriptLink($action, T('Administration'), 'admin'));
  2333. }
  2334. return @elements ? $q->span({-class=>'edit bar'}, $q->br(), @elements) : '';
  2335. }
  2336. sub GetCommentForm {
  2337. my ($id, $rev, $comment) = @_;
  2338. if ($CommentsPattern ne '' and $id and $rev ne 'history' and $rev ne 'edit'
  2339. and $id =~ /$CommentsPattern/o and UserCanEdit($id, 0, 1)) {
  2340. return $q->div({-class=>'comment'}, GetFormStart(undef, undef, 'comment'), # protected by questionasker
  2341. $q->p(GetHiddenValue('title', $id),
  2342. GetTextArea('aftertext', $comment ? $comment : $NewComment, 10)), $EditNote,
  2343. $q->p($q->span({-class=>'username'},
  2344. $q->label({-for=>'username'}, T('Username:')), ' ',
  2345. $q->textfield(-name=>'username', -id=>'username',
  2346. -default=>GetParam('username', ''),
  2347. -override=>1, -size=>20, -maxlength=>50)),
  2348. $q->span({-class=>'homepage'},
  2349. $q->label({-for=>'homepage'}, T('Homepage URL:')), ' ',
  2350. $q->textfield(-name=>'homepage', -id=>'homepage',
  2351. -default=>GetParam('homepage', ''),
  2352. -override=>1, -size=>40, -maxlength=>100))),
  2353. $q->p($q->submit(-name=>'Save', -accesskey=>T('s'), -value=>T('Save')), ' ',
  2354. $q->submit(-name=>'Preview', -accesskey=>T('p'), -value=>T('Preview'))),
  2355. $q->end_form());
  2356. }
  2357. return '';
  2358. }
  2359. sub GetFormStart {
  2360. my ($ignore, $method, $class) = @_;
  2361. $method ||= 'post';
  2362. $class ||= 'form';
  2363. return $q->start_multipart_form(-method=>$method, -action=>$FullUrl,
  2364. -accept_charset=>'utf-8', -class=>$class);
  2365. }
  2366. sub GetSearchForm {
  2367. my $form = $q->label({-for=>'search'}, T('Search:')) . ' '
  2368. . $q->textfield(-name=>'search', -id=>'search', -size=>20,
  2369. -accesskey=>T('f')) . ' ';
  2370. if ($ReplaceForm) {
  2371. $form .= $q->label({-for=>'replace'}, T('Replace:')) . ' '
  2372. . $q->textfield(-name=>'replace', -id=>'replace', -size=>20) . ' '
  2373. . $q->checkbox(-name=>'delete', -label=>T('Delete')) . ' ';
  2374. }
  2375. if (%Languages) {
  2376. $form .= $q->label({-for=>'searchlang'}, T('Language:')) . ' '
  2377. . $q->textfield(-name=>'lang', -id=>'searchlang', -size=>10,
  2378. -default=>GetParam('lang', '')) . ' ';
  2379. }
  2380. return GetFormStart(undef, 'get', 'search')
  2381. . $q->p($form . $q->submit('dosearch', T('Go!'))) . $q->end_form;
  2382. }
  2383. sub GetValidatorLink {
  2384. return $q->a({-href => 'http://validator.w3.org/check/referer'}, T('Validate HTML'))
  2385. . ' ' . $q->a({-href=>'http://jigsaw.w3.org/css-validator/check/referer'}, T('Validate CSS'));
  2386. }
  2387. sub GetGotoBar { # ignore $id parameter
  2388. return $q->span({-class=>'gotobar bar'}, (map { GetPageLink($_) }
  2389. @UserGotoBarPages), $UserGotoBar);
  2390. }
  2391. sub PrintHtmlDiff {
  2392. my ($type, $old, $new, $text, $summary) = @_;
  2393. my $intro = T('Last edit');
  2394. my $diff = GetCacheDiff($type == 1 ? 'major' : 'minor');
  2395. # compute old revision if cache is disabled or no cached diff is available
  2396. if (not $old and (not $diff or GetParam('cache', $UseCache) < 1)) {
  2397. if ($type == 1) {
  2398. $old = $Page{lastmajor} - 1;
  2399. ($text, $new, $summary) = GetTextRevision($Page{lastmajor}, 1)
  2400. unless $new or $Page{lastmajor} == $Page{revision};
  2401. } elsif ($new) {
  2402. $old = $new - 1;
  2403. } else {
  2404. $old = $Page{revision} - 1;
  2405. }
  2406. }
  2407. $summary = $Page{summary} if not $summary and not $new;
  2408. $summary = $q->p({-class=>'summary'}, T('Summary:') . ' ' . $summary) if $summary;
  2409. if ($old > 0) { # generate diff if the computed old revision makes sense
  2410. $diff = GetKeptDiff($text, $old);
  2411. $intro = Tss('Difference between revision %1 and %2', $old,
  2412. $new ? Ts('revision %s', $new) : T('current revision'));
  2413. } elsif ($type == 1 and $Page{lastmajor} != $Page{revision}) {
  2414. $intro = Ts('Last major edit (%s)', ScriptLinkDiff(1, $OpenPageName, T('later minor edits'),
  2415. undef, $Page{lastmajor}||1));
  2416. }
  2417. $diff =~ s!<p><strong>(.*?)</strong></p>!'<p><strong>' . T($1) . '</strong></p>'!ge;
  2418. $diff = T('No diff available.') unless $diff;
  2419. print $q->div({-class=>'diff'}, $q->p($q->b($intro)), $summary, $diff);
  2420. }
  2421. sub GetCacheDiff {
  2422. my $type = shift;
  2423. my $diff = $Page{"diff-$type"};
  2424. $diff = $Page{"diff-minor"} if ($diff eq '1'); # if major eq minor diff
  2425. return $diff;
  2426. }
  2427. sub GetKeptDiff {
  2428. my ($new, $revision) = @_;
  2429. $revision = 1 unless $revision;
  2430. my ($old, $rev) = GetTextRevision($revision, 1);
  2431. return '' unless $rev;
  2432. return T("The two revisions are the same.") if $old eq $new;
  2433. return GetDiff($old, $new, $rev);
  2434. }
  2435. sub DoDiff { # Actualy call the diff program
  2436. CreateDir($TempDir);
  2437. my $oldName = "$TempDir/old";
  2438. my $newName = "$TempDir/new";
  2439. RequestLockDir('diff') or return '';
  2440. WriteStringToFile($oldName, $_[0]);
  2441. WriteStringToFile($newName, $_[1]);
  2442. my $diff_out = `diff $oldName $newName`;
  2443. utf8::decode($diff_out); # needs decoding
  2444. $diff_out =~ s/\\ No newline.*\n//g; # Get rid of common complaint.
  2445. ReleaseLockDir('diff');
  2446. # No need to unlink temp files--next diff will just overwrite.
  2447. return $diff_out;
  2448. }
  2449. sub GetDiff {
  2450. my ($old, $new, $revision) = @_;
  2451. my $old_is_file = (TextIsFile($old))[0] || '';
  2452. my $old_is_image = ($old_is_file =~ /^image\//);
  2453. my $new_is_file = TextIsFile($new);
  2454. if ($old_is_file or $new_is_file) {
  2455. return $q->p($q->strong(T('Old revision:')))
  2456. . $q->div({-class=>'old'}, # don't pring new revision, because that's the one that gets shown!
  2457. $q->p($old_is_file ? GetDownloadLink($OpenPageName, $old_is_image, $revision) : $old))
  2458. }
  2459. $old =~ s/[\r\n]+/\n/g;
  2460. $new =~ s/[\r\n]+/\n/g;
  2461. return ImproveDiff(DoDiff($old, $new));
  2462. }
  2463. sub ImproveDiff { # NO NEED TO BE called within a diff lock
  2464. my $diff = QuoteHtml(shift);
  2465. $diff =~ tr/\r//d;
  2466. my @hunks = split (/^(\d+,?\d*[adc]\d+,?\d*\n)/m, $diff);
  2467. my $result = shift (@hunks); # intro
  2468. while ($#hunks > 0) # at least one header and a real hunk
  2469. {
  2470. my $header = shift (@hunks);
  2471. $header =~ s|^(\d+.*c.*)|<p><strong>Changed:</strong></p>| # T('Changed:')
  2472. or $header =~ s|^(\d+.*d.*)|<p><strong>Deleted:</strong></p>| # T('Deleted:')
  2473. or $header =~ s|^(\d+.*a.*)|<p><strong>Added:</strong></p>|; # T('Added:')
  2474. $result .= $header;
  2475. my $chunk = shift (@hunks);
  2476. my ($old, $new) = split (/\n---\n/, $chunk, 2);
  2477. if ($old and $new) {
  2478. ($old, $new) = DiffMarkWords($old, $new);
  2479. $result .= "$old<p><strong>to</strong></p>\n$new"; # T('to')
  2480. } else {
  2481. if (substr($chunk,0,2) eq '&g') {
  2482. $result .= DiffAddPrefix(DiffStripPrefix($chunk), '&gt; ', 'new');
  2483. } else {
  2484. $result .= DiffAddPrefix(DiffStripPrefix($chunk), '&lt; ', 'old');
  2485. }
  2486. }
  2487. }
  2488. return $result;
  2489. }
  2490. sub DiffMarkWords {
  2491. my ($old, $new) = map { DiffStripPrefix($_) } @_;
  2492. my @diffs = grep(/^\d/, split(/\n/, DoDiff(join("\n",split(/\s+|\b/,$old)) . "\n",
  2493. join("\n",split(/\s+|\b/,$new)) . "\n")));
  2494. foreach my $diff (reverse @diffs) { # so that new html tags don't confuse word counts
  2495. my ($start1,$end1,$type,$start2,$end2) = $diff =~ /^(\d+),?(\d*)([adc])(\d+),?(\d*)$/mg;
  2496. if ($type eq 'd' or $type eq 'c') {
  2497. $end1 = $start1 unless $end1;
  2498. $old = DiffHtmlMarkWords($old,$start1,$end1);
  2499. }
  2500. if ($type eq 'a' or $type eq 'c') {
  2501. $end2 = $start2 unless $end2;
  2502. $new = DiffHtmlMarkWords($new,$start2,$end2);
  2503. }
  2504. }
  2505. return (DiffAddPrefix($old, '&lt; ', 'old'),
  2506. DiffAddPrefix($new, '&gt; ', 'new'));
  2507. }
  2508. sub DiffHtmlMarkWords {
  2509. my ($text,$start,$end) = @_;
  2510. my @fragments = split(/(\s+|\b)/, $text);
  2511. splice(@fragments, 2 * ($start - 1), 0, '<strong class="changes">');
  2512. splice(@fragments, 2 * $end, 0, '</strong>');
  2513. my $result = join('', @fragments);
  2514. $result =~ s!&<(/?)strong([^>]*)>(amp|[gl]t);!<$1strong$2>&$3;!g;
  2515. $result =~ s!&(amp|[gl]t)<(/?)strong([^>]*)>;!&$1;<$2strong$3>!g;
  2516. return $result;
  2517. }
  2518. sub DiffStripPrefix {
  2519. my $str = shift;
  2520. $str =~ s/^&[lg]t; //gm;
  2521. return $str;
  2522. }
  2523. sub DiffAddPrefix {
  2524. my ($str, $prefix, $class) = @_;
  2525. my @lines = split(/\n/,$str);
  2526. for my $line (@lines) {
  2527. $line = $prefix . $line;
  2528. }
  2529. return $q->div({-class=>$class},$q->p(join($q->br(), @lines)));
  2530. }
  2531. sub ParseData { # called a lot during search, so it was optimized
  2532. my $data = shift; # by eliminating non-trivial regular expressions
  2533. my %result;
  2534. my $end = index($data, ': ');
  2535. my $key = substr($data, 0, $end);
  2536. my $start = $end += 2; # skip ': '
  2537. while ($end = index($data, "\n", $end) + 1) { # include \n
  2538. next if substr($data, $end, 1) eq "\t"; # continue after \n\t
  2539. $result{$key} = substr($data, $start, $end - $start - 1); # strip last \n
  2540. $start = index($data, ': ', $end); # starting at $end begins the new key
  2541. last if $start == -1;
  2542. $key = substr($data, $end, $start - $end);
  2543. $end = $start += 2; # skip ': '
  2544. }
  2545. $result{$key} .= substr($data, $end, -1); # strip last \n
  2546. foreach (keys %result) {
  2547. $result{$_} =~ s/\n\t/\n/g;
  2548. }
  2549. return %result;
  2550. }
  2551. sub OpenPage { # Sets global variables
  2552. my $id = shift;
  2553. if ($OpenPageName eq $id) {
  2554. return;
  2555. }
  2556. if ($IndexHash{$id}) {
  2557. %Page = ParseData(ReadFileOrDie(GetPageFile($id)));
  2558. } else {
  2559. %Page = ();
  2560. $Page{ts} = $Now;
  2561. $Page{revision} = 0;
  2562. if ($id eq $HomePage
  2563. and (open(F, '<:utf8', $ReadMe)
  2564. or open(F, '<:utf8', 'README'))) {
  2565. local $/ = undef;
  2566. $Page{text} = <F>;
  2567. close F;
  2568. }
  2569. }
  2570. $OpenPageName = $id;
  2571. }
  2572. sub GetTextAtTime { # call with opened page, return $minor if all pages between now and $ts are minor!
  2573. my $ts = shift;
  2574. my $minor = $Page{minor};
  2575. return ($Page{text}, $minor, 0) if $Page{ts} <= $ts; # current page is old enough
  2576. return ($DeletedPage, $minor, 0) if $Page{revision} == 1 and $Page{ts} > $ts; # created after $ts
  2577. my %keep = (); # info may be needed after the loop
  2578. foreach my $revision (GetKeepRevisions($OpenPageName)) {
  2579. %keep = GetKeptRevision($revision);
  2580. $minor = 0 if not $keep{minor} and $keep{ts} >= $ts; # ignore keep{minor} if keep{ts} is too old
  2581. return ($keep{text}, $minor, 0) if $keep{ts} <= $ts;
  2582. }
  2583. return ($DeletedPage, $minor, 0) if $keep{revision} == 1; # then the page was created after $ts!
  2584. return ($keep{text}, $minor, $keep{ts}); # the oldest revision available is not old enough
  2585. }
  2586. sub GetTextRevision {
  2587. my ($revision, $quiet) = @_;
  2588. $revision =~ s/\D//g; # Remove non-numeric chars
  2589. return ($Page{text}, $revision, $Page{summary}) unless $revision and $revision ne $Page{revision};
  2590. my %keep = GetKeptRevision($revision);
  2591. if (not %keep) {
  2592. $Message .= $q->p(Ts('Revision %s not available', $revision)
  2593. . ' (' . T('showing current revision instead') . ')') unless $quiet;
  2594. return ($Page{text}, '', '');
  2595. }
  2596. $Message .= $q->p(Ts('Showing revision %s', $revision)) unless $quiet;
  2597. return ($keep{text}, $revision, $keep{summary});
  2598. }
  2599. sub GetPageContent {
  2600. my $id = shift;
  2601. if ($IndexHash{$id}) {
  2602. my %data = ParseData(ReadFileOrDie(GetPageFile($id)));
  2603. return $data{text};
  2604. }
  2605. return '';
  2606. }
  2607. sub GetKeptRevision { # Call after OpenPage
  2608. my ($status, $data) = ReadFile(GetKeepFile($OpenPageName, (shift)));
  2609. return () unless $status;
  2610. return ParseData($data);
  2611. }
  2612. sub GetPageFile {
  2613. my ($id) = @_;
  2614. return $PageDir . '/' . GetPageDirectory($id) . "/$id.pg";
  2615. }
  2616. sub GetKeepFile {
  2617. my ($id, $revision) = @_; die "No revision for $id" unless $revision; #FIXME
  2618. return $KeepDir . '/' . GetPageDirectory($id) . "/$id/$revision.kp";
  2619. }
  2620. sub GetKeepDir {
  2621. my $id = shift; die 'No id' unless $id; #FIXME
  2622. return $KeepDir . '/' . GetPageDirectory($id) . '/' . $id;
  2623. }
  2624. sub GetKeepFiles {
  2625. return bsd_glob(GetKeepDir(shift) . '/*.kp'); # files such as 1.kp, 2.kp, etc.
  2626. }
  2627. sub GetKeepRevisions {
  2628. return sort {$b <=> $a} map { m/([0-9]+)\.kp$/; $1; } GetKeepFiles(shift);
  2629. }
  2630. sub GetPageDirectory {
  2631. my $id = shift;
  2632. if ($id =~ /^([a-zA-Z])/) {
  2633. return uc($1);
  2634. }
  2635. return 'other';
  2636. }
  2637. # Always call SavePage within a lock.
  2638. sub SavePage { # updating the cache will not change timestamp and revision!
  2639. ReportError(T('Cannot save a nameless page.'), '400 BAD REQUEST', 1) unless $OpenPageName;
  2640. ReportError(T('Cannot save a page without revision.'), '400 BAD REQUEST', 1) unless $Page{revision};
  2641. CreatePageDir($PageDir, $OpenPageName);
  2642. WriteStringToFile(GetPageFile($OpenPageName), EncodePage(%Page));
  2643. }
  2644. sub SaveKeepFile {
  2645. return if ($Page{revision} < 1); # Don't keep 'empty' revision
  2646. delete $Page{blocks}; # delete some info from the page
  2647. delete $Page{flags};
  2648. delete $Page{'diff-major'};
  2649. delete $Page{'diff-minor'};
  2650. $Page{'keep-ts'} = $Now; # expire only $KeepDays from $Now!
  2651. CreateKeepDir($KeepDir, $OpenPageName);
  2652. WriteStringToFile(GetKeepFile($OpenPageName, $Page{revision}), EncodePage(%Page));
  2653. }
  2654. sub EncodePage {
  2655. my @data = @_;
  2656. my $result = '';
  2657. $result .= (shift @data) . ': ' . EscapeNewlines(shift @data) . "\n" while (@data);
  2658. return $result;
  2659. }
  2660. sub EscapeNewlines {
  2661. $_[0] =~ s/\n/\n\t/g; # modify original instead of copying
  2662. return $_[0];
  2663. }
  2664. sub ExpireKeepFiles { # call with opened page
  2665. return unless $KeepDays;
  2666. my $expirets = $Now - ($KeepDays * 86400); # 24*60*60
  2667. foreach my $revision (GetKeepRevisions($OpenPageName)) {
  2668. my %keep = GetKeptRevision($revision);
  2669. next if $keep{'keep-ts'} >= $expirets;
  2670. next if $KeepMajor and $keep{revision} == $Page{lastmajor};
  2671. unlink GetKeepFile($OpenPageName, $revision);
  2672. }
  2673. }
  2674. sub ReadFile {
  2675. my $file = shift;
  2676. utf8::encode($file); # filenames are bytes!
  2677. if (open(IN, '<:utf8', $file)) {
  2678. local $/ = undef; # Read complete files
  2679. my $data=<IN>;
  2680. close IN;
  2681. return (1, $data);
  2682. }
  2683. return (0, '');
  2684. }
  2685. sub ReadFileOrDie {
  2686. my ($file) = @_;
  2687. my ($status, $data);
  2688. ($status, $data) = ReadFile($file);
  2689. if (!$status) {
  2690. ReportError(Ts('Cannot open %s', $file) . ": $!", '500 INTERNAL SERVER ERROR');
  2691. }
  2692. return $data;
  2693. }
  2694. sub WriteStringToFile {
  2695. my ($file, $string) = @_;
  2696. utf8::encode($file);
  2697. open(OUT, '>:encoding(UTF-8)', $file)
  2698. or ReportError(Ts('Cannot write %s', $file) . ": $!", '500 INTERNAL SERVER ERROR');
  2699. print OUT $string;
  2700. close(OUT);
  2701. }
  2702. sub AppendStringToFile {
  2703. my ($file, $string) = @_;
  2704. utf8::encode($file);
  2705. open(OUT, '>>:encoding(UTF-8)', $file)
  2706. or ReportError(Ts('Cannot write %s', $file) . ": $!", '500 INTERNAL SERVER ERROR');
  2707. print OUT $string;
  2708. close(OUT);
  2709. }
  2710. sub CreateDir {
  2711. my ($newdir) = @_;
  2712. utf8::encode($newdir);
  2713. return if -d $newdir;
  2714. mkdir($newdir, 0775)
  2715. or ReportError(Ts('Cannot create %s', $newdir) . ": $!", '500 INTERNAL SERVER ERROR');
  2716. }
  2717. sub CreatePageDir {
  2718. my ($dir, $id) = @_;
  2719. CreateDir($dir);
  2720. CreateDir($dir . '/' . GetPageDirectory($id));
  2721. }
  2722. sub CreateKeepDir {
  2723. my ($dir, $id) = @_;
  2724. CreatePageDir($dir, $id);
  2725. CreateDir($dir . '/' . GetPageDirectory($id) . '/' . $id);
  2726. }
  2727. sub GetLockedPageFile {
  2728. my $id = shift;
  2729. return $PageDir . '/' . GetPageDirectory($id) . "/$id.lck";
  2730. }
  2731. sub RequestLockDir {
  2732. my ($name, $tries, $wait, $error, $retried) = @_;
  2733. $tries = 4 unless $tries;
  2734. $wait = 2 unless $wait;
  2735. CreateDir($TempDir);
  2736. my $lock = $LockDir . $name;
  2737. my $n = 0;
  2738. while (mkdir($lock, 0555) == 0) {
  2739. if ($n++ >= $tries) {
  2740. my $ts = (stat($lock))[9];
  2741. if ($Now - $ts > $LockExpiration and $LockExpires{$name}
  2742. and not $retried) {
  2743. ReleaseLockDir($name); # try to expire lock (no checking)
  2744. return 1 if RequestLockDir($name, undef, undef, undef, 1);
  2745. }
  2746. return 0 unless $error;
  2747. ReportError(Ts('Could not get %s lock', $name) . ": $!. "
  2748. . Ts('The lock was created %s.', CalcTimeSince($Now - $ts))
  2749. . ($retried ? ' ' . T('Maybe the user running this script is no longer allowed to remove the lock directory?') : ''),
  2750. '503 SERVICE UNAVAILABLE');
  2751. }
  2752. sleep($wait);
  2753. }
  2754. $Locks{$name} = 1;
  2755. return 1;
  2756. }
  2757. sub ReleaseLockDir {
  2758. my $name = shift; # We don't check whether we succeeded.
  2759. rmdir($LockDir . $name); # Before fixing, make sure we only call this
  2760. delete $Locks{$name}; # when we know the lock exists.
  2761. }
  2762. sub RequestLockOrError {
  2763. # 10 tries, 3 second wait, die on error
  2764. return RequestLockDir('main', 10, 3, 1);
  2765. }
  2766. sub ReleaseLock {
  2767. ReleaseLockDir('main');
  2768. }
  2769. sub ForceReleaseLock {
  2770. my $pattern = shift;
  2771. my $forced;
  2772. foreach my $name (bsd_glob $pattern) {
  2773. # First try to obtain lock (in case of normal edit lock)
  2774. $forced = 1 if !RequestLockDir($name, 5, 3, 0);
  2775. ReleaseLockDir($name); # Release the lock, even if we didn't get it.
  2776. }
  2777. return $forced;
  2778. }
  2779. sub DoUnlock {
  2780. my $message = '';
  2781. print GetHeader('', T('Unlock Wiki'), undef, 'nocache');
  2782. print $q->p(T('This operation may take several seconds...'));
  2783. for my $lock (@KnownLocks) {
  2784. if (ForceReleaseLock($lock)) {
  2785. $message .= $q->p(Ts('Forced unlock of %s lock.', $lock));
  2786. }
  2787. }
  2788. if ($message) {
  2789. print $message;
  2790. } else {
  2791. print $q->p(T('No unlock required.'));
  2792. }
  2793. PrintFooter();
  2794. }
  2795. sub CalcDay {
  2796. my ($sec, $min, $hour, $mday, $mon, $year) = gmtime(shift);
  2797. return sprintf('%4d-%02d-%02d', $year+1900, $mon+1, $mday);
  2798. }
  2799. sub CalcTime {
  2800. my ($sec, $min, $hour, $mday, $mon, $year) = gmtime(shift);
  2801. return sprintf('%02d:%02d UTC', $hour, $min);
  2802. }
  2803. sub CalcTimeSince {
  2804. my $total = shift;
  2805. if ($total >= 7200) {
  2806. return Ts('%s hours ago',int($total/3600));
  2807. } elsif ($total >= 3600) {
  2808. return T('1 hour ago');
  2809. } elsif ($total >= 120) {
  2810. return Ts('%s minutes ago',int($total/60));
  2811. } elsif ($total >= 60) {
  2812. return T('1 minute ago');
  2813. } elsif ($total >= 2) {
  2814. return Ts('%s seconds ago',int($total));
  2815. } elsif ($total == 1) {
  2816. return T('1 second ago');
  2817. } else {
  2818. return T('just now');
  2819. }
  2820. }
  2821. sub TimeToText {
  2822. my $t = shift;
  2823. return CalcDay($t) . ' ' . CalcTime($t);
  2824. }
  2825. sub TimeToW3 { # Complete date plus hours and minutes: YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00)
  2826. my ($sec, $min, $hour, $mday, $mon, $year) = gmtime(shift); # use special UTC designator ("Z")
  2827. return sprintf('%4d-%02d-%02dT%02d:%02dZ', $year+1900, $mon+1, $mday, $hour, $min);
  2828. }
  2829. sub TimeToRFC822 {
  2830. my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime(shift); # Sat, 07 Sep 2002 00:00:01 GMT
  2831. return sprintf("%s, %02d %s %04d %02d:%02d:%02d GMT", qw(Sun Mon Tue Wed Thu Fri Sat)[$wday], $mday,
  2832. qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)[$mon], $year+1900, $hour, $min, $sec);
  2833. }
  2834. sub GetHiddenValue {
  2835. my ($name, $value) = @_;
  2836. $q->param($name, $value);
  2837. return $q->input({-type=>"hidden", -name=>$name, -value=>$value});
  2838. }
  2839. sub GetRemoteHost { # when testing, these variables are undefined.
  2840. my $rhost = $ENV{REMOTE_HOST}; # tests are written to avoid -w warnings.
  2841. if (not $rhost and $UseLookup and $ENV{REMOTE_ADDR}) {
  2842. # Catch errors (including bad input) without aborting the script
  2843. eval 'use Socket; my $iaddr = inet_aton($ENV{REMOTE_ADDR});'
  2844. . '$rhost = gethostbyaddr($iaddr, AF_INET) if $iaddr;';
  2845. }
  2846. if (not $rhost) {
  2847. $rhost = $ENV{REMOTE_ADDR};
  2848. }
  2849. return $rhost;
  2850. }
  2851. sub FreeToNormal { # trim all spaces and convert them to underlines
  2852. my $id = shift;
  2853. return '' unless $id;
  2854. $id =~ s/ /_/g;
  2855. $id =~ s/__+/_/g;
  2856. $id =~ s/^_//;
  2857. $id =~ s/_$//;
  2858. return UnquoteHtml($id);
  2859. }
  2860. sub ItemName {
  2861. my $id = shift; # id
  2862. return NormalToFree($id) unless GetParam('short', 1) and $RssStrip;
  2863. my $comment = $id =~ s/^($CommentsPrefix)//o; # strip first so that ^ works
  2864. $id =~ s/^$RssStrip//o;
  2865. $id = $CommentsPrefix . $id if $comment;
  2866. return NormalToFree($id);
  2867. }
  2868. sub NormalToFree { # returns HTML quoted title with spaces
  2869. my $title = shift;
  2870. $title =~ s/_/ /g;
  2871. return QuoteHtml($title);
  2872. }
  2873. sub UnWiki {
  2874. my $str = shift;
  2875. return $str unless $WikiLinks and $str =~ /^$LinkPattern$/;
  2876. $str =~ s/([[:lower:]])([[:upper:]])/$1 $2/g;
  2877. return $str;
  2878. }
  2879. sub DoEdit {
  2880. my ($id, $newText, $preview) = @_;
  2881. ValidIdOrDie($id);
  2882. my $upload = GetParam('upload', undef);
  2883. if (!UserCanEdit($id, 1)) {
  2884. my $rule = UserIsBanned();
  2885. if ($rule) {
  2886. ReportError(T('Edit Denied'), '403 FORBIDDEN', undef,
  2887. $q->p(T('Editing not allowed: user, ip, or network is blocked.')),
  2888. $q->p(T('Contact the wiki administrator for more information.')),
  2889. $q->p(Ts('The rule %s matched for you.', $rule) . ' '
  2890. . Ts('See %s for more information.', GetPageLink($BannedHosts))));
  2891. } else {
  2892. ReportError(T('Edit Denied'), '403 FORBIDDEN', undef,
  2893. $q->p(Ts('Editing not allowed: %s is read-only.', NormalToFree($id))));
  2894. }
  2895. } elsif ($upload and not $UploadAllowed and not UserIsAdmin()) {
  2896. ReportError(T('Only administrators can upload files.'), '403 FORBIDDEN');
  2897. }
  2898. OpenPage($id);
  2899. my ($text, $revision) = GetTextRevision(GetParam('revision', ''), 1); # maybe revision reset!
  2900. my $oldText = $preview ? $newText : $text;
  2901. my $isFile = TextIsFile($oldText);
  2902. $upload = $isFile if not defined $upload;
  2903. if ($upload and not $UploadAllowed and not UserIsAdmin()) {
  2904. ReportError(T('Only administrators can upload files.'), '403 FORBIDDEN');
  2905. }
  2906. if ($upload) { # shortcut lots of code
  2907. $revision = '';
  2908. $preview = 0;
  2909. } elsif ($isFile and not $upload) {
  2910. $oldText = '';
  2911. }
  2912. my $header;
  2913. if ($revision and not $upload) {
  2914. $header = Ts('Editing revision %s of', $revision) . ' ' . NormalToFree($id);
  2915. } else {
  2916. $header = Ts('Editing %s', NormalToFree($id));
  2917. }
  2918. print GetHeader('', $header), $q->start_div({-class=>'content edit'});
  2919. if ($preview and not $upload) {
  2920. print $q->start_div({-class=>'preview'});
  2921. print $q->h2(T('Preview:'));
  2922. PrintWikiToHTML($oldText); # no caching, current revision, unlocked
  2923. print $q->hr(), $q->h2(T('Preview only, not yet saved')), $q->end_div();
  2924. }
  2925. if ($revision) {
  2926. print $q->strong(Ts('Editing old revision %s.', $revision) . ' '
  2927. . T('Saving this page will replace the latest revision with this text.'))
  2928. }
  2929. print GetEditForm($id, $upload, $oldText, $revision), $q->end_div();
  2930. PrintFooter($id, 'edit');
  2931. }
  2932. sub GetEditForm {
  2933. my ($page_name, $upload, $oldText, $revision) = @_;
  2934. my $html = GetFormStart(undef, undef, $upload ? 'edit upload' : 'edit text') # protected by questionasker
  2935. .$q->p(GetHiddenValue("title", $page_name), ($revision ? GetHiddenValue('revision', $revision) : ''),
  2936. GetHiddenValue('oldtime', $Page{ts}), ($upload ? GetUpload() : GetTextArea('text', $oldText)));
  2937. my $summary = UnquoteHtml(GetParam('summary', ''))
  2938. || ($Now - $Page{ts} < ($SummaryHours * 3600) ? $Page{summary} : '');
  2939. $html .= $q->p(T('Summary:').$q->br().GetTextArea('summary', $summary, 2))
  2940. .$q->p($q->checkbox(-name=>'recent_edit', -checked=>(GetParam('recent_edit', '') eq 'on'),
  2941. -label=>T('This change is a minor edit.')));
  2942. $html .= T($EditNote) if $EditNote; # Allow translation
  2943. my $username = GetParam('username', '');
  2944. $html .= $q->p($q->label({-for=>'username'}, T('Username:')).' '
  2945. .$q->textfield(-name=>'username', -id=>'username', -default=>$username,
  2946. -override=>1, -size=>20, -maxlength=>50))
  2947. .$q->p($q->submit(-name=>'Save', -accesskey=>T('s'), -value=>T('Save')),
  2948. ($upload ? '' : ' ' . $q->submit(-name=>'Preview', -accesskey=>T('p'), -value=>T('Preview'))).
  2949. ' '.$q->submit(-name=>'Cancel', -value=>T('Cancel')));
  2950. if ($upload) {
  2951. $html .= $q->p(ScriptLink('action=edit;upload=0;id='.UrlEncode($page_name), T('Replace this file with text'), 'upload'));
  2952. }
  2953. elsif ($UploadAllowed or UserIsAdmin()) {
  2954. $html .= $q->p(ScriptLink('action=edit;upload=1;id='.UrlEncode($page_name), T('Replace this text with a file'), 'upload'));
  2955. }
  2956. $html .= $q->end_form();
  2957. return $html;
  2958. }
  2959. sub GetTextArea {
  2960. my ($name, $text, $rows) = @_;
  2961. return $q->textarea(-id=>$name, -name=>$name, -default=>$text, -rows=>$rows||25, -columns=>78, -override=>1);
  2962. }
  2963. sub GetUpload {
  2964. return T('File to upload: ') . $q->filefield(-name=>'file', -size=>50, -maxlength=>100);
  2965. }
  2966. sub DoDownload {
  2967. my $id = shift;
  2968. OpenPage($id) if ValidIdOrDie($id);
  2969. print $q->header(-status=>'304 NOT MODIFIED') and return if FileFresh(); # FileFresh needs an OpenPage!
  2970. my ($text, $revision) = GetTextRevision(GetParam('revision', '')); # maybe revision reset!
  2971. my $ts = $Page{ts};
  2972. if (my ($type, $encoding) = TextIsFile($text)) {
  2973. my ($data) = $text =~ /^[^\n]*\n(.*)/s;
  2974. my %allowed = map {$_ => 1} @UploadTypes;
  2975. ReportError(Ts('Files of type %s are not allowed.', $type), '415 UNSUPPORTED MEDIA TYPE')
  2976. if @UploadTypes and not $allowed{$type};
  2977. print GetHttpHeader($type, $ts, undef, $encoding);
  2978. require MIME::Base64;
  2979. binmode(STDOUT, ":pop:raw"); # need to pop utf8 for Windows users!?
  2980. print MIME::Base64::decode($data);
  2981. } else {
  2982. print GetHttpHeader('text/plain', $ts);
  2983. print $text;
  2984. }
  2985. }
  2986. sub DoPassword {
  2987. print GetHeader('',T('Password')), $q->start_div({-class=>'content password'});
  2988. 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.'));
  2989. if (UserIsAdmin()) {
  2990. print $q->p(T('You are currently an administrator on this site.'));
  2991. } elsif (UserIsEditor()) {
  2992. print $q->p(T('You are currently an editor on this site.'));
  2993. } else {
  2994. print $q->p(T('You are a normal user on this site.'));
  2995. if ($AdminPass or $EditPass) {
  2996. print $q->p(T('Your password does not match any of the administrator or editor passwords.'));
  2997. }
  2998. }
  2999. if ($AdminPass or $EditPass) {
  3000. print GetFormStart(undef, undef, 'password'),
  3001. $q->p(GetHiddenValue('action', 'password'), T('Password:'), ' ',
  3002. $q->password_field(-name=>'pwd', -size=>20, -maxlength=>50),
  3003. $q->submit(-name=>'Save', -accesskey=>T('s'), -value=>T('Save'))), $q->end_form;
  3004. } else {
  3005. print $q->p(T('This site does not use admin or editor passwords.'));
  3006. }
  3007. print $q->end_div();
  3008. PrintFooter();
  3009. }
  3010. sub UserIsEditorOrError {
  3011. UserIsEditor()
  3012. or ReportError(T('This operation is restricted to site editors only...'), '403 FORBIDDEN');
  3013. return 1;
  3014. }
  3015. sub UserIsAdminOrError {
  3016. UserIsAdmin()
  3017. or ReportError(T('This operation is restricted to administrators only...'), '403 FORBIDDEN');
  3018. return 1;
  3019. }
  3020. sub UserCanEdit {
  3021. my ($id, $editing, $comment) = @_;
  3022. return 0 if $id eq 'SampleUndefinedPage' or $id eq T('SampleUndefinedPage')
  3023. or $id eq 'Sample_Undefined_Page' or $id eq T('Sample_Undefined_Page');
  3024. return 1 if UserIsAdmin();
  3025. return 0 if $id ne '' and -f GetLockedPageFile($id);
  3026. return 0 if $LockOnCreation{$id} and not -f GetPageFile($id); # new page
  3027. return 1 if UserIsEditor();
  3028. return 0 if !$EditAllowed or -f $NoEditFile;
  3029. return 0 if $editing and UserIsBanned(); # this call is more expensive
  3030. return 0 if $EditAllowed >= 2 and (not $CommentsPattern or $id !~ /$CommentsPattern/o);
  3031. return 1 if $EditAllowed >= 3 and ($comment or (GetParam('aftertext', '') and not GetParam('text', '')));
  3032. return 0 if $EditAllowed >= 3;
  3033. return 1;
  3034. }
  3035. sub UserIsBanned {
  3036. return 0 if GetParam('action', '') eq 'password'; # login is always ok
  3037. my ($host, $ip);
  3038. $ip = $ENV{'REMOTE_ADDR'};
  3039. $host = GetRemoteHost();
  3040. foreach (split(/\n/, GetPageContent($BannedHosts))) {
  3041. if (/^\s*([^#]\S+)/) { # all lines except empty lines and comments, trim whitespace
  3042. my $regexp = $1;
  3043. return $regexp if ($ip =~ /$regexp/i);
  3044. return $regexp if ($host =~ /$regexp/i);
  3045. }
  3046. }
  3047. return 0;
  3048. }
  3049. sub UserIsAdmin {
  3050. return UserHasPassword(GetParam('pwd', ''), $AdminPass);
  3051. }
  3052. sub UserIsEditor {
  3053. return 1 if UserIsAdmin(); # Admin includes editor
  3054. return UserHasPassword(GetParam('pwd', ''), $EditPass);
  3055. }
  3056. sub UserHasPassword {
  3057. my ($pwd, $pass) = @_;
  3058. return 0 if not $pass;
  3059. if ($PassHashFunction ne '') {
  3060. no strict 'refs';
  3061. $pwd = &$PassHashFunction($pwd . $PassSalt);
  3062. }
  3063. foreach (split(/\s+/, $pass)) {
  3064. return 1 if $pwd eq $_;
  3065. }
  3066. return 0;
  3067. }
  3068. sub BannedContent {
  3069. my $str = shift;
  3070. my @urls = $str =~ /$FullUrlPattern/go;
  3071. foreach (split(/\n/, GetPageContent($BannedContent))) {
  3072. next unless m/^\s*([^#]+?)\s*(#\s*(\d\d\d\d-\d\d-\d\d\s*)?(.*))?$/;
  3073. my ($regexp, $comment, $re) = ($1, $4, undef);
  3074. foreach my $url (@urls) {
  3075. eval { $re = qr/$regexp/i; };
  3076. if (defined($re) && $url =~ $re) {
  3077. return Tss('Rule "%1" matched "%2" on this page.', $regexp, $url) . ' '
  3078. . ($comment ? Ts('Reason: %s.', $comment) : T('Reason unknown.')) . ' '
  3079. . Ts('See %s for more information.', GetPageLink($BannedContent));
  3080. }
  3081. }
  3082. }
  3083. return 0;
  3084. }
  3085. sub DoIndex {
  3086. my $raw = GetParam('raw', 0);
  3087. my $match = GetParam('match', '');
  3088. my @pages = ();
  3089. my @menu = ($q->label({-for=>'indexmatch'}, T('Filter:')) . ' '
  3090. . $q->textfield(-name=>'match', -id=>'indexmatch', -size=>20));
  3091. foreach my $data (@IndexOptions) {
  3092. my ($option, $text, $default, $sub) = @$data;
  3093. my $value = GetParam($option, $default); # HTML checkbox warning!
  3094. $value = 0 if GetParam('manual', 0) and $value ne 'on';
  3095. push(@pages, &$sub) if $value;
  3096. push(@menu, $q->checkbox(-name=>$option, -checked=>$value, -label=>$text));
  3097. }
  3098. @pages = grep /$match/i, @pages if $match;
  3099. @pages = sort @pages;
  3100. if ($raw) {
  3101. print GetHttpHeader('text/plain'); # and ignore @menu
  3102. } else {
  3103. print GetHeader('', T('Index of all pages'));
  3104. push(@menu, GetHiddenValue('manual', 1) . $q->submit(-value=>T('Go!')));
  3105. push(@menu, $q->b(Ts('(for %s)', GetParam('lang', '')))) if GetParam('lang', '');
  3106. print $q->start_div({-class=>'content index'}),
  3107. GetFormStart(undef, 'get', 'index'), GetHiddenValue('action', 'index'),
  3108. $q->p(join($q->br(), @menu)), $q->end_form(),
  3109. $q->h2(Ts('%s pages found.', ($#pages + 1))), $q->start_p();
  3110. }
  3111. foreach (@pages) {
  3112. PrintPage($_);
  3113. }
  3114. print $q->end_p(), $q->end_div() unless $raw;
  3115. PrintFooter() unless $raw;
  3116. }
  3117. sub PrintPage {
  3118. my $id = shift;
  3119. my $lang = GetParam('lang', 0);
  3120. if ($lang) {
  3121. OpenPage($id);
  3122. my @languages = split(/,/, $Page{languages});
  3123. next if (@languages and not grep(/$lang/, @languages));
  3124. }
  3125. if (GetParam('raw', 0)) {
  3126. if (GetParam('search', '') and GetParam('context',1)) {
  3127. print "title: $id\n\n"; # for near links without full search
  3128. } else {
  3129. print $id, "\n";
  3130. }
  3131. } else {
  3132. print GetPageOrEditLink($id, NormalToFree($id)), $q->br();
  3133. }
  3134. }
  3135. sub AllPagesList {
  3136. my $refresh = GetParam('refresh', 0);
  3137. return @IndexList if @IndexList and not $refresh;
  3138. SetParam('refresh', 0) if $refresh;
  3139. if (not $refresh and -f $IndexFile) {
  3140. my ($status, $rawIndex) = ReadFile($IndexFile); # not fatal
  3141. if ($status) {
  3142. %IndexHash = split(/ /, $rawIndex);
  3143. @IndexList = sort(keys %IndexHash);
  3144. return @IndexList;
  3145. }
  3146. # If open fails just refresh the index
  3147. }
  3148. @IndexList = ();
  3149. %IndexHash = ();
  3150. # If file exists and cannot be changed, error!
  3151. my $locked = RequestLockDir('index', undef, undef, -f $IndexFile);
  3152. foreach (bsd_glob("$PageDir/*/*.pg"), bsd_glob("$PageDir/*/.*.pg")) {
  3153. next unless m|/.*/(.+)\.pg$|;
  3154. my $id = $1;
  3155. utf8::decode($id);
  3156. push(@IndexList, $id);
  3157. $IndexHash{$id} = 1;
  3158. }
  3159. WriteStringToFile($IndexFile, join(' ', %IndexHash)) if $locked;
  3160. ReleaseLockDir('index') if $locked;
  3161. return @IndexList;
  3162. }
  3163. sub DoSearch {
  3164. my $string = shift;
  3165. return DoIndex() if $string eq '';
  3166. eval { qr/$string/ }
  3167. or $@ and ReportError(Ts('Malformed regular expression in %s', $string),
  3168. '400 BAD REQUEST');
  3169. my $replacement = GetParam('replace',undef);
  3170. my $raw = GetParam('raw','');
  3171. my @results;
  3172. if ($replacement or GetParam('delete', 0)) {
  3173. return unless UserIsAdminOrError();
  3174. print GetHeader('', Ts('Replaced: %s', $string . " &#x2192; " . $replacement)),
  3175. $q->start_div({-class=>'content replacement'});
  3176. @results = Replace($string,$replacement);
  3177. foreach (@results) {
  3178. PrintSearchResult($_, SearchRegexp($replacement||$string));
  3179. }
  3180. } else {
  3181. if ($raw) {
  3182. print GetHttpHeader('text/plain');
  3183. print RcTextItem('title', Ts('Search for: %s', $string)), RcTextItem('date', TimeToText($Now)),
  3184. RcTextItem('link', $q->url(-path_info=>1, -query=>1)), "\n" if GetParam('context', 1);
  3185. } else {
  3186. print GetHeader('', Ts('Search for: %s', $string)), $q->start_div({-class=>'content search'});
  3187. $ReplaceForm = UserIsAdmin();
  3188. print $q->p({-class=>'links'}, SearchMenu($string));
  3189. }
  3190. @results = SearchTitleAndBody($string, \&PrintSearchResult, SearchRegexp($string));
  3191. }
  3192. print SearchResultCount($#results + 1), $q->end_div() unless $raw;
  3193. PrintFooter() unless $raw;
  3194. }
  3195. sub SearchMenu {
  3196. return ScriptLink('action=rc;rcfilteronly=' . UrlEncode(shift),
  3197. T('View changes for these pages'));
  3198. }
  3199. sub SearchResultCount { $q->p({-class=>'result'}, Ts('%s pages found.', (shift))); }
  3200. sub PageIsUploadedFile {
  3201. my $id = shift;
  3202. return undef if $OpenPageName eq $id;
  3203. if ($IndexHash{$id}) {
  3204. my $file = GetPageFile($id);
  3205. utf8::encode($file); # filenames are bytes!
  3206. open(FILE, '<:utf8', $file)
  3207. or ReportError(Ts('Cannot open %s', $file) . ": $!", '500 INTERNAL SERVER ERROR');
  3208. while (defined($_ = <FILE>) and $_ !~ /^text: /) {
  3209. } # read lines until we get to the text key
  3210. close FILE;
  3211. return TextIsFile(substr($_,6)); # pass "#FILE image/png\n" to the test
  3212. }
  3213. }
  3214. sub SearchTitleAndBody { # expects search string to be HTML quoted and will unquote it
  3215. my ($string, $func, @args) = @_;
  3216. $string = UnquoteHtml($string);
  3217. my @found;
  3218. my $lang = GetParam('lang', '');
  3219. foreach my $id (GrepFiltered($string, AllPagesList())) {
  3220. my $name = NormalToFree($id);
  3221. my ($text) = PageIsUploadedFile($id); # set to mime-type if this is an uploaded file
  3222. if (not $text) { # not uploaded file, therefore allow searching of page body
  3223. local ($OpenPageName, %Page); # this is local!
  3224. OpenPage($id); # this opens a page twice if it is not uploaded, but that's ok
  3225. if ($lang) {
  3226. my @languages = split(/,/, $Page{languages});
  3227. next if (@languages and not grep(/$lang/, @languages));
  3228. }
  3229. $text = $Page{text};
  3230. }
  3231. if (SearchString($string, $name . "\n" . $text)) { # the real search code
  3232. push(@found, $id);
  3233. &$func($id, @args) if $func;
  3234. }
  3235. }
  3236. return @found;
  3237. }
  3238. sub GrepFiltered { # grep is so much faster!!
  3239. my ($string, @pages) = @_;
  3240. my $regexp = SearchRegexp($string);
  3241. return @pages unless GetParam('grep', $UseGrep) and $regexp;
  3242. my @result = grep(/$regexp/i, @pages);
  3243. my %found = map {$_ => 1} @result;
  3244. $regexp =~ s/\\n(\)*)$/\$$1/g; # sometimes \n can be replaced with $
  3245. $regexp =~ s/([?+{|()])/\\$1/g; # basic regular expressions from man grep
  3246. # if we know of any remaining grep incompatibilities we should
  3247. # return @pages here!
  3248. $regexp = quotemeta($regexp);
  3249. open(F, '-|:encoding(UTF-8)', "grep -rli $regexp '$PageDir' 2>/dev/null");
  3250. while (<F>) {
  3251. push(@result, $1) if m/.*\/(.*)\.pg/ and not $found{$1};
  3252. }
  3253. close(F);
  3254. return sort @result;
  3255. }
  3256. sub SearchString {
  3257. my ($string, $data) = @_;
  3258. my @strings = grep /./, $string =~ /\"([^\"]+)\"|(\S+)/g; # skip null entries
  3259. foreach my $str (@strings) {
  3260. return 0 unless ($data =~ /$str/i);
  3261. }
  3262. return 1;
  3263. }
  3264. sub SearchRegexp {
  3265. my $regexp = join '|', map { index($_,'|') == -1 ? $_ : "($_)" }
  3266. grep /./, shift =~ /\"([^\"]+)\"|(\S+)/g; # this acts as OR
  3267. $regexp =~ s/\\s/[[:space:]]/g;
  3268. return $regexp;
  3269. }
  3270. sub PrintSearchResult {
  3271. my ($name, $regex) = @_;
  3272. return PrintPage($name) if not GetParam('context',1);
  3273. my $raw = GetParam('raw', 0);
  3274. OpenPage($name); # should be open already, just making sure!
  3275. my $text = $Page{text};
  3276. my ($type) = TextIsFile($text); # MIME type if an uploaded file
  3277. my %entry;
  3278. # get the page, filter it, remove all tags
  3279. $text =~ s/$FS//go; # Remove separators (paranoia)
  3280. $text =~ s/[\s]+/ /g; # Shrink whitespace
  3281. $text =~ s/([-_=\\*\\.]){10,}/$1$1$1$1$1/g ; # e.g. shrink "----------"
  3282. $entry{title} = $name;
  3283. $entry{description} = $type || SearchExtract(QuoteHtml($text), $regex);
  3284. $entry{size} = int((length($text)/1024)+1) . 'K';
  3285. $entry{'last-modified'} = TimeToText($Page{ts});
  3286. $entry{username} = $Page{username};
  3287. $entry{host} = $Page{host};
  3288. PrintSearchResultEntry(\%entry, $regex);
  3289. }
  3290. sub PrintSearchResultEntry {
  3291. my %entry = %{(shift)}; # get value from reference
  3292. my $regex = shift;
  3293. if (GetParam('raw', 0)) {
  3294. $entry{generator} = GetAuthor($entry{host}, $entry{username});
  3295. foreach my $key (qw(title description size last-modified generator username host)) {
  3296. print RcTextItem($key, $entry{$key});
  3297. }
  3298. print RcTextItem('link', "$ScriptName?$entry{title}"), "\n";
  3299. } else {
  3300. my $author = GetAuthorLink($entry{host}, $entry{username});
  3301. $author = $entry{generator} unless $author;
  3302. my $id = $entry{title};
  3303. my ($class, $resolved, $title, $exists) = ResolveId($id);
  3304. my $text = NormalToFree($id);
  3305. my $result = $q->span({-class=>'result'}, ScriptLink(UrlEncode($resolved), $text, $class, undef, $title));
  3306. my $description = $entry{description};
  3307. $description = $q->br() . SearchHighlight($description, $regex) if $description;
  3308. my $info = $entry{size};
  3309. $info .= ' - ' if $info;
  3310. $info .= T('last updated') . ' ' . $entry{'last-modified'} if $entry{'last-modified'};
  3311. $info .= ' ' . T('by') . ' ' . $author if $author;
  3312. $info = $q->br() . $q->span({-class=>'info'}, $info) if $info;
  3313. print $q->p($result, $description, $info);
  3314. }
  3315. }
  3316. sub SearchHighlight {
  3317. my ($data, $regex) = @_;
  3318. $data =~ s/($regex)/<strong>$1<\/strong>/gi;
  3319. return $data;
  3320. }
  3321. sub SearchExtract {
  3322. my ($data, $string) = @_;
  3323. my ($snippetlen, $maxsnippets) = (100, 4) ; # these seem nice.
  3324. # show a snippet from the beginning of the document
  3325. my $j = index($data, ' ', $snippetlen); # end on word boundary
  3326. my $t = substr($data, 0, $j);
  3327. my $result = $t . ' . . .';
  3328. $data = substr($data, $j); # to avoid rematching
  3329. my $jsnippet = 0 ;
  3330. while ($jsnippet < $maxsnippets && $data =~ m/($string)/i) {
  3331. $jsnippet++;
  3332. if (($j = index($data, $1)) > -1 ) {
  3333. # get substr containing (start of) match, ending on word boundaries
  3334. my $start = index($data, ' ', $j-($snippetlen/2));
  3335. $start = 0 if ($start == -1);
  3336. my $end = index($data, ' ', $j+($snippetlen/2));
  3337. $end = length($data ) if ($end == -1);
  3338. $t = substr($data, $start, $end-$start);
  3339. $result .= $t . ' . . .';
  3340. # truncate text to avoid rematching the same string.
  3341. $data = substr($data, $end);
  3342. }
  3343. }
  3344. return $result;
  3345. }
  3346. sub Replace {
  3347. my ($from, $to) = @_;
  3348. my $lang = GetParam('lang', '');
  3349. my @result;
  3350. RequestLockOrError(); # fatal
  3351. foreach my $id (AllPagesList()) {
  3352. OpenPage($id);
  3353. if ($lang) {
  3354. my @languages = split(/,/, $Page{languages});
  3355. next if (@languages and not grep(/$lang/, @languages));
  3356. }
  3357. $_ = $Page{text};
  3358. if (eval "s{$from}{$to}gi") { # allows use of backreferences
  3359. push (@result, $id);
  3360. Save($id, $_, $from . ' -> ' . $to, 1,
  3361. ($Page{ip} ne $ENV{REMOTE_ADDR}));
  3362. }
  3363. }
  3364. ReleaseLock();
  3365. return @result;
  3366. }
  3367. sub DoPost {
  3368. my $id = FreeToNormal(shift);
  3369. ValidIdOrDie($id);
  3370. ReportError(Ts('Editing not allowed for %s.', $id), '403 FORBIDDEN') unless UserCanEdit($id, 1);
  3371. # Lock before getting old page to prevent races
  3372. RequestLockOrError(); # fatal
  3373. OpenPage($id);
  3374. my $old = $Page{text};
  3375. my $string = UnquoteHtml(GetParam('text', undef));
  3376. $string =~ s/(\r|$FS)//go;
  3377. my ($type) = TextIsFile($string); # MIME type if an uploaded file
  3378. my $filename = GetParam('file', undef);
  3379. if (($filename or $type) and not $UploadAllowed and not UserIsAdmin()) {
  3380. ReportError(T('Only administrators can upload files.'), '403 FORBIDDEN');
  3381. }
  3382. my $comment = UnquoteHtml(GetParam('aftertext', undef));
  3383. $comment =~ s/(\r|$FS)//go;
  3384. if (defined($comment) and (not $comment or $comment eq $NewComment)) {
  3385. ReleaseLock();
  3386. ReBrowsePage($id);
  3387. }
  3388. if ($filename) { # upload file
  3389. my $file = $q->upload('file');
  3390. if (not $file and $q->cgi_error) {
  3391. ReportError(Ts('Transfer Error: %s', $q->cgi_error), '500 INTERNAL SERVER ERROR');
  3392. }
  3393. ReportError(T('Browser reports no file info.'), '500 INTERNAL SERVER ERROR')
  3394. unless $q->uploadInfo($filename);
  3395. $type = $q->uploadInfo($filename)->{'Content-Type'};
  3396. ReportError(T('Browser reports no file type.'), '415 UNSUPPORTED MEDIA TYPE') unless $type;
  3397. local $/ = undef; # Read complete files
  3398. my $content = <$file>; # Apparently we cannot count on <$file> to always work within the eval!?
  3399. my $encoding = 'gzip' if substr($content,0,2) eq "\x1f\x8b";
  3400. eval { require MIME::Base64; $_ = MIME::Base64::encode($content) };
  3401. $string = "#FILE $type $encoding\n" . $_;
  3402. } else { # ordinary text edit
  3403. $string = AddComment($old, $comment) if $comment;
  3404. $string = substr($string, length($DeletedPage)) # undelete pages when adding a comment
  3405. if $comment and substr($string, 0, length($DeletedPage)) eq $DeletedPage; # no regexp!
  3406. $string .= "\n" if ($string !~ /\n$/); # add trailing newline
  3407. $string = RunMyMacros($string); # run macros on text pages only
  3408. }
  3409. my %allowed = map {$_ => 1} @UploadTypes;
  3410. ReportError(Ts('Files of type %s are not allowed.', $type), '415 UNSUPPORTED MEDIA TYPE')
  3411. if @UploadTypes and $type and not $allowed{$type};
  3412. # Banned Content
  3413. my $summary = GetSummary();
  3414. if (not UserIsEditor()) {
  3415. my $rule = BannedContent($string) || BannedContent($summary);
  3416. ReportError(T('Edit Denied'), '403 FORBIDDEN', undef, $q->p(T('The page contains banned text.')),
  3417. $q->p(T('Contact the wiki administrator for more information.')), $q->p($rule)) if $rule;
  3418. }
  3419. # rebrowse if no changes
  3420. my $oldrev = $Page{revision};
  3421. if (GetParam('Preview', '')) { # Preview button was used
  3422. ReleaseLock();
  3423. if ($comment) {
  3424. BrowsePage($id, 0, RunMyMacros($comment)); # show macros in preview
  3425. } else {
  3426. DoEdit($id, $string, 1);
  3427. }
  3428. return;
  3429. } elsif ($old eq $string) {
  3430. ReleaseLock(); # No changes -- just show the same page again
  3431. return ReBrowsePage($id);
  3432. } elsif ($oldrev == 0 and ($string eq $NewText or $string eq "\n")) {
  3433. ReportError(T('No changes to be saved.'), '400 BAD REQUEST'); # don't fake page creation because of webdav
  3434. }
  3435. my $newAuthor = 0;
  3436. if ($oldrev) { # the first author (no old revision) is not considered to be "new"
  3437. # prefer usernames for potential new author detection
  3438. $newAuthor = 1 if not $Page{username} or $Page{username} ne GetParam('username', '');
  3439. $newAuthor = 1 if not $ENV{REMOTE_ADDR} or not $Page{ip} or $ENV{REMOTE_ADDR} ne $Page{ip};
  3440. }
  3441. my $oldtime = $Page{ts};
  3442. my $myoldtime = GetParam('oldtime', ''); # maybe empty!
  3443. # Handle raw edits with the meta info on the first line
  3444. if (GetParam('raw', 0) == 2 and $string =~ /^([0-9]+).*\n((.*\n)*.*)/) {
  3445. $myoldtime = $1;
  3446. $string = $2;
  3447. }
  3448. my $generalwarning = 0;
  3449. if ($newAuthor and $oldtime ne $myoldtime and not $comment) {
  3450. if ($myoldtime) {
  3451. my ($ancestor) = GetTextAtTime($myoldtime);
  3452. if ($ancestor and $old ne $ancestor) {
  3453. my $new = MergeRevisions($string, $ancestor, $old);
  3454. if ($new) {
  3455. $string = $new;
  3456. if ($new =~ /^<<<<<<</m and $new =~ /^>>>>>>>/m) {
  3457. SetParam('msg', Ts('This page was changed by somebody else %s.',
  3458. CalcTimeSince($Now - $Page{ts}))
  3459. . ' ' . T('The changes conflict. Please check the page again.'));
  3460. } # else no conflict
  3461. } else {
  3462. $generalwarning = 1;
  3463. } # else merge revision didn't work
  3464. } # else nobody changed the page in the mean time (same text)
  3465. } else {
  3466. $generalwarning = 1;
  3467. } # no way to be sure since myoldtime is missing
  3468. } # same author or nobody changed the page in the mean time (same timestamp)
  3469. if ($generalwarning and ($Now - $Page{ts}) < 600) {
  3470. SetParam('msg', Ts('This page was changed by somebody else %s.',
  3471. CalcTimeSince($Now - $Page{ts}))
  3472. . ' ' . T('Please check whether you overwrote those changes.'));
  3473. }
  3474. Save($id, $string, $summary, (GetParam('recent_edit', '') eq 'on'), $filename);
  3475. ReleaseLock();
  3476. ReBrowsePage($id);
  3477. }
  3478. sub GetSummary {
  3479. my $text = GetParam('aftertext', '') || ($Page{revision} > 0 ? '' : GetParam('text', ''));
  3480. if ($SummaryDefaultLength and length($text) > $SummaryDefaultLength) {
  3481. $text = substr($text, 0, $SummaryDefaultLength);
  3482. $text =~ s/\s*\S*$/ . . ./;
  3483. }
  3484. my $summary = GetParam('summary', '') || $text; # not GetParam('summary', $text) work because '' is defined
  3485. $summary =~ s/$FS|[\r\n]+/ /go; # remove linebreaks and separator characters
  3486. $summary =~ s/\[$FullUrlPattern\s+(.*?)\]/$2/go; # fix common annoyance when copying text to summary
  3487. $summary =~ s/\[$FullUrlPattern\]//go;
  3488. $summary =~ s/\[\[$FreeLinkPattern\]\]/$1/go;
  3489. return UnquoteHtml($summary);
  3490. }
  3491. sub AddComment {
  3492. my ($string, $comment) = @_;
  3493. $comment =~ s/\r//g; # Remove "\r"-s (0x0d) from the string
  3494. $comment =~ s/\s+$//g; # Remove whitespace at the end
  3495. if ($comment ne '' and $comment ne $NewComment) {
  3496. my $author = GetParam('username', T('Anonymous'));
  3497. my $homepage = GetParam('homepage', '');
  3498. $homepage = 'http://' . $homepage
  3499. if $homepage and $homepage !~ /^($UrlProtocols):/;
  3500. $author = "[$homepage $author]" if $homepage;
  3501. $string .= "\n----\n\n" if $string and $string ne "\n";
  3502. $string .= $comment . "\n\n"
  3503. . '-- ' . $author . ' ' . TimeToText($Now) . "\n\n";
  3504. }
  3505. return $string;
  3506. }
  3507. sub Save { # call within lock, with opened page
  3508. my ($id, $new, $summary, $minor, $upload) = @_;
  3509. my $user = GetParam('username', '');
  3510. my $host = GetRemoteHost();
  3511. my $revision = $Page{revision} + 1;
  3512. my $old = $Page{text};
  3513. my $olddiff = $Page{'diff-major'} == '1' ? $Page{'diff-minor'} : $Page{'diff-major'};
  3514. if ($revision == 1 and -e $IndexFile and not unlink($IndexFile)) { # regenerate index on next request
  3515. SetParam('msg', Ts('Cannot delete the index file %s.', $IndexFile)
  3516. . ' ' . T('Please check the directory permissions.')
  3517. . ' ' . T('Your changes were not saved.'));
  3518. return 0;
  3519. }
  3520. ReInit($id);
  3521. TouchIndexFile();
  3522. SaveKeepFile(); # deletes blocks, flags, diff-major, and diff-minor, and sets keep-ts
  3523. ExpireKeepFiles();
  3524. $Page{ts} = $Now;
  3525. $Page{lastmajor} = $revision unless $minor;
  3526. $Page{revision} = $revision;
  3527. $Page{summary} = $summary;
  3528. $Page{username} = $user;
  3529. $Page{ip} = $ENV{REMOTE_ADDR};
  3530. $Page{host} = $host;
  3531. $Page{minor} = $minor;
  3532. $Page{text} = $new;
  3533. if ($UseDiff and $UseCache > 1 and $revision > 1 and not $upload and not TextIsFile($old)) {
  3534. UpdateDiffs($old, $new, $olddiff); # sets diff-major and diff-minor
  3535. }
  3536. my $languages;
  3537. $languages = GetLanguages($new) unless $upload;
  3538. $Page{languages} = $languages;
  3539. SavePage();
  3540. if ($revision == 1 and $LockOnCreation{$id}) {
  3541. WriteStringToFile(GetLockedPageFile($id), 'LockOnCreation');
  3542. }
  3543. WriteRcLog($id, $summary, $minor, $revision, $user, $host, $languages, GetCluster($new));
  3544. if ($revision == 1) {
  3545. $IndexHash{$id} = 1;
  3546. @IndexList = sort(keys %IndexHash);
  3547. WriteStringToFile($IndexFile, join(' ', %IndexHash));
  3548. }
  3549. }
  3550. sub TouchIndexFile {
  3551. my $ts = time;
  3552. utime $ts, $ts, $IndexFile;
  3553. $LastUpdate = $Now = $ts;
  3554. }
  3555. sub GetLanguages {
  3556. my $text = shift;
  3557. my @result;
  3558. for my $lang (sort keys %Languages) {
  3559. my @matches = $text =~ /$Languages{$lang}/ig;
  3560. push(@result, $lang) if $#matches >= $LanguageLimit;
  3561. }
  3562. return join(',', @result);
  3563. }
  3564. sub GetCluster {
  3565. $_ = shift;
  3566. return '' unless $PageCluster;
  3567. return $1 if ($WikiLinks && /^$LinkPattern\n/o)
  3568. or ($FreeLinks && /^\[\[$FreeLinkPattern\]\]\n/o);
  3569. }
  3570. sub MergeRevisions { # merge change from file2 to file3 into file1
  3571. my ($file1, $file2, $file3) = @_;
  3572. my ($name1, $name2, $name3) = ("$TempDir/file1", "$TempDir/file2", "$TempDir/file3");
  3573. CreateDir($TempDir);
  3574. RequestLockDir('merge') or return T('Could not get a lock to merge!');
  3575. WriteStringToFile($name1, $file1);
  3576. WriteStringToFile($name2, $file2);
  3577. WriteStringToFile($name3, $file3);
  3578. my ($you,$ancestor,$other) = (T('you'), T('ancestor'), T('other'));
  3579. my $output = `diff3 -m -L "$you" -L "$ancestor" -L "$other" $name1 $name2 $name3`;
  3580. ReleaseLockDir('merge'); # don't unlink temp files--next merge will just overwrite.
  3581. return $output;
  3582. }
  3583. # Note: all diff and recent-list operations should be done within locks.
  3584. sub WriteRcLog {
  3585. my ($id, $summary, $minor, $revision, $username, $host, $languages, $cluster) = @_;
  3586. my $line = join($FS, $Now, $id, $minor, $summary, $host,
  3587. $username, $revision, $languages, $cluster);
  3588. AppendStringToFile($RcFile, $line . "\n");
  3589. }
  3590. sub UpdateDiffs { # this could be optimized, but isn't frequent enough
  3591. my ($old, $new, $olddiff) = @_;
  3592. $Page{'diff-minor'} = GetDiff($old, $new); # create new diff-minor
  3593. # 1 is a special value for GetCacheDiff telling it to use diff-minor
  3594. $Page{'diff-major'} = $Page{lastmajor} == $Page{revision} ? 1 : $olddiff;
  3595. }
  3596. sub DoMaintain {
  3597. print GetHeader('', T('Run Maintenance')), $q->start_div({-class=>'content maintain'});
  3598. my $fname = "$DataDir/maintain";
  3599. if (!UserIsAdmin()) {
  3600. if ((-f $fname) && ((-M $fname) < 0.5)) {
  3601. print $q->p(T('Maintenance not done.') . ' ' . T('(Maintenance can only be done once every 12 hours.)')
  3602. . ' ', T('Remove the "maintain" file or wait.')), $q->end_div();
  3603. PrintFooter();
  3604. return;
  3605. }
  3606. }
  3607. print '<p>', T('Expiring keep files and deleting pages marked for deletion');
  3608. # Expire all keep files
  3609. foreach my $name (AllPagesList()) {
  3610. print $q->br(), GetPageLink($name);
  3611. OpenPage($name);
  3612. my $delete = PageDeletable();
  3613. if ($delete) {
  3614. my $status = DeletePage($OpenPageName);
  3615. print ' ' . ($status ? T('not deleted: ') . $status : T('deleted'));
  3616. } else {
  3617. ExpireKeepFiles();
  3618. }
  3619. }
  3620. print '</p>';
  3621. RequestLockOrError();
  3622. print $q->p(T('Main lock obtained.'));
  3623. print $q->p(Ts('Moving part of the %s log file.', $RCName));
  3624. # Determine the number of days to go back
  3625. my $days = 0;
  3626. foreach (@RcDays) {
  3627. $days = $_ if $_ > $days;
  3628. }
  3629. my $starttime = $Now - $days * 86400; # 24*60*60
  3630. # Read the current file
  3631. my ($status, $data) = ReadFile($RcFile);
  3632. if (!$status) {
  3633. print $q->p($q->strong(Ts('Could not open %s log file', $RCName) . ':') . ' '. $RcFile),
  3634. $q->p(T('Error was') . ':'), $q->pre($!), $q->p(T('Note: This error is normal if no changes have been made.'));
  3635. }
  3636. # Move the old stuff from rc to temp
  3637. my @rc = split(/\n/, $data);
  3638. my @tmp = ();
  3639. for my $line (@rc) {
  3640. my ($ts, $id, $minor, $summary, $host, @rest) = split(/$FS/o, $line);
  3641. last if ($ts >= $starttime);
  3642. push(@tmp, join($FS, $ts, $id, $minor, $summary, 'Anonymous', @rest));
  3643. }
  3644. print $q->p(Ts('Moving %s log entries.', scalar(@tmp)));
  3645. if (@tmp) {
  3646. # Write new files, and backups
  3647. AppendStringToFile($RcOldFile, join("\n", @tmp) . "\n");
  3648. WriteStringToFile($RcFile . '.old', $data);
  3649. splice(@rc, 0, scalar(@tmp)); # strip
  3650. WriteStringToFile($RcFile, @rc ? join("\n",@rc) . "\n" : '');
  3651. }
  3652. if (opendir(DIR, $RssDir)) { # cleanup if they should expire anyway
  3653. foreach (readdir(DIR)) {
  3654. unlink "$RssDir/$_" if $Now - (stat($_))[9] > $RssCacheHours * 3600;
  3655. }
  3656. closedir DIR;
  3657. }
  3658. foreach my $sub (@MyMaintenance) {
  3659. &$sub;
  3660. }
  3661. WriteStringToFile($fname, 'Maintenance done at ' . TimeToText($Now));
  3662. ReleaseLock();
  3663. print $q->p(T('Main lock released.')), $q->end_div();
  3664. PrintFooter();
  3665. }
  3666. sub PageDeletable {
  3667. return unless $KeepDays;
  3668. my $expirets = $Now - ($KeepDays * 86400); # 24*60*60
  3669. return 0 unless $Page{ts} < $expirets;
  3670. return PageMarkedForDeletion();
  3671. }
  3672. sub PageMarkedForDeletion {
  3673. return 1 if $Page{text} =~ /^\s*$/; # only whitespace is also to be deleted
  3674. return $DeletedPage && substr($Page{text}, 0, length($DeletedPage)) eq $DeletedPage; # no regexp!
  3675. }
  3676. sub DeletePage { # Delete must be done inside locks.
  3677. my $id = shift;
  3678. ValidIdOrDie($id);
  3679. foreach my $name (GetPageFile($id), GetKeepFiles($id), GetKeepDir($id), GetLockedPageFile($id), $IndexFile) {
  3680. unlink $name if -f $name;
  3681. rmdir $name if -d $name;
  3682. }
  3683. ReInit($id);
  3684. delete $IndexHash{$id};
  3685. @IndexList = sort(keys %IndexHash);
  3686. return ''; # no error
  3687. }
  3688. sub DoEditLock {
  3689. return unless UserIsAdminOrError();
  3690. print GetHeader('', T('Set or Remove global edit lock'));
  3691. my $fname = "$NoEditFile";
  3692. if (GetParam("set", 1)) {
  3693. WriteStringToFile($fname, 'editing locked.');
  3694. } else {
  3695. unlink($fname);
  3696. }
  3697. utime time, time, $IndexFile; # touch index file
  3698. print $q->p(-f $fname ? T('Edit lock created.') : T('Edit lock removed.'));
  3699. PrintFooter();
  3700. }
  3701. sub DoPageLock {
  3702. return unless UserIsAdminOrError();
  3703. print GetHeader('', T('Set or Remove page edit lock'));
  3704. my $id = GetParam('id', '');
  3705. my $fname = GetLockedPageFile($id) if ValidIdOrDie($id);
  3706. if (GetParam('set', 1)) {
  3707. WriteStringToFile($fname, 'editing locked.');
  3708. } else {
  3709. unlink($fname);
  3710. }
  3711. utime time, time, $IndexFile; # touch index file
  3712. print $q->p(-f $fname ? Ts('Lock for %s created.', GetPageLink($id))
  3713. : Ts('Lock for %s removed.', GetPageLink($id)));
  3714. PrintFooter();
  3715. }
  3716. sub DoShowVersion {
  3717. print GetHeader('', T('Displaying Wiki Version')), $q->start_div({-class=>'content version'});
  3718. print $WikiDescription, $q->p($q->server_software()),
  3719. $q->p(sprintf('Perl v%vd', $^V)),
  3720. $q->p($ENV{MOD_PERL} ? $ENV{MOD_PERL} : "no mod_perl"), $q->p('CGI: ', $CGI::VERSION),
  3721. $q->p('LWP::UserAgent ', eval { local $SIG{__DIE__}; require LWP::UserAgent; $LWP::UserAgent::VERSION; }),
  3722. $q->p('XML::RSS: ', eval { local $SIG{__DIE__}; require XML::RSS; $XML::RSS::VERSION; }),
  3723. $q->p('XML::Parser: ', eval { local $SIG{__DIE__}; $XML::Parser::VERSION; });
  3724. print $q->p('diff: ' . (`diff --version` || $!)), $q->p('diff3: ' . (`diff3 --version` || $!)) if $UseDiff;
  3725. print $q->p('grep: ' . (`grep --version` || $!)) if $UseGrep;
  3726. print $q->end_div();
  3727. PrintFooter();
  3728. }
  3729. sub DoDebug {
  3730. print GetHeader('', T('Debugging Information')),
  3731. $q->start_div({-class=>'content debug'});
  3732. foreach my $sub (@Debugging) { &$sub }
  3733. print $q->end_div();
  3734. PrintFooter();
  3735. }
  3736. sub DoSurgeProtection {
  3737. return unless $SurgeProtection;
  3738. my $name = GetParam('username','');
  3739. $name = $ENV{'REMOTE_ADDR'} if not $name and $SurgeProtection;
  3740. return unless $name;
  3741. ReadRecentVisitors();
  3742. AddRecentVisitor($name);
  3743. if (RequestLockDir('visitors')) { # not fatal
  3744. WriteRecentVisitors();
  3745. ReleaseLockDir('visitors');
  3746. if (DelayRequired($name)) {
  3747. ReportError(Ts('Too many connections by %s',$name)
  3748. . ': ' . Tss('Please do not fetch more than %1 pages in %2 seconds.',
  3749. $SurgeProtectionViews, $SurgeProtectionTime),
  3750. '503 SERVICE UNAVAILABLE');
  3751. }
  3752. } elsif (GetParam('action', '') ne 'unlock') {
  3753. 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');
  3754. }
  3755. }
  3756. sub DelayRequired {
  3757. my $name = shift;
  3758. my @entries = @{$RecentVisitors{$name}};
  3759. my $ts = $entries[$SurgeProtectionViews];
  3760. return ($Now - $ts) < $SurgeProtectionTime;
  3761. }
  3762. sub AddRecentVisitor {
  3763. my $name = shift;
  3764. my $value = $RecentVisitors{$name};
  3765. my @entries = ($Now);
  3766. push(@entries, @{$value}) if $value;
  3767. $RecentVisitors{$name} = \@entries;
  3768. }
  3769. sub ReadRecentVisitors {
  3770. my ($status, $data) = ReadFile($VisitorFile);
  3771. %RecentVisitors = ();
  3772. return unless $status;
  3773. foreach (split(/\n/,$data)) {
  3774. my @entries = split /$FS/o;
  3775. my $name = shift(@entries);
  3776. $RecentVisitors{$name} = \@entries if $name;
  3777. }
  3778. }
  3779. sub WriteRecentVisitors {
  3780. my $data = '';
  3781. my $limit = $Now - $SurgeProtectionTime;
  3782. foreach my $name (keys %RecentVisitors) {
  3783. my @entries = @{$RecentVisitors{$name}};
  3784. if ($entries[0] >= $limit) { # if the most recent one is too old, do not keep
  3785. $data .= join($FS, $name, @entries[0 .. $SurgeProtectionViews - 1]) . "\n";
  3786. }
  3787. }
  3788. WriteStringToFile($VisitorFile, $data);
  3789. }
  3790. sub TextIsFile { $_[0] =~ /^#FILE (\S+) ?(\S+)?\n/ }
  3791. DoWikiRequest() if $RunCGI and not exists $ENV{MOD_PERL}; # Do everything.
  3792. 1; # In case we are loaded from elsewhere