First Commit
This commit is contained in:
Executable
+653
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
Executable
+465
@@ -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;
|
||||
}
|
||||
}
|
||||
Executable
+108
@@ -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)));
|
||||
}
|
||||
}
|
||||
Executable
+97
@@ -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]);
|
||||
}
|
||||
}
|
||||
Executable
+98
@@ -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';
|
||||
}
|
||||
}
|
||||
Executable
+302
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user