First Commit

This commit is contained in:
marco
2026-09-01 10:46:29 -03:00
commit bf96b77e9c
10000 changed files with 3508600 additions and 0 deletions
+653
View File
@@ -0,0 +1,653 @@
<?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
]);
}
}
+465
View File
@@ -0,0 +1,465 @@
<?php
namespace App\Controllers;
use Kernel\View;
use Modules\Auth\Models\User;
use App\Models\Module;
use Modules\Company\Models\Company;
use Illuminate\Database\Capsule\Manager as DB;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
abstract class BaseAdminController
{
protected View $view;
public function __construct(View $view)
{
$this->view = $view;
}
/**
* Render an admin view with common data (sidebar, user, etc).
*
* @param string $viewName
* @param array $data
* @return Response
*/
protected function renderAdminView(string $viewName, array $data = []): Response
{
$user = $this->getCurrentUser();
if (!$user) {
return new Response('', 302, ['Location' => url('/login')]);
}
// Get flash messages
$flashMessages = [];
if (isset($_SESSION['flash'])) {
$flashMessages = $_SESSION['flash'];
unset($_SESSION['flash']);
}
// Get enabled modules for the user's company
$enabledModules = $this->getEnabledModules($user);
// Build sidebar based on user permissions
$sidebarItems = $this->buildUserSidebar($enabledModules, $user);
// Build navbar based on user permissions
$navbarItems = $this->buildUserNavbar($enabledModules, $user);
// Get admin theme slug (default sb-admin)
$adminThemeSlug = 'sb-admin';
// Get config pages visibility
$configPagesVisibility = [];
if (class_exists('Modules\Configindex\Models\ConfigPage')) {
$baseDir = dirname(dirname(__DIR__));
$activeTheme = \Kernel\View::getActiveTheme() ?? 'default';
$indexViewsDir = $baseDir . '/modules/index/views';
$themeViewsDir = $baseDir . '/themes/' . $activeTheme . '/modules/index/views';
$pages = \Modules\Configindex\Models\ConfigPage::where('config_index_bus_id', $user->users_bus_id)->get();
foreach ($pages as $page) {
$key = $page->config_index_key;
// Verifica se o arquivo físico existe
$fileExists = false;
if ($key === 'index') {
$fileExists = true;
} else {
$possiblePaths = [
$indexViewsDir . '/' . $key . '.twig',
$indexViewsDir . '/' . $key . '.php',
$themeViewsDir . '/' . $key . '.twig',
$themeViewsDir . '/' . $key . '.php',
$baseDir . '/themes/' . $activeTheme . '/' . $key . '.twig',
$baseDir . '/themes/' . $activeTheme . '/' . $key . '.php',
$baseDir . '/modules/' . $key . '/views/index.twig',
];
foreach ($possiblePaths as $path) {
if (is_file($path)) {
$fileExists = true;
break;
}
}
}
$configPagesVisibility[$key] = [
'public' => (bool)$page->config_index_show_public_nav && $fileExists,
'dashboard' => (bool)$page->config_index_show_dashboard_nav && $fileExists,
'title' => $page->config_index_title ?: ucfirst($key)
];
}
// Garante que as páginas padrão tenham uma configuração inicial, mas respeita o banco de dados
foreach (['about', 'contact', 'services'] as $key) {
if (!isset($configPagesVisibility[$key])) {
$configPagesVisibility[$key] = ['public' => false, 'dashboard' => false, 'title' => ucfirst($key)];
}
}
}
$commonData = [
'auth' => ['user' => $user],
'user' => $user,
'sidebar_items' => $sidebarItems,
'navbar_items' => $navbarItems,
'current_company' => $this->getCurrentCompany($user),
'admin_theme_slug' => $adminThemeSlug,
'config_pages_visibility' => $configPagesVisibility,
'flash_messages' => $flashMessages
];
return new Response($this->view->render($viewName, array_merge($commonData, $data)));
}
protected function redirect(string $path, int $status = 302): Response
{
return new RedirectResponse(url($path), $status);
}
/**
* Get current authenticated user.
*
* @return User|null
*/
protected function getCurrentUser(): ?User
{
if (!isset($_SESSION['user_id'])) {
return null;
}
try {
return User::find($_SESSION['user_id']);
} catch (\Exception $e) {
error_log("Error loading user: " . $e->getMessage());
return null;
}
}
/**
* Check if current user can edit/delete an object.
* Only author, admin or super_admin are allowed.
*
* @param int|string $authorId
* @return bool
*/
protected function canEditObject($authorId): bool
{
$user = $this->getCurrentUser();
if (!$user) {
return false;
}
if ($user->isSuperAdmin() || $user->isAdmin()) {
return true;
}
return (int) $user->users_id === (int) $authorId;
}
/**
* Get enabled modules for the user.
*
* @param User $user
* @return \Illuminate\Database\Eloquent\Collection
*/
protected function getEnabledModules(User $user)
{
if ($user->isSuperAdmin()) {
// Super Admin sees all active modules
return Module::where('modules_ative', true)
->orderBy('modules_order', 'asc')
->get();
}
// For other users, get modules enabled for their company
$modules = Module::join('bus_modules', 'modules.modules_id', '=', 'bus_modules.bus_modules_module_id')
->where('bus_modules.bus_modules_bus_id', $user->users_bus_id)
->where('bus_modules.bus_modules_is_enabled', true)
->where('modules.modules_ative', true)
->orderBy('modules.modules_order', 'asc')
->select('modules.*', 'bus_modules.bus_modules_show_public_nav', 'bus_modules.bus_modules_show_dashboard_nav')
->get();
// If user is Admin, they see everything enabled for the company.
// If user is Member or User, we MUST ensure they see modules they have explicit permissions for,
// even if the module doesn't have bus_modules_show_dashboard_nav = true (if that was the blocker).
// If we're missing core modules (auth, index) in the company's enabled list,
// we should add them manually if they are active system-wide,
// because they are essential for the dashboard to function.
// We also add 'company' to ensure Admins can always manage their company details.
$coreModules = Module::whereIn('modules_name', ['auth', 'index', 'company'])
->where('modules_ative', true)
->get();
foreach ($coreModules as $core) {
if (!$modules->contains('modules_name', $core->modules_name)) {
$modules->push($core);
}
}
return $modules;
}
/**
* Build user sidebar based on permissions.
*
* @param \Illuminate\Database\Eloquent\Collection $modules
* @param User $user
* @return array
*/
protected function buildUserSidebar($modules, User $user): array
{
$sidebarItems = [];
// Ensure company settings link is always present for Admins
if ($user->isAdmin() || $user->isSuperAdmin()) {
$sidebarItems[] = [
'icon' => 'bi bi-building',
'label' => __('company.menu.dashboard'),
'route' => 'admin/company/settings',
'order' => 30
];
}
foreach ($modules as $module) {
// Skip company module if we already added it manually, or auth/index
if ($module->modules_name === 'auth' || $module->modules_name === 'index' || $module->modules_name === 'company') {
continue;
}
// Check if module is enabled for sidebar
if (!$module->menu_side_bar) {
continue;
}
$moduleConfig = $module->modules_settings ?: [];
// If settings are empty, try to load from module.json file as a fallback
if (empty($moduleConfig) || !isset($moduleConfig['menu'])) {
$moduleJsonPath = dirname(__DIR__, 2) . '/modules/' . $module->modules_name . '/module.json';
if (file_exists($moduleJsonPath)) {
$jsonContent = json_decode(file_get_contents($moduleJsonPath), true);
if ($jsonContent && isset($jsonContent['menu'])) {
$moduleConfig['menu'] = $jsonContent['menu'];
}
}
}
// Fix for company module if settings are still empty (due to migration issue)
if ((empty($moduleConfig) || !isset($moduleConfig['menu'])) && $module->modules_name === 'company') {
$moduleConfig['menu'] = [
'dashboard' => [
'icon' => 'bi bi-building',
'label' => 'company.menu.dashboard',
'route' => 'admin/company/settings',
'order' => 30
]
];
}
if (isset($moduleConfig['menu']['dashboard'])) {
$menuItem = $moduleConfig['menu']['dashboard'];
// Check if user can access this menu item
// Respect company visibility preference when not super admin or admin
// $companyAllows = ($user->isSuperAdmin() || $user->isAdmin()) || (isset($module->bus_modules_show_dashboard_nav) ? (bool) $module->bus_modules_show_dashboard_nav : true);
// Simplified logic: if it's in the $modules list (which already filters by company enablement)
// we only need to check the user's specific permissions.
if ($this->userCanAccessMenuItem($user, $menuItem, $module)) {
$sidebarItems[] = [
'icon' => $menuItem['icon'] ?? $module->modules_icon,
'label' => __($menuItem['label'] ?? $module->modules_display_name),
'route' => $menuItem['route'] ?? 'admin/' . $module->modules_name,
'order' => $menuItem['order'] ?? $module->modules_order
];
}
}
}
// Sort by order
usort($sidebarItems, function ($a, $b) {
return $a['order'] <=> $b['order'];
});
return $sidebarItems;
}
/**
* Build user navbar based on permissions.
*
* @param \Illuminate\Database\Eloquent\Collection $modules
* @param User $user
* @return array
*/
protected function buildUserNavbar($modules, User $user): array
{
$navbarItems = [];
foreach ($modules as $module) {
// Check if module is enabled for navbar
if (!$module->menu_nav_bar) {
continue;
}
$moduleConfig = $module->modules_settings ?: [];
// If settings are empty, try to load from module.json file as a fallback
if (empty($moduleConfig) || !isset($moduleConfig['menu'])) {
$moduleJsonPath = dirname(__DIR__, 2) . '/modules/' . $module->modules_name . '/module.json';
if (file_exists($moduleJsonPath)) {
$jsonContent = json_decode(file_get_contents($moduleJsonPath), true);
if ($jsonContent && isset($jsonContent['menu'])) {
$moduleConfig['menu'] = $jsonContent['menu'];
}
}
}
// Try to get specific navbar config, otherwise fallback to public or dashboard/general
if (isset($moduleConfig['menu']['navbar'])) {
$menuItem = $moduleConfig['menu']['navbar'];
} elseif (isset($moduleConfig['menu']['public'])) {
$menuItem = $moduleConfig['menu']['public'];
} elseif (isset($moduleConfig['menu']['dashboard'])) {
$menuItem = $moduleConfig['menu']['dashboard'];
} else {
continue;
}
// Check if user can access this menu item
// Respect company visibility preference when not super admin or admin
// $companyAllows = ($user->isSuperAdmin() || $user->isAdmin()) || (isset($module->bus_modules_show_public_nav) ? (bool) $module->bus_modules_show_public_nav : true);
if ($this->userCanAccessMenuItem($user, $menuItem, $module)) {
$route = $menuItem['route'] ?? $module->modules_name;
$navbarItems[] = [
'icon' => $menuItem['icon'] ?? $module->modules_icon,
'label' => __($menuItem['label'] ?? $module->modules_display_name),
'route' => $route,
'order' => $menuItem['order'] ?? $module->modules_order
];
}
}
// Sort by order
usort($navbarItems, function ($a, $b) {
return $a['order'] <=> $b['order'];
});
return $navbarItems;
}
/**
* Check if user can access a menu item.
*
* @param User $user
* @param array $menuItem
* @param Module|null $module
* @return bool
*/
protected function userCanAccessMenuItem(User $user, array $menuItem, ?Module $module = null): bool
{
// Custom restriction for companies management (Must check before general admin/superadmin bypass)
if (isset($menuItem['route']) && $menuItem['route'] === 'admin/companies') {
return $user->isSuperAdmin();
}
// Super Admin and Admin have access to everything authorized for the company
if ($user->isSuperAdmin() || $user->isAdmin()) {
return true;
}
// If specific permission defined for menu item, check it
if (!empty($menuItem['permission'])) {
return $user->hasPermission($menuItem['permission']);
}
// If no specific permission, but a module is provided
if ($module) {
// If the module is a core/blocked module, it's generally accessible
// (like Auth or Index)
if ($module->isBlocked()) {
return true;
}
// For Member role, if no specific permission is required for the menu item,
// we check if they have ANY permission for this module.
// This ensures that if an Admin gave them "pagebuilder.view", the Page Builder
// module link appears in the sidebar.
return $this->userHasModulePermission($user, $module);
}
// Default to allowed for items without permissions or module context
return true;
}
/**
* Get current company for the user.
*
* @param User $user
* @return Company|null
*/
protected function getCurrentCompany(User $user): ?Company
{
if ($user->isSuperAdmin()) {
return null; // Super Admin can switch companies
}
return Company::find($user->users_bus_id);
}
/**
* Check if user has permission for module.
*
* @param User $user
* @param Module $module
* @return bool
*/
protected function userHasModulePermission(User $user, Module $module): bool
{
// Super Admin and Admin have access to all modules enabled for their company
if ($user->isSuperAdmin() || $user->isAdmin()) {
return true;
}
// Core modules are accessible to all authenticated users
if ($module->isBlocked()) {
return true;
}
$modulePermissions = $module->getPermissionsAttribute();
// If module has no permissions defined and is public, it might be a general utility
if (empty($modulePermissions) && $module->isPublic()) {
return true;
}
foreach ($modulePermissions as $permission) {
if ($user->hasPermission($permission)) {
return true;
}
}
return false;
}
/**
* Add a flash message to the session.
*
* @param string $type success|error|info|warning
* @param string $message
*/
protected function addFlash(string $type, string $message): void
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$_SESSION['flash'][$type] = $message;
}
}
+108
View File
@@ -0,0 +1,108 @@
<?php
namespace App\Controllers;
use Kernel\View;
use Symfony\Component\HttpFoundation\Response;
abstract class BaseController
{
protected View $view;
public function __construct(View $view)
{
$this->view = $view;
}
/**
* Render a view with data.
*
* @param string $viewName
* @param array $data
* @return Response
*/
protected function renderView(string $viewName, array $data = []): Response
{
// Get config pages visibility
$configPagesVisibility = [];
if (class_exists('Modules\Configindex\Models\ConfigPage')) {
$busId = $_SESSION['user_idbus'] ?? $_SESSION['current_company_id'] ?? 1;
$baseDir = dirname(dirname(__DIR__));
$activeTheme = \Kernel\View::getActiveTheme() ?? 'default';
$indexViewsDir = $baseDir . '/modules/index/views';
$themeViewsDir = $baseDir . '/themes/' . $activeTheme . '/modules/index/views';
$pages = \Modules\Configindex\Models\ConfigPage::where('config_index_bus_id', $busId)->get();
foreach ($pages as $page) {
$key = $page->config_index_key;
// Verifica se o arquivo físico existe
$fileExists = false;
if ($key === 'index') {
$fileExists = true;
} else {
$possiblePaths = [
$indexViewsDir . '/' . $key . '.twig',
$indexViewsDir . '/' . $key . '.php',
$themeViewsDir . '/' . $key . '.twig',
$themeViewsDir . '/' . $key . '.php',
$baseDir . '/themes/' . $activeTheme . '/' . $key . '.twig',
$baseDir . '/themes/' . $activeTheme . '/' . $key . '.php',
$baseDir . '/modules/' . $key . '/views/index.twig', // Adicionado para checar módulos
];
foreach ($possiblePaths as $path) {
if (is_file($path)) {
$fileExists = true;
break;
}
}
}
$configPagesVisibility[$key] = [
'public' => (bool)$page->config_index_show_public_nav && $fileExists,
'dashboard' => (bool)$page->config_index_show_dashboard_nav && $fileExists,
'title' => $page->config_index_title ?: ucfirst($key)
];
}
// Garante que as páginas padrão tenham uma configuração inicial, mas respeita o banco de dados
foreach (['about', 'contact', 'services'] as $key) {
if (!isset($configPagesVisibility[$key])) {
// Verifica se o arquivo físico existe para os padrões também
$fileExists = false;
$possiblePaths = [
$indexViewsDir . '/' . $key . '.twig',
$indexViewsDir . '/' . $key . '.php',
$themeViewsDir . '/' . $key . '.twig',
$themeViewsDir . '/' . $key . '.php',
$baseDir . '/themes/' . $activeTheme . '/' . $key . '.twig',
$baseDir . '/themes/' . $activeTheme . '/' . $key . '.php',
];
foreach ($possiblePaths as $path) {
if (is_file($path)) {
$fileExists = true;
break;
}
}
// Se não estiver no banco, o padrão é oculto (false) para evitar exibição indesejada
$configPagesVisibility[$key] = ['public' => false, 'dashboard' => false, 'title' => ucfirst($key)];
}
}
}
// Get SEO settings
$seoSettings = [];
if (class_exists('App\Models\Module')) {
$seoModule = \App\Models\Module::where('modules_name', 'seo')->first();
if ($seoModule && $seoModule->modules_settings) {
$seoSettings = $seoModule->modules_settings;
}
}
$commonData = [
'config_pages_visibility' => $configPagesVisibility,
'seo' => $seoSettings
];
return new Response($this->view->render($viewName, array_merge($commonData, $data)));
}
}
+97
View File
@@ -0,0 +1,97 @@
<?php
namespace App\Controllers;
use Kernel\View;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class QuillDemoController extends BaseAdminController
{
/**
* Displays the Quill demo page
*/
public function index(Request $request): Response
{
// Check if the user is authenticated
if (!$this->isAuthenticated()) {
return $this->redirectToRoute('login');
}
// Render the demo view
$view = new View();
$content = $view->render('examples/quill-demo.twig');
return new Response($content);
}
/**
* Processes the demo form submission
*/
public function submit(Request $request): Response
{
// Check if the user is authenticated
if (!$this->isAuthenticated()) {
return $this->redirectToRoute('login');
}
// Get form data
$title = $request->get('title');
$content = $request->get('content');
$advancedContent = $request->get('advanced_content');
$simpleContent = $request->get('simple_content');
// Save to session to display on results page
$_SESSION['form_data'] = [
'title' => $title,
'content' => $content,
'advanced_content' => $advancedContent,
'simple_content' => $simpleContent
];
// Redirect to results page
return $this->redirectToRoute('quill-demo-results');
}
/**
* Displays form results
*/
public function results(Request $request): Response
{
// Check if the user is authenticated
if (!$this->isAuthenticated()) {
return $this->redirectToRoute('login');
}
// Get session data
$formData = $_SESSION['form_data'] ?? [];
// Clear session data
unset($_SESSION['form_data']);
// Render the results view
$view = new View();
$content = $view->render('examples/quill-results.twig', [
'formData' => $formData
]);
return new Response($content);
}
/**
* Checks if the user is authenticated
*/
private function isAuthenticated(): bool
{
return isset($_SESSION['user_id']);
}
/**
* Redirects to a route
*/
private function redirectToRoute(string $route): Response
{
$url = url($route);
return new Response("", 302, ["Location" => $url]);
}
}
+98
View File
@@ -0,0 +1,98 @@
<?php
namespace App\Controllers;
use Kernel\View;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
class StorageController extends BaseController
{
/**
* Serve a file from storage/uploads/{module}/{filename}
*
* @param Request $request
* @param string $module
* @param string $filename
* @return Response
*/
public function serve(Request $request, string $module, string $filename): Response
{
// Sanitize inputs to prevent directory traversal
$module = preg_replace('/[^a-zA-Z0-9_-]/', '', $module);
// Allow subfolders in filename (e.g. "123/image.jpg")
$filename = ltrim($filename, '/');
if (strpos($filename, '..') !== false) {
return new Response('Access denied.', 403);
}
// Build the physical path
$basePath = dirname(__DIR__, 2) . '/storage/uploads';
$filePath = $basePath . '/' . $module . '/' . $filename;
// Security check: Ensure the file is actually inside storage/uploads
$realPath = realpath($filePath);
if (!$realPath || strpos($realPath, realpath($basePath)) !== 0) {
return new Response('File not found or access denied.', 404);
}
if (!file_exists($realPath)) {
return new Response('File not found.', 404);
}
// Determine MIME type
$mimeType = $this->getMimeType($realPath);
// Create BinaryFileResponse for efficient file serving
$response = new BinaryFileResponse($realPath);
$response->headers->set('Content-Type', $mimeType);
$response->setContentDisposition(
ResponseHeaderBag::DISPOSITION_INLINE,
$filename
);
// Add cache headers (cache for 30 days)
$response->setPublic();
$response->setMaxAge(2592000);
$response->setSharedMaxAge(2592000);
return $response;
}
/**
* Get MIME type of a file
*/
private function getMimeType(string $path): string
{
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
$mimes = [
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'webp' => 'image/webp',
'svg' => 'image/svg+xml',
'pdf' => 'application/pdf',
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xls' => 'application/vnd.ms-excel',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'txt' => 'text/plain',
'zip' => 'application/zip',
];
if (isset($mimes[$extension])) {
return $mimes[$extension];
}
if (function_exists('mime_content_type')) {
return mime_content_type($path);
}
return 'application/octet-stream';
}
}
+302
View File
@@ -0,0 +1,302 @@
<?php
namespace App\Controllers;
use Kernel\View;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Modules\Auth\Models\User;
use Illuminate\Database\Capsule\Manager as DB;
class SystemController extends BaseAdminController
{
private $logPath;
private $cachePath;
public function __construct(View $view)
{
parent::__construct($view);
$this->logPath = dirname(__DIR__, 2) . '/logs';
$this->cachePath = dirname(__DIR__, 2) . '/cache';
}
/**
* Display system information.
*
* @param Request $request
* @return Response
*/
public function info(Request $request): Response
{
$user = $this->getCurrentUser();
if (!$user || !$user->isSuperAdmin()) {
return new Response($this->view->render('components/alerts.twig', [
'type' => 'danger',
'message' => 'Access denied.'
]), 403);
}
$dbVersion = 'Unknown';
try {
$pdo = DB::connection()->getPdo();
$dbVersion = $pdo->getAttribute(\PDO::ATTR_SERVER_VERSION);
} catch (\Exception $e) {
$dbVersion = 'Error: ' . $e->getMessage();
}
$info = [
'app' => [
'name' => 'SysCMS',
'version' => '1.0.0', // Should be from config or const
'env' => getenv('APP_ENV') ?: 'production',
'debug' => getenv('APP_DEBUG') ?: false,
],
'system' => [
'os' => php_uname('s') . ' ' . php_uname('r'),
'hostname' => gethostname(),
'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown',
'timezone' => date_default_timezone_get(),
],
'php' => [
'version' => phpversion(),
'sapi' => php_sapi_name(),
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time') . 's',
'upload_max_filesize' => ini_get('upload_max_filesize'),
'post_max_size' => ini_get('post_max_size'),
'extensions' => get_loaded_extensions(),
],
'database' => [
'driver' => DB::connection()->getDriverName(),
'version' => $dbVersion,
'database' => DB::connection()->getDatabaseName(),
],
'disk' => [
'total' => $this->formatBytes(disk_total_space('.')),
'free' => $this->formatBytes(disk_free_space('.')),
'used' => $this->formatBytes(disk_total_space('.') - disk_free_space('.')),
]
];
return $this->renderAdminView('system/info.twig', [
'page_title' => 'System Information',
'info' => $info
]);
}
/**
* Display PHP Info.
*
* @param Request $request
* @return Response
*/
public function phpInfo(Request $request): Response
{
$user = $this->getCurrentUser();
if (!$user || !$user->isSuperAdmin()) {
return new Response('Access denied', 403);
}
ob_start();
phpinfo();
$phpinfo = ob_get_clean();
// Extract body content to embed cleanly if desired, or just output raw.
// For admin panel consistency, usually raw in a new tab is easiest,
// but embedding provides better UX.
// Let's strip body tags to embed.
$phpinfo = preg_replace('%^.*<body>(.*)</body>.*$%ms', '$1', $phpinfo);
return $this->renderAdminView('system/phpinfo.twig', [
'page_title' => 'PHP Info',
'phpinfo' => $phpinfo
]);
}
/**
* List all log files.
*
* @param Request $request
* @return Response
*/
public function logsIndex(Request $request): Response
{
$user = $this->getCurrentUser();
if (!$user || !$user->isSuperAdmin()) {
return new Response($this->view->render('components/alerts.twig', [
'type' => 'danger',
'message' => 'Access denied.'
]), 403);
}
$files = [];
if (is_dir($this->logPath)) {
$files = glob($this->logPath . '/*.log');
}
// Sort by modification time desc
usort($files, function($a, $b) {
return filemtime($b) - filemtime($a);
});
$logs = [];
foreach ($files as $file) {
$logs[] = [
'name' => basename($file),
'size' => $this->formatBytes(filesize($file)),
'modified' => date('Y-m-d H:i:s', filemtime($file))
];
}
return $this->renderAdminView('logs/index.twig', [
'page_title' => 'System Logs',
'logs' => $logs
]);
}
/**
* Show log file content.
*
* @param Request $request
* @return Response
*/
public function logsShow(Request $request): Response
{
$user = $this->getCurrentUser();
if (!$user || !$user->isSuperAdmin()) {
return new Response($this->view->render('components/alerts.twig', [
'type' => 'danger',
'message' => 'Access denied.'
]), 403);
}
$filename = $request->query->get('file');
if (!$filename) {
return new RedirectResponse(url('admin/logs'));
}
// Security check: prevent directory traversal
if (strpos($filename, '..') !== false || strpos($filename, '/') !== false || strpos($filename, '\\') !== false) {
return new Response('Invalid filename', 400);
}
$filePath = $this->logPath . '/' . $filename;
if (!file_exists($filePath)) {
return new Response('File not found', 404);
}
// Limit file size read to avoid memory issues (e.g., 5MB)
if (filesize($filePath) > 5 * 1024 * 1024) {
// Read only last 500 lines if file is too big
$content = $this->tailCustom($filePath, 500);
$content = "File too large, showing last 500 lines:\n\n" . $content;
} else {
$content = file_get_contents($filePath);
}
return $this->renderAdminView('logs/show.twig', [
'page_title' => 'Log: ' . $filename,
'filename' => $filename,
'content' => $content
]);
}
/**
* Clear system cache.
*
* @param Request $request
* @return Response
*/
public function clearCache(Request $request): Response
{
$user = $this->getCurrentUser();
if (!$user || !$user->isSuperAdmin()) {
return new Response($this->view->render('components/alerts.twig', [
'type' => 'danger',
'message' => 'Access denied.'
]), 403);
}
$deletedCount = 0;
// Clear file cache
if (is_dir($this->cachePath)) {
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($this->cachePath, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $fileinfo) {
// Skip .gitignore
if ($fileinfo->getFilename() === '.gitignore') {
continue;
}
$todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
if (@$todo($fileinfo->getRealPath())) {
if ($todo === 'unlink') {
$deletedCount++;
}
}
}
}
// Clear OpCache
$opcacheCleared = false;
if (function_exists('opcache_reset')) {
$opcacheCleared = opcache_reset();
}
// Add a success flash message (if flash system exists, otherwise just redirect with query param)
// Since we don't have a flash message system clearly defined in context, let's use query param
return new RedirectResponse(url('dashboard?cache_cleared=1&deleted=' . $deletedCount . '&opcache=' . ($opcacheCleared ? 1 : 0)));
}
private function formatBytes($bytes, $precision = 2) {
$units = array('B', 'KB', 'MB', 'GB', 'TB');
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, $precision) . ' ' . $units[$pow];
}
/**
* Slightly modified tail function
*/
private function tailCustom($filepath, $lines = 1) {
$f = @fopen($filepath, "rb");
if ($f === false) return false;
$buffer = 4096;
fseek($f, -1, SEEK_END);
if (fread($f, 1) != "\n") $lines -= 1;
$output = '';
$chunk = '';
while (ftell($f) > 0 && $lines >= 0) {
$seek = min(ftell($f), $buffer);
fseek($f, -$seek, SEEK_CUR);
$chunk = fread($f, $seek);
$output = $chunk . $output;
fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
$lines -= substr_count($chunk, "\n");
}
while ($lines++ < 0) {
$output = substr($output, strpos($output, "\n") + 1);
}
fclose($f);
return $output;
}
}