mirror of
https://github.com/torrentpier/torrentpier
synced 2025-08-21 22:03:49 -07:00
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:
parent
5b5bf49f4e
commit
273121a49f
25 changed files with 2051 additions and 449 deletions
|
@ -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');
|
||||
|
|
|
@ -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
255
composer.lock
generated
|
@ -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
102
config/routes.php
Normal 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
437
controllers/index.php
Normal 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']}&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'] . ': ' . '<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 ? ' <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 ? ' <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');
|
|
@ -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
447
index.php
|
@ -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']}&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'] . ': ' . '<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 ? ' <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 ? ' <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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
14
phinx.php
14
phinx.php
|
@ -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 [
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(),
|
||||
|
|
30
src/Infrastructure/Http/Middleware/BaseMiddleware.php
Normal file
30
src/Infrastructure/Http/Middleware/BaseMiddleware.php
Normal 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;
|
||||
}
|
||||
}
|
58
src/Infrastructure/Http/Middleware/CorsMiddleware.php
Normal file
58
src/Infrastructure/Http/Middleware/CorsMiddleware.php
Normal 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;
|
||||
}
|
||||
}
|
28
src/Infrastructure/Http/RequestFactory.php
Normal file
28
src/Infrastructure/Http/RequestFactory.php
Normal 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;
|
||||
}
|
||||
}
|
54
src/Infrastructure/Http/ResponseFactory.php
Normal file
54
src/Infrastructure/Http/ResponseFactory.php
Normal 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);
|
||||
}
|
||||
}
|
91
src/Infrastructure/Http/Router.php
Normal file
91
src/Infrastructure/Http/Router.php
Normal 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;
|
||||
}
|
||||
}
|
108
src/Presentation/Http/Controllers/Web/HelloWorldController.php
Normal file
108
src/Presentation/Http/Controllers/Web/HelloWorldController.php
Normal 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'
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
104
src/Presentation/Http/Controllers/Web/LegacyController.php
Normal file
104
src/Presentation/Http/Controllers/Web/LegacyController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
152
src/Presentation/Http/Kernel.php
Normal file
152
src/Presentation/Http/Kernel.php
Normal 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();
|
||||
}
|
||||
}
|
37
src/Presentation/Http/Routes/web.php
Normal file
37
src/Presentation/Http/Routes/web.php
Normal 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
|
||||
};
|
|
@ -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 () {
|
||||
|
|
|
@ -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 () {
|
||||
|
|
251
tests/Unit/Infrastructure/Http/RouterTest.php
Normal file
251
tests/Unit/Infrastructure/Http/RouterTest.php
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
201
tests/Unit/Presentation/Http/KernelTest.php
Normal file
201
tests/Unit/Presentation/Http/KernelTest.php
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue