Prepares the site for handover to the incoming committee. Three groups of
change: security fixes, a production stability fix, and documentation.
SECURITY
- upload_blog_image.php accepted unauthenticated uploads with no extension
or MIME validation, into a web-served directory. Anyone could upload and
execute a .php file. Now requires a session, checks blog ownership, and
runs through validateFileUpload() with a new blog_image type.
- Added uploads/.htaccess and assets/uploads/.htaccess disabling the PHP
engine, so an upload bypass still cannot execute. Force-added: .gitignore
contains a bare .htaccess rule.
- blog/autosave.php updated posts by blog_id alone, letting any logged-in
member overwrite anyone's post. Now scoped to the author, admin override
retained. It also silently reassigned authorship to the editing admin;
the existing author is now preserved.
- add_to_cart.php took item_price and user_id from POST, so drinks could be
charged at any price to any member's tab, unauthenticated. Price and name
are now looked up server-side from bar_items. Its require paths were also
relative and broken.
- Session cookies now httponly/secure/samesite with strict mode, set in both
.htaccess and .user.ini since php_flag and .user.ini apply under different
SAPIs. The 30 minute idle timeout that login set but never checked is now
enforced. Google login now regenerates the session id.
- Forced HTTPS. Required, because Secure cookies are never sent over plain
HTTP and login would fail silently. LetsEncrypt acme-challenge is exempted
above the redirect so certificate renewal keeps working.
- display_errors off; .htaccess now blocks *.log, error_log, .git and
dotfiles. Debug logs containing member IDs, raw webhook bodies and payment
signatures were publicly fetchable.
- Removed the IKHOKHA_BYPASS_SIGNATURE switch and the logging of raw webhook
bodies, app IDs and signatures. Replaced a hardcoded personal Gmail
recipient with the optional DEVELOPER_EMAIL variable.
- Added auth to fetch_users, fetch_bar_tabs, fetch_drinks, create_bar_tab and
submit_order. Converted get_tab_total to a prepared statement.
- Tightened 11 mkdir(0777) calls to 0755.
- Fixed the validate_login rewrite, which pointed at a nonexistent path and
only worked by falling through to the generic .php rule.
DATABASE STABILITY
openDatabaseConnection() opened a new connection on every call across 73 call
sites, and the server was hitting "Too many connections" in production. It now
returns one shared handle per request, reusing the connection connection.php
already opened. The 40 conn->close() calls in functions.php are removed, since
closing the shared handle breaks every later caller; PHP releases it at end of
request. A liveness check reconnects transparently if anything closes it.
Verified no function was lost (99 before and after), every removed close was
inside a braced block, and no transaction spans a helper that would open a
nested one.
DOCUMENTATION AND TOOLING
- docs/HANDOVER.md: operator guide covering architecture, routing, roles, the
membership lifecycle, payments, backups, POPIA obligations, environment
variables, known issues and a handover checklist.
- docs/FUNCTIONS.md: reference for the helpers in functions.php.
- scripts/backup.sh: nightly database and uploads backup, written outside the
web root, with rotation and verification. Dumps are written to a temp file
and only promoted after passing gzip, CREATE TABLE and completion-marker
checks, so a failed run can never overwrite a good backup.
- .deployignore and .gitattributes added; .gitattributes pins LF on shell
scripts and server config because core.autocrlf is true on Windows.
- Removed leftover debris: src/api/test_log.php and .htaccess1.
All 22 modified PHP files pass php -l. The backup script was tested against a
stub mysqldump for healthy, truncated and failing dumps, and for rotation.
Nothing was run against a live database or web server.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Created new track-map page with aerial image and SVG overlay
- Implemented custom rotated square markers with obstacle numbers
- Added admin edit mode for placing and repositioning markers
- Database migration for track_obstacles table
- Modal form for adding new obstacles (replaces browser alerts)
- Drag-to-reposition functionality with auto-save
- Color-coded markers (green/red/black/split) for difficulty levels
- Clickable popups showing obstacle details
- Added track-map to navigation menu and sitemap
- URL rewrite rule for clean /track-map URL
- Move process_event.php from src/admin to src/processors
- Move toggle_event_published.php from src/admin to src/processors
- Move delete_event.php from src/admin to src/processors
- Update .htaccess rewrite rules to point event processors to correct location
- Keep admin_events.php and manage_events.php in admin (display pages only)
- Fetch cover_image in album query
- Set album-header background-image with cover image
- Add dark overlay (50% opacity) over cover for text readability
- Increased padding for better header spacing with cover image
- Improved visual design using cover image as backdrop
- Fallback to overlay-only design if no cover image exists
- Enhanced header layout with proper z-index for content layering
- Changed gallery from carousel to responsive grid layout (similar to about page)
- Shows album cover images with titles, creator info, and photo count
- Improved visual design with hover effects and better spacing
- Edit buttons now only visible to album owners (uses current_user_id variable)
- Added proper ownership verification in all album edit/delete operations
- Enhanced styling for mobile/tablet/desktop responsiveness
- Simplified layout makes it easier to browse multiple albums at once
- Move header() call to before any includes that might output
- Start output buffering at the beginning
- Clean output buffer before sending JSON response
- Add dataType: 'json' to AJAX call to properly parse JSON response
- Add Content-Type header to upload_profile_picture.php
- Add error callback with console logging for debugging
- Remove manual JSON parsing since jQuery handles it with dataType