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
|
||||
RedirectMatch 403 ^/\.well-known
|
||||
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');
|
||||
include_once('functions.php');
|
||||
require_once("env.php");
|
||||
|
||||
use Middleware\CsrfMiddleware;
|
||||
|
||||
session_start();
|
||||
|
||||
// Validate CSRF token
|
||||
CsrfMiddleware::requireToken($_POST);
|
||||
|
||||
$user_id = $_SESSION['user_id']; // assuming you're storing it like this
|
||||
|
||||
// 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();
|
||||
|
||||
// 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);
|
||||
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";
|
||||
} else {
|
||||
if (getUserMemberStatus($_SESSION['user_id'])) {
|
||||
$blog_link = $row['link'];
|
||||
$blog_link = "blog_read.php?token=".encryptData($blog_id, $salt);
|
||||
$button_hover = "Read More";
|
||||
$icon = "fa-arrow-right";
|
||||
} else {
|
||||
@@ -96,7 +96,7 @@ if (!empty($bannerImages)) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$blog_link = $row['link'];
|
||||
$blog_link = "blog_read.php?token=".encryptData($blog_id, $salt);
|
||||
$button_hover = "Read More";
|
||||
$icon = "fa-arrow-right";
|
||||
}
|
||||
@@ -105,7 +105,7 @@ if (!empty($bannerImages)) {
|
||||
echo '
|
||||
<div class="blog-item style-three" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||
<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 class="content">
|
||||
<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();
|
||||
|
||||
// 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);
|
||||
$page_id = 'bush_mechanics';
|
||||
?>
|
||||
@@ -95,7 +95,6 @@ if (!empty($bannerImages)) {
|
||||
<div class="blog-sidebar tour-sidebar">
|
||||
<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">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo \Middleware\CsrfMiddleware::getToken(); ?>">
|
||||
<ul class="tickets clearfix">
|
||||
<li>
|
||||
Select Date
|
||||
@@ -115,7 +114,7 @@ if (!empty($bannerImages)) {
|
||||
</select>
|
||||
</li>
|
||||
<?php
|
||||
if ($is_member || $pending_member) {
|
||||
if ($is_member) {
|
||||
echo '
|
||||
<li>
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
$button_text = "Book Now";
|
||||
$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>
|
||||
<button type="submit" class="theme-btn style-two w-100 mt-15 mb-5">
|
||||
<span data-hover="Book Now">Book Now</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</button>
|
||||
<div class="text-center">
|
||||
@@ -366,7 +357,6 @@ if (!empty($bannerImages)) {
|
||||
|
||||
// Fetch PHP variables
|
||||
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_nonmembers = <?= getPrice('bush_mechanics', 'nonmember');?>;
|
||||
|
||||
@@ -374,7 +364,7 @@ if (!empty($bannerImages)) {
|
||||
var total = 0;
|
||||
|
||||
// Calculate cost for members
|
||||
if (isMember || pendingMember) {
|
||||
if (isMember) {
|
||||
total = (cost_members) + (members * cost_members) + (nonmembers * cost_nonmembers);
|
||||
} else {
|
||||
// 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">
|
||||
<h5 class="widget-title">Book your Campsite</h5>
|
||||
<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">
|
||||
<b>From Date</b>
|
||||
<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-dialog">
|
||||
<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-header">
|
||||
<h5 class="modal-title">Add Campsite</h5>
|
||||
|
||||
@@ -2,14 +2,9 @@
|
||||
checkUserSession();
|
||||
|
||||
// SQL query to fetch dates for driver training
|
||||
$sql = "SELECT course_id, date
|
||||
FROM courses
|
||||
WHERE course_type = 'driver_training'
|
||||
AND date >= CURDATE()";
|
||||
|
||||
$sql = "SELECT course_id, date FROM courses WHERE course_type = 'driver_training'";
|
||||
$result = $conn->query($sql);
|
||||
$page_id = 'driver_training';
|
||||
|
||||
?>
|
||||
|
||||
<style>
|
||||
@@ -99,17 +94,16 @@ if (!empty($bannerImages)) {
|
||||
<div class="blog-sidebar tour-sidebar">
|
||||
<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">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo \Middleware\CsrfMiddleware::getToken(); ?>">
|
||||
<ul class="tickets clearfix">
|
||||
<li>
|
||||
Select Date
|
||||
<select name="course_id" id="course_id" required>
|
||||
<?php
|
||||
if ($result && $result->num_rows > 0) {
|
||||
if ($result->num_rows > 0) {
|
||||
// Output each course as an option
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$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>";
|
||||
}
|
||||
} else {
|
||||
@@ -117,10 +111,9 @@ if (!empty($bannerImages)) {
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
||||
</li>
|
||||
<?php
|
||||
if ($is_member || $pending_member) {
|
||||
if ($is_member) {
|
||||
echo '
|
||||
<li>
|
||||
Additional Members <span class="price"></span>
|
||||
@@ -143,7 +136,6 @@ if (!empty($bannerImages)) {
|
||||
<option value="3">03</option>
|
||||
</select>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
$button_text = "Book Now";
|
||||
$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>
|
||||
<button type="submit" class="theme-btn style-two w-100 mt-15 mb-5">
|
||||
<span data-hover="Book Now">Book Now</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</button>
|
||||
<div class="text-center">
|
||||
@@ -371,7 +355,6 @@ if (!empty($bannerImages)) {
|
||||
|
||||
// Fetch PHP variables
|
||||
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_nonmembers = <?= getPrice('driver_training', 'nonmember'); ?>;
|
||||
|
||||
@@ -379,7 +362,7 @@ if (!empty($bannerImages)) {
|
||||
var total = 0;
|
||||
|
||||
// Calculate cost for members
|
||||
if (isMember || pendingMember) {
|
||||
if (isMember) {
|
||||
total = (cost_members) + (members * cost_members) + (nonmembers * cost_nonmembers);
|
||||
} else {
|
||||
// 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->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("connection.php");
|
||||
require_once("functions.php");
|
||||
|
||||
// Import services
|
||||
use Services\AuthenticationService;
|
||||
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();
|
||||
$is_logged_in = isset($_SESSION['user_id']);
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
$is_member = getUserMemberStatus($_SESSION['user_id']);
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$is_member = getUserMemberStatus($user_id);
|
||||
$pending_member = getUserMemberStatusPending($user_id);
|
||||
} else {
|
||||
$is_member = false;
|
||||
$pending_member = false;
|
||||
$user_id = null;
|
||||
}
|
||||
$role = getUserRole();
|
||||
logVisitor();
|
||||
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
@@ -247,6 +211,7 @@ logVisitor();
|
||||
<!-- <li><a href="admin_payments.php">Payfast 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="admin_blogs.php">Manage Blogs</a></li>
|
||||
<!-- <li><a href="bar_tabs.php">Bar</a></li> -->
|
||||
<?php if ($role === 'superadmin') { ?>
|
||||
<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="membership_details.php">Membership</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="logout.php">Log Out</a></li>
|
||||
</ul>
|
||||
|
||||
@@ -8,7 +8,6 @@ $is_logged_in = isset($_SESSION['user_id']);
|
||||
$role = getUserRole();
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
$is_member = getUserMemberStatus($_SESSION['user_id']);
|
||||
$pending_member = getUserMemberStatusPending($_SESSION['user_id']);
|
||||
$user_id = $_SESSION['user_id'];
|
||||
}
|
||||
logVisitor();
|
||||
@@ -34,6 +33,8 @@ logVisitor();
|
||||
<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>
|
||||
<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 -->
|
||||
<link rel="stylesheet" href="assets/css/flaticon.min.css">
|
||||
<!-- Font Awesome -->
|
||||
@@ -225,6 +226,7 @@ logVisitor();
|
||||
<!-- <li><a href="admin_payments.php">Payfast 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="admin_blogs.php">Manage Blogs</a></li>
|
||||
<?php if ($role === 'superadmin') { ?>
|
||||
<li><a href="admin_visitors.php">Visitor Log</a></li>
|
||||
<?php } ?>
|
||||
@@ -239,6 +241,7 @@ logVisitor();
|
||||
<li><a href="account_settings.php">Account Settings</a></li>
|
||||
<li><a href="membership_details.php">Membership</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="logout.php">Log Out</a></li>
|
||||
</ul>
|
||||
|
||||
188
index.php
188
index.php
@@ -190,6 +190,105 @@ if (countUpcomingTrips() > 0) { ?>
|
||||
</a>
|
||||
</div>
|
||||
</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 -->
|
||||
<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>
|
||||
</a>
|
||||
</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 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 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>
|
||||
</section>
|
||||
<!-- 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 -->
|
||||
<section class="blog-area py-70 rel z-1">
|
||||
<div class="container">
|
||||
@@ -371,7 +541,7 @@ if (countUpcomingTrips() > 0) { ?>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<?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);
|
||||
|
||||
if ($result->num_rows > 0) {
|
||||
@@ -392,7 +562,7 @@ if (countUpcomingTrips() > 0) { ?>
|
||||
$icon = "fa-lock";
|
||||
}else{
|
||||
if (getUserMemberStatus($_SESSION['user_id'])) {
|
||||
$blog_link = $row['link'];
|
||||
$blog_link = "blog_read.php?token=".encryptData($blog_id, $salt);
|
||||
$button_hover = "Read More";
|
||||
$icon = "fa-arrow-right";
|
||||
}else{
|
||||
@@ -402,7 +572,7 @@ if (countUpcomingTrips() > 0) { ?>
|
||||
}
|
||||
}
|
||||
}else{
|
||||
$blog_link = $row['link'];
|
||||
$blog_link = "blog_read.php?token=".encryptData($blog_id, $salt);
|
||||
$button_hover = "Read More";
|
||||
$icon = "fa-arrow-right";
|
||||
}
|
||||
@@ -421,7 +591,7 @@ if (countUpcomingTrips() > 0) { ?>
|
||||
</ul>
|
||||
</div>
|
||||
<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>
|
||||
<a style="width:100%;" href="' . $blog_link . '" class="theme-btn">
|
||||
<span style="width:100%;" data-hover="'.$button_hover.'">Read More</span>
|
||||
@@ -494,6 +664,16 @@ if (countUpcomingTrips() > 0) { ?>
|
||||
</form>
|
||||
</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>
|
||||
|
||||
@@ -40,7 +40,6 @@ $login_url = $client->createAuthUrl();
|
||||
<div class="">
|
||||
<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">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo \Middleware\CsrfMiddleware::getToken(); ?>">
|
||||
<div class="section-title">
|
||||
<h2>Log in</h2>
|
||||
<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="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">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo \Middleware\CsrfMiddleware::getToken(); ?>">
|
||||
<div class="section-title">
|
||||
<div id="responseMessage"></div> <!-- Message display area -->
|
||||
</div>
|
||||
|
||||
@@ -4,16 +4,12 @@ require_once("session.php");
|
||||
require_once("connection.php");
|
||||
require_once("functions.php");
|
||||
|
||||
use Middleware\CsrfMiddleware;
|
||||
|
||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||
$eft_id = strtoupper($user_id." SUBS ".date("Y")." ".getInitialSurname($user_id));
|
||||
$status = 'AWAITING PAYMENT';
|
||||
$description = 'Membership Fees '.date("Y")." ".getInitialSurname($user_id);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Validate CSRF token
|
||||
CsrfMiddleware::requireToken($_POST);
|
||||
|
||||
// Get all the form fields
|
||||
$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_date = date('Y-m-d');
|
||||
$membership_start_date = $payment_date;
|
||||
// $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');
|
||||
$membership_end_date = date('Y-12-31');
|
||||
|
||||
$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', ?)");
|
||||
|
||||
@@ -3,8 +3,6 @@ require_once("env.php");
|
||||
require_once("connection.php");
|
||||
require_once("functions.php");
|
||||
|
||||
use Middleware\CsrfMiddleware;
|
||||
|
||||
// Start session to retrieve the logged-in user's ID
|
||||
session_start();
|
||||
|
||||
@@ -13,9 +11,6 @@ $user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||
|
||||
// Check if the form has been submitted
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Validate CSRF token
|
||||
CsrfMiddleware::requireToken($_POST);
|
||||
|
||||
// Get values from the form
|
||||
$from_date = $_POST['from_date'];
|
||||
$to_date = $_POST['to_date'];
|
||||
|
||||
@@ -3,8 +3,6 @@ require_once("env.php");
|
||||
require_once("connection.php");
|
||||
require_once("functions.php");
|
||||
|
||||
use Middleware\CsrfMiddleware;
|
||||
|
||||
// Start session to retrieve the logged-in user's ID
|
||||
session_start();
|
||||
|
||||
@@ -20,8 +18,6 @@ $is_member = getUserMemberStatus($user_id);
|
||||
|
||||
// Check if the form has been submitted
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Validate CSRF token
|
||||
CsrfMiddleware::requireToken($_POST);
|
||||
// Get values from the form
|
||||
$from_date = $_POST['from_date'];
|
||||
$to_date = $_POST['to_date'];
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
require_once("env.php");
|
||||
require_once("connection.php");
|
||||
require_once("functions.php");
|
||||
|
||||
use Middleware\CsrfMiddleware;
|
||||
|
||||
session_start();
|
||||
|
||||
|
||||
@@ -17,14 +14,11 @@ if (!$user_id) {
|
||||
exit();
|
||||
}
|
||||
$is_member = getUserMemberStatus($user_id);
|
||||
$pending_member = getUserMemberStatusPending($user_id);
|
||||
|
||||
// Check if the form has been submitted
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Validate CSRF token
|
||||
CsrfMiddleware::requireToken($_POST);
|
||||
// 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
|
||||
$course_id = isset($_POST['course_id']) ? intval($_POST['course_id']) : 0; // Default to 0 children
|
||||
checkAndRedirectCourseBooking($course_id);
|
||||
@@ -60,13 +54,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$description = "General Course " . $date; // Default fallback description
|
||||
}
|
||||
|
||||
// Assume the membership status is determined elsewhere
|
||||
$is_member = getUserMemberStatus($user_id);
|
||||
|
||||
// Initialize total and discount amount
|
||||
$total = 0;
|
||||
|
||||
// Calculate total based on membership
|
||||
if ($is_member || $pending_member) {
|
||||
$num_members = 1 + $additional_members;
|
||||
$total = ($num_members * $cost_members) + ($num_adults * $cost_nonmembers);
|
||||
if ($is_member) {
|
||||
$num_members = 1 + $members;
|
||||
$total = (($cost_members) + ($members * $cost_members) + ($num_adults * $cost_nonmembers));
|
||||
$payment_amount = $total;
|
||||
} else {
|
||||
$num_members = 0;
|
||||
@@ -81,22 +78,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$num_vehicles = 1;
|
||||
$discountAmount = 0;
|
||||
$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
|
||||
$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)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
$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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
$stmt = $conn->prepare($sql);
|
||||
|
||||
if (!$stmt) {
|
||||
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()) {
|
||||
$booking_id = $conn->insert_id;
|
||||
@@ -121,6 +114,28 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
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();
|
||||
$conn->close();
|
||||
} else {
|
||||
|
||||
@@ -3,19 +3,10 @@ require_once("env.php");
|
||||
require_once("session.php");
|
||||
require_once("connection.php");
|
||||
require_once("functions.php");
|
||||
|
||||
use Middleware\CsrfMiddleware;
|
||||
|
||||
checkAdmin();
|
||||
if (!isset($_GET['token']) || empty($_GET['token'])) {
|
||||
die("Invalid request.");
|
||||
}
|
||||
|
||||
// Validate CSRF token if this is a POST request
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
CsrfMiddleware::requireToken($_POST);
|
||||
}
|
||||
|
||||
$token = $_GET['token'];
|
||||
// echo $token;
|
||||
$eft_id = decryptData($token, $salt);
|
||||
|
||||
@@ -3,16 +3,9 @@ require_once("env.php");
|
||||
require_once("connection.php");
|
||||
require_once("functions.php");
|
||||
|
||||
use Middleware\CsrfMiddleware;
|
||||
|
||||
// Start session to retrieve the logged-in user's ID
|
||||
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)
|
||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||
|
||||
|
||||
@@ -4,15 +4,11 @@ require_once("session.php");
|
||||
require_once("connection.php");
|
||||
require_once("functions.php");
|
||||
|
||||
use Middleware\CsrfMiddleware;
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
die(json_encode(['status' => 'error', 'message' => 'User not logged in']));
|
||||
}
|
||||
|
||||
if (isset($_POST['signature'])) {
|
||||
// Validate CSRF token
|
||||
CsrfMiddleware::requireToken($_POST);
|
||||
$user_id = $_SESSION['user_id']; // Get the user ID from the session
|
||||
$signature = $_POST['signature']; // Base64 image data
|
||||
|
||||
|
||||
@@ -2,16 +2,8 @@
|
||||
require_once("env.php");
|
||||
require_once("connection.php");
|
||||
require_once("functions.php");
|
||||
|
||||
use Middleware\CsrfMiddleware;
|
||||
|
||||
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)
|
||||
$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();
|
||||
|
||||
// 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);
|
||||
$page_id = 'rescue_recovery';
|
||||
?>
|
||||
@@ -94,7 +94,6 @@ if (!empty($bannerImages)) {
|
||||
<div class="blog-sidebar tour-sidebar">
|
||||
<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">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo \Middleware\CsrfMiddleware::getToken(); ?>">
|
||||
<ul class="tickets clearfix">
|
||||
<li>
|
||||
Select Date
|
||||
@@ -114,7 +113,7 @@ if (!empty($bannerImages)) {
|
||||
</select>
|
||||
</li>
|
||||
<?php
|
||||
if ($is_member || $pending_member) {
|
||||
if ($is_member) {
|
||||
echo '
|
||||
<li>
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
$button_text = "Book Now";
|
||||
$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>
|
||||
<button type="submit" class="theme-btn style-two w-100 mt-15 mb-5">
|
||||
<span data-hover="Book Now">Book Now</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</button>
|
||||
<div class="text-center">
|
||||
@@ -299,7 +290,6 @@ if (!empty($bannerImages)) {
|
||||
|
||||
// Fetch PHP variables
|
||||
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_nonmembers = <?= getPrice('rescue_recovery', 'nonmember'); ?>;
|
||||
|
||||
@@ -307,7 +297,7 @@ if (!empty($bannerImages)) {
|
||||
var total = 0;
|
||||
|
||||
// Calculate cost for members
|
||||
if (isMember || pendingMember) {
|
||||
if (isMember) {
|
||||
total = (cost_members) + (members * cost_members) + (nonmembers * cost_nonmembers);
|
||||
} else {
|
||||
// Calculate cost for non-members
|
||||
|
||||
@@ -3,21 +3,9 @@ require_once("env.php");
|
||||
require_once("connection.php");
|
||||
require_once("functions.php");
|
||||
|
||||
use Middleware\RateLimitMiddleware;
|
||||
|
||||
$response = array('status' => 'error', 'message' => 'Something went wrong');
|
||||
|
||||
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'];
|
||||
|
||||
// Check if the email exists
|
||||
@@ -35,7 +23,7 @@ if (isset($_POST['email'])) {
|
||||
$token = bin2hex(random_bytes(50));
|
||||
|
||||
// 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 (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE token = VALUES(token), expires_at = VALUES(expires_at)";
|
||||
$stmt = $conn->prepare($sql);
|
||||
@@ -48,14 +36,9 @@ if (isset($_POST['email'])) {
|
||||
$message = "Click the following link to reset your password: $reset_link";
|
||||
sendEmail($email, $subject, $message);
|
||||
|
||||
// Reset rate limit on successful request
|
||||
RateLimitMiddleware::reset('password_reset');
|
||||
|
||||
$response['status'] = 'success';
|
||||
$response['message'] = 'Password reset link has been sent to your email.';
|
||||
} else {
|
||||
// Increment rate limit even for non-existent emails (prevent email enumeration)
|
||||
RateLimitMiddleware::incrementAttempt('password_reset', 1800);
|
||||
$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');
|
||||
checkUserSession();
|
||||
umask(002); // At the top of the PHP script, before move_uploaded_file()
|
||||
|
||||
|
||||
$user_id = $_SESSION['user_id'] ?? null;
|
||||
|
||||
@@ -52,7 +50,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
|
||||
if (move_uploaded_file($file['tmp_name'], $target_file)) {
|
||||
chmod($target_file, 0664);
|
||||
// Update EFT and booking status
|
||||
$payment_type = $_POST['payment_type'] ?? 'booking';
|
||||
|
||||
@@ -76,33 +73,43 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$stmt2->execute();
|
||||
}
|
||||
|
||||
// Send notification email using sendPOP()
|
||||
$fullname = getFullName($user_id); // Assuming this returns "First Last"
|
||||
|
||||
$eftDetails = getEFTDetails($eft_id);
|
||||
$modified = str_replace(' ', '_', $eft_id);
|
||||
// Notify n8n and send the path to the uploaded file
|
||||
$webhook_url = 'https://n8n.4wdcsa.co.za/webhook/process-pop';
|
||||
|
||||
if ($eftDetails) {
|
||||
$amount = "R" . number_format($eftDetails['amount'], 2);
|
||||
$description = $eftDetails['description'];
|
||||
$postData = [
|
||||
'eft_id' => $eft_id,
|
||||
'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 {
|
||||
$amount = "R0.00";
|
||||
$description = "Payment"; // fallback
|
||||
echo "<div class='alert alert-danger'>Unable to move uploaded file.</div>";
|
||||
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">
|
||||
<h5 class="widget-title">Book your Trip</h5>
|
||||
<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; ?>">
|
||||
<ul class="radio-filter pt-5">
|
||||
<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 '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
|
||||
if (!$conn) {
|
||||
json_encode(['status' => 'error', 'message' => 'Database connection failed.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Validate CSRF token for POST requests (email/password login)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !isset($_GET['code'])) {
|
||||
CsrfMiddleware::requireToken($_POST);
|
||||
}
|
||||
|
||||
// Google Client Setup
|
||||
$client = new Google_Client();
|
||||
$client->setClientId('948441222188-8qhboq2urr8o9n35mc70s5h2nhd52v0m.apps.googleusercontent.com');
|
||||
@@ -66,10 +57,6 @@ if (isset($_GET['code'])) {
|
||||
$_SESSION['first_name'] = $first_name;
|
||||
$_SESSION['profile_pic'] = $picture;
|
||||
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']);
|
||||
header("Location: index.php");
|
||||
exit();
|
||||
@@ -85,10 +72,6 @@ if (isset($_GET['code'])) {
|
||||
$_SESSION['first_name'] = $row['first_name'];
|
||||
$_SESSION['profile_pic'] = $row['profile_pic'];
|
||||
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']);
|
||||
header("Location: index.php");
|
||||
exit();
|
||||
@@ -103,17 +86,6 @@ if (isset($_GET['code'])) {
|
||||
|
||||
// Check if email and password login is requested
|
||||
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
|
||||
$email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
|
||||
$password = trim($_POST['password']); // Remove extra spaces
|
||||
@@ -121,13 +93,11 @@ if (isset($_POST['email']) && isset($_POST['password'])) {
|
||||
// Validate input
|
||||
if (empty($email) || empty($password)) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Please enter both email and password.']);
|
||||
RateLimitMiddleware::incrementAttempt('login', 900);
|
||||
exit();
|
||||
}
|
||||
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid email format.']);
|
||||
RateLimitMiddleware::incrementAttempt('login', 900);
|
||||
exit();
|
||||
}
|
||||
|
||||
@@ -151,7 +121,6 @@ if (isset($_POST['email']) && isset($_POST['password'])) {
|
||||
// Check if the user is verified
|
||||
if ($row['is_verified'] == 0) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Your account is not verified. Please check your email for the verification link.']);
|
||||
RateLimitMiddleware::incrementAttempt('login', 900);
|
||||
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['first_name'] = $row['first_name']; // Adjust as per your table structure
|
||||
$_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']);
|
||||
} else {
|
||||
// Password is incorrect - increment rate limit
|
||||
RateLimitMiddleware::incrementAttempt('login', 900);
|
||||
// Password is incorrect
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid password.']);
|
||||
}
|
||||
} else {
|
||||
// User does not exist - still increment rate limit to prevent email enumeration
|
||||
RateLimitMiddleware::incrementAttempt('login', 900);
|
||||
// User does not exist
|
||||
echo json_encode(['status' => 'error', 'message' => 'User with that email does not exist.']);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user