[],'members'=>[],'contacts'=>[],'ambiguous'=>false,'verified'=>false];} public static function requestId(string $email):int { $id=is_string($_POST['id']??null)?filter_var(wp_unslash($_POST['id']),FILTER_VALIDATE_INT):false; $security=is_string($_POST['security']??null)?sanitize_text_field(wp_unslash($_POST['security'])):''; $request=$id?wp_get_user_request($id):false; if(!$request)return 0; $export=$request->action_name==='export_personal_data';$cap=$export?'export_others_personal_data':'erase_others_personal_data'; $action=($export?'wp-privacy-export-personal-data-':'wp-privacy-erase-personal-data-').$id; if(!current_user_can($cap)||!wp_verify_nonce($security,$action))return 0; return $request&&in_array($request->action_name,['export_personal_data','remove_personal_data'],true)&&strcasecmp($request->email,$email)===0?(int)$id:0; } public function resolve(string $email,int $request=0):array { $i=self::empty();if(!is_email($email))return $i; if($request){$approval=get_post_meta($request,'abece_booking_privacy_identity',true);$native=wp_get_user_request($request); if($native&&$native->confirmed_timestamp&&in_array($native->status,['request-confirmed','request-completed'],true)&&strcasecmp($native->email,$email)===0&&is_array($approval)&&hash_equals($approval['email_hash'],hash('sha256',strtolower($email)))&&$approval['expires']>time())return array_intersect_key($approval,$i)+$i; } global $wpdb;$ids=$wpdb->get_col($wpdb->prepare('SELECT ID FROM %i WHERE user_email=%s ORDER BY ID',$wpdb->users,$email));$i['users']=array_map('intval',$ids);if(count($ids)>1)$i['ambiguous']=true; $members=$this->r->rows('members','email=%s OR wp_user_id IN ('.R::in($i['users']).')',[$email],'id,wp_user_id,email'); $contacts=$this->r->rows('external_contacts','email=%s',[$email],'id'); // A member explicitly linked to the matching user belongs to the same proven identity. $components=count($i['users']);foreach($members as $m){$i['members'][]=(int)$m['id'];if(!$m['wp_user_id']||!in_array((int)$m['wp_user_id'],$i['users'],true))$components++;if($m['wp_user_id']&&!in_array((int)$m['wp_user_id'],$i['users'],true))$i['ambiguous']=true;} // Self-service DOB/member-number knowledge is not proof of ownership of a different mailbox. foreach($i['users'] as $uid)foreach(\ABeCe\Booking\Support\LegacyIdentifiers::user_metadata($uid,'abece_booking_membership_link_audit') as $event){if(!is_array($event)||!in_array($event['method']??'',['email_dob','member_number_dob'],true))continue;foreach($members as $member)if((int)$member['id']===(int)($event['member_id']??0)&&(int)$member['wp_user_id']===$uid&&(!$member['email']||strcasecmp(trim($member['email']),trim($email))!==0))$i['ambiguous']=true;} foreach($contacts as $c){$i['contacts'][]=(int)$c['id'];$components++;} $i['ambiguous']=$i['ambiguous']||$components>1;return $i; } /** Detect a relink or changed identity between native AJAX pages, including explicit manual selections. */ public function fingerprint(array $i):string { $parts=[];foreach($i['users'] as $uid){$u=get_userdata($uid);$parts['users'][$uid]=$u?$u->user_email:null;} foreach(['members'=>'members','contacts'=>'external_contacts'] as $key=>$table)$parts[$key]=$this->r->rows($table,'id IN ('.R::in($i[$key]).')',[],$key==='members'?'id,wp_user_id,email':'id,email'); return hash('sha256',wp_json_encode($parts)); } public function approve(int $request,array $input):array { if(!current_user_can('manage_privacy_options')||!current_user_can('export_others_personal_data')||!current_user_can('erase_others_personal_data'))throw new \ABeCe\Booking\Domain\CatalogException('abece_booking_forbidden',__('You do not have permission to manage privacy requests.','abece-booking'),403); BookingValidator::fields($input,['users','members','contacts','confirmed']); $native=wp_get_user_request($request);if(!$native||!in_array($native->status,['request-confirmed','request-completed'],true)||!$native->confirmed_timestamp||($input['confirmed']??false)!==true)CatalogValidator::invalid('request_id',__('Confirm the WordPress request and verify this person’s authority before selecting their records.','abece-booking')); $i=self::empty();foreach(['users','members','contacts'] as $key){$values=$input[$key]??[];if(!is_array($values)||!array_is_list($values)||count($values)>10)CatalogValidator::invalid($key,__('Select only records belonging to the verified person.','abece-booking'));foreach($values as $id){$id=CatalogValidator::integer($id,$key,1,PHP_INT_MAX);if($key==='users'?(!get_userdata($id)&&!$this->r->count('bookings','customer_user_id=%d OR created_by_user_id=%d',[$id,$id])&&!$this->r->count('credit_accounts','user_id=%d',[$id])&&!$this->r->count('payment_transactions','user_id=%d',[$id])):!$this->r->count($key==='contacts'?'external_contacts':'members','id=%d',[$id]))CatalogValidator::invalid($key,__('The selected record does not exist.','abece-booking'));$i[$key][]=$id;}$i[$key]=array_values(array_unique($i[$key]));} if(!$i['users']&&!$i['members']&&!$i['contacts'])CatalogValidator::invalid('request_id',__('Select at least one verified record.','abece-booking')); // Explicit administrator review, bound to one confirmed request, not a global email alias. $i['verified']=true;$approval=$i+['email_hash'=>hash('sha256',strtolower($native->email)),'reviewed_by'=>get_current_user_id(),'reviewed_at'=>time(),'expires'=>time()+DAY_IN_SECONDS]; wp_update_post(['ID'=>$request,'post_status'=>'request-confirmed']);delete_post_meta($request,'_wp_user_request_completed_timestamp'); delete_post_meta($request,'abece_booking_privacy_review_required');update_post_meta($request,'abece_booking_privacy_identity',$approval);wp_schedule_single_event($approval['expires'],'abece_booking_privacy_plan_expire',[$request]);delete_post_meta($request,'abece_booking_privacy_export_plan');return ['verified'=>true,'expires'=>$approval['expires']]; } }