CANDIDv2 was originally created in 2004 with httpd/PHP4 (when Apache was pseudonymous with httpd). Over 20 years of software evolvution and with zero investment in this project, CANDIDv2 was not able to run in modern day. With the help of Claude Code, this file descriptions phases of implementation to modernize the project (modern PHP, modern MySQL, modern libraries, migrate away from abandoned dependencies, testing) and add a few features along the way (modal-based interactions, responsive design, upload indicators, soft deletes).
While this file is no longer required for the project, it remains here as an example artifact.
- Read the full codebase and produce a project overview
- Identify all third-party dependencies (jscalendar 0.9.6, Treeview/treemenu.net, BarelyFitz Slideshow, Mint analytics)
- Catalog security vulnerabilities (SQL injection via
mysql_*, XSS, CSRF, command injection, open redirects, file upload validation) - Catalog deprecated PHP patterns (
mysql_*, short open tags,split(),ereg_replace(),$HTTP_POST_FILES,$$var['key']) - Produce detailed MODERNIZATION_PLAN.md based on all tasks listed
- Create Dockerfile (PHP 8.2 + Apache with GD, PDO_MySQL, EXIF, zip extensions)
- Create docker-compose.yml (PHP+Apache and MySQL 8.0, one-command dev environment)
- Create docker/php.ini with upload limits and error reporting overrides
- Create .env.example with environment configuration template
- Create .gitignore for .env, storage files, IDE files, Docker volumes, vendor/
- Create database/schema.sql (canonical schema reconciled from setup.php)
- Create database/seed.sql (default admin user with bcrypt password)
- Create storage/images/.gitkeep and storage/uploads/.gitkeep
- Update README.md with Docker quick-start and production deployment instructions
- Create new directory structure:
candidv2/ ├── public/ # Web root (rename from htdocs/) │ ├── index.php # Front controller (single entry point) │ ├── assets/ # Static files (CSS, JS, images) │ │ ├── css/ │ │ ├── js/ │ │ └── images/ │ └── themes/ ├── src/ # Application code (PSR-4: App\) │ ├── Controller/ # Request handlers │ ├── Service/ # Business logic │ ├── Repository/ # Database queries │ ├── Entity/ # Data models │ └── Helper/ # Utility functions ├── config/ # Configuration files │ └── config.php ├── templates/ # View templates ├── storage/ # Runtime files │ ├── uploads/ │ ├── images/ │ ├── cache/ │ └── logs/ ├── database/ # Schema and migrations ├── scripts/ # CLI utilities ├── tests/ # Test suites ├── legacy/ # Deprecated code └── vendor/ # Composer dependencies - Create composer.json with PSR-4 autoloading (namespace:
App\) - Create front controller (public/index.php) with router
- Create base Controller class with common functionality
- Move includes/*.inc to src/ as namespaced classes:
- db.inc → src/Service/Database.php
- auth.inc → src/Service/Auth.php
- template.inc → src/Service/Template.php
- image.inc → (functionality in ImageController)
- category.inc → src/Service/CategoryService.php
- query.inc → (replaced by PDO in Database.php)
- upload.inc → (functionality in ImageController::add)
- import.inc → (deprecated, see Phase 3c)
- profile.inc → src/Service/ProfileService.php
- comment.inc → src/Service/CommentService.php
- history.inc → src/Service/HistoryService.php
- misc.inc → (superseded by Controller base class + functions.php)
- Convert page files to Controllers:
- htdocs/main.php → src/Controller/MainController.php
- htdocs/login.php → src/Controller/AuthController.php
- htdocs/register.php → src/Controller/AuthController.php
- htdocs/search.php → src/Controller/SearchController.php
- htdocs/browse.php → src/Controller/BrowseController.php
- htdocs/image/*.php → src/Controller/ImageController.php
- htdocs/category/*.php → src/Controller/CategoryController.php
- htdocs/profile/*.php → src/Controller/ProfileController.php
- htdocs/comment/*.php → src/Controller/CommentController.php
- Move view logic to templates/ directory
- Create src/Helper/functions.php for global helper functions (h(), csrf_token(), etc.)
- Update all internal paths and references
- Move original htdocs/ to legacy/htdocs/ (preserve for reference)
- Create src/Service/Database.php — PDO wrapper with query(), lastInsertId(), getConnection() (completed in 3a)
- Implement parameterized queries (Database.php provides this directly; separate QueryBuilder not needed)
- Update config/config.php — .env loading, auto-detect base paths (completed in 3a)
- Migrate all
mysql_*calls to PDO prepared statements (new code uses PDO; legacy in legacy/) - Replace
PASSWORD()/OLD_PASSWORD()withpassword_hash()/password_verify()(Auth.php uses bcrypt) - Replace
split()withexplode()throughout (no split() in new code) - Remove
addslashes()calls (no addslashes() in new code)
- Create CSRF token generation and validation (in functions.php: csrf_token(), csrf_field(), verify_csrf())
- Add CSRF protection to all POST forms and POST handlers (validateCsrf() in all controllers)
- Add XSS output escaping (
h()helper) on all user-supplied output (h() in functions.php, used in all templates) - Add file upload validation (magic byte detection via finfo, filename sanitization in ImageController)
- Replace all
system()/shell_exec()calls with native PHP equivalents (none exist in src/) - Fix open redirects with
safe_redirect()function (redirect() in functions.php validates same-host) - Remove MMS/email import features (import.inc already in legacy/, not used in src/)
- Fix
ereg_replace()→preg_replace()(none in src/) - Fix
$HTTP_POST_FILES→$_FILES(none in src/) - Fix variable-variable
$$var['key']→${$var['key']}(none in src/) - Fix
implode()single-argument calls (none in src/, all use correct syntax) - Fix
displayImage()float-to-int deprecation (not applicable, new code) - Fix all short open tags (
<?→<?php) (none in src/ or templates/) - Fix deprecated
${var}string interpolation →{$var}(none in src/) - Fix superglobal function parameters (none in src/)
- Fix illegal
global $_SERVER/global $_COOKIEdeclarations (none in src/) - Fix
implode($array, $glue)argument order (all correct in src/) - Remove
globalkeyword usage — use dependency injection (none in src/) - Add type declarations to all functions (new code has types)
- Replace
die()/exit()with exception hierarchy (HttpException, ForbiddenException)
- Move unused .BAK files, dead pages (new-browse.php, search-test.php, ajax.phps, devel/) to legacy/ (already in legacy/htdocs/)
- Remove defunct Mint analytics script include (none in src/ or templates/)
- Fix hardcoded URLs (candid.scurvy.net) to use config-based URLs (none in src/, templates/, or config/)
- Create ImageStorage.php — hash-based sharding for image files
- Migrate image storage from database BLOBs to filesystem (ImageController updated, migration script created)
- Update ImageController to use ImageStorage service (legacy code in legacy/ unchanged)
- Create migration script: scripts/migrate_images_to_filesystem.php
- Replace open registration with admin-only user creation (registration redirects to login with message)
- Add
must_change_passwordcolumn to user table - Create AdminController with user management (list, create, edit, delete)
- Create UserService for user operations
- Forced password change redirect in Controller::requireAuth()
- Replace jscalendar 0.9.6 date picker with native HTML5
<input type="date">(new templates use HTML5 date) - Find all input fields handling date and make sure the type is updated to
date(done: image/add.php, search/index.php) - Remove jscalendar script includes from template.inc (not included in new templates)
- Move htdocs/js/jscalendar-0.9.6/ to legacy/ (already in legacy/)
- Create vanilla JS tree view (not needed - categories use simple HTML lists)
- Move htdocs/js/Treeview/ to legacy/ (already in legacy/)
- Slideshow functionality not included in new code (can be added later if needed)
- Move htdocs/js/slideshow.js to legacy/ (already in legacy/)
- Rewrite CSS with CSS custom properties (templates/layouts/main.php has modern CSS with custom properties)
- Remove obsolete jscalendar CSS rules (not included in new templates)
- Extract inline
style=attributes into named CSS classes (all templates converted) - Consolidate to single default theme; move classic theme to legacy/ (already in legacy/)
- Modern CSS Grid and Flexbox layout classes (.grid-2, .flex-between, .flex-center)
- Image grid with CSS Grid (.image-grid with auto-fill)
- Tables use semantic HTML with .table class
- Header nav uses flexbox
- Add viewport meta tag (in templates/layouts/main.php)
- Add responsive media queries (768px and 480px breakpoints)
- Mobile-friendly table styles with .hide-mobile class
- Responsive button and card sizing
- Add composer.json with PHPUnit 10.5 dev dependency (already configured)
- Create phpunit.xml with unit and integration test suites
- Create tests/bootstrap.php for test environment setup
- Write unit tests
- HelperFunctionsTest (h(), csrf_*, config(), format_date/datetime())
- ImageStorageTest (store, retrieve, delete, exists)
- UserServiceTest (CRUD operations with mocked database)
- CategoryServiceTest (getAll, getTree, find, getImages, countImages, getBreadcrumb)
- Write integration tests
- DatabaseTest (connection, query, transactions)
- Create .github/workflows/tests.yml for CI (GitHub Actions)
- Matrix testing for PHP 8.2 and 8.3
- MySQL 8.0 service for integration tests
- PHP syntax linting
- Update README.md with instructions for running the test suite
- Evaluate Slim 4, Flight, and frameworkless approaches
- Document recommendation in FRAMEWORK_EVALUATION.md
- Recommendation: Continue frameworkless; migration cost exceeds benefit for this application's scope
- Create lightbox overlay component (vanilla JS)
- Click image thumbnail to open in overlay instead of navigating to detail page
- Add left/right navigation arrows for carousel
- Add keyboard navigation (arrow keys, Escape to close)
- Preload adjacent images for smooth navigation
- Display image metadata (description, date, photographer) in overlay
- Add close button and click-outside-to-close behavior
- Maintain URL state (update URL hash for shareable links)
- Ensure mobile-friendly touch gestures (swipe left/right)
- Enhanced metadata display (date taken, photographer, camera model)
- Action links (Details, Edit) with permission-based visibility
- Data attributes on image cards for metadata extraction
- Compute MD5 hash of uploaded image files
- Check for existing images with same hash in the target category
- Skip duplicates and report count in flash message
- Store md5_hash in image_info table for all uploads
- Migration script to backfill MD5 hashes for existing images
- Add sort dropdown to category view (Date Taken, Date Added, Description)
- Sort selection auto-submits form
- Maintain sort preference via query parameter
- Default sort order per category (stored in sort_by column)
- Category edit page includes sort order dropdown
- PHPStan static analysis (level 6)
- Added phpstan/phpstan to composer.json
- Created phpstan.neon configuration
- Added to CI workflow
- Additional integration tests
- AuthServiceTest (login, logout, password hashing, admin checks)
- UserServiceTest (CRUD with real database)
- ImageStorageTest (filesystem operations)
- Playwright E2E testing
- Created package.json with Playwright dependency
- Created playwright.config.ts (Chrome, Firefox, Safari, mobile)
- Auth tests (login, logout, registration redirect)
- Browse tests (categories, lightbox navigation)
- Search tests (form, filters, results)
- Image tests (upload, edit, delete)
- Added E2E job to CI workflow
- Test counts: 102 PHPUnit tests, 31 Playwright E2E tests
- Soft-delete for categories and images
- Added deleted_at and deleted_by columns to category and image_info tables
- Created migration 003_add_soft_delete.sql
- Categories and images marked as deleted instead of permanent removal
- Deleted items hidden from all browse/search views
- ImageService for image operations
- softDelete(), restore(), hardDelete(), getDeleted() methods
- Registered in Container
- CategoryService soft-delete methods
- softDelete() deletes category tree and orphaned images
- restore() restores category (moves to root if parent deleted)
- hardDelete() for permanent removal
- getDeleted(), countDeleted(), getDeletionStats()
- Admin Trash page
- View all soft-deleted categories and images
- Tabbed interface (Categories/Images)
- Individual restore and permanent delete actions
- Bulk restore and delete with checkboxes
- Empty Trash button to purge all
- Trash link in admin navigation
- Category delete confirmation modal
- Shows count of subcategories and images affected
- JSON endpoint /category/{id}/deletion-stats
- Modal replaces browser alert dialog
- Unit tests for ImageService and CategoryService soft-delete methods
- Upload progress indicator
- Progress bar with percentage during file upload
- XMLHttpRequest with upload progress event tracking
- Visual feedback while processing
- Inline category creation
- "New" button next to category dropdown on upload page
- Modal to create category without leaving page
- JSON endpoint POST /category/add-json
- New category automatically selected after creation
- Increased upload limits
- max_file_uploads increased from 20 to 100
- post_max_size increased from 55M to 500M
- Profile dropdown menu
- Profile icon (person silhouette) triggers hover dropdown
- Contains Profile, Logout links
- Admin section with Users and Trash (admin only)
- Login/Register for logged-out users
- Two-column layout for image view and edit pages
- Image displayed on left (main content area)
- View page: metadata and comments in sidebar on right
- Edit page: form fields in sidebar on right, full-size image with rotate button
- CSS Grid layout with responsive breakpoint at 900px
- Stacks vertically on mobile devices
- Thumbnail dimensions and quality in config/config.php
- thumb_width, thumb_height, thumb_quality settings
- Environment variable support (THUMB_WIDTH, THUMB_HEIGHT, THUMB_QUALITY)
- Default: 400x400 at 100% quality
- ImageController uses config values
- Added config() helper to base Controller class
- createThumbnail() reads from config instead of hardcoded values
- regenerate_thumbnails.php script uses config values
- Displays configured dimensions and quality in output
Tasks discovered during implementation that were not in the original plan:
- Image edit functionality — /image/{id}/edit route, showEdit()/edit() controller methods, and templates/image/edit.php were missing from original plan
- Route ordering fix — Specific routes (/image/add) must come before parameterized routes (/image/{id}) in public/index.php to avoid 404 errors
- Bcrypt hash in seed.sql — Placeholder hash was invalid; regenerated correct hash for "changeme" password
- Controller method naming conflict — ImageController::view() conflicted with parent Controller::view(); renamed to detail()
- Category add/edit UI links — Routes and controller existed but browse templates lacked add/edit links; added links and canEdit/canAddCategory flags to BrowseController
- Test fixtures not matching implementation — Unit tests used wrong method mocks (query vs fetchAll/fetchOne), wrong path format for ImageStorage, wrong date format, wrong HTML entity encoding; fixed all tests to match actual implementation
- Test files not mounted in Docker — phpunit.xml and tests/ directory missing from docker-compose.yml volumes
- Bulk image upload — Upload multiple images at once instead of one at a time
- Default description from filename — Pre-populate description field with the image filename (minus extension)
- EXIF date pre-population — Use EXIF DateTimeOriginal to pre-fill date_taken field on upload
- User management navigation links — Admin and Profile links missing from header navigation; functionality existed but wasn't accessible from UI
- People tagging UI — Database table existed for tagging users in images but no UI to manage tags; added multi-select on image edit page
- Search by tagged people — Added "People Tagged" multi-select filter to search form to find images where any of the selected users are tagged
- Upload button in category view — Added Upload button that pre-selects the current category in the upload form
- Image deletion — No way to delete images; added delete route, controller method, and button on edit page
- Return to category after edit/delete — After editing or deleting an image, redirect back to the category the user was browsing
- Button styling inconsistency — Many submit buttons missing
.btnclass; standardized across all 17 templates - Button layout standardization — Shortened button text (Save/Cancel/Create/Delete), consistent
.btn-grouplayout with Save/Cancel left and Delete right, delete buttons use.btn-text-dangerstyle - Image rotation — Added rotate CW button on image edit page; rotates full image and regenerates thumbnail
- Edit icon standardization — Use pencil SVG icon with
.action-iconclass for edit actions (used in admin/users, image view, category breadcrumb) - Bulk image editing — Mass edit metadata for multiple images from search results or category browse:
- Add/remove people tags
- Update date taken
- Update description
- Rotate images
- Update photographer
- Change category
- Set access level
- Set private flag
- UI: checkboxes on image grids, bulk actions bar, bulk edit page
- Routes: GET/POST /image/bulk/edit, POST /image/bulk/rotate
- E2E tests: 4 tests for bulk edit functionality
- Intervention Image migration — Consolidated all image processing to use Intervention Image v3:
- Replaced GD extension with Intervention Image using Imagick driver
- Cleaner thumbnail generation with automatic EXIF orientation
- Native HEIC format support
- Updated ImageController with imageManager() and createThumbnail() methods
- Updated regenerate_thumbnails.php script
- Updated Dockerfile (removed GD, kept imagick)
- Updated composer.json (removed ext-gd, added intervention/image)
- Lazy loading images — Added native
loading="lazy"attribute to image thumbnails:- Applied to browse/category.php, search/results.php, image/bulk-edit.php
- Improves initial page load performance
- Expandable flash message details — Flash messages can include collapsible details:
- Upload feedback shows skipped duplicates and failed files
- Details section is collapsed by default
- Removed query limits — Browse and search pages show all results:
- Category browse: removed LIMIT 50
- Search results: removed LIMIT 100
- Full datetime for date_taken — Store time component from EXIF:
- Changed date_taken column from DATE to DATETIME
- EXIF DateTimeOriginal now stored as 'Y-m-d H:i:s'
- Migration 005_date_taken_datetime.sql
- Backfill script scripts/backfill_date_times.php
- Modal dialog system — Replaced all JavaScript alerts/confirms with modals:
- Created public/assets/js/modal.js with Modal.alert(), Modal.confirm(), Modal.confirmDanger()
- Added modal CSS to templates/layouts/main.php
- Uses data-confirm attributes for declarative form/button confirmation
- Updated 7 templates: browse/category, search/results, image/edit, image/bulk-edit, image/view, admin/users, admin/trash
- Upload progress indicator improvements — Better feedback during file processing:
- Shimmer animation for processing state after upload completes
- Descriptive text during thumbnail generation
- Spinner icon during processing
- Bulk action UI improvements — Cleaner interface per Elastic UI guidelines:
- Replaced Select text button with grid+checkmark icon
- Replaced Done button with X icon on right side
- Header buttons hidden when category has no images
- Removed unused profile fields — Cleaned up user profile:
- Removed numrows/numcols form fields from edit page
- Removed from controller and service update methods
- Project file reorganization — Aligned directory layout with Laravel/Symfony conventions:
e2e/→tests/e2e/(all tests under onetests/tree)Dockerfile→docker/Dockerfile(all container config together)scripts/→bin/(Symfony/Composer CLI convention)templates/→resources/(Laravel/Symfony view convention)bootstrap.php→bootstrap/app.php(Laravel bootstrap pattern)- Updated all references in
docker-compose.yml,docker/Dockerfile,playwright.config.ts,public/index.php,config/config.php, and allbin/scripts