14 Commits
v1.9 ... v1.10

Author SHA1 Message Date
Alex Schroeder
04e8cb3ee8 Prepare for release 2024-05-09 16:31:02 +02:00
Alex Schroeder
2be4fe503d Add preview action 2024-05-09 16:27:45 +02:00
Alex Schroeder
7a405b22b8 Fix markup in nginx man page 2024-05-09 10:40:38 +02:00
Alex Schroeder
3f6fce7165 Switch MemoryHigh and MemoryMax 2024-05-09 10:40:05 +02:00
Alex Schroeder
721d5907d8 wikiLink doesn't need the parser argument 2024-05-09 10:39:41 +02:00
Alex Schroeder
cb0dbedaed Update the chat theme
Support blockquotes as coming from other people.
2024-05-09 10:39:05 +02:00
Alex Schroeder
3ba967781e Add Unix domain socket setup for nginx 2024-04-21 12:43:29 +02:00
Alex Schroeder
e985707b51 Replace ' with ’ in the upload template 2024-04-21 12:42:22 +02:00
Alex Schroeder
a5e7dca7d8 Fixed tests for the "actual" parameter 2024-04-02 14:08:58 +02:00
Alex Schroeder
74387910d8 Append links to file uploads, directly
This adds two new functions for the upload template, Base and Title.
The upload template uses these to offer to append files just uploaded
to the suspected page they belong to.
2024-04-02 13:58:22 +02:00
Alex Schroeder
121408d6d9 Report all the links of uploaded images 2024-03-24 22:37:10 +01:00
Alex Schroeder
1b7419466a Fix regexp for file uploads
Without this fix, only the last digit gets incremented and the
filenames used is 1 .. 9, 10, 11 .. 19, 110, 111 .. 119, 1110, …
2024-03-24 22:11:38 +01:00
Alex Schroeder
8a513746d5 Links to parent pages 2024-03-17 14:17:33 +01:00
Alex Schroeder
e736802da5 Homepage theme uses the new template features
Use {{.Language}} for add.html and edit.html so that the edit area has
the language of the original text, if any.

Use {{.IsBlog}} to only add the date to an empty textarea for add.html
if the original page is a blog page.
2024-03-15 09:43:50 +01:00
28 changed files with 499 additions and 97 deletions

View File

@@ -18,6 +18,7 @@ form, textarea { width: 100%; }
Text" lang="{{.Language}}" autofocus>{{printf "%s" .Body}}</textarea>
<p><label><input type="checkbox" name="notify" checked> Add link to <a href="changes">the list of changes</a>.</label></p>
<p><input type="submit" value="Save">
<button formaction="/preview/{{.Name}}" type="submit">Preview</button>
<a href="/view/{{.Name}}"><button type="button">Cancel</button></a></p>
</form>
</body>

View File

@@ -5,13 +5,13 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-APACHE" "5" "2024-02-19"
.TH "ODDMU-APACHE" "5" "2024-04-21"
.PP
.SH NAME
.PP
oddmu-apache - how to setup Apache as a reverse proxy for Oddmu
.PP
.SS DESCRIPTION
.SH DESCRIPTION
.PP
The oddmu program serves the current working directory as a wiki on port 8080.\&
This is an unpriviledged port so an ordinary user account can do this.\&
@@ -22,7 +22,7 @@ The best way to protect the wiki against vandalism and spam is to use a regular
web server as reverse proxy.\& This page explains how to setup Apache on Debian to
do this.\&
.PP
.SS CONFIGURATION
.SH CONFIGURATION
.PP
HTTPS is not part of Oddmu.\& You probably want to configure this in your
webserver.\& I guess you could use stunnel, too.\& If you'\&re using Apache, you can
@@ -144,15 +144,6 @@ Instead of having Oddmu listen on a TCP port, you can have it listen on a
Unix-domain socket.\& This requires socket activation.\& An example of configuring
the service is given in \fIoddmu.\&service(5)\fR.\&
.PP
To test just the unix domain socket, use \fIncat(1)\fR:
.PP
.nf
.RS 4
echo -e "GET /view/index HTTP/1\&.1rnHost: localhostrnrn"
| ncat --unixsock /run/oddmu/oddmu\&.sock
.fi
.RE
.PP
On the Apache side, you can proxy to the socket directly.\& This sends all
requests to the socket:
.PP

View File

@@ -4,7 +4,7 @@ ODDMU-APACHE(5)
oddmu-apache - how to setup Apache as a reverse proxy for Oddmu
## DESCRIPTION
# DESCRIPTION
The oddmu program serves the current working directory as a wiki on port 8080.
This is an unpriviledged port so an ordinary user account can do this.
@@ -15,7 +15,7 @@ The best way to protect the wiki against vandalism and spam is to use a regular
web server as reverse proxy. This page explains how to setup Apache on Debian to
do this.
## CONFIGURATION
# CONFIGURATION
HTTPS is not part of Oddmu. You probably want to configure this in your
webserver. I guess you could use stunnel, too. If you're using Apache, you can
@@ -123,13 +123,6 @@ Instead of having Oddmu listen on a TCP port, you can have it listen on a
Unix-domain socket. This requires socket activation. An example of configuring
the service is given in _oddmu.service(5)_.
To test just the unix domain socket, use _ncat(1)_:
```
echo -e "GET /view/index HTTP/1.1\r\nHost: localhost\r\n\r\n" \
| ncat --unixsock /run/oddmu/oddmu.sock
```
On the Apache side, you can proxy to the socket directly. This sends all
requests to the socket:

View File

