App-phoebe

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN


- 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 \

README.md  view on Meta::CPAN


**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)

README.md  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.

# 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

README.md  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

## 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]

README.md  view on Meta::CPAN

(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

README.md  view on Meta::CPAN


- `--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

README.md  view on Meta::CPAN

      `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

README.md  view on Meta::CPAN


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

README.md  view on Meta::CPAN

    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);

README.md  view on Meta::CPAN

`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);

t/basics.t  view on Meta::CPAN

# 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);
}

t/spaces.t  view on Meta::CPAN


$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);



( run in 3.353 seconds using v1.01-cache-2.11-cpan-b16cb0d3907 )