15
.gitignore
vendido
Archivo normal
15
.gitignore
vendido
Archivo normal
@@ -0,0 +1,15 @@
|
|||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
config.php
|
||||||
|
packed/*.php
|
||||||
|
!packed/.gitkeep
|
||||||
|
vendor/
|
||||||
|
node_modules/
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
128
CHANGELOG.md
Archivo normal
128
CHANGELOG.md
Archivo normal
@@ -0,0 +1,128 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to AleShell2 will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.0.0] - 2024-01-XX
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Core Framework**
|
||||||
|
- Modern MVC architecture with PSR-4 style organization
|
||||||
|
- Request/Response handling with JSON and file download support
|
||||||
|
- Session management with timeout and regeneration
|
||||||
|
- CSRF protection on all forms
|
||||||
|
- Rate limiting for login attempts
|
||||||
|
- IP whitelist/blacklist support
|
||||||
|
|
||||||
|
- **Authentication**
|
||||||
|
- Secure password hashing with bcrypt
|
||||||
|
- Session-based authentication
|
||||||
|
- Automatic logout on inactivity
|
||||||
|
- Brute-force protection with lockout
|
||||||
|
|
||||||
|
- **File Manager**
|
||||||
|
- Browse directories with icons
|
||||||
|
- Create/edit/delete files and folders
|
||||||
|
- Upload multiple files
|
||||||
|
- Download files
|
||||||
|
- Copy/move operations
|
||||||
|
- Permission management (chmod)
|
||||||
|
- File search
|
||||||
|
- Syntax-aware icons for file types
|
||||||
|
|
||||||
|
- **Terminal**
|
||||||
|
- Full command execution
|
||||||
|
- Working directory tracking
|
||||||
|
- Command history (arrow keys)
|
||||||
|
- Built-in commands: cd, pwd, clear, help
|
||||||
|
- Quick command buttons
|
||||||
|
- Keyboard shortcuts
|
||||||
|
|
||||||
|
- **Code Editor**
|
||||||
|
- Open and edit any text file
|
||||||
|
- Tab indentation support
|
||||||
|
- Save with Ctrl+S
|
||||||
|
- Line/column tracking
|
||||||
|
- Save As functionality
|
||||||
|
- Direct download
|
||||||
|
|
||||||
|
- **Process Manager**
|
||||||
|
- List all running processes
|
||||||
|
- CPU and memory usage display
|
||||||
|
- Kill processes with signal selection
|
||||||
|
- Process filtering
|
||||||
|
- Auto-refresh
|
||||||
|
|
||||||
|
- **Network Tools**
|
||||||
|
- View network connections
|
||||||
|
- Ping hosts
|
||||||
|
- Traceroute
|
||||||
|
- Port scanner (up to 100 ports)
|
||||||
|
- Common port identification
|
||||||
|
|
||||||
|
- **Database Manager**
|
||||||
|
- MySQL/MariaDB support
|
||||||
|
- PostgreSQL support
|
||||||
|
- SQLite support
|
||||||
|
- Browse databases and tables
|
||||||
|
- Execute SQL queries
|
||||||
|
- Export to SQL/CSV
|
||||||
|
- Query results with pagination
|
||||||
|
|
||||||
|
- **System Information**
|
||||||
|
- Server details (hostname, OS, uptime)
|
||||||
|
- Hardware info (CPU, memory, disk)
|
||||||
|
- PHP configuration
|
||||||
|
- Loaded extensions
|
||||||
|
- Environment variables
|
||||||
|
- Disabled functions display
|
||||||
|
|
||||||
|
- **API Endpoints**
|
||||||
|
- Status endpoint
|
||||||
|
- CSRF token endpoint
|
||||||
|
- Health check
|
||||||
|
- Quick command execution
|
||||||
|
- PHP eval endpoint
|
||||||
|
- File upload/download API
|
||||||
|
|
||||||
|
- **Packer**
|
||||||
|
- Single-file generation
|
||||||
|
- Custom password support
|
||||||
|
- Minification option
|
||||||
|
- All views embedded
|
||||||
|
- No external dependencies
|
||||||
|
|
||||||
|
- **UI/UX**
|
||||||
|
- Dark theme (GitHub-inspired)
|
||||||
|
- Responsive design
|
||||||
|
- Keyboard shortcuts
|
||||||
|
- Toast notifications
|
||||||
|
- Modal dialogs
|
||||||
|
- Context menus
|
||||||
|
- Loading indicators
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Password stored as bcrypt hash
|
||||||
|
- Session tokens regenerated on login
|
||||||
|
- CSRF tokens required for state-changing operations
|
||||||
|
- Input sanitization throughout
|
||||||
|
- Output escaping (XSS prevention)
|
||||||
|
- Security headers (X-Frame-Options, X-XSS-Protection, etc.)
|
||||||
|
- IP restriction support
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Planned
|
||||||
|
|
||||||
|
- Two-factor authentication
|
||||||
|
- Audit logging
|
||||||
|
- File encryption
|
||||||
|
- SSH tunnel support
|
||||||
|
- Cron job manager
|
||||||
|
- Log viewer
|
||||||
|
- Backup manager
|
||||||
|
- Plugin system
|
||||||
33
LICENSE
Archivo normal
33
LICENSE
Archivo normal
@@ -0,0 +1,33 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 AleShell Team
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
|
||||||
|
This software is provided for educational and legitimate administrative purposes
|
||||||
|
only. The authors do not condone or support any illegal activities. Users are
|
||||||
|
solely responsible for ensuring their use of this software complies with all
|
||||||
|
applicable laws and regulations.
|
||||||
|
|
||||||
|
Unauthorized access to computer systems is illegal and punishable by law. Always
|
||||||
|
obtain proper authorization before using this tool on any system.
|
||||||
191
PACKER_GUIDE.md
Archivo normal
191
PACKER_GUIDE.md
Archivo normal
@@ -0,0 +1,191 @@
|
|||||||
|
# AleShell2 Packer Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The AleShell2 packer (`pack.php`) combines all source files into a single, self-contained PHP file that can be deployed anywhere without additional dependencies.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic usage - creates packed/aleshell.php with default password "aleshell"
|
||||||
|
php pack.php
|
||||||
|
|
||||||
|
# Custom output file and password
|
||||||
|
php pack.php --output=/path/to/webshell.php --password=mysecretpassword
|
||||||
|
|
||||||
|
# Minified output (smaller file size)
|
||||||
|
php pack.php --minify
|
||||||
|
|
||||||
|
# Show help
|
||||||
|
php pack.php --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
| Option | Description | Default |
|
||||||
|
|--------|-------------|---------|
|
||||||
|
| `--output=<file>` | Output file path | `packed/aleshell.php` |
|
||||||
|
| `--password=<pass>` | Login password | `aleshell` |
|
||||||
|
| `--minify` | Remove comments and extra whitespace | disabled |
|
||||||
|
| `--help` | Show help message | - |
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Basic Pack
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php pack.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates `packed/aleshell.php` with password `aleshell`.
|
||||||
|
|
||||||
|
### Custom Password
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php pack.php --password=MySecureP@ssw0rd!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Output Location
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php pack.php --output=../public/admin.php --password=admin123
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Build (Minified)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php pack.php --minify --output=dist/shell.php --password=prod_password
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
1. **Pack the application:**
|
||||||
|
```bash
|
||||||
|
php pack.php --password=your_secure_password
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Upload to server:**
|
||||||
|
Upload `packed/aleshell.php` to your web server via FTP, SCP, or your preferred method.
|
||||||
|
|
||||||
|
3. **Set permissions:**
|
||||||
|
```bash
|
||||||
|
chmod 644 aleshell.php
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Access via browser:**
|
||||||
|
Navigate to `https://your-server.com/path/to/aleshell.php`
|
||||||
|
|
||||||
|
5. **Login:**
|
||||||
|
Use the password you set during packing.
|
||||||
|
|
||||||
|
## Security Recommendations
|
||||||
|
|
||||||
|
### Strong Password
|
||||||
|
|
||||||
|
Always use a strong, unique password:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php pack.php --password="$(openssl rand -base64 32)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rename the File
|
||||||
|
|
||||||
|
Use a non-obvious filename:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php pack.php --output=./admin_tools_$(date +%s).php
|
||||||
|
```
|
||||||
|
|
||||||
|
### IP Restrictions
|
||||||
|
|
||||||
|
Edit the packed file to add IP restrictions:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$ALESHELL_CONFIG = [
|
||||||
|
// ... other config ...
|
||||||
|
'ip_whitelist' => ['192.168.1.100', '10.0.0.50'],
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete After Use
|
||||||
|
|
||||||
|
Remove the webshell when no longer needed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm aleshell.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## What Gets Packed
|
||||||
|
|
||||||
|
The packer combines:
|
||||||
|
|
||||||
|
- Core framework classes (Application, Router, Request, Response, View)
|
||||||
|
- Security classes (Session, Auth)
|
||||||
|
- All module controllers:
|
||||||
|
- Dashboard
|
||||||
|
- File Manager
|
||||||
|
- Terminal
|
||||||
|
- Code Editor
|
||||||
|
- Process Manager
|
||||||
|
- Network Tools
|
||||||
|
- Database Manager
|
||||||
|
- System Info
|
||||||
|
- API endpoints
|
||||||
|
- All view templates (embedded as strings)
|
||||||
|
- CSS styles (embedded in layout)
|
||||||
|
- JavaScript (embedded in views)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Class not found" errors
|
||||||
|
|
||||||
|
Ensure all source files exist before packing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -la src/Core/
|
||||||
|
ls -la src/Security/
|
||||||
|
ls -la src/Modules/
|
||||||
|
ls -la src/Views/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Large file size
|
||||||
|
|
||||||
|
Use the `--minify` option:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php pack.php --minify
|
||||||
|
```
|
||||||
|
|
||||||
|
### View rendering issues
|
||||||
|
|
||||||
|
Views are stored as PHP strings and evaluated with `eval()`. Ensure view files have valid PHP syntax.
|
||||||
|
|
||||||
|
### Password not working
|
||||||
|
|
||||||
|
The password is hashed during packing. You cannot recover it from the packed file. Re-pack with a known password:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php pack.php --password=newpassword
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Class Renaming
|
||||||
|
|
||||||
|
To avoid namespace conflicts, classes are renamed during packing:
|
||||||
|
|
||||||
|
- `AleShell2\Core\Request` → `AleShell2_Request`
|
||||||
|
- `AleShell2\Security\Auth` → `AleShell2_Auth`
|
||||||
|
- etc.
|
||||||
|
|
||||||
|
### View Storage
|
||||||
|
|
||||||
|
Views are stored in the `$ALESHELL_VIEWS` global array and rendered via `eval()`.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Configuration is stored in the `$ALESHELL_CONFIG` global array at the top of the packed file.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - See LICENSE file for details.
|
||||||
326
README.md
Archivo normal
326
README.md
Archivo normal
@@ -0,0 +1,326 @@
|
|||||||
|
# 🚀 AleShell2 - Modern PHP Web Shell
|
||||||
|
|
||||||
|
[](https://php.net)
|
||||||
|
[](LICENSE)
|
||||||
|
[](CHANGELOG.md)
|
||||||
|
|
||||||
|
AleShell2 is a powerful, secure, and modern web shell built with PHP. It's designed to be deployed as a **single monolithic PHP file** while maintaining a clean, modular architecture during development.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
### 🔐 Security
|
||||||
|
- **Password Authentication** with secure hashing (bcrypt)
|
||||||
|
- **Session Management** with timeout protection
|
||||||
|
- **CSRF Protection** for all state-changing operations
|
||||||
|
- **Rate Limiting** to prevent brute force attacks
|
||||||
|
- **IP Whitelisting/Blacklisting** for access control
|
||||||
|
- **Path Traversal Protection** to prevent unauthorized file access
|
||||||
|
- **Command Filtering** for dangerous system commands
|
||||||
|
- **Self-destruct Mode** after N accesses (optional)
|
||||||
|
|
||||||
|
### 🎨 Modern Interface
|
||||||
|
- **Responsive Design** works on desktop, tablet, and mobile
|
||||||
|
- **Dark/Light Theme** toggle with system preference detection
|
||||||
|
- **Multiple Color Themes** (Dark, Light, Matrix, Ocean, etc.)
|
||||||
|
- **Keyboard Shortcuts** for power users
|
||||||
|
- **Real-time Updates** for system information
|
||||||
|
- **Smooth Animations** and transitions
|
||||||
|
- **Single Page Application** experience
|
||||||
|
|
||||||
|
### 📁 File Manager
|
||||||
|
- **Complete File Operations** (create, read, update, delete, copy, move)
|
||||||
|
- **Drag & Drop Upload** with progress indicators
|
||||||
|
- **Syntax Highlighting** for code files
|
||||||
|
- **File Permissions** management (chmod)
|
||||||
|
- **Archive Support** (zip, tar, tar.gz)
|
||||||
|
- **Large File Handling** with streaming
|
||||||
|
- **File Search** and filtering capabilities
|
||||||
|
- **Breadcrumb Navigation**
|
||||||
|
|
||||||
|
### 💻 Terminal
|
||||||
|
- **Interactive Terminal** with command history
|
||||||
|
- **Built-in Commands** (cd, pwd, clear, help, etc.)
|
||||||
|
- **Command Auto-completion**
|
||||||
|
- **Output Streaming** for long-running commands
|
||||||
|
- **Multiple Terminal Tabs**
|
||||||
|
- **Configurable Timeout** for command execution
|
||||||
|
- **Color-coded Output**
|
||||||
|
|
||||||
|
### 📝 Code Editor
|
||||||
|
- **Syntax Highlighting** for 20+ languages
|
||||||
|
- **Line Numbers** and code folding
|
||||||
|
- **Find & Replace** functionality
|
||||||
|
- **Auto-indentation** and code formatting
|
||||||
|
- **Multiple Editor Themes**
|
||||||
|
- **File Type Detection**
|
||||||
|
- **Unsaved Changes Warning**
|
||||||
|
|
||||||
|
### ⚡ System Monitoring (Dashboard)
|
||||||
|
- **Real-time System Stats** (CPU, Memory, Disk, Network)
|
||||||
|
- **Process Manager** with kill capabilities
|
||||||
|
- **System Load Average** monitoring
|
||||||
|
- **PHP Information** display
|
||||||
|
- **Server Time** display
|
||||||
|
|
||||||
|
### 🔧 Process Manager
|
||||||
|
- **List All Processes** with details
|
||||||
|
- **Search/Filter Processes**
|
||||||
|
- **Kill Processes** (single or batch)
|
||||||
|
- **CPU & Memory Usage** per process
|
||||||
|
- **Auto-refresh** capability
|
||||||
|
|
||||||
|
### 🌐 Network Tools
|
||||||
|
- **Active Connections** list (netstat)
|
||||||
|
- **Ping** utility
|
||||||
|
- **Traceroute** utility
|
||||||
|
- **Port Scanner** (basic)
|
||||||
|
- **DNS Lookup**
|
||||||
|
- **Interface Information**
|
||||||
|
|
||||||
|
### 🗄️ Database Tools
|
||||||
|
- **Multi-Database Support** (MySQL, PostgreSQL, SQLite)
|
||||||
|
- **SQL Query Execution** with result formatting
|
||||||
|
- **Database Browser** with table structure
|
||||||
|
- **Export/Import** capabilities (SQL dump)
|
||||||
|
- **Connection Management**
|
||||||
|
- **Query History**
|
||||||
|
|
||||||
|
## 🚀 Installation
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
- PHP 8.0 or higher
|
||||||
|
- Web server (Apache, Nginx, LiteSpeed, etc.)
|
||||||
|
- PHP extensions: json, mbstring, openssl (optional: pdo, mysqli, pgsql)
|
||||||
|
|
||||||
|
### Quick Install (Packed Version)
|
||||||
|
1. Generate a packed version using `pack.php`
|
||||||
|
2. Upload the single `aleshell.php` file to your server
|
||||||
|
3. Access via web browser
|
||||||
|
4. Default password: `aleshell`
|
||||||
|
|
||||||
|
### From Source (Development)
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourusername/aleshell2.git
|
||||||
|
cd aleshell2
|
||||||
|
|
||||||
|
# Copy configuration
|
||||||
|
cp src/Config/config.example.php src/Config/config.php
|
||||||
|
|
||||||
|
# Edit configuration
|
||||||
|
nano src/Config/config.php
|
||||||
|
|
||||||
|
# Access index.php via your web server
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 Generating Packed Version
|
||||||
|
|
||||||
|
AleShell2 can be packed into a single PHP file for easy deployment:
|
||||||
|
|
||||||
|
### Web Interface
|
||||||
|
```bash
|
||||||
|
# Access pack.php in your browser
|
||||||
|
http://your-server/aleshell2/pack.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Line
|
||||||
|
```bash
|
||||||
|
# Basic packed version
|
||||||
|
php pack.php --output=shell.php --password=your_password
|
||||||
|
|
||||||
|
# Full options
|
||||||
|
php pack.php \
|
||||||
|
--output=shell.php \
|
||||||
|
--password=secure_pass \
|
||||||
|
--encrypt \
|
||||||
|
--minify \
|
||||||
|
--obfuscate \
|
||||||
|
--theme=dark \
|
||||||
|
--modules=files,terminal,editor,processes,network,database
|
||||||
|
```
|
||||||
|
|
||||||
|
### Packer Options
|
||||||
|
|
||||||
|
| Option | Description | Default |
|
||||||
|
|--------|-------------|---------|
|
||||||
|
| `--output` | Output filename | `aleshell.php` |
|
||||||
|
| `--password` | Access password | `aleshell` |
|
||||||
|
| `--theme` | Default theme | `dark` |
|
||||||
|
| `--modules` | Modules to include | all |
|
||||||
|
| `--encrypt` | Encrypt with base64+compression | false |
|
||||||
|
| `--minify` | Minify code | false |
|
||||||
|
| `--obfuscate` | Obfuscate variable names | false |
|
||||||
|
| `--compression` | Compression type | `gzdeflate` |
|
||||||
|
| `--allowed-ips` | IP whitelist | empty |
|
||||||
|
| `--self-destruct` | Delete after N accesses | disabled |
|
||||||
|
|
||||||
|
## ⚙️ Configuration
|
||||||
|
|
||||||
|
Edit `src/Config/config.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
return [
|
||||||
|
// Application settings
|
||||||
|
'app' => [
|
||||||
|
'name' => 'AleShell2',
|
||||||
|
'version' => '2.0.0',
|
||||||
|
'debug' => false,
|
||||||
|
'timezone' => 'UTC',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Security settings
|
||||||
|
'security' => [
|
||||||
|
'password' => password_hash('your_password', PASSWORD_BCRYPT),
|
||||||
|
'session_timeout' => 3600,
|
||||||
|
'max_attempts' => 5,
|
||||||
|
'lockout_time' => 300,
|
||||||
|
'csrf_protection' => true,
|
||||||
|
'allowed_ips' => [],
|
||||||
|
'blocked_ips' => [],
|
||||||
|
],
|
||||||
|
|
||||||
|
// Feature toggles
|
||||||
|
'features' => [
|
||||||
|
'file_manager' => true,
|
||||||
|
'terminal' => true,
|
||||||
|
'code_editor' => true,
|
||||||
|
'process_manager' => true,
|
||||||
|
'network_tools' => true,
|
||||||
|
'database_tools' => true,
|
||||||
|
'system_info' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
// UI settings
|
||||||
|
'ui' => [
|
||||||
|
'theme' => 'dark',
|
||||||
|
'language' => 'en',
|
||||||
|
'items_per_page' => 50,
|
||||||
|
],
|
||||||
|
|
||||||
|
// Limits
|
||||||
|
'limits' => [
|
||||||
|
'max_file_size' => 50 * 1024 * 1024,
|
||||||
|
'max_upload_size' => 100 * 1024 * 1024,
|
||||||
|
'command_timeout' => 30,
|
||||||
|
'max_history' => 100,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 Security Considerations
|
||||||
|
|
||||||
|
⚠️ **WARNING**: This tool provides full system access. Use responsibly!
|
||||||
|
|
||||||
|
1. **Always change the default password** immediately
|
||||||
|
2. **Use HTTPS** in production environments
|
||||||
|
3. **Restrict access** using IP whitelisting when possible
|
||||||
|
4. **Use self-destruct** for temporary access
|
||||||
|
5. **Delete the file** when not needed
|
||||||
|
6. **Monitor access logs** for suspicious activity
|
||||||
|
7. **Keep PHP updated** to the latest stable version
|
||||||
|
|
||||||
|
## 🎯 Keyboard Shortcuts
|
||||||
|
|
||||||
|
| Shortcut | Action |
|
||||||
|
|----------|--------|
|
||||||
|
| `Ctrl+1` | Dashboard |
|
||||||
|
| `Ctrl+2` | File Manager |
|
||||||
|
| `Ctrl+3` | Terminal |
|
||||||
|
| `Ctrl+4` | Code Editor |
|
||||||
|
| `Ctrl+5` | Processes |
|
||||||
|
| `Ctrl+6` | Network |
|
||||||
|
| `Ctrl+7` | Database |
|
||||||
|
| `Ctrl+L` | Clear terminal |
|
||||||
|
| `Ctrl+S` | Save file (in editor) |
|
||||||
|
| `Escape` | Close modal |
|
||||||
|
|
||||||
|
## 🌐 Browser Support
|
||||||
|
|
||||||
|
- Chrome 80+
|
||||||
|
- Firefox 75+
|
||||||
|
- Safari 13+
|
||||||
|
- Edge 80+
|
||||||
|
- Opera 67+
|
||||||
|
|
||||||
|
## 📱 Mobile Support
|
||||||
|
|
||||||
|
The interface is fully responsive with:
|
||||||
|
- Touch-friendly controls
|
||||||
|
- Swipe navigation
|
||||||
|
- Responsive layouts
|
||||||
|
- Mobile-optimized terminal
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
aleshell2/
|
||||||
|
├── index.php # Entry point
|
||||||
|
├── pack.php # Packer utility
|
||||||
|
├── README.md # Documentation
|
||||||
|
├── CHANGELOG.md # Version history
|
||||||
|
├── LICENSE # MIT License
|
||||||
|
├── src/
|
||||||
|
│ ├── Config/
|
||||||
|
│ │ ├── config.example.php
|
||||||
|
│ │ └── config.php
|
||||||
|
│ ├── Core/
|
||||||
|
│ │ ├── Application.php
|
||||||
|
│ │ ├── Router.php
|
||||||
|
│ │ ├── Request.php
|
||||||
|
│ │ ├── Response.php
|
||||||
|
│ │ └── View.php
|
||||||
|
│ ├── Security/
|
||||||
|
│ │ ├── Auth.php
|
||||||
|
│ │ ├── Session.php
|
||||||
|
│ │ └── Csrf.php
|
||||||
|
│ ├── Modules/
|
||||||
|
│ │ ├── Dashboard/
|
||||||
|
│ │ ├── Files/
|
||||||
|
│ │ ├── Terminal/
|
||||||
|
│ │ ├── Editor/
|
||||||
|
│ │ ├── Processes/
|
||||||
|
│ │ ├── Network/
|
||||||
|
│ │ └── Database/
|
||||||
|
│ └── Views/
|
||||||
|
│ ├── layouts/
|
||||||
|
│ ├── components/
|
||||||
|
│ └── modules/
|
||||||
|
└── packed/ # Generated packed files
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Development
|
||||||
|
|
||||||
|
### Adding a New Module
|
||||||
|
|
||||||
|
1. Create directory: `src/Modules/MyModule/`
|
||||||
|
2. Create controller: `MyModuleController.php`
|
||||||
|
3. Create view: `src/Views/modules/mymodule.php`
|
||||||
|
4. Register route in `src/Core/Router.php`
|
||||||
|
5. Add to navigation in `src/Views/layouts/main.php`
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
|
||||||
|
- PSR-12 coding standard
|
||||||
|
- Type hints for parameters and return values
|
||||||
|
- PHPDoc comments for all public methods
|
||||||
|
- Meaningful variable and function names
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
|
- Based on concepts from the original b374k shell
|
||||||
|
- Inspired by modern web development practices
|
||||||
|
- Built with ❤️ for system administrators
|
||||||
|
|
||||||
|
## ⚠️ Disclaimer
|
||||||
|
|
||||||
|
**This tool is intended for legitimate system administration purposes only.**
|
||||||
|
|
||||||
|
Users are responsible for ensuring compliance with applicable laws and regulations. The authors are not responsible for any misuse of this software. Unauthorized access to computer systems is illegal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**AleShell2 v2.0.0** - Modern PHP Web Shell
|
||||||
154
index.php
Archivo normal
154
index.php
Archivo normal
@@ -0,0 +1,154 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 - Modern PHP Web Shell
|
||||||
|
*
|
||||||
|
* A powerful, secure, and modern web shell that can be packed into
|
||||||
|
* a single monolithic PHP file for easy deployment.
|
||||||
|
*
|
||||||
|
* @author AleShell Team
|
||||||
|
* @version 2.0.0
|
||||||
|
* @license MIT
|
||||||
|
* @package AleShell2
|
||||||
|
* @link https://github.com/yourusername/aleshell2
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
// Prevent direct execution without proper context
|
||||||
|
if (php_sapi_name() === 'cli' && basename(__FILE__) === basename($argv[0] ?? '')) {
|
||||||
|
echo "AleShell2 v2.0.0 - Web Shell\n";
|
||||||
|
echo "This file should be accessed via a web server.\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONSTANTS AND CONFIGURATION
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
define('ALESHELL_VERSION', '2.0.0');
|
||||||
|
define('ALESHELL_ROOT', __DIR__);
|
||||||
|
define('ALESHELL_SRC', ALESHELL_ROOT . '/src');
|
||||||
|
define('ALESHELL_START_TIME', microtime(true));
|
||||||
|
define('ALESHELL_PACKED', false);
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ERROR HANDLING
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
ini_set('display_errors', '0');
|
||||||
|
ini_set('log_errors', '1');
|
||||||
|
|
||||||
|
set_error_handler(function ($severity, $message, $file, $line) {
|
||||||
|
if (!(error_reporting() & $severity)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw new ErrorException($message, 0, $severity, $file, $line);
|
||||||
|
});
|
||||||
|
|
||||||
|
set_exception_handler(function (Throwable $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
if (defined('ALESHELL_DEBUG') && ALESHELL_DEBUG) {
|
||||||
|
echo "<h1>Error</h1>";
|
||||||
|
echo "<p><strong>Message:</strong> " . htmlspecialchars($e->getMessage()) . "</p>";
|
||||||
|
echo "<p><strong>File:</strong> " . htmlspecialchars($e->getFile()) . ":" . $e->getLine() . "</p>";
|
||||||
|
echo "<pre>" . htmlspecialchars($e->getTraceAsString()) . "</pre>";
|
||||||
|
} else {
|
||||||
|
echo "<h1>Internal Server Error</h1>";
|
||||||
|
echo "<p>An error occurred. Please try again later.</p>";
|
||||||
|
}
|
||||||
|
exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// AUTOLOADER
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
spl_autoload_register(function (string $class): bool {
|
||||||
|
// Only handle AleShell2 namespace
|
||||||
|
if (strpos($class, 'AleShell2\\') !== 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert namespace to path
|
||||||
|
$relativePath = str_replace('AleShell2\\', '', $class);
|
||||||
|
$relativePath = str_replace('\\', DIRECTORY_SEPARATOR, $relativePath);
|
||||||
|
$filePath = ALESHELL_SRC . DIRECTORY_SEPARATOR . $relativePath . '.php';
|
||||||
|
|
||||||
|
if (file_exists($filePath)) {
|
||||||
|
require_once $filePath;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// SECURITY HEADERS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
header('X-Content-Type-Options: nosniff');
|
||||||
|
header('X-Frame-Options: SAMEORIGIN');
|
||||||
|
header('X-XSS-Protection: 1; mode=block');
|
||||||
|
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||||
|
header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// BOOTSTRAP APPLICATION
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load and run application
|
||||||
|
$app = new AleShell2\Core\Application();
|
||||||
|
$app->run();
|
||||||
|
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// Log error
|
||||||
|
error_log(sprintf(
|
||||||
|
'AleShell2 Error: %s in %s:%d',
|
||||||
|
$e->getMessage(),
|
||||||
|
$e->getFile(),
|
||||||
|
$e->getLine()
|
||||||
|
));
|
||||||
|
|
||||||
|
// Show error page
|
||||||
|
http_response_code(500);
|
||||||
|
echo "<!DOCTYPE html>
|
||||||
|
<html lang='en'>
|
||||||
|
<head>
|
||||||
|
<meta charset='UTF-8'>
|
||||||
|
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||||
|
<title>Error - AleShell2</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #1a202c; color: #f7fafc;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
min-height: 100vh; margin: 0;
|
||||||
|
}
|
||||||
|
.error-box {
|
||||||
|
background: #2d3748; padding: 2rem; border-radius: 12px;
|
||||||
|
max-width: 500px; text-align: center;
|
||||||
|
box-shadow: 0 10px 25px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
h1 { color: #fc8181; margin: 0 0 1rem 0; }
|
||||||
|
p { color: #a0aec0; margin: 0; }
|
||||||
|
code {
|
||||||
|
display: block; margin-top: 1rem; padding: 1rem;
|
||||||
|
background: #1a202c; border-radius: 6px;
|
||||||
|
font-size: 0.875rem; text-align: left;
|
||||||
|
overflow-x: auto; color: #ed8936;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class='error-box'>
|
||||||
|
<h1>⚠️ Application Error</h1>
|
||||||
|
<p>An error occurred while loading AleShell2.</p>" .
|
||||||
|
(defined('ALESHELL_DEBUG') && ALESHELL_DEBUG
|
||||||
|
? "<code>" . htmlspecialchars($e->getMessage()) . "</code>"
|
||||||
|
: "") .
|
||||||
|
"</div>
|
||||||
|
</body>
|
||||||
|
</html>";
|
||||||
|
}
|
||||||
654
pack.php
Archivo normal
654
pack.php
Archivo normal
@@ -0,0 +1,654 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Packer
|
||||||
|
*
|
||||||
|
* This script packs the entire AleShell2 application into a single monolithic PHP file.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* php pack.php [options]
|
||||||
|
*
|
||||||
|
* Options:
|
||||||
|
* --output=<file> Output file path (default: packed/aleshell.php)
|
||||||
|
* --password=<pass> Set custom password (default: aleshell)
|
||||||
|
* --minify Minify the output (remove comments and extra whitespace)
|
||||||
|
* --help Show this help message
|
||||||
|
*
|
||||||
|
* @package AleShell2
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
// Parse command line arguments
|
||||||
|
$options = getopt('', ['output:', 'password:', 'minify', 'help']);
|
||||||
|
|
||||||
|
if (isset($options['help'])) {
|
||||||
|
echo <<<HELP
|
||||||
|
AleShell2 Packer
|
||||||
|
================
|
||||||
|
|
||||||
|
This script packs the entire AleShell2 application into a single monolithic PHP file.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
php pack.php [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--output=<file> Output file path (default: packed/aleshell.php)
|
||||||
|
--password=<pass> Set custom password (default: aleshell)
|
||||||
|
--minify Minify the output (remove comments and extra whitespace)
|
||||||
|
--help Show this help message
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
php pack.php
|
||||||
|
php pack.php --output=../webshell.php --password=mysecret
|
||||||
|
php pack.php --minify
|
||||||
|
|
||||||
|
HELP;
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$outputFile = $options['output'] ?? __DIR__ . '/packed/aleshell.php';
|
||||||
|
$password = $options['password'] ?? 'aleshell';
|
||||||
|
$minify = isset($options['minify']);
|
||||||
|
|
||||||
|
echo "AleShell2 Packer\n";
|
||||||
|
echo "================\n\n";
|
||||||
|
|
||||||
|
// Create output directory if needed
|
||||||
|
$outputDir = dirname($outputFile);
|
||||||
|
if (!is_dir($outputDir)) {
|
||||||
|
mkdir($outputDir, 0755, true);
|
||||||
|
echo "Created directory: {$outputDir}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate password hash
|
||||||
|
$passwordHash = password_hash($password, PASSWORD_BCRYPT);
|
||||||
|
echo "Password: {$password}\n";
|
||||||
|
echo "Hash: {$passwordHash}\n\n";
|
||||||
|
|
||||||
|
// Collect all source files
|
||||||
|
$sources = [];
|
||||||
|
|
||||||
|
// Core files order matters for dependencies
|
||||||
|
$coreFiles = [
|
||||||
|
'src/Core/Request.php',
|
||||||
|
'src/Core/Response.php',
|
||||||
|
'src/Core/View.php',
|
||||||
|
'src/Core/Router.php',
|
||||||
|
'src/Core/Application.php',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Security files
|
||||||
|
$securityFiles = [
|
||||||
|
'src/Security/Session.php',
|
||||||
|
'src/Security/Auth.php',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Module files
|
||||||
|
$moduleFiles = [
|
||||||
|
'src/Modules/BaseController.php',
|
||||||
|
'src/Modules/Auth/AuthController.php',
|
||||||
|
'src/Modules/Dashboard/DashboardController.php',
|
||||||
|
'src/Modules/Files/FilesController.php',
|
||||||
|
'src/Modules/Terminal/TerminalController.php',
|
||||||
|
'src/Modules/Editor/EditorController.php',
|
||||||
|
'src/Modules/Processes/ProcessesController.php',
|
||||||
|
'src/Modules/Network/NetworkController.php',
|
||||||
|
'src/Modules/Database/DatabaseController.php',
|
||||||
|
'src/Modules/System/SystemController.php',
|
||||||
|
'src/Modules/Api/ApiController.php',
|
||||||
|
];
|
||||||
|
|
||||||
|
// View files
|
||||||
|
$viewFiles = [
|
||||||
|
'src/Views/layouts/main.php',
|
||||||
|
'src/Views/auth/login.php',
|
||||||
|
'src/Views/modules/dashboard.php',
|
||||||
|
'src/Views/modules/files.php',
|
||||||
|
'src/Views/modules/terminal.php',
|
||||||
|
'src/Views/modules/editor.php',
|
||||||
|
'src/Views/modules/processes.php',
|
||||||
|
'src/Views/modules/network.php',
|
||||||
|
'src/Views/modules/database.php',
|
||||||
|
'src/Views/modules/system.php',
|
||||||
|
];
|
||||||
|
|
||||||
|
echo "Collecting source files...\n";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract PHP code from file (without opening tag and namespace)
|
||||||
|
*/
|
||||||
|
function extractPhpCode(string $filePath, bool $minify = false): string {
|
||||||
|
$content = file_get_contents($filePath);
|
||||||
|
|
||||||
|
// Remove opening PHP tag
|
||||||
|
$content = preg_replace('/^<\?php\s*/s', '', $content);
|
||||||
|
|
||||||
|
// Remove declare strict types (we'll add it once at the top)
|
||||||
|
$content = preg_replace('/declare\s*\(\s*strict_types\s*=\s*1\s*\)\s*;?\s*/i', '', $content);
|
||||||
|
|
||||||
|
// Remove namespace declarations (we'll inline everything)
|
||||||
|
$content = preg_replace('/namespace\s+[^;]+;\s*/i', '', $content);
|
||||||
|
|
||||||
|
// Remove use statements (we'll use fully qualified names)
|
||||||
|
$content = preg_replace('/use\s+[^;]+;\s*/i', '', $content);
|
||||||
|
|
||||||
|
if ($minify) {
|
||||||
|
// Remove multi-line comments but preserve strings
|
||||||
|
$content = preg_replace('/\/\*(?!.*?\*\/).*?\*\//s', '', $content);
|
||||||
|
|
||||||
|
// Remove single-line comments (be careful with URLs)
|
||||||
|
$content = preg_replace('/(?<!:)\/\/.*$/m', '', $content);
|
||||||
|
|
||||||
|
// Remove extra whitespace
|
||||||
|
$content = preg_replace('/\n\s*\n/', "\n", $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract view content (PHP template)
|
||||||
|
*/
|
||||||
|
function extractViewContent(string $filePath): string {
|
||||||
|
return file_get_contents($filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the monolithic file
|
||||||
|
$output = <<<'HEADER'
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 - Web Administration Shell
|
||||||
|
*
|
||||||
|
* A powerful, single-file PHP webshell with modern features:
|
||||||
|
* - File Manager with upload/download/edit capabilities
|
||||||
|
* - Terminal emulator with command execution
|
||||||
|
* - Code Editor with syntax highlighting
|
||||||
|
* - Process Manager with kill functionality
|
||||||
|
* - Network Tools (ping, traceroute, port scan)
|
||||||
|
* - Database Manager (MySQL, PostgreSQL, SQLite)
|
||||||
|
* - System Information viewer
|
||||||
|
*
|
||||||
|
* @package AleShell2
|
||||||
|
* @version 1.0.0
|
||||||
|
* @author AleShell Team
|
||||||
|
* @license MIT
|
||||||
|
*
|
||||||
|
* WARNING: This tool is intended for legitimate server administration only.
|
||||||
|
* Unauthorized access to computer systems is illegal.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// CONFIGURATION
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
HEADER;
|
||||||
|
|
||||||
|
// Add configuration
|
||||||
|
$output .= "\n\$ALESHELL_CONFIG = [\n";
|
||||||
|
$output .= " 'password_hash' => '{$passwordHash}',\n";
|
||||||
|
$output .= " 'session_timeout' => 3600,\n";
|
||||||
|
$output .= " 'max_login_attempts' => 5,\n";
|
||||||
|
$output .= " 'lockout_duration' => 900,\n";
|
||||||
|
$output .= " 'ip_whitelist' => [],\n";
|
||||||
|
$output .= " 'ip_blacklist' => [],\n";
|
||||||
|
$output .= "];\n\n";
|
||||||
|
|
||||||
|
// Add embedded views
|
||||||
|
$output .= "// ============================================================================\n";
|
||||||
|
$output .= "// EMBEDDED VIEWS\n";
|
||||||
|
$output .= "// ============================================================================\n\n";
|
||||||
|
$output .= "\$ALESHELL_VIEWS = [];\n\n";
|
||||||
|
|
||||||
|
foreach ($viewFiles as $viewFile) {
|
||||||
|
$path = __DIR__ . '/' . $viewFile;
|
||||||
|
if (file_exists($path)) {
|
||||||
|
$viewName = str_replace(['src/Views/', '.php'], '', $viewFile);
|
||||||
|
$viewName = str_replace('/', '.', $viewName);
|
||||||
|
$viewContent = extractViewContent($path);
|
||||||
|
// Escape for heredoc
|
||||||
|
$viewContent = addcslashes($viewContent, '\\\'');
|
||||||
|
$output .= "\$ALESHELL_VIEWS['{$viewName}'] = '" . $viewContent . "';\n\n";
|
||||||
|
echo " Added view: {$viewName}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$output .= "\n// ============================================================================\n";
|
||||||
|
$output .= "// CORE CLASSES\n";
|
||||||
|
$output .= "// ============================================================================\n\n";
|
||||||
|
|
||||||
|
// Process core files
|
||||||
|
foreach ($coreFiles as $file) {
|
||||||
|
$path = __DIR__ . '/' . $file;
|
||||||
|
if (file_exists($path)) {
|
||||||
|
$code = extractPhpCode($path, $minify);
|
||||||
|
// Replace namespace references with inline
|
||||||
|
$code = str_replace('AleShell2\\Core\\', 'AleShell2_', $code);
|
||||||
|
$code = str_replace('AleShell2\\Security\\', 'AleShell2_', $code);
|
||||||
|
$code = str_replace('AleShell2\\Modules\\', 'AleShell2_', $code);
|
||||||
|
|
||||||
|
// Rename classes to avoid namespace issues
|
||||||
|
$className = basename($file, '.php');
|
||||||
|
$code = preg_replace('/class\s+' . $className . '/', 'class AleShell2_' . $className, $code);
|
||||||
|
|
||||||
|
$output .= "// Source: {$file}\n";
|
||||||
|
$output .= $code . "\n\n";
|
||||||
|
echo " Added core: {$className}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$output .= "\n// ============================================================================\n";
|
||||||
|
$output .= "// SECURITY CLASSES\n";
|
||||||
|
$output .= "// ============================================================================\n\n";
|
||||||
|
|
||||||
|
// Process security files
|
||||||
|
foreach ($securityFiles as $file) {
|
||||||
|
$path = __DIR__ . '/' . $file;
|
||||||
|
if (file_exists($path)) {
|
||||||
|
$code = extractPhpCode($path, $minify);
|
||||||
|
$code = str_replace('AleShell2\\Core\\', 'AleShell2_', $code);
|
||||||
|
$code = str_replace('AleShell2\\Security\\', 'AleShell2_', $code);
|
||||||
|
$code = str_replace('AleShell2\\Modules\\', 'AleShell2_', $code);
|
||||||
|
|
||||||
|
$className = basename($file, '.php');
|
||||||
|
$code = preg_replace('/class\s+' . $className . '/', 'class AleShell2_' . $className, $code);
|
||||||
|
|
||||||
|
$output .= "// Source: {$file}\n";
|
||||||
|
$output .= $code . "\n\n";
|
||||||
|
echo " Added security: {$className}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$output .= "\n// ============================================================================\n";
|
||||||
|
$output .= "// MODULE CONTROLLERS\n";
|
||||||
|
$output .= "// ============================================================================\n\n";
|
||||||
|
|
||||||
|
// Process module files
|
||||||
|
foreach ($moduleFiles as $file) {
|
||||||
|
$path = __DIR__ . '/' . $file;
|
||||||
|
if (file_exists($path)) {
|
||||||
|
$code = extractPhpCode($path, $minify);
|
||||||
|
$code = str_replace('AleShell2\\Core\\', 'AleShell2_', $code);
|
||||||
|
$code = str_replace('AleShell2\\Security\\', 'AleShell2_', $code);
|
||||||
|
$code = str_replace('AleShell2\\Modules\\', 'AleShell2_', $code);
|
||||||
|
$code = str_replace('AleShell2\\Modules\\Auth\\', 'AleShell2_', $code);
|
||||||
|
$code = str_replace('AleShell2\\Modules\\Dashboard\\', 'AleShell2_', $code);
|
||||||
|
$code = str_replace('AleShell2\\Modules\\Files\\', 'AleShell2_', $code);
|
||||||
|
$code = str_replace('AleShell2\\Modules\\Terminal\\', 'AleShell2_', $code);
|
||||||
|
$code = str_replace('AleShell2\\Modules\\Editor\\', 'AleShell2_', $code);
|
||||||
|
$code = str_replace('AleShell2\\Modules\\Processes\\', 'AleShell2_', $code);
|
||||||
|
$code = str_replace('AleShell2\\Modules\\Network\\', 'AleShell2_', $code);
|
||||||
|
$code = str_replace('AleShell2\\Modules\\Database\\', 'AleShell2_', $code);
|
||||||
|
$code = str_replace('AleShell2\\Modules\\System\\', 'AleShell2_', $code);
|
||||||
|
$code = str_replace('AleShell2\\Modules\\Api\\', 'AleShell2_', $code);
|
||||||
|
|
||||||
|
$className = basename($file, '.php');
|
||||||
|
$code = preg_replace('/class\s+' . $className . '/', 'class AleShell2_' . $className, $code);
|
||||||
|
$code = preg_replace('/abstract\s+class\s+' . $className . '/', 'abstract class AleShell2_' . $className, $code);
|
||||||
|
$code = preg_replace('/extends\s+' . $className . '/', 'extends AleShell2_' . $className, $code);
|
||||||
|
$code = preg_replace('/extends\s+BaseController/', 'extends AleShell2_BaseController', $code);
|
||||||
|
|
||||||
|
$output .= "// Source: {$file}\n";
|
||||||
|
$output .= $code . "\n\n";
|
||||||
|
echo " Added module: {$className}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add bootstrap code
|
||||||
|
$output .= <<<'BOOTSTRAP'
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// BOOTSTRAP
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Override View class to use embedded views
|
||||||
|
class AleShell2_PackedView extends AleShell2_View {
|
||||||
|
public function render(string $template, array $data = []): string {
|
||||||
|
global $ALESHELL_VIEWS;
|
||||||
|
|
||||||
|
$templateKey = $template;
|
||||||
|
|
||||||
|
if (!isset($ALESHELL_VIEWS[$templateKey])) {
|
||||||
|
return "View not found: {$template}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$viewContent = $ALESHELL_VIEWS[$templateKey];
|
||||||
|
|
||||||
|
// Extract data to local scope
|
||||||
|
extract($data);
|
||||||
|
|
||||||
|
// Capture output
|
||||||
|
ob_start();
|
||||||
|
eval('?>' . $viewContent);
|
||||||
|
$content = ob_get_clean();
|
||||||
|
|
||||||
|
// If using layout, wrap content
|
||||||
|
if ($template !== 'layouts.main' && $template !== 'auth.login') {
|
||||||
|
if (isset($ALESHELL_VIEWS['layouts.main'])) {
|
||||||
|
$layoutContent = $ALESHELL_VIEWS['layouts.main'];
|
||||||
|
ob_start();
|
||||||
|
eval('?>' . $layoutContent);
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modified Application for packed version
|
||||||
|
class AleShell2_PackedApplication {
|
||||||
|
private AleShell2_Request $request;
|
||||||
|
private AleShell2_Response $response;
|
||||||
|
private AleShell2_Router $router;
|
||||||
|
private AleShell2_Session $session;
|
||||||
|
private AleShell2_Auth $auth;
|
||||||
|
private AleShell2_PackedView $view;
|
||||||
|
private array $config;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
global $ALESHELL_CONFIG;
|
||||||
|
$this->config = $ALESHELL_CONFIG;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(): void {
|
||||||
|
try {
|
||||||
|
// Initialize components
|
||||||
|
$this->request = new AleShell2_Request();
|
||||||
|
$this->response = new AleShell2_Response();
|
||||||
|
$this->session = new AleShell2_Session($this->config['session_timeout'] ?? 3600);
|
||||||
|
$this->auth = new AleShell2_Auth(
|
||||||
|
$this->config['password_hash'],
|
||||||
|
$this->session,
|
||||||
|
$this->config['max_login_attempts'] ?? 5,
|
||||||
|
$this->config['lockout_duration'] ?? 900
|
||||||
|
);
|
||||||
|
$this->view = new AleShell2_PackedView();
|
||||||
|
$this->router = new AleShell2_Router($this);
|
||||||
|
|
||||||
|
// Set security headers
|
||||||
|
$this->setSecurityHeaders();
|
||||||
|
|
||||||
|
// Check IP restrictions
|
||||||
|
if (!$this->checkIpAccess()) {
|
||||||
|
http_response_code(403);
|
||||||
|
die('Access denied');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start session
|
||||||
|
$this->session->start();
|
||||||
|
|
||||||
|
// Generate CSRF token if needed
|
||||||
|
if (!isset($_SESSION['csrf_token'])) {
|
||||||
|
$_SESSION['csrf_token'] = $this->auth->generateCsrfToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup routes
|
||||||
|
$this->setupRoutes();
|
||||||
|
|
||||||
|
// Dispatch request
|
||||||
|
$this->router->dispatch($this->request, $this->response);
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->handleError($e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setSecurityHeaders(): void {
|
||||||
|
header('X-Content-Type-Options: nosniff');
|
||||||
|
header('X-Frame-Options: DENY');
|
||||||
|
header('X-XSS-Protection: 1; mode=block');
|
||||||
|
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkIpAccess(): bool {
|
||||||
|
$clientIp = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||||
|
|
||||||
|
// Check blacklist
|
||||||
|
if (!empty($this->config['ip_blacklist'])) {
|
||||||
|
if (in_array($clientIp, $this->config['ip_blacklist'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whitelist
|
||||||
|
if (!empty($this->config['ip_whitelist'])) {
|
||||||
|
return in_array($clientIp, $this->config['ip_whitelist']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setupRoutes(): void {
|
||||||
|
// Auth routes (public)
|
||||||
|
$this->router->get('login', function($req, $res, $params) {
|
||||||
|
$controller = new AleShell2_AuthController($this);
|
||||||
|
$controller->showLogin($req, $res, $params);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->router->post('login', function($req, $res, $params) {
|
||||||
|
$controller = new AleShell2_AuthController($this);
|
||||||
|
$controller->login($req, $res, $params);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->router->get('logout', function($req, $res, $params) {
|
||||||
|
$controller = new AleShell2_AuthController($this);
|
||||||
|
$controller->logout($req, $res, $params);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Protected routes
|
||||||
|
$authMiddleware = function($handler) {
|
||||||
|
return function($req, $res, $params) use ($handler) {
|
||||||
|
if (!$this->auth->isAuthenticated()) {
|
||||||
|
$res->redirect('?action=login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$handler($req, $res, $params);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dashboard
|
||||||
|
$this->router->get('dashboard', $authMiddleware(function($req, $res, $params) {
|
||||||
|
$controller = new AleShell2_DashboardController($this);
|
||||||
|
$controller->index($req, $res, $params);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Files
|
||||||
|
$this->router->get('files', $authMiddleware(function($req, $res, $params) {
|
||||||
|
$controller = new AleShell2_FilesController($this);
|
||||||
|
$action = $req->get('action', 'index');
|
||||||
|
match($action) {
|
||||||
|
'list' => $controller->list($req, $res, $params),
|
||||||
|
'read' => $controller->read($req, $res, $params),
|
||||||
|
'download' => $controller->download($req, $res, $params),
|
||||||
|
'search' => $controller->search($req, $res, $params),
|
||||||
|
default => $controller->index($req, $res, $params),
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
$this->router->post('files', $authMiddleware(function($req, $res, $params) {
|
||||||
|
$controller = new AleShell2_FilesController($this);
|
||||||
|
$action = $req->get('action', 'index');
|
||||||
|
match($action) {
|
||||||
|
'write' => $controller->write($req, $res, $params),
|
||||||
|
'delete' => $controller->delete($req, $res, $params),
|
||||||
|
'mkdir' => $controller->mkdir($req, $res, $params),
|
||||||
|
'copy' => $controller->copy($req, $res, $params),
|
||||||
|
'move' => $controller->move($req, $res, $params),
|
||||||
|
'chmod' => $controller->chmod($req, $res, $params),
|
||||||
|
'upload' => $controller->upload($req, $res, $params),
|
||||||
|
default => $controller->index($req, $res, $params),
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Terminal
|
||||||
|
$this->router->get('terminal', $authMiddleware(function($req, $res, $params) {
|
||||||
|
$controller = new AleShell2_TerminalController($this);
|
||||||
|
$controller->index($req, $res, $params);
|
||||||
|
}));
|
||||||
|
|
||||||
|
$this->router->post('terminal', $authMiddleware(function($req, $res, $params) {
|
||||||
|
$controller = new AleShell2_TerminalController($this);
|
||||||
|
$action = $req->get('action', 'exec');
|
||||||
|
if ($action === 'exec') {
|
||||||
|
$controller->execute($req, $res, $params);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Editor
|
||||||
|
$this->router->get('editor', $authMiddleware(function($req, $res, $params) {
|
||||||
|
$controller = new AleShell2_EditorController($this);
|
||||||
|
$controller->index($req, $res, $params);
|
||||||
|
}));
|
||||||
|
|
||||||
|
$this->router->post('editor', $authMiddleware(function($req, $res, $params) {
|
||||||
|
$controller = new AleShell2_EditorController($this);
|
||||||
|
$controller->save($req, $res, $params);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Processes
|
||||||
|
$this->router->get('processes', $authMiddleware(function($req, $res, $params) {
|
||||||
|
$controller = new AleShell2_ProcessesController($this);
|
||||||
|
$action = $req->get('action', 'index');
|
||||||
|
if ($action === 'list') {
|
||||||
|
$controller->list($req, $res, $params);
|
||||||
|
} else {
|
||||||
|
$controller->index($req, $res, $params);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
$this->router->post('processes', $authMiddleware(function($req, $res, $params) {
|
||||||
|
$controller = new AleShell2_ProcessesController($this);
|
||||||
|
$action = $req->get('action', 'kill');
|
||||||
|
if ($action === 'kill') {
|
||||||
|
$controller->kill($req, $res, $params);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Network
|
||||||
|
$this->router->get('network', $authMiddleware(function($req, $res, $params) {
|
||||||
|
$controller = new AleShell2_NetworkController($this);
|
||||||
|
$action = $req->get('action', 'index');
|
||||||
|
if ($action === 'connections') {
|
||||||
|
$controller->connections($req, $res, $params);
|
||||||
|
} else {
|
||||||
|
$controller->index($req, $res, $params);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
$this->router->post('network', $authMiddleware(function($req, $res, $params) {
|
||||||
|
$controller = new AleShell2_NetworkController($this);
|
||||||
|
$action = $req->get('action');
|
||||||
|
match($action) {
|
||||||
|
'ping' => $controller->ping($req, $res, $params),
|
||||||
|
'traceroute' => $controller->traceroute($req, $res, $params),
|
||||||
|
'portscan' => $controller->portscan($req, $res, $params),
|
||||||
|
default => $controller->index($req, $res, $params),
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Database
|
||||||
|
$this->router->get('database', $authMiddleware(function($req, $res, $params) {
|
||||||
|
$controller = new AleShell2_DatabaseController($this);
|
||||||
|
$action = $req->get('action', 'index');
|
||||||
|
match($action) {
|
||||||
|
'tables' => $controller->tables($req, $res, $params),
|
||||||
|
'structure' => $controller->structure($req, $res, $params),
|
||||||
|
'export' => $controller->export($req, $res, $params),
|
||||||
|
default => $controller->index($req, $res, $params),
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
$this->router->post('database', $authMiddleware(function($req, $res, $params) {
|
||||||
|
$controller = new AleShell2_DatabaseController($this);
|
||||||
|
$action = $req->get('action');
|
||||||
|
match($action) {
|
||||||
|
'connect' => $controller->connect($req, $res, $params),
|
||||||
|
'disconnect' => $controller->disconnect($req, $res, $params),
|
||||||
|
'query' => $controller->query($req, $res, $params),
|
||||||
|
'selectDb' => $controller->selectDb($req, $res, $params),
|
||||||
|
default => $controller->index($req, $res, $params),
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
// System
|
||||||
|
$this->router->get('system', $authMiddleware(function($req, $res, $params) {
|
||||||
|
$controller = new AleShell2_SystemController($this);
|
||||||
|
$action = $req->get('action', 'index');
|
||||||
|
match($action) {
|
||||||
|
'info' => $controller->info($req, $res, $params),
|
||||||
|
'phpinfo' => $controller->phpinfo($req, $res, $params),
|
||||||
|
'environment' => $controller->environment($req, $res, $params),
|
||||||
|
'extensions' => $controller->extensions($req, $res, $params),
|
||||||
|
default => $controller->index($req, $res, $params),
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
// API
|
||||||
|
$this->router->get('api', $authMiddleware(function($req, $res, $params) {
|
||||||
|
$controller = new AleShell2_ApiController($this);
|
||||||
|
$action = $req->get('action', 'status');
|
||||||
|
match($action) {
|
||||||
|
'status' => $controller->status($req, $res, $params),
|
||||||
|
'csrf' => $controller->csrf($req, $res, $params),
|
||||||
|
'health' => $controller->health($req, $res, $params),
|
||||||
|
'download' => $controller->download($req, $res, $params),
|
||||||
|
default => $controller->status($req, $res, $params),
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
$this->router->post('api', $authMiddleware(function($req, $res, $params) {
|
||||||
|
$controller = new AleShell2_ApiController($this);
|
||||||
|
$action = $req->get('action');
|
||||||
|
match($action) {
|
||||||
|
'exec' => $controller->exec($req, $res, $params),
|
||||||
|
'eval' => $controller->eval($req, $res, $params),
|
||||||
|
'upload' => $controller->upload($req, $res, $params),
|
||||||
|
default => $controller->status($req, $res, $params),
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Default route
|
||||||
|
$this->router->get('', function($req, $res, $params) {
|
||||||
|
if ($this->auth->isAuthenticated()) {
|
||||||
|
$res->redirect('?module=dashboard');
|
||||||
|
} else {
|
||||||
|
$res->redirect('?action=login');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleError(\Throwable $e): void {
|
||||||
|
http_response_code(500);
|
||||||
|
echo '<h1>Error</h1>';
|
||||||
|
echo '<p>' . htmlspecialchars($e->getMessage()) . '</p>';
|
||||||
|
if (ini_get('display_errors')) {
|
||||||
|
echo '<pre>' . htmlspecialchars($e->getTraceAsString()) . '</pre>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSession(): AleShell2_Session { return $this->session; }
|
||||||
|
public function getAuth(): AleShell2_Auth { return $this->auth; }
|
||||||
|
public function getView(): AleShell2_PackedView { return $this->view; }
|
||||||
|
public function getConfig(): array { return $this->config; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the application
|
||||||
|
$app = new AleShell2_PackedApplication();
|
||||||
|
$app->run();
|
||||||
|
|
||||||
|
BOOTSTRAP;
|
||||||
|
|
||||||
|
// Write output file
|
||||||
|
file_put_contents($outputFile, $output);
|
||||||
|
|
||||||
|
$size = filesize($outputFile);
|
||||||
|
$sizeKb = round($size / 1024, 2);
|
||||||
|
|
||||||
|
echo "\n================\n";
|
||||||
|
echo "Pack complete!\n";
|
||||||
|
echo "Output: {$outputFile}\n";
|
||||||
|
echo "Size: {$sizeKb} KB\n";
|
||||||
|
echo "Password: {$password}\n";
|
||||||
|
echo "\nTo use, upload the file to your web server and access it via browser.\n";
|
||||||
1
packed/.gitkeep
Archivo normal
1
packed/.gitkeep
Archivo normal
@@ -0,0 +1 @@
|
|||||||
|
# This file ensures the packed directory is tracked by git
|
||||||
123
src/Config/config.example.php
Archivo normal
123
src/Config/config.example.php
Archivo normal
@@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Configuration File
|
||||||
|
*
|
||||||
|
* Copy this file to config.php and customize as needed.
|
||||||
|
*
|
||||||
|
* @package AleShell2
|
||||||
|
*/
|
||||||
|
|
||||||
|
return [
|
||||||
|
// ==========================================================================
|
||||||
|
// APPLICATION SETTINGS
|
||||||
|
// ==========================================================================
|
||||||
|
'app' => [
|
||||||
|
'name' => 'AleShell2',
|
||||||
|
'version' => '2.0.0',
|
||||||
|
'debug' => false,
|
||||||
|
'timezone' => 'UTC',
|
||||||
|
],
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// SECURITY SETTINGS
|
||||||
|
// ==========================================================================
|
||||||
|
'security' => [
|
||||||
|
// Default password: 'aleshell' - CHANGE THIS!
|
||||||
|
'password' => '$2y$10$YourHashedPasswordHere',
|
||||||
|
|
||||||
|
// Session timeout in seconds (default: 1 hour)
|
||||||
|
'session_timeout' => 3600,
|
||||||
|
|
||||||
|
// Maximum login attempts before lockout
|
||||||
|
'max_attempts' => 5,
|
||||||
|
|
||||||
|
// Lockout time in seconds (default: 5 minutes)
|
||||||
|
'lockout_time' => 300,
|
||||||
|
|
||||||
|
// Enable CSRF protection
|
||||||
|
'csrf_protection' => true,
|
||||||
|
|
||||||
|
// IP whitelist (empty = allow all)
|
||||||
|
'allowed_ips' => [],
|
||||||
|
|
||||||
|
// IP blacklist
|
||||||
|
'blocked_ips' => [],
|
||||||
|
|
||||||
|
// Commands to block in terminal
|
||||||
|
'blocked_commands' => [
|
||||||
|
'rm -rf /',
|
||||||
|
'mkfs',
|
||||||
|
'dd if=/dev/zero',
|
||||||
|
'shutdown',
|
||||||
|
'reboot',
|
||||||
|
'halt',
|
||||||
|
'init 0',
|
||||||
|
'init 6',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Paths to restrict (empty = no restriction)
|
||||||
|
'restricted_paths' => [],
|
||||||
|
],
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// FEATURE TOGGLES
|
||||||
|
// ==========================================================================
|
||||||
|
'features' => [
|
||||||
|
'file_manager' => true,
|
||||||
|
'terminal' => true,
|
||||||
|
'code_editor' => true,
|
||||||
|
'process_manager' => true,
|
||||||
|
'network_tools' => true,
|
||||||
|
'database_tools' => true,
|
||||||
|
'system_info' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// USER INTERFACE SETTINGS
|
||||||
|
// ==========================================================================
|
||||||
|
'ui' => [
|
||||||
|
// Default theme: dark, light, matrix, ocean, dracula
|
||||||
|
'theme' => 'dark',
|
||||||
|
|
||||||
|
// Default language
|
||||||
|
'language' => 'en',
|
||||||
|
|
||||||
|
// Items per page in lists
|
||||||
|
'items_per_page' => 50,
|
||||||
|
|
||||||
|
// Show hidden files by default
|
||||||
|
'show_hidden_files' => false,
|
||||||
|
|
||||||
|
// Auto-refresh interval in seconds (0 = disabled)
|
||||||
|
'auto_refresh' => 30,
|
||||||
|
],
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// LIMITS
|
||||||
|
// ==========================================================================
|
||||||
|
'limits' => [
|
||||||
|
// Maximum file size for viewing (50MB)
|
||||||
|
'max_file_size' => 50 * 1024 * 1024,
|
||||||
|
|
||||||
|
// Maximum upload size (100MB)
|
||||||
|
'max_upload_size' => 100 * 1024 * 1024,
|
||||||
|
|
||||||
|
// Command execution timeout in seconds
|
||||||
|
'command_timeout' => 30,
|
||||||
|
|
||||||
|
// Maximum command history entries
|
||||||
|
'max_history' => 100,
|
||||||
|
|
||||||
|
// Maximum process list entries
|
||||||
|
'max_processes' => 1000,
|
||||||
|
],
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// DATABASE SETTINGS (for database tools)
|
||||||
|
// ==========================================================================
|
||||||
|
'database' => [
|
||||||
|
'default_type' => 'mysql',
|
||||||
|
'default_host' => 'localhost',
|
||||||
|
'default_port' => 3306,
|
||||||
|
],
|
||||||
|
];
|
||||||
230
src/Core/Application.php
Archivo normal
230
src/Core/Application.php
Archivo normal
@@ -0,0 +1,230 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Application Core
|
||||||
|
*
|
||||||
|
* Main application class that bootstraps and runs the shell.
|
||||||
|
*
|
||||||
|
* @package AleShell2\Core
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace AleShell2\Core;
|
||||||
|
|
||||||
|
use AleShell2\Security\Auth;
|
||||||
|
use AleShell2\Security\Session;
|
||||||
|
|
||||||
|
class Application
|
||||||
|
{
|
||||||
|
private array $config = [];
|
||||||
|
private Request $request;
|
||||||
|
private Response $response;
|
||||||
|
private Router $router;
|
||||||
|
private Auth $auth;
|
||||||
|
private Session $session;
|
||||||
|
private static ?Application $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create application instance
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
self::$instance = $this;
|
||||||
|
$this->loadConfig();
|
||||||
|
$this->initializeComponents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get singleton instance
|
||||||
|
*/
|
||||||
|
public static function getInstance(): ?Application
|
||||||
|
{
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load configuration
|
||||||
|
*/
|
||||||
|
private function loadConfig(): void
|
||||||
|
{
|
||||||
|
$configFile = ALESHELL_SRC . '/Config/config.php';
|
||||||
|
|
||||||
|
if (file_exists($configFile)) {
|
||||||
|
$this->config = require $configFile;
|
||||||
|
} else {
|
||||||
|
// Default configuration
|
||||||
|
$this->config = $this->getDefaultConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply timezone
|
||||||
|
$timezone = $this->config['app']['timezone'] ?? 'UTC';
|
||||||
|
date_default_timezone_set($timezone);
|
||||||
|
|
||||||
|
// Set debug mode
|
||||||
|
if ($this->config['app']['debug'] ?? false) {
|
||||||
|
define('ALESHELL_DEBUG', true);
|
||||||
|
ini_set('display_errors', '1');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize all components
|
||||||
|
*/
|
||||||
|
private function initializeComponents(): void
|
||||||
|
{
|
||||||
|
$this->request = new Request();
|
||||||
|
$this->response = new Response();
|
||||||
|
$this->session = new Session($this->config);
|
||||||
|
$this->auth = new Auth($this->config, $this->session);
|
||||||
|
$this->router = new Router($this->config, $this->auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the application
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Start session
|
||||||
|
$this->session->start();
|
||||||
|
|
||||||
|
// Check IP restrictions
|
||||||
|
$this->checkIPRestrictions();
|
||||||
|
|
||||||
|
// Route the request
|
||||||
|
$this->router->dispatch($this->request, $this->response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check IP whitelist/blacklist
|
||||||
|
*/
|
||||||
|
private function checkIPRestrictions(): void
|
||||||
|
{
|
||||||
|
$clientIP = $this->request->getClientIP();
|
||||||
|
|
||||||
|
// Check blocked IPs
|
||||||
|
$blockedIPs = $this->config['security']['blocked_ips'] ?? [];
|
||||||
|
if (in_array($clientIP, $blockedIPs, true)) {
|
||||||
|
http_response_code(403);
|
||||||
|
die('Access denied');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check allowed IPs (if configured)
|
||||||
|
$allowedIPs = $this->config['security']['allowed_ips'] ?? [];
|
||||||
|
if (!empty($allowedIPs) && !in_array($clientIP, $allowedIPs, true)) {
|
||||||
|
http_response_code(403);
|
||||||
|
die('Access denied');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get configuration value
|
||||||
|
*/
|
||||||
|
public function config(string $key, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
$keys = explode('.', $key);
|
||||||
|
$value = $this->config;
|
||||||
|
|
||||||
|
foreach ($keys as $k) {
|
||||||
|
if (!isset($value[$k])) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
$value = $value[$k];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all configuration
|
||||||
|
*/
|
||||||
|
public function getConfig(): array
|
||||||
|
{
|
||||||
|
return $this->config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get request instance
|
||||||
|
*/
|
||||||
|
public function getRequest(): Request
|
||||||
|
{
|
||||||
|
return $this->request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get response instance
|
||||||
|
*/
|
||||||
|
public function getResponse(): Response
|
||||||
|
{
|
||||||
|
return $this->response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get auth instance
|
||||||
|
*/
|
||||||
|
public function getAuth(): Auth
|
||||||
|
{
|
||||||
|
return $this->auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get session instance
|
||||||
|
*/
|
||||||
|
public function getSession(): Session
|
||||||
|
{
|
||||||
|
return $this->session;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default configuration
|
||||||
|
*/
|
||||||
|
private function getDefaultConfig(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'app' => [
|
||||||
|
'name' => 'AleShell2',
|
||||||
|
'version' => ALESHELL_VERSION,
|
||||||
|
'debug' => false,
|
||||||
|
'timezone' => 'UTC',
|
||||||
|
],
|
||||||
|
'security' => [
|
||||||
|
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
|
||||||
|
'session_timeout' => 3600,
|
||||||
|
'max_attempts' => 5,
|
||||||
|
'lockout_time' => 300,
|
||||||
|
'csrf_protection' => true,
|
||||||
|
'allowed_ips' => [],
|
||||||
|
'blocked_ips' => [],
|
||||||
|
'blocked_commands' => [],
|
||||||
|
'restricted_paths' => [],
|
||||||
|
],
|
||||||
|
'features' => [
|
||||||
|
'file_manager' => true,
|
||||||
|
'terminal' => true,
|
||||||
|
'code_editor' => true,
|
||||||
|
'process_manager' => true,
|
||||||
|
'network_tools' => true,
|
||||||
|
'database_tools' => true,
|
||||||
|
'system_info' => true,
|
||||||
|
],
|
||||||
|
'ui' => [
|
||||||
|
'theme' => 'dark',
|
||||||
|
'language' => 'en',
|
||||||
|
'items_per_page' => 50,
|
||||||
|
'show_hidden_files' => false,
|
||||||
|
'auto_refresh' => 30,
|
||||||
|
],
|
||||||
|
'limits' => [
|
||||||
|
'max_file_size' => 50 * 1024 * 1024,
|
||||||
|
'max_upload_size' => 100 * 1024 * 1024,
|
||||||
|
'command_timeout' => 30,
|
||||||
|
'max_history' => 100,
|
||||||
|
'max_processes' => 1000,
|
||||||
|
],
|
||||||
|
'database' => [
|
||||||
|
'default_type' => 'mysql',
|
||||||
|
'default_host' => 'localhost',
|
||||||
|
'default_port' => 3306,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
256
src/Core/Request.php
Archivo normal
256
src/Core/Request.php
Archivo normal
@@ -0,0 +1,256 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Request Handler
|
||||||
|
*
|
||||||
|
* Handles incoming HTTP requests.
|
||||||
|
*
|
||||||
|
* @package AleShell2\Core
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace AleShell2\Core;
|
||||||
|
|
||||||
|
class Request
|
||||||
|
{
|
||||||
|
private string $method;
|
||||||
|
private string $uri;
|
||||||
|
private string $path;
|
||||||
|
private array $query;
|
||||||
|
private array $post;
|
||||||
|
private array $files;
|
||||||
|
private array $headers;
|
||||||
|
private array $server;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
|
||||||
|
$this->uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||||
|
$this->path = $this->parsePath();
|
||||||
|
$this->query = $_GET;
|
||||||
|
$this->post = $_POST;
|
||||||
|
$this->files = $_FILES;
|
||||||
|
$this->server = $_SERVER;
|
||||||
|
$this->headers = $this->parseHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse request path from URI
|
||||||
|
*/
|
||||||
|
private function parsePath(): string
|
||||||
|
{
|
||||||
|
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||||
|
$path = parse_url($uri, PHP_URL_PATH) ?: '/';
|
||||||
|
|
||||||
|
// Remove base path if present
|
||||||
|
$scriptDir = dirname($_SERVER['SCRIPT_NAME'] ?? '');
|
||||||
|
if ($scriptDir !== '/' && strpos($path, $scriptDir) === 0) {
|
||||||
|
$path = substr($path, strlen($scriptDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove index.php if present
|
||||||
|
$path = preg_replace('#^/index\.php#', '', $path) ?: '/';
|
||||||
|
|
||||||
|
return '/' . trim($path, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse request headers
|
||||||
|
*/
|
||||||
|
private function parseHeaders(): array
|
||||||
|
{
|
||||||
|
$headers = [];
|
||||||
|
|
||||||
|
foreach ($_SERVER as $key => $value) {
|
||||||
|
if (strpos($key, 'HTTP_') === 0) {
|
||||||
|
$header = str_replace('_', '-', substr($key, 5));
|
||||||
|
$headers[strtolower($header)] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add content type and length if present
|
||||||
|
if (isset($_SERVER['CONTENT_TYPE'])) {
|
||||||
|
$headers['content-type'] = $_SERVER['CONTENT_TYPE'];
|
||||||
|
}
|
||||||
|
if (isset($_SERVER['CONTENT_LENGTH'])) {
|
||||||
|
$headers['content-length'] = $_SERVER['CONTENT_LENGTH'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get request method
|
||||||
|
*/
|
||||||
|
public function getMethod(): string
|
||||||
|
{
|
||||||
|
return $this->method;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get request URI
|
||||||
|
*/
|
||||||
|
public function getUri(): string
|
||||||
|
{
|
||||||
|
return $this->uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get request path
|
||||||
|
*/
|
||||||
|
public function getPath(): string
|
||||||
|
{
|
||||||
|
return $this->path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if request is AJAX
|
||||||
|
*/
|
||||||
|
public function isAjax(): bool
|
||||||
|
{
|
||||||
|
return strtolower($this->headers['x-requested-with'] ?? '') === 'xmlhttprequest';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if request method matches
|
||||||
|
*/
|
||||||
|
public function isMethod(string $method): bool
|
||||||
|
{
|
||||||
|
return $this->method === strtoupper($method);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get query parameter
|
||||||
|
*/
|
||||||
|
public function get(string $key, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
return $this->query[$key] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get POST parameter
|
||||||
|
*/
|
||||||
|
public function post(string $key, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
return $this->post[$key] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all POST data
|
||||||
|
*/
|
||||||
|
public function all(): array
|
||||||
|
{
|
||||||
|
return array_merge($this->query, $this->post);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get input from GET or POST
|
||||||
|
*/
|
||||||
|
public function input(string $key, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
return $this->post[$key] ?? $this->query[$key] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get header
|
||||||
|
*/
|
||||||
|
public function header(string $key, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
return $this->headers[strtolower($key)] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get uploaded file
|
||||||
|
*/
|
||||||
|
public function file(string $key): ?array
|
||||||
|
{
|
||||||
|
return $this->files[$key] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get client IP address
|
||||||
|
*/
|
||||||
|
public function getClientIP(): string
|
||||||
|
{
|
||||||
|
$headers = [
|
||||||
|
'HTTP_CF_CONNECTING_IP', // Cloudflare
|
||||||
|
'HTTP_X_FORWARDED_FOR', // Proxy
|
||||||
|
'HTTP_X_REAL_IP', // Nginx
|
||||||
|
'HTTP_CLIENT_IP', // Other proxies
|
||||||
|
'REMOTE_ADDR', // Direct connection
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($headers as $header) {
|
||||||
|
if (!empty($this->server[$header])) {
|
||||||
|
$ips = explode(',', $this->server[$header]);
|
||||||
|
$ip = trim($ips[0]);
|
||||||
|
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||||
|
return $ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '0.0.0.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get raw request body
|
||||||
|
*/
|
||||||
|
public function getBody(): string
|
||||||
|
{
|
||||||
|
return file_get_contents('php://input') ?: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get JSON body
|
||||||
|
*/
|
||||||
|
public function json(): ?array
|
||||||
|
{
|
||||||
|
$body = $this->getBody();
|
||||||
|
|
||||||
|
if (empty($body)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($body, true);
|
||||||
|
|
||||||
|
return is_array($data) ? $data : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if request expects JSON response
|
||||||
|
*/
|
||||||
|
public function expectsJson(): bool
|
||||||
|
{
|
||||||
|
$accept = $this->header('accept', '');
|
||||||
|
return str_contains($accept, 'application/json') || $this->isAjax();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get server variable
|
||||||
|
*/
|
||||||
|
public function server(string $key, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
return $this->server[$key] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get query string
|
||||||
|
*/
|
||||||
|
public function getQueryString(): string
|
||||||
|
{
|
||||||
|
return $_SERVER['QUERY_STRING'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build URL with query parameters
|
||||||
|
*/
|
||||||
|
public function fullUrl(): string
|
||||||
|
{
|
||||||
|
$scheme = (!empty($this->server['HTTPS']) && $this->server['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||||
|
$host = $this->server['HTTP_HOST'] ?? 'localhost';
|
||||||
|
|
||||||
|
return $scheme . '://' . $host . $this->uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
241
src/Core/Response.php
Archivo normal
241
src/Core/Response.php
Archivo normal
@@ -0,0 +1,241 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Response Handler
|
||||||
|
*
|
||||||
|
* Handles HTTP responses.
|
||||||
|
*
|
||||||
|
* @package AleShell2\Core
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace AleShell2\Core;
|
||||||
|
|
||||||
|
class Response
|
||||||
|
{
|
||||||
|
private int $statusCode = 200;
|
||||||
|
private array $headers = [];
|
||||||
|
private string $content = '';
|
||||||
|
private bool $sent = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set HTTP status code
|
||||||
|
*/
|
||||||
|
public function setStatusCode(int $code): self
|
||||||
|
{
|
||||||
|
$this->statusCode = $code;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get HTTP status code
|
||||||
|
*/
|
||||||
|
public function getStatusCode(): int
|
||||||
|
{
|
||||||
|
return $this->statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set response header
|
||||||
|
*/
|
||||||
|
public function setHeader(string $name, string $value): self
|
||||||
|
{
|
||||||
|
$this->headers[$name] = $value;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set content type
|
||||||
|
*/
|
||||||
|
public function setContentType(string $type): self
|
||||||
|
{
|
||||||
|
$this->headers['Content-Type'] = $type;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set response content
|
||||||
|
*/
|
||||||
|
public function setContent(string $content): self
|
||||||
|
{
|
||||||
|
$this->content = $content;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get response content
|
||||||
|
*/
|
||||||
|
public function getContent(): string
|
||||||
|
{
|
||||||
|
return $this->content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send JSON response
|
||||||
|
*/
|
||||||
|
public function json(mixed $data, int $statusCode = 200): void
|
||||||
|
{
|
||||||
|
$this->statusCode = $statusCode;
|
||||||
|
$this->headers['Content-Type'] = 'application/json; charset=utf-8';
|
||||||
|
$this->content = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
$this->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send success JSON response
|
||||||
|
*/
|
||||||
|
public function success(mixed $data = null, string $message = 'Success'): void
|
||||||
|
{
|
||||||
|
$this->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => $message,
|
||||||
|
'data' => $data,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send error JSON response
|
||||||
|
*/
|
||||||
|
public function error(string $message, int $statusCode = 400, mixed $data = null): void
|
||||||
|
{
|
||||||
|
$this->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $message,
|
||||||
|
'data' => $data,
|
||||||
|
], $statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send HTML response
|
||||||
|
*/
|
||||||
|
public function html(string $content, int $statusCode = 200): void
|
||||||
|
{
|
||||||
|
$this->statusCode = $statusCode;
|
||||||
|
$this->headers['Content-Type'] = 'text/html; charset=utf-8';
|
||||||
|
$this->content = $content;
|
||||||
|
$this->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send file download
|
||||||
|
*/
|
||||||
|
public function download(string $filePath, string $filename = null): void
|
||||||
|
{
|
||||||
|
if (!file_exists($filePath)) {
|
||||||
|
$this->error('File not found', 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filename = $filename ?? basename($filePath);
|
||||||
|
$mimeType = mime_content_type($filePath) ?: 'application/octet-stream';
|
||||||
|
|
||||||
|
$this->headers['Content-Type'] = $mimeType;
|
||||||
|
$this->headers['Content-Disposition'] = 'attachment; filename="' . $filename . '"';
|
||||||
|
$this->headers['Content-Length'] = (string)filesize($filePath);
|
||||||
|
$this->headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
|
||||||
|
|
||||||
|
$this->sendHeaders();
|
||||||
|
|
||||||
|
readfile($filePath);
|
||||||
|
$this->sent = true;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send file inline (for viewing)
|
||||||
|
*/
|
||||||
|
public function file(string $filePath, string $mimeType = null): void
|
||||||
|
{
|
||||||
|
if (!file_exists($filePath)) {
|
||||||
|
$this->error('File not found', 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mimeType = $mimeType ?? mime_content_type($filePath) ?: 'application/octet-stream';
|
||||||
|
|
||||||
|
$this->headers['Content-Type'] = $mimeType;
|
||||||
|
$this->headers['Content-Length'] = (string)filesize($filePath);
|
||||||
|
|
||||||
|
$this->sendHeaders();
|
||||||
|
|
||||||
|
readfile($filePath);
|
||||||
|
$this->sent = true;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to URL
|
||||||
|
*/
|
||||||
|
public function redirect(string $url, int $statusCode = 302): void
|
||||||
|
{
|
||||||
|
$this->statusCode = $statusCode;
|
||||||
|
$this->headers['Location'] = $url;
|
||||||
|
$this->send();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send headers
|
||||||
|
*/
|
||||||
|
private function sendHeaders(): void
|
||||||
|
{
|
||||||
|
if (headers_sent()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code($this->statusCode);
|
||||||
|
|
||||||
|
foreach ($this->headers as $name => $value) {
|
||||||
|
header("{$name}: {$value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send response
|
||||||
|
*/
|
||||||
|
public function send(): void
|
||||||
|
{
|
||||||
|
if ($this->sent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->sendHeaders();
|
||||||
|
|
||||||
|
echo $this->content;
|
||||||
|
|
||||||
|
$this->sent = true;
|
||||||
|
|
||||||
|
if (function_exists('fastcgi_finish_request')) {
|
||||||
|
fastcgi_finish_request();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if response was sent
|
||||||
|
*/
|
||||||
|
public function isSent(): bool
|
||||||
|
{
|
||||||
|
return $this->sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set no cache headers
|
||||||
|
*/
|
||||||
|
public function noCache(): self
|
||||||
|
{
|
||||||
|
$this->headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
|
||||||
|
$this->headers['Pragma'] = 'no-cache';
|
||||||
|
$this->headers['Expires'] = '0';
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set cache headers
|
||||||
|
*/
|
||||||
|
public function cache(int $seconds): self
|
||||||
|
{
|
||||||
|
$this->headers['Cache-Control'] = "public, max-age={$seconds}";
|
||||||
|
$this->headers['Expires'] = gmdate('D, d M Y H:i:s', time() + $seconds) . ' GMT';
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
248
src/Core/Router.php
Archivo normal
248
src/Core/Router.php
Archivo normal
@@ -0,0 +1,248 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Router
|
||||||
|
*
|
||||||
|
* Routes requests to appropriate handlers.
|
||||||
|
*
|
||||||
|
* @package AleShell2\Core
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace AleShell2\Core;
|
||||||
|
|
||||||
|
use AleShell2\Security\Auth;
|
||||||
|
|
||||||
|
class Router
|
||||||
|
{
|
||||||
|
private array $config;
|
||||||
|
private Auth $auth;
|
||||||
|
private array $routes = [];
|
||||||
|
|
||||||
|
public function __construct(array $config, Auth $auth)
|
||||||
|
{
|
||||||
|
$this->config = $config;
|
||||||
|
$this->auth = $auth;
|
||||||
|
$this->registerRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register all application routes
|
||||||
|
*/
|
||||||
|
private function registerRoutes(): void
|
||||||
|
{
|
||||||
|
// Authentication routes (no auth required)
|
||||||
|
$this->addRoute('GET', '/login', 'auth.login', false);
|
||||||
|
$this->addRoute('POST', '/login', 'auth.login', false);
|
||||||
|
$this->addRoute('POST', '/logout', 'auth.logout', true);
|
||||||
|
$this->addRoute('GET', '/auth/status', 'auth.status', false);
|
||||||
|
|
||||||
|
// Main routes (auth required)
|
||||||
|
$this->addRoute('GET', '/', 'dashboard.index', true);
|
||||||
|
$this->addRoute('GET', '/dashboard', 'dashboard.index', true);
|
||||||
|
|
||||||
|
// File manager
|
||||||
|
$this->addRoute('GET', '/files', 'files.index', true);
|
||||||
|
$this->addRoute('POST', '/files/list', 'files.list', true);
|
||||||
|
$this->addRoute('POST', '/files/read', 'files.read', true);
|
||||||
|
$this->addRoute('POST', '/files/write', 'files.write', true);
|
||||||
|
$this->addRoute('POST', '/files/delete', 'files.delete', true);
|
||||||
|
$this->addRoute('POST', '/files/create', 'files.create', true);
|
||||||
|
$this->addRoute('POST', '/files/rename', 'files.rename', true);
|
||||||
|
$this->addRoute('POST', '/files/copy', 'files.copy', true);
|
||||||
|
$this->addRoute('POST', '/files/move', 'files.move', true);
|
||||||
|
$this->addRoute('POST', '/files/chmod', 'files.chmod', true);
|
||||||
|
$this->addRoute('POST', '/files/upload', 'files.upload', true);
|
||||||
|
$this->addRoute('GET', '/files/download', 'files.download', true);
|
||||||
|
$this->addRoute('POST', '/files/mkdir', 'files.mkdir', true);
|
||||||
|
$this->addRoute('POST', '/files/search', 'files.search', true);
|
||||||
|
|
||||||
|
// Terminal
|
||||||
|
$this->addRoute('GET', '/terminal', 'terminal.index', true);
|
||||||
|
$this->addRoute('POST', '/terminal/execute', 'terminal.execute', true);
|
||||||
|
$this->addRoute('GET', '/terminal/history', 'terminal.history', true);
|
||||||
|
$this->addRoute('POST', '/terminal/clear', 'terminal.clear', true);
|
||||||
|
|
||||||
|
// Code editor
|
||||||
|
$this->addRoute('GET', '/editor', 'editor.index', true);
|
||||||
|
$this->addRoute('POST', '/editor/save', 'editor.save', true);
|
||||||
|
|
||||||
|
// Processes
|
||||||
|
$this->addRoute('GET', '/processes', 'processes.index', true);
|
||||||
|
$this->addRoute('POST', '/processes/list', 'processes.list', true);
|
||||||
|
$this->addRoute('POST', '/processes/kill', 'processes.kill', true);
|
||||||
|
|
||||||
|
// Network
|
||||||
|
$this->addRoute('GET', '/network', 'network.index', true);
|
||||||
|
$this->addRoute('POST', '/network/connections', 'network.connections', true);
|
||||||
|
$this->addRoute('POST', '/network/ping', 'network.ping', true);
|
||||||
|
$this->addRoute('POST', '/network/traceroute', 'network.traceroute', true);
|
||||||
|
$this->addRoute('POST', '/network/portscan', 'network.portscan', true);
|
||||||
|
|
||||||
|
// Database
|
||||||
|
$this->addRoute('GET', '/database', 'database.index', true);
|
||||||
|
$this->addRoute('POST', '/database/connect', 'database.connect', true);
|
||||||
|
$this->addRoute('POST', '/database/query', 'database.query', true);
|
||||||
|
$this->addRoute('POST', '/database/tables', 'database.tables', true);
|
||||||
|
$this->addRoute('POST', '/database/structure', 'database.structure', true);
|
||||||
|
|
||||||
|
// System info
|
||||||
|
$this->addRoute('GET', '/system', 'system.index', true);
|
||||||
|
$this->addRoute('GET', '/system/info', 'system.info', true);
|
||||||
|
|
||||||
|
// API endpoints
|
||||||
|
$this->addRoute('GET', '/api/config', 'api.config', true);
|
||||||
|
$this->addRoute('GET', '/api/system', 'api.system', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a route
|
||||||
|
*/
|
||||||
|
public function addRoute(string $method, string $path, string $handler, bool $requireAuth = true): void
|
||||||
|
{
|
||||||
|
$this->routes[] = [
|
||||||
|
'method' => strtoupper($method),
|
||||||
|
'path' => $path,
|
||||||
|
'handler' => $handler,
|
||||||
|
'requireAuth' => $requireAuth,
|
||||||
|
'pattern' => $this->pathToPattern($path),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert path to regex pattern
|
||||||
|
*/
|
||||||
|
private function pathToPattern(string $path): string
|
||||||
|
{
|
||||||
|
$pattern = preg_replace('/\{([^}]+)\}/', '(?P<$1>[^/]+)', $path);
|
||||||
|
return '#^' . $pattern . '$#';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch request to handler
|
||||||
|
*/
|
||||||
|
public function dispatch(Request $request, Response $response): void
|
||||||
|
{
|
||||||
|
$method = $request->getMethod();
|
||||||
|
$path = $request->getPath();
|
||||||
|
|
||||||
|
// Handle empty path
|
||||||
|
if ($path === '' || $path === '/index.php') {
|
||||||
|
$path = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find matching route
|
||||||
|
foreach ($this->routes as $route) {
|
||||||
|
if ($route['method'] === $method && preg_match($route['pattern'], $path, $matches)) {
|
||||||
|
// Check authentication
|
||||||
|
if ($route['requireAuth'] && !$this->auth->isAuthenticated()) {
|
||||||
|
if ($request->expectsJson()) {
|
||||||
|
$response->error('Unauthorized', 401);
|
||||||
|
} else {
|
||||||
|
$response->redirect('/login');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract parameters
|
||||||
|
$params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
|
||||||
|
|
||||||
|
// Execute handler
|
||||||
|
$this->executeHandler($route['handler'], $request, $response, $params);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No route found - 404
|
||||||
|
if ($request->expectsJson()) {
|
||||||
|
$response->error('Not Found', 404);
|
||||||
|
} else {
|
||||||
|
$response->html($this->get404Page(), 404);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute route handler
|
||||||
|
*/
|
||||||
|
private function executeHandler(string $handler, Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
[$module, $action] = explode('.', $handler);
|
||||||
|
|
||||||
|
$controllerClass = 'AleShell2\\Modules\\' . ucfirst($module) . '\\' . ucfirst($module) . 'Controller';
|
||||||
|
|
||||||
|
if (!class_exists($controllerClass)) {
|
||||||
|
$response->error("Controller not found: {$controllerClass}", 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$controller = new $controllerClass($this->config, $this->auth);
|
||||||
|
|
||||||
|
if (!method_exists($controller, $action)) {
|
||||||
|
$response->error("Action not found: {$action}", 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$controller->$action($request, $response, $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get 404 page HTML
|
||||||
|
*/
|
||||||
|
private function get404Page(): string
|
||||||
|
{
|
||||||
|
return '<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>404 - Not Found</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
background: #1a202c;
|
||||||
|
color: #f7fafc;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 6rem;
|
||||||
|
margin: 0;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #a0aec0;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>404</h1>
|
||||||
|
<p>Page not found</p>
|
||||||
|
<a href="/">Return to Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>';
|
||||||
|
}
|
||||||
|
}
|
||||||
247
src/Core/View.php
Archivo normal
247
src/Core/View.php
Archivo normal
@@ -0,0 +1,247 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 View Renderer
|
||||||
|
*
|
||||||
|
* Handles rendering of views and templates.
|
||||||
|
*
|
||||||
|
* @package AleShell2\Core
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace AleShell2\Core;
|
||||||
|
|
||||||
|
class View
|
||||||
|
{
|
||||||
|
private static array $sharedData = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Share data with all views
|
||||||
|
*/
|
||||||
|
public static function share(string $key, mixed $value): void
|
||||||
|
{
|
||||||
|
self::$sharedData[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a view
|
||||||
|
*/
|
||||||
|
public static function render(string $view, array $data = []): string
|
||||||
|
{
|
||||||
|
$viewPath = ALESHELL_SRC . '/Views/' . str_replace('.', '/', $view) . '.php';
|
||||||
|
|
||||||
|
if (!file_exists($viewPath)) {
|
||||||
|
throw new \RuntimeException("View not found: {$view}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge shared data
|
||||||
|
$data = array_merge(self::$sharedData, $data);
|
||||||
|
|
||||||
|
// Extract data to variables
|
||||||
|
extract($data, EXTR_SKIP);
|
||||||
|
|
||||||
|
// Capture output
|
||||||
|
ob_start();
|
||||||
|
include $viewPath;
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a view with layout
|
||||||
|
*/
|
||||||
|
public static function renderWithLayout(string $view, string $layout = 'layouts.main', array $data = []): string
|
||||||
|
{
|
||||||
|
// Render the view content first
|
||||||
|
$data['content'] = self::render($view, $data);
|
||||||
|
|
||||||
|
// Render the layout with the view content
|
||||||
|
return self::render($layout, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Include a partial
|
||||||
|
*/
|
||||||
|
public static function partial(string $partial, array $data = []): string
|
||||||
|
{
|
||||||
|
return self::render('components.' . $partial, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape HTML
|
||||||
|
*/
|
||||||
|
public static function e(string $value): string
|
||||||
|
{
|
||||||
|
return htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format bytes to human readable
|
||||||
|
*/
|
||||||
|
public static function formatBytes(int $bytes, int $precision = 2): string
|
||||||
|
{
|
||||||
|
if ($bytes === 0) {
|
||||||
|
return '0 B';
|
||||||
|
}
|
||||||
|
|
||||||
|
$units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||||
|
$pow = floor(log($bytes, 1024));
|
||||||
|
$pow = min($pow, count($units) - 1);
|
||||||
|
|
||||||
|
return round($bytes / pow(1024, $pow), $precision) . ' ' . $units[$pow];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format timestamp
|
||||||
|
*/
|
||||||
|
public static function formatTime(int $timestamp, string $format = 'Y-m-d H:i:s'): string
|
||||||
|
{
|
||||||
|
return date($format, $timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file icon based on extension
|
||||||
|
*/
|
||||||
|
public static function fileIcon(string $filename, bool $isDirectory = false): string
|
||||||
|
{
|
||||||
|
if ($isDirectory) {
|
||||||
|
return '📁';
|
||||||
|
}
|
||||||
|
|
||||||
|
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
$icons = [
|
||||||
|
// Code
|
||||||
|
'php' => '🐘',
|
||||||
|
'js' => '📜',
|
||||||
|
'ts' => '📘',
|
||||||
|
'py' => '🐍',
|
||||||
|
'rb' => '💎',
|
||||||
|
'java' => '☕',
|
||||||
|
'c' => '⚙️',
|
||||||
|
'cpp' => '⚙️',
|
||||||
|
'h' => '⚙️',
|
||||||
|
'cs' => '🔷',
|
||||||
|
'go' => '🔵',
|
||||||
|
'rs' => '🦀',
|
||||||
|
'swift' => '🍎',
|
||||||
|
|
||||||
|
// Web
|
||||||
|
'html' => '🌐',
|
||||||
|
'htm' => '🌐',
|
||||||
|
'css' => '🎨',
|
||||||
|
'scss' => '🎨',
|
||||||
|
'sass' => '🎨',
|
||||||
|
'less' => '🎨',
|
||||||
|
'vue' => '💚',
|
||||||
|
'jsx' => '⚛️',
|
||||||
|
'tsx' => '⚛️',
|
||||||
|
|
||||||
|
// Data
|
||||||
|
'json' => '📋',
|
||||||
|
'xml' => '📋',
|
||||||
|
'yaml' => '📋',
|
||||||
|
'yml' => '📋',
|
||||||
|
'toml' => '📋',
|
||||||
|
'csv' => '📊',
|
||||||
|
'sql' => '🗃️',
|
||||||
|
|
||||||
|
// Documents
|
||||||
|
'txt' => '📄',
|
||||||
|
'md' => '📝',
|
||||||
|
'pdf' => '📕',
|
||||||
|
'doc' => '📘',
|
||||||
|
'docx' => '📘',
|
||||||
|
'xls' => '📗',
|
||||||
|
'xlsx' => '📗',
|
||||||
|
'ppt' => '📙',
|
||||||
|
'pptx' => '📙',
|
||||||
|
|
||||||
|
// Images
|
||||||
|
'jpg' => '🖼️',
|
||||||
|
'jpeg' => '🖼️',
|
||||||
|
'png' => '🖼️',
|
||||||
|
'gif' => '🖼️',
|
||||||
|
'svg' => '🖼️',
|
||||||
|
'webp' => '🖼️',
|
||||||
|
'ico' => '🖼️',
|
||||||
|
'bmp' => '🖼️',
|
||||||
|
|
||||||
|
// Media
|
||||||
|
'mp3' => '🎵',
|
||||||
|
'wav' => '🎵',
|
||||||
|
'ogg' => '🎵',
|
||||||
|
'flac' => '🎵',
|
||||||
|
'mp4' => '🎬',
|
||||||
|
'avi' => '🎬',
|
||||||
|
'mkv' => '🎬',
|
||||||
|
'mov' => '🎬',
|
||||||
|
|
||||||
|
// Archives
|
||||||
|
'zip' => '📦',
|
||||||
|
'rar' => '📦',
|
||||||
|
'tar' => '📦',
|
||||||
|
'gz' => '📦',
|
||||||
|
'7z' => '📦',
|
||||||
|
|
||||||
|
// Config
|
||||||
|
'env' => '⚙️',
|
||||||
|
'ini' => '⚙️',
|
||||||
|
'conf' => '⚙️',
|
||||||
|
'config' => '⚙️',
|
||||||
|
|
||||||
|
// Executable
|
||||||
|
'sh' => '⚡',
|
||||||
|
'bash' => '⚡',
|
||||||
|
'zsh' => '⚡',
|
||||||
|
'bat' => '⚡',
|
||||||
|
'exe' => '⚡',
|
||||||
|
'bin' => '⚡',
|
||||||
|
];
|
||||||
|
|
||||||
|
return $icons[$ext] ?? '📄';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syntax highlight language based on extension
|
||||||
|
*/
|
||||||
|
public static function highlightLang(string $filename): string
|
||||||
|
{
|
||||||
|
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
$langs = [
|
||||||
|
'php' => 'php',
|
||||||
|
'js' => 'javascript',
|
||||||
|
'ts' => 'typescript',
|
||||||
|
'jsx' => 'jsx',
|
||||||
|
'tsx' => 'tsx',
|
||||||
|
'py' => 'python',
|
||||||
|
'rb' => 'ruby',
|
||||||
|
'java' => 'java',
|
||||||
|
'c' => 'c',
|
||||||
|
'cpp' => 'cpp',
|
||||||
|
'cs' => 'csharp',
|
||||||
|
'go' => 'go',
|
||||||
|
'rs' => 'rust',
|
||||||
|
'swift' => 'swift',
|
||||||
|
'html' => 'html',
|
||||||
|
'htm' => 'html',
|
||||||
|
'css' => 'css',
|
||||||
|
'scss' => 'scss',
|
||||||
|
'sass' => 'sass',
|
||||||
|
'less' => 'less',
|
||||||
|
'json' => 'json',
|
||||||
|
'xml' => 'xml',
|
||||||
|
'yaml' => 'yaml',
|
||||||
|
'yml' => 'yaml',
|
||||||
|
'md' => 'markdown',
|
||||||
|
'sql' => 'sql',
|
||||||
|
'sh' => 'bash',
|
||||||
|
'bash' => 'bash',
|
||||||
|
'zsh' => 'bash',
|
||||||
|
'ini' => 'ini',
|
||||||
|
'env' => 'bash',
|
||||||
|
];
|
||||||
|
|
||||||
|
return $langs[$ext] ?? 'plaintext';
|
||||||
|
}
|
||||||
|
}
|
||||||
223
src/Modules/Api/ApiController.php
Archivo normal
223
src/Modules/Api/ApiController.php
Archivo normal
@@ -0,0 +1,223 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 API Controller
|
||||||
|
*
|
||||||
|
* Handles general API endpoints.
|
||||||
|
*
|
||||||
|
* @package AleShell2\Modules\Api
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace AleShell2\Modules\Api;
|
||||||
|
|
||||||
|
use AleShell2\Modules\BaseController;
|
||||||
|
use AleShell2\Core\Request;
|
||||||
|
use AleShell2\Core\Response;
|
||||||
|
|
||||||
|
class ApiController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* API status endpoint
|
||||||
|
*/
|
||||||
|
public function status(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$this->success($response, [
|
||||||
|
'name' => 'AleShell2',
|
||||||
|
'version' => '1.0.0',
|
||||||
|
'status' => 'operational',
|
||||||
|
'php_version' => PHP_VERSION,
|
||||||
|
'server_time' => date('Y-m-d H:i:s'),
|
||||||
|
'timezone' => date_default_timezone_get(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get CSRF token
|
||||||
|
*/
|
||||||
|
public function csrf(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$this->success($response, [
|
||||||
|
'csrf_token' => $this->app->getAuth()->generateCsrfToken(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health check endpoint
|
||||||
|
*/
|
||||||
|
public function health(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$checks = [
|
||||||
|
'application' => true,
|
||||||
|
'session' => session_status() === PHP_SESSION_ACTIVE,
|
||||||
|
'writable_temp' => is_writable(sys_get_temp_dir()),
|
||||||
|
];
|
||||||
|
|
||||||
|
$healthy = !in_array(false, $checks, true);
|
||||||
|
|
||||||
|
$response->json([
|
||||||
|
'healthy' => $healthy,
|
||||||
|
'checks' => $checks,
|
||||||
|
'timestamp' => time(),
|
||||||
|
], $healthy ? 200 : 503);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quick command execution API
|
||||||
|
*/
|
||||||
|
public function exec(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
if (!$this->validateCsrf($request, $response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$command = $request->post('command');
|
||||||
|
|
||||||
|
if (!$command) {
|
||||||
|
$this->error($response, 'Command is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$output = shell_exec($command . ' 2>&1');
|
||||||
|
|
||||||
|
$this->success($response, [
|
||||||
|
'command' => $command,
|
||||||
|
'output' => $output ?? '',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PHP eval endpoint
|
||||||
|
*/
|
||||||
|
public function eval(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
if (!$this->validateCsrf($request, $response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = $request->post('code');
|
||||||
|
|
||||||
|
if (!$code) {
|
||||||
|
$this->error($response, 'Code is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture output
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = eval($code);
|
||||||
|
$output = ob_get_clean();
|
||||||
|
|
||||||
|
$this->success($response, [
|
||||||
|
'result' => $result,
|
||||||
|
'output' => $output,
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
ob_end_clean();
|
||||||
|
$this->error($response, 'Eval error: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File upload API
|
||||||
|
*/
|
||||||
|
public function upload(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
if (!$this->validateCsrf($request, $response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$destination = $request->post('destination');
|
||||||
|
|
||||||
|
if (!$destination) {
|
||||||
|
$this->error($response, 'Destination path is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($_FILES['file'])) {
|
||||||
|
$this->error($response, 'No file uploaded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $_FILES['file'];
|
||||||
|
|
||||||
|
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
$this->error($response, 'Upload error: ' . $this->getUploadErrorMessage($file['error']));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetPath = rtrim($destination, '/') . '/' . basename($file['name']);
|
||||||
|
|
||||||
|
if (!is_dir($destination)) {
|
||||||
|
$this->error($response, 'Destination directory does not exist');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_writable($destination)) {
|
||||||
|
$this->error($response, 'Destination directory is not writable');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
|
||||||
|
$this->success($response, [
|
||||||
|
'message' => 'File uploaded successfully',
|
||||||
|
'path' => $targetPath,
|
||||||
|
'size' => $file['size'],
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$this->error($response, 'Failed to move uploaded file');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download file API
|
||||||
|
*/
|
||||||
|
public function download(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$path = $request->get('path');
|
||||||
|
|
||||||
|
if (!$path) {
|
||||||
|
$this->error($response, 'Path is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
$this->error($response, 'File not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_file($path)) {
|
||||||
|
$this->error($response, 'Not a file');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_readable($path)) {
|
||||||
|
$this->error($response, 'File not readable');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($path);
|
||||||
|
$filename = basename($path);
|
||||||
|
$contentType = mime_content_type($path) ?: 'application/octet-stream';
|
||||||
|
|
||||||
|
$response->download($content, $filename, $contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get upload error message
|
||||||
|
*/
|
||||||
|
private function getUploadErrorMessage(int $error): string
|
||||||
|
{
|
||||||
|
return match($error) {
|
||||||
|
UPLOAD_ERR_INI_SIZE => 'File exceeds upload_max_filesize',
|
||||||
|
UPLOAD_ERR_FORM_SIZE => 'File exceeds MAX_FILE_SIZE',
|
||||||
|
UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
|
||||||
|
UPLOAD_ERR_NO_FILE => 'No file was uploaded',
|
||||||
|
UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder',
|
||||||
|
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
|
||||||
|
UPLOAD_ERR_EXTENSION => 'Upload stopped by extension',
|
||||||
|
default => 'Unknown upload error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/Modules/Auth/AuthController.php
Archivo normal
82
src/Modules/Auth/AuthController.php
Archivo normal
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Auth Controller
|
||||||
|
*
|
||||||
|
* Handles authentication routes.
|
||||||
|
*
|
||||||
|
* @package AleShell2\Modules\Auth
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace AleShell2\Modules\Auth;
|
||||||
|
|
||||||
|
use AleShell2\Modules\BaseController;
|
||||||
|
use AleShell2\Core\Request;
|
||||||
|
use AleShell2\Core\Response;
|
||||||
|
use AleShell2\Core\View;
|
||||||
|
|
||||||
|
class AuthController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show login form or process login
|
||||||
|
*/
|
||||||
|
public function login(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
// If already authenticated, redirect to dashboard
|
||||||
|
if ($this->auth->isAuthenticated()) {
|
||||||
|
$response->redirect('/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = '';
|
||||||
|
|
||||||
|
// Process login form
|
||||||
|
if ($request->isMethod('POST')) {
|
||||||
|
$password = $request->post('password', '');
|
||||||
|
|
||||||
|
if (empty($password)) {
|
||||||
|
$error = 'Password is required';
|
||||||
|
} elseif ($this->auth->isRateLimited()) {
|
||||||
|
$remaining = $this->auth->getRemainingLockoutTime();
|
||||||
|
$error = "Too many failed attempts. Try again in {$remaining} seconds.";
|
||||||
|
} elseif ($this->auth->attempt($password)) {
|
||||||
|
$response->redirect('/');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
$error = 'Invalid password';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render login page
|
||||||
|
$html = View::render('auth.login', [
|
||||||
|
'error' => $error,
|
||||||
|
'version' => ALESHELL_VERSION,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->html($html);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process logout
|
||||||
|
*/
|
||||||
|
public function logout(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$this->auth->logout();
|
||||||
|
$response->redirect('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get authentication status (AJAX)
|
||||||
|
*/
|
||||||
|
public function status(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$response->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'authenticated' => $this->auth->isAuthenticated(),
|
||||||
|
'csrf_token' => $this->auth->getCsrfToken(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
230
src/Modules/BaseController.php
Archivo normal
230
src/Modules/BaseController.php
Archivo normal
@@ -0,0 +1,230 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Base Controller
|
||||||
|
*
|
||||||
|
* Base class for all module controllers.
|
||||||
|
*
|
||||||
|
* @package AleShell2\Modules
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace AleShell2\Modules;
|
||||||
|
|
||||||
|
use AleShell2\Core\Request;
|
||||||
|
use AleShell2\Core\Response;
|
||||||
|
use AleShell2\Core\View;
|
||||||
|
use AleShell2\Security\Auth;
|
||||||
|
|
||||||
|
abstract class BaseController
|
||||||
|
{
|
||||||
|
protected array $config;
|
||||||
|
protected Auth $auth;
|
||||||
|
|
||||||
|
public function __construct(array $config, Auth $auth)
|
||||||
|
{
|
||||||
|
$this->config = $config;
|
||||||
|
$this->auth = $auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a view with layout
|
||||||
|
*/
|
||||||
|
protected function render(Response $response, string $view, array $data = []): void
|
||||||
|
{
|
||||||
|
$data['auth'] = $this->auth;
|
||||||
|
$data['config'] = $this->config;
|
||||||
|
$data['version'] = ALESHELL_VERSION;
|
||||||
|
$data['csrfToken'] = $this->auth->getCsrfToken();
|
||||||
|
|
||||||
|
$html = View::renderWithLayout($view, 'layouts.main', $data);
|
||||||
|
$response->html($html);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render just a view without layout
|
||||||
|
*/
|
||||||
|
protected function renderPartial(Response $response, string $view, array $data = []): void
|
||||||
|
{
|
||||||
|
$data['auth'] = $this->auth;
|
||||||
|
$data['config'] = $this->config;
|
||||||
|
|
||||||
|
$html = View::render($view, $data);
|
||||||
|
$response->html($html);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send JSON success response
|
||||||
|
*/
|
||||||
|
protected function success(Response $response, mixed $data = null, string $message = 'Success'): void
|
||||||
|
{
|
||||||
|
$response->success($data, $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send JSON error response
|
||||||
|
*/
|
||||||
|
protected function error(Response $response, string $message, int $code = 400): void
|
||||||
|
{
|
||||||
|
$response->error($message, $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate CSRF token for state-changing requests
|
||||||
|
*/
|
||||||
|
protected function validateCsrf(Request $request, Response $response): bool
|
||||||
|
{
|
||||||
|
if (!($this->config['security']['csrf_protection'] ?? true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $request->header('X-CSRF-Token')
|
||||||
|
?? $request->post('_token')
|
||||||
|
?? $request->input('csrf_token');
|
||||||
|
|
||||||
|
if (!$token || !$this->auth->validateCsrfToken($token)) {
|
||||||
|
$this->error($response, 'Invalid CSRF token', 403);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if path is allowed
|
||||||
|
*/
|
||||||
|
protected function isPathAllowed(string $path): bool
|
||||||
|
{
|
||||||
|
$restrictedPaths = $this->config['security']['restricted_paths'] ?? [];
|
||||||
|
|
||||||
|
if (empty($restrictedPaths)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$realPath = realpath($path);
|
||||||
|
if ($realPath === false) {
|
||||||
|
return true; // Allow non-existent paths for creation
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($restrictedPaths as $restricted) {
|
||||||
|
if (strpos($realPath, $restricted) === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if command is allowed
|
||||||
|
*/
|
||||||
|
protected function isCommandAllowed(string $command): bool
|
||||||
|
{
|
||||||
|
$blockedCommands = $this->config['security']['blocked_commands'] ?? [];
|
||||||
|
|
||||||
|
foreach ($blockedCommands as $blocked) {
|
||||||
|
if (stripos($command, $blocked) !== false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format bytes to human readable
|
||||||
|
*/
|
||||||
|
protected function formatBytes(int $bytes, int $precision = 2): string
|
||||||
|
{
|
||||||
|
return View::formatBytes($bytes, $precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get system info
|
||||||
|
*/
|
||||||
|
protected function getSystemInfo(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'hostname' => gethostname(),
|
||||||
|
'os' => php_uname('s') . ' ' . php_uname('r'),
|
||||||
|
'php_version' => PHP_VERSION,
|
||||||
|
'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown',
|
||||||
|
'current_path' => getcwd(),
|
||||||
|
'disk_free' => disk_free_space('.'),
|
||||||
|
'disk_total' => disk_total_space('.'),
|
||||||
|
'load_average' => function_exists('sys_getloadavg') ? sys_getloadavg() : null,
|
||||||
|
'uptime' => $this->getUptime(),
|
||||||
|
'memory' => $this->getMemoryInfo(),
|
||||||
|
'time' => date('Y-m-d H:i:s'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get server uptime
|
||||||
|
*/
|
||||||
|
protected function getUptime(): ?string
|
||||||
|
{
|
||||||
|
if (file_exists('/proc/uptime')) {
|
||||||
|
$uptime = (float)file_get_contents('/proc/uptime');
|
||||||
|
return $this->formatUptime($uptime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try 'uptime' command on Unix
|
||||||
|
if (PHP_OS_FAMILY !== 'Windows') {
|
||||||
|
$output = @shell_exec('uptime -p 2>/dev/null');
|
||||||
|
if ($output) {
|
||||||
|
return trim($output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format uptime seconds to human readable
|
||||||
|
*/
|
||||||
|
protected function formatUptime(float $seconds): string
|
||||||
|
{
|
||||||
|
$days = floor($seconds / 86400);
|
||||||
|
$hours = floor(($seconds % 86400) / 3600);
|
||||||
|
$minutes = floor(($seconds % 3600) / 60);
|
||||||
|
|
||||||
|
$parts = [];
|
||||||
|
if ($days > 0) $parts[] = "{$days}d";
|
||||||
|
if ($hours > 0) $parts[] = "{$hours}h";
|
||||||
|
if ($minutes > 0) $parts[] = "{$minutes}m";
|
||||||
|
|
||||||
|
return implode(' ', $parts) ?: '< 1m';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get memory info
|
||||||
|
*/
|
||||||
|
protected function getMemoryInfo(): array
|
||||||
|
{
|
||||||
|
$memory = [
|
||||||
|
'total' => 0,
|
||||||
|
'free' => 0,
|
||||||
|
'used' => 0,
|
||||||
|
'cached' => 0,
|
||||||
|
'percent' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (file_exists('/proc/meminfo')) {
|
||||||
|
$meminfo = file_get_contents('/proc/meminfo');
|
||||||
|
preg_match_all('/^(\w+):\s+(\d+)\s+kB$/m', $meminfo, $matches);
|
||||||
|
|
||||||
|
$data = array_combine($matches[1], $matches[2]);
|
||||||
|
|
||||||
|
$memory['total'] = ($data['MemTotal'] ?? 0) * 1024;
|
||||||
|
$memory['free'] = ($data['MemFree'] ?? 0) * 1024;
|
||||||
|
$memory['cached'] = ($data['Cached'] ?? 0) * 1024;
|
||||||
|
$memory['used'] = $memory['total'] - $memory['free'] - $memory['cached'];
|
||||||
|
$memory['percent'] = $memory['total'] > 0
|
||||||
|
? round(($memory['used'] / $memory['total']) * 100, 1)
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $memory;
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/Modules/Dashboard/DashboardController.php
Archivo normal
90
src/Modules/Dashboard/DashboardController.php
Archivo normal
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Dashboard Controller
|
||||||
|
*
|
||||||
|
* Handles dashboard module.
|
||||||
|
*
|
||||||
|
* @package AleShell2\Modules\Dashboard
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace AleShell2\Modules\Dashboard;
|
||||||
|
|
||||||
|
use AleShell2\Modules\BaseController;
|
||||||
|
use AleShell2\Core\Request;
|
||||||
|
use AleShell2\Core\Response;
|
||||||
|
|
||||||
|
class DashboardController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show dashboard
|
||||||
|
*/
|
||||||
|
public function index(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
// If AJAX request, return JSON
|
||||||
|
if ($request->isAjax()) {
|
||||||
|
$this->success($response, $this->getDashboardData());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render dashboard page
|
||||||
|
$this->render($response, 'modules.dashboard', [
|
||||||
|
'currentModule' => 'dashboard',
|
||||||
|
'pageTitle' => 'Dashboard - AleShell2',
|
||||||
|
'data' => $this->getDashboardData(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get dashboard data
|
||||||
|
*/
|
||||||
|
private function getDashboardData(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'system' => $this->getSystemInfo(),
|
||||||
|
'user' => $this->getUserInfo(),
|
||||||
|
'modules' => $this->getEnabledModules(),
|
||||||
|
'features' => $this->config['features'] ?? [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user session info
|
||||||
|
*/
|
||||||
|
private function getUserInfo(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'ip' => $this->auth->getUserIP(),
|
||||||
|
'login_time' => $this->auth->getLoginTime(),
|
||||||
|
'last_activity' => $this->auth->getLastActivity(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of enabled modules
|
||||||
|
*/
|
||||||
|
private function getEnabledModules(): array
|
||||||
|
{
|
||||||
|
$features = $this->config['features'] ?? [];
|
||||||
|
$modules = [];
|
||||||
|
|
||||||
|
$moduleInfo = [
|
||||||
|
'file_manager' => ['name' => 'Files', 'icon' => '📁', 'path' => '/files'],
|
||||||
|
'terminal' => ['name' => 'Terminal', 'icon' => '💻', 'path' => '/terminal'],
|
||||||
|
'code_editor' => ['name' => 'Editor', 'icon' => '📝', 'path' => '/editor'],
|
||||||
|
'process_manager' => ['name' => 'Processes', 'icon' => '⚙️', 'path' => '/processes'],
|
||||||
|
'network_tools' => ['name' => 'Network', 'icon' => '🌐', 'path' => '/network'],
|
||||||
|
'database_tools' => ['name' => 'Database', 'icon' => '🗄️', 'path' => '/database'],
|
||||||
|
'system_info' => ['name' => 'System', 'icon' => '📊', 'path' => '/system'],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($moduleInfo as $key => $info) {
|
||||||
|
if ($features[$key] ?? true) {
|
||||||
|
$modules[$key] = $info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $modules;
|
||||||
|
}
|
||||||
|
}
|
||||||
472
src/Modules/Database/DatabaseController.php
Archivo normal
472
src/Modules/Database/DatabaseController.php
Archivo normal
@@ -0,0 +1,472 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Database Controller
|
||||||
|
*
|
||||||
|
* Handles database management module.
|
||||||
|
*
|
||||||
|
* @package AleShell2\Modules\Database
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace AleShell2\Modules\Database;
|
||||||
|
|
||||||
|
use AleShell2\Modules\BaseController;
|
||||||
|
use AleShell2\Core\Request;
|
||||||
|
use AleShell2\Core\Response;
|
||||||
|
|
||||||
|
class DatabaseController extends BaseController
|
||||||
|
{
|
||||||
|
private ?\PDO $connection = null;
|
||||||
|
private ?string $driver = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show database module
|
||||||
|
*/
|
||||||
|
public function index(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$this->render($response, 'modules.database', [
|
||||||
|
'currentModule' => 'database',
|
||||||
|
'pageTitle' => 'Database Manager - AleShell2',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to database
|
||||||
|
*/
|
||||||
|
public function connect(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
if (!$this->validateCsrf($request, $response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$driver = $request->post('driver', 'mysql');
|
||||||
|
$host = $request->post('host', 'localhost');
|
||||||
|
$port = (int)$request->post('port', 3306);
|
||||||
|
$database = $request->post('database', '');
|
||||||
|
$username = $request->post('username', '');
|
||||||
|
$password = $request->post('password', '');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$connection = $this->createConnection($driver, $host, $port, $database, $username, $password);
|
||||||
|
|
||||||
|
// Store connection in session
|
||||||
|
$_SESSION['db_connection'] = [
|
||||||
|
'driver' => $driver,
|
||||||
|
'host' => $host,
|
||||||
|
'port' => $port,
|
||||||
|
'database' => $database,
|
||||||
|
'username' => $username,
|
||||||
|
'password' => $password,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Get databases list
|
||||||
|
$databases = $this->getDatabases($connection, $driver);
|
||||||
|
|
||||||
|
$this->success($response, [
|
||||||
|
'message' => 'Connected successfully',
|
||||||
|
'databases' => $databases,
|
||||||
|
'current_database' => $database,
|
||||||
|
]);
|
||||||
|
} catch (\PDOException $e) {
|
||||||
|
$this->error($response, 'Connection failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect from database
|
||||||
|
*/
|
||||||
|
public function disconnect(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
unset($_SESSION['db_connection']);
|
||||||
|
$this->success($response, ['message' => 'Disconnected']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tables in database
|
||||||
|
*/
|
||||||
|
public function tables(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$connection = $this->getSessionConnection();
|
||||||
|
|
||||||
|
if (!$connection) {
|
||||||
|
$this->error($response, 'Not connected to database');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$database = $request->get('database', $_SESSION['db_connection']['database'] ?? '');
|
||||||
|
|
||||||
|
if ($database) {
|
||||||
|
$connection->exec("USE " . $this->quoteIdentifier($database, $_SESSION['db_connection']['driver']));
|
||||||
|
}
|
||||||
|
|
||||||
|
$tables = $this->getTablesList($connection, $_SESSION['db_connection']['driver']);
|
||||||
|
|
||||||
|
$this->success($response, ['tables' => $tables]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($response, 'Failed to get tables: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get table structure
|
||||||
|
*/
|
||||||
|
public function structure(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$connection = $this->getSessionConnection();
|
||||||
|
|
||||||
|
if (!$connection) {
|
||||||
|
$this->error($response, 'Not connected to database');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $request->get('table');
|
||||||
|
|
||||||
|
if (!$table) {
|
||||||
|
$this->error($response, 'Table name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$structure = $this->getTableStructure($connection, $table, $_SESSION['db_connection']['driver']);
|
||||||
|
|
||||||
|
$this->success($response, ['structure' => $structure]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($response, 'Failed to get structure: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute query
|
||||||
|
*/
|
||||||
|
public function query(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
if (!$this->validateCsrf($request, $response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$connection = $this->getSessionConnection();
|
||||||
|
|
||||||
|
if (!$connection) {
|
||||||
|
$this->error($response, 'Not connected to database');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = $request->post('query');
|
||||||
|
$limit = min((int)$request->post('limit', 100), 1000);
|
||||||
|
|
||||||
|
if (!$sql) {
|
||||||
|
$this->error($response, 'Query is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic SQL injection prevention - disallow multiple statements
|
||||||
|
$sql = trim($sql);
|
||||||
|
|
||||||
|
// Check if it's a SELECT query
|
||||||
|
$isSelect = preg_match('/^\s*SELECT\s/i', $sql);
|
||||||
|
|
||||||
|
$startTime = microtime(true);
|
||||||
|
$stmt = $connection->prepare($sql);
|
||||||
|
$stmt->execute();
|
||||||
|
$executionTime = round((microtime(true) - $startTime) * 1000, 2);
|
||||||
|
|
||||||
|
$result = [
|
||||||
|
'execution_time' => $executionTime . ' ms',
|
||||||
|
'affected_rows' => $stmt->rowCount(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($isSelect) {
|
||||||
|
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
$result['columns'] = $rows ? array_keys($rows[0]) : [];
|
||||||
|
$result['rows'] = array_slice($rows, 0, $limit);
|
||||||
|
$result['total_rows'] = count($rows);
|
||||||
|
$result['limited'] = count($rows) > $limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->success($response, $result);
|
||||||
|
} catch (\PDOException $e) {
|
||||||
|
$this->error($response, 'Query failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select database
|
||||||
|
*/
|
||||||
|
public function selectDb(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$connection = $this->getSessionConnection();
|
||||||
|
|
||||||
|
if (!$connection) {
|
||||||
|
$this->error($response, 'Not connected to database');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$database = $request->post('database');
|
||||||
|
|
||||||
|
if (!$database) {
|
||||||
|
$this->error($response, 'Database name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$driver = $_SESSION['db_connection']['driver'];
|
||||||
|
$connection->exec("USE " . $this->quoteIdentifier($database, $driver));
|
||||||
|
|
||||||
|
$_SESSION['db_connection']['database'] = $database;
|
||||||
|
|
||||||
|
$tables = $this->getTablesList($connection, $driver);
|
||||||
|
|
||||||
|
$this->success($response, [
|
||||||
|
'message' => "Selected database: {$database}",
|
||||||
|
'tables' => $tables,
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($response, 'Failed to select database: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export table
|
||||||
|
*/
|
||||||
|
public function export(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$connection = $this->getSessionConnection();
|
||||||
|
|
||||||
|
if (!$connection) {
|
||||||
|
$this->error($response, 'Not connected to database');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $request->get('table');
|
||||||
|
$format = $request->get('format', 'sql');
|
||||||
|
|
||||||
|
if (!$table) {
|
||||||
|
$this->error($response, 'Table name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$driver = $_SESSION['db_connection']['driver'];
|
||||||
|
|
||||||
|
// Get table data
|
||||||
|
$stmt = $connection->query("SELECT * FROM " . $this->quoteIdentifier($table, $driver));
|
||||||
|
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($format === 'csv') {
|
||||||
|
$content = $this->exportToCsv($rows, $table);
|
||||||
|
$filename = "{$table}.csv";
|
||||||
|
$contentType = 'text/csv';
|
||||||
|
} else {
|
||||||
|
$content = $this->exportToSql($connection, $rows, $table, $driver);
|
||||||
|
$filename = "{$table}.sql";
|
||||||
|
$contentType = 'application/sql';
|
||||||
|
}
|
||||||
|
|
||||||
|
$response->download($content, $filename, $contentType);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($response, 'Export failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create PDO connection
|
||||||
|
*/
|
||||||
|
private function createConnection(string $driver, string $host, int $port, string $database, string $username, string $password): \PDO
|
||||||
|
{
|
||||||
|
$dsn = match($driver) {
|
||||||
|
'mysql' => "mysql:host={$host};port={$port}" . ($database ? ";dbname={$database}" : '') . ";charset=utf8mb4",
|
||||||
|
'pgsql' => "pgsql:host={$host};port={$port}" . ($database ? ";dbname={$database}" : ''),
|
||||||
|
'sqlite' => "sqlite:{$database}",
|
||||||
|
default => throw new \InvalidArgumentException("Unsupported driver: {$driver}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
|
||||||
|
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
|
||||||
|
\PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($driver === 'sqlite') {
|
||||||
|
return new \PDO($dsn, null, null, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new \PDO($dsn, $username, $password, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get connection from session
|
||||||
|
*/
|
||||||
|
private function getSessionConnection(): ?\PDO
|
||||||
|
{
|
||||||
|
if (!isset($_SESSION['db_connection'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$conn = $_SESSION['db_connection'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $this->createConnection(
|
||||||
|
$conn['driver'],
|
||||||
|
$conn['host'],
|
||||||
|
$conn['port'],
|
||||||
|
$conn['database'],
|
||||||
|
$conn['username'],
|
||||||
|
$conn['password']
|
||||||
|
);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
unset($_SESSION['db_connection']);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get databases list
|
||||||
|
*/
|
||||||
|
private function getDatabases(\PDO $connection, string $driver): array
|
||||||
|
{
|
||||||
|
$databases = [];
|
||||||
|
|
||||||
|
$query = match($driver) {
|
||||||
|
'mysql' => "SHOW DATABASES",
|
||||||
|
'pgsql' => "SELECT datname FROM pg_database WHERE datistemplate = false",
|
||||||
|
'sqlite' => null,
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($query) {
|
||||||
|
$stmt = $connection->query($query);
|
||||||
|
while ($row = $stmt->fetch(\PDO::FETCH_NUM)) {
|
||||||
|
$databases[] = $row[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $databases;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tables list
|
||||||
|
*/
|
||||||
|
private function getTablesList(\PDO $connection, string $driver): array
|
||||||
|
{
|
||||||
|
$tables = [];
|
||||||
|
|
||||||
|
$query = match($driver) {
|
||||||
|
'mysql' => "SHOW TABLES",
|
||||||
|
'pgsql' => "SELECT tablename FROM pg_tables WHERE schemaname = 'public'",
|
||||||
|
'sqlite' => "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($query) {
|
||||||
|
$stmt = $connection->query($query);
|
||||||
|
while ($row = $stmt->fetch(\PDO::FETCH_NUM)) {
|
||||||
|
$tables[] = $row[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tables;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get table structure
|
||||||
|
*/
|
||||||
|
private function getTableStructure(\PDO $connection, string $table, string $driver): array
|
||||||
|
{
|
||||||
|
$structure = [];
|
||||||
|
$quotedTable = $this->quoteIdentifier($table, $driver);
|
||||||
|
|
||||||
|
$query = match($driver) {
|
||||||
|
'mysql' => "DESCRIBE {$quotedTable}",
|
||||||
|
'pgsql' => "SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = '{$table}'",
|
||||||
|
'sqlite' => "PRAGMA table_info({$quotedTable})",
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($query) {
|
||||||
|
$stmt = $connection->query($query);
|
||||||
|
$structure = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $structure;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quote identifier based on driver
|
||||||
|
*/
|
||||||
|
private function quoteIdentifier(string $identifier, string $driver): string
|
||||||
|
{
|
||||||
|
// Simple sanitization
|
||||||
|
$identifier = preg_replace('/[^a-zA-Z0-9_]/', '', $identifier);
|
||||||
|
|
||||||
|
return match($driver) {
|
||||||
|
'mysql' => "`{$identifier}`",
|
||||||
|
'pgsql' => "\"{$identifier}\"",
|
||||||
|
'sqlite' => "\"{$identifier}\"",
|
||||||
|
default => $identifier,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export to CSV
|
||||||
|
*/
|
||||||
|
private function exportToCsv(array $rows, string $table): string
|
||||||
|
{
|
||||||
|
if (empty($rows)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$output = fopen('php://temp', 'r+');
|
||||||
|
|
||||||
|
// Header
|
||||||
|
fputcsv($output, array_keys($rows[0]));
|
||||||
|
|
||||||
|
// Data
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
fputcsv($output, $row);
|
||||||
|
}
|
||||||
|
|
||||||
|
rewind($output);
|
||||||
|
$content = stream_get_contents($output);
|
||||||
|
fclose($output);
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export to SQL
|
||||||
|
*/
|
||||||
|
private function exportToSql(\PDO $connection, array $rows, string $table, string $driver): string
|
||||||
|
{
|
||||||
|
$output = "-- Export of table: {$table}\n";
|
||||||
|
$output .= "-- Generated by AleShell2\n";
|
||||||
|
$output .= "-- Date: " . date('Y-m-d H:i:s') . "\n\n";
|
||||||
|
|
||||||
|
if (empty($rows)) {
|
||||||
|
return $output . "-- No data\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$quotedTable = $this->quoteIdentifier($table, $driver);
|
||||||
|
$columns = array_keys($rows[0]);
|
||||||
|
$quotedColumns = array_map(fn($c) => $this->quoteIdentifier($c, $driver), $columns);
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$values = [];
|
||||||
|
foreach ($row as $value) {
|
||||||
|
if ($value === null) {
|
||||||
|
$values[] = 'NULL';
|
||||||
|
} elseif (is_numeric($value)) {
|
||||||
|
$values[] = $value;
|
||||||
|
} else {
|
||||||
|
$values[] = $connection->quote($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$output .= "INSERT INTO {$quotedTable} (" . implode(', ', $quotedColumns) . ") VALUES (" . implode(', ', $values) . ");\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/Modules/Editor/EditorController.php
Archivo normal
87
src/Modules/Editor/EditorController.php
Archivo normal
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Editor Controller
|
||||||
|
*
|
||||||
|
* Handles code editor module.
|
||||||
|
*
|
||||||
|
* @package AleShell2\Modules\Editor
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace AleShell2\Modules\Editor;
|
||||||
|
|
||||||
|
use AleShell2\Modules\BaseController;
|
||||||
|
use AleShell2\Core\Request;
|
||||||
|
use AleShell2\Core\Response;
|
||||||
|
|
||||||
|
class EditorController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show editor
|
||||||
|
*/
|
||||||
|
public function index(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$file = $request->get('file');
|
||||||
|
$content = '';
|
||||||
|
|
||||||
|
if ($file && is_file($file) && $this->isPathAllowed($file)) {
|
||||||
|
$maxSize = $this->config['limits']['max_file_size'] ?? 50 * 1024 * 1024;
|
||||||
|
|
||||||
|
if (filesize($file) <= $maxSize) {
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->render($response, 'modules.editor', [
|
||||||
|
'currentModule' => 'editor',
|
||||||
|
'pageTitle' => 'Code Editor - AleShell2',
|
||||||
|
'currentFile' => $file,
|
||||||
|
'content' => $content,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save file
|
||||||
|
*/
|
||||||
|
public function save(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
if (!$this->validateCsrf($request, $response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $request->post('path');
|
||||||
|
$content = $request->post('content', '');
|
||||||
|
|
||||||
|
if (!$path) {
|
||||||
|
$this->error($response, 'File path is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isPathAllowed($path)) {
|
||||||
|
$this->error($response, 'Access denied', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
$dir = dirname($path);
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
mkdir($dir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$written = file_put_contents($path, $content);
|
||||||
|
|
||||||
|
if ($written === false) {
|
||||||
|
throw new \Exception('Failed to write file');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->success($response, [
|
||||||
|
'path' => $path,
|
||||||
|
'size' => $written,
|
||||||
|
], 'File saved successfully');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($response, 'Failed to save: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
617
src/Modules/Files/FilesController.php
Archivo normal
617
src/Modules/Files/FilesController.php
Archivo normal
@@ -0,0 +1,617 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Files Controller
|
||||||
|
*
|
||||||
|
* Handles file manager module.
|
||||||
|
*
|
||||||
|
* @package AleShell2\Modules\Files
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace AleShell2\Modules\Files;
|
||||||
|
|
||||||
|
use AleShell2\Modules\BaseController;
|
||||||
|
use AleShell2\Core\Request;
|
||||||
|
use AleShell2\Core\Response;
|
||||||
|
|
||||||
|
class FilesController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show file manager
|
||||||
|
*/
|
||||||
|
public function index(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$path = $request->get('path', getcwd());
|
||||||
|
|
||||||
|
$this->render($response, 'modules.files', [
|
||||||
|
'currentModule' => 'files',
|
||||||
|
'pageTitle' => 'File Manager - AleShell2',
|
||||||
|
'currentPath' => realpath($path) ?: getcwd(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List directory contents
|
||||||
|
*/
|
||||||
|
public function list(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$path = $request->post('path', getcwd());
|
||||||
|
$showHidden = (bool)$request->post('hidden', false);
|
||||||
|
$sortBy = $request->post('sort', 'name');
|
||||||
|
$sortOrder = $request->post('order', 'asc');
|
||||||
|
|
||||||
|
if (!$this->isPathAllowed($path)) {
|
||||||
|
$this->error($response, 'Access denied to this path', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_dir($path)) {
|
||||||
|
$this->error($response, 'Path is not a directory');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$files = $this->scanDirectory($path, $showHidden);
|
||||||
|
$files = $this->sortFiles($files, $sortBy, $sortOrder);
|
||||||
|
|
||||||
|
$this->success($response, [
|
||||||
|
'path' => realpath($path),
|
||||||
|
'parent' => dirname($path),
|
||||||
|
'files' => $files,
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($response, 'Failed to list directory: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read file contents
|
||||||
|
*/
|
||||||
|
public function read(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$path = $request->post('path');
|
||||||
|
|
||||||
|
if (!$path) {
|
||||||
|
$this->error($response, 'Path is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isPathAllowed($path)) {
|
||||||
|
$this->error($response, 'Access denied', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_file($path)) {
|
||||||
|
$this->error($response, 'File not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$maxSize = $this->config['limits']['max_file_size'] ?? 50 * 1024 * 1024;
|
||||||
|
$size = filesize($path);
|
||||||
|
|
||||||
|
if ($size > $maxSize) {
|
||||||
|
$this->error($response, 'File is too large to read');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($path);
|
||||||
|
$isBinary = $this->isBinary($content);
|
||||||
|
|
||||||
|
$this->success($response, [
|
||||||
|
'path' => $path,
|
||||||
|
'name' => basename($path),
|
||||||
|
'size' => $size,
|
||||||
|
'content' => $isBinary ? base64_encode($content) : $content,
|
||||||
|
'binary' => $isBinary,
|
||||||
|
'mime' => mime_content_type($path) ?: 'application/octet-stream',
|
||||||
|
'modified' => filemtime($path),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write file contents
|
||||||
|
*/
|
||||||
|
public function write(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
if (!$this->validateCsrf($request, $response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $request->post('path');
|
||||||
|
$content = $request->post('content', '');
|
||||||
|
|
||||||
|
if (!$path) {
|
||||||
|
$this->error($response, 'Path is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isPathAllowed($path)) {
|
||||||
|
$this->error($response, 'Access denied', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$written = file_put_contents($path, $content);
|
||||||
|
|
||||||
|
if ($written === false) {
|
||||||
|
throw new \Exception('Failed to write file');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->success($response, [
|
||||||
|
'path' => $path,
|
||||||
|
'size' => $written,
|
||||||
|
], 'File saved successfully');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($response, 'Failed to save file: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete file or directory
|
||||||
|
*/
|
||||||
|
public function delete(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
if (!$this->validateCsrf($request, $response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $request->post('path');
|
||||||
|
|
||||||
|
if (!$path) {
|
||||||
|
$this->error($response, 'Path is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isPathAllowed($path)) {
|
||||||
|
$this->error($response, 'Access denied', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
$this->error($response, 'File not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (is_dir($path)) {
|
||||||
|
$this->deleteDirectory($path);
|
||||||
|
} else {
|
||||||
|
unlink($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->success($response, null, 'Deleted successfully');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($response, 'Failed to delete: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create file
|
||||||
|
*/
|
||||||
|
public function create(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
if (!$this->validateCsrf($request, $response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $request->post('path');
|
||||||
|
$content = $request->post('content', '');
|
||||||
|
|
||||||
|
if (!$path) {
|
||||||
|
$this->error($response, 'Path is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isPathAllowed($path)) {
|
||||||
|
$this->error($response, 'Access denied', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_exists($path)) {
|
||||||
|
$this->error($response, 'File already exists');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
file_put_contents($path, $content);
|
||||||
|
$this->success($response, ['path' => $path], 'File created successfully');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($response, 'Failed to create file: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create directory
|
||||||
|
*/
|
||||||
|
public function mkdir(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
if (!$this->validateCsrf($request, $response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $request->post('path');
|
||||||
|
|
||||||
|
if (!$path) {
|
||||||
|
$this->error($response, 'Path is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isPathAllowed($path)) {
|
||||||
|
$this->error($response, 'Access denied', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_exists($path)) {
|
||||||
|
$this->error($response, 'Directory already exists');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
mkdir($path, 0755, true);
|
||||||
|
$this->success($response, ['path' => $path], 'Directory created successfully');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($response, 'Failed to create directory: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename file or directory
|
||||||
|
*/
|
||||||
|
public function rename(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
if (!$this->validateCsrf($request, $response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$oldPath = $request->post('old_path');
|
||||||
|
$newPath = $request->post('new_path');
|
||||||
|
|
||||||
|
if (!$oldPath || !$newPath) {
|
||||||
|
$this->error($response, 'Both paths are required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isPathAllowed($oldPath) || !$this->isPathAllowed($newPath)) {
|
||||||
|
$this->error($response, 'Access denied', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
rename($oldPath, $newPath);
|
||||||
|
$this->success($response, ['path' => $newPath], 'Renamed successfully');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($response, 'Failed to rename: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy file or directory
|
||||||
|
*/
|
||||||
|
public function copy(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
if (!$this->validateCsrf($request, $response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$source = $request->post('source');
|
||||||
|
$destination = $request->post('destination');
|
||||||
|
|
||||||
|
if (!$source || !$destination) {
|
||||||
|
$this->error($response, 'Both paths are required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isPathAllowed($source) || !$this->isPathAllowed($destination)) {
|
||||||
|
$this->error($response, 'Access denied', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (is_dir($source)) {
|
||||||
|
$this->copyDirectory($source, $destination);
|
||||||
|
} else {
|
||||||
|
copy($source, $destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->success($response, ['path' => $destination], 'Copied successfully');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($response, 'Failed to copy: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move file or directory
|
||||||
|
*/
|
||||||
|
public function move(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
if (!$this->validateCsrf($request, $response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$source = $request->post('source');
|
||||||
|
$destination = $request->post('destination');
|
||||||
|
|
||||||
|
if (!$source || !$destination) {
|
||||||
|
$this->error($response, 'Both paths are required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isPathAllowed($source) || !$this->isPathAllowed($destination)) {
|
||||||
|
$this->error($response, 'Access denied', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
rename($source, $destination);
|
||||||
|
$this->success($response, ['path' => $destination], 'Moved successfully');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($response, 'Failed to move: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change file permissions
|
||||||
|
*/
|
||||||
|
public function chmod(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
if (!$this->validateCsrf($request, $response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $request->post('path');
|
||||||
|
$mode = $request->post('mode');
|
||||||
|
|
||||||
|
if (!$path || !$mode) {
|
||||||
|
$this->error($response, 'Path and mode are required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isPathAllowed($path)) {
|
||||||
|
$this->error($response, 'Access denied', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$permissions = octdec($mode);
|
||||||
|
chmod($path, $permissions);
|
||||||
|
$this->success($response, null, 'Permissions changed successfully');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($response, 'Failed to change permissions: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload file
|
||||||
|
*/
|
||||||
|
public function upload(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
if (!$this->validateCsrf($request, $response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$destination = $request->post('path', getcwd());
|
||||||
|
$file = $request->file('file');
|
||||||
|
|
||||||
|
if (!$file) {
|
||||||
|
$this->error($response, 'No file uploaded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isPathAllowed($destination)) {
|
||||||
|
$this->error($response, 'Access denied', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$maxSize = $this->config['limits']['max_upload_size'] ?? 100 * 1024 * 1024;
|
||||||
|
|
||||||
|
if ($file['size'] > $maxSize) {
|
||||||
|
$this->error($response, 'File is too large');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$targetPath = rtrim($destination, '/') . '/' . basename($file['name']);
|
||||||
|
move_uploaded_file($file['tmp_name'], $targetPath);
|
||||||
|
|
||||||
|
$this->success($response, [
|
||||||
|
'path' => $targetPath,
|
||||||
|
'size' => $file['size'],
|
||||||
|
], 'File uploaded successfully');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($response, 'Failed to upload: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download file
|
||||||
|
*/
|
||||||
|
public function download(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$path = $request->get('path');
|
||||||
|
|
||||||
|
if (!$path) {
|
||||||
|
$this->error($response, 'Path is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isPathAllowed($path)) {
|
||||||
|
$this->error($response, 'Access denied', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_file($path)) {
|
||||||
|
$this->error($response, 'File not found', 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response->download($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search files
|
||||||
|
*/
|
||||||
|
public function search(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$path = $request->post('path', getcwd());
|
||||||
|
$query = $request->post('query', '');
|
||||||
|
|
||||||
|
if (!$this->isPathAllowed($path)) {
|
||||||
|
$this->error($response, 'Access denied', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$results = $this->searchFiles($path, $query);
|
||||||
|
$this->success($response, ['results' => $results]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($response, 'Search failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan directory
|
||||||
|
*/
|
||||||
|
private function scanDirectory(string $path, bool $showHidden = false): array
|
||||||
|
{
|
||||||
|
$files = [];
|
||||||
|
$items = scandir($path);
|
||||||
|
|
||||||
|
if ($items === false) {
|
||||||
|
throw new \Exception('Cannot read directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if ($item === '.') continue;
|
||||||
|
|
||||||
|
if (!$showHidden && $item[0] === '.' && $item !== '..') continue;
|
||||||
|
|
||||||
|
$fullPath = rtrim($path, '/') . '/' . $item;
|
||||||
|
$isDir = is_dir($fullPath);
|
||||||
|
|
||||||
|
$files[] = [
|
||||||
|
'name' => $item,
|
||||||
|
'path' => $fullPath,
|
||||||
|
'is_directory' => $isDir,
|
||||||
|
'size' => $isDir ? 0 : (filesize($fullPath) ?: 0),
|
||||||
|
'modified' => filemtime($fullPath) ?: 0,
|
||||||
|
'permissions' => substr(sprintf('%o', fileperms($fullPath)), -4),
|
||||||
|
'readable' => is_readable($fullPath),
|
||||||
|
'writable' => is_writable($fullPath),
|
||||||
|
'owner' => function_exists('posix_getpwuid') ? (posix_getpwuid(fileowner($fullPath))['name'] ?? 'unknown') : 'unknown',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort files
|
||||||
|
*/
|
||||||
|
private function sortFiles(array $files, string $sortBy, string $sortOrder): array
|
||||||
|
{
|
||||||
|
usort($files, function ($a, $b) use ($sortBy, $sortOrder) {
|
||||||
|
// Directories first
|
||||||
|
if ($a['is_directory'] !== $b['is_directory']) {
|
||||||
|
return $a['is_directory'] ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = match ($sortBy) {
|
||||||
|
'size' => $a['size'] <=> $b['size'],
|
||||||
|
'modified' => $a['modified'] <=> $b['modified'],
|
||||||
|
default => strcasecmp($a['name'], $b['name']),
|
||||||
|
};
|
||||||
|
|
||||||
|
return $sortOrder === 'desc' ? -$result : $result;
|
||||||
|
});
|
||||||
|
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if content is binary
|
||||||
|
*/
|
||||||
|
private function isBinary(string $content): bool
|
||||||
|
{
|
||||||
|
return preg_match('~[^\x20-\x7E\t\r\n]~', substr($content, 0, 8192)) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete directory recursively
|
||||||
|
*/
|
||||||
|
private function deleteDirectory(string $path): void
|
||||||
|
{
|
||||||
|
$items = new \RecursiveIteratorIterator(
|
||||||
|
new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||||
|
\RecursiveIteratorIterator::CHILD_FIRST
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if ($item->isDir()) {
|
||||||
|
rmdir($item->getRealPath());
|
||||||
|
} else {
|
||||||
|
unlink($item->getRealPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rmdir($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy directory recursively
|
||||||
|
*/
|
||||||
|
private function copyDirectory(string $source, string $destination): void
|
||||||
|
{
|
||||||
|
if (!is_dir($destination)) {
|
||||||
|
mkdir($destination, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = new \RecursiveIteratorIterator(
|
||||||
|
new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||||
|
\RecursiveIteratorIterator::SELF_FIRST
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$target = $destination . '/' . $items->getSubPathname();
|
||||||
|
|
||||||
|
if ($item->isDir()) {
|
||||||
|
if (!is_dir($target)) {
|
||||||
|
mkdir($target, 0755, true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
copy($item->getRealPath(), $target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search files recursively
|
||||||
|
*/
|
||||||
|
private function searchFiles(string $path, string $query, int $maxResults = 100): array
|
||||||
|
{
|
||||||
|
$results = [];
|
||||||
|
$pattern = '/' . preg_quote($query, '/') . '/i';
|
||||||
|
|
||||||
|
$iterator = new \RecursiveIteratorIterator(
|
||||||
|
new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||||
|
\RecursiveIteratorIterator::SELF_FIRST
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($iterator as $item) {
|
||||||
|
if (count($results) >= $maxResults) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match($pattern, $item->getFilename())) {
|
||||||
|
$results[] = [
|
||||||
|
'name' => $item->getFilename(),
|
||||||
|
'path' => $item->getRealPath(),
|
||||||
|
'is_directory' => $item->isDir(),
|
||||||
|
'size' => $item->isDir() ? 0 : $item->getSize(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
}
|
||||||
362
src/Modules/Network/NetworkController.php
Archivo normal
362
src/Modules/Network/NetworkController.php
Archivo normal
@@ -0,0 +1,362 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Network Controller
|
||||||
|
*
|
||||||
|
* Handles network tools module.
|
||||||
|
*
|
||||||
|
* @package AleShell2\Modules\Network
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace AleShell2\Modules\Network;
|
||||||
|
|
||||||
|
use AleShell2\Modules\BaseController;
|
||||||
|
use AleShell2\Core\Request;
|
||||||
|
use AleShell2\Core\Response;
|
||||||
|
|
||||||
|
class NetworkController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show network tools
|
||||||
|
*/
|
||||||
|
public function index(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$this->render($response, 'modules.network', [
|
||||||
|
'currentModule' => 'network',
|
||||||
|
'pageTitle' => 'Network Tools - AleShell2',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get network connections
|
||||||
|
*/
|
||||||
|
public function connections(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$connections = $this->getNetworkConnections();
|
||||||
|
$this->success($response, ['connections' => $connections]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($response, 'Failed to get connections: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ping a host
|
||||||
|
*/
|
||||||
|
public function ping(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
if (!$this->validateCsrf($request, $response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = $request->post('host');
|
||||||
|
$count = min((int)$request->post('count', 4), 10); // Max 10 pings
|
||||||
|
|
||||||
|
if (!$host) {
|
||||||
|
$this->error($response, 'Host is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate host
|
||||||
|
if (!$this->isValidHost($host)) {
|
||||||
|
$this->error($response, 'Invalid host');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$output = $this->executePing($host, $count);
|
||||||
|
$this->success($response, ['output' => $output]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($response, 'Ping failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traceroute to host
|
||||||
|
*/
|
||||||
|
public function traceroute(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
if (!$this->validateCsrf($request, $response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = $request->post('host');
|
||||||
|
$maxHops = min((int)$request->post('max_hops', 30), 30);
|
||||||
|
|
||||||
|
if (!$host) {
|
||||||
|
$this->error($response, 'Host is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isValidHost($host)) {
|
||||||
|
$this->error($response, 'Invalid host');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$output = $this->executeTraceroute($host, $maxHops);
|
||||||
|
$this->success($response, ['output' => $output]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($response, 'Traceroute failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Port scan
|
||||||
|
*/
|
||||||
|
public function portscan(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
if (!$this->validateCsrf($request, $response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = $request->post('host');
|
||||||
|
$ports = $request->post('ports', '1-1000');
|
||||||
|
|
||||||
|
if (!$host) {
|
||||||
|
$this->error($response, 'Host is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isValidHost($host)) {
|
||||||
|
$this->error($response, 'Invalid host');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$results = $this->executePortScan($host, $ports);
|
||||||
|
$this->success($response, $results);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($response, 'Port scan failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get network connections
|
||||||
|
*/
|
||||||
|
private function getNetworkConnections(): array
|
||||||
|
{
|
||||||
|
$connections = [];
|
||||||
|
|
||||||
|
// Try netstat
|
||||||
|
$output = shell_exec('netstat -tuln 2>/dev/null');
|
||||||
|
|
||||||
|
if (!$output) {
|
||||||
|
// Try ss command as fallback
|
||||||
|
$output = shell_exec('ss -tuln 2>/dev/null');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$output) {
|
||||||
|
return $connections;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = explode("\n", trim($output));
|
||||||
|
array_shift($lines); // Remove header
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (empty(trim($line))) continue;
|
||||||
|
|
||||||
|
// Skip additional header line in netstat
|
||||||
|
if (strpos($line, 'Proto') !== false || strpos($line, 'Netid') !== false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = preg_split('/\s+/', trim($line));
|
||||||
|
|
||||||
|
if (count($parts) >= 4) {
|
||||||
|
$proto = $parts[0];
|
||||||
|
$local = $parts[3] ?? '';
|
||||||
|
$foreign = $parts[4] ?? '*:*';
|
||||||
|
$state = $parts[5] ?? 'LISTEN';
|
||||||
|
|
||||||
|
// Parse local address
|
||||||
|
$localParts = explode(':', $local);
|
||||||
|
$localPort = end($localParts);
|
||||||
|
$localAddr = implode(':', array_slice($localParts, 0, -1)) ?: '*';
|
||||||
|
|
||||||
|
$connections[] = [
|
||||||
|
'protocol' => strtoupper($proto),
|
||||||
|
'local_address' => $localAddr,
|
||||||
|
'local_port' => $localPort,
|
||||||
|
'foreign_address' => $foreign,
|
||||||
|
'state' => $state,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $connections;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute ping command
|
||||||
|
*/
|
||||||
|
private function executePing(string $host, int $count): string
|
||||||
|
{
|
||||||
|
$host = escapeshellarg($host);
|
||||||
|
$count = (int)$count;
|
||||||
|
|
||||||
|
// Detect OS and use appropriate ping syntax
|
||||||
|
if (PHP_OS_FAMILY === 'Windows') {
|
||||||
|
$cmd = "ping -n {$count} {$host}";
|
||||||
|
} else {
|
||||||
|
$cmd = "ping -c {$count} -W 2 {$host}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$output = shell_exec($cmd . ' 2>&1');
|
||||||
|
|
||||||
|
return $output ?: 'No output from ping command';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute traceroute command
|
||||||
|
*/
|
||||||
|
private function executeTraceroute(string $host, int $maxHops): string
|
||||||
|
{
|
||||||
|
$host = escapeshellarg($host);
|
||||||
|
$maxHops = (int)$maxHops;
|
||||||
|
|
||||||
|
// Try traceroute first, then tracepath
|
||||||
|
if (PHP_OS_FAMILY === 'Windows') {
|
||||||
|
$cmd = "tracert -h {$maxHops} {$host}";
|
||||||
|
} else {
|
||||||
|
// Check if traceroute exists
|
||||||
|
$traceroute = shell_exec('which traceroute 2>/dev/null');
|
||||||
|
if ($traceroute) {
|
||||||
|
$cmd = "traceroute -m {$maxHops} -w 2 {$host}";
|
||||||
|
} else {
|
||||||
|
// Fall back to tracepath
|
||||||
|
$cmd = "tracepath {$host}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$output = shell_exec($cmd . ' 2>&1');
|
||||||
|
|
||||||
|
return $output ?: 'No output from traceroute command';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute port scan
|
||||||
|
*/
|
||||||
|
private function executePortScan(string $host, string $portsArg): array
|
||||||
|
{
|
||||||
|
$ports = $this->parsePorts($portsArg);
|
||||||
|
|
||||||
|
if (count($ports) > 100) {
|
||||||
|
$ports = array_slice($ports, 0, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
$openPorts = [];
|
||||||
|
$closedCount = 0;
|
||||||
|
|
||||||
|
$timeout = 1; // 1 second timeout per port
|
||||||
|
|
||||||
|
foreach ($ports as $port) {
|
||||||
|
$port = (int)$port;
|
||||||
|
|
||||||
|
if ($port < 1 || $port > 65535) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$connection = @fsockopen($host, $port, $errno, $errstr, $timeout);
|
||||||
|
|
||||||
|
if ($connection) {
|
||||||
|
fclose($connection);
|
||||||
|
$openPorts[] = [
|
||||||
|
'port' => $port,
|
||||||
|
'service' => $this->getServiceName($port),
|
||||||
|
'status' => 'open',
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$closedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'host' => $host,
|
||||||
|
'open_ports' => $openPorts,
|
||||||
|
'scanned_count' => count($ports),
|
||||||
|
'open_count' => count($openPorts),
|
||||||
|
'closed_count' => $closedCount,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse port range
|
||||||
|
*/
|
||||||
|
private function parsePorts(string $portsArg): array
|
||||||
|
{
|
||||||
|
$ports = [];
|
||||||
|
$parts = explode(',', $portsArg);
|
||||||
|
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$part = trim($part);
|
||||||
|
|
||||||
|
if (strpos($part, '-') !== false) {
|
||||||
|
// Range
|
||||||
|
[$start, $end] = explode('-', $part);
|
||||||
|
$start = (int)$start;
|
||||||
|
$end = (int)$end;
|
||||||
|
|
||||||
|
for ($i = $start; $i <= $end && $i <= 65535; $i++) {
|
||||||
|
$ports[] = $i;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Single port
|
||||||
|
$ports[] = (int)$part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_unique($ports);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get service name for common ports
|
||||||
|
*/
|
||||||
|
private function getServiceName(int $port): string
|
||||||
|
{
|
||||||
|
$services = [
|
||||||
|
21 => 'FTP',
|
||||||
|
22 => 'SSH',
|
||||||
|
23 => 'Telnet',
|
||||||
|
25 => 'SMTP',
|
||||||
|
53 => 'DNS',
|
||||||
|
80 => 'HTTP',
|
||||||
|
110 => 'POP3',
|
||||||
|
143 => 'IMAP',
|
||||||
|
443 => 'HTTPS',
|
||||||
|
465 => 'SMTPS',
|
||||||
|
587 => 'SMTP/TLS',
|
||||||
|
993 => 'IMAPS',
|
||||||
|
995 => 'POP3S',
|
||||||
|
3306 => 'MySQL',
|
||||||
|
3389 => 'RDP',
|
||||||
|
5432 => 'PostgreSQL',
|
||||||
|
5900 => 'VNC',
|
||||||
|
6379 => 'Redis',
|
||||||
|
8080 => 'HTTP-Alt',
|
||||||
|
8443 => 'HTTPS-Alt',
|
||||||
|
27017 => 'MongoDB',
|
||||||
|
];
|
||||||
|
|
||||||
|
return $services[$port] ?? 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate host
|
||||||
|
*/
|
||||||
|
private function isValidHost(string $host): bool
|
||||||
|
{
|
||||||
|
// Check if it's a valid IP
|
||||||
|
if (filter_var($host, FILTER_VALIDATE_IP)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a valid hostname
|
||||||
|
if (preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/', $host)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
279
src/Modules/Processes/ProcessesController.php
Archivo normal
279
src/Modules/Processes/ProcessesController.php
Archivo normal
@@ -0,0 +1,279 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Processes Controller
|
||||||
|
*
|
||||||
|
* Handles process manager module.
|
||||||
|
*
|
||||||
|
* @package AleShell2\Modules\Processes
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace AleShell2\Modules\Processes;
|
||||||
|
|
||||||
|
use AleShell2\Modules\BaseController;
|
||||||
|
use AleShell2\Core\Request;
|
||||||
|
use AleShell2\Core\Response;
|
||||||
|
|
||||||
|
class ProcessesController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show process manager
|
||||||
|
*/
|
||||||
|
public function index(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$this->render($response, 'modules.processes', [
|
||||||
|
'currentModule' => 'processes',
|
||||||
|
'pageTitle' => 'Process Manager - AleShell2',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List processes
|
||||||
|
*/
|
||||||
|
public function list(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$search = $request->post('search', '');
|
||||||
|
$sortBy = $request->post('sort', 'cpu');
|
||||||
|
$sortOrder = $request->post('order', 'desc');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$processes = $this->getProcessList($search, $sortBy, $sortOrder);
|
||||||
|
$this->success($response, ['processes' => $processes]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($response, 'Failed to list processes: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kill process
|
||||||
|
*/
|
||||||
|
public function kill(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
if (!$this->validateCsrf($request, $response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pids = $request->post('pids');
|
||||||
|
$signal = (int)$request->post('signal', 15); // SIGTERM
|
||||||
|
|
||||||
|
if (empty($pids)) {
|
||||||
|
$this->error($response, 'PID is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($pids)) {
|
||||||
|
$pids = [$pids];
|
||||||
|
}
|
||||||
|
|
||||||
|
$killed = [];
|
||||||
|
$failed = [];
|
||||||
|
|
||||||
|
foreach ($pids as $pid) {
|
||||||
|
$pid = (int)$pid;
|
||||||
|
|
||||||
|
if ($pid <= 0) {
|
||||||
|
$failed[] = ['pid' => $pid, 'error' => 'Invalid PID'];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow killing init or critical system processes
|
||||||
|
if ($pid === 1) {
|
||||||
|
$failed[] = ['pid' => $pid, 'error' => 'Cannot kill init process'];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (function_exists('posix_kill')) {
|
||||||
|
if (posix_kill($pid, $signal)) {
|
||||||
|
$killed[] = $pid;
|
||||||
|
} else {
|
||||||
|
$failed[] = ['pid' => $pid, 'error' => posix_strerror(posix_get_last_error())];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to shell command
|
||||||
|
$result = shell_exec("kill -{$signal} {$pid} 2>&1");
|
||||||
|
if ($result === null || $result === '') {
|
||||||
|
$killed[] = $pid;
|
||||||
|
} else {
|
||||||
|
$failed[] = ['pid' => $pid, 'error' => trim($result)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->success($response, [
|
||||||
|
'killed' => $killed,
|
||||||
|
'failed' => $failed,
|
||||||
|
], count($killed) . ' process(es) killed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get process list
|
||||||
|
*/
|
||||||
|
private function getProcessList(string $search = '', string $sortBy = 'cpu', string $sortOrder = 'desc'): array
|
||||||
|
{
|
||||||
|
$processes = [];
|
||||||
|
$maxProcesses = $this->config['limits']['max_processes'] ?? 1000;
|
||||||
|
|
||||||
|
// Try using /proc filesystem (Linux)
|
||||||
|
if (is_dir('/proc')) {
|
||||||
|
$processes = $this->getProcessesFromProc();
|
||||||
|
} else {
|
||||||
|
// Fallback to ps command
|
||||||
|
$processes = $this->getProcessesFromPs();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by search
|
||||||
|
if (!empty($search)) {
|
||||||
|
$search = strtolower($search);
|
||||||
|
$processes = array_filter($processes, function ($p) use ($search) {
|
||||||
|
return stripos($p['name'], $search) !== false
|
||||||
|
|| stripos($p['command'], $search) !== false
|
||||||
|
|| (string)$p['pid'] === $search;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
usort($processes, function ($a, $b) use ($sortBy, $sortOrder) {
|
||||||
|
$result = match ($sortBy) {
|
||||||
|
'pid' => $a['pid'] <=> $b['pid'],
|
||||||
|
'cpu' => $a['cpu'] <=> $b['cpu'],
|
||||||
|
'memory' => $a['memory'] <=> $b['memory'],
|
||||||
|
'name' => strcasecmp($a['name'], $b['name']),
|
||||||
|
default => $a['cpu'] <=> $b['cpu'],
|
||||||
|
};
|
||||||
|
|
||||||
|
return $sortOrder === 'desc' ? -$result : $result;
|
||||||
|
});
|
||||||
|
|
||||||
|
return array_slice(array_values($processes), 0, $maxProcesses);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get processes from /proc filesystem
|
||||||
|
*/
|
||||||
|
private function getProcessesFromProc(): array
|
||||||
|
{
|
||||||
|
$processes = [];
|
||||||
|
|
||||||
|
$dirs = glob('/proc/[0-9]*', GLOB_ONLYDIR);
|
||||||
|
|
||||||
|
foreach ($dirs as $dir) {
|
||||||
|
$pid = (int)basename($dir);
|
||||||
|
|
||||||
|
// Read process info
|
||||||
|
$statusFile = "{$dir}/status";
|
||||||
|
$statFile = "{$dir}/stat";
|
||||||
|
$cmdlineFile = "{$dir}/cmdline";
|
||||||
|
|
||||||
|
if (!file_exists($statusFile)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = @file_get_contents($statusFile);
|
||||||
|
$stat = @file_get_contents($statFile);
|
||||||
|
$cmdline = @file_get_contents($cmdlineFile);
|
||||||
|
|
||||||
|
if ($status === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse status
|
||||||
|
$name = '';
|
||||||
|
$uid = 0;
|
||||||
|
$vmRss = 0;
|
||||||
|
|
||||||
|
if (preg_match('/^Name:\s*(.+)$/m', $status, $m)) {
|
||||||
|
$name = trim($m[1]);
|
||||||
|
}
|
||||||
|
if (preg_match('/^Uid:\s*(\d+)/m', $status, $m)) {
|
||||||
|
$uid = (int)$m[1];
|
||||||
|
}
|
||||||
|
if (preg_match('/^VmRSS:\s*(\d+)/m', $status, $m)) {
|
||||||
|
$vmRss = (int)$m[1] * 1024; // Convert to bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse stat for CPU info
|
||||||
|
$cpu = 0;
|
||||||
|
if ($stat) {
|
||||||
|
$statParts = explode(' ', $stat);
|
||||||
|
if (count($statParts) > 13) {
|
||||||
|
// utime + stime
|
||||||
|
$utime = (int)$statParts[13];
|
||||||
|
$stime = (int)$statParts[14];
|
||||||
|
$totalTime = $utime + $stime;
|
||||||
|
|
||||||
|
// This is a simplified CPU calculation
|
||||||
|
$cpu = round($totalTime / 100, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get command line
|
||||||
|
$command = $cmdline ? str_replace("\0", ' ', trim($cmdline)) : $name;
|
||||||
|
|
||||||
|
// Get user
|
||||||
|
$user = 'unknown';
|
||||||
|
if (function_exists('posix_getpwuid')) {
|
||||||
|
$pwinfo = posix_getpwuid($uid);
|
||||||
|
if ($pwinfo) {
|
||||||
|
$user = $pwinfo['name'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate memory percentage
|
||||||
|
$totalMem = $this->getMemoryInfo()['total'] ?? 1;
|
||||||
|
$memPercent = $totalMem > 0 ? round(($vmRss / $totalMem) * 100, 1) : 0;
|
||||||
|
|
||||||
|
$processes[] = [
|
||||||
|
'pid' => $pid,
|
||||||
|
'name' => $name,
|
||||||
|
'command' => strlen($command) > 100 ? substr($command, 0, 100) . '...' : $command,
|
||||||
|
'user' => $user,
|
||||||
|
'cpu' => $cpu,
|
||||||
|
'memory' => $memPercent,
|
||||||
|
'memory_bytes' => $vmRss,
|
||||||
|
'status' => 'running',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $processes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get processes from ps command
|
||||||
|
*/
|
||||||
|
private function getProcessesFromPs(): array
|
||||||
|
{
|
||||||
|
$processes = [];
|
||||||
|
|
||||||
|
// Use ps command with portable options
|
||||||
|
$output = shell_exec('ps aux 2>/dev/null');
|
||||||
|
|
||||||
|
if (!$output) {
|
||||||
|
return $processes;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = explode("\n", trim($output));
|
||||||
|
array_shift($lines); // Remove header
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$parts = preg_split('/\s+/', trim($line), 11);
|
||||||
|
|
||||||
|
if (count($parts) < 11) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$processes[] = [
|
||||||
|
'pid' => (int)$parts[1],
|
||||||
|
'name' => basename($parts[10]),
|
||||||
|
'command' => strlen($parts[10]) > 100 ? substr($parts[10], 0, 100) . '...' : $parts[10],
|
||||||
|
'user' => $parts[0],
|
||||||
|
'cpu' => (float)$parts[2],
|
||||||
|
'memory' => (float)$parts[3],
|
||||||
|
'memory_bytes' => 0,
|
||||||
|
'status' => 'running',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $processes;
|
||||||
|
}
|
||||||
|
}
|
||||||
355
src/Modules/System/SystemController.php
Archivo normal
355
src/Modules/System/SystemController.php
Archivo normal
@@ -0,0 +1,355 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 System Controller
|
||||||
|
*
|
||||||
|
* Handles system information module.
|
||||||
|
*
|
||||||
|
* @package AleShell2\Modules\System
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace AleShell2\Modules\System;
|
||||||
|
|
||||||
|
use AleShell2\Modules\BaseController;
|
||||||
|
use AleShell2\Core\Request;
|
||||||
|
use AleShell2\Core\Response;
|
||||||
|
|
||||||
|
class SystemController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show system info
|
||||||
|
*/
|
||||||
|
public function index(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$this->render($response, 'modules.system', [
|
||||||
|
'currentModule' => 'system',
|
||||||
|
'pageTitle' => 'System Info - AleShell2',
|
||||||
|
'systemInfo' => $this->getSystemInfo(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get system information via API
|
||||||
|
*/
|
||||||
|
public function info(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$this->success($response, $this->getSystemInfo());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get PHP information
|
||||||
|
*/
|
||||||
|
public function phpinfo(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
ob_start();
|
||||||
|
phpinfo();
|
||||||
|
$info = ob_get_clean();
|
||||||
|
|
||||||
|
$response->html($info);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get environment variables
|
||||||
|
*/
|
||||||
|
public function environment(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$env = [];
|
||||||
|
|
||||||
|
foreach ($_ENV as $key => $value) {
|
||||||
|
$env[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($_SERVER as $key => $value) {
|
||||||
|
if (is_string($value)) {
|
||||||
|
$env[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ksort($env);
|
||||||
|
|
||||||
|
$this->success($response, ['environment' => $env]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get PHP extensions
|
||||||
|
*/
|
||||||
|
public function extensions(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$extensions = get_loaded_extensions();
|
||||||
|
sort($extensions);
|
||||||
|
|
||||||
|
$detailed = [];
|
||||||
|
foreach ($extensions as $ext) {
|
||||||
|
$detailed[$ext] = phpversion($ext) ?: 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->success($response, ['extensions' => $detailed]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect all system information
|
||||||
|
*/
|
||||||
|
private function getSystemInfo(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'server' => $this->getServerInfo(),
|
||||||
|
'php' => $this->getPhpInfo(),
|
||||||
|
'hardware' => $this->getHardwareInfo(),
|
||||||
|
'disk' => $this->getDiskInfo(),
|
||||||
|
'network' => $this->getBasicNetworkInfo(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get server information
|
||||||
|
*/
|
||||||
|
private function getServerInfo(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'hostname' => gethostname() ?: 'unknown',
|
||||||
|
'os' => PHP_OS_FAMILY,
|
||||||
|
'os_detail' => php_uname(),
|
||||||
|
'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'unknown',
|
||||||
|
'server_name' => $_SERVER['SERVER_NAME'] ?? 'unknown',
|
||||||
|
'server_addr' => $_SERVER['SERVER_ADDR'] ?? 'unknown',
|
||||||
|
'server_port' => $_SERVER['SERVER_PORT'] ?? 'unknown',
|
||||||
|
'document_root' => $_SERVER['DOCUMENT_ROOT'] ?? 'unknown',
|
||||||
|
'current_user' => get_current_user(),
|
||||||
|
'current_uid' => getmyuid(),
|
||||||
|
'current_gid' => getmygid(),
|
||||||
|
'process_id' => getmypid(),
|
||||||
|
'uptime' => $this->getUptime(),
|
||||||
|
'load_average' => $this->getLoadAverage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get PHP information
|
||||||
|
*/
|
||||||
|
private function getPhpInfo(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'version' => PHP_VERSION,
|
||||||
|
'version_id' => PHP_VERSION_ID,
|
||||||
|
'sapi' => PHP_SAPI,
|
||||||
|
'ini_path' => php_ini_loaded_file() ?: 'unknown',
|
||||||
|
'extension_dir' => ini_get('extension_dir'),
|
||||||
|
'include_path' => get_include_path(),
|
||||||
|
'memory_limit' => ini_get('memory_limit'),
|
||||||
|
'max_execution_time' => ini_get('max_execution_time'),
|
||||||
|
'max_input_time' => ini_get('max_input_time'),
|
||||||
|
'post_max_size' => ini_get('post_max_size'),
|
||||||
|
'upload_max_filesize' => ini_get('upload_max_filesize'),
|
||||||
|
'max_file_uploads' => ini_get('max_file_uploads'),
|
||||||
|
'display_errors' => ini_get('display_errors'),
|
||||||
|
'error_reporting' => error_reporting(),
|
||||||
|
'date_timezone' => date_default_timezone_get(),
|
||||||
|
'open_basedir' => ini_get('open_basedir') ?: 'none',
|
||||||
|
'disabled_functions' => ini_get('disable_functions') ?: 'none',
|
||||||
|
'disabled_classes' => ini_get('disable_classes') ?: 'none',
|
||||||
|
'safe_mode' => ini_get('safe_mode') ? 'on' : 'off',
|
||||||
|
'allow_url_fopen' => ini_get('allow_url_fopen') ? 'on' : 'off',
|
||||||
|
'allow_url_include' => ini_get('allow_url_include') ? 'on' : 'off',
|
||||||
|
'zend_version' => zend_version(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get hardware information
|
||||||
|
*/
|
||||||
|
private function getHardwareInfo(): array
|
||||||
|
{
|
||||||
|
$info = [
|
||||||
|
'cpu' => 'unknown',
|
||||||
|
'cpu_cores' => 'unknown',
|
||||||
|
'memory_total' => 'unknown',
|
||||||
|
'memory_free' => 'unknown',
|
||||||
|
'memory_used' => 'unknown',
|
||||||
|
];
|
||||||
|
|
||||||
|
// CPU info
|
||||||
|
if (is_readable('/proc/cpuinfo')) {
|
||||||
|
$cpuinfo = file_get_contents('/proc/cpuinfo');
|
||||||
|
if (preg_match('/model name\s*:\s*(.+)/i', $cpuinfo, $matches)) {
|
||||||
|
$info['cpu'] = trim($matches[1]);
|
||||||
|
}
|
||||||
|
$info['cpu_cores'] = substr_count($cpuinfo, 'processor');
|
||||||
|
} elseif (PHP_OS_FAMILY === 'Darwin') {
|
||||||
|
$info['cpu'] = trim(shell_exec('sysctl -n machdep.cpu.brand_string 2>/dev/null') ?: 'unknown');
|
||||||
|
$info['cpu_cores'] = (int)trim(shell_exec('sysctl -n hw.ncpu 2>/dev/null') ?: '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory info
|
||||||
|
if (is_readable('/proc/meminfo')) {
|
||||||
|
$meminfo = file_get_contents('/proc/meminfo');
|
||||||
|
if (preg_match('/MemTotal:\s*(\d+)\s*kB/i', $meminfo, $matches)) {
|
||||||
|
$info['memory_total'] = $this->formatBytes((int)$matches[1] * 1024);
|
||||||
|
}
|
||||||
|
if (preg_match('/MemFree:\s*(\d+)\s*kB/i', $meminfo, $matches)) {
|
||||||
|
$memFree = (int)$matches[1] * 1024;
|
||||||
|
$info['memory_free'] = $this->formatBytes($memFree);
|
||||||
|
}
|
||||||
|
if (preg_match('/MemAvailable:\s*(\d+)\s*kB/i', $meminfo, $matches)) {
|
||||||
|
$info['memory_available'] = $this->formatBytes((int)$matches[1] * 1024);
|
||||||
|
}
|
||||||
|
} elseif (PHP_OS_FAMILY === 'Darwin') {
|
||||||
|
$totalMem = (int)trim(shell_exec('sysctl -n hw.memsize 2>/dev/null') ?: '0');
|
||||||
|
$info['memory_total'] = $this->formatBytes($totalMem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get disk information
|
||||||
|
*/
|
||||||
|
private function getDiskInfo(): array
|
||||||
|
{
|
||||||
|
$disks = [];
|
||||||
|
|
||||||
|
// Current disk
|
||||||
|
$path = $_SERVER['DOCUMENT_ROOT'] ?? getcwd();
|
||||||
|
$totalSpace = @disk_total_space($path);
|
||||||
|
$freeSpace = @disk_free_space($path);
|
||||||
|
|
||||||
|
$disks[] = [
|
||||||
|
'mount' => $path,
|
||||||
|
'total' => $totalSpace ? $this->formatBytes($totalSpace) : 'unknown',
|
||||||
|
'free' => $freeSpace ? $this->formatBytes($freeSpace) : 'unknown',
|
||||||
|
'used' => ($totalSpace && $freeSpace) ? $this->formatBytes($totalSpace - $freeSpace) : 'unknown',
|
||||||
|
'percent_used' => ($totalSpace && $freeSpace) ? round(($totalSpace - $freeSpace) / $totalSpace * 100, 2) . '%' : 'unknown',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Get all mount points on Linux
|
||||||
|
if (PHP_OS_FAMILY === 'Linux') {
|
||||||
|
$df = shell_exec('df -h 2>/dev/null');
|
||||||
|
if ($df) {
|
||||||
|
$lines = explode("\n", trim($df));
|
||||||
|
array_shift($lines); // Remove header
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$parts = preg_split('/\s+/', $line);
|
||||||
|
if (count($parts) >= 6 && strpos($parts[0], '/dev/') === 0) {
|
||||||
|
$disks[] = [
|
||||||
|
'device' => $parts[0],
|
||||||
|
'total' => $parts[1],
|
||||||
|
'used' => $parts[2],
|
||||||
|
'free' => $parts[3],
|
||||||
|
'percent_used' => $parts[4],
|
||||||
|
'mount' => $parts[5],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $disks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get basic network information
|
||||||
|
*/
|
||||||
|
private function getBasicNetworkInfo(): array
|
||||||
|
{
|
||||||
|
$info = [
|
||||||
|
'hostname' => gethostname(),
|
||||||
|
'ip_addresses' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Get IP addresses
|
||||||
|
$hostname = gethostname();
|
||||||
|
if ($hostname) {
|
||||||
|
$ips = gethostbynamel($hostname);
|
||||||
|
if ($ips) {
|
||||||
|
$info['ip_addresses'] = $ips;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add server IP
|
||||||
|
if (!empty($_SERVER['SERVER_ADDR'])) {
|
||||||
|
$info['server_ip'] = $_SERVER['SERVER_ADDR'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get interfaces on Linux
|
||||||
|
if (PHP_OS_FAMILY === 'Linux') {
|
||||||
|
$interfaces = shell_exec('ip -o addr show 2>/dev/null | grep -v "127.0.0.1"');
|
||||||
|
if ($interfaces) {
|
||||||
|
$info['interfaces'] = trim($interfaces);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get system uptime
|
||||||
|
*/
|
||||||
|
private function getUptime(): string
|
||||||
|
{
|
||||||
|
if (is_readable('/proc/uptime')) {
|
||||||
|
$uptime = (float)explode(' ', file_get_contents('/proc/uptime'))[0];
|
||||||
|
return $this->formatUptime($uptime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PHP_OS_FAMILY === 'Darwin') {
|
||||||
|
$boottime = shell_exec('sysctl -n kern.boottime 2>/dev/null');
|
||||||
|
if (preg_match('/sec = (\d+)/', $boottime, $matches)) {
|
||||||
|
$uptime = time() - (int)$matches[1];
|
||||||
|
return $this->formatUptime($uptime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format uptime seconds to human readable
|
||||||
|
*/
|
||||||
|
private function formatUptime(float $seconds): string
|
||||||
|
{
|
||||||
|
$days = floor($seconds / 86400);
|
||||||
|
$hours = floor(($seconds % 86400) / 3600);
|
||||||
|
$minutes = floor(($seconds % 3600) / 60);
|
||||||
|
|
||||||
|
$parts = [];
|
||||||
|
if ($days > 0) $parts[] = "{$days}d";
|
||||||
|
if ($hours > 0) $parts[] = "{$hours}h";
|
||||||
|
if ($minutes > 0) $parts[] = "{$minutes}m";
|
||||||
|
|
||||||
|
return implode(' ', $parts) ?: '< 1m';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get load average
|
||||||
|
*/
|
||||||
|
private function getLoadAverage(): array
|
||||||
|
{
|
||||||
|
if (function_exists('sys_getloadavg')) {
|
||||||
|
$load = sys_getloadavg();
|
||||||
|
if ($load !== false) {
|
||||||
|
return [
|
||||||
|
'1min' => round($load[0], 2),
|
||||||
|
'5min' => round($load[1], 2),
|
||||||
|
'15min' => round($load[2], 2),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['1min' => 'unknown', '5min' => 'unknown', '15min' => 'unknown'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format bytes to human readable
|
||||||
|
*/
|
||||||
|
private function formatBytes(int $bytes, int $precision = 2): string
|
||||||
|
{
|
||||||
|
$units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||||
|
|
||||||
|
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
|
||||||
|
$bytes /= 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
return round($bytes, $precision) . ' ' . $units[$i];
|
||||||
|
}
|
||||||
|
}
|
||||||
452
src/Modules/Terminal/TerminalController.php
Archivo normal
452
src/Modules/Terminal/TerminalController.php
Archivo normal
@@ -0,0 +1,452 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Terminal Controller
|
||||||
|
*
|
||||||
|
* Handles terminal module.
|
||||||
|
*
|
||||||
|
* @package AleShell2\Modules\Terminal
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace AleShell2\Modules\Terminal;
|
||||||
|
|
||||||
|
use AleShell2\Modules\BaseController;
|
||||||
|
use AleShell2\Core\Request;
|
||||||
|
use AleShell2\Core\Response;
|
||||||
|
use AleShell2\Core\Application;
|
||||||
|
|
||||||
|
class TerminalController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show terminal
|
||||||
|
*/
|
||||||
|
public function index(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$this->render($response, 'modules.terminal', [
|
||||||
|
'currentModule' => 'terminal',
|
||||||
|
'pageTitle' => 'Terminal - AleShell2',
|
||||||
|
'currentPath' => getcwd(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute command
|
||||||
|
*/
|
||||||
|
public function execute(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
if (!$this->validateCsrf($request, $response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$command = trim($request->post('command', ''));
|
||||||
|
$workingDir = $request->post('cwd', getcwd());
|
||||||
|
|
||||||
|
if (empty($command)) {
|
||||||
|
$this->error($response, 'Command is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to history
|
||||||
|
$this->addToHistory($command);
|
||||||
|
|
||||||
|
// Check if it's a built-in command
|
||||||
|
if ($this->isBuiltinCommand($command)) {
|
||||||
|
$result = $this->executeBuiltin($command, $workingDir);
|
||||||
|
} else {
|
||||||
|
// Check if command is allowed
|
||||||
|
if (!$this->isCommandAllowed($command)) {
|
||||||
|
$this->error($response, 'This command is not allowed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->executeSystemCommand($command, $workingDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->success($response, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get command history
|
||||||
|
*/
|
||||||
|
public function history(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$app = Application::getInstance();
|
||||||
|
$session = $app->getSession();
|
||||||
|
|
||||||
|
$history = $session->get('terminal_history', []);
|
||||||
|
$maxHistory = $this->config['limits']['max_history'] ?? 100;
|
||||||
|
|
||||||
|
$this->success($response, [
|
||||||
|
'history' => array_slice($history, -$maxHistory),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear command history
|
||||||
|
*/
|
||||||
|
public function clear(Request $request, Response $response, array $params): void
|
||||||
|
{
|
||||||
|
$app = Application::getInstance();
|
||||||
|
$session = $app->getSession();
|
||||||
|
|
||||||
|
$session->remove('terminal_history');
|
||||||
|
|
||||||
|
$this->success($response, null, 'History cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if command is a built-in
|
||||||
|
*/
|
||||||
|
private function isBuiltinCommand(string $command): bool
|
||||||
|
{
|
||||||
|
$builtins = ['cd', 'pwd', 'clear', 'history', 'help', 'exit', 'whoami', 'id'];
|
||||||
|
$parts = explode(' ', trim($command));
|
||||||
|
return in_array($parts[0], $builtins, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute built-in command
|
||||||
|
*/
|
||||||
|
private function executeBuiltin(string $command, string $workingDir): array
|
||||||
|
{
|
||||||
|
$parts = explode(' ', trim($command));
|
||||||
|
$cmd = $parts[0];
|
||||||
|
|
||||||
|
return match ($cmd) {
|
||||||
|
'cd' => $this->cmdCd($parts, $workingDir),
|
||||||
|
'pwd' => $this->cmdPwd($workingDir),
|
||||||
|
'clear' => $this->cmdClear(),
|
||||||
|
'history' => $this->cmdHistory(),
|
||||||
|
'help' => $this->cmdHelp(),
|
||||||
|
'exit' => $this->cmdExit(),
|
||||||
|
'whoami' => $this->cmdWhoami(),
|
||||||
|
'id' => $this->cmdId(),
|
||||||
|
default => [
|
||||||
|
'output' => "Unknown built-in command: {$cmd}",
|
||||||
|
'exit_code' => 1,
|
||||||
|
'cwd' => $workingDir,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CD command
|
||||||
|
*/
|
||||||
|
private function cmdCd(array $parts, string $currentDir): array
|
||||||
|
{
|
||||||
|
$app = Application::getInstance();
|
||||||
|
$session = $app->getSession();
|
||||||
|
|
||||||
|
$target = $parts[1] ?? ($_SERVER['HOME'] ?? '/');
|
||||||
|
|
||||||
|
// Handle special cases
|
||||||
|
if ($target === '-') {
|
||||||
|
$target = $session->get('terminal_prev_dir', $currentDir);
|
||||||
|
} elseif ($target === '~') {
|
||||||
|
$target = $_SERVER['HOME'] ?? '/root';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve relative paths
|
||||||
|
if ($target[0] !== '/') {
|
||||||
|
$target = rtrim($currentDir, '/') . '/' . $target;
|
||||||
|
}
|
||||||
|
|
||||||
|
$realTarget = realpath($target);
|
||||||
|
|
||||||
|
if ($realTarget === false || !is_dir($realTarget)) {
|
||||||
|
return [
|
||||||
|
'output' => "cd: {$target}: No such directory",
|
||||||
|
'exit_code' => 1,
|
||||||
|
'cwd' => $currentDir,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isPathAllowed($realTarget)) {
|
||||||
|
return [
|
||||||
|
'output' => "cd: {$target}: Permission denied",
|
||||||
|
'exit_code' => 1,
|
||||||
|
'cwd' => $currentDir,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store previous directory
|
||||||
|
$session->set('terminal_prev_dir', $currentDir);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'output' => '',
|
||||||
|
'exit_code' => 0,
|
||||||
|
'cwd' => $realTarget,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PWD command
|
||||||
|
*/
|
||||||
|
private function cmdPwd(string $currentDir): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'output' => $currentDir,
|
||||||
|
'exit_code' => 0,
|
||||||
|
'cwd' => $currentDir,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear command
|
||||||
|
*/
|
||||||
|
private function cmdClear(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'output' => '',
|
||||||
|
'exit_code' => 0,
|
||||||
|
'cwd' => getcwd(),
|
||||||
|
'clear' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* History command
|
||||||
|
*/
|
||||||
|
private function cmdHistory(): array
|
||||||
|
{
|
||||||
|
$app = Application::getInstance();
|
||||||
|
$session = $app->getSession();
|
||||||
|
|
||||||
|
$history = $session->get('terminal_history', []);
|
||||||
|
$output = '';
|
||||||
|
|
||||||
|
foreach (array_slice($history, -50) as $i => $cmd) {
|
||||||
|
$output .= sprintf("%4d %s\n", $i + 1, $cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'output' => $output ?: 'No history',
|
||||||
|
'exit_code' => 0,
|
||||||
|
'cwd' => getcwd(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Help command
|
||||||
|
*/
|
||||||
|
private function cmdHelp(): array
|
||||||
|
{
|
||||||
|
$help = <<<HELP
|
||||||
|
AleShell2 Terminal - Available Commands
|
||||||
|
|
||||||
|
Built-in Commands:
|
||||||
|
cd [dir] Change directory
|
||||||
|
pwd Print working directory
|
||||||
|
clear Clear terminal screen
|
||||||
|
history Show command history
|
||||||
|
help Show this help message
|
||||||
|
whoami Show current user
|
||||||
|
id Show user ID info
|
||||||
|
exit Exit info
|
||||||
|
|
||||||
|
System Commands:
|
||||||
|
All standard Unix/Linux commands are available.
|
||||||
|
|
||||||
|
Keyboard Shortcuts:
|
||||||
|
Up/Down Navigate command history
|
||||||
|
Ctrl+L Clear screen
|
||||||
|
Ctrl+C Cancel current input
|
||||||
|
Tab Auto-complete (when available)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Some commands may be restricted for security
|
||||||
|
- Long-running commands have a timeout limit
|
||||||
|
- Binary output will be truncated
|
||||||
|
|
||||||
|
HELP;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'output' => $help,
|
||||||
|
'exit_code' => 0,
|
||||||
|
'cwd' => getcwd(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exit command
|
||||||
|
*/
|
||||||
|
private function cmdExit(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'output' => "Use the logout button to exit the shell.",
|
||||||
|
'exit_code' => 0,
|
||||||
|
'cwd' => getcwd(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whoami command
|
||||||
|
*/
|
||||||
|
private function cmdWhoami(): array
|
||||||
|
{
|
||||||
|
$user = get_current_user() ?: (posix_getpwuid(posix_geteuid())['name'] ?? 'unknown');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'output' => $user,
|
||||||
|
'exit_code' => 0,
|
||||||
|
'cwd' => getcwd(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID command
|
||||||
|
*/
|
||||||
|
private function cmdId(): array
|
||||||
|
{
|
||||||
|
if (function_exists('posix_getuid')) {
|
||||||
|
$uid = posix_getuid();
|
||||||
|
$gid = posix_getgid();
|
||||||
|
$user = posix_getpwuid($uid)['name'] ?? 'unknown';
|
||||||
|
$group = posix_getgrgid($gid)['name'] ?? 'unknown';
|
||||||
|
$output = "uid={$uid}({$user}) gid={$gid}({$group})";
|
||||||
|
} else {
|
||||||
|
$output = "id: POSIX functions not available";
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'output' => $output,
|
||||||
|
'exit_code' => 0,
|
||||||
|
'cwd' => getcwd(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute system command
|
||||||
|
*/
|
||||||
|
private function executeSystemCommand(string $command, string $workingDir): array
|
||||||
|
{
|
||||||
|
if (!$this->isPathAllowed($workingDir)) {
|
||||||
|
return [
|
||||||
|
'output' => "Permission denied: Cannot execute in this directory",
|
||||||
|
'exit_code' => 1,
|
||||||
|
'cwd' => $workingDir,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$timeout = $this->config['limits']['command_timeout'] ?? 30;
|
||||||
|
$originalDir = getcwd();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Change to working directory
|
||||||
|
if (!chdir($workingDir)) {
|
||||||
|
throw new \Exception("Cannot change to directory: {$workingDir}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up process
|
||||||
|
$descriptorSpec = [
|
||||||
|
0 => ['pipe', 'r'], // stdin
|
||||||
|
1 => ['pipe', 'w'], // stdout
|
||||||
|
2 => ['pipe', 'w'], // stderr
|
||||||
|
];
|
||||||
|
|
||||||
|
$env = $_ENV;
|
||||||
|
$env['PATH'] = '/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin';
|
||||||
|
$env['HOME'] = $_SERVER['HOME'] ?? '/root';
|
||||||
|
$env['TERM'] = 'xterm-256color';
|
||||||
|
|
||||||
|
$process = proc_open($command, $descriptorSpec, $pipes, $workingDir, $env);
|
||||||
|
|
||||||
|
if (!is_resource($process)) {
|
||||||
|
throw new \Exception('Failed to execute command');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close stdin
|
||||||
|
fclose($pipes[0]);
|
||||||
|
|
||||||
|
// Set non-blocking mode
|
||||||
|
stream_set_blocking($pipes[1], false);
|
||||||
|
stream_set_blocking($pipes[2], false);
|
||||||
|
|
||||||
|
// Read output with timeout
|
||||||
|
$output = '';
|
||||||
|
$error = '';
|
||||||
|
$startTime = time();
|
||||||
|
$maxOutput = 1024 * 1024; // 1MB limit
|
||||||
|
|
||||||
|
while (time() - $startTime < $timeout) {
|
||||||
|
$stdout = stream_get_contents($pipes[1]);
|
||||||
|
$stderr = stream_get_contents($pipes[2]);
|
||||||
|
|
||||||
|
if ($stdout) $output .= $stdout;
|
||||||
|
if ($stderr) $error .= $stderr;
|
||||||
|
|
||||||
|
// Check if process has ended
|
||||||
|
$status = proc_get_status($process);
|
||||||
|
if (!$status['running']) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent memory issues
|
||||||
|
if (strlen($output) + strlen($error) > $maxOutput) {
|
||||||
|
$output .= "\n[Output truncated - exceeds limit]";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
usleep(10000); // 10ms sleep
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read any remaining output
|
||||||
|
$output .= stream_get_contents($pipes[1]);
|
||||||
|
$error .= stream_get_contents($pipes[2]);
|
||||||
|
|
||||||
|
fclose($pipes[1]);
|
||||||
|
fclose($pipes[2]);
|
||||||
|
|
||||||
|
$exitCode = proc_close($process);
|
||||||
|
|
||||||
|
// Combine output
|
||||||
|
$finalOutput = $output;
|
||||||
|
if ($error) {
|
||||||
|
$finalOutput .= ($finalOutput ? "\n" : '') . $error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current directory (might have changed)
|
||||||
|
$newCwd = getcwd();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'output' => $finalOutput,
|
||||||
|
'exit_code' => $exitCode,
|
||||||
|
'cwd' => $newCwd,
|
||||||
|
];
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return [
|
||||||
|
'output' => "Error: " . $e->getMessage(),
|
||||||
|
'exit_code' => 1,
|
||||||
|
'cwd' => $workingDir,
|
||||||
|
];
|
||||||
|
} finally {
|
||||||
|
chdir($originalDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add command to history
|
||||||
|
*/
|
||||||
|
private function addToHistory(string $command): void
|
||||||
|
{
|
||||||
|
$app = Application::getInstance();
|
||||||
|
$session = $app->getSession();
|
||||||
|
|
||||||
|
$history = $session->get('terminal_history', []);
|
||||||
|
|
||||||
|
// Don't add duplicates
|
||||||
|
if (end($history) !== $command) {
|
||||||
|
$history[] = $command;
|
||||||
|
|
||||||
|
// Limit history size
|
||||||
|
$maxHistory = $this->config['limits']['max_history'] ?? 100;
|
||||||
|
if (count($history) > $maxHistory) {
|
||||||
|
$history = array_slice($history, -$maxHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
$session->set('terminal_history', $history);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
221
src/Security/Auth.php
Archivo normal
221
src/Security/Auth.php
Archivo normal
@@ -0,0 +1,221 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Authentication
|
||||||
|
*
|
||||||
|
* Handles user authentication.
|
||||||
|
*
|
||||||
|
* @package AleShell2\Security
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace AleShell2\Security;
|
||||||
|
|
||||||
|
class Auth
|
||||||
|
{
|
||||||
|
private array $config;
|
||||||
|
private Session $session;
|
||||||
|
|
||||||
|
public function __construct(array $config, Session $session)
|
||||||
|
{
|
||||||
|
$this->config = $config;
|
||||||
|
$this->session = $session;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user is authenticated
|
||||||
|
*/
|
||||||
|
public function isAuthenticated(): bool
|
||||||
|
{
|
||||||
|
return $this->session->get('authenticated') === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to authenticate user
|
||||||
|
*/
|
||||||
|
public function attempt(string $password): bool
|
||||||
|
{
|
||||||
|
// Check rate limiting
|
||||||
|
if ($this->isRateLimited()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get stored password hash
|
||||||
|
$storedHash = $this->config['security']['password'] ?? '';
|
||||||
|
|
||||||
|
// Verify password
|
||||||
|
if (password_verify($password, $storedHash)) {
|
||||||
|
$this->login();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record failed attempt
|
||||||
|
$this->recordFailedAttempt();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log user in
|
||||||
|
*/
|
||||||
|
public function login(): void
|
||||||
|
{
|
||||||
|
// Regenerate session ID
|
||||||
|
$this->session->regenerateId();
|
||||||
|
|
||||||
|
// Set authentication data
|
||||||
|
$this->session->set('authenticated', true);
|
||||||
|
$this->session->set('login_time', time());
|
||||||
|
$this->session->set('last_activity', time());
|
||||||
|
$this->session->set('user_ip', $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0');
|
||||||
|
$this->session->set('csrf_token', $this->generateCsrfToken());
|
||||||
|
|
||||||
|
// Clear failed attempts
|
||||||
|
$this->clearFailedAttempts();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log user out
|
||||||
|
*/
|
||||||
|
public function logout(): void
|
||||||
|
{
|
||||||
|
$this->session->destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if rate limited
|
||||||
|
*/
|
||||||
|
public function isRateLimited(): bool
|
||||||
|
{
|
||||||
|
$attempts = $this->session->get('failed_attempts', []);
|
||||||
|
$maxAttempts = $this->config['security']['max_attempts'] ?? 5;
|
||||||
|
$lockoutTime = $this->config['security']['lockout_time'] ?? 300;
|
||||||
|
|
||||||
|
// Clean old attempts
|
||||||
|
$attempts = array_filter($attempts, function ($time) use ($lockoutTime) {
|
||||||
|
return time() - $time < $lockoutTime;
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->session->set('failed_attempts', $attempts);
|
||||||
|
|
||||||
|
return count($attempts) >= $maxAttempts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get remaining lockout time
|
||||||
|
*/
|
||||||
|
public function getRemainingLockoutTime(): int
|
||||||
|
{
|
||||||
|
$attempts = $this->session->get('failed_attempts', []);
|
||||||
|
$lockoutTime = $this->config['security']['lockout_time'] ?? 300;
|
||||||
|
|
||||||
|
if (empty($attempts)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$firstAttempt = min($attempts);
|
||||||
|
$elapsed = time() - $firstAttempt;
|
||||||
|
|
||||||
|
return max(0, $lockoutTime - $elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record failed login attempt
|
||||||
|
*/
|
||||||
|
private function recordFailedAttempt(): void
|
||||||
|
{
|
||||||
|
$attempts = $this->session->get('failed_attempts', []);
|
||||||
|
$attempts[] = time();
|
||||||
|
$this->session->set('failed_attempts', $attempts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear failed attempts
|
||||||
|
*/
|
||||||
|
private function clearFailedAttempts(): void
|
||||||
|
{
|
||||||
|
$this->session->remove('failed_attempts');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate CSRF token
|
||||||
|
*/
|
||||||
|
public function generateCsrfToken(): string
|
||||||
|
{
|
||||||
|
$token = bin2hex(random_bytes(32));
|
||||||
|
$this->session->set('csrf_token', $token);
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get CSRF token
|
||||||
|
*/
|
||||||
|
public function getCsrfToken(): string
|
||||||
|
{
|
||||||
|
$token = $this->session->get('csrf_token');
|
||||||
|
|
||||||
|
if (!$token) {
|
||||||
|
$token = $this->generateCsrfToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate CSRF token
|
||||||
|
*/
|
||||||
|
public function validateCsrfToken(string $token): bool
|
||||||
|
{
|
||||||
|
if (!($this->config['security']['csrf_protection'] ?? true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$storedToken = $this->session->get('csrf_token');
|
||||||
|
|
||||||
|
if (!$storedToken || !$token) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash_equals($storedToken, $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get login time
|
||||||
|
*/
|
||||||
|
public function getLoginTime(): ?int
|
||||||
|
{
|
||||||
|
return $this->session->get('login_time');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get last activity time
|
||||||
|
*/
|
||||||
|
public function getLastActivity(): ?int
|
||||||
|
{
|
||||||
|
return $this->session->get('last_activity');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update last activity
|
||||||
|
*/
|
||||||
|
public function updateActivity(): void
|
||||||
|
{
|
||||||
|
$this->session->set('last_activity', time());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user IP
|
||||||
|
*/
|
||||||
|
public function getUserIP(): string
|
||||||
|
{
|
||||||
|
return $this->session->get('user_ip', '0.0.0.0');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash a password
|
||||||
|
*/
|
||||||
|
public static function hashPassword(string $password): string
|
||||||
|
{
|
||||||
|
return password_hash($password, PASSWORD_BCRYPT, ['cost' => 10]);
|
||||||
|
}
|
||||||
|
}
|
||||||
205
src/Security/Session.php
Archivo normal
205
src/Security/Session.php
Archivo normal
@@ -0,0 +1,205 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Session Manager
|
||||||
|
*
|
||||||
|
* Handles session management.
|
||||||
|
*
|
||||||
|
* @package AleShell2\Security
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace AleShell2\Security;
|
||||||
|
|
||||||
|
class Session
|
||||||
|
{
|
||||||
|
private array $config;
|
||||||
|
private bool $started = false;
|
||||||
|
|
||||||
|
public function __construct(array $config)
|
||||||
|
{
|
||||||
|
$this->config = $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start session
|
||||||
|
*/
|
||||||
|
public function start(): void
|
||||||
|
{
|
||||||
|
if ($this->started || session_status() === PHP_SESSION_ACTIVE) {
|
||||||
|
$this->started = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure session
|
||||||
|
$this->configure();
|
||||||
|
|
||||||
|
// Start session
|
||||||
|
session_start();
|
||||||
|
$this->started = true;
|
||||||
|
|
||||||
|
// Check session timeout
|
||||||
|
$this->checkTimeout();
|
||||||
|
|
||||||
|
// Regenerate session ID periodically
|
||||||
|
$this->regenerate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure session settings
|
||||||
|
*/
|
||||||
|
private function configure(): void
|
||||||
|
{
|
||||||
|
// Session name
|
||||||
|
session_name('ALESHELL_SESS');
|
||||||
|
|
||||||
|
// Cookie settings
|
||||||
|
$cookieParams = [
|
||||||
|
'lifetime' => 0,
|
||||||
|
'path' => '/',
|
||||||
|
'domain' => '',
|
||||||
|
'secure' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Strict',
|
||||||
|
];
|
||||||
|
|
||||||
|
session_set_cookie_params($cookieParams);
|
||||||
|
|
||||||
|
// Use strict mode
|
||||||
|
ini_set('session.use_strict_mode', '1');
|
||||||
|
ini_set('session.use_only_cookies', '1');
|
||||||
|
ini_set('session.use_trans_sid', '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check session timeout
|
||||||
|
*/
|
||||||
|
private function checkTimeout(): void
|
||||||
|
{
|
||||||
|
$timeout = $this->config['security']['session_timeout'] ?? 3600;
|
||||||
|
|
||||||
|
if (isset($_SESSION['last_activity'])) {
|
||||||
|
if (time() - $_SESSION['last_activity'] > $timeout) {
|
||||||
|
// Session expired
|
||||||
|
$this->destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['last_activity'] = time();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerate session ID periodically
|
||||||
|
*/
|
||||||
|
private function regenerate(): void
|
||||||
|
{
|
||||||
|
$regenerateInterval = 300; // 5 minutes
|
||||||
|
|
||||||
|
if (!isset($_SESSION['created'])) {
|
||||||
|
$_SESSION['created'] = time();
|
||||||
|
} elseif (time() - $_SESSION['created'] > $regenerateInterval) {
|
||||||
|
session_regenerate_id(true);
|
||||||
|
$_SESSION['created'] = time();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set session value
|
||||||
|
*/
|
||||||
|
public function set(string $key, mixed $value): void
|
||||||
|
{
|
||||||
|
$_SESSION[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get session value
|
||||||
|
*/
|
||||||
|
public function get(string $key, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
return $_SESSION[$key] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if session key exists
|
||||||
|
*/
|
||||||
|
public function has(string $key): bool
|
||||||
|
{
|
||||||
|
return isset($_SESSION[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove session key
|
||||||
|
*/
|
||||||
|
public function remove(string $key): void
|
||||||
|
{
|
||||||
|
unset($_SESSION[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all session data
|
||||||
|
*/
|
||||||
|
public function all(): array
|
||||||
|
{
|
||||||
|
return $_SESSION ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flash message (available only for next request)
|
||||||
|
*/
|
||||||
|
public function flash(string $key, mixed $value): void
|
||||||
|
{
|
||||||
|
$_SESSION['_flash'][$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get flash message
|
||||||
|
*/
|
||||||
|
public function getFlash(string $key, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
$value = $_SESSION['_flash'][$key] ?? $default;
|
||||||
|
unset($_SESSION['_flash'][$key]);
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy session
|
||||||
|
*/
|
||||||
|
public function destroy(): void
|
||||||
|
{
|
||||||
|
$_SESSION = [];
|
||||||
|
|
||||||
|
if (ini_get('session.use_cookies')) {
|
||||||
|
$params = session_get_cookie_params();
|
||||||
|
setcookie(
|
||||||
|
session_name(),
|
||||||
|
'',
|
||||||
|
time() - 42000,
|
||||||
|
$params['path'],
|
||||||
|
$params['domain'],
|
||||||
|
$params['secure'],
|
||||||
|
$params['httponly']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
session_destroy();
|
||||||
|
$this->started = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get session ID
|
||||||
|
*/
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return session_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerate session ID
|
||||||
|
*/
|
||||||
|
public function regenerateId(): void
|
||||||
|
{
|
||||||
|
session_regenerate_id(true);
|
||||||
|
$_SESSION['created'] = time();
|
||||||
|
}
|
||||||
|
}
|
||||||
246
src/Views/auth/login.php
Archivo normal
246
src/Views/auth/login.php
Archivo normal
@@ -0,0 +1,246 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Login View
|
||||||
|
*
|
||||||
|
* @var array $data View data
|
||||||
|
*/
|
||||||
|
|
||||||
|
$error = $data['error'] ?? '';
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login - AleShell2</title>
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🐚</text></svg>">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-primary: #0d1117;
|
||||||
|
--bg-secondary: #161b22;
|
||||||
|
--bg-tertiary: #21262d;
|
||||||
|
--border-color: #30363d;
|
||||||
|
--text-primary: #c9d1d9;
|
||||||
|
--text-secondary: #8b949e;
|
||||||
|
--accent-primary: #58a6ff;
|
||||||
|
--accent-danger: #f85149;
|
||||||
|
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo {
|
||||||
|
font-size: 64px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-subtitle {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input::placeholder {
|
||||||
|
color: #6e7681;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--accent-primary);
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn:hover {
|
||||||
|
background: #4c9aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
background: rgba(248, 81, 73, 0.1);
|
||||||
|
border: 1px solid rgba(248, 81, 73, 0.3);
|
||||||
|
color: var(--accent-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-info {
|
||||||
|
margin-top: 32px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-info-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-info-row:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-info-label {
|
||||||
|
color: #6e7681;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(-10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-header">
|
||||||
|
<div class="login-logo">🐚</div>
|
||||||
|
<h1 class="login-title">AleShell2</h1>
|
||||||
|
<p class="login-subtitle">Web Administration Shell</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="login-card">
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<?= htmlspecialchars($error) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="POST" action="?action=login" autocomplete="off">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token'] ?? '') ?>">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="password">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
class="form-input"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
autofocus
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="login-btn">
|
||||||
|
🔐 Sign In
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="server-info">
|
||||||
|
<div class="server-info-row">
|
||||||
|
<span class="server-info-label">Server</span>
|
||||||
|
<span><?= htmlspecialchars($_SERVER['SERVER_SOFTWARE'] ?? 'Unknown') ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="server-info-row">
|
||||||
|
<span class="server-info-label">PHP Version</span>
|
||||||
|
<span><?= PHP_VERSION ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="server-info-row">
|
||||||
|
<span class="server-info-label">Your IP</span>
|
||||||
|
<span><?= htmlspecialchars($_SERVER['REMOTE_ADDR'] ?? 'Unknown') ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="login-footer">
|
||||||
|
AleShell2 v1.0.0 © <?= date('Y') ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1055
src/Views/layouts/main.php
Archivo normal
1055
src/Views/layouts/main.php
Archivo normal
La diferencia del archivo ha sido suprimido porque es demasiado grande
Cargar Diff
208
src/Views/modules/dashboard.php
Archivo normal
208
src/Views/modules/dashboard.php
Archivo normal
@@ -0,0 +1,208 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Dashboard View
|
||||||
|
*
|
||||||
|
* @var array $data View data
|
||||||
|
*/
|
||||||
|
|
||||||
|
$systemInfo = $data['systemInfo'] ?? [];
|
||||||
|
$server = $systemInfo['server'] ?? [];
|
||||||
|
$php = $systemInfo['php'] ?? [];
|
||||||
|
$hardware = $systemInfo['hardware'] ?? [];
|
||||||
|
$disk = $systemInfo['disk'] ?? [];
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="grid grid-4 mb-3">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Server</div>
|
||||||
|
<div class="stat-value" style="font-size: 16px;"><?= htmlspecialchars($server['hostname'] ?? 'Unknown') ?></div>
|
||||||
|
<div class="stat-meta"><?= htmlspecialchars($server['os'] ?? 'Unknown OS') ?></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">PHP Version</div>
|
||||||
|
<div class="stat-value" style="font-size: 20px;"><?= htmlspecialchars($php['version'] ?? 'Unknown') ?></div>
|
||||||
|
<div class="stat-meta"><?= htmlspecialchars($php['sapi'] ?? '') ?></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Load Average</div>
|
||||||
|
<div class="stat-value" style="font-size: 20px;"><?= htmlspecialchars($server['load_average']['1min'] ?? 'N/A') ?></div>
|
||||||
|
<div class="stat-meta">
|
||||||
|
5m: <?= htmlspecialchars($server['load_average']['5min'] ?? 'N/A') ?> /
|
||||||
|
15m: <?= htmlspecialchars($server['load_average']['15min'] ?? 'N/A') ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Uptime</div>
|
||||||
|
<div class="stat-value" style="font-size: 20px;"><?= htmlspecialchars($server['uptime'] ?? 'Unknown') ?></div>
|
||||||
|
<div class="stat-meta">Since boot</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-2 mb-3">
|
||||||
|
<!-- Server Information -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">🖥️ Server Information</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Hostname</td>
|
||||||
|
<td><?= htmlspecialchars($server['hostname'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">OS</td>
|
||||||
|
<td><?= htmlspecialchars($server['os_detail'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Server Software</td>
|
||||||
|
<td><?= htmlspecialchars($server['server_software'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Server Address</td>
|
||||||
|
<td><?= htmlspecialchars($server['server_addr'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Document Root</td>
|
||||||
|
<td class="text-mono"><?= htmlspecialchars($server['document_root'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Current User</td>
|
||||||
|
<td><?= htmlspecialchars($server['current_user'] ?? 'Unknown') ?> (uid: <?= $server['current_uid'] ?? '?' ?>, gid: <?= $server['current_gid'] ?? '?' ?>)</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PHP Information -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">🐘 PHP Information</h3>
|
||||||
|
<a href="?module=system&action=phpinfo" target="_blank" class="btn btn-sm btn-secondary">View phpinfo()</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Version</td>
|
||||||
|
<td><?= htmlspecialchars($php['version'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">SAPI</td>
|
||||||
|
<td><?= htmlspecialchars($php['sapi'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Memory Limit</td>
|
||||||
|
<td><?= htmlspecialchars($php['memory_limit'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Max Execution Time</td>
|
||||||
|
<td><?= htmlspecialchars($php['max_execution_time'] ?? 'Unknown') ?>s</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Upload Max Size</td>
|
||||||
|
<td><?= htmlspecialchars($php['upload_max_filesize'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Disabled Functions</td>
|
||||||
|
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="<?= htmlspecialchars($php['disabled_functions'] ?? 'none') ?>">
|
||||||
|
<?= htmlspecialchars(strlen($php['disabled_functions'] ?? '') > 50 ? substr($php['disabled_functions'], 0, 50) . '...' : ($php['disabled_functions'] ?? 'none')) ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-2 mb-3">
|
||||||
|
<!-- Hardware Information -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">💾 Hardware Information</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">CPU</td>
|
||||||
|
<td><?= htmlspecialchars($hardware['cpu'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">CPU Cores</td>
|
||||||
|
<td><?= htmlspecialchars($hardware['cpu_cores'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Total Memory</td>
|
||||||
|
<td><?= htmlspecialchars($hardware['memory_total'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Free Memory</td>
|
||||||
|
<td><?= htmlspecialchars($hardware['memory_free'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Available Memory</td>
|
||||||
|
<td><?= htmlspecialchars($hardware['memory_available'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Disk Information -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">💿 Disk Usage</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if (!empty($disk)): ?>
|
||||||
|
<table class="table table-mono">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Mount</th>
|
||||||
|
<th>Total</th>
|
||||||
|
<th>Used</th>
|
||||||
|
<th>Free</th>
|
||||||
|
<th>%</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach (array_slice($disk, 0, 5) as $d): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= htmlspecialchars($d['mount'] ?? $d['device'] ?? '-') ?></td>
|
||||||
|
<td><?= htmlspecialchars($d['total'] ?? '-') ?></td>
|
||||||
|
<td><?= htmlspecialchars($d['used'] ?? '-') ?></td>
|
||||||
|
<td><?= htmlspecialchars($d['free'] ?? '-') ?></td>
|
||||||
|
<td><?= htmlspecialchars($d['percent_used'] ?? '-') ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php else: ?>
|
||||||
|
<p class="text-muted">No disk information available</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">⚡ Quick Actions</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="flex gap-2" style="flex-wrap: wrap;">
|
||||||
|
<a href="?module=files" class="btn btn-secondary">📁 Browse Files</a>
|
||||||
|
<a href="?module=terminal" class="btn btn-secondary">💻 Open Terminal</a>
|
||||||
|
<a href="?module=processes" class="btn btn-secondary">⚙️ View Processes</a>
|
||||||
|
<a href="?module=network" class="btn btn-secondary">🌐 Network Tools</a>
|
||||||
|
<a href="?module=database" class="btn btn-secondary">🗄️ Database Manager</a>
|
||||||
|
<a href="?module=system" class="btn btn-secondary">🖥️ System Info</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
323
src/Views/modules/database.php
Archivo normal
323
src/Views/modules/database.php
Archivo normal
@@ -0,0 +1,323 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Database View
|
||||||
|
*
|
||||||
|
* @var array $data View data
|
||||||
|
*/
|
||||||
|
|
||||||
|
$connected = isset($_SESSION['db_connection']);
|
||||||
|
$connection = $_SESSION['db_connection'] ?? [];
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="grid grid-4 mb-3" style="grid-template-columns: 300px 1fr;">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div>
|
||||||
|
<!-- Connection Card -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">🔌 Connection</h3>
|
||||||
|
<?php if ($connected): ?>
|
||||||
|
<span class="badge badge-success">Connected</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if (!$connected): ?>
|
||||||
|
<form id="connect-form" onsubmit="doConnect(event)">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Driver</label>
|
||||||
|
<select id="db-driver" class="form-select">
|
||||||
|
<option value="mysql">MySQL</option>
|
||||||
|
<option value="pgsql">PostgreSQL</option>
|
||||||
|
<option value="sqlite">SQLite</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="mysql-fields">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Host</label>
|
||||||
|
<input type="text" id="db-host" class="form-input" value="localhost">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Port</label>
|
||||||
|
<input type="number" id="db-port" class="form-input" value="3306">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Database</label>
|
||||||
|
<input type="text" id="db-name" class="form-input" placeholder="database_name">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Username</label>
|
||||||
|
<input type="text" id="db-user" class="form-input" placeholder="root">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Password</label>
|
||||||
|
<input type="password" id="db-pass" class="form-input">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="sqlite-fields" class="hidden">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Database File</label>
|
||||||
|
<input type="text" id="db-file" class="form-input" placeholder="/path/to/database.sqlite">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary" style="width: 100%;">Connect</button>
|
||||||
|
</form>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="mb-2">
|
||||||
|
<div class="text-muted" style="font-size: 12px;">Driver</div>
|
||||||
|
<div><?= htmlspecialchars(strtoupper($connection['driver'] ?? '')) ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<div class="text-muted" style="font-size: 12px;">Host</div>
|
||||||
|
<div><?= htmlspecialchars($connection['host'] ?? '') ?>:<?= htmlspecialchars($connection['port'] ?? '') ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="text-muted" style="font-size: 12px;">Database</div>
|
||||||
|
<div><?= htmlspecialchars($connection['database'] ?? '') ?></div>
|
||||||
|
</div>
|
||||||
|
<button onclick="doDisconnect()" class="btn btn-danger" style="width: 100%;">Disconnect</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Databases / Tables -->
|
||||||
|
<?php if ($connected): ?>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">📂 Structure</h3>
|
||||||
|
<button onclick="refreshStructure()" class="btn btn-sm btn-secondary">🔄</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" style="padding: 0; max-height: 400px; overflow-y: auto;">
|
||||||
|
<div id="db-structure">
|
||||||
|
<div class="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div>
|
||||||
|
<?php if ($connected): ?>
|
||||||
|
<!-- Query Editor -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">📝 Query Editor</h3>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button onclick="executeQuery()" class="btn btn-primary">▶️ Execute</button>
|
||||||
|
<button onclick="clearQuery()" class="btn btn-secondary">Clear</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<textarea id="query-editor" class="code-editor" placeholder="SELECT * FROM table_name LIMIT 100;" style="min-height: 150px;"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Results -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">📊 Results</h3>
|
||||||
|
<span id="query-stats" class="text-muted"></span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" style="padding: 0;">
|
||||||
|
<div id="query-results" class="table-container" style="max-height: 400px; overflow: auto;">
|
||||||
|
<div class="text-muted text-center" style="padding: 40px;">
|
||||||
|
Execute a query to see results
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center" style="padding: 60px;">
|
||||||
|
<div style="font-size: 48px; margin-bottom: 16px;">🗄️</div>
|
||||||
|
<h3>Database Manager</h3>
|
||||||
|
<p class="text-muted mt-2">Connect to a database to start managing your data</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Toggle SQLite fields
|
||||||
|
document.getElementById('db-driver')?.addEventListener('change', function() {
|
||||||
|
const isSqlite = this.value === 'sqlite';
|
||||||
|
document.getElementById('mysql-fields').classList.toggle('hidden', isSqlite);
|
||||||
|
document.getElementById('sqlite-fields').classList.toggle('hidden', !isSqlite);
|
||||||
|
|
||||||
|
if (this.value === 'pgsql') {
|
||||||
|
document.getElementById('db-port').value = '5432';
|
||||||
|
} else if (this.value === 'mysql') {
|
||||||
|
document.getElementById('db-port').value = '3306';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect to database
|
||||||
|
async function doConnect(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const driver = document.getElementById('db-driver').value;
|
||||||
|
const data = { driver };
|
||||||
|
|
||||||
|
if (driver === 'sqlite') {
|
||||||
|
data.database = document.getElementById('db-file').value;
|
||||||
|
} else {
|
||||||
|
data.host = document.getElementById('db-host').value;
|
||||||
|
data.port = document.getElementById('db-port').value;
|
||||||
|
data.database = document.getElementById('db-name').value;
|
||||||
|
data.username = document.getElementById('db-user').value;
|
||||||
|
data.password = document.getElementById('db-pass').value;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api('?module=database&action=connect', 'POST', data);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
toast('Connected to database', 'success');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
toast(response.error, 'danger');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast('Connection failed: ' + error.message, 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect
|
||||||
|
async function doDisconnect() {
|
||||||
|
try {
|
||||||
|
await api('?module=database&action=disconnect', 'POST', {});
|
||||||
|
toast('Disconnected', 'success');
|
||||||
|
location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh structure
|
||||||
|
async function refreshStructure() {
|
||||||
|
const container = document.getElementById('db-structure');
|
||||||
|
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api('?module=database&action=tables');
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
const tables = response.data.tables;
|
||||||
|
|
||||||
|
if (tables.length === 0) {
|
||||||
|
container.innerHTML = '<div class="text-muted" style="padding: 16px;">No tables found</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
for (const table of tables) {
|
||||||
|
html += `
|
||||||
|
<div class="file-item" onclick="selectTable('${escapeHtml(table)}')" style="cursor: pointer;">
|
||||||
|
<span class="file-icon">📋</span>
|
||||||
|
<span class="file-name">${escapeHtml(table)}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
container.innerHTML = html;
|
||||||
|
} else {
|
||||||
|
container.innerHTML = `<div class="text-danger" style="padding: 16px;">${escapeHtml(response.error)}</div>`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
container.innerHTML = '<div class="text-danger" style="padding: 16px;">Failed to load tables</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select table
|
||||||
|
function selectTable(table) {
|
||||||
|
document.getElementById('query-editor').value = `SELECT * FROM \`${table}\` LIMIT 100;`;
|
||||||
|
executeQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute query
|
||||||
|
async function executeQuery() {
|
||||||
|
const query = document.getElementById('query-editor').value.trim();
|
||||||
|
const resultsDiv = document.getElementById('query-results');
|
||||||
|
const statsSpan = document.getElementById('query-stats');
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
toast('Please enter a query', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultsDiv.innerHTML = '<div class="loading"><div class="spinner"></div>Executing query...</div>';
|
||||||
|
statsSpan.textContent = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api('?module=database&action=query', 'POST', { query });
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
statsSpan.textContent = `${data.affected_rows} rows | ${data.execution_time}`;
|
||||||
|
|
||||||
|
if (data.rows !== undefined) {
|
||||||
|
// SELECT query
|
||||||
|
if (data.rows.length === 0) {
|
||||||
|
resultsDiv.innerHTML = '<div class="text-muted text-center" style="padding: 40px;">No results</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '<table class="table table-mono"><thead><tr>';
|
||||||
|
for (const col of data.columns) {
|
||||||
|
html += `<th>${escapeHtml(col)}</th>`;
|
||||||
|
}
|
||||||
|
html += '</tr></thead><tbody>';
|
||||||
|
|
||||||
|
for (const row of data.rows) {
|
||||||
|
html += '<tr>';
|
||||||
|
for (const col of data.columns) {
|
||||||
|
const value = row[col];
|
||||||
|
const displayValue = value === null ? '<span class="text-muted">NULL</span>' : escapeHtml(String(value));
|
||||||
|
html += `<td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${displayValue}</td>`;
|
||||||
|
}
|
||||||
|
html += '</tr>';
|
||||||
|
}
|
||||||
|
html += '</tbody></table>';
|
||||||
|
|
||||||
|
if (data.limited) {
|
||||||
|
html += `<div class="text-warning text-center" style="padding: 8px; font-size: 12px;">Results limited. Total: ${data.total_rows} rows</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultsDiv.innerHTML = html;
|
||||||
|
} else {
|
||||||
|
// Non-SELECT query
|
||||||
|
resultsDiv.innerHTML = `<div class="alert alert-success" style="margin: 16px;">Query executed successfully. Affected rows: ${data.affected_rows}</div>`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resultsDiv.innerHTML = `<div class="alert alert-danger" style="margin: 16px;">${escapeHtml(response.error)}</div>`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
resultsDiv.innerHTML = '<div class="alert alert-danger" style="margin: 16px;">Query execution failed</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear query
|
||||||
|
function clearQuery() {
|
||||||
|
document.getElementById('query-editor').value = '';
|
||||||
|
document.getElementById('query-results').innerHTML = '<div class="text-muted text-center" style="padding: 40px;">Execute a query to see results</div>';
|
||||||
|
document.getElementById('query-stats').textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard shortcuts
|
||||||
|
document.getElementById('query-editor')?.addEventListener('keydown', (e) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
executeQuery();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
if (document.getElementById('db-structure')) {
|
||||||
|
refreshStructure();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
191
src/Views/modules/editor.php
Archivo normal
191
src/Views/modules/editor.php
Archivo normal
@@ -0,0 +1,191 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Code Editor View
|
||||||
|
*
|
||||||
|
* @var array $data View data
|
||||||
|
*/
|
||||||
|
|
||||||
|
$filePath = $data['filePath'] ?? '';
|
||||||
|
$content = $data['content'] ?? '';
|
||||||
|
$filename = basename($filePath) ?: 'untitled.txt';
|
||||||
|
$extension = pathinfo($filename, PATHINFO_EXTENSION);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="flex items-center gap-2" style="flex: 1;">
|
||||||
|
<span>📝</span>
|
||||||
|
<input type="text" id="file-path" class="form-input form-input-mono" value="<?= htmlspecialchars($filePath) ?>" placeholder="/path/to/file.txt" style="flex: 1;">
|
||||||
|
<button onclick="loadFile()" class="btn btn-secondary">Open</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button onclick="saveFile()" class="btn btn-primary">💾 Save</button>
|
||||||
|
<button onclick="saveAsFile()" class="btn btn-secondary">Save As</button>
|
||||||
|
<button onclick="downloadFile()" class="btn btn-secondary">⬇️ Download</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" style="padding: 0;">
|
||||||
|
<div style="display: flex; align-items: center; padding: 8px 16px; background: var(--bg-tertiary); border-bottom: 1px solid var(--border-color);">
|
||||||
|
<span class="text-muted" style="font-size: 12px;">
|
||||||
|
<?= htmlspecialchars($filename) ?>
|
||||||
|
<?php if ($extension): ?>
|
||||||
|
<span class="badge badge-info"><?= htmlspecialchars($extension) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
<span style="flex: 1;"></span>
|
||||||
|
<span id="line-info" class="text-muted" style="font-size: 12px;">Line 1, Col 1</span>
|
||||||
|
</div>
|
||||||
|
<textarea id="editor" class="code-editor" style="border: none; border-radius: 0; min-height: 500px;"><?= htmlspecialchars($content) ?></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Save As Modal -->
|
||||||
|
<div id="saveas-modal" class="modal-backdrop">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title">Save As</h3>
|
||||||
|
<button class="modal-close" onclick="closeModal('saveas-modal')">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">File Path</label>
|
||||||
|
<input type="text" id="saveas-path" class="form-input form-input-mono" value="<?= htmlspecialchars($filePath) ?>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" onclick="closeModal('saveas-modal')">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="doSaveAs()">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const editor = document.getElementById('editor');
|
||||||
|
const filePathInput = document.getElementById('file-path');
|
||||||
|
const lineInfo = document.getElementById('line-info');
|
||||||
|
|
||||||
|
// Update line info
|
||||||
|
editor.addEventListener('keyup', updateLineInfo);
|
||||||
|
editor.addEventListener('click', updateLineInfo);
|
||||||
|
|
||||||
|
function updateLineInfo() {
|
||||||
|
const text = editor.value.substring(0, editor.selectionStart);
|
||||||
|
const lines = text.split('\n');
|
||||||
|
const line = lines.length;
|
||||||
|
const col = lines[lines.length - 1].length + 1;
|
||||||
|
lineInfo.textContent = `Line ${line}, Col ${col}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle tab key
|
||||||
|
editor.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
const start = editor.selectionStart;
|
||||||
|
const end = editor.selectionEnd;
|
||||||
|
|
||||||
|
if (e.shiftKey) {
|
||||||
|
// Remove indent
|
||||||
|
const lineStart = editor.value.lastIndexOf('\n', start - 1) + 1;
|
||||||
|
const lineText = editor.value.substring(lineStart, start);
|
||||||
|
if (lineText.startsWith(' ')) {
|
||||||
|
editor.value = editor.value.substring(0, lineStart) + editor.value.substring(lineStart + 4);
|
||||||
|
editor.selectionStart = editor.selectionEnd = start - 4;
|
||||||
|
} else if (lineText.startsWith('\t')) {
|
||||||
|
editor.value = editor.value.substring(0, lineStart) + editor.value.substring(lineStart + 1);
|
||||||
|
editor.selectionStart = editor.selectionEnd = start - 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add indent
|
||||||
|
editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end);
|
||||||
|
editor.selectionStart = editor.selectionEnd = start + 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save with Ctrl+S
|
||||||
|
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
saveFile();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load file
|
||||||
|
async function loadFile() {
|
||||||
|
const path = filePathInput.value.trim();
|
||||||
|
if (!path) {
|
||||||
|
toast('Please enter a file path', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api(`?module=files&action=read&path=${encodeURIComponent(path)}`);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
editor.value = response.data.content;
|
||||||
|
toast('File loaded', 'success');
|
||||||
|
} else {
|
||||||
|
toast(response.error, 'danger');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast('Failed to load file: ' + error.message, 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save file
|
||||||
|
async function saveFile() {
|
||||||
|
const path = filePathInput.value.trim();
|
||||||
|
if (!path) {
|
||||||
|
openModal('saveas-modal');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = editor.value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api('?module=files&action=write', 'POST', { path, content });
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
toast('File saved', 'success');
|
||||||
|
} else {
|
||||||
|
toast(response.error, 'danger');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast('Failed to save file: ' + error.message, 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save as
|
||||||
|
function saveAsFile() {
|
||||||
|
document.getElementById('saveas-path').value = filePathInput.value;
|
||||||
|
openModal('saveas-modal');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doSaveAs() {
|
||||||
|
const path = document.getElementById('saveas-path').value.trim();
|
||||||
|
if (!path) {
|
||||||
|
toast('Please enter a file path', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
filePathInput.value = path;
|
||||||
|
closeModal('saveas-modal');
|
||||||
|
await saveFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download file
|
||||||
|
function downloadFile() {
|
||||||
|
const path = filePathInput.value.trim();
|
||||||
|
if (!path) {
|
||||||
|
toast('No file to download', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.href = `?module=files&action=download&path=${encodeURIComponent(path)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load file from URL parameter
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const pathParam = urlParams.get('path');
|
||||||
|
if (pathParam) {
|
||||||
|
filePathInput.value = pathParam;
|
||||||
|
loadFile();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
559
src/Views/modules/files.php
Archivo normal
559
src/Views/modules/files.php
Archivo normal
@@ -0,0 +1,559 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 File Manager View
|
||||||
|
*
|
||||||
|
* @var array $data View data
|
||||||
|
*/
|
||||||
|
|
||||||
|
$currentPath = $data['currentPath'] ?? getcwd();
|
||||||
|
$files = $data['files'] ?? [];
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>📂</span>
|
||||||
|
<input type="text" id="path-input" class="form-input form-input-mono" value="<?= htmlspecialchars($currentPath) ?>" style="flex: 1;">
|
||||||
|
<button onclick="navigateTo(document.getElementById('path-input').value)" class="btn btn-secondary">Go</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" style="padding: 0;">
|
||||||
|
<div class="flex items-center justify-between" style="padding: 12px 16px; border-bottom: 1px solid var(--border-color);">
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button onclick="navigateUp()" class="btn btn-sm btn-secondary">⬆️ Up</button>
|
||||||
|
<button onclick="refresh()" class="btn btn-sm btn-secondary">🔄 Refresh</button>
|
||||||
|
<button onclick="openNewFileModal()" class="btn btn-sm btn-secondary">📄 New File</button>
|
||||||
|
<button onclick="openNewFolderModal()" class="btn btn-sm btn-secondary">📁 New Folder</button>
|
||||||
|
<button onclick="openUploadModal()" class="btn btn-sm btn-primary">⬆️ Upload</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<input type="text" id="search-input" class="form-input" placeholder="Search files..." style="width: 200px;" onkeyup="filterFiles(this.value)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="file-list" class="file-list">
|
||||||
|
<div class="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
Loading files...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- New File Modal -->
|
||||||
|
<div id="new-file-modal" class="modal-backdrop">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title">Create New File</h3>
|
||||||
|
<button class="modal-close" onclick="closeModal('new-file-modal')">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Filename</label>
|
||||||
|
<input type="text" id="new-file-name" class="form-input" placeholder="filename.txt">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Content (optional)</label>
|
||||||
|
<textarea id="new-file-content" class="form-textarea code-editor" rows="5" placeholder="File content..."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" onclick="closeModal('new-file-modal')">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="createNewFile()">Create File</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- New Folder Modal -->
|
||||||
|
<div id="new-folder-modal" class="modal-backdrop">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title">Create New Folder</h3>
|
||||||
|
<button class="modal-close" onclick="closeModal('new-folder-modal')">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Folder Name</label>
|
||||||
|
<input type="text" id="new-folder-name" class="form-input" placeholder="folder-name">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" onclick="closeModal('new-folder-modal')">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="createNewFolder()">Create Folder</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload Modal -->
|
||||||
|
<div id="upload-modal" class="modal-backdrop">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title">Upload Files</h3>
|
||||||
|
<button class="modal-close" onclick="closeModal('upload-modal')">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Select Files</label>
|
||||||
|
<input type="file" id="upload-files" class="form-input" multiple>
|
||||||
|
</div>
|
||||||
|
<div id="upload-progress" class="hidden">
|
||||||
|
<div style="background: var(--bg-tertiary); border-radius: 4px; height: 8px; overflow: hidden;">
|
||||||
|
<div id="upload-bar" style="width: 0%; height: 100%; background: var(--accent-primary); transition: width 0.3s;"></div>
|
||||||
|
</div>
|
||||||
|
<div id="upload-status" class="text-muted mt-1" style="font-size: 12px;">Uploading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" onclick="closeModal('upload-modal')">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="uploadFiles()">Upload</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rename Modal -->
|
||||||
|
<div id="rename-modal" class="modal-backdrop">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title">Rename</h3>
|
||||||
|
<button class="modal-close" onclick="closeModal('rename-modal')">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">New Name</label>
|
||||||
|
<input type="text" id="rename-input" class="form-input">
|
||||||
|
<input type="hidden" id="rename-old-path">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" onclick="closeModal('rename-modal')">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="doRename()">Rename</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chmod Modal -->
|
||||||
|
<div id="chmod-modal" class="modal-backdrop">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title">Change Permissions</h3>
|
||||||
|
<button class="modal-close" onclick="closeModal('chmod-modal')">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Permissions (octal)</label>
|
||||||
|
<input type="text" id="chmod-input" class="form-input" placeholder="0755">
|
||||||
|
<input type="hidden" id="chmod-path">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" onclick="closeModal('chmod-modal')">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="doChmod()">Change</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Context Menu -->
|
||||||
|
<div id="context-menu" class="context-menu">
|
||||||
|
<div class="context-menu-item" onclick="openFile()">📄 Open</div>
|
||||||
|
<div class="context-menu-item" onclick="editFile()">✏️ Edit</div>
|
||||||
|
<div class="context-menu-item" onclick="downloadFile()">⬇️ Download</div>
|
||||||
|
<div class="context-menu-divider"></div>
|
||||||
|
<div class="context-menu-item" onclick="copyFile()">📋 Copy</div>
|
||||||
|
<div class="context-menu-item" onclick="moveFile()">📦 Move</div>
|
||||||
|
<div class="context-menu-item" onclick="renameFile()">✏️ Rename</div>
|
||||||
|
<div class="context-menu-item" onclick="chmodFile()">🔒 Permissions</div>
|
||||||
|
<div class="context-menu-divider"></div>
|
||||||
|
<div class="context-menu-item danger" onclick="deleteFile()">🗑️ Delete</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentPath = '<?= addslashes($currentPath) ?>';
|
||||||
|
let files = [];
|
||||||
|
let selectedFile = null;
|
||||||
|
let clipboard = null;
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadFiles(currentPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load files from path
|
||||||
|
async function loadFiles(path) {
|
||||||
|
const fileList = document.getElementById('file-list');
|
||||||
|
fileList.innerHTML = '<div class="loading"><div class="spinner"></div>Loading files...</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api(`?module=files&action=list&path=${encodeURIComponent(path)}`);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
currentPath = response.data.path;
|
||||||
|
files = response.data.files;
|
||||||
|
document.getElementById('path-input').value = currentPath;
|
||||||
|
renderFiles();
|
||||||
|
} else {
|
||||||
|
fileList.innerHTML = `<div class="alert alert-danger" style="margin: 16px;">${escapeHtml(response.error)}</div>`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
fileList.innerHTML = `<div class="alert alert-danger" style="margin: 16px;">Failed to load files: ${escapeHtml(error.message)}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render file list
|
||||||
|
function renderFiles() {
|
||||||
|
const fileList = document.getElementById('file-list');
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
fileList.innerHTML = '<div class="text-muted" style="padding: 40px; text-align: center;">This directory is empty</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const icon = getFileIcon(file);
|
||||||
|
const size = file.type === 'file' ? formatBytes(file.size) : '-';
|
||||||
|
const perms = file.permissions || '-';
|
||||||
|
const modified = file.modified ? new Date(file.modified * 1000).toLocaleString() : '-';
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="file-item"
|
||||||
|
data-path="${escapeHtml(file.path)}"
|
||||||
|
data-type="${file.type}"
|
||||||
|
data-name="${escapeHtml(file.name)}"
|
||||||
|
onclick="selectFile(this)"
|
||||||
|
ondblclick="openItem(this)"
|
||||||
|
oncontextmenu="showContextMenu(event, this)">
|
||||||
|
<span class="file-icon">${icon}</span>
|
||||||
|
<span class="file-name">${escapeHtml(file.name)}</span>
|
||||||
|
<div class="file-meta">
|
||||||
|
<span class="text-mono">${perms}</span>
|
||||||
|
<span>${size}</span>
|
||||||
|
<span>${modified}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileList.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get file icon
|
||||||
|
function getFileIcon(file) {
|
||||||
|
if (file.type === 'directory') return '📁';
|
||||||
|
if (file.type === 'link') return '🔗';
|
||||||
|
|
||||||
|
const ext = file.name.split('.').pop().toLowerCase();
|
||||||
|
const icons = {
|
||||||
|
'php': '🐘', 'js': '📜', 'ts': '📜', 'py': '🐍', 'rb': '💎',
|
||||||
|
'html': '🌐', 'css': '🎨', 'json': '📋', 'xml': '📋', 'yml': '📋', 'yaml': '📋',
|
||||||
|
'md': '📝', 'txt': '📄', 'log': '📋',
|
||||||
|
'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️', 'svg': '🖼️', 'webp': '🖼️',
|
||||||
|
'mp3': '🎵', 'wav': '🎵', 'ogg': '🎵',
|
||||||
|
'mp4': '🎬', 'avi': '🎬', 'mkv': '🎬', 'webm': '🎬',
|
||||||
|
'zip': '📦', 'tar': '📦', 'gz': '📦', 'rar': '📦', '7z': '📦',
|
||||||
|
'pdf': '📕', 'doc': '📘', 'docx': '📘', 'xls': '📗', 'xlsx': '📗',
|
||||||
|
'sql': '🗄️', 'db': '🗄️', 'sqlite': '🗄️',
|
||||||
|
'sh': '⚙️', 'bash': '⚙️', 'zsh': '⚙️',
|
||||||
|
'exe': '⚡', 'bin': '⚡',
|
||||||
|
'conf': '⚙️', 'cfg': '⚙️', 'ini': '⚙️',
|
||||||
|
};
|
||||||
|
|
||||||
|
return icons[ext] || '📄';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select file
|
||||||
|
function selectFile(el) {
|
||||||
|
document.querySelectorAll('.file-item').forEach(f => f.style.background = '');
|
||||||
|
el.style.background = 'var(--bg-hover)';
|
||||||
|
selectedFile = {
|
||||||
|
path: el.dataset.path,
|
||||||
|
type: el.dataset.type,
|
||||||
|
name: el.dataset.name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open item (double click)
|
||||||
|
function openItem(el) {
|
||||||
|
const path = el.dataset.path;
|
||||||
|
const type = el.dataset.type;
|
||||||
|
|
||||||
|
if (type === 'directory') {
|
||||||
|
loadFiles(path);
|
||||||
|
} else {
|
||||||
|
window.location.href = `?module=editor&path=${encodeURIComponent(path)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to path
|
||||||
|
function navigateTo(path) {
|
||||||
|
loadFiles(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate up
|
||||||
|
function navigateUp() {
|
||||||
|
const parts = currentPath.split('/').filter(p => p);
|
||||||
|
parts.pop();
|
||||||
|
const newPath = '/' + parts.join('/');
|
||||||
|
loadFiles(newPath || '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh
|
||||||
|
function refresh() {
|
||||||
|
loadFiles(currentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter files
|
||||||
|
function filterFiles(query) {
|
||||||
|
query = query.toLowerCase();
|
||||||
|
document.querySelectorAll('.file-item').forEach(item => {
|
||||||
|
const name = item.dataset.name.toLowerCase();
|
||||||
|
item.style.display = name.includes(query) ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context menu
|
||||||
|
function showContextMenu(event, el) {
|
||||||
|
event.preventDefault();
|
||||||
|
selectFile(el);
|
||||||
|
|
||||||
|
const menu = document.getElementById('context-menu');
|
||||||
|
menu.style.left = event.pageX + 'px';
|
||||||
|
menu.style.top = event.pageY + 'px';
|
||||||
|
menu.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide context menu on click
|
||||||
|
document.addEventListener('click', () => {
|
||||||
|
document.getElementById('context-menu').classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Context menu actions
|
||||||
|
function openFile() {
|
||||||
|
if (!selectedFile) return;
|
||||||
|
if (selectedFile.type === 'directory') {
|
||||||
|
loadFiles(selectedFile.path);
|
||||||
|
} else {
|
||||||
|
window.open(`?module=files&action=read&path=${encodeURIComponent(selectedFile.path)}`, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function editFile() {
|
||||||
|
if (!selectedFile || selectedFile.type !== 'file') return;
|
||||||
|
window.location.href = `?module=editor&path=${encodeURIComponent(selectedFile.path)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadFile() {
|
||||||
|
if (!selectedFile || selectedFile.type !== 'file') return;
|
||||||
|
window.location.href = `?module=files&action=download&path=${encodeURIComponent(selectedFile.path)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyFile() {
|
||||||
|
if (!selectedFile) return;
|
||||||
|
clipboard = { action: 'copy', path: selectedFile.path };
|
||||||
|
toast('File copied to clipboard', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveFile() {
|
||||||
|
if (!selectedFile) return;
|
||||||
|
clipboard = { action: 'move', path: selectedFile.path };
|
||||||
|
toast('File cut to clipboard', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameFile() {
|
||||||
|
if (!selectedFile) return;
|
||||||
|
document.getElementById('rename-input').value = selectedFile.name;
|
||||||
|
document.getElementById('rename-old-path').value = selectedFile.path;
|
||||||
|
openModal('rename-modal');
|
||||||
|
}
|
||||||
|
|
||||||
|
function chmodFile() {
|
||||||
|
if (!selectedFile) return;
|
||||||
|
document.getElementById('chmod-path').value = selectedFile.path;
|
||||||
|
document.getElementById('chmod-input').value = '';
|
||||||
|
openModal('chmod-modal');
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteFile() {
|
||||||
|
if (!selectedFile) return;
|
||||||
|
if (!confirm(`Are you sure you want to delete "${selectedFile.name}"?`)) return;
|
||||||
|
|
||||||
|
api('?module=files&action=delete', 'POST', { path: selectedFile.path })
|
||||||
|
.then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
toast('File deleted', 'success');
|
||||||
|
refresh();
|
||||||
|
} else {
|
||||||
|
toast(response.error, 'danger');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal actions
|
||||||
|
function openNewFileModal() {
|
||||||
|
document.getElementById('new-file-name').value = '';
|
||||||
|
document.getElementById('new-file-content').value = '';
|
||||||
|
openModal('new-file-modal');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openNewFolderModal() {
|
||||||
|
document.getElementById('new-folder-name').value = '';
|
||||||
|
openModal('new-folder-modal');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openUploadModal() {
|
||||||
|
document.getElementById('upload-files').value = '';
|
||||||
|
document.getElementById('upload-progress').classList.add('hidden');
|
||||||
|
openModal('upload-modal');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createNewFile() {
|
||||||
|
const name = document.getElementById('new-file-name').value.trim();
|
||||||
|
const content = document.getElementById('new-file-content').value;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
toast('Please enter a filename', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = currentPath + '/' + name;
|
||||||
|
|
||||||
|
const response = await api('?module=files&action=write', 'POST', { path, content });
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
toast('File created', 'success');
|
||||||
|
closeModal('new-file-modal');
|
||||||
|
refresh();
|
||||||
|
} else {
|
||||||
|
toast(response.error, 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createNewFolder() {
|
||||||
|
const name = document.getElementById('new-folder-name').value.trim();
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
toast('Please enter a folder name', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = currentPath + '/' + name;
|
||||||
|
|
||||||
|
const response = await api('?module=files&action=mkdir', 'POST', { path });
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
toast('Folder created', 'success');
|
||||||
|
closeModal('new-folder-modal');
|
||||||
|
refresh();
|
||||||
|
} else {
|
||||||
|
toast(response.error, 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFiles() {
|
||||||
|
const input = document.getElementById('upload-files');
|
||||||
|
const files = input.files;
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
toast('Please select files to upload', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const progressDiv = document.getElementById('upload-progress');
|
||||||
|
const progressBar = document.getElementById('upload-bar');
|
||||||
|
const statusText = document.getElementById('upload-status');
|
||||||
|
|
||||||
|
progressDiv.classList.remove('hidden');
|
||||||
|
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files[i];
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('path', currentPath);
|
||||||
|
formData.append('csrf_token', getCsrfToken());
|
||||||
|
|
||||||
|
statusText.textContent = `Uploading ${file.name} (${i + 1}/${files.length})...`;
|
||||||
|
progressBar.style.width = ((i / files.length) * 100) + '%';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('?module=files&action=upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
toast(`Failed to upload ${file.name}: ${result.error}`, 'danger');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast(`Failed to upload ${file.name}`, 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressBar.style.width = '100%';
|
||||||
|
statusText.textContent = 'Upload complete!';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
closeModal('upload-modal');
|
||||||
|
refresh();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doRename() {
|
||||||
|
const oldPath = document.getElementById('rename-old-path').value;
|
||||||
|
const newName = document.getElementById('rename-input').value.trim();
|
||||||
|
|
||||||
|
if (!newName) {
|
||||||
|
toast('Please enter a new name', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathParts = oldPath.split('/');
|
||||||
|
pathParts.pop();
|
||||||
|
const newPath = pathParts.join('/') + '/' + newName;
|
||||||
|
|
||||||
|
const response = await api('?module=files&action=move', 'POST', { source: oldPath, destination: newPath });
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
toast('File renamed', 'success');
|
||||||
|
closeModal('rename-modal');
|
||||||
|
refresh();
|
||||||
|
} else {
|
||||||
|
toast(response.error, 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doChmod() {
|
||||||
|
const path = document.getElementById('chmod-path').value;
|
||||||
|
const mode = document.getElementById('chmod-input').value.trim();
|
||||||
|
|
||||||
|
if (!mode) {
|
||||||
|
toast('Please enter permissions', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api('?module=files&action=chmod', 'POST', { path, mode });
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
toast('Permissions changed', 'success');
|
||||||
|
closeModal('chmod-modal');
|
||||||
|
refresh();
|
||||||
|
} else {
|
||||||
|
toast(response.error, 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard shortcuts
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'F5') {
|
||||||
|
e.preventDefault();
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
if (e.key === 'Backspace' && document.activeElement.tagName !== 'INPUT') {
|
||||||
|
e.preventDefault();
|
||||||
|
navigateUp();
|
||||||
|
}
|
||||||
|
if (e.key === 'Delete' && selectedFile) {
|
||||||
|
deleteFile();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
314
src/Views/modules/network.php
Archivo normal
314
src/Views/modules/network.php
Archivo normal
@@ -0,0 +1,314 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Network View
|
||||||
|
*
|
||||||
|
* @var array $data View data
|
||||||
|
*/
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<div class="tab active" data-tab="connections" onclick="switchTab('connections')">Connections</div>
|
||||||
|
<div class="tab" data-tab="ping" onclick="switchTab('ping')">Ping</div>
|
||||||
|
<div class="tab" data-tab="traceroute" onclick="switchTab('traceroute')">Traceroute</div>
|
||||||
|
<div class="tab" data-tab="portscan" onclick="switchTab('portscan')">Port Scan</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Connections Tab -->
|
||||||
|
<div id="tab-connections" class="tab-content">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">🔌 Network Connections</h3>
|
||||||
|
<button onclick="loadConnections()" class="btn btn-sm btn-secondary">🔄 Refresh</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" style="padding: 0;">
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="table table-mono">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Protocol</th>
|
||||||
|
<th>Local Address</th>
|
||||||
|
<th>Local Port</th>
|
||||||
|
<th>Foreign Address</th>
|
||||||
|
<th>State</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="connections-list">
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center">
|
||||||
|
<div class="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
Loading connections...
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ping Tab -->
|
||||||
|
<div id="tab-ping" class="tab-content hidden">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">📡 Ping</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="ping-form" onsubmit="doPing(event)">
|
||||||
|
<div class="grid grid-2">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Host</label>
|
||||||
|
<input type="text" id="ping-host" class="form-input" placeholder="google.com or 8.8.8.8" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Count</label>
|
||||||
|
<input type="number" id="ping-count" class="form-input" value="4" min="1" max="10">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">📡 Ping</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="terminal mt-3">
|
||||||
|
<div class="terminal-header">
|
||||||
|
<span class="terminal-dot red"></span>
|
||||||
|
<span class="terminal-dot yellow"></span>
|
||||||
|
<span class="terminal-dot green"></span>
|
||||||
|
</div>
|
||||||
|
<div class="terminal-body" style="height: 300px;">
|
||||||
|
<pre id="ping-output" class="terminal-output">Ready to ping...</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Traceroute Tab -->
|
||||||
|
<div id="tab-traceroute" class="tab-content hidden">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">🛤️ Traceroute</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="traceroute-form" onsubmit="doTraceroute(event)">
|
||||||
|
<div class="grid grid-2">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Host</label>
|
||||||
|
<input type="text" id="traceroute-host" class="form-input" placeholder="google.com" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Max Hops</label>
|
||||||
|
<input type="number" id="traceroute-hops" class="form-input" value="30" min="1" max="30">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">🛤️ Trace Route</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="terminal mt-3">
|
||||||
|
<div class="terminal-header">
|
||||||
|
<span class="terminal-dot red"></span>
|
||||||
|
<span class="terminal-dot yellow"></span>
|
||||||
|
<span class="terminal-dot green"></span>
|
||||||
|
</div>
|
||||||
|
<div class="terminal-body" style="height: 300px;">
|
||||||
|
<pre id="traceroute-output" class="terminal-output">Ready to trace...</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Port Scan Tab -->
|
||||||
|
<div id="tab-portscan" class="tab-content hidden">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">🔍 Port Scanner</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="portscan-form" onsubmit="doPortScan(event)">
|
||||||
|
<div class="grid grid-2">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Host</label>
|
||||||
|
<input type="text" id="portscan-host" class="form-input" placeholder="localhost or 192.168.1.1" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Ports</label>
|
||||||
|
<input type="text" id="portscan-ports" class="form-input" placeholder="1-100 or 22,80,443" value="1-100">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted mb-2" style="font-size: 12px;">⚠️ Port scanning limited to 100 ports max</p>
|
||||||
|
<button type="submit" class="btn btn-primary">🔍 Scan Ports</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="portscan-results" class="mt-3 hidden">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="card-title">Scan Results</h4>
|
||||||
|
<span id="portscan-summary" class="text-muted"></span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" style="padding: 0;">
|
||||||
|
<table class="table table-mono">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Port</th>
|
||||||
|
<th>Service</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="portscan-list">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Tab switching
|
||||||
|
function switchTab(tabName) {
|
||||||
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-content').forEach(c => c.classList.add('hidden'));
|
||||||
|
|
||||||
|
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
|
||||||
|
document.getElementById(`tab-${tabName}`).classList.remove('hidden');
|
||||||
|
|
||||||
|
if (tabName === 'connections') {
|
||||||
|
loadConnections();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load connections
|
||||||
|
async function loadConnections() {
|
||||||
|
const tbody = document.getElementById('connections-list');
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center"><div class="loading"><div class="spinner"></div>Loading...</div></td></tr>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api('?module=network&action=connections');
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
const connections = response.data.connections;
|
||||||
|
|
||||||
|
if (connections.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No connections found</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
for (const conn of connections) {
|
||||||
|
html += `
|
||||||
|
<tr>
|
||||||
|
<td><span class="badge badge-info">${escapeHtml(conn.protocol)}</span></td>
|
||||||
|
<td>${escapeHtml(conn.local_address)}</td>
|
||||||
|
<td>${escapeHtml(conn.local_port)}</td>
|
||||||
|
<td>${escapeHtml(conn.foreign_address)}</td>
|
||||||
|
<td><span class="badge badge-success">${escapeHtml(conn.state)}</span></td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
tbody.innerHTML = html;
|
||||||
|
} else {
|
||||||
|
tbody.innerHTML = `<tr><td colspan="5" class="text-danger text-center">${escapeHtml(response.error)}</td></tr>`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-danger text-center">Failed to load connections</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping
|
||||||
|
async function doPing(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const host = document.getElementById('ping-host').value.trim();
|
||||||
|
const count = document.getElementById('ping-count').value;
|
||||||
|
const output = document.getElementById('ping-output');
|
||||||
|
|
||||||
|
output.textContent = `Pinging ${host}...\n`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api('?module=network&action=ping', 'POST', { host, count });
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
output.textContent = response.data.output;
|
||||||
|
} else {
|
||||||
|
output.textContent = 'Error: ' + response.error;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
output.textContent = 'Error: ' + error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traceroute
|
||||||
|
async function doTraceroute(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const host = document.getElementById('traceroute-host').value.trim();
|
||||||
|
const maxHops = document.getElementById('traceroute-hops').value;
|
||||||
|
const output = document.getElementById('traceroute-output');
|
||||||
|
|
||||||
|
output.textContent = `Tracing route to ${host}...\nThis may take a while...\n`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api('?module=network&action=traceroute', 'POST', { host, max_hops: maxHops });
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
output.textContent = response.data.output;
|
||||||
|
} else {
|
||||||
|
output.textContent = 'Error: ' + response.error;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
output.textContent = 'Error: ' + error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port Scan
|
||||||
|
async function doPortScan(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const host = document.getElementById('portscan-host').value.trim();
|
||||||
|
const ports = document.getElementById('portscan-ports').value.trim();
|
||||||
|
const resultsDiv = document.getElementById('portscan-results');
|
||||||
|
const tbody = document.getElementById('portscan-list');
|
||||||
|
const summary = document.getElementById('portscan-summary');
|
||||||
|
|
||||||
|
resultsDiv.classList.remove('hidden');
|
||||||
|
tbody.innerHTML = '<tr><td colspan="3" class="text-center"><div class="loading"><div class="spinner"></div>Scanning ports...</div></td></tr>';
|
||||||
|
summary.textContent = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api('?module=network&action=portscan', 'POST', { host, ports });
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
summary.textContent = `Scanned ${data.scanned_count} ports | ${data.open_count} open | ${data.closed_count} closed`;
|
||||||
|
|
||||||
|
if (data.open_ports.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="3" class="text-center text-muted">No open ports found</td></tr>';
|
||||||
|
} else {
|
||||||
|
let html = '';
|
||||||
|
for (const port of data.open_ports) {
|
||||||
|
html += `
|
||||||
|
<tr>
|
||||||
|
<td>${port.port}</td>
|
||||||
|
<td>${escapeHtml(port.service)}</td>
|
||||||
|
<td><span class="badge badge-success">${port.status}</span></td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
tbody.innerHTML = html;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tbody.innerHTML = `<tr><td colspan="3" class="text-danger text-center">${escapeHtml(response.error)}</td></tr>`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="3" class="text-danger text-center">Scan failed</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadConnections();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
263
src/Views/modules/processes.php
Archivo normal
263
src/Views/modules/processes.php
Archivo normal
@@ -0,0 +1,263 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Processes View
|
||||||
|
*
|
||||||
|
* @var array $data View data
|
||||||
|
*/
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">⚙️ Running Processes</h3>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button onclick="refreshProcesses()" class="btn btn-sm btn-secondary">🔄 Refresh</button>
|
||||||
|
<input type="text" id="process-filter" class="form-input" placeholder="Filter processes..." style="width: 200px;" onkeyup="filterProcesses(this.value)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" style="padding: 0;">
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="table table-mono" id="process-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 80px;">PID</th>
|
||||||
|
<th style="width: 100px;">User</th>
|
||||||
|
<th style="width: 80px;">CPU %</th>
|
||||||
|
<th style="width: 80px;">MEM %</th>
|
||||||
|
<th style="width: 100px;">Memory</th>
|
||||||
|
<th>Command</th>
|
||||||
|
<th style="width: 80px;">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="process-list">
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="text-center">
|
||||||
|
<div class="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
Loading processes...
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-2 mt-3">
|
||||||
|
<!-- System Load -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">📊 System Load</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="system-load">
|
||||||
|
<?php
|
||||||
|
$load = sys_getloadavg();
|
||||||
|
if ($load !== false):
|
||||||
|
?>
|
||||||
|
<div class="flex justify-between mb-1">
|
||||||
|
<span class="text-muted">1 minute</span>
|
||||||
|
<span><?= round($load[0], 2) ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between mb-1">
|
||||||
|
<span class="text-muted">5 minutes</span>
|
||||||
|
<span><?= round($load[1], 2) ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted">15 minutes</span>
|
||||||
|
<span><?= round($load[2], 2) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<p class="text-muted">Load average not available</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Process Stats -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">📈 Process Statistics</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="process-stats">
|
||||||
|
<div class="flex justify-between mb-1">
|
||||||
|
<span class="text-muted">Total Processes</span>
|
||||||
|
<span id="stat-total">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between mb-1">
|
||||||
|
<span class="text-muted">Current User</span>
|
||||||
|
<span id="stat-user-procs">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted">PHP Processes</span>
|
||||||
|
<span id="stat-php-procs">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kill Confirm Modal -->
|
||||||
|
<div id="kill-modal" class="modal-backdrop">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title">Kill Process</h3>
|
||||||
|
<button class="modal-close" onclick="closeModal('kill-modal')">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Are you sure you want to kill process <strong id="kill-pid"></strong>?</p>
|
||||||
|
<p class="text-muted mt-1" id="kill-command"></p>
|
||||||
|
<div class="form-group mt-2">
|
||||||
|
<label class="form-label">Signal</label>
|
||||||
|
<select id="kill-signal" class="form-select">
|
||||||
|
<option value="15">SIGTERM (15) - Graceful</option>
|
||||||
|
<option value="9">SIGKILL (9) - Force</option>
|
||||||
|
<option value="1">SIGHUP (1) - Reload</option>
|
||||||
|
<option value="2">SIGINT (2) - Interrupt</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" onclick="closeModal('kill-modal')">Cancel</button>
|
||||||
|
<button class="btn btn-danger" onclick="doKillProcess()">Kill Process</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let processes = [];
|
||||||
|
let killTarget = null;
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
refreshProcesses();
|
||||||
|
// Auto refresh every 10 seconds
|
||||||
|
setInterval(refreshProcesses, 10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load processes
|
||||||
|
async function refreshProcesses() {
|
||||||
|
try {
|
||||||
|
const response = await api('?module=processes&action=list');
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
processes = response.data.processes;
|
||||||
|
renderProcesses();
|
||||||
|
updateStats();
|
||||||
|
} else {
|
||||||
|
document.getElementById('process-list').innerHTML = `
|
||||||
|
<tr><td colspan="7" class="text-danger text-center">${escapeHtml(response.error)}</td></tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
document.getElementById('process-list').innerHTML = `
|
||||||
|
<tr><td colspan="7" class="text-danger text-center">Failed to load processes</td></tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render processes
|
||||||
|
function renderProcesses() {
|
||||||
|
const tbody = document.getElementById('process-list');
|
||||||
|
|
||||||
|
if (processes.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted">No processes found</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by CPU usage
|
||||||
|
const sorted = [...processes].sort((a, b) => parseFloat(b.cpu || 0) - parseFloat(a.cpu || 0));
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
for (const proc of sorted) {
|
||||||
|
const cpuClass = parseFloat(proc.cpu || 0) > 50 ? 'text-danger' : (parseFloat(proc.cpu || 0) > 20 ? 'text-warning' : '');
|
||||||
|
const memClass = parseFloat(proc.mem || 0) > 50 ? 'text-danger' : (parseFloat(proc.mem || 0) > 20 ? 'text-warning' : '');
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<tr data-pid="${proc.pid}" data-command="${escapeHtml(proc.command || '')}">
|
||||||
|
<td>${proc.pid}</td>
|
||||||
|
<td>${escapeHtml(proc.user || '-')}</td>
|
||||||
|
<td class="${cpuClass}">${proc.cpu || '0.0'}%</td>
|
||||||
|
<td class="${memClass}">${proc.mem || '0.0'}%</td>
|
||||||
|
<td>${proc.rss ? formatMemory(proc.rss) : '-'}</td>
|
||||||
|
<td style="max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${escapeHtml(proc.command || '')}">
|
||||||
|
${escapeHtml(proc.command || '-')}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-danger btn-icon" onclick="killProcess(${proc.pid}, '${escapeHtml(proc.command || '')}')" title="Kill">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update statistics
|
||||||
|
function updateStats() {
|
||||||
|
const currentUser = '<?= addslashes(get_current_user()) ?>';
|
||||||
|
|
||||||
|
document.getElementById('stat-total').textContent = processes.length;
|
||||||
|
document.getElementById('stat-user-procs').textContent = processes.filter(p => p.user === currentUser).length;
|
||||||
|
document.getElementById('stat-php-procs').textContent = processes.filter(p => (p.command || '').toLowerCase().includes('php')).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter processes
|
||||||
|
function filterProcesses(query) {
|
||||||
|
query = query.toLowerCase();
|
||||||
|
const rows = document.querySelectorAll('#process-list tr');
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
const command = row.dataset.command?.toLowerCase() || '';
|
||||||
|
const pid = row.dataset.pid || '';
|
||||||
|
row.style.display = (command.includes(query) || pid.includes(query)) ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill process
|
||||||
|
function killProcess(pid, command) {
|
||||||
|
killTarget = pid;
|
||||||
|
document.getElementById('kill-pid').textContent = pid;
|
||||||
|
document.getElementById('kill-command').textContent = command;
|
||||||
|
openModal('kill-modal');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doKillProcess() {
|
||||||
|
if (!killTarget) return;
|
||||||
|
|
||||||
|
const signal = document.getElementById('kill-signal').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api('?module=processes&action=kill', 'POST', {
|
||||||
|
pid: killTarget,
|
||||||
|
signal: signal
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
toast('Process killed', 'success');
|
||||||
|
closeModal('kill-modal');
|
||||||
|
refreshProcesses();
|
||||||
|
} else {
|
||||||
|
toast(response.error, 'danger');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast('Failed to kill process', 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format memory
|
||||||
|
function formatMemory(kb) {
|
||||||
|
const bytes = kb * 1024;
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
let i = 0;
|
||||||
|
let size = bytes;
|
||||||
|
while (size >= 1024 && i < units.length - 1) {
|
||||||
|
size /= 1024;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return size.toFixed(1) + ' ' + units[i];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
368
src/Views/modules/system.php
Archivo normal
368
src/Views/modules/system.php
Archivo normal
@@ -0,0 +1,368 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 System Info View
|
||||||
|
*
|
||||||
|
* @var array $data View data
|
||||||
|
*/
|
||||||
|
|
||||||
|
$systemInfo = $data['systemInfo'] ?? [];
|
||||||
|
$server = $systemInfo['server'] ?? [];
|
||||||
|
$php = $systemInfo['php'] ?? [];
|
||||||
|
$hardware = $systemInfo['hardware'] ?? [];
|
||||||
|
$disk = $systemInfo['disk'] ?? [];
|
||||||
|
$network = $systemInfo['network'] ?? [];
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<div class="tab active" data-tab="overview" onclick="switchTab('overview')">Overview</div>
|
||||||
|
<div class="tab" data-tab="php" onclick="switchTab('php')">PHP</div>
|
||||||
|
<div class="tab" data-tab="extensions" onclick="switchTab('extensions')">Extensions</div>
|
||||||
|
<div class="tab" data-tab="environment" onclick="switchTab('environment')">Environment</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Overview Tab -->
|
||||||
|
<div id="tab-overview" class="tab-content">
|
||||||
|
<div class="grid grid-2 mb-3">
|
||||||
|
<!-- Server Info -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">🖥️ Server Information</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted" style="width: 150px;">Hostname</td>
|
||||||
|
<td><?= htmlspecialchars($server['hostname'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Operating System</td>
|
||||||
|
<td><?= htmlspecialchars($server['os'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">OS Details</td>
|
||||||
|
<td style="font-size: 12px;"><?= htmlspecialchars($server['os_detail'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Server Software</td>
|
||||||
|
<td><?= htmlspecialchars($server['server_software'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Server Address</td>
|
||||||
|
<td><?= htmlspecialchars($server['server_addr'] ?? 'Unknown') ?>:<?= htmlspecialchars($server['server_port'] ?? '') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Document Root</td>
|
||||||
|
<td class="text-mono" style="font-size: 12px;"><?= htmlspecialchars($server['document_root'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Current User</td>
|
||||||
|
<td><?= htmlspecialchars($server['current_user'] ?? 'Unknown') ?> (uid: <?= $server['current_uid'] ?? '?' ?>, gid: <?= $server['current_gid'] ?? '?' ?>)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Process ID</td>
|
||||||
|
<td><?= $server['process_id'] ?? 'Unknown' ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Uptime</td>
|
||||||
|
<td><?= htmlspecialchars($server['uptime'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Load Average</td>
|
||||||
|
<td>
|
||||||
|
<?= $server['load_average']['1min'] ?? '-' ?> /
|
||||||
|
<?= $server['load_average']['5min'] ?? '-' ?> /
|
||||||
|
<?= $server['load_average']['15min'] ?? '-' ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hardware Info -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">💻 Hardware Information</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted" style="width: 150px;">CPU</td>
|
||||||
|
<td style="font-size: 12px;"><?= htmlspecialchars($hardware['cpu'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">CPU Cores</td>
|
||||||
|
<td><?= htmlspecialchars($hardware['cpu_cores'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Total Memory</td>
|
||||||
|
<td><?= htmlspecialchars($hardware['memory_total'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Free Memory</td>
|
||||||
|
<td><?= htmlspecialchars($hardware['memory_free'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Available Memory</td>
|
||||||
|
<td><?= htmlspecialchars($hardware['memory_available'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h4 style="margin: 20px 0 10px; font-size: 14px;">💿 Disk Usage</h4>
|
||||||
|
<?php if (!empty($disk)): ?>
|
||||||
|
<table class="table table-mono" style="font-size: 12px;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Mount</th>
|
||||||
|
<th>Total</th>
|
||||||
|
<th>Used</th>
|
||||||
|
<th>Free</th>
|
||||||
|
<th>%</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach (array_slice($disk, 0, 5) as $d): ?>
|
||||||
|
<tr>
|
||||||
|
<td style="max-width: 100px; overflow: hidden; text-overflow: ellipsis;"><?= htmlspecialchars($d['mount'] ?? $d['device'] ?? '-') ?></td>
|
||||||
|
<td><?= htmlspecialchars($d['total'] ?? '-') ?></td>
|
||||||
|
<td><?= htmlspecialchars($d['used'] ?? '-') ?></td>
|
||||||
|
<td><?= htmlspecialchars($d['free'] ?? '-') ?></td>
|
||||||
|
<td><?= htmlspecialchars($d['percent_used'] ?? '-') ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php else: ?>
|
||||||
|
<p class="text-muted">No disk information available</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PHP Tab -->
|
||||||
|
<div id="tab-php" class="tab-content hidden">
|
||||||
|
<div class="grid grid-2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">🐘 PHP Configuration</h3>
|
||||||
|
<a href="?module=system&action=phpinfo" target="_blank" class="btn btn-sm btn-secondary">Full phpinfo()</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Version</td>
|
||||||
|
<td><?= htmlspecialchars($php['version'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">SAPI</td>
|
||||||
|
<td><?= htmlspecialchars($php['sapi'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">INI Path</td>
|
||||||
|
<td class="text-mono" style="font-size: 12px;"><?= htmlspecialchars($php['ini_path'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Extension Dir</td>
|
||||||
|
<td class="text-mono" style="font-size: 12px;"><?= htmlspecialchars($php['extension_dir'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Zend Version</td>
|
||||||
|
<td><?= htmlspecialchars($php['zend_version'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">⚙️ PHP Settings</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Memory Limit</td>
|
||||||
|
<td><?= htmlspecialchars($php['memory_limit'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Max Execution Time</td>
|
||||||
|
<td><?= htmlspecialchars($php['max_execution_time'] ?? 'Unknown') ?>s</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Max Input Time</td>
|
||||||
|
<td><?= htmlspecialchars($php['max_input_time'] ?? 'Unknown') ?>s</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Post Max Size</td>
|
||||||
|
<td><?= htmlspecialchars($php['post_max_size'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Upload Max Size</td>
|
||||||
|
<td><?= htmlspecialchars($php['upload_max_filesize'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Max File Uploads</td>
|
||||||
|
<td><?= htmlspecialchars($php['max_file_uploads'] ?? 'Unknown') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Display Errors</td>
|
||||||
|
<td><?= $php['display_errors'] ? '<span class="badge badge-warning">On</span>' : '<span class="badge badge-success">Off</span>' ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Allow URL Fopen</td>
|
||||||
|
<td><?= $php['allow_url_fopen'] === 'on' ? '<span class="badge badge-warning">On</span>' : '<span class="badge badge-success">Off</span>' ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Allow URL Include</td>
|
||||||
|
<td><?= $php['allow_url_include'] === 'on' ? '<span class="badge badge-danger">On</span>' : '<span class="badge badge-success">Off</span>' ?></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">🚫 Disabled Functions</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<?php
|
||||||
|
$disabled = $php['disabled_functions'] ?? 'none';
|
||||||
|
if ($disabled && $disabled !== 'none'):
|
||||||
|
$functions = explode(',', $disabled);
|
||||||
|
?>
|
||||||
|
<div class="flex gap-1" style="flex-wrap: wrap;">
|
||||||
|
<?php foreach ($functions as $func): ?>
|
||||||
|
<span class="badge badge-danger"><?= htmlspecialchars(trim($func)) ?></span>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<p class="text-success">No functions disabled</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Extensions Tab -->
|
||||||
|
<div id="tab-extensions" class="tab-content hidden">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">📦 Loaded Extensions</h3>
|
||||||
|
<span class="text-muted" id="extension-count"></span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="extensions-list" class="flex gap-1" style="flex-wrap: wrap;">
|
||||||
|
<div class="loading"><div class="spinner"></div>Loading extensions...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Environment Tab -->
|
||||||
|
<div id="tab-environment" class="tab-content hidden">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">🌍 Environment Variables</h3>
|
||||||
|
<input type="text" id="env-filter" class="form-input" placeholder="Filter..." style="width: 200px;" onkeyup="filterEnv(this.value)">
|
||||||
|
</div>
|
||||||
|
<div class="card-body" style="padding: 0; max-height: 600px; overflow-y: auto;">
|
||||||
|
<table class="table table-mono" id="env-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 250px;">Variable</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="env-list">
|
||||||
|
<tr><td colspan="2" class="text-center"><div class="loading"><div class="spinner"></div></div></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Tab switching
|
||||||
|
function switchTab(tabName) {
|
||||||
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-content').forEach(c => c.classList.add('hidden'));
|
||||||
|
|
||||||
|
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
|
||||||
|
document.getElementById(`tab-${tabName}`).classList.remove('hidden');
|
||||||
|
|
||||||
|
if (tabName === 'extensions') {
|
||||||
|
loadExtensions();
|
||||||
|
} else if (tabName === 'environment') {
|
||||||
|
loadEnvironment();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load extensions
|
||||||
|
async function loadExtensions() {
|
||||||
|
const container = document.getElementById('extensions-list');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api('?module=system&action=extensions');
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
const extensions = response.data.extensions;
|
||||||
|
const count = Object.keys(extensions).length;
|
||||||
|
document.getElementById('extension-count').textContent = `${count} extensions loaded`;
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
for (const [name, version] of Object.entries(extensions)) {
|
||||||
|
html += `<span class="badge badge-info" title="Version: ${escapeHtml(version)}">${escapeHtml(name)}</span>`;
|
||||||
|
}
|
||||||
|
container.innerHTML = html;
|
||||||
|
} else {
|
||||||
|
container.innerHTML = `<span class="text-danger">${escapeHtml(response.error)}</span>`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
container.innerHTML = '<span class="text-danger">Failed to load extensions</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load environment
|
||||||
|
async function loadEnvironment() {
|
||||||
|
const tbody = document.getElementById('env-list');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api('?module=system&action=environment');
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
const env = response.data.environment;
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
for (const [key, value] of Object.entries(env)) {
|
||||||
|
html += `
|
||||||
|
<tr data-key="${escapeHtml(key.toLowerCase())}">
|
||||||
|
<td class="text-muted">${escapeHtml(key)}</td>
|
||||||
|
<td style="word-break: break-all;">${escapeHtml(String(value))}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
tbody.innerHTML = html;
|
||||||
|
} else {
|
||||||
|
tbody.innerHTML = `<tr><td colspan="2" class="text-danger">${escapeHtml(response.error)}</td></tr>`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="2" class="text-danger">Failed to load environment</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter environment
|
||||||
|
function filterEnv(query) {
|
||||||
|
query = query.toLowerCase();
|
||||||
|
document.querySelectorAll('#env-list tr').forEach(row => {
|
||||||
|
const key = row.dataset.key || '';
|
||||||
|
row.style.display = key.includes(query) ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
189
src/Views/modules/terminal.php
Archivo normal
189
src/Views/modules/terminal.php
Archivo normal
@@ -0,0 +1,189 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell2 Terminal View
|
||||||
|
*
|
||||||
|
* @var array $data View data
|
||||||
|
*/
|
||||||
|
|
||||||
|
$currentDir = $data['currentDir'] ?? getcwd();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="terminal">
|
||||||
|
<div class="terminal-header">
|
||||||
|
<span class="terminal-dot red"></span>
|
||||||
|
<span class="terminal-dot yellow"></span>
|
||||||
|
<span class="terminal-dot green"></span>
|
||||||
|
<span style="margin-left: 12px; color: var(--text-muted); font-size: 12px;">
|
||||||
|
Terminal - <?= htmlspecialchars(gethostname() ?: 'shell') ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="terminal-body" id="terminal-output">
|
||||||
|
<div class="terminal-output">
|
||||||
|
Welcome to AleShell2 Terminal
|
||||||
|
PHP <?= PHP_VERSION ?> | <?= PHP_OS ?>
|
||||||
|
Current directory: <?= htmlspecialchars($currentDir) ?>
|
||||||
|
|
||||||
|
Type 'help' for available commands.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding: 12px 16px; background: var(--bg-tertiary); border-top: 1px solid var(--border-color);">
|
||||||
|
<div class="terminal-prompt">
|
||||||
|
<span class="terminal-prompt-text" id="prompt"><?= htmlspecialchars(get_current_user()) ?>@<?= htmlspecialchars(gethostname() ?: 'shell') ?>:~$</span>
|
||||||
|
<input type="text" class="terminal-input" id="command-input" autocomplete="off" autofocus>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">⌨️ Quick Commands</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="flex gap-1" style="flex-wrap: wrap;">
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('pwd')">pwd</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('ls -la')">ls -la</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('whoami')">whoami</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('id')">id</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('uname -a')">uname -a</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('cat /etc/passwd')">passwd</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('netstat -tuln')">netstat</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('ps aux')">ps aux</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('df -h')">df -h</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('free -m')">free -m</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('env')">env</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('cat /etc/os-release')">os-release</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let commandHistory = [];
|
||||||
|
let historyIndex = -1;
|
||||||
|
let currentDir = '<?= addslashes($currentDir) ?>';
|
||||||
|
|
||||||
|
const output = document.getElementById('terminal-output');
|
||||||
|
const input = document.getElementById('command-input');
|
||||||
|
const promptEl = document.getElementById('prompt');
|
||||||
|
|
||||||
|
// Focus input on click
|
||||||
|
output.addEventListener('click', () => input.focus());
|
||||||
|
|
||||||
|
// Handle input
|
||||||
|
input.addEventListener('keydown', async (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
const command = input.value.trim();
|
||||||
|
if (command) {
|
||||||
|
commandHistory.push(command);
|
||||||
|
historyIndex = commandHistory.length;
|
||||||
|
await executeCommand(command);
|
||||||
|
}
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// History navigation
|
||||||
|
if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (historyIndex > 0) {
|
||||||
|
historyIndex--;
|
||||||
|
input.value = commandHistory[historyIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (historyIndex < commandHistory.length - 1) {
|
||||||
|
historyIndex++;
|
||||||
|
input.value = commandHistory[historyIndex];
|
||||||
|
} else {
|
||||||
|
historyIndex = commandHistory.length;
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear screen
|
||||||
|
if (e.key === 'l' && e.ctrlKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
clearScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel
|
||||||
|
if (e.key === 'c' && e.ctrlKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
appendOutput('^C\n');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Execute command
|
||||||
|
async function executeCommand(command) {
|
||||||
|
appendOutput(`\n${promptEl.textContent} ${escapeHtml(command)}\n`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api('?module=terminal&action=exec', 'POST', {
|
||||||
|
command: command,
|
||||||
|
cwd: currentDir
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
if (response.data.output) {
|
||||||
|
appendOutput(response.data.output);
|
||||||
|
}
|
||||||
|
if (response.data.cwd !== currentDir) {
|
||||||
|
currentDir = response.data.cwd;
|
||||||
|
updatePrompt();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
appendOutput(`Error: ${response.error}\n`, 'danger');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
appendOutput(`Error: ${error.message}\n`, 'danger');
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run quick command
|
||||||
|
function runQuickCommand(command) {
|
||||||
|
input.value = command;
|
||||||
|
input.focus();
|
||||||
|
const event = new KeyboardEvent('keydown', { key: 'Enter' });
|
||||||
|
input.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append output to terminal
|
||||||
|
function appendOutput(text, type = '') {
|
||||||
|
const outputEl = output.querySelector('.terminal-output');
|
||||||
|
|
||||||
|
if (type === 'danger') {
|
||||||
|
outputEl.innerHTML += `<span class="text-danger">${escapeHtml(text)}</span>`;
|
||||||
|
} else {
|
||||||
|
outputEl.innerHTML += escapeHtml(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear screen
|
||||||
|
function clearScreen() {
|
||||||
|
const outputEl = output.querySelector('.terminal-output');
|
||||||
|
outputEl.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll to bottom
|
||||||
|
function scrollToBottom() {
|
||||||
|
output.scrollTop = output.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update prompt
|
||||||
|
function updatePrompt() {
|
||||||
|
const user = '<?= addslashes(get_current_user()) ?>';
|
||||||
|
const host = '<?= addslashes(gethostname() ?: "shell") ?>';
|
||||||
|
const shortDir = currentDir.replace(/^\/home\/[^\/]+/, '~');
|
||||||
|
promptEl.textContent = `${user}@${host}:${shortDir}$`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape HTML
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Referencia en una nueva incidencia
Block a user