@@ -5,13 +5,13 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-NGINX" "5" "2024-02-19"
.TH "ODDMU-NGINX" "5" "2024-05-09"
.PP
.SH NAME
.PP
oddmu-nginx - how to setup Nginx as a reverse proxy for Oddmu
.PP
.SS DESCRIPTION
.SH DESCRIPTION
.PP
The oddmu program serves the current working directory as a wiki on port 8080.\&
This is an unpriviledged port so an ordinary user account can do this.\&
@@ -20,7 +20,7 @@ This page explains how to setup NGINX on Debian to act as a reverse proxy for
Oddmu.\& Once this is done, you can use NGINX to provide HTTPS, request users to
authenticate themselves, and so on.\&
.PP
.SS CONFIGURATION
.SH CONFIGURATION
.PP
The site is defined in "/etc/nginx/sites-available/default", in the \fIserver\fR
section.\& Add a new \fIlocation\fR section after the existing \fIlocation\fR section:
@@ -38,6 +38,12 @@ get passed on to Oddmu.\& They are essentially disabled.\& Somebody on the same
machine pointing their browser at http://localhost:8080/ directly would still
have access to all the actions, of course.\&
.PP
.SS Access
.PP
Access control is not part of Oddmu.\& By default, the wiki is editable by all.\&
This is most likely not what you want unless you'\&re running it stand-alone,
unconnected to the Internet a personal memex on your laptop, for example.\&
.PP
To restrict access to some actions, use two different \fIlocation\fR sections:
.PP
.nf
@@ -73,6 +79,49 @@ alex:$1$DOwphABk$W4VmR9p8t2\&.htxF6ctXHX\&.
.fi
.RE
.PP
These instructions create user accounts with passwords just for Oddmu.\&
These users are not real users on the web server and don'\&t have access to a
shell, mail, or any other service.\&
.PP
.SS Using a Unix-domain Socket
.PP
Instead of having Oddmu listen on a TCP port, you can have it listen on a
Unix-domain socket.\& This requires socket activation.\& An example of configuring
the service is given in \fIoddmu.\&service\fR(5).\&
.PP
On the nginx side, you can proxy to the socket using an \fIupstream\fR section.\& This
sends all requests to the socket.\& Use the upstream name as the server name for
\fIproxy_pass\fR.\& Add something like the configuration below to your existing nginx
server configuration.\& On a Debian system, that'\&d be in
"/etc/nginx/sites-available/default".\&
.PP
.nf
.RS 4
location ~ ^/(view|diff|edit|save|add|append|upload|drop|search|archive)/ {
proxy_pass http://unix:/run/oddmu/oddmu\&.sock:;
}
.fi
.RE
.PP
Reload the configuration:
.PP
.nf
.RS 4
sudo systemd reload nginx
.fi
.RE
.PP
Now, all traffic between the web server and the wiki goes over the socket at
"/run/oddmu/oddmu.\&sock".\&
.PP
To test it on the command-line, use a tool like \fIcurl(1)\fR.\&
.PP
.nf
.RS 4
curl http://localhost/view/index
.fi
.RE
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIoddmu-apache\fR(5)

View File

