feat(http): implement HTTP routing infrastructure with middleware support

- Add Router class with route registration and dispatching
- Implement RequestFactory and ResponseFactory for HTTP handling
- Create BaseMiddleware and CorsMiddleware for request processing
- Add Kernel class for HTTP request/response lifecycle management
- Introduce HelloWorldController and LegacyController for web routes
- Move terms.php to controllers directory following new structure
- Add route definitions in config/routes.php and web.php
- Update dependency injection definitions for HTTP components
- Integrate routing system with existing application entry points
- Add comprehensive test coverage for new HTTP components

This establishes a modern HTTP routing foundation that supports:
- Middleware pipeline execution
- RESTful route definitions
- Proper separation of concerns between routing and business logic
- Backward compatibility through LegacyController
This commit is contained in:
Yury Pikhtarev 2025-06-22 02:26:25 +04:00
commit 273121a49f
No known key found for this signature in database
25 changed files with 2051 additions and 449 deletions

View file

@ -34,9 +34,6 @@ if (empty($_SERVER['SERVER_ADDR'])) {
if (!defined('BB_ROOT')) {
define('BB_ROOT', './');
}
if (!defined('BB_SCRIPT')) {
define('BB_SCRIPT', null);
}
header('X-Frame-Options: SAMEORIGIN');
date_default_timezone_set('UTC');

View file

@ -62,6 +62,7 @@
"jacklul/monolog-telegram": "^3.1",
"josantonius/cookie": "^2.0",
"league/flysystem": "^3.28",
"league/route": "^6.2",
"longman/ip-tools": "1.2.1",
"monolog/monolog": "^3.4",
"nette/caching": "^3.3",

255
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "57713d8849e71683b70d934a81f7e18c",
"content-hash": "8334ef08115fb688baf7bde81a18031c",
"packages": [
{
"name": "arokettu/bencode",
@ -2271,6 +2271,96 @@
],
"time": "2024-09-21T08:32:55+00:00"
},
{
"name": "league/route",
"version": "6.2.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/route.git",
"reference": "38775ed32d49ff1ce98d88adaa06a8d66b923436"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/route/zipball/38775ed32d49ff1ce98d88adaa06a8d66b923436",
"reference": "38775ed32d49ff1ce98d88adaa06a8d66b923436",
"shasum": ""
},
"require": {
"laravel/serializable-closure": "^2.0.0",
"nikic/fast-route": "^1.3",
"php": "^8.1",
"psr/container": "^2.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^2.0",
"psr/http-server-handler": "^1.0.1",
"psr/http-server-middleware": "^1.0.1",
"psr/simple-cache": "^3.0"
},
"replace": {
"orno/http": "~1.0",
"orno/route": "~1.0"
},
"require-dev": {
"laminas/laminas-diactoros": "^3.5",
"phpstan/phpstan": "^1.12",
"phpstan/phpstan-phpunit": "^1.3",
"phpunit/phpunit": "^10.2",
"roave/security-advisories": "dev-latest",
"scrutinizer/ocular": "^1.8",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-1.x": "1.x-dev",
"dev-2.x": "2.x-dev",
"dev-3.x": "3.x-dev",
"dev-4.x": "4.x-dev",
"dev-5.x": "5.x-dev",
"dev-6.x": "6.x-dev",
"dev-master": "6.x-dev"
}
},
"autoload": {
"psr-4": {
"League\\Route\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Phil Bennett",
"email": "mail@philbennett.co.uk",
"role": "Developer"
}
],
"description": "Fast routing and dispatch component including PSR-15 middleware, built on top of FastRoute.",
"homepage": "https://github.com/thephpleague/route",
"keywords": [
"dispatcher",
"league",
"psr-15",
"psr-7",
"psr15",
"psr7",
"route",
"router"
],
"support": {
"issues": "https://github.com/thephpleague/route/issues",
"source": "https://github.com/thephpleague/route/tree/6.2.0"
},
"funding": [
{
"url": "https://github.com/philipobenito",
"type": "github"
}
],
"time": "2024-11-25T08:10:15+00:00"
},
{
"name": "longman/ip-tools",
"version": "1.2.1",
@ -2669,6 +2759,56 @@
},
"time": "2025-06-03T04:55:08+00:00"
},
{
"name": "nikic/fast-route",
"version": "v1.3.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/FastRoute.git",
"reference": "181d480e08d9476e61381e04a71b34dc0432e812"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812",
"reference": "181d480e08d9476e61381e04a71b34dc0432e812",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35|~5.7"
},
"type": "library",
"autoload": {
"files": [
"src/functions.php"
],
"psr-4": {
"FastRoute\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Nikita Popov",
"email": "nikic@php.net"
}
],
"description": "Fast request router for PHP",
"keywords": [
"router",
"routing"
],
"support": {
"issues": "https://github.com/nikic/FastRoute/issues",
"source": "https://github.com/nikic/FastRoute/tree/master"
},
"time": "2018-02-13T20:26:39+00:00"
},
{
"name": "nikic/iter",
"version": "v2.4.1",
@ -3315,6 +3455,119 @@
},
"time": "2023-04-04T09:54:51+00:00"
},
{
"name": "psr/http-server-handler",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-server-handler.git",
"reference": "84c4fb66179be4caaf8e97bd239203245302e7d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4",
"reference": "84c4fb66179be4caaf8e97bd239203245302e7d4",
"shasum": ""
},
"require": {
"php": ">=7.0",
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Server\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP server-side request handler",
"keywords": [
"handler",
"http",
"http-interop",
"psr",
"psr-15",
"psr-7",
"request",
"response",
"server"
],
"support": {
"source": "https://github.com/php-fig/http-server-handler/tree/1.0.2"
},
"time": "2023-04-10T20:06:20+00:00"
},
{
"name": "psr/http-server-middleware",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-server-middleware.git",
"reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829",
"reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829",
"shasum": ""
},
"require": {
"php": ">=7.0",
"psr/http-message": "^1.0 || ^2.0",
"psr/http-server-handler": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Server\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP server-side middleware",
"keywords": [
"http",
"http-interop",
"middleware",
"psr",
"psr-15",
"psr-7",
"request",
"response"
],
"support": {
"issues": "https://github.com/php-fig/http-server-middleware/issues",
"source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2"
},
"time": "2023-04-11T06:14:47+00:00"
},
{
"name": "psr/log",
"version": "3.0.2",

102
config/routes.php Normal file
View file

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
/**
* Route Configuration
*
* This file contains routing configuration for the TorrentPier application.
* It defines route patterns, middleware, and other routing-related settings.
*
* NOTE: This configuration file is currently not used by the routing system.
* Routes are loaded directly from src/Presentation/Http/Routes/web.php
* This file serves as a template for future routing configuration implementation.
*/
// Configuration is commented out as it's not currently integrated
/*
return [
// Route caching configuration
'cache' => [
'enabled' => env('ROUTE_CACHE_ENABLED', false),
'path' => env('ROUTE_CACHE_PATH', __DIR__ . '/../internal_data/cache/routes.php'),
'ttl' => env('ROUTE_CACHE_TTL', 3600), // 1 hour
],
// Global middleware (applied to all routes)
'middleware' => [
// 'TorrentPier\Infrastructure\Http\Middleware\CorsMiddleware',
// 'TorrentPier\Infrastructure\Http\Middleware\SecurityHeadersMiddleware',
],
// Route groups configuration
'groups' => [
// Web routes (HTML responses)
'web' => [
'prefix' => '',
'middleware' => [
// 'TorrentPier\Infrastructure\Http\Middleware\WebMiddleware',
// 'TorrentPier\Infrastructure\Http\Middleware\StartSession',
// 'TorrentPier\Infrastructure\Http\Middleware\VerifyCsrfToken',
],
],
// API routes (JSON responses)
'api' => [
'prefix' => '/api',
'middleware' => [
// 'TorrentPier\Infrastructure\Http\Middleware\ApiMiddleware',
// 'TorrentPier\Infrastructure\Http\Middleware\RateLimitMiddleware',
// 'TorrentPier\Infrastructure\Http\Middleware\AuthenticationMiddleware',
],
],
// Admin routes (Administrative interface)
'admin' => [
'prefix' => '/admin',
'middleware' => [
// 'TorrentPier\Infrastructure\Http\Middleware\AdminMiddleware',
// 'TorrentPier\Infrastructure\Http\Middleware\RequireAdminAuth',
// 'TorrentPier\Infrastructure\Http\Middleware\AuditLoggingMiddleware',
],
],
],
// Route defaults
'defaults' => [
'namespace' => 'TorrentPier\Presentation\Http\Controllers',
'timeout' => 30, // seconds
'methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
],
// Route constraints
'constraints' => [
'id' => '\d+',
'hash' => '[a-fA-F0-9]+',
'slug' => '[a-z0-9-]+',
'uuid' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
],
// Route patterns for common use cases
'patterns' => [
// Forum routes
'forum' => '/forum/{forum_id:\d+}',
'topic' => '/topic/{topic_id:\d+}',
'post' => '/post/{post_id:\d+}',
// Torrent routes
'torrent' => '/torrent/{torrent_id:\d+}',
'torrent_hash' => '/torrent/{info_hash:[a-fA-F0-9]+}',
// User routes
'user' => '/user/{user_id:\d+}',
'profile' => '/profile/{username:[a-zA-Z0-9_-]+}',
],
// Fallback routes
'fallback' => [
'enabled' => true,
'handler' => 'TorrentPier\Presentation\Http\Controllers\Web\FallbackController@handle',
],
];
*/

437
controllers/index.php Normal file
View file

@ -0,0 +1,437 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
define('BB_SCRIPT', 'index');
// Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
$page_cfg['load_tpl_vars'] = [
'post_icons'
];
// Show last topic
$show_last_topic = true;
$last_topic_max_len = 28;
// Show online stats
$show_online_users = true;
// Show subforums
$show_subforums = true;
$datastore->enqueue([
'stats',
'moderators',
'cat_forums'
]);
if (config()->get('show_latest_news')) {
$datastore->enqueue([
'latest_news'
]);
}
if (config()->get('show_network_news')) {
$datastore->enqueue([
'network_news'
]);
}
// Init userdata
$user->session_start();
// Set meta description
$page_cfg['meta_description'] = config()->get('site_desc');
// Init main vars
$viewcat = isset($_GET[POST_CAT_URL]) ? (int)$_GET[POST_CAT_URL] : 0;
$lastvisit = IS_GUEST ? TIMENOW : $userdata['user_lastvisit'];
// Caching output
$req_page = 'index_page';
$req_page .= $viewcat ? "_c{$viewcat}" : '';
define('REQUESTED_PAGE', $req_page);
caching_output(IS_GUEST, 'send', REQUESTED_PAGE . '_guest_' . config()->get('default_lang'));
$hide_cat_opt = isset($user->opt_js['h_cat']) ? (string)$user->opt_js['h_cat'] : 0;
$hide_cat_user = array_flip(explode('-', $hide_cat_opt));
$showhide = isset($_GET['sh']) ? (int)$_GET['sh'] : 0;
// Topics read tracks
$tracking_topics = get_tracks('topic');
$tracking_forums = get_tracks('forum');
// Statistics
$stats = $datastore->get('stats');
if ($stats === false) {
$datastore->update('stats');
$stats = $datastore->get('stats');
}
// Forums data
$forums = $datastore->get('cat_forums');
if ($forums === false) {
$datastore->update('cat_forums');
$forums = $datastore->get('cat_forums');
}
$cat_title_html = $forums['cat_title_html'];
$forum_name_html = $forums['forum_name_html'];
$anon = GUEST_UID;
$excluded_forums_csv = $user->get_excluded_forums(AUTH_VIEW);
$excluded_forums_array = $excluded_forums_csv ? explode(',', $excluded_forums_csv) : [];
$only_new = $user->opt_js['only_new'];
// Validate requested category id
if ($viewcat && !($viewcat =& $forums['c'][$viewcat]['cat_id'])) {
redirect('index.php');
}
// Forums
$forums_join_sql = 'f.cat_id = c.cat_id';
$forums_join_sql .= $viewcat ? "
AND f.cat_id = $viewcat
" : '';
$forums_join_sql .= $excluded_forums_csv ? "
AND f.forum_id NOT IN($excluded_forums_csv)
AND f.forum_parent NOT IN($excluded_forums_csv)
" : '';
// Posts
$posts_join_sql = 'p.post_id = f.forum_last_post_id';
$posts_join_sql .= ($only_new == ONLY_NEW_POSTS) ? "
AND p.post_time > $lastvisit
" : '';
$join_p_type = ($only_new == ONLY_NEW_POSTS) ? 'INNER JOIN' : 'LEFT JOIN';
// Topics
$topics_join_sql = 't.topic_last_post_id = p.post_id';
$topics_join_sql .= ($only_new == ONLY_NEW_TOPICS) ? "
AND t.topic_time > $lastvisit
" : '';
$join_t_type = ($only_new == ONLY_NEW_TOPICS) ? 'INNER JOIN' : 'LEFT JOIN';
$sql = "
SELECT f.cat_id, f.forum_id, f.forum_status, f.forum_parent, f.show_on_index,
p.post_id AS last_post_id, p.post_time AS last_post_time,
t.topic_id AS last_topic_id, t.topic_title AS last_topic_title,
u.user_id AS last_post_user_id, u.user_rank AS last_post_user_rank,
IF(p.poster_id = $anon, p.post_username, u.username) AS last_post_username
FROM " . BB_CATEGORIES . ' c
INNER JOIN ' . BB_FORUMS . " f ON($forums_join_sql)
$join_p_type " . BB_POSTS . " p ON($posts_join_sql)
$join_t_type " . BB_TOPICS . " t ON($topics_join_sql)
LEFT JOIN " . BB_USERS . ' u ON(u.user_id = p.poster_id)
ORDER BY c.cat_order, f.forum_order
';
$replace_in_parent = [
'last_post_id',
'last_post_time',
'last_post_user_id',
'last_post_username',
'last_post_user_rank',
'last_topic_title',
'last_topic_id'
];
$cache_name = 'index_sql_' . hash('xxh128', $sql);
if (!$cat_forums = CACHE('bb_cache')->get($cache_name)) {
$cat_forums = [];
foreach (DB()->fetch_rowset($sql) as $row) {
if (!($cat_id = $row['cat_id']) || !($forum_id = $row['forum_id'])) {
continue;
}
if ($parent_id = $row['forum_parent']) {
if (!$parent =& $cat_forums[$cat_id]['f'][$parent_id]) {
$parent = $forums['f'][$parent_id];
$parent['last_post_time'] = 0;
}
if ($row['last_post_time'] > $parent['last_post_time']) {
foreach ($replace_in_parent as $key) {
$parent[$key] = $row[$key];
}
}
if ($show_subforums && $row['show_on_index']) {
$parent['last_sf_id'] = $forum_id;
} else {
continue;
}
} else {
$f =& $forums['f'][$forum_id];
$row['forum_desc'] = $f['forum_desc'];
$row['forum_posts'] = $f['forum_posts'];
$row['forum_topics'] = $f['forum_topics'];
}
$cat_forums[$cat_id]['f'][$forum_id] = $row;
}
CACHE('bb_cache')->set($cache_name, $cat_forums, 180);
unset($row, $forums);
$datastore->rm('cat_forums');
}
// Obtain list of moderators
$moderators = [];
$mod = $datastore->get('moderators');
if ($mod === false) {
$datastore->update('moderators');
$mod = $datastore->get('moderators');
}
if (!empty($mod)) {
foreach ($mod['mod_users'] as $forum_id => $user_ids) {
foreach ($user_ids as $user_id) {
$moderators[$forum_id][] = '<a href="' . PROFILE_URL . $user_id . '">' . $mod['name_users'][$user_id] . '</a>';
}
}
foreach ($mod['mod_groups'] as $forum_id => $group_ids) {
foreach ($group_ids as $group_id) {
$moderators[$forum_id][] = '<a href="' . GROUP_URL . $group_id . '">' . $mod['name_groups'][$group_id] . '</a>';
}
}
}
unset($mod);
$datastore->rm('moderators');
// Build index page
$forums_count = 0;
foreach ($cat_forums as $cid => $c) {
$template->assign_block_vars('h_c', [
'H_C_ID' => $cid,
'H_C_TITLE' => $cat_title_html[$cid],
'H_C_CHEKED' => in_array($cid, preg_split('/[-]+/', $hide_cat_opt)) ? 'checked' : '',
]);
$template->assign_vars(['H_C_AL_MESS' => $hide_cat_opt && !$showhide]);
if (!$showhide && isset($hide_cat_user[$cid]) && !$viewcat) {
continue;
}
$template->assign_block_vars('c', [
'CAT_ID' => $cid,
'CAT_TITLE' => $cat_title_html[$cid],
'U_VIEWCAT' => CAT_URL . $cid,
]);
foreach ($c['f'] as $fid => $f) {
if (!$fname_html =& $forum_name_html[$fid]) {
continue;
}
$is_sf = $f['forum_parent'];
$forums_count++;
$new = is_unread($f['last_post_time'], $f['last_topic_id'], $f['forum_id']) ? '_new' : '';
$folder_image = $is_sf ? $images["icon_minipost{$new}"] : $images["forum{$new}"];
if ($f['forum_status'] == FORUM_LOCKED) {
$folder_image = $is_sf ? $images['icon_minipost'] : $images['forum_locked'];
}
if ($is_sf) {
$template->assign_block_vars('c.f.sf', [
'SF_ID' => $fid,
'SF_NAME' => $fname_html,
'SF_NEW' => $new ? ' new' : ''
]);
continue;
}
$template->assign_block_vars('c.f', [
'FORUM_FOLDER_IMG' => $folder_image,
'FORUM_ID' => $fid,
'FORUM_NAME' => $fname_html,
'FORUM_DESC' => $f['forum_desc'],
'POSTS' => commify($f['forum_posts']),
'TOPICS' => commify($f['forum_topics']),
'LAST_SF_ID' => $f['last_sf_id'] ?? null,
'MODERATORS' => isset($moderators[$fid]) ? implode(', ', $moderators[$fid]) : '',
'FORUM_FOLDER_ALT' => $new ? $lang['NEW'] : $lang['OLD']
]);
if ($f['last_post_id']) {
$template->assign_block_vars('c.f.last', [
'LAST_TOPIC_ID' => $f['last_topic_id'],
'LAST_TOPIC_TIP' => $f['last_topic_title'],
'LAST_TOPIC_TITLE' => str_short($f['last_topic_title'], $last_topic_max_len),
'LAST_POST_TIME' => bb_date($f['last_post_time'], config()->get('last_post_date_format')),
'LAST_POST_USER' => profile_url(['username' => str_short($f['last_post_username'], 15), 'user_id' => $f['last_post_user_id'], 'user_rank' => $f['last_post_user_rank']]),
]);
}
}
}
$template->assign_vars([
'SHOW_FORUMS' => $forums_count,
'SHOW_MAP' => isset($_GET['map']) && !IS_GUEST,
'PAGE_TITLE' => $viewcat ? $cat_title_html[$viewcat] : $lang['HOME'],
'NO_FORUMS_MSG' => $only_new ? $lang['NO_NEW_POSTS'] : $lang['NO_FORUMS'],
'TOTAL_TOPICS' => sprintf($lang['POSTED_TOPICS_TOTAL'], $stats['topiccount']),
'TOTAL_POSTS' => sprintf($lang['POSTED_ARTICLES_TOTAL'], $stats['postcount']),
'TOTAL_USERS' => sprintf($lang['REGISTERED_USERS_TOTAL'], $stats['usercount']),
'TOTAL_GENDER' => config()->get('gender') ? sprintf(
$lang['USERS_TOTAL_GENDER'],
$stats['male'],
$stats['female'],
$stats['unselect']
) : '',
'NEWEST_USER' => sprintf($lang['NEWEST_USER'], profile_url($stats['newestuser'])),
// Tracker stats
'TORRENTS_STAT' => config()->get('tor_stats') ? sprintf(
$lang['TORRENTS_STAT'],
$stats['torrentcount'],
humn_size($stats['size'])
) : '',
'PEERS_STAT' => config()->get('tor_stats') ? sprintf(
$lang['PEERS_STAT'],
$stats['peers'],
$stats['seeders'],
$stats['leechers']
) : '',
'SPEED_STAT' => config()->get('tor_stats') ? sprintf(
$lang['SPEED_STAT'],
humn_size($stats['speed']) . '/s'
) : '',
'SHOW_MOD_INDEX' => config()->get('show_mod_index'),
'FORUM_IMG' => $images['forum'],
'FORUM_NEW_IMG' => $images['forum_new'],
'FORUM_LOCKED_IMG' => $images['forum_locked'],
'SHOW_ONLY_NEW_MENU' => true,
'ONLY_NEW_POSTS_ON' => $only_new == ONLY_NEW_POSTS,
'ONLY_NEW_TOPICS_ON' => $only_new == ONLY_NEW_TOPICS,
'U_SEARCH_NEW' => 'search.php?new=1',
'U_SEARCH_SELF_BY_MY' => "search.php?uid={$userdata['user_id']}&amp;o=1",
'U_SEARCH_LATEST' => 'search.php?search_id=latest',
'U_SEARCH_UNANSWERED' => 'search.php?search_id=unanswered',
'U_ATOM_FEED' => is_file(config()->get('atom.path') . '/f/0.atom') ? make_url(config()->get('atom.url') . '/f/0.atom') : false,
'SHOW_LAST_TOPIC' => $show_last_topic,
'BOARD_START' => config()->get('show_board_start_index') ? ($lang['BOARD_STARTED'] . ':&nbsp;' . '<b>' . bb_date(config()->get('board_startdate')) . '</b>') : false,
]);
// Set tpl vars for bt_userdata
if (config()->get('bt_show_dl_stat_on_index') && !IS_GUEST) {
show_bt_userdata($userdata['user_id']);
}
// Latest news
if (config()->get('show_latest_news')) {
$latest_news = $datastore->get('latest_news');
if ($latest_news === false) {
$datastore->update('latest_news');
$latest_news = $datastore->get('latest_news');
}
$template->assign_vars(['SHOW_LATEST_NEWS' => true]);
foreach ($latest_news as $news) {
if (in_array($news['forum_id'], $excluded_forums_array)) {
continue;
}
$template->assign_block_vars('news', [
'NEWS_TOPIC_ID' => $news['topic_id'],
'NEWS_TITLE' => str_short(censor()->censorString($news['topic_title']), config()->get('max_news_title')),
'NEWS_TIME' => bb_date($news['topic_time'], 'd-M', false),
'NEWS_IS_NEW' => is_unread($news['topic_time'], $news['topic_id'], $news['forum_id']),
]);
}
}
// Network news
if (config()->get('show_network_news')) {
$network_news = $datastore->get('network_news');
if ($network_news === false) {
$datastore->update('network_news');
$network_news = $datastore->get('network_news');
}
$template->assign_vars(['SHOW_NETWORK_NEWS' => true]);
foreach ($network_news as $net) {
if (in_array($net['forum_id'], $excluded_forums_array)) {
continue;
}
$template->assign_block_vars('net', [
'NEWS_TOPIC_ID' => $net['topic_id'],
'NEWS_TITLE' => str_short(censor()->censorString($net['topic_title']), config()->get('max_net_title')),
'NEWS_TIME' => bb_date($net['topic_time'], 'd-M', false),
'NEWS_IS_NEW' => is_unread($net['topic_time'], $net['topic_id'], $net['forum_id']),
]);
}
}
if (config()->get('birthday_check_day') && config()->get('birthday_enabled')) {
$week_list = $today_list = [];
$week_all = $today_all = false;
if (!empty($stats['birthday_week_list'])) {
shuffle($stats['birthday_week_list']);
foreach ($stats['birthday_week_list'] as $i => $week) {
if ($i >= 5) {
$week_all = true;
continue;
}
$week_list[] = profile_url($week) . ' <span class="small">(' . birthday_age(date('Y-m-d', strtotime('-1 year', strtotime($week['user_birthday'])))) . ')</span>';
}
$week_all = $week_all ? '&nbsp;<a class="txtb" href="#" onclick="ajax.exec({action: \'index_data\', mode: \'birthday_week\'}); return false;" title="' . $lang['ALL'] . '">...</a>' : '';
$week_list = sprintf($lang['BIRTHDAY_WEEK'], config()->get('birthday_check_day'), implode(', ', $week_list)) . $week_all;
} else {
$week_list = sprintf($lang['NOBIRTHDAY_WEEK'], config()->get('birthday_check_day'));
}
if (!empty($stats['birthday_today_list'])) {
shuffle($stats['birthday_today_list']);
foreach ($stats['birthday_today_list'] as $i => $today) {
if ($i >= 5) {
$today_all = true;
continue;
}
$today_list[] = profile_url($today) . ' <span class="small">(' . birthday_age($today['user_birthday']) . ')</span>';
}
$today_all = $today_all ? '&nbsp;<a class="txtb" href="#" onclick="ajax.exec({action: \'index_data\', mode: \'birthday_today\'}); return false;" title="' . $lang['ALL'] . '">...</a>' : '';
$today_list = $lang['BIRTHDAY_TODAY'] . implode(', ', $today_list) . $today_all;
} else {
$today_list = $lang['NOBIRTHDAY_TODAY'];
}
$template->assign_vars([
'WHOSBIRTHDAY_WEEK' => $week_list,
'WHOSBIRTHDAY_TODAY' => $today_list
]);
}
// Allow cron
if (IS_AM) {
if (is_file(CRON_RUNNING)) {
if (is_file(CRON_ALLOWED)) {
unlink(CRON_ALLOWED);
}
rename(CRON_RUNNING, CRON_ALLOWED);
}
}
// Display page
define('SHOW_ONLINE', $show_online_users);
if (isset($_GET['map'])) {
$template->assign_vars(['PAGE_TITLE' => $lang['FORUM_MAP']]);
}
print_page('index.tpl');

View file

@ -9,7 +9,10 @@
define('BB_SCRIPT', 'terms');
require __DIR__ . '/common.php';
// Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
require INC_DIR . '/bbcode.php';
// Start session management

447
index.php
View file

@ -1,434 +1,35 @@
<?php
<?php /** @noinspection PhpDefineCanBeReplacedWithConstInspection */
declare(strict_types=1);
/**
* TorrentPier Bull-powered BitTorrent tracker engine
* Modern routing entry point for TorrentPier 3.0
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
* This file bootstraps the new hexagonal architecture routing system
* using league/route and the dependency injection container.
*/
define('BB_SCRIPT', 'index');
// Bootstrap autoloader
require_once __DIR__ . '/vendor/autoload.php';
require __DIR__ . '/common.php';
// Load legacy common.php to initialize global functions and legacy components
require_once __DIR__ . '/common.php';
$page_cfg['load_tpl_vars'] = [
'post_icons'
];
// Using define() instead of const to ensure global accessibility for legacy controllers
define('IN_TORRENTPIER', true);
// Show last topic
$show_last_topic = true;
$last_topic_max_len = 28;
use TorrentPier\Infrastructure\DependencyInjection\Bootstrap;
use TorrentPier\Presentation\Http\Kernel;
// Show online stats
$show_online_users = true;
// Initialize the dependency injection container
$container = Bootstrap::init(__DIR__);
// Show subforums
$show_subforums = true;
// Get the HTTP kernel from the container
$kernel = $container->get(Kernel::class);
$datastore->enqueue([
'stats',
'moderators',
'cat_forums'
]);
// Load web routes
$routesFile = __DIR__ . '/src/Presentation/Http/Routes/web.php';
$kernel->loadRoutes($routesFile);
if (config()->get('show_latest_news')) {
$datastore->enqueue([
'latest_news'
]);
}
if (config()->get('show_network_news')) {
$datastore->enqueue([
'network_news'
]);
}
// Init userdata
$user->session_start();
// Set meta description
$page_cfg['meta_description'] = config()->get('site_desc');
// Init main vars
$viewcat = isset($_GET[POST_CAT_URL]) ? (int)$_GET[POST_CAT_URL] : 0;
$lastvisit = IS_GUEST ? TIMENOW : $userdata['user_lastvisit'];
// Caching output
$req_page = 'index_page';
$req_page .= $viewcat ? "_c{$viewcat}" : '';
define('REQUESTED_PAGE', $req_page);
caching_output(IS_GUEST, 'send', REQUESTED_PAGE . '_guest_' . config()->get('default_lang'));
$hide_cat_opt = isset($user->opt_js['h_cat']) ? (string)$user->opt_js['h_cat'] : 0;
$hide_cat_user = array_flip(explode('-', $hide_cat_opt));
$showhide = isset($_GET['sh']) ? (int)$_GET['sh'] : 0;
// Topics read tracks
$tracking_topics = get_tracks('topic');
$tracking_forums = get_tracks('forum');
// Statistics
$stats = $datastore->get('stats');
if ($stats === false) {
$datastore->update('stats');
$stats = $datastore->get('stats');
}
// Forums data
$forums = $datastore->get('cat_forums');
if ($forums === false) {
$datastore->update('cat_forums');
$forums = $datastore->get('cat_forums');
}
$cat_title_html = $forums['cat_title_html'];
$forum_name_html = $forums['forum_name_html'];
$anon = GUEST_UID;
$excluded_forums_csv = $user->get_excluded_forums(AUTH_VIEW);
$excluded_forums_array = $excluded_forums_csv ? explode(',', $excluded_forums_csv) : [];
$only_new = $user->opt_js['only_new'];
// Validate requested category id
if ($viewcat && !($viewcat =& $forums['c'][$viewcat]['cat_id'])) {
redirect('index.php');
}
// Forums
$forums_join_sql = 'f.cat_id = c.cat_id';
$forums_join_sql .= $viewcat ? "
AND f.cat_id = $viewcat
" : '';
$forums_join_sql .= $excluded_forums_csv ? "
AND f.forum_id NOT IN($excluded_forums_csv)
AND f.forum_parent NOT IN($excluded_forums_csv)
" : '';
// Posts
$posts_join_sql = 'p.post_id = f.forum_last_post_id';
$posts_join_sql .= ($only_new == ONLY_NEW_POSTS) ? "
AND p.post_time > $lastvisit
" : '';
$join_p_type = ($only_new == ONLY_NEW_POSTS) ? 'INNER JOIN' : 'LEFT JOIN';
// Topics
$topics_join_sql = 't.topic_last_post_id = p.post_id';
$topics_join_sql .= ($only_new == ONLY_NEW_TOPICS) ? "
AND t.topic_time > $lastvisit
" : '';
$join_t_type = ($only_new == ONLY_NEW_TOPICS) ? 'INNER JOIN' : 'LEFT JOIN';
$sql = "
SELECT f.cat_id, f.forum_id, f.forum_status, f.forum_parent, f.show_on_index,
p.post_id AS last_post_id, p.post_time AS last_post_time,
t.topic_id AS last_topic_id, t.topic_title AS last_topic_title,
u.user_id AS last_post_user_id, u.user_rank AS last_post_user_rank,
IF(p.poster_id = $anon, p.post_username, u.username) AS last_post_username
FROM " . BB_CATEGORIES . ' c
INNER JOIN ' . BB_FORUMS . " f ON($forums_join_sql)
$join_p_type " . BB_POSTS . " p ON($posts_join_sql)
$join_t_type " . BB_TOPICS . " t ON($topics_join_sql)
LEFT JOIN " . BB_USERS . ' u ON(u.user_id = p.poster_id)
ORDER BY c.cat_order, f.forum_order
';
$replace_in_parent = [
'last_post_id',
'last_post_time',
'last_post_user_id',
'last_post_username',
'last_post_user_rank',
'last_topic_title',
'last_topic_id'
];
$cache_name = 'index_sql_' . hash('xxh128', $sql);
if (!$cat_forums = CACHE('bb_cache')->get($cache_name)) {
$cat_forums = [];
foreach (DB()->fetch_rowset($sql) as $row) {
if (!($cat_id = $row['cat_id']) || !($forum_id = $row['forum_id'])) {
continue;
}
if ($parent_id = $row['forum_parent']) {
if (!$parent =& $cat_forums[$cat_id]['f'][$parent_id]) {
$parent = $forums['f'][$parent_id];
$parent['last_post_time'] = 0;
}
if ($row['last_post_time'] > $parent['last_post_time']) {
foreach ($replace_in_parent as $key) {
$parent[$key] = $row[$key];
}
}
if ($show_subforums && $row['show_on_index']) {
$parent['last_sf_id'] = $forum_id;
} else {
continue;
}
} else {
$f =& $forums['f'][$forum_id];
$row['forum_desc'] = $f['forum_desc'];
$row['forum_posts'] = $f['forum_posts'];
$row['forum_topics'] = $f['forum_topics'];
}
$cat_forums[$cat_id]['f'][$forum_id] = $row;
}
CACHE('bb_cache')->set($cache_name, $cat_forums, 180);
unset($row, $forums);
$datastore->rm('cat_forums');
}
// Obtain list of moderators
$moderators = [];
$mod = $datastore->get('moderators');
if ($mod === false) {
$datastore->update('moderators');
$mod = $datastore->get('moderators');
}
if (!empty($mod)) {
foreach ($mod['mod_users'] as $forum_id => $user_ids) {
foreach ($user_ids as $user_id) {
$moderators[$forum_id][] = '<a href="' . PROFILE_URL . $user_id . '">' . $mod['name_users'][$user_id] . '</a>';
}
}
foreach ($mod['mod_groups'] as $forum_id => $group_ids) {
foreach ($group_ids as $group_id) {
$moderators[$forum_id][] = '<a href="' . GROUP_URL . $group_id . '">' . $mod['name_groups'][$group_id] . '</a>';
}
}
}
unset($mod);
$datastore->rm('moderators');
// Build index page
$forums_count = 0;
foreach ($cat_forums as $cid => $c) {
$template->assign_block_vars('h_c', [
'H_C_ID' => $cid,
'H_C_TITLE' => $cat_title_html[$cid],
'H_C_CHEKED' => in_array($cid, preg_split('/[-]+/', $hide_cat_opt)) ? 'checked' : '',
]);
$template->assign_vars(['H_C_AL_MESS' => $hide_cat_opt && !$showhide]);
if (!$showhide && isset($hide_cat_user[$cid]) && !$viewcat) {
continue;
}
$template->assign_block_vars('c', [
'CAT_ID' => $cid,
'CAT_TITLE' => $cat_title_html[$cid],
'U_VIEWCAT' => CAT_URL . $cid,
]);
foreach ($c['f'] as $fid => $f) {
if (!$fname_html =& $forum_name_html[$fid]) {
continue;
}
$is_sf = $f['forum_parent'];
$forums_count++;
$new = is_unread($f['last_post_time'], $f['last_topic_id'], $f['forum_id']) ? '_new' : '';
$folder_image = $is_sf ? $images["icon_minipost{$new}"] : $images["forum{$new}"];
if ($f['forum_status'] == FORUM_LOCKED) {
$folder_image = $is_sf ? $images['icon_minipost'] : $images['forum_locked'];
}
if ($is_sf) {
$template->assign_block_vars('c.f.sf', [
'SF_ID' => $fid,
'SF_NAME' => $fname_html,
'SF_NEW' => $new ? ' new' : ''
]);
continue;
}
$template->assign_block_vars('c.f', [
'FORUM_FOLDER_IMG' => $folder_image,
'FORUM_ID' => $fid,
'FORUM_NAME' => $fname_html,
'FORUM_DESC' => $f['forum_desc'],
'POSTS' => commify($f['forum_posts']),
'TOPICS' => commify($f['forum_topics']),
'LAST_SF_ID' => $f['last_sf_id'] ?? null,
'MODERATORS' => isset($moderators[$fid]) ? implode(', ', $moderators[$fid]) : '',
'FORUM_FOLDER_ALT' => $new ? $lang['NEW'] : $lang['OLD']
]);
if ($f['last_post_id']) {
$template->assign_block_vars('c.f.last', [
'LAST_TOPIC_ID' => $f['last_topic_id'],
'LAST_TOPIC_TIP' => $f['last_topic_title'],
'LAST_TOPIC_TITLE' => str_short($f['last_topic_title'], $last_topic_max_len),
'LAST_POST_TIME' => bb_date($f['last_post_time'], config()->get('last_post_date_format')),
'LAST_POST_USER' => profile_url(['username' => str_short($f['last_post_username'], 15), 'user_id' => $f['last_post_user_id'], 'user_rank' => $f['last_post_user_rank']]),
]);
}
}
}
$template->assign_vars([
'SHOW_FORUMS' => $forums_count,
'SHOW_MAP' => isset($_GET['map']) && !IS_GUEST,
'PAGE_TITLE' => $viewcat ? $cat_title_html[$viewcat] : $lang['HOME'],
'NO_FORUMS_MSG' => $only_new ? $lang['NO_NEW_POSTS'] : $lang['NO_FORUMS'],
'TOTAL_TOPICS' => sprintf($lang['POSTED_TOPICS_TOTAL'], $stats['topiccount']),
'TOTAL_POSTS' => sprintf($lang['POSTED_ARTICLES_TOTAL'], $stats['postcount']),
'TOTAL_USERS' => sprintf($lang['REGISTERED_USERS_TOTAL'], $stats['usercount']),
'TOTAL_GENDER' => config()->get('gender') ? sprintf(
$lang['USERS_TOTAL_GENDER'],
$stats['male'],
$stats['female'],
$stats['unselect']
) : '',
'NEWEST_USER' => sprintf($lang['NEWEST_USER'], profile_url($stats['newestuser'])),
// Tracker stats
'TORRENTS_STAT' => config()->get('tor_stats') ? sprintf(
$lang['TORRENTS_STAT'],
$stats['torrentcount'],
humn_size($stats['size'])
) : '',
'PEERS_STAT' => config()->get('tor_stats') ? sprintf(
$lang['PEERS_STAT'],
$stats['peers'],
$stats['seeders'],
$stats['leechers']
) : '',
'SPEED_STAT' => config()->get('tor_stats') ? sprintf(
$lang['SPEED_STAT'],
humn_size($stats['speed']) . '/s'
) : '',
'SHOW_MOD_INDEX' => config()->get('show_mod_index'),
'FORUM_IMG' => $images['forum'],
'FORUM_NEW_IMG' => $images['forum_new'],
'FORUM_LOCKED_IMG' => $images['forum_locked'],
'SHOW_ONLY_NEW_MENU' => true,
'ONLY_NEW_POSTS_ON' => $only_new == ONLY_NEW_POSTS,
'ONLY_NEW_TOPICS_ON' => $only_new == ONLY_NEW_TOPICS,
'U_SEARCH_NEW' => 'search.php?new=1',
'U_SEARCH_SELF_BY_MY' => "search.php?uid={$userdata['user_id']}&amp;o=1",
'U_SEARCH_LATEST' => 'search.php?search_id=latest',
'U_SEARCH_UNANSWERED' => 'search.php?search_id=unanswered',
'U_ATOM_FEED' => is_file(config()->get('atom.path') . '/f/0.atom') ? make_url(config()->get('atom.url') . '/f/0.atom') : false,
'SHOW_LAST_TOPIC' => $show_last_topic,
'BOARD_START' => config()->get('show_board_start_index') ? ($lang['BOARD_STARTED'] . ':&nbsp;' . '<b>' . bb_date(config()->get('board_startdate')) . '</b>') : false,
]);
// Set tpl vars for bt_userdata
if (config()->get('bt_show_dl_stat_on_index') && !IS_GUEST) {
show_bt_userdata($userdata['user_id']);
}
// Latest news
if (config()->get('show_latest_news')) {
$latest_news = $datastore->get('latest_news');
if ($latest_news === false) {
$datastore->update('latest_news');
$latest_news = $datastore->get('latest_news');
}
$template->assign_vars(['SHOW_LATEST_NEWS' => true]);
foreach ($latest_news as $news) {
if (in_array($news['forum_id'], $excluded_forums_array)) {
continue;
}
$template->assign_block_vars('news', [
'NEWS_TOPIC_ID' => $news['topic_id'],
'NEWS_TITLE' => str_short(censor()->censorString($news['topic_title']), config()->get('max_news_title')),
'NEWS_TIME' => bb_date($news['topic_time'], 'd-M', false),
'NEWS_IS_NEW' => is_unread($news['topic_time'], $news['topic_id'], $news['forum_id']),
]);
}
}
// Network news
if (config()->get('show_network_news')) {
$network_news = $datastore->get('network_news');
if ($network_news === false) {
$datastore->update('network_news');
$network_news = $datastore->get('network_news');
}
$template->assign_vars(['SHOW_NETWORK_NEWS' => true]);
foreach ($network_news as $net) {
if (in_array($net['forum_id'], $excluded_forums_array)) {
continue;
}
$template->assign_block_vars('net', [
'NEWS_TOPIC_ID' => $net['topic_id'],
'NEWS_TITLE' => str_short(censor()->censorString($net['topic_title']), config()->get('max_net_title')),
'NEWS_TIME' => bb_date($net['topic_time'], 'd-M', false),
'NEWS_IS_NEW' => is_unread($net['topic_time'], $net['topic_id'], $net['forum_id']),
]);
}
}
if (config()->get('birthday_check_day') && config()->get('birthday_enabled')) {
$week_list = $today_list = [];
$week_all = $today_all = false;
if (!empty($stats['birthday_week_list'])) {
shuffle($stats['birthday_week_list']);
foreach ($stats['birthday_week_list'] as $i => $week) {
if ($i >= 5) {
$week_all = true;
continue;
}
$week_list[] = profile_url($week) . ' <span class="small">(' . birthday_age(date('Y-m-d', strtotime('-1 year', strtotime($week['user_birthday'])))) . ')</span>';
}
$week_all = $week_all ? '&nbsp;<a class="txtb" href="#" onclick="ajax.exec({action: \'index_data\', mode: \'birthday_week\'}); return false;" title="' . $lang['ALL'] . '">...</a>' : '';
$week_list = sprintf($lang['BIRTHDAY_WEEK'], config()->get('birthday_check_day'), implode(', ', $week_list)) . $week_all;
} else {
$week_list = sprintf($lang['NOBIRTHDAY_WEEK'], config()->get('birthday_check_day'));
}
if (!empty($stats['birthday_today_list'])) {
shuffle($stats['birthday_today_list']);
foreach ($stats['birthday_today_list'] as $i => $today) {
if ($i >= 5) {
$today_all = true;
continue;
}
$today_list[] = profile_url($today) . ' <span class="small">(' . birthday_age($today['user_birthday']) . ')</span>';
}
$today_all = $today_all ? '&nbsp;<a class="txtb" href="#" onclick="ajax.exec({action: \'index_data\', mode: \'birthday_today\'}); return false;" title="' . $lang['ALL'] . '">...</a>' : '';
$today_list = $lang['BIRTHDAY_TODAY'] . implode(', ', $today_list) . $today_all;
} else {
$today_list = $lang['NOBIRTHDAY_TODAY'];
}
$template->assign_vars([
'WHOSBIRTHDAY_WEEK' => $week_list,
'WHOSBIRTHDAY_TODAY' => $today_list
]);
}
// Allow cron
if (IS_AM) {
if (is_file(CRON_RUNNING)) {
if (is_file(CRON_ALLOWED)) {
unlink(CRON_ALLOWED);
}
rename(CRON_RUNNING, CRON_ALLOWED);
}
}
// Display page
define('SHOW_ONLINE', $show_online_users);
if (isset($_GET['map'])) {
$template->assign_vars(['PAGE_TITLE' => $lang['FORUM_MAP']]);
}
print_page('index.tpl');
// Handle the request and send response
$kernel->run();

View file

@ -108,13 +108,16 @@ $template->assign_vars([
'HAVE_UNREAD_PM' => $have_unread_pm
]);
// Defines the current displayed controller to hide frontend blocks
$bb_script = defined('BB_SCRIPT') ? BB_SCRIPT : null;
// The following assigns all _common_ variables that may be used at any point in a template
$template->assign_vars([
'SIMPLE_HEADER' => !empty($gen_simple_header),
'CONTENT_ENCODING' => DEFAULT_CHARSET,
'IN_ADMIN' => defined('IN_ADMIN'),
'USER_HIDE_CAT' => (BB_SCRIPT == 'index'),
'USER_HIDE_CAT' => ($bb_script == 'index'),
'USER_LANG' => $userdata['user_lang'],
@ -170,8 +173,8 @@ $template->assign_vars([
'U_TERMS' => config()->get('terms_and_conditions_url'),
'U_TRACKER' => 'tracker.php',
'SHOW_SIDEBAR1' => !empty(config()->get('page.show_sidebar1')[BB_SCRIPT]) || config()->get('show_sidebar1_on_every_page'),
'SHOW_SIDEBAR2' => !empty(config()->get('page.show_sidebar2')[BB_SCRIPT]) || config()->get('show_sidebar2_on_every_page'),
'SHOW_SIDEBAR1' => !empty(config()->get('page.show_sidebar1')[$bb_script]) || config()->get('show_sidebar1_on_every_page'),
'SHOW_SIDEBAR2' => !empty(config()->get('page.show_sidebar2')[$bb_script]) || config()->get('show_sidebar2_on_every_page'),
'HTML_AGREEMENT' => LANG_DIR . 'html/user_agreement.html',
'HTML_COPYRIGHT' => LANG_DIR . 'html/copyright_holders.html',
@ -208,7 +211,7 @@ $template->assign_vars([
'U_WATCHED_TOPICS' => 'profile.php?mode=watch'
]);
if (!empty(config()->get('page.show_torhelp')[BB_SCRIPT]) && !empty($userdata['torhelp'])) {
if (!empty(config()->get('page.show_torhelp')[$bb_script]) && !empty($userdata['torhelp'])) {
$ignore_time = !empty($_COOKIE['torhelp']) ? (int)$_COOKIE['torhelp'] : 0;
if (TIMENOW > $ignore_time) {

View file

@ -23,13 +23,15 @@ if (file_exists(__DIR__ . '/.env')) {
}
// Helper function for environment variables
function env(string $key, mixed $default = null): mixed
{
$value = $_ENV[$key] ?? getenv($key);
if ($value === false) {
return $default;
if (!function_exists('env')) {
function env(string $key, mixed $default = null): mixed
{
$value = $_ENV[$key] ?? getenv($key);
if ($value === false) {
return $default;
}
return $value;
}
return $value;
}
return [

View file

@ -11,12 +11,27 @@ use Nette\Caching\Storages\MemcachedStorage;
use Nette\Caching\Storages\SQLiteStorage;
use Nette\Database\Connection;
use Psr\Container\ContainerInterface;
use TorrentPier\Config;
class InfrastructureDefinitions
{
public static function getDefinitions(array $config = []): array
{
return [
// Configuration
Config::class => DI\factory(function () {
// Use the config singleton that's already initialized in common.php
return config();
}),
// HTTP Infrastructure
'TorrentPier\Infrastructure\Http\Router' => DI\autowire(),
'TorrentPier\Infrastructure\Http\RequestFactory' => DI\autowire(),
'TorrentPier\Infrastructure\Http\ResponseFactory' => DI\autowire(),
// Middleware
'TorrentPier\Infrastructure\Http\Middleware\CorsMiddleware' => DI\autowire(),
// TODO: Add infrastructure service definitions as they are implemented
// Example: Database Connection (implement when Nette Database integration is ready)

View file

@ -12,8 +12,13 @@ class PresentationDefinitions
public static function getDefinitions(): array
{
return [
// HTTP Kernel
'TorrentPier\Presentation\Http\Kernel' => DI\autowire(),
// HTTP Controllers
// Controllers are typically autowired with their dependencies
'TorrentPier\Presentation\Http\Controllers\Web\HelloWorldController' => DI\autowire(),
'TorrentPier\Presentation\Http\Controllers\Web\LegacyController' => DI\autowire(),
// Web Controllers
// 'TorrentPier\Presentation\Http\Controllers\Web\HomeController' => DI\autowire(),

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Infrastructure\Http\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
abstract class BaseMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$request = $this->before($request);
$response = $handler->handle($request);
return $this->after($request, $response);
}
protected function before(ServerRequestInterface $request): ServerRequestInterface
{
return $request;
}
protected function after(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
return $response;
}
}

View file

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Infrastructure\Http\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
class CorsMiddleware extends BaseMiddleware
{
private array $allowedOrigins;
private array $allowedHeaders;
private array $allowedMethods;
public function __construct(
array $allowedOrigins = ['*'],
array $allowedHeaders = ['Content-Type', 'Authorization', 'X-Requested-With'],
array $allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
)
{
$this->allowedOrigins = $allowedOrigins;
$this->allowedHeaders = $allowedHeaders;
$this->allowedMethods = $allowedMethods;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ($request->getMethod() === 'OPTIONS') {
return $this->createPreflightResponse();
}
$response = $handler->handle($request);
return $this->addCorsHeaders($response, $request);
}
private function createPreflightResponse(): ResponseInterface
{
$response = new \GuzzleHttp\Psr7\Response(200);
return $this->addCorsHeaders($response);
}
private function addCorsHeaders(ResponseInterface $response, ?ServerRequestInterface $request = null): ResponseInterface
{
$origin = $request ? $request->getHeaderLine('Origin') : '';
if (in_array('*', $this->allowedOrigins) || in_array($origin, $this->allowedOrigins)) {
$response = $response->withHeader('Access-Control-Allow-Origin', $origin ?: '*');
}
$response = $response->withHeader('Access-Control-Allow-Methods', implode(', ', $this->allowedMethods));
$response = $response->withHeader('Access-Control-Allow-Headers', implode(', ', $this->allowedHeaders));
$response = $response->withHeader('Access-Control-Max-Age', '86400');
return $response;
}
}

View file

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Infrastructure\Http;
use GuzzleHttp\Psr7\ServerRequest;
use Psr\Http\Message\ServerRequestInterface;
class RequestFactory
{
public static function fromGlobals(): ServerRequestInterface
{
return ServerRequest::fromGlobals();
}
public static function create(
string $method = 'GET',
string $uri = '/',
array $headers = [],
$body = null,
string $protocolVersion = '1.1'
): ServerRequestInterface
{
$request = new ServerRequest($method, $uri, $headers, $body, $protocolVersion);
return $request;
}
}

View file

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Infrastructure\Http;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
class ResponseFactory
{
public static function create(
int $status = 200,
array $headers = [],
$body = null,
string $protocolVersion = '1.1'
): ResponseInterface
{
$response = new Response($status, $headers, $body);
return $response->withProtocolVersion($protocolVersion);
}
public static function json(
array $data,
int $status = 200,
array $headers = [],
int $encodingOptions = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT
): ResponseInterface
{
$json = json_encode($data, $encodingOptions);
$headers['content-type'] = 'application/json; charset=utf-8';
return new Response($status, $headers, $json);
}
public static function html(
string $html,
int $status = 200,
array $headers = []
): ResponseInterface
{
$headers['content-type'] = 'text/html; charset=utf-8';
return new Response($status, $headers, $html);
}
public static function redirect(
string $uri,
int $status = 302,
array $headers = []
): ResponseInterface
{
$headers['location'] = $uri;
return new Response($status, $headers);
}
}

View file

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Infrastructure\Http;
use League\Route\RouteGroup;
use League\Route\Router as LeagueRouter;
use League\Route\Strategy\ApplicationStrategy;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use TorrentPier\Infrastructure\DependencyInjection\Container;
class Router
{
private LeagueRouter $router;
private Container $container;
public function __construct(Container $container)
{
$this->container = $container;
$this->router = new LeagueRouter();
// Set up the application strategy with DI container
$strategy = new ApplicationStrategy();
$strategy->setContainer($this->container->getWrappedContainer());
$this->router->setStrategy($strategy);
}
public function get(string $path, $handler): \League\Route\Route
{
return $this->router->map('GET', $path, $handler);
}
public function post(string $path, $handler): \League\Route\Route
{
return $this->router->map('POST', $path, $handler);
}
public function put(string $path, $handler): \League\Route\Route
{
return $this->router->map('PUT', $path, $handler);
}
public function patch(string $path, $handler): \League\Route\Route
{
return $this->router->map('PATCH', $path, $handler);
}
public function delete(string $path, $handler): \League\Route\Route
{
return $this->router->map('DELETE', $path, $handler);
}
public function options(string $path, $handler): \League\Route\Route
{
return $this->router->map('OPTIONS', $path, $handler);
}
public function head(string $path, $handler): \League\Route\Route
{
return $this->router->map('HEAD', $path, $handler);
}
public function any(string $path, $handler): \League\Route\Route
{
return $this->router->map(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], $path, $handler);
}
public function group(string $prefix, callable $callback): RouteGroup
{
return $this->router->group($prefix, $callback);
}
public function middleware(MiddlewareInterface $middleware): self
{
$this->router->middleware($middleware);
return $this;
}
public function dispatch(ServerRequestInterface $request): ResponseInterface
{
return $this->router->dispatch($request);
}
public function getLeagueRouter(): LeagueRouter
{
return $this->router;
}
}

View file

@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Presentation\Http\Controllers\Web;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TorrentPier\Config;
use TorrentPier\Infrastructure\Http\ResponseFactory;
class HelloWorldController
{
private Config $config;
public function __construct(Config $config)
{
$this->config = $config;
}
public function index(ServerRequestInterface $request): ResponseInterface
{
$siteName = $this->config->get('sitename', 'TorrentPier');
$currentTime = date('Y-m-d H:i:s');
$html = "
<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
<title>Hello World - {$siteName}</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background: #f5f5f5;
}
.container {
background: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 { color: #333; }
.info {
background: #e8f4f8;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
border-left: 4px solid #2196F3;
}
</style>
</head>
<body>
<div class=\"container\">
<h1>Hello World from {$siteName}!</h1>
<p>This is a test route demonstrating the new hexagonal architecture routing system.</p>
<div class=\"info\">
<strong>Route Information:</strong><br>
URI: {$request->getUri()}<br>
Method: {$request->getMethod()}<br>
Time: {$currentTime}<br>
Controller: HelloWorldController
</div>
<p>The routing system is working correctly! This response was generated using:</p>
<ul>
<li>League/Route for routing</li>
<li>PSR-7 HTTP messages</li>
<li>Dependency Injection Container</li>
<li>Hexagonal Architecture structure</li>
</ul>
<p><a href=\"/\">← Back to main site</a></p>
</div>
</body>
</html>";
return ResponseFactory::html($html);
}
public function json(ServerRequestInterface $request): ResponseInterface
{
$siteName = $this->config->get('sitename', 'TorrentPier');
return ResponseFactory::json([
'message' => 'Hello World!',
'site' => $siteName,
'timestamp' => time(),
'datetime' => date('c'),
'route' => [
'uri' => (string)$request->getUri(),
'method' => $request->getMethod(),
'controller' => self::class,
],
'architecture' => [
'pattern' => 'Hexagonal Architecture',
'router' => 'League/Route',
'psr' => 'PSR-7 HTTP Messages',
'di' => 'PHP-DI Container'
]
]);
}
}

View file

@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Presentation\Http\Controllers\Web;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TorrentPier\Config;
use TorrentPier\Infrastructure\Http\ResponseFactory;
class LegacyController
{
private Config $config;
public function __construct(Config $config)
{
$this->config = $config;
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
// Get the legacy controller name from the URL path
$path = $request->getUri()->getPath();
$controller = null;
// Extract controller name from .php files or without extension
if (preg_match('/\/([^\/]+)\.php$/', $path, $matches)) {
// URL like /terms.php
$controller = $matches[1];
} elseif (preg_match('/\/([^\/]+)$/', $path, $matches)) {
// URL like /terms (without extension)
$controller = $matches[1];
} elseif ($path === '/') {
// Root path should serve index.php
$controller = 'index';
}
if (!$controller) {
return ResponseFactory::html('Legacy controller not specified', 404);
}
$rootPath = dirname(__DIR__, 5);
$controllerPath = $rootPath . '/controllers/' . $controller . '.php';
if (!file_exists($controllerPath)) {
return ResponseFactory::html(
"<h1>404 - Not Found</h1><p>Legacy controller '{$controller}' not found</p>",
404
);
}
// Capture the legacy controller output
$output = '';
$originalObLevel = ob_get_level();
try {
ob_start();
// Save current globals state (in case they're modified)
$originalServer = $_SERVER;
$originalGet = $_GET;
$originalPost = $_POST;
// Make legacy globals available in the included file's scope
global $bb_cfg, $config, $user, $template, $datastore, $lang, $userdata, $userinfo, $images;
// Include the legacy controller
// Note: We don't use require_once to allow multiple includes if needed
include $controllerPath;
// Get the captured output - make sure we only clean our own buffer
$output = ob_get_clean();
// Restore globals if needed
$_SERVER = $originalServer;
$_GET = $originalGet;
$_POST = $originalPost;
// Return the output as HTML response
return ResponseFactory::html($output);
} catch (\Throwable $e) {
// Clean up any extra output buffers that were started, but preserve original level
while (ob_get_level() > $originalObLevel) {
ob_end_clean();
}
// Return error response
$errorHtml = "
<h1>Legacy Controller Error</h1>
<p><strong>Controller:</strong> {$controller}</p>
<p><strong>Error:</strong> " . htmlspecialchars($e->getMessage()) . "</p>
<p><strong>File:</strong> " . htmlspecialchars($e->getFile()) . ":" . $e->getLine() . "</p>
";
if (function_exists('dev') && dev()->isDebugEnabled()) {
$errorHtml .= "<pre>" . htmlspecialchars($e->getTraceAsString()) . "</pre>";
}
return ResponseFactory::html($errorHtml, 500);
}
}
}

View file

@ -0,0 +1,152 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Presentation\Http;
use League\Route\Http\Exception\MethodNotAllowedException;
use League\Route\Http\Exception\NotFoundException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Throwable;
use TorrentPier\Infrastructure\DependencyInjection\Container;
use TorrentPier\Infrastructure\Http\RequestFactory;
use TorrentPier\Infrastructure\Http\ResponseFactory;
use TorrentPier\Infrastructure\Http\Router;
class Kernel
{
private Container $container;
private Router $router;
private array $middleware = [];
public function __construct(Container $container)
{
$this->container = $container;
$this->router = $container->get(Router::class);
}
public function addMiddleware(MiddlewareInterface $middleware): self
{
$this->middleware[] = $middleware;
return $this;
}
public function loadRoutes(string $routesFile): self
{
if (!file_exists($routesFile)) {
throw new \RuntimeException("Routes file not found: {$routesFile}");
}
$routeLoader = require $routesFile;
if (is_callable($routeLoader)) {
$routeLoader($this->router);
}
return $this;
}
public function handle(?ServerRequestInterface $request = null): ResponseInterface
{
try {
$request = $request ?: RequestFactory::fromGlobals();
// Apply middleware to router
foreach ($this->middleware as $middleware) {
$this->router->middleware($middleware);
}
return $this->router->dispatch($request);
} catch (NotFoundException $e) {
return ResponseFactory::html(
'<h1>404 - Not Found</h1><p>The requested page could not be found.</p>',
404
);
} catch (MethodNotAllowedException $e) {
return ResponseFactory::html(
'<h1>405 - Method Not Allowed</h1><p>The request method is not allowed for this endpoint.</p>',
405
);
} catch (Throwable $e) {
return $this->handleException($e, $request);
}
}
public function run(?ServerRequestInterface $request = null): void
{
$response = $this->handle($request);
$this->sendResponse($response);
}
private function handleException(Throwable $e, ?ServerRequestInterface $request = null): ResponseInterface
{
// TODO: Replace bb_log() with injected PSR-3 LoggerInterface
// This is a temporary coupling to the legacy logging system
if (function_exists('bb_log')) {
bb_log($e->getMessage() . "\n" . $e->getTraceAsString(), 'kernel_errors');
}
// TODO: Replace legacy dev() singleton with injected EnvironmentInterface
// This is a temporary coupling to the legacy system during the migration period
// Once all legacy controllers are migrated, inject a proper debug/environment service
if (function_exists('dev') && dev()->isDebugEnabled()) {
$html = "
<!DOCTYPE html>
<html>
<head>
<title>Application Error</title>
<style>
body { font-family: monospace; margin: 40px; background: #f5f5f5; }
.error { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.error h1 { color: #d32f2f; margin: 0 0 20px 0; }
.trace { background: #f8f8f8; padding: 15px; border-radius: 4px; overflow: auto; }
pre { margin: 0; white-space: pre-wrap; }
</style>
</head>
<body>
<div class=\"error\">
<h1>Application Error</h1>
<p><strong>Message:</strong> " . htmlspecialchars($e->getMessage()) . "</p>
<p><strong>File:</strong> " . htmlspecialchars($e->getFile()) . ":" . $e->getLine() . "</p>
<div class=\"trace\">
<strong>Stack Trace:</strong>
<pre>" . htmlspecialchars($e->getTraceAsString()) . "</pre>
</div>
</div>
</body>
</html>";
return ResponseFactory::html($html, 500);
}
// Production error response
return ResponseFactory::html('
<!DOCTYPE html>
<html>
<head><title>Server Error</title></head>
<body>
<h1>500 - Internal Server Error</h1>
<p>Something went wrong. Please try again later.</p>
</body>
</html>
', 500);
}
private function sendResponse(ResponseInterface $response): void
{
// Send status line
http_response_code($response->getStatusCode());
// Send headers
foreach ($response->getHeaders() as $name => $values) {
foreach ($values as $value) {
header("{$name}: {$value}", false);
}
}
// Send body
echo $response->getBody()->getContents();
}
}

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
use TorrentPier\Infrastructure\Http\Router;
use TorrentPier\Presentation\Http\Controllers\Web\HelloWorldController;
use TorrentPier\Presentation\Http\Controllers\Web\LegacyController;
return function (Router $router): void {
// Hello World test routes
$router->get('/hello', [HelloWorldController::class, 'index']);
$router->get('/hello/json', [HelloWorldController::class, 'json']);
// Legacy controller routes (hacky but organized approach)
$legacyRoutes = [
'index.php',
'terms.php',
// Add more legacy controllers here as needed:
// 'login.php',
// 'search.php',
// 'tracker.php',
];
foreach ($legacyRoutes as $route) {
// Route with .php extension
$router->get('/' . $route, [LegacyController::class, 'handle']);
// Route without .php extension (e.g., /terms for /terms.php)
$routeWithoutExtension = str_replace('.php', '', $route);
$router->get('/' . $routeWithoutExtension, [LegacyController::class, 'handle']);
}
// Root route should serve the legacy index.php controller
$router->get('/', [LegacyController::class, 'handle']);
// Future modern routes will be added here
};

View file

@ -15,12 +15,16 @@ describe('InfrastructureDefinitions', function () {
expect($definitions)->toBeArray();
});
it('returns empty array when no infrastructure services are implemented yet', function () {
it('returns infrastructure service definitions', function () {
$definitions = InfrastructureDefinitions::getDefinitions();
// Since we're in Phase 1 and infrastructure services aren't implemented yet,
// the definitions should be empty (all examples are commented out)
expect($definitions)->toBe([]);
// Should contain HTTP infrastructure services that have been implemented
expect($definitions)->toBeArray();
expect($definitions)->toHaveKey('TorrentPier\Config');
expect($definitions)->toHaveKey('TorrentPier\Infrastructure\Http\Router');
expect($definitions)->toHaveKey('TorrentPier\Infrastructure\Http\RequestFactory');
expect($definitions)->toHaveKey('TorrentPier\Infrastructure\Http\ResponseFactory');
expect($definitions)->toHaveKey('TorrentPier\Infrastructure\Http\Middleware\CorsMiddleware');
});
it('follows infrastructure layer principles', function () {
@ -43,7 +47,9 @@ describe('InfrastructureDefinitions', function () {
$definitions1 = InfrastructureDefinitions::getDefinitions();
$definitions2 = InfrastructureDefinitions::getDefinitions();
expect($definitions1)->toBe($definitions2);
// Should return same structure (though objects may be different instances)
expect(array_keys($definitions1))->toBe(array_keys($definitions2));
expect(count($definitions1))->toBe(count($definitions2));
});
it('can handle different configurations', function () {
@ -79,7 +85,8 @@ describe('InfrastructureDefinitions', function () {
// Connection::class => DI\get('database.connection.default'),
// For now, verify the method works without breaking
expect(count($definitions))->toBeGreaterThanOrEqual(0);
// Should at least contain the HTTP infrastructure services
expect(count($definitions))->toBeGreaterThanOrEqual(5);
});
it('is prepared for future cache services', function () {

View file

@ -9,12 +9,14 @@ describe('PresentationDefinitions', function () {
expect($definitions)->toBeArray();
});
it('returns empty array when no presentation services are implemented yet', function () {
it('returns presentation service definitions', function () {
$definitions = PresentationDefinitions::getDefinitions();
// Since we're in Phase 1 and presentation services aren't implemented yet,
// the definitions should be empty (all examples are commented out)
expect($definitions)->toBe([]);
// Should contain HTTP presentation services that have been implemented
expect($definitions)->toBeArray();
expect($definitions)->toHaveKey('TorrentPier\Presentation\Http\Kernel');
expect($definitions)->toHaveKey('TorrentPier\Presentation\Http\Controllers\Web\HelloWorldController');
expect($definitions)->toHaveKey('TorrentPier\Presentation\Http\Controllers\Web\LegacyController');
});
it('follows presentation layer principles', function () {
@ -37,7 +39,9 @@ describe('PresentationDefinitions', function () {
$definitions1 = PresentationDefinitions::getDefinitions();
$definitions2 = PresentationDefinitions::getDefinitions();
expect($definitions1)->toBe($definitions2);
// Should return same structure (though objects may be different instances)
expect(array_keys($definitions1))->toBe(array_keys($definitions2));
expect(count($definitions1))->toBe(count($definitions2));
});
it('is prepared for future HTTP controllers', function () {

View file

@ -0,0 +1,251 @@
<?php
use DI\Container as DIContainer;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\ServerRequest;
use TorrentPier\Infrastructure\DependencyInjection\Container;
use TorrentPier\Infrastructure\Http\Router;
describe('Router', function () {
beforeEach(function () {
// Create a real DI container and wrap it in our custom container
$diContainer = new DIContainer();
$this->container = new Container($diContainer);
// Create router instance
$this->router = new Router($this->container);
});
describe('route registration', function () {
it('registers GET routes', function () {
$this->router->get('/test', function () {
return new Response(200, [], 'GET test');
});
$request = new ServerRequest('GET', '/test');
$response = $this->router->dispatch($request);
expect($response->getStatusCode())->toBe(200);
expect((string)$response->getBody())->toBe('GET test');
});
it('registers POST routes', function () {
$this->router->post('/test', function () {
return new Response(200, [], 'POST test');
});
$request = new ServerRequest('POST', '/test');
$response = $this->router->dispatch($request);
expect($response->getStatusCode())->toBe(200);
expect((string)$response->getBody())->toBe('POST test');
});
it('registers PUT routes', function () {
$this->router->put('/test', function () {
return new Response(200, [], 'PUT test');
});
$request = new ServerRequest('PUT', '/test');
$response = $this->router->dispatch($request);
expect($response->getStatusCode())->toBe(200);
expect((string)$response->getBody())->toBe('PUT test');
});
it('registers PATCH routes', function () {
$this->router->patch('/test', function () {
return new Response(200, [], 'PATCH test');
});
$request = new ServerRequest('PATCH', '/test');
$response = $this->router->dispatch($request);
expect($response->getStatusCode())->toBe(200);
expect((string)$response->getBody())->toBe('PATCH test');
});
it('registers DELETE routes', function () {
$this->router->delete('/test', function () {
return new Response(200, [], 'DELETE test');
});
$request = new ServerRequest('DELETE', '/test');
$response = $this->router->dispatch($request);
expect($response->getStatusCode())->toBe(200);
expect((string)$response->getBody())->toBe('DELETE test');
});
it('registers HEAD routes', function () {
$this->router->head('/test', function () {
return new Response(200);
});
$request = new ServerRequest('HEAD', '/test');
$response = $this->router->dispatch($request);
expect($response->getStatusCode())->toBe(200);
});
it('registers OPTIONS routes', function () {
$this->router->options('/test', function () {
return new Response(200, ['Allow' => 'GET, POST, OPTIONS']);
});
$request = new ServerRequest('OPTIONS', '/test');
$response = $this->router->dispatch($request);
expect($response->getStatusCode())->toBe(200);
expect($response->getHeader('Allow'))->toBe(['GET, POST, OPTIONS']);
});
});
describe('route parameters', function () {
it('captures route parameters', function () {
$this->router->get('/user/{id}', function ($request, $args) {
return new Response(200, [], 'User ID: ' . $args['id']);
});
$request = new ServerRequest('GET', '/user/123');
$response = $this->router->dispatch($request);
expect($response->getStatusCode())->toBe(200);
expect((string)$response->getBody())->toBe('User ID: 123');
});
it('captures multiple route parameters', function () {
$this->router->get('/post/{year}/{month}/{slug}', function ($request, $args) {
return new Response(200, [], sprintf(
'Post: %s/%s/%s',
$args['year'],
$args['month'],
$args['slug']
));
});
$request = new ServerRequest('GET', '/post/2024/06/hello-world');
$response = $this->router->dispatch($request);
expect($response->getStatusCode())->toBe(200);
expect((string)$response->getBody())->toBe('Post: 2024/06/hello-world');
});
});
describe('controller resolution', function () {
it('resolves controller from DI container', function () {
// Create a test controller
$testController = new class {
public function index()
{
return new Response(200, [], 'Controller response');
}
};
// Register controller in container using the wrapped DI container
$this->container->getWrappedContainer()->set('TestController', $testController);
// Register route with controller
$this->router->get('/test', ['TestController', 'index']);
$request = new ServerRequest('GET', '/test');
$response = $this->router->dispatch($request);
expect($response->getStatusCode())->toBe(200);
expect((string)$response->getBody())->toBe('Controller response');
});
it('resolves controller by class name', function () {
// Define a test controller class
$controllerClass = 'TestControllerClass' . uniqid();
eval("
class $controllerClass {
public function handle() {
return new \GuzzleHttp\Psr7\Response(200, [], 'Class-based controller');
}
}
");
$this->router->get('/test', [$controllerClass, 'handle']);
$request = new ServerRequest('GET', '/test');
$response = $this->router->dispatch($request);
expect($response->getStatusCode())->toBe(200);
expect((string)$response->getBody())->toBe('Class-based controller');
});
});
describe('middleware support', function () {
it('applies middleware to routes', function () {
// Create a simple middleware that implements PSR-15
$middleware = new class implements \Psr\Http\Server\MiddlewareInterface {
public function process(
\Psr\Http\Message\ServerRequestInterface $request,
\Psr\Http\Server\RequestHandlerInterface $handler
): \Psr\Http\Message\ResponseInterface
{
$response = $handler->handle($request);
return $response->withHeader('X-Middleware', 'Applied');
}
};
$this->router->middleware($middleware)
->get('/test', function () {
return new Response(200, [], 'With middleware');
});
$request = new ServerRequest('GET', '/test');
$response = $this->router->dispatch($request);
expect($response->getStatusCode())->toBe(200);
expect($response->getHeader('X-Middleware'))->toBe(['Applied']);
expect((string)$response->getBody())->toBe('With middleware');
});
});
describe('error handling', function () {
it('throws NotFoundException for undefined routes', function () {
$request = new ServerRequest('GET', '/undefined');
expect(fn() => $this->router->dispatch($request))
->toThrow(League\Route\Http\Exception\NotFoundException::class);
});
it('throws MethodNotAllowedException for wrong HTTP method', function () {
$this->router->post('/test', function () {
return new Response(200);
});
$request = new ServerRequest('GET', '/test');
expect(fn() => $this->router->dispatch($request))
->toThrow(League\Route\Http\Exception\MethodNotAllowedException::class);
});
});
describe('route groups', function () {
it('supports route groups with prefix', function () {
$this->router->group('/api', function ($router) {
$router->get('/users', function () {
return new Response(200, [], 'Users list');
});
$router->get('/posts', function () {
return new Response(200, [], 'Posts list');
});
});
// Test /api/users
$request = new ServerRequest('GET', '/api/users');
$response = $this->router->dispatch($request);
expect($response->getStatusCode())->toBe(200);
expect((string)$response->getBody())->toBe('Users list');
// Test /api/posts
$request = new ServerRequest('GET', '/api/posts');
$response = $this->router->dispatch($request);
expect($response->getStatusCode())->toBe(200);
expect((string)$response->getBody())->toBe('Posts list');
});
});
});

View file

@ -0,0 +1,58 @@
<?php
use GuzzleHttp\Psr7\ServerRequest;
use TorrentPier\Config;
use TorrentPier\Presentation\Http\Controllers\Web\LegacyController;
describe('LegacyController', function () {
beforeEach(function () {
// Create a mock config for testing that doesn't require actual files
$mockConfig = Mockery::mock(Config::class);
$mockConfig->shouldReceive('all')->andReturn([]);
$mockConfig->shouldReceive('get')->andReturn('/fake/path/');
// Create controller with mock config
$this->controller = new LegacyController($mockConfig);
});
afterEach(function () {
Mockery::close();
});
describe('basic functionality', function () {
it('can be instantiated', function () {
expect($this->controller)->toBeInstanceOf(LegacyController::class);
});
it('implements proper method signature', function () {
$reflection = new ReflectionClass($this->controller);
$method = $reflection->getMethod('handle');
expect($method->getParameters())->toHaveCount(1);
expect($method->getParameters()[0]->getType()->getName())->toBe('Psr\Http\Message\ServerRequestInterface');
});
it('returns PSR-7 response interface', function () {
$request = new ServerRequest('GET', 'http://example.com/nonexistent');
$response = $this->controller->handle($request);
expect($response)->toBeInstanceOf(\Psr\Http\Message\ResponseInterface::class);
});
it('returns 404 for non-existent files', function () {
$request = new ServerRequest('GET', 'http://example.com/nonexistent');
$response = $this->controller->handle($request);
expect($response->getStatusCode())->toBe(404);
});
});
describe('security', function () {
it('prevents directory traversal', function () {
$request = new ServerRequest('GET', 'http://example.com/../../../etc/passwd');
$response = $this->controller->handle($request);
expect($response->getStatusCode())->toBe(404);
});
});
});

View file

@ -0,0 +1,201 @@
<?php
use DI\Container as DIContainer;
use GuzzleHttp\Psr7\ServerRequest;
use TorrentPier\Infrastructure\DependencyInjection\Container;
use TorrentPier\Infrastructure\Http\Router;
use TorrentPier\Presentation\Http\Kernel;
describe('HTTP Kernel', function () {
beforeEach(function () {
// Create a real DI container wrapped in our custom container
$diContainer = new DIContainer();
$this->container = new Container($diContainer);
// Create a real router instance and register it in the container
$this->router = new Router($this->container);
$this->container->getWrappedContainer()->set(Router::class, $this->router);
// Create kernel instance
$this->kernel = new Kernel($this->container);
// Store original superglobals
$this->originalServer = $_SERVER;
$this->originalGet = $_GET;
$this->originalPost = $_POST;
});
afterEach(function () {
// Restore original superglobals
$_SERVER = $this->originalServer;
$_GET = $this->originalGet;
$_POST = $this->originalPost;
});
describe('route loading', function () {
it('loads routes from a file', function () {
// Create a temporary routes file
$routesFile = tempnam(sys_get_temp_dir(), 'routes');
file_put_contents($routesFile, '<?php
return function ($router) {
$router->get("/test", function () {
return new \GuzzleHttp\Psr7\Response(200, [], "Test route");
});
};
');
$this->kernel->loadRoutes($routesFile);
// Clean up
unlink($routesFile);
// Verify route was loaded by trying to handle it
$request = new ServerRequest('GET', 'http://example.com/test');
$response = $this->kernel->handle($request);
expect($response->getStatusCode())->toBe(200);
expect((string)$response->getBody())->toBe('Test route');
});
it('throws exception for non-existent routes file', function () {
$nonExistentFile = '/path/to/nonexistent/routes.php';
expect(fn() => $this->kernel->loadRoutes($nonExistentFile))
->toThrow(RuntimeException::class);
});
});
describe('request handling', function () {
beforeEach(function () {
// Set up a test route
$routesFile = tempnam(sys_get_temp_dir(), 'routes');
file_put_contents($routesFile, '<?php
return function ($router) {
$router->get("/hello", function () {
return new \GuzzleHttp\Psr7\Response(200, [], "Hello World");
});
$router->post("/submit", function ($request) {
$body = $request->getParsedBody();
return new \GuzzleHttp\Psr7\Response(200, [], json_encode($body));
});
};
');
$this->kernel->loadRoutes($routesFile);
$this->routesFile = $routesFile;
});
afterEach(function () {
if (isset($this->routesFile)) {
unlink($this->routesFile);
}
});
it('handles GET requests', function () {
$request = new ServerRequest('GET', 'http://example.com/hello');
$response = $this->kernel->handle($request);
expect($response->getStatusCode())->toBe(200);
expect((string)$response->getBody())->toBe('Hello World');
});
it('handles POST requests', function () {
$request = new ServerRequest('POST', 'http://example.com/submit');
$request = $request->withParsedBody(['name' => 'John']);
$response = $this->kernel->handle($request);
expect($response->getStatusCode())->toBe(200);
expect((string)$response->getBody())->toBe('{"name":"John"}');
});
it('returns 404 for undefined routes', function () {
$request = new ServerRequest('GET', 'http://example.com/undefined');
$response = $this->kernel->handle($request);
expect($response->getStatusCode())->toBe(404);
});
it('returns 405 for wrong HTTP method', function () {
$request = new ServerRequest('POST', 'http://example.com/hello');
$response = $this->kernel->handle($request);
expect($response->getStatusCode())->toBe(405);
});
});
describe('error handling', function () {
it('handles exceptions gracefully', function () {
// Set up a route that throws an exception
$routesFile = tempnam(sys_get_temp_dir(), 'routes');
file_put_contents($routesFile, '<?php
return function ($router) {
$router->get("/error", function () {
throw new \RuntimeException("Test exception");
});
};
');
$this->kernel->loadRoutes($routesFile);
$request = new ServerRequest('GET', 'http://example.com/error');
$response = $this->kernel->handle($request);
expect($response->getStatusCode())->toBe(500);
// Clean up
unlink($routesFile);
});
it('logs exceptions when bb_log function exists', function () {
// This is hard to test without actually defining bb_log
// We'll just verify the error response structure
$routesFile = tempnam(sys_get_temp_dir(), 'routes');
file_put_contents($routesFile, '<?php
return function ($router) {
$router->get("/error", function () {
throw new \Exception("Test error");
});
};
');
$this->kernel->loadRoutes($routesFile);
$request = new ServerRequest('GET', 'http://example.com/error');
$response = $this->kernel->handle($request);
expect($response->getStatusCode())->toBe(500);
expect($response->getBody()->getContents())->toContain('Internal Server Error');
unlink($routesFile);
});
});
describe('run method', function () {
it('creates request from globals and sends response', function () {
// Set up globals
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/hello';
$_SERVER['HTTP_HOST'] = 'example.com';
$_GET = ['foo' => 'bar'];
// Set up a route
$routesFile = tempnam(sys_get_temp_dir(), 'routes');
file_put_contents($routesFile, '<?php
return function ($router) {
$router->get("/hello", function ($request) {
$query = $request->getQueryParams();
return new \GuzzleHttp\Psr7\Response(200, [], $query["foo"] ?? "no foo");
});
};
');
$this->kernel->loadRoutes($routesFile);
// Capture output
ob_start();
$this->kernel->run();
$output = ob_get_clean();
expect($output)->toContain('bar');
unlink($routesFile);
});
});
});