diff --git a/.cliffignore b/.cliffignore deleted file mode 100644 index 187668fd1..000000000 --- a/.cliffignore +++ /dev/null @@ -1,7 +0,0 @@ -9766c534bddad8e82e6d19f9bad5cf70b9887f9a -92ce77ec0ec703c08a659419087a373f76e711f7 -2d53efc945c7747be1755d0b66557a86bdc12cbd -602137b65129b817811b80975a369ebde3270c6d -4eb26ae37e1f4c82a45961517ffeb54c20200408 -e59adce848a9e10ee5775254045cbbd915236b8b -9e0a64108d62236ab07b3f8d10e8c78269b8e1d1 diff --git a/.github/ISSUE_TEMPLATE/feature---enhancement-request.md b/.github/ISSUE_TEMPLATE/feature---enhancement-request.md deleted file mode 100644 index 9f68fc3a6..000000000 --- a/.github/ISSUE_TEMPLATE/feature---enhancement-request.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: Feature / Enhancement request -about: Suggest an idea for TorrentPier -title: "[Feature]" -labels: [Feature, Enhancement] -assignees: '' ---- diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml deleted file mode 100644 index 8e256db33..000000000 --- a/.github/workflows/cd.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 424e53a1f..000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/phpmd.yml b/.github/workflows/phpmd.yml deleted file mode 100644 index 3e06d7538..000000000 --- a/.github/workflows/phpmd.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/schedule.yml b/.github/workflows/schedule.yml deleted file mode 100644 index c1ad4f3c1..000000000 --- a/.github/workflows/schedule.yml +++ /dev/null @@ -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 diff --git a/.htaccess b/.htaccess deleted file mode 100644 index a689fba84..000000000 --- a/.htaccess +++ /dev/null @@ -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 - -Require all denied - diff --git a/CLAUDE.md b/CLAUDE.md index 22af6c3cf..b3b58a20c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,38 +10,69 @@ TorrentPier is a BitTorrent tracker engine written in PHP, designed for hosting - **PHP 8.3+** with modern features - **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 - **Custom BitTorrent tracker** implementation +- **Laravel-style MVC Architecture** with familiar patterns ## Key Directory Structure -- `/src/` - Modern PHP classes (PSR-4 autoloaded as `TorrentPier\`) -- `/library/` - Core application logic and legacy code -- `/admin/` - Administrative interface +### Laravel-style Structure +- `/app/` - Main application directory (PSR-4 autoloaded as `App\`) + - `/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) -- `/styles/` - Templates, CSS, JS, images -- `/internal_data/` - Cache, logs, compiled templates -- `/install/` - Installation scripts and configuration examples -- `/migrations/` - Database migration files (Phinx) +- `/styles/` - Legacy templates, CSS, JS, images +- `/internal_data/` - Legacy cache, logs, compiled templates ## Entry Points & Key Files -- `index.php` - Main forum homepage -- `tracker.php` - Torrent search/browse interface +### Modern Entry Points +- `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/scrape.php` - BitTorrent scrape endpoint -- `admin/index.php` - Administrative panel -- `cron.php` - Background task runner (CLI only) -- `install.php` - Installation script (CLI only) +- `admin/index.php` - Legacy administrative panel +- `cron.php` - Background task runner ## Development Commands ### Installation & Setup ```bash -# Automated installation (CLI) -php install.php - # Install dependencies composer install @@ -58,24 +89,82 @@ php cron.php ### Code Quality 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/`) -- **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 - 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/`) - **Unified caching** using Nette Caching internally - Replaces existing `CACHE()` and $datastore systems - Supports file, SQLite, memory, and Memcached storage -- **API changes planned** for improved developer experience +- Used by services and repositories ### Configuration Management - Environment-based config with `.env` files +- **Illuminate Config** for Laravel-style configuration - 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 - `.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 - **StyleCI** for code style enforcement - **Dependabot** for dependency updates -- **FTP deployment** to demo environment ### Installation Methods -1. **Automated**: `php install.php` (recommended) -2. **Composer**: `composer create-project torrentpier/torrentpier` -3. **Manual**: Git clone + `composer install` + database setup +1. **Composer**: `composer create-project torrentpier/torrentpier` +2. **Manual**: Git clone + `composer install` ## 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 seed data: `20250619000002_seed_initial_data.php` - UTF-8 (utf8mb4) character set required @@ -118,25 +205,132 @@ php vendor/bin/phinx migrate --fake --configuration=phinx.php ## 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 -- **Clean architecture**: Separation of concerns, dependency injection +- **Developer friendly**: Familiar Laravel patterns for easier contribution - **Performance improvements**: Optimized database queries, efficient caching -- **Developer experience**: Better debugging, testing, and maintenance - **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. -## Migration Path for 3.0 +## Current Architecture -- **Database layer**: Legacy SqlDb calls will be removed, migrate to new Database class -- **Cache system**: Replace existing CACHE() and $datastore calls with new unified API -- **Configuration**: Update legacy global $bb_cfg access to use config() singleton -- **Templates**: Legacy template syntax may be deprecated in favor of modern Twig features -- **Language system**: Update global $lang usage to new Language singleton methods +### Container & Dependency Injection +- **Illuminate Container**: Laravel's container for dependency injection +- **Bootstrap**: Clean container setup in `/bootstrap/container.php` +- **Service Providers**: Laravel-style providers in `/app/Providers/` +- **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 diff --git a/README.md b/README.md index 5e7a13049..989eaa4aa 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,143 @@ TorrentPier includes a comprehensive testing suite built with **Pest PHP**. Run For detailed testing documentation, see [tests/README.md](tests/README.md). +## 🖥️ CLI Usage + +TorrentPier 3.0 includes a Laravel-style CLI tool called **Dexter** for managing your application: + +```shell +# Basic usage +php dexter # Shows available commands +php dexter info # System information +php dexter cache:clear # Clear caches +php dexter migrate # Run database migrations +php dexter help # Command help +``` + +## 🦌 Laravel Herd Configuration + +If you're using [Laravel Herd](https://herd.laravel.com) for local development, you'll need special configuration to properly serve the legacy `/admin/` and `/bt/` directories. By default, Herd routes all requests through the modern front controller, but these directories need to be served directly. + +### The Problem + +TorrentPier has legacy directories (`/admin/` and `/bt/`) that contain their own `index.php` files and should be processed directly by the web server, not through the Laravel-style routing system. Laravel Herd's default nginx configuration sends all requests to `public/index.php`, which causes these legacy endpoints to fail. + +### Solution: Site-Specific Nginx Configuration + +#### Step 1: Generate Custom Nginx Config + +Run one of these commands to create a site-specific nginx configuration: + +```shell +# Option A: Isolate PHP version +herd isolate php@8.4 + +# Option B: Secure the site with SSL +herd secure +``` + +This generates a custom nginx configuration file at: +`~/Library/Application\ Support/Herd/config/valet/Nginx/[your-site-name]` + +#### Step 2: Edit the Generated Config + +Open the generated nginx configuration file and add these location blocks **before** the main Laravel location block: + +```nginx +# Serve /admin/ directory directly +location /admin/ { + alias /path/to/your/torrentpier/admin/; + index index.php; + + location ~ \.php$ { + fastcgi_pass unix:/opt/homebrew/var/run/php/php8.4-fpm.sock; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $request_filename; + include fastcgi_params; + } +} + +# Serve /bt/ directory directly +location /bt/ { + alias /path/to/your/torrentpier/bt/; + index index.php; + + location ~ \.php$ { + fastcgi_pass unix:/opt/homebrew/var/run/php/php8.4-fpm.sock; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $request_filename; + include fastcgi_params; + } +} +``` + +> **Note**: Replace `/path/to/your/torrentpier/` with the actual path to your TorrentPier installation. + +#### Step 3: Restart Herd + +```shell +herd restart +``` + +### Alternative Solutions + +#### Option 1: Root .htaccess (May work with some Herd configurations) + +Create a `.htaccess` file in your project root: + +```apache +RewriteEngine On + +# Exclude admin and bt directories from Laravel routing +RewriteRule ^admin/ - [L] +RewriteRule ^bt/ - [L] + +# Handle everything else through Laravel +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^(.*)$ public/index.php [L] +``` + +#### Option 2: Move to Public Directory + +Move the directories to the public folder: + +```shell +mv admin/ public/admin/ +mv bt/ public/bt/ +``` + +> **Warning**: This requires updating any hardcoded paths in the legacy code. + +### Testing Your Configuration + +After applying the configuration, test these URLs: + +```shell +# Admin panel should load +curl -I http://your-site.test/admin/ + +# BitTorrent tracker should respond +curl -I http://your-site.test/bt/ + +# Announce endpoint should work +curl -I http://your-site.test/bt/announce.php + +# Modern routes should still work +curl -I http://your-site.test/hello +``` + +All should return HTTP 200 status codes. + +### Troubleshooting + +- **502 Bad Gateway**: Check PHP-FPM socket path in nginx config +- **404 Not Found**: Verify directory paths in nginx location blocks +- **403 Forbidden**: Check file permissions on admin/bt directories +- **Still routing through Laravel**: Ensure location blocks are placed before the main Laravel location block + +For more details about Herd nginx configuration, see the [official Herd documentation](https://herd.laravel.com/docs/macos/sites/nginx-configuration). + ## 📌 Our recommendations * *It's recommended to run `cron.php`.* - For significant tracker speed increase it may be required to replace the built-in cron.php with an operating system daemon. diff --git a/UPGRADE_GUIDE.md b/UPGRADE_GUIDE.md index ed045381f..aae12f127 100644 --- a/UPGRADE_GUIDE.md +++ b/UPGRADE_GUIDE.md @@ -595,8 +595,8 @@ $announceUrl = $bb_cfg['bt_announce_url']; $dbHost = $bb_cfg['database']['host']; // ✅ New way (recommended) -$announceUrl = config()->get('bt_announce_url'); -$dbHost = config()->get('database.host'); +$announceUrl = tp_config()->get('bt_announce_url'); +$dbHost = tp_config()->get('database.host'); ``` ### Key Configuration Changes @@ -604,57 +604,57 @@ $dbHost = config()->get('database.host'); #### Basic Usage ```php // Get configuration values using dot notation -$siteName = config()->get('sitename'); -$dbHost = config()->get('database.host'); -$cacheTimeout = config()->get('cache.timeout'); +$siteName = tp_config()->get('sitename'); +$dbHost = tp_config()->get('database.host'); +$cacheTimeout = tp_config()->get('cache.timeout'); // Get with default value if key doesn't exist -$maxUsers = config()->get('max_users_online', 100); -$debugMode = config()->get('debug.enabled', false); +$maxUsers = tp_config()->get('max_users_online', 100); +$debugMode = tp_config()->get('debug.enabled', false); ``` #### Setting Values ```php // Set configuration values -config()->set('sitename', 'My Awesome Tracker'); -config()->set('database.port', 3306); -config()->set('cache.enabled', true); +tp_config()->set('sitename', 'My Awesome Tracker'); +tp_config()->set('database.port', 3306); +tp_config()->set('cache.enabled', true); ``` #### Working with Sections ```php // Get entire configuration section -$dbConfig = config()->getSection('database'); -$trackerConfig = config()->getSection('tracker'); +$dbConfig = tp_config()->getSection('database'); +$trackerConfig = tp_config()->getSection('tracker'); // Check if configuration exists -if (config()->has('bt_announce_url')) { - $announceUrl = config()->get('bt_announce_url'); +if (tp_config()->has('bt_announce_url')) { + $announceUrl = tp_config()->get('bt_announce_url'); } ``` ### Common Configuration Mappings -| Old Syntax | New Syntax | -|------------|------------| -| `$bb_cfg['sitename']` | `config()->get('sitename')` | -| `$bb_cfg['database']['host']` | `config()->get('database.host')` | -| `$bb_cfg['tracker']['enabled']` | `config()->get('tracker.enabled')` | -| `$bb_cfg['cache']['timeout']` | `config()->get('cache.timeout')` | -| `$bb_cfg['torr_server']['url']` | `config()->get('torr_server.url')` | +| Old Syntax | New Syntax | +|------------|---------------------------------------| +| `$bb_cfg['sitename']` | `tp_config()->get('sitename')` | +| `$bb_cfg['database']['host']` | `tp_config()->get('database.host')` | +| `$bb_cfg['tracker']['enabled']` | `tp_config()->get('tracker.enabled')` | +| `$bb_cfg['cache']['timeout']` | `tp_config()->get('cache.timeout')` | +| `$bb_cfg['torr_server']['url']` | `tp_config()->get('torr_server.url')` | ### Magic Methods Support ```php // Magic getter -$siteName = config()->sitename; -$dbHost = config()->{'database.host'}; +$siteName = tp_config()->sitename; +$dbHost = tp_config()->{'database.host'}; // Magic setter -config()->sitename = 'New Site Name'; -config()->{'database.port'} = 3306; +tp_config()->sitename = 'New Site Name'; +tp_config()->{'database.port'} = 3306; // Magic isset -if (isset(config()->bt_announce_url)) { +if (isset(tp_config()->bt_announce_url)) { // Configuration exists } ``` @@ -804,7 +804,7 @@ _e('WELCOME_MESSAGE'); // Same as: echo __('WELCOME_MESSAGE') _e('USER_ONLINE', 'Online'); // With default value // ✅ Common usage patterns -$title = __('PAGE_TITLE', config()->get('sitename')); +$title = __('PAGE_TITLE', tp_config()->get('sitename')); $error = __('ERROR.INVALID_INPUT', 'Invalid input'); ``` @@ -1113,8 +1113,8 @@ $environment = [ - **New Implementation**: Uses Nette Database v3.2 with improved API requiring code updates ### Deprecated Functions -- `get_config()` → Use `config()->get()` -- `set_config()` → Use `config()->set()` +- `get_config()` → Use `tp_config()->get()` +- `set_config()` → Use `tp_config()->set()` - Direct `$bb_cfg` access → Use `config()` methods ### Deprecated Patterns @@ -1139,11 +1139,11 @@ $environment = [ ### Configuration Management ```php // ✅ Always provide defaults -$timeout = config()->get('api.timeout', 30); +$timeout = tp_config()->get('api.timeout', 30); // ✅ Use type hints function getMaxUploadSize(): int { - return (int) config()->get('upload.max_size', 10485760); + return (int) tp_config()->get('upload.max_size', 10485760); } // ✅ Cache frequently used values @@ -1151,7 +1151,7 @@ class TrackerService { private string $announceUrl; 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 // ✅ Graceful error handling try { - $dbConfig = config()->getSection('database'); + $dbConfig = tp_config()->getSection('database'); // Database operations } catch (Exception $e) { error_log("Database configuration error: " . $e->getMessage()); @@ -1232,7 +1232,7 @@ try { ### Performance Optimization ```php // ✅ Minimize configuration calls in loops -$cacheEnabled = config()->get('cache.enabled', false); +$cacheEnabled = tp_config()->get('cache.enabled', false); for ($i = 0; $i < 1000; $i++) { if ($cacheEnabled) { // Use cached value @@ -1244,12 +1244,12 @@ for ($i = 0; $i < 1000; $i++) { ```php // ✅ Validate configuration values $maxFileSize = min( - config()->get('upload.max_size', 1048576), + tp_config()->get('upload.max_size', 1048576), 1048576 * 100 // Hard limit: 100MB ); // ✅ Sanitize user-configurable values -$siteName = htmlspecialchars(config()->get('sitename', 'TorrentPier')); +$siteName = htmlspecialchars(tp_config()->get('sitename', 'TorrentPier')); ``` ### Testing and Quality Assurance diff --git a/_cleanup.php b/_cleanup.php deleted file mode 100644 index d9802822a..000000000 --- a/_cleanup.php +++ /dev/null @@ -1,57 +0,0 @@ -php ' . basename(__FILE__) . ' 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'); diff --git a/app/Console/Commands/ClearCacheCommand.php b/app/Console/Commands/ClearCacheCommand.php new file mode 100644 index 000000000..1daad14c0 --- /dev/null +++ b/app/Console/Commands/ClearCacheCommand.php @@ -0,0 +1,116 @@ +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; + } +} diff --git a/app/Console/Commands/Command.php b/app/Console/Commands/Command.php new file mode 100644 index 000000000..e529b9cfc --- /dev/null +++ b/app/Console/Commands/Command.php @@ -0,0 +1,207 @@ +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); + } +} diff --git a/app/Console/Commands/InfoCommand.php b/app/Console/Commands/InfoCommand.php new file mode 100644 index 000000000..8368de43b --- /dev/null +++ b/app/Console/Commands/InfoCommand.php @@ -0,0 +1,113 @@ +app->make('path.base'); + + $this->line(''); + $this->line('TorrentPier System Information'); + $this->line('=============================='); + $this->line(''); + + // Application info + $this->line('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('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('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('Required Extensions:'); + foreach ($requiredExtensions as $ext) { + $status = extension_loaded($ext) ? '✓' : '✗'; + $this->line(" {$status} {$ext}"); + } + $this->line(''); + + // File permissions + $this->line('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 ? '✓' : '✗'; + $this->line(" {$status} {$path}"); + } else { + $this->line(" ? {$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('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('Database: Configuration error'); + } + + $this->line(''); + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/MigrateCommand.php b/app/Console/Commands/MigrateCommand.php new file mode 100644 index 000000000..b5696f9bd --- /dev/null +++ b/app/Console/Commands/MigrateCommand.php @@ -0,0 +1,101 @@ +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; + } +} diff --git a/app/Container/Container.php b/app/Container/Container.php new file mode 100644 index 000000000..bd3e1fff6 --- /dev/null +++ b/app/Container/Container.php @@ -0,0 +1,36 @@ +resolve($id); + } + + /** + * Check if a service exists in the container + */ + public function has(string $id): bool + { + return $this->bound($id); + } +} diff --git a/app/Events/TorrentUploaded.php b/app/Events/TorrentUploaded.php new file mode 100644 index 000000000..1727b6a35 --- /dev/null +++ b/app/Events/TorrentUploaded.php @@ -0,0 +1,66 @@ +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; + } +} diff --git a/app/Events/UserRegistered.php b/app/Events/UserRegistered.php new file mode 100644 index 000000000..3298e518c --- /dev/null +++ b/app/Events/UserRegistered.php @@ -0,0 +1,57 @@ +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; + } +} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php new file mode 100644 index 000000000..9f5d96458 --- /dev/null +++ b/app/Exceptions/Handler.php @@ -0,0 +1,48 @@ +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 '