@@ -4,7 +4,7 @@ ODDMU-NGINX(5)
oddmu-nginx - how to setup Nginx as a reverse proxy for Oddmu
## DESCRIPTION
# DESCRIPTION
The oddmu program serves the current working directory as a wiki on port 8080.
This is an unpriviledged port so an ordinary user account can do this.
@@ -13,7 +13,7 @@ This page explains how to setup NGINX on Debian to act as a reverse proxy for
Oddmu. Once this is done, you can use NGINX to provide HTTPS, request users to
authenticate themselves, and so on.
## CONFIGURATION
# CONFIGURATION
The site is defined in "/etc/nginx/sites-available/default", in the _server_
section. Add a new _location_ section after the existing _location_ section:
@@ -29,6 +29,12 @@ get passed on to Oddmu. They are essentially disabled. Somebody on the same
machine pointing their browser at http://localhost:8080/ directly would still
have access to all the actions, of course.
## Access
Access control is not part of Oddmu. By default, the wiki is editable by all.
This is most likely not what you want unless you're running it stand-alone,
unconnected to the Internet a personal memex on your laptop, for example.
To restrict access to some actions, use two different _location_ sections:
```
@@ -58,6 +64,43 @@ using this password:
alex:$1$DOwphABk$W4VmR9p8t2.htxF6ctXHX.
```
These instructions create user accounts with passwords just for Oddmu.
These users are not real users on the web server and don't have access to a
shell, mail, or any other service.
## Using a Unix-domain Socket
Instead of having Oddmu listen on a TCP port, you can have it listen on a
Unix-domain socket. This requires socket activation. An example of configuring
the service is given in _oddmu.service_(5).
On the nginx side, you can proxy to the socket using an _upstream_ section. This
sends all requests to the socket. Use the upstream name as the server name for
_proxy_pass_. Add something like the configuration below to your existing nginx
server configuration. On a Debian system, that'd be in
"/etc/nginx/sites-available/default".
```
location ~ ^/(view|diff|edit|save|add|append|upload|drop|search|archive)/ {
proxy_pass http://unix:/run/oddmu/oddmu.sock:;
}
```
Reload the configuration:
```
sudo systemd reload nginx
```
Now, all traffic between the web server and the wiki goes over the socket at
"/run/oddmu/oddmu.sock".
To test it on the command-line, use a tool like _curl(1)_.
```
curl http://localhost/view/index
```
# SEE ALSO
_oddmu_(1), _oddmu-apache_(5)

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-RELEASES" "7" "2024-03-10"
.TH "ODDMU-RELEASES" "7" "2024-05-09"
.PP
.SH NAME
.PP
@@ -15,6 +15,32 @@ oddmu-releases - what'\&s new in this releases?\&
.PP
This page lists user-visible features and template changes to consider.\&
.PP
.SS 1.10 (2024)
.PP
You can now preview edits instead of saving them.\&
.PP
.PD 0
.IP \(bu 4
a preview button was added to "edit.\&html"
.IP \(bu 4
a new "preview.\&html" was added
.PD
.PP
If you want to take advantage of this, you'\&ll need to adapt your templates
accordingly.\& The "preview.\&html" template is a mix of "view.\&html" and
"edit.\&html".\&
.PP
There is an optional change to make to copies of \fIupload.\&html\fR if you upload
multiple images at a time.\& Instead of showing just the link to the last upload,
you can now show the link (and the images or links, if you want to) to all the
files uploaded.\& Use like this, for example:
.PP
.nf
.RS 4
Links:<tt>{{range \&.Actual}}<br>![]({{\&.}}){{end}}</tt>
.fi
.RE
.PP
.SS 1.9 (2024)
.PP
There is a change to make to copies of \fIupload.\&html\fR if subdirectories are being

View File

@@ -8,6 +8,26 @@ oddmu-releases - what's new in this releases?
This page lists user-visible features and template changes to consider.
## 1.10 (2024)
You can now preview edits instead of saving them.
- a preview button was added to "edit.html"
- a new "preview.html" was added
If you want to take advantage of this, you'll need to adapt your templates
accordingly. The "preview.html" template is a mix of "view.html" and
"edit.html".
There is an optional change to make to copies of _upload.html_ if you upload
multiple images at a time. Instead of showing just the link to the last upload,
you can now show the link (and the images or links, if you want to) to all the
files uploaded. Use like this, for example:
```
Links:<tt>{{range .Actual}}<br>![]({{.}}){{end}}</tt>
```
## 1.9 (2024)
There is a change to make to copies of _upload.html_ if subdirectories are being

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-TEMPLATES" "5" "2024-03-14" "File Formats Manual"
.TH "ODDMU-TEMPLATES" "5" "2024-04-02" "File Formats Manual"
.PP
.SH NAME
.PP
@@ -85,6 +85,10 @@ date.\&
\fI{{.\&Today}}\fR is the current date, in ISO format.\& This is useful for "new page"
like links or forms (see \fBEXAMPLE\fR below).\&
.PP
\fI{{.\&Parents}}\fR is the array of links to parent pages (see \fBEXAMPLE\fR below).\& To
refer to them, you need to use a \fI{{range .\&Parents}}\fR\fI{{end}}\fR construct.\& A
link has to properties, \fI{{.\&Title}}\fR and \fI{{.\&Url}}\fR.\&
.PP
\fI{{.\&Diff}}\fR is the page diff for \fIdiff.\&html\fR.\& It is only computed on demand so
it can be used in other templates, too.\& It probably doesn'\&t make much sense to
do so, however.\&
@@ -130,7 +134,15 @@ path, percent-escaped except for the slashes.\&
.PP
\fI{{.\&Name}}\fR is the \fIfilename\fR query parameter.\&
.PP
\fI{{.\&Last}}\fR is the filename of the last image uploaded.\&
\fI{{.\&Last}}\fR is the filename of the last file uploaded.\&
.PP
\fI{{.\&Actual}}\fR is an array of filenames of all the files uploaded.\& Use {{range
Actual}} … {{.\&}} … {{end}} to loop over all the filenames.\&
.PP
\fI{{.\&Base}}\fR is the basename of the first file uploaded (without the directory,
extension and numeric part at the end), escaped for use in URLs.\&
.PP
\fI{{.\&Title}}\fR is the title of the basename, if it exists.\&
.PP
\fI{{.\&Image}}\fR is a boolean to indicate whether the last file uploaded has a file
name indicating an image or not (such as ending in \fI.\&jpg\fR).\& If so, a thumbnail
@@ -202,6 +214,16 @@ itself is a blog page.\& Useful for \fIadd.\&html\fR:
.fi
.RE
.PP
The following adds a list of links to parent directories.\& Useful for \fIview.\&html\fR:
.PP
.nf
.RS 4
<nav>
{{range \&.Parents}}/ <a href="{{\&.Url}}">{{\&.Title}}</a>{{end}}
</nav>
.fi
.RE
.PP
.SH NOTES
.PP
The templates are always used as-is, irrespective of the current directory.\&

View File

@@ -63,6 +63,10 @@ date.
_{{.Today}}_ is the current date, in ISO format. This is useful for "new page"
like links or forms (see *EXAMPLE* below).
_{{.Parents}}_ is the array of links to parent pages (see *EXAMPLE* below). To
refer to them, you need to use a _{{range .Parents}}_ … _{{end}}_ construct. A
link has to properties, _{{.Title}}_ and _{{.Url}}_.
_{{.Diff}}_ is the page diff for _diff.html_. It is only computed on demand so
it can be used in other templates, too. It probably doesn't make much sense to
do so, however.
@@ -108,7 +112,15 @@ path, percent-escaped except for the slashes.
_{{.Name}}_ is the _filename_ query parameter.
_{{.Last}}_ is the filename of the last image uploaded.
_{{.Last}}_ is the filename of the last file uploaded.
_{{.Actual}}_ is an array of filenames of all the files uploaded. Use {{range
.Actual}} … {{.}} … {{end}} to loop over all the filenames.
_{{.Base}}_ is the basename of the first file uploaded (without the directory,
extension and numeric part at the end), escaped for use in URLs.
_{{.Title}}_ is the title of the basename, if it exists.
_{{.Image}}_ is a boolean to indicate whether the last file uploaded has a file
name indicating an image or not (such as ending in _.jpg_). If so, a thumbnail
@@ -174,6 +186,14 @@ itself is a blog page. Useful for _add.html_:
autofocus required>{{- if .IsBlog}}**{{.Today}}**. {{end}}</textarea>
```
The following adds a list of links to parent directories. Useful for _view.html_:
```
<nav>
{{range .Parents}}/ <a href="{{.Url}}">{{.Title}}</a>{{end}}
</nav>
```
# NOTES
The templates are always used as-is, irrespective of the current directory.

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU" "1" "2024-03-07"
.TH "ODDMU" "1" "2024-04-21"
.PP
.SH NAME
.PP
@@ -212,13 +212,12 @@ Unix-domain socket under systemd and Apache.\&
.PP
If the machine you are running Oddmu on is accessible from the Internet, you
must secure your installation.\& The best way to do this is use a regular web
server as a reverse proxy.\&
.PP
See \fIoddmu-apache\fR(5) for an example.\&
server as a reverse proxy.\& See \fIoddmu-apache\fR(5) and \fIoddmu-nginx\fR(5) for
example configurations.\&
.PP
Oddmu assumes that all the users that can edit pages or upload files are trusted
users and therefore their content is trusted.\& Therefore, Oddmu doesn'\&t perform
HTML sanitization!\&
users and therefore their content is trusted.\& Oddmu does not perform HTML
sanitization!\&
.PP
For an extra dose of security, consider using a Unix-domain socket.\&
.PP

View File

@@ -167,13 +167,12 @@ Unix-domain socket under systemd and Apache.
If the machine you are running Oddmu on is accessible from the Internet, you
must secure your installation. The best way to do this is use a regular web
server as a reverse proxy.
See _oddmu-apache_(5) for an example.
server as a reverse proxy. See _oddmu-apache_(5) and _oddmu-nginx_(5) for
example configurations.
Oddmu assumes that all the users that can edit pages or upload files are trusted
users and therefore their content is trusted. Therefore, Oddmu doesn't perform
HTML sanitization!
users and therefore their content is trusted. Oddmu does not perform HTML
sanitization!
For an extra dose of security, consider using a Unix-domain socket.

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU.SERVICE" "5" "2024-02-17"
.TH "ODDMU.SERVICE" "5" "2024-04-21"
.PP
.SH NAME
.PP
@@ -32,13 +32,14 @@ all the templates files ending in ".\&html" from the source distribution to
If you want to keep everything in one place, copy the binary "oddmu" and the
service file "oddmu.\&service" to "/home/oddmu", too.\&
.PP
Edit the `oddmu.\&service` file.\& These are the lines you most likely have to take
Edit the "oddmu.\&service" file.\& These are the lines you most likely have to take
care of:
.PP
.nf
.RS 4
ExecStart=/home/oddmu/oddmu
WorkingDirectory=/home/oddmu
ReadWritePaths=/home/oddmu
Environment="ODDMU_PORT=8080"
Environment="ODDMU_WEBFINGER=1"
.fi
@@ -71,15 +72,6 @@ journalctl --follow --unit oddmu
.fi
.RE
.PP
For it to restart when the server reboots:
.PP
.nf
.RS 4
sudo ln -sf /home/oddmu/oddmu\&.service
/etc/systemd/system/multi-user\&.target\&.wants/
.fi
.RE
.PP
.SH Socket Activation
.PP
Alternatively, you can let systemd handle the creation of the listening socket,
@@ -91,19 +83,58 @@ tells systemd to pass the socket to the service as its standard input.\&
"Accept=no" tells systemd to pass a listening socket, rather than to try calling
Oddmu for each connection.\&
.PP
The instructions for starting and enabling the systemd service are almost
exactly the same as those in the previous section, with "oddmu.\&service" replaced
by "oddmu-unix-domain.\&service".\& You'\&ll also need to run the following:
Instead of using "oddmu.\&service", you need to use "oddmu-unix-domain.\&socket" and
"oddmu-unix-domain.\&service".\&
.PP
The unit file for the socket defines a file name.\& You probably need to create
the directory or change the file name.\&
.PP
.nf
.RS 4
sudo mkdir /run/oddmu
.fi
.RE
.PP
The unit file for the service defines where the "oddmu" is and where the data
directory is.\& These are the lines you most likely have to take care of:
.PP
.nf
.RS 4
ExecStart=/home/oddmu/oddmu
WorkingDirectory=/home/oddmu
ReadWritePaths=/home/oddmu
Environment="ODDMU_PORT=8080"
Environment="ODDMU_WEBFINGER=1"
.fi
.RE
.PP
To install, enable and start both units:
.PP
.nf
.RS 4
ln -s /home/oddmu/oddmu-unix-domain\&.socket /etc/systemd/system
ln -s /home/oddmu/oddmu-unix-domain\&.service /etc/systemd/system
systemctl enable --now oddmu-unix-domain\&.socket
systemctl enable --now oddmu-unix-domain\&.service
.fi
.RE
.PP
To test just the unix domain socket, use \fIncat(1)\fR:
.PP
.nf
.RS 4
echo -e "GET /view/index HTTP/1\&.1rnHost: localhostrnrn"
| ncat --unixsock /run/oddmu/oddmu\&.sock
.fi
.RE
.PP
Now you need to set up your web browser to use the Unix domain socket.\& See
\fIoddmu-apache\fR(5) or \fIoddmu-nginx\fR(5) for example configurations.\&
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIsystemd.\&exec\fR(5), \fIsystemd.\&socket(5), \fRcapabilities_(7)
\fIoddmu\fR(1), \fIoddmu-apache\fR(5), \fIoddmu-nginx\fR(5), \fIsystemd.\&exec\fR(5),
\fIsystemd.\&socket(5), \fRcapabilities_(7)
.PP
.SH AUTHORS
.PP

View File

@@ -23,12 +23,13 @@ all the templates files ending in ".html" from the source distribution to
If you want to keep everything in one place, copy the binary "oddmu" and the
service file "oddmu.service" to "/home/oddmu", too.
Edit the `oddmu.service` file. These are the lines you most likely have to take
Edit the "oddmu.service" file. These are the lines you most likely have to take
care of:
```
ExecStart=/home/oddmu/oddmu
WorkingDirectory=/home/oddmu
ReadWritePaths=/home/oddmu
Environment="ODDMU_PORT=8080"
Environment="ODDMU_WEBFINGER=1"
```
@@ -54,13 +55,6 @@ Follow the log:
journalctl --follow --unit oddmu
```
For it to restart when the server reboots:
```
sudo ln -sf /home/oddmu/oddmu.service \
/etc/systemd/system/multi-user.target.wants/
```
# Socket Activation
Alternatively, you can let systemd handle the creation of the listening socket,
@@ -72,17 +66,50 @@ tells systemd to pass the socket to the service as its standard input.
"Accept=no" tells systemd to pass a listening socket, rather than to try calling
Oddmu for each connection.
The instructions for starting and enabling the systemd service are almost
exactly the same as those in the previous section, with "oddmu.service" replaced
by "oddmu-unix-domain.service". You'll also need to run the following:
Instead of using "oddmu.service", you need to use "oddmu-unix-domain.socket" and
"oddmu-unix-domain.service".
The unit file for the socket defines a file name. You probably need to create
the directory or change the file name.
```
sudo mkdir /run/oddmu
```
The unit file for the service defines where the "oddmu" is and where the data
directory is. These are the lines you most likely have to take care of:
```
ExecStart=/home/oddmu/oddmu
WorkingDirectory=/home/oddmu
ReadWritePaths=/home/oddmu
Environment="ODDMU_PORT=8080"
Environment="ODDMU_WEBFINGER=1"
```
To install, enable and start both units:
```
ln -s /home/oddmu/oddmu-unix-domain.socket /etc/systemd/system
ln -s /home/oddmu/oddmu-unix-domain.service /etc/systemd/system
systemctl enable --now oddmu-unix-domain.socket
systemctl enable --now oddmu-unix-domain.service
```
To test just the unix domain socket, use _ncat(1)_:
```
echo -e "GET /view/index HTTP/1.1\r\nHost: localhost\r\n\r\n" \
| ncat --unixsock /run/oddmu/oddmu.sock
```
Now you need to set up your web browser to use the Unix domain socket. See
_oddmu-apache_(5) or _oddmu-nginx_(5) for example configurations.
# SEE ALSO
_oddmu_(1), _systemd.exec_(5), _systemd.socket(5), _capabilities_(7)
_oddmu_(1), _oddmu-apache_(5), _oddmu-nginx_(5), _systemd.exec_(5),
_systemd.socket(5), _capabilities_(7)
# AUTHORS

View File

@@ -7,8 +7,8 @@ WantedBy=multi-user.target
Type=simple
Restart=always
DynamicUser=true
MemoryMax=100M
MemoryHigh=120M
MemoryMax=120M
MemoryHigh=100M
ExecStart=/home/oddmu/oddmu
WorkingDirectory=/home/oddmu
Environment="ODDMU_PORT=8080"

47
page.go
View File

@@ -14,12 +14,10 @@ import (
"time"
)
// Page is a struct containing information about a single page. Title
// is the title extracted from the page content using titleRegexp.
// Name is the path without extension (so a path of "foo.md"
// results in the Name "foo"). Body is the Markdown content of the
// page and Html is the rendered HTML for that Markdown. Score is a
// number indicating how well the page matched for a search query.
// Page is a struct containing information about a single page. Title is the title extracted from the page content using
// titleRegexp. Name is the path without extension (so a path of "foo.md" results in the Name "foo"). Body is the
// Markdown content of the page and Html is the rendered HTML for that Markdown. Score is a number indicating how well
// the page matched for a search query.
type Page struct {
Title string
Name string
@@ -29,6 +27,15 @@ type Page struct {
Hashtags []string
}
// Link is a struct containing a title and a name. Name is the path without extension (so a path of "foo.md" results in
// the Name "foo").
type Link struct {
Title string
Url string
}
// blogRe is a regular expression that matches blog pages. If the filename of a blog page starts with an ISO date
// (YYYY-MM-DD), then it's a blog page.
var blogRe = regexp.MustCompile(`^\d\d\d\d-\d\d-\d\d`)
// santizeStrict uses bluemonday to sanitize the HTML away. No elements are allowed except for the b tag because this is
@@ -154,8 +161,8 @@ func (p *Page) Dir() string {
return d + "/"
}
// Base returns the basename of the page name: no directory and no extension. This is used to create the upload link
// in "view.html", for example.
// Base returns the basename of the page name: no directory. This is used to create the upload link in "view.html", for
// example.
func (p *Page) Base() string {
n := filepath.Base(p.Name)
if n == "." {
@@ -168,3 +175,27 @@ func (p *Page) Base() string {
func (p *Page) Today() string {
return time.Now().Format(time.DateOnly)
}
// Parents returns a Link array to parent pages, up the directory structure.
func (p *Page) Parents() []*Link {
links := make([]*Link, 0)
index.RLock()
defer index.RUnlock()
// foo/bar/baz ⇒ index, foo/index
elems := strings.Split(p.Name, "/")
if len(elems) == 1 {
return links
}
s := ""
for i := 0; i < len(elems) - 1; i++ {
name := s + "index"
title, ok := index.titles[name]
if !ok {
title = "…"
}
link := &Link{ Title: title, Url: strings.Repeat("../", len(elems) - i - 1) + "index" }
links = append(links, link)
s += elems[i] + "/"
}
return links
}

View File

@@ -40,3 +40,30 @@ Moonlight floods the aisle`)}
// But the backup still exists.
assert.FileExists(t, "testdata/dir/moon.md~")
}
func TestPageParents(t *testing.T) {
cleanup(t, "testdata/parents")
index.load()
p := &Page{Name: "testdata/parents/index", Body: []byte(`# Solar
The air dances here
Water puddles flicker and
disappear anon`)}
p.save()
p = &Page{Name: "testdata/parents/children/index", Body: []byte(`# Lunar
Behind running clouds
Shines cold light from ages past
And untouchable`)}
p.save()
p = &Page{Name: "testdata/parents/children/something/other"}
// "testdata/parents/children/something/index" is a sibling and doesn't count!
parents := p.Parents()
assert.Equal(t, "Welcome to Oddµ", parents[0].Title)
assert.Equal(t, "../../../../index", parents[0].Url)
assert.Equal(t, "…", parents[1].Title)
assert.Equal(t, "../../../index", parents[1].Url)
assert.Equal(t, "Solar", parents[2].Title)
assert.Equal(t, "../../index", parents[2].Url)
assert.Equal(t, "Lunar", parents[3].Title)
assert.Equal(t, "../index", parents[3].Url)
assert.Equal(t, 4, len(parents))
}

View File

@@ -12,7 +12,7 @@ import (
// wikiLink returns an inline parser function. This indirection is
// required because we want to call the previous definition in case
// this is not a wikiLink.
func wikiLink(p *parser.Parser, fn func(p *parser.Parser, data []byte, offset int) (int, ast.Node)) func(p *parser.Parser, data []byte, offset int) (int, ast.Node) {
func wikiLink(fn func(p *parser.Parser, data []byte, offset int) (int, ast.Node)) func(p *parser.Parser, data []byte, offset int) (int, ast.Node) {
return func(p *parser.Parser, original []byte, offset int) (int, ast.Node) {
data := original[offset:]
n := len(data)
@@ -64,7 +64,7 @@ func wikiParser() (*parser.Parser, *[]string) {
extensions := (parser.CommonExtensions | parser.AutoHeadingIDs | parser.Attributes) & ^parser.MathJax
parser := parser.NewWithExtensions(extensions)
prev := parser.RegisterInline('[', nil)
parser.RegisterInline('[', wikiLink(parser, prev))
parser.RegisterInline('[', wikiLink(prev))
fn, hashtags := hashtag()
parser.RegisterInline('#', fn)
if useWebfinger {

39
preview.html Normal file
View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="{{.Language}}">
<head>
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width">
<title>Preview: {{.Title}}</title>
<style>
html { max-width: 70ch; padding: 1ch; margin: auto; color: #111; background-color: #ffe; }
body { hyphens: auto; }
header a { margin-right: 1ch; }
form { display: inline-block; }
input#search { width: 12ch; }
button { background-color: #eee; color: inherit; border-radius: 4px; border-width: 1px; }
footer { border-top: 1px solid #888 }
img { max-width: 100%; }
</style>
</head>
<body>
<header>
<a href="#edit">Skip to edit form</a>
</header>
<main>
<h1>Previewing {{.Title}}</h1>
{{.Html}}
</main>
<hr>
<section id="edit">
<h2>Editing {{.Title}}</h2>
<form action="/save/{{.Name}}" method="POST">
<textarea name="body" rows="20" cols="80" lang="{{.Language}}" autofocus>{{printf "# %s\n\n%s" .Title .Body}}</textarea>
<p><label><input type="checkbox" name="notify" checked> Add link to <a href="changes">the list of changes</a>.</label></p>
<p><input type="submit" value="Save">
<button formaction="/preview/{{.Name}}" type="submit">Preview</button>
<a href="/view/{{.Name}}"><button type="button">Cancel</button></a></p>
</form>
</section>
</body>
</html>

View File

@@ -14,7 +14,7 @@ import (
// templateFiles are the various HTML template files used. These files must exist in the root directory for Oddmu to be
// able to generate HTML output. This always requires a template.
var templateFiles = []string{"edit.html", "add.html", "view.html",
var templateFiles = []string{"edit.html", "add.html", "view.html", "preview.html",
"diff.html", "search.html", "static.html", "upload.html", "feed.html"}
// templateStore controls access to map of parsed HTML templates. Make sure to lock and unlock as appropriate. See

View File

@@ -10,7 +10,7 @@
<body>
<h1>Adding to {{.Title}}</h1>
<form action="/append/{{.Name}}" method="POST">
<textarea name="body" rows="20" cols="80" placeholder="Text" lang="" autofocus required>**{{.Today}}**. </textarea>
<textarea name="body" rows="20" cols="80" placeholder="Text" lang="{{.Language}}" autofocus required>{{if .IsBlog}}**{{.Today}}**. {{end}}</textarea>
<p><label><input type="checkbox" name="notify" checked> Add link to <a href="/view/changes">the list of changes</a>.</label></p>
<p><input type="submit" value="Add">
<a href="/view/{{.Name}}"><button type="button">Cancel</button></a></p>

View File

@@ -12,7 +12,7 @@
<form action="/save/{{.Name}}" method="POST">
<textarea name="body" rows="20" cols="80" placeholder="# Title
Text" lang="" autofocus>{{ or .Body (printf "# %s " .Today) | printf "%s" }}</textarea>
Text" lang="{{.Language}}" autofocus>{{ or .Body (printf "# %s " .Today) | printf "%s" }}</textarea>
<p><label><input type="checkbox" name="notify" checked> Add link to <a href="/view/changes">the list of changes</a>.</label></p>
<p><input type="submit" value="Save">
<a href="/view/{{.Name}}"><button type="button">Cancel</button></a></p>

View File

@@ -4,10 +4,18 @@ A theme that focuses on appending short paragraphs to existing pages.
This theme makes it all look like chat. 😍
Type and submit. 🥳
Hm. 🤔
> Oh, and quotes are messages from other people! 😲
I think I like it! 😄
More [themes](../index)! 👀
> You are not alone. 👍
Type and submit in the textarea. 🥳
It really does feel like chat. 🍵 😌
> Check out the other [themes](../index)! 👀
No. 🙃

View File

@@ -16,7 +16,16 @@ main > *, footer { clear: both; }
main p {
float: right; text-align: right;
color: #000; background: #8fd;
padding: 3px 1ch; margin: 1pt 0; border-radius: 6px; border: 1px outset #eee; }
padding: 3px 1ch; margin: 1pt 0 1pt 5ch;
border-radius: 6px; border: 1px outset #eee; }
main blockquote {
padding: 0; margin: 0; }
main blockquote p {
float: left; text-align: left;
color: #000; background: #ccc;
padding: 3px 1ch; margin: 1pt 5ch 1pt 0;
border-radius: 6px; border: 1px outset #eee; }
p + blockquote > p, blockquote + p { margin-top: 5pt; }
footer p { margin: 0.5ch 0 0 0; }
textarea {
width: 97%; margin: 1ch 0 0 0; padding: 0 0.5ch; font: inherit;

View File

@@ -76,10 +76,16 @@ window.addEventListener('load', uploadFiles.init);
<p>Previous upload: <a href="/view/{{.Dir}}{{.Last}}">{{.Last}}</a>
{{if .Image}}
<p><img class="last" src="/view/{{.Dir}}{{.Last}}"><br>
<tt>Link: ![]({{.Last}})</tt>
Links:<tt>{{range .Actual}}<br>![]({{.}}){{end}}</tt>
{{else}}
<p>Link: <tt>[text]({{.Last}})</tt>
{{end}}
<form id="add" action="/append/{{.Dir}}{{.Base}}" method="POST">
<input type="hidden" name="body" value="{{range .Actual}}![]({{.}})
{{end}}">
<p>Append this to <a href="/view/{{.Dir}}{{.Base}}">{{.Title}}</a>?
<input type="submit" value="Add">
</form>
{{end}}
<form id="upload" action="/drop/{{.Dir}}" method="POST" enctype="multipart/form-data">
<p>When uploading a picture from a phone, its filename is going to be something like IMG_1234.JPG.
@@ -99,7 +105,7 @@ window.addEventListener('load', uploadFiles.init);
<p>Finally, pick the files or photos to upload.
Picture metadata is only removed if the pictures gets resized.
Providing a new max width is recommended for all pictures.
If you're uploading multiple files, they are all renamed using the filename above and therefore they all get the same extension so they must be of the same type.
If youre uploading multiple files, they are all renamed using the filename above and therefore they all get the same extension so they must be of the same type.
<p>To delete a file, upload an empty file.
<p><label for="file">Files to upload:</label>
<input type="file" name="file" required multiple>

View File

@@ -14,6 +14,7 @@ import (
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strconv"
@@ -28,9 +29,11 @@ type upload struct {
Image bool
MaxWidth string
Quality string
Actual []string
}
var lastRe = regexp.MustCompile(`^(.*)([0-9]+)(.*)$`)
var lastRe = regexp.MustCompile(`^(.*?)([0-9]+)([^0-9]*)$`)
var baseRe = regexp.MustCompile(`^(.*?)-[0-9]+$`)
// uploadHandler uses the "upload.html" template to enable uploads. The file is saved using the dropHandler. URL
// parameters are used to copy name, maxwidth and quality from the previous upload. If the previous name contains a
@@ -54,6 +57,7 @@ func uploadHandler(w http.ResponseWriter, r *http.Request, dir string) {
mimeType := mime.TypeByExtension(filepath.Ext(last))
data.Image = strings.HasPrefix(mimeType, "image/")
data.Name, err = next(dir, last, 1)
data.Actual = r.Form["actual"]
}
if err != nil {
log.Println(err)
@@ -129,7 +133,7 @@ func dropHandler(w http.ResponseWriter, r *http.Request, dir string) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
data.Add("maxwidth", maxwidth)
data.Set("maxwidth", maxwidth)
// determine how the file will be written
ext := strings.ToLower(filepath.Ext(filename))
switch ext {
@@ -143,7 +147,7 @@ func dropHandler(w http.ResponseWriter, r *http.Request, dir string) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
data.Add("quality", q)
data.Set("quality", q)
}
format = imaging.JPEG
default:
@@ -223,6 +227,7 @@ func dropHandler(w http.ResponseWriter, r *http.Request, dir string) {
log.Println("Delete", path)
}
}
data.Add("actual", filename)
username, _, ok := r.BasicAuth()
if ok {
log.Println("Save", path, "by", username)
@@ -234,6 +239,31 @@ func dropHandler(w http.ResponseWriter, r *http.Request, dir string) {
data.Set("last", filename) // has no slashes
http.Redirect(w, r, "/upload/"+dir+"?"+data.Encode(), http.StatusFound)
}
// Base returns a page name matching the first uploaded file: no extension and no appended number. If the name
// refers to a directory, returns "index". This is used to create the form target in "upload.html", for example.
func (u *upload) Base() string {
n := u.Name[:strings.LastIndex(u.Name, ".")]
m := baseRe.FindStringSubmatch(n)
if m != nil {
return m[1]
}
if n == "." {
return "index"
}
return n
}
// Title returns the title of the matching page, if it exists.
func (u *upload) Title() string {
index.RLock()
defer index.RUnlock()
name := path.Join(u.Dir, u.Base())
title, ok := index.titles[name]
if ok {
return title
}
return name
}
// Today returns the date, as a string, for use in templates.
func (u *upload) Today() string {

View File

@@ -31,7 +31,7 @@ func TestUpload(t *testing.T) {
err = writer.Close()
assert.NoError(t, err)
HTTPUploadAndRedirectTo(t, makeHandler(dropHandler, false), "/drop/testdata/files/",
writer.FormDataContentType(), form, "/upload/testdata/files/?last=ok.txt")
writer.FormDataContentType(), form, "/upload/testdata/files/?actual=ok.txt&last=ok.txt")
assert.Contains(t,
assert.HTTPBody(makeHandler(viewHandler, false), "GET", "/view/testdata/files/ok.txt", nil),
"Hello!")
@@ -50,7 +50,7 @@ func TestUploadPng(t *testing.T) {
png.Encode(file, img)
writer.Close()
HTTPUploadAndRedirectTo(t, makeHandler(dropHandler, false), "/drop/testdata/png/",
writer.FormDataContentType(), form, "/upload/testdata/png/?last=ok.png")
writer.FormDataContentType(), form, "/upload/testdata/png/?actual=ok.png&last=ok.png")
}
func TestUploadJpg(t *testing.T) {
@@ -66,7 +66,7 @@ func TestUploadJpg(t *testing.T) {
jpeg.Encode(file, img, &jpeg.Options{Quality: 90})
writer.Close()
HTTPUploadAndRedirectTo(t, makeHandler(dropHandler, false), "/drop/testdata/jpg/",
writer.FormDataContentType(), form, "/upload/testdata/jpg/?last=ok.jpg")
writer.FormDataContentType(), form, "/upload/testdata/jpg/?actual=ok.jpg&last=ok.jpg")
}
func TestUploadHeic(t *testing.T) {
@@ -94,7 +94,7 @@ YXQAAAApKAGvEyE1mvXho5qH3STtzcWnOxedwNIXAKNDaJNqz3uONoCHeUhi/HA=`
file.Write(img)
writer.Close()
HTTPUploadAndRedirectTo(t, makeHandler(dropHandler, false), "/drop/testdata/heic/",
writer.FormDataContentType(), form, "/upload/testdata/heic/?last=ok.jpg")
writer.FormDataContentType(), form, "/upload/testdata/heic/?actual=ok.jpg&last=ok.jpg")
}
func TestDeleteFile(t *testing.T) {
@@ -116,7 +116,7 @@ What happened just now?`), 0644))
file.Write([]byte(""))
writer.Close()
HTTPUploadAndRedirectTo(t, makeHandler(dropHandler, false), "/drop/testdata/delete/",
writer.FormDataContentType(), form, "/upload/testdata/delete/?last=nothing.txt")
writer.FormDataContentType(), form, "/upload/testdata/delete/?actual=nothing.txt&last=nothing.txt")
// check that it worked
assert.NoFileExists(t, "testdata/delete/nothing.txt")
}
@@ -255,3 +255,19 @@ func TestUploadTwoInOneAgain(t *testing.T) {
assert.FileExists(t, "testdata/zwei/image.jpg")
assert.FileExists(t, "testdata/zwei/image-1.jpg")
}
func TestUploadNext(t *testing.T) {
cleanup(t, "testdata/next")
os.MkdirAll("testdata/next", 0755)
s := []string{}
nm := "test-1.jpg"
var err error
for i := 0; i < 25; i++ {
nm, err = next("testdata/next", nm, 0)
assert.NoError(t, err)
s = append(s, nm)
os.Create("testdata/next/" + nm)
}
r := []string{ "test-1.jpg", "test-2.jpg", "test-3.jpg", "test-4.jpg", "test-5.jpg", "test-6.jpg", "test-7.jpg", "test-8.jpg", "test-9.jpg", "test-10.jpg", "test-11.jpg", "test-12.jpg", "test-13.jpg", "test-14.jpg", "test-15.jpg", "test-16.jpg", "test-17.jpg", "test-18.jpg", "test-19.jpg", "test-20.jpg", "test-21.jpg", "test-22.jpg", "test-23.jpg", "test-24.jpg", "test-25.jpg" }
assert.Equal(t, r, s)
}

14
view.go
View File

@@ -140,3 +140,17 @@ func viewHandler(w http.ResponseWriter, r *http.Request, path string) {
p.renderHtml()
renderTemplate(w, p.Dir(), "view", p)
}
// previewHandler is a bit like saveHandler and viewHandler. Instead of saving the date to a page, we create a synthetic
// Page and render it. Note that when saving, the carriage returns (\r) are removed. We need to do this as well,
// otherwise the rendered template has garbage bytes at the end. Note also that we need to remove the title from the
// page so that the preview works as intended (and much like the "view.html" template) where as the editing requires the
// page content including the header… which is why it needs to be added in the "preview.html" template. This makes me
// sad.
func previewHandler(w http.ResponseWriter, r *http.Request, path string) {
body := strings.ReplaceAll(r.FormValue("body"), "\r", "")
p := &Page{Name: path, Body: []byte(body)}
p.handleTitle(true)
p.renderHtml()
renderTemplate(w, p.Dir(), "preview", p)
}

View File

@@ -171,6 +171,7 @@ func serve() {
http.HandleFunc("/", rootHandler)
http.HandleFunc("/archive/", makeHandler(archiveHandler, true))
http.HandleFunc("/view/", makeHandler(viewHandler, false))
http.HandleFunc("/preview/", makeHandler(previewHandler, false))
http.HandleFunc("/diff/", makeHandler(diffHandler, true))
http.HandleFunc("/edit/", makeHandler(editHandler, true))
http.HandleFunc("/save/", makeHandler(saveHandler, true))