This commit is contained in:
Yury Pikhtarev 2025-06-23 11:47:06 +00:00 committed by GitHub
commit 1e14fa9098
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1590 changed files with 9385 additions and 5736 deletions

View file

@ -1,7 +0,0 @@
9766c534bddad8e82e6d19f9bad5cf70b9887f9a
92ce77ec0ec703c08a659419087a373f76e711f7
2d53efc945c7747be1755d0b66557a86bdc12cbd
602137b65129b817811b80975a369ebde3270c6d
4eb26ae37e1f4c82a45961517ffeb54c20200408
e59adce848a9e10ee5775254045cbbd915236b8b
9e0a64108d62236ab07b3f8d10e8c78269b8e1d1

View file

@ -1,7 +0,0 @@
---
name: Feature / Enhancement request
about: Suggest an idea for TorrentPier
title: "[Feature]"
labels: [Feature, Enhancement]
assignees: ''
---

View file

@ -1,80 +0,0 @@
name: Continuous Deployment
on:
push:
tags:
- "v*.*.*"
jobs:
generate-changelog:
name: Generate changelog
runs-on: ubuntu-22.04
outputs:
release_body: ${{ steps.git-cliff.outputs.content }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate a changelog
uses: orhun/git-cliff-action@v4
id: git-cliff
with:
config: cliff.toml
args: -vv --latest --no-exec --github-repo ${{ github.repository }}
- name: Print the changelog
run: cat "${{ steps.git-cliff.outputs.changelog }}"
release:
name: Create release
needs: [ generate-changelog ]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set the release version
shell: bash
run: echo "RELEASE_VERSION=${GITHUB_REF:11}" >> $GITHUB_ENV
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Install Composer dependencies
run: composer install --no-dev --no-progress --prefer-dist --optimize-autoloader
- name: Cleanup
run: php _cleanup.php && rm _cleanup.php
- name: Create archive
id: create-zip
run: |
ZIP_NAME="torrentpier-v${{ env.RELEASE_VERSION }}.zip"
zip -r "$ZIP_NAME" . -x ".git/*"
echo "ZIP_NAME=$ZIP_NAME" >> $GITHUB_OUTPUT
- name: Publish to GitHub
if: ${{ !contains(github.ref, '-') }}
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ steps.create-zip.outputs.ZIP_NAME }}
overwrite: true
tag: ${{ github.ref }}
release_name: "v${{ env.RELEASE_VERSION }}"
body: "${{ needs.generate-changelog.outputs.release_body }}"
- name: Publish to GitHub (pre-release)
if: ${{ contains(github.ref, '-') }}
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ steps.create-zip.outputs.ZIP_NAME }}
overwrite: true
tag: ${{ github.ref }}
release_name: "v${{ env.RELEASE_VERSION }}"
body: "${{ needs.generate-changelog.outputs.release_body }}"
prerelease: true

View file

@ -1,74 +0,0 @@
name: Continuous Integration
on:
push:
branches:
- master
jobs:
nightly:
name: Nightly builds 📦
runs-on: ubuntu-22.04
steps:
- name: Checkout code 🗳
uses: actions/checkout@v4
- name: Setup PHP 🔩
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Install Composer dependencies 🪚
run: composer install --no-dev --no-progress --prefer-dist --optimize-autoloader
- name: Get commit hash 🔗
id: get-commit-hash
run: |
COMMIT_HASH=$(git rev-parse --short HEAD)
echo "COMMIT_HASH=$COMMIT_HASH" >> $GITHUB_OUTPUT
- name: Cleanup
run: php _cleanup.php && rm _cleanup.php
- name: Create archive 🗞
id: create-zip
run: |
ZIP_NAME="torrentpier-${{ steps.get-commit-hash.outputs.COMMIT_HASH }}.zip"
zip -r "$ZIP_NAME" . -x ".git/*"
echo "ZIP_NAME=$ZIP_NAME" >> $GITHUB_OUTPUT
- name: Upload Archive 📤
uses: actions/upload-artifact@v4
with:
name: TorrentPier-master
path: ${{ steps.create-zip.outputs.ZIP_NAME }}
deploy:
name: 🎉 Deploy
runs-on: ubuntu-22.04
steps:
- name: 🚚 Get latest code
uses: actions/checkout@v4
- name: 🔩 Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: 🖇 Install Composer dependencies
run: composer install --no-dev --no-progress --prefer-dist --optimize-autoloader
- name: 📂 Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
with:
server: ${{ secrets.FTP_SERVER }}
username: ${{ secrets.FTP_USERNAME }}
password: ${{ secrets.FTP_PASSWORD }}
server-dir: ${{ secrets.FTP_DIR }}
protocol: ${{ secrets.FTP_PROTOCOL }}
port: ${{ secrets.FTP_PORT }}
exclude: |
**/.git*
**/.git*/**
.env

View file

@ -1,57 +0,0 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# PHPMD is a spin-off project of PHP Depend and
# aims to be a PHP equivalent of the well known Java tool PMD.
# What PHPMD does is: It takes a given PHP source code base
# and look for several potential problems within that source.
# These problems can be things like:
# Possible bugs
# Suboptimal code
# Overcomplicated expressions
# Unused parameters, methods, properties
# More details at https://phpmd.org/
name: PHPMD
on:
push:
branches: [ "master" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "master" ]
schedule:
- cron: '40 0 * * 3'
permissions:
contents: read
jobs:
PHPMD:
name: Run PHPMD scanning
runs-on: ubuntu-latest
permissions:
contents: read # for checkout to fetch code
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@aa1fe473f9c687b6fb896056d771232c0bc41161
with:
coverage: none
tools: phpmd
- name: Run PHPMD
run: phpmd . sarif codesize --reportfile phpmd-results.sarif
continue-on-error: true
- name: Upload analysis results to GitHub
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: phpmd-results.sarif
wait-for-processing: true

View file

@ -1,41 +0,0 @@
name: Changelog generation
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs:
changelog:
name: Changelog generation
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: master
token: ${{ secrets.REPO_TOKEN }}
- name: Generate a changelog
uses: orhun/git-cliff-action@v4
id: git-cliff
with:
config: cliff.toml
args: v2.4.6-alpha.4.. --verbose
env:
OUTPUT: CHANGELOG.md
GITHUB_REPO: ${{ github.repository }}
- name: Print the changelog
run: cat "${{ steps.git-cliff.outputs.changelog }}"
- name: Commit changelog
run: |
git checkout master
git config --local user.name 'belomaxorka'
git config --local user.email 'roman25052006.kelesh@gmail.com'
set +e
git add CHANGELOG.md
git commit -m "changelog: Update CHANGELOG.md 📖"
git push https://${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git master

View file

@ -1,18 +0,0 @@
## set default server charset
AddDefaultCharset UTF-8
## folder listing access control
Options All -Indexes
## sitemap and atom rewrite
RewriteEngine On
RewriteRule ^sitemap.xml$ sitemap/sitemap.xml [L]
RewriteRule ^/internal_data/atom/(.*) /atom$1 [L]
## deny access to git folder
RedirectMatch 404 /\\.git(/|$)
## deny access to system files
<FilesMatch "\.(.*sql|tpl|db|inc|log|md|env)|(config|common).php$">
Require all denied
</FilesMatch>

266
CLAUDE.md
View file

@ -10,38 +10,69 @@ TorrentPier is a BitTorrent tracker engine written in PHP, designed for hosting
- **PHP 8.3+** with modern features - **PHP 8.3+** with modern features
- **MySQL/MariaDB/Percona** database - **MySQL/MariaDB/Percona** database
- **Nette Database** with temporary backward-compatible wrapper - **Nette Database** for data access (primary)
- **Illuminate Database** for Eloquent ORM (optional)
- **Illuminate Container** for dependency injection
- **Illuminate Support** for collections and helpers
- **Illuminate Collections** for enhanced collection handling
- **Illuminate Events** for event-driven architecture
- **Illuminate Routing** for Laravel-style routing
- **Illuminate Validation** for request validation
- **Illuminate HTTP** for request/response handling
- **Composer** for dependency management - **Composer** for dependency management
- **Custom BitTorrent tracker** implementation - **Custom BitTorrent tracker** implementation
- **Laravel-style MVC Architecture** with familiar patterns
## Key Directory Structure ## Key Directory Structure
- `/src/` - Modern PHP classes (PSR-4 autoloaded as `TorrentPier\`) ### Laravel-style Structure
- `/library/` - Core application logic and legacy code - `/app/` - Main application directory (PSR-4 autoloaded as `App\`)
- `/admin/` - Administrative interface - `/Console/Commands/` - Artisan-style CLI commands for Dexter
- `/Http/Controllers/` - Web, API, and Admin controllers
- `/Http/Middleware/` - HTTP middleware
- `/Http/Routing/` - Routing components (uses Illuminate Routing)
- `/Models/` - Data models using Nette Database
- `/Services/` - Business logic services
- `/Providers/` - Service providers
- `/Container/` - Container wrapper and extensions
- `/Support/` - Helper classes and utilities
- `/bootstrap/` - Application bootstrap files (app.php, container.php)
- `/config/` - Laravel-style configuration files (app.php, database.php, etc.)
- `/database/` - Migrations, seeders, factories
- `/public/` - Web root with front controller (index.php)
- `/resources/` - Views, language files, assets
- `/routes/` - Route definitions (web.php, api.php, admin.php)
- `/storage/` - Application storage (app/, framework/, logs/)
- `dexter` - CLI interface
### Core Utilities & Legacy
- `/src/` - Core utilities and services (PSR-4 autoloaded as `TorrentPier\`)
- `/library/` - Legacy core application logic
- `/controllers/` - Legacy PHP controllers (being migrated)
- `/admin/` - Legacy administrative interface
- `/bt/` - BitTorrent tracker functionality (announce.php, scrape.php) - `/bt/` - BitTorrent tracker functionality (announce.php, scrape.php)
- `/styles/` - Templates, CSS, JS, images - `/styles/` - Legacy templates, CSS, JS, images
- `/internal_data/` - Cache, logs, compiled templates - `/internal_data/` - Legacy cache, logs, compiled templates
- `/install/` - Installation scripts and configuration examples
- `/migrations/` - Database migration files (Phinx)
## Entry Points & Key Files ## Entry Points & Key Files
- `index.php` - Main forum homepage ### Modern Entry Points
- `tracker.php` - Torrent search/browse interface - `public/index.php` - Laravel-style front controller (web requests)
- `dexter` - CLI interface (console commands)
- `bootstrap/app.php` - Application bootstrap
- `bootstrap/container.php` - Container setup and configuration
- `bootstrap/console.php` - Console bootstrap
### Legacy Entry Points (Backward Compatibility)
- `bt/announce.php` - BitTorrent announce endpoint - `bt/announce.php` - BitTorrent announce endpoint
- `bt/scrape.php` - BitTorrent scrape endpoint - `bt/scrape.php` - BitTorrent scrape endpoint
- `admin/index.php` - Administrative panel - `admin/index.php` - Legacy administrative panel
- `cron.php` - Background task runner (CLI only) - `cron.php` - Background task runner
- `install.php` - Installation script (CLI only)
## Development Commands ## Development Commands
### Installation & Setup ### Installation & Setup
```bash ```bash
# Automated installation (CLI)
php install.php
# Install dependencies # Install dependencies
composer install composer install
@ -58,24 +89,82 @@ php cron.php
### Code Quality ### Code Quality
The project uses **StyleCI** with PSR-2 preset for code style enforcement. StyleCI configuration is in `.styleci.yml` targeting `src/` directory. The project uses **StyleCI** with PSR-2 preset for code style enforcement. StyleCI configuration is in `.styleci.yml` targeting `src/` directory.
## Modern Architecture Components ## MVC Architecture Components
### Models (`/app/Models/`)
- **Simple Active Record pattern** using Nette Database (primary)
- **Eloquent ORM** available via Illuminate Database (optional)
- Base `Model` class provides common CRUD operations
- No complex ORM required, just straightforward database access
- Example: `Torrent`, `User`, `Forum`, `Post` models
### Controllers (`/app/Http/Controllers/`)
- **Thin controllers** that delegate to services
- Organized by area: `Web/`, `Api/`, `Admin/`
- `LegacyController` maintains backward compatibility
- Base `Controller` class provides common methods
### Services (`/app/Services/`)
- **Business logic layer** between controllers and models
- Handles complex operations and workflows
- Example: `TorrentService`, `AuthService`, `ForumService`
- Injected via dependency injection
### Views (`/resources/views/`)
- **PHP templates** (planning future Twig integration)
- Organized by feature areas
- Layouts for consistent structure
- Partials for reusable components
## Infrastructure Components
### Database Layer (`/src/Database/`) ### Database Layer (`/src/Database/`)
- **Nette Database** replacing legacy SqlDb system - **Nette Database** for all data access (primary)
- **Illuminate Database** available for Eloquent ORM features
- Modern singleton pattern accessible via `DB()` function - Modern singleton pattern accessible via `DB()` function
- Support for multiple database connections and debug functionality - Support for multiple database connections and debug functionality
- **Breaking changes expected** during 3.0 migration to ORM-style queries - Direct SQL queries when needed
### Cache System (`/src/Cache/`) ### Cache System (`/src/Cache/`)
- **Unified caching** using Nette Caching internally - **Unified caching** using Nette Caching internally
- Replaces existing `CACHE()` and $datastore systems - Replaces existing `CACHE()` and $datastore systems
- Supports file, SQLite, memory, and Memcached storage - Supports file, SQLite, memory, and Memcached storage
- **API changes planned** for improved developer experience - Used by services and repositories
### Configuration Management ### Configuration Management
- Environment-based config with `.env` files - Environment-based config with `.env` files
- **Illuminate Config** for Laravel-style configuration
- Modern singleton `Config` class accessible via `config()` function - Modern singleton `Config` class accessible via `config()` function
- **Legacy config access will be removed** in favor of new patterns - Configuration files in `/config/` directory
### Event System (`/app/Events/` & `/app/Listeners/`)
- **Illuminate Events** for decoupled, event-driven architecture
- Event classes in `/app/Events/`
- Listener classes in `/app/Listeners/`
- Event-listener mappings in `EventServiceProvider`
- Global `event()` helper function for dispatching events
- Support for queued listeners (when queue system is configured)
### Routing System
- **Illuminate Routing** for full Laravel-compatible routing (as of TorrentPier 3.0)
- Route definitions in `/routes/` directory (web.php, api.php, admin.php)
- Support for route groups, middleware, named routes, route model binding
- Resource controllers and RESTful routing patterns
- Custom `IlluminateRouter` wrapper for smooth Laravel integration
- Legacy custom router preserved as `LegacyRouter` for reference
- Backward compatibility maintained through `Router` alias
### Validation Layer
- **Illuminate Validation** for robust input validation
- Form request classes in `/app/Http/Requests/`
- Base `FormRequest` class for common validation logic
- Custom validation rules and messages
- Automatic validation exception handling
### HTTP Layer
- **Illuminate HTTP** for request/response handling
- Middleware support for request filtering
- JSON response helpers and content negotiation
## Configuration Files ## Configuration Files
- `.env` - Environment variables (copy from `.env.example`) - `.env` - Environment variables (copy from `.env.example`)
@ -89,16 +178,14 @@ The project uses **StyleCI** with PSR-2 preset for code style enforcement. Style
- **GitHub Actions** for automated testing and deployment - **GitHub Actions** for automated testing and deployment
- **StyleCI** for code style enforcement - **StyleCI** for code style enforcement
- **Dependabot** for dependency updates - **Dependabot** for dependency updates
- **FTP deployment** to demo environment
### Installation Methods ### Installation Methods
1. **Automated**: `php install.php` (recommended) 1. **Composer**: `composer create-project torrentpier/torrentpier`
2. **Composer**: `composer create-project torrentpier/torrentpier` 2. **Manual**: Git clone + `composer install`
3. **Manual**: Git clone + `composer install` + database setup
## Database & Schema ## Database & Schema
- **Database migrations** managed via Phinx in `/migrations/` directory - **Database migrations** managed via Phinx in `/database/migrations/` directory
- Initial schema: `20250619000001_initial_schema.php` - Initial schema: `20250619000001_initial_schema.php`
- Initial seed data: `20250619000002_seed_initial_data.php` - Initial seed data: `20250619000002_seed_initial_data.php`
- UTF-8 (utf8mb4) character set required - UTF-8 (utf8mb4) character set required
@ -118,25 +205,132 @@ php vendor/bin/phinx migrate --fake --configuration=phinx.php
## TorrentPier 3.0 Modernization Strategy ## TorrentPier 3.0 Modernization Strategy
The TorrentPier 3.0 release represents a major architectural shift focused on: The TorrentPier 3.0 release represents a major architectural shift to Laravel-style MVC:
- **Laravel-style MVC Architecture**: Clean Model-View-Controller pattern
- **Illuminate Container**: Laravel's dependency injection container
- **Modern PHP practices**: PSR standards, namespaces, autoloading - **Modern PHP practices**: PSR standards, namespaces, autoloading
- **Clean architecture**: Separation of concerns, dependency injection - **Developer friendly**: Familiar Laravel patterns for easier contribution
- **Performance improvements**: Optimized database queries, efficient caching - **Performance improvements**: Optimized database queries, efficient caching
- **Developer experience**: Better debugging, testing, and maintenance
- **Breaking changes**: Legacy code removal and API modernization - **Breaking changes**: Legacy code removal and API modernization
**Important**: TorrentPier 3.0 will introduce breaking changes to achieve these modernization goals. Existing deployments should remain on 2.x versions until they're ready to migrate to the new architecture. **Important**: TorrentPier 3.0 will introduce breaking changes to achieve these modernization goals. Existing deployments should remain on 2.x versions until they're ready to migrate to the new architecture.
## Migration Path for 3.0 ## Current Architecture
- **Database layer**: Legacy SqlDb calls will be removed, migrate to new Database class ### Container & Dependency Injection
- **Cache system**: Replace existing CACHE() and $datastore calls with new unified API - **Illuminate Container**: Laravel's container for dependency injection
- **Configuration**: Update legacy global $bb_cfg access to use config() singleton - **Bootstrap**: Clean container setup in `/bootstrap/container.php`
- **Templates**: Legacy template syntax may be deprecated in favor of modern Twig features - **Service Providers**: Laravel-style providers in `/app/Providers/`
- **Language system**: Update global $lang usage to new Language singleton methods - **Helper Functions**: Global helpers - `app()`, `config()`, `event()`
When working with this codebase, prioritize modern architecture patterns and clean code practices. Focus on the new systems in `/src/` directory rather than maintaining legacy compatibility. ### MVC Structure
- **Controllers**: All controllers in `/app/Http/Controllers/`
- **Models**: Simple models in `/app/Models/` (using Nette Database or Eloquent)
- **Services**: Business logic in `/app/Services/`
- **Routes**: Laravel-style route definitions in `/routes/`
- **Middleware**: HTTP middleware in `/app/Http/Middleware/`
- **Events**: Event classes in `/app/Events/`
- **Listeners**: Event listeners in `/app/Listeners/`
- **Requests**: Form request validation in `/app/Http/Requests/`
### Migration Steps for New Features
1. Create models in `/app/Models/` extending base `Model` class (or Eloquent models)
2. Add business logic to services in `/app/Services/`
3. Create form request classes in `/app/Http/Requests/` for validation
4. Create thin controllers in `/app/Http/Controllers/`
5. Define routes in `/routes/` files using Illuminate Routing syntax
6. Create events in `/app/Events/` and listeners in `/app/Listeners/`
7. Register event listeners in `EventServiceProvider`
8. Use helper functions: `app()`, `config()`, `event()` for easy access
### What to Avoid
- Don't use complex DDD patterns (aggregates, value objects)
- Don't implement CQRS or event sourcing
- Don't create repository interfaces (use concrete classes if needed)
- Don't over-engineer - keep it simple and Laravel-like
When working with this codebase, prioritize simplicity and maintainability. New features should be built in the `/app/` directory using Laravel-style MVC patterns.
## Example Usage
### Events and Listeners
```php
// Dispatch an event from a service
use App\Events\UserRegistered;
event(new UserRegistered($user));
// Create an event listener
class SendWelcomeEmail
{
public function handle(UserRegistered $event): void
{
// Send welcome email to $event->user
}
}
```
### Form Validation
```php
// Create a form request class
class RegisterUserRequest extends FormRequest
{
public function rules(): array
{
return [
'email' => 'required|email|unique:users',
'username' => 'required|string|min:3|max:20',
'password' => 'required|string|min:8',
];
}
}
// Use in controller
public function register(RegisterUserRequest $request): JsonResponse
{
// Request is automatically validated
$validated = $request->validated();
// ... create user
}
```
### Routing with Groups and Middleware
```php
// In routes/api.php
$router->group(['prefix' => 'v1', 'middleware' => 'auth'], function () use ($router) {
$router->resource('torrents', 'TorrentController');
$router->get('stats', 'StatsController::index');
});
```
### Using Collections
```php
use Illuminate\Support\Collection;
$users = collect(User::all());
$activeUsers = $users->filter(fn($user) => $user->isActive())
->sortBy('last_seen')
->take(10);
```
### Middleware Usage
```php
// Apply middleware to routes
$router->middleware(['auth', 'admin'])->group(function () use ($router) {
$router->get('/admin/users', 'AdminController::users');
});
// Create custom middleware
class CustomMiddleware
{
public function handle(Request $request, \Closure $next)
{
// Middleware logic here
return $next($request);
}
}
```
## Markdown File Guidelines ## Markdown File Guidelines

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). 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 ## 📌 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. * *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

@ -595,8 +595,8 @@ $announceUrl = $bb_cfg['bt_announce_url'];
$dbHost = $bb_cfg['database']['host']; $dbHost = $bb_cfg['database']['host'];
// ✅ New way (recommended) // ✅ New way (recommended)
$announceUrl = config()->get('bt_announce_url'); $announceUrl = tp_config()->get('bt_announce_url');
$dbHost = config()->get('database.host'); $dbHost = tp_config()->get('database.host');
``` ```
### Key Configuration Changes ### Key Configuration Changes
@ -604,57 +604,57 @@ $dbHost = config()->get('database.host');
#### Basic Usage #### Basic Usage
```php ```php
// Get configuration values using dot notation // Get configuration values using dot notation
$siteName = config()->get('sitename'); $siteName = tp_config()->get('sitename');
$dbHost = config()->get('database.host'); $dbHost = tp_config()->get('database.host');
$cacheTimeout = config()->get('cache.timeout'); $cacheTimeout = tp_config()->get('cache.timeout');
// Get with default value if key doesn't exist // Get with default value if key doesn't exist
$maxUsers = config()->get('max_users_online', 100); $maxUsers = tp_config()->get('max_users_online', 100);
$debugMode = config()->get('debug.enabled', false); $debugMode = tp_config()->get('debug.enabled', false);
``` ```
#### Setting Values #### Setting Values
```php ```php
// Set configuration values // Set configuration values
config()->set('sitename', 'My Awesome Tracker'); tp_config()->set('sitename', 'My Awesome Tracker');
config()->set('database.port', 3306); tp_config()->set('database.port', 3306);
config()->set('cache.enabled', true); tp_config()->set('cache.enabled', true);
``` ```
#### Working with Sections #### Working with Sections
```php ```php
// Get entire configuration section // Get entire configuration section
$dbConfig = config()->getSection('database'); $dbConfig = tp_config()->getSection('database');
$trackerConfig = config()->getSection('tracker'); $trackerConfig = tp_config()->getSection('tracker');
// Check if configuration exists // Check if configuration exists
if (config()->has('bt_announce_url')) { if (tp_config()->has('bt_announce_url')) {
$announceUrl = config()->get('bt_announce_url'); $announceUrl = tp_config()->get('bt_announce_url');
} }
``` ```
### Common Configuration Mappings ### Common Configuration Mappings
| Old Syntax | New Syntax | | Old Syntax | New Syntax |
|------------|------------| |------------|---------------------------------------|
| `$bb_cfg['sitename']` | `config()->get('sitename')` | | `$bb_cfg['sitename']` | `tp_config()->get('sitename')` |
| `$bb_cfg['database']['host']` | `config()->get('database.host')` | | `$bb_cfg['database']['host']` | `tp_config()->get('database.host')` |
| `$bb_cfg['tracker']['enabled']` | `config()->get('tracker.enabled')` | | `$bb_cfg['tracker']['enabled']` | `tp_config()->get('tracker.enabled')` |
| `$bb_cfg['cache']['timeout']` | `config()->get('cache.timeout')` | | `$bb_cfg['cache']['timeout']` | `tp_config()->get('cache.timeout')` |
| `$bb_cfg['torr_server']['url']` | `config()->get('torr_server.url')` | | `$bb_cfg['torr_server']['url']` | `tp_config()->get('torr_server.url')` |
### Magic Methods Support ### Magic Methods Support
```php ```php
// Magic getter // Magic getter
$siteName = config()->sitename; $siteName = tp_config()->sitename;
$dbHost = config()->{'database.host'}; $dbHost = tp_config()->{'database.host'};
// Magic setter // Magic setter
config()->sitename = 'New Site Name'; tp_config()->sitename = 'New Site Name';
config()->{'database.port'} = 3306; tp_config()->{'database.port'} = 3306;
// Magic isset // Magic isset
if (isset(config()->bt_announce_url)) { if (isset(tp_config()->bt_announce_url)) {
// Configuration exists // Configuration exists
} }
``` ```
@ -804,7 +804,7 @@ _e('WELCOME_MESSAGE'); // Same as: echo __('WELCOME_MESSAGE')
_e('USER_ONLINE', 'Online'); // With default value _e('USER_ONLINE', 'Online'); // With default value
// ✅ Common usage patterns // ✅ Common usage patterns
$title = __('PAGE_TITLE', config()->get('sitename')); $title = __('PAGE_TITLE', tp_config()->get('sitename'));
$error = __('ERROR.INVALID_INPUT', 'Invalid input'); $error = __('ERROR.INVALID_INPUT', 'Invalid input');
``` ```
@ -1113,8 +1113,8 @@ $environment = [
- **New Implementation**: Uses Nette Database v3.2 with improved API requiring code updates - **New Implementation**: Uses Nette Database v3.2 with improved API requiring code updates
### Deprecated Functions ### Deprecated Functions
- `get_config()` → Use `config()->get()` - `get_config()` → Use `tp_config()->get()`
- `set_config()` → Use `config()->set()` - `set_config()` → Use `tp_config()->set()`
- Direct `$bb_cfg` access → Use `config()` methods - Direct `$bb_cfg` access → Use `config()` methods
### Deprecated Patterns ### Deprecated Patterns
@ -1139,11 +1139,11 @@ $environment = [
### Configuration Management ### Configuration Management
```php ```php
// ✅ Always provide defaults // ✅ Always provide defaults
$timeout = config()->get('api.timeout', 30); $timeout = tp_config()->get('api.timeout', 30);
// ✅ Use type hints // ✅ Use type hints
function getMaxUploadSize(): int { function getMaxUploadSize(): int {
return (int) config()->get('upload.max_size', 10485760); return (int) tp_config()->get('upload.max_size', 10485760);
} }
// ✅ Cache frequently used values // ✅ Cache frequently used values
@ -1151,7 +1151,7 @@ class TrackerService {
private string $announceUrl; private string $announceUrl;
public function __construct() { public function __construct() {
$this->announceUrl = config()->get('bt_announce_url'); $this->announceUrl = tp_config()->get('bt_announce_url');
} }
} }
``` ```
@ -1221,7 +1221,7 @@ function setupCustomCensoring(): void {
```php ```php
// ✅ Graceful error handling // ✅ Graceful error handling
try { try {
$dbConfig = config()->getSection('database'); $dbConfig = tp_config()->getSection('database');
// Database operations // Database operations
} catch (Exception $e) { } catch (Exception $e) {
error_log("Database configuration error: " . $e->getMessage()); error_log("Database configuration error: " . $e->getMessage());
@ -1232,7 +1232,7 @@ try {
### Performance Optimization ### Performance Optimization
```php ```php
// ✅ Minimize configuration calls in loops // ✅ Minimize configuration calls in loops
$cacheEnabled = config()->get('cache.enabled', false); $cacheEnabled = tp_config()->get('cache.enabled', false);
for ($i = 0; $i < 1000; $i++) { for ($i = 0; $i < 1000; $i++) {
if ($cacheEnabled) { if ($cacheEnabled) {
// Use cached value // Use cached value
@ -1244,12 +1244,12 @@ for ($i = 0; $i < 1000; $i++) {
```php ```php
// ✅ Validate configuration values // ✅ Validate configuration values
$maxFileSize = min( $maxFileSize = min(
config()->get('upload.max_size', 1048576), tp_config()->get('upload.max_size', 1048576),
1048576 * 100 // Hard limit: 100MB 1048576 * 100 // Hard limit: 100MB
); );
// ✅ Sanitize user-configurable values // ✅ Sanitize user-configurable values
$siteName = htmlspecialchars(config()->get('sitename', 'TorrentPier')); $siteName = htmlspecialchars(tp_config()->get('sitename', 'TorrentPier'));
``` ```
### Testing and Quality Assurance ### Testing and Quality Assurance

View file

@ -1,57 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
if (!defined('BB_ROOT')) {
define('BB_ROOT', __DIR__ . DIRECTORY_SEPARATOR);
define('BB_PATH', BB_ROOT);
}
// Check CLI mode
if (PHP_SAPI != 'cli') {
exit;
}
if (!function_exists('removeFile')) {
// Get all constants
require_once BB_ROOT . 'library/defines.php';
// Include CLI functions
require INC_DIR . '/functions_cli.php';
}
$items = [
'.github',
'.cliffignore',
'.editorconfig',
'.gitignore',
'.styleci.yml',
'_release.php',
'CHANGELOG.md',
'CLAUDE.md',
'cliff.toml',
'CODE_OF_CONDUCT.md',
'CONTRIBUTING.md',
'crowdin.yml',
'HISTORY.md',
'phpunit.xml',
'README.md',
'SECURITY.md',
'tests',
'UPGRADE_GUIDE.md'
];
foreach ($items as $item) {
$path = BB_ROOT . $item;
if (is_file($path)) {
removeFile($path);
} elseif (is_dir($path)) {
removeDir($path);
}
}

View file

@ -1,130 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
define('BB_ROOT', __DIR__ . DIRECTORY_SEPARATOR);
define('BB_PATH', BB_ROOT);
// Check CLI mode
if (PHP_SAPI != 'cli') {
die('Please run <code style="background:#222;color:#00e01f;padding:2px 6px;border-radius:3px;">php ' . basename(__FILE__) . '</code> in CLI mode');
}
// Get all constants
require_once BB_ROOT . 'library/defines.php';
// Include CLI functions
require INC_DIR . '/functions_cli.php';
// Welcoming message
out("--- Release creation tool ---\n", 'info');
$configFile = BB_PATH . '/library/config.php';
if (!is_file($configFile)) {
out('- Config file ' . basename($configFile) . ' not found', 'error');
exit;
}
if (!is_readable($configFile)) {
out('- Config file ' . basename($configFile) . ' is not readable', 'error');
exit;
}
if (!is_writable($configFile)) {
out('- Config file ' . basename($configFile) . ' is not writable', 'error');
exit;
}
// Ask for version
fwrite(STDOUT, 'Enter version number (e.g, v2.4.0): ');
$version = trim(fgets(STDIN));
if (empty($version)) {
out("- Version cannot be empty. Please enter a valid version number", 'error');
exit;
} else {
// Add 'v' prefix if missing
if (!str_starts_with($version, 'v')) {
$version = 'v' . $version;
}
out("- Using version: $version", 'info');
}
// Ask for version emoji
fwrite(STDOUT, 'Enter version emoji: ');
$versionEmoji = trim(fgets(STDIN));
if (!empty($versionEmoji)) {
out("- Using version emoji: $versionEmoji", 'info');
}
// Ask for release date or use today's date
fwrite(STDOUT, "Enter release date (e.g. 25-05-2025), leave empty to use today's date: ");
$date = trim(fgets(STDIN));
if (empty($date)) {
$date = date('d-m-Y');
out("- Using current date: $date", 'info');
} else {
// Validate date format (dd-mm-yyyy)
$dateObj = DateTime::createFromFormat('d-m-Y', $date);
if (!$dateObj || $dateObj->format('d-m-Y') !== $date) {
out("- Invalid date format. Expected format: DD-MM-YYYY", 'error');
exit;
}
out("- Using date: $date", 'info');
}
// Read config file content
$content = file_get_contents($configFile);
// Update version
$content = preg_replace(
"/\\\$bb_cfg\['tp_version'\]\s*=\s*'[^']*';/",
"\$bb_cfg['tp_version'] = '$version';",
$content
);
// Update release date
$content = preg_replace(
"/\\\$bb_cfg\['tp_release_date'\]\s*=\s*'[^']*';/",
"\$bb_cfg['tp_release_date'] = '$date';",
$content
);
// Save updated config
$bytesWritten = file_put_contents($configFile, $content);
if ($bytesWritten === false) {
out("- Failed to write to config file", 'error');
exit;
}
if ($bytesWritten === 0) {
out("- Config file was not updated (0 bytes written)", 'error');
exit;
}
out("\n- Config file has been updated!", 'success');
// Update CHANGELOG.md
runProcess('npx git-cliff v2.4.6-alpha.4.. --config cliff.toml --tag "' . $version . '" > CHANGELOG.md');
// Git add & commit
runProcess('git add -A && git commit -m "release: ' . escapeshellarg($version) . (!empty($versionEmoji) ? (' ' . $versionEmoji) : '') . '"');
// Git tag
runProcess("git tag -a \"$version\" -m \"Release $version\"");
runProcess("git tag -v \"$version\"");
// Git push
runProcess("git push origin master");
runProcess("git push origin $version");
out("\n- Release $version has been successfully prepared, committed and pushed!", 'success');

View file

@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Symfony\Component\Console\Input\InputOption;
/**
* Clear Cache Command
*
* Clears application cache files
*/
class ClearCacheCommand extends Command
{
/**
* The command signature
*/
protected string $signature = 'cache:clear';
/**
* The command description
*/
protected string $description = 'Clear application cache';
/**
* Configure the command
*/
protected function configure(): void
{
$this->addOption(
'force',
'f',
InputOption::VALUE_NONE,
'Force clearing cache without confirmation'
);
}
/**
* Handle the command
*/
public function handle(): int
{
$force = $this->option('force');
if (!$force && !$this->confirm('Are you sure you want to clear all cache?', true)) {
$this->info('Cache clear cancelled.');
return self::SUCCESS;
}
$this->info('Clearing application cache...');
$cleared = 0;
// Clear file cache
$cacheDir = $this->app->make('path.base') . '/storage/framework/cache';
if (is_dir($cacheDir)) {
$files = glob($cacheDir . '/*');
foreach ($files as $file) {
if (is_file($file)) {
unlink($file);
$cleared++;
}
}
$this->line("✓ File cache cleared ({$cleared} files)");
}
// Clear view cache
$viewCacheDir = $this->app->make('path.base') . '/storage/framework/views';
if (is_dir($viewCacheDir)) {
$viewFiles = glob($viewCacheDir . '/*');
$viewCleared = 0;
foreach ($viewFiles as $file) {
if (is_file($file)) {
unlink($file);
$viewCleared++;
}
}
$this->line("✓ View cache cleared ({$viewCleared} files)");
$cleared += $viewCleared;
}
// Clear legacy cache directories
$legacyCacheDir = $this->app->make('path.base') . '/internal_data/cache';
if (is_dir($legacyCacheDir)) {
$legacyCleared = $this->clearDirectoryRecursive($legacyCacheDir);
$this->line("✓ Legacy cache cleared ({$legacyCleared} files)");
$cleared += $legacyCleared;
}
$this->success("Cache cleared successfully! Total files removed: {$cleared}");
return self::SUCCESS;
}
/**
* Recursively clear a directory
*/
private function clearDirectoryRecursive(string $dir): int
{
$cleared = 0;
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $file) {
if ($file->isFile()) {
unlink($file->getRealPath());
$cleared++;
}
}
return $cleared;
}
}

