Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
63a2547
mv: move default banner to banners sub priority
perdedora Jun 19, 2025
3625846
config.php: add permissions, template, and default banner size
perdedora Jun 19, 2025
0797940
pages.php: add upload banner logic
perdedora Jun 19, 2025
df61213
banners.html: new template to manage banners in use
perdedora Jun 19, 2025
2f032e0
dashboard.html: add links to banners
perdedora Jun 19, 2025
f08b883
mod.php: add route
perdedora Jun 19, 2025
338b630
BannersService.php: add service to handle serving banners
perdedora Jun 19, 2025
8ea9ec2
context.php: update context to include new service
perdedora Jun 19, 2025
6e56103
b.php: modify current banner script to use new service
perdedora Jun 19, 2025
b86d6f5
config.php: add config to include ukko in banner
perdedora Jun 19, 2025
18a55c5
index.php and thread.php: modify html to use new banner
perdedora Jun 19, 2025
7414a2c
ukko: add template variable to indicate is running on ukko instead of…
perdedora Jun 19, 2025
52fa1d9
config.php: add config to configure allowed exts
perdedora Jun 20, 2025
2652334
pages.php: use config for allowed banner extensions
perdedora Jun 20, 2025
76e4c35
BannersService.php: use config for allowed banner extension
perdedora Jun 20, 2025
35cbf8e
BannersService.php: add logging
perdedora Jun 20, 2025
a87dbbd
b.php: remove try/catch
perdedora Jun 20, 2025
e7d48e5
context.php: update dependencies of service
perdedora Jun 20, 2025
5d0ae26
pages.php: if mkdir fails, display error
perdedora Jun 20, 2025
19bdee1
BannersService.php: remove caching
perdedora Jun 20, 2025
14da62d
context.php: remove caching from depedencies
perdedora Jun 20, 2025
1c1472c
BannersService.php: cleanup use cachedriver
perdedora Jun 20, 2025
df58710
ImageType.php: add known web-ready image extensions
Zankaria Nov 5, 2025
c0ac9df
config.php: remove banner_ext option
Zankaria Nov 5, 2025
53dec25
context.php: remove banner_ext option
Zankaria Nov 5, 2025
ea6654d
BannersService.php: use statically known web-ready image types
Zankaria Nov 5, 2025
cb84372
BannersService.php: refactor to minimize IO system calls
Zankaria Nov 5, 2025
f39f9bf
BannersService.php: use redirect trick to maximize client cache usage
Zankaria Nov 5, 2025
14c6d69
config.php: add priority banner likelihood denominator option.
Zankaria Nov 6, 2025
b1e0d99
BannersService.php: add argument to control priority banners likelihood
Zankaria Nov 6, 2025
039b73f
context.php: handle priority banner likelihood option
Zankaria Nov 6, 2025
b9c29c2
Merge pull request #1 from Zankaria/banners-8chan-patch
perdedora Nov 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions b.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
<?php

$files = scandir('static/banners/', SCANDIR_SORT_NONE);
$files = array_diff($files, ['.', '..']);
use Vichan\Service\BannersService;

$name = $files[array_rand($files)];
header("Location: /static/banners/$name", true, 307);
header('Cache-Control: no-cache');
require_once 'inc/bootstrap.php';
use function Vichan\build_context;

$board = htmlspecialchars($_GET['board'] ?? $config['banner_ukko'], ENT_QUOTES, 'UTF-8');
$ctx = build_context($config);
$banners = $ctx->get(BannersService::class);
$banners->serve($board);
10 changes: 10 additions & 0 deletions inc/Data/Model/ImageType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php
namespace Vichan\Data\Model;

class ImageType {
/**
* Known image types which are web-compatible, extensions
* @var array
*/
public const KNOWN_WEB_IMAGE_EXT = [ 'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp' ];
}
99 changes: 99 additions & 0 deletions inc/Service/BannersService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

namespace Vichan\Service;

use Vichan\Data\Driver\LogDriver;
use Vichan\Data\Model\ImageType;


