$headers ) { if ( 0 === strpos( (string) $file, 'silotree/' ) ) { continue; } $update_uri = isset( $headers['UpdateURI'] ) ? (string) $headers['UpdateURI'] : ''; if ( ( isset( $headers['TextDomain'] ) && 'silotree-pro' === $headers['TextDomain'] ) || false !== strpos( $update_uri, 'silotree.io/silotree-pro' ) || ( isset( $headers['Name'] ) && 'silotree pro' === strtolower( trim( (string) $headers['Name'] ) ) ) ) { return true; } } return false; } /** * Everything Silotree keeps on the current site. * * @param bool $pro_installed Whether Silotree Pro is still installed. */ function silotree_uninstall_site( $pro_installed ) { global $wpdb; $silotree_mode = get_option( 'silotree_uninstall_links', 'keep' ); if ( ! in_array( $silotree_mode, array( 'keep', 'strip', 'remove' ), true ) ) { $silotree_mode = 'keep'; } if ( 'keep' !== $silotree_mode ) { silotree_uninstall_rewrite_posts( $silotree_mode ); } // The copies of Elementor pages kept for undo. $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE %s", $wpdb->esc_like( '_silotree_el_backup_' ) . '%' ) ); // The copies of other builders' pages (Divi, Beaver Builder, Bricks, Thrive, ACF) kept for undo. $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE %s", $wpdb->esc_like( '_silotree_bk_' ) . '%' ) ); $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like( '_silotree_batch_revisions_' ) . '%' ) ); // Each post's target keywords, and when they were set. $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->postmeta} WHERE meta_key IN (%s, %s)", '_silotree_keywords', '_silotree_keywords_at' ) ); foreach ( array( 'silotree_site_key', 'silotree_bound_user', 'silotree_inbox', 'silotree_remote_apply', 'silotree_local_schema', 'silotree_scan_state', 'silotree_show_dashboard_widget', 'silotree_uninstall_links', 'silotree_app_url', 'silotree_last_map', 'silotree_map_pending', 'silotree_connected_at', 'silotree_paired_domain', 'silotree_signed_at', 'silotree_version', 'silotree_show_connect_notice', 'silotree_related_synced', 'silotree_never_link_sent', 'silotree_links_written', 'silotree_plan', 'silotree_trial_ended', 'silotree_ignore', 'silotree_review_done', 'silotree_rescued', 'silotree_setup_seen', 'silotree_click_counts', 'silotree_link_icons', 'silotree_clicks_daily', 'silotree_pages_flags', 'silotree_clicks_clear_pending', ) as $silotree_option ) { delete_option( $silotree_option ); } /* * Whatever else carries Silotree's name: settings from earlier versions (keyword rules lived * here until 0.6.0), and anything an add-on such as Silotree Pro saved. Left alone while that * add-on is still installed, because it is that plugin's data and deleting it is that plugin's * uninstall's job; with nothing else of Silotree's on the site, nothing of Silotree's is left. */ if ( ! $pro_installed ) { $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like( 'silotree_' ) . '%' ) ); foreach ( (array) _get_cron_array() as $silotree_events ) { foreach ( array_keys( (array) $silotree_events ) as $silotree_hook ) { if ( 0 === strpos( (string) $silotree_hook, 'silotree_' ) ) { wp_unschedule_hook( $silotree_hook ); } } } } // Transients and their expiry rows (_transient_timeout_silotree_*), the app's cached answers included. $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", $wpdb->esc_like( '_transient_silotree_' ) . '%', $wpdb->esc_like( '_transient_timeout_silotree_' ) . '%' ) ); $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}silotree_links" ); $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}silotree_pages" ); $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}silotree_related" ); $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}silotree_clicks" ); // This plugin's own events, whether or not Pro is installed. wp_clear_scheduled_hook( 'silotree_scan_batch' ); wp_clear_scheduled_hook( 'silotree_resolve_term_links' ); wp_unschedule_hook( 'silotree_map_posts' ); // Every cursor, not only the no-argument event. wp_clear_scheduled_hook( 'silotree_recompute_graph' ); wp_unschedule_hook( 'silotree_notify_retry' ); // Every path and body it was scheduled with. wp_unschedule_hook( 'silotree_api_refresh' ); wp_clear_scheduled_hook( 'silotree_related_sync' ); wp_clear_scheduled_hook( 'silotree_never_link_push' ); wp_clear_scheduled_hook( 'silotree_clicks_push' ); wp_clear_scheduled_hook( 'silotree_daily' ); } /** * The post types Silotree may have written into: every public type, attachments aside. Named * rather than 'any', so revisions, menus, templates and other plugins' private records are never * read on the way out. * * @return string[] */ function silotree_uninstall_post_types() { $types = array_values( array_diff( get_post_types( array( 'public' => true ) ), array( 'attachment' ) ) ); return $types ? $types : array( 'post', 'page' ); } /** * Walk the writing and honour the uninstall choice. * * Only posts whose content carries Silotree's mark are read, a hundred at a time by id, and each * change is written straight to the posts table and its cache cleared: no revision per post, no * save_post hooks for every other plugin to run, and no kses filter to strip an author's embeds on * multisite. The only change is removing Silotree's own marks or links. * * @param string $silotree_mode 'strip' or 'remove'. */ function silotree_uninstall_rewrite_posts( $silotree_mode ) { global $wpdb; require_once __DIR__ . '/includes/class-silotree-text.php'; require_once __DIR__ . '/includes/class-silotree-linker.php'; require_once __DIR__ . '/includes/class-silotree-elementor.php'; foreach ( array( 'store', 'store-elementor', 'store-divi', 'store-divi5', 'store-beaver', 'store-bricks', 'store-thrive', 'store-acf', 'builders' ) as $silotree_file ) { require_once __DIR__ . '/includes/class-silotree-' . $silotree_file . '.php'; } if ( function_exists( 'wp_raise_memory_limit' ) ) { wp_raise_memory_limit( 'admin' ); } if ( function_exists( 'set_time_limit' ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, Squiz.PHP.DiscouragedFunctions.Discouraged -- this uninstall walks every marked post, and a host that kills a long request would leave the rewrite half done. function_exists skips hosts that disable it; the @ is for the ones that warn anyway, and the walk continues either way. @set_time_limit( 0 ); } $types = silotree_uninstall_post_types(); $types_sql = implode( ',', array_fill( 0, count( $types ), '%s' ) ); $like_marked = '%' . $wpdb->esc_like( 'data-silotree' ) . '%'; $like_st = '%' . $wpdb->esc_like( 'data-st' ) . '%'; $batch = 100; // The post content. $cursor = 0; while ( true ) { $rows = $wpdb->get_results( $wpdb->prepare( "SELECT ID, post_content FROM {$wpdb->posts} WHERE ID > %d AND post_type IN ($types_sql) AND post_status NOT IN ('trash', 'auto-draft', 'inherit') AND ( post_content LIKE %s OR post_content LIKE %s ) ORDER BY ID ASC LIMIT %d", array_merge( array( $cursor ), $types, array( $like_marked, $like_st, $batch ) ) ) ); if ( ! $rows ) { break; } foreach ( $rows as $row ) { $cursor = (int) $row->ID; $original = (string) $row->post_content; $updated = 'remove' === $silotree_mode ? SiloTree_Linker::unwrap_ours( $original ) : SiloTree_Linker::strip_marks( $original ); if ( $updated !== $original ) { $wpdb->update( $wpdb->posts, array( 'post_content' => $updated ), array( 'ID' => $cursor ), array( '%s' ), array( '%d' ) ); clean_post_cache( $cursor ); } } if ( count( $rows ) < $batch ) { break; } } // Category and tag descriptions, where Silotree writes links too (0.8.2). $cursor = 0; while ( true ) { $rows = $wpdb->get_results( $wpdb->prepare( "SELECT term_taxonomy_id, term_id, taxonomy, description FROM {$wpdb->term_taxonomy} WHERE term_taxonomy_id > %d AND taxonomy IN ('category', 'post_tag') AND ( description LIKE %s OR description LIKE %s ) ORDER BY term_taxonomy_id ASC LIMIT %d", $cursor, $like_marked, $like_st, $batch ) ); if ( ! $rows ) { break; } foreach ( $rows as $row ) { $cursor = (int) $row->term_taxonomy_id; $original = (string) $row->description; $updated = 'remove' === $silotree_mode ? SiloTree_Linker::unwrap_ours( $original ) : SiloTree_Linker::strip_marks( $original ); if ( $updated !== $original ) { $wpdb->update( $wpdb->term_taxonomy, array( 'description' => $updated ), array( 'term_taxonomy_id' => $cursor ), array( '%s' ), array( '%d' ) ); clean_term_cache( (int) $row->term_id, (string) $row->taxonomy ); } } if ( count( $rows ) < $batch ) { break; } } // Elementor pages' own copy, where Silotree also writes its links. $cursor = 0; while ( true ) { $ids = array_map( 'intval', (array) $wpdb->get_col( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id > %d AND ( meta_value LIKE %s OR meta_value LIKE %s ) ORDER BY post_id ASC LIMIT %d", SiloTree_Elementor::DATA_META, $cursor, $like_marked, $like_st, $batch ) ) ); if ( ! $ids ) { break; } foreach ( $ids as $post_id ) { $cursor = $post_id; silotree_uninstall_elementor( $post_id, $silotree_mode ); } if ( count( $ids ) < $batch ) { break; } } /* * Every other builder's own copy (Beaver Builder, Bricks, Thrive Architect), ACF fields, and * Divi 5 pages, whose marks sit inside block JSON where the post_content pass above cannot * reach them. Found by the mark in a meta value other than Silotree's own, or in a Divi 5 page. */ $cursor = 0; while ( true ) { // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are $wpdb's own; every value is a placeholder. $ids = array_map( 'intval', (array) $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT id FROM ( SELECT post_id AS id FROM {$wpdb->postmeta} WHERE post_id > %d AND meta_key NOT LIKE %s AND meta_key <> %s AND meta_value LIKE %s UNION SELECT ID AS id FROM {$wpdb->posts} WHERE ID > %d AND post_content LIKE %s AND post_content LIKE %s ) AS marked ORDER BY id ASC LIMIT %d", $cursor, $wpdb->esc_like( '_silotree' ) . '%', SiloTree_Elementor::DATA_META, $like_marked, $cursor, '%' . $wpdb->esc_like( '