chore!: mark codebase as fully broken

BREAKING CHANGE: The current codebase is in a non-functional state and requires significant refactoring before it can be used.
Notes:
- Core functionality is currently broken
- Dependencies may be missing or incompatible
- Configuration files may need updates
- Database migrations may be incomplete
- Legacy code integration issues present
- Requires comprehensive testing and fixes before deployment

This commit serves as a checkpoint to document the current broken state
before beginning major refactoring efforts.
This commit is contained in:
Yury Pikhtarev 2025-06-23 15:46:49 +04:00
commit e5ca170b4c
No known key found for this signature in database
70 changed files with 3999 additions and 21 deletions

137
README.md
View file

@ -143,6 +143,143 @@ TorrentPier includes a comprehensive testing suite built with **Pest PHP**. Run
For detailed testing documentation, see [tests/README.md](tests/README.md).
## 🖥️ CLI Usage
TorrentPier 3.0 includes a Laravel-style CLI tool called **Dexter** for managing your application:
```shell
# Basic usage
php dexter # Shows available commands
php dexter info # System information
php dexter cache:clear # Clear caches
php dexter migrate # Run database migrations
php dexter help <command> # Command help
```
## 🦌 Laravel Herd Configuration
If you're using [Laravel Herd](https://herd.laravel.com) for local development, you'll need special configuration to properly serve the legacy `/admin/` and `/bt/` directories. By default, Herd routes all requests through the modern front controller, but these directories need to be served directly.
### The Problem
TorrentPier has legacy directories (`/admin/` and `/bt/`) that contain their own `index.php` files and should be processed directly by the web server, not through the Laravel-style routing system. Laravel Herd's default nginx configuration sends all requests to `public/index.php`, which causes these legacy endpoints to fail.
### Solution: Site-Specific Nginx Configuration
#### Step 1: Generate Custom Nginx Config
Run one of these commands to create a site-specific nginx configuration:
```shell
# Option A: Isolate PHP version
herd isolate php@8.4
# Option B: Secure the site with SSL
herd secure
```
This generates a custom nginx configuration file at:
`~/Library/Application\ Support/Herd/config/valet/Nginx/[your-site-name]`
#### Step 2: Edit the Generated Config
Open the generated nginx configuration file and add these location blocks **before** the main Laravel location block:
```nginx
# Serve /admin/ directory directly
location /admin/ {
alias /path/to/your/torrentpier/admin/;
index index.php;
location ~ \.php$ {
fastcgi_pass unix:/opt/homebrew/var/run/php/php8.4-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $request_filename;
include fastcgi_params;
}
}
# Serve /bt/ directory directly
location /bt/ {
alias /path/to/your/torrentpier/bt/;
index index.php;
location ~ \.php$ {
fastcgi_pass unix:/opt/homebrew/var/run/php/php8.4-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $request_filename;
include fastcgi_params;
}
}
```
> **Note**: Replace `/path/to/your/torrentpier/` with the actual path to your TorrentPier installation.
#### Step 3: Restart Herd
```shell
herd restart
```
### Alternative Solutions
#### Option 1: Root .htaccess (May work with some Herd configurations)
Create a `.htaccess` file in your project root:
```apache
RewriteEngine On
# Exclude admin and bt directories from Laravel routing
RewriteRule ^admin/ - [L]
RewriteRule ^bt/ - [L]
# Handle everything else through Laravel
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ public/index.php [L]
```
#### Option 2: Move to Public Directory
Move the directories to the public folder:
```shell
mv admin/ public/admin/
mv bt/ public/bt/
```
> **Warning**: This requires updating any hardcoded paths in the legacy code.
### Testing Your Configuration
After applying the configuration, test these URLs:
```shell
# Admin panel should load
curl -I http://your-site.test/admin/
# BitTorrent tracker should respond
curl -I http://your-site.test/bt/
# Announce endpoint should work
curl -I http://your-site.test/bt/announce.php
# Modern routes should still work
curl -I http://your-site.test/hello
```
All should return HTTP 200 status codes.
### Troubleshooting
- **502 Bad Gateway**: Check PHP-FPM socket path in nginx config
- **404 Not Found**: Verify directory paths in nginx location blocks
- **403 Forbidden**: Check file permissions on admin/bt directories
- **Still routing through Laravel**: Ensure location blocks are placed before the main Laravel location block
For more details about Herd nginx configuration, see the [official Herd documentation](https://herd.laravel.com/docs/macos/sites/nginx-configuration).
## 📌 Our recommendations
* *It's recommended to run `cron.php`.* - For significant tracker speed increase it may be required to replace the built-in cron.php with an operating system daemon.

View file

@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\RegisterUserRequest;
use App\Models\User;
use App\Services\User\UserService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
/**
* User API Controller - Pure Laravel-style implementation
*/
class UserController extends Controller
{
public function __construct(
private UserService $userService
)
{
}
/**
* Register a new user
*/
public function register(RegisterUserRequest $request): JsonResponse
{
try {
// Laravel automatically validates the request via RegisterUserRequest
$validated = $request->validated();
// Create the user using the service
$user = $this->userService->register($validated);
return $this->json([
'success' => true,
'message' => 'User registered successfully',
'data' => [
'id' => $user->getKey(),
'username' => $user->username,
'email' => $user->user_email,
'registered_at' => now()->toISOString()
]
], 201);
} catch (ValidationException $e) {
return $this->json([
'success' => false,
'message' => 'Validation failed',
'errors' => $e->errors()
], 422);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'Registration failed',
'error' => $e->getMessage()
], 500);
}
}
/**
* Get user profile
*/
public function show(Request $request, int $userId): JsonResponse
{
try {
$user = User::find($userId);
if (!$user) {
return $this->json([
'success' => false,
'message' => 'User not found'
], 404);
}
$stats = $user->getStats();
return $this->json([
'success' => true,
'data' => [
'id' => $user->getKey(),
'username' => $user->username,
'level' => $user->user_level,
'active' => $user->isActive(),
'registered' => now()->createFromTimestamp($user->user_regdate)->toISOString(),
'last_visit' => now()->createFromTimestamp($user->user_lastvisit)->diffForHumans(),
'stats' => [
'uploaded' => $stats['u_up_total'] ?? 0,
'downloaded' => $stats['u_down_total'] ?? 0,
'ratio' => $user->getRatio(),
],
'permissions' => [
'is_admin' => $user->isAdmin(),
'is_moderator' => $user->isModerator(),
]
]
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'Failed to retrieve user',
'error' => $e->getMessage()
], 500);
}
}
/**
* List users with filtering and search
*/
public function index(Request $request): JsonResponse
{
try {
// Use Laravel-style parameter handling
$page = (int)$request->get('page', 1);
$perPage = min((int)$request->get('per_page', 20), 100);
$search = Str::limit(trim($request->get('search', '')), 50);
$level = $request->get('level');
// Get users using collection helpers
$users = collect(User::all())
->when(!empty($search), function ($collection) use ($search) {
return $collection->filter(function ($user) use ($search) {
return Str::contains(Str::lower($user->username), Str::lower($search)) ||
Str::contains(Str::lower($user->user_email), Str::lower($search));
});
})
->when($level !== null, function ($collection) use ($level) {
return $collection->where('user_level', $level);
})
->where('user_active', 1)
->sortBy('username')
->forPage($page, $perPage)
->map(function ($user) {
return [
'id' => $user->getKey(),
'username' => $user->username,
'level' => $user->user_level,
'registered' => now()->createFromTimestamp($user->user_regdate)->format('Y-m-d'),
'is_admin' => $user->isAdmin(),
'is_moderator' => $user->isModerator(),
];
})
->values();
return $this->json([
'success' => true,
'data' => $users->toArray(),
'meta' => [
'page' => $page,
'per_page' => $perPage,
'search' => (string)$search,
'level_filter' => $level
]
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'Failed to retrieve users',
'error' => $e->getMessage()
], 500);
}
}
}

View file

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Validation\Factory as ValidationFactory;
use Illuminate\Validation\ValidationException;
/**
* Base Controller class using Illuminate HTTP components
*/
abstract class Controller
{
protected ValidationFactory $validator;
public function __construct(ValidationFactory $validator)
{
$this->validator = $validator;
}
/**
* Render a view with data
*/
protected function view(string $view, array $data = []): Response
{
$viewPath = $this->resolveViewPath($view);
if (!file_exists($viewPath)) {
return new Response("View not found: {$view}", 404);
}
// Extract data for use in view
extract($data);
// Capture view output
ob_start();
require $viewPath;
$content = ob_get_clean();
return new Response($content);
}
/**
* Return a JSON response
*/
protected function json(array $data, int $status = 200, array $headers = []): JsonResponse
{
return new JsonResponse($data, $status, $headers);
}
/**
* Redirect to a URL
*/
protected function redirect(string $url, int $status = 302): RedirectResponse
{
return new RedirectResponse($url, $status);
}
/**
* Return a plain response
*/
protected function response(string $content = '', int $status = 200, array $headers = []): Response
{
return new Response($content, $status, $headers);
}
/**
* Validate request data using Illuminate validation
*/
protected function validate(Request $request, array $rules, array $messages = [], array $attributes = []): array
{
$validator = $this->validator->make($request->all(), $rules, $messages, $attributes);
if ($validator->fails()) {
throw new ValidationException($validator);
}
return $validator->validated();
}
/**
* Resolve view file path
*/
private function resolveViewPath(string $view): string
{
$view = str_replace('.', '/', $view);
return resource_path('views/' . $view . '.php');
}
}

View file

@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Validation\Factory as ValidationFactory;
use TorrentPier\Config;
class HelloWorldController extends Controller
{
public function __construct(
ValidationFactory $validator,
private Config $config
) {
parent::__construct($validator);
}
/**
* Show the hello world page
*/
public function index(Request $request): Response
{
$siteName = $this->config->get('sitename', 'TorrentPier');
$currentTime = now()->format('Y-m-d H:i:s');
$data = [
'siteName' => $siteName,
'currentTime' => $currentTime,
'request' => (object) [
'uri' => $request->fullUrl(),
'method' => $request->method()
],
'architecture' => 'MVC with Illuminate HTTP'
];
return $this->view('hello', $data);
}
/**
* Return JSON response for hello world
*/
public function jsonResponse(Request $request): JsonResponse
{
$siteName = $this->config->get('sitename', 'TorrentPier');
return $this->json([
'message' => 'Hello World from TorrentPier!',
'site' => $siteName,
'timestamp' => now()->timestamp,
'datetime' => now()->toISOString(),
'route' => [
'uri' => $request->fullUrl(),
'method' => $request->method(),
'controller' => self::class,
],
'architecture' => [
'pattern' => 'Laravel-style MVC',
'router' => 'Custom Laravel-style Router',
'http' => 'Illuminate HTTP',
'support' => 'Illuminate Support',
'di' => 'Illuminate Container'
],
'features' => [
'illuminate_http' => 'Response and Request handling',
'illuminate_support' => 'Collections, Str, Arr helpers',
'carbon' => 'Date manipulation with now() and today()',
'validation' => 'Laravel-style request validation',
'collections' => 'collect() helper for data manipulation'
]
]);
}
/**
* Demonstrate modern Laravel-style features
*/
public function features(Request $request): JsonResponse
{
// Demonstrate collections
$users = collect([
['name' => 'Alice', 'age' => 25],
['name' => 'Bob', 'age' => 30],
['name' => 'Charlie', 'age' => 35]
]);
$adults = $users->where('age', '>=', 18)->pluck('name');
// Demonstrate string helpers
$title = str('hello world')->title()->append('!');
// Demonstrate array helpers
$config = [
'app' => [
'name' => 'TorrentPier',
'version' => '3.0'
]
];
$appName = data_get($config, 'app.name', 'Unknown');
return $this->json([
'collections_demo' => [
'original_users' => $users->toArray(),
'adult_names' => $adults->toArray()
],
'string_demo' => [
'original' => 'hello world',
'transformed' => (string) $title
],
'array_helpers_demo' => [
'config' => $config,
'app_name' => $appName
],
'date_helpers' => [
'now' => now()->toISOString(),
'today' => today()->toDateString(),
'timestamp' => now()->timestamp
]
]);
}
/**
* Extended features demonstration
*/
public function extended(Request $request): JsonResponse
{
$siteName = $this->config->get('sitename', 'TorrentPier');
return $this->json([
'message' => 'Extended Laravel-style features!',
'site' => $siteName,
'timestamp' => now()->timestamp,
'datetime' => now()->toISOString(),
'request_info' => [
'url' => $request->fullUrl(),
'method' => $request->method(),
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
],
'laravel_features' => [
'collections' => 'Native Laravel collections',
'request' => 'Pure Illuminate Request',
'response' => 'Pure Illuminate JsonResponse',
'validation' => 'Built-in validation',
'helpers' => 'Laravel helper functions'
]
]);
}
}

View file

@ -0,0 +1,219 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Web;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use TorrentPier\Config;
class LegacyController
{
private Config $config;
public function __construct(Config $config)
{
$this->config = $config;
}
public function index(Request $request): Response
{
return $this->handleController($request, 'index');
}
public function ajax(Request $request): Response
{
return $this->handleController($request, 'ajax');
}
public function dl(Request $request): Response
{
return $this->handleController($request, 'dl');
}
public function dl_list(Request $request): Response
{
return $this->handleController($request, 'dl_list');
}
public function feed(Request $request): Response
{
return $this->handleController($request, 'feed');
}
public function filelist(Request $request): Response
{
return $this->handleController($request, 'filelist');
}
public function group(Request $request): Response
{
return $this->handleController($request, 'group');
}
public function group_edit(Request $request): Response
{
return $this->handleController($request, 'group_edit');
}
public function info(Request $request): Response
{
return $this->handleController($request, 'info');
}
public function login(Request $request): Response
{
return $this->handleController($request, 'login');
}
public function memberlist(Request $request): Response
{
return $this->handleController($request, 'memberlist');
}
public function modcp(Request $request): Response
{
return $this->handleController($request, 'modcp');
}
public function playback_m3u(Request $request): Response
{
return $this->handleController($request, 'playback_m3u');
}
public function poll(Request $request): Response
{
return $this->handleController($request, 'poll');
}
public function posting(Request $request): Response
{
return $this->handleController($request, 'posting');
}
public function privmsg(Request $request): Response
{
return $this->handleController($request, 'privmsg');
}
public function profile(Request $request): Response
{
return $this->handleController($request, 'profile');
}
public function search(Request $request): Response
{
return $this->handleController($request, 'search');
}
public function terms(Request $request): Response
{
return $this->handleController($request, 'terms');
}
public function tracker(Request $request): Response
{
return $this->handleController($request, 'tracker');
}
public function viewforum(Request $request): Response
{
return $this->handleController($request, 'viewforum');
}
public function viewtopic(Request $request): Response
{
return $this->handleController($request, 'viewtopic');
}
public function handleController(Request $request, string $controller): Response
{
$rootPath = dirname(__DIR__, 4);
$controllerPath = $rootPath . '/controllers/' . $controller . '.php';
if (!file_exists($controllerPath)) {
return new Response(
"<h1>404 - Not Found</h1><p>Legacy controller '{$controller}' not found</p>",
404,
['Content-Type' => 'text/html']
);
}
// Capture the legacy controller output
$output = '';
$originalObLevel = ob_get_level();
try {
// Ensure legacy common.php is loaded for legacy controllers
if (!defined('BB_PATH')) {
require_once $rootPath . '/common.php';
}
ob_start();
// No need to save/restore superglobals - legacy controllers may modify them intentionally
// Signal to legacy code that we're running through modern routing
if (!defined('MODERN_ROUTING')) {
define('MODERN_ROUTING', true);
}
// Import essential legacy globals into local scope
global $bb_cfg, $config, $user, $template, $datastore, $lang, $userdata, $userinfo, $images,
$tracking_topics, $tracking_forums, $theme, $bf, $attach_config, $gen_simple_header,
$client_ip, $user_ip, $log_action, $html, $wordCensor, $search_id,
$session_id, $items_found, $per_page, $topic_id, $req_topics, $forum_id, $mode,
$is_auth, $t_data, $postrow, $group_id, $group_info, $post_id, $folder, $post_info,
$tor, $post_data, $privmsg, $forums, $redirect, $attachment, $forum_data, $search_all,
$redirect_url, $topic_csv, $poster_id, $emailer, $s_hidden_fields, $opt, $msg, $stats,
$page_cfg, $ads, $cat_forums, $last_session_data, $announce_interval, $auth_pages,
$lastvisit, $current_time, $excluded_forums_csv, $sphinx, $dl_link_css, $dl_status_css,
$upload_dir, $topic_data, $attachments;
// GPC variables created dynamically via $GLOBALS in tracker.php and search.php
global $all_words_key, $all_words_val, $active_key, $active_val, $cat_key, $cat_val,
$dl_cancel_key, $dl_cancel_val, $dl_compl_key, $dl_compl_val, $dl_down_key, $dl_down_val,
$dl_will_key, $dl_will_val, $forum_key, $forum_val, $my_key, $my_val, $new_key, $new_val,
$title_match_key, $title_match_val, $order_key, $order_val, $poster_id_key, $poster_id_val,
$poster_name_key, $poster_name_val, $user_releases_key, $user_releases_val, $sort_key, $sort_val,
$seed_exist_key, $seed_exist_val, $show_author_key, $show_author_val, $show_cat_key, $show_cat_val,
$show_forum_key, $show_forum_val, $show_speed_key, $show_speed_val, $s_rg_key, $s_rg_val,
$s_not_seen_key, $s_not_seen_val, $time_key, $time_val, $tor_type_key, $tor_type_val,
$hash_key, $hash_val, $chars_key, $chars_val, $display_as_key, $display_as_val,
$dl_user_id_key, $dl_user_id_val, $my_topics_key, $my_topics_val, $new_topics_key, $new_topics_val,
$text_match_key, $text_match_val, $title_only_key, $title_only_val, $topic_key, $topic_val;
// 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();
// Return the output as HTML response
return new Response($output, 200, ['Content-Type' => 'text/html']);
} 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 new Response($errorHtml, 500, ['Content-Type' => 'text/html']);
}
}
}

View file

@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
/**
* Base Form Request class for validation
*/
abstract class FormRequest
{
protected Request $request;
protected array $validated = [];
public function __construct(Request $request)
{
$this->request = $request;
$this->runValidation();
}
/**
* Get the validation rules that apply to the request
*/
abstract public function rules(): array;
/**
* Get custom validation messages
*/
public function messages(): array
{
return [];
}
/**
* Get custom attributes for validator errors
*/
public function attributes(): array
{
return [];
}
/**
* Determine if the user is authorized to make this request
*/
public function authorize(): bool
{
return true;
}
/**
* Get validated data
*/
public function validated(): array
{
return $this->validated;
}
/**
* Get specific validated field
*/
public function get(string $key, mixed $default = null): mixed
{
return data_get($this->validated, $key, $default);
}
/**
* Get all request data
*/
public function all(): array
{
return $this->request->all();
}
/**
* Get only specific fields from request
*/
public function only(array $keys): array
{
return $this->request->only($keys);
}
/**
* Get request data except specific fields
*/
public function except(array $keys): array
{
return $this->request->except($keys);
}
/**
* Run the validation
*/
protected function runValidation(): void
{
if (!$this->authorize()) {
throw new \Illuminate\Auth\Access\AuthorizationException('This action is unauthorized.');
}
$validator = Validator::make(
$this->request->all(),
$this->rules(),
$this->messages(),
$this->attributes()
);
if ($validator->fails()) {
throw new ValidationException($validator);
}
$this->validated = $validator->validated();
}
}

View file

@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests;
/**
* User Registration Request Validation
*/
class RegisterUserRequest extends FormRequest
{
/**
* Get the validation rules
*/
public function rules(): array
{
return [
'username' => [
'required',
'string',
'min:3',
'max:25',
'regex:/^[a-zA-Z0-9_-]+$/',
'unique:bb_users,username'
],
'email' => [
'required',
'email',
'max:255',
'unique:bb_users,user_email'
],
'password' => [
'required',
'string',
'min:6',
'max:72'
],
'password_confirmation' => [
'required',
'same:password'
],
'terms' => [
'accepted'
]
];
}
/**
* Get custom validation messages
*/
public function messages(): array
{
return [
'username.required' => 'Username is required.',
'username.min' => 'Username must be at least 3 characters.',
'username.max' => 'Username cannot exceed 25 characters.',
'username.regex' => 'Username can only contain letters, numbers, hyphens, and underscores.',
'username.unique' => 'This username is already taken.',
'email.required' => 'Email address is required.',
'email.email' => 'Please enter a valid email address.',
'email.unique' => 'This email address is already registered.',
'password.required' => 'Password is required.',
'password.min' => 'Password must be at least 6 characters.',
'password.max' => 'Password cannot exceed 72 characters.',
'password_confirmation.required' => 'Password confirmation is required.',
'password_confirmation.same' => 'Password confirmation does not match.',
'terms.accepted' => 'You must accept the terms and conditions.'
];
}
/**
* Get custom attributes for validator errors
*/
public function attributes(): array
{
return [
'username' => 'username',
'email' => 'email address',
'password' => 'password',
'password_confirmation' => 'password confirmation',
'terms' => 'terms and conditions'
];
}
/**
* Get the username
*/
public function getUsername(): string
{
return str($this->get('username'))->trim()->lower();
}
/**
* Get the email
*/
public function getEmail(): string
{
return str($this->get('email'))->trim()->lower();
}
/**
* Get the password
*/
public function getPassword(): string
{
return $this->get('password');
}
/**
* Check if terms are accepted
*/
public function hasAcceptedTerms(): bool
{
return (bool) $this->get('terms');
}
}

223
app/Http/Routing/Router.php Normal file
View file

@ -0,0 +1,223 @@
<?php
declare(strict_types=1);
namespace App\Http\Routing;
use Illuminate\Container\Container;
use Illuminate\Events\Dispatcher;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Illuminate\Routing\Router as LaravelRouter;
use Illuminate\Routing\UrlGenerator;
/**
* Router
*
* Illuminate Routing wrapper for TorrentPier
*/
class Router
{
private LaravelRouter $router;
private Container $container;
public function __construct(Container $container)
{
$this->container = $container;
// Create event dispatcher if not already bound
if (!$container->bound('events')) {
$container->singleton('events', function () {
return new Dispatcher($this->container);
});
}
// Create the Illuminate Router
$this->router = new LaravelRouter($container->make('events'), $container);
// Register middleware aliases
$this->registerMiddleware();
// Bind router instance
$container->instance('router', $this->router);
$container->instance(LaravelRouter::class, $this->router);
// Create and bind URL generator
$request = $container->bound('request') ? $container->make('request') : Request::capture();
$container->instance('request', $request);
$url = new UrlGenerator($this->router->getRoutes(), $request);
$container->instance('url', $url);
$container->instance(UrlGenerator::class, $url);
}
/**
* Get the underlying Laravel Router instance
*/
public function getRouter(): LaravelRouter
{
return $this->router;
}
/**
* Register a GET route
*/
public function get(string $uri, $action): \Illuminate\Routing\Route
{
return $this->router->get($uri, $this->parseAction($action));
}
/**
* Register a POST route
*/
public function post(string $uri, $action): \Illuminate\Routing\Route
{
return $this->router->post($uri, $this->parseAction($action));
}
/**
* Register a PUT route
*/
public function put(string $uri, $action): \Illuminate\Routing\Route
{
return $this->router->put($uri, $this->parseAction($action));
}
/**
* Register a PATCH route
*/
public function patch(string $uri, $action): \Illuminate\Routing\Route
{
return $this->router->patch($uri, $this->parseAction($action));
}
/**
* Register a DELETE route
*/
public function delete(string $uri, $action): \Illuminate\Routing\Route
{
return $this->router->delete($uri, $this->parseAction($action));
}
/**
* Register an OPTIONS route
*/
public function options(string $uri, $action): \Illuminate\Routing\Route
{
return $this->router->options($uri, $this->parseAction($action));
}
/**
* Register a route for any HTTP verb
*/
public function any(string $uri, $action): \Illuminate\Routing\Route
{
return $this->router->any($uri, $this->parseAction($action));
}
/**
* Register a route group
*/
public function group(array $attributes, \Closure $callback): void
{
$this->router->group($attributes, $callback);
}
/**
* Register a route prefix
*/
public function prefix(string $prefix): \Illuminate\Routing\RouteRegistrar
{
return $this->router->prefix($prefix);
}
/**
* Register middleware
*/
public function middleware($middleware): \Illuminate\Routing\RouteRegistrar
{
return $this->router->middleware($middleware);
}
/**
* Register a resource controller
*/
public function resource(string $name, string $controller, array $options = []): \Illuminate\Routing\PendingResourceRegistration
{
return $this->router->resource($name, $controller, $options);
}
/**
* Dispatch the request to the application
*/
public function dispatch(Request $request): Response|JsonResponse
{
try {
$response = $this->router->dispatch($request);
// Ensure we always return a Response object
if (!$response instanceof Response && !$response instanceof JsonResponse) {
if (is_array($response) || is_object($response)) {
return new JsonResponse($response);
}
return new Response($response);
}
return $response;
} catch (\Symfony\Component\HttpKernel\Exception\NotFoundHttpException $e) {
return new Response('Not Found', 404);
} catch (\Exception $e) {
// Log the error if logger is available
if ($this->container->bound('log')) {
$this->container->make('log')->error($e->getMessage(), ['exception' => $e]);
}
return new Response('Internal Server Error', 500);
}
}
/**
* Parse the action to convert Class::method to array format
*/
private function parseAction($action)
{
if (is_string($action) && str_contains($action, '::')) {
return str_replace('::', '@', $action);
}
return $action;
}
/**
* Get all registered routes
*/
public function getRoutes(): \Illuminate\Routing\RouteCollection
{
return $this->router->getRoutes();
}
/**
* Set the fallback route
*/
public function fallback($action): \Illuminate\Routing\Route
{
return $this->router->fallback($this->parseAction($action));
}
/**
* Register middleware aliases
*/
private function registerMiddleware(): void
{
$middlewareAliases = [
'auth' => \App\Http\Middleware\AuthMiddleware::class,
'admin' => \App\Http\Middleware\AdminMiddleware::class,
'cors' => \App\Http\Middleware\CorsMiddleware::class,
];
foreach ($middlewareAliases as $alias => $middleware) {
$this->router->aliasMiddleware($alias, $middleware);
}
}
}

View file

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Listeners;
use App\Events\UserRegistered;
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;
/**
* Send welcome email to newly registered users
*/
class SendWelcomeEmail implements ShouldHandleEventsAfterCommit
{
/**
* Handle the event
*/
public function handle(UserRegistered $event): void
{
// TODO: Implement email sending logic
// This is where you would queue an email to be sent
// For now, just log the event
if (function_exists('bb_log')) {
bb_log(sprintf(
'Welcome email queued for user: %s (ID: %d, Email: %s)',
$event->getUsername(),
$event->getUserId(),
$event->getEmail()
));
}
}
/**
* Determine whether the listener should be queued
*/
public function shouldQueue(UserRegistered $event): bool
{
return true;
}
}

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Listeners;
use App\Events\TorrentUploaded;
/**
* Update user statistics when a torrent is uploaded
*/
class UpdateUserStatistics
{
/**
* Handle the event
*/
public function handle(TorrentUploaded $event): void
{
// TODO: Implement statistics update logic
// This would typically update the user's upload count, ratio, etc.
if (function_exists('bb_log')) {
bb_log(sprintf(
'User statistics update triggered for user %d after uploading torrent: %s (ID: %d, Size: %d bytes)',
$event->getUploaderId(),
$event->getTorrentName(),
$event->getTorrentId(),
$event->getSize()
));
}
}
}

223
app/Models/Model.php Normal file
View file

@ -0,0 +1,223 @@
<?php
declare(strict_types=1);
namespace App\Models;
use TorrentPier\Database\Database;
/**
* Base Model class for all models
* Provides basic database operations using Nette Database
*/
abstract class Model
{
protected string $table;
protected string $primaryKey = 'id';
protected array $attributes = [];
protected array $original = [];
public function __construct(
protected Database $db,
array $attributes = []
) {
$this->fill($attributes);
$this->syncOriginal();
}
/**
* Find a model by its primary key
*/
public static function find(int|string $id): ?static
{
$instance = new static(DB());
$data = $instance->db->table($instance->table)
->where($instance->primaryKey, $id)
->fetch();
if (!$data) {
return null;
}
return new static(DB(), (array) $data);
}
/**
* Find a model by a specific column
*/
public static function findBy(string $column, mixed $value): ?static
{
$instance = new static(DB());
$data = $instance->db->table($instance->table)
->where($column, $value)
->fetch();
if (!$data) {
return null;
}
return new static(DB(), (array) $data);
}
/**
* Get all models
*/
public static function all(): array
{
$instance = new static(DB());
$rows = $instance->db->table($instance->table)->fetchAll();
$models = [];
foreach ($rows as $row) {
$models[] = new static(DB(), (array) $row);
}
return $models;
}
/**
* Fill the model with an array of attributes
*/
public function fill(array $attributes): self
{
foreach ($attributes as $key => $value) {
$this->attributes[$key] = $value;
}
return $this;
}
/**
* Save the model to the database
*/
public function save(): bool
{
if ($this->exists()) {
return $this->update();
}
return $this->insert();
}
/**
* Insert a new record
*/
protected function insert(): bool
{
$this->db->table($this->table)->insert($this->attributes);
if (!isset($this->attributes[$this->primaryKey])) {
$this->attributes[$this->primaryKey] = $this->db->getInsertId();
}
$this->syncOriginal();
return true;
}
/**
* Update an existing record
*/
protected function update(): bool
{
$dirty = $this->getDirty();
if (empty($dirty)) {
return true;
}
$this->db->table($this->table)
->where($this->primaryKey, $this->getKey())
->update($dirty);
$this->syncOriginal();
return true;
}
/**
* Delete the model
*/
public function delete(): bool
{
if (!$this->exists()) {
return false;
}
$this->db->table($this->table)
->where($this->primaryKey, $this->getKey())
->delete();
return true;
}
/**
* Check if the model exists in the database
*/
public function exists(): bool
{
return isset($this->original[$this->primaryKey]);
}
/**
* Get the primary key value
*/
public function getKey(): int|string|null
{
return $this->attributes[$this->primaryKey] ?? null;
}
/**
* Get attributes that have been changed
*/
public function getDirty(): array
{
$dirty = [];
foreach ($this->attributes as $key => $value) {
if (!array_key_exists($key, $this->original) || $value !== $this->original[$key]) {
$dirty[$key] = $value;
}
}
return $dirty;
}
/**
* Sync the original attributes with the current
*/
protected function syncOriginal(): void
{
$this->original = $this->attributes;
}
/**
* Get an attribute
*/
public function __get(string $key): mixed
{
return $this->attributes[$key] ?? null;
}
/**
* Set an attribute
*/
public function __set(string $key, mixed $value): void
{
$this->attributes[$key] = $value;
}
/**
* Check if an attribute exists
*/
public function __isset(string $key): bool
{
return isset($this->attributes[$key]);
}
/**
* Convert the model to an array
*/
public function toArray(): array
{
return $this->attributes;
}
}

109
app/Models/Torrent.php Normal file
View file

@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace App\Models;
/**
* Torrent Model
*
* Represents a torrent in the database
*/
class Torrent extends Model
{
protected string $table = 'bb_bt_torrents';
protected string $primaryKey = 'topic_id';
/**
* Get active peers for this torrent
*/
public function getPeers(): array
{
return $this->db->table('bb_bt_tracker')
->where('topic_id', $this->getKey())
->where('complete', 0)
->fetchAll();
}
/**
* Get completed peers (seeders) for this torrent
*/
public function getSeeders(): array
{
return $this->db->table('bb_bt_tracker')
->where('topic_id', $this->getKey())
->where('complete', 1)
->fetchAll();
}
/**
* Get torrent statistics
*/
public function getStats(): array
{
$stats = $this->db->table('bb_bt_tracker_snap')
->where('topic_id', $this->getKey())
->fetch();
return $stats ? (array) $stats : [
'seeders' => 0,
'leechers' => 0,
'complete' => 0
];
}
/**
* Get the user who uploaded this torrent
*/
public function getUploader(): ?User
{
return User::find($this->poster_id);
}
/**
* Get the forum topic associated with this torrent
*/
public function getTopic(): array
{
$topic = $this->db->table('bb_topics')
->where('topic_id', $this->getKey())
->fetch();
return $topic ? (array) $topic : [];
}
/**
* Check if torrent is active
*/
public function isActive(): bool
{
return (int) $this->tor_status === 0;
}
/**
* Find torrent by info hash
*/
public static function findByInfoHash(string $infoHash): ?self
{
return self::findBy('info_hash', $infoHash);
}
/**
* Get recent torrents
*/
public static function getRecent(int $limit = 10): array
{
$instance = new static(DB());
$rows = $instance->db->table($instance->table)
->orderBy('reg_time DESC')
->limit($limit)
->fetchAll();
$torrents = [];
foreach ($rows as $row) {
$torrents[] = new static(DB(), (array) $row);
}
return $torrents;
}
}

139
app/Models/User.php Normal file
View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace App\Models;
/**
* User Model
*
* Represents a user in the system
*/
class User extends Model
{
protected string $table = 'bb_users';
protected string $primaryKey = 'user_id';
/**
* Find user by username
*/
public static function findByUsername(string $username): ?self
{
return self::findBy('username', $username);
}
/**
* Find user by email
*/
public static function findByEmail(string $email): ?self
{
return self::findBy('user_email', $email);
}
/**
* Get user's torrents
*/
public function getTorrents(): array
{
return $this->db->table('bb_bt_torrents')
->where('poster_id', $this->getKey())
->orderBy('reg_time DESC')
->fetchAll();
}
/**
* Get user's posts
*/
public function getPosts(int $limit = 10): array
{
return $this->db->table('bb_posts')
->where('poster_id', $this->getKey())
->orderBy('post_time DESC')
->limit($limit)
->fetchAll();
}
/**
* Get user's groups
*/
public function getGroups(): array
{
return $this->db->table('bb_user_group')
->where('user_id', $this->getKey())
->where('user_pending', 0)
->fetchAll();
}
/**
* Check if user is admin
*/
public function isAdmin(): bool
{
return (int) $this->user_level === 1;
}
/**
* Check if user is moderator
*/
public function isModerator(): bool
{
return (int) $this->user_level === 2;
}
/**
* Check if user is active
*/
public function isActive(): bool
{
return (int) $this->user_active === 1;
}
/**
* Get user's upload/download statistics
*/
public function getStats(): array
{
$stats = $this->db->table('bb_bt_users')
->where('user_id', $this->getKey())
->fetch();
return $stats ? (array) $stats : [
'u_up_total' => 0,
'u_down_total' => 0,
'u_up_release' => 0,
'u_up_bonus' => 0
];
}
/**
* Get user's ratio
*/
public function getRatio(): float
{
$stats = $this->getStats();
$downloaded = (int) $stats['u_down_total'];
$uploaded = (int) $stats['u_up_total'];
if ($downloaded === 0) {
return 0.0;
}
return round($uploaded / $downloaded, 2);
}
/**
* Verify password
*/
public function verifyPassword(string $password): bool
{
return password_verify($password, $this->user_password);
}
/**
* Update password
*/
public function updatePassword(string $newPassword): void
{
$this->user_password = password_hash($newPassword, PASSWORD_BCRYPT);
}
}

View file

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
/**
* Application Service Provider
*
* Bootstrap any application services here
*/
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services
*/
public function register(): void
{
// Register application services
$this->registerEvents();
$this->registerValidation();
}
/**
* Bootstrap any application services
*/
public function boot(): void
{
// Bootstrap services that need the application to be fully loaded
}
/**
* Register the event dispatcher
*/
protected function registerEvents(): void
{
$this->app->singleton('events', function ($app) {
return new \Illuminate\Events\Dispatcher($app);
});
$this->app->alias('events', \Illuminate\Events\Dispatcher::class);
$this->app->alias('events', \Illuminate\Contracts\Events\Dispatcher::class);
}
/**
* Register the validation factory
*/
protected function registerValidation(): void
{
$this->app->singleton('validator', function ($app) {
$loader = new \Illuminate\Translation\ArrayLoader();
$translator = new \Illuminate\Translation\Translator($loader, 'en');
return new \Illuminate\Validation\Factory($translator, $app);
});
$this->app->bind(\Illuminate\Validation\Factory::class, function ($app) {
return $app['validator'];
});
$this->app->alias('validator', \Illuminate\Validation\Factory::class);
$this->app->alias('validator', \Illuminate\Contracts\Validation\Factory::class);
}
}

View file

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Symfony\Component\Finder\Finder;
/**
* Console Service Provider
*
* Automatically discovers and registers console commands
*/
class ConsoleServiceProvider extends ServiceProvider
{
/**
* Register console commands
*/
public function register(): void
{
$this->registerCommands();
}
/**
* Register console commands
*/
protected function registerCommands(): void
{
$commands = $this->discoverCommands();
$this->app->bind('console.commands', function () use ($commands) {
return $commands;
});
// Register each command in the container
foreach ($commands as $command) {
$this->app->bind($command, function ($app) use ($command) {
return new $command();
});
}
}
/**
* Discover commands in the Commands directory
*/
protected function discoverCommands(): array
{
$commands = [];
$commandsPath = $this->app->make('path.app') . '/Console/Commands';
if (!is_dir($commandsPath)) {
return $commands;
}
$finder = new Finder();
$finder->files()->name('*Command.php')->in($commandsPath);
foreach ($finder as $file) {
$relativePath = $file->getRelativePathname();
$className = 'App\\Console\\Commands\\' . str_replace(['/', '.php'], ['\\', ''], $relativePath);
if (class_exists($className)) {
$reflection = new \ReflectionClass($className);
// Only include concrete command classes that extend our base Command
if (!$reflection->isAbstract() &&
$reflection->isSubclassOf(\App\Console\Commands\Command::class)) {
$commands[] = $className;
}
}
}
return $commands;
}
}

View file

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Providers;
use App\Events\UserRegistered;
use App\Events\TorrentUploaded;
use App\Listeners\SendWelcomeEmail;
use App\Listeners\UpdateUserStatistics;
use Illuminate\Support\ServiceProvider;
/**
* Event Service Provider
*
* Register event listeners and subscribers
*/
class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application
*/
protected array $listen = [
UserRegistered::class => [
SendWelcomeEmail::class,
],
TorrentUploaded::class => [
UpdateUserStatistics::class,
],
];
/**
* The event subscriber classes to register
*/
protected array $subscribe = [
// Add event subscribers here
];
/**
* Register any events for your application
*/
public function boot(): void
{
$events = $this->app->make('events');
foreach ($this->listen as $event => $listeners) {
foreach ($listeners as $listener) {
$events->listen($event, $listener);
}
}
foreach ($this->subscribe as $subscriber) {
$events->subscribe($subscriber);
}
}
/**
* Determine if events and listeners should be automatically discovered
*/
public function shouldDiscoverEvents(): bool
{
return false;
}
}

View file

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace App\Providers;
use App\Http\Routing\Router;
use Illuminate\Support\ServiceProvider;
use Illuminate\Http\Request;
/**
* Route Service Provider
*
* Loads and registers application routes using Illuminate Routing
*/
class RouteServiceProvider extends ServiceProvider
{
/**
* The path to the "home" route for your application
*/
public const HOME = '/';
/**
* Register services
*/
public function register(): void
{
// Register the Router
$this->app->singleton(Router::class, function ($app) {
return new Router($app);
});
// Alias for convenience
$this->app->alias(Router::class, 'router');
}
/**
* Define your route model bindings, pattern filters, etc.
*/
public function boot(): void
{
$this->configureRateLimiting();
$this->routes(function () {
$this->mapApiRoutes();
$this->mapWebRoutes();
$this->mapAdminRoutes();
});
}
/**
* Configure the rate limiters for the application
*/
protected function configureRateLimiting(): void
{
// Rate limiting can be configured here when needed
// Example:
// RateLimiter::for('api', function (Request $request) {
// return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
// });
}
/**
* Define the routes for the application
*/
protected function routes(\Closure $callback): void
{
$callback();
}
/**
* Define the "web" routes for the application
*/
protected function mapWebRoutes(): void
{
$router = $this->app->make(Router::class);
$routeFile = $this->app->make('path.base') . '/routes/web.php';
if (file_exists($routeFile)) {
$router->group([], function () use ($routeFile) {
require $routeFile;
});
}
}
/**
* Define the "api" routes for the application
*/
protected function mapApiRoutes(): void
{
$router = $this->app->make(Router::class);
$routeFile = $this->app->make('path.base') . '/routes/api.php';
if (file_exists($routeFile)) {
$router->group([
'prefix' => 'api',
], function () use ($routeFile) {
require $routeFile;
});
}
}
/**
* Define the "admin" routes for the application
*/
protected function mapAdminRoutes(): void
{
$router = $this->app->make(Router::class);
$routeFile = $this->app->make('path.base') . '/routes/admin.php';
if (file_exists($routeFile)) {
$router->group([
'prefix' => 'admin',
'middleware' => [
'auth',
'admin',
]
], function () use ($routeFile) {
require $routeFile;
});
}
}
}

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Support\ServiceProvider as IlluminateServiceProvider;
/**
* Base Service Provider
*
* All application service providers should extend this class
*/
abstract class ServiceProvider extends IlluminateServiceProvider
{
// Add any application-specific service provider functionality here
}

View file

@ -0,0 +1,244 @@
<?php
declare(strict_types=1);
namespace App\Services\Tracker;
use App\Models\Torrent;
use App\Models\User;
use TorrentPier\Cache\CacheManager;
use TorrentPier\Database\Database;
/**
* Torrent Service
*
* Handles business logic for torrent operations
*/
class TorrentService
{
public function __construct(
private Database $db,
private CacheManager $cache
) {}
/**
* Register a new torrent
*/
public function register(array $data, User $user): Torrent
{
// Validate torrent data
$this->validateTorrentData($data);
// Create torrent record
$torrent = new Torrent($this->db);
$torrent->fill([
'info_hash' => $data['info_hash'],
'poster_id' => $user->getKey(),
'size' => $data['size'],
'reg_time' => time(),
'tor_status' => 0,
'checked_user_id' => 0,
'checked_time' => 0,
'tor_type' => 0,
'speed_up' => 0,
'speed_down' => 0,
]);
$torrent->save();
// Clear cache
$this->cache->delete('recent_torrents');
return $torrent;
}
/**
* Update torrent information
*/
public function update(Torrent $torrent, array $data): bool
{
$torrent->fill($data);
$result = $torrent->save();
// Clear relevant caches
$this->cache->delete('torrent:' . $torrent->info_hash);
$this->cache->delete('recent_torrents');
return $result;
}
/**
* Delete a torrent
*/
public function delete(Torrent $torrent): bool
{
// Delete related data
$this->db->table('bb_bt_tracker')
->where('topic_id', $torrent->getKey())
->delete();
$this->db->table('bb_bt_tracker_snap')
->where('topic_id', $torrent->getKey())
->delete();
// Delete torrent
$result = $torrent->delete();
// Clear cache
$this->cache->delete('torrent:' . $torrent->info_hash);
$this->cache->delete('recent_torrents');
return $result;
}
/**
* Get paginated torrents using Laravel-style collections
*/
public function paginate(int $page = 1, ?string $category = null, int $perPage = 20): array
{
$offset = ($page - 1) * $perPage;
$query = $this->db->table('bb_bt_torrents as t')
->select('t.*, ts.seeders, ts.leechers, ts.complete')
->leftJoin('bb_bt_tracker_snap as ts', 'ts.topic_id = t.topic_id')
->where('t.tor_status', 0)
->orderBy('t.reg_time DESC')
->limit($perPage)
->offset($offset);
if ($category !== null) {
$query->where('t.tor_type', $category);
}
$rows = $query->fetchAll();
// Use Laravel-style collection for better data manipulation
$torrents = collect($rows)
->map(fn($row) => new Torrent($this->db, (array) $row))
->values()
->toArray();
return [
'data' => $torrents,
'page' => $page,
'per_page' => $perPage,
'total' => $this->countTorrents($category),
'from' => $offset + 1,
'to' => min($offset + $perPage, $this->countTorrents($category)),
'last_page' => ceil($this->countTorrents($category) / $perPage)
];
}
/**
* Search torrents using collections and modern string helpers
*/
public function search(string $query, array $filters = []): array
{
$cacheKey = 'search:' . str($query . serialize($filters))->hash('md5');
return $this->cache->remember($cacheKey, 300, function() use ($query, $filters) {
$qb = $this->db->table('bb_bt_torrents as t')
->select('t.*, ts.seeders, ts.leechers')
->leftJoin('bb_bt_tracker_snap as ts', 'ts.topic_id = t.topic_id')
->leftJoin('bb_topics as top', 'top.topic_id = t.topic_id')
->where('t.tor_status', 0);
// Search in topic title (cleaned query)
if (!empty($query)) {
$cleanQuery = str($query)->trim()->lower()->limit(100);
$qb->where('LOWER(top.topic_title) LIKE ?', '%' . $cleanQuery . '%');
}
// Apply filters using data_get helper
if ($category = data_get($filters, 'category')) {
$qb->where('t.tor_type', $category);
}
if ($minSeeders = data_get($filters, 'min_seeders')) {
$qb->where('ts.seeders >= ?', $minSeeders);
}
$rows = $qb->limit(100)->fetchAll();
// Use collection to transform and filter results
$torrents = collect($rows)
->map(fn($row) => new Torrent($this->db, (array) $row))
->when(data_get($filters, 'sort') === 'popular', function ($collection) {
return $collection->sortByDesc(fn($torrent) => $torrent->seeders ?? 0);
})
->when(data_get($filters, 'sort') === 'recent', function ($collection) {
return $collection->sortByDesc('reg_time');
})
->values()
->toArray();
return $torrents;
});
}
/**
* Get torrent statistics
*/
public function getStatistics(): array
{
return $this->cache->remember('torrent_stats', 3600, function() {
$stats = [];
// Total torrents
$stats['total_torrents'] = $this->db->table('bb_bt_torrents')
->where('tor_status', 0)
->count('*');
// Total size
$totalSize = $this->db->table('bb_bt_torrents')
->where('tor_status', 0)
->sum('size');
$stats['total_size'] = $totalSize ?: 0;
// Active peers
$stats['active_peers'] = $this->db->table('bb_bt_tracker')
->count('*');
// Completed downloads
$stats['total_completed'] = $this->db->table('bb_bt_torrents')
->where('tor_status', 0)
->sum('complete_count');
return $stats;
});
}
/**
* Validate torrent data
*/
private function validateTorrentData(array $data): void
{
if (empty($data['info_hash']) || strlen($data['info_hash']) !== 40) {
throw new \InvalidArgumentException('Invalid info hash');
}
if (empty($data['size']) || $data['size'] <= 0) {
throw new \InvalidArgumentException('Invalid torrent size');
}
// Check if torrent already exists
$existing = Torrent::findByInfoHash($data['info_hash']);
if ($existing !== null) {
throw new \InvalidArgumentException('Torrent already exists');
}
}
/**
* Count torrents
*/
private function countTorrents(?string $category = null): int
{
$query = $this->db->table('bb_bt_torrents')
->where('tor_status', 0);
if ($category !== null) {
$query->where('tor_type', $category);
}
return $query->count('*');
}
}

View file

@ -0,0 +1,196 @@
<?php
declare(strict_types=1);
namespace App\Services\User;
use App\Models\User;
use TorrentPier\Cache\CacheManager;
use TorrentPier\Database\Database;
/**
* User Service
*
* Handles business logic for user operations
*/
class UserService
{
public function __construct(
private Database $db,
private CacheManager $cache
) {}
/**
* Register a new user
*/
public function register(array $data): User
{
// Validate data
$this->validateRegistrationData($data);
// Create user
$user = new User($this->db);
$user->fill([
'username' => $data['username'],
'user_email' => $data['email'],
'user_password' => password_hash($data['password'], PASSWORD_BCRYPT),
'user_level' => 0, // Regular user
'user_active' => 1,
'user_regdate' => now()->timestamp,
'user_lastvisit' => now()->timestamp,
'user_timezone' => 0,
'user_lang' => 'en',
'user_dateformat' => 'd M Y H:i',
]);
$user->save();
// Clear user cache
$this->cache->delete('user_count');
return $user;
}
/**
* Update user profile
*/
public function updateProfile(User $user, array $data): bool
{
$allowedFields = [
'user_timezone',
'user_lang',
'user_dateformat',
];
$updateData = collect($data)
->only($allowedFields)
->filter()
->toArray();
if (empty($updateData)) {
return true;
}
$user->fill($updateData);
$result = $user->save();
// Clear user cache
$this->cache->delete('user:' . $user->getKey());
return $result;
}
/**
* Change user password
*/
public function changePassword(User $user, string $currentPassword, string $newPassword): bool
{
if (!$user->verifyPassword($currentPassword)) {
throw new \InvalidArgumentException('Current password is incorrect');
}
$user->updatePassword($newPassword);
return $user->save();
}
/**
* Get user statistics
*/
public function getUserStats(User $user): array
{
$cacheKey = 'user_stats:' . $user->getKey();
return $this->cache->remember($cacheKey, 1800, function() use ($user) {
$stats = $user->getStats();
$torrents = $user->getTorrents();
$posts = $user->getPosts(5);
return [
'upload_stats' => [
'total_uploaded' => $stats['u_up_total'] ?? 0,
'total_downloaded' => $stats['u_down_total'] ?? 0,
'ratio' => $user->getRatio(),
],
'activity' => [
'torrents_count' => count($torrents),
'recent_posts' => count($posts),
'last_visit' => now()->createFromTimestamp($user->user_lastvisit)->diffForHumans(),
],
'permissions' => [
'level' => $user->user_level,
'is_admin' => $user->isAdmin(),
'is_moderator' => $user->isModerator(),
'is_active' => $user->isActive(),
]
];
});
}
/**
* Search users using modern collection methods
*/
public function searchUsers(string $query, array $filters = []): array
{
$cacheKey = 'user_search:' . str($query . serialize($filters))->hash('md5');
return $this->cache->remember($cacheKey, 600, function() use ($query, $filters) {
// Get all active users (in a real app, this would be paginated)
$users = collect(User::all())
->where('user_active', 1);
// Apply search filter
if (!empty($query)) {
$searchTerm = str($query)->lower();
$users = $users->filter(function ($user) use ($searchTerm) {
return str($user->username)->lower()->contains($searchTerm) ||
str($user->user_email)->lower()->contains($searchTerm);
});
}
// Apply level filter
if ($level = data_get($filters, 'level')) {
$users = $users->where('user_level', $level);
}
// Apply sorting
$sortBy = data_get($filters, 'sort', 'username');
$sortDirection = data_get($filters, 'direction', 'asc');
$users = $sortDirection === 'desc'
? $users->sortByDesc($sortBy)
: $users->sortBy($sortBy);
return $users->values()->toArray();
});
}
/**
* Validate registration data
*/
private function validateRegistrationData(array $data): void
{
if (empty($data['username'])) {
throw new \InvalidArgumentException('Username is required');
}
if (empty($data['email'])) {
throw new \InvalidArgumentException('Email is required');
}
if (empty($data['password'])) {
throw new \InvalidArgumentException('Password is required');
}
// Check if username already exists
$existingUser = User::findByUsername($data['username']);
if ($existingUser) {
throw new \InvalidArgumentException('Username already exists');
}
// Check if email already exists
$existingEmail = User::findByEmail($data['email']);
if ($existingEmail) {
throw new \InvalidArgumentException('Email already exists');
}
}
}

View file

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Events\UserRegistered;
/**
* User Service
*
* Example service demonstrating event usage
*/
class UserService
{
/**
* Register a new user
*
* @param array $userData User registration data
* @return int User ID
*/
public function registerUser(array $userData): int
{
// TODO: Implement actual user registration logic
// This is a simplified example
// Simulate user creation
$userId = random_int(1000, 9999);
$username = $userData['username'] ?? 'user' . $userId;
$email = $userData['email'] ?? $username . '@example.com';
// Dispatch the UserRegistered event
event(new UserRegistered(
userId: $userId,
username: $username,
email: $email,
registeredAt: new \DateTime()
));
return $userId;
}
}

243
app/helpers.php Normal file
View file

@ -0,0 +1,243 @@
<?php
declare(strict_types=1);
/**
* Laravel-style helper functions for TorrentPier
*/
if (!function_exists('app_path')) {
/**
* Get the path to the app directory
*/
function app_path(string $path = ''): string
{
return rtrim(__DIR__, '/') . ($path ? '/' . ltrim($path, '/') : '');
}
}
if (!function_exists('base_path')) {
/**
* Get the path to the base directory
*/
function base_path(string $path = ''): string
{
return rtrim(dirname(__DIR__), '/') . ($path ? '/' . ltrim($path, '/') : '');
}
}
if (!function_exists('config_path')) {
/**
* Get the path to the config directory
*/
function config_path(string $path = ''): string
{
return base_path('config') . ($path ? '/' . ltrim($path, '/') : '');
}
}
if (!function_exists('database_path')) {
/**
* Get the path to the database directory
*/
function database_path(string $path = ''): string
{
return base_path('database') . ($path ? '/' . ltrim($path, '/') : '');
}
}
if (!function_exists('public_path')) {
/**
* Get the path to the public directory
*/
function public_path(string $path = ''): string
{
return base_path('public') . ($path ? '/' . ltrim($path, '/') : '');
}
}
if (!function_exists('resource_path')) {
/**
* Get the path to the resources directory
*/
function resource_path(string $path = ''): string
{
return base_path('resources') . ($path ? '/' . ltrim($path, '/') : '');
}
}
if (!function_exists('storage_path')) {
/**
* Get the path to the storage directory
*/
function storage_path(string $path = ''): string
{
return base_path('storage') . ($path ? '/' . ltrim($path, '/') : '');
}
}
if (!function_exists('view')) {
/**
* Create a view response (simple implementation)
*/
function view(string $view, array $data = []): string
{
$viewPath = resource_path('views/' . str_replace('.', '/', $view) . '.php');
if (!file_exists($viewPath)) {
throw new \InvalidArgumentException("View [{$view}] not found.");
}
extract($data);
ob_start();
require $viewPath;
return ob_get_clean();
}
}
if (!function_exists('fake')) {
/**
* Create fake data using FakerPHP (Laravel-style implementation)
*/
function fake(?string $locale = null): \Faker\Generator
{
return \Faker\Factory::create($locale ?? 'en_US');
}
}
if (!function_exists('collect')) {
/**
* Create a collection from the given value (using Illuminate Support)
*/
function collect(mixed $value = []): \Illuminate\Support\Collection
{
return new \Illuminate\Support\Collection($value);
}
}
if (!function_exists('data_get')) {
/**
* Get an item from an array or object using "dot" notation
*/
function data_get(mixed $target, string|array|int|null $key, mixed $default = null): mixed
{
return \Illuminate\Support\Arr::get($target, $key, $default);
}
}
if (!function_exists('data_set')) {
/**
* Set an item on an array or object using "dot" notation
*/
function data_set(mixed &$target, string|array $key, mixed $value, bool $overwrite = true): mixed
{
return \Illuminate\Support\Arr::set($target, $key, $value, $overwrite);
}
}
if (!function_exists('str')) {
/**
* Create a new stringable object from the given string
*/
function str(string $string = ''): \Illuminate\Support\Stringable
{
return \Illuminate\Support\Str::of($string);
}
}
if (!function_exists('now')) {
/**
* Create a new Carbon instance for the current time
*/
function now($tz = null): \Carbon\Carbon
{
return \Carbon\Carbon::now($tz);
}
}
if (!function_exists('today')) {
/**
* Create a new Carbon instance for today
*/
function today($tz = null): \Carbon\Carbon
{
return \Carbon\Carbon::today($tz);
}
}
if (!function_exists('tap')) {
/**
* Call the given Closure with the given value then return the value
*/
function tap(mixed $value, ?callable $callback = null): mixed
{
if (is_null($callback)) {
return new \Illuminate\Support\HigherOrderTapProxy($value);
}
$callback($value);
return $value;
}
}
if (!function_exists('optional')) {
/**
* Provide access to optional objects
*/
function optional(mixed $value = null, ?callable $callback = null): mixed
{
if (is_null($callback)) {
return new \Illuminate\Support\Optional($value);
}
if (!is_null($value)) {
return $callback($value);
}
return null;
}
}
if (!function_exists('app')) {
/**
* Get the available container instance
*/
function app(?string $abstract = null, array $parameters = []): mixed
{
if (is_null($abstract)) {
return \Illuminate\Container\Container::getInstance();
}
return \Illuminate\Container\Container::getInstance()->make($abstract, $parameters);
}
}
if (!function_exists('config')) {
/**
* Get / set the specified configuration value
*/
function config(array|string|null $key = null, mixed $default = null): mixed
{
if (is_null($key)) {
return app('config');
}
if (is_array($key)) {
return app('config')->set($key);
}
return app('config')->get($key, $default);
}
}
if (!function_exists('event')) {
/**
* Dispatch an event and call the listeners
*/
function event(string|object $event, mixed $payload = [], bool $halt = false): array|null
{
return app('events')->dispatch($event, $payload, $halt);
}
}

41
bootstrap/app.php Normal file
View file

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* Application Bootstrap
*
* This file creates and configures the application instance
*/
// Load environment variables
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
$dotenv->safeLoad();
// Load legacy common.php only if not already loaded (will be loaded by LegacyController when needed)
// This prevents header conflicts for modern API routes
if (!defined('BB_PATH')) {
// Only load for legacy routes - modern routes will skip this
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
$urlPath = parse_url($requestUri, PHP_URL_PATH) ?: $requestUri;
$isLegacyRoute = str_ends_with($urlPath, '.php') || $requestUri === '/' || str_contains($requestUri, 'tracker') || str_contains($requestUri, 'forum');
if ($isLegacyRoute) {
require_once dirname(__DIR__) . '/common.php';
}
}
// Define application constants
define('IN_TORRENTPIER', true);
// Load container bootstrap
require_once __DIR__ . '/container.php';
// Create the application container
$container = createContainer(dirname(__DIR__));
// Get the Router instance (it will be created and registered by RouteServiceProvider)
$router = $container->make(\App\Http\Routing\Router::class);
// Return the router for handling requests
return $router;

115
bootstrap/container.php Normal file
View file

@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
/**
* Container Bootstrap
*
* Creates and configures the Illuminate Container instance
*/
use Illuminate\Container\Container;
/**
* Create and configure the application container
*/
function createContainer(string $rootPath): Container
{
// Load environment variables first
$dotenv = \Dotenv\Dotenv::createImmutable($rootPath);
$dotenv->safeLoad();
$container = new Container();
// Set the container instance globally
Container::setInstance($container);
// Register base paths
$container->instance('path.base', $rootPath);
$container->instance('path.app', $rootPath . '/app');
$container->instance('path.config', $rootPath . '/config');
$container->instance('path.database', $rootPath . '/database');
$container->instance('path.public', $rootPath . '/public');
$container->instance('path.resources', $rootPath . '/resources');
$container->instance('path.storage', $rootPath . '/storage');
// Register the container itself
$container->instance(Container::class, $container);
$container->alias(Container::class, 'app');
$container->alias(Container::class, Illuminate\Contracts\Container\Container::class);
$container->alias(Container::class, Psr\Container\ContainerInterface::class);
// Load configuration
loadConfiguration($container, $rootPath);
// Register service providers
registerServiceProviders($container);
return $container;
}
/**
* Load configuration files
*/
function loadConfiguration(Container $container, string $rootPath): void
{
$configPath = $rootPath . '/config';
// Create unified config repository
$config = new \Illuminate\Config\Repository();
// Load services configuration
if (file_exists($configPath . '/services.php')) {
$services = require $configPath . '/services.php';
foreach ($services as $abstract => $concrete) {
if (is_callable($concrete)) {
$container->bind($abstract, $concrete);
} else {
$container->bind($abstract, $concrete);
}
}
}
// Load all config files into the repository
foreach (glob($configPath . '/*.php') as $file) {
$key = basename($file, '.php');
$value = require $file;
$config->set($key, $value);
// Also register individual config files for backward compatibility
$container->instance("config.{$key}", $value);
}
// Register the unified config repository
$container->instance('config', $config);
$container->bind(\Illuminate\Config\Repository::class, function() use ($config) {
return $config;
});
}
/**
* Register service providers
*/
function registerServiceProviders(Container $container): void
{
$providers = [
// Register your service providers here
\App\Providers\AppServiceProvider::class,
\App\Providers\EventServiceProvider::class,
\App\Providers\RouteServiceProvider::class,
\App\Providers\ConsoleServiceProvider::class,
];
foreach ($providers as $providerClass) {
if (class_exists($providerClass)) {
$provider = new $providerClass($container);
if (method_exists($provider, 'register')) {
$provider->register();
}
if (method_exists($provider, 'boot')) {
$container->call([$provider, 'boot']);
}
}
}
}

77
config/app.php Normal file
View file

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
/**
* Application Configuration
*/
return [
/*
|--------------------------------------------------------------------------
| Application Name
|--------------------------------------------------------------------------
*/
'name' => env('APP_NAME', 'TorrentPier'),
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
*/
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
*/
'debug' => env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
*/
'url' => env('APP_URL', 'http://localhost'),
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
*/
'timezone' => env('APP_TIMEZONE', 'UTC'),
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
*/
'locale' => env('APP_LOCALE', 'en'),
'fallback_locale' => 'en',
/*
|--------------------------------------------------------------------------
| Application Key
|--------------------------------------------------------------------------
*/
'key' => env('APP_KEY'),
/*
|--------------------------------------------------------------------------
| Autoloaded Service Providers
|--------------------------------------------------------------------------
*/
'providers' => [
// Add service providers here
],
/*
|--------------------------------------------------------------------------
| Class Aliases
|--------------------------------------------------------------------------
*/
'aliases' => [
// Add class aliases here
],
];

92
config/auth.php Normal file
View file

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
/**
* Authentication Configuration
*/
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
*/
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
*/
'providers' => [
'users' => [
'driver' => 'database',
'table' => 'bb_users',
],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
*/
'password_timeout' => 10800,
/*
|--------------------------------------------------------------------------
| Session Configuration
|--------------------------------------------------------------------------
*/
'session' => [
'lifetime' => env('SESSION_LIFETIME', 120),
'expire_on_close' => false,
'encrypt' => false,
'files' => storage_path('framework/sessions'),
'connection' => null,
'table' => 'sessions',
'store' => null,
'lottery' => [2, 100],
'cookie' => env('SESSION_COOKIE', 'torrentpier_session'),
'path' => '/',
'domain' => env('SESSION_DOMAIN', null),
'secure' => env('SESSION_SECURE_COOKIE', false),
'http_only' => true,
'same_site' => 'lax',
],
];

77
config/cache.php Normal file
View file

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
/**
* Cache Configuration
*/
return [
/*
|--------------------------------------------------------------------------
| Default Cache Store
|--------------------------------------------------------------------------
*/
'default' => env('CACHE_DRIVER', 'file'),
/*
|--------------------------------------------------------------------------
| Cache Stores
|--------------------------------------------------------------------------
*/
'stores' => [
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache'),
],
'redis' => [
'driver' => 'redis',
'connection' => 'cache',
'lock_connection' => 'default',
],
'memcached' => [
'driver' => 'memcached',
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
'sasl' => [
env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'),
],
'options' => [
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', 11211),
'weight' => 100,
],
],
],
'array' => [
'driver' => 'array',
'serialize' => false,
],
],
/*
|--------------------------------------------------------------------------
| Cache Key Prefix
|--------------------------------------------------------------------------
*/
'prefix' => env('CACHE_PREFIX', 'tp_cache'),
/*
|--------------------------------------------------------------------------
| Cache Tags
|--------------------------------------------------------------------------
*/
'tags' => [
'torrents' => 3600,
'users' => 1800,
'forums' => 900,
'stats' => 300,
],
];

86
config/database.php Normal file
View file

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
/**
* Database Configuration
*/
return [
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
*/
'default' => env('DB_CONNECTION', 'mysql'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
*/
'connections' => [
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', 3306),
'database' => env('DB_DATABASE', 'torrentpier'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => env('DB_PREFIX', 'bb_'),
'options' => [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_STRINGIFY_FETCHES => false,
],
],
'sqlite' => [
'driver' => 'sqlite',
'database' => env('DB_DATABASE_SQLITE', storage_path('database.sqlite')),
'prefix' => '',
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
*/
'migrations' => 'migrations',
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
*/
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', 'tp_'),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_DB', 0),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_CACHE_DB', 1),
],
],
];

71
config/filesystems.php Normal file
View file

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/**
* Filesystem Configuration
*/
return [
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
*/
'default' => env('FILESYSTEM_DISK', 'local'),
/*
|--------------------------------------------------------------------------
| Filesystem Disks
|--------------------------------------------------------------------------
*/
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
'throw' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL') . '/storage',
'visibility' => 'public',
'throw' => false,
],
'torrents' => [
'driver' => 'local',
'root' => storage_path('app/torrents'),
'throw' => false,
],
'avatars' => [
'driver' => 'local',
'root' => public_path('images/avatars'),
'url' => env('APP_URL') . '/images/avatars',
'visibility' => 'public',
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
],
],
/*
|--------------------------------------------------------------------------
| Symbolic Links
|--------------------------------------------------------------------------
*/
'links' => [
public_path('storage') => storage_path('app/public'),
],
];

View file

@ -1,30 +1,35 @@
<?php
use function DI\factory;
use function DI\get;
use function DI\autowire;
declare(strict_types=1);
/**
* Service Container Bindings
*
* Define service bindings for the Illuminate Container
*/
return [
// Add custom service definitions here as they are implemented
// Examples (uncomment and modify when implementing):
// Config service binding
\TorrentPier\Config::class => function () {
return \TorrentPier\Config::getInstance();
},
// Future service bindings can be added here:
// Logger service example
// 'logger' => factory(function () {
// 'logger' => function () {
// $logger = new \Monolog\Logger('torrentpier');
// $logger->pushHandler(new \Monolog\Handler\StreamHandler(__DIR__ . '/../internal_data/logs/app.log'));
// return $logger;
// }),
// },
// Configuration service example
// 'config' => factory(function () {
// return [
// 'app' => require __DIR__ . '/app.php',
// 'database' => require __DIR__ . '/database.php',
// 'cache' => require __DIR__ . '/cache.php',
// ];
// }),
// Database service example
// 'database' => function () {
// return \TorrentPier\Database\DB::getInstance();
// },
// Interface to implementation binding example
// 'ServiceInterface' => autowire('ConcreteService'),
// Cache service example
// 'cache' => function () {
// return \TorrentPier\Cache\Cache::getInstance();
// },
];

91
config/tracker.php Normal file
View file

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
/**
* BitTorrent Tracker Configuration
*/
return [
/*
|--------------------------------------------------------------------------
| Tracker Settings
|--------------------------------------------------------------------------
*/
'announce_interval' => env('TRACKER_ANNOUNCE_INTERVAL', 900), // 15 minutes
'min_interval' => env('TRACKER_MIN_INTERVAL', 300), // 5 minutes
'max_peers' => env('TRACKER_MAX_PEERS', 50),
'max_peers_per_torrent' => env('TRACKER_MAX_PEERS_PER_TORRENT', 100),
/*
|--------------------------------------------------------------------------
| Peer Settings
|--------------------------------------------------------------------------
*/
'peer_timeout' => env('TRACKER_PEER_TIMEOUT', 1800), // 30 minutes
'peer_compact' => env('TRACKER_PEER_COMPACT', true),
'peer_no_peer_id' => env('TRACKER_PEER_NO_PEER_ID', false),
/*
|--------------------------------------------------------------------------
| Ratio Requirements
|--------------------------------------------------------------------------
*/
'ratio_required' => env('TRACKER_RATIO_REQUIRED', false),
'min_ratio' => env('TRACKER_MIN_RATIO', 0.5),
'ratio_warning_threshold' => env('TRACKER_RATIO_WARNING', 0.8),
/*
|--------------------------------------------------------------------------
| Upload/Download Limits
|--------------------------------------------------------------------------
*/
'max_upload_speed' => env('TRACKER_MAX_UPLOAD_SPEED', 0), // 0 = unlimited
'max_download_speed' => env('TRACKER_MAX_DOWNLOAD_SPEED', 0), // 0 = unlimited
'max_torrents_per_user' => env('TRACKER_MAX_TORRENTS_PER_USER', 0), // 0 = unlimited
/*
|--------------------------------------------------------------------------
| Security Settings
|--------------------------------------------------------------------------
*/
'passkey_required' => env('TRACKER_PASSKEY_REQUIRED', true),
'ip_validation' => env('TRACKER_IP_VALIDATION', true),
'user_agent_validation' => env('TRACKER_USER_AGENT_VALIDATION', false),
/*
|--------------------------------------------------------------------------
| Allowed Clients
|--------------------------------------------------------------------------
*/
'allowed_clients' => [
'qBittorrent',
'uTorrent',
'BitTorrent',
'Transmission',
'Deluge',
'rtorrent',
'libtorrent',
],
/*
|--------------------------------------------------------------------------
| Banned Clients
|--------------------------------------------------------------------------
*/
'banned_clients' => [
'BitComet',
'BitLord',
'Thunder',
'Xunlei',
],
/*
|--------------------------------------------------------------------------
| Statistics
|--------------------------------------------------------------------------
*/
'stats_update_interval' => env('TRACKER_STATS_UPDATE_INTERVAL', 300), // 5 minutes
'enable_scrape' => env('TRACKER_ENABLE_SCRAPE', true),
'scrape_interval' => env('TRACKER_SCRAPE_INTERVAL', 600), // 10 minutes
];

View file

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Database\Factories;
use App\Models\User;
/**
* User Factory
*
* Create fake user data for testing
*/
class UserFactory
{
/**
* Define the model's default state using Laravel-style helpers
*/
public function definition(): array
{
return [
'username' => str('user_')->append(str()->random(8))->lower(),
'user_email' => fake()->email(),
'user_password' => password_hash('password', PASSWORD_BCRYPT),
'user_level' => 0, // Regular user
'user_active' => 1,
'user_regdate' => now()->timestamp,
'user_lastvisit' => now()->timestamp,
'user_timezone' => 0,
'user_lang' => 'en',
'user_dateformat' => 'd M Y H:i',
];
}
/**
* Create a user instance
*/
public function make(array $attributes = []): User
{
$data = array_merge($this->definition(), $attributes);
return new User(DB(), $data);
}
/**
* Create and save a user instance
*/
public function create(array $attributes = []): User
{
$user = $this->make($attributes);
$user->save();
return $user;
}
/**
* Create an admin user
*/
public function admin(): self
{
return $this->state(['user_level' => 1]);
}
/**
* Create a moderator user
*/
public function moderator(): self
{
return $this->state(['user_level' => 2]);
}
/**
* Apply state to the factory
*/
public function state(array $state): self
{
$clone = clone $this;
$clone->states[] = $state;
return $clone;
}
private array $states = [];
}

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Database\Seeders;
/**
* Database Seeder
*
* Run all seeders for the application
*/
class DatabaseSeeder
{
/**
* Seed the application's database
*/
public function run(): void
{
// Call individual seeders here
// $this->call(UserSeeder::class);
// $this->call(ForumSeeder::class);
// $this->call(TorrentSeeder::class);
}
/**
* Call a seeder class
*/
protected function call(string $seederClass): void
{
echo "Seeding: {$seederClass}\n";
$seeder = new $seederClass();
$seeder->run();
}
}

View file

