このコミットが含まれているのは:
たかし 2024-01-04 14:41:55 +00:00
コミット 8ef1dae28a
28個のファイルの変更416行の追加407行の削除

ファイルの表示

@ -1,10 +1,13 @@
<?php
// 初期化
if (BIBIS_DEBUG) {
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
}
// 定数
define('GUEST_ID', '-');
define('NOREPLY_ID', '-');
define('GUESTNAME', 'GUEST');
@ -13,199 +16,15 @@ define('DELETEDTEXT', 'この書き込みは削除されました。');
define('ACCEPT_IMAGE_TYPE', ['image/png', 'image/jpeg', 'image/gif']);
define('SESSION_NAME', 'bibis');
// PHP の設定
date_default_timezone_set('UTC');
ini_set('zlib.output_compression', 1);
ini_set('session.use_strict_mode', 1);
if (OPEN_BASEDIR != '') {
ini_set('open_basedir', OPEN_BASEDIR);
}
// セッション開始
session_name(SESSION_NAME);
session_start(['cookie_httponly' => true]);
set_csrf_token();
// HTTP
function get_fp() {
return md5(
($_SERVER['HTTP_USER_AGENT'] ?? '')
. ($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '')
. ($_SERVER['HTTP_ACCEPT_ENCODING'] ?? '')
. ($_SERVER['HTTP_ACCEPT'] ?? '')
);
}
function has_cookie() {
return isset($_COOKIE['bibis']);
}
function on_error($code, $errors) {
http_response_code(400);
if (function_exists('bibis_http_header')) { bibis_http_header(); }
$view['errors'] = $errors;
require(__DIR__ . '/view/header.php');
exit;
}
function output_html($view, $_components) {
if (function_exists('bibis_http_header')) { bibis_http_header(); }
foreach ($_components as $_name) {
require(__DIR__ . "/view/$_name");
}
exit;
}
// CSRF token
function set_csrf_token() {
if (empty($_SESSION['csrf_token'])) {
$_SESSION["csrf_token"] = bin2hex(random_bytes(32));
}
}
function get_csrf_token() {
if (!REQUIRE_COOKIE && !isset($_SESSION['user'])) {
$date = date('YmdH');
return $date . get_fp();
}
return $_SESSION['csrf_token'] ?? null;
}
function get_csrf_token_2($force_cookie = false) {
if (!REQUIRE_COOKIE && !isset($_SESSION['user'])) {
$date = date('YmdH', strtotime('-1 hour'));
return $date . get_fp();
}
return $_SESSION['csrf_token'] ?? null;
}
function get_csrf_token_hashed($prefix = '', $token = null) {
if ($token === null) { $token = get_csrf_token(); }
if ($token === null) { return null; }
return md5($prefix . $token);
}
function check_csrf_token() {
$hashed = get_csrf_token_hashed();
$hashed_2 = get_csrf_token_hashed('', get_csrf_token_2());
if ($hashed === null || $hashed_2 === null) { return ['CSRF トークンが不正。要再試行。']; }
$token_key = get_csrf_token_hashed('csrf_token');
$csrf_token = $_POST[$token_key] ?? '';
if ($csrf_token !== $hashed && $csrf_token !== $hashed_2) { return ['CSRF トークンが不正。要再試行。']; }
return [];
}
function output_csrf_token_hidden() {
$hashed = get_csrf_token_hashed();
if ($hashed === null) { return ''; }
$token_key = get_csrf_token_hashed('csrf_token');
return '<input type="hidden" name="' . htmlspecialchars($token_key) . '" value="' . $hashed . '">';
}
// String
function anchor_to_link($s, $thread_id) {
return preg_replace(
'/&gt;&gt;([1-9]+[0-9]{0,10})/',
'<a href="' . sitebase('post/?id=') . $thread_id . '&amp;res=\1">&gt;&gt;\1</a>',
$s
);
}
function url_to_link($s) {
return preg_replace(
'/((http|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&amp;:\/~+#-]*[\w@?^=%&amp;\/~+#-])?)/',
'<a href="\1" rel="noopener noreferrer">\1</a>',
$s
);
}
function mbtrim($s) {
return preg_replace('/^\s+|\s+$/u', '', $s);
}
function twochan_trip($tripkey) {
// twochan_trip('atomikka') -> '◆.../5Betyws'
// Thanks: https://refirio.org/view/62
// Test data: https://trip-table.kokage.cc/sample01.php
$tripkey = mb_convert_encoding($tripkey, 'sjis-win');
$salt = substr($tripkey . 'H.', 1, 2);
$salt = preg_replace('/[^\.-z]/', '.', $salt);
$salt = strtr($salt, ':;<=>?@[\\]^_`', 'ABCDEFGabcdef');
$trip = crypt($tripkey, $salt);
$trip = substr($trip, -10);
return '◆' . $trip;
}
function parse_key_value($s, $key_list) {
$result = [];
$lines = explode(PHP_EOL, $s);
foreach ($lines as $line) {
foreach ($key_list as $key) {
if (strpos($line, "{$key}=") === 0) {
$value = explode("{$key}=", $line, 2)[1] ?? null;
$result[$key] = $value;
$value = null;
}
}
$key = null;
}
$lines = null;
$line = null;
return $result;
}
// File
function mkdir_p($path, $permission = 0700) {
if (file_exists($path)) { return true; }
return mkdir($path, $permission);
}
function get_image_type($s) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$type = finfo_buffer($finfo, $s);
if (isset($type) && in_array($type, ACCEPT_IMAGE_TYPE)) {
return $type;
}
return null;
}
function sitebase($path = '') {
return SITEBASE . $path;
}
function get_style_css() {
if (THEME === null || THEME === '') { return sitebase('style.css'); }
return sitebase('theme/' . THEME);
}
// Limit and permission
function is_logged_in() {
return isset($_SESSION['user']);
}
function can_post() {
if (!post_limited()) { return false; }
if (!ENABLE_GUEST && !is_logged_in()) { return false; }
return true;
}
function post_limited() {
return ENABLE_POST && (count_post_today() < POST_LIMIT_PER_DAY) && (count_post() < POST_LIMIT);
}
function can_regist() {
return ENABLE_REGISTER;
}

