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
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user