Initial commit: AutoBlogger v0.1.0
WordPress plugin that auto-researches topics from Reddit, generates SEO-optimized articles via OpenRouter LLMs, runs them through a chief editor agent, and publishes on a daily schedule. Includes: - Full generation pipeline (collect → analyze → score → generate → review → publish) - Cost tracking and budget enforcement - Admin UI: settings, review queue, metrics, activity log - CI/CD pipeline (GitHub Actions → WordPress REST deploy) - Unit test suite (PHPUnit + Brain Monkey) - Structured logging with configurable verbosity Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
207
includes/admin/class-admin-page.php
Executable file
207
includes/admin/class-admin-page.php
Executable file
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Registers and renders the main AutoBlogger settings page in wp-admin.
|
||||
*
|
||||
* Uses the WordPress Settings API for option registration, sanitization, and rendering.
|
||||
* Settings are defined as a declarative array — adding a new setting is one array entry.
|
||||
*
|
||||
* Triggered by: Autoblogger::register_admin_hooks() registers menu and settings hooks.
|
||||
* Dependencies: Autoblogger_Encryption (for API key storage), Autoblogger_Settings_Fields.
|
||||
*
|
||||
* @see class-autoblogger.php — Registers hooks that call this class.
|
||||
* @see class-settings-fields.php — Defines all settings fields and sections.
|
||||
* @see CONVENTIONS.md — "How To: Add a New Admin Setting".
|
||||
*/
|
||||
class Autoblogger_Admin_Page {
|
||||
|
||||
private const PAGE_SLUG = 'autoblogger-settings';
|
||||
private const OPTION_GROUP = 'autoblogger_settings_group';
|
||||
|
||||
/** Register the top-level AutoBlogger menu item. */
|
||||
public function on_register_menu(): void {
|
||||
add_menu_page(
|
||||
__( 'AutoBlogger', 'autoblogger' ),
|
||||
__( 'AutoBlogger', 'autoblogger' ),
|
||||
'manage_options',
|
||||
self::PAGE_SLUG,
|
||||
[ $this, 'render_page' ],
|
||||
'dashicons-edit-page',
|
||||
30
|
||||
);
|
||||
}
|
||||
|
||||
/** Register all settings with the WordPress Settings API. */
|
||||
public function on_register_settings(): void {
|
||||
$sections = Autoblogger_Settings_Fields::get_sections();
|
||||
$fields = Autoblogger_Settings_Fields::get_fields();
|
||||
|
||||
foreach ( $sections as $section_id => $section ) {
|
||||
add_settings_section( $section_id, $section['title'], '__return_empty_string', self::PAGE_SLUG );
|
||||
}
|
||||
|
||||
foreach ( $fields as $field ) {
|
||||
$section = $field['section'] ?? 'autoblogger_api';
|
||||
register_setting( self::OPTION_GROUP, $field['id'], [
|
||||
'type' => $field['wp_type'] ?? 'string',
|
||||
'sanitize_callback' => [ $this, 'sanitize_field' ],
|
||||
'default' => $field['default'] ?? '',
|
||||
] );
|
||||
add_settings_field( $field['id'], $field['label'], [ $this, 'render_field' ], self::PAGE_SLUG, $section, $field );
|
||||
}
|
||||
}
|
||||
|
||||
/** Enqueue admin CSS and JS on all AutoBlogger admin pages. */
|
||||
public function on_enqueue_assets( string $hook_suffix ): void {
|
||||
$pages = [
|
||||
'toplevel_page_' . self::PAGE_SLUG,
|
||||
'autoblogger_page_autoblogger-metrics',
|
||||
'autoblogger_page_autoblogger-review-queue',
|
||||
'autoblogger_page_autoblogger-logs',
|
||||
];
|
||||
if ( ! in_array( $hook_suffix, $pages, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style( 'autoblogger-admin', AUTOBLOGGER_PLUGIN_URL . 'assets/css/admin.css', [], AUTOBLOGGER_VERSION );
|
||||
wp_enqueue_script( 'autoblogger-admin', AUTOBLOGGER_PLUGIN_URL . 'assets/js/admin.js', [ 'jquery' ], AUTOBLOGGER_VERSION, true );
|
||||
|
||||
wp_localize_script( 'autoblogger-admin', 'autobloggerAdmin', [
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'generateNonce' => wp_create_nonce( 'autoblogger_generate_now' ),
|
||||
'testNonce' => wp_create_nonce( 'autoblogger_test_connection' ),
|
||||
'reviewNonce' => wp_create_nonce( 'autoblogger_review_queue' ),
|
||||
'generatingText' => __( 'Generating...', 'autoblogger' ),
|
||||
'generateText' => __( 'Generate Now', 'autoblogger' ),
|
||||
'testingText' => __( 'Testing...', 'autoblogger' ),
|
||||
'testText' => __( 'Test Connections', 'autoblogger' ),
|
||||
] );
|
||||
}
|
||||
|
||||
/** Render the settings page (delegates to template). */
|
||||
public function render_page(): void {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
$sections = Autoblogger_Settings_Fields::get_sections();
|
||||
$fields = Autoblogger_Settings_Fields::get_fields();
|
||||
include AUTOBLOGGER_PLUGIN_DIR . 'templates/admin/settings-page.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single settings field based on its type.
|
||||
*
|
||||
* @param array<string, mixed> $args Field definition.
|
||||
*/
|
||||
public function render_field( array $args ): void {
|
||||
$id = esc_attr( $args['id'] );
|
||||
$type = $args['type'] ?? 'text';
|
||||
$default = $args['default'] ?? '';
|
||||
$desc = $args['description'] ?? '';
|
||||
$value = get_option( $args['id'], $default );
|
||||
|
||||
if ( 'password' === $type && '' !== $value ) {
|
||||
$decrypted = Autoblogger_Encryption::decrypt( $value );
|
||||
$value = '' !== $decrypted ? '••••••••' : '';
|
||||
}
|
||||
|
||||
switch ( $type ) {
|
||||
case 'text':
|
||||
case 'number':
|
||||
case 'url':
|
||||
case 'time':
|
||||
$attrs = '';
|
||||
if ( isset( $args['min'] ) ) { $attrs .= ' min="' . esc_attr( (string) $args['min'] ) . '"'; }
|
||||
if ( isset( $args['max'] ) ) { $attrs .= ' max="' . esc_attr( (string) $args['max'] ) . '"'; }
|
||||
if ( isset( $args['step'] ) ) { $attrs .= ' step="' . esc_attr( (string) $args['step'] ) . '"'; }
|
||||
printf( '<input type="%s" id="%s" name="%s" value="%s" class="ab-input" %s />', esc_attr( $type ), $id, $id, esc_attr( (string) $value ), $attrs );
|
||||
break;
|
||||
|
||||
case 'password':
|
||||
printf( '<input type="password" id="%s" name="%s" value="" class="ab-input" placeholder="%s" autocomplete="off" />',
|
||||
$id, $id, '' !== $value ? esc_attr__( 'Saved (enter new value to change)', 'autoblogger' ) : '' );
|
||||
break;
|
||||
|
||||
case 'textarea':
|
||||
printf( '<textarea id="%s" name="%s" rows="3" class="ab-textarea">%s</textarea>', $id, $id, esc_textarea( (string) $value ) );
|
||||
break;
|
||||
|
||||
case 'select':
|
||||
printf( '<select id="%s" name="%s" class="ab-select">', $id, $id );
|
||||
foreach ( ( $args['options'] ?? [] ) as $v => $label ) {
|
||||
printf( '<option value="%s"%s>%s</option>', esc_attr( (string) $v ), selected( $value, (string) $v, false ), esc_html( $label ) );
|
||||
}
|
||||
echo '</select>';
|
||||
break;
|
||||
|
||||
case 'toggle':
|
||||
$checked = in_array( $value, [ '1', 'yes', true ], true );
|
||||
printf( '<label class="ab-toggle"><input type="hidden" name="%s" value="0" /><input type="checkbox" name="%s" value="1" %s /><span class="ab-toggle-slider"></span></label>',
|
||||
$id, $id, checked( $checked, true, false ) );
|
||||
break;
|
||||
|
||||
case 'checkboxes':
|
||||
$current = json_decode( (string) $value, true ) ?: [];
|
||||
foreach ( ( $args['options'] ?? [] ) as $v => $label ) {
|
||||
$is_checked = in_array( $v, $current, true );
|
||||
$disabled = strpos( $label, 'coming soon' ) !== false ? ' disabled' : '';
|
||||
printf( '<label class="ab-checkbox-label"><input type="checkbox" name="%s[]" value="%s" %s%s /> %s</label>',
|
||||
$id, esc_attr( $v ), checked( $is_checked, true, false ), $disabled, esc_html( $label ) );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'author_select':
|
||||
wp_dropdown_users( [ 'name' => $id, 'id' => $id, 'selected' => absint( $value ), 'show_option_none' => __( '— Auto (first admin) —', 'autoblogger' ), 'option_none_value' => '0', 'class' => 'ab-select' ] );
|
||||
break;
|
||||
|
||||
case 'category_select':
|
||||
wp_dropdown_categories( [ 'name' => $id, 'id' => $id, 'selected' => absint( $value ), 'show_option_none' => __( '— Auto-assign by type —', 'autoblogger' ), 'option_none_value' => '0', 'class' => 'ab-select', 'hide_empty' => false ] );
|
||||
break;
|
||||
}
|
||||
|
||||
if ( isset( $args['badge'] ) ) {
|
||||
printf( ' <span class="ab-badge">%s</span>', esc_html( $args['badge'] ) );
|
||||
}
|
||||
if ( '' !== $desc ) {
|
||||
printf( '<p class="ab-field-desc">%s</p>', esc_html( $desc ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a settings field value.
|
||||
*
|
||||
* @param mixed $value The submitted value.
|
||||
* @return mixed Sanitized value.
|
||||
*/
|
||||
public function sanitize_field( $value ) {
|
||||
$option_name = '';
|
||||
$filter = current_filter();
|
||||
if ( 0 === strpos( $filter, 'sanitize_option_' ) ) {
|
||||
$option_name = substr( $filter, strlen( 'sanitize_option_' ) );
|
||||
}
|
||||
|
||||
$encrypted = [ 'autoblogger_openrouter_api_key', 'autoblogger_reddit_client_secret', 'autoblogger_ga4_credentials_json' ];
|
||||
if ( in_array( $option_name, $encrypted, true ) ) {
|
||||
if ( '' === $value ) { return get_option( $option_name, '' ); }
|
||||
return Autoblogger_Encryption::encrypt( sanitize_text_field( $value ) );
|
||||
}
|
||||
|
||||
$json_fields = [ 'autoblogger_target_subreddits', 'autoblogger_topic_exclusions', 'autoblogger_enabled_sources' ];
|
||||
if ( in_array( $option_name, $json_fields, true ) ) {
|
||||
if ( is_array( $value ) ) { return wp_json_encode( array_values( array_map( 'sanitize_text_field', $value ) ) ); }
|
||||
if ( is_string( $value ) && '[' !== substr( trim( $value ), 0, 1 ) ) {
|
||||
$items = array_filter( array_map( 'trim', explode( ',', $value ) ) );
|
||||
return wp_json_encode( array_values( $items ) );
|
||||
}
|
||||
return sanitize_text_field( $value );
|
||||
}
|
||||
|
||||
$numeric = [ 'autoblogger_daily_article_target', 'autoblogger_monthly_budget_usd', 'autoblogger_min_word_count', 'autoblogger_max_word_count', 'autoblogger_default_author', 'autoblogger_default_category' ];
|
||||
if ( in_array( $option_name, $numeric, true ) ) {
|
||||
return is_numeric( $value ) ? $value : 0;
|
||||
}
|
||||
|
||||
return sanitize_text_field( (string) $value );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user