forked from github/kensanata.oddmuse
Compare commits
159 Commits
| 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 | ||
|
|
f0d0942bfb | ||
|
|
cd9246ebed | ||
|
|
f7b23d854f | ||
|
|
104a1395e7 | ||
|
|
ceb4c3a9cc | ||
|
|
536757e8e2 | ||
|
|
00af1aa638 | ||
|
|
c2cf3e7b43 | ||
|
|
05c14d37b2 | ||
|
|
fc3614f291 | ||
|
|
e201c77696 | ||
|
|
f280cb5df4 | ||
|
|
29863d7109 | ||
|
|
b514ea7846 | ||
|
|
eeaf615d3b | ||
|
|
f003481c5e | ||
|
|
4d10ef389a | ||
|
|
726ffdced1 | ||
|
|
18c4071da8 | ||
|
|
fd7fa0c3ab | ||
|
|
2ba5b72242 | ||
|
|
fa5a2f7a1a | ||
|
|
ad042630b6 | ||
|
|
9cf35b9b52 | ||
|
|
4f69103b8c | ||
|
|
37c882780a | ||
|
|
6d5f97e1ba | ||
|
|
4b1063c699 | ||
|
|
b891674a6f | ||
|
|
1a65df6e36 | ||
|
|
6043be852c | ||
|
|
bb11bdf789 | ||
|
|
540fd588c9 | ||
|
|
278fad1f43 | ||
|
|
eadeb460f5 | ||
|
|
5da9ce64c0 | ||
|
|
40498b53f7 | ||
|
|
eaf97602ff | ||
|
|
987c262425 | ||
|
|
c33ee0a9e6 | ||
|
|
eb7665661f | ||
|
|
72ae1bf56f | ||
|
|
8f30ed8109 | ||
|
|
19e71f1180 | ||
|
|
9397a38394 | ||
|
|
17bd2d08cd | ||
|
|
47a5e81000 | ||
|
|
7bfe740fb2 | ||
|
|
6a324b59b9 | ||
|
|
23545006a5 | ||
|
|
65012eacbb | ||
|
|
91107143f3 | ||
|
|
cafda90555 | ||
|
|
32dfec102d | ||
|
|
c1cdca5f95 | ||
|
|
61dc928e33 | ||
|
|
d43fe3fab9 | ||
|
|
3acb572c0d | ||
|
|
0f6787d349 | ||
|
|
af287a1279 | ||
|
|
6bbd43f8a3 | ||
|
|
364d7c695b | ||
|
|
871af41881 | ||
|
|
4648bfbd83 | ||
|
|
129d02850b | ||
|
|
ee23ef509c | ||
|
|
7e865696b0 | ||
|
|
244d06ca3b | ||
|
|
1a59075b51 | ||
|
|
e0b3c18499 | ||
|
|
5434136a4d | ||
|
|
aeeb182dad | ||
|
|
4d6882ffc7 | ||
|
|
828482f439 | ||
|
|
2b6f2dfa0c | ||
|
|
413b43174c | ||
|
|
9709c87185 | ||
|
|
0cca358de2 | ||
|
|
acff0cb69f | ||
|
|
8d5956cb7f | ||
|
|
e9773ea694 | ||
|
|
7094ec098b | ||
|
|
04fe4d6991 | ||
|
|
570a6b1f07 | ||
|
|
4211d6aa03 | ||
|
|
5941fcd7e6 | ||
|
|
f3df2eb289 | ||
|
|
8b975cd5e5 | ||
|
|
850f292260 | ||
|
|
ced1252b9d | ||
|
|
c0a6bfebf2 | ||
|
|
36815767f3 | ||
|
|
2cba2d30b7 | ||
|
|
fded17520c | ||
|
|
46580a3958 | ||
|
|
d584899447 | ||
|
|
79c0a9fd02 | ||
|
|
c3f21d60c2 | ||
|
|
43221ea8a9 | ||
|
|
25350e93cb | ||
|
|
415a8aa9a8 | ||
|
|
a3d740aa67 | ||
|
|
1956335640 | ||
|
|
b89c8b99e8 | ||
|
|
4e4d8ee784 | ||
|
|
7ecd7b784e | ||
|
|
0d9764b0de | ||
|
|
5d7964977c | ||
|
|
d50cda9cea | ||
|
|
7257bfb6d5 | ||
|
|
f2470256ae | ||
|
|
3c7a56ba5a | ||
|
|
07d68b4400 | ||
|
|
51d2d3e0ca | ||
|
|
0a361873de | ||
|
|
6af02a8a1e | ||
|
|
dc283ea828 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,3 +9,4 @@
|
||||
wiki.log
|
||||
.prove
|
||||
TAGS
|
||||
/.vscode/
|
||||
|
||||
20
Makefile
20
Makefile
@@ -3,7 +3,7 @@
|
||||
# subdirectory.
|
||||
|
||||
VERSION_NO=$(shell git describe --tags)
|
||||
TRANSLATIONS=$(wildcard modules/translations/[a-z]*-utf8.pl$)
|
||||
TRANSLATIONS=$(wildcard modules/translations/*-utf8.pl)
|
||||
MODULES=$(sort $(wildcard modules/*.pl))
|
||||
BUILD=build/wiki.pl $(foreach file, $(notdir $(MODULES)) $(notdir $(TRANSLATIONS)), build/$(file))
|
||||
|
||||
@@ -60,3 +60,21 @@ jobs ?= 4
|
||||
test:
|
||||
prove t/setup.pl
|
||||
prove --jobs=$(jobs) --state=slow,save t
|
||||
|
||||
# Spin up a quick test
|
||||
|
||||
development:
|
||||
@if grep --quiet 'ScriptName = "http://127.0.0.1:8080";' test-data/config; then \
|
||||
echo Not overwriting \$$ScriptName in test-data/config; \
|
||||
else \
|
||||
echo '$ScriptName = "http://127.0.0.1:8080";' >> test-data/config; \
|
||||
fi
|
||||
morbo --listen http://*:8080 \
|
||||
--watch wiki.pl --watch test-data/config --watch test-data/modules/ \
|
||||
stuff/mojolicious-app.pl
|
||||
|
||||
%.pem:
|
||||
openssl req -new -x509 -days 365 -nodes -out cert.pem -keyout key.pem
|
||||
|
||||
gemini: cert.pem key.pem
|
||||
perl stuff/gemini-server.pl --wiki_cert_file=cert.pem --wiki_key_file=key.pem
|
||||
|
||||
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; }
|
||||
|
||||
|
||||
425
css/latex.css
Normal file
425
css/latex.css
Normal file
@@ -0,0 +1,425 @@
|
||||
/*!
|
||||
* LaTeX.css (https://latex.now.sh/)
|
||||
*
|
||||
* Source: https://github.com/vincentdoerig/latex-css
|
||||
* Licensed under MIT (https://github.com/vincentdoerig/latex-css/blob/master/LICENSE)
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: 'Latin Modern';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-display: swap;
|
||||
src: url('/style/fonts/LM-regular.woff2') format('woff2'),
|
||||
url('/style/fonts/LM-regular.woff') format('woff'),
|
||||
url('/style/fonts/LM-regular.ttf') format('truetype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Latin Modern';
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
font-display: swap;
|
||||
src: url('/style/fonts/LM-italic.woff2') format('woff2'),
|
||||
url('/style/fonts/LM-italic.woff') format('woff'),
|
||||
url('/style/fonts/LM-italic.ttf') format('truetype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Latin Modern';
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
font-display: swap;
|
||||
src: url('/style/fonts/LM-bold.woff2') format('woff2'),
|
||||
url('/style/fonts/LM-bold.woff') format('woff'),
|
||||
url('/style/fonts/LM-bold.ttf') format('truetype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Latin Modern';
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
font-display: swap;
|
||||
src: url('/style/fonts/LM-bold-italic.woff2') format('woff2'),
|
||||
url('/style/fonts/LM-bold-italic.woff') format('woff'),
|
||||
url('/style/fonts/LM-bold-italic.ttf') format('truetype');
|
||||
}
|
||||
|
||||
/* Box sizing rules */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Remove default margin */
|
||||
body,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
p,
|
||||
ul[class],
|
||||
ol[class],
|
||||
li,
|
||||
figure,
|
||||
figcaption,
|
||||
dl,
|
||||
dd {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Make default font-size 1rem and add smooth scrolling to anchors */
|
||||
html {
|
||||
font-size: 1.4rem;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Latin Modern', Georgia, Cambria, 'DejaVu Serif', 'Times New Roman', Times, serif;
|
||||
line-height: 1.4;
|
||||
max-width: 80ch;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.25rem;
|
||||
|
||||
counter-reset: theorem;
|
||||
counter-reset: definition;
|
||||
|
||||
color: hsl(0, 5%, 10%);
|
||||
background-color: hsl(210, 20%, 98%);
|
||||
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
/* Justify and hyphenate all paragraphs */
|
||||
p {
|
||||
text-align: justify;
|
||||
hyphens: auto;
|
||||
-webkit-hyphens: auto;
|
||||
-moz-hyphens: auto;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* A elements that don't have a class get default styles */
|
||||
a:not([class]) {
|
||||
text-decoration-skip-ink: auto;
|
||||
}
|
||||
|
||||
/* Make links red */
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #a00;
|
||||
}
|
||||
a:visited {
|
||||
text-decoration: none;
|
||||
color: #800;
|
||||
}
|
||||
|
||||
a:focus {
|
||||
outline-offset: 2px;
|
||||
outline: 2px solid hsl(220, 90%, 52%);
|
||||
}
|
||||
|
||||
|
||||
/* Ueberschriften mit Links nur dezent einfärben */
|
||||
h1 a, h1 a:visited,
|
||||
h2 a, h2 a:visited,
|
||||
h3 a, h3 a:visited,
|
||||
h4 a, h4 a:visited,
|
||||
h5 a, h5 a:visited,
|
||||
h6 a, h6 a:visited {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* goto bar */
|
||||
div.menu form.search {
|
||||
font-size:75%;
|
||||
margin-top:2em;
|
||||
margin-bottom:3em;
|
||||
}
|
||||
|
||||
div.menu span.gotobar a.local,
|
||||
div.menu span.gotobar a.local:visited {
|
||||
text-decoration: none;
|
||||
color: #1e133c87;
|
||||
margin-right:1.1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Make images easier to work with */
|
||||
img {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Inherit fonts for inputs and buttons */
|
||||
input,
|
||||
button,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
/* Prevent textarea from overflowing */
|
||||
textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Natural flow and rhythm in articles by default */
|
||||
article > * + * {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
/* Styles for inline code or code snippets */
|
||||
code,
|
||||
pre,
|
||||
kbd {
|
||||
font-family: Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||
monospace;
|
||||
font-size: 85%;
|
||||
}
|
||||
pre {
|
||||
padding: 1rem 1.4rem;
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
border-radius: 4px;
|
||||
background: hsl(210, 28%, 93%);
|
||||
}
|
||||
pre code {
|
||||
font-size: 95%;
|
||||
position: relative;
|
||||
}
|
||||
kbd {
|
||||
background: hsl(210, 5%, 100%);
|
||||
border: 1px solid hsl(210, 5%, 70%);
|
||||
border-radius: 2px;
|
||||
padding: 2px 4px;
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
/* Make table 100% width, add borders between rows */
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
td {
|
||||
border-bottom: 1px solid hsl(0, 0%, 85%);
|
||||
}
|
||||
thead th {
|
||||
border-bottom: 2px solid hsl(0, 0%, 70%);
|
||||
}
|
||||
tfoot th {
|
||||
border-top: 2px solid hsl(0, 0%, 70%);
|
||||
}
|
||||
|
||||
/* Center align the title */
|
||||
h1:first-child {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Nested ordered list for ToC */
|
||||
nav ol {
|
||||
counter-reset: item;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
nav li {
|
||||
display: block;
|
||||
}
|
||||
nav li:before {
|
||||
content: counters(item, '.') ' ';
|
||||
counter-increment: item;
|
||||
padding-right: 0.85rem;
|
||||
}
|
||||
|
||||
/* Center definitions (most useful for display equations) */
|
||||
dl dd {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Theorem */
|
||||
.theorem {
|
||||
counter-increment: theorem;
|
||||
display: block;
|
||||
margin: 12px 0;
|
||||
font-style: italic;
|
||||
}
|
||||
.theorem::before {
|
||||
content: 'Satz ' counter(theorem) '. ';
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* Lemma */
|
||||
.lemma {
|
||||
counter-increment: theorem;
|
||||
display: block;
|
||||
margin: 12px 0;
|
||||
font-style: italic;
|
||||
}
|
||||
.lemma::before {
|
||||
content: 'Lemma ' counter(theorem) '. ';
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* Proof */
|
||||
.proof {
|
||||
display: block;
|
||||
margin: 12px 0;
|
||||
font-style: normal;
|
||||
position: relative;
|
||||
}
|
||||
.proof::before {
|
||||
content: 'Beweis. ' attr(title);
|
||||
font-style: italic;
|
||||
}
|
||||
.proof:after {
|
||||
content: '◾️';
|
||||
position: absolute;
|
||||
right: -12px;
|
||||
bottom: -2px;
|
||||
}
|
||||
|
||||
/* Definition */
|
||||
.definition {
|
||||
counter-increment: definition;
|
||||
display: block;
|
||||
margin: 12px 0;
|
||||
font-style: normal;
|
||||
}
|
||||
.definition::before {
|
||||
content: 'Definition ' counter(definition) '. ';
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* Center align author name, use small caps and add vertical spacing */
|
||||
.author {
|
||||
margin: 0.85rem 0;
|
||||
font-variant-caps: small-caps;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Make footnote text smaller and left align it (looks bad with long URLs) */
|
||||
.footnotes p {
|
||||
text-align: left;
|
||||
line-height: 1.5;
|
||||
font-size: 85%;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.footnotes {
|
||||
border-top: 1px solid hsl(0, 0%, 39%);
|
||||
}
|
||||
|
||||
/* Center title and paragraph */
|
||||
.abstract,
|
||||
.abstract p {
|
||||
text-align: center;
|
||||
}
|
||||
.abstract {
|
||||
margin: 2.25rem 0;
|
||||
}
|
||||
|
||||
/* Format the LaTeX symbol correctly (a higher up, e lower) */
|
||||
.latex span:nth-child(1) {
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75em;
|
||||
vertical-align: 0.28em;
|
||||
margin-left: -0.48em;
|
||||
margin-right: -0.15em;
|
||||
line-height: 1ex;
|
||||
}
|
||||
|
||||
.latex span:nth-child(2) {
|
||||
text-transform: uppercase;
|
||||
vertical-align: -0.5ex;
|
||||
margin-left: -0.1667em;
|
||||
margin-right: -0.125em;
|
||||
line-height: 1ex;
|
||||
}
|
||||
|
||||
/* Heading typography */
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
line-height: 3.25rem;
|
||||
margin-bottom: 1.625rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.7rem;
|
||||
line-height: 2rem;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.4rem;
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1rem;
|
||||
margin-top: 1.8rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.625rem;
|
||||
}
|
||||
|
||||
h1 + h2 {
|
||||
margin-top: 1.625rem;
|
||||
}
|
||||
|
||||
h2 + h3,
|
||||
h3 + h4,
|
||||
h4 + h5 {
|
||||
margin-top: 0.8rem;
|
||||
}
|
||||
|
||||
h5 + h6 {
|
||||
margin-top: -0.8rem;
|
||||
}
|
||||
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
div.diff div.old {
|
||||
background-color: #FFFFAF;
|
||||
}
|
||||
|
||||
div.diff div.new {
|
||||
background-color: #CFFFCF;
|
||||
}
|
||||
|
||||
div.content blockquote {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2013-2016 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2013-2021 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
|
||||
@@ -96,16 +96,20 @@ sub DoBanHosts {
|
||||
if (IsItBanned($_, \@regexps)) {
|
||||
print $q->p(Ts("%s is banned", $name));
|
||||
} else {
|
||||
my ($start, $end) = BanContributors::get_range($_);
|
||||
$range = "[$start - $end]";
|
||||
$name .= " " . $range;
|
||||
print GetFormStart(undef, 'get', 'ban'),
|
||||
GetHiddenValue('action', 'ban'),
|
||||
GetHiddenValue('id', $id),
|
||||
GetHiddenValue('range', $range),
|
||||
GetHiddenValue('regexp', BanContributors::get_regexp_ip($start, $end)),
|
||||
GetHiddenValue('recent_edit', 'on'),
|
||||
$q->p($name, $q->submit(T('Ban!'))), $q->end_form();
|
||||
my @pairs = BanContributors::get_range($_);
|
||||
while (@pairs) {
|
||||
my $start = shift(@pairs);
|
||||
my $end = shift(@pairs);
|
||||
$range = "[$start - $end]";
|
||||
$name .= " " . $range;
|
||||
print GetFormStart(undef, 'get', 'ban'),
|
||||
GetHiddenValue('action', 'ban'),
|
||||
GetHiddenValue('id', $id),
|
||||
GetHiddenValue('range', $range),
|
||||
GetHiddenValue('regexp', BanContributors::get_regexp_ip($start, $end)),
|
||||
GetHiddenValue('recent_edit', 'on'),
|
||||
$q->p($name, $q->submit(T('Ban!'))), $q->end_form();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,47 +171,73 @@ sub NewBanContributorsWriteRcLog {
|
||||
|
||||
package BanContributors;
|
||||
use Net::Whois::Parser qw/parse_whois/;
|
||||
use Net::IP;
|
||||
|
||||
sub get_range {
|
||||
my $ip = shift;
|
||||
my $response = parse_whois(domain => $ip);
|
||||
my ($start, $end);
|
||||
my $re = '(?:[0-9]{1,3}\.){3}[0-9]{1,3}';
|
||||
my ($start, $end) = $response->{inetnum} =~ /($re) *- *($re)/;
|
||||
return $start, $end;
|
||||
# Just try all the keys and see whether there is a range match.
|
||||
for (keys %$response) {
|
||||
my @result;
|
||||
$_ = $response->{$_};
|
||||
for (ref eq 'ARRAY' ? @$_ : $_) {
|
||||
$ip = Net::IP->new($_);
|
||||
push(@result, $ip->ip, $ip->last_ip) if $ip;
|
||||
}
|
||||
return @result if @result;
|
||||
}
|
||||
# Fallback
|
||||
return $ip, $ip;
|
||||
}
|
||||
|
||||
sub get_groups {
|
||||
my ($from, $to) = @_;
|
||||
my @groups;
|
||||
if ($from < 10) {
|
||||
my $to = $to >= 10 ? 9 : $to;
|
||||
if ($from == $to) {
|
||||
return [$from, $to];
|
||||
}
|
||||
# ones up to the nearest ten
|
||||
if ($from < $to and ($from % 10 or $from < 10)) {
|
||||
# from 5-7: as is
|
||||
# from 5-17: 5 + 9 - 5 = 9 thus 5-9, set $from to 10
|
||||
my $to2 = int($to/10) > int($from/10) ? $from + 9 - $from % 10 : $to;
|
||||
push(@groups, [$from, $to2]);
|
||||
$from = $to2 + 1;
|
||||
}
|
||||
# tens up to the nearest hundred
|
||||
if ($from < $to and $from % 100) {
|
||||
# 10-17: as is
|
||||
# 10-82: 10 to 79, set $from to 80 (8*10-1)
|
||||
# 10-182: 10 to 99, set $from to 100 (10+99=10=99)
|
||||
# 110-182: 110 to 179, set $from to 180 (170)
|
||||
# 110-222: 110 to 199, set $from to 200 (110+99-10 = 199)
|
||||
my $to2 = int($to/100) > int($from/100) ? $from + 99 - $from % 100
|
||||
: int($to/10) > int($from/10) ? int($to / 10) * 10 - 1
|
||||
: $to;
|
||||
push(@groups, [$from, $to2]);
|
||||
$from = $to2 + 1;
|
||||
}
|
||||
# up to the next hundred
|
||||
if (int($to/100) > int($from/100)) {
|
||||
# from 100 to 223: set $from to 200 (2*100-1)
|
||||
my $to2 = int($to/100) * 100 - 1;
|
||||
push(@groups, [$from, $to2]);
|
||||
$from = $to2 + 1;
|
||||
}
|
||||
# up to the next ten
|
||||
if (int($to/10) > int($from/10)) {
|
||||
# 10 to 17: skip
|
||||
# 100 to 143: set $from to 140 (14*10-1)
|
||||
my $to2 = int($to / 10) * 10 - 1;
|
||||
push(@groups, [$from, $to2]);
|
||||
$from = $to2 + 1;
|
||||
}
|
||||
# up to the next one
|
||||
if ($from <= $to) {
|
||||
push(@groups, [$from, $to]);
|
||||
$from = $to + 1;
|
||||
}
|
||||
while ($from < $to) {
|
||||
my $to = int($from/100) < int($to/100) ? $from + 99 - $from % 100 : $to;
|
||||
if ($from % 10) {
|
||||
push(@groups, [$from, $from + 9 - $from % 10]);
|
||||
$from += 10 - $from % 10;
|
||||
}
|
||||
if (int($from/10) < int($to/10)) {
|
||||
if ($to % 10 == 9) {
|
||||
push(@groups, [$from, $to]);
|
||||
$from = 1 + $to;
|
||||
} else {
|
||||
push(@groups, [$from, $to - 1 - $to % 10]);
|
||||
$from = $to - $to % 10;
|
||||
}
|
||||
} else {
|
||||
push(@groups, [$from - $from % 10, $to]);
|
||||
last;
|
||||
}
|
||||
if ($to % 10 != 9) {
|
||||
push(@groups, [$from, $to]);
|
||||
$from = 1 + $to; # jump from 99 to 100
|
||||
}
|
||||
}
|
||||
# warn join("; ", map { "@$_" } @groups);
|
||||
return \@groups;
|
||||
}
|
||||
|
||||
@@ -235,24 +265,42 @@ sub get_regexp_ip {
|
||||
my $regexp = "^";
|
||||
for my $i (0 .. 3) {
|
||||
if ($start[$i] eq $end[$i]) {
|
||||
# if the byte is the same, use it as is
|
||||
$regexp .= $start[$i];
|
||||
} elsif ($start[$i] eq '0' and $end[$i] eq '255') {
|
||||
$regexp .= '\.' if $i < 3;
|
||||
} elsif ($start[$i] == 0 and $end[$i] == 255) {
|
||||
# the starting byte is 0 and the end byte is 255, then anything goes:
|
||||
# we're done, e.g. 185.244.214.0 - 185.244.214.255 results in 185\.244\.214\.
|
||||
last;
|
||||
} elsif ($start[$i + 1] > 0) {
|
||||
$regexp .= '(' . $start[$i] . '\.('
|
||||
. get_regexp_range($start[$i + 1], '255') . ')|'
|
||||
. get_regexp_range($start[$i] + 1, $end[$i + 1]) . ')';
|
||||
$regexp .= '\.';
|
||||
} elsif ($i == 3 and $start[$i] != $end[$i]) {
|
||||
# example 45.87.2.128 - 45.87.2.255: the last bytes differ
|
||||
$regexp .= '(' . get_regexp_range($start[$i], $end[$i]) . ')';
|
||||
last;
|
||||
} elsif ($start[$i + 1] == 0 and $end[$i + 1] == 255) {
|
||||
# if we're here, we already know that the start byte and the end byte are
|
||||
# not the same; if the next bytes are from 0 to 255, we know that
|
||||
# everything else doesn't matter, e.g. 42.118.48.0 - 42.118.63.255
|
||||
$regexp .= '(' . get_regexp_range($start[$i], $end[$i]) . ')';
|
||||
$regexp .= '\.' if $i < 3;
|
||||
last;
|
||||
} elsif ($end[$i] - $start[$i] == 1 and $start[$i + 1] > 0 and $end[$i + 1] < 255) {
|
||||
# if we're here, we already know that the start byte and the end byte are
|
||||
# not the same; if the starting byte of the next (!) byte is bigger than
|
||||
# zero, then we need groups: in the case 77.56.180.0 - 77.57.70.255 for
|
||||
# example,
|
||||
$regexp .= '(' . $start[$i] . '\.(' . get_regexp_range($start[$i + 1], 255) . ')|'
|
||||
. $end[$i] . '\.(' . get_regexp_range(0, $end[$i + 1]) . ')';
|
||||
$regexp .= '\.' if $i < 3;
|
||||
last;
|
||||
} else {
|
||||
$regexp .= '(' . get_regexp_range($start[$i], $end[$i]) . ')$';
|
||||
warn "Unhandled regexp: $from - $to ($i)";
|
||||
$regexp .= 'XXX';
|
||||
$regexp .= '\.' if $i < 3;
|
||||
last;
|
||||
}
|
||||
$regexp .= '\.' if $i < 3;
|
||||
}
|
||||
return $regexp;
|
||||
}
|
||||
|
||||
# this is required in case we concatenate other modules to this one
|
||||
package OddMuse;
|
||||
|
||||
|
||||
@@ -64,13 +64,11 @@ sub RegexpNewBannedContent {
|
||||
my $str = shift;
|
||||
# check whether Banned Content complains
|
||||
my $rule = RegexpOldBannedContent($str, @_);
|
||||
# remove URLs as they have been checked by $BannedContent
|
||||
$str =~ s/$FullUrlPattern//g;
|
||||
if (not $rule) {
|
||||
foreach (split(/\n/, GetPageContent($BannedRegexps))) {
|
||||
next unless m/^\s*([^#]+?)\s*(#\s*(\d\d\d\d-\d\d-\d\d\s*)?(.*))?$/;
|
||||
my ($regexp, $comment, $re) = ($1, $4, undef);
|
||||
eval { $re = qr/$regexp/i; };
|
||||
eval { $re = qr/($regexp)/i; };
|
||||
if (defined($re) && $str =~ $re) {
|
||||
my $group1 = $1;
|
||||
my $explanation = ($group1
|
||||
|
||||
@@ -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.
|
||||
|
||||
66
modules/cook-lang.pl
Normal file
66
modules/cook-lang.pl
Normal file
@@ -0,0 +1,66 @@
|
||||
# Copyright (C) 2021 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;
|
||||
|
||||
AddModuleDescription('cook-lang.pl', 'Cooklang Extension');
|
||||
|
||||
our ($q, $bol, @MyRules);
|
||||
|
||||
push(@MyRules, \&CookLangRule);
|
||||
|
||||
sub CookLangRule {
|
||||
if (/\G#([^\n#\@\{\}]+)\{(?:([^\n%\}]+)(?:%([^\n\}]+))?)?\}/cg) {
|
||||
# #canning funnel{}
|
||||
my $html = "";
|
||||
$html .= $q->strong({-title=>"number"}, $2) if $2;
|
||||
$html .= " " if $2 and $3;
|
||||
$html .= $q->strong({-title=>"unit"}, $3) if $3;
|
||||
$html .= " " if $1 and ($2 or $3);
|
||||
$html .= $q->strong({-title=>"cookware"}, $1);
|
||||
return $html;
|
||||
} elsif (/\G#(\w+)/cg) {
|
||||
# #pot
|
||||
return $q->strong({-title=>"cookware"}, $1);
|
||||
} elsif (/\G\@([^\n#\@\{\}]+)\{(?:([^\n%\}]+)(?:%([^\n\}]+))?)?\}/cg) {
|
||||
# @ground black pepper{}
|
||||
my $html = "";
|
||||
$html .= $q->strong({-title=>"number"}, $2) if $2;
|
||||
$html .= " " if $2 and $3;
|
||||
$html .= $q->strong({-title=>"unit"}, $3) if $3;
|
||||
$html .= " " if $1 and ($2 or $3);
|
||||
$html .= $q->strong({-title=>"ingredient"}, $1);
|
||||
return $html;
|
||||
} elsif (/\G\@(\w+)/cg) {
|
||||
# @salt
|
||||
return $q->strong({-title=>"ingredient"}, $1);
|
||||
} elsif (/\G\~\{([^\n%\}]+)(?:%([^\n\}]+))?\}/cg) {
|
||||
# ~{25%minutes}
|
||||
my $html = $q->strong({-title=>"number"}, $1);
|
||||
$html .= " " if $1 and $2;
|
||||
$html .= $q->strong({-title=>"unit"}, $2) if $2;
|
||||
return $html;
|
||||
} elsif (/\G\/\/\s*(.*)/cg) {
|
||||
# // Don't burn the roux!
|
||||
return $q->em({-title=>"comment"}, $1);
|
||||
} elsif ($bol and /\G>>\s*(.*)/cg) {
|
||||
# // Don't burn the roux!
|
||||
return CloseHtmlEnvironments()
|
||||
. $q->blockquote({-title=>"meta"}, $1)
|
||||
. AddHtmlEnvironment('p');
|
||||
}
|
||||
# no match
|
||||
return;
|
||||
}
|
||||
@@ -246,7 +246,8 @@ sub CreoleRule {
|
||||
$q->img({-src=> UnquoteHtml($1),
|
||||
-alt=> UnquoteHtml($3),
|
||||
-title=> UnquoteHtml($3),
|
||||
-class=> 'url outside'})));
|
||||
-class=> 'url outside',
|
||||
-loading=>'lazy'})));
|
||||
}
|
||||
# image link: [[link|{{pic}}]] and [[link|{{pic|text}}]]
|
||||
elsif (m/\G(\[\[$FreeLinkPattern$CreoleLinkPipePattern
|
||||
@@ -257,7 +258,8 @@ sub CreoleRule {
|
||||
$q->img({-src=> GetDownloadLink(FreeToNormal($3), 2),
|
||||
-alt=> UnquoteHtml($text),
|
||||
-title=> UnquoteHtml($text),
|
||||
-class=> 'upload'}), 'image')), $text);
|
||||
-class=> 'upload',
|
||||
-loading=>'lazy'}), 'image')), $text);
|
||||
}
|
||||
# image link: [[link|{{url}}]] and [[link|{{url|text}}]]
|
||||
elsif (m/\G(\[\[$FreeLinkPattern$CreoleLinkPipePattern
|
||||
@@ -268,7 +270,8 @@ sub CreoleRule {
|
||||
$q->img({-src=> UnquoteHtml($3),
|
||||
-alt=> UnquoteHtml($text),
|
||||
-title=> UnquoteHtml($text),
|
||||
-class=> 'url outside'}), 'image')), $text);
|
||||
-class=> 'url outside',
|
||||
-loading=>'lazy'}), 'image')), $text);
|
||||
}
|
||||
# image link: [[url|{{pic}}]] and [[url|{{pic|text}}]]
|
||||
elsif (m/\G(\[\[$FullUrlPattern$CreoleLinkPipePattern
|
||||
@@ -279,7 +282,8 @@ sub CreoleRule {
|
||||
$q->img({-src=> GetDownloadLink(FreeToNormal($3), 2),
|
||||
-alt=> UnquoteHtml($text),
|
||||
-title=> UnquoteHtml($text),
|
||||
-class=> 'upload'}))), $text);
|
||||
-class=> 'upload',
|
||||
-loading=>'lazy'}))), $text);
|
||||
}
|
||||
# image link: [[url|{{url}}]] and [[url|{{url|text}}]]
|
||||
elsif (m/\G\[\[$FullUrlPattern$CreoleLinkPipePattern
|
||||
@@ -289,7 +293,8 @@ sub CreoleRule {
|
||||
$q->img({-src=> UnquoteHtml($2),
|
||||
-alt=> UnquoteHtml($4),
|
||||
-title=> UnquoteHtml($4),
|
||||
-class=> 'url outside'})));
|
||||
-class=> 'url outside',
|
||||
-loading=>'lazy'})));
|
||||
}
|
||||
# link: [[url]] and [[url|text]]
|
||||
elsif (m/\G\[\[$FullUrlPattern$CreoleLinkTextPattern\]\]/cgs) {
|
||||
|
||||
@@ -217,7 +217,7 @@ sub PrintPageContentCrossbar {
|
||||
|
||||
# If the crossbar div is placed immediately after the content div, place it
|
||||
# immediately before the content div.
|
||||
if (not ($html =~ s~(<div class="content browse">)$crossbar_pattern~$2$1~)) {
|
||||
if (not ($html =~ s~(<div class="content browse" lang="[a-z]*">)$crossbar_pattern~$2$1~)) {
|
||||
# Otherwise, if the crossbar div is placed immediately before the end of the
|
||||
# content div, place it immediately after the end of the content div.
|
||||
$html =~
|
||||
|
||||
37
modules/definition-lists.pl
Normal file
37
modules/definition-lists.pl
Normal file
@@ -0,0 +1,37 @@
|
||||
#! /usr/bin/perl
|
||||
# Copyright (C) 2019 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;
|
||||
|
||||
AddModuleDescription('definition-lists.pl', 'Definition Lists Extension');
|
||||
|
||||
our ($q, $bol, @MyRules, @HtmlStack, $Fragment);
|
||||
|
||||
push(@MyRules, \&DefinitionListsRule);
|
||||
|
||||
# term
|
||||
# : definition
|
||||
|
||||
sub DefinitionListsRule {
|
||||
if ($bol and /\G(?:\s*\n)*(\S.*)\n[ \t]*:[ \t]*/cg) {
|
||||
return OpenHtmlEnvironment('dl', 1) . "<dt>$1</dt>" . AddHtmlEnvironment('dd');
|
||||
} elsif (InElement('dd') and /\G(?:\s*\n)+(\S.*)\n[ \t]*:[ \t]*/cg) {
|
||||
return OpenHtmlEnvironment('dl', 1) . "<dt>$1</dt>" . AddHtmlEnvironment('dd');
|
||||
} elsif (InElement('dd') and /\G(\s*\n)+[ \t]*:[ \t]*/cg) {
|
||||
return OpenHtmlEnvironment('dl', 1) . AddHtmlEnvironment('dd');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -43,7 +43,7 @@ sub DitaaRule {
|
||||
my $data = MIME::Base64::encode_base64($image);
|
||||
my $url = "data:image/png;base64,$data";
|
||||
return CloseHtmlEnvironments()
|
||||
. "<div$style>" . $q->img({-src=>$url, -alt=>$map}) . "</div>";
|
||||
. "<div$style>" . $q->img({-src=>$url, -alt=>$map, -loading=>'lazy'}) . "</div>";
|
||||
}
|
||||
return undef;
|
||||
}
|
||||
|
||||
@@ -49,5 +49,5 @@ sub DuckDuckGoSearchInit {
|
||||
|
||||
sub DoDuckDuckGoSearch {
|
||||
my $search = UrlEncode(GetParam('search', undef));
|
||||
print $q->redirect({-uri=>"https://www.duckduckgo.com/?q=$search+site%3A$DuckDuckGoSearchDomain"});
|
||||
print $q->redirect({-uri=>"https://duckduckgo.com/?q=$search+site%3A$DuckDuckGoSearchDomain"});
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ sub EmojiRule {
|
||||
} elsif (/\G>:-?\(/cg) {
|
||||
# 😠 1F620 ANGRY FACE
|
||||
return '😠';
|
||||
} elsif (/\G:-?[Ppb]/cg) {
|
||||
} elsif (/\G:-?[Ppb]\b/cg) {
|
||||
# 😝 1F61D FACE WITH STUCK-OUT TONGUE AND TIGHTLY-CLOSED EYES
|
||||
return '😝';
|
||||
} elsif (/\G<3/cg) {
|
||||
|
||||
48
modules/grep-filtered.pl
Normal file
48
modules/grep-filtered.pl
Normal file
@@ -0,0 +1,48 @@
|
||||
# Copyright (C) 2020 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2020 Daniel MacKay <daniel@bonmot.ca>
|
||||
#
|
||||
# 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;
|
||||
|
||||
AddModuleDescription('grep-filtered.pl', 'Grep Filtered');
|
||||
|
||||
our ($PageDir);
|
||||
our ($UseGrep);
|
||||
|
||||
$UseGrep = 1;
|
||||
|
||||
*OldGrepFiltered = \&Filtered;
|
||||
*Filtered = \&NewGrepFiltered;
|
||||
|
||||
sub NewGrepFiltered {
|
||||
my ($string, @pages) = @_;
|
||||
my @pages = OldGrepFiltered(@_);
|
||||
my $regexp = SearchRegexp($string);
|
||||
return @pages unless GetParam('grep', $UseGrep) and $regexp;
|
||||
my @result = grep(/$regexp/i, @pages); # search parameter for page titles
|
||||
my %found = map {$_ => 1} @result;
|
||||
$regexp =~ s/\\n(\)*)$/\$$1/g; # sometimes \n can be replaced with $
|
||||
$regexp =~ s/([?+{|()])/\\$1/g; # basic regular expressions from man grep
|
||||
# if we know of any remaining grep incompatibilities we should
|
||||
# return @pages here!
|
||||
$regexp = quotemeta($regexp);
|
||||
open(F, '-|:encoding(UTF-8)', "find $PageDir -type f -print0 | xargs -0 -n10 -P4 grep --ignore-case -l '$regexp'") ;
|
||||
while (<F>) {
|
||||
my ($pageName) = m/.*\/(.*)\.pg$/ ;
|
||||
push(@result, $pageName) if not $found{$pageName};
|
||||
} close(F);
|
||||
return sort @result;
|
||||
}
|
||||
@@ -66,7 +66,7 @@ sub ImageSupportRule {
|
||||
$src = $ImageUrlPath . '/' . ImageUrlEncode($name);
|
||||
}
|
||||
if ($found) {
|
||||
$result = $q->img({-src=>$src, -alt=>$alt, -title=>$alt, -class=>'upload'});
|
||||
$result = $q->img({-src=>$src, -alt=>$alt, -title=>$alt, -class=>'upload', -loading=>'lazy'});
|
||||
$result = $q->a({-href=>$link, -class=>$linkclass}, $result);
|
||||
if ($comments) {
|
||||
for (split '\n', $comments) {
|
||||
|
||||
@@ -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–2018 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
|
||||
@@ -18,7 +18,8 @@ use v5.10;
|
||||
|
||||
AddModuleDescription('journal-rss.pl', 'Journal RSS Extension');
|
||||
|
||||
our ($OpenPageName, $CollectingJournal, %Page, %Action, @MyInitVariables, $DeletedPage, %NearLinksException);
|
||||
our ($OpenPageName, $CollectingJournal, %Page, %Action, @MyInitVariables, $DeletedPage, %NearLinksException,
|
||||
$RecentLink, $SiteName, $SiteDescription, $ScriptName, $RssRights);
|
||||
$Action{journal} = \&DoJournalRss;
|
||||
|
||||
# Currently RSS works like RecentChanges, which is not what bloggers
|
||||
@@ -30,22 +31,40 @@ 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;
|
||||
SetParam('full', 1);
|
||||
print GetHttpHeader('application/xml') . GetRcRss();
|
||||
if (GetParam('raw', 0)) {
|
||||
print GetHttpHeader('text/plain');
|
||||
print RcTextItem('title', $SiteName),
|
||||
RcTextItem('description', $SiteDescription), RcTextItem('link', $ScriptName),
|
||||
RcTextItem('generator', 'Oddmuse'), RcTextItem('rights', $RssRights);
|
||||
ProcessRcLines(sub {}, \&RcTextRevision);
|
||||
} else {
|
||||
print GetHttpHeader('application/xml') . GetRcRss();
|
||||
}
|
||||
}
|
||||
|
||||
sub JournalRssParameters {
|
||||
my $more = '';
|
||||
foreach (@_, qw(rsslimit match search reverse monthly)) {
|
||||
my $val = GetParam($_, '');
|
||||
$more .= ";$_=$val" if $val;
|
||||
$more .= ";$_=" . UrlEncode($val) if $val;
|
||||
}
|
||||
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));
|
||||
}
|
||||
@@ -67,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–2015 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
|
||||
@@ -230,6 +230,10 @@ sub MailMenu {
|
||||
ScriptLink('action=subscriptionlist',
|
||||
T('All mail subscriptions'),
|
||||
'subscriptionlist')) if UserIsAdmin();
|
||||
push(@$menuref,
|
||||
ScriptLink('action=subscribers',
|
||||
T('All mail subscribers'),
|
||||
'subscribers')) if UserIsAdmin();
|
||||
}
|
||||
|
||||
=head1 Your subscriptions
|
||||
@@ -283,9 +287,9 @@ sub MailSubscription {
|
||||
|
||||
=head1 Administrator Access
|
||||
|
||||
The subscriptionlist action will show you the subscription database,
|
||||
if you're an administrator. It's a plain text file of the data, which
|
||||
you can use for debugging and scripting purposes.
|
||||
The C<subscriptionlist> action will show you the subscription database, if
|
||||
you're an administrator. With the C<raw> parameter set it's a plain text file of
|
||||
the data, which you can use for debugging and scripting purposes.
|
||||
|
||||
=cut
|
||||
|
||||
@@ -327,6 +331,41 @@ sub MailLink {
|
||||
. join(';', map { "pages=$_" } @pages), $str);
|
||||
}
|
||||
|
||||
=pod
|
||||
|
||||
The C<subscribers> action lists each unique email address for easier mass
|
||||
unsubscribing of email addresses after a wave of wiki spam.
|
||||
|
||||
=cut
|
||||
|
||||
$Action{subscribers} = \&DoMailSubscribers;
|
||||
|
||||
sub DoMailSubscribers {
|
||||
UserIsAdminOrError();
|
||||
my $raw = GetParam('raw', 0);
|
||||
if ($raw) {
|
||||
print GetHttpHeader('text/plain');
|
||||
} else {
|
||||
print GetHeader('', T('Subscriptions')),
|
||||
$q->start_div({-class=>'content subscribtionlist'}),
|
||||
$q->p(T('Mail addresses are linked to unsubscription links.')),
|
||||
'<ul>';
|
||||
}
|
||||
my %authors;
|
||||
require DB_File;
|
||||
tie my %h, "DB_File", encode_utf8($MailFile);
|
||||
for my $author (sort grep /\@/, map { UrlDecode($_) } keys %h) {
|
||||
if ($raw) {
|
||||
print "$author\n";
|
||||
} else {
|
||||
print $q->li(ScriptLink("action=unsubscribe;who=$author", $author));
|
||||
}
|
||||
}
|
||||
print '</ul></div>' unless $raw;
|
||||
PrintFooter() unless $raw;
|
||||
untie %h;
|
||||
}
|
||||
|
||||
=head1 Subscription
|
||||
|
||||
The subscribe action will subscribe you to pages. The mail parameter
|
||||
@@ -402,13 +441,13 @@ sub MailSubscribe {
|
||||
|
||||
=head1 Unsubscription
|
||||
|
||||
The unsubscribe action will unsubscribe you from pages. The mail
|
||||
parameter contains the mail address to use and defaults to the value
|
||||
store in your cookie. Multiple pages parameters contain the pages to
|
||||
unsubscribe.
|
||||
The unsubscribe action will unsubscribe you from pages. The mail parameter
|
||||
contains the mail address to use and defaults to the value store in your cookie.
|
||||
Multiple pages parameters contain the pages to unsubscribe. Without naming
|
||||
pages, you will be unsubscribed from all pages.
|
||||
|
||||
The who parameter overrides the mail parameter and is used for
|
||||
administrator unsubscription from the subscriptionlist action.
|
||||
The who parameter overrides the mail parameter and is used for administrator
|
||||
unsubscription from the subscriptionlist action.
|
||||
|
||||
=cut
|
||||
|
||||
@@ -423,9 +462,13 @@ sub DoMailUnsubscribe {
|
||||
# MailUnsubscribe will set a parameter and must run before printing
|
||||
# the header.
|
||||
print GetHeader('', T('Subscriptions')),
|
||||
$q->start_div({-class=>'content unsubscribe'});
|
||||
print $q->p(Ts('Unsubscribed %s from the following pages:', $mail));
|
||||
print $q->ul($q->li([map { GetPageLink($_) } @pages]));
|
||||
$q->start_div({-class=>'content unsubscribe'});
|
||||
if (@pages) {
|
||||
print $q->p(Ts('Unsubscribed %s from the following pages:', $mail));
|
||||
print $q->ul($q->li([map { GetPageLink($_) } @pages]));
|
||||
} else {
|
||||
print $q->p(Ts('Unsubscribed %s from all pages.', $mail));
|
||||
}
|
||||
print $q->p(ScriptLink('action=subscriptions', T('Your mail subscriptions'),
|
||||
'subscriptions') . '.');
|
||||
print $q->end_div();
|
||||
@@ -434,10 +477,11 @@ sub DoMailUnsubscribe {
|
||||
|
||||
sub MailUnsubscribe {
|
||||
my ($mail, @pages) = @_;
|
||||
return unless $mail and @pages;
|
||||
return unless $mail;
|
||||
require DB_File;
|
||||
tie my %h, "DB_File", encode_utf8($MailFile);
|
||||
my %subscriptions = map {$_=>1} split(/$FS/, UrlDecode($h{UrlEncode($mail)}));
|
||||
@pages = keys %subscriptions unless @pages;
|
||||
foreach my $id (@pages) {
|
||||
delete $subscriptions{$id};
|
||||
# take care of reverse lookup
|
||||
@@ -461,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–2017 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
|
||||
@@ -18,7 +18,7 @@ use v5.10;
|
||||
|
||||
AddModuleDescription('markdown-rule.pl', 'Markdown Rule Extension');
|
||||
|
||||
our ($q, $bol, %RuleOrder, @MyRules, $UrlProtocols, $FullUrlPattern, @HtmlStack);
|
||||
our ($q, $bol, %RuleOrder, @MyRules, $UrlProtocols, $FullUrlPattern, @HtmlStack, $Fragment);
|
||||
|
||||
push(@MyRules, \&MarkdownRule);
|
||||
# Since we want this package to be a simple add-on, we try and avoid
|
||||
@@ -31,6 +31,7 @@ $RuleOrder{\&MarkdownRule} = 200;
|
||||
# https://help.github.com/articles/github-flavored-markdown
|
||||
|
||||
sub MarkdownRule {
|
||||
my $alignment;
|
||||
# \escape
|
||||
if (m/\G\\([-#>*`=])/cg) {
|
||||
return $1;
|
||||
@@ -50,9 +51,28 @@ sub MarkdownRule {
|
||||
}
|
||||
# > blockquote
|
||||
# with continuation
|
||||
elsif ($bol and m/\G>/cg) {
|
||||
return CloseHtmlEnvironments()
|
||||
. AddHtmlEnvironment('blockquote');
|
||||
elsif ($bol and m/\G((?:>.*\n?)+)/cg) {
|
||||
Clean(CloseHtmlEnvironments());
|
||||
Dirty($1);
|
||||
my $text = $1;
|
||||
my ($oldpos, $old_) = ((pos), $_);
|
||||
print '<blockquote>';
|
||||
$text =~ s/^> ?//gm;
|
||||
ApplyRules($text, 1, 1, undef, 'p'); # local links, anchors, no revision, start with p
|
||||
print '</blockquote>';
|
||||
Clean(AddHtmlEnvironment('p')); # if dirty block is looked at later, this will disappear
|
||||
($_, pos) = ($old_, $oldpos); # restore \G (assignment order matters!)
|
||||
}
|
||||
# """ = blockquote, too
|
||||
elsif ($bol and m/\G("""[ \t]*\n(.*?)\n"""[ \t]*(?:\n|$))/cgs) {
|
||||
Clean(CloseHtmlEnvironments());
|
||||
Dirty($1);
|
||||
my ($oldpos, $old_) = ((pos), $_);
|
||||
print '<blockquote>';
|
||||
ApplyRules($2, 1, 1, undef, 'p'); # local links, anchors, no revision, start with p
|
||||
print '</blockquote>';
|
||||
Clean(AddHtmlEnvironment('p')); # if dirty block is looked at later, this will disappear
|
||||
($_, pos) = ($old_, $oldpos); # restore \G (assignment order matters!)
|
||||
}
|
||||
# ``` = code
|
||||
elsif ($bol and m/\G```[ \t]*\n(.*?)\n```[ \t]*(\n|$)/cgs) {
|
||||
@@ -123,34 +143,47 @@ sub MarkdownRule {
|
||||
# beginning of a table
|
||||
elsif ($bol and !InElement('table') and m/\G\|/cg) {
|
||||
# warn pos . " beginning of a table";
|
||||
$alignment = 'style="text-align: right"' if m/\G([ \t]+)/cg;
|
||||
$alignment = 'style="text-align: center"' if $alignment and m/\G(?=[^|]+[ \t]+\|)/cg;
|
||||
$Fragment =~ s/[ \t]+$//; # cleanup trailing whitespace if previous column was centered
|
||||
return OpenHtmlEnvironment('table',1)
|
||||
. AddHtmlEnvironment('tr')
|
||||
. AddHtmlEnvironment('th');
|
||||
. AddHtmlEnvironment('th', $alignment);
|
||||
}
|
||||
# end of a row and beginning of a new row
|
||||
elsif (InElement('table') and m/\G\|?\n\|/cg) {
|
||||
# warn pos . " end of a row and beginning of a new row";
|
||||
$alignment = 'style="text-align: right"' if m/\G([ \t]+)/cg;
|
||||
$alignment = 'style="text-align: center"' if $alignment and m/\G(?=[^|]+[ \t]+\|)/cg;
|
||||
$Fragment =~ s/[ \t]+$//; # cleanup trailing whitespace if previous column was centered
|
||||
return CloseHtmlEnvironment('tr')
|
||||
. AddHtmlEnvironment('tr')
|
||||
. AddHtmlEnvironment('td');
|
||||
. AddHtmlEnvironment('td', $alignment);
|
||||
}
|
||||
# otherwise the table ends
|
||||
elsif (InElement('table') and m/\G\|?(\n|$)/cg) {
|
||||
# warn pos . " otherwise the table ends";
|
||||
$Fragment =~ s/[ \t]+$//; # cleanup trailing whitespace if previous column was centered
|
||||
return CloseHtmlEnvironment('table')
|
||||
. AddHtmlEnvironment('p');
|
||||
}
|
||||
# continuation of the first row
|
||||
elsif (InElement('th') and m/\G\|/cg) {
|
||||
# warn pos . " continuation of the first row";
|
||||
$alignment = 'style="text-align: right"' if m/\G([ \t]+)/cg;
|
||||
$alignment = 'style="text-align: center"' if $alignment and m/\G(?=[^|]+[ \t]+\|)/cg;
|
||||
$Fragment =~ s/[ \t]+$//; # cleanup trailing whitespace if previous column was centered
|
||||
return CloseHtmlEnvironment('th')
|
||||
. AddHtmlEnvironment('th');
|
||||
. AddHtmlEnvironment('th', $alignment);
|
||||
}
|
||||
# continuation of other rows
|
||||
elsif (InElement('td') and m/\G\|/cg) {
|
||||
# warn pos . " continuation of other rows";
|
||||
$alignment = 'style="text-align: right"' if m/\G([ \t]+)/cg;
|
||||
$alignment = 'style="text-align: center"' if $alignment and m/\G(?=[^|]+[ \t]+\|)/cg;
|
||||
$Fragment =~ s/[ \t]+$//; # cleanup trailing whitespace if previous column was centered
|
||||
return CloseHtmlEnvironment('td')
|
||||
. AddHtmlEnvironment('td');
|
||||
. AddHtmlEnvironment('td', $alignment);
|
||||
}
|
||||
# whitespace indentation = code
|
||||
elsif ($bol and m/\G(\s*\n)*( .+)\n?/cg) {
|
||||
@@ -160,13 +193,21 @@ sub MarkdownRule {
|
||||
}
|
||||
return OpenHtmlEnvironment('pre',1) . $str; # always level 1
|
||||
}
|
||||
# [an example](http://example.com/ "Title")
|
||||
elsif (m/\G\[(.+?)\]\($FullUrlPattern(\s+"(.+?)")?\)/cg) {
|
||||
# link: [an example](http://example.com/ "Title")
|
||||
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
|
||||
@@ -42,10 +42,18 @@ AddModuleDescription('namespaces.pl', 'Namespaces Extension');
|
||||
|
||||
use File::Glob ':glob';
|
||||
|
||||
our ($q, %Action, %Page, @IndexList, $Now, %InterSite, $SiteName, $ScriptName, $UsePathInfo, $DataDir, $HomePage, @MyInitVariables, @MyAdminCode, $FullUrl, $LinkPattern, $InterSitePattern, $FreeLinks, $FreeLinkPattern, $InterLinkPattern, $FreeInterLinkPattern, $UrlProtocols, $WikiLinks, $FS, $RcFile, $RcOldFile, $RcDefault, $PageDir, $KeepDir, $LockDir, $TempDir, $IndexFile, $VisitorFile, $NoEditFile, $WikiDescription, $LastUpdate, $StaticDir, $StaticUrl, $InterWikiMoniker, $RefererDir, $PermanentAnchorsFile);
|
||||
our ($q, %Action, %Page, @IndexList, $Now, %InterSite, $SiteName, $ScriptName,
|
||||
$UsePathInfo, $DataDir, $HomePage, @MyInitVariables, @MyAdminCode, $FullUrl,
|
||||
$LinkPattern, $InterSitePattern, $FreeLinks, $FreeLinkPattern,
|
||||
$InterLinkPattern, $FreeInterLinkPattern, $UrlProtocols, $WikiLinks, $FS,
|
||||
$BannedContent, $BannedHosts, $RcFile, $RcOldFile, $RcDefault, $PageDir,
|
||||
$KeepDir, $LockDir, $TempDir, $IndexFile, $VisitorFile, $NoEditFile,
|
||||
$WikiDescription, $LastUpdate, $StaticDir, $StaticUrl, $InterWikiMoniker,
|
||||
$RefererDir, $PermanentAnchorsFile, @IndexList, %IndexHash);
|
||||
|
||||
our ($NamespacesMain, $NamespacesSelf, $NamespaceCurrent,
|
||||
$NamespaceRoot, $NamespaceSlashing, @NamespaceParameters,
|
||||
%Namespaces);
|
||||
$NamespaceRoot, $NamespaceSlashing, @NamespaceParameters,
|
||||
%Namespaces, $NamespacesRootDataDir);
|
||||
|
||||
$NamespacesMain = 'Main'; # to get back to the main namespace
|
||||
$NamespacesSelf = 'Self'; # for your own namespace
|
||||
@@ -110,6 +118,7 @@ sub NamespacesInitVariables {
|
||||
}
|
||||
}
|
||||
$NamespaceRoot = $ScriptName; # $ScriptName may be changed below
|
||||
$NamespacesRootDataDir = $DataDir; # $DataDir may be chanegd below
|
||||
$NamespaceCurrent = '';
|
||||
my $ns = GetNamespace();
|
||||
if ($ns
|
||||
@@ -117,7 +126,7 @@ sub NamespacesInitVariables {
|
||||
and $ns ne $NamespacesSelf) {
|
||||
$NamespaceCurrent = $ns;
|
||||
# Change some stuff from the original InitVariables call:
|
||||
$SiteName .= ' ' . $NamespaceCurrent;
|
||||
$SiteName .= ' ' . NormalToFree($NamespaceCurrent);
|
||||
$InterWikiMoniker = $NamespaceCurrent;
|
||||
$DataDir .= '/' . $NamespaceCurrent;
|
||||
$PageDir = "$DataDir/page";
|
||||
@@ -162,6 +171,56 @@ sub NamespaceRequiredByParameter {
|
||||
}
|
||||
}
|
||||
|
||||
=head Spam fighting
|
||||
|
||||
We want to share C<BannedContent> and C<BannedHosts> between all the wiki
|
||||
namespaces. Therefore, we need to handle a number of cases:
|
||||
|
||||
C<UserIsBanned> uses C<GetPageContent($BannedHosts)> and C<BannedContent> uses
|
||||
C<GetPageContent($BannedContent)>, therefore C<GetPageContent> is going to get
|
||||
modified.
|
||||
|
||||
C<DoBanHosts> in F<ban-contributors.pl> uses C<DoPost($BannedContent)> and
|
||||
C<DoPost($BannedHosts)>, therefore C<DoPost> is going to get modified.
|
||||
|
||||
=cut
|
||||
|
||||
*OldNamespaceGetPageContent = \&GetPageContent;
|
||||
*GetPageContent = \&NewNamespaceGetPageContent;
|
||||
|
||||
sub NewNamespaceGetPageContent {
|
||||
my ($id) = @_;
|
||||
if ($NamespaceCurrent and ($id eq $BannedContent or $id eq $BannedHosts)) {
|
||||
local $PageDir = "$NamespacesRootDataDir/page";
|
||||
# we cannot use ReadFileOrDie because our $IndexHash{$id} does not reflect the existence of the root file
|
||||
my ($status, $data) = ReadFile(GetPageFile($id));
|
||||
return ParseData($data)->{text} if $status;
|
||||
return '';
|
||||
}
|
||||
return OldNamespaceGetPageContent(@_);
|
||||
}
|
||||
|
||||
*OldNamespaceDoPost = \&DoPost;
|
||||
*DoPost = \&NewNamespaceDoPost;
|
||||
|
||||
sub NewNamespaceDoPost {
|
||||
my ($id) = @_;
|
||||
if ($NamespaceCurrent and ($id eq $BannedContent or $id eq $BannedHosts)) {
|
||||
local $DataDir = $NamespacesRootDataDir;
|
||||
local $PageDir = "$DataDir/page";
|
||||
local $KeepDir = "$DataDir/keep";
|
||||
local $LockDir = "$TempDir/lock";
|
||||
local $NoEditFile = "$DataDir/noedit";
|
||||
local $RcFile = "$DataDir/rc.log";
|
||||
local $RcOldFile = "$DataDir/oldrc.log";
|
||||
local $IndexFile = "$DataDir/pageidx";
|
||||
@IndexList = %IndexHash = ();
|
||||
AllPagesList(); # reload from new pageidx
|
||||
return OldNamespaceDoPost(@_);
|
||||
}
|
||||
return OldNamespaceDoPost(@_);
|
||||
}
|
||||
|
||||
=head2 RecentChanges
|
||||
|
||||
RecentChanges in the main namespace will list changes to all the
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ sub PortraitSupportRule {
|
||||
$PortraitSupportColorDiv = 0;
|
||||
return $html;
|
||||
} elsif ($bol && m/\Gportrait:$UrlPattern/cg) {
|
||||
return $q->img({-src=>$1, -alt=>T("Portrait"), -class=>'portrait'});
|
||||
return $q->img({-src=>$1, -alt=>T("Portrait"), -class=>'portrait', -loading=>'lazy'});
|
||||
} elsif ($bol && m/\G(:*)\[new(.*)\]/cg) {
|
||||
my $portrait = '';
|
||||
my $depth = length($1);
|
||||
|
||||
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;
|
||||
@@ -171,7 +171,7 @@ sub ReCaptchaGetQuestion {
|
||||
# if (defined $ReCaptchaTabIndex) { $recaptcha_options{tabindex} = $ReCaptchaTabIndex; }
|
||||
|
||||
eval "use Captcha::reCAPTCHA";
|
||||
my $captcha_html = Captcha::reCAPTCHA->new()->get_html(
|
||||
my $captcha_html = Captcha::reCAPTCHA->new()->get_html_v2(
|
||||
$ReCaptchaPublicKey, undef, $ENV{'HTTPS'} eq 'on', undef);
|
||||
my $submit_html = $need_button ? $q->submit(-value=> T('Go!')) : '';
|
||||
my $options_html = '
|
||||
@@ -239,11 +239,12 @@ sub NewReCaptchaDoPost {
|
||||
|
||||
sub ReCaptchaCheckAnswer {
|
||||
eval "use Captcha::reCAPTCHA";
|
||||
my $result = Captcha::reCAPTCHA->new()->check_answer(
|
||||
my $answer = GetParam('g-recaptcha-response');
|
||||
return 0 unless $answer;
|
||||
my $result = Captcha::reCAPTCHA->new()->check_answer_v2(
|
||||
$ReCaptchaPrivateKey,
|
||||
$q->remote_addr(),
|
||||
GetParam('recaptcha_challenge_field'),
|
||||
GetParam('recaptcha_response_field')
|
||||
$answer,
|
||||
$q->remote_addr()
|
||||
);
|
||||
return $result->{is_valid};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ sub StaticGetDownloadLink {
|
||||
return '[' . ($image ? 'image' : 'link') . ':' . $name . ']' unless $IndexHash{$id};
|
||||
if ($image) {
|
||||
return StaticFileName($id) if $image == 2;
|
||||
my $result = $q->img({-src=>StaticFileName($id), -alt=>$alt, -class=>'upload'});
|
||||
my $result = $q->img({-src=>StaticFileName($id), -alt=>$alt, -class=>'upload', -loading=>'lazy'});
|
||||
$result = ScriptLink($id, $result, 'image');
|
||||
return $result;
|
||||
} else {
|
||||
@@ -198,7 +198,7 @@ EOT
|
||||
my $logo = $LogoUrl;
|
||||
$logo =~ s|.*/||; # just the filename
|
||||
my $alt = T('[Home]');
|
||||
$header .= $q->img({-src=>$logo, -alt=>$alt, -class=>'logo'}) if $logo;
|
||||
$header .= $q->img({-src=>$logo, -alt=>$alt, -class=>'logo', -loading=>'lazy'}) if $logo;
|
||||
}
|
||||
# top toolbar
|
||||
local $UserGotoBar = ''; # only allow @UserGotoBarPages
|
||||
@@ -317,7 +317,7 @@ sub GetDownloadLink {
|
||||
$action = $ScriptName . '?' . $action;
|
||||
}
|
||||
return $action if $image == 2;
|
||||
my $result = $q->img({-src=>$action, -alt=>$alt, -class=>'upload'});
|
||||
my $result = $q->img({-src=>$action, -alt=>$alt, -class=>'upload', -loading=>'lazy'});
|
||||
$result = ScriptLink(UrlEncode($id), $result, 'image') unless $id eq $OpenPageName;
|
||||
return $result;
|
||||
} else {
|
||||
|
||||
@@ -95,7 +95,7 @@ sub StaticGetDownloadLink {
|
||||
# if the page does not exist
|
||||
return '[' . ($image ? 'image' : 'link') . ':' . $name . ']' unless $IndexHash{$id};
|
||||
if ($image) {
|
||||
my $result = $q->img({-src=>StaticFileName($id), -alt=>$alt, -class=>'upload'});
|
||||
my $result = $q->img({-src=>StaticFileName($id), -alt=>$alt, -class=>'upload', -loading=>'lazy'});
|
||||
$result = ScriptLink($id, $result, 'image');
|
||||
return $result;
|
||||
} else {
|
||||
@@ -271,7 +271,7 @@ sub GetDownloadLink {
|
||||
} else {
|
||||
$action = $ScriptName . '?' . $action;
|
||||
}
|
||||
my $result = $q->img({-src=>$action, -alt=>$alt, -class=>'upload'});
|
||||
my $result = $q->img({-src=>$action, -alt=>$alt, -class=>'upload', -loading=>'lazy'});
|
||||
$result = ScriptLink(UrlEncode($id), $result, 'image') unless $id eq $OpenPageName;
|
||||
return $result;
|
||||
} else {
|
||||
|
||||
@@ -62,16 +62,26 @@ Example:
|
||||
|
||||
$TagFeedIcon = 'http://www.example.org/pics/rss.png';
|
||||
|
||||
=head2 $TagCloudSize
|
||||
|
||||
The number of most used tags when looking at the tag cloud. The
|
||||
default is 50.
|
||||
|
||||
Example:
|
||||
|
||||
$TagCloudSize = 20;
|
||||
|
||||
=cut
|
||||
|
||||
our ($q, $Now, %Action, %Page, $FreeLinkPattern, @MyInitVariables, @MyRules, @MyAdminCode, $DataDir, $ScriptName);
|
||||
our ($TagUrl, $TagFeed, $TagFeedIcon, $TagFile);
|
||||
our ($TagUrl, $TagFeed, $TagFeedIcon, $TagFile, $TagCloudSize);
|
||||
|
||||
push(@MyInitVariables, \&TagsInit);
|
||||
|
||||
sub TagsInit {
|
||||
$TagUrl = ScriptUrl('action=rc;rcfilteronly=tag:%s') unless $TagUrl;
|
||||
$TagFeed = ScriptUrl('action=rss;rcfilteronly=tag:%s') unless $TagFeed;
|
||||
$TagCloudSize = 50 unless $TagCloudSize;
|
||||
$TagFile = "$DataDir/tag.db";
|
||||
}
|
||||
|
||||
@@ -113,7 +123,8 @@ sub TagsRule {
|
||||
-title=>T('Feed for this tag'),
|
||||
-rel=>'feed'
|
||||
}, $q->img({-src=>$TagFeedIcon,
|
||||
-alt=>T('RSS')}));
|
||||
-alt=>T('RSS'),
|
||||
-loading=>'lazy'}));
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
@@ -305,27 +316,16 @@ $Action{tagcloud} = \&TagCloud;
|
||||
|
||||
sub TagCloud {
|
||||
print GetHeader('', T('Tag Cloud'), ''),
|
||||
$q->start_div({-class=>'content cloud'}) . '<p>';
|
||||
$q->start_div({-class=>'content cloud'});
|
||||
require HTML::TagCloud;
|
||||
my $cloud = HTML::TagCloud->new;
|
||||
# open the DB file
|
||||
my %h = TagReadHash();
|
||||
my $max = 0;
|
||||
my $min = 0;
|
||||
my %count = ();
|
||||
foreach my $tag (grep !/^_/, keys %h) {
|
||||
$count{$tag} = @{$h{$tag}};
|
||||
$max = $count{$tag} if $count{$tag} > $max;
|
||||
$min = $count{$tag} if not $min or $count{$tag} < $min;
|
||||
$cloud->add(NormalToFree($tag), "$ScriptName?search=tag:" . UrlEncode($tag), scalar @{$h{$tag}});
|
||||
}
|
||||
foreach my $tag (sort keys %count) {
|
||||
my $n = $count{$tag};
|
||||
print $q->a({-href => "$ScriptName?search=tag:" . UrlEncode($tag),
|
||||
-title => $n,
|
||||
-style => 'font-size: '
|
||||
. int(80+120*($max == $min ? 1 : ($n-$min)/($max-$min)))
|
||||
. '%;',
|
||||
}, NormalToFree($tag)), ' ... ';
|
||||
}
|
||||
print '</p></div>';
|
||||
print $cloud->html_and_css($TagCloudSize);
|
||||
print '</div>';
|
||||
PrintFooter();
|
||||
}
|
||||
|
||||
@@ -425,13 +425,12 @@ sub TagsMenu {
|
||||
my ($id, $menuref, $restref) = @_;
|
||||
push(@$menuref,
|
||||
ScriptLink('action=reindex', T('Rebuild tag index'), 'reindex')
|
||||
. ', ' . ScriptLink('action=taglist', T('list tags'), 'taglist')
|
||||
. ', ' . ScriptLink('action=tagcloud', T('tag cloud'), 'tagcloud'));
|
||||
}
|
||||
|
||||
=head1 COPYRIGHT AND LICENSE
|
||||
|
||||
Copyright (C) 2005–2015 Alex Schroeder <alex@gnu.org>
|
||||
Copyright (C) 2005–2019 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
|
||||
|
||||
@@ -49,7 +49,7 @@ Le nom d’utilisateur ne doit pas dépasser 50 caractères : non sauvegardé
|
||||
This page contains an uploaded file:
|
||||
Cette page contient un fichier téléversé :
|
||||
No summary was provided for this file.
|
||||
|
||||
Aucun résumé fourni pour ce fichier.
|
||||
Recursive include of %s!
|
||||
Inclusion par récursivité de %s !
|
||||
Clear Cache
|
||||
@@ -95,13 +95,13 @@ Page non valide %s (ne doit pas se terminer par .lck)
|
||||
Invalid Page %s
|
||||
Page non valide %s
|
||||
There are no comments, yet. Be the first to leave a comment!
|
||||
|
||||
Pas encore de commentaires. Soyez le premier à laisser un commentaire !
|
||||
Welcome!
|
||||
|
||||
Bienvenue !
|
||||
This page does not exist, but you can %s.
|
||||
|
||||
Cette page n’existe pas, mais vous pouvez %s.
|
||||
create it now
|
||||
|
||||
la créer maintenant
|
||||
Too many redirections
|
||||
Trop de redirections
|
||||
No redirection for old revisions
|
||||
@@ -113,7 +113,7 @@ SVP allez à %s.
|
||||
Updates since %s
|
||||
Mises à jour depuis %s
|
||||
up to %s
|
||||
jusqu’à
|
||||
jusqu’à %s
|
||||
Updates in the last %s days
|
||||
Mises à jour durant les derniers %s jours
|
||||
Updates in the last day
|
||||
@@ -133,7 +133,7 @@ Lister seulement les modifications majeures
|
||||
Include minor changes
|
||||
Inclure les modifications mineures
|
||||
days
|
||||
|
||||
jours
|
||||
List later changes
|
||||
Lister les modifications plus récentes
|
||||
RSS
|
||||
@@ -263,9 +263,9 @@ par %s
|
||||
(diff)
|
||||
(diff)
|
||||
a
|
||||
|
||||
a
|
||||
c
|
||||
c
|
||||
|
||||
Edit revision %s of this page
|
||||
Modifier la version %s de cette page
|
||||
e
|
||||
@@ -409,13 +409,13 @@ Vous êtes actuellement éditeur de ce site.
|
||||
You are a normal user on this site.
|
||||
Vous êtes un utilisateur normal de ce site.
|
||||
You do not have a password set.
|
||||
|
||||
Vous n’avez pas défini de mot de passe.
|
||||
Your password does not match any of the administrator or editor passwords.
|
||||
Vote mot de passe ne correspond ni au mot de passe administrateur ni au mot de passe éditeur.
|
||||
Password:
|
||||
Mot de passe :
|
||||
Return to %s
|
||||
|
||||
Retour à %s
|
||||
This operation is restricted to site editors only...
|
||||
Cette opération est réservée aux éditeurs du site seulement...
|
||||
This operation is restricted to administrators only...
|
||||
@@ -445,7 +445,7 @@ Raison inconnue.
|
||||
%s pages found.
|
||||
%s pages trouvées.
|
||||
Preview: %s
|
||||
|
||||
Prévisualiser: %s
|
||||
Replaced: %s
|
||||
Remplacé(e) : %s
|
||||
Search for: %s
|
||||
@@ -544,7 +544,7 @@ Deleted %s
|
||||
Renaming %1 to %2.
|
||||
Renomme %1 en %2.
|
||||
The page %s does not exist
|
||||
La page %s n'existe pas
|
||||
La page %s n’existe pas
|
||||
The page %s already exists
|
||||
La page %s existe déjà
|
||||
Cannot rename %1 to %2
|
||||
@@ -563,9 +563,9 @@ Renommer %s en :
|
||||
# modules/advanced-uploads.pl
|
||||
################################################################################
|
||||
Attach file:
|
||||
|
||||
Joindre un fichier:
|
||||
Upload
|
||||
|
||||
Uploader
|
||||
################################################################################
|
||||
# modules/aggregate.pl
|
||||
################################################################################
|
||||
@@ -609,7 +609,7 @@ Ban Contributors to %s
|
||||
Ban!
|
||||
|
||||
Regular expression:
|
||||
|
||||
Expression régulière :
|
||||
%s is banned
|
||||
|
||||
These URLs were rolled back. Perhaps you want to add a regular expression to %s?
|
||||
@@ -620,9 +620,9 @@ Consider banning the IP number as well:
|
||||
# modules/banned-regexps.pl
|
||||
################################################################################
|
||||
Regular expression "%1" matched "%2" on this page.
|
||||
|
||||
Expression régulière "%1" correspond à "%2" sur cette page.
|
||||
Regular expression "%s" matched on this page.
|
||||
|
||||
Expression régulière "%s" correspond à cette page.
|
||||
################################################################################
|
||||
# modules/big-brother.pl
|
||||
################################################################################
|
||||
@@ -724,7 +724,7 @@ Une expression régulière manque au tag de compilation.
|
||||
# modules/creationdate.pl
|
||||
################################################################################
|
||||
Add creation date to page files
|
||||
|
||||
Ajouter une date de création aux fichiers des pages
|
||||
################################################################################
|
||||
# modules/css-install.pl
|
||||
################################################################################
|
||||
@@ -733,7 +733,7 @@ Installer CSS
|
||||
Copy one of the following stylesheets to %s:
|
||||
Copier une des feuilles de style suivantes sur %s.
|
||||
Reset
|
||||
|
||||
Réinitialiser
|
||||
################################################################################
|
||||
# modules/dates.pl
|
||||
################################################################################
|
||||
@@ -764,9 +764,9 @@ Impossible de trouver une version sans texte indésirable.
|
||||
# modules/diff.pl
|
||||
################################################################################
|
||||
Page diff
|
||||
|
||||
Page diff
|
||||
Diff
|
||||
Diff
|
||||
|
||||
################################################################################
|
||||
# modules/drafts.pl
|
||||
################################################################################
|
||||
@@ -806,11 +806,11 @@ modifications ordinaires
|
||||
# modules/edit-paragraphs.pl
|
||||
################################################################################
|
||||
Could not identify the paragraph you were editing
|
||||
|
||||
Impossible d'identifier le paragraphe que vous avez édité
|
||||
This is the section you edited:
|
||||
|
||||
C’est la section que vous avez éditée :
|
||||
This is the current page:
|
||||
|
||||
C’est la page actuelle
|
||||
################################################################################
|
||||
# modules/find.pl
|
||||
################################################################################
|
||||
@@ -820,53 +820,53 @@ Pages correspondant aux noms :
|
||||
# modules/fix-encoding.pl
|
||||
################################################################################
|
||||
Fix character encoding
|
||||
|
||||
Corriger l’encodage des caractères
|
||||
Fix HTML escapes
|
||||
|
||||
Corriger les caractères d’échappement HTML
|
||||
################################################################################
|
||||
# modules/form_timeout.pl
|
||||
################################################################################
|
||||
Set $FormTimeoutSalt.
|
||||
|
||||
Définir $FormTimeoutSalt.
|
||||
Form Timeout
|
||||
|
||||
################################################################################
|
||||
# modules/gd_security_image.pl
|
||||
################################################################################
|
||||
GD or Image::Magick modules not available.
|
||||
|
||||
modules GD ou Image::Magick non disponibles.
|
||||
GD::SecurityImage module not available.
|
||||
|
||||
module GD::SecurityImage non disponible.
|
||||
Image storing failed. (%s)
|
||||
|
||||
Erreur d’enregistrement de l’image. (%s)
|
||||
Bad gd_security_image_id.
|
||||
|
||||
Invalide gd_security_image_id.
|
||||
Please type the six characters from the anti-spam image
|
||||
|
||||
Entrez les six caractères de l’image anti-spam
|
||||
Submit
|
||||
|
||||
Soumettre
|
||||
CAPTCHA
|
||||
CAPTCHA
|
||||
|
||||
You did not answer correctly.
|
||||
Vous n’avez pas répondu correctement.
|
||||
$GdSecurityImageFont is not set.
|
||||
|
||||
$GdSecurityImageFont n’est pas défini.
|
||||
################################################################################
|
||||
# modules/git-another.pl
|
||||
################################################################################
|
||||
No summary provided
|
||||
|
||||
Aucun résumé fourni
|
||||
################################################################################
|
||||
# modules/git.pl
|
||||
################################################################################
|
||||
no summary available
|
||||
|
||||
aucun résumé disponible
|
||||
page was marked for deletion
|
||||
|
||||
page marquée pour suppression
|
||||
Oddmuse
|
||||
Oddmuse
|
||||
|
||||
Cleaning up git repository
|
||||
|
||||
Nettoyage du dépôt git
|
||||
################################################################################
|
||||
# modules/google-plus-one.pl
|
||||
################################################################################
|
||||
@@ -880,7 +880,7 @@ This page lists the twenty last diary entries and their +1 buttons.
|
||||
# modules/gravatar.pl
|
||||
################################################################################
|
||||
Email:
|
||||
|
||||
Courriel :
|
||||
################################################################################
|
||||
# modules/header-and-footer-templates.pl
|
||||
################################################################################
|
||||
@@ -902,53 +902,53 @@ Index
|
||||
# modules/joiner.pl
|
||||
################################################################################
|
||||
The username %s already exists.
|
||||
|
||||
Le nom d’utilisateur %s existe déjà.
|
||||
The email address %s has already been used.
|
||||
|
||||
L’adresse courriel %s a déjà été utilisée.
|
||||
Wait %s minutes before try again.
|
||||
|
||||
Attendez %s minutes avant de réessayer.
|
||||
Registration Confirmation
|
||||
|
||||
Confirmation de l’enregistrement
|
||||
Visit the link below to confirm registration.
|
||||
|
||||
Visitez le lien ci-dessous pour confirmer l’enregistrement.
|
||||
Recover Account
|
||||
|
||||
Restaurer le compte
|
||||
You can login by following the link below. Then set new password.
|
||||
|
||||
Change Email Address
|
||||
|
||||
Changer l’adresse courriel
|
||||
To confirm changing email address, follow the link below.
|
||||
|
||||
To submit this form you must answer this question:
|
||||
|
||||
Question:
|
||||
|
||||
Question:
|
||||
CAPTCHA:
|
||||
CAPTCHA:
|
||||
|
||||
Registration
|
||||
|
||||
Enregistrement
|
||||
The username must be valid page name.
|
||||
|
||||
Le nom d’utilisateur doit être un nom de page valide.
|
||||
Confirmation email will be sent to the email address.
|
||||
|
||||
Un courriel de confirmation sera envoyé à l’adresse courriel.
|
||||
Repeat Password:
|
||||
|
||||
Répétez le mot de passse :
|
||||
Bad email address format.
|
||||
|
||||
Format d’adresse courriel invalide.
|
||||
Password needs to have at least %s characters.
|
||||
|
||||
Le mot de passe doit avoir au moins %s caractères.
|
||||
Passwords differ.
|
||||
|
||||
Mots de passe différents.
|
||||
Email Sent
|
||||
|
||||
Courriel envoyé
|
||||
Confirmation email has been sent to %s. Visit the link on the mail to confirm registration.
|
||||
|
||||
Courriel de confirmation envoyé à %s. Visitez le lien du courriel de confirmation d'enregistrement.
|
||||
Failed to Confirm Registration
|
||||
|
||||
Echec de confirmation d'enregistrement.
|
||||
Invalid key.
|
||||
|
||||
Clé non valide.
|
||||
The key expired.
|
||||
|
||||
Clé expirée.
|
||||
Registration Confirmed
|
||||
|
||||
Now, you can login by using username and password.
|
||||
@@ -956,65 +956,65 @@ Now, you can login by using username and password.
|
||||
Forgot your password?
|
||||
Mot de passe oublié ?
|
||||
Login failed.
|
||||
|
||||
Connexion échouée.
|
||||
You are banned.
|
||||
|
||||
You must confirm email address.
|
||||
|
||||
Vous devez confirmer l'adresse courriel.
|
||||
Logged in
|
||||
|
||||
Connecté
|
||||
%s has logged in.
|
||||
|
||||
%s est connecté
|
||||
You should set new password immediately.
|
||||
|
||||
Vous devriez définir un nouveau mot de passe immédiatement.
|
||||
Change Password
|
||||
|
||||
Changer le mot de passe
|
||||
Logged out
|
||||
|
||||
Déconnecté
|
||||
%s has logged out.
|
||||
|
||||
%s s’est déconnecté
|
||||
Account Settings
|
||||
|
||||
Paramètres de compte
|
||||
Logout
|
||||
Se déconnecter
|
||||
Current Password:
|
||||
|
||||
Mot de passe actuel:
|
||||
New Password:
|
||||
|
||||
Nouveau mot de passe:
|
||||
Repeat New Password:
|
||||
|
||||
Répétez le mot de passe:
|
||||
Password is wrong.
|
||||
|
||||
Mot de passe incorrect.
|
||||
Password Changed
|
||||
|
||||
Mot de passe modifié
|
||||
Your password has been changed.
|
||||
Votre mot de passe a été modifié.
|
||||
Forgot Password
|
||||
|
||||
Mot de passe oublié
|
||||
Enter email address, and recovery login ticket will be sent.
|
||||
|
||||
Entrez une adresse courriel, un ticket de récupération de connexion sera envoyé.
|
||||
Not found.
|
||||
|
||||
Non trouvé.
|
||||
The mail address is not valid anymore.
|
||||
|
||||
L’adresse courriel n’est plus valide.
|
||||
An email has been sent to %s with further instructions.
|
||||
|
||||
Un courriel a été envoyé à %s avec les instructions complémentaires.
|
||||
New Email Address:
|
||||
|
||||
Nouvelle adresse courriel:
|
||||
Failed to load account.
|
||||
|
||||
Echec du chargement du compte.
|
||||
An email has been sent to %s with a login ticket.
|
||||
|
||||
Un courriel a été envoyé à %s avec un ticket de connexion.
|
||||
Confirmation Failed
|
||||
|
||||
Echec de confirmation
|
||||
Failed to confirm.
|
||||
|
||||
Echec de confirmation
|
||||
Email Address Changed
|
||||
|
||||
Adresse courriel modifiée
|
||||
Email address for %1 has been changed to %2.
|
||||
|
||||
Adresse courriel pour %1 modifiée en %2.
|
||||
Account Management
|
||||
|
||||
Gestion des comptes
|
||||
Ban Account
|
||||
|
||||
Enter username of the account to ban:
|
||||
@@ -1034,7 +1034,7 @@ Unban
|
||||
%s has been unbanned.
|
||||
|
||||
Register
|
||||
|
||||
Enregistrement
|
||||
################################################################################
|
||||
# modules/lang.pl
|
||||
################################################################################
|
||||
@@ -1052,7 +1052,7 @@ Voir !
|
||||
====1 person liked this====
|
||||
|
||||
I like this!
|
||||
|
||||
J’aime!
|
||||
################################################################################
|
||||
# modules/link-all.pl
|
||||
################################################################################
|
||||
@@ -1091,9 +1091,9 @@ Le modèle %s est soit vide soit n'existe pas.
|
||||
# modules/localnames.pl
|
||||
################################################################################
|
||||
Name:
|
||||
|
||||
Nom :
|
||||
URL:
|
||||
URL:
|
||||
|
||||
Define Local Names
|
||||
|
||||
Define external redirect:
|
||||
@@ -1106,7 +1106,7 @@ Noms locaux définis sur %1 : %2
|
||||
# modules/logbannedcontent.pl
|
||||
################################################################################
|
||||
IP number matched %s
|
||||
|
||||
Numéro IP correspond à %s
|
||||
################################################################################
|
||||
# modules/login.pl
|
||||
################################################################################
|
||||
@@ -1209,8 +1209,8 @@ All mail subscriptions
|
||||
Tous les abonnements e-mail
|
||||
Subscriptions
|
||||
Abonnements
|
||||
Email:
|
||||
|
||||
Email:
|
||||
Courriel :
|
||||
Show
|
||||
Voir
|
||||
Subscriptions for %s:
|
||||
@@ -1247,30 +1247,30 @@ Help convert %s to Markdown
|
||||
List all non-Markdown pages
|
||||
|
||||
Converting %s
|
||||
|
||||
Conversion de %s
|
||||
Candidates for Conversion to Markdown
|
||||
|
||||
################################################################################
|
||||
# modules/module-bisect.pl
|
||||
################################################################################
|
||||
Bisect modules
|
||||
|
||||
Modules Bisect
|
||||
Module Bisect
|
||||
Module Bisect
|
||||
|
||||
All modules enabled now!
|
||||
|
||||
Tous les modules activés maintenant !
|
||||
Go back
|
||||
|
||||
Retour
|
||||
Test / Always enabled / Always disabled
|
||||
|
||||
Start
|
||||
|
||||
Démarrer
|
||||
Bisecting proccess is already active.
|
||||
|
||||
Stop
|
||||
|
||||
Stop
|
||||
It seems like module %s is causing your problem.
|
||||
|
||||
Le module %s sembler causer votre problème.
|
||||
Please note that this module does not handle situations when your problem is caused by a combination of specific modules (which is rare anyway).
|
||||
|
||||
Good luck fixing your problem! ;)
|
||||
@@ -1278,13 +1278,13 @@ Good luck fixing your problem! ;)
|
||||
Module count (only testable modules):
|
||||
|
||||
Current module statuses:
|
||||
|
||||
Statuts du module courant :
|
||||
Good
|
||||
|
||||
Bon
|
||||
Bad
|
||||
|
||||
Mauvais
|
||||
Enabling %s
|
||||
|
||||
Activer %s
|
||||
################################################################################
|
||||
# modules/module-updater.pl
|
||||
################################################################################
|
||||
@@ -1357,9 +1357,9 @@ L'extension "404 handler" nécessite une base de données de liens (links.pl).
|
||||
# modules/offline.pl
|
||||
################################################################################
|
||||
Make available offline
|
||||
|
||||
Rendre disponible hors ligne
|
||||
Offline
|
||||
|
||||
Hors ligne
|
||||
You are currently offline and what you requested is not part of the offline application. You need to be online to do this.
|
||||
|
||||
################################################################################
|
||||
@@ -1424,25 +1424,25 @@ Portrait
|
||||
# modules/preview.pl
|
||||
################################################################################
|
||||
Pages with changed HTML
|
||||
|
||||
Pages avec HTML modifié
|
||||
Preview changes in HTML output
|
||||
|
||||
Visualiser les changenements de la sortie HTML
|
||||
################################################################################
|
||||
# modules/private-pages.pl
|
||||
################################################################################
|
||||
This page is password protected. If you know the password, you can %s. Once you have done that, return and reload this page.
|
||||
|
||||
supply the password now
|
||||
|
||||
donner le mot de passe maintenant
|
||||
################################################################################
|
||||
# modules/private-wiki.pl
|
||||
################################################################################
|
||||
This error should not happen. If your password is set correctly and you are still seeing this message, then it is a bug, please report it. If you are just a stranger and trying to get unsolicited access, then keep in mind that all of the data is encrypted with AES-256 and the key is not stored on the server, good luck.
|
||||
|
||||
Attempt to read encrypted data without a password.
|
||||
|
||||
Tentative de lire des données cryptés sans mot de passe.
|
||||
Cannot refresh index.
|
||||
|
||||
Impossible de mettre à jour l'index
|
||||
################################################################################
|
||||
# modules/publish.pl
|
||||
################################################################################
|
||||
@@ -1493,17 +1493,17 @@ Index de toutes les pages de petite taille
|
||||
# modules/sort.pl
|
||||
################################################################################
|
||||
Sort alphabetically
|
||||
|
||||
Trier alphabétiquement
|
||||
Sorted alphabetically
|
||||
|
||||
Trié alphabétiquement
|
||||
Sorted by last update first
|
||||
|
||||
Trié par dernière modification en premier
|
||||
Sort by last update
|
||||
|
||||
Trier par dernière modification
|
||||
Sorted by creation date
|
||||
|
||||
Trié par date de création
|
||||
Sort by creation date
|
||||
|
||||
Trier par date de création
|
||||
################################################################################
|
||||
# modules/static-copy.pl
|
||||
################################################################################
|
||||
@@ -1581,7 +1581,7 @@ Failed to run %1 to create thumbnail: %2
|
||||
%s ran into an error
|
||||
|
||||
%s produced no output
|
||||
|
||||
%s n’a produit aucun résultat
|
||||
Failed to parse %s.
|
||||
|
||||
################################################################################
|
||||
@@ -1615,13 +1615,13 @@ Traduire %s
|
||||
Thank you for writing a translation of %s.
|
||||
Merci pour la traduction de %s.
|
||||
Please indicate what language you will be using.
|
||||
Merci d'indiquer quelle langue vous allez utiliser.
|
||||
Merci d’indiquer quelle langue vous allez utiliser.
|
||||
Language is missing
|
||||
La langue est manquante
|
||||
Suggested languages:
|
||||
Langues suggérées
|
||||
Please indicate a page name for the translation of %s.
|
||||
Indiquez s'il vous plaît un nom de page pour la traduction de %s.
|
||||
Indiquez s’il vous plaît un nom de page pour la traduction de %s.
|
||||
More help may be available here: %s.
|
||||
Plus d'aide disponible ici : %s.
|
||||
Translated page:
|
||||
@@ -1647,11 +1647,11 @@ Upgrading Database
|
||||
Did the previous upgrade end with an error? A lock was left behind.
|
||||
|
||||
Unlock wiki
|
||||
|
||||
Déverrouiller le wiki
|
||||
Upgrade complete.
|
||||
|
||||
Mise à jour terminée
|
||||
Upgrade complete. Please remove $ModuleDir/upgade.pl, now.
|
||||
|
||||
Mise à jour terminée. SVP, supprimez $ModuleDir/upgade.pl maintenant.
|
||||
################################################################################
|
||||
# modules/usemod.pl
|
||||
################################################################################
|
||||
|
||||
@@ -296,6 +296,8 @@ Delete
|
||||
|
||||
Filter:
|
||||
|
||||
Summary:
|
||||
|
||||
Last edit
|
||||
|
||||
revision %s
|
||||
@@ -310,8 +312,6 @@ later minor edits
|
||||
|
||||
No diff available.
|
||||
|
||||
Summary:
|
||||
|
||||
Old revision:
|
||||
|
||||
Changed:
|
||||
@@ -1201,6 +1201,8 @@ Your mail subscriptions
|
||||
|
||||
All mail subscriptions
|
||||
|
||||
All mail subscribers
|
||||
|
||||
Subscriptions
|
||||
|
||||
Email:
|
||||
@@ -1227,6 +1229,8 @@ The remaining pages do not exist.
|
||||
|
||||
Unsubscribed %s from the following pages:
|
||||
|
||||
Unsubscribed %s from all pages.
|
||||
|
||||
Migrating Subscriptions
|
||||
|
||||
No non-migrated email addresses found, migration not necessary.
|
||||
@@ -1409,6 +1413,11 @@ Click to search for references to this permanent anchor
|
||||
|
||||
Include permanent anchors
|
||||
|
||||
################################################################################
|
||||
# modules/pingback-server.pl
|
||||
################################################################################
|
||||
Only XML-RPC POST requests recognised
|
||||
|
||||
################################################################################
|
||||
# modules/portrait-support.pl
|
||||
################################################################################
|
||||
@@ -1473,6 +1482,17 @@ Referrers
|
||||
################################################################################
|
||||
All Referrers
|
||||
|
||||
################################################################################
|
||||
# modules/rename-pages.pl
|
||||
################################################################################
|
||||
Target page already exists.
|
||||
|
||||
Source page does not exist.
|
||||
|
||||
Copied from %s
|
||||
|
||||
Moved to %s
|
||||
|
||||
################################################################################
|
||||
# modules/search-list.pl
|
||||
################################################################################
|
||||
@@ -1539,8 +1559,6 @@ Rebuilding index not done.
|
||||
|
||||
Rebuild tag index
|
||||
|
||||
list tags
|
||||
|
||||
tag cloud
|
||||
|
||||
################################################################################
|
||||
@@ -1649,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
|
||||
@@ -1697,5 +1707,56 @@ Edit %s.
|
||||
################################################################################
|
||||
Tags:
|
||||
|
||||
################################################################################
|
||||
# modules/webmention.pl
|
||||
################################################################################
|
||||
Webmention module requires $CommentsPrefix to be set
|
||||
|
||||
Webmention requires a POST request
|
||||
|
||||
Webmention requires x-www-form-urlencoded requests
|
||||
|
||||
Webmention must mention a specific page
|
||||
|
||||
Webmention must mention a valid page
|
||||
|
||||
Your IP number is blocked: %s
|
||||
|
||||
Webmention must mention an existing page
|
||||
|
||||
Webmention must mention source
|
||||
|
||||
Webmention must mention target
|
||||
|
||||
The URL is blocked: %s
|
||||
|
||||
Webmention source cannot be verified: %1 returns %2 %3
|
||||
|
||||
Webmention source does not link to %s
|
||||
|
||||
Webmention for %s already exists
|
||||
|
||||
Webmention OK!
|
||||
|
||||
Add webmentions
|
||||
|
||||
Webmentioning others from %s
|
||||
|
||||
Webmention!
|
||||
|
||||
No links found.
|
||||
|
||||
Webmentioning somebody from %s
|
||||
|
||||
Contacting %s
|
||||
|
||||
Target reports an error: %s
|
||||
|
||||
No Webmention URL found
|
||||
|
||||
Success: %s
|
||||
|
||||
Failure: %s
|
||||
|
||||
#
|
||||
END_OF_TRANSLATION
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
# Zrajm C Akfohg <zrajm@klingonska.org>
|
||||
# Copyright (c) 2004-06 Johan Adler <alltid@nyfiken.org>
|
||||
# Copyright (c) 2004 Zrajm C Akfohg <zrajm@klingonska.org>
|
||||
# Copyright (c) 2021 Tobias Fendin
|
||||
#
|
||||
# Permission is granted to copy, distribute and/or modify this
|
||||
# document under the terms of the GNU Free Documentation License,
|
||||
@@ -300,6 +301,8 @@ Delete
|
||||
|
||||
Filter:
|
||||
|
||||
Summary:
|
||||
Sammanfattning:
|
||||
Last edit
|
||||
|
||||
revision %s
|
||||
@@ -314,8 +317,6 @@ later minor edits
|
||||
|
||||
No diff available.
|
||||
Information om ändring är inte tillgänglig.
|
||||
Summary:
|
||||
Sammanfattning:
|
||||
Old revision:
|
||||
Gammal version:
|
||||
Changed:
|
||||
@@ -699,11 +700,6 @@ Clustermap
|
||||
Klusterkarta
|
||||
Pages without a Cluster
|
||||
Sidor utan kluster
|
||||
################################################################################
|
||||
# modules/comment-div-wrapper.pl
|
||||
################################################################################
|
||||
Comments:
|
||||
|
||||
################################################################################
|
||||
# modules/commentcount.pl
|
||||
################################################################################
|
||||
@@ -711,6 +707,11 @@ Comments on
|
||||
Kommentarer till
|
||||
Comment on
|
||||
Kommentar till
|
||||
################################################################################
|
||||
# modules/comment-div-wrapper.pl
|
||||
################################################################################
|
||||
Comments:
|
||||
|
||||
################################################################################
|
||||
# modules/compilation.pl
|
||||
################################################################################
|
||||
@@ -1205,6 +1206,8 @@ Your mail subscriptions
|
||||
|
||||
All mail subscriptions
|
||||
|
||||
All mail subscribers
|
||||
|
||||
Subscriptions
|
||||
|
||||
Email:
|
||||
@@ -1231,6 +1234,8 @@ The remaining pages do not exist.
|
||||
|
||||
Unsubscribed %s from the following pages:
|
||||
|
||||
Unsubscribed %s from all pages.
|
||||
|
||||
Migrating Subscriptions
|
||||
|
||||
No non-migrated email addresses found, migration not necessary.
|
||||
@@ -1304,6 +1309,11 @@ You linked more than %s times to the same domain. It would seem that only a spam
|
||||
|
||||
Namespaces
|
||||
|
||||
################################################################################
|
||||
# modules/nearlink-create.pl
|
||||
################################################################################
|
||||
(create locally)
|
||||
|
||||
################################################################################
|
||||
# modules/near-links.pl
|
||||
################################################################################
|
||||
@@ -1323,11 +1333,6 @@ EditNearLinks
|
||||
Redigera närlänkar
|
||||
The same page on other sites:
|
||||
Samma sida på andra siter:
|
||||
################################################################################
|
||||
# modules/nearlink-create.pl
|
||||
################################################################################
|
||||
(create locally)
|
||||
|
||||
################################################################################
|
||||
# modules/no-question-mark.pl
|
||||
################################################################################
|
||||
@@ -1413,6 +1418,11 @@ Click to search for references to this permanent anchor
|
||||
Klicka för att söka efter referenser till det här permanenta ankaret
|
||||
Include permanent anchors
|
||||
Med permanenta ankare
|
||||
################################################################################
|
||||
# modules/pingback-server.pl
|
||||
################################################################################
|
||||
Only XML-RPC POST requests recognised
|
||||
|
||||
################################################################################
|
||||
# modules/portrait-support.pl
|
||||
################################################################################
|
||||
@@ -1477,6 +1487,17 @@ Sidor som länkat hit
|
||||
################################################################################
|
||||
All Referrers
|
||||
Alla som länkat hit
|
||||
################################################################################
|
||||
# modules/rename-pages.pl
|
||||
################################################################################
|
||||
Target page already exists.
|
||||
|
||||
Source page does not exist.
|
||||
|
||||
Copied from %s
|
||||
|
||||
Moved to %s
|
||||
|
||||
################################################################################
|
||||
# modules/search-list.pl
|
||||
################################################################################
|
||||
@@ -1486,22 +1507,22 @@ Page list for %s
|
||||
# modules/small.pl
|
||||
################################################################################
|
||||
Index of all small pages
|
||||
|
||||
Index av alla små sidor
|
||||
################################################################################
|
||||
# modules/sort.pl
|
||||
################################################################################
|
||||
Sort alphabetically
|
||||
|
||||
Sortera alfabetiskt
|
||||
Sorted alphabetically
|
||||
|
||||
Sorterad alfabetiskt
|
||||
Sorted by last update first
|
||||
|
||||
Sorterad med senast uppdaterad först
|
||||
Sort by last update
|
||||
|
||||
Sortera på uppdateringstid
|
||||
Sorted by creation date
|
||||
|
||||
Sorterad på skapande datum
|
||||
Sort by creation date
|
||||
|
||||
Sortera på skapande datum
|
||||
################################################################################
|
||||
# modules/static-copy.pl
|
||||
################################################################################
|
||||
@@ -1518,35 +1539,33 @@ Editing not allowed for %s.
|
||||
# modules/svg-edit.pl
|
||||
################################################################################
|
||||
Edit image in the browser
|
||||
|
||||
Redigera bilden i webbläsaren
|
||||
Summary of your changes:
|
||||
|
||||
Sammanställning av dina ändringar:
|
||||
################################################################################
|
||||
# modules/sync.pl
|
||||
################################################################################
|
||||
Copy to %1 succeeded: %2.
|
||||
|
||||
Kopiering till %1 lyckades: %2.
|
||||
Copy to %1 failed: %2.
|
||||
|
||||
Kopiering till %1 misslyckades: %2.
|
||||
################################################################################
|
||||
# modules/tags.pl
|
||||
################################################################################
|
||||
Tag
|
||||
|
||||
Tagg
|
||||
Feed for this tag
|
||||
|
||||
Flöde för denna tagg
|
||||
Tag Cloud
|
||||
|
||||
Taggmoln
|
||||
Rebuilding index not done.
|
||||
|
||||
Ombyggnad av index inte klart.
|
||||
(Rebuilding the index can only be done once every 12 hours.)
|
||||
|
||||
(Ombyggnad av indexet kan endast göras var 12:e timme.)
|
||||
Rebuild tag index
|
||||
|
||||
list tags
|
||||
|
||||
Bygg om tagg index
|
||||
tag cloud
|
||||
|
||||
taggmoln
|
||||
################################################################################
|
||||
# modules/templates.pl
|
||||
################################################################################
|
||||
@@ -1556,41 +1575,41 @@ Eller använd en av följande mallar:
|
||||
# modules/throttle.pl
|
||||
################################################################################
|
||||
Too many instances. Only %s allowed.
|
||||
|
||||
För många instanser. Endast %s tillåtna.
|
||||
Please try again later. Perhaps somebody is running maintenance or doing a long search. Unfortunately the site has limited resources, and so we must ask you for a bit of patience.
|
||||
|
||||
Vänligen försök senare. Kanske pågår ett underhåll eller så har någon gjort en lång sökning. Dessvärre har denna sida begränsade resurser, så vi ber dig ha tålamod.
|
||||
################################################################################
|
||||
# modules/thumbs.pl
|
||||
################################################################################
|
||||
thumb
|
||||
|
||||
Error creating thumbnail from nonexisting page %s.
|
||||
|
||||
Misslyckades med att skapa miniatyr av en icke existerande sida %s.
|
||||
Can not create thumbnail for file type %s.
|
||||
|
||||
Kan inte skapa miniatyr för filtypen %s.
|
||||
Can not create thumbnail for a text document
|
||||
|
||||
Kan inte skapa en miniatyr av ett textdokument
|
||||
Can not create path for thumbnail - %s
|
||||
|
||||
Kan inte skapa sökväg för miniatyr - %s
|
||||
Could not open %s for writing whilst trying to save image before creating thumbnail. Check write permissions.
|
||||
|
||||
Kan inte öppna %s för skrivning före skapande av miniatyrbilden. Kontrollera skrivrättigheter.
|
||||
Failed to run %1 to create thumbnail: %2
|
||||
|
||||
Kunde inte köra %1 för att skapa miniatyren: %2
|
||||
%s ran into an error
|
||||
|
||||
%s misslyckades
|
||||
%s produced no output
|
||||
|
||||
%s producerade ingen utdata
|
||||
Failed to parse %s.
|
||||
|
||||
Misslyckades med att tolka %s.
|
||||
################################################################################
|
||||
# modules/timezone.pl
|
||||
################################################################################
|
||||
Timezone
|
||||
|
||||
Tidszon
|
||||
Pick your timezone:
|
||||
|
||||
Välj din tidszon:
|
||||
Set
|
||||
|
||||
Sätt
|
||||
################################################################################
|
||||
# modules/toc-headers.pl
|
||||
################################################################################
|
||||
@@ -1600,7 +1619,7 @@ Innehåll
|
||||
# modules/today.pl
|
||||
################################################################################
|
||||
Create a new page for today
|
||||
|
||||
Skapa en ny sida för idag
|
||||
################################################################################
|
||||
# modules/translation-links.pl
|
||||
################################################################################
|
||||
@@ -1641,15 +1660,15 @@ Sidan finns inte.
|
||||
# modules/upgrade.pl
|
||||
################################################################################
|
||||
Upgrading Database
|
||||
|
||||
Uppgradering av databas
|
||||
Did the previous upgrade end with an error? A lock was left behind.
|
||||
|
||||
Misslyckades den senaste uppgraderingen med ett fel? Ett lås finns kvar.
|
||||
Unlock wiki
|
||||
|
||||
Lås upp wiki
|
||||
Upgrade complete.
|
||||
|
||||
Uppgradering klar.
|
||||
Upgrade complete. Please remove $ModuleDir/upgade.pl, now.
|
||||
|
||||
Uppgradering klar. Vänlingen ta bort $ModuleDir/upgade.pl nu.
|
||||
################################################################################
|
||||
# modules/usemod.pl
|
||||
################################################################################
|
||||
@@ -1676,7 +1695,7 @@ Wanted Pages
|
||||
# modules/webapp.pl
|
||||
################################################################################
|
||||
Web application for offline browsing
|
||||
|
||||
Webbapplikation för off-line visning
|
||||
################################################################################
|
||||
# modules/webdav.pl
|
||||
################################################################################
|
||||
@@ -1691,15 +1710,68 @@ Blogg
|
||||
# modules/weblog-3.pl
|
||||
################################################################################
|
||||
Matching pages:
|
||||
|
||||
Matchande sidor:
|
||||
New
|
||||
|
||||
Nytt
|
||||
Edit %s.
|
||||
|
||||
Redigera %s.
|
||||
################################################################################
|
||||
# modules/weblog-4.pl
|
||||
################################################################################
|
||||
Tags:
|
||||
Taggar:
|
||||
|
||||
################################################################################
|
||||
# modules/webmention.pl
|
||||
################################################################################
|
||||
Webmention module requires $CommentsPrefix to be set
|
||||
Webmention modulen kräver att $CommentsPrefix är satt
|
||||
Webmention requires a POST request
|
||||
Webmention kräver en POST förfrågan
|
||||
Webmention requires x-www-form-urlencoded requests
|
||||
Webmention kräver x-www-form-urlencoded förfrågan
|
||||
Webmention must mention a specific page
|
||||
Webmention måste ange en specifik sida
|
||||
Webmention must mention a valid page
|
||||
Webmention måste ange en giltig sida
|
||||
Your IP number is blocked: %s
|
||||
Ditt IP-nummer är blockerat: %s
|
||||
Webmention must mention an existing page
|
||||
Webmention måste ange en existerande sida
|
||||
Webmention must mention source
|
||||
Webmention måste ange en källa
|
||||
Webmention must mention target
|
||||
Webmention måste ange ett mål
|
||||
The URL is blocked: %s
|
||||
URL:en är blockerad: %s
|
||||
Webmention source cannot be verified: %1 returns %2 %3
|
||||
Webmention källan kan inte verifieras: %1 returnerar %2 %3
|
||||
Webmention source does not link to %s
|
||||
Webmention källan länkar inte till %s
|
||||
Webmention for %s already exists
|
||||
Webmention för %s finns redan
|
||||
Webmention OK!
|
||||
|
||||
Add webmentions
|
||||
Lägg till webmention
|
||||
# Could not find a good translation of "Webmentioning"
|
||||
Webmentioning others from %s
|
||||
Webmentioning andra från %s
|
||||
Webmention!
|
||||
|
||||
No links found.
|
||||
Inga länkar hittade
|
||||
Webmentioning somebody from %s
|
||||
Webmentioning någon från %s
|
||||
Contacting %s
|
||||
Kontakta %s
|
||||
Target reports an error: %s
|
||||
Målet rapporterar ett fel: %s
|
||||
No Webmention URL found
|
||||
Ingen Webmention URL hittad
|
||||
Success: %s
|
||||
Lyckat: %s
|
||||
Failure: %s
|
||||
Misslyckat: %s
|
||||
#
|
||||
END_OF_TRANSLATION
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
227
modules/webmention.pl
Normal file
227
modules/webmention.pl
Normal file
@@ -0,0 +1,227 @@
|
||||
# Copyright (C) 2004 Brock Wilcox <awwaiid@thelackthereof.org>
|
||||
# Copyright (C) 2019 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 LWP::UserAgent;
|
||||
use Modern::Perl;
|
||||
use XML::LibXML;
|
||||
|
||||
AddModuleDescription('webmention.pl', 'Webmention Server Extension');
|
||||
|
||||
# Specification: https://www.w3.org/TR/webmention/
|
||||
|
||||
our ($CommentsPrefix, $q, $HtmlHeaders, %Action, $QuestionaskerSecretKey,
|
||||
@MyInitVariables, %IndexHash, $BannedContent, $UsePathInfo, $HomePage,
|
||||
$Message, @MyAdminCode, $FullUrlPattern);
|
||||
|
||||
push(@MyInitVariables, \&WebmentionServerAddLink, \&WebmentionAddAction);
|
||||
|
||||
# Add webmentions metadata to our pages
|
||||
|
||||
sub WebmentionServerAddLink {
|
||||
$Message .= T('Webmention module requires $CommentsPrefix to be set') unless $CommentsPrefix;
|
||||
# only allow linking to reasonable pages: no URL parameters!
|
||||
my @params = $q->param;
|
||||
return unless GetParam('action', 'browse') eq 'browse';
|
||||
return if GetParam('revision');
|
||||
my $id = GetId() || $HomePage;
|
||||
return if $id =~ /^$CommentsPrefix/;
|
||||
my $link = '<link rel="webmention" type="application/wiki" href="'
|
||||
. ScriptUrl('webmention/' . UrlEncode($id)) . '" />';
|
||||
$HtmlHeaders .= $link unless $HtmlHeaders =~ /rel="webmention"/;
|
||||
}
|
||||
|
||||
sub WebmentionAddAction {
|
||||
SetParam('action', 'webmention') if $q->path_info =~ m|/webmention\b|;
|
||||
}
|
||||
|
||||
# Process incoming webmentions
|
||||
|
||||
$Action{webmention} = \&DoWebmentionServer;
|
||||
|
||||
sub DoWebmentionServer {
|
||||
my $id = FreeToNormal(shift);
|
||||
|
||||
# some sanity checks for the request
|
||||
if ($q->request_method() ne 'POST') {
|
||||
ReportError(T('Webmention requires a POST request'), '400 BAD REQUEST');
|
||||
}
|
||||
if ($q->content_type() ne 'application/x-www-form-urlencoded') {
|
||||
ReportError(T('Webmention requires x-www-form-urlencoded requests'), '400 BAD REQUEST');
|
||||
}
|
||||
|
||||
# some sanity checks for the target page name
|
||||
if (not $id) {
|
||||
ReportError(T('Webmention must mention a specific page'), '400 BAD REQUEST');
|
||||
}
|
||||
my $error = ValidId($id);
|
||||
if ($error) {
|
||||
ReportError(T('Webmention must mention a valid page'), '400 BAD REQUEST');
|
||||
}
|
||||
|
||||
# check the IP number for bans
|
||||
my $rule = UserIsBanned();
|
||||
if ($rule) {
|
||||
ReportError(Ts('Your IP number is blocked: %s', $rule), '403 FORBIDDEN');
|
||||
}
|
||||
|
||||
# check that the target page exists
|
||||
AllPagesList();
|
||||
if (not $IndexHash{$id}) {
|
||||
ReportError(T('Webmention must mention an existing page'), '404 NOT FOUND');
|
||||
}
|
||||
|
||||
# verify parameters
|
||||
my $source = GetParam('source', undef) or ReportError(T('Webmention must mention source'), '400 BAD REQUEST');
|
||||
my $target = GetParam('target', undef) or ReportError(T('Webmention must mention target'), '400 BAD REQUEST');
|
||||
|
||||
# verify that the source isn't banned
|
||||
$rule = BannedContent($source);
|
||||
if ($rule) {
|
||||
ReportError(Ts('The URL is blocked: %s', $rule), '403 FORBIDDEN');
|
||||
}
|
||||
|
||||
# verify that the webmention is legit
|
||||
my $ua = LWP::UserAgent->new(agent => 'Oddmuse Webmention Server/0.1');
|
||||
my $response = $ua->get($source);
|
||||
if (not $response->is_success) {
|
||||
ReportError(Tss('Webmention source cannot be verified: %1 returns %2 %3',
|
||||
$source, $response->code, $response->message), '400 BAD REQUEST');
|
||||
}
|
||||
my $self = ScriptUrl(UrlEncode($id));
|
||||
if ($response->decoded_content !~ /$self/) {
|
||||
ReportError(Ts('Webmention source does not link to %s', $self), '400 BAD REQUEST');
|
||||
}
|
||||
$id = $CommentsPrefix . $id;
|
||||
if (GetPageContent($id) =~ /$source/) {
|
||||
ReportError(Ts('Webmention for %s already exists', $source), '400 BAD REQUEST');
|
||||
}
|
||||
|
||||
# try to determine a name and a link
|
||||
my ($username, $homepage);
|
||||
my $parser = XML::LibXML->new(recover => 2);
|
||||
my $dom = $parser->load_html(string => $response->decoded_content);
|
||||
my @nodes = $dom->findnodes('//*[@rel="author"]');
|
||||
if (@nodes) {
|
||||
my $node = shift @nodes;
|
||||
$username = $node->textContent;
|
||||
$homepage = $node->getAttribute('href');
|
||||
}
|
||||
|
||||
# post a comment without redirect at the end
|
||||
SetParam('aftertext', 'Webmention: ' . $source);
|
||||
SetParam('summary', 'Webmention');
|
||||
SetParam('username', $username || T('Anonymous'));
|
||||
SetParam('homepage', $homepage);
|
||||
SetParam($QuestionaskerSecretKey, 1) if $QuestionaskerSecretKey;
|
||||
local *ReBrowsePage = sub {};
|
||||
DoPost($id);
|
||||
|
||||
# response
|
||||
print GetHeader('', T('Webmention OK!'));
|
||||
print $q->start_div({-class=>'content webmention'}),
|
||||
$q->p(GetPageLink($BannedContent)),
|
||||
$q->end_div;
|
||||
PrintFooter();
|
||||
}
|
||||
|
||||
# Allow user to webmention other sites
|
||||
|
||||
push(@MyAdminCode, \&WebmentionMenu);
|
||||
|
||||
sub WebmentionMenu {
|
||||
my ($id, $menuref, $restref) = @_;
|
||||
if ($id) {
|
||||
push(@$menuref, ScriptLink('action=webmentions;id=' . $id, T('Add webmentions'), 'webmentions'));
|
||||
}
|
||||
}
|
||||
|
||||
$Action{webmentions} = \&DoWebmentionMenu;
|
||||
|
||||
sub DoWebmentionMenu {
|
||||
my $id = GetId();
|
||||
ValidIdOrDie($id);
|
||||
print GetHeader('', Ts('Webmentioning others from %s', NormalToFree($id)), '');
|
||||
my $text = GetPageContent($id);
|
||||
my @urls = $text =~ /$FullUrlPattern/g;
|
||||
if (@urls) {
|
||||
print GetFormStart();
|
||||
print GetHiddenValue('action', 'webmentioning');
|
||||
print GetHiddenValue('from', UrlEncode($id));
|
||||
print '<p>';
|
||||
print $q->checkbox_group('to', \@urls, undef, 'true');
|
||||
print '</p>';
|
||||
print $q->submit('go', T('Webmention!'));
|
||||
} else {
|
||||
print $q->p(T('No links found.'));
|
||||
}
|
||||
PrintFooter();
|
||||
}
|
||||
|
||||
$Action{webmentioning} = \&DoWebmention;
|
||||
|
||||
sub DoWebmention {
|
||||
my $id = GetParam('from');
|
||||
ValidIdOrDie($id);
|
||||
my $from = ScriptUrl($id);
|
||||
my @to = $q->multi_param('to');
|
||||
ReportError('Missing target') unless @to;
|
||||
print GetHeader('', Ts('Webmentioning somebody from %s', NormalToFree($id)), '');
|
||||
for my $to (@to) {
|
||||
Webmention($from, $to);
|
||||
}
|
||||
PrintFooter();
|
||||
}
|
||||
|
||||
sub Webmention {
|
||||
my ($from, $to) = @_;
|
||||
ReportError('Target must be an URL', '400 BAD REQUEST', 0, $q->p($to)) unless $to =~ /$FullUrlPattern/;
|
||||
my $ua = LWP::UserAgent->new(agent => "Oddmuse Webmention Client/0.1");
|
||||
|
||||
print $q->p(Ts('Contacting %s', $to));
|
||||
my $response = $ua->get($to);
|
||||
if (!$response->is_success) {
|
||||
print $q->p(Ts('Target reports an error: %s', $response->status_line));
|
||||
return;
|
||||
}
|
||||
|
||||
print $q->p("Parsing response");
|
||||
my $data = $response->decoded_content;
|
||||
my $parser = XML::LibXML->new(recover => 2);
|
||||
my $dom = $parser->load_html(string => $data);
|
||||
my $webmention = $dom->findvalue('//link[@rel="webmention"]/@href');
|
||||
|
||||
if (!$webmention) {
|
||||
print $q->p(T('No Webmention URL found'));
|
||||
return;
|
||||
}
|
||||
|
||||
print $q->p("Webmention URL is $webmention");
|
||||
|
||||
$response = $ua->post($webmention, { source => $from, target => $to });
|
||||
my $message = $response->code . " " . $response->message;
|
||||
if ($response->is_success) {
|
||||
print $q->p(Ts("Success: %s", $message));
|
||||
} else {
|
||||
print $q->p(Ts("Failure: %s", $message));
|
||||
$dom = $parser->load_html(string => $response->decoded_content());
|
||||
for my $node ($dom->getElementsByTagName('script')) { $node->parentNode->removeChild($node) };
|
||||
for my $node ($dom->getElementsByTagName('style')) { $node->parentNode->removeChild($node) };
|
||||
print $q->p($dom->textContent);
|
||||
}
|
||||
}
|
||||
61
modules/wordcount.pl
Normal file
61
modules/wordcount.pl
Normal file
@@ -0,0 +1,61 @@
|
||||
# Copyright (C) 2005 Robin V. Stacey (robin@greywulf.net)
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the
|
||||
# Free Software Foundation, Inc.
|
||||
# 59 Temple Place, Suite 330
|
||||
# Boston, MA 02111-1307 USA
|
||||
|
||||
# This module adds a wordcount to the bottom of edit boxes. The javascript code is munged from
|
||||
# Richard Livsey's Textarea Tools page: http://livsey.org/experiments/textareatools/
|
||||
# Though I've stripped it down to it's barest necessities
|
||||
|
||||
use strict;
|
||||
use v5.10;
|
||||
|
||||
our (@MyInitVariables, $HtmlHeaders, $EditNote);
|
||||
|
||||
AddModuleDescription('wordcount.pl', 'Word Count Extension');
|
||||
|
||||
push(@MyInitVariables, \&WordcountAddScript);
|
||||
|
||||
sub WordcountAddScript {
|
||||
$HtmlHeaders .= "<script type='text/javascript'>
|
||||
function addEvent(obj, evType, fn) {
|
||||
if (obj.addEventListener) {
|
||||
obj.addEventListener(evType, fn, true);
|
||||
return true;
|
||||
} else if (obj.attachEvent) {
|
||||
var r = obj.attachEvent('on'+evType, fn);
|
||||
return r;
|
||||
} else { return false; }
|
||||
}
|
||||
|
||||
addEvent(window, 'load', function() {
|
||||
document.getElementById('textWordCount').innerHTML = numWords(document.getElementById('text').value);
|
||||
document.getElementById('text').onkeyup = function() {
|
||||
document.getElementById('textWordCount').innerHTML = numWords(document.getElementById('text').value);
|
||||
}
|
||||
});
|
||||
|
||||
function numWords(string) {
|
||||
string = string + ' ';
|
||||
string = string.replace(/^[^A-Za-z0-9]+/gi, '');
|
||||
string = string.replace(/[^A-Za-z0-9]+/gi, ' ');
|
||||
var items = string.split(' ');
|
||||
return items.length -1;
|
||||
}
|
||||
</script>";
|
||||
}
|
||||
|
||||
$EditNote = "Words: <span id='textWordCount'></span>" . $EditNote;
|
||||
94
scripts/ip-to-regexp.pl
Normal file
94
scripts/ip-to-regexp.pl
Normal file
@@ -0,0 +1,94 @@
|
||||
use Modern::Perl;
|
||||
use Net::Whois::Parser qw/parse_whois/;
|
||||
|
||||
sub main {
|
||||
my $ip = shift(@ARGV);
|
||||
die "Provide an IP number as argument.\n" unless $ip;
|
||||
print get_regexp_ip(get_range($ip)), "\n";
|
||||
}
|
||||
|
||||
sub get_range {
|
||||
my $ip = shift;
|
||||
my $response = parse_whois(domain => $ip);
|
||||
my $re = '(?:[0-9]{1,3}\.){3}[0-9]{1,3}';
|
||||
my ($start, $end) = $response->{inetnum} =~ /($re) *- *($re)/;
|
||||
return $start, $end;
|
||||
}
|
||||
|
||||
sub get_groups {
|
||||
my ($from, $to) = @_;
|
||||
my @groups;
|
||||
if ($from < 10) {
|
||||
my $to = $to >= 10 ? 9 : $to;
|
||||
push(@groups, [$from, $to]);
|
||||
$from = $to + 1;
|
||||
}
|
||||
while ($from < $to) {
|
||||
my $to = int($from/100) < int($to/100) ? $from + 99 - $from % 100 : $to;
|
||||
if ($from % 10) {
|
||||
push(@groups, [$from, $from + 9 - $from % 10]);
|
||||
$from += 10 - $from % 10;
|
||||
}
|
||||
if (int($from/10) < int($to/10)) {
|
||||
if ($to % 10 == 9) {
|
||||
push(@groups, [$from, $to]);
|
||||
$from = 1 + $to;
|
||||
} else {
|
||||
push(@groups, [$from, $to - 1 - $to % 10]);
|
||||
$from = $to - $to % 10;
|
||||
}
|
||||
} else {
|
||||
push(@groups, [$from - $from % 10, $to]);
|
||||
last;
|
||||
}
|
||||
if ($to % 10 != 9) {
|
||||
push(@groups, [$from, $to]);
|
||||
$from = 1 + $to; # jump from 99 to 100
|
||||
}
|
||||
}
|
||||
return \@groups;
|
||||
}
|
||||
|
||||
sub get_regexp_range {
|
||||
my @chars;
|
||||
for my $group (@{get_groups(@_)}) {
|
||||
my ($from, $to) = @$group;
|
||||
my $char;
|
||||
for (my $i = length($from); $i >= 1; $i--) {
|
||||
if (substr($from, - $i, 1) eq substr($to, - $i, 1)) {
|
||||
$char .= substr($from, - $i, 1);
|
||||
} else {
|
||||
$char .= '[' . substr($from, - $i, 1) . '-' . substr($to, - $i, 1). ']';
|
||||
}
|
||||
}
|
||||
push(@chars, $char);
|
||||
}
|
||||
return join('|', @chars);
|
||||
}
|
||||
|
||||
sub get_regexp_ip {
|
||||
my ($from, $to) = @_;
|
||||
my @start = split(/\./, $from);
|
||||
my @end = split(/\./, $to);
|
||||
my $regexp = "^";
|
||||
for my $i (0 .. 3) {
|
||||
if ($start[$i] eq $end[$i]) {
|
||||
$regexp .= $start[$i];
|
||||
} elsif ($start[$i] eq '0' and $end[$i] eq '255') {
|
||||
last;
|
||||
} elsif ($start[$i + 1] > 0) {
|
||||
$regexp .= '(' . $start[$i] . '\.('
|
||||
. get_regexp_range($start[$i + 1], '255') . ')|'
|
||||
. get_regexp_range($start[$i] + 1, $end[$i + 1]) . ')';
|
||||
$regexp .= '\.';
|
||||
last;
|
||||
} else {
|
||||
$regexp .= '(' . get_regexp_range($start[$i], $end[$i]) . ')$';
|
||||
last;
|
||||
}
|
||||
$regexp .= '\.' if $i < 3;
|
||||
}
|
||||
return $regexp;
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,5 +1,5 @@
|
||||
#! /usr/bin/perl
|
||||
# Copyright (C) 2010–2018 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2010–2019 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
|
||||
@@ -22,6 +22,8 @@ use MIME::Entity;
|
||||
use File::Temp;
|
||||
use File::Basename;
|
||||
use File::Path;
|
||||
use Net::SMTP;
|
||||
use Authen::SASL qw(Perl);
|
||||
|
||||
# This script can be invoked as follows:
|
||||
# perl rc2mail.pl -r http://localhost/cgi-bin/wiki \
|
||||
@@ -36,7 +38,8 @@ use File::Path;
|
||||
# gets http://localhost/cgi-bin/wiki?action=rss;days=1;full=1;short=0
|
||||
# And http://localhost/cgi-bin/wiki?action=subscriptionlist;raw=1;pwd=foo
|
||||
# -m user:password@mailhost for sending email using SMTP Auth. Without this
|
||||
# information, the script will send mail to localhost.
|
||||
# information, the script will send mail to localhost. The host can end
|
||||
# in a port number, e.g. "kensanata:*secret*@smtp.migadu.com:587"
|
||||
# -f email address to use as the sender.
|
||||
# -t timestamp file; it's last modified date is used to determine when the
|
||||
# the last run was and an appropriate URL is used. Instead of days=1 it
|
||||
@@ -170,37 +173,20 @@ sub send_mail {
|
||||
Path => $fh,
|
||||
Type=> "text/html");
|
||||
if ($host) {
|
||||
print "Sending $title to $subscriber using ${user}\@${host}\n" if $verbose;
|
||||
eval {
|
||||
require Net::SMTP::TLS;
|
||||
my $smtp = Net::SMTP::TLS->new($host,
|
||||
User => $user,
|
||||
Password => $password);
|
||||
$smtp->mail($from);
|
||||
$smtp->to($subscriber);
|
||||
print "$root\nSending $title to $subscriber using ${user}\@${host}\n" if $verbose;
|
||||
my $smtp = Net::SMTP->new($host, Debug => $debug);
|
||||
$smtp->starttls();
|
||||
# the following requires Authen::SASL!
|
||||
$smtp->auth($user, $password);
|
||||
$smtp->mail($from);
|
||||
if ($smtp->to($subscriber)) {
|
||||
$smtp->data;
|
||||
$smtp->datasend($mail->stringify);
|
||||
$smtp->dataend;
|
||||
$smtp->quit;
|
||||
};
|
||||
if ($@) {
|
||||
require Net::SMTP::SSL;
|
||||
my $smtp = Net::SMTP::SSL->new($host, Port => 465);
|
||||
$smtp->auth($user, $password);
|
||||
$smtp->mail($from);
|
||||
$smtp->to($subscriber);
|
||||
$smtp->data;
|
||||
$smtp->datasend($mail->stringify);
|
||||
$smtp->dataend;
|
||||
$smtp->quit;
|
||||
}
|
||||
} else {
|
||||
my @recipients = $mail->smtpsend();
|
||||
if (@recipients) {
|
||||
print "Sent $title to ", join(', ', @recipients), "\n" unless $quiet;
|
||||
} else {
|
||||
print "Failed to send $title to $subscriber\n" unless $quiet;
|
||||
warn "Error: ", $smtp->message();
|
||||
}
|
||||
$smtp->quit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
91
scripts/stats.pl
Executable file
91
scripts/stats.pl
Executable file
@@ -0,0 +1,91 @@
|
||||
#! /usr/bin/perl -w
|
||||
|
||||
# Copyright (C) 2005, 2007, 2021 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 Modern::Perl;
|
||||
|
||||
sub ParseData {
|
||||
my $data = shift;
|
||||
my %result;
|
||||
while ($data =~ /(\S+?): (.*?)(?=\n[^ \t]|\Z)/sg) {
|
||||
my ($key, $value) = ($1, $2);
|
||||
$value =~ s/\n\t/\n/g;
|
||||
$result{$key} = $value;
|
||||
}
|
||||
return %result;
|
||||
}
|
||||
|
||||
sub main {
|
||||
my ($PageDir) = @_;
|
||||
my $pages = 0;
|
||||
my $texts = 0;
|
||||
my $redirects = 0;
|
||||
my $files = 0;
|
||||
my $big = 0;
|
||||
# include dotfiles!
|
||||
local $/ = undef; # Read complete files
|
||||
say "Reading files...";
|
||||
my @files = glob("$PageDir/*.pg $PageDir/.*.pg");
|
||||
my $n = @files;
|
||||
local $| = 1; # flush!
|
||||
foreach my $file (@files) {
|
||||
if (not --$n % 10) {
|
||||
printf("\r%06d files to go", $n);
|
||||
}
|
||||
next unless $file =~ m|.*/(.+)\.pg$|;
|
||||
my $page = $1;
|
||||
open(F, $file) or die "Cannot read $page file: $!";
|
||||
my $data = <F>;
|
||||
close(F);
|
||||
my %result = ParseData($data);
|
||||
$pages++;
|
||||
if ($result{text} =~ /^#FILE /) {
|
||||
$files++;
|
||||
} elsif ($result{text} =~ /^#REDIRECT /) {
|
||||
$redirects++;
|
||||
} else {
|
||||
$texts++;
|
||||
$big++ if length($result{text}) > 15000;
|
||||
}
|
||||
}
|
||||
printf("\r%06d files to go\n", 0);
|
||||
printf("Pages: %7d\n", $pages);
|
||||
printf("Files: %7d\n", $files);
|
||||
printf("Redirects: %6d\n", $redirects);
|
||||
printf("Texts: %7d\n", $texts);
|
||||
printf("Big: %7d\n", $big);
|
||||
}
|
||||
|
||||
use Getopt::Long;
|
||||
my $regexp = undef;
|
||||
my $page = 'page';
|
||||
my $help;
|
||||
GetOptions ("page=s" => \$page,
|
||||
"help" => \$help);
|
||||
|
||||
if ($help) {
|
||||
print qq{
|
||||
Usage: $0 [--page DIR]
|
||||
|
||||
Prints some stats about the pages in DIR.
|
||||
|
||||
--page designates the page directory. By default this is 'page' in the
|
||||
current directory. If you run this script in your data directory,
|
||||
the default should be fine.
|
||||
}
|
||||
} else {
|
||||
main ($page);
|
||||
}
|
||||
@@ -1,24 +1,41 @@
|
||||
#!/usr/bin/env perl
|
||||
use Modern::Perl;
|
||||
use Mojolicious::Lite;
|
||||
use Mojo::Cache;
|
||||
use Archive::Tar;
|
||||
use File::Basename;
|
||||
use Sort::Versions;
|
||||
use Encode qw(decode_utf8);
|
||||
my $dir = "/home/alex/oddmuse.org/releases";
|
||||
my $cache = Mojo::Cache->new(max_keys => 50);
|
||||
|
||||
get '/' => sub {
|
||||
my $c = shift;
|
||||
my @tarballs = sort map {
|
||||
sub tarballs {
|
||||
my @tarballs = reverse sort versioncmp map {
|
||||
my ($name, $path, $suffix) = fileparse($_, '.tar.gz');
|
||||
$name;
|
||||
} <$dir/*.tar.gz>;
|
||||
$c->render(template => 'index', tarballs => \@tarballs);
|
||||
return \@tarballs;
|
||||
}
|
||||
|
||||
sub tarball {
|
||||
my $tarball = shift;
|
||||
if ($tarball eq 'latest') {
|
||||
my $tarballs = tarballs();
|
||||
$tarball = shift @$tarballs;
|
||||
}
|
||||
return $tarball;
|
||||
}
|
||||
|
||||
get '/' => sub {
|
||||
my $c = shift;
|
||||
my $tarballs = tarballs();
|
||||
unshift @$tarballs, 'latest';
|
||||
$c->render(template => 'index', tarballs => $tarballs);
|
||||
} => 'main';
|
||||
|
||||
get '/#tarball' => sub {
|
||||
my $c = shift;
|
||||
my $tarball = $c->param('tarball');
|
||||
my $tarball = tarball $c->param('tarball');
|
||||
my $files = $cache->get($tarball);
|
||||
if (not $files) {
|
||||
$c->app->log->info("Reading $tarball.tar.gz");
|
||||
@@ -36,7 +53,7 @@ get '/#tarball' => sub {
|
||||
|
||||
get '/#tarball/#file' => sub {
|
||||
my $c = shift;
|
||||
my $tarball = $c->param('tarball');
|
||||
my $tarball = tarball $c->param('tarball');
|
||||
my $file = $c->param('file');
|
||||
my $text = $cache->get("$tarball/$file");
|
||||
if (not $text) {
|
||||
@@ -68,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>
|
||||
|
||||
120
scripts/unsubscribe.pl
Normal file
120
scripts/unsubscribe.pl
Normal file
@@ -0,0 +1,120 @@
|
||||
#! /usr/bin/perl
|
||||
# Copyright (C) 2010–2021 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/>.
|
||||
|
||||
=head1 NAME
|
||||
|
||||
unsubscribe.pl - mass unsubscribe from Oddmuse
|
||||
|
||||
=head2 SYNOPSIS
|
||||
|
||||
B<perl unsubscribe.pl> F<MAILDB> [B<--regexp=>I<REGEXP>]
|
||||
|
||||
B<perl unsubscribe.pl> F<MAILDB> [B<--dump>]
|
||||
|
||||
=head2 DESCRIPTION
|
||||
|
||||
If you use the Mail Extension to Oddmuse, you end up with subscriptions to very
|
||||
old pages. This script helps you unsubsribe people from old pages.
|
||||
|
||||
C<--regexp> indicates a regular expression matching pages names
|
||||
|
||||
The mandatory F<MAILDB> argument is the file containing all the mail
|
||||
subscriptions.
|
||||
|
||||
=head2 EXAMPLES
|
||||
|
||||
Make a copy, unsubscribe people, check a dump of the remaining subscriptions,
|
||||
and move the file back to the wiki data directory.
|
||||
|
||||
cp ~/alexschroeder/mail.db copy.db
|
||||
perl ~/src/oddmuse/scripts/unsubscribe.pl copy.db --regexp='20[01][0-9]'
|
||||
perl ~/src/oddmuse/scripts/unsubscribe.pl copy.db --dump
|
||||
mv copy.db ~/alexschroeder/mail.db
|
||||
|
||||
=cut;
|
||||
|
||||
use Modern::Perl;
|
||||
use Getopt::Long;
|
||||
use Encode qw(encode_utf8 decode_utf8);
|
||||
use DB_File;
|
||||
|
||||
binmode(STDOUT, ":utf8");
|
||||
|
||||
my $re = "";
|
||||
my $confirm;
|
||||
my $dump;
|
||||
|
||||
GetOptions ("regexp=s" => \$re,
|
||||
"dump" => \$dump,
|
||||
"confirm" => \$confirm, );
|
||||
|
||||
my $file = shift;
|
||||
|
||||
die "Not a file: $file" unless -f $file;
|
||||
die "Unknown arguments: @ARGV" if @ARGV;
|
||||
|
||||
sub UrlEncode {
|
||||
my $str = shift;
|
||||
return '' unless $str;
|
||||
my @letters = split(//, encode_utf8($str));
|
||||
my %safe = map {$_ => 1} ('a' .. 'z', 'A' .. 'Z', '0' .. '9', '-', '_', '.', '!', '~', '*', "'", '(', ')', '#');
|
||||
foreach my $letter (@letters) {
|
||||
$letter = sprintf("%%%02x", ord($letter)) unless $safe{$letter};
|
||||
}
|
||||
return join('', @letters);
|
||||
}
|
||||
|
||||
sub UrlDecode {
|
||||
my $str = shift;
|
||||
return '' unless $str;
|
||||
$str =~ s/%([0-9a-f][0-9a-f])/chr(hex($1))/eig;
|
||||
return decode_utf8($str);
|
||||
}
|
||||
|
||||
tie my %h, "DB_File", $file;
|
||||
my $FS = "\x1e";
|
||||
|
||||
if ($dump) {
|
||||
for my $key (keys %h) {
|
||||
my @value = split /$FS/, UrlDecode($h{$key});
|
||||
say UrlDecode($key), ": @value";
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
for my $raw (keys %h) {
|
||||
if ($raw =~ /@/) {
|
||||
# email address
|
||||
my $mail = UrlDecode($raw);
|
||||
my $value = $h{$raw};
|
||||
my @subscriptions = grep !/$re/, map { UrlDecode($_) } split /$FS/, $value;
|
||||
if (@subscriptions) {
|
||||
$h{$raw} = join $FS, map { UrlEncode($_) } @subscriptions if $confirm;
|
||||
say "> $mail: remains subscribed to @subscriptions";
|
||||
} else {
|
||||
delete $h{$raw} if $confirm;
|
||||
say "> $mail: unsubscribe from all pages";
|
||||
}
|
||||
} else {
|
||||
my $id = UrlDecode($raw);
|
||||
next unless $id =~ /$re/;
|
||||
delete $h{$raw} if $confirm;
|
||||
say "Delete $id";
|
||||
}
|
||||
}
|
||||
|
||||
untie %h;
|
||||
|
||||
say "Use --confirm to actually do it" unless $confirm;
|
||||
84
scripts/webmention.pl
Normal file
84
scripts/webmention.pl
Normal file
@@ -0,0 +1,84 @@
|
||||
#! /usr/bin/perl -w
|
||||
|
||||
# Copyright (C) 2019 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/>.
|
||||
|
||||
=head1 Webmention from one site to another
|
||||
|
||||
If you link from page A on your site to page B on some other site, you can
|
||||
invoke this script with the command-line arguments A and B. In theory, this will
|
||||
create a link back from B to A, letting them and all their visitors know that
|
||||
you wrote something in response.
|
||||
|
||||
=cut
|
||||
|
||||
use Modern::Perl;
|
||||
use XML::LibXML;
|
||||
use LWP::UserAgent;
|
||||
use Data::Dumper;
|
||||
|
||||
if (@ARGV != 2) {
|
||||
die "Usage: webmention FROM TO\n";
|
||||
}
|
||||
|
||||
my $parser = XML::LibXML->new(recover => 2);
|
||||
|
||||
my ($from, $to) = @ARGV;
|
||||
my $ua = LWP::UserAgent->new(agent => "Oddmuse Webmention Client/0.1");
|
||||
|
||||
print "Getting $from\n";
|
||||
my $response = $ua->get($from);
|
||||
|
||||
if (!$response->is_success) {
|
||||
die $response->status_line;
|
||||
}
|
||||
|
||||
print "Parsing $from\n";
|
||||
my ($username, $homepage);
|
||||
my $dom = $parser->load_html(string => $response->decoded_content);
|
||||
my @nodes = $dom->findnodes('//*[@rel="author"]');
|
||||
if (@nodes) {
|
||||
my $node = shift @nodes;
|
||||
$username = $node->textContent;
|
||||
$homepage = $node->getAttribute('href');
|
||||
}
|
||||
print "Webmention from " . join(", ", $username, $homepage) . "\n"
|
||||
if $username or $homepage;
|
||||
|
||||
print "Getting $to\n";
|
||||
$response = $ua->get($to);
|
||||
|
||||
if (!$response->is_success) {
|
||||
die $response->status_line;
|
||||
}
|
||||
|
||||
print "Parsing $to\n";
|
||||
$dom = $parser->load_html(string => $response->decoded_content);
|
||||
my $webmention = $dom->findvalue('//link[@rel="webmention"]/@href');
|
||||
|
||||
if (!$webmention) {
|
||||
die "Webmention URL not found in $to\n";
|
||||
}
|
||||
|
||||
print "Webmention URL is $webmention\n";
|
||||
|
||||
$response = $ua->post($webmention, { source => $from, target => $to });
|
||||
|
||||
my $message = $response->code . " " . $response->message . "\n";
|
||||
if ($response->is_success) {
|
||||
print $message;
|
||||
} else {
|
||||
die $message;
|
||||
}
|
||||
@@ -3,3 +3,66 @@ Extra Files
|
||||
|
||||
Some of the modules requires extra files -- graphics, templates, and
|
||||
so on.
|
||||
|
||||
Gopher Server
|
||||
-------------
|
||||
|
||||
Example usage:
|
||||
|
||||
Change your working directory to the root of the Oddmuse repository
|
||||
(the parent directory of this directory).
|
||||
|
||||
Set the environment variable `WikiDataDir` to `test-data`:
|
||||
|
||||
```
|
||||
export WikiDataDir=test-data
|
||||
```
|
||||
|
||||
Test that the simple web server works by running `stuff/server.pl`.
|
||||
This should start the web server on `http://localhost:8080/`. Visit
|
||||
the link using your web browser and edit `HomePage`.
|
||||
|
||||
You should see a `test-data` directory containing the new page.
|
||||
|
||||
Now start the gopher server on port 7070 by running
|
||||
`stuff/gopher-server.pl --port=7070`. If you don't provide an explicit
|
||||
port a random port is used and you'll need to read the server output
|
||||
to determine the actual port. That's why we're setting the port
|
||||
ourselves. Remember that using ports below 1024 require special
|
||||
privileges. Don't use them unless you know what you're doing.
|
||||
|
||||
Test the gopher server by simulating a request using `echo HomePage |
|
||||
nc localhost 7070`. You should get back the content of the page you
|
||||
wrote.
|
||||
|
||||
Let's test encryption. Create a self-signed certificate and a private
|
||||
key. If you use the following command, you can leave all the fields
|
||||
empty except for the common name. The common name you provide must
|
||||
match the server name you are using. In our case, that would be
|
||||
`localhost`.
|
||||
|
||||
```
|
||||
openssl req -new -x509 -days 365 -nodes -out \
|
||||
gopher-server-cert.pem -keyout gopher-server-key.pem
|
||||
```
|
||||
|
||||
Start the gopher server on port 7443 using this information with
|
||||
`stuff/gopher-server.pl --port=7443
|
||||
--wiki_key_file=gopher-server-key.pem
|
||||
--wiki_cert_file=gopher-server-cert.pem`.
|
||||
|
||||
If you test this by simulating an unencrypted request using `echo
|
||||
HomePage | nc localhost 7443`, you shouldn't get any output. Use `echo
|
||||
HomePage | gnutls-cli --no-ca-verification localhost:7443` and you
|
||||
should get back your page. Actually, you have the certificate right
|
||||
there so you might as well provide it: `echo HomePage | gnutls-cli
|
||||
--x509cafile=gopher-server-cert.pem localhost:7443`
|
||||
|
||||
What you'd expect to see is a lot of cryptography output by
|
||||
`gnutls-cli` and at the very end the content of the page. If you're
|
||||
seeing `Fatal error: Error in the pull function` instead, then perhaps
|
||||
the timing of things is a bit off. Introducing a short wait fixed this
|
||||
for me. `(sleep 1;echo HomePage) | gnutls-cli
|
||||
--x509cafile=gopher-server-cert.pem localhost:7443`
|
||||
|
||||
Good luck!
|
||||
|
||||
1036
stuff/gemini-server.pl
Executable file
1036
stuff/gemini-server.pl
Executable file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env perl
|
||||
# Copyright (C) 2017–2018 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2017–2019 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
|
||||
@@ -82,6 +82,10 @@ sub post_configure_hook {
|
||||
$self->log(3, "PID $$");
|
||||
$self->log(3, "Host " . ("@{$self->{server}->{host}}" || "*"));
|
||||
$self->log(3, "Port @{$self->{server}->{port}}");
|
||||
|
||||
# Note: if you use sudo to run gopher-server.pl, these options might not work!
|
||||
$self->log(4, "--wikir_dir says $self->{server}->{wiki_dir}\n");
|
||||
$self->log(4, "\$WikiDataDir says $ENV{WikiDataDir}\n");
|
||||
$self->log(3, "Wiki data dir is $DataDir\n");
|
||||
|
||||
$RunCGI = 0;
|
||||
@@ -121,7 +125,7 @@ wiki_key_file - the filename containing a private key in PEM format
|
||||
For many of the options, more information can be had in the Net::Server
|
||||
documentation. This is important if you want to daemonize the server. You'll
|
||||
need to use --pid_file so that you can stop it using a script, --setsid to
|
||||
daemonize it, --log_file to write keep logs, and you'll net to set the user or
|
||||
daemonize it, --log_file to write keep logs, and you'll need to set the user or
|
||||
group using --user or --group such that the server has write access to the data
|
||||
directory.
|
||||
|
||||
@@ -192,6 +196,14 @@ sub normal_to_free {
|
||||
return $title;
|
||||
}
|
||||
|
||||
sub free_to_normal {
|
||||
my $title = shift;
|
||||
$title =~ s/^ +//g;
|
||||
$title =~ s/ +$//g;
|
||||
$title =~ s/ +/_/g;
|
||||
return $title;
|
||||
}
|
||||
|
||||
sub print_text {
|
||||
my $self = shift;
|
||||
my $text = shift;
|
||||
@@ -238,17 +250,17 @@ sub serve_main_menu {
|
||||
my @pages = sort { $b cmp $a } grep(/^\d\d\d\d-\d\d-\d\d/, @IndexList);
|
||||
# we should check for pages marked for deletion!
|
||||
for my $id (@pages[0..9]) {
|
||||
$self->print_menu("1" . normal_to_free($id), "$id/menu");
|
||||
$self->print_menu("1" . normal_to_free($id), free_to_normal($id) . "/menu");
|
||||
}
|
||||
$self->print_menu("1" . "More...", "do/more");
|
||||
$self->print_info("");
|
||||
|
||||
for my $id (@{$self->{server}->{wiki_pages}}) {
|
||||
$self->print_menu("1" . normal_to_free($id), "$id/menu");
|
||||
$self->print_menu("1" . normal_to_free($id), free_to_normal($id) . "/menu");
|
||||
}
|
||||
|
||||
for my $id (@{$self->{server}->{menu}}) {
|
||||
$self->print_menu("1" . normal_to_free($id), "map/$id");
|
||||
$self->print_menu("1" . normal_to_free($id), "map/" . free_to_normal($id));
|
||||
}
|
||||
|
||||
$self->print_menu("1" . "Recent Changes", "do/rc");
|
||||
@@ -271,7 +283,7 @@ sub serve_phlog_archive {
|
||||
$self->log(3, "Serving phlog archive");
|
||||
my @pages = sort { $b cmp $a } grep(/^\d\d\d\d-\d\d-\d\d/, @IndexList);
|
||||
for my $id (@pages) {
|
||||
$self->print_menu("1" . normal_to_free($id), "$id/menu");
|
||||
$self->print_menu("1" . normal_to_free($id), free_to_normal($id) . "/menu");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +291,7 @@ sub serve_index {
|
||||
my $self = shift;
|
||||
$self->log(3, "Serving index of all pages");
|
||||
for my $id (sort newest_first @IndexList) {
|
||||
$self->print_menu("1" . normal_to_free($id), "$id/menu");
|
||||
$self->print_menu("1" . normal_to_free($id), free_to_normal($id) . "/menu");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,7 +302,7 @@ sub serve_match {
|
||||
$self->print_info("Use a regular expression to match page titles.");
|
||||
$self->print_info("Spaces in page titles are underlines, '_'.");
|
||||
for my $id (sort newest_first grep(/$match/i, @IndexList)) {
|
||||
$self->print_menu( "1" . normal_to_free($id), "$id/menu");
|
||||
$self->print_menu( "1" . normal_to_free($id), free_to_normal($id) . "/menu");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +313,7 @@ sub serve_search {
|
||||
$self->print_info("Use regular expressions separated by spaces.");
|
||||
SearchTitleAndBody($str, sub {
|
||||
my $id = shift;
|
||||
$self->print_menu("1" . normal_to_free($id), "$id/menu");
|
||||
$self->print_menu("1" . normal_to_free($id), free_to_normal($id) . "/menu");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -315,7 +327,7 @@ sub serve_tags {
|
||||
$count{$tag} = @{$h{$tag}};
|
||||
}
|
||||
foreach my $id (sort { $count{$b} <=> $count{$a} } keys %count) {
|
||||
$self->print_menu("1" . normal_to_free($id), "$id/tag");
|
||||
$self->print_menu("1" . normal_to_free($id), free_to_normal($id) . "/tag");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +354,7 @@ sub serve_rc {
|
||||
sub {
|
||||
my($id, $ts, $author_host, $username, $summary, $minor, $revision,
|
||||
$languages, $cluster, $last) = @_;
|
||||
$self->print_menu("1" . normal_to_free($id), "$id/menu");
|
||||
$self->print_menu("1" . normal_to_free($id), free_to_normal($id) . "/menu");
|
||||
for my $line (split(/\n/, wrap(' ', ' ', $summary))) {
|
||||
$self->print_info($line);
|
||||
}
|
||||
@@ -395,9 +407,9 @@ sub serve_page_comment_link {
|
||||
# sometimes we are on a comment page and cannot derive the original
|
||||
$self->print_menu("1" . "Back to the original page",
|
||||
"$original/menu") if $original;
|
||||
$self->print_menu("w" . "Add a comment", "$id/append/text");
|
||||
$self->print_menu("w" . "Add a comment", free_to_normal($id) . "/append/text");
|
||||
} else {
|
||||
my $comments = $CommentsPrefix . $id;
|
||||
my $comments = free_to_normal($CommentsPrefix . $id);
|
||||
$self->print_menu("1" . "Comments on this page", "$comments/menu");
|
||||
}
|
||||
}
|
||||
@@ -408,7 +420,7 @@ sub serve_page_history_link {
|
||||
my $id = shift;
|
||||
my $revision = shift;
|
||||
if (not $revision) {
|
||||
$self->print_menu("1" . "Page History", "$id/history");
|
||||
$self->print_menu("1" . "Page History", free_to_normal($id) . "/history");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,7 +432,7 @@ sub serve_file_page_menu {
|
||||
my $code = substr($type, 0, 6) eq 'image/' ? 'I' : '9';
|
||||
$self->log(3, "Serving file page menu for " . UrlEncode($id));
|
||||
$self->print_menu($code . normal_to_free($id)
|
||||
. ($revision ? "/$revision" : ""), $id);
|
||||
. ($revision ? "/$revision" : ""), free_to_normal($id));
|
||||
$self->serve_page_comment_link($id, $revision);
|
||||
$self->serve_page_history_link($id, $revision);
|
||||
}
|
||||
@@ -430,16 +442,16 @@ sub serve_text_page_menu {
|
||||
my $id = shift;
|
||||
my $page = shift;
|
||||
my $revision = shift;
|
||||
$self->log(3, "Serving text page menu for " . UrlEncode($id)
|
||||
$self->log(3, "Serving text page menu for $id"
|
||||
. ($revision ? "/$revision" : ""));
|
||||
|
||||
$self->print_info("The text of this page:");
|
||||
$self->print_menu("0" . normal_to_free($id),
|
||||
$id . ($revision ? "/$revision" : ""));
|
||||
free_to_normal($id) . ($revision ? "/$revision" : ""));
|
||||
$self->print_menu("h" . normal_to_free($id),
|
||||
$id . ($revision ? "/$revision" : "") . "/html");
|
||||
free_to_normal($id) . ($revision ? "/$revision" : "") . "/html");
|
||||
$self->print_menu("w" . "Replace " . normal_to_free($id),
|
||||
$id . "/write/text");
|
||||
free_to_normal($id) . "/write/text");
|
||||
|
||||
$self->serve_page_comment_link($id, $revision);
|
||||
$self->serve_page_history_link($id, $revision);
|
||||
@@ -448,35 +460,47 @@ sub serve_text_page_menu {
|
||||
while ($page->{text} =~ /
|
||||
\[\[ (?<title>[^\]|]*) (?:\|(?<text>[^\]]*))? \]\]
|
||||
| \[ (?<url>https?:\/\/\S+) \s+ (?<text>[^\]]*) \]
|
||||
| (?<url>https?:\/\/\S+)
|
||||
| \[ (?<text>[^\]]*) \] \( (?<url>https?:\/\/\S+) \)
|
||||
| \[ gopher:\/\/ (?<hostname>[^:\/]*) (?::(?<port>\d+))?
|
||||
(?:\/(?<type>\d) (?<selector>\S+))?
|
||||
\s+ (?<text>[^\]]+)\]
|
||||
| \[ gophers?:\/\/ (?<hostname>[^:\/]*) (?::(?<port>\d+))?
|
||||
(?:\/(?<type>\d)? (?<selector>\S+))? \]
|
||||
| \[ gophers?:\/\/ (?<hostname>[^:\/]*) (?::(?<port>\d+))?
|
||||
(?:\/(?<type>\d)? (?<selector>\S+))?
|
||||
\s+ (?<text>[^\]]+) \]
|
||||
| \[ (?<text>[^\]]+) \]
|
||||
\( gopher:\/\/ (?<hostname>[^:\/]*) (?::(?<port>\d+))?
|
||||
(?:\/(?<type>\d) (?<selector>\S+))? \)
|
||||
\( gophers?:\/\/ (?<hostname>[^:\/]*) (?::(?<port>\d+))?
|
||||
(?:\/(?<type>\d)? (?<selector>\S+))? \)
|
||||
/xg) {
|
||||
# remember $type can be "0" and thus "false" -- use // and defined instead!
|
||||
my ($title, $text, $url, $hostname,
|
||||
$port, $type, $selector)
|
||||
= ($+{title}, $+{text}, $+{url}, $+{hostname},
|
||||
$+{port}||70, $+{type}||1, $+{selector});
|
||||
$+{port}||70, $+{type}//1, $+{selector});
|
||||
$title =~ s/\n/ /g;
|
||||
$text =~ s/\n/ /g;
|
||||
if ($first) {
|
||||
$self->print_info("");
|
||||
$self->print_info("Links leaving " . normal_to_free($id) . ":");
|
||||
$first = 0;
|
||||
}
|
||||
if ($hostname) {
|
||||
if ($hostname and $text) {
|
||||
$self->print_text(join("\t", $type . $text, $selector, $hostname, $port) . "\r\n");
|
||||
} elsif ($url) {
|
||||
} elsif ($hostname and $selector) {
|
||||
$self->print_text(join("\t", "$type$hostname:$port/$type$selector", $selector, $hostname, $port) . "\r\n");
|
||||
} elsif ($hostname) {
|
||||
$self->print_text(join("\t", "1$hostname:$port", $selector, $hostname, $port) . "\r\n");
|
||||
} elsif ($url and $text) {
|
||||
$self->print_menu("h$text", "URL:" . $url, undef, undef, 1);
|
||||
} elsif ($url) {
|
||||
$self->print_menu("h$url", "URL:" . $url, undef, undef, 1);
|
||||
} elsif ($title and substr($title, 0, 4) eq 'tag:') {
|
||||
$self->print_menu("1" . ($text||substr($title, 4)),
|
||||
substr($title, 4) . "/tag");
|
||||
free_to_normal(substr($title, 4)) . "/tag");
|
||||
} elsif ($title =~ s!^image[/a-z]* external:!pics/!) {
|
||||
$self->print_menu("I" . $text||$title, $title);
|
||||
$self->print_menu("I" . $text||$title, $title); # do not normalize space
|
||||
} elsif ($title) {
|
||||
$title =~ s!^image[/a-z]*:!!i;
|
||||
$self->print_menu("1" . ($text||$title), $title . "/menu");
|
||||
$self->print_menu("1" . ($text||$title), free_to_normal($title) . "/menu");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,7 +530,7 @@ sub serve_page_history {
|
||||
$self->log(3, "Serving history of " . UrlEncode($id));
|
||||
OpenPage($id);
|
||||
|
||||
$self->print_menu("1" . normal_to_free($id) . " (current)", "$id/menu");
|
||||
$self->print_menu("1" . normal_to_free($id) . " (current)", free_to_normal($id) . "/menu");
|
||||
$self->print_info(CalcTime($Page{ts})
|
||||
. " by " . GetAuthor($Page{username})
|
||||
. ($Page{summary} ? ": $Page{summary}" : "")
|
||||
@@ -515,7 +539,7 @@ sub serve_page_history {
|
||||
foreach my $revision (GetKeepRevisions($OpenPageName)) {
|
||||
my $keep = GetKeptRevision($revision);
|
||||
$self->print_menu("1" . normal_to_free($id) . " ($keep->{revision})",
|
||||
"$id/$keep->{revision}/menu");
|
||||
free_to_normal($id) . "/$keep->{revision}/menu");
|
||||
$self->print_info(CalcTime($keep->{ts})
|
||||
. " by " . GetAuthor($keep->{username})
|
||||
. ($keep->{summary} ? ": $keep->{summary}" : "")
|
||||
@@ -660,7 +684,7 @@ sub serve_tag_list {
|
||||
my $tag = shift;
|
||||
$self->print_info("Search result for tag $tag:");
|
||||
for my $id (sort newest_first TagFind($tag)) {
|
||||
$self->print_menu("1" . normal_to_free($id), "$id/menu");
|
||||
$self->print_menu("1" . normal_to_free($id), free_to_normal($id) . "/menu");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -670,7 +694,7 @@ sub serve_tag {
|
||||
$self->log(3, "Serving tag " . UrlEncode($tag));
|
||||
if ($IndexHash{$tag}) {
|
||||
$self->print_info("This page is about the tag $tag.");
|
||||
$self->print_menu("1" . normal_to_free($tag), "$tag/menu");
|
||||
$self->print_menu("1" . normal_to_free($tag), free_to_normal($tag) . "/menu");
|
||||
$self->print_info("");
|
||||
}
|
||||
$self->serve_tag_list($tag);
|
||||
@@ -708,7 +732,7 @@ sub write_page_ok {
|
||||
my $self = shift;
|
||||
my $id = shift;
|
||||
$self->print_info("Page was saved.");
|
||||
$self->print_menu("1" . normal_to_free($id), "$id/menu");
|
||||
$self->print_menu("1" . normal_to_free($id), free_to_normal($id) . "/menu");
|
||||
}
|
||||
|
||||
sub write_page_error {
|
||||
|
||||
4
stuff/hypnotoad.pl
Normal file
4
stuff/hypnotoad.pl
Normal file
@@ -0,0 +1,4 @@
|
||||
use Mojo::Server::Hypnotoad;
|
||||
warn "Use hypnotoad -s stuff/hypnotoad.pl to stop the server\n";
|
||||
my $hypnotoad = Mojo::Server::Hypnotoad->new;
|
||||
$hypnotoad->run('stuff/mojolicious-app.pl');
|
||||
17
stuff/mojolicious-app.pl
Normal file
17
stuff/mojolicious-app.pl
Normal file
@@ -0,0 +1,17 @@
|
||||
# From the root directory, run one of the following:
|
||||
# 1. stuff/mojolicious-app.pl daemon -l http://localhost:8080
|
||||
# 2. stuff/hypnotoad.pl
|
||||
# 3. stuff/toadfarm.pl start
|
||||
|
||||
use Mojolicious::Lite;
|
||||
|
||||
plugin CGI => {
|
||||
support_semicolon_in_query_string => 1,
|
||||
};
|
||||
|
||||
plugin CGI => {
|
||||
route => '/',
|
||||
script => 'wiki.pl',
|
||||
};
|
||||
|
||||
app->start;
|
||||
@@ -33,7 +33,7 @@ EOT
|
||||
|
||||
my $min = version->parse(shift || "2.3.0");
|
||||
|
||||
my @tags = grep { /(\d+\.\d+\.\d+)/ and version->parse($1) >= $min }
|
||||
my @tags = grep { /(\d+\.\d+\.\d+)/ and version->parse($1) >= $min }
|
||||
split(/\n/, qx{git tag --list});
|
||||
|
||||
unless (@tags) {
|
||||
@@ -47,7 +47,7 @@ for my $tag (@tags) {
|
||||
next;
|
||||
}
|
||||
print "Preparing $tag\n";
|
||||
|
||||
|
||||
system("git", "checkout", $tag) == 0
|
||||
or die "Failed to git checkout $tag\n";
|
||||
system("make", "prepare") == 0
|
||||
@@ -62,5 +62,5 @@ for my $tag (@tags) {
|
||||
or die "Failed to remove the directory oddmuse-$tag\n";
|
||||
}
|
||||
|
||||
system("git", "checkout", "master") == 0
|
||||
or die "Failed to git checkout master\n";
|
||||
system("git", "checkout", "main") == 0
|
||||
or die "Failed to git checkout main\n";
|
||||
|
||||
29
stuff/server.pl
Normal file → Executable file
29
stuff/server.pl
Normal file → Executable file
@@ -1,4 +1,4 @@
|
||||
#!/bin/env perl
|
||||
#!/usr/bin/env perl
|
||||
# Copyright (C) 2015 Alex Schroeder <alex@gnu.org>
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
@@ -13,6 +13,23 @@
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# What is this?
|
||||
# =============
|
||||
#
|
||||
# This is a script that will server a wiki using a web server written in Perl,
|
||||
# without a fancy framework like Mojolicious. Instead, it uses
|
||||
# HTTP::Server::Simple::CGI.
|
||||
#
|
||||
# A simple usecase would be that you have had a wiki running years ago but then
|
||||
# you forgot all about it and your Apache config no longer works and who knows
|
||||
# how the system Perl is doing. So check out the data dir and notice that the
|
||||
# files belong to a user called _www... And so you run the following:
|
||||
#
|
||||
# sudo -u _www perl stuff/server.pl ./wiki.pl 3000 \
|
||||
# /Users/alex/WebServer/Oddmuse
|
||||
#
|
||||
# Your old wiki is served on localhost:3000 for you to examine.
|
||||
|
||||
my $wiki = $ARGV[0] || './wiki.pl';
|
||||
my $port = $ARGV[1] || 8080;
|
||||
my $dir = $ARGV[2];
|
||||
@@ -33,6 +50,10 @@ $ENV{WikiDataDir} = $dir if $dir;
|
||||
package OddMuse;
|
||||
$q = shift;
|
||||
|
||||
# The equivalent of use CGI qw(-utf8) because it didn't work as part of
|
||||
# cgi_init.
|
||||
$CGI::PARAM_UTF8++;
|
||||
|
||||
# NPH, or "no-parsed-header", scripts bypass the server completely by
|
||||
# sending the complete HTTP header directly to the browser.
|
||||
$q->nph(1);
|
||||
@@ -44,10 +65,10 @@ $ENV{WikiDataDir} = $dir if $dir;
|
||||
die <<'EOT' unless -f $wiki;
|
||||
Usage: perl server.pl [WIKI [PORT [DIR]]]
|
||||
|
||||
Example: perl server.pl wiki.pl 8080 ~/src/oddmuse/test-data
|
||||
Example: perl server.pl ./wiki.pl 8080 ~/src/oddmuse/test-data
|
||||
|
||||
You may provide the Oddmuse wiki script on the command line. If you do not
|
||||
provide it, WIKI will default to 'wiki.pl'.
|
||||
provide it, WIKI will default to './wiki.pl'.
|
||||
|
||||
You may provide a port number on the command line. If you do not provide it,
|
||||
PORT will default to 8080.
|
||||
@@ -67,7 +88,7 @@ echo <<EOF > "$WikiDataDir/config"
|
||||
$AdminPass = 'foo';
|
||||
$ScriptName = 'http://localhost/';
|
||||
EOF
|
||||
perl stuff/server.pl wiki.pl &
|
||||
perl stuff/server.pl ./wiki.pl &
|
||||
SERVER=$!
|
||||
sleep 1
|
||||
w3m http://localhost:8080/
|
||||
|
||||
8
stuff/toadfarm.pl
Normal file
8
stuff/toadfarm.pl
Normal file
@@ -0,0 +1,8 @@
|
||||
use Toadfarm -init;
|
||||
|
||||
mount "stuff/mojolicious-app.pl" => {
|
||||
"Host" => qr{^localhost:8080$},
|
||||
mount_point => '/',
|
||||
};
|
||||
|
||||
start;
|
||||
5
t/atom.t
5
t/atom.t
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2006–2015 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2006–2019 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
|
||||
@@ -138,5 +138,6 @@ sub trim {
|
||||
s/\s+$//g;
|
||||
return $_;
|
||||
}
|
||||
ok(trim($result->content->body) eq ("<p>" . trim($content) . '</p>'), 'verify content');
|
||||
my $re = "<p>" . trim($content) . '</p>';
|
||||
like($result->content->body, qr/$re/, 'verify content');
|
||||
ok($result->author->name eq $username, 'verify author');
|
||||
|
||||
@@ -15,13 +15,42 @@
|
||||
require './t/test.pl';
|
||||
package OddMuse;
|
||||
use Test::More;
|
||||
use Net::IP;
|
||||
|
||||
add_module('ban-contributors.pl');
|
||||
|
||||
# 0-255
|
||||
is(BanContributors::get_regexp_ip('185.244.214.0', '185.244.214.255'),
|
||||
'^185\.244\.214\.',
|
||||
'185.244.214.0 - 185.244.214.255');
|
||||
|
||||
# 48.0-63.255
|
||||
is(BanContributors::get_regexp_ip('42.118.48.0', '42.118.63.255'),
|
||||
'^42\.118\.(4[8-9]|5[0-9]|6[0-3])\.',
|
||||
'42.118.48.0 - 42.118.63.255');
|
||||
|
||||
# 192.0-223.255
|
||||
is(BanContributors::get_regexp_ip('118.71.192.0', '118.71.223.255'),
|
||||
'^118\.71\.(19[2-9]|2[0-1][0-9]|22[0-3])\.',
|
||||
'118.71.192.0 - 118.71.223.255');
|
||||
|
||||
# 56.180-57.70
|
||||
is(BanContributors::get_regexp_ip('77.56.180.0', '77.57.70.255'),
|
||||
'^77\.(56\.(1[8-9][0-9]|2[0-4][0-9]|25[0-5])|5[7-9]|6[0-9]|70)\.',
|
||||
'^77\.(56\.(1[8-9][0-9]|2[0-4][0-9]|25[0-5])|57\.([0-9]|[1-6][0-9]|70)\.',
|
||||
'77.56.180.0 - 77.57.70.255');
|
||||
|
||||
# 45.87.2.128 - 45.87.2.255
|
||||
is(BanContributors::get_regexp_ip('45.87.2.128', '45.87.2.255'),
|
||||
'^45\.87\.2\.(12[8-9]|1[3-9][0-9]|2[0-4][0-9]|25[0-5])',
|
||||
'45.87.2.128 - 45.87.2.255');
|
||||
|
||||
# 191.101.0.0/16
|
||||
# verify that Net::IP works as intended
|
||||
my $ip = Net::IP->new('191.101.0.0/16');
|
||||
ok($ip, 'Net::IP parsed CIDR');
|
||||
is($ip->ip, '191.101.0.0', 'First IP in range');
|
||||
is($ip->last_ip, '191.101.255.255', 'Last IP in range');
|
||||
|
||||
$localhost = '127.0.0.1';
|
||||
$ENV{'REMOTE_ADDR'} = $localhost;
|
||||
|
||||
@@ -57,10 +86,9 @@ test_page($page, 'Rolling back changes', 'These URLs were rolled back',
|
||||
'doxycycline');
|
||||
test_page_negative($page, 'amoxil');
|
||||
|
||||
# 127.0.0.1 has no inetnum
|
||||
test_page(get_page("action=ban id=Test"),
|
||||
'Ban Contributors to Test',
|
||||
quotemeta('127.0.0.1 () [ - ]'));
|
||||
quotemeta('127.0.0.1 () [127.0.0.0 - 127.255.255.255]'));
|
||||
|
||||
SKIP: {
|
||||
skip "Net::Whois::Parser doesn't always return the same result", 4;
|
||||
@@ -73,7 +101,7 @@ SKIP: {
|
||||
|
||||
test_page(get_page('action=ban id=Test regexp="^46\.101\.([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-7])" range="[46.101.0.0 - 46.101.127.255]" recent_edit=on pwd=foo'),
|
||||
'Location: http://localhost/wiki.pl/BannedHosts');
|
||||
|
||||
|
||||
test_page(get_page('BannedHosts'),
|
||||
quotemeta('^46\.101\.([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-7]) # '
|
||||
. CalcDay($Now)
|
||||
|
||||
@@ -31,15 +31,15 @@ add_module('calendar.pl');
|
||||
|
||||
test_page(update_page("with_cal", "zulu\n\ncalendar:2006\n\nwarrior\n"),
|
||||
'<p>zulu</p><p class="nav">',
|
||||
'</pre></div><p>warrior</p></div><div class="wrapper close"></div></div><div class="footer">');
|
||||
'</pre></div><p>warrior</p></div><div class="wrapper close"></div></div><footer>');
|
||||
|
||||
test_page(update_page("with_cal", "zulu\n\nmonth:2006-09\n\nwarrior\n"),
|
||||
'<p>zulu</p><div class="cal"><div class="month"><pre>',
|
||||
'</pre></div></div><p>warrior</p></div><div class="wrapper close"></div></div><div class="footer">');
|
||||
'</pre></div></div><p>warrior</p></div><div class="wrapper close"></div></div><footer>');
|
||||
|
||||
test_page(update_page("with_cal", "zulu\n\nmonth:+0\n\nwarrior\n"),
|
||||
'<p>zulu</p><div class="cal"><div class="month"><pre>',
|
||||
'</pre></div></div><p>warrior</p></div><div class="wrapper close"></div></div><div class="footer">');
|
||||
'</pre></div></div><p>warrior</p></div><div class="wrapper close"></div></div><footer>');
|
||||
|
||||
xpath_test(get_page('action=calendar'),
|
||||
# yearly navigation
|
||||
|
||||
@@ -21,4 +21,4 @@ AppendStringToFile($ConfigFile, "\$ConfigPage = 'Config';\n");
|
||||
|
||||
xpath_test(update_page('Config', '@UserGotoBarPages = ("Foo", "Bar");',
|
||||
'config', 0, 1),
|
||||
'//div[@class="header"]/div[@class="menu"]/span[@class="gotobar bar"]/a[@class="local"][text()="Foo"]/following-sibling::a[@class="local"][text()="Bar"]');
|
||||
'//header/nav/span[@class="gotobar bar"]/a[@class="local"][text()="Foo"]/following-sibling::a[@class="local"][text()="Bar"]');
|
||||
|
||||
@@ -100,7 +100,7 @@ bla
|
||||
quotemeta(qq{<div class="crossbar"><p>bla</p><h2>mu</h2>}),
|
||||
quotemeta(qq{<p>bla </p><div class="toc"><h2>$TocHeaderText</h2>}),
|
||||
quotemeta(qq{<ol><li><a href="#${TocAnchorPrefix}1">two</a><ol><li><a href="#${TocAnchorPrefix}2">three</a></li></ol></li><li><a href="#${TocAnchorPrefix}3">one</a></li></ol>}),
|
||||
quotemeta(qq{one</a></li></ol></div></div><div class="content browse"><p>bla}));
|
||||
quotemeta(qq{one</a></li></ol></div></div><div class="content browse" lang="en"><p>bla}));
|
||||
quotemeta(qq{<h2 id="${TocAnchorPrefix}1">two</h2>}),
|
||||
quotemeta(qq{<h2 id="${TocAnchorPrefix}3">one</h2>}),
|
||||
|
||||
|
||||
41
t/definition-lists.t
Normal file
41
t/definition-lists.t
Normal file
@@ -0,0 +1,41 @@
|
||||
# Copyright (C) 2019 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;
|
||||
|
||||
add_module('definition-lists.pl');
|
||||
|
||||
run_tests(split('\n',<<'EOT'));
|
||||
this is a test\n\na test!
|
||||
this is a test<p>a test!</p>
|
||||
test\n: some definition
|
||||
<dl><dt>test</dt><dd>some definition</dd></dl>
|
||||
test\n: some definition\nand some text
|
||||
<dl><dt>test</dt><dd>some definition and some text</dd></dl>
|
||||
test\n: some definition\n\nbut this is not
|
||||
<dl><dt>test</dt><dd>some definition</dd></dl><p>but this is not</p>
|
||||
an introduction\n\ntest\n: some definition
|
||||
an introduction<dl><dt>test</dt><dd>some definition</dd></dl>
|
||||
test\n: some definition\nand this\n: is another definition
|
||||
<dl><dt>test</dt><dd>some definition</dd><dt>and this</dt><dd>is another definition</dd></dl>
|
||||
test\n: some definition\n: another definition
|
||||
<dl><dt>test</dt><dd>some definition</dd><dd>another definition</dd></dl>
|
||||
test\n: some definition\n\nand this\n: is another definition
|
||||
<dl><dt>test</dt><dd>some definition</dd><dt>and this</dt><dd>is another definition</dd></dl>
|
||||
EOT
|
||||
|
||||
done_testing();
|
||||
@@ -51,4 +51,4 @@ test_page(get_page('search=alex'),
|
||||
AppendStringToFile($ConfigFile, "\$ScriptName = 'http://emacswiki.org/';\n");
|
||||
test_page(get_page('search=alex'),
|
||||
'Status: 302',
|
||||
'Location: https://www.duckduckgo.com/\?q=alex\+site%3Aemacswiki\.org');
|
||||
'Location: https://duckduckgo.com/\?q=alex\+site%3Aemacswiki\.org');
|
||||
|
||||
@@ -23,18 +23,18 @@ test_page(update_page('2011-07-06', 'Hallo'),
|
||||
'Comments_on_2011-07-06');
|
||||
xpath_test(update_page('Hi', '<journal>'),
|
||||
'//h1/a[text()="2011-07-06"]',
|
||||
'//div[@class="journal h-feed"]/div[@class="page h-entry"]/p[@class="comment"]/a[text()="Comments on this page"]');
|
||||
'//div[@class="journal h-feed"]/article[@class="h-entry"]/p[@class="comment"]/a[text()="Comments on this page"]');
|
||||
|
||||
add_module('dynamic-comments.pl');
|
||||
|
||||
xpath_test(get_page('Hi'),
|
||||
'//div[@class="journal h-feed"]/div[@class="page h-entry"]/p[@class="comment"]/a[@href="http://localhost/wiki.pl/Comments_on_2011-07-06"][text()="Add Comment"]');
|
||||
'//div[@class="journal h-feed"]/article[@class="h-entry"]/p[@class="comment"]/a[@href="http://localhost/wiki.pl/Comments_on_2011-07-06"][text()="Add Comment"]');
|
||||
|
||||
test_page(update_page('Comments_on_2011-07-06', 'Yo'),
|
||||
'Yo');
|
||||
|
||||
xpath_test(get_page('Hi'),
|
||||
'//div[@class="journal h-feed"]/div[@class="page h-entry"]/p[@class="comment"]/a[@href="javascript:togglecomments(\'id0\')"][text()="Comments on 2011-07-06"]');
|
||||
'//div[@class="journal h-feed"]/article[@class="h-entry"]/p[@class="comment"]/a[@href="javascript:togglecomments(\'id0\')"][text()="Comments on 2011-07-06"]');
|
||||
|
||||
# encoding basics
|
||||
$page = update_page('2011-07-06_(…)_Dü', 'Hallo Dü');
|
||||
@@ -44,6 +44,6 @@ xpath_test($page, '//p[contains(text(), "Dü")]');
|
||||
update_page('Comments_on_2011-07-06_(…)_Dü', 'Yo');
|
||||
|
||||
xpath_test(update_page('Hi', '<journal>'),
|
||||
'//h1/a[text()="2011-07-06 (…) Dü"]',
|
||||
'//div[@class="journal h-feed"]/div[@class="page h-entry"]/p[@class="comment"]/a[text()="Comments on 2011-07-06 (…) Dü"]',
|
||||
'//div[@class="journal h-feed"]/div[@class="page h-entry"]/p[@class="comment"]/a[@href="javascript:togglecomments(\'id0\')"]');
|
||||
'//h1/a[text()="2011-07-06 (…) Dü"]',
|
||||
'//div[@class="journal h-feed"]/article[@class="h-entry"]/p[@class="comment"]/a[text()="Comments on 2011-07-06 (…) Dü"]',
|
||||
'//div[@class="journal h-feed"]/article[@class="h-entry"]/p[@class="comment"]/a[@href="javascript:togglecomments(\'id0\')"]');
|
||||
|
||||
2
t/edit.t
2
t/edit.t
@@ -21,5 +21,5 @@ xpath_test(get_page('action=edit id=NewPage'),
|
||||
'//textarea[@name="text"][@id="text"][not(boolean(text()))]',
|
||||
'//div[@class="wrapper"]/div[@class="content edit"]',
|
||||
'//div[@class="content edit"]/following-sibling::div[@class="wrapper close"]',
|
||||
'//div[@class="wrapper"]/following-sibling::div[@class="footer"]',
|
||||
'//div[@class="wrapper"]/following-sibling::footer',
|
||||
);
|
||||
|
||||
340
t/gemini-server.t
Normal file
340
t/gemini-server.t
Normal file
@@ -0,0 +1,340 @@
|
||||
# Copyright (C) 2017–2020 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/>.
|
||||
|
||||
package OddMuse;
|
||||
use strict;
|
||||
use 5.10.0;
|
||||
use Test::More;
|
||||
use IO::Socket::SSL;
|
||||
use utf8; # tests contain UTF-8 characters and it matters
|
||||
use Modern::Perl;
|
||||
use XML::RSS;
|
||||
use XML::LibXML;
|
||||
|
||||
require './t/test.pl';
|
||||
require './stuff/gemini-server.pl';
|
||||
|
||||
add_module('tags.pl');
|
||||
|
||||
# enable uploads and filtering by language
|
||||
our($ConfigFile);
|
||||
AppendStringToFile($ConfigFile, <<'EOT');
|
||||
$UploadAllowed = 1;
|
||||
%Languages = (
|
||||
'de' => '\b(der|die|das|und|oder)\b',
|
||||
'en' => '\b(i|he|she|it|we|they|this|that|a|is|was)\b', );
|
||||
EOT
|
||||
|
||||
# enable comments
|
||||
our($CommentsPrefix);
|
||||
$CommentsPrefix = 'Comments_on_';
|
||||
AppendStringToFile($ConfigFile, "\$CommentsPrefix = 'Comments_on_';\n");
|
||||
AppendStringToFile($ConfigFile, "\@QuestionaskerQuestions = (['Who rules in Rivendell?' => sub { shift =~ /^Elrond/i }]);\n");
|
||||
|
||||
# write a gemini-only extension
|
||||
our($DataDir);
|
||||
WriteStringToFile("$DataDir/gemini_config", <<'EOT');
|
||||
package OddMuse;
|
||||
use Modern::Perl;
|
||||
our (@extensions, @main_menu_links);
|
||||
push(@extensions, \&serve_cert);
|
||||
sub serve_cert {
|
||||
my $self = shift;
|
||||
my $url = shift;
|
||||
my $selector = shift;
|
||||
my $base = $self->base();
|
||||
if ($selector =~ m!^do/test!) {
|
||||
say "20 text/plain\r";
|
||||
say "Test";
|
||||
return 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
1;
|
||||
EOT
|
||||
|
||||
my $host = "127.0.0.1";
|
||||
my $port = random_port();
|
||||
my $pid = fork();
|
||||
|
||||
END {
|
||||
# kill server
|
||||
if ($pid) {
|
||||
kill 'KILL', $pid or warn "Could not kill server $pid";
|
||||
}
|
||||
}
|
||||
|
||||
if (!defined $pid) {
|
||||
die "Cannot fork: $!";
|
||||
} elsif ($pid == 0) {
|
||||
use Config;
|
||||
my $secure_perl_path = $Config{perlpath};
|
||||
exec($secure_perl_path,
|
||||
"stuff/gemini-server.pl",
|
||||
"--host=$host",
|
||||
"--port=$port",
|
||||
"--wiki_cert_file=t/cert.pem",
|
||||
"--wiki_key_file=t/key.pem",
|
||||
"--log_level=0", # set to 4 for verbose logging
|
||||
"--wiki=./wiki.pl",
|
||||
"--wiki_dir=$DataDir",
|
||||
"--wiki_pages=Alex",
|
||||
"--wiki_pages=Berta",
|
||||
"--wiki_pages=Chris")
|
||||
or die "Cannot exec: $!";
|
||||
}
|
||||
|
||||
# Sorting
|
||||
is(sub{$a="Alex"; $b="Berta"; newest_first()}->(), -1, "Alex before Berta");
|
||||
is(sub{$a="Alex"; $b="Comments_on_Alex"; newest_first()}->(), -1, "Alex before Comments_on_Alex");
|
||||
is(sub{$a="Chris"; $b="Comments_on_Alex"; newest_first()}->(), 1, "Chris after Comments_on_A");
|
||||
is(sub{$a="Image_1_for_Alex"; $b="Image_10_for_Alex"; newest_first()}->(), -1, "Image_1_for_Alex before Image_10_for_Alex");
|
||||
is(sub{$a="Comments_on_Alex"; $b="Image_1_for_Alex"; newest_first()}->(), -1, "Comments_on_Alex before Image_1_for_Alex");
|
||||
is(join(" ", sort newest_first qw(Alex Berta Chris)), "Alex Berta Chris", "Sort alphabetically");
|
||||
is(join(" ", sort newest_first qw(2017-12-25 2017-12-26 2017-12-27)), "2017-12-27 2017-12-26 2017-12-25", "Sort by date descending");
|
||||
is(join(" ", sort newest_first qw(Alex Comments_on_Alex Berta Chris)), "Alex Comments_on_Alex Berta Chris", "Comments after pages");
|
||||
is(join(" ", sort newest_first qw(2017-12-25 2017-12-26 Comments_on_2017-12-26 2017-12-27)), "2017-12-27 2017-12-26 Comments_on_2017-12-26 2017-12-25", "Comments after date pages");
|
||||
is(join(" ", sort newest_first qw(Alex Comments_on_Alex Image_1_for_Alex Image_2_for_Alex Image_10_for_Alex Berta Chris)), "Alex Comments_on_Alex Image_1_for_Alex Image_2_for_Alex Image_10_for_Alex Berta Chris", "Images sorted numerically");
|
||||
|
||||
update_page('Alex', "My best friend is [[Berta]].\n\nTags: [[tag:Friends]]\n");
|
||||
update_page('Berta', "This is me.\n\nTags: [[tag:Friends]]\n");
|
||||
update_page('Chris', "I'm Chris.\n\nTags: [[tag:Friends]]\n");
|
||||
update_page('Friends', "Some friends.\n");
|
||||
update_page('2017-12-25', 'It was a Monday.\n\nTags: [[tag:Day]]');
|
||||
update_page('2017-12-26', 'It was a Tuesday.\n\nTags: [[tag:Day]]');
|
||||
update_page('2017-12-27', 'It was a Wednesday.\n\nTags: [[tag:Day]]');
|
||||
update_page('Friends', "News about friends.\n", 'rewrite', 1); # minor change
|
||||
update_page('Friends', "News about friends:\n\n<journal search tag:friends>\n",
|
||||
'add journal tag', 1); # minor change
|
||||
|
||||
# file created using convert NULL: test.png && base64 test.png
|
||||
update_page('Picture',
|
||||
"#FILE image/png\niVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bv"
|
||||
. "kkAAAACklEQVQI12NoAAAAggCB3UNq9AAAAABJRU5ErkJggg==");
|
||||
|
||||
sub query_gemini {
|
||||
my $query = shift;
|
||||
my $text = shift;
|
||||
|
||||
# create client
|
||||
my $socket = IO::Socket::SSL->new(
|
||||
PeerHost => "localhost",
|
||||
PeerService => $port,
|
||||
SSL_cert_file => 'cert.pem',
|
||||
SSL_key_file => 'key.pem',
|
||||
SSL_verify_mode => SSL_VERIFY_NONE)
|
||||
or die "Cannot construct client socket: $@";
|
||||
|
||||
$socket->print("$query\r\n");
|
||||
$socket->print($text);
|
||||
|
||||
undef $/; # slurp
|
||||
return <$socket>;
|
||||
}
|
||||
|
||||
my $base = "gemini://$host:$port";
|
||||
|
||||
# main menu
|
||||
my $page = query_gemini("$base/");
|
||||
|
||||
for my $item(qw(Alex Berta Chris 2017-12-25 2017-12-26 2017-12-27)) {
|
||||
like($page, qr/^=> $base\/$item $item/m, "main menu contains $item");
|
||||
}
|
||||
|
||||
unlike($page, qr/^=> .*\/$/m, "No empty links in the menu");
|
||||
|
||||
$page = query_gemini("$base/Alex");
|
||||
|
||||
like($page, qr/^My best friend is Berta\.$/m, "Local free link (text)");
|
||||
like($page, qr/=> $base\/Berta Berta$/m, "Local free link (link)");
|
||||
like($page, qr/^Tags:$/m, "Tags footer");
|
||||
like($page, qr/^Tags:$/m, "Tags footer");
|
||||
like($page, qr/=> $base\/tag\/Friends Friends$/m, "Tag link");
|
||||
like($page, qr/^=> $base\/raw\/Alex Raw text$/m, "Raw text link");
|
||||
like($page, qr/^=> $base\/history\/Alex History$/m, "History");
|
||||
like($page, qr/^=> $base\/Comments_on_Alex Comments on this page$/m, "Comment link");
|
||||
|
||||
# language tag
|
||||
$page = query_gemini("$base\/2017-12-25");
|
||||
like($page, qr/^20 text\/gemini; charset=UTF-8; lang=en\r\n/, "Result 20 with MIME type and language");
|
||||
|
||||
# plain text
|
||||
$page = query_gemini("$base\/raw\/Alex");
|
||||
like($page, qr/^My best friend is \[\[Berta\]\]\.$/m, "Raw text");
|
||||
|
||||
# history
|
||||
$page = query_gemini("$base/history/Friends");
|
||||
like($page, qr/^=> $base\/Friends\/1 Friends \(1\)/m, "Revision 1 is listed");
|
||||
like($page, qr/^=> $base\/Friends\/2 Friends \(2\)/m, "Revision 2 is listed");
|
||||
like($page, qr/^=> $base\/diff\/Friends\/1 Diff between revision 1 and the current one/m, "Diff 1 link");
|
||||
like($page, qr/^=> $base\/diff\/Friends\/2 Diff between revision 2 and the current one/m, "Diff 2 link");
|
||||
like($page, qr/^=> $base\/Friends Friends \(current\)/m, "Current revision is listed");
|
||||
$page = query_gemini("$base/Friends/1");
|
||||
like($page, qr/^Some friends\.$/m, "Revision 1 content");
|
||||
$page = query_gemini("$base/Friends/2");
|
||||
like($page, qr/^News about friends\.$/m, "Revision 2 content");
|
||||
|
||||
#diffs
|
||||
$page = query_gemini("$base/diff/Friends/1");
|
||||
like($page, qr/^< Some friends\.\n-+\n> News about friends:\n> \n> <journal search tag:friends>\n$/m, "Diff 1 content");
|
||||
$page = query_gemini("$base/diff/Friends/2");
|
||||
like($page, qr/^< News about friends\.\n-+\n> News about friends:\n> \n> <journal search tag:friends>\n$/m, "Diff 1 content");
|
||||
|
||||
# tags
|
||||
$page = query_gemini("$base\/tag\/Friends");
|
||||
like($page, qr/^This page is about the tag Friends\.$/m, "tag menu intro");
|
||||
for my $item(qw(Friends Alex Berta Chris)) {
|
||||
like($page, qr/^=> $base\/$item $item$/m, "tag menu contains $item");
|
||||
}
|
||||
|
||||
# tags
|
||||
$page = query_gemini("$base\/tag\/Day");
|
||||
like($page, qr/2017-12-27.*2017-12-26.*2017-12-25/s,
|
||||
"tag menu sorted newest first");
|
||||
|
||||
# match
|
||||
$page = query_gemini("$base\/do/match?2017");
|
||||
for my $item(qw(2017-12-25 2017-12-26 2017-12-27)) {
|
||||
like($page, qr/^=> $base\/$item $item$/m, "match menu contains $item");
|
||||
}
|
||||
like($page, qr/2017-12-27.*2017-12-26.*2017-12-25/s,
|
||||
"match menu sorted newest first");
|
||||
|
||||
# search
|
||||
$page = query_gemini("$base\/do/search?tag:day");
|
||||
for my $item(qw(2017-12-25 2017-12-26 2017-12-27)) {
|
||||
like($page, qr/^=> $base\/$item $item/m, "search menu contains $item");
|
||||
}
|
||||
like($page, qr/2017-12-27.*2017-12-26.*2017-12-25/s,
|
||||
"search menu sorted newest first");
|
||||
|
||||
# rc
|
||||
$page = query_gemini("$base\/do/rc");
|
||||
my $re = join(".*", "Picture", "2017-12-27", "2017-12-26", "2017-12-25",
|
||||
"Friends", "Chris", "Berta", "Alex");
|
||||
like($page, qr/$re/s, "rc in the right order");
|
||||
|
||||
$page = query_gemini("$base\/do/rc/minor");
|
||||
|
||||
$re = join(".*", "Friends", "2017-12-27", "2017-12-26", "2017-12-25");
|
||||
like($page, qr/$re/s, "minor rc in the right order");
|
||||
|
||||
# feeds
|
||||
my $xpc = XML::LibXML::XPathContext->new;
|
||||
$xpc->registerNs('atom', 'http://www.w3.org/2005/Atom');
|
||||
|
||||
# rss with regular pages
|
||||
my $feed = new XML::RSS;
|
||||
$page = query_gemini("$base\/do/rss");
|
||||
ok($page =~ s!^20 application/rss\+xml\r\n!!, "RSS header OK");
|
||||
ok($feed->parse($page), "RSS parse OK");
|
||||
for my $item(qw(Alex Berta Chris 2017-12-25 2017-12-26 2017-12-27)) {
|
||||
ok(grep(/$item/, map { $_->{title} } @{$feed->{items}}), "$item found in RSS feed");
|
||||
}
|
||||
|
||||
# atom with regular pages
|
||||
$page = query_gemini("$base\/do/atom");
|
||||
ok($page =~ s!^20 application/atom\+xml\r\n!!, "Atom header OK");
|
||||
# $feed->parse($page) results in warnings that I can't get rid of
|
||||
ok(my $doc = XML::LibXML->load_xml(string => $page), "Atom parse OK");
|
||||
for my $item(qw(Alex Berta Chris 2017-12-25 2017-12-26 2017-12-27)) {
|
||||
ok($xpc->find("//atom:entry/atom:title[text()='$item']", $doc), "$item found in Atom feed");
|
||||
}
|
||||
|
||||
add_module('journal-rss.pl');
|
||||
|
||||
# rss with just the journal
|
||||
$page = query_gemini("$base\/do/rss");
|
||||
ok($page =~ s!^20 application/rss\+xml\r\n!!, "RSS header OK");
|
||||
ok($feed->parse($page), "RSS parse OK");
|
||||
for my $item(qw(2017-12-25 2017-12-26 2017-12-27)) {
|
||||
ok(grep(/$item/, map { $_->{title} } @{$feed->{items}}), "$item found in RSS feed");
|
||||
}
|
||||
for my $item(qw(Alex Berta Chris)) {
|
||||
ok(!grep(/$item/, map { $_->{title} } @{$feed->{items}}), "$item not found in RSS feed");
|
||||
}
|
||||
my ($sec, $min, $hour, $mday, $mon, $year, $wday) = localtime;
|
||||
$year += 1900;
|
||||
# Fri, 19 Jun 2020 20:41:55 GMT
|
||||
my $today = sprintf("%s, %02d %s %d",
|
||||
qw(Sun Mon Tue Wed Thu Fri Sat)[$wday], $mday,
|
||||
qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)[$mon], $year);
|
||||
like($page, qr!<pubDate>$today \d\d:\d\d:\d\d GMT</pubDate>!, "Update timestamp for today");
|
||||
|
||||
# atom with just the journal
|
||||
$page = query_gemini("$base\/do/atom");
|
||||
ok($page =~ s!^20 application/atom\+xml\r\n!!, "Atom header OK");
|
||||
# $feed->parse($page) results in warnings that I can't get rid of
|
||||
ok($doc = XML::LibXML->load_xml(string => $page), "Atom parse OK");
|
||||
for my $item(qw(2017-12-25 2017-12-26 2017-12-27)) {
|
||||
ok($xpc->find("//atom:entry/atom:title[text()='$item']", $doc), "$item found in Atom feed");
|
||||
}
|
||||
for my $item(qw(Alex Berta Chris)) {
|
||||
ok(!$xpc->find("//atom:entry/atom:title[text()='$item']", $doc), "$item not found in Atom feed");
|
||||
}
|
||||
$today = sprintf("%d-%02d-%02d", $year, $mon+1, $mday);
|
||||
like($page, qr!<updated>${today}T\d\d:\d\d:\d\dZ</updated>!, "Update timestamp for today");
|
||||
|
||||
# upload text
|
||||
|
||||
my $titan = "titan://$host:$port";
|
||||
|
||||
my $haiku = <<EOT;
|
||||
Quiet disk ratling
|
||||
Keyboard clicking, then it stops.
|
||||
Rain falls and I think
|
||||
EOT
|
||||
|
||||
$page = query_gemini("$titan/raw/Haiku;size=76;mime=text/plain", $haiku);
|
||||
like($page, qr/^30 $base\/Haiku\r$/, "Titan Haiku");
|
||||
|
||||
my $haiku_re = $haiku;
|
||||
$haiku_re =~ s/\s+/ /g; # lines get wrapped
|
||||
$haiku_re =~ s/\s+$//g;
|
||||
$haiku_re = quotemeta($haiku_re);
|
||||
$page = query_gemini("$base/Haiku");
|
||||
like($page, qr/^$haiku_re/m, "Haiku saved");
|
||||
|
||||
# comment
|
||||
|
||||
like($page, qr/^=> $base\/Comments_on_Haiku Comments on this page$/m, "Comment page link");
|
||||
|
||||
$page = query_gemini("$base/Comments_on_Haiku");
|
||||
like($page, qr/^=> $base\/do\/comment\/Comments_on_Haiku Leave a comment$/m, "Leave comment link");
|
||||
|
||||
$page = query_gemini("$base/do/comment/Comments_on_Haiku");
|
||||
like($page, qr/^30 $base\/do\/comment\/Comments_on_Haiku\/0\r$/, "Redirect to a question");
|
||||
|
||||
$page = query_gemini("$base/do/comment/Comments_on_Haiku/0");
|
||||
like($page, qr/^10 Who rules in Rivendell\?\r$/, "Ask security question");
|
||||
|
||||
$page = query_gemini("$base/do/comment/Comments_on_Haiku/0?elrond");
|
||||
like($page, qr/^30 $base\/do\/comment\/Comments_on_Haiku\/0\/elrond\r$/, "Redirect to comment prompt");
|
||||
|
||||
$page = query_gemini("$base/do/comment/Comments_on_Haiku/0/elrond");
|
||||
like($page, qr/^10 Comment\r$/, "Ask for comment");
|
||||
|
||||
$page = query_gemini("$base/do/comment/Comments_on_Haiku/0/elrond?Give%20me%20the%20ring!");
|
||||
like($page, qr/^30 $base\/Comments_on_Haiku\r$/, "Redirect back to the main page");
|
||||
|
||||
$page = query_gemini("$base/Comments_on_Haiku");
|
||||
like($page, qr/^Give me the ring!\n\n-- Anonymous/m, "Comment saved");
|
||||
|
||||
# extension
|
||||
|
||||
$page = query_gemini("$base/do/test");
|
||||
like($page, qr/^Test\n/m, "Extension runs");
|
||||
|
||||
done_testing();
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017–2018 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2017–2019 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
|
||||
@@ -46,6 +46,7 @@ if (!defined $pid) {
|
||||
my $secure_perl_path = $Config{perlpath};
|
||||
exec($secure_perl_path,
|
||||
"stuff/gopher-server.pl",
|
||||
"--host=127.0.0.1",
|
||||
"--port=$port",
|
||||
"--log_level=0", # set to 4 for verbose logging
|
||||
"--wiki=./wiki.pl",
|
||||
@@ -300,16 +301,96 @@ like($page, qr/^0Zürich♥\tZ%c3%bcrich%e2%99%a5\t/m, "UTF-8 encoded text link"
|
||||
like($page, qr/^1Üetliberg♥\t%c3%9cetliberg%e2%99%a5\/menu\t/m,
|
||||
"UTF-8 encoded links");
|
||||
|
||||
# Space normalization
|
||||
test_page(update_page('my_page', '[[my page]]'));
|
||||
$page = query_gopher("my_page"); # all pages are normalized
|
||||
like($page, qr/\[\[my page\]\]/, "Page name with space");
|
||||
|
||||
$page = query_gopher("my_page/menu");
|
||||
like($page, qr/^0my page\tmy_page\t/m, "Space translates to underscore in links");
|
||||
|
||||
$page = <<EOF;
|
||||
Floodgap link, and the typical Gopher link:
|
||||
[http://gopher.floodgap.com/gopher/gw?a=gopher%3A%2F%2Fsdf.org%3A70%2F0%2Fusers%2Fsolderpunk%2Fphlog%2Fintroducing-vf1.txt VF-1], [gopher://sdf.org:70/1/phlogs/ Phlogs]
|
||||
|
||||
Solderpunk was writing about Gopher and the Web again.
|
||||
[gopher://zaibatsu.circumlunar.space:70/0/~solderpunk/phlog/protocol-pondering-intensifies.txt]
|
||||
[gopher://zaibatsu.circumlunar.space:70/0/~solderpunk/phlog/protocol-pondering-intensifies-ii.txt]
|
||||
[gopher://zaibatsu.circumlunar.space:70/0/~solderpunk/phlog/protocol-pondering-intensifies-iii.txt]
|
||||
|
||||
So that's what I did. I wrote a little server that serves text files.
|
||||
Requests are simple selectors. Like Gopher. Like Finger. Remember,
|
||||
[[2019-01-09 Finger is Gopher|finger is gopher]]!
|
||||
|
||||
I called it *Nimi Mute*, "many words."
|
||||
|
||||
* https://alexschroeder.ch/cgit/nimi-mute/about/
|
||||
* [https://github.com/kensanata/nimi-mute Nimi Mute]
|
||||
|
||||
As you can see in the README, you can even use `finger` or `lynx` to
|
||||
get text files from it! It's all the same. `telnet` and `nc` also
|
||||
work, of course. :)
|
||||
|
||||
Tags: [[tag:Gopher]] [[tag:Finger]] [[tag:Perl 5]]
|
||||
EOF
|
||||
|
||||
# gopher links
|
||||
update_page('Gopher', '[http://gopher.floodgap.com/gopher/gw?a=gopher%3A%2F%2Fsdf.org%3A70%2F0%2Fusers%2Fsolderpunk%2Fphlog%2Fintroducing-vf1.txt VF-1], [gopher://sdf.org:70/1/phlogs/ Phlogs]');
|
||||
update_page('Gopher', $page);
|
||||
$page = query_gopher("Gopher/menu");
|
||||
like($page, qr/^1Phlogs\t\/phlogs\/\tsdf\.org\t70/m, "Direct Gopher link");
|
||||
like($page, qr/^0VF-1\t\/users\/solderpunk\/phlog\/introducing-vf1.txt\tsdf\.org\t70/m, "Floodgap proxy link");
|
||||
|
||||
# gopher tags
|
||||
update_page('Gopher', 'Tags: [[tag:Gopher]] [[tag:Perl 6]]');
|
||||
my $re = "^0"
|
||||
. join("\t",
|
||||
quotemeta("zaibatsu.circumlunar.space:70/0/~solderpunk/phlog/protocol-pondering-intensifies.txt"),
|
||||
quotemeta("/~solderpunk/phlog/protocol-pondering-intensifies.txt"),
|
||||
quotemeta("zaibatsu.circumlunar.space"),
|
||||
"70");
|
||||
like($page, qr/$re/m, "Gopher link 1");
|
||||
|
||||
my $re = "^0"
|
||||
. join("\t",
|
||||
quotemeta("zaibatsu.circumlunar.space:70/0/~solderpunk/phlog/protocol-pondering-intensifies-ii.txt"),
|
||||
quotemeta("/~solderpunk/phlog/protocol-pondering-intensifies-ii.txt"),
|
||||
quotemeta("zaibatsu.circumlunar.space"),
|
||||
"70");
|
||||
like($page, qr/$re/m, "Gopher link 2");
|
||||
|
||||
my $re = "^0"
|
||||
. join("\t",
|
||||
quotemeta("zaibatsu.circumlunar.space:70/0/~solderpunk/phlog/protocol-pondering-intensifies-iii.txt"),
|
||||
quotemeta("/~solderpunk/phlog/protocol-pondering-intensifies-iii.txt"),
|
||||
quotemeta("zaibatsu.circumlunar.space"),
|
||||
"70");
|
||||
like($page, qr/$re/m, "Gopher link 3");
|
||||
|
||||
my $re = "^1"
|
||||
. join("\t",
|
||||
quotemeta("finger is gopher"),
|
||||
quotemeta("2019-01-09_Finger_is_Gopher/menu"),
|
||||
"127\.0\.0\.1",
|
||||
$port);
|
||||
like($page, qr/$re/m, "Internal link");
|
||||
|
||||
my $re = "^h"
|
||||
. join("\t",
|
||||
quotemeta("Nimi Mute"),
|
||||
quotemeta("URL:https://github.com/kensanata/nimi-mute"),
|
||||
"127\.0\.0\.1",
|
||||
$port);
|
||||
like($page, qr/$re/m, "HTML Link");
|
||||
|
||||
my $re = "^h"
|
||||
. join("\t",
|
||||
quotemeta("https://alexschroeder.ch/cgit/nimi-mute/about/"),
|
||||
quotemeta("URL:https://alexschroeder.ch/cgit/nimi-mute/about/"),
|
||||
"127\.0\.0\.1",
|
||||
$port);
|
||||
like($page, qr/$re/m, "Bare HTML Link");
|
||||
|
||||
# and on the page itself, tags are rendered differently
|
||||
$page = query_gopher("Gopher");
|
||||
like($page, qr/#Gopher/m, "Gopher tag");
|
||||
like($page, qr/#Perl_6/m, "Gopher multi-word tag");
|
||||
like($page, qr/#Perl_5/m, "Gopher multi-word tag");
|
||||
|
||||
done_testing();
|
||||
|
||||
201
t/grep-filtered.t
Normal file
201
t/grep-filtered.t
Normal file
@@ -0,0 +1,201 @@
|
||||
# Copyright (C) 2006–2020 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/>.
|
||||
|
||||
# These tests were copied from search.t...
|
||||
|
||||
require './t/test.pl';
|
||||
package OddMuse;
|
||||
use Test::More;
|
||||
use utf8; # tests contain UTF-8 characters and it matters
|
||||
|
||||
add_module('grep-filtered.pl');
|
||||
|
||||
# Search for broken regular expressions
|
||||
|
||||
test_page(get_page('search=%2Btest'), 'Search for: \+test');
|
||||
|
||||
# Test search, make sure ordinary users don't see the replacement form
|
||||
|
||||
update_page('SearchAndReplace', 'This is fooz and this is barz.', '', 1);
|
||||
$page = get_page('search=fooz');
|
||||
test_page($page,
|
||||
'<h1>Search for: fooz</h1>',
|
||||
'<p class="result">1 pages found.</p>',
|
||||
'This is <strong>fooz</strong> and this is barz.');
|
||||
xpath_test($page, '//span[@class="result"]/a[@class="local"][@href="http://localhost/wiki.pl/SearchAndReplace"][text()="SearchAndReplace"]');
|
||||
test_page_negative($page, 'Replace:');
|
||||
|
||||
# Search page name
|
||||
$page = get_page('search=andreplace');
|
||||
test_page($page,
|
||||
'<h1>Search for: andreplace</h1>',
|
||||
'<p class="result">1 pages found.</p>');
|
||||
# FIXME: Not sure this should work... 'Search<strong>AndReplace</strong>'
|
||||
xpath_test($page, '//span[@class="result"]/a[@class="local"][@href="http://localhost/wiki.pl/SearchAndReplace"][text()="SearchAndReplace"]');
|
||||
|
||||
# Brackets in the page name
|
||||
|
||||
test_page(update_page('Search (and replace)', 'Muu'),
|
||||
'search=%22Search\+%5c\(and\+replace%5c\)%22');
|
||||
|
||||
# Make sure only admins can replace
|
||||
|
||||
test_page(get_page('search=foo pwd=foo'),
|
||||
'Replace:');
|
||||
|
||||
test_page(get_page('search=foo replace=bar'),
|
||||
'This operation is restricted to administrators only...');
|
||||
|
||||
# Preview simple replacement operation
|
||||
|
||||
test_page(get_page('search=fooz replace=fuuz preview=1 pwd=foo'), split('\n',<<'EOT'));
|
||||
<h1>Preview: fooz → fuuz</h1>
|
||||
<p class="result">1 pages found.</p>
|
||||
<div class="old"><p>< This is <strong class="changes">fooz</strong> and this is barz.
|
||||
<div class="new"><p>> This is <strong class="changes">fuuz</strong> and this is barz.
|
||||
EOT
|
||||
|
||||
# Verify that the change has not been made
|
||||
|
||||
test_page(get_page('SearchAndReplace'), 'This is fooz and this is barz.');
|
||||
|
||||
# Simple replace where the replacement pattern is found
|
||||
|
||||
test_page(get_page('search=fooz replace=fuuz pwd=foo'), split('\n',<<'EOT'));
|
||||
<h1>Replaced: fooz → fuuz</h1>
|
||||
<p class="result">1 pages found.</p>
|
||||
This is <strong>fuuz</strong> and this is barz.
|
||||
EOT
|
||||
|
||||
# Verify that the change has been made
|
||||
|
||||
test_page(get_page('SearchAndReplace'), 'This is fuuz and this is barz.');
|
||||
|
||||
# Replace with empty string
|
||||
|
||||
test_page(get_page('search=this%20is%20 replace= pwd=foo delete=1'), split('\n',<<'EOT'));
|
||||
<h1>Replaced: this is → </h1>
|
||||
<p class="result">1 pages found.</p>
|
||||
fuuz and barz.
|
||||
EOT
|
||||
|
||||
test_page(get_page('SearchAndReplace'), '<p>fuuz and barz.');
|
||||
|
||||
# Creating 12 pages
|
||||
for my $i ('A' .. 'M') {
|
||||
OpenPage("Page_$i");
|
||||
Save("Page_$i", 'Something');
|
||||
}
|
||||
|
||||
# Testing default pagination (10 pages)
|
||||
|
||||
$page = get_page('search=Something replace=Other preview=1 pwd=foo');
|
||||
test_page($page, split('\n',<<'EOT'));
|
||||
<h1>Preview: Something → Other</h1>
|
||||
<p class="result">13 pages found.</p>
|
||||
<div class="old"><p>< <strong class="changes">Something</strong>
|
||||
<div class="new"><p>> <strong class="changes">Other</strong>
|
||||
EOT
|
||||
|
||||
test_page($page, map { "Page_$_" } ('A' .. 'J'));
|
||||
test_page_negative($page, map { "Page_$_" } ('K' .. 'M'));
|
||||
xpath_test($page, '//a[@class="more"][@href="http://localhost/wiki.pl?search=Something;preview=1;offset=10;num=10;replace=Other"]');
|
||||
|
||||
# Next page
|
||||
|
||||
$page = get_page('search=Something preview=1 offset=10 num=10 replace=Other pwd=foo');
|
||||
test_page($page, map { "Page_$_" } ('K' .. 'M'));
|
||||
|
||||
# Now do the replacement
|
||||
|
||||
$page = get_page('search=Something replace=Other pwd=foo');
|
||||
test_page($page, 'Replaced: Something → Other', '13 pages found',
|
||||
map { "Page_$_" } ('A' .. 'M'));
|
||||
|
||||
# Verify that the change has been made
|
||||
|
||||
test_page(get_page('search=Other'), 'Search for: Other', '13 pages found');
|
||||
|
||||
|
||||
# Replace with backreferences, where the replacement pattern is no longer found.
|
||||
# Take 'fuuz and barz.' and replace ([a-z]+)z with x$1 results in 'xfuu and xbar.'
|
||||
test_page(get_page('"search=([a-z]%2b)z" replace=x%241 pwd=foo'), '1 pages found');
|
||||
test_page(get_page('SearchAndReplace'), 'xfuu and xbar.');
|
||||
|
||||
# Create an extra page that should not be found
|
||||
update_page('NegativeSearchTest', 'this page contains an ab');
|
||||
update_page('NegativeSearchTestTwo', 'this page contains another ab');
|
||||
test_page(get_page('search=xb replace=[xa]b pwd=foo'), '1 pages found'); # not two ab!
|
||||
test_page(get_page('SearchAndReplace'), 'xfuu and \[xa\]bar.');
|
||||
|
||||
# Handle quoting
|
||||
test_page(get_page('search=xfuu replace=/fuu/ pwd=foo'), '1 pages found'); # not two ab!
|
||||
test_page(get_page('SearchAndReplace'), '/fuu/ and \[xa\]bar.');
|
||||
test_page(get_page('search=/fuu/ replace={{fuu}} pwd=foo'), '1 pages found');
|
||||
test_page(get_page('SearchAndReplace'), '\{\{fuu\}\} and \[xa\]bar.');
|
||||
|
||||
# Check headers especially the quoting of non-ASCII characters.
|
||||
|
||||
$page = update_page("Alexander_Schröder", "Edit [[Alexander Schröder]]!");
|
||||
xpath_test($page,
|
||||
'//h1/a[@title="Click to search for references to this page"][@href="http://localhost/wiki.pl?search=%22Alexander+Schr%c3%b6der%22"][text()="Alexander Schröder" or text()="' . Encode::encode_utf8('Alexander Schröder') . '"]',
|
||||
'//a[@class="local"][@href="http://localhost/wiki.pl/Alexander_Schr%c3%b6der"][text()="Alexander Schröder" or text()="' . Encode::encode_utf8('Alexander Schröder') . '"]');
|
||||
|
||||
xpath_test(update_page('IncludeSearch',
|
||||
"first line\n<search \"ab\">\nlast line"),
|
||||
'//p[text()="first line "]', # note the NL -> SPC
|
||||
'//div[@class="search"]/p/span[@class="result"]/a[@class="local"][@href="http://localhost/wiki.pl/NegativeSearchTest"][text()="NegativeSearchTest"]',
|
||||
'//div[@class="search"]/p/span[@class="result"]/a[@class="local"][@href="http://localhost/wiki.pl/NegativeSearchTestTwo"][text()="NegativeSearchTestTwo"]',
|
||||
'//p[text()=" last line"]'); # note the NL -> SPC
|
||||
|
||||
xpath_test(get_page('search=Schröder'),
|
||||
'//input[@name="search"][@value="Schröder"]');
|
||||
|
||||
# Search for zero
|
||||
|
||||
update_page("Zero", "This is about 0 and the empty string ''.");
|
||||
test_page(get_page('search=0'),
|
||||
'<h1>Search for: 0</h1>',
|
||||
'<p class="result">1 pages found.</p>',
|
||||
"This is about <strong>0</strong> and the empty string ''.",
|
||||
'meta name="robots" content="NOINDEX,FOLLOW"');
|
||||
|
||||
# Search for tags
|
||||
|
||||
update_page("Tag", "This is <b>bold</b>.");
|
||||
test_page(get_page('search="<b>"'),
|
||||
'<h1>Search for: <b></h1>',
|
||||
'<p class="result">1 pages found.</p>',
|
||||
"This is <strong><b></strong>.");
|
||||
|
||||
# Search for quoted strings
|
||||
|
||||
update_page("Tugend", "Ein wirklich tugendhafter Mensch
|
||||
bemüht sich nicht um seine Tugend,
|
||||
darum ist er tugendhaft.");
|
||||
update_page("Laster", "Ein scheinbar tugendhafter Mensch
|
||||
bemüht sich dauernd um seine Tugend,
|
||||
darum ist er nicht wirklich tugendhaft.");
|
||||
|
||||
# unordered words
|
||||
test_page(get_page('search="darum ist er tugendhaft" raw=1'),
|
||||
'title: Tugend', 'title: Laster');
|
||||
|
||||
# in order
|
||||
$page = get_page('search="\"darum ist er tugendhaft\"" raw=1');
|
||||
test_page($page, 'title: Tugend');
|
||||
test_page_negative($page, 'title: Laster');
|
||||
|
||||
done_testing;
|
||||
14
t/hr.t
14
t/hr.t
@@ -27,18 +27,18 @@ test_page(get_page('hr'), 'one ---- two');
|
||||
add_module('usemod.pl');
|
||||
update_page('hr', "one\n----\nthree\n");
|
||||
test_page(get_page('hr'),
|
||||
'<div class="content browse"><p>one </p><hr /><p>three</p></div>');
|
||||
'<div class="content browse" lang="en"><p>one </p><hr /><p>three</p></div>');
|
||||
remove_rule(\&UsemodRule);
|
||||
|
||||
# headers only
|
||||
add_module('headers.pl');
|
||||
update_page('hr', "one\n----\ntwo\n");
|
||||
test_page(get_page('hr'),
|
||||
'<div class="content browse"><h3>one</h3><p>two</p></div>');
|
||||
'<div class="content browse" lang="en"><h3>one</h3><p>two</p></div>');
|
||||
|
||||
update_page('hr', "one\n\n----\nthree\n");
|
||||
test_page(get_page('hr'),
|
||||
'<div class="content browse"><p>one</p><hr /><p>three</p></div>');
|
||||
'<div class="content browse" lang="en"><p>one</p><hr /><p>three</p></div>');
|
||||
remove_rule(\&HeadersRule);
|
||||
|
||||
# with portrait support
|
||||
@@ -49,19 +49,19 @@ clear_pages();
|
||||
add_module('portrait-support.pl');
|
||||
update_page('hr', "[new]one\n----\ntwo\n");
|
||||
test_page(get_page('hr'),
|
||||
'<div class="content browse"><div class="color one level0"><p>one </p></div><hr /><p>two</p></div>');
|
||||
'<div class="content browse" lang="en"><div class="color one level0"><p>one </p></div><hr /><p>two</p></div>');
|
||||
|
||||
# usemod and portrait-support
|
||||
add_module('usemod.pl');
|
||||
update_page('hr', "one\n----\nthree\n");
|
||||
test_page(get_page('hr'),
|
||||
'<div class="content browse"><p>one </p><hr /><p>three</p></div>');
|
||||
'<div class="content browse" lang="en"><p>one </p><hr /><p>three</p></div>');
|
||||
remove_rule(\&UsemodRule);
|
||||
|
||||
# headers and portrait-support
|
||||
add_module('headers.pl');
|
||||
update_page('hr', "one\n----\ntwo\n");
|
||||
test_page(get_page('hr'), '<div class="content browse"><h3>one</h3><p>two</p></div>');
|
||||
test_page(get_page('hr'), '<div class="content browse" lang="en"><h3>one</h3><p>two</p></div>');
|
||||
|
||||
update_page('hr', "one\n\n----\nthree\n");
|
||||
test_page(get_page('hr'), '<div class="content browse"><p>one</p><hr /><p>three</p></div>');
|
||||
test_page(get_page('hr'), '<div class="content browse" lang="en"><p>one</p><hr /><p>three</p></div>');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2008-2018 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2008-2021 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
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
require './t/test.pl';
|
||||
package OddMuse;
|
||||
use Test::More tests => 43;
|
||||
use Test::More tests => 45;
|
||||
|
||||
add_module('journal-rss.pl');
|
||||
|
||||
@@ -110,6 +110,10 @@ xpath_test(get_page('action=journal offset=10'),
|
||||
|
||||
# check next page but with a tag search
|
||||
xpath_test(get_page('action=journal search=tag:oddmuse'),
|
||||
'//atom:link[@rel="self"][@href="http://localhost/wiki.pl?action=journal;search=tag:oddmuse"]',
|
||||
'//atom:link[@rel="last"][@href="http://localhost/wiki.pl?action=journal;search=tag:oddmuse"]',
|
||||
'//atom:link[@rel="previous"][@href="http://localhost/wiki.pl?action=journal;offset=10;search=tag:oddmuse"]');
|
||||
'//atom:link[@rel="self"][@href="http://localhost/wiki.pl?action=journal;search=tag%3aoddmuse"]',
|
||||
'//atom:link[@rel="last"][@href="http://localhost/wiki.pl?action=journal;search=tag%3aoddmuse"]',
|
||||
'//atom:link[@rel="previous"][@href="http://localhost/wiki.pl?action=journal;offset=10;search=tag%3aoddmuse"]');
|
||||
|
||||
# check raw
|
||||
$page = get_page('action=journal raw=1 rsslimit=1');
|
||||
test_page($page, 'generator: Oddmuse', 'title: 2008-09-22');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2006 Alex Schroeder <alex@emacswiki.org>
|
||||
# Copyright (C) 2006-2019 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
|
||||
@@ -15,15 +15,38 @@
|
||||
|
||||
require './t/test.pl';
|
||||
package OddMuse;
|
||||
use Test::More tests => 5;
|
||||
use Test::More;
|
||||
use utf8; # tests contain UTF-8 characters and it matters
|
||||
|
||||
%Languages = ('de' => '\b(der|die|das|und|oder)\b',
|
||||
'fr' => '\b(et|le|la|pas)\b', );
|
||||
'fr' => '\b(et|le|la|ne|pas)\b', );
|
||||
|
||||
is(GetLanguages('This is English text and cannot be identified.'), '', 'unknown language');
|
||||
is(GetLanguages('Die Katze tritt die Treppe krumm.'), '', 'not enough German words');
|
||||
is(GetLanguages('Die Katze tritt die Treppe und die Stiege krumm.'), 'de', 'enough German words');
|
||||
is(GetLanguages('Le chat fait la même chose et ne chante pas.'), 'fr', 'enough French words');
|
||||
is(GetLanguages('Die Katze tritt die Treppe und die Stiege krumm. '
|
||||
. 'Le chat fait la même chose et ne chante pas.'), 'de,fr', 'both German and French');
|
||||
is(GetLanguages('Die Katze tritt die Treppe und die Stiege krumm. ' # 4 matches
|
||||
. 'Le chat fait la même chose et ne chante pas.' # 5 matches
|
||||
), 'fr,de', 'both German and French');
|
||||
|
||||
is(GetLanguage('This is English text and cannot be identified.'), 'en', 'now it defaults to English');
|
||||
is(GetLanguage('Die Katze tritt die Treppe krumm.'), 'en', 'not enough German words but it defaults to English');
|
||||
is(GetLanguage('Die Katze tritt die Treppe krumm und so.'), 'de', 'three German words');
|
||||
is(GetLanguage('Die Katze tritt die Treppe und die Stiege krumm. ' # 4 matches
|
||||
. 'Le chat fait la même chose et ne chante pas.' # 5 matches
|
||||
), 'fr', 'French has the most hits');
|
||||
|
||||
my $id = 'Test';
|
||||
my $text = 'Die Katze tritt die Treppe und die Stiege krumm. ' # 4 matches
|
||||
. 'Le chat fait la même chose et ne chante pas.'; # 5 matches
|
||||
|
||||
AppendStringToFile($ConfigFile,<<'EOT');
|
||||
%Languages = ('de' => '\b(der|die|das|und|oder)\b',
|
||||
'fr' => '\b(et|le|la|ne|pas)\b', );
|
||||
EOT
|
||||
|
||||
test_page(update_page($id, $text), /Die Katze/);
|
||||
test_page(ReadFileOrDie($RcFile), /\bfr,de\b/);
|
||||
test_page(get_page($id), /lang="fr"/);
|
||||
|
||||
done_testing;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env perl
|
||||
# Copyright (C) 2014–2017 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2014–2019 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
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
require './t/test.pl';
|
||||
package OddMuse;
|
||||
use Test::More tests => 53;
|
||||
use Test::More tests => 68;
|
||||
|
||||
add_module('markdown-rule.pl');
|
||||
add_module('bbcode.pl');
|
||||
@@ -99,6 +99,8 @@ foo\n=\nbar
|
||||
<h2>foo ##</h2>
|
||||
bar\n##foo\nbar
|
||||
bar <h2>foo</h2><p>bar</p>
|
||||
this is #foo tag
|
||||
this is #foo tag
|
||||
```\nfoo\n```\nbar
|
||||
<pre>foo</pre><p>bar</p>
|
||||
```\nfoo\n```
|
||||
@@ -107,6 +109,10 @@ bar <h2>foo</h2><p>bar</p>
|
||||
``` foo ``` bar
|
||||
`bar`
|
||||
<code>bar</code>
|
||||
"""\n*foo*\n"""\nhallo
|
||||
<blockquote><p><em>foo</em></p></blockquote><p>hallo</p>
|
||||
> *foo*\nhallo
|
||||
<blockquote><p><em>foo</em></p></blockquote><p>hallo</p>
|
||||
|a|b|\n|c|d|\nbar
|
||||
<table><tr><th>a</th><th>b</th></tr><tr><td>c</td><td>d</td></tr></table><p>bar</p>
|
||||
|a|b|\n|c|d|
|
||||
@@ -119,17 +125,52 @@ bar <h2>foo</h2><p>bar</p>
|
||||
<table><tr><th><em>foo</em></th></tr></table>
|
||||
|_foo_
|
||||
<table><tr><th><em style="font-style: normal; text-decoration: underline">foo</em></th></tr></table>
|
||||
| a| b|\n| c| d|
|
||||
<table><tr><th style="text-align: right">a</th><th style="text-align: right">b</th></tr><tr><td style="text-align: right">c</td><td style="text-align: right">d</td></tr></table>
|
||||
| a | b |\n| c | d |
|
||||
<table><tr><th style="text-align: center">a</th><th style="text-align: center">b</th></tr><tr><td style="text-align: center">c</td><td style="text-align: center">d</td></tr></table>
|
||||
foo ~~bar~~
|
||||
foo <del>bar</del>
|
||||
pay 1.-/month
|
||||
pay 1.-/month
|
||||
EOT
|
||||
|
||||
xpath_run_tests(split('\n',<<'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"][@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
|
||||
|
||||
# test the quote again, writing an actual page
|
||||
test_page(update_page('cache', qq{"""\n*foo*\n"""\nhallo}),
|
||||
'<blockquote><p><em>foo</em></p></blockquote><p>hallo</p>');
|
||||
# test the page again to find errors in dirty block marking
|
||||
test_page(get_page('cache'),
|
||||
'<blockquote><p><em>foo</em></p></blockquote><p>hallo</p>');
|
||||
|
||||
# test the other quote again, writing an actual page
|
||||
test_page(update_page('cache', qq{> *foo*\n> bar\nhallo}),
|
||||
'<blockquote><p><em>foo</em> bar</p></blockquote><p>hallo</p>');
|
||||
# test the page again to find errors in dirty block marking
|
||||
test_page(get_page('cache'),
|
||||
'<blockquote><p><em>foo</em> bar</p></blockquote><p>hallo</p>');
|
||||
|
||||
@MyRules = grep { $_ ne \&MarkdownExtraRule } @MyRules;
|
||||
|
||||
run_tests(split(/\n/,<<'EOT'));
|
||||
__underline__
|
||||
__underline__
|
||||
_underline_
|
||||
_underline_
|
||||
//italic//
|
||||
//italic//
|
||||
/italic/
|
||||
/italic/
|
||||
EOT
|
||||
|
||||
2
t/meta.t
2
t/meta.t
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2015-2016 Alex Schroeder <alex@gnu.com>
|
||||
# Copyright (C) 2015-2019 Alex Schroeder <alex@gnu.com>
|
||||
# Copyright (C) 2015 Alex Jakimenko <alex.jakimenko@gmail.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2016 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2016-2019 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2016 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2016-2019 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2006–2016 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2006–2019 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
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
require './t/test.pl';
|
||||
package OddMuse;
|
||||
use Test::More tests => 78;
|
||||
use Test::More tests => 89;
|
||||
use utf8; # tests contain UTF-8 characters and it matters
|
||||
|
||||
add_module('namespaces.pl');
|
||||
@@ -188,3 +188,28 @@ $feed = get_page('action=rc raw=1');
|
||||
test_page($feed, 'title: Wiki', 'title: Muu:Test');
|
||||
# BackHome never existed and Muu:BackHome was rolled back
|
||||
test_page_negative($feed, 'title: Muu:BackHome', 'title: BackHome');
|
||||
|
||||
AppendStringToFile($ConfigFile, <<'EOT');
|
||||
$InterSitePattern = '[\p{Uppercase}\d][\w_ ]*';
|
||||
$InterLinkPattern = "($InterSitePattern:[-a-zA-Z0-9\x{0080}-\x{fffd}_=!?#\$\@~`\%&*+\\/:;.,]*[-a-zA-Z0-9\x{0080}-\x{fffd}_=#\$\@~`\%&*+\\/])$QDelim";
|
||||
$FreeInterLinkPattern = "($InterSitePattern:[-a-zA-Z0-9\x{0080}-\x{fffd}_=!?#\$\@~`\%&*+\\/:;.,()' ]+)";
|
||||
EOT
|
||||
|
||||
test_page(update_page('Bond', 'My name is Bond', '007 ns', undef, undef,
|
||||
'ns=007', 'username=James'),
|
||||
'<title>Wiki 007: Bond</title>',
|
||||
'<p>My name is Bond</p>');
|
||||
test_page(get_page('action=browse id=Bond ns=007'),
|
||||
'<title>Wiki 007: Bond</title>',
|
||||
'<p>My name is Bond</p>');
|
||||
|
||||
# BannedHosts are shared! Editing the BannedHosts in a namespace results in the root BannedHosts getting written.
|
||||
|
||||
test_page(update_page('BannedHosts', '^127\.0\.0\.1', 'ban myself', undef, 1, 'ns=007', 'username=James'),
|
||||
'<title>Wiki 007: Banned Hosts</title>', 'This page does not exist');
|
||||
test_page(get_page('BannedHosts'), quotemeta('^127\.0\.0\.1'));
|
||||
test_page(update_page('Mr._Q', 'Hello'), 'This page does not exist');
|
||||
test_page(update_page('Mr._Q', 'Hello', undef, undef, undef, 'ns=007'), 'This page does not exist');
|
||||
test_page(update_page('Mr._Q', 'Hello', undef, undef, undef, 'ns=008'), 'This page does not exist');
|
||||
update_page('BannedHosts', '', 'unban myself', undef, 1);
|
||||
test_page(update_page('Mr._Q', 'Hello'), 'Hello');
|
||||
|
||||
@@ -22,9 +22,9 @@ add_module('page-trail.pl');
|
||||
my $page = get_page('FirstPage');
|
||||
|
||||
xpath_test($page,
|
||||
'//div[@class="header"]/div[@class="menu"]/span[@class="gotobar bar"]/following-sibling::span[@class="trail"]',
|
||||
'//span[@class="trail"][contains(text(),"Trail: ")]/br',
|
||||
'//span[@class="trail"]/a[@class="local"][@href="http://localhost/wiki.pl/FirstPage"][text()="FirstPage"]');
|
||||
'//header/nav/span[@class="gotobar bar"]/following-sibling::span[@class="trail"]',
|
||||
'//span[@class="trail"][contains(text(),"Trail: ")]/br',
|
||||
'//span[@class="trail"]/a[@class="local"][@href="http://localhost/wiki.pl/FirstPage"][text()="FirstPage"]');
|
||||
|
||||
# verify cookie
|
||||
test_page($page, 'Set-Cookie: Wiki=trail%251eFirstPage');
|
||||
|
||||
@@ -34,7 +34,7 @@ test_page(get_page('headers'), '<div class="color one level0"><p>foo </p></div><
|
||||
add_module('toc.pl');
|
||||
test_page(update_page('headers', "[new]foo\n== one ==\ntext\n== two ==\ntext\n== three ==\ntext\n"),
|
||||
# default to before the header
|
||||
'<div class="content browse"><div class="color one level0"><p>foo </p></div>',
|
||||
'<div class="content browse" lang="en"><div class="color one level0"><p>foo </p></div>',
|
||||
'<div class="toc"><h2>Contents</h2><ol>',
|
||||
qq{<li><a href="#${TocAnchorPrefix}1">one</a></li>},
|
||||
qq{<li><a href="#${TocAnchorPrefix}2">two</a></li>},
|
||||
|
||||
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");
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
require './t/test.pl';
|
||||
package OddMuse;
|
||||
use Test::More tests => 2;
|
||||
use Test::More tests => 3;
|
||||
|
||||
add_module('pygmentize.pl');
|
||||
|
||||
@@ -23,9 +23,14 @@ add_module('pygmentize.pl');
|
||||
skip "pygmentize not found", 2;
|
||||
}
|
||||
|
||||
my $text = '{{{perl\nmy $x = "hello";\n}}}\n';
|
||||
$page = apply_rules(newlines($text));
|
||||
test_page($page,
|
||||
'<span style="color: #666666">=</span>');
|
||||
|
||||
$ENV{PATH} = '.'; # pygmentize is not installed in the current directory
|
||||
$page = apply_rules(newlines('{{{\ntest\n}}}\n'));
|
||||
$page = apply_rules(newlines($text));
|
||||
test_page($page,
|
||||
'\bsh\b.*\bpygmentize\b.*\bnot found\b',
|
||||
'<pre>test</pre>');
|
||||
'<pre>my \$x = "hello";</pre>');
|
||||
}
|
||||
|
||||
2
t/rc.t
2
t/rc.t
@@ -51,7 +51,7 @@ test_page(get_page('action=rc raw=1'), 'title: Wiki');
|
||||
WriteStringToFile($RcFile, "1${FS}test${FS}${FS}test${FS}127.0.0.1${FS}${FS}1${FS}${FS}\n");
|
||||
test_page_negative(get_page('action=rc raw=1'), 'title: test');
|
||||
test_page(get_page('action=rc raw=1 from=1'), 'title: Wiki', 'title: test',
|
||||
'description: test', 'generator: Anonymous',
|
||||
'description: test', 'generator: \d\d\d\d',
|
||||
'link: http://localhost/wiki.pl/test',
|
||||
'last-modified: 1970-01-01T00:00Z', 'revision: 1');
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2013–2015 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2013–2019 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
|
||||
|
||||
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');
|
||||
2
t/rss.t
2
t/rss.t
@@ -70,7 +70,7 @@ test_page(get_page('action=rss full=1'),
|
||||
'<title>12h50 Forget It</title>', # wrong
|
||||
'<title>2008-08-08</title>',
|
||||
'<title>Comments on New Hope</title>',
|
||||
'<description><p>foo foo</p></description>');
|
||||
'<description><div class="e-content" lang="en"><p>foo foo</p></div></description>');
|
||||
|
||||
# no stripping of dates
|
||||
test_page(get_page('action=rss short=0'),
|
||||
|
||||
8
t/tags.t
8
t/tags.t
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2006-2018 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2006-2019 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
|
||||
@@ -184,9 +184,9 @@ test_page_negative($page, qw(Alex Jeff));
|
||||
# check the tag cloud
|
||||
xpath_test(get_page('action=tagcloud'),
|
||||
'//h1[text()="Tag Cloud"]',
|
||||
'//a[@style="font-size: 200%;"][@href="http://localhost/wiki.pl?search=tag:podcast"][@title="3"][text()="podcast"]',
|
||||
'//a[@style="font-size: 80%;"][@href="http://localhost/wiki.pl?search=tag:old_school"][@title="1"][text()="old school"]',
|
||||
'//a[@style="font-size: 80%;"][@href="http://localhost/wiki.pl?search=tag:mag"][@title="1"][text()="mag"]');
|
||||
'//a[@href="http://localhost/wiki.pl?search=tag:podcast"][text()="podcast"]',
|
||||
'//a[@href="http://localhost/wiki.pl?search=tag:old_school"][text()="old school"]',
|
||||
'//a[@href="http://localhost/wiki.pl?search=tag:mag"][text()="mag"]');
|
||||
|
||||
# check interference; in order for this test to work, we need to make
|
||||
# sure that localnames is loaded first
|
||||
|
||||
26
t/test.pl
26
t/test.pl
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2004–2015 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2004–2019 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2015 Alex-Daniel Jakimenko <alex.jakimenko@gmail.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
@@ -42,7 +42,7 @@ require 'wiki.pl';
|
||||
# our $ENV{PATH} is set to /bin:/usr/bin in order to find diff and
|
||||
# grep.
|
||||
if ($ENV{PERLBREW_PATH}) {
|
||||
$ENV{PATH} = $ENV{PERLBREW_PATH} . ':' . $ENV{PATH};
|
||||
$ENV{PATH} = join(':', split(/ /, $ENV{PERLBREW_PATH}), $ENV{PATH});
|
||||
} elsif (-f '/usr/local/bin/perl') {
|
||||
$ENV{PATH} = '/usr/local/bin:' . $ENV{PATH};
|
||||
}
|
||||
@@ -197,16 +197,24 @@ sub test_page_negative {
|
||||
|
||||
sub xpath_do {
|
||||
my ($check, $message, $page, @tests) = @_;
|
||||
$page =~ s/^.*?(<html)/$1/s; # strip headers
|
||||
$page =~ s/^.*?<\?xml.*?>\s*//s; # strip xml processing
|
||||
$page =~ s/^(.+\r\n)*\r\n//; # strip headers
|
||||
my $xml = $page =~ s/^.*?<\?xml.*?>\s*//s; # strip xml processing
|
||||
my $page_shown = 0;
|
||||
my $parser = XML::LibXML->new();
|
||||
my $parser = XML::LibXML->new(recover => 1, suppress_errors => 1); # allow HTML5 tags
|
||||
my $doc;
|
||||
my @result;
|
||||
SKIP: {
|
||||
eval { $doc = $parser->parse_html_string($page) };
|
||||
eval { $doc = $parser->parse_string($page) } if $@;
|
||||
skip("Cannot parse ".name($page).": $@", $#tests + 1) if $@;
|
||||
SKIP:
|
||||
{
|
||||
if ($xml) {
|
||||
eval { $doc = $parser->parse_string($page) };
|
||||
} else {
|
||||
eval { $doc = $parser->parse_html_string($page) };
|
||||
}
|
||||
if ($@) {
|
||||
skip("Cannot parse ".name($page).": $@", $#tests + 1);
|
||||
return;
|
||||
}
|
||||
# warn "Doc: '$doc'\n";
|
||||
foreach my $test (@tests) {
|
||||
my $nodelist;
|
||||
# libxml2 is not aware of UTF8 flag
|
||||
|
||||
@@ -33,7 +33,7 @@ $page = update_page('HomePage', 'This is the homepage. [[de:HauptSeite]] [[fr:Pa
|
||||
test_page($page, 'This is the homepage.', 'fr:PagePrincipale',
|
||||
'action=translate;id=HomePage;missing=en', 'Add Translation');
|
||||
test_page_negative($page, 'de:HauptSeite');
|
||||
xpath_test($page, '//div[@class="footer"]/span[@class="translation bar"]/a[@class="translation de"][@href="http://localhost/wiki.pl/HauptSeite"][text()="Deutsch"]');
|
||||
xpath_test($page, '//footer/span[@class="translation bar"]/a[@class="translation de"][@href="http://localhost/wiki.pl/HauptSeite"][text()="Deutsch"]');
|
||||
|
||||
AppendStringToFile($ConfigFile, q{
|
||||
%Languages = ('de' => '\b(der|die|das|und|oder)\b',
|
||||
@@ -45,7 +45,7 @@ $Translate{en} = 'English';
|
||||
});
|
||||
|
||||
xpath_test(update_page('HomePage', 'Simple test. [[de:HauptSeite]]'),
|
||||
'//div[@class="footer"]/span[@class="translation bar"]/a[@class="translation new"][text()="Add Translation"][@href="http://localhost/wiki.pl?action=translate;id=HomePage;missing=en_fr"]');
|
||||
'//footer/span[@class="translation bar"]/a[@class="translation new"][text()="Add Translation"][@href="http://localhost/wiki.pl?action=translate;id=HomePage;missing=en_fr"]');
|
||||
|
||||
$page = get_page('action=translate id=HomePage missing=en_fr');
|
||||
test_page($page, 'Français', 'English');
|
||||
@@ -53,7 +53,7 @@ test_page_negative($page, 'Deutsch');
|
||||
|
||||
# the page is now autoidentified as English, therefore French is the only one that is missing!
|
||||
xpath_test(update_page('HomePage', 'The the the the test. [[de:HauptSeite]]'),
|
||||
'//div[@class="footer"]/span[@class="translation bar"]/a[@class="translation new"][text()="Add Translation"][@href="http://localhost/wiki.pl?action=translate;id=HomePage;missing=fr"]');
|
||||
'//footer/span[@class="translation bar"]/a[@class="translation new"][text()="Add Translation"][@href="http://localhost/wiki.pl?action=translate;id=HomePage;missing=fr"]');
|
||||
|
||||
test_page(get_page('action=translate id=HomePage target=PagePrincipale translation=fr'),
|
||||
'Editing PagePrincipale');
|
||||
@@ -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');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2019 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
|
||||
|
||||
103
t/webmention.t
Normal file
103
t/webmention.t
Normal file
@@ -0,0 +1,103 @@
|
||||
# Copyright (C) 2019 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 => 21;
|
||||
use LWP::UserAgent;
|
||||
use XML::LibXML;
|
||||
|
||||
add_module('webmention.pl');
|
||||
AppendStringToFile($ConfigFile, <<'EOT');
|
||||
$CommentsPrefix = 'Comments_on_';
|
||||
$FooterNote = '<p>Author: <a rel="author" href="https://alexschroeder.ch/">Alex Schroeder</a></p>';
|
||||
EOT
|
||||
|
||||
$CommentsPrefix = 'Comments_on_';
|
||||
|
||||
like(get_page(''), qr/webmention/,
|
||||
"Webmention link for default URL");
|
||||
like(get_page('HomePage'), qr/webmention/,
|
||||
"Webmention link for homepage");
|
||||
unlike(get_page('Comments_on_HomePage'), qr/webmention/,
|
||||
"No webmention link for comment pages");
|
||||
unlike(get_page('action=history id=HomePage'), qr/webmention/,
|
||||
"No webmention link for history action");
|
||||
unlike(get_page('action=browse id=HomePage revision=1'), qr/webmention/,
|
||||
"No webmention link for browse action with revision");
|
||||
|
||||
$UsePathInfo = 1;
|
||||
|
||||
# This test is is going to use two servers in addition to this script, but in
|
||||
# actual fact we are all going to share the data directory.
|
||||
|
||||
test_page(update_page('Target', 'This is the test page.'), 'This is the test page');
|
||||
|
||||
# Server 1 is going to be the webmention server.
|
||||
|
||||
start_server();
|
||||
|
||||
# Check whether the child is up and running
|
||||
my $ua = LWP::UserAgent->new;
|
||||
my $response = $ua->get("$ScriptName?action=version");
|
||||
ok($response->is_success, "There is a wiki running at $ScriptName");
|
||||
like($response->decoded_content, qr/\bwebmention\.pl/, "The server has the webmention extension installed");
|
||||
|
||||
# Now that we have the webmention server running, we need to get the URL of the
|
||||
# test page, including its port.
|
||||
my $target_url = ScriptUrl('Target');
|
||||
|
||||
# Verify that the target exists via external request
|
||||
$response = $ua->get($target_url);
|
||||
ok($response->is_success, "Target URL response");
|
||||
like($response->decoded_content, qr/This is the test page/, "Target URL decoded");
|
||||
|
||||
# Create the Source page before starting the next server (so that it knows about
|
||||
# the new page)
|
||||
test_page(update_page('Source', "Link to $target_url"), 'Link to');
|
||||
|
||||
# Server 2 is going to be the source server.
|
||||
start_server(2);
|
||||
|
||||
# Check whether the child is up and running (with a new $ScriptName!)
|
||||
$response = $ua->get("$ScriptName?action=version");
|
||||
ok($response->is_success, "There is a wiki running at $ScriptName");
|
||||
|
||||
# New script name means we can now get the source_url.
|
||||
my $source_url = ScriptUrl('Source');
|
||||
|
||||
# Verify that the source exists via external request
|
||||
$response = $ua->get($source_url);
|
||||
ok($response->is_success, "Source URL response");
|
||||
like($response->decoded_content, qr/Link to/, "Source URL decoded");
|
||||
like($response->decoded_content, qr/$target_url/, "Source page links to Target page");
|
||||
|
||||
# Find the Webmention URL
|
||||
$response = $ua->get($target_url);
|
||||
ok($response->is_success, "Target URL response");
|
||||
like($response->decoded_content, qr/rel="webmention"/, "Target page has webmention link");
|
||||
|
||||
# Parse target page
|
||||
my $parser = XML::LibXML->new(recover => 2);
|
||||
my $dom = $parser->load_html(string => $response->decoded_content);
|
||||
my $webmention = $dom->findvalue('//link[@rel="webmention"]/@href');
|
||||
|
||||
$response = $ua->post($webmention, { source => $source_url, target => $target_url });
|
||||
|
||||
ok($response->is_success, 'Got webmention response: ' . $response->message);
|
||||
|
||||
$page = get_page('Comments_on_Target');
|
||||
test_page($page, 'Webmention:', $source_url);
|
||||
xpath_test($page, '//a[@class="url https outside"][@href="https://alexschroeder.ch/"][text()="Alex Schroeder"]');
|
||||
207
wiki.pl
Normal file → Executable file
207
wiki.pl
Normal file → Executable file
@@ -1,5 +1,5 @@
|
||||
#! /usr/bin/perl
|
||||
# Copyright (C) 2001-2019
|
||||
#! /usr/bin/env perl
|
||||
# Copyright (C) 2001-2023
|
||||
# Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2014-2015
|
||||
# Alex Jakimenko <alex.jakimenko@gmail.com>
|
||||
@@ -157,12 +157,11 @@ our $CommentsPrefix = ''; # prefix for comment pages, eg. 'Comments_on
|
||||
our $CommentsPattern = undef; # regex used to match comment pages
|
||||
our $HtmlHeaders = ''; # Additional stuff to put in the HTML <head> section
|
||||
our $IndentLimit = 20; # Maximum depth of nested lists
|
||||
our $CurrentLanguage = 'en'; # Language of error messages etc
|
||||
our $LanguageLimit = 3; # Number of matches req. for each language
|
||||
our $JournalLimit = 200; # how many pages can be collected in one go?
|
||||
our $PageNameLimit = 120; # max length of page name in bytes
|
||||
$DocumentHeader = qq(<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN")
|
||||
. qq( "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n)
|
||||
. qq(<html xmlns="http://www.w3.org/1999/xhtml">);
|
||||
$DocumentHeader = "<!DOCTYPE html>\n<html>";
|
||||
our @MyFooters = (\&GetCommentForm, \&WrapperEnd, \&DefaultFooter);
|
||||
# Checkboxes at the end of the index.
|
||||
our @IndexOptions = ();
|
||||
@@ -209,7 +208,7 @@ sub ReportError { # fatal!
|
||||
my ($errmsg, $status, $log, @html) = @_;
|
||||
InitRequest(); # make sure we can report errors before InitRequest
|
||||
print GetHttpHeader('text/html', 'nocache', $status), GetHtmlHeader(T('Error')),
|
||||
$q->start_div({class=>"error"}), $q->h1(QuoteHtml($errmsg)), @html, $q->end_div,
|
||||
$q->start_div({class=>'error'}), $q->h1(QuoteHtml($errmsg)), @html, $q->end_div,
|
||||
$q->end_html, "\n\n"; # newlines for FCGI because of exit()
|
||||
WriteStringToFile("$TempDir/error", '<body>' . $q->h1("$status $errmsg") . $q->Dump) if $log;
|
||||
map { ReleaseLockDir($_); } keys %Locks;
|
||||
@@ -386,13 +385,13 @@ sub InitLinkPatterns {
|
||||
$InterSitePattern = '[A-Z\x{0080}-\x{fffd}]+[A-Za-z\x{0080}-\x{fffd}]+';
|
||||
$InterLinkPattern = "($InterSitePattern:[-a-zA-Z0-9\x{0080}-\x{fffd}_=!?#\$\@~`\%&*+\\/:;.,]*[-a-zA-Z0-9\x{0080}-\x{fffd}_=#\$\@~`\%&*+\\/])$QDelim";
|
||||
$FreeInterLinkPattern = "($InterSitePattern:[-a-zA-Z0-9\x{0080}-\x{fffd}_=!?#\$\@~`\%&*+\\/:;.,()' ]+)"; # plus space and other characters, and no restrictions on the end of the pattern
|
||||
$UrlProtocols = 'http|https|ftp|afs|news|nntp|mid|cid|mailto|wais|prospero|telnet|gopher|irc|feed';
|
||||
$UrlProtocols = 'https?|ftp|afs|news|nntp|mid|cid|mailto|wais|prospero|telnet|gophers?|irc|feed';
|
||||
$UrlProtocols .= '|file' if $NetworkFile;
|
||||
my $UrlChars = '[-a-zA-Z0-9/@=+$_~*.,;:?!\'"()&#%]'; # see RFC 2396
|
||||
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 {
|
||||
@@ -449,7 +448,7 @@ sub ApplyRules {
|
||||
if ($type eq 'text') {
|
||||
print $q->pre({class=>"include $uri"}, QuoteHtml(GetRaw($uri)));
|
||||
} else { # never use local links for remote pages, with a starting tag
|
||||
print $q->start_div({class=>"include"});
|
||||
print $q->start_div({class=>'include'});
|
||||
ApplyRules(QuoteHtml(GetRaw($uri)), 0, ($type eq 'with-anchors'), undef, 'p');
|
||||
print $q->end_div();
|
||||
}
|
||||
@@ -515,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) {
|
||||
@@ -665,7 +664,7 @@ sub OpenHtmlEnvironment { # close the previous $html_tag and open a new one
|
||||
@HtmlStack = @stack if $found; # if not starting a new list
|
||||
$depth = $IndentLimit if $depth > $IndentLimit; # requested depth 0 makes no sense
|
||||
$html_tag_attr = qq/class="$html_tag_attr"/ # backwards-compatibility hack: classically, the third argument to this function was a single CSS class, rather than string of HTML tag attributes as in the second argument to the "AddHtmlEnvironment" function. To allow both sorts, we conditionally change this string to 'class="$html_tag_attr"' when this string is a single CSS class.
|
||||
if $html_tag_attr and $html_tag_attr !~ m/^\s*[[:alpha:]]@@+\s*=\s*('|").+\1/;
|
||||
if $html_tag_attr and $html_tag_attr !~ m/=/;
|
||||
splice(@HtmlAttrStack, 0, @HtmlAttrStack - @HtmlStack); # truncate to size of @HtmlStack
|
||||
foreach ($found .. $depth - 1) {
|
||||
unshift(@HtmlStack, $html_tag);
|
||||
@@ -801,7 +800,7 @@ sub UrlEncode {
|
||||
|
||||
sub UrlDecode {
|
||||
my $str = shift;
|
||||
return decode_utf8($str) if $str =~ s/%([0-9a-f][0-9a-f])/chr(hex($1))/eg;
|
||||
return decode_utf8($str) if $str =~ s/%([0-9a-f][0-9a-f])/chr(hex($1))/eig;
|
||||
return $str;
|
||||
}
|
||||
|
||||
@@ -876,16 +875,13 @@ sub PrintAllPages {
|
||||
next if $lang and @languages and not grep(/$lang/, @languages);
|
||||
next if PageMarkedForDeletion();
|
||||
next if substr($Page{text}, 0, 10) eq '#REDIRECT ';
|
||||
print $q->start_div({-class=>'page h-entry'}),
|
||||
$q->h1({-class => 'entry-title'},
|
||||
$links ? GetPageLink($id) : $q->a({-name=>$id}, UrlEncode(FreeToNormal($id))));
|
||||
print '<article class="h-entry">', $q->h1({-class => 'p-name'},
|
||||
$links ? GetPageLink($id) : $q->a({-name=>$id}, UrlEncode(FreeToNormal($id))));
|
||||
if ($variation ne 'titles') {
|
||||
print $q->start_div({-class=>'entry-content'});
|
||||
PrintPageHtml();
|
||||
print $q->end_div();
|
||||
PrintPageCommentsLink($id, $comments);
|
||||
}
|
||||
print $q->end_div();
|
||||
print '</article>';
|
||||
$n++; # pages actually printed
|
||||
}
|
||||
return $i;
|
||||
@@ -1133,7 +1129,7 @@ sub GetUrl {
|
||||
}
|
||||
$url = UnquoteHtml($url); # links should be unquoted again
|
||||
if ($images and $url =~ /^(http:|https:|ftp:).+\.$ImageExtensions$/i) {
|
||||
return $q->img({-src=>$url, -alt=>$url, -class=>$class});
|
||||
return $q->img({-src=>$url, -alt=>$url, -class=>$class, -loading=>'lazy'});
|
||||
} else {
|
||||
return $q->a({-href=>$url, -class=>$class}, $text);
|
||||
}
|
||||
@@ -1222,7 +1218,8 @@ sub GetDownloadLink {
|
||||
if ($image) {
|
||||
$action = $ScriptName . (($UsePathInfo and not $revision) ? '/' : '?') . $action;
|
||||
return $action if $image == 2;
|
||||
my $result = $q->img({-src=>$action, -alt=>UnquoteHtml($alt), -title=>UnquoteHtml($alt), -class=>'upload'});
|
||||
my $result = $q->img({-src=>$action, -alt=>UnquoteHtml($alt), -title=>UnquoteHtml($alt),
|
||||
-class=>'upload', -loading=>'lazy'});
|
||||
$result = ScriptLink(UrlEncode($id), $result, 'image') unless $id eq $OpenPageName;
|
||||
return $result;
|
||||
} else {
|
||||
@@ -1244,12 +1241,15 @@ sub PrintCache { # Use after OpenPage!
|
||||
}
|
||||
|
||||
sub PrintPageHtml { # print an open page
|
||||
return unless GetParam('page', 1);
|
||||
return unless GetParam('page', 1) and $Page{text};
|
||||
my $lang = (split /,/, $Page{languages})[0] || $CurrentLanguage;
|
||||
print qq{<div class="e-content" lang="$lang">};
|
||||
if ($Page{blocks} and defined $Page{flags} and GetParam('cache', $UseCache) > 0) {
|
||||
PrintCache();
|
||||
} else {
|
||||
PrintWikiToHTML($Page{text}, 1); # save cache, current revision, no main lock
|
||||
}
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
sub PrintPageDiff { # print diff for open page
|
||||
@@ -1276,7 +1276,8 @@ sub PageHtml {
|
||||
OpenPage($id);
|
||||
my $diff = ToString \&PrintPageDiff;
|
||||
return $error if $limit and length($diff) > $limit;
|
||||
my $page = ToString \&PrintPageHtml;
|
||||
my $lang = (split /,/, $Page{languages})[0] // $CurrentLanguage;
|
||||
my $page .= ToString \&PrintPageHtml;
|
||||
return $diff . $q->p($error) if $limit and length($diff . $page) > $limit;
|
||||
return $diff . $page;
|
||||
}
|
||||
@@ -1312,7 +1313,7 @@ sub GetId {
|
||||
SetParam($p, 1); # script/p/q -> p=1
|
||||
}
|
||||
}
|
||||
return $id;
|
||||
return FreeToNormal($id);
|
||||
}
|
||||
|
||||
sub DoBrowseRequest {
|
||||
@@ -1542,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
|
||||
}
|
||||
@@ -1706,11 +1713,16 @@ sub RcOtherParameters {
|
||||
my $more = '';
|
||||
foreach (@_, qw(page diff full all showedit rollback rcidonly rcuseronly rchostonly rcclusteronly rcfilteronly match lang followup)) {
|
||||
my $val = GetParam($_, '');
|
||||
$more .= ";$_=$val" if $val;
|
||||
$more .= ";$_=" . UrlEncode($val) if $val;
|
||||
}
|
||||
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));
|
||||
@@ -1856,7 +1868,7 @@ sub RcTextRevision {
|
||||
$summary = GetPageContent($id) if GetParam('full', 0);
|
||||
print "\n", RcTextItem('title', NormalToFree($id)),
|
||||
RcTextItem('description', $summary),
|
||||
RcTextItem('generator', GetAuthor($username)),
|
||||
RcTextItem('generator', GetAuthor($username, $host)),
|
||||
RcTextItem('language', join(', ', @{$languages})), RcTextItem('link', $link),
|
||||
RcTextItem('last-modified', TimeToW3($ts)),
|
||||
RcTextItem('revision', $revision),
|
||||
@@ -1873,11 +1885,11 @@ sub PrintRcText { # print text rss header and call ProcessRcLines
|
||||
|
||||
sub GetRcRss {
|
||||
my $date = TimeToRFC822($LastUpdate);
|
||||
my %excluded = ();
|
||||
my @excluded = ();
|
||||
if (GetParam("exclude", 1)) {
|
||||
foreach (split(/\n/, GetPageContent($RssExclude))) {
|
||||
if (/^ ([^ ]+)[ \t]*$/) { # only read lines with one word after one space
|
||||
$excluded{$1} = 1;
|
||||
push(@excluded, $1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1897,7 +1909,7 @@ sub GetRcRss {
|
||||
};
|
||||
my $title = QuoteHtml($SiteName) . ': ' . GetParam('title', QuoteHtml(NormalToFree($HomePage)));
|
||||
$rss .= "<title>$title</title>\n";
|
||||
$rss .= "<link>" . ScriptUrl($HomePage) . "</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};
|
||||
@@ -1917,14 +1929,14 @@ sub GetRcRss {
|
||||
$rss .= "<image>\n";
|
||||
$rss .= "<url>$RssImageUrl</url>\n";
|
||||
$rss .= "<title>$title</title>\n"; # the same as the channel
|
||||
$rss .= "<link>$ScriptName</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
|
||||
my $count = 0;
|
||||
ProcessRcLines(sub {}, sub {
|
||||
my $id = shift;
|
||||
return if $excluded{$id} or ($limit ne 'all' and $count++ >= $limit);
|
||||
return if grep { $id =~ /$_/ } @excluded or ($limit ne 'all' and $count++ >= $limit);
|
||||
$rss .= "\n" . RssItem($id, @_);
|
||||
});
|
||||
$rss .= "</channel>\n</rss>\n";
|
||||
@@ -2212,11 +2224,16 @@ sub ScriptLinkDiff {
|
||||
return ScriptLink($action, $text, 'diff');
|
||||
}
|
||||
|
||||
sub ColorCode {
|
||||
sub Code {
|
||||
my ($str) = @_;
|
||||
my $num = unpack("L",B::hash($str)); # 32-bit integer
|
||||
my $code = sprintf("%o", $num); # octal is 0-7
|
||||
my @indexes = split(//, substr($code, 0, 4)); # four numbers
|
||||
return substr($code, 0, 4); # four numbers
|
||||
}
|
||||
|
||||
sub ColorCode {
|
||||
my $code = Code(@_);
|
||||
my @indexes = split(//, $code); # four numbers
|
||||
my @colors = qw/red orange yellow green blue indigo violet white/;
|
||||
return $q->span({-class => 'ip-code', -title => T('Anonymous')},
|
||||
join('', map { $q->span({-class => $colors[$_]}, $_) }
|
||||
@@ -2224,9 +2241,10 @@ sub ColorCode {
|
||||
}
|
||||
|
||||
sub GetAuthor {
|
||||
my ($username) = @_;
|
||||
my ($username, $host) = @_;
|
||||
return $username if $username;
|
||||
return T('Anonymous');
|
||||
return T('Anonymous') if $host eq 'Anonymous';
|
||||
return Code($host);
|
||||
}
|
||||
|
||||
sub GetAuthorLink {
|
||||
@@ -2275,12 +2293,12 @@ sub GetHeader {
|
||||
|
||||
sub GetHeaderDiv {
|
||||
my ($id, $title, $oldId, $embed) = @_;
|
||||
my $result .= $q->start_div({-class=>'header'});
|
||||
my $result .= '<header>';
|
||||
if (not $embed and $LogoUrl) {
|
||||
my $url = $IndexHash{$LogoUrl} ? GetDownloadLink($LogoUrl, 2) : $LogoUrl;
|
||||
$result .= ScriptLink(UrlEncode($HomePage), $q->img({-src=>$url, -alt=>T('[Home]'), -class=>'logo'}), 'logo');
|
||||
}
|
||||
$result .= $q->start_div({-class=>'menu'});
|
||||
$result .= '<nav>';
|
||||
if (GetParam('toplinkbar', $TopLinkBar) != 2) {
|
||||
$result .= GetGotoBar($id);
|
||||
if (%SpecialDays) {
|
||||
@@ -2292,10 +2310,10 @@ sub GetHeaderDiv {
|
||||
}
|
||||
}
|
||||
$result .= GetSearchForm() if GetParam('topsearchform', $TopSearchForm) != 2;
|
||||
$result .= $q->end_div();
|
||||
$result .= '</nav>';
|
||||
$result .= $q->div({-class=>'message'}, $Message) if $Message;
|
||||
$result .= GetHeaderTitle($id, $title, $oldId);
|
||||
$result .= $q->end_div();
|
||||
$result .= '</header>';
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -2361,15 +2379,14 @@ sub Cookie {
|
||||
|
||||
sub GetHtmlHeader { # always HTML!
|
||||
my ($title, $id) = @_;
|
||||
my $edit_link = '';
|
||||
$edit_link = '<link rel="alternate" type="application/wiki" title="'
|
||||
. T('Edit this page') . '" href="'
|
||||
. ScriptUrl('action=edit;id=' . UrlEncode($id)) . '" />' if $id;
|
||||
my $edit_link = $id ? '<link rel="alternate" type="application/wiki" title="'
|
||||
. T('Edit this page') . '" href="' . ScriptUrl('action=edit;id=' . UrlEncode($id)) . '" />' : '';
|
||||
my $theme = GetParam('theme', 'default');
|
||||
return $DocumentHeader
|
||||
. $q->head($q->title($title) . $edit_link
|
||||
. GetCss() . GetRobots() . GetFeeds() . $HtmlHeaders
|
||||
. '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />')
|
||||
. '<body class="' . GetParam('theme', 'default') . '">';
|
||||
. qq{<body class="$theme" lang="$CurrentLanguage">};
|
||||
}
|
||||
|
||||
sub GetRobots { # NOINDEX for non-browse pages.
|
||||
@@ -2407,7 +2424,8 @@ sub GetCss { # prevent javascript injection
|
||||
|
||||
sub PrintPageContent {
|
||||
my ($text, $revision, $comment) = @_;
|
||||
print $q->start_div({-class=>'content browse'});
|
||||
print $q->start_div({-class=>'content browse', -lang=>GetLanguage($text)});
|
||||
# This is a lot like PrintPageHtml except that it also works for older revisions
|
||||
if ($revision eq '' and $Page{blocks} and GetParam('cache', $UseCache) > 0) {
|
||||
PrintCache();
|
||||
} else {
|
||||
@@ -2443,7 +2461,7 @@ sub WrapperEnd { # called via @MyFooters
|
||||
|
||||
sub DefaultFooter { # called via @MyFooters
|
||||
my ($id, $rev, $comment, $page) = @_;
|
||||
my $html = $q->start_div({-class=>'footer'}) . $q->hr();
|
||||
my $html = $q->hr();
|
||||
$html .= GetGotoBar($id) if GetParam('toplinkbar', $TopLinkBar) != 1;
|
||||
$html .= GetFooterLinks($id, $rev);
|
||||
$html .= GetFooterTimestamp($id, $rev, $page);
|
||||
@@ -2454,8 +2472,7 @@ sub DefaultFooter { # called via @MyFooters
|
||||
}
|
||||
$html .= T($FooterNote) if $FooterNote;
|
||||
$html .= $q->p(Ts('%s seconds', (time - $Now))) if GetParam('timing', 0);
|
||||
$html .= $q->end_div();
|
||||
return $html;
|
||||
return "<footer>$html</footer>";
|
||||
}
|
||||
|
||||
sub GetFooterTimestamp {
|
||||
@@ -2546,31 +2563,37 @@ 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;
|
||||
}
|
||||
|
||||
sub GetGotoBar { # ignore $id parameter
|
||||
return $q->span({-class=>'gotobar bar'}, (map { GetPageLink($_) }
|
||||
@UserGotoBarPages), $UserGotoBar);
|
||||
sub GetGotoBar { # ignore $id parameter
|
||||
return $q->span({-class=>'gotobar bar'}, (map { GetPageLink($_) } @UserGotoBarPages), $UserGotoBar);
|
||||
}
|
||||
|
||||
# return list of summaries between two revisions, assuming the open page is the upper one
|
||||
@@ -3351,7 +3374,7 @@ 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:')) . ' '
|
||||
. $q->textfield(-name=>'match', -id=>'indexmatch', -size=>20));
|
||||
@@ -3362,8 +3385,9 @@ 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) {
|
||||
print GetHttpHeader('text/plain'); # and ignore @menu
|
||||
} else {
|
||||
@@ -3534,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 {
|
||||
@@ -3873,12 +3910,16 @@ sub TouchIndexFile {
|
||||
|
||||
sub GetLanguages {
|
||||
my $text = shift;
|
||||
my @result;
|
||||
for my $lang (sort keys %Languages) {
|
||||
my %result;
|
||||
for my $lang (keys %Languages) {
|
||||
my @matches = $text =~ /$Languages{$lang}/gi;
|
||||
push(@result, $lang) if $#matches >= $LanguageLimit;
|
||||
$result{$lang} = @matches if @matches >= $LanguageLimit;
|
||||
}
|
||||
return join(',', @result);
|
||||
return join(',', sort { $result{$b} <=> $result{$a} } keys %result);
|
||||
}
|
||||
|
||||
sub GetLanguage { # the first language, or the default language
|
||||
return ((split /,/, GetLanguages(@_))[0] or $CurrentLanguage);
|
||||
}
|
||||
|
||||
sub GetCluster {
|
||||
|
||||
Reference in New Issue
Block a user