'notification_name' => 'google-analytics-v3-deprecation', 'title' => __( 'Universal Analytics V3 Deprecation Notice', 'all-in-one-seo-pack' ), 'content' => sprintf( // Translators: 1 - Line break HTML tags, 2 - Plugin short name ("AIOSEO"), Analytics plugin name (e.g. "MonsterInsights"). __( 'You have been using the %2$s Google Analytics V3 (Universal Analytics) integration which has been deprecated by Google and is no longer supported. This may affect your website\'s data accuracy and performance.%1$sTo ensure a seamless analytics experience, we recommend migrating to %3$s, a powerful analytics solution.%1$s%3$s offers advanced features such as real-time tracking, enhanced e-commerce analytics, and easy-to-understand reports, helping you make informed decisions to grow your online presence effectively.%1$sClick the button below to be redirected to the %3$s setup process, where you can start benefiting from its robust analytics capabilities immediately.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded '

', AIOSEO_PLUGIN_SHORT_NAME, $pluginName ), 'type' => 'error', 'level' => [ 'all' ], 'button1_label' => __( 'Fix Now', 'all-in-one-seo-pack' ), 'button1_action' => admin_url( 'admin.php?page=aioseo-monsterinsights' ), 'start' => gmdate( 'Y-m-d H:i:s' ) ] ); } /** * Adds a notification for the query arg monitor. * * @since 4.5.8 * * @return void */ private function addQueryArgMonitorNotification() { $options = $this->getRawOptions(); if ( empty( $options['searchAppearance']['advanced']['crawlCleanup']['enable'] ) || empty( $options['searchAppearance']['advanced']['crawlCleanup']['removeUnrecognizedQueryArgs'] ) ) { return; } $notification = Models\Notification::getNotificationByName( 'crawl-cleanup-updated' ); if ( $notification->exists() ) { return; } Models\Notification::addNotification( [ 'slug' => uniqid(), 'notification_name' => 'crawl-cleanup-updated', 'title' => __( 'Crawl Cleanup changes you should know about', 'all-in-one-seo-pack' ), 'content' => __( 'We\'ve made some significant changes to how we monitor Query Args for our Crawl Cleanup feature. Instead of DISABLING all query args and requiring you to add individual exceptions, we\'ve now changed it to ALLOW all query args by default with the option to easily block unrecognized ones through our new log table.', 'all-in-one-seo-pack' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded 'type' => 'info', 'level' => [ 'all' ], 'button1_label' => __( 'Learn More', 'all-in-one-seo-pack' ), 'button1_action' => 'http://route#aioseo-search-appearance&aioseo-scroll=aioseo-query-arg-monitoring&aioseo-highlight=aioseo-query-arg-monitoring:advanced', 'start' => gmdate( 'Y-m-d H:i:s' ) ] ); } /** * Deprecates the "No Pagination for Canonical URLs" setting. * * @since 4.5.9 * * @return void */ public function deprecateNoPaginationForCanonicalUrlsSetting() { $options = $this->getRawOptions(); if ( empty( $options['searchAppearance']['advanced']['noPaginationForCanonical'] ) ) { return; } $deprecatedOptions = aioseo()->internalOptions->deprecatedOptions; if ( ! in_array( 'noPaginationForCanonical', $deprecatedOptions, true ) ) { $deprecatedOptions[] = 'noPaginationForCanonical'; aioseo()->internalOptions->deprecatedOptions = $deprecatedOptions; } aioseo()->options->deprecated->searchAppearance->advanced->noPaginationForCanonical = true; } /** * Deprecates the "Breadcrumbs enabled" setting. * * @since 4.6.5 * * @return void */ public function deprecateBreadcrumbsEnabledSetting() { $options = $this->getRawOptions(); if ( ! isset( $options['breadcrumbs']['enable'] ) || 1 === intval( $options['breadcrumbs']['enable'] ) ) { return; } $deprecatedOptions = aioseo()->internalOptions->deprecatedOptions; if ( ! in_array( 'breadcrumbsEnable', $deprecatedOptions, true ) ) { $deprecatedOptions[] = 'breadcrumbsEnable'; aioseo()->internalOptions->deprecatedOptions = $deprecatedOptions; } aioseo()->options->deprecated->breadcrumbs->enable = false; } /** * Cancels all outstanding sitemap ping actions. * This is needed because we've removed the Ping class. * * @since 4.7.5 * * @return void */ private function cancelScheduledSitemapPings() { as_unschedule_all_actions( 'aioseo_sitemap_ping' ); as_unschedule_all_actions( 'aioseo_sitemap_ping_recurring' ); } /** * Disable email reports. * * @since 4.7.7 * * @return void */ private function disableEmailReports() { aioseo()->options->advanced->emailSummary->enable = false; // Schedule a notification to remind the user to enable email reports in 2 weeks. aioseo()->actionScheduler->scheduleSingle( 'aioseo_email_reports_enable_reminder', 2 * WEEK_IN_SECONDS ); } /** * Cancels all occurrences of the report summary task. * This is needed in order to force the scheduled date to be reset. * * @since 4.7.9 * * @return void */ private function rescheduleEmailReport() { as_unschedule_all_actions( aioseo()->emailReports->summary->actionHook ); } /** * Fixes headlines that could not be analyzed. * * @since 4.7.9 * * @return void */ private function fixSavedHeadlines() { $headlines = aioseo()->internalOptions->internal->headlineAnalysis->headlines; if ( empty( $headlines ) ) { return; } foreach ( $headlines as $key => $headline ) { if ( ! json_decode( $headline ) ) { unset( $headlines[ $key ] ); } } aioseo()->internalOptions->internal->headlineAnalysis->headlines = $headlines; } /** * Resets the image scan date in order to force a new scan. * This is needed because we're now storing relative URLs in order to support site migrations. * * @since 4.8.3 * * @return void */ private function resetImageScanDate() { aioseo()->core->db->update( 'aioseo_posts' ) ->set( [ 'image_scan_date' => null ] ) ->run(); } /** * Migrate the SeoAnalyzer homepage results from the Internal Optinos to the new table. * * @since 4.8.3 * * @return void */ private function migrateSeoAnalyzerResults() { $internalOptions = $this->getRawInternalOptions(); $results = ! empty( $internalOptions['internal']['siteAnalysis']['results'] ) ? $internalOptions['internal']['siteAnalysis']['results'] : []; if ( empty( $results ) ) { return; } $parsedData = [ 'results' => is_string( $results ) ? json_decode( $results, true ) : $results, 'score' => $internalOptions['internal']['siteAnalysis']['score'], ]; Models\SeoAnalyzerResult::addResults( $parsedData ); aioseo()->core->cache->delete( 'analyze_site_code' ); aioseo()->core->cache->delete( 'analyze_site_body' ); } /** * Migrate the SeoAnalyzer competitors results from the Internal Optinos to the new table. * * @since 4.8.3 * * @return void */ private function migrateSeoAnalyzerCompetitors() { $internalOptions = $this->getRawInternalOptions(); $competitors = ! empty( $internalOptions['internal']['siteAnalysis']['competitors'] ) ? $internalOptions['internal']['siteAnalysis']['competitors'] : []; if ( empty( $competitors ) ) { return; } foreach ( $competitors as $url => $competitor ) { $parsedData = is_string( $competitor ) ? json_decode( $competitor, true ) : $competitor; $results = empty( $parsedData['results'] ) ? [] : $parsedData['results']; if ( empty( $results ) ) { continue; } Models\SeoAnalyzerResult::addResults( [ 'results' => $results, 'score' => $parsedData['score'], ], $url ); } aioseo()->core->cache->delete( 'analyze_site_code' ); aioseo()->core->cache->delete( 'analyze_site_body' ); } /** * Returns the raw options from the database. * * @since 4.8.3 * * @return array */ private function getRawInternalOptions() { // Options from the DB. $rawInternalOptions = get_option( aioseo()->internalOptions->optionsName ); $internalOptions = is_string( $rawInternalOptions ) ? json_decode( $rawInternalOptions, true ) : $rawInternalOptions; if ( empty( $internalOptions ) ) { $internalOptions = []; } return $internalOptions; } /** * Adds the column index for the cornerstone content table. * * @since 4.8.7 * * @return void */ private function addColumnIndexForCornerstoneContent() { if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'pillar_content' ) || aioseo()->core->db->indexExists( 'aioseo_posts', 'ndx_aioseo_posts_pillar_content' ) ) { return; } $tableName = aioseo()->core->db->db->prefix . 'aioseo_posts'; aioseo()->core->db->execute( "ALTER TABLE {$tableName} ADD INDEX ndx_aioseo_posts_pillar_content (pillar_content)" ); } /** * Synchronizes database schema with defined schema using dbDelta. * * This method uses WordPress's dbDelta() function to automatically: * - Create tables that don't exist * - Add missing columns to existing tables * - Modify column definitions that have changed * * Note: dbDelta CANNOT drop columns or rename columns. Those operations * must be handled separately with custom SQL in version-gated migrations. * * @since 4.9.7 * * @return void */ private function updateDbSchema() { if ( ! function_exists( 'dbDelta' ) ) { require_once ABSPATH . 'wp-admin/includes/upgrade.php'; } // Ensure the cache table is properly cleaned before running dbDelta. // This is a safety net for cases where PreUpdates couldn't acquire its lock // (e.g. concurrent requests) and the table still has the old schema with rows. // Without this, dbDelta adds the 'name' column with default '' to all existing rows, // causing "Duplicate entry" errors when adding the UNIQUE KEY. aioseo()->preUpdates->createCacheTable(); aioseo()->preUpdates->createCrawlCleanupLogsTable(); // Get all schema definitions and run dbDelta $schemas = aioseo()->dbSchema->getSchema(); dbDelta( $schemas ); // Clear schema cache so columnExists/tableExists work correctly aioseo()->core->cache->delete( 'db_schema' ); } /** * Downloads the spell checker dictionary for the current locale if needed. * * @since 5.0.0 * * @return void */ private function downloadDictionaries() { if ( ! aioseo()->options->advanced->spellChecker ) { return; } $dictionary = new \AIOSEO\Plugin\Common\SpellChecker\Dictionary(); $supportedLocales = $dictionary->getSpellCheckableLocales(); $locale = get_locale(); if ( ! in_array( $locale, $supportedLocales, true ) ) { return; } if ( ! $dictionary->needsDownload( $locale ) ) { return; } $result = $dictionary->downloadForLocale( $locale ); if ( is_wp_error( $result ) ) { $notification = Models\Notification::getNotificationByName( 'spell-checker-dictionary-download-failed' ); if ( ! $notification->exists() ) { Models\Notification::addNotification( [ 'slug' => uniqid(), 'notification_name' => 'spell-checker-dictionary-download-failed', 'title' => __( 'Spell Checker Dictionary Download Failed', 'all-in-one-seo-pack' ), 'content' => __( 'We were unable to download the spell checker dictionary for your language. You can try again from the Settings page.', 'all-in-one-seo-pack' ), 'type' => 'warning', 'level' => [ 'all' ], 'button1_label' => __( 'Go to Settings', 'all-in-one-seo-pack' ), 'button1_action' => 'http://route#aioseo-settings:advanced', 'start' => gmdate( 'Y-m-d H:i:s' ) ] ); } } } /** * Migrates sensitive values from Options/InternalOptions to the new SensitiveOptions storage. * * @since 4.9.5 * * @return void */ private function migrateSensitiveOptions() { // Migrate from InternalOptions. $rawInternalOptions = json_decode( (string) get_option( 'aioseo_options_internal', '' ), true ); if ( ! is_array( $rawInternalOptions ) ) { $rawInternalOptions = []; } $internalMappings = [ 'connectLicenseKey' => [ 'internal', 'connectLicenseKey' ], 'aiAccessToken' => [ 'internal', 'ai', 'accessToken' ], 'semrushAccessToken' => [ 'integrations', 'semrush', 'accessToken' ], 'semrushRefreshToken' => [ 'integrations', 'semrush', 'refreshToken' ], 'searchStatisticsTrustToken' => [ 'internal', 'searchStatistics', 'trustToken' ], 'siteAnalysisConnectToken' => [ 'internal', 'siteAnalysis', 'connectToken' ] ]; $internalOptionsChanged = false; foreach ( $internalMappings as $newKey => $path ) { $value = aioseo()->helpers->getNestedValue( $rawInternalOptions, $path ); if ( ! empty( $value ) && is_string( $value ) ) { aioseo()->sensitiveOptions->set( $newKey, $value ); // Remove the old value from the internal options. $this->unsetNestedValue( $rawInternalOptions, $path ); $internalOptionsChanged = true; } } // Migrate search statistics profile key and token from the profile array. $profile = aioseo()->helpers->getNestedValue( $rawInternalOptions, [ 'internal', 'searchStatistics', 'profile' ] ); if ( is_array( $profile ) ) { if ( ! empty( $profile['key'] ) && is_string( $profile['key'] ) ) { aioseo()->sensitiveOptions->set( 'searchStatisticsProfileKey', $profile['key'] ); unset( $rawInternalOptions['internal']['searchStatistics']['profile']['key'] ); $internalOptionsChanged = true; } if ( ! empty( $profile['token'] ) && is_string( $profile['token'] ) ) { aioseo()->sensitiveOptions->set( 'searchStatisticsProfileToken', $profile['token'] ); unset( $rawInternalOptions['internal']['searchStatistics']['profile']['token'] ); $internalOptionsChanged = true; } } if ( $internalOptionsChanged ) { update_option( 'aioseo_options_internal', wp_json_encode( $rawInternalOptions ), false ); } // Migrate Lite connect key/token. if ( ! aioseo()->pro ) { $rawLiteInternalOptions = json_decode( (string) get_option( 'aioseo_options_internal_lite', '' ), true ); if ( is_array( $rawLiteInternalOptions ) ) { $liteOptionsChanged = false; $connectKey = aioseo()->helpers->getNestedValue( $rawLiteInternalOptions, [ 'internal', 'connect', 'key' ] ); if ( ! empty( $connectKey ) && is_string( $connectKey ) ) { aioseo()->sensitiveOptions->set( 'connectKey', $connectKey ); unset( $rawLiteInternalOptions['internal']['connect']['key'] ); $liteOptionsChanged = true; } $connectToken = aioseo()->helpers->getNestedValue( $rawLiteInternalOptions, [ 'internal', 'connect', 'token' ] ); if ( ! empty( $connectToken ) && is_string( $connectToken ) ) { aioseo()->sensitiveOptions->set( 'connectToken', $connectToken ); unset( $rawLiteInternalOptions['internal']['connect']['token'] ); $liteOptionsChanged = true; } if ( $liteOptionsChanged ) { update_option( 'aioseo_options_internal_lite', wp_json_encode( $rawLiteInternalOptions ), false ); } } } // Force save the sensitive options. aioseo()->sensitiveOptions->save( true ); } /** * Removes leaked `key` and `token` from the Search Statistics profile array. * * The 4.9.6 sensitive-options migration unset these on the raw DB option, but the * in-memory InternalOptions model (already loaded at boot) re-saved them on shutdown * because `searchStatistics.profile` was a generic `array` leaf — its inner keys * weren't filtered against the schema. The schema is now structured, so a forced * save strips legacy subkeys automatically; backfill sensitive options first in case * the prior migration missed them. * * @since 4.9.7 * * @return void */ private function cleanupSearchStatisticsProfile() { $rawInternalOptions = json_decode( (string) get_option( 'aioseo_options_internal', '' ), true ); $profile = is_array( $rawInternalOptions ) ? aioseo()->helpers->getNestedValue( $rawInternalOptions, [ 'internal', 'searchStatistics', 'profile' ] ) : null; if ( is_array( $profile ) ) { if ( ! empty( $profile['key'] ) && is_string( $profile['key'] ) && ! aioseo()->sensitiveOptions->hasValue( 'searchStatisticsProfileKey' ) ) { aioseo()->sensitiveOptions->set( 'searchStatisticsProfileKey', $profile['key'] ); } if ( ! empty( $profile['token'] ) && is_string( $profile['token'] ) && ! aioseo()->sensitiveOptions->hasValue( 'searchStatisticsProfileToken' ) ) { aioseo()->sensitiveOptions->set( 'searchStatisticsProfileToken', $profile['token'] ); } aioseo()->sensitiveOptions->save( true ); } // Force-save internal options so the structured `profile` schema strips legacy subkeys. aioseo()->internalOptions->save( true ); } /** * Unsets a nested value in an array by path. * * @since 4.9.6 * * @param array $array The array to modify (passed by reference). * @param array $path The path to the value. * @return void */ private function unsetNestedValue( &$array, $path ) { $lastKey = array_pop( $path ); $current = &$array; foreach ( $path as $key ) { if ( ! isset( $current[ $key ] ) || ! is_array( $current[ $key ] ) ) { return; } $current = &$current[ $key ]; } unset( $current[ $lastKey ] ); } }