View file

@ -0,0 +1,207 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command as SymfonyCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Illuminate\Container\Container;
/**
* Base Command Class
*
* Laravel-style base command class for TorrentPier console commands
*/
abstract class Command extends SymfonyCommand
{
/**
* The command signature (Laravel-style)
* Example: 'cache:clear {--force : Force clearing without confirmation}'
*/
protected string $signature = '';
/**
* The command description
*/
protected string $description = '';
/**
* Application container
*/
protected Container $app;
/**
* Console input interface
*/
protected InputInterface $input;
/**
* Console output interface
*/
protected OutputInterface $output;
/**
* Symfony style interface
*/
protected SymfonyStyle $io;
/**
* Create a new command instance
*/
public function __construct(?string $name = null)
{
// Parse signature if provided
if ($this->signature) {
$name = $this->parseSignature();
}
parent::__construct($name);
// Set description
if ($this->description) {
$this->setDescription($this->description);
}
// Get container instance
$this->app = Container::getInstance();
}
/**
* Execute the command
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->input = $input;
$this->output = $output;
$this->io = new SymfonyStyle($input, $output);
try {
$result = $this->handle();
return is_int($result) ? $result : self::SUCCESS;
} catch (\Exception $e) {
$this->error('Command failed: ' . $e->getMessage());
return self::FAILURE;
}
}
/**
* Handle the command (implement in subclasses)
*/
abstract public function handle(): int;
/**
* Parse Laravel-style signature
*/
protected function parseSignature(): string
{
// Simple signature parsing - just extract command name for now
// Full Laravel signature parsing would be more complex
$parts = explode(' ', trim($this->signature));
return $parts[0];
}
/**
* Get an argument value
*/
protected function argument(?string $key = null): mixed
{
if ($key === null) {
return $this->input->getArguments();
}
return $this->input->getArgument($key);
}
/**
* Get an option value
*/
protected function option(?string $key = null): mixed
{
if ($key === null) {
return $this->input->getOptions();
}
return $this->input->getOption($key);
}
/**
* Display an info message
*/
protected function info(string $message): void
{
$this->io->info($message);
}
/**
* Display an error message
*/
protected function error(string $message): void
{
$this->io->error($message);
}
/**
* Display a warning message
*/
protected function warn(string $message): void
{
$this->io->warning($message);
}
/**
* Display a success message
*/
protected function success(string $message): void
{
$this->io->success($message);
}
/**
* Display a line of text
*/
protected function line(string $message): void
{
$this->output->writeln($message);
}
/**
* Ask a question
*/
protected function ask(string $question, ?string $default = null): ?string
{
return $this->io->ask($question, $default);
}
/**
* Ask for confirmation
*/
protected function confirm(string $question, bool $default = false): bool
{
return $this->io->confirm($question, $default);
}
/**
* Ask the user to select from a list of options
*/
protected function choice(string $question, array $choices, ?string $default = null): string
{
return $this->io->choice($question, $choices, $default);
}
/**
* Get application configuration
*/
protected function config(?string $key = null, mixed $default = null): mixed
{
$config = $this->app->make('config');
if ($key === null) {
return $config;
}
return $config->get($key, $default);
}
}

View file

@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
/**
* Info Command
*
* Display TorrentPier system information
*/
class InfoCommand extends Command
{
/**
* The command signature
*/
protected string $signature = 'info';
/**
* The command description
*/
protected string $description = 'Display system information';
/**
* Handle the command
*/
public function handle(): int
{
$basePath = $this->app->make('path.base');
$this->line('');
$this->line('<fg=cyan>TorrentPier System Information</>');
$this->line('<fg=cyan>==============================</>');
$this->line('');
// Application info
$this->line('<fg=yellow>Application:</>');
$this->line(' Name: TorrentPier');
$this->line(' Version: 3.0-dev');
$this->line(' Environment: ' . ($_ENV['APP_ENV'] ?? 'production'));
$this->line(' Debug Mode: ' . (($_ENV['APP_DEBUG'] ?? false) ? 'enabled' : 'disabled'));
$this->line('');
// Paths
$this->line('<fg=yellow>Paths:</>');
$this->line(' Base: ' . $basePath);
$this->line(' App: ' . $this->app->make('path.app'));
$this->line(' Config: ' . $this->app->make('path.config'));
$this->line(' Storage: ' . $this->app->make('path.storage'));
$this->line(' Public: ' . $this->app->make('path.public'));
$this->line('');
// PHP info
$this->line('<fg=yellow>PHP:</>');
$this->line(' Version: ' . PHP_VERSION);
$this->line(' SAPI: ' . PHP_SAPI);
$this->line(' Memory Limit: ' . ini_get('memory_limit'));
$this->line(' Max Execution Time: ' . ini_get('max_execution_time') . 's');
$this->line('');
// Extensions
$requiredExtensions = ['pdo', 'curl', 'gd', 'mbstring', 'openssl', 'zip'];
$this->line('<fg=yellow>Required Extensions:</>');
foreach ($requiredExtensions as $ext) {
$status = extension_loaded($ext) ? '<fg=green>✓</>' : '<fg=red>✗</>';
$this->line(" {$status} {$ext}");
}
$this->line('');
// File permissions
$this->line('<fg=yellow>File Permissions:</>');
$writablePaths = [
'storage',
'storage/app',
'storage/framework',
'storage/logs',
'internal_data/cache',
'data/uploads'
];
foreach ($writablePaths as $path) {
$fullPath = $basePath . '/' . $path;
if (file_exists($fullPath)) {
$writable = is_writable($fullPath);
$status = $writable ? '<fg=green>✓</>' : '<fg=red>✗</>';
$this->line(" {$status} {$path}");
} else {
$this->line(" <fg=yellow>?</> {$path} (not found)");
}
}
$this->line('');
// Database
try {
if ($this->app->bound('config')) {
$config = $this->app->make('config');
$dbConfig = $config->get('database', []);
if (!empty($dbConfig)) {
$this->line('<fg=yellow>Database:</>');
$this->line(' Host: ' . ($dbConfig['host'] ?? 'not configured'));
$this->line(' Database: ' . ($dbConfig['dbname'] ?? 'not configured'));
$this->line(' Driver: ' . ($dbConfig['driver'] ?? 'not configured'));
}
}
} catch (\Exception $e) {
$this->line('<fg=yellow>Database:</> Configuration error');
}
$this->line('');
return self::SUCCESS;
}
}

View file

@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Symfony\Component\Console\Input\InputOption;
/**
* Migrate Command
*
* Run database migrations using Phinx
*/
class MigrateCommand extends Command
{
/**
* The command signature
*/
protected string $signature = 'migrate';
/**
* The command description
*/
protected string $description = 'Run database migrations';
/**
* Configure the command
*/
protected function configure(): void
{
$this->addOption(
'fake',
null,
InputOption::VALUE_NONE,
'Mark migrations as run without actually running them'
)
->addOption(
'target',
't',
InputOption::VALUE_REQUIRED,
'Target migration version'
)
->addOption(
'force',
'f',
InputOption::VALUE_NONE,
'Force running migrations in production'
);
}
/**
* Handle the command
*/
public function handle(): int
{
$basePath = $this->app->make('path.base');
$phinxConfig = $basePath . '/phinx.php';
if (!file_exists($phinxConfig)) {
$this->error('Phinx configuration file not found at: ' . $phinxConfig);
return self::FAILURE;
}
$this->info('Running database migrations...');
// Build phinx command
$command = 'cd ' . escapeshellarg($basePath) . ' && ';
$command .= 'vendor/bin/phinx migrate';
$command .= ' --configuration=' . escapeshellarg($phinxConfig);
if ($this->option('fake')) {
$command .= ' --fake';
}
if ($this->option('target')) {
$command .= ' --target=' . escapeshellarg($this->option('target'));
}
if ($this->option('force')) {
$command .= ' --no-interaction';
}
// Execute the command
$output = [];
$returnCode = 0;
exec($command . ' 2>&1', $output, $returnCode);
// Display output
foreach ($output as $line) {
$this->line($line);
}
if ($returnCode === 0) {
$this->success('Migrations completed successfully!');
} else {
$this->error('Migration failed with exit code: ' . $returnCode);
}
return $returnCode === 0 ? self::SUCCESS : self::FAILURE;
}
}

View file

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Container;
use Illuminate\Container\Container as IlluminateContainer;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Container\CircularDependencyException;
/**
* Application Container
*
* Extends Illuminate Container with application-specific functionality
*/
class Container extends IlluminateContainer
{
/**
* Get a service from the container
*
* @throws BindingResolutionException When the service cannot be resolved
* @throws CircularDependencyException
*/
public function get(string $id): mixed
{
return $this->resolve($id);
}
/**
* Check if a service exists in the container
*/
public function has(string $id): bool
{
return $this->bound($id);
}
}

View file

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace App\Events;
use DateTimeInterface;
/**
* Event fired when a new torrent is uploaded
*/
readonly class TorrentUploaded
{
/**
* Create a new event instance
*/
public function __construct(
public int $torrentId,
public int $uploaderId,
public string $torrentName,
public int $size,
public DateTimeInterface $uploadedAt
)
{
}
/**
* Get the torrent ID
*/
public function getTorrentId(): int
{
return $this->torrentId;
}
/**
* Get the uploader user ID
*/
public function getUploaderId(): int
{
return $this->uploaderId;
}
/**
* Get the torrent name
*/
public function getTorrentName(): string
{
return $this->torrentName;
}
/**
* Get the torrent size in bytes
*/
public function getSize(): int
{
return $this->size;
}
/**
* Get the upload timestamp
*/
public function getUploadedAt(): DateTimeInterface
{
return $this->uploadedAt;
}
}

View file

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Events;
use DateTimeInterface;
/**
* Event fired when a new user is registered
*/
readonly class UserRegistered
{
/**
* Create a new event instance
*/
public function __construct(
public int $userId,
public string $username,
public string $email,
public DateTimeInterface $registeredAt
)
{
}
/**
* Get the user ID
*/
public function getUserId(): int
{
return $this->userId;
}
/**
* Get the username
*/
public function getUsername(): string
{
return $this->username;
}
/**
* Get the email
*/
public function getEmail(): string
{
return $this->email;
}
/**
* Get the registration timestamp
*/
public function getRegisteredAt(): DateTimeInterface
{
return $this->registeredAt;
}
}

View file

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Exceptions;
use Throwable;
/**
* Application Exception Handler
*
* Handles all uncaught exceptions in the application
*/
class Handler
{
/**
* Handle an uncaught exception
*/
public function handle(Throwable $exception): void
{
// Log the exception
error_log($exception->getMessage() . ' in ' . $exception->getFile() . ':' . $exception->getLine());
// You can add custom handling here
}
/**
* Render an exception for HTTP response
*/
public function render(Throwable $exception): string
{
if (php_sapi_name() === 'cli') {
return $exception->getMessage() . "\n";
}
// Return JSON for API requests or HTML for web
$isJson = isset($_SERVER['HTTP_ACCEPT']) && str_contains($_SERVER['HTTP_ACCEPT'], 'application/json');
if ($isJson) {
return json_encode([
'error' => $exception->getMessage(),
'code' => $exception->getCode()
]);
}
return '<h1>Error</h1><p>' . htmlspecialchars($exception->getMessage()) . '</p>';
}
}

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,52 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
/**
* Admin Authorization Middleware
*
* Example middleware showing how to implement role-based authorization
* using Illuminate HTTP components
*/
class AdminMiddleware
{
/**
* Handle an incoming request
*
* @param Request $request
* @param Closure $next
* @return Response|JsonResponse
*/
public function handle(Request $request, Closure $next): Response|JsonResponse
{
// Check if user is authenticated first
$userId = $request->attributes->get('authenticated_user_id');
if (!$userId) {
if ($request->expectsJson()) {
return new JsonResponse(['error' => 'Authentication required'], 401);
}
return new Response('Authentication required', 401);
}
// TODO: Implement actual admin role check
// For now, accept user ID 1 as admin
if ($userId !== 1) {
if ($request->expectsJson()) {
return new JsonResponse(['error' => 'Admin access required'], 403);
}
return new Response('Admin access required', 403);
}
return $next($request);
}
}

View file

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
/**
* Authentication Middleware
*
* Example middleware showing how to implement authentication
* using Illuminate HTTP components
*/
class AuthMiddleware
{
/**
* Handle an incoming request
*
* @param Request $request
* @param Closure $next
* @return Response|JsonResponse
*/
public function handle(Request $request, Closure $next): Response|JsonResponse
{
// Example authentication check
$token = $request->bearerToken() ?? $request->input('api_token');
if (!$token) {
if ($request->expectsJson()) {
return new JsonResponse(['error' => 'Authentication required'], 401);
}
return new Response('Authentication required', 401);
}
// TODO: Implement actual token validation
// For now, accept any token that starts with 'valid_'
if (!str_starts_with($token, 'valid_')) {
if ($request->expectsJson()) {
return new JsonResponse(['error' => 'Invalid token'], 401);
}
return new Response('Invalid token', 401);
}
// Add user info to request for use in controllers
$request->attributes->set('authenticated_user_id', 1);
return $next($request);
}
}

View file

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
abstract class BaseMiddleware
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next): Response
{
$request = $this->before($request);
$response = $next($request);
return $this->after($request, $response);
}
/**
* Process request before passing to next middleware
*/
protected function before(Request $request): Request
{
return $request;
}
/**
* Process response after middleware chain
*/
protected function after(Request $request, Response $response): Response
{
return $response;
}
}

View file

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CorsMiddleware
{
private array $allowedOrigins;
private array $allowedHeaders;
private array $allowedMethods;
public function __construct(
array $allowedOrigins = ['*'],
array $allowedHeaders = ['Content-Type', 'Authorization', 'X-Requested-With'],
array $allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
)
{
$this->allowedOrigins = $allowedOrigins;
$this->allowedHeaders = $allowedHeaders;
$this->allowedMethods = $allowedMethods;
}
public function handle(Request $request, Closure $next): Response
{
if ($request->getMethod() === 'OPTIONS') {
return $this->createPreflightResponse($request);
}
$response = $next($request);
return $this->addCorsHeaders($response, $request);
}
private function createPreflightResponse(Request $request): Response
{
$response = new Response('', 200);
return $this->addCorsHeaders($response, $request);
}
private function addCorsHeaders(Response $response, Request $request): Response
{
$origin = $request->headers->get('Origin', '');
if (in_array('*', $this->allowedOrigins) || in_array($origin, $this->allowedOrigins)) {
$response->headers->set('Access-Control-Allow-Origin', $origin ?: '*');
}
$response->headers->set('Access-Control-Allow-Methods', implode(', ', $this->allowedMethods));
$response->headers->set('Access-Control-Allow-Headers', implode(', ', $this->allowedHeaders));
$response->headers->set('Access-Control-Max-Age', '86400');
return $response;
}
}

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;

47
bootstrap/console.php Normal file
View file

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/**
* Console Bootstrap
*
* Bootstrap the console application
*/
use Illuminate\Contracts\Container\BindingResolutionException;
use Symfony\Component\Console\Application;
// Only define DEXTER_BINARY if not already defined
if (!defined('DEXTER_BINARY')) {
define('DEXTER_BINARY', true);
}
require_once __DIR__ . '/../vendor/autoload.php';
// Load container bootstrap
require_once __DIR__ . '/container.php';
// Create the application container
$container = createContainer(dirname(__DIR__));
// Create Symfony Console Application
$app = new Application('TorrentPier Console', '3.0-dev');
// Get registered commands from the container
try {
if ($container->bound('console.commands')) {
$commands = $container->make('console.commands');
foreach ($commands as $command) {
try {
$app->add($container->make($command));
} catch (BindingResolutionException $e) {
// Skip commands that can't be resolved - console still works with built-in commands
continue;
}
}
}
} catch (BindingResolutionException $e) {
// No commands registered or service binding failed - console still works with built-in commands
}
// Return the console application
return $app;

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']);
}
}
}
}

View file

@ -18,8 +18,8 @@ if (empty($userAgent)) {
die; die;
} }
$announce_interval = config()->get('announce_interval'); $announce_interval = tp_config()->get('announce_interval');
$passkey_key = config()->get('passkey_key'); $passkey_key = tp_config()->get('passkey_key');
// Recover info_hash // Recover info_hash
if (isset($_GET['?info_hash']) && !isset($_GET['info_hash'])) { if (isset($_GET['?info_hash']) && !isset($_GET['info_hash'])) {
@ -65,10 +65,10 @@ if (strlen($peer_id) !== 20) {
} }
// Check for client ban // Check for client ban
if (config()->get('client_ban.enabled')) { if (tp_config()->get('client_ban.enabled')) {
$targetClient = []; $targetClient = [];
foreach (config()->get('client_ban.clients') as $clientId => $banReason) { foreach (tp_config()->get('client_ban.clients') as $clientId => $banReason) {
if (str_starts_with($peer_id, $clientId)) { if (str_starts_with($peer_id, $clientId)) {
$targetClient = [ $targetClient = [
'peer_id' => $clientId, 'peer_id' => $clientId,
@ -78,7 +78,7 @@ if (config()->get('client_ban.enabled')) {
} }
} }
if (config()->get('client_ban.only_allow_mode')) { if (tp_config()->get('client_ban.only_allow_mode')) {
if (empty($targetClient['peer_id'])) { if (empty($targetClient['peer_id'])) {
msg_die('Your BitTorrent client has been banned!'); msg_die('Your BitTorrent client has been banned!');
} }
@ -129,7 +129,7 @@ if (
|| !is_numeric($port) || !is_numeric($port)
|| ($port < 1024 && !$stopped) || ($port < 1024 && !$stopped)
|| $port > 0xFFFF || $port > 0xFFFF
|| (!empty(config()->get('disallowed_ports')) && in_array($port, config()->get('disallowed_ports'))) || (!empty(tp_config()->get('disallowed_ports')) && in_array($port, tp_config()->get('disallowed_ports')))
) { ) {
msg_die('Invalid port: ' . $port); msg_die('Invalid port: ' . $port);
} }
@ -168,13 +168,13 @@ if (preg_match('/(Mozilla|Browser|Chrome|Safari|AppleWebKit|Opera|Links|Lynx|Bot
$ip = $_SERVER['REMOTE_ADDR']; $ip = $_SERVER['REMOTE_ADDR'];
// 'ip' query handling // 'ip' query handling
if (!config()->get('ignore_reported_ip') && isset($_GET['ip']) && $ip !== $_GET['ip']) { if (!tp_config()->get('ignore_reported_ip') && isset($_GET['ip']) && $ip !== $_GET['ip']) {
if (!config()->get('verify_reported_ip') && isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { if (!tp_config()->get('verify_reported_ip') && isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$x_ip = $_SERVER['HTTP_X_FORWARDED_FOR']; $x_ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
if ($x_ip === $_GET['ip']) { if ($x_ip === $_GET['ip']) {
$filteredIp = filter_var($x_ip, FILTER_VALIDATE_IP); $filteredIp = filter_var($x_ip, FILTER_VALIDATE_IP);
if ($filteredIp !== false && (config()->get('allow_internal_ip') || !filter_var($filteredIp, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE))) { if ($filteredIp !== false && (tp_config()->get('allow_internal_ip') || !filter_var($filteredIp, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE))) {
$ip = $filteredIp; $ip = $filteredIp;
} }
} }
@ -270,7 +270,7 @@ if ($lp_info) {
define('IS_MOD', !IS_GUEST && (int)$row['user_level'] === MOD); define('IS_MOD', !IS_GUEST && (int)$row['user_level'] === MOD);
define('IS_GROUP_MEMBER', !IS_GUEST && (int)$row['user_level'] === GROUP_MEMBER); define('IS_GROUP_MEMBER', !IS_GUEST && (int)$row['user_level'] === GROUP_MEMBER);
define('IS_USER', !IS_GUEST && (int)$row['user_level'] === USER); define('IS_USER', !IS_GUEST && (int)$row['user_level'] === USER);
define('IS_SUPER_ADMIN', IS_ADMIN && isset(config()->get('super_admins')[$user_id])); define('IS_SUPER_ADMIN', IS_ADMIN && isset(tp_config()->get('super_admins')[$user_id]));
define('IS_AM', IS_ADMIN || IS_MOD); define('IS_AM', IS_ADMIN || IS_MOD);
$topic_id = $row['topic_id']; $topic_id = $row['topic_id'];
$releaser = (int)($user_id == $row['poster_id']); $releaser = (int)($user_id == $row['poster_id']);
@ -278,13 +278,13 @@ if ($lp_info) {
$tor_status = $row['tor_status']; $tor_status = $row['tor_status'];
// Check tor status // Check tor status
if (!IS_AM && isset(config()->get('tor_frozen')[$tor_status]) && !(isset(config()->get('tor_frozen_author_download')[$tor_status]) && $releaser)) { if (!IS_AM && isset(tp_config()->get('tor_frozen')[$tor_status]) && !(isset(tp_config()->get('tor_frozen_author_download')[$tor_status]) && $releaser)) {
msg_die('Torrent frozen and cannot be downloaded'); msg_die('Torrent frozen and cannot be downloaded');
} }
// Check hybrid status // Check hybrid status
if (!empty($row['info_hash']) && !empty($row['info_hash_v2'])) { if (!empty($row['info_hash']) && !empty($row['info_hash_v2'])) {
$stat_protocol = match ((int)config()->get('tracker.hybrid_stat_protocol')) { $stat_protocol = match ((int)tp_config()->get('tracker.hybrid_stat_protocol')) {
2 => substr($row['info_hash_v2'], 0, 20), 2 => substr($row['info_hash_v2'], 0, 20),
default => $row['info_hash'] // 1 default => $row['info_hash'] // 1
}; };
@ -294,7 +294,7 @@ if ($lp_info) {
} }
// Ratio limits // Ratio limits
if ((RATIO_ENABLED || config()->get('tracker.limit_concurrent_ips')) && !$stopped) { if ((RATIO_ENABLED || tp_config()->get('tracker.limit_concurrent_ips')) && !$stopped) {
$user_ratio = get_bt_ratio($row); $user_ratio = get_bt_ratio($row);
if ($user_ratio === null) { if ($user_ratio === null) {
$user_ratio = 1; $user_ratio = 1;
@ -302,10 +302,10 @@ if ($lp_info) {
$rating_msg = ''; $rating_msg = '';
if (!$seeder) { if (!$seeder) {
foreach (config()->get('rating') as $ratio => $limit) { foreach (tp_config()->get('rating') as $ratio => $limit) {
if ($user_ratio < $ratio) { if ($user_ratio < $ratio) {
config()->set('tracker.limit_active_tor', 1); tp_config()->set('tracker.limit_active_tor', 1);
config()->set('tracker.limit_leech_count', $limit); tp_config()->set('tracker.limit_leech_count', $limit);
$rating_msg = " (ratio < $ratio)"; $rating_msg = " (ratio < $ratio)";
break; break;
} }
@ -313,29 +313,29 @@ if ($lp_info) {
} }
// Limit active torrents // Limit active torrents
if (!isset(config()->get('unlimited_users')[$user_id]) && config()->get('tracker.limit_active_tor') && ((config()->get('tracker.limit_seed_count') && $seeder) || (config()->get('tracker.limit_leech_count') && !$seeder))) { if (!isset(tp_config()->get('unlimited_users')[$user_id]) && tp_config()->get('tracker.limit_active_tor') && ((tp_config()->get('tracker.limit_seed_count') && $seeder) || (tp_config()->get('tracker.limit_leech_count') && !$seeder))) {
$sql = "SELECT COUNT(DISTINCT topic_id) AS active_torrents $sql = "SELECT COUNT(DISTINCT topic_id) AS active_torrents
FROM " . BB_BT_TRACKER . " FROM " . BB_BT_TRACKER . "
WHERE user_id = $user_id WHERE user_id = $user_id
AND seeder = $seeder AND seeder = $seeder
AND topic_id != $topic_id"; AND topic_id != $topic_id";
if (!$seeder && config()->get('tracker.leech_expire_factor') && $user_ratio < 0.5) { if (!$seeder && tp_config()->get('tracker.leech_expire_factor') && $user_ratio < 0.5) {
$sql .= " AND update_time > " . (TIMENOW - 60 * config()->get('tracker.leech_expire_factor')); $sql .= " AND update_time > " . (TIMENOW - 60 * tp_config()->get('tracker.leech_expire_factor'));
} }
$sql .= " GROUP BY user_id"; $sql .= " GROUP BY user_id";
if ($row = DB()->fetch_row($sql)) { if ($row = DB()->fetch_row($sql)) {
if ($seeder && config()->get('tracker.limit_seed_count') && $row['active_torrents'] >= config()->get('tracker.limit_seed_count')) { if ($seeder && tp_config()->get('tracker.limit_seed_count') && $row['active_torrents'] >= tp_config()->get('tracker.limit_seed_count')) {
msg_die('Only ' . config()->get('tracker.limit_seed_count') . ' torrent(s) allowed for seeding'); msg_die('Only ' . tp_config()->get('tracker.limit_seed_count') . ' torrent(s) allowed for seeding');
} elseif (!$seeder && config()->get('tracker.limit_leech_count') && $row['active_torrents'] >= config()->get('tracker.limit_leech_count')) { } elseif (!$seeder && tp_config()->get('tracker.limit_leech_count') && $row['active_torrents'] >= tp_config()->get('tracker.limit_leech_count')) {
msg_die('Only ' . config()->get('tracker.limit_leech_count') . ' torrent(s) allowed for leeching' . $rating_msg); msg_die('Only ' . tp_config()->get('tracker.limit_leech_count') . ' torrent(s) allowed for leeching' . $rating_msg);
} }
} }
} }
// Limit concurrent IPs // Limit concurrent IPs
if (config()->get('tracker.limit_concurrent_ips') && ((config()->get('tracker.limit_seed_ips') && $seeder) || (config()->get('tracker.limit_leech_ips') && !$seeder))) { if (tp_config()->get('tracker.limit_concurrent_ips') && ((tp_config()->get('tracker.limit_seed_ips') && $seeder) || (tp_config()->get('tracker.limit_leech_ips') && !$seeder))) {
$sql = "SELECT COUNT(DISTINCT ip) AS ips $sql = "SELECT COUNT(DISTINCT ip) AS ips
FROM " . BB_BT_TRACKER . " FROM " . BB_BT_TRACKER . "
WHERE topic_id = $topic_id WHERE topic_id = $topic_id
@ -343,16 +343,16 @@ if ($lp_info) {
AND seeder = $seeder AND seeder = $seeder
AND $ip_version != '$ip_sql'"; AND $ip_version != '$ip_sql'";
if (!$seeder && config()->get('tracker.leech_expire_factor')) { if (!$seeder && tp_config()->get('tracker.leech_expire_factor')) {
$sql .= " AND update_time > " . (TIMENOW - 60 * config()->get('tracker.leech_expire_factor')); $sql .= " AND update_time > " . (TIMENOW - 60 * tp_config()->get('tracker.leech_expire_factor'));
} }
$sql .= " GROUP BY topic_id"; $sql .= " GROUP BY topic_id";
if ($row = DB()->fetch_row($sql)) { if ($row = DB()->fetch_row($sql)) {
if ($seeder && config()->get('tracker.limit_seed_ips') && $row['ips'] >= config()->get('tracker.limit_seed_ips')) { if ($seeder && tp_config()->get('tracker.limit_seed_ips') && $row['ips'] >= tp_config()->get('tracker.limit_seed_ips')) {
msg_die('You can seed only from ' . config()->get('tracker.limit_seed_ips') . " IP's"); msg_die('You can seed only from ' . tp_config()->get('tracker.limit_seed_ips') . " IP's");
} elseif (!$seeder && config()->get('tracker.limit_leech_ips') && $row['ips'] >= config()->get('tracker.limit_leech_ips')) { } elseif (!$seeder && tp_config()->get('tracker.limit_leech_ips') && $row['ips'] >= tp_config()->get('tracker.limit_leech_ips')) {
msg_die('You can leech only from ' . config()->get('tracker.limit_leech_ips') . " IP's"); msg_die('You can leech only from ' . tp_config()->get('tracker.limit_leech_ips') . " IP's");
} }
} }
} }
@ -376,7 +376,7 @@ $up_add = ($lp_info && $uploaded > $lp_info['uploaded']) ? $uploaded - $lp_info[
$down_add = ($lp_info && $downloaded > $lp_info['downloaded']) ? $downloaded - $lp_info['downloaded'] : 0; $down_add = ($lp_info && $downloaded > $lp_info['downloaded']) ? $downloaded - $lp_info['downloaded'] : 0;
// Gold/Silver releases // Gold/Silver releases
if (config()->get('tracker.gold_silver_enabled') && $down_add) { if (tp_config()->get('tracker.gold_silver_enabled') && $down_add) {
if ($tor_type == TOR_TYPE_GOLD) { if ($tor_type == TOR_TYPE_GOLD) {
$down_add = 0; $down_add = 0;
} // Silver releases } // Silver releases
@ -386,7 +386,7 @@ if (config()->get('tracker.gold_silver_enabled') && $down_add) {
} }
// Freeleech // Freeleech
if (config()->get('tracker.freeleech') && $down_add) { if (tp_config()->get('tracker.freeleech') && $down_add) {
$down_add = 0; $down_add = 0;
} }
@ -464,8 +464,8 @@ $output = CACHE('tr_cache')->get(PEERS_LIST_PREFIX . $topic_id);
if (!$output) { if (!$output) {
// Retrieve peers // Retrieve peers
$numwant = (int)config()->get('tracker.numwant'); $numwant = (int)tp_config()->get('tracker.numwant');
$compact_mode = (config()->get('tracker.compact_mode') || !empty($compact)); $compact_mode = (tp_config()->get('tracker.compact_mode') || !empty($compact));
$rowset = DB()->fetch_rowset(" $rowset = DB()->fetch_rowset("
SELECT ip, ipv6, port SELECT ip, ipv6, port
@ -510,7 +510,7 @@ if (!$output) {
$seeders = $leechers = $client_completed = 0; $seeders = $leechers = $client_completed = 0;
if (config()->get('tracker.scrape')) { if (tp_config()->get('tracker.scrape')) {
$row = DB()->fetch_row(" $row = DB()->fetch_row("
SELECT seeders, leechers, completed SELECT seeders, leechers, completed
FROM " . BB_BT_TRACKER_SNAP . " FROM " . BB_BT_TRACKER_SNAP . "

View file

@ -12,8 +12,8 @@ if (!defined('IN_TRACKER')) {
} }
// Exit if tracker is disabled // Exit if tracker is disabled
if (config()->get('tracker.bt_off')) { if (tp_config()->get('tracker.bt_off')) {
msg_die(config()->get('tracker.bt_off_reason')); msg_die(tp_config()->get('tracker.bt_off_reason'));
} }
// //

View file

@ -11,7 +11,7 @@ define('IN_TRACKER', true);
define('BB_ROOT', './../'); define('BB_ROOT', './../');
require dirname(__DIR__) . '/common.php'; require dirname(__DIR__) . '/common.php';
if (!config()->get('tracker.scrape')) { if (!tp_config()->get('tracker.scrape')) {
msg_die('Please disable SCRAPE!'); msg_die('Please disable SCRAPE!');
} }
@ -58,8 +58,8 @@ foreach ($info_hash_array[1] as $hash) {
$info_hash_count = count($info_hashes); $info_hash_count = count($info_hashes);
if (!empty($info_hash_count)) { if (!empty($info_hash_count)) {
if ($info_hash_count > config()->get('max_scrapes')) { if ($info_hash_count > tp_config()->get('max_scrapes')) {
$info_hashes = array_slice($info_hashes, 0, config()->get('max_scrapes')); $info_hashes = array_slice($info_hashes, 0, tp_config()->get('max_scrapes'));
} }
$info_hashes_sql = implode('\', \'', $info_hashes); $info_hashes_sql = implode('\', \'', $info_hashes);

View file

@ -1,126 +0,0 @@
# git-cliff ~ TorrentPier configuration file
# https://git-cliff.org/docs/configuration
#
# Lines starting with "#" are comments.
# Configuration options are organized into tables and keys.
# See documentation for more information on available options.
[remote.github]
owner = "torrentpier"
repo = "torrentpier"
[changelog]
# template for the changelog header
header = """
[![TorrentPier](https://raw.githubusercontent.com/torrentpier/.github/refs/heads/main/versions/Cattle.png)](https://github.com/torrentpier)\n
# 📖 Change Log\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
body = """
{%- macro remote_url() -%}
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
{%- endmacro -%}
{%- macro nightly_url() -%}
https://nightly.link/{{ remote.github.owner }}/{{ remote.github.repo }}/workflows/ci/master/TorrentPier-master
{%- endmacro -%}
{% macro print_commit(commit) -%}
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
{% if commit.breaking %}[**breaking**] {% endif %}\
{{ commit.message | upper_first }} - \
([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
{% endmacro -%}
{% if version %}\
{% if previous.version %}\
## [{{ version }}]\
({{ self::remote_url() }}/compare/{{ previous.version }}..{{ version }}) ({{ timestamp | date(format="%Y-%m-%d") }})
{% else %}\
## {{ version }} ({{ timestamp | date(format="%Y-%m-%d") }})
{% endif %}\
{% else %}\
## [nightly]({{ self::nightly_url() }})
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits
| filter(attribute="scope")
| sort(attribute="scope") %}
{{ self::print_commit(commit=commit) }}
{%- endfor %}
{% for commit in commits %}
{%- if not commit.scope -%}
{{ self::print_commit(commit=commit) }}
{% endif -%}
{% endfor -%}
{% endfor -%}
{%- if github -%}
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
## New Contributors ❤️
{% endif %}\
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
* @{{ contributor.username }} made their first contribution
{%- if contributor.pr_number %} in \
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
{%- endif %}
{%- endfor -%}
{%- endif %}
"""
# template for the changelog footer
footer = """
"""
# remove the leading and trailing whitespace from the templates
trim = true
# postprocessors
postprocessors = [
{ pattern = '<REPO>', replace = "https://github.com/torrentpier/torrentpier" }, # replace repository URL
]
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
# Replace issue numbers
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/pull/${2}))" },
# Check spelling of the commit with https://github.com/crate-ci/typos
# If the spelling is incorrect, it will be automatically fixed.
# { pattern = '.*', replace_command = 'typos --write-changes -' },
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
{ message = "^ignore|^release|^changelog", skip = true },
{ message = "^chore|^ci|^misc", group = "<!-- 7 -->⚙️ Miscellaneous" },
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
{ message = "^crowdin|^crodwin", group = "<!-- 10 -->🈳 New translations" }, # crowdin pulls supporting
{ message = "^Composer", group = "<!-- 11 -->📦 Dependencies" }, # dependabot pulls supporting
{ message = "^rem|^drop|^removed", group = "<!-- 12 -->🗑️ Removed" },
{ message = ".*", group = "<!-- 13 -->💼 Other" },
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers
filter_commits = false
# regex for matching git tags
tag_pattern = "v[0-9].*"
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "newest"

View file

@ -34,9 +34,6 @@ if (empty($_SERVER['SERVER_ADDR'])) {
if (!defined('BB_ROOT')) { if (!defined('BB_ROOT')) {
define('BB_ROOT', './'); define('BB_ROOT', './');
} }
if (!defined('BB_SCRIPT')) {
define('BB_SCRIPT', null);
}
header('X-Frame-Options: SAMEORIGIN'); header('X-Frame-Options: SAMEORIGIN');
date_default_timezone_set('UTC'); date_default_timezone_set('UTC');
@ -58,18 +55,6 @@ if (!is_file(BB_PATH . '/vendor/autoload.php')) {
} }
require_once BB_PATH . '/vendor/autoload.php'; require_once BB_PATH . '/vendor/autoload.php';
/**
* Gets the value of an environment variable.
*
* @param string $key
* @param mixed|null $default
* @return mixed
*/
function env(string $key, mixed $default = null): mixed
{
return \TorrentPier\Env::get($key, $default);
}
// Load ENV // Load ENV
try { try {
$dotenv = Dotenv\Dotenv::createMutable(BB_PATH); $dotenv = Dotenv\Dotenv::createMutable(BB_PATH);
@ -95,7 +80,7 @@ $config = \TorrentPier\Config::init($bb_cfg);
* *
* @return \TorrentPier\Config * @return \TorrentPier\Config
*/ */
function config(): \TorrentPier\Config function tp_config(): \TorrentPier\Config
{ {
return \TorrentPier\Config::getInstance(); return \TorrentPier\Config::getInstance();
} }
@ -168,14 +153,14 @@ if (APP_ENV === 'development') {
/** /**
* Server variables initialize * Server variables initialize
*/ */
$server_protocol = config()->get('cookie_secure') ? 'https://' : 'http://'; $server_protocol = tp_config()->get('cookie_secure') ? 'https://' : 'http://';
$server_port = in_array((int)config()->get('server_port'), [80, 443], true) ? '' : ':' . config()->get('server_port'); $server_port = in_array((int)tp_config()->get('server_port'), [80, 443], true) ? '' : ':' . tp_config()->get('server_port');
define('FORUM_PATH', config()->get('script_path')); define('FORUM_PATH', tp_config()->get('script_path'));
define('FULL_URL', $server_protocol . config()->get('server_name') . $server_port . config()->get('script_path')); define('FULL_URL', $server_protocol . tp_config()->get('server_name') . $server_port . tp_config()->get('script_path'));
unset($server_protocol, $server_port); unset($server_protocol, $server_port);
// Initialize the new DB factory with database configuration // Initialize the new DB factory with database configuration
TorrentPier\Database\DatabaseFactory::init(config()->get('db'), config()->get('db_alias', [])); TorrentPier\Database\DatabaseFactory::init(tp_config()->get('db'), tp_config()->get('db_alias', []));
/** /**
* Get the Database instance * Get the Database instance
@ -189,7 +174,7 @@ function DB(string $db_alias = 'db'): \TorrentPier\Database\Database
} }
// Initialize Unified Cache System // Initialize Unified Cache System
TorrentPier\Cache\UnifiedCacheSystem::getInstance(config()->all()); TorrentPier\Cache\UnifiedCacheSystem::getInstance(tp_config()->all());
/** /**
* Get cache manager instance (replaces legacy cache system) * Get cache manager instance (replaces legacy cache system)
@ -209,7 +194,7 @@ function CACHE(string $cache_name): \TorrentPier\Cache\CacheManager
*/ */
function datastore(): \TorrentPier\Cache\DatastoreManager function datastore(): \TorrentPier\Cache\DatastoreManager
{ {
return TorrentPier\Cache\UnifiedCacheSystem::getInstance()->getDatastore(config()->get('datastore_type', 'file')); return TorrentPier\Cache\UnifiedCacheSystem::getInstance()->getDatastore(tp_config()->get('datastore_type', 'file'));
} }
/** /**
@ -433,9 +418,9 @@ if (!defined('IN_TRACKER')) {
} else { } else {
define('DUMMY_PEER', pack('Nn', \TorrentPier\Helpers\IPHelper::ip2long($_SERVER['REMOTE_ADDR']), !empty($_GET['port']) ? (int)$_GET['port'] : random_int(1000, 65000))); define('DUMMY_PEER', pack('Nn', \TorrentPier\Helpers\IPHelper::ip2long($_SERVER['REMOTE_ADDR']), !empty($_GET['port']) ? (int)$_GET['port'] : random_int(1000, 65000)));
define('PEER_HASH_EXPIRE', round(config()->get('announce_interval') * (0.85 * config()->get('tracker.expire_factor')))); define('PEER_HASH_EXPIRE', round(tp_config()->get('announce_interval') * (0.85 * tp_config()->get('tracker.expire_factor'))));
define('PEERS_LIST_EXPIRE', round(config()->get('announce_interval') * 0.7)); define('PEERS_LIST_EXPIRE', round(tp_config()->get('announce_interval') * 0.7));
define('SCRAPE_LIST_EXPIRE', round(config()->get('scrape_interval') * 0.7)); define('SCRAPE_LIST_EXPIRE', round(tp_config()->get('scrape_interval') * 0.7));
define('PEER_HASH_PREFIX', 'peer_'); define('PEER_HASH_PREFIX', 'peer_');
define('PEERS_LIST_PREFIX', 'peers_list_'); define('PEERS_LIST_PREFIX', 'peers_list_');

View file

@ -55,10 +55,20 @@
"bugsnag/bugsnag": "^v3.29.1", "bugsnag/bugsnag": "^v3.29.1",
"claviska/simpleimage": "^4.0", "claviska/simpleimage": "^4.0",
"egulias/email-validator": "^4.0.1", "egulias/email-validator": "^4.0.1",
"fakerphp/faker": "^1.24",
"filp/whoops": "^2.15", "filp/whoops": "^2.15",
"gemorroj/m3u-parser": "^6.0.1", "gemorroj/m3u-parser": "^6.0.1",
"gigablah/sphinxphp": "2.0.8", "gigablah/sphinxphp": "2.0.8",
"google/recaptcha": "^1.3", "google/recaptcha": "^1.3",
"illuminate/collections": "^12.19",
"illuminate/config": "^12.19",
"illuminate/container": "^12.19",
"illuminate/database": "^12.19",
"illuminate/events": "^12.19",
"illuminate/http": "^12.19",
"illuminate/routing": "^12.19",
"illuminate/support": "^12.19",
"illuminate/validation": "^12.19",
"jacklul/monolog-telegram": "^3.1", "jacklul/monolog-telegram": "^3.1",
"josantonius/cookie": "^2.0", "josantonius/cookie": "^2.0",
"league/flysystem": "^3.28", "league/flysystem": "^3.28",
@ -67,9 +77,9 @@
"nette/caching": "^3.3", "nette/caching": "^3.3",
"nette/database": "^3.2", "nette/database": "^3.2",
"php-curl-class/php-curl-class": "^12.0.0", "php-curl-class/php-curl-class": "^12.0.0",
"php-di/php-di": "^7.0",
"robmorgan/phinx": "^0.16.9", "robmorgan/phinx": "^0.16.9",
"samdark/sitemap": "2.4.1", "samdark/sitemap": "2.4.1",
"symfony/console": "^7.3",
"symfony/mailer": "^7.3", "symfony/mailer": "^7.3",
"symfony/polyfill": "v1.32.0", "symfony/polyfill": "v1.32.0",
"vlucas/phpdotenv": "^5.5", "vlucas/phpdotenv": "^5.5",
@ -82,10 +92,11 @@
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"TorrentPier\\": "src/" "TorrentPier\\": "src/",
"App\\": "app/"
}, },
"files": [ "files": [
"src/helpers.php" "app/helpers.php"
] ]
}, },
"autoload-dev": { "autoload-dev": {

2477
composer.lock generated

File diff suppressed because it is too large Load diff

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'),
],
];

102
config/routes.php Normal file
View file

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

View file

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

@ -10,7 +10,10 @@
define('BB_SCRIPT', 'ajax'); define('BB_SCRIPT', 'ajax');
define('IN_AJAX', true); define('IN_AJAX', true);
require __DIR__ . '/common.php'; // Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
// Init Ajax class // Init Ajax class
$ajax = new TorrentPier\Ajax(); $ajax = new TorrentPier\Ajax();

View file

@ -10,7 +10,11 @@
define('BB_SCRIPT', 'dl'); define('BB_SCRIPT', 'dl');
define('NO_GZIP', true); define('NO_GZIP', true);
require __DIR__ . '/common.php'; // Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
require ATTACH_DIR . '/attachment_mod.php'; require ATTACH_DIR . '/attachment_mod.php';
$datastore->enqueue([ $datastore->enqueue([
@ -170,7 +174,7 @@ if (!IS_AM && ($attachment['mimetype'] === TORRENT_MIMETYPE)) {
$row = DB()->sql_fetchrow($result); $row = DB()->sql_fetchrow($result);
if (isset(config()->get('tor_frozen')[$row['tor_status']]) && !(isset(config()->get('tor_frozen_author_download')[$row['tor_status']]) && $userdata['user_id'] === $row['poster_id'])) { if (isset(tp_config()->get('tor_frozen')[$row['tor_status']]) && !(isset(tp_config()->get('tor_frozen_author_download')[$row['tor_status']]) && $userdata['user_id'] === $row['poster_id'])) {
bb_die($lang['TOR_STATUS_FORBIDDEN'] . $lang['TOR_STATUS_NAME'][$row['tor_status']]); bb_die($lang['TOR_STATUS_FORBIDDEN'] . $lang['TOR_STATUS_NAME'][$row['tor_status']]);
} }
@ -219,7 +223,7 @@ switch ($download_mode) {
header('Location: ' . $url); header('Location: ' . $url);
exit; exit;
case INLINE_LINK: case INLINE_LINK:
if (IS_GUEST && !config()->get('captcha.disabled') && !bb_captcha('check')) { if (IS_GUEST && !tp_config()->get('captcha.disabled') && !bb_captcha('check')) {
global $template; global $template;
$redirect_url = $_POST['redirect_url'] ?? $_SERVER['HTTP_REFERER'] ?? '/'; $redirect_url = $_POST['redirect_url'] ?? $_SERVER['HTTP_REFERER'] ?? '/';

View file

@ -9,7 +9,10 @@
define('BB_SCRIPT', 'dl_list'); define('BB_SCRIPT', 'dl_list');
require __DIR__ . '/common.php'; // Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
$forum_id = $_REQUEST[POST_FORUM_URL] ?? 0; $forum_id = $_REQUEST[POST_FORUM_URL] ?? 0;
$topic_id = $_REQUEST[POST_TOPIC_URL] ?? 0; $topic_id = $_REQUEST[POST_TOPIC_URL] ?? 0;

View file

@ -9,7 +9,10 @@
define('BB_SCRIPT', 'feed'); define('BB_SCRIPT', 'feed');
require __DIR__ . '/common.php'; // Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
// Init userdata // Init userdata
$user->session_start(['req_login' => true]); $user->session_start(['req_login' => true]);
@ -34,11 +37,11 @@ if ($mode === 'get_feed_url' && ($type === 'f' || $type === 'u') && $id >= 0) {
bb_simple_die($lang['ATOM_ERROR'] . ' #1'); bb_simple_die($lang['ATOM_ERROR'] . ' #1');
} }
} }
if (is_file(config()->get('atom.path') . '/f/' . $id . '.atom') && filemtime(config()->get('atom.path') . '/f/' . $id . '.atom') > $timecheck) { if (is_file(tp_config()->get('atom.path') . '/f/' . $id . '.atom') && filemtime(tp_config()->get('atom.path') . '/f/' . $id . '.atom') > $timecheck) {
redirect(config()->get('atom.url') . '/f/' . $id . '.atom'); redirect(tp_config()->get('atom.url') . '/f/' . $id . '.atom');
} else { } else {
if (\TorrentPier\Legacy\Atom::update_forum_feed($id, $forum_data)) { if (\TorrentPier\Legacy\Atom::update_forum_feed($id, $forum_data)) {
redirect(config()->get('atom.url') . '/f/' . $id . '.atom'); redirect(tp_config()->get('atom.url') . '/f/' . $id . '.atom');
} else { } else {
bb_simple_die($lang['ATOM_NO_FORUM']); bb_simple_die($lang['ATOM_NO_FORUM']);
} }
@ -52,11 +55,11 @@ if ($mode === 'get_feed_url' && ($type === 'f' || $type === 'u') && $id >= 0) {
if (!$username = get_username($id)) { if (!$username = get_username($id)) {
bb_simple_die($lang['ATOM_ERROR'] . ' #3'); bb_simple_die($lang['ATOM_ERROR'] . ' #3');
} }
if (is_file(config()->get('atom.path') . '/u/' . floor($id / 5000) . '/' . ($id % 100) . '/' . $id . '.atom') && filemtime(config()->get('atom.path') . '/u/' . floor($id / 5000) . '/' . ($id % 100) . '/' . $id . '.atom') > $timecheck) { if (is_file(tp_config()->get('atom.path') . '/u/' . floor($id / 5000) . '/' . ($id % 100) . '/' . $id . '.atom') && filemtime(tp_config()->get('atom.path') . '/u/' . floor($id / 5000) . '/' . ($id % 100) . '/' . $id . '.atom') > $timecheck) {
redirect(config()->get('atom.url') . '/u/' . floor($id / 5000) . '/' . ($id % 100) . '/' . $id . '.atom'); redirect(tp_config()->get('atom.url') . '/u/' . floor($id / 5000) . '/' . ($id % 100) . '/' . $id . '.atom');
} else { } else {
if (\TorrentPier\Legacy\Atom::update_user_feed($id, $username)) { if (\TorrentPier\Legacy\Atom::update_user_feed($id, $username)) {
redirect(config()->get('atom.url') . '/u/' . floor($id / 5000) . '/' . ($id % 100) . '/' . $id . '.atom'); redirect(tp_config()->get('atom.url') . '/u/' . floor($id / 5000) . '/' . ($id % 100) . '/' . $id . '.atom');
} else { } else {
bb_simple_die($lang['ATOM_NO_USER']); bb_simple_die($lang['ATOM_NO_USER']);
} }

View file

@ -9,12 +9,15 @@
define('BB_SCRIPT', 'filelist'); define('BB_SCRIPT', 'filelist');
require __DIR__ . '/common.php'; // Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
// Start session management // Start session management
$user->session_start(); $user->session_start();
if (config()->get('bt_disable_dht') && IS_GUEST) { if (tp_config()->get('bt_disable_dht') && IS_GUEST) {
bb_die($lang['BT_PRIVATE_TRACKER'], 403); bb_die($lang['BT_PRIVATE_TRACKER'], 403);
} }
@ -55,7 +58,7 @@ if (!is_file($file_path)) {
} }
$file_contents = file_get_contents($file_path); $file_contents = file_get_contents($file_path);
if (config()->get('flist_max_files')) { if (tp_config()->get('flist_max_files')) {
$filetree_pos = $meta_v2 ? strpos($file_contents, '9:file tree') : false; $filetree_pos = $meta_v2 ? strpos($file_contents, '9:file tree') : false;
$files_pos = $meta_v1 ? strpos($file_contents, '5:files', $filetree_pos) : false; $files_pos = $meta_v1 ? strpos($file_contents, '5:files', $filetree_pos) : false;
@ -65,8 +68,8 @@ if (config()->get('flist_max_files')) {
$file_count = substr_count($file_contents, '6:length', $files_pos); $file_count = substr_count($file_contents, '6:length', $files_pos);
} }
if ($file_count > config()->get('flist_max_files')) { if ($file_count > tp_config()->get('flist_max_files')) {
bb_die(sprintf($lang['BT_FLIST_LIMIT'], config()->get('flist_max_files'), $file_count), 410); bb_die(sprintf($lang['BT_FLIST_LIMIT'], tp_config()->get('flist_max_files'), $file_count), 410);
} }
} }

View file

@ -9,7 +9,11 @@
define('BB_SCRIPT', 'group'); define('BB_SCRIPT', 'group');
require __DIR__ . '/common.php'; // Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
require INC_DIR . '/bbcode.php'; require INC_DIR . '/bbcode.php';
$page_cfg['use_tablesorter'] = true; $page_cfg['use_tablesorter'] = true;
@ -24,7 +28,7 @@ set_die_append_msg();
$group_id = isset($_REQUEST[POST_GROUPS_URL]) ? (int)$_REQUEST[POST_GROUPS_URL] : null; $group_id = isset($_REQUEST[POST_GROUPS_URL]) ? (int)$_REQUEST[POST_GROUPS_URL] : null;
$start = isset($_REQUEST['start']) ? abs((int)$_REQUEST['start']) : 0; $start = isset($_REQUEST['start']) ? abs((int)$_REQUEST['start']) : 0;
$per_page = config()->get('group_members_per_page'); $per_page = tp_config()->get('group_members_per_page');
$view_mode = isset($_REQUEST['view']) ? (string)$_REQUEST['view'] : null; $view_mode = isset($_REQUEST['view']) ? (string)$_REQUEST['view'] : null;
$rel_limit = 50; $rel_limit = 50;
@ -168,7 +172,7 @@ if (!$group_id) {
\TorrentPier\Legacy\Group::add_user_into_group($group_id, $userdata['user_id'], 1, TIMENOW); \TorrentPier\Legacy\Group::add_user_into_group($group_id, $userdata['user_id'], 1, TIMENOW);
if (config()->get('group_send_email')) { if (tp_config()->get('group_send_email')) {
// Sending email // Sending email
$emailer = new TorrentPier\Emailer(); $emailer = new TorrentPier\Emailer();
@ -224,7 +228,7 @@ if (!$group_id) {
\TorrentPier\Legacy\Group::add_user_into_group($group_id, $row['user_id']); \TorrentPier\Legacy\Group::add_user_into_group($group_id, $row['user_id']);
if (config()->get('group_send_email')) { if (tp_config()->get('group_send_email')) {
// Sending email // Sending email
$emailer = new TorrentPier\Emailer(); $emailer = new TorrentPier\Emailer();
@ -273,7 +277,7 @@ if (!$group_id) {
} }
} }
// Email users when they are approved // Email users when they are approved
if (!empty($_POST['approve']) && config()->get('group_send_email')) { if (!empty($_POST['approve']) && tp_config()->get('group_send_email')) {
$sql_select = "SELECT username, user_email, user_lang $sql_select = "SELECT username, user_email, user_lang
FROM " . BB_USERS . " FROM " . BB_USERS . "
WHERE user_id IN($sql_in)"; WHERE user_id IN($sql_in)";

View file

@ -9,7 +9,10 @@
define('BB_SCRIPT', 'group_edit'); define('BB_SCRIPT', 'group_edit');
require __DIR__ . '/common.php'; // Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
$page_cfg['include_bbcode_js'] = true; $page_cfg['include_bbcode_js'] = true;
@ -35,10 +38,10 @@ if ($group_id) {
if ($is_moderator) { if ($is_moderator) {
// Avatar // Avatar
if ($submit) { if ($submit) {
if (!empty($_FILES['avatar']['name']) && config()->get('group_avatars.up_allowed')) { if (!empty($_FILES['avatar']['name']) && tp_config()->get('group_avatars.up_allowed')) {
$upload = new TorrentPier\Legacy\Common\Upload(); $upload = new TorrentPier\Legacy\Common\Upload();
if ($upload->init(config()->get('group_avatars'), $_FILES['avatar']) and $upload->store('avatar', ['user_id' => GROUP_AVATAR_MASK . $group_id, 'avatar_ext_id' => $group_info['avatar_ext_id']])) { if ($upload->init(tp_config()->get('group_avatars'), $_FILES['avatar']) and $upload->store('avatar', ['user_id' => GROUP_AVATAR_MASK . $group_id, 'avatar_ext_id' => $group_info['avatar_ext_id']])) {
$avatar_ext_id = (int)$upload->file_ext_id; $avatar_ext_id = (int)$upload->file_ext_id;
DB()->query("UPDATE " . BB_GROUPS . " SET avatar_ext_id = $avatar_ext_id WHERE group_id = $group_id LIMIT 1"); DB()->query("UPDATE " . BB_GROUPS . " SET avatar_ext_id = $avatar_ext_id WHERE group_id = $group_id LIMIT 1");
} else { } else {
@ -76,7 +79,7 @@ if ($is_moderator) {
'S_HIDDEN_FIELDS' => $s_hidden_fields, 'S_HIDDEN_FIELDS' => $s_hidden_fields,
'S_GROUP_CONFIG_ACTION' => "group_edit.php?" . POST_GROUPS_URL . "=$group_id", 'S_GROUP_CONFIG_ACTION' => "group_edit.php?" . POST_GROUPS_URL . "=$group_id",
'AVATAR_EXPLAIN' => sprintf($lang['AVATAR_EXPLAIN'], config()->get('group_avatars.max_width'), config()->get('group_avatars.max_height'), humn_size(config()->get('group_avatars.max_size'))), 'AVATAR_EXPLAIN' => sprintf($lang['AVATAR_EXPLAIN'], tp_config()->get('group_avatars.max_width'), tp_config()->get('group_avatars.max_height'), humn_size(tp_config()->get('group_avatars.max_size'))),
'AVATAR_IMG' => get_avatar(GROUP_AVATAR_MASK . $group_id, $group_info['avatar_ext_id']), 'AVATAR_IMG' => get_avatar(GROUP_AVATAR_MASK . $group_id, $group_info['avatar_ext_id']),
]); ]);

View file

@ -9,7 +9,10 @@
define('BB_SCRIPT', 'index'); define('BB_SCRIPT', 'index');
require __DIR__ . '/common.php'; // Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
$page_cfg['load_tpl_vars'] = [ $page_cfg['load_tpl_vars'] = [
'post_icons' 'post_icons'
@ -31,12 +34,12 @@ $datastore->enqueue([
'cat_forums' 'cat_forums'
]); ]);
if (config()->get('show_latest_news')) { if (tp_config()->get('show_latest_news')) {
$datastore->enqueue([ $datastore->enqueue([
'latest_news' 'latest_news'
]); ]);
} }
if (config()->get('show_network_news')) { if (tp_config()->get('show_network_news')) {
$datastore->enqueue([ $datastore->enqueue([
'network_news' 'network_news'
]); ]);
@ -46,7 +49,7 @@ if (config()->get('show_network_news')) {
$user->session_start(); $user->session_start();
// Set meta description // Set meta description
$page_cfg['meta_description'] = config()->get('site_desc'); $page_cfg['meta_description'] = tp_config()->get('site_desc');
// Init main vars // Init main vars
$viewcat = isset($_GET[POST_CAT_URL]) ? (int)$_GET[POST_CAT_URL] : 0; $viewcat = isset($_GET[POST_CAT_URL]) ? (int)$_GET[POST_CAT_URL] : 0;
@ -57,7 +60,7 @@ $req_page = 'index_page';
$req_page .= $viewcat ? "_c{$viewcat}" : ''; $req_page .= $viewcat ? "_c{$viewcat}" : '';
define('REQUESTED_PAGE', $req_page); define('REQUESTED_PAGE', $req_page);
caching_output(IS_GUEST, 'send', REQUESTED_PAGE . '_guest_' . config()->get('default_lang')); caching_output(IS_GUEST, 'send', REQUESTED_PAGE . '_guest_' . tp_config()->get('default_lang'));
$hide_cat_opt = isset($user->opt_js['h_cat']) ? (string)$user->opt_js['h_cat'] : 0; $hide_cat_opt = isset($user->opt_js['h_cat']) ? (string)$user->opt_js['h_cat'] : 0;
$hide_cat_user = array_flip(explode('-', $hide_cat_opt)); $hide_cat_user = array_flip(explode('-', $hide_cat_opt));
@ -262,7 +265,7 @@ foreach ($cat_forums as $cid => $c) {
'LAST_TOPIC_ID' => $f['last_topic_id'], 'LAST_TOPIC_ID' => $f['last_topic_id'],
'LAST_TOPIC_TIP' => $f['last_topic_title'], 'LAST_TOPIC_TIP' => $f['last_topic_title'],
'LAST_TOPIC_TITLE' => str_short($f['last_topic_title'], $last_topic_max_len), 'LAST_TOPIC_TITLE' => str_short($f['last_topic_title'], $last_topic_max_len),
'LAST_POST_TIME' => bb_date($f['last_post_time'], config()->get('last_post_date_format')), 'LAST_POST_TIME' => bb_date($f['last_post_time'], tp_config()->get('last_post_date_format')),
'LAST_POST_USER' => profile_url(['username' => str_short($f['last_post_username'], 15), 'user_id' => $f['last_post_user_id'], 'user_rank' => $f['last_post_user_rank']]), 'LAST_POST_USER' => profile_url(['username' => str_short($f['last_post_username'], 15), 'user_id' => $f['last_post_user_id'], 'user_rank' => $f['last_post_user_rank']]),
]); ]);
} }
@ -278,7 +281,7 @@ $template->assign_vars([
'TOTAL_TOPICS' => sprintf($lang['POSTED_TOPICS_TOTAL'], $stats['topiccount']), 'TOTAL_TOPICS' => sprintf($lang['POSTED_TOPICS_TOTAL'], $stats['topiccount']),
'TOTAL_POSTS' => sprintf($lang['POSTED_ARTICLES_TOTAL'], $stats['postcount']), 'TOTAL_POSTS' => sprintf($lang['POSTED_ARTICLES_TOTAL'], $stats['postcount']),
'TOTAL_USERS' => sprintf($lang['REGISTERED_USERS_TOTAL'], $stats['usercount']), 'TOTAL_USERS' => sprintf($lang['REGISTERED_USERS_TOTAL'], $stats['usercount']),
'TOTAL_GENDER' => config()->get('gender') ? sprintf( 'TOTAL_GENDER' => tp_config()->get('gender') ? sprintf(
$lang['USERS_TOTAL_GENDER'], $lang['USERS_TOTAL_GENDER'],
$stats['male'], $stats['male'],
$stats['female'], $stats['female'],
@ -287,22 +290,22 @@ $template->assign_vars([
'NEWEST_USER' => sprintf($lang['NEWEST_USER'], profile_url($stats['newestuser'])), 'NEWEST_USER' => sprintf($lang['NEWEST_USER'], profile_url($stats['newestuser'])),
// Tracker stats // Tracker stats
'TORRENTS_STAT' => config()->get('tor_stats') ? sprintf( 'TORRENTS_STAT' => tp_config()->get('tor_stats') ? sprintf(
$lang['TORRENTS_STAT'], $lang['TORRENTS_STAT'],
$stats['torrentcount'], $stats['torrentcount'],
humn_size($stats['size']) humn_size($stats['size'])
) : '', ) : '',
'PEERS_STAT' => config()->get('tor_stats') ? sprintf( 'PEERS_STAT' => tp_config()->get('tor_stats') ? sprintf(
$lang['PEERS_STAT'], $lang['PEERS_STAT'],
$stats['peers'], $stats['peers'],
$stats['seeders'], $stats['seeders'],
$stats['leechers'] $stats['leechers']
) : '', ) : '',
'SPEED_STAT' => config()->get('tor_stats') ? sprintf( 'SPEED_STAT' => tp_config()->get('tor_stats') ? sprintf(
$lang['SPEED_STAT'], $lang['SPEED_STAT'],
humn_size($stats['speed']) . '/s' humn_size($stats['speed']) . '/s'
) : '', ) : '',
'SHOW_MOD_INDEX' => config()->get('show_mod_index'), 'SHOW_MOD_INDEX' => tp_config()->get('show_mod_index'),
'FORUM_IMG' => $images['forum'], 'FORUM_IMG' => $images['forum'],
'FORUM_NEW_IMG' => $images['forum_new'], 'FORUM_NEW_IMG' => $images['forum_new'],
'FORUM_LOCKED_IMG' => $images['forum_locked'], 'FORUM_LOCKED_IMG' => $images['forum_locked'],
@ -315,19 +318,19 @@ $template->assign_vars([
'U_SEARCH_SELF_BY_MY' => "search.php?uid={$userdata['user_id']}&amp;o=1", 'U_SEARCH_SELF_BY_MY' => "search.php?uid={$userdata['user_id']}&amp;o=1",
'U_SEARCH_LATEST' => 'search.php?search_id=latest', 'U_SEARCH_LATEST' => 'search.php?search_id=latest',
'U_SEARCH_UNANSWERED' => 'search.php?search_id=unanswered', 'U_SEARCH_UNANSWERED' => 'search.php?search_id=unanswered',
'U_ATOM_FEED' => is_file(config()->get('atom.path') . '/f/0.atom') ? make_url(config()->get('atom.url') . '/f/0.atom') : false, 'U_ATOM_FEED' => is_file(tp_config()->get('atom.path') . '/f/0.atom') ? make_url(tp_config()->get('atom.url') . '/f/0.atom') : false,
'SHOW_LAST_TOPIC' => $show_last_topic, 'SHOW_LAST_TOPIC' => $show_last_topic,
'BOARD_START' => config()->get('show_board_start_index') ? ($lang['BOARD_STARTED'] . ':&nbsp;' . '<b>' . bb_date(config()->get('board_startdate')) . '</b>') : false, 'BOARD_START' => tp_config()->get('show_board_start_index') ? ($lang['BOARD_STARTED'] . ':&nbsp;' . '<b>' . bb_date(tp_config()->get('board_startdate')) . '</b>') : false,
]); ]);
// Set tpl vars for bt_userdata // Set tpl vars for bt_userdata
if (config()->get('bt_show_dl_stat_on_index') && !IS_GUEST) { if (tp_config()->get('bt_show_dl_stat_on_index') && !IS_GUEST) {
show_bt_userdata($userdata['user_id']); show_bt_userdata($userdata['user_id']);
} }
// Latest news // Latest news
if (config()->get('show_latest_news')) { if (tp_config()->get('show_latest_news')) {
$latest_news = $datastore->get('latest_news'); $latest_news = $datastore->get('latest_news');
if ($latest_news === false) { if ($latest_news === false) {
$datastore->update('latest_news'); $datastore->update('latest_news');
@ -343,7 +346,7 @@ if (config()->get('show_latest_news')) {
$template->assign_block_vars('news', [ $template->assign_block_vars('news', [
'NEWS_TOPIC_ID' => $news['topic_id'], 'NEWS_TOPIC_ID' => $news['topic_id'],
'NEWS_TITLE' => str_short(censor()->censorString($news['topic_title']), config()->get('max_news_title')), 'NEWS_TITLE' => str_short(censor()->censorString($news['topic_title']), tp_config()->get('max_news_title')),
'NEWS_TIME' => bb_date($news['topic_time'], 'd-M', false), 'NEWS_TIME' => bb_date($news['topic_time'], 'd-M', false),
'NEWS_IS_NEW' => is_unread($news['topic_time'], $news['topic_id'], $news['forum_id']), 'NEWS_IS_NEW' => is_unread($news['topic_time'], $news['topic_id'], $news['forum_id']),
]); ]);
@ -351,7 +354,7 @@ if (config()->get('show_latest_news')) {
} }
// Network news // Network news
if (config()->get('show_network_news')) { if (tp_config()->get('show_network_news')) {
$network_news = $datastore->get('network_news'); $network_news = $datastore->get('network_news');
if ($network_news === false) { if ($network_news === false) {
$datastore->update('network_news'); $datastore->update('network_news');
@ -367,14 +370,14 @@ if (config()->get('show_network_news')) {
$template->assign_block_vars('net', [ $template->assign_block_vars('net', [
'NEWS_TOPIC_ID' => $net['topic_id'], 'NEWS_TOPIC_ID' => $net['topic_id'],
'NEWS_TITLE' => str_short(censor()->censorString($net['topic_title']), config()->get('max_net_title')), 'NEWS_TITLE' => str_short(censor()->censorString($net['topic_title']), tp_config()->get('max_net_title')),
'NEWS_TIME' => bb_date($net['topic_time'], 'd-M', false), 'NEWS_TIME' => bb_date($net['topic_time'], 'd-M', false),
'NEWS_IS_NEW' => is_unread($net['topic_time'], $net['topic_id'], $net['forum_id']), 'NEWS_IS_NEW' => is_unread($net['topic_time'], $net['topic_id'], $net['forum_id']),
]); ]);
} }
} }
if (config()->get('birthday_check_day') && config()->get('birthday_enabled')) { if (tp_config()->get('birthday_check_day') && tp_config()->get('birthday_enabled')) {
$week_list = $today_list = []; $week_list = $today_list = [];
$week_all = $today_all = false; $week_all = $today_all = false;
@ -388,9 +391,9 @@ if (config()->get('birthday_check_day') && config()->get('birthday_enabled')) {
$week_list[] = profile_url($week) . ' <span class="small">(' . birthday_age(date('Y-m-d', strtotime('-1 year', strtotime($week['user_birthday'])))) . ')</span>'; $week_list[] = profile_url($week) . ' <span class="small">(' . birthday_age(date('Y-m-d', strtotime('-1 year', strtotime($week['user_birthday'])))) . ')</span>';
} }
$week_all = $week_all ? '&nbsp;<a class="txtb" href="#" onclick="ajax.exec({action: \'index_data\', mode: \'birthday_week\'}); return false;" title="' . $lang['ALL'] . '">...</a>' : ''; $week_all = $week_all ? '&nbsp;<a class="txtb" href="#" onclick="ajax.exec({action: \'index_data\', mode: \'birthday_week\'}); return false;" title="' . $lang['ALL'] . '">...</a>' : '';
$week_list = sprintf($lang['BIRTHDAY_WEEK'], config()->get('birthday_check_day'), implode(', ', $week_list)) . $week_all; $week_list = sprintf($lang['BIRTHDAY_WEEK'], tp_config()->get('birthday_check_day'), implode(', ', $week_list)) . $week_all;
} else { } else {
$week_list = sprintf($lang['NOBIRTHDAY_WEEK'], config()->get('birthday_check_day')); $week_list = sprintf($lang['NOBIRTHDAY_WEEK'], tp_config()->get('birthday_check_day'));
} }
if (!empty($stats['birthday_today_list'])) { if (!empty($stats['birthday_today_list'])) {

View file

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

View file

@ -10,7 +10,10 @@
define('BB_SCRIPT', 'login'); define('BB_SCRIPT', 'login');
define('IN_LOGIN', true); define('IN_LOGIN', true);
require __DIR__ . '/common.php'; // Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
array_deep($_POST, 'trim'); array_deep($_POST, 'trim');
@ -63,7 +66,7 @@ $login_password = $_POST['login_password'] ?? '';
$need_captcha = false; $need_captcha = false;
if (!$mod_admin_login) { if (!$mod_admin_login) {
$need_captcha = CACHE('bb_login_err')->get('l_err_' . USER_IP); $need_captcha = CACHE('bb_login_err')->get('l_err_' . USER_IP);
if ($need_captcha < config()->get('invalid_logins')) { if ($need_captcha < tp_config()->get('invalid_logins')) {
$need_captcha = false; $need_captcha = false;
} }
} }
@ -80,13 +83,13 @@ if (isset($_POST['login'])) {
} }
// Captcha // Captcha
if ($need_captcha && !config()->get('captcha.disabled') && !bb_captcha('check')) { if ($need_captcha && !tp_config()->get('captcha.disabled') && !bb_captcha('check')) {
$login_errors[] = $lang['CAPTCHA_WRONG']; $login_errors[] = $lang['CAPTCHA_WRONG'];
} }
if (!$login_errors) { if (!$login_errors) {
if ($user->login($_POST, $mod_admin_login)) { if ($user->login($_POST, $mod_admin_login)) {
$redirect_url = (defined('FIRST_LOGON')) ? config()->get('first_logon_redirect_url') : $redirect_url; $redirect_url = (defined('FIRST_LOGON')) ? tp_config()->get('first_logon_redirect_url') : $redirect_url;
// Reset when entering the correct login/password combination // Reset when entering the correct login/password combination
CACHE('bb_login_err')->rm('l_err_' . USER_IP); CACHE('bb_login_err')->rm('l_err_' . USER_IP);
@ -101,7 +104,7 @@ if (isset($_POST['login'])) {
if (!$mod_admin_login) { if (!$mod_admin_login) {
$login_err = CACHE('bb_login_err')->get('l_err_' . USER_IP); $login_err = CACHE('bb_login_err')->get('l_err_' . USER_IP);
if ($login_err > config()->get('invalid_logins')) { if ($login_err > tp_config()->get('invalid_logins')) {
$need_captcha = true; $need_captcha = true;
} }
CACHE('bb_login_err')->set('l_err_' . USER_IP, ($login_err + 1), 3600); CACHE('bb_login_err')->set('l_err_' . USER_IP, ($login_err + 1), 3600);
@ -118,7 +121,7 @@ if (IS_GUEST || $mod_admin_login) {
'ERROR_MESSAGE' => implode('<br />', $login_errors), 'ERROR_MESSAGE' => implode('<br />', $login_errors),
'ADMIN_LOGIN' => $mod_admin_login, 'ADMIN_LOGIN' => $mod_admin_login,
'REDIRECT_URL' => htmlCHR($redirect_url), 'REDIRECT_URL' => htmlCHR($redirect_url),
'CAPTCHA_HTML' => ($need_captcha && !config()->get('captcha.disabled')) ? bb_captcha('get') : '', 'CAPTCHA_HTML' => ($need_captcha && !tp_config()->get('captcha.disabled')) ? bb_captcha('get') : '',
'PAGE_TITLE' => $lang['LOGIN'], 'PAGE_TITLE' => $lang['LOGIN'],
'S_LOGIN_ACTION' => LOGIN_URL 'S_LOGIN_ACTION' => LOGIN_URL
]); ]);

View file

@ -9,7 +9,10 @@
define('BB_SCRIPT', 'memberlist'); define('BB_SCRIPT', 'memberlist');
require __DIR__ . '/common.php'; // Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
$user->session_start(['req_login' => true]); $user->session_start(['req_login' => true]);
@ -54,26 +57,26 @@ $select_sort_role .= '</select>';
switch ($mode) { switch ($mode) {
case 'username': case 'username':
$order_by = "username $sort_order LIMIT $start, " . config()->get('topics_per_page'); $order_by = "username $sort_order LIMIT $start, " . tp_config()->get('topics_per_page');
break; break;
case 'location': case 'location':
$order_by = "user_from $sort_order LIMIT $start, " . config()->get('topics_per_page'); $order_by = "user_from $sort_order LIMIT $start, " . tp_config()->get('topics_per_page');
break; break;
case 'posts': case 'posts':
$order_by = "user_posts $sort_order LIMIT $start, " . config()->get('topics_per_page'); $order_by = "user_posts $sort_order LIMIT $start, " . tp_config()->get('topics_per_page');
break; break;
case 'email': case 'email':
$order_by = "user_email $sort_order LIMIT $start, " . config()->get('topics_per_page'); $order_by = "user_email $sort_order LIMIT $start, " . tp_config()->get('topics_per_page');
break; break;
case 'website': case 'website':
$order_by = "user_website $sort_order LIMIT $start, " . config()->get('topics_per_page'); $order_by = "user_website $sort_order LIMIT $start, " . tp_config()->get('topics_per_page');
break; break;
case 'topten': case 'topten':
$order_by = "user_posts $sort_order LIMIT 10"; $order_by = "user_posts $sort_order LIMIT 10";
break; break;
case 'joined': case 'joined':
default: default:
$order_by = "user_regdate $sort_order LIMIT $start, " . config()->get('topics_per_page'); $order_by = "user_regdate $sort_order LIMIT $start, " . tp_config()->get('topics_per_page');
break; break;
} }
@ -134,7 +137,7 @@ if ($mode != 'topten') {
} }
if ($total = DB()->sql_fetchrow($result)) { if ($total = DB()->sql_fetchrow($result)) {
$total_members = $total['total']; $total_members = $total['total'];
generate_pagination($paginationurl, $total_members, config()->get('topics_per_page'), $start); generate_pagination($paginationurl, $total_members, tp_config()->get('topics_per_page'), $start);
} }
DB()->sql_freeresult($result); DB()->sql_freeresult($result);
} }

View file

@ -9,7 +9,11 @@
define('BB_SCRIPT', 'modcp'); define('BB_SCRIPT', 'modcp');
require __DIR__ . '/common.php'; // Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
require INC_DIR . '/bbcode.php'; require INC_DIR . '/bbcode.php';
// //
@ -223,16 +227,16 @@ switch ($mode) {
$result = \TorrentPier\Legacy\Admin\Common::topic_delete($req_topics, $forum_id); $result = \TorrentPier\Legacy\Admin\Common::topic_delete($req_topics, $forum_id);
//Обновление кеша новостей на главной //Обновление кеша новостей на главной
$news_forums = array_flip(explode(',', config()->get('latest_news_forum_id'))); $news_forums = array_flip(explode(',', tp_config()->get('latest_news_forum_id')));
if (isset($news_forums[$forum_id]) && config()->get('show_latest_news') && $result) { if (isset($news_forums[$forum_id]) && tp_config()->get('show_latest_news') && $result) {
$datastore->enqueue([ $datastore->enqueue([
'latest_news' 'latest_news'
]); ]);
$datastore->update('latest_news'); $datastore->update('latest_news');
} }
$net_forums = array_flip(explode(',', config()->get('network_news_forum_id'))); $net_forums = array_flip(explode(',', tp_config()->get('network_news_forum_id')));
if (isset($net_forums[$forum_id]) && config()->get('show_network_news') && $result) { if (isset($net_forums[$forum_id]) && tp_config()->get('show_network_news') && $result) {
$datastore->enqueue([ $datastore->enqueue([
'network_news' 'network_news'
]); ]);
@ -258,16 +262,16 @@ switch ($mode) {
$result = \TorrentPier\Legacy\Admin\Common::topic_move($req_topics, $new_forum_id, $forum_id, isset($_POST['move_leave_shadow']), isset($_POST['insert_bot_msg']), $_POST['reason_move_bot']); $result = \TorrentPier\Legacy\Admin\Common::topic_move($req_topics, $new_forum_id, $forum_id, isset($_POST['move_leave_shadow']), isset($_POST['insert_bot_msg']), $_POST['reason_move_bot']);
//Обновление кеша новостей на главной //Обновление кеша новостей на главной
$news_forums = array_flip(explode(',', config()->get('latest_news_forum_id'))); $news_forums = array_flip(explode(',', tp_config()->get('latest_news_forum_id')));
if ((isset($news_forums[$forum_id]) || isset($news_forums[$new_forum_id])) && config()->get('show_latest_news') && $result) { if ((isset($news_forums[$forum_id]) || isset($news_forums[$new_forum_id])) && tp_config()->get('show_latest_news') && $result) {
$datastore->enqueue([ $datastore->enqueue([
'latest_news' 'latest_news'
]); ]);
$datastore->update('latest_news'); $datastore->update('latest_news');
} }
$net_forums = array_flip(explode(',', config()->get('network_news_forum_id'))); $net_forums = array_flip(explode(',', tp_config()->get('network_news_forum_id')));
if ((isset($net_forums[$forum_id]) || isset($net_forums[$new_forum_id])) && config()->get('show_network_news') && $result) { if ((isset($net_forums[$forum_id]) || isset($net_forums[$new_forum_id])) && tp_config()->get('show_network_news') && $result) {
$datastore->enqueue([ $datastore->enqueue([
'network_news' 'network_news'
]); ]);
@ -557,7 +561,7 @@ switch ($mode) {
$poster = $postrow[$i]['username']; $poster = $postrow[$i]['username'];
$poster_rank = $postrow[$i]['user_rank']; $poster_rank = $postrow[$i]['user_rank'];
$post_date = bb_date($postrow[$i]['post_time'], config()->get('post_date_format')); $post_date = bb_date($postrow[$i]['post_time'], tp_config()->get('post_date_format'));
$message = $postrow[$i]['post_text']; $message = $postrow[$i]['post_text'];

View file

@ -9,9 +9,12 @@
define('BB_SCRIPT', 'playback_m3u'); define('BB_SCRIPT', 'playback_m3u');
require __DIR__ . '/common.php'; // Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
if (!config()->get('torr_server.enabled')) { if (!tp_config()->get('torr_server.enabled')) {
redirect('index.php'); redirect('index.php');
} }
@ -22,7 +25,7 @@ $validFormats = [
]; ];
// Start session management // Start session management
$user->session_start(['req_login' => config()->get('torr_server.disable_for_guest')]); $user->session_start(['req_login' => tp_config()->get('torr_server.disable_for_guest')]);
// Disable robots indexing // Disable robots indexing
$page_cfg['allow_robots'] = false; $page_cfg['allow_robots'] = false;

View file

@ -9,7 +9,10 @@
define('BB_SCRIPT', 'vote'); define('BB_SCRIPT', 'vote');
require __DIR__ . '/common.php'; // Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
// Start session management // Start session management
$user->session_start(['req_login' => true]); $user->session_start(['req_login' => true]);
@ -47,8 +50,8 @@ if ($mode != 'poll_vote') {
// Checking the ability to make changes // Checking the ability to make changes
if ($mode == 'poll_delete') { if ($mode == 'poll_delete') {
if ($t_data['topic_time'] < TIMENOW - config()->get('poll_max_days') * 86400) { if ($t_data['topic_time'] < TIMENOW - tp_config()->get('poll_max_days') * 86400) {
bb_die(sprintf(__('NEW_POLL_DAYS'), config()->get('poll_max_days'))); bb_die(sprintf(__('NEW_POLL_DAYS'), tp_config()->get('poll_max_days')));
} }
if (!IS_ADMIN && ($t_data['topic_vote'] != POLL_FINISHED)) { if (!IS_ADMIN && ($t_data['topic_vote'] != POLL_FINISHED)) {
bb_die(__('CANNOT_DELETE_POLL')); bb_die(__('CANNOT_DELETE_POLL'));

View file

@ -9,7 +9,11 @@
define('BB_SCRIPT', 'posting'); define('BB_SCRIPT', 'posting');
require __DIR__ . '/common.php'; // Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
require INC_DIR . '/bbcode.php'; require INC_DIR . '/bbcode.php';
require ATTACH_DIR . '/attachment_mod.php'; require ATTACH_DIR . '/attachment_mod.php';
@ -221,7 +225,7 @@ if (!$is_auth[$is_auth_type]) {
} }
if ($mode == 'new_rel') { if ($mode == 'new_rel') {
if ($tor_status = implode(',', config()->get('tor_cannot_new'))) { if ($tor_status = implode(',', tp_config()->get('tor_cannot_new'))) {
$sql = DB()->fetch_rowset("SELECT t.topic_title, t.topic_id, tor.tor_status $sql = DB()->fetch_rowset("SELECT t.topic_title, t.topic_id, tor.tor_status
FROM " . BB_BT_TORRENTS . " tor, " . BB_TOPICS . " t FROM " . BB_BT_TORRENTS . " tor, " . BB_TOPICS . " t
WHERE poster_id = {$userdata['user_id']} WHERE poster_id = {$userdata['user_id']}
@ -232,7 +236,7 @@ if ($mode == 'new_rel') {
$topics = ''; $topics = '';
foreach ($sql as $row) { foreach ($sql as $row) {
$topics .= config()->get('tor_icons')[$row['tor_status']] . '<a href="' . TOPIC_URL . $row['topic_id'] . '">' . $row['topic_title'] . '</a><div class="spacer_12"></div>'; $topics .= tp_config()->get('tor_icons')[$row['tor_status']] . '<a href="' . TOPIC_URL . $row['topic_id'] . '">' . $row['topic_title'] . '</a><div class="spacer_12"></div>';
} }
if ($topics && !(IS_SUPER_ADMIN && !empty($_REQUEST['edit_tpl']))) { if ($topics && !(IS_SUPER_ADMIN && !empty($_REQUEST['edit_tpl']))) {
bb_die($topics . $lang['UNEXECUTED_RELEASE']); bb_die($topics . $lang['UNEXECUTED_RELEASE']);
@ -243,9 +247,9 @@ if ($mode == 'new_rel') {
} }
// Disallowed release editing with a certain status // Disallowed release editing with a certain status
if (!empty(config()->get('tor_cannot_edit')) && $post_info['allow_reg_tracker'] && $post_data['first_post'] && !IS_AM) { if (!empty(tp_config()->get('tor_cannot_edit')) && $post_info['allow_reg_tracker'] && $post_data['first_post'] && !IS_AM) {
if ($tor_status = DB()->fetch_row("SELECT tor_status FROM " . BB_BT_TORRENTS . " WHERE topic_id = $topic_id AND forum_id = $forum_id AND tor_status IN(" . implode(',', config()->get('tor_cannot_edit')) . ") LIMIT 1")) { if ($tor_status = DB()->fetch_row("SELECT tor_status FROM " . BB_BT_TORRENTS . " WHERE topic_id = $topic_id AND forum_id = $forum_id AND tor_status IN(" . implode(',', tp_config()->get('tor_cannot_edit')) . ") LIMIT 1")) {
bb_die($lang['NOT_EDIT_TOR_STATUS'] . ':&nbsp;<span title="' . $lang['TOR_STATUS_NAME'][$tor_status['tor_status']] . '">' . config()->get('tor_icons')[$tor_status['tor_status']] . '&nbsp;' . $lang['TOR_STATUS_NAME'][$tor_status['tor_status']] . '</span>.'); bb_die($lang['NOT_EDIT_TOR_STATUS'] . ':&nbsp;<span title="' . $lang['TOR_STATUS_NAME'][$tor_status['tor_status']] . '">' . tp_config()->get('tor_icons')[$tor_status['tor_status']] . '&nbsp;' . $lang['TOR_STATUS_NAME'][$tor_status['tor_status']] . '</span>.');
} }
} }
@ -281,7 +285,7 @@ if (!IS_GUEST && $mode != 'newtopic' && ($submit || $preview || $mode == 'quote'
AND pt.post_id = p.post_id AND pt.post_id = p.post_id
AND p.post_time > $topic_last_read AND p.post_time > $topic_last_read
ORDER BY p.post_time ORDER BY p.post_time
LIMIT " . config()->get('posts_per_page'); LIMIT " . tp_config()->get('posts_per_page');
if ($rowset = DB()->fetch_rowset($sql)) { if ($rowset = DB()->fetch_rowset($sql)) {
$topic_has_new_posts = true; $topic_has_new_posts = true;
@ -291,7 +295,7 @@ if (!IS_GUEST && $mode != 'newtopic' && ($submit || $preview || $mode == 'quote'
'ROW_CLASS' => !($i % 2) ? 'row1' : 'row2', 'ROW_CLASS' => !($i % 2) ? 'row1' : 'row2',
'POSTER' => profile_url($row), 'POSTER' => profile_url($row),
'POSTER_NAME_JS' => addslashes($row['username']), 'POSTER_NAME_JS' => addslashes($row['username']),
'POST_DATE' => '<a class="small" href="' . POST_URL . $row['post_id'] . '#' . $row['post_id'] . '" title="' . $lang['POST_LINK'] . '">' . bb_date($row['post_time'], config()->get('post_date_format')) . '</a>', 'POST_DATE' => '<a class="small" href="' . POST_URL . $row['post_id'] . '#' . $row['post_id'] . '" title="' . $lang['POST_LINK'] . '">' . bb_date($row['post_time'], tp_config()->get('post_date_format')) . '</a>',
'MESSAGE' => get_parsed_post($row) 'MESSAGE' => get_parsed_post($row)
]); ]);
} }
@ -374,9 +378,9 @@ if (($delete || $mode == 'delete') && !$confirm) {
set_tracks(COOKIE_TOPIC, $tracking_topics, $topic_id); set_tracks(COOKIE_TOPIC, $tracking_topics, $topic_id);
} }
if (defined('TORRENT_ATTACH_ID') && config()->get('bt_newtopic_auto_reg') && !$error_msg) { if (defined('TORRENT_ATTACH_ID') && tp_config()->get('bt_newtopic_auto_reg') && !$error_msg) {
if (!DB()->fetch_row("SELECT attach_id FROM " . BB_BT_TORRENTS . " WHERE attach_id = " . TORRENT_ATTACH_ID)) { if (!DB()->fetch_row("SELECT attach_id FROM " . BB_BT_TORRENTS . " WHERE attach_id = " . TORRENT_ATTACH_ID)) {
if (config()->get('premod')) { if (tp_config()->get('premod')) {
// Getting a list of forum ids starting with "parent" // Getting a list of forum ids starting with "parent"
$forum_parent = $forum_id; $forum_parent = $forum_id;
if ($post_info['forum_parent']) { if ($post_info['forum_parent']) {
@ -468,7 +472,7 @@ if ($refresh || $error_msg || ($submit && $topic_has_new_posts)) {
$message = '[quote="' . $quote_username . '"][qpost=' . $post_info['post_id'] . ']' . $message . '[/quote]'; $message = '[quote="' . $quote_username . '"][qpost=' . $post_info['post_id'] . ']' . $message . '[/quote]';
// hide user passkey // hide user passkey
$message = preg_replace('#(?<=[\?&;]' . config()->get('passkey_key') . '=)[a-zA-Z0-9]#', 'passkey', $message); $message = preg_replace('#(?<=[\?&;]' . tp_config()->get('passkey_key') . '=)[a-zA-Z0-9]#', 'passkey', $message);
// hide sid // hide sid
$message = preg_replace('#(?<=[\?&;]sid=)[a-zA-Z0-9]#', 'sid', $message); $message = preg_replace('#(?<=[\?&;]sid=)[a-zA-Z0-9]#', 'sid', $message);
@ -618,7 +622,7 @@ $template->assign_vars([
'U_VIEW_FORUM' => FORUM_URL . $forum_id, 'U_VIEW_FORUM' => FORUM_URL . $forum_id,
'USERNAME' => @$username, 'USERNAME' => @$username,
'CAPTCHA_HTML' => (IS_GUEST && !config()->get('captcha.disabled')) ? bb_captcha('get') : '', 'CAPTCHA_HTML' => (IS_GUEST && !tp_config()->get('captcha.disabled')) ? bb_captcha('get') : '',
'SUBJECT' => $subject, 'SUBJECT' => $subject,
'MESSAGE' => $message, 'MESSAGE' => $message,

View file

@ -10,7 +10,11 @@
define('BB_SCRIPT', 'pm'); define('BB_SCRIPT', 'pm');
define('IN_PM', true); define('IN_PM', true);
require __DIR__ . '/common.php'; // Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
require INC_DIR . '/bbcode.php'; require INC_DIR . '/bbcode.php';
$privmsg_sent_id = $l_box_name = $to_username = $privmsg_subject = $privmsg_message = $error_msg = ''; $privmsg_sent_id = $l_box_name = $to_username = $privmsg_subject = $privmsg_message = $error_msg = '';
@ -24,7 +28,7 @@ $page_cfg['load_tpl_vars'] = [
// //
// Is PM disabled? // Is PM disabled?
// //
if (config()->get('privmsg_disable')) { if (tp_config()->get('privmsg_disable')) {
bb_die('PM_DISABLED'); bb_die('PM_DISABLED');
} }
@ -59,13 +63,13 @@ $user->session_start(['req_login' => true]);
$template->assign_vars([ $template->assign_vars([
'IN_PM' => true, 'IN_PM' => true,
'QUICK_REPLY' => config()->get('show_quick_reply') && $folder == 'inbox' && $mode == 'read', 'QUICK_REPLY' => tp_config()->get('show_quick_reply') && $folder == 'inbox' && $mode == 'read',
]); ]);
// //
// Set mode for quick reply // Set mode for quick reply
// //
if (empty($mode) && config()->get('show_quick_reply') && $folder == 'inbox' && $preview) { if (empty($mode) && tp_config()->get('show_quick_reply') && $folder == 'inbox' && $preview) {
$mode = 'reply'; $mode = 'reply';
} }
@ -206,7 +210,7 @@ if ($mode == 'read') {
} }
if ($sent_info = DB()->sql_fetchrow($result)) { if ($sent_info = DB()->sql_fetchrow($result)) {
if (config()->get('max_sentbox_privmsgs') && $sent_info['sent_items'] >= config()->get('max_sentbox_privmsgs')) { if (tp_config()->get('max_sentbox_privmsgs') && $sent_info['sent_items'] >= tp_config()->get('max_sentbox_privmsgs')) {
$sql = "SELECT privmsgs_id FROM " . BB_PRIVMSGS . " $sql = "SELECT privmsgs_id FROM " . BB_PRIVMSGS . "
WHERE privmsgs_type = " . PRIVMSGS_SENT_MAIL . " WHERE privmsgs_type = " . PRIVMSGS_SENT_MAIL . "
AND privmsgs_date = " . $sent_info['oldest_post_time'] . " AND privmsgs_date = " . $sent_info['oldest_post_time'] . "
@ -604,7 +608,7 @@ if ($mode == 'read') {
} }
if ($saved_info = DB()->sql_fetchrow($result)) { if ($saved_info = DB()->sql_fetchrow($result)) {
if (config()->get('max_savebox_privmsgs') && $saved_info['savebox_items'] >= config()->get('max_savebox_privmsgs')) { if (tp_config()->get('max_savebox_privmsgs') && $saved_info['savebox_items'] >= tp_config()->get('max_savebox_privmsgs')) {
$sql = "SELECT privmsgs_id FROM " . BB_PRIVMSGS . " $sql = "SELECT privmsgs_id FROM " . BB_PRIVMSGS . "
WHERE ( ( privmsgs_to_userid = " . $userdata['user_id'] . " WHERE ( ( privmsgs_to_userid = " . $userdata['user_id'] . "
AND privmsgs_type = " . PRIVMSGS_SAVED_IN_MAIL . " ) AND privmsgs_type = " . PRIVMSGS_SAVED_IN_MAIL . " )
@ -749,7 +753,7 @@ if ($mode == 'read') {
$last_post_time = $db_row['last_post_time']; $last_post_time = $db_row['last_post_time'];
$current_time = TIMENOW; $current_time = TIMENOW;
if (($current_time - $last_post_time) < config()->get('flood_interval')) { if (($current_time - $last_post_time) < tp_config()->get('flood_interval')) {
bb_die($lang['FLOOD_ERROR']); bb_die($lang['FLOOD_ERROR']);
} }
} }
@ -802,11 +806,11 @@ if ($mode == 'read') {
} }
// Check smilies limit // Check smilies limit
if (config()->get('max_smilies_pm')) { if (tp_config()->get('max_smilies_pm')) {
$count_smilies = substr_count(bbcode2html($privmsg_message), '<img class="smile" src="' . config()->get('smilies_path')); $count_smilies = substr_count(bbcode2html($privmsg_message), '<img class="smile" src="' . tp_config()->get('smilies_path'));
if ($count_smilies > config()->get('max_smilies_pm')) { if ($count_smilies > tp_config()->get('max_smilies_pm')) {
$error = true; $error = true;
$error_msg .= ((!empty($error_msg)) ? '<br />' : '') . sprintf($lang['MAX_SMILIES_PER_POST'], config()->get('max_smilies_pm')); $error_msg .= ((!empty($error_msg)) ? '<br />' : '') . sprintf($lang['MAX_SMILIES_PER_POST'], tp_config()->get('max_smilies_pm'));
} }
} }
} }
@ -834,7 +838,7 @@ if ($mode == 'read') {
} }
if ($inbox_info = DB()->sql_fetchrow($result)) { if ($inbox_info = DB()->sql_fetchrow($result)) {
if (config()->get('max_inbox_privmsgs') && $inbox_info['inbox_items'] >= config()->get('max_inbox_privmsgs')) { if (tp_config()->get('max_inbox_privmsgs') && $inbox_info['inbox_items'] >= tp_config()->get('max_inbox_privmsgs')) {
$sql = "SELECT privmsgs_id FROM " . BB_PRIVMSGS . " $sql = "SELECT privmsgs_id FROM " . BB_PRIVMSGS . "
WHERE ( privmsgs_type = " . PRIVMSGS_NEW_MAIL . " WHERE ( privmsgs_type = " . PRIVMSGS_NEW_MAIL . "
OR privmsgs_type = " . PRIVMSGS_READ_MAIL . " OR privmsgs_type = " . PRIVMSGS_READ_MAIL . "
@ -902,7 +906,7 @@ if ($mode == 'read') {
\TorrentPier\Sessions::cache_rm_user_sessions($to_userdata['user_id']); \TorrentPier\Sessions::cache_rm_user_sessions($to_userdata['user_id']);
if (bf($to_userdata['user_opt'], 'user_opt', 'user_notify_pm') && $to_userdata['user_active'] && config()->get('pm_notify_enabled')) { if (bf($to_userdata['user_opt'], 'user_opt', 'user_notify_pm') && $to_userdata['user_active'] && tp_config()->get('pm_notify_enabled')) {
// Sending email // Sending email
$emailer = new TorrentPier\Emailer(); $emailer = new TorrentPier\Emailer();
@ -1252,7 +1256,7 @@ if ($mode == 'read') {
$msg_days = 0; $msg_days = 0;
} }
$sql .= $limit_msg_time . " ORDER BY pm.privmsgs_date DESC LIMIT $start, " . config()->get('topics_per_page'); $sql .= $limit_msg_time . " ORDER BY pm.privmsgs_date DESC LIMIT $start, " . tp_config()->get('topics_per_page');
$sql_all_tot = $sql_tot; $sql_all_tot = $sql_tot;
$sql_tot .= $limit_msg_time_total; $sql_tot .= $limit_msg_time_total;
@ -1308,11 +1312,11 @@ if ($mode == 'read') {
// Output data for inbox status // Output data for inbox status
// //
$box_limit_img_length = $box_limit_percent = $l_box_size_status = ''; $box_limit_img_length = $box_limit_percent = $l_box_size_status = '';
$max_pm = ($folder != 'outbox') ? config()->get("max_{$folder}_privmsgs") : null; $max_pm = ($folder != 'outbox') ? tp_config()->get("max_{$folder}_privmsgs") : null;
if ($max_pm) { if ($max_pm) {
$box_limit_percent = min(round(($pm_all_total / $max_pm) * 100), 100); $box_limit_percent = min(round(($pm_all_total / $max_pm) * 100), 100);
$box_limit_img_length = min(round(($pm_all_total / $max_pm) * config()->get('privmsg_graphic_length')), config()->get('privmsg_graphic_length')); $box_limit_img_length = min(round(($pm_all_total / $max_pm) * tp_config()->get('privmsg_graphic_length')), tp_config()->get('privmsg_graphic_length'));
$box_limit_remain = max(($max_pm - $pm_all_total), 0); $box_limit_remain = max(($max_pm - $pm_all_total), 0);
$template->assign_var('PM_BOX_SIZE_INFO'); $template->assign_var('PM_BOX_SIZE_INFO');
@ -1410,7 +1414,7 @@ if ($mode == 'read') {
]); ]);
} while ($row = DB()->sql_fetchrow($result)); } while ($row = DB()->sql_fetchrow($result));
generate_pagination(PM_URL . "?folder=$folder", $pm_total, config()->get('topics_per_page'), $start); generate_pagination(PM_URL . "?folder=$folder", $pm_total, tp_config()->get('topics_per_page'), $start);
} else { } else {
$template->assign_block_vars('switch_no_messages', []); $template->assign_block_vars('switch_no_messages', []);
} }

View file

@ -10,7 +10,10 @@
define('BB_SCRIPT', 'profile'); define('BB_SCRIPT', 'profile');
define('IN_PROFILE', true); define('IN_PROFILE', true);
require __DIR__ . '/common.php'; // Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
// Start session management // Start session management
$user->session_start(); $user->session_start();

View file

@ -9,7 +9,11 @@
define('BB_SCRIPT', 'search'); define('BB_SCRIPT', 'search');
require __DIR__ . '/common.php'; // Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
require INC_DIR . '/bbcode.php'; require INC_DIR . '/bbcode.php';
$page_cfg['use_tablesorter'] = true; $page_cfg['use_tablesorter'] = true;
@ -20,7 +24,7 @@ $page_cfg['load_tpl_vars'] = [
]; ];
// Start session management // Start session management
$user->session_start(array('req_login' => config()->get('disable_search_for_guest'))); $user->session_start(array('req_login' => tp_config()->get('disable_search_for_guest')));
set_die_append_msg(); set_die_append_msg();
@ -289,7 +293,7 @@ if (empty($_GET) && empty($_POST)) {
'MY_TOPICS_ID' => 'my_topics', 'MY_TOPICS_ID' => 'my_topics',
'MY_TOPICS_CHBOX' => build_checkbox($my_topics_key, $lang['SEARCH_MY_TOPICS'], $my_topics_val, true, null, 'my_topics'), 'MY_TOPICS_CHBOX' => build_checkbox($my_topics_key, $lang['SEARCH_MY_TOPICS'], $my_topics_val, true, null, 'my_topics'),
'TITLE_ONLY_CHBOX' => build_checkbox($title_only_key, $lang['SEARCH_TITLES_ONLY'], true, config()->get('disable_ft_search_in_posts')), 'TITLE_ONLY_CHBOX' => build_checkbox($title_only_key, $lang['SEARCH_TITLES_ONLY'], true, tp_config()->get('disable_ft_search_in_posts')),
'ALL_WORDS_CHBOX' => build_checkbox($all_words_key, $lang['SEARCH_ALL_WORDS'], true), 'ALL_WORDS_CHBOX' => build_checkbox($all_words_key, $lang['SEARCH_ALL_WORDS'], true),
'DL_CANCEL_CHBOX' => build_checkbox($dl_cancel_key, $lang['SEARCH_DL_CANCEL'], $dl_cancel_val, IS_GUEST, 'dlCancel'), 'DL_CANCEL_CHBOX' => build_checkbox($dl_cancel_key, $lang['SEARCH_DL_CANCEL'], $dl_cancel_val, IS_GUEST, 'dlCancel'),
'DL_COMPL_CHBOX' => build_checkbox($dl_compl_key, $lang['SEARCH_DL_COMPLETE'], $dl_compl_val, IS_GUEST, 'dlComplete'), 'DL_COMPL_CHBOX' => build_checkbox($dl_compl_key, $lang['SEARCH_DL_COMPLETE'], $dl_compl_val, IS_GUEST, 'dlComplete'),
@ -421,7 +425,7 @@ $prev_days = ($time_val != $search_all);
$new_topics = (!IS_GUEST && ($new_topics_val || isset($_GET['newposts']))); $new_topics = (!IS_GUEST && ($new_topics_val || isset($_GET['newposts'])));
$my_topics = ($poster_id_val && $my_topics_val); $my_topics = ($poster_id_val && $my_topics_val);
$my_posts = ($poster_id_val && !$my_topics_val); $my_posts = ($poster_id_val && !$my_topics_val);
$title_match = ($text_match_sql && ($title_only_val || config()->get('disable_ft_search_in_posts'))); $title_match = ($text_match_sql && ($title_only_val || tp_config()->get('disable_ft_search_in_posts')));
// "Display as" mode (posts or topics) // "Display as" mode (posts or topics)
$post_mode = (!$dl_search && ($display_as_val == $as_posts || isset($_GET['search_author']))); $post_mode = (!$dl_search && ($display_as_val == $as_posts || isset($_GET['search_author'])));
@ -433,7 +437,7 @@ $SQL = DB()->get_empty_sql_array();
if ($post_mode) { if ($post_mode) {
$order = $order_opt[$order_val]['sql']; $order = $order_opt[$order_val]['sql'];
$sort = $sort_opt[$sort_val]['sql']; $sort = $sort_opt[$sort_val]['sql'];
$per_page = config()->get('posts_per_page'); $per_page = tp_config()->get('posts_per_page');
$display_as_val = $as_posts; $display_as_val = $as_posts;
// Run initial search for post_ids // Run initial search for post_ids
@ -594,7 +598,7 @@ if ($post_mode) {
'POSTER_ID' => $post['poster_id'], 'POSTER_ID' => $post['poster_id'],
'POSTER' => profile_url($post), 'POSTER' => profile_url($post),
'POST_ID' => $post['post_id'], 'POST_ID' => $post['post_id'],
'POST_DATE' => bb_date($post['post_time'], config()->get('post_date_format')), 'POST_DATE' => bb_date($post['post_time'], tp_config()->get('post_date_format')),
'IS_UNREAD' => is_unread($post['post_time'], $topic_id, $forum_id), 'IS_UNREAD' => is_unread($post['post_time'], $topic_id, $forum_id),
'MESSAGE' => $message, 'MESSAGE' => $message,
'POSTED_AFTER' => '', 'POSTED_AFTER' => '',
@ -613,7 +617,7 @@ if ($post_mode) {
else { else {
$order = $order_opt[$order_val]['sql']; $order = $order_opt[$order_val]['sql'];
$sort = $sort_opt[$sort_val]['sql']; $sort = $sort_opt[$sort_val]['sql'];
$per_page = config()->get('topics_per_page'); $per_page = tp_config()->get('topics_per_page');
$display_as_val = $as_topics; $display_as_val = $as_topics;
// Run initial search for topic_ids // Run initial search for topic_ids
@ -739,7 +743,7 @@ else {
// Build SQL for displaying topics // Build SQL for displaying topics
$SQL = DB()->get_empty_sql_array(); $SQL = DB()->get_empty_sql_array();
$join_dl = (config()->get('show_dl_status_in_search') && !IS_GUEST); $join_dl = (tp_config()->get('show_dl_status_in_search') && !IS_GUEST);
$SQL['SELECT'][] = " $SQL['SELECT'][] = "
t.*, t.topic_poster AS first_user_id, u1.user_rank AS first_user_rank, t.*, t.topic_poster AS first_user_id, u1.user_rank AS first_user_rank,
@ -796,7 +800,7 @@ else {
'TOPIC_TITLE' => censor()->censorString($topic['topic_title']), 'TOPIC_TITLE' => censor()->censorString($topic['topic_title']),
'IS_UNREAD' => $is_unread, 'IS_UNREAD' => $is_unread,
'TOPIC_ICON' => get_topic_icon($topic, $is_unread), 'TOPIC_ICON' => get_topic_icon($topic, $is_unread),
'PAGINATION' => $moved ? '' : build_topic_pagination(TOPIC_URL . $topic_id, $topic['topic_replies'], config()->get('posts_per_page')), 'PAGINATION' => $moved ? '' : build_topic_pagination(TOPIC_URL . $topic_id, $topic['topic_replies'], tp_config()->get('posts_per_page')),
'REPLIES' => $moved ? '' : $topic['topic_replies'], 'REPLIES' => $moved ? '' : $topic['topic_replies'],
'ATTACH' => $topic['topic_attachment'], 'ATTACH' => $topic['topic_attachment'],
'STATUS' => $topic['topic_status'], 'STATUS' => $topic['topic_status'],
@ -894,13 +898,13 @@ function fetch_search_ids($sql, $search_type = SEARCH_TYPE_POST)
function prevent_huge_searches($SQL) function prevent_huge_searches($SQL)
{ {
if (config()->get('limit_max_search_results')) { if (tp_config()->get('limit_max_search_results')) {
$SQL['select_options'][] = 'SQL_CALC_FOUND_ROWS'; $SQL['select_options'][] = 'SQL_CALC_FOUND_ROWS';
$SQL['ORDER BY'] = []; $SQL['ORDER BY'] = [];
$SQL['LIMIT'] = array('0'); $SQL['LIMIT'] = array('0');
if (DB()->query($SQL) and $row = DB()->fetch_row("SELECT FOUND_ROWS() AS rows_count")) { if (DB()->query($SQL) and $row = DB()->fetch_row("SELECT FOUND_ROWS() AS rows_count")) {
if ($row['rows_count'] > config()->get('limit_max_search_results')) { if ($row['rows_count'] > tp_config()->get('limit_max_search_results')) {
# bb_log(str_compact(DB()->build_sql($SQL)) ." [{$row['rows_count']} rows]". LOG_LF, 'sql_huge_search'); # bb_log(str_compact(DB()->build_sql($SQL)) ." [{$row['rows_count']} rows]". LOG_LF, 'sql_huge_search');
bb_die('Too_many_search_results'); bb_die('Too_many_search_results');
} }

View file

@ -9,19 +9,22 @@
define('BB_SCRIPT', 'terms'); define('BB_SCRIPT', 'terms');
require __DIR__ . '/common.php'; // Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
require INC_DIR . '/bbcode.php'; require INC_DIR . '/bbcode.php';
// Start session management // Start session management
$user->session_start(); $user->session_start();
if (!config()->get('terms') && !IS_ADMIN) { if (!tp_config()->get('terms') && !IS_ADMIN) {
redirect('index.php'); redirect('index.php');
} }
$template->assign_vars([ $template->assign_vars([
'TERMS_EDIT' => bbcode2html(sprintf($lang['TERMS_EMPTY_TEXT'], make_url('admin/admin_terms.php'))), 'TERMS_EDIT' => bbcode2html(sprintf($lang['TERMS_EMPTY_TEXT'], make_url('admin/admin_terms.php'))),
'TERMS_HTML' => bbcode2html(config()->get('terms')), 'TERMS_HTML' => bbcode2html(tp_config()->get('terms')),
]); ]);
print_page('terms.tpl'); print_page('terms.tpl');

View file

@ -9,7 +9,10 @@
define('BB_SCRIPT', 'tracker'); define('BB_SCRIPT', 'tracker');
require __DIR__ . '/common.php'; // Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
// Page config // Page config
$page_cfg['include_bbcode_js'] = true; $page_cfg['include_bbcode_js'] = true;
@ -19,7 +22,7 @@ $page_cfg['load_tpl_vars'] = [
]; ];
// Session start // Session start
$user->session_start(array('req_login' => config()->get('bt_tor_browse_only_reg'))); $user->session_start(array('req_login' => tp_config()->get('bt_tor_browse_only_reg')));
set_die_append_msg(); set_die_append_msg();
@ -30,7 +33,7 @@ $max_forums_selected = 50;
$title_match_max_len = 60; $title_match_max_len = 60;
$poster_name_max_len = 25; $poster_name_max_len = 25;
$tor_colspan = 12; // torrents table colspan with all columns $tor_colspan = 12; // torrents table colspan with all columns
$per_page = config()->get('topics_per_page'); $per_page = tp_config()->get('topics_per_page');
$tracker_url = basename(__FILE__); $tracker_url = basename(__FILE__);
$time_format = 'H:i'; $time_format = 'H:i';
@ -299,8 +302,8 @@ if (isset($_GET[$user_releases_key])) {
} }
// Random release // Random release
if (config()->get('tracker.random_release_button') && isset($_GET['random_release'])) { if (tp_config()->get('tracker.random_release_button') && isset($_GET['random_release'])) {
if ($random_release = DB()->fetch_row("SELECT topic_id FROM " . BB_BT_TORRENTS . " WHERE tor_status NOT IN(" . implode(', ', array_keys(config()->get('tor_frozen'))) . ") ORDER BY RAND() LIMIT 1")) { if ($random_release = DB()->fetch_row("SELECT topic_id FROM " . BB_BT_TORRENTS . " WHERE tor_status NOT IN(" . implode(', ', array_keys(tp_config()->get('tor_frozen'))) . ") ORDER BY RAND() LIMIT 1")) {
redirect(TOPIC_URL . $random_release['topic_id']); redirect(TOPIC_URL . $random_release['topic_id']);
} else { } else {
bb_die($lang['NO_MATCH']); bb_die($lang['NO_MATCH']);
@ -749,8 +752,8 @@ if ($allowed_forums) {
'MAGNET' => $tor_magnet, 'MAGNET' => $tor_magnet,
'TOR_TYPE' => is_gold($tor['tor_type']), 'TOR_TYPE' => is_gold($tor['tor_type']),
'TOR_FROZEN' => !IS_AM ? isset(config()->get('tor_frozen')[$tor['tor_status']]) : '', 'TOR_FROZEN' => !IS_AM ? isset(tp_config()->get('tor_frozen')[$tor['tor_status']]) : '',
'TOR_STATUS_ICON' => config()->get('tor_icons')[$tor['tor_status']], 'TOR_STATUS_ICON' => tp_config()->get('tor_icons')[$tor['tor_status']],
'TOR_STATUS_TEXT' => $lang['TOR_STATUS_NAME'][$tor['tor_status']], 'TOR_STATUS_TEXT' => $lang['TOR_STATUS_NAME'][$tor['tor_status']],
'TOR_SIZE_RAW' => $size, 'TOR_SIZE_RAW' => $size,
@ -819,9 +822,9 @@ $search_all_opt = '<option value="' . $search_all . '" value="fs-' . $search_all
$cat_forum_select = "\n" . '<select id="fs-main" style="width: 100%;" name="' . $forum_key . '[]" multiple size="' . $forum_select_size . "\">\n" . $search_all_opt . $opt . "</select>\n"; $cat_forum_select = "\n" . '<select id="fs-main" style="width: 100%;" name="' . $forum_key . '[]" multiple size="' . $forum_select_size . "\">\n" . $search_all_opt . $opt . "</select>\n";
// Status select // Status select
if (config()->get('tracker.search_by_tor_status')) { if (tp_config()->get('tracker.search_by_tor_status')) {
$statuses = '<table border="0" cellpadding="0" cellspacing="0">'; $statuses = '<table border="0" cellpadding="0" cellspacing="0">';
foreach (array_chunk(config()->get('tor_icons'), 2, true) as $statuses_part) { foreach (array_chunk(tp_config()->get('tor_icons'), 2, true) as $statuses_part) {
$statuses .= '<tr>'; $statuses .= '<tr>';
foreach ($statuses_part as $status_id => $status_styles) { foreach ($statuses_part as $status_id => $status_styles) {
$checked_status = in_array($status_id, $status) ? 'checked' : ''; $checked_status = in_array($status_id, $status) ? 'checked' : '';

View file

@ -9,7 +9,10 @@
define('BB_SCRIPT', 'forum'); define('BB_SCRIPT', 'forum');
require __DIR__ . '/common.php'; // Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
$page_cfg['include_bbcode_js'] = true; $page_cfg['include_bbcode_js'] = true;
@ -121,7 +124,7 @@ if ($mark_read && !IS_GUEST) {
} }
// Subforums // Subforums
$show_subforums = config()->get('sf_on_first_page_only') ? !$start : true; $show_subforums = tp_config()->get('sf_on_first_page_only') ? !$start : true;
if (!$forums = $datastore->get('cat_forums')) { if (!$forums = $datastore->get('cat_forums')) {
$datastore->update('cat_forums'); $datastore->update('cat_forums');
@ -179,7 +182,7 @@ if (!$forum_data['forum_parent'] && isset($forums['f'][$forum_id]['subforums'])
$last_post_user = profile_url(['username' => $sf_data['sf_last_username'], 'user_id' => $sf_data['sf_last_user_id'], 'user_rank' => $sf_data['user_rank']]); $last_post_user = profile_url(['username' => $sf_data['sf_last_username'], 'user_id' => $sf_data['sf_last_user_id'], 'user_rank' => $sf_data['user_rank']]);
if ($sf_data['forum_last_post_id']) { if ($sf_data['forum_last_post_id']) {
$last_post = bb_date($sf_data['topic_last_post_time'], config()->get('last_post_date_format')); $last_post = bb_date($sf_data['topic_last_post_time'], tp_config()->get('last_post_date_format'));
$last_post .= "<br />$last_post_user"; $last_post .= "<br />$last_post_user";
$last_post .= '<a href="' . POST_URL . $sf_data['forum_last_post_id'] . '#' . $sf_data['forum_last_post_id'] . '"><img src="' . $images['icon_latest_reply'] . '" class="icon2" alt="latest" title="' . $lang['VIEW_LATEST_POST'] . '" /></a>'; $last_post .= '<a href="' . POST_URL . $sf_data['forum_last_post_id'] . '#' . $sf_data['forum_last_post_id'] . '"><img src="' . $images['icon_latest_reply'] . '" class="icon2" alt="latest" title="' . $lang['VIEW_LATEST_POST'] . '" /></a>';
} }
@ -203,7 +206,7 @@ if (!$forum_data['forum_parent'] && isset($forums['f'][$forum_id]['subforums'])
'LAST_TOPIC_ID' => $sf_data['last_topic_id'], 'LAST_TOPIC_ID' => $sf_data['last_topic_id'],
'LAST_TOPIC_TIP' => $sf_data['last_topic_title'], 'LAST_TOPIC_TIP' => $sf_data['last_topic_title'],
'LAST_TOPIC_TITLE' => str_short($sf_data['last_topic_title'], $last_topic_max_len), 'LAST_TOPIC_TITLE' => str_short($sf_data['last_topic_title'], $last_topic_max_len),
'LAST_POST_TIME' => bb_date($sf_data['topic_last_post_time'], config()->get('last_post_date_format')), 'LAST_POST_TIME' => bb_date($sf_data['topic_last_post_time'], tp_config()->get('last_post_date_format')),
'LAST_POST_ID' => $sf_data['forum_last_post_id'], 'LAST_POST_ID' => $sf_data['forum_last_post_id'],
'LAST_POST_USER' => $last_post_user, 'LAST_POST_USER' => $last_post_user,
'ICON_LATEST_REPLY' => $images['icon_latest_reply'] 'ICON_LATEST_REPLY' => $images['icon_latest_reply']
@ -217,16 +220,16 @@ unset($rowset);
$datastore->rm('cat_forums'); $datastore->rm('cat_forums');
// Topics per page // Topics per page
$topics_per_page = config()->get('topics_per_page'); $topics_per_page = tp_config()->get('topics_per_page');
$select_tpp = ''; $select_tpp = '';
if ($is_auth['auth_mod']) { if ($is_auth['auth_mod']) {
if ($req_tpp = abs((int)(@$_REQUEST['tpp'])) and in_array($req_tpp, config()->get('allowed_topics_per_page'))) { if ($req_tpp = abs((int)(@$_REQUEST['tpp'])) and in_array($req_tpp, tp_config()->get('allowed_topics_per_page'))) {
$topics_per_page = $req_tpp; $topics_per_page = $req_tpp;
} }
$select_tpp = []; $select_tpp = [];
foreach (config()->get('allowed_topics_per_page') as $tpp) { foreach (tp_config()->get('allowed_topics_per_page') as $tpp) {
$select_tpp[$tpp] = $tpp; $select_tpp[$tpp] = $tpp;
} }
} }
@ -282,7 +285,7 @@ $order_sql = "ORDER BY t.topic_type DESC, $sort_method $order_method";
$limit_topics_time_sql = ($topic_days) ? "AND t.topic_last_post_time > " . (TIMENOW - 86400 * $topic_days) : ''; $limit_topics_time_sql = ($topic_days) ? "AND t.topic_last_post_time > " . (TIMENOW - 86400 * $topic_days) : '';
$select_tor_sql = $join_tor_sql = ''; $select_tor_sql = $join_tor_sql = '';
$join_dl = (config()->get('show_dl_status_in_forum') && !IS_GUEST); $join_dl = (tp_config()->get('show_dl_status_in_forum') && !IS_GUEST);
$where_tor_sql = ''; $where_tor_sql = '';
if ($forum_data['allow_reg_tracker']) { if ($forum_data['allow_reg_tracker']) {
@ -423,7 +426,7 @@ foreach ($topic_rowset as $topic) {
$topic_id = $topic['topic_id']; $topic_id = $topic['topic_id'];
$moved = ($topic['topic_status'] == TOPIC_MOVED); $moved = ($topic['topic_status'] == TOPIC_MOVED);
$replies = $topic['topic_replies']; $replies = $topic['topic_replies'];
$t_hot = ($replies >= config()->get('hot_threshold')); $t_hot = ($replies >= tp_config()->get('hot_threshold'));
$t_type = $topic['topic_type']; $t_type = $topic['topic_type'];
$separator = ''; $separator = '';
$is_unread = is_unread($topic['topic_last_post_time'], $topic_id, $forum_id); $is_unread = is_unread($topic['topic_last_post_time'], $topic_id, $forum_id);
@ -449,14 +452,14 @@ foreach ($topic_rowset as $topic) {
'TOPICS_SEPARATOR' => $separator, 'TOPICS_SEPARATOR' => $separator,
'IS_UNREAD' => $is_unread, 'IS_UNREAD' => $is_unread,
'TOPIC_ICON' => get_topic_icon($topic, $is_unread), 'TOPIC_ICON' => get_topic_icon($topic, $is_unread),
'PAGINATION' => $moved ? '' : build_topic_pagination(TOPIC_URL . $topic_id, $replies, config()->get('posts_per_page')), 'PAGINATION' => $moved ? '' : build_topic_pagination(TOPIC_URL . $topic_id, $replies, tp_config()->get('posts_per_page')),
'REPLIES' => $moved ? '' : $replies, 'REPLIES' => $moved ? '' : $replies,
'VIEWS' => $moved ? '' : $topic['topic_views'], 'VIEWS' => $moved ? '' : $topic['topic_views'],
'TOR_STALED' => ($forum_data['allow_reg_tracker'] && !($t_type == POST_ANNOUNCE || $t_type == POST_STICKY || $topic['tor_size'])), 'TOR_STALED' => ($forum_data['allow_reg_tracker'] && !($t_type == POST_ANNOUNCE || $t_type == POST_STICKY || $topic['tor_size'])),
'TOR_FROZEN' => isset($topic['tor_status']) ? ((!IS_AM) ? isset(config()->get('tor_frozen')[$topic['tor_status']]) : '') : '', 'TOR_FROZEN' => isset($topic['tor_status']) ? ((!IS_AM) ? isset(tp_config()->get('tor_frozen')[$topic['tor_status']]) : '') : '',
'TOR_TYPE' => isset($topic['tor_type']) ? is_gold($topic['tor_type']) : '', 'TOR_TYPE' => isset($topic['tor_type']) ? is_gold($topic['tor_type']) : '',
'TOR_STATUS_ICON' => isset($topic['tor_status']) ? config()->get('tor_icons')[$topic['tor_status']] : '', 'TOR_STATUS_ICON' => isset($topic['tor_status']) ? tp_config()->get('tor_icons')[$topic['tor_status']] : '',
'TOR_STATUS_TEXT' => isset($topic['tor_status']) ? $lang['TOR_STATUS_NAME'][$topic['tor_status']] : '', 'TOR_STATUS_TEXT' => isset($topic['tor_status']) ? $lang['TOR_STATUS_NAME'][$topic['tor_status']] : '',
'ATTACH' => $topic['topic_attachment'] ?? false, 'ATTACH' => $topic['topic_attachment'] ?? false,
@ -468,7 +471,7 @@ foreach ($topic_rowset as $topic) {
'TOPIC_AUTHOR' => profile_url(['username' => str_short($topic['first_username'], 15), 'user_id' => $topic['first_user_id'], 'user_rank' => $topic['first_user_rank']]), 'TOPIC_AUTHOR' => profile_url(['username' => str_short($topic['first_username'], 15), 'user_id' => $topic['first_user_id'], 'user_rank' => $topic['first_user_rank']]),
'LAST_POSTER' => profile_url(['username' => str_short($topic['last_username'], 15), 'user_id' => $topic['last_user_id'], 'user_rank' => $topic['last_user_rank']]), 'LAST_POSTER' => profile_url(['username' => str_short($topic['last_username'], 15), 'user_id' => $topic['last_user_id'], 'user_rank' => $topic['last_user_rank']]),
'LAST_POST_TIME' => bb_date($topic['topic_last_post_time'], config()->get('last_post_date_format')), 'LAST_POST_TIME' => bb_date($topic['topic_last_post_time'], tp_config()->get('last_post_date_format')),
'LAST_POST_ID' => $topic['topic_last_post_id'], 'LAST_POST_ID' => $topic['topic_last_post_id'],
]); ]);
@ -498,7 +501,7 @@ $pg_url .= $topic_days ? "&amp;topicdays=$topic_days" : '';
$pg_url .= $sort_value ? "&amp;sort=$sort_value" : ''; $pg_url .= $sort_value ? "&amp;sort=$sort_value" : '';
$pg_url .= $order_value ? "&amp;order=$order_value" : ''; $pg_url .= $order_value ? "&amp;order=$order_value" : '';
$pg_url .= $moderation ? "&amp;mod=1" : ''; $pg_url .= $moderation ? "&amp;mod=1" : '';
$pg_url .= ($topics_per_page != config()->get('topics_per_page')) ? "&amp;tpp=$topics_per_page" : ''; $pg_url .= ($topics_per_page != tp_config()->get('topics_per_page')) ? "&amp;tpp=$topics_per_page" : '';
if ($found_topics) { if ($found_topics) {
generate_pagination($pg_url, $forum_topics, $topics_per_page, $start); generate_pagination($pg_url, $forum_topics, $topics_per_page, $start);

View file

@ -9,7 +9,11 @@
define('BB_SCRIPT', 'topic'); define('BB_SCRIPT', 'topic');
require __DIR__ . '/common.php'; // Skip loading common.php if already loaded (when run through routing system)
if (!defined('IN_TORRENTPIER')) {
require __DIR__ . '/../common.php';
}
require INC_DIR . '/bbcode.php'; require INC_DIR . '/bbcode.php';
$datastore->enqueue([ $datastore->enqueue([
@ -34,16 +38,16 @@ $user->session_start();
set_die_append_msg(); set_die_append_msg();
// Posts per page // Posts per page
$posts_per_page = config()->get('posts_per_page'); $posts_per_page = tp_config()->get('posts_per_page');
$select_ppp = ''; $select_ppp = '';
if ($userdata['session_admin']) { if ($userdata['session_admin']) {
if ($req_ppp = abs((int)(@$_REQUEST['ppp'])) and in_array($req_ppp, config()->get('allowed_posts_per_page'))) { if ($req_ppp = abs((int)(@$_REQUEST['ppp'])) and in_array($req_ppp, tp_config()->get('allowed_posts_per_page'))) {
$posts_per_page = $req_ppp; $posts_per_page = $req_ppp;
} }
$select_ppp = []; $select_ppp = [];
foreach (config()->get('allowed_posts_per_page') as $ppp) { foreach (tp_config()->get('allowed_posts_per_page') as $ppp) {
$select_ppp[$ppp] = $ppp; $select_ppp[$ppp] = $ppp;
} }
} }
@ -236,7 +240,7 @@ if ($post_id && !empty($t_data['prev_posts'])) {
// Is user watching this thread? // Is user watching this thread?
$can_watch_topic = $is_watching_topic = false; $can_watch_topic = $is_watching_topic = false;
if (config()->get('topic_notify_enabled')) { if (tp_config()->get('topic_notify_enabled')) {
if (!IS_GUEST) { if (!IS_GUEST) {
$can_watch_topic = true; $can_watch_topic = true;
@ -426,7 +430,7 @@ $pg_url .= $post_days ? "&amp;postdays=$post_days" : '';
$pg_url .= ($post_order != 'asc') ? "&amp;postorder=$post_order" : ''; $pg_url .= ($post_order != 'asc') ? "&amp;postorder=$post_order" : '';
$pg_url .= isset($_REQUEST['single']) ? "&amp;single=1" : ''; $pg_url .= isset($_REQUEST['single']) ? "&amp;single=1" : '';
$pg_url .= $moderation ? "&amp;mod=1" : ''; $pg_url .= $moderation ? "&amp;mod=1" : '';
$pg_url .= ($posts_per_page != config()->get('posts_per_page')) ? "&amp;ppp=$posts_per_page" : ''; $pg_url .= ($posts_per_page != tp_config()->get('posts_per_page')) ? "&amp;ppp=$posts_per_page" : '';
generate_pagination($pg_url, $total_replies, $posts_per_page, $start); generate_pagination($pg_url, $total_replies, $posts_per_page, $start);
@ -448,7 +452,7 @@ $sel_post_order_ary = [
]; ];
$topic_has_poll = $t_data['topic_vote']; $topic_has_poll = $t_data['topic_vote'];
$poll_time_expired = ($t_data['topic_time'] < TIMENOW - config()->get('poll_max_days') * 86400); $poll_time_expired = ($t_data['topic_time'] < TIMENOW - tp_config()->get('poll_max_days') * 86400);
$can_manage_poll = ($t_data['topic_poster'] == $userdata['user_id'] || $is_auth['auth_mod']); $can_manage_poll = ($t_data['topic_poster'] == $userdata['user_id'] || $is_auth['auth_mod']);
$can_add_poll = ($can_manage_poll && !$topic_has_poll && !$poll_time_expired && !$start); $can_add_poll = ($can_manage_poll && !$topic_has_poll && !$poll_time_expired && !$start);
@ -470,19 +474,19 @@ $template->assign_vars([
'TOPIC_TITLE' => $topic_title, 'TOPIC_TITLE' => $topic_title,
'PORNO_FORUM' => $t_data['allow_porno_topic'], 'PORNO_FORUM' => $t_data['allow_porno_topic'],
'REPLY_IMG' => $reply_img, 'REPLY_IMG' => $reply_img,
'SHOW_BOT_NICK' => config()->get('show_bot_nick'), 'SHOW_BOT_NICK' => tp_config()->get('show_bot_nick'),
'T_POST_REPLY' => $reply_alt, 'T_POST_REPLY' => $reply_alt,
'HIDE_FROM' => $user->opt_js['h_from'], 'HIDE_FROM' => $user->opt_js['h_from'],
'HIDE_AVATAR' => $user->opt_js['h_av'], 'HIDE_AVATAR' => $user->opt_js['h_av'],
'HIDE_RANK_IMG' => ($user->opt_js['h_rnk_i'] && config()->get('show_rank_image')), 'HIDE_RANK_IMG' => ($user->opt_js['h_rnk_i'] && tp_config()->get('show_rank_image')),
'HIDE_POST_IMG' => $user->opt_js['h_post_i'], 'HIDE_POST_IMG' => $user->opt_js['h_post_i'],
'HIDE_SMILE' => $user->opt_js['h_smile'], 'HIDE_SMILE' => $user->opt_js['h_smile'],
'HIDE_SIGNATURE' => $user->opt_js['h_sig'], 'HIDE_SIGNATURE' => $user->opt_js['h_sig'],
'SPOILER_OPENED' => $user->opt_js['sp_op'], 'SPOILER_OPENED' => $user->opt_js['sp_op'],
'SHOW_IMG_AFTER_LOAD' => $user->opt_js['i_aft_l'], 'SHOW_IMG_AFTER_LOAD' => $user->opt_js['i_aft_l'],
'HIDE_RANK_IMG_DIS' => !config()->get('show_rank_image'), 'HIDE_RANK_IMG_DIS' => !tp_config()->get('show_rank_image'),
'PINNED_FIRST_POST' => $t_data['topic_show_first_post'], 'PINNED_FIRST_POST' => $t_data['topic_show_first_post'],
'PIN_HREF' => $t_data['topic_show_first_post'] ? "modcp.php?" . POST_TOPIC_URL . "=$topic_id&amp;mode=post_unpin" : "modcp.php?" . POST_TOPIC_URL . "=$topic_id&amp;mode=post_pin", 'PIN_HREF' => $t_data['topic_show_first_post'] ? "modcp.php?" . POST_TOPIC_URL . "=$topic_id&amp;mode=post_unpin" : "modcp.php?" . POST_TOPIC_URL . "=$topic_id&amp;mode=post_pin",
@ -559,7 +563,7 @@ for ($i = 0; $i < $total_posts; $i++) {
$poster_bot = ($poster_id == BOT_UID); $poster_bot = ($poster_id == BOT_UID);
$poster = $poster_guest ? $lang['GUEST'] : $postrow[$i]['username']; $poster = $poster_guest ? $lang['GUEST'] : $postrow[$i]['username'];
$post_date = bb_date($postrow[$i]['post_time'], config()->get('post_date_format')); $post_date = bb_date($postrow[$i]['post_time'], tp_config()->get('post_date_format'));
$max_post_time = max($max_post_time, $postrow[$i]['post_time']); $max_post_time = max($max_post_time, $postrow[$i]['post_time']);
$poster_posts = !$poster_guest ? $postrow[$i]['user_posts'] : ''; $poster_posts = !$poster_guest ? $postrow[$i]['user_posts'] : '';
$poster_from = ($postrow[$i]['user_from'] && !$poster_guest) ? $postrow[$i]['user_from'] : ''; $poster_from = ($postrow[$i]['user_from'] && !$poster_guest) ? $postrow[$i]['user_from'] : '';
@ -584,8 +588,8 @@ for ($i = 0; $i < $total_posts; $i++) {
$poster_rank = $rank_image = ''; $poster_rank = $rank_image = '';
$user_rank = $postrow[$i]['user_rank']; $user_rank = $postrow[$i]['user_rank'];
if (!$user->opt_js['h_rnk_i'] and isset($ranks[$user_rank])) { if (!$user->opt_js['h_rnk_i'] and isset($ranks[$user_rank])) {
$rank_image = (config()->get('show_rank_image') && $ranks[$user_rank]['rank_image']) ? '<img src="' . $ranks[$user_rank]['rank_image'] . '" alt="" title="" border="0" />' : ''; $rank_image = (tp_config()->get('show_rank_image') && $ranks[$user_rank]['rank_image']) ? '<img src="' . $ranks[$user_rank]['rank_image'] . '" alt="" title="" border="0" />' : '';
$poster_rank = config()->get('show_rank_text') ? $ranks[$user_rank]['rank_title'] : ''; $poster_rank = tp_config()->get('show_rank_text') ? $ranks[$user_rank]['rank_title'] : '';
} }
// Handle anon users posting with usernames // Handle anon users posting with usernames
@ -611,7 +615,7 @@ for ($i = 0; $i < $total_posts; $i++) {
// Parse message and sig // Parse message and sig
$message = get_parsed_post($postrow[$i]); $message = get_parsed_post($postrow[$i]);
$user_sig = (config()->get('allow_sig') && !$user->opt_js['h_sig'] && $postrow[$i]['user_sig']) ? $postrow[$i]['user_sig'] : ''; $user_sig = (tp_config()->get('allow_sig') && !$user->opt_js['h_sig'] && $postrow[$i]['user_sig']) ? $postrow[$i]['user_sig'] : '';
if (bf($postrow[$i]['user_opt'], 'user_opt', 'dis_sig')) { if (bf($postrow[$i]['user_opt'], 'user_opt', 'dis_sig')) {
$user_sig = $lang['SIGNATURE_DISABLE']; $user_sig = $lang['SIGNATURE_DISABLE'];
@ -642,7 +646,7 @@ for ($i = 0; $i < $total_posts; $i++) {
// Replace newlines (we use this rather than nl2br because till recently it wasn't XHTML compliant) // Replace newlines (we use this rather than nl2br because till recently it wasn't XHTML compliant)
if ($user_sig) { if ($user_sig) {
$user_sig = config()->get('user_signature_start') . $user_sig . config()->get('user_signature_end'); $user_sig = tp_config()->get('user_signature_start') . $user_sig . tp_config()->get('user_signature_end');
} }
// Editing information // Editing information
@ -686,11 +690,11 @@ for ($i = 0; $i < $total_posts; $i++) {
'POSTER_NAME_JS' => addslashes($poster), 'POSTER_NAME_JS' => addslashes($poster),
'POSTER_RANK' => $poster_rank, 'POSTER_RANK' => $poster_rank,
'RANK_IMAGE' => $rank_image, 'RANK_IMAGE' => $rank_image,
'POSTER_JOINED' => config()->get('show_poster_joined') ? $poster_longevity : '', 'POSTER_JOINED' => tp_config()->get('show_poster_joined') ? $poster_longevity : '',
'POSTER_JOINED_DATE' => $poster_joined, 'POSTER_JOINED_DATE' => $poster_joined,
'POSTER_POSTS' => (config()->get('show_poster_posts') && $poster_posts) ? '<a href="search.php?search_author=1&amp;uid=' . $poster_id . '" target="_blank">' . $poster_posts . '</a>' : '', 'POSTER_POSTS' => (tp_config()->get('show_poster_posts') && $poster_posts) ? '<a href="search.php?search_author=1&amp;uid=' . $poster_id . '" target="_blank">' . $poster_posts . '</a>' : '',
'POSTER_FROM' => config()->get('show_poster_from') ? render_flag($poster_from, false) : '', 'POSTER_FROM' => tp_config()->get('show_poster_from') ? render_flag($poster_from, false) : '',
'POSTER_BOT' => $poster_bot, 'POSTER_BOT' => $poster_bot,
'POSTER_GUEST' => $poster_guest, 'POSTER_GUEST' => $poster_guest,
'POSTER_ID' => $poster_id, 'POSTER_ID' => $poster_id,
@ -764,13 +768,13 @@ if (defined('SPLIT_FORM_START')) {
} }
// Quick Reply // Quick Reply
if (config()->get('show_quick_reply')) { if (tp_config()->get('show_quick_reply')) {
if ($is_auth['auth_reply'] && !$locked) { if ($is_auth['auth_reply'] && !$locked) {
$template->assign_vars([ $template->assign_vars([
'QUICK_REPLY' => true, 'QUICK_REPLY' => true,
'QR_POST_ACTION' => POSTING_URL, 'QR_POST_ACTION' => POSTING_URL,
'QR_TOPIC_ID' => $topic_id, 'QR_TOPIC_ID' => $topic_id,
'CAPTCHA_HTML' => (IS_GUEST && !config()->get('captcha.disabled')) ? bb_captcha('get') : '' 'CAPTCHA_HTML' => (IS_GUEST && !tp_config()->get('captcha.disabled')) ? bb_captcha('get') : ''
]); ]);
if (!IS_GUEST) { if (!IS_GUEST) {
@ -788,7 +792,7 @@ foreach ($is_auth as $name => $is) {
$template->assign_vars(['PG_ROW_CLASS' => $pg_row_class ?? 'row1']); $template->assign_vars(['PG_ROW_CLASS' => $pg_row_class ?? 'row1']);
if (IS_ADMIN) { if (IS_ADMIN) {
$template->assign_vars(['U_LOGS' => "admin/admin_log.php?" . POST_TOPIC_URL . "=$topic_id&amp;db=" . config()->get('log_days_keep')]); $template->assign_vars(['U_LOGS' => "admin/admin_log.php?" . POST_TOPIC_URL . "=$topic_id&amp;db=" . tp_config()->get('log_days_keep')]);
} }
print_page('viewtopic.tpl'); print_page('viewtopic.tpl');

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();
}
}

21
dexter Executable file
View file

@ -0,0 +1,21 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
* Dexter - TorrentPier Command Line Interface
*
* Laravel-style CLI for running commands
*/
// Load the bootstrap and get the application
$app = require __DIR__ . '/bootstrap/console.php';
// Run the console application with proper error handling
try {
exit($app->run());
} catch (\Exception $e) {
fwrite(STDERR, "Error: " . $e->getMessage() . "\n");
exit(1);
}

View file

@ -1,99 +0,0 @@
<?php
/**
* Example: How to use the DI Container in TorrentPier 3.0
*
* NOTE: These are examples for future implementation following the hexagonal architecture spec.
* Most services referenced here don't exist yet and will be implemented in phases.
*/
use TorrentPier\Infrastructure\DependencyInjection\Bootstrap;
use TorrentPier\Infrastructure\DependencyInjection\ContainerFactory;
// ===== PHASE 1: Foundation (CURRENT) =====
// 1. Bootstrap the container (typically done once in index.php or bootstrap file)
$rootPath = __DIR__ . '/../..';
$container = Bootstrap::init($rootPath);
// 2. Basic container usage (works now)
$containerInstance = app(); // Get container itself
$hasService = $container->has('some.service'); // Check if service exists
// ===== PHASE 2: Domain Modeling (FUTURE) =====
// 3. Repository interfaces (when implemented in Domain layer)
// $userRepository = app('TorrentPier\Domain\User\Repository\UserRepositoryInterface');
// $torrentRepository = app('TorrentPier\Domain\Tracker\Repository\TorrentRepositoryInterface');
// $forumRepository = app('TorrentPier\Domain\Forum\Repository\ForumRepositoryInterface');
// ===== PHASE 3: Application Services (FUTURE) =====
// 4. Command/Query handlers (when implemented)
// $registerUserHandler = app('TorrentPier\Application\User\Handler\RegisterUserHandler');
// $announceHandler = app('TorrentPier\Application\Tracker\Handler\ProcessAnnounceHandler');
// $createPostHandler = app('TorrentPier\Application\Forum\Handler\CreatePostHandler');
// 5. Making command instances with parameters
// $command = $container->make('TorrentPier\Application\User\Command\RegisterUserCommand', [
// 'username' => 'john_doe',
// 'email' => 'john@example.com',
// 'password' => 'secure_password'
// ]);
// ===== PHASE 4: Infrastructure (FUTURE) =====
// 6. Database and cache (when infrastructure is implemented)
// $database = app('database.connection.default');
// $cache = app('cache.factory')('forum'); // Get cache instance for 'forum' namespace
// ===== PHASE 5: Presentation (FUTURE) =====
// 7. Controllers (when implemented)
// $userController = app('TorrentPier\Presentation\Http\Controllers\Api\UserController');
// $trackerController = app('TorrentPier\Presentation\Http\Controllers\Web\TrackerController');
// ===== TESTING EXAMPLES =====
// 8. Testing with custom container (works now)
$testContainer = ContainerFactory::create([
'definitions' => [
'test.service' => \DI\factory(function () {
return new class {
public function test() { return 'test'; }
};
}),
],
'environment' => 'testing',
]);
// 9. Safe service resolution (works now)
try {
$service = app('optional.service');
} catch (RuntimeException $e) {
// Service not found, handle gracefully
$service = null;
}
// ===== LEGACY INTEGRATION (CURRENT) =====
// 10. Integration with legacy code
// In legacy files, after including common.php or similar:
if (!Bootstrap::getContainer()) {
Bootstrap::init(BB_ROOT ?? __DIR__ . '/../..');
}
// 11. Method injection (works now if service exists)
class ExampleService
{
public function processData(string $data)
{
// Container can inject dependencies when calling this method
return "Processed: $data";
}
}
$exampleService = new ExampleService();
$result = $container->call([$exampleService, 'processData'], [
'data' => 'test data'
]);

View file

@ -0,0 +1,212 @@
# Illuminate Package Integration Guide
This document outlines the integration of Laravel's Illuminate packages in TorrentPier 3.0.
> **Note**: After these changes, run `composer update` to install the newly added `illuminate/config` package.
## Installed Illuminate Packages
The following Illuminate packages are available in TorrentPier:
1. **illuminate/collections** - Powerful array/collection manipulation
2. **illuminate/config** - Configuration repository system
3. **illuminate/container** - Dependency injection container
4. **illuminate/database** - Database query builder and Eloquent ORM
5. **illuminate/events** - Event dispatcher system
6. **illuminate/http** - HTTP request/response handling
7. **illuminate/routing** - Routing system
8. **illuminate/support** - Support utilities (Str, Arr, etc.)
9. **illuminate/validation** - Data validation
## Current Integration Status
### ✅ Fully Integrated
1. **Container (illuminate/container)**
- Used as the core DI container via `bootstrap/container.php`
- Custom wrapper in `App\Container\Container`
- Global `app()` helper function available
2. **Support (illuminate/support)**
- Helper functions: `collect()`, `str()`, `data_get()`, `data_set()`, `tap()`, `optional()`
- String and array manipulation utilities
- Collection class for data manipulation
3. **Events (illuminate/events)**
- Event dispatcher registered in `AppServiceProvider`
- Event classes in `app/Events/`
- Listener classes in `app/Listeners/`
- `EventServiceProvider` for registering event listeners
- Global `event()` helper function
4. **Config (illuminate/config)**
- Configuration repository registered in `AppServiceProvider`
- Global `config()` helper function
- Loads all PHP files from `/config/` directory
5. **Validation (illuminate/validation)**
- Used in `App\Http\Requests\FormRequest` base class
- Provides Laravel-style request validation
### ⚠️ Partially Integrated
1. **HTTP (illuminate/http)**
- Request/Response classes used in routing
- Not fully utilizing all HTTP features
2. **Collections (illuminate/collections)**
- Available via `collect()` helper
- Could be used more extensively in models/services
### ❌ Not Yet Integrated
1. **Database (illuminate/database)**
- Package installed but not used
- Project uses Nette Database instead
- Could migrate to Eloquent ORM in future
2. **Routing (illuminate/routing)**
- Package installed but custom router is used
- Could migrate to full Illuminate routing
## Usage Examples
### Using the Event System
```php
// Dispatch an event
use App\Events\UserRegistered;
event(new UserRegistered(
userId: $userId,
username: $username,
email: $email,
registeredAt: new DateTime()
));
// In a listener
class SendWelcomeEmail
{
public function handle(UserRegistered $event): void
{
// Send welcome email
mail($event->getEmail(), 'Welcome!', 'Welcome to TorrentPier!');
}
}
```
### Using Configuration
```php
// Get a config value
$appName = config('app.name');
$debug = config('app.debug', false);
// Set a config value
config(['app.timezone' => 'UTC']);
// Get entire config array
$databaseConfig = config('database');
```
### Using Collections
```php
// Create a collection
$users = collect($userArray);
// Chain methods
$activeAdmins = $users
->where('status', 'active')
->where('role', 'admin')
->sortBy('name')
->values();
// Use collection helpers
$names = $users->pluck('name');
$grouped = $users->groupBy('role');
```
### Using Validation
```php
use App\Http\Requests\FormRequest;
class RegisterRequest extends FormRequest
{
public function rules(): array
{
return [
'username' => 'required|string|min:3|max:25',
'email' => 'required|email',
'password' => 'required|min:8|confirmed',
];
}
}
// In controller
public function register(RegisterRequest $request)
{
$validated = $request->validated();
// Process registration with validated data
}
```
## Service Providers
Service providers bootstrap the Illuminate packages:
1. **AppServiceProvider** - Registers config and events
2. **EventServiceProvider** - Maps events to listeners
3. **RouteServiceProvider** - Loads route files
Register new providers in `bootstrap/container.php`.
## Future Integration Opportunities
1. **Migrate to Eloquent ORM**
- Replace Nette Database with Eloquent
- Use migrations and model relationships
- Leverage query scopes and mutators
2. **Full Routing Integration**
- Replace custom router with Illuminate routing
- Use route model binding
- Implement route caching
3. **Add Queue System**
- Install `illuminate/queue`
- Process long-running tasks asynchronously
- Integrate with event listeners
4. **Add Cache Integration**
- Install `illuminate/cache`
- Replace custom cache with Laravel's cache
- Use cache tags and TTL
5. **Add Filesystem Integration**
- Already have `league/flysystem`
- Add `illuminate/filesystem` for Laravel integration
- Unified file operations
## Best Practices
1. **Use Dependency Injection**
- Inject services via constructor
- Use the container for resolution
- Avoid service location pattern
2. **Leverage Events**
- Decouple components with events
- Use listeners for side effects
- Consider queued listeners for heavy tasks
3. **Configuration Management**
- Keep environment-specific values in `.env`
- Use config files for application settings
- Cache configuration in production
4. **Follow Laravel Conventions**
- Use Laravel naming conventions
- Structure code like a Laravel app
- Leverage Laravel patterns and practices

View file

@ -1,436 +0,0 @@
# Hexagonal Architecture Directory Structure Specification
## Overview
This document specifies the new hexagonal architecture directory structure for TorrentPier 3.0. The structure follows Domain-Driven Design (DDD) principles and implements a clean separation of concerns through hexagonal architecture (ports and adapters pattern).
## Directory Structure
```
/src/
├── Domain/ # Core business logic - no framework dependencies
│ ├── Forum/ # Forum bounded context
│ │ ├── Model/ # Aggregates and entities
│ │ ├── ValueObject/ # Value objects (PostId, ThreadTitle, etc.)
│ │ ├── Repository/ # Repository interfaces
│ │ └── Exception/ # Domain-specific exceptions
│ ├── Tracker/ # BitTorrent tracker bounded context
│ │ ├── Model/ # Torrent, Peer aggregates
│ │ ├── ValueObject/ # InfoHash, PeerId, etc.
│ │ ├── Repository/ # Repository interfaces
│ │ └── Exception/ # Tracker-specific exceptions
│ ├── User/ # User management bounded context
│ │ ├── Model/ # User aggregate
│ │ ├── ValueObject/ # UserId, Email, Username
│ │ ├── Repository/ # User repository interface
│ │ └── Exception/ # Authentication/authorization exceptions
│ └── Shared/ # Shared kernel - minimal shared concepts
│ ├── Model/ # Base classes (AggregateRoot, Entity)
│ ├── ValueObject/ # Common value objects (Id, DateTime)
│ └── Event/ # Domain events base classes
├── Application/ # Application services - orchestration layer
│ ├── Forum/
│ │ ├── Command/ # Commands (CreatePost, LockThread)
│ │ ├── Query/ # Queries (GetThreadList, SearchPosts)
│ │ └── Handler/ # Command and query handlers
│ ├── Tracker/
│ │ ├── Command/ # Commands (RegisterTorrent, ProcessAnnounce)
│ │ ├── Query/ # Queries (GetPeerList, GetTorrentStats)
│ │ └── Handler/ # Command and query handlers
│ └── User/
│ ├── Command/ # Commands (RegisterUser, ChangePassword)
│ ├── Query/ # Queries (GetUserProfile, SearchUsers)
│ └── Handler/ # Command and query handlers
├── Infrastructure/ # External concerns and implementations
│ ├── Persistence/ # Data persistence layer
│ │ ├── Database/ # Database adapter and connection management
│ │ ├── Migration/ # Database migrations
│ │ └── Repository/ # Repository implementations
│ ├── Cache/ # Caching implementations
│ │ ├── Redis/ # Redis adapter
│ │ ├── Memcached/ # Memcached adapter
│ │ └── File/ # File-based cache adapter
│ ├── Email/ # Email service implementations
│ │ ├── Template/ # Email templates
│ │ └── Transport/ # SMTP, API transports
│ └── FileStorage/ # File storage abstractions
│ ├── Local/ # Local filesystem storage
│ └── S3/ # AWS S3 storage adapter
└── Presentation/ # User interface layer
├── Http/ # Web interface
│ ├── Controllers/ # HTTP controllers
│ │ ├── Admin/ # Admin panel controllers
│ │ ├── Api/ # REST API controllers
│ │ └── Web/ # Web UI controllers
│ ├── Middleware/ # HTTP middleware (auth, CORS, etc.)
│ ├── Requests/ # Request DTOs and validation
│ └── Responses/ # Response transformers
└── Cli/ # Command line interface
└── Commands/ # Console commands
# Additional directories (outside /src/)
/config/ # Application configuration
├── app.php # Main application settings
├── database.php # Database connections
├── cache.php # Cache drivers configuration
├── tracker.php # BitTorrent tracker settings
└── environments/ # Environment-specific overrides
/tests/ # Test suites (Pest)
├── Unit/ # Unit tests (mirrors src/ structure)
├── Feature/ # Feature/Integration tests
├── Pest.php # Pest configuration
└── TestCase.php # Base test case
```
## Directory README.md Templates
### Domain Layer READMEs
#### `/src/Domain/README.md`
```markdown
# Domain Layer
This directory contains the core business logic of TorrentPier. Code here should:
- Have no dependencies on frameworks or infrastructure
- Represent pure business rules and domain models
- Be testable in isolation
- Use only PHP language features and domain concepts
## Bounded Contexts
- **Forum**: Discussion forums, posts, threads
- **Tracker**: BitTorrent tracking, peers, torrents
- **User**: User management, authentication, profiles
- **Shared**: Minimal shared concepts between contexts
```
#### `/src/Domain/Tracker/Model/README.md`
```markdown
# Tracker Domain Models
Contains aggregate roots and entities for the BitTorrent tracker:
- `Torrent`: Aggregate root for torrent management
- `Peer`: Entity representing a BitTorrent peer
- `TorrentStatistics`: Value object for torrent stats
Example:
```php
class Torrent extends AggregateRoot
{
public function announce(Peer $peer, AnnounceEvent $event): void
{
// Business logic for handling announces
}
}
```
#### `/src/Domain/Tracker/ValueObject/README.md`
```markdown
# Tracker Value Objects
Immutable objects representing domain concepts:
- `InfoHash`: 20-byte torrent identifier
- `PeerId`: Peer client identifier
- `Port`: Network port (1-65535)
- `BytesTransferred`: Upload/download bytes
Example:
```php
final class InfoHash
{
private string $hash;
public function __construct(string $hash)
{
$this->guardAgainstInvalidHash($hash);
$this->hash = $hash;
}
}
```
### Application Layer READMEs
#### `/src/Application/README.md`
```markdown
# Application Layer
Contains application services that orchestrate domain objects to fulfill use cases.
- Commands: Write operations that change state
- Queries: Read operations for data retrieval
- Handlers: Process commands and queries
This layer should:
- Coordinate domain objects
- Handle transactions
- Dispatch domain events
- Not contain business logic
```
#### `/src/Application/Tracker/Command/README.md`
```markdown
# Tracker Commands
Commands representing write operations:
- `RegisterTorrentCommand`: Register new torrent
- `UpdateTorrentCommand`: Modify torrent details
- `DeleteTorrentCommand`: Remove torrent from tracker
Example:
```php
final class RegisterTorrentCommand
{
public function __construct(
public readonly string $infoHash,
public readonly int $uploaderId,
public readonly string $name,
public readonly int $size
) {}
}
```
### Infrastructure Layer READMEs
#### `/src/Infrastructure/README.md`
```markdown
# Infrastructure Layer
Technical implementations and external service adapters:
- Database persistence
- Caching mechanisms
- Email services
- File storage
- Third-party integrations
Infrastructure depends on domain, not vice versa.
```
#### `/src/Infrastructure/Persistence/Repository/README.md`
```markdown
# Repository Implementations
Concrete implementations of domain repository interfaces:
- Uses database adapter for persistence
- Implements caching strategies
- Handles query optimization
- Supports multiple database backends
Example:
```php
class TorrentRepository implements TorrentRepositoryInterface
{
public function __construct(
private DatabaseAdapterInterface $db
) {}
public function findByInfoHash(InfoHash $infoHash): ?Torrent
{
// Database adapter implementation
$row = $this->db->select('torrents')
->where('info_hash', $infoHash->toString())
->first();
return $row ? $this->hydrateFromRow($row) : null;
}
}
```
### Presentation Layer READMEs
#### `/src/Presentation/README.md`
```markdown
# Presentation Layer
User interface implementations:
- HTTP controllers for web and API
- CLI commands for console operations
- Request/response handling
- Input validation
- Output formatting
This layer translates between external format and application format.
```
#### `/src/Presentation/Http/Controllers/Api/README.md`
```markdown
# API Controllers
RESTful API endpoints following OpenAPI specification:
- JSON request/response format
- Proper HTTP status codes
- HATEOAS where applicable
- Rate limiting aware
Example:
```php
class UserController
{
public function register(RegisterRequest $request): JsonResponse
{
$command = new RegisterUserCommand(
$request->getUsername(),
$request->getEmail(),
$request->getPassword()
);
$userId = $this->commandBus->handle($command);
return new JsonResponse([
'id' => $userId,
'username' => $request->getUsername()
], Response::HTTP_CREATED);
}
}
```
#### `/src/Presentation/Http/Controllers/Admin/README.md`
```markdown
# Admin Panel Controllers
Administrative interface controllers with enhanced security:
- Role-based access control (RBAC)
- Audit logging for all actions
- Additional authentication checks
- Administrative dashboards and reports
Example:
```php
class AdminUserController
{
public function index(Request $request): Response
{
$query = new GetUsersQuery(
page: $request->getPage(),
filters: $request->getFilters()
);
$users = $this->queryBus->handle($query);
return $this->render('admin/users/index', [
'users' => $users,
'filters' => $request->getFilters()
]);
}
}
```
#### `/config/README.md`
```markdown
# Application Configuration
System configuration files using PHP arrays for type safety and IDE support:
- **app.php**: Core application settings
- Site name, URL, timezone
- Debug mode, environment
- Feature flags and toggles
- **database.php**: Database connection settings
- Multiple connection definitions
- Read/write splitting configuration
- Connection pooling settings
- **cache.php**: Cache driver configurations
- Redis, Memcached, file-based settings
- TTL defaults per cache type
- Cache key prefixes
- **tracker.php**: BitTorrent tracker settings
- Announce intervals
- Peer limits
- Ratio requirements
- **environments/**: Environment-specific overrides
- Development, staging, production settings
- Local developer configurations
Example database configuration:
```php
<?php
return [
'default' => env('DB_CONNECTION', 'mysql'),
'connections' => [
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', 3306),
'database' => env('DB_DATABASE', 'tp'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'options' => [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
],
],
],
];
```
## Implementation Order
1. **Phase 1: Foundation**
- Create directory structure
- Set up base classes in Domain/Shared
- Configure dependency injection
2. **Phase 2: Domain Modeling**
- Implement core aggregates
- Create value objects
- Define repository interfaces
3. **Phase 3: Application Services**
- Create commands and queries
- Implement handlers
- Set up event dispatching
4. **Phase 4: Infrastructure**
- Implement repositories
- Configure database adapter
- Set up caching
5. **Phase 5: Presentation**
- Create controllers
- Implement middleware
- Build CLI commands
## Migration Strategy
- Existing code remains in current locations
- New features built in hexagonal architecture
- Gradual migration using strangler fig pattern
- Legacy adapters bridge old and new code
- Feature flags control rollout
## Key Principles
1. **Dependency Rule**: Dependencies point inward (Presentation → Application → Domain)
2. **Domain Isolation**: Business logic has no framework dependencies
3. **Interface Segregation**: Small, focused interfaces
4. **CQRS**: Separate read and write models
5. **Event-Driven**: Domain events for cross-context communication
## Testing Strategy
- **Domain**: Pure unit tests, no mocks needed
- **Application**: Unit tests with mocked repositories
- **Infrastructure**: Integration tests with real services
- **Presentation**: E2E tests for user journeys
## Notes for Developers
- Start reading code from the Domain layer
- Business rules live in aggregates, not services
- Use value objects for type safety
- Prefer composition over inheritance
- Keep bounded contexts loosely coupled

View file

@ -0,0 +1,496 @@
# MVC Architecture Directory Structure Specification
## Overview
This document specifies the MVC (Model-View-Controller) architecture directory structure for TorrentPier 3.0. The structure follows a simple, Laravel-inspired approach that prioritizes developer familiarity and ease of maintenance over complex enterprise patterns.
## Directory Structure
```
# Laravel-style root structure
/app/ # Application code (PSR-4: App\)
├── Console/ # Console commands
│ └── Commands/ # Artisan-style commands for Dexter
├── Http/ # HTTP layer
│ ├── Controllers/ # Controllers
│ │ ├── Admin/ # Admin panel controllers
│ │ ├── Api/ # API controllers
│ │ └── Web/ # Web controllers
│ ├── Middleware/ # HTTP middleware
│ └── Requests/ # Form request validation
├── Models/ # Data models (using Nette Database)
│ ├── Forum/ # Forum models (Post, Thread, Forum)
│ ├── Tracker/ # Tracker models (Torrent, Peer, Announce)
│ └── User/ # User models (User, Profile, Permission)
├── Services/ # Business logic services
│ ├── Forum/ # Forum services
│ ├── Tracker/ # Tracker services
│ └── User/ # User services
├── Repositories/ # Data access layer (optional)
├── Events/ # Event classes
├── Listeners/ # Event listeners
├── Jobs/ # Background jobs
├── Mail/ # Mailable classes
├── Notifications/ # Notification classes
└── Exceptions/ # Exception handling
/bootstrap/ # Application bootstrap
├── app.php # Application bootstrap
├── console.php # Console bootstrap
└── cache/ # Bootstrap cache
/config/ # Configuration files
├── app.php # Application config
├── auth.php # Authentication config
├── cache.php # Cache configuration
├── database.php # Database connections
├── filesystems.php # File storage config
├── tracker.php # BitTorrent tracker settings
└── (legacy configs...) # Existing config files
/database/ # Database files
├── migrations/ # Database migrations (moved from /migrations/)
├── factories/ # Model factories for testing
└── seeders/ # Database seeders
/public/ # Public web root
├── index.php # Front controller
├── css/ # Public CSS
├── js/ # Public JavaScript
├── images/ # Public images
└── fonts/ # Web fonts
/resources/ # Resources
├── views/ # View templates
│ ├── admin/ # Admin panel views
│ ├── forum/ # Forum views
│ ├── tracker/ # Tracker views
│ └── layouts/ # Layout templates
├── lang/ # Language files
├── js/ # JavaScript source
└── css/ # CSS/SCSS source
/routes/ # Route definitions
├── web.php # Web routes
├── api.php # API routes
├── admin.php # Admin routes
└── console.php # Console routes
/src/ # Framework/Infrastructure code (PSR-4: TorrentPier\)
├── Database/ # Database abstraction
├── Cache/ # Cache system
├── Infrastructure/ # DI container, HTTP routing, etc.
├── Legacy/ # Legacy code adapters
└── helpers.php # Global helper functions
/storage/ # Storage directory
├── app/ # Application storage
│ ├── public/ # Publicly accessible files
│ └── private/ # Private files
├── framework/ # Framework storage
│ ├── cache/ # File cache
│ ├── sessions/ # Session files
│ └── views/ # Compiled view cache
└── logs/ # Application logs
/tests/ # Test suites
├── Feature/ # Feature tests
├── Unit/ # Unit tests
└── TestCase.php # Base test case
# Legacy directories (being migrated)
/library/ # Legacy core code
/controllers/ # Legacy PHP controllers
/admin/ # Legacy admin interface
/styles/ # Legacy templates/assets
/internal_data/ # Legacy cache/logs
# Root files
.env # Environment variables
.env.example # Environment example
composer.json # Dependencies (App\ and TorrentPier\ namespaces)
dexter # CLI interface
index.php # Legacy entry point (redirects to public/)
```
## Directory README.md Templates
### Application Layer READMEs
#### `/app/README.md`
```markdown
# Application Directory
This directory contains the core application code following MVC pattern:
- **Models**: Database models and business entities
- **Controllers**: Handle HTTP requests and responses
- **Services**: Business logic and application services
- **Console**: CLI commands for maintenance and operations
## Key Components
- **Http**: Web and API controllers, middleware, requests
- **Models**: Database models using Nette Database
- **Services**: Reusable business logic
- **Events**: Application events and listeners
```
#### `/app/Models/Tracker/README.md`
```markdown
# Tracker Models
Database models for BitTorrent tracker functionality:
- `Torrent`: Torrent information and metadata
- `Peer`: Active peers in swarms
- `Announce`: Announce history and statistics
Example:
```php
class Torrent extends Model
{
protected string $table = 'bb_torrents';
public function getPeers(): array
{
return $this->db->table('bb_peers')
->where('torrent_id', $this->id)
->fetchAll();
}
public function getUser(): ?User
{
return User::find($this->user_id);
}
}
```
#### `/app/Services/Tracker/README.md`
```markdown
# Tracker Services
Business logic for tracker operations:
- `AnnounceService`: Handle peer announces
- `ScrapeService`: Provide torrent statistics
- `TorrentService`: Torrent management operations
Example:
```php
class AnnounceService
{
public function __construct(
private TorrentRepository $torrents,
private PeerRepository $peers
) {}
public function handleAnnounce(string $infoHash, array $data): array
{
$torrent = $this->torrents->findByInfoHash($infoHash);
$peers = $this->peers->getActivePeers($torrent->id);
return ['peers' => $peers, 'interval' => 900];
}
}
```
### Controllers READMEs
#### `/app/Http/Controllers/README.md`
```markdown
# Controllers
HTTP controllers following RESTful conventions:
- Accept HTTP requests
- Validate input
- Call services for business logic
- Return appropriate responses
Controllers should be thin - delegate business logic to services.
```
#### `/app/Http/Controllers/Web/TrackerController.php`
```markdown
# Tracker Web Controller
Handles web interface for tracker functionality:
Example:
```php
class TrackerController extends Controller
{
public function __construct(
private TorrentService $torrentService
) {}
public function index(Request $request)
{
$torrents = $this->torrentService->paginate(
$request->get('page', 1),
$request->get('category')
);
return view('tracker.index', compact('torrents'));
}
public function store(StoreTorrentRequest $request)
{
$torrent = $this->torrentService->create(
$request->validated(),
$request->user()
);
return redirect()->route('torrents.show', $torrent);
}
}
```
### Services Layer READMEs
#### `/app/Services/README.md`
```markdown
# Services
Reusable business logic organized by feature:
- Encapsulate complex operations
- Coordinate between models
- Handle external integrations
- Maintain single responsibility
Services are injected into controllers and commands.
```
#### `/app/Repositories/README.md`
```markdown
# Repositories (Optional)
Data access layer for complex queries:
- Abstracts database queries from models
- Implements caching strategies
- Handles query optimization
Example:
```php
class TorrentRepository
{
public function __construct(
private Database $db,
private CacheManager $cache
) {}
public function findByInfoHash(string $infoHash): ?Torrent
{
return $this->cache->remember("torrent:{$infoHash}", 3600, function() use ($infoHash) {
$data = $this->db->table('bb_torrents')
->where('info_hash', $infoHash)
->fetch();
return $data ? new Torrent($data) : null;
});
}
public function getPopularTorrents(int $limit = 10): array
{
return $this->db->table('bb_torrents')
->select('torrents.*, COUNT(peers.id) as peer_count')
->leftJoin('bb_peers', 'peers.torrent_id = torrents.id')
->groupBy('torrents.id')
->orderBy('peer_count DESC')
->limit($limit)
->fetchAll();
}
}
```
### Views READMEs
#### `/resources/views/README.md`
```markdown
# Views
Template files for rendering HTML:
- Layouts for consistent structure
- Partials for reusable components
- Feature-specific views
- Email templates
Using PHP templates with simple helper functions.
```
#### `/app/Http/Controllers/Api/README.md`
```markdown
# API Controllers
RESTful API endpoints:
- JSON request/response format
- Proper HTTP status codes
- API versioning support
- Rate limiting aware
Example:
```php
class UserController extends Controller
{
public function __construct(
private UserService $userService
) {}
public function register(Request $request): JsonResponse
{
$validatedData = $this->validate($request, [
'username' => 'required|unique:users',
'email' => 'required|email|unique:users',
'password' => 'required|min:8'
]);
$user = $this->userService->register($validatedData);
return response()->json([
'id' => $user->id,
'username' => $user->username
], 201);
}
}
```
#### `/app/Http/Controllers/Admin/README.md`
```markdown
# Admin Panel Controllers
Administrative interface controllers:
- Protected by admin middleware
- Activity logging
- Bulk operations support
- Dashboard and reports
Example:
```php
class AdminUserController extends Controller
{
public function __construct(
private UserService $userService
) {
$this->middleware('admin');
}
public function index(Request $request)
{
$query = $request->get('search');
$users = $this->userService->searchUsers($query)
->paginate(20);
return view('admin.users.index', compact('users'));
}
}
```
#### `/config/README.md`
```markdown
# Application Configuration
System configuration files using PHP arrays:
- **app.php**: Core application settings
- Site name, URL, timezone
- Debug mode, environment
- Feature flags and toggles
- **database.php**: Database connection settings
- Multiple connection definitions
- Read/write splitting configuration
- Connection pooling settings
- **cache.php**: Cache driver configurations
- Redis, Memcached, file-based settings
- TTL defaults per cache type
- Cache key prefixes
- **tracker.php**: BitTorrent tracker settings
- Announce intervals
- Peer limits
- Ratio requirements
Example database configuration:
```php
<?php
return [
'default' => env('DB_CONNECTION', 'mysql'),
'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',
'options' => [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
],
],
],
];
```
## Implementation Order
1. **Phase 1: Foundation**
- Create directory structure
- Set up routing files
- Configure service providers
- Set up middleware
2. **Phase 2: Models & Database**
- Create database models
- Define relationships
- Set up migrations
- Create seeders
3. **Phase 3: Services & Business Logic**
- Implement service classes
- Create repositories (if needed)
- Set up events and listeners
4. **Phase 4: Controllers & Routes**
- Create web controllers
- Build API endpoints
- Set up admin controllers
- Define routes
5. **Phase 5: Views & Frontend**
- Create template layouts
- Build view components
- Set up assets pipeline
## Migration Strategy
- Move existing controllers to /app/Http/Controllers/Legacy
- Gradually rewrite to new MVC structure
- Use service classes to encapsulate business logic
- Maintain backward compatibility through routing
- Progressive enhancement approach
## Key Principles
1. **Simplicity**: Straightforward MVC pattern
2. **Convention over Configuration**: Consistent naming and structure
3. **Fat Models, Skinny Controllers**: Business logic in models/services
4. **Service Layer**: Complex operations in service classes
5. **Repository Pattern**: Optional for complex queries
## Testing Strategy
- **Unit Tests**: Models and services
- **Feature Tests**: HTTP endpoints and user flows
- **Integration Tests**: Database and external services
- **Browser Tests**: Critical user journeys
## Notes for Developers
- Keep controllers thin, move logic to services
- Use dependency injection for testability
- Use Nette Database for data access
- Write readable code over clever code
- Focus on maintainability

View file

@ -1,331 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
define('BB_ROOT', __DIR__ . DIRECTORY_SEPARATOR);
define('BB_PATH', BB_ROOT);
// Check CLI mode
if (PHP_SAPI != 'cli') {
die('Please run <code style="background:#222;color:#00e01f;padding:2px 6px;border-radius:3px;">php ' . basename(__FILE__) . '</code> in CLI mode');
}
// Get all constants
require_once BB_ROOT . 'library/defines.php';
// Include CLI functions
require INC_DIR . '/functions_cli.php';
/**
* System requirements
*/
const CHECK_REQUIREMENTS = [
'php_min_version' => '8.3.0',
'ext_list' => [
'json',
'curl',
'readline',
'mysqli',
'bcmath',
'mbstring',
'intl',
'xml',
'xmlwriter',
'zip',
'gd'
],
];
// Welcoming message
out("--- TorrentPier Installer ---\n", 'info');
// Checking extensions
out("- Checking installed extensions...", 'info');
// [1] Check PHP Version
if (!version_compare(PHP_VERSION, CHECK_REQUIREMENTS['php_min_version'], '>=')) {
out("- TorrentPier requires PHP version " . CHECK_REQUIREMENTS['php_min_version'] . "+ Your PHP version " . PHP_VERSION, 'warning');
}
// [2] Check installed PHP Extensions on server
foreach (CHECK_REQUIREMENTS['ext_list'] as $ext) {
if (!extension_loaded($ext)) {
out("- ext-$ext not installed. Check out php.ini file", 'error');
if (!defined('EXTENSIONS_NOT_INSTALLED')) {
define('EXTENSIONS_NOT_INSTALLED', true);
}
} else {
out("- ext-$ext installed!");
}
}
if (!defined('EXTENSIONS_NOT_INSTALLED')) {
out("- All extensions are installed!\n", 'success');
} else {
exit;
}
// Check if already installed
if (is_file(BB_ROOT . '.env')) {
out('- TorrentPier already installed', 'warning');
echo 'Are you sure want to re-install TorrentPier? [y/N]: ';
if (str_starts_with(mb_strtolower(trim(readline())), 'y')) {
out("\n- Re-install process started...", 'info');
// environment
if (is_file(BB_ROOT . '.env')) {
if (unlink(BB_ROOT . '.env')) {
out('- Environment file successfully removed!');
} else {
out('- Cannot remove environment (.env) file. Delete it manually', 'error');
exit;
}
}
// composer.phar
if (is_file(BB_ROOT . 'composer.phar')) {
if (unlink(BB_ROOT . 'composer.phar')) {
out("- composer.phar file successfully removed!");
} else {
out('- Cannot remove composer.phar file. Delete it manually', 'error');
exit;
}
}
// composer dir
if (is_dir(BB_ROOT . 'vendor')) {
removeDir(BB_ROOT . 'vendor', true);
if (!is_dir(BB_ROOT . 'vendor')) {
out("- Composer directory successfully removed!");
} else {
out('- Cannot remove Composer directory. Delete it manually', 'error');
exit;
}
}
out("- Re-install process completed!\n", 'success');
out('- Starting installation...', 'info');
} else {
exit;
}
}
// Applying permissions
out("- Applying permissions for folders...", 'info');
chmod_r(BB_ROOT . 'data', 0755, 0644);
chmod_r(BB_ROOT . 'internal_data', 0755, 0644);
chmod_r(BB_ROOT . 'sitemap', 0755, 0644);
out("- Permissions successfully applied!\n", 'success');
// Check composer installation
if (!is_file(BB_ROOT . 'vendor/autoload.php')) {
out('- Hmm, it seems there are no Composer dependencies', 'info');
// Downloading composer
if (!is_file(BB_ROOT . 'composer.phar')) {
out('- Downloading Composer...', 'info');
if (copy('https://getcomposer.org/installer', BB_ROOT . 'composer-setup.php')) {
out("- Composer successfully downloaded!\n", 'success');
runProcess('php ' . BB_ROOT . 'composer-setup.php --install-dir=' . BB_ROOT);
} else {
out('- Cannot download Composer. Please, download it (composer.phar) manually', 'error');
exit;
}
if (is_file(BB_ROOT . 'composer-setup.php')) {
if (unlink(BB_ROOT . 'composer-setup.php')) {
out("- Composer installation file successfully removed!\n", 'success');
} else {
out('- Cannot remove Composer installation file (composer-setup.php). Please, delete it manually', 'warning');
}
}
} else {
out("- composer.phar file found!\n", 'success');
}
// Installing dependencies
if (is_file(BB_ROOT . 'composer.phar')) {
out('- Installing dependencies...', 'info');
runProcess('php ' . BB_ROOT . 'composer.phar install --no-interaction --no-ansi');
define('COMPOSER_COMPLETED', true);
} else {
out('- composer.phar not found. Please, download it (composer.phar) manually', 'error');
exit;
}
} else {
out('- Composer dependencies are present!', 'success');
out("- Note: Remove 'vendor' folder if you want to re-install dependencies\n");
}
// Check composer dependencies
if (defined('COMPOSER_COMPLETED')) {
if (is_file(BB_ROOT . 'vendor/autoload.php')) {
out("- Completed! Composer dependencies are installed!\n", 'success');
} else {
exit;
}
}
// Preparing ENV
if (is_file(BB_ROOT . '.env.example') && !is_file(BB_ROOT . '.env')) {
if (copy(BB_ROOT . '.env.example', BB_ROOT . '.env')) {
out("- Environment file created!\n", 'success');
} else {
out('- Cannot create environment file', 'error');
exit;
}
}
// Editing ENV file
$DB_HOST = 'localhost';
$DB_PORT = 3306;
$DB_DATABASE = '';
$DB_USERNAME = '';
$DB_PASSWORD = '';
if (is_file(BB_ROOT . '.env')) {
out("--- Configuring TorrentPier ---", 'info');
$envContent = file_get_contents(BB_ROOT . '.env');
if ($envContent === false) {
out('- Cannot open environment file', 'error');
exit;
}
$envLines = explode("\n", $envContent);
$editedLines = [];
foreach ($envLines as $line) {
if (trim($line) !== '' && !str_starts_with($line, '#')) {
$parts = explode('=', $line, 2);
$key = trim($parts[0]);
$value = (!empty($parts[1]) && $key !== 'DB_PASSWORD') ? trim($parts[1]) : '';
out("\nCurrent value of $key: $value", 'debug');
echo "Enter a new value for $key (or leave empty to not change): ";
$newValue = trim(readline());
if (!empty($newValue) || $key === 'DB_PASSWORD') {
if ($key === 'TP_HOST') {
if (!preg_match('/^https?:\/\//', $newValue)) {
$newValue = 'https://' . $newValue;
}
$newValue = parse_url($newValue, PHP_URL_HOST);
}
$line = "$key=$newValue";
$$key = $newValue;
} else {
$$key = $value;
}
}
$editedLines[] = $line;
}
$newEnvContent = implode("\n", $editedLines);
if (file_put_contents(BB_ROOT . '.env', $newEnvContent)) {
out("- TorrentPier successfully configured!\n", 'success');
} else {
out('- Cannot save environment file', 'error');
exit;
}
} else {
out('- Environment file not found', 'error');
exit;
}
if (!empty($DB_HOST) && !empty($DB_DATABASE) && !empty($DB_USERNAME)) {
out("--- Checking environment settings ---\n", 'info');
// Connecting to database
out("- Trying connect to MySQL...", 'info');
// Checking mysqli extension installed
if (!extension_loaded('mysqli')) {
out('- ext-mysqli not found. Check out php.ini file', 'error');
exit;
}
// Connect to MySQL server
try {
$conn = new mysqli($DB_HOST, $DB_USERNAME, $DB_PASSWORD, port: $DB_PORT);
} catch (mysqli_sql_exception $exception) {
out("- Connection failed: {$exception->getMessage()}", 'error');
exit;
}
if (!$conn->connect_error) {
out('- Connected successfully!', 'success');
}
// Creating database if not exist
if ($conn->query("CREATE DATABASE IF NOT EXISTS $DB_DATABASE CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")) {
out('- Database created successfully!', 'success');
} else {
out("- Cannot create database: $DB_DATABASE", 'error');
exit;
}
$conn->select_db($DB_DATABASE);
// Close database connection - migrations will handle their own connections
$conn->close();
// Run database migrations
out('- Setting up database using migrations...', 'info');
// Check if phinx.php exists
if (!is_file(BB_ROOT . 'phinx.php')) {
out('- Migration configuration (phinx.php) not found', 'error');
exit;
}
// Run migrations
$migrationResult = runProcess('php vendor/bin/phinx migrate --configuration=' . BB_ROOT . 'phinx.php');
if ($migrationResult !== 0) {
out('- Database migration failed', 'error');
exit;
}
out("- Database setup completed!\n", 'success');
// Autofill host in robots.txt
$robots_txt_file = BB_ROOT . 'robots.txt';
if (isset($TP_HOST) && is_file($robots_txt_file)) {
$content = file_get_contents($robots_txt_file);
$content = str_replace('example.com', $TP_HOST, $content);
file_put_contents($robots_txt_file, $content);
}
if (isset($APP_ENV) && $APP_ENV === 'local') {
if (!is_file(BB_ROOT . 'library/config.local.php')) {
if (copy(BB_ROOT . 'library/config.php', BB_ROOT . 'library/config.local.php')) {
out('- Local configuration file created!', 'success');
} else {
out('- Cannot create library/config.local.php file. You can create it manually, just copy config.php and rename it to config.local.php', 'warning');
}
}
} else {
if (rename(__FILE__, __FILE__ . '_' . hash('xxh128', time()))) {
out("- Installation file renamed!", 'success');
} else {
out('- Cannot rename installation file (' . __FILE__ . '). Please, rename it manually for security reasons', 'warning');
}
}
// Cleanup...
if (is_file(BB_ROOT . '_cleanup.php')) {
out("\n--- Finishing installation (Cleanup) ---\n", 'info');
out('The cleanup process will remove:');
out('- Development documentation (README, CHANGELOG)', 'debug');
out('- Git configuration files', 'debug');
out('- CI/CD pipelines and code analysis tools', 'debug');
out('- Translation and contribution guidelines', 'debug');
echo 'Do you want to delete these files permanently? [y/N]: ';
if (str_starts_with(mb_strtolower(trim(readline())), 'y')) {
out("\n- Cleanup...", 'info');
require_once BB_ROOT . '_cleanup.php';
unlink(BB_ROOT . '_cleanup.php');
} else {
out('- Skipping...', 'info');
}
}
out("\n- Voila! Good luck & have fun!", 'success');
}

View file

@ -1,27 +0,0 @@
# Example Caddy configuration for TorrentPier
example.com {
root * /path/to/root
encode gzip zstd
php_fastcgi unix//run/php/php-fpm.sock
try_files {path} {path}/ /index.php?{query}
file_server
@blocked {
path /install/* /internal_data/* /library/*
path /.ht* /.en*
path /.git/*
path *.sql *.tpl *.db *.inc *.log *.md
}
respond @blocked 404
redir /sitemap.xml /sitemap/sitemap.xml
@html_css_js {
path *.html *.css *.js *.json *.xml *.txt
}
header @html_css_js Content-Type "{mime}; charset=utf-8"
}
# Refer to the Caddy docs for more information:
# https://caddyserver.com/docs/caddyfile

View file

@ -1,39 +0,0 @@
# Example nginx configuration for TorrentPier
server {
listen 80; # port
server_name example.com; # your domain
root /path/to/root; # folder with TorrentPier installed
index index.php;
charset utf-8;
location / {
try_files \$uri \$uri/ /index.php?\$args;
}
location ~ \/(install|internal_data|library)\/ {
return 404;
}
location ~ /\.(ht|en) {
return 404;
}
location ~ /\.git {
return 404;
}
location ~ \.(.*sql|tpl|db|inc|log|md)$ {
return 404;
}
rewrite ^/sitemap.xml$ /sitemap/sitemap.xml;
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
include fastcgi_params;
}
}

View file

@ -13,7 +13,7 @@ if (!defined('IN_AJAX')) {
global $userdata, $lang; global $userdata, $lang;
if (!config()->get('callseed')) { if (!tp_config()->get('callseed')) {
$this->ajax_die($lang['MODULE_OFF']); $this->ajax_die($lang['MODULE_OFF']);
} }
@ -32,7 +32,7 @@ if ($t_data['seeders'] >= 3) {
} elseif ($t_data['call_seed_time'] >= (TIMENOW - 86400)) { } elseif ($t_data['call_seed_time'] >= (TIMENOW - 86400)) {
$time_left = delta_time($t_data['call_seed_time'] + 86400, TIMENOW, 'days'); $time_left = delta_time($t_data['call_seed_time'] + 86400, TIMENOW, 'days');
$this->ajax_die(sprintf($lang['CALLSEED_MSG_SPAM'], $time_left)); $this->ajax_die(sprintf($lang['CALLSEED_MSG_SPAM'], $time_left));
} elseif (isset(config()->get('tor_no_tor_act')[$t_data['tor_status']])) { } elseif (isset(tp_config()->get('tor_no_tor_act')[$t_data['tor_status']])) {
$this->ajax_die($lang['NOT_AVAILABLE']); $this->ajax_die($lang['NOT_AVAILABLE']);
} }

Some files were not shown because too many files have changed in this diff Show more