Component Details

Deep dive into the configuration and assets associated with this component.

Basic Information
Name ssh
Display Name SSH Tool
Version 1.0.0
Status Active
Downloads 92
Description Component: SSH Tool
Created 2025-11-11 20:42:10
Last Updated 2025-11-12 14:58:17
Controller Code
<?php

namespace App\Http\Controllers;

use App\Services\CommandGeneratorService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use phpseclib3\Net\SSH2;
use phpseclib3\Crypt\PublicKeyLoader;

class SshController extends Controller
{
    /**
     * Display the SSH command interface.
     */
    public function index()
    {
        $user = auth()->user();
        
        // Check if user has SSH configured
        $sshConfigured = $user && $user->ssh_host && $user->ssh_username;
        
        // Define essential daily Matomo commands (only commands that exist in console_output.json)
        $essentialCommands = [
            // Core operations
            'core:archive' => 'Archive analytics data for specified sites and periods',
            'core:invalidate-report-data' => 'Invalidate cached report data',
            'core:clear-caches' => 'Clear all cached data',
            'core:update' => 'Update Matomo to the latest version',
            'core:version' => 'Display the current Matomo version',
            'core:test-email' => 'Test email configuration',
            
            // Diagnostics
            'diagnostics:run' => 'Run diagnostics to check system health and configuration',
            
            // Database operations
            'database:optimize-archive-tables' => 'Optimize archive database tables for better performance',
            
            // Scheduled tasks
            'scheduled-tasks:run' => 'Run all scheduled tasks',
            
            // Plugin management
            'plugin:activate' => 'Activate a Matomo plugin',
            'plugin:deactivate' => 'Deactivate a Matomo plugin',
            'plugin:list' => 'List all available plugins',
        ];
        
        // Get all commands and filter to only essential ones
        $commandGenerator = app(CommandGeneratorService::class);
        $allCommands = $commandGenerator->getCommands();
        
        // Group essential commands by category
        $commandsByCategory = [];
        foreach ($essentialCommands as $commandName => $defaultDescription) {
            // Check if command exists in the full list
            if (isset($allCommands[$commandName])) {
                $commandData = $allCommands[$commandName];
                $category = $commandData['category'] ?? 'General';
            } else {
                // Use category from command name (e.g., "core:archive" -> "Core", "diagnostics:run" -> "Diagnostics")
                $categoryParts = explode(':', $commandName);
                $category = ucfirst($categoryParts[0] ?? 'General');
                $commandData = ['description' => $defaultDescription, 'parameters' => []];
            }
            
            if (!isset($commandsByCategory[$category])) {
                $commandsByCategory[$category] = [];
            }
            $commandsByCategory[$category][] = [
                'name' => $commandName,
                'description' => $commandData['description'] ?? $defaultDescription,
                'parameters' => $commandData['parameters'] ?? [],
            ];
        }
        
        // Sort categories
        ksort($commandsByCategory);
        
        // Sort commands within each category
        foreach ($commandsByCategory as $category => &$commands) {
            usort($commands, fn($a, $b) => strcmp($a['name'], $b['name']));
        }
        
        return view('components.ssh.index', compact('commandsByCategory', 'sshConfigured'));
    }
    
