├── meta ├── data │ ├── slug.txt │ ├── post │ │ └── 0 │ │ │ └── 0 │ │ │ ├── 5.txt │ │ │ ├── 6.txt │ │ │ ├── 7.txt │ │ │ ├── 8.txt │ │ │ ├── 9.txt │ │ │ ├── 3.txt │ │ │ ├── 1.txt │ │ │ ├── 4.txt │ │ │ └── 2.txt │ └── opt.lisp ├── logo │ ├── favicon.tex │ └── Makefile └── geometry.md ├── .gitignore ├── web ├── img │ ├── favicon.ico │ └── favicon.png ├── js │ └── mathb │ │ ├── options.js │ │ └── mathb.js ├── html │ ├── error.html │ └── mathb.html └── css │ ├── view.css │ └── mathb.css ├── etc ├── logrotate ├── crontab ├── mathb.service └── nginx │ ├── http.mathb.in │ └── https.mathb.in ├── LICENSE.md ├── CHANGES.md ├── Makefile ├── README.md ├── mathb.lisp └── test.lisp /meta/data/slug.txt: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | texmf/ 2 | _site/ 3 | _live/ 4 | -------------------------------------------------------------------------------- /web/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susam/mathb/HEAD/web/img/favicon.ico -------------------------------------------------------------------------------- /web/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susam/mathb/HEAD/web/img/favicon.png -------------------------------------------------------------------------------- /etc/logrotate: -------------------------------------------------------------------------------- 1 | /opt/log/mathb/*.log { 2 | rotate 14 3 | daily 4 | missingok 5 | } 6 | -------------------------------------------------------------------------------- /meta/data/post/0/0/5.txt: -------------------------------------------------------------------------------- 1 | Date: 2012-03-25 00:00:00 +0000 2 | Title: 3 | Name: 4 | 5 | $$ e^{-i \pi} = -1. $$ 6 | -------------------------------------------------------------------------------- /meta/data/post/0/0/6.txt: -------------------------------------------------------------------------------- 1 | Date: 2012-03-25 00:00:00 +0000 2 | Title: 3 | Name: 4 | 5 | $$ e^{-i \pi} = -1. $$ 6 | -------------------------------------------------------------------------------- /meta/data/post/0/0/7.txt: -------------------------------------------------------------------------------- 1 | Date: 2012-03-25 00:00:00 +0000 2 | Title: 3 | Name: 4 | 5 | $$ e^{-i \pi} = -1. $$ 6 | -------------------------------------------------------------------------------- /meta/data/post/0/0/8.txt: -------------------------------------------------------------------------------- 1 | Date: 2012-03-25 00:00:00 +0000 2 | Title: 3 | Name: 4 | 5 | $$ e^{-i \pi} = -1. $$ 6 | -------------------------------------------------------------------------------- /meta/data/post/0/0/9.txt: -------------------------------------------------------------------------------- 1 | Date: 2012-03-25 00:00:00 +0000 2 | Title: 3 | Name: 4 | 5 | $$ e^{-i \pi} = -1. $$ 6 | -------------------------------------------------------------------------------- /web/js/mathb/options.js: -------------------------------------------------------------------------------- 1 | window.texme = { 2 | renderOnLoad: false, 3 | markdownURL: 'js/marked/marked.min.js', 4 | MathJaxURL: 'js/mathjax/es5/tex-mml-chtml.js' 5 | } 6 | -------------------------------------------------------------------------------- /meta/logo/favicon.tex: -------------------------------------------------------------------------------- 1 | \documentclass[border=3px]{standalone} 2 | \usepackage{xcolor} 3 | \pagecolor[HTML]{111111} 4 | \color[HTML]{eeeeee} 5 | \begin{document} 6 | \( \sum \) 7 | \end{document} 8 | -------------------------------------------------------------------------------- /etc/crontab: -------------------------------------------------------------------------------- 1 | 0 1 * * * (date; certbot renew -n; date; echo ::::) >> /var/log/certbot-renew.log 2>&1 2 | 0 2 * * * (date; systemctl reload nginx; date; echo ::::) >> /var/log/nginx-reload.log 2>&1 3 | 0 3 * * * (date; make -C /opt/mathb.in/ backup; date; echo ::::) >> /var/log/mathb-backup.log 2>&1 4 | -------------------------------------------------------------------------------- /meta/data/opt.lisp: -------------------------------------------------------------------------------- 1 | (:lock-down nil 2 | :read-only nil 3 | :min-title-length 0 4 | :max-title-length 120 5 | :min-name-length 0 6 | :max-name-length 120 7 | :min-code-length 1 8 | :max-code-length 10000 9 | :global-post-interval 0 10 | :client-post-interval 0 11 | :expect () 12 | :block ("berk" "naff" "xxx") 13 | :ban () 14 | :protect 0) 15 | -------------------------------------------------------------------------------- /etc/mathb.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=MathB 3 | After=network.target 4 | 5 | [Service] 6 | User=www-data 7 | WorkingDirectory=/opt/mathb.in 8 | Environment=XDG_CACHE_HOME=/opt/cache/lisp 9 | ExecStart=/usr/bin/sbcl --load /opt/quicklisp/setup.lisp --load mathb.lisp 10 | Restart=always 11 | RestartSec=5 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | 16 | # Note: ASDF (comes with Quicklisp) creates a 'common-lisp' directory 17 | # under the specified XDG_CACHE_HOME directory. 18 | -------------------------------------------------------------------------------- /web/html/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ status-code }} {{ reason-phrase }} 5 | 13 | 14 | 15 |

{{ status-code }} {{ reason-phrase }}

16 |
17 | MathB.in 18 | 19 | 20 | -------------------------------------------------------------------------------- /etc/nginx/http.mathb.in: -------------------------------------------------------------------------------- 1 | # Rate limit 2 | limit_req_zone $binary_remote_addr zone=passlimit:10m rate=4r/m; 3 | limit_req_status 429; 4 | 5 | # http://mathb.in/ 6 | server { 7 | listen 80; 8 | listen [::]:80; 9 | server_name mathb.in mathb; 10 | root /var/www/mathb.in; 11 | location ~ ^/[0-9]*$ { 12 | limit_req zone=passlimit burst=10 nodelay; 13 | proxy_pass http://127.0.0.1:4242; 14 | proxy_pass_request_headers off; 15 | proxy_set_header Accept $http_accept; 16 | proxy_set_header Content-Length $http_content_length; 17 | proxy_set_header Content-Type $http_content_type; 18 | proxy_set_header If-Modified-Since $http_if_modified_since; 19 | proxy_set_header Referer $http_referer; 20 | proxy_set_header User-Agent $http_user_agent; 21 | proxy_set_header X-Forwarded-For $remote_addr; 22 | } 23 | } 24 | 25 | # http://www.mathb.in/ => http://mathb.in/ 26 | server { 27 | listen 80; 28 | listen [::]:80; 29 | server_name www.mathb.in mathb.in; 30 | return 301 http://mathb.in$request_uri; 31 | } 32 | -------------------------------------------------------------------------------- /meta/logo/Makefile: -------------------------------------------------------------------------------- 1 | all: favicon dist clear 2 | 3 | # With border=0px in favicon.tex (tight cropping), the width of the 4 | # standalone image is 11 (density=72) or 190 (density=1300). With 5 | # further 17.4% padding on each side, we get a total image width of 6 | # 190 * 1.348 = 256. 7 | ImageOptim = /Applications/ImageOptim.app/Contents/MacOS/ImageOptim 8 | favicon: 9 | TEXMFHOME=texmf pdflatex favicon.tex 10 | convert -density 1300 favicon.pdf favicon.tmp1.png 11 | convert -density 1300 favicon.pdf -gravity center -crop 256x256+0+0 favicon.tmp2.png 12 | cp favicon.tmp2.png favicon.png 13 | if command -v "$(ImageOptim)"; then "$(ImageOptim)" favicon.png; fi 14 | convert favicon.tmp2.png favicon.ico 15 | file favicon*.png favicon.ico 16 | ls -l favicon*png favicon.ico 17 | 18 | dist: 19 | cp favicon.png favicon.ico ../../web/img/ 20 | 21 | deps: 22 | if command -v brew; then brew install --cask imageoptim; fi 23 | rm -rf texmf texmfvar ~/Library/texlive 24 | TEXMFHOME=texmf tlmgr init-usertree 25 | TEXMFHOME=texmf tlmgr --usermode install standalone 26 | 27 | clear: clean 28 | rm -f *.pdf *.png *.ico 29 | 30 | clean: 31 | rm -f *.aux *.log 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | =========== 3 | 4 | Copyright (c) 2012-2022 Susam Pal 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /web/css/view.css: -------------------------------------------------------------------------------- 1 | #view > :first-child { 2 | margin-top: 0 3 | } 4 | #view > :last-child { 5 | margin-bottom: 0 6 | } 7 | #view h1.title { 8 | margin-top: 0; 9 | text-align: center; 10 | margin-bottom: 1.5rem; 11 | } 12 | #view h2.author { 13 | margin-top: 0; 14 | text-align: center; 15 | margin-bottom: 1.5rem; 16 | } 17 | #view h1, #view h2, #view h3, #view h4, #view h5, #view h6 { 18 | margin: 1.25em 0 0.5em 0; 19 | line-height: 1.2; 20 | } 21 | #view img { 22 | max-width: 100%; 23 | } 24 | #view pre, #view code { 25 | font-family: monospace, monospace; 26 | color: #050; 27 | } 28 | #view pre { 29 | overflow: auto; 30 | } 31 | #view blockquote { 32 | border-left: medium solid #ccc; 33 | margin: 1em 0; 34 | } 35 | #view blockquote :first-child { 36 | margin-top: 0; 37 | } 38 | #view blockquote :last-child { 39 | margin-bottom: 0; 40 | } 41 | #view table { 42 | border-collapse: collapse; 43 | } 44 | #view th, #view td { 45 | border: thin solid #999; 46 | padding: 0.3em 0.4em; 47 | text-align: left; 48 | } 49 | @media (prefers-color-scheme: dark) { 50 | #view pre, #view code { 51 | color: #9c6; 52 | } 53 | #view pre, #view blockquote { 54 | background: #000; 55 | } 56 | #view blockquote { 57 | border-color: #333; 58 | } 59 | #view th, #view td { 60 | border-color: #666; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /etc/nginx/https.mathb.in: -------------------------------------------------------------------------------- 1 | # Rate limit 2 | limit_req_zone $binary_remote_addr zone=passlimit:10m rate=4r/m; 3 | limit_req_status 429; 4 | 5 | # https://mathb.in/ 6 | server { 7 | listen 443 ssl; 8 | listen [::]:443 ssl; 9 | ssl_certificate /etc/letsencrypt/live/mathb.in/fullchain.pem; 10 | ssl_certificate_key /etc/letsencrypt/live/mathb.in/privkey.pem; 11 | server_name mathb.in mathb; 12 | root /var/www/mathb.in; 13 | location ~ ^/[0-9]*$ { 14 | limit_req zone=passlimit burst=10 nodelay; 15 | proxy_pass http://127.0.0.1:4242; 16 | proxy_pass_request_headers off; 17 | proxy_set_header Accept $http_accept; 18 | proxy_set_header Content-Length $http_content_length; 19 | proxy_set_header Content-Type $http_content_type; 20 | proxy_set_header If-Modified-Since $http_if_modified_since; 21 | proxy_set_header Referer $http_referer; 22 | proxy_set_header User-Agent $http_user_agent; 23 | proxy_set_header X-Forwarded-For $remote_addr; 24 | } 25 | } 26 | 27 | # https://www.mathb.in/ => https://mathb.in/ 28 | server { 29 | listen 443 ssl; 30 | listen [::]:443 ssl; 31 | ssl_certificate /etc/letsencrypt/live/mathb.in/fullchain.pem; 32 | ssl_certificate_key /etc/letsencrypt/live/mathb.in/privkey.pem; 33 | server_name www.mathb.in mathb.in; 34 | return 301 https://mathb.in$request_uri; 35 | } 36 | 37 | # http://mathb.in/, http://www.mathb.in/ => https://mathb.in/ 38 | server { 39 | listen 80; 40 | listen [::]:80; 41 | server_name www.mathb.in mathb.in; 42 | return 301 https://mathb.in$request_uri; 43 | } 44 | -------------------------------------------------------------------------------- /meta/geometry.md: -------------------------------------------------------------------------------- 1 | User Interface Geometry 2 | ======================= 3 | 4 | Common Geometry 5 | --------------- 6 | 7 | - Header: 5rem 8 | - Line height: 3rem 9 | - Top margin: 1 rem 10 | - Bottom margin: 1rem 11 | 12 | - Form: 11rem + 8px + C 13 | - Border: 1px 14 | - Padding: 1rem 15 | - Code: C 16 | - Margin: 1rem 17 | - Title: 2rem + 2px 18 | - Margin: 1rem 19 | - Name: 2rem + 2px 20 | - Margin: 1rem 21 | - Submit: 2rem + 4px 22 | - Padding: 1rem 23 | - Border: 1px 24 | 25 | - HR: 2rem + 1px 26 | - Top margin: 1rem 27 | - Border: 1px 28 | - Bottom margin: 1rem 29 | 30 | 31 | Narrow View Geometry 32 | -------------------- 33 | 34 | - Header: 5rem 35 | 36 | - Form: 11rem + 8px + C 37 | 38 | - Margin: 1rem 39 | 40 | - Output: 7.5rem + 8px + V 41 | - View: V 42 | - Date: 1.5rem 43 | - Margin: 1rem 44 | - URL: 2rem + 4px 45 | - Margin: 1rem 46 | - Copy: 2rem + 4px 47 | 48 | - HR: 2rem + 1px 49 | 50 | - Header + Form + Margin + Output + HR = 26.5rem + 17px + C + V = 100vh 51 | - Thus V = 100vh - 26.5rem - 17px - C. 52 | - Let C = 10 rem 53 | - Thus V = 100vh - 36.5rem - 17px ~ 100vh - 38rem 54 | 55 | 56 | 57 | Wide View Geometry 58 | ------------------ 59 | 60 | - Header: 5rem 61 | 62 | - Form: 11rem + 8px + C 63 | 64 | - Output: 4.5rem + 4px + V 65 | - View: V 66 | - Date: 1.5rem 67 | - Margin: 1rem 68 | - URL/Copy: 2rem + 4px 69 | 70 | - HR: 2rem + 1px 71 | 72 | - Header + Form + HR: 18rem + 13px + C = 100vh 73 | - Thus C = 100vh - 18rem - 13px ~ 100vh - 19rem 74 | 75 | - Header + Output + H$: 11.5rem + 5px + V = 100vh 76 | - Thus V = 100vh - 11.5rem - 5px ~ 110vh - 12rem 77 | -------------------------------------------------------------------------------- /web/html/mathb.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MathB.in - Share Mathematics with LaTeX and Markdown 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |

MathB.in

22 |
23 | 24 |
25 | 26 | 27 | 28 | 29 |
30 |
31 |
32 | {{ error }} 35 |
36 |
{{ date }}
37 |
38 |
39 | 40 |
41 |
42 | 43 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /meta/data/post/0/0/3.txt: -------------------------------------------------------------------------------- 1 | Date: 2012-03-25 00:00:00 +0000 2 | Title: About MathB.in 3 | Name: 4 | 5 | [MathB.in] is a website meant for sharing snippets 6 | of mathematical text with others on the web. This 7 | is a pastebin for mathematics. The source code of 8 | MathB.in is available at 9 | . 10 | 11 | [MathB.in]: ./ 12 | 13 | 14 | Posting and Sharing 15 | ------------------- 16 | 17 | A new post can be composed by visiting the [home 18 | page](/) and writing or pasting code in the box on 19 | the left hand pane of the page. Once a post is 20 | composed and submitted, the page is saved and it 21 | becomes accessible with a new unique URL. The new 22 | page looks similar to this page and it has a 23 | unique URL of its own. The URL can be shared with 24 | anyone on the web and they will be able to visit 25 | your post. 26 | 27 | 28 | Post Format 29 | ----------- 30 | 31 | A post can be composed in a mixture of LaTeX and 32 | Markdown text. For a demonstration of how LaTeX 33 | and Markdown is rendered, see the [demo page](1). 34 | To quickly get started with posting mathematical 35 | snippets, see the [tutorial](2). 36 | 37 | 38 | Usage 39 | ----- 40 | 41 | This website is meant for creating mathematical 42 | snippets. Science and technical posts are also 43 | welcome on this website. Any other type of post 44 | should not be posted to this website. Any post 45 | that is not mathematical, scientific, or technical 46 | in nature will be removed. 47 | 48 | 49 | History 50 | ------- 51 | 52 | [MathB.in] is the oldest mathematics pastebin that 53 | is still online and serving its community of 54 | users. It isn't the first mathematics pastebin 55 | though. It's the second. The first pastebin was 56 | written by Mark A. Stratman. It was hosted at the 57 | domain *mathbin.net* until 2020. 58 | 59 | MathB.in was born on Sunday, 25 March 2012, after 60 | a single night of furious coding. This was a 61 | result of stumbling upon [math.stackexchange.com] 62 | the previous night which used MathJax to render 63 | mathematics formula on the web browser. Thanks to 64 | that chance encounter with MathJax, the rest of 65 | the Saturday night was spent in coding a new 66 | mathematics pastebin using MathJax and PHP. After 67 | coding all through the night, registering a new 68 | domain name, and setting up a website, [MathB.in] 69 | was released early Sunday morning. 70 | 71 | The current version of MathB.in no longer runs on 72 | PHP. It has been rewritten in Common Lisp since 73 | then. See the blog post [MathB.in Turns Ten] for 74 | more details about the history of MathB.in. 75 | 76 | [math.stackexchange.com]: https://math.stackexchange.com/ 77 | [MathB.in Turns Ten]: https://susam.net/blog/mathbin-turns-ten.html 78 | 79 | 80 | Software 81 | -------- 82 | 83 | MathB.in is powered by the following software: 84 | 85 | - [SBCL](https://www.sbcl.org/) 86 | - [Hunchentoot](https://edicl.github.io/hunchentoot/) 87 | - [MathB](https://github.com/susam/texme) 88 | - [TeXMe](https://github.com/susam/texme) 89 | - [MathJax](https://www.mathjax.org/) 90 | - [Marked](https://github.com/markedjs/marked) 91 | - [Nginx](https://www.nginx.com/) 92 | - [Debian GNU/Linux](https://www.debian.org/) 93 | 94 | 95 | Bug Reports and Suggestions 96 | --------------------------- 97 | 98 | To report bugs, suggest improvements, or ask 99 | questions, create a new issue at 100 | . 101 | -------------------------------------------------------------------------------- /meta/data/post/0/0/1.txt: -------------------------------------------------------------------------------- 1 | Date: 2012-03-25 00:00:00 +0000 2 | Title: MathB.in Demo 3 | Name: 4 | 5 | Binomial Theorem 6 | ---------------- 7 | 8 | $$ (x+y)^n = \sum_{k=0}^n {n \choose k} x^{n - k} y^k. $$ 9 | 10 | 11 | Exponential Function 12 | -------------------- 13 | 14 | $$ e^x = \lim_{n \to \infty} \left( 1+ \frac{x}{n} \right)^n. $$ 15 | 16 | 17 | Cauchy-Schwarz Inequality 18 | ------------------------- 19 | 20 | $$ 21 | \left( \sum_{k=1}^n a_k b_k \right)^2 \leq 22 | \left( \sum_{k=1}^n a_k^2 \right) 23 | \left( \sum_{k=1}^n b_k^2 \right). 24 | $$ 25 | 26 | 27 | Bayes' Theorem 28 | -------------- 29 | 30 | $$ P(A \mid B) = \frac{P(B \mid A) \, P(A)}{P(B)}. $$ 31 | 32 | 33 | Euler's Summation Formula 34 | ------------------------- 35 | 36 | **Theorem** (Euler's summation formula). _If $ f $ 37 | has a continuous derivative $ f' $ on the interval 38 | $ [y, x], $ where $ 0 < y < x, $ then_ 39 | \begin{align} 40 | \sum_{y < n \le x} f(n) 41 | = & \int_y^x f(t) \, dt + \int_y^x (t - [t]) f'(t) \, dt 42 | \notag \\ 43 | & + f(x)([x] - x) - f(y)([y] - y). \label{theorem} 44 | \end{align} 45 | 46 | _Proof._ Let $ m = [y] $ and $ k = [x]. $ For 47 | integers $ n $ and $ n - 1 $ in $ [y, x] $ we have 48 | \begin{align*} 49 | \int_{n - 1}^n [t] f'(t) \, dt 50 | & = \int_{n - 1}^n (n - 1) f'(t) \, dt \\ 51 | & = (n - 1) \{ f(n) - f(n - 1) \} \\ 52 | & = \{n f(n) - (n - 1) f(n - 1)\} - f(n). 53 | \end{align*} 54 | Summing from $ n = m + 2 $ to $ n = k $ we find 55 | that the first sum telescopes, hence 56 | \begin{align*} 57 | \int_{m + 1}^k [t] f'(t) \, dt 58 | & = k f(k) - (m + 1) f(m + 1) - \sum_{n = m + 2}^k f(n) \\ 59 | & = k f(k) - m f(m + 1) - \sum_{y < n \le x} f(n). 60 | \end{align*} 61 | Therefore 62 | \begin{align} 63 | \sum_{y < n \le x} f(n) 64 | & = - \int_{m + 1}^k [t] f'(t) \, dt + k f(k) - m f(m + 1) \notag \\ 65 | & = - \int_y^x [t] f'(t) \, dt + k f(x) - m f(y). \label{summation} 66 | \end{align} 67 | Integration by parts gives us 68 | $$ 69 | \int_y^x f(t) \, dt = x f(x) - y f(y) - \int_y^x t f'(t) \, dt, 70 | $$ 71 | and when this is combined with \eqref{summation} 72 | we obtain \eqref{theorem}. 73 | 74 | 75 | Hello World Program 76 | ------------------- 77 | 78 | Here is an example of `"hello, world"` program 79 | written in the C programming language: 80 | 81 | ``` 82 | #include 83 | 84 | int main() 85 | { 86 | printf("hello, world\n"); 87 | return 0; 88 | } 89 | ``` 90 | 91 | 92 | Issac Newton Quotes 93 | ------------------- 94 | 95 | Issac Newton was relatively modest about his 96 | achievements, writing in a letter to Robert Hooke 97 | in February 1676: 98 | 99 | > If I have seen further it is by standing on the 100 | shoulders of giants. 101 | 102 | In a later memoir, Newton wrote: 103 | 104 | > I do not know what I may appear to the world, 105 | > but to myself I seem to have been only like a 106 | > boy playing on the sea-shore, and diverting 107 | > myself in now and then finding a smoother pebble 108 | > or a prettier shell than ordinary, whilst the 109 | > great ocean of truth lay all undiscovered before 110 | > me. 111 | 112 | 113 | Table of Number Theory Functions 114 | -------------------------------- 115 | 116 | The following table shows information about a few 117 | important functions in number theory. 118 | 119 | | Name | Notation | First few values | Multiplicative property | 120 | |-|-|-|-| 121 | | Möbius function | $ \mu(n) $ | $ 1, -1, -1, 0, -1 $ | Multiplicative | 122 | | Euler's totient function | $ \varphi(n) $ | $ 1, 1, 2, 2, 4 $ | Multiplicative | 123 | | Mangoldt function | $ \Lambda(n) $ | $ 0, \log 2, \log 3, \log 2, \log 5 $ | Not multiplicative | 124 | | Liouville's function | $ \lambda(n) $ | $ 1, -1, -1, 1, -1 $ | Completely multiplicative | 125 | 126 | 127 | About This Demo 128 | --------------- 129 | 130 | This is a demo of a MathB.in post written in 131 | Markdown + LaTeX. 132 | -------------------------------------------------------------------------------- /meta/data/post/0/0/4.txt: -------------------------------------------------------------------------------- 1 | Date: 2012-09-20 00:00:00 +0000 2 | Title: Privacy Notice 3 | Name: 4 | 5 | [MathB.in] (this website) is a community website 6 | maintained by one or more volunteers. This 7 | privacy notice explains how this website uses the 8 | data it collects from you when you use this 9 | website. 10 | 11 | [MathB.in]: ./ 12 | 13 | 14 | ## Data Items 15 | 16 | This website collects the following data: 17 | 18 | - The text submitted by you while submitting a new 19 | post using this website. This text may contain 20 | a post title, your name, and arbitrary code 21 | written in LaTeX and Markdown format. This text 22 | is written by you, so you are in complete 23 | control of what you submit in the text. You 24 | must not submit sensitive personal data to this 25 | website. 26 | 27 | - Your Internet Protocol (IP) address used to 28 | visit this website. 29 | 30 | - HTTP referrer URL sent by your web browser or 31 | client software while connecting to this 32 | website. A web browser or a client software 33 | typically, but not necessarily, sends the URL of 34 | the web page or website where you clicked on a 35 | link to visit this website as the HTTP referrer 36 | value. If this website is visited directly 37 | without clicking on any link, then a web browser 38 | or client does not typically send any HTTP 39 | referrer data. 40 | 41 | - User agent string sent by your web browser or 42 | client software while visiting this website. A 43 | web browser or a client software typically, but 44 | not necessarily, sends the names and versions of 45 | your device, operating system, and web browser 46 | or client software as the user agent string. 47 | 48 | 49 | ## Data Collection 50 | 51 | When you visit this website, your IP address, 52 | referrer data, and user agent string are 53 | automatically recorded in web server log files. 54 | 55 | When you submit a new post to this website 56 | successfully, your text is written to a file on 57 | the web server and your text is published at a new 58 | URL that is shown to you immediately after you 59 | successfully submit the post. If the post 60 | submission fails due to errors, then your text is 61 | neither saved nor published. 62 | 63 | 64 | ## Data Usage 65 | 66 | The IP address, referrer data, and user agent 67 | string is used to monitor abuse of this website 68 | such as submitting spam posts, submitting 69 | non-mathematical posts, sending too many requests 70 | to reduce the availability of this website, etc. 71 | This data is also used to mitigate abuse of this 72 | website by blocking certain combinations of IP 73 | address, referrer data, and/or user agent from 74 | accessing this website or by limiting the number 75 | of requests such a combination may send to this 76 | website. 77 | 78 | The post text is saved in a text file for an 79 | indefinitely long period of time. The post text 80 | is published at a new URL. This post text is 81 | available publicly to anyone who visits this URL. 82 | This URL is shown to you immediately after you 83 | successfully submit a new post. You must not 84 | submit sensitive personal data to this website. 85 | 86 | 87 | ## Data Storage 88 | 89 | The IP address, referrer data, and user agent 90 | string are stored in web server log files. This 91 | data is retained for 15 days. After 15 days, this 92 | data is deleted automatically. 93 | 94 | The post text is saved in text files and made 95 | available publicly on this website via unique 96 | URLs. Anyone with the URL of your post can view 97 | the post text. You must not submit sensitive 98 | personal data to this website. 99 | 100 | 101 | ## Cookies 102 | 103 | This website does not use cookies. 104 | 105 | 106 | ## Third-Party Analytics 107 | 108 | This website does not use third-party analytics. 109 | 110 | 111 | ## Marketing 112 | 113 | The data collected from you is not used for marketing. 114 | 115 | 116 | ## Contact 117 | 118 | If you have any questions about the data collected 119 | by this website, please create a new issue at 120 | [github.com/susam/mathb/issues][issues] or contact 121 | susam (dot) pal (at) gmail (dot) com. 122 | 123 | [issues]: https://github.com/susam/mathb/issues 124 | -------------------------------------------------------------------------------- /web/css/mathb.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #333; 3 | font-family: georgia, serif; 4 | line-height: 1.5; 5 | margin: 0 auto; 6 | padding: 0 1em; 7 | } 8 | a:link { 9 | color: #00e; 10 | } 11 | a:visited { 12 | color: #518; 13 | } 14 | a:focus, a:hover { 15 | color: #03f; 16 | } 17 | a:active { 18 | color: #e00; 19 | } 20 | header h1 { 21 | text-align: center; 22 | margin: 0.5em 0; 23 | } 24 | header h1 a:link, header h1 a:visited { 25 | color: #333; 26 | text-decoration: none; 27 | } 28 | header h1 a:focus, header h1 a:hover { 29 | color: #555; 30 | text-decoration: underline; 31 | } 32 | header nav { 33 | display: none; 34 | } 35 | footer hr { 36 | border: none; 37 | border-bottom: thin dotted #666; 38 | margin: 1em 0; 39 | } 40 | form { 41 | background: #eee; 42 | border: thin solid #ccc; 43 | padding: 1em; 44 | } 45 | textarea { 46 | box-sizing: border-box; 47 | display: block; 48 | width: 100%; 49 | max-width: 100%; 50 | padding: 0.5rem; 51 | min-height: 10rem; 52 | } 53 | #view { 54 | box-sizing: border-box; 55 | min-height: calc(100vh - 38em); 56 | } 57 | input { 58 | box-sizing: border-box; 59 | display: block; 60 | width: 100%; 61 | padding: 0.5rem; 62 | margin-top: 1rem; 63 | } 64 | #submit, #copy { 65 | box-sizing: border-box; 66 | display: block; 67 | width: 50%; 68 | margin-left: auto; 69 | margin-right: auto; 70 | } 71 | #output { 72 | margin-top: 1em; 73 | } 74 | #sheet { 75 | border: thin solid #999; 76 | overflow: auto; 77 | } 78 | #output { 79 | box-sizing: border-box; 80 | } 81 | #copy { 82 | width: 25%; 83 | } 84 | footer div { 85 | max-width: 40em; 86 | margin-left: auto; 87 | margin-right: auto; 88 | } 89 | footer nav, footer nav + p { 90 | text-align: center; 91 | } 92 | footer nav a { 93 | line-height: 2.5em; 94 | } 95 | header nav a, footer nav a { 96 | margin-right: 2em; 97 | } 98 | header nav a:last-child, footer nav a:last-child { 99 | margin-right: 0; 100 | } 101 | #view { 102 | padding: 1em; 103 | } 104 | #date { 105 | font-size: small; 106 | color: #666; 107 | line-height: 1.5rem; 108 | text-align: right; 109 | padding: 0 1rem; 110 | } 111 | noscript, #error { 112 | font-size: large; 113 | color: #f00; 114 | border-color: #f00; 115 | } 116 | #error { 117 | margin-bottom: 1em; 118 | } 119 | #widget { 120 | display: none; 121 | } 122 | .post #widget { 123 | display: block; 124 | } 125 | .notice { 126 | border: thick double #f00; 127 | } 128 | @media screen and (min-width: 50em) { 129 | header h1 { 130 | display: inline-block; 131 | width: 50%; 132 | text-align: left; 133 | } 134 | header nav { 135 | display: inline-block; 136 | width: 50%; 137 | text-align: right; 138 | } 139 | form, #output { 140 | display: inline-block; 141 | box-sizing: border-box; 142 | width: calc(50% - 0.5em); 143 | vertical-align: top; 144 | } 145 | textarea { 146 | min-height: calc(100vh - 19rem); 147 | } 148 | #output { 149 | margin-left: 1em; 150 | margin-top: 0em; 151 | } 152 | #url { 153 | display: inline-block; 154 | width: 75%; 155 | } 156 | #copy { 157 | display: inline-block; 158 | margin-left: 1em; 159 | width: calc(25% - 1em); 160 | } 161 | #view { 162 | min-height: calc(100vh - 12em); 163 | } 164 | } 165 | @media (prefers-color-scheme: dark) { 166 | body { 167 | background: #111; 168 | color: #bbb; 169 | } 170 | a:link { 171 | color: #9bf; 172 | } 173 | a:visited { 174 | color: #caf; 175 | } 176 | a:focus, a:hover { 177 | color: #9cf; 178 | } 179 | a:active { 180 | color: #faa; 181 | } 182 | header h1 a:link, header h1 a:visited { 183 | color: #bbb; 184 | } 185 | header h1 a:focus, header h1 a:hover { 186 | color: #ddd; 187 | } 188 | form { 189 | background: #222; 190 | } 191 | input, textarea, button { 192 | background: #000; 193 | color: #bbb; 194 | } 195 | form, input, textarea, button, #output { 196 | border-color: #666; 197 | } 198 | noscript, #error { 199 | color: #f99; 200 | border-color: #f99; 201 | } 202 | .notice { 203 | font-size: large; 204 | border-color: #f99; 205 | margin-bottom: 1em; 206 | padding: 0 1em; 207 | } 208 | .notice p { 209 | max-width: 40em; 210 | margin-left: auto; 211 | margin-right: auto; 212 | } 213 | } 214 | @media print { 215 | header, form, footer, #date, .post #widget, .notice { 216 | display: none; 217 | } 218 | #sheet { 219 | border: 0; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 1.3.0 (2025-03-16) 5 | ------------------ 6 | 7 | This release captures the state of the MathB project as it was on Sun, 8 | 16 March 2025, when the `MathB.in` service powered by this software 9 | was shut down. 10 | 11 | 12 | ### Added 13 | 14 | - Runtime property `:expect` to enforce presence of tokens in code. 15 | - Special variable `*log-directory*` to specify directory to write 16 | logs to. 17 | 18 | 19 | ### Changed 20 | 21 | - Logs are now written to `/opt/log/mathb/` by default. 22 | - Change colour to display code from blue to green. 23 | 24 | 25 | ### Removed 26 | 27 | - Runtime property `:initial-year`. 28 | - Runtime property `:copyright-owner`. 29 | - Copyright notice from the footer. 30 | 31 | 32 | ### Fixed 33 | 34 | - Warning for `redefining HUNCHENTOOT:ACCEPTOR-STATUS-MESSAGE` during 35 | start-up. 36 | 37 | 38 | 1.2.0 (2022-10-16) 39 | ------------------ 40 | 41 | ### Added 42 | 43 | - Runtime property `:lock-down` to disable post viewing and post 44 | submission. 45 | - Runtime property `:min-title-length` to specify minimum title 46 | length. 47 | - Runtime property `:min-name-length` to specify minimum name length. 48 | - Runtime property `:min-code-length` to specify minimum code length. 49 | - Runtime property `:initial-year` to customize the initial yearin 50 | footer. 51 | - Runtime property `:copyright-owner` to customize the owner name in 52 | footer. 53 | - Link to privacy notice. 54 | - Show only rendered content in print preview mode. 55 | 56 | 57 | ### Changed 58 | 59 | - Do not allow code textarea to be dragged to overlap with output 60 | sheet. 61 | - Reject post if title field contains carriage return or line feed 62 | character. 63 | - Reject post if name field contains carriage return or line feed 64 | character. 65 | - Remove carriage returns from code before writing to data file. 66 | 67 | 68 | ### Fixed 69 | 70 | - Rendering error for LaTeX commands that need additional MathJax 71 | extensions. 72 | - Alternate HTML elements in the output not getting sanitized. 73 | 74 | 75 | 1.1.0 (2022-09-30) 76 | ------------------ 77 | 78 | ### Added 79 | 80 | - Runtime property `:ban` to reject posts from specific IP addresses. 81 | - Runtime property `:protect` to protect posts in case of data 82 | corruption. 83 | - Nginx configuration to work around Hunchentoot memory leakage issue. 84 | - Show metadata at URL path `/0`. 85 | 86 | 87 | ### Changed 88 | 89 | - Empty header value in post text file no longer has a trailing space. 90 | 91 | 92 | ### Fixed 93 | 94 | - Table rows disappearing from rendered output. 95 | - Incorrect zero top margin for display math. 96 | - HTTP 500 error on post submission when options file is missing. 97 | 98 | 99 | 1.0.0 (2022-09-20) 100 | ------------------ 101 | 102 | ### Added 103 | 104 | - The first major release of MathB since 2012. 105 | - Common Lisp source code is made available in this release. 106 | - Dark colour scheme for clients that prefer dark colour scheme. 107 | - Responsive layout to adapt the user interface to narrow screens. 108 | - Use TeXMe to render Markdown + LaTeX. 109 | - Add post interval features to control flooding. 110 | - Data is read from `/opt/data/mathb/` by default. 111 | - Special variable `*data-directory*` to specify default data 112 | directory. 113 | - Control runtime behaviour with `opt.lisp`. 114 | - Runtime property `:read-only` to run MathB in read-only mode. 115 | - Runtime property `:max-title-length` to specify maximum title 116 | length. 117 | - Runtime property `:max-name-length` to specify maximum name length. 118 | - Runtime property `:max-code-length` to specify maximum code length. 119 | - Runtime property `:global-post-interval` to specify minimum interval 120 | between two posts from arbitrary clients. 121 | - Runtime property `:client-post-interval` to specify minimum interval 122 | between two posts from the same client. 123 | - Runtime property `:block` to specify blocked strings in post. 124 | 125 | 126 | ### Changed 127 | 128 | - No more PHP source code. All application code is in Common Lisp 129 | now. 130 | 131 | 132 | 0.2.0 (2020-11-28) 133 | ------------------ 134 | 135 | ### Added 136 | 137 | - Stop rendering `` elements. 138 | 139 | 140 | 0.1.0 (2013-12-02) 141 | ------------------ 142 | 143 | ### Added 144 | 145 | - Support Markdown format. 146 | - Reset equation numbers while rendering input. 147 | 148 | 149 | 0.0.1 (2012-03-25) 150 | ------------------ 151 | 152 | ### Added 153 | 154 | - The first release of MathB written in PHP. 155 | -------------------------------------------------------------------------------- /web/js/mathb/mathb.js: -------------------------------------------------------------------------------- 1 | window.onload = function () { 2 | const form = document.getElementById('form') 3 | const code = document.getElementById('code') 4 | const title = document.getElementById('title') 5 | const name = document.getElementById('name') 6 | const submit = document.getElementById('submit') 7 | const view = document.getElementById('view') 8 | const error = document.getElementById('error') 9 | const widget = document.getElementById('widget') 10 | const url = document.getElementById('url') 11 | const copy = document.getElementById('copy') 12 | const slug = parseInt(window.location.pathname.substring(1)) 13 | 14 | let rendering = false 15 | let timeout = null 16 | 17 | const allowedNodes = [ 18 | "#text", "blockquote", "br", "code", "div", "em", 19 | "h1", "h2", "h3", "h4", "h5", "h6", 20 | "hr", "li", "ol", "p", "pre", "strong", 21 | "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul" 22 | ] 23 | 24 | function removeMarkup(s) { 25 | const div = document.createElement('div') 26 | div.innerHTML = s 27 | return div.innerText 28 | } 29 | 30 | function sanitizeDOM(node) { 31 | if (allowedNodes.indexOf(node.nodeName.toLowerCase()) === -1) { 32 | node.parentNode.removeChild(node) 33 | return 34 | } 35 | if (node.nodeType === Node.ELEMENT_NODE) { 36 | for (let i = 0; i < node.attributes.length; i++) { 37 | node.removeAttributeNode(node.attributes[i]) 38 | } 39 | } 40 | for (let i = node.childNodes.length - 1; i >= 0; i--) { 41 | sanitizeDOM(node.childNodes[i]) 42 | } 43 | } 44 | 45 | function sanitizeHTML(html) { 46 | if (slug < 10) { 47 | return html 48 | } 49 | const div = document.createElement('div') 50 | div.innerHTML = html 51 | sanitizeDOM(div) 52 | return div.innerHTML 53 | } 54 | 55 | function renderView() { 56 | if (rendering) { 57 | return 58 | } 59 | rendering = true 60 | 61 | const titleValue = title.value.trim() 62 | const nameValue = name.value.trim() 63 | const codeValue = code.value.trim() 64 | 65 | let h1 = '' 66 | let h2 = '' 67 | let body = '' 68 | 69 | if (titleValue !== '') { 70 | h1 = '

' + removeMarkup(titleValue) + '

' 71 | } 72 | 73 | if (nameValue !== '') { 74 | h2 = '

' + removeMarkup(nameValue) + '

' 75 | } 76 | 77 | if (codeValue != '') { 78 | body = sanitizeHTML(texme.render(codeValue)) 79 | } 80 | 81 | view.innerHTML = h1 + h2 + body 82 | window.MathJax.texReset() 83 | window.MathJax.typesetPromise().then(function () { 84 | rendering = false 85 | }) 86 | } 87 | 88 | // Schedule input handler to process input after a short delay. 89 | // 90 | // When the user edits an element in the input form, the 91 | // corresponding element of the output sheet is not updated 92 | // immediately for two reasons: 93 | // 94 | // - A fast typist can type 7 to 10 characters per second. 95 | // Updating the output sheet so frequently, causes the user 96 | // interface to become less responsive. 97 | // 98 | // - The onpaste or oncut functions of an input element gets the 99 | // old value of the element instead of the new value resulting 100 | // from the cut or paste operation. 101 | // 102 | // This function works around the above issues by scheduling the 103 | // renderView() function to be called after 100 milliseconds. This 104 | // ensures that the output is not updated more than 10 times per 105 | // second. This also ensures that when the renderView() function is 106 | // invoked as a result of a cut or paste operation on a text field 107 | // element, then it gets the updated value of the element. 108 | function scheduleInputHandler() { 109 | if (timeout !== null) { 110 | window.clearTimeout(timeout) 111 | timeout = null 112 | } 113 | timeout = window.setTimeout(renderView, 100) 114 | } 115 | 116 | function insertToken() { 117 | const a = 123 + Math.floor((1000 - 123) * Math.random()) 118 | const b = a % 91 119 | const c = a % 87 120 | const x = 1000000 * a + 1000 * b + c 121 | const input = document.createElement('input') 122 | input.setAttribute('type', 'hidden') 123 | input.setAttribute('name', 'token') 124 | input.setAttribute('value', x.toString()) 125 | form.appendChild(input) 126 | } 127 | 128 | function copyURL() { 129 | url.select() 130 | document.execCommand('copy') 131 | window.setTimeout(function () { url.blur() }, 125) 132 | } 133 | 134 | function init() { 135 | form.onsubmit = insertToken 136 | 137 | code.onkeyup = scheduleInputHandler 138 | code.onpaste = scheduleInputHandler 139 | code.oncut = scheduleInputHandler 140 | 141 | title.onkeyup = scheduleInputHandler 142 | title.onpaste = scheduleInputHandler 143 | title.oncut = scheduleInputHandler 144 | 145 | name.onkeyup = scheduleInputHandler 146 | name.onpaste = scheduleInputHandler 147 | name.oncut = scheduleInputHandler 148 | 149 | url.onclick = url.select 150 | copy.onclick = copyURL 151 | 152 | code.focus() 153 | 154 | if (window.location.pathname !== '/') { 155 | url.value = window.location.href 156 | } 157 | 158 | if (error === null) { 159 | renderView() 160 | } 161 | } 162 | 163 | init() 164 | } 165 | -------------------------------------------------------------------------------- /meta/data/post/0/0/2.txt: -------------------------------------------------------------------------------- 1 | Date: 2012-03-25 00:00:00 +0000 2 | Title: Get Started 3 | Name: 4 | 5 | Inline Mathematics 6 | ------------------ 7 | 8 | Inline mathematics can be written in two ways. 9 | They can be enclosed within \begin{md}`$`\end{md} 10 | and \begin{md}`$`\end{md}, or 11 | \begin{md}`\(`\end{md} and \begin{md}`\)`\end{md}. 12 | For example, typing `$ e^{i \pi} + 1 = 0 $` or `\( 13 | e^{i \pi} + 1 = 0 \)` in the code pane produces 14 | the following output in the output pane: $ e^{i 15 | \pi} + 1 = 0 $. 16 | 17 | 18 | Displayed Mathematics 19 | --------------------- 20 | 21 | Displayed mathematics appears on its own line in 22 | the output. To render displayed math, they can be 23 | enclosed within \begin{md}`$$`\end{md} and 24 | \begin{md}`$$`\end{md}, or \begin{md}`\[`\end{md} 25 | and \begin{md}`\]`\end{md}. For example, typing 26 | `$$ e^{i \pi} + 1 = 0. $$` or `\[ e^{i \pi} + 1 = 27 | 0. \]` in the code pane produces the following 28 | output in the output pane: $$ e^{i \pi} + 1 = 0. 29 | $$ Apart from this, many LaTeX environments such 30 | as `equation`, `align`, etc. are also supported. 31 | 32 | 33 | Text 34 | ---- 35 | 36 | Mathematics and text can be mixed freely while 37 | posting a mathematics snippet. Text is written in 38 | Markdown format. As you can see, this tutorial 39 | itself is another post, so you can compare the 40 | code in the code pane with the output in output 41 | pane as you read this tutorial. A new paragraph 42 | begins by placing a blank line in the code. 43 | 44 | This is a new paragraph. Check the code pane on 45 | to ensure that there indeed is a blank line 46 | between this paragraph and the previous one. 47 | 48 | A code fence can be created by writing three 49 | consecutive backticks (` ``` `) on its own line. 50 | Code blocks are placed between two code fences: 51 | 52 | ``` 53 | #include 54 | 55 | int main(int argc, char **argv) 56 | { 57 | printf("hello, world\n"); 58 | } 59 | ``` 60 | 61 | Ordered lists can be written by numbering each 62 | line: 63 | 64 | 1. Apple 65 | 2. Mango 66 | 3. Banana 67 | 68 | Unordered lists can be written by using an 69 | asterisk (`*`), plus sign (`+`), or minus sign 70 | (`-`) as bullet for each item in the list: 71 | 72 | - Apple 73 | - Mango 74 | - Banana 75 | 76 | Enclose text within a pair of asterisks to 77 | *emphasize* it. Enclose text within a pair of 78 | double-asterisks to **emphasize it strongly**. 79 | Underscores may be used instead of asterisks to 80 | add emphasis too. 81 | 82 | Various other features are supported in Markdown 83 | such as inline code, blockquotes, horizontal 84 | rules, etc. This website conforms to GitHub 85 | Flavoured Markdown (GFM) specification. Note that 86 | GFM is a strict superset of CommonMark. 87 | 88 | 89 | LaTeX 90 | ----- 91 | 92 | This website uses [MathJax][M1] to render 93 | mathematics on the web browser. See [MathJax: 94 | Supported TeX/LaTeX commands][M2] for a list of 95 | LaTeX commands supported by MathJax. 96 | 97 | 98 | Markdown 99 | -------- 100 | 101 | This website supports GitHub Flavoured Markdown 102 | (GFM). GFM is a strict superset of the CommonMark 103 | specification of Markdown. See the [GitHub 104 | Flavoured Markdown Specification][G1] for more 105 | details on the specification. 106 | 107 | 108 | TeXMe 109 | ----- 110 | 111 | This website uses [TeXMe][T1] to allow mixing 112 | LaTeX with Markdown. TeXMe reads the input and 113 | renders it while preventing the Markdown renderer 114 | from seeing the LaTeX code, so that the LaTeX code 115 | remains intact for rendering by MathJax. 116 | 117 | However, when Markdown code contains mathematics 118 | delimiters like \begin{md}`$`\end{md}, 119 | \begin{md}`\(`\end{md}, etc., TeXMe may identify 120 | them as mathematics instead of Markdown code as 121 | intended. To prevent mathematics delimiters in 122 | Markdown code from being interpreted as LaTeX, 123 | enclose the Markdown code within 124 | \begin{mdx}`\begin{md}`\end{mdx} and 125 | \begin{mdx}`\end{md}`\end{mdx}. Here is an 126 | example: 127 | 128 | \begin{mdx} 129 | ``` 130 | > The variables \begin{md}`$foo`\end{md} and 131 | > \begin{md}`$bar`\end{md} are metasyntactic 132 | > variables. 133 | ``` 134 | \end{mdx} 135 | 136 | Here is how the above code is rendered in the 137 | output: 138 | 139 | > The variables \begin{md}`$foo`\end{md} and 140 | > \begin{md}`$bar`\end{md} are metasyntactic 141 | > variables. 142 | 143 | See [TeXMe: Markdown Priority Environment][T2] to 144 | learn more about this. 145 | 146 | 147 | Print 148 | ----- 149 | 150 | While the primary purpose of this website is to 151 | allow users to write mathematical snippets, save 152 | them, and share a link to them with others, the 153 | stylesheet used in this website takes special care 154 | to allow printing beautifully rendered pages to 155 | paper or PDF. 156 | 157 | When a page from this website is printed, only the 158 | rendered content appears in the print. The input 159 | form, buttons, navigation links, and other user 160 | interface elements do not appear in the print. 161 | 162 | 163 | Save PDF 164 | -------- 165 | 166 | It is possible to turn a post on this website into 167 | a PDF file using the printing facility of most web 168 | browsers running on a desktop or laptop. The 169 | exact steps to save a web page as PDF vary from 170 | browser to browser but the steps to do so roughly 171 | look like this: 172 | 173 | - Select File > Print from the web browser menu. 174 | - Then in the print window or dialog box that 175 | comes up, deselect/disable the options to print 176 | headers and footers. 177 | - Finally, choose the option to save a PDF. 178 | 179 | You can try the above steps right now with this 180 | page and save this page as PDF. 181 | 182 | If everything works as expected, the saved PDF 183 | should contain only the rendered content with all 184 | mathematical formulas rendered properly. This 185 | website has special styling rules to ensure that 186 | the input form, buttons, navigation links, and 187 | other user interface elements do not appear in 188 | saved PDF. 189 | 190 | 191 | [M1]: http://www.mathjax.org/ 192 | [M2]: https://docs.mathjax.org/en/latest/input/tex/macros/ 193 | [G1]: https://github.github.com/gfm/ 194 | [T1]: https://github.com/susam/texme#texme 195 | [T2]: https://github.com/susam/texme#markdown-priority-environment 196 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME = mathb 2 | FQDN = $(NAME).in 3 | MAIL = $(FQDN)@yahoo.com 4 | 5 | help: 6 | @echo 'Usage: make [target]' 7 | @echo 8 | @echo 'Targets to run on live server:' 9 | @echo ' setup Install Debian packages and Quicklisp for website.' 10 | @echo ' https Reinstall live website and serve with Nginx via HTTPS.' 11 | @echo ' http Reinstall live website and serve with Nginx via HTTP.' 12 | @echo ' rm Uninstall live website.' 13 | @echo ' backup Create live server data backup.' 14 | @echo ' review Review posts listed in /tmp/review.txt.' 15 | @echo ' follow-log Follow logs on live server.' 16 | @echo ' follow-post Follow POST logs on live server.' 17 | @echo ' post-log Filter logs to find all successful posts.' 18 | @echo ' top-get-log Filter logs to find most popular pages.' 19 | @echo 20 | @echo 'Low-level targets:' 21 | @echo ' live Generate live website.' 22 | @echo ' site Generate local website.' 23 | @echo 24 | @echo 'Development targets:' 25 | @echo ' opt Create directories at /opt for testing.' 26 | @echo ' run Run application.' 27 | @echo ' test Test application code.' 28 | @echo ' checks Validate consistency within configuration.' 29 | @echo ' pub Publish updated website on live server.' 30 | @echo ' force-pub Reset website on live server and publish website.' 31 | @echo ' pull-backup Pull a backup of data from live server.' 32 | @echo 33 | @echo 'Default target:' 34 | @echo ' help Show this help message.' 35 | 36 | 37 | # Targets for Live Server 38 | # ----------------------- 39 | 40 | setup: 41 | apt-get update 42 | apt-get -y install nginx certbot sbcl 43 | rm -rf /opt/quicklisp.lisp /opt/quicklisp 44 | curl https://beta.quicklisp.org/quicklisp.lisp -o /opt/quicklisp.lisp 45 | sbcl --load /opt/quicklisp.lisp \ 46 | --eval '(quicklisp-quickstart:install :path "/opt/quicklisp/")' \ 47 | --quit 48 | chown -R www-data:www-data /opt/quicklisp 49 | 50 | https: http wait-http 51 | @echo Setting up HTTPS website ... 52 | certbot certonly -n --agree-tos -m '$(MAIL)' --webroot \ 53 | -w '/var/www/$(FQDN)' -d '$(FQDN),www.$(FQDN)' 54 | (crontab -l | sed '/::::/d'; cat etc/crontab) | crontab 55 | ln -snf "$$PWD/etc/nginx/https.$(FQDN)" '/etc/nginx/sites-enabled/$(FQDN)' 56 | systemctl restart nginx 57 | @echo Done; echo 58 | 59 | http: rm live mathb 60 | @echo Setting up HTTP website ... 61 | ln -snf "$$PWD/_live" '/var/www/$(FQDN)' 62 | ln -snf "$$PWD/etc/nginx/http.$(FQDN)" '/etc/nginx/sites-enabled/$(FQDN)' 63 | ln -snf "$$PWD/etc/logrotate" /etc/logrotate.d/mathb 64 | systemctl restart nginx 65 | echo 127.0.0.1 '$(NAME)' >> /etc/hosts 66 | @echo Done; echo 67 | 68 | wait-http: 69 | @echo Waiting for HTTP website to start ... 70 | while ! curl http://localhost:4242/; do sleep 1; echo Retrying ...; done 71 | @echo Done; echo 72 | 73 | mathb: 74 | @echo Setting up mathb ... 75 | mkdir -p /opt/cache/ /opt/data/mathb/ /opt/log/mathb/ 76 | chown -R www-data:www-data /opt/cache/ /opt/data/mathb/ /opt/log/mathb/ 77 | systemctl enable "/opt/mathb.in/etc/mathb.service" 78 | systemctl daemon-reload 79 | systemctl start mathb 80 | @echo Done; echo 81 | 82 | rm: checkroot 83 | @echo Removing website ... 84 | rm -f /etc/logrotate/mathb 85 | rm -f '/etc/nginx/sites-enabled/$(FQDN)' 86 | rm -f '/var/www/$(FQDN)' 87 | systemctl restart nginx 88 | sed -i '/$(NAME)/d' /etc/hosts 89 | @echo 90 | @echo Removing mathb ... 91 | -systemctl stop mathb 92 | -systemctl disable mathb 93 | systemctl daemon-reload 94 | @echo 95 | @echo Following crontab entries left intact: 96 | crontab -l | grep -v "^#" || : 97 | @echo Done; echo 98 | 99 | checkroot: 100 | @echo Checking if current user is root ... 101 | [ $$(id -u) = 0 ] 102 | @echo Done; echo 103 | 104 | backup: 105 | tar -caf "/opt/cache/mathb-$$(date "+%Y-%m-%d_%H-%M-%S").tgz" -C /opt/data/ mathb/ 106 | ls -1 /opt/cache/mathb-*.tgz | sort -r | tail -n +100 | xargs rm -vf 107 | ls -lh /opt/cache/ 108 | df -h / 109 | 110 | review: checkroot 111 | mkdir -p /tmp/deleted 112 | [ -f /tmp/review.txt ] 113 | for f in $$(cat /tmp/review.txt); do \ 114 | reply=e; \ 115 | echo "Editing $$f ..."; \ 116 | while [ "$$reply" = e ]; do \ 117 | emacs "$$f"; \ 118 | echo; head "$$f"; printf "\n\n...\n\n"; tail "$$f"; echo; \ 119 | reply=; \ 120 | while ! printf '%s' "$$reply" | grep -qE '^(e|d|n|q)$$'; do \ 121 | printf "Action for $$f (d, e, n, q)? "; \ 122 | read reply; \ 123 | [ "$$reply" = d ] && mv "$$f" /tmp/deleted; \ 124 | [ "$$reply" = q ] && exit; \ 125 | done; \ 126 | done; \ 127 | done; echo Done; echo 128 | 129 | follow-log: 130 | tail -F /opt/log/mathb/mathb.log 131 | 132 | follow-post: 133 | tail -F /opt/log/mathb/mathb.log | grep POST 134 | 135 | post-log: 136 | tail -F /opt/log/mathb/mathb.log | grep written 137 | 138 | top-get-log: 139 | grep ' 200 ' /opt/log/mathb/mathb.log* | grep -o 'GET /[0-9]*' | sort | uniq -c | sort -nr | nl | less 140 | 141 | 142 | # Low-Level Targets 143 | # ----------------- 144 | 145 | live: site 146 | @echo Setting up live directory ... 147 | mv _live _gone || : 148 | mv _site _live 149 | rm -rf _gone 150 | @echo Done; echo 151 | 152 | site: 153 | @echo Setting up site directory ... 154 | rm -rf _site/ 155 | mkdir -p _site/css/ _site/js/ 156 | cp -R web/css/* _site/css/ 157 | cp -R web/js/* _site/js/ 158 | cp -R web/img/* _site/ 159 | git -C _site/js/ clone -b 1.2.0 --depth 1 https://github.com/susam/texme.git 160 | git -C _site/js/ clone -b v4.1.0 --depth 1 https://github.com/markedjs/marked.git 161 | git -C _site/js/ clone -b 3.2.2 --depth 1 https://github.com/mathjax/mathjax.git 162 | rm -rf _site/js/texme/.git 163 | rm -rf _site/js/marked/.git/ 164 | rm -rf _site/js/mathjax/.git/ 165 | @echo Done; echo 166 | 167 | 168 | # Development Targets 169 | # ------------------- 170 | 171 | opt: 172 | sudo mkdir -p /opt/data/mathb/ /opt/log/mathb/ 173 | sudo cp -R meta/data/* /opt/data/mathb/ 174 | sudo chown -R "$$USER" /opt/data/mathb/ /opt/log/mathb/ 175 | 176 | run: 177 | sbcl --load mathb.lisp 178 | 179 | test: 180 | sbcl --noinform --eval "(defvar *quit* t)" --load test.lisp 181 | 182 | checks: 183 | # Ensure http.mathb.in and https.mathb.in are consistent. 184 | sed -n '1,/limit_req_status/p' etc/nginx/http.mathb.in > /tmp/http.mathb.in 185 | sed -n '1,/limit_req_status/p' etc/nginx/https.mathb.in > /tmp/https.mathb.in 186 | diff -u /tmp/http.mathb.in /tmp/https.mathb.in 187 | sed -n '/server_name [^w]/,/^}/p' etc/nginx/http.mathb.in > /tmp/http.mathb.in 188 | sed -n '/server_name [^w]/,/^}/p' etc/nginx/https.mathb.in > /tmp/https.mathb.in 189 | diff -u /tmp/http.mathb.in /tmp/https.mathb.in 190 | @echo Done; echo 191 | 192 | pub: 193 | git push 194 | ssh -t mathb.in "cd /opt/mathb.in/ && sudo git pull && sudo cp meta/data/post/0/0/*.txt /opt/data/mathb/post/0/0/ && sudo chown -R www-data:www-data meta/data/post/0/0/*.txt && sudo make live && sudo systemctl restart nginx mathb && sudo systemctl --no-pager status nginx mathb" 195 | 196 | force-pub: 197 | git push -f 198 | ssh -t mathb.in "cd /opt/mathb.in/ && sudo git reset --hard HEAD~5 && sudo git pull && sudo cp meta/data/post/0/0/*.txt /opt/data/mathb/post/0/0/ && sudo chown -R www-data:www-data meta/data/post/0/0/*.txt && sudo make live && sudo systemctl restart nginx mathb && sudo systemctl --no-pager status nginx mathb" 199 | 200 | pull-backup: 201 | mkdir -p ~/bkp/ 202 | ssh mathb.in "tar -czf - -C /opt/data/ mathb/" > ~/bkp/mathb-$$(date "+%Y-%m-%d_%H-%M-%S").tgz 203 | ls -lh ~/bkp/ 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MathB 2 | ===== 3 | 4 | MathB is a mathematics pastebin software that powered `MathB.in` from 5 | 2012 to 2025. It is a web-based service meant for sharing snippets of 6 | mathematical text with others on the world wide web. 7 | 8 | 9 | Contents 10 | -------- 11 | 12 | * [Features](#features) 13 | * [Quick Start](#quick-start) 14 | * [Custom Directory Paths](#custom-directory-paths) 15 | * [Data Files](#data-files) 16 | * [Runtime Options](#runtime-options) 17 | * [Templates Files](#template-files) 18 | * [Static Files](#static-files) 19 | * [Live Directory](#live-directory) 20 | * [Print](#print) 21 | * [Save PDF](#save-pdf) 22 | * [History](#history) 23 | * [License](#license) 24 | * [Support](#support) 25 | * [Channels](#channels) 26 | * [More](#more) 27 | 28 | 29 | Features 30 | -------- 31 | 32 | - Minimalist user interface that has not changed much over a decade. 33 | - Live preview of Markdown and LaTeX content as it is typed. 34 | - Support for mixing Markdown and LaTeX code freely. 35 | - Printing a post to PDF or paper prints only the rendered content. 36 | - All UI elements apart from rendered content are excluded from 37 | prints. 38 | - No web cookies. 39 | - No web analytics. 40 | 41 | 42 | Quick Start 43 | ----------- 44 | 45 | This section explains how to run this project locally. The steps 46 | assume a macOS, Debian, or Debian-based Linux distribution. However, 47 | it should be possible to adapt these steps for another operating 48 | system. 49 | 50 | 1. Install SBCL and Git. 51 | 52 | On macOS, enter the following command if you have Homebrew: 53 | 54 | ```sh 55 | brew install sbcl git 56 | ``` 57 | 58 | On Debian, Ubuntu, or another Debian-based Linux system, enter the 59 | following command: 60 | 61 | ```sh 62 | sudo apt-get update 63 | sudo apt-get install sbcl git 64 | ``` 65 | 66 | 2. Install Quicklisp with the following commands: 67 | 68 | ```sh 69 | curl -O https://beta.quicklisp.org/quicklisp.lisp 70 | sbcl --load quicklisp.lisp --eval "(quicklisp-quickstart:install)" --quit 71 | sbcl --load ~/quicklisp/setup.lisp --eval "(ql:add-to-init-file)" --quit 72 | ``` 73 | 74 | 3. From here on, we assume that all commands are being run in the 75 | top-level directory of this project. Set up dependencies 76 | necessary to run this project by running this command within the 77 | top-level directory of this project: 78 | 79 | ```sh 80 | make live 81 | ``` 82 | 83 | This creates a `_live` directory within the current directory and 84 | copies all necessary dependencies to it. 85 | 86 | 4. Create data and log directories: 87 | 88 | ```sh 89 | sudo mkdir -p /opt/data/mathb/ /opt/log/mathb/ 90 | sudo cp -R meta/data/* /opt/data/mathb/ 91 | sudo chown -R "$USER" /opt/data/mathb/ /opt/log/mathb/ 92 | ``` 93 | 94 | By default, MathB reads post data from and writes posts to 95 | `/opt/data/mathb/`. It writes logs to `/opt/log/mathb/` by 96 | default. The next section explains how to make it use custom 97 | directory paths. 98 | 99 | 4. Run MathB with the following command: 100 | 101 | ```sh 102 | sbcl --load mathb.lisp 103 | ``` 104 | 105 | 5. Visit http://localhost:4242/ with a web browser to use MathB. 106 | 107 | After starting MathB in this manner, click on the various navigation 108 | links and make a new post to confirm that MathB is working as 109 | expected. 110 | 111 | 112 | Custom Directory Paths 113 | ---------------------- 114 | 115 | In the previous section, we created a data directory at 116 | `/opt/data/mathb/` and a log directory at `/opt/log/mathb/`. By 117 | default, MathB writes new posts to and reads posts from this directory 118 | path. To make it use a different path for the data directory, set the 119 | variable named `*data-directory*` before loading it. Similarly, set 120 | the variable named `*log-directory*` to specify a different path for 121 | the log directory. The following steps demonstrate how to do this: 122 | 123 | 1. Create data directory at a custom path, say, at `~/data`: 124 | 125 | ```sh 126 | mkdir -p ~/data/ ~/log/ 127 | cp -R meta/data/* ~/data/ 128 | ``` 129 | 130 | 2. Run MathB with the following command: 131 | 132 | ```sh 133 | sbcl --eval '(defvar *data-directory* "~/data/")' \ 134 | --eval '(defvar *log-directory* "~/log/")' \ 135 | --load mathb.lisp 136 | ``` 137 | 138 | 3. Visit http://localhost:4242/ with a web browser to use MathB. 139 | 140 | After starting MathB in this manner, click on the various navigation 141 | links and make a new post to confirm that MathB is working as 142 | expected. 143 | 144 | 145 | Data Files 146 | ---------- 147 | 148 | The data directory contains the following files: 149 | 150 | - [`opt.lisp`]: This file contains a property list that can be 151 | modified to alter the behaviour of MathB. This is explained in 152 | detail in the next section. 153 | 154 | - [`slug.txt`]: This file contains the ID of the latest post 155 | successfully saved. 156 | 157 | - [`post/X/Y/*.txt`]: These files contain the actual posts submitted 158 | by users where `X` and `Y` are placeholders for two integers 159 | explained shortly. Each `.txt` file contains a post submitted by a 160 | user. 161 | 162 | In the last point, the placeholder `X` is the post ID divided 163 | by 1000000. The placeholder `Y` is the post ID divided by 1000. For 164 | example, for a post with ID 1, `X` is `0` and `Y` is `0`, so a post 165 | with this ID is saved at `post/0/0/1.txt`. For a more illustrative 166 | example, consider a post with with ID 2301477. Now `X` is `2` and `Y` 167 | is `2301`, so a post with this ID is saved at 168 | `post/2/2301/2301477.txt`. 169 | 170 | Let us call each `X` directory a short-prefix directory and each `Y` 171 | directory under it a long-prefix directory. As a result of the 172 | calculation explained above, each short-prefix directory contains a 173 | maximum of 1000 long-prefix directories and each long-prefix directory 174 | contains a maximum of 1000 post files. Thus, each short-prefix 175 | directory contains a maximum of one million post files under it. 176 | 177 | [`opt.lisp`]: meta/data/opt.lisp 178 | [`slug.txt`]: meta/data/slug.txt 179 | [`post/X/Y/*.txt`]: meta/data/post/0/0 180 | 181 | 182 | Runtime Options 183 | --------------- 184 | 185 | MathB reads runtime properties from `opt.lisp`. This file contains a 186 | property list. Each property in this list is followed by a value for 187 | that property. This property list may be used to alter the behaviour 188 | of MathB. A list of all supported properties and their descriptions 189 | is provided below. 190 | 191 | - `:lock-down` (default is `nil`): A value of `t` makes MathB run in 192 | lock-down mode, i.e., existing posts cannot be viewed and new 193 | posts cannot be submitted. 194 | 195 | - `:read-only` (default is `nil`): A value of `t` makes MathB run in 196 | read-only mode, i.e., old posts can be viewed but new posts cannot 197 | be made. If the values of both this property and the previous 198 | property are `nil`, then MathB runs normally in read-write mode. 199 | 200 | - `:min-title-length` (default is `0`): The minimum number of 201 | characters allowed in the title field. 202 | 203 | - `:max-title-length` (default is `120`): The maximum number of 204 | characters allowed in the title field. 205 | 206 | - `:min-name-length` (default is `0`): The minimum number of 207 | characters allowed in the name field. 208 | 209 | - `:max-name-length` (default is `120`): The maximum number of 210 | characters allowed in the name field. 211 | 212 | - `:min-code-length` (default is `1`): The minimum number of 213 | characters allowed in the code field. 214 | 215 | - `:max-code-length` (default is `10000`): The maximum number of 216 | characters allowed in the code field. 217 | 218 | - `:global-post-interval` (default is `0`): The minimum interval (in 219 | seconds) required between two consecutive successful posts. 220 | 221 | Example: If this value is `10` and one client submits a new post 222 | at 10:00:00 and another client submits a post at 10:00:07, the 223 | post of the second client is rejected with an error message that 224 | they must wait for 3 more seconds before submitting the post. An 225 | attempt to submit the post at 10:00:10 or later would succeed, 226 | provided that no other client submitted another post between 227 | 10:00:10 and the second client's attempt to make a post. 228 | 229 | - `:client-post-interval` (default is `0`): The minimum interval (in 230 | seconds) between two consecutive successful posts allowed from the 231 | same client. 232 | 233 | Example: If this value is `10` and one client submits a new post 234 | at 10:00:00, then the same client is allowed to make the next 235 | successful post submission at 10:00:10 or later. If the same 236 | client submits another post at 10:00:07, the post is rejected with 237 | an error message that they must wait for 3 more seconds before 238 | submitting the post. This does not affect the posting behaviour 239 | for other clients. For example, another client can successfully 240 | submit their post at 10:00:07 while the first client cannot. 241 | 242 | - `:expect` (default is `nil`): A list of strings. At least one 243 | string from this list must occur in the submitted code field. 244 | 245 | Example: If this value is `("\(" "\[")` and the submitted post 246 | contains `\[ 1 + 1 = 2. \]` in the code field, then the post is 247 | accepted successfully. However, if the submitted code contains 248 | only `1 + 1 = 2`, then the post is rejected because neither the 249 | string `"\("` nor the string `"\["` occurs in the code field of 250 | this submission. 251 | 252 | - `:block` (default is `nil`): A list of strings that are not 253 | allowed in a post. If a post contains any string in this list, 254 | the post is rejected and the input form is returned intact to the 255 | client. 256 | 257 | Example: If this value is `("berk" "naff" "xxx")` and a client 258 | posts content which contains the string `xxx` in any field (code, 259 | title, or name), the post is rejected. 260 | 261 | - `:ban` (default is `nil`): A list of IPv4 or IPv6 address 262 | prefixes. If the address of the remote client (as it appears in 263 | the logs) matches any prefix in this list, the post from the 264 | client is rejected. The prefixes must be expressed as simple 265 | string literals. CIDRs, globs, regular expressions, etc. are not 266 | supported. A dollar sign (`$`) at the end of a prefix string 267 | matches the end of the client's address string. 268 | 269 | Example: Let us consider a value of `("10.1." "10.2.0.2" 270 | "10.3.0.2$")` for this property. If a client from IP address 271 | `10.1.2.3` submits a post, it is rejected because the prefix 272 | `10.1.` matches this IP address. If a client from IP address 273 | `10.2.0.23` submits a post, it is rejected because the prefix 274 | `10.2.0.2` matches this IP address. If a client from IP address 275 | `10.3.0.2` submits a post, it is rejected because the prefix 276 | `10.3.0.2$` matches this IP address. If a client from IP address 277 | `10.3.0.23` submits a post, it is accepted because none of the 278 | prefixes match this IP address. 279 | 280 | - `:protect` (default is `0`): The maximum ID of protected posts. 281 | If MathB determines that the post ID of the next post is less than 282 | or equal to this value, then it rejects the post. Setting this 283 | property is almost never required. However, it is provided for 284 | paranoid administrators who might worry what would happen if the 285 | data file `slug.txt` ever becomes corrupt. This property ensures 286 | that in case this data file ever becomes corrupt, MathB would 287 | never ever overwrite old posts with IDs less than or equal to the 288 | number set for this property. 289 | 290 | Example: Let us assume that the current value in `slug.txt` 291 | is 1200. Now normally, the next time a client submits a new post, 292 | their post would be saved with an ID of 1201 and the value in 293 | `slug.txt` would be incremented to 1201. But instead, let us 294 | assume that due to an unforeseen scenario (say, a bug in MathB or 295 | a hardware failure), the value in `slug.txt` is corrupted to `12`. 296 | With a value of `0` for `:protect`, MathB would overwrite an 297 | existing post at `post/0/0/13.txt`. However, with a value of say, 298 | `100` for `:protect`, MathB would refuse to overwrite the existing 299 | port. 300 | 301 | If a property name is missing from this file or if the file itself is 302 | missing, then the default value of the property mentioned within 303 | parentheses above is used. 304 | 305 | Whenever a post is rejected due to a runtime option, the entire input 306 | form is returned intact to the client with an error message, so that 307 | they can fix the errors or wait for the suggested post interval and 308 | resubmit the post again. 309 | 310 | The property values in `opt.lisp` may be modified at any time, even 311 | while MathB is running. It is not necessary to restart MathB after 312 | changing property values in `opt.lisp`. The changes are picked up 313 | automatically while processing the next HTTP POST request. 314 | 315 | 316 | Template Files 317 | -------------- 318 | 319 | There are two template files to generate the HTML pages sent to the 320 | clients: 321 | 322 | - [`web/html/mathb.html`]: This template file is used to generate 323 | the HTML response for the home page, a mathematical snippet page, 324 | as well as an HTTP response page when the post is rejected due to 325 | a validation error. 326 | 327 | - [`web/html/error.html`]: This template file is used to generate 328 | HTTP error pages. 329 | 330 | A template file may be modified at any time, even while MathB is 331 | running. It is not necessary to restart MathB after changing a 332 | template file. The changes are picked up automatically while 333 | processing the next HTTP request. 334 | 335 | [`web/html/mathb.html`]: web/html/mathb.html 336 | [`web/html/error.html`]: web/html/error.html 337 | 338 | 339 | Static Files 340 | ------------ 341 | 342 | There are three types of static files that MathB uses to for its HTML 343 | pages: 344 | 345 | - [`web/js/`]: This directory contains the JavaScript files that 346 | perform input rendering as a user types out content in the input 347 | form. 348 | 349 | - [`web/css/`]: This directory contains the stylesheets for the HTML 350 | pages generated by MathB. 351 | 352 | - [`web/img/`]: This directory contains the favicons for the 353 | website. These icons are generated using a LaTeX project in the 354 | [`meta/logo/`] directory. 355 | 356 | A static file may be modified at any time, even while MathB is 357 | running. It is not necessary to restart MathB after adding, deleting, 358 | or editing a static file. However, it is necessary to run `make live` 359 | (in the top-level directory of the project) to copy the static files 360 | to the live directory (explained in the next section) from which MathB 361 | serves the static files. 362 | 363 | [`web/js/`]: web/js/ 364 | [`web/css/`]: web/css/ 365 | [`web/img/`]: web/img/ 366 | [`meta/logo/`]: meta/logo/ 367 | 368 | 369 | Live Directory 370 | -------------- 371 | 372 | MathB needs to pull additional JavaScript libraries named TeXMe, 373 | Marked, and MathJax that are essential for rendering Markdown and 374 | LaTeX input. This is done by running the following command in the 375 | top-level directory of this project: 376 | 377 | ```sh 378 | make live 379 | ``` 380 | 381 | The above command creates a `_live` directory from scratch, copies the 382 | static files to it, then pulls the additional JavaScript libraries 383 | into it, and sets up the `_live` directory, so that MathB can serve 384 | the static files from it. 385 | 386 | The live directory should never be modified directly because every 387 | `make live` run deletes the entire directory and creates it from 388 | scratch again. Any modification necessary should be made to the 389 | template files or static files explained in the previous two sections. 390 | 391 | 392 | Print 393 | ----- 394 | 395 | While the primary purpose of this project is to allow users to write 396 | mathematical snippets, save them, and share a link to them with 397 | others, the stylesheet used in this project takes special care to 398 | allow printing beautifully rendered pages to paper. 399 | 400 | When a MathB page is printed, only the rendered content appears in the 401 | print. The input form, buttons, navigation links, and other user 402 | interface elements do not appear in the print. 403 | 404 | 405 | Save PDF 406 | -------- 407 | 408 | It is possible to turn a MathB post into a PDF file using the printing 409 | facility of most web browsers running on a desktop or laptop. The 410 | exact steps to save a web page as PDF vary from browser to browser but 411 | the steps to do so look roughly like this: 412 | 413 | - Select File > Print from the web browser menu. 414 | - Then in the print window or dialog box that comes up, 415 | deselect/disable the options to print headers and footers. 416 | - Finally, choose the option to save a PDF. 417 | 418 | If everything works as expected, the saved PDF should contain only the 419 | rendered content with all mathematical formulas rendered properly. 420 | The web pages generated by this project use special styling rules to 421 | ensure that the input form, buttons, navigation links, and other user 422 | interface elements do not appear in saved PDF. 423 | 424 | 425 | History 426 | ------- 427 | 428 | MathB.in was the longest-running mathematics pastebin, serving its 429 | community of users for over a decade until its discontinuation in 430 | March 2025. It was not the first mathematics pastebin on the world 431 | wide web though. It was the second one. The first one was created by 432 | Mark A. Stratman which was hosted at `mathbin.net` until 2020. 433 | 434 | MathB.in was launched on Sunday, 25 March 2012, after a single night 435 | of furious coding. This was a result of stumbling upon 436 | [math.stackexchange.com][] the previous night, which used MathJax to 437 | render mathematical formulas in a web browser. Thanks to that chance 438 | encounter with MathJax, the rest of that Saturday night was spent 439 | coding a new mathematics pastebin using MathJax and PHP. After coding 440 | through the night, registering a domain name, and setting up a 441 | website, MathB.in was released early Sunday morning. 442 | 443 | Over the years, MathB.in received occasional refinements. It was 444 | eventually rewritten in Common Lisp, replacing its original PHP 445 | implementation. For a detailed account of its evolution, see the post 446 | [MathB.in Turns Ten][]. 447 | 448 | MathB.in remained a useful tool for IRC communities, students, 449 | educators, and collaborators for 13 years. It was eventually shut 450 | down on Sunday, 16 March 2025. At the time of its discontinuation, it 451 | held the distinction of being the longest-running mathematics 452 | pastebin. For more details about why this service was discontinued, 453 | see the post [MathB.in Is Shutting Down][]. 454 | 455 | Although the original MathB.in service no longer exists, its source 456 | code is still available as free and open-source software through this 457 | project. 458 | 459 | [math.stackexchange.com]: https://math.stackexchange.com/ 460 | [MathB.in Turns Ten]: https://susam.net/blog/mathbin-turns-ten.html 461 | [MathB.in Is Shutting Down]: https://susam.net/blog/mathbin-is-shutting-down.html 462 | 463 | 464 | License 465 | ------- 466 | 467 | This is free and open source software. You can use, copy, modify, 468 | merge, publish, distribute, sublicense, and/or sell copies of it, 469 | under the terms of the MIT License. See [LICENSE.md][L] for details. 470 | 471 | This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, 472 | express or implied. See [LICENSE.md][L] for details. 473 | 474 | [L]: LICENSE.md 475 | 476 | 477 | Support 478 | ------- 479 | 480 | To report bugs, suggest improvements, or ask questions, [create 481 | issues][issues]. 482 | 483 | [issues]: https://github.com/susam/mathb/issues 484 | 485 | 486 | Channels 487 | -------- 488 | 489 | The author of this project hangs out at the following places online: 490 | 491 | - Website: [susam.net](https://susam.net) 492 | - GitHub: [@susam](https://github.com/susam) on GitHub 493 | - Mastodon: [@susam@mastodon.social](https://mastodon.social/@susam) 494 | 495 | You are welcome to subscribe to, follow, or join one or more of the 496 | above channels to receive updates from the author or ask questions 497 | about this project. 498 | 499 | 500 | More 501 | ---- 502 | 503 | If you like this project, check out related projects 504 | [TeXMe](https://github.com/susam/texme) and 505 | [Muboard](https://github.com/susam/muboard). 506 | -------------------------------------------------------------------------------- /mathb.lisp: -------------------------------------------------------------------------------- 1 | ;;;; MathB - A Mathematics Pastebin that Powers MathB.in 2 | ;;;; =================================================== 3 | 4 | 5 | (ql:quickload "hunchentoot") 6 | (require "uiop") 7 | 8 | 9 | ;;; Special Modes 10 | ;;; ------------- 11 | 12 | (defvar *log-mode* t 13 | "Write logs iff true.") 14 | 15 | (defvar *main-mode* t 16 | "Run main function iff true.") 17 | 18 | 19 | ;;; General Definitions 20 | ;;; ------------------- 21 | 22 | (defun universal-time-string (universal-time-seconds) 23 | "Return given universal time in yyyy-mm-dd HH:MM:SS +0000 format." 24 | (multiple-value-bind (sec min hour date month year) 25 | (decode-universal-time universal-time-seconds 0) 26 | (format nil "~4,'0d-~2,'0d-~2,'0d ~2,'0d:~2,'0d:~2,'0d +0000" 27 | year month date hour min sec))) 28 | 29 | (defun current-utc-time-string () 30 | "Return current UTC date and time in yyyy-mm-dd HH:MM:SS +0000 format." 31 | (universal-time-string (get-universal-time))) 32 | 33 | (defun directory-exists-p (path) 34 | "Check whether the specified directory exists on the filesystem." 35 | (uiop:directory-exists-p path)) 36 | 37 | (defun make-directory (path) 38 | "Create a new directory along with its parents." 39 | (ensure-directories-exist path)) 40 | 41 | (defun read-file (filename) 42 | "Read file and close the file." 43 | (uiop:read-file-string filename)) 44 | 45 | (defun write-file (filename text) 46 | "Write text to file and close the file." 47 | (make-directory filename) 48 | (with-open-file (f filename :direction :output :if-exists :supersede) 49 | (write-sequence text f))) 50 | 51 | (defun append-file (filename text) 52 | "Append text to file and close the file." 53 | (make-directory filename) 54 | (with-open-file (f filename :direction :output 55 | :if-exists :append 56 | :if-does-not-exist :create) 57 | (write-sequence text f))) 58 | 59 | (defun real-ip () 60 | "Return address of the remote client (not of the local reverse-proxy)." 61 | (hunchentoot:real-remote-addr)) 62 | 63 | (defun string-starts-with (prefix string) 64 | "Test that string starts with the given prefix." 65 | (and (<= (length prefix) (length string)) 66 | (string= prefix string :end2 (length prefix)))) 67 | 68 | (defun string-replace (old new string) 69 | "Replace non-empty old substring in string with new substring." 70 | (with-output-to-string (s) 71 | (let* ((next-index 0) 72 | (match-index)) 73 | (loop 74 | (setf match-index (search old string :start2 next-index)) 75 | (unless match-index 76 | (format s "~a" (subseq string next-index)) 77 | (return)) 78 | (format s "~a~a" (subseq string next-index match-index) new) 79 | (setf next-index (+ match-index (length old))))))) 80 | 81 | (defun string-trim-whitespace (s) 82 | "Trim whitespace from given string." 83 | (string-trim '(#\Space #\Tab #\Return #\Newline) s)) 84 | 85 | (defun fix-lines (s) 86 | "Remove carriage returns from string." 87 | (remove #\Return s)) 88 | 89 | (defun weekday-name (weekday-index) 90 | "Given an index, return the corresponding day of week." 91 | (nth weekday-index '("Mon" "Tue" "Wed" "Thu" "Fri" "Sat" "Sun"))) 92 | 93 | (defun month-name (month-number) 94 | "Given a number, return the corresponding month." 95 | (nth month-number '("X" "Jan" "Feb" "Mar" "Apr" "May" "Jun" 96 | "Jul" "Aug" "Sep" "Oct" "Nov" "Dec"))) 97 | 98 | (defun decode-weekday-name (year month date) 99 | "Given a date, return the day of week." 100 | (let* ((encoded-time (encode-universal-time 0 0 0 date month year)) 101 | (decoded-week (nth-value 6 (decode-universal-time encoded-time))) 102 | (weekday-name (weekday-name decoded-week))) 103 | weekday-name)) 104 | 105 | (defun simple-date (date-string) 106 | "Convert yyyy-mm-dd HH:MM:SS TZ to simple date." 107 | (let* ((year (parse-integer (subseq date-string 0 4))) 108 | (month (parse-integer (subseq date-string 5 7))) 109 | (date (parse-integer (subseq date-string 8 10))) 110 | (hour (parse-integer (subseq date-string 11 13))) 111 | (minute (parse-integer (subseq date-string 14 16))) 112 | (month-name (month-name month)) 113 | (weekday-name (decode-weekday-name year month date))) 114 | (format nil "~a, ~2,'0d ~a ~4,'0d ~2,'0d:~2,'0d GMT" 115 | weekday-name date month-name year hour minute))) 116 | 117 | (defun alist-get (key alist) 118 | "Given a key, return its value found in the list of parameters." 119 | (cdr (assoc key alist :test #'string=))) 120 | 121 | 122 | ;;; Tool Definitions 123 | ;;; ---------------- 124 | 125 | (defvar *data-directory* "/opt/data/mathb/" 126 | "Directory where post files and data are written to and read from.") 127 | 128 | (defvar *log-directory* "/opt/log/mathb/" 129 | "Directory where log files are written to.") 130 | 131 | (defun log-file-path () 132 | "Return path to the log file." 133 | (format nil "~amathb.log" *log-directory*)) 134 | 135 | (defun write-log (fmt &rest args) 136 | "Log message with specified arguments." 137 | (when *log-mode* 138 | (append-file (log-file-path) 139 | (with-output-to-string (s) 140 | (format s "~a - [~a] \"~a ~a\" " 141 | (real-ip) 142 | (current-utc-time-string) 143 | (hunchentoot:request-method*) 144 | (hunchentoot:request-uri*)) 145 | (apply #'format s fmt args) 146 | (terpri s))))) 147 | 148 | (defun lock (directory) 149 | "Acquire lock for specified directory and return t iff successful." 150 | (let ((lock-dir (merge-pathnames "lock/" directory)) 151 | (failures 0) 152 | (max-failures 10) 153 | (status)) 154 | (loop 155 | (when (nth-value 1 (ensure-directories-exist lock-dir)) 156 | (write-log "Acquired lock") 157 | (setf status t) 158 | (return)) 159 | (incf failures) 160 | (write-log "Could not acquire lock ~a after ~a of ~a attempts" 161 | lock-dir failures max-failures) 162 | (when (= failures max-failures) 163 | (write-log "Failed to acquire lock") 164 | (return)) 165 | (sleep 0.2)) 166 | status)) 167 | 168 | (defun unlock (directory) 169 | "Release lock for specified directory." 170 | (uiop:delete-empty-directory (merge-pathnames "lock/" directory)) 171 | (write-log "Released lock")) 172 | 173 | (defun read-options (directory) 174 | "Read options file." 175 | (let ((path (merge-pathnames "opt.lisp" directory))) 176 | (when (probe-file path) 177 | (read-from-string (read-file path))))) 178 | 179 | (defun from-post (name) 180 | "Get the value of a POST parameter." 181 | (hunchentoot:post-parameter name)) 182 | 183 | (defun home-request-p (request) 184 | "Return true iff the home page is requested." 185 | (and (member (hunchentoot:request-method request) '(:head :get)) 186 | (string= (hunchentoot:script-name request) "/"))) 187 | 188 | (defun meta-request-p (request) 189 | "Return true iff the meta page is requested." 190 | (and (member (hunchentoot:request-method request) '(:head :get)) 191 | (string= (hunchentoot:script-name request) "/0"))) 192 | 193 | (defun math-request-p (request) 194 | "Return true iff a mathematics post is requested." 195 | (and (member (hunchentoot:request-method request) '(:head :get)) 196 | (string/= (hunchentoot:script-name request) "/") 197 | (string/= (hunchentoot:script-name request) "/0") 198 | (every #'digit-char-p (subseq (hunchentoot:script-name request) 1)))) 199 | 200 | (defun post-request-p (request) 201 | "Return true iff a post submission has been made." 202 | (and (member (hunchentoot:request-method request) '(:post)) 203 | (or (string= (hunchentoot:script-name request) "/") 204 | (every #'digit-char-p (subseq (hunchentoot:script-name request) 1))))) 205 | 206 | (defun slug-to-path (directory slug) 207 | "Convert a slug to path, e.g., 1234567 to /directory/post/1/1234/1234567.txt" 208 | (let* ((short-prefix (floor slug 1000000)) 209 | (long-prefix (floor slug 1000))) 210 | (format nil "~apost/~d/~d/~d.txt" directory short-prefix long-prefix slug))) 211 | 212 | (defun split-text (text) 213 | "Split text into head and body." 214 | (let ((delimiter-index (search (format nil "~%~%") text))) 215 | (values (subseq text 0 (1+ delimiter-index)) 216 | (subseq text (+ 2 delimiter-index))))) 217 | 218 | (defun parse-headers (head) 219 | "Parse head content into an alist of key-value pairs." 220 | (let ((next-index 0) 221 | (headers)) 222 | (loop 223 | (let* ((sep-index (search ":" head :start2 next-index)) 224 | (end-index (search (format nil "~%") head :start2 sep-index)) 225 | (key (subseq head next-index sep-index)) 226 | (tail (subseq head (1+ sep-index) end-index)) 227 | (value (if (string= tail "") "" (subseq tail 1)))) 228 | (push (cons key value) headers) 229 | (setf next-index (1+ end-index)) 230 | (when (= next-index (length head)) 231 | (return)))) 232 | headers)) 233 | 234 | (defun parse-text (text) 235 | "Parse text into an alist of headers and body." 236 | (multiple-value-bind (head body) (split-text text) 237 | (let ((headers (parse-headers head))) 238 | (values (alist-get "Date" headers) 239 | (alist-get "Title" headers) 240 | (alist-get "Name" headers) 241 | body)))) 242 | 243 | (defun read-slug (directory) 244 | "Read slug if it exists." 245 | (let ((filename (merge-pathnames "slug.txt" directory))) 246 | (when (probe-file filename) 247 | (parse-integer (read-file filename))))) 248 | 249 | (defun valid-slug (new-slug protected-slug) 250 | "Check if new slug is protected." 251 | (if (<= new-slug protected-slug) 252 | (write-log "Slug ~a is protected" new-slug) 253 | t)) 254 | 255 | (defun increment-slug (directory protected-slug) 256 | "Acquire data lock, increment slug, and return t iff successful." 257 | (let ((filename (merge-pathnames "slug.txt" directory)) 258 | (slug 0)) 259 | (when (lock directory) 260 | (when (probe-file filename) 261 | (setf slug (parse-integer (read-file filename)))) 262 | (incf slug) 263 | (when (<= slug protected-slug) 264 | (write-log "Cannot use protected slug ~a" slug) 265 | (setf slug nil)) 266 | (when slug 267 | (write-file filename (format nil "~a~%" slug))) 268 | (unlock directory) 269 | slug))) 270 | 271 | (defun meta-code (directory last-post-time flood-table) 272 | "Return post code for meta page." 273 | (let ((slug (read-slug directory)) 274 | (options (read-options directory))) 275 | (format nil "- Current time: ~a 276 | - Last post time: ~a 277 | - Last post slug: [~a](~a) 278 | - Flood table size: ~a 279 | - Options: ~a" 280 | (current-utc-time-string) 281 | (universal-time-string last-post-time) 282 | slug slug 283 | (hash-table-count flood-table) 284 | (floor (length options) 2)))) 285 | 286 | (defun header-value (s) 287 | "Format header value to make is suitable for writing to text file." 288 | (if (string= s "") "" (format nil " ~a" s))) 289 | 290 | (defun make-text (date title name code) 291 | "Convert post fields to text to be written to post file." 292 | (format nil "Date:~a~%Title:~a~%Name:~a~%~%~a~%" 293 | (header-value date) (header-value title) (header-value name) code)) 294 | 295 | (defun render-html (html date title name code error class) 296 | "Render HTML for a page." 297 | (setf html (string-replace "{{ date }}" date html)) 298 | (setf html (string-replace "{{ title }}" title html)) 299 | (setf html (string-replace "{{ name }}" name html)) 300 | (setf html (string-replace "{{ code }}" code html)) 301 | (setf html (string-replace "{{ error }}" error html)) 302 | (setf html (string-replace "{{ class }}" class html))) 303 | 304 | (defun error-html (reason) 305 | "Return HTML for an error message." 306 | (format nil "
ERROR: ~a
" reason)) 307 | 308 | 309 | ;;; Post Control 310 | ;;; ------------ 311 | 312 | (defvar *last-post-time* 0 313 | "The universal-time at which the last post was successfully submitted.") 314 | 315 | (defvar *flood-table* (make-hash-table :test #'equal :synchronized t) 316 | "A map of IP addresses to last time they made a successful post.") 317 | 318 | (defmacro set-flood-data (ip current-time last-post-time-var flood-table-var) 319 | "Update flood control state variables." 320 | `(progn 321 | (setf ,last-post-time-var ,current-time) 322 | (setf (gethash ,ip ,flood-table-var) ,current-time))) 323 | 324 | (defun accept-post (ip current-time directory slug title name code) 325 | "Accept post, update flood data, and redirect client to saved post." 326 | (write-file (slug-to-path directory slug) 327 | (make-text (universal-time-string current-time) title name code)) 328 | (set-flood-data ip current-time *last-post-time* *flood-table*) 329 | (write-log "Post ~a written successfully" slug) 330 | (hunchentoot:redirect (format nil "/~a" slug))) 331 | 332 | (defun reject-post (title name code reason) 333 | "Reject post with an error message." 334 | (let ((html (read-file "web/html/mathb.html"))) 335 | (write-log "Post rejected: ~a" reason) 336 | (render-html html "" title name code (error-html reason) ""))) 337 | 338 | (defun process-post (options ip current-time directory title name code) 339 | "Process post and either accept it or reject it." 340 | (let ((slug (increment-slug directory (getf options :protect 0)))) 341 | (cond ((not slug) 342 | (reject-post title name code "Cannot make slug!")) 343 | (t 344 | (accept-post ip current-time directory slug title name code))))) 345 | 346 | 347 | ;;; Validators 348 | ;;; ---------- 349 | 350 | (defun bad-length-p (string min-length max-length) 351 | "Check if the length of the given string is within a specified range." 352 | (cond ((< (length string) min-length) 353 | (format nil "must contain at least ~a character~a" 354 | min-length (if (= 1 min-length) "" "s"))) 355 | ((> (length string) max-length) 356 | (format nil "must not contain more than ~a character~a" 357 | max-length (if (= 1 max-length) "" "s"))))) 358 | 359 | (defun improper-code-p (options code) 360 | "Check if expected tokens are missing from submitted code." 361 | (let ((tokens (getf options :expect))) 362 | (and tokens 363 | (notany (lambda (token) (search token code)) tokens)))) 364 | 365 | (defun dodgy-content-p (options title name code) 366 | "Check if post content contains banned words." 367 | (let ((words (getf options :block)) 368 | (text (format nil "~a:~a:~a" title name code))) 369 | (some (lambda (word) (search word text)) words))) 370 | 371 | (defun calc-token (a) 372 | "Calculate token from given integer." 373 | (let ((b (mod a 91)) 374 | (c (mod a 87))) 375 | (+ (* 1000000 a) (* 1000 b) c))) 376 | 377 | (defun dodgy-post-p (token) 378 | "Check if post content contains invalid token." 379 | (let* ((digits (and (string/= token "") (every #'digit-char-p token))) 380 | (x (if digits (parse-integer token) 0)) 381 | (a (floor x 1000000)) 382 | (xx (calc-token a))) 383 | (or (< x 123) (/= x xx)))) 384 | 385 | (defun dodgy-ip-p (options ip) 386 | "Check if given IP address is not allowed to post." 387 | (let ((prefixes (getf options :ban)) 388 | (formatted-ip (format nil "~a$" ip)) 389 | (result)) 390 | (dolist (prefix prefixes) 391 | (when (string-starts-with prefix formatted-ip) 392 | (setf result ip) 393 | (return))) 394 | result)) 395 | 396 | (defun global-flood-p (options current-time last-post-time) 397 | "Compute number of seconds before next post will be accepted." 398 | (let* ((post-interval (getf options :global-post-interval 0)) 399 | (wait-time (- (+ last-post-time post-interval) current-time))) 400 | (when (plusp wait-time) 401 | wait-time))) 402 | 403 | (defun client-flood-p (options ip current-time flood-table) 404 | "Compute number of seconds client must wait to avoid client flooding." 405 | (let ((post-interval (or (getf options :client-post-interval) 0))) 406 | (maphash #'(lambda (key value) 407 | (when (>= current-time (+ value post-interval)) 408 | (remhash key flood-table))) 409 | flood-table) 410 | (write-log "Flood table size is ~a" (hash-table-count flood-table)) 411 | (let* ((last-post-time (gethash ip flood-table 0)) 412 | (wait-time (- (+ last-post-time post-interval) current-time))) 413 | (when (plusp wait-time) 414 | wait-time)))) 415 | 416 | (defun reject-post-p (options ip current-time title name code token) 417 | "Validate post and return error message if validation fails." 418 | (let ((min-title-length (getf options :min-title-length 0)) 419 | (max-title-length (getf options :max-title-length 120)) 420 | (min-name-length (getf options :min-name-length 0)) 421 | (max-name-length (getf options :max-name-length 120)) 422 | (min-code-length (getf options :min-code-length 1)) 423 | (max-code-length (getf options :max-code-length 10000)) 424 | (result)) 425 | (cond ((getf options :lock-down) 426 | "Site is locked down!") 427 | ((getf options :read-only) 428 | "New posts have been disabled!") 429 | ((setf result (bad-length-p title min-title-length max-title-length)) 430 | (format nil "Title ~a!" result)) 431 | ((setf result (bad-length-p name min-name-length max-name-length)) 432 | (format nil "Name ~a!" result)) 433 | ((setf result (bad-length-p code min-code-length max-code-length)) 434 | (format nil "Code ~a!" result)) 435 | ((or (find #\Return title) (find #\Newline title)) 436 | (format nil "Title must not contain newline!")) 437 | ((or (find #\Return name) (find #\Newline name)) 438 | (format nil "Name must not contain newline!")) 439 | ((improper-code-p options code) 440 | "Improper code!") 441 | ((dodgy-content-p options title name code) 442 | "Dodgy content!") 443 | ((dodgy-post-p token) 444 | "Dodgy post!") 445 | ((setf result (dodgy-ip-p options ip)) 446 | (format nil "IP address ~a is banned!" result)) 447 | ((setf result (global-flood-p options current-time *last-post-time*)) 448 | (format nil "Wait for ~a s before submitting!" result)) 449 | ((setf result (client-flood-p options ip current-time *flood-table*)) 450 | (format nil "Wait for ~a s before resubmitting!" result))))) 451 | 452 | 453 | ;;; HTTP Request Handlers 454 | ;;; --------------------- 455 | 456 | (defun home-page () 457 | "Return HTML of the home page." 458 | (let ((html (read-file "web/html/mathb.html"))) 459 | (render-html html "" "" "" "" "" ""))) 460 | 461 | (defun meta-page (directory) 462 | "Return HTML of meta page." 463 | (let ((html (read-file "web/html/mathb.html")) 464 | (date (simple-date (current-utc-time-string))) 465 | (code (meta-code directory *last-post-time* *flood-table*)) 466 | (class " class=\"post\"")) 467 | (render-html html date "Metadata" "" code "" class))) 468 | 469 | (defun math-page (directory) 470 | "Return page to client." 471 | (let* ((html (read-file "web/html/mathb.html")) 472 | (options (read-options directory)) 473 | (slug (parse-integer (subseq (hunchentoot:script-name*) 1))) 474 | (path (slug-to-path directory slug)) 475 | (class " class=\"post\"")) 476 | (cond ((getf options :lock-down) 477 | (render-html html "" "" "" "" (error-html "Site is locked down!") "")) 478 | ((probe-file path) 479 | (multiple-value-bind (date title name code) (parse-text (read-file path)) 480 | (render-html html (simple-date date) title name code "" class))) 481 | (t 482 | (progn (setf (hunchentoot:return-code*) 404) nil))))) 483 | 484 | (defun post-response (directory) 485 | "Process submitted post form." 486 | (let* ((options (read-options directory)) 487 | (ip (real-ip)) 488 | (current-time (get-universal-time)) 489 | (title (string-trim-whitespace (or (from-post "title") ""))) 490 | (name (string-trim-whitespace (or (from-post "name") ""))) 491 | (code (string-trim-whitespace (fix-lines (or (from-post "code") "")))) 492 | (token (or (from-post "token") "")) 493 | (reject (reject-post-p options ip current-time title name code token))) 494 | (if reject 495 | (reject-post title name code reject) 496 | (process-post options ip current-time directory title name code)))) 497 | 498 | (defun define-handlers () 499 | "Define handlers for HTTP requests." 500 | (let ((directory *data-directory*)) 501 | (hunchentoot:define-easy-handler (home-handler :uri #'home-request-p) () 502 | (home-page)) 503 | (hunchentoot:define-easy-handler (meta-handler :uri #'meta-request-p) () 504 | (meta-page directory)) 505 | (hunchentoot:define-easy-handler (math-handler :uri #'math-request-p) () 506 | (math-page directory)) 507 | (hunchentoot:define-easy-handler (post-handler :uri #'post-request-p) () 508 | (post-response directory)))) 509 | 510 | (defclass custom-acceptor (hunchentoot:easy-acceptor) 511 | ()) 512 | 513 | (defmethod hunchentoot:acceptor-status-message 514 | ((acceptor custom-acceptor) http-status-code &key) 515 | "Custom error page." 516 | (let ((html (read-file "web/html/error.html")) 517 | (reason-phrase (hunchentoot:reason-phrase http-status-code))) 518 | (setf html (string-replace "{{ status-code }}" http-status-code html)) 519 | (setf html (string-replace "{{ reason-phrase }}" reason-phrase html)) 520 | html)) 521 | 522 | (defun start-server () 523 | "Start HTTP server." 524 | (let ((acceptor (make-instance 'custom-acceptor 525 | :address "127.0.0.1" 526 | :port 4242 527 | :access-log-destination (log-file-path)))) 528 | (setf (hunchentoot:acceptor-document-root acceptor) #p"_live/") 529 | (hunchentoot:start acceptor))) 530 | 531 | (defun main () 532 | "Set up HTTP request handlers and start server." 533 | (define-handlers) 534 | (start-server) 535 | (sleep most-positive-fixnum)) 536 | 537 | (when *main-mode* 538 | (main)) 539 | -------------------------------------------------------------------------------- /test.lisp: -------------------------------------------------------------------------------- 1 | ;;;; Tests 2 | ;;;; ===== 3 | 4 | (require "uiop") 5 | 6 | 7 | ;;; Test Definitions 8 | ;;; ---------------- 9 | 10 | (defparameter *pass* 0) 11 | (defparameter *fail* 0) 12 | (defvar *quit* nil) 13 | 14 | (defun remove-directory (path) 15 | "Remove the specified directory tree from the file system." 16 | (uiop:delete-directory-tree (pathname path) :validate t 17 | :if-does-not-exist :ignore)) 18 | 19 | (defmacro test-case (name &body body) 20 | "Execute a test case and print pass or fail status." 21 | `(progn 22 | (remove-directory #p"test-tmp/") 23 | (ensure-directories-exist #p"test-tmp/") 24 | (let ((test-name (string-downcase ',name))) 25 | (format t "~&~a: " test-name) 26 | (handler-case (progn ,@body) 27 | (:no-error (c) 28 | (declare (ignore c)) 29 | (incf *pass*) 30 | (format t "pass~%")) 31 | (error (c) 32 | (incf *fail*) 33 | (format t "FAIL~%") 34 | (format t "~& ~a: error: ~a~%" test-name c))) 35 | (remove-directory #p"test-tmp/")))) 36 | 37 | (defmacro test-case! (name &body body) 38 | "Execute a test case and error out on failure." 39 | `(progn 40 | (remove-directory #p"test-tmp/") 41 | (ensure-directories-exist #p"test-tmp/") 42 | (let ((test-name (string-downcase ',name))) 43 | (format t "~&~a: " test-name) 44 | ,@body 45 | (incf *pass*) 46 | (format t "pass!~%") 47 | (remove-directory #p"test-tmp/")))) 48 | 49 | (defun test-done () 50 | "Print test statistics." 51 | (format t "~&~%PASS: ~a~%" *pass*) 52 | (when (plusp *fail*) 53 | (format t "~&FAIL: ~a~%" *fail*)) 54 | (when *quit* 55 | (format t "~&~%quitting ...~%~%") 56 | (uiop:quit (if (zerop *fail*) 0 1)))) 57 | 58 | 59 | ;;; Begin Test Cases 60 | ;;; ---------------- 61 | 62 | (defvar *log-mode* nil) 63 | (defvar *main-mode* nil) 64 | (setf *log-mode* nil) 65 | (setf *main-mode* nil) 66 | (load "mathb.lisp") 67 | 68 | 69 | ;;; Test Mocks 70 | ;;; ---------- 71 | 72 | (defclass mock-request () 73 | ((method :initarg :method :reader hunchentoot:request-method) 74 | (script-name :initarg :script-name :reader hunchentoot:script-name))) 75 | 76 | (defun make-mock-request (method script-name) 77 | (make-instance 'mock-request :method method :script-name script-name)) 78 | 79 | 80 | ;;; Test Cases for Reusable Definitions 81 | ;;; ----------------------------------- 82 | 83 | (test-case universal-time-string 84 | (string= (universal-time-string 0) "1900-01-01 00:00:00 +0000") 85 | (string= (universal-time-string 1) "1900-01-01 00:00:01 +0000") 86 | (string= (universal-time-string 86399) "1900-01-01 23:59:59 +0000") 87 | (string= (universal-time-string 86400) "1900-01-02 00:00:00 +0000") 88 | (string= (universal-time-string 86401) "1900-01-02 00:00:01 +0000") 89 | (string= (universal-time-string 3541622400) "2012-03-25 00:00:00 +0000")) 90 | 91 | (test-case make-directory 92 | (make-directory "test-tmp/foo/bar/") 93 | (assert (directory-exists-p "test-tmp/foo/bar/"))) 94 | 95 | (test-case remove-directory 96 | (make-directory "test-tmp/foo/bar/") 97 | (assert (directory-exists-p "test-tmp/foo/bar/")) 98 | (remove-directory "test-tmp/foo/") 99 | (assert (not (directory-exists-p "test-tmp/foo/")))) 100 | 101 | (test-case read-write-file-single-line 102 | (let ((text "foo")) 103 | (write-file "test-tmp/foo.txt" text) 104 | (assert (string= (read-file "test-tmp/foo.txt") text)))) 105 | 106 | (test-case read-write-file-multiple-lines 107 | (let ((text (format nil "foo~%bar~%baz~%"))) 108 | (write-file "test-tmp/foo.txt" text) 109 | (assert (string= (read-file "test-tmp/foo.txt") text)))) 110 | 111 | (test-case read-write-file-nested-directories 112 | (write-file "test-tmp/foo/bar/baz/qux.txt" "foo") 113 | (assert (string= (read-file "test-tmp/foo/bar/baz/qux.txt") "foo"))) 114 | 115 | (test-case append-file-single-line 116 | (append-file "test-tmp/foo.txt" "foo") 117 | (assert (string= (read-file "test-tmp/foo.txt") "foo"))) 118 | 119 | (test-case append-file-multiple-lines 120 | (append-file "test-tmp/foo.txt" "foo") 121 | (append-file "test-tmp/foo.txt" (format nil "bar~%")) 122 | (append-file "test-tmp/foo.txt" "baz") 123 | (append-file "test-tmp/foo.txt" (format nil "qux~%")) 124 | (assert (string= (read-file "test-tmp/foo.txt") 125 | (format nil "foobar~%bazqux~%")))) 126 | 127 | (test-case append-file-nested-directories 128 | (append-file "test-tmp/foo/bar/baz/qux.txt" "foo") 129 | (append-file "test-tmp/foo/bar/baz/qux.txt" "bar") 130 | (assert (string= (read-file "test-tmp/foo/bar/baz/qux.txt") "foobar"))) 131 | 132 | (test-case write-log 133 | (write-log "~a, ~a" "hello" "world")) 134 | 135 | (test-case string-starts-with 136 | (assert (eq (string-starts-with "" "") t)) 137 | (assert (eq (string-starts-with "foo" "foo") t)) 138 | (assert (eq (string-starts-with "foo" "foobar") t)) 139 | (assert (eq (string-starts-with "foo" "bazfoobar") nil)) 140 | (assert (eq (string-starts-with "foo" "fo") nil)) 141 | (assert (eq (string-starts-with "foo" "fox") nil)) 142 | (assert (eq (string-starts-with "foo" "foO") nil))) 143 | 144 | (test-case string-replace-single 145 | (assert (string= (string-replace "foo" "foo" "foo") "foo")) 146 | (assert (string= (string-replace "foo" "bar" "") "")) 147 | (assert (string= (string-replace "foo" "bar" "foo") "bar")) 148 | (assert (string= (string-replace "foo" "bar" "foofoo") "barbar")) 149 | (assert (string= (string-replace "foo" "bar" "foo foo") "bar bar"))) 150 | 151 | (test-case string-replace-multiple 152 | (assert (string= (string-replace "foo" "x" "foo:foo") "x:x")) 153 | (assert (string= (string-replace "foo" "x" "foo:foo:") "x:x:"))) 154 | 155 | (test-case string-trim-whitespace 156 | (assert (string= (string-trim-whitespace "") "")) 157 | (assert (string= (string-trim-whitespace " ") "")) 158 | (assert (string= (string-trim-whitespace " x ") "x")) 159 | (assert (string= (string-trim-whitespace (format nil "~%x~%")) "x")) 160 | (assert (string= (string-trim-whitespace (format nil "x~a" #\Tab)) "x")) 161 | (assert (string= (string-trim-whitespace (format nil "x~a" #\Return)) "x")) 162 | (assert (string= (string-trim-whitespace (format nil "x~a" #\Newline)) "x"))) 163 | 164 | (test-case fix-lines 165 | (assert (string= (fix-lines (format nil "")) "")) 166 | (assert (string= (fix-lines (format nil "~c" #\Return)) "")) 167 | (assert (string= (fix-lines (format nil "~c~%" #\Return)) (format nil "~%"))) 168 | (assert (string= (fix-lines (format nil "a~c" #\Return)) (format nil "a"))) 169 | (assert (string= (fix-lines (format nil "foo~%")) (format nil "foo~%")))) 170 | 171 | (test-case weekday-name 172 | (assert (string= (weekday-name 0) "Mon")) 173 | (assert (string= (weekday-name 1) "Tue")) 174 | (assert (string= (weekday-name 2) "Wed")) 175 | (assert (string= (weekday-name 3) "Thu")) 176 | (assert (string= (weekday-name 4) "Fri")) 177 | (assert (string= (weekday-name 5) "Sat")) 178 | (assert (string= (weekday-name 6) "Sun"))) 179 | 180 | (test-case month-name 181 | (assert (string= (month-name 1) "Jan")) 182 | (assert (string= (month-name 2) "Feb")) 183 | (assert (string= (month-name 3) "Mar")) 184 | (assert (string= (month-name 4) "Apr")) 185 | (assert (string= (month-name 5) "May")) 186 | (assert (string= (month-name 6) "Jun")) 187 | (assert (string= (month-name 7) "Jul")) 188 | (assert (string= (month-name 8) "Aug")) 189 | (assert (string= (month-name 9) "Sep")) 190 | (assert (string= (month-name 10) "Oct")) 191 | (assert (string= (month-name 11) "Nov")) 192 | (assert (string= (month-name 12) "Dec"))) 193 | 194 | (test-case decode-weekday-name 195 | (assert (string= (decode-weekday-name 2019 01 07) "Mon")) 196 | (assert (string= (decode-weekday-name 2019 03 05) "Tue")) 197 | (assert (string= (decode-weekday-name 2020 01 01) "Wed")) 198 | (assert (string= (decode-weekday-name 2020 02 27) "Thu")) 199 | (assert (string= (decode-weekday-name 2020 02 28) "Fri")) 200 | (assert (string= (decode-weekday-name 2020 02 29) "Sat")) 201 | (assert (string= (decode-weekday-name 2020 03 01) "Sun"))) 202 | 203 | (test-case simple-date 204 | (assert (string= (simple-date "2012-03-25 00:00:00 +0000") 205 | "Sun, 25 Mar 2012 00:00 GMT")) 206 | (assert (string= (simple-date "2022-08-01 09:10:11 +0000") 207 | "Mon, 01 Aug 2022 09:10 GMT"))) 208 | 209 | (test-case alist-get 210 | (assert (not (alist-get nil nil))) 211 | (assert (not (alist-get "a" nil))) 212 | (assert (not (alist-get "" '(("a" . "apple") ("b" . "ball"))))) 213 | (assert (string= (alist-get "a" '(("a" . "apple") ("b" . "ball"))) "apple"))) 214 | 215 | (test-case lock-behaviour 216 | (assert (lock "test-tmp/")) 217 | (assert (not (lock "test-tmp/"))) 218 | (unlock "test-tmp/") 219 | (assert (lock "test-tmp/")) 220 | (unlock "test-tmp/")) 221 | 222 | (test-case lock-implementation 223 | (lock "test-tmp/") 224 | (assert (directory-exists-p "test-tmp/lock/")) 225 | (unlock "test-tmp/") 226 | (assert (not (directory-exists-p "test-tmp/lock/")))) 227 | 228 | 229 | ;;; Test Cases for Tool Definitions 230 | ;;; ------------------------------- 231 | 232 | (test-case home-request-p 233 | (assert (home-request-p (make-mock-request :get "/"))) 234 | (assert (home-request-p (make-mock-request :head "/"))) 235 | (assert (not (home-request-p (make-mock-request :post "/")))) 236 | (assert (not (home-request-p (make-mock-request :get "/foo")))) 237 | (assert (not (home-request-p (make-mock-request :head "/foo")))) 238 | (assert (not (home-request-p (make-mock-request :post "/foo"))))) 239 | 240 | (test-case meta-request-p 241 | (assert (meta-request-p (make-mock-request :get "/0"))) 242 | (assert (meta-request-p (make-mock-request :head "/0"))) 243 | (assert (not (meta-request-p (make-mock-request :post "/0")))) 244 | (assert (not (meta-request-p (make-mock-request :get "/")))) 245 | (assert (not (meta-request-p (make-mock-request :head "/")))) 246 | (assert (not (meta-request-p (make-mock-request :post "/")))) 247 | (assert (not (meta-request-p (make-mock-request :get "/-1")))) 248 | (assert (not (meta-request-p (make-mock-request :head "/-1")))) 249 | (assert (not (meta-request-p (make-mock-request :post "/-1")))) 250 | (assert (not (meta-request-p (make-mock-request :get "/123")))) 251 | (assert (not (meta-request-p (make-mock-request :post "/123")))) 252 | (assert (not (meta-request-p (make-mock-request :post "/123"))))) 253 | 254 | (test-case math-request-p 255 | (assert (math-request-p (make-mock-request :get "/1"))) 256 | (assert (math-request-p (make-mock-request :head "/1"))) 257 | (assert (math-request-p (make-mock-request :get "/123"))) 258 | (assert (math-request-p (make-mock-request :head "/123"))) 259 | (assert (not (math-request-p (make-mock-request :post "/123")))) 260 | (assert (not (math-request-p (make-mock-request :get "/")))) 261 | (assert (not (math-request-p (make-mock-request :head "/")))) 262 | (assert (not (math-request-p (make-mock-request :post "/")))) 263 | (assert (not (math-request-p (make-mock-request :get "/0")))) 264 | (assert (not (math-request-p (make-mock-request :head "/0")))) 265 | (assert (not (math-request-p (make-mock-request :post "/0")))) 266 | (assert (not (math-request-p (make-mock-request :get "/-1")))) 267 | (assert (not (math-request-p (make-mock-request :head "/-1")))) 268 | (assert (not (math-request-p (make-mock-request :post "/-1"))))) 269 | 270 | (test-case post-request-p 271 | (assert (not (post-request-p (make-mock-request :get "/")))) 272 | (assert (not (post-request-p (make-mock-request :head "/")))) 273 | (assert (post-request-p (make-mock-request :post "/"))) 274 | (assert (not (post-request-p (make-mock-request :get "/foo")))) 275 | (assert (not (post-request-p (make-mock-request :head "/foo")))) 276 | (assert (not (post-request-p (make-mock-request :post "/foo"))))) 277 | 278 | (test-case slug-to-path 279 | (assert (string= (slug-to-path "/x/" 1) "/x/post/0/0/1.txt")) 280 | (assert (string= (slug-to-path "/x/" 12) "/x/post/0/0/12.txt")) 281 | (assert (string= (slug-to-path "/x/" 123) "/x/post/0/0/123.txt")) 282 | (assert (string= (slug-to-path "/x/" 1234) "/x/post/0/1/1234.txt")) 283 | (assert (string= (slug-to-path "/x/" 12345) "/x/post/0/12/12345.txt")) 284 | (assert (string= (slug-to-path "/x/" 123456) "/x/post/0/123/123456.txt")) 285 | (assert (string= (slug-to-path "/x/" 1234567) "/x/post/1/1234/1234567.txt")) 286 | (assert (string= (slug-to-path "/x/" 12345678) "/x/post/12/12345/12345678.txt"))) 287 | 288 | (test-case split-text 289 | (let ((text (format nil "foo~%~%"))) 290 | (multiple-value-bind (head body) (split-text text) 291 | (assert (string= head (format nil "foo~%"))) 292 | (assert (string= body (format nil ""))))) 293 | (let ((text (format nil "foo~%~%bar"))) 294 | (multiple-value-bind (head body) (split-text text) 295 | (assert (string= head (format nil "foo~%"))) 296 | (assert (string= body (format nil "bar"))))) 297 | (let ((text (format nil "foo~%~%bar~%"))) 298 | (multiple-value-bind (head body) (split-text text) 299 | (assert (string= head (format nil "foo~%"))) 300 | (assert (string= body (format nil "bar~%"))))) 301 | (let ((text (format nil "foo~%~%~%bar~%"))) 302 | (multiple-value-bind (head body) (split-text text) 303 | (assert (string= head (format nil "foo~%"))) 304 | (assert (string= body (format nil "~%bar~%"))))) 305 | (let ((text (format nil "foo~%bar~%baz~%~%quux~%quuz~%"))) 306 | (multiple-value-bind (head body) (split-text text) 307 | (assert (string= head (format nil "foo~%bar~%baz~%"))) 308 | (assert (string= body (format nil "quux~%quuz~%")))))) 309 | 310 | (test-case parse-headers 311 | (assert (equal (parse-headers (format nil "a: apple~%")) 312 | (list (cons "a" "apple")))) 313 | (assert (equal (parse-headers (format nil "a: apple~%b: ball~%")) 314 | (list (cons "b" "ball") (cons "a" "apple")))) 315 | (assert (equal (parse-headers (format nil "a: apple~%b: ball~%")) 316 | (list (cons "b" "ball") (cons "a" "apple")))) 317 | (assert (equal (parse-headers (format nil "a:~%")) 318 | (list (cons "a" "")))) 319 | (assert (equal (parse-headers (format nil "a:~%b:~%c: cat~%")) 320 | (list (cons "c" "cat") (cons "b" "") (cons "a" "")))) 321 | (assert (equal (parse-headers (format nil "a: ~%b: ~%c: cat~%")) 322 | (list (cons "c" "cat") (cons "b" "") (cons "a" ""))))) 323 | 324 | (test-case parse-text 325 | (let ((text (format nil "Date: 2012-03-25 00:00:00 +0000~%~%Foo"))) 326 | (multiple-value-bind (date title name body) (parse-text text) 327 | (assert (string= date "2012-03-25 00:00:00 +0000")) 328 | (assert (not title)) 329 | (assert (not name)) 330 | (assert (string= body "Foo")))) 331 | (let ((text "Date: 2012-03-25 00:00:00 +0000 332 | Title: Hello World 333 | Name: Alice 334 | 335 | Foo 336 | Bar")) 337 | (multiple-value-bind (date title name body) (parse-text text) 338 | (assert (string= date "2012-03-25 00:00:00 +0000")) 339 | (assert (string= title "Hello World")) 340 | (assert (string= name "Alice")) 341 | (assert (string= body (format nil "Foo~%Bar")))))) 342 | 343 | (test-case increment-slug 344 | (assert (= (increment-slug "test-tmp/" 0) 1)) 345 | (assert (= (increment-slug "test-tmp/" 0) 2)) 346 | (assert (= (increment-slug "test-tmp/" 0) 3)) 347 | (lock "test-tmp/") 348 | (assert (not (increment-slug "test-tmp/" 0))) 349 | (unlock "test-tmp/") 350 | (assert (= (increment-slug "test-tmp/" 0) 4)) 351 | (assert (= (increment-slug "test-tmp/" 0) 5))) 352 | 353 | (test-case increment-slug-protected 354 | (assert (= (increment-slug "test-tmp/" 0) 1)) 355 | (assert (= (increment-slug "test-tmp/" 1) 2)) 356 | (assert (not (increment-slug "test-tmp/" 3))) 357 | (assert (not (increment-slug "test-tmp/" 4))) 358 | (assert (= (increment-slug "test-tmp/" 2) 3))) 359 | 360 | (test-case make-text 361 | (assert (string= (make-text "" "" "" "") 362 | (format nil "Date:~%Title:~%Name:~%~%~%"))) 363 | (assert (string= (make-text "date" "" "" "") 364 | (format nil "Date: date~%Title:~%Name:~%~%~%"))) 365 | (assert (string= (make-text "" "title" "" "") 366 | (format nil "Date:~%Title: title~%Name:~%~%~%"))) 367 | (assert (string= (make-text "" "" "name" "") 368 | (format nil "Date:~%Title:~%Name: name~%~%~%"))) 369 | (assert (string= (make-text "" "" "" "code") 370 | (format nil "Date:~%Title:~%Name:~%~%code~%"))) 371 | (assert (string= (make-text "date" "title" "name" "body") 372 | (format nil "Date: date~%Title: title~%Name: name~%~%body~%")))) 373 | 374 | (test-case set-flood-data 375 | (let ((x 0) 376 | (y (make-hash-table :test #'equal))) 377 | (set-flood-data "ip1" 1000 x y) 378 | (assert (= x 1000)) 379 | (assert (= (gethash "ip1" y) 1000)))) 380 | 381 | (test-case improper-code-p 382 | (assert (not (improper-code-p nil "foo"))) 383 | (assert (not (improper-code-p '(:expect ("foo")) "foo"))) 384 | (assert (not (improper-code-p '(:expect ("foo" "bar")) "foo"))) 385 | (assert (not (improper-code-p '(:expect ("foo" "bar")) "bar"))) 386 | (assert (not (improper-code-p '(:expect ("foo" "bar")) "foobarbaz"))) 387 | (assert (improper-code-p '(:expect ("bar")) "foo")) 388 | (assert (improper-code-p '(:expect ("bar" "baz")) "foo"))) 389 | 390 | (test-case dodgy-content-p 391 | (assert (not (dodgy-content-p nil "foo" "bar" "qux"))) 392 | (assert (not (dodgy-content-p '(:block ("quux")) "foo" "bar" "qux"))) 393 | (assert (not (dodgy-content-p '(:block ("bar")) "b" "a" "r"))) 394 | (assert (dodgy-content-p '(:block ("foo")) "foo" "bar" "qux")) 395 | (assert (dodgy-content-p '(:block ("bar")) "foo" "bar" "qux")) 396 | (assert (dodgy-content-p '(:block ("qux")) "foo" "bar" "qux")) 397 | (assert (dodgy-content-p '(:block ("bar")) "foobarqux" "" "")) 398 | (assert (dodgy-content-p '(:block ("bar")) "" "foobarqux" "")) 399 | (assert (dodgy-content-p '(:block ("bar")) "" "" "foobarqux")) 400 | (assert (dodgy-content-p '(:block ("bar")) "" "" "foobarqux")) 401 | (assert (dodgy-content-p '(:block ("foo" "bar" "baz")) "foo" "" "")) 402 | (assert (dodgy-content-p '(:block ("foo" "bar" "baz")) "" "bar" "")) 403 | (assert (dodgy-content-p '(:block ("foo" "bar" "baz")) "" "" "baz")) 404 | (assert (dodgy-content-p '(:block ("foo" "bar" "baz")) "foobarbaz" "" "")) 405 | (assert (dodgy-content-p '(:block ("foo" "bar" "baz")) "" "foobarbaz" "")) 406 | (assert (dodgy-content-p '(:block ("foo" "bar" "baz")) "" "" "foobarbaz"))) 407 | 408 | (test-case dodgy-ip-p 409 | (assert (not (dodgy-ip-p nil "ip1"))) 410 | (assert (not (dodgy-ip-p '(:ban nil) "ip1"))) 411 | (assert (not (dodgy-ip-p '(:ban ("ip1$")) "ip12"))) 412 | (assert (not (dodgy-ip-p '(:ban ("ip2")) "ip1"))) 413 | (assert (not (dodgy-ip-p '(:ban ("ip2" "ip3")) "ip1"))) 414 | (assert (not (dodgy-ip-p '(:ban ("ip1" "ip2")) "xip123"))) 415 | (assert (string= (dodgy-ip-p '(:ban ("ip1")) "ip1") "ip1")) 416 | (assert (string= (dodgy-ip-p '(:ban ("ip1$")) "ip1") "ip1")) 417 | (assert (string= (dodgy-ip-p '(:ban ("ip1")) "ip12") "ip12")) 418 | (assert (string= (dodgy-ip-p '(:ban ("ip1")) "ip123") "ip123")) 419 | (assert (string= (dodgy-ip-p '(:ban ("ip1" "ip2" "ip3$")) "ip1") "ip1")) 420 | (assert (string= (dodgy-ip-p '(:ban ("ip1" "ip2" "ip3$")) "ip2") "ip2")) 421 | (assert (string= (dodgy-ip-p '(:ban ("ip1" "ip2" "ip3$")) "ip3") "ip3"))) 422 | 423 | (test-case client-flood-p-no-options 424 | (let ((table (make-hash-table :test #'equal))) 425 | (assert (not (client-flood-p nil "ip1" 1000 table))))) 426 | 427 | (test-case client-flood-p-no-interval 428 | (let ((table (make-hash-table :test #'equal)) 429 | (options '(:client-post-interval nil))) 430 | (assert (not (client-flood-p options "ip1" 1000 table))))) 431 | 432 | (test-case client-flood-p-one-client 433 | (let ((table (make-hash-table :test #'equal)) 434 | (options '(:client-post-interval 10))) 435 | (assert (not (client-flood-p options "ip1" 1000 table))) 436 | (setf (gethash "ip1" table) 1000) 437 | (assert (= (client-flood-p options "ip1" 1001 table) 9)) 438 | (assert (= (client-flood-p options "ip1" 1005 table) 5)) 439 | (assert (= (client-flood-p options "ip1" 1009 table) 1)) 440 | (assert (not (client-flood-p options "ip1" 1010 table))))) 441 | 442 | (test-case client-flood-p-two-clients 443 | (let ((table (make-hash-table :test #'equal)) 444 | (options '(:client-post-interval 10))) 445 | (assert (not (client-flood-p options "ip1" 1000 table))) 446 | (assert (not (client-flood-p options "ip2" 1003 table))) 447 | (setf (gethash "ip1" table) 1000) 448 | (setf (gethash "ip2" table) 1003) 449 | (assert (= (client-flood-p options "ip1" 1001 table) 9)) 450 | (assert (= (client-flood-p options "ip1" 1005 table) 5)) 451 | (assert (= (client-flood-p options "ip1" 1009 table) 1)) 452 | (assert (not (client-flood-p options "ip1" 1010 table))) 453 | (assert (= (client-flood-p options "ip2" 1001 table) 12)) 454 | (assert (= (client-flood-p options "ip2" 1005 table) 8)) 455 | (assert (= (client-flood-p options "ip2" 1009 table) 4)) 456 | (assert (not (client-flood-p options "ip2" 1013 table))))) 457 | 458 | (test-case reject-post-p-good 459 | (let ((x (write-to-string (calc-token 123)))) 460 | (assert (not (reject-post-p nil "ip1" 0 "foo" "bar" "baz" x))) 461 | (assert (not (reject-post-p '(:block ("quux")) "ip1" 0 "foo" "bar" "baz" x))))) 462 | 463 | (test-case reject-post-p-read-only 464 | (let ((x (write-to-string (calc-token 123)))) 465 | (assert (string= (reject-post-p '(:read-only t) "ip1" 0 "foo" "bar" "baz" x) 466 | "New posts have been disabled temporarily!")))) 467 | 468 | (test-case reject-post-p-title-length 469 | (let ((x (write-to-string (calc-token 123))) 470 | (opt '(:min-title-length 2 :max-title-length 4))) 471 | (assert (not (reject-post-p nil "ip1" 0 "" "bar" "baz" x))) 472 | (assert (string= (reject-post-p opt "ip1" 0 "" "bar" "baz" x) 473 | "Title must contain at least 2 characters!")) 474 | (assert (string= (reject-post-p opt "ip1" 0 "a" "bar" "baz" x) 475 | "Title must contain at least 2 characters!")) 476 | (assert (not (reject-post-p opt "ip1" 0 "ab" "bar" "baz" x))) 477 | (assert (not (reject-post-p opt "ip1" 0 "abcd" "bar" "baz" x))) 478 | (assert (string= (reject-post-p opt "ip1" 0 "abcde" "bar" "baz" x) 479 | "Title must not contain more than 4 characters!")))) 480 | 481 | (test-case reject-post-p-name-length 482 | (let ((x (write-to-string (calc-token 123))) 483 | (opt '(:min-name-length 2 :max-name-length 4))) 484 | (assert (not (reject-post-p nil "ip1" 0 "foo" "" "baz" x))) 485 | (assert (string= (reject-post-p opt "ip1" 0 "foo" "" "baz" x) 486 | "Name must contain at least 2 characters!")) 487 | (assert (string= (reject-post-p opt "ip1" 0 "foo" "a" "baz" x) 488 | "Name must contain at least 2 characters!")) 489 | (assert (not (reject-post-p opt "ip1" 0 "foo" "ab" "baz" x))) 490 | (assert (not (reject-post-p opt "ip1" 0 "foo" "abcd" "baz" x))) 491 | (assert (string= (reject-post-p opt "ip1" 0 "foo" "abcde" "baz" x) 492 | "Name must not contain more than 4 characters!")))) 493 | 494 | (test-case reject-post-p-code-length 495 | (let ((x (write-to-string (calc-token 123))) 496 | (opt '(:min-code-length 2 :max-code-length 4))) 497 | (assert (string= (reject-post-p nil "ip1" 0 "foo" "bar" "" x) 498 | "Code must contain at least 1 character!")) 499 | (assert (string= (reject-post-p opt "ip1" 0 "foo" "bar" "" x) 500 | "Code must contain at least 2 characters!")) 501 | (assert (string= (reject-post-p opt "ip1" 0 "foo" "bar" "a" x) 502 | "Code must contain at least 2 characters!")) 503 | (assert (not (reject-post-p opt "ip1" 0 "foo" "bar" "ab" x))) 504 | (assert (not (reject-post-p opt "ip1" 0 "foo" "bar" "abcd" x))) 505 | (assert (string= (reject-post-p opt "ip1" 0 "foo" "bar" "abcde" x) 506 | "Code must not contain more than 4 characters!")))) 507 | 508 | (test-case reject-post-p-title-has-line-break 509 | (let ((x (write-to-string (calc-token 123))) 510 | (err "Title must not contain newline!") 511 | (title)) 512 | (setf title (format nil "foo~c" #\Return)) 513 | (assert (string= (reject-post-p nil "ip1" 0 title "" "bar" x) err)) 514 | (setf title (format nil "foo~c" #\Newline)) 515 | (assert (string= (reject-post-p nil "ip1" 0 title "" "bar" x) err)))) 516 | 517 | (test-case reject-post-p-name-has-line-break 518 | (let ((x (write-to-string (calc-token 123))) 519 | (err "Name must not contain newline!") 520 | (name)) 521 | (setf name (format nil "foo~c" #\Return)) 522 | (assert (string= (reject-post-p nil "ip1" 0 "" name "bar" x) err)) 523 | (setf name (format nil "foo~c" #\Newline)) 524 | (assert (string= (reject-post-p nil "ip1" 0 "" name "bar" x) err)))) 525 | 526 | (test-case reject-post-p-expect 527 | (let ((x (write-to-string (calc-token 123)))) 528 | (assert (string= (reject-post-p '(:expect ("foo")) "ip1" 0 "" "" "bar" x) 529 | "Improper code!")))) 530 | 531 | (test-case reject-post-p-block 532 | (let ((x (write-to-string (calc-token 123)))) 533 | (assert (string= (reject-post-p '(:block ("xy")) "ip1" 0 "xy" "yz" "zx" x) 534 | "Dodgy content!")))) 535 | 536 | (test-case reject-post-p-ban 537 | (let ((x (write-to-string (calc-token 123)))) 538 | (assert (string= (reject-post-p '(:ban ("ip1")) "ip1xy" 0 "xy" "yz" "zx" x) 539 | "IP address ip1xy is banned!")))) 540 | 541 | (test-case reject-post-p-client-post-interval 542 | (let ((options '(:client-post-interval 10)) 543 | (x (write-to-string (calc-token 123))) 544 | (msg "Wait for ~a s before resubmitting!")) 545 | (assert (not (reject-post-p options "ip1" 1000 "foo" "bar" "baz" x))) 546 | (setf (gethash "ip1" *flood-table*) 1000) 547 | (assert (string= (reject-post-p options "ip1" 1000 "foo" "bar" "baz" x) 548 | (format nil msg 10))) 549 | (assert (string= (reject-post-p options "ip1" 1001 "foo" "bar" "baz" x) 550 | (format nil msg 9)))) 551 | (clrhash *flood-table*)) 552 | 553 | ;; End test cases. 554 | (test-done) 555 | --------------------------------------------------------------------------------