654 lines
22 KiB
PHP
Executable File
654 lines
22 KiB
PHP
Executable File
<?php
|
|
|
|
/**
|
|
* SysCMS - Content Management System
|
|
*
|
|
* Copyright (c) 2025 Marco Costa
|
|
* All rights reserved.
|
|
*
|
|
* Author: Marco Costa
|
|
* Email: contato@syspanel.com.br
|
|
* Project: SysCMS
|
|
*
|
|
* This software is licensed, not sold.
|
|
* Permission is granted to use this software
|
|
* subject to the terms of the SysCMS Commercial License.
|
|
*
|
|
* Redistribution, sublicensing, resale, or
|
|
* disclosure of this software, in whole or in part,
|
|
* is strictly prohibited without prior written permission.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
|
|
*/
|
|
|
|
namespace App\Controllers;
|
|
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Modules\Auth\Models\User;
|
|
use App\Models\Module;
|
|
use Modules\Company\Models\Company;
|
|
use Illuminate\Database\Capsule\Manager as DB;
|
|
|
|
class AdminController extends BaseAdminController
|
|
{
|
|
/**
|
|
* Display the main dashboard.
|
|
*
|
|
* @param Request $request
|
|
* @return Response
|
|
*/
|
|
public function index(Request $request): Response
|
|
{
|
|
$user = $this->getCurrentUser();
|
|
|
|
if (!$user) {
|
|
return new Response('', 302, ['Location' => url('/login')]);
|
|
}
|
|
|
|
// Get enabled modules for the user's company
|
|
$enabledModules = $this->getEnabledModules($user);
|
|
|
|
// Get widgets from authorized modules
|
|
$widgets = $this->getAuthorizedWidgets($enabledModules, $user);
|
|
|
|
// Get system statistics
|
|
$stats = $this->getSystemStats($user);
|
|
|
|
// Sidebar and common data are handled by renderAdminView,
|
|
// but since dashboard/index.twig expects specific variables,
|
|
// we can still pass them explicitly if we use the old way,
|
|
// OR we can use renderAdminView and just pass the extra data.
|
|
|
|
// Check if SEO module is installed but not configured
|
|
$seoPending = false;
|
|
try {
|
|
$seoModule = Module::where('modules_name', 'seo')->active()->first();
|
|
if ($seoModule && empty($seoModule->modules_settings)) {
|
|
$seoPending = true;
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// Silently fail if something goes wrong with SEO check
|
|
error_log("SEO Check Error: " . $e->getMessage());
|
|
}
|
|
|
|
// Using renderAdminView for consistency:
|
|
return $this->renderAdminView('dashboard/index.twig', [
|
|
'stats' => $stats,
|
|
'recent_activity' => $this->getRecentActivity($user),
|
|
'widgets' => $widgets,
|
|
'seo_pending' => $seoPending,
|
|
'cache_cleared' => $request->query->get('cache_cleared'),
|
|
'deleted_count' => $request->query->get('deleted'),
|
|
'opcache_cleared' => $request->query->get('opcache')
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get authorized widgets for the user.
|
|
*
|
|
* @param \Illuminate\Database\Eloquent\Collection $modules
|
|
* @param User $user
|
|
* @return array
|
|
*/
|
|
protected function getAuthorizedWidgets($modules, User $user): array
|
|
{
|
|
$widgets = [];
|
|
|
|
foreach ($modules as $module) {
|
|
if (!$this->userHasModulePermission($user, $module)) {
|
|
continue;
|
|
}
|
|
|
|
$moduleNamespace = $module->modules_namespace;
|
|
|
|
// If namespace is not defined, try to guess it from name (fallback)
|
|
if (!$moduleNamespace) {
|
|
$moduleNamespace = "Modules\\" . ucfirst($module->modules_name);
|
|
}
|
|
|
|
// Ensure namespace starts with \Modules\
|
|
if (strpos($moduleNamespace, 'Modules\\') !== 0 && strpos($moduleNamespace, '\\Modules\\') !== 0) {
|
|
$moduleNamespace = "Modules\\" . ltrim($moduleNamespace, '\\');
|
|
}
|
|
|
|
$widgetClass = (strpos($moduleNamespace, '\\') === 0 ? "" : "\\") . $moduleNamespace . "\\Widgets\\DashboardWidget";
|
|
|
|
if (class_exists($widgetClass)) {
|
|
try {
|
|
$widget = new $widgetClass();
|
|
|
|
// Check if user can view this widget
|
|
if ($this->userCanViewWidget($user, $widget)) {
|
|
$widgets[] = $widget->render();
|
|
}
|
|
} catch (\Throwable $e) {
|
|
error_log("Error loading widget {$widgetClass}: " . $e->getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
return $widgets;
|
|
}
|
|
|
|
/**
|
|
* Check if user can view a widget.
|
|
*
|
|
* @param User $user
|
|
* @param mixed $widget
|
|
* @return bool
|
|
*/
|
|
protected function userCanViewWidget(User $user, $widget): bool
|
|
{
|
|
// Check if widget defines required permissions
|
|
if (method_exists($widget, 'getRequiredPermission')) {
|
|
$requiredPermission = $widget->getRequiredPermission();
|
|
return $user->hasPermission($requiredPermission);
|
|
}
|
|
|
|
// If no specific permission, check module access
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get system statistics.
|
|
*
|
|
* @param User $user
|
|
* @return array
|
|
*/
|
|
protected function getSystemStats(User $user): array
|
|
{
|
|
try {
|
|
if ($user->isSuperAdmin()) {
|
|
return $this->getSuperAdminStats();
|
|
}
|
|
|
|
return $this->getCompanyStats($user);
|
|
} catch (\Exception $e) {
|
|
error_log("Error loading system stats: " . $e->getMessage());
|
|
return [
|
|
'total_companies' => 0,
|
|
'active_companies' => 0,
|
|
'total_users' => 0,
|
|
'active_users' => 0,
|
|
'total_modules' => 0,
|
|
'active_modules' => 0,
|
|
'user_count' => 0,
|
|
'enabled_modules' => 0
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get statistics for Super Admin.
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function getSuperAdminStats(): array
|
|
{
|
|
return [
|
|
'total_companies' => class_exists('Modules\Company\Models\Company') ? \Modules\Company\Models\Company::count() : 0,
|
|
'active_companies' => class_exists('Modules\Company\Models\Company') ? \Modules\Company\Models\Company::active()->count() : 0,
|
|
'total_users' => User::count(),
|
|
'active_users' => User::active()->count(),
|
|
'total_modules' => Module::count(),
|
|
'active_modules' => Module::active()->count()
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get statistics for company users.
|
|
*
|
|
* @param User $user
|
|
* @return array
|
|
*/
|
|
protected function getCompanyStats(User $user): array
|
|
{
|
|
$companyId = $user->users_bus_id;
|
|
|
|
return [
|
|
'user_count' => User::where('users_bus_id', $companyId)->count(),
|
|
'active_users' => User::where('users_bus_id', $companyId)->active()->count(),
|
|
'enabled_modules' => DB::table('bus_modules')
|
|
->where('bus_modules_bus_id', $companyId)
|
|
->where('bus_modules_is_enabled', true)
|
|
->count()
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get recent activity for the user.
|
|
*
|
|
* @param User $user
|
|
* @return \Illuminate\Database\Eloquent\Collection
|
|
*/
|
|
protected function getRecentActivity(User $user)
|
|
{
|
|
try {
|
|
if (!class_exists('App\Models\ActivityLog')) {
|
|
return collect([]);
|
|
}
|
|
|
|
return \App\Models\ActivityLog::with('user')
|
|
->forUser($user->users_id)
|
|
->orderByDate('desc')
|
|
->limit(10)
|
|
->get();
|
|
} catch (\Exception $e) {
|
|
error_log("Error loading recent activity: " . $e->getMessage());
|
|
return collect([]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show module installation form.
|
|
*
|
|
* @param Request $request
|
|
* @return Response
|
|
*/
|
|
public function showInstallModule(Request $request): Response
|
|
{
|
|
$user = $this->getCurrentUser();
|
|
|
|
if (!$user || !$user->isSuperAdmin()) {
|
|
return new Response('', 302, ['Location' => url('/dashboard')]);
|
|
}
|
|
|
|
$success = isset($_SESSION['flash_success']) ? (array) $_SESSION['flash_success'] : [];
|
|
$error = isset($_SESSION['flash_error']) ? (array) $_SESSION['flash_error'] : [];
|
|
unset($_SESSION['flash_success'], $_SESSION['flash_error']);
|
|
return $this->renderAdminView('modules/install.twig', [
|
|
'error' => $error,
|
|
'success' => $success
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Install a module from zip file.
|
|
*
|
|
* @param Request $request
|
|
* @return Response
|
|
*/
|
|
public function installModule(Request $request): Response
|
|
{
|
|
$user = $this->getCurrentUser();
|
|
|
|
if (!$user || !$user->isSuperAdmin()) {
|
|
return new Response('', 302, ['Location' => url('/dashboard')]);
|
|
}
|
|
|
|
try {
|
|
// Validate uploaded file
|
|
if (!$request->files->has('module_zip')) {
|
|
throw new \Exception(__('modules.error_no_file'));
|
|
}
|
|
|
|
$file = $request->files->get('module_zip');
|
|
|
|
// Validate file type
|
|
if ($file->getMimeType() !== 'application/zip' && $file->getMimeType() !== 'application/x-zip-compressed') {
|
|
throw new \Exception(__('modules.error_invalid_zip'));
|
|
}
|
|
|
|
// Create temporary directory
|
|
$tempDir = sys_get_temp_dir() . '/module_install_' . time();
|
|
mkdir($tempDir, 0755, true);
|
|
|
|
// Extract zip file
|
|
$zip = new \ZipArchive();
|
|
if ($zip->open($file->getPathname()) !== TRUE) {
|
|
throw new \Exception(__('modules.error_open_zip'));
|
|
}
|
|
|
|
if (!$zip->extractTo($tempDir)) {
|
|
throw new \Exception(__('modules.error_extract_zip'));
|
|
}
|
|
$zip->close();
|
|
|
|
// Validate module structure
|
|
$this->validateModuleStructure($tempDir);
|
|
|
|
// Read module configuration
|
|
$moduleConfig = $this->readModuleConfig($tempDir);
|
|
|
|
// Check if module already exists
|
|
if (Module::where('modules_name', $moduleConfig['name'])->exists()) {
|
|
throw new \Exception(__('modules.error_module_exists'));
|
|
}
|
|
|
|
// Copy module files to modules directory
|
|
$modulesPath = dirname(__DIR__, 2) . '/modules/' . $moduleConfig['name'];
|
|
$this->recursiveCopy($tempDir, $modulesPath);
|
|
|
|
// Run migrations if they exist
|
|
$this->runModuleMigrations($modulesPath);
|
|
|
|
// Check if module was registered by migrations
|
|
$module = Module::where('modules_name', $moduleConfig['name'])->first();
|
|
|
|
if (!$module) {
|
|
// Register module in database
|
|
$module = $this->registerModule($moduleConfig);
|
|
}
|
|
|
|
// Enable module for all companies if specified
|
|
if ($request->request->get('enable_for_all', false)) {
|
|
$this->enableModuleForAllCompanies($module->modules_id, $user->users_id);
|
|
}
|
|
|
|
// Clean up
|
|
$this->recursiveDelete($tempDir);
|
|
|
|
$_SESSION['flash_success'][] = __('modules.success_install');
|
|
|
|
return new Response('', 302, ['Location' => url('/admin/modules/install')]);
|
|
|
|
} catch (\Throwable $e) {
|
|
// Clean up on error
|
|
if (isset($tempDir) && is_dir($tempDir)) {
|
|
$this->recursiveDelete($tempDir);
|
|
}
|
|
|
|
$_SESSION['flash_error'][] = $e->getMessage();
|
|
return new Response('', 302, ['Location' => url('/admin/modules/install')]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate module structure.
|
|
*
|
|
* @param string $modulePath
|
|
* @throws \Exception
|
|
*/
|
|
protected function validateModuleStructure(string $modulePath): void
|
|
{
|
|
$requiredFiles = ['module.json'];
|
|
|
|
foreach ($requiredFiles as $file) {
|
|
if (!file_exists($modulePath . '/' . $file)) {
|
|
throw new \Exception(__('modules.error_missing_file', ['file' => $file]));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read module configuration.
|
|
*
|
|
* @param string $modulePath
|
|
* @return array
|
|
* @throws \Exception
|
|
*/
|
|
protected function readModuleConfig(string $modulePath): array
|
|
{
|
|
$configPath = $modulePath . '/module.json';
|
|
|
|
if (!file_exists($configPath)) {
|
|
throw new \Exception(__('modules.error_missing_config'));
|
|
}
|
|
|
|
$config = json_decode(file_get_contents($configPath), true);
|
|
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
throw new \Exception(__('modules.error_invalid_config', ['error' => json_last_error_msg()]));
|
|
}
|
|
|
|
$requiredFields = ['name', 'version', 'description'];
|
|
|
|
foreach ($requiredFields as $field) {
|
|
if (empty($config[$field])) {
|
|
throw new \Exception(__('modules.error_missing_field', ['field' => $field]));
|
|
}
|
|
}
|
|
|
|
return $config;
|
|
}
|
|
|
|
/**
|
|
* Recursively copy directory.
|
|
*
|
|
* @param string $source
|
|
* @param string $destination
|
|
*/
|
|
protected function recursiveCopy(string $source, string $destination): void
|
|
{
|
|
if (!is_dir($destination)) {
|
|
mkdir($destination, 0755, true);
|
|
}
|
|
|
|
$iterator = new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
|
|
\RecursiveIteratorIterator::SELF_FIRST
|
|
);
|
|
|
|
foreach ($iterator as $item) {
|
|
// Calculate subPathName manually to ensure compatibility
|
|
$len = strlen($source);
|
|
$subPathName = substr($item->getPathname(), $len + 1);
|
|
|
|
$targetPath = $destination . '/' . $subPathName;
|
|
|
|
if ($item->isDir()) {
|
|
mkdir($targetPath, 0755, true);
|
|
} else {
|
|
copy($item, $targetPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recursively delete directory.
|
|
*
|
|
* @param string $dir
|
|
*/
|
|
protected function recursiveDelete(string $dir): void
|
|
{
|
|
if (!is_dir($dir)) {
|
|
return;
|
|
}
|
|
|
|
$files = array_diff(scandir($dir), ['.', '..']);
|
|
|
|
foreach ($files as $file) {
|
|
$path = $dir . '/' . $file;
|
|
|
|
if (is_dir($path)) {
|
|
$this->recursiveDelete($path);
|
|
} else {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
rmdir($dir);
|
|
}
|
|
|
|
/**
|
|
* Run module migrations.
|
|
*
|
|
* @param string $modulePath
|
|
*/
|
|
protected function runModuleMigrations(string $modulePath): void
|
|
{
|
|
$migrationsPath = $modulePath . '/database/migrations';
|
|
|
|
if (!is_dir($migrationsPath)) {
|
|
return;
|
|
}
|
|
|
|
$migrations = glob($migrationsPath . '/*.php');
|
|
|
|
foreach ($migrations as $migration) {
|
|
require_once $migration;
|
|
|
|
// Try to find class name from file content
|
|
$content = file_get_contents($migration);
|
|
if (preg_match('/class\s+(\w+)/', $content, $matches)) {
|
|
$className = $matches[1];
|
|
|
|
if (class_exists($className)) {
|
|
$instance = new $className();
|
|
|
|
if (method_exists($instance, 'up')) {
|
|
try {
|
|
$instance->up();
|
|
} catch (\Exception $e) {
|
|
error_log("Migration failed for $className: " . $e->getMessage());
|
|
throw $e; // Re-throw to show error to user
|
|
}
|
|
}
|
|
} else {
|
|
error_log("Class $className not found in migration file: $migration");
|
|
}
|
|
} else {
|
|
error_log("Could not find class name in migration file: $migration");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fix module menus in database.
|
|
*
|
|
* @return Response
|
|
*/
|
|
public function fixModuleMenus(): Response
|
|
{
|
|
$user = $this->getCurrentUser();
|
|
|
|
if (!$user || !$user->isSuperAdmin()) {
|
|
return new Response('Access denied', 403);
|
|
}
|
|
|
|
$modules = Module::all();
|
|
$count = 0;
|
|
|
|
foreach ($modules as $module) {
|
|
$modulePath = dirname(__DIR__, 2) . '/' . $module->modules_path;
|
|
|
|
try {
|
|
$config = $this->readModuleConfig($modulePath);
|
|
|
|
// Update module settings with menu
|
|
$module->modules_settings = array_merge($module->modules_settings ?? [], $config['settings'] ?? [], ['menu' => $config['menu'] ?? []]);
|
|
|
|
// Also update top-level module fields that might be in module.json
|
|
if (isset($config['display_name'])) $module->modules_display_name = $config['display_name'];
|
|
if (isset($config['description'])) $module->modules_description = $config['description'];
|
|
if (isset($config['version'])) $module->modules_version = $config['version'];
|
|
if (isset($config['icon'])) $module->modules_icon = $config['icon'];
|
|
if (isset($config['menu_side_bar'])) $module->menu_side_bar = (bool)$config['menu_side_bar'];
|
|
if (isset($config['menu_nav_bar'])) $module->menu_nav_bar = (bool)$config['menu_nav_bar'];
|
|
if (isset($config['order'])) $module->modules_order = (int)$config['order'];
|
|
if (isset($config['namespace'])) $module->modules_namespace = $config['namespace'];
|
|
|
|
$module->save();
|
|
$count++;
|
|
} catch (\Exception $e) {
|
|
// Ignore errors for individual modules
|
|
error_log("Failed to update menu for module {$module->modules_name}: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
$_SESSION['flash_success'][] = __('modules.success_menus_updated', ['count' => $count]);
|
|
|
|
return new Response('', 302, ['Location' => url('/admin/modules/install')]);
|
|
}
|
|
|
|
/**
|
|
* Register module in database.
|
|
*
|
|
* @param array $config
|
|
* @return Module
|
|
*/
|
|
protected function registerModule(array $config): Module
|
|
{
|
|
return Module::create([
|
|
'modules_name' => $config['name'],
|
|
'modules_display_name' => $config['display_name'] ?? $config['name'],
|
|
'modules_description' => $config['description'],
|
|
'modules_version' => $config['version'],
|
|
'modules_author' => $config['author'] ?? 'Unknown',
|
|
'modules_ative' => true,
|
|
'modules_public' => $config['public'] ?? false,
|
|
'modules_blocked' => false,
|
|
'modules_dependencies' => $config['dependencies'] ?? [],
|
|
'modules_permissions' => $config['permissions'] ?? [],
|
|
'modules_settings' => array_merge($config['settings'] ?? [], ['menu' => $config['menu'] ?? []]),
|
|
'modules_path' => 'modules/' . $config['name'],
|
|
'modules_namespace' => $config['namespace'] ?? ('Modules\\' . ucfirst($config['name'])),
|
|
'modules_icon' => $config['icon'] ?? 'bi bi-gear',
|
|
'menu_side_bar' => $config['menu_side_bar'] ?? true,
|
|
'menu_nav_bar' => $config['menu_nav_bar'] ?? false,
|
|
'modules_order' => $config['order'] ?? 999,
|
|
'modules_install_date' => \Carbon\Carbon::now(),
|
|
'modules_update_date' => \Carbon\Carbon::now()
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Enable module for all companies.
|
|
*
|
|
* @param int $moduleId
|
|
* @param int $enabledBy
|
|
*/
|
|
protected function enableModuleForAllCompanies(int $moduleId, int $enabledBy): void
|
|
{
|
|
$companies = Company::all();
|
|
|
|
foreach ($companies as $company) {
|
|
$company->enableModule($moduleId, $enabledBy);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Search for modules and users.
|
|
*
|
|
* @param Request $request
|
|
* @return Response
|
|
*/
|
|
public function search(Request $request): Response
|
|
{
|
|
$user = $this->getCurrentUser();
|
|
$query = $request->query->get('q');
|
|
|
|
if (!$user) {
|
|
return new Response('', 302, ['Location' => url('/login')]);
|
|
}
|
|
|
|
if (empty($query)) {
|
|
return $this->renderAdminView('dashboard/search_results.twig', [
|
|
'query' => '',
|
|
'results' => ['modules' => [], 'users' => []]
|
|
]);
|
|
}
|
|
|
|
$results = [
|
|
'modules' => [],
|
|
'users' => []
|
|
];
|
|
|
|
// Search Modules
|
|
$results['modules'] = Module::where('modules_name', 'LIKE', "%{$query}%")
|
|
->orWhere('modules_display_name', 'LIKE', "%{$query}%")
|
|
->orWhere('modules_description', 'LIKE', "%{$query}%")
|
|
->get();
|
|
|
|
// Search Users (Super Admin or Company Admin)
|
|
if ($user->isSuperAdmin()) {
|
|
$results['users'] = User::where('users_name', 'LIKE', "%{$query}%")
|
|
->orWhere('users_email', 'LIKE', "%{$query}%")
|
|
->get();
|
|
} elseif ($user->users_bus_id) {
|
|
$results['users'] = User::where('users_bus_id', $user->users_bus_id)
|
|
->where(function($q) use ($query) {
|
|
$q->where('users_name', 'LIKE', "%{$query}%")
|
|
->orWhere('users_email', 'LIKE', "%{$query}%");
|
|
})
|
|
->get();
|
|
}
|
|
|
|
return $this->renderAdminView('dashboard/search_results.twig', [
|
|
'query' => $query,
|
|
'results' => $results
|
|
]);
|
|
}
|
|
}
|