A web-based internal proofing system for organizing, uploading, and browsing manuscript images. Built with PHP and MySQL, featuring a file browser with gallery view, zip folder download with progress tracking, and a multi-user admin interface.
- Sign-in, a home page of module cards and user management (admin / standard roles), as one small Vue 3 shell
- File browser: folder tree, cards, list and thumbnail views, search across all folders, filters and sorting
- A details panel for a file or folder (title, description, rename, move), and for several selected items
- New folder, move, remove (administrators), undo for moves, drag-and-drop between folders, a right-click menu and keys
- Upload into the open folder, by button or by dropping files from the desktop, with a progress tray
- Zip a folder with a progress bar; administrators can edit the names, titles and descriptions of a folder's files together
- Image viewer with next/previous and rotate (for looking only)
- Optional modules: the HTML editor (
ips/html_editor/), the TEI/XML editor, backup download - The file editor shows the artefact a document describes (page images or a PDF) beside it, found through a project, the TEI's facsimile, a
<id>-pagesfolder or<id>.pdf, and follows the cursor by folio label - The file editor can check XML against the Menota schema, line by line (
file_editor/validate_xml.py: a command-line tool, and the editor's Validate button on a host with a fast enough lxml; the schema is fetched bytools/fetch-menota-schema.sh, not kept here) - Optional Projects module (
ips/projects/): group files of the file store into projects (role, order, label, note), from the upload dialog, the details panel or a selection, and filter the browser to one project
No bundler: Vue 3 (the global build) and plain scripts in ips/js/ui/, styled by a Tailwind stylesheet compiled in advance
(ips/css/ips-ui.css). Nothing loads from a CDN: Vue, the Newsreader / IBM Plex fonts and the icons are in the tree.
Colours, fonts and the logo are CSS variables (ips/css/ips-theme.css); a site re-themes by adding ips/css/site-theme.css
and can add panels to the home page from ips/js/site-home.js (IPS.registerHomePanel). Rebuild the stylesheet with
tools/build-ui-css.sh after adding a Tailwind class, and refresh the vendored assets with tools/vendor-ui-assets.sh.
The file browser uses the existing ips/json/*.php endpoints and upload/index.php, so the server's path confinement and
ownership rules apply to everything it does: standard users may add, move and rename only inside files/<their username>.
- PHP 8.x with extensions:
mysqli,zip,session - MySQL 5.7+ or MariaDB 10.3+
- nginx (for zip download via X-Accel-Redirect)
- Sufficient disk space for uploads and temporary zip files
git clone git@github.com:RyanEiri/mssdocs.gitPlace the repository root so that the ips/ directory is accessible under your web root, e.g. /var/www/myproject/.
Create a MySQL database and user:
CREATE DATABASE your_db_name CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'your_db_user'@'localhost' IDENTIFIED BY 'your_db_password';
GRANT ALL PRIVILEGES ON your_db_name.* TO 'your_db_user'@'localhost';Import the schema:
mysql -u your_db_user -p your_db_name < ips/schema.sqlCopy the example config and fill in your values:
cp ips/php/classes/config.example.php ips/php/classes/config.phpEdit config.php:
define("DB_HOST", "localhost");
define("DB_USER", "your_db_user");
define("DB_PASS", "your_db_password");
define("DB_NAME", "your_db_name");
define("SITE_NAME", "Your Project Name");
define("SITE_ABBR", "YPN");
define("SITE_DESCRIPTION", "A brief description of your project.");
define("COPYRIGHT_HOLDER", "Your Name or Organization");
define("COPYRIGHT_YEARS", "2024");The web server needs write access to the upload directories:
chown -R www-data:www-data ips/upload/files ips/upload/tmp ips/upload/archives ips/backupsThe zip download feature uses nginx's X-Accel-Redirect to serve files without loading them into PHP memory. Add the following to your nginx server block, adjusting the path to match your installation:
# Internal location for zip download — served by nginx, not PHP
location /ips/upload/tmp/ {
internal;
alias /var/www/myproject/ips/upload/tmp/;
}Insert an initial admin user directly into the database. Passwords are stored with PHP's password_hash() (bcrypt) and checked with password_verify(), so a plain SHA-512 hash will not log in. Generate the hash and the secretword:
php -r 'echo password_hash("yourpassword", PASSWORD_DEFAULT), "\n";'
openssl rand -hex 30 # 60 characters, for the secretwordINSERT INTO users (username, email, password, secretword, admin)
VALUES ('admin', 'admin@example.com', '<password_hash output>', '<60-char-random-string>', 1);The secretword is a per-user random token that signs the login cookie; changing a user's password replaces it, which signs that user out.
- Login cookie:
loginisHttpOnly,SameSite=Lax, andSecureunless you defineSITE_ENV(seeconfig.example.php; only needed for a plain-HTTP local copy). - User management is CSRF-protected.
json-add_user.php,json-change_user.phpandjson-remove_user.phpaccept only same-origin POSTs that carry the signed-in user's token in anX-CSRFheader;users.phpsends it on every request ($.ajaxSetup). If you build your own client for those endpoints, read the token fromUserCookie::CsrfToken()on a page the user is signed in to. - Guards: an admin cannot delete their own account, the last administrator cannot be removed or demoted, and editing a user with the password fields left blank keeps their current password (and their session).
When pulling updates from the repository, re-check config.example.php for any newly added constants and add them to your live config.php.
GPL-3.0. See LICENSE.
Third-party libraries included in ips/js/ and ips/css/ are MIT or Apache 2.0 licensed and retain their original copyright headers.