Changeset 3458138
- Timestamp:
- 02/10/2026 02:55:24 PM (6 weeks ago)
- Location:
- apicoid-ghostwriter
- Files:
-
- 6 added
- 6 edited
- 1 copied
-
tags/1.3.1 (copied) (copied from apicoid-ghostwriter/trunk)
-
tags/1.3.1/CHANGELOG.md (added)
-
tags/1.3.1/apicoid-ghostwriter.php (modified) (10 diffs)
-
tags/1.3.1/assets/css/admin.css (modified) (1 diff)
-
tags/1.3.1/assets/js/google-index.js (added)
-
tags/1.3.1/includes/google-index-page.php (added)
-
tags/1.3.1/readme.txt (modified) (3 diffs)
-
trunk/CHANGELOG.md (added)
-
trunk/apicoid-ghostwriter.php (modified) (10 diffs)
-
trunk/assets/css/admin.css (modified) (1 diff)
-
trunk/assets/js/google-index.js (added)
-
trunk/includes/google-index-page.php (added)
-
trunk/readme.txt (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
apicoid-ghostwriter/tags/1.3.1/apicoid-ghostwriter.php
r3457596 r3458138 4 4 * Plugin URI: https://wordpress.org/plugins/apicoid-ghostwriter/ 5 5 * 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. 06 * Version: 1.3.1 7 7 * Author: Api.co.id 8 8 * Author URI: https://api.co.id … … 21 21 22 22 // Define plugin constants 23 define( 'APICOID_GW_VERSION', '1.3. 0' );23 define( 'APICOID_GW_VERSION', '1.3.1' ); 24 24 define( 'APICOID_GW_DIR', plugin_dir_path( __FILE__ ) ); 25 25 define( 'APICOID_GW_URL', plugin_dir_url( __FILE__ ) ); … … 110 110 111 111 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 ); 112 127 113 128 // Enqueue admin scripts and styles … … 127 142 // Create database table 128 143 $this->create_database_table(); 144 145 // Create Google Index log table 146 $this->create_google_index_log_table(); 129 147 130 148 // Set default article generator settings … … 199 217 return $wpdb->prefix . 'apicoid_gw_posts'; 200 218 } 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 202 257 /** 203 258 * Plugin deactivation … … 457 512 ); 458 513 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 459 532 // Add settings section 460 533 add_settings_section( … … 2459 2532 // ); 2460 2533 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 2461 2544 // Add Settings submenu 2462 2545 add_submenu_page( … … 2520 2603 // Include settings page template 2521 2604 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'; 2522 2618 } 2523 2619 … … 2621 2717 // on sanitize_title() of the menu title which can change. 2622 2718 $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' ); 2624 2720 if ( ! in_array( $current_page, $our_pages, true ) ) { 2625 2721 return; … … 2698 2794 ); 2699 2795 } 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 ) ); 2700 3317 } 2701 3318 } -
apicoid-ghostwriter/tags/1.3.1/assets/css/admin.css
r3457596 r3458138 433 433 } 434 434 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 5 5 Requires at least: 6.2 6 6 Tested up to: 6.9 7 Stable tag: 1.3. 07 Stable tag: 1.3.1 8 8 Requires PHP: 7.4 9 9 License: GPLv2 or later … … 129 129 130 130 == 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 131 141 132 142 = 1.3.0 = … … 180 190 == Upgrade Notice == 181 191 192 = 1.3.1 = 193 This 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 182 195 = 1.3.0 = 183 196 This 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 4 4 * Plugin URI: https://wordpress.org/plugins/apicoid-ghostwriter/ 5 5 * 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. 06 * Version: 1.3.1 7 7 * Author: Api.co.id 8 8 * Author URI: https://api.co.id … … 21 21 22 22 // Define plugin constants 23 define( 'APICOID_GW_VERSION', '1.3. 0' );23 define( 'APICOID_GW_VERSION', '1.3.1' ); 24 24 define( 'APICOID_GW_DIR', plugin_dir_path( __FILE__ ) ); 25 25 define( 'APICOID_GW_URL', plugin_dir_url( __FILE__ ) ); … … 110 110 111 111 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 ); 112 127 113 128 // Enqueue admin scripts and styles … … 127 142 // Create database table 128 143 $this->create_database_table(); 144 145 // Create Google Index log table 146 $this->create_google_index_log_table(); 129 147 130 148 // Set default article generator settings … … 199 217 return $wpdb->prefix . 'apicoid_gw_posts'; 200 218 } 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 202 257 /** 203 258 * Plugin deactivation … … 457 512 ); 458 513 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 459 532 // Add settings section 460 533 add_settings_section( … … 2459 2532 // ); 2460 2533 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 2461 2544 // Add Settings submenu 2462 2545 add_submenu_page( … … 2520 2603 // Include settings page template 2521 2604 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'; 2522 2618 } 2523 2619 … … 2621 2717 // on sanitize_title() of the menu title which can change. 2622 2718 $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' ); 2624 2720 if ( ! in_array( $current_page, $our_pages, true ) ) { 2625 2721 return; … … 2698 2794 ); 2699 2795 } 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 ) ); 2700 3317 } 2701 3318 } -
apicoid-ghostwriter/trunk/assets/css/admin.css
r3457596 r3458138 433 433 } 434 434 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 5 5 Requires at least: 6.2 6 6 Tested up to: 6.9 7 Stable tag: 1.3. 07 Stable tag: 1.3.1 8 8 Requires PHP: 7.4 9 9 License: GPLv2 or later … … 129 129 130 130 == 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 131 141 132 142 = 1.3.0 = … … 180 190 == Upgrade Notice == 181 191 192 = 1.3.1 = 193 This 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 182 195 = 1.3.0 = 183 196 This 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.