# Hobbesgram — Changelog ## v1.15 ### Contributor requested-category on upload - **Requested Category field** — when a contributor's account has a staging category set (per-user `upload_category` or global `contributor_category`), the upload form now shows the staging category as a locked read-only field and adds a separate **Requested Category** dropdown. The contributor selects where they want the file placed after review; the file is physically stored in the staging category while pending. - **`requested_category` metadata field** — saved on every web upload. When no staging restriction is in effect the field is set to the same value as `category` (no behaviour change for unrestricted contributors). - **Pool approval form updated** — the category dropdown pre-fills from `requested_category` (the contributor's intended destination) rather than the staging location. The "Submitted by" line now reads *"Submitted by X — requested: [path]"* and shows the staging location in parentheses when it differs from the requested category. Editors can still change the category before approving; the existing move-on-approve logic handles the file relocation. ### Search improvements - **Periods preserved in tokens** — `search_tokenize()` no longer strips periods. A term like `file.txt` is indexed and queried as a single literal token rather than being split into `file` and `txt`. Both the search index and the query path use the same updated tokenizer, so a full index rebuild is needed after upgrading. - **Space-separated terms are OR** — `search_query()` now unions results for all space-separated tokens. The previous AND-first / OR-fallback logic has been removed; spaces always mean OR. - **Quoted phrase search** — wrapping a phrase in double quotes (e.g. `"file manager"`) performs a literal case-insensitive substring match against the full metadata corpus (title, description, author, version, tags, category, filename, uploader, OS/2 version, requirements, license). Quoted phrases and unquoted tokens in the same query are OR'd together. A new `search_build_corpus()` helper centralises corpus construction. --- ## v1.12 ### Mirroring infrastructure - **`/catalog.json`** — public endpoint (no credentials required) that serves a full JSON manifest of every approved file. Each entry includes title, description, category path, size, uploader, version, author, license, requirements, tags, download count, and absolute `download_url`, `meta_url`, and `info_url`. Response carries `Cache-Control: public, max-age=300` and `Access-Control-Allow-Origin: *`. - **`/download/{path}.json` metadata sidecars** — every file exposes its metadata at the download URL with `.json` appended (e.g. `/download/utils/archivers/unzip.zip.json`). Always public regardless of the site's auth settings. Mirrors can fetch the binary (credentials required if the site enforces them) and the sidecar (always open) in one pass — the same `.zip` + `.zip.json` pairing used by the Archive.org bulk item upload. - **`/mirror` page** — public-facing instructions page with the catalog URL, per-file sidecar pattern, `wget`/`curl` examples, and a copy-paste shell script for bulk mirroring (requires `wget` and `jq`). - **`file_public_meta(array $meta, string $base = '')`** added to `includes/functions.php`. Strips internal fields (`stored_name`, `archiveorg_*`) and appends computed `download_url`, `meta_url`, and `info_url`. Accepts an optional absolute URL prefix. - **`.htaccess`** created for v1.12 (based on v1.01). Comment clarifies why the `FilesMatch "\.(json|ini)$"` block does not affect virtual routes (`/catalog.json`, `*.zip.json`): those rewrite to `index.php` before Apache's authorization phase evaluates `FilesMatch`. - **Access gate** exemption in `index.php` — `.json`-suffixed requests under `/download/` bypass the auth gate via `$is_public_meta`, so sidecars are always accessible even when binary downloads require a login. ### Category slug stability - Renaming a category in **Admin → Categories** no longer regenerates its slug. The slug — and therefore every file URL stored under that category — is fixed at creation time and never altered by a rename. ### Contributor upload category limits - **`upload_category` field** on user records — single category ID that restricts a contributor to submitting only to that category. Empty string means no per-user restriction. - **Global default** (`contributor_category` in `settings.json`, `config.php` `DEFAULT_SETTINGS`) — set from **Admin → Site Settings**. Applies to every contributor with no per-user override. - **Priority chain**: per-user `upload_category` → global `contributor_category` → no restriction (any category). - **Per-user override page** `/admin/user-limits/{username}` — single dropdown showing full category paths (e.g. `Utilities › Archivers`). Selecting "No restriction" clears the override. Only affects the `contributor` role; editors and admins are unaffected. - **Admin → Users** — new "Cat. Access" column shows the restricted category name or "All" for each contributor. A "Limits" button (contributor rows only) links to the per-user limits page. - **Upload form** — when a restriction is active the category field renders as a locked read-only input; a hidden `` carries the value for submission. Server-side validation rejects any submission to a different category even if the form is tampered with. ### Pool approval workflow - **Inline edit + approve** — web-upload items in `/pool` now show a full editable form (title, description, author, version, homepage, category, OS/2 version, license, requirements, tags) pre-filled from the contributor's submission. The editor reviews and edits everything in place, then clicks **Approve** once. The separate "Edit Metadata" step has been removed. - **Category override on approval** — changing the category before approving moves the physical file on disk to the new category directory and updates `stored_name` in the metadata record, consistent with the post-approval edit flow in `file/edit.php`. - **Submitted-by line** — each pool item shows "Submitted by *username* to *category path*" so the editor can compare the contributor's original intent with the category selected in the form. - **Reject form** is a separate `
` below the approve form (HTML does not allow nested forms). --- ## v1.07 ### Theming overhaul - **CSS custom properties:** The entire inline style block in `templates/header.php` now uses CSS custom properties (`--c-*`) on `:root`. All PHP color echoes have been removed from the template; every element references variables. - **`THEME_PRESETS` constant** added to `config.php` — five named presets with full color arrays: OS/2 Classic, Dark Mode, Green Terminal, Hobbes OG, Amber. - **Site-default preset selector** in Admin → CSS. The chosen preset key is saved as `active_preset` in `settings.json`. Applying a named preset marks it active; editing individual palette values clears the active preset name. - **Per-user theme override:** Users can select a personal display theme from their Profile page. The selection is stored as `theme_preset` in their user JSON and applied at header render time, overriding the site default. Choosing "Site Default" reverts to the admin-selected preset. - **Links styled as buttons** site-wide via a global `a { ... }` rule with inverse-color hover. Structural links (header, nav, sidebar, tag pills) use targeted overrides to opt out of the button appearance. - **Semantic status classes** — `.status-ok`, `.status-warn`, `.status-err` — replace hardcoded hex color strings throughout admin pages (meta_merge, repair, bulk_delete, admin dashboard stat numbers). - **Readability fixes** in `pages/admin/meta_merge.php`, `repair.php`, and `bulk_delete.php`: hardcoded white and off-white backgrounds replaced with `var(--c-panel-bg)` and `var(--c-tr-alt)`; hardcoded border colors replaced with `var(--c-border)`. ### Admin: file rename - Admins can rename the physical file on disk from the Danger Zone section of `/file/edit/{id}`. - Validation: extension must match the original; the new name is passed through `safe_filename()`; a collision check via `realpath()` prevents overwriting an existing file. - `original_name` and `stored_name` are updated in metadata. If a category move is requested in the same form submission, the move step uses the post-rename path. - Orphaned-file case: if the physical file is missing, metadata is updated only. ### Admin: Quality Reports (`/admin/reports`) New page with three reports for archive hygiene: - **Duplicate Files** (`?r=duplicates`): groups approved files by size, then computes MD5 within each size group to confirm true duplicates. Shows each group with file locations and Edit links. - **Same Filename** (`?r=samename`): groups files by lowercase filename regardless of category path; highlights only groups with two or more locations. - **Missing Descriptions** (`?r=nodesc`): lists approved files whose description is absent or a placeholder value ("Unknown", "N/A", etc.). All reports link directly to `/file/edit/{id}` for quick remediation. ### Admin: Archive.org mirroring (`/admin/mirror`) - **Credentials** stored in `settings.json` under `archiveorg` (key, secret, collection); configured in Admin → Settings → Archive.org Mirror Credentials. - **Per-file upload:** PUT to `https://s3.us.archive.org/hobbesgram-{id}/{filename}` using `Authorization: LOW key:secret`. All available metadata fields are sent as `x-archive-meta-*` headers. `x-archive-auto-make-bucket: 1` creates the item on first upload. - **Batch upload:** selectable batch size (5, 10, 25, or 50); uploads the next N unmirrored approved files in sequence. `set_time_limit(7200)` and `CURLOPT_TIMEOUT => 7200` handle large files. - On success, `archiveorg_id` and `archiveorg_ts` are saved to file metadata. The file detail page shows an Archive.org link when `archiveorg_id` is set. - Mirror status panel shows mirrored / unmirrored / total counts. - Re-upload button available for already-mirrored files. - Admin → index links updated to include Quality Reports and Mirror to Archive.org. ### wget / curl HTTP Basic Auth - `auth_try_basic()` added to `includes/auth.php`. Checks `PHP_AUTH_USER` / `PHP_AUTH_PW` (set by Apache `CGIPassAuth On`) and falls back to parsing the `Authorization` header from `HTTP_AUTHORIZATION` or `REDIRECT_HTTP_AUTHORIZATION` (set by `.htaccess` `RewriteRule E=HTTP_AUTHORIZATION`). - Static helper `_basic_auth_flag()` tracks whether the current request was authenticated via Basic Auth within the PHP request cycle. - `auth_try_basic()` is called immediately after `auth_start()` in `index.php`. - **401 challenge:** Unauthenticated requests to `/download/*` return `HTTP 401 Unauthorized` with a `WWW-Authenticate: Basic realm="..."` header and a plain-text body showing `wget` and `curl` usage, so command-line tools know to prompt for credentials. - **`.htaccess`** updated with `CGIPassAuth On` and `RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]` for broad shared-hosting compatibility. - File detail page shows a pre-filled `wget` command for logged-in users. ### wget / curl download log - `DLLOG_FILE` constant in `config.php` points to `data/index/dllog.json`. - `dllog_append()` and `dllog_load()` added to `includes/storage.php`. Log is capped at 5,000 entries (oldest entries dropped when cap is reached). - `pages/download.php` calls `dllog_append()` whenever `basic_auth_user()` is non-null, recording: timestamp, username, file ID, filename, IP address, User-Agent string. - **Download Log tab** added to `/admin/mirror`. Shows all log entries most- recent-first, paginated at 50 per page (columns: date/time, user, file, IP, User-Agent). --- ## v1.06 ### Meta Merge: ZIP bundle support - The Meta Merge upload form now accepts `.zip` files in addition to `.txt` files. Each `.txt` entry inside the ZIP is extracted and parsed as a separate source; all matched entries are merged in a single review session. - Upload size overrun detection: when PHP silently drops the POST body because `post_max_size` is exceeded, an informative error is shown rather than a blank form. ### Meta Merge: improved filename auto-matching - A file is now auto-matched when its filename is unique across the entire archive, regardless of whether a `DIR` path was present in the `.txt` entry. Previously auto-match only fired when the path field was empty. ### Storage: legacy encoding robustness - `storage_write()` now uses `JSON_INVALID_UTF8_SUBSTITUTE` when encoding JSON. Invalid byte sequences (e.g. CP850 / CP437 characters in legacy OS/2 text files) are replaced with U+FFFD instead of causing `json_encode()` to return `false` and silently lose data. --- ## v1.05 ### Shared hosting server config - **`.htaccess`** added to web root with: `Options -Indexes`, `mod_rewrite` rules for clean URLs, `DirectoryIndex index.php`, PHP limit overrides (`upload_max_filesize 200M`, `post_max_size 210M`, `memory_limit 256M`, `max_execution_time 300`), block rules for direct access to `.json` and `.ini` files. - **`.user.ini`** added for PHP-FPM environments (equivalent limit overrides without requiring `.htaccess` PHP directives support). --- ## v1.04 ### Admin: Archive Repair tool (`/admin/repair`) New read-only integrity report for administrators: - **Orphaned files** — physical files in `data/uploads/` that have no matching metadata record in `data/files/`. - **Empty-browse categories** — categories with no approved files and no sub-categories (candidates for pruning). - **Duplicate categories** — category names that appear more than once under the same parent node. No automatic changes are made; the report is informational only. --- ## v1.03 (2026-03-24) ### Meta Merge tool (Admin / Editor, `/admin/meta-merge`) - Upload a `.txt` file to bulk-import metadata into existing archive entries without re-uploading the files themselves. - Parses two formats automatically: - **hobbes.txt:** dashed-separator blocks with `DIR` / `FILE` / `DESC` fields. - **pmmail.txt:** aligned `key: value` pairs (Archive Filename, Short Description, Long Description, Proposed directory, Your name, Program URL, Operating System/Version, Additional requirements). - Matches entries to archive files by normalising the `DIR` path to slugified category segments and comparing against each file's stored `category_upload_path` + `original_name`. - Three-phase workflow: upload → review report → apply. - Review page shows: - *Matched entries* — before/after diff table per field; individual checkboxes to include or exclude each entry. - *Unmatched entries* — filename-only suggestions, manual file-ID input, or Skip. - *Skipped summary* — entries marked skip listed for reference. - Merge mode: fill blank/"Unknown" fields only (default), or overwrite all existing values. - Approved files are re-indexed in the search index after merge. - Temporary sessions stored in `data/merges/` and auto-expired at 2 hours. ### Bulk Delete tool (Admin only, `/admin/bulk-delete`) - Select any category to list its files (approved and pending). - Per-page selector: 25 / 50 / 100 / All. Warning banner when "All" is chosen with more than 250 files. - "Select all on this page" and "Select ALL N files in this category" checkboxes. - JavaScript confirmation dialog shows the deletion count before committing. - Removes physical file from disk, metadata JSON, and all search index entries. - Empty category upload directories are pruned after deletion. ### Single-file delete from edit page (Admin only) - Danger Zone section added at the bottom of `/file/edit/{id}`. - Visible to admin role only. Confirmation dialog includes the filename. - Handled by `POST /admin/file-delete/{id}`; redirects to the file's category browse page after deletion. ### Download counter displayed in all views - **browse.php:** "DLs" column added to the file listing table. - **file.php:** "Downloads" row added to the file detail info table. - **search.php:** "DLs" column added to search results table. - Counter has been tracked since v1.01; this release makes it visible to all users including unauthenticated OS/2 guests. ### Approve All Web Uploads button (`/pool`) - A summary bar appears when one or more web-uploaded files are pending approval. - "Approve All Web Uploads" approves every pending web upload in a single action (`POST /pool/approve_all`, CSRF-protected). - FTP single files and FTP folder imports are intentionally excluded (they require metadata to be filled in or imported first). - JavaScript confirmation dialog shows the count before committing. ### Admin dashboard links updated - "Meta Merge" and "Bulk Delete" (styled red) buttons added to the Site Management panel. --- ## v1.02 ### FTP folder batch import (Editor+, `/pool`) - Drop an entire directory tree into `data/pool/` via FTP. - The pool listing detects folders and presents a Batch Import Folder button. - Sub-folders are mapped to nested categories using `category_find_or_create()`, which creates missing categories on the fly (slugified, de-duplicated) without overwriting existing ones. - Files receive titles derived from their filenames; required fields default to "Unknown" and can be corrected post-import. - Approve-immediately checkbox available for trusted bulk imports. - The pool folder is recursively removed after a successful import. - Reject & Delete Folder button purges the folder without importing. ### "Hobbes OG" CSS preset (Admin → CSS) - Fourth built-in color preset added. - Deep navy blue palette inspired by the original hobbes.nmsu.edu color scheme: dark blue panels, cream text, teal accents. ### New helper functions - **`category_find_or_create()`** (`includes/functions.php`) — looks up a category by name + parent; creates it with a unique slug if absent. Used by folder import. - **`category_upload_path_from_cats()`** (`includes/functions.php`) — variant of `category_upload_path()` that accepts an explicit array instead of the static-cached result. Required during batch import when the category list has been mutated but not yet written to disk. - **`rmdir_recursive()`** (`includes/functions.php`) — recursively removes a directory tree. Used to clean up pool folders after import or rejection. - **`pool_folder_file_count()`**, **`pool_folder_files()`** (`includes/storage.php`) — helpers for counting and listing files inside pool folders during batch import. ### Fixed: Pool route regex (`index.php`) - Changed `[a-z]+` to `[a-z_]+` so that `import_folder` and `reject_folder` action names are matched correctly. --- ## v1.01 (initial release) ### Core archive system - Flat-file JSON storage; no database required. - Atomic file writes via temp file + `rename()` (no `flock()`). - Custom PHP session handler using the same atomic pattern. ### Browsing and downloads - Category tree browser (`/browse`, `/browse/{slug}`). - File detail page with full metadata display. - Download streaming in 64 KB chunks with per-file download counter. - Canonical URL scheme: `/files/{cat/path}/{filename}` and `/download/{cat/path}/{filename}`. - Legacy `/file/{id}` redirects (301) to canonical URL. ### Search - Inverted keyword index (`data/index/search.json`). - AND search with OR fallback; stop-word filter; minimum 3 characters. - Index updated on approval and on metadata edit. - Admin: Rebuild Search Index action. ### User system - Four roles: guest, contributor, editor, admin. - OS/2 User-Agent auto-detection for guest browse access. - Invite-code registration (invite generates code, recipient redeems). - Open registration toggle in site settings. - Password hashing via bcrypt (`PASSWORD_BCRYPT`). ### Uploads and approval - Web upload form for contributors (`/upload`). - FTP pool import for single files (`/pool`). - Optional `.meta.json` companion file to pre-fill pool import forms. - Editor approval workflow: approve, reject, or edit before publishing. - File stored using category slug path (`uploads/{cat}/{subcat}/file`). ### File editing and organisation - Edit metadata for any approved file (`/file/edit/{id}`). - Category change moves physical file on disk and updates `stored_name`. - Legacy per-file `id/` directory layout supported and cleaned up on move. ### Admin tools - Site settings: name, tagline, open registration toggle. - CSS theme editor: 26 individually configurable colors. - Three CSS presets: OS/2 Classic, Dark Mode, Green Terminal. - Category manager: add, rename, nest, delete. - User manager: list, change role, deactivate. - Landing page and splash screen Markdown editors.