CVE-2025-57423
Description
A SQL injection vulnerability was discovered in the /articles endpoint of MyClub 0.5, affecting the query parameters Content, GroupName, PersonName, lastUpdate, pool, and title. Due to insufficient input sanitisation, an unauthenticated remote attacker could inject arbitrary SQL commands via a crafted GET request, potentially leading to information disclosure or manipulation of the database.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
An unauthenticated SQL injection vulnerability in MyClub 0.5 allows attackers to execute arbitrary SQL commands via the /articles endpoint, leading to full database compromise.
Vulnerability
Analysis
A SQL injection vulnerability (CVE-2025-57423) exists in MyClub version 0.5, specifically within the /articles endpoint. The vulnerability affects six GET parameters: Content, GroupName, PersonName, lastUpdate, pool, and title [1]. Due to insufficient input sanitization, unauthenticated remote attackers can inject arbitrary SQL commands by crafting a malicious GET request [1].
Exploitation
Details
An attacker can exploit this vulnerability with no authentication required. A simple proof-of-concept injection using a single quote (') in the PersonName parameter immediately reveals the SQL error, exposing the query structure, file paths, and database engine (SQLite) [1]. Adding a second quote bypasses the error, confirming that arbitrary SQL commands can be injected [1]. The vulnerability allows attackers to extract the entire database schema, revealing 28 sensitive tables such as Person, Authorization, Group, Settings, and Message [1].
Impact
Successful exploitation can lead to full database access, including potential information disclosure, data manipulation, privilege escalation, and even denial of service [1].
Mitigation
The vulnerability was responsibly disclosed by William Fieldhouse of Aardwolf Security and has been patched in subsequent commits [2][3][4]. The developer addressed the issue by removing unsanitized Fluent query patterns and implementing parameterized queries in affected endpoints, as shown in commits 5741f39 and f067bb6 [2][3]. Users are strongly advised to update to the latest patched version of MyClub.
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
2f067bb63ac7dbig refactoring fixes (work in progress)
1 file changed · +11 −9
WebSite/app/helpers/LogDataHelper.php+11 −9 modified@@ -682,17 +682,19 @@ public function getVisitedPages($perPage, $logPage) return [$query->fetchAll(), ceil($total / $perPage)]; } - public function getPersons($filteredPersonEmails) + public function getPersons(array $filteredPersonEmails): array { - $filteredPersonEmails = array_filter($filteredPersonEmails); - $query = $this->fluent->from('Person') - ->select(null) - ->select('LOWER(Email) AS Email, FirstName, LastName'); - if (!empty($filteredPersonEmails)) { - $placeholders = implode(',', array_fill(0, count($filteredPersonEmails), '?')); - $query->where("Email COLLATE NOCASE IN ($placeholders)", $filteredPersonEmails); + $emails = array_filter(array_map('trim', array_values($filteredPersonEmails))); + $sql = "SELECT LOWER(Email) AS Email, FirstName, LastName FROM Person"; + $params = []; + if (!empty($emails)) { + $placeholders = implode(',', array_fill(0, count($emails), '?')); + $sql .= " WHERE LOWER(Email) IN ($placeholders)"; + $params = array_map('strtolower', $emails); } - return $query->fetchAll(); + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + return $stmt->fetchAll(); } public function getTopArticles(string $dateCondition, int $top): array
5741f39cf022Big refactoring fixes and fluent removing (work in progress)
22 files changed · +364 −485
dev/detect_circular_dependencies.php+0 −116 removed@@ -1,116 +0,0 @@ -<?php - -$root = __DIR__ . '/../WebSite/app'; - -function getPhpFiles($dir) -{ - $rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)); - $files = []; - foreach ($rii as $file) { - if (!$file->isDir() && pathinfo($file, PATHINFO_EXTENSION) === 'php') $files[] = $file->getPathname(); - } - return $files; -} - -function parseFileDependencies($filePath, $root) -{ - $content = file_get_contents($filePath); - $dependencies = []; - - // Normalize slashes - $normalizedPath = str_replace($root . '/', '', $filePath); - - // Match `use Some\Namespace\ClassName;` - if (preg_match_all('/use\s+([a-zA-Z0-9_\\\\]+)\s*;/', $content, $matches)) { - foreach ($matches[1] as $match) { - $dependencies[] = $match; - } - } - - // Match new ClassName() or \Fully\Qualified\ClassName - if (preg_match_all('/new\s+(\\\?[a-zA-Z0-9_\\\\]+)/', $content, $matches2)) { - foreach ($matches2[1] as $match) { - $dependencies[] = ltrim($match, '\\'); - } - } - - // Also match static calls like ClassName::method() - if (preg_match_all('/([a-zA-Z0-9_\\\\]+)::/', $content, $matches3)) { - foreach ($matches3[1] as $match) { - if (!in_array($match, $dependencies)) { - $dependencies[] = $match; - } - } - } - - return [$normalizedPath, array_unique($dependencies)]; -} - -function buildDependencyGraph($files, $root) -{ - $graph = []; - - foreach ($files as $file) { - list($filename, $deps) = parseFileDependencies($file, $root); - $graph[$filename] = []; - - foreach ($deps as $dep) { - $graph[$filename][] = $dep; - } - } - - return $graph; -} - -function detectCycles($graph) -{ - $visited = []; - $recStack = []; - $cycles = []; - - foreach (array_keys($graph) as $node) { - dfs($node, $graph, $visited, $recStack, [], $cycles); - } - - return $cycles; -} - -function dfs($node, $graph, &$visited, &$recStack, $path, &$cycles) -{ - if (isset($recStack[$node]) && $recStack[$node]) { - $cycleStart = array_search($node, $path); - if ($cycleStart !== false) { - $cycle = array_slice($path, $cycleStart); - $cycles[] = $cycle; - } - return; - } - - if (isset($visited[$node])) return; - - $visited[$node] = true; - $recStack[$node] = true; - $path[] = $node; - - foreach ($graph[$node] ?? [] as $neighbor) { - dfs($neighbor, $graph, $visited, $recStack, $path, $cycles); - } - - $recStack[$node] = false; -} - -// --- Main --- -echo "🔍 Analyse des dépendances...\n"; - -$files = getPhpFiles($root); -$graph = buildDependencyGraph($files, $root); -$cycles = detectCycles($graph); - -if (empty($cycles)) { - echo "✅ Aucune dépendance circulaire détectée.\n"; -} else { - echo "⚠️ Dépendances circulaires détectées :\n"; - foreach ($cycles as $cycle) { - echo " - " . implode(' → ', $cycle) . " → " . $cycle[0] . "\n"; - } -}
dev/LinesByFiles.txt+95 −60 modified@@ -1,5 +1,5 @@ === Détail par fichier === - 331 ../WebSite/index.php + 326 ../WebSite/index.php 65 ../WebSite/app/views/dbbrowser/edit.latte 43 ../WebSite/app/views/dbbrowser/index.latte 181 ../WebSite/app/views/dbbrowser/table.latte @@ -17,8 +17,8 @@ 40 ../WebSite/app/views/eventType/attributes-list.latte 27 ../WebSite/app/views/persons/index.latte 32 ../WebSite/app/views/navbar/redactor.latte - 52 ../WebSite/app/views/navbar/user.latte - 73 ../WebSite/app/views/navbar/home.latte + 66 ../WebSite/app/views/navbar/user.latte + 87 ../WebSite/app/views/navbar/home.latte 128 ../WebSite/app/views/navbar/index.latte 32 ../WebSite/app/views/navbar/eventManager.latte 29 ../WebSite/app/views/navbar/personManager.latte @@ -34,9 +34,9 @@ 190 ../WebSite/app/views/event/needs.latte 354 ../WebSite/app/views/event/nextEvents.latte 121 ../WebSite/app/views/event/chat.latte - 188 ../WebSite/app/views/event/weekEvents.latte + 187 ../WebSite/app/views/event/weekEvents.latte 63 ../WebSite/app/views/contact.latte - 60 ../WebSite/app/views/logs/referer.latte + 60 ../WebSite/app/views/logs/referent.latte 153 ../WebSite/app/views/logs/crossTab.latte 80 ../WebSite/app/views/logs/topPages.latte 178 ../WebSite/app/views/logs/analytics.latte @@ -55,7 +55,7 @@ 124 ../WebSite/app/views/user/presentation.latte 142 ../WebSite/app/views/user/ffaSearch.latte 4 ../WebSite/app/views/user/user.latte - 98 ../WebSite/app/views/user/articles.latte + 100 ../WebSite/app/views/user/articles.latte 30 ../WebSite/app/views/user/groups.latte 43 ../WebSite/app/views/user/map.latte 97 ../WebSite/app/views/user/editPresentation.latte @@ -74,35 +74,42 @@ 43 ../WebSite/app/views/media/upload.latte 27 ../WebSite/app/views/emails/copyToClipBoard.latte 76 ../WebSite/app/views/emails/getEmails.latte - 129 ../WebSite/app/views/common/dataTable.latte + 132 ../WebSite/app/views/common/dataTable.latte 156 ../WebSite/app/views/common/crosstab.latte 12 ../WebSite/app/views/info.latte 106 ../WebSite/app/views/import/form.latte 60 ../WebSite/app/views/contact/registration-success.latte - 164 ../WebSite/app/api/WebmasterApi.php - 105 ../WebSite/app/api/CarouselApi.php - 268 ../WebSite/app/api/ArticleApi.php - 706 ../WebSite/app/api/EventApi.php - 101 ../WebSite/app/controllers/EmailController.php - 319 ../WebSite/app/controllers/DbBrowserController.php - 299 ../WebSite/app/controllers/BaseController.php - 699 ../WebSite/app/controllers/UserController.php - 97 ../WebSite/app/controllers/NavBarController.php - 280 ../WebSite/app/controllers/WebmasterController.php - 124 ../WebSite/app/controllers/SurveyController.php - 39 ../WebSite/app/controllers/FFAController.php - 128 ../WebSite/app/controllers/EventTypeController.php - 161 ../WebSite/app/controllers/MediaController.php - 414 ../WebSite/app/controllers/EventController.php - 148 ../WebSite/app/controllers/ImportController.php - 323 ../WebSite/app/controllers/PersonController.php - 340 ../WebSite/app/controllers/LogController.php - 139 ../WebSite/app/controllers/RegistrationController.php - 351 ../WebSite/app/controllers/ArticleController.php - 44 ../WebSite/app/controllers/TableController.php - 85 ../WebSite/app/controllers/DesignController.php - 11 ../WebSite/app/controllers/CrudControllerInterface.php - 165 ../WebSite/app/controllers/GroupController.php + 10 ../WebSite/app/enums/EventAudience.php + 12 ../WebSite/app/enums/Authorization.php + 16 ../WebSite/app/enums/ApplicationError.php + 80 ../WebSite/app/controllers/EmailController.php + 107 ../WebSite/app/controllers/DbBrowserController.php + 100 ../WebSite/app/controllers/BaseController.php + 526 ../WebSite/app/controllers/UserController.php + 72 ../WebSite/app/controllers/NavBarController.php + 233 ../WebSite/app/controllers/WebmasterController.php + 105 ../WebSite/app/controllers/SurveyController.php + 43 ../WebSite/app/controllers/FFAController.php + 91 ../WebSite/app/controllers/EventTypeController.php + 130 ../WebSite/app/controllers/MediaController.php + 349 ../WebSite/app/controllers/EventController.php + 88 ../WebSite/app/controllers/ImportController.php + 234 ../WebSite/app/controllers/PersonController.php + 161 ../WebSite/app/controllers/LogController.php + 63 ../WebSite/app/controllers/RegistrationController.php + 250 ../WebSite/app/controllers/ArticleController.php + 58 ../WebSite/app/controllers/TableController.php + 59 ../WebSite/app/controllers/DesignController.php + 99 ../WebSite/app/controllers/GroupController.php + 9 ../WebSite/app/interfaces/NewsProviderInterface.php + 10 ../WebSite/app/interfaces/DatabaseMigratorInterface.php + 11 ../WebSite/app/interfaces/CrudControllerInterface.php + 120 ../WebSite/app/apis/WebmasterApi.php + 19 ../WebSite/app/apis/ImportApi.php + 84 ../WebSite/app/apis/CarouselApi.php + 140 ../WebSite/app/apis/ArticleApi.php + 45 ../WebSite/app/apis/BaseApi.php + 405 ../WebSite/app/apis/EventApi.php 69 ../WebSite/app/js/layout.js 55 ../WebSite/app/js/designs/index.js 48 ../WebSite/app/js/designs/create.js @@ -117,7 +124,7 @@ 143 ../WebSite/app/js/event/needs.js 539 ../WebSite/app/js/event/nextEvents.js 23 ../WebSite/app/js/event/guest.js - 46 ../WebSite/app/js/logs/topArticles.js + 45 ../WebSite/app/js/logs/topArticles.js 127 ../WebSite/app/js/admin/arwards.js 39 ../WebSite/app/js/survey/add.js 75 ../WebSite/app/js/user/preferences.js @@ -127,44 +134,72 @@ 22 ../WebSite/app/js/user/presentation.js 55 ../WebSite/app/js/user/map.js 86 ../WebSite/app/js/user/signIn.js - 27 ../WebSite/app/js/user/account.js + 23 ../WebSite/app/js/user/account.js 119 ../WebSite/app/js/user/article.js 55 ../WebSite/app/js/user/statistics.js 52 ../WebSite/app/js/media/upload.js 73 ../WebSite/app/js/media/list.js 87 ../WebSite/app/js/import/form.js - 61 ../WebSite/app/helpers/Message.php - 10 ../WebSite/app/helpers/EventAudience.php - 28 ../WebSite/app/helpers/Settings.php - 148 ../WebSite/app/helpers/Authorization.php - 19 ../WebSite/app/helpers/PasswordManager.php - 52 ../WebSite/app/helpers/Arwards.php - 92 ../WebSite/app/helpers/database/Database.php - 17 ../WebSite/app/helpers/database/File.php - 82 ../WebSite/app/helpers/TranslationManager.php - 66 ../WebSite/app/helpers/Alert.php + 22 ../WebSite/app/helpers/NeedDataHelper.php + 221 ../WebSite/app/helpers/PersonDataHelper.php + 743 ../WebSite/app/helpers/LogDataHelper.php + 593 ../WebSite/app/helpers/EventDataHelper.php + 59 ../WebSite/app/helpers/CarouselDataHelper.php + 16 ../WebSite/app/helpers/Generic.php + 45 ../WebSite/app/helpers/ReplyDataHelper.php + 88 ../WebSite/app/helpers/PageDataHelper.php + 90 ../WebSite/app/helpers/Database.php + 60 ../WebSite/app/helpers/ImportDataHelper.php + 192 ../WebSite/app/helpers/GroupDataHelper.php + 32 ../WebSite/app/helpers/TableControllerHelper.php + 11 ../WebSite/app/helpers/DataHelper.php + 302 ../WebSite/app/helpers/ArticleDataHelper.php + 186 ../WebSite/app/helpers/Data.php + 78 ../WebSite/app/helpers/Period.php + 47 ../WebSite/app/helpers/ArwardsDataHelper.php + 47 ../WebSite/app/helpers/ApiNeedTypeDataHelper.php + 37 ../WebSite/app/helpers/database/migrators/V1ToV2Migrator.php + 85 ../WebSite/app/helpers/AttributeDataHelper.php + 68 ../WebSite/app/helpers/TranslationManager.php 140 ../WebSite/app/helpers/FFAScraper.php - 75 ../WebSite/app/helpers/Client.php + 74 ../WebSite/app/helpers/Client.php + 228 ../WebSite/app/helpers/DbBrowserHelper.php 102 ../WebSite/app/helpers/Backup.php - 192 ../WebSite/app/helpers/News.php + 99 ../WebSite/app/helpers/SurveyDataHelper.php + 26 ../WebSite/app/helpers/ParticipantDataHelper.php + 28 ../WebSite/app/helpers/News.php 36 ../WebSite/app/helpers/GravatarHandler.php - 504 ../WebSite/app/helpers/PersonStatistics.php - 138 ../WebSite/app/helpers/Application.php - 38 ../WebSite/app/helpers/BaseHelper.php - 214 ../WebSite/app/helpers/Article.php - 647 ../WebSite/app/helpers/Log.php - 96 ../WebSite/app/helpers/Crosstab.php - 119 ../WebSite/app/helpers/PersonPreferences.php - 14 ../WebSite/app/helpers/Params.php - 430 ../WebSite/app/helpers/Event.php - 41 ../WebSite/app/helpers/Person.php - 127 ../WebSite/app/helpers/Email.php - 19484 total + 498 ../WebSite/app/helpers/PersonStatistics.php + 115 ../WebSite/app/helpers/Application.php + 86 ../WebSite/app/helpers/Sign.php + 111 ../WebSite/app/helpers/CrosstabDataHelper.php + 61 ../WebSite/app/helpers/ArticleTableDataHelper.php + 24 ../WebSite/app/helpers/File.php + 97 ../WebSite/app/helpers/AuthorizationDataHelper.php + 113 ../WebSite/app/helpers/ConnectedUser.php + 107 ../WebSite/app/helpers/MessageDataHelper.php + 52 ../WebSite/app/helpers/WebApp.php + 28 ../WebSite/app/helpers/ApiImportHelper.php + 42 ../WebSite/app/helpers/ApiNeedDataHelper.php + 90 ../WebSite/app/helpers/DesignDataHelper.php + 82 ../WebSite/app/helpers/ApiEventDataHelper.php + 19 ../WebSite/app/helpers/LanguagesDataHelper.php + 40 ../WebSite/app/helpers/PersonGroupDataHelper.php + 76 ../WebSite/app/helpers/Media.php + 69 ../WebSite/app/helpers/PersonPreferences.php + 42 ../WebSite/app/helpers/ArticleCrosstabDataHelper.php + 51 ../WebSite/app/helpers/Params.php + 31 ../WebSite/app/helpers/EventTypeDataHelper.php + 19 ../WebSite/app/helpers/Password.php + 21 ../WebSite/app/helpers/EventNeedHelper.php + 78 ../WebSite/app/helpers/ErrorManager.php + 53 ../WebSite/app/helpers/Email.php + 19997 total === Total par type de fichier === Fichiers PHP : -9329 +9815 Fichiers Latte : -7064 +7096 Fichiers Javascript : -3091 +3086
WebSite/app/apis/CarouselApi.php+8 −12 modified@@ -7,28 +7,25 @@ use app\helpers\Application; use app\helpers\CarouselDataHelper; +use app\helpers\DataHelper; use app\helpers\WebApp; class CarouselApi extends BaseApi { - private AuthorizationDataHelper $authorizationDataHelper; - private CarouselDataHelper $carouselDataHelper; public function __construct(Application $application) { parent::__construct($application); - $this->authorizationDataHelper = new AuthorizationDataHelper($application); - $this->carouselDataHelper = new CarouselDataHelper($this->application); } public function getItems($idArticle) { $person = $this->connectedUser->get()->person ?? false; - if (!$person || !$this->authorizationDataHelper->getArticle($idArticle, $person)) { + if (!$person || !(new AuthorizationDataHelper($this->application))->getArticle($idArticle, $person)) { $this->renderJson(['error' => 'Accès non autorisé'], 403); return; } - $items = $this->carouselDataHelper->getByArticle($idArticle); + $items = $this->dataHelper->gets('Carousel', ['IdArticle' => $idArticle]); $this->renderJson(['success' => true, 'items' => $items]); } @@ -44,14 +41,13 @@ public function saveItem() $this->renderJson(['error' => 'Données invalides'], 400); return; } - if (!$this->authorizationDataHelper->getArticle($data['idArticle'], $person)) { + if (!(new AuthorizationDataHelper($this->application))->getArticle($data['idArticle'], $person)) { $this->renderJson(['error' => 'Vous n\'êtes pas autorisé à modifier cet article'], 403); return; } - $item = WebApp::sanitizeHtml($data['item']); try { - $message = $this->carouselDataHelper->set_($data, $item); + $message = (new CarouselDataHelper($this->application))->set_($data, $item); $this->renderJson(['success' => true, 'message' => $message]); } catch (Throwable $e) { $this->renderJson(['error' => 'Erreur lors de l\'enregistrement: ' . $e->getMessage()], 500); @@ -65,17 +61,17 @@ public function deleteItem($id) $this->renderJson(['error' => 'Utilisateur non connecté'], 401); return; } - $item = $this->carouselDataHelper->get_($id); + $item = (new DataHelper($this->application))->get('Carousel', ['Id' => $id]); if (!$item) { $this->renderJson(['error' => 'Élément non trouvé'], 404); return; } - if (!$this->authorizationDataHelper->getArticle($item->IdArticle, $person)) { + if (!(new AuthorizationDataHelper($this->application))->getArticle($item->IdArticle, $person)) { $this->renderJson(['error' => 'Vous n\'êtes pas autorisé à modifier cet article'], 403); return; } try { - $this->carouselDataHelper->delete_($id); + (new DataHelper($this->application))->delete('Carousel', ['Id' => $id]); $this->renderJson(['success' => true, 'message' => 'Élément supprimé avec succès']); } catch (Throwable $e) { $this->renderJson(['error' => 'Erreur lors de la suppression: ' . $e->getMessage()], 500);
WebSite/app/apis/WebmasterApi.php+1 −1 modified@@ -32,7 +32,7 @@ public function addToGroup($personId, $groupId) } else $this->renderJson(['success' => false, 'message' => 'User not allowed'], 403); } - public function getPersonsInGroup($id) + public function getPersonsInGroup(?int $id): void { if ($this->connectedUser->get()->person ?? false) { if ($_SERVER['REQUEST_METHOD'] === 'GET') {
WebSite/app/controllers/ArticleController.php+3 −3 modified@@ -11,7 +11,7 @@ use app\helpers\ArticleTableDataHelper; use app\helpers\AuthorizationDataHelper; use app\helpers\Backup; -use app\helpers\CarouselDataHelper; +use app\helpers\DataHelper; use app\helpers\Params; use app\helpers\Period; use app\helpers\PersonDataHelper; @@ -121,13 +121,13 @@ public function show($id): void 'latestArticles' => $this->articleDataHelper->getLatestArticles_($articleIds), 'canEdit' => $canEdit, 'groups' => $this->dataHelper->gets('Group', ['Inactivated' => 0], 'Id, Name', 'Name'), - 'hasSurvey' => $this->dataHelper->get('Survey',['IdArticle' => $id]), + 'hasSurvey' => $this->dataHelper->get('Survey', ['IdArticle' => $id]), 'id' => $id, 'userConnected' => $this->connectedUser->person, 'navItems' => $this->getNavItems($this->connectedUser->person), 'publishedBy' => $chosenArticle->PublishedBy && $chosenArticle->PublishedBy != $chosenArticle->CreatedBy ? (new PersonDataHelper($this->application))->getPublisher($chosenArticle->PublishedBy) : '', 'canReadPool' => $this->authorizationDatahelper->canPersonReadSurveyResults($chosenArticle, $this->connectedUser->person), - 'carouselItems' => (new CarouselDataHelper($this->application))->getsForArticle($id), + 'carouselItems' => (new DataHelper($this->application))->gets('Carousel', ['IdArticle' => $id]), 'message' => $messages, ])); } else if ($this->connectedUser->person == '') $this->application->getErrorManager()->raise(ApplicationError::NotAllowed, 'Il faut être connecté pour pouvoir consulter cet article', 5000);
WebSite/app/controllers/BaseController.php+4 −0 modified@@ -96,5 +96,9 @@ private function addLatteFilters(): void elseif ($bytes >= 1024) return number_format($bytes / 1024, 2) . ' KB'; else return $bytes . ' bytes'; }); + + $this->latte->addFilter('readableDuration', function ($duration) { + return TranslationManager::getReadableDuration($duration); + }); } }
WebSite/app/controllers/EmailController.php+3 −3 modified@@ -21,8 +21,8 @@ public function fetchEmails() { if ($this->connectedUser->get()->isEventManager() ?? false) { if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $idGroup = $_POST['idGroup'] ?? ''; - $idEventType = $_POST['idEventType'] ?? ''; + $idGroup = isset($_POST['idGroup']) && is_numeric($_POST['idGroup']) ? (int)$_POST['idGroup'] : null; + $idEventType = isset($_POST['idGroup']) && is_numeric($_POST['idGroup']) ? (int)$_POST['idEventType'] : null; $dayOfWeek = $_POST['dayOfWeek'] ?? ''; $timeOfDay = $_POST['timeOfDay'] ?? ''; $filteredEmails = (new PersonDataHelper($this->application))->getEmailsOfInterestedPeople($idGroup, $idEventType, $dayOfWeek, $timeOfDay); @@ -34,7 +34,7 @@ public function fetchEmails() 'emailsJson' => json_encode($filteredEmails), 'emails' => $filteredEmails, 'filters' => "$groupName / $eventTypeName / $dayOfWeekName / $timeOfDay", - 'phones' => $this->dataHelper->gets('Person', ['Inactivated' => 0], "Email', 'Phone'"), + 'people' => $this->dataHelper->gets('Person', ['Inactivated' => 0], 'Email, Phone, FirstName, LastName', '', true), ])); } else if ($_SERVER['REQUEST_METHOD'] === 'GET') { $this->render('app/views/emails/getEmails.latte', Params::getAll([
WebSite/app/controllers/EventController.php+7 −7 modified@@ -37,9 +37,9 @@ public function nextEvents(): void 'navItems' => $this->getNavItems($this->connectedUser->get()->person ?? false), 'events' => $this->eventDataHelper->getEvents($this->connectedUser->person, $mode, $offset, $filterByPreferences), 'person' => $this->connectedUser->person, - 'eventTypes' => $this->dataHelper->gets('EventType', ['Inactivated' => 0], "'Id', 'Name'", 'Name'), - 'needTypes' => $this->dataHelper->gets('NeedType', [], "'Id', 'Name'", 'Name'), - 'eventAttributes' => $this->dataHelper->gets('Attribute', [], "'Id', 'Name, Detail, Color'"), + 'eventTypes' => $this->dataHelper->gets('EventType', ['Inactivated' => 0], 'Id, Name'), + 'needTypes' => $this->dataHelper->gets('NeedType', [], 'Id, Name'), + 'eventAttributes' => $this->dataHelper->gets('Attribute', [], 'Id, Name, Detail, Color'), 'offset' => $offset, 'mode' => $mode, 'filterByPreferences' => $filterByPreferences, @@ -51,8 +51,8 @@ public function weekEvents(): void { $this->render('app/views/event/weekEvents.latte', Params::getAll([ 'events' => $this->eventDataHelper->getNextWeekEvents(), - 'eventTypes' => $this->dataHelper->gets('EventType', ['Inactivated', 0], "'Id', 'Name'", 'Name'), - 'eventAttributes' => $this->dataHelper->gets('Attribute', [], "'Id', 'Name, Detail, Color'"), + 'eventTypes' => $this->dataHelper->gets('EventType', ['Inactivated' => 0], 'Id, Name'), + 'eventAttributes' => $this->dataHelper->gets('Attribute', [], 'Id, Name, Detail, Color'), 'navItems' => $this->getNavItems($this->connectedUser->get()->person ?? false), 'layout' => WebApp::getLayout() ])); @@ -182,7 +182,7 @@ public function guestInvite() } else $this->application->getErrorManager()->raise(ApplicationError::NotAllowed, 'Page not allowed in file ' . __FILE__ . ' at line ' . __LINE__); } - public function show($eventId, $message = null, $messageType = null): void + public function show(int $eventId, string $message = '', string $messageType = ''): void { $person = $this->connectedUser->get()->person ?? false; $userEmail = $person->Email ?? ''; @@ -196,7 +196,7 @@ public function show($eventId, $message = null, $messageType = null): void 'isRegistered' => $this->eventDataHelper->isUserRegistered($eventId, $userEmail), 'navItems' => $this->getNavItems($person), 'countOfMessages' => count($this->dataHelper->gets('Message', [ - 'From' => 'User', + '"From"' => 'User', 'EventId' => $eventId ])), 'eventNeeds' => $this->eventDataHelper->getEventNeeds($eventId),
WebSite/app/controllers/LogController.php+1 −1 modified@@ -129,7 +129,7 @@ public function crossTab() $emailFilter = $_GET['email'] ?? ''; $groupFilter = $_GET['group'] ?? ''; $period = $_GET['period'] ?? 'today'; - [$sortedCrossTabData, $filteredPersons, $columnTotals] = (new CrosstabDataHelper($this->application))->getPersons(Period::getDateConditions($period)); + [$sortedCrossTabData, $filteredPersons, $columnTotals] = (new CrosstabDataHelper($this->application))->getPersons(Period::getDateConditions($period), $uriFilter, $emailFilter, $groupFilter); $this->render('app/views/logs/crossTab.latte', Params::getAll([ 'title' => 'Tableau croisé dynamique des visites',
WebSite/app/helpers/AttributeDataHelper.php+20 −29 modified@@ -15,13 +15,8 @@ public function delete_($id) { try { $this->pdo->beginTransaction(); - $this->fluent->deleteFrom('EventTypeAttribute') - ->where('IdAttribute', $id) - ->execute(); - $this->fluent->deleteFrom('Attribute') - ->where('Id', $id) - ->execute(); - + $this->delete('EventTypeAttribute', ['IdAttribute' => $id]); + $this->delete('Attribute', ['Id' => $id]); $this->pdo->commit(); return [['success' => true], 200]; } catch (Throwable $e) { @@ -34,11 +29,11 @@ public function insert($data) { try { $this->pdo->beginTransaction(); - $this->fluent->insertInto('Attribute', [ + $this->set('Attribute', [ 'Name' => $data['name'], 'Detail' => $data['detail'], 'Color' => $data['color'] - ])->execute(); + ]); $this->pdo->commit(); return [['success' => true], 200]; } catch (Throwable $e) { @@ -47,34 +42,30 @@ public function insert($data) } } - public function getAttributesOf($eventTypeId) + public function getAttributesOf(int $eventTypeId): array { - return $this->fluent->from('EventTypeAttribute') - ->select('Attribute.*') - ->join('Attribute ON EventTypeAttribute.IdAttribute = Attribute.Id') - ->where('EventTypeAttribute.IdEventType', $eventTypeId) - ->fetchall(); + $sql = ' + SELECT Attribute.* + FROM EventTypeAttribute + INNER JOIN Attribute ON EventTypeAttribute.IdAttribute = Attribute.Id + WHERE EventTypeAttribute.IdEventType = :id + '; + $stmt = $this->pdo->prepare($sql); + $stmt->execute([':id' => $eventTypeId]); + return $stmt->fetchAll(); } - public function gets_() - { - return $this->fluent->from('Attribute') - ->orderBy('Name') - ->fetchAll(); - } + public function update($data) { try { $this->pdo->beginTransaction(); - $this->fluent->update('Attribute') - ->set([ - 'Name' => $data['name'], - 'Detail' => $data['detail'], - 'Color' => $data['color'] - ]) - ->where('Id', $data['id']) - ->execute(); + $this->set('Attribute', [ + 'Name' => $data['name'], + 'Detail' => $data['detail'], + 'Color' => $data['color'] + ], ['Id' => $data['id']]); $this->pdo->commit(); return ['success' => true]; } catch (Throwable $e) {
WebSite/app/helpers/AuthorizationDataHelper.php+20 −28 modified@@ -24,52 +24,47 @@ public function getsFor(ConnectedUser $connectedUser): array return array_column($query->fetchAll(), 'Name'); } - public function canPersonReadSurveyResults($article, $person): bool + public function canPersonReadSurveyResults(object $article, object $person): bool { - $survey = $this->fluent->from('Survey')->where('IdArticle', $article->Id)->fetch(); - if (!$survey || !$person) { - return false; - } - + $survey = $this->get('Survey', ['IdArticle' => $article->Id]); + if (!$survey || !$person) return false; $now = (new DateTime())->format('Y-m-d'); $closingDate = $survey->ClosingDate; - if ( $article->CreatedBy == $person->Id || $survey->Visibility == 'all' || $survey->Visibility == 'allAfterClosing' && $closingDate < $now - ) { - return true; - } - + ) return true; $stmt = $this->pdo->prepare('SELECT COUNT(*) FROM Reply WHERE IdSurvey = ? AND IdPerson = ?'); $stmt->execute([$survey->Id, $person->Id]); $hasVoted = $stmt->fetchColumn() > 0; - if ($hasVoted && ($survey->Visibility == 'voters' || ($survey->Visibility == 'votersAfterClosing' && $closingDate < $now))) { + if ($hasVoted && ($survey->Visibility == 'voters' || ($survey->Visibility == 'votersAfterClosing' && $closingDate < $now))) return true; - } return false; } - public function getArticle($id, $connectedUser) + public function getArticle($id, $connectedUser): object|false { - $article = $this->fluent->from('Article')->where('Id', $id)->fetch(); - if (!$this->canReadArticle($article, $connectedUser)) { - return false; - } + $article = $this->get('Article', ['Id' => $id]); + if (!$this->canReadArticle($article, $connectedUser)) return false; return $article; } public function getUserGroups(string $userEmail): array { - $rows = $this->fluent->from('PersonGroup') - ->select('PersonGroup.IdGroup AS IdGroup') - ->leftJoin('Person ON Person.Id = PersonGroup.IdPerson') - ->where('Person.Email', $userEmail) - ->fetchAll(); + $sql = ' + SELECT PersonGroup.IdGroup AS IdGroup + FROM PersonGroup + LEFT JOIN Person ON Person.Id = PersonGroup.IdPerson + WHERE Person.Email COLLATE NOCASE = :email + '; + $stmt = $this->pdo->prepare($sql); + $stmt->execute([':email' => $userEmail]); + $rows = $stmt->fetchAll(); return array_column($rows, 'IdGroup'); } + public function isUserInGroup($personEmail, $groupsFilter) { return !empty(array_intersect($this->getGroups($groupsFilter), $this->getUserGroups($personEmail))); @@ -88,10 +83,7 @@ private function canReadArticle($article, ConnectedUser $connectedUser) private function getGroups($groupsFilter): array { - $rows = $this->fluent->from('"Group"') - ->select('Id AS IdGroup') - ->where('Name LIKE "%' . $groupsFilter . '%"') - ->fetchAll(); - return array_column($rows, 'IdGroup'); + $rows = $this->gets('Group', ['Name LIKE "%' . $groupsFilter . '%"' => null]); + return array_column($rows, 'Id'); } }
WebSite/app/helpers/CarouselDataHelper.php+5 −41 modified@@ -10,50 +10,14 @@ public function __construct(Application $application) parent::__construct($application); } - public function delete_($id) - { - $this->fluent->deleteFrom('Carousel') - ->where('Id', $id) - ->execute(); - } - - public function get_($id) - { - return $this->fluent->from('Carousel') - ->where('Id', $id) - ->fetch(); - } - - public function getsForArticle($id) - { - $this->fluent->from('Carousel')->where('IdArticle', $id)->fetchAll(); - } - - public function getByArticle($idArticle) - { - return $this->fluent->from('Carousel')->where('IdArticle', $idArticle)->fetchAll(); - } - - public function set_($data, $item): string + public function set_(array $data, string $item): string { if (!empty($data['id'])) { - $this->fluent->update('Carousel') - ->set([ - 'Item' => $item - ]) - ->where('Id', $data['id']) - ->where('IdArticle', $data['idArticle']) - ->execute(); - $message = 'Élément mis à jour avec succès'; + $this->set('Carousel', ['Item' => $item], ['id' => $data['id'], 'IdArticle' => $data['idArticle']]); + return 'Élément mis à jour avec succès'; } else { - $this->fluent->insertInto('Carousel') - ->values([ - 'IdArticle' => $data['idArticle'], - 'Item' => $item - ]) - ->execute(); - $message = 'Élément ajouté avec succès'; + $this->set('Carousel', ['Item' => $item, 'IdArticle' => $data['idArticle']]); + return 'Élément ajouté avec succès'; } - return $message; } }
WebSite/app/helpers/CrosstabDataHelper.php+37 −27 modified@@ -55,23 +55,23 @@ public function generateCrosstab($sql, $params = [], $rowsTitle = 'Lignes', $col public function getevents($period) { $sql = " - SELECT - p.FirstName || ' ' || p.LastName || - CASE - WHEN p.NickName IS NOT NULL AND p.NickName != '' THEN ' (' || p.NickName || ')' - ELSE '' - END AS columnForCrosstab, - et.Name AS rowForCrosstab, - COUNT(DISTINCT e.Id) AS countForCrosstab, - COUNT(part.Id) AS count2ForCrosstab - FROM Person p - JOIN Event e ON p.Id = e.CreatedBy - JOIN EventType et ON e.IdEventType = et.Id - LEFT JOIN Participant part ON part.IdEvent = e.Id - WHERE e.LastUpdate BETWEEN :start AND :end - GROUP BY p.Id, et.Id - ORDER BY p.LastName, p.FirstName - "; + SELECT + p.FirstName || ' ' || p.LastName || + CASE + WHEN p.NickName IS NOT NULL AND p.NickName != '' THEN ' (' || p.NickName || ')' + ELSE '' + END AS columnForCrosstab, + et.Name AS rowForCrosstab, + COUNT(DISTINCT e.Id) AS countForCrosstab, + COUNT(part.Id) AS count2ForCrosstab + FROM Person p + JOIN Event e ON p.Id = e.CreatedBy + JOIN EventType et ON e.IdEventType = et.Id + LEFT JOIN Participant part ON part.IdEvent = e.Id + WHERE e.LastUpdate BETWEEN :start AND :end + GROUP BY p.Id, et.Id + ORDER BY p.LastName, p.FirstName + "; $dateRange = Period::getDateRangeFor($period); $crosstabData = $this->generateCrosstab( $sql, @@ -82,24 +82,34 @@ public function getevents($period) return [$dateRange, $crosstabData]; } - public function getPersons($dateCondition) + public function getPersons(string $dateCondition, ?string $uriFilter = null, ?string $emailFilter = null, ?string $groupFilter = null): array { - $crossTabQuery = $this->fluentForLog->from('Log') - ->select(null) - ->select('Uri, Who, COUNT(*) as count') - ->where($dateCondition) - ->groupBy('Uri, Who'); - if (!empty($uriFilter)) $crossTabQuery->where('Uri LIKE ?', "%$uriFilter%"); - if (!empty($emailFilter)) $crossTabQuery->where('Who LIKE ?', "%$emailFilter%"); - $crossTabData = $crossTabQuery->fetchAll(); + $sql = ' + SELECT Uri, Who, COUNT(*) as count + FROM Log + WHERE ' . $dateCondition . ' + '; + $params = []; + if (!empty($uriFilter)) { + $sql .= ' AND Uri LIKE :uriFilter'; + $params[':uriFilter'] = "%$uriFilter%"; + } + if (!empty($emailFilter)) { + $sql .= ' AND Who LIKE :emailFilter'; + $params[':emailFilter'] = "%$emailFilter%"; + } + $sql .= ' GROUP BY Uri, Who'; + $stmt = $this->pdoForLog->prepare($sql); + $stmt->execute($params); + $crossTabData = $stmt->fetchAll(); $filteredPersons = array_unique(array_column($crossTabData, 'Who')); $sortedCrossTabData = []; $columnTotals = []; foreach ($crossTabData as $row) { $uri = $row->Uri; $who = $row->Who; if (!empty($groupFilter) && !(new AuthorizationDataHelper($this->application))->isUserInGroup($who, $groupFilter)) continue; - $count = $row->count; + $count = (int) $row->count; if (!isset($sortedCrossTabData[$uri])) $sortedCrossTabData[$uri] = ['visits' => [], 'total' => 0]; $sortedCrossTabData[$uri]['visits'][$who] = $count; $sortedCrossTabData[$uri]['total'] += $count;
WebSite/app/helpers/Data.php+28 −13 modified@@ -2,10 +2,12 @@ namespace app\helpers; -use app\enums\ApplicationError; +use InvalidArgumentException; use PDO; use PDOException; +use app\enums\ApplicationError; + /* Examples ======== @@ -70,7 +72,7 @@ public function get(string $table, array $where = [], $fields = '*'): object|fal if (is_array($fields)) $fieldsStr = implode(', ', $fields); else $fieldsStr = $fields; - $sql = "SELECT {$fieldsStr} FROM '{$table}'"; + $sql = "SELECT {$fieldsStr} FROM \"{$table}\""; $params = []; if (!empty($where)) { $conditions = []; @@ -90,29 +92,42 @@ public function get(string $table, array $where = [], $fields = '*'): object|fal } } - public function gets(string $table, array $where = [], $fields = '*', string $orderBy = ''): array + public function gets(string $table, array $where = [], string $fields = '*', string $orderBy = '', bool $keyPair = false): array { try { - if (is_array($fields)) $fieldsStr = implode(', ', $fields); - else $fieldsStr = $fields; - - $sql = "SELECT {$fieldsStr} FROM '{$table}'"; + $firstField = null; + if ($keyPair) { + if ($fields === '*') throw new InvalidArgumentException("Cannot use keyPair with fields='*'"); + $fieldParts = array_map('trim', explode(',', $fields)); + $firstField = $fieldParts[0]; + } + $sql = "SELECT {$fields} FROM \"{$table}\""; $params = []; if (!empty($where)) { $conditions = []; foreach ($where as $field => $value) { - if ($value == null) $conditions[] = "{$field}"; + if ($value === null) $conditions[] = "{$field}"; else { - $conditions[] = "{$field} = :{$field}"; - $params[":{$field}"] = $value; + if (strtolower($field) === 'email') $conditions[] = "{$field} COLLATE NOCASE = ?"; + else $conditions[] = "{$field} = ?"; + $params[] = $value; } } $sql .= " WHERE " . implode(' AND ', $conditions); - if ($orderBy !== '') $sql .= " ORDER BY " . $orderBy; } + if ($orderBy !== '') $sql .= " ORDER BY " . $orderBy; $stmt = $this->pdo->prepare($sql); $stmt->execute($params); - return $stmt->fetchall(); + if ($keyPair) { + $results = $stmt->fetchAll(PDO::FETCH_OBJ); + $keyPairArray = []; + foreach ($results as $result) { + $key = $result->{$firstField}; + $keyPairArray[$key] = $result; + } + return $keyPairArray; + } + return $stmt->fetchAll(); } catch (PDOException $e) { $this->application->getErrorManager()->raise(ApplicationError::Error, 'Database error: ' . $e->getMessage() . ' in file ' . __FILE__ . ' at line ' . __LINE__); throw $e; @@ -129,7 +144,7 @@ public function query(string $sql, array $parameters = []): mixed $queryType = strtoupper(substr(trim($sql), 0, 6)); switch ($queryType) { case 'SELECT': - return $stmt->fetchAll(PDO::FETCH_OBJ); + return $stmt->fetchAll(); case 'INSERT': return $this->pdo->lastInsertId(); case 'UPDATE':
WebSite/app/helpers/DbBrowserHelper.php+1 −1 modified@@ -86,7 +86,7 @@ public function getTableColumnsDetails($table) $stmt->execute(); $columns = []; - while ($row = $stmt->fetch(PDO::FETCH_OBJ)) { + while ($row = $stmt->fetch()) { $columns[] = [ 'name' => $row->name, 'notnull' => $row->notnull
WebSite/app/helpers/ErrorManager.php+1 −3 modified@@ -48,16 +48,14 @@ private function log(int $code, string $message): void $client = new Client();; try { - $email = filter_var($_SESSION['user'] ?? '', FILTER_VALIDATE_EMAIL) ?: 'anonymous'; - + $email = filter_var($_SESSION['user'] ?? '', FILTER_VALIDATE_EMAIL) ?: ''; $stmt = $this->pdoForLog->prepare(" INSERT INTO Log ( IpAddress, Referer, Os, Browser, ScreenResolution, Type, Uri, Token, Who, Code, Message, CreatedAt ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) "); - $stmt->execute([ $client->getIp(), $client->getReferer(),
WebSite/app/helpers/EventDataHelper.php+2 −2 modified@@ -127,7 +127,7 @@ public function getEventAttributes($eventId) "; $stmt = $this->pdo->prepare($sql); $stmt->execute(['eventId' => $eventId]); - $result = $stmt->fetchAll(PDO::FETCH_OBJ); + $result = $stmt->fetchAll(); return $result; } @@ -316,7 +316,7 @@ public function getEventNeeds($eventId): array "; $stmt = $this->pdo->prepare($sql); $stmt->execute([$eventId, $eventId, $eventId]); - return $stmt->fetchAll(PDO::FETCH_OBJ); + return $stmt->fetchAll(); } public function getNews($person, $searchFrom): array
WebSite/app/helpers/LogDataHelper.php+93 −85 modified@@ -19,47 +19,53 @@ public function __construct(Application $application) $this->host = Application::$root . '%'; } - public function add($code, $message) + public function add(string $code, string $message): void { - $this->fluentForLog - ->insertInto('Log', [ - 'IpAddress' => $_SERVER['REMOTE_ADDR'], - 'Referer' => $_SERVER['HTTP_REFERER'] ?? '', - 'Os' => '', - 'Browser' => '', - 'ScreenResolution' => '', - 'Type' => '', - 'Uri' => $_SERVER['REQUEST_URI'], - 'Token' => '', - 'Who' => gethostbyaddr($_SERVER['REMOTE_ADDR']) ?? '', - 'Code' => $code, - 'Message' => $message - ]) - ->execute(); + $sql = ' + INSERT INTO Log (IpAddress, Referer, Os, Browser, ScreenResolution, Type, Uri, Token, Who, Code, Message) + VALUES (:ipAddress, :referer, :os, :browser, :screenResolution, :type, :uri, :token, :who, :code, :message) + '; + $params = [ + ':ipAddress' => $_SERVER['REMOTE_ADDR'], + ':referer' => $_SERVER['HTTP_REFERER'] ?? '', + ':os' => '', + ':browser' => '', + ':screenResolution' => '', + ':type' => '', + ':uri' => $_SERVER['REQUEST_URI'], + ':token' => '', + ':who' => gethostbyaddr($_SERVER['REMOTE_ADDR']) ?? '', + ':code' => $code, + ':message' => $message, + ]; + $stmt = $this->pdoForLog->prepare($sql); + $stmt->execute($params); } - public function getOsDistribution() + public function getOsDistribution(): array { - $query = $this->fluentForLog - ->from('Log') - ->select('Os, COUNT(*) as count') - ->groupBy('Os') - ->orderBy('count DESC'); - $results = $query->fetchAll(); - + $sql = ' + SELECT Os, COUNT(*) AS count + FROM Log + GROUP BY Os + ORDER BY count DESC + '; + $stmt = $this->pdoForLog->prepare($sql); + $stmt->execute(); + $results = $stmt->fetchAll(); $labels = []; $data = []; - foreach ($results as $row) { $labels[] = $row->Os ?: 'Inconnu'; - $data[] = $row->count; + $data[] = (int) $row->count; } return [ 'labels' => $labels, 'data' => $data ]; } + public function getBrowserDistribution() { $query = $this->pdoForLog->query(" @@ -85,14 +91,12 @@ public function getBrowserDistribution() GROUP BY word ORDER BY count DESC"); $results = $query->fetchAll(); - $labels = []; $data = []; foreach ($results as $row) { $labels[] = $row->Browser ?? 'Inconnu'; $data[] = $row->count; } - return [ 'labels' => $labels, 'data' => $data @@ -101,15 +105,15 @@ public function getBrowserDistribution() public function getScreenResolutionDistribution() { - $query = $this->fluentForLog - ->from('Log') - ->select(null) - ->select('ScreenResolution, COUNT(*) as count') - ->groupBy('ScreenResolution') - ->orderBy('count DESC'); - - $results = $query->fetchAll(); - + $sql = ' + SELECT ScreenResolution, COUNT(*) AS count + FROM Log + GROUP BY ScreenResolution + ORDER BY count DESC + '; + $stmt = $this->pdoForLog->prepare($sql); + $stmt->execute(); + $results = $stmt->fetchAll(); $typeGroups = []; $typeResolutions = []; @@ -124,15 +128,13 @@ public function getScreenResolutionDistribution() $typeResolutions[$typeKey] = []; } $typeGroups[$typeKey] += $row->count; - if ($row->ScreenResolution && $row->ScreenResolution !== 'Inconnu') { $typeResolutions[$typeKey][] = $row->ScreenResolution; } } arsort($typeGroups); $labels = []; $data = []; - foreach ($typeGroups as $typeKey => $count) { $label = $typeKey; if (isset($typeResolutions[$typeKey]) && !empty($typeResolutions[$typeKey])) { @@ -142,7 +144,6 @@ public function getScreenResolutionDistribution() $labels[] = $label; $data[] = $count; } - return [ 'labels' => $labels, 'data' => $data @@ -251,22 +252,21 @@ private function getResolutionType($resolution) public function getTypeDistribution() { - $query = $this->fluentForLog - ->from('Log') - ->select('Type, COUNT(*) as count') - ->groupBy('Type') - ->orderBy('count DESC'); - - $results = $query->fetchAll(); - + $sql = ' + SELECT Type, COUNT(*) AS count + FROM Log + GROUP BY Type + ORDER BY count DESC + '; + $stmt = $this->pdoForLog->prepare($sql); + $stmt->execute(); + $results = $stmt->fetchAll(); $labels = []; $data = []; - foreach ($results as $row) { $labels[] = $row->Type ?: 'Inconnu'; $data[] = $row->count; } - return [ 'labels' => $labels, 'data' => $data @@ -588,27 +588,31 @@ public function getLastVisitPerActivePersonWithTimeAgo($activePersons) } return $visits; } - private function getLastVisitPerActivePerson($activePersons) + private function getLastVisitPerActivePerson(array $activePersons): array { $result = []; - foreach ($activePersons as $person) { - $lastLog = $this->fluentForLog->from('Log') - ->select('Uri, CreatedAt, Os, Browser') - ->where('Who COLLATE NOCASE', $person->Email) - ->orderBy('CreatedAt DESC') - ->limit(1) - ->fetch(); + $sql = ' + SELECT Uri, CreatedAt, Os, Browser + FROM Log + WHERE Who COLLATE NOCASE = :email + ORDER BY CreatedAt DESC + LIMIT 1 + '; + $stmt = $this->pdoForLog->prepare($sql); + foreach ($activePersons as $person) { + $stmt->execute([':email' => $person->Email]); + $lastLog = $stmt->fetch(); if ($lastLog) { $result[] = (object)[ - 'PersonId' => $person->Id, - 'FullName' => $person->FirstName . ' ' . $person->LastName, - 'Email' => $person->Email, - 'Avatar' => $person->Avatar, - 'LastPage' => $lastLog->Uri, + 'PersonId' => $person->Id, + 'FullName' => $person->FirstName . ' ' . $person->LastName, + 'Email' => $person->Email, + 'Avatar' => $person->Avatar, + 'LastPage' => $lastLog->Uri, 'LastActivity' => $lastLog->CreatedAt, - 'Os' => $lastLog->Os, - 'Browser' => $lastLog->Browser + 'Os' => $lastLog->Os, + 'Browser' => $lastLog->Browser ]; } } @@ -618,6 +622,7 @@ private function getLastVisitPerActivePerson($activePersons) return $result; } + private function calculateTimeAgo($dateTime) { $datetime = new DateTime($dateTime, new DateTimeZone('UTC')); @@ -690,28 +695,31 @@ public function getPersons($filteredPersonEmails) return $query->fetchAll(); } - public function getTopArticles($dateCondition, $top) + public function getTopArticles(string $dateCondition, int $top): array { - $query = $this->fluentForLog - ->from('Log') - ->select(' - Uri, - COUNT(*) AS visits, - CASE - WHEN Uri LIKE "/articles/%" THEN CAST(substr(Uri, 11) AS INTEGER) - WHEN Uri LIKE "/navbar/show/article/%" THEN CAST(substr(Uri, 22) AS INTEGER) - ELSE NULL - END AS articleId') - ->where($dateCondition) - ->where('( - (Uri LIKE "/articles/%" AND Uri GLOB "/articles/[0-9]*" AND Uri NOT LIKE "/articles/%/%") - OR + $sql = ' + SELECT + Uri, + COUNT(*) AS visits, + CASE + WHEN Uri LIKE "/articles/%" THEN CAST(substr(Uri, 11) AS INTEGER) + WHEN Uri LIKE "/navbar/show/article/%" THEN CAST(substr(Uri, 22) AS INTEGER) + ELSE NULL + END AS articleId + FROM Log + WHERE ' . $dateCondition . ' + AND ( + (Uri LIKE "/articles/%" AND Uri GLOB "/articles/[0-9]*" AND Uri NOT LIKE "/articles/%/%") + OR (Uri LIKE "/navbar/show/article/%" AND Uri GLOB "/navbar/show/article/[0-9]*" AND Uri NOT LIKE "/navbar/show/article/%/%") - )') - ->groupBy('Uri') - ->orderBy('visits DESC') - ->limit($top); - return $query->fetchAll(); + ) + GROUP BY Uri + ORDER BY visits DESC + LIMIT :top + '; + $stmt = $this->pdoForLog->prepare($sql); + $stmt->execute([':top' => $top]); + return $stmt->fetchAll(); } public function getTopPages($dateCondition, $top)
WebSite/app/helpers/PageDataHelper.php+17 −28 modified@@ -34,37 +34,27 @@ public function insertOrUpdate($data) if (empty($data['id'])) { $maxPosition = $this->fluent->from('Page')->select('MAX(Position) AS MaxPos')->fetch(); $newPosition = ($maxPosition && $maxPosition->MaxPos) ? $maxPosition->MaxPos + 1 : 1; - $this->fluent->insertInto('Page') - ->values([ - 'Name' => $data['name'], - 'Route' => $data['route'], - 'Position' => $newPosition, - 'IdGroup' => $data['idGroup'], - 'ForMembers' => $data['forMembers'], - 'ForAnonymous' => $data['forAnonymous'], - ]) - ->execute(); - } else { - $this->fluent->update('Page') - ->set([ - 'Name' => $data['name'], - 'Route' => $data['route'], - 'IdGroup' => $data['idGroup'], - 'ForMembers' => $data['forMembers'], - 'ForAnonymous' => $data['forAnonymous'], - ]) - ->where('Id', $data['id']) - ->execute(); - } + $this->set('Page', [ + 'Name' => $data['name'], + 'Route' => $data['route'], + 'Position' => $newPosition, + 'IdGroup' => $data['idGroup'], + 'ForMembers' => $data['forMembers'], + 'ForAnonymous' => $data['forAnonymous'] + ]); + } else $this->set('Page', [ + 'Name' => $data['name'], + 'Route' => $data['route'], + 'IdGroup' => $data['idGroup'], + 'ForMembers' => $data['forMembers'], + 'ForAnonymous' => $data['forAnonymous'], + ], ['Id' => $data['id']]); } public function updates($positions) { foreach ($positions as $id => $position) { - $this->fluent->update('Page') - ->set(['Position' => $position]) - ->where('Id', $id) - ->execute(); + $this->set('Page', ['Position' => $position], ['Id' => $id]); } } @@ -77,8 +67,7 @@ public function authorizedUser($page, $person): bool ->fetch(); if (!$pageData) return false; if (!$pageData->IdGroup) { - if (!$person && $pageData->ForAnonymous) return true; - if ($person && $pageData->ForMembers) return true; + if ((!$person && $pageData->ForAnonymous) || ($person && $pageData->ForMembers)) return true; return false; } if (!$person) return false;
WebSite/app/helpers/PersonDataHelper.php+16 −23 modified@@ -13,21 +13,20 @@ public function __construct(Application $application) parent::__construct($application); } - public function getPersonsInGroup(int $idGroup, bool $everybodyIfNoGroup = false): array + public function getPersonsInGroup(?int $idGroup): array { $innerJoin = $and = ''; - if (!empty($idGroup) || $everybodyIfNoGroup) { + if ($idGroup !== null) { $innerJoin = 'INNER JOIN PersonGroup on PersonGroup.IdPerson = Person.Id'; $and = 'AND PersonGroup.IdGroup = ' . $idGroup; } - $query = $this->pdo->query(" + return $this->pdo->query(" SELECT Person.Id, FirstName, LastName, Email, Preferences, Availabilities FROM Person $innerJoin WHERE Person.Inactivated = 0 $and ORDER BY FirstName, LastName - "); - return $query->fetchAll(); + ")->fetchAll(); } public function getPersonsInGroupForDirectory($groupId) @@ -50,8 +49,6 @@ public function getPublisher($id): string|null return "publié par " . $person->FirstName . " " . $person->LastName; } - - public function create() { $query = $this->pdo->prepare("SELECT Id FROM Person WHERE Email = ''"); @@ -137,8 +134,8 @@ public function getNews($person, $searchFrom): array public function getPersonWantedToBeAlerted($idArticle): array { - $idGroup = $this->fluent->from('Article')->where('Id', $idArticle)->fetch('IdGroup'); - $idSurvey = $this->fluent->from('Survey')->where('IdArticle', $idArticle)->fetch('Id'); + $idGroup = $this->get('Article', ['Id' => $idArticle], 'IdGroup')->IdGroup; + $idSurvey = $this->get('Survey', ['IdArticle' => $idArticle], 'Id')->Id; $persons = (new PersonDataHelper($this->application))->getPersonsInGroup($idGroup); $filteredEmails = []; foreach ($persons as $person) { @@ -147,28 +144,24 @@ public function getPersonWantedToBeAlerted($idArticle): array $preferences = json_decode($person->Preferences ?? '', true); if ($preferences != '' && isset($preferences['eventTypes']['newArticle'])) { if (isset($preferences['eventTypes']['newArticle']['pollOnly'])) { - if ($idSurvey) { - $include = true; - } + if ($idSurvey) $include = true; } else $include = true; } } if ($include) { $filteredEmails[] = $person->Email; - $this->fluent->insertInto('Message') - ->values([ - 'EventId' => null, - 'PersonId' => $person->Id, - 'Text' => "New article \n\n /articles/" . $idArticle, - '"From"' => 'Webapp' - ]) - ->execute(); + $this->set('Message', [ + 'EventId' => null, + 'PersonId' => $person->Id, + 'Text' => "New article \n\n /articles/" . $idArticle, + '"From"' => 'Webapp' + ]); } } return $filteredEmails; } - public function getEmailsOfInterestedPeople($idGroup, $idEventType, $dayOfWeek, $timeOfDay) + public function getEmailsOfInterestedPeople(?int $idGroup, ?int $idEventType, string $dayOfWeek, string $timeOfDay): array { $persons = $this->getInterestedPeople($idGroup, $idEventType, $dayOfWeek, $timeOfDay); $filteredEmails = []; @@ -178,9 +171,9 @@ public function getEmailsOfInterestedPeople($idGroup, $idEventType, $dayOfWeek, return $filteredEmails; } - public function getInterestedPeople($idGroup, $idEventType, $dayOfWeek, $timeOfDay): array + public function getInterestedPeople(?int $idGroup, ?int $idEventType, string $dayOfWeek, string $timeOfDay): array { - $persons = (new PersonDataHelper($this->application))->getPersonsInGroup($idGroup, true); + $persons = (new PersonDataHelper($this->application))->getPersonsInGroup($idGroup); $filteredPeople = []; foreach ($persons as $person) { if ((new PersonPreferences())->isPersonInterested($person, $idEventType, $dayOfWeek, $timeOfDay)) $filteredPeople[] = $person;
WebSite/app/views/emails/copyToClipBoard.latte+1 −1 modified@@ -6,7 +6,7 @@ <h2>Liste des adresses email ({count($emails)}) avec {$filters}</h2> <ul> {foreach $emails as $email} - <li>{$email} ({$phones[$email]->Phone})</li> + <li>{$email} (📞{$people[$email]->Phone} 👤{$people[$email]->FirstName} {$people[$email]->LastName})</li> {/foreach} </ul> </div>
WebSite/index.php+1 −1 modified@@ -300,7 +300,7 @@ $application->getErrorManager()->raise(ApplicationError::Error, 'Error ' . $ex->getMessage() . ' in file ' . $ex->getFile() . ' at line ' . $ex->getLine()); }); $flight->after('start', function () use ($logDataHelper, $flight) { - $logDataHelper->add($flight->getData('code'), $flight->getData('message')); + $logDataHelper->add($flight->getData('code') ?? '', $flight->getData('message') ?? ''); }); $flight->start();
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.