forked from github/kensanata.oddmuse
Compare commits
42 Commits
namespaces
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aba9fcfa40 | ||
|
|
16eb72c8db | ||
|
|
4f99e2c4bd | ||
|
|
0b993a002a | ||
|
|
f5cb40d21c | ||
|
|
97bc55bef3 | ||
|
|
a30f9bb40c | ||
|
|
35c04beb2a | ||
|
|
c7a37261d1 | ||
|
|
64bc459a3e | ||
|
|
c7e563d02f | ||
|
|
d489281f5c | ||
|
|
cc7240dc98 | ||
|
|
726dfc2d5d | ||
|
|
23e1cceead | ||
|
|
6c3eb92fff | ||
|
|
a3ef9c2040 | ||
|
|
4e16082b70 | ||
|
|
6234b05a50 | ||
|
|
567ea8e0a8 | ||
|
|
0974b7bbd8 | ||
|
|
f73d420957 | ||
|
|
17ef2aaf88 | ||
|
|
b70c8e8def | ||
|
|
f8752e69bc | ||
|
|
9d48f875a2 | ||
|
|
39e9cea7b0 | ||
|
|
e7b718f610 | ||
|
|
261aeccb3f | ||
|
|
a09c846700 | ||
|
|
8dbede3813 | ||
|
|
89d9f27b2a | ||
|
|
f21f257c1b | ||
|
|
48916943a1 | ||
|
|
3b185e5521 | ||
|
|
612af8f7fb | ||
|
|
dc9131e600 | ||
|
|
99af4d984d | ||
|
|
88f4fe3b89 | ||
|
|
851f2f77e8 | ||
|
|
975e15c9f8 | ||
|
|
d235d6ac47 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,3 +9,4 @@
|
||||
wiki.log
|
||||
.prove
|
||||
TAGS
|
||||
/.vscode/
|
||||
|
||||
197
README.creole
197
README.creole
@@ -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
161
README.md
Normal 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.
|
||||
472
css/green.css
472
css/green.css
@@ -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; }
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2004, 2005, 2006 Alex Schroeder <alex@emacswiki.org>
|
||||
# Copyright (C) 2004–2023 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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2004, 2007 Alex Schroeder <alex@emacswiki.org>
|
||||
# Copyright (C) 2004–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 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);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2004–2021 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2004–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 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';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2009–2020 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2009–2022 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
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#! /usr/bin/perl
|
||||
# Copyright (C) 2014–2019 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2014–2022 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);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2012 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2004–2022 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
198
modules/network-blocker.pl
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
166
modules/post-instead-of-get.pl
Normal file
166
modules/post-instead-of-get.pl
Normal 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;
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2019 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2019–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 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2006, 2007, 2008 Alex Schroeder <alex@emacswiki.org>
|
||||
# Copyright (C) 2006–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 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();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2007–2014 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2007–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 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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
26
t/post-instead-of-get.t
Normal 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
58
t/rollback-hang.t
Normal file
@@ -0,0 +1,58 @@
|
||||
# Copyright (C) 2006–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 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');
|
||||
@@ -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
100
wiki.pl
@@ -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&([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 {
|
||||
|
||||
Reference in New Issue
Block a user