class BannersService {
private const BANNERS_DIR = 'static/banners/';
private const PRIORITY_DIR = 'static/banners_priority/';
private LogDriver $logger;
private int $priority_denominator;

private static function isImage(string $fileName): bool {
// For speed reasons, we trust the extension.
$extension = \strtolower(\pathinfo($fileName, PATHINFO_EXTENSION));
return \in_array($extension, ImageType::KNOWN_WEB_IMAGE_EXT, true);
}

private static function getFilesInDirectory(string $dir): array {
return \array_diff(\scandir($dir, SCANDIR_SORT_NONE), ['.', '..']);
}

private static function serveBanner(string $filePath): void {
header("Location: $filePath", true, 307);
header('Cache-Control: no-cache');
exit;
}

/**
* @param LogDriver $logger Driver to write logs
* @param int $priority_denominator The denominator over the likelihood of a priory banner being chosen.
* Must be >= 0. Use 0 to disable priority banners (except as a fallback).
*/
public function __construct(LogDriver $logger, int $priority_denominator) {
$this->logger = $logger;
$this->priority_denominator = $priority_denominator;
}

/**
* Select a banner file to serve
* @param string $dir The directory the files belong to.
* @param array $fileNames The file names
* @return ?string Path to the selected file, if a suitable one is found.
*/
private function selectFile(string $dir, array $fileNames): ?string {
if (empty($fileNames)) {
return null;
}
$offset = \mt_rand(0, \count($fileNames));
for ($i = 0; $i < \count($fileNames); $i++) {
$j = ($offset + $i) % \count($fileNames);
$name = $fileNames[$j];
$filePath = $dir . $name;

if (!\is_file($filePath)) {
$this->logger->log(LogDriver::ERROR, "Banner '{$filePath}' is not file");
continue;
}
if (!\is_readable($filePath)) {
$this->logger->log(LogDriver::ERROR, "Banner '{$filePath}' is not readable");
continue;
}
if (!self::isImage($filePath)) {
$this->logger->log(LogDriver::ERROR, "Banner '{$filePath}' is not an valid image");
continue;
}
return $filePath;
}
return null;
}

public function serve(string $subdir): void {
$usePriority = empty($subdir) || ($this->priority_denominator > 0 && \mt_rand(0, $this->priority_denominator) === 0);

if (!$usePriority) {
$bannerDir = self::BANNERS_DIR . $subdir . '/';

if (\is_dir($bannerDir)) {
$names = self::getFilesInDirectory($bannerDir);
$filePath = $this->selectFile($bannerDir, $names);
if ($filePath !== null) {
self::serveBanner($filePath);
}
}
}

$names = self::getFilesInDirectory(self::PRIORITY_DIR);
$filePath = $this->selectFile(self::PRIORITY_DIR, $names);
if ($filePath !== null) {
self::serveBanner( $filePath);
} else {
$this->logger->log(LogDriver::ERROR, "No suitable image for banner found!");
\http_response_code(404);
exit;
}
}
}
11 changes: 11 additions & 0 deletions inc/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,13 @@
// Setting the banner dimensions stops the page shifting as it loads. If you have banners of various different sizes, unset these.
$config['banner_width'] = 300;
$config['banner_height'] = 100;
$config['banner_size'] = 3 * 1024 * 1024;
// Which board should we serve for ukko
$config['banner_ukko'] = 'ukko';
// Control the likelihood of a priority banner being served through the denominator.
// The chance is 1 / <value>.
// Set to 0 to disable priority banners and use them only as a fallback option.
$config['banner_priority_den'] = 4;

// Custom stylesheets available for the user to choose. See the "stylesheets/" folder for a list of
// available stylesheets (or create your own).
Expand Down Expand Up @@ -1460,6 +1467,8 @@
$config['file_mod_debug_recent_posts'] = 'mod/debug/recent_posts.html';
$config['file_mod_debug_sql'] = 'mod/debug/sql.html';

$config['file_mod_banners'] = 'mod/banners.html';

// Board directory, followed by a forward-slash (/).
$config['board_path'] = '%s/';
// Misc directories.
Expand Down Expand Up @@ -1869,6 +1878,8 @@
$config['mod']['recent'] = MOD;
// Create pages
$config['mod']['edit_pages'] = MOD;

$config['mod']['edit_banners'] = MOD;
$config['pages_max'] = 10;

// Config editor permissions
Expand Down
13 changes: 12 additions & 1 deletion inc/context.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Vichan\Data\Driver\{CacheDriver, HttpDriver, ErrorLogLogDriver, FileLogDriver, LogDriver, StderrLogDriver, SyslogLogDriver};
use Vichan\Data\Driver\Dns\{DnsDriver, HostDnsDriver, LibcDnsDriver};
use Vichan\Data\Queries\{FloodQueries, IpNoteQueries, UserPostQueries, ReportQueries};
use Vichan\Service\BannersService;
use Vichan\Service\FilterService;
use Vichan\Service\FloodService;
use Vichan\Service\HCaptchaQuery;
Expand Down Expand Up @@ -127,7 +128,17 @@ function build_context(array $config): Context {
$config['dnsbl_exceptions'],
$config['fcrdns']
);
}
},
BannersService::class => function(Context $c): BannersService {
$config = $c->get('config');
$den = $config['banner_priority_den'];
$den = \is_numeric($den) && ((int)$den) > 0 ? ((int)$den) : 0;

return new BannersService(
$c->get(LogDriver::class),
$den
);
},
]);
}

