Skip to content

v3.0.0

Choose a tag to compare

@kevquirk kevquirk released this 19 Jul 12:08

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.

Read more about that here.

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

  1. Back up your site — download a copy of your entire Pure Blog directory before proceeding.
  2. Download the v3 release zip from the Codeberg releases page and extract it.
  3. Delete everything EXCEPT for the config/, content/ and data/ directories - these hold your posts, pages, images, and configuration and must not be overwritten.
  4. Copy the new files to your existing installation, replacing everything except your config/, content/ and data/ directories.
  5. Restore any other file(s) (like .htaccess) that you have customised from your backup.
  6. 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.php broken up — the 2444-line monolith is now a 142-line loader; 100 functions live in five focused files under includes/lib/ (i18n, auth, content, template, cache). admin/settings-updates.php similarly reduced from ~1000 lines to 264 by extracting its updater logic into includes/updater.php.
  • Six performance winsload_config() memoised, get_all_posts() and get_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 fixeditor_type is now validated against an allow-list (post/page) and slug is validated with is_safe_image_slug before being used to build the autosave file path. Previously an authenticated admin could write a .json file 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 .php files via a crafted post. (post.php)
  • Image directory PHP execution block — Added content/images/.htaccess denying PHP execution. upload-image.php will 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 finfo MIME 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.json with an explicit 90-day expiry. Logout now deletes the server-side token. Session is regenerated on cookie restore. A new clear_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 in admin/bootstrap.php. Each file does a single require __DIR__ . '/bootstrap.php' instead.
  • Delete handlers consolidatedadmin/delete-post.php and admin/delete-page.php were identical except for entity type. Replaced with a single admin/delete-content.php that accepts a validated type parameter (post or page).
  • HTTP helpers extracted — Four duplicated curl/stream-fallback blocks consolidated into pureblog_http_get() and pureblog_http_download() in functions.php. fetch_latest_pureblog_release() moved to functions.php so admin/dashboard.php can 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 in includes/updater.php. The view file is reduced to controller setup + HTML (~264 lines).
  • functions.php split into focused lib files — The 2444-line monolith now delegates 100 functions to five files under includes/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.php is a 142-line loader of constants, bootstrap functions, and require calls.

Performance & Dead Code

  • load_config() memoised — Added a static cache 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 redundant array_filter on every subsequent call. (includes/lib/content.php)
  • Metadata-only post loadingparse_post_meta_only() reads a post file via fgets, 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.php and admin/content.php now skip reading post bodies entirely. Its cache is kept in sync with the full post cache via build_search_index(). (includes/lib/content.php)
  • get_all_pages() published-list cached — Applied the same two-slot static cache pattern as get_all_posts(), eliminating a redundant array_filter on every published-only call (e.g. masthead nav rendering). (includes/lib/content.php)
  • get_excerpt() early truncation — Excerpt text is now capped at $length × 6 characters 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 redundant glob + full-parse loop with a lookup against the get_all_posts() static cache, eliminating a duplicate filesystem scan. (includes/lib/content.php)
  • Dead code removed — Deleted render_liquid_loop(), resolve_layout_file(), and render_post_navigation() (no callers). Deleted orphan admin/pages.php (no links).