Files
Umbra-Bookshelf/umbra-bookshelf/includes/books/class-umbra-admin.php
bdm 0bd78e587a Resolve the known issues; switch licence to GPL
Closes the three problems that blocked the consolidation and records the rest
as tasks.

Licence: GPLv2 or later (WordPress.org compatible)

  LICENSE is now the full GPLv2 text and NOTICE is gone (it was Apache
  specific). All 101 SPDX headers read GPL-2.0-or-later and every shipped
  header says "GPLv2 or later".

  This also fixes an inconsistency that predated the consolidation: the theme's
  style.css declared GPLv2 while its PHP files carried Apache headers.

  Old/ and archive/pre-umbra-releases/ keep their original Apache headers on
  purpose -- they are frozen snapshots, not shipped code.

Schema: the catalogue schema is canonical for bd_books and bd_series

  Adds Umbra_Schema_Migration. The catalogue and library modules previously
  both created those two tables with different columns, so whichever plugin
  activated first won and the other read and wrote against the wrong shape.

  The migration detects a library-shaped table, RENAMES it aside (never drops
  it), creates the canonical table and copies rows across:

    - book IDs preserved, keeping bd_book_meta, bd_reviews, bd_ratings and
      bd_review_shortlinks valid
    - title/slug/description resolved from the linked post
    - the library's single isbn/asin land on the ebook columns; amazon_asin is
      also set so existing product links keep working
    - series migrated first so books.series_id still resolves
    - post linkage rewritten as _bd_bookshelf_book_id (preserved meta key)
    - publish_date kept as _bd_bookshelf_publish_date (no catalogue column)
    - bd_ratings goodreads/amazon folded into rating_goodreads/rating_amazon
    - rows whose post is gone are skipped with a log line, not turned into
      unusable books
    - duplicate slugs de-duplicated, since catalogue slug is UNIQUE

  It runs before dbDelta, and on admin_init as well as activation, because
  plugin updates do not re-run activation hooks. Ordering matters: dbDelta adds
  columns but never drops them, so pointed at a library-shaped table it would
  graft on catalogue columns and leave a table satisfying neither schema.

  Umbra_Bookshelf_Helper_Install no longer creates bd_books or bd_series.

Scraping: Goodreads only, one implementation

  There were three Goodreads implementations and two Amazon ones; both library
  "scrapers" were empty stubs attached to cron events that fired on schedule to
  do nothing, and both real scraper classes were never called at all.

  Umbra_Amazon_Scraper is deleted and the duplicate HTML parsing is gone from
  Umbra_Rating_Sync, which now delegates to Umbra_Goodreads_Scraper. The two
  library stubs and their cron registrations are removed. One schedule
  survives: bd_bookshelf_sync_ratings, every 12 hours.

  The rating_amazon column is retained (it holds data and is populated during
  migration) and Amazon retail links are unaffected.

  A new rating fetch failure keeps the last known value instead of writing zero.
  The admin ratings notice no longer warns about Amazon scraping.

Umbra_SEO is now instantiated

  It registers its hooks from a constructor and the bootstrap never constructed
  it, so the class was dead code and no SEO output was produced. Its meta, Open
  Graph, Twitter card and sitemap hooks now attach. Completing SEO for generated
  book detail pages is docs/TASKS.md #2.

Assets that 404'd

  - assets/images/default-cover.png now holds a real 512x815 cover
    (WorkingTitleCover_512x815.png). The helper returned this path but the
    repository contained no images at all.
  - email/assets/js/admin.js is committed as an empty placeholder. It was
    enqueued on every email admin page but had never been committed.

Tests

  - tests/schema_migration_test.php: 42 checks against a real database, driven
    through the sqlite3 CLI on a throwaway file. Covers shape detection, row
    mapping, ID preservation, orphan handling, rating folding, idempotency and
    slug collisions.
  - tests/run_all.sh runs every suite.
  - The declared version is now checked against the plugin header rather than a
    literal, so a version bump cannot silently break the suite.

Docs

  - KNOWN-ISSUES.md rewritten: three resolved, three remaining.
  - TASKS.md added: email admin behaviour, book detail page SEO, batching the
    rating sync, reclaiming legacy tables, reconciling the two book models,
    retiring the Old/ archive.
  - SPEC and ARCHITECTURE updated for schema ownership, the migration ordering
    constraint, and the single scraper.

Version 1.1.0.
2026-10-22 09:47:00 -07:00

1372 lines
64 KiB
PHP

<?php
// Copyright 2026 Umbra Tools (umbratools.com)
// SPDX-License-Identifier: GPL-2.0-or-later
/**
* Admin Interface class
*
* @package Umbra_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
class Umbra_Admin {
/**
* Constructor
*/
public function __construct() {
add_action('admin_menu', [$this, 'add_admin_menu']);
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_scripts']);
// AJAX handlers
add_action('wp_ajax_bd_bookshelf_save_book', [$this, 'ajax_save_book']);
add_action('wp_ajax_bd_bookshelf_delete_book', [$this, 'ajax_delete_book']);
add_action('wp_ajax_bd_bookshelf_get_author', [$this, 'ajax_get_author']);
add_action('wp_ajax_bd_bookshelf_save_author', [$this, 'ajax_save_author']);
add_action('wp_ajax_bd_bookshelf_delete_author', [$this, 'ajax_delete_author']);
add_action('wp_ajax_bd_bookshelf_get_series', [$this, 'ajax_get_series']);
add_action('wp_ajax_bd_bookshelf_save_series', [$this, 'ajax_save_series']);
add_action('wp_ajax_bd_bookshelf_delete_series', [$this, 'ajax_delete_series']);
add_action('wp_ajax_bd_bookshelf_get_shortcode', [$this, 'ajax_get_shortcode']);
add_action('wp_ajax_bd_bookshelf_save_shortcode', [$this, 'ajax_save_shortcode']);
add_action('wp_ajax_bd_bookshelf_delete_shortcode', [$this, 'ajax_delete_shortcode']);
add_action('wp_ajax_bd_bookshelf_save_sites', [$this, 'ajax_save_sites']);
add_action('wp_ajax_bd_bookshelf_save_settings', [$this, 'ajax_save_settings']);
add_action('wp_ajax_bd_bookshelf_import_csv', [$this, 'ajax_import_csv']);
add_action('wp_ajax_bd_bookshelf_import_posts', [$this, 'ajax_import_posts']);
add_action('wp_ajax_bd_bookshelf_sync_ratings', [$this, 'ajax_sync_ratings']);
}
/**
* Add admin menu
*/
public function add_admin_menu() {
// 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, 'umbra-bookshelf') === false) {
return;
}
wp_enqueue_media();
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('umbra-bookshelf-admin', 'umbraBookshelfAdmin', [
'ajaxurl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('bd_bookshelf_admin_nonce'),
'mediaTitle' => __('Select Cover Image', 'umbra-bookshelf'),
'mediaButton' => __('Use This Image', 'umbra-bookshelf'),
]);
}
/**
* Render Books list page
*/
public function render_books_page() {
if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
}
$books = Umbra_Book_Manager::get_books(['number' => 50]);
?>
<div class="wrap">
<h1 class="wp-heading-inline"><?php esc_html_e('Books', 'umbra-bookshelf'); ?></h1>
<a href="<?php echo admin_url('admin.php?page=bd-bookshelf-add'); ?>" class="page-title-action"><?php esc_html_e('Add New Book', 'umbra-bookshelf'); ?></a>
<hr class="wp-header-end">
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th style="width:30%"><?php esc_html_e('Title', 'umbra-bookshelf'); ?></th>
<th style="width:15%"><?php esc_html_e('ISBNs', 'umbra-bookshelf'); ?></th>
<th style="width:15%"><?php esc_html_e('Ratings', 'umbra-bookshelf'); ?></th>
<th style="width:15%"><?php esc_html_e('Last Sync', 'umbra-bookshelf'); ?></th>
<th style="width:15%"><?php esc_html_e('Actions', 'umbra-bookshelf'); ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($books)) : ?>
<tr><td colspan="5"><?php esc_html_e('No books found. Click "Add New Book" to get started.', 'umbra-bookshelf'); ?></td></tr>
<?php else : ?>
<?php foreach ($books as $book) : ?>
<tr>
<td>
<a href="<?php echo admin_url('admin.php?page=bd-bookshelf-add&book_id=' . $book->id); ?>">
<strong><?php echo esc_html($book->title); ?></strong>
</a>
</td>
<td>
<?php
$isbns = [];
if (!empty($book->isbn_ebook)) $isbns[] = 'eBook';
if (!empty($book->isbn_paperback)) $isbns[] = 'PB';
if (!empty($book->isbn_hardcover)) $isbns[] = 'HC';
if (!empty($book->isbn_audio)) $isbns[] = 'Audio';
echo esc_html(implode(', ', $isbns));
?>
</td>
<td>
Amazon: <?php echo number_format($book->rating_amazon, 1); ?><br>
Goodreads: <?php echo number_format($book->rating_goodreads, 1); ?>
</td>
<td><?php echo $book->last_sync ? date('M j, Y g:i a', strtotime($book->last_sync)) : '-'; ?></td>
<td>
<a href="<?php echo admin_url('admin.php?page=bd-bookshelf-add&book_id=' . $book->id); ?>"><?php esc_html_e('Edit', 'umbra-bookshelf'); ?></a> |
<a href="#" class="bd-bookshelf-delete" data-id="<?php echo $book->id; ?>" style="color:#a00"><?php esc_html_e('Delete', 'umbra-bookshelf'); ?></a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php
}
/**
* Render Book form
*/
public function render_book_form() {
if (!current_user_can('manage_options')) {
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 ? 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 = 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 = Umbra_Retail_Sites::get_sites(true);
?>
<div class="wrap">
<h1><?php echo $book ? esc_html__('Edit Book', 'umbra-bookshelf') : esc_html__('Add New Book', 'umbra-bookshelf'); ?></h1>
<form id="bd-bookshelf-book-form">
<?php wp_nonce_field('bd_bookshelf_save_book', 'nonce'); ?>
<input type="hidden" name="book_id" value="<?php echo $book_id; ?>">
<h2><?php esc_html_e('Book Details', 'umbra-bookshelf'); ?></h2>
<table class="form-table">
<tr>
<th><label for="title"><?php esc_html_e('Title', 'umbra-bookshelf'); ?> <span class="required">*</span></label></th>
<td><input type="text" name="title" id="title" value="<?php echo $book ? esc_attr($book->title) : ''; ?>" class="regular-text" required></td>
</tr>
<tr>
<th><label for="description"><?php esc_html_e('Description', 'umbra-bookshelf'); ?></label></th>
<td><textarea name="description" id="description" rows="5" class="large-text"><?php echo $book ? esc_textarea($book->description) : ''; ?></textarea></td>
</tr>
<tr>
<th><label><?php esc_html_e('Cover Image', 'umbra-bookshelf'); ?></label></th>
<td>
<div class="bd-bookshelf-cover-upload">
<input type="hidden" name="cover_image" id="cover_image" value="<?php echo $book ? esc_url($book->cover_image) : ''; ?>">
<div id="cover-preview" style="margin-bottom:10px">
<?php if ($book && !empty($book->cover_image)) : ?>
<img src="<?php echo esc_url($book->cover_image); ?>" style="max-width:200px;height:auto">
<?php endif; ?>
</div>
<button type="button" id="cover-upload-btn" class="button"><?php esc_html_e('Select or Upload Cover', 'umbra-bookshelf'); ?></button>
<button type="button" id="cover-remove-btn" class="button" style="display:none;color:#a00"><?php esc_html_e('Remove', 'umbra-bookshelf'); ?></button>
</div>
</td>
</tr>
<tr>
<th><label for="series_id"><?php esc_html_e('Series', 'umbra-bookshelf'); ?></label></th>
<td>
<select name="series_id" id="series_id">
<option value="0"><?php esc_html_e('None', 'umbra-bookshelf'); ?></option>
<?php foreach ($series as $s) : ?>
<option value="<?php echo $s->id; ?>" <?php selected($book->series_id ?? 0, $s->id); ?>><?php echo esc_html($s->name); ?></option>
<?php endforeach; ?>
</select>
<a href="<?php echo admin_url('admin.php?page=bd-bookshelf-series'); ?>" target="_blank" class="button button-small"><?php esc_html_e('Manage Series', 'umbra-bookshelf'); ?></a>
</td>
</tr>
</table>
<h2><?php esc_html_e('Authors', 'umbra-bookshelf'); ?></h2>
<p class="description"><?php esc_html_e('Select up to 3 authors for this book.', 'umbra-bookshelf'); ?></p>
<div id="bd-bookshelf-authors-container">
<?php
$book_authors = [];
if ($book && !empty($book->authors)) {
foreach ($book->authors as $ba) {
$book_authors[] = $ba->author_id;
}
}
for ($i = 0; $i < 3; $i++) :
$selected = $book_authors[$i] ?? 0;
?>
<div class="bd-bookshelf-author-row">
<select name="authors[]" class="bd-bookshelf-author-select">
<option value="0"><?php esc_html_e('-- Select Author --', 'umbra-bookshelf'); ?></option>
<?php foreach ($authors as $a) : ?>
<option value="<?php echo $a->id; ?>" <?php selected($selected, $a->id); ?>><?php echo esc_html($a->display_name); ?></option>
<?php endforeach; ?>
</select>
</div>
<?php endfor; ?>
</div>
<a href="<?php echo admin_url('admin.php?page=bd-bookshelf-authors'); ?>" target="_blank" class="button button-small"><?php esc_html_e('Manage Authors', 'umbra-bookshelf'); ?></a>
<h2><?php esc_html_e('ISBNs & ASINs', 'umbra-bookshelf'); ?></h2>
<table class="wp-list-table widefat fixed striped bd-bookshelf-isbn-grid">
<thead>
<tr>
<th style="width:120px"><?php esc_html_e('Format', 'umbra-bookshelf'); ?></th>
<th><?php esc_html_e('ISBN', 'umbra-bookshelf'); ?></th>
<th><?php esc_html_e('Amazon ASIN', 'umbra-bookshelf'); ?></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong><?php esc_html_e('eBook', 'umbra-bookshelf'); ?></strong></td>
<td><input type="text" name="isbn_ebook" value="<?php echo $book ? esc_attr($book->isbn_ebook) : ''; ?>" class="regular-text"></td>
<td><input type="text" name="asin_ebook" value="<?php echo $book ? esc_attr($book->asin_ebook ?? '') : ''; ?>" class="regular-text"></td>
</tr>
<tr>
<td><strong><?php esc_html_e('Paperback', 'umbra-bookshelf'); ?></strong></td>
<td><input type="text" name="isbn_paperback" value="<?php echo $book ? esc_attr($book->isbn_paperback) : ''; ?>" class="regular-text"></td>
<td><input type="text" name="asin_paperback" value="<?php echo $book ? esc_attr($book->asin_paperback ?? '') : ''; ?>" class="regular-text"></td>
</tr>
<tr>
<td><strong><?php esc_html_e('Hardcover', 'umbra-bookshelf'); ?></strong></td>
<td><input type="text" name="isbn_hardcover" value="<?php echo $book ? esc_attr($book->isbn_hardcover) : ''; ?>" class="regular-text"></td>
<td><input type="text" name="asin_hardcover" value="<?php echo $book ? esc_attr($book->asin_hardcover ?? '') : ''; ?>" class="regular-text"></td>
</tr>
<tr>
<td><strong><?php esc_html_e('Audio', 'umbra-bookshelf'); ?></strong></td>
<td><input type="text" name="isbn_audio" value="<?php echo $book ? esc_attr($book->isbn_audio) : ''; ?>" class="regular-text"></td>
<td><input type="text" name="asin_audio" value="<?php echo $book ? esc_attr($book->asin_audio ?? '') : ''; ?>" class="regular-text"></td>
</tr>
</tbody>
</table>
<input type="hidden" name="goodreads_id" value="<?php echo $book ? esc_attr($book->goodreads_id) : ''; ?>">
<h2><?php esc_html_e('Retail Links', 'umbra-bookshelf'); ?></h2>
<p class="description"><?php esc_html_e('Paste the direct purchase URL for each format and retailer. Leave blank if not available.', 'umbra-bookshelf'); ?></p>
<?php
$formats_config = [
'ebook' => [
'label' => 'eBook',
'sites' => ['Amazon', 'Apple Books', 'Google Play', 'Barnes & Noble', 'Kobo'],
],
'paperback' => [
'label' => 'Paperback',
'sites' => ['Amazon', 'Barnes & Noble', 'Kobo'],
],
'hardcover' => [
'label' => 'Hardcover',
'sites' => ['Amazon', 'Barnes & Noble', 'Kobo'],
],
'audio' => [
'label' => 'Audio',
'sites' => ['Amazon', 'Apple Books', 'Barnes & Noble', 'Kobo'],
],
];
?>
<?php foreach ($formats_config as $format_key => $format_config) : ?>
<h3><?php echo esc_html($format_config['label']); ?></h3>
<table class="form-table">
<?php foreach ($format_config['sites'] as $site_name) : ?>
<tr>
<th><label><?php echo esc_html($site_name); ?></label></th>
<td>
<input type="url"
name="retail_url[<?php echo esc_attr($format_key); ?>][<?php echo esc_attr($site_name); ?>]"
value="<?php echo esc_url($existing_links[$format_key][$site_name] ?? ''); ?>"
class="regular-text"
placeholder="https://...">
</td>
</tr>
<?php endforeach; ?>
</table>
<?php endforeach; ?>
<p class="submit">
<button type="submit" class="button button-primary button-hero"><?php esc_html_e('Save Book', 'umbra-bookshelf'); ?></button>
</p>
</form>
</div>
<?php
}
/**
* Render Authors page
*/
public function render_authors_page() {
if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
}
$authors = Umbra_Authors::get_authors(['number' => 100]);
?>
<div class="wrap">
<h1 class="wp-heading-inline"><?php esc_html_e('Authors', 'umbra-bookshelf'); ?></h1>
<button type="button" id="bd-bookshelf-add-author" class="page-title-action"><?php esc_html_e('Add New Author', 'umbra-bookshelf'); ?></button>
<hr class="wp-header-end">
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th style="width:20%"><?php esc_html_e('Name', 'umbra-bookshelf'); ?></th>
<th style="width:20%"><?php esc_html_e('Email', 'umbra-bookshelf'); ?></th>
<th style="width:15%"><?php esc_html_e('Website', 'umbra-bookshelf'); ?></th>
<th style="width:15%"><?php esc_html_e('Goodreads ID', 'umbra-bookshelf'); ?></th>
<th style="width:15%"><?php esc_html_e('Actions', 'umbra-bookshelf'); ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($authors)) : ?>
<tr><td colspan="5"><?php esc_html_e('No authors found.', 'umbra-bookshelf'); ?></td></tr>
<?php else : ?>
<?php foreach ($authors as $author) : ?>
<tr>
<td><strong><?php echo esc_html($author->display_name); ?></strong></td>
<td><?php echo esc_html($author->email); ?></td>
<td><?php echo $author->website ? '<a href="' . esc_url($author->website) . '" target="_blank">' . esc_html($author->website) . '</a>' : '-'; ?></td>
<td><?php echo esc_html($author->goodreads_author_id); ?></td>
<td>
<a href="#" class="bd-bookshelf-edit-author" data-id="<?php echo $author->id; ?>"><?php esc_html_e('Edit', 'umbra-bookshelf'); ?></a> |
<a href="#" class="bd-bookshelf-delete-author" data-id="<?php echo $author->id; ?>" style="color:#a00"><?php esc_html_e('Delete', 'umbra-bookshelf'); ?></a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Author Modal -->
<div id="bd-bookshelf-author-modal" class="bd-bookshelf-modal" style="display:none">
<div class="bd-bookshelf-modal-content">
<div class="bd-bookshelf-modal-header">
<h2 id="bd-bookshelf-author-modal-title"><?php esc_html_e('Add New Author', 'umbra-bookshelf'); ?></h2>
<button type="button" class="bd-bookshelf-modal-close">&times;</button>
</div>
<form id="bd-bookshelf-author-form">
<?php wp_nonce_field('bd_bookshelf_admin_nonce', 'author_nonce'); ?>
<input type="hidden" name="author_id" id="author_id" value="0">
<table class="form-table">
<tr>
<th><label for="author_display_name"><?php esc_html_e('Display Name', 'umbra-bookshelf'); ?> <span class="required">*</span></label></th>
<td><input type="text" name="display_name" id="author_display_name" class="regular-text" required></td>
</tr>
<tr>
<th><label for="author_email"><?php esc_html_e('Email', 'umbra-bookshelf'); ?></label></th>
<td><input type="email" name="email" id="author_email" class="regular-text"></td>
</tr>
<tr>
<th><label for="author_website"><?php esc_html_e('Website', 'umbra-bookshelf'); ?></label></th>
<td><input type="url" name="website" id="author_website" class="regular-text"></td>
</tr>
<tr>
<th><label for="author_bio"><?php esc_html_e('Bio', 'umbra-bookshelf'); ?></label></th>
<td><textarea name="bio" id="author_bio" rows="4" class="large-text"></textarea></td>
</tr>
<tr>
<th><label for="author_goodreads_id"><?php esc_html_e('Goodreads Author ID', 'umbra-bookshelf'); ?></label></th>
<td><input type="text" name="goodreads_author_id" id="author_goodreads_id" class="regular-text"></td>
</tr>
<tr>
<th><label><?php esc_html_e('Photo', 'umbra-bookshelf'); ?></label></th>
<td>
<input type="hidden" name="photo_url" id="author_photo_url" value="">
<div id="author-photo-preview" style="margin-bottom:10px"></div>
<button type="button" id="author-photo-btn" class="button"><?php esc_html_e('Select Photo', 'umbra-bookshelf'); ?></button>
<button type="button" id="author-photo-remove" class="button" style="display:none;color:#a00"><?php esc_html_e('Remove', 'umbra-bookshelf'); ?></button>
</td>
</tr>
</table>
<p class="submit">
<button type="submit" class="button button-primary"><?php esc_html_e('Save Author', 'umbra-bookshelf'); ?></button>
</p>
</form>
</div>
</div>
<?php
}
/**
* Render Series page
*/
public function render_series_page() {
if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
}
$series = Umbra_Series::get_series(['number' => 100]);
?>
<div class="wrap">
<h1 class="wp-heading-inline"><?php esc_html_e('Series', 'umbra-bookshelf'); ?></h1>
<button type="button" id="bd-bookshelf-add-series" class="page-title-action"><?php esc_html_e('Add New Series', 'umbra-bookshelf'); ?></button>
<hr class="wp-header-end">
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th style="width:30%"><?php esc_html_e('Name', 'umbra-bookshelf'); ?></th>
<th style="width:50%"><?php esc_html_e('Description', 'umbra-bookshelf'); ?></th>
<th style="width:20%"><?php esc_html_e('Actions', 'umbra-bookshelf'); ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($series)) : ?>
<tr><td colspan="3"><?php esc_html_e('No series found.', 'umbra-bookshelf'); ?></td></tr>
<?php else : ?>
<?php foreach ($series as $s) : ?>
<tr>
<td><strong><?php echo esc_html($s->name); ?></strong></td>
<td><?php echo esc_html(wp_trim_words($s->description, 15)); ?></td>
<td>
<a href="#" class="bd-bookshelf-edit-series" data-id="<?php echo $s->id; ?>"><?php esc_html_e('Edit', 'umbra-bookshelf'); ?></a> |
<a href="#" class="bd-bookshelf-delete-series" data-id="<?php echo $s->id; ?>" style="color:#a00"><?php esc_html_e('Delete', 'umbra-bookshelf'); ?></a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Series Modal -->
<div id="bd-bookshelf-series-modal" class="bd-bookshelf-modal" style="display:none">
<div class="bd-bookshelf-modal-content">
<div class="bd-bookshelf-modal-header">
<h2 id="bd-bookshelf-series-modal-title"><?php esc_html_e('Add New Series', 'umbra-bookshelf'); ?></h2>
<button type="button" class="bd-bookshelf-modal-close">&times;</button>
</div>
<form id="bd-bookshelf-series-form">
<?php wp_nonce_field('bd_bookshelf_admin_nonce', 'series_nonce'); ?>
<input type="hidden" name="series_id" id="series_id_input" value="0">
<table class="form-table">
<tr>
<th><label for="series_name"><?php esc_html_e('Series Name', 'umbra-bookshelf'); ?> <span class="required">*</span></label></th>
<td><input type="text" name="name" id="series_name" class="regular-text" required></td>
</tr>
<tr>
<th><label for="series_description"><?php esc_html_e('Description', 'umbra-bookshelf'); ?></label></th>
<td><textarea name="description" id="series_description" rows="4" class="large-text"></textarea></td>
</tr>
</table>
<p class="submit">
<button type="submit" class="button button-primary"><?php esc_html_e('Save Series', 'umbra-bookshelf'); ?></button>
</p>
</form>
</div>
</div>
<?php
}
/**
* Render Sites page
*/
public function render_sites_page() {
if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
}
$sites = Umbra_Retail_Sites::get_sites(false);
?>
<div class="wrap">
<h1><?php esc_html_e('Retail Sites & Affiliate Codes', 'umbra-bookshelf'); ?></h1>
<p class="description"><?php esc_html_e('Enter your affiliate codes for each retail site. These codes will be appended to book links.', 'umbra-bookshelf'); ?></p>
<form id="bd-bookshelf-sites-form">
<?php wp_nonce_field('bd_bookshelf_admin_nonce', 'sites_nonce'); ?>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th style="width:25%"><?php esc_html_e('Site', 'umbra-bookshelf'); ?></th>
<th style="width:35%"><?php esc_html_e('URL Pattern', 'umbra-bookshelf'); ?></th>
<th style="width:15%"><?php esc_html_e('Affiliate Param', 'umbra-bookshelf'); ?></th>
<th style="width:25%"><?php esc_html_e('Your Affiliate Code', 'umbra-bookshelf'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($sites as $site) : ?>
<tr>
<td><strong><?php echo esc_html($site->name); ?></strong></td>
<td><code style="font-size:11px"><?php echo esc_html($site->url_pattern); ?></code></td>
<td><?php echo esc_html($site->affiliate_param); ?></td>
<td>
<input type="text" name="affiliate[<?php echo $site->id; ?>]" value="<?php echo esc_attr($site->affiliate_code); ?>" class="regular-text" placeholder="<?php esc_attr_e('Enter affiliate code', 'umbra-bookshelf'); ?>">
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p class="submit">
<button type="submit" class="button button-primary button-hero"><?php esc_html_e('Save Affiliate Codes', 'umbra-bookshelf'); ?></button>
</p>
</form>
</div>
<?php
}
/**
* Render Shortcodes page
*/
public function render_shortcodes_page() {
if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
}
global $wpdb;
$table = Umbra_Database::table('shortcodes');
$shortcodes = $wpdb->get_results("SELECT * FROM {$table} ORDER BY created_at DESC");
?>
<div class="wrap">
<h1 class="wp-heading-inline"><?php esc_html_e('Custom Shortcodes', 'umbra-bookshelf'); ?></h1>
<button type="button" id="bd-bookshelf-add-shortcode" class="page-title-action"><?php esc_html_e('Add New Shortcode', 'umbra-bookshelf'); ?></button>
<hr class="wp-header-end">
<p class="description"><?php esc_html_e('Create shortcodes that redirect to external URLs. Use [slug] in your posts to redirect visitors.', 'umbra-bookshelf'); ?></p>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th style="width:20%"><?php esc_html_e('Shortcode', 'umbra-bookshelf'); ?></th>
<th style="width:30%"><?php esc_html_e('Target URL', 'umbra-bookshelf'); ?></th>
<th style="width:15%"><?php esc_html_e('Redirect', 'umbra-bookshelf'); ?></th>
<th style="width:15%"><?php esc_html_e('Clicks', 'umbra-bookshelf'); ?></th>
<th style="width:20%"><?php esc_html_e('Actions', 'umbra-bookshelf'); ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($shortcodes)) : ?>
<tr><td colspan="5"><?php esc_html_e('No shortcodes found.', 'umbra-bookshelf'); ?></td></tr>
<?php else : ?>
<?php foreach ($shortcodes as $sc) : ?>
<tr>
<td><code>[<?php echo esc_html($sc->slug); ?>]</code></td>
<td><a href="<?php echo esc_url($sc->target_url); ?>" target="_blank"><?php echo esc_html($sc->target_url); ?></a></td>
<td><?php echo esc_html($sc->redirect_type); ?></td>
<td><?php echo number_format($sc->click_count); ?></td>
<td>
<a href="#" class="bd-bookshelf-edit-shortcode" data-id="<?php echo $sc->id; ?>"><?php esc_html_e('Edit', 'umbra-bookshelf'); ?></a> |
<a href="#" class="bd-bookshelf-delete-shortcode" data-id="<?php echo $sc->id; ?>" style="color:#a00"><?php esc_html_e('Delete', 'umbra-bookshelf'); ?></a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Shortcode Modal -->
<div id="bd-bookshelf-shortcode-modal" class="bd-bookshelf-modal" style="display:none">
<div class="bd-bookshelf-modal-content">
<div class="bd-bookshelf-modal-header">
<h2 id="bd-bookshelf-shortcode-modal-title"><?php esc_html_e('Add New Shortcode', 'umbra-bookshelf'); ?></h2>
<button type="button" class="bd-bookshelf-modal-close">&times;</button>
</div>
<form id="bd-bookshelf-shortcode-form">
<?php wp_nonce_field('bd_bookshelf_admin_nonce', 'shortcode_nonce'); ?>
<input type="hidden" name="shortcode_id" id="shortcode_id" value="0">
<table class="form-table">
<tr>
<th><label for="shortcode_slug"><?php esc_html_e('Shortcode Slug', 'umbra-bookshelf'); ?> <span class="required">*</span></label></th>
<td>
<input type="text" name="slug" id="shortcode_slug" class="regular-text" required pattern="[a-z0-9\-]+">
<p class="description"><?php esc_html_e('Only lowercase letters, numbers, and hyphens. Use as: [your-slug]', 'umbra-bookshelf'); ?></p>
</td>
</tr>
<tr>
<th><label for="shortcode_target"><?php esc_html_e('Target URL', 'umbra-bookshelf'); ?> <span class="required">*</span></label></th>
<td><input type="url" name="target_url" id="shortcode_target" class="regular-text" required></td>
</tr>
<tr>
<th><label for="shortcode_redirect"><?php esc_html_e('Redirect Type', 'umbra-bookshelf'); ?></label></th>
<td>
<select name="redirect_type" id="shortcode_redirect">
<option value="301">301 - Permanent</option>
<option value="302">302 - Temporary</option>
<option value="307">307 - Temporary</option>
</select>
</td>
</tr>
<tr>
<th><label for="shortcode_description"><?php esc_html_e('Description', 'umbra-bookshelf'); ?></label></th>
<td><input type="text" name="description" id="shortcode_description" class="regular-text"></td>
</tr>
</table>
<p class="submit">
<button type="submit" class="button button-primary"><?php esc_html_e('Save Shortcode', 'umbra-bookshelf'); ?></button>
</p>
</form>
</div>
</div>
<?php
}
/**
* Render Sync page
*/
public function render_sync_page() {
if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
}
$last_sync = Umbra_Rating_Sync::get_last_sync_info();
?>
<div class="wrap">
<h1><?php esc_html_e('Rating Sync', 'umbra-bookshelf'); ?></h1>
<div class="bd-bookshelf-import-section">
<h2><?php esc_html_e('Sync Status', 'umbra-bookshelf'); ?></h2>
<?php if (!empty($last_sync)) : ?>
<table class="form-table">
<tr>
<th><?php esc_html_e('Last Sync Time', 'umbra-bookshelf'); ?></th>
<td><?php echo esc_html($last_sync['time']); ?></td>
</tr>
<tr>
<th><?php esc_html_e('Books Synced', 'umbra-bookshelf'); ?></th>
<td><?php echo esc_html($last_sync['synced']); ?></td>
</tr>
<tr>
<th><?php esc_html_e('Books Failed', 'umbra-bookshelf'); ?></th>
<td><?php echo esc_html($last_sync['failed']); ?></td>
</tr>
<tr>
<th><?php esc_html_e('Total Books', 'umbra-bookshelf'); ?></th>
<td><?php echo esc_html($last_sync['total']); ?></td>
</tr>
</table>
<?php else : ?>
<p><?php esc_html_e('No sync has been performed yet.', 'umbra-bookshelf'); ?></p>
<?php endif; ?>
<p class="submit">
<button type="button" id="bd-bookshelf-manual-sync" class="button button-primary"><?php esc_html_e('Sync Now', 'umbra-bookshelf'); ?></button>
</p>
</div>
</div>
<?php
}
/**
* Render Settings page
*/
public function render_settings_page() {
if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
}
?>
<div class="wrap">
<h1><?php esc_html_e('Settings', 'umbra-bookshelf'); ?></h1>
<form id="bd-bookshelf-settings-form">
<?php wp_nonce_field('bd_bookshelf_save_settings', 'nonce'); ?>
<h2><?php esc_html_e('GDPR Settings', 'umbra-bookshelf'); ?></h2>
<table class="form-table">
<tr>
<th><label for="gdpr_text"><?php esc_html_e('Consent Banner Text', 'umbra-bookshelf'); ?></label></th>
<td>
<textarea name="gdpr_text" id="gdpr_text" rows="3" class="large-text"><?php echo esc_textarea(Umbra_GDPR::get_consent_text()); ?></textarea>
</td>
</tr>
</table>
<p class="submit">
<button type="submit" class="button button-primary"><?php esc_html_e('Save Settings', 'umbra-bookshelf'); ?></button>
</p>
</form>
</div>
<?php
}
/**
* Render Import page
*/
public function render_import_page() {
if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
}
?>
<div class="wrap">
<h1><?php esc_html_e('Import Books', 'umbra-bookshelf'); ?></h1>
<div class="bd-bookshelf-import-section">
<h2><?php esc_html_e('Import from CSV', 'umbra-bookshelf'); ?></h2>
<p><?php esc_html_e('Import books from a CSV file. Required column: title. Optional columns: description, isbn_ebook, isbn_paperback, isbn_hardcover, isbn_audio, amazon_asin, goodreads_id.', 'umbra-bookshelf'); ?></p>
<form id="bd-bookshelf-import-csv-form" enctype="multipart/form-data">
<?php wp_nonce_field('bd_bookshelf_admin_nonce', 'import_nonce'); ?>
<table class="form-table">
<tr>
<th><label for="csv_file"><?php esc_html_e('CSV File', 'umbra-bookshelf'); ?></label></th>
<td>
<input type="file" name="csv_file" id="csv_file" accept=".csv" required>
</td>
</tr>
</table>
<p class="submit">
<button type="submit" class="button button-primary"><?php esc_html_e('Import CSV', 'umbra-bookshelf'); ?></button>
</p>
</form>
<div id="bd-bookshelf-import-results" style="display:none">
<h3><?php esc_html_e('Import Results', 'umbra-bookshelf'); ?></h3>
<p id="bd-bookshelf-import-message"></p>
</div>
</div>
<div class="bd-bookshelf-import-section">
<h2><?php esc_html_e('Import from WordPress Posts', 'umbra-bookshelf'); ?></h2>
<p><?php esc_html_e('Import books from existing WordPress posts or custom post types.', 'umbra-bookshelf'); ?></p>
<form id="bd-bookshelf-import-posts-form">
<?php wp_nonce_field('bd_bookshelf_admin_nonce', 'import_posts_nonce'); ?>
<table class="form-table">
<tr>
<th><label for="post_type"><?php esc_html_e('Post Type', 'umbra-bookshelf'); ?></label></th>
<td>
<select name="post_type" id="post_type">
<option value="post"><?php esc_html_e('Posts', 'umbra-bookshelf'); ?></option>
<option value="page"><?php esc_html_e('Pages', 'umbra-bookshelf'); ?></option>
<option value="book"><?php esc_html_e('Books (if available)', 'umbra-bookshelf'); ?></option>
</select>
</td>
</tr>
<tr>
<th><label for="limit"><?php esc_html_e('Limit', 'umbra-bookshelf'); ?></label></th>
<td>
<input type="number" name="limit" id="limit" value="100" min="1" max="1000">
</td>
</tr>
</table>
<p class="submit">
<button type="submit" class="button button-primary"><?php esc_html_e('Import Posts', 'umbra-bookshelf'); ?></button>
</p>
</form>
</div>
</div>
<?php
}
/**
* AJAX handler for saving book
*/
public function ajax_save_book() {
check_ajax_referer('bd_bookshelf_save_book', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Insufficient permissions']);
}
$book_id = absint($_POST['book_id'] ?? 0);
$data = [
'title' => sanitize_text_field($_POST['title'] ?? ''),
'description' => wp_kses_post($_POST['description'] ?? ''),
'cover_image' => esc_url_raw($_POST['cover_image'] ?? ''),
'isbn_ebook' => sanitize_text_field($_POST['isbn_ebook'] ?? ''),
'isbn_paperback' => sanitize_text_field($_POST['isbn_paperback'] ?? ''),
'isbn_hardcover' => sanitize_text_field($_POST['isbn_hardcover'] ?? ''),
'isbn_audio' => sanitize_text_field($_POST['isbn_audio'] ?? ''),
'asin_ebook' => sanitize_text_field($_POST['asin_ebook'] ?? ''),
'asin_paperback' => sanitize_text_field($_POST['asin_paperback'] ?? ''),
'asin_hardcover' => sanitize_text_field($_POST['asin_hardcover'] ?? ''),
'asin_audio' => sanitize_text_field($_POST['asin_audio'] ?? ''),
'amazon_asin' => sanitize_text_field($_POST['asin_ebook'] ?? ''),
'goodreads_id' => sanitize_text_field($_POST['goodreads_id'] ?? ''),
'series_id' => absint($_POST['series_id'] ?? 0),
'author_ids' => array_map('absint', $_POST['authors'] ?? []),
];
if ($book_id) {
$result = Umbra_Book_Manager::update_book($book_id, $data);
} else {
$data['slug'] = sanitize_title($data['title']);
$book_id = Umbra_Book_Manager::create_book($data);
$result = $book_id !== false;
}
if ($result) {
// Save retail URLs
$retail_urls = $_POST['retail_url'] ?? [];
$this->save_retail_urls($book_id, $retail_urls);
wp_send_json_success(['message' => 'Book saved', 'id' => $book_id]);
} else {
wp_send_json_error(['message' => 'Failed to save book']);
}
}
/**
* Save retail URLs for a book
*/
private function save_retail_urls($book_id, $retail_urls) {
global $wpdb;
$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']);
if (empty($retail_urls) || !is_array($retail_urls)) {
return;
}
foreach ($retail_urls as $format => $sites) {
if (!is_array($sites)) continue;
foreach ($sites as $site_name => $url) {
$url = esc_url_raw(trim($url));
if (empty($url)) continue;
// Find or create site
$site = $wpdb->get_row($wpdb->prepare(
"SELECT id FROM {$sites_table} WHERE name = %s",
$site_name
));
if (!$site) {
$wpdb->insert($sites_table, [
'name' => $site_name,
'url_pattern' => '',
'is_custom' => 1,
'is_active' => 1,
]);
$site_id = $wpdb->insert_id;
} else {
$site_id = $site->id;
}
$wpdb->insert($links_table, [
'book_id' => $book_id,
'site_id' => $site_id,
'format' => $format,
'url' => $url,
'is_custom' => 0,
]);
}
}
}
/**
* AJAX handler for deleting book
*/
public function ajax_delete_book() {
check_ajax_referer('bd_bookshelf_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Insufficient permissions']);
}
$book_id = absint($_POST['id'] ?? 0);
if (!$book_id) {
wp_send_json_error(['message' => 'Invalid book ID']);
}
$result = Umbra_Book_Manager::delete_book($book_id);
if ($result) {
wp_send_json_success(['message' => 'Book deleted']);
} else {
wp_send_json_error(['message' => 'Failed to delete book']);
}
}
/**
* AJAX handler for getting author
*/
public function ajax_get_author() {
check_ajax_referer('bd_bookshelf_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Insufficient permissions']);
}
$author_id = absint($_POST['id'] ?? 0);
if (!$author_id) {
wp_send_json_error(['message' => 'Invalid author ID']);
}
$author = Umbra_Authors::get_author($author_id);
if (!$author) {
wp_send_json_error(['message' => 'Author not found']);
}
wp_send_json_success($author);
}
/**
* AJAX handler for saving author
*/
public function ajax_save_author() {
check_ajax_referer('bd_bookshelf_admin_nonce', 'author_nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Insufficient permissions']);
}
$author_id = absint($_POST['author_id'] ?? 0);
$data = [
'display_name' => sanitize_text_field($_POST['display_name'] ?? ''),
'email' => sanitize_email($_POST['email'] ?? ''),
'website' => esc_url_raw($_POST['website'] ?? ''),
'bio' => wp_kses_post($_POST['bio'] ?? ''),
'goodreads_author_id' => sanitize_text_field($_POST['goodreads_author_id'] ?? ''),
'photo_url' => esc_url_raw($_POST['photo_url'] ?? ''),
];
if ($author_id) {
$result = Umbra_Authors::update_author($author_id, $data);
} else {
$author_id = Umbra_Authors::create_author($data);
$result = $author_id !== false;
}
if ($result) {
wp_send_json_success(['message' => 'Author saved', 'id' => $author_id]);
} else {
wp_send_json_error(['message' => 'Failed to save author']);
}
}
/**
* AJAX handler for deleting author
*/
public function ajax_delete_author() {
check_ajax_referer('bd_bookshelf_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Insufficient permissions']);
}
$author_id = absint($_POST['id'] ?? 0);
if (!$author_id) {
wp_send_json_error(['message' => 'Invalid author ID']);
}
$result = Umbra_Authors::delete_author($author_id);
if ($result) {
wp_send_json_success(['message' => 'Author deleted']);
} else {
wp_send_json_error(['message' => 'Failed to delete author']);
}
}
/**
* AJAX handler for getting series
*/
public function ajax_get_series() {
check_ajax_referer('bd_bookshelf_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Insufficient permissions']);
}
$series_id = absint($_POST['id'] ?? 0);
if (!$series_id) {
wp_send_json_error(['message' => 'Invalid series ID']);
}
$series = Umbra_Series::get_series_by_id($series_id);
if (!$series) {
wp_send_json_error(['message' => 'Series not found']);
}
wp_send_json_success($series);
}
/**
* AJAX handler for saving series
*/
public function ajax_save_series() {
check_ajax_referer('bd_bookshelf_admin_nonce', 'series_nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Insufficient permissions']);
}
$series_id = absint($_POST['series_id'] ?? 0);
$data = [
'name' => sanitize_text_field($_POST['name'] ?? ''),
'description' => wp_kses_post($_POST['description'] ?? ''),
];
if ($series_id) {
$result = Umbra_Series::update_series($series_id, $data);
} else {
$series_id = Umbra_Series::create_series($data);
$result = $series_id !== false;
}
if ($result) {
wp_send_json_success(['message' => 'Series saved', 'id' => $series_id]);
} else {
wp_send_json_error(['message' => 'Failed to save series']);
}
}
/**
* AJAX handler for deleting series
*/
public function ajax_delete_series() {
check_ajax_referer('bd_bookshelf_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Insufficient permissions']);
}
$series_id = absint($_POST['id'] ?? 0);
if (!$series_id) {
wp_send_json_error(['message' => 'Invalid series ID']);
}
$result = Umbra_Series::delete_series($series_id);
if ($result) {
wp_send_json_success(['message' => 'Series deleted']);
} else {
wp_send_json_error(['message' => 'Failed to delete series']);
}
}
/**
* AJAX handler for getting shortcode
*/
public function ajax_get_shortcode() {
check_ajax_referer('bd_bookshelf_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Insufficient permissions']);
}
$shortcode_id = absint($_POST['id'] ?? 0);
if (!$shortcode_id) {
wp_send_json_error(['message' => 'Invalid shortcode ID']);
}
global $wpdb;
$table = Umbra_Database::table('shortcodes');
$shortcode = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$table} WHERE id = %d",
$shortcode_id
)
);
if (!$shortcode) {
wp_send_json_error(['message' => 'Shortcode not found']);
}
wp_send_json_success($shortcode);
}
/**
* AJAX handler for saving shortcode
*/
public function ajax_save_shortcode() {
check_ajax_referer('bd_bookshelf_admin_nonce', 'shortcode_nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Insufficient permissions']);
}
$shortcode_id = absint($_POST['shortcode_id'] ?? 0);
$data = [
'slug' => sanitize_title($_POST['slug'] ?? ''),
'target_url' => esc_url_raw($_POST['target_url'] ?? ''),
'redirect_type' => sanitize_text_field($_POST['redirect_type'] ?? '301'),
'description' => sanitize_text_field($_POST['description'] ?? ''),
];
if (empty($data['slug']) || empty($data['target_url'])) {
wp_send_json_error(['message' => 'Slug and Target URL are required']);
}
if ($shortcode_id) {
$result = Umbra_Shortcodes::update_shortcode($shortcode_id, $data);
} else {
$shortcode_id = Umbra_Shortcodes::create_shortcode($data['slug'], $data['target_url'], $data['redirect_type'], $data['description']);
$result = $shortcode_id !== false;
}
if ($result) {
wp_send_json_success(['message' => 'Shortcode saved', 'id' => $shortcode_id]);
} else {
wp_send_json_error(['message' => 'Failed to save shortcode']);
}
}
/**
* AJAX handler for deleting shortcode
*/
public function ajax_delete_shortcode() {
check_ajax_referer('bd_bookshelf_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Insufficient permissions']);
}
$shortcode_id = absint($_POST['id'] ?? 0);
if (!$shortcode_id) {
wp_send_json_error(['message' => 'Invalid shortcode ID']);
}
$result = Umbra_Shortcodes::delete_shortcode($shortcode_id);
if ($result) {
wp_send_json_success(['message' => 'Shortcode deleted']);
} else {
wp_send_json_error(['message' => 'Failed to delete shortcode']);
}
}
/**
* AJAX handler for saving sites/affiliate codes
*/
public function ajax_save_sites() {
check_ajax_referer('bd_bookshelf_admin_nonce', 'sites_nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Insufficient permissions']);
}
if (!empty($_POST['affiliate'])) {
global $wpdb;
$table = Umbra_Database::table('retail_sites');
foreach ($_POST['affiliate'] as $site_id => $code) {
$wpdb->update(
$table,
['affiliate_code' => sanitize_text_field($code)],
['id' => absint($site_id)],
['%s'],
['%d']
);
}
}
wp_send_json_success(['message' => 'Affiliate codes saved']);
}
/**
* AJAX handler for saving settings
*/
public function ajax_save_settings() {
check_ajax_referer('bd_bookshelf_save_settings', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Insufficient permissions']);
}
Umbra_GDPR::update_consent_text($_POST['gdpr_text'] ?? '');
wp_send_json_success(['message' => 'Settings saved']);
}
/**
* AJAX handler for CSV import
*/
public function ajax_import_csv() {
check_ajax_referer('bd_bookshelf_admin_nonce', 'import_nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Insufficient permissions']);
}
if (empty($_FILES['csv_file']['tmp_name'])) {
wp_send_json_error(['message' => 'No file uploaded']);
}
$file = $_FILES['csv_file']['tmp_name'];
$handle = fopen($file, 'r');
if (!$handle) {
wp_send_json_error(['message' => 'Unable to open file']);
}
// Get header row
$header = fgetcsv($handle);
$imported = 0;
$updated = 0;
$skipped = 0;
$errors = [];
while (($row = fgetcsv($handle)) !== false) {
$data = array_combine($header, $row);
if (empty($data['title'])) {
$errors[] = 'Row ' . ($imported + $skipped + 1) . ': Missing title';
continue;
}
// Check if already imported - update instead of skip
$existing = Umbra_Book_Manager::get_book_by_title($data['title']);
$book_data = [
'title' => $data['title'],
'slug' => sanitize_title($data['title']),
'description' => $data['description'] ?? '',
'isbn_ebook' => $data['isbn_ebook'] ?? '',
'isbn_paperback' => $data['isbn_paperback'] ?? '',
'isbn_hardcover' => $data['isbn_hardcover'] ?? '',
'isbn_audio' => $data['isbn_audio'] ?? '',
'amazon_asin' => $data['amazon_asin'] ?? '',
'goodreads_id' => $data['goodreads_id'] ?? '',
];
if ($existing) {
// Update existing book - fill in empty fields
$update_data = [];
foreach ($book_data as $key => $value) {
if ($key === 'title' || $key === 'slug') continue;
if (!empty($value) && empty($existing->$key)) {
$update_data[$key] = $value;
}
}
if (!empty($update_data)) {
Umbra_Book_Manager::update_book($existing->id, $update_data);
$updated++;
} else {
$skipped++;
}
} else {
// Create new book
$book_id = Umbra_Book_Manager::create_book($book_data);
if ($book_id) {
$imported++;
} else {
$errors[] = 'Row ' . ($imported + $skipped + 1) . ': Failed to create book';
}
}
}
fclose($handle);
$message = sprintf('Imported %d new books, updated %d existing, skipped %d', $imported, $updated, $skipped);
if (!empty($errors)) {
$message .= '. ' . count($errors) . ' errors.';
}
wp_send_json_success([
'message' => $message,
'imported' => $imported,
'updated' => $updated,
'skipped' => $skipped,
'errors' => $errors,
]);
}
/**
* AJAX handler for posts import
*/
public function ajax_import_posts() {
check_ajax_referer('bd_bookshelf_admin_nonce', 'import_posts_nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Insufficient permissions']);
}
$post_type = sanitize_text_field($_POST['post_type'] ?? 'post');
$limit = absint($_POST['limit'] ?? 100);
$posts = get_posts([
'post_type' => $post_type,
'post_status' => 'publish',
'numberposts' => $limit,
]);
$imported = 0;
$skipped = 0;
foreach ($posts as $post) {
// Check if already imported
$existing = Umbra_Book_Manager::get_book_by_title($post->post_title);
if ($existing) {
$skipped++;
continue;
}
// Import book
$book_id = Umbra_Book_Manager::create_book([
'title' => $post->post_title,
'description' => $post->post_content,
]);
if ($book_id) {
// Link to WordPress post
update_post_meta($post->ID, '_bd_bookshelf_book_id', $book_id);
$imported++;
}
}
wp_send_json_success([
'message' => sprintf('Imported %d books, skipped %d', $imported, $skipped),
'imported' => $imported,
'skipped' => $skipped,
]);
}
/**
* AJAX handler for manual sync
*/
public function ajax_sync_ratings() {
check_ajax_referer('bd_bookshelf_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Insufficient permissions']);
}
$result = Umbra_Rating_Sync::manual_sync();
wp_send_json_success([
'message' => sprintf('Synced %d books, failed %d', $result['synced'], $result['failed']),
'result' => $result,
]);
}
}