Compare commits

...

42 Commits

Author SHA1 Message Date
Alex Schroeder
aba9fcfa40 [post-instead-of-get] use utf8 2025-07-25 19:24:18 +02:00
Alex Schroeder
16eb72c8db [post-instead-of-get] Fix link to "View all changes" 2025-07-25 18:58:48 +02:00
Alex Schroeder
4f99e2c4bd Fix README markup 2025-07-25 16:06:10 +02:00
Alex Schroeder
0b993a002a [post-instead-of-get] Change filter in DoIndex 2025-07-16 17:43:53 +02:00
Alex Schroeder
f5cb40d21c [post-instead-of-get] Change More... link in RcHtml 2025-07-16 17:17:50 +02:00
Alex Schroeder
97bc55bef3 [post-instead-of-get] Add back RcHeader
Just print the subheading.
2025-07-15 11:51:00 +02:00
Alex Schroeder
a30f9bb40c [post-instead-of-get] Fix list later changes form 2025-07-15 11:50:36 +02:00
Alex Schroeder
35c04beb2a [post-instead-of-get] Fix copyright 2025-07-15 11:38:59 +02:00
Alex Schroeder
c7a37261d1 [post-instead-of-get] Fix checkboxes
Use -checked correctly. Make label clickable by adding an id to the
checkboxes. Put the space inside the label.
2025-07-15 11:20:29 +02:00
Alex Schroeder
64bc459a3e [post-instead-of-get] Add line break
Add a linebreak before the Go! button the on the RecentChanges search
filter form.
2025-07-15 11:00:00 +02:00
Alex Schroeder
c7e563d02f CSS changes from 2020 2025-07-15 10:57:44 +02:00
Alex Schroeder
d489281f5c [post-instead-of-get] New module 2025-07-15 10:52:20 +02:00
Alex Schroeder
cc7240dc98 [nosearch] no class, no GET
Don't wrap the title in a span with extra stuff.