Error

' . htmlspecialchars($exception->getMessage()) . '

'; + } +} diff --git a/config/environments/.keep b/app/Http/Controllers/Admin/.keep similarity index 100% rename from config/environments/.keep rename to app/Http/Controllers/Admin/.keep diff --git a/app/Http/Controllers/Api/UserController.php b/app/Http/Controllers/Api/UserController.php new file mode 100644 index 000000000..dfcea39af --- /dev/null +++ b/app/Http/Controllers/Api/UserController.php @@ -0,0 +1,170 @@ +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); + } + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php new file mode 100644 index 000000000..c1db9aba9 --- /dev/null +++ b/app/Http/Controllers/Controller.php @@ -0,0 +1,94 @@ +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'); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Web/HelloWorldController.php b/app/Http/Controllers/Web/HelloWorldController.php new file mode 100644 index 000000000..108ef970c --- /dev/null +++ b/app/Http/Controllers/Web/HelloWorldController.php @@ -0,0 +1,153 @@ +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' + ] + ]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Web/LegacyController.php b/app/Http/Controllers/Web/LegacyController.php new file mode 100644 index 000000000..06684d128 --- /dev/null +++ b/app/Http/Controllers/Web/LegacyController.php @@ -0,0 +1,219 @@ +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( + "

404 - Not Found

Legacy controller '{$controller}' not found

", + 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 = " +

Legacy Controller Error

+

Controller: {$controller}

+

Error: " . htmlspecialchars($e->getMessage()) . "

+

File: " . htmlspecialchars($e->getFile()) . ":" . $e->getLine() . "

