1 Commits

Author SHA1 Message Date
7d078cb954 WIP: Blogposts 2025-12-02 17:41:24 +02:00
61 changed files with 2966 additions and 3474 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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!** 🚀

View File

@@ -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.

View File

@@ -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
View 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'">&times;</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"); ?>

View File

@@ -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
View 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;
}

View File

@@ -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
View 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
View 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
View 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
View 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
View 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;
}
?>

View File

@@ -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

View File

@@ -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">

View File

@@ -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>

View File

@@ -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,13 +94,12 @@ 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
@@ -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
View File

@@ -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;
});

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View File

@@ -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>

186
index.php
View File

@@ -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">
@@ -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>

View File

@@ -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 -->

View File

@@ -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>

View File

@@ -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', ?)");

View File

@@ -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'];

View File

@@ -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'];

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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;

View File

@@ -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

View File

@@ -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
View 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;
}
?>

View File

@@ -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

View File

@@ -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.';
}
}

View File

@@ -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;
}
}

View File

@@ -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
];
}
}

View File

@@ -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, '/');
}
}

View File

@@ -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");
}
}

View File

@@ -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"
);
}
}

View File

@@ -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);
}
}

View File

@@ -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
View 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");

View File

@@ -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,35 +73,45 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt2->execute();
}
// Send notification email using sendPOP()
$fullname = getFullName($user_id); // Assuming this returns "First Last"
// Notify n8n and send the path to the uploaded file
$webhook_url = 'https://n8n.4wdcsa.co.za/webhook/process-pop';
$eftDetails = getEFTDetails($eft_id);
$modified = str_replace(' ', '_', $eft_id);
$postData = [
'eft_id' => $eft_id,
'payment_type' => $payment_type,
];
if ($eftDetails) {
$amount = "R" . number_format($eftDetails['amount'], 2);
$description = $eftDetails['description'];
} else {
$amount = "R0.00";
$description = "Payment"; // fallback
}
$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 (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.";
}
if ($error) {
error_log("Webhook Error: $error");
$_SESSION['message'] = $error;
header("Location: bookings.php");
exit;
} else {
$_SESSION['message'] = "Thank you! We are busy processing your payment!";
header("Location: bookings.php");
}
exit;
} else {
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;
}
}
}
// Fetch bookings for dropdown

View File

@@ -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
View 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
View 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
View File

124
user_blogs.php Normal file
View 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'">&times;</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"); ?>

View File

@@ -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.']);
}