Don't use GET requests for the backlink search.
2025-07-15 10:52:20 +02:00
Alex Schroeder
726dfc2d5d Don't use List::Util "all" 2025-07-15 10:52:20 +02:00
Alex Schroeder
23e1cceead Ignore .vscode 2025-06-25 00:28:41 +02:00
Alex Schroeder
6c3eb92fff Add webp to image extensions 2025-06-25 00:28:09 +02:00
Alex Schroeder
a3ef9c2040 Switch README to Markdown 2024-08-12 10:09:24 +02:00
Alex Schroeder
4e16082b70 New empty translation file 2023-11-21 11:35:13 +01:00
Alex Schroeder
6234b05a50 [usemod] Switch ISBN lookups to Wikipedia 2023-11-21 11:31:17 +01:00
Alex Schroeder
567ea8e0a8 Fix rollback-hang tests 2023-11-21 11:31:09 +01:00
Alex Schroeder
0974b7bbd8 wordcount: fix test 2023-10-25 18:42:25 +02:00
Alex Schroeder
f73d420957 markdown-rule: be more lenient
Don't just limit to full URL: accept any non-whitespace characters as
a link.
2023-10-25 18:40:43 +02:00
Alex Schroeder
17ef2aaf88 CapnDan tells me this line is missing at the end 2023-08-12 21:22:49 +02:00
Alex Schroeder
b70c8e8def Add rolblack marker stripping back in 2023-08-10 11:23:18 +02:00
Alex Schroeder
f8752e69bc Update years 2023-08-10 11:20:46 +02:00
Alex Schroeder
9d48f875a2 Fix rollback code 2023-08-08 16:31:09 +02:00
Alex Schroeder
39e9cea7b0 Add Matched sub
This allows the use of whitespace to separate terms when using page
matching. This seems more natural than requiring underscores to match
the space between words in a page title. So now, a page with id
"one_two" is matched by terms such as "one_two", "two_one", "one two"
and "two one" (notice the word order).
2023-08-08 14:59:53 +02:00
Alex Schroeder
e7b718f610 [network-blocker] Ignore missing file 2023-07-15 21:06:04 +02:00
Alex Schroeder
261aeccb3f [network-blocker] New module 2023-07-15 21:00:58 +02:00
Alex Schroeder
a09c846700 Fix a rollback issue
Without this fix, Oddmuse would enter an infinite loop if the list of
items to show began with a rollback tag.
2023-06-30 13:02:03 +02:00
Alex Schroeder
8dbede3813 Tarballs doesn't link to a latest.tar.gz 2023-06-21 22:05:16 +02:00
Alex Schroeder
89d9f27b2a [rename-pages] Close form 2023-05-29 20:24:55 +02:00
Alex Schroeder
f21f257c1b Fix parenthesis 2023-03-24 21:59:52 +01:00
Alex Schroeder
48916943a1 More spans for the search bar 2023-03-24 21:16:47 +01:00
Alex Schroeder
3b185e5521 Add some spans to the gotobar for better styling 2023-03-24 15:38:50 +01:00
Alex Schroeder
612af8f7fb Make feed link more flexible
The result is that feeds generated by journal-rss.pl contain a link to
the Recent Changes page instead of linking twice to the feed.
2023-02-27 14:12:15 +01:00
Alex Schroeder
dc9131e600 Fix translation-link.t 2023-02-27 14:12:04 +01:00
Alex Schroeder
99af4d984d Handle [an example](#foo "Title") 2023-02-17 17:16:23 +01:00
Alex Schroeder
88f4fe3b89 Whitespace 2023-02-17 17:16:02 +01:00
Sandra Snan
851f2f77e8 Handle image/right
Everyone loves hacky regexes♥
2023-02-17 17:14:24 +01:00
Alex Schroeder
975e15c9f8 Don't turn all whitespace into a space
We want to honor NO-BREAK SPACE and the like!
2022-08-26 13:42:35 +02:00
Alex Schroeder
d235d6ac47 GetId returns the normal form of $id.
This means, "2022-07-15 The Joy of Exploration", which arrives as
"2022-07-15%20The%20Joy%20of%20Exploration", gets turned into
"2022-07-15_The_Joy_of_Exploration". The problem is that when posting,
$id = FreeToNormal(shift), so pages are always written to the page
with underscores. If you then request the raw history of a page,
however, no such call was happening and so no keep files were found by
DoHistory.
2022-07-18 17:51:22 +02:00
26 changed files with 988 additions and 560 deletions

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@
wiki.log
.prove
TAGS
/.vscode/

View File

@@ -1,197 +0,0 @@
This is the README file distributed together with the
[[https://oddmuse.org/|Oddmuse]] script.
== Installing Oddmuse on a Debian System running Apache
The following instructions require a number of tools. You can make sure
they're all installed by issuing the following command as {{{root}}}:
{{{
apt-get install coreutils apache2 sudo wget w3m perl \
libwww-perl libxml-rss-perl diffutils
}}}
You probably created an account for yourself. You might have to add this
user to the {{{sudo}}} group. Here's how I created my own user as
{{{root}}}:
{{{
adduser alex
usermod -a -G sudo alex
}}}
Now you can login as {{{alex}}} and do everything else using {{{sudo}}}.
You need to copy wiki.pl into your cgi-bin directory, and you need to
make the script executable. You might also have to change its owner to
an appropriate user on your system.
{{{
sudo wget -O /usr/lib/cgi-bin/wiki.pl \
http://git.savannah.gnu.org/cgit/oddmuse.git/plain/wiki.pl
sudo chmod +x /usr/lib/cgi-bin/wiki.pl
sudo chown www-data.www-data /usr/lib/cgi-bin/wiki.pl
}}}
If you're on SUSE, the user might not be {{{www-data}}} but
{{{wwwrun}}} without appropriate group:
{{{
sudo chown wwwrun.root /usr/lib/cgi-bin/wiki.pl
}}}
You should be able to test it right now! Visit
{{{http://localhost/cgi-bin/wiki.pl}}}. If your site is available from
the outside, you will be able to use a normal browser. If don't have a
domain name yet, you'll probably have to use a text browser like
{{{w3m}}}.
{{{
w3m http://localhost/cgi-bin/wiki.pl
}}}
If you create pages in this wiki, these will get stored in a temporary
directory. You need change the data directory from {{{"/tmp/oddmuse"}}}
to like {{{"/var/local/oddmuse"}}}. The best way to do this without
changing {{{wiki.pl}}} is by editing
{{{/etc/apache2/sites-available/default}}}. Add the following line:
{{{
SetEnv WikiDataDir /var/local/oddmuse
}}}
Enable the default site by calling the following command:
{{{
sudo a2ensite default
}}}
Reload the Apache configuration by calling the following command:
{{{
sudo service apache2 reload
}}}
You need to create the new data directory. You webserver runs CGI
scripts as {{{www-data}}}. Thus, you need to change the owner and group
of the directory to {{{www-data}}}.
{{{
sudo mkdir -p /var/local/oddmuse
sudo chown www-data.www-data /var/local/oddmuse
}}}
Done! Visit your wiki and start editing. Click on the edit link (the
first link below the navigation bar, at the bottom of the page). This
will allow you to enter some text for this page. Click the Save button
and you are done.
To add new pages, edit the homepage and add links to new pages by
putting their names in {{{[[double square brackets]]}}}.
Enjoy your wiki experience.
Visit https://www.oddmuse.org/ to learn more about the translation
files and modules that are part of this package.
== Checking the Apache Setup
If you think this information doesn't work for you, here are some things
to check.
Apache's config directory is {{{/etc/apache2/apache2.conf}}}. This is
where we get the {{{www-data}}} username from. It says:
{{{
# These need to be set in /etc/apache2/envvars
User ${APACHE_RUN_USER}
Group ${APACHE_RUN_GROUP}
}}}
Checking {{{/etc/apache2/envvars}}} we see the following:
{{{
export APACHE_RUN_USER=www-data
export APACHE_RUN_GROUP=www-data
}}}
So that's what we're using in the {{{chown}}} command in our
instructions above.
The default site is configured in
{{{/etc/apache2/sites-available/default}}}. In order for it to be
//enabled//, there must be a symlink from a file in
{{{/etc/apache2/sites-enabled}}} to the file in
{{{sites-available}}}. You can enable it using the following command:
{{{
sudo a2ensite default
}}}
This file also lists the directories we've used in our instructions
above.
{{{
ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
}}}
This means that {{{http://localhost/cgi-bin/wiki.pl}}} will call
{{{/usr/lib/cgi-bin/wiki.pl}}}
Don't forget to reload the Apache configuration as shown above, or
simply restart it all:
{{{
sudo service apache2 graceful
}}}
== Using just Perl
You can use Mojolicious as your web server. There is a simple
##server.pl## which you can use. Here's how you might start it:
{{{
mkdir ~/oddmuse
WikiDataDir=$HOME/oddmuse perl server.pl daemon
}}}
This makes the server available on {{{http://localhost:3000/wiki}}}.
Make sure you create the directory before starting the server!
If you don't, you'll get a strange error:
`STDERR: : No such file or directory at ... perl5/Mojolicious/Plugin/CGI.pm`.
If it works, feel free to upgrade to Hypnotoad.
{{{
WikiDataDir=$HOME/oddmuse hypnotoad server.pl
}}}
Note: Hypnotoad uses a different default port. The above makes the
server available on {{{http://localhost:8080/wiki}}}. Hypnotoad will
keep forking new processes. To stop it, use the {{{-s}}} flag.
{{{
hypnotoad -s server.pl
}}}
== License
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.1 or
any later version published by the Free Software Foundation.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or (at
your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
Both the GNU Free Documentation License, and the GNU General Public
License are distributed together with this script. See the files
[[https://github.com/kensanata/oddmuse/blob/master/FDL|FDL]] and
[[https://github.com/kensanata/oddmuse/blob/master/GPL|GPL]],
respectively.

161
README.md Normal file
View File

@@ -0,0 +1,161 @@
This is the README file distributed together with the
[Oddmuse](https://oddmuse.org/) script.
## Installing Oddmuse on a Debian System running Apache
The following instructions require a number of tools. You can make sure
they're all installed by issuing the following command as `root`:
apt-get install coreutils apache2 sudo wget w3m perl \
libwww-perl libxml-rss-perl diffutils
You probably created an account for yourself. You might have to add this
user to the `sudo` group. Here's how I created my own user as `root`:
adduser alex
usermod -a -G sudo alex
Now you can login as `alex` and do everything else using `sudo`.
You need to copy wiki.pl into your cgi-bin directory, and you need to
make the script executable. You might also have to change its owner to
an appropriate user on your system.
sudo wget -O /usr/lib/cgi-bin/wiki.pl \
http://git.savannah.gnu.org/cgit/oddmuse.git/plain/wiki.pl
sudo chmod +x /usr/lib/cgi-bin/wiki.pl
sudo chown www-data.www-data /usr/lib/cgi-bin/wiki.pl
If you're on SUSE, the user might not be `www-data` but `wwwrun` without
appropriate group:
sudo chown wwwrun.root /usr/lib/cgi-bin/wiki.pl
You should be able to test it right now! Visit
`http://localhost/cgi-bin/wiki.pl`. If your site is available from the
outside, you will be able to use a normal browser. If don't have a
domain name yet, you'll probably have to use a text browser like `w3m`.
w3m http://localhost/cgi-bin/wiki.pl
If you create pages in this wiki, these will get stored in a temporary
directory. You need change the data directory from `"/tmp/oddmuse"` to
like `"/var/local/oddmuse"`. The best way to do this without changing
`wiki.pl` is by editing `/etc/apache2/sites-available/default`. Add the
following line:
SetEnv WikiDataDir /var/local/oddmuse
Enable the default site by calling the following command:
sudo a2ensite default
Reload the Apache configuration by calling the following command:
sudo service apache2 reload
You need to create the new data directory. You webserver runs CGI
scripts as `www-data`. Thus, you need to change the owner and group of
the directory to `www-data`.
sudo mkdir -p /var/local/oddmuse
sudo chown www-data.www-data /var/local/oddmuse
Done! Visit your wiki and start editing. Click on the edit link (the
first link below the navigation bar, at the bottom of the page). This
will allow you to enter some text for this page. Click the Save button
and you are done.
To add new pages, edit the homepage and add links to new pages by
putting their names in `[[double square brackets]]`.
Enjoy your wiki experience.
Visit <https://www.oddmuse.org/> to learn more about the translation
files and modules that are part of this package.
## Checking the Apache Setup
If you think this information doesn't work for you, here are some things
to check.
Apache's config directory is `/etc/apache2/apache2.conf`. This is where
we get the `www-data` username from. It says:
# These need to be set in /etc/apache2/envvars
User ${APACHE_RUN_USER}
Group ${APACHE_RUN_GROUP}
Checking `/etc/apache2/envvars` we see the following:
export APACHE_RUN_USER=www-data
export APACHE_RUN_GROUP=www-data
So that's what we're using in the `chown` command in our instructions
above.
The default site is configured in
`/etc/apache2/sites-available/default`. In order for it to be *enabled*,
there must be a symlink from a file in `/etc/apache2/sites-enabled` to
the file in `sites-available`. You can enable it using the following
command:
sudo a2ensite default
This file also lists the directories we've used in our instructions
above.
ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
This means that `http://localhost/cgi-bin/wiki.pl` will call
`/usr/lib/cgi-bin/wiki.pl`
Don't forget to reload the Apache configuration as shown above, or
simply restart it all:
sudo service apache2 graceful
## Using just Perl
You can use Mojolicious as your web server. There is a simple
`server.pl` which you can use. Here's how you might start it:
mkdir ~/oddmuse
WikiDataDir=$HOME/oddmuse perl server.pl daemon
This makes the server available on `http://localhost:3000/wiki`. Make
sure you create the directory before starting the server! If you don't,
you'll get a strange error: \`STDERR: : No such file or directory at ...
perl5/Mojolicious/Plugin/CGI.pm\`.
If it works, feel free to upgrade to Hypnotoad.
WikiDataDir=$HOME/oddmuse hypnotoad server.pl
Note: Hypnotoad uses a different default port. The above makes the
server available on `http://localhost:8080/wiki`. Hypnotoad will keep
forking new processes. To stop it, use the `-s` flag.
hypnotoad -s server.pl
## License
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.1 or
any later version published by the Free Software Foundation.
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 2 of the License, or (at your
option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
Public License for more details.
Both the GNU Free Documentation License, and the GNU General Public
License are distributed together with this script. See the files
[FDL](https://github.com/kensanata/oddmuse/blob/master/FDL) and
[GPL](https://github.com/kensanata/oddmuse/blob/master/GPL),
respectively.

View File

@@ -1,32 +1,51 @@
/* Authors: Murray Altheim (2004), Alex Schroeder (2004, 2005, 2006,
2009), Bayle Shanks (2006), Lion Kimbro (2006).
2009, 2020), Bayle Shanks (2006), Lion Kimbro (2006).
This file is in the public domain.
*/
html, body { /* hue 84 */
html {
/* background-color:#becc92; */
background-color:#def4b5;
text-align: center;
}
body { /* hue 84 */
/* Default color for text. It doesn't appear that many places,
but you'll see it in Recent Changes summary comments, and
you'll see it in the languages at the bottom of the place;
It seeps out, here and there. */
color:#000;
/* This is the main light green background color. */
background-color:#def4b5;
margin:0;
padding:0;
font-size: 14pt;
max-width: 80ex;
display: inline-block;
text-align: left;
}
/* This is not used in all that many places. */
body.gray {
background-color:#d5e0c5;
}
/* The next section includes some funky selectors.
See http://www.w3.org/TR/REC-CSS2/selector.html for more. */
div.content, div.rc, body > form, div.footnotes, div.edit text {
margin: 1em;
.wrapper {
padding: 1ex;
}
.languages {
display: none;
}
@media only screen and (max-width: 1200px) {
body {
padding: 5pt;
font-size: 15pt;
}
}
@media only screen and (max-width: 800px) {
body {
padding: 3pt;
font-size: 18pt;
}
}
/* The following statement hides the result count at the end
@@ -70,53 +89,16 @@ body.arrows a.near:before, body.arrows a.outside:before { content:"\2197"; }
body.arrows a.near, body.arrows a.outside { text-decoration:none; }
/* add every specific a here */
div.header h1 a:hover, h1 a:hover, h2 a:hover, h3 a:hover { color: #fbb; }
header h1 a:hover, h1 a:hover, h2 a:hover, h3 a:hover { color: #fbb; }
header h1 a:visited { color: #fff; }
a.definition:hover, a.near:hover, a:hover { color:#f00; }
dl.irc dt { width:20ex; float:left; text-align:right; clear:left; }
dl.irc dt span.time { float:left; }
dl.irc dd { margin-left:22ex; }
div.header {
background-color: #becc92;
margin: 0;
padding: 0;
height: 6em;
}
div.sidebar {
float: right;
width: 15%;
padding: 0 0.5em 0 1em;
margin: 1em 0 1em 5em;
font-size: x-small;
border: 3px solid #000;
text-align: left;
background-color: #dea;
}
div.sidebar h2 {
font-weight: bold;
font-size: small;
color: #000;
background-color: #dea;
padding: 0;
margin-right: 7%;
border-bottom: 1px solid #ab7;
}
div.sidebar ul, div.sidebar li {
display:block;
margin:0;
padding:0;
}
div.sidebar a:before {
content:"";
}
div.sidebar a {
font-weight: normal;
header {
margin: 0;
}
h3 {
@@ -128,52 +110,49 @@ h3 {
padding:0.3ex;
}
div.header span.gotobar {
header span.gotobar {
display:block;
padding:1ex;
}
div.message {
position: absolute;
top: 1.5em;
left:0;
right:0;
z-index: 5;
}
div.message p {
display:inline;
}
div.message, div.question {
background-color:#fee;
color:#f00;
border:solid #f00;
font-weight:bold;
padding:0.1em 0 0.1em 1em;
.message p, div.question {
font-size: smaller;
margin: 0;
padding: 0 0 0.5ex 1ex;
}
div.header h1 {
position: absolute;
top: 1.5em;
left:0;
right:0;
header h1 {
background-color:#517005;
font-family: "Tahoma", "Arial", "Helvetica", sans-serif;
font-size:xx-large;
border-bottom:2px dotted #87a036;
margin-top: 0;
padding: 0.125em 0.5em;
font-size: xx-large;
margin: 0;
padding: 0.1em 0.5ex;
}
div.header h1 a {
header h1 a {
text-decoration:none;
color: #ffffff;
}
img.logo {
position:absolute;
top:1ex;
right:1ex;
img.logo {
float: right;
height: 4em;
border:none;
z-index:10;
}
@media only screen and (max-width: 1200px) {
img.logo {
height: 3em;
}
}
@media only screen and (max-width: 800px) {
img.logo {
height: 2em;
}
}
.fit {
max-width: 100%;
}
.right {
@@ -182,6 +161,7 @@ img.logo {
.left {
float:left;
margin-right: 8px;
}
.top {
@@ -221,7 +201,7 @@ a.small img {
h1 {
font-weight:bold;
font-size:larger;
font-size:150%;
clear:left;
color:#fff;
background:#69aa00;
@@ -237,17 +217,23 @@ span.specialdays {
h2 {
font-weight:bold;
font-size:larger;
font-size:130%;
color:#fff;
background:#69aa00;
padding:0.7ex;
clear:left;
}
h2 a, div.journal h1 a {
/* Links in page titles */
h1 a, h2 a, div.journal h1 a {
text-decoration:none; color:#fff;
}
h3 {
font-weight:bold; font-size:medium; clear:left;
color:#fff; background:#84d600; padding:0.7ex;
font-weight:bold;
font-size: 110%;
clear:left;
color:#fff;
background:#84d600;
padding:0.7ex;
}
h3 a, div.journal h2 a {
text-decoration:none; color:#fff;
@@ -277,8 +263,8 @@ div.footnotes hr + p {
font-weight:bold;
}
div.footer {
background-color:#becc92;
footer {
background-color: #cd9;
border-bottom:solid;
clear: both;
margin: 3em 0 0 0;
@@ -287,13 +273,89 @@ div.footer {
color:black;
}
div.footer hr {
footer hr {
display:none;
}
/* License, definitions, near links */
.note, .more {
font-size: 80%;
}
.more {
margin: 1ex;
margin-bottom: 3em;
}
div.near, div.definition {
display: none;
}
#toggle_more:checked ~ div.near, #toggle_more:checked ~ div.definition {
display: block;
}
/* Recent Changes */
div.rc { margin-top:4ex; }
div.rc hr { display:none; }
div.rc {
overflow: hidden;
}
div.rc li + li {
margin-top: 1em;
}
div.rc li strong, table.history strong, strong.description {
font-family: inherit;
font-weight: inherit;
}
/* Colour flags for anonymous edits */
.red {
background: red;
color: red;
}
.orange {
background: orange;
color: orange;
}
.yellow {
background: yellow;
color: yellow;
}
.green {
background: green;
color: green;
}
.blue {
background: blue;
color: blue;
}
.indigo {
background: indigo;
color: indigo;
}
.violet {
background: violet;
color: violet;
}
.white {
background: white;
color: white;
}
.ip-code {
border: 1px solid #666;
}
/* Diff */
div.old { background-color:#ffd; }
div.new { background-color:#dfd; }
div.diff {
@@ -306,6 +368,9 @@ div.diff {
div.diff + hr {
display: none;
}
/* Referrers */
div.refer {
padding-left:5%;
padding-right:5%;
@@ -313,10 +378,9 @@ div.refer {
div.refer hr {
display: none;
}
div.rss { background-color:#ce9; }
body.gray div.rss {
background-color:#dec;
}
/* Sister Sites */
div.sister {
float:left;
margin-right:1ex;
@@ -326,7 +390,9 @@ div.sister p { padding:1ex; margin:0; }
div.sister hr { display:none; }
div.near, div.definition { padding:1ex; margin:0; }
div.near p, div.definition p { margin: 0; }
div.footer + hr { display:none; }
footer + hr { display:none; }
/* Headers in Journal Pages (e.g. Blog) */
div.journal hr { display:none; }
div.journal h1, div.journal h2, div.journal h3, div.journal h4 {
@@ -343,23 +409,65 @@ div.include {
span.description { font-weight:bold; }
span.new { display:inline; font-weight:bold; }
table.user { border-collapse:collapse; border:thin dotted; padding:1ex;
margin-bottom:1ex; width:inherit; margin:0 5%; }
table.user tr td { padding: 0.5ex 1em; border: thin dotted; text-align:left; }
/* Tables in wiki content */
table.user {
border-collapse:collapse;
border: none;
padding:1ex;
margin-bottom:1ex;
width:inherit;
margin:0 5%;
background-color: #efd;
}
/* table.user .even { */
/* background-color: #efd; */
/* } */
/* table.user .odd { */
/* background-color: #efe; */
/* } */
/* table.user .first { */
/* background-color: #eff; */
/* } */
table.user td, table.user th {
padding: 0.5ex 1em;
border: none;
text-align:left;
}
table.user th {
padding: 1ex 1em;
text-align: center;
}
/* Tables in page history */
table.history td[colspan="3"] {
padding: 1em 0;
}
/* Lists (definitions list being used by IRC logs?) */
dt { font-weight:bold; }
dd, li {
dd {
margin-bottom: 0.5ex;
margin-left: 2em;
}
li {
margin-bottom: 0.5ex;
margin-left: 0;
}
dl, ol, ul { margin-left:0em; }
textarea#text { width:75%; height:70%; }
textarea#summary { width:75%; height:10%; }
/* textarea, summary */
textarea {
box-sizing: border-box;
width:100%;
padding:5pt;
font-size: inherit;
}
textarea#text { height:70%; min-height: 20ex; }
textarea#summary { height:10%; min-height: 2ex; }
/* links to change from text to file and back */
@@ -375,12 +483,11 @@ form.edit a.svg, form.edit a.upload {
/* images */
img { border:0; }
pre, img.upload {
border: #777 1px solid; padding: 0.5em;
margin-left: 1em; margin-right: 2em;
pre, img.portrait, img.upload {
border: #777 1px solid; padding: 8px;
white-space: pre;
background-color: #fff; color: black;
overflow: hidden;
overflow: scroll;
}
a.smiley img.upload {
border:none;
@@ -388,32 +495,21 @@ a.smiley img.upload {
padding:0;
background-color:inherit;
}
.color { min-height: 60px; }
img.portrait {
float:left; clear:left;
background-color:#fff;
border:#999 1px solid;
padding:10px;
margin:10px;
}
div.portrait {
float:left; clear:left;
font-size:xx-small;
padding-left:10px;
}
div.portrait img.portrait {
float:none;
margin:10px 10px 0 0;
}
div.portrait a {
text-decoration:none;
color:#999;
}
div.color {
clear: left;
min-height:105px;
margin: 0;
padding: 0;
float: left;
margin-right: 8px;
}
div.portrait br { display: none }
div.portrait, div.portrait p { display: inline}
div.portrait p:after { content: ": " }
div.color > p:first-of-type { display: inline }
div.color { padding: 1ex 0.5ex 1em 0.5ex; }
.half img { max-width: 50%; }
/* indentation */
div.one {
background-color: #efb;
}
@@ -434,101 +530,6 @@ hr {
clear: left;
}
div.month {
float:left;
margin:3ex;
height:15ex;
}
div.month pre {
background-color:inherit;
border:none;
padding:0;
margin:0;
}
div.month a.edit {
font-weight:normal;
color:#000;
}
rss {
color:#000;
margin:0;
padding:0;
background-color:#def4b5;
}
docs {
position: absolute;
top:0;
left:0;
right:0;
font-size: xx-large;
height: 1.5em;
color: #becc92; /* invisible */
background-color: #becc92;
}
channel * {
display: block;
}
/* if IE can't parse this, no problem */
channel > title {
font-family: "Tahoma", "Arial", "Helvetica", sans-serif;
}
title {
background-color:#517005;
font-size:xx-large;
font-weight:bold;
margin-top: 1.5em;
padding: 0.125em 0.5em;
border-bottom:2px dotted #87a036;
color:#fff;
}
item title {
background-color:#69aa00;
font-size: medium;
margin: 0 0 0 1em;
padding:0.7ex 0.5em;
}
copyright {
font-size: smaller;
margin: 1em 4em;
}
channel > link:before {
font-size: x-large;
display: block;
margin: 1em;
padding: 0.5em;
content: "This is an RSS feed, designed to be read in a feed reader.";
color: red;
border: 1px solid red;
}
link, license {
font-size: smaller;
margin: 1em 2em;
}
username, description, generator, interwiki { margin: 1em; }
username:before { content: "Last edited by "; }
username:after { content: "."; }
generator:before { content: "Feed generated by "; }
generator:after { content: "."; }
channel description {
font-weight: bold;
}
item description {
font-style: italic;
font-weight: normal;
margin: 1em;
}
language,
pubDate, lastBuildDate, ttl, guid, category, comments,
image title, image link,
status, version, diff, history, importance {
display: none;
}
/*
== Printing ==
@@ -542,10 +543,10 @@ when the page is printed (or during print preview). More information:
@media print {
/* When printing, turn off a bunch of stuff. */
div.header span.gotobar,
header span.gotobar,
span.specialdays,
div.refer,
div.footer,
footer,
div.near,
div.definition,
div.sister,
@@ -592,19 +593,16 @@ span[lang=pt], .pt { background-color:#bfb; }
span[lang=es], .es { background-color:#fec; }
span[lang=sv], .sv { background-color:#adf; }
body.simple div.footer p.note,
body.simple div.footer span.gotobar + br,
body.simple div.footer span.gotobar,
body.simple footer p.note,
body.simple footer span.gotobar + br,
body.simple footer span.gotobar,
body.simple div.sister,
body.simple div.near,
body.simple div.definition,
body.simple div.languages { display:none; }
body.explicit a.near[title=MeatBall]:before { content:"MeatBall:"; }
body.explicit a.near[title=WikiFeatures]:before { content:"WikiFeatures:"; }
body.explicit a.near[title=CraoWiki]:before { content:"CraoWiki:"; }
body.explicit a.near[title=InterWiki]:before { content:"InterWiki:"; }
body.explicit a.near[title=OpenMeatballWiki]:before { content:"OpenMeatballWiki:"; }
body.explicit a.near[title=Wiki]:before { content:"Wiki:"; }
body.nolang span[lang] { background-color:inherit; }

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2004, 2005, 2006 Alex Schroeder <alex@emacswiki.org>
# Copyright (C) 20042023 Alex Schroeder <alex@gnu.org>
# Copyright (C) 2006 Ingo Belka
#
# This program is free software; you can redistribute it and/or modify
@@ -112,9 +112,7 @@ sub DoCollect {
my $search = GetParam('search', '');
ReportError(T('The match parameter is missing.')) unless $match or $search;
print GetHeader('', Ts('Page Collection for %s', $match||$search), '');
my @pages = (grep(/$match/, $search
? SearchTitleAndBody($search)
: AllPagesList()));
my @pages = Matched($match, $search ? SearchTitleAndBody($search) : AllPagesList());
if (!$CollectingJournal) {
$CollectingJournal = 1;
# Now save information required for saving the cache of the current page.

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2004, 2007 Alex Schroeder <alex@emacswiki.org>
# Copyright (C) 20042023 Alex Schroeder <alex@gnu.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -47,8 +47,5 @@ sub PrintableIndexPages {
push(@pages, AllPagesList()) if GetParam('pages', 1);
push(@pages, keys %PermanentAnchors) if GetParam('permanentanchors', 1);
push(@pages, keys %NearSource) if GetParam('near', 0);
my $match = GetParam('match', '');
@pages = grep /$match/i, @pages if $match;
@pages = sort @pages;
return @pages;
return sort Matched(GetParam('match'), @pages);
}

View File

@@ -1,4 +1,4 @@
# Copyright (C) 20042021 Alex Schroeder <alex@gnu.org>
# Copyright (C) 20042023 Alex Schroeder <alex@gnu.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -31,6 +31,7 @@ sub DoJournalRss {
local $CollectingJournal = 1;
# Fake the result of GetRcLines()
local *GetRcLines = \&JournalRssGetRcLines;
local *RcSelfWebsite = \&JournalRssSelfWebsite;
local *RcSelfAction = \&JournalRssSelfAction;
local *RcPreviousAction = \&JournalRssPreviousAction;
local *RcLastAction = \&JournalRssLastAction;
@@ -55,6 +56,15 @@ sub JournalRssParameters {
return $more;
}
sub JournalRssSelfWebsite {
my $more = '';
my $search = GetParam('rcfilteronly', '');
$more .= ";search=" . UrlEncode($search) if $search;
my $match = GetParam('match', '');
$more .= ";match=" . UrlEncode($match) if $match;
return $more;
}
sub JournalRssSelfAction {
return "action=journal" . JournalRssParameters(qw(offset));
}
@@ -76,7 +86,7 @@ sub JournalRssGetRcLines {
my $reverse = GetParam('reverse', 0);
my $monthly = GetParam('monthly', 0);
my $offset = GetParam('offset', 0);
my @pages = sort JournalSort (grep(/$match/, $search ? SearchTitleAndBody($search) : AllPagesList()));
my @pages = sort JournalSort (Matched($match, $search ? SearchTitleAndBody($search) : AllPagesList()));
if ($monthly and not $match) {
my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime();
$match = '^' . sprintf("%04d-%02d", $year+1900, $mon+1) . '-\d\d';

View File

@@ -1,4 +1,4 @@
# Copyright (C) 20092020 Alex Schroeder <alex@gnu.org>
# Copyright (C) 20092022 Alex Schroeder <alex@gnu.org>
# Copyright (C) 2015 Aleks-Daniel Jakimenko <alex.jakimenko@gmail.com>
#
# This program is free software; you can redistribute it and/or modify it under
@@ -505,9 +505,9 @@ sub MailUnsubscribe {
=head1 Migrate
The mailmigrate action will migrate your subscription list from the
old format to the new format. This is necessary because these days
because the keys and values of the DB_File are URL encoded.
The mailmigrate action will migrate your subscription list from the old format
to the new format. This is necessary because these days the keys and values of
the DB_File are URL encoded.
=cut

View File

@@ -1,5 +1,5 @@
#! /usr/bin/perl
# Copyright (C) 20142019 Alex Schroeder <alex@gnu.org>
# Copyright (C) 20142022 Alex Schroeder <alex@gnu.org>
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
@@ -194,12 +194,20 @@ sub MarkdownRule {
return OpenHtmlEnvironment('pre',1) . $str; # always level 1
}
# link: [an example](http://example.com/ "Title")
elsif (m/\G\[((?:[^]\n]+\n?)+)\]\($FullUrlPattern(\s+"(.+?)")?\)/cg) {
elsif (m/\G\[((?:[^]\n]+\n?)+)\]\((\S+)(\s+"(.+?)")?\)/cg) {
my ($text, $url, $title) = ($1, $2, $4);
$url =~ /^($UrlProtocols)/;
my %params;
$params{-href} = $url;
$params{-class} = "url $1";
$params{-class} = "url";
$params{-title} = $title if $title;
return $q->a(\%params, $text);
}
# link: [an example](#foo "Title")
elsif (m/\G\[((?:[^]\n]+\n?)+)\]\((#\S)+(\s+"(.+?)")?\)/cg) {
my ($text, $url, $title) = ($1, $2, $4);
my %params;
$params{-href} = $url;
$params{-class} = "named-anchor";
$params{-title} = $title if $title;
return $q->a(\%params, $text);
}

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2012 Alex Schroeder <alex@gnu.org>
# Copyright (C) 20042022 Alex Schroeder <alex@gnu.org>
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software

198
modules/network-blocker.pl Normal file
View File

@@ -0,0 +1,198 @@
# -*- mode: perl -*-
# Copyright (C) 2023 Alex Schroeder <alex@gnu.org>
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <https://www.gnu.org/licenses/>.
=encoding utf8
=head1 NAME
Oddmuse Network Blocker
=head1 DESCRIPTION
This module hooks into regular Oddmuse Surge Protection. It adds the following
features:
Repeated offenders are blocked for increasingly longer times.
For every offender, we record the CIDR their IP number belongs to. Everytime an
IP number is blocked, all the CIDRs of the other blocked IPs are checked: if
there are three or more blocked IP numbers sharing the same CIDRs, the CIDR
itself is blocked.
CIDR blocking works the same way: Repeated offenders are blocked for
increasingly longer times.
=head2 Behind a reverse proxy
Make sure your config file copies the IP number to the correct environment
variable:
$ENV{REMOTE_ADDR} = $ENV{HTTP_X_FORWARDED_FOR};
=head1 SEE ALSO
<Oddmuse Surge Protection|https://oddmuse.org/wiki/Surge_Protection>
=cut
use strict;
use v5.10;
use Net::IP qw(:PROC);
use Net::DNS qw(rr);
AddModuleDescription('network-blocker.pl', 'Network Blocker Extension');
our ($Now, $DataDir, $SurgeProtectionViews, $SurgeProtectionTime);
{
no warnings 'redefine';
*OldNetworkBlockerDelayRequired = \&DelayRequired;
*DelayRequired = \&NewNetworkBlockerDelayRequired;
}
# Block for at least this many seconds.
my $NetworkBlockerMinimumPeriod = 30;
# Every violation doubles the current period until this maximum is reached (four weeks).
my $NetworkBlockerMaximumPeriod = 60 * 60 * 24 * 7 * 4;
# All the blocked networks. Maps CIDR to an array [expiry timestamp, expiry
# period].
my %NetworkBlockerList;
# Candidates are remembered for this many seconds.
my $NetworkBlockerCachePeriod = 600;
# All the candidate networks for a block. Maps IP to an array [ts, cidr, ...].
# Candidates are removed after $NetworkBlockerCachePeriod.
my %NetworkBlockerCandidates;
sub NetworkBlockerRead {
my ($status, $data) = ReadFile("$DataDir/network-blocks");
return unless $status;
my @lines = split(/\n/, $data);
while ($_ = shift(@lines)) {
my @items = split(/,/);
$NetworkBlockerList{shift(@items)} = \@items;
}
# an empty line separates the two sections
while ($_ = shift(@lines)) {
my @items = split(/,/);
$NetworkBlockerCandidates{shift(@items)} = \@items;
}
return 1;
}
sub NetworkBlockerWrite {
RequestLockDir('network-blocks') or return '';
WriteStringToFile(
"$DataDir/network-blocks",
join("\n\n",
join("\n", map {
join(",", $_, @{$NetworkBlockerList{$_}})
} keys %NetworkBlockerList),
join("\n", map {
join(",", $_, @{$NetworkBlockerCandidates{$_}})
} keys %NetworkBlockerCandidates)));
ReleaseLockDir('network-blocks');
}
sub NewNetworkBlockerDelayRequired {
my $ip = shift;
# If $ip is a name and not an IP number, parsing fails. In this case, run the
# regular code.
my $ob = new Net::IP($ip);
return OldNetworkBlockerDelayRequired($ip) unless $ob;
# Read the file. If the file does not exist, no problem.
NetworkBlockerRead();
# See if the current IP number is one of the blocked CIDR ranges.
for my $cidr (keys %NetworkBlockerList) {
# Perhaps this CIDR block can be expired.
if ($NetworkBlockerList{$cidr}->[0] < $Now) {
delete $NetworkBlockerList{$cidr};
next;
}
# Forget the CIDR if it cannot be turned into a range.
my $range = new Net::IP($cidr);
if (not $range) {
warn "CIDR $cidr is blocked but has no range: " . Net::IP::Error();
delete $NetworkBlockerList{$cidr};
next;
}
# If the CIDR overlaps with the remote IP number, it's a block.
warn "Checking whether $ip is in $cidr\n";
my $overlap = $range->overlaps($ob);
# $IP_PARTIAL_OVERLAP (ranges overlap) $IP_NO_OVERLAP (no overlap)
# $IP_A_IN_B_OVERLAP (range2 contains range1) $IP_B_IN_A_OVERLAP (range1
# contains range2) $IP_IDENTICAL (ranges are identical) undef (problem)
if (defined $overlap and $overlap != $IP_NO_OVERLAP) {
# Double the block period unless it has reached $NetworkBlockerMaximumPeriod.
if ($NetworkBlockerList{$cidr}->[1] < $NetworkBlockerMaximumPeriod / 2) {
$NetworkBlockerList{$cidr}->[1] *= 2;
} else {
$NetworkBlockerList{$cidr}->[1] = $NetworkBlockerMaximumPeriod;
}
$NetworkBlockerList{$cidr}->[0] = $Now + $NetworkBlockerList{$cidr}->[1];
# And we're done!
NetworkBlockerWrite();
ReportError(Ts('Too many connections by %s', $cidr)
. ': ' . Tss('Please do not fetch more than %1 pages in %2 seconds.',
$SurgeProtectionViews, $SurgeProtectionTime),
'503 SERVICE UNAVAILABLE');
}
}
# If the CIDR isn't blocked, let's see if Surge Protection wants to block it.
my $result = OldNetworkBlockerDelayRequired($ip);
warn "$ip was blocked\n" if $result;
# If the IP is to be blocked, determine its CIDRs and put them on a list. Sadly,
# routeviews does not support IPv6 at the moment!
if ($result and not ip_is_ipv6($ip) and not $NetworkBlockerCandidates{$ip}) {
my $reverse = $ob->reverse_ip();
$reverse =~ s/in-addr\.arpa\.$/asn.routeviews.org/;
my @candidates;
for my $rr (rr($reverse, "TXT")) {
next unless $rr->type eq "TXT";
my @data = $rr->txtdata;
push(@candidates, join("/", @data[1..2]));
}
warn "$ip is in @candidates\n";
$NetworkBlockerCandidates{$ip} = [$Now, @candidates];
# Expire any of the other candidates
for my $other_ip (keys %NetworkBlockerCandidates) {
if ($NetworkBlockerCandidates{$other_ip}->[0] < $Now - $NetworkBlockerCachePeriod) {
delete $NetworkBlockerCandidates{$other_ip};
}
}
# Determine if any of the CIDRs is to be blocked.
my $save;
for my $cidr (@candidates) {
# Count how often the candidate CIDRs show up for other IP numbers.
my $count = 0;
for my $other_ip (keys %NetworkBlockerCandidates) {
my @data = $NetworkBlockerCandidates{$other_ip};
for my $other_cidr (@data[1 .. $#data]) {
$count++ if $cidr eq $other_cidr;
}
}
if ($count >= 3) {
$NetworkBlockerList{$cidr} = [$Now + $NetworkBlockerMinimumPeriod, $NetworkBlockerMinimumPeriod];
$save = 1;
}
}
NetworkBlockerWrite() if $save;
}
return $result;
}

View File

@@ -22,21 +22,18 @@ our ($q, @MyAdminCode);
*OldGetSearchLink = \&GetSearchLink;
*GetSearchLink = \&NewGetSearchLink;
sub NewGetSearchLink {
my ($text, $class, $name, $title) = @_;
$name = UrlEncode($name);
$text =~ s/_/ /g;
return $q->span({-class=>$class}, $text);
my ($id, $class, $name, $title) = @_;
return NormalToFree($id);
}
push(@MyAdminCode, \&BacklinksMenu);
sub BacklinksMenu {
my ($id, $menuref, $restref) = @_;
if ($id) {
my $text = T('Backlinks');
my $class = 'backlinks';
my $name = 'backlinks';
my $title = T('Click to search for references to this page');
my $link = ScriptLink('search=' . $id, $text, $class, $name, $title);
push(@$menuref, $link);
my $form = GetFormStart(undef, 'post', 'search');
$form .= $q->input({-type=>'hidden', -name=>'search', -value=>'"'.NormalToFree($id).'"'});
$form .= $q->p(T('Click to search for references to this page'));
$form .= $q->submit('search', T('Go!')) . $q->end_form;
push(@$menuref, $form);
}
}

View File

@@ -0,0 +1,166 @@
#! /usr/bin/perl
# Copyright (C) 2025 Alex Schroeder <alex@gnu.org>
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
use strict;
use v5.10;
use utf8;
AddModuleDescription('post-instead-of-get.pl', 'POST instead of GET extension');
our ($q, $Now, $LastUpdate, %Action, @RcDays, $RcDefault, $ShowRollbacks, $ShowAll,
$ShowEdits, %Languages, $FullUrl);
# You should install nosearch.pl, too.
# Change the search from GET to POST
*PostOldGetSearchForm=*GetSearchForm;
*GetSearchForm=*PostNewGetSearchForm;
sub PostNewGetSearchForm {
my $html = PostOldGetSearchForm(@_);
$html =~ s/method="get"/method="post"/;
return $html;
}
# Change the index filter from GET to POST
*PostOldDoIndex=*DoIndex;
*DoIndex=*PostNewDoIndex;
# Update action hash as well!
$Action{index} = \&DoIndex;
sub PostNewDoIndex {
# Must capture the output.
my $html = ToString(\&PostOldDoIndex);
$html =~ s/method="get"/method="post"/;
print $html;
}
# Disable links in the Recent Changes menu
*PostOldRcHeader=*RcHeader;
*RcHeader=*PostNewRcHeader;
sub PostNewRcHeader {
my ($from, $upto, $html) = (GetParam('from', 0), GetParam('upto', 0), '');
my $days = GetParam('days') + 0 || $RcDefault; # force numeric $days
my $all = GetParam('all', $ShowAll);
if ($from) {
$html .= $q->h2(Ts('Updates since %s', TimeToText(GetParam('from', 0))) . ' '
. ($upto ? Ts('up to %s', TimeToText($upto)) : ''));
} else {
$html .= $q->h2((GetParam('days', $RcDefault) != 1)
? Ts('Updates in the last %s days', $days)
: Ts('Updates in the last day'));
}
$html .= $q->p({-class => 'documentation'}, T('Using the 「rollback」 button on this page will reset the wiki to that particular point in time, undoing any later changes to all of the pages.')) if UserIsAdmin() and $all;
return $html;
}
# Change the More... link
*PostOldRcHtml=*RcHtml;
*RcHtml=*PostNewRcHtml;
sub PostNewRcHtml {
my $html = PostOldRcHtml(@_);
# Based on RcPreviousAction
my $form = GetFormStart(undef, 'post', 'more');
my $interval = GetParam('days', $RcDefault) * 86400;
# use delta between from and upto, or use days, whichever is available
my $to = GetParam('from', GetParam('upto', $Now - $interval));
my $from = $to - (GetParam('upto') ? GetParam('upto') - GetParam('from') : $interval);
$form .= $q->input({-type=>'hidden', -name=>'action', -value=>'rc'});
$form .= $q->input({-type=>'hidden', -name=>'from', -value=>$from});
$form .= $q->input({-type=>'hidden', -name=>'upto', -value=>$to});
# Based on RcOtherParameters
foreach (qw(days page diff full all showedit rollback rcidonly rcuseronly rchostonly rcclusteronly rcfilteronly match lang followup)) {
my $val = GetParam($_, '');
$form .= $q->input({-type=>'hidden', -name=>$_, -value=>$val}) if $val;
}
$form .= $q->submit('more', T('More...'));
$form .= $q->end_form();
$html =~ s/<p class="more">.*?<\/p>//;
return $html . $form;
}
# Change Recent Changes filter form to represent all options.
*PostOldGetFilterForm=*GetFilterForm;
*GetFilterForm=*PostNewGetFilterForm;
sub PostNewGetFilterForm {
my $all = GetParam('all', $ShowAll);
my $showedit = GetParam('showedit', $ShowEdits);
my $rollback = GetParam('rollback', $ShowRollbacks);
my $lang = GetParam('lang', '');
my $form = GetFormStart(undef, 'post', 'filter') . $q->h2(T('Filters'));
$form .= $q->input({-type=>'hidden', -name=>'action', -value=>'rc'});
$form .= $q->radio_group(-name=>'days', -values=>\@RcDays, -default=> $RcDefault) . ' ' . T('days') . $q->br();
$form .= $q->input({-type=>'checkbox', -id=>'all', -name=>'all', -value=>1, $all && '-checked'});
$form .= $q->label({-for=>'all'}, ' ' . T('List all changes')) . $q->br();
$form .= $q->input({-type=>'checkbox', -id=>'showedit', -name=>'showedit', -value=>1, $showedit && '-checked'});
$form .= $q->label({-for=>'showedit'}, ' ' . T('Include minor changes')) . $q->br();
$form .= $q->input({-type=>'checkbox', -id=>'rollback', -name=>'rollback', -value=>1, $rollback && '-checked'});
$form .= $q->label({-for=>'rollback'}, ' ' . T('Include rollbacks')) . $q->br();
foreach my $h (['match' => T('Title:')], ['rcfilteronly' => T('Title and Body:')],
['rcuseronly' => T('Username:')], ['rchostonly' => T('Host:')], ['followup' => T('Follow up to:')]) {
$form .= $q->label({-for=>$h->[0], -style=>'width:20ch; display:inline-block'}, $h->[1]);
$form .= $q->textfield(-name=>$h->[0], -id=>$h->[0], -size=>20);
$form .= $q->br();
}
if (%Languages) {
$form .= $q->label({-for=>'rclang', -style=>'width:20ch; display:inline-block'}, T('Language:'));
$form .= $q->textfield(-name=>'lang', -id=>'rclang', -size=>20, -default=>$lang);
}
$form .= $q->br() . $q->submit('dofilter', T('Go!')) . $q->end_form;
$form .= GetFormStart(undef, 'post', 'later');
$form .= $q->input({-type=>'hidden', -name=>'action', -value=>'rc'});
$form .= $q->input({-type=>'hidden', -name=>'all', -value=>1}) if $all;
$form .= $q->input({-type=>'hidden', -name=>'showedit', -value=>1}) if $showedit;
$form .= $q->input({-type=>'hidden', -name=>'from', -value=>$LastUpdate+1});
$form .= $q->p(T('List later changes') . ' ' . $q->submit('dofilter', T('Go!')));
$form .= $q->end_form;
$form .= $q->p({-class => 'documentation'}, T('Using the 「rollback」 button on this page will reset the wiki to that particular point in time, undoing any later changes to all of the pages.')) if UserIsAdmin() and $all;
return $form;
}
# History page with new form
*PostOldGetFooterLinks=*GetFooterLinks;
*GetFooterLinks=*PostNewGetFooterLinks;
sub PostNewGetFooterLinks {
my $html = PostOldGetFooterLinks(@_);
my ($id, $rev) = @_;
if ($Action{history} and $rev ne '') {
my $label = T('View all changes');
my $unwanted = quotemeta(GetRCLink($id, $label));
my $form = qq{
<form style="display: inline" method="POST" action="$FullUrl">
<input type="hidden" name="action" value="rc"/>
<input type="hidden" name="all" value="1"/>
<input type="hidden" name="showedit" value="1"/>
<input type="hidden" name="from" value="1"/>
<input type="hidden" name="rcidonly" value="$id"/>
<input type="submit" name="dobacklinks" value="$label">
</form>};
$html =~ s/$unwanted/$form/;
}
return $html;
}
1;

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2019 Alex Schroeder <alex@gnu.org>
# Copyright (C) 20192023 Alex Schroeder <alex@gnu.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -65,6 +65,7 @@ sub RenamePageMenu {
. GetHiddenValue('id', $id)
. $q->textfield(-name=>'to', -size=>20)
. ' '
. $q->submit('Do it'));
. $q->submit('Do it')
. $q->end_form());
}
}

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2006, 2007, 2008 Alex Schroeder <alex@emacswiki.org>
# Copyright (C) 20062023 Alex Schroeder <alex@gnu.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -36,18 +36,12 @@ sub SearchListRule {
$term = GetId();
}
local ($OpenPageName, %Page);
my %hash = ();
my @found;
if ($variation eq 'list') {
foreach my $id (SearchTitleAndBody($term)) {
$hash{$id} = 1 unless $id eq $original; # skip the page with the query
}
@found = grep { $_ ne $original } SearchTitleAndBody($term);
} elsif ($variation eq 'titlelist') {
@found = grep { $_ ne $original } Matched($term, AllPagesList());
}
if ($variation eq 'titlelist') {
foreach my $id (grep(/$term/, AllPagesList())) {
$hash{$id} = 1 unless $id eq $original; # skip the page with the query
}
}
my @found = keys %hash;
if (defined &PageSort) {
@found = sort PageSort @found;
} else {
@@ -63,32 +57,24 @@ sub SearchListRule {
return;
}
# Add a new action list
$Action{list} = \&DoList;
sub DoList {
my $id = shift;
my $match = GetParam('match', '');
my $search = GetParam('search', '');
my $id = shift;
my $match = GetParam('match', '');
my $search = GetParam('search', '');
ReportError(T('The search parameter is missing.')) unless $match or $search;
print GetHeader('', Ts('Page list for %s', $match||$search), '');
local (%Page, $OpenPageName);
my %hash = ();
foreach my $id (grep(/$match/, $search
? SearchTitleAndBody($search)
: AllPagesList())) {
$hash{$id} = 1;
}
my @found = keys %hash;
if (defined &PageSort) {
@found = sort PageSort @found;
} else {
@found = sort(@found);
}
@found = map { $q->li(GetPageLink($_)) } @found;
print $q->start_div({-class=>'search list'}),
$q->ul(@found), $q->end_div;
my @found = Matched($match, $search ? SearchTitleAndBody($search) : AllPagesList());
if (defined &PageSort) {
@found = sort PageSort @found;
} else {
@found = sort(@found);
}
@found = map { $q->li(GetPageLink($_)) } @found;
print $q->start_div({-class=>'search list'}), $q->ul(@found), $q->end_div;
PrintFooter();
}

View File

@@ -1,4 +1,4 @@
# Copyright (C) 20072014 Alex Schroeder <alex@gnu.org>
# Copyright (C) 20072023 Alex Schroeder <alex@gnu.org>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
@@ -36,8 +36,7 @@ sub SisterPages {
push(@pages, AllPagesList()) if GetParam('pages', 1);
push(@pages, keys %PermanentAnchors) if GetParam('permanentanchors', 1);
push(@pages, keys %NearSource) if GetParam('near', 0);
my $match = GetParam('match', '');
@pages = grep /$match/i, @pages if $match;
@pages = Matched(GetParam('match', ''), @pages);
@pages = sort @pages;
return @pages;
}

View File

@@ -1667,15 +1667,7 @@ Upgrade complete. Please remove $ModuleDir/upgade.pl, now.
################################################################################
# modules/usemod.pl
################################################################################
http://search.barnesandnoble.com/booksearch/isbninquiry.asp?ISBN=%s
http://www.amazon.com/exec/obidos/ISBN=%s
alternate
http://www.pricescan.com/books/BookDetail.asp?isbn=%s
search
https://en.wikipedia.org/wiki/Special:BookSources/%s
################################################################################
# modules/wanted.pl

View File

@@ -213,13 +213,8 @@ sub ISBN {
$num =~ s/[- ]//g;
my $len = length($num);
return "ISBN $rawnum" unless $len == 10 or $len == 13 or $len = 14; # be prepared for 2007-01-01
my $first = $q->a({-href => Ts('http://search.barnesandnoble.com/booksearch/isbninquiry.asp?ISBN=%s', $num)},
my $html = $q->a({-href => Ts("https://en.wikipedia.org/wiki/Special:BookSources/%s", $num)},
"ISBN " . $rawprint);
my $second = $q->a({-href => Ts('http://www.amazon.com/exec/obidos/ISBN=%s', $num)},
T('alternate'));
my $third = $q->a({-href => Ts('http://www.pricescan.com/books/BookDetail.asp?isbn=%s', $num)},
T('search'));
my $html = "$first ($second, $third)";
$html .= ' ' if ($rawnum =~ / $/); # Add space if old ISBN had space.
return $html;
}

View File

@@ -23,7 +23,7 @@
use strict;
use v5.10;
our (@MyInitVariables, $HtmlHeaders);
our (@MyInitVariables, $HtmlHeaders, $EditNote);
AddModuleDescription('wordcount.pl', 'Word Count Extension');
@@ -57,3 +57,5 @@ sub WordcountAddScript {
}
</script>";
}
$EditNote = "Words: <span id='textWordCount'></span>" . $EditNote;

View File

@@ -85,7 +85,9 @@ versions of Oddmuse.</p>
<ul>
% for my $tarball (@$tarballs) {
<li>
% if ($tarball ne 'latest') {
<a href="https://oddmuse.org/releases/<%= $tarball %>.tar.gz"><%= $tarball %>.tar.gz</a>
% }
(files for <%= link_to release => {tarball => $tarball} => begin %>\
<%= $tarball =%><%= end %>)
</li>

View File

@@ -675,10 +675,10 @@ sub gemini_text {
$block =~ s/\[\[tag:([^]|]+)\]\]/push(@links, $self->gemini_link("tag\/$1", $1)); $1/ge;
$block =~ s/\[\[tag:([^]|]+)\|([^\]|]+)\]\]/push(@links, $self->gemini_link("tag\/$1", $2)); $2/ge;
$block =~ s/<journal search tag:(\S+)>\n*/push(@links, $self->gemini_link("tag\/$1", "Explore the $1 tag")); ""/ge;
$block =~ s/\[\[image:([^]|]+)\]\]/push(@links, $self->gemini_link($1, "$1 (image)")); "$1"/ge;
$block =~ s/\[\[image:([^]|]+)\|([^\]|]+)\]\]/push(@links, $self->gemini_link($1, "$2 (image)")); "$2"/ge;
$block =~ s/\[\[image:([^]|]+)\|([^\]|]*)\|([^\]|]+)\]\]/push(@links, $self->gemini_link($1, "$2 (image)"), $self->gemini_link($3, "$2 (follow-up)")); "$2"/ge;
$block =~ s/\[\[image:([^]|]+)\|([^\]|]*)\|([^\]|]*)\|([^\]|]+)\]\]/push(@links, $self->gemini_link($1, "$2 (image)"), $self->gemini_link($3, "$4 (follow-up)")); "$2"/ge;
$block =~ s/\[\[image(?:\/right)?:([^]|]+)\]\]/push(@links, $self->gemini_link($1, "$1 (image)")); "$1"/ge;
$block =~ s/\[\[image(?:\/right)?:([^]|]+)\|([^\]|]+)\]\]/push(@links, $self->gemini_link($1, "$2 (image)")); "$2"/ge;
$block =~ s/\[\[image(?:\/right)?:([^]|]+)\|([^\]|]*)\|([^\]|]+)\]\]/push(@links, $self->gemini_link($1, "$2 (image)"), $self->gemini_link($3, "$2 (follow-up)")); "$2"/ge;
$block =~ s/\[\[image(?:\/right)?:([^]|]+)\|([^\]|]*)\|([^\]|]*)\|([^\]|]+)\]\]/push(@links, $self->gemini_link($1, "$2 (image)"), $self->gemini_link($3, "$4 (follow-up)")); "$2"/ge;
$block =~ s/\[\[$FreeLinkPattern\|([^\]|]+)\]\]/push(@links, $self->gemini_link($1, $2)); $2/ge;
$block =~ s/\[\[$FreeLinkPattern\]\]/push(@links, $self->gemini_link($1)); $1/ge;
$block =~ s/\[color=([^]]+)\]/color($1)/ge;

View File

@@ -137,13 +137,13 @@ EOT
xpath_run_tests(split(/\n/,<<'EOT'));
[example](http://example.com/)
//a[@class="url http"][@href="http://example.com/"][text()="example"]
//a[@class="url"][@href="http://example.com/"][text()="example"]
[an example](http://example.com/)
//a[@class="url http"][@href="http://example.com/"][text()="an example"]
//a[@class="url"][@href="http://example.com/"][text()="an example"]
[an example](http://example.com/ "Title")
//a[@class="url http"][@href="http://example.com/"][@title="Title"][text()="an example"]
//a[@class="url"][@href="http://example.com/"][@title="Title"][text()="an example"]
[an\nexample](http://example.com/)
//a[@class="url http"][@href="http://example.com/"][text()="an\nexample"]
//a[@class="url"][@href="http://example.com/"][text()="an\nexample"]
\n[an\n\nexample](http://example.com/)
//p[text()="[an"]/following-sibling::p//text()[contains(string(),"example](")]
EOT

26
t/post-instead-of-get.t Normal file
View File

@@ -0,0 +1,26 @@
# Copyright (C) 2025 Alex Schroeder <alex@gnu.org>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
require './t/test.pl';
package OddMuse;
use Test::More tests => 6;
add_module('nosearch.pl');
add_module('post-instead-of-get.pl');
like(get_page('HomePage'), qr/<h1>HomePage<\/h1>/, "no link in the title");
my $page = get_page('RecentChanges');
for my $day (@RcDays) {
like($page, qr/$day/, "$day days found");
}

58
t/rollback-hang.t Normal file
View File

@@ -0,0 +1,58 @@
# Copyright (C) 20062023 Alex Schroeder <alex@gnu.org>
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
require './t/test.pl';
package OddMuse;
use Test::More tests => 4;
use utf8;
# Reproduce a particular bug from alexschroeder.ch with the rc.log provided.
WriteStringToFile($RcFile, <<'EOT');
16853910992023-05-29_Net_newsHow to IRCAnonymousAlex2en
16854004152023-05-29_Net_newsHow to IRCAnonymousAlex3en
1685430599[[rollback]]1685400415Anonymous
16855185032023-05-29_Net_newsAnonymousAlex4en
EOT
local $SIG{ALRM} = sub { fail "timeout!"; kill 'KILL', $$; };
alarm 3;
# this is recent changes from between the rollback and the page before it, so there are no pages to roll back
my $page = get_page("action=rss full=1 short=0 from=1685413682");
alarm 0;
test_page($page, '2023-05-29 Net news');
test_page_negative($page, 'rollback');
# Reproduce a follow-up bug. First, rolling back just Test works as intended.
WriteStringToFile($RcFile, <<'EOT');
1691499987Testham127.0.0.1Berta1
1691499988Mustuff127.0.0.1Chris1
1691499989Testspam127.0.0.1Spammer2
1691499990Test0Rollback to 2023-08-08 13:06 UTC127.0.0.1Alex3
1691499990[[rollback]]1691499987Test
EOT
my $feed = get_page('action=rc raw=1 from=1691499900'); # need from or the result is empty
test_page($feed, 'title: Test');
# Rolling back all of the wiki doesn't work.
WriteStringToFile($RcFile, <<'EOT');
1691499987Testham127.0.0.1Berta1
1691499988Mustuff127.0.0.1Chris1
1691499989Testspam127.0.0.1Spammer2
1691499990Test0Rollback to 2023-08-08 13:06 UTC127.0.0.1Alex3
1691499990[[rollback]]1691499987
EOT
$feed = get_page('action=rc raw=1 from=1691499900'); # need from or the result is empty
test_page($feed, 'title: Test');

View File

@@ -129,8 +129,8 @@ test_page(update_page('Testing', 'This is spam.'), 'This page does not exist');
test_page(update_page('Spam', 'Trying again.'), 'This page does not exist');
test_page(get_page('action=translate id=Spam target=Harmless translation=en'),
'Edit Denied',
'Regular expression "spam" matched on this page');
'Regular expression "spam" matched "Spam" on this page');
test_page(get_page('Spam'), 'This page does not exist');
test_page(get_page('action=translate id=Harmless target=Spam translation=en'),
'Edit Denied',
'Regular expression "spam" matched on this page');
'Regular expression "spam" matched "Spam" on this page');

100
wiki.pl
View File

@@ -1,5 +1,5 @@
#! /usr/bin/env perl
# Copyright (C) 2001-2020
# Copyright (C) 2001-2023
# Alex Schroeder <alex@gnu.org>
# Copyright (C) 2014-2015
# Alex Jakimenko <alex.jakimenko@gmail.com>
@@ -391,7 +391,7 @@ sub InitLinkPatterns {
my $EndChars = '[-a-zA-Z0-9/@=+$_~*]'; # no punctuation at the end of the url.
$UrlPattern = "((?:$UrlProtocols):$UrlChars+$EndChars)";
$FullUrlPattern="((?:$UrlProtocols):$UrlChars+)"; # when used in square brackets
$ImageExtensions = '(gif|jpg|jpeg|png|bmp|svg)';
$ImageExtensions = '(gif|jpg|jpeg|png|bmp|svg|webp)';
}
sub Clean {
@@ -514,7 +514,7 @@ sub ApplyRules {
Clean(CloseHtmlEnvironments() . AddHtmlEnvironment('p')); # another one like this further up
} elsif (m/\G&amp;([A-Za-z]+|#[0-9]+|#x[A-Za-f0-9]+);/cg) { # entity references
Clean("&$1;");
} elsif (m/\G\s+/cg) {
} elsif (m/\G[ \t\r\n]+/cg) { # don't use \s because we want to honor NO-BREAK SPACE etc
Clean(' ');
} elsif (m/\G([A-Za-z\x{0080}-\x{fffd}]+([ \t]+[a-z\x{0080}-\x{fffd}]+)*[ \t]+)/cg
or m/\G([A-Za-z\x{0080}-\x{fffd}]+)/cg or m/\G(\S)/cg) {
@@ -1313,7 +1313,7 @@ sub GetId {
SetParam($p, 1); # script/p/q -> p=1
}
}
return $id;
return FreeToNormal($id);
}
sub DoBrowseRequest {
@@ -1543,28 +1543,34 @@ sub LatestChanges {
sub StripRollbacks {
my @result = @_;
if (not (GetParam('all', $ShowAll) or GetParam('rollback', $ShowRollbacks))) { # strip rollbacks
my (%rollback);
my (%rollback); # used for single-page rollbacks
for (my $i = $#result; $i >= 0; $i--) {
# some fields have a different meaning if looking at rollbacks
my ($ts, $id, $target_ts, $target_id) = @{$result[$i]};
# if this is a rollback marker
if ($id eq '[[rollback]]') {
# if this is a single page rollback marker, strip it
if ($target_id) {
$rollback{$target_id} = $target_ts; # single page rollback
splice(@result, $i, 1); # strip marker
# if this page is not already being rolled back, remember the target
# id and target ts so that those lines can be stripped below
if (not $rollback{$target_id} or $target_ts < $rollback{$target_id}) {
$rollback{$target_id} = $target_ts;
}
# the marker is always stripped
splice(@result, $i, 1);
} else {
# if this is a global rollback, things are different: we're going to
# find the correct timestamp and strip all of those lines immediately
my $end = $i;
while ($ts > $target_ts and $i > 0) {
$i--; # quickly skip all these lines
$ts = $result[$i][0];
}
splice(@result, $i + 1, $end - $i);
$i++; # compensate $i-- in for loop
$i-- while $i > 0 and $target_ts < $result[$i-1][0];
# splice the lines found
splice(@result, $i, $end - $i + 1);
}
} elsif ($rollback{$id} and $ts > $rollback{$id}) {
splice(@result, $i, 1); # strip rolled back single pages
}
}
} else { # just strip the marker left by DoRollback()
} else { # if rollbacks are not not shown, just strip the markers
for (my $i = $#result; $i >= 0; $i--) {
splice(@result, $i, 1) if $result[$i][1] eq '[[rollback]]'; # id
}
@@ -1712,6 +1718,11 @@ sub RcOtherParameters {
return $more;
}
sub RcSelfWebsite {
my $action = 'rc';
return "action=$action" . RcOtherParameters(qw(from upto days));
}
sub RcSelfAction {
my $action = GetParam('action', 'rc');
return "action=$action" . RcOtherParameters(qw(from upto days));
@@ -1898,7 +1909,7 @@ sub GetRcRss {
};
my $title = QuoteHtml($SiteName) . ': ' . GetParam('title', QuoteHtml(NormalToFree($HomePage)));
$rss .= "<title>$title</title>\n";
$rss .= "<link>$ScriptName?" . RcSelfAction() . "</link>\n";
$rss .= "<link>$ScriptName?" . RcSelfWebsite() . "</link>\n";
$rss .= qq{<atom:link href="$ScriptName?} . RcSelfAction() . qq{" rel="self" type="application/rss+xml" />\n};
$rss .= qq{<atom:link href="$ScriptName?} . RcPreviousAction() . qq{" rel="previous" type="application/rss+xml" />\n};
$rss .= qq{<atom:link href="$ScriptName?} . RcLastAction() . qq{" rel="last" type="application/rss+xml" />\n};
@@ -1918,7 +1929,7 @@ sub GetRcRss {
$rss .= "<image>\n";
$rss .= "<url>$RssImageUrl</url>\n";
$rss .= "<title>$title</title>\n"; # the same as the channel
$rss .= "<link>$ScriptName?" . RcSelfAction() . "</link>\n"; # the same as the channel
$rss .= "<link>$ScriptName?" . RcSelfWebsite() . "</link>\n"; # the same as the channel
$rss .= "</image>\n";
}
my $limit = GetParam("rsslimit", 15); # Only take the first 15 entries
@@ -2552,23 +2563,30 @@ sub GetFormStart {
}
sub GetSearchForm {
my $html = GetFormStart(undef, 'get', 'search') . $q->start_p;
$html .= $q->label({-for=>'search'}, T('Search:')) . ' '
. $q->textfield(-name=>'search', -id=>'search', -size=>15, -accesskey=>T('f')) . ' ';
if (GetParam('search') ne '' and UserIsAdmin()) { # see DoBrowseRequest
$html .= $q->label({-for=>'replace'}, T('Replace:')) . ' '
. $q->textfield(-name=>'replace', -id=>'replace', -size=>20) . ' '
. $q->label({-for=>'delete', -title=>'If you want to replace matches with the empty string'}, T('Delete')) . ' '
. $q->input({-type=>'checkbox', -name=>'delete'})
. $q->submit('preview', T('Preview'));
my $html = GetFormStart(undef, 'get', 'search');
my $replacing = (GetParam('search') ne '' and UserIsAdmin());
$html .= $q->start_p({-class => ($replacing ? 'replace' : 'search')});
$html .= $q->span({-class=>'search'},
$q->label({-for=>'search'}, T('Search:')) . ' '
. $q->textfield(-name=>'search', -id=>'search', -size=>15, -accesskey=>T('f'))) . ' ';
if ($replacing) { # see DoBrowseRequest
$html .= $q->span({-class=>'replace'},
$q->label({-for=>'replace'}, T('Replace:')) . ' '
. $q->textfield(-name=>'replace', -id=>'replace', -size=>20)) . ' '
. $q->span({-class=>'delete'},
$q->label({-for=>'delete', -title=>'If you want to replace matches with the empty string'}, T('Delete')) . ' '
. $q->input({-type=>'checkbox', -name=>'delete'})) . ' '
. $q->submit('preview', T('Preview')) . ' ';
}
if (GetParam('matchingpages', $MatchingPages)) {
$html .= $q->label({-for=>'matchingpage'}, T('Filter:')) . ' '
. $q->textfield(-name=>'match', -id=>'matchingpage', -size=>15) . ' ';
$html .= $q->span({-class=>'match'},
$q->label({-for=>'matchingpage'}, T('Filter:')) . ' '
. $q->textfield(-name=>'match', -id=>'matchingpage', -size=>15)) . ' ';
}
if (%Languages) {
$html .= $q->label({-for=>'searchlang'}, T('Language:')) . ' '
. $q->textfield(-name=>'lang', -id=>'searchlang', -size=>5, -default=>GetParam('lang', '')) . ' ';
$html .= $q->span({-class=>'lang'},
$q->label({-for=>'searchlang'}, T('Language:')) . ' '
. $q->textfield(-name=>'lang', -id=>'searchlang', -size=>5, -default=>GetParam('lang', ''))) . ' ';
}
$html .= $q->submit('dosearch', T('Go!')) . $q->end_p . $q->end_form;
return $html;
@@ -3356,7 +3374,6 @@ sub SortIndex {
sub DoIndex {
my $raw = GetParam('raw', 0);
my $match = GetParam('match', '');
my $limit = GetParam('n', '');
my @pages = ();
my @menu = ($q->label({-for=>'indexmatch'}, T('Filter:')) . ' '
@@ -3368,7 +3385,7 @@ sub DoIndex {
push(@pages, $sub->()) if $value;
push(@menu, $q->checkbox(-name=>$option, -checked=>$value, -label=>$text));
}
@pages = grep /$match/i, @pages if $match;
@pages = Matched(GetParam('match', ''), @pages);
@pages = sort SortIndex @pages;
@pages = @pages[0 .. $limit - 1] if $limit;
if ($raw) {
@@ -3541,11 +3558,24 @@ sub SearchTitleAndBody {
return @found;
}
sub Filtered { # this is overwriten in extensions such as tags.pl
# Filter the pages to be searched for $string. The default implementation
# ignores $string and uses $match instead, just in case the user used both
# search and match parameters. This is overwritten in extensions such as tags.pl
# which extract tags from $string and use that to filter the pages.
sub Filtered {
my ($string, @pages) = @_;
my $match = GetParam('match', '');
@pages = grep /$match/i, @pages if $match;
return @pages;
return Matched(GetParam('match', ''), @pages);
}
sub Matched { # strictly for page titles
my ($string, @pages) = @_;
return @pages unless $string;
my @terms = grep { $_ } split(/[ _]+/, $string);
return grep {
my $id = $_;
for (@terms) { return unless $id =~ /$_/i }
return $id;
} @pages;
}
sub SearchString {