ファイルの表示

@ -1,5 +1,6 @@
<?php
// require: PASSWORD_SOLT, and PASSWORD_ITER, and DATA_ROOT
// config.php の項目に対するデフォルト値を定義する
// ただし次の項目は必須: PASSWORD_SOLT, and PASSWORD_ITER, and DATA_ROOT
default_config('OPEN_BASEDIR', null);
default_config('USERS_TSV', DATA_ROOT . 'users.tsv');

ファイルの表示

@ -1,5 +1,5 @@
<?php
// Data controller (post section)
// 投稿 (Post) 関連の関数
// [投稿ファイルについて]
// 投稿ファイルは POST_DIR (data/post) フォルダーに保存される。
@ -25,7 +25,7 @@
// [添附ファイルについて]
// 添附ファイルは ATTACHMENT_DIR (data/attachment) フォルダーに gzip 形式で保存される。
// ファイル名の形式: {id}.gz
/// ファイル名の形式: {id}.gz
// idにはランダムな英數字が入る。
// [聯投防止について]
@ -37,21 +37,20 @@
$post_title_cache = [];
function load_postfile($filepath) {
$text = file_get_contents($filepath);
$text = file_get_contents($filepath);
$split = preg_split('/^-$/m', $text, 2);
$text = null;
unset($text);
$head = mbtrim($split[0]);
$body = mbtrim($split[1] ?? '');
$split = null;
unset($split);
$parsed = parse_key_value($head, ['title', 'attachment_id']);
$head = null;
unset($head);
$title = $parsed['title'] ?? '';
$attachment_id = $parsed['attachment_id'] ?? '';
$parsed = null;
unset($parsed);
$deleted = $body === '';
if ($deleted) {
@ -75,8 +74,7 @@ function get_post_metadata($file, $deleted = false) {
$format = 'Y-m-d';
if (!HIDE_TIME || $is_future) { $format .= ' H:i'; }
$time = $date->format($format);
$format = null;
$date = null;
unset($format, $date);
// TODO:これだとファイル名がマッチしない時に予期せぬ結果になる
@ -88,22 +86,18 @@ function get_post_metadata($file, $deleted = false) {
if ($thread_id === NOREPLY_ID) { $thread_id = ''; }
// 投稿ユーザーID: ".us{$id}"
$userid = '';
$userid = GUEST_ID;
if (!$deleted) {
$userid = preg_replace('/.+\.us([^.]+)\..+/', '\1', $file);
}
if ($userid === GUEST_ID) {
$is_guest = $userid === GUEST_ID;
if ($is_guest) {
$userid = '';
}
$is_guest = $userid === '';
$thread_title = '';
if ($thread_id != '') {
if ($thread_id != '') {
$thread_title = load_post_title_by_id($thread_id);
}
if ($thread_id != '' && $thread_title == '') {
$thread_title = '無題#' . mb_substr($thread_id, 0, 7);
}
$detail_url = sitebase('post/?id=' . $id);
@ -123,18 +117,18 @@ function load_res_num($id) {
$file = basename($filepath);
$meta = get_post_metadata($file);
$thread_id = $meta['thread_id'];
$meta = null;
unset($meta);
if ($thread_id <= '') { return 1; }
$pattern = "2*Z*.th{$thread_id}.txt";
$list = glob(POST_DIR . $pattern);
$search = ".id{$id}.";
$i = 0;
foreach ($list as $i => $filepath) {
foreach ($list as $i => $filepath) {
if (strpos(basename($filepath), $search) > 0) {
return $i + 2;
}
}
}
return 1; // fallback
}
@ -147,14 +141,16 @@ function load_post($filepath) {
$title = $detail['title'];
$body_raw = $detail['body'];
$attachment_id = $detail['attachment_id'];
$detail = null;
unset($detail);
$metadata = get_post_metadata(basename($filepath), $deleted);
if ($metadata['thread_id'] == '' && $title == '') {
$title = post_default_title($metadata['id']);
}
$userid = $metadata['userid'];
$users = load_users();
$username = GUESTNAME;
$username = GUESTNAME;
if ($deleted) {
$username = DELETEDNAME;
}
@ -193,9 +189,9 @@ function load_post_by_id($id) {
}
function search_post($options = []) {
if (!file_exists(POST_DIR)) {
die(POST_DIR.': ディレクトリは存在しません。');
}
if (!file_exists(POST_DIR)) {
die(POST_DIR.': ディレクトリは存在しません。');
}
$includes_future = $options['includes_future'] ?? false;
$pagesize = $options['pagesize'] ?? 0;
@ -206,7 +202,7 @@ function search_post($options = []) {
$key = $options['key'] ?? '';
$value = $options['value'] ?? '';
$page = $options['page'] ?? 1;
$options = null;
unset($options);
$pattern = '2*Z*.txt';
if ($key === 'userid' && $value != '' && $value !== 'tl') {
@ -217,9 +213,7 @@ function search_post($options = []) {
$pattern = "2*Z*.th{$value}.txt";
$thread = true;
}
$key = null;
$value = null;
unset($key, $value);
$now = date('Y-m-d\\TH:i:s\\Z');
$files = glob(POST_DIR . $pattern);
@ -243,8 +237,7 @@ function search_post($options = []) {
else {
$last_page = 0;
}
if ($page < 1) { $page = 1; }
if ($page > $last_page) { $page = $last_page; }
$page = max(1, min($last_page, $page));
if ($has_paging) {
$tail = array_slice($files, ($page - 1) * $pagesize, $pagesize);
@ -252,14 +245,13 @@ function search_post($options = []) {
else {
$tail = $files;
}
$files = null;
unset($files);
$post_list = [];
foreach ($tail as $filepath) {
$post_list[] = load_post($filepath);
}
$filepath = null;
$tail = null;
unset($filepath, $tail);
$has_pager = $last_page > 1;
$pager = [];
@ -310,12 +302,14 @@ function add_post($userid, $title, $body, $attachment_id, $file_hash, &$post_id_
if ($error) { return ['すでに同じ画像有り(削除済みを含む)。連投防止。']; }
}
if (!mkdir_p(POST_DIR)) {
die("mkdir_pを実行に失敗しました。");
}
if (!mkdir_p(POST_DIR)) {
die('mkdir_pを実行に失敗しました。');
}
$result = file_put_contents(POST_DIR . $file, $text, LOCK_EX);
if ($result === false) { return ['書き込みの保存に失敗。']; }
if ($result === false) {
die('書き込みの保存に失敗しました。');
}
touch(POST_DIR . $file, $timestamp, $timestamp);
chmod(POST_DIR . $file, 0644);
@ -334,11 +328,11 @@ function add_post($userid, $title, $body, $attachment_id, $file_hash, &$post_id_
function check_uploaded_image($key) {
$size = $_FILES[$key]['size'] ?? 0;
if ($size <= 0) { return ['画像ファイルが不正。']; }
else if ($size > 1024 * 500) { ['画像ファイルは 500 kb 以内。']; }
else if ($size > 1024 * 500) { ['画像ファイルは 500 kb 以内。']; }
$buffer = file_get_contents($_FILES[$key]['tmp_name']);
$type = get_image_type($buffer);
$buffer = null;
unset($buffer);
if (!isset($type)) { return ['画像ファイルが不正。']; }
return [];
@ -350,14 +344,19 @@ function save_uploaded_image($key, $attachment_id) {
if (!isset($type)) { on_error(400, ['画像ファイルが不正。']); }
if (!$result = mkdir_p(ATTACHMENT_DIR, 0700)) {
return ['フォルダの作成に失敗。'];
}
return ['フォルダの作成に失敗。'];
}
$target = ATTACHMENT_DIR . $attachment_id . '.gz';
if (!$gz = gzopen($target, 'w1')) { return ['画像の書き込みに失敗。']; }
if (!$gz = gzopen($target, 'w1')) {
die('画像の書き込みに失敗しました。');
}
$result = gzwrite($gz, $buffer);
if ($result === false) { return ['画像の書き込みに失敗。']; }
if ($result === false) {
die('画像の書き込みに失敗しました。');
}
gzclose($gz);
chmod($target, 0777);
@ -369,7 +368,7 @@ function delete_post($id, $hard = false) {
if (sizeof($files) !== 1) { return ['書き込みのファイルが存在しない。']; }
$filepath = $files[0];
$files = null;
unset($files);
$attachment_id = load_postfile($filepath)['attachment_id'];
@ -378,8 +377,9 @@ function delete_post($id, $hard = false) {
if ($attachment_id) {
$attachment_path = ATTACHMENT_DIR . $attachment_id . '.gz';
if (file_exists($attachment_path)) {
$result = unlink($attachment_path);
if (!$result) { return ['ファイルの削除に失敗。']; }
if (!unlink($attachment_path)) {
die('ファイルの削除に失敗。');
}
}
}
@ -388,7 +388,9 @@ function delete_post($id, $hard = false) {
} else {
$result = file_put_contents($filepath, '', LOCK_EX);
}
if ($result === false) { return ['書き込みの削除に失敗。']; }
if ($result === false) {
return die('書き込みの削除に失敗。');
}
return [];
}
@ -403,7 +405,8 @@ function load_threads() {
$metadata = get_post_metadata(basename($filepath));
$is_future = $metadata['is_future'];
$thread_id = $metadata['thread_id'];
$metadata = null;
unset($metadata);
if ($is_future) { continue; }
if ($thread_id === '') { continue; }
@ -416,12 +419,7 @@ function load_threads() {
if ($parent['deleted']) { continue; }
$thread = ['count' => 2];
if (($parent['title'] ?? '') != '') {
$thread['title'] = $parent['title'];
}
else {
$thread['title'] = '無題#' . mb_substr($thread_id, 0, 7);
}
$thread['title'] = $parent['title'] ?? '';
$thread['detail_url'] = $parent['detail_url'];
$text = preg_replace("/[\n\r]+/", ' ', $parent['body_raw']);
@ -429,36 +427,34 @@ function load_threads() {
if (mb_strlen($text) > 30) {
$thread['sammary'] .= '(...)';
}
$text = null;
unset($text);
$threads[$thread_id] = $thread;
$thread = null;
unset($thread);
}
$files = null;
$filepath = null;
unset($files, $filepath);
return $threads;
}
function make_repeating_info($body, $datetime, $file_hash = '') {
if ($file_hash != '') {
return ['hash' => $file_fash, 'limit' => mb_substr($datetime, 0, 10)];
if ($file_hash != '') {
return ['hash' => $file_fash, 'limit' => mb_substr($datetime, 0, 10)];
}
$letter_only = preg_replace('/[^\p{Ll}\p{Lt}\p{Lo}\p{Lu}\p{N}]/u', '', $body);
if (mb_strlen($letter_only) > 0) { $body = $letter_only; }
$hash = md5($body);
$len = mb_strlen($body);
if ($len <= 2) { $size = 16; }
elseif ($len <= 30) { $size = 13; }
elseif ($len <= 40) { $size = 10; }
else { $size = 7; }
$limit = mb_substr($datetime, 0, $size);
return [
'hash' => $hash,
'limit' => mb_substr($datetime, 0, $size),
];
return compact('hash', 'limit');
}
function add_repeating_info($repeating_info) {
@ -470,14 +466,14 @@ function add_repeating_info($repeating_info) {
}
function check_repeating_info($body, $datetime, $file_hash = '') {
if (!file_exists(LIMIT_TSV)) {
die(LIMIT_TSV.": ファイルを見つけられません。");
}
if (!file_exists(LIMIT_TSV)) {
touch(LIMIT_TSV, time());
chmod(LIMIT_TSV, 0644);
}
$hash = make_repeating_info($body, $datetime, $file_hash)['hash'];
$text = file_get_contents(LIMIT_TSV) ?? '';
$lines = explode(PHP_EOL, $text);
$text = null;
unset($text);
$out = '';
$count = 0;
@ -492,9 +488,10 @@ function check_repeating_info($body, $datetime, $file_hash = '') {
$count += 1;
}
}
unset($array);
unset($line);
}
unset($array);
unset($line);
}
unset($lines);
@ -506,28 +503,28 @@ function check_repeating_info($body, $datetime, $file_hash = '') {
// Util
function count_post() {
if (!file_exists(POST_DIR)) {
die(POST_DIR.': ディレクトリは存在しません。');
}
if (!file_exists(POST_DIR)) {
die(POST_DIR.': ディレクトリは存在しません。');
}
$pattern = '2*Z*.txt';
$files = glob(POST_DIR . '2*Z*.txt');
$size = sizeof($files);
$files = null;
unset($files);
return $size;
}
function count_post_today() {
if (!file_exists(POST_DIR)) {
die(POST_DIR.': ディレクトリは存在しません。');
}
if (!file_exists(POST_DIR)) {
die(POST_DIR.': ディレクトリは存在しません。');
}
$date = date('Y-m-d');
$pattern = "{$date}T*Z.*.txt";
$files = glob(POST_DIR . $pattern);
$size = sizeof($files);
$files = null;
unset($files);
return $size;
}
@ -545,9 +542,9 @@ function filepath_by_post_id($id) {
}
function load_post_title_by_id($id) {
if (!file_exists(POST_DIR)) {
die(POST_DIR.': ディレクトリは存在しません。');
}
if (!file_exists(POST_DIR)) {
die(POST_DIR.': ディレクトリは存在しません。');
}
global $post_title_cache;
@ -555,14 +552,18 @@ function load_post_title_by_id($id) {
$files = glob(POST_DIR . '2*Z*.id' . $id . '.*.txt');
if (sizeof($files) != 1) {
$files = null;
unset($files);
$post_title_cache[$id] = '';
return '';
}
$filepath = $files[0];
$files = null;
$post_title_cache[$id] = load_postfile($filepath)['title'] ?? '';
unset($files);
$title = load_postfile($filepath)['title'] ?? '';
if ($title === '') {
$title = post_default_title($id);
}
$post_title_cache[$id] = $title;
return $post_title_cache[$id];
return $title;
}

ファイルの表示

@ -1,11 +1,11 @@
<?php
// Data controller
// データ関連の関数
// Post
// 投稿 (Post)
require_once(__DIR__ . '/data-post.php');
// User
// 利用者 (User)
// ユーザー情報は USER_TSV (data/user.tsv) に保存される。
// 内容はタブ区切り・ヘッダー行は無い。
@ -31,7 +31,7 @@ function load_users() {
$users_cache[$user['id']] = $user;
unset($user);
}
unset($rows);
unset($rows, $row);
return $users_cache;
}
@ -45,7 +45,7 @@ function load_users_with_special() {
function update_user($user) {
// 注意:ここを変更したら、必ず、ユーザーが消えない事をテストすること。
if (!$user) { return ['ユーザーが不正。']; }
if (!$user) { return ['ユーザーが不正。']; }
if (!isset($user['id'])) { return ['ユーザーIDが不正。']; }
$id = $user['id'];
$username = $user['username'];
@ -59,7 +59,7 @@ function update_user($user) {
if ($prev === false) { $prev = ''; }
$rows = explode(PHP_EOL, $prev);
$prev = null;
unset($prev);
$output = '';
$search_id = $id . "\t";
@ -103,9 +103,9 @@ function update_user($user) {
}
function add_user($user) {
if (!file_exists(USERS_TSV)) {
die(USERS_TSV.': ファイルを見つけられません。');
}
if (!file_exists(USERS_TSV)) {
die(USERS_TSV.': ファイルを見つけられません。');
}
$id = $user['id'];
$username = $user['username'];
@ -116,37 +116,40 @@ function add_user($user) {
unset($match);
$hash_password = hash_password($password);
$record = "{$id}\t{$username}\t{$hash_password}\t" . PHP_EOL;
$result = false;
$record = "{$id}\t{$username}\t{$hash_password}\t" . PHP_EOL;
$result = false;
if (is_writable(USERS_TSV)) {
$result = file_put_contents(USERS_TSV, $record, FILE_APPEND | LOCK_EX);
}
if ($result === false) { return ['ファイルの書き込みに失敗。']; }
if (is_writable(USERS_TSV)) {
$result = file_put_contents(USERS_TSV, $record, FILE_APPEND | LOCK_EX);
}
if ($result === false) {
die('ファイルの書き込みに失敗しました。');
}
chmod(USERS_TSV, 644);
return [];
}
// Profile (bio)
// 利用者のプロフィール (Profile)
// プロフィールは PROFILE_DIR (data/profile) 配下に {ID}.txt のファイル名で保存される。
// 先頭に「-」だけの行があり、それより後の行がプロフィール本文になる。
// 将来的に「-」より前の行にユーザーごとの設定値などを記録する予定。
function load_profile($id) {
if ($id === 'tl') { return ['bio' => '']; }
if (!file_exists(PROFILE_DIR)) {
if (!mkdir_p(PROFILE_DIR, 755)) die(PROFILE_DIR.'を作成に失敗。');
}
if (!file_exists(PROFILE_DIR.$id.'.txt')) {
touch(PROFILE_DIR.$id.'.txt', time());
}
if (!file_exists(PROFILE_DIR)) {
if (!mkdir_p(PROFILE_DIR, 755)) {
die(PROFILE_DIR.'を作成に失敗。');
}
}
if (!file_exists(PROFILE_DIR.$id.'.txt')) {
touch(PROFILE_DIR.$id.'.txt', time());
}
$profile = '';
if (is_writable(PROFILE_DIR.$id.'.txt')) {
$profile = file_get_contents(PROFILE_DIR . $id . '.txt') ?? '';
}
$profile = '';
if (is_writable(PROFILE_DIR.$id.'.txt')) {
$profile = file_get_contents(PROFILE_DIR . $id . '.txt') ?? '';
}
$split = preg_split('/^-$/m', $profile, 2);
$bio = mbtrim($split[1] ?? '');
@ -156,15 +159,19 @@ function load_profile($id) {
function save_profile($id, $profile) {
$bio = $profile['bio'] ?? '';
if (!mkdir_p(PROFILE_DIR)) {
die(PROFILE_DIR.'を作成に失敗。');
}
if (!mkdir_p(PROFILE_DIR)) { return ['フォルダーの作成に失敗。']; }
$result = file_put_contents(PROFILE_DIR . $id . '.txt', '-' . PHP_EOL . $bio . PHP_EOL);
if ($result === false) { return ['ファイルの書き込みに失敗。']; }
if ($result === false) {
die(PROFILE_DIR.$id.'を作成に失敗。');
}
return [];
}
// Auth
// 認証
function auth_user($id, $password) {
$id = trim($id);
@ -184,12 +191,12 @@ function find_user_row($id) {
$users = file_get_contents(USERS_TSV) ?? '';
$rows = explode(PHP_EOL, $users);
$search_id = $id . "\t";
foreach ($rows as $row) {
if (stripos($row, $search_id) === 0) {
return $row;
}
}
$search_id = $id . "\t";
foreach ($rows as $row) {
if (stripos($row, $search_id) === 0) {
return $row;
}
}
return null;
}

ファイルの表示

@ -1,35 +1,33 @@
<?php
require_once(__DIR__ . '/../../require.php');
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
if (!ENABLE_ATTACHMENT) { return on_error(404, ['Not Found.']); }
if (!ENABLE_ATTACHMENT) { return on_error(404, ['Not Found.']); }
$id = $_GET['id'] ?? '';
if ($id == '' || !preg_match('/^[a-z0-9]{32}$/', $id)) { return on_error(400, ['URLが不正。']); }
$id = $_GET['id'] ?? '';
if ($id == '' || !preg_match('/^[a-z0-9]{32}$/', $id)) { return on_error(400, ['URLが不正。']); }
$filepath = ATTACHMENT_DIR . $id . '.gz';
if (!file_exists($filepath)) { return on_error(404, ['Not Found.']); }
$filepath = ATTACHMENT_DIR . $id . '.gz';
if (!file_exists($filepath)) { return on_error(404, ['Not Found.']); }
ob_start();
readgzfile($filepath);
$buffer = ob_get_clean();
$type = get_image_type($buffer);
if (!isset($type)) { return on_error(500, ['ファイルが不正。']); }
ob_start();
readgzfile($filepath);
$buffer = ob_get_clean();
$type = get_image_type($buffer);
if (!isset($type)) { return on_error(500, ['ファイルが不正。']); }
$php_time = filemtime(__FILE__);
$attachment_time = filemtime($filepath);
$etag = '"' . $php_time . '.' . $attachment_time . '"';
$php_time = filemtime(__FILE__);
$attachment_time = filemtime($filepath);
$etag = '"' . $php_time . '.' . $attachment_time . '"';
header('Cache-Control: max-age=86400');
header("ETag: {$etag}");
header('Cache-Control: max-age=86400');
header("ETag: {$etag}");
if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
if ($_SERVER['HTTP_IF_NONE_MATCH'] === $etag) {
header('HTTP/1.1 304 Not Modified', true, 304);
exit();
}
if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
if ($_SERVER['HTTP_IF_NONE_MATCH'] === $etag) {
header('HTTP/1.1 304 Not Modified', true, 304);
exit();
}
header("Content-Type: {$type}");
echo $buffer;
}
header("Content-Type: {$type}");
echo $buffer;

ファイルの表示

@ -1,15 +1,13 @@
<?php
require_once(__DIR__ . '/../require.php');
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$page = max(1, (int)($_GET['page'] ?? 1));
$result = search_post([
'page' => $page,
'pagesize' => POSTS_PER_PAGE,
'pager_prefix' => '?page=',
]);
$view['post_list'] = $result['post_list'];
$view['pager'] = $result['pager'];
$page = max(1, (int)($_GET['page'] ?? 1));
$result = search_post([
'page' => $page,
'pagesize' => POSTS_PER_PAGE,
'pager_prefix' => '?page=',
]);
$view['post_list'] = $result['post_list'];
$view['pager'] = $result['pager'];
output_html($view, ['header.php', 'post-form.php', 'timeline.php', 'post-list.php', 'pager.php']);
}
output_html($view, ['header.php', 'post-form.php', 'timeline.php', 'post-list.php', 'pager.php']);

ファイルの表示

@ -1,7 +1,5 @@
<?php
require_once(__DIR__ . '/../../require.php');
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$view['threads'] = load_threads();
output_html($view, ['header.php', 'thread.php']);
}
$view['threads'] = load_threads();
output_html($view, ['header.php', 'thread.php']);

ファイルの表示

@ -1,10 +1,8 @@
<?php
require_once(__DIR__ . '/../../require.php');
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$user_list = load_users();
ksort($user_list, SORT_STRING | SORT_FLAG_CASE);
$view['user_list'] = $user_list;
$user_list = load_users();
ksort($user_list, SORT_STRING | SORT_FLAG_CASE);
$view['user_list'] = $user_list;
output_html($view, ['header.php', 'user-list.php']);
}
output_html($view, ['header.php', 'user-list.php']);

ファイルの表示

@ -1,35 +1,33 @@
<?php
require_once(__DIR__ . '/../../require.php');
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$id = '' . ($_GET['id'] ?? '');
if ($id !== 'tl' && (!strlen($id) || validate_register_id($id))) { return on_error(400, ['不正なリクエスト。']); }
$id = '' . ($_GET['id'] ?? '');
if ($id !== 'tl' && (!strlen($id) || validate_register_id($id))) { return on_error(400, ['不正なリクエスト。']); }
$users = load_users_with_special();
if (!isset($users[$id])) { return on_error(400, ['存在しない ID']); }
$users = load_users_with_special();
if (!isset($users[$id])) { return on_error(400, ['存在しない ID']); }
$view['user'] = $users[$id];
$users = null;
$view['user'] = $users[$id];
$users = null;
$profile = load_profile($id);
$bio = $profile['bio'] ?? '';
$bio = url_to_link(nl2br(htmlspecialchars($bio), false));
$view['bio'] = $bio;
$profile = null;
$profile = load_profile($id);
$bio = $profile['bio'] ?? '';
$bio = url_to_link(nl2br(htmlspecialchars($bio), false));
$view['bio'] = $bio;
$profile = null;
$page = max(1, $_GET['page'] ?? 0);
$result = search_post([
'key' => 'userid',
'value' => $id,
'page' => $page,
'pagesize' => POSTS_PER_PAGE,
'pager_prefix' => "?id={$id}&page=",
'includes_future' => ($id === ($_SESSION['user']['id'] ?? '')),
]);
$view['post_list'] = $result['post_list'];
$view['post_count'] = $result['total'];
$view['pager'] = $result['pager'];
$result = null;
$page = max(1, $_GET['page'] ?? 0);
$result = search_post([
'key' => 'userid',
'value' => $id,
'page' => $page,
'pagesize' => POSTS_PER_PAGE,
'pager_prefix' => "?id={$id}&page=",
'includes_future' => ($id === ($_SESSION['user']['id'] ?? '')),
]);
$view['post_list'] = $result['post_list'];
$view['post_count'] = $result['total'];
$view['pager'] = $result['pager'];
$result = null;
output_html($view, ['header.php', 'user.php', 'post-list.php', 'pager.php']);
}
output_html($view, ['header.php', 'user.php', 'post-list.php', 'pager.php']);

ファイルの表示

@ -1,6 +1,7 @@
<?php
require_once(__DIR__ . '/config.php');
require_once(__DIR__ . '/config-default.php');
require_once(__DIR__ . '/util.php');
require_once(__DIR__ . '/common.php');
require_once(__DIR__ . '/data.php');
require_once(__DIR__ . '/validate.php');

187
util.php ノーマルファイル
ファイルの表示

@ -0,0 +1,187 @@
<?php
// 共通処理
// HTTP関連
function get_fp() {
return md5(
($_SERVER['HTTP_USER_AGENT'] ?? '')
. ($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '')
. ($_SERVER['HTTP_ACCEPT_ENCODING'] ?? '')
. ($_SERVER['HTTP_ACCEPT'] ?? '')
);
}
function has_cookie() {
return isset($_COOKIE['bibis']);
}
function on_error($code, $errors) {
http_response_code(400);
if (function_exists('bibis_http_header')) { bibis_http_header(); }
$view['errors'] = $errors;
require(__DIR__ . '/view/header.php');
exit;
}
function output_html($view, $_components) {
if (function_exists('bibis_http_header')) { bibis_http_header(); }
foreach ($_components as $_name) {
require(__DIR__ . "/view/$_name");
}
exit;
}
// CSRFトークン関連
function set_csrf_token() {
if (empty($_SESSION['csrf_token'])) {
$_SESSION["csrf_token"] = bin2hex(random_bytes(32));
}
}
function get_csrf_token() {
if (!REQUIRE_COOKIE && !isset($_SESSION['user'])) {
$date = date('YmdH');
return $date . get_fp();
}
return $_SESSION['csrf_token'] ?? null;
}
function get_csrf_token_2($force_cookie = false) {
if (!REQUIRE_COOKIE && !isset($_SESSION['user'])) {
$date = date('YmdH', strtotime('-1 hour'));
return $date . get_fp();
}
return $_SESSION['csrf_token'] ?? null;
}
function get_csrf_token_hashed($prefix = '', $token = null) {
if ($token === null) { $token = get_csrf_token(); }
if ($token === null) { return null; }
return md5($prefix . $token);
}
function check_csrf_token() {
$hashed = get_csrf_token_hashed();
$hashed_2 = get_csrf_token_hashed('', get_csrf_token_2());
if ($hashed === null || $hashed_2 === null) { return ['CSRF トークンが不正。要再試行。']; }
$token_key = get_csrf_token_hashed('csrf_token');
$csrf_token = $_POST[$token_key] ?? '';
if ($csrf_token !== $hashed && $csrf_token !== $hashed_2) { return ['CSRF トークンが不正。要再試行。']; }
return [];
}
function output_csrf_token_hidden() {
$hashed = get_csrf_token_hashed();
if ($hashed === null) { return ''; }
$token_key = get_csrf_token_hashed('csrf_token');
return '<input type="hidden" name="' . htmlspecialchars($token_key) . '" value="' . $hashed . '">';
}
// 文字列関連
function post_default_title($id) {
return '無題#' . mb_substr($id, 0, 7);
}
function anchor_to_link($s, $thread_id) {
return preg_replace(
'/&gt;&gt;([1-9]+[0-9]{0,10})/',
'<a href="' . sitebase('post/?id=') . $thread_id . '&amp;res=\1">&gt;&gt;\1</a>',
$s
);
}
function url_to_link($s) {
return preg_replace(
'/((http|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&amp;:\/~+#-]*[\w@?^=%&amp;\/~+#-])?)/',
'<a href="\1" rel="noopener noreferrer">\1</a>',
$s
);
}
function mbtrim($s) {
return preg_replace('/^\s+|\s+$/u', '', $s);
}
function twochan_trip($tripkey) {
// twochan_trip('atomikka') -> '◆.../5Betyws'
// Thanks: https://refirio.org/view/62
// Test data: https://trip-table.kokage.cc/sample01.php
$tripkey = mb_convert_encoding($tripkey, 'sjis-win');
$salt = substr($tripkey . 'H.', 1, 2);
$salt = preg_replace('/[^\.-z]/', '.', $salt);
$salt = strtr($salt, ':;<=>?@[\\]^_`', 'ABCDEFGabcdef');
$trip = crypt($tripkey, $salt);
$trip = substr($trip, -10);
return '◆' . $trip;
}
function parse_key_value($s, $key_list) {
$result = [];
$lines = explode(PHP_EOL, $s);
foreach ($lines as $line) {
foreach ($key_list as $key) {
if (strpos($line, "{$key}=") === 0) {
$result[$key] = explode("{$key}=", $line, 2)[1] ?? null;
}
}
unset($key);
}
unset($lines, $line);
return $result;
}
// ファイル関連
function mkdir_p($path, $permission = 0700) {
if (file_exists($path)) { return true; }
return mkdir($path, $permission);
}
function get_image_type($s) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$type = finfo_buffer($finfo, $s);
if (isset($type) && in_array($type, ACCEPT_IMAGE_TYPE)) {
return $type;
}
return null;
}
function sitebase($path = '') {
return SITEBASE . $path;
}
function get_style_css() {
if (THEME === null || THEME === '') { return sitebase('style.css'); }
return sitebase('theme/' . THEME);
}
// 制限・権限
function is_logged_in() {
return isset($_SESSION['user']);
}
function can_post() {
if (!post_limited()) { return false; }
if (!ENABLE_GUEST && !is_logged_in()) { return false; }
return true;
}
function post_limited() {
return ENABLE_POST && (count_post_today() < POST_LIMIT_PER_DAY) && (count_post() < POST_LIMIT);
}
function can_regist() {
return ENABLE_REGISTER;
}

ファイルの表示

@ -1,5 +1,5 @@
<?php
// Validation, and sanitize
// バリデーションやサニタイズの関数
function validate_register_id($s) {
$s = mbtrim($s);
@ -75,9 +75,8 @@ function sanitize_post_body($s) {
return sanitize_multiline($s);
}
// Common
// See: https://www.php.net/manual/ja/regexp.reference.unicode.php
// 共通
// 正規表現の内訳: https://www.php.net/manual/ja/regexp.reference.unicode.php
function sanitize_multiline($s) {
$s = preg_replace('/[^\p{L}\{M}\p{N}\p{P}\p{Sc}\p{S}\p{Z}\012\015\040-\176]/u', '', $s);

ファイルの表示

@ -1,5 +1,5 @@
<?php
// HTML Header, and global navigation
// 共通ヘッダー
$view['login_user'] = $_SESSION['user'] ?? null;
$view['messages'] = $_SESSION['messages'] ?? null;

ファイルの表示

@ -1,5 +1,5 @@
<?php
// Login form
// ログイン画面
?>
<h2>ログイン・Login</h2>
<form action="<?= sitebase('login/') ?>" method="POST">

ファイルの表示

@ -1,5 +1,5 @@
<?php
// Pager
// ページャー
?>
<?php if (isset($view['pager'])): ?>
<ul class="pager">

ファイルの表示

@ -1,5 +1,5 @@
<?php
// Single post
// 単独の投稿
function view_post($post, $options = []) {
$res_num = $options['res_num'] ?? 0;
@ -62,7 +62,7 @@ function view_post($post, $options = []) {
<dt>件名:<b><?= $title ?></b></dt>
<?php endif; ?>
<dt><?= $res_num > 0 ? "{$res_num} " : '' ?><?= $html_user ?> <?= $html_time ?></dt>
<?php if ($link_to_thread && $thread_id > ''): ?>
<?php if (!$is_single && $link_to_thread && $thread_id != ''): ?>
<dd>RE: <a href="<?= $thread_url ?>"><?= $thread_title ?></a></dd>
<?php endif; ?>
<dd><?= $body ?>

ファイルの表示

@ -1,5 +1,5 @@
<?php
// Confirm (delete post)
// 削除画面
require __DIR__ . '/post-common.php';
?>

ファイルの表示

@ -1,5 +1,5 @@
<?php
// Post form
// 投稿フォーム
$value = '';
if (($view['res_num'] ?? 0) >= 2) {
$value = '>>' . $view['res_num'] . PHP_EOL;

ファイルの表示

@ -1,4 +1,6 @@
<?php
// 投稿一覧
require __DIR__ . '/post-common.php';
if (!($view['post_list'] ?? false)) {

ファイルの表示

@ -1,5 +1,5 @@
<?php
// Post/thread detail
// 投稿詳細
require __DIR__ . '/post-common.php';
$post = $view['post'] ?? [];
@ -16,10 +16,8 @@ $total_view = $total > 1 ? " ($total)" : '';
<h2>件名:<?= htmlspecialchars("$title{$total_view}") ?></h2>
<?php elseif ($thread_id == ''): ?>
<h2><?= htmlspecialchars('無題#' . mb_substr($post_id, 0, 7) . $total_view) ?></h2>
<?php elseif ($thread_title != ''): ?>
<?php else: ?>
<h2>RE:<a href="<?= $thread_url ?>"><?= htmlspecialchars($thread_title) ?></a></h2>
<?php elseif ($thread_id != ''): ?>
<h2>RE:<a href="<?= $thread_url ?>"><?= htmlspecialchars('無題#' . mb_substr($thread_id, 0, 7)) ?></a></h2>
<?php endif; ?>
<?php
view_post($post, ['res_num' => $thread_id != '' ? 0 : 1, 'is_single' => true, 'link_to_thread' => true, 'res_num' => $view['res_num'] ?? 1]);
@ -28,4 +26,4 @@ view_post($post, ['res_num' => $thread_id != '' ? 0 : 1, 'is_single' => true, 'l
foreach ($reply_list as $i => $reply) {
view_post($reply, ['res_num' => $i + 2]);
}
?>
?>

ファイルの表示

@ -1,3 +1,6 @@
<?php
// 投稿フォーム
?>
<?php if (isset($view['form']['thread_id']) && $view['form']['thread_id'] >= 0): ?>
<h2 id="REPLY_FORM">返信</h2>
<?php endif; ?>

ファイルの表示

@ -1,4 +1,5 @@
<?php
// 投稿一覧
$reply_list_mode = isset($view['reply_list_mode']) && $view['reply_list_mode'];
?>
<?php if (isset($view['post_list'])): ?>

ファイルの表示

@ -1,5 +1,5 @@
<?php
// Register form (stop)
// 新規登録 (停止中)
?>
<h2>新規登録・Register</h2>
<ul>

ファイルの表示

@ -1,5 +1,5 @@
<?php
// Register form
// 新規登録
?>
<h2>新規登録・Register</h2>
<form action="<?= sitebase('register/') ?>" method="POST">

ファイルの表示

@ -1,5 +1,5 @@
<?php
// Settings form
// 設定
?>
<h2>設定(ユーザー情報変更)・Settings</h2>
<p>パスワード変更を希望しない場合、新パスワードの欄を空にしてください。</p>

ファイルの表示

@ -1,5 +1,5 @@
<?php
// List of threads
// スレッド一覧
?>
<h2>スレッド一覧・Threads (<?= sizeof($view['threads'] ?? []) ?>)</h2>
<p>返信が1件以上あるスレッドの一覧。

ファイルの表示

@ -1,5 +1,5 @@
<?php
// List of users
// 利用者一覧
// TODO: できればユーザー詳細のURLは /public/user-list/index.php で生成したい。
?>
<h2>利用者一覧・Users (<?= sizeof($view['user_list'] ?? []) ?>)</h2>

ファイルの表示

@ -1,5 +1,5 @@
<?php
// User profile
// 利用者のプロフィール
?>
<dl>
<dt><b><?= htmlspecialchars($view['user']['username'] ?? '') ?></b> @<?= htmlspecialchars($view['user']['id']) ?></dt>