Expand Down
71 changes: 71 additions & 0 deletions inc/mod/pages.php
Original file line number Diff line number Diff line change
Expand Up @@ -3364,3 +3364,74 @@ function mod_debug_sql(Context $ctx) {

mod_page(_('Debug: SQL'), $config['file_mod_debug_sql'], $args, $mod);
}

function mod_banners(Context $ctx, $b) {
global $board, $mod;
$config = $ctx->get('config');

if (!hasPermission($config['mod']['edit_banners'], $b)) {
error($config['error']['noaccess']);
}

if ($b !== "banners_priority" && !openBoard($b)) {
error("Could not open board!");
}

$dir = ($b === "banners_priority") ? 'static/' . $b : 'static/banners/' . $b;

if (!is_dir($dir)) {
$ret = mkdir($dir, 0755, true);
if (!$ret) {
error(_('Failed to create folder ' . $b));
}
}

if (isset($_FILES['files'])){
foreach ($_FILES['files']['tmp_name'] as $index => $upload) {
if (!is_readable($upload)) {
error($config['error']['nomove']);
}

$id = time() . substr(microtime(), 2, 3);
$originalName = $_FILES['files']['name'][$index];
$extension = strtolower(pathinfo((string) $originalName, PATHINFO_EXTENSION));

if (!in_array($extension, $config['banner_ext'])) {
error(sprintf($config['error']['fileext'], $extension));
}

if (filesize($upload) > $config['banner_size']) {
error($config['error']['maxsize']);
}

$size = @getimagesize($upload);

if (!$size || $size[0] !== $config['banner_width'] || $size[1] !== $config['banner_height']) {
error(_('Wrong image size!'));
}

if (!move_uploaded_file($upload, "$dir/$id.$extension")) {
error('Failed to save uploaded file ' . $_FILES['files']['name'][$index]);
}
}
}

if (isset($_POST['delete'])) {
foreach ($_POST['delete'] as $fileName) {
if (!preg_match('/^\d+\.(png|jpeg|jpg|gif|webp)$/', (string) $fileName)) {
error(_('Nice try.'));
}
$filePath = "$dir/$fileName";
if (file_exists($filePath)) {
unlink($filePath);
}
}
}

$banners = array_diff(scandir($dir), ['..', '.']);
mod_page(_('Edit banners'), $config['file_mod_banners'], [
'board' => $board,
'banners' => $banners,
'token' => make_secure_link_token('banners/' . $b)
], $mod);
}
2 changes: 2 additions & 0 deletions mod.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ private function initializePages(): void {
'/themes/(\w+)/rebuild' => 'secure theme_rebuild', // rebuild theme
'/themes/(\w+)/uninstall' => 'secure theme_uninstall', // uninstall theme

'/banners/(\%b)' => 'secure_POST banners', // view/upload banners

'/config' => 'secure_POST config', // config editor
'/config/(\%b)' => 'secure_POST config', // config editor

Expand Down
11 changes: 10 additions & 1 deletion templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,16 @@
{{ boardlist.top }}

{% if pm %}<div class="top_notice">You have <a href="?/PM/{{ pm.id }}">an unread PM</a>{% if pm.waiting > 0 %}, plus {{ pm.waiting }} more waiting{% endif %}.</div><hr />{% endif %}
{% if config.url_banner %}<img class="board_image" src="{{ config.url_banner }}" {% if config.banner_width or config.banner_height %}style="{% if config.banner_width %}width:{{ config.banner_width }}px{% endif %};{% if config.banner_width %}height:{{ config.banner_height }}px{% endif %}" {% endif %}alt="" />{% endif %}
{% if config.url_banner %}
<a href="/" style="display: block; margin: 20px auto 0 auto;
{% if config.banner_width or config.banner_height %} width:{{ config.banner_width }}px; height:{{ config.banner_height }}px;{% endif %}">
<img class="board_image" src="{{ config.url_banner }}?board={{ not isukko ? board.uri|url_encode : config.banner_ukko|url_encode }}"
{% if config.banner_width or config.banner_height %}
style="width: {{ config.banner_width }}px; height: {{ config.banner_height }}px;"
{% endif %}
loading="lazy" />
</a>
{% endif %}

<header>
<h1>{{ board.url }} - {{ board.title|e }}</h1>
Expand Down
34 changes: 34 additions & 0 deletions templates/mod/banners.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<div style="text-align:center">
<form action="{{ action }}" method="post" enctype="multipart/form-data">
<input type="hidden" name="token" value="{{ token }}">
<h2>{% trans %}Upload banner{% endtrans %}</h2>
<p><input type="file" name="files[]" multiple></p>

{% set banner_size = config.banner_size|filesize %}
{% set banner_dimensions = config.banner_width ~ 'px x ' ~ config.banner_height ~ 'px' %}

<p>
<small>{% trans %}Banners must be &lt; {{ banner_size }} and have image dimensions {{ banner_dimensions }}{% endtrans %}<br/></small>
</p>

<p><input type="submit" value="{% trans 'Upload' %}"></p>
</form>
<hr>
<h2>{{ banners|length }}&nbsp;{% trans %}Banners already in use{% endtrans %}</h2>
<form action="{{ action }}" method="post">
<input type="hidden" name="token" value="{{ token }}">
<table>
<tbody>
{% for banner in banners %}
<tr>
<td><input name="delete[]" type="checkbox" value="{{ banner }}"></td>
<td>
<img src="static/{{ board.uri != '' ? 'banners/' ~ board.uri : 'banners_priority' }}/{{ banner }}" alt="{{ board.uri != '' ? 'Banner' : 'Priority Banner' }}">
</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><input type="submit" value="{% trans %}Delete selected{% endtrans %}"></p>
</form>
</div>
6 changes: 6 additions & 0 deletions templates/mod/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
{% if mod|hasPermission(config.mod.edit_pages) %}
<a href="?/edit_pages/{{ board.uri }}"><small>[{% trans 'pages' %}]</small></a>
{% endif %}
{% if mod|hasPermission(config.mod.edit_banners) %}
<a href="?/banners/{{ board.uri }}"><small>[{% trans %}banners{% endtrans %}]</small></a>
{% endif %}
</li>
{% endfor %}

Expand Down Expand Up @@ -80,6 +83,9 @@
<legend>{% trans 'Administration' %}</legend>

<ul>
{% if mod|hasPermission(config.mod.edit_banners) %}
<li><a href="?/banners/banners_priority">{% trans %}Banners General{% endtrans %}</a></li>
{% endif %}
{% if mod|hasPermission(config.mod.reports) %}
<li>
{% if reports > 0 %}<strong>{% endif %}
Expand Down
1 change: 1 addition & 0 deletions templates/themes/ukko/theme.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ public function build($mod = false) {
'no_post_form' => true,
'body' => $body,
'mod' => $mod,
'isukko' => true,
'boardlist' => createBoardlist($mod),
));
}
Expand Down
11 changes: 10 additions & 1 deletion templates/thread.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,16 @@
{{ boardlist.top }}
<a name="top"></a>
{% if pm %}<div class="top_notice">You have <a href="?/PM/{{ pm.id }}">an unread PM</a>{% if pm.waiting > 0 %}, plus {{ pm.waiting }} more waiting{% endif %}.</div><hr />{% endif %}
{% if config.url_banner %}<img class="board_image" src="{{ config.url_banner }}" {% if config.banner_width or config.banner_height %}style="{% if config.banner_width %}width:{{ config.banner_width }}px{% endif %};{% if config.banner_width %}height:{{ config.banner_height }}px{% endif %}" {% endif %}alt="" />{% endif %}
{% if config.url_banner %}
<a href="/" style="display: block; margin: 20px auto 0 auto;
{% if config.banner_width or config.banner_height %} width:{{ config.banner_width }}px; height:{{ config.banner_height }}px;{% endif %}">
<img class="board_image" src="{{ config.url_banner }}?board={{ not isukko ? board.uri|url_encode : config.banner_ukko|url_encode }}"
{% if config.banner_width or config.banner_height %}
style="width: {{ config.banner_width }}px; height: {{ config.banner_height }}px;"
{% endif %}
loading="lazy" />
</a>
{% endif %}
<header>
<h1>{{ board.url }} - {{ board.title|e }}</h1>
<div class="subtitle">
Expand Down