# 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 `