Compare commits
1 Commits
a4526979c4
...
feature/bl
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d078cb954 |
34
.env.example
34
.env.example
@@ -1,34 +0,0 @@
|
|||||||
# Database Configuration
|
|
||||||
DB_HOST=localhost
|
|
||||||
DB_USER=root
|
|
||||||
DB_PASS=
|
|
||||||
DB_NAME=4wdcsa
|
|
||||||
|
|
||||||
# Security
|
|
||||||
SALT=your-random-salt-here
|
|
||||||
|
|
||||||
# Mailjet Email Service
|
|
||||||
MAILJET_API_KEY=1a44f8d5e847537dbb8d3c76fe73a93c
|
|
||||||
MAILJET_API_SECRET=ec98b45c53a7694c4f30d09eee9ad280
|
|
||||||
MAILJET_FROM_EMAIL=info@4wdcsa.co.za
|
|
||||||
MAILJET_FROM_NAME=4WDCSA
|
|
||||||
ADMIN_EMAIL=admin@4wdcsa.co.za
|
|
||||||
|
|
||||||
# PayFast Payment Gateway
|
|
||||||
PAYFAST_MERCHANT_ID=10021495
|
|
||||||
PAYFAST_MERCHANT_KEY=yzpdydo934j92
|
|
||||||
PAYFAST_PASSPHRASE=SheSells7Shells
|
|
||||||
PAYFAST_DOMAIN=www.thepinto.co.za/4wdcsa
|
|
||||||
PAYFAST_TESTING_MODE=true
|
|
||||||
|
|
||||||
# Google OAuth
|
|
||||||
GOOGLE_CLIENT_ID=your-google-client-id
|
|
||||||
GOOGLE_CLIENT_SECRET=your-google-client-secret
|
|
||||||
|
|
||||||
# Instagram (optional)
|
|
||||||
INSTAGRAM_ACCESS_TOKEN=your-instagram-token
|
|
||||||
|
|
||||||
# Application Settings
|
|
||||||
APP_ENV=development
|
|
||||||
APP_DEBUG=true
|
|
||||||
APP_URL=https://www.thepinto.co.za/4wdcsa
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
php_flag display_errors Off
|
php_flag display_errors On
|
||||||
# php_value error_reporting -1
|
# php_value error_reporting -1
|
||||||
RedirectMatch 403 ^/\.well-known
|
RedirectMatch 403 ^/\.well-known
|
||||||
Options -Indexes
|
Options -Indexes
|
||||||
|
|||||||
@@ -1,429 +0,0 @@
|
|||||||
# Migration Guide: Using the New Service Layer
|
|
||||||
|
|
||||||
## For Developers
|
|
||||||
|
|
||||||
### Understanding the New Architecture
|
|
||||||
|
|
||||||
The code has been refactored to use a **Service Layer pattern**. Instead of functions directly accessing the database, they delegate to service classes:
|
|
||||||
|
|
||||||
#### Old Way (Before):
|
|
||||||
```php
|
|
||||||
function sendVerificationEmail($email, $name, $token) {
|
|
||||||
// ... 30 lines of Mailjet code with hardcoded credentials ...
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendInvoice($email, $name, $eft_id, $amount, $description) {
|
|
||||||
// ... 30 lines of Mailjet code (DUPLICATE) ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### New Way (After):
|
|
||||||
```php
|
|
||||||
function sendVerificationEmail($email, $name, $token) {
|
|
||||||
$service = new EmailService();
|
|
||||||
return $service->sendVerificationEmail($email, $name, $token);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using Services Directly (New Code)
|
|
||||||
|
|
||||||
When writing **new** code, you can use services directly for cleaner syntax:
|
|
||||||
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
require_once 'env.php';
|
|
||||||
|
|
||||||
use Services\UserService;
|
|
||||||
use Services\EmailService;
|
|
||||||
|
|
||||||
// Direct service usage (recommended for new code)
|
|
||||||
$userService = new UserService();
|
|
||||||
$emailService = new EmailService();
|
|
||||||
|
|
||||||
$email = $userService->getEmail(123);
|
|
||||||
$success = $emailService->sendVerificationEmail(
|
|
||||||
$email,
|
|
||||||
'John Doe',
|
|
||||||
'token123'
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Legacy Wrapper Functions
|
|
||||||
|
|
||||||
All original function names still work for **backward compatibility**:
|
|
||||||
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
// These still work and do the same thing
|
|
||||||
$fullName = getFullName(123);
|
|
||||||
$email = getEmail(123);
|
|
||||||
$success = sendVerificationEmail('user@example.com', 'John', 'token');
|
|
||||||
```
|
|
||||||
|
|
||||||
You can use either approach, but **new code should prefer services**.
|
|
||||||
|
|
||||||
## Specific Service Usage
|
|
||||||
|
|
||||||
### UserService
|
|
||||||
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
use Services\UserService;
|
|
||||||
|
|
||||||
$userService = new UserService();
|
|
||||||
|
|
||||||
// Get single field
|
|
||||||
$firstName = $userService->getFirstName($userId);
|
|
||||||
$email = $userService->getEmail($userId);
|
|
||||||
$profilePic = $userService->getProfilePic($userId);
|
|
||||||
|
|
||||||
// Get multiple fields at once (more efficient)
|
|
||||||
$userData = $userService->getUserInfo($userId, [
|
|
||||||
'first_name',
|
|
||||||
'last_name',
|
|
||||||
'email',
|
|
||||||
'phone'
|
|
||||||
]);
|
|
||||||
echo $userData['first_name'];
|
|
||||||
echo $userData['email'];
|
|
||||||
```
|
|
||||||
|
|
||||||
### EmailService
|
|
||||||
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
use Services\EmailService;
|
|
||||||
|
|
||||||
$emailService = new EmailService();
|
|
||||||
|
|
||||||
// Send using template (Mailjet)
|
|
||||||
$emailService->sendVerificationEmail(
|
|
||||||
'user@example.com',
|
|
||||||
'John Doe',
|
|
||||||
'verification-token-xyz'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Send custom HTML email
|
|
||||||
$emailService->sendCustom(
|
|
||||||
'user@example.com',
|
|
||||||
'John Doe',
|
|
||||||
'Welcome!',
|
|
||||||
'<h1>Welcome to 4WDCSA</h1><p>Your account is ready.</p>'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Send admin notification
|
|
||||||
$emailService->sendAdminNotification(
|
|
||||||
'New Booking',
|
|
||||||
'A new booking has been submitted for review.'
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### PaymentService
|
|
||||||
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
use Services\PaymentService;
|
|
||||||
use Services\UserService;
|
|
||||||
|
|
||||||
$paymentService = new PaymentService();
|
|
||||||
$userService = new UserService();
|
|
||||||
|
|
||||||
$user_id = $_SESSION['user_id'];
|
|
||||||
$userInfo = $userService->getUserInfo($user_id, [
|
|
||||||
'first_name',
|
|
||||||
'last_name',
|
|
||||||
'email'
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Generate PayFast payment form
|
|
||||||
$html = $paymentService->processBookingPayment(
|
|
||||||
'PAY-001', // payment_id
|
|
||||||
1500.00, // amount
|
|
||||||
'Trip Booking', // description
|
|
||||||
'https://domain.com/success',
|
|
||||||
'https://domain.com/cancel',
|
|
||||||
'https://domain.com/notify',
|
|
||||||
$userInfo // user details
|
|
||||||
);
|
|
||||||
echo $html; // Outputs form + auto-submit script
|
|
||||||
```
|
|
||||||
|
|
||||||
### DatabaseService
|
|
||||||
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
use Services\DatabaseService;
|
|
||||||
|
|
||||||
// Get the singleton connection
|
|
||||||
$db = DatabaseService::getInstance();
|
|
||||||
$conn = $db->getConnection();
|
|
||||||
|
|
||||||
// Use it like normal MySQLi
|
|
||||||
$result = $conn->query("SELECT * FROM trips");
|
|
||||||
$row = $result->fetch_assoc();
|
|
||||||
|
|
||||||
// Or use convenience methods
|
|
||||||
$stmt = $db->prepare("SELECT * FROM users WHERE user_id = ?");
|
|
||||||
$stmt->bind_param('i', $userId);
|
|
||||||
$stmt->execute();
|
|
||||||
$result = $stmt->get_result();
|
|
||||||
```
|
|
||||||
|
|
||||||
### AuthenticationService
|
|
||||||
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
use Services\AuthenticationService;
|
|
||||||
|
|
||||||
// Generate CSRF token (called automatically in header01.php)
|
|
||||||
$token = AuthenticationService::generateCsrfToken();
|
|
||||||
|
|
||||||
// Validate CSRF token (on form submission)
|
|
||||||
$isValid = AuthenticationService::validateCsrfToken($_POST['csrf_token']);
|
|
||||||
|
|
||||||
// Check if user is logged in
|
|
||||||
if (AuthenticationService::isLoggedIn()) {
|
|
||||||
echo "User is logged in";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regenerate session after login (prevents session fixation)
|
|
||||||
AuthenticationService::regenerateSession();
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding CSRF Tokens to Forms
|
|
||||||
|
|
||||||
All forms should now include CSRF tokens for protection:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<form method="POST" action="process_booking.php">
|
|
||||||
<!-- Add CSRF token as hidden field -->
|
|
||||||
<input type="hidden" name="csrf_token" value="<?php echo AuthenticationService::generateCsrfToken(); ?>">
|
|
||||||
|
|
||||||
<!-- Rest of form -->
|
|
||||||
<input type="text" name="trip_id">
|
|
||||||
<button type="submit">Book Trip</button>
|
|
||||||
</form>
|
|
||||||
```
|
|
||||||
|
|
||||||
Processing the form:
|
|
||||||
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
use Services\AuthenticationService;
|
|
||||||
|
|
||||||
if ($_POST) {
|
|
||||||
// Validate CSRF token
|
|
||||||
if (!AuthenticationService::validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
|
||||||
die("Invalid request. Please try again.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process the form safely
|
|
||||||
$tripId = $_POST['trip_id'];
|
|
||||||
// ... rest of processing ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Migration Checklist for Existing Code
|
|
||||||
|
|
||||||
If you're updating old code to use the new services:
|
|
||||||
|
|
||||||
### Step 1: Replace Database Calls
|
|
||||||
```php
|
|
||||||
// OLD
|
|
||||||
function getUserEmail($user_id) {
|
|
||||||
$conn = openDatabaseConnection();
|
|
||||||
// ... 5 lines of query code ...
|
|
||||||
$conn->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// NEW
|
|
||||||
use Services\UserService;
|
|
||||||
|
|
||||||
$userService = new UserService();
|
|
||||||
$email = $userService->getEmail($user_id);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Replace Email Sends
|
|
||||||
```php
|
|
||||||
// OLD
|
|
||||||
sendVerificationEmail($email, $name, $token);
|
|
||||||
|
|
||||||
// NEW - Still works the same way
|
|
||||||
sendVerificationEmail($email, $name, $token);
|
|
||||||
|
|
||||||
// OR use service directly
|
|
||||||
$emailService = new EmailService();
|
|
||||||
$emailService->sendVerificationEmail($email, $name, $token);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Add CSRF Protection
|
|
||||||
```php
|
|
||||||
// Add to all forms
|
|
||||||
<input type="hidden" name="csrf_token" value="<?php echo AuthenticationService::generateCsrfToken(); ?>">
|
|
||||||
|
|
||||||
// Validate on form processing
|
|
||||||
use Services\AuthenticationService;
|
|
||||||
if (!AuthenticationService::validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
|
||||||
die("Invalid request");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Regenerate Sessions
|
|
||||||
```php
|
|
||||||
// After successful login
|
|
||||||
use Services\AuthenticationService;
|
|
||||||
|
|
||||||
$_SESSION['user_id'] = $userId;
|
|
||||||
AuthenticationService::regenerateSession();
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
The `.env` file must contain all required credentials:
|
|
||||||
|
|
||||||
```
|
|
||||||
# Database
|
|
||||||
DB_HOST=localhost
|
|
||||||
DB_USER=root
|
|
||||||
DB_PASS=password
|
|
||||||
DB_NAME=4wdcsa
|
|
||||||
|
|
||||||
# Mailjet
|
|
||||||
MAILJET_API_KEY=your-key-here
|
|
||||||
MAILJET_API_SECRET=your-secret-here
|
|
||||||
MAILJET_FROM_EMAIL=info@4wdcsa.co.za
|
|
||||||
MAILJET_FROM_NAME=4WDCSA
|
|
||||||
|
|
||||||
# PayFast
|
|
||||||
PAYFAST_MERCHANT_ID=your-merchant-id
|
|
||||||
PAYFAST_MERCHANT_KEY=your-merchant-key
|
|
||||||
PAYFAST_PASSPHRASE=your-passphrase
|
|
||||||
PAYFAST_DOMAIN=www.yourdomain.co.za
|
|
||||||
PAYFAST_TESTING_MODE=true
|
|
||||||
|
|
||||||
# Admin
|
|
||||||
ADMIN_EMAIL=admin@4wdcsa.co.za
|
|
||||||
```
|
|
||||||
|
|
||||||
**IMPORTANT**: `.env` should never be committed to git. Add to `.gitignore`:
|
|
||||||
```
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
.env.*.local
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing Your Changes
|
|
||||||
|
|
||||||
### Quick Test: Database Connection
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
require_once 'env.php';
|
|
||||||
|
|
||||||
use Services\DatabaseService;
|
|
||||||
|
|
||||||
$db = DatabaseService::getInstance();
|
|
||||||
$result = $db->query("SELECT 1");
|
|
||||||
echo $result ? "✓ Database connected" : "✗ Connection failed";
|
|
||||||
```
|
|
||||||
|
|
||||||
### Quick Test: Email Service
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
require_once 'env.php';
|
|
||||||
|
|
||||||
use Services\EmailService;
|
|
||||||
|
|
||||||
$emailService = new EmailService();
|
|
||||||
$success = $emailService->sendVerificationEmail(
|
|
||||||
'test@example.com',
|
|
||||||
'Test User',
|
|
||||||
'test-token'
|
|
||||||
);
|
|
||||||
echo $success ? "✓ Email sent" : "✗ Email failed";
|
|
||||||
```
|
|
||||||
|
|
||||||
### Quick Test: User Service
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
require_once 'env.php';
|
|
||||||
|
|
||||||
use Services\UserService;
|
|
||||||
|
|
||||||
$userService = new UserService();
|
|
||||||
$email = $userService->getEmail(1);
|
|
||||||
echo $email ? "✓ User data retrieved: " . $email : "✗ User not found";
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Issue: "Class not found: Services\UserService"
|
|
||||||
**Solution**: Ensure `env.php` is required at the top of your file:
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
require_once 'env.php'; // Must be first
|
|
||||||
use Services\UserService;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Issue: "CSRF token validation failed"
|
|
||||||
**Solution**: Ensure token is included in form AND validated on submission:
|
|
||||||
```html
|
|
||||||
<!-- In form -->
|
|
||||||
<input type="hidden" name="csrf_token" value="<?php echo AuthenticationService::generateCsrfToken(); ?>">
|
|
||||||
|
|
||||||
<!-- In processor -->
|
|
||||||
if (!AuthenticationService::validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
|
||||||
die("Invalid request");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Issue: "Mailjet credentials not configured"
|
|
||||||
**Solution**: Check that `.env` file has:
|
|
||||||
```
|
|
||||||
MAILJET_API_KEY=...
|
|
||||||
MAILJET_API_SECRET=...
|
|
||||||
```
|
|
||||||
|
|
||||||
And that the file is in the correct location (root of application).
|
|
||||||
|
|
||||||
### Issue: "Database connection failed"
|
|
||||||
**Solution**: Verify `.env` has correct database credentials:
|
|
||||||
```
|
|
||||||
DB_HOST=localhost
|
|
||||||
DB_USER=root
|
|
||||||
DB_PASS=your-password
|
|
||||||
DB_NAME=4wdcsa
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Notes
|
|
||||||
|
|
||||||
### Connection Pooling
|
|
||||||
The old code opened a **new database connection** for each function call. The new `DatabaseService` uses a **singleton pattern** with a single persistent connection:
|
|
||||||
|
|
||||||
- **Before**: 20 functions × 10 page views = 200 connections/sec
|
|
||||||
- **After**: 20 functions × 10 page views = 1 connection/sec
|
|
||||||
- **Improvement**: 200x fewer connection overhead!
|
|
||||||
|
|
||||||
### Query Efficiency
|
|
||||||
The new `UserService.getUserInfo()` method allows fetching multiple fields in one query:
|
|
||||||
|
|
||||||
```php
|
|
||||||
// OLD: 3 database queries
|
|
||||||
$firstName = getFirstName($id); // Query 1
|
|
||||||
$lastName = getLastName($id); // Query 2
|
|
||||||
$email = getEmail($id); // Query 3
|
|
||||||
|
|
||||||
// NEW: 1 database query
|
|
||||||
$data = $userService->getUserInfo($id, ['first_name', 'last_name', 'email']);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
1. **Test everything thoroughly** - no functional changes should be visible to users
|
|
||||||
2. **Update forms** - add CSRF tokens to all POST forms
|
|
||||||
3. **Review logs** - ensure no error logging issues
|
|
||||||
4. **Deploy to staging** - test in staging environment first
|
|
||||||
5. **Deploy to production** - follow your deployment procedure
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
For questions or issues, refer to `REFACTORING_PHASE1.md` for complete technical details.
|
|
||||||
@@ -1,330 +0,0 @@
|
|||||||
# 🎉 Phase 1 Implementation Complete: Service Layer Refactoring
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
Your 4WDCSA membership site has been successfully modernized with **zero functional changes** (100% backward compatible). The refactoring eliminates 59% of code duplication while dramatically improving security, maintainability, and performance.
|
|
||||||
|
|
||||||
**Total work**: ~3 hours
|
|
||||||
**Code eliminated**: 1,750+ lines (59% reduction)
|
|
||||||
**Security improvements**: 7 major security enhancements
|
|
||||||
**Backward compatibility**: 100% (all existing code still works)
|
|
||||||
**Branch**: `feature/site-restructure`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What Changed
|
|
||||||
|
|
||||||
### ✅ Created Service Layer (5 new classes)
|
|
||||||
|
|
||||||
| Service | Purpose | Files Reduced | Lines Saved |
|
|
||||||
|---------|---------|---------------|------------|
|
|
||||||
| **DatabaseService** | Connection pooling singleton | 20+ calls → 1 | ~100 lines |
|
|
||||||
| **EmailService** | Consolidated email sending | 6 functions → 1 | ~160 lines |
|
|
||||||
| **PaymentService** | Consolidated payment processing | 4 functions → 1 | ~200 lines |
|
|
||||||
| **AuthenticationService** | Auth + CSRF + session mgmt | 2 functions → 1 | ~40 lines |
|
|
||||||
| **UserService** | Consolidated user info getters | 6 functions → 1 | ~40 lines |
|
|
||||||
|
|
||||||
### ✅ Enhanced Security
|
|
||||||
|
|
||||||
- ✅ **HTTPS Enforcement**: Automatic HTTP → HTTPS redirect
|
|
||||||
- ✅ **HSTS Headers**: 1-year max-age with preload
|
|
||||||
- ✅ **CSRF Protection**: Token generation & validation
|
|
||||||
- ✅ **Session Security**: HttpOnly, Secure, SameSite cookies
|
|
||||||
- ✅ **Security Headers**: X-Frame-Options, X-XSS-Protection, CSP
|
|
||||||
- ✅ **Credential Management**: Removed hardcoded API keys from source code
|
|
||||||
- ✅ **Error Handling**: No database errors exposed to users
|
|
||||||
|
|
||||||
### ✅ Improved Code Quality
|
|
||||||
|
|
||||||
**Before refactoring:**
|
|
||||||
- functions.php: 1,980 lines
|
|
||||||
- 6 duplicate email functions (240 lines of duplicate code)
|
|
||||||
- 4 duplicate payment functions (300+ lines of duplicate code)
|
|
||||||
- 20+ database connection calls
|
|
||||||
- Hardcoded credentials scattered throughout code
|
|
||||||
- Mixed concerns (business logic + data access + presentation)
|
|
||||||
|
|
||||||
**After refactoring:**
|
|
||||||
- functions.php: 660 lines (67% reduction)
|
|
||||||
- Single EmailService class (all email logic)
|
|
||||||
- Single PaymentService class (all payment logic)
|
|
||||||
- DatabaseService singleton (1 connection, no duplicates)
|
|
||||||
- All credentials in .env file
|
|
||||||
- Clean separation of concerns
|
|
||||||
|
|
||||||
### ✅ Backward Compatibility
|
|
||||||
|
|
||||||
**100% of existing code still works unchanged:**
|
|
||||||
```php
|
|
||||||
// All these still work exactly the same way:
|
|
||||||
getFullName($userId);
|
|
||||||
sendVerificationEmail($email, $name, $token);
|
|
||||||
processPayment($id, $amount, $description);
|
|
||||||
checkAdmin();
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Improvements
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
- **Connection Overhead**: Reduced from 20 connections/request → 1 connection
|
|
||||||
- **Query Efficiency**: Multi-field user lookups now 1 query instead of 3
|
|
||||||
- **Memory Usage**: Reduced through singleton pattern
|
|
||||||
|
|
||||||
### Maintainability
|
|
||||||
- **Cleaner Code**: 59% reduction in lines
|
|
||||||
- **No Duplication**: Single source of truth for each operation
|
|
||||||
- **Better Organization**: Services grouped by responsibility
|
|
||||||
- **Easier Testing**: Services can be unit tested independently
|
|
||||||
|
|
||||||
### Security
|
|
||||||
- **HTTPS Enforced**: Automatic redirects
|
|
||||||
- **CSRF Protected**: All forms can use token validation
|
|
||||||
- **Session Hardened**: Can't access cookies via JavaScript
|
|
||||||
- **Safe Credentials**: API keys in .env, not in source code
|
|
||||||
|
|
||||||
### Developer Experience
|
|
||||||
- **Clear API**: Services have obvious, predictable methods
|
|
||||||
- **Better Documentation**: Inline comments explain each service
|
|
||||||
- **PSR-4 Autoloading**: No more manual `require_once` for new classes
|
|
||||||
- **Future-Ready**: Foundation for additional services/features
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Files Changed
|
|
||||||
|
|
||||||
### New Files (Created)
|
|
||||||
```
|
|
||||||
src/Services/DatabaseService.php (98 lines)
|
|
||||||
src/Services/EmailService.php (163 lines)
|
|
||||||
src/Services/PaymentService.php (240 lines)
|
|
||||||
src/Services/AuthenticationService.php (118 lines)
|
|
||||||
src/Services/UserService.php (168 lines)
|
|
||||||
.env.example (30 lines)
|
|
||||||
REFACTORING_PHASE1.md (350+ lines documentation)
|
|
||||||
MIGRATION_GUIDE.md (400+ lines developer guide)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Modified Files
|
|
||||||
```
|
|
||||||
functions.php (1980 → 660 lines, 67% reduction)
|
|
||||||
header01.php (Added security headers + CSRF)
|
|
||||||
env.php (Added PSR-4 autoloader)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Unchanged Files
|
|
||||||
```
|
|
||||||
connection.php ✓ No changes
|
|
||||||
session.php ✓ No changes
|
|
||||||
index.php ✓ No changes
|
|
||||||
All other files ✓ No changes
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Checklist
|
|
||||||
|
|
||||||
✅ **Credentials**
|
|
||||||
- All API keys moved to .env file
|
|
||||||
- Credentials no longer in source code
|
|
||||||
- .env.example provided as template
|
|
||||||
|
|
||||||
✅ **Session Management**
|
|
||||||
- Session cookies marked HttpOnly (JavaScript can't access)
|
|
||||||
- Secure flag set (HTTPS only)
|
|
||||||
- SameSite=Strict (CSRF protection)
|
|
||||||
- Regeneration method available
|
|
||||||
|
|
||||||
✅ **CSRF Protection**
|
|
||||||
- Token generation implemented
|
|
||||||
- Token validation method available
|
|
||||||
- Can be added to all POST forms
|
|
||||||
|
|
||||||
✅ **HTTPS**
|
|
||||||
- Automatic HTTP → HTTPS redirect
|
|
||||||
- HSTS header (1 year)
|
|
||||||
- Preload directive included
|
|
||||||
|
|
||||||
✅ **Security Headers**
|
|
||||||
- X-Frame-Options (clickjacking prevention)
|
|
||||||
- X-XSS-Protection
|
|
||||||
- X-Content-Type-Options
|
|
||||||
- Referrer-Policy
|
|
||||||
- Permissions-Policy
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How to Use
|
|
||||||
|
|
||||||
### For Current Code
|
|
||||||
Everything continues to work as-is. No changes needed to existing functionality.
|
|
||||||
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
// This all still works:
|
|
||||||
$name = getFullName(123);
|
|
||||||
sendVerificationEmail('user@example.com', 'John', 'token');
|
|
||||||
processPayment('PAY-001', 1500, 'Trip Booking');
|
|
||||||
```
|
|
||||||
|
|
||||||
### For New Code (Recommended)
|
|
||||||
Use the new services directly for cleaner code:
|
|
||||||
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
use Services\UserService;
|
|
||||||
use Services\EmailService;
|
|
||||||
|
|
||||||
$userService = new UserService();
|
|
||||||
$emailService = new EmailService();
|
|
||||||
|
|
||||||
$email = $userService->getEmail(123);
|
|
||||||
$emailService->sendVerificationEmail($email, 'John', 'token');
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment Setup
|
|
||||||
1. Copy `.env.example` to `.env`
|
|
||||||
2. Update `.env` with your actual credentials
|
|
||||||
3. Never commit `.env` to git (add to .gitignore)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next Phases (Coming Soon)
|
|
||||||
|
|
||||||
### Phase 2: Authentication Hardening (Est. 1-2 weeks)
|
|
||||||
- [ ] Add CSRF tokens to all POST forms
|
|
||||||
- [ ] Rate limiting on login/password reset
|
|
||||||
- [ ] Proper password reset flow
|
|
||||||
- [ ] Enhanced logging
|
|
||||||
|
|
||||||
### Phase 3: Business Logic Services (Est. 2-3 weeks)
|
|
||||||
- [ ] BookingService class
|
|
||||||
- [ ] MembershipService class
|
|
||||||
- [ ] Transaction support
|
|
||||||
- [ ] Audit logging
|
|
||||||
|
|
||||||
### Phase 4: Testing & Documentation (Est. 1 week)
|
|
||||||
- [ ] Unit tests for critical paths
|
|
||||||
- [ ] Integration tests
|
|
||||||
- [ ] API documentation
|
|
||||||
- [ ] Performance benchmarks
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Checklist
|
|
||||||
|
|
||||||
Before deploying to production, verify:
|
|
||||||
|
|
||||||
- [ ] Website loads without errors
|
|
||||||
- [ ] User can log in
|
|
||||||
- [ ] Email sending works (check inbox)
|
|
||||||
- [ ] Bookings can be created
|
|
||||||
- [ ] Payments work in test mode
|
|
||||||
- [ ] Admin pages are accessible
|
|
||||||
- [ ] HTTPS redirect works (try http://...)
|
|
||||||
- [ ] No security header warnings
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
Two comprehensive guides have been created:
|
|
||||||
|
|
||||||
1. **REFACTORING_PHASE1.md** - Technical implementation details
|
|
||||||
- Complete list of all changes
|
|
||||||
- Code reduction summary
|
|
||||||
- Service architecture overview
|
|
||||||
- Security improvements documented
|
|
||||||
- Validation checklist
|
|
||||||
|
|
||||||
2. **MIGRATION_GUIDE.md** - Developer guide
|
|
||||||
- How to use each service
|
|
||||||
- Code examples for all services
|
|
||||||
- Adding CSRF tokens to forms
|
|
||||||
- Environment configuration
|
|
||||||
- Troubleshooting guide
|
|
||||||
- Performance notes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Commit Information
|
|
||||||
|
|
||||||
**Branch:** `feature/site-restructure`
|
|
||||||
**Commits:** 2 commits
|
|
||||||
- Commit 1: Service layer refactoring + modernized functions.php
|
|
||||||
- Commit 2: Documentation files
|
|
||||||
|
|
||||||
**How to view changes:**
|
|
||||||
```bash
|
|
||||||
git log --oneline -n 2
|
|
||||||
git diff HEAD~2..HEAD # View all changes
|
|
||||||
git show <commit-hash> # View specific commit
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
### Immediate (This Week)
|
|
||||||
1. Review REFACTORING_PHASE1.md for technical details
|
|
||||||
2. Review MIGRATION_GUIDE.md for developer usage
|
|
||||||
3. Test thoroughly in development environment
|
|
||||||
4. Verify email and payment processing still work
|
|
||||||
5. Merge to main branch when satisfied
|
|
||||||
|
|
||||||
### Short Term (Next Week)
|
|
||||||
1. Add CSRF tokens to all POST forms
|
|
||||||
2. Add rate limiting to authentication endpoints
|
|
||||||
3. Implement proper password reset flow
|
|
||||||
4. Add comprehensive logging
|
|
||||||
|
|
||||||
### Medium Term (2-4 Weeks)
|
|
||||||
1. Continue with Phase 2-4 services
|
|
||||||
2. Add unit tests
|
|
||||||
3. Add integration tests
|
|
||||||
4. Performance optimization
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Questions?
|
|
||||||
|
|
||||||
If you have any questions about the refactoring:
|
|
||||||
|
|
||||||
1. **Architecture questions** → See `REFACTORING_PHASE1.md`
|
|
||||||
2. **Implementation questions** → See `MIGRATION_GUIDE.md`
|
|
||||||
3. **Code examples** → See `MIGRATION_GUIDE.md` - Specific Service Usage section
|
|
||||||
4. **Troubleshooting** → See `MIGRATION_GUIDE.md` - Troubleshooting section
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary Statistics
|
|
||||||
|
|
||||||
| Metric | Value |
|
|
||||||
|--------|-------|
|
|
||||||
| **Total Lines Eliminated** | 1,750+ |
|
|
||||||
| **Code Reduction** | 59% |
|
|
||||||
| **Functions Consolidated** | 23 |
|
|
||||||
| **Duplicate Code Removed** | 100% |
|
|
||||||
| **Security Enhancements** | 7 major |
|
|
||||||
| **New Service Classes** | 5 |
|
|
||||||
| **Backward Compatibility** | 100% |
|
|
||||||
| **Lint Errors** | 0 |
|
|
||||||
| **Breaking Changes** | 0 |
|
|
||||||
| **Performance Improvement** | 200x (connections) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Your Site Is Now
|
|
||||||
|
|
||||||
✅ **More Secure** - HTTPS, CSRF, hardened sessions, no exposed credentials
|
|
||||||
✅ **Better Organized** - Clear service layer architecture
|
|
||||||
✅ **More Maintainable** - 59% less code, no duplication
|
|
||||||
✅ **Faster** - Single database connection, optimized queries
|
|
||||||
✅ **Production Ready** - For a 200-user club
|
|
||||||
✅ **Well Documented** - Complete guides for developers
|
|
||||||
✅ **Future Ready** - Foundation for continued improvements
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Phase 1 is complete. Ready for Phase 2 whenever you are!** 🚀
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
# Phase 1 Implementation Complete: Service Layer Refactoring
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
Successfully refactored the 4WDCSA membership site from a monolithic procedural structure to a modular service-oriented architecture. **Zero functional changes** - all backward compatible while eliminating 59% code duplication.
|
|
||||||
|
|
||||||
## What Was Done
|
|
||||||
|
|
||||||
### 1. Created Service Layer Architecture
|
|
||||||
Converted scattered procedural code into organized service classes:
|
|
||||||
|
|
||||||
#### **DatabaseService** (`src/Services/DatabaseService.php`)
|
|
||||||
- Singleton pattern for connection pooling
|
|
||||||
- Eliminates 20+ `openDatabaseConnection()` calls
|
|
||||||
- Single reusable MySQLi connection
|
|
||||||
- Methods: `getConnection()`, `query()`, `prepare()`, `beginTransaction()`, `commit()`, `rollback()`
|
|
||||||
|
|
||||||
#### **EmailService** (`src/Services/EmailService.php`)
|
|
||||||
- Consolidates 6 duplicate email functions into 1 reusable service
|
|
||||||
- **Reduction: 240 lines → 80 lines (67% reduction)**
|
|
||||||
- Methods: `sendVerificationEmail()`, `sendInvoice()`, `sendPOP()`, `sendAdminNotification()`, `sendPaymentConfirmation()`, `sendTemplate()`, `sendCustom()`
|
|
||||||
- Removed hardcoded Mailjet credentials from source code
|
|
||||||
|
|
||||||
#### **PaymentService** (`src/Services/PaymentService.php`)
|
|
||||||
- Consolidates `processPayment()`, `processMembershipPayment()`, `processPaymentTest()`, `processZeroPayment()`
|
|
||||||
- **Reduction: 300+ lines → 100 lines (67% reduction)**
|
|
||||||
- Extracted `generatePayFastSignature()` method to eliminate nested function definitions
|
|
||||||
- Methods: `processBookingPayment()`, `processMembershipPayment()`, `processTestPayment()`, `processZeroPayment()`
|
|
||||||
- Removed hardcoded PayFast credentials from source code
|
|
||||||
|
|
||||||
#### **AuthenticationService** (`src/Services/AuthenticationService.php`)
|
|
||||||
- Consolidates `checkAdmin()` and `checkSuperAdmin()` (50% duplication eliminated)
|
|
||||||
- **Reduction: 80 lines → 40 lines (50% reduction)**
|
|
||||||
- Added CSRF token generation: `generateCsrfToken()`, `validateCsrfToken()`
|
|
||||||
- Added session regeneration: `regenerateSession()` (prevents session fixation attacks)
|
|
||||||
- Methods: `requireAdmin()`, `requireSuperAdmin()`, `isLoggedIn()`, `getUserRole()`, `logout()`
|
|
||||||
|
|
||||||
#### **UserService** (`src/Services/UserService.php`)
|
|
||||||
- Consolidates 6 nearly-identical user info getters: `getFullName()`, `getEmail()`, `getProfilePic()`, `getLastName()`, `getInitialSurname()`, `get_user_info()`
|
|
||||||
- **Reduction: 54 lines → 15 lines (72% reduction)**
|
|
||||||
- Generic `getUserColumn()` method prevents duplication
|
|
||||||
- Methods: `getFullName()`, `getFirstName()`, `getLastName()`, `getEmail()`, `getProfilePic()`, `getInitialSurname()`, `getUserInfo()`, `userExists()`
|
|
||||||
|
|
||||||
### 2. Enhanced Security
|
|
||||||
|
|
||||||
#### Added to `header01.php`:
|
|
||||||
- **HTTPS Enforcement**: Automatic redirect from HTTP to HTTPS
|
|
||||||
- **Security Headers**:
|
|
||||||
- `Strict-Transport-Security`: 1-year HSTS max-age + preload
|
|
||||||
- `X-Content-Type-Options: nosniff` (prevent MIME sniffing)
|
|
||||||
- `X-Frame-Options: SAMEORIGIN` (clickjacking prevention)
|
|
||||||
- `X-XSS-Protection: 1; mode=block` (XSS protection)
|
|
||||||
- `Referrer-Policy: strict-origin-when-cross-origin`
|
|
||||||
- `Permissions-Policy` (geolocation, microphone, camera denial)
|
|
||||||
|
|
||||||
#### Session Security:
|
|
||||||
- `session.cookie_httponly = 1` (JavaScript cannot access cookies)
|
|
||||||
- `session.cookie_secure = 1` (HTTPS only)
|
|
||||||
- `session.cookie_samesite = Strict` (CSRF protection)
|
|
||||||
- CSRF token generation on every page load
|
|
||||||
|
|
||||||
### 3. Modernized functions.php
|
|
||||||
- **Original: 1980 lines** → **New: 660 lines (59% reduction)**
|
|
||||||
- All 6 duplicate email functions → single wrapper
|
|
||||||
- All payment processing functions → single wrapper
|
|
||||||
- All user info functions → single wrapper
|
|
||||||
- Maintains 100% backward compatibility
|
|
||||||
- Clear function organization with commented sections
|
|
||||||
- Proper error handling and logging throughout
|
|
||||||
|
|
||||||
### 4. Credential Management
|
|
||||||
|
|
||||||
#### Created `.env.example`:
|
|
||||||
All credentials now template-based:
|
|
||||||
```
|
|
||||||
MAILJET_API_KEY=your-key-here
|
|
||||||
MAILJET_API_SECRET=your-secret-here
|
|
||||||
PAYFAST_MERCHANT_ID=your-merchant-id
|
|
||||||
PAYFAST_MERCHANT_KEY=your-key
|
|
||||||
PAYFAST_PASSPHRASE=your-passphrase
|
|
||||||
ADMIN_EMAIL=admin@4wdcsa.co.za
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Removed from source code:
|
|
||||||
- ✅ Mailjet API key: `1a44f8d5e847537dbb8d3c76fe73a93c` (was in 6 places)
|
|
||||||
- ✅ Mailjet API secret: `ec98b45c53a7694c4f30d09eee9ad280` (was in 6 places)
|
|
||||||
- ✅ PayFast merchant ID: `10021495` (was in 3 places)
|
|
||||||
- ✅ PayFast merchant key: `yzpdydo934j92` (was in 3 places)
|
|
||||||
- ✅ PayFast passphrase: `SheSells7Shells` (was in 3 places)
|
|
||||||
|
|
||||||
### 5. PSR-4 Autoloader
|
|
||||||
Added to `env.php`:
|
|
||||||
```php
|
|
||||||
spl_autoload_register(function ($class) {
|
|
||||||
// Automatically loads Services\*, Controllers\*, Middleware\* classes
|
|
||||||
});
|
|
||||||
```
|
|
||||||
No need for manual `require_once` statements for new classes.
|
|
||||||
|
|
||||||
### 6. Directory Structure
|
|
||||||
```
|
|
||||||
4WDCSA.co.za/
|
|
||||||
├── src/
|
|
||||||
│ ├── Services/
|
|
||||||
│ │ ├── DatabaseService.php
|
|
||||||
│ │ ├── EmailService.php
|
|
||||||
│ │ ├── PaymentService.php
|
|
||||||
│ │ ├── AuthenticationService.php
|
|
||||||
│ │ └── UserService.php
|
|
||||||
│ ├── Controllers/ (Ready for future use)
|
|
||||||
│ └── Middleware/ (Ready for future use)
|
|
||||||
├── config/ (Ready for future use)
|
|
||||||
├── .env.example
|
|
||||||
└── functions.php (Modernized)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Code Reduction Summary
|
|
||||||
|
|
||||||
| Component | Before | After | Reduction |
|
|
||||||
|-----------|--------|-------|-----------|
|
|
||||||
| Email Functions | 240 lines | 80 lines | 67% ↓ |
|
|
||||||
| Payment Functions | 300+ lines | 100 lines | 67% ↓ |
|
|
||||||
| Auth Checks | 80 lines | 40 lines | 50% ↓ |
|
|
||||||
| User Info Getters | 54 lines | 15 lines | 72% ↓ |
|
|
||||||
| functions.php | 1980 lines | 660 lines | 59% ↓ |
|
|
||||||
| **TOTAL** | **~2650 lines** | **~895 lines** | **~59% reduction** |
|
|
||||||
|
|
||||||
## Backward Compatibility
|
|
||||||
✅ **100% backward compatible**
|
|
||||||
- All old function names still work
|
|
||||||
- Old code continues to function unchanged
|
|
||||||
- Services used internally via wrappers
|
|
||||||
- Zero breaking changes
|
|
||||||
|
|
||||||
## Security Improvements Implemented
|
|
||||||
✅ HTTPS enforcement
|
|
||||||
✅ HSTS headers
|
|
||||||
✅ Session cookie security (HttpOnly, Secure, SameSite)
|
|
||||||
✅ CSRF token generation
|
|
||||||
✅ Credentials removed from source code
|
|
||||||
✅ Better error handling (no DB errors to users)
|
|
||||||
|
|
||||||
## Next Steps (Phase 2-4)
|
|
||||||
|
|
||||||
### Phase 2: Authentication & Authorization (1-2 weeks)
|
|
||||||
- [ ] Add CSRF token validation to all POST forms
|
|
||||||
- [ ] Implement rate limiting on login/password reset endpoints
|
|
||||||
- [ ] Add session regeneration on login
|
|
||||||
- [ ] Implement proper password reset flow
|
|
||||||
- [ ] Add 2FA support (optional)
|
|
||||||
|
|
||||||
### Phase 3: Booking & Payment (1-2 weeks)
|
|
||||||
- [ ] Create BookingService class
|
|
||||||
- [ ] Create MembershipService class
|
|
||||||
- [ ] Add transaction support for payment processing
|
|
||||||
- [ ] Add audit logging for sensitive operations
|
|
||||||
- [ ] Implement idempotent payment handling
|
|
||||||
|
|
||||||
### Phase 4: Testing & Documentation (1 week)
|
|
||||||
- [ ] Add unit tests for critical paths (payments, auth, bookings)
|
|
||||||
- [ ] Add integration tests
|
|
||||||
- [ ] API documentation
|
|
||||||
- [ ] Service class documentation
|
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
Ensure your `.env` file includes all keys from `.env.example`:
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
# Edit .env and add your actual credentials
|
|
||||||
```
|
|
||||||
|
|
||||||
### Git Credentials Safety
|
|
||||||
**The `.env` file should NEVER be committed to git.**
|
|
||||||
|
|
||||||
Ensure `.gitignore` includes:
|
|
||||||
```
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
.env.*.local
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing Checklist
|
|
||||||
Before deployment to production:
|
|
||||||
- [ ] Test user login flow
|
|
||||||
- [ ] Test email sending (verification, booking confirmation)
|
|
||||||
- [ ] Test payment processing (test mode)
|
|
||||||
- [ ] Test membership application
|
|
||||||
- [ ] Test password reset
|
|
||||||
- [ ] Test admin pages (if applicable)
|
|
||||||
- [ ] Verify HTTPS redirect works
|
|
||||||
- [ ] Check security headers with online tool
|
|
||||||
|
|
||||||
## Files Changed
|
|
||||||
|
|
||||||
### New Files Created:
|
|
||||||
- `src/Services/DatabaseService.php`
|
|
||||||
- `src/Services/EmailService.php`
|
|
||||||
- `src/Services/PaymentService.php`
|
|
||||||
- `src/Services/AuthenticationService.php`
|
|
||||||
- `src/Services/UserService.php`
|
|
||||||
- `.env.example`
|
|
||||||
|
|
||||||
### Modified Files:
|
|
||||||
- `functions.php` (completely refactored, 59% reduction)
|
|
||||||
- `header01.php` (added security headers and CSRF)
|
|
||||||
- `env.php` (added PSR-4 autoloader)
|
|
||||||
|
|
||||||
### Preserved:
|
|
||||||
- `connection.php` (unchanged)
|
|
||||||
- `session.php` (unchanged)
|
|
||||||
- All other application files (unchanged)
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
✅ No lint errors in any PHP files
|
|
||||||
✅ All functions backward compatible
|
|
||||||
✅ Services properly namespaced
|
|
||||||
✅ Autoloader functional
|
|
||||||
✅ Git committed successfully
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Questions or Issues?
|
|
||||||
|
|
||||||
If you encounter any issues:
|
|
||||||
1. Check browser console for JavaScript errors
|
|
||||||
2. Check PHP error log for backend errors
|
|
||||||
3. Verify `.env` file has all required credentials
|
|
||||||
4. Verify session.php and connection.php are unchanged
|
|
||||||
5. Test with a fresh browser session (new incognito window)
|
|
||||||
|
|
||||||
The refactoring is complete and ready for Phase 2 work on authentication hardening.
|
|
||||||
@@ -1,14 +1,7 @@
|
|||||||
<?php include_once('connection.php');
|
<?php include_once('connection.php');
|
||||||
include_once('functions.php');
|
include_once('functions.php');
|
||||||
require_once("env.php");
|
require_once("env.php");
|
||||||
|
|
||||||
use Middleware\CsrfMiddleware;
|
|
||||||
|
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
// Validate CSRF token
|
|
||||||
CsrfMiddleware::requireToken($_POST);
|
|
||||||
|
|
||||||
$user_id = $_SESSION['user_id']; // assuming you're storing it like this
|
$user_id = $_SESSION['user_id']; // assuming you're storing it like this
|
||||||
|
|
||||||
// campsites.php
|
// campsites.php
|
||||||
|
|||||||
144
admin_blogs.php
Normal file
144
admin_blogs.php
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
<?php include_once('header02.php');
|
||||||
|
checkAdmin();
|
||||||
|
checkUserSession();
|
||||||
|
|
||||||
|
$result = $conn->prepare("
|
||||||
|
SELECT
|
||||||
|
b.blog_id,
|
||||||
|
b.title,
|
||||||
|
b.description,
|
||||||
|
b.status,
|
||||||
|
b.date,
|
||||||
|
b.image,
|
||||||
|
CONCAT(u.first_name, ' ', u.last_name) AS author_name,
|
||||||
|
u.email AS author_email,
|
||||||
|
u.profile_pic
|
||||||
|
FROM blogs b
|
||||||
|
JOIN users u ON b.author = u.user_id
|
||||||
|
WHERE b.status != 'deleted'
|
||||||
|
ORDER BY b.date DESC
|
||||||
|
");
|
||||||
|
|
||||||
|
$result->execute();
|
||||||
|
$posts = $result->get_result();
|
||||||
|
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.image {
|
||||||
|
width: 400px;
|
||||||
|
/* Set your desired width */
|
||||||
|
height: 350px;
|
||||||
|
/* Set your desired height */
|
||||||
|
overflow: hidden;
|
||||||
|
/* Hide any overflow */
|
||||||
|
display: block;
|
||||||
|
/* Ensure proper block behavior */
|
||||||
|
}
|
||||||
|
|
||||||
|
.image img {
|
||||||
|
width: 100%;
|
||||||
|
/* Image scales to fill the container */
|
||||||
|
height: 100%;
|
||||||
|
/* Image scales to fill the container */
|
||||||
|
object-fit: cover;
|
||||||
|
/* Fills the container while maintaining aspect ratio */
|
||||||
|
object-position: top;
|
||||||
|
/* Aligns the top of the image with the top of the container */
|
||||||
|
display: block;
|
||||||
|
/* Prevents inline whitespace issues */
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$bannerFolder = 'assets/images/banners/';
|
||||||
|
$bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
||||||
|
|
||||||
|
$randomBanner = 'assets/images/base4/camping.jpg'; // default fallback
|
||||||
|
if (!empty($bannerImages)) {
|
||||||
|
$randomBanner = $bannerImages[array_rand($bannerImages)];
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<section class="page-banner-area pt-50 pb-35 rel z-1 bgs-cover" style="background-image: url('<?php echo $randomBanner; ?>');">
|
||||||
|
<!-- Overlay PNG -->
|
||||||
|
<div class="banner-overlay"></div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="banner-inner text-white">
|
||||||
|
<h2 class="page-title mb-10" data-aos="fade-left" data-aos-duration="1500" data-aos-offset="50">Admin Blogs</h2>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb justify-content-center mb-20" data-aos="fade-right" data-aos-delay="200" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<li class="breadcrumb-item"><a href="index.php">Home</a></li>
|
||||||
|
<li class="breadcrumb-item active">Admin Blogs</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Page Banner End -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Blog List Area start -->
|
||||||
|
<section class="blog-list-page py-100 rel z-1">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
|
||||||
|
<h2>My Posts</h2>
|
||||||
|
<?php if (isset($_SESSION['message'])): ?>
|
||||||
|
<div class="alert alert-warning message-box">
|
||||||
|
<?php echo $_SESSION['message']; ?>
|
||||||
|
<span class="close-btn" onclick="this.parentElement.style.display='none'">×</span>
|
||||||
|
</div>
|
||||||
|
<?php unset($_SESSION['message']); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<a href="blog_create.php">+ New Post</a>
|
||||||
|
|
||||||
|
<?php while ($post = $posts->fetch_assoc()):
|
||||||
|
// Output the HTML structure with dynamic data
|
||||||
|
echo '
|
||||||
|
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="image" style="width:200px;height:200px;">
|
||||||
|
<img src="' . $post["image"] . '" alt="' . $post["title"] . '">
|
||||||
|
</div>
|
||||||
|
<div class="content" style="width:100%;">
|
||||||
|
<div class="destination-header d-flex align-items-start gap-3">
|
||||||
|
<img src="' . $post["profile_pic"] . '" alt="Author" class="rounded-circle border" width="80" height="80">
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-dark mb-1">' . strtoupper($post["status"]) . '</span>
|
||||||
|
<h5 class="mb-0">' . $post["title"] . '</h5>
|
||||||
|
<small class="text-muted">' . $post["author_name"] . '</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>' . $post["description"] . '</p>
|
||||||
|
<div class="destination-footer">
|
||||||
|
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px; margin-top:10px;">
|
||||||
|
<a href="blog_edit.php?token='.encryptData($post["blog_id"], $salt).'" class="btn btn-sm" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><i class="bi bi-pencil"></i></a>
|
||||||
|
<a href="blog_read.php?token='.encryptData($post["blog_id"], $salt).'" class="btn btn-sm" data-bs-toggle="tooltip" data-bs-placement="top" title="Preview"><i class="bi bi-eye"></i></a>
|
||||||
|
<a href="blog_delete.php?token='.encryptData($post["blog_id"], $salt).'" class="btn btn-sm" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete"><i class="bi bi-trash"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
';
|
||||||
|
endwhile; ?>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Blog List Area end -->
|
||||||
|
<script>
|
||||||
|
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||||
|
tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<?php include_once("insta_footer.php"); ?>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
checkAdmin();
|
checkAdmin();
|
||||||
|
|
||||||
// Fetch all trips
|
// Fetch all trips
|
||||||
$courseSql = "SELECT date, course_id, course_type FROM courses";
|
$courseSql = "SELECT date, course_id, course_type FROM courses WHERE DATE(date) >= CURDATE()";
|
||||||
|
|
||||||
$courseResult = $conn->query($courseSql);
|
$courseResult = $conn->query($courseSql);
|
||||||
if (!$courseResult) {
|
if (!$courseResult) {
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 49 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 340 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 374 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 482 KiB |
101
autosave.php
Normal file
101
autosave.php
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
require_once("env.php");
|
||||||
|
require_once("session.php");
|
||||||
|
require_once("connection.php");
|
||||||
|
require_once("functions.php");
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo "Not authorized";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$article_id = (int)($_POST['id'] ?? 0);
|
||||||
|
$title = $_POST['title'] ?? '';
|
||||||
|
$content = $_POST['content'] ?? '';
|
||||||
|
$description = $_POST['subtitle'] ?? '';
|
||||||
|
$category = $_POST['category'] ?? '';
|
||||||
|
$user_id = $_SESSION['user_id'];
|
||||||
|
|
||||||
|
|
||||||
|
// Default to current user
|
||||||
|
$author_id = $_SESSION['user_id'];
|
||||||
|
|
||||||
|
// Allow override if admin
|
||||||
|
$role = getUserRole();
|
||||||
|
if (($role === 'admin' || $role === 'superadmin') && isset($_POST['author'])) {
|
||||||
|
$author_id = (int)$_POST['author'];
|
||||||
|
}
|
||||||
|
echo $author_id;
|
||||||
|
|
||||||
|
$cover_image_path = null;
|
||||||
|
|
||||||
|
// Only attempt upload if a file was submitted
|
||||||
|
if (!empty($_FILES['cover_image']['name'])) {
|
||||||
|
$uploadDir = __DIR__ . "/uploads/blogs/".$article_id."/images/";
|
||||||
|
if (!is_dir($uploadDir)) {
|
||||||
|
mkdir($uploadDir, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize and rename file
|
||||||
|
$originalName = basename($_FILES['cover_image']['name']);
|
||||||
|
$originalName = preg_replace("/[^a-zA-Z0-9\._-]/", "_", $originalName); // remove unsafe characters
|
||||||
|
|
||||||
|
$targetPath = $uploadDir . $originalName;
|
||||||
|
$publicPath = "/uploads/blogs/".$article_id."/images/" . $originalName;
|
||||||
|
|
||||||
|
// Error detection before upload
|
||||||
|
$fileError = $_FILES['cover_image']['error'];
|
||||||
|
if ($fileError !== UPLOAD_ERR_OK) {
|
||||||
|
$errorMessages = [
|
||||||
|
UPLOAD_ERR_INI_SIZE => 'The uploaded file exceeds the upload_max_filesize directive in php.ini.',
|
||||||
|
UPLOAD_ERR_FORM_SIZE => 'The uploaded file exceeds the MAX_FILE_SIZE directive in the HTML form.',
|
||||||
|
UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially uploaded.',
|
||||||
|
UPLOAD_ERR_NO_FILE => 'No file was uploaded.',
|
||||||
|
UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder.',
|
||||||
|
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.',
|
||||||
|
UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the upload.',
|
||||||
|
];
|
||||||
|
$errorMessage = $errorMessages[$fileError] ?? 'Unknown upload error.';
|
||||||
|
http_response_code(500);
|
||||||
|
echo "Upload error: $errorMessage";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip upload if identical file already exists
|
||||||
|
if (file_exists($targetPath)) {
|
||||||
|
$cover_image_path = $publicPath;
|
||||||
|
} else {
|
||||||
|
if (move_uploaded_file($_FILES['cover_image']['tmp_name'], $targetPath)) {
|
||||||
|
$cover_image_path = $publicPath;
|
||||||
|
} else {
|
||||||
|
http_response_code(500);
|
||||||
|
echo "Failed to move uploaded file.";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare SQL with/without image update
|
||||||
|
if ($cover_image_path) {
|
||||||
|
$stmt = $conn->prepare("
|
||||||
|
UPDATE blogs
|
||||||
|
SET title = ?, content = ?, description = ?, category = ?, image = ?, author = ?
|
||||||
|
WHERE blog_id = ?
|
||||||
|
");
|
||||||
|
$stmt->bind_param("ssssssi", $title, $content, $description, $category, $cover_image_path, $author_id, $article_id);
|
||||||
|
} else {
|
||||||
|
$stmt = $conn->prepare("
|
||||||
|
UPDATE blogs
|
||||||
|
SET title = ?, content = ?, description = ?, category = ?, author = ?
|
||||||
|
WHERE blog_id = ?
|
||||||
|
");
|
||||||
|
$stmt->bind_param("ssssii", $title, $content, $description, $category, $author_id, $article_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo "Saved";
|
||||||
|
} else {
|
||||||
|
http_response_code(500);
|
||||||
|
echo "Database update failed: " . $stmt->error;
|
||||||
|
}
|
||||||
6
blog.php
6
blog.php
@@ -86,7 +86,7 @@ if (!empty($bannerImages)) {
|
|||||||
$icon = "fa-lock";
|
$icon = "fa-lock";
|
||||||
} else {
|
} else {
|
||||||
if (getUserMemberStatus($_SESSION['user_id'])) {
|
if (getUserMemberStatus($_SESSION['user_id'])) {
|
||||||
$blog_link = $row['link'];
|
$blog_link = "blog_read.php?token=".encryptData($blog_id, $salt);
|
||||||
$button_hover = "Read More";
|
$button_hover = "Read More";
|
||||||
$icon = "fa-arrow-right";
|
$icon = "fa-arrow-right";
|
||||||
} else {
|
} else {
|
||||||
@@ -96,7 +96,7 @@ if (!empty($bannerImages)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$blog_link = $row['link'];
|
$blog_link = "blog_read.php?token=".encryptData($blog_id, $salt);
|
||||||
$button_hover = "Read More";
|
$button_hover = "Read More";
|
||||||
$icon = "fa-arrow-right";
|
$icon = "fa-arrow-right";
|
||||||
}
|
}
|
||||||
@@ -105,7 +105,7 @@ if (!empty($bannerImages)) {
|
|||||||
echo '
|
echo '
|
||||||
<div class="blog-item style-three" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
<div class="blog-item style-three" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
<div class="image">
|
<div class="image">
|
||||||
<img style="border-radius:20px;" src="assets/images/blog/' . $blog_id . '/' . $image . '" alt="Blog List">
|
<img style="border-radius:20px;" src="' . $image . '" alt="Blog List">
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<a href="blog.php" class="category">' . $category . '</a>
|
<a href="blog.php" class="category">' . $category . '</a>
|
||||||
|
|||||||
33
blog_create.php
Normal file
33
blog_create.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
// session_start();
|
||||||
|
require_once("env.php");
|
||||||
|
require_once("session.php");
|
||||||
|
require_once("connection.php");
|
||||||
|
require_once("functions.php");
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
die("Not logged in");
|
||||||
|
}
|
||||||
|
$user_id = $_SESSION['user_id'];
|
||||||
|
$role = getUserRole();
|
||||||
|
|
||||||
|
if(!getUserMemberStatus($user_id)){
|
||||||
|
if ($role === 'user'){
|
||||||
|
$_SESSION['message'] = "Blogs only available to active members. Please contact info@4wdcsa.co.za for more information.";
|
||||||
|
header("Location: user_blogs.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$date = date('Y-m-d');
|
||||||
|
$status = 'draft';
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("INSERT INTO blogs (author, title, category, description, content, date, status)
|
||||||
|
VALUES (?, '', '', '', '', ?, ?)");
|
||||||
|
$stmt->bind_param("iss", $user_id, $date, $status);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
$blog_id = $stmt->insert_id;
|
||||||
|
header("Location: blog_edit.php?token=" . encryptData($blog_id, $salt));
|
||||||
|
exit;
|
||||||
36
blog_delete.php
Normal file
36
blog_delete.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
require_once("env.php");
|
||||||
|
require_once("session.php");
|
||||||
|
require_once("connection.php");
|
||||||
|
require_once("functions.php");
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
$_SESSION['message'] = "Not authorized.";
|
||||||
|
header("Location: user_blogs.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $_GET['token'];
|
||||||
|
// Sanitize the trip_id to prevent SQL injection
|
||||||
|
$article_id = intval(decryptData($token, $salt)); // Ensures $trip_id is treated as an integer
|
||||||
|
|
||||||
|
$user_id = $_SESSION['user_id'];
|
||||||
|
|
||||||
|
if ($article_id <= 0) {
|
||||||
|
$_SESSION['message'] = "Invalid blog ID.";
|
||||||
|
header("Location: user_blogs.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("UPDATE blogs SET status = 'deleted' WHERE blog_id = ? AND author = ?");
|
||||||
|
$stmt->bind_param("ii", $article_id, $user_id);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
$_SESSION['message'] = "Blog deleted!";
|
||||||
|
} else {
|
||||||
|
$_SESSION['message'] = "Failed to delete blog: " . $stmt->error;
|
||||||
|
}
|
||||||
|
|
||||||
|
header("Location: user_blogs.php");
|
||||||
|
exit;
|
||||||
|
?>
|
||||||
265
blog_edit.php
Normal file
265
blog_edit.php
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
<?php
|
||||||
|
include_once('header02.php');
|
||||||
|
|
||||||
|
// Ensure the user is logged in
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
die("User not logged in.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $_GET['token'];
|
||||||
|
// Sanitize the trip_id to prevent SQL injection
|
||||||
|
$blog_id = intval(decryptData($token, $salt)); // Ensures $trip_id is treated as an integer
|
||||||
|
|
||||||
|
$user_id = $_SESSION['user_id'];
|
||||||
|
$role = getUserRole();
|
||||||
|
|
||||||
|
// Fetch article info
|
||||||
|
$stmt = $conn->prepare("SELECT * FROM blogs WHERE blog_id = ?");
|
||||||
|
$stmt->bind_param("i", $blog_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
if ($result->num_rows === 0) {
|
||||||
|
die("Blog post not found.");
|
||||||
|
}
|
||||||
|
$article = $result->fetch_assoc();
|
||||||
|
$stmt->close();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<script src="https://cdn.tiny.cloud/1/o6xuedbd9z22xk0p5zszinevn4bdbljxnfwn0tjjvv6r37pb/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
|
||||||
|
<script>
|
||||||
|
tinymce.init({
|
||||||
|
selector: '#content',
|
||||||
|
plugins: 'image code link',
|
||||||
|
toolbar: 'undo redo | blocks | bold italic | alignleft aligncenter alignright | code | image | link',
|
||||||
|
images_upload_url: 'upload.php?blog_id=<?= $blog_id ?>',
|
||||||
|
image_class_list: [
|
||||||
|
{ title: 'None', value: '' },
|
||||||
|
{ title: 'Left Align', value: 'img-left' },
|
||||||
|
{ title: 'Right Align', value: 'img-right' },
|
||||||
|
{ title: 'Rounded', value: 'img-rounded' }
|
||||||
|
],
|
||||||
|
automatic_uploads: true,
|
||||||
|
images_upload_credentials: true, // include cookies if needed
|
||||||
|
content_style: "body { font-family:Helvetica,Arial,sans-serif; font-size:14px }",
|
||||||
|
|
||||||
|
setup: function (editor) {
|
||||||
|
editor.on('init', function () {
|
||||||
|
setTimeout(() => {
|
||||||
|
editor.setContent(`<?= str_replace("`", "\`", addslashes($article['content'])) ?>`);
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="account-settings-area py-70 rel z-1">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<div class="comment-form bgc-lighter z-1 rel mb-55">
|
||||||
|
<form action="submit_blog.php" method="POST" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="article_id" value="<?= htmlspecialchars($blog_id) ?>">
|
||||||
|
<div class="section-title py-20">
|
||||||
|
<h2>Edit Blog</h2>
|
||||||
|
<div id="autosave-status" style="font-style: italic; font-size: 0.9em;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-35">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="title">Blog Title</label>
|
||||||
|
<input type="text" id="title" class="form-control" name="title" placeholder="Title" required value="<?= htmlspecialchars($article['title']) ?>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subtitle">Description</label>
|
||||||
|
<input type="text" id="subtitle" class="form-control" name="subtitle" placeholder="Description" required value="<?= htmlspecialchars($article['description']) ?>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cover_image">Cover Image</label>
|
||||||
|
<input type="file" class="form-control" name="cover_image" id="cover_image" accept="image/*">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 mb-10">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="category">Blog Category</label>
|
||||||
|
<select name="category" class="form-control" id="category" required>
|
||||||
|
<option value="Trip Report" <?= $article['category'] == 'Trip Report' ? 'selected' : '' ?>>Trip Report</option>
|
||||||
|
<option value="Gear Review" <?= $article['category'] == 'Gear Review' ? 'selected' : '' ?>>Gear Review</option>
|
||||||
|
<option value="Talking Dirty" <?= $article['category'] == 'Talking Dirty' ? 'selected' : '' ?>>Talking Dirty</option>
|
||||||
|
<option value="Report" <?= $article['category'] == 'Report' ? 'selected' : '' ?>>Report</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 mb-10">
|
||||||
|
<div class="form-group">
|
||||||
|
<?php if ($role === 'admin' || $role === 'superadmin'): ?>
|
||||||
|
<label for="author">Author:</label>
|
||||||
|
<select class="form-control" name="author" id="author">
|
||||||
|
<?php
|
||||||
|
$user_query = $conn->query("SELECT user_id, CONCAT(first_name, ' ', last_name) AS name FROM users ORDER BY first_name ASC");
|
||||||
|
while ($user = $user_query->fetch_assoc()):
|
||||||
|
?>
|
||||||
|
<option value="<?= $user['user_id'] ?>" <?= $user['user_id'] == $article['author'] ? 'selected' : '' ?>>
|
||||||
|
<?= htmlspecialchars($user['name']) ?>
|
||||||
|
</option>
|
||||||
|
<?php endwhile; ?>
|
||||||
|
</select>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<textarea id="content" name="content"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="button" class="theme-btn style-three" style="width:100%;" id="manualSaveBtn">Save Draft</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<a href="blog_read.php?token=<?php echo encryptData($blog_id, $salt); ?>" class="theme-btn style-three" style="width:100%;">Preview</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<?php
|
||||||
|
if ($article['status'] == 'draft'){
|
||||||
|
echo '<div class="form-group">
|
||||||
|
<button type="button" class="theme-btn style-two" style="width:100%;" id="manualPostBtn">Publish</button>
|
||||||
|
|
||||||
|
</div> ';
|
||||||
|
} else {
|
||||||
|
echo '<div class="form-group">
|
||||||
|
<button type="button" class="theme-btn style-two" style="width:100%;" id="manualDraftBtn">Un-Publish</button>
|
||||||
|
|
||||||
|
</div> ';
|
||||||
|
}?>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||||
|
<script>
|
||||||
|
function autosavePost() {
|
||||||
|
const title = document.querySelector('[name="title"]').value;
|
||||||
|
const content = tinymce.get("content").getContent();
|
||||||
|
const subtitle = document.querySelector('[name="subtitle"]').value;
|
||||||
|
const category = document.querySelector('[name="category"]').value;
|
||||||
|
const author = document.querySelector('[name="author"]').value;
|
||||||
|
const articleId = document.querySelector('[name="article_id"]').value;
|
||||||
|
const coverImageInput = document.querySelector('[name="cover_image"]');
|
||||||
|
|
||||||
|
console.log("Saving: ", { title, subtitle, content, category, articleId, author });
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("id", articleId);
|
||||||
|
formData.append("title", title);
|
||||||
|
formData.append("content", content);
|
||||||
|
formData.append("subtitle", subtitle);
|
||||||
|
formData.append("category", category);
|
||||||
|
formData.append("author", author);
|
||||||
|
|
||||||
|
// Only append image if a new file is selected
|
||||||
|
if (coverImageInput.files.length > 0) {
|
||||||
|
formData.append("cover_image", coverImageInput.files[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch("autosave.php", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData
|
||||||
|
}).then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
document.getElementById("autosave-status").innerText = "Draft autosaved at " + new Date().toLocaleTimeString();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
document.getElementById("autosave-status").innerText = "Autosave failed";
|
||||||
|
console.error("Autosave failed", response.statusText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Autosave error:", err);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger autosave every 15s
|
||||||
|
setInterval(autosavePost, 15000);
|
||||||
|
|
||||||
|
// Manual autosave button
|
||||||
|
document.getElementById("manualSaveBtn").addEventListener("click", autosavePost);
|
||||||
|
|
||||||
|
// Manual publish button
|
||||||
|
document.getElementById("manualPostBtn").addEventListener("click", function () {
|
||||||
|
autosavePost().then(success => {
|
||||||
|
if (!success) return;
|
||||||
|
|
||||||
|
const articleId = document.querySelector('[name="article_id"]').value;
|
||||||
|
const publishData = new FormData();
|
||||||
|
publishData.append("id", articleId);
|
||||||
|
|
||||||
|
fetch("publish_blog.php", {
|
||||||
|
method: "POST",
|
||||||
|
body: publishData
|
||||||
|
}).then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
alert("Post published successfully!");
|
||||||
|
// Optional: redirect to the live post
|
||||||
|
window.location.href = "blog_read.php?token=<?php echo encryptData($blog_id, $salt);?>";
|
||||||
|
} else {
|
||||||
|
alert("Publish failed.");
|
||||||
|
console.error("Publish error:", response.statusText);
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Publish error:", err);
|
||||||
|
alert("Publish failed due to network error.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manual unpublish button
|
||||||
|
document.getElementById("manualDraftBtn").addEventListener("click", function () {
|
||||||
|
autosavePost().then(success => {
|
||||||
|
if (!success) return;
|
||||||
|
|
||||||
|
const articleId = document.querySelector('[name="article_id"]').value;
|
||||||
|
const publishData = new FormData();
|
||||||
|
publishData.append("id", articleId);
|
||||||
|
|
||||||
|
fetch("blog_unpublish.php", {
|
||||||
|
method: "POST",
|
||||||
|
body: publishData
|
||||||
|
}).then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
alert("Post unpublished successfully!");
|
||||||
|
// Optional: redirect to the live post
|
||||||
|
window.location.href = "blog_read.php?token=<?php echo encryptData($blog_id, $salt);?>";
|
||||||
|
} else {
|
||||||
|
alert("unPublish failed.");
|
||||||
|
console.error("Publish error:", response.statusText);
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Publish error:", err);
|
||||||
|
alert("Publish failed due to network error.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<?php include_once("insta_footer.php"); ?>
|
||||||
176
blog_read.php
Normal file
176
blog_read.php
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<?php include_once('header02.php');
|
||||||
|
|
||||||
|
$token = $_GET['token'];
|
||||||
|
// Sanitize the trip_id to prevent SQL injection
|
||||||
|
$blog_id = intval(decryptData($token, $salt)); // Ensures $trip_id is treated as an integer
|
||||||
|
|
||||||
|
|
||||||
|
$page_id = 'blog_'.$blog_id;
|
||||||
|
echo getCommentCount($page_id);
|
||||||
|
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("
|
||||||
|
SELECT a.title, a.category, a.description, a.content, a.date,
|
||||||
|
u.first_name, u.last_name
|
||||||
|
FROM blogs a
|
||||||
|
JOIN users u ON a.author = u.user_id
|
||||||
|
WHERE a.blog_id = ?
|
||||||
|
");
|
||||||
|
$stmt->bind_param("i", $blog_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
if ($result->num_rows === 0) {
|
||||||
|
die("Article not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = $result->fetch_assoc();
|
||||||
|
$author = htmlspecialchars($row['first_name'] . ' ' . $row['last_name']);
|
||||||
|
?>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.image {
|
||||||
|
width: 400px;
|
||||||
|
/* Set your desired width */
|
||||||
|
height: 350px;
|
||||||
|
/* Set your desired height */
|
||||||
|
overflow: hidden;
|
||||||
|
/* Hide any overflow */
|
||||||
|
display: block;
|
||||||
|
/* Ensure proper block behavior */
|
||||||
|
}
|
||||||
|
|
||||||
|
.image img {
|
||||||
|
width: 100%;
|
||||||
|
/* Image scales to fill the container */
|
||||||
|
height: 100%;
|
||||||
|
/* Image scales to fill the container */
|
||||||
|
object-fit: cover;
|
||||||
|
/* Fills the container while maintaining aspect ratio */
|
||||||
|
object-position: top;
|
||||||
|
/* Aligns the top of the image with the top of the container */
|
||||||
|
display: block;
|
||||||
|
/* Prevents inline whitespace issues */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
/* font-family: Arial, sans-serif; */
|
||||||
|
line-height: 1.6;
|
||||||
|
/* max-width: 800px; */
|
||||||
|
margin: auto;
|
||||||
|
/* padding: 20px; */
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-left,
|
||||||
|
.img-right {
|
||||||
|
max-width: 30%;
|
||||||
|
margin: 20px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-left {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-right {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearfix {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$bannerFolder = 'assets/images/banners/';
|
||||||
|
$bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
||||||
|
|
||||||
|
$randomBanner = 'assets/images/base4/camping.jpg'; // default fallback
|
||||||
|
if (!empty($bannerImages)) {
|
||||||
|
$randomBanner = $bannerImages[array_rand($bannerImages)];
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<section class="page-banner-area pt-50 pb-35 rel z-1 bgs-cover" style="background-image: url('<?php echo $randomBanner; ?>');">
|
||||||
|
<div class="banner-overlay"></div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="banner-inner text-white">
|
||||||
|
<h2 class="page-title mb-10" data-aos="fade-left" data-aos-duration="1500" data-aos-offset="50"><?= htmlspecialchars($row['title']) ?></h2>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb justify-content-center mb-20" data-aos="fade-right" data-aos-delay="200" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<li class="breadcrumb-item"><a href="index.php">Home</a></li>
|
||||||
|
<li class="breadcrumb-item active"><?= htmlspecialchars($row['title']) ?></li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Page Banner End -->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Blog Detaisl Area start -->
|
||||||
|
<section class="blog-detaisl-page py-100 rel z-1">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="blog-details-content" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<a href="blog.html" class="category"><?= htmlspecialchars($row['category']) ?></a>
|
||||||
|
<ul class="blog-meta mb-30">
|
||||||
|
<li><img src="assets/images/pp/default.png" alt="Admin"> <a href="#"><?= $author?></a></li>
|
||||||
|
<li><i class="far fa-calendar-alt"></i> <a href="#"><?= htmlspecialchars($row['date']) ?></a></li>
|
||||||
|
<li><i class="far fa-comments"></i> <a href="#">Comments (<?= getCommentCount($page_id);?>)</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<?= $row['content'] ?>
|
||||||
|
</div>
|
||||||
|
<hr class="mb-45">
|
||||||
|
<div class="tag-share mb-50">
|
||||||
|
<div class="item" data-aos="fade-left" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<h6>Tags </h6>
|
||||||
|
<div class="tag-coulds">
|
||||||
|
<a href="blog.php"><?= htmlspecialchars($row['category']) ?></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php include_once('comment_box.php'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4 col-md-8 col-sm-10 rmt-75">
|
||||||
|
<div class="blog-sidebar">
|
||||||
|
<div class="widget widget-gallery" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<h5 class="widget-title">Gallery</h5>
|
||||||
|
<div class="gallery">
|
||||||
|
<?php
|
||||||
|
$folder = 'uploads/blogs/'.$blog_id.'/images/';
|
||||||
|
$files = glob($folder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
||||||
|
shuffle($files); // Randomize the order
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
echo '<a href="' . $file . '" style="width: 110px; height: 110px; overflow: hidden; display: inline-block; margin: 2px;">';
|
||||||
|
echo '<img src="' . $file . '" alt="Gallery" style="width: 100%; height: 100%; object-fit: cover; display: block;">';
|
||||||
|
echo '</a>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php include_once("insta_footer.php"); ?>
|
||||||
31
blog_unpublish.php
Normal file
31
blog_unpublish.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
require_once("env.php");
|
||||||
|
require_once("session.php");
|
||||||
|
require_once("connection.php");
|
||||||
|
require_once("functions.php");
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo "Not authorized";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$article_id = (int)($_POST['id'] ?? 0);
|
||||||
|
$user_id = $_SESSION['user_id'];
|
||||||
|
|
||||||
|
if ($article_id <= 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo "Invalid blog ID";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("UPDATE blogs SET status = 'draft' WHERE blog_id = ? AND author = ?");
|
||||||
|
$stmt->bind_param("ii", $article_id, $user_id);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo "Published";
|
||||||
|
} else {
|
||||||
|
http_response_code(500);
|
||||||
|
echo "Failed to publish: " . $stmt->error;
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
checkUserSession();
|
checkUserSession();
|
||||||
|
|
||||||
// SQL query to fetch dates for driver training
|
// SQL query to fetch dates for driver training
|
||||||
$sql = "SELECT course_id, date FROM courses WHERE course_type = 'bush_mechanics' AND date >= CURDATE()";
|
$sql = "SELECT course_id, date FROM courses WHERE course_type = 'bush_mechanics'";
|
||||||
$result = $conn->query($sql);
|
$result = $conn->query($sql);
|
||||||
$page_id = 'bush_mechanics';
|
$page_id = 'bush_mechanics';
|
||||||
?>
|
?>
|
||||||
@@ -95,7 +95,6 @@ if (!empty($bannerImages)) {
|
|||||||
<div class="blog-sidebar tour-sidebar">
|
<div class="blog-sidebar tour-sidebar">
|
||||||
<div class="widget widget-booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
<div class="widget widget-booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
<form action="process_course_booking.php" method="POST">
|
<form action="process_course_booking.php" method="POST">
|
||||||
<input type="hidden" name="csrf_token" value="<?php echo \Middleware\CsrfMiddleware::getToken(); ?>">
|
|
||||||
<ul class="tickets clearfix">
|
<ul class="tickets clearfix">
|
||||||
<li>
|
<li>
|
||||||
Select Date
|
Select Date
|
||||||
@@ -115,7 +114,7 @@ if (!empty($bannerImages)) {
|
|||||||
</select>
|
</select>
|
||||||
</li>
|
</li>
|
||||||
<?php
|
<?php
|
||||||
if ($is_member || $pending_member) {
|
if ($is_member) {
|
||||||
echo '
|
echo '
|
||||||
<li>
|
<li>
|
||||||
Additional Members <span class="price"></span>
|
Additional Members <span class="price"></span>
|
||||||
@@ -170,16 +169,8 @@ if (!empty($bannerImages)) {
|
|||||||
<label for="agreeCheckbox" id="agreeLabel" style="color: #888;">I have read and agree to the indemnity terms</label>
|
<label for="agreeCheckbox" id="agreeLabel" style="color: #888;">I have read and agree to the indemnity terms</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<button type="submit" class="theme-btn style-two w-100 mt-15 mb-5">
|
||||||
$button_text = "Book Now";
|
<span data-hover="Book Now">Book Now</span>
|
||||||
$button_disabled = "";
|
|
||||||
if (!$result || $result->num_rows == 0) {
|
|
||||||
$button_text = "No booking dates available";
|
|
||||||
$button_disabled = "disabled";
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<button type="submit" class="theme-btn style-two w-100 mt-15 mb-5" <?php echo $button_disabled; ?>>
|
|
||||||
<span data-hover="<?php echo $button_text; ?>"><?php echo $button_text; ?></span>
|
|
||||||
<i class="fal fa-arrow-right"></i>
|
<i class="fal fa-arrow-right"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
@@ -366,7 +357,6 @@ if (!empty($bannerImages)) {
|
|||||||
|
|
||||||
// Fetch PHP variables
|
// Fetch PHP variables
|
||||||
var isMember = <?php echo $is_member ? 'true' : 'false'; ?>;
|
var isMember = <?php echo $is_member ? 'true' : 'false'; ?>;
|
||||||
var pendingMember = <?php echo $pending_member ? 'true' : 'false'; ?>;
|
|
||||||
var cost_members = <?= getPrice('bush_mechanics', 'member');?>;
|
var cost_members = <?= getPrice('bush_mechanics', 'member');?>;
|
||||||
var cost_nonmembers = <?= getPrice('bush_mechanics', 'nonmember');?>;
|
var cost_nonmembers = <?= getPrice('bush_mechanics', 'nonmember');?>;
|
||||||
|
|
||||||
@@ -374,7 +364,7 @@ if (!empty($bannerImages)) {
|
|||||||
var total = 0;
|
var total = 0;
|
||||||
|
|
||||||
// Calculate cost for members
|
// Calculate cost for members
|
||||||
if (isMember || pendingMember) {
|
if (isMember) {
|
||||||
total = (cost_members) + (members * cost_members) + (nonmembers * cost_nonmembers);
|
total = (cost_members) + (members * cost_members) + (nonmembers * cost_nonmembers);
|
||||||
} else {
|
} else {
|
||||||
// Calculate cost for non-members
|
// Calculate cost for non-members
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ checkUserSession();
|
|||||||
<div class="widget widget-booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
<div class="widget widget-booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
<h5 class="widget-title">Book your Campsite</h5>
|
<h5 class="widget-title">Book your Campsite</h5>
|
||||||
<form action="process_camp_booking.php" method="POST">
|
<form action="process_camp_booking.php" method="POST">
|
||||||
<input type="hidden" name="csrf_token" value="<?php echo \Middleware\CsrfMiddleware::getToken(); ?>">
|
|
||||||
<div class="date mb-25">
|
<div class="date mb-25">
|
||||||
<b>From Date</b>
|
<b>From Date</b>
|
||||||
<input type="date" id="from_date" name="from_date">
|
<input type="date" id="from_date" name="from_date">
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ if (!empty($bannerImages)) {
|
|||||||
<div class="modal fade" id="addCampsiteModal" tabindex="-1">
|
<div class="modal fade" id="addCampsiteModal" tabindex="-1">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<form id="addCampsiteForm" method="POST" action="add_campsite.php" enctype="multipart/form-data">
|
<form id="addCampsiteForm" method="POST" action="add_campsite.php" enctype="multipart/form-data">
|
||||||
<input type="hidden" name="csrf_token" value="<?php echo \Middleware\CsrfMiddleware::getToken(); ?>">
|
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">Add Campsite</h5>
|
<h5 class="modal-title">Add Campsite</h5>
|
||||||
|
|||||||
@@ -2,14 +2,9 @@
|
|||||||
checkUserSession();
|
checkUserSession();
|
||||||
|
|
||||||
// SQL query to fetch dates for driver training
|
// SQL query to fetch dates for driver training
|
||||||
$sql = "SELECT course_id, date
|
$sql = "SELECT course_id, date FROM courses WHERE course_type = 'driver_training'";
|
||||||
FROM courses
|
|
||||||
WHERE course_type = 'driver_training'
|
|
||||||
AND date >= CURDATE()";
|
|
||||||
|
|
||||||
$result = $conn->query($sql);
|
$result = $conn->query($sql);
|
||||||
$page_id = 'driver_training';
|
$page_id = 'driver_training';
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -99,17 +94,16 @@ if (!empty($bannerImages)) {
|
|||||||
<div class="blog-sidebar tour-sidebar">
|
<div class="blog-sidebar tour-sidebar">
|
||||||
<div class="widget widget-booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
<div class="widget widget-booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
<form action="process_course_booking.php" method="POST">
|
<form action="process_course_booking.php" method="POST">
|
||||||
<input type="hidden" name="csrf_token" value="<?php echo \Middleware\CsrfMiddleware::getToken(); ?>">
|
|
||||||
<ul class="tickets clearfix">
|
<ul class="tickets clearfix">
|
||||||
<li>
|
<li>
|
||||||
Select Date
|
Select Date
|
||||||
<select name="course_id" id="course_id" required>
|
<select name="course_id" id="course_id" required>
|
||||||
<?php
|
<?php
|
||||||
if ($result && $result->num_rows > 0) {
|
if ($result->num_rows > 0) {
|
||||||
// Output each course as an option
|
// Output each course as an option
|
||||||
while ($row = $result->fetch_assoc()) {
|
while ($row = $result->fetch_assoc()) {
|
||||||
$course_id = htmlspecialchars($row['course_id']); // Escape output for security
|
$course_id = htmlspecialchars($row['course_id']); // Escape output for security
|
||||||
$date = htmlspecialchars($row['date']); // Escape output for security
|
$date = htmlspecialchars($row['date']); // Escape output for security
|
||||||
echo "<option value='$course_id'>$date</option>";
|
echo "<option value='$course_id'>$date</option>";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -117,10 +111,9 @@ if (!empty($bannerImages)) {
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
<?php
|
<?php
|
||||||
if ($is_member || $pending_member) {
|
if ($is_member) {
|
||||||
echo '
|
echo '
|
||||||
<li>
|
<li>
|
||||||
Additional Members <span class="price"></span>
|
Additional Members <span class="price"></span>
|
||||||
@@ -143,7 +136,6 @@ if (!empty($bannerImages)) {
|
|||||||
<option value="3">03</option>
|
<option value="3">03</option>
|
||||||
</select>
|
</select>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
<hr class="mb-25">
|
<hr class="mb-25">
|
||||||
|
|
||||||
@@ -176,16 +168,8 @@ if (!empty($bannerImages)) {
|
|||||||
<label for="agreeCheckbox" id="agreeLabel" style="color: #888;">I have read and agree to the indemnity terms</label>
|
<label for="agreeCheckbox" id="agreeLabel" style="color: #888;">I have read and agree to the indemnity terms</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<button type="submit" class="theme-btn style-two w-100 mt-15 mb-5">
|
||||||
$button_text = "Book Now";
|
<span data-hover="Book Now">Book Now</span>
|
||||||
$button_disabled = "";
|
|
||||||
if (!$result || $result->num_rows == 0) {
|
|
||||||
$button_text = "No booking dates available";
|
|
||||||
$button_disabled = "disabled";
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<button type="submit" class="theme-btn style-two w-100 mt-15 mb-5" <?php echo $button_disabled; ?>>
|
|
||||||
<span data-hover="<?php echo $button_text; ?>"><?php echo $button_text; ?></span>
|
|
||||||
<i class="fal fa-arrow-right"></i>
|
<i class="fal fa-arrow-right"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
@@ -371,7 +355,6 @@ if (!empty($bannerImages)) {
|
|||||||
|
|
||||||
// Fetch PHP variables
|
// Fetch PHP variables
|
||||||
var isMember = <?php echo $is_member ? 'true' : 'false'; ?>;
|
var isMember = <?php echo $is_member ? 'true' : 'false'; ?>;
|
||||||
var pendingMember = <?php echo $pending_member ? 'true' : 'false'; ?>;
|
|
||||||
var cost_members = <?= getPrice('driver_training', 'member'); ?>;
|
var cost_members = <?= getPrice('driver_training', 'member'); ?>;
|
||||||
var cost_nonmembers = <?= getPrice('driver_training', 'nonmember'); ?>;
|
var cost_nonmembers = <?= getPrice('driver_training', 'nonmember'); ?>;
|
||||||
|
|
||||||
@@ -379,7 +362,7 @@ if (!empty($bannerImages)) {
|
|||||||
var total = 0;
|
var total = 0;
|
||||||
|
|
||||||
// Calculate cost for members
|
// Calculate cost for members
|
||||||
if (isMember || pendingMember) {
|
if (isMember) {
|
||||||
total = (cost_members) + (members * cost_members) + (nonmembers * cost_nonmembers);
|
total = (cost_members) + (members * cost_members) + (nonmembers * cost_nonmembers);
|
||||||
} else {
|
} else {
|
||||||
// Calculate cost for non-members
|
// Calculate cost for non-members
|
||||||
|
|||||||
30
env.php
30
env.php
@@ -3,33 +3,3 @@ require_once __DIR__ . '/vendor/autoload.php';
|
|||||||
|
|
||||||
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
|
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
|
||||||
$dotenv->load();
|
$dotenv->load();
|
||||||
|
|
||||||
// PSR-4 Autoloader for Services and Controllers
|
|
||||||
spl_autoload_register(function ($class) {
|
|
||||||
// Remove leading namespace separator
|
|
||||||
$class = ltrim($class, '\\');
|
|
||||||
|
|
||||||
// Define namespace to directory mapping
|
|
||||||
$prefixes = [
|
|
||||||
'Services\\' => __DIR__ . '/src/Services/',
|
|
||||||
'Controllers\\' => __DIR__ . '/src/Controllers/',
|
|
||||||
'Middleware\\' => __DIR__ . '/src/Middleware/',
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($prefixes as $prefix => $baseDir) {
|
|
||||||
if (strpos($class, $prefix) === 0) {
|
|
||||||
// Remove the prefix from the class
|
|
||||||
$relativeClass = substr($class, strlen($prefix));
|
|
||||||
|
|
||||||
// Build the file path
|
|
||||||
$file = $baseDir . str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php';
|
|
||||||
|
|
||||||
if (file_exists($file)) {
|
|
||||||
require_once $file;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|||||||
2241
functions.php
2241
functions.php
File diff suppressed because it is too large
Load Diff
44
header01.php
44
header01.php
@@ -4,51 +4,15 @@ require_once("env.php");
|
|||||||
require_once("session.php");
|
require_once("session.php");
|
||||||
require_once("connection.php");
|
require_once("connection.php");
|
||||||
require_once("functions.php");
|
require_once("functions.php");
|
||||||
|
$is_logged_in = isset($_SESSION['user_id']);
|
||||||
// Import services
|
if (isset($_SESSION['user_id'])) {
|
||||||
use Services\AuthenticationService;
|
$is_member = getUserMemberStatus($_SESSION['user_id']);
|
||||||
use Services\UserService;
|
|
||||||
|
|
||||||
// Security Headers
|
|
||||||
// Enforce HTTPS
|
|
||||||
if (empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === 'off') {
|
|
||||||
header('Location: https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'], true, 301);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTP Security Headers
|
|
||||||
header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
|
|
||||||
header('X-Content-Type-Options: nosniff');
|
|
||||||
header('X-Frame-Options: SAMEORIGIN');
|
|
||||||
header('X-XSS-Protection: 1; mode=block');
|
|
||||||
header('Referrer-Policy: strict-origin-when-cross-origin');
|
|
||||||
header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
|
|
||||||
|
|
||||||
// Session Security Configuration
|
|
||||||
ini_set('session.cookie_httponly', 1);
|
|
||||||
ini_set('session.cookie_secure', 1);
|
|
||||||
ini_set('session.cookie_samesite', 'Strict');
|
|
||||||
ini_set('session.use_only_cookies', 1);
|
|
||||||
|
|
||||||
// Generate CSRF token if not exists
|
|
||||||
AuthenticationService::generateCsrfToken();
|
|
||||||
|
|
||||||
// User session management
|
|
||||||
$is_logged_in = AuthenticationService::isLoggedIn();
|
|
||||||
if ($is_logged_in) {
|
|
||||||
$authService = new AuthenticationService();
|
|
||||||
$userService = new UserService();
|
|
||||||
$user_id = $_SESSION['user_id'];
|
$user_id = $_SESSION['user_id'];
|
||||||
$is_member = getUserMemberStatus($user_id);
|
|
||||||
$pending_member = getUserMemberStatusPending($user_id);
|
|
||||||
} else {
|
} else {
|
||||||
$is_member = false;
|
$is_member = false;
|
||||||
$pending_member = false;
|
|
||||||
$user_id = null;
|
|
||||||
}
|
}
|
||||||
$role = getUserRole();
|
$role = getUserRole();
|
||||||
logVisitor();
|
logVisitor();
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@@ -247,6 +211,7 @@ logVisitor();
|
|||||||
<!-- <li><a href="admin_payments.php">Payfast Payments</a></li> -->
|
<!-- <li><a href="admin_payments.php">Payfast Payments</a></li> -->
|
||||||
<li><a href="admin_efts.php">EFT Payments</a></li>
|
<li><a href="admin_efts.php">EFT Payments</a></li>
|
||||||
<li><a href="process_payments.php">Process Payments</a></li>
|
<li><a href="process_payments.php">Process Payments</a></li>
|
||||||
|
<li><a href="admin_blogs.php">Manage Blogs</a></li>
|
||||||
<!-- <li><a href="bar_tabs.php">Bar</a></li> -->
|
<!-- <li><a href="bar_tabs.php">Bar</a></li> -->
|
||||||
<?php if ($role === 'superadmin') { ?>
|
<?php if ($role === 'superadmin') { ?>
|
||||||
<li><a href="admin_visitors.php">Visitor Log</a></li>
|
<li><a href="admin_visitors.php">Visitor Log</a></li>
|
||||||
@@ -268,6 +233,7 @@ logVisitor();
|
|||||||
<li><a href="account_settings.php">Account Settings</a></li>
|
<li><a href="account_settings.php">Account Settings</a></li>
|
||||||
<li><a href="membership_details.php">Membership</a></li>
|
<li><a href="membership_details.php">Membership</a></li>
|
||||||
<li><a href="bookings.php">My Bookings</a></li>
|
<li><a href="bookings.php">My Bookings</a></li>
|
||||||
|
<li><a href="user_blogs.php">My Blogs</a></li>
|
||||||
<li><a href="submit_pop.php">Submit P.O.P</a></li>
|
<li><a href="submit_pop.php">Submit P.O.P</a></li>
|
||||||
<li><a href="logout.php">Log Out</a></li>
|
<li><a href="logout.php">Log Out</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ $is_logged_in = isset($_SESSION['user_id']);
|
|||||||
$role = getUserRole();
|
$role = getUserRole();
|
||||||
if (isset($_SESSION['user_id'])) {
|
if (isset($_SESSION['user_id'])) {
|
||||||
$is_member = getUserMemberStatus($_SESSION['user_id']);
|
$is_member = getUserMemberStatus($_SESSION['user_id']);
|
||||||
$pending_member = getUserMemberStatusPending($_SESSION['user_id']);
|
|
||||||
$user_id = $_SESSION['user_id'];
|
$user_id = $_SESSION['user_id'];
|
||||||
}
|
}
|
||||||
logVisitor();
|
logVisitor();
|
||||||
@@ -34,6 +33,8 @@ logVisitor();
|
|||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
|
||||||
|
|
||||||
<!-- Flaticon -->
|
<!-- Flaticon -->
|
||||||
<link rel="stylesheet" href="assets/css/flaticon.min.css">
|
<link rel="stylesheet" href="assets/css/flaticon.min.css">
|
||||||
<!-- Font Awesome -->
|
<!-- Font Awesome -->
|
||||||
@@ -225,6 +226,7 @@ logVisitor();
|
|||||||
<!-- <li><a href="admin_payments.php">Payfast Payments</a></li> -->
|
<!-- <li><a href="admin_payments.php">Payfast Payments</a></li> -->
|
||||||
<li><a href="admin_efts.php">EFT Payments</a></li>
|
<li><a href="admin_efts.php">EFT Payments</a></li>
|
||||||
<li><a href="process_payments.php">Process Payments</a></li>
|
<li><a href="process_payments.php">Process Payments</a></li>
|
||||||
|
<li><a href="admin_blogs.php">Manage Blogs</a></li>
|
||||||
<?php if ($role === 'superadmin') { ?>
|
<?php if ($role === 'superadmin') { ?>
|
||||||
<li><a href="admin_visitors.php">Visitor Log</a></li>
|
<li><a href="admin_visitors.php">Visitor Log</a></li>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
@@ -239,6 +241,7 @@ logVisitor();
|
|||||||
<li><a href="account_settings.php">Account Settings</a></li>
|
<li><a href="account_settings.php">Account Settings</a></li>
|
||||||
<li><a href="membership_details.php">Membership</a></li>
|
<li><a href="membership_details.php">Membership</a></li>
|
||||||
<li><a href="bookings.php">My Bookings</a></li>
|
<li><a href="bookings.php">My Bookings</a></li>
|
||||||
|
<li><a href="user_blogs.php">My Blogs</a></li>
|
||||||
<li><a href="submit_pop.php">Submit P.O.P</a></li>
|
<li><a href="submit_pop.php">Submit P.O.P</a></li>
|
||||||
<li><a href="logout.php">Log Out</a></li>
|
<li><a href="logout.php">Log Out</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
188
index.php
188
index.php
@@ -190,6 +190,105 @@ if (countUpcomingTrips() > 0) { ?>
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<!-- Popular Destinations Area start -->
|
||||||
|
<!-- <section class="popular-destinations-area rel z-1">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="popular-destinations-wrap br-20 bgc-lighter pt-100 pb-70">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<div class="section-title text-center counter-text-wrap mb-70" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<h2>Explore Popular Destinations</h2>
|
||||||
|
<p>One site <span class="count-text plus" data-speed="3000" data-stop="34500">0</span> most popular experience</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-xl-3 col-md-6">
|
||||||
|
<div class="destination-item style-two" data-aos="flip-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="image">
|
||||||
|
<a href="#" class="heart"><i class="fas fa-heart"></i></a>
|
||||||
|
<img src="assets/images/destinations/destination1.jpg" alt="Destination">
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h6><a href="destination-details.html">Thailand beach</a></h6>
|
||||||
|
<span class="time">5352+ tours & 856+ Activity</span>
|
||||||
|
<a href="#" class="more"><i class="fas fa-chevron-right"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-3 col-md-6">
|
||||||
|
<div class="destination-item style-two" data-aos="flip-up" data-aos-delay="100" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="image">
|
||||||
|
<a href="#" class="heart"><i class="fas fa-heart"></i></a>
|
||||||
|
<img src="assets/images/destinations/destination2.jpg" alt="Destination">
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h6><a href="destination-details.html">Parga, Greece</a></h6>
|
||||||
|
<span class="time">5352+ tours & 856+ Activity</span>
|
||||||
|
<a href="#" class="more"><i class="fas fa-chevron-right"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="destination-item style-two" data-aos="flip-up" data-aos-delay="200" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="image">
|
||||||
|
<a href="#" class="heart"><i class="fas fa-heart"></i></a>
|
||||||
|
<img src="assets/images/destinations/destination3.jpg" alt="Destination">
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h6><a href="destination-details.html">Castellammare del Golfo, Italy</a></h6>
|
||||||
|
<span class="time">5352+ tours & 856+ Activity</span>
|
||||||
|
<a href="#" class="more"><i class="fas fa-chevron-right"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="destination-item style-two" data-aos="flip-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="image">
|
||||||
|
<a href="#" class="heart"><i class="fas fa-heart"></i></a>
|
||||||
|
<img src="assets/images/destinations/destination4.jpg" alt="Destination">
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h6><a href="destination-details.html">Reserve of Canada, Canada</a></h6>
|
||||||
|
<span class="time">5352+ tours & 856+ Activity</span>
|
||||||
|
<a href="#" class="more"><i class="fas fa-chevron-right"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-3 col-md-6">
|
||||||
|
<div class="destination-item style-two" data-aos="flip-up" data-aos-delay="100" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="image">
|
||||||
|
<a href="#" class="heart"><i class="fas fa-heart"></i></a>
|
||||||
|
<img src="assets/images/destinations/destination5.jpg" alt="Destination">
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h6><a href="destination-details.html">Dubai united states</a></h6>
|
||||||
|
<span class="time">5352+ tours & 856+ Activity</span>
|
||||||
|
<a href="#" class="more"><i class="fas fa-chevron-right"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-3 col-md-6">
|
||||||
|
<div class="destination-item style-two" data-aos="flip-up" data-aos-delay="200" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="image">
|
||||||
|
<a href="#" class="heart"><i class="fas fa-heart"></i></a>
|
||||||
|
<img src="assets/images/destinations/destination6.jpg" alt="Destination">
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h6><a href="destination-details.html">Milos, Greece</a></h6>
|
||||||
|
<span class="time">5352+ tours & 856+ Activity</span>
|
||||||
|
<a href="#" class="more"><i class="fas fa-chevron-right"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section> -->
|
||||||
|
<!-- Popular Destinations Area end -->
|
||||||
|
|
||||||
|
|
||||||
<!-- Features Area start -->
|
<!-- Features Area start -->
|
||||||
<section class="features-area pt-100 pb-45 rel z-1">
|
<section class="features-area pt-100 pb-45 rel z-1">
|
||||||
@@ -212,6 +311,31 @@ if (countUpcomingTrips() > 0) { ?>
|
|||||||
<i class="fal fa-arrow-right"></i>
|
<i class="fal fa-arrow-right"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- <div class="menu-btns py-10">
|
||||||
|
<a href="campsite_booking.php" class="theme-btn style-two bgc-secondary">
|
||||||
|
<span data-hover="Book a Campsite">Book a Campsite</span>
|
||||||
|
<i class="fal fa-arrow-right"></i>
|
||||||
|
</a>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- <div class="features-customer-box">
|
||||||
|
<div class="image">
|
||||||
|
<img src="assets/images/features/features-box.jpg" alt="Features">
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="feature-authors mb-15">
|
||||||
|
<img src="assets/images/features/feature-author1.jpg" alt="Author">
|
||||||
|
<img src="assets/images/features/feature-author2.jpg" alt="Author">
|
||||||
|
<img src="assets/images/features/feature-author3.jpg" alt="Author">
|
||||||
|
<span>4k+</span>
|
||||||
|
</div>
|
||||||
|
<h6>850K+ Happy Customer</h6>
|
||||||
|
<div class="divider style-two counter-text-wrap my-25"><span><span class="count-text plus" data-speed="3000" data-stop="25">0</span> Years</span></div>
|
||||||
|
<p>We pride ourselves offering personalized itineraries</p>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xl-6" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
|
<div class="col-xl-6" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
|
||||||
@@ -354,10 +478,56 @@ if (countUpcomingTrips() > 0) { ?>
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- <div class="hotel-more-btn text-center mt-40">
|
||||||
|
<a href="destination2.html" class="theme-btn style-four">
|
||||||
|
<span data-hover="Explore More Hotel">Explore More Hotel</span>
|
||||||
|
<i class="fal fa-arrow-right"></i>
|
||||||
|
</a>
|
||||||
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<!-- Hotel Area end -->
|
<!-- Hotel Area end -->
|
||||||
|
|
||||||
|
<!-- CTA Area start -->
|
||||||
|
<!-- <section class="cta-area pt-100 rel z-1">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xl-4 col-md-6" data-aos="zoom-in-down" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="cta-item" style="background-image: url(assets/images/cta/cta1.jpg);">
|
||||||
|
<span class="category">Tent Camping</span>
|
||||||
|
<h2>Explore the world best tourism</h2>
|
||||||
|
<a href="trip-details.php" class="theme-btn style-two bgc-secondary">
|
||||||
|
<span data-hover="Explore Tours">Explore Tours</span>
|
||||||
|
<i class="fal fa-arrow-right"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-4 col-md-6" data-aos="zoom-in-down" data-aos-delay="50" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="cta-item" style="background-image: url(assets/images/cta/cta2.jpg);">
|
||||||
|
<span class="category">Sea Beach</span>
|
||||||
|
<h2>World largest Sea Beach in Thailand</h2>
|
||||||
|
<a href="trip-details.php" class="theme-btn style-two">
|
||||||
|
<span data-hover="Explore Tours">Explore Tours</span>
|
||||||
|
<i class="fal fa-arrow-right"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-4 col-md-6" data-aos="zoom-in-down" data-aos-delay="100" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="cta-item" style="background-image: url(assets/images/cta/cta3.jpg);">
|
||||||
|
<span class="category">Water Falls</span>
|
||||||
|
<h2>Largest Water falls Bali, Indonesia</h2>
|
||||||
|
<a href="trip-details.php" class="theme-btn style-two bgc-secondary">
|
||||||
|
<span data-hover="Explore Tours">Explore Tours</span>
|
||||||
|
<i class="fal fa-arrow-right"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section> -->
|
||||||
|
<!-- CTA Area end -->
|
||||||
|
|
||||||
|
|
||||||
<!-- Blog Area start -->
|
<!-- Blog Area start -->
|
||||||
<section class="blog-area py-70 rel z-1">
|
<section class="blog-area py-70 rel z-1">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -371,7 +541,7 @@ if (countUpcomingTrips() > 0) { ?>
|
|||||||
</div>
|
</div>
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<?php
|
<?php
|
||||||
$sql = "SELECT blog_id, title, date, category, image, description, author, link, members_only FROM blogs WHERE status = 'published' ORDER BY date DESC LIMIT 3";
|
$sql = "SELECT blog_id, title, date, category, image, description, author, link, members_only FROM blogs WHERE status = 'published' ORDER BY date DESC LIMIT 3 ";
|
||||||
$result = $conn->query($sql);
|
$result = $conn->query($sql);
|
||||||
|
|
||||||
if ($result->num_rows > 0) {
|
if ($result->num_rows > 0) {
|
||||||
@@ -392,7 +562,7 @@ if (countUpcomingTrips() > 0) { ?>
|
|||||||
$icon = "fa-lock";
|
$icon = "fa-lock";
|
||||||
}else{
|
}else{
|
||||||
if (getUserMemberStatus($_SESSION['user_id'])) {
|
if (getUserMemberStatus($_SESSION['user_id'])) {
|
||||||
$blog_link = $row['link'];
|
$blog_link = "blog_read.php?token=".encryptData($blog_id, $salt);
|
||||||
$button_hover = "Read More";
|
$button_hover = "Read More";
|
||||||
$icon = "fa-arrow-right";
|
$icon = "fa-arrow-right";
|
||||||
}else{
|
}else{
|
||||||
@@ -402,7 +572,7 @@ if (countUpcomingTrips() > 0) { ?>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
$blog_link = $row['link'];
|
$blog_link = "blog_read.php?token=".encryptData($blog_id, $salt);
|
||||||
$button_hover = "Read More";
|
$button_hover = "Read More";
|
||||||
$icon = "fa-arrow-right";
|
$icon = "fa-arrow-right";
|
||||||
}
|
}
|
||||||
@@ -421,7 +591,7 @@ if (countUpcomingTrips() > 0) { ?>
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="image">
|
<div class="image">
|
||||||
<img style="border-radius:20px;" src="assets/images/blog/' . $blog_id . '/' . $blog_image . '" alt="Blog List">
|
<img style="border-radius:20px;" src="' . $blog_image . '" alt="Blog List">
|
||||||
</div>
|
</div>
|
||||||
<a style="width:100%;" href="' . $blog_link . '" class="theme-btn">
|
<a style="width:100%;" href="' . $blog_link . '" class="theme-btn">
|
||||||
<span style="width:100%;" data-hover="'.$button_hover.'">Read More</span>
|
<span style="width:100%;" data-hover="'.$button_hover.'">Read More</span>
|
||||||
@@ -494,6 +664,16 @@ if (countUpcomingTrips() > 0) { ?>
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<form class="newsletter-form mb-50" action="#">
|
||||||
|
<input id="news-email" type="email" placeholder="Email Address" required>
|
||||||
|
<button type="submit" class="theme-btn bgc-secondary style-two">
|
||||||
|
<span data-hover="Subscribe">Subscribe</span>
|
||||||
|
<i class="fal fa-arrow-right"></i>
|
||||||
|
</button>
|
||||||
|
</form> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ $login_url = $client->createAuthUrl();
|
|||||||
<div class="">
|
<div class="">
|
||||||
<div class="comment-form bgc-lighter z-1 rel mb-30 rmb-55">
|
<div class="comment-form bgc-lighter z-1 rel mb-30 rmb-55">
|
||||||
<form id="loginForm" class="loginForm" name="loginForm" action="assets/php/form-process.php" method="post" data-aos="fade-left" data-aos-duration="1500" data-aos-offset="50">
|
<form id="loginForm" class="loginForm" name="loginForm" action="assets/php/form-process.php" method="post" data-aos="fade-left" data-aos-duration="1500" data-aos-offset="50">
|
||||||
<input type="hidden" name="csrf_token" value="<?php echo \Middleware\CsrfMiddleware::getToken(); ?>">
|
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<h2>Log in</h2>
|
<h2>Log in</h2>
|
||||||
<div style="text-align: center;" id="responseMessage"></div> <!-- Message display area -->
|
<div style="text-align: center;" id="responseMessage"></div> <!-- Message display area -->
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ if (!empty($bannerImages)) {
|
|||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
<div class="comment-form bgc-lighter z-1 rel mb-30 rmb-55">
|
<div class="comment-form bgc-lighter z-1 rel mb-30 rmb-55">
|
||||||
<form id="registerForm" name="registerForm" action="process_application.php" method="post" data-aos="fade-left" data-aos-duration="1500" data-aos-offset="50">
|
<form id="registerForm" name="registerForm" action="process_application.php" method="post" data-aos="fade-left" data-aos-duration="1500" data-aos-offset="50">
|
||||||
<input type="hidden" name="csrf_token" value="<?php echo \Middleware\CsrfMiddleware::getToken(); ?>">
|
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<div id="responseMessage"></div> <!-- Message display area -->
|
<div id="responseMessage"></div> <!-- Message display area -->
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,16 +4,12 @@ require_once("session.php");
|
|||||||
require_once("connection.php");
|
require_once("connection.php");
|
||||||
require_once("functions.php");
|
require_once("functions.php");
|
||||||
|
|
||||||
use Middleware\CsrfMiddleware;
|
|
||||||
|
|
||||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||||
$eft_id = strtoupper($user_id." SUBS ".date("Y")." ".getInitialSurname($user_id));
|
$eft_id = strtoupper($user_id." SUBS ".date("Y")." ".getInitialSurname($user_id));
|
||||||
$status = 'AWAITING PAYMENT';
|
$status = 'AWAITING PAYMENT';
|
||||||
$description = 'Membership Fees '.date("Y")." ".getInitialSurname($user_id);
|
$description = 'Membership Fees '.date("Y")." ".getInitialSurname($user_id);
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
// Validate CSRF token
|
|
||||||
CsrfMiddleware::requireToken($_POST);
|
|
||||||
|
|
||||||
// Get all the form fields
|
// Get all the form fields
|
||||||
$first_name = $_POST['first_name'];
|
$first_name = $_POST['first_name'];
|
||||||
@@ -117,24 +113,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$payment_amount = calculateProrata(210); // Assuming a fixed membership fee, adjust as needed
|
$payment_amount = calculateProrata(210); // Assuming a fixed membership fee, adjust as needed
|
||||||
$payment_date = date('Y-m-d');
|
$payment_date = date('Y-m-d');
|
||||||
$membership_start_date = $payment_date;
|
$membership_start_date = $payment_date;
|
||||||
// $membership_end_date = date('Y-12-31');
|
$membership_end_date = date('Y-12-31');
|
||||||
|
|
||||||
// Get today's date
|
|
||||||
$today = new DateTime();
|
|
||||||
|
|
||||||
// Determine the target February
|
|
||||||
if ($today->format('n') > 2) {
|
|
||||||
// If we're past February, target is next year's Feb 28/29
|
|
||||||
$year = $today->format('Y') + 1;
|
|
||||||
} else {
|
|
||||||
// Otherwise, this year's February
|
|
||||||
$year = $today->format('Y');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle leap year (Feb 29) automatically
|
|
||||||
$membership_end_date = (new DateTime("$year-02-01"))
|
|
||||||
->modify('last day of this month')
|
|
||||||
->format('Y-m-d');
|
|
||||||
|
|
||||||
$stmt = $conn->prepare("INSERT INTO membership_fees (user_id, payment_amount, payment_date, membership_start_date, membership_end_date, payment_status, payment_id)
|
$stmt = $conn->prepare("INSERT INTO membership_fees (user_id, payment_amount, payment_date, membership_start_date, membership_end_date, payment_status, payment_id)
|
||||||
VALUES (?, ?, ?, ?, ?, 'PENDING', ?)");
|
VALUES (?, ?, ?, ?, ?, 'PENDING', ?)");
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ require_once("env.php");
|
|||||||
require_once("connection.php");
|
require_once("connection.php");
|
||||||
require_once("functions.php");
|
require_once("functions.php");
|
||||||
|
|
||||||
use Middleware\CsrfMiddleware;
|
|
||||||
|
|
||||||
// Start session to retrieve the logged-in user's ID
|
// Start session to retrieve the logged-in user's ID
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
@@ -13,9 +11,6 @@ $user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
|||||||
|
|
||||||
// Check if the form has been submitted
|
// Check if the form has been submitted
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
// Validate CSRF token
|
|
||||||
CsrfMiddleware::requireToken($_POST);
|
|
||||||
|
|
||||||
// Get values from the form
|
// Get values from the form
|
||||||
$from_date = $_POST['from_date'];
|
$from_date = $_POST['from_date'];
|
||||||
$to_date = $_POST['to_date'];
|
$to_date = $_POST['to_date'];
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ require_once("env.php");
|
|||||||
require_once("connection.php");
|
require_once("connection.php");
|
||||||
require_once("functions.php");
|
require_once("functions.php");
|
||||||
|
|
||||||
use Middleware\CsrfMiddleware;
|
|
||||||
|
|
||||||
// Start session to retrieve the logged-in user's ID
|
// Start session to retrieve the logged-in user's ID
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
@@ -20,8 +18,6 @@ $is_member = getUserMemberStatus($user_id);
|
|||||||
|
|
||||||
// Check if the form has been submitted
|
// Check if the form has been submitted
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
// Validate CSRF token
|
|
||||||
CsrfMiddleware::requireToken($_POST);
|
|
||||||
// Get values from the form
|
// Get values from the form
|
||||||
$from_date = $_POST['from_date'];
|
$from_date = $_POST['from_date'];
|
||||||
$to_date = $_POST['to_date'];
|
$to_date = $_POST['to_date'];
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
require_once("env.php");
|
require_once("env.php");
|
||||||
require_once("connection.php");
|
require_once("connection.php");
|
||||||
require_once("functions.php");
|
require_once("functions.php");
|
||||||
|
|
||||||
use Middleware\CsrfMiddleware;
|
|
||||||
|
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
|
|
||||||
@@ -17,14 +14,11 @@ if (!$user_id) {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
$is_member = getUserMemberStatus($user_id);
|
$is_member = getUserMemberStatus($user_id);
|
||||||
$pending_member = getUserMemberStatusPending($user_id);
|
|
||||||
|
|
||||||
// Check if the form has been submitted
|
// Check if the form has been submitted
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
// Validate CSRF token
|
|
||||||
CsrfMiddleware::requireToken($_POST);
|
|
||||||
// Input variables from the form (use default values if not provided)
|
// Input variables from the form (use default values if not provided)
|
||||||
$additional_members = isset($_POST['members']) ? intval($_POST['members']) : 0; // Default to 1 vehicle
|
$members = isset($_POST['members']) ? intval($_POST['members']) : 0; // Default to 1 vehicle
|
||||||
$num_adults = isset($_POST['non-members']) ? intval($_POST['non-members']) : 0; // Default to 1 adult
|
$num_adults = isset($_POST['non-members']) ? intval($_POST['non-members']) : 0; // Default to 1 adult
|
||||||
$course_id = isset($_POST['course_id']) ? intval($_POST['course_id']) : 0; // Default to 0 children
|
$course_id = isset($_POST['course_id']) ? intval($_POST['course_id']) : 0; // Default to 0 children
|
||||||
checkAndRedirectCourseBooking($course_id);
|
checkAndRedirectCourseBooking($course_id);
|
||||||
@@ -60,13 +54,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$description = "General Course " . $date; // Default fallback description
|
$description = "General Course " . $date; // Default fallback description
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assume the membership status is determined elsewhere
|
||||||
|
$is_member = getUserMemberStatus($user_id);
|
||||||
|
|
||||||
// Initialize total and discount amount
|
// Initialize total and discount amount
|
||||||
$total = 0;
|
$total = 0;
|
||||||
|
|
||||||
// Calculate total based on membership
|
// Calculate total based on membership
|
||||||
if ($is_member || $pending_member) {
|
if ($is_member) {
|
||||||
$num_members = 1 + $additional_members;
|
$num_members = 1 + $members;
|
||||||
$total = ($num_members * $cost_members) + ($num_adults * $cost_nonmembers);
|
$total = (($cost_members) + ($members * $cost_members) + ($num_adults * $cost_nonmembers));
|
||||||
$payment_amount = $total;
|
$payment_amount = $total;
|
||||||
} else {
|
} else {
|
||||||
$num_members = 0;
|
$num_members = 0;
|
||||||
@@ -81,22 +78,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$num_vehicles = 1;
|
$num_vehicles = 1;
|
||||||
$discountAmount = 0;
|
$discountAmount = 0;
|
||||||
$eft_id = strtoupper("COURSE ".date("m-d", strtotime($date))." ".getInitialSurname($user_id));
|
$eft_id = strtoupper("COURSE ".date("m-d", strtotime($date))." ".getInitialSurname($user_id));
|
||||||
$notes = "";
|
|
||||||
if ($pending_member){
|
|
||||||
$notes = "Membership Payment pending at time of booking. Please confirm payment has been received.";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Insert booking into the database
|
// Insert booking into the database
|
||||||
$sql = "INSERT INTO bookings (booking_type, user_id, from_date, to_date, num_vehicles, num_adults, total_amount, discount_amount, status, payment_id, course_id, course_non_members, eft_id, notes)
|
$sql = "INSERT INTO bookings (booking_type, user_id, from_date, to_date, num_vehicles, num_adults, total_amount, discount_amount, status, payment_id, course_id, course_non_members, eft_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||||
$stmt = $conn->prepare($sql);
|
$stmt = $conn->prepare($sql);
|
||||||
|
|
||||||
if (!$stmt) {
|
if (!$stmt) {
|
||||||
die("Preparation failed: " . $conn->error);
|
die("Preparation failed: " . $conn->error);
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt->bind_param('sissiiddssiiss', $type, $user_id, $date, $date, $num_vehicles, $num_members, $total, $discountAmount, $status, $payment_id, $course_id, $num_adults, $eft_id, $notes);
|
$stmt->bind_param('sissiiddssiis', $type, $user_id, $date, $date, $num_vehicles, $num_members, $total, $discountAmount, $status, $payment_id, $course_id, $num_adults, $eft_id);
|
||||||
|
|
||||||
if ($stmt->execute()) {
|
if ($stmt->execute()) {
|
||||||
$booking_id = $conn->insert_id;
|
$booking_id = $conn->insert_id;
|
||||||
@@ -121,6 +114,28 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
echo "Error processing booking: $error_message";
|
echo "Error processing booking: $error_message";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if ($stmt->execute()) {
|
||||||
|
// if ($payment_amount < 1) {
|
||||||
|
// if (processZeroPayment($payment_id, $payment_amount, $description)) {
|
||||||
|
// echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>";
|
||||||
|
// } else {
|
||||||
|
// $error_message = $stmt->error;
|
||||||
|
// echo "Error processing booking: $error_message";
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// if (processPayment($payment_id, $payment_amount, $description)) {
|
||||||
|
// echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>";
|
||||||
|
// } else {
|
||||||
|
// $error_message = $stmt->error;
|
||||||
|
// echo "Error processing booking: $error_message";
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// // Handle error if insert fails and echo the MySQL error
|
||||||
|
// $error_message = $stmt->error;
|
||||||
|
// echo "Error processing booking: $error_message";
|
||||||
|
// }
|
||||||
|
|
||||||
$stmt->close();
|
$stmt->close();
|
||||||
$conn->close();
|
$conn->close();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,19 +3,10 @@ require_once("env.php");
|
|||||||
require_once("session.php");
|
require_once("session.php");
|
||||||
require_once("connection.php");
|
require_once("connection.php");
|
||||||
require_once("functions.php");
|
require_once("functions.php");
|
||||||
|
|
||||||
use Middleware\CsrfMiddleware;
|
|
||||||
|
|
||||||
checkAdmin();
|
checkAdmin();
|
||||||
if (!isset($_GET['token']) || empty($_GET['token'])) {
|
if (!isset($_GET['token']) || empty($_GET['token'])) {
|
||||||
die("Invalid request.");
|
die("Invalid request.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate CSRF token if this is a POST request
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
CsrfMiddleware::requireToken($_POST);
|
|
||||||
}
|
|
||||||
|
|
||||||
$token = $_GET['token'];
|
$token = $_GET['token'];
|
||||||
// echo $token;
|
// echo $token;
|
||||||
$eft_id = decryptData($token, $salt);
|
$eft_id = decryptData($token, $salt);
|
||||||
|
|||||||
@@ -3,16 +3,9 @@ require_once("env.php");
|
|||||||
require_once("connection.php");
|
require_once("connection.php");
|
||||||
require_once("functions.php");
|
require_once("functions.php");
|
||||||
|
|
||||||
use Middleware\CsrfMiddleware;
|
|
||||||
|
|
||||||
// Start session to retrieve the logged-in user's ID
|
// Start session to retrieve the logged-in user's ID
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
// Validate CSRF token early if this is a POST request
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
CsrfMiddleware::requireToken($_POST);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get user ID from session (assuming user is logged in)
|
// Get user ID from session (assuming user is logged in)
|
||||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,11 @@ require_once("session.php");
|
|||||||
require_once("connection.php");
|
require_once("connection.php");
|
||||||
require_once("functions.php");
|
require_once("functions.php");
|
||||||
|
|
||||||
use Middleware\CsrfMiddleware;
|
|
||||||
|
|
||||||
if (!isset($_SESSION['user_id'])) {
|
if (!isset($_SESSION['user_id'])) {
|
||||||
die(json_encode(['status' => 'error', 'message' => 'User not logged in']));
|
die(json_encode(['status' => 'error', 'message' => 'User not logged in']));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($_POST['signature'])) {
|
if (isset($_POST['signature'])) {
|
||||||
// Validate CSRF token
|
|
||||||
CsrfMiddleware::requireToken($_POST);
|
|
||||||
$user_id = $_SESSION['user_id']; // Get the user ID from the session
|
$user_id = $_SESSION['user_id']; // Get the user ID from the session
|
||||||
$signature = $_POST['signature']; // Base64 image data
|
$signature = $_POST['signature']; // Base64 image data
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,8 @@
|
|||||||
require_once("env.php");
|
require_once("env.php");
|
||||||
require_once("connection.php");
|
require_once("connection.php");
|
||||||
require_once("functions.php");
|
require_once("functions.php");
|
||||||
|
|
||||||
use Middleware\CsrfMiddleware;
|
|
||||||
|
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
// Validate CSRF token early if this is a POST request
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
CsrfMiddleware::requireToken($_POST);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the trip_id from the request (ensure it's sanitized)
|
// Get the trip_id from the request (ensure it's sanitized)
|
||||||
$trip_id = isset($_POST['trip_id']) ? intval($_POST['trip_id']) : 0;
|
$trip_id = isset($_POST['trip_id']) ? intval($_POST['trip_id']) : 0;
|
||||||
|
|
||||||
|
|||||||
31
publish_blog.php
Normal file
31
publish_blog.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
require_once("env.php");
|
||||||
|
require_once("session.php");
|
||||||
|
require_once("connection.php");
|
||||||
|
require_once("functions.php");
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo "Not authorized";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$article_id = (int)($_POST['id'] ?? 0);
|
||||||
|
$user_id = $_SESSION['user_id'];
|
||||||
|
|
||||||
|
if ($article_id <= 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo "Invalid blog ID";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("UPDATE blogs SET status = 'published' WHERE blog_id = ? AND author = ?");
|
||||||
|
$stmt->bind_param("ii", $article_id, $user_id);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo "Published";
|
||||||
|
} else {
|
||||||
|
http_response_code(500);
|
||||||
|
echo "Failed to publish: " . $stmt->error;
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
checkUserSession();
|
checkUserSession();
|
||||||
|
|
||||||
// SQL query to fetch dates for driver training
|
// SQL query to fetch dates for driver training
|
||||||
$sql = "SELECT course_id, date FROM courses WHERE course_type = 'rescue_recovery' AND date >= CURDATE()";
|
$sql = "SELECT course_id, date FROM courses WHERE course_type = 'rescue_recovery'";
|
||||||
$result = $conn->query($sql);
|
$result = $conn->query($sql);
|
||||||
$page_id = 'rescue_recovery';
|
$page_id = 'rescue_recovery';
|
||||||
?>
|
?>
|
||||||
@@ -94,7 +94,6 @@ if (!empty($bannerImages)) {
|
|||||||
<div class="blog-sidebar tour-sidebar">
|
<div class="blog-sidebar tour-sidebar">
|
||||||
<div class="widget widget-booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
<div class="widget widget-booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
<form action="process_course_booking.php" method="POST">
|
<form action="process_course_booking.php" method="POST">
|
||||||
<input type="hidden" name="csrf_token" value="<?php echo \Middleware\CsrfMiddleware::getToken(); ?>">
|
|
||||||
<ul class="tickets clearfix">
|
<ul class="tickets clearfix">
|
||||||
<li>
|
<li>
|
||||||
Select Date
|
Select Date
|
||||||
@@ -114,7 +113,7 @@ if (!empty($bannerImages)) {
|
|||||||
</select>
|
</select>
|
||||||
</li>
|
</li>
|
||||||
<?php
|
<?php
|
||||||
if ($is_member || $pending_member) {
|
if ($is_member) {
|
||||||
echo '
|
echo '
|
||||||
<li>
|
<li>
|
||||||
Additional Members <span class="price"></span>
|
Additional Members <span class="price"></span>
|
||||||
@@ -169,16 +168,8 @@ if (!empty($bannerImages)) {
|
|||||||
<label for="agreeCheckbox" id="agreeLabel" style="color: #888;">I have read and agree to the indemnity terms</label>
|
<label for="agreeCheckbox" id="agreeLabel" style="color: #888;">I have read and agree to the indemnity terms</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<button type="submit" class="theme-btn style-two w-100 mt-15 mb-5">
|
||||||
$button_text = "Book Now";
|
<span data-hover="Book Now">Book Now</span>
|
||||||
$button_disabled = "";
|
|
||||||
if (!$result || $result->num_rows == 0) {
|
|
||||||
$button_text = "No booking dates available";
|
|
||||||
$button_disabled = "disabled";
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<button type="submit" class="theme-btn style-two w-100 mt-15 mb-5" <?php echo $button_disabled; ?>>
|
|
||||||
<span data-hover="<?php echo $button_text; ?>"><?php echo $button_text; ?></span>
|
|
||||||
<i class="fal fa-arrow-right"></i>
|
<i class="fal fa-arrow-right"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
@@ -299,7 +290,6 @@ if (!empty($bannerImages)) {
|
|||||||
|
|
||||||
// Fetch PHP variables
|
// Fetch PHP variables
|
||||||
var isMember = <?php echo $is_member ? 'true' : 'false'; ?>;
|
var isMember = <?php echo $is_member ? 'true' : 'false'; ?>;
|
||||||
var pendingMember = <?php echo $pending_member ? 'true' : 'false'; ?>;
|
|
||||||
var cost_members = <?= getPrice('rescue_recovery', 'member'); ?>;
|
var cost_members = <?= getPrice('rescue_recovery', 'member'); ?>;
|
||||||
var cost_nonmembers = <?= getPrice('rescue_recovery', 'nonmember'); ?>;
|
var cost_nonmembers = <?= getPrice('rescue_recovery', 'nonmember'); ?>;
|
||||||
|
|
||||||
@@ -307,7 +297,7 @@ if (!empty($bannerImages)) {
|
|||||||
var total = 0;
|
var total = 0;
|
||||||
|
|
||||||
// Calculate cost for members
|
// Calculate cost for members
|
||||||
if (isMember || pendingMember) {
|
if (isMember) {
|
||||||
total = (cost_members) + (members * cost_members) + (nonmembers * cost_nonmembers);
|
total = (cost_members) + (members * cost_members) + (nonmembers * cost_nonmembers);
|
||||||
} else {
|
} else {
|
||||||
// Calculate cost for non-members
|
// Calculate cost for non-members
|
||||||
|
|||||||
@@ -3,21 +3,9 @@ require_once("env.php");
|
|||||||
require_once("connection.php");
|
require_once("connection.php");
|
||||||
require_once("functions.php");
|
require_once("functions.php");
|
||||||
|
|
||||||
use Middleware\RateLimitMiddleware;
|
|
||||||
|
|
||||||
$response = array('status' => 'error', 'message' => 'Something went wrong');
|
$response = array('status' => 'error', 'message' => 'Something went wrong');
|
||||||
|
|
||||||
if (isset($_POST['email'])) {
|
if (isset($_POST['email'])) {
|
||||||
// Check rate limit first (3 attempts per 30 minutes to prevent abuse)
|
|
||||||
if (RateLimitMiddleware::isLimited('password_reset', 3, 1800)) {
|
|
||||||
$remaining = RateLimitMiddleware::getTimeRemaining('password_reset', 1800);
|
|
||||||
$response['status'] = 'error';
|
|
||||||
$response['message'] = "Too many password reset requests. Please try again in {$remaining} seconds.";
|
|
||||||
$response['retry_after'] = $remaining;
|
|
||||||
echo json_encode($response);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
$email = $_POST['email'];
|
$email = $_POST['email'];
|
||||||
|
|
||||||
// Check if the email exists
|
// Check if the email exists
|
||||||
@@ -35,7 +23,7 @@ if (isset($_POST['email'])) {
|
|||||||
$token = bin2hex(random_bytes(50));
|
$token = bin2hex(random_bytes(50));
|
||||||
|
|
||||||
// Store the token and expiration time in the database
|
// Store the token and expiration time in the database
|
||||||
$expiry = date("Y-m-d H:i:s", strtotime('+3 hour')); // Token expires in 3 hour
|
$expiry = date("Y-m-d H:i:s", strtotime('+3 hour')); // Token expires in 1 hour
|
||||||
$sql = "INSERT INTO password_resets (user_id, token, expires_at) VALUES (?, ?, ?)
|
$sql = "INSERT INTO password_resets (user_id, token, expires_at) VALUES (?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE token = VALUES(token), expires_at = VALUES(expires_at)";
|
ON DUPLICATE KEY UPDATE token = VALUES(token), expires_at = VALUES(expires_at)";
|
||||||
$stmt = $conn->prepare($sql);
|
$stmt = $conn->prepare($sql);
|
||||||
@@ -48,14 +36,9 @@ if (isset($_POST['email'])) {
|
|||||||
$message = "Click the following link to reset your password: $reset_link";
|
$message = "Click the following link to reset your password: $reset_link";
|
||||||
sendEmail($email, $subject, $message);
|
sendEmail($email, $subject, $message);
|
||||||
|
|
||||||
// Reset rate limit on successful request
|
|
||||||
RateLimitMiddleware::reset('password_reset');
|
|
||||||
|
|
||||||
$response['status'] = 'success';
|
$response['status'] = 'success';
|
||||||
$response['message'] = 'Password reset link has been sent to your email.';
|
$response['message'] = 'Password reset link has been sent to your email.';
|
||||||
} else {
|
} else {
|
||||||
// Increment rate limit even for non-existent emails (prevent email enumeration)
|
|
||||||
RateLimitMiddleware::incrementAttempt('password_reset', 1800);
|
|
||||||
$response['message'] = 'Email not found.';
|
$response['message'] = 'Email not found.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Middleware;
|
|
||||||
|
|
||||||
use Services\AuthenticationService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CsrfMiddleware - CSRF Token Protection
|
|
||||||
*
|
|
||||||
* Provides helper methods for CSRF token generation and validation.
|
|
||||||
* Use in conjunction with AuthenticationService for token management.
|
|
||||||
*
|
|
||||||
* Usage in forms:
|
|
||||||
* <input type="hidden" name="csrf_token" value="<?php echo CsrfMiddleware::getToken(); ?>">
|
|
||||||
*
|
|
||||||
* Usage in processors:
|
|
||||||
* if (!CsrfMiddleware::validateToken($_POST['csrf_token'] ?? '')) {
|
|
||||||
* die('Invalid request');
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
class CsrfMiddleware
|
|
||||||
{
|
|
||||||
const TOKEN_FIELD = 'csrf_token';
|
|
||||||
const TOKEN_SESSION_KEY = 'csrf_token';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current CSRF token, generate if missing
|
|
||||||
* Safe to call multiple times
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public static function getToken(): string
|
|
||||||
{
|
|
||||||
return AuthenticationService::generateCsrfToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate CSRF token from form submission
|
|
||||||
*
|
|
||||||
* @param string $token
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function validateToken(string $token): bool
|
|
||||||
{
|
|
||||||
return AuthenticationService::validateCsrfToken($token);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Require valid CSRF token, dies if invalid
|
|
||||||
* Use at start of POST processor
|
|
||||||
*
|
|
||||||
* @param array $data Usually $_POST
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function requireToken(array $data): void
|
|
||||||
{
|
|
||||||
$token = $data[self::TOKEN_FIELD] ?? '';
|
|
||||||
|
|
||||||
if (!self::validateToken($token)) {
|
|
||||||
http_response_code(403);
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
echo json_encode([
|
|
||||||
'status' => 'error',
|
|
||||||
'message' => 'Invalid or missing security token. Please try again.'
|
|
||||||
]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get hidden HTML input field for forms
|
|
||||||
*
|
|
||||||
* @return string HTML input element
|
|
||||||
*/
|
|
||||||
public static function getInputField(): string
|
|
||||||
{
|
|
||||||
$token = self::getToken();
|
|
||||||
return '<input type="hidden" name="' . self::TOKEN_FIELD . '" value="' . htmlspecialchars($token) . '">';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Regenerate token (useful for one-time use tokens)
|
|
||||||
* Warning: Will invalidate previous token
|
|
||||||
*
|
|
||||||
* @return string New token
|
|
||||||
*/
|
|
||||||
public static function regenerateToken(): string
|
|
||||||
{
|
|
||||||
$_SESSION[self::TOKEN_SESSION_KEY] = bin2hex(random_bytes(32));
|
|
||||||
return $_SESSION[self::TOKEN_SESSION_KEY];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear CSRF token (call on logout)
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function clearToken(): void
|
|
||||||
{
|
|
||||||
unset($_SESSION[self::TOKEN_SESSION_KEY]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if token exists in POST data
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function hasToken(): bool
|
|
||||||
{
|
|
||||||
return isset($_POST[self::TOKEN_FIELD]) && !empty($_POST[self::TOKEN_FIELD]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get token from POST data
|
|
||||||
*
|
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
public static function getTokenFromPost(): ?string
|
|
||||||
{
|
|
||||||
return $_POST[self::TOKEN_FIELD] ?? null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Middleware;
|
|
||||||
|
|
||||||
use Services\DatabaseService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rate Limiting Middleware
|
|
||||||
*
|
|
||||||
* Provides rate limiting for sensitive endpoints like login, password reset,
|
|
||||||
* and API endpoints. Uses session-based counters with time windows.
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Time-window based rate limiting (e.g., 5 attempts per 15 minutes)
|
|
||||||
* - IP-based and user-based tracking
|
|
||||||
* - Graceful degradation if storage unavailable
|
|
||||||
* - Configurable limits per endpoint
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* RateLimitMiddleware::checkLimit('login', 5, 900); // 5 attempts per 15 mins
|
|
||||||
* RateLimitMiddleware::incrementAttempt('login');
|
|
||||||
* RateLimitMiddleware::reset('login'); // After successful attempt
|
|
||||||
*/
|
|
||||||
class RateLimitMiddleware
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Session key prefix for rate limiting counters
|
|
||||||
*/
|
|
||||||
private const RATE_LIMIT_PREFIX = '_rate_limit_';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Session key for timestamp tracking
|
|
||||||
*/
|
|
||||||
private const RATE_LIMIT_TIME_PREFIX = '_rate_limit_time_';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if client has exceeded rate limit
|
|
||||||
*
|
|
||||||
* @param string $endpoint Name of the endpoint (e.g., 'login', 'password_reset')
|
|
||||||
* @param int $maxAttempts Maximum attempts allowed
|
|
||||||
* @param int $timeWindow Time window in seconds (default: 900 = 15 minutes)
|
|
||||||
* @return bool True if limit exceeded, false if within limit
|
|
||||||
*/
|
|
||||||
public static function isLimited(
|
|
||||||
string $endpoint,
|
|
||||||
int $maxAttempts = 5,
|
|
||||||
int $timeWindow = 900
|
|
||||||
): bool {
|
|
||||||
self::startSession();
|
|
||||||
|
|
||||||
$counterKey = self::RATE_LIMIT_PREFIX . $endpoint;
|
|
||||||
$timeKey = self::RATE_LIMIT_TIME_PREFIX . $endpoint;
|
|
||||||
|
|
||||||
$currentTime = time();
|
|
||||||
$lastAttemptTime = $_SESSION[$timeKey] ?? 0;
|
|
||||||
$attempts = $_SESSION[$counterKey] ?? 0;
|
|
||||||
|
|
||||||
// Reset if time window has expired
|
|
||||||
if ($currentTime - $lastAttemptTime > $timeWindow) {
|
|
||||||
$_SESSION[$counterKey] = 0;
|
|
||||||
$_SESSION[$timeKey] = $currentTime;
|
|
||||||
return false; // Not limited (fresh window)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if limit exceeded
|
|
||||||
return $attempts >= $maxAttempts;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Increment the attempt counter for an endpoint
|
|
||||||
*
|
|
||||||
* @param string $endpoint Name of the endpoint
|
|
||||||
* @param int $timeWindow Time window in seconds (default: 900 = 15 minutes)
|
|
||||||
* @return int New attempt count
|
|
||||||
*/
|
|
||||||
public static function incrementAttempt(
|
|
||||||
string $endpoint,
|
|
||||||
int $timeWindow = 900
|
|
||||||
): int {
|
|
||||||
self::startSession();
|
|
||||||
|
|
||||||
$counterKey = self::RATE_LIMIT_PREFIX . $endpoint;
|
|
||||||
$timeKey = self::RATE_LIMIT_TIME_PREFIX . $endpoint;
|
|
||||||
|
|
||||||
$currentTime = time();
|
|
||||||
$lastAttemptTime = $_SESSION[$timeKey] ?? 0;
|
|
||||||
$attempts = $_SESSION[$counterKey] ?? 0;
|
|
||||||
|
|
||||||
// Reset if time window has expired
|
|
||||||
if ($currentTime - $lastAttemptTime > $timeWindow) {
|
|
||||||
$_SESSION[$counterKey] = 1;
|
|
||||||
$_SESSION[$timeKey] = $currentTime;
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Increment counter
|
|
||||||
$_SESSION[$counterKey] = ++$attempts;
|
|
||||||
|
|
||||||
// Update timestamp (keep initial window start)
|
|
||||||
if (!isset($_SESSION[$timeKey])) {
|
|
||||||
$_SESSION[$timeKey] = $currentTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $attempts;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get remaining attempts for an endpoint
|
|
||||||
*
|
|
||||||
* @param string $endpoint Name of the endpoint
|
|
||||||
* @param int $maxAttempts Maximum attempts allowed
|
|
||||||
* @param int $timeWindow Time window in seconds
|
|
||||||
* @return int Number of remaining attempts (0 if limit exceeded)
|
|
||||||
*/
|
|
||||||
public static function getRemainingAttempts(
|
|
||||||
string $endpoint,
|
|
||||||
int $maxAttempts = 5,
|
|
||||||
int $timeWindow = 900
|
|
||||||
): int {
|
|
||||||
self::startSession();
|
|
||||||
|
|
||||||
$counterKey = self::RATE_LIMIT_PREFIX . $endpoint;
|
|
||||||
$timeKey = self::RATE_LIMIT_TIME_PREFIX . $endpoint;
|
|
||||||
|
|
||||||
$currentTime = time();
|
|
||||||
$lastAttemptTime = $_SESSION[$timeKey] ?? 0;
|
|
||||||
$attempts = $_SESSION[$counterKey] ?? 0;
|
|
||||||
|
|
||||||
// Reset if time window has expired
|
|
||||||
if ($currentTime - $lastAttemptTime > $timeWindow) {
|
|
||||||
return $maxAttempts;
|
|
||||||
}
|
|
||||||
|
|
||||||
return max(0, $maxAttempts - $attempts);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get seconds remaining in the current time window
|
|
||||||
*
|
|
||||||
* @param string $endpoint Name of the endpoint
|
|
||||||
* @param int $timeWindow Time window in seconds
|
|
||||||
* @return int Seconds remaining in window
|
|
||||||
*/
|
|
||||||
public static function getTimeRemaining(
|
|
||||||
string $endpoint,
|
|
||||||
int $timeWindow = 900
|
|
||||||
): int {
|
|
||||||
self::startSession();
|
|
||||||
|
|
||||||
$timeKey = self::RATE_LIMIT_TIME_PREFIX . $endpoint;
|
|
||||||
|
|
||||||
$currentTime = time();
|
|
||||||
$lastAttemptTime = $_SESSION[$timeKey] ?? 0;
|
|
||||||
|
|
||||||
if ($lastAttemptTime === 0) {
|
|
||||||
return $timeWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
$elapsed = $currentTime - $lastAttemptTime;
|
|
||||||
|
|
||||||
if ($elapsed >= $timeWindow) {
|
|
||||||
return $timeWindow; // Window expired, new window starts
|
|
||||||
}
|
|
||||||
|
|
||||||
return $timeWindow - $elapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset the rate limit counter for an endpoint
|
|
||||||
* Call this after successful operation (e.g., after successful login)
|
|
||||||
*
|
|
||||||
* @param string $endpoint Name of the endpoint
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function reset(string $endpoint): void
|
|
||||||
{
|
|
||||||
self::startSession();
|
|
||||||
|
|
||||||
$counterKey = self::RATE_LIMIT_PREFIX . $endpoint;
|
|
||||||
$timeKey = self::RATE_LIMIT_TIME_PREFIX . $endpoint;
|
|
||||||
|
|
||||||
unset($_SESSION[$counterKey]);
|
|
||||||
unset($_SESSION[$timeKey]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check limit and throw exception if exceeded
|
|
||||||
* Dies immediately with message if limit is reached
|
|
||||||
*
|
|
||||||
* @param string $endpoint Name of the endpoint
|
|
||||||
* @param int $maxAttempts Maximum attempts allowed
|
|
||||||
* @param int $timeWindow Time window in seconds
|
|
||||||
* @param string $customMessage Optional custom error message
|
|
||||||
* @return void Dies if limit exceeded
|
|
||||||
*/
|
|
||||||
public static function requireLimit(
|
|
||||||
string $endpoint,
|
|
||||||
int $maxAttempts = 5,
|
|
||||||
int $timeWindow = 900,
|
|
||||||
string $customMessage = null
|
|
||||||
): void {
|
|
||||||
if (self::isLimited($endpoint, $maxAttempts, $timeWindow)) {
|
|
||||||
$remaining = self::getTimeRemaining($endpoint, $timeWindow);
|
|
||||||
|
|
||||||
$message = $customMessage ?? sprintf(
|
|
||||||
'Too many attempts. Please try again in %d seconds.',
|
|
||||||
$remaining
|
|
||||||
);
|
|
||||||
|
|
||||||
if (self::isAjaxRequest()) {
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
http_response_code(429); // Too Many Requests
|
|
||||||
die(json_encode([
|
|
||||||
'status' => 'error',
|
|
||||||
'message' => $message,
|
|
||||||
'retry_after' => $remaining
|
|
||||||
]));
|
|
||||||
} else {
|
|
||||||
http_response_code(429);
|
|
||||||
die("<h1>Too Many Requests</h1><p>$message</p>");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if request is AJAX
|
|
||||||
*
|
|
||||||
* @return bool True if request is AJAX
|
|
||||||
*/
|
|
||||||
private static function isAjaxRequest(): bool
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
|
||||||
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'
|
|
||||||
) || (
|
|
||||||
isset($_SERVER['CONTENT_TYPE']) &&
|
|
||||||
strpos($_SERVER['CONTENT_TYPE'], 'application/json') !== false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start session if not already started
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
private static function startSession(): void
|
|
||||||
{
|
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
|
||||||
session_start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get rate limit status for an endpoint
|
|
||||||
* Useful for logging and monitoring
|
|
||||||
*
|
|
||||||
* @param string $endpoint Name of the endpoint
|
|
||||||
* @param int $maxAttempts Maximum attempts allowed
|
|
||||||
* @param int $timeWindow Time window in seconds
|
|
||||||
* @return array Status array with keys: attempts, max_attempts, remaining, time_remaining, limited
|
|
||||||
*/
|
|
||||||
public static function getStatus(
|
|
||||||
string $endpoint,
|
|
||||||
int $maxAttempts = 5,
|
|
||||||
int $timeWindow = 900
|
|
||||||
): array {
|
|
||||||
self::startSession();
|
|
||||||
|
|
||||||
$counterKey = self::RATE_LIMIT_PREFIX . $endpoint;
|
|
||||||
$attempts = $_SESSION[$counterKey] ?? 0;
|
|
||||||
$remaining = self::getRemainingAttempts($endpoint, $maxAttempts, $timeWindow);
|
|
||||||
$timeRemaining = self::getTimeRemaining($endpoint, $timeWindow);
|
|
||||||
$isLimited = self::isLimited($endpoint, $maxAttempts, $timeWindow);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'endpoint' => $endpoint,
|
|
||||||
'attempts' => $attempts,
|
|
||||||
'max_attempts' => $maxAttempts,
|
|
||||||
'remaining' => $remaining,
|
|
||||||
'time_remaining' => $timeRemaining,
|
|
||||||
'limited' => $isLimited
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Services;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AuthenticationService - Consolidated authentication and authorization
|
|
||||||
* Replaces: checkAdmin, checkSuperAdmin, and adds session regeneration + CSRF
|
|
||||||
*/
|
|
||||||
class AuthenticationService
|
|
||||||
{
|
|
||||||
private DatabaseService $db;
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->db = DatabaseService::getInstance();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate CSRF token for form protection
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public static function generateCsrfToken(): string
|
|
||||||
{
|
|
||||||
if (!isset($_SESSION['csrf_token'])) {
|
|
||||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
|
||||||
}
|
|
||||||
return $_SESSION['csrf_token'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate CSRF token
|
|
||||||
*
|
|
||||||
* @param string $token
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function validateCsrfToken(string $token): bool
|
|
||||||
{
|
|
||||||
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Regenerate session ID after login
|
|
||||||
* Prevents session fixation attacks
|
|
||||||
*/
|
|
||||||
public static function regenerateSession(): void
|
|
||||||
{
|
|
||||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
|
||||||
session_regenerate_id(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if user is logged in
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function isLoggedIn(): bool
|
|
||||||
{
|
|
||||||
return isset($_SESSION['user_id']) && !empty($_SESSION['user_id']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if user is admin
|
|
||||||
* Redirects to login if not authorized
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function requireAdmin(): bool
|
|
||||||
{
|
|
||||||
if (!$this->isLoggedIn()) {
|
|
||||||
header("Location: login.php");
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->hasAdminRole($_SESSION['user_id'])) {
|
|
||||||
http_response_code(403);
|
|
||||||
die("Access denied. Admin privileges required.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if user is superadmin
|
|
||||||
* Redirects to login if not authorized
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function requireSuperAdmin(): bool
|
|
||||||
{
|
|
||||||
if (!$this->isLoggedIn()) {
|
|
||||||
header("Location: login.php");
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->hasSuperAdminRole($_SESSION['user_id'])) {
|
|
||||||
http_response_code(403);
|
|
||||||
die("Access denied. Super Admin privileges required.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if user has admin role
|
|
||||||
*
|
|
||||||
* @param int $userId
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
private function hasAdminRole(int $userId): bool
|
|
||||||
{
|
|
||||||
$conn = $this->db->getConnection();
|
|
||||||
$stmt = $conn->prepare("SELECT role FROM users WHERE user_id = ? LIMIT 1");
|
|
||||||
|
|
||||||
if (!$stmt) {
|
|
||||||
error_log("AuthenticationService::hasAdminRole prepare error: " . $conn->error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt->bind_param('i', $userId);
|
|
||||||
$stmt->execute();
|
|
||||||
$stmt->bind_result($role);
|
|
||||||
$stmt->fetch();
|
|
||||||
$stmt->close();
|
|
||||||
|
|
||||||
return in_array($role, ['admin', 'superadmin'], true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if user has superadmin role
|
|
||||||
*
|
|
||||||
* @param int $userId
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
private function hasSuperAdminRole(int $userId): bool
|
|
||||||
{
|
|
||||||
$conn = $this->db->getConnection();
|
|
||||||
$stmt = $conn->prepare("SELECT role FROM users WHERE user_id = ? LIMIT 1");
|
|
||||||
|
|
||||||
if (!$stmt) {
|
|
||||||
error_log("AuthenticationService::hasSuperAdminRole prepare error: " . $conn->error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt->bind_param('i', $userId);
|
|
||||||
$stmt->execute();
|
|
||||||
$stmt->bind_result($role);
|
|
||||||
$stmt->fetch();
|
|
||||||
$stmt->close();
|
|
||||||
|
|
||||||
return $role === 'superadmin';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current user role
|
|
||||||
*
|
|
||||||
* @param int $userId
|
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
public function getUserRole(int $userId): ?string
|
|
||||||
{
|
|
||||||
$conn = $this->db->getConnection();
|
|
||||||
$stmt = $conn->prepare("SELECT role FROM users WHERE user_id = ? LIMIT 1");
|
|
||||||
|
|
||||||
if (!$stmt) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt->bind_param('i', $userId);
|
|
||||||
$stmt->execute();
|
|
||||||
$stmt->bind_result($role);
|
|
||||||
$stmt->fetch();
|
|
||||||
$stmt->close();
|
|
||||||
|
|
||||||
return $role;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log user out and destroy session
|
|
||||||
*/
|
|
||||||
public static function logout(): void
|
|
||||||
{
|
|
||||||
session_destroy();
|
|
||||||
setcookie('PHPSESSID', '', time() - 3600, '/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Services;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DatabaseService - Singleton pattern for database connection pooling
|
|
||||||
* Eliminates repeated database connection creation/closure overhead
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* $conn = DatabaseService::getInstance();
|
|
||||||
* $result = $conn->query("SELECT ...");
|
|
||||||
*/
|
|
||||||
class DatabaseService
|
|
||||||
{
|
|
||||||
private static ?self $instance = null;
|
|
||||||
private \mysqli $connection;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Private constructor to prevent direct instantiation
|
|
||||||
*/
|
|
||||||
private function __construct()
|
|
||||||
{
|
|
||||||
$this->connection = $this->connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get singleton instance
|
|
||||||
*
|
|
||||||
* @return DatabaseService
|
|
||||||
*/
|
|
||||||
public static function getInstance(): self
|
|
||||||
{
|
|
||||||
if (self::$instance === null) {
|
|
||||||
self::$instance = new self();
|
|
||||||
}
|
|
||||||
return self::$instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Establish database connection
|
|
||||||
*
|
|
||||||
* @return \mysqli
|
|
||||||
* @throws \Exception
|
|
||||||
*/
|
|
||||||
private function connect(): \mysqli
|
|
||||||
{
|
|
||||||
$dbhost = $_ENV['DB_HOST'] ?? 'localhost';
|
|
||||||
$dbuser = $_ENV['DB_USER'] ?? 'root';
|
|
||||||
$dbpass = $_ENV['DB_PASS'] ?? '';
|
|
||||||
$dbname = $_ENV['DB_NAME'] ?? '4wdcsa';
|
|
||||||
|
|
||||||
$conn = new \mysqli($dbhost, $dbuser, $dbpass, $dbname);
|
|
||||||
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
error_log("Database connection failed: " . $conn->connect_error);
|
|
||||||
throw new \Exception("Database connection failed", 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set charset to utf8mb4
|
|
||||||
$conn->set_charset("utf8mb4");
|
|
||||||
|
|
||||||
return $conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the MySQLi connection object
|
|
||||||
* Allows direct access to connection for backward compatibility
|
|
||||||
*
|
|
||||||
* @return \mysqli
|
|
||||||
*/
|
|
||||||
public function getConnection(): \mysqli
|
|
||||||
{
|
|
||||||
return $this->connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a query (for backward compatibility with existing code)
|
|
||||||
*
|
|
||||||
* @param string $sql
|
|
||||||
* @return \mysqli_result|bool
|
|
||||||
*/
|
|
||||||
public function query(string $sql)
|
|
||||||
{
|
|
||||||
return $this->connection->query($sql);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepare a statement
|
|
||||||
*
|
|
||||||
* @param string $sql
|
|
||||||
* @return \mysqli_stmt|false
|
|
||||||
*/
|
|
||||||
public function prepare(string $sql)
|
|
||||||
{
|
|
||||||
return $this->connection->prepare($sql);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Escape string
|
|
||||||
*
|
|
||||||
* @param string $string
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function escapeString(string $string): string
|
|
||||||
{
|
|
||||||
return $this->connection->real_escape_string($string);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get last insert ID
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getLastInsertId(): int
|
|
||||||
{
|
|
||||||
return $this->connection->insert_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the number of affected rows
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getAffectedRows(): int
|
|
||||||
{
|
|
||||||
return $this->connection->affected_rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Begin a transaction
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function beginTransaction(): bool
|
|
||||||
{
|
|
||||||
return $this->connection->begin_transaction();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Commit a transaction
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function commit(): bool
|
|
||||||
{
|
|
||||||
return $this->connection->commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rollback a transaction
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function rollback(): bool
|
|
||||||
{
|
|
||||||
return $this->connection->rollback();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get error message
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getError(): string
|
|
||||||
{
|
|
||||||
return $this->connection->error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close connection (cleanup, rarely needed with singleton)
|
|
||||||
*/
|
|
||||||
public function closeConnection(): void
|
|
||||||
{
|
|
||||||
if ($this->connection) {
|
|
||||||
$this->connection->close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prevent cloning
|
|
||||||
*/
|
|
||||||
private function __clone() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prevent unserialize
|
|
||||||
*/
|
|
||||||
public function __wakeup()
|
|
||||||
{
|
|
||||||
throw new \Exception("Cannot unserialize DatabaseService");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Services;
|
|
||||||
|
|
||||||
use GuzzleHttp\Client;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* EmailService - Consolidated email management
|
|
||||||
* Eliminates 240 lines of duplicate Mailjet code across 6 separate functions
|
|
||||||
*
|
|
||||||
* Replaces: sendVerificationEmail, sendInvoice, sendPOP, sendEmail,
|
|
||||||
* sendAdminNotification, sendPaymentConfirmation
|
|
||||||
*/
|
|
||||||
class EmailService
|
|
||||||
{
|
|
||||||
private Client $client;
|
|
||||||
private string $apiKey;
|
|
||||||
private string $apiSecret;
|
|
||||||
private string $fromEmail;
|
|
||||||
private string $fromName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize EmailService with Mailjet credentials
|
|
||||||
*/
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->apiKey = $_ENV['MAILJET_API_KEY'] ?? '';
|
|
||||||
$this->apiSecret = $_ENV['MAILJET_API_SECRET'] ?? '';
|
|
||||||
$this->fromEmail = $_ENV['MAILJET_FROM_EMAIL'] ?? 'info@4wdcsa.co.za';
|
|
||||||
$this->fromName = $_ENV['MAILJET_FROM_NAME'] ?? '4WDCSA';
|
|
||||||
|
|
||||||
$this->client = new Client([
|
|
||||||
'base_uri' => 'https://api.mailjet.com/v3.1/',
|
|
||||||
'timeout' => 30,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Validate credentials are set
|
|
||||||
if (!$this->apiKey || !$this->apiSecret) {
|
|
||||||
error_log("EmailService: Mailjet credentials not configured in .env file");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send email using Mailjet template
|
|
||||||
*
|
|
||||||
* @param string $recipientEmail
|
|
||||||
* @param string $recipientName
|
|
||||||
* @param int $templateId
|
|
||||||
* @param array $variables
|
|
||||||
* @param string|null $subject
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function sendTemplate(
|
|
||||||
string $recipientEmail,
|
|
||||||
string $recipientName,
|
|
||||||
int $templateId,
|
|
||||||
array $variables = [],
|
|
||||||
?string $subject = null
|
|
||||||
): bool {
|
|
||||||
$message = [
|
|
||||||
'Messages' => [
|
|
||||||
[
|
|
||||||
'From' => [
|
|
||||||
'Email' => $this->fromEmail,
|
|
||||||
'Name' => $this->fromName
|
|
||||||
],
|
|
||||||
'To' => [
|
|
||||||
[
|
|
||||||
'Email' => $recipientEmail,
|
|
||||||
'Name' => $recipientName
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'TemplateID' => $templateId,
|
|
||||||
'TemplateLanguage' => true,
|
|
||||||
'Variables' => $variables
|
|
||||||
]
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
// Add subject if provided
|
|
||||||
if ($subject) {
|
|
||||||
$message['Messages'][0]['Subject'] = $subject;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->send($message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send custom email (not using template)
|
|
||||||
*
|
|
||||||
* @param string $recipientEmail
|
|
||||||
* @param string $recipientName
|
|
||||||
* @param string $subject
|
|
||||||
* @param string $htmlContent
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function sendCustom(
|
|
||||||
string $recipientEmail,
|
|
||||||
string $recipientName,
|
|
||||||
string $subject,
|
|
||||||
string $htmlContent
|
|
||||||
): bool {
|
|
||||||
$message = [
|
|
||||||
'Messages' => [
|
|
||||||
[
|
|
||||||
'From' => [
|
|
||||||
'Email' => $this->fromEmail,
|
|
||||||
'Name' => $this->fromName
|
|
||||||
],
|
|
||||||
'To' => [
|
|
||||||
[
|
|
||||||
'Email' => $recipientEmail,
|
|
||||||
'Name' => $recipientName
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'Subject' => $subject,
|
|
||||||
'HTMLPart' => $htmlContent
|
|
||||||
]
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
return $this->send($message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consolidated email sending method
|
|
||||||
*
|
|
||||||
* @param array $message
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
private function send(array $message): bool
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$response = $this->client->request('POST', 'send', [
|
|
||||||
'json' => $message,
|
|
||||||
'auth' => [$this->apiKey, $this->apiSecret]
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($response->getStatusCode() === 200) {
|
|
||||||
$body = json_decode($response->getBody());
|
|
||||||
if (!empty($body->Messages) && $body->Messages[0]->Status === 'success') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
error_log("EmailService error: " . $e->getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send verification email
|
|
||||||
*
|
|
||||||
* @param string $email
|
|
||||||
* @param string $name
|
|
||||||
* @param string $token
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function sendVerificationEmail(string $email, string $name, string $token): bool
|
|
||||||
{
|
|
||||||
return $this->sendTemplate(
|
|
||||||
$email,
|
|
||||||
$name,
|
|
||||||
6689736, // Template ID
|
|
||||||
[
|
|
||||||
'token' => $token,
|
|
||||||
'first_name' => $name
|
|
||||||
],
|
|
||||||
"4WDCSA - Verify your Email"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send invoice/booking confirmation
|
|
||||||
*
|
|
||||||
* @param string $email
|
|
||||||
* @param string $name
|
|
||||||
* @param string $eftId
|
|
||||||
* @param float $amount
|
|
||||||
* @param string $description
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function sendInvoice(string $email, string $name, string $eftId, float $amount, string $description): bool
|
|
||||||
{
|
|
||||||
return $this->sendTemplate(
|
|
||||||
$email,
|
|
||||||
$name,
|
|
||||||
6891432, // Template ID
|
|
||||||
[
|
|
||||||
'eft_id' => $eftId,
|
|
||||||
'amount' => number_format($amount, 2),
|
|
||||||
'description' => $description,
|
|
||||||
],
|
|
||||||
"4WDCSA - Thank you for your booking."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send POP (Proof of Payment) email
|
|
||||||
*
|
|
||||||
* @param string $email
|
|
||||||
* @param string $name
|
|
||||||
* @param string $popId
|
|
||||||
* @param string $amount
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function sendPOP(string $email, string $name, string $popId, string $amount): bool
|
|
||||||
{
|
|
||||||
return $this->sendTemplate(
|
|
||||||
$email,
|
|
||||||
$name,
|
|
||||||
6891432, // Template ID - can be customized
|
|
||||||
[
|
|
||||||
'pop_id' => $popId,
|
|
||||||
'amount' => $amount,
|
|
||||||
],
|
|
||||||
"4WDCSA - Proof of Payment"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send admin notification
|
|
||||||
*
|
|
||||||
* @param string $subject
|
|
||||||
* @param string $message
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function sendAdminNotification(string $subject, string $message): bool
|
|
||||||
{
|
|
||||||
$adminEmail = $_ENV['ADMIN_EMAIL'] ?? 'admin@4wdcsa.co.za';
|
|
||||||
|
|
||||||
return $this->sendCustom(
|
|
||||||
$adminEmail,
|
|
||||||
'Administrator',
|
|
||||||
$subject,
|
|
||||||
"<p>" . nl2br(htmlspecialchars($message)) . "</p>"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send payment confirmation
|
|
||||||
*
|
|
||||||
* @param string $email
|
|
||||||
* @param string $name
|
|
||||||
* @param string $paymentId
|
|
||||||
* @param float $amount
|
|
||||||
* @param string $description
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function sendPaymentConfirmation(string $email, string $name, string $paymentId, float $amount, string $description): bool
|
|
||||||
{
|
|
||||||
return $this->sendTemplate(
|
|
||||||
$email,
|
|
||||||
$name,
|
|
||||||
6891432, // Template ID
|
|
||||||
[
|
|
||||||
'payment_id' => $paymentId,
|
|
||||||
'amount' => number_format($amount, 2),
|
|
||||||
'description' => $description,
|
|
||||||
],
|
|
||||||
"4WDCSA - Payment Confirmation"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Services;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PaymentService - Consolidated payment processing
|
|
||||||
* Eliminates 300+ lines of duplicate PayFast code across 4 separate functions
|
|
||||||
*
|
|
||||||
* Replaces: processPayment, processMembershipPayment, processPaymentTest, processZeroPayment
|
|
||||||
*/
|
|
||||||
class PaymentService
|
|
||||||
{
|
|
||||||
private DatabaseService $db;
|
|
||||||
private string $merchantId;
|
|
||||||
private string $merchantKey;
|
|
||||||
private string $passPhrase;
|
|
||||||
private string $domain;
|
|
||||||
private bool $testingMode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize PaymentService with PayFast credentials
|
|
||||||
*/
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->db = DatabaseService::getInstance();
|
|
||||||
$this->merchantId = $_ENV['PAYFAST_MERCHANT_ID'] ?? '10021495';
|
|
||||||
$this->merchantKey = $_ENV['PAYFAST_MERCHANT_KEY'] ?? '';
|
|
||||||
$this->passPhrase = $_ENV['PAYFAST_PASSPHRASE'] ?? '';
|
|
||||||
$this->domain = $_ENV['PAYFAST_DOMAIN'] ?? 'www.thepinto.co.za/4wdcsa';
|
|
||||||
$this->testingMode = ($_ENV['PAYFAST_TESTING_MODE'] ?? 'true') === 'true';
|
|
||||||
|
|
||||||
if (!$this->merchantKey || !$this->passPhrase) {
|
|
||||||
error_log("PaymentService: PayFast credentials not fully configured");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process booking payment via PayFast
|
|
||||||
*
|
|
||||||
* @param string $paymentId
|
|
||||||
* @param float $amount
|
|
||||||
* @param string $description
|
|
||||||
* @param string $returnUrl
|
|
||||||
* @param string $cancelUrl
|
|
||||||
* @param string $notifyUrl
|
|
||||||
* @param array $userInfo
|
|
||||||
* @return string HTML form to redirect to PayFast
|
|
||||||
*/
|
|
||||||
public function processBookingPayment(
|
|
||||||
string $paymentId,
|
|
||||||
float $amount,
|
|
||||||
string $description,
|
|
||||||
string $returnUrl,
|
|
||||||
string $cancelUrl,
|
|
||||||
string $notifyUrl,
|
|
||||||
array $userInfo
|
|
||||||
): string {
|
|
||||||
// Insert payment record
|
|
||||||
$this->insertPayment($paymentId, $userInfo['user_id'], $amount, 'AWAITING PAYMENT', $description);
|
|
||||||
|
|
||||||
// Generate PayFast form
|
|
||||||
return $this->generatePayFastForm(
|
|
||||||
$paymentId,
|
|
||||||
$amount,
|
|
||||||
$description,
|
|
||||||
$returnUrl,
|
|
||||||
$cancelUrl,
|
|
||||||
$notifyUrl,
|
|
||||||
$userInfo
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process membership payment via PayFast
|
|
||||||
*
|
|
||||||
* @param string $paymentId
|
|
||||||
* @param float $amount
|
|
||||||
* @param string $description
|
|
||||||
* @param array $userInfo
|
|
||||||
* @return string HTML form
|
|
||||||
*/
|
|
||||||
public function processMembershipPayment(
|
|
||||||
string $paymentId,
|
|
||||||
float $amount,
|
|
||||||
string $description,
|
|
||||||
array $userInfo
|
|
||||||
): string {
|
|
||||||
// Insert payment record
|
|
||||||
$this->insertPayment($paymentId, $userInfo['user_id'], $amount, 'AWAITING PAYMENT', $description);
|
|
||||||
|
|
||||||
// Generate PayFast form with membership-specific URLs
|
|
||||||
return $this->generatePayFastForm(
|
|
||||||
$paymentId,
|
|
||||||
$amount,
|
|
||||||
$description,
|
|
||||||
'https://' . $this->domain . '/account_settings.php',
|
|
||||||
'https://' . $this->domain . '/cancel_application.php?id=' . base64_encode($paymentId),
|
|
||||||
'https://' . $this->domain . '/confirm2.php',
|
|
||||||
$userInfo
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process test/immediate payment (marks as PAID without PayFast)
|
|
||||||
*
|
|
||||||
* @param string $paymentId
|
|
||||||
* @param float $amount
|
|
||||||
* @param string $description
|
|
||||||
* @param int $userId
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function processTestPayment(
|
|
||||||
string $paymentId,
|
|
||||||
float $amount,
|
|
||||||
string $description,
|
|
||||||
int $userId
|
|
||||||
): bool {
|
|
||||||
try {
|
|
||||||
// Insert payment record as PAID
|
|
||||||
$this->insertPayment($paymentId, $userId, $amount, 'PAID', $description);
|
|
||||||
|
|
||||||
// Update booking status to PAID
|
|
||||||
return $this->updateBookingStatus($paymentId, 'PAID');
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
error_log("PaymentService::processTestPayment error: " . $e->getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process zero-amount payment (free booking)
|
|
||||||
*
|
|
||||||
* @param string $paymentId
|
|
||||||
* @param string $description
|
|
||||||
* @param int $userId
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function processZeroPayment(
|
|
||||||
string $paymentId,
|
|
||||||
string $description,
|
|
||||||
int $userId
|
|
||||||
): bool {
|
|
||||||
try {
|
|
||||||
// Insert payment record
|
|
||||||
$this->insertPayment($paymentId, $userId, 0, 'PAID', $description);
|
|
||||||
|
|
||||||
// Update booking status to PAID
|
|
||||||
return $this->updateBookingStatus($paymentId, 'PAID');
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
error_log("PaymentService::processZeroPayment error: " . $e->getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert payment record into database
|
|
||||||
*
|
|
||||||
* @param string $paymentId
|
|
||||||
* @param int $userId
|
|
||||||
* @param float $amount
|
|
||||||
* @param string $status
|
|
||||||
* @param string $description
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
private function insertPayment(
|
|
||||||
string $paymentId,
|
|
||||||
int $userId,
|
|
||||||
float $amount,
|
|
||||||
string $status,
|
|
||||||
string $description
|
|
||||||
): bool {
|
|
||||||
$conn = $this->db->getConnection();
|
|
||||||
$stmt = $conn->prepare("
|
|
||||||
INSERT INTO payments (payment_id, user_id, amount, status, description)
|
|
||||||
VALUES (?, ?, ?, ?, ?)
|
|
||||||
");
|
|
||||||
|
|
||||||
if (!$stmt) {
|
|
||||||
error_log("PaymentService::insertPayment prepare error: " . $conn->error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt->bind_param('sidss', $paymentId, $userId, $amount, $status, $description);
|
|
||||||
|
|
||||||
if (!$stmt->execute()) {
|
|
||||||
error_log("PaymentService::insertPayment execute error: " . $stmt->error);
|
|
||||||
$stmt->close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt->close();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update booking status
|
|
||||||
*
|
|
||||||
* @param string $paymentId
|
|
||||||
* @param string $newStatus
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
private function updateBookingStatus(string $paymentId, string $newStatus): bool
|
|
||||||
{
|
|
||||||
$conn = $this->db->getConnection();
|
|
||||||
$stmt = $conn->prepare("
|
|
||||||
UPDATE bookings
|
|
||||||
SET status = ?
|
|
||||||
WHERE payment_id = ?
|
|
||||||
");
|
|
||||||
|
|
||||||
if (!$stmt) {
|
|
||||||
error_log("PaymentService::updateBookingStatus prepare error: " . $conn->error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt->bind_param('ss', $newStatus, $paymentId);
|
|
||||||
|
|
||||||
if (!$stmt->execute()) {
|
|
||||||
error_log("PaymentService::updateBookingStatus execute error: " . $stmt->error);
|
|
||||||
$stmt->close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt->close();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate PayFast payment form
|
|
||||||
*
|
|
||||||
* @param string $paymentId
|
|
||||||
* @param float $amount
|
|
||||||
* @param string $description
|
|
||||||
* @param string $returnUrl
|
|
||||||
* @param string $cancelUrl
|
|
||||||
* @param string $notifyUrl
|
|
||||||
* @param array $userInfo (user_id, first_name, last_name, email)
|
|
||||||
* @return string HTML form with auto-submit script
|
|
||||||
*/
|
|
||||||
private function generatePayFastForm(
|
|
||||||
string $paymentId,
|
|
||||||
float $amount,
|
|
||||||
string $description,
|
|
||||||
string $returnUrl,
|
|
||||||
string $cancelUrl,
|
|
||||||
string $notifyUrl,
|
|
||||||
array $userInfo
|
|
||||||
): string {
|
|
||||||
// Construct PayFast data array
|
|
||||||
$data = [
|
|
||||||
'merchant_id' => $this->merchantId,
|
|
||||||
'merchant_key' => $this->merchantKey,
|
|
||||||
'return_url' => $returnUrl,
|
|
||||||
'cancel_url' => $cancelUrl,
|
|
||||||
'notify_url' => $notifyUrl,
|
|
||||||
'name_first' => $userInfo['first_name'] ?? '',
|
|
||||||
'name_last' => $userInfo['last_name'] ?? '',
|
|
||||||
'email_address' => $userInfo['email'] ?? '',
|
|
||||||
'm_payment_id' => $paymentId,
|
|
||||||
'amount' => number_format(sprintf('%.2f', $amount), 2, '.', ''),
|
|
||||||
'item_name' => '4WDCSA: ' . htmlspecialchars($description)
|
|
||||||
];
|
|
||||||
|
|
||||||
// Generate signature
|
|
||||||
$data['signature'] = $this->generateSignature($data);
|
|
||||||
|
|
||||||
// Determine PayFast host
|
|
||||||
$pfHost = $this->testingMode ? 'sandbox.payfast.co.za' : 'www.payfast.co.za';
|
|
||||||
|
|
||||||
// Build HTML form
|
|
||||||
$html = '<form id="payfastForm" action="https://' . $pfHost . '/eng/process" method="post">';
|
|
||||||
foreach ($data as $name => $value) {
|
|
||||||
$html .= '<input name="' . htmlspecialchars($name) . '" type="hidden" value="' . htmlspecialchars($value) . '" />';
|
|
||||||
}
|
|
||||||
$html .= '</form>';
|
|
||||||
|
|
||||||
// Add auto-submit script
|
|
||||||
$html .= '<script type="text/javascript">';
|
|
||||||
$html .= 'document.getElementById("payfastForm").submit();';
|
|
||||||
$html .= '</script>';
|
|
||||||
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate PayFast signature
|
|
||||||
*
|
|
||||||
* @param array $data
|
|
||||||
* @return string MD5 hash signature
|
|
||||||
*/
|
|
||||||
private function generateSignature(array $data): string
|
|
||||||
{
|
|
||||||
// Create parameter string
|
|
||||||
$pfOutput = '';
|
|
||||||
foreach ($data as $key => $val) {
|
|
||||||
if (!empty($val)) {
|
|
||||||
$pfOutput .= $key . '=' . urlencode(trim($val)) . '&';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove last ampersand
|
|
||||||
$getString = substr($pfOutput, 0, -1);
|
|
||||||
|
|
||||||
// Add passphrase if configured
|
|
||||||
if (!empty($this->passPhrase)) {
|
|
||||||
$getString .= '&passphrase=' . urlencode(trim($this->passPhrase));
|
|
||||||
}
|
|
||||||
|
|
||||||
return md5($getString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Services;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UserService - Consolidated user information retrieval
|
|
||||||
* Eliminates 54 lines of duplicate code across 6 similar user info getter functions
|
|
||||||
*
|
|
||||||
* Replaces: getFullName, getEmail, getProfilePic, getLastName, getInitialSurname, get_user_info
|
|
||||||
*/
|
|
||||||
class UserService
|
|
||||||
{
|
|
||||||
private DatabaseService $db;
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->db = DatabaseService::getInstance();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user information by column
|
|
||||||
* Generic method to replace 6 separate getter functions
|
|
||||||
*
|
|
||||||
* @param int $userId
|
|
||||||
* @param string $column
|
|
||||||
* @return mixed|null
|
|
||||||
*/
|
|
||||||
private function getUserColumn(int $userId, string $column)
|
|
||||||
{
|
|
||||||
// Validate column name to prevent injection
|
|
||||||
$allowedColumns = ['user_id', 'first_name', 'last_name', 'email', 'phone', 'profile_pic', 'role', 'membership_status'];
|
|
||||||
|
|
||||||
if (!in_array($column, $allowedColumns, true)) {
|
|
||||||
error_log("UserService::getUserColumn - Invalid column requested: " . $column);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$conn = $this->db->getConnection();
|
|
||||||
$query = "SELECT `" . $column . "` FROM users WHERE user_id = ? LIMIT 1";
|
|
||||||
$stmt = $conn->prepare($query);
|
|
||||||
|
|
||||||
if (!$stmt) {
|
|
||||||
error_log("UserService::getUserColumn prepare error: " . $conn->error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt->bind_param('i', $userId);
|
|
||||||
$stmt->execute();
|
|
||||||
$stmt->bind_result($value);
|
|
||||||
$stmt->fetch();
|
|
||||||
$stmt->close();
|
|
||||||
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user's full name
|
|
||||||
*
|
|
||||||
* @param int $userId
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getFullName(int $userId): string
|
|
||||||
{
|
|
||||||
$firstName = $this->getUserColumn($userId, 'first_name') ?? '';
|
|
||||||
$lastName = $this->getUserColumn($userId, 'last_name') ?? '';
|
|
||||||
return trim($firstName . ' ' . $lastName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user's first name only
|
|
||||||
*
|
|
||||||
* @param int $userId
|
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
public function getFirstName(int $userId): ?string
|
|
||||||
{
|
|
||||||
return $this->getUserColumn($userId, 'first_name');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user's last name only
|
|
||||||
*
|
|
||||||
* @param int $userId
|
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
public function getLastName(int $userId): ?string
|
|
||||||
{
|
|
||||||
return $this->getUserColumn($userId, 'last_name');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get initial/first letter of surname
|
|
||||||
*
|
|
||||||
* @param int $userId
|
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
public function getInitialSurname(int $userId): ?string
|
|
||||||
{
|
|
||||||
$lastName = $this->getUserColumn($userId, 'last_name');
|
|
||||||
return $lastName ? strtoupper(substr($lastName, 0, 1)) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user email
|
|
||||||
*
|
|
||||||
* @param int $userId
|
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
public function getEmail(int $userId): ?string
|
|
||||||
{
|
|
||||||
return $this->getUserColumn($userId, 'email');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user profile picture
|
|
||||||
*
|
|
||||||
* @param int $userId
|
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
public function getProfilePic(int $userId): ?string
|
|
||||||
{
|
|
||||||
return $this->getUserColumn($userId, 'profile_pic');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user phone number
|
|
||||||
*
|
|
||||||
* @param int $userId
|
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
public function getPhone(int $userId): ?string
|
|
||||||
{
|
|
||||||
return $this->getUserColumn($userId, 'phone');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user role
|
|
||||||
*
|
|
||||||
* @param int $userId
|
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
public function getRole(int $userId): ?string
|
|
||||||
{
|
|
||||||
return $this->getUserColumn($userId, 'role');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get multiple user fields at once (more efficient than separate calls)
|
|
||||||
*
|
|
||||||
* @param int $userId
|
|
||||||
* @param array $columns
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function getUserInfo(int $userId, array $columns = ['first_name', 'last_name', 'email']): array
|
|
||||||
{
|
|
||||||
// Validate columns
|
|
||||||
$allowedColumns = ['user_id', 'first_name', 'last_name', 'email', 'phone', 'profile_pic', 'role', 'membership_status'];
|
|
||||||
$validColumns = array_intersect($columns, $allowedColumns);
|
|
||||||
|
|
||||||
if (empty($validColumns)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$conn = $this->db->getConnection();
|
|
||||||
$columnList = '`' . implode('`, `', $validColumns) . '`';
|
|
||||||
$query = "SELECT " . $columnList . " FROM users WHERE user_id = ? LIMIT 1";
|
|
||||||
|
|
||||||
$stmt = $conn->prepare($query);
|
|
||||||
|
|
||||||
if (!$stmt) {
|
|
||||||
error_log("UserService::getUserInfo prepare error: " . $conn->error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt->bind_param('i', $userId);
|
|
||||||
$stmt->execute();
|
|
||||||
$result = $stmt->get_result();
|
|
||||||
$stmt->close();
|
|
||||||
|
|
||||||
return $result->fetch_assoc() ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if user exists
|
|
||||||
*
|
|
||||||
* @param int $userId
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function userExists(int $userId): bool
|
|
||||||
{
|
|
||||||
$conn = $this->db->getConnection();
|
|
||||||
$stmt = $conn->prepare("SELECT user_id FROM users WHERE user_id = ? LIMIT 1");
|
|
||||||
|
|
||||||
if (!$stmt) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt->bind_param('i', $userId);
|
|
||||||
$stmt->execute();
|
|
||||||
$stmt->store_result();
|
|
||||||
$exists = $stmt->num_rows > 0;
|
|
||||||
$stmt->close();
|
|
||||||
|
|
||||||
return $exists;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
submit_blog.php
Normal file
24
submit_blog.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require_once("env.php");
|
||||||
|
require_once("session.php");
|
||||||
|
require_once("connection.php");
|
||||||
|
require_once("functions.php");
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
die("Login required");
|
||||||
|
}
|
||||||
|
|
||||||
|
$title = $_POST['title'];
|
||||||
|
$category = $_POST['category'];
|
||||||
|
$description = $_POST['description'];
|
||||||
|
$content = $_POST['content'];
|
||||||
|
$user_id = $_SESSION['user_id'];
|
||||||
|
$date = date('Y-m-d');
|
||||||
|
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("INSERT INTO blogs (author, title, content, description, category, date) VALUES (?, ?, ?, ?, ?, ?)");
|
||||||
|
$stmt->bind_param("isssss", $user_id, $title, $content, $description, $category, $date);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
header("Location: blog.php");
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
<?php include_once('header02.php');
|
<?php include_once('header02.php');
|
||||||
checkUserSession();
|
checkUserSession();
|
||||||
umask(002); // At the top of the PHP script, before move_uploaded_file()
|
|
||||||
|
|
||||||
|
|
||||||
$user_id = $_SESSION['user_id'] ?? null;
|
$user_id = $_SESSION['user_id'] ?? null;
|
||||||
|
|
||||||
@@ -52,7 +50,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (move_uploaded_file($file['tmp_name'], $target_file)) {
|
if (move_uploaded_file($file['tmp_name'], $target_file)) {
|
||||||
chmod($target_file, 0664);
|
|
||||||
// Update EFT and booking status
|
// Update EFT and booking status
|
||||||
$payment_type = $_POST['payment_type'] ?? 'booking';
|
$payment_type = $_POST['payment_type'] ?? 'booking';
|
||||||
|
|
||||||
@@ -76,33 +73,43 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$stmt2->execute();
|
$stmt2->execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send notification email using sendPOP()
|
// Notify n8n and send the path to the uploaded file
|
||||||
$fullname = getFullName($user_id); // Assuming this returns "First Last"
|
$webhook_url = 'https://n8n.4wdcsa.co.za/webhook/process-pop';
|
||||||
|
|
||||||
$eftDetails = getEFTDetails($eft_id);
|
|
||||||
$modified = str_replace(' ', '_', $eft_id);
|
|
||||||
|
|
||||||
if ($eftDetails) {
|
$postData = [
|
||||||
$amount = "R" . number_format($eftDetails['amount'], 2);
|
'eft_id' => $eft_id,
|
||||||
$description = $eftDetails['description'];
|
'payment_type' => $payment_type,
|
||||||
|
];
|
||||||
|
|
||||||
|
$ch = curl_init($webhook_url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Content-Type: application/json'
|
||||||
|
]);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData));
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$error = curl_error($ch);
|
||||||
|
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
error_log("Webhook Error: $error");
|
||||||
|
$_SESSION['message'] = $error;
|
||||||
|
header("Location: bookings.php");
|
||||||
|
} else {
|
||||||
|
$_SESSION['message'] = "Thank you! We are busy processing your payment!";
|
||||||
|
header("Location: bookings.php");
|
||||||
|
}
|
||||||
|
|
||||||
|
exit;
|
||||||
} else {
|
} else {
|
||||||
$amount = "R0.00";
|
echo "<div class='alert alert-danger'>Unable to move uploaded file.</div>";
|
||||||
$description = "Payment"; // fallback
|
echo "<pre>Tmp file exists? " . (file_exists($file['tmp_name']) ? "Yes" : "No") . "</pre>";
|
||||||
|
echo "<pre>Tmp file path: " . htmlspecialchars($file['tmp_name']) . "</pre>";
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (sendPOP($fullname, $modified, $amount, $description)) {
|
|
||||||
$_SESSION['message'] = "Thank you! Your payment proof has been uploaded and notification sent.";
|
|
||||||
} else {
|
|
||||||
$_SESSION['message'] = "Payment uploaded, but notification email could not be sent.";
|
|
||||||
}
|
|
||||||
|
|
||||||
header("Location: bookings.php");
|
|
||||||
exit;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
echo "<div class='alert alert-danger'>Unable to move uploaded file.</div>";
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -434,7 +434,6 @@ $conn->close();
|
|||||||
<div class="widget widget-booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
<div class="widget widget-booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
<h5 class="widget-title">Book your Trip</h5>
|
<h5 class="widget-title">Book your Trip</h5>
|
||||||
<form action="process_trip_booking.php" method="POST">
|
<form action="process_trip_booking.php" method="POST">
|
||||||
<input type="hidden" name="csrf_token" value="<?php echo \Middleware\CsrfMiddleware::getToken(); ?>">
|
|
||||||
<input type="hidden" name="trip_id" id="trip_id" value="<?php echo $trip_id; ?>">
|
<input type="hidden" name="trip_id" id="trip_id" value="<?php echo $trip_id; ?>">
|
||||||
<ul class="radio-filter pt-5">
|
<ul class="radio-filter pt-5">
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
27
upload.php
Normal file
27
upload.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$blog_id = $_GET['blog_id'] ?? null;
|
||||||
|
|
||||||
|
|
||||||
|
if (!isset($_FILES['file'])) {
|
||||||
|
echo json_encode(['error' => 'No file uploaded']);
|
||||||
|
http_response_code(400);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetDir = "uploads/blogs/".$blog_id."/images/";
|
||||||
|
if (!file_exists($targetDir)) {
|
||||||
|
mkdir($targetDir, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tmp = $_FILES['file']['tmp_name'];
|
||||||
|
$name = basename($_FILES['file']['name']);
|
||||||
|
$targetFile = $targetDir . uniqid() . "-" . $name;
|
||||||
|
|
||||||
|
if (move_uploaded_file($tmp, $targetFile)) {
|
||||||
|
echo json_encode(['location' => $targetFile]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['error' => 'Failed to move uploaded file']);
|
||||||
|
http_response_code(500);
|
||||||
|
}
|
||||||
24
upload_blog_image.php
Normal file
24
upload_blog_image.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if (!isset($_FILES['file'])) {
|
||||||
|
echo json_encode(['error' => 'No file uploaded']);
|
||||||
|
http_response_code(400);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetDir = "uploads/blogs/images/";
|
||||||
|
if (!file_exists($targetDir)) {
|
||||||
|
mkdir($targetDir, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tmp = $_FILES['file']['tmp_name'];
|
||||||
|
$name = basename($_FILES['file']['name']);
|
||||||
|
$targetFile = $targetDir . uniqid() . "-" . $name;
|
||||||
|
|
||||||
|
if (move_uploaded_file($tmp, $targetFile)) {
|
||||||
|
echo json_encode(['location' => $targetFile]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['error' => 'Failed to move uploaded file']);
|
||||||
|
http_response_code(500);
|
||||||
|
}
|
||||||
0
upload_debug.log
Normal file
0
upload_debug.log
Normal file
124
user_blogs.php
Normal file
124
user_blogs.php
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<?php include_once('header02.php');
|
||||||
|
|
||||||
|
checkUserSession();
|
||||||
|
|
||||||
|
$result = $conn->prepare("SELECT blog_id, title, description, status, date, image FROM blogs WHERE author = ? AND status != 'deleted' ORDER BY date DESC");
|
||||||
|
|
||||||
|
$result->bind_param("i", $user_id);
|
||||||
|
$result->execute();
|
||||||
|
$posts = $result->get_result();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.image {
|
||||||
|
width: 400px;
|
||||||
|
/* Set your desired width */
|
||||||
|
height: 350px;
|
||||||
|
/* Set your desired height */
|
||||||
|
overflow: hidden;
|
||||||
|
/* Hide any overflow */
|
||||||
|
display: block;
|
||||||
|
/* Ensure proper block behavior */
|
||||||
|
}
|
||||||
|
|
||||||
|
.image img {
|
||||||
|
width: 100%;
|
||||||
|
/* Image scales to fill the container */
|
||||||
|
height: 100%;
|
||||||
|
/* Image scales to fill the container */
|
||||||
|
object-fit: cover;
|
||||||
|
/* Fills the container while maintaining aspect ratio */
|
||||||
|
object-position: top;
|
||||||
|
/* Aligns the top of the image with the top of the container */
|
||||||
|
display: block;
|
||||||
|
/* Prevents inline whitespace issues */
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$bannerFolder = 'assets/images/banners/';
|
||||||
|
$bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
||||||
|
|
||||||
|
$randomBanner = 'assets/images/base4/camping.jpg'; // default fallback
|
||||||
|
if (!empty($bannerImages)) {
|
||||||
|
$randomBanner = $bannerImages[array_rand($bannerImages)];
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<section class="page-banner-area pt-50 pb-35 rel z-1 bgs-cover" style="background-image: url('<?php echo $randomBanner; ?>');">
|
||||||
|
<!-- Overlay PNG -->
|
||||||
|
<div class="banner-overlay"></div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="banner-inner text-white">
|
||||||
|
<h2 class="page-title mb-10" data-aos="fade-left" data-aos-duration="1500" data-aos-offset="50">My Blogs</h2>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb justify-content-center mb-20" data-aos="fade-right" data-aos-delay="200" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<li class="breadcrumb-item"><a href="index.php">Home</a></li>
|
||||||
|
<li class="breadcrumb-item active">My Blogs</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Page Banner End -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Blog List Area start -->
|
||||||
|
<section class="blog-list-page py-100 rel z-1">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
|
||||||
|
<h2>My Posts</h2>
|
||||||
|
<?php if (isset($_SESSION['message'])): ?>
|
||||||
|
<div class="alert alert-warning message-box">
|
||||||
|
<?php echo $_SESSION['message']; ?>
|
||||||
|
<span class="close-btn" onclick="this.parentElement.style.display='none'">×</span>
|
||||||
|
</div>
|
||||||
|
<?php unset($_SESSION['message']); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<a href="blog_create.php">+ New Post</a>
|
||||||
|
|
||||||
|
<?php while ($post = $posts->fetch_assoc()):
|
||||||
|
// Output the HTML structure with dynamic data
|
||||||
|
echo '
|
||||||
|
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="image" style="width:200px;height:200px;"><img src="' . $post["image"] . '" alt="' . $post["title"] . '"></div>
|
||||||
|
<div class="content" style="width:100%;">
|
||||||
|
|
||||||
|
<div class="destination-header">
|
||||||
|
<span class="badge bg-dark"> ' . strtoupper($post["status"]) . '</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5>' . $post["title"] . '</a></h5>
|
||||||
|
<p>' . $post["description"] . '</p>
|
||||||
|
<div class="destination-footer">
|
||||||
|
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px; margin-top:10px;">
|
||||||
|
<a href="blog_edit.php?token='.encryptData($post["blog_id"], $salt).'" class="btn btn-sm" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><i class="bi bi-pencil"></i></a>
|
||||||
|
<a href="blog_read.php?token='.encryptData($post["blog_id"], $salt).'" class="btn btn-sm" data-bs-toggle="tooltip" data-bs-placement="top" title="Preview"><i class="bi bi-eye"></i></a>
|
||||||
|
<a href="blog_delete.php?token='.encryptData($post["blog_id"], $salt).'" class="btn btn-sm" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete"><i class="bi bi-trash"></i></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>';
|
||||||
|
endwhile; ?>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Blog List Area end -->
|
||||||
|
<script>
|
||||||
|
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||||
|
tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<?php include_once("insta_footer.php"); ?>
|
||||||
@@ -5,21 +5,12 @@ require_once("connection.php");
|
|||||||
require_once("functions.php");
|
require_once("functions.php");
|
||||||
require_once 'google-client/vendor/autoload.php'; // Add this line for Google Client
|
require_once 'google-client/vendor/autoload.php'; // Add this line for Google Client
|
||||||
|
|
||||||
use Middleware\CsrfMiddleware;
|
|
||||||
use Middleware\RateLimitMiddleware;
|
|
||||||
use Services\AuthenticationService;
|
|
||||||
|
|
||||||
// Check if connection is established
|
// Check if connection is established
|
||||||
if (!$conn) {
|
if (!$conn) {
|
||||||
json_encode(['status' => 'error', 'message' => 'Database connection failed.']);
|
json_encode(['status' => 'error', 'message' => 'Database connection failed.']);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate CSRF token for POST requests (email/password login)
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !isset($_GET['code'])) {
|
|
||||||
CsrfMiddleware::requireToken($_POST);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Google Client Setup
|
// Google Client Setup
|
||||||
$client = new Google_Client();
|
$client = new Google_Client();
|
||||||
$client->setClientId('948441222188-8qhboq2urr8o9n35mc70s5h2nhd52v0m.apps.googleusercontent.com');
|
$client->setClientId('948441222188-8qhboq2urr8o9n35mc70s5h2nhd52v0m.apps.googleusercontent.com');
|
||||||
@@ -66,10 +57,6 @@ if (isset($_GET['code'])) {
|
|||||||
$_SESSION['first_name'] = $first_name;
|
$_SESSION['first_name'] = $first_name;
|
||||||
$_SESSION['profile_pic'] = $picture;
|
$_SESSION['profile_pic'] = $picture;
|
||||||
processLegacyMembership($_SESSION['user_id']);
|
processLegacyMembership($_SESSION['user_id']);
|
||||||
// Regenerate session to prevent session fixation attacks
|
|
||||||
AuthenticationService::regenerateSession();
|
|
||||||
// Reset rate limit on successful login
|
|
||||||
RateLimitMiddleware::reset('login');
|
|
||||||
// echo json_encode(['status' => 'success', 'message' => 'Google login successful']);
|
// echo json_encode(['status' => 'success', 'message' => 'Google login successful']);
|
||||||
header("Location: index.php");
|
header("Location: index.php");
|
||||||
exit();
|
exit();
|
||||||
@@ -85,10 +72,6 @@ if (isset($_GET['code'])) {
|
|||||||
$_SESSION['first_name'] = $row['first_name'];
|
$_SESSION['first_name'] = $row['first_name'];
|
||||||
$_SESSION['profile_pic'] = $row['profile_pic'];
|
$_SESSION['profile_pic'] = $row['profile_pic'];
|
||||||
sendEmail('chrispintoza@gmail.com', '4WDCSA: New User Login', $name.' has just logged in using Google Login.');
|
sendEmail('chrispintoza@gmail.com', '4WDCSA: New User Login', $name.' has just logged in using Google Login.');
|
||||||
// Regenerate session to prevent session fixation attacks
|
|
||||||
AuthenticationService::regenerateSession();
|
|
||||||
// Reset rate limit on successful login
|
|
||||||
RateLimitMiddleware::reset('login');
|
|
||||||
// echo json_encode(['status' => 'success', 'message' => 'Google login successful']);
|
// echo json_encode(['status' => 'success', 'message' => 'Google login successful']);
|
||||||
header("Location: index.php");
|
header("Location: index.php");
|
||||||
exit();
|
exit();
|
||||||
@@ -103,17 +86,6 @@ if (isset($_GET['code'])) {
|
|||||||
|
|
||||||
// Check if email and password login is requested
|
// Check if email and password login is requested
|
||||||
if (isset($_POST['email']) && isset($_POST['password'])) {
|
if (isset($_POST['email']) && isset($_POST['password'])) {
|
||||||
// Check rate limit first (5 attempts per 15 minutes)
|
|
||||||
if (RateLimitMiddleware::isLimited('login', 5, 900)) {
|
|
||||||
$remaining = RateLimitMiddleware::getTimeRemaining('login', 900);
|
|
||||||
echo json_encode([
|
|
||||||
'status' => 'error',
|
|
||||||
'message' => "Too many login attempts. Please try again in {$remaining} seconds.",
|
|
||||||
'retry_after' => $remaining
|
|
||||||
]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve and sanitize form data
|
// Retrieve and sanitize form data
|
||||||
$email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
|
$email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
|
||||||
$password = trim($_POST['password']); // Remove extra spaces
|
$password = trim($_POST['password']); // Remove extra spaces
|
||||||
@@ -121,13 +93,11 @@ if (isset($_POST['email']) && isset($_POST['password'])) {
|
|||||||
// Validate input
|
// Validate input
|
||||||
if (empty($email) || empty($password)) {
|
if (empty($email) || empty($password)) {
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Please enter both email and password.']);
|
echo json_encode(['status' => 'error', 'message' => 'Please enter both email and password.']);
|
||||||
RateLimitMiddleware::incrementAttempt('login', 900);
|
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Invalid email format.']);
|
echo json_encode(['status' => 'error', 'message' => 'Invalid email format.']);
|
||||||
RateLimitMiddleware::incrementAttempt('login', 900);
|
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +121,6 @@ if (isset($_POST['email']) && isset($_POST['password'])) {
|
|||||||
// Check if the user is verified
|
// Check if the user is verified
|
||||||
if ($row['is_verified'] == 0) {
|
if ($row['is_verified'] == 0) {
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Your account is not verified. Please check your email for the verification link.']);
|
echo json_encode(['status' => 'error', 'message' => 'Your account is not verified. Please check your email for the verification link.']);
|
||||||
RateLimitMiddleware::incrementAttempt('login', 900);
|
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,19 +129,13 @@ if (isset($_POST['email']) && isset($_POST['password'])) {
|
|||||||
$_SESSION['user_id'] = $row['user_id']; // Adjust as per your table structure
|
$_SESSION['user_id'] = $row['user_id']; // Adjust as per your table structure
|
||||||
$_SESSION['first_name'] = $row['first_name']; // Adjust as per your table structure
|
$_SESSION['first_name'] = $row['first_name']; // Adjust as per your table structure
|
||||||
$_SESSION['profile_pic'] = $row['profile_pic'];
|
$_SESSION['profile_pic'] = $row['profile_pic'];
|
||||||
// Regenerate session to prevent session fixation attacks
|
|
||||||
AuthenticationService::regenerateSession();
|
|
||||||
// Reset rate limit on successful login
|
|
||||||
RateLimitMiddleware::reset('login');
|
|
||||||
echo json_encode(['status' => 'success', 'message' => 'Successful Login']);
|
echo json_encode(['status' => 'success', 'message' => 'Successful Login']);
|
||||||
} else {
|
} else {
|
||||||
// Password is incorrect - increment rate limit
|
// Password is incorrect
|
||||||
RateLimitMiddleware::incrementAttempt('login', 900);
|
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Invalid password.']);
|
echo json_encode(['status' => 'error', 'message' => 'Invalid password.']);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// User does not exist - still increment rate limit to prevent email enumeration
|
// User does not exist
|
||||||
RateLimitMiddleware::incrementAttempt('login', 900);
|
|
||||||
echo json_encode(['status' => 'error', 'message' => 'User with that email does not exist.']);
|
echo json_encode(['status' => 'error', 'message' => 'User with that email does not exist.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user