view release on metacpan or search on metacpan
- App::Phoebe::StaticFiles is new; use it to serve static files from
directories on your server
- App::Phoebe::Ijirait is new; a MUSH, i.e. a shared world for all
participants to build and interact in; script/ijirait is also new, a
custom CLI client for the game
- App::Phoebe::Spartan is new; an alternative to Gemini and Titan
since it's very simply, suggests gemtext for text formatting, allows
uploads, but doesn't do TLS
- App::Phoebe::Iapetus is new; an alternative to the Titan protocol
for uploading
- App::Phoebe::SpeedBump loads the block list on startup; as it saves
every half an hour, that means you no longer have to worry too much
about losing information on the blocked IP numbers and networks
- App::Gopher is new and improved; it no longer just prints the Gemini
text but does line wrapping and all that
3.00
Makefile.PL view on Meta::CPAN
# requires the use of 'sudo'; the --user and --group options make sure
# that it drops priviledges to the current user as soon as it started.
web:
sudo ./script/phoebe \
--cert_file=./t/cert.pem \
--key_file=./t/key.pem \
--user=$(shell id -un) --group=$(shell id -gn) \
--port=443 --port=1965
# Start the wiki on port 2020, with debug log level, two example
# spaces, allowing the upload of JPEG files, and use 'morbo' (which is
# part of the Mojolicious Perl package) to watch for changes of the
# code: when it detects a change, the server is restarted
# automatically.
dev:
morbo --watch ./script/phoebe \
--watch ./wiki/config -- \
./script/phoebe \
--host localhost --host 127.0.0.1 --port=2020 \
--wiki_space=127.0.0.1/alex --wiki_space=localhost/berta \
--log_level=4 --wiki_main_page=Welcome \
**Table of Contents**
- [Phoebe](#phoebe)
- [Gemtext](#gemtext)
- [Editing the wiki](#editing-the-wiki)
- [Editing via the web](#editing-via-the-web)
- [Installation](#installation)
- [Dependencies](#dependencies)
- [Quickstart](#quickstart)
- [Image uploads](#image-uploads)
- [Using systemd](#using-systemd)
- [Troubleshooting](#troubleshooting)
- [Files](#files)
- [Options](#options)
- [Uploads](#uploads)
- [Notes](#notes)
- [Security](#security)
- [Privacy](#privacy)
- [Example](#example)
- [Certificates and File Permission](#certificates-and-file-permission)
- [Main Page and Title](#main-page-and-title)
- [robots.txt](#robots-txt)
- [Configuration](#configuration)
- [Wiki Spaces](#wiki-spaces)
- [Tokens per Wiki Space](#tokens-per-wiki-space)
A line starting with ">", followed by a space and some text is a quote.
The monologue at the end is fantastic, with the city lights and the rain.
> I've seen things you people wouldn't believe.
# Editing the wiki
How do you edit a Phoebe wiki? You need to use a Titan-enabled client.
Titan is a companion protocol to Gemini: it allows clients to upload files to
Gemini sites, if servers allow this. On Phoebe, you can edit "raw" pages. That
is, at the bottom of a page you'll see a link to the "raw" page. If you follow
it, you'll see the page content as plain text. You can submit a changed version
of this text to the same URL using Titan. There is more information for
developers available on Community Wiki. [https://communitywiki.org/wiki/Titan](https://communitywiki.org/wiki/Titan)
Known clients:
This repository comes with a Perl script called `titan` to upload files.
[https://alexschroeder.ch/cgit/phoebe/plain/titan](https://alexschroeder.ch/cgit/phoebe/plain/titan)
_Gemini Write_ is an extension for the Emacs Gopher and Gemini client
_Elpher_. [https://alexschroeder.ch/cgit/gemini-write/](https://alexschroeder.ch/cgit/gemini-write/)
[https://thelambdalab.xyz/elpher/](https://thelambdalab.xyz/elpher/)
Gemini & Titan for Bash are two shell functions that allow you to download and
upload files. [https://alexschroeder.ch/cgit/gemini-titan/about/](https://alexschroeder.ch/cgit/gemini-titan/about/)
## Editing via the web
The Configuration section of the Phoebe space on _The Transjovian Council_ has
an example config on how to enable editing via the web.
- [https://transjovian.org:1965/phoebe/page/Configuration](https://transjovian.org:1965/phoebe/page/Configuration)
- [gemini://transjovian.org/phoebe/page/Configuration](gemini://transjovian.org/phoebe/page/Configuration)
# Installation
perl5/bin/gemini gemini://localhost:1965/page/Welcome
You should get back a page that starts as follows:
20 text/gemini; charset=UTF-8
Welcome to the wiki!
Please be kind.
Yay! ðð ðð
If you have a bunch of Gemtext files in a directory, you can upload them all in
one go:
titan --url=titan://localhost/ --token=hello *.gmi
## Image uploads
OK, how do image uploads work? First, we need to specify which MIME types Phoebe
accepts. The files are going to be served back with that MIME type, so even if
somebody uploads an executable and claim it's an image, other people's clients
will treat it as an image instead of executing it (one hopes!) â so let's start
with a list of common MIME types.
- `image/jpeg` is for photos (usually with the `jpg` extension)
- `image/png` is for graphics (usually with the `png` extension)
- `audio/mpeg` is for sound (usually with the `mp3` extension)
Let's continue using the setup we used for the ["Quickstart"](#quickstart) section. Restart
the server and allow photos:
perl5/bin/phoebe --wiki_mime_type=image/jpeg
Upload the image using the `titan` script:
perl5/bin/titan --url=titan://localhost:1965/jupiter.jpg \
--token=hello Pictures/Planets/Juno.jpg
You should get back a redirect to the uploaded image:
30 gemini://localhost:1965/file/jupiter.jpg
How did the `titan` script know the MIME-type to use for the upload? If you
don't specify a MIME-type using `--mime`, the `file` utility is called to
guess the MIME type of the file.
Test it:
file --mime-type --brief Pictures/Planets/Juno.jpg
The result is the MIME-type we enabled for our wiki:
image/jpeg
Here's what happens when you're trying to upload an unsupported MIME-type:
titan --url=titan://localhost:1965/earth.png \
--token=hello Pictures/Planets/Earth.png
What you get back explains the problem:
59 This wiki does not allow image/png
In order to allow such graphics as well, you need to restart Phoebe:
phoebe --wiki_mime_type=image/jpeg --wiki_mime_type=image/png
Except that in my case, the image is too big:
59 This wiki does not allow more than 100000 bytes per page
I could scale it down before I upload the image, using `convert` (which is part
of ImageMagick):
convert -scale 20% Pictures/Planets/Earth.png earth-small.png
Try again:
titan --url=titan://localhost:1965/earth.png \
--token=hello earth-small.png
Alternatively, you can increase the size limit using the
`--wiki_page_size_limit` option, but you need to restart Phoebe:
phoebe --wiki_page_size_limit=10000000 \
--wiki_mime_type=image/jpeg --wiki_mime_type=image/png
Now you can upload about 10MBâ¦
## Using systemd
Systemd is going to handle daemonisation for us. There's more documentation
available online.
[https://www.freedesktop.org/software/systemd/man/systemd.service.html](https://www.freedesktop.org/software/systemd/man/systemd.service.html).
Basically, this is the template for our service:
[Unit]
(without the `.gmi` extension) per line, with lines separated from each other
by a single `\n`
`keep` is the directory with all the old revisions of pages in it â if you've
only made one change, then it won't exist; if you don't care about the older
revisions, you can delete them; assuming you have a page called `Welcome` and
edit it once, you have the current revision as `page/Welcome.gmi`, and the old
revision in `keep/Welcome/1.gmi` (the page name turns into a subdirectory and
each revision gets an apropriate number)
`file` is the directory with all the uploaded files in it â if you haven't
uploaded any files, then it won't exist; you must explicitly allow MIME types
for upload using the `--wiki_mime_type` option (see _Options_ below)
`meta` is the directory with all the meta data for uploaded files in it â there
should be a file here for every file in the `file` directory; if you create new
files in the `file` directory, you should create a matching file here; if you
have a file `file/alex.jpg` you want to create a file `meta/alex.jpg`
containing the line `content-type: image/jpeg`
`changes.log` is a file listing all the pages made to the wiki; if you make
changes to the files in the `page` or `file` directory, they aren't going to
be listed in this file and thus people will be confused by the changes you made
â your call (but in all fairness, if you're collaborating with others you
probably shouldn't do this); the format is one change per line, with lines
- `--wiki_token` is for the token that users editing pages have to
provide; the default is "hello"; you can use this option multiple times
and give different users different passwords, if you want
- `--wiki_page` is an extra page to show in the main menu; you can use
this option multiple times; this is ideal for general items like _About_
or _Contact_
- `--wiki_main_page` is the page containing your header for the main
page; that's were you would put your ASCII art header, your welcome
message, and so on, see ["Main Page and Title"](#main-page-and-title) below
- `--wiki_mime_type` is a MIME type to allow for uploads; text/plain is
always allowed and doesn't need to be listed; you can also just list the
type without a subtype, eg. `image` will allow all sorts of images (make
sure random people can't use your server to exchange images â set a
password using `--wiki_token`)
- `--wiki_page_size_limit` is the number of bytes to allow for uploads,
both for pages and for files; the default is 10000 (10kB)
- `--host` is the hostname to serve; the default is `localhost` â you
probably want to pick the name of your machine, if it is reachable from
the Internet; if you use it multiple times, each host gets its own wiki
space (see `--wiki_space` below)
- `--port` is the port to use; the default is 1965
- `--wiki_dir` is the wiki data directory to use; the default is either
the value of the `PHOEBE_DATA_DIR` environment variable, or the "./wiki"
subdirectory
- `--wiki_space` adds an extra space that acts as its own wiki; a
`cert.pem`
- `--key_file` is the private key PEM file to use; the default is
`key.pem`
- `--log_level` is the log level to use (`fatal`, `error`, `warn`,
`info`, `debug`); the default is `warn`
- `--log_file` is the log file to use; the default is undefined, which
means that STDERR is used
## Uploads
If you allow uploads of binary files, these are stored separately from the
regular pages; the wiki doesn't keep old revisions of files around. If somebody
overwrites a file, the old revision is gone.
You definitely don't want random people uploading all sorts of images, videos
and binaries to your server. Make sure you set up those [tokens](#security)
using `--wiki_token`!
# Notes
## Security
The server uses "access tokens" to check whether people are allowed to edit
files. You could also call them "passwords", if you want. They aren't associated
with a username. You set them using the `--wiki_token` option. By default, the
Send some text:
echo "Hello $USER!" | script/spartan spartan://mozz.us/echo
# Titan
**titan** \[**--help**\] **--url=**_URL_ \[**--token=**_TOKEN_\] \[**--mime=**_MIMETYPE_\]
\[**--cert\_file=**_FILE_ **--key\_file=**_FILE_\] \[_FILES_ ...\]
This is a script to upload content to a Titan-enabled site like Phoebe.
**--url=URL** specifies the Titan URL to use; this should be really similar to
the Gemini URL you used to read the page.
**--token=TOKEN** specifies the token to use; this is optional but spammers and
vandals basically ensured that any site out on the Internet needs some sort of
protection; how to get a token depends on the site you're editing.
**--mime=MIMETYPE** specifies the MIME type to send to the server. If you don't
specify a MIME type, the `file` utility is used to determine the MIME type of
the file you're uploading.
**FILES...** are the files to upload, if any; this is optional: you can also use
a pipe, or type a few words by hand (terminating it with a Ctrl-D, the end of
transmission byte).
Note that if you specify multiple files, the URL must end in a slash and all the
filenames are used as page names. So, uploading `Alex.gmi` and `Berta.gmi` to
`titan://localhost/` will create `gemini://localhost/Alex` and
`gemini://localhost/Berta`.
The following two options control the use of client certificates:
**--cert\_file=FILE** specifies an optional client certificate to use; if you
don't specify one, the default is to try to use `client-cert.pem` in the
current directory.
**--key\_file=FILE** specifies an optional client certificate key to use; if you
package App::Phoebe;
our @known_fingerprints = qw(
sha256$fce75346ccbcf0da647e887271c3d3666ef8c7b181f2a3b22e976ddc8fa38401);
use App::Phoebe::HeapDump;
Once have restarted the server, [gemini://localhost/do/heap-dump](gemini://localhost/do/heap-dump) will write a
heap dump to its wiki data directory. See [Devel::MAT::UserGuide](https://metacpan.org/pod/Devel%3A%3AMAT%3A%3AUserGuide) for more.
# App::Phoebe::Iapetus
This allows known editors to upload files and pages using the Iapetus protocol.
See [Iapetus documentation](https://codeberg.org/oppenlab/iapetus).
In order to be a known editor, you need to set `@known_fingerprints` in your
`config` file. Hereâs an example:
package App::Phoebe;
our @known_fingerprints;
@known_fingerprints = qw(
sha256$fce75346ccbcf0da647e887271c3d3666ef8c7b181f2a3b22e976ddc8fa38401
sha256$54c0b95dd56aebac1432a3665107d3aec0d4e28fef905020ed6762db49e84ee1);
`client-cert.pem`:
openssl x509 -in client-cert.pem -noout -sha256 -fingerprint \
| sed -e 's/://g' -e 's/SHA256 Fingerprint=/sha256$/' \
| tr [:upper:] [:lower:]
This should give you the fingerprint in the correct format to add to the list
above.
Make sure your main menu has a link to the login page. The login page allows
people to pick the right certificate without interrupting their uploads.
=> /login Login
# App::Phoebe::Ijirait
The ijirait are red-eyed shape shifters, and a game one plays via the Gemini
protocol, and Ijiraq is also one of the moons of Saturn.
The Ijirait game is modelled along traditional MUSH games ("multi-user shared
hallucination"), that is: players have a character in the game world; the game
lib/App/Phoebe.pm view on Meta::CPAN
my $code = shift;
my $meta = shift;
my $data = shift||"";
$stream->write("$code $meta\r\n$data");
}
sub handle_titan {
my $stream = shift;
my $data = shift;
# extra processing of the request if we didn't do that, yet
$data->{upload} ||= is_upload($stream, $data->{request}) or return;
my $size = $data->{upload}->{params}->{size};
my $actual = length($data->{buffer});
if ($actual == $size) {
$log->debug("Handle Titan request");
process_titan($stream, $data->{request}, $data->{upload}, $data->{buffer}, $size);
# do not close in case we're waiting for the lock
return;
} elsif ($actual > $size) {
$log->debug("Received more than the promised $size bytes");
result($stream, "59", "Received more than the promised $size bytes");
$stream->close_gracefully();
return;
}
$log->debug("Waiting for " . ($size - $actual) . " more bytes");
}
sub process_titan {
my ($stream, $request, $upload, $buffer, $size) = @_;
eval {
local $SIG{'ALRM'} = sub { $log->error("Timeout processing upload $request") };
alarm(10); # timeout
save_page($stream, $upload->{host}, $upload->{space}, $upload->{id},
$upload->{params}->{mime}, $buffer, $size);
alarm(0);
};
return unless $@;
$log->error("Error: $@");
$stream->close_gracefully();
}
sub save_page {
my $stream = shift;
my $host = shift;
lib/App/Phoebe.pm view on Meta::CPAN
}
} elsif (@{$server->{wiki_space}}) {
$spaces{"$scheme://$host:$port/"} = "Main space";
for (sort @{$server->{wiki_space}}) {
$spaces{"$scheme://$host:$port/$_/"} = $_;
}
}
return \%spaces;
}
sub is_upload {
my $stream = shift;
my $request = shift;
$log->info("Looking at $request");
my $hosts = host_regex();
my $spaces_regex = space_regex();
my $port = port($stream);
if ($request =~ m!^titan://($hosts)(?::$port)?!) {
my $host = $1;
my($scheme, $authority, $path, $query, $fragment) =
$request =~ m|(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?|;
lib/App/Phoebe/Iapetus.pm view on Meta::CPAN
# 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
App::Phoebe::Iapetus - uploads using the Iapetus protocol
=head1 DESCRIPTION
This allows known editors to upload files and pages using the Iapetus protocol.
See L<Iapetus documentation|https://codeberg.org/oppenlab/iapetus>.
In order to be a known editor, you need to set C<@known_fingerprints> in your
F<config> file. Hereâs an example:
package App::Phoebe;
our @known_fingerprints;
@known_fingerprints = qw(
sha256$fce75346ccbcf0da647e887271c3d3666ef8c7b181f2a3b22e976ddc8fa38401
sha256$54c0b95dd56aebac1432a3665107d3aec0d4e28fef905020ed6762db49e84ee1);
lib/App/Phoebe/Iapetus.pm view on Meta::CPAN
F<client-cert.pem>:
openssl x509 -in client-cert.pem -noout -sha256 -fingerprint \
| sed -e 's/://g' -e 's/SHA256 Fingerprint=/sha256$/' \
| tr [:upper:] [:lower:]
This should give you the fingerprint in the correct format to add to the list
above.
Make sure your main menu has a link to the login page. The login page allows
people to pick the right certificate without interrupting their uploads.
=> /login Login
=cut
package App::Phoebe::Iapetus;
use App::Phoebe qw($server $log @request_handlers @extensions host_regex space_regex space port result
valid_id valid_mime_type valid_size @known_fingerprints process_titan);
use Modern::Perl;
use File::MimeInfo qw(globs);
use Encode qw(decode_utf8);
use URI::Escape;
push(@{$server->{wiki_mime_type}},'text/gemini');
unshift(@request_handlers, '^iapetus://' => \&handle_iapetus);
sub handle_iapetus {
my $stream = shift;
my $data = shift;
# extra processing of the request if we didn't do that, yet
return setup_iapetus($stream, $data) unless $data->{upload};
my $size = $data->{upload}->{params}->{size};
my $actual = length($data->{buffer});
if ($actual == $size) {
$log->debug("Handle Iapetus request as Titan request");
process_titan($stream, $data->{request}, $data->{upload}, $data->{buffer}, $size);
# do not close in case we're waiting for the lock
return;
} elsif ($actual > $size) {
$log->debug("Received more than the promised $size bytes");
result($stream, "59", "Received more than the promised $size bytes");
$stream->close_gracefully();
return;
}
$log->debug("Waiting for " . ($size - $actual) . " more bytes");
}
lib/App/Phoebe/Iapetus.pm view on Meta::CPAN
my($scheme, $authority, $path, $query, $fragment, $size) =
$request =~ m|(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(\S*))?\s+(\d+)|;
if ($path =~ m!^(?:/($spaces_regex))?(?:/raw)?/([^/;=&]+)!) {
my ($space, $id) = ($1, $2);
return unless valid_id($stream, $host, $space, $id);
my $type = globs($id) || mime_type($id);
my $params = { size => $size, mime => $type };
return unless valid_mime_type($stream, $host, $space, $id, $params);
return unless valid_size($stream, $host, $space, $id, $params);
return unless valid_client_cert($stream, $host, $space, $id, $params);
$data->{upload} = {
host => $host,
space => space($stream, $host, $space),
id => decode_utf8(uri_unescape($id)),
params => $params,
};
result($stream, "10", "Continue"); # weird!
return 1;
} else {
$log->debug("The path $path is malformed");
result($stream, "59", "The path $path is malformed");
script/phoebe view on Meta::CPAN
A line starting with ">", followed by a space and some text is a quote.
The monologue at the end is fantastic, with the city lights and the rain.
> I've seen things you people wouldn't believe.
=head1 EDITING THE WIKI
How do you edit a Phoebe wiki? You need to use a Titan-enabled client.
Titan is a companion protocol to Gemini: it allows clients to upload files to
Gemini sites, if servers allow this. On Phoebe, you can edit "raw" pages. That
is, at the bottom of a page you'll see a link to the "raw" page. If you follow
it, you'll see the page content as plain text. You can submit a changed version
of this text to the same URL using Titan. There is more information for
developers available on Community Wiki. L<https://communitywiki.org/wiki/Titan>
Known clients:
This repository comes with a Perl script called F<titan> to upload files.
L<https://alexschroeder.ch/cgit/phoebe/plain/titan>
I<Gemini Write> is an extension for the Emacs Gopher and Gemini client
I<Elpher>. L<https://alexschroeder.ch/cgit/gemini-write/>
L<https://thelambdalab.xyz/elpher/>
Gemini & Titan for Bash are two shell functions that allow you to download and
upload files. L<https://alexschroeder.ch/cgit/gemini-titan/about/>
=head2 Editing via the web
The Configuration section of the Phoebe space on I<The Transjovian Council> has
an example config on how to enable editing via the web.
=over
=item * L<https://transjovian.org:1965/phoebe/page/Configuration>
script/phoebe view on Meta::CPAN
perl5/bin/gemini gemini://localhost:1965/page/Welcome
You should get back a page that starts as follows:
20 text/gemini; charset=UTF-8
Welcome to the wiki!
Please be kind.
Yay! ðð ðð
If you have a bunch of Gemtext files in a directory, you can upload them all in
one go:
titan --url=titan://localhost/ --token=hello *.gmi
=head2 Image uploads
OK, how do image uploads work? First, we need to specify which MIME types Phoebe
accepts. The files are going to be served back with that MIME type, so even if
somebody uploads an executable and claim it's an image, other people's clients
will treat it as an image instead of executing it (one hopes!) â so let's start
with a list of common MIME types.
=over
=item * C<image/jpeg> is for photos (usually with the C<jpg> extension)
=item * C<image/png> is for graphics (usually with the C<png> extension)
=item * C<audio/mpeg> is for sound (usually with the C<mp3> extension)
script/phoebe view on Meta::CPAN
Let's continue using the setup we used for the L</Quickstart> section. Restart
the server and allow photos:
perl5/bin/phoebe --wiki_mime_type=image/jpeg
Upload the image using the C<titan> script:
perl5/bin/titan --url=titan://localhost:1965/jupiter.jpg \
--token=hello Pictures/Planets/Juno.jpg
You should get back a redirect to the uploaded image:
30 gemini://localhost:1965/file/jupiter.jpg
How did the C<titan> script know the MIME-type to use for the upload? If you
don't specify a MIME-type using C<--mime>, the C<file> utility is called to
guess the MIME type of the file.
Test it:
file --mime-type --brief Pictures/Planets/Juno.jpg
The result is the MIME-type we enabled for our wiki:
image/jpeg
Here's what happens when you're trying to upload an unsupported MIME-type:
titan --url=titan://localhost:1965/earth.png \
--token=hello Pictures/Planets/Earth.png
What you get back explains the problem:
59 This wiki does not allow image/png
In order to allow such graphics as well, you need to restart Phoebe:
phoebe --wiki_mime_type=image/jpeg --wiki_mime_type=image/png
Except that in my case, the image is too big:
59 This wiki does not allow more than 100000 bytes per page
I could scale it down before I upload the image, using C<convert> (which is part
of ImageMagick):
convert -scale 20% Pictures/Planets/Earth.png earth-small.png
Try again:
titan --url=titan://localhost:1965/earth.png \
--token=hello earth-small.png
Alternatively, you can increase the size limit using the
C<--wiki_page_size_limit> option, but you need to restart Phoebe:
phoebe --wiki_page_size_limit=10000000 \
--wiki_mime_type=image/jpeg --wiki_mime_type=image/png
Now you can upload about 10MBâ¦
=head2 Using systemd
Systemd is going to handle daemonisation for us. There's more documentation
available online.
L<https://www.freedesktop.org/software/systemd/man/systemd.service.html>.
Basically, this is the template for our service:
[Unit]
script/phoebe view on Meta::CPAN
(without the C<.gmi> extension) per line, with lines separated from each other
by a single C<\n>
F<keep> is the directory with all the old revisions of pages in it â if you've
only made one change, then it won't exist; if you don't care about the older
revisions, you can delete them; assuming you have a page called C<Welcome> and
edit it once, you have the current revision as F<page/Welcome.gmi>, and the old
revision in F<keep/Welcome/1.gmi> (the page name turns into a subdirectory and
each revision gets an apropriate number)
F<file> is the directory with all the uploaded files in it â if you haven't
uploaded any files, then it won't exist; you must explicitly allow MIME types
for upload using the C<--wiki_mime_type> option (see I<Options> below)
F<meta> is the directory with all the meta data for uploaded files in it â there
should be a file here for every file in the F<file> directory; if you create new
files in the F<file> directory, you should create a matching file here; if you
have a file F<file/alex.jpg> you want to create a file F<meta/alex.jpg>
containing the line C<content-type: image/jpeg>
F<changes.log> is a file listing all the pages made to the wiki; if you make
changes to the files in the F<page> or F<file> directory, they aren't going to
be listed in this file and thus people will be confused by the changes you made
â your call (but in all fairness, if you're collaborating with others you
probably shouldn't do this); the format is one change per line, with lines
script/phoebe view on Meta::CPAN
and give different users different passwords, if you want
=item * C<--wiki_page> is an extra page to show in the main menu; you can use
this option multiple times; this is ideal for general items like I<About>
or I<Contact>
=item * C<--wiki_main_page> is the page containing your header for the main
page; that's were you would put your ASCII art header, your welcome
message, and so on, see L</Main Page and Title> below
=item * C<--wiki_mime_type> is a MIME type to allow for uploads; text/plain is
always allowed and doesn't need to be listed; you can also just list the
type without a subtype, eg. C<image> will allow all sorts of images (make
sure random people can't use your server to exchange images â set a
password using C<--wiki_token>)
=item * C<--wiki_page_size_limit> is the number of bytes to allow for uploads,
both for pages and for files; the default is 10000 (10kB)
=item * C<--host> is the hostname to serve; the default is C<localhost> â you
probably want to pick the name of your machine, if it is reachable from
the Internet; if you use it multiple times, each host gets its own wiki
space (see C<--wiki_space> below)
=item * C<--port> is the port to use; the default is 1965
=item * C<--wiki_dir> is the wiki data directory to use; the default is either
script/phoebe view on Meta::CPAN
=item * C<--log_level> is the log level to use (C<fatal>, C<error>, C<warn>,
C<info>, C<debug>); the default is C<warn>
=item * C<--log_file> is the log file to use; the default is undefined, which
means that STDERR is used
=back
=head2 UPLOADS
If you allow uploads of binary files, these are stored separately from the
regular pages; the wiki doesn't keep old revisions of files around. If somebody
overwrites a file, the old revision is gone.
You definitely don't want random people uploading all sorts of images, videos
and binaries to your server. Make sure you set up those L<tokens|/Security>
using C<--wiki_token>!
=head1 NOTES
=head2 Security
The server uses "access tokens" to check whether people are allowed to edit
files. You could also call them "passwords", if you want. They aren't associated
with a username. You set them using the C<--wiki_token> option. By default, the
script/titan view on Meta::CPAN
# 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
Titan - a text and file uploader for using the Titan protocol
=head1 SYNOPSIS
B<titan> [B<--help>] B<--url=>I<URL> [B<--token=>I<TOKEN>] [B<--mime=>I<MIMETYPE>]
[B<--cert_file=>I<FILE> B<--key_file=>I<FILE>] [I<FILES> ...]
=head1 DESCRIPTION
This is a script to upload content to a Titan-enabled site like Phoebe.
B<--url=URL> specifies the Titan URL to use; this should be really similar to
the Gemini URL you used to read the page.
B<--token=TOKEN> specifies the token to use; this is optional but spammers and
vandals basically ensured that any site out on the Internet needs some sort of
protection; how to get a token depends on the site you're editing.
B<--mime=MIMETYPE> specifies the MIME type to send to the server. If you don't
specify a MIME type, the C<file> utility is used to determine the MIME type of
the file you're uploading.
B<FILES...> are the files to upload, if any; this is optional: you can also use
a pipe, or type a few words by hand (terminating it with a Ctrl-D, the end of
transmission byte).
Note that if you specify multiple files, the URL must end in a slash and all the
filenames are used as page names. So, uploading F<Alex.gmi> and F<Berta.gmi> to
C<titan://localhost/> will create C<gemini://localhost/Alex> and
C<gemini://localhost/Berta>.
The following two options control the use of client certificates:
B<--cert_file=FILE> specifies an optional client certificate to use; if you
don't specify one, the default is to try to use F<client-cert.pem> in the
current directory.
B<--key_file=FILE> specifies an optional client certificate key to use; if you
t/Iapetus.t view on Meta::CPAN
# variables set by test.pl
our $host;
our $base;
our $port;
# test page
my $page = query_gemini("$base/Haiku");
like($page, qr/^51 Path not found/m, "Test page does not exist");
# upload text
sub iapetus {
my $request = shift;
my $data = shift;
my $socket = IO::Socket::SSL->new(
PeerHost => $host, PeerPort => $port,
# don't verify the server certificate
SSL_verify_mode => SSL_VERIFY_NONE,
SSL_cert_file => 't/cert.pem',
SSL_key_file => 't/key.pem', );
$socket->print($request);
# main menu
$page = query_gemini("$base/");
unlike($page, qr/^=> .*\/$/m, "No empty links in the menu");
# --wiki_page
for my $item(qw(Alex Berta Chris)) {
like($page, qr/^=> $base\/page\/$item $item/m, "main menu contains $item");
}
# 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;token=hello", $haiku);
like($page, qr/^30 $base\/page\/Haiku\r$/, "Titan Haiku");
$page = query_gemini("$base/page/Haiku");
like($page, qr/^20 text\/gemini; charset=UTF-8\r\n# Haiku\n$haiku/, "Haiku saved");
# plain text
$page = query_gemini("$base\/raw\/Haiku");
like($page, qr/$haiku/m, "Raw text");
# upload image
my $data = read_binary("t/alex.jpg");
my $size = length($data);
$page = query_gemini("$titan/raw/Alex;size=$size;mime=image/png;token=hello", $data);
like($page, qr/^59 This wiki does not allow image\/png/, "Upload image with wrong MIME type");
$page = query_gemini("$base/page/Alex");
like($page, qr/This page does not yet exist/, "Save of unsupported MIME type failed");
$page = query_gemini("$titan/raw/Alex;size=$size;mime=image/jpeg;token=hello", $data);
like($page, qr/^30 $base\/file\/Alex\r/, "Upload image");
t/encoding.t view on Meta::CPAN
use utf8; # tests contain UTF-8 characters and it matters
my $idn = "æ±äº¬.jp";
our @hosts = ("localhost", $idn);
our $port;
our $base;
our $dir;
require './t/test.pl';
# upload text
my $titan = "titan://$hosts[0]:$port";
my $name = "æ¥æ¬èª";
my $encoded_name = uri_escape_utf8($name);
my $text = <<EOT;
Schröder answered: ï½¢éµä¾¿å±ï½£
EOT
my $encoded_text = encode_utf8($text);
my $length = length($encoded_text);
t/oddmuse-wiki.pl view on Meta::CPAN
$text =~ s/\r\n/\n/g; # DOS to Unix
$text =~ s/\n+$//g; # No trailing paragraphs
return if $text eq ''; # allow the text '0'
local $Fragment = ''; # the clean HTML fragment not yet on @Blocks
local @Blocks = (); # the list of cached HTML blocks
local @Flags = (); # a list for each block, 1 = dirty, 0 = clean
Clean(join('', map { AddHtmlEnvironment($_) } @tags));
if ($OpenPageName and $PlainTextPages{$OpenPageName}) { # there should be no $PlainTextPages{''}
Clean(CloseHtmlEnvironments() . $q->pre($text));
} elsif (my ($type) = TextIsFile($text)) { # TODO? $type defined here??
Clean(CloseHtmlEnvironments() . $q->p(T('This page contains an uploaded file:'))
. $q->p(GetDownloadLink($OpenPageName, (substr($type, 0, 6) eq 'image/'), $revision))
. (length $Page{summary} > 0 ? $q->blockquote(QuoteHtml($Page{summary})) : $q->p(T('No summary was provided for this file.'))));
} else {
my $smileyregex = join "|", keys %Smilies;
$smileyregex = qr/(?=$smileyregex)/;
local $_ = $text;
local $bol = 1;
while (1) {
# Block level elements should eat trailing empty lines to prevent empty p elements.
if ($bol and m/\G(\s*\n)+/cg) {
t/oddmuse-wiki.pl view on Meta::CPAN
sub GetPageLink { # use if you want to force a link to local pages, whether it exists or not
my ($id, $name, $class, $accesskey) = @_;
$id = FreeToNormal($id);
$name ||= $id;
$class .= ' ' if $class;
return ScriptLink(UrlEncode($id), NormalToFree($name), $class . 'local',
undef, undef, $accesskey);
}
sub GetEditLink { # shortcut
my ($id, $name, $upload, $accesskey) = @_;
$id = FreeToNormal($id);
my $action = 'action=edit;id=' . UrlEncode($id);
$action .= ';upload=1' if $upload;
return ScriptLink($action, NormalToFree($name), 'edit', undef, T('Click to edit this page'), $accesskey);
}
sub ScriptUrl {
my $action = shift;
if ($action =~ /^($UrlProtocols)\%3a/ or $action =~ /^\%2f/) { # nearlinks and other URLs
$action =~ s/%([0-9a-f][0-9a-f])/chr(hex($1))/eg; # undo urlencode
# do nothing
} else {
$action = $ScriptName . (($UsePathInfo and index($action, '=') == -1) ? '/' : '?') . $action;
t/oddmuse-wiki.pl view on Meta::CPAN
$action = "action=download;id=" . UrlEncode($id) . ";revision=$revision";
} elsif ($UsePathInfo) {
$action = "download/" . UrlEncode($id);
} else {
$action = "action=download;id=" . UrlEncode($id);
}
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', -loading=>'lazy'});
$result = ScriptLink(UrlEncode($id), $result, 'image') unless $id eq $OpenPageName;
return $result;
} else {
return ScriptLink($action, $alt, 'upload');
}
}
sub PrintCache { # Use after OpenPage!
my @blocks = split($FS, $Page{blocks});
my @flags = split($FS, $Page{flags});
$FootnoteNumber = 0;
foreach my $block (@blocks) {
if (shift(@flags)) {
ApplyRules($block, 1, 1); # local links, anchors, current revision, no start tag
t/oddmuse-wiki.pl view on Meta::CPAN
sub UnWiki {
my $str = shift;
return $str unless $WikiLinks and $str =~ /^$LinkPattern$/;
$str =~ s/([[:lower:]])([[:upper:]])/$1 $2/g;
return $str;
}
sub DoEdit {
my ($id, $newText, $preview) = @_;
UserCanEditOrDie($id);
my $upload = GetParam('upload', undef);
if ($upload and not $UploadAllowed and not UserIsAdmin()) {
ReportError(T('Only administrators can upload files.'), '403 FORBIDDEN');
}
OpenPage($id);
my ($revisionPage, $revision) = GetTextRevision(GetParam('revision', ''), 1); # maybe revision reset!
my $oldText = $preview ? $newText : $revisionPage->{text};
my $isFile = TextIsFile($oldText);
$upload //= $isFile;
if ($upload and not $UploadAllowed and not UserIsAdmin()) {
ReportError(T('Only administrators can upload files.'), '403 FORBIDDEN');
}
if ($upload) { # shortcut lots of code
$revision = '';
$preview = 0;
} elsif ($isFile) {
$oldText = '';
}
my $header;
if ($revision and not $upload) {
$header = Ts('Editing revision %s of', $revision) . ' ' . NormalToFree($id);
} else {
$header = Ts('Editing %s', NormalToFree($id));
}
print GetHeader('', $header), $q->start_div({-class=>'content edit'});
if ($preview and not $upload) {
print $q->start_div({-class=>'preview'});
print $q->h2(T('Preview:'));
PrintWikiToHTML($oldText); # no caching, current revision, unlocked
print $q->hr(), $q->h2(T('Preview only, not yet saved')), $q->end_div();
}
if ($revision) {
print $q->strong(Ts('Editing old revision %s.', $revision) . ' '
. T('Saving this page will replace the latest revision with this text.'))
}
print GetEditForm($id, $upload, $oldText, $revision), $q->end_div();
PrintFooter($id, 'edit');
}
sub GetEditForm {
my ($page_name, $upload, $oldText, $revision) = @_;
my $html = GetFormStart(undef, undef, $upload ? 'edit upload' : 'edit text') # protected by questionasker
.$q->p(GetHiddenValue("title", $page_name),
($revision ? GetHiddenValue('revision', $revision) : ''),
GetHiddenValue('oldtime', GetParam('oldtime', $Page{ts})), # prefer parameter over actual timestamp
($upload ? GetUpload() : GetTextArea('text', $oldText)));
my $summary = UnquoteHtml(GetParam('summary', ''))
|| ($Now - $Page{ts} < ($SummaryHours * 3600) ? $Page{summary} : '');
$html .= $q->p(T('Summary:').$q->br().GetTextArea('summary', $summary, 2))
.$q->p($q->checkbox(-name=>'recent_edit', -checked=>(GetParam('recent_edit', '') eq 'on'),
-label=>T('This change is a minor edit.')));
$html .= T($EditNote) if $EditNote; # Allow translation
my $username = GetParam('username', '');
$html .= $q->p($q->label({-for=>'username'}, T('Username:')).' '
.$q->textfield(-name=>'username', -id=>'username', -default=>$username,
-override=>1, -size=>20, -maxlength=>50))
.$q->p($q->submit(-name=>'Save', -accesskey=>T('s'), -value=>T('Save')),
($upload ? '' : ' ' . $q->submit(-name=>'Preview', -accesskey=>T('p'), -value=>T('Preview'))).
' '.$q->submit(-name=>'Cancel', -value=>T('Cancel')));
if ($upload) {
$html .= $q->p(ScriptLink('action=edit;upload=0;id=' . UrlEncode($page_name), T('Replace this file with text'), 'upload'));
} elsif ($UploadAllowed or UserIsAdmin()) {
$html .= $q->p(ScriptLink('action=edit;upload=1;id=' . UrlEncode($page_name), T('Replace this text with a file'), 'upload'));
}
$html .= $q->end_form();
foreach my $sub (@MyFormChanges) {
$html = $sub->($html, 'edit', $upload);
}
return $html;
}
sub GetTextArea {
my ($name, $text, $rows) = @_;
return $q->textarea(-id=>$name, -name=>$name, -default=>$text, -rows=>$rows || 25, -columns=>78, -override=>1);
}
sub GetUpload {
return T('File to upload:') . ' ' . $q->filefield(-name=>'file', -size=>50, -maxlength=>100);
}
sub DoDownload {
my $id = shift;
OpenPage($id) if ValidIdOrDie($id);
print $q->header(-status=>'304 NOT MODIFIED') and return if FileFresh(); # FileFresh needs an OpenPage!
my ($revisionPage, $revision) = GetTextRevision(GetParam('revision', '')); # maybe revision reset!
my $text = $revisionPage->{text};
if (my ($type, $encoding) = TextIsFile($text)) {
my ($data) = $text =~ /^[^\n]*\n(.*)/s;
t/oddmuse-wiki.pl view on Meta::CPAN
return TextIsFile(substr($_, 6)); # pass "#FILE image/png\n" to the test
}
}
sub SearchTitleAndBody {
my ($regex, $func, @args) = @_;
my @found;
my $lang = GetParam('lang', '');
foreach my $id (Filtered($regex, AllPagesList())) {
my $name = NormalToFree($id);
my ($text) = PageIsUploadedFile($id); # set to mime-type if this is an uploaded file
local ($OpenPageName, %Page); # this is local!
if (not $text) { # not uploaded file, therefore allow searching of page body
OpenPage($id); # this opens a page twice if it is not uploaded, but that's ok
if ($lang) {
my @languages = split(/,/, $Page{languages});
next if (@languages and not grep(/$lang/, @languages));
}
$text = $Page{text};
}
if (SearchString($regex, $name . "\n" . $text)) { # the real search code
push(@found, $id);
$func->($id, @args) if $func;
}
t/oddmuse-wiki.pl view on Meta::CPAN
grep /./, shift =~ /\"([^\"]+)\"|(\S+)/g; # this acts as OR
$regexp =~ s/\\s/[[:space:]]/g;
return $regexp;
}
sub PrintSearchResult {
my ($name, $regex) = @_;
return PrintPage($name) if not GetParam('context', 1);
OpenPage($name); # should be open already, just making sure!
my $text = $Page{text};
my ($type) = TextIsFile($text); # MIME type if an uploaded file
my %entry;
# get the page, filter it, remove all tags
$text =~ s/$FS//g; # Remove separators (paranoia)
$text =~ s/[\s]+/ /g; # Shrink whitespace
$text =~ s/([-_=\\*\\.]){10,}/$1$1$1$1$1/g ; # e.g. shrink "----------"
$entry{title} = $name;
$entry{description} = $type || SearchHighlight(QuoteHtml(SearchExtract($text, $regex)), QuoteHtml($regex));
$entry{size} = int((length($text) / 1024) + 1) . 'K';
$entry{'last-modified'} = TimeToText($Page{ts});
$entry{username} = $Page{username};
t/oddmuse-wiki.pl view on Meta::CPAN
sub DoPost {
my $id = FreeToNormal(shift);
UserCanEditOrDie($id);
# Lock before getting old page to prevent races
RequestLockOrError(); # fatal
OpenPage($id);
my $old = $Page{text};
my $string = UnquoteHtml(GetParam('text', undef));
$string =~ s/(\r|$FS)//g;
my ($type) = TextIsFile($string); # MIME type if an uploaded file
my $filename = GetParam('file', undef);
if (($filename or $type) and not $UploadAllowed and not UserIsAdmin()) {
ReportError(T('Only administrators can upload files.'), '403 FORBIDDEN');
}
my $comment = UnquoteHtml(GetParam('aftertext', undef));
$comment =~ s/(\r|$FS)//g;
if (defined $comment and $comment eq '') {
ReleaseLock();
return ReBrowsePage($id);
}
if ($filename) { # upload file
my $file = $q->upload('file');
if (not $file and $q->cgi_error) {
ReportError(Ts('Transfer Error: %s', $q->cgi_error), '500 INTERNAL SERVER ERROR');
}
ReportError(T('Browser reports no file info.'), '500 INTERNAL SERVER ERROR') unless $q->uploadInfo($filename);
$type = $q->uploadInfo($filename)->{'Content-Type'};
ReportError(T('Browser reports no file type.'), '415 UNSUPPORTED MEDIA TYPE') unless $type;
local $/ = undef; # Read complete files
my $content = <$file>; # Apparently we cannot count on <$file> to always work within the eval!?
my $encoding = substr($content, 0, 2) eq "\x1f\x8b" ? 'gzip' : '';
eval { require MIME::Base64; $_ = MIME::Base64::encode($content) };
$string = "#FILE $type $encoding\n" . $_;
} else { # ordinary text edit
$string = AddComment($old, $comment) if defined $comment;
if ($comment and substr($string, 0, length($DeletedPage)) eq $DeletedPage) { # look ma, no regexp!
$string = substr($string, length($DeletedPage)); # undelete pages when adding a comment
t/oddmuse-wiki.pl view on Meta::CPAN
$homepage = 'http://' . $homepage if $homepage and $homepage !~ /^($UrlProtocols):/;
$author = "[$homepage $author]" if $homepage;
$string .= "\n----\n\n" if $string and $string ne "\n";
$string .= $comment . "\n\n"
. '-- ' . $author . ' ' . TimeToText($Now) . "\n\n";
}
return $string;
}
sub Save { # call within lock, with opened page
my ($id, $new, $summary, $minor, $upload) = @_;
my $user = GetParam('username', '');
my $revision = $Page{revision} + 1;
my $old = $Page{text};
my $olddiff = $Page{'diff-major'} == '1' ? $Page{'diff-minor'} : $Page{'diff-major'};
if ($revision == 1 and IsFile($IndexFile) and not Unlink($IndexFile)) { # regenerate index on next request
SetParam('msg', Ts('Cannot delete the index file %s.', $IndexFile)
. ' ' . T('Please check the directory permissions.')
. ' ' . T('Your changes were not saved.'));
return 0;
}
ReInit($id);
TouchIndexFile();
SaveKeepFile(); # deletes blocks, flags, diff-major, and diff-minor, and sets keep-ts
ExpireKeepFiles();
$Page{lastmajor} = $revision unless $minor;
$Page{lastmajorsummary} = $summary unless $minor;
@Page{qw(ts revision summary username minor text)} =
($Now, $revision, $summary, $user, $minor, $new);
if ($UseDiff and $UseCache > 1 and $revision > 1 and not $upload and not TextIsFile($old)) {
UpdateDiffs($old, $new, $olddiff); # sets diff-major and diff-minor
}
my $languages;
$languages = GetLanguages($new) unless $upload;
$Page{languages} = $languages;
SavePage();
if ($revision == 1 and $LockOnCreation{$id}) {
WriteStringToFile(GetLockedPageFile($id), 'LockOnCreation');
}
my $host = $q->remote_addr();
WriteRcLog($id, $summary, $minor, $revision, $user, $host, $languages, GetCluster($new));
AddToIndex($id) if ($revision == 1);
}
$page = query_gemini("$base/alex");
like($page, qr/^=> $base\/alex\/page\/Alex Alex/m, "main menu contains Alex");
$page = query_gemini("$base/alex/page/Alex");
like($page, qr/^This page does not yet exist/m, "Alex page is empty in the alex space");
# redirect of reserved word
$page = query_gemini("$base/alex/do");
is($page, "31 $base/alex\r\n", "Redirect reserved word");
# upload
my $titan = "titan://$host:$port";
my $haiku = <<EOT;
Outside children shout
Everybody is running
Recess is the best
EOT
$page = query_gemini("$titan/alex/raw/Haiku;size=63;mime=text/plain;token=hello", $haiku);