ayudawp_load_includes(); $this->ayudawp_init_hooks(); } /** * Load include files */ private function ayudawp_load_includes() { require_once AYUDAWP_AISS_PLUGIN_DIR . 'includes/functions-helpers.php'; require_once AYUDAWP_AISS_PLUGIN_DIR . 'includes/class-icons.php'; require_once AYUDAWP_AISS_PLUGIN_DIR . 'includes/class-buttons.php'; require_once AYUDAWP_AISS_PLUGIN_DIR . 'includes/class-frontend.php'; require_once AYUDAWP_AISS_PLUGIN_DIR . 'includes/class-floating-bar.php'; require_once AYUDAWP_AISS_PLUGIN_DIR . 'includes/class-shortcode.php'; require_once AYUDAWP_AISS_PLUGIN_DIR . 'includes/class-admin.php'; require_once AYUDAWP_AISS_PLUGIN_DIR . 'includes/class-seo-integration.php'; require_once AYUDAWP_AISS_PLUGIN_DIR . 'includes/class-meta-box.php'; require_once AYUDAWP_AISS_PLUGIN_DIR . 'includes/class-ai-summary.php'; // Analytics system (v1.5.0). require_once AYUDAWP_AISS_PLUGIN_DIR . 'includes/class-database.php'; require_once AYUDAWP_AISS_PLUGIN_DIR . 'includes/class-rest-api.php'; require_once AYUDAWP_AISS_PLUGIN_DIR . 'includes/class-analytics-page.php'; require_once AYUDAWP_AISS_PLUGIN_DIR . 'includes/class-dashboard-widget.php'; require_once AYUDAWP_AISS_PLUGIN_DIR . 'includes/class-aiss-promo-banner.php'; } /** * Initialize hooks */ private function ayudawp_init_hooks() { add_action( 'init', array( $this, 'ayudawp_init' ) ); register_activation_hook( __FILE__, array( $this, 'ayudawp_activate' ) ); register_deactivation_hook( __FILE__, array( $this, 'ayudawp_deactivate' ) ); register_uninstall_hook( __FILE__, array( 'AyudaWP_AI_Share_Summarize', 'ayudawp_uninstall' ) ); // Daily cron for data retention purge. add_action( 'ayudawp_aiss_daily_purge', array( $this, 'ayudawp_run_data_purge' ) ); // Initialize classes. new AyudaWP_AISS_Admin(); new AyudaWP_AISS_Frontend(); new AyudaWP_AISS_Floating_Bar(); new AyudaWP_AISS_Shortcode(); new AyudaWP_AISS_Meta_Box(); new AyudaWP_AISS_AI_Summary(); // REST API endpoints. add_action( 'rest_api_init', array( 'AyudaWP_AISS_REST_API', 'ayudawp_register_routes' ) ); // Dashboard widget. add_action( 'wp_dashboard_setup', array( 'AyudaWP_AISS_Dashboard_Widget', 'ayudawp_register' ) ); // AJAX handler for VigIA tip dismiss. add_action( 'wp_ajax_aiss_dismiss_vigia_tip', array( $this, 'ayudawp_dismiss_vigia_tip' ) ); } /** * Plugin initialization */ public function ayudawp_init() { $this->ayudawp_check_version_update(); } /** * Check for version update and migrate settings if needed */ private function ayudawp_check_version_update() { $current_version = get_option( 'ayudawp_aiss_version', '1.0.9' ); if ( version_compare( $current_version, AYUDAWP_AISS_VERSION, '<' ) ) { $this->ayudawp_migrate_settings( $current_version ); update_option( 'ayudawp_aiss_version', AYUDAWP_AISS_VERSION ); // Create or update database tables on version upgrade. AyudaWP_AISS_Database::ayudawp_create_tables(); // Ensure daily purge cron is scheduled. if ( ! wp_next_scheduled( 'ayudawp_aiss_daily_purge' ) ) { wp_schedule_event( time(), 'daily', 'ayudawp_aiss_daily_purge' ); } } } /** * Migrate settings from older versions * * @param string $from_version Version migrating from. */ private function ayudawp_migrate_settings( $from_version ) { $options = get_option( 'ayudawp_aiss_options' ); if ( ! $options ) { return; } // Migration for v1.1.0: Add seo_button_type. if ( version_compare( $from_version, '1.1.0', '<' ) ) { if ( ! isset( $options['seo_button_type'] ) ) { $options['seo_button_type'] = 'link'; } } // Migration for v1.4.0: Add exclude_noindex option. if ( version_compare( $from_version, '1.4.0', '<' ) ) { if ( ! isset( $options['exclude_noindex'] ) ) { $options['exclude_noindex'] = true; } } // Migration for v1.6.0: Add new options, merge default/minimal styles. if ( version_compare( $from_version, '1.6.0', '<' ) ) { // Merge old 'default' style into 'minimal'. if ( isset( $options['button_colors'] ) && 'default' === $options['button_colors'] ) { $options['button_colors'] = 'minimal'; } if ( ! isset( $options['data_retention'] ) ) { $options['data_retention'] = 'forever'; } if ( ! isset( $options['button_size'] ) ) { $options['button_size'] = 'normal'; } if ( ! isset( $options['ai_section_title'] ) ) { $options['ai_section_title'] = ''; } if ( ! isset( $options['social_section_title'] ) ) { $options['social_section_title'] = ''; } if ( ! isset( $options['mastodon_instance'] ) ) { $options['mastodon_instance'] = 'mastodon.social'; } if ( ! isset( $options['custom_color_bg'] ) ) { $options['custom_color_bg'] = '#333333'; } if ( ! isset( $options['custom_color_text'] ) ) { $options['custom_color_text'] = '#ffffff'; } if ( ! isset( $options['button_custom_order'] ) ) { $options['button_custom_order'] = array(); } } // Migration for v2.0.0: AI Summary feature defaults. if ( version_compare( $from_version, '2.0.0', '<' ) ) { $summary_defaults = array( 'ai_summary_enabled' => true, 'ai_summary_use_extractive_fallback' => true, 'ai_summary_sentences' => 3, 'ai_summary_position' => 'before_buttons', 'ai_summary_collapsed_default' => true, 'ai_summary_frontend_button' => false, 'ai_summary_frontend_force_extractive' => true, ); foreach ( $summary_defaults as $key => $value ) { if ( ! isset( $options[ $key ] ) ) { $options[ $key ] = $value; } } // Seed the summary post-type list from the buttons list so the // feature works out of the box on the same content where buttons // are already enabled, without forcing the admin to re-pick. if ( ! isset( $options['ai_summary_content_types'] ) ) { $options['ai_summary_content_types'] = isset( $options['auto_insert_content_types'] ) && is_array( $options['auto_insert_content_types'] ) ? $options['auto_insert_content_types'] : array( 'post' ); } } // Migration for v2.2.0: collapse the two AI-summary booleans into a single // explicit mode, then drop the legacy keys. if ( version_compare( $from_version, '2.2.0', '<' ) ) { if ( ! isset( $options['ai_summary_mode'] ) ) { $was_enabled = ! isset( $options['ai_summary_enabled'] ) || ! empty( $options['ai_summary_enabled'] ); $was_fallback = ! isset( $options['ai_summary_use_extractive_fallback'] ) || ! empty( $options['ai_summary_use_extractive_fallback'] ); if ( ! $was_enabled ) { $options['ai_summary_mode'] = 'disabled'; } else { $options['ai_summary_mode'] = $was_fallback ? 'ai_fallback' : 'ai_only'; } } unset( $options['ai_summary_enabled'], $options['ai_summary_use_extractive_fallback'] ); } /* * Migration for v2.4.1: drop a prompt that only holds a copy of the * bundled default, so it goes back to being resolved from the language * pack on every request and the AI is asked for the summary in the * language of each reader. Activation used to store it, and the settings * field came pre-filled, so the first save froze it in the language of * the admin. A prompt the site wrote itself never matches any of the * bundled defaults and is left untouched, and so is an empty one, which * means send no instructions at all. */ if ( version_compare( $from_version, '2.4.1', '<' ) ) { if ( isset( $options['default_prompt'] ) && ayudawp_aiss_is_default_prompt( $options['default_prompt'] ) ) { unset( $options['default_prompt'] ); } } /* * Migration for v2.5.0: the three settings that governed where the * buttons appear collapse into one map of destination => position. * * Until 2.4.4 a post needed `display_locations` to contain "posts" AND * `auto_insert_content_types` to contain "post" AND * `auto_insert_position` to be something other than "disabled". The * first two listed overlapping things and nothing on screen said the * condition was an AND, so the usual outcome of ticking one of them was * no buttons and no explanation. * * What is migrated is the VISIBLE RESULT, not the literal settings: a * site with "Pages" ticked under locations but not under content types * was showing nothing on pages, so `page` is not written. Migrating the * settings literally would have made buttons appear where that site * never had them. The old keys are left in place as a rollback snapshot * (see the carry-over in ayudawp_aiss_validate_options()). */ if ( version_compare( $from_version, '2.5.0', '<' ) && ! isset( $options['placements'] ) ) { $options['placements'] = self::ayudawp_derive_placements_from_legacy( $options ); } /* * Same move for the summary: its content-type list and its single * position become one map, so a site can put the summary before the * content on pages and next to the buttons on posts. */ if ( version_compare( $from_version, '2.5.0', '<' ) ) { $summary_options = get_option( 'ayudawp_aiss_summary_options' ); if ( is_array( $summary_options ) && ! isset( $summary_options['ai_summary_placements'] ) ) { $summary_options['ai_summary_placements'] = self::ayudawp_derive_summary_placements_from_legacy( $summary_options ); update_option( 'ayudawp_aiss_summary_options', $summary_options ); } } // Persist before the 2.3.0 block: its derivation helper reads this // option back from the database, so earlier in-memory migrations // (mode materialization above) must already be stored. update_option( 'ayudawp_aiss_options', $options ); /* * Migration for v2.3.0: the AI-summary settings move to their own * option, `ayudawp_aiss_summary_options`, behind the new AI Summary * settings tab. COPY, do not move: the `ai_summary_*` keys stay in * `ayudawp_aiss_options` as a frozen snapshot so a rollback to 2.2.x * finds its configuration intact (deleted by the 2.6.0 migration * below). The derivation also materializes the * old read-time inheritance of the buttons' content-type list, which * 2.3.0 removes from the runtime resolver. */ if ( version_compare( $from_version, '2.3.0', '<' ) ) { if ( false === get_option( 'ayudawp_aiss_summary_options' ) ) { add_option( 'ayudawp_aiss_summary_options', ayudawp_aiss_derive_summary_options_from_legacy() ); } $this->ayudawp_migrate_summary_exclusion_meta(); } /* * Migration for v2.6.0: delete that frozen `ai_summary_*` snapshot. * After eight releases of the 2.3+ series with no migration problem * reported, going back to 2.2.x is not worth the weight these keys add * to an autoloaded option. Runs after the 2.3.0 block, so a site * coming straight from 2.2.x gets its summary option first, and only * when that option exists: until then ayudawp_aiss_get_summary_options() * still reads these keys. */ if ( version_compare( $from_version, '2.6.0', '<' ) && is_array( get_option( 'ayudawp_aiss_summary_options' ) ) ) { $current = get_option( 'ayudawp_aiss_options' ); if ( is_array( $current ) ) { $kept = array(); foreach ( $current as $key => $value ) { if ( 0 !== strpos( (string) $key, 'ai_summary' ) ) { $kept[ $key ] = $value; } } if ( count( $kept ) !== count( $current ) ) { update_option( 'ayudawp_aiss_options', $kept ); } } } } /** * Turn the pre-2.5.0 location settings into the unified placements map * * Static and side-effect free so the release harness can call it with * fabricated inputs and check every combination. * * @since 2.5.0 * @param array $options Stored options, pre-migration. * @return array Destination key => position. */ public static function ayudawp_derive_placements_from_legacy( $options ) { $position = isset( $options['auto_insert_position'] ) ? $options['auto_insert_position'] : 'after_content'; // "Shortcode only" becomes an empty map: no destination is ticked. if ( 'disabled' === $position ) { return array(); } if ( ! in_array( $position, array( 'before_content', 'after_content', 'both' ), true ) ) { $position = 'after_content'; } $locations = isset( $options['display_locations'] ) && is_array( $options['display_locations'] ) ? $options['display_locations'] : array(); $types = isset( $options['auto_insert_content_types'] ) && is_array( $options['auto_insert_content_types'] ) ? $options['auto_insert_content_types'] : array(); $placements = array(); foreach ( $types as $type ) { $type = (string) $type; if ( 'post' === $type ) { $gate = in_array( 'posts', $locations, true ); } elseif ( 'page' === $type ) { $gate = in_array( 'pages', $locations, true ); } else { $gate = in_array( 'cpt', $locations, true ); } if ( $gate ) { $placements[ $type ] = $position; } } if ( in_array( 'home', $locations, true ) ) { $placements['home'] = $position; } if ( in_array( 'archives', $locations, true ) ) { $placements['archive'] = $position; } return $placements; } /** * Turn the pre-2.5.0 summary settings into its placements map * * @since 2.5.0 * @param array $options Stored summary options, pre-migration. * @return array Post type => position. */ public static function ayudawp_derive_summary_placements_from_legacy( $options ) { $types = isset( $options['ai_summary_content_types'] ) && is_array( $options['ai_summary_content_types'] ) ? $options['ai_summary_content_types'] : array(); if ( empty( $types ) ) { return array(); } $position = isset( $options['ai_summary_position'] ) ? $options['ai_summary_position'] : 'before_buttons'; // "Do not display" kept generating summaries, and so does its // replacement, which is a position of its own rather than an off switch. if ( 'disabled' === $position ) { $position = 'hidden'; } if ( ! in_array( $position, array( 'before_buttons', 'after_buttons', 'before_content', 'hidden' ), true ) ) { $position = 'before_buttons'; } $placements = array(); foreach ( $types as $type ) { $placements[ (string) $type ] = $position; } return $placements; } /** * Materialize the per-post summary exclusion for posts excluded pre-2.3.0 * * Before 2.3.0 the single "Hide share buttons" checkbox silently hid the * AI summary too. 2.3.0 splits that into two independent controls, so * every post that had the old meta gets the new summary-exclusion meta * stamped once here — existing exclusions keep behaving exactly as they * did, while both controls become editable independently from now on. * * Runs once from the 2.3.0 migration. add_post_meta() with $unique keeps * it idempotent and refreshes each post's meta cache as it goes. * * @since 2.3.0 */ private function ayudawp_migrate_summary_exclusion_meta() { global $wpdb; // Direct lookup on purpose: it must cover every post status and every // post type (WP_Query would need fragile any-status/any-type gymnastics // for a one-time upgrade sweep this small). // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- one-time 2.3.0 upgrade sweep over our own meta key, result consumed immediately. $post_ids = $wpdb->get_col( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value = '1'", '_ayudawp_aiss_exclude' ) ); foreach ( $post_ids as $post_id ) { add_post_meta( (int) $post_id, '_ayudawp_aiss_exclude_summary', '1', true ); } } /** * Plugin activation */ public function ayudawp_activate() { // `default_prompt` is left out on purpose: it is resolved at read time // from the language pack, so storing it here would freeze it in one // language. Translations are not even loaded during activation, which // is what made the old code store plain English. $default_options = array( 'enabled_buttons' => array( 'chatgpt', 'claude', 'twitter', 'linkedin' ), // One map since 2.5.0: destination => position. 'placements' => array( 'post' => 'after_content' ), 'twitter_handle' => '', 'mastodon_instance' => '', 'custom_text' => '', 'title_text' => '', 'title_style' => 'span', 'ai_section_title' => '', 'social_section_title' => '', 'button_order' => 'ayudawp_ai_first', 'button_alignment' => 'left', 'button_colors' => 'minimal', 'button_size' => 'normal', 'custom_color_bg' => '#333333', 'custom_color_text' => '#ffffff', 'dark_mode_adaptation' => 'disabled', 'show_icons' => false, 'icon_style' => 'circular', 'delete_data_on_uninstall' => false, 'data_retention' => 'forever', 'seo_button_type' => 'link', 'exclude_noindex' => true, 'button_custom_order' => array(), // v2.5.0. All three off by default, so a fresh install looks like // every previous version until the admin turns them on. 'floating_bar_placements' => array(), 'floating_bar_autohide' => true, 'floating_bar_hide_mobile' => true, 'floating_bar_hide_with_inline' => false, // Compact mode on by default: a phone screen with fifteen buttons // is the complaint this setting exists to answer. 'mobile_compact' => 'single', 'mobile_compact_visible' => 3, 'utm_enabled' => false, 'utm_campaign' => '', ); // AI-summary settings live in their own option since 2.3.0. $default_summary_options = array( 'ai_summary_mode' => 'ai_fallback', 'ai_summary_sentences' => 3, 'ai_summary_position' => 'before_buttons', 'ai_summary_collapsed_default' => true, 'ai_summary_frontend_button' => false, 'ai_summary_frontend_force_extractive' => true, 'ai_summary_content_types' => array( 'post' ), ); add_option( 'ayudawp_aiss_options', $default_options ); add_option( 'ayudawp_aiss_summary_options', $default_summary_options ); update_option( 'ayudawp_aiss_version', AYUDAWP_AISS_VERSION ); set_transient( 'ayudawp_aiss_just_activated', true, 60 ); // Create analytics database tables. AyudaWP_AISS_Database::ayudawp_create_tables(); // Schedule daily data retention purge. if ( ! wp_next_scheduled( 'ayudawp_aiss_daily_purge' ) ) { wp_schedule_event( time(), 'daily', 'ayudawp_aiss_daily_purge' ); } } /** * Plugin deactivation: unschedule cron */ public function ayudawp_deactivate() { $timestamp = wp_next_scheduled( 'ayudawp_aiss_daily_purge' ); if ( $timestamp ) { wp_unschedule_event( $timestamp, 'ayudawp_aiss_daily_purge' ); } // Clear any pending async summary generation events. wp_clear_scheduled_hook( 'ayudawp_aiss_generate_summary_async' ); } /** * Cron callback: purge old analytics data based on retention setting */ public function ayudawp_run_data_purge() { AyudaWP_AISS_Database::ayudawp_purge_old_data(); } /** * Plugin uninstallation */ public static function ayudawp_uninstall() { $options = get_option( 'ayudawp_aiss_options' ); $delete_data = isset( $options['delete_data_on_uninstall'] ) ? $options['delete_data_on_uninstall'] : false; // Always unschedule cron on uninstall. $timestamp = wp_next_scheduled( 'ayudawp_aiss_daily_purge' ); if ( $timestamp ) { wp_unschedule_event( $timestamp, 'ayudawp_aiss_daily_purge' ); } if ( $delete_data ) { delete_option( 'ayudawp_aiss_options' ); delete_option( 'ayudawp_aiss_summary_options' ); delete_option( 'ayudawp_aiss_version' ); delete_option( 'ayudawp_aiss_db_version' ); delete_option( 'ayudawp_aiss_activation_notice_dismissed' ); delete_option( 'ayudawp_aiss_vigia_tip_dismissed' ); delete_option( 'ayudawp_aiss_last_ai_error' ); delete_transient( 'ayudawp_aiss_just_activated' ); // Clean up post meta. global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- postmeta cleanup on uninstall, not cached by design. $wpdb->delete( $wpdb->postmeta, array( 'meta_key' => '_ayudawp_aiss_exclude' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- postmeta cleanup on uninstall, not cached by design. $wpdb->delete( $wpdb->postmeta, array( 'meta_key' => '_ayudawp_aiss_exclude_summary' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- postmeta cleanup on uninstall, not cached by design. $wpdb->delete( $wpdb->postmeta, array( 'meta_key' => '_ayudawp_aiss_summary' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- postmeta cleanup on uninstall, not cached by design. $wpdb->delete( $wpdb->postmeta, array( 'meta_key' => '_ayudawp_aiss_summary_provider' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- postmeta cleanup on uninstall, not cached by design. $wpdb->delete( $wpdb->postmeta, array( 'meta_key' => '_ayudawp_aiss_summary_hash' ) ); // Drop analytics table. $table_name = $wpdb->prefix . 'aiss_clicks'; $table_name_safe = esc_sql( $table_name ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- DROP on own table at uninstall, name is escaped. $wpdb->query( "DROP TABLE IF EXISTS {$table_name_safe}" ); } } /** * AJAX handler: Dismiss VigIA tip notice */ public function ayudawp_dismiss_vigia_tip() { check_ajax_referer( 'aiss_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error(); } update_option( 'ayudawp_aiss_vigia_tip_dismissed', true ); wp_send_json_success(); } } // Initialize the plugin. AyudaWP_AI_Share_Summarize::get_instance();