@ -92,7 +92,7 @@ function init_display_template($template_var, $replacement, $filename = 'viewtop
*/
function display_post_attachments($post_id, $switch_attachment)
{
global $attach_config, $is_auth;
global $attach_config, $is_auth, $attachments;
if ((int)$switch_attachment == 0 || (int)$attach_config['disable_mod']) {
return;
@ -170,7 +170,7 @@ function display_attachments($post_id)
{
global $template, $upload_dir, $userdata, $allowed_extensions, $display_categories, $download_modes, $lang, $attachments, $upload_icons, $attach_config;
$num_attachments = @count($attachments['_' . $post_id]);
$num_attachments = isset($attachments['_' . $post_id]) ? count($attachments['_' . $post_id]) : 0;
if ($num_attachments == 0) {
return;

View file

@ -54,7 +54,7 @@ $bb_cfg['db_alias'] = [
// Cache
$bb_cfg['cache'] = [
'db_dir' => realpath(BB_ROOT) . '/internal_data/cache/filecache/',
'db_dir' => BB_PATH . '/storage/framework/cache/',
'prefix' => 'tp_',
'memcached' => [
'host' => '127.0.0.1',

37
public/index.php Normal file
View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/**
* TorrentPier - The public entry point for the web application
*
* This is the front controller that handles all incoming HTTP requests
* Following Laravel-style architecture with illuminate/http
*/
// Define the application start time
define('TORRENTPIER_START', microtime(true));
// Register the autoloader
require __DIR__ . '/../vendor/autoload.php';
// Bootstrap the application
$router = require_once __DIR__ . '/../bootstrap/app.php';
// Create Laravel-style request from globals
$request = \Illuminate\Http\Request::createFromGlobals();
try {
// Dispatch the request through the router
$response = $router->dispatch($request);
} catch (\Exception $e) {
// Simple error handling - create Laravel-style response
$response = new \Illuminate\Http\Response('Internal Server Error', 500);
}
// Send the response - handle JsonResponse specially if headers already sent
if ($response instanceof \Illuminate\Http\JsonResponse && headers_sent()) {
echo $response->getContent();
} else {
$response->send();
}

139
resources/views/hello.php Normal file
View file

@ -0,0 +1,139 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World - <?= htmlspecialchars($siteName) ?></title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
max-width: 900px;
margin: 50px auto;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.container {
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
h1 {
color: #2c3e50;
margin-bottom: 20px;
font-size: 2.5rem;
}
.info {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin: 20px 0;
}
.feature-card {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.feature-card h3 {
margin-top: 0;
color: #2c3e50;
}
.btn {
display: inline-block;
padding: 10px 20px;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 6px;
margin: 5px;
transition: background 0.3s;
}
.btn:hover {
background: #5a6fd8;
}
.code {
background: #2d3748;
color: #e2e8f0;
padding: 15px;
border-radius: 6px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.9rem;
overflow-x: auto;
}
</style>
</head>
<body>
<div class="container">
<h1>🚀 Hello World from <?= htmlspecialchars($siteName) ?>!</h1>
<p>Welcome to TorrentPier 3.0 with modern MVC architecture! This page demonstrates the new Laravel-inspired structure using <strong><?= htmlspecialchars($architecture) ?></strong>.</p>
<div class="info">
<h3>🎯 Route Information</h3>
<strong>URI:</strong> <?= htmlspecialchars($request->uri) ?><br>
<strong>Method:</strong> <?= htmlspecialchars($request->method) ?><br>
<strong>Time:</strong> <?= htmlspecialchars($currentTime) ?><br>
<strong>Controller:</strong> HelloWorldController
</div>
<h2> New Features & Technologies</h2>
<div class="feature-grid">
<div class="feature-card">
<h3>🏗️ MVC Architecture</h3>
<p>Clean separation of concerns with Models, Views, and Controllers following Laravel conventions.</p>
</div>
<div class="feature-card">
<h3>📦 Illuminate HTTP</h3>
<p>Powerful request/response handling with JsonResponse, RedirectResponse, and more.</p>
</div>
<div class="feature-card">
<h3>🛠️ Illuminate Support</h3>
<p>Collections, string helpers, array utilities, and Carbon date handling.</p>
</div>
<div class="feature-card">
<h3>📁 Laravel-style Structure</h3>
<p>Familiar directory layout: /app, /config, /database, /routes, /storage, etc.</p>
</div>
</div>
<h2>🧪 Try the New Features</h2>
<div class="code">
// Modern helper functions now available:<br>
$users = collect(['Alice', 'Bob', 'Charlie']);<br>
$title = str('hello world')->title();<br>
$time = now()->format('Y-m-d H:i:s');<br>
$value = data_get($config, 'app.name', 'default');
</div>
<div style="margin: 30px 0;">
<a href="/hello.json" class="btn">📊 View JSON API</a>
<a href="/hello/features" class="btn">🎨 Feature Demo</a>
<a href="/" class="btn">🏠 Back to Main Site</a>
</div>
<div style="margin-top: 30px; padding: 20px; background: #f8f9fa; border-radius: 8px; font-size: 0.9rem; color: #666;">
<strong>Technical Stack:</strong><br>
🔹 League/Route for routing<br>
🔹 Illuminate HTTP & Support packages<br>
🔹 PHP-DI dependency injection<br>
🔹 Nette Database for data access<br>
🔹 PSR-7 compatibility layer
</div>
</div>
</body>
</html>

56
routes/admin.php Normal file
View file

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
use App\Http\Routing\Router;
/**
* Admin Routes
*
* Define all admin panel routes here
* These routes will be prefixed with /admin and protected by admin middleware
*/
/** @var Router $router */
$router = app(Router::class);
// Admin Dashboard
// $router->get('/', 'App\Http\Controllers\Admin\DashboardController::index')->name('admin.dashboard');
// User Management
// $router->get('/users', 'App\Http\Controllers\Admin\UserController::index')->name('admin.users.index');
// $router->get('/users/{id}/edit', 'App\Http\Controllers\Admin\UserController::edit')->name('admin.users.edit');
// $router->put('/users/{id}', 'App\Http\Controllers\Admin\UserController::update')->name('admin.users.update');
// $router->delete('/users/{id}', 'App\Http\Controllers\Admin\UserController::destroy')->name('admin.users.destroy');
// Torrent Management
// $router->get('/torrents', 'App\Http\Controllers\Admin\TorrentController::index')->name('admin.torrents.index');
// $router->get('/torrents/{id}', 'App\Http\Controllers\Admin\TorrentController::show')->name('admin.torrents.show');
// $router->delete('/torrents/{id}', 'App\Http\Controllers\Admin\TorrentController::destroy')->name('admin.torrents.destroy');
// Forum Management
// $router->resource('forums', 'App\Http\Controllers\Admin\ForumController');
// Settings
// $router->get('/settings', 'App\Http\Controllers\Admin\SettingsController::index')->name('admin.settings.index');
// $router->put('/settings', 'App\Http\Controllers\Admin\SettingsController::update')->name('admin.settings.update');
// Logs
// $router->get('/logs', 'App\Http\Controllers\Admin\LogController::index')->name('admin.logs.index');
// $router->get('/logs/{type}', 'App\Http\Controllers\Admin\LogController::show')->name('admin.logs.show');
// Reports
// $router->get('/reports', 'App\Http\Controllers\Admin\ReportController::index')->name('admin.reports.index');
// $router->get('/reports/{id}', 'App\Http\Controllers\Admin\ReportController::show')->name('admin.reports.show');
// $router->put('/reports/{id}/resolve', 'App\Http\Controllers\Admin\ReportController::resolve')->name('admin.reports.resolve');
// Bans
// $router->get('/bans', 'App\Http\Controllers\Admin\BanController::index')->name('admin.bans.index');
// $router->post('/bans', 'App\Http\Controllers\Admin\BanController::store')->name('admin.bans.store');
// $router->delete('/bans/{id}', 'App\Http\Controllers\Admin\BanController::destroy')->name('admin.bans.destroy');
// Statistics
// $router->get('/stats', 'App\Http\Controllers\Admin\StatsController::index')->name('admin.stats.index');
// $router->get('/stats/users', 'App\Http\Controllers\Admin\StatsController::users')->name('admin.stats.users');
// $router->get('/stats/torrents', 'App\Http\Controllers\Admin\StatsController::torrents')->name('admin.stats.torrents');
// $router->get('/stats/tracker', 'App\Http\Controllers\Admin\StatsController::tracker')->name('admin.stats.tracker');

49
routes/api.php Normal file
View file

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
use App\Http\Routing\Router;
/**
* API Routes
*
* Define all API routes here
* These routes will be prefixed with /api
*/
/** @var Router $router */
$router = app(Router::class);
// API routes demonstrating Laravel-style functionality
// User API (showcasing illuminate/http and illuminate/validation)
$router->get('/users', 'App\Http\Controllers\Api\UserController::index')->name('api.users.index');
$router->get('/users/{id}', 'App\Http\Controllers\Api\UserController::show')->name('api.users.show');
$router->post('/users/register', 'App\Http\Controllers\Api\UserController::register')->name('api.users.register');
// Future API routes:
// Authentication
// $router->post('/auth/login', 'App\Http\Controllers\Api\AuthController::login')->name('api.auth.login');
// $router->post('/auth/logout', 'App\Http\Controllers\Api\AuthController::logout')->name('api.auth.logout');
// $router->post('/auth/refresh', 'App\Http\Controllers\Api\AuthController::refresh')->name('api.auth.refresh');
// Tracker API
// $router->get('/torrents', 'App\Http\Controllers\Api\TorrentController::index')->name('api.torrents.index');
// $router->get('/torrents/{id}', 'App\Http\Controllers\Api\TorrentController::show')->name('api.torrents.show');
// $router->post('/torrents', 'App\Http\Controllers\Api\TorrentController::store')->name('api.torrents.store');
// $router->put('/torrents/{id}', 'App\Http\Controllers\Api\TorrentController::update')->name('api.torrents.update');
// $router->delete('/torrents/{id}', 'App\Http\Controllers\Api\TorrentController::destroy')->name('api.torrents.destroy');
// Forum API
// $router->resource('/forums', 'App\Http\Controllers\Api\ForumController');
// Stats API
// $router->get('/stats/tracker', 'App\Http\Controllers\Api\StatsController::tracker')->name('api.stats.tracker');
// $router->get('/stats/forum', 'App\Http\Controllers\Api\StatsController::forum')->name('api.stats.forum');
// $router->get('/stats/users', 'App\Http\Controllers\Api\StatsController::users')->name('api.stats.users');
// Search API
// $router->get('/search/torrents', 'App\Http\Controllers\Api\SearchController::torrents')->name('api.search.torrents');
// $router->get('/search/users', 'App\Http\Controllers\Api\SearchController::users')->name('api.search.users');
// $router->get('/search/forums', 'App\Http\Controllers\Api\SearchController::forums')->name('api.search.forums');

80
routes/web.php Normal file
View file

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
use App\Http\Routing\Router;
use Illuminate\Http\Request;
/**
* Web Routes
*
* Define all web-accessible routes here
*/
/** @var Router $router */
$router = app(Router::class);
// Test routes showcasing new Laravel-style features
$router->get('/hello', 'App\Http\Controllers\Web\HelloWorldController::index')->name('hello');
$router->get('/hello.json', 'App\Http\Controllers\Web\HelloWorldController::jsonResponse')->name('hello.json');
$router->get('/hello/json', 'App\Http\Controllers\Web\HelloWorldController::jsonResponse')->name('hello.json.alt');
$router->get('/hello/features', 'App\Http\Controllers\Web\HelloWorldController::features')->name('hello.features');
// Future routes can be added here:
// Home page
// $router->get('/', 'App\Http\Controllers\Web\HomeController::index')->name('home');
// Forum routes
// $router->get('/forum', 'App\Http\Controllers\Web\ForumController::index')->name('forum.index');
// $router->get('/forum/{id}', 'App\Http\Controllers\Web\ForumController::show')->name('forum.show');
// $router->get('/forum/{id}/topics', 'App\Http\Controllers\Web\TopicController::index')->name('forum.topics');
// Tracker routes
// $router->get('/tracker', 'App\Http\Controllers\Web\TrackerController::index')->name('tracker.index');
// $router->get('/tracker/torrent/{id}', 'App\Http\Controllers\Web\TrackerController::show')->name('torrent.show');
// $router->get('/tracker/categories', 'App\Http\Controllers\Web\CategoryController::index')->name('categories.index');
// User routes
// $router->get('/profile/{id}', 'App\Http\Controllers\Web\UserController::profile')->name('user.profile');
// $router->get('/users', 'App\Http\Controllers\Web\UserController::index')->name('users.index');
// Authentication routes (if using custom auth)
// $router->get('/login', 'App\Http\Controllers\Auth\LoginController::showLoginForm')->name('login');
// $router->post('/login', 'App\Http\Controllers\Auth\LoginController::login');
// $router->post('/logout', 'App\Http\Controllers\Auth\LoginController::logout')->name('logout');
// $router->get('/register', 'App\Http\Controllers\Auth\RegisterController::showRegistrationForm')->name('register');
// $router->post('/register', 'App\Http\Controllers\Auth\RegisterController::register');
// Legacy routes - explicit routes for each legacy controller
$router->get('/', 'App\Http\Controllers\Web\LegacyController@index')->name('legacy.index');
$router->get('/ajax.php', 'App\Http\Controllers\Web\LegacyController@ajax')->name('legacy.ajax');
$router->get('/dl.php', 'App\Http\Controllers\Web\LegacyController@dl')->name('legacy.dl');
$router->get('/dl_list.php', 'App\Http\Controllers\Web\LegacyController@dl_list')->name('legacy.dl_list');
$router->get('/feed.php', 'App\Http\Controllers\Web\LegacyController@feed')->name('legacy.feed');
$router->get('/filelist.php', 'App\Http\Controllers\Web\LegacyController@filelist')->name('legacy.filelist');
$router->get('/group.php', 'App\Http\Controllers\Web\LegacyController@group')->name('legacy.group');
$router->get('/group_edit.php', 'App\Http\Controllers\Web\LegacyController@group_edit')->name('legacy.group_edit');
$router->get('/index.php', 'App\Http\Controllers\Web\LegacyController@index')->name('legacy.index_php');
$router->get('/info.php', 'App\Http\Controllers\Web\LegacyController@info')->name('legacy.info');
$router->get('/login.php', 'App\Http\Controllers\Web\LegacyController@login')->name('legacy.login');
$router->get('/memberlist.php', 'App\Http\Controllers\Web\LegacyController@memberlist')->name('legacy.memberlist');
$router->get('/modcp.php', 'App\Http\Controllers\Web\LegacyController@modcp')->name('legacy.modcp');
$router->get('/playback_m3u.php', 'App\Http\Controllers\Web\LegacyController@playback_m3u')->name('legacy.playback_m3u');
$router->get('/poll.php', 'App\Http\Controllers\Web\LegacyController@poll')->name('legacy.poll');
$router->get('/posting.php', 'App\Http\Controllers\Web\LegacyController@posting')->name('legacy.posting');
$router->get('/privmsg.php', 'App\Http\Controllers\Web\LegacyController@privmsg')->name('legacy.privmsg');
$router->get('/profile.php', 'App\Http\Controllers\Web\LegacyController@profile')->name('legacy.profile');
$router->get('/search.php', 'App\Http\Controllers\Web\LegacyController@search')->name('legacy.search');
$router->get('/terms.php', 'App\Http\Controllers\Web\LegacyController@terms')->name('legacy.terms');
$router->get('/tracker.php', 'App\Http\Controllers\Web\LegacyController@tracker')->name('legacy.tracker');
$router->get('/viewforum.php', 'App\Http\Controllers\Web\LegacyController@viewforum')->name('legacy.viewforum');
$router->get('/viewtopic.php', 'App\Http\Controllers\Web\LegacyController@viewtopic')->name('legacy.viewtopic');
// POST routes for legacy controllers that need them
$router->post('/ajax.php', 'App\Http\Controllers\Web\LegacyController@ajax')->name('legacy.ajax.post');
$router->post('/login.php', 'App\Http\Controllers\Web\LegacyController@login')->name('legacy.login.post');
$router->post('/posting.php', 'App\Http\Controllers\Web\LegacyController@posting')->name('legacy.posting.post');
$router->post('/privmsg.php', 'App\Http\Controllers\Web\LegacyController@privmsg')->name('legacy.privmsg.post');
$router->post('/profile.php', 'App\Http\Controllers\Web\LegacyController@profile')->name('legacy.profile.post');
$router->post('/search.php', 'App\Http\Controllers\Web\LegacyController@search')->name('legacy.search.post');