date('Y-m-d\\TH:i:s\\Z'); $date = new Datetime($datetime); if (TIMEZONE !== null) { $date->setTimezone(new DateTimeZone(TIMEZONE)); } $format = 'Y-m-d'; if (!HIDE_TIME || $is_future) { $format .= ' H:i'; } $time = $date->format($format); $format = null; $date = null; // TODO:これだとファイル名がマッチしない時に予期せぬ結果になる // 投稿ID: ".id{$id}." $id = preg_replace('/.+\.id([^.]+)\..+/', '\1', $file); // 返信先スレッドID: ".th{$id}." $thread_id = preg_replace('/.+\.th([^.]+)\..+/', '\1', $file); if ($thread_id === NOREPLY_ID) { $thread_id = ''; } // 投稿ユーザーID: ".us{$id}" $userid = ''; if (!$deleted) { $userid = preg_replace('/.+\.us([^.]+)\..+/', '\1', $file); } if ($userid === GUEST_ID) { $userid = ''; } $is_guest = $userid === ''; $thread_title = ''; 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); $delete_url = sitebase('post-delete/?id=' . $id); $user_url = $is_guest ? '' : sitebase('user/?id=' . $userid); $thread_url = $thread_id != '' ? sitebase('post/?id=' . $thread_id) : ''; $is_mine = !$is_guest && $userid === ($_SESSION['user']['id'] ?? ''); // too long, sorry. return compact('id', 'is_guest', 'userid', 'detail_url', 'delete_url', 'user_url', 'datetime', 'time', 'thread_id', 'thread_title', 'thread_url', 'is_mine', 'is_future'); } function load_post($filepath) { $files = null; $detail = load_postfile($filepath); $deleted = $detail['deleted']; $title = $detail['title']; $body_raw = $detail['body']; $body = url_to_link(nl2br(htmlspecialchars($body_raw), false)); $attachment_id = $detail['attachment_id']; $detail = null; $metadata = get_post_metadata(basename($filepath), $deleted); $userid = $metadata['userid']; $users = load_users(); $username = GUESTNAME; if ($deleted) { $username = DELETEDNAME; } elseif ($userid != '') { $username = $users[$userid]['username'] ?? 'UNKNOWN'; } $attachments = []; if ($attachment_id != '') { $attachments[] = [ 'url' => sitebase('attachment/?id=' . $attachment_id), 'name' => '添付画像(Image)', ]; } return array_merge( $metadata, compact('username', 'deleted', 'title', 'body', 'body_raw', 'attachments'), ); } function load_post_by_id($id) { if (!file_exists(POST_DIR)) { die(POST_DIR.': ディレクトリは存在しません。'); } $files = glob(POST_DIR . '2*Z*.id' . $id . '.*.txt'); if (sizeof($files) !== 1) { $files = null; return null; } $filepath = $files[0]; return load_post($filepath); } function search_post($options = []) { if (!file_exists(POST_DIR)) { die(POST_DIR.': ディレクトリは存在しません。'); } $pagesize = $options['pagesize'] ?? 0; $has_paging = $pagesize > 0; $thread = false; $pager_prefix = $options['pager_prefix'] ?? '?page='; $key = $options['key'] ?? ''; $value = $options['value'] ?? ''; $page = $options['page'] ?? 1; $options = null; $pattern = '2*Z*.txt'; if ($key === 'userid' && $value != '') { $pattern = "2*Z*.us{$value}.*.txt"; } elseif ($key == 'thread' && $value >= 0) { // 'th' is last $pattern = "2*Z*.th{$value}.txt"; $thread = true; } $key = null; $value = null; $now = date('Y-m-d\\TH:i:s\\Z'); $files = glob(POST_DIR . $pattern); $files = array_filter( $files, function ($v) use($now) { $datetime = substr(basename($v), 0, 20); return $datetime <= $now; }, ARRAY_FILTER_USE_BOTH ); if (!$thread) { $files = array_reverse($files); } $total = sizeof($files); if ($has_paging) { $last_page = (int)ceil($total / $pagesize); } else { $last_page = 0; } if ($page < 1) { $page = 1; } if ($page > $last_page) { $page = $last_page; } if ($has_paging) { $tail = array_slice($files, ($page - 1) * $pagesize, $pagesize); } else { $tail = $files; } $files = null; $post_list = []; foreach ($tail as $filepath) { $post_list[] = load_post($filepath); } $filepath = null; $tail = null; $has_pager = $last_page > 1; $pager = []; if ($has_paging && $has_pager) { if ($page > 1) { $pager['after'] = $pager_prefix . ($page - 1); } if ($page < $last_page) { $pager['before'] = $pager_prefix . ($page + 1); } $pager['pagesize'] = POSTS_PER_PAGE; } return compact('post_list', 'total', 'page', 'pager', 'last_page'); } function add_post($userid, $title, $body, $attachment_id, $file_hash, &$post_id_ref, $thread_id, $spooftime) { // TODO:引数は配列の方が良い? // TODO:$post_id_refの参照渡しはイマイチ if ($thread_id !== NOREPLY_ID) { // 返信の場合件名を捨てる $title = ''; } $text = ''; if ($title) { $text .= 'title=' . $title . PHP_EOL; } if (isset($attachment_id)) { $text .= 'attachment_id=' . $attachment_id . PHP_EOL; } $text .= '-' . PHP_EOL . $body . PHP_EOL; $id = md5(bin2hex(random_bytes(256)) . $userid . $title . $body); $dto = new DateTime($spooftime ? '+' . (mt_rand(60 * 60 * 3, 60 * 60 * 27 - 1)) . 'seconds' : ''); $datetime = $dto->format('Y-m-d\\TH:i:s\\Z'); $timestamp = $dto->getTimestamp(); unset($dto); $file = "{$datetime}.id{$id}.us{$userid}.to-.th{$thread_id}.txt"; if ($body !== '.' || $file_hash === '') { $error = check_repeating_info($body, $datetime); if ($error) { return ['すでに同様の書き込み有り(削除済みを含む)。連投防止。']; } } if ($file_hash != '') { $error = check_repeating_info($body, $datetime, $file_hash); if ($error) { return ['すでに同じ画像有り(削除済みを含む)。連投防止。']; } } if (!mkdir_p(POST_DIR)) { die("mkdir_pを実行に失敗しました。"); } $result = file_put_contents(POST_DIR . $file, $text, LOCK_EX); if ($result === false) { return ['書き込みの保存に失敗。']; } touch(POST_DIR . $file, $timestamp, $timestamp); chmod(POST_DIR . $file, 0644); if ($body !== '.' || $file_hash === '') { add_repeating_info(make_repeating_info($body, $datetime)); } if ($file_hash != '') { add_repeating_info(make_repeating_info($body, $datetime, $file_hash)); } $post_id_ref = $id; return []; } function check_uploaded_image($key) { $size = $_FILES[$key]['size'] ?? 0; if ($size <= 0) { return ['画像ファイルが不正。']; } else if ($size > 1024 * 500) { ['画像ファイルは 500 kb 以内。']; } $buffer = file_get_contents($_FILES[$key]['tmp_name']); $type = get_image_type($buffer); $buffer = null; if (!isset($type)) { return ['画像ファイルが不正。']; } return []; } function save_uploaded_image($key, $attachment_id) { $buffer = file_get_contents($_FILES[$key]['tmp_name']); $type = get_image_type($buffer); if (!isset($type)) { on_error(400, ['画像ファイルが不正。']); } if (!$result = mkdir_p(ATTACHMENT_DIR, 0700)) { return ['フォルダの作成に失敗。']; } $target = ATTACHMENT_DIR . $attachment_id . '.gz'; if (!$gz = gzopen($target, 'w1')) { return ['画像の書き込みに失敗。']; } $result = gzwrite($gz, $buffer); if ($result === false) { return ['画像の書き込みに失敗。']; } gzclose($gz); chmod($target, 0777); return []; } function delete_post($id) { $files = glob(POST_DIR . '2*Z*.id' . $id . '.*.txt'); if (sizeof($files) !== 1) { return ['書き込みのファイルが存在しない。']; } $filepath = $files[0]; $files = null; $attachment_id = load_postfile($filepath)['attachment_id']; // 存在確認済みだからmkdir_pは不要 if ($attachment_id) { $attachment_path = ATTACHMENT_DIR . $attachment_id . '.gz'; if (file_exists($attachment_path)) { $result = unlink($attachment_path); if (!$result) { return ['ファイルの削除に失敗。']; } } } $result = file_put_contents($filepath, '', LOCK_EX); if ($result === false) { return ['書き込みの削除に失敗。']; } return []; } function load_threads() { $pattern = '2*Z.*.th*.txt'; $files = glob(POST_DIR . $pattern); $files = array_reverse($files); $threads = []; foreach ($files as $filepath) { $metadata = get_post_metadata(basename($filepath)); $is_future = $metadata['is_future']; $thread_id = $metadata['thread_id']; $metadata = null; if ($is_future) { continue; } if ($thread_id === '') { continue; } if (isset($threads[$thread_id])) { $threads[$thread_id]['count'] += 1; continue; } $parent = load_post_by_id($thread_id); if ($parent['deleted']) { continue; } $thread = ['count' => 2]; if (($parent['title'] ?? '') != '') { $thread['title'] = $parent['title']; } else { $thread['title'] = '無題#' . mb_substr($thread_id, 0, 7); } $thread['detail_url'] = $parent['detail_url']; $text = preg_replace("/[\n\r]+/", ' ', $parent['body_raw']); $thread['sammary'] = htmlspecialchars(mb_substr($text, 0, 30)); if (mb_strlen($text) > 30) { $thread['sammary'] .= '(...)'; } $text = null; $threads[$thread_id] = $thread; $thread = null; } $files = null; $filepath = null; return $threads; } function make_repeating_info($body, $datetime, $file_hash = '') { 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; } return [ 'hash' => $hash, 'limit' => mb_substr($datetime, 0, $size), ]; } function add_repeating_info($repeating_info) { // NOTE:大して重要ではないためロックしない $hash = $repeating_info['hash'] ?? ''; $limit = $repeating_info['limit'] ?? ''; $text = "$hash\t$limit" . PHP_EOL; $result = file_put_contents(LIMIT_TSV, $text, FILE_APPEND | LOCK_EX); } function check_repeating_info($body, $datetime, $file_hash = '') { if (!file_exists(LIMIT_TSV)) { die(LIMIT_TSV.": ファイルを見つけられません。"); } $hash = make_repeating_info($body, $datetime, $file_hash)['hash']; $text = file_get_contents(LIMIT_TSV) ?? ''; $lines = explode(PHP_EOL, $text); $text = null; $out = ''; $count = 0; foreach ($lines as $line) { if (mbtrim($line) === '') { continue; } $array = explode("\t", $line); if (strpos($datetime, $array[1] ?? '') === 0) { $out .= $line . PHP_EOL; if ($array[0] === $hash) { $count += 1; } } unset($array); unset($line); } unset($lines); $result = file_put_contents(LIMIT_TSV, $out, LOCK_EX); return $count >= 1; } // Util function count_post() { if (!file_exists(POST_DIR)) { die(POST_DIR.': ディレクトリは存在しません。'); } $pattern = '2*Z*.txt'; $files = glob(POST_DIR . '2*Z*.txt'); $size = sizeof($files); $files = null; return $size; } function count_post_today() { 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; return $size; } function load_post_title_by_id($id) { if (!file_exists(POST_DIR)) { die(POST_DIR.': ディレクトリは存在しません。'); } global $post_title_cache; if (isset($post_title_cache[$id])) { return $post_title_cache[$id]; } $files = glob(POST_DIR . '2*Z*.id' . $id . '.*.txt'); if (sizeof($files) !== 1) { $files = null; $post_title_cache[$id] = ''; return ''; } $filepath = $files[0]; $files = null; $post_title_cache[$id] = load_postfile($filepath)['title'] ?? ''; return $post_title_cache[$id]; }