Deep dive into the configuration and assets associated with this component.
| 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 |
<?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);
}
}
}
<?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');
});
index.blade.php
Length: 19607 chars