v3.0.0
v3.0.0 started off as a maintenance release, but as I got more and more into it, it ended up being a significant re-write of parts of the code to improve security, performance, and general code quality. At the same time I decided to move away from GitHub and v3.0.0 seemed to be a good time to do it.
⚠ Upgrade notice
This release restructures core PHP files and cannot be applied via the in-app updater. Please upgrade manually by downloading the release zip from Codeberg. Future releases without breaking changes can resume using the in-app updater.
Existing pb_remember cookies will be invalidated by the remember-me change - you will need to log in once and re-tick "remember me".
Manual upgrade steps
- Back up your site — download a copy of your entire Pure Blog directory before proceeding.
- Download the v3 release zip from the Codeberg releases page and extract it.
- Delete everything EXCEPT for the
config/,content/anddata/directories - these hold your posts, pages, images, and configuration and must not be overwritten. - Copy the new files to your existing installation, replacing everything except your
config/,content/anddata/directories. - Restore any other file(s) (like
.htaccess) that you have customised from your backup. - Log back in — your remember-me cookie will no longer be valid; tick "remember me" again if you use it.
Note: The data/ directory doesn't exist in the default build. It will only be there if you've made use of data files.
Summary
Major code overhaul covering security hardening, performance improvements, and structural refactoring:
- 6 security fixes — closed two path-traversal vulnerabilities, hardened the login lockout and remember-me systems, added PHP-execution blocking on the image directory, and tightened file upload MIME validation.
- Significant reduction in duplication — eliminated ~800 lines of duplicated or dead code: 4 copy-pasted HTTP fetch blocks collapsed into 2 helpers, identical delete handlers merged, 15-file auth boilerplate replaced by a single bootstrap include, and 3 dead functions removed.
functions.phpbroken up — the 2444-line monolith is now a 142-line loader; 100 functions live in five focused files underincludes/lib/(i18n,auth,content,template,cache).admin/settings-updates.phpsimilarly reduced from ~1000 lines to 264 by extracting its updater logic intoincludes/updater.php.- Six performance wins —
load_config()memoised,get_all_posts()andget_all_pages()published-only lists cached, metadata-only post loading for sitemap and admin content list (skips reading post bodies entirely),get_excerpt()truncates input before running 8 regexes,find_post_filepath_by_slug()routed through the existing post cache instead of re-globbing disk. - Moved source code from GitHub to Codeberg.
Security
- Autosave path traversal fix —
editor_typeis now validated against an allow-list (post/page) andslugis validated withis_safe_image_slugbefore being used to build the autosave file path. Previously an authenticated admin could write a.jsonfile to arbitrary server paths. (admin/autosave.php) - Layout name path traversal fix — The
layout:front-matter field is now stripped to[a-zA-Z0-9_-]before being joined into a file path, preventing an admin from including arbitrary.phpfiles via a crafted post. (post.php) - Image directory PHP execution block — Added
content/images/.htaccessdenying PHP execution.upload-image.phpwill also recreate this file if it is ever missing, so new installs are covered automatically. - Favicon / OG image MIME validation — The favicon and OG image upload handlers in site settings now run
finfoMIME type validation (jpeg, png, gif, webp, avif) before accepting a file, matching the protection already in place for post image uploads. (admin/settings-site.php) - IP-based login lockout — Login failure tracking moved from
$_SESSION(which an attacker could reset by dropping their cookie) to a server-side file keyed by IP address (data/login-failures.json). The 5-failure / 5-minute lockout now actually works against automated brute-force. (functions.php,admin/index.php) - Remember-me token redesign — Replaced the static HMAC token (derived from the password hash, never expiring, no server-side state) with a random selector + validator scheme. The hashed validator is stored in
data/remember-me.jsonwith an explicit 90-day expiry. Logout now deletes the server-side token. Session is regenerated on cookie restore. A newclear_all_remember_me_tokens()helper is available for use on password change. (functions.php)
Code Quality
- Admin bootstrap extracted — The repeated 4-line auth opener (
require functions.php,require_setup_redirect(),start_admin_session(),require_admin_login()) shared by 15 admin files is now inadmin/bootstrap.php. Each file does a singlerequire __DIR__ . '/bootstrap.php'instead. - Delete handlers consolidated —
admin/delete-post.phpandadmin/delete-page.phpwere identical except for entity type. Replaced with a singleadmin/delete-content.phpthat accepts a validatedtypeparameter (postorpage). - HTTP helpers extracted — Four duplicated curl/stream-fallback blocks consolidated into
pureblog_http_get()andpureblog_http_download()infunctions.php.fetch_latest_pureblog_release()moved tofunctions.phpsoadmin/dashboard.phpcan call it directly instead of inlining its own GitHub fetch. (functions.php,admin/settings-updates.php,admin/dashboard.php) - Updater logic extracted — The 21 update/backup/restore functions previously defined inline in
admin/settings-updates.php(≈780 lines of logic) are now inincludes/updater.php. The view file is reduced to controller setup + HTML (~264 lines). functions.phpsplit into focused lib files — The 2444-line monolith now delegates 100 functions to five files underincludes/lib/:i18n.php(i18n + hooks),auth.php(session/CSRF/remember-me),content.php(config, URL helpers, dates, post/page CRUD),template.php(markdown, liquid, layout rendering, search/indexes, pagination),cache.php(cache, version detection, HTTP helpers).functions.phpis a 142-line loader of constants, bootstrap functions, andrequirecalls.
Performance & Dead Code
load_config()memoised — Added astaticcache so the config file is parsed once per request instead of on every call. (includes/lib/content.php)get_all_posts()published-list cached — The published-only post list is now built once at load time and stored in a second static slot, eliminating a redundantarray_filteron every subsequent call. (includes/lib/content.php)- Metadata-only post loading —
parse_post_meta_only()reads a post file viafgets, stopping at the closing---front-matter delimiter without loading the post body.get_all_posts_meta()uses this to serve callers that only need metadata.sitemap.phpandadmin/content.phpnow skip reading post bodies entirely. Its cache is kept in sync with the full post cache viabuild_search_index(). (includes/lib/content.php) get_all_pages()published-list cached — Applied the same two-slot static cache pattern asget_all_posts(), eliminating a redundantarray_filteron every published-only call (e.g. masthead nav rendering). (includes/lib/content.php)get_excerpt()early truncation — Excerpt text is now capped at$length × 6characters before running 8 Markdown-stripping regexes, avoiding unnecessary work on large post bodies. (includes/lib/template.php)find_post_filepath_by_slug()uses post cache — Replaced a redundantglob+ full-parse loop with a lookup against theget_all_posts()static cache, eliminating a duplicate filesystem scan. (includes/lib/content.php)- Dead code removed — Deleted
render_liquid_loop(),resolve_layout_file(), andrender_post_navigation()(no callers). Deleted orphanadmin/pages.php(no links).