generated from jric11/baseProject
ChatGPT's update to harden software and run db updates.
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/** Members see their own tickets; assigned club administrators see that club's tickets. */
|
||||
class ClubTickets extends RosterService {
|
||||
public const SUBJECT_LIMIT=200;
|
||||
public const MESSAGE_LIMIT=8000;
|
||||
private function text(string $value,int $limit,string $label): string {
|
||||
if(!mb_check_encoding($value,'UTF-8')||str_contains($value,"\0"))throw new AppException($label.' must contain valid text.');
|
||||
$value=trim($value);
|
||||
if(preg_match('/\A[\s\p{Z}]*\z/u',$value))throw new AppException($label.' is required.');
|
||||
if(mb_strlen($value,'UTF-8')>$limit)throw new AppException($label.' must be '.$limit.' characters or fewer.');
|
||||
return $value;
|
||||
}
|
||||
public function normalizeSearch(string $search): string {
|
||||
$search=trim($search);return $search===''?'':$this->text($search,100,'Ticket search');
|
||||
}
|
||||
public function page(int $club,int $page=1,string $search=''): array {
|
||||
$this->club($club);if($page<1)throw new AppException('Choose a valid ticket page.');
|
||||
$search=$this->normalizeSearch($search);$manage=$this->manages($club);$args=$manage?[$club]:[$club,$_SESSION[self::USER_SERIAL]??0];
|
||||
$scope=' WHERE club_serial=?'.($manage?'':' AND user_serial=?');
|
||||
if($search!==''){$scope.=" AND subject LIKE ? ESCAPE '!'";$args[]='%'.str_replace(['!','%','_'],['!!','!%','!_'],$search).'%';}
|
||||
$db=$this->connect();$stmt=$db->prepare('SELECT COUNT(*) FROM club_tickets'.$scope);$stmt->execute($args);$count=(int)$stmt->fetchColumn();
|
||||
$paging=Pagination::calculate($count,25,$page);$pages=max(1,(int)ceil($count/$paging['size']));
|
||||
$stmt=$db->prepare('SELECT ticket_serial,subject,ticket_status,user_serial,created_at FROM club_tickets'.$scope.' ORDER BY ticket_serial DESC LIMIT '.$paging['size'].' OFFSET '.$paging['offset']);$stmt->execute($args);
|
||||
return $paging+['rows'=>$stmt->fetchAll(),'count'=>$count,'pages'=>$pages,'search'=>$search];
|
||||
}
|
||||
public function listing(int $club,int $page=1,string $search=''): array {return $this->page($club,$page,$search)['rows'];}
|
||||
public function ticket(int $club,int $id,?PDO $db=null): array {
|
||||
$this->club($club,$db);$stmt=($db??$this->connect())->prepare('SELECT * FROM club_tickets WHERE club_serial=? AND ticket_serial=?');$stmt->execute([$club,$id]);$row=$stmt->fetch();
|
||||
if(!$row||(!$this->manages($club,$db)&&(int)$row['user_serial']!==(int)($_SESSION[self::USER_SERIAL]??0)))throw new AppException('Ticket not found.',404);return $row;
|
||||
}
|
||||
public function messages(int $club,int $id,?int $page=null): array {
|
||||
$this->ticket($club,$id);return BoundedList::rows($this->connect(),'SELECT m.message_text,m.created_at,u.user_first_name,u.user_last_name FROM club_ticket_messages m JOIN users u ON u.user_serial=m.author_serial WHERE ticket_serial=? ORDER BY message_serial',[$id],$page,'messages');
|
||||
}
|
||||
public function createTicket(int $club,string $subject,string $message): int {
|
||||
$subject=$this->text($subject,self::SUBJECT_LIMIT,'Subject');$message=$this->text($message,self::MESSAGE_LIMIT,'Message');
|
||||
if(!(new AuthService())->allowAttempt('club-ticket',(string)($_SESSION[self::USER_SERIAL]??0),20,3600))throw new AppException('Too many tickets. Try again later.',429);
|
||||
(new ClubResources())->consume($club,'ticket');
|
||||
$db=$this->connect();return DatabaseTransaction::run($db,function(PDO $db)use($club,$subject,$message){
|
||||
$selected=$this->club($club,$db);$stmt=$db->prepare('SELECT club_status FROM clubs WHERE club_serial=? FOR UPDATE');$stmt->execute([$club]);if($stmt->fetchColumn()!=='active')throw new AppException('This club is archived.',409);
|
||||
$this->club($club,$db);$stmt=$db->prepare('INSERT INTO club_tickets(club_serial,user_serial,subject) VALUES(?,?,?)');$stmt->execute([$club,$_SESSION[self::USER_SERIAL],$subject]);$id=(int)$db->lastInsertId();
|
||||
$stmt=$db->prepare('INSERT INTO club_ticket_messages(ticket_serial,author_serial,message_text) VALUES(?,?,?)');$stmt->execute([$id,$_SESSION[self::USER_SERIAL],$message]);Audit::required($db,(int)$_SESSION[self::USER_SERIAL],'opened club ticket '.json_encode(['club'=>$club,'ticket'=>$id]));return $id;
|
||||
});
|
||||
}
|
||||
public function reply(int $club,int $id,string $message,string $status=''): void {
|
||||
$message=$this->text($message,self::MESSAGE_LIMIT,'Message');if(!in_array($status,['','open','closed'],true))throw new AppException('Invalid ticket status.');
|
||||
if(!(new AuthService())->allowAttempt('ticket-reply',(string)($_SESSION[self::USER_SERIAL]??0),60,3600))throw new AppException('Too many replies. Try again later.',429);
|
||||
$this->ticket($club,$id);(new ClubResources())->consume($club,'reply');
|
||||
$db=$this->connect();DatabaseTransaction::run($db,function(PDO $db)use($club,$id,$message,$status){
|
||||
$stmt=$db->prepare('SELECT club_status FROM clubs WHERE club_serial=? FOR UPDATE');$stmt->execute([$club]);if($stmt->fetchColumn()!=='active')throw new AppException('This club is archived.',409);
|
||||
$row=$this->ticket($club,$id,$db);if($status!=='')(new TenantAccess())->requireManager($club,$db);
|
||||
if($row['ticket_status']==='closed'&&$status!=='open')throw new AppException('A club administrator must reopen the ticket before replying.',409);
|
||||
$stmt=$db->prepare('INSERT INTO club_ticket_messages(ticket_serial,author_serial,message_text) VALUES(?,?,?)');$stmt->execute([$id,$_SESSION[self::USER_SERIAL],$message]);
|
||||
$stmt=$db->prepare('UPDATE club_tickets SET ticket_status=?,updated_at=CURRENT_TIMESTAMP WHERE ticket_serial=?');$stmt->execute([$status?:$row['ticket_status'],$id]);Audit::required($db,(int)$_SESSION[self::USER_SERIAL],'replied to club ticket '.json_encode(['club'=>$club,'ticket'=>$id,'status'=>$status?:$row['ticket_status']]));
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user