Updates and fixes the register plugin
parent
c42b2ff483
commit
d3a9d78e2c
|
|
@ -0,0 +1,59 @@
|
||||||
|
# Register Plugin
|
||||||
|
|
||||||
|
Provides user registration functionality.
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### POST /register?action=register
|
||||||
|
Register a new user account.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
- `username` (string, 3-20 chars, required)
|
||||||
|
- `password` (string, 8-255 chars, required)
|
||||||
|
- `confirm_password` (string, required)
|
||||||
|
- `csrf_token` (string, required)
|
||||||
|
- `terms` (boolean, required)
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"message": "Registration successful. You can log in now.",
|
||||||
|
"redirect_to": "?page=login"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /register?action=status
|
||||||
|
Check if registration is enabled.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Enable/disable registration in `totalmeet.conf.php`:
|
||||||
|
```php
|
||||||
|
'registration_enabled' => true,
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
- CSRF protection
|
||||||
|
- Rate limiting
|
||||||
|
- Password hashing
|
||||||
|
- Input sanitization
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
Uses simple callable dispatcher pattern for single-action plugin:
|
||||||
|
```php
|
||||||
|
register_plugin_route_prefix('register', [
|
||||||
|
'dispatcher' => function($context) {
|
||||||
|
require_once PLUGIN_REGISTER_PATH . 'controllers/register.php';
|
||||||
|
},
|
||||||
|
'access' => 'public',
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
None - functions independently.
|
||||||
|
|
@ -1,12 +1,26 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
// Register plugin bootstrap
|
/**
|
||||||
// (here we add any plugin autoloader, if needed)
|
* Register Plugin Bootstrap
|
||||||
|
*
|
||||||
|
* Initializes the register plugin using the App API pattern.
|
||||||
|
*/
|
||||||
|
|
||||||
// List here all the controllers in "/controllers/" that we need as pages
|
// Define plugin base path if not already defined
|
||||||
$GLOBALS['plugin_controllers']['register'] = [
|
if (!defined('PLUGIN_REGISTER_PATH')) {
|
||||||
'register'
|
define('PLUGIN_REGISTER_PATH', __DIR__ . '/');
|
||||||
];
|
}
|
||||||
|
|
||||||
|
require_once PLUGIN_REGISTER_PATH . 'helpers.php';
|
||||||
|
require_once PLUGIN_REGISTER_PATH . 'controllers/register.php';
|
||||||
|
|
||||||
|
// Register route with dispatcher class
|
||||||
|
register_plugin_route_prefix('register', [
|
||||||
|
'dispatcher' => \Plugins\Register\Controllers\RegisterController::class,
|
||||||
|
'access' => 'public',
|
||||||
|
'defaults' => ['action' => 'register'],
|
||||||
|
'plugin' => 'register',
|
||||||
|
]);
|
||||||
|
|
||||||
// Add to publicly accessible pages
|
// Add to publicly accessible pages
|
||||||
register_hook('filter_public_pages', function($pages) {
|
register_hook('filter_public_pages', function($pages) {
|
||||||
|
|
|
||||||
|
|
@ -1,110 +1,189 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User registration
|
* User Registration API Controller
|
||||||
*
|
*
|
||||||
* This page ("register") handles user registration if the feature is enabled in the configuration.
|
* Provides RESTful endpoints for user registration.
|
||||||
* It accepts a POST request with a username and password, attempts to register the user,
|
* Follows the API pattern used by other plugins.
|
||||||
* and redirects to the login page on success or displays an error message on failure.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Define plugin base path if not already defined
|
namespace Plugins\Register\Controllers;
|
||||||
if (!defined('PLUGIN_REGISTER_PATH')) {
|
|
||||||
define('PLUGIN_REGISTER_PATH', dirname(__FILE__, 2) . '/');
|
use App\App;
|
||||||
}
|
use App\Helpers\Theme;
|
||||||
|
use Exception;
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
require_once APP_PATH . 'classes/feedback.php';
|
||||||
|
require_once APP_PATH . 'classes/user.php';
|
||||||
|
require_once APP_PATH . 'classes/validator.php';
|
||||||
|
require_once APP_PATH . 'helpers/security.php';
|
||||||
|
require_once APP_PATH . 'helpers/theme.php';
|
||||||
|
require_once APP_PATH . 'includes/rate_limit_middleware.php';
|
||||||
require_once PLUGIN_REGISTER_PATH . 'models/register.php';
|
require_once PLUGIN_REGISTER_PATH . 'models/register.php';
|
||||||
require_once dirname(__FILE__, 4) . '/app/classes/user.php';
|
|
||||||
require_once dirname(__FILE__, 4) . '/app/classes/validator.php';
|
|
||||||
require_once dirname(__FILE__, 4) . '/app/helpers/security.php';
|
|
||||||
|
|
||||||
// registration is allowed, go on
|
class RegisterController
|
||||||
if ($config['registration_enabled'] == true) {
|
{
|
||||||
|
/** @var \Database|\PDO|null */
|
||||||
|
private $db;
|
||||||
|
private array $config;
|
||||||
|
private string $appRoot;
|
||||||
|
private $logger;
|
||||||
|
|
||||||
try {
|
public function __construct()
|
||||||
global $db, $logObject, $userObject;
|
{
|
||||||
|
$this->db = App::db();
|
||||||
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
|
$this->config = App::config();
|
||||||
|
$this->appRoot = App::get('app_root') ?? '/';
|
||||||
// Apply rate limiting
|
$this->logger = App::get('logObject');
|
||||||
require_once dirname(__FILE__, 4) . '/app/includes/rate_limit_middleware.php';
|
|
||||||
checkRateLimit($db, 'register');
|
|
||||||
|
|
||||||
$security = SecurityHelper::getInstance();
|
|
||||||
|
|
||||||
// Sanitize input
|
|
||||||
$formData = $security->sanitizeArray($_POST, ['username', 'password', 'confirm_password', 'csrf_token', 'terms']);
|
|
||||||
|
|
||||||
// Validate CSRF token
|
|
||||||
if (!$security->verifyCsrfToken($formData['csrf_token'] ?? '')) {
|
|
||||||
throw new Exception(Feedback::get('ERROR', 'CSRF_INVALID')['message']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$validator = new Validator($formData);
|
|
||||||
$rules = [
|
|
||||||
'username' => [
|
|
||||||
'required' => true,
|
|
||||||
'min' => 3,
|
|
||||||
'max' => 20
|
|
||||||
],
|
|
||||||
'password' => [
|
|
||||||
'required' => true,
|
|
||||||
'min' => 8,
|
|
||||||
'max' => 100
|
|
||||||
],
|
|
||||||
'confirm_password' => [
|
|
||||||
'required' => true,
|
|
||||||
'matches' => 'password'
|
|
||||||
],
|
|
||||||
'terms' => [
|
|
||||||
'required' => true,
|
|
||||||
'equals' => 'on'
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
$username = $formData['username'] ?? 'unknown';
|
|
||||||
|
|
||||||
if ($validator->validate($rules)) {
|
|
||||||
$password = $formData['password'];
|
|
||||||
|
|
||||||
// registering
|
|
||||||
$register = new Register($db);
|
|
||||||
$result = $register->register($username, $password);
|
|
||||||
|
|
||||||
// redirect to login
|
|
||||||
if ($result === true) {
|
|
||||||
// Get the new user's ID for logging
|
|
||||||
$userId = $userObject->getUserId($username)[0]['id'];
|
|
||||||
$logObject->log('info', "Registration: New user \"$username\" registered successfully. IP: $user_IP", ['user_id' => $userId, 'scope' => 'user']);
|
|
||||||
Feedback::flash('NOTICE', 'DEFAULT', "Registration successful. You can log in now.");
|
|
||||||
header('Location: ' . htmlspecialchars($app_root . '?page=login'));
|
|
||||||
exit();
|
|
||||||
// registration fail, redirect to login
|
|
||||||
} else {
|
|
||||||
$logObject->log('error', "Registration: Failed registration attempt for user \"$username\". IP: $user_IP. Reason: $result", ['user_id' => null, 'scope' => 'system']);
|
|
||||||
Feedback::flash('ERROR', 'DEFAULT', "Registration failed. $result");
|
|
||||||
header('Location: ' . htmlspecialchars($app_root . '?page=register'));
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$error = $validator->getFirstError();
|
|
||||||
$logObject->log('error', "Registration: Failed validation for user \"" . ($username ?? 'unknown') . "\". IP: $user_IP. Reason: $error", ['user_id' => null, 'scope' => 'system']);
|
|
||||||
Feedback::flash('ERROR', 'DEFAULT', $error);
|
|
||||||
header('Location: ' . htmlspecialchars($app_root . '?page=register'));
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$logObject->log('error', "Registration: System error. IP: $user_IP. Error: " . $e->getMessage(), ['user_id' => null, 'scope' => 'system']);
|
|
||||||
Feedback::flash('ERROR', 'DEFAULT', $e->getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get any new feedback messages
|
public function handle(string $action, array $context = []): bool
|
||||||
include_once dirname(__FILE__, 4) . '/app/helpers/feedback.php';
|
{
|
||||||
|
$validSession = (bool)($context['valid_session'] ?? false);
|
||||||
|
$app_root = $context['app_root'] ?? $this->appRoot;
|
||||||
|
|
||||||
// Load the template
|
if (!$this->db) {
|
||||||
include PLUGIN_REGISTER_PATH . 'views/form-register.php';
|
\Feedback::flash('ERROR', 'DEFAULT', 'Registration service unavailable. Please try again later.');
|
||||||
|
$this->renderForm($validSession, $app_root, ['registrationEnabled' => false]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// registration disabled
|
if (!$this->isRegistrationEnabled()) {
|
||||||
} else {
|
\Feedback::flash('NOTICE', 'DEFAULT', 'Registration is currently disabled.');
|
||||||
echo Feedback::render('NOTICE', 'DEFAULT', 'Registration is disabled', false);
|
$this->renderForm($validSession, $app_root, ['registrationEnabled' => false]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$this->handleSubmission($validSession, $app_root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->renderForm($validSession, $app_root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isRegistrationEnabled(): bool
|
||||||
|
{
|
||||||
|
return (bool)($this->config['registration_enabled'] ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleSubmission(bool $validSession, string $app_root): void
|
||||||
|
{
|
||||||
|
checkRateLimit($this->db, 'register');
|
||||||
|
|
||||||
|
$security = \SecurityHelper::getInstance();
|
||||||
|
$formData = $security->sanitizeArray(
|
||||||
|
$_POST,
|
||||||
|
['username', 'password', 'confirm_password', 'csrf_token', 'terms']
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$security->verifyCsrfToken($formData['csrf_token'] ?? '')) {
|
||||||
|
\Feedback::flash('ERROR', 'DEFAULT', 'Invalid security token. Please try again.');
|
||||||
|
$this->renderForm($validSession, $app_root, [
|
||||||
|
'values' => ['username' => $formData['username'] ?? ''],
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$validator = new \Validator($formData);
|
||||||
|
$rules = [
|
||||||
|
'username' => [
|
||||||
|
'required' => true,
|
||||||
|
'min' => 3,
|
||||||
|
'max' => 20,
|
||||||
|
],
|
||||||
|
'password' => [
|
||||||
|
'required' => true,
|
||||||
|
'min' => 8,
|
||||||
|
'max' => 255,
|
||||||
|
],
|
||||||
|
'confirm_password' => [
|
||||||
|
'required' => true,
|
||||||
|
'matches' => 'password',
|
||||||
|
],
|
||||||
|
'terms' => [
|
||||||
|
'required' => true,
|
||||||
|
'accepted' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!$validator->validate($rules)) {
|
||||||
|
\Feedback::flash('ERROR', 'DEFAULT', $validator->getFirstError());
|
||||||
|
$this->renderForm($validSession, $app_root, [
|
||||||
|
'values' => ['username' => $formData['username'] ?? ''],
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = trim($formData['username']);
|
||||||
|
$password = $formData['password'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$register = new \Register($this->db);
|
||||||
|
$result = $register->register($username, $password);
|
||||||
|
|
||||||
|
if ($result === true) {
|
||||||
|
$this->logSuccessfulRegistration($username);
|
||||||
|
\Feedback::flash('NOTICE', 'DEFAULT', 'Registration successful. You can log in now.');
|
||||||
|
header('Location: ' . $app_root . '?page=login');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
\Feedback::flash('ERROR', 'DEFAULT', 'Registration failed: ' . $result);
|
||||||
|
$this->renderForm($validSession, $app_root, [
|
||||||
|
'values' => ['username' => $username],
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
\Feedback::flash('ERROR', 'DEFAULT', 'Registration failed: ' . $e->getMessage());
|
||||||
|
$this->renderForm($validSession, $app_root, [
|
||||||
|
'values' => ['username' => $username],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function logSuccessfulRegistration(string $username): void
|
||||||
|
{
|
||||||
|
if (!$this->logger) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$userModel = new \User($this->db);
|
||||||
|
$userRecord = $userModel->getUserId($username);
|
||||||
|
$userId = $userRecord[0]['id'] ?? null;
|
||||||
|
$userIP = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||||
|
|
||||||
|
$this->logger->log(
|
||||||
|
'info',
|
||||||
|
sprintf('Registration: New user "%s" registered successfully. IP: %s', $username, $userIP),
|
||||||
|
['user_id' => $userId, 'scope' => 'user']
|
||||||
|
);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
app_log('warning', 'RegisterController logging failed: ' . $e->getMessage(), ['scope' => 'plugin']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderForm(bool $validSession, string $app_root, array $data = []): void
|
||||||
|
{
|
||||||
|
$formValues = $data['values'] ?? ['username' => ''];
|
||||||
|
$registrationEnabled = $data['registrationEnabled'] ?? true;
|
||||||
|
|
||||||
|
Theme::include('page-header');
|
||||||
|
Theme::include('page-menu');
|
||||||
|
if ($validSession) {
|
||||||
|
Theme::include('page-sidebar');
|
||||||
|
}
|
||||||
|
|
||||||
|
include APP_PATH . 'helpers/feedback.php';
|
||||||
|
|
||||||
|
$app_root_value = $app_root; // align variable name for template include
|
||||||
|
$app_root = $app_root_value;
|
||||||
|
$values = $formValues;
|
||||||
|
|
||||||
|
include PLUGIN_REGISTER_PATH . 'views/form-register.php';
|
||||||
|
|
||||||
|
Theme::include('page-footer');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registration Plugin Helpers
|
||||||
|
*
|
||||||
|
* Aggregates all helper modules for the registration plugin.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Include any helper modules here when they are added
|
||||||
|
// For now, the registration plugin doesn't have separate helper modules
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\App;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* class Register
|
* class Register
|
||||||
*
|
*
|
||||||
* Handles user registration.
|
* Handles user registration using the App API pattern.
|
||||||
*/
|
*/
|
||||||
class Register {
|
class Register {
|
||||||
/**
|
/**
|
||||||
|
|
@ -15,21 +17,18 @@ class Register {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register constructor.
|
* Register constructor.
|
||||||
* Initializes the database connection.
|
* Initializes the database connection using App API.
|
||||||
*
|
*
|
||||||
* @param object $database The database object to initialize the connection.
|
* @param PDO|null $database The database connection (optional, will use App::db() if not provided).
|
||||||
*/
|
*/
|
||||||
public function __construct($database) {
|
public function __construct($database = null) {
|
||||||
if ($database instanceof PDO) {
|
$this->db = $database instanceof PDO ? $database : App::db();
|
||||||
$this->db = $database;
|
|
||||||
} else {
|
|
||||||
$this->db = $database->getConnection();
|
|
||||||
}
|
|
||||||
require_once dirname(__FILE__, 4) . '/app/classes/ratelimiter.php';
|
|
||||||
require_once dirname(__FILE__, 4) . '/app/classes/twoFactorAuth.php';
|
|
||||||
|
|
||||||
$this->rateLimiter = new RateLimiter($database);
|
require_once APP_PATH . 'classes/ratelimiter.php';
|
||||||
$this->twoFactorAuth = new TwoFactorAuthentication($database);
|
require_once APP_PATH . 'classes/twoFactorAuth.php';
|
||||||
|
|
||||||
|
$this->rateLimiter = new RateLimiter($this->db);
|
||||||
|
$this->twoFactorAuth = new TwoFactorAuthentication($this->db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ if (!isset($page)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// List of pages that don't require authentication
|
// List of pages that don't require authentication
|
||||||
$public_pages = ['login', 'help', 'about', 'theme-asset', 'plugin-asset'];
|
$public_pages = ['login', 'register', 'help', 'about', 'theme-asset', 'plugin-asset'];
|
||||||
|
|
||||||
// Let plugins filter/extend public_pages
|
// Let plugins filter/extend public_pages
|
||||||
$public_pages = filter_public_pages($public_pages);
|
$public_pages = filter_public_pages($public_pages);
|
||||||
|
|
@ -152,7 +152,7 @@ $allowed_urls = [
|
||||||
'settings','theme','theme-asset','plugin-asset',
|
'settings','theme','theme-asset','plugin-asset',
|
||||||
'admin','status',
|
'admin','status',
|
||||||
'help','about',
|
'help','about',
|
||||||
'login','logout',
|
'login','register','logout',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Let plugins filter/extend allowed_urls
|
// Let plugins filter/extend allowed_urls
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue