mirror of
https://github.com/torrentpier/torrentpier
synced 2025-08-22 06:13:58 -07:00
Merge e5ca170b4c
into 5b5bf49f4e
This commit is contained in:
commit
1e14fa9098
1590 changed files with 9385 additions and 5736 deletions
|
@ -1,7 +0,0 @@
|
|||
9766c534bddad8e82e6d19f9bad5cf70b9887f9a
|
||||
92ce77ec0ec703c08a659419087a373f76e711f7
|
||||
2d53efc945c7747be1755d0b66557a86bdc12cbd
|
||||
602137b65129b817811b80975a369ebde3270c6d
|
||||
4eb26ae37e1f4c82a45961517ffeb54c20200408
|
||||
e59adce848a9e10ee5775254045cbbd915236b8b
|
||||
9e0a64108d62236ab07b3f8d10e8c78269b8e1d1
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
name: Feature / Enhancement request
|
||||
about: Suggest an idea for TorrentPier
|
||||
title: "[Feature]"
|
||||
labels: [Feature, Enhancement]
|
||||
assignees: ''
|
||||
---
|
80
.github/workflows/cd.yml
vendored
80
.github/workflows/cd.yml
vendored
|
@ -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
|
74
.github/workflows/ci.yml
vendored
74
.github/workflows/ci.yml
vendored
|
@ -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
|
57
.github/workflows/phpmd.yml
vendored
57
.github/workflows/phpmd.yml
vendored
|
@ -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
|
41
.github/workflows/schedule.yml
vendored
41
.github/workflows/schedule.yml
vendored
|
@ -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
|
18
.htaccess
18
.htaccess
|
@ -1,18 +0,0 @@
|
|||
## set default server charset
|
||||
AddDefaultCharset UTF-8
|
||||
|
||||
## folder listing access control
|
||||
Options All -Indexes
|
||||
|
||||
## sitemap and atom rewrite
|
||||
RewriteEngine On
|
||||
RewriteRule ^sitemap.xml$ sitemap/sitemap.xml [L]
|
||||
RewriteRule ^/internal_data/atom/(.*) /atom$1 [L]
|
||||
|
||||
## deny access to git folder
|
||||
RedirectMatch 404 /\\.git(/|$)
|
||||
|
||||
## deny access to system files
|
||||
<FilesMatch "\.(.*sql|tpl|db|inc|log|md|env)|(config|common).php$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
266
CLAUDE.md
266
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
|
||||
|
||||
|
|
137
README.md
137
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> # 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.
|
||||
|
|
|
@ -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
|
||||
|
|
57
_cleanup.php
57
_cleanup.php
|
@ -1,57 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* TorrentPier – Bull-powered BitTorrent tracker engine
|
||||
*
|
||||
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
|
||||
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
|
||||
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
|
||||
*/
|
||||
|
||||
if (!defined('BB_ROOT')) {
|
||||
define('BB_ROOT', __DIR__ . DIRECTORY_SEPARATOR);
|
||||
define('BB_PATH', BB_ROOT);
|
||||
}
|
||||
|
||||
// Check CLI mode
|
||||
if (PHP_SAPI != 'cli') {
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!function_exists('removeFile')) {
|
||||
// Get all constants
|
||||
require_once BB_ROOT . 'library/defines.php';
|
||||
|
||||
// Include CLI functions
|
||||
require INC_DIR . '/functions_cli.php';
|
||||
}
|
||||
|
||||
$items = [
|
||||
'.github',
|
||||
'.cliffignore',
|
||||
'.editorconfig',
|
||||
'.gitignore',
|
||||
'.styleci.yml',
|
||||
'_release.php',
|
||||
'CHANGELOG.md',
|
||||
'CLAUDE.md',
|
||||
'cliff.toml',
|
||||
'CODE_OF_CONDUCT.md',
|
||||
'CONTRIBUTING.md',
|
||||
'crowdin.yml',
|
||||
'HISTORY.md',
|
||||
'phpunit.xml',
|
||||
'README.md',
|
||||
'SECURITY.md',
|
||||
'tests',
|
||||
'UPGRADE_GUIDE.md'
|
||||
];
|
||||
|
||||
foreach ($items as $item) {
|
||||
$path = BB_ROOT . $item;
|
||||
|
||||
if (is_file($path)) {
|
||||
removeFile($path);
|
||||
} elseif (is_dir($path)) {
|
||||
removeDir($path);
|
||||
}
|
||||
}
|
130
_release.php
130
_release.php
|
@ -1,130 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* TorrentPier – Bull-powered BitTorrent tracker engine
|
||||
*
|
||||
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
|
||||
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
|
||||
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
|
||||
*/
|
||||
|
||||
define('BB_ROOT', __DIR__ . DIRECTORY_SEPARATOR);
|
||||
define('BB_PATH', BB_ROOT);
|
||||
|
||||
// Check CLI mode
|
||||
if (PHP_SAPI != 'cli') {
|
||||
die('Please run <code style="background:#222;color:#00e01f;padding:2px 6px;border-radius:3px;">php ' . basename(__FILE__) . '</code> in CLI mode');
|
||||
}
|
||||
|
||||
// Get all constants
|
||||
require_once BB_ROOT . 'library/defines.php';
|
||||
|
||||
// Include CLI functions
|
||||
require INC_DIR . '/functions_cli.php';
|
||||
|
||||
// Welcoming message
|
||||
out("--- Release creation tool ---\n", 'info');
|
||||
|
||||
$configFile = BB_PATH . '/library/config.php';
|
||||
|
||||
if (!is_file($configFile)) {
|
||||
out('- Config file ' . basename($configFile) . ' not found', 'error');
|
||||
exit;
|
||||
}
|
||||
if (!is_readable($configFile)) {
|
||||
out('- Config file ' . basename($configFile) . ' is not readable', 'error');
|
||||
exit;
|
||||
}
|
||||
if (!is_writable($configFile)) {
|
||||
out('- Config file ' . basename($configFile) . ' is not writable', 'error');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ask for version
|
||||
fwrite(STDOUT, 'Enter version number (e.g, v2.4.0): ');
|
||||
$version = trim(fgets(STDIN));
|
||||
|
||||
if (empty($version)) {
|
||||
out("- Version cannot be empty. Please enter a valid version number", 'error');
|
||||
exit;
|
||||
} else {
|
||||
// Add 'v' prefix if missing
|
||||
if (!str_starts_with($version, 'v')) {
|
||||
$version = 'v' . $version;
|
||||
}
|
||||
|
||||
out("- Using version: $version", 'info');
|
||||
}
|
||||
|
||||
// Ask for version emoji
|
||||
fwrite(STDOUT, 'Enter version emoji: ');
|
||||
$versionEmoji = trim(fgets(STDIN));
|
||||
|
||||
if (!empty($versionEmoji)) {
|
||||
out("- Using version emoji: $versionEmoji", 'info');
|
||||
}
|
||||
|
||||
// Ask for release date or use today's date
|
||||
fwrite(STDOUT, "Enter release date (e.g. 25-05-2025), leave empty to use today's date: ");
|
||||
$date = trim(fgets(STDIN));
|
||||
|
||||
if (empty($date)) {
|
||||
$date = date('d-m-Y');
|
||||
out("- Using current date: $date", 'info');
|
||||
} else {
|
||||
// Validate date format (dd-mm-yyyy)
|
||||
$dateObj = DateTime::createFromFormat('d-m-Y', $date);
|
||||
if (!$dateObj || $dateObj->format('d-m-Y') !== $date) {
|
||||
out("- Invalid date format. Expected format: DD-MM-YYYY", 'error');
|
||||
exit;
|
||||
}
|
||||
|
||||
out("- Using date: $date", 'info');
|
||||
}
|
||||
|
||||
// Read config file content
|
||||
$content = file_get_contents($configFile);
|
||||
|
||||
// Update version
|
||||
$content = preg_replace(
|
||||
"/\\\$bb_cfg\['tp_version'\]\s*=\s*'[^']*';/",
|
||||
"\$bb_cfg['tp_version'] = '$version';",
|
||||
$content
|
||||
);
|
||||
|
||||
// Update release date
|
||||
$content = preg_replace(
|
||||
"/\\\$bb_cfg\['tp_release_date'\]\s*=\s*'[^']*';/",
|
||||
"\$bb_cfg['tp_release_date'] = '$date';",
|
||||
$content
|
||||
);
|
||||
|
||||
// Save updated config
|
||||
$bytesWritten = file_put_contents($configFile, $content);
|
||||
|
||||
if ($bytesWritten === false) {
|
||||
out("- Failed to write to config file", 'error');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($bytesWritten === 0) {
|
||||
out("- Config file was not updated (0 bytes written)", 'error');
|
||||
exit;
|
||||
}
|
||||
|
||||
out("\n- Config file has been updated!", 'success');
|
||||
|
||||
// Update CHANGELOG.md
|
||||
runProcess('npx git-cliff v2.4.6-alpha.4.. --config cliff.toml --tag "' . $version . '" > CHANGELOG.md');
|
||||
|
||||
// Git add & commit
|
||||
runProcess('git add -A && git commit -m "release: ' . escapeshellarg($version) . (!empty($versionEmoji) ? (' ' . $versionEmoji) : '') . '"');
|
||||
|
||||
// Git tag
|
||||
runProcess("git tag -a \"$version\" -m \"Release $version\"");
|
||||
runProcess("git tag -v \"$version\"");
|
||||
|
||||
// Git push
|
||||
runProcess("git push origin master");
|
||||
runProcess("git push origin $version");
|
||||
|
||||
out("\n- Release $version has been successfully prepared, committed and pushed!", 'success');
|
116
app/Console/Commands/ClearCacheCommand.php
Normal file
116
app/Console/Commands/ClearCacheCommand.php
Normal file
|
@ -0,0 +1,116 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
/**
|
||||
* Clear Cache Command
|
||||
*
|
||||
* Clears application cache files
|
||||
*/
|
||||
class ClearCacheCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The command signature
|
||||
*/
|
||||
protected string $signature = 'cache:clear';
|
||||
|
||||
/**
|
||||
* The command description
|
||||
*/
|
||||
protected string $description = 'Clear application cache';
|
||||
|
||||
/**
|
||||
* Configure the command
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption(
|
||||
'force',
|
||||
'f',
|
||||
InputOption::VALUE_NONE,
|
||||
'Force clearing cache without confirmation'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the command
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$force = $this->option('force');
|
||||
|
||||
if (!$force && !$this->confirm('Are you sure you want to clear all cache?', true)) {
|
||||
$this->info('Cache clear cancelled.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info('Clearing application cache...');
|
||||
|
||||
$cleared = 0;
|
||||
|
||||
// Clear file cache
|
||||
$cacheDir = $this->app->make('path.base') . '/storage/framework/cache';
|
||||
if (is_dir($cacheDir)) {
|
||||
$files = glob($cacheDir . '/*');
|
||||
foreach ($files as $file) {
|
||||
if (is_file($file)) {
|
||||
unlink($file);
|
||||
$cleared++;
|
||||
}
|
||||
}
|
||||
$this->line("✓ File cache cleared ({$cleared} files)");
|
||||
}
|
||||
|
||||
// Clear view cache
|
||||
$viewCacheDir = $this->app->make('path.base') . '/storage/framework/views';
|
||||
if (is_dir($viewCacheDir)) {
|
||||
$viewFiles = glob($viewCacheDir . '/*');
|
||||
$viewCleared = 0;
|
||||
foreach ($viewFiles as $file) {
|
||||
if (is_file($file)) {
|
||||
unlink($file);
|
||||
$viewCleared++;
|
||||
}
|
||||
}
|
||||
$this->line("✓ View cache cleared ({$viewCleared} files)");
|
||||
$cleared += $viewCleared;
|
||||
}
|
||||
|
||||
// Clear legacy cache directories
|
||||
$legacyCacheDir = $this->app->make('path.base') . '/internal_data/cache';
|
||||
if (is_dir($legacyCacheDir)) {
|
||||
$legacyCleared = $this->clearDirectoryRecursive($legacyCacheDir);
|
||||
$this->line("✓ Legacy cache cleared ({$legacyCleared} files)");
|
||||
$cleared += $legacyCleared;
|
||||
}
|
||||
|
||||
$this->success("Cache cleared successfully! Total files removed: {$cleared}");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively clear a directory
|
||||
*/
|
||||
private function clearDirectoryRecursive(string $dir): int
|
||||
{
|
||||
$cleared = 0;
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile()) {
|
||||
unlink($file->getRealPath());
|
||||
$cleared++;
|
||||
}
|
||||
}
|
||||
|
||||
return $cleared;
|
||||
}
|
||||
}
|
207
app/Console/Commands/Command.php
Normal file
207
app/Console/Commands/Command.php
Normal file
|
@ -0,0 +1,207 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Symfony\Component\Console\Command\Command as SymfonyCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Illuminate\Container\Container;
|
||||
|
||||
/**
|
||||
* Base Command Class
|
||||
*
|
||||
* Laravel-style base command class for TorrentPier console commands
|
||||
*/
|
||||
abstract class Command extends SymfonyCommand
|
||||
{
|
||||
/**
|
||||
* The command signature (Laravel-style)
|
||||
* Example: 'cache:clear {--force : Force clearing without confirmation}'
|
||||
*/
|
||||
protected string $signature = '';
|
||||
|
||||
/**
|
||||
* The command description
|
||||
*/
|
||||
protected string $description = '';
|
||||
|
||||
/**
|
||||
* Application container
|
||||
*/
|
||||
protected Container $app;
|
||||
|
||||
/**
|
||||
* Console input interface
|
||||
*/
|
||||
protected InputInterface $input;
|
||||
|
||||
/**
|
||||
* Console output interface
|
||||
*/
|
||||
protected OutputInterface $output;
|
||||
|
||||
/**
|
||||
* Symfony style interface
|
||||
*/
|
||||
protected SymfonyStyle $io;
|
||||
|
||||
/**
|
||||
* Create a new command instance
|
||||
*/
|
||||
public function __construct(?string $name = null)
|
||||
{
|
||||
// Parse signature if provided
|
||||
if ($this->signature) {
|
||||
$name = $this->parseSignature();
|
||||
}
|
||||
|
||||
parent::__construct($name);
|
||||
|
||||
// Set description
|
||||
if ($this->description) {
|
||||
$this->setDescription($this->description);
|
||||
}
|
||||
|
||||
// Get container instance
|
||||
$this->app = Container::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the command
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->input = $input;
|
||||
$this->output = $output;
|
||||
$this->io = new SymfonyStyle($input, $output);
|
||||
|
||||
try {
|
||||
$result = $this->handle();
|
||||
return is_int($result) ? $result : self::SUCCESS;
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Command failed: ' . $e->getMessage());
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the command (implement in subclasses)
|
||||
*/
|
||||
abstract public function handle(): int;
|
||||
|
||||
/**
|
||||
* Parse Laravel-style signature
|
||||
*/
|
||||
protected function parseSignature(): string
|
||||
{
|
||||
// Simple signature parsing - just extract command name for now
|
||||
// Full Laravel signature parsing would be more complex
|
||||
$parts = explode(' ', trim($this->signature));
|
||||
return $parts[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an argument value
|
||||
*/
|
||||
protected function argument(?string $key = null): mixed
|
||||
{
|
||||
if ($key === null) {
|
||||
return $this->input->getArguments();
|
||||
}
|
||||
|
||||
return $this->input->getArgument($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an option value
|
||||
*/
|
||||
protected function option(?string $key = null): mixed
|
||||
{
|
||||
if ($key === null) {
|
||||
return $this->input->getOptions();
|
||||
}
|
||||
|
||||
return $this->input->getOption($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display an info message
|
||||
*/
|
||||
protected function info(string $message): void
|
||||
{
|
||||
$this->io->info($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display an error message
|
||||
*/
|
||||
protected function error(string $message): void
|
||||
{
|
||||
$this->io->error($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a warning message
|
||||
*/
|
||||
protected function warn(string $message): void
|
||||
{
|
||||
$this->io->warning($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a success message
|
||||
*/
|
||||
protected function success(string $message): void
|
||||
{
|
||||
$this->io->success($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a line of text
|
||||
*/
|
||||
protected function line(string $message): void
|
||||
{
|
||||
$this->output->writeln($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask a question
|
||||
*/
|
||||
protected function ask(string $question, ?string $default = null): ?string
|
||||
{
|
||||
return $this->io->ask($question, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask for confirmation
|
||||
*/
|
||||
protected function confirm(string $question, bool $default = false): bool
|
||||
{
|
||||
return $this->io->confirm($question, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the user to select from a list of options
|
||||
*/
|
||||
protected function choice(string $question, array $choices, ?string $default = null): string
|
||||
{
|
||||
return $this->io->choice($question, $choices, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get application configuration
|
||||
*/
|
||||
protected function config(?string $key = null, mixed $default = null): mixed
|
||||
{
|
||||
$config = $this->app->make('config');
|
||||
|
||||
if ($key === null) {
|
||||
return $config;
|
||||
}
|
||||
|
||||
return $config->get($key, $default);
|
||||
}
|
||||
}
|
113
app/Console/Commands/InfoCommand.php
Normal file
113
app/Console/Commands/InfoCommand.php
Normal file
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
/**
|
||||
* Info Command
|
||||
*
|
||||
* Display TorrentPier system information
|
||||
*/
|
||||
class InfoCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The command signature
|
||||
*/
|
||||
protected string $signature = 'info';
|
||||
|
||||
/**
|
||||
* The command description
|
||||
*/
|
||||
protected string $description = 'Display system information';
|
||||
|
||||
/**
|
||||
* Handle the command
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$basePath = $this->app->make('path.base');
|
||||
|
||||
$this->line('');
|
||||
$this->line('<fg=cyan>TorrentPier System Information</>');
|
||||
$this->line('<fg=cyan>==============================</>');
|
||||
$this->line('');
|
||||
|
||||
// Application info
|
||||
$this->line('<fg=yellow>Application:</>');
|
||||
$this->line(' Name: TorrentPier');
|
||||
$this->line(' Version: 3.0-dev');
|
||||
$this->line(' Environment: ' . ($_ENV['APP_ENV'] ?? 'production'));
|
||||
$this->line(' Debug Mode: ' . (($_ENV['APP_DEBUG'] ?? false) ? 'enabled' : 'disabled'));
|
||||
$this->line('');
|
||||
|
||||
// Paths
|
||||
$this->line('<fg=yellow>Paths:</>');
|
||||
$this->line(' Base: ' . $basePath);
|
||||
$this->line(' App: ' . $this->app->make('path.app'));
|
||||
$this->line(' Config: ' . $this->app->make('path.config'));
|
||||
$this->line(' Storage: ' . $this->app->make('path.storage'));
|
||||
$this->line(' Public: ' . $this->app->make('path.public'));
|
||||
$this->line('');
|
||||
|
||||
// PHP info
|
||||
$this->line('<fg=yellow>PHP:</>');
|
||||
$this->line(' Version: ' . PHP_VERSION);
|
||||
$this->line(' SAPI: ' . PHP_SAPI);
|
||||
$this->line(' Memory Limit: ' . ini_get('memory_limit'));
|
||||
$this->line(' Max Execution Time: ' . ini_get('max_execution_time') . 's');
|
||||
$this->line('');
|
||||
|
||||
// Extensions
|
||||
$requiredExtensions = ['pdo', 'curl', 'gd', 'mbstring', 'openssl', 'zip'];
|
||||
$this->line('<fg=yellow>Required Extensions:</>');
|
||||
foreach ($requiredExtensions as $ext) {
|
||||
$status = extension_loaded($ext) ? '<fg=green>✓</>' : '<fg=red>✗</>';
|
||||
$this->line(" {$status} {$ext}");
|
||||
}
|
||||
$this->line('');
|
||||
|
||||
// File permissions
|
||||
$this->line('<fg=yellow>File Permissions:</>');
|
||||
$writablePaths = [
|
||||
'storage',
|
||||
'storage/app',
|
||||
'storage/framework',
|
||||
'storage/logs',
|
||||
'internal_data/cache',
|
||||
'data/uploads'
|
||||
];
|
||||
|
||||
foreach ($writablePaths as $path) {
|
||||
$fullPath = $basePath . '/' . $path;
|
||||
if (file_exists($fullPath)) {
|
||||
$writable = is_writable($fullPath);
|
||||
$status = $writable ? '<fg=green>✓</>' : '<fg=red>✗</>';
|
||||
$this->line(" {$status} {$path}");
|
||||
} else {
|
||||
$this->line(" <fg=yellow>?</> {$path} (not found)");
|
||||
}
|
||||
}
|
||||
$this->line('');
|
||||
|
||||
// Database
|
||||
try {
|
||||
if ($this->app->bound('config')) {
|
||||
$config = $this->app->make('config');
|
||||
$dbConfig = $config->get('database', []);
|
||||
|
||||
if (!empty($dbConfig)) {
|
||||
$this->line('<fg=yellow>Database:</>');
|
||||
$this->line(' Host: ' . ($dbConfig['host'] ?? 'not configured'));
|
||||
$this->line(' Database: ' . ($dbConfig['dbname'] ?? 'not configured'));
|
||||
$this->line(' Driver: ' . ($dbConfig['driver'] ?? 'not configured'));
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->line('<fg=yellow>Database:</> Configuration error');
|
||||
}
|
||||
|
||||
$this->line('');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
101
app/Console/Commands/MigrateCommand.php
Normal file
101
app/Console/Commands/MigrateCommand.php
Normal file
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
/**
|
||||
* Migrate Command
|
||||
*
|
||||
* Run database migrations using Phinx
|
||||
*/
|
||||
class MigrateCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The command signature
|
||||
*/
|
||||
protected string $signature = 'migrate';
|
||||
|
||||
/**
|
||||
* The command description
|
||||
*/
|
||||
protected string $description = 'Run database migrations';
|
||||
|
||||
/**
|
||||
* Configure the command
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption(
|
||||
'fake',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Mark migrations as run without actually running them'
|
||||
)
|
||||
->addOption(
|
||||
'target',
|
||||
't',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Target migration version'
|
||||
)
|
||||
->addOption(
|
||||
'force',
|
||||
'f',
|
||||
InputOption::VALUE_NONE,
|
||||
'Force running migrations in production'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the command
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$basePath = $this->app->make('path.base');
|
||||
$phinxConfig = $basePath . '/phinx.php';
|
||||
|
||||
if (!file_exists($phinxConfig)) {
|
||||
$this->error('Phinx configuration file not found at: ' . $phinxConfig);
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info('Running database migrations...');
|
||||
|
||||
// Build phinx command
|
||||
$command = 'cd ' . escapeshellarg($basePath) . ' && ';
|
||||
$command .= 'vendor/bin/phinx migrate';
|
||||
$command .= ' --configuration=' . escapeshellarg($phinxConfig);
|
||||
|
||||
if ($this->option('fake')) {
|
||||
$command .= ' --fake';
|
||||
}
|
||||
|
||||
if ($this->option('target')) {
|
||||
$command .= ' --target=' . escapeshellarg($this->option('target'));
|
||||
}
|
||||
|
||||
if ($this->option('force')) {
|
||||
$command .= ' --no-interaction';
|
||||
}
|
||||
|
||||
// Execute the command
|
||||
$output = [];
|
||||
$returnCode = 0;
|
||||
exec($command . ' 2>&1', $output, $returnCode);
|
||||
|
||||
// Display output
|
||||
foreach ($output as $line) {
|
||||
$this->line($line);
|
||||
}
|
||||
|
||||
if ($returnCode === 0) {
|
||||
$this->success('Migrations completed successfully!');
|
||||
} else {
|
||||
$this->error('Migration failed with exit code: ' . $returnCode);
|
||||
}
|
||||
|
||||
return $returnCode === 0 ? self::SUCCESS : self::FAILURE;
|
||||
}
|
||||
}
|
36
app/Container/Container.php
Normal file
36
app/Container/Container.php
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Container;
|
||||
|
||||
use Illuminate\Container\Container as IlluminateContainer;
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Illuminate\Contracts\Container\CircularDependencyException;
|
||||
|
||||
/**
|
||||
* Application Container
|
||||
*
|
||||
* Extends Illuminate Container with application-specific functionality
|
||||
*/
|
||||
class Container extends IlluminateContainer
|
||||
{
|
||||
/**
|
||||
* Get a service from the container
|
||||
*
|
||||
* @throws BindingResolutionException When the service cannot be resolved
|
||||
* @throws CircularDependencyException
|
||||
*/
|
||||
public function get(string $id): mixed
|
||||
{
|
||||
return $this->resolve($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a service exists in the container
|
||||
*/
|
||||
public function has(string $id): bool
|
||||
{
|
||||
return $this->bound($id);
|
||||
}
|
||||
}
|
66
app/Events/TorrentUploaded.php
Normal file
66
app/Events/TorrentUploaded.php
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use DateTimeInterface;
|
||||
|
||||
/**
|
||||
* Event fired when a new torrent is uploaded
|
||||
*/
|
||||
readonly class TorrentUploaded
|
||||
{
|
||||
/**
|
||||
* Create a new event instance
|
||||
*/
|
||||
public function __construct(
|
||||
public int $torrentId,
|
||||
public int $uploaderId,
|
||||
public string $torrentName,
|
||||
public int $size,
|
||||
public DateTimeInterface $uploadedAt
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the torrent ID
|
||||
*/
|
||||
public function getTorrentId(): int
|
||||
{
|
||||
return $this->torrentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the uploader user ID
|
||||
*/
|
||||
public function getUploaderId(): int
|
||||
{
|
||||
return $this->uploaderId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the torrent name
|
||||
*/
|
||||
public function getTorrentName(): string
|
||||
{
|
||||
return $this->torrentName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the torrent size in bytes
|
||||
*/
|
||||
public function getSize(): int
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the upload timestamp
|
||||
*/
|
||||
public function getUploadedAt(): DateTimeInterface
|
||||
{
|
||||
return $this->uploadedAt;
|
||||
}
|
||||
}
|
57
app/Events/UserRegistered.php
Normal file
57
app/Events/UserRegistered.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use DateTimeInterface;
|
||||
|
||||
/**
|
||||
* Event fired when a new user is registered
|
||||
*/
|
||||
readonly class UserRegistered
|
||||
{
|
||||
/**
|
||||
* Create a new event instance
|
||||
*/
|
||||
public function __construct(
|
||||
public int $userId,
|
||||
public string $username,
|
||||
public string $email,
|
||||
public DateTimeInterface $registeredAt
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user ID
|
||||
*/
|
||||
public function getUserId(): int
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the username
|
||||
*/
|
||||
public function getUsername(): string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the email
|
||||
*/
|
||||
public function getEmail(): string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the registration timestamp
|
||||
*/
|
||||
public function getRegisteredAt(): DateTimeInterface
|
||||
{
|
||||
return $this->registeredAt;
|
||||
}
|
||||
}
|
48
app/Exceptions/Handler.php
Normal file
48
app/Exceptions/Handler.php
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Application Exception Handler
|
||||
*
|
||||
* Handles all uncaught exceptions in the application
|
||||
*/
|
||||
class Handler
|
||||
{
|
||||
/**
|
||||
* Handle an uncaught exception
|
||||
*/
|
||||
public function handle(Throwable $exception): void
|
||||
{
|
||||
// Log the exception
|
||||
error_log($exception->getMessage() . ' in ' . $exception->getFile() . ':' . $exception->getLine());
|
||||
|
||||
// You can add custom handling here
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an exception for HTTP response
|
||||
*/
|
||||
public function render(Throwable $exception): string
|
||||
{
|
||||
if (php_sapi_name() === 'cli') {
|
||||
return $exception->getMessage() . "\n";
|
||||
}
|
||||
|
||||
// Return JSON for API requests or HTML for web
|
||||
$isJson = isset($_SERVER['HTTP_ACCEPT']) && str_contains($_SERVER['HTTP_ACCEPT'], 'application/json');
|
||||
|
||||
if ($isJson) {
|
||||
return json_encode([
|
||||
'error' => $exception->getMessage(),
|
||||
'code' => $exception->getCode()
|
||||
]);
|
||||
}
|
||||
|
||||
return '<h1>Error</h1><p>' . htmlspecialchars($exception->getMessage()) . '</p>';
|
||||
}
|
||||
}
|
170
app/Http/Controllers/Api/UserController.php
Normal file
170
app/Http/Controllers/Api/UserController.php
Normal file
|
@ -0,0 +1,170 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\RegisterUserRequest;
|
||||
use App\Models\User;
|
||||
use App\Services\User\UserService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/**
|
||||
* User API Controller - Pure Laravel-style implementation
|
||||
*/
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private UserService $userService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new user
|
||||
*/
|
||||
public function register(RegisterUserRequest $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
// Laravel automatically validates the request via RegisterUserRequest
|
||||
$validated = $request->validated();
|
||||
|
||||
// Create the user using the service
|
||||
$user = $this->userService->register($validated);
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'message' => 'User registered successfully',
|
||||
'data' => [
|
||||
'id' => $user->getKey(),
|
||||
'username' => $user->username,
|
||||
'email' => $user->user_email,
|
||||
'registered_at' => now()->toISOString()
|
||||
]
|
||||
], 201);
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation failed',
|
||||
'errors' => $e->errors()
|
||||
], 422);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'Registration failed',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user profile
|
||||
*/
|
||||
public function show(Request $request, int $userId): JsonResponse
|
||||
{
|
||||
try {
|
||||
$user = User::find($userId);
|
||||
|
||||
if (!$user) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'User not found'
|
||||
], 404);
|
||||
}
|
||||
|
||||
$stats = $user->getStats();
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'id' => $user->getKey(),
|
||||
'username' => $user->username,
|
||||
'level' => $user->user_level,
|
||||
'active' => $user->isActive(),
|
||||
'registered' => now()->createFromTimestamp($user->user_regdate)->toISOString(),
|
||||
'last_visit' => now()->createFromTimestamp($user->user_lastvisit)->diffForHumans(),
|
||||
'stats' => [
|
||||
'uploaded' => $stats['u_up_total'] ?? 0,
|
||||
'downloaded' => $stats['u_down_total'] ?? 0,
|
||||
'ratio' => $user->getRatio(),
|
||||
],
|
||||
'permissions' => [
|
||||
'is_admin' => $user->isAdmin(),
|
||||
'is_moderator' => $user->isModerator(),
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to retrieve user',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List users with filtering and search
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
// Use Laravel-style parameter handling
|
||||
$page = (int)$request->get('page', 1);
|
||||
$perPage = min((int)$request->get('per_page', 20), 100);
|
||||
$search = Str::limit(trim($request->get('search', '')), 50);
|
||||
$level = $request->get('level');
|
||||
|
||||
// Get users using collection helpers
|
||||
$users = collect(User::all())
|
||||
->when(!empty($search), function ($collection) use ($search) {
|
||||
return $collection->filter(function ($user) use ($search) {
|
||||
return Str::contains(Str::lower($user->username), Str::lower($search)) ||
|
||||
Str::contains(Str::lower($user->user_email), Str::lower($search));
|
||||
});
|
||||
})
|
||||
->when($level !== null, function ($collection) use ($level) {
|
||||
return $collection->where('user_level', $level);
|
||||
})
|
||||
->where('user_active', 1)
|
||||
->sortBy('username')
|
||||
->forPage($page, $perPage)
|
||||
->map(function ($user) {
|
||||
return [
|
||||
'id' => $user->getKey(),
|
||||
'username' => $user->username,
|
||||
'level' => $user->user_level,
|
||||
'registered' => now()->createFromTimestamp($user->user_regdate)->format('Y-m-d'),
|
||||
'is_admin' => $user->isAdmin(),
|
||||
'is_moderator' => $user->isModerator(),
|
||||
];
|
||||
})
|
||||
->values();
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'data' => $users->toArray(),
|
||||
'meta' => [
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'search' => (string)$search,
|
||||
'level_filter' => $level
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to retrieve users',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
94
app/Http/Controllers/Controller.php
Normal file
94
app/Http/Controllers/Controller.php
Normal file
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Validation\Factory as ValidationFactory;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/**
|
||||
* Base Controller class using Illuminate HTTP components
|
||||
*/
|
||||
abstract class Controller
|
||||
{
|
||||
protected ValidationFactory $validator;
|
||||
|
||||
public function __construct(ValidationFactory $validator)
|
||||
{
|
||||
$this->validator = $validator;
|
||||
}
|
||||
/**
|
||||
* Render a view with data
|
||||
*/
|
||||
protected function view(string $view, array $data = []): Response
|
||||
{
|
||||
$viewPath = $this->resolveViewPath($view);
|
||||
|
||||
if (!file_exists($viewPath)) {
|
||||
return new Response("View not found: {$view}", 404);
|
||||
}
|
||||
|
||||
// Extract data for use in view
|
||||
extract($data);
|
||||
|
||||
// Capture view output
|
||||
ob_start();
|
||||
require $viewPath;
|
||||
$content = ob_get_clean();
|
||||
|
||||
return new Response($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a JSON response
|
||||
*/
|
||||
protected function json(array $data, int $status = 200, array $headers = []): JsonResponse
|
||||
{
|
||||
return new JsonResponse($data, $status, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to a URL
|
||||
*/
|
||||
protected function redirect(string $url, int $status = 302): RedirectResponse
|
||||
{
|
||||
return new RedirectResponse($url, $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a plain response
|
||||
*/
|
||||
protected function response(string $content = '', int $status = 200, array $headers = []): Response
|
||||
{
|
||||
return new Response($content, $status, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate request data using Illuminate validation
|
||||
*/
|
||||
protected function validate(Request $request, array $rules, array $messages = [], array $attributes = []): array
|
||||
{
|
||||
$validator = $this->validator->make($request->all(), $rules, $messages, $attributes);
|
||||
|
||||
if ($validator->fails()) {
|
||||
throw new ValidationException($validator);
|
||||
}
|
||||
|
||||
return $validator->validated();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resolve view file path
|
||||
*/
|
||||
private function resolveViewPath(string $view): string
|
||||
{
|
||||
$view = str_replace('.', '/', $view);
|
||||
return resource_path('views/' . $view . '.php');
|
||||
}
|
||||
}
|
153
app/Http/Controllers/Web/HelloWorldController.php
Normal file
153
app/Http/Controllers/Web/HelloWorldController.php
Normal file
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Validation\Factory as ValidationFactory;
|
||||
use TorrentPier\Config;
|
||||
|
||||
class HelloWorldController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
ValidationFactory $validator,
|
||||
private Config $config
|
||||
) {
|
||||
parent::__construct($validator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the hello world page
|
||||
*/
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$siteName = $this->config->get('sitename', 'TorrentPier');
|
||||
$currentTime = now()->format('Y-m-d H:i:s');
|
||||
|
||||
$data = [
|
||||
'siteName' => $siteName,
|
||||
'currentTime' => $currentTime,
|
||||
'request' => (object) [
|
||||
'uri' => $request->fullUrl(),
|
||||
'method' => $request->method()
|
||||
],
|
||||
'architecture' => 'MVC with Illuminate HTTP'
|
||||
];
|
||||
|
||||
return $this->view('hello', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return JSON response for hello world
|
||||
*/
|
||||
public function jsonResponse(Request $request): JsonResponse
|
||||
{
|
||||
$siteName = $this->config->get('sitename', 'TorrentPier');
|
||||
|
||||
return $this->json([
|
||||
'message' => 'Hello World from TorrentPier!',
|
||||
'site' => $siteName,
|
||||
'timestamp' => now()->timestamp,
|
||||
'datetime' => now()->toISOString(),
|
||||
'route' => [
|
||||
'uri' => $request->fullUrl(),
|
||||
'method' => $request->method(),
|
||||
'controller' => self::class,
|
||||
],
|
||||
'architecture' => [
|
||||
'pattern' => 'Laravel-style MVC',
|
||||
'router' => 'Custom Laravel-style Router',
|
||||
'http' => 'Illuminate HTTP',
|
||||
'support' => 'Illuminate Support',
|
||||
'di' => 'Illuminate Container'
|
||||
],
|
||||
'features' => [
|
||||
'illuminate_http' => 'Response and Request handling',
|
||||
'illuminate_support' => 'Collections, Str, Arr helpers',
|
||||
'carbon' => 'Date manipulation with now() and today()',
|
||||
'validation' => 'Laravel-style request validation',
|
||||
'collections' => 'collect() helper for data manipulation'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Demonstrate modern Laravel-style features
|
||||
*/
|
||||
public function features(Request $request): JsonResponse
|
||||
{
|
||||
// Demonstrate collections
|
||||
$users = collect([
|
||||
['name' => 'Alice', 'age' => 25],
|
||||
['name' => 'Bob', 'age' => 30],
|
||||
['name' => 'Charlie', 'age' => 35]
|
||||
]);
|
||||
|
||||
$adults = $users->where('age', '>=', 18)->pluck('name');
|
||||
|
||||
// Demonstrate string helpers
|
||||
$title = str('hello world')->title()->append('!');
|
||||
|
||||
// Demonstrate array helpers
|
||||
$config = [
|
||||
'app' => [
|
||||
'name' => 'TorrentPier',
|
||||
'version' => '3.0'
|
||||
]
|
||||
];
|
||||
|
||||
$appName = data_get($config, 'app.name', 'Unknown');
|
||||
|
||||
return $this->json([
|
||||
'collections_demo' => [
|
||||
'original_users' => $users->toArray(),
|
||||
'adult_names' => $adults->toArray()
|
||||
],
|
||||
'string_demo' => [
|
||||
'original' => 'hello world',
|
||||
'transformed' => (string) $title
|
||||
],
|
||||
'array_helpers_demo' => [
|
||||
'config' => $config,
|
||||
'app_name' => $appName
|
||||
],
|
||||
'date_helpers' => [
|
||||
'now' => now()->toISOString(),
|
||||
'today' => today()->toDateString(),
|
||||
'timestamp' => now()->timestamp
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended features demonstration
|
||||
*/
|
||||
public function extended(Request $request): JsonResponse
|
||||
{
|
||||
$siteName = $this->config->get('sitename', 'TorrentPier');
|
||||
|
||||
return $this->json([
|
||||
'message' => 'Extended Laravel-style features!',
|
||||
'site' => $siteName,
|
||||
'timestamp' => now()->timestamp,
|
||||
'datetime' => now()->toISOString(),
|
||||
'request_info' => [
|
||||
'url' => $request->fullUrl(),
|
||||
'method' => $request->method(),
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
],
|
||||
'laravel_features' => [
|
||||
'collections' => 'Native Laravel collections',
|
||||
'request' => 'Pure Illuminate Request',
|
||||
'response' => 'Pure Illuminate JsonResponse',
|
||||
'validation' => 'Built-in validation',
|
||||
'helpers' => 'Laravel helper functions'
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
219
app/Http/Controllers/Web/LegacyController.php
Normal file
219
app/Http/Controllers/Web/LegacyController.php
Normal file
|
@ -0,0 +1,219 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use TorrentPier\Config;
|
||||
|
||||
class LegacyController
|
||||
{
|
||||
private Config $config;
|
||||
|
||||
public function __construct(Config $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'index');
|
||||
}
|
||||
|
||||
public function ajax(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'ajax');
|
||||
}
|
||||
|
||||
public function dl(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'dl');
|
||||
}
|
||||
|
||||
public function dl_list(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'dl_list');
|
||||
}
|
||||
|
||||
public function feed(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'feed');
|
||||
}
|
||||
|
||||
public function filelist(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'filelist');
|
||||
}
|
||||
|
||||
public function group(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'group');
|
||||
}
|
||||
|
||||
public function group_edit(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'group_edit');
|
||||
}
|
||||
|
||||
public function info(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'info');
|
||||
}
|
||||
|
||||
public function login(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'login');
|
||||
}
|
||||
|
||||
public function memberlist(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'memberlist');
|
||||
}
|
||||
|
||||
public function modcp(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'modcp');
|
||||
}
|
||||
|
||||
public function playback_m3u(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'playback_m3u');
|
||||
}
|
||||
|
||||
public function poll(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'poll');
|
||||
}
|
||||
|
||||
public function posting(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'posting');
|
||||
}
|
||||
|
||||
public function privmsg(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'privmsg');
|
||||
}
|
||||
|
||||
public function profile(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'profile');
|
||||
}
|
||||
|
||||
public function search(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'search');
|
||||
}
|
||||
|
||||
public function terms(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'terms');
|
||||
}
|
||||
|
||||
public function tracker(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'tracker');
|
||||
}
|
||||
|
||||
public function viewforum(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'viewforum');
|
||||
}
|
||||
|
||||
public function viewtopic(Request $request): Response
|
||||
{
|
||||
return $this->handleController($request, 'viewtopic');
|
||||
}
|
||||
|
||||
|
||||
public function handleController(Request $request, string $controller): Response
|
||||
{
|
||||
$rootPath = dirname(__DIR__, 4);
|
||||
$controllerPath = $rootPath . '/controllers/' . $controller . '.php';
|
||||
|
||||
if (!file_exists($controllerPath)) {
|
||||
return new Response(
|
||||
"<h1>404 - Not Found</h1><p>Legacy controller '{$controller}' not found</p>",
|
||||
404,
|
||||
['Content-Type' => 'text/html']
|
||||
);
|
||||
}
|
||||
|
||||
// Capture the legacy controller output
|
||||
$output = '';
|
||||
$originalObLevel = ob_get_level();
|
||||
|
||||
try {
|
||||
// Ensure legacy common.php is loaded for legacy controllers
|
||||
if (!defined('BB_PATH')) {
|
||||
require_once $rootPath . '/common.php';
|
||||
}
|
||||
|
||||
ob_start();
|
||||
|
||||
// No need to save/restore superglobals - legacy controllers may modify them intentionally
|
||||
|
||||
// Signal to legacy code that we're running through modern routing
|
||||
if (!defined('MODERN_ROUTING')) {
|
||||
define('MODERN_ROUTING', true);
|
||||
}
|
||||
|
||||
// Import essential legacy globals into local scope
|
||||
global $bb_cfg, $config, $user, $template, $datastore, $lang, $userdata, $userinfo, $images,
|
||||
$tracking_topics, $tracking_forums, $theme, $bf, $attach_config, $gen_simple_header,
|
||||
$client_ip, $user_ip, $log_action, $html, $wordCensor, $search_id,
|
||||
$session_id, $items_found, $per_page, $topic_id, $req_topics, $forum_id, $mode,
|
||||
$is_auth, $t_data, $postrow, $group_id, $group_info, $post_id, $folder, $post_info,
|
||||
$tor, $post_data, $privmsg, $forums, $redirect, $attachment, $forum_data, $search_all,
|
||||
$redirect_url, $topic_csv, $poster_id, $emailer, $s_hidden_fields, $opt, $msg, $stats,
|
||||
$page_cfg, $ads, $cat_forums, $last_session_data, $announce_interval, $auth_pages,
|
||||
$lastvisit, $current_time, $excluded_forums_csv, $sphinx, $dl_link_css, $dl_status_css,
|
||||
$upload_dir, $topic_data, $attachments;
|
||||
|
||||
// GPC variables created dynamically via $GLOBALS in tracker.php and search.php
|
||||
global $all_words_key, $all_words_val, $active_key, $active_val, $cat_key, $cat_val,
|
||||
$dl_cancel_key, $dl_cancel_val, $dl_compl_key, $dl_compl_val, $dl_down_key, $dl_down_val,
|
||||
$dl_will_key, $dl_will_val, $forum_key, $forum_val, $my_key, $my_val, $new_key, $new_val,
|
||||
$title_match_key, $title_match_val, $order_key, $order_val, $poster_id_key, $poster_id_val,
|
||||
$poster_name_key, $poster_name_val, $user_releases_key, $user_releases_val, $sort_key, $sort_val,
|
||||
$seed_exist_key, $seed_exist_val, $show_author_key, $show_author_val, $show_cat_key, $show_cat_val,
|
||||
$show_forum_key, $show_forum_val, $show_speed_key, $show_speed_val, $s_rg_key, $s_rg_val,
|
||||
$s_not_seen_key, $s_not_seen_val, $time_key, $time_val, $tor_type_key, $tor_type_val,
|
||||
$hash_key, $hash_val, $chars_key, $chars_val, $display_as_key, $display_as_val,
|
||||
$dl_user_id_key, $dl_user_id_val, $my_topics_key, $my_topics_val, $new_topics_key, $new_topics_val,
|
||||
$text_match_key, $text_match_val, $title_only_key, $title_only_val, $topic_key, $topic_val;
|
||||
|
||||
// Include the legacy controller
|
||||
// Note: We don't use require_once to allow multiple includes if needed
|
||||
include $controllerPath;
|
||||
|
||||
// Get the captured output - make sure we only clean our own buffer
|
||||
$output = ob_get_clean();
|
||||
|
||||
// Return the output as HTML response
|
||||
return new Response($output, 200, ['Content-Type' => 'text/html']);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
// Clean up any extra output buffers that were started, but preserve original level
|
||||
while (ob_get_level() > $originalObLevel) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
// Return error response
|
||||
$errorHtml = "
|
||||
<h1>Legacy Controller Error</h1>
|
||||
<p><strong>Controller:</strong> {$controller}</p>
|
||||
<p><strong>Error:</strong> " . htmlspecialchars($e->getMessage()) . "</p>
|
||||
<p><strong>File:</strong> " . htmlspecialchars($e->getFile()) . ":" . $e->getLine() . "</p>
|
||||
";
|
||||
|
||||
if (function_exists('dev') && dev()->isDebugEnabled()) {
|
||||
$errorHtml .= "<pre>" . htmlspecialchars($e->getTraceAsString()) . "</pre>";
|
||||
}
|
||||
|
||||
return new Response($errorHtml, 500, ['Content-Type' => 'text/html']);
|
||||
}
|
||||
}
|
||||
}
|
52
app/Http/Middleware/AdminMiddleware.php
Normal file
52
app/Http/Middleware/AdminMiddleware.php
Normal file
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* Admin Authorization Middleware
|
||||
*
|
||||
* Example middleware showing how to implement role-based authorization
|
||||
* using Illuminate HTTP components
|
||||
*/
|
||||
class AdminMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
* @return Response|JsonResponse
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response|JsonResponse
|
||||
{
|
||||
// Check if user is authenticated first
|
||||
$userId = $request->attributes->get('authenticated_user_id');
|
||||
|
||||
if (!$userId) {
|
||||
if ($request->expectsJson()) {
|
||||
return new JsonResponse(['error' => 'Authentication required'], 401);
|
||||
}
|
||||
|
||||
return new Response('Authentication required', 401);
|
||||
}
|
||||
|
||||
// TODO: Implement actual admin role check
|
||||
// For now, accept user ID 1 as admin
|
||||
if ($userId !== 1) {
|
||||
if ($request->expectsJson()) {
|
||||
return new JsonResponse(['error' => 'Admin access required'], 403);
|
||||
}
|
||||
|
||||
return new Response('Admin access required', 403);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
55
app/Http/Middleware/AuthMiddleware.php
Normal file
55
app/Http/Middleware/AuthMiddleware.php
Normal file
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* Authentication Middleware
|
||||
*
|
||||
* Example middleware showing how to implement authentication
|
||||
* using Illuminate HTTP components
|
||||
*/
|
||||
class AuthMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
* @return Response|JsonResponse
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response|JsonResponse
|
||||
{
|
||||
// Example authentication check
|
||||
$token = $request->bearerToken() ?? $request->input('api_token');
|
||||
|
||||
if (!$token) {
|
||||
if ($request->expectsJson()) {
|
||||
return new JsonResponse(['error' => 'Authentication required'], 401);
|
||||
}
|
||||
|
||||
return new Response('Authentication required', 401);
|
||||
}
|
||||
|
||||
// TODO: Implement actual token validation
|
||||
// For now, accept any token that starts with 'valid_'
|
||||
if (!str_starts_with($token, 'valid_')) {
|
||||
if ($request->expectsJson()) {
|
||||
return new JsonResponse(['error' => 'Invalid token'], 401);
|
||||
}
|
||||
|
||||
return new Response('Invalid token', 401);
|
||||
}
|
||||
|
||||
// Add user info to request for use in controllers
|
||||
$request->attributes->set('authenticated_user_id', 1);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
38
app/Http/Middleware/BaseMiddleware.php
Normal file
38
app/Http/Middleware/BaseMiddleware.php
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
abstract class BaseMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$request = $this->before($request);
|
||||
$response = $next($request);
|
||||
return $this->after($request, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process request before passing to next middleware
|
||||
*/
|
||||
protected function before(Request $request): Request
|
||||
{
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process response after middleware chain
|
||||
*/
|
||||
protected function after(Request $request, Response $response): Response
|
||||
{
|
||||
return $response;
|
||||
}
|
||||
}
|
58
app/Http/Middleware/CorsMiddleware.php
Normal file
58
app/Http/Middleware/CorsMiddleware.php
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CorsMiddleware
|
||||
{
|
||||
private array $allowedOrigins;
|
||||
private array $allowedHeaders;
|
||||
private array $allowedMethods;
|
||||
|
||||
public function __construct(
|
||||
array $allowedOrigins = ['*'],
|
||||
array $allowedHeaders = ['Content-Type', 'Authorization', 'X-Requested-With'],
|
||||
array $allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
|
||||
)
|
||||
{
|
||||
$this->allowedOrigins = $allowedOrigins;
|
||||
$this->allowedHeaders = $allowedHeaders;
|
||||
$this->allowedMethods = $allowedMethods;
|
||||
}
|
||||
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if ($request->getMethod() === 'OPTIONS') {
|
||||
return $this->createPreflightResponse($request);
|
||||
}
|
||||
|
||||
$response = $next($request);
|
||||
return $this->addCorsHeaders($response, $request);
|
||||
}
|
||||
|
||||
private function createPreflightResponse(Request $request): Response
|
||||
{
|
||||
$response = new Response('', 200);
|
||||
return $this->addCorsHeaders($response, $request);
|
||||
}
|
||||
|
||||
private function addCorsHeaders(Response $response, Request $request): Response
|
||||
{
|
||||
$origin = $request->headers->get('Origin', '');
|
||||
|
||||
if (in_array('*', $this->allowedOrigins) || in_array($origin, $this->allowedOrigins)) {
|
||||
$response->headers->set('Access-Control-Allow-Origin', $origin ?: '*');
|
||||
}
|
||||
|
||||
$response->headers->set('Access-Control-Allow-Methods', implode(', ', $this->allowedMethods));
|
||||
$response->headers->set('Access-Control-Allow-Headers', implode(', ', $this->allowedHeaders));
|
||||
$response->headers->set('Access-Control-Max-Age', '86400');
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
116
app/Http/Requests/FormRequest.php
Normal file
116
app/Http/Requests/FormRequest.php
Normal file
|
@ -0,0 +1,116 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/**
|
||||
* Base Form Request class for validation
|
||||
*/
|
||||
abstract class FormRequest
|
||||
{
|
||||
protected Request $request;
|
||||
protected array $validated = [];
|
||||
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->runValidation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request
|
||||
*/
|
||||
abstract public function rules(): array;
|
||||
|
||||
/**
|
||||
* Get custom validation messages
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom attributes for validator errors
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validated data
|
||||
*/
|
||||
public function validated(): array
|
||||
{
|
||||
return $this->validated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific validated field
|
||||
*/
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return data_get($this->validated, $key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all request data
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->request->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get only specific fields from request
|
||||
*/
|
||||
public function only(array $keys): array
|
||||
{
|
||||
return $this->request->only($keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get request data except specific fields
|
||||
*/
|
||||
public function except(array $keys): array
|
||||
{
|
||||
return $this->request->except($keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the validation
|
||||
*/
|
||||
protected function runValidation(): void
|
||||
{
|
||||
if (!$this->authorize()) {
|
||||
throw new \Illuminate\Auth\Access\AuthorizationException('This action is unauthorized.');
|
||||
}
|
||||
|
||||
$validator = Validator::make(
|
||||
$this->request->all(),
|
||||
$this->rules(),
|
||||
$this->messages(),
|
||||
$this->attributes()
|
||||
);
|
||||
|
||||
if ($validator->fails()) {
|
||||
throw new ValidationException($validator);
|
||||
}
|
||||
|
||||
$this->validated = $validator->validated();
|
||||
}
|
||||
}
|
120
app/Http/Requests/RegisterUserRequest.php
Normal file
120
app/Http/Requests/RegisterUserRequest.php
Normal file
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
/**
|
||||
* User Registration Request Validation
|
||||
*/
|
||||
class RegisterUserRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'username' => [
|
||||
'required',
|
||||
'string',
|
||||
'min:3',
|
||||
'max:25',
|
||||
'regex:/^[a-zA-Z0-9_-]+$/',
|
||||
'unique:bb_users,username'
|
||||
],
|
||||
'email' => [
|
||||
'required',
|
||||
'email',
|
||||
'max:255',
|
||||
'unique:bb_users,user_email'
|
||||
],
|
||||
'password' => [
|
||||
'required',
|
||||
'string',
|
||||
'min:6',
|
||||
'max:72'
|
||||
],
|
||||
'password_confirmation' => [
|
||||
'required',
|
||||
'same:password'
|
||||
],
|
||||
'terms' => [
|
||||
'accepted'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom validation messages
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'username.required' => 'Username is required.',
|
||||
'username.min' => 'Username must be at least 3 characters.',
|
||||
'username.max' => 'Username cannot exceed 25 characters.',
|
||||
'username.regex' => 'Username can only contain letters, numbers, hyphens, and underscores.',
|
||||
'username.unique' => 'This username is already taken.',
|
||||
|
||||
'email.required' => 'Email address is required.',
|
||||
'email.email' => 'Please enter a valid email address.',
|
||||
'email.unique' => 'This email address is already registered.',
|
||||
|
||||
'password.required' => 'Password is required.',
|
||||
'password.min' => 'Password must be at least 6 characters.',
|
||||
'password.max' => 'Password cannot exceed 72 characters.',
|
||||
|
||||
'password_confirmation.required' => 'Password confirmation is required.',
|
||||
'password_confirmation.same' => 'Password confirmation does not match.',
|
||||
|
||||
'terms.accepted' => 'You must accept the terms and conditions.'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom attributes for validator errors
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'username' => 'username',
|
||||
'email' => 'email address',
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password confirmation',
|
||||
'terms' => 'terms and conditions'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the username
|
||||
*/
|
||||
public function getUsername(): string
|
||||
{
|
||||
return str($this->get('username'))->trim()->lower();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the email
|
||||
*/
|
||||
public function getEmail(): string
|
||||
{
|
||||
return str($this->get('email'))->trim()->lower();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the password
|
||||
*/
|
||||
public function getPassword(): string
|
||||
{
|
||||
return $this->get('password');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if terms are accepted
|
||||
*/
|
||||
public function hasAcceptedTerms(): bool
|
||||
{
|
||||
return (bool) $this->get('terms');
|
||||
}
|
||||
}
|
223
app/Http/Routing/Router.php
Normal file
223
app/Http/Routing/Router.php
Normal file
|
@ -0,0 +1,223 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Routing;
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Events\Dispatcher;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Routing\Router as LaravelRouter;
|
||||
use Illuminate\Routing\UrlGenerator;
|
||||
|
||||
/**
|
||||
* Router
|
||||
*
|
||||
* Illuminate Routing wrapper for TorrentPier
|
||||
*/
|
||||
class Router
|
||||
{
|
||||
private LaravelRouter $router;
|
||||
private Container $container;
|
||||
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
|
||||
// Create event dispatcher if not already bound
|
||||
if (!$container->bound('events')) {
|
||||
$container->singleton('events', function () {
|
||||
return new Dispatcher($this->container);
|
||||
});
|
||||
}
|
||||
|
||||
// Create the Illuminate Router
|
||||
$this->router = new LaravelRouter($container->make('events'), $container);
|
||||
|
||||
// Register middleware aliases
|
||||
$this->registerMiddleware();
|
||||
|
||||
// Bind router instance
|
||||
$container->instance('router', $this->router);
|
||||
$container->instance(LaravelRouter::class, $this->router);
|
||||
|
||||
// Create and bind URL generator
|
||||
$request = $container->bound('request') ? $container->make('request') : Request::capture();
|
||||
$container->instance('request', $request);
|
||||
|
||||
$url = new UrlGenerator($this->router->getRoutes(), $request);
|
||||
$container->instance('url', $url);
|
||||
$container->instance(UrlGenerator::class, $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying Laravel Router instance
|
||||
*/
|
||||
public function getRouter(): LaravelRouter
|
||||
{
|
||||
return $this->router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a GET route
|
||||
*/
|
||||
public function get(string $uri, $action): \Illuminate\Routing\Route
|
||||
{
|
||||
return $this->router->get($uri, $this->parseAction($action));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a POST route
|
||||
*/
|
||||
public function post(string $uri, $action): \Illuminate\Routing\Route
|
||||
{
|
||||
return $this->router->post($uri, $this->parseAction($action));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a PUT route
|
||||
*/
|
||||
public function put(string $uri, $action): \Illuminate\Routing\Route
|
||||
{
|
||||
return $this->router->put($uri, $this->parseAction($action));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a PATCH route
|
||||
*/
|
||||
public function patch(string $uri, $action): \Illuminate\Routing\Route
|
||||
{
|
||||
return $this->router->patch($uri, $this->parseAction($action));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a DELETE route
|
||||
*/
|
||||
public function delete(string $uri, $action): \Illuminate\Routing\Route
|
||||
{
|
||||
return $this->router->delete($uri, $this->parseAction($action));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an OPTIONS route
|
||||
*/
|
||||
public function options(string $uri, $action): \Illuminate\Routing\Route
|
||||
{
|
||||
return $this->router->options($uri, $this->parseAction($action));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a route for any HTTP verb
|
||||
*/
|
||||
public function any(string $uri, $action): \Illuminate\Routing\Route
|
||||
{
|
||||
return $this->router->any($uri, $this->parseAction($action));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a route group
|
||||
*/
|
||||
public function group(array $attributes, \Closure $callback): void
|
||||
{
|
||||
$this->router->group($attributes, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a route prefix
|
||||
*/
|
||||
public function prefix(string $prefix): \Illuminate\Routing\RouteRegistrar
|
||||
{
|
||||
return $this->router->prefix($prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register middleware
|
||||
*/
|
||||
public function middleware($middleware): \Illuminate\Routing\RouteRegistrar
|
||||
{
|
||||
return $this->router->middleware($middleware);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a resource controller
|
||||
*/
|
||||
public function resource(string $name, string $controller, array $options = []): \Illuminate\Routing\PendingResourceRegistration
|
||||
{
|
||||
return $this->router->resource($name, $controller, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the request to the application
|
||||
*/
|
||||
public function dispatch(Request $request): Response|JsonResponse
|
||||
{
|
||||
try {
|
||||
$response = $this->router->dispatch($request);
|
||||
|
||||
// Ensure we always return a Response object
|
||||
if (!$response instanceof Response && !$response instanceof JsonResponse) {
|
||||
if (is_array($response) || is_object($response)) {
|
||||
return new JsonResponse($response);
|
||||
}
|
||||
return new Response($response);
|
||||
}
|
||||
|
||||
return $response;
|
||||
} catch (\Symfony\Component\HttpKernel\Exception\NotFoundHttpException $e) {
|
||||
return new Response('Not Found', 404);
|
||||
} catch (\Exception $e) {
|
||||
// Log the error if logger is available
|
||||
if ($this->container->bound('log')) {
|
||||
$this->container->make('log')->error($e->getMessage(), ['exception' => $e]);
|
||||
}
|
||||
|
||||
return new Response('Internal Server Error', 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the action to convert Class::method to array format
|
||||
*/
|
||||
private function parseAction($action)
|
||||
{
|
||||
if (is_string($action) && str_contains($action, '::')) {
|
||||
return str_replace('::', '@', $action);
|
||||
}
|
||||
|
||||
return $action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered routes
|
||||
*/
|
||||
public function getRoutes(): \Illuminate\Routing\RouteCollection
|
||||
{
|
||||
return $this->router->getRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the fallback route
|
||||
*/
|
||||
public function fallback($action): \Illuminate\Routing\Route
|
||||
{
|
||||
return $this->router->fallback($this->parseAction($action));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register middleware aliases
|
||||
*/
|
||||
private function registerMiddleware(): void
|
||||
{
|
||||
$middlewareAliases = [
|
||||
'auth' => \App\Http\Middleware\AuthMiddleware::class,
|
||||
'admin' => \App\Http\Middleware\AdminMiddleware::class,
|
||||
'cors' => \App\Http\Middleware\CorsMiddleware::class,
|
||||
];
|
||||
|
||||
foreach ($middlewareAliases as $alias => $middleware) {
|
||||
$this->router->aliasMiddleware($alias, $middleware);
|
||||
}
|
||||
}
|
||||
}
|
41
app/Listeners/SendWelcomeEmail.php
Normal file
41
app/Listeners/SendWelcomeEmail.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\UserRegistered;
|
||||
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;
|
||||
|
||||
/**
|
||||
* Send welcome email to newly registered users
|
||||
*/
|
||||
class SendWelcomeEmail implements ShouldHandleEventsAfterCommit
|
||||
{
|
||||
/**
|
||||
* Handle the event
|
||||
*/
|
||||
public function handle(UserRegistered $event): void
|
||||
{
|
||||
// TODO: Implement email sending logic
|
||||
// This is where you would queue an email to be sent
|
||||
// For now, just log the event
|
||||
|
||||
if (function_exists('bb_log')) {
|
||||
bb_log(sprintf(
|
||||
'Welcome email queued for user: %s (ID: %d, Email: %s)',
|
||||
$event->getUsername(),
|
||||
$event->getUserId(),
|
||||
$event->getEmail()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the listener should be queued
|
||||
*/
|
||||
public function shouldQueue(UserRegistered $event): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
32
app/Listeners/UpdateUserStatistics.php
Normal file
32
app/Listeners/UpdateUserStatistics.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\TorrentUploaded;
|
||||
|
||||
/**
|
||||
* Update user statistics when a torrent is uploaded
|
||||
*/
|
||||
class UpdateUserStatistics
|
||||
{
|
||||
/**
|
||||
* Handle the event
|
||||
*/
|
||||
public function handle(TorrentUploaded $event): void
|
||||
{
|
||||
// TODO: Implement statistics update logic
|
||||
// This would typically update the user's upload count, ratio, etc.
|
||||
|
||||
if (function_exists('bb_log')) {
|
||||
bb_log(sprintf(
|
||||
'User statistics update triggered for user %d after uploading torrent: %s (ID: %d, Size: %d bytes)',
|
||||
$event->getUploaderId(),
|
||||
$event->getTorrentName(),
|
||||
$event->getTorrentId(),
|
||||
$event->getSize()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
223
app/Models/Model.php
Normal file
223
app/Models/Model.php
Normal file
|
@ -0,0 +1,223 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use TorrentPier\Database\Database;
|
||||
|
||||
/**
|
||||
* Base Model class for all models
|
||||
* Provides basic database operations using Nette Database
|
||||
*/
|
||||
abstract class Model
|
||||
{
|
||||
protected string $table;
|
||||
protected string $primaryKey = 'id';
|
||||
protected array $attributes = [];
|
||||
protected array $original = [];
|
||||
|
||||
public function __construct(
|
||||
protected Database $db,
|
||||
array $attributes = []
|
||||
) {
|
||||
$this->fill($attributes);
|
||||
$this->syncOriginal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a model by its primary key
|
||||
*/
|
||||
public static function find(int|string $id): ?static
|
||||
{
|
||||
$instance = new static(DB());
|
||||
$data = $instance->db->table($instance->table)
|
||||
->where($instance->primaryKey, $id)
|
||||
->fetch();
|
||||
|
||||
if (!$data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new static(DB(), (array) $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a model by a specific column
|
||||
*/
|
||||
public static function findBy(string $column, mixed $value): ?static
|
||||
{
|
||||
$instance = new static(DB());
|
||||
$data = $instance->db->table($instance->table)
|
||||
->where($column, $value)
|
||||
->fetch();
|
||||
|
||||
if (!$data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new static(DB(), (array) $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all models
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
$instance = new static(DB());
|
||||
$rows = $instance->db->table($instance->table)->fetchAll();
|
||||
|
||||
$models = [];
|
||||
foreach ($rows as $row) {
|
||||
$models[] = new static(DB(), (array) $row);
|
||||
}
|
||||
|
||||
return $models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the model with an array of attributes
|
||||
*/
|
||||
public function fill(array $attributes): self
|
||||
{
|
||||
foreach ($attributes as $key => $value) {
|
||||
$this->attributes[$key] = $value;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the model to the database
|
||||
*/
|
||||
public function save(): bool
|
||||
{
|
||||
if ($this->exists()) {
|
||||
return $this->update();
|
||||
}
|
||||
|
||||
return $this->insert();
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new record
|
||||
*/
|
||||
protected function insert(): bool
|
||||
{
|
||||
$this->db->table($this->table)->insert($this->attributes);
|
||||
|
||||
if (!isset($this->attributes[$this->primaryKey])) {
|
||||
$this->attributes[$this->primaryKey] = $this->db->getInsertId();
|
||||
}
|
||||
|
||||
$this->syncOriginal();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing record
|
||||
*/
|
||||
protected function update(): bool
|
||||
{
|
||||
$dirty = $this->getDirty();
|
||||
|
||||
if (empty($dirty)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->db->table($this->table)
|
||||
->where($this->primaryKey, $this->getKey())
|
||||
->update($dirty);
|
||||
|
||||
$this->syncOriginal();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the model
|
||||
*/
|
||||
public function delete(): bool
|
||||
{
|
||||
if (!$this->exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->db->table($this->table)
|
||||
->where($this->primaryKey, $this->getKey())
|
||||
->delete();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the model exists in the database
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return isset($this->original[$this->primaryKey]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the primary key value
|
||||
*/
|
||||
public function getKey(): int|string|null
|
||||
{
|
||||
return $this->attributes[$this->primaryKey] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attributes that have been changed
|
||||
*/
|
||||
public function getDirty(): array
|
||||
{
|
||||
$dirty = [];
|
||||
|
||||
foreach ($this->attributes as $key => $value) {
|
||||
if (!array_key_exists($key, $this->original) || $value !== $this->original[$key]) {
|
||||
$dirty[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $dirty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the original attributes with the current
|
||||
*/
|
||||
protected function syncOriginal(): void
|
||||
{
|
||||
$this->original = $this->attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an attribute
|
||||
*/
|
||||
public function __get(string $key): mixed
|
||||
{
|
||||
return $this->attributes[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an attribute
|
||||
*/
|
||||
public function __set(string $key, mixed $value): void
|
||||
{
|
||||
$this->attributes[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an attribute exists
|
||||
*/
|
||||
public function __isset(string $key): bool
|
||||
{
|
||||
return isset($this->attributes[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the model to an array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->attributes;
|
||||
}
|
||||
}
|
109
app/Models/Torrent.php
Normal file
109
app/Models/Torrent.php
Normal file
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* Torrent Model
|
||||
*
|
||||
* Represents a torrent in the database
|
||||
*/
|
||||
class Torrent extends Model
|
||||
{
|
||||
protected string $table = 'bb_bt_torrents';
|
||||
protected string $primaryKey = 'topic_id';
|
||||
|
||||
/**
|
||||
* Get active peers for this torrent
|
||||
*/
|
||||
public function getPeers(): array
|
||||
{
|
||||
return $this->db->table('bb_bt_tracker')
|
||||
->where('topic_id', $this->getKey())
|
||||
->where('complete', 0)
|
||||
->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get completed peers (seeders) for this torrent
|
||||
*/
|
||||
public function getSeeders(): array
|
||||
{
|
||||
return $this->db->table('bb_bt_tracker')
|
||||
->where('topic_id', $this->getKey())
|
||||
->where('complete', 1)
|
||||
->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get torrent statistics
|
||||
*/
|
||||
public function getStats(): array
|
||||
{
|
||||
$stats = $this->db->table('bb_bt_tracker_snap')
|
||||
->where('topic_id', $this->getKey())
|
||||
->fetch();
|
||||
|
||||
return $stats ? (array) $stats : [
|
||||
'seeders' => 0,
|
||||
'leechers' => 0,
|
||||
'complete' => 0
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who uploaded this torrent
|
||||
*/
|
||||
public function getUploader(): ?User
|
||||
{
|
||||
return User::find($this->poster_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the forum topic associated with this torrent
|
||||
*/
|
||||
public function getTopic(): array
|
||||
{
|
||||
$topic = $this->db->table('bb_topics')
|
||||
->where('topic_id', $this->getKey())
|
||||
->fetch();
|
||||
|
||||
return $topic ? (array) $topic : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if torrent is active
|
||||
*/
|
||||
public function isActive(): bool
|
||||
{
|
||||
return (int) $this->tor_status === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find torrent by info hash
|
||||
*/
|
||||
public static function findByInfoHash(string $infoHash): ?self
|
||||
{
|
||||
return self::findBy('info_hash', $infoHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent torrents
|
||||
*/
|
||||
public static function getRecent(int $limit = 10): array
|
||||
{
|
||||
$instance = new static(DB());
|
||||
$rows = $instance->db->table($instance->table)
|
||||
->orderBy('reg_time DESC')
|
||||
->limit($limit)
|
||||
->fetchAll();
|
||||
|
||||
$torrents = [];
|
||||
foreach ($rows as $row) {
|
||||
$torrents[] = new static(DB(), (array) $row);
|
||||
}
|
||||
|
||||
return $torrents;
|
||||
}
|
||||
}
|
139
app/Models/User.php
Normal file
139
app/Models/User.php
Normal file
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* User Model
|
||||
*
|
||||
* Represents a user in the system
|
||||
*/
|
||||
class User extends Model
|
||||
{
|
||||
protected string $table = 'bb_users';
|
||||
protected string $primaryKey = 'user_id';
|
||||
|
||||
/**
|
||||
* Find user by username
|
||||
*/
|
||||
public static function findByUsername(string $username): ?self
|
||||
{
|
||||
return self::findBy('username', $username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find user by email
|
||||
*/
|
||||
public static function findByEmail(string $email): ?self
|
||||
{
|
||||
return self::findBy('user_email', $email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's torrents
|
||||
*/
|
||||
public function getTorrents(): array
|
||||
{
|
||||
return $this->db->table('bb_bt_torrents')
|
||||
->where('poster_id', $this->getKey())
|
||||
->orderBy('reg_time DESC')
|
||||
->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's posts
|
||||
*/
|
||||
public function getPosts(int $limit = 10): array
|
||||
{
|
||||
return $this->db->table('bb_posts')
|
||||
->where('poster_id', $this->getKey())
|
||||
->orderBy('post_time DESC')
|
||||
->limit($limit)
|
||||
->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's groups
|
||||
*/
|
||||
public function getGroups(): array
|
||||
{
|
||||
return $this->db->table('bb_user_group')
|
||||
->where('user_id', $this->getKey())
|
||||
->where('user_pending', 0)
|
||||
->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is admin
|
||||
*/
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return (int) $this->user_level === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is moderator
|
||||
*/
|
||||
public function isModerator(): bool
|
||||
{
|
||||
return (int) $this->user_level === 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is active
|
||||
*/
|
||||
public function isActive(): bool
|
||||
{
|
||||
return (int) $this->user_active === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's upload/download statistics
|
||||
*/
|
||||
public function getStats(): array
|
||||
{
|
||||
$stats = $this->db->table('bb_bt_users')
|
||||
->where('user_id', $this->getKey())
|
||||
->fetch();
|
||||
|
||||
return $stats ? (array) $stats : [
|
||||
'u_up_total' => 0,
|
||||
'u_down_total' => 0,
|
||||
'u_up_release' => 0,
|
||||
'u_up_bonus' => 0
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's ratio
|
||||
*/
|
||||
public function getRatio(): float
|
||||
{
|
||||
$stats = $this->getStats();
|
||||
$downloaded = (int) $stats['u_down_total'];
|
||||
$uploaded = (int) $stats['u_up_total'];
|
||||
|
||||
if ($downloaded === 0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return round($uploaded / $downloaded, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify password
|
||||
*/
|
||||
public function verifyPassword(string $password): bool
|
||||
{
|
||||
return password_verify($password, $this->user_password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update password
|
||||
*/
|
||||
public function updatePassword(string $newPassword): void
|
||||
{
|
||||
$this->user_password = password_hash($newPassword, PASSWORD_BCRYPT);
|
||||
}
|
||||
}
|
66
app/Providers/AppServiceProvider.php
Normal file
66
app/Providers/AppServiceProvider.php
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
/**
|
||||
* Application Service Provider
|
||||
*
|
||||
* Bootstrap any application services here
|
||||
*/
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
// Register application services
|
||||
$this->registerEvents();
|
||||
$this->registerValidation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
// Bootstrap services that need the application to be fully loaded
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Register the event dispatcher
|
||||
*/
|
||||
protected function registerEvents(): void
|
||||
{
|
||||
$this->app->singleton('events', function ($app) {
|
||||
return new \Illuminate\Events\Dispatcher($app);
|
||||
});
|
||||
|
||||
$this->app->alias('events', \Illuminate\Events\Dispatcher::class);
|
||||
$this->app->alias('events', \Illuminate\Contracts\Events\Dispatcher::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the validation factory
|
||||
*/
|
||||
protected function registerValidation(): void
|
||||
{
|
||||
$this->app->singleton('validator', function ($app) {
|
||||
$loader = new \Illuminate\Translation\ArrayLoader();
|
||||
$translator = new \Illuminate\Translation\Translator($loader, 'en');
|
||||
return new \Illuminate\Validation\Factory($translator, $app);
|
||||
});
|
||||
|
||||
$this->app->bind(\Illuminate\Validation\Factory::class, function ($app) {
|
||||
return $app['validator'];
|
||||
});
|
||||
|
||||
$this->app->alias('validator', \Illuminate\Validation\Factory::class);
|
||||
$this->app->alias('validator', \Illuminate\Contracts\Validation\Factory::class);
|
||||
}
|
||||
}
|
76
app/Providers/ConsoleServiceProvider.php
Normal file
76
app/Providers/ConsoleServiceProvider.php
Normal file
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
/**
|
||||
* Console Service Provider
|
||||
*
|
||||
* Automatically discovers and registers console commands
|
||||
*/
|
||||
class ConsoleServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register console commands
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->registerCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register console commands
|
||||
*/
|
||||
protected function registerCommands(): void
|
||||
{
|
||||
$commands = $this->discoverCommands();
|
||||
|
||||
$this->app->bind('console.commands', function () use ($commands) {
|
||||
return $commands;
|
||||
});
|
||||
|
||||
// Register each command in the container
|
||||
foreach ($commands as $command) {
|
||||
$this->app->bind($command, function ($app) use ($command) {
|
||||
return new $command();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover commands in the Commands directory
|
||||
*/
|
||||
protected function discoverCommands(): array
|
||||
{
|
||||
$commands = [];
|
||||
$commandsPath = $this->app->make('path.app') . '/Console/Commands';
|
||||
|
||||
if (!is_dir($commandsPath)) {
|
||||
return $commands;
|
||||
}
|
||||
|
||||
$finder = new Finder();
|
||||
$finder->files()->name('*Command.php')->in($commandsPath);
|
||||
|
||||
foreach ($finder as $file) {
|
||||
$relativePath = $file->getRelativePathname();
|
||||
$className = 'App\\Console\\Commands\\' . str_replace(['/', '.php'], ['\\', ''], $relativePath);
|
||||
|
||||
if (class_exists($className)) {
|
||||
$reflection = new \ReflectionClass($className);
|
||||
|
||||
// Only include concrete command classes that extend our base Command
|
||||
if (!$reflection->isAbstract() &&
|
||||
$reflection->isSubclassOf(\App\Console\Commands\Command::class)) {
|
||||
$commands[] = $className;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $commands;
|
||||
}
|
||||
}
|
64
app/Providers/EventServiceProvider.php
Normal file
64
app/Providers/EventServiceProvider.php
Normal file
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Events\UserRegistered;
|
||||
use App\Events\TorrentUploaded;
|
||||
use App\Listeners\SendWelcomeEmail;
|
||||
use App\Listeners\UpdateUserStatistics;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
/**
|
||||
* Event Service Provider
|
||||
*
|
||||
* Register event listeners and subscribers
|
||||
*/
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The event listener mappings for the application
|
||||
*/
|
||||
protected array $listen = [
|
||||
UserRegistered::class => [
|
||||
SendWelcomeEmail::class,
|
||||
],
|
||||
TorrentUploaded::class => [
|
||||
UpdateUserStatistics::class,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* The event subscriber classes to register
|
||||
*/
|
||||
protected array $subscribe = [
|
||||
// Add event subscribers here
|
||||
];
|
||||
|
||||
/**
|
||||
* Register any events for your application
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$events = $this->app->make('events');
|
||||
|
||||
foreach ($this->listen as $event => $listeners) {
|
||||
foreach ($listeners as $listener) {
|
||||
$events->listen($event, $listener);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->subscribe as $subscriber) {
|
||||
$events->subscribe($subscriber);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if events and listeners should be automatically discovered
|
||||
*/
|
||||
public function shouldDiscoverEvents(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
123
app/Providers/RouteServiceProvider.php
Normal file
123
app/Providers/RouteServiceProvider.php
Normal file
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Http\Routing\Router;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* Route Service Provider
|
||||
*
|
||||
* Loads and registers application routes using Illuminate Routing
|
||||
*/
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The path to the "home" route for your application
|
||||
*/
|
||||
public const HOME = '/';
|
||||
|
||||
/**
|
||||
* Register services
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
// Register the Router
|
||||
$this->app->singleton(Router::class, function ($app) {
|
||||
return new Router($app);
|
||||
});
|
||||
|
||||
// Alias for convenience
|
||||
$this->app->alias(Router::class, 'router');
|
||||
}
|
||||
|
||||
/**
|
||||
* Define your route model bindings, pattern filters, etc.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->configureRateLimiting();
|
||||
|
||||
$this->routes(function () {
|
||||
$this->mapApiRoutes();
|
||||
$this->mapWebRoutes();
|
||||
$this->mapAdminRoutes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the rate limiters for the application
|
||||
*/
|
||||
protected function configureRateLimiting(): void
|
||||
{
|
||||
// Rate limiting can be configured here when needed
|
||||
// Example:
|
||||
// RateLimiter::for('api', function (Request $request) {
|
||||
// return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the routes for the application
|
||||
*/
|
||||
protected function routes(\Closure $callback): void
|
||||
{
|
||||
$callback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the "web" routes for the application
|
||||
*/
|
||||
protected function mapWebRoutes(): void
|
||||
{
|
||||
$router = $this->app->make(Router::class);
|
||||
$routeFile = $this->app->make('path.base') . '/routes/web.php';
|
||||
|
||||
if (file_exists($routeFile)) {
|
||||
$router->group([], function () use ($routeFile) {
|
||||
require $routeFile;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the "api" routes for the application
|
||||
*/
|
||||
protected function mapApiRoutes(): void
|
||||
{
|
||||
$router = $this->app->make(Router::class);
|
||||
$routeFile = $this->app->make('path.base') . '/routes/api.php';
|
||||
|
||||
if (file_exists($routeFile)) {
|
||||
$router->group([
|
||||
'prefix' => 'api',
|
||||
], function () use ($routeFile) {
|
||||
require $routeFile;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the "admin" routes for the application
|
||||
*/
|
||||
protected function mapAdminRoutes(): void
|
||||
{
|
||||
$router = $this->app->make(Router::class);
|
||||
$routeFile = $this->app->make('path.base') . '/routes/admin.php';
|
||||
|
||||
if (file_exists($routeFile)) {
|
||||
$router->group([
|
||||
'prefix' => 'admin',
|
||||
'middleware' => [
|
||||
'auth',
|
||||
'admin',
|
||||
]
|
||||
], function () use ($routeFile) {
|
||||
require $routeFile;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
17
app/Providers/ServiceProvider.php
Normal file
17
app/Providers/ServiceProvider.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider as IlluminateServiceProvider;
|
||||
|
||||
/**
|
||||
* Base Service Provider
|
||||
*
|
||||
* All application service providers should extend this class
|
||||
*/
|
||||
abstract class ServiceProvider extends IlluminateServiceProvider
|
||||
{
|
||||
// Add any application-specific service provider functionality here
|
||||
}
|
244
app/Services/Tracker/TorrentService.php
Normal file
244
app/Services/Tracker/TorrentService.php
Normal file
|
@ -0,0 +1,244 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Tracker;
|
||||
|
||||
use App\Models\Torrent;
|
||||
use App\Models\User;
|
||||
use TorrentPier\Cache\CacheManager;
|
||||
use TorrentPier\Database\Database;
|
||||
|
||||
/**
|
||||
* Torrent Service
|
||||
*
|
||||
* Handles business logic for torrent operations
|
||||
*/
|
||||
class TorrentService
|
||||
{
|
||||
public function __construct(
|
||||
private Database $db,
|
||||
private CacheManager $cache
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Register a new torrent
|
||||
*/
|
||||
public function register(array $data, User $user): Torrent
|
||||
{
|
||||
// Validate torrent data
|
||||
$this->validateTorrentData($data);
|
||||
|
||||
// Create torrent record
|
||||
$torrent = new Torrent($this->db);
|
||||
$torrent->fill([
|
||||
'info_hash' => $data['info_hash'],
|
||||
'poster_id' => $user->getKey(),
|
||||
'size' => $data['size'],
|
||||
'reg_time' => time(),
|
||||
'tor_status' => 0,
|
||||
'checked_user_id' => 0,
|
||||
'checked_time' => 0,
|
||||
'tor_type' => 0,
|
||||
'speed_up' => 0,
|
||||
'speed_down' => 0,
|
||||
]);
|
||||
$torrent->save();
|
||||
|
||||
// Clear cache
|
||||
$this->cache->delete('recent_torrents');
|
||||
|
||||
return $torrent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update torrent information
|
||||
*/
|
||||
public function update(Torrent $torrent, array $data): bool
|
||||
{
|
||||
$torrent->fill($data);
|
||||
$result = $torrent->save();
|
||||
|
||||
// Clear relevant caches
|
||||
$this->cache->delete('torrent:' . $torrent->info_hash);
|
||||
$this->cache->delete('recent_torrents');
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a torrent
|
||||
*/
|
||||
public function delete(Torrent $torrent): bool
|
||||
{
|
||||
// Delete related data
|
||||
$this->db->table('bb_bt_tracker')
|
||||
->where('topic_id', $torrent->getKey())
|
||||
->delete();
|
||||
|
||||
$this->db->table('bb_bt_tracker_snap')
|
||||
->where('topic_id', $torrent->getKey())
|
||||
->delete();
|
||||
|
||||
// Delete torrent
|
||||
$result = $torrent->delete();
|
||||
|
||||
// Clear cache
|
||||
$this->cache->delete('torrent:' . $torrent->info_hash);
|
||||
$this->cache->delete('recent_torrents');
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get paginated torrents using Laravel-style collections
|
||||
*/
|
||||
public function paginate(int $page = 1, ?string $category = null, int $perPage = 20): array
|
||||
{
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$query = $this->db->table('bb_bt_torrents as t')
|
||||
->select('t.*, ts.seeders, ts.leechers, ts.complete')
|
||||
->leftJoin('bb_bt_tracker_snap as ts', 'ts.topic_id = t.topic_id')
|
||||
->where('t.tor_status', 0)
|
||||
->orderBy('t.reg_time DESC')
|
||||
->limit($perPage)
|
||||
->offset($offset);
|
||||
|
||||
if ($category !== null) {
|
||||
$query->where('t.tor_type', $category);
|
||||
}
|
||||
|
||||
$rows = $query->fetchAll();
|
||||
|
||||
// Use Laravel-style collection for better data manipulation
|
||||
$torrents = collect($rows)
|
||||
->map(fn($row) => new Torrent($this->db, (array) $row))
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'data' => $torrents,
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'total' => $this->countTorrents($category),
|
||||
'from' => $offset + 1,
|
||||
'to' => min($offset + $perPage, $this->countTorrents($category)),
|
||||
'last_page' => ceil($this->countTorrents($category) / $perPage)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Search torrents using collections and modern string helpers
|
||||
*/
|
||||
public function search(string $query, array $filters = []): array
|
||||
{
|
||||
$cacheKey = 'search:' . str($query . serialize($filters))->hash('md5');
|
||||
|
||||
return $this->cache->remember($cacheKey, 300, function() use ($query, $filters) {
|
||||
$qb = $this->db->table('bb_bt_torrents as t')
|
||||
->select('t.*, ts.seeders, ts.leechers')
|
||||
->leftJoin('bb_bt_tracker_snap as ts', 'ts.topic_id = t.topic_id')
|
||||
->leftJoin('bb_topics as top', 'top.topic_id = t.topic_id')
|
||||
->where('t.tor_status', 0);
|
||||
|
||||
// Search in topic title (cleaned query)
|
||||
if (!empty($query)) {
|
||||
$cleanQuery = str($query)->trim()->lower()->limit(100);
|
||||
$qb->where('LOWER(top.topic_title) LIKE ?', '%' . $cleanQuery . '%');
|
||||
}
|
||||
|
||||
// Apply filters using data_get helper
|
||||
if ($category = data_get($filters, 'category')) {
|
||||
$qb->where('t.tor_type', $category);
|
||||
}
|
||||
|
||||
if ($minSeeders = data_get($filters, 'min_seeders')) {
|
||||
$qb->where('ts.seeders >= ?', $minSeeders);
|
||||
}
|
||||
|
||||
$rows = $qb->limit(100)->fetchAll();
|
||||
|
||||
// Use collection to transform and filter results
|
||||
$torrents = collect($rows)
|
||||
->map(fn($row) => new Torrent($this->db, (array) $row))
|
||||
->when(data_get($filters, 'sort') === 'popular', function ($collection) {
|
||||
return $collection->sortByDesc(fn($torrent) => $torrent->seeders ?? 0);
|
||||
})
|
||||
->when(data_get($filters, 'sort') === 'recent', function ($collection) {
|
||||
return $collection->sortByDesc('reg_time');
|
||||
})
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
return $torrents;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get torrent statistics
|
||||
*/
|
||||
public function getStatistics(): array
|
||||
{
|
||||
return $this->cache->remember('torrent_stats', 3600, function() {
|
||||
$stats = [];
|
||||
|
||||
// Total torrents
|
||||
$stats['total_torrents'] = $this->db->table('bb_bt_torrents')
|
||||
->where('tor_status', 0)
|
||||
->count('*');
|
||||
|
||||
// Total size
|
||||
$totalSize = $this->db->table('bb_bt_torrents')
|
||||
->where('tor_status', 0)
|
||||
->sum('size');
|
||||
$stats['total_size'] = $totalSize ?: 0;
|
||||
|
||||
// Active peers
|
||||
$stats['active_peers'] = $this->db->table('bb_bt_tracker')
|
||||
->count('*');
|
||||
|
||||
// Completed downloads
|
||||
$stats['total_completed'] = $this->db->table('bb_bt_torrents')
|
||||
->where('tor_status', 0)
|
||||
->sum('complete_count');
|
||||
|
||||
return $stats;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate torrent data
|
||||
*/
|
||||
private function validateTorrentData(array $data): void
|
||||
{
|
||||
if (empty($data['info_hash']) || strlen($data['info_hash']) !== 40) {
|
||||
throw new \InvalidArgumentException('Invalid info hash');
|
||||
}
|
||||
|
||||
if (empty($data['size']) || $data['size'] <= 0) {
|
||||
throw new \InvalidArgumentException('Invalid torrent size');
|
||||
}
|
||||
|
||||
// Check if torrent already exists
|
||||
$existing = Torrent::findByInfoHash($data['info_hash']);
|
||||
if ($existing !== null) {
|
||||
throw new \InvalidArgumentException('Torrent already exists');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count torrents
|
||||
*/
|
||||
private function countTorrents(?string $category = null): int
|
||||
{
|
||||
$query = $this->db->table('bb_bt_torrents')
|
||||
->where('tor_status', 0);
|
||||
|
||||
if ($category !== null) {
|
||||
$query->where('tor_type', $category);
|
||||
}
|
||||
|
||||
return $query->count('*');
|
||||
}
|
||||
}
|
196
app/Services/User/UserService.php
Normal file
196
app/Services/User/UserService.php
Normal file
|
@ -0,0 +1,196 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\User;
|
||||
|
||||
use App\Models\User;
|
||||
use TorrentPier\Cache\CacheManager;
|
||||
use TorrentPier\Database\Database;
|
||||
|
||||
/**
|
||||
* User Service
|
||||
*
|
||||
* Handles business logic for user operations
|
||||
*/
|
||||
class UserService
|
||||
{
|
||||
public function __construct(
|
||||
private Database $db,
|
||||
private CacheManager $cache
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Register a new user
|
||||
*/
|
||||
public function register(array $data): User
|
||||
{
|
||||
// Validate data
|
||||
$this->validateRegistrationData($data);
|
||||
|
||||
// Create user
|
||||
$user = new User($this->db);
|
||||
$user->fill([
|
||||
'username' => $data['username'],
|
||||
'user_email' => $data['email'],
|
||||
'user_password' => password_hash($data['password'], PASSWORD_BCRYPT),
|
||||
'user_level' => 0, // Regular user
|
||||
'user_active' => 1,
|
||||
'user_regdate' => now()->timestamp,
|
||||
'user_lastvisit' => now()->timestamp,
|
||||
'user_timezone' => 0,
|
||||
'user_lang' => 'en',
|
||||
'user_dateformat' => 'd M Y H:i',
|
||||
]);
|
||||
|
||||
$user->save();
|
||||
|
||||
// Clear user cache
|
||||
$this->cache->delete('user_count');
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user profile
|
||||
*/
|
||||
public function updateProfile(User $user, array $data): bool
|
||||
{
|
||||
$allowedFields = [
|
||||
'user_timezone',
|
||||
'user_lang',
|
||||
'user_dateformat',
|
||||
];
|
||||
|
||||
$updateData = collect($data)
|
||||
->only($allowedFields)
|
||||
->filter()
|
||||
->toArray();
|
||||
|
||||
if (empty($updateData)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$user->fill($updateData);
|
||||
$result = $user->save();
|
||||
|
||||
// Clear user cache
|
||||
$this->cache->delete('user:' . $user->getKey());
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change user password
|
||||
*/
|
||||
public function changePassword(User $user, string $currentPassword, string $newPassword): bool
|
||||
{
|
||||
if (!$user->verifyPassword($currentPassword)) {
|
||||
throw new \InvalidArgumentException('Current password is incorrect');
|
||||
}
|
||||
|
||||
$user->updatePassword($newPassword);
|
||||
return $user->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user statistics
|
||||
*/
|
||||
public function getUserStats(User $user): array
|
||||
{
|
||||
$cacheKey = 'user_stats:' . $user->getKey();
|
||||
|
||||
return $this->cache->remember($cacheKey, 1800, function() use ($user) {
|
||||
$stats = $user->getStats();
|
||||
$torrents = $user->getTorrents();
|
||||
$posts = $user->getPosts(5);
|
||||
|
||||
return [
|
||||
'upload_stats' => [
|
||||
'total_uploaded' => $stats['u_up_total'] ?? 0,
|
||||
'total_downloaded' => $stats['u_down_total'] ?? 0,
|
||||
'ratio' => $user->getRatio(),
|
||||
],
|
||||
'activity' => [
|
||||
'torrents_count' => count($torrents),
|
||||
'recent_posts' => count($posts),
|
||||
'last_visit' => now()->createFromTimestamp($user->user_lastvisit)->diffForHumans(),
|
||||
],
|
||||
'permissions' => [
|
||||
'level' => $user->user_level,
|
||||
'is_admin' => $user->isAdmin(),
|
||||
'is_moderator' => $user->isModerator(),
|
||||
'is_active' => $user->isActive(),
|
||||
]
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Search users using modern collection methods
|
||||
*/
|
||||
public function searchUsers(string $query, array $filters = []): array
|
||||
{
|
||||
$cacheKey = 'user_search:' . str($query . serialize($filters))->hash('md5');
|
||||
|
||||
return $this->cache->remember($cacheKey, 600, function() use ($query, $filters) {
|
||||
// Get all active users (in a real app, this would be paginated)
|
||||
$users = collect(User::all())
|
||||
->where('user_active', 1);
|
||||
|
||||
// Apply search filter
|
||||
if (!empty($query)) {
|
||||
$searchTerm = str($query)->lower();
|
||||
$users = $users->filter(function ($user) use ($searchTerm) {
|
||||
return str($user->username)->lower()->contains($searchTerm) ||
|
||||
str($user->user_email)->lower()->contains($searchTerm);
|
||||
});
|
||||
}
|
||||
|
||||
// Apply level filter
|
||||
if ($level = data_get($filters, 'level')) {
|
||||
$users = $users->where('user_level', $level);
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
$sortBy = data_get($filters, 'sort', 'username');
|
||||
$sortDirection = data_get($filters, 'direction', 'asc');
|
||||
|
||||
$users = $sortDirection === 'desc'
|
||||
? $users->sortByDesc($sortBy)
|
||||
: $users->sortBy($sortBy);
|
||||
|
||||
return $users->values()->toArray();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate registration data
|
||||
*/
|
||||
private function validateRegistrationData(array $data): void
|
||||
{
|
||||
if (empty($data['username'])) {
|
||||
throw new \InvalidArgumentException('Username is required');
|
||||
}
|
||||
|
||||
if (empty($data['email'])) {
|
||||
throw new \InvalidArgumentException('Email is required');
|
||||
}
|
||||
|
||||
if (empty($data['password'])) {
|
||||
throw new \InvalidArgumentException('Password is required');
|
||||
}
|
||||
|
||||
// Check if username already exists
|
||||
$existingUser = User::findByUsername($data['username']);
|
||||
if ($existingUser) {
|
||||
throw new \InvalidArgumentException('Username already exists');
|
||||
}
|
||||
|
||||
// Check if email already exists
|
||||
$existingEmail = User::findByEmail($data['email']);
|
||||
if ($existingEmail) {
|
||||
throw new \InvalidArgumentException('Email already exists');
|
||||
}
|
||||
}
|
||||
}
|
42
app/Services/UserService.php
Normal file
42
app/Services/UserService.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Events\UserRegistered;
|
||||
|
||||
/**
|
||||
* User Service
|
||||
*
|
||||
* Example service demonstrating event usage
|
||||
*/
|
||||
class UserService
|
||||
{
|
||||
/**
|
||||
* Register a new user
|
||||
*
|
||||
* @param array $userData User registration data
|
||||
* @return int User ID
|
||||
*/
|
||||
public function registerUser(array $userData): int
|
||||
{
|
||||
// TODO: Implement actual user registration logic
|
||||
// This is a simplified example
|
||||
|
||||
// Simulate user creation
|
||||
$userId = random_int(1000, 9999);
|
||||
$username = $userData['username'] ?? 'user' . $userId;
|
||||
$email = $userData['email'] ?? $username . '@example.com';
|
||||
|
||||
// Dispatch the UserRegistered event
|
||||
event(new UserRegistered(
|
||||
userId: $userId,
|
||||
username: $username,
|
||||
email: $email,
|
||||
registeredAt: new \DateTime()
|
||||
));
|
||||
|
||||
return $userId;
|
||||
}
|
||||
}
|
243
app/helpers.php
Normal file
243
app/helpers.php
Normal file
|
@ -0,0 +1,243 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Laravel-style helper functions for TorrentPier
|
||||
*/
|
||||
|
||||
if (!function_exists('app_path')) {
|
||||
/**
|
||||
* Get the path to the app directory
|
||||
*/
|
||||
function app_path(string $path = ''): string
|
||||
{
|
||||
return rtrim(__DIR__, '/') . ($path ? '/' . ltrim($path, '/') : '');
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('base_path')) {
|
||||
/**
|
||||
* Get the path to the base directory
|
||||
*/
|
||||
function base_path(string $path = ''): string
|
||||
{
|
||||
return rtrim(dirname(__DIR__), '/') . ($path ? '/' . ltrim($path, '/') : '');
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('config_path')) {
|
||||
/**
|
||||
* Get the path to the config directory
|
||||
*/
|
||||
function config_path(string $path = ''): string
|
||||
{
|
||||
return base_path('config') . ($path ? '/' . ltrim($path, '/') : '');
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('database_path')) {
|
||||
/**
|
||||
* Get the path to the database directory
|
||||
*/
|
||||
function database_path(string $path = ''): string
|
||||
{
|
||||
return base_path('database') . ($path ? '/' . ltrim($path, '/') : '');
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('public_path')) {
|
||||
/**
|
||||
* Get the path to the public directory
|
||||
*/
|
||||
function public_path(string $path = ''): string
|
||||
{
|
||||
return base_path('public') . ($path ? '/' . ltrim($path, '/') : '');
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('resource_path')) {
|
||||
/**
|
||||
* Get the path to the resources directory
|
||||
*/
|
||||
function resource_path(string $path = ''): string
|
||||
{
|
||||
return base_path('resources') . ($path ? '/' . ltrim($path, '/') : '');
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('storage_path')) {
|
||||
/**
|
||||
* Get the path to the storage directory
|
||||
*/
|
||||
function storage_path(string $path = ''): string
|
||||
{
|
||||
return base_path('storage') . ($path ? '/' . ltrim($path, '/') : '');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!function_exists('view')) {
|
||||
/**
|
||||
* Create a view response (simple implementation)
|
||||
*/
|
||||
function view(string $view, array $data = []): string
|
||||
{
|
||||
$viewPath = resource_path('views/' . str_replace('.', '/', $view) . '.php');
|
||||
|
||||
if (!file_exists($viewPath)) {
|
||||
throw new \InvalidArgumentException("View [{$view}] not found.");
|
||||
}
|
||||
|
||||
extract($data);
|
||||
ob_start();
|
||||
require $viewPath;
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('fake')) {
|
||||
/**
|
||||
* Create fake data using FakerPHP (Laravel-style implementation)
|
||||
*/
|
||||
function fake(?string $locale = null): \Faker\Generator
|
||||
{
|
||||
return \Faker\Factory::create($locale ?? 'en_US');
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('collect')) {
|
||||
/**
|
||||
* Create a collection from the given value (using Illuminate Support)
|
||||
*/
|
||||
function collect(mixed $value = []): \Illuminate\Support\Collection
|
||||
{
|
||||
return new \Illuminate\Support\Collection($value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('data_get')) {
|
||||
/**
|
||||
* Get an item from an array or object using "dot" notation
|
||||
*/
|
||||
function data_get(mixed $target, string|array|int|null $key, mixed $default = null): mixed
|
||||
{
|
||||
return \Illuminate\Support\Arr::get($target, $key, $default);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('data_set')) {
|
||||
/**
|
||||
* Set an item on an array or object using "dot" notation
|
||||
*/
|
||||
function data_set(mixed &$target, string|array $key, mixed $value, bool $overwrite = true): mixed
|
||||
{
|
||||
return \Illuminate\Support\Arr::set($target, $key, $value, $overwrite);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('str')) {
|
||||
/**
|
||||
* Create a new stringable object from the given string
|
||||
*/
|
||||
function str(string $string = ''): \Illuminate\Support\Stringable
|
||||
{
|
||||
return \Illuminate\Support\Str::of($string);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('now')) {
|
||||
/**
|
||||
* Create a new Carbon instance for the current time
|
||||
*/
|
||||
function now($tz = null): \Carbon\Carbon
|
||||
{
|
||||
return \Carbon\Carbon::now($tz);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('today')) {
|
||||
/**
|
||||
* Create a new Carbon instance for today
|
||||
*/
|
||||
function today($tz = null): \Carbon\Carbon
|
||||
{
|
||||
return \Carbon\Carbon::today($tz);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('tap')) {
|
||||
/**
|
||||
* Call the given Closure with the given value then return the value
|
||||
*/
|
||||
function tap(mixed $value, ?callable $callback = null): mixed
|
||||
{
|
||||
if (is_null($callback)) {
|
||||
return new \Illuminate\Support\HigherOrderTapProxy($value);
|
||||
}
|
||||
|
||||
$callback($value);
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('optional')) {
|
||||
/**
|
||||
* Provide access to optional objects
|
||||
*/
|
||||
function optional(mixed $value = null, ?callable $callback = null): mixed
|
||||
{
|
||||
if (is_null($callback)) {
|
||||
return new \Illuminate\Support\Optional($value);
|
||||
}
|
||||
|
||||
if (!is_null($value)) {
|
||||
return $callback($value);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('app')) {
|
||||
/**
|
||||
* Get the available container instance
|
||||
*/
|
||||
function app(?string $abstract = null, array $parameters = []): mixed
|
||||
{
|
||||
if (is_null($abstract)) {
|
||||
return \Illuminate\Container\Container::getInstance();
|
||||
}
|
||||
|
||||
return \Illuminate\Container\Container::getInstance()->make($abstract, $parameters);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('config')) {
|
||||
/**
|
||||
* Get / set the specified configuration value
|
||||
*/
|
||||
function config(array|string|null $key = null, mixed $default = null): mixed
|
||||
{
|
||||
if (is_null($key)) {
|
||||
return app('config');
|
||||
}
|
||||
|
||||
if (is_array($key)) {
|
||||
return app('config')->set($key);
|
||||
}
|
||||
|
||||
return app('config')->get($key, $default);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('event')) {
|
||||
/**
|
||||
* Dispatch an event and call the listeners
|
||||
*/
|
||||
function event(string|object $event, mixed $payload = [], bool $halt = false): array|null
|
||||
{
|
||||
return app('events')->dispatch($event, $payload, $halt);
|
||||
}
|
||||
}
|
41
bootstrap/app.php
Normal file
41
bootstrap/app.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Application Bootstrap
|
||||
*
|
||||
* This file creates and configures the application instance
|
||||
*/
|
||||
|
||||
// Load environment variables
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
|
||||
$dotenv->safeLoad();
|
||||
|
||||
// Load legacy common.php only if not already loaded (will be loaded by LegacyController when needed)
|
||||
// This prevents header conflicts for modern API routes
|
||||
if (!defined('BB_PATH')) {
|
||||
// Only load for legacy routes - modern routes will skip this
|
||||
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
|
||||
$urlPath = parse_url($requestUri, PHP_URL_PATH) ?: $requestUri;
|
||||
$isLegacyRoute = str_ends_with($urlPath, '.php') || $requestUri === '/' || str_contains($requestUri, 'tracker') || str_contains($requestUri, 'forum');
|
||||
|
||||
if ($isLegacyRoute) {
|
||||
require_once dirname(__DIR__) . '/common.php';
|
||||
}
|
||||
}
|
||||
|
||||
// Define application constants
|
||||
define('IN_TORRENTPIER', true);
|
||||
|
||||
// Load container bootstrap
|
||||
require_once __DIR__ . '/container.php';
|
||||
|
||||
// Create the application container
|
||||
$container = createContainer(dirname(__DIR__));
|
||||
|
||||
// Get the Router instance (it will be created and registered by RouteServiceProvider)
|
||||
$router = $container->make(\App\Http\Routing\Router::class);
|
||||
|
||||
// Return the router for handling requests
|
||||
return $router;
|
47
bootstrap/console.php
Normal file
47
bootstrap/console.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Console Bootstrap
|
||||
*
|
||||
* Bootstrap the console application
|
||||
*/
|
||||
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Symfony\Component\Console\Application;
|
||||
|
||||
// Only define DEXTER_BINARY if not already defined
|
||||
if (!defined('DEXTER_BINARY')) {
|
||||
define('DEXTER_BINARY', true);
|
||||
}
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
// Load container bootstrap
|
||||
require_once __DIR__ . '/container.php';
|
||||
|
||||
// Create the application container
|
||||
$container = createContainer(dirname(__DIR__));
|
||||
|
||||
// Create Symfony Console Application
|
||||
$app = new Application('TorrentPier Console', '3.0-dev');
|
||||
|
||||
// Get registered commands from the container
|
||||
try {
|
||||
if ($container->bound('console.commands')) {
|
||||
$commands = $container->make('console.commands');
|
||||
foreach ($commands as $command) {
|
||||
try {
|
||||
$app->add($container->make($command));
|
||||
} catch (BindingResolutionException $e) {
|
||||
// Skip commands that can't be resolved - console still works with built-in commands
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (BindingResolutionException $e) {
|
||||
// No commands registered or service binding failed - console still works with built-in commands
|
||||
}
|
||||
|
||||
// Return the console application
|
||||
return $app;
|
115
bootstrap/container.php
Normal file
115
bootstrap/container.php
Normal file
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Container Bootstrap
|
||||
*
|
||||
* Creates and configures the Illuminate Container instance
|
||||
*/
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
|
||||
/**
|
||||
* Create and configure the application container
|
||||
*/
|
||||
function createContainer(string $rootPath): Container
|
||||
{
|
||||
// Load environment variables first
|
||||
$dotenv = \Dotenv\Dotenv::createImmutable($rootPath);
|
||||
$dotenv->safeLoad();
|
||||
|
||||
$container = new Container();
|
||||
|
||||
// Set the container instance globally
|
||||
Container::setInstance($container);
|
||||
|
||||
// Register base paths
|
||||
$container->instance('path.base', $rootPath);
|
||||
$container->instance('path.app', $rootPath . '/app');
|
||||
$container->instance('path.config', $rootPath . '/config');
|
||||
$container->instance('path.database', $rootPath . '/database');
|
||||
$container->instance('path.public', $rootPath . '/public');
|
||||
$container->instance('path.resources', $rootPath . '/resources');
|
||||
$container->instance('path.storage', $rootPath . '/storage');
|
||||
|
||||
// Register the container itself
|
||||
$container->instance(Container::class, $container);
|
||||
$container->alias(Container::class, 'app');
|
||||
$container->alias(Container::class, Illuminate\Contracts\Container\Container::class);
|
||||
$container->alias(Container::class, Psr\Container\ContainerInterface::class);
|
||||
|
||||
// Load configuration
|
||||
loadConfiguration($container, $rootPath);
|
||||
|
||||
// Register service providers
|
||||
registerServiceProviders($container);
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration files
|
||||
*/
|
||||
function loadConfiguration(Container $container, string $rootPath): void
|
||||
{
|
||||
$configPath = $rootPath . '/config';
|
||||
|
||||
// Create unified config repository
|
||||
$config = new \Illuminate\Config\Repository();
|
||||
|
||||
// Load services configuration
|
||||
if (file_exists($configPath . '/services.php')) {
|
||||
$services = require $configPath . '/services.php';
|
||||
foreach ($services as $abstract => $concrete) {
|
||||
if (is_callable($concrete)) {
|
||||
$container->bind($abstract, $concrete);
|
||||
} else {
|
||||
$container->bind($abstract, $concrete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load all config files into the repository
|
||||
foreach (glob($configPath . '/*.php') as $file) {
|
||||
$key = basename($file, '.php');
|
||||
$value = require $file;
|
||||
$config->set($key, $value);
|
||||
// Also register individual config files for backward compatibility
|
||||
$container->instance("config.{$key}", $value);
|
||||
}
|
||||
|
||||
// Register the unified config repository
|
||||
$container->instance('config', $config);
|
||||
$container->bind(\Illuminate\Config\Repository::class, function() use ($config) {
|
||||
return $config;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register service providers
|
||||
*/
|
||||
function registerServiceProviders(Container $container): void
|
||||
{
|
||||
$providers = [
|
||||
// Register your service providers here
|
||||
\App\Providers\AppServiceProvider::class,
|
||||
\App\Providers\EventServiceProvider::class,
|
||||
\App\Providers\RouteServiceProvider::class,
|
||||
\App\Providers\ConsoleServiceProvider::class,
|
||||
];
|
||||
|
||||
foreach ($providers as $providerClass) {
|
||||
if (class_exists($providerClass)) {
|
||||
$provider = new $providerClass($container);
|
||||
|
||||
if (method_exists($provider, 'register')) {
|
||||
$provider->register();
|
||||
}
|
||||
|
||||
if (method_exists($provider, 'boot')) {
|
||||
$container->call([$provider, 'boot']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 . "
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
|
||||
//
|
||||
|
|
|
@ -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);
|
||||
|
|
126
cliff.toml
126
cliff.toml
|
@ -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 = """
|
||||
[](https://github.com/torrentpier)\n
|
||||
# 📖 Change Log\n
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://keats.github.io/tera/docs/#introduction
|
||||
body = """
|
||||
{%- macro remote_url() -%}
|
||||
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- macro nightly_url() -%}
|
||||
https://nightly.link/{{ remote.github.owner }}/{{ remote.github.repo }}/workflows/ci/master/TorrentPier-master
|
||||
{%- endmacro -%}
|
||||
|
||||
{% macro print_commit(commit) -%}
|
||||
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
|
||||
{% if commit.breaking %}[**breaking**] {% endif %}\
|
||||
{{ commit.message | upper_first }} - \
|
||||
([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
|
||||
{% endmacro -%}
|
||||
|
||||
{% if version %}\
|
||||
{% if previous.version %}\
|
||||
## [{{ version }}]\
|
||||
({{ self::remote_url() }}/compare/{{ previous.version }}..{{ version }}) ({{ timestamp | date(format="%Y-%m-%d") }})
|
||||
{% else %}\
|
||||
## {{ version }} ({{ timestamp | date(format="%Y-%m-%d") }})
|
||||
{% endif %}\
|
||||
{% else %}\
|
||||
## [nightly]({{ self::nightly_url() }})
|
||||
{% endif %}\
|
||||
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | striptags | trim | upper_first }}
|
||||
{% for commit in commits
|
||||
| filter(attribute="scope")
|
||||
| sort(attribute="scope") %}
|
||||
{{ self::print_commit(commit=commit) }}
|
||||
{%- endfor %}
|
||||
{% for commit in commits %}
|
||||
{%- if not commit.scope -%}
|
||||
{{ self::print_commit(commit=commit) }}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
{% endfor -%}
|
||||
{%- if github -%}
|
||||
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
|
||||
## New Contributors ❤️
|
||||
{% endif %}\
|
||||
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
|
||||
* @{{ contributor.username }} made their first contribution
|
||||
{%- if contributor.pr_number %} in \
|
||||
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
|
||||
{%- endif %}
|
||||
{%- endfor -%}
|
||||
{%- endif %}
|
||||
|
||||
|
||||
"""
|
||||
# template for the changelog footer
|
||||
footer = """
|
||||
"""
|
||||
# remove the leading and trailing whitespace from the templates
|
||||
trim = true
|
||||
# postprocessors
|
||||
postprocessors = [
|
||||
{ pattern = '<REPO>', replace = "https://github.com/torrentpier/torrentpier" }, # replace repository URL
|
||||
]
|
||||
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# filter out the commits that are not conventional
|
||||
filter_unconventional = true
|
||||
# process each line of a commit as an individual commit
|
||||
split_commits = false
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
# Replace issue numbers
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/pull/${2}))" },
|
||||
# Check spelling of the commit with https://github.com/crate-ci/typos
|
||||
# If the spelling is incorrect, it will be automatically fixed.
|
||||
# { pattern = '.*', replace_command = 'typos --write-changes -' },
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
|
||||
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
|
||||
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
|
||||
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
|
||||
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
|
||||
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
|
||||
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
|
||||
{ message = "^ignore|^release|^changelog", skip = true },
|
||||
{ message = "^chore|^ci|^misc", group = "<!-- 7 -->⚙️ Miscellaneous" },
|
||||
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
|
||||
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
|
||||
{ message = "^crowdin|^crodwin", group = "<!-- 10 -->🈳 New translations" }, # crowdin pulls supporting
|
||||
{ message = "^Composer", group = "<!-- 11 -->📦 Dependencies" }, # dependabot pulls supporting
|
||||
{ message = "^rem|^drop|^removed", group = "<!-- 12 -->🗑️ Removed" },
|
||||
{ message = ".*", group = "<!-- 13 -->💼 Other" },
|
||||
]
|
||||
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
||||
protect_breaking_commits = false
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = false
|
||||
# regex for matching git tags
|
||||
tag_pattern = "v[0-9].*"
|
||||
# sort the tags topologically
|
||||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "newest"
|
37
common.php
37
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_');
|
||||
|
|
|
@ -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": {
|
||||
|
|
2477
composer.lock
generated
2477
composer.lock
generated
File diff suppressed because it is too large
Load diff
77
config/app.php
Normal file
77
config/app.php
Normal file
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Application Configuration
|
||||
*/
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'name' => env('APP_NAME', 'TorrentPier'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Environment
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'debug' => env('APP_DEBUG', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application URL
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Locale Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
'fallback_locale' => 'en',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Key
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Autoloaded Service Providers
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'providers' => [
|
||||
// Add service providers here
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Class Aliases
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'aliases' => [
|
||||
// Add class aliases here
|
||||
],
|
||||
];
|
92
config/auth.php
Normal file
92
config/auth.php
Normal file
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Authentication Configuration
|
||||
*/
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'defaults' => [
|
||||
'guard' => 'web',
|
||||
'passwords' => 'users',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Guards
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'driver' => 'token',
|
||||
'provider' => 'users',
|
||||
'hash' => false,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Providers
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'database',
|
||||
'table' => 'bb_users',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Resetting Passwords
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => 'password_resets',
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Confirmation Timeout
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'password_timeout' => 10800,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'session' => [
|
||||
'lifetime' => env('SESSION_LIFETIME', 120),
|
||||
'expire_on_close' => false,
|
||||
'encrypt' => false,
|
||||
'files' => storage_path('framework/sessions'),
|
||||
'connection' => null,
|
||||
'table' => 'sessions',
|
||||
'store' => null,
|
||||
'lottery' => [2, 100],
|
||||
'cookie' => env('SESSION_COOKIE', 'torrentpier_session'),
|
||||
'path' => '/',
|
||||
'domain' => env('SESSION_DOMAIN', null),
|
||||
'secure' => env('SESSION_SECURE_COOKIE', false),
|
||||
'http_only' => true,
|
||||
'same_site' => 'lax',
|
||||
],
|
||||
];
|
77
config/cache.php
Normal file
77
config/cache.php
Normal file
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Cache Configuration
|
||||
*/
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'default' => env('CACHE_DRIVER', 'file'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Stores
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'stores' => [
|
||||
'file' => [
|
||||
'driver' => 'file',
|
||||
'path' => storage_path('framework/cache'),
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => 'cache',
|
||||
'lock_connection' => 'default',
|
||||
],
|
||||
|
||||
'memcached' => [
|
||||
'driver' => 'memcached',
|
||||
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||
'sasl' => [
|
||||
env('MEMCACHED_USERNAME'),
|
||||
env('MEMCACHED_PASSWORD'),
|
||||
],
|
||||
'options' => [
|
||||
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||
],
|
||||
'servers' => [
|
||||
[
|
||||
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||
'port' => env('MEMCACHED_PORT', 11211),
|
||||
'weight' => 100,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'array' => [
|
||||
'driver' => 'array',
|
||||
'serialize' => false,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Key Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'prefix' => env('CACHE_PREFIX', 'tp_cache'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Tags
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'tags' => [
|
||||
'torrents' => 3600,
|
||||
'users' => 1800,
|
||||
'forums' => 900,
|
||||
'stats' => 300,
|
||||
],
|
||||
];
|
86
config/database.php
Normal file
86
config/database.php
Normal file
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Database Configuration
|
||||
*/
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Database Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'default' => env('DB_CONNECTION', 'mysql'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Connections
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'connections' => [
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', 3306),
|
||||
'database' => env('DB_DATABASE', 'torrentpier'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => env('DB_PREFIX', 'bb_'),
|
||||
'options' => [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::ATTR_STRINGIFY_FETCHES => false,
|
||||
],
|
||||
],
|
||||
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'database' => env('DB_DATABASE_SQLITE', storage_path('database.sqlite')),
|
||||
'prefix' => '',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Migration Repository Table
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'migrations' => 'migrations',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Redis Databases
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'redis' => [
|
||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||
|
||||
'options' => [
|
||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||
'prefix' => env('REDIS_PREFIX', 'tp_'),
|
||||
],
|
||||
|
||||
'default' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', 6379),
|
||||
'database' => env('REDIS_DB', 0),
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', 6379),
|
||||
'database' => env('REDIS_CACHE_DB', 1),
|
||||
],
|
||||
],
|
||||
];
|
71
config/filesystems.php
Normal file
71
config/filesystems.php
Normal file
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Filesystem Configuration
|
||||
*/
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Filesystem Disk
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Filesystem Disks
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'disks' => [
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app'),
|
||||
'throw' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => env('APP_URL') . '/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
],
|
||||
|
||||
'torrents' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/torrents'),
|
||||
'throw' => false,
|
||||
],
|
||||
|
||||
'avatars' => [
|
||||
'driver' => 'local',
|
||||
'root' => public_path('images/avatars'),
|
||||
'url' => env('APP_URL') . '/images/avatars',
|
||||
'visibility' => 'public',
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION'),
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'url' => env('AWS_URL'),
|
||||
'endpoint' => env('AWS_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||
'throw' => false,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Symbolic Links
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'links' => [
|
||||
public_path('storage') => storage_path('app/public'),
|
||||
],
|
||||
];
|
102
config/routes.php
Normal file
102
config/routes.php
Normal file
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Route Configuration
|
||||
*
|
||||
* This file contains routing configuration for the TorrentPier application.
|
||||
* It defines route patterns, middleware, and other routing-related settings.
|
||||
*
|
||||
* NOTE: This configuration file is currently not used by the routing system.
|
||||
* Routes are loaded directly from src/Presentation/Http/Routes/web.php
|
||||
* This file serves as a template for future routing configuration implementation.
|
||||
*/
|
||||
|
||||
// Configuration is commented out as it's not currently integrated
|
||||
/*
|
||||
return [
|
||||
// Route caching configuration
|
||||
'cache' => [
|
||||
'enabled' => env('ROUTE_CACHE_ENABLED', false),
|
||||
'path' => env('ROUTE_CACHE_PATH', __DIR__ . '/../internal_data/cache/routes.php'),
|
||||
'ttl' => env('ROUTE_CACHE_TTL', 3600), // 1 hour
|
||||
],
|
||||
|
||||
// Global middleware (applied to all routes)
|
||||
'middleware' => [
|
||||
// 'TorrentPier\Infrastructure\Http\Middleware\CorsMiddleware',
|
||||
// 'TorrentPier\Infrastructure\Http\Middleware\SecurityHeadersMiddleware',
|
||||
],
|
||||
|
||||
// Route groups configuration
|
||||
'groups' => [
|
||||
// Web routes (HTML responses)
|
||||
'web' => [
|
||||
'prefix' => '',
|
||||
'middleware' => [
|
||||
// 'TorrentPier\Infrastructure\Http\Middleware\WebMiddleware',
|
||||
// 'TorrentPier\Infrastructure\Http\Middleware\StartSession',
|
||||
// 'TorrentPier\Infrastructure\Http\Middleware\VerifyCsrfToken',
|
||||
],
|
||||
],
|
||||
|
||||
// API routes (JSON responses)
|
||||
'api' => [
|
||||
'prefix' => '/api',
|
||||
'middleware' => [
|
||||
// 'TorrentPier\Infrastructure\Http\Middleware\ApiMiddleware',
|
||||
// 'TorrentPier\Infrastructure\Http\Middleware\RateLimitMiddleware',
|
||||
// 'TorrentPier\Infrastructure\Http\Middleware\AuthenticationMiddleware',
|
||||
],
|
||||
],
|
||||
|
||||
// Admin routes (Administrative interface)
|
||||
'admin' => [
|
||||
'prefix' => '/admin',
|
||||
'middleware' => [
|
||||
// 'TorrentPier\Infrastructure\Http\Middleware\AdminMiddleware',
|
||||
// 'TorrentPier\Infrastructure\Http\Middleware\RequireAdminAuth',
|
||||
// 'TorrentPier\Infrastructure\Http\Middleware\AuditLoggingMiddleware',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
// Route defaults
|
||||
'defaults' => [
|
||||
'namespace' => 'TorrentPier\Presentation\Http\Controllers',
|
||||
'timeout' => 30, // seconds
|
||||
'methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||
],
|
||||
|
||||
// Route constraints
|
||||
'constraints' => [
|
||||
'id' => '\d+',
|
||||
'hash' => '[a-fA-F0-9]+',
|
||||
'slug' => '[a-z0-9-]+',
|
||||
'uuid' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
|
||||
],
|
||||
|
||||
// Route patterns for common use cases
|
||||
'patterns' => [
|
||||
// Forum routes
|
||||
'forum' => '/forum/{forum_id:\d+}',
|
||||
'topic' => '/topic/{topic_id:\d+}',
|
||||
'post' => '/post/{post_id:\d+}',
|
||||
|
||||
// Torrent routes
|
||||
'torrent' => '/torrent/{torrent_id:\d+}',
|
||||
'torrent_hash' => '/torrent/{info_hash:[a-fA-F0-9]+}',
|
||||
|
||||
// User routes
|
||||
'user' => '/user/{user_id:\d+}',
|
||||
'profile' => '/profile/{username:[a-zA-Z0-9_-]+}',
|
||||
],
|
||||
|
||||
// Fallback routes
|
||||
'fallback' => [
|
||||
'enabled' => true,
|
||||
'handler' => 'TorrentPier\Presentation\Http\Controllers\Web\FallbackController@handle',
|
||||
],
|
||||
];
|
||||
*/
|
|
@ -1,30 +1,35 @@
|
|||
<?php
|
||||
|
||||
use function DI\factory;
|
||||
use function DI\get;
|
||||
use function DI\autowire;
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Service Container Bindings
|
||||
*
|
||||
* Define service bindings for the Illuminate Container
|
||||
*/
|
||||
|
||||
return [
|
||||
// Add custom service definitions here as they are implemented
|
||||
// Config service binding
|
||||
\TorrentPier\Config::class => function () {
|
||||
return \TorrentPier\Config::getInstance();
|
||||
},
|
||||
|
||||
// Examples (uncomment and modify when implementing):
|
||||
// Future service bindings can be added here:
|
||||
|
||||
// Logger service example
|
||||
// 'logger' => factory(function () {
|
||||
// 'logger' => function () {
|
||||
// $logger = new \Monolog\Logger('torrentpier');
|
||||
// $logger->pushHandler(new \Monolog\Handler\StreamHandler(__DIR__ . '/../internal_data/logs/app.log'));
|
||||
// return $logger;
|
||||
// }),
|
||||
// },
|
||||
|
||||
// Configuration service example
|
||||
// 'config' => factory(function () {
|
||||
// return [
|
||||
// 'app' => require __DIR__ . '/app.php',
|
||||
// 'database' => require __DIR__ . '/database.php',
|
||||
// 'cache' => require __DIR__ . '/cache.php',
|
||||
// ];
|
||||
// }),
|
||||
// Database service example
|
||||
// 'database' => function () {
|
||||
// return \TorrentPier\Database\DB::getInstance();
|
||||
// },
|
||||
|
||||
// Interface to implementation binding example
|
||||
// 'ServiceInterface' => autowire('ConcreteService'),
|
||||
// Cache service example
|
||||
// 'cache' => function () {
|
||||
// return \TorrentPier\Cache\Cache::getInstance();
|
||||
// },
|
||||
];
|
||||
|
|
91
config/tracker.php
Normal file
91
config/tracker.php
Normal file
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* BitTorrent Tracker Configuration
|
||||
*/
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Tracker Settings
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'announce_interval' => env('TRACKER_ANNOUNCE_INTERVAL', 900), // 15 minutes
|
||||
'min_interval' => env('TRACKER_MIN_INTERVAL', 300), // 5 minutes
|
||||
'max_peers' => env('TRACKER_MAX_PEERS', 50),
|
||||
'max_peers_per_torrent' => env('TRACKER_MAX_PEERS_PER_TORRENT', 100),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Peer Settings
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'peer_timeout' => env('TRACKER_PEER_TIMEOUT', 1800), // 30 minutes
|
||||
'peer_compact' => env('TRACKER_PEER_COMPACT', true),
|
||||
'peer_no_peer_id' => env('TRACKER_PEER_NO_PEER_ID', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Ratio Requirements
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'ratio_required' => env('TRACKER_RATIO_REQUIRED', false),
|
||||
'min_ratio' => env('TRACKER_MIN_RATIO', 0.5),
|
||||
'ratio_warning_threshold' => env('TRACKER_RATIO_WARNING', 0.8),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Upload/Download Limits
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'max_upload_speed' => env('TRACKER_MAX_UPLOAD_SPEED', 0), // 0 = unlimited
|
||||
'max_download_speed' => env('TRACKER_MAX_DOWNLOAD_SPEED', 0), // 0 = unlimited
|
||||
'max_torrents_per_user' => env('TRACKER_MAX_TORRENTS_PER_USER', 0), // 0 = unlimited
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Security Settings
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'passkey_required' => env('TRACKER_PASSKEY_REQUIRED', true),
|
||||
'ip_validation' => env('TRACKER_IP_VALIDATION', true),
|
||||
'user_agent_validation' => env('TRACKER_USER_AGENT_VALIDATION', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Allowed Clients
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'allowed_clients' => [
|
||||
'qBittorrent',
|
||||
'uTorrent',
|
||||
'BitTorrent',
|
||||
'Transmission',
|
||||
'Deluge',
|
||||
'rtorrent',
|
||||
'libtorrent',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Banned Clients
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'banned_clients' => [
|
||||
'BitComet',
|
||||
'BitLord',
|
||||
'Thunder',
|
||||
'Xunlei',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Statistics
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'stats_update_interval' => env('TRACKER_STATS_UPDATE_INTERVAL', 300), // 5 minutes
|
||||
'enable_scrape' => env('TRACKER_ENABLE_SCRAPE', true),
|
||||
'scrape_interval' => env('TRACKER_SCRAPE_INTERVAL', 600), // 10 minutes
|
||||
];
|
|
@ -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();
|
|
@ -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'] ?? '/';
|
|
@ -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;
|
|
@ -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']);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)";
|
|
@ -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']),
|
||||
]);
|
||||
|
|
@ -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'] . ': ' . '<b>' . bb_date(config()->get('board_startdate')) . '</b>') : false,
|
||||
'BOARD_START' => tp_config()->get('show_board_start_index') ? ($lang['BOARD_STARTED'] . ': ' . '<b>' . bb_date(tp_config()->get('board_startdate')) . '</b>') : 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) . ' <span class="small">(' . birthday_age(date('Y-m-d', strtotime('-1 year', strtotime($week['user_birthday'])))) . ')</span>';
|
||||
}
|
||||
$week_all = $week_all ? ' <a class="txtb" href="#" onclick="ajax.exec({action: \'index_data\', mode: \'birthday_week\'}); return false;" title="' . $lang['ALL'] . '">...</a>' : '';
|
||||
$week_list = sprintf($lang['BIRTHDAY_WEEK'], config()->get('birthday_check_day'), implode(', ', $week_list)) . $week_all;
|
||||
$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'])) {
|
|
@ -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();
|
|
@ -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('<br />', $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
|
||||
]);
|
|
@ -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 .= '</select>';
|
|||
|
||||
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);
|
||||
}
|
|
@ -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'];
|
||||
|
|
@ -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;
|
|
@ -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'));
|
|
@ -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']] . '<a href="' . TOPIC_URL . $row['topic_id'] . '">' . $row['topic_title'] . '</a><div class="spacer_12"></div>';
|
||||
$topics .= tp_config()->get('tor_icons')[$row['tor_status']] . '<a href="' . TOPIC_URL . $row['topic_id'] . '">' . $row['topic_title'] . '</a><div class="spacer_12"></div>';
|
||||
}
|
||||
if ($topics && !(IS_SUPER_ADMIN && !empty($_REQUEST['edit_tpl']))) {
|
||||
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'] . ': <span title="' . $lang['TOR_STATUS_NAME'][$tor_status['tor_status']] . '">' . config()->get('tor_icons')[$tor_status['tor_status']] . ' ' . $lang['TOR_STATUS_NAME'][$tor_status['tor_status']] . '</span>.');
|
||||
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'] . ': <span title="' . $lang['TOR_STATUS_NAME'][$tor_status['tor_status']] . '">' . tp_config()->get('tor_icons')[$tor_status['tor_status']] . ' ' . $lang['TOR_STATUS_NAME'][$tor_status['tor_status']] . '</span>.');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -281,7 +285,7 @@ if (!IS_GUEST && $mode != 'newtopic' && ($submit || $preview || $mode == 'quote'
|
|||
AND pt.post_id = p.post_id
|
||||
AND 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' => '<a class="small" href="' . POST_URL . $row['post_id'] . '#' . $row['post_id'] . '" title="' . $lang['POST_LINK'] . '">' . bb_date($row['post_time'], config()->get('post_date_format')) . '</a>',
|
||||
'POST_DATE' => '<a class="small" href="' . POST_URL . $row['post_id'] . '#' . $row['post_id'] . '" title="' . $lang['POST_LINK'] . '">' . bb_date($row['post_time'], tp_config()->get('post_date_format')) . '</a>',
|
||||
'MESSAGE' => get_parsed_post($row)
|
||||
]);
|
||||
}
|
||||
|
@ -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,
|
||||
|
|
@ -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), '<img class="smile" src="' . config()->get('smilies_path'));
|
||||
if ($count_smilies > config()->get('max_smilies_pm')) {
|
||||
if (tp_config()->get('max_smilies_pm')) {
|
||||
$count_smilies = substr_count(bbcode2html($privmsg_message), '<img class="smile" src="' . tp_config()->get('smilies_path'));
|
||||
if ($count_smilies > tp_config()->get('max_smilies_pm')) {
|
||||
$error = true;
|
||||
$error_msg .= ((!empty($error_msg)) ? '<br />' : '') . sprintf($lang['MAX_SMILIES_PER_POST'], config()->get('max_smilies_pm'));
|
||||
$error_msg .= ((!empty($error_msg)) ? '<br />' : '') . sprintf($lang['MAX_SMILIES_PER_POST'], tp_config()->get('max_smilies_pm'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -834,7 +838,7 @@ if ($mode == 'read') {
|
|||
}
|
||||
|
||||
if ($inbox_info = DB()->sql_fetchrow($result)) {
|
||||
if (config()->get('max_inbox_privmsgs') && $inbox_info['inbox_items'] >= config()->get('max_inbox_privmsgs')) {
|
||||
if (tp_config()->get('max_inbox_privmsgs') && $inbox_info['inbox_items'] >= tp_config()->get('max_inbox_privmsgs')) {
|
||||
$sql = "SELECT privmsgs_id FROM " . BB_PRIVMSGS . "
|
||||
WHERE ( privmsgs_type = " . PRIVMSGS_NEW_MAIL . "
|
||||
OR privmsgs_type = " . PRIVMSGS_READ_MAIL . "
|
||||
|
@ -902,7 +906,7 @@ if ($mode == 'read') {
|
|||
|
||||
\TorrentPier\Sessions::cache_rm_user_sessions($to_userdata['user_id']);
|
||||
|
||||
if (bf($to_userdata['user_opt'], 'user_opt', 'user_notify_pm') && $to_userdata['user_active'] && config()->get('pm_notify_enabled')) {
|
||||
if (bf($to_userdata['user_opt'], 'user_opt', 'user_notify_pm') && $to_userdata['user_active'] && tp_config()->get('pm_notify_enabled')) {
|
||||
// Sending email
|
||||
$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', []);
|
||||
}
|
|
@ -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();
|
|
@ -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');
|
||||
}
|
|
@ -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');
|
|
@ -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 = '<option value="' . $search_all . '" value="fs-' . $search_all
|
|||
$cat_forum_select = "\n" . '<select id="fs-main" style="width: 100%;" name="' . $forum_key . '[]" multiple size="' . $forum_select_size . "\">\n" . $search_all_opt . $opt . "</select>\n";
|
||||
|
||||
// Status select
|
||||
if (config()->get('tracker.search_by_tor_status')) {
|
||||
if (tp_config()->get('tracker.search_by_tor_status')) {
|
||||
$statuses = '<table border="0" cellpadding="0" cellspacing="0">';
|
||||
foreach (array_chunk(config()->get('tor_icons'), 2, true) as $statuses_part) {
|
||||
foreach (array_chunk(tp_config()->get('tor_icons'), 2, true) as $statuses_part) {
|
||||
$statuses .= '<tr>';
|
||||
foreach ($statuses_part as $status_id => $status_styles) {
|
||||
$checked_status = in_array($status_id, $status) ? 'checked' : '';
|
|
@ -9,7 +9,10 @@
|
|||
|
||||
define('BB_SCRIPT', 'forum');
|
||||
|
||||
require __DIR__ . '/common.php';
|
||||
// Skip loading common.php if already loaded (when run through routing system)
|
||||
if (!defined('IN_TORRENTPIER')) {
|
||||
require __DIR__ . '/../common.php';
|
||||
}
|
||||
|
||||
$page_cfg['include_bbcode_js'] = true;
|
||||
|
||||
|
@ -121,7 +124,7 @@ if ($mark_read && !IS_GUEST) {
|
|||
}
|
||||
|
||||
// Subforums
|
||||
$show_subforums = config()->get('sf_on_first_page_only') ? !$start : true;
|
||||
$show_subforums = tp_config()->get('sf_on_first_page_only') ? !$start : true;
|
||||
|
||||
if (!$forums = $datastore->get('cat_forums')) {
|
||||
$datastore->update('cat_forums');
|
||||
|
@ -179,7 +182,7 @@ if (!$forum_data['forum_parent'] && isset($forums['f'][$forum_id]['subforums'])
|
|||
$last_post_user = profile_url(['username' => $sf_data['sf_last_username'], 'user_id' => $sf_data['sf_last_user_id'], 'user_rank' => $sf_data['user_rank']]);
|
||||
|
||||
if ($sf_data['forum_last_post_id']) {
|
||||
$last_post = bb_date($sf_data['topic_last_post_time'], config()->get('last_post_date_format'));
|
||||
$last_post = bb_date($sf_data['topic_last_post_time'], tp_config()->get('last_post_date_format'));
|
||||
$last_post .= "<br />$last_post_user";
|
||||
$last_post .= '<a href="' . POST_URL . $sf_data['forum_last_post_id'] . '#' . $sf_data['forum_last_post_id'] . '"><img src="' . $images['icon_latest_reply'] . '" class="icon2" alt="latest" title="' . $lang['VIEW_LATEST_POST'] . '" /></a>';
|
||||
}
|
||||
|
@ -203,7 +206,7 @@ if (!$forum_data['forum_parent'] && isset($forums['f'][$forum_id]['subforums'])
|
|||
'LAST_TOPIC_ID' => $sf_data['last_topic_id'],
|
||||
'LAST_TOPIC_TIP' => $sf_data['last_topic_title'],
|
||||
'LAST_TOPIC_TITLE' => str_short($sf_data['last_topic_title'], $last_topic_max_len),
|
||||
'LAST_POST_TIME' => bb_date($sf_data['topic_last_post_time'], config()->get('last_post_date_format')),
|
||||
'LAST_POST_TIME' => bb_date($sf_data['topic_last_post_time'], tp_config()->get('last_post_date_format')),
|
||||
'LAST_POST_ID' => $sf_data['forum_last_post_id'],
|
||||
'LAST_POST_USER' => $last_post_user,
|
||||
'ICON_LATEST_REPLY' => $images['icon_latest_reply']
|
||||
|
@ -217,16 +220,16 @@ unset($rowset);
|
|||
$datastore->rm('cat_forums');
|
||||
|
||||
// Topics per page
|
||||
$topics_per_page = config()->get('topics_per_page');
|
||||
$topics_per_page = tp_config()->get('topics_per_page');
|
||||
$select_tpp = '';
|
||||
|
||||
if ($is_auth['auth_mod']) {
|
||||
if ($req_tpp = abs((int)(@$_REQUEST['tpp'])) and in_array($req_tpp, config()->get('allowed_topics_per_page'))) {
|
||||
if ($req_tpp = abs((int)(@$_REQUEST['tpp'])) and in_array($req_tpp, tp_config()->get('allowed_topics_per_page'))) {
|
||||
$topics_per_page = $req_tpp;
|
||||
}
|
||||
|
||||
$select_tpp = [];
|
||||
foreach (config()->get('allowed_topics_per_page') as $tpp) {
|
||||
foreach (tp_config()->get('allowed_topics_per_page') as $tpp) {
|
||||
$select_tpp[$tpp] = $tpp;
|
||||
}
|
||||
}
|
||||
|
@ -282,7 +285,7 @@ $order_sql = "ORDER BY t.topic_type DESC, $sort_method $order_method";
|
|||
$limit_topics_time_sql = ($topic_days) ? "AND t.topic_last_post_time > " . (TIMENOW - 86400 * $topic_days) : '';
|
||||
|
||||
$select_tor_sql = $join_tor_sql = '';
|
||||
$join_dl = (config()->get('show_dl_status_in_forum') && !IS_GUEST);
|
||||
$join_dl = (tp_config()->get('show_dl_status_in_forum') && !IS_GUEST);
|
||||
|
||||
$where_tor_sql = '';
|
||||
if ($forum_data['allow_reg_tracker']) {
|
||||
|
@ -423,7 +426,7 @@ foreach ($topic_rowset as $topic) {
|
|||
$topic_id = $topic['topic_id'];
|
||||
$moved = ($topic['topic_status'] == TOPIC_MOVED);
|
||||
$replies = $topic['topic_replies'];
|
||||
$t_hot = ($replies >= config()->get('hot_threshold'));
|
||||
$t_hot = ($replies >= tp_config()->get('hot_threshold'));
|
||||
$t_type = $topic['topic_type'];
|
||||
$separator = '';
|
||||
$is_unread = is_unread($topic['topic_last_post_time'], $topic_id, $forum_id);
|
||||
|
@ -449,14 +452,14 @@ foreach ($topic_rowset as $topic) {
|
|||
'TOPICS_SEPARATOR' => $separator,
|
||||
'IS_UNREAD' => $is_unread,
|
||||
'TOPIC_ICON' => get_topic_icon($topic, $is_unread),
|
||||
'PAGINATION' => $moved ? '' : build_topic_pagination(TOPIC_URL . $topic_id, $replies, config()->get('posts_per_page')),
|
||||
'PAGINATION' => $moved ? '' : build_topic_pagination(TOPIC_URL . $topic_id, $replies, tp_config()->get('posts_per_page')),
|
||||
'REPLIES' => $moved ? '' : $replies,
|
||||
'VIEWS' => $moved ? '' : $topic['topic_views'],
|
||||
'TOR_STALED' => ($forum_data['allow_reg_tracker'] && !($t_type == POST_ANNOUNCE || $t_type == POST_STICKY || $topic['tor_size'])),
|
||||
'TOR_FROZEN' => isset($topic['tor_status']) ? ((!IS_AM) ? isset(config()->get('tor_frozen')[$topic['tor_status']]) : '') : '',
|
||||
'TOR_FROZEN' => isset($topic['tor_status']) ? ((!IS_AM) ? isset(tp_config()->get('tor_frozen')[$topic['tor_status']]) : '') : '',
|
||||
'TOR_TYPE' => isset($topic['tor_type']) ? is_gold($topic['tor_type']) : '',
|
||||
|
||||
'TOR_STATUS_ICON' => isset($topic['tor_status']) ? config()->get('tor_icons')[$topic['tor_status']] : '',
|
||||
'TOR_STATUS_ICON' => isset($topic['tor_status']) ? tp_config()->get('tor_icons')[$topic['tor_status']] : '',
|
||||
'TOR_STATUS_TEXT' => isset($topic['tor_status']) ? $lang['TOR_STATUS_NAME'][$topic['tor_status']] : '',
|
||||
|
||||
'ATTACH' => $topic['topic_attachment'] ?? false,
|
||||
|
@ -468,7 +471,7 @@ foreach ($topic_rowset as $topic) {
|
|||
|
||||
'TOPIC_AUTHOR' => profile_url(['username' => str_short($topic['first_username'], 15), 'user_id' => $topic['first_user_id'], 'user_rank' => $topic['first_user_rank']]),
|
||||
'LAST_POSTER' => profile_url(['username' => str_short($topic['last_username'], 15), 'user_id' => $topic['last_user_id'], 'user_rank' => $topic['last_user_rank']]),
|
||||
'LAST_POST_TIME' => bb_date($topic['topic_last_post_time'], config()->get('last_post_date_format')),
|
||||
'LAST_POST_TIME' => bb_date($topic['topic_last_post_time'], tp_config()->get('last_post_date_format')),
|
||||
'LAST_POST_ID' => $topic['topic_last_post_id'],
|
||||
]);
|
||||
|
||||
|
@ -498,7 +501,7 @@ $pg_url .= $topic_days ? "&topicdays=$topic_days" : '';
|
|||
$pg_url .= $sort_value ? "&sort=$sort_value" : '';
|
||||
$pg_url .= $order_value ? "&order=$order_value" : '';
|
||||
$pg_url .= $moderation ? "&mod=1" : '';
|
||||
$pg_url .= ($topics_per_page != config()->get('topics_per_page')) ? "&tpp=$topics_per_page" : '';
|
||||
$pg_url .= ($topics_per_page != tp_config()->get('topics_per_page')) ? "&tpp=$topics_per_page" : '';
|
||||
|
||||
if ($found_topics) {
|
||||
generate_pagination($pg_url, $forum_topics, $topics_per_page, $start);
|
|
@ -9,7 +9,11 @@
|
|||
|
||||
define('BB_SCRIPT', 'topic');
|
||||
|
||||
require __DIR__ . '/common.php';
|
||||
// Skip loading common.php if already loaded (when run through routing system)
|
||||
if (!defined('IN_TORRENTPIER')) {
|
||||
require __DIR__ . '/../common.php';
|
||||
}
|
||||
|
||||
require INC_DIR . '/bbcode.php';
|
||||
|
||||
$datastore->enqueue([
|
||||
|
@ -34,16 +38,16 @@ $user->session_start();
|
|||
set_die_append_msg();
|
||||
|
||||
// Posts per page
|
||||
$posts_per_page = config()->get('posts_per_page');
|
||||
$posts_per_page = tp_config()->get('posts_per_page');
|
||||
$select_ppp = '';
|
||||
|
||||
if ($userdata['session_admin']) {
|
||||
if ($req_ppp = abs((int)(@$_REQUEST['ppp'])) and in_array($req_ppp, config()->get('allowed_posts_per_page'))) {
|
||||
if ($req_ppp = abs((int)(@$_REQUEST['ppp'])) and in_array($req_ppp, tp_config()->get('allowed_posts_per_page'))) {
|
||||
$posts_per_page = $req_ppp;
|
||||
}
|
||||
|
||||
$select_ppp = [];
|
||||
foreach (config()->get('allowed_posts_per_page') as $ppp) {
|
||||
foreach (tp_config()->get('allowed_posts_per_page') as $ppp) {
|
||||
$select_ppp[$ppp] = $ppp;
|
||||
}
|
||||
}
|
||||
|
@ -236,7 +240,7 @@ if ($post_id && !empty($t_data['prev_posts'])) {
|
|||
// Is user watching this thread?
|
||||
$can_watch_topic = $is_watching_topic = false;
|
||||
|
||||
if (config()->get('topic_notify_enabled')) {
|
||||
if (tp_config()->get('topic_notify_enabled')) {
|
||||
if (!IS_GUEST) {
|
||||
$can_watch_topic = true;
|
||||
|
||||
|
@ -426,7 +430,7 @@ $pg_url .= $post_days ? "&postdays=$post_days" : '';
|
|||
$pg_url .= ($post_order != 'asc') ? "&postorder=$post_order" : '';
|
||||
$pg_url .= isset($_REQUEST['single']) ? "&single=1" : '';
|
||||
$pg_url .= $moderation ? "&mod=1" : '';
|
||||
$pg_url .= ($posts_per_page != config()->get('posts_per_page')) ? "&ppp=$posts_per_page" : '';
|
||||
$pg_url .= ($posts_per_page != tp_config()->get('posts_per_page')) ? "&ppp=$posts_per_page" : '';
|
||||
|
||||
generate_pagination($pg_url, $total_replies, $posts_per_page, $start);
|
||||
|
||||
|
@ -448,7 +452,7 @@ $sel_post_order_ary = [
|
|||
];
|
||||
|
||||
$topic_has_poll = $t_data['topic_vote'];
|
||||
$poll_time_expired = ($t_data['topic_time'] < TIMENOW - config()->get('poll_max_days') * 86400);
|
||||
$poll_time_expired = ($t_data['topic_time'] < TIMENOW - tp_config()->get('poll_max_days') * 86400);
|
||||
$can_manage_poll = ($t_data['topic_poster'] == $userdata['user_id'] || $is_auth['auth_mod']);
|
||||
$can_add_poll = ($can_manage_poll && !$topic_has_poll && !$poll_time_expired && !$start);
|
||||
|
||||
|
@ -470,19 +474,19 @@ $template->assign_vars([
|
|||
'TOPIC_TITLE' => $topic_title,
|
||||
'PORNO_FORUM' => $t_data['allow_porno_topic'],
|
||||
'REPLY_IMG' => $reply_img,
|
||||
'SHOW_BOT_NICK' => config()->get('show_bot_nick'),
|
||||
'SHOW_BOT_NICK' => tp_config()->get('show_bot_nick'),
|
||||
'T_POST_REPLY' => $reply_alt,
|
||||
|
||||
'HIDE_FROM' => $user->opt_js['h_from'],
|
||||
'HIDE_AVATAR' => $user->opt_js['h_av'],
|
||||
'HIDE_RANK_IMG' => ($user->opt_js['h_rnk_i'] && config()->get('show_rank_image')),
|
||||
'HIDE_RANK_IMG' => ($user->opt_js['h_rnk_i'] && tp_config()->get('show_rank_image')),
|
||||
'HIDE_POST_IMG' => $user->opt_js['h_post_i'],
|
||||
'HIDE_SMILE' => $user->opt_js['h_smile'],
|
||||
'HIDE_SIGNATURE' => $user->opt_js['h_sig'],
|
||||
'SPOILER_OPENED' => $user->opt_js['sp_op'],
|
||||
'SHOW_IMG_AFTER_LOAD' => $user->opt_js['i_aft_l'],
|
||||
|
||||
'HIDE_RANK_IMG_DIS' => !config()->get('show_rank_image'),
|
||||
'HIDE_RANK_IMG_DIS' => !tp_config()->get('show_rank_image'),
|
||||
|
||||
'PINNED_FIRST_POST' => $t_data['topic_show_first_post'],
|
||||
'PIN_HREF' => $t_data['topic_show_first_post'] ? "modcp.php?" . POST_TOPIC_URL . "=$topic_id&mode=post_unpin" : "modcp.php?" . POST_TOPIC_URL . "=$topic_id&mode=post_pin",
|
||||
|
@ -559,7 +563,7 @@ for ($i = 0; $i < $total_posts; $i++) {
|
|||
$poster_bot = ($poster_id == BOT_UID);
|
||||
$poster = $poster_guest ? $lang['GUEST'] : $postrow[$i]['username'];
|
||||
|
||||
$post_date = bb_date($postrow[$i]['post_time'], config()->get('post_date_format'));
|
||||
$post_date = bb_date($postrow[$i]['post_time'], tp_config()->get('post_date_format'));
|
||||
$max_post_time = max($max_post_time, $postrow[$i]['post_time']);
|
||||
$poster_posts = !$poster_guest ? $postrow[$i]['user_posts'] : '';
|
||||
$poster_from = ($postrow[$i]['user_from'] && !$poster_guest) ? $postrow[$i]['user_from'] : '';
|
||||
|
@ -584,8 +588,8 @@ for ($i = 0; $i < $total_posts; $i++) {
|
|||
$poster_rank = $rank_image = '';
|
||||
$user_rank = $postrow[$i]['user_rank'];
|
||||
if (!$user->opt_js['h_rnk_i'] and isset($ranks[$user_rank])) {
|
||||
$rank_image = (config()->get('show_rank_image') && $ranks[$user_rank]['rank_image']) ? '<img src="' . $ranks[$user_rank]['rank_image'] . '" alt="" title="" border="0" />' : '';
|
||||
$poster_rank = config()->get('show_rank_text') ? $ranks[$user_rank]['rank_title'] : '';
|
||||
$rank_image = (tp_config()->get('show_rank_image') && $ranks[$user_rank]['rank_image']) ? '<img src="' . $ranks[$user_rank]['rank_image'] . '" alt="" title="" border="0" />' : '';
|
||||
$poster_rank = tp_config()->get('show_rank_text') ? $ranks[$user_rank]['rank_title'] : '';
|
||||
}
|
||||
|
||||
// Handle anon users posting with usernames
|
||||
|
@ -611,7 +615,7 @@ for ($i = 0; $i < $total_posts; $i++) {
|
|||
// Parse message and sig
|
||||
$message = get_parsed_post($postrow[$i]);
|
||||
|
||||
$user_sig = (config()->get('allow_sig') && !$user->opt_js['h_sig'] && $postrow[$i]['user_sig']) ? $postrow[$i]['user_sig'] : '';
|
||||
$user_sig = (tp_config()->get('allow_sig') && !$user->opt_js['h_sig'] && $postrow[$i]['user_sig']) ? $postrow[$i]['user_sig'] : '';
|
||||
|
||||
if (bf($postrow[$i]['user_opt'], 'user_opt', 'dis_sig')) {
|
||||
$user_sig = $lang['SIGNATURE_DISABLE'];
|
||||
|
@ -642,7 +646,7 @@ for ($i = 0; $i < $total_posts; $i++) {
|
|||
|
||||
// Replace newlines (we use this rather than nl2br because till recently it wasn't XHTML compliant)
|
||||
if ($user_sig) {
|
||||
$user_sig = config()->get('user_signature_start') . $user_sig . config()->get('user_signature_end');
|
||||
$user_sig = tp_config()->get('user_signature_start') . $user_sig . tp_config()->get('user_signature_end');
|
||||
}
|
||||
|
||||
// Editing information
|
||||
|
@ -686,11 +690,11 @@ for ($i = 0; $i < $total_posts; $i++) {
|
|||
'POSTER_NAME_JS' => addslashes($poster),
|
||||
'POSTER_RANK' => $poster_rank,
|
||||
'RANK_IMAGE' => $rank_image,
|
||||
'POSTER_JOINED' => config()->get('show_poster_joined') ? $poster_longevity : '',
|
||||
'POSTER_JOINED' => tp_config()->get('show_poster_joined') ? $poster_longevity : '',
|
||||
|
||||
'POSTER_JOINED_DATE' => $poster_joined,
|
||||
'POSTER_POSTS' => (config()->get('show_poster_posts') && $poster_posts) ? '<a href="search.php?search_author=1&uid=' . $poster_id . '" target="_blank">' . $poster_posts . '</a>' : '',
|
||||
'POSTER_FROM' => config()->get('show_poster_from') ? render_flag($poster_from, false) : '',
|
||||
'POSTER_POSTS' => (tp_config()->get('show_poster_posts') && $poster_posts) ? '<a href="search.php?search_author=1&uid=' . $poster_id . '" target="_blank">' . $poster_posts . '</a>' : '',
|
||||
'POSTER_FROM' => tp_config()->get('show_poster_from') ? render_flag($poster_from, false) : '',
|
||||
'POSTER_BOT' => $poster_bot,
|
||||
'POSTER_GUEST' => $poster_guest,
|
||||
'POSTER_ID' => $poster_id,
|
||||
|
@ -764,13 +768,13 @@ if (defined('SPLIT_FORM_START')) {
|
|||
}
|
||||
|
||||
// Quick Reply
|
||||
if (config()->get('show_quick_reply')) {
|
||||
if (tp_config()->get('show_quick_reply')) {
|
||||
if ($is_auth['auth_reply'] && !$locked) {
|
||||
$template->assign_vars([
|
||||
'QUICK_REPLY' => true,
|
||||
'QR_POST_ACTION' => POSTING_URL,
|
||||
'QR_TOPIC_ID' => $topic_id,
|
||||
'CAPTCHA_HTML' => (IS_GUEST && !config()->get('captcha.disabled')) ? bb_captcha('get') : ''
|
||||
'CAPTCHA_HTML' => (IS_GUEST && !tp_config()->get('captcha.disabled')) ? bb_captcha('get') : ''
|
||||
]);
|
||||
|
||||
if (!IS_GUEST) {
|
||||
|
@ -788,7 +792,7 @@ foreach ($is_auth as $name => $is) {
|
|||
$template->assign_vars(['PG_ROW_CLASS' => $pg_row_class ?? 'row1']);
|
||||
|
||||
if (IS_ADMIN) {
|
||||
$template->assign_vars(['U_LOGS' => "admin/admin_log.php?" . POST_TOPIC_URL . "=$topic_id&db=" . config()->get('log_days_keep')]);
|
||||
$template->assign_vars(['U_LOGS' => "admin/admin_log.php?" . POST_TOPIC_URL . "=$topic_id&db=" . tp_config()->get('log_days_keep')]);
|
||||
}
|
||||
|
||||
print_page('viewtopic.tpl');
|
81
database/factories/UserFactory.php
Normal file
81
database/factories/UserFactory.php
Normal file
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
/**
|
||||
* User Factory
|
||||
*
|
||||
* Create fake user data for testing
|
||||
*/
|
||||
class UserFactory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state using Laravel-style helpers
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'username' => str('user_')->append(str()->random(8))->lower(),
|
||||
'user_email' => fake()->email(),
|
||||
'user_password' => password_hash('password', PASSWORD_BCRYPT),
|
||||
'user_level' => 0, // Regular user
|
||||
'user_active' => 1,
|
||||
'user_regdate' => now()->timestamp,
|
||||
'user_lastvisit' => now()->timestamp,
|
||||
'user_timezone' => 0,
|
||||
'user_lang' => 'en',
|
||||
'user_dateformat' => 'd M Y H:i',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a user instance
|
||||
*/
|
||||
public function make(array $attributes = []): User
|
||||
{
|
||||
$data = array_merge($this->definition(), $attributes);
|
||||
return new User(DB(), $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and save a user instance
|
||||
*/
|
||||
public function create(array $attributes = []): User
|
||||
{
|
||||
$user = $this->make($attributes);
|
||||
$user->save();
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an admin user
|
||||
*/
|
||||
public function admin(): self
|
||||
{
|
||||
return $this->state(['user_level' => 1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a moderator user
|
||||
*/
|
||||
public function moderator(): self
|
||||
{
|
||||
return $this->state(['user_level' => 2]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply state to the factory
|
||||
*/
|
||||
public function state(array $state): self
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->states[] = $state;
|
||||
return $clone;
|
||||
}
|
||||
|
||||
private array $states = [];
|
||||
}
|
34
database/seeders/DatabaseSeeder.php
Normal file
34
database/seeders/DatabaseSeeder.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
/**
|
||||
* Database Seeder
|
||||
*
|
||||
* Run all seeders for the application
|
||||
*/
|
||||
class DatabaseSeeder
|
||||
{
|
||||
/**
|
||||
* Seed the application's database
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// Call individual seeders here
|
||||
// $this->call(UserSeeder::class);
|
||||
// $this->call(ForumSeeder::class);
|
||||
// $this->call(TorrentSeeder::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a seeder class
|
||||
*/
|
||||
protected function call(string $seederClass): void
|
||||
{
|
||||
echo "Seeding: {$seederClass}\n";
|
||||
$seeder = new $seederClass();
|
||||
$seeder->run();
|
||||
}
|
||||
}
|
21
dexter
Executable file
21
dexter
Executable file
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Dexter - TorrentPier Command Line Interface
|
||||
*
|
||||
* Laravel-style CLI for running commands
|
||||
*/
|
||||
|
||||
// Load the bootstrap and get the application
|
||||
$app = require __DIR__ . '/bootstrap/console.php';
|
||||
|
||||
// Run the console application with proper error handling
|
||||
try {
|
||||
exit($app->run());
|
||||
} catch (\Exception $e) {
|
||||
fwrite(STDERR, "Error: " . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Example: How to use the DI Container in TorrentPier 3.0
|
||||
*
|
||||
* NOTE: These are examples for future implementation following the hexagonal architecture spec.
|
||||
* Most services referenced here don't exist yet and will be implemented in phases.
|
||||
*/
|
||||
|
||||
use TorrentPier\Infrastructure\DependencyInjection\Bootstrap;
|
||||
use TorrentPier\Infrastructure\DependencyInjection\ContainerFactory;
|
||||
|
||||
// ===== PHASE 1: Foundation (CURRENT) =====
|
||||
|
||||
// 1. Bootstrap the container (typically done once in index.php or bootstrap file)
|
||||
$rootPath = __DIR__ . '/../..';
|
||||
$container = Bootstrap::init($rootPath);
|
||||
|
||||
// 2. Basic container usage (works now)
|
||||
$containerInstance = app(); // Get container itself
|
||||
$hasService = $container->has('some.service'); // Check if service exists
|
||||
|
||||
// ===== PHASE 2: Domain Modeling (FUTURE) =====
|
||||
|
||||
// 3. Repository interfaces (when implemented in Domain layer)
|
||||
// $userRepository = app('TorrentPier\Domain\User\Repository\UserRepositoryInterface');
|
||||
// $torrentRepository = app('TorrentPier\Domain\Tracker\Repository\TorrentRepositoryInterface');
|
||||
// $forumRepository = app('TorrentPier\Domain\Forum\Repository\ForumRepositoryInterface');
|
||||
|
||||
// ===== PHASE 3: Application Services (FUTURE) =====
|
||||
|
||||
// 4. Command/Query handlers (when implemented)
|
||||
// $registerUserHandler = app('TorrentPier\Application\User\Handler\RegisterUserHandler');
|
||||
// $announceHandler = app('TorrentPier\Application\Tracker\Handler\ProcessAnnounceHandler');
|
||||
// $createPostHandler = app('TorrentPier\Application\Forum\Handler\CreatePostHandler');
|
||||
|
||||
// 5. Making command instances with parameters
|
||||
// $command = $container->make('TorrentPier\Application\User\Command\RegisterUserCommand', [
|
||||
// 'username' => 'john_doe',
|
||||
// 'email' => 'john@example.com',
|
||||
// 'password' => 'secure_password'
|
||||
// ]);
|
||||
|
||||
// ===== PHASE 4: Infrastructure (FUTURE) =====
|
||||
|
||||
// 6. Database and cache (when infrastructure is implemented)
|
||||
// $database = app('database.connection.default');
|
||||
// $cache = app('cache.factory')('forum'); // Get cache instance for 'forum' namespace
|
||||
|
||||
// ===== PHASE 5: Presentation (FUTURE) =====
|
||||
|
||||
// 7. Controllers (when implemented)
|
||||
// $userController = app('TorrentPier\Presentation\Http\Controllers\Api\UserController');
|
||||
// $trackerController = app('TorrentPier\Presentation\Http\Controllers\Web\TrackerController');
|
||||
|
||||
// ===== TESTING EXAMPLES =====
|
||||
|
||||
// 8. Testing with custom container (works now)
|
||||
$testContainer = ContainerFactory::create([
|
||||
'definitions' => [
|
||||
'test.service' => \DI\factory(function () {
|
||||
return new class {
|
||||
public function test() { return 'test'; }
|
||||
};
|
||||
}),
|
||||
],
|
||||
'environment' => 'testing',
|
||||
]);
|
||||
|
||||
// 9. Safe service resolution (works now)
|
||||
try {
|
||||
$service = app('optional.service');
|
||||
} catch (RuntimeException $e) {
|
||||
// Service not found, handle gracefully
|
||||
$service = null;
|
||||
}
|
||||
|
||||
// ===== LEGACY INTEGRATION (CURRENT) =====
|
||||
|
||||
// 10. Integration with legacy code
|
||||
// In legacy files, after including common.php or similar:
|
||||
if (!Bootstrap::getContainer()) {
|
||||
Bootstrap::init(BB_ROOT ?? __DIR__ . '/../..');
|
||||
}
|
||||
|
||||
// 11. Method injection (works now if service exists)
|
||||
class ExampleService
|
||||
{
|
||||
public function processData(string $data)
|
||||
{
|
||||
// Container can inject dependencies when calling this method
|
||||
return "Processed: $data";
|
||||
}
|
||||
}
|
||||
|
||||
$exampleService = new ExampleService();
|
||||
$result = $container->call([$exampleService, 'processData'], [
|
||||
'data' => 'test data'
|
||||
]);
|
212
docs/illuminate-integration.md
Normal file
212
docs/illuminate-integration.md
Normal file
|
@ -0,0 +1,212 @@
|
|||
# Illuminate Package Integration Guide
|
||||
|
||||
This document outlines the integration of Laravel's Illuminate packages in TorrentPier 3.0.
|
||||
|
||||
> **Note**: After these changes, run `composer update` to install the newly added `illuminate/config` package.
|
||||
|
||||
## Installed Illuminate Packages
|
||||
|
||||
The following Illuminate packages are available in TorrentPier:
|
||||
|
||||
1. **illuminate/collections** - Powerful array/collection manipulation
|
||||
2. **illuminate/config** - Configuration repository system
|
||||
3. **illuminate/container** - Dependency injection container
|
||||
4. **illuminate/database** - Database query builder and Eloquent ORM
|
||||
5. **illuminate/events** - Event dispatcher system
|
||||
6. **illuminate/http** - HTTP request/response handling
|
||||
7. **illuminate/routing** - Routing system
|
||||
8. **illuminate/support** - Support utilities (Str, Arr, etc.)
|
||||
9. **illuminate/validation** - Data validation
|
||||
|
||||
## Current Integration Status
|
||||
|
||||
### ✅ Fully Integrated
|
||||
|
||||
1. **Container (illuminate/container)**
|
||||
- Used as the core DI container via `bootstrap/container.php`
|
||||
- Custom wrapper in `App\Container\Container`
|
||||
- Global `app()` helper function available
|
||||
|
||||
2. **Support (illuminate/support)**
|
||||
- Helper functions: `collect()`, `str()`, `data_get()`, `data_set()`, `tap()`, `optional()`
|
||||
- String and array manipulation utilities
|
||||
- Collection class for data manipulation
|
||||
|
||||
3. **Events (illuminate/events)**
|
||||
- Event dispatcher registered in `AppServiceProvider`
|
||||
- Event classes in `app/Events/`
|
||||
- Listener classes in `app/Listeners/`
|
||||
- `EventServiceProvider` for registering event listeners
|
||||
- Global `event()` helper function
|
||||
|
||||
4. **Config (illuminate/config)**
|
||||
- Configuration repository registered in `AppServiceProvider`
|
||||
- Global `config()` helper function
|
||||
- Loads all PHP files from `/config/` directory
|
||||
|
||||
5. **Validation (illuminate/validation)**
|
||||
- Used in `App\Http\Requests\FormRequest` base class
|
||||
- Provides Laravel-style request validation
|
||||
|
||||
### ⚠️ Partially Integrated
|
||||
|
||||
1. **HTTP (illuminate/http)**
|
||||
- Request/Response classes used in routing
|
||||
- Not fully utilizing all HTTP features
|
||||
|
||||
2. **Collections (illuminate/collections)**
|
||||
- Available via `collect()` helper
|
||||
- Could be used more extensively in models/services
|
||||
|
||||
### ❌ Not Yet Integrated
|
||||
|
||||
1. **Database (illuminate/database)**
|
||||
- Package installed but not used
|
||||
- Project uses Nette Database instead
|
||||
- Could migrate to Eloquent ORM in future
|
||||
|
||||
2. **Routing (illuminate/routing)**
|
||||
- Package installed but custom router is used
|
||||
- Could migrate to full Illuminate routing
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Using the Event System
|
||||
|
||||
```php
|
||||
// Dispatch an event
|
||||
use App\Events\UserRegistered;
|
||||
|
||||
event(new UserRegistered(
|
||||
userId: $userId,
|
||||
username: $username,
|
||||
email: $email,
|
||||
registeredAt: new DateTime()
|
||||
));
|
||||
|
||||
// In a listener
|
||||
class SendWelcomeEmail
|
||||
{
|
||||
public function handle(UserRegistered $event): void
|
||||
{
|
||||
// Send welcome email
|
||||
mail($event->getEmail(), 'Welcome!', 'Welcome to TorrentPier!');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Configuration
|
||||
|
||||
```php
|
||||
// Get a config value
|
||||
$appName = config('app.name');
|
||||
$debug = config('app.debug', false);
|
||||
|
||||
// Set a config value
|
||||
config(['app.timezone' => 'UTC']);
|
||||
|
||||
// Get entire config array
|
||||
$databaseConfig = config('database');
|
||||
```
|
||||
|
||||
### Using Collections
|
||||
|
||||
```php
|
||||
// Create a collection
|
||||
$users = collect($userArray);
|
||||
|
||||
// Chain methods
|
||||
$activeAdmins = $users
|
||||
->where('status', 'active')
|
||||
->where('role', 'admin')
|
||||
->sortBy('name')
|
||||
->values();
|
||||
|
||||
// Use collection helpers
|
||||
$names = $users->pluck('name');
|
||||
$grouped = $users->groupBy('role');
|
||||
```
|
||||
|
||||
### Using Validation
|
||||
|
||||
```php
|
||||
use App\Http\Requests\FormRequest;
|
||||
|
||||
class RegisterRequest extends FormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'username' => 'required|string|min:3|max:25',
|
||||
'email' => 'required|email',
|
||||
'password' => 'required|min:8|confirmed',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// In controller
|
||||
public function register(RegisterRequest $request)
|
||||
{
|
||||
$validated = $request->validated();
|
||||
// Process registration with validated data
|
||||
}
|
||||
```
|
||||
|
||||
## Service Providers
|
||||
|
||||
Service providers bootstrap the Illuminate packages:
|
||||
|
||||
1. **AppServiceProvider** - Registers config and events
|
||||
2. **EventServiceProvider** - Maps events to listeners
|
||||
3. **RouteServiceProvider** - Loads route files
|
||||
|
||||
Register new providers in `bootstrap/container.php`.
|
||||
|
||||
## Future Integration Opportunities
|
||||
|
||||
1. **Migrate to Eloquent ORM**
|
||||
- Replace Nette Database with Eloquent
|
||||
- Use migrations and model relationships
|
||||
- Leverage query scopes and mutators
|
||||
|
||||
2. **Full Routing Integration**
|
||||
- Replace custom router with Illuminate routing
|
||||
- Use route model binding
|
||||
- Implement route caching
|
||||
|
||||
3. **Add Queue System**
|
||||
- Install `illuminate/queue`
|
||||
- Process long-running tasks asynchronously
|
||||
- Integrate with event listeners
|
||||
|
||||
4. **Add Cache Integration**
|
||||
- Install `illuminate/cache`
|
||||
- Replace custom cache with Laravel's cache
|
||||
- Use cache tags and TTL
|
||||
|
||||
5. **Add Filesystem Integration**
|
||||
- Already have `league/flysystem`
|
||||
- Add `illuminate/filesystem` for Laravel integration
|
||||
- Unified file operations
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Dependency Injection**
|
||||
- Inject services via constructor
|
||||
- Use the container for resolution
|
||||
- Avoid service location pattern
|
||||
|
||||
2. **Leverage Events**
|
||||
- Decouple components with events
|
||||
- Use listeners for side effects
|
||||
- Consider queued listeners for heavy tasks
|
||||
|
||||
3. **Configuration Management**
|
||||
- Keep environment-specific values in `.env`
|
||||
- Use config files for application settings
|
||||
- Cache configuration in production
|
||||
|
||||
4. **Follow Laravel Conventions**
|
||||
- Use Laravel naming conventions
|
||||
- Structure code like a Laravel app
|
||||
- Leverage Laravel patterns and practices
|
|
@ -1,436 +0,0 @@
|
|||
# Hexagonal Architecture Directory Structure Specification
|
||||
|
||||
## Overview
|
||||
|
||||
This document specifies the new hexagonal architecture directory structure for TorrentPier 3.0. The structure follows Domain-Driven Design (DDD) principles and implements a clean separation of concerns through hexagonal architecture (ports and adapters pattern).
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
/src/
|
||||
├── Domain/ # Core business logic - no framework dependencies
|
||||
│ ├── Forum/ # Forum bounded context
|
||||
│ │ ├── Model/ # Aggregates and entities
|
||||
│ │ ├── ValueObject/ # Value objects (PostId, ThreadTitle, etc.)
|
||||
│ │ ├── Repository/ # Repository interfaces
|
||||
│ │ └── Exception/ # Domain-specific exceptions
|
||||
│ ├── Tracker/ # BitTorrent tracker bounded context
|
||||
│ │ ├── Model/ # Torrent, Peer aggregates
|
||||
│ │ ├── ValueObject/ # InfoHash, PeerId, etc.
|
||||
│ │ ├── Repository/ # Repository interfaces
|
||||
│ │ └── Exception/ # Tracker-specific exceptions
|
||||
│ ├── User/ # User management bounded context
|
||||
│ │ ├── Model/ # User aggregate
|
||||
│ │ ├── ValueObject/ # UserId, Email, Username
|
||||
│ │ ├── Repository/ # User repository interface
|
||||
│ │ └── Exception/ # Authentication/authorization exceptions
|
||||
│ └── Shared/ # Shared kernel - minimal shared concepts
|
||||
│ ├── Model/ # Base classes (AggregateRoot, Entity)
|
||||
│ ├── ValueObject/ # Common value objects (Id, DateTime)
|
||||
│ └── Event/ # Domain events base classes
|
||||
│
|
||||
├── Application/ # Application services - orchestration layer
|
||||
│ ├── Forum/
|
||||
│ │ ├── Command/ # Commands (CreatePost, LockThread)
|
||||
│ │ ├── Query/ # Queries (GetThreadList, SearchPosts)
|
||||
│ │ └── Handler/ # Command and query handlers
|
||||
│ ├── Tracker/
|
||||
│ │ ├── Command/ # Commands (RegisterTorrent, ProcessAnnounce)
|
||||
│ │ ├── Query/ # Queries (GetPeerList, GetTorrentStats)
|
||||
│ │ └── Handler/ # Command and query handlers
|
||||
│ └── User/
|
||||
│ ├── Command/ # Commands (RegisterUser, ChangePassword)
|
||||
│ ├── Query/ # Queries (GetUserProfile, SearchUsers)
|
||||
│ └── Handler/ # Command and query handlers
|
||||
│
|
||||
├── Infrastructure/ # External concerns and implementations
|
||||
│ ├── Persistence/ # Data persistence layer
|
||||
│ │ ├── Database/ # Database adapter and connection management
|
||||
│ │ ├── Migration/ # Database migrations
|
||||
│ │ └── Repository/ # Repository implementations
|
||||
│ ├── Cache/ # Caching implementations
|
||||
│ │ ├── Redis/ # Redis adapter
|
||||
│ │ ├── Memcached/ # Memcached adapter
|
||||
│ │ └── File/ # File-based cache adapter
|
||||
│ ├── Email/ # Email service implementations
|
||||
│ │ ├── Template/ # Email templates
|
||||
│ │ └── Transport/ # SMTP, API transports
|
||||
│ └── FileStorage/ # File storage abstractions
|
||||
│ ├── Local/ # Local filesystem storage
|
||||
│ └── S3/ # AWS S3 storage adapter
|
||||
│
|
||||
└── Presentation/ # User interface layer
|
||||
├── Http/ # Web interface
|
||||
│ ├── Controllers/ # HTTP controllers
|
||||
│ │ ├── Admin/ # Admin panel controllers
|
||||
│ │ ├── Api/ # REST API controllers
|
||||
│ │ └── Web/ # Web UI controllers
|
||||
│ ├── Middleware/ # HTTP middleware (auth, CORS, etc.)
|
||||
│ ├── Requests/ # Request DTOs and validation
|
||||
│ └── Responses/ # Response transformers
|
||||
└── Cli/ # Command line interface
|
||||
└── Commands/ # Console commands
|
||||
|
||||
# Additional directories (outside /src/)
|
||||
/config/ # Application configuration
|
||||
├── app.php # Main application settings
|
||||
├── database.php # Database connections
|
||||
├── cache.php # Cache drivers configuration
|
||||
├── tracker.php # BitTorrent tracker settings
|
||||
└── environments/ # Environment-specific overrides
|
||||
|
||||
/tests/ # Test suites (Pest)
|
||||
├── Unit/ # Unit tests (mirrors src/ structure)
|
||||
├── Feature/ # Feature/Integration tests
|
||||
├── Pest.php # Pest configuration
|
||||
└── TestCase.php # Base test case
|
||||
```
|
||||
|
||||
## Directory README.md Templates
|
||||
|
||||
### Domain Layer READMEs
|
||||
|
||||
#### `/src/Domain/README.md`
|
||||
```markdown
|
||||
# Domain Layer
|
||||
|
||||
This directory contains the core business logic of TorrentPier. Code here should:
|
||||
- Have no dependencies on frameworks or infrastructure
|
||||
- Represent pure business rules and domain models
|
||||
- Be testable in isolation
|
||||
- Use only PHP language features and domain concepts
|
||||
|
||||
## Bounded Contexts
|
||||
- **Forum**: Discussion forums, posts, threads
|
||||
- **Tracker**: BitTorrent tracking, peers, torrents
|
||||
- **User**: User management, authentication, profiles
|
||||
- **Shared**: Minimal shared concepts between contexts
|
||||
```
|
||||
|
||||
#### `/src/Domain/Tracker/Model/README.md`
|
||||
```markdown
|
||||
# Tracker Domain Models
|
||||
|
||||
Contains aggregate roots and entities for the BitTorrent tracker:
|
||||
- `Torrent`: Aggregate root for torrent management
|
||||
- `Peer`: Entity representing a BitTorrent peer
|
||||
- `TorrentStatistics`: Value object for torrent stats
|
||||
|
||||
Example:
|
||||
```php
|
||||
class Torrent extends AggregateRoot
|
||||
{
|
||||
public function announce(Peer $peer, AnnounceEvent $event): void
|
||||
{
|
||||
// Business logic for handling announces
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `/src/Domain/Tracker/ValueObject/README.md`
|
||||
```markdown
|
||||
# Tracker Value Objects
|
||||
|
||||
Immutable objects representing domain concepts:
|
||||
- `InfoHash`: 20-byte torrent identifier
|
||||
- `PeerId`: Peer client identifier
|
||||
- `Port`: Network port (1-65535)
|
||||
- `BytesTransferred`: Upload/download bytes
|
||||
|
||||
Example:
|
||||
```php
|
||||
final class InfoHash
|
||||
{
|
||||
private string $hash;
|
||||
|
||||
public function __construct(string $hash)
|
||||
{
|
||||
$this->guardAgainstInvalidHash($hash);
|
||||
$this->hash = $hash;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Application Layer READMEs
|
||||
|
||||
#### `/src/Application/README.md`
|
||||
```markdown
|
||||
# Application Layer
|
||||
|
||||
Contains application services that orchestrate domain objects to fulfill use cases.
|
||||
- Commands: Write operations that change state
|
||||
- Queries: Read operations for data retrieval
|
||||
- Handlers: Process commands and queries
|
||||
|
||||
This layer should:
|
||||
- Coordinate domain objects
|
||||
- Handle transactions
|
||||
- Dispatch domain events
|
||||
- Not contain business logic
|
||||
```
|
||||
|
||||
#### `/src/Application/Tracker/Command/README.md`
|
||||
```markdown
|
||||
# Tracker Commands
|
||||
|
||||
Commands representing write operations:
|
||||
- `RegisterTorrentCommand`: Register new torrent
|
||||
- `UpdateTorrentCommand`: Modify torrent details
|
||||
- `DeleteTorrentCommand`: Remove torrent from tracker
|
||||
|
||||
Example:
|
||||
```php
|
||||
final class RegisterTorrentCommand
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $infoHash,
|
||||
public readonly int $uploaderId,
|
||||
public readonly string $name,
|
||||
public readonly int $size
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
### Infrastructure Layer READMEs
|
||||
|
||||
#### `/src/Infrastructure/README.md`
|
||||
```markdown
|
||||
# Infrastructure Layer
|
||||
|
||||
Technical implementations and external service adapters:
|
||||
- Database persistence
|
||||
- Caching mechanisms
|
||||
- Email services
|
||||
- File storage
|
||||
- Third-party integrations
|
||||
|
||||
Infrastructure depends on domain, not vice versa.
|
||||
```
|
||||
|
||||
#### `/src/Infrastructure/Persistence/Repository/README.md`
|
||||
```markdown
|
||||
# Repository Implementations
|
||||
|
||||
Concrete implementations of domain repository interfaces:
|
||||
- Uses database adapter for persistence
|
||||
- Implements caching strategies
|
||||
- Handles query optimization
|
||||
- Supports multiple database backends
|
||||
|
||||
Example:
|
||||
```php
|
||||
class TorrentRepository implements TorrentRepositoryInterface
|
||||
{
|
||||
public function __construct(
|
||||
private DatabaseAdapterInterface $db
|
||||
) {}
|
||||
|
||||
public function findByInfoHash(InfoHash $infoHash): ?Torrent
|
||||
{
|
||||
// Database adapter implementation
|
||||
$row = $this->db->select('torrents')
|
||||
->where('info_hash', $infoHash->toString())
|
||||
->first();
|
||||
|
||||
return $row ? $this->hydrateFromRow($row) : null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Presentation Layer READMEs
|
||||
|
||||
#### `/src/Presentation/README.md`
|
||||
```markdown
|
||||
# Presentation Layer
|
||||
|
||||
User interface implementations:
|
||||
- HTTP controllers for web and API
|
||||
- CLI commands for console operations
|
||||
- Request/response handling
|
||||
- Input validation
|
||||
- Output formatting
|
||||
|
||||
This layer translates between external format and application format.
|
||||
```
|
||||
|
||||
#### `/src/Presentation/Http/Controllers/Api/README.md`
|
||||
```markdown
|
||||
# API Controllers
|
||||
|
||||
RESTful API endpoints following OpenAPI specification:
|
||||
- JSON request/response format
|
||||
- Proper HTTP status codes
|
||||
- HATEOAS where applicable
|
||||
- Rate limiting aware
|
||||
|
||||
Example:
|
||||
```php
|
||||
class UserController
|
||||
{
|
||||
public function register(RegisterRequest $request): JsonResponse
|
||||
{
|
||||
$command = new RegisterUserCommand(
|
||||
$request->getUsername(),
|
||||
$request->getEmail(),
|
||||
$request->getPassword()
|
||||
);
|
||||
|
||||
$userId = $this->commandBus->handle($command);
|
||||
|
||||
return new JsonResponse([
|
||||
'id' => $userId,
|
||||
'username' => $request->getUsername()
|
||||
], Response::HTTP_CREATED);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `/src/Presentation/Http/Controllers/Admin/README.md`
|
||||
```markdown
|
||||
# Admin Panel Controllers
|
||||
|
||||
Administrative interface controllers with enhanced security:
|
||||
- Role-based access control (RBAC)
|
||||
- Audit logging for all actions
|
||||
- Additional authentication checks
|
||||
- Administrative dashboards and reports
|
||||
|
||||
Example:
|
||||
```php
|
||||
class AdminUserController
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$query = new GetUsersQuery(
|
||||
page: $request->getPage(),
|
||||
filters: $request->getFilters()
|
||||
);
|
||||
|
||||
$users = $this->queryBus->handle($query);
|
||||
|
||||
return $this->render('admin/users/index', [
|
||||
'users' => $users,
|
||||
'filters' => $request->getFilters()
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `/config/README.md`
|
||||
```markdown
|
||||
# Application Configuration
|
||||
|
||||
System configuration files using PHP arrays for type safety and IDE support:
|
||||
|
||||
- **app.php**: Core application settings
|
||||
- Site name, URL, timezone
|
||||
- Debug mode, environment
|
||||
- Feature flags and toggles
|
||||
|
||||
- **database.php**: Database connection settings
|
||||
- Multiple connection definitions
|
||||
- Read/write splitting configuration
|
||||
- Connection pooling settings
|
||||
|
||||
- **cache.php**: Cache driver configurations
|
||||
- Redis, Memcached, file-based settings
|
||||
- TTL defaults per cache type
|
||||
- Cache key prefixes
|
||||
|
||||
- **tracker.php**: BitTorrent tracker settings
|
||||
- Announce intervals
|
||||
- Peer limits
|
||||
- Ratio requirements
|
||||
|
||||
- **environments/**: Environment-specific overrides
|
||||
- Development, staging, production settings
|
||||
- Local developer configurations
|
||||
|
||||
Example database configuration:
|
||||
```php
|
||||
<?php
|
||||
return [
|
||||
'default' => env('DB_CONNECTION', 'mysql'),
|
||||
|
||||
'connections' => [
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', 3306),
|
||||
'database' => env('DB_DATABASE', 'tp'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'options' => [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. **Phase 1: Foundation**
|
||||
|
||||
- Create directory structure
|
||||
- Set up base classes in Domain/Shared
|
||||
- Configure dependency injection
|
||||
|
||||
2. **Phase 2: Domain Modeling**
|
||||
|
||||
- Implement core aggregates
|
||||
- Create value objects
|
||||
- Define repository interfaces
|
||||
|
||||
3. **Phase 3: Application Services**
|
||||
|
||||
- Create commands and queries
|
||||
- Implement handlers
|
||||
- Set up event dispatching
|
||||
|
||||
4. **Phase 4: Infrastructure**
|
||||
|
||||
- Implement repositories
|
||||
- Configure database adapter
|
||||
- Set up caching
|
||||
|
||||
5. **Phase 5: Presentation**
|
||||
|
||||
- Create controllers
|
||||
- Implement middleware
|
||||
- Build CLI commands
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
- Existing code remains in current locations
|
||||
- New features built in hexagonal architecture
|
||||
- Gradual migration using strangler fig pattern
|
||||
- Legacy adapters bridge old and new code
|
||||
- Feature flags control rollout
|
||||
|
||||
## Key Principles
|
||||
|
||||
1. **Dependency Rule**: Dependencies point inward (Presentation → Application → Domain)
|
||||
2. **Domain Isolation**: Business logic has no framework dependencies
|
||||
3. **Interface Segregation**: Small, focused interfaces
|
||||
4. **CQRS**: Separate read and write models
|
||||
5. **Event-Driven**: Domain events for cross-context communication
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- **Domain**: Pure unit tests, no mocks needed
|
||||
- **Application**: Unit tests with mocked repositories
|
||||
- **Infrastructure**: Integration tests with real services
|
||||
- **Presentation**: E2E tests for user journeys
|
||||
|
||||
## Notes for Developers
|
||||
|
||||
- Start reading code from the Domain layer
|
||||
- Business rules live in aggregates, not services
|
||||
- Use value objects for type safety
|
||||
- Prefer composition over inheritance
|
||||
- Keep bounded contexts loosely coupled
|
496
docs/specs/mvc-architecture-spec.md
Normal file
496
docs/specs/mvc-architecture-spec.md
Normal file
|
@ -0,0 +1,496 @@
|
|||
# MVC Architecture Directory Structure Specification
|
||||
|
||||
## Overview
|
||||
|
||||
This document specifies the MVC (Model-View-Controller) architecture directory structure for TorrentPier 3.0. The structure follows a simple, Laravel-inspired approach that prioritizes developer familiarity and ease of maintenance over complex enterprise patterns.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
# Laravel-style root structure
|
||||
/app/ # Application code (PSR-4: App\)
|
||||
├── Console/ # Console commands
|
||||
│ └── Commands/ # Artisan-style commands for Dexter
|
||||
├── Http/ # HTTP layer
|
||||
│ ├── Controllers/ # Controllers
|
||||
│ │ ├── Admin/ # Admin panel controllers
|
||||
│ │ ├── Api/ # API controllers
|
||||
│ │ └── Web/ # Web controllers
|
||||
│ ├── Middleware/ # HTTP middleware
|
||||
│ └── Requests/ # Form request validation
|
||||
├── Models/ # Data models (using Nette Database)
|
||||
│ ├── Forum/ # Forum models (Post, Thread, Forum)
|
||||
│ ├── Tracker/ # Tracker models (Torrent, Peer, Announce)
|
||||
│ └── User/ # User models (User, Profile, Permission)
|
||||
├── Services/ # Business logic services
|
||||
│ ├── Forum/ # Forum services
|
||||
│ ├── Tracker/ # Tracker services
|
||||
│ └── User/ # User services
|
||||
├── Repositories/ # Data access layer (optional)
|
||||
├── Events/ # Event classes
|
||||
├── Listeners/ # Event listeners
|
||||
├── Jobs/ # Background jobs
|
||||
├── Mail/ # Mailable classes
|
||||
├── Notifications/ # Notification classes
|
||||
└── Exceptions/ # Exception handling
|
||||
|
||||
/bootstrap/ # Application bootstrap
|
||||
├── app.php # Application bootstrap
|
||||
├── console.php # Console bootstrap
|
||||
└── cache/ # Bootstrap cache
|
||||
|
||||
/config/ # Configuration files
|
||||
├── app.php # Application config
|
||||
├── auth.php # Authentication config
|
||||
├── cache.php # Cache configuration
|
||||
├── database.php # Database connections
|
||||
├── filesystems.php # File storage config
|
||||
├── tracker.php # BitTorrent tracker settings
|
||||
└── (legacy configs...) # Existing config files
|
||||
|
||||
/database/ # Database files
|
||||
├── migrations/ # Database migrations (moved from /migrations/)
|
||||
├── factories/ # Model factories for testing
|
||||
└── seeders/ # Database seeders
|
||||
|
||||
/public/ # Public web root
|
||||
├── index.php # Front controller
|
||||
├── css/ # Public CSS
|
||||
├── js/ # Public JavaScript
|
||||
├── images/ # Public images
|
||||
└── fonts/ # Web fonts
|
||||
|
||||
/resources/ # Resources
|
||||
├── views/ # View templates
|
||||
│ ├── admin/ # Admin panel views
|
||||
│ ├── forum/ # Forum views
|
||||
│ ├── tracker/ # Tracker views
|
||||
│ └── layouts/ # Layout templates
|
||||
├── lang/ # Language files
|
||||
├── js/ # JavaScript source
|
||||
└── css/ # CSS/SCSS source
|
||||
|
||||
/routes/ # Route definitions
|
||||
├── web.php # Web routes
|
||||
├── api.php # API routes
|
||||
├── admin.php # Admin routes
|
||||
└── console.php # Console routes
|
||||
|
||||
/src/ # Framework/Infrastructure code (PSR-4: TorrentPier\)
|
||||
├── Database/ # Database abstraction
|
||||
├── Cache/ # Cache system
|
||||
├── Infrastructure/ # DI container, HTTP routing, etc.
|
||||
├── Legacy/ # Legacy code adapters
|
||||
└── helpers.php # Global helper functions
|
||||
|
||||
/storage/ # Storage directory
|
||||
├── app/ # Application storage
|
||||
│ ├── public/ # Publicly accessible files
|
||||
│ └── private/ # Private files
|
||||
├── framework/ # Framework storage
|
||||
│ ├── cache/ # File cache
|
||||
│ ├── sessions/ # Session files
|
||||
│ └── views/ # Compiled view cache
|
||||
└── logs/ # Application logs
|
||||
|
||||
/tests/ # Test suites
|
||||
├── Feature/ # Feature tests
|
||||
├── Unit/ # Unit tests
|
||||
└── TestCase.php # Base test case
|
||||
|
||||
# Legacy directories (being migrated)
|
||||
/library/ # Legacy core code
|
||||
/controllers/ # Legacy PHP controllers
|
||||
/admin/ # Legacy admin interface
|
||||
/styles/ # Legacy templates/assets
|
||||
/internal_data/ # Legacy cache/logs
|
||||
|
||||
# Root files
|
||||
.env # Environment variables
|
||||
.env.example # Environment example
|
||||
composer.json # Dependencies (App\ and TorrentPier\ namespaces)
|
||||
dexter # CLI interface
|
||||
index.php # Legacy entry point (redirects to public/)
|
||||
```
|
||||
|
||||
## Directory README.md Templates
|
||||
|
||||
### Application Layer READMEs
|
||||
|
||||
#### `/app/README.md`
|
||||
```markdown
|
||||
# Application Directory
|
||||
|
||||
This directory contains the core application code following MVC pattern:
|
||||
- **Models**: Database models and business entities
|
||||
- **Controllers**: Handle HTTP requests and responses
|
||||
- **Services**: Business logic and application services
|
||||
- **Console**: CLI commands for maintenance and operations
|
||||
|
||||
## Key Components
|
||||
- **Http**: Web and API controllers, middleware, requests
|
||||
- **Models**: Database models using Nette Database
|
||||
- **Services**: Reusable business logic
|
||||
- **Events**: Application events and listeners
|
||||
```
|
||||
|
||||
#### `/app/Models/Tracker/README.md`
|
||||
```markdown
|
||||
# Tracker Models
|
||||
|
||||
Database models for BitTorrent tracker functionality:
|
||||
- `Torrent`: Torrent information and metadata
|
||||
- `Peer`: Active peers in swarms
|
||||
- `Announce`: Announce history and statistics
|
||||
|
||||
Example:
|
||||
```php
|
||||
class Torrent extends Model
|
||||
{
|
||||
protected string $table = 'bb_torrents';
|
||||
|
||||
public function getPeers(): array
|
||||
{
|
||||
return $this->db->table('bb_peers')
|
||||
->where('torrent_id', $this->id)
|
||||
->fetchAll();
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return User::find($this->user_id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `/app/Services/Tracker/README.md`
|
||||
```markdown
|
||||
# Tracker Services
|
||||
|
||||
Business logic for tracker operations:
|
||||
- `AnnounceService`: Handle peer announces
|
||||
- `ScrapeService`: Provide torrent statistics
|
||||
- `TorrentService`: Torrent management operations
|
||||
|
||||
Example:
|
||||
```php
|
||||
class AnnounceService
|
||||
{
|
||||
public function __construct(
|
||||
private TorrentRepository $torrents,
|
||||
private PeerRepository $peers
|
||||
) {}
|
||||
|
||||
public function handleAnnounce(string $infoHash, array $data): array
|
||||
{
|
||||
$torrent = $this->torrents->findByInfoHash($infoHash);
|
||||
$peers = $this->peers->getActivePeers($torrent->id);
|
||||
|
||||
return ['peers' => $peers, 'interval' => 900];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Controllers READMEs
|
||||
|
||||
#### `/app/Http/Controllers/README.md`
|
||||
```markdown
|
||||
# Controllers
|
||||
|
||||
HTTP controllers following RESTful conventions:
|
||||
- Accept HTTP requests
|
||||
- Validate input
|
||||
- Call services for business logic
|
||||
- Return appropriate responses
|
||||
|
||||
Controllers should be thin - delegate business logic to services.
|
||||
```
|
||||
|
||||
#### `/app/Http/Controllers/Web/TrackerController.php`
|
||||
```markdown
|
||||
# Tracker Web Controller
|
||||
|
||||
Handles web interface for tracker functionality:
|
||||
|
||||
Example:
|
||||
```php
|
||||
class TrackerController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private TorrentService $torrentService
|
||||
) {}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$torrents = $this->torrentService->paginate(
|
||||
$request->get('page', 1),
|
||||
$request->get('category')
|
||||
);
|
||||
|
||||
return view('tracker.index', compact('torrents'));
|
||||
}
|
||||
|
||||
public function store(StoreTorrentRequest $request)
|
||||
{
|
||||
$torrent = $this->torrentService->create(
|
||||
$request->validated(),
|
||||
$request->user()
|
||||
);
|
||||
|
||||
return redirect()->route('torrents.show', $torrent);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Services Layer READMEs
|
||||
|
||||
#### `/app/Services/README.md`
|
||||
```markdown
|
||||
# Services
|
||||
|
||||
Reusable business logic organized by feature:
|
||||
- Encapsulate complex operations
|
||||
- Coordinate between models
|
||||
- Handle external integrations
|
||||
- Maintain single responsibility
|
||||
|
||||
Services are injected into controllers and commands.
|
||||
```
|
||||
|
||||
#### `/app/Repositories/README.md`
|
||||
```markdown
|
||||
# Repositories (Optional)
|
||||
|
||||
Data access layer for complex queries:
|
||||
- Abstracts database queries from models
|
||||
- Implements caching strategies
|
||||
- Handles query optimization
|
||||
|
||||
Example:
|
||||
```php
|
||||
class TorrentRepository
|
||||
{
|
||||
public function __construct(
|
||||
private Database $db,
|
||||
private CacheManager $cache
|
||||
) {}
|
||||
|
||||
public function findByInfoHash(string $infoHash): ?Torrent
|
||||
{
|
||||
return $this->cache->remember("torrent:{$infoHash}", 3600, function() use ($infoHash) {
|
||||
$data = $this->db->table('bb_torrents')
|
||||
->where('info_hash', $infoHash)
|
||||
->fetch();
|
||||
|
||||
return $data ? new Torrent($data) : null;
|
||||
});
|
||||
}
|
||||
|
||||
public function getPopularTorrents(int $limit = 10): array
|
||||
{
|
||||
return $this->db->table('bb_torrents')
|
||||
->select('torrents.*, COUNT(peers.id) as peer_count')
|
||||
->leftJoin('bb_peers', 'peers.torrent_id = torrents.id')
|
||||
->groupBy('torrents.id')
|
||||
->orderBy('peer_count DESC')
|
||||
->limit($limit)
|
||||
->fetchAll();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Views READMEs
|
||||
|
||||
#### `/resources/views/README.md`
|
||||
```markdown
|
||||
# Views
|
||||
|
||||
Template files for rendering HTML:
|
||||
- Layouts for consistent structure
|
||||
- Partials for reusable components
|
||||
- Feature-specific views
|
||||
- Email templates
|
||||
|
||||
Using PHP templates with simple helper functions.
|
||||
```
|
||||
|
||||
#### `/app/Http/Controllers/Api/README.md`
|
||||
```markdown
|
||||
# API Controllers
|
||||
|
||||
RESTful API endpoints:
|
||||
- JSON request/response format
|
||||
- Proper HTTP status codes
|
||||
- API versioning support
|
||||
- Rate limiting aware
|
||||
|
||||
Example:
|
||||
```php
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private UserService $userService
|
||||
) {}
|
||||
|
||||
public function register(Request $request): JsonResponse
|
||||
{
|
||||
$validatedData = $this->validate($request, [
|
||||
'username' => 'required|unique:users',
|
||||
'email' => 'required|email|unique:users',
|
||||
'password' => 'required|min:8'
|
||||
]);
|
||||
|
||||
$user = $this->userService->register($validatedData);
|
||||
|
||||
return response()->json([
|
||||
'id' => $user->id,
|
||||
'username' => $user->username
|
||||
], 201);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `/app/Http/Controllers/Admin/README.md`
|
||||
```markdown
|
||||
# Admin Panel Controllers
|
||||
|
||||
Administrative interface controllers:
|
||||
- Protected by admin middleware
|
||||
- Activity logging
|
||||
- Bulk operations support
|
||||
- Dashboard and reports
|
||||
|
||||
Example:
|
||||
```php
|
||||
class AdminUserController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private UserService $userService
|
||||
) {
|
||||
$this->middleware('admin');
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = $request->get('search');
|
||||
|
||||
$users = $this->userService->searchUsers($query)
|
||||
->paginate(20);
|
||||
|
||||
return view('admin.users.index', compact('users'));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `/config/README.md`
|
||||
```markdown
|
||||
# Application Configuration
|
||||
|
||||
System configuration files using PHP arrays:
|
||||
|
||||
- **app.php**: Core application settings
|
||||
- Site name, URL, timezone
|
||||
- Debug mode, environment
|
||||
- Feature flags and toggles
|
||||
|
||||
- **database.php**: Database connection settings
|
||||
- Multiple connection definitions
|
||||
- Read/write splitting configuration
|
||||
- Connection pooling settings
|
||||
|
||||
- **cache.php**: Cache driver configurations
|
||||
- Redis, Memcached, file-based settings
|
||||
- TTL defaults per cache type
|
||||
- Cache key prefixes
|
||||
|
||||
- **tracker.php**: BitTorrent tracker settings
|
||||
- Announce intervals
|
||||
- Peer limits
|
||||
- Ratio requirements
|
||||
|
||||
Example database configuration:
|
||||
```php
|
||||
<?php
|
||||
return [
|
||||
'default' => env('DB_CONNECTION', 'mysql'),
|
||||
|
||||
'connections' => [
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', 3306),
|
||||
'database' => env('DB_DATABASE', 'torrentpier'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'options' => [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. **Phase 1: Foundation**
|
||||
- Create directory structure
|
||||
- Set up routing files
|
||||
- Configure service providers
|
||||
- Set up middleware
|
||||
|
||||
2. **Phase 2: Models & Database**
|
||||
- Create database models
|
||||
- Define relationships
|
||||
- Set up migrations
|
||||
- Create seeders
|
||||
|
||||
3. **Phase 3: Services & Business Logic**
|
||||
- Implement service classes
|
||||
- Create repositories (if needed)
|
||||
- Set up events and listeners
|
||||
|
||||
4. **Phase 4: Controllers & Routes**
|
||||
- Create web controllers
|
||||
- Build API endpoints
|
||||
- Set up admin controllers
|
||||
- Define routes
|
||||
|
||||
5. **Phase 5: Views & Frontend**
|
||||
- Create template layouts
|
||||
- Build view components
|
||||
- Set up assets pipeline
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
- Move existing controllers to /app/Http/Controllers/Legacy
|
||||
- Gradually rewrite to new MVC structure
|
||||
- Use service classes to encapsulate business logic
|
||||
- Maintain backward compatibility through routing
|
||||
- Progressive enhancement approach
|
||||
|
||||
## Key Principles
|
||||
|
||||
1. **Simplicity**: Straightforward MVC pattern
|
||||
2. **Convention over Configuration**: Consistent naming and structure
|
||||
3. **Fat Models, Skinny Controllers**: Business logic in models/services
|
||||
4. **Service Layer**: Complex operations in service classes
|
||||
5. **Repository Pattern**: Optional for complex queries
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- **Unit Tests**: Models and services
|
||||
- **Feature Tests**: HTTP endpoints and user flows
|
||||
- **Integration Tests**: Database and external services
|
||||
- **Browser Tests**: Critical user journeys
|
||||
|
||||
## Notes for Developers
|
||||
|
||||
- Keep controllers thin, move logic to services
|
||||
- Use dependency injection for testability
|
||||
- Use Nette Database for data access
|
||||
- Write readable code over clever code
|
||||
- Focus on maintainability
|
331
install.php
331
install.php
|
@ -1,331 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* TorrentPier – Bull-powered BitTorrent tracker engine
|
||||
*
|
||||
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
|
||||
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
|
||||
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
|
||||
*/
|
||||
|
||||
define('BB_ROOT', __DIR__ . DIRECTORY_SEPARATOR);
|
||||
define('BB_PATH', BB_ROOT);
|
||||
|
||||
// Check CLI mode
|
||||
if (PHP_SAPI != 'cli') {
|
||||
die('Please run <code style="background:#222;color:#00e01f;padding:2px 6px;border-radius:3px;">php ' . basename(__FILE__) . '</code> in CLI mode');
|
||||
}
|
||||
|
||||
// Get all constants
|
||||
require_once BB_ROOT . 'library/defines.php';
|
||||
|
||||
// Include CLI functions
|
||||
require INC_DIR . '/functions_cli.php';
|
||||
|
||||
/**
|
||||
* System requirements
|
||||
*/
|
||||
const CHECK_REQUIREMENTS = [
|
||||
'php_min_version' => '8.3.0',
|
||||
'ext_list' => [
|
||||
'json',
|
||||
'curl',
|
||||
'readline',
|
||||
'mysqli',
|
||||
'bcmath',
|
||||
'mbstring',
|
||||
'intl',
|
||||
'xml',
|
||||
'xmlwriter',
|
||||
'zip',
|
||||
'gd'
|
||||
],
|
||||
];
|
||||
|
||||
// Welcoming message
|
||||
out("--- TorrentPier Installer ---\n", 'info');
|
||||
|
||||
// Checking extensions
|
||||
out("- Checking installed extensions...", 'info');
|
||||
|
||||
// [1] Check PHP Version
|
||||
if (!version_compare(PHP_VERSION, CHECK_REQUIREMENTS['php_min_version'], '>=')) {
|
||||
out("- TorrentPier requires PHP version " . CHECK_REQUIREMENTS['php_min_version'] . "+ Your PHP version " . PHP_VERSION, 'warning');
|
||||
}
|
||||
|
||||
// [2] Check installed PHP Extensions on server
|
||||
foreach (CHECK_REQUIREMENTS['ext_list'] as $ext) {
|
||||
if (!extension_loaded($ext)) {
|
||||
out("- ext-$ext not installed. Check out php.ini file", 'error');
|
||||
if (!defined('EXTENSIONS_NOT_INSTALLED')) {
|
||||
define('EXTENSIONS_NOT_INSTALLED', true);
|
||||
}
|
||||
} else {
|
||||
out("- ext-$ext installed!");
|
||||
}
|
||||
}
|
||||
if (!defined('EXTENSIONS_NOT_INSTALLED')) {
|
||||
out("- All extensions are installed!\n", 'success');
|
||||
} else {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check if already installed
|
||||
if (is_file(BB_ROOT . '.env')) {
|
||||
out('- TorrentPier already installed', 'warning');
|
||||
echo 'Are you sure want to re-install TorrentPier? [y/N]: ';
|
||||
if (str_starts_with(mb_strtolower(trim(readline())), 'y')) {
|
||||
out("\n- Re-install process started...", 'info');
|
||||
// environment
|
||||
if (is_file(BB_ROOT . '.env')) {
|
||||
if (unlink(BB_ROOT . '.env')) {
|
||||
out('- Environment file successfully removed!');
|
||||
} else {
|
||||
out('- Cannot remove environment (.env) file. Delete it manually', 'error');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
// composer.phar
|
||||
if (is_file(BB_ROOT . 'composer.phar')) {
|
||||
if (unlink(BB_ROOT . 'composer.phar')) {
|
||||
out("- composer.phar file successfully removed!");
|
||||
} else {
|
||||
out('- Cannot remove composer.phar file. Delete it manually', 'error');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
// composer dir
|
||||
if (is_dir(BB_ROOT . 'vendor')) {
|
||||
removeDir(BB_ROOT . 'vendor', true);
|
||||
if (!is_dir(BB_ROOT . 'vendor')) {
|
||||
out("- Composer directory successfully removed!");
|
||||
} else {
|
||||
out('- Cannot remove Composer directory. Delete it manually', 'error');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
out("- Re-install process completed!\n", 'success');
|
||||
out('- Starting installation...', 'info');
|
||||
} else {
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Applying permissions
|
||||
out("- Applying permissions for folders...", 'info');
|
||||
chmod_r(BB_ROOT . 'data', 0755, 0644);
|
||||
chmod_r(BB_ROOT . 'internal_data', 0755, 0644);
|
||||
chmod_r(BB_ROOT . 'sitemap', 0755, 0644);
|
||||
out("- Permissions successfully applied!\n", 'success');
|
||||
|
||||
// Check composer installation
|
||||
if (!is_file(BB_ROOT . 'vendor/autoload.php')) {
|
||||
out('- Hmm, it seems there are no Composer dependencies', 'info');
|
||||
|
||||
// Downloading composer
|
||||
if (!is_file(BB_ROOT . 'composer.phar')) {
|
||||
out('- Downloading Composer...', 'info');
|
||||
if (copy('https://getcomposer.org/installer', BB_ROOT . 'composer-setup.php')) {
|
||||
out("- Composer successfully downloaded!\n", 'success');
|
||||
runProcess('php ' . BB_ROOT . 'composer-setup.php --install-dir=' . BB_ROOT);
|
||||
} else {
|
||||
out('- Cannot download Composer. Please, download it (composer.phar) manually', 'error');
|
||||
exit;
|
||||
}
|
||||
if (is_file(BB_ROOT . 'composer-setup.php')) {
|
||||
if (unlink(BB_ROOT . 'composer-setup.php')) {
|
||||
out("- Composer installation file successfully removed!\n", 'success');
|
||||
} else {
|
||||
out('- Cannot remove Composer installation file (composer-setup.php). Please, delete it manually', 'warning');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out("- composer.phar file found!\n", 'success');
|
||||
}
|
||||
|
||||
// Installing dependencies
|
||||
if (is_file(BB_ROOT . 'composer.phar')) {
|
||||
out('- Installing dependencies...', 'info');
|
||||
|
||||
runProcess('php ' . BB_ROOT . 'composer.phar install --no-interaction --no-ansi');
|
||||
define('COMPOSER_COMPLETED', true);
|
||||
} else {
|
||||
out('- composer.phar not found. Please, download it (composer.phar) manually', 'error');
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
out('- Composer dependencies are present!', 'success');
|
||||
out("- Note: Remove 'vendor' folder if you want to re-install dependencies\n");
|
||||
}
|
||||
|
||||
// Check composer dependencies
|
||||
if (defined('COMPOSER_COMPLETED')) {
|
||||
if (is_file(BB_ROOT . 'vendor/autoload.php')) {
|
||||
out("- Completed! Composer dependencies are installed!\n", 'success');
|
||||
} else {
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Preparing ENV
|
||||
if (is_file(BB_ROOT . '.env.example') && !is_file(BB_ROOT . '.env')) {
|
||||
if (copy(BB_ROOT . '.env.example', BB_ROOT . '.env')) {
|
||||
out("- Environment file created!\n", 'success');
|
||||
} else {
|
||||
out('- Cannot create environment file', 'error');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Editing ENV file
|
||||
$DB_HOST = 'localhost';
|
||||
$DB_PORT = 3306;
|
||||
$DB_DATABASE = '';
|
||||
$DB_USERNAME = '';
|
||||
$DB_PASSWORD = '';
|
||||
|
||||
if (is_file(BB_ROOT . '.env')) {
|
||||
out("--- Configuring TorrentPier ---", 'info');
|
||||
|
||||
$envContent = file_get_contents(BB_ROOT . '.env');
|
||||
if ($envContent === false) {
|
||||
out('- Cannot open environment file', 'error');
|
||||
exit;
|
||||
}
|
||||
$envLines = explode("\n", $envContent);
|
||||
|
||||
$editedLines = [];
|
||||
foreach ($envLines as $line) {
|
||||
if (trim($line) !== '' && !str_starts_with($line, '#')) {
|
||||
$parts = explode('=', $line, 2);
|
||||
$key = trim($parts[0]);
|
||||
$value = (!empty($parts[1]) && $key !== 'DB_PASSWORD') ? trim($parts[1]) : '';
|
||||
|
||||
out("\nCurrent value of $key: $value", 'debug');
|
||||
echo "Enter a new value for $key (or leave empty to not change): ";
|
||||
$newValue = trim(readline());
|
||||
|
||||
if (!empty($newValue) || $key === 'DB_PASSWORD') {
|
||||
if ($key === 'TP_HOST') {
|
||||
if (!preg_match('/^https?:\/\//', $newValue)) {
|
||||
$newValue = 'https://' . $newValue;
|
||||
}
|
||||
$newValue = parse_url($newValue, PHP_URL_HOST);
|
||||
}
|
||||
$line = "$key=$newValue";
|
||||
$$key = $newValue;
|
||||
} else {
|
||||
$$key = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$editedLines[] = $line;
|
||||
}
|
||||
|
||||
$newEnvContent = implode("\n", $editedLines);
|
||||
if (file_put_contents(BB_ROOT . '.env', $newEnvContent)) {
|
||||
out("- TorrentPier successfully configured!\n", 'success');
|
||||
} else {
|
||||
out('- Cannot save environment file', 'error');
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
out('- Environment file not found', 'error');
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!empty($DB_HOST) && !empty($DB_DATABASE) && !empty($DB_USERNAME)) {
|
||||
out("--- Checking environment settings ---\n", 'info');
|
||||
// Connecting to database
|
||||
out("- Trying connect to MySQL...", 'info');
|
||||
|
||||
// Checking mysqli extension installed
|
||||
if (!extension_loaded('mysqli')) {
|
||||
out('- ext-mysqli not found. Check out php.ini file', 'error');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Connect to MySQL server
|
||||
try {
|
||||
$conn = new mysqli($DB_HOST, $DB_USERNAME, $DB_PASSWORD, port: $DB_PORT);
|
||||
} catch (mysqli_sql_exception $exception) {
|
||||
out("- Connection failed: {$exception->getMessage()}", 'error');
|
||||
exit;
|
||||
}
|
||||
if (!$conn->connect_error) {
|
||||
out('- Connected successfully!', 'success');
|
||||
}
|
||||
|
||||
// Creating database if not exist
|
||||
if ($conn->query("CREATE DATABASE IF NOT EXISTS $DB_DATABASE CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")) {
|
||||
out('- Database created successfully!', 'success');
|
||||
} else {
|
||||
out("- Cannot create database: $DB_DATABASE", 'error');
|
||||
exit;
|
||||
}
|
||||
$conn->select_db($DB_DATABASE);
|
||||
|
||||
// Close database connection - migrations will handle their own connections
|
||||
$conn->close();
|
||||
|
||||
// Run database migrations
|
||||
out('- Setting up database using migrations...', 'info');
|
||||
|
||||
// Check if phinx.php exists
|
||||
if (!is_file(BB_ROOT . 'phinx.php')) {
|
||||
out('- Migration configuration (phinx.php) not found', 'error');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Run migrations
|
||||
$migrationResult = runProcess('php vendor/bin/phinx migrate --configuration=' . BB_ROOT . 'phinx.php');
|
||||
if ($migrationResult !== 0) {
|
||||
out('- Database migration failed', 'error');
|
||||
exit;
|
||||
}
|
||||
|
||||
out("- Database setup completed!\n", 'success');
|
||||
|
||||
// Autofill host in robots.txt
|
||||
$robots_txt_file = BB_ROOT . 'robots.txt';
|
||||
if (isset($TP_HOST) && is_file($robots_txt_file)) {
|
||||
$content = file_get_contents($robots_txt_file);
|
||||
$content = str_replace('example.com', $TP_HOST, $content);
|
||||
file_put_contents($robots_txt_file, $content);
|
||||
}
|
||||
|
||||
if (isset($APP_ENV) && $APP_ENV === 'local') {
|
||||
if (!is_file(BB_ROOT . 'library/config.local.php')) {
|
||||
if (copy(BB_ROOT . 'library/config.php', BB_ROOT . 'library/config.local.php')) {
|
||||
out('- Local configuration file created!', 'success');
|
||||
} else {
|
||||
out('- Cannot create library/config.local.php file. You can create it manually, just copy config.php and rename it to config.local.php', 'warning');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (rename(__FILE__, __FILE__ . '_' . hash('xxh128', time()))) {
|
||||
out("- Installation file renamed!", 'success');
|
||||
} else {
|
||||
out('- Cannot rename installation file (' . __FILE__ . '). Please, rename it manually for security reasons', 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup...
|
||||
if (is_file(BB_ROOT . '_cleanup.php')) {
|
||||
out("\n--- Finishing installation (Cleanup) ---\n", 'info');
|
||||
out('The cleanup process will remove:');
|
||||
out('- Development documentation (README, CHANGELOG)', 'debug');
|
||||
out('- Git configuration files', 'debug');
|
||||
out('- CI/CD pipelines and code analysis tools', 'debug');
|
||||
out('- Translation and contribution guidelines', 'debug');
|
||||
echo 'Do you want to delete these files permanently? [y/N]: ';
|
||||
if (str_starts_with(mb_strtolower(trim(readline())), 'y')) {
|
||||
out("\n- Cleanup...", 'info');
|
||||
require_once BB_ROOT . '_cleanup.php';
|
||||
unlink(BB_ROOT . '_cleanup.php');
|
||||
} else {
|
||||
out('- Skipping...', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
out("\n- Voila! Good luck & have fun!", 'success');
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
# Example Caddy configuration for TorrentPier
|
||||
|
||||
example.com {
|
||||
root * /path/to/root
|
||||
encode gzip zstd
|
||||
php_fastcgi unix//run/php/php-fpm.sock
|
||||
try_files {path} {path}/ /index.php?{query}
|
||||
file_server
|
||||
|
||||
@blocked {
|
||||
path /install/* /internal_data/* /library/*
|
||||
path /.ht* /.en*
|
||||
path /.git/*
|
||||
path *.sql *.tpl *.db *.inc *.log *.md
|
||||
}
|
||||
respond @blocked 404
|
||||
|
||||
redir /sitemap.xml /sitemap/sitemap.xml
|
||||
|
||||
@html_css_js {
|
||||
path *.html *.css *.js *.json *.xml *.txt
|
||||
}
|
||||
header @html_css_js Content-Type "{mime}; charset=utf-8"
|
||||
}
|
||||
|
||||
# Refer to the Caddy docs for more information:
|
||||
# https://caddyserver.com/docs/caddyfile
|
|
@ -1,39 +0,0 @@
|
|||
# Example nginx configuration for TorrentPier
|
||||
|
||||
server {
|
||||
listen 80; # port
|
||||
server_name example.com; # your domain
|
||||
root /path/to/root; # folder with TorrentPier installed
|
||||
index index.php;
|
||||
charset utf-8;
|
||||
|
||||
location / {
|
||||
try_files \$uri \$uri/ /index.php?\$args;
|
||||
}
|
||||
|
||||
location ~ \/(install|internal_data|library)\/ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location ~ /\.(ht|en) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location ~ /\.git {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location ~ \.(.*sql|tpl|db|inc|log|md)$ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
rewrite ^/sitemap.xml$ /sitemap/sitemap.xml;
|
||||
|
||||
location ~ \.php$ {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/run/php/php-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ if (!defined('IN_AJAX')) {
|
|||
|
||||
global $userdata, $lang;
|
||||
|
||||
if (!config()->get('callseed')) {
|
||||
if (!tp_config()->get('callseed')) {
|
||||
$this->ajax_die($lang['MODULE_OFF']);
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ if ($t_data['seeders'] >= 3) {
|
|||
} elseif ($t_data['call_seed_time'] >= (TIMENOW - 86400)) {
|
||||
$time_left = delta_time($t_data['call_seed_time'] + 86400, TIMENOW, 'days');
|
||||
$this->ajax_die(sprintf($lang['CALLSEED_MSG_SPAM'], $time_left));
|
||||
} elseif (isset(config()->get('tor_no_tor_act')[$t_data['tor_status']])) {
|
||||
} elseif (isset(tp_config()->get('tor_no_tor_act')[$t_data['tor_status']])) {
|
||||
$this->ajax_die($lang['NOT_AVAILABLE']);
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue