db));$missing=[];$collations=[];$unsafe=[]; foreach($expected as $table){$r=$this->db->get_row($this->db->prepare('SELECT ENGINE,TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=%s',$table),ARRAY_A);if(!$r)$missing[]=$table;else {$collations[]=$r['ENGINE'].' / '.$r['TABLE_COLLATION'];if($r['ENGINE']!=='InnoDB')$unsafe[]=$table;}} return ['expected'=>count($expected),'actual'=>count($expected)-count($missing),'missing'=>$missing,'unsafe_engines'=>$unsafe,'collations'=>array_values(array_unique($collations)),'prefix'=>$this->db->prefix,'server'=>$this->db->db_server_info()]; } public function exists(string $suffix,string $where='1=1'): bool { if(!in_array($suffix,['sports','resources','opening_hours','price_lists','price_rules','memberships','bookings'],true))return false; $table=\ABeCe\Booking\Infrastructure\Database\Tables::name( $this->db, $suffix ); if(!$this->db->get_var($this->db->prepare('SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=%s',$table)))return false; return (bool)$this->db->get_var($this->db->prepare('SELECT EXISTS(SELECT 1 FROM %i WHERE '.$where.')',$table)); } public function hasPublicPrices(): bool { if(!$this->exists('price_lists')||!$this->exists('price_rules'))return false; return (bool)$this->db->get_var($this->db->prepare("SELECT EXISTS(SELECT 1 FROM %i r JOIN %i l ON l.id=r.price_list_id WHERE r.is_active=1 AND l.is_active=1 AND r.rule_type=%s)",\ABeCe\Booking\Infrastructure\Database\Tables::name( $this->db, 'price_rules' ),\ABeCe\Booking\Infrastructure\Database\Tables::name( $this->db, 'price_lists' ),'public_hourly_rate')); } public function dismiss(bool $value): void {update_user_meta(get_current_user_id(),'abece_booking_onboarding_dismissed',$value);} /** Explicit names only. DDL is not transactional: report partial results if a host refuses a DROP. */ public function delete(): array { if(!preg_match('/^[A-Za-z0-9_]+$/D',$this->db->prefix))throw new CatalogException('abece_booking_delete_failed',__('Plugin data could not be removed. Contact your administrator.','abece-booking'),500); $tables=array_keys(Schema::current($this->db));$lock=\ABeCe\Booking\Infrastructure\Database\Tables::lock($this->db,'schema'); if((string)$this->db->get_var($this->db->prepare('SELECT GET_LOCK(%s,5)',$lock))!=='1')throw new CatalogException('abece_booking_delete_busy',__('Another maintenance operation is running. Try again later.','abece-booking'),409); $removed=[];$failed=[]; try{ require_once ABSPATH.'wp-admin/includes/plugin.php';deactivate_plugins(['abece-booking/abece-booking.php','abece-booking-premium/abece-booking.php'],false,false); foreach($tables as $table){if(false===$this->db->query($this->db->prepare('DROP TABLE IF EXISTS %i',$table)))$failed[]=$table;else $removed[]=$table;} if($failed)return ['complete'=>false,'removed_tables'=>$removed,'failed_tables'=>$failed,'removed_options'=>[],'plugin_deactivated'=>true]; $removedOptions=[];$failedOptions=[];foreach(array_unique(array_merge(self::OPTIONS,array_values(\ABeCe\Booking\Support\LegacyIdentifiers::OPTIONS),array_keys(\ABeCe\Booking\Support\LegacyIdentifiers::OPTIONS),[\ABeCe\Booking\Support\LegacyIdentifiers::MARKER,\ABeCe\Booking\Infrastructure\Database\DatabaseState::JOURNAL,\ABeCe\Booking\Infrastructure\Database\DatabaseState::FRESH])) as $key){delete_option($key);if(get_option($key,null)!==null)$failedOptions[]=$key;else $removedOptions[]=$key;} if($failedOptions)return ['complete'=>false,'removed_tables'=>$removed,'failed_tables'=>[],'removed_options'=>$removedOptions,'failed_options'=>$failedOptions,'plugin_deactivated'=>true]; // Owned transient families carry random keys. Enumerate them; never drop wildcard tables. foreach(['_transient_abece_booking_member_import_','_transient_timeout_abece_booking_member_import_','_transient_abece_booking_link_rate_','_transient_timeout_abece_booking_link_rate_','_transient_abece_booking_test_email_','_transient_timeout_abece_booking_test_email_'] as $prefix)foreach($this->db->get_col($this->db->prepare('SELECT option_name FROM %i WHERE option_name LIKE %s',$this->db->options,$this->db->esc_like($prefix).'%')) as $key)delete_option($key); // Clear matching object-cache transients too, including scheduled import expiry arguments. foreach(_get_cron_array()?:[] as $stamp=>$hooks)foreach($hooks['abece_booking_member_import_expire']??[] as $event){$key=$event['args'][0]??'';if(is_string($key)&&str_starts_with($key,'abece_booking_member_import_'))delete_transient($key);} wp_unschedule_hook('abece_booking_member_import_expire'); foreach(['abece_booking_phone','abece_booking_onboarding_dismissed'] as $key)delete_metadata('user',0,$key,'',true); foreach(wp_roles()->roles as $name=>$role)foreach(array_merge(array_keys(\ABeCe\Booking\Support\LegacyIdentifiers::CAPS),array_values(\ABeCe\Booking\Support\LegacyIdentifiers::CAPS)) as $cap)wp_roles()->remove_cap($name,$cap); return ['complete'=>true,'removed_tables'=>$removed,'failed_tables'=>[],'removed_options'=>self::OPTIONS,'plugin_deactivated'=>true]; }finally{$this->db->get_var($this->db->prepare('SELECT RELEASE_LOCK(%s)',$lock));} } }