+ "; + + if (function_exists('dev') && dev()->isDebugEnabled()) { + $errorHtml .= "
" . htmlspecialchars($e->getTraceAsString()) . "
"; + } + + return new Response($errorHtml, 500, ['Content-Type' => 'text/html']); + } + } +} diff --git a/app/Http/Middleware/AdminMiddleware.php b/app/Http/Middleware/AdminMiddleware.php new file mode 100644 index 000000000..7115f0b9b --- /dev/null +++ b/app/Http/Middleware/AdminMiddleware.php @@ -0,0 +1,52 @@ +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); + } +} diff --git a/app/Http/Middleware/AuthMiddleware.php b/app/Http/Middleware/AuthMiddleware.php new file mode 100644 index 000000000..9e92b9989 --- /dev/null +++ b/app/Http/Middleware/AuthMiddleware.php @@ -0,0 +1,55 @@ +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); + } +} diff --git a/app/Http/Middleware/BaseMiddleware.php b/app/Http/Middleware/BaseMiddleware.php new file mode 100644 index 000000000..97bf454ca --- /dev/null +++ b/app/Http/Middleware/BaseMiddleware.php @@ -0,0 +1,38 @@ +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; + } +} diff --git a/app/Http/Middleware/CorsMiddleware.php b/app/Http/Middleware/CorsMiddleware.php new file mode 100644 index 000000000..871b4fa38 --- /dev/null +++ b/app/Http/Middleware/CorsMiddleware.php @@ -0,0 +1,58 @@ +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; + } +} diff --git a/app/Http/Requests/FormRequest.php b/app/Http/Requests/FormRequest.php new file mode 100644 index 000000000..1bc1fbd1f --- /dev/null +++ b/app/Http/Requests/FormRequest.php @@ -0,0 +1,116 @@ +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(); + } +} \ No newline at end of file diff --git a/app/Http/Requests/RegisterUserRequest.php b/app/Http/Requests/RegisterUserRequest.php new file mode 100644 index 000000000..c821b2fe7 --- /dev/null +++ b/app/Http/Requests/RegisterUserRequest.php @@ -0,0 +1,120 @@ + [ + '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'); + } +} \ No newline at end of file diff --git a/app/Http/Routing/Router.php b/app/Http/Routing/Router.php new file mode 100644 index 000000000..b13d2be56 --- /dev/null +++ b/app/Http/Routing/Router.php @@ -0,0 +1,223 @@ +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); + } + } +} \ No newline at end of file diff --git a/app/Listeners/SendWelcomeEmail.php b/app/Listeners/SendWelcomeEmail.php new file mode 100644 index 000000000..b06971f5d --- /dev/null +++ b/app/Listeners/SendWelcomeEmail.php @@ -0,0 +1,41 @@ +getUsername(), + $event->getUserId(), + $event->getEmail() + )); + } + } + + /** + * Determine whether the listener should be queued + */ + public function shouldQueue(UserRegistered $event): bool + { + return true; + } +} \ No newline at end of file diff --git a/app/Listeners/UpdateUserStatistics.php b/app/Listeners/UpdateUserStatistics.php new file mode 100644 index 000000000..10bb51d22 --- /dev/null +++ b/app/Listeners/UpdateUserStatistics.php @@ -0,0 +1,32 @@ +getUploaderId(), + $event->getTorrentName(), + $event->getTorrentId(), + $event->getSize() + )); + } + } +} \ No newline at end of file diff --git a/app/Models/Model.php b/app/Models/Model.php new file mode 100644 index 000000000..5a7e3cb7c --- /dev/null +++ b/app/Models/Model.php @@ -0,0 +1,223 @@ +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; + } +} \ No newline at end of file diff --git a/app/Models/Torrent.php b/app/Models/Torrent.php new file mode 100644 index 000000000..f9da5d7c9 --- /dev/null +++ b/app/Models/Torrent.php @@ -0,0 +1,109 @@ +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; + } +} \ No newline at end of file diff --git a/app/Models/User.php b/app/Models/User.php new file mode 100644 index 000000000..b54b7eaf3 --- /dev/null +++ b/app/Models/User.php @@ -0,0 +1,139 @@ +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); + } +} \ No newline at end of file diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php new file mode 100644 index 000000000..c6c849bda --- /dev/null +++ b/app/Providers/AppServiceProvider.php @@ -0,0 +1,66 @@ +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); + } +} \ No newline at end of file diff --git a/app/Providers/ConsoleServiceProvider.php b/app/Providers/ConsoleServiceProvider.php new file mode 100644 index 000000000..2409f3125 --- /dev/null +++ b/app/Providers/ConsoleServiceProvider.php @@ -0,0 +1,76 @@ +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; + } +} \ No newline at end of file diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php new file mode 100644 index 000000000..589769256 --- /dev/null +++ b/app/Providers/EventServiceProvider.php @@ -0,0 +1,64 @@ + [ + 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; + } +} \ No newline at end of file diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php new file mode 100644 index 000000000..7c3c19484 --- /dev/null +++ b/app/Providers/RouteServiceProvider.php @@ -0,0 +1,123 @@ +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; + }); + } + } +} diff --git a/app/Providers/ServiceProvider.php b/app/Providers/ServiceProvider.php new file mode 100644 index 000000000..b7dff9204 --- /dev/null +++ b/app/Providers/ServiceProvider.php @@ -0,0 +1,17 @@ +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('*'); + } +} \ No newline at end of file diff --git a/app/Services/User/UserService.php b/app/Services/User/UserService.php new file mode 100644 index 000000000..4e7730121 --- /dev/null +++ b/app/Services/User/UserService.php @@ -0,0 +1,196 @@ +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'); + } + } +} \ No newline at end of file diff --git a/app/Services/UserService.php b/app/Services/UserService.php new file mode 100644 index 000000000..df199d30f --- /dev/null +++ b/app/Services/UserService.php @@ -0,0 +1,42 @@ +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); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php new file mode 100644 index 000000000..43c5862db --- /dev/null +++ b/bootstrap/app.php @@ -0,0 +1,41 @@ +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; \ No newline at end of file diff --git a/bootstrap/console.php b/bootstrap/console.php new file mode 100644 index 000000000..efd395b39 --- /dev/null +++ b/bootstrap/console.php @@ -0,0 +1,47 @@ +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; diff --git a/bootstrap/container.php b/bootstrap/container.php new file mode 100644 index 000000000..05e7f98ad --- /dev/null +++ b/bootstrap/container.php @@ -0,0 +1,115 @@ +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']); + } + } + } +} \ No newline at end of file diff --git a/bt/announce.php b/bt/announce.php index ea1cca19a..f6db656be 100644 --- a/bt/announce.php +++ b/bt/announce.php @@ -18,8 +18,8 @@ if (empty($userAgent)) { die; } -$announce_interval = config()->get('announce_interval'); -$passkey_key = config()->get('passkey_key'); +$announce_interval = tp_config()->get('announce_interval'); +$passkey_key = tp_config()->get('passkey_key'); // Recover info_hash if (isset($_GET['?info_hash']) && !isset($_GET['info_hash'])) { @@ -65,10 +65,10 @@ if (strlen($peer_id) !== 20) { } // Check for client ban -if (config()->get('client_ban.enabled')) { +if (tp_config()->get('client_ban.enabled')) { $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)) { $targetClient = [ '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'])) { msg_die('Your BitTorrent client has been banned!'); } @@ -129,7 +129,7 @@ if ( || !is_numeric($port) || ($port < 1024 && !$stopped) || $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); } @@ -168,13 +168,13 @@ if (preg_match('/(Mozilla|Browser|Chrome|Safari|AppleWebKit|Opera|Links|Lynx|Bot $ip = $_SERVER['REMOTE_ADDR']; // 'ip' query handling -if (!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('ignore_reported_ip') && isset($_GET['ip']) && $ip !== $_GET['ip']) { + if (!tp_config()->get('verify_reported_ip') && isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $x_ip = $_SERVER['HTTP_X_FORWARDED_FOR']; if ($x_ip === $_GET['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; } } @@ -270,7 +270,7 @@ if ($lp_info) { define('IS_MOD', !IS_GUEST && (int)$row['user_level'] === MOD); define('IS_GROUP_MEMBER', !IS_GUEST && (int)$row['user_level'] === GROUP_MEMBER); 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); $topic_id = $row['topic_id']; $releaser = (int)($user_id == $row['poster_id']); @@ -278,13 +278,13 @@ if ($lp_info) { $tor_status = $row['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'); } // Check hybrid status 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), default => $row['info_hash'] // 1 }; @@ -294,7 +294,7 @@ if ($lp_info) { } // 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); if ($user_ratio === null) { $user_ratio = 1; @@ -302,10 +302,10 @@ if ($lp_info) { $rating_msg = ''; if (!$seeder) { - foreach (config()->get('rating') as $ratio => $limit) { + foreach (tp_config()->get('rating') as $ratio => $limit) { if ($user_ratio < $ratio) { - config()->set('tracker.limit_active_tor', 1); - config()->set('tracker.limit_leech_count', $limit); + tp_config()->set('tracker.limit_active_tor', 1); + tp_config()->set('tracker.limit_leech_count', $limit); $rating_msg = " (ratio < $ratio)"; break; } @@ -313,29 +313,29 @@ if ($lp_info) { } // 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 FROM " . BB_BT_TRACKER . " WHERE user_id = $user_id AND seeder = $seeder AND topic_id != $topic_id"; - if (!$seeder && config()->get('tracker.leech_expire_factor') && $user_ratio < 0.5) { - $sql .= " AND update_time > " . (TIMENOW - 60 * config()->get('tracker.leech_expire_factor')); + if (!$seeder && tp_config()->get('tracker.leech_expire_factor') && $user_ratio < 0.5) { + $sql .= " AND update_time > " . (TIMENOW - 60 * tp_config()->get('tracker.leech_expire_factor')); } $sql .= " GROUP BY user_id"; if ($row = DB()->fetch_row($sql)) { - if ($seeder && config()->get('tracker.limit_seed_count') && $row['active_torrents'] >= config()->get('tracker.limit_seed_count')) { - msg_die('Only ' . 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')) { - msg_die('Only ' . config()->get('tracker.limit_leech_count') . ' torrent(s) allowed for leeching' . $rating_msg); + if ($seeder && tp_config()->get('tracker.limit_seed_count') && $row['active_torrents'] >= tp_config()->get('tracker.limit_seed_count')) { + msg_die('Only ' . tp_config()->get('tracker.limit_seed_count') . ' torrent(s) allowed for seeding'); + } elseif (!$seeder && tp_config()->get('tracker.limit_leech_count') && $row['active_torrents'] >= tp_config()->get('tracker.limit_leech_count')) { + msg_die('Only ' . tp_config()->get('tracker.limit_leech_count') . ' torrent(s) allowed for leeching' . $rating_msg); } } } // 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 FROM " . BB_BT_TRACKER . " WHERE topic_id = $topic_id @@ -343,16 +343,16 @@ if ($lp_info) { AND seeder = $seeder AND $ip_version != '$ip_sql'"; - if (!$seeder && config()->get('tracker.leech_expire_factor')) { - $sql .= " AND update_time > " . (TIMENOW - 60 * config()->get('tracker.leech_expire_factor')); + if (!$seeder && tp_config()->get('tracker.leech_expire_factor')) { + $sql .= " AND update_time > " . (TIMENOW - 60 * tp_config()->get('tracker.leech_expire_factor')); } $sql .= " GROUP BY topic_id"; if ($row = DB()->fetch_row($sql)) { - if ($seeder && config()->get('tracker.limit_seed_ips') && $row['ips'] >= config()->get('tracker.limit_seed_ips')) { - msg_die('You can seed only from ' . config()->get('tracker.limit_seed_ips') . " IP's"); - } elseif (!$seeder && config()->get('tracker.limit_leech_ips') && $row['ips'] >= config()->get('tracker.limit_leech_ips')) { - msg_die('You can leech only from ' . config()->get('tracker.limit_leech_ips') . " IP's"); + 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 ' . tp_config()->get('tracker.limit_seed_ips') . " IP's"); + } 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 ' . 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; // 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) { $down_add = 0; } // Silver releases @@ -386,7 +386,7 @@ if (config()->get('tracker.gold_silver_enabled') && $down_add) { } // Freeleech -if (config()->get('tracker.freeleech') && $down_add) { +if (tp_config()->get('tracker.freeleech') && $down_add) { $down_add = 0; } @@ -464,8 +464,8 @@ $output = CACHE('tr_cache')->get(PEERS_LIST_PREFIX . $topic_id); if (!$output) { // Retrieve peers - $numwant = (int)config()->get('tracker.numwant'); - $compact_mode = (config()->get('tracker.compact_mode') || !empty($compact)); + $numwant = (int)tp_config()->get('tracker.numwant'); + $compact_mode = (tp_config()->get('tracker.compact_mode') || !empty($compact)); $rowset = DB()->fetch_rowset(" SELECT ip, ipv6, port @@ -510,7 +510,7 @@ if (!$output) { $seeders = $leechers = $client_completed = 0; - if (config()->get('tracker.scrape')) { + if (tp_config()->get('tracker.scrape')) { $row = DB()->fetch_row(" SELECT seeders, leechers, completed FROM " . BB_BT_TRACKER_SNAP . " diff --git a/bt/includes/init_tr.php b/bt/includes/init_tr.php index 283c71ede..d0085b4ee 100644 --- a/bt/includes/init_tr.php +++ b/bt/includes/init_tr.php @@ -12,8 +12,8 @@ if (!defined('IN_TRACKER')) { } // Exit if tracker is disabled -if (config()->get('tracker.bt_off')) { - msg_die(config()->get('tracker.bt_off_reason')); +if (tp_config()->get('tracker.bt_off')) { + msg_die(tp_config()->get('tracker.bt_off_reason')); } // diff --git a/bt/scrape.php b/bt/scrape.php index dd94ab8ff..392032426 100644 --- a/bt/scrape.php +++ b/bt/scrape.php @@ -11,7 +11,7 @@ define('IN_TRACKER', true); define('BB_ROOT', './../'); require dirname(__DIR__) . '/common.php'; -if (!config()->get('tracker.scrape')) { +if (!tp_config()->get('tracker.scrape')) { msg_die('Please disable SCRAPE!'); } @@ -58,8 +58,8 @@ foreach ($info_hash_array[1] as $hash) { $info_hash_count = count($info_hashes); if (!empty($info_hash_count)) { - if ($info_hash_count > config()->get('max_scrapes')) { - $info_hashes = array_slice($info_hashes, 0, config()->get('max_scrapes')); + if ($info_hash_count > tp_config()->get('max_scrapes')) { + $info_hashes = array_slice($info_hashes, 0, tp_config()->get('max_scrapes')); } $info_hashes_sql = implode('\', \'', $info_hashes); diff --git a/cliff.toml b/cliff.toml deleted file mode 100644 index 1798567f1..000000000 --- a/cliff.toml +++ /dev/null @@ -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 = '', 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}](/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 = "🚀 Features" }, - { message = "^fix", group = "🐛 Bug Fixes" }, - { message = "^doc", group = "📚 Documentation" }, - { message = "^perf", group = "⚡ Performance" }, - { message = "^refactor", group = "🚜 Refactor" }, - { message = "^style", group = "🎨 Styling" }, - { message = "^test", group = "🧪 Testing" }, - { message = "^ignore|^release|^changelog", skip = true }, - { message = "^chore|^ci|^misc", group = "⚙️ Miscellaneous" }, - { body = ".*security", group = "🛡️ Security" }, - { message = "^revert", group = "◀️ Revert" }, - { message = "^crowdin|^crodwin", group = "🈳 New translations" }, # crowdin pulls supporting - { message = "^Composer", group = "📦 Dependencies" }, # dependabot pulls supporting - { message = "^rem|^drop|^removed", group = "🗑️ Removed" }, - { message = ".*", group = "💼 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" diff --git a/common.php b/common.php index de0a8cab4..931b49931 100644 --- a/common.php +++ b/common.php @@ -34,9 +34,6 @@ if (empty($_SERVER['SERVER_ADDR'])) { if (!defined('BB_ROOT')) { define('BB_ROOT', './'); } -if (!defined('BB_SCRIPT')) { - define('BB_SCRIPT', null); -} header('X-Frame-Options: SAMEORIGIN'); date_default_timezone_set('UTC'); @@ -58,18 +55,6 @@ if (!is_file(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 try { $dotenv = Dotenv\Dotenv::createMutable(BB_PATH); @@ -95,7 +80,7 @@ $config = \TorrentPier\Config::init($bb_cfg); * * @return \TorrentPier\Config */ -function config(): \TorrentPier\Config +function tp_config(): \TorrentPier\Config { return \TorrentPier\Config::getInstance(); } @@ -168,14 +153,14 @@ if (APP_ENV === 'development') { /** * Server variables initialize */ -$server_protocol = config()->get('cookie_secure') ? 'https://' : 'http://'; -$server_port = in_array((int)config()->get('server_port'), [80, 443], true) ? '' : ':' . config()->get('server_port'); -define('FORUM_PATH', config()->get('script_path')); -define('FULL_URL', $server_protocol . config()->get('server_name') . $server_port . config()->get('script_path')); +$server_protocol = tp_config()->get('cookie_secure') ? 'https://' : 'http://'; +$server_port = in_array((int)tp_config()->get('server_port'), [80, 443], true) ? '' : ':' . tp_config()->get('server_port'); +define('FORUM_PATH', tp_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); // 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 @@ -189,7 +174,7 @@ function DB(string $db_alias = 'db'): \TorrentPier\Database\Database } // 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) @@ -209,7 +194,7 @@ function CACHE(string $cache_name): \TorrentPier\Cache\CacheManager */ 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 { 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('PEERS_LIST_EXPIRE', round(config()->get('announce_interval') * 0.7)); - define('SCRAPE_LIST_EXPIRE', round(config()->get('scrape_interval') * 0.7)); + define('PEER_HASH_EXPIRE', round(tp_config()->get('announce_interval') * (0.85 * tp_config()->get('tracker.expire_factor')))); + define('PEERS_LIST_EXPIRE', round(tp_config()->get('announce_interval') * 0.7)); + define('SCRAPE_LIST_EXPIRE', round(tp_config()->get('scrape_interval') * 0.7)); define('PEER_HASH_PREFIX', 'peer_'); define('PEERS_LIST_PREFIX', 'peers_list_'); diff --git a/composer.json b/composer.json index 97cf8ec23..cf3792c55 100644 --- a/composer.json +++ b/composer.json @@ -55,10 +55,20 @@ "bugsnag/bugsnag": "^v3.29.1", "claviska/simpleimage": "^4.0", "egulias/email-validator": "^4.0.1", + "fakerphp/faker": "^1.24", "filp/whoops": "^2.15", "gemorroj/m3u-parser": "^6.0.1", "gigablah/sphinxphp": "2.0.8", "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", "josantonius/cookie": "^2.0", "league/flysystem": "^3.28", @@ -67,9 +77,9 @@ "nette/caching": "^3.3", "nette/database": "^3.2", "php-curl-class/php-curl-class": "^12.0.0", - "php-di/php-di": "^7.0", "robmorgan/phinx": "^0.16.9", "samdark/sitemap": "2.4.1", + "symfony/console": "^7.3", "symfony/mailer": "^7.3", "symfony/polyfill": "v1.32.0", "vlucas/phpdotenv": "^5.5", @@ -82,10 +92,11 @@ }, "autoload": { "psr-4": { - "TorrentPier\\": "src/" + "TorrentPier\\": "src/", + "App\\": "app/" }, "files": [ - "src/helpers.php" + "app/helpers.php" ] }, "autoload-dev": { diff --git a/composer.lock b/composer.lock index a513013f6..92c963901 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "57713d8849e71683b70d934a81f7e18c", + "content-hash": "697bc73a6378851ccaea6fb2c3c4f61d", "packages": [ { "name": "arokettu/bencode", @@ -480,6 +480,66 @@ }, "time": "2025-03-10T13:15:53+00:00" }, + { + "name": "brick/math", + "version": "0.13.1", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/fc7ed316430118cc7836bf45faff18d5dfc8de04", + "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "6.8.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.13.1" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2025-03-29T13:50:30+00:00" + }, { "name": "bugsnag/bugsnag", "version": "v3.29.3", @@ -867,6 +927,75 @@ }, "time": "2025-05-21T14:35:19+00:00" }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/dbal": "<4.0.0 || >=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, { "name": "claviska/simpleimage", "version": "4.2.1", @@ -996,6 +1125,97 @@ ], "time": "2025-05-26T15:08:54+00:00" }, + { + "name": "doctrine/inflector", + "version": "2.0.10", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^11.0", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^8.5 || ^9.5", + "vimeo/psalm": "^4.25 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.0.10" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2024-02-18T20:23:39+00:00" + }, { "name": "doctrine/lexer", "version": "3.0.1", @@ -1140,6 +1360,69 @@ ], "time": "2025-03-06T22:45:56+00:00" }, + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, { "name": "filp/whoops", "version": "2.18.3", @@ -1211,6 +1494,77 @@ ], "time": "2025-06-16T00:02:10+00:00" }, + { + "name": "fruitcake/php-cors", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "symfony/http-foundation": "^4.4|^5.4|^6|^7" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2023-10-12T05:21:21+00:00" + }, { "name": "gemorroj/m3u-parser", "version": "6.0.1", @@ -1754,6 +2108,1053 @@ ], "time": "2025-03-27T12:30:47+00:00" }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.4", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "30e286560c137526eccd4ce21b2de477ab0676d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2", + "reference": "30e286560c137526eccd4ce21b2de477ab0676d2", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2025-02-03T10:55:03+00:00" + }, + { + "name": "illuminate/bus", + "version": "v12.19.3", + "source": { + "type": "git", + "url": "https://github.com/illuminate/bus.git", + "reference": "60da78ea881c539ce56c5b66321be73755c5918c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/bus/zipball/60da78ea881c539ce56c5b66321be73755c5918c", + "reference": "60da78ea881c539ce56c5b66321be73755c5918c", + "shasum": "" + }, + "require": { + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/pipeline": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "suggest": { + "illuminate/queue": "Required to use closures when chaining jobs (^12.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Bus\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Bus package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-05-13T15:08:45+00:00" + }, + { + "name": "illuminate/collections", + "version": "v12.19.3", + "source": { + "type": "git", + "url": "https://github.com/illuminate/collections.git", + "reference": "21a206b2b2297e838c181b482b5f8bbe7ac48f61" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/collections/zipball/21a206b2b2297e838c181b482b5f8bbe7ac48f61", + "reference": "21a206b2b2297e838c181b482b5f8bbe7ac48f61", + "shasum": "" + }, + "require": { + "illuminate/conditionable": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "php": "^8.2" + }, + "suggest": { + "illuminate/http": "Required to convert collections to API resources (^12.0).", + "symfony/var-dumper": "Required to use the dump method (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php", + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Collections package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-06-12T14:21:37+00:00" + }, + { + "name": "illuminate/conditionable", + "version": "v12.19.3", + "source": { + "type": "git", + "url": "https://github.com/illuminate/conditionable.git", + "reference": "ec677967c1f2faf90b8428919124d2184a4c9b49" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/conditionable/zipball/ec677967c1f2faf90b8428919124d2184a4c9b49", + "reference": "ec677967c1f2faf90b8428919124d2184a4c9b49", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Conditionable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-05-13T15:08:45+00:00" + }, + { + "name": "illuminate/config", + "version": "v12.19.3", + "source": { + "type": "git", + "url": "https://github.com/illuminate/config.git", + "reference": "76824d4e853dba43359a201eba2422ab11e21e4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/config/zipball/76824d4e853dba43359a201eba2422ab11e21e4d", + "reference": "76824d4e853dba43359a201eba2422ab11e21e4d", + "shasum": "" + }, + "require": { + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Config\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Config package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-19T20:10:05+00:00" + }, + { + "name": "illuminate/container", + "version": "v12.19.3", + "source": { + "type": "git", + "url": "https://github.com/illuminate/container.git", + "reference": "ff9dde2c8dce16ea9ecf0418095749311240aff9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/container/zipball/ff9dde2c8dce16ea9ecf0418095749311240aff9", + "reference": "ff9dde2c8dce16ea9ecf0418095749311240aff9", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^12.0", + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1" + }, + "provide": { + "psr/container-implementation": "1.1|2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Container\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Container package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-06-09T14:04:48+00:00" + }, + { + "name": "illuminate/contracts", + "version": "v12.19.3", + "source": { + "type": "git", + "url": "https://github.com/illuminate/contracts.git", + "reference": "ad1d16d827927455d3b7e39fabac66b1afb82582" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/ad1d16d827927455d3b7e39fabac66b1afb82582", + "reference": "ad1d16d827927455d3b7e39fabac66b1afb82582", + "shasum": "" + }, + "require": { + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/simple-cache": "^1.0|^2.0|^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Contracts\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Contracts package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-06-12T15:07:31+00:00" + }, + { + "name": "illuminate/database", + "version": "v12.19.3", + "source": { + "type": "git", + "url": "https://github.com/illuminate/database.git", + "reference": "758dcd2128af1bc9427a6f72d247a4f0c078a24a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/database/zipball/758dcd2128af1bc9427a6f72d247a4f0c078a24a", + "reference": "758dcd2128af1bc9427a6f72d247a4f0c078a24a", + "shasum": "" + }, + "require": { + "brick/math": "^0.11|^0.12|^0.13", + "ext-pdo": "*", + "illuminate/collections": "^12.0", + "illuminate/container": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "laravel/serializable-closure": "^1.3|^2.0", + "php": "^8.2" + }, + "suggest": { + "ext-filter": "Required to use the Postgres database driver.", + "fakerphp/faker": "Required to use the eloquent factory builder (^1.24).", + "illuminate/console": "Required to use the database commands (^12.0).", + "illuminate/events": "Required to use the observers with Eloquent (^12.0).", + "illuminate/filesystem": "Required to use the migrations (^12.0).", + "illuminate/http": "Required to convert Eloquent models to API resources (^12.0).", + "illuminate/pagination": "Required to paginate the result set (^12.0).", + "symfony/finder": "Required to use Eloquent model factories (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Database\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Database package.", + "homepage": "https://laravel.com", + "keywords": [ + "database", + "laravel", + "orm", + "sql" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-06-18T12:55:09+00:00" + }, + { + "name": "illuminate/events", + "version": "v12.19.3", + "source": { + "type": "git", + "url": "https://github.com/illuminate/events.git", + "reference": "bf1f121ea51e077e893d32e2848e102513d4b1b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/events/zipball/bf1f121ea51e077e893d32e2848e102513d4b1b5", + "reference": "bf1f121ea51e077e893d32e2848e102513d4b1b5", + "shasum": "" + }, + "require": { + "illuminate/bus": "^12.0", + "illuminate/collections": "^12.0", + "illuminate/container": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Illuminate\\Events\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Events package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-05-13T15:08:45+00:00" + }, + { + "name": "illuminate/filesystem", + "version": "v12.19.3", + "source": { + "type": "git", + "url": "https://github.com/illuminate/filesystem.git", + "reference": "a5ec0cc347d46ff4aa3615c7739f321df3183fb7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/filesystem/zipball/a5ec0cc347d46ff4aa3615c7739f321df3183fb7", + "reference": "a5ec0cc347d46ff4aa3615c7739f321df3183fb7", + "shasum": "" + }, + "require": { + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2", + "symfony/finder": "^7.2.0" + }, + "suggest": { + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-hash": "Required to use the Filesystem class.", + "illuminate/http": "Required for handling uploaded files (^12.0).", + "league/flysystem": "Required to use the Flysystem local driver (^3.25.1).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", + "symfony/mime": "Required to enable support for guessing extensions (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Illuminate\\Filesystem\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Filesystem package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-06-17T23:40:32+00:00" + }, + { + "name": "illuminate/http", + "version": "v12.19.3", + "source": { + "type": "git", + "url": "https://github.com/illuminate/http.git", + "reference": "a2a8a2ee62a6dacee2dc4162c59f9bbeb68ec9ee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/http/zipball/a2a8a2ee62a6dacee2dc4162c59f9bbeb68ec9ee", + "reference": "a2a8a2ee62a6dacee2dc4162c59f9bbeb68ec9ee", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/uri-template": "^1.0", + "illuminate/collections": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/session": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.2.0", + "symfony/mime": "^7.2.0", + "symfony/polyfill-php83": "^1.31" + }, + "suggest": { + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image()." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Http\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Http package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-06-11T21:44:58+00:00" + }, + { + "name": "illuminate/macroable", + "version": "v12.19.3", + "source": { + "type": "git", + "url": "https://github.com/illuminate/macroable.git", + "reference": "e862e5648ee34004fa56046b746f490dfa86c613" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/macroable/zipball/e862e5648ee34004fa56046b746f490dfa86c613", + "reference": "e862e5648ee34004fa56046b746f490dfa86c613", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Macroable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2024-07-23T16:31:01+00:00" + }, + { + "name": "illuminate/pipeline", + "version": "v12.19.3", + "source": { + "type": "git", + "url": "https://github.com/illuminate/pipeline.git", + "reference": "a1039dfe54854470cdda37782bab0901aa588dd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/pipeline/zipball/a1039dfe54854470cdda37782bab0901aa588dd4", + "reference": "a1039dfe54854470cdda37782bab0901aa588dd4", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Pipeline\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Pipeline package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-05-13T15:08:45+00:00" + }, + { + "name": "illuminate/routing", + "version": "v12.19.3", + "source": { + "type": "git", + "url": "https://github.com/illuminate/routing.git", + "reference": "f4cc40e27fb8771fec7ff6f1daf8a13020a9eba5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/routing/zipball/f4cc40e27fb8771fec7ff6f1daf8a13020a9eba5", + "reference": "f4cc40e27fb8771fec7ff6f1daf8a13020a9eba5", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "ext-hash": "*", + "illuminate/collections": "^12.0", + "illuminate/container": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/http": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/pipeline": "^12.0", + "illuminate/session": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.2.0", + "symfony/routing": "^7.2.0" + }, + "suggest": { + "illuminate/console": "Required to use the make commands (^12.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Routing\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Routing package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-06-17T17:01:27+00:00" + }, + { + "name": "illuminate/session", + "version": "v12.19.3", + "source": { + "type": "git", + "url": "https://github.com/illuminate/session.git", + "reference": "9d27155b34bca502fe3e1adc16035b46d5ef3ed8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/session/zipball/9d27155b34bca502fe3e1adc16035b46d5ef3ed8", + "reference": "9d27155b34bca502fe3e1adc16035b46d5ef3ed8", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-session": "*", + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/filesystem": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2", + "symfony/finder": "^7.2.0", + "symfony/http-foundation": "^7.2.0" + }, + "suggest": { + "illuminate/console": "Required to use the session:table command (^12.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Session\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Session package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-05-13T15:08:45+00:00" + }, + { + "name": "illuminate/support", + "version": "v12.19.3", + "source": { + "type": "git", + "url": "https://github.com/illuminate/support.git", + "reference": "4e5d098d1cdbf5cabff09c1903a141bd9747ae75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/support/zipball/4e5d098d1cdbf5cabff09c1903a141bd9747ae75", + "reference": "4e5d098d1cdbf5cabff09c1903a141bd9747ae75", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-mbstring": "*", + "illuminate/collections": "^12.0", + "illuminate/conditionable": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "nesbot/carbon": "^3.8.4", + "php": "^8.2", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "replace": { + "spatie/once": "*" + }, + "suggest": { + "illuminate/filesystem": "Required to use the Composer class (^12.0).", + "laravel/serializable-closure": "Required to use the once function (^1.3|^2.0).", + "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.7).", + "league/uri": "Required to use the Uri class (^7.5.1).", + "ramsey/uuid": "Required to use Str::uuid() (^4.7).", + "symfony/process": "Required to use the Composer class (^7.2).", + "symfony/uid": "Required to use Str::ulid() (^7.2).", + "symfony/var-dumper": "Required to use the dd function (^7.2).", + "vlucas/phpdotenv": "Required to use the Env class and env helper (^5.6.1)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php", + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Support package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-06-12T15:07:56+00:00" + }, + { + "name": "illuminate/translation", + "version": "v12.19.3", + "source": { + "type": "git", + "url": "https://github.com/illuminate/translation.git", + "reference": "705bdc5e8616ac76d247302831d05ac5ba352b44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/translation/zipball/705bdc5e8616ac76d247302831d05ac5ba352b44", + "reference": "705bdc5e8616ac76d247302831d05ac5ba352b44", + "shasum": "" + }, + "require": { + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/filesystem": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Translation\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Translation package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-05-26T17:31:37+00:00" + }, + { + "name": "illuminate/validation", + "version": "v12.19.3", + "source": { + "type": "git", + "url": "https://github.com/illuminate/validation.git", + "reference": "42ac1d782c8541c3e779f7ffff33093edd5739a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/validation/zipball/42ac1d782c8541c3e779f7ffff33093edd5739a4", + "reference": "42ac1d782c8541c3e779f7ffff33093edd5739a4", + "shasum": "" + }, + "require": { + "brick/math": "^0.11|^0.12|^0.13", + "egulias/email-validator": "^3.2.5|^4.0", + "ext-filter": "*", + "ext-mbstring": "*", + "illuminate/collections": "^12.0", + "illuminate/container": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "illuminate/translation": "^12.0", + "php": "^8.2", + "symfony/http-foundation": "^7.2", + "symfony/mime": "^7.2" + }, + "suggest": { + "illuminate/database": "Required to use the database presence verifier (^12.0).", + "ramsey/uuid": "Required to use Validator::validateUuid() (^4.7)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Validation\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Validation package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-06-13T13:58:47+00:00" + }, { "name": "jacklul/monolog-telegram", "version": "3.2.0", @@ -2434,6 +3835,111 @@ ], "time": "2025-03-24T10:02:05+00:00" }, + { + "name": "nesbot/carbon", + "version": "3.10.1", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "1fd1935b2d90aef2f093c5e35f7ae1257c448d00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/1fd1935b2d90aef2f093c5e35f7ae1257c448d00", + "reference": "1fd1935b2d90aef2f093c5e35f7ae1257c448d00", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.75.0", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.17", + "phpunit/phpunit": "^10.5.46", + "squizlabs/php_codesniffer": "^3.13.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbon.nesbot.com/docs", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2025-06-21T15:19:35+00:00" + }, { "name": "nette/caching", "version": "v3.3.1", @@ -2801,134 +4307,6 @@ }, "time": "2025-03-25T18:04:16+00:00" }, - { - "name": "php-di/invoker", - "version": "2.3.6", - "source": { - "type": "git", - "url": "https://github.com/PHP-DI/Invoker.git", - "reference": "59f15608528d8a8838d69b422a919fd6b16aa576" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/59f15608528d8a8838d69b422a919fd6b16aa576", - "reference": "59f15608528d8a8838d69b422a919fd6b16aa576", - "shasum": "" - }, - "require": { - "php": ">=7.3", - "psr/container": "^1.0|^2.0" - }, - "require-dev": { - "athletic/athletic": "~0.1.8", - "mnapoli/hard-mode": "~0.3.0", - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Invoker\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Generic and extensible callable invoker", - "homepage": "https://github.com/PHP-DI/Invoker", - "keywords": [ - "callable", - "dependency", - "dependency-injection", - "injection", - "invoke", - "invoker" - ], - "support": { - "issues": "https://github.com/PHP-DI/Invoker/issues", - "source": "https://github.com/PHP-DI/Invoker/tree/2.3.6" - }, - "funding": [ - { - "url": "https://github.com/mnapoli", - "type": "github" - } - ], - "time": "2025-01-17T12:49:27+00:00" - }, - { - "name": "php-di/php-di", - "version": "7.0.11", - "source": { - "type": "git", - "url": "https://github.com/PHP-DI/PHP-DI.git", - "reference": "32f111a6d214564520a57831d397263e8946c1d2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/32f111a6d214564520a57831d397263e8946c1d2", - "reference": "32f111a6d214564520a57831d397263e8946c1d2", - "shasum": "" - }, - "require": { - "laravel/serializable-closure": "^1.0 || ^2.0", - "php": ">=8.0", - "php-di/invoker": "^2.0", - "psr/container": "^1.1 || ^2.0" - }, - "provide": { - "psr/container-implementation": "^1.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3", - "friendsofphp/proxy-manager-lts": "^1", - "mnapoli/phpunit-easymock": "^1.3", - "phpunit/phpunit": "^9.6 || ^10 || ^11", - "vimeo/psalm": "^5|^6" - }, - "suggest": { - "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "DI\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "The dependency injection container for humans", - "homepage": "https://php-di.org/", - "keywords": [ - "PSR-11", - "container", - "container-interop", - "dependency injection", - "di", - "ioc", - "psr11" - ], - "support": { - "issues": "https://github.com/PHP-DI/PHP-DI/issues", - "source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.11" - }, - "funding": [ - { - "url": "https://github.com/mnapoli", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/php-di/php-di", - "type": "tidelift" - } - ], - "time": "2025-06-03T07:45:57+00:00" - }, { "name": "phpoption/phpoption", "version": "1.9.3", @@ -3606,6 +4984,80 @@ ], "time": "2023-11-01T08:41:34+00:00" }, + { + "name": "symfony/clock", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, { "name": "symfony/config", "version": "v7.3.0", @@ -3842,6 +5294,83 @@ ], "time": "2024-09-25T14:21:43+00:00" }, + { + "name": "symfony/error-handler", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "cf68d225bc43629de4ff54778029aee6dc191b83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/cf68d225bc43629de4ff54778029aee6dc191b83", + "reference": "cf68d225bc43629de4ff54778029aee6dc191b83", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^6.4|^7.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-29T07:19:49+00:00" + }, { "name": "symfony/event-dispatcher", "version": "v7.3.0", @@ -4128,6 +5657,199 @@ ], "time": "2024-12-30T19:00:26+00:00" }, + { + "name": "symfony/http-foundation", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "4236baf01609667d53b20371486228231eb135fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/4236baf01609667d53b20371486228231eb135fd", + "reference": "4236baf01609667d53b20371486228231eb135fd", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/clock": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-12T14:48:23+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "ac7b8e163e8c83dce3abcc055a502d4486051a9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/ac7b8e163e8c83dce3abcc055a502d4486051a9f", + "reference": "ac7b8e163e8c83dce3abcc055a502d4486051a9f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^7.3", + "symfony/http-foundation": "^7.3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dom-crawler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^7.1", + "symfony/routing": "^6.4|^7.0", + "symfony/serializer": "^7.1", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symfony/var-exporter": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-29T07:47:32+00:00" + }, { "name": "symfony/mailer", "version": "v7.3.0", @@ -4409,6 +6131,87 @@ ], "time": "2025-05-02T09:40:28+00:00" }, + { + "name": "symfony/routing", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "8e213820c5fea844ecea29203d2a308019007c15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/8e213820c5fea844ecea29203d2a308019007c15", + "reference": "8e213820c5fea844ecea29203d2a308019007c15", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-24T20:43:28+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.6.0", @@ -4579,6 +6382,264 @@ ], "time": "2025-04-20T20:19:01+00:00" }, + { + "name": "symfony/translation", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "4aba29076a29a3aa667e09b791e5f868973a8667" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/4aba29076a29a3aa667e09b791e5f868973a8667", + "reference": "4aba29076a29a3aa667e09b791e5f868973a8667", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5|^3.0" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-29T07:19:49+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-27T08:32:26+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/548f6760c54197b1084e1e5c71f6d9d523f2f78e", + "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "ext-iconv": "*", + "symfony/console": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-27T18:39:23+00:00" + }, { "name": "vlucas/phpdotenv", "version": "v5.6.2", @@ -4663,6 +6724,80 @@ ], "time": "2025-04-30T23:37:27+00:00" }, + { + "name": "voku/portable-ascii", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2024-11-21T01:49:47+00:00" + }, { "name": "z4kn4fein/php-semver", "version": "v3.0.0", @@ -7570,90 +9705,6 @@ ], "time": "2025-04-17T09:11:12+00:00" }, - { - "name": "symfony/var-dumper", - "version": "v7.3.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/var-dumper.git", - "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/548f6760c54197b1084e1e5c71f6d9d523f2f78e", - "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "symfony/console": "<6.4" - }, - "require-dev": { - "ext-iconv": "*", - "symfony/console": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", - "twig/twig": "^3.12" - }, - "bin": [ - "Resources/bin/var-dump-server" - ], - "type": "library", - "autoload": { - "files": [ - "Resources/functions/dump.php" - ], - "psr-4": { - "Symfony\\Component\\VarDumper\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides mechanisms for walking through any arbitrary PHP variable", - "homepage": "https://symfony.com", - "keywords": [ - "debug", - "dump" - ], - "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-04-27T18:39:23+00:00" - }, { "name": "ta-tikoma/phpunit-architecture-test", "version": "0.8.5", diff --git a/config/app.php b/config/app.php new file mode 100644 index 000000000..11f203a3e --- /dev/null +++ b/config/app.php @@ -0,0 +1,77 @@ + 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 + ], +]; \ No newline at end of file diff --git a/config/auth.php b/config/auth.php new file mode 100644 index 000000000..43b4271e8 --- /dev/null +++ b/config/auth.php @@ -0,0 +1,92 @@ + [ + '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', + ], +]; \ No newline at end of file diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 000000000..d9506b5f1 --- /dev/null +++ b/config/cache.php @@ -0,0 +1,77 @@ + 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, + ], +]; \ No newline at end of file diff --git a/config/database.php b/config/database.php new file mode 100644 index 000000000..b7be4c6f8 --- /dev/null +++ b/config/database.php @@ -0,0 +1,86 @@ + 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), + ], + ], +]; \ No newline at end of file diff --git a/config/filesystems.php b/config/filesystems.php new file mode 100644 index 000000000..06f4a52fe --- /dev/null +++ b/config/filesystems.php @@ -0,0 +1,71 @@ + 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'), + ], +]; \ No newline at end of file diff --git a/config/routes.php b/config/routes.php new file mode 100644 index 000000000..54bc665ee --- /dev/null +++ b/config/routes.php @@ -0,0 +1,102 @@ + [ + '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', + ], +]; +*/ diff --git a/config/services.php b/config/services.php index 5070cdf57..8f9f29a2c 100644 --- a/config/services.php +++ b/config/services.php @@ -1,30 +1,35 @@ function () { + return \TorrentPier\Config::getInstance(); + }, + // Future service bindings can be added here: + // Logger service example - // 'logger' => factory(function () { + // 'logger' => function () { // $logger = new \Monolog\Logger('torrentpier'); // $logger->pushHandler(new \Monolog\Handler\StreamHandler(__DIR__ . '/../internal_data/logs/app.log')); // return $logger; - // }), + // }, - // Configuration service example - // 'config' => factory(function () { - // return [ - // 'app' => require __DIR__ . '/app.php', - // 'database' => require __DIR__ . '/database.php', - // 'cache' => require __DIR__ . '/cache.php', - // ]; - // }), + // Database service example + // 'database' => function () { + // return \TorrentPier\Database\DB::getInstance(); + // }, - // Interface to implementation binding example - // 'ServiceInterface' => autowire('ConcreteService'), + // Cache service example + // 'cache' => function () { + // return \TorrentPier\Cache\Cache::getInstance(); + // }, ]; diff --git a/config/tracker.php b/config/tracker.php new file mode 100644 index 000000000..7bf49540a --- /dev/null +++ b/config/tracker.php @@ -0,0 +1,91 @@ + 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 +]; \ No newline at end of file diff --git a/ajax.php b/controllers/ajax.php similarity index 86% rename from ajax.php rename to controllers/ajax.php index abe638c8e..bc6fbc562 100644 --- a/ajax.php +++ b/controllers/ajax.php @@ -10,7 +10,10 @@ define('BB_SCRIPT', 'ajax'); 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 $ajax = new TorrentPier\Ajax(); diff --git a/dl.php b/controllers/dl.php similarity index 94% rename from dl.php rename to controllers/dl.php index bfc0ef678..72fbaa5b0 100644 --- a/dl.php +++ b/controllers/dl.php @@ -10,7 +10,11 @@ define('BB_SCRIPT', 'dl'); 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'; $datastore->enqueue([ @@ -170,7 +174,7 @@ if (!IS_AM && ($attachment['mimetype'] === TORRENT_MIMETYPE)) { $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']]); } @@ -219,7 +223,7 @@ switch ($download_mode) { header('Location: ' . $url); exit; 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; $redirect_url = $_POST['redirect_url'] ?? $_SERVER['HTTP_REFERER'] ?? '/'; diff --git a/dl_list.php b/controllers/dl_list.php similarity index 96% rename from dl_list.php rename to controllers/dl_list.php index d31bb701b..56f92f685 100644 --- a/dl_list.php +++ b/controllers/dl_list.php @@ -9,7 +9,10 @@ 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; $topic_id = $_REQUEST[POST_TOPIC_URL] ?? 0; diff --git a/feed.php b/controllers/feed.php similarity index 65% rename from feed.php rename to controllers/feed.php index bbd9eb3e0..5a4a9b46c 100644 --- a/feed.php +++ b/controllers/feed.php @@ -9,7 +9,10 @@ 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 $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'); } } - if (is_file(config()->get('atom.path') . '/f/' . $id . '.atom') && filemtime(config()->get('atom.path') . '/f/' . $id . '.atom') > $timecheck) { - redirect(config()->get('atom.url') . '/f/' . $id . '.atom'); + if (is_file(tp_config()->get('atom.path') . '/f/' . $id . '.atom') && filemtime(tp_config()->get('atom.path') . '/f/' . $id . '.atom') > $timecheck) { + redirect(tp_config()->get('atom.url') . '/f/' . $id . '.atom'); } else { 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 { 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)) { 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) { - redirect(config()->get('atom.url') . '/u/' . floor($id / 5000) . '/' . ($id % 100) . '/' . $id . '.atom'); + 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(tp_config()->get('atom.url') . '/u/' . floor($id / 5000) . '/' . ($id % 100) . '/' . $id . '.atom'); } else { 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 { bb_simple_die($lang['ATOM_NO_USER']); } diff --git a/filelist.php b/controllers/filelist.php similarity index 90% rename from filelist.php rename to controllers/filelist.php index 81a241d20..176882e37 100644 --- a/filelist.php +++ b/controllers/filelist.php @@ -9,12 +9,15 @@ 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 $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); } @@ -55,7 +58,7 @@ if (!is_file($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; $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); } - if ($file_count > config()->get('flist_max_files')) { - bb_die(sprintf($lang['BT_FLIST_LIMIT'], config()->get('flist_max_files'), $file_count), 410); + if ($file_count > tp_config()->get('flist_max_files')) { + bb_die(sprintf($lang['BT_FLIST_LIMIT'], tp_config()->get('flist_max_files'), $file_count), 410); } } diff --git a/group.php b/controllers/group.php similarity index 98% rename from group.php rename to controllers/group.php index e88bb6de7..5fc3f40e7 100644 --- a/group.php +++ b/controllers/group.php @@ -9,7 +9,11 @@ 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'; $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; $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; $rel_limit = 50; @@ -168,7 +172,7 @@ if (!$group_id) { \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 $emailer = new TorrentPier\Emailer(); @@ -224,7 +228,7 @@ if (!$group_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 $emailer = new TorrentPier\Emailer(); @@ -273,7 +277,7 @@ if (!$group_id) { } } // 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 FROM " . BB_USERS . " WHERE user_id IN($sql_in)"; diff --git a/group_edit.php b/controllers/group_edit.php similarity index 83% rename from group_edit.php rename to controllers/group_edit.php index 041365bf4..394a03308 100644 --- a/group_edit.php +++ b/controllers/group_edit.php @@ -9,7 +9,10 @@ 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; @@ -35,10 +38,10 @@ if ($group_id) { if ($is_moderator) { // Avatar 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(); - 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; DB()->query("UPDATE " . BB_GROUPS . " SET avatar_ext_id = $avatar_ext_id WHERE group_id = $group_id LIMIT 1"); } else { @@ -76,7 +79,7 @@ if ($is_moderator) { 'S_HIDDEN_FIELDS' => $s_hidden_fields, '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']), ]); diff --git a/index.php b/controllers/index.php similarity index 89% rename from index.php rename to controllers/index.php index 2cf22e305..cd46233b6 100644 --- a/index.php +++ b/controllers/index.php @@ -9,7 +9,10 @@ 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'] = [ 'post_icons' @@ -31,12 +34,12 @@ $datastore->enqueue([ 'cat_forums' ]); -if (config()->get('show_latest_news')) { +if (tp_config()->get('show_latest_news')) { $datastore->enqueue([ 'latest_news' ]); } -if (config()->get('show_network_news')) { +if (tp_config()->get('show_network_news')) { $datastore->enqueue([ 'network_news' ]); @@ -46,7 +49,7 @@ if (config()->get('show_network_news')) { $user->session_start(); // Set meta description -$page_cfg['meta_description'] = config()->get('site_desc'); +$page_cfg['meta_description'] = tp_config()->get('site_desc'); // Init main vars $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}" : ''; 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_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_TIP' => $f['last_topic_title'], 'LAST_TOPIC_TITLE' => str_short($f['last_topic_title'], $last_topic_max_len), - 'LAST_POST_TIME' => bb_date($f['last_post_time'], config()->get('last_post_date_format')), + 'LAST_POST_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']]), ]); } @@ -278,7 +281,7 @@ $template->assign_vars([ 'TOTAL_TOPICS' => sprintf($lang['POSTED_TOPICS_TOTAL'], $stats['topiccount']), 'TOTAL_POSTS' => sprintf($lang['POSTED_ARTICLES_TOTAL'], $stats['postcount']), 'TOTAL_USERS' => sprintf($lang['REGISTERED_USERS_TOTAL'], $stats['usercount']), - 'TOTAL_GENDER' => config()->get('gender') ? sprintf( + 'TOTAL_GENDER' => tp_config()->get('gender') ? sprintf( $lang['USERS_TOTAL_GENDER'], $stats['male'], $stats['female'], @@ -287,22 +290,22 @@ $template->assign_vars([ 'NEWEST_USER' => sprintf($lang['NEWEST_USER'], profile_url($stats['newestuser'])), // Tracker stats - 'TORRENTS_STAT' => config()->get('tor_stats') ? sprintf( + 'TORRENTS_STAT' => tp_config()->get('tor_stats') ? sprintf( $lang['TORRENTS_STAT'], $stats['torrentcount'], humn_size($stats['size']) ) : '', - 'PEERS_STAT' => config()->get('tor_stats') ? sprintf( + 'PEERS_STAT' => tp_config()->get('tor_stats') ? sprintf( $lang['PEERS_STAT'], $stats['peers'], $stats['seeders'], $stats['leechers'] ) : '', - 'SPEED_STAT' => config()->get('tor_stats') ? sprintf( + 'SPEED_STAT' => tp_config()->get('tor_stats') ? sprintf( $lang['SPEED_STAT'], 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_NEW_IMG' => $images['forum_new'], 'FORUM_LOCKED_IMG' => $images['forum_locked'], @@ -315,19 +318,19 @@ $template->assign_vars([ 'U_SEARCH_SELF_BY_MY' => "search.php?uid={$userdata['user_id']}&o=1", 'U_SEARCH_LATEST' => 'search.php?search_id=latest', 'U_SEARCH_UNANSWERED' => 'search.php?search_id=unanswered', - 'U_ATOM_FEED' => is_file(config()->get('atom.path') . '/f/0.atom') ? make_url(config()->get('atom.url') . '/f/0.atom') : false, + '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, - 'BOARD_START' => config()->get('show_board_start_index') ? ($lang['BOARD_STARTED'] . ': ' . '' . bb_date(config()->get('board_startdate')) . '') : false, + 'BOARD_START' => tp_config()->get('show_board_start_index') ? ($lang['BOARD_STARTED'] . ': ' . '' . bb_date(tp_config()->get('board_startdate')) . '') : false, ]); // 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']); } // Latest news -if (config()->get('show_latest_news')) { +if (tp_config()->get('show_latest_news')) { $latest_news = $datastore->get('latest_news'); if ($latest_news === false) { $datastore->update('latest_news'); @@ -343,7 +346,7 @@ if (config()->get('show_latest_news')) { $template->assign_block_vars('news', [ 'NEWS_TOPIC_ID' => $news['topic_id'], - 'NEWS_TITLE' => str_short(censor()->censorString($news['topic_title']), config()->get('max_news_title')), + 'NEWS_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_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 -if (config()->get('show_network_news')) { +if (tp_config()->get('show_network_news')) { $network_news = $datastore->get('network_news'); if ($network_news === false) { $datastore->update('network_news'); @@ -367,14 +370,14 @@ if (config()->get('show_network_news')) { $template->assign_block_vars('net', [ 'NEWS_TOPIC_ID' => $net['topic_id'], - 'NEWS_TITLE' => str_short(censor()->censorString($net['topic_title']), config()->get('max_net_title')), + 'NEWS_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_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_all = $today_all = false; @@ -388,9 +391,9 @@ if (config()->get('birthday_check_day') && config()->get('birthday_enabled')) { $week_list[] = profile_url($week) . ' (' . birthday_age(date('Y-m-d', strtotime('-1 year', strtotime($week['user_birthday'])))) . ')'; } $week_all = $week_all ? ' ...' : ''; - $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 { - $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'])) { diff --git a/info.php b/controllers/info.php similarity index 89% rename from info.php rename to controllers/info.php index e47c3ef32..eecde3e52 100644 --- a/info.php +++ b/controllers/info.php @@ -9,7 +9,10 @@ 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 $user->session_start(); diff --git a/login.php b/controllers/login.php similarity index 86% rename from login.php rename to controllers/login.php index eb45e7109..1ee720608 100644 --- a/login.php +++ b/controllers/login.php @@ -10,7 +10,10 @@ define('BB_SCRIPT', 'login'); 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'); @@ -63,7 +66,7 @@ $login_password = $_POST['login_password'] ?? ''; $need_captcha = false; if (!$mod_admin_login) { $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; } } @@ -80,13 +83,13 @@ if (isset($_POST['login'])) { } // 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']; } if (!$login_errors) { 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 CACHE('bb_login_err')->rm('l_err_' . USER_IP); @@ -101,7 +104,7 @@ if (isset($_POST['login'])) { if (!$mod_admin_login) { $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; } 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('
', $login_errors), 'ADMIN_LOGIN' => $mod_admin_login, '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'], 'S_LOGIN_ACTION' => LOGIN_URL ]); diff --git a/memberlist.php b/controllers/memberlist.php similarity index 88% rename from memberlist.php rename to controllers/memberlist.php index e70cfc0e3..9886a46d3 100644 --- a/memberlist.php +++ b/controllers/memberlist.php @@ -9,7 +9,10 @@ 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]); @@ -54,26 +57,26 @@ $select_sort_role .= ''; switch ($mode) { 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; 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; 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; 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; 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; case 'topten': $order_by = "user_posts $sort_order LIMIT 10"; break; case 'joined': 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; } @@ -134,7 +137,7 @@ if ($mode != 'topten') { } if ($total = DB()->sql_fetchrow($result)) { $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); } diff --git a/modcp.php b/controllers/modcp.php similarity index 96% rename from modcp.php rename to controllers/modcp.php index 63b059130..4384447b0 100644 --- a/modcp.php +++ b/controllers/modcp.php @@ -9,7 +9,11 @@ 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'; // @@ -223,16 +227,16 @@ switch ($mode) { $result = \TorrentPier\Legacy\Admin\Common::topic_delete($req_topics, $forum_id); //Обновление кеша новостей на главной - $news_forums = array_flip(explode(',', config()->get('latest_news_forum_id'))); - if (isset($news_forums[$forum_id]) && config()->get('show_latest_news') && $result) { + $news_forums = array_flip(explode(',', tp_config()->get('latest_news_forum_id'))); + if (isset($news_forums[$forum_id]) && tp_config()->get('show_latest_news') && $result) { $datastore->enqueue([ 'latest_news' ]); $datastore->update('latest_news'); } - $net_forums = array_flip(explode(',', config()->get('network_news_forum_id'))); - if (isset($net_forums[$forum_id]) && config()->get('show_network_news') && $result) { + $net_forums = array_flip(explode(',', tp_config()->get('network_news_forum_id'))); + if (isset($net_forums[$forum_id]) && tp_config()->get('show_network_news') && $result) { $datastore->enqueue([ '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']); //Обновление кеша новостей на главной - $news_forums = array_flip(explode(',', config()->get('latest_news_forum_id'))); - if ((isset($news_forums[$forum_id]) || isset($news_forums[$new_forum_id])) && config()->get('show_latest_news') && $result) { + $news_forums = array_flip(explode(',', tp_config()->get('latest_news_forum_id'))); + if ((isset($news_forums[$forum_id]) || isset($news_forums[$new_forum_id])) && tp_config()->get('show_latest_news') && $result) { $datastore->enqueue([ 'latest_news' ]); $datastore->update('latest_news'); } - $net_forums = array_flip(explode(',', config()->get('network_news_forum_id'))); - if ((isset($net_forums[$forum_id]) || isset($net_forums[$new_forum_id])) && config()->get('show_network_news') && $result) { + $net_forums = array_flip(explode(',', tp_config()->get('network_news_forum_id'))); + if ((isset($net_forums[$forum_id]) || isset($net_forums[$new_forum_id])) && tp_config()->get('show_network_news') && $result) { $datastore->enqueue([ 'network_news' ]); @@ -557,7 +561,7 @@ switch ($mode) { $poster = $postrow[$i]['username']; $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']; diff --git a/playback_m3u.php b/controllers/playback_m3u.php similarity index 91% rename from playback_m3u.php rename to controllers/playback_m3u.php index 0cdcc3115..e11e21b8b 100644 --- a/playback_m3u.php +++ b/controllers/playback_m3u.php @@ -9,9 +9,12 @@ 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'); } @@ -22,7 +25,7 @@ $validFormats = [ ]; // 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 $page_cfg['allow_robots'] = false; diff --git a/poll.php b/controllers/poll.php similarity index 94% rename from poll.php rename to controllers/poll.php index b770e49c5..620cee370 100644 --- a/poll.php +++ b/controllers/poll.php @@ -9,7 +9,10 @@ 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 $user->session_start(['req_login' => true]); @@ -47,8 +50,8 @@ if ($mode != 'poll_vote') { // Checking the ability to make changes if ($mode == 'poll_delete') { - if ($t_data['topic_time'] < TIMENOW - config()->get('poll_max_days') * 86400) { - bb_die(sprintf(__('NEW_POLL_DAYS'), config()->get('poll_max_days'))); + if ($t_data['topic_time'] < TIMENOW - tp_config()->get('poll_max_days') * 86400) { + bb_die(sprintf(__('NEW_POLL_DAYS'), tp_config()->get('poll_max_days'))); } if (!IS_ADMIN && ($t_data['topic_vote'] != POLL_FINISHED)) { bb_die(__('CANNOT_DELETE_POLL')); diff --git a/posting.php b/controllers/posting.php similarity index 94% rename from posting.php rename to controllers/posting.php index 5fb5146cb..d05dfbb93 100644 --- a/posting.php +++ b/controllers/posting.php @@ -9,7 +9,11 @@ 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 ATTACH_DIR . '/attachment_mod.php'; @@ -221,7 +225,7 @@ if (!$is_auth[$is_auth_type]) { } 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 FROM " . BB_BT_TORRENTS . " tor, " . BB_TOPICS . " t WHERE poster_id = {$userdata['user_id']} @@ -232,7 +236,7 @@ if ($mode == 'new_rel') { $topics = ''; foreach ($sql as $row) { - $topics .= config()->get('tor_icons')[$row['tor_status']] . '' . $row['topic_title'] . '
'; + $topics .= tp_config()->get('tor_icons')[$row['tor_status']] . '' . $row['topic_title'] . '
'; } if ($topics && !(IS_SUPER_ADMIN && !empty($_REQUEST['edit_tpl']))) { bb_die($topics . $lang['UNEXECUTED_RELEASE']); @@ -243,9 +247,9 @@ if ($mode == 'new_rel') { } // 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 ($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")) { - bb_die($lang['NOT_EDIT_TOR_STATUS'] . ': ' . config()->get('tor_icons')[$tor_status['tor_status']] . ' ' . $lang['TOR_STATUS_NAME'][$tor_status['tor_status']] . '.'); +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(',', tp_config()->get('tor_cannot_edit')) . ") LIMIT 1")) { + bb_die($lang['NOT_EDIT_TOR_STATUS'] . ': ' . tp_config()->get('tor_icons')[$tor_status['tor_status']] . ' ' . $lang['TOR_STATUS_NAME'][$tor_status['tor_status']] . '.'); } } @@ -281,7 +285,7 @@ if (!IS_GUEST && $mode != 'newtopic' && ($submit || $preview || $mode == 'quote' AND pt.post_id = p.post_id AND p.post_time > $topic_last_read ORDER BY p.post_time - LIMIT " . config()->get('posts_per_page'); + LIMIT " . tp_config()->get('posts_per_page'); if ($rowset = DB()->fetch_rowset($sql)) { $topic_has_new_posts = true; @@ -291,7 +295,7 @@ if (!IS_GUEST && $mode != 'newtopic' && ($submit || $preview || $mode == 'quote' 'ROW_CLASS' => !($i % 2) ? 'row1' : 'row2', 'POSTER' => profile_url($row), 'POSTER_NAME_JS' => addslashes($row['username']), - 'POST_DATE' => '' . bb_date($row['post_time'], config()->get('post_date_format')) . '', + 'POST_DATE' => '' . bb_date($row['post_time'], tp_config()->get('post_date_format')) . '', 'MESSAGE' => get_parsed_post($row) ]); } @@ -374,9 +378,9 @@ if (($delete || $mode == 'delete') && !$confirm) { 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 (config()->get('premod')) { + if (tp_config()->get('premod')) { // Getting a list of forum ids starting with "parent" $forum_parent = $forum_id; 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]'; // 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 $message = preg_replace('#(?<=[\?&;]sid=)[a-zA-Z0-9]#', 'sid', $message); @@ -618,7 +622,7 @@ $template->assign_vars([ 'U_VIEW_FORUM' => FORUM_URL . $forum_id, '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, 'MESSAGE' => $message, diff --git a/privmsg.php b/controllers/privmsg.php similarity index 97% rename from privmsg.php rename to controllers/privmsg.php index 409d0aacb..a24346eae 100644 --- a/privmsg.php +++ b/controllers/privmsg.php @@ -10,7 +10,11 @@ define('BB_SCRIPT', 'pm'); 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'; $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? // -if (config()->get('privmsg_disable')) { +if (tp_config()->get('privmsg_disable')) { bb_die('PM_DISABLED'); } @@ -59,13 +63,13 @@ $user->session_start(['req_login' => true]); $template->assign_vars([ '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 // -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'; } @@ -206,7 +210,7 @@ if ($mode == 'read') { } 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 . " WHERE privmsgs_type = " . PRIVMSGS_SENT_MAIL . " AND privmsgs_date = " . $sent_info['oldest_post_time'] . " @@ -604,7 +608,7 @@ if ($mode == 'read') { } 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 . " WHERE ( ( privmsgs_to_userid = " . $userdata['user_id'] . " AND privmsgs_type = " . PRIVMSGS_SAVED_IN_MAIL . " ) @@ -749,7 +753,7 @@ if ($mode == 'read') { $last_post_time = $db_row['last_post_time']; $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']); } } @@ -802,11 +806,11 @@ if ($mode == 'read') { } // Check smilies limit - if (config()->get('max_smilies_pm')) { - $count_smilies = substr_count(bbcode2html($privmsg_message), '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 $emailer = new TorrentPier\Emailer(); @@ -1252,7 +1256,7 @@ if ($mode == 'read') { $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_tot .= $limit_msg_time_total; @@ -1308,11 +1312,11 @@ if ($mode == 'read') { // Output data for inbox 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) { $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); $template->assign_var('PM_BOX_SIZE_INFO'); @@ -1410,7 +1414,7 @@ if ($mode == 'read') { ]); } 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 { $template->assign_block_vars('switch_no_messages', []); } diff --git a/profile.php b/controllers/profile.php similarity index 89% rename from profile.php rename to controllers/profile.php index 9f036bf7e..de1f7706f 100644 --- a/profile.php +++ b/controllers/profile.php @@ -10,7 +10,10 @@ define('BB_SCRIPT', 'profile'); 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 $user->session_start(); diff --git a/search.php b/controllers/search.php similarity index 97% rename from search.php rename to controllers/search.php index 7075e6a23..ac367aecb 100644 --- a/search.php +++ b/controllers/search.php @@ -9,7 +9,11 @@ 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'; $page_cfg['use_tablesorter'] = true; @@ -20,7 +24,7 @@ $page_cfg['load_tpl_vars'] = [ ]; // 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(); @@ -289,7 +293,7 @@ if (empty($_GET) && empty($_POST)) { 'MY_TOPICS_ID' => '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), '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'), @@ -421,7 +425,7 @@ $prev_days = ($time_val != $search_all); $new_topics = (!IS_GUEST && ($new_topics_val || isset($_GET['newposts']))); $my_topics = ($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) $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) { $order = $order_opt[$order_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; // Run initial search for post_ids @@ -594,7 +598,7 @@ if ($post_mode) { 'POSTER_ID' => $post['poster_id'], 'POSTER' => profile_url($post), '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), 'MESSAGE' => $message, 'POSTED_AFTER' => '', @@ -613,7 +617,7 @@ if ($post_mode) { else { $order = $order_opt[$order_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; // Run initial search for topic_ids @@ -739,7 +743,7 @@ else { // Build SQL for displaying topics $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'][] = " 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']), 'IS_UNREAD' => $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'], 'ATTACH' => $topic['topic_attachment'], 'STATUS' => $topic['topic_status'], @@ -894,13 +898,13 @@ function fetch_search_ids($sql, $search_type = SEARCH_TYPE_POST) 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['ORDER BY'] = []; $SQL['LIMIT'] = array('0'); 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_die('Too_many_search_results'); } diff --git a/terms.php b/controllers/terms.php similarity index 70% rename from terms.php rename to controllers/terms.php index e3598073f..c1a37c9b5 100644 --- a/terms.php +++ b/controllers/terms.php @@ -9,19 +9,22 @@ define('BB_SCRIPT', 'terms'); -require __DIR__ . '/common.php'; +// Skip loading common.php if already loaded (when run through routing system) +if (!defined('IN_TORRENTPIER')) { + require __DIR__ . '/../common.php'; +} require INC_DIR . '/bbcode.php'; // Start session management $user->session_start(); -if (!config()->get('terms') && !IS_ADMIN) { +if (!tp_config()->get('terms') && !IS_ADMIN) { redirect('index.php'); } $template->assign_vars([ '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'); diff --git a/tracker.php b/controllers/tracker.php similarity index 97% rename from tracker.php rename to controllers/tracker.php index ddb56a51f..d3ab3eca0 100644 --- a/tracker.php +++ b/controllers/tracker.php @@ -9,7 +9,10 @@ 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_cfg['include_bbcode_js'] = true; @@ -19,7 +22,7 @@ $page_cfg['load_tpl_vars'] = [ ]; // 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(); @@ -30,7 +33,7 @@ $max_forums_selected = 50; $title_match_max_len = 60; $poster_name_max_len = 25; $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__); $time_format = 'H:i'; @@ -299,8 +302,8 @@ if (isset($_GET[$user_releases_key])) { } // Random release -if (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 (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(tp_config()->get('tor_frozen'))) . ") ORDER BY RAND() LIMIT 1")) { redirect(TOPIC_URL . $random_release['topic_id']); } else { bb_die($lang['NO_MATCH']); @@ -749,8 +752,8 @@ if ($allowed_forums) { 'MAGNET' => $tor_magnet, 'TOR_TYPE' => is_gold($tor['tor_type']), - 'TOR_FROZEN' => !IS_AM ? isset(config()->get('tor_frozen')[$tor['tor_status']]) : '', - 'TOR_STATUS_ICON' => config()->get('tor_icons')[$tor['tor_status']], + 'TOR_FROZEN' => !IS_AM ? isset(tp_config()->get('tor_frozen')[$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_SIZE_RAW' => $size, @@ -819,9 +822,9 @@ $search_all_opt = '