diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..8ae84728 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +**/.git +**/.gitignore +/local-instances +**/.gitkeep diff --git a/.gitignore b/.gitignore index 220b0e11..5e0ab052 100644 --- a/.gitignore +++ b/.gitignore @@ -44,5 +44,6 @@ Thumbs.db #vichan custom favicon.ico /static/spoiler.png +/local-instances /vendor/ diff --git a/README.md b/README.md index b1794df9..2c7001be 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,11 @@ WebM support ------------ Read `inc/lib/webm/README.md` for information about enabling webm. +Docker +------------ +Vichan comes with a Dockerfile and docker-compose configuration, the latter aimed primarily at development and testing. +See the `docker/doc.md` file for more information. + vichan API ---------- vichan provides by default a 4chan-compatible JSON API. For documentation on this, see: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..da45b113 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,40 @@ +services: + #nginx webserver + php 8.x + web: + build: + context: . + dockerfile: ./docker/nginx/Dockerfile + ports: + - "9090:80" + depends_on: + - db + volumes: + - ./local-instances/1/www:/var/www/html + - ./docker/nginx/vichan.conf:/etc/nginx/conf.d/default.conf + - ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf + - ./docker/nginx/proxy.conf:/etc/nginx/conf.d/proxy.conf + links: + - php + + php: + build: + context: . + dockerfile: ./docker/php/Dockerfile + volumes: + - ./local-instances/1/www:/var/www + - ./docker/php/www.conf:/usr/local/etc/php-fpm.d/www.conf + - ./docker/php/jit.ini:/usr/local/etc/php/conf.d/jit.ini + + #MySQL Service + db: + image: mysql:8.0.35 + container_name: db + restart: unless-stopped + tty: true + ports: + - "3306:3306" + environment: + MYSQL_DATABASE: vichan + MYSQL_ROOT_PASSWORD: password + volumes: + - ./local-instances/1/mysql:/var/lib/mysql diff --git a/docker/doc.md b/docker/doc.md new file mode 100644 index 00000000..e022f170 --- /dev/null +++ b/docker/doc.md @@ -0,0 +1,16 @@ +The `php-fpm` process runs containerized. +The php application always uses `/var/www` as it's work directory and home folder, and if `/var/www` is bind mounted it +is necessary to adjust the path passed via FastCGI to `php-fpm` by changing the root directory to `/var/www`. +This can achieved in nginx by setting the `fastcgi_param SCRIPT_FILENAME` to `/var/www/$fastcgi_script_name;` + +The default docker compose settings are intended for development and testing purposes. +The folder structure expected by compose is as follows + +``` + +└── local-instances + └── 1 + ├── mysql + └── www +``` +The vichan container is by itself much less rigid. diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile new file mode 100644 index 00000000..d9d4bcc4 --- /dev/null +++ b/docker/nginx/Dockerfile @@ -0,0 +1,8 @@ +FROM nginx:1.25.3-alpine + +COPY . /code +RUN adduser --system www-data \ + && adduser www-data www-data + +CMD [ "nginx", "-g", "daemon off;" ] +EXPOSE 80 diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf new file mode 100644 index 00000000..7c6b6587 --- /dev/null +++ b/docker/nginx/nginx.conf @@ -0,0 +1,34 @@ +# This and proxy.conf are based on +# https://github.com/dead-guru/devichan/blob/master/nginx/nginx.conf + +user www-data; +worker_processes auto; + +error_log /dev/stdout warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Switch logging to console out to view via Docker + access_log /dev/stdout; + error_log /dev/stdout warn; + sendfile on; + keepalive_timeout 5; + + gzip on; + gzip_http_version 1.0; + gzip_vary on; + gzip_comp_level 6; + gzip_types text/xml text/plain text/css application/xhtml+xml application/xml application/rss+xml application/atom_xml application/x-javascript application/x-httpd-php; + gzip_disable "MSIE [1-6]\."; + + + include /etc/nginx/conf.d/*.conf; + include /etc/nginx/sites-available/*.conf; +} \ No newline at end of file diff --git a/docker/nginx/proxy.conf b/docker/nginx/proxy.conf new file mode 100644 index 00000000..6830cd5f --- /dev/null +++ b/docker/nginx/proxy.conf @@ -0,0 +1,40 @@ +proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=czone:4m max_size=50m inactive=120m; +proxy_temp_path /var/tmp/nginx; +proxy_cache_key "$scheme://$host$request_uri"; + + +map $http_forwarded_request_id $x_request_id { + "" $request_id; + default $http_forwarded_request_id; +} + +map $http_forwarded_forwarded_host $forwardedhost { + "" $host; + default $http_forwarded_forwarded_host; +} + + +map $http_x_forwarded_proto $fcgi_https { + default ""; + https on; +} + +map $http_x_forwarded_proto $real_scheme { + default $scheme; + https https; +} + +proxy_set_header Host $host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-Host $host; +proxy_set_header X-Forwarded-Server $host; + +real_ip_header X-Forwarded-For; + +set_real_ip_from 10.0.0.0/8; +set_real_ip_from 172.16.0.0/12; +set_real_ip_from 172.18.0.0; +set_real_ip_from 192.168.0.0/24; +set_real_ip_from 127.0.0.0/8; + +real_ip_recursive on; \ No newline at end of file diff --git a/docker/nginx/vichan.conf b/docker/nginx/vichan.conf new file mode 100644 index 00000000..35f6bc08 --- /dev/null +++ b/docker/nginx/vichan.conf @@ -0,0 +1,66 @@ +upstream php-upstream { + server php:9000; +} + +server { + listen 80 default_server; + listen [::]:80 default_server ipv6only=on; + server_name vichan; + root /var/www/html; + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-Content-Type-Options "nosniff"; + + index index.html index.php; + + charset utf-8; + + location ~ ^([^.\?]*[^\/])$ { + try_files $uri @addslash; + } + + # Expire rules for static content + # Media: images, icons, video, audio, HTC + location ~* \.(?:jpg|jpeg|gif|png|webp|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ { + expires 1M; + access_log off; + log_not_found off; + add_header Cache-Control "public"; + } + # CSS and Javascript + location ~* \.(?:css|js)$ { + expires 1y; + access_log off; + log_not_found off; + add_header Cache-Control "public"; + } + + location ~* \.(html)$ { + expires -1; + } + + location @addslash { + return 301 $uri/; + } + + location / { + try_files $uri $uri/ /index.php$is_args$args; + } + + client_max_body_size 2G; + + location ~ \.php$ { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Request-Id $x_request_id; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header Forwarded-Request-Id $x_request_id; + fastcgi_pass php-upstream; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME /var/www/$fastcgi_script_name; + fastcgi_read_timeout 600; + include fastcgi_params; + } + + location = /favicon.ico { access_log off; log_not_found off; } + location = /robots.txt { access_log off; log_not_found off; } +} diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile new file mode 100644 index 00000000..0e2f741d --- /dev/null +++ b/docker/php/Dockerfile @@ -0,0 +1,87 @@ +# Based on https://github.com/dead-guru/devichan/blob/master/php-fpm/Dockerfile + +FROM composer AS composer +FROM php:8.1-fpm-alpine + +RUN apk add --no-cache \ + zlib \ + zlib-dev \ + libpng \ + libpng-dev \ + libjpeg-turbo \ + libjpeg-turbo-dev \ + libwebp \ + libwebp-dev \ + libcurl \ + curl-dev \ + imagemagick \ + graphicsmagick \ + gifsicle \ + ffmpeg \ + bind-tools \ + gettext \ + gettext-dev \ + icu-dev \ + oniguruma \ + oniguruma-dev \ + libmcrypt \ + libmcrypt-dev \ + lz4-libs \ + lz4-dev \ + imagemagick-dev \ + pcre-dev \ + $PHPIZE_DEPS \ + && docker-php-ext-configure gd \ + --with-webp=/usr/include/webp \ + --with-jpeg=/usr/include \ + && docker-php-ext-install -j$(nproc) \ + gd \ + curl \ + bcmath \ + opcache \ + pdo_mysql \ + gettext \ + intl \ + mbstring \ + && pecl update-channels \ + && pecl install -o -f igbinary \ + && pecl install redis \ + && pecl install imagick \ + $$ docker-php-ext-enable \ + igbinary \ + redis \ + imagick \ + && apk del \ + zlib-dev \ + libpng-dev \ + libjpeg-turbo-dev \ + libwebp-dev \ + curl-dev \ + gettext-dev \ + oniguruma-dev \ + libmcrypt-dev \ + lz4-dev \ + imagemagick-dev \ + pcre-dev \ + $PHPIZE_DEPS \ + && rm -rf /var/cache/* +RUN rmdir /var/www/html \ + && install -d -m 744 -o www-data -g www-data /var/www \ + && install -d -m 700 -o www-data -g www-data /var/tmp/vichan \ + && install -d -m 700 -o www-data -g www-data /var/cache/gen-cache \ + && install -d -m 700 -o www-data -g www-data /var/cache/template-cache + +# Copy the bootstrap script. +COPY ./docker/php/bootstrap.sh /usr/local/bin/bootstrap.sh + +COPY --from=composer /usr/bin/composer /usr/local/bin/composer + +# Copy the actual project (use .dockerignore to exclude stuff). +COPY . /code + +# Install the compose depedencies. +RUN cd /code && composer install + +WORKDIR "/var/www" +CMD [ "bootstrap.sh" ] +EXPOSE 9000 diff --git a/docker/php/Dockerfile.profile b/docker/php/Dockerfile.profile new file mode 100644 index 00000000..ad2019ab --- /dev/null +++ b/docker/php/Dockerfile.profile @@ -0,0 +1,16 @@ +# syntax = devthefuture/dockerfile-x +INCLUDE ./docker/php/Dockerfile + +RUN apk add --no-cache \ + linux-headers \ + $PHPIZE_DEPS \ + && pecl update-channels \ + && pecl install xdebug \ + && docker-php-ext-enable xdebug \ + && apk del \ + linux-headers \ + $PHPIZE_DEPS \ + && rm -rf /var/cache/* + +ENV XDEBUG_OUT_DIR=/var/www/xdebug_out +CMD [ "bootstrap.sh" ] \ No newline at end of file diff --git a/docker/php/bootstrap.sh b/docker/php/bootstrap.sh new file mode 100755 index 00000000..cc3f43d0 --- /dev/null +++ b/docker/php/bootstrap.sh @@ -0,0 +1,87 @@ +#!/bin/sh + +set -eu + +function set_cfg() { + if [ -L "/var/www/inc/$1" ]; then + echo "INFO: Resetting $1" + rm "/var/www/inc/$1" + cp "/code/inc/$1" "/var/www/inc/$1" + chown www-data "/var/www/inc/$1" + chgrp www-data "/var/www/inc/$1" + chmod 600 "/var/www/inc/$1" + else + echo "INFO: Using existing $1" + fi +} + +if ! mountpoint -q /var/www; then + echo "WARNING: '/var/www' is not a mountpoint. All the data will remain inside the container!" +fi + +if [ ! -w /var/www ] ; then + echo "ERROR: '/var/www' is not writable. Closing." + exit 1 +fi + +if [ -z "${XDEBUG_OUT_DIR:-''}" ] ; then + echo "INFO: Initializing xdebug out directory at $XDEBUG_OUT_DIR" + mkdir -p "$XDEBUG_OUT_DIR" + chown www-data "$XDEBUG_OUT_DIR" + chgrp www-data "$XDEBUG_OUT_DIR" + chmod 755 "$XDEBUG_OUT_DIR" +fi + +# Link the entrypoints from the exposed directory. +ln -nfs \ + /code/tools/ \ + /code/*.php \ + /code/LICENSE.* \ + /code/install.sql \ + /var/www/ +# Static files accessible from the webserver must be copied. +cp -ur /code/static /var/www/ +cp -ur /code/stylesheets /var/www/ + +# Ensure correct permissions are set, since this might be bind mount. +chown www-data /var/www +chgrp www-data /var/www + +# Initialize an empty robots.txt with the default if it doesn't exist. +touch /var/www/robots.txt + +# Link the cache and tmp files directory. +ln -nfs /var/tmp/vichan /var/www/tmp + +# Link the javascript directory. +ln -nfs /code/js /var/www/ + +# Link the html templates directory and it's cache. +ln -nfs /code/templates /var/www/ +ln -nfs -T /var/cache/template-cache /var/www/templates/cache +chown -h www-data /var/www/templates/cache +chgrp -h www-data /var/www/templates/cache + +# Link the generic cache. +ln -nfs -T /var/cache/gen-cache /var/www/tmp/cache +chown -h www-data /var/www/tmp/cache +chgrp -h www-data /var/www/tmp/cache + +# Create the included files directory and link them +install -d -m 700 -o www-data -g www-data /var/www/inc +for file in /code/inc/*; do + file="${file##*/}" + if [ ! -e /var/www/inc/$file ]; then + ln -s /code/inc/$file /var/www/inc/ + fi +done + +# Copy an empty instance configuration if the file is a link (it was linked because it did not exist before). +set_cfg 'instance-config.php' +set_cfg 'secrets.php' + +# Link the composer dependencies. +ln -nfs /code/vendor /var/www/ + +# Start the php-fpm server. +exec php-fpm diff --git a/docker/php/jit.ini b/docker/php/jit.ini new file mode 100644 index 00000000..ecfb44c5 --- /dev/null +++ b/docker/php/jit.ini @@ -0,0 +1,2 @@ +opcache.jit_buffer_size=192M +opcache.jit=tracing diff --git a/docker/php/www.conf b/docker/php/www.conf new file mode 100644 index 00000000..6e78ad26 --- /dev/null +++ b/docker/php/www.conf @@ -0,0 +1,13 @@ +[www] +access.log = /proc/self/fd/2 + +; Ensure worker stdout and stderr are sent to the main error log. +catch_workers_output = yes +decorate_workers_output = no + +user = www-data +group = www-data + +listen = 127.0.0.1:9000 +pm = static +pm.max_children = 16 diff --git a/docker/php/xdebug-prof.ini b/docker/php/xdebug-prof.ini new file mode 100644 index 00000000..c6dc008e --- /dev/null +++ b/docker/php/xdebug-prof.ini @@ -0,0 +1,7 @@ +zend_extension=xdebug + +[xdebug] +xdebug.mode = profile +xdebug.start_with_request = start +error_reporting = E_ALL +xdebug.output_dir = /var/www/xdebug_out diff --git a/inc/template.php b/inc/template.php index 0362111c..26ba6cc0 100644 --- a/inc/template.php +++ b/inc/template.php @@ -11,12 +11,14 @@ $twig = false; function load_twig() { global $twig, $config; + $cache_dir = "{$config['dir']['template']}/cache/"; + $loader = new Twig\Loader\FilesystemLoader($config['dir']['template']); $loader->setPaths($config['dir']['template']); $twig = new Twig\Environment($loader, array( 'autoescape' => false, - 'cache' => is_writable('templates') || (is_dir('templates/cache') && is_writable('templates/cache')) ? - new Twig_Cache_TinyboardFilesystem("{$config['dir']['template']}/cache") : false, + 'cache' => is_writable('templates/') || (is_dir($cache_dir) && is_writable($cache_dir)) ? + new Twig_Cache_TinyboardFilesystem($cache_dir) : false, 'debug' => $config['debug'], 'auto_reload' => $config['twig_auto_reload'] )); @@ -28,17 +30,17 @@ function load_twig() { function Element($templateFile, array $options) { global $config, $debug, $twig, $build_pages; - + if (!$twig) load_twig(); - + if (function_exists('create_pm_header') && ((isset($options['mod']) && $options['mod']) || isset($options['__mod'])) && !preg_match('!^mod/!', $templateFile)) { $options['pm'] = create_pm_header(); } - + if (isset($options['body']) && $config['debug']) { $_debug = $debug; - + if (isset($debug['start'])) { $_debug['time']['total'] = '~' . round((microtime(true) - $_debug['start']) * 1000, 2) . 'ms'; $_debug['time']['init'] = '~' . round(($_debug['start_debug'] - $_debug['start']) * 1000, 2) . 'ms'; @@ -56,15 +58,15 @@ function Element($templateFile, array $options) { str_replace("\n", '
', utf8tohtml(print_r($_debug, true))) . ''; } - + // Read the template file if (@file_get_contents("{$config['dir']['template']}/${templateFile}")) { $body = $twig->render($templateFile, $options); - + if ($config['minify_html'] && preg_match('/\.html$/', $templateFile)) { $body = trim(preg_replace("/[\t\r\n]/", '', $body)); } - + return $body; } else { throw new Exception("Template file '${templateFile}' does not exist or is empty in '{$config['dir']['template']}'!"); @@ -102,7 +104,7 @@ class Tinyboard extends Twig\Extension\AbstractExtension new Twig\TwigFilter('cloak_mask', 'cloak_mask'), ); } - + /** * Returns a list of functions to add to the existing list. * @@ -122,7 +124,7 @@ class Tinyboard extends Twig\Extension\AbstractExtension new Twig\TwigFunction('link_for', 'link_for') ); } - + /** * Returns the name of the extension. * @@ -154,7 +156,7 @@ function twig_hasPermission_filter($mod, $permission, $board = null) { function twig_extension_filter($value, $case_insensitive = true) { $ext = mb_substr($value, mb_strrpos($value, '.') + 1); if($case_insensitive) - $ext = mb_strtolower($ext); + $ext = mb_strtolower($ext); return $ext; } @@ -179,7 +181,7 @@ function twig_filename_truncate_filter($value, $length = 30, $separator = '…') $value = strrev($value); $array = array_reverse(explode(".", $value, 2)); $array = array_map("strrev", $array); - + $filename = &$array[0]; $extension = isset($array[1]) ? $array[1] : false; diff --git a/install.php b/install.php index e72d7117..eeab3b0b 100644 --- a/install.php +++ b/install.php @@ -5,6 +5,11 @@ define('VERSION', '5.1.4'); require 'inc/bootstrap.php'; loadConfig(); +if (!is_writable('inc/secrets.php') || !is_writable('inc/')) { + echo 'install.php does not have permission to write to /inc/secrets.php and/or /inc/, without permission the installer cannot continue'; + exit(); +} + // Salt generators class SaltGen { public $salt_length = 128; @@ -856,14 +861,14 @@ if ($step == 0) { array( 'category' => 'File permissions', 'name' => getcwd() . '/templates/cache', - 'result' => is_writable('templates') || (is_dir('templates/cache') && is_writable('templates/cache')), + 'result' => is_dir('templates/cache/') && is_writable('templates/cache/'), 'required' => true, 'message' => 'You must give vichan permission to create (and write to) the templates/cache directory or performance will be drastically reduced.' ), array( 'category' => 'File permissions', 'name' => getcwd() . '/tmp/cache', - 'result' => is_dir('tmp/cache') && is_writable('tmp/cache'), + 'result' => is_dir('tmp/cache/') && is_writable('tmp/cache/'), 'required' => true, 'message' => 'You must give vichan permission to write to the tmp/cache directory.' ), @@ -1036,4 +1041,3 @@ if ($step == 0) { echo Element('page.html', $page); } - diff --git a/templates/cache/.gitkeep b/templates/cache/.gitkeep deleted file mode 100644 index 8b137891..00000000 --- a/templates/cache/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -