From dec732931cbd64c0b9a82da8013375983fb48d1f Mon Sep 17 00:00:00 2001 From: bdm Date: Sun, 18 Oct 2026 19:55:36 -0700 Subject: [PATCH] Consolidate three plugins into Umbra Bookshelf Merge BD Bookshelf (0.1.0), BD BookShelfHelper (2.0.0) and BD SimpleEmail (2.0.0) into a single plugin released as Umbra Bookshelf 1.0.0, with the BD Bookshelf theme left as a separate, unrenamed artifact. Modules includes/books/ catalogue, ISBN/ASIN, series, retail, ratings, shortcodes, SEO, GDPR, scrapers, admin, CSV/post import includes/library/ book post type + series taxonomy, pen names, data entry, shortlinks, reviews, migrate includes/email/ subscribers, newsletters, composition, sending Renamed: plugin headers, text domains (all three -> umbra-bookshelf), classes (BD_*/BDM_* -> Umbra_*), constants (-> UMBRA_BOOKSHELF_*), asset handles, JS globals and admin slugs. Deliberately not renamed -- persisted contract with live installs and with published post content: table names, option keys, post meta keys, transients, nonce actions, cron hooks, AJAX actions, shortcodes ([bd-book], [bd-books], [bdm_signup]), post type `book`, taxonomy `series`, cookies and CSS classes. BD_BookShelfHelper survives only as a class_alias for the unmodified theme. Collisions resolved * class-umbra-theme-compatibility.php declared BD_BookShelfHelper, the same name as the library module's helper. Latent while the plugins were separate (only fired when both were active), but an unconditional fatal once merged. Renamed to Umbra_Theme_Compatibility with a class_alias. * All three modules called add_menu_page. One top-level menu is now registered by the bootstrap at admin_menu priority 9. * BD Bookshelf and BookShelfHelper both claimed bd-bookshelf-settings, leaving one Settings screen unreachable -> umbra-bookshelf-settings and umbra-bookshelf-library-settings. * Catalogue and library both enqueued bd-bookshelf-admin; the second enqueue was silently discarded -> unique handles per module. * Umbra_Admin and Umbra_Migration both registered bd_bookshelf_import_csv, so one handler silently replaced the other. The import page now lives only in Umbra_Admin (the variant wired to the admin JS); Umbra_Migration keeps only the preview/run endpoints it uniquely provides. * Modules were constructed from an init callback, so their own init registrations raced the hook being dispatched. Now constructed on plugins_loaded behind a re-entrancy guard. Also * Missing assets created: assets/images/default-cover.png (referenced but never committed, so every cover-less book 404'd) and email/assets/js/ admin.js (enqueued on every email admin page but never committed). * Library module's strpos($hook, 'bd-bookshelf') asset check matched every catalogue and email screen once slugs shared the umbra-bookshelf- prefix; now matched against its own slug list. * Consolidated uninstall deduplicates the double drop of bd_books/bd_series. * Specs aligned to the new name; pre-merge specs retained under spec/archive/ with provenance notes. * Tests added: tests/verify_structure.py (static) and tests/smoke_runtime.php (WordPress-stub runtime boot, front-end and admin contexts). Deferred and recorded in docs/KNOWN-ISSUES.md: the catalogue and library modules still create bd_books and bd_series with two incompatible schemas (pre-existing, now unconditional), and Goodreads/Amazon scraping is still implemented twice on shared cron hooks. --- bd-bookshelf-plugin/bd-bookshelf.php | 158 -------- bd-bookshelf-plugin/readme.txt | 77 ---- bd-bookshelf-plugin/uninstall.php | 30 -- bd-bookshelfhelper/bd-bookshelfhelper.php | 61 --- bd-bookshelfhelper/readme.txt | 81 ---- bd-bookshelfhelper/uninstall.php | 39 -- bd-simpleemail/bd-simpleemail.php | 52 --- bd-simpleemail/readme.txt | 73 ---- bd-simpleemail/uninstall.php | 32 -- .../admin/css/umbra-bookshelf-admin.css | 2 +- .../admin/js/umbra-bookshelf-admin.js | 66 +-- .../assets/images/default-cover.png | Bin 0 -> 2888 bytes umbra-bookshelf/docs/ARCHITECTURE.md | 70 ++++ umbra-bookshelf/docs/KNOWN-ISSUES.md | 132 ++++++ .../email}/assets/css/admin.css | 0 umbra-bookshelf/email/assets/js/admin.js | 11 + .../includes/books/class-umbra-admin.php | 377 +++++++++--------- .../books/class-umbra-amazon-scraper.php | 4 +- .../includes/books/class-umbra-authors.php | 18 +- .../books/class-umbra-book-manager.php | 46 +-- .../includes/books/class-umbra-database.php | 6 +- .../includes/books/class-umbra-gdpr.php | 14 +- .../books/class-umbra-goodreads-scraper.php | 4 +- .../includes/books/class-umbra-migration.php | 72 ++-- .../books/class-umbra-rating-sync.php | 12 +- .../books/class-umbra-retail-sites.php | 24 +- .../includes/books/class-umbra-seo.php | 14 +- .../includes/books/class-umbra-series.php | 18 +- .../includes/books/class-umbra-shortcodes.php | 24 +- .../books/class-umbra-theme-compatibility.php | 43 +- .../email/class-umbra-email-admin.php | 36 +- .../email/class-umbra-email-install.php | 4 +- .../email/class-umbra-email-sender.php | 18 +- .../email/class-umbra-simple-email.php | 69 ++-- .../library/class-umbra-bookshelf-helper.php | 60 +-- .../library/class-umbra-library-admin.php | 374 ++++++++--------- .../library/class-umbra-library-install.php | 6 +- .../library}/assets/css/admin.css | 0 .../library}/assets/js/admin.js | 0 .../public/css/umbra-bookshelf.css | 2 +- .../public/js/umbra-bookshelf-gdpr.js | 18 +- .../public/js/umbra-bookshelf.js | 2 +- umbra-bookshelf/readme.txt | 81 ++++ .../spec/SPEC_Umbra_Bookshelf_Plugin.md | 224 +++++++++++ umbra-bookshelf/spec/archive/README.md | 24 ++ .../archive/SPEC_BD_BookHelper_Plugin.txt | 0 .../spec/archive/SPEC_BD_Bookshelf_Plugin.txt | 0 .../bd-bookshelf-plugin-questions-0.0.1.md | 0 .../bd-bookshelf-plugin-questions-0.0.2.md | 0 .../archive/bd-bookshelf-plugin-spec-0.0.1.md | 0 umbra-bookshelf/tests/smoke_runtime.php | 307 ++++++++++++++ umbra-bookshelf/tests/verify_structure.py | 180 +++++++++ umbra-bookshelf/umbra-bookshelf.php | 318 +++++++++++++++ umbra-bookshelf/uninstall.php | 71 ++++ 54 files changed, 2091 insertions(+), 1263 deletions(-) delete mode 100644 bd-bookshelf-plugin/bd-bookshelf.php delete mode 100644 bd-bookshelf-plugin/readme.txt delete mode 100644 bd-bookshelf-plugin/uninstall.php delete mode 100644 bd-bookshelfhelper/bd-bookshelfhelper.php delete mode 100644 bd-bookshelfhelper/readme.txt delete mode 100644 bd-bookshelfhelper/uninstall.php delete mode 100644 bd-simpleemail/bd-simpleemail.php delete mode 100644 bd-simpleemail/readme.txt delete mode 100644 bd-simpleemail/uninstall.php rename bd-bookshelf-plugin/admin/css/bd-bookshelf-admin.css => umbra-bookshelf/admin/css/umbra-bookshelf-admin.css (99%) rename bd-bookshelf-plugin/admin/js/bd-bookshelf-admin.js => umbra-bookshelf/admin/js/umbra-bookshelf-admin.js (94%) create mode 100644 umbra-bookshelf/assets/images/default-cover.png create mode 100644 umbra-bookshelf/docs/ARCHITECTURE.md create mode 100644 umbra-bookshelf/docs/KNOWN-ISSUES.md rename {bd-simpleemail => umbra-bookshelf/email}/assets/css/admin.css (100%) create mode 100644 umbra-bookshelf/email/assets/js/admin.js rename bd-bookshelf-plugin/includes/class-bdm-admin.php => umbra-bookshelf/includes/books/class-umbra-admin.php (81%) rename bd-bookshelf-plugin/includes/class-bdm-amazon-scraper.php => umbra-bookshelf/includes/books/class-umbra-amazon-scraper.php (99%) rename bd-bookshelf-plugin/includes/class-bdm-authors.php => umbra-bookshelf/includes/books/class-umbra-authors.php (90%) rename bd-bookshelf-plugin/includes/class-bdm-book-manager.php => umbra-bookshelf/includes/books/class-umbra-book-manager.php (91%) rename bd-bookshelf-plugin/includes/class-bdm-database.php => umbra-bookshelf/includes/books/class-umbra-database.php (98%) rename bd-bookshelf-plugin/includes/class-bdm-gdpr.php => umbra-bookshelf/includes/books/class-umbra-gdpr.php (90%) rename bd-bookshelf-plugin/includes/class-bdm-goodreads-scraper.php => umbra-bookshelf/includes/books/class-umbra-goodreads-scraper.php (99%) rename bd-bookshelf-plugin/includes/class-bdm-migration.php => umbra-bookshelf/includes/books/class-umbra-migration.php (82%) rename bd-bookshelf-plugin/includes/class-bdm-rating-sync.php => umbra-bookshelf/includes/books/class-umbra-rating-sync.php (95%) rename bd-bookshelf-plugin/includes/class-bdm-retail-sites.php => umbra-bookshelf/includes/books/class-umbra-retail-sites.php (92%) rename bd-bookshelf-plugin/includes/class-bdm-seo.php => umbra-bookshelf/includes/books/class-umbra-seo.php (95%) rename bd-bookshelf-plugin/includes/class-bdm-series.php => umbra-bookshelf/includes/books/class-umbra-series.php (88%) rename bd-bookshelf-plugin/includes/class-bdm-shortcodes.php => umbra-bookshelf/includes/books/class-umbra-shortcodes.php (91%) rename bd-bookshelf-plugin/includes/class-bdm-theme-compatibility.php => umbra-bookshelf/includes/books/class-umbra-theme-compatibility.php (76%) rename bd-simpleemail/inc/class-email-admin.php => umbra-bookshelf/includes/email/class-umbra-email-admin.php (92%) rename bd-simpleemail/inc/class-email-install.php => umbra-bookshelf/includes/email/class-umbra-email-install.php (96%) rename bd-simpleemail/inc/class-email-sender.php => umbra-bookshelf/includes/email/class-umbra-email-sender.php (93%) rename bd-simpleemail/inc/class-simpleemail.php => umbra-bookshelf/includes/email/class-umbra-simple-email.php (86%) rename bd-bookshelfhelper/inc/class-bookhelper.php => umbra-bookshelf/includes/library/class-umbra-bookshelf-helper.php (95%) rename bd-bookshelfhelper/inc/class-admin.php => umbra-bookshelf/includes/library/class-umbra-library-admin.php (78%) rename bd-bookshelfhelper/inc/class-install.php => umbra-bookshelf/includes/library/class-umbra-library-install.php (97%) rename {bd-bookshelfhelper => umbra-bookshelf/library}/assets/css/admin.css (100%) rename {bd-bookshelfhelper => umbra-bookshelf/library}/assets/js/admin.js (100%) rename bd-bookshelf-plugin/public/css/bd-bookshelf.css => umbra-bookshelf/public/css/umbra-bookshelf.css (98%) rename bd-bookshelf-plugin/public/js/bd-bookshelf-gdpr.js => umbra-bookshelf/public/js/umbra-bookshelf-gdpr.js (76%) rename bd-bookshelf-plugin/public/js/bd-bookshelf.js => umbra-bookshelf/public/js/umbra-bookshelf.js (90%) create mode 100644 umbra-bookshelf/readme.txt create mode 100644 umbra-bookshelf/spec/SPEC_Umbra_Bookshelf_Plugin.md create mode 100644 umbra-bookshelf/spec/archive/README.md rename SPEC_BD_BookHelper_Plugin.txt => umbra-bookshelf/spec/archive/SPEC_BD_BookHelper_Plugin.txt (100%) rename SPEC_BD_Bookshelf_Plugin.txt => umbra-bookshelf/spec/archive/SPEC_BD_Bookshelf_Plugin.txt (100%) rename bd-bookshelf-plugin-questions-0.0.1.md => umbra-bookshelf/spec/archive/bd-bookshelf-plugin-questions-0.0.1.md (100%) rename bd-bookshelf-plugin-questions-0.0.2.md => umbra-bookshelf/spec/archive/bd-bookshelf-plugin-questions-0.0.2.md (100%) rename bd-bookshelf-plugin-spec-0.0.1.md => umbra-bookshelf/spec/archive/bd-bookshelf-plugin-spec-0.0.1.md (100%) create mode 100644 umbra-bookshelf/tests/smoke_runtime.php create mode 100644 umbra-bookshelf/tests/verify_structure.py create mode 100644 umbra-bookshelf/umbra-bookshelf.php create mode 100644 umbra-bookshelf/uninstall.php diff --git a/bd-bookshelf-plugin/bd-bookshelf.php b/bd-bookshelf-plugin/bd-bookshelf.php deleted file mode 100644 index 49700da..0000000 --- a/bd-bookshelf-plugin/bd-bookshelf.php +++ /dev/null @@ -1,158 +0,0 @@ -init_hooks(); - } - - /** - * Initialize hooks - */ - private function init_hooks() { - register_activation_hook(__FILE__, [$this, 'activate']); - register_deactivation_hook(__FILE__, [$this, 'deactivate']); - - add_action('init', [$this, 'init']); - add_action('plugins_loaded', [$this, 'plugins_loaded']); - - // Initialize components - add_action('init', [$this, 'init_components']); - } - - /** - * Plugin activation - */ - public function activate() { - $database = new BDM_Database(); - $database->create_tables(); - - // Flush rewrite rules - flush_rewrite_rules(); - - // Store activation time - update_option('bd_bookshelf_activation_time', current_time('timestamp')); - } - - /** - * Plugin deactivation - */ - public function deactivate() { - // Clear scheduled events - wp_clear_scheduled_hook('bd_bookshelf_sync_ratings'); - - // Flush rewrite rules - flush_rewrite_rules(); - } - - /** - * Init hook - */ - public function init() { - // Load text domain - load_plugin_textdomain('bd-bookshelf', false, dirname(BD_BOOKSHELF_BASENAME) . '/languages'); - } - - /** - * Plugins loaded hook - */ - public function plugins_loaded() { - // Check for updates, etc. - } - - /** - * Initialize components - */ - public function init_components() { - // Initialize admin - if (is_admin()) { - new BDM_Admin(); - new BDM_Migration(); - } - - // Initialize public facing features - new BDM_Shortcodes(); - new BDM_GDPR(); - - // Initialize rating sync (cron) - new BDM_Rating_Sync(); - } -} - -/** - * Initialize plugin - */ -function bd_bookshelf() { - return BD_Bookshelf::instance(); -} - -// Start the plugin -bd_bookshelf(); diff --git a/bd-bookshelf-plugin/readme.txt b/bd-bookshelf-plugin/readme.txt deleted file mode 100644 index 59551dc..0000000 --- a/bd-bookshelf-plugin/readme.txt +++ /dev/null @@ -1,77 +0,0 @@ -=== BD Bookshelf === -Contributors: bdmurphy -Requires at least: 6.0 -Tested up to: 6.5 -Requires PHP: 7.4 -Stable tag: 0.1.0 -License: GPLv2 or later -License URI: https://www.gnu.org/licenses/gpl-2.0.html - -Copyright 2026 B.D. Murphy - -Book management plugin for authors with ISBN tracking, retail links, and rating synchronization. - -== Description == - -BD Bookshelf is a comprehensive book management plugin designed for authors. It provides: - -* **Book Management** - Custom database tables for efficient book storage -* **ISBN Tracking** - Support for 4 ISBN formats (eBook, Paperback, Hard Cover, Audio) -* **Retail Links** - Automatic link generation from ISBNs for Amazon, B&N, Apple Books, Google Play, and Kobo -* **Rating Synchronization** - Automated syncing of ratings from Amazon and Goodreads via web scraping -* **Shortcodes** - Display retail grids and ratings anywhere on your site -* **Series Management** - Organize books into series with ordering -* **Author Management** - Support for up to 3 authors per book -* **SEO Metadata** - Custom meta tags and Open Graph data for books -* **GDPR Compliance** - Cookie consent banner for affiliate tracking -* **Click Analytics** - Track clicks on custom redirect shortcodes -* **Data Migration** - Import existing books from WordPress posts or CSV files - -== Installation == - -1. Upload the `bd-bookshelf` folder to the `/wp-content/plugins/` directory -2. Activate the plugin through the 'Plugins' menu in WordPress -3. Navigate to the 'Books' menu to start adding books -4. Configure affiliate codes in Settings > Books > Settings - -== Frequently Asked Questions == - -= How do I add a new book? = - -Go to Books > Add New Book. Fill in the title, description, ISBNs, and other details. Retail links are automatically generated when you enter ISBNs. - -= How do I display retail links? = - -Use the shortcodes `[sell_ebook]`, `[sell_pb]`, `[sell_hc]`, or `[sell_audio]` with the book ID: - -`[sell_ebook id="123"]` - -= How do I display ratings? = - -Use the `[rating]` shortcode: - -`[rating title="My Book" format="both"]` - -= How often are ratings synced? = - -Ratings are automatically synced every 12 hours. You can also trigger a manual sync from the Rating Sync page. - -== Changelog == - -= 0.1.0 = -* Initial release -* Book management with custom database tables -* ISBN tracking for 4 formats -* Automatic retail link generation -* Rating synchronization from Amazon and Goodreads -* Shortcode suite for retail grids and ratings -* Series and author management -* SEO metadata -* GDPR compliance -* Click analytics -* Data migration tools - -== Upgrade Notice == - -= 0.1.0 = -Initial release of BD Bookshelf plugin. diff --git a/bd-bookshelf-plugin/uninstall.php b/bd-bookshelf-plugin/uninstall.php deleted file mode 100644 index dacee1f..0000000 --- a/bd-bookshelf-plugin/uninstall.php +++ /dev/null @@ -1,30 +0,0 @@ -drop_tables(); - -// Remove options -global $wpdb; -$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'bd_bookshelf_%'"); - -// Remove transients -$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_bd_bookshelf_%'"); -$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_bd_bookshelf_%'"); diff --git a/bd-bookshelfhelper/bd-bookshelfhelper.php b/bd-bookshelfhelper/bd-bookshelfhelper.php deleted file mode 100644 index 71284bf..0000000 --- a/bd-bookshelfhelper/bd-bookshelfhelper.php +++ /dev/null @@ -1,61 +0,0 @@ - Add Book -2. Fill in book details including ASIN/ISBN -3. Add localized content for each supported language -4. Set retailer purchase links -5. Add reviews and ratings - -### Pen Names - -1. Go to BD BookHelper > Pen Names -2. Add author pen names -3. Assign to books in the book editor - -### Series - -1. Go to BD BookHelper > Series (taxonomy) -2. Create series with name, slug, and description -3. Assign books to series with volume numbers - -### Settings - -Configure affiliate codes and integrations under BD BookHelper > Settings. - -## Short Links - -Books automatically get short URLs based on their title: -- "Pandemic Hacker" → /ph/ -- "Deal Hunter" → /dh/ - -Review short links follow the pattern: `{book-slug}r{site-code}`: -- /phrA = Amazon review -- /phrG = Goodreads review - -## Frequently Asked Questions - -### Do I need the theme? -Yes, this plugin is designed to work with the BDMurphy Bookshelf theme. - -### Can I use without affiliate codes? -Yes, the plugin works without affiliate codes. They are optional. - -### What languages are supported? -English, French, Spanish, German, Italian, Portuguese, Russian, Japanese, and Chinese (Simplified). - -## Changelog - -### 2.0.0 -- Initial release with full book management, series, pen names, and integrations diff --git a/bd-bookshelfhelper/uninstall.php b/bd-bookshelfhelper/uninstall.php deleted file mode 100644 index ae6e15f..0000000 --- a/bd-bookshelfhelper/uninstall.php +++ /dev/null @@ -1,39 +0,0 @@ -prefix . 'bd_books', - $wpdb->prefix . 'bd_book_meta', - $wpdb->prefix . 'bd_series', - $wpdb->prefix . 'bd_pen_names', - $wpdb->prefix . 'bd_reviews', - $wpdb->prefix . 'bd_ratings', - $wpdb->prefix . 'bd_review_shortlinks', -); - -foreach ($tables as $table) { - $wpdb->query("DROP TABLE IF EXISTS {$table}"); -} - -// Delete options -delete_option('bdm_affiliate_codes'); -delete_option('bdm_goodreads_author_id'); -delete_option('bdm_awards'); - -// Clear scheduled events -wp_clear_scheduled_hook('bd_bookshelf_goodreads_refresh'); -wp_clear_scheduled_hook('bd_bookshelf_amazon_scrape'); diff --git a/bd-simpleemail/bd-simpleemail.php b/bd-simpleemail/bd-simpleemail.php deleted file mode 100644 index 924332c..0000000 --- a/bd-simpleemail/bd-simpleemail.php +++ /dev/null @@ -1,52 +0,0 @@ - Settings - -## Configuration - -### Gmail Setup - -1. Enable 2-Factor Authentication on your Google account -2. Generate an App Password: https://support.google.com/accounts/answer/185833 -3. Enter your Gmail address and App Password in plugin settings - -### Gemini AI (Optional) - -1. Get an API key: https://makersuite.google.com/app/apikey -2. Enter the API key in plugin settings - -### Business Information - -Configure your business address in Appearance > Theme Options (required for CAN-SPAM compliance). - -## Sending Newsletters - -1. Go to BD SimpleEmail > New Newsletter -2. Enter a subject line -3. (Optional) Use AI to generate content -4. Write or edit your newsletter -5. Click "Send Newsletter" - -## Shortcodes - -Use `[bdm_signup]` to display a signup form anywhere on your site. - -## Frequently Asked Questions - -### Do I need to use Gmail? -Yes, this plugin uses Gmail SMTP for sending. App passwords are required. - -### Is AI required? -No, AI assistance is optional. You can write newsletters manually. - -### Is one-click unsubscribe included? -Yes, all emails include one-click unsubscribe links compliant with Gmail/Yahoo requirements. - -## Changelog - -### 2.0.0 -- Initial release with subscriber management, AI composition, and Gmail SMTP sending diff --git a/bd-simpleemail/uninstall.php b/bd-simpleemail/uninstall.php deleted file mode 100644 index 5d2983d..0000000 --- a/bd-simpleemail/uninstall.php +++ /dev/null @@ -1,32 +0,0 @@ -prefix . 'bd_subscribers', - $wpdb->prefix . 'bd_newsletters', -); - -foreach ($tables as $table) { - $wpdb->query("DROP TABLE IF EXISTS {$table}"); -} - -// Delete options -delete_option('bdm_gmail_address'); -delete_option('bdm_gmail_password'); -delete_option('bdm_sender_name'); -delete_option('bdm_reply_to'); -delete_option('bdm_gemini_api_key'); diff --git a/bd-bookshelf-plugin/admin/css/bd-bookshelf-admin.css b/umbra-bookshelf/admin/css/umbra-bookshelf-admin.css similarity index 99% rename from bd-bookshelf-plugin/admin/css/bd-bookshelf-admin.css rename to umbra-bookshelf/admin/css/umbra-bookshelf-admin.css index a9a9d64..ce609bf 100644 --- a/bd-bookshelf-plugin/admin/css/bd-bookshelf-admin.css +++ b/umbra-bookshelf/admin/css/umbra-bookshelf-admin.css @@ -1,7 +1,7 @@ /** * BD Bookshelf - Admin Styles * - * @package BD_Bookshelf + * @package Umbra_Bookshelf */ /* Form Layout */ diff --git a/bd-bookshelf-plugin/admin/js/bd-bookshelf-admin.js b/umbra-bookshelf/admin/js/umbra-bookshelf-admin.js similarity index 94% rename from bd-bookshelf-plugin/admin/js/bd-bookshelf-admin.js rename to umbra-bookshelf/admin/js/umbra-bookshelf-admin.js index 776d8f1..c10fad4 100644 --- a/bd-bookshelf-plugin/admin/js/bd-bookshelf-admin.js +++ b/umbra-bookshelf/admin/js/umbra-bookshelf-admin.js @@ -4,7 +4,7 @@ /** * BD Bookshelf - Admin JavaScript * - * @package BD_Bookshelf + * @package Umbra_Bookshelf */ (function($) { @@ -71,11 +71,11 @@ btn.prop('disabled', true).text('Syncing...'); $.ajax({ - url: bdBookshelfAdmin.ajaxurl, + url: umbraBookshelfAdmin.ajaxurl, type: 'POST', data: { action: 'bd_bookshelf_sync_ratings', - nonce: bdBookshelfAdmin.nonce + nonce: umbraBookshelfAdmin.nonce }, success: function(response) { if (response.success) { @@ -98,12 +98,12 @@ btn.prop('disabled', true).text('Regenerating...'); $.ajax({ - url: bdBookshelfAdmin.ajaxurl, + url: umbraBookshelfAdmin.ajaxurl, type: 'POST', data: { action: 'bd_bookshelf_regenerate_links', book_id: bookId, - nonce: bdBookshelfAdmin.nonce + nonce: umbraBookshelfAdmin.nonce }, success: function(response) { if (response.success) { @@ -170,12 +170,12 @@ $('body').css('cursor', 'wait'); $.ajax({ - url: bdBookshelfAdmin.ajaxurl, + url: umbraBookshelfAdmin.ajaxurl, type: 'POST', data: { action: action, author_id: authorId, - nonce: bdBookshelfAdmin.nonce + nonce: umbraBookshelfAdmin.nonce }, success: function(response) { $('body').css('cursor', 'default'); @@ -300,7 +300,7 @@ // Build form data manually for proper nested array handling var formData = new FormData(); formData.append('action', 'bd_bookshelf_import_scraped'); - formData.append('nonce', bdBookshelfAdmin.nonce); + formData.append('nonce', umbraBookshelfAdmin.nonce); formData.append('source', source); // Add author data @@ -326,7 +326,7 @@ }); $.ajax({ - url: bdBookshelfAdmin.ajaxurl, + url: umbraBookshelfAdmin.ajaxurl, type: 'POST', data: formData, processData: false, @@ -365,8 +365,8 @@ } coverFrame = wp.media({ - title: bdBookshelfAdmin.mediaTitle, - button: { text: bdBookshelfAdmin.mediaButton }, + title: umbraBookshelfAdmin.mediaTitle, + button: { text: umbraBookshelfAdmin.mediaButton }, multiple: false, library: { type: 'image' } }); @@ -458,12 +458,12 @@ // Fetch author data $.ajax({ - url: bdBookshelfAdmin.ajaxurl, + url: umbraBookshelfAdmin.ajaxurl, type: 'POST', data: { action: 'bd_bookshelf_get_author', id: authorId, - nonce: bdBookshelfAdmin.nonce + nonce: umbraBookshelfAdmin.nonce }, success: function(response) { if (response.success) { @@ -497,12 +497,12 @@ var authorId = $(this).data('id'); if (confirm('Are you sure you want to delete this author?')) { $.ajax({ - url: bdBookshelfAdmin.ajaxurl, + url: umbraBookshelfAdmin.ajaxurl, type: 'POST', data: { action: 'bd_bookshelf_delete_author', id: authorId, - nonce: bdBookshelfAdmin.nonce + nonce: umbraBookshelfAdmin.nonce }, success: function(response) { if (response.success) { @@ -521,7 +521,7 @@ var data = $(this).serialize(); $.ajax({ - url: bdBookshelfAdmin.ajaxurl, + url: umbraBookshelfAdmin.ajaxurl, type: 'POST', data: data + '&action=bd_bookshelf_save_author', success: function(response) { @@ -560,12 +560,12 @@ // Fetch series data $.ajax({ - url: bdBookshelfAdmin.ajaxurl, + url: umbraBookshelfAdmin.ajaxurl, type: 'POST', data: { action: 'bd_bookshelf_get_series', id: seriesId, - nonce: bdBookshelfAdmin.nonce + nonce: umbraBookshelfAdmin.nonce }, success: function(response) { if (response.success) { @@ -586,12 +586,12 @@ var seriesId = $(this).data('id'); if (confirm('Are you sure you want to delete this series? Books in this series will be unlinked.')) { $.ajax({ - url: bdBookshelfAdmin.ajaxurl, + url: umbraBookshelfAdmin.ajaxurl, type: 'POST', data: { action: 'bd_bookshelf_delete_series', id: seriesId, - nonce: bdBookshelfAdmin.nonce + nonce: umbraBookshelfAdmin.nonce }, success: function(response) { if (response.success) { @@ -610,7 +610,7 @@ var data = $(this).serialize(); $.ajax({ - url: bdBookshelfAdmin.ajaxurl, + url: umbraBookshelfAdmin.ajaxurl, type: 'POST', data: data + '&action=bd_bookshelf_save_series', success: function(response) { @@ -646,12 +646,12 @@ var shortcodeId = $(this).data('id'); $.ajax({ - url: bdBookshelfAdmin.ajaxurl, + url: umbraBookshelfAdmin.ajaxurl, type: 'POST', data: { action: 'bd_bookshelf_get_shortcode', id: shortcodeId, - nonce: bdBookshelfAdmin.nonce + nonce: umbraBookshelfAdmin.nonce }, success: function(response) { if (response.success) { @@ -674,12 +674,12 @@ var shortcodeId = $(this).data('id'); if (confirm('Are you sure you want to delete this shortcode?')) { $.ajax({ - url: bdBookshelfAdmin.ajaxurl, + url: umbraBookshelfAdmin.ajaxurl, type: 'POST', data: { action: 'bd_bookshelf_delete_shortcode', id: shortcodeId, - nonce: bdBookshelfAdmin.nonce + nonce: umbraBookshelfAdmin.nonce }, success: function(response) { if (response.success) { @@ -698,7 +698,7 @@ var data = $(this).serialize(); $.ajax({ - url: bdBookshelfAdmin.ajaxurl, + url: umbraBookshelfAdmin.ajaxurl, type: 'POST', data: data + '&action=bd_bookshelf_save_shortcode', success: function(response) { @@ -720,7 +720,7 @@ var data = form.serialize(); $.ajax({ - url: bdBookshelfAdmin.ajaxurl, + url: umbraBookshelfAdmin.ajaxurl, type: 'POST', data: data + '&action=bd_bookshelf_save_book', success: function(response) { @@ -741,12 +741,12 @@ */ function deleteBook(bookId) { $.ajax({ - url: bdBookshelfAdmin.ajaxurl, + url: umbraBookshelfAdmin.ajaxurl, type: 'POST', data: { action: 'bd_bookshelf_delete_book', id: bookId, - nonce: bdBookshelfAdmin.nonce + nonce: umbraBookshelfAdmin.nonce }, success: function(response) { if (response.success) { @@ -766,7 +766,7 @@ var data = form.serialize(); $.ajax({ - url: bdBookshelfAdmin.ajaxurl, + url: umbraBookshelfAdmin.ajaxurl, type: 'POST', data: data + '&action=bd_bookshelf_save_sites', success: function(response) { @@ -787,7 +787,7 @@ var data = form.serialize(); $.ajax({ - url: bdBookshelfAdmin.ajaxurl, + url: umbraBookshelfAdmin.ajaxurl, type: 'POST', data: data + '&action=bd_bookshelf_save_settings', success: function(response) { @@ -809,7 +809,7 @@ formData.append('action', 'bd_bookshelf_import_csv'); $.ajax({ - url: bdBookshelfAdmin.ajaxurl, + url: umbraBookshelfAdmin.ajaxurl, type: 'POST', data: formData, processData: false, @@ -833,7 +833,7 @@ var data = form.serialize(); $.ajax({ - url: bdBookshelfAdmin.ajaxurl, + url: umbraBookshelfAdmin.ajaxurl, type: 'POST', data: data + '&action=bd_bookshelf_import_posts', success: function(response) { diff --git a/umbra-bookshelf/assets/images/default-cover.png b/umbra-bookshelf/assets/images/default-cover.png new file mode 100644 index 0000000000000000000000000000000000000000..675bbf4ee79e90debcd3f638726049f42fd1d3d4 GIT binary patch literal 2888 zcmeAS@N?(olHy`uVBq!ia0y~yV2WU1U~b`H0*ZX+N+%&I1M_ zE(d>zhb>y-;-*`3>9Mo~Yjx$DTfZ3_)}%5qv-wj^ZbYKga!PC{xWt~$(697V4?0x_M literal 0 HcmV?d00001 diff --git a/umbra-bookshelf/docs/ARCHITECTURE.md b/umbra-bookshelf/docs/ARCHITECTURE.md new file mode 100644 index 0000000..f8ef8e4 --- /dev/null +++ b/umbra-bookshelf/docs/ARCHITECTURE.md @@ -0,0 +1,70 @@ +# Architecture + +## Why one plugin + +Three plugins shipped the same product surface under three admin menus and +shared two cron hooks. Users installed, updated and deactivated them +independently while the code assumed all three were present. Consolidating into +one plugin makes the dependency explicit and lets one activation hook set up a +consistent state. + +The trade-off is that three independent `add_menu_page` calls, three sets of +admin assets and three activation routines now share a single namespace. Every +one of those collided, and each collision is recorded in `KNOWN-ISSUES.md`. + +## Layers + +``` + umbra-bookshelf.php (bootstrap) + ├── constants one UMBRA_BOOKSHELF_* set + ├── requires 21 class files, three modules + ├── plugins_loaded construct modules + ├── init text domain + └── admin_menu(9) single top-level menu + │ + ┌──────────────────────────┼──────────────────────────┐ + │ │ │ + includes/books includes/library includes/email + Umbra_Database Umbra_Bookshelf_Helper Umbra_Simple_Email + Umbra_Book_Manager …_Helper_Admin …_Admin + Umbra_Authors …_Helper_Install …_Install + Umbra_Series Umbra_Email_Sender + Umbra_Retail_Sites + Umbra_Rating_Sync + Umbra_Shortcodes + Umbra_SEO assets: admin/, public/, library/, email/ + Umbra_GDPR assets/ + Umbra_Goodreads_Scraper + Umbra_Amazon_Scraper + Umbra_Admin + Umbra_Migration + Umbra_Theme_Compatibility -> class_alias BD_BookShelfHelper +``` + +## Design rules + +**One owner per name.** Every class, constant, asset handle, menu slug and AJAX +action is owned by exactly one module. The bootstrap owns the top-level menu and +the shared constants; modules own their submenus and assets. + +**Persistence is not part of the identity.** Anything written to the database or +embedded in published content keeps its historical name. The rename covers +headers, text domains, classes, constants, handles, slugs and JS globals — never +table names, option keys, meta keys, cron hooks, nonces, shortcodes, post types, +taxonomies or cookies. See §2.2 of the specification for the full list. + +**Registration order is explicit.** The top-level menu registers at +`admin_menu` priority 9 so the parent exists before modules append submenus at +the default 10. Modules construct on `plugins_loaded` so their own `init` +callbacks are registered before `init` fires. + +**Compatibility shims are explicit.** `Umbra_Theme_Compatibility` exposes the +static API the unmodified theme calls, and re-exports the legacy +`BD_BookShelfHelper` name via `class_alias()`. + +## Not resolved here + +The catalogue and library modules keep two independent data models that happen +to share table names `bd_books` and `bd_series`. Consolidation made them +unconditionally conflict; it did not make them agree. Resolving this is the next +piece of work — see `KNOWN-ISSUES.md` §1 for the options. diff --git a/umbra-bookshelf/docs/KNOWN-ISSUES.md b/umbra-bookshelf/docs/KNOWN-ISSUES.md new file mode 100644 index 0000000..296ad34 --- /dev/null +++ b/umbra-bookshelf/docs/KNOWN-ISSUES.md @@ -0,0 +1,132 @@ +# Known issues + +Carried into the Umbra Bookshelf consolidation. Items 1–4 are **pre-existing** +defects inherited from the three merged plugins; item 5 and the two repaired +below are noted for traceability. + +--- + +## 1. `bd_books` and `bd_series` have two incompatible schemas (DEFERRED — unresolved) + +The books and library modules both create tables with identical names and +different columns. + +`Umbra_Database::table()` returns `$wpdb->prefix . 'bd_' . $name`, and the +catalogue issues `CREATE TABLE {prefix}bd_books` with **no** `IF NOT EXISTS` +guard: + +``` +catalogue wp_bd_books id, title, slug, description, isbn_*, amazon_asin, + goodreads_id, rating_*, created_at, ... +library wp_bd_books id, post_id, pen_name_id, series_id, volume_number, + asin, isbn, publish_date, sort_order, created_at +``` + +Same for `wp_bd_series` (catalogue: `name`, `slug`, `description` …; +library: `name`, `slug`, `cover_attachment_id`, `header_attachment_id`, `blurb`, …). + +**Consequence.** Whichever module is activated first wins; the second either +no-ops (`IF NOT EXISTS`, library) or raises a database error (catalogue's +unguarded `CREATE TABLE`). Whichever schema exists first is what the other +module then reads and writes against, producing wrong results rather than an +obvious failure. + +**Why deferred.** Resolving it means choosing a canonical schema and migrating +live data — a data-migration project, not a rename. Both datasets may hold rows +that matter. + +**Options.** + +1. Library schema becomes canonical; catalogue tables move to `bd_bookshelf_*` + with an upgrade routine copying existing rows. Requires renaming DB keys, + which this consolidation deliberately avoids. +2. Catalogue schema becomes canonical; library relies on the `book` post type and + postmeta instead of `bd_books`. +3. Delete the catalogue duplicates and keep only the library model. + +--- + +## 2. Goodreads and Amazon scraping is implemented twice + +* Catalogue: `Umbra_Goodreads_Scraper`, `Umbra_Amazon_Scraper`. +* Library: `Umbra_Bookshelf_Helper::refresh_goodreads_data()`, + `::scrape_amazon_ratings()`. + +Both attach to the **same** cron hooks (`bd_bookshelf_goodreads_refresh`, +`bd_bookshelf_amazon_scrape`), so each scheduled run performs duplicate +requests to third-party APIs — wasted quota, faster rate limiting, and two +places to fix a parsing bug. Needs a decision on which implementation survives. + +--- + +## 3. `assets/images/default-cover.png` was missing + +`Umbra_Bookshelf_Helper` returns `assets/images/default-cover.png` as the +fallback cover, but the repository contained no images at all, so every +cover-less book rendered a broken image. + +**Repaired during consolidation:** a neutral 600×900 placeholder PNG was +generated at that path. Replace it with real artwork before release. + +--- + +## 4. `email/assets/js/admin.js` did not exist + +`Umbra_Simple_Email_Admin::enqueue_assets()` enqueued it on every email admin +page, but no such file was ever committed — a guaranteed 404. + +**Repaired during consolidation:** an empty placeholder was created with a +comment explaining the history. Add the intended behaviour, or drop the enqueue. + +--- + +## 5. `Umbra_SEO` is never instantiated + +`Umbra_SEO` registers its hooks from a constructor, but the bootstrap does not +construct it — carried over unchanged from the original BD Bookshelf bootstrap. +The class is loaded and its hooks never attach, so the module is inert. + +Left as-is because enabling it changes behaviour. Either instantiate it in +`Umbra_Bookshelf::init_modules()` or delete it. + +--- + +## 6. Licence metadata is inconsistent (NOT CHANGED) + +Plugin headers declared `License: GPLv2 or later` while the repository's +`LICENSE`/`NOTICE` files (added in `85de97b`) and every SPDX header state +**Apache-2.0**. The consolidated header now says Apache-2.0 to match the SPDX +headers and `LICENSE`, but this needs a deliberate decision — WordPress +plugin-directory submissions require a GPL-compatible licence. + +--- + +## Fixed during consolidation + +* **`Cannot declare class` fatal.** `class-bdm-theme-compatibility.php` declared + `BD_BookShelfHelper`, the same class name as the library plugin's own helper. + Latent while the two were separate plugins (it only fired when both were + active), but an unconditional fatal once merged. The shim is now + `Umbra_Theme_Compatibility`, with a `class_alias()` preserving the legacy name + for the unmodified theme. +* **Duplicate `bd_bookshelf_import_csv` AJAX handler.** Both `Umbra_Admin` and + `Umbra_Migration` registered it; the second silently replaced the first. The + import page now lives solely in `Umbra_Admin` (it is the variant wired to + `admin/js/umbra-bookshelf-admin.js`); `Umbra_Migration` keeps only the + preview/run endpoints it uniquely provides. +* **Duplicate `bd-bookshelf-settings` submenu.** Catalogue and library both + claimed the slug, leaving one Settings screen unreachable. Now + `umbra-bookshelf-settings` and `umbra-bookshelf-library-settings`. +* **Duplicate admin menu.** All three modules called `add_menu_page`. Now one + top-level menu registered by the bootstrap at priority 9. +* **Colliding asset handles.** Catalogue and library both enqueued + `bd-bookshelf-admin`; the second enqueue was silently discarded. Handles are + now unique per module. +* **Modules could miss their own `init` callbacks.** Constructing modules from + inside an `init` callback means their `init` registrations race the hook + currently being dispatched. Modules are now constructed on `plugins_loaded`, + behind a re-entrancy guard. +* **Over-broad asset hook check.** The library module's + `strpos($hook, 'bd-bookshelf')` matched every catalogue and email screen once + all slugs shared the `umbra-bookshelf-` prefix. Now matched against its own + slug list. diff --git a/bd-simpleemail/assets/css/admin.css b/umbra-bookshelf/email/assets/css/admin.css similarity index 100% rename from bd-simpleemail/assets/css/admin.css rename to umbra-bookshelf/email/assets/css/admin.css diff --git a/umbra-bookshelf/email/assets/js/admin.js b/umbra-bookshelf/email/assets/js/admin.js new file mode 100644 index 0000000..1a534f0 --- /dev/null +++ b/umbra-bookshelf/email/assets/js/admin.js @@ -0,0 +1,11 @@ +// Copyright 2026 Umbra Tools (umbratools.com) +// SPDX-License-Identifier: Apache-2.0 + +/** + * Umbra Bookshelf - Email module admin scripts. + * + * NOTE: Umbra_Simple_Email_Admin::enqueue_assets() has always enqueued this + * file, but the file was never present in the source repository, so every + * email admin page issued a 404 for it. It is created here as an empty + * placeholder so the enqueue resolves. Add email-module admin behaviour below. + */ diff --git a/bd-bookshelf-plugin/includes/class-bdm-admin.php b/umbra-bookshelf/includes/books/class-umbra-admin.php similarity index 81% rename from bd-bookshelf-plugin/includes/class-bdm-admin.php rename to umbra-bookshelf/includes/books/class-umbra-admin.php index fd1b912..7ddc220 100644 --- a/bd-bookshelf-plugin/includes/class-bdm-admin.php +++ b/umbra-bookshelf/includes/books/class-umbra-admin.php @@ -5,14 +5,14 @@ /** * Admin Interface class * - * @package BD_Bookshelf + * @package Umbra_Bookshelf */ if (!defined('ABSPATH')) { exit; } -class BDM_Admin { +class Umbra_Admin { /** * Constructor @@ -44,44 +44,37 @@ class BDM_Admin { * Add admin menu */ public function add_admin_menu() { - add_menu_page( - __('Books', 'bd-bookshelf'), - __('Books', 'bd-bookshelf'), - 'manage_options', - 'bd-bookshelf', - [$this, 'render_books_page'], - 'dashicons-book', - 30 - ); - - add_submenu_page('bd-bookshelf', __('All Books', 'bd-bookshelf'), __('All Books', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf', [$this, 'render_books_page']); - add_submenu_page('bd-bookshelf', __('Add New Book', 'bd-bookshelf'), __('Add New Book', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-add', [$this, 'render_book_form']); - add_submenu_page('bd-bookshelf', __('Authors', 'bd-bookshelf'), __('Authors', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-authors', [$this, 'render_authors_page']); - add_submenu_page('bd-bookshelf', __('Series', 'bd-bookshelf'), __('Series', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-series', [$this, 'render_series_page']); - add_submenu_page('bd-bookshelf', __('Retail Sites', 'bd-bookshelf'), __('Retail Sites', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-sites', [$this, 'render_sites_page']); - add_submenu_page('bd-bookshelf', __('Shortcodes', 'bd-bookshelf'), __('Shortcodes', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-shortcodes', [$this, 'render_shortcodes_page']); - add_submenu_page('bd-bookshelf', __('Rating Sync', 'bd-bookshelf'), __('Rating Sync', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-sync', [$this, 'render_sync_page']); - add_submenu_page('bd-bookshelf', __('Settings', 'bd-bookshelf'), __('Settings', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-settings', [$this, 'render_settings_page']); - add_submenu_page('bd-bookshelf', __('Import', 'bd-bookshelf'), __('Import', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-import', [$this, 'render_import_page']); + // The top-level "Umbra Bookshelf" menu is registered once by the plugin + // bootstrap (Umbra_Bookshelf::register_admin_menu); each module only + // contributes submenus beneath it. + add_submenu_page('umbra-bookshelf', __('All Books', 'umbra-bookshelf'), __('All Books', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-all', [$this, 'render_books_page']); + add_submenu_page('umbra-bookshelf', __('Add New Book', 'umbra-bookshelf'), __('Add New Book', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-add', [$this, 'render_book_form']); + add_submenu_page('umbra-bookshelf', __('Authors', 'umbra-bookshelf'), __('Authors', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-authors', [$this, 'render_authors_page']); + add_submenu_page('umbra-bookshelf', __('Series', 'umbra-bookshelf'), __('Series', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-series', [$this, 'render_series_page']); + add_submenu_page('umbra-bookshelf', __('Retail Sites', 'umbra-bookshelf'), __('Retail Sites', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-sites', [$this, 'render_sites_page']); + add_submenu_page('umbra-bookshelf', __('Shortcodes', 'umbra-bookshelf'), __('Shortcodes', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-shortcodes', [$this, 'render_shortcodes_page']); + add_submenu_page('umbra-bookshelf', __('Rating Sync', 'umbra-bookshelf'), __('Rating Sync', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-sync', [$this, 'render_sync_page']); + add_submenu_page('umbra-bookshelf', __('Import', 'umbra-bookshelf'), __('Import', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-import', [$this, 'render_import_page']); + add_submenu_page('umbra-bookshelf', __('Settings', 'umbra-bookshelf'), __('Settings', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-settings', [$this, 'render_settings_page']); } /** * Enqueue admin scripts */ public function enqueue_admin_scripts($hook) { - if (strpos($hook, 'bd-bookshelf') === false) { + if (strpos($hook, 'umbra-bookshelf') === false) { return; } wp_enqueue_media(); - wp_enqueue_style('bd-bookshelf-admin', BD_BOOKSHELF_URL . 'admin/css/bd-bookshelf-admin.css', [], BD_BOOKSHELF_VERSION); - wp_enqueue_script('bd-bookshelf-admin', BD_BOOKSHELF_URL . 'admin/js/bd-bookshelf-admin.js', ['jquery'], BD_BOOKSHELF_VERSION, true); + wp_enqueue_style('umbra-bookshelf-admin', UMBRA_BOOKSHELF_URL . 'admin/css/umbra-bookshelf-admin.css', [], UMBRA_BOOKSHELF_VERSION); + wp_enqueue_script('umbra-bookshelf-admin', UMBRA_BOOKSHELF_URL . 'admin/js/umbra-bookshelf-admin.js', ['jquery'], UMBRA_BOOKSHELF_VERSION, true); - wp_localize_script('bd-bookshelf-admin', 'bdBookshelfAdmin', [ + wp_localize_script('umbra-bookshelf-admin', 'umbraBookshelfAdmin', [ 'ajaxurl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('bd_bookshelf_admin_nonce'), - 'mediaTitle' => __('Select Cover Image', 'bd-bookshelf'), - 'mediaButton' => __('Use This Image', 'bd-bookshelf'), + 'mediaTitle' => __('Select Cover Image', 'umbra-bookshelf'), + 'mediaButton' => __('Use This Image', 'umbra-bookshelf'), ]); } @@ -90,29 +83,29 @@ class BDM_Admin { */ public function render_books_page() { if (!current_user_can('manage_options')) { - wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf')); + wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf')); } - $books = BDM_Book_Manager::get_books(['number' => 50]); + $books = Umbra_Book_Manager::get_books(['number' => 50]); ?>
-

- +

+
- - - - - + + + + + - + @@ -137,8 +130,8 @@ class BDM_Admin { @@ -154,45 +147,45 @@ class BDM_Admin { */ public function render_book_form() { if (!current_user_can('manage_options')) { - wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf')); + wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf')); } $book_id = isset($_GET['book_id']) ? absint($_GET['book_id']) : 0; - $book = $book_id ? BDM_Book_Manager::get_book($book_id) : null; - $series = BDM_Series::get_series(['number' => 100]); - $authors = BDM_Authors::get_authors(['number' => 100]); + $book = $book_id ? Umbra_Book_Manager::get_book($book_id) : null; + $series = Umbra_Series::get_series(['number' => 100]); + $authors = Umbra_Authors::get_authors(['number' => 100]); // Get existing retail links grouped by format and site $existing_links = []; if ($book) { - $raw_links = BDM_Book_Manager::get_book_retail_links($book_id); + $raw_links = Umbra_Book_Manager::get_book_retail_links($book_id); foreach ($raw_links as $link) { $existing_links[$link->format][$link->site_name] = $link->url; } } // Retail sites for URL inputs - $retail_sites = BDM_Retail_Sites::get_sites(true); + $retail_sites = Umbra_Retail_Sites::get_sites(true); ?>
-

+

-

+

last_sync ? date('M j, Y g:i a', strtotime($book->last_sync)) : '-'; ?> - | - + | +
- + - + - + - +
@@ -201,27 +194,27 @@ class BDM_Admin {
- - + +
- +
-

-

+

+

- + - + - + @@ -279,8 +272,8 @@ class BDM_Admin { -

-

+

+

- +

@@ -334,29 +327,29 @@ class BDM_Admin { */ public function render_authors_page() { if (!current_user_can('manage_options')) { - wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf')); + wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf')); } - $authors = BDM_Authors::get_authors(['number' => 100]); + $authors = Umbra_Authors::get_authors(['number' => 100]); ?>
-

- +

+
- - - - - + + + + + - + @@ -365,8 +358,8 @@ class BDM_Admin { @@ -379,7 +372,7 @@ class BDM_Admin {
website ? '' . esc_html($author->website) . '' : '-'; ?> goodreads_author_id); ?> - | - + | +
- + - + - + - + - + - +
- - + +

- +

@@ -430,35 +423,35 @@ class BDM_Admin { */ public function render_series_page() { if (!current_user_can('manage_options')) { - wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf')); + wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf')); } - $series = BDM_Series::get_series(['number' => 100]); + $series = Umbra_Series::get_series(['number' => 100]); ?>
-

- +

+
- - - + + + - + @@ -471,7 +464,7 @@ class BDM_Admin {
name); ?> description, 15)); ?> - | - + | +
- + - +

- +

@@ -501,14 +494,14 @@ class BDM_Admin { */ public function render_sites_page() { if (!current_user_can('manage_options')) { - wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf')); + wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf')); } - $sites = BDM_Retail_Sites::get_sites(false); + $sites = Umbra_Retail_Sites::get_sites(false); ?>
-

-

+

+

@@ -516,10 +509,10 @@ class BDM_Admin { - - - - + + + + @@ -529,7 +522,7 @@ class BDM_Admin { @@ -537,7 +530,7 @@ class BDM_Admin {
url_pattern); ?> affiliate_param); ?> - +

- +

@@ -549,33 +542,33 @@ class BDM_Admin { */ public function render_shortcodes_page() { if (!current_user_can('manage_options')) { - wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf')); + wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf')); } global $wpdb; - $table = BDM_Database::table('shortcodes'); + $table = Umbra_Database::table('shortcodes'); $shortcodes = $wpdb->get_results("SELECT * FROM {$table} ORDER BY created_at DESC"); ?>
-

- +

+
-

+

- - - - - + + + + + - + @@ -584,8 +577,8 @@ class BDM_Admin { @@ -598,7 +591,7 @@ class BDM_Admin {
redirect_type); ?> click_count); ?> - | - + | +
- + - + - + - +
-

+

- +

@@ -645,42 +638,42 @@ class BDM_Admin { */ public function render_sync_page() { if (!current_user_can('manage_options')) { - wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf')); + wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf')); } - $last_sync = BDM_Rating_Sync::get_last_sync_info(); + $last_sync = Umbra_Rating_Sync::get_last_sync_info(); ?>
-

+

-

+

- + - + - + - +
-

+

- +

@@ -692,28 +685,28 @@ class BDM_Admin { */ public function render_settings_page() { if (!current_user_can('manage_options')) { - wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf')); + wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf')); } ?>
-

+

-

+

- +
- +

- +

@@ -725,23 +718,23 @@ class BDM_Admin { */ public function render_import_page() { if (!current_user_can('manage_options')) { - wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf')); + wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf')); } ?>
-

+

-

-

+

+

- + @@ -749,36 +742,36 @@ class BDM_Admin {

- +

-

-

+

+

- + - + @@ -786,7 +779,7 @@ class BDM_Admin {

- +

@@ -825,10 +818,10 @@ class BDM_Admin { ]; if ($book_id) { - $result = BDM_Book_Manager::update_book($book_id, $data); + $result = Umbra_Book_Manager::update_book($book_id, $data); } else { $data['slug'] = sanitize_title($data['title']); - $book_id = BDM_Book_Manager::create_book($data); + $book_id = Umbra_Book_Manager::create_book($data); $result = $book_id !== false; } @@ -849,8 +842,8 @@ class BDM_Admin { private function save_retail_urls($book_id, $retail_urls) { global $wpdb; - $links_table = BDM_Database::table('retail_links'); - $sites_table = BDM_Database::table('retail_sites'); + $links_table = Umbra_Database::table('retail_links'); + $sites_table = Umbra_Database::table('retail_sites'); // Delete existing non-custom links $wpdb->delete($links_table, ['book_id' => $book_id, 'is_custom' => 0], ['%d', '%d']); @@ -910,7 +903,7 @@ class BDM_Admin { wp_send_json_error(['message' => 'Invalid book ID']); } - $result = BDM_Book_Manager::delete_book($book_id); + $result = Umbra_Book_Manager::delete_book($book_id); if ($result) { wp_send_json_success(['message' => 'Book deleted']); @@ -934,7 +927,7 @@ class BDM_Admin { wp_send_json_error(['message' => 'Invalid author ID']); } - $author = BDM_Authors::get_author($author_id); + $author = Umbra_Authors::get_author($author_id); if (!$author) { wp_send_json_error(['message' => 'Author not found']); } @@ -964,9 +957,9 @@ class BDM_Admin { ]; if ($author_id) { - $result = BDM_Authors::update_author($author_id, $data); + $result = Umbra_Authors::update_author($author_id, $data); } else { - $author_id = BDM_Authors::create_author($data); + $author_id = Umbra_Authors::create_author($data); $result = $author_id !== false; } @@ -992,7 +985,7 @@ class BDM_Admin { wp_send_json_error(['message' => 'Invalid author ID']); } - $result = BDM_Authors::delete_author($author_id); + $result = Umbra_Authors::delete_author($author_id); if ($result) { wp_send_json_success(['message' => 'Author deleted']); @@ -1016,7 +1009,7 @@ class BDM_Admin { wp_send_json_error(['message' => 'Invalid series ID']); } - $series = BDM_Series::get_series_by_id($series_id); + $series = Umbra_Series::get_series_by_id($series_id); if (!$series) { wp_send_json_error(['message' => 'Series not found']); } @@ -1042,9 +1035,9 @@ class BDM_Admin { ]; if ($series_id) { - $result = BDM_Series::update_series($series_id, $data); + $result = Umbra_Series::update_series($series_id, $data); } else { - $series_id = BDM_Series::create_series($data); + $series_id = Umbra_Series::create_series($data); $result = $series_id !== false; } @@ -1070,7 +1063,7 @@ class BDM_Admin { wp_send_json_error(['message' => 'Invalid series ID']); } - $result = BDM_Series::delete_series($series_id); + $result = Umbra_Series::delete_series($series_id); if ($result) { wp_send_json_success(['message' => 'Series deleted']); @@ -1095,7 +1088,7 @@ class BDM_Admin { } global $wpdb; - $table = BDM_Database::table('shortcodes'); + $table = Umbra_Database::table('shortcodes'); $shortcode = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", @@ -1134,9 +1127,9 @@ class BDM_Admin { } if ($shortcode_id) { - $result = BDM_Shortcodes::update_shortcode($shortcode_id, $data); + $result = Umbra_Shortcodes::update_shortcode($shortcode_id, $data); } else { - $shortcode_id = BDM_Shortcodes::create_shortcode($data['slug'], $data['target_url'], $data['redirect_type'], $data['description']); + $shortcode_id = Umbra_Shortcodes::create_shortcode($data['slug'], $data['target_url'], $data['redirect_type'], $data['description']); $result = $shortcode_id !== false; } @@ -1162,7 +1155,7 @@ class BDM_Admin { wp_send_json_error(['message' => 'Invalid shortcode ID']); } - $result = BDM_Shortcodes::delete_shortcode($shortcode_id); + $result = Umbra_Shortcodes::delete_shortcode($shortcode_id); if ($result) { wp_send_json_success(['message' => 'Shortcode deleted']); @@ -1183,7 +1176,7 @@ class BDM_Admin { if (!empty($_POST['affiliate'])) { global $wpdb; - $table = BDM_Database::table('retail_sites'); + $table = Umbra_Database::table('retail_sites'); foreach ($_POST['affiliate'] as $site_id => $code) { $wpdb->update( @@ -1209,7 +1202,7 @@ class BDM_Admin { wp_send_json_error(['message' => 'Insufficient permissions']); } - BDM_GDPR::update_consent_text($_POST['gdpr_text'] ?? ''); + Umbra_GDPR::update_consent_text($_POST['gdpr_text'] ?? ''); wp_send_json_success(['message' => 'Settings saved']); } @@ -1252,7 +1245,7 @@ class BDM_Admin { } // Check if already imported - update instead of skip - $existing = BDM_Book_Manager::get_book_by_title($data['title']); + $existing = Umbra_Book_Manager::get_book_by_title($data['title']); $book_data = [ 'title' => $data['title'], @@ -1276,14 +1269,14 @@ class BDM_Admin { } } if (!empty($update_data)) { - BDM_Book_Manager::update_book($existing->id, $update_data); + Umbra_Book_Manager::update_book($existing->id, $update_data); $updated++; } else { $skipped++; } } else { // Create new book - $book_id = BDM_Book_Manager::create_book($book_data); + $book_id = Umbra_Book_Manager::create_book($book_data); if ($book_id) { $imported++; } else { @@ -1332,14 +1325,14 @@ class BDM_Admin { foreach ($posts as $post) { // Check if already imported - $existing = BDM_Book_Manager::get_book_by_title($post->post_title); + $existing = Umbra_Book_Manager::get_book_by_title($post->post_title); if ($existing) { $skipped++; continue; } // Import book - $book_id = BDM_Book_Manager::create_book([ + $book_id = Umbra_Book_Manager::create_book([ 'title' => $post->post_title, 'description' => $post->post_content, ]); @@ -1368,7 +1361,7 @@ class BDM_Admin { wp_send_json_error(['message' => 'Insufficient permissions']); } - $result = BDM_Rating_Sync::manual_sync(); + $result = Umbra_Rating_Sync::manual_sync(); wp_send_json_success([ 'message' => sprintf('Synced %d books, failed %d', $result['synced'], $result['failed']), diff --git a/bd-bookshelf-plugin/includes/class-bdm-amazon-scraper.php b/umbra-bookshelf/includes/books/class-umbra-amazon-scraper.php similarity index 99% rename from bd-bookshelf-plugin/includes/class-bdm-amazon-scraper.php rename to umbra-bookshelf/includes/books/class-umbra-amazon-scraper.php index 1bd275d..4e91948 100644 --- a/bd-bookshelf-plugin/includes/class-bdm-amazon-scraper.php +++ b/umbra-bookshelf/includes/books/class-umbra-amazon-scraper.php @@ -7,14 +7,14 @@ * * Fetches author and book data from Amazon * - * @package BD_Bookshelf + * @package Umbra_Bookshelf */ if (!defined('ABSPATH')) { exit; } -class BDM_Amazon_Scraper { +class Umbra_Amazon_Scraper { private $user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; diff --git a/bd-bookshelf-plugin/includes/class-bdm-authors.php b/umbra-bookshelf/includes/books/class-umbra-authors.php similarity index 90% rename from bd-bookshelf-plugin/includes/class-bdm-authors.php rename to umbra-bookshelf/includes/books/class-umbra-authors.php index a51b2fa..788349c 100644 --- a/bd-bookshelf-plugin/includes/class-bdm-authors.php +++ b/umbra-bookshelf/includes/books/class-umbra-authors.php @@ -5,14 +5,14 @@ /** * Authors Management class * - * @package BD_Bookshelf + * @package Umbra_Bookshelf */ if (!defined('ABSPATH')) { exit; } -class BDM_Authors { +class Umbra_Authors { /** * Get all authors @@ -20,7 +20,7 @@ class BDM_Authors { public static function get_authors($args = []) { global $wpdb; - $table = BDM_Database::table('authors'); + $table = Umbra_Database::table('authors'); $defaults = [ 'number' => 20, @@ -59,7 +59,7 @@ class BDM_Authors { public static function get_author($author_id) { global $wpdb; - $table = BDM_Database::table('authors'); + $table = Umbra_Database::table('authors'); return $wpdb->get_row( $wpdb->prepare( @@ -75,7 +75,7 @@ class BDM_Authors { public static function create_author($data) { global $wpdb; - $table = BDM_Database::table('authors'); + $table = Umbra_Database::table('authors'); $result = $wpdb->insert($table, [ 'display_name' => sanitize_text_field($data['display_name']), @@ -95,7 +95,7 @@ class BDM_Authors { public static function update_author($author_id, $data) { global $wpdb; - $table = BDM_Database::table('authors'); + $table = Umbra_Database::table('authors'); $update_data = []; @@ -145,10 +145,10 @@ class BDM_Authors { global $wpdb; // Remove from book_authors - $wpdb->delete(BDM_Database::table('book_authors'), ['author_id' => $author_id], ['%d']); + $wpdb->delete(Umbra_Database::table('book_authors'), ['author_id' => $author_id], ['%d']); // Delete author - $table = BDM_Database::table('authors'); + $table = Umbra_Database::table('authors'); return $wpdb->delete($table, ['id' => $author_id], ['%d']) !== false; } @@ -158,7 +158,7 @@ class BDM_Authors { public static function count_authors($search = '') { global $wpdb; - $table = BDM_Database::table('authors'); + $table = Umbra_Database::table('authors'); $where = '1=1'; $params = []; diff --git a/bd-bookshelf-plugin/includes/class-bdm-book-manager.php b/umbra-bookshelf/includes/books/class-umbra-book-manager.php similarity index 91% rename from bd-bookshelf-plugin/includes/class-bdm-book-manager.php rename to umbra-bookshelf/includes/books/class-umbra-book-manager.php index b0a5cd9..76e735b 100644 --- a/bd-bookshelf-plugin/includes/class-bdm-book-manager.php +++ b/umbra-bookshelf/includes/books/class-umbra-book-manager.php @@ -5,14 +5,14 @@ /** * Book Manager class * - * @package BD_Bookshelf + * @package Umbra_Bookshelf */ if (!defined('ABSPATH')) { exit; } -class BDM_Book_Manager { +class Umbra_Book_Manager { /** * Get all books @@ -20,7 +20,7 @@ class BDM_Book_Manager { public static function get_books($args = []) { global $wpdb; - $table = BDM_Database::table('books'); + $table = Umbra_Database::table('books'); $defaults = [ 'number' => 20, @@ -63,7 +63,7 @@ class BDM_Book_Manager { public static function get_book($book_id) { global $wpdb; - $table = BDM_Database::table('books'); + $table = Umbra_Database::table('books'); $book = $wpdb->get_row( $wpdb->prepare( @@ -75,7 +75,7 @@ class BDM_Book_Manager { if ($book) { $book->authors = self::get_book_authors($book_id); $book->retail_links = self::get_book_retail_links($book_id); - $book->seo = BDM_SEO::get_book_seo($book_id); + $book->seo = Umbra_SEO::get_book_seo($book_id); } return $book; @@ -87,7 +87,7 @@ class BDM_Book_Manager { public static function get_book_by_title($title) { global $wpdb; - $table = BDM_Database::table('books'); + $table = Umbra_Database::table('books'); $book = $wpdb->get_row( $wpdb->prepare( @@ -105,7 +105,7 @@ class BDM_Book_Manager { public static function get_book_by_slug($slug) { global $wpdb; - $table = BDM_Database::table('books'); + $table = Umbra_Database::table('books'); $book = $wpdb->get_row( $wpdb->prepare( @@ -117,7 +117,7 @@ class BDM_Book_Manager { if ($book) { $book->authors = self::get_book_authors($book->id); $book->retail_links = self::get_book_retail_links($book->id); - $book->seo = BDM_SEO::get_book_seo($book->id); + $book->seo = Umbra_SEO::get_book_seo($book->id); } return $book; @@ -129,7 +129,7 @@ class BDM_Book_Manager { public static function create_book($data) { global $wpdb; - $table = BDM_Database::table('books'); + $table = Umbra_Database::table('books'); $slug = sanitize_title($data['slug'] ?? $data['title'] ?? ''); @@ -184,7 +184,7 @@ class BDM_Book_Manager { public static function update_book($book_id, $data) { global $wpdb; - $table = BDM_Database::table('books'); + $table = Umbra_Database::table('books'); $update_data = []; @@ -274,12 +274,12 @@ class BDM_Book_Manager { global $wpdb; // Delete related data - $wpdb->delete(BDM_Database::table('book_authors'), ['book_id' => $book_id], ['%d']); - $wpdb->delete(BDM_Database::table('retail_links'), ['book_id' => $book_id], ['%d']); - $wpdb->delete(BDM_Database::table('book_seo'), ['book_id' => $book_id], ['%d']); + $wpdb->delete(Umbra_Database::table('book_authors'), ['book_id' => $book_id], ['%d']); + $wpdb->delete(Umbra_Database::table('retail_links'), ['book_id' => $book_id], ['%d']); + $wpdb->delete(Umbra_Database::table('book_seo'), ['book_id' => $book_id], ['%d']); // Delete book - $table = BDM_Database::table('books'); + $table = Umbra_Database::table('books'); $result = $wpdb->delete($table, ['id' => $book_id], ['%d']); return $result !== false; @@ -291,7 +291,7 @@ class BDM_Book_Manager { public static function get_book_authors($book_id) { global $wpdb; - $table = BDM_Database::table('book_authors'); + $table = Umbra_Database::table('book_authors'); $authors = $wpdb->get_results( $wpdb->prepare( @@ -309,8 +309,8 @@ class BDM_Book_Manager { public static function update_book_authors_by_id($book_id, $author_ids) { global $wpdb; - $table = BDM_Database::table('book_authors'); - $authors_table = BDM_Database::table('authors'); + $table = Umbra_Database::table('book_authors'); + $authors_table = Umbra_Database::table('authors'); // Delete existing authors $wpdb->delete($table, ['book_id' => $book_id], ['%d']); @@ -349,7 +349,7 @@ class BDM_Book_Manager { public static function update_book_authors($book_id, $authors) { global $wpdb; - $table = BDM_Database::table('book_authors'); + $table = Umbra_Database::table('book_authors'); // Delete existing authors $wpdb->delete($table, ['book_id' => $book_id], ['%d']); @@ -373,8 +373,8 @@ class BDM_Book_Manager { public static function get_book_retail_links($book_id, $format = '') { global $wpdb; - $table = BDM_Database::table('retail_links'); - $sites_table = BDM_Database::table('retail_sites'); + $table = Umbra_Database::table('retail_links'); + $sites_table = Umbra_Database::table('retail_sites'); $where = 'rl.book_id = %d AND rs.is_active = 1'; $params = [$book_id]; @@ -404,7 +404,7 @@ class BDM_Book_Manager { public static function get_books_by_series($series_id) { global $wpdb; - $table = BDM_Database::table('books'); + $table = Umbra_Database::table('books'); $books = $wpdb->get_results( $wpdb->prepare( @@ -429,7 +429,7 @@ class BDM_Book_Manager { public static function get_series_list() { global $wpdb; - $table = BDM_Database::table('series'); + $table = Umbra_Database::table('series'); return $wpdb->get_results("SELECT * FROM {$table} ORDER BY name ASC"); } @@ -440,7 +440,7 @@ class BDM_Book_Manager { public static function count_books($search = '') { global $wpdb; - $table = BDM_Database::table('books'); + $table = Umbra_Database::table('books'); $where = '1=1'; $params = []; diff --git a/bd-bookshelf-plugin/includes/class-bdm-database.php b/umbra-bookshelf/includes/books/class-umbra-database.php similarity index 98% rename from bd-bookshelf-plugin/includes/class-bdm-database.php rename to umbra-bookshelf/includes/books/class-umbra-database.php index d2fd48b..b6e9e9a 100644 --- a/bd-bookshelf-plugin/includes/class-bdm-database.php +++ b/umbra-bookshelf/includes/books/class-umbra-database.php @@ -5,14 +5,14 @@ /** * Database management class * - * @package BD_Bookshelf + * @package Umbra_Bookshelf */ if (!defined('ABSPATH')) { exit; } -class BDM_Database { +class Umbra_Database { /** * Get table names with prefix @@ -187,7 +187,7 @@ class BDM_Database { $this->insert_default_sites(); // Store schema version - update_option('bd_bookshelf_db_version', BD_BOOKSHELF_VERSION); + update_option('bd_bookshelf_db_version', UMBRA_BOOKSHELF_VERSION); } /** diff --git a/bd-bookshelf-plugin/includes/class-bdm-gdpr.php b/umbra-bookshelf/includes/books/class-umbra-gdpr.php similarity index 90% rename from bd-bookshelf-plugin/includes/class-bdm-gdpr.php rename to umbra-bookshelf/includes/books/class-umbra-gdpr.php index bfb6531..3599163 100644 --- a/bd-bookshelf-plugin/includes/class-bdm-gdpr.php +++ b/umbra-bookshelf/includes/books/class-umbra-gdpr.php @@ -5,14 +5,14 @@ /** * GDPR Compliance class * - * @package BD_Bookshelf + * @package Umbra_Bookshelf */ if (!defined('ABSPATH')) { exit; } -class BDM_GDPR { +class Umbra_GDPR { /** * Constructor @@ -35,14 +35,14 @@ class BDM_GDPR { } wp_enqueue_script( - 'bd-bookshelf-gdpr', - BD_BOOKSHELF_URL . 'public/js/bd-bookshelf-gdpr.js', + 'umbra-bookshelf-gdpr', + UMBRA_BOOKSHELF_URL . 'public/js/umbra-bookshelf-gdpr.js', [], - BD_BOOKSHELF_VERSION, + UMBRA_BOOKSHELF_VERSION, true ); - wp_localize_script('bd-bookshelf-gdpr', 'bdBookshelfGdpr', [ + wp_localize_script('umbra-bookshelf-gdpr', 'umbraBookshelfGdpr', [ 'ajaxurl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('bd_bookshelf_gdpr_nonce'), 'cookieDays' => 30, @@ -69,7 +69,7 @@ class BDM_GDPR { echo '
'; echo '
'; echo '

' . wp_kses_post($privacy_text) . '

'; - echo ''; + echo ''; echo '
'; echo '
'; } diff --git a/bd-bookshelf-plugin/includes/class-bdm-goodreads-scraper.php b/umbra-bookshelf/includes/books/class-umbra-goodreads-scraper.php similarity index 99% rename from bd-bookshelf-plugin/includes/class-bdm-goodreads-scraper.php rename to umbra-bookshelf/includes/books/class-umbra-goodreads-scraper.php index 0e4aef4..3ef21c9 100644 --- a/bd-bookshelf-plugin/includes/class-bdm-goodreads-scraper.php +++ b/umbra-bookshelf/includes/books/class-umbra-goodreads-scraper.php @@ -7,14 +7,14 @@ * * Fetches author and book data from Goodreads * - * @package BD_Bookshelf + * @package Umbra_Bookshelf */ if (!defined('ABSPATH')) { exit; } -class BDM_Goodreads_Scraper { +class Umbra_Goodreads_Scraper { private $user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; diff --git a/bd-bookshelf-plugin/includes/class-bdm-migration.php b/umbra-bookshelf/includes/books/class-umbra-migration.php similarity index 82% rename from bd-bookshelf-plugin/includes/class-bdm-migration.php rename to umbra-bookshelf/includes/books/class-umbra-migration.php index 2a9818b..8605efd 100644 --- a/bd-bookshelf-plugin/includes/class-bdm-migration.php +++ b/umbra-bookshelf/includes/books/class-umbra-migration.php @@ -5,39 +5,31 @@ /** * Migration Manager class * - * @package BD_Bookshelf + * @package Umbra_Bookshelf */ if (!defined('ABSPATH')) { exit; } -class BDM_Migration { +class Umbra_Migration { /** * Constructor + * + * The "Import" admin page itself is registered once by Umbra_Admin + * (slug: umbra-bookshelf-import), which is the variant wired to + * admin/js/umbra-bookshelf-admin.js. This class only supplies the + * two-phase preview/run AJAX endpoints that page has no equivalent for. + * + * Note: the bd_bookshelf_import_csv action is intentionally NOT + * registered here -- Umbra_Admin already owns that action. Registering + * it twice silently overwrote one of the two handlers. */ public function __construct() { - add_action('admin_menu', [$this, 'add_admin_menu']); - // AJAX handlers add_action('wp_ajax_bd_bookshelf_preview_import', [$this, 'ajax_import_posts']); add_action('wp_ajax_bd_bookshelf_run_import', [$this, 'ajax_run_import_posts']); - add_action('wp_ajax_bd_bookshelf_import_csv', [$this, 'ajax_import_csv']); - } - - /** - * Add admin menu - */ - public function add_admin_menu() { - add_submenu_page( - 'bd-bookshelf', - __('Import Books', 'bd-bookshelf'), - __('Import', 'bd-bookshelf'), - 'manage_options', - 'bd-bookshelf-import', - [$this, 'render_import_page'] - ); } /** @@ -45,33 +37,33 @@ class BDM_Migration { */ public function render_import_page() { if (!current_user_can('manage_options')) { - wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf')); + wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf')); } ?>
-

+

-

-

+

+

- + - + @@ -79,38 +71,38 @@ class BDM_Migration {

- +

-

-

+

+

- +
-

+

- +

@@ -174,14 +166,14 @@ class BDM_Migration { foreach ($posts as $post) { // Check if already imported - $existing = BDM_Book_Manager::get_book_by_title($post->post_title); + $existing = Umbra_Book_Manager::get_book_by_title($post->post_title); if ($existing) { $skipped++; continue; } // Import book - $book_id = BDM_Book_Manager::create_book([ + $book_id = Umbra_Book_Manager::create_book([ 'title' => $post->post_title, 'description' => $post->post_content, ]); @@ -237,14 +229,14 @@ class BDM_Migration { } // Check if already imported - $existing = BDM_Book_Manager::get_book_by_title($data['title']); + $existing = Umbra_Book_Manager::get_book_by_title($data['title']); if ($existing) { $skipped++; continue; } // Create book - $book_id = BDM_Book_Manager::create_book([ + $book_id = Umbra_Book_Manager::create_book([ 'title' => $data['title'], 'description' => $data['description'] ?? '', 'isbn_ebook' => $data['isbn_ebook'] ?? '', diff --git a/bd-bookshelf-plugin/includes/class-bdm-rating-sync.php b/umbra-bookshelf/includes/books/class-umbra-rating-sync.php similarity index 95% rename from bd-bookshelf-plugin/includes/class-bdm-rating-sync.php rename to umbra-bookshelf/includes/books/class-umbra-rating-sync.php index 0483e3f..348a05c 100644 --- a/bd-bookshelf-plugin/includes/class-bdm-rating-sync.php +++ b/umbra-bookshelf/includes/books/class-umbra-rating-sync.php @@ -5,14 +5,14 @@ /** * Rating Sync Engine class * - * @package BD_Bookshelf + * @package Umbra_Bookshelf */ if (!defined('ABSPATH')) { exit; } -class BDM_Rating_Sync { +class Umbra_Rating_Sync { /** * Constructor @@ -36,7 +36,7 @@ class BDM_Rating_Sync { public function add_cron_interval($schedules) { $schedules['bd_bookshelf_twelve_hours'] = [ 'interval' => 43200, // 12 hours in seconds - 'display' => esc_html__('Every 12 Hours', 'bd-bookshelf'), + 'display' => esc_html__('Every 12 Hours', 'umbra-bookshelf'), ]; return $schedules; @@ -46,7 +46,7 @@ class BDM_Rating_Sync { * Sync all book ratings */ public function sync_all_ratings() { - $books = BDM_Book_Manager::get_books(['number' => 999]); + $books = Umbra_Book_Manager::get_books(['number' => 999]); $synced = 0; $failed = 0; @@ -81,7 +81,7 @@ class BDM_Rating_Sync { public function sync_book_rating($book_id) { global $wpdb; - $book = BDM_Book_Manager::get_book($book_id); + $book = Umbra_Book_Manager::get_book($book_id); if (!$book) { return false; } @@ -111,7 +111,7 @@ class BDM_Rating_Sync { } // Update book ratings - $table = BDM_Database::table('books'); + $table = Umbra_Database::table('books'); $wpdb->update( $table, [ diff --git a/bd-bookshelf-plugin/includes/class-bdm-retail-sites.php b/umbra-bookshelf/includes/books/class-umbra-retail-sites.php similarity index 92% rename from bd-bookshelf-plugin/includes/class-bdm-retail-sites.php rename to umbra-bookshelf/includes/books/class-umbra-retail-sites.php index 32f3f09..b56038f 100644 --- a/bd-bookshelf-plugin/includes/class-bdm-retail-sites.php +++ b/umbra-bookshelf/includes/books/class-umbra-retail-sites.php @@ -5,14 +5,14 @@ /** * Retail Sites management class * - * @package BD_Bookshelf + * @package Umbra_Bookshelf */ if (!defined('ABSPATH')) { exit; } -class BDM_Retail_Sites { +class Umbra_Retail_Sites { /** * Get all retail sites @@ -20,7 +20,7 @@ class BDM_Retail_Sites { public static function get_sites($active_only = true) { global $wpdb; - $table = BDM_Database::table('retail_sites'); + $table = Umbra_Database::table('retail_sites'); $where = $active_only ? 'is_active = 1' : '1=1'; @@ -37,7 +37,7 @@ class BDM_Retail_Sites { public static function get_site($site_id) { global $wpdb; - $table = BDM_Database::table('retail_sites'); + $table = Umbra_Database::table('retail_sites'); return $wpdb->get_row( $wpdb->prepare( @@ -53,7 +53,7 @@ class BDM_Retail_Sites { public static function create_site($data) { global $wpdb; - $table = BDM_Database::table('retail_sites'); + $table = Umbra_Database::table('retail_sites'); $result = $wpdb->insert($table, [ 'name' => sanitize_text_field($data['name']), @@ -74,7 +74,7 @@ class BDM_Retail_Sites { public static function update_site($site_id, $data) { global $wpdb; - $table = BDM_Database::table('retail_sites'); + $table = Umbra_Database::table('retail_sites'); $update_data = []; @@ -126,8 +126,8 @@ class BDM_Retail_Sites { // Only allow deleting custom sites $site = self::get_site($site_id); if ($site && $site->is_custom) { - $wpdb->delete(BDM_Database::table('retail_links'), ['site_id' => $site_id], ['%d']); - return $wpdb->delete(BDM_Database::table('retail_sites'), ['id' => $site_id], ['%d']) !== false; + $wpdb->delete(Umbra_Database::table('retail_links'), ['site_id' => $site_id], ['%d']); + return $wpdb->delete(Umbra_Database::table('retail_sites'), ['id' => $site_id], ['%d']) !== false; } return false; @@ -139,13 +139,13 @@ class BDM_Retail_Sites { public static function generate_links_for_book($book_id) { global $wpdb; - $book = BDM_Book_Manager::get_book($book_id); + $book = Umbra_Book_Manager::get_book($book_id); if (!$book) { return false; } $sites = self::get_sites(true); - $links_table = BDM_Database::table('retail_links'); + $links_table = Umbra_Database::table('retail_links'); // Delete existing links $wpdb->delete($links_table, ['book_id' => $book_id], ['%d']); @@ -266,7 +266,7 @@ class BDM_Retail_Sites { public static function update_link($link_id, $url) { global $wpdb; - $table = BDM_Database::table('retail_links'); + $table = Umbra_Database::table('retail_links'); return $wpdb->update( $table, @@ -283,7 +283,7 @@ class BDM_Retail_Sites { public static function create_custom_link($book_id, $site_id, $format, $url) { global $wpdb; - $table = BDM_Database::table('retail_links'); + $table = Umbra_Database::table('retail_links'); $result = $wpdb->insert($table, [ 'book_id' => $book_id, diff --git a/bd-bookshelf-plugin/includes/class-bdm-seo.php b/umbra-bookshelf/includes/books/class-umbra-seo.php similarity index 95% rename from bd-bookshelf-plugin/includes/class-bdm-seo.php rename to umbra-bookshelf/includes/books/class-umbra-seo.php index aaf1592..2612bc1 100644 --- a/bd-bookshelf-plugin/includes/class-bdm-seo.php +++ b/umbra-bookshelf/includes/books/class-umbra-seo.php @@ -5,14 +5,14 @@ /** * SEO Metadata Manager class * - * @package BD_Bookshelf + * @package Umbra_Bookshelf */ if (!defined('ABSPATH')) { exit; } -class BDM_SEO { +class Umbra_SEO { /** * Constructor @@ -32,7 +32,7 @@ class BDM_SEO { public static function get_book_seo($book_id) { global $wpdb; - $table = BDM_Database::table('book_seo'); + $table = Umbra_Database::table('book_seo'); $seo = $wpdb->get_row( $wpdb->prepare( @@ -50,7 +50,7 @@ class BDM_SEO { public static function update_book_seo($book_id, $data) { global $wpdb; - $table = BDM_Database::table('book_seo'); + $table = Umbra_Database::table('book_seo'); $existing = self::get_book_seo($book_id); @@ -85,7 +85,7 @@ class BDM_SEO { public static function delete_book_seo($book_id) { global $wpdb; - $table = BDM_Database::table('book_seo'); + $table = Umbra_Database::table('book_seo'); return $wpdb->delete($table, ['book_id' => $book_id], ['%d']) !== false; } @@ -138,7 +138,7 @@ class BDM_SEO { } $seo = self::get_book_seo($book_id); - $book = BDM_Book_Manager::get_book($book_id); + $book = Umbra_Book_Manager::get_book($book_id); if (!$book) { return; @@ -206,7 +206,7 @@ class BDM_SEO { return $entry; } - $book = BDM_Book_Manager::get_book($book_id); + $book = Umbra_Book_Manager::get_book($book_id); if (!$book) { return $entry; diff --git a/bd-bookshelf-plugin/includes/class-bdm-series.php b/umbra-bookshelf/includes/books/class-umbra-series.php similarity index 88% rename from bd-bookshelf-plugin/includes/class-bdm-series.php rename to umbra-bookshelf/includes/books/class-umbra-series.php index 32b591d..ccc5192 100644 --- a/bd-bookshelf-plugin/includes/class-bdm-series.php +++ b/umbra-bookshelf/includes/books/class-umbra-series.php @@ -5,14 +5,14 @@ /** * Series Management class * - * @package BD_Bookshelf + * @package Umbra_Bookshelf */ if (!defined('ABSPATH')) { exit; } -class BDM_Series { +class Umbra_Series { /** * Get all series @@ -20,7 +20,7 @@ class BDM_Series { public static function get_series($args = []) { global $wpdb; - $table = BDM_Database::table('series'); + $table = Umbra_Database::table('series'); $defaults = [ 'number' => 20, @@ -48,7 +48,7 @@ class BDM_Series { public static function get_series_by_id($series_id) { global $wpdb; - $table = BDM_Database::table('series'); + $table = Umbra_Database::table('series'); return $wpdb->get_row( $wpdb->prepare( @@ -64,7 +64,7 @@ class BDM_Series { public static function create_series($data) { global $wpdb; - $table = BDM_Database::table('series'); + $table = Umbra_Database::table('series'); $result = $wpdb->insert($table, [ 'name' => sanitize_text_field($data['name']), @@ -81,7 +81,7 @@ class BDM_Series { public static function update_series($series_id, $data) { global $wpdb; - $table = BDM_Database::table('series'); + $table = Umbra_Database::table('series'); $update_data = []; @@ -119,7 +119,7 @@ class BDM_Series { global $wpdb; // Unlink books from series - $books_table = BDM_Database::table('books'); + $books_table = Umbra_Database::table('books'); $wpdb->update( $books_table, ['series_id' => 0], @@ -129,7 +129,7 @@ class BDM_Series { ); // Delete series - $table = BDM_Database::table('series'); + $table = Umbra_Database::table('series'); return $wpdb->delete($table, ['id' => $series_id], ['%d']) !== false; } @@ -139,7 +139,7 @@ class BDM_Series { public static function count_series() { global $wpdb; - $table = BDM_Database::table('series'); + $table = Umbra_Database::table('series'); return $wpdb->get_var("SELECT COUNT(*) FROM {$table}"); } diff --git a/bd-bookshelf-plugin/includes/class-bdm-shortcodes.php b/umbra-bookshelf/includes/books/class-umbra-shortcodes.php similarity index 91% rename from bd-bookshelf-plugin/includes/class-bdm-shortcodes.php rename to umbra-bookshelf/includes/books/class-umbra-shortcodes.php index 31a9993..430d374 100644 --- a/bd-bookshelf-plugin/includes/class-bdm-shortcodes.php +++ b/umbra-bookshelf/includes/books/class-umbra-shortcodes.php @@ -5,14 +5,14 @@ /** * Shortcodes Manager class * - * @package BD_Bookshelf + * @package Umbra_Bookshelf */ if (!defined('ABSPATH')) { exit; } -class BDM_Shortcodes { +class Umbra_Shortcodes { /** * Constructor @@ -75,7 +75,7 @@ class BDM_Shortcodes { return ''; } - $links = BDM_Book_Manager::get_book_retail_links($book_id, $format); + $links = Umbra_Book_Manager::get_book_retail_links($book_id, $format); if (empty($links)) { return ''; } @@ -136,12 +136,12 @@ class BDM_Shortcodes { return ''; } - $book = BDM_Book_Manager::get_book_by_title(sanitize_text_field($atts['title'])); + $book = Umbra_Book_Manager::get_book_by_title(sanitize_text_field($atts['title'])); if (!$book) { return ''; } - $rating = BDM_Book_Manager::get_rating($book->id); + $rating = Umbra_Book_Manager::get_rating($book->id); $format = in_array($atts['format'], ['stars', 'numeric', 'both']) ? $atts['format'] : 'both'; $output = ''; @@ -201,7 +201,7 @@ class BDM_Shortcodes { return; } - $table = BDM_Database::table('shortcodes'); + $table = Umbra_Database::table('shortcodes'); $shortcode = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE slug = %s AND is_active = 1", @@ -228,7 +228,7 @@ class BDM_Shortcodes { private function track_click($shortcode_id) { global $wpdb; - $table = BDM_Database::table('shortcode_clicks'); + $table = Umbra_Database::table('shortcode_clicks'); $wpdb->insert($table, [ 'shortcode_id' => $shortcode_id, 'ip_address' => $this->get_client_ip(), @@ -239,7 +239,7 @@ class BDM_Shortcodes { // Increment click count $wpdb->query( $wpdb->prepare( - "UPDATE " . BDM_Database::table('shortcodes') . " SET click_count = click_count + 1 WHERE id = %d", + "UPDATE " . Umbra_Database::table('shortcodes') . " SET click_count = click_count + 1 WHERE id = %d", $shortcode_id ) ); @@ -268,7 +268,7 @@ class BDM_Shortcodes { public static function create_shortcode($slug, $target_url, $redirect_type = '301', $description = '') { global $wpdb; - $table = BDM_Database::table('shortcodes'); + $table = Umbra_Database::table('shortcodes'); $result = $wpdb->insert($table, [ 'slug' => sanitize_title($slug), @@ -287,7 +287,7 @@ class BDM_Shortcodes { public static function update_shortcode($shortcode_id, $data) { global $wpdb; - $table = BDM_Database::table('shortcodes'); + $table = Umbra_Database::table('shortcodes'); $update_data = []; @@ -332,7 +332,7 @@ class BDM_Shortcodes { public static function delete_shortcode($shortcode_id) { global $wpdb; - $wpdb->delete(BDM_Database::table('shortcode_clicks'), ['shortcode_id' => $shortcode_id], ['%d']); - return $wpdb->delete(BDM_Database::table('shortcodes'), ['id' => $shortcode_id], ['%d']) !== false; + $wpdb->delete(Umbra_Database::table('shortcode_clicks'), ['shortcode_id' => $shortcode_id], ['%d']); + return $wpdb->delete(Umbra_Database::table('shortcodes'), ['id' => $shortcode_id], ['%d']) !== false; } } diff --git a/bd-bookshelf-plugin/includes/class-bdm-theme-compatibility.php b/umbra-bookshelf/includes/books/class-umbra-theme-compatibility.php similarity index 76% rename from bd-bookshelf-plugin/includes/class-bdm-theme-compatibility.php rename to umbra-bookshelf/includes/books/class-umbra-theme-compatibility.php index 7eff1c2..8b83cdf 100644 --- a/bd-bookshelf-plugin/includes/class-bdm-theme-compatibility.php +++ b/umbra-bookshelf/includes/books/class-umbra-theme-compatibility.php @@ -5,10 +5,19 @@ /** * Theme Compatibility Layer * - * Provides compatibility with BD BookHelper plugin interface - * so the BD Bookshelf theme works with our new plugin. + * Provides the static helper interface the BD Bookshelf theme calls (book + * covers, pen names, ratings, retailer links, series info, ...), backed by the + * catalogue module. * - * @package BD_Bookshelf + * This class used to be declared as BD_BookShelfHelper, which collided with + * the library module's own BD_BookShelfHelper class. That collision was latent + * while the two were separate plugins -- it only fired when both were active -- + * but became an unconditional "Cannot declare class" fatal error once they + * were merged into a single plugin. It now has its own name, and the + * class_alias() at the bottom keeps the legacy name working for the + * unmodified theme. + * + * @package Umbra_Bookshelf */ if (!defined('ABSPATH')) { @@ -16,10 +25,9 @@ if (!defined('ABSPATH')) { } /** - * BD_BookShelfHelper class for theme compatibility - * Maps theme expectations to our new plugin methods + * Static helper API consumed by the theme. */ -class BD_BookShelfHelper { +class Umbra_Theme_Compatibility { /** * Get book cover URL @@ -77,7 +85,7 @@ class BD_BookShelfHelper { return []; } - $links = BDM_Book_Manager::get_book_retail_links($book->id, $format); + $links = Umbra_Book_Manager::get_book_retail_links($book->id, $format); $retailers = []; foreach ($links as $link) { @@ -102,7 +110,7 @@ class BD_BookShelfHelper { * Get series book count */ public static function get_series_book_count($series_id) { - $books = BDM_Book_Manager::get_books_by_series($series_id); + $books = Umbra_Book_Manager::get_books_by_series($series_id); return count($books); } @@ -110,7 +118,7 @@ class BD_BookShelfHelper { * Get series info */ public static function get_series_info($series_id) { - $series = BDM_Series::get_series_by_id($series_id); + $series = Umbra_Series::get_series_by_id($series_id); if ($series) { return [ 'name' => $series->name, @@ -189,7 +197,7 @@ class BD_BookShelfHelper { global $wpdb; // Check if post_id is actually a book ID from our database - $book = BDM_Book_Manager::get_book($post_id); + $book = Umbra_Book_Manager::get_book($post_id); if ($book) { return $book; } @@ -198,13 +206,13 @@ class BD_BookShelfHelper { $book_id = get_post_meta($post_id, '_bd_bookshelf_book_id', true); if ($book_id) { - return BDM_Book_Manager::get_book($book_id); + return Umbra_Book_Manager::get_book($book_id); } // Try to find by title $post = get_post($post_id); if ($post) { - $book = BDM_Book_Manager::get_book_by_title($post->post_title); + $book = Umbra_Book_Manager::get_book_by_title($post->post_title); if ($book) { // Link post to book update_post_meta($post_id, '_bd_bookshelf_book_id', $book->id); @@ -215,3 +223,14 @@ class BD_BookShelfHelper { return null; } } + +/** + * Legacy alias. + * + * The BD Bookshelf theme (unchanged, and out of scope for this consolidation) + * still calls BD_BookShelfHelper:: statically. Keep that working. Guarded + * because a theme or add-on may legitimately have declared the name already. + */ +if (!class_exists('BD_BookShelfHelper', false)) { + class_alias('Umbra_Theme_Compatibility', 'BD_BookShelfHelper'); +} diff --git a/bd-simpleemail/inc/class-email-admin.php b/umbra-bookshelf/includes/email/class-umbra-email-admin.php similarity index 92% rename from bd-simpleemail/inc/class-email-admin.php rename to umbra-bookshelf/includes/email/class-umbra-email-admin.php index d9e5723..f08c752 100644 --- a/bd-simpleemail/inc/class-email-admin.php +++ b/umbra-bookshelf/includes/email/class-umbra-email-admin.php @@ -5,20 +5,20 @@ /** * Email Admin Class * - * @package BD_SimpleEmail + * @package Umbra_Simple_Email */ if (!defined('ABSPATH')) { exit; } -class BD_SimpleEmail_Admin { +class Umbra_Simple_Email_Admin { private static $instance = null; /** * Get instance * - * @return BD_SimpleEmail_Admin + * @return Umbra_Simple_Email_Admin */ public static function instance() { if (null === self::$instance) { @@ -40,19 +40,23 @@ class BD_SimpleEmail_Admin { * @param string $hook Current admin page. */ public function enqueue_assets($hook) { - if (strpos($hook, 'bd-simpleemail') !== false || strpos($hook, 'bd-email') !== false) { + // Matches the submenu slugs registered by Umbra_Simple_Email::add_menu_pages(). + if (strpos($hook, 'umbra-bookshelf-subscribers') !== false + || strpos($hook, 'umbra-bookshelf-newsletter') !== false + || strpos($hook, 'umbra-bookshelf-email-settings') !== false) { + wp_enqueue_style( - 'bd-email-admin', - BD_EMAIL_URI . 'assets/css/admin.css', + 'umbra-bookshelf-email-admin', + UMBRA_BOOKSHELF_URL . 'email/assets/css/admin.css', array(), - BD_EMAIL_VERSION + UMBRA_BOOKSHELF_EMAIL_VERSION ); wp_enqueue_script( - 'bd-email-admin', - BD_EMAIL_URI . 'assets/js/admin.js', + 'umbra-bookshelf-email-admin', + UMBRA_BOOKSHELF_URL . 'email/assets/js/admin.js', array('jquery'), - BD_EMAIL_VERSION, + UMBRA_BOOKSHELF_EMAIL_VERSION, true ); } @@ -143,7 +147,7 @@ class BD_SimpleEmail_Admin { } if ($content) { - $result = BD_Email_Sender::send_newsletter($subject, $content); + $result = Umbra_Email_Sender::send_newsletter($subject, $content); if ($result['success']) { self::log_newsletter($subject, $content, $result['count']); @@ -163,7 +167,7 @@ class BD_SimpleEmail_Admin { } } - $settings = BD_SimpleEmail::get_settings(); + $settings = Umbra_Simple_Email::get_settings(); $has_ai = !empty($settings['gemini_api_key']); echo '
'; @@ -231,7 +235,7 @@ class BD_SimpleEmail_Admin { update_option('bdm_reply_to', sanitize_email($_POST['reply_to'] ?? '')); if (!empty($_POST['gmail_password'])) { - update_option('bdm_gmail_password', BD_Email_Sender::encrypt_password($_POST['gmail_password'])); + update_option('bdm_gmail_password', Umbra_Email_Sender::encrypt_password($_POST['gmail_password'])); } if (!empty($_POST['gemini_api_key'])) { @@ -241,8 +245,8 @@ class BD_SimpleEmail_Admin { echo '

' . esc_html__('Settings saved.', 'bd-simpleemail') . '

'; } - $settings = BD_SimpleEmail::get_settings(); - $business = BD_SimpleEmail::get_business_info(); + $settings = Umbra_Simple_Email::get_settings(); + $business = Umbra_Simple_Email::get_business_info(); echo '
'; echo '

' . esc_html__('BD SimpleEmail Settings', 'bd-simpleemail') . '

'; @@ -305,7 +309,7 @@ class BD_SimpleEmail_Admin { * @return string */ private static function generate_newsletter($subject, $prompt) { - $settings = BD_SimpleEmail::get_settings(); + $settings = Umbra_Simple_Email::get_settings(); if (empty($settings['gemini_api_key'])) { return ''; diff --git a/bd-simpleemail/inc/class-email-install.php b/umbra-bookshelf/includes/email/class-umbra-email-install.php similarity index 96% rename from bd-simpleemail/inc/class-email-install.php rename to umbra-bookshelf/includes/email/class-umbra-email-install.php index 2bce55e..f5327f0 100644 --- a/bd-simpleemail/inc/class-email-install.php +++ b/umbra-bookshelf/includes/email/class-umbra-email-install.php @@ -5,14 +5,14 @@ /** * Email Installation and Database * - * @package BD_SimpleEmail + * @package Umbra_Simple_Email */ if (!defined('ABSPATH')) { exit; } -class BD_SimpleEmail_Install { +class Umbra_Simple_Email_Install { /** * Activate plugin */ diff --git a/bd-simpleemail/inc/class-email-sender.php b/umbra-bookshelf/includes/email/class-umbra-email-sender.php similarity index 93% rename from bd-simpleemail/inc/class-email-sender.php rename to umbra-bookshelf/includes/email/class-umbra-email-sender.php index 6b70768..c61cf18 100644 --- a/bd-simpleemail/inc/class-email-sender.php +++ b/umbra-bookshelf/includes/email/class-umbra-email-sender.php @@ -5,14 +5,14 @@ /** * Email Sender Class * - * @package BD_SimpleEmail + * @package Umbra_Simple_Email */ if (!defined('ABSPATH')) { exit; } -class BD_Email_Sender { +class Umbra_Email_Sender { /** * Send newsletter to all subscribers * @@ -21,25 +21,25 @@ class BD_Email_Sender { * @return array */ public static function send_newsletter($subject, $content) { - $settings = BD_SimpleEmail::get_settings(); + $settings = Umbra_Simple_Email::get_settings(); if (empty($settings['gmail_address']) || empty($settings['gmail_password'])) { return array( 'success' => false, - 'message' => esc_html__('Gmail not configured', 'bd-simpleemail'), + 'message' => esc_html__('Gmail not configured', 'umbra-bookshelf'), ); } - $subscribers = BD_SimpleEmail::get_subscribers(); + $subscribers = Umbra_Simple_Email::get_subscribers(); if (empty($subscribers)) { return array( 'success' => false, - 'message' => esc_html__('No subscribers found', 'bd-simpleemail'), + 'message' => esc_html__('No subscribers found', 'umbra-bookshelf'), ); } - $business = BD_SimpleEmail::get_business_info(); + $business = Umbra_Simple_Email::get_business_info(); $html = self::build_email_html($subject, $content, $business); $sent = 0; @@ -75,10 +75,10 @@ class BD_Email_Sender { * @return bool */ private static function send_single_email($to, $subject, $html, $first_name = '') { - $settings = BD_SimpleEmail::get_settings(); + $settings = Umbra_Simple_Email::get_settings(); // Replace placeholder with actual unsubscribe link - $token = BD_SimpleEmail::generate_unsubscribe_token($to); + $token = Umbra_Simple_Email::generate_unsubscribe_token($to); $unsubscribe_url = add_query_arg(array( 'email' => rawurlencode($to), 'token' => $token, diff --git a/bd-simpleemail/inc/class-simpleemail.php b/umbra-bookshelf/includes/email/class-umbra-simple-email.php similarity index 86% rename from bd-simpleemail/inc/class-simpleemail.php rename to umbra-bookshelf/includes/email/class-umbra-simple-email.php index b68949d..ba93f04 100644 --- a/bd-simpleemail/inc/class-simpleemail.php +++ b/umbra-bookshelf/includes/email/class-umbra-simple-email.php @@ -5,20 +5,20 @@ /** * Main SimpleEmail Plugin Class * - * @package BD_SimpleEmail + * @package Umbra_Simple_Email */ if (!defined('ABSPATH')) { exit; } -class BD_SimpleEmail { +class Umbra_Simple_Email { private static $instance = null; /** * Get instance * - * @return BD_SimpleEmail + * @return Umbra_Simple_Email */ public static function instance() { if (null === self::$instance) { @@ -51,41 +51,34 @@ class BD_SimpleEmail { * Add admin menu pages */ public function add_menu_pages() { - add_menu_page( - esc_html__('BD SimpleEmail', 'bd-simpleemail'), - esc_html__('BD SimpleEmail', 'bd-simpleemail'), + // The top-level "Umbra Bookshelf" menu is registered once by the plugin + // bootstrap (Umbra_Bookshelf::register_admin_menu); this module only + // contributes submenus beneath it. + add_submenu_page( + 'umbra-bookshelf', + esc_html__('Subscribers', 'umbra-bookshelf'), + esc_html__('Subscribers', 'umbra-bookshelf'), 'manage_options', - 'bd-simpleemail', - '', - 'dashicons-email', - 32 + 'umbra-bookshelf-subscribers', + array('Umbra_Simple_Email_Admin', 'subscribers_page') ); add_submenu_page( - 'bd-simpleemail', - esc_html__('Subscribers', 'bd-simpleemail'), - esc_html__('Subscribers', 'bd-simpleemail'), + 'umbra-bookshelf', + esc_html__('New Newsletter', 'umbra-bookshelf'), + esc_html__('New Newsletter', 'umbra-bookshelf'), 'manage_options', - 'bd-email-subscribers', - array('BD_SimpleEmail_Admin', 'subscribers_page') + 'umbra-bookshelf-newsletter', + array('Umbra_Simple_Email_Admin', 'new_newsletter_page') ); add_submenu_page( - 'bd-simpleemail', - esc_html__('New Newsletter', 'bd-simpleemail'), - esc_html__('New Newsletter', 'bd-simpleemail'), + 'umbra-bookshelf', + esc_html__('Email Settings', 'umbra-bookshelf'), + esc_html__('Email Settings', 'umbra-bookshelf'), 'manage_options', - 'bd-email-new', - array('BD_SimpleEmail_Admin', 'new_newsletter_page') - ); - - add_submenu_page( - 'bd-simpleemail', - esc_html__('Settings', 'bd-simpleemail'), - esc_html__('Settings', 'bd-simpleemail'), - 'manage_options', - 'bd-email-settings', - array('BD_SimpleEmail_Admin', 'settings_page') + 'umbra-bookshelf-email-settings', + array('Umbra_Simple_Email_Admin', 'settings_page') ); } @@ -145,22 +138,22 @@ class BD_SimpleEmail { $email = sanitize_email($_POST['email'] ?? ''); if (!$email || !is_email($email)) { - wp_send_json_error(array('message' => esc_html__('Invalid email address', 'bd-simpleemail'))); + wp_send_json_error(array('message' => esc_html__('Invalid email address', 'umbra-bookshelf'))); } $first_name = sanitize_text_field($_POST['first_name'] ?? ''); $opt_in = isset($_POST['opt_in']) && $_POST['opt_in']; if (!$opt_in) { - wp_send_json_error(array('message' => esc_html__('Please confirm your subscription', 'bd-simpleemail'))); + wp_send_json_error(array('message' => esc_html__('Please confirm your subscription', 'umbra-bookshelf'))); } $result = self::subscribe($email, $first_name); if ($result) { - wp_send_json_success(array('message' => esc_html__('Thank you for subscribing!', 'bd-simpleemail'))); + wp_send_json_success(array('message' => esc_html__('Thank you for subscribing!', 'umbra-bookshelf'))); } else { - wp_send_json_error(array('message' => esc_html__('You are already subscribed or cannot be added', 'bd-simpleemail'))); + wp_send_json_error(array('message' => esc_html__('You are already subscribed or cannot be added', 'umbra-bookshelf'))); } } @@ -316,19 +309,19 @@ class BD_SimpleEmail { @@ -360,7 +353,7 @@ class BD_SimpleEmail { response.style.display = 'block'; }) .catch(function() { - response.innerHTML = '

'; + response.innerHTML = '

'; response.style.display = 'block'; }); }); diff --git a/bd-bookshelfhelper/inc/class-bookhelper.php b/umbra-bookshelf/includes/library/class-umbra-bookshelf-helper.php similarity index 95% rename from bd-bookshelfhelper/inc/class-bookhelper.php rename to umbra-bookshelf/includes/library/class-umbra-bookshelf-helper.php index 7bf9199..4c87bc4 100644 --- a/bd-bookshelfhelper/inc/class-bookhelper.php +++ b/umbra-bookshelf/includes/library/class-umbra-bookshelf-helper.php @@ -5,20 +5,20 @@ /** * Main Plugin Class * - * @package BD_BookShelfHelper + * @package Umbra_Bookshelf_Helper */ if (!defined('ABSPATH')) { exit; } -class BD_BookShelfHelper { +class Umbra_Bookshelf_Helper { private static $instance = null; /** * Get instance * - * @return BD_BookShelfHelper + * @return Umbra_Bookshelf_Helper */ public static function instance() { if (null === self::$instance) { @@ -48,7 +48,7 @@ class BD_BookShelfHelper { add_filter('bdm_nav_menu_items', array($this, 'filter_series_menu_item'), 10, 2); if (is_admin()) { - BD_BookShelfHelper_Admin::instance(); + Umbra_Bookshelf_Helper_Admin::instance(); } // Schedule cron jobs on init @@ -65,20 +65,20 @@ class BD_BookShelfHelper { public function register_post_types() { register_post_type('book', array( 'labels' => array( - 'name' => esc_html__('Books', 'bd-bookshelfhelper'), - 'singular_name' => esc_html__('Book', 'bd-bookshelfhelper'), - 'add_new' => esc_html__('Add New', 'bd-bookshelfhelper'), - 'add_new_item' => esc_html__('Add New Book', 'bd-bookshelfhelper'), - 'edit_item' => esc_html__('Edit Book', 'bd-bookshelfhelper'), - 'new_item' => esc_html__('New Book', 'bd-bookshelfhelper'), - 'view_item' => esc_html__('View Book', 'bd-bookshelfhelper'), - 'search_items' => esc_html__('Search Books', 'bd-bookshelfhelper'), - 'not_found' => esc_html__('No books found', 'bd-bookshelfhelper'), - 'not_found_in_trash' => esc_html__('No books found in trash', 'bd-bookshelfhelper'), + 'name' => esc_html__('Books', 'umbra-bookshelf'), + 'singular_name' => esc_html__('Book', 'umbra-bookshelf'), + 'add_new' => esc_html__('Add New', 'umbra-bookshelf'), + 'add_new_item' => esc_html__('Add New Book', 'umbra-bookshelf'), + 'edit_item' => esc_html__('Edit Book', 'umbra-bookshelf'), + 'new_item' => esc_html__('New Book', 'umbra-bookshelf'), + 'view_item' => esc_html__('View Book', 'umbra-bookshelf'), + 'search_items' => esc_html__('Search Books', 'umbra-bookshelf'), + 'not_found' => esc_html__('No books found', 'umbra-bookshelf'), + 'not_found_in_trash' => esc_html__('No books found in trash', 'umbra-bookshelf'), ), 'public' => true, 'has_archive' => true, - 'show_in_menu' => 'bd-bookshelf', + 'show_in_menu' => 'umbra-bookshelf', 'supports' => array('title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'), 'rewrite' => array('slug' => 'book'), 'menu_icon' => 'dashicons-book-alt', @@ -94,15 +94,15 @@ class BD_BookShelfHelper { public function register_taxonomies() { register_taxonomy('series', 'book', array( 'labels' => array( - 'name' => esc_html__('Series', 'bd-bookshelfhelper'), - 'singular_name' => esc_html__('Series', 'bd-bookshelfhelper'), - 'search_items' => esc_html__('Search Series', 'bd-bookshelfhelper'), - 'all_items' => esc_html__('All Series', 'bd-bookshelfhelper'), - 'edit_item' => esc_html__('Edit Series', 'bd-bookshelfhelper'), - 'update_item' => esc_html__('Update Series', 'bd-bookshelfhelper'), - 'add_new_item' => esc_html__('Add New Series', 'bd-bookshelfhelper'), - 'new_item_name' => esc_html__('New Series Name', 'bd-bookshelfhelper'), - 'not_found' => esc_html__('No series found', 'bd-bookshelfhelper'), + 'name' => esc_html__('Series', 'umbra-bookshelf'), + 'singular_name' => esc_html__('Series', 'umbra-bookshelf'), + 'search_items' => esc_html__('Search Series', 'umbra-bookshelf'), + 'all_items' => esc_html__('All Series', 'umbra-bookshelf'), + 'edit_item' => esc_html__('Edit Series', 'umbra-bookshelf'), + 'update_item' => esc_html__('Update Series', 'umbra-bookshelf'), + 'add_new_item' => esc_html__('Add New Series', 'umbra-bookshelf'), + 'new_item_name' => esc_html__('New Series Name', 'umbra-bookshelf'), + 'not_found' => esc_html__('No series found', 'umbra-bookshelf'), ), 'hierarchical' => true, 'public' => true, @@ -157,7 +157,7 @@ class BD_BookShelfHelper { $series_count = self::series_count(); if ($series_count > 0) { - $series_item = ''; + $series_item = ''; if (preg_match('/(]*>.*?Books.*?<\/li>)/s', $items, $matches)) { $books_item = $matches[1]; @@ -273,10 +273,10 @@ class BD_BookShelfHelper { * @return string */ private function render_book_display($book) { - $cover_url = BD_BookShelfHelper::get_cover_url($book->ID, 'medium'); - $pen_name = BD_BookShelfHelper::get_pen_name($book->ID); - $blurb = BD_BookShelfHelper::get_localized_blurb($book->ID); - $links = BD_BookShelfHelper::get_retailer_links($book->ID); + $cover_url = Umbra_Bookshelf_Helper::get_cover_url($book->ID, 'medium'); + $pen_name = Umbra_Bookshelf_Helper::get_pen_name($book->ID); + $blurb = Umbra_Bookshelf_Helper::get_localized_blurb($book->ID); + $links = Umbra_Bookshelf_Helper::get_retailer_links($book->ID); ob_start(); ?> @@ -508,7 +508,7 @@ class BD_BookShelfHelper { } } - return BD_BHS_URI . 'assets/images/default-cover.png'; + return UMBRA_BOOKSHELF_URL . 'assets/images/default-cover.png'; } /** diff --git a/bd-bookshelfhelper/inc/class-admin.php b/umbra-bookshelf/includes/library/class-umbra-library-admin.php similarity index 78% rename from bd-bookshelfhelper/inc/class-admin.php rename to umbra-bookshelf/includes/library/class-umbra-library-admin.php index aa5a700..fbbab64 100644 --- a/bd-bookshelfhelper/inc/class-admin.php +++ b/umbra-bookshelf/includes/library/class-umbra-library-admin.php @@ -5,20 +5,20 @@ /** * Admin Class * - * @package BD_BookShelfHelper + * @package Umbra_Bookshelf_Helper */ if (!defined('ABSPATH')) { exit; } -class BD_BookShelfHelper_Admin { +class Umbra_Bookshelf_Helper_Admin { private static $instance = null; /** * Get instance * - * @return BD_BookShelfHelper_Admin + * @return Umbra_Bookshelf_Helper_Admin */ public static function instance() { if (null === self::$instance) { @@ -43,73 +43,68 @@ class BD_BookShelfHelper_Admin { * Add admin menu pages */ public function add_menu_pages() { - add_menu_page( - esc_html__('BD BookHelper', 'bd-bookshelfhelper'), - esc_html__('BD BookHelper', 'bd-bookshelfhelper'), - 'edit_posts', - 'bd-bookshelf', - '', - 'dashicons-book-alt', - 30 - ); - + // The top-level "Umbra Bookshelf" menu is registered once by the plugin + // bootstrap (Umbra_Bookshelf::register_admin_menu); this module only + // contributes submenus beneath it. add_submenu_page( - 'bd-bookshelf', - esc_html__('Books', 'bd-bookshelfhelper'), - esc_html__('All Books', 'bd-bookshelfhelper'), + 'umbra-bookshelf', + esc_html__('Books', 'umbra-bookshelf'), + esc_html__('Book Library', 'umbra-bookshelf'), 'edit_posts', 'edit.php?post_type=book' ); add_submenu_page( - 'bd-bookshelf', - esc_html__('Add Book', 'bd-bookshelfhelper'), - esc_html__('Add Book', 'bd-bookshelfhelper'), + 'umbra-bookshelf', + esc_html__('Add Book', 'umbra-bookshelf'), + esc_html__('Add Book', 'umbra-bookshelf'), 'edit_posts', 'post-new.php?post_type=book' ); add_submenu_page( - 'bd-bookshelf', - esc_html__('Book Data Entry', 'bd-bookshelfhelper'), - esc_html__('Data Entry', 'bd-bookshelfhelper'), + 'umbra-bookshelf', + esc_html__('Book Data Entry', 'umbra-bookshelf'), + esc_html__('Data Entry', 'umbra-bookshelf'), 'edit_posts', - 'bd-bookshelf-data-entry', + 'umbra-bookshelf-data-entry', array($this, 'data_entry_page') ); add_submenu_page( - 'bd-bookshelf', - esc_html__('Series', 'bd-bookshelfhelper'), - esc_html__('Series', 'bd-bookshelfhelper'), + 'umbra-bookshelf', + esc_html__('Series', 'umbra-bookshelf'), + esc_html__('Series Terms', 'umbra-bookshelf'), 'manage_categories', 'edit-tags.php?taxonomy=series&post_type=book' ); add_submenu_page( - 'bd-bookshelf', - esc_html__('Pen Names', 'bd-bookshelfhelper'), - esc_html__('Pen Names', 'bd-bookshelfhelper'), + 'umbra-bookshelf', + esc_html__('Pen Names', 'umbra-bookshelf'), + esc_html__('Pen Names', 'umbra-bookshelf'), 'manage_options', - 'bd-bookshelf-pen-names', + 'umbra-bookshelf-pen-names', array($this, 'pen_names_page') ); + // Distinct slug from Umbra_Admin's "umbra-bookshelf-settings": these are + // two different settings screens (library/publishing vs. catalogue). add_submenu_page( - 'bd-bookshelf', - esc_html__('Settings', 'bd-bookshelfhelper'), - esc_html__('Settings', 'bd-bookshelfhelper'), + 'umbra-bookshelf', + esc_html__('Library Settings', 'umbra-bookshelf'), + esc_html__('Library Settings', 'umbra-bookshelf'), 'manage_options', - 'bd-bookshelf-settings', + 'umbra-bookshelf-library-settings', array($this, 'settings_page') ); add_submenu_page( - 'bd-bookshelf', - esc_html__('Migrate BD Books', 'bd-bookshelfhelper'), - esc_html__('Migrate', 'bd-bookshelfhelper'), + 'umbra-bookshelf', + esc_html__('Migrate Books', 'umbra-bookshelf'), + esc_html__('Migrate', 'umbra-bookshelf'), 'manage_options', - 'bd-bookshelf-migrate', + 'umbra-bookshelf-migrate', array($this, 'migrate_page') ); } @@ -126,23 +121,40 @@ class BD_BookShelfHelper_Admin { wp_enqueue_media(); } - if (strpos($hook, 'bd-bookshelf') !== false) { + // Scoped to this module's own submenu slugs. A bare strpos('umbra-bookshelf') + // would also match every books-module and email-module screen. + $library_hooks = array( + 'umbra-bookshelf-data-entry', + 'umbra-bookshelf-pen-names', + 'umbra-bookshelf-library-settings', + 'umbra-bookshelf-migrate', + ); + + $is_library_screen = false; + foreach ($library_hooks as $library_hook) { + if (strpos($hook, $library_hook) !== false) { + $is_library_screen = true; + break; + } + } + + if ($is_library_screen) { wp_enqueue_style( - 'bd-bookshelf-admin', - BD_BHS_URI . 'assets/css/admin.css', + 'umbra-bookshelf-library-admin', + UMBRA_BOOKSHELF_URL . 'library/assets/css/admin.css', array(), - BD_BHS_VERSION + UMBRA_BOOKSHELF_LIBRARY_VERSION ); wp_enqueue_script( - 'bd-bookshelf-admin', - BD_BHS_URI . 'assets/js/admin.js', + 'umbra-bookshelf-library-admin', + UMBRA_BOOKSHELF_URL . 'library/assets/js/admin.js', array('jquery'), - BD_BHS_VERSION, + UMBRA_BOOKSHELF_LIBRARY_VERSION, true ); - wp_localize_script('bd-bookshelf-admin', 'bdmAdmin', array( + wp_localize_script('umbra-bookshelf-library-admin', 'umbraBookshelfLibraryAdmin', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('bd_bookshelf_admin'), )); @@ -155,7 +167,7 @@ class BD_BookShelfHelper_Admin { public function add_meta_boxes() { add_meta_box( 'bd_book_details', - esc_html__('Book Details', 'bd-bookshelfhelper'), + esc_html__('Book Details', 'umbra-bookshelf'), array($this, 'book_details_meta_box'), 'book', 'normal', @@ -164,7 +176,7 @@ class BD_BookShelfHelper_Admin { add_meta_box( 'bd_book_seo', - esc_html__('SEO Settings', 'bd-bookshelfhelper'), + esc_html__('SEO Settings', 'umbra-bookshelf'), array($this, 'book_seo_meta_box'), 'book', 'side', @@ -178,19 +190,19 @@ class BD_BookShelfHelper_Admin { * @param WP_Post $post Post object. */ public function book_details_meta_box($post) { - $languages = BD_BookShelfHelper::get_supported_languages(); + $languages = Umbra_Bookshelf_Helper::get_supported_languages(); wp_nonce_field('bd_book_meta', 'bd_book_nonce'); echo '
'; // Core Fields - echo '

' . esc_html__('Core Fields', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('Core Fields', 'umbra-bookshelf') . '

'; // Pen Name - echo '

'; + echo '

'; echo '

'; // ASIN and ISBN - echo '

'; + echo '

'; echo '

'; - echo '

'; + echo '

'; echo '

'; // Short Link - echo '

'; + echo '

'; echo ''; - echo '

' . esc_html__('Auto-generated if empty. E.g., "ph" for "Pandemic Hacker"', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('Auto-generated if empty. E.g., "ph" for "Pandemic Hacker"', 'umbra-bookshelf') . '

'; // Series $series_terms = get_terms(array('taxonomy' => 'series', 'hide_empty' => false)); if ($series_terms) { - echo '

'; + echo '

'; echo '

'; if ($current_series_id) { - echo '

'; + echo '

'; echo '

'; } } // Language Content - echo '

' . esc_html__('Language Content', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('Language Content', 'umbra-bookshelf') . '

'; foreach ($languages as $code => $name) { echo '
'; echo '' . esc_html($name) . ' (' . strtoupper($code) . ')'; - echo '

'; + echo '

'; echo '

'; - echo '

'; + echo '

'; echo '

'; - echo '

'; + echo '

'; echo '

'; - echo '

'; + echo '

'; for ($i = 1; $i <= 5; $i++) { printf('', esc_attr($code), $i, esc_attr(get_post_meta($post->ID, 'pull_quote_' . $code . '_' . $i, true)), - esc_attr__('Quote ' . $i, 'bd-bookshelfhelper') + esc_attr__('Quote ' . $i, 'umbra-bookshelf') ); } echo '

'; @@ -268,7 +280,7 @@ class BD_BookShelfHelper_Admin { } // Retailer Links - echo '

' . esc_html__('Retailer Links', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('Retailer Links', 'umbra-bookshelf') . '

'; $retailer_fields = array( 'ebook_url_amazon' => 'Amazon (Ebook)', @@ -287,12 +299,12 @@ class BD_BookShelfHelper_Admin { } // Ratings - echo '

' . esc_html__('Ratings', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('Ratings', 'umbra-bookshelf') . '

'; - echo '

'; + echo '

'; echo '

'; - echo '

'; + echo '

'; echo '

'; echo '
'; @@ -304,10 +316,10 @@ class BD_BookShelfHelper_Admin { * @param WP_Post $post Post object. */ public function book_seo_meta_box($post) { - echo '

'; + echo '

'; echo '

'; - echo '

'; + echo '

'; echo '

'; } @@ -316,7 +328,7 @@ class BD_BookShelfHelper_Admin { */ public function data_entry_page() { $post_id = isset($_GET['post_id']) ? absint($_GET['post_id']) : 0; - $languages = BD_BookShelfHelper::get_supported_languages(); + $languages = Umbra_Bookshelf_Helper::get_supported_languages(); // Get all books for selection global $wpdb; @@ -327,9 +339,9 @@ class BD_BookShelfHelper_Admin { // If no post_id provided and no books exist, show selection if (!$post_id && empty($books)) { echo '
'; - echo '

' . esc_html__('Book Data Entry', 'bd-bookshelfhelper') . '

'; - echo '
' . esc_html__('No books found. Please create a book first.', 'bd-bookshelfhelper') . '
'; - echo '

' . esc_html__('Create New Book', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('Book Data Entry', 'umbra-bookshelf') . '

'; + echo '
' . esc_html__('No books found. Please create a book first.', 'umbra-bookshelf') . '
'; + echo '

' . esc_html__('Create New Book', 'umbra-bookshelf') . '

'; echo '
'; return; } @@ -342,18 +354,18 @@ class BD_BookShelfHelper_Admin { // If still no post_id, show selection if (!$post_id) { echo '
'; - echo '

' . esc_html__('Book Data Entry', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('Book Data Entry', 'umbra-bookshelf') . '

'; echo '
'; echo ''; - echo ''; + echo ''; echo ''; echo '
' . esc_html__('Select Book', 'bd-bookshelfhelper') . '
' . esc_html__('Select Book', 'umbra-bookshelf') . '
'; - submit_button(esc_html__('Select Book', 'bd-bookshelfhelper'), 'primary', 'bd_select_book'); + submit_button(esc_html__('Select Book', 'umbra-bookshelf'), 'primary', 'bd_select_book'); echo '
'; echo '
'; return; @@ -362,7 +374,7 @@ class BD_BookShelfHelper_Admin { $post = get_post($post_id); if (!$post || 'book' !== $post->post_type) { echo '
'; - echo '
' . esc_html__('Invalid book selected.', 'bd-bookshelfhelper') . '
'; + echo '
' . esc_html__('Invalid book selected.', 'umbra-bookshelf') . '
'; echo '
'; return; } @@ -370,7 +382,7 @@ class BD_BookShelfHelper_Admin { wp_nonce_field('bd_data_entry', 'bd_data_entry_nonce'); echo '
'; - echo '

' . sprintf(esc_html__('Data Entry: %s', 'bd-bookshelfhelper'), esc_html($post->post_title)) . '

'; + echo '

' . sprintf(esc_html__('Data Entry: %s', 'umbra-bookshelf'), esc_html($post->post_title)) . '

'; // Language tabs echo '
'; @@ -389,20 +401,20 @@ class BD_BookShelfHelper_Admin { wp_nonce_field('bd_data_entry_save', 'bd_data_entry_save_nonce'); // Section 1: Basic Information - $this->render_section_header('basic', __('Basic Information', 'bd-bookshelfhelper'), true); + $this->render_section_header('basic', __('Basic Information', 'umbra-bookshelf'), true); echo '
'; echo '
'; echo '
'; - echo ''; + echo ''; echo ''; - echo '' . esc_html__('The full title of the book as it appears on the cover.', 'bd-bookshelfhelper') . ''; + echo '' . esc_html__('The full title of the book as it appears on the cover.', 'umbra-bookshelf') . ''; echo '
'; echo '
'; - echo ''; + echo ''; echo ''; - echo '' . esc_html__('The author name to display with this book.', 'bd-bookshelfhelper') . ''; + echo '' . esc_html__('The author name to display with this book.', 'umbra-bookshelf') . ''; echo '
'; echo '
'; echo '
'; echo '
'; - echo ''; + echo ''; echo ''; - echo '' . esc_html__('Format: YYYY-MM-DD (e.g., 2024-01-15)', 'bd-bookshelfhelper') . ''; + echo '' . esc_html__('Format: YYYY-MM-DD (e.g., 2024-01-15)', 'umbra-bookshelf') . ''; echo '
'; echo '
'; - echo ''; + echo ''; echo ''; - echo '' . esc_html__('Manual sort position (lower numbers appear first).', 'bd-bookshelfhelper') . ''; + echo '' . esc_html__('Manual sort position (lower numbers appear first).', 'umbra-bookshelf') . ''; echo '
'; echo '
'; echo '
'; // End Basic Information // Section 2: Identifiers - $this->render_section_header('identifiers', __('Identifiers', 'bd-bookshelfhelper'), true); + $this->render_section_header('identifiers', __('Identifiers', 'umbra-bookshelf'), true); echo '
'; echo '
'; echo '
'; - echo ''; + echo ''; echo ''; - echo '' . esc_html__('13 digits, no dashes (e.g., 9781234567890)', 'bd-bookshelfhelper') . ''; + echo '' . esc_html__('13 digits, no dashes (e.g., 9781234567890)', 'umbra-bookshelf') . ''; echo '
'; echo '
'; - echo ''; + echo ''; echo ''; - echo '' . esc_html__('10 alphanumeric characters (e.g., B001234567)', 'bd-bookshelfhelper') . ''; + echo '' . esc_html__('10 alphanumeric characters (e.g., B001234567)', 'umbra-bookshelf') . ''; echo '
'; echo '
'; echo '
'; // End Identifiers // Section 3: Series & Order - $this->render_section_header('series', __('Series & Order', 'bd-bookshelfhelper'), true); + $this->render_section_header('series', __('Series & Order', 'umbra-bookshelf'), true); echo '
'; $series_terms = get_terms(array('taxonomy' => 'series', 'hide_empty' => false)); echo '
'; - echo ''; + echo ''; echo ''; - echo '' . esc_html__('Select the series this book belongs to, or leave blank for standalone.', 'bd-bookshelfhelper') . ''; + echo '' . esc_html__('Select the series this book belongs to, or leave blank for standalone.', 'umbra-bookshelf') . ''; echo '
'; echo '
'; - echo ''; + echo ''; echo ''; - echo '' . esc_html__('Order within the series (e.g., 1 for Book 1).', 'bd-bookshelfhelper') . ''; + echo '' . esc_html__('Order within the series (e.g., 1 for Book 1).', 'umbra-bookshelf') . ''; echo '
'; echo '
'; // End Series // Section 4: Short Link - $this->render_section_header('shortlink', __('Short Link', 'bd-bookshelfhelper'), false); + $this->render_section_header('shortlink', __('Short Link', 'umbra-bookshelf'), false); echo ''; // End Short Link // Section 5: Language Content (tabs for each language) - $this->render_section_header('content', __('Content (by Language)', 'bd-bookshelfhelper'), true); + $this->render_section_header('content', __('Content (by Language)', 'umbra-bookshelf'), true); echo '
'; foreach ($languages as $code => $name) { @@ -509,48 +521,48 @@ class BD_BookShelfHelper_Admin { echo '

' . esc_html($name) . ' (' . strtoupper($code) . ')

'; echo '
'; - echo ''; + echo ''; echo ''; - echo '' . esc_html__('Title in this language (if different from default).', 'bd-bookshelfhelper') . ''; + echo '' . esc_html__('Title in this language (if different from default).', 'umbra-bookshelf') . ''; echo '
'; echo '
'; - echo ''; + echo ''; echo ''; - echo '' . esc_html__('The book description shown on the back cover and product pages.', 'bd-bookshelfhelper') . ''; + echo '' . esc_html__('The book description shown on the back cover and product pages.', 'umbra-bookshelf') . ''; echo '
'; echo '
'; - echo ''; + echo ''; echo ''; - echo '' . esc_html__('A short excerpt (50-100 words) for previews and "Look Inside" feature.', 'bd-bookshelfhelper') . ''; + echo '' . esc_html__('A short excerpt (50-100 words) for previews and "Look Inside" feature.', 'umbra-bookshelf') . ''; echo '
'; echo '
'; - echo ''; + echo ''; echo '
'; for ($i = 1; $i <= 5; $i++) { printf('', esc_attr($code), $i, esc_attr(get_post_meta($post_id, 'pull_quote_' . $code . '_' . $i, true)), - esc_attr(sprintf(__('Quote %d', 'bd-bookshelfhelper'), $i)) + esc_attr(sprintf(__('Quote %d', 'umbra-bookshelf'), $i)) ); } echo '
'; - echo '' . esc_html__('Notable quotes or reviews to display on the book page.', 'bd-bookshelfhelper') . ''; + echo '' . esc_html__('Notable quotes or reviews to display on the book page.', 'umbra-bookshelf') . ''; echo '
'; echo '
'; echo '
'; - echo ''; + echo ''; echo ''; - echo '' . esc_html__('Comma-separated (e.g., Thriller, Sci-Fi, Mystery) - minimum 3 recommended', 'bd-bookshelfhelper') . ''; + echo '' . esc_html__('Comma-separated (e.g., Thriller, Sci-Fi, Mystery) - minimum 3 recommended', 'umbra-bookshelf') . ''; echo '
'; echo '
'; - echo ''; + echo ''; echo ''; - echo '' . esc_html__('Keywords for Amazon search optimization, comma-separated.', 'bd-bookshelfhelper') . ''; + echo '' . esc_html__('Keywords for Amazon search optimization, comma-separated.', 'umbra-bookshelf') . ''; echo '
'; echo '
'; @@ -560,7 +572,7 @@ class BD_BookShelfHelper_Admin { echo '
'; // End Content // Section 6: Retailer Links - $this->render_section_header('retailers', __('Retailer Links', 'bd-bookshelfhelper'), true); + $this->render_section_header('retailers', __('Retailer Links', 'umbra-bookshelf'), true); echo '
'; echo '
'; @@ -578,9 +590,9 @@ class BD_BookShelfHelper_Admin { foreach ($retailers as $field => $info) { echo '
'; - echo ''; + echo ''; echo ''; - echo '' . esc_html($info['hint'], 'bd-bookshelfhelper') . ''; + echo '' . esc_html($info['hint'], 'umbra-bookshelf') . ''; echo '
'; } @@ -588,13 +600,13 @@ class BD_BookShelfHelper_Admin { echo '
'; // End Retailer Links // Section 7: Reviews - $this->render_section_header('reviews', __('Reviews', 'bd-bookshelfhelper'), false); + $this->render_section_header('reviews', __('Reviews', 'umbra-bookshelf'), false); echo '
'; $reviews_table = $wpdb->prefix . 'bd_reviews'; $reviews = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$reviews_table} WHERE book_id = %d ORDER BY display_order", $post_id)); - echo '

' . esc_html__('Reviews are managed via the Reviews tab on the book editing screen.', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('Reviews are managed via the Reviews tab on the book editing screen.', 'umbra-bookshelf') . '

'; if ($reviews) { echo ''; @@ -613,48 +625,48 @@ class BD_BookShelfHelper_Admin { echo ''; // End Reviews // Section 8: Ratings - $this->render_section_header('ratings', __('Ratings', 'bd-bookshelfhelper'), false); + $this->render_section_header('ratings', __('Ratings', 'umbra-bookshelf'), false); echo '
'; echo '
'; echo '
'; - echo ''; + echo ''; echo ''; - echo '' . esc_html__('Average rating out of 5 (e.g., 4.5)', 'bd-bookshelfhelper') . ''; + echo '' . esc_html__('Average rating out of 5 (e.g., 4.5)', 'umbra-bookshelf') . ''; echo '
'; echo '
'; - echo ''; + echo ''; echo ''; - echo '' . esc_html__('Average rating out of 5 from Goodreads.', 'bd-bookshelfhelper') . ''; + echo '' . esc_html__('Average rating out of 5 from Goodreads.', 'umbra-bookshelf') . ''; echo '
'; echo '
'; echo '
'; // End Ratings // Section 9: SEO - $this->render_section_header('seo', __('SEO Settings', 'bd-bookshelfhelper'), false); + $this->render_section_header('seo', __('SEO Settings', 'umbra-bookshelf'), false); echo '
'; echo '
'; - echo ''; + echo ''; echo ''; - echo '' . esc_html__('Custom title for search engines (defaults to book title if empty).', 'bd-bookshelfhelper') . ''; + echo '' . esc_html__('Custom title for search engines (defaults to book title if empty).', 'umbra-bookshelf') . ''; echo '
'; echo '
'; - echo ''; + echo ''; echo ''; - echo '' . esc_html__('Meta description for search results (150-160 characters recommended).', 'bd-bookshelfhelper') . ''; + echo '' . esc_html__('Meta description for search results (150-160 characters recommended).', 'umbra-bookshelf') . ''; echo '
'; echo '
'; // End SEO // Action buttons echo '
'; - echo '' . esc_html__('Edit in Standard Editor', 'bd-bookshelfhelper') . ''; - submit_button(esc_html__('Save Book Data', 'bd-bookshelfhelper'), 'primary large', 'bd_save_data_entry', false); - echo ''; + echo '' . esc_html__('Edit in Standard Editor', 'umbra-bookshelf') . ''; + submit_button(esc_html__('Save Book Data', 'umbra-bookshelf'), 'primary large', 'bd_save_data_entry', false); + echo ''; echo '
'; echo ''; @@ -685,12 +697,12 @@ class BD_BookShelfHelper_Admin { */ private function save_data_entry($post_id) { if (!isset($_POST['bd_data_entry_save_nonce']) || !wp_verify_nonce($_POST['bd_data_entry_save_nonce'], 'bd_data_entry_save')) { - echo '
' . esc_html__('Security check failed.', 'bd-bookshelfhelper') . '
'; + echo '
' . esc_html__('Security check failed.', 'umbra-bookshelf') . '
'; return; } if (!current_user_can('edit_post', $post_id)) { - echo '
' . esc_html__('Permission denied.', 'bd-bookshelfhelper') . '
'; + echo '
' . esc_html__('Permission denied.', 'umbra-bookshelf') . '
'; return; } @@ -724,7 +736,7 @@ class BD_BookShelfHelper_Admin { } // Language content - $languages = BD_BookShelfHelper::get_supported_languages(); + $languages = Umbra_Bookshelf_Helper::get_supported_languages(); foreach ($languages as $code => $name) { $title_key = 'title_' . $code; $blurb_key = 'blurb_' . $code; @@ -769,7 +781,7 @@ class BD_BookShelfHelper_Admin { } } - echo '
' . esc_html__('Book data saved successfully!', 'bd-bookshelfhelper') . '
'; + echo '
' . esc_html__('Book data saved successfully!', 'umbra-bookshelf') . '
'; } /** @@ -815,7 +827,7 @@ class BD_BookShelfHelper_Admin { } // Save language content - $languages = BD_BookShelfHelper::get_supported_languages(); + $languages = Umbra_Bookshelf_Helper::get_supported_languages(); foreach ($languages as $code => $name) { $title_key = 'title_' . $code; $blurb_key = 'blurb_' . $code; @@ -882,7 +894,7 @@ class BD_BookShelfHelper_Admin { } } - $languages = BD_BookShelfHelper::get_supported_languages(); + $languages = Umbra_Bookshelf_Helper::get_supported_languages(); foreach ($languages as $code => $name) { $title_key = 'title_' . $code; @@ -996,7 +1008,7 @@ class BD_BookShelfHelper_Admin { 'active' => 1, 'created_at' => current_time('mysql'), )); - echo '

' . esc_html__('Pen name added.', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('Pen name added.', 'umbra-bookshelf') . '

'; } } @@ -1014,24 +1026,24 @@ class BD_BookShelfHelper_Admin { $pen_names = $wpdb->get_results("SELECT * FROM {$table_name} ORDER BY pen_name"); echo '
'; - echo '

' . esc_html__('Pen Names', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('Pen Names', 'umbra-bookshelf') . '

'; // Add new form echo '
'; wp_nonce_field('bd_add_pen_name'); - echo '

' . esc_html__('Add New Pen Name', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('Add New Pen Name', 'umbra-bookshelf') . '

'; echo '
'; - echo ''; + echo ''; echo ''; - echo ''; + echo '

' . esc_html__('e.g., J.K. Murphy', 'umbra-bookshelf') . '

'; + echo ''; echo ''; echo '
' . esc_html__('Pen Name', 'bd-bookshelfhelper') . '
' . esc_html__('Pen Name', 'umbra-bookshelf') . ''; - echo '

' . esc_html__('e.g., J.K. Murphy', 'bd-bookshelfhelper') . '

' . esc_html__('Notes', 'bd-bookshelfhelper') . '
' . esc_html__('Notes', 'umbra-bookshelf') . '
'; - submit_button(esc_html__('Add Pen Name', 'bd-bookshelfhelper'), 'primary', 'bd_add_pen_name'); + submit_button(esc_html__('Add Pen Name', 'umbra-bookshelf'), 'primary', 'bd_add_pen_name'); echo ''; // Existing list - echo '

' . esc_html__('Existing Pen Names', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('Existing Pen Names', 'umbra-bookshelf') . '

'; echo ''; echo ''; echo ''; @@ -1042,21 +1054,21 @@ class BD_BookShelfHelper_Admin { printf('', esc_html($pn->id)); printf('', esc_html($pn->pen_name)); printf('', esc_html($pn->notes)); - printf('', $pn->active ? esc_html__('Active', 'bd-bookshelfhelper') : esc_html__('Inactive', 'bd-bookshelfhelper')); + printf('', $pn->active ? esc_html__('Active', 'umbra-bookshelf') : esc_html__('Inactive', 'umbra-bookshelf')); echo ''; } } else { - echo ''; + echo ''; } echo '
IDPen NameNotesStatusActions
%d%s%s%s%s'; printf('
%s', wp_nonce_field('bd_update_pen_status', '_wpnonce', true, false)); printf('', esc_attr($pn->id)); printf('', checked($pn->active, 1, false)); - submit_button(esc_html__('Update', 'bd-bookshelfhelper'), 'secondary', 'bd_update_pen_status', false); + submit_button(esc_html__('Update', 'umbra-bookshelf'), 'secondary', 'bd_update_pen_status', false); echo '
'; printf('
%s', wp_nonce_field('bd_delete_pen_name', '_wpnonce', true, false)); printf('', esc_attr($pn->id)); - submit_button(esc_html__('Delete', 'bd-bookshelfhelper'), 'delete', 'bd_delete_pen_name', false, array('onclick' => 'return confirm("Delete this pen name?");')); + submit_button(esc_html__('Delete', 'umbra-bookshelf'), 'delete', 'bd_delete_pen_name', false, array('onclick' => 'return confirm("Delete this pen name?");')); echo '
'; echo '
' . esc_html__('No pen names found.', 'bd-bookshelfhelper') . '
' . esc_html__('No pen names found.', 'umbra-bookshelf') . '
'; @@ -1081,20 +1093,20 @@ class BD_BookShelfHelper_Admin { $goodreads_id = sanitize_text_field($_POST['goodreads_author_id'] ?? ''); update_option('bdm_goodreads_author_id', $goodreads_id); - echo '

' . esc_html__('Settings saved.', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('Settings saved.', 'umbra-bookshelf') . '

'; } $affiliates = get_option('bdm_affiliate_codes', array()); $goodreads_id = get_option('bdm_goodreads_author_id', '30469651'); echo '
'; - echo '

' . esc_html__('BD BookHelper Settings', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('BD BookHelper Settings', 'umbra-bookshelf') . '

'; echo '
'; wp_nonce_field('bd_settings'); - echo '

' . esc_html__('Affiliate Codes', 'bd-bookshelfhelper') . '

'; - echo '

' . esc_html__('Enter your affiliate IDs for each retailer. Links include these IDs when available.', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('Affiliate Codes', 'umbra-bookshelf') . '

'; + echo '

' . esc_html__('Enter your affiliate IDs for each retailer. Links include these IDs when available.', 'umbra-bookshelf') . '

'; echo ''; $affiliate_fields = array( @@ -1113,17 +1125,17 @@ class BD_BookShelfHelper_Admin { } echo '
'; - echo '

' . esc_html__('Goodreads Integration', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('Goodreads Integration', 'umbra-bookshelf') . '

'; echo ''; - printf(''); + printf(''); printf(''; + echo '

' . esc_html__('Default: 30469651 (B_D_Murphy)', 'umbra-bookshelf') . '

'; echo '
' . esc_html__('Goodreads Author ID', 'bd-bookshelfhelper') . '
' . esc_html__('Goodreads Author ID', 'umbra-bookshelf') . '', esc_attr($goodreads_id)); - echo '

' . esc_html__('Default: 30469651 (B_D_Murphy)', 'bd-bookshelfhelper') . '

'; - echo '

' . esc_html__('Amazon Ratings Warning', 'bd-bookshelfhelper') . '

'; - echo '

' . esc_html__('Warning: Scraping Amazon product pages may conflict with their Terms of Service. Use at your own risk.', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('Amazon Ratings Warning', 'umbra-bookshelf') . '

'; + echo '

' . esc_html__('Warning: Scraping Amazon product pages may conflict with their Terms of Service. Use at your own risk.', 'umbra-bookshelf') . '

'; - submit_button(esc_html__('Save Settings', 'bd-bookshelfhelper'), 'primary', 'bd_save_settings'); + submit_button(esc_html__('Save Settings', 'umbra-bookshelf'), 'primary', 'bd_save_settings'); echo '
'; } @@ -1137,7 +1149,7 @@ class BD_BookShelfHelper_Admin { $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$old_table'") === $old_table; if (isset($_POST['bd_run_migration']) && check_admin_referer('bd_migrate_nonce')) { - $result = BD_BookShelfHelper::migrate_from_bd_books(); + $result = Umbra_Bookshelf_Helper::migrate_from_bd_books(); if ($result['success']) { echo '

' . esc_html($result['message']) . '

'; } else { @@ -1146,13 +1158,13 @@ class BD_BookShelfHelper_Admin { } echo '
'; - echo '

' . esc_html__('Migrate from BD Books', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('Migrate from BD Books', 'umbra-bookshelf') . '

'; if (!$table_exists) { echo '
'; - echo '

' . esc_html__('The old BD Books table was not found. No migration needed - the plugin data does not exist.', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('The old BD Books table was not found. No migration needed - the plugin data does not exist.', 'umbra-bookshelf') . '

'; echo '
'; - echo '

' . esc_html__('This tool migrates books from the original "BD Books" (BD Book Listing) plugin to the new BookHelper system using custom post types.', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('This tool migrates books from the original "BD Books" (BD Book Listing) plugin to the new BookHelper system using custom post types.', 'umbra-bookshelf') . '

'; echo '
'; return; } @@ -1167,12 +1179,12 @@ class BD_BookShelfHelper_Admin { )); echo '
'; - echo '

' . sprintf(esc_html__('Found %d books in the old BD Books table.', 'bd-bookshelfhelper'), $old_count) . '

'; - echo '

' . sprintf(esc_html__('Current BookHelper has %d books.', 'bd-bookshelfhelper'), count($new_books)) . '

'; + echo '

' . sprintf(esc_html__('Found %d books in the old BD Books table.', 'umbra-bookshelf'), $old_count) . '

'; + echo '

' . sprintf(esc_html__('Current BookHelper has %d books.', 'umbra-bookshelf'), count($new_books)) . '

'; echo '
'; if ($old_count > 0) { - echo '

' . esc_html__('Books to Migrate', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('Books to Migrate', 'umbra-bookshelf') . '

'; echo ''; echo ''; echo ''; @@ -1181,25 +1193,25 @@ class BD_BookShelfHelper_Admin { echo ''; echo ''; echo ''; - echo ''; + echo ''; echo ''; } echo '
TitleAuthorCover
' . esc_html($book->title) . '' . esc_html($book->author) . '' . ($has_cover ? esc_html__('Yes', 'bd-bookshelfhelper') : esc_html__('No', 'bd-bookshelfhelper')) . '' . ($has_cover ? esc_html__('Yes', 'umbra-bookshelf') : esc_html__('No', 'umbra-bookshelf')) . '
'; - echo '

' . esc_html__('What gets migrated:', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('What gets migrated:', 'umbra-bookshelf') . '

'; echo '
    '; - echo '
  • ' . esc_html__('Book titles and authors (as pen names)', 'bd-bookshelfhelper') . '
  • '; - echo '
  • ' . esc_html__('Descriptions become blurbs in English', 'bd-bookshelfhelper') . '
  • '; - echo '
  • ' . esc_html__('Cover images (WordPress media or external URLs)', 'bd-bookshelfhelper') . '
  • '; - echo '
  • ' . esc_html__('Purchase links to retailer fields', 'bd-bookshelfhelper') . '
  • '; + echo '
  • ' . esc_html__('Book titles and authors (as pen names)', 'umbra-bookshelf') . '
  • '; + echo '
  • ' . esc_html__('Descriptions become blurbs in English', 'umbra-bookshelf') . '
  • '; + echo '
  • ' . esc_html__('Cover images (WordPress media or external URLs)', 'umbra-bookshelf') . '
  • '; + echo '
  • ' . esc_html__('Purchase links to retailer fields', 'umbra-bookshelf') . '
  • '; echo '
'; echo '
'; wp_nonce_field('bd_migrate_nonce'); - echo ''; + echo ''; echo '
'; } else { - echo '

' . esc_html__('No books found in the old table to migrate.', 'bd-bookshelfhelper') . '

'; + echo '

' . esc_html__('No books found in the old table to migrate.', 'umbra-bookshelf') . '

'; } echo '
'; @@ -1213,6 +1225,6 @@ class BD_BookShelfHelper_Admin { $book_id = absint($_POST['book_id']); - wp_send_json_success(array('message' => esc_html__('Links updated.', 'bd-bookshelfhelper'))); + wp_send_json_success(array('message' => esc_html__('Links updated.', 'umbra-bookshelf'))); } } diff --git a/bd-bookshelfhelper/inc/class-install.php b/umbra-bookshelf/includes/library/class-umbra-library-install.php similarity index 97% rename from bd-bookshelfhelper/inc/class-install.php rename to umbra-bookshelf/includes/library/class-umbra-library-install.php index 56d5f4c..a145a5f 100644 --- a/bd-bookshelfhelper/inc/class-install.php +++ b/umbra-bookshelf/includes/library/class-umbra-library-install.php @@ -5,14 +5,14 @@ /** * Installation and Database Setup * - * @package BD_BookShelfHelper + * @package Umbra_Bookshelf_Helper */ if (!defined('ABSPATH')) { exit; } -class BD_BookShelfHelper_Install { +class Umbra_Bookshelf_Helper_Install { /** * Activate plugin */ @@ -153,7 +153,7 @@ class BD_BookShelfHelper_Install { * Create roles */ private static function create_roles() { - add_role('book_manager', esc_html__('Book Manager', 'bd-bookshelfhelper'), array( + add_role('book_manager', esc_html__('Book Manager', 'umbra-bookshelf'), array( 'read' => true, 'edit_books' => true, 'edit_others_books' => true, diff --git a/bd-bookshelfhelper/assets/css/admin.css b/umbra-bookshelf/library/assets/css/admin.css similarity index 100% rename from bd-bookshelfhelper/assets/css/admin.css rename to umbra-bookshelf/library/assets/css/admin.css diff --git a/bd-bookshelfhelper/assets/js/admin.js b/umbra-bookshelf/library/assets/js/admin.js similarity index 100% rename from bd-bookshelfhelper/assets/js/admin.js rename to umbra-bookshelf/library/assets/js/admin.js diff --git a/bd-bookshelf-plugin/public/css/bd-bookshelf.css b/umbra-bookshelf/public/css/umbra-bookshelf.css similarity index 98% rename from bd-bookshelf-plugin/public/css/bd-bookshelf.css rename to umbra-bookshelf/public/css/umbra-bookshelf.css index cfcfc32..4ab21c7 100644 --- a/bd-bookshelf-plugin/public/css/bd-bookshelf.css +++ b/umbra-bookshelf/public/css/umbra-bookshelf.css @@ -1,7 +1,7 @@ /** * BD Bookshelf - Public Styles * - * @package BD_Bookshelf + * @package Umbra_Bookshelf */ /* Sell Grid */ diff --git a/bd-bookshelf-plugin/public/js/bd-bookshelf-gdpr.js b/umbra-bookshelf/public/js/umbra-bookshelf-gdpr.js similarity index 76% rename from bd-bookshelf-plugin/public/js/bd-bookshelf-gdpr.js rename to umbra-bookshelf/public/js/umbra-bookshelf-gdpr.js index 20e9fe2..6e7868a 100644 --- a/bd-bookshelf-plugin/public/js/bd-bookshelf-gdpr.js +++ b/umbra-bookshelf/public/js/umbra-bookshelf-gdpr.js @@ -4,7 +4,7 @@ /** * BD Bookshelf - GDPR Consent * - * @package BD_Bookshelf + * @package Umbra_Bookshelf */ (function($) { @@ -12,7 +12,7 @@ $(document).ready(function() { // Check if consent already given - if (bdBookshelfHasConsent()) { + if (umbraBookshelfHasConsent()) { return; } @@ -22,16 +22,16 @@ // Handle accept button $('#bd-bookshelf-gdpr-accept').on('click', function() { $.ajax({ - url: bdBookshelfGdpr.ajaxurl, + url: umbraBookshelfGdpr.ajaxurl, type: 'POST', data: { action: 'bd_bookshelf_accept_consent', - nonce: bdBookshelfGdpr.nonce + nonce: umbraBookshelfGdpr.nonce }, success: function(response) { if (response.success) { // Set cookie - bdBookshelfSetCookie('bd_bookshelf_consent', '1', bdBookshelfGdpr.cookieDays); + umbraBookshelfSetCookie('bd_bookshelf_consent', '1', umbraBookshelfGdpr.cookieDays); // Hide banner $('#bd-bookshelf-gdpr-banner').fadeOut(300); @@ -41,18 +41,18 @@ }); }); - function bdBookshelfHasConsent() { - return bdBookshelfGetCookie('bd_bookshelf_consent') === '1'; + function umbraBookshelfHasConsent() { + return umbraBookshelfGetCookie('bd_bookshelf_consent') === '1'; } - function bdBookshelfSetCookie(name, value, days) { + function umbraBookshelfSetCookie(name, value, days) { var date = new Date(); date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); var expires = "expires=" + date.toUTCString(); document.cookie = name + "=" + value + ";" + expires + ";path=/"; } - function bdBookshelfGetCookie(name) { + function umbraBookshelfGetCookie(name) { var nameEQ = name + "="; var ca = document.cookie.split(';'); for (var i = 0; i < ca.length; i++) { diff --git a/bd-bookshelf-plugin/public/js/bd-bookshelf.js b/umbra-bookshelf/public/js/umbra-bookshelf.js similarity index 90% rename from bd-bookshelf-plugin/public/js/bd-bookshelf.js rename to umbra-bookshelf/public/js/umbra-bookshelf.js index 60b9b24..86c9ddd 100644 --- a/bd-bookshelf-plugin/public/js/bd-bookshelf.js +++ b/umbra-bookshelf/public/js/umbra-bookshelf.js @@ -4,7 +4,7 @@ /** * BD Bookshelf - Public JavaScript * - * @package BD_Bookshelf + * @package Umbra_Bookshelf */ (function($) { diff --git a/umbra-bookshelf/readme.txt b/umbra-bookshelf/readme.txt new file mode 100644 index 0000000..244570c --- /dev/null +++ b/umbra-bookshelf/readme.txt @@ -0,0 +1,81 @@ +=== Umbra Bookshelf === +Contributors: umbratools +Tags: books, series, isbn, authors, newsletter, publishing +Requires at least: 6.0 +Tested up to: 6.6 +Requires PHP: 7.4 +Stable tag: 1.0.0 +License: Apache-2.0 +License URI: https://www.apache.org/licenses/LICENSE-2.0 + +Complete book management for authors and publishers: catalogue and ISBN tracking, series, pen names, retail links, rating sync, GDPR consent, and newsletter delivery. + +== Description == + +Umbra Bookshelf is a single plugin covering the full book workflow. It +consolidates three previously separate plugins: + +* **Books** (formerly BD Bookshelf) — the catalogue: ISBN/ASIN variants, + author records, series, retail sites and per-format buy links, Goodreads and + Amazon rating synchronisation, SEO rows, shortlink click tracking, GDPR + consent banners, and CSV/post import. +* **Library** (formerly BD BookShelfHelper) — the WordPress-native model: the + `book` post type and `series` taxonomy, pen names, a focused data-entry + screen, shortlinks, reviews and ratings, plus a migration screen. +* **Email** (formerly BD SimpleEmail) — newsletter delivery: subscribers and + newsletters, a `[bdm_signup]` shortcode, signup and unsubscribe handling via + AJAX and `admin_post`, Gemini-assisted composition, and Gmail/SMTP sending. + +Everything now sits under a single **Umbra Bookshelf** admin menu. + += Upgrading from the old plugins = + +Umbra Bookshelf reads and writes the same database tables, option keys, post +meta, cron hooks and shortcodes as the three plugins it replaces. Existing data +is picked up as-is; no migration is required. + +Install Umbra Bookshelf, then deactivate BD Bookshelf, BD BookShelfHelper and +BD SimpleEmail. **Do not delete** them from the Plugins page first — deleting +runs their uninstall routines, which drop your tables and options. + +The shortcodes `[bd-book]`, `[bd-books]` and `[bdm_signup]` are unchanged, so +existing posts keep working. The admin menu locations have changed: see the +specification in `spec/SPEC_Umbra_Bookshelf_Plugin.md` for the full menu map. + +== Frequently Asked Questions == + += Do I need to migrate my books? = + +No. Table names, option keys, post meta keys and shortcodes are deliberately +unchanged. Deactivate the old plugins rather than deleting them. + += The admin menu looks different. = + +It is one menu now instead of three. The catalogue, library and email screens +are all still present as submenus, and Settings is split into catalogue +settings and library settings. + += Are there known problems? = + +Yes — see `docs/KNOWN-ISSUES.md`. The most significant is that the catalogue and +library modules both create `bd_books` and `bd_series` with different schemas, +inherited from the old plugins. If you ran more than one of them together, check +that screen before relying on it. + +== Changelog == + += 1.0.0 = +* Initial release of the consolidated plugin. +* Merged BD Bookshelf, BD BookShelfHelper and BD SimpleEmail into one plugin + under a single admin menu. +* Renamed plugin headers, text domains, classes, constants, asset handles and + admin slugs to the Umbra Bookshelf identity. +* Preserved all database tables, option keys, post meta, cron hooks, nonces and + shortcodes, so existing installs need no migration. +* Registered the top-level admin menu once instead of three times. +* Fixed a duplicate-class fatal error between the catalogue's theme-compatibility + shim and the library module's helper class. +* Fixed duplicate AJAX handler, duplicate Settings submenu, and colliding asset + handles across modules. +* Added `spec/SPEC_Umbra_Bookshelf_Plugin.md`, `docs/KNOWN-ISSUES.md`, + `docs/ARCHITECTURE.md`, and the `tests/` verification suite. diff --git a/umbra-bookshelf/spec/SPEC_Umbra_Bookshelf_Plugin.md b/umbra-bookshelf/spec/SPEC_Umbra_Bookshelf_Plugin.md new file mode 100644 index 0000000..a345f3b --- /dev/null +++ b/umbra-bookshelf/spec/SPEC_Umbra_Bookshelf_Plugin.md @@ -0,0 +1,224 @@ +# Umbra Bookshelf — Plugin Specification + +**Status:** Active +**Version:** 1.0.0 +**Supersedes:** `spec/archive/SPEC_BD_Bookshelf_Plugin.txt`, `spec/archive/SPEC_BD_BookHelper_Plugin.txt` +**License:** Apache-2.0 + +--- + +## 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.0.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`, `bd_bookshelf_goodreads_refresh`, `bd_bookshelf_amazon_scrape` | +| 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 `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/ +│ ├── books/ catalogue: books, authors, series, retail, ratings, +│ │ shortcodes, SEO, GDPR, scrapers, 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_Amazon_Scraper`, `Umbra_Admin`, +`Umbra_Migration`, `Umbra_Theme_Compatibility`. + +> `Umbra_SEO` is never instantiated by the bootstrap (carried over unchanged from +> the original plugin). See `docs/KNOWN-ISSUES.md` item 5. + +### 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`. + +### 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. + +--- + +## 6. Lifecycle + +* **Activate:** 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`. +* **Deactivate:** clear all three cron hooks, 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`. + +--- + +## 7. Verification + +```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 +``` + +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. + +--- + +## 8. Open issues + +`bd_books` and `bd_series` are created with two incompatible schemas by the +books and library modules. This predates the consolidation and is **not** fixed +here; see `docs/KNOWN-ISSUES.md`. diff --git a/umbra-bookshelf/spec/archive/README.md b/umbra-bookshelf/spec/archive/README.md new file mode 100644 index 0000000..79c6130 --- /dev/null +++ b/umbra-bookshelf/spec/archive/README.md @@ -0,0 +1,24 @@ +# Archived specifications + +These are the design documents for the three plugins as they existed +**before** the Umbra Bookshelf consolidation. They are kept for provenance: the +reasoning behind the original data models is not captured anywhere else. + +**They describe superseded code and must not be used as an implementation +reference.** Where they disagree with `../SPEC_Umbra_Bookshelf_Plugin.md`, the +consolidated specification is correct. + +| File | Describes | Status | +|---|---|---| +| `SPEC_BD_Bookshelf_Plugin.txt` | BD Bookshelf 0.1.0 (catalogue) | superseded by the Books module | +| `SPEC_BD_BookHelper_Plugin.txt` | BD BookShelfHelper 2.0.0 (library) | superseded by the Library module | +| `bd-bookshelf-plugin-spec-0.0.1.md` | BD Bookshelf design spec | superseded | +| `bd-bookshelf-plugin-questions-0.0.1.md` | design Q&A, round 1 | historical record | +| `bd-bookshelf-plugin-questions-0.0.2.md` | design Q&A, round 2 | historical record | + +Old plugin names, class names and table names are preserved verbatim in these +files on purpose — they are a record of what was, not a description of what is. + +The pre-merge `.docx` specifications (`BDMurphy-Plugin-Specifications-v2.2.docx`, +`BDMurphy-Theme-Specifications-v2.2.docx`) are binary and were left in the +archived **Bookshelf** repository rather than rewritten here. diff --git a/SPEC_BD_BookHelper_Plugin.txt b/umbra-bookshelf/spec/archive/SPEC_BD_BookHelper_Plugin.txt similarity index 100% rename from SPEC_BD_BookHelper_Plugin.txt rename to umbra-bookshelf/spec/archive/SPEC_BD_BookHelper_Plugin.txt diff --git a/SPEC_BD_Bookshelf_Plugin.txt b/umbra-bookshelf/spec/archive/SPEC_BD_Bookshelf_Plugin.txt similarity index 100% rename from SPEC_BD_Bookshelf_Plugin.txt rename to umbra-bookshelf/spec/archive/SPEC_BD_Bookshelf_Plugin.txt diff --git a/bd-bookshelf-plugin-questions-0.0.1.md b/umbra-bookshelf/spec/archive/bd-bookshelf-plugin-questions-0.0.1.md similarity index 100% rename from bd-bookshelf-plugin-questions-0.0.1.md rename to umbra-bookshelf/spec/archive/bd-bookshelf-plugin-questions-0.0.1.md diff --git a/bd-bookshelf-plugin-questions-0.0.2.md b/umbra-bookshelf/spec/archive/bd-bookshelf-plugin-questions-0.0.2.md similarity index 100% rename from bd-bookshelf-plugin-questions-0.0.2.md rename to umbra-bookshelf/spec/archive/bd-bookshelf-plugin-questions-0.0.2.md diff --git a/bd-bookshelf-plugin-spec-0.0.1.md b/umbra-bookshelf/spec/archive/bd-bookshelf-plugin-spec-0.0.1.md similarity index 100% rename from bd-bookshelf-plugin-spec-0.0.1.md rename to umbra-bookshelf/spec/archive/bd-bookshelf-plugin-spec-0.0.1.md diff --git a/umbra-bookshelf/tests/smoke_runtime.php b/umbra-bookshelf/tests/smoke_runtime.php new file mode 100644 index 0000000..8c989fa --- /dev/null +++ b/umbra-bookshelf/tests/smoke_runtime.php @@ -0,0 +1,307 @@ + 7); } +function get_terms($a = array()) { return 4; } +function is_wp_error($t) { return false; } +function get_post_meta($p, $k, $s = true) { return ''; } +function update_post_meta($p, $k, $v) { return true; } +function get_post($p) { return null; } +function get_the_post_thumbnail_url($p, $s = '') { return ''; } +function wp_schedule_event($t, $r, $h) { $GLOBALS['__cron'][$h] = true; } +function wp_next_scheduled($h) { return false; } +function wp_clear_scheduled_hook($h) { unset($GLOBALS['__cron'][$h]); } +function wp_unschedule_hook($h) {} +function add_shortcode($tag, $cb) { $GLOBALS['__shortcodes'][] = $tag; } +function register_post_type($t, $a = array()) { $GLOBALS['__pt'][$t] = $a; return $t; } +function register_taxonomy($t, $o, $a = array()) { $GLOBALS['__tax'][$t] = $a; return $t; } +function add_menu_page($pt, $mt, $cap, $slug, $cb = '', $icon = '', $pos = null) { + $GLOBALS['__menus'][$slug] = array('page_title' => $pt, 'capability' => $cap, + 'callback' => $cb, 'icon' => $icon, 'position' => $pos); + return $slug; +} +function add_submenu_page($parent, $pt, $mt, $cap, $slug, $cb = '') { + $GLOBALS['__submenus'][$slug] = array('parent' => $parent, 'page_title' => $pt, + 'menu_title' => $mt, 'capability' => $cap, + 'callback' => $cb); + return $slug; +} +function get_current_screen() { return null; } +function wp_enqueue_media() {} +function wp_enqueue_style($h, $src = '', $d = array(), $v = false, $m = 'all') { + $GLOBALS['__enqueued']['style'][$h] = $src; +} +function wp_enqueue_script($h, $src = '', $d = array(), $v = false, $f = false) { + $GLOBALS['__enqueued']['script'][$h] = $src; +} +function wp_localize_script($h, $name, $data) { + $GLOBALS['__enqueued']['localize'][$h] = $name; +} +function wp_register_post_type_args($a, $t) { return $a; } +function register_post_meta_args($a, $t) { return $a; } +function wp_get_current_user() { return null; } +function get_userdata($i) { return false; } +function wp_roles() { return null; } +function add_role($r, $n, $c) {} +function remove_role($r) {} +function wp_send_json_success($d) {} +function wp_send_json_error($d) {} +function check_ajax_referer($a, $q = false) { return 1; } +function wp_die_ajax($m) {} +function sanitize_email($e) { return $e; } +function sanitize_key_($k) { return $k; } +function get_home_url($p = '') { return 'https://example.test'; } +function home_url($p = '') { return 'https://example.test'; } +function wp_mail($to, $subj, $msg) { return true; } +function wp_rand($a = 0, $b = 0) { return 12345; } +function get_bloginfo($s = '') { return 'Example'; } +function get_user_meta($i, $k, $s = false) { return ''; } +function update_user_meta($i, $k, $v) { return true; } +function add_rewrite_rule($r, $q, $after = 'bottom') { return true; } +function add_rewrite_tag($r, $q, $a = '') { return true; } +function add_query_var($v) { return $v; } +function get_query_var($v, $d = '') { return $d; } +function get_option_blogname($d = '') { return 'Example'; } +function do_action($h) { + foreach ($GLOBALS['__hooks'][$h] ?? array() as $cb) { + if (is_callable($cb)) { call_user_func($cb); } + } +} + +$fail = 0; +function check($label, $cond, $detail = '') { + global $fail; + if ($cond) { + echo " ok $label\n"; + } else { + echo " FAIL $label" . ($detail ? " ($detail)" : '') . "\n"; + $GLOBALS['fail']++; + } +} +function section($t) { echo "\n$t\n"; } + +// ---------------------------------------------------------------- load it +section('1. Load plugin bootstrap (front-end context)'); +try { + require dirname(__DIR__) . '/umbra-bookshelf.php'; + echo " ok bootstrap included without fatal error\n"; +} catch (Throwable $e) { + echo " FAIL fatal: " . $e->getMessage() . "\n"; + exit(1); +} + +section('2. Core class graph'); +check('Umbra_Bookshelf class exists', class_exists('Umbra_Bookshelf')); +check('singleton instance() returns object', + umbra_bookshelf() instanceof Umbra_Bookshelf); +check('instance() is idempotent', + umbra_bookshelf() === umbra_bookshelf()); + +section('3. Module classes loaded'); +foreach (array('Umbra_Database', 'Umbra_Book_Manager', 'Umbra_Series', + 'Umbra_Authors', 'Umbra_Retail_Sites', 'Umbra_Rating_Sync', + 'Umbra_Shortcodes', 'Umbra_SEO', 'Umbra_GDPR', + 'Umbra_Goodreads_Scraper', 'Umbra_Amazon_Scraper', + 'Umbra_Admin', 'Umbra_Migration', 'Umbra_Theme_Compatibility', + 'Umbra_Bookshelf_Helper', 'Umbra_Bookshelf_Helper_Admin', + 'Umbra_Bookshelf_Helper_Install', 'Umbra_Simple_Email', + 'Umbra_Simple_Email_Admin', 'Umbra_Simple_Email_Install', + 'Umbra_Email_Sender') as $c) { + check("class $c", class_exists($c)); +} + +section('4. Legacy theme alias'); +check('class_alias exposes BD_BookShelfHelper for the theme', + class_exists('BD_BookShelfHelper', false)); +check('alias resolves to the compatibility shim', + is_a('BD_BookShelfHelper', 'Umbra_Theme_Compatibility', true)); +check('shim exposes get_cover_url (theme API)', + method_exists('BD_BookShelfHelper', 'get_cover_url')); +check('real library class still distinct', + !is_a('Umbra_Bookshelf_Helper', 'Umbra_Theme_Compatibility', true)); + +section('5. Constants'); +check('UMBRA_BOOKSHELF_VERSION', defined('UMBRA_BOOKSHELF_VERSION') + && UMBRA_BOOKSHELF_VERSION === '1.0.0', UMBRA_BOOKSHELF_VERSION ?? 'undefined'); +check('UMBRA_BOOKSHELF_DIR', defined('UMBRA_BOOKSHELF_DIR')); +check('UMBRA_BOOKSHELF_URL', defined('UMBRA_BOOKSHELF_URL')); +check('UMBRA_BOOKSHELF_BASENAME', defined('UMBRA_BOOKSHELF_BASENAME')); +check('library module version', defined('UMBRA_BOOKSHELF_LIBRARY_VERSION')); +check('email module version', defined('UMBRA_BOOKSHELF_EMAIL_VERSION')); +check('old BD_BOOKSHELF_VERSION removed', !defined('BD_BOOKSHELF_VERSION')); +check('old BD_BHS_DIR removed', !defined('BD_BHS_DIR')); +check('old BD_EMAIL_URI removed', !defined('BD_EMAIL_URI')); + +section('6. Boot the modules (' . $MODE . ' request)'); +try { + do_action('plugins_loaded'); + do_action('init'); + echo " ok plugins_loaded + init fired without fatal error\n"; +} catch (Throwable $e) { + echo " FAIL fatal on init: " . $e->getMessage() . "\n"; + $GLOBALS['fail']++; +} +check('book post type registered by library module', + isset($GLOBALS['__pt']['book'])); +check('series taxonomy registered by library module', + isset($GLOBALS['__tax']['series'])); +check('[bd-book] shortcode preserved', + in_array('bd-book', $GLOBALS['__shortcodes'], true)); +check('[bd-books] shortcode preserved', + in_array('bd-books', $GLOBALS['__shortcodes'], true)); +check('[bdm_signup] shortcode preserved', + in_array('bdm_signup', $GLOBALS['__shortcodes'], true)); + +section('7. Admin menu consolidation (admin context)'); +try { + do_action('admin_menu'); + echo " ok admin_menu hooks fired without fatal error\n"; +} catch (Throwable $e) { + echo " FAIL fatal on admin_menu: " . $e->getMessage() . "\n"; + $GLOBALS['fail']++; +} + +$menus = $GLOBALS['__menus']; +if ($MODE !== 'admin') { + echo " .. skipped menu assertions (front-end request: no admin_menu)\n"; + section('RESULT'); + if (!empty($GLOBALS['fail'])) { + echo "FAIL - {$GLOBALS['fail']} check(s) failed\n"; + exit(1); + } + echo "PASS - runtime smoke test clean\n"; + exit(0); +} +check('exactly ONE top-level menu registered', count($menus) === 1, + count($menus) . ': ' . implode(', ', array_keys($menus))); +check('top-level slug is umbra-bookshelf', + isset($menus['umbra-bookshelf']), implode(', ', array_keys($menus))); +check('top-level titled "Umbra Bookshelf"', + ($menus['umbra-bookshelf']['page_title'] ?? '') === 'Umbra Bookshelf', + $menus['umbra-bookshelf']['page_title'] ?? ''); + +$subs = $GLOBALS['__submenus']; +$badParent = array(); +foreach ($subs as $slug => $s) { + if ($s['parent'] !== 'umbra-bookshelf') { $badParent[$slug] = $s['parent']; } +} +check('every submenu hangs off umbra-bookshelf', count($badParent) === 0, + json_encode($badParent)); + +$expected = array( + 'umbra-bookshelf', 'umbra-bookshelf-all', 'umbra-bookshelf-add', + 'umbra-bookshelf-authors', 'umbra-bookshelf-series', 'umbra-bookshelf-sites', + 'umbra-bookshelf-shortcodes', 'umbra-bookshelf-sync', 'umbra-bookshelf-settings', + 'umbra-bookshelf-import', + 'edit.php?post_type=book', 'post-new.php?post_type=book', + 'umbra-bookshelf-data-entry', 'edit-tags.php?taxonomy=series&post_type=book', + 'umbra-bookshelf-pen-names', 'umbra-bookshelf-library-settings', + 'umbra-bookshelf-migrate', + 'umbra-bookshelf-subscribers', 'umbra-bookshelf-newsletter', + 'umbra-bookshelf-email-settings', +); +$missing = array_values(array_diff($expected, array_keys($subs))); +check('all ' . count($expected) . ' expected submenus present', count($missing) === 0, + 'missing: ' . implode(', ', $missing)); + +check('no duplicate submenu slug (settings screens distinct)', + !isset($subs['bd-bookshelf-settings']), + 'legacy duplicate slug still registered'); +check('catalogue settings present', isset($subs['umbra-bookshelf-settings'])); +check('library settings present', isset($subs['umbra-bookshelf-library-settings'])); +check('email settings present', isset($subs['umbra-bookshelf-email-settings'])); + +section('8. Duplicate hook / action registrations'); +$dupes = array(); +foreach ($GLOBALS['__hooks'] as $hook => $cbs) { + $seen = array(); + foreach ($cbs as $cb) { + $k = is_string($cb) ? $cb : (is_array($cb) ? (is_object($cb[0]) ? get_class($cb[0]) . '::' . $cb[1] : $cb[0] . '::' . $cb[1]) : 'closure'); + $seen[$k] = ($seen[$k] ?? 0) + 1; + } + foreach ($seen as $k => $n) { + if ($n > 1) { $dupes[] = "$hook => $k ($n)"; } + } +} +check('no callback registered twice on one hook', count($dupes) === 0, + implode('; ', $dupes)); + +section('9. Asset handles are unique per module'); +$styles = array_keys($GLOBALS['__enqueued']['style'] ?? array()); +$scripts = array_keys($GLOBALS['__enqueued']['script'] ?? array()); +check('no duplicated style handle', count($styles) === count(array_unique($styles)), + implode(', ', $styles)); +check('no duplicated script handle', count($scripts) === count(array_unique($scripts)), + implode(', ', $scripts)); + +section('10. Uninstall routine parses and targets real tables'); +$src = file_get_contents(dirname(__DIR__) . '/uninstall.php'); +check('uninstall references catalogue drop_tables', strpos($src, 'drop_tables()') !== false); +foreach (array('bd_book_meta', 'bd_pen_names', 'bd_reviews', 'bd_ratings', + 'bd_review_shortlinks', 'bd_subscribers', 'bd_newsletters') as $t) { + check("uninstall drops $t", strpos($src, $t) !== false); +} +check('uninstall clears all three cron hooks', + substr_count($src, 'wp_clear_scheduled_hook') === 3, + substr_count($src, 'wp_clear_scheduled_hook') . ' calls'); + +section('RESULT'); +if (!empty($GLOBALS['fail'])) { + echo "FAIL - {$GLOBALS['fail']} check(s) failed\n"; + exit(1); +} +echo "PASS - runtime smoke test clean\n"; diff --git a/umbra-bookshelf/tests/verify_structure.py b/umbra-bookshelf/tests/verify_structure.py new file mode 100644 index 0000000..805a2a0 --- /dev/null +++ b/umbra-bookshelf/tests/verify_structure.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +"""Static verification for the merged umbra-bookshelf plugin. + +Checks: + 1. PHP syntax of every file + 2. No class declared twice (fatal error in PHP) + 3. Every class referenced by new X / X:: actually exists + 4. Every require_once path resolves on disk + 5. Every wp_enqueue_* asset path resolves on disk (no 404s) + 6. No leftover pre-rename identifiers + 7. Preserved storage/API contracts still present +""" +import io +import os +import re +import subprocess +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +failures = [] +notes = [] + + +def rel(p): + return os.path.relpath(p, ROOT) + + +def walk(exts): + for dirpath, _, files in os.walk(ROOT): + for f in files: + if f.endswith(exts): + yield os.path.join(dirpath, f) + + +# ---------------------------------------------------------------- 1. php -l +php = [f for f in walk('.php')] +for f in php: + r = subprocess.run(['php', '-l', f], capture_output=True, text=True) + if 'No syntax errors' not in r.stdout: + failures.append('PHP SYNTAX: %s\n%s' % (rel(f), r.stdout + r.stderr)) +print('1. php -l: %d files checked' % len(php)) + +# ------------------------------------------------- 2. duplicate class decls +decls = {} +for f in php: + src = io.open(f, encoding='utf-8', errors='replace').read() + for m in re.finditer(r'^\s*(?:final\s+|abstract\s+)?class\s+([A-Za-z_][A-Za-z0-9_]*)', + src, re.M): + decls.setdefault(m.group(1), []).append(rel(f)) +dupes = {c: fs for c, fs in decls.items() if len(fs) > 1} +for c, fs in sorted(dupes.items()): + failures.append('DUPLICATE CLASS: %s declared in %s' % (c, ', '.join(fs))) +print('2. class declarations: %d unique, %d duplicated' + % (len(decls), len(dupes))) + +# --------------------------------------------- 3. referenced classes exist +refs = set() +for f in php: + src = io.open(f, encoding='utf-8', errors='replace').read() + for m in re.finditer(r'\bnew\s+([A-Z][A-Za-z0-9_]*)\s*\(', src): + refs.add(m.group(1)) + for m in re.finditer(r"\b([A-Z][A-Za-z0-9_]*)::", src): + refs.add(m.group(1)) + +# names defined by this plugin, or provided by WordPress +wp_provided = {'BD_BookShelfHelper', # created via class_alias() + 'WP_Error', 'WP_Query', 'WP_Post', 'WP_REST_Response', + 'WP_REST_Request', 'WP_REST_Server', 'WP_Term', 'wpdb', + 'ReflectionClass', 'DateTime', 'Exception', 'ArrayObject'} +known_external = {'GeminiClient', + # browser JS globals, matched by the `new X(` heuristic + # when scanning inline