deleteDataOnUninstall(); return (bool) apply_filters('DayTidy/F__DayTidy/Plugin/DataRetention__should_delete/status', $delete, $blog_id); } public static function cleanup(int $blog_id = 1): void { self::clearScheduledEvents(); self::deletePosts(); self::deleteWooProducts(); self::deletePostMeta(); self::deleteTerms(); self::deleteTermMeta(); self::deleteWooOrderItemMeta(); self::dropTables(); self::deleteOptions(); do_action('DayTidy/A__DayTidy/Plugin/DataRetention__cleanup/done', $blog_id); } public static function clearScheduledEvents(): void { foreach (self::scheduledHooks() as $hook) { if (!$hook) continue; wp_clear_scheduled_hook($hook); } } /** * @return string[] */ public static function tables(): array { return self::pluginTableSuffixes(self::uniqueStrings(apply_filters( 'DayTidy/F__DayTidy/Plugin/DataRetention__tables/tables', CreateTables::tables() ))); } /** * @return string[] */ public static function postTypes(): array { $post_types = array_merge([ Offering::PT, Booking::PT, Coupon::PT, Notification::PT, ViewPost::PT, ], Entity::postTypes()); return self::uniqueStrings(apply_filters('DayTidy/F__DayTidy/Plugin/DataRetention__post_types/post_types', $post_types)); } /** * @return string[] */ public static function taxonomies(): array { $taxonomies = array_merge([ Category::TAX, Host::TAX, Label::TAX, Location::TAX, 'daytidy-offering-presenter', 'daytidy-offering-sponsor', Tag::TAX, ], Entity::taxonomies()); return self::uniqueStrings(apply_filters('DayTidy/F__DayTidy/Plugin/DataRetention__taxonomies/taxonomies', $taxonomies)); } /** * @return string[] */ private static function scheduledHooks(): array { $hooks = array_merge(array_keys(Hooks::crons()), [ OfferingDataIndexer::CRON_HOOK, OfferingVisibility::CRON_HOOK, ]); return self::uniqueStrings(apply_filters('DayTidy/F__DayTidy/Plugin/DataRetention__scheduled_hooks/hooks', $hooks)); } private static function deletePosts(): void { foreach (self::postTypes() as $post_type) { $ids = get_posts([ 'post_type' => $post_type, 'post_status' => ['any', 'trash'], 'fields' => 'ids', 'posts_per_page' => -1, 'orderby' => 'ID', 'order' => 'ASC', 'no_found_rows' => true, // Retention cleanup must remove every DayTidy-owned post regardless of third-party query filters. // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters 'suppress_filters' => true, ]); foreach ((array) $ids as $id) { wp_delete_post((int) $id, true); } } } private static function deleteWooProducts(): void { $ids = get_posts([ 'post_type' => 'product', 'post_status' => ['any', 'trash'], 'fields' => 'ids', 'posts_per_page' => -1, 'orderby' => 'ID', 'order' => 'ASC', 'no_found_rows' => true, // Retention cleanup must remove every DayTidy-owned product regardless of third-party query filters. // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters 'suppress_filters' => true, 'meta_key' => 'daytidy_option', ]); foreach ((array) $ids as $id) { wp_delete_post((int) $id, true); } } private static function deleteTerms(): void { foreach (self::taxonomies() as $taxonomy) { if (function_exists('taxonomy_exists') && !taxonomy_exists($taxonomy) && function_exists('register_taxonomy')) { register_taxonomy($taxonomy, [], ['public' => false]); } $terms = get_terms([ 'taxonomy' => $taxonomy, 'hide_empty' => false, 'fields' => 'ids', ]); if (function_exists('is_wp_error') && is_wp_error($terms)) continue; foreach ((array) $terms as $term_id) { wp_delete_term((int) $term_id, $taxonomy); } } } private static function deletePostMeta(): void { $db = new Db(); self::deleteByPrefixes($db, $db->table('postmeta'), 'meta_key', self::metaKeyPrefixes()); } private static function deleteTermMeta(): void { $db = new Db(); self::deleteByPrefixes($db, $db->table('termmeta'), 'meta_key', self::metaKeyPrefixes()); } private static function deleteWooOrderItemMeta(): void { $db = new Db(); $table = $db->table('woocommerce_order_itemmeta'); if (!self::tableExists($db, $table)) return; self::deleteByPrefixes($db, $table, 'meta_key', self::metaKeyPrefixes()); } private static function dropTables(): void { $db = new Db(); foreach (array_reverse(self::tables()) as $table) { $db->query("DROP TABLE IF EXISTS `#__{$table}`"); } } private static function deleteOptions(): void { $db = new Db(); self::deleteByPrefixes($db, $db->table('options'), 'option_name', self::optionNamePrefixes()); if (function_exists('is_multisite') && is_multisite()) { self::deleteByPrefixes($db, $db->table('sitemeta', true), 'meta_key', self::siteOptionNamePrefixes()); } } /** * @return string[] */ private static function metaKeyPrefixes(): array { return self::uniqueStrings(apply_filters('DayTidy/F__DayTidy/Plugin/DataRetention__meta_key_prefixes/prefixes', [ 'daytidy_', ])); } /** * @return string[] */ private static function optionNamePrefixes(): array { return self::uniqueStrings(apply_filters('DayTidy/F__DayTidy/Plugin/DataRetention__option_name_prefixes/prefixes', [ 'daytidy_', '_transient_daytidy_', '_transient_timeout_daytidy_', '_site_transient_daytidy_', '_site_transient_timeout_daytidy_', ])); } /** * @return string[] */ private static function siteOptionNamePrefixes(): array { return self::uniqueStrings(apply_filters('DayTidy/F__DayTidy/Plugin/DataRetention__site_option_name_prefixes/prefixes', [ 'daytidy_', '_site_transient_daytidy_', '_site_transient_timeout_daytidy_', ])); } /** * @param string[] $prefixes */ private static function deleteByPrefixes(Db $db, string $table, string $column, array $prefixes): void { if (!count($prefixes)) return; if (!self::canDeleteFromTable($db, $table) || !self::canDeleteFromColumn($column)) return; $clauses = []; $values = []; $quoted_column = self::quoteIdentifier($column); foreach ($prefixes as $prefix) { $clauses[] = "{$quoted_column} LIKE %s"; $values[] = $db->escLike($prefix) . '%'; } $db->query($db->prepare( "DELETE FROM " . self::quoteIdentifier($table) . " WHERE " . implode(' OR ', $clauses), $values )); } /** * @param string[] $tables * @return string[] */ private static function pluginTableSuffixes(array $tables): array { $allowed = []; foreach ($tables as $table) { if (!self::isPluginTableSuffix($table)) continue; $allowed[] = $table; } return $allowed; } private static function isPluginTableSuffix(string $table): bool { return preg_match('/\Adaytidy_[a-z0-9_]+\z/', $table) === 1; } private static function canDeleteFromTable(Db $db, string $table): bool { if (!self::isSqlIdentifier($table)) return false; return in_array($table, [ $db->table('postmeta'), $db->table('termmeta'), $db->table('woocommerce_order_itemmeta'), $db->table('options'), $db->table('sitemeta', true), ], true); } private static function canDeleteFromColumn(string $column): bool { return in_array($column, [ 'meta_key', 'option_name', ], true); } private static function isSqlIdentifier(string $identifier): bool { return preg_match('/\A[A-Za-z0-9_]+\z/', $identifier) === 1; } private static function quoteIdentifier(string $identifier): string { return '`' . $identifier . '`'; } private static function tableExists(Db $db, string $table): bool { return (string) $db->getVar($db->prepare('SHOW TABLES LIKE %s', $table)) === $table; } /** * @param mixed $values * @return string[] */ private static function uniqueStrings($values): array { $strings = []; foreach ((array) $values as $value) { if (!is_string($value)) continue; $value = trim($value); if ($value === '') continue; $strings[$value] = true; } return array_keys($strings); } }