    /**
     * Get command details for form generation.
     */
    public function getCommandDetails(Request $request)
    {
        $commandName = $request->input('command');
        
        // Load from JSON to get original definition format
        $jsonFile = base_path('console_output.json');
        if (!file_exists($jsonFile)) {
            return response()->json([
                'success' => false,
                'message' => 'Command list not found'
            ], 404);
        }
        
        $jsonContent = file_get_contents($jsonFile);
        $data = json_decode($jsonContent, true);
        
        if (!isset($data['commands'])) {
            return response()->json([
                'success' => false,
                'message' => 'No commands available'
            ], 404);
        }
        
        // Find the command
        $found = null;
        foreach ($data['commands'] as $cmd) {
            if ($cmd['name'] === $commandName) {
                $found = $cmd;
                break;
            }
        }
        
        if (!$found) {
            return response()->json([
                'success' => false,
                'message' => 'Command not found'
            ], 404);
        }
        
        // Build parameters structure for form
        $parameters = [];
        
        // Add arguments
        if (isset($found['definition']['arguments'])) {
            foreach ($found['definition']['arguments'] as $argName => $argDef) {
                $parameters[$argName] = [
                    'is_argument' => true,
                    'type' => $this->inferType($argDef),
                    'required' => $argDef['is_required'] ?? false,
                    'description' => $argDef['description'] ?? '',
                ];
            }
        }
        
        // Add options
        if (isset($found['definition']['options'])) {
            foreach ($found['definition']['options'] as $optName => $optDef) {
                // Skip common system options
                if (in_array($optName, ['help', 'quiet', 'verbose', 'version', 'ansi', 'no-ansi', 'no-interaction', 'matomo-domain', 'xhprof', 'ignore-warn'])) {
                    continue;
                }
                
                $acceptValue = $optDef['accept_value'] ?? true;
                $parameters[$optName] = [
                    'is_argument' => false,
                    'type' => $acceptValue ? ($this->inferType($optDef)) : 'boolean',
                    'required' => $optDef['is_value_required'] ?? false,
                    'description' => $optDef['description'] ?? '',
                    'accept_value' => $acceptValue,
                ];
                
                // Extract options from description if available
                // Try multiple patterns: (opt1,opt2), [opt1,opt2], or "opt1,opt2"
                if (isset($optDef['description'])) {
                    $desc = $optDef['description'];
                    $optionsList = null;
                    
                    // Pattern 1: (option1, option2, option3)
                    if (preg_match('/\(([^)]+)\)/', $desc, $matches)) {
                        $optionsList = array_map('trim', explode(',', $matches[1]));
                    }
                    // Pattern 2: [option1, option2, option3]
                    elseif (preg_match('/\[([^\]]+)\]/', $desc, $matches)) {
                        $optionsList = array_map('trim', explode(',', $matches[1]));
                    }
                    // Pattern 3: "option1, option2, option3" or 'option1, option2, option3'
                    elseif (preg_match('/["\']([^"\']+)["\']/', $desc, $matches)) {
                        $optionsList = array_map('trim', explode(',', $matches[1]));
                    }
                    // Pattern 4: Look for "eg. option1, option2" or "e.g. option1, option2"
                    elseif (preg_match('/(?:eg\.|e\.g\.|example:)\s*([^\.]+)/i', $desc, $matches)) {
                        $optionsList = array_map('trim', explode(',', $matches[1]));
                    }
                    
                    // Only set options if we found valid options (not empty, not just one item that looks like description)
                    if ($optionsList && count($optionsList) > 0) {
                        // Filter out options that are too long (likely not actual options)
                        // Also filter out if description mentions "comma separated" - those are free text fields
                        $optionsList = array_filter($optionsList, function($opt) {
                            return strlen(trim($opt)) > 0 && strlen(trim($opt)) < 50;
                        });
                        
                        // Don't create options if description mentions "comma separated" - it's a free text field
                        $isCommaSeparated = stripos($desc, 'comma separated') !== false || 
                                           stripos($desc, 'comma-separated') !== false ||
                                           stripos($desc, 'separated by comma') !== false;
                        
                        // Only set options if:
                        // 1. We have valid options
                        // 2. It's not a comma-separated field
                        // 3. We have a reasonable number of options (not too many)
                        if (count($optionsList) > 0 && !$isCommaSeparated && count($optionsList) <= 20) {
                            $parameters[$optName]['options'] = array_values($optionsList);
                        }
                    }
                }
            }
        }
        
        return response()->json([
            'success' => true,
            'command' => [
                'name' => $commandName,
                'description' => $found['description'] ?? 'No description available.',
                'category' => explode(':', $commandName)[0] ?? 'General',
                'parameters' => $parameters,
                'examples' => $found['usage'] ?? [],
            ]
        ]);
    }
    
    /**
     * Infer parameter type from definition
     */
    private function inferType($def): string
    {
        if (isset($def['accept_value']) && !$def['accept_value']) {
            return 'boolean';
        }
        
        $desc = $def['description'] ?? '';
        if (stripos($desc, 'integer') !== false || stripos($desc, 'number') !== false) {
            return 'integer';
        }
        if (stripos($desc, 'date') !== false) {
            return 'date';
        }
        if (stripos($desc, 'boolean') !== false || stripos($desc, 'flag') !== false) {
            return 'boolean';
        }
        
        return 'string';
    }
    
    /**
     * Execute a command via SSH.
     */
    public function executeCommand(Request $request)
    {
        $user = auth()->user();
        
        if (!$user || !$user->ssh_host || !$user->ssh_username) {
            return response()->json([
                'success' => false,
                'message' => 'SSH not configured. Please configure SSH in your profile first.'
            ], 400);
        }
        
        $commandName = $request->input('command');
        $args = $request->input('args', []);
        $opts = $request->input('opts', []);
        
        // Load command definition from JSON (same as DashboardController)
        $jsonFile = base_path('console_output.json');
        if (!file_exists($jsonFile)) {
            return response()->json([
                'success' => false,
                'message' => 'Command list not found.'
            ], 404);
        }
        
        $jsonContent = file_get_contents($jsonFile);
        $data = json_decode($jsonContent, true);
        
        if (!isset($data['commands'])) {
            return response()->json([
                'success' => false,
                'message' => 'No commands available.'
            ], 404);
        }
        
        // Find the command
        $found = null;
        foreach ($data['commands'] as $cmd) {
            if ($cmd['name'] === $commandName) {
                $found = $cmd;
                break;
            }
        }
        
        if (!$found) {
            return response()->json([
                'success' => false,
                'message' => 'Command not found.'
            ], 404);
        }
        
        // Build command string (same logic as DashboardController)
        $matomoFolder = $user->ssh_matomo_folder ?: '/var/www/matomo';
        $cmdString = 'php ' . escapeshellarg($matomoFolder . '/console') . ' ' . escapeshellcmd($commandName);
        
        // Add arguments in order
        if (isset($found['definition']['arguments'])) {
            foreach ($found['definition']['arguments'] as $argName => $argDef) {
                // Try to get value by name first, then by index
                $argValue = null;
                if (isset($args[$argName]) && $args[$argName] !== '') {
                    $argValue = $args[$argName];
                } else {
                    // Try numeric index
                    $argKeys = array_keys($found['definition']['arguments']);
                    $argIndex = array_search($argName, $argKeys);
                    if ($argIndex !== false && isset($args[$argIndex]) && $args[$argIndex] !== '') {
                        $argValue = $args[$argIndex];
                    }
                }
                
                if ($argValue !== null && $argValue !== '') {
                    $cmdString .= ' ' . escapeshellarg($argValue);
                }
            }
        }
        
        // Add options
        if (isset($found['definition']['options'])) {
            foreach ($found['definition']['options'] as $optName => $optDef) {
                // Skip common system options
                if (in_array($optName, ['help', 'quiet', 'verbose', 'version', 'ansi', 'no-ansi', 'no-interaction', 'matomo-domain', 'xhprof', 'ignore-warn'])) {
                    continue;
                }
                
                if (isset($opts[$optName])) {
                    $value = $opts[$optName];
                    if (is_bool($value) && $value) {
                        // Boolean option: --option-name
                        $cmdString .= ' --' . escapeshellcmd($optName);
                    } elseif ($value !== '' && $value !== false && $value !== '0' && $value !== 'false') {
                        // Option with value: --option-name=value
                        $cmdString .= ' --' . escapeshellcmd($optName) . '=' . escapeshellarg($value);
                    }
                }
            }
        }
        
        try {
            // Connect via SSH
            $ssh = new SSH2($user->ssh_host, (int)($user->ssh_port ?: 22));
            
            // Authenticate
            if ($user->ssh_use_key && $user->ssh_private_key) {
                $privateKey = decrypt($user->ssh_private_key);
                $key = PublicKeyLoader::load($privateKey);
                $login = $ssh->login($user->ssh_username, $key);
            } else {
                $password = $user->ssh_password ? decrypt($user->ssh_password) : null;
                $login = $ssh->login($user->ssh_username, $password);
            }
            
            if (!$login) {
                return response()->json([
                    'success' => false,
                    'message' => 'SSH login failed. Please check your SSH credentials in profile settings.'
                ], 401);
            }
            
            // Execute command
            $output = $ssh->exec($cmdString);
            
            // Check if command failed (no output might indicate failure)
            if ($output === false) {
                return response()->json([
                    'success' => false,
                    'message' => 'Command execution failed',
                    'command' => $cmdString
                ], 500);
            }
            
            return response()->json([
                'success' => true,
                'output' => $output ?: 'Command executed successfully (no output)',
                'command' => $cmdString
            ]);
            
        } catch (\Exception $e) {
            Log::error('SSH command execution failed: ' . $e->getMessage());
            return response()->json([
                'success' => false,
                'message' => 'SSH error: ' . $e->getMessage()
            ], 500);
        }
    }
}

Routes Code
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\SshController;

Route::middleware(['auth', 'component.visible'])->group(function () {
    Route::get('/ssh', [SshController::class, 'index'])->name('dashboard.ssh');
    Route::post('/ssh/command-details', [SshController::class, 'getCommandDetails'])->name('ssh.command-details');
    Route::post('/ssh/execute', [SshController::class, 'executeCommand'])->name('ssh.execute');
});
Views (1)
  • index.blade.php Length: 19607 chars