diff --git a/public/main/auth/inscription.php b/public/main/auth/inscription.php index 6b556b3f668..b0f43215fa1 100644 --- a/public/main/auth/inscription.php +++ b/public/main/auth/inscription.php @@ -288,6 +288,15 @@ $form->addEmailRule('email'); + $form->addRule( + 'email', + get_lang('This e-mail address has already been used by the maximum number of allowed accounts. Please use another.'), + 'callback', + function ($email) { + return !api_email_reached_registration_limit($email); + } + ); + // USERNAME if ('true' != api_get_setting('login_is_email')) { $form->addText( diff --git a/public/main/inc/lib/api.lib.php b/public/main/inc/lib/api.lib.php index a249ac87130..f813420fa65 100644 --- a/public/main/inc/lib/api.lib.php +++ b/public/main/inc/lib/api.lib.php @@ -7574,3 +7574,21 @@ function api_calculate_increment_percent(int $newValue, int $oldValue): string } return $result; } + +/** + * @todo Move to UserRegistrationHelper when migrating inscription.php to Symfony + */ +function api_email_reached_registration_limit(string $email): bool +{ + $limit = (int) api_get_setting('platform.hosting_limit_identical_email'); + + if ($limit <= 0 || empty($email)) { + return false; + } + + $repo = Container::getUserRepository(); + $count = $repo->countUsersByEmail($email); + + return $count >= $limit; +} + diff --git a/public/main/inc/lib/internationalization.lib.php b/public/main/inc/lib/internationalization.lib.php index 14b93bf2860..dd2e6220fa4 100644 --- a/public/main/inc/lib/internationalization.lib.php +++ b/public/main/inc/lib/internationalization.lib.php @@ -219,6 +219,9 @@ function api_get_timezone(): string Session::write('system_timezone', $timezone); } + // Replace backslashes by forward slashes in case of wrong timezone format + $timezone = str_replace('\\', '/', $timezone); + return $timezone; } diff --git a/src/CoreBundle/DataFixtures/SettingsCurrentFixtures.php b/src/CoreBundle/DataFixtures/SettingsCurrentFixtures.php index 8a5623bc529..7345ee8eb0e 100644 --- a/src/CoreBundle/DataFixtures/SettingsCurrentFixtures.php +++ b/src/CoreBundle/DataFixtures/SettingsCurrentFixtures.php @@ -2012,6 +2012,11 @@ public static function getNewConfigurationSettings(): array ], ], 'platform' => [ + [ + 'name' => 'hosting_limit_identical_email', + 'title' => 'Limit identical email usage', + 'comment' => 'Maximum number of accounts allowed to share the same e-mail address. Set to 0 to disable this limit.', + ], [ 'name' => 'allow_double_validation_in_registration', 'title' => 'Double validation for registration process', diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20250709201100.php b/src/CoreBundle/Migrations/Schema/V200/Version20250709201100.php new file mode 100644 index 00000000000..ce5dcdf6331 --- /dev/null +++ b/src/CoreBundle/Migrations/Schema/V200/Version20250709201100.php @@ -0,0 +1,87 @@ + 'hosting_limit_identical_email', + 'selected_value' => '0', + 'title' => 'Limit identical email usage', + 'comment' => 'Maximum number of accounts allowed to share the same e-mail address. Set to 0 to disable this limit.', + 'category' => 'platform', + ]; + + $sqlCheck = sprintf( + "SELECT COUNT(*) as count + FROM settings + WHERE variable = '%s' + AND subkey IS NULL + AND access_url = 1", + addslashes($setting['variable']) + ); + + $stmt = $this->connection->executeQuery($sqlCheck); + $result = $stmt->fetchAssociative(); + + if ($result && (int)$result['count'] > 0) { + // UPDATE existing setting + $this->addSql(sprintf( + "UPDATE settings + SET selected_value = '%s', + title = '%s', + comment = '%s', + category = '%s' + WHERE variable = '%s' + AND subkey IS NULL + AND access_url = 1", + addslashes($setting['selected_value']), + addslashes($setting['title']), + addslashes($setting['comment']), + addslashes($setting['category']), + addslashes($setting['variable']) + )); + $this->write(sprintf("Updated setting: %s", $setting['variable'])); + } else { + // INSERT new setting + $this->addSql(sprintf( + "INSERT INTO settings + (variable, subkey, type, category, selected_value, title, comment, access_url_changeable, access_url_locked, access_url) + VALUES + ('%s', NULL, NULL, '%s', '%s', '%s', '%s', 1, 0, 1)", + addslashes($setting['variable']), + addslashes($setting['category']), + addslashes($setting['selected_value']), + addslashes($setting['title']), + addslashes($setting['comment']) + )); + $this->write(sprintf("Inserted setting: %s", $setting['variable'])); + } + } + + public function down(Schema $schema): void + { + $this->addSql(" + DELETE FROM settings + WHERE variable = 'hosting_limit_identical_email' + AND subkey IS NULL + AND access_url = 1 + "); + + $this->write("Removed setting: hosting_limit_identical_email."); + } +} diff --git a/src/CoreBundle/Repository/Node/UserRepository.php b/src/CoreBundle/Repository/Node/UserRepository.php index f75114afa7f..33ee9150928 100644 --- a/src/CoreBundle/Repository/Node/UserRepository.php +++ b/src/CoreBundle/Repository/Node/UserRepository.php @@ -1270,4 +1270,21 @@ public function findUsersForSessionAdmin( return $qb->getQuery()->getResult(); } + + /** + * Returns the number of users registered with a given email. + * + * @param string $email + * + * @return int + */ + public function countUsersByEmail(string $email): int + { + return (int) $this->createQueryBuilder('u') + ->select('COUNT(u.id)') + ->where('u.email = :email') + ->setParameter('email', $email) + ->getQuery() + ->getSingleScalarResult(); + } } diff --git a/src/CoreBundle/Settings/PlatformSettingsSchema.php b/src/CoreBundle/Settings/PlatformSettingsSchema.php index 4f934a3304c..ff68516682d 100644 --- a/src/CoreBundle/Settings/PlatformSettingsSchema.php +++ b/src/CoreBundle/Settings/PlatformSettingsSchema.php @@ -98,6 +98,7 @@ public function buildSettings(AbstractSettingsBuilder $builder): void 'show_tabs_per_role' => '{}', 'session_admin_user_subscription_search_extra_field_to_search' => '', 'push_notification_settings' => '', + 'hosting_limit_identical_email' => '0', ] ) ->setTransformer( @@ -161,6 +162,7 @@ public function buildForm(FormBuilderInterface $builder): void ->add('use_custom_pages', YesNoType::class) ->add('pdf_logo_header') ->add('allow_my_files', YesNoType::class) + // old settings with no category ->add('chamilo_database_version') ->add( 'load_term_conditions_section', @@ -247,7 +249,16 @@ public function buildForm(FormBuilderInterface $builder): void 'help' => 'User extra field key to use when searching and naming sessions from /admin-dashboard/register.', ] ) - ->add('push_notification_settings', TextareaType::class); + ->add('push_notification_settings', TextareaType::class) + ->add( + 'hosting_limit_identical_email', + TextType::class, + [ + 'label' => 'Limit identical emails', + 'help' => 'Maximum number of accounts allowed with the same email. Set to 0 to disable limit.' + ] + ) + ; $this->updateFormFieldsFromSettingsInfo($builder); }