Plugin Directory

Changeset 3458138


Ignore:
Timestamp:
02/10/2026 02:55:24 PM (6 weeks ago)
Author:
rifaldye
Message:

Version 1.3.1: Auto Google Index - automatically submit URLs to Google Indexing API on publish/update, submission log with pagination, encrypted service account storage, test connection feature, and setup guide with tutorial link

Location:
apicoid-ghostwriter
Files:
6 added
6 edited
1 copied

Legend:

Unmodified
Added
Removed
  • apicoid-ghostwriter/tags/1.3.1/apicoid-ghostwriter.php

    r3457596 r3458138  
    44 * Plugin URI:  https://wordpress.org/plugins/apicoid-ghostwriter/
    55 * Description: Connects your WordPress site to Api.co.id to generate content and rewrite content automatically using AI. Features include article generation, content rewriting, automatic related article linking, SEO integration, and image generation.
    6  * Version:     1.3.0
     6 * Version:     1.3.1
    77 * Author:      Api.co.id
    88 * Author URI:  https://api.co.id
     
    2121
    2222// Define plugin constants
    23 define( 'APICOID_GW_VERSION', '1.3.0' );
     23define( 'APICOID_GW_VERSION', '1.3.1' );
    2424define( 'APICOID_GW_DIR', plugin_dir_path( __FILE__ ) );
    2525define( 'APICOID_GW_URL', plugin_dir_url( __FILE__ ) );
     
    110110       
    111111        add_action( 'wp_ajax_apicoid_gw_generate_article_by_category', array( $this, 'ajax_generate_article_by_category' ) );
     112
     113        // Handle Google Index settings save
     114        add_action( 'wp_ajax_apicoid_gw_save_google_index_settings', array( $this, 'ajax_save_google_index_settings' ) );
     115
     116        // Handle Google Index test connection
     117        add_action( 'wp_ajax_apicoid_gw_test_google_index', array( $this, 'ajax_test_google_index' ) );
     118
     119        // Handle Google Index log fetch
     120        add_action( 'wp_ajax_apicoid_gw_get_google_index_logs', array( $this, 'ajax_get_google_index_logs' ) );
     121
     122        // Handle Google Index log clear
     123        add_action( 'wp_ajax_apicoid_gw_clear_google_index_logs', array( $this, 'ajax_clear_google_index_logs' ) );
     124
     125        // Auto-submit URL to Google on post publish/update
     126        add_action( 'transition_post_status', array( $this, 'on_post_publish_update' ), 10, 3 );
    112127       
    113128        // Enqueue admin scripts and styles
     
    127142        // Create database table
    128143        $this->create_database_table();
     144
     145        // Create Google Index log table
     146        $this->create_google_index_log_table();
    129147
    130148        // Set default article generator settings
     
    199217        return $wpdb->prefix . 'apicoid_gw_posts';
    200218    }
    201    
     219
     220    /**
     221     * Get the Google Index log table name
     222     *
     223     * @return string Table name with prefix
     224     */
     225    public function get_google_index_log_table_name() {
     226        global $wpdb;
     227        return $wpdb->prefix . 'apicoid_gw_google_index_log';
     228    }
     229
     230    /**
     231     * Create database table for Google Index logs
     232     */
     233    private function create_google_index_log_table() {
     234        global $wpdb;
     235
     236        $table_name      = $this->get_google_index_log_table_name();
     237        $charset_collate = $wpdb->get_charset_collate();
     238
     239        $sql = "CREATE TABLE $table_name (
     240            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
     241            url varchar(2083) NOT NULL,
     242            post_id bigint(20) unsigned DEFAULT 0,
     243            status varchar(20) NOT NULL DEFAULT 'success',
     244            message text,
     245            source varchar(50) NOT NULL DEFAULT 'auto',
     246            created_at datetime DEFAULT CURRENT_TIMESTAMP,
     247            PRIMARY KEY  (id),
     248            KEY status_index (status),
     249            KEY created_at_index (created_at),
     250            KEY post_id_index (post_id)
     251        ) $charset_collate;";
     252
     253        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
     254        dbDelta( $sql );
     255    }
     256
    202257    /**
    203258     * Plugin deactivation
     
    457512        );
    458513       
     514        // Register Google Index settings
     515        register_setting(
     516            'apicoid_gw_google_index_settings',
     517            'apicoid_gw_google_index_enabled',
     518            array(
     519                'sanitize_callback' => 'rest_sanitize_boolean',
     520                'default'           => false,
     521            )
     522        );
     523
     524        register_setting(
     525            'apicoid_gw_google_index_settings',
     526            'apicoid_gw_google_index_base_url',
     527            array(
     528                'sanitize_callback' => 'sanitize_text_field',
     529            )
     530        );
     531
    459532        // Add settings section
    460533        add_settings_section(
     
    24592532        // );
    24602533       
     2534        // Add Auto Google Index submenu
     2535        add_submenu_page(
     2536            'apicoid-ghostwriter',
     2537            __( 'Auto Google Index', 'apicoid-ghostwriter' ),
     2538            __( 'Auto Google Index', 'apicoid-ghostwriter' ),
     2539            'manage_options',
     2540            'apicoid-gw-google-index',
     2541            array( $this, 'render_google_index_page' )
     2542        );
     2543
    24612544        // Add Settings submenu
    24622545        add_submenu_page(
     
    25202603        // Include settings page template
    25212604        include APICOID_GW_DIR . 'includes/settings-page.php';
     2605    }
     2606
     2607    /**
     2608     * Render Google Index page
     2609     */
     2610    public function render_google_index_page() {
     2611        // Check user capabilities
     2612        if ( ! current_user_can( 'manage_options' ) ) {
     2613            return;
     2614        }
     2615
     2616        // Include google index page template
     2617        include APICOID_GW_DIR . 'includes/google-index-page.php';
    25222618    }
    25232619   
     
    26212717        // on sanitize_title() of the menu title which can change.
    26222718        $current_page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    2623         $our_pages = array( 'apicoid-ghostwriter', 'apicoid-gw-article-generator', 'apicoid-gw-settings' );
     2719        $our_pages = array( 'apicoid-ghostwriter', 'apicoid-gw-article-generator', 'apicoid-gw-settings', 'apicoid-gw-google-index' );
    26242720        if ( ! in_array( $current_page, $our_pages, true ) ) {
    26252721            return;
     
    26982794            );
    26992795        }
     2796
     2797        // Load Google Index script on Google Index page
     2798        if ( 'apicoid-gw-google-index' === $current_page ) {
     2799            wp_enqueue_script(
     2800                'apicoid-gw-google-index-script',
     2801                APICOID_GW_URL . 'assets/js/google-index.js',
     2802                array( 'jquery' ),
     2803                APICOID_GW_VERSION,
     2804                true
     2805            );
     2806
     2807            // Localize script for AJAX
     2808            wp_localize_script(
     2809                'apicoid-gw-google-index-script',
     2810                'apicoidGwGoogleIndex',
     2811                array(
     2812                    'ajax_url'    => admin_url( 'admin-ajax.php' ),
     2813                    'save_nonce'  => wp_create_nonce( 'apicoid_gw_save_google_index_settings' ),
     2814                    'test_nonce'  => wp_create_nonce( 'apicoid_gw_test_google_index' ),
     2815                    'log_nonce'   => wp_create_nonce( 'apicoid_gw_google_index_logs' ),
     2816                    'clear_nonce' => wp_create_nonce( 'apicoid_gw_clear_google_index_logs' ),
     2817                    'strings'     => array(
     2818                        'saving'        => __( 'Saving...', 'apicoid-ghostwriter' ),
     2819                        'testing'       => __( 'Testing connection...', 'apicoid-ghostwriter' ),
     2820                        'save_success'  => __( 'Settings saved successfully!', 'apicoid-ghostwriter' ),
     2821                        'save_error'    => __( 'Failed to save settings.', 'apicoid-ghostwriter' ),
     2822                        'test_success'  => __( 'Connection successful! URL submitted to Google.', 'apicoid-ghostwriter' ),
     2823                        'test_error'    => __( 'Connection test failed.', 'apicoid-ghostwriter' ),
     2824                        'confirm_clear' => __( 'Are you sure you want to disable Auto Google Index and clear all settings?', 'apicoid-ghostwriter' ),
     2825                        'confirm_clear_logs' => __( 'Are you sure you want to clear all submission logs?', 'apicoid-ghostwriter' ),
     2826                        'no_logs'       => __( 'No submission logs yet.', 'apicoid-ghostwriter' ),
     2827                        'clearing'      => __( 'Clearing...', 'apicoid-ghostwriter' ),
     2828                    ),
     2829                )
     2830            );
     2831        }
     2832    }
     2833
     2834    /**
     2835     * Encrypt service account key JSON
     2836     *
     2837     * @param string $json_string The JSON string to encrypt.
     2838     * @return string|false Encrypted string or false on failure.
     2839     */
     2840    private function encrypt_service_account_key( $json_string ) {
     2841        if ( ! function_exists( 'openssl_encrypt' ) ) {
     2842            return false;
     2843        }
     2844
     2845        $key    = hash( 'sha256', AUTH_KEY, true );
     2846        $iv     = openssl_random_pseudo_bytes( 16 );
     2847        $cipher = openssl_encrypt( $json_string, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv );
     2848
     2849        if ( false === $cipher ) {
     2850            return false;
     2851        }
     2852
     2853        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Required for storing encrypted binary data safely in wp_options.
     2854        return base64_encode( $iv . $cipher );
     2855    }
     2856
     2857    /**
     2858     * Decrypt service account key JSON
     2859     *
     2860     * @return string|false Decrypted JSON string or false on failure.
     2861     */
     2862    private function decrypt_service_account_key() {
     2863        if ( ! function_exists( 'openssl_decrypt' ) ) {
     2864            return false;
     2865        }
     2866
     2867        $encrypted = get_option( 'apicoid_gw_google_index_service_account', '' );
     2868        if ( empty( $encrypted ) ) {
     2869            return false;
     2870        }
     2871
     2872        $key  = hash( 'sha256', AUTH_KEY, true );
     2873        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Required for decrypting binary data stored as base64 in wp_options.
     2874        $data = base64_decode( $encrypted );
     2875
     2876        if ( false === $data || strlen( $data ) < 17 ) {
     2877            return false;
     2878        }
     2879
     2880        $iv        = substr( $data, 0, 16 );
     2881        $cipher    = substr( $data, 16 );
     2882        $decrypted = openssl_decrypt( $cipher, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv );
     2883
     2884        return $decrypted;
     2885    }
     2886
     2887    /**
     2888     * Create a signed JWT for Google service account authentication
     2889     *
     2890     * @param array $service_account Parsed service account data.
     2891     * @return string|false The signed JWT or false on failure.
     2892     */
     2893    private function create_google_jwt( $service_account ) {
     2894        $now = time();
     2895
     2896        $header = array(
     2897            'alg' => 'RS256',
     2898            'typ' => 'JWT',
     2899        );
     2900
     2901        $claims = array(
     2902            'iss'   => $service_account['client_email'],
     2903            'scope' => 'https://www.googleapis.com/auth/indexing',
     2904            'aud'   => isset( $service_account['token_uri'] ) ? $service_account['token_uri'] : 'https://oauth2.googleapis.com/token',
     2905            'iat'   => $now,
     2906            'exp'   => $now + 3600,
     2907        );
     2908
     2909        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Required for JWT encoding per RFC 7519.
     2910        $header_encoded = rtrim( strtr( base64_encode( wp_json_encode( $header ) ), '+/', '-_' ), '=' );
     2911        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Required for JWT encoding per RFC 7519.
     2912        $claims_encoded = rtrim( strtr( base64_encode( wp_json_encode( $claims ) ), '+/', '-_' ), '=' );
     2913
     2914        $signature_input = $header_encoded . '.' . $claims_encoded;
     2915
     2916        $private_key = openssl_pkey_get_private( $service_account['private_key'] );
     2917        if ( false === $private_key ) {
     2918            return false;
     2919        }
     2920
     2921        $signature = '';
     2922        $success   = openssl_sign( $signature_input, $signature, $private_key, OPENSSL_ALGO_SHA256 );
     2923
     2924        if ( ! $success ) {
     2925            return false;
     2926        }
     2927
     2928        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Required for JWT encoding per RFC 7519.
     2929        $signature_encoded = rtrim( strtr( base64_encode( $signature ), '+/', '-_' ), '=' );
     2930
     2931        return $signature_input . '.' . $signature_encoded;
     2932    }
     2933
     2934    /**
     2935     * Exchange JWT for Google OAuth2 access token
     2936     *
     2937     * @param string $jwt    The signed JWT.
     2938     * @param string $token_uri Optional token URI.
     2939     * @return string|WP_Error Access token string or WP_Error on failure.
     2940     */
     2941    private function get_google_access_token( $jwt, $token_uri = 'https://oauth2.googleapis.com/token' ) {
     2942        $response = wp_remote_post(
     2943            $token_uri,
     2944            array(
     2945                'body'    => array(
     2946                    'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
     2947                    'assertion'  => $jwt,
     2948                ),
     2949                'timeout' => 30,
     2950            )
     2951        );
     2952
     2953        if ( is_wp_error( $response ) ) {
     2954            return $response;
     2955        }
     2956
     2957        $body = json_decode( wp_remote_retrieve_body( $response ), true );
     2958
     2959        if ( empty( $body['access_token'] ) ) {
     2960            $error_msg = isset( $body['error_description'] ) ? $body['error_description'] : __( 'Failed to obtain access token from Google.', 'apicoid-ghostwriter' );
     2961            return new WP_Error( 'google_auth_error', $error_msg );
     2962        }
     2963
     2964        return $body['access_token'];
     2965    }
     2966
     2967    /**
     2968     * Log a Google Index submission to the database
     2969     *
     2970     * @param string $url     The URL that was submitted.
     2971     * @param int    $post_id The post ID (0 if not associated with a post).
     2972     * @param string $status  'success' or 'error'.
     2973     * @param string $message The result message.
     2974     * @param string $source  Source of the submission: 'auto', 'test', or 'manual'.
     2975     */
     2976    private function log_google_index_submission( $url, $post_id, $status, $message, $source = 'auto' ) {
     2977        global $wpdb;
     2978
     2979        // Ensure log table exists
     2980        $table_name = $this->get_google_index_log_table_name();
     2981
     2982        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table, no WP API available.
     2983        $wpdb->insert(
     2984            $table_name,
     2985            array(
     2986                'url'        => $url,
     2987                'post_id'    => $post_id,
     2988                'status'     => $status,
     2989                'message'    => $message,
     2990                'source'     => $source,
     2991                'created_at' => current_time( 'mysql' ),
     2992            ),
     2993            array( '%s', '%d', '%s', '%s', '%s', '%s' )
     2994        );
     2995    }
     2996
     2997    /**
     2998     * Submit a URL to Google Indexing API
     2999     *
     3000     * @param string $url The URL to submit.
     3001     * @return array|WP_Error Response array with 'success' and 'message' keys, or WP_Error.
     3002     */
     3003    public function submit_url_to_google( $url ) {
     3004        // Check if enabled
     3005        $enabled = get_option( 'apicoid_gw_google_index_enabled', false );
     3006        if ( ! $enabled ) {
     3007            return new WP_Error( 'not_enabled', __( 'Auto Google Index is not enabled.', 'apicoid-ghostwriter' ) );
     3008        }
     3009
     3010        // Decrypt service account key
     3011        $json_string = $this->decrypt_service_account_key();
     3012        if ( false === $json_string ) {
     3013            return new WP_Error( 'decrypt_failed', __( 'Failed to decrypt service account key.', 'apicoid-ghostwriter' ) );
     3014        }
     3015
     3016        $service_account = json_decode( $json_string, true );
     3017        if ( empty( $service_account ) || empty( $service_account['client_email'] ) || empty( $service_account['private_key'] ) ) {
     3018            return new WP_Error( 'invalid_key', __( 'Service account key is invalid or incomplete.', 'apicoid-ghostwriter' ) );
     3019        }
     3020
     3021        // Create JWT
     3022        $jwt = $this->create_google_jwt( $service_account );
     3023        if ( false === $jwt ) {
     3024            return new WP_Error( 'jwt_failed', __( 'Failed to create JWT token.', 'apicoid-ghostwriter' ) );
     3025        }
     3026
     3027        // Get access token
     3028        $token_uri    = isset( $service_account['token_uri'] ) ? $service_account['token_uri'] : 'https://oauth2.googleapis.com/token';
     3029        $access_token = $this->get_google_access_token( $jwt, $token_uri );
     3030        if ( is_wp_error( $access_token ) ) {
     3031            return $access_token;
     3032        }
     3033
     3034        // Submit URL to Google Indexing API
     3035        $response = wp_remote_post(
     3036            'https://indexing.googleapis.com/v3/urlNotifications:publish',
     3037            array(
     3038                'headers' => array(
     3039                    'Content-Type'  => 'application/json',
     3040                    'Authorization' => 'Bearer ' . $access_token,
     3041                ),
     3042                'body'    => wp_json_encode(
     3043                    array(
     3044                        'url'  => $url,
     3045                        'type' => 'URL_UPDATED',
     3046                    )
     3047                ),
     3048                'timeout' => 30,
     3049            )
     3050        );
     3051
     3052        if ( is_wp_error( $response ) ) {
     3053            return $response;
     3054        }
     3055
     3056        $response_code = wp_remote_retrieve_response_code( $response );
     3057        $response_body = json_decode( wp_remote_retrieve_body( $response ), true );
     3058
     3059        if ( 200 === $response_code ) {
     3060            return array(
     3061                'success' => true,
     3062                'message' => __( 'URL submitted successfully to Google.', 'apicoid-ghostwriter' ),
     3063                'data'    => $response_body,
     3064            );
     3065        }
     3066
     3067        $error_msg = isset( $response_body['error']['message'] ) ? $response_body['error']['message'] : __( 'Unknown error from Google Indexing API.', 'apicoid-ghostwriter' );
     3068        return new WP_Error( 'google_api_error', $error_msg, array( 'status' => $response_code ) );
     3069    }
     3070
     3071    /**
     3072     * AJAX handler: Save Google Index settings
     3073     */
     3074    public function ajax_save_google_index_settings() {
     3075        check_ajax_referer( 'apicoid_gw_save_google_index_settings', 'nonce' );
     3076
     3077        if ( ! current_user_can( 'manage_options' ) ) {
     3078            wp_send_json_error( array( 'message' => __( 'You do not have permission to change these settings.', 'apicoid-ghostwriter' ) ) );
     3079        }
     3080
     3081        $enabled  = isset( $_POST['enabled'] ) ? rest_sanitize_boolean( sanitize_text_field( wp_unslash( $_POST['enabled'] ) ) ) : false;
     3082        $base_url = isset( $_POST['base_url'] ) ? sanitize_text_field( wp_unslash( $_POST['base_url'] ) ) : '';
     3083
     3084        // If disabling, clear settings
     3085        if ( ! $enabled ) {
     3086            update_option( 'apicoid_gw_google_index_enabled', false );
     3087            update_option( 'apicoid_gw_google_index_base_url', '' );
     3088            delete_option( 'apicoid_gw_google_index_service_account' );
     3089            wp_send_json_success( array( 'message' => __( 'Auto Google Index has been disabled.', 'apicoid-ghostwriter' ) ) );
     3090        }
     3091
     3092        // Validate base URL
     3093        $base_url = untrailingslashit( $base_url );
     3094        if ( empty( $base_url ) || ! filter_var( $base_url, FILTER_VALIDATE_URL ) ) {
     3095            wp_send_json_error( array( 'message' => __( 'Please enter a valid website URL (e.g. https://example.com).', 'apicoid-ghostwriter' ) ) );
     3096        }
     3097
     3098        if ( strpos( $base_url, 'https://' ) !== 0 ) {
     3099            wp_send_json_error( array( 'message' => __( 'Website URL must start with https://.', 'apicoid-ghostwriter' ) ) );
     3100        }
     3101
     3102        // Validate and encrypt service account key
     3103        $service_account_json = isset( $_POST['service_account_key'] ) ? sanitize_textarea_field( wp_unslash( $_POST['service_account_key'] ) ) : '';
     3104
     3105        // Only require new key if no existing key is stored
     3106        $existing_key = get_option( 'apicoid_gw_google_index_service_account', '' );
     3107        if ( empty( $service_account_json ) && ! empty( $existing_key ) ) {
     3108            // Keep existing key, just update other settings
     3109            update_option( 'apicoid_gw_google_index_enabled', true );
     3110            update_option( 'apicoid_gw_google_index_base_url', $base_url );
     3111            wp_send_json_success( array( 'message' => __( 'Settings saved successfully!', 'apicoid-ghostwriter' ) ) );
     3112        }
     3113
     3114        if ( empty( $service_account_json ) ) {
     3115            wp_send_json_error( array( 'message' => __( 'Please provide the service account key JSON.', 'apicoid-ghostwriter' ) ) );
     3116        }
     3117
     3118        $service_account = json_decode( $service_account_json, true );
     3119        if ( null === $service_account ) {
     3120            wp_send_json_error( array( 'message' => __( 'Invalid JSON format. Please paste valid service account key JSON.', 'apicoid-ghostwriter' ) ) );
     3121        }
     3122
     3123        // Validate required fields
     3124        $required_fields = array( 'client_email', 'private_key' );
     3125        foreach ( $required_fields as $field ) {
     3126            if ( empty( $service_account[ $field ] ) ) {
     3127                /* translators: %s: Missing field name in the service account JSON */
     3128                wp_send_json_error( array( 'message' => sprintf( __( 'Service account key is missing required field: %s', 'apicoid-ghostwriter' ), $field ) ) );
     3129            }
     3130        }
     3131
     3132        // Validate private key format
     3133        if ( strpos( $service_account['private_key'], '-----BEGIN' ) === false ) {
     3134            wp_send_json_error( array( 'message' => __( 'Invalid private key format in service account JSON.', 'apicoid-ghostwriter' ) ) );
     3135        }
     3136
     3137        // Encrypt and save
     3138        $encrypted = $this->encrypt_service_account_key( $service_account_json );
     3139        if ( false === $encrypted ) {
     3140            wp_send_json_error( array( 'message' => __( 'Failed to encrypt service account key. Please ensure OpenSSL is available.', 'apicoid-ghostwriter' ) ) );
     3141        }
     3142
     3143        update_option( 'apicoid_gw_google_index_enabled', true );
     3144        update_option( 'apicoid_gw_google_index_base_url', $base_url );
     3145        update_option( 'apicoid_gw_google_index_service_account', $encrypted );
     3146
     3147        wp_send_json_success( array( 'message' => __( 'Settings saved successfully! Service account key has been encrypted and stored.', 'apicoid-ghostwriter' ) ) );
     3148    }
     3149
     3150    /**
     3151     * AJAX handler: Test Google Index connection
     3152     */
     3153    public function ajax_test_google_index() {
     3154        check_ajax_referer( 'apicoid_gw_test_google_index', 'nonce' );
     3155
     3156        if ( ! current_user_can( 'manage_options' ) ) {
     3157            wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'apicoid-ghostwriter' ) ) );
     3158        }
     3159
     3160        $enabled = get_option( 'apicoid_gw_google_index_enabled', false );
     3161        if ( ! $enabled ) {
     3162            wp_send_json_error( array( 'message' => __( 'Please enable and save settings first before testing.', 'apicoid-ghostwriter' ) ) );
     3163        }
     3164
     3165        $base_url = get_option( 'apicoid_gw_google_index_base_url', '' );
     3166        if ( empty( $base_url ) ) {
     3167            wp_send_json_error( array( 'message' => __( 'No base URL configured. Please save settings first.', 'apicoid-ghostwriter' ) ) );
     3168        }
     3169
     3170        // Use the base URL itself as the test URL
     3171        $test_url = trailingslashit( $base_url );
     3172
     3173        $result = $this->submit_url_to_google( $test_url );
     3174
     3175        if ( is_wp_error( $result ) ) {
     3176            $this->log_google_index_submission( $test_url, 0, 'error', $result->get_error_message(), 'test' );
     3177            wp_send_json_error( array( 'message' => $result->get_error_message() ) );
     3178        }
     3179
     3180        $success_message = sprintf(
     3181            /* translators: %s: The URL that was submitted to Google Indexing API */
     3182            __( 'Connection successful! Test URL (%s) was submitted to Google Indexing API.', 'apicoid-ghostwriter' ),
     3183            $test_url
     3184        );
     3185
     3186        $this->log_google_index_submission( $test_url, 0, 'success', $success_message, 'test' );
     3187
     3188        wp_send_json_success(
     3189            array(
     3190                'message' => $success_message,
     3191                'data'    => $result['data'],
     3192            )
     3193        );
     3194    }
     3195
     3196    /**
     3197     * AJAX handler: Get Google Index logs with pagination
     3198     */
     3199    public function ajax_get_google_index_logs() {
     3200        check_ajax_referer( 'apicoid_gw_google_index_logs', 'nonce' );
     3201
     3202        if ( ! current_user_can( 'manage_options' ) ) {
     3203            wp_send_json_error( array( 'message' => __( 'You do not have permission to view logs.', 'apicoid-ghostwriter' ) ) );
     3204        }
     3205
     3206        global $wpdb;
     3207
     3208        $page     = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
     3209        $per_page = 15;
     3210        $offset   = ( $page - 1 ) * $per_page;
     3211
     3212        $table_name = $this->get_google_index_log_table_name();
     3213
     3214        // Ensure log table exists (in case plugin was not reactivated after update)
     3215        $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
     3216        if ( $table_exists !== $table_name ) {
     3217            $this->create_google_index_log_table();
     3218        }
     3219
     3220        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table, no WP API available.
     3221        $total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $table_name" );
     3222
     3223        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table with pagination, no WP API available.
     3224        $logs = $wpdb->get_results(
     3225            $wpdb->prepare(
     3226                "SELECT * FROM $table_name ORDER BY created_at DESC LIMIT %d OFFSET %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is generated internally.
     3227                $per_page,
     3228                $offset
     3229            ),
     3230            ARRAY_A
     3231        );
     3232
     3233        $total_pages = ceil( $total / $per_page );
     3234
     3235        wp_send_json_success(
     3236            array(
     3237                'logs'        => $logs,
     3238                'total'       => $total,
     3239                'page'        => $page,
     3240                'per_page'    => $per_page,
     3241                'total_pages' => $total_pages,
     3242            )
     3243        );
     3244    }
     3245
     3246    /**
     3247     * AJAX handler: Clear Google Index logs
     3248     */
     3249    public function ajax_clear_google_index_logs() {
     3250        check_ajax_referer( 'apicoid_gw_clear_google_index_logs', 'nonce' );
     3251
     3252        if ( ! current_user_can( 'manage_options' ) ) {
     3253            wp_send_json_error( array( 'message' => __( 'You do not have permission to clear logs.', 'apicoid-ghostwriter' ) ) );
     3254        }
     3255
     3256        global $wpdb;
     3257        $table_name = $this->get_google_index_log_table_name();
     3258
     3259        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Truncating custom table.
     3260        $wpdb->query( "TRUNCATE TABLE $table_name" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is generated internally, not user input.
     3261
     3262        wp_send_json_success( array( 'message' => __( 'All logs have been cleared.', 'apicoid-ghostwriter' ) ) );
     3263    }
     3264
     3265    /**
     3266     * Auto-submit URL to Google when a post/page is published or updated
     3267     *
     3268     * @param string  $new_status New post status.
     3269     * @param string  $old_status Old post status.
     3270     * @param WP_Post $post       Post object.
     3271     */
     3272    public function on_post_publish_update( $new_status, $old_status, $post ) {
     3273        // Only process when post becomes published or is updated while published
     3274        if ( 'publish' !== $new_status ) {
     3275            return;
     3276        }
     3277
     3278        // Only for posts and pages
     3279        $allowed_post_types = array( 'post', 'page' );
     3280        if ( ! in_array( $post->post_type, $allowed_post_types, true ) ) {
     3281            return;
     3282        }
     3283
     3284        // Check if auto index is enabled
     3285        $enabled = get_option( 'apicoid_gw_google_index_enabled', false );
     3286        if ( ! $enabled ) {
     3287            return;
     3288        }
     3289
     3290        // Get permalink
     3291        $permalink = get_permalink( $post->ID );
     3292        if ( empty( $permalink ) ) {
     3293            return;
     3294        }
     3295
     3296        // Submit URL to Google
     3297        $result = $this->submit_url_to_google( $permalink );
     3298
     3299        // Log the result
     3300        $is_success = ! is_wp_error( $result );
     3301        $message    = is_wp_error( $result ) ? $result->get_error_message() : $result['message'];
     3302        $this->log_google_index_submission(
     3303            $permalink,
     3304            $post->ID,
     3305            $is_success ? 'success' : 'error',
     3306            $message,
     3307            'auto'
     3308        );
     3309
     3310        // Also store in post meta for quick access
     3311        update_post_meta( $post->ID, '_apicoid_gw_google_index_status', array(
     3312            'url'       => $permalink,
     3313            'timestamp' => current_time( 'mysql' ),
     3314            'success'   => $is_success,
     3315            'message'   => $message,
     3316        ) );
    27003317    }
    27013318}
  • apicoid-ghostwriter/tags/1.3.1/assets/css/admin.css

    r3457596 r3458138  
    433433}
    434434
     435/* Google Index Page Styles */
     436.apicoid-gw-google-index-actions {
     437    display: flex;
     438    gap: 10px;
     439    margin: 20px 0;
     440    padding-top: 10px;
     441}
     442
     443.apicoid-gw-google-index-actions .button {
     444    min-width: 140px;
     445    text-align: center;
     446}
     447
     448.apicoid-gw-service-key-wrapper textarea {
     449    font-family: monospace;
     450    font-size: 12px;
     451    resize: vertical;
     452}
     453
     454.apicoid-gw-key-status {
     455    margin-top: 8px;
     456}
     457
     458.apicoid-gw-key-status .dashicons {
     459    font-size: 16px;
     460    width: 16px;
     461    height: 16px;
     462    vertical-align: text-bottom;
     463}
     464
     465.apicoid-gw-index-log {
     466    margin-top: 20px;
     467    padding: 15px 20px;
     468    background: #f9f9f9;
     469    border: 1px solid #ccd0d4;
     470    border-radius: 3px;
     471}
     472
     473.apicoid-gw-index-log h3 {
     474    margin-top: 0;
     475    margin-bottom: 10px;
     476}
     477
     478.apicoid-gw-setup-steps {
     479    margin: 0;
     480    padding-left: 20px;
     481}
     482
     483.apicoid-gw-setup-steps li {
     484    margin-bottom: 8px;
     485    color: #555;
     486    line-height: 1.5;
     487}
     488
     489.apicoid-gw-setup-link {
     490    margin-top: 12px;
     491    margin-bottom: 0;
     492}
     493
     494.apicoid-gw-setup-link a {
     495    font-weight: 600;
     496}
     497
     498#apicoid-gw-google-index-notice {
     499    margin: 15px 0;
     500}
     501
     502#apicoid-gw-google-index-notice p {
     503    margin: 0.5em 0;
     504}
     505
     506/* Log Table Styles */
     507.apicoid-gw-log-header {
     508    display: flex;
     509    justify-content: space-between;
     510    align-items: center;
     511    margin-bottom: 5px;
     512}
     513
     514.apicoid-gw-log-header h2 {
     515    margin: 0;
     516}
     517
     518#apicoid-gw-clear-logs {
     519    text-decoration: none;
     520    border: none;
     521    background: none;
     522    color: #b32d2e;
     523    cursor: pointer;
     524    padding: 0;
     525    font-size: 13px;
     526}
     527
     528#apicoid-gw-clear-logs:hover {
     529    color: #a00;
     530}
     531
     532#apicoid-gw-clear-logs:disabled {
     533    opacity: 0.5;
     534    cursor: not-allowed;
     535}
     536
     537#apicoid-gw-log-table {
     538    margin-top: 12px;
     539}
     540
     541#apicoid-gw-log-table .column-status {
     542    width: 80px;
     543}
     544
     545#apicoid-gw-log-table .column-url {
     546    width: 30%;
     547}
     548
     549#apicoid-gw-log-table .column-source {
     550    width: 70px;
     551}
     552
     553#apicoid-gw-log-table .column-message {
     554    width: auto;
     555}
     556
     557#apicoid-gw-log-table .column-date {
     558    width: 150px;
     559}
     560
     561#apicoid-gw-log-table .column-url a {
     562    word-break: break-all;
     563}
     564
     565.apicoid-gw-log-badge {
     566    display: inline-block;
     567    padding: 3px 8px;
     568    border-radius: 3px;
     569    font-size: 11px;
     570    font-weight: 600;
     571    text-transform: uppercase;
     572    line-height: 1.4;
     573}
     574
     575.apicoid-gw-log-success {
     576    background: #46b450;
     577    color: #fff;
     578}
     579
     580.apicoid-gw-log-error {
     581    background: #dc3232;
     582    color: #fff;
     583}
     584
     585.apicoid-gw-log-source {
     586    display: inline-block;
     587    padding: 2px 6px;
     588    border-radius: 3px;
     589    font-size: 11px;
     590    font-weight: 500;
     591}
     592
     593.apicoid-gw-log-source-auto {
     594    background: #e5e5e5;
     595    color: #555;
     596}
     597
     598.apicoid-gw-log-source-test {
     599    background: #cce5ff;
     600    color: #004085;
     601}
     602
     603.apicoid-gw-log-postid {
     604    color: #999;
     605    font-size: 12px;
     606}
     607
     608.apicoid-gw-log-loading {
     609    text-align: center;
     610    padding: 20px !important;
     611    color: #666;
     612}
     613
     614.apicoid-gw-log-loading .spinner {
     615    margin-right: 5px;
     616}
     617
     618.apicoid-gw-log-empty {
     619    text-align: center;
     620    padding: 20px !important;
     621    color: #999;
     622    font-style: italic;
     623}
     624
     625#apicoid-gw-log-pagination {
     626    margin-top: 10px;
     627}
     628
     629#apicoid-gw-log-pagination .tablenav-pages {
     630    float: right;
     631}
     632
     633#apicoid-gw-log-pagination .displaying-num {
     634    margin-right: 10px;
     635    color: #666;
     636    font-style: italic;
     637}
     638
     639#apicoid-gw-log-pagination .paging-input {
     640    margin: 0 5px;
     641    font-size: 13px;
     642}
     643
     644#apicoid-gw-log-pagination .button {
     645    min-width: 28px;
     646    text-align: center;
     647    padding: 2px 6px;
     648    margin: 0 1px;
     649}
     650
     651#apicoid-gw-log-pagination .button.disabled {
     652    opacity: 0.5;
     653    cursor: default;
     654    pointer-events: none;
     655}
     656
  • apicoid-ghostwriter/tags/1.3.1/readme.txt

    r3457596 r3458138  
    55Requires at least: 6.2
    66Tested up to: 6.9
    7 Stable tag: 1.3.0
     7Stable tag: 1.3.1
    88Requires PHP: 7.4
    99License: GPLv2 or later
     
    129129
    130130== Changelog ==
     131
     132= 1.3.1 =
     133* Auto Google Index: New feature to automatically submit post and page URLs to Google Indexing API when published or updated
     134* Google Indexing API integration: Full integration with Google's Indexing API using service account authentication (JWT/OAuth2)
     135* Submission log: Comprehensive logging system with database table to track all submission attempts (success/failure) with pagination
     136* Encrypted service account storage: Service account JSON keys are encrypted using AES-256-CBC before storing in database
     137* Test connection feature: Built-in test connection button to verify Google Indexing API credentials before enabling auto-submission
     138* Setup guide with tutorial link: Added step-by-step setup guide with link to full tutorial at https://api.co.id/blog/auto-index-ghostwriter/
     139* Auto-submit on publish/update: Automatically submits URLs to Google when posts or pages are published or updated (for post and page post types only)
     140* Log management: View submission history with status indicators, source tracking (auto/test), and clear logs functionality
    131141
    132142= 1.3.0 =
     
    180190== Upgrade Notice ==
    181191
     192= 1.3.1 =
     193This update introduces Auto Google Index feature that automatically submits your post and page URLs to Google Indexing API when published or updated. Includes comprehensive submission logging, encrypted service account storage, and test connection functionality. See the new "Auto Google Index" menu for setup instructions.
     194
    182195= 1.3.0 =
    183196This update introduces "Generate Article by Category" feature with AI-powered suggestions and automatic related article linking. Includes improved two-column modal layout for better workflow.
  • apicoid-ghostwriter/trunk/apicoid-ghostwriter.php

    r3457596 r3458138  
    44 * Plugin URI:  https://wordpress.org/plugins/apicoid-ghostwriter/
    55 * Description: Connects your WordPress site to Api.co.id to generate content and rewrite content automatically using AI. Features include article generation, content rewriting, automatic related article linking, SEO integration, and image generation.
    6  * Version:     1.3.0
     6 * Version:     1.3.1
    77 * Author:      Api.co.id
    88 * Author URI:  https://api.co.id
     
    2121
    2222// Define plugin constants
    23 define( 'APICOID_GW_VERSION', '1.3.0' );
     23define( 'APICOID_GW_VERSION', '1.3.1' );
    2424define( 'APICOID_GW_DIR', plugin_dir_path( __FILE__ ) );
    2525define( 'APICOID_GW_URL', plugin_dir_url( __FILE__ ) );
     
    110110       
    111111        add_action( 'wp_ajax_apicoid_gw_generate_article_by_category', array( $this, 'ajax_generate_article_by_category' ) );
     112
     113        // Handle Google Index settings save
     114        add_action( 'wp_ajax_apicoid_gw_save_google_index_settings', array( $this, 'ajax_save_google_index_settings' ) );
     115
     116        // Handle Google Index test connection
     117        add_action( 'wp_ajax_apicoid_gw_test_google_index', array( $this, 'ajax_test_google_index' ) );
     118
     119        // Handle Google Index log fetch
     120        add_action( 'wp_ajax_apicoid_gw_get_google_index_logs', array( $this, 'ajax_get_google_index_logs' ) );
     121
     122        // Handle Google Index log clear
     123        add_action( 'wp_ajax_apicoid_gw_clear_google_index_logs', array( $this, 'ajax_clear_google_index_logs' ) );
     124
     125        // Auto-submit URL to Google on post publish/update
     126        add_action( 'transition_post_status', array( $this, 'on_post_publish_update' ), 10, 3 );
    112127       
    113128        // Enqueue admin scripts and styles
     
    127142        // Create database table
    128143        $this->create_database_table();
     144
     145        // Create Google Index log table
     146        $this->create_google_index_log_table();
    129147
    130148        // Set default article generator settings
     
    199217        return $wpdb->prefix . 'apicoid_gw_posts';
    200218    }
    201    
     219
     220    /**
     221     * Get the Google Index log table name
     222     *
     223     * @return string Table name with prefix
     224     */
     225    public function get_google_index_log_table_name() {
     226        global $wpdb;
     227        return $wpdb->prefix . 'apicoid_gw_google_index_log';
     228    }
     229
     230    /**
     231     * Create database table for Google Index logs
     232     */
     233    private function create_google_index_log_table() {
     234        global $wpdb;
     235
     236        $table_name      = $this->get_google_index_log_table_name();
     237        $charset_collate = $wpdb->get_charset_collate();
     238
     239        $sql = "CREATE TABLE $table_name (
     240            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
     241            url varchar(2083) NOT NULL,
     242            post_id bigint(20) unsigned DEFAULT 0,
     243            status varchar(20) NOT NULL DEFAULT 'success',
     244            message text,
     245            source varchar(50) NOT NULL DEFAULT 'auto',
     246            created_at datetime DEFAULT CURRENT_TIMESTAMP,
     247            PRIMARY KEY  (id),
     248            KEY status_index (status),
     249            KEY created_at_index (created_at),
     250            KEY post_id_index (post_id)
     251        ) $charset_collate;";
     252
     253        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
     254        dbDelta( $sql );
     255    }
     256
    202257    /**
    203258     * Plugin deactivation
     
    457512        );
    458513       
     514        // Register Google Index settings
     515        register_setting(
     516            'apicoid_gw_google_index_settings',
     517            'apicoid_gw_google_index_enabled',
     518            array(
     519                'sanitize_callback' => 'rest_sanitize_boolean',
     520                'default'           => false,
     521            )
     522        );
     523
     524        register_setting(
     525            'apicoid_gw_google_index_settings',
     526            'apicoid_gw_google_index_base_url',
     527            array(
     528                'sanitize_callback' => 'sanitize_text_field',
     529            )
     530        );
     531
    459532        // Add settings section
    460533        add_settings_section(
     
    24592532        // );
    24602533       
     2534        // Add Auto Google Index submenu
     2535        add_submenu_page(
     2536            'apicoid-ghostwriter',
     2537            __( 'Auto Google Index', 'apicoid-ghostwriter' ),
     2538            __( 'Auto Google Index', 'apicoid-ghostwriter' ),
     2539            'manage_options',
     2540            'apicoid-gw-google-index',
     2541            array( $this, 'render_google_index_page' )
     2542        );
     2543
    24612544        // Add Settings submenu
    24622545        add_submenu_page(
     
    25202603        // Include settings page template
    25212604        include APICOID_GW_DIR . 'includes/settings-page.php';
     2605    }
     2606
     2607    /**
     2608     * Render Google Index page
     2609     */
     2610    public function render_google_index_page() {
     2611        // Check user capabilities
     2612        if ( ! current_user_can( 'manage_options' ) ) {
     2613            return;
     2614        }
     2615
     2616        // Include google index page template
     2617        include APICOID_GW_DIR . 'includes/google-index-page.php';
    25222618    }
    25232619   
     
    26212717        // on sanitize_title() of the menu title which can change.
    26222718        $current_page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    2623         $our_pages = array( 'apicoid-ghostwriter', 'apicoid-gw-article-generator', 'apicoid-gw-settings' );
     2719        $our_pages = array( 'apicoid-ghostwriter', 'apicoid-gw-article-generator', 'apicoid-gw-settings', 'apicoid-gw-google-index' );
    26242720        if ( ! in_array( $current_page, $our_pages, true ) ) {
    26252721            return;
     
    26982794            );
    26992795        }
     2796
     2797        // Load Google Index script on Google Index page
     2798        if ( 'apicoid-gw-google-index' === $current_page ) {
     2799            wp_enqueue_script(
     2800                'apicoid-gw-google-index-script',
     2801                APICOID_GW_URL . 'assets/js/google-index.js',
     2802                array( 'jquery' ),
     2803                APICOID_GW_VERSION,
     2804                true
     2805            );
     2806
     2807            // Localize script for AJAX
     2808            wp_localize_script(
     2809                'apicoid-gw-google-index-script',
     2810                'apicoidGwGoogleIndex',
     2811                array(
     2812                    'ajax_url'    => admin_url( 'admin-ajax.php' ),
     2813                    'save_nonce'  => wp_create_nonce( 'apicoid_gw_save_google_index_settings' ),
     2814                    'test_nonce'  => wp_create_nonce( 'apicoid_gw_test_google_index' ),
     2815                    'log_nonce'   => wp_create_nonce( 'apicoid_gw_google_index_logs' ),
     2816                    'clear_nonce' => wp_create_nonce( 'apicoid_gw_clear_google_index_logs' ),
     2817                    'strings'     => array(
     2818                        'saving'        => __( 'Saving...', 'apicoid-ghostwriter' ),
     2819                        'testing'       => __( 'Testing connection...', 'apicoid-ghostwriter' ),
     2820                        'save_success'  => __( 'Settings saved successfully!', 'apicoid-ghostwriter' ),
     2821                        'save_error'    => __( 'Failed to save settings.', 'apicoid-ghostwriter' ),
     2822                        'test_success'  => __( 'Connection successful! URL submitted to Google.', 'apicoid-ghostwriter' ),
     2823                        'test_error'    => __( 'Connection test failed.', 'apicoid-ghostwriter' ),
     2824                        'confirm_clear' => __( 'Are you sure you want to disable Auto Google Index and clear all settings?', 'apicoid-ghostwriter' ),
     2825                        'confirm_clear_logs' => __( 'Are you sure you want to clear all submission logs?', 'apicoid-ghostwriter' ),
     2826                        'no_logs'       => __( 'No submission logs yet.', 'apicoid-ghostwriter' ),
     2827                        'clearing'      => __( 'Clearing...', 'apicoid-ghostwriter' ),
     2828                    ),
     2829                )
     2830            );
     2831        }
     2832    }
     2833
     2834    /**
     2835     * Encrypt service account key JSON
     2836     *
     2837     * @param string $json_string The JSON string to encrypt.
     2838     * @return string|false Encrypted string or false on failure.
     2839     */
     2840    private function encrypt_service_account_key( $json_string ) {
     2841        if ( ! function_exists( 'openssl_encrypt' ) ) {
     2842            return false;
     2843        }
     2844
     2845        $key    = hash( 'sha256', AUTH_KEY, true );
     2846        $iv     = openssl_random_pseudo_bytes( 16 );
     2847        $cipher = openssl_encrypt( $json_string, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv );
     2848
     2849        if ( false === $cipher ) {
     2850            return false;
     2851        }
     2852
     2853        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Required for storing encrypted binary data safely in wp_options.
     2854        return base64_encode( $iv . $cipher );
     2855    }
     2856
     2857    /**
     2858     * Decrypt service account key JSON
     2859     *
     2860     * @return string|false Decrypted JSON string or false on failure.
     2861     */
     2862    private function decrypt_service_account_key() {
     2863        if ( ! function_exists( 'openssl_decrypt' ) ) {
     2864            return false;
     2865        }
     2866
     2867        $encrypted = get_option( 'apicoid_gw_google_index_service_account', '' );
     2868        if ( empty( $encrypted ) ) {
     2869            return false;
     2870        }
     2871
     2872        $key  = hash( 'sha256', AUTH_KEY, true );
     2873        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Required for decrypting binary data stored as base64 in wp_options.
     2874        $data = base64_decode( $encrypted );
     2875
     2876        if ( false === $data || strlen( $data ) < 17 ) {
     2877            return false;
     2878        }
     2879
     2880        $iv        = substr( $data, 0, 16 );
     2881        $cipher    = substr( $data, 16 );
     2882        $decrypted = openssl_decrypt( $cipher, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv );
     2883
     2884        return $decrypted;
     2885    }
     2886
     2887    /**
     2888     * Create a signed JWT for Google service account authentication
     2889     *
     2890     * @param array $service_account Parsed service account data.
     2891     * @return string|false The signed JWT or false on failure.
     2892     */
     2893    private function create_google_jwt( $service_account ) {
     2894        $now = time();
     2895
     2896        $header = array(
     2897            'alg' => 'RS256',
     2898            'typ' => 'JWT',
     2899        );
     2900
     2901        $claims = array(
     2902            'iss'   => $service_account['client_email'],
     2903            'scope' => 'https://www.googleapis.com/auth/indexing',
     2904            'aud'   => isset( $service_account['token_uri'] ) ? $service_account['token_uri'] : 'https://oauth2.googleapis.com/token',
     2905            'iat'   => $now,
     2906            'exp'   => $now + 3600,
     2907        );
     2908
     2909        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Required for JWT encoding per RFC 7519.
     2910        $header_encoded = rtrim( strtr( base64_encode( wp_json_encode( $header ) ), '+/', '-_' ), '=' );
     2911        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Required for JWT encoding per RFC 7519.
     2912        $claims_encoded = rtrim( strtr( base64_encode( wp_json_encode( $claims ) ), '+/', '-_' ), '=' );
     2913
     2914        $signature_input = $header_encoded . '.' . $claims_encoded;
     2915
     2916        $private_key = openssl_pkey_get_private( $service_account['private_key'] );
     2917        if ( false === $private_key ) {
     2918            return false;
     2919        }
     2920
     2921        $signature = '';
     2922        $success   = openssl_sign( $signature_input, $signature, $private_key, OPENSSL_ALGO_SHA256 );
     2923
     2924        if ( ! $success ) {
     2925            return false;
     2926        }
     2927
     2928        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Required for JWT encoding per RFC 7519.
     2929        $signature_encoded = rtrim( strtr( base64_encode( $signature ), '+/', '-_' ), '=' );
     2930
     2931        return $signature_input . '.' . $signature_encoded;
     2932    }
     2933
     2934    /**
     2935     * Exchange JWT for Google OAuth2 access token
     2936     *
     2937     * @param string $jwt    The signed JWT.
     2938     * @param string $token_uri Optional token URI.
     2939     * @return string|WP_Error Access token string or WP_Error on failure.
     2940     */
     2941    private function get_google_access_token( $jwt, $token_uri = 'https://oauth2.googleapis.com/token' ) {
     2942        $response = wp_remote_post(
     2943            $token_uri,
     2944            array(
     2945                'body'    => array(
     2946                    'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
     2947                    'assertion'  => $jwt,
     2948                ),
     2949                'timeout' => 30,
     2950            )
     2951        );
     2952
     2953        if ( is_wp_error( $response ) ) {
     2954            return $response;
     2955        }
     2956
     2957        $body = json_decode( wp_remote_retrieve_body( $response ), true );
     2958
     2959        if ( empty( $body['access_token'] ) ) {
     2960            $error_msg = isset( $body['error_description'] ) ? $body['error_description'] : __( 'Failed to obtain access token from Google.', 'apicoid-ghostwriter' );
     2961            return new WP_Error( 'google_auth_error', $error_msg );
     2962        }
     2963
     2964        return $body['access_token'];
     2965    }
     2966
     2967    /**
     2968     * Log a Google Index submission to the database
     2969     *
     2970     * @param string $url     The URL that was submitted.
     2971     * @param int    $post_id The post ID (0 if not associated with a post).
     2972     * @param string $status  'success' or 'error'.
     2973     * @param string $message The result message.
     2974     * @param string $source  Source of the submission: 'auto', 'test', or 'manual'.
     2975     */
     2976    private function log_google_index_submission( $url, $post_id, $status, $message, $source = 'auto' ) {
     2977        global $wpdb;
     2978
     2979        // Ensure log table exists
     2980        $table_name = $this->get_google_index_log_table_name();
     2981
     2982        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table, no WP API available.
     2983        $wpdb->insert(
     2984            $table_name,
     2985            array(
     2986                'url'        => $url,
     2987                'post_id'    => $post_id,
     2988                'status'     => $status,
     2989                'message'    => $message,
     2990                'source'     => $source,
     2991                'created_at' => current_time( 'mysql' ),
     2992            ),
     2993            array( '%s', '%d', '%s', '%s', '%s', '%s' )
     2994        );
     2995    }
     2996
     2997    /**
     2998     * Submit a URL to Google Indexing API
     2999     *
     3000     * @param string $url The URL to submit.
     3001     * @return array|WP_Error Response array with 'success' and 'message' keys, or WP_Error.
     3002     */
     3003    public function submit_url_to_google( $url ) {
     3004        // Check if enabled
     3005        $enabled = get_option( 'apicoid_gw_google_index_enabled', false );
     3006        if ( ! $enabled ) {
     3007            return new WP_Error( 'not_enabled', __( 'Auto Google Index is not enabled.', 'apicoid-ghostwriter' ) );
     3008        }
     3009
     3010        // Decrypt service account key
     3011        $json_string = $this->decrypt_service_account_key();
     3012        if ( false === $json_string ) {
     3013            return new WP_Error( 'decrypt_failed', __( 'Failed to decrypt service account key.', 'apicoid-ghostwriter' ) );
     3014        }
     3015
     3016        $service_account = json_decode( $json_string, true );
     3017        if ( empty( $service_account ) || empty( $service_account['client_email'] ) || empty( $service_account['private_key'] ) ) {
     3018            return new WP_Error( 'invalid_key', __( 'Service account key is invalid or incomplete.', 'apicoid-ghostwriter' ) );
     3019        }
     3020
     3021        // Create JWT
     3022        $jwt = $this->create_google_jwt( $service_account );
     3023        if ( false === $jwt ) {
     3024            return new WP_Error( 'jwt_failed', __( 'Failed to create JWT token.', 'apicoid-ghostwriter' ) );
     3025        }
     3026
     3027        // Get access token
     3028        $token_uri    = isset( $service_account['token_uri'] ) ? $service_account['token_uri'] : 'https://oauth2.googleapis.com/token';
     3029        $access_token = $this->get_google_access_token( $jwt, $token_uri );
     3030        if ( is_wp_error( $access_token ) ) {
     3031            return $access_token;
     3032        }
     3033
     3034        // Submit URL to Google Indexing API
     3035        $response = wp_remote_post(
     3036            'https://indexing.googleapis.com/v3/urlNotifications:publish',
     3037            array(
     3038                'headers' => array(
     3039                    'Content-Type'  => 'application/json',
     3040                    'Authorization' => 'Bearer ' . $access_token,
     3041                ),
     3042                'body'    => wp_json_encode(
     3043                    array(
     3044                        'url'  => $url,
     3045                        'type' => 'URL_UPDATED',
     3046                    )
     3047                ),
     3048                'timeout' => 30,
     3049            )
     3050        );
     3051
     3052        if ( is_wp_error( $response ) ) {
     3053            return $response;
     3054        }
     3055
     3056        $response_code = wp_remote_retrieve_response_code( $response );
     3057        $response_body = json_decode( wp_remote_retrieve_body( $response ), true );
     3058
     3059        if ( 200 === $response_code ) {
     3060            return array(
     3061                'success' => true,
     3062                'message' => __( 'URL submitted successfully to Google.', 'apicoid-ghostwriter' ),
     3063                'data'    => $response_body,
     3064            );
     3065        }
     3066
     3067        $error_msg = isset( $response_body['error']['message'] ) ? $response_body['error']['message'] : __( 'Unknown error from Google Indexing API.', 'apicoid-ghostwriter' );
     3068        return new WP_Error( 'google_api_error', $error_msg, array( 'status' => $response_code ) );
     3069    }
     3070
     3071    /**
     3072     * AJAX handler: Save Google Index settings
     3073     */
     3074    public function ajax_save_google_index_settings() {
     3075        check_ajax_referer( 'apicoid_gw_save_google_index_settings', 'nonce' );
     3076
     3077        if ( ! current_user_can( 'manage_options' ) ) {
     3078            wp_send_json_error( array( 'message' => __( 'You do not have permission to change these settings.', 'apicoid-ghostwriter' ) ) );
     3079        }
     3080
     3081        $enabled  = isset( $_POST['enabled'] ) ? rest_sanitize_boolean( sanitize_text_field( wp_unslash( $_POST['enabled'] ) ) ) : false;
     3082        $base_url = isset( $_POST['base_url'] ) ? sanitize_text_field( wp_unslash( $_POST['base_url'] ) ) : '';
     3083
     3084        // If disabling, clear settings
     3085        if ( ! $enabled ) {
     3086            update_option( 'apicoid_gw_google_index_enabled', false );
     3087            update_option( 'apicoid_gw_google_index_base_url', '' );
     3088            delete_option( 'apicoid_gw_google_index_service_account' );
     3089            wp_send_json_success( array( 'message' => __( 'Auto Google Index has been disabled.', 'apicoid-ghostwriter' ) ) );
     3090        }
     3091
     3092        // Validate base URL
     3093        $base_url = untrailingslashit( $base_url );
     3094        if ( empty( $base_url ) || ! filter_var( $base_url, FILTER_VALIDATE_URL ) ) {
     3095            wp_send_json_error( array( 'message' => __( 'Please enter a valid website URL (e.g. https://example.com).', 'apicoid-ghostwriter' ) ) );
     3096        }
     3097
     3098        if ( strpos( $base_url, 'https://' ) !== 0 ) {
     3099            wp_send_json_error( array( 'message' => __( 'Website URL must start with https://.', 'apicoid-ghostwriter' ) ) );
     3100        }
     3101
     3102        // Validate and encrypt service account key
     3103        $service_account_json = isset( $_POST['service_account_key'] ) ? sanitize_textarea_field( wp_unslash( $_POST['service_account_key'] ) ) : '';
     3104
     3105        // Only require new key if no existing key is stored
     3106        $existing_key = get_option( 'apicoid_gw_google_index_service_account', '' );
     3107        if ( empty( $service_account_json ) && ! empty( $existing_key ) ) {
     3108            // Keep existing key, just update other settings
     3109            update_option( 'apicoid_gw_google_index_enabled', true );
     3110            update_option( 'apicoid_gw_google_index_base_url', $base_url );
     3111            wp_send_json_success( array( 'message' => __( 'Settings saved successfully!', 'apicoid-ghostwriter' ) ) );
     3112        }
     3113
     3114        if ( empty( $service_account_json ) ) {
     3115            wp_send_json_error( array( 'message' => __( 'Please provide the service account key JSON.', 'apicoid-ghostwriter' ) ) );
     3116        }
     3117
     3118        $service_account = json_decode( $service_account_json, true );
     3119        if ( null === $service_account ) {
     3120            wp_send_json_error( array( 'message' => __( 'Invalid JSON format. Please paste valid service account key JSON.', 'apicoid-ghostwriter' ) ) );
     3121        }
     3122
     3123        // Validate required fields
     3124        $required_fields = array( 'client_email', 'private_key' );
     3125        foreach ( $required_fields as $field ) {
     3126            if ( empty( $service_account[ $field ] ) ) {
     3127                /* translators: %s: Missing field name in the service account JSON */
     3128                wp_send_json_error( array( 'message' => sprintf( __( 'Service account key is missing required field: %s', 'apicoid-ghostwriter' ), $field ) ) );
     3129            }
     3130        }
     3131
     3132        // Validate private key format
     3133        if ( strpos( $service_account['private_key'], '-----BEGIN' ) === false ) {
     3134            wp_send_json_error( array( 'message' => __( 'Invalid private key format in service account JSON.', 'apicoid-ghostwriter' ) ) );
     3135        }
     3136
     3137        // Encrypt and save
     3138        $encrypted = $this->encrypt_service_account_key( $service_account_json );
     3139        if ( false === $encrypted ) {
     3140            wp_send_json_error( array( 'message' => __( 'Failed to encrypt service account key. Please ensure OpenSSL is available.', 'apicoid-ghostwriter' ) ) );
     3141        }
     3142
     3143        update_option( 'apicoid_gw_google_index_enabled', true );
     3144        update_option( 'apicoid_gw_google_index_base_url', $base_url );
     3145        update_option( 'apicoid_gw_google_index_service_account', $encrypted );
     3146
     3147        wp_send_json_success( array( 'message' => __( 'Settings saved successfully! Service account key has been encrypted and stored.', 'apicoid-ghostwriter' ) ) );
     3148    }
     3149
     3150    /**
     3151     * AJAX handler: Test Google Index connection
     3152     */
     3153    public function ajax_test_google_index() {
     3154        check_ajax_referer( 'apicoid_gw_test_google_index', 'nonce' );
     3155
     3156        if ( ! current_user_can( 'manage_options' ) ) {
     3157            wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'apicoid-ghostwriter' ) ) );
     3158        }
     3159
     3160        $enabled = get_option( 'apicoid_gw_google_index_enabled', false );
     3161        if ( ! $enabled ) {
     3162            wp_send_json_error( array( 'message' => __( 'Please enable and save settings first before testing.', 'apicoid-ghostwriter' ) ) );
     3163        }
     3164
     3165        $base_url = get_option( 'apicoid_gw_google_index_base_url', '' );
     3166        if ( empty( $base_url ) ) {
     3167            wp_send_json_error( array( 'message' => __( 'No base URL configured. Please save settings first.', 'apicoid-ghostwriter' ) ) );
     3168        }
     3169
     3170        // Use the base URL itself as the test URL
     3171        $test_url = trailingslashit( $base_url );
     3172
     3173        $result = $this->submit_url_to_google( $test_url );
     3174
     3175        if ( is_wp_error( $result ) ) {
     3176            $this->log_google_index_submission( $test_url, 0, 'error', $result->get_error_message(), 'test' );
     3177            wp_send_json_error( array( 'message' => $result->get_error_message() ) );
     3178        }
     3179
     3180        $success_message = sprintf(
     3181            /* translators: %s: The URL that was submitted to Google Indexing API */
     3182            __( 'Connection successful! Test URL (%s) was submitted to Google Indexing API.', 'apicoid-ghostwriter' ),
     3183            $test_url
     3184        );
     3185
     3186        $this->log_google_index_submission( $test_url, 0, 'success', $success_message, 'test' );
     3187
     3188        wp_send_json_success(
     3189            array(
     3190                'message' => $success_message,
     3191                'data'    => $result['data'],
     3192            )
     3193        );
     3194    }
     3195
     3196    /**
     3197     * AJAX handler: Get Google Index logs with pagination
     3198     */
     3199    public function ajax_get_google_index_logs() {
     3200        check_ajax_referer( 'apicoid_gw_google_index_logs', 'nonce' );
     3201
     3202        if ( ! current_user_can( 'manage_options' ) ) {
     3203            wp_send_json_error( array( 'message' => __( 'You do not have permission to view logs.', 'apicoid-ghostwriter' ) ) );
     3204        }
     3205
     3206        global $wpdb;
     3207
     3208        $page     = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
     3209        $per_page = 15;
     3210        $offset   = ( $page - 1 ) * $per_page;
     3211
     3212        $table_name = $this->get_google_index_log_table_name();
     3213
     3214        // Ensure log table exists (in case plugin was not reactivated after update)
     3215        $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
     3216        if ( $table_exists !== $table_name ) {
     3217            $this->create_google_index_log_table();
     3218        }
     3219
     3220        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table, no WP API available.
     3221        $total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $table_name" );
     3222
     3223        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table with pagination, no WP API available.
     3224        $logs = $wpdb->get_results(
     3225            $wpdb->prepare(
     3226                "SELECT * FROM $table_name ORDER BY created_at DESC LIMIT %d OFFSET %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is generated internally.
     3227                $per_page,
     3228                $offset
     3229            ),
     3230            ARRAY_A
     3231        );
     3232
     3233        $total_pages = ceil( $total / $per_page );
     3234
     3235        wp_send_json_success(
     3236            array(
     3237                'logs'        => $logs,
     3238                'total'       => $total,
     3239                'page'        => $page,
     3240                'per_page'    => $per_page,
     3241                'total_pages' => $total_pages,
     3242            )
     3243        );
     3244    }
     3245
     3246    /**
     3247     * AJAX handler: Clear Google Index logs
     3248     */
     3249    public function ajax_clear_google_index_logs() {
     3250        check_ajax_referer( 'apicoid_gw_clear_google_index_logs', 'nonce' );
     3251
     3252        if ( ! current_user_can( 'manage_options' ) ) {
     3253            wp_send_json_error( array( 'message' => __( 'You do not have permission to clear logs.', 'apicoid-ghostwriter' ) ) );
     3254        }
     3255
     3256        global $wpdb;
     3257        $table_name = $this->get_google_index_log_table_name();
     3258
     3259        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Truncating custom table.
     3260        $wpdb->query( "TRUNCATE TABLE $table_name" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is generated internally, not user input.
     3261
     3262        wp_send_json_success( array( 'message' => __( 'All logs have been cleared.', 'apicoid-ghostwriter' ) ) );
     3263    }
     3264
     3265    /**
     3266     * Auto-submit URL to Google when a post/page is published or updated
     3267     *
     3268     * @param string  $new_status New post status.
     3269     * @param string  $old_status Old post status.
     3270     * @param WP_Post $post       Post object.
     3271     */
     3272    public function on_post_publish_update( $new_status, $old_status, $post ) {
     3273        // Only process when post becomes published or is updated while published
     3274        if ( 'publish' !== $new_status ) {
     3275            return;
     3276        }
     3277
     3278        // Only for posts and pages
     3279        $allowed_post_types = array( 'post', 'page' );
     3280        if ( ! in_array( $post->post_type, $allowed_post_types, true ) ) {
     3281            return;
     3282        }
     3283
     3284        // Check if auto index is enabled
     3285        $enabled = get_option( 'apicoid_gw_google_index_enabled', false );
     3286        if ( ! $enabled ) {
     3287            return;
     3288        }
     3289
     3290        // Get permalink
     3291        $permalink = get_permalink( $post->ID );
     3292        if ( empty( $permalink ) ) {
     3293            return;
     3294        }
     3295
     3296        // Submit URL to Google
     3297        $result = $this->submit_url_to_google( $permalink );
     3298
     3299        // Log the result
     3300        $is_success = ! is_wp_error( $result );
     3301        $message    = is_wp_error( $result ) ? $result->get_error_message() : $result['message'];
     3302        $this->log_google_index_submission(
     3303            $permalink,
     3304            $post->ID,
     3305            $is_success ? 'success' : 'error',
     3306            $message,
     3307            'auto'
     3308        );
     3309
     3310        // Also store in post meta for quick access
     3311        update_post_meta( $post->ID, '_apicoid_gw_google_index_status', array(
     3312            'url'       => $permalink,
     3313            'timestamp' => current_time( 'mysql' ),
     3314            'success'   => $is_success,
     3315            'message'   => $message,
     3316        ) );
    27003317    }
    27013318}
  • apicoid-ghostwriter/trunk/assets/css/admin.css

    r3457596 r3458138  
    433433}
    434434
     435/* Google Index Page Styles */
     436.apicoid-gw-google-index-actions {
     437    display: flex;
     438    gap: 10px;
     439    margin: 20px 0;
     440    padding-top: 10px;
     441}
     442
     443.apicoid-gw-google-index-actions .button {
     444    min-width: 140px;
     445    text-align: center;
     446}
     447
     448.apicoid-gw-service-key-wrapper textarea {
     449    font-family: monospace;
     450    font-size: 12px;
     451    resize: vertical;
     452}
     453
     454.apicoid-gw-key-status {
     455    margin-top: 8px;
     456}
     457
     458.apicoid-gw-key-status .dashicons {
     459    font-size: 16px;
     460    width: 16px;
     461    height: 16px;
     462    vertical-align: text-bottom;
     463}
     464
     465.apicoid-gw-index-log {
     466    margin-top: 20px;
     467    padding: 15px 20px;
     468    background: #f9f9f9;
     469    border: 1px solid #ccd0d4;
     470    border-radius: 3px;
     471}
     472
     473.apicoid-gw-index-log h3 {
     474    margin-top: 0;
     475    margin-bottom: 10px;
     476}
     477
     478.apicoid-gw-setup-steps {
     479    margin: 0;
     480    padding-left: 20px;
     481}
     482
     483.apicoid-gw-setup-steps li {
     484    margin-bottom: 8px;
     485    color: #555;
     486    line-height: 1.5;
     487}
     488
     489.apicoid-gw-setup-link {
     490    margin-top: 12px;
     491    margin-bottom: 0;
     492}
     493
     494.apicoid-gw-setup-link a {
     495    font-weight: 600;
     496}
     497
     498#apicoid-gw-google-index-notice {
     499    margin: 15px 0;
     500}
     501
     502#apicoid-gw-google-index-notice p {
     503    margin: 0.5em 0;
     504}
     505
     506/* Log Table Styles */
     507.apicoid-gw-log-header {
     508    display: flex;
     509    justify-content: space-between;
     510    align-items: center;
     511    margin-bottom: 5px;
     512}
     513
     514.apicoid-gw-log-header h2 {
     515    margin: 0;
     516}
     517
     518#apicoid-gw-clear-logs {
     519    text-decoration: none;
     520    border: none;
     521    background: none;
     522    color: #b32d2e;
     523    cursor: pointer;
     524    padding: 0;
     525    font-size: 13px;
     526}
     527
     528#apicoid-gw-clear-logs:hover {
     529    color: #a00;
     530}
     531
     532#apicoid-gw-clear-logs:disabled {
     533    opacity: 0.5;
     534    cursor: not-allowed;
     535}
     536
     537#apicoid-gw-log-table {
     538    margin-top: 12px;
     539}
     540
     541#apicoid-gw-log-table .column-status {
     542    width: 80px;
     543}
     544
     545#apicoid-gw-log-table .column-url {
     546    width: 30%;
     547}
     548
     549#apicoid-gw-log-table .column-source {
     550    width: 70px;
     551}
     552
     553#apicoid-gw-log-table .column-message {
     554    width: auto;
     555}
     556
     557#apicoid-gw-log-table .column-date {
     558    width: 150px;
     559}
     560
     561#apicoid-gw-log-table .column-url a {
     562    word-break: break-all;
     563}
     564
     565.apicoid-gw-log-badge {
     566    display: inline-block;
     567    padding: 3px 8px;
     568    border-radius: 3px;
     569    font-size: 11px;
     570    font-weight: 600;
     571    text-transform: uppercase;
     572    line-height: 1.4;
     573}
     574
     575.apicoid-gw-log-success {
     576    background: #46b450;
     577    color: #fff;
     578}
     579
     580.apicoid-gw-log-error {
     581    background: #dc3232;
     582    color: #fff;
     583}
     584
     585.apicoid-gw-log-source {
     586    display: inline-block;
     587    padding: 2px 6px;
     588    border-radius: 3px;
     589    font-size: 11px;
     590    font-weight: 500;
     591}
     592
     593.apicoid-gw-log-source-auto {
     594    background: #e5e5e5;
     595    color: #555;
     596}
     597
     598.apicoid-gw-log-source-test {
     599    background: #cce5ff;
     600    color: #004085;
     601}
     602
     603.apicoid-gw-log-postid {
     604    color: #999;
     605    font-size: 12px;
     606}
     607
     608.apicoid-gw-log-loading {
     609    text-align: center;
     610    padding: 20px !important;
     611    color: #666;
     612}
     613
     614.apicoid-gw-log-loading .spinner {
     615    margin-right: 5px;
     616}
     617
     618.apicoid-gw-log-empty {
     619    text-align: center;
     620    padding: 20px !important;
     621    color: #999;
     622    font-style: italic;
     623}
     624
     625#apicoid-gw-log-pagination {
     626    margin-top: 10px;
     627}
     628
     629#apicoid-gw-log-pagination .tablenav-pages {
     630    float: right;
     631}
     632
     633#apicoid-gw-log-pagination .displaying-num {
     634    margin-right: 10px;
     635    color: #666;
     636    font-style: italic;
     637}
     638
     639#apicoid-gw-log-pagination .paging-input {
     640    margin: 0 5px;
     641    font-size: 13px;
     642}
     643
     644#apicoid-gw-log-pagination .button {
     645    min-width: 28px;
     646    text-align: center;
     647    padding: 2px 6px;
     648    margin: 0 1px;
     649}
     650
     651#apicoid-gw-log-pagination .button.disabled {
     652    opacity: 0.5;
     653    cursor: default;
     654    pointer-events: none;
     655}
     656
  • apicoid-ghostwriter/trunk/readme.txt

    r3457596 r3458138  
    55Requires at least: 6.2
    66Tested up to: 6.9
    7 Stable tag: 1.3.0
     7Stable tag: 1.3.1
    88Requires PHP: 7.4
    99License: GPLv2 or later
     
    129129
    130130== Changelog ==
     131
     132= 1.3.1 =
     133* Auto Google Index: New feature to automatically submit post and page URLs to Google Indexing API when published or updated
     134* Google Indexing API integration: Full integration with Google's Indexing API using service account authentication (JWT/OAuth2)
     135* Submission log: Comprehensive logging system with database table to track all submission attempts (success/failure) with pagination
     136* Encrypted service account storage: Service account JSON keys are encrypted using AES-256-CBC before storing in database
     137* Test connection feature: Built-in test connection button to verify Google Indexing API credentials before enabling auto-submission
     138* Setup guide with tutorial link: Added step-by-step setup guide with link to full tutorial at https://api.co.id/blog/auto-index-ghostwriter/
     139* Auto-submit on publish/update: Automatically submits URLs to Google when posts or pages are published or updated (for post and page post types only)
     140* Log management: View submission history with status indicators, source tracking (auto/test), and clear logs functionality
    131141
    132142= 1.3.0 =
     
    180190== Upgrade Notice ==
    181191
     192= 1.3.1 =
     193This update introduces Auto Google Index feature that automatically submits your post and page URLs to Google Indexing API when published or updated. Includes comprehensive submission logging, encrypted service account storage, and test connection functionality. See the new "Auto Google Index" menu for setup instructions.
     194
    182195= 1.3.0 =
    183196This update introduces "Generate Article by Category" feature with AI-powered suggestions and automatic related article linking. Includes improved two-column modal layout for better workflow.
Note: See TracChangeset for help on using the changeset viewer.