Closes the three problems that blocked the consolidation and records the rest
as tasks.
Licence: GPLv2 or later (WordPress.org compatible)
LICENSE is now the full GPLv2 text and NOTICE is gone (it was Apache
specific). All 101 SPDX headers read GPL-2.0-or-later and every shipped
header says "GPLv2 or later".
This also fixes an inconsistency that predated the consolidation: the theme's
style.css declared GPLv2 while its PHP files carried Apache headers.
Old/ and archive/pre-umbra-releases/ keep their original Apache headers on
purpose -- they are frozen snapshots, not shipped code.
Schema: the catalogue schema is canonical for bd_books and bd_series
Adds Umbra_Schema_Migration. The catalogue and library modules previously
both created those two tables with different columns, so whichever plugin
activated first won and the other read and wrote against the wrong shape.
The migration detects a library-shaped table, RENAMES it aside (never drops
it), creates the canonical table and copies rows across:
- book IDs preserved, keeping bd_book_meta, bd_reviews, bd_ratings and
bd_review_shortlinks valid
- title/slug/description resolved from the linked post
- the library's single isbn/asin land on the ebook columns; amazon_asin is
also set so existing product links keep working
- series migrated first so books.series_id still resolves
- post linkage rewritten as _bd_bookshelf_book_id (preserved meta key)
- publish_date kept as _bd_bookshelf_publish_date (no catalogue column)
- bd_ratings goodreads/amazon folded into rating_goodreads/rating_amazon
- rows whose post is gone are skipped with a log line, not turned into
unusable books
- duplicate slugs de-duplicated, since catalogue slug is UNIQUE
It runs before dbDelta, and on admin_init as well as activation, because
plugin updates do not re-run activation hooks. Ordering matters: dbDelta adds
columns but never drops them, so pointed at a library-shaped table it would
graft on catalogue columns and leave a table satisfying neither schema.
Umbra_Bookshelf_Helper_Install no longer creates bd_books or bd_series.
Scraping: Goodreads only, one implementation
There were three Goodreads implementations and two Amazon ones; both library
"scrapers" were empty stubs attached to cron events that fired on schedule to
do nothing, and both real scraper classes were never called at all.
Umbra_Amazon_Scraper is deleted and the duplicate HTML parsing is gone from
Umbra_Rating_Sync, which now delegates to Umbra_Goodreads_Scraper. The two
library stubs and their cron registrations are removed. One schedule
survives: bd_bookshelf_sync_ratings, every 12 hours.
The rating_amazon column is retained (it holds data and is populated during
migration) and Amazon retail links are unaffected.
A new rating fetch failure keeps the last known value instead of writing zero.
The admin ratings notice no longer warns about Amazon scraping.
Umbra_SEO is now instantiated
It registers its hooks from a constructor and the bootstrap never constructed
it, so the class was dead code and no SEO output was produced. Its meta, Open
Graph, Twitter card and sitemap hooks now attach. Completing SEO for generated
book detail pages is docs/TASKS.md #2.
Assets that 404'd
- assets/images/default-cover.png now holds a real 512x815 cover
(WorkingTitleCover_512x815.png). The helper returned this path but the
repository contained no images at all.
- email/assets/js/admin.js is committed as an empty placeholder. It was
enqueued on every email admin page but had never been committed.
Tests
- tests/schema_migration_test.php: 42 checks against a real database, driven
through the sqlite3 CLI on a throwaway file. Covers shape detection, row
mapping, ID preservation, orphan handling, rating folding, idempotency and
slug collisions.
- tests/run_all.sh runs every suite.
- The declared version is now checked against the plugin header rather than a
literal, so a version bump cannot silently break the suite.
Docs
- KNOWN-ISSUES.md rewritten: three resolved, three remaining.
- TASKS.md added: email admin behaviour, book detail page SEO, batching the
rating sync, reclaiming legacy tables, reconciling the two book models,
retiring the Old/ archive.
- SPEC and ARCHITECTURE updated for schema ownership, the migration ordering
constraint, and the single scraper.
Version 1.1.0.
302 lines
14 KiB
Markdown
302 lines
14 KiB
Markdown
# Umbra Bookshelf — Plugin Specification
|
|
|
|
**Status:** Active
|
|
**Version:** 1.1.0
|
|
**Supersedes:** `spec/archive/SPEC_BD_Bookshelf_Plugin.txt`, `spec/archive/SPEC_BD_BookHelper_Plugin.txt`
|
|
**License:** GPL-2.0-or-later (WordPress.org compatible)
|
|
|
|
---
|
|
|
|
## 1. Identity
|
|
|
|
| Field | Value |
|
|
|---|---|
|
|
| Plugin name | Umbra Bookshelf |
|
|
| Slug / folder | `umbra-bookshelf` |
|
|
| Entry point | `umbra-bookshelf.php` |
|
|
| Text domain | `umbra-bookshelf` |
|
|
| Domain path | `/languages` |
|
|
| Author | Umbra Tools |
|
|
| Requires | WordPress 6.0, PHP 7.4 |
|
|
| Version | 1.1.0 |
|
|
|
|
The three pre-merge plugins map onto modules as follows:
|
|
|
|
| Former plugin | Former version | Module | Code location |
|
|
|---|---|---|---|
|
|
| BD Bookshelf | 0.1.0 | **Books** (catalogue) | `includes/books/` |
|
|
| BD BookShelfHelper | 2.0.0 | **Library** | `includes/library/` |
|
|
| BD SimpleEmail | 2.0.0 | **Email** | `includes/email/` |
|
|
|
|
---
|
|
|
|
## 2. Naming rules
|
|
|
|
Two categories of identifier, treated differently on purpose.
|
|
|
|
### 2.1 Renamed to the Umbra Bookshelf identity
|
|
|
|
* Plugin header fields (`Plugin Name`, `Plugin URI`, `Author`, `Author URI`, `License`).
|
|
* Text domain: `bd-bookshelf`, `bd-bookshelfhelper`, `bd-simpleemail` → `umbra-bookshelf`.
|
|
* PHP classes: `BD_Bookshelf`, `BDM_*`, `BD_BookShelfHelper*`, `BD_SimpleEmail*`,
|
|
`BD_Email_Sender` → `Umbra_*`.
|
|
* Constants: `BD_BOOKSHELF_*` → `UMBRA_BOOKSHELF_*`; `BD_BHS_VERSION` →
|
|
`UMBRA_BOOKSHELF_LIBRARY_VERSION`; `BD_EMAIL_*` → `UMBRA_BOOKSHELF_EMAIL_VERSION`.
|
|
`BD_BHS_DIR` / `BD_EMAIL_URI` were removed in favour of the single
|
|
`UMBRA_BOOKSHELF_DIR` / `UMBRA_BOOKSHELF_URL` pair.
|
|
* Asset handles, one per module so they cannot shadow each other:
|
|
`umbra-bookshelf-admin`, `umbra-bookshelf-library-admin`,
|
|
`umbra-bookshelf-email-admin`, `umbra-bookshelf-gdpr`.
|
|
* JavaScript globals: `bdBookshelfAdmin` → `umbraBookshelfAdmin`,
|
|
`bdBookshelfGdpr` → `umbraBookshelfGdpr`, `bdmAdmin` → `umbraBookshelfLibraryAdmin`.
|
|
* Admin menu slugs (see §5).
|
|
|
|
### 2.2 Deliberately **not** renamed
|
|
|
|
These are the persisted contract with live installations and with published
|
|
post content. Renaming any of them silently orphans existing data.
|
|
|
|
| Contract | Values |
|
|
|---|---|
|
|
| Table names | `bd_books`, `bd_series`, `bd_book_authors`, `bd_authors`, `bd_book_seo`, `bd_retail_links`, `bd_retail_sites`, `bd_shortcodes`, `bd_shortcode_clicks`, `bd_book_meta`, `bd_pen_names`, `bd_reviews`, `bd_ratings`, `bd_review_shortlinks`, `bd_subscribers`, `bd_newsletters` |
|
|
| Option keys | `bd_bookshelf_db_version`, `bd_bookshelf_activation_time`, `bdm_affiliate_codes`, `bdm_goodreads_author_id`, `bdm_awards`, `bdm_gmail_address`, `bdm_gmail_password`, `bdm_sender_name`, `bdm_reply_to`, `bdm_gemini_api_key` |
|
|
| Post meta | `_bd_bookshelf_book_id` |
|
|
| Transient prefix | `_transient_bd_bookshelf_*` |
|
|
| Nonce actions | `bd_bookshelf_admin_nonce`, `bd_bookshelf_import_posts`, `bd_bookshelf_import_csv`, `bd_bookshelf_admin`, `bd_bookshelf_gdpr_nonce` |
|
|
| Cron hooks | `bd_bookshelf_sync_ratings` (active). `bd_bookshelf_goodreads_refresh` and `bd_bookshelf_amazon_scrape` are **retired** but still cleared on deactivate/uninstall so upgrades leave no stale schedules |
|
|
| AJAX actions | `bd_bookshelf_*`, `bdm_update_links`, `bdm_autosave`, `bdm_ajax_signup`, `bdm_signup_submit`, `bdm_unsubscribe_submit` |
|
|
| Shortcodes | `[bd-book]`, `[bd-books]`, `[bdm_signup]` |
|
|
| Content types | post type `book`, taxonomy `series` |
|
|
| Cookies | `bd_bookshelf_consent` |
|
|
| CSS classes | `bd-bookshelf-*`, `bd-email-*` (paired HTML/CSS, may be styled by themes) |
|
|
|
|
One legacy PHP class name is retained by design: `class-umbra-theme-compatibility.php`
|
|
declares `Umbra_Theme_Compatibility` and then
|
|
`class_alias('Umbra_Theme_Compatibility', 'BD_BookShelfHelper')` so the
|
|
unmodified `theme/` (bd-bookshelf) theme's static `BD_BookShelfHelper::` calls keep working.
|
|
|
|
---
|
|
|
|
## 3. Architecture
|
|
|
|
```
|
|
umbra-bookshelf/
|
|
├── umbra-bookshelf.php bootstrap: constants, requires, singleton, single top-level menu
|
|
├── uninstall.php drops every table and option from all three modules
|
|
├── includes/
|
|
│ ├── class-umbra-schema-migration.php reconciles pre-consolidation schemas
|
|
│ ├── books/ catalogue: books, authors, series, retail, ratings,
|
|
│ │ shortcodes, SEO, GDPR, Goodreads scraper, admin, import
|
|
│ ├── library/ book post type + taxonomy, pen names, data entry,
|
|
│ │ shortlinks, reviews, ratings, migrate
|
|
│ └── email/ subscribers, newsletters, AI-assisted composition, sending
|
|
├── admin/ css, js catalogue admin assets
|
|
├── public/ css, js front-end + GDPR consent assets
|
|
├── library/ assets library admin assets
|
|
├── email/ assets email admin assets
|
|
├── assets/ images default-cover.png
|
|
├── languages/ translation catalogues
|
|
├── spec/ this document; archive/ holds pre-merge specs
|
|
├── docs/ architecture and known issues
|
|
└── tests/ verify_structure.py, smoke_runtime.php
|
|
```
|
|
|
|
**Boot order.** Modules are constructed on `plugins_loaded`, not `init`. Several
|
|
modules register their own `init` callbacks (post type, taxonomy, shortcodes,
|
|
rewrite rules); constructing them from inside an `init` callback makes it
|
|
accidental whether those callbacks run. A `$modules_initialized` flag prevents
|
|
double construction.
|
|
|
|
---
|
|
|
|
## 4. Module responsibilities
|
|
|
|
### 4.1 Books (catalogue) — `includes/books/`
|
|
|
|
Owns the custom-table catalogue model: books with ISBN-10/13 and ASIN variants,
|
|
author records and pen-name linkage, series, retail sites and per-format retail
|
|
links, Goodreads and Amazon rating synchronisation, SEO rows, shortlink
|
|
click tracking, GDPR consent, and CSV/post import.
|
|
|
|
Classes: `Umbra_Database`, `Umbra_Book_Manager`, `Umbra_Authors`, `Umbra_Series`,
|
|
`Umbra_Retail_Sites`, `Umbra_Rating_Sync`, `Umbra_Shortcodes`, `Umbra_SEO`,
|
|
`Umbra_GDPR`, `Umbra_Goodreads_Scraper`, `Umbra_Admin`, `Umbra_Migration`,
|
|
`Umbra_Theme_Compatibility`.
|
|
|
|
**This module owns `{prefix}bd_books` and `{prefix}bd_series`.** Its schema is
|
|
canonical; the library module no longer creates either table.
|
|
|
|
**Goodreads is the only rating source.** `Umbra_Goodreads_Scraper` is the single
|
|
scraper; `Umbra_Rating_Sync` delegates all HTML parsing to it. Amazon rating
|
|
scraping was removed. The `rating_amazon` column and Amazon retail links are
|
|
unaffected.
|
|
|
|
`Umbra_SEO` supplies per-book meta description, Open Graph, Twitter cards and
|
|
sitemap weighting for generated book detail pages. Completing it is
|
|
`docs/TASKS.md` §2.
|
|
|
|
### 4.2 Library — `includes/library/`
|
|
|
|
Owns the WordPress-native model: registers post type `book` and taxonomy
|
|
`series`, pen names, a focused data-entry screen, shortlink handling, reviews and
|
|
ratings, and a migration screen for data created by earlier plugins.
|
|
|
|
Classes: `Umbra_Bookshelf_Helper`, `Umbra_Bookshelf_Helper_Admin`,
|
|
`Umbra_Bookshelf_Helper_Install`.
|
|
|
|
Owns `bd_book_meta`, `bd_pen_names`, `bd_reviews`, `bd_ratings` and
|
|
`bd_review_shortlinks`. It does **not** create `bd_books` or `bd_series` — see
|
|
§5a.
|
|
|
|
### 4.3 Email — `includes/email/`
|
|
|
|
Owns newsletter delivery: subscriber and newsletter tables, `[bdm_signup]`,
|
|
signup and unsubscribe form handling (AJAX and `admin_post`), Gemini-assisted
|
|
newsletter composition, and SMTP/Gmail sending.
|
|
|
|
Classes: `Umbra_Simple_Email`, `Umbra_Simple_Email_Admin`,
|
|
`Umbra_Simple_Email_Install`, `Umbra_Email_Sender`.
|
|
|
|
---
|
|
|
|
## 5. Admin menu
|
|
|
|
One top-level menu, registered exactly once by the bootstrap at
|
|
`admin_menu` priority 9. Each module contributes only submenus beneath it.
|
|
|
|
**Umbra Bookshelf** (`umbra-bookshelf`, `dashicons-book-alt`, position 30)
|
|
|
|
| Submenu slug | Module | Capability |
|
|
|---|---|---|
|
|
| `umbra-bookshelf` | bootstrap | `edit_posts` |
|
|
| `umbra-bookshelf-all` | books | `manage_options` |
|
|
| `umbra-bookshelf-add` | books | `manage_options` |
|
|
| `umbra-bookshelf-authors` | books | `manage_options` |
|
|
| `umbra-bookshelf-series` | books | `manage_options` |
|
|
| `umbra-bookshelf-sites` | books | `manage_options` |
|
|
| `umbra-bookshelf-shortcodes` | books | `manage_options` |
|
|
| `umbra-bookshelf-sync` | books | `manage_options` |
|
|
| `umbra-bookshelf-settings` | books | `manage_options` |
|
|
| `umbra-bookshelf-import` | books | `manage_options` |
|
|
| `edit.php?post_type=book` | library | `edit_posts` |
|
|
| `post-new.php?post_type=book` | library | `edit_posts` |
|
|
| `umbra-bookshelf-data-entry` | library | `edit_posts` |
|
|
| `edit-tags.php?taxonomy=series&post_type=book` | library | `manage_categories` |
|
|
| `umbra-bookshelf-pen-names` | library | `manage_options` |
|
|
| `umbra-bookshelf-library-settings` | library | `manage_options` |
|
|
| `umbra-bookshelf-migrate` | library | `manage_options` |
|
|
| `umbra-bookshelf-subscribers` | email | `manage_options` |
|
|
| `umbra-bookshelf-newsletter` | email | `manage_options` |
|
|
| `umbra-bookshelf-email-settings` | email | `manage_options` |
|
|
|
|
Catalogue and library settings are deliberately **separate** screens with
|
|
separate slugs; the pre-merge plugins both claimed `bd-bookshelf-settings`, so one
|
|
of the two was unreachable.
|
|
|
|
---
|
|
|
|
## 5a. Schema ownership and reconciliation
|
|
|
|
`bd_books` and `bd_series` are owned by the catalogue module. Before the
|
|
consolidation the library module created the same two table names with a
|
|
different column set, so whichever plugin activated first won and the other
|
|
read and wrote against the wrong shape.
|
|
|
|
`Umbra_Schema_Migration` reconciles a library-shaped installation. It runs
|
|
**before** `dbDelta`, on activation and on `admin_init` when the recorded schema
|
|
version is behind, because plugin updates do not re-run activation hooks.
|
|
|
|
Ordering matters: `dbDelta` does not drop columns, so pointed at a library-shaped
|
|
table it would graft catalogue columns on and leave a table satisfying neither
|
|
schema. The migration therefore ensures the table is either correctly
|
|
catalogue-shaped or absent before `dbDelta` is allowed near it.
|
|
|
|
Behaviour:
|
|
|
|
| Step | Detail |
|
|
|---|---|
|
|
| Detect | `bd_books` is library-shaped if it has `post_id` and no `title`; `bd_series` if it has `blurb` and no `description` |
|
|
| Set aside | `RENAME TABLE` to `bd_*_legacy_library`. **Never dropped** — rows stay recoverable |
|
|
| Create | canonical schema via `Umbra_Database::create_tables()` |
|
|
| Copy | series first, so `books.series_id` still resolves |
|
|
| Preserve | book IDs (keeps `bd_book_meta`, `bd_reviews`, `bd_ratings`, `bd_review_shortlinks` valid) |
|
|
| Map | `title`/`slug`/`description` from the linked post; library's `isbn`/`asin` onto the ebook columns; `amazon_asin` also set so product links keep working |
|
|
| Re-link | writes `_bd_bookshelf_book_id`; keeps `publish_date` as `_bd_bookshelf_publish_date` (no catalogue column exists) |
|
|
| Fold ratings | `bd_ratings` goodreads/amazon values into `rating_goodreads` / `rating_amazon`, only where still zero |
|
|
| Skip | rows whose post no longer exists, with a log entry, rather than creating an unusable book |
|
|
| De-duplicate | slugs, because the catalogue's `slug` column is `UNIQUE` |
|
|
|
|
Tracked by the `umbra_bookshelf_schema_version` option (current target: `2`).
|
|
Re-running is a no-op. A failure does not advance the version, so the next
|
|
attempt retries.
|
|
|
|
## 6. Lifecycle
|
|
|
|
* **Activate:** reconcile the schema (`Umbra_Schema_Migration::maybe_migrate()`),
|
|
then create catalogue tables (`Umbra_Database::create_tables()`), library tables
|
|
+ roles (`Umbra_Bookshelf_Helper_Install::activate()`), email tables
|
|
(`Umbra_Simple_Email_Install::activate()`), flush rewrites, record
|
|
`bd_bookshelf_activation_time`.
|
|
* **Update:** plugin updates do not re-run activation, so `admin_init` re-checks
|
|
the schema version and reconciles if it is behind.
|
|
* **Deactivate:** clear all cron hooks named in §2.2 — including the retired
|
|
`bd_bookshelf_goodreads_refresh` and `bd_bookshelf_amazon_scrape`, so no stale
|
|
schedules survive an upgrade — run module deactivators, flush rewrites.
|
|
* **Uninstall:** drop every table across all three modules (deduplicated so
|
|
`bd_books`/`bd_series` are dropped once), delete `bd_bookshelf_%` options and
|
|
transients, delete the `bdm_*` options, clear cron. Guarded by
|
|
`WP_UNINSTALL_PLUGIN`. Set-aside `bd_*_legacy_library` tables are **not**
|
|
dropped; removing those is a deliberate operator action (see `docs/TASKS.md` §4).
|
|
|
|
---
|
|
|
|
## 7. Verification
|
|
|
|
```bash
|
|
umbra-bookshelf/tests/run_all.sh # every suite
|
|
```
|
|
|
|
or individually:
|
|
|
|
```bash
|
|
python3 umbra-bookshelf/tests/verify_structure.py # static checks
|
|
php umbra-bookshelf/tests/smoke_runtime.php front # front-end boot
|
|
php umbra-bookshelf/tests/smoke_runtime.php admin # admin boot + menu shape
|
|
php umbra-bookshelf/tests/schema_migration_test.php # migration vs. a real database
|
|
```
|
|
|
|
The static check enforces: valid PHP syntax, no class declared twice, no
|
|
reference to an undefined class, every `require_once` and enqueued asset
|
|
resolves on disk, no leftover pre-merge identifiers in shipped source, and all
|
|
preserved contracts of §2.2 still present.
|
|
|
|
The runtime check stubs WordPress, loads the bootstrap, fires `plugins_loaded`,
|
|
`init` and `admin_menu`, and asserts the post type, taxonomy and shortcodes
|
|
register, that exactly one top-level menu appears with all 20 expected submenus
|
|
under it, that no callback is registered twice on a hook, and that asset handles
|
|
are unique per module. The declared version is checked against the plugin
|
|
header rather than a literal, so a version bump cannot silently break it.
|
|
|
|
The schema check drives a throwaway SQLite database through the `sqlite3` CLI —
|
|
no credentials, no shared server — and verifies shape detection, row mapping,
|
|
ID preservation, orphan handling, rating folding, idempotency and slug
|
|
collisions against real data. MySQL `RENAME TABLE` is translated to SQLite's
|
|
`ALTER TABLE ... RENAME TO` by the shim, since the migration itself targets
|
|
MySQL.
|
|
|
|
---
|
|
|
|
## 8. Open issues and tasks
|
|
|
|
The three problems that blocked the consolidation — the dual `bd_books` /
|
|
`bd_series` schemas, duplicated scraping, and the Apache/GPL licence mismatch —
|
|
are resolved as of 1.1.0.
|
|
|
|
Remaining, in `docs/KNOWN-ISSUES.md`: the rating sync blocks a worker with
|
|
`sleep()`; the catalogue and library still model a book differently, with no
|
|
synchronisation between them; and set-aside legacy tables are never reclaimed.
|
|
|
|
Intended work, in `docs/TASKS.md`: the email module's admin behaviour, completing
|
|
SEO for generated book detail pages, batching the rating sync, cleaning up legacy
|
|
tables, reconciling the two book models, and retiring the `Old/` archive.
|