*/ class ParTCP_Events_Messages { static function process_votings_list( $eventId, $votings, $sender ){ // helper function for event-definition and event-update-request global $Events, $FileSystem, $ServerData, $Timestamp, $Votings; $Votings->set_base_dir( $Events->get_dir( $eventId ) ); $results = []; foreach ( $votings as $key => $voting ){ $hasChanged = FALSE; $pos = $key + 1; if ( isset( $voting['period_start'] ) && ( ! ( $start = strtotime( $voting['period_start'] ) ) || $start < time() ) ){ $results[] = "#{$pos}: " . strtoupper( _('error') ) . '-' . _('Invalid period start'); continue; } if ( isset( $voting['period_end'] ) && ( ! ( $end = strtotime( $voting['period_end'] ) ) || $end < time() || $end <= $start ) ){ $results[] = "#{$pos}: " . strtoupper( _('error') ) . '-' . _('Invalid period end'); continue; } if ( isset( $voting['id'] ) ){ // update existing voting $msgType = 'voting-update-request'; $votingData = $Votings->get_data( $voting['id'] ); if ( ! $votingData ){ $results[] = "#{$pos}: " . strtoupper( _('error') ) . '-' . sprintf( _('Voting %s does not exist'), $voting['id'] ); continue; } if ( $votingData['status'] != 'idle' ){ $results[] = "#{$pos}: " . strtoupper( _('error') ) . '-' . sprintf( _('Voting %s cannot be updated anymore'), $voting['id'] ); continue; } $hasChanged = ptcp_update_object( $votingData, $Votings->purge_data( $voting ) ); if ( $hasChanged ){ $votingData['modified_on'] = date( 'c', $Timestamp ); $votingData['modified_by'] = $sender; $results[] = "#{$pos}: " . sprintf( _('Voting %s updated successfully'), $voting['id'] ); } else { $results[] = "#{$pos}: " . sprintf( _('Voting %s has not been changed'), $voting['id'] ); } } else { // create new voting $msgType = 'voting-definition'; $votingId = strtolower( $voting['name'] ); $votingData = $Votings->get_data( $votingId ); if ( $votingData ){ $results[] = "#{$pos}: " . strtoupper( _('error') ) . '-' . sprintf( _('A voting named "%s" already exists'), $voting['name'] ); continue; } $votingData = [ 'id' => $votingId, 'created_on' => date( 'c', $Timestamp ), 'created_by' => $sender, 'modified_on' => null, 'modified_by' => null, 'status' => 'idle', ]; $votingData = array_merge( $votingData, $Votings->purge_data( $voting ) ); $results[] = "#{$pos}: " . sprintf( _('Voting %s created successfully'), $votingData['id'] ); $hasChanged = TRUE; } if ( ! $hasChanged ){ continue; } $localReceipt = [ 'From' => $ServerData['name'], 'Date' => date( 'c', $Timestamp ), 'Message-Type' => "surrogate-{$msgType}", 'Event-Id' => $eventId, 'Voting-Data' => $votingData, ]; $dir = $Votings->get_dir( $votingData['id'] ); $fileName = date( 'Ymd-His', $Timestamp ) . "-surrogate-{$msgType}"; $FileSystem->put_contents( "{$dir}/{$fileName}", ptcp_yaml_emit( $localReceipt ) ); } return $results; } static function handle_event_definition( $message, $receipt ){ global $Events, $FileSystem, $Groups, $Partcp, $Shortcoder, $ServerData, $Timestamp; $eventData = $message->get('Event-Data'); if ( empty( $eventData['name'] ) || empty( $eventData['estimated_turnout'] ) ){ $receipt->set_rejection( 21, _('Incomplete event data') ); return $receipt->dump( TRUE ); } if ( ! $Events->validate_fields( $eventData ) ){ $receipt->set_rejection( 22, sprintf( _('Invalid value for Event-Data field %s'), $Events->lastInvalidField ) ); return $receipt->dump( TRUE ); } if ( empty( $eventData['date'] ) ){ $eventData['date'] = date('Y-m-d'); $date = time(); } else { $date = strtotime( $eventData['date'] ); } $object = [ 'type' => 'server', 'id' => NULL, 'dir' => '/' ]; $groupId = $message->get('Group-Id'); if ( $groupId && is_object( $Groups ) ){ if ( ! $Groups->get_data( $groupId ) ){ $receipt->set_rejection( 41, _('Group does not exist') ); return $receipt->dump( TRUE ); } $object = [ 'type' => 'group', 'id' => $groupId, 'dir' => $Groups->get_dir( $groupId ) ]; } $sender = $message->get('From'); if ( ! ptcp_is_authorized( $sender, 'event-definition', $object ) ){ $receipt->set_rejection( 31, _('Sender is not authorized to define event here') ); return $receipt->dump( TRUE ); } if ( ! empty( $eventData['id'] ) ){ $id = $groupId ? "{$groupId}/{$eventData['id']}" : $eventData['id']; $dir = $Events->get_dir( $id ); if ( $FileSystem->exists( $dir ) ){ $receipt->set_rejection( 42, _('An event with this ID already exists') ); return $receipt->dump( TRUE ); } } else { $name = substr( $Events->string_to_name( $eventData['name'] ), 0, 32 ); $id = $origId = ( $groupId ? $groupId . '/' : '' ) . date( 'Ymd', $date ) . "-{$name}"; $dir = $Events->get_dir( $id ); while ( $FileSystem->exists( $dir ) ){ $count = $id == $origId ? 2 : (int) substr( $id, strrpos( $id, '-' ) + 1 ) + 1; $id = "{$origId}-{$count}"; $dir = $Events->get_dir( $id ); } } $success = $FileSystem->make_dir( $dir ); if ( ! $success ){ $receipt->set_failure( _('Could not create event directory') . ': ' . $FileSystem->lastError ); return $receipt->dump( TRUE ); } $Events->generate_secret( $id ); $data = [ 'id' => $id, 'created_on' => date( 'c', $Timestamp ), 'created_by' => $sender, 'modified_on' => null, 'modified_by' => null, 'date_open' => null, 'date_close' => null, 'naming_rules' => [ 'prefix' => 'p', 'crc_length' => 1, 'group_length' => 3, 'group_separator' => '.' ], 'credential_rules' => [ 'char_list' => '1234567890', 'final_length' => 9, 'crc_length' => 1, 'group_length' => 3, 'group_separator' => '.' ], 'lot_code_rules' => [ 'char_list' => 'ABCDEFGHJKLMNPQRSTUVWXYZ123456789', 'final_length' => 32, 'crc_length' => 0, 'group_length' => 0, 'group_separator' => '.' ], ]; ptcp_update_object( $data, $Events->purge_data( $eventData ) ); $data['naming_rules']['counter_width'] = strlen( $data['estimated_turnout'] ); $length = $ServerData['mod.events.shortcode_length'] ?? 3; $data['shortcode'] = $Shortcoder->create( $id, 'events', $length ); $Events->update_calculated_fields( $data ); $fileName = date( 'Ymd-His', $Timestamp ) . '-event-definition'; $receipt->set( 'Message-Type', 'event-definition-confirmation' ); $receipt->set( 'Event-Data', $data ); if ( $votings = $message->get('Votings') ){ $results = self::process_votings_list( $id, $votings, $sender ); $receipt->set( 'Votings-Results', $results ); } $receiptString = $receipt->dump( TRUE ); $FileSystem->put_contents( "{$dir}/{$fileName}", $receiptString ); if ( ! empty( $eventData['issue_service'] ) ){ $Events->update_issue_service( $eventData ); } return $receiptString; } static function handle_event_update_request( $message, $receipt ){ global $Counter, $Events, $FileSystem, $Shortcoder, $Timestamp; $eventId = $message->get('Event-Id'); $eventData = $Events->get_data( $eventId ); if ( ! $eventData ){ $receipt->set_rejection( 41, sprintf( _('Event %s does not exist'), $eventId ) ); return $receipt->dump( TRUE ); } $eventDir = $Events->get_dir( $eventId ); $object = [ 'type' => 'event', 'id' => $eventId, 'dir' => $eventDir ]; $sender = $message->get('From'); if ( ! ptcp_is_authorized( $sender, 'event-update-request', $object ) ){ $receipt->set_rejection( 31, _('Sender is not authorized to update event') ); return $receipt->dump( TRUE ); } $stats = $Events->get_statistics( $eventId ); $newData = $message->get('Event-Data'); if ( ! $Events->validate_fields( $newData ) ){ $receipt->set_rejection( 21, sprintf( _('Invalid value for Event-Data field %s'), $Events->lastInvalidField ) ); return $receipt->dump( TRUE ); } if ( ! empty( $stats['lots_created'] ) ){ // some data must not be changed after first lot generation unset( $newData['estimated_turnout'], $newData['naming_rules'], $newData['credential_rules'], $newData['lot_code_rules'], $newData['issue_service'] ); } unset( $newData['date'] ); // Synchronize old and new attribute names (can be removed when all clients have been updated) if ( ! isset( $newData['short_description'] ) && isset( $newData['client_data']['short-description'] ) ){ $newData['short_description'] = $newData['client_data']['short-description']; } if ( ! isset( $newData['description'] ) && isset( $newData['client_data']['description'] ) ){ $newData['description'] = $newData['client_data']['description']; } if ( ! isset( $newData['link_url'] ) && isset( $newData['client_data']['link-url'] ) ){ $newData['link_url'] = $newData['client_data']['link-url']; } if ( ! isset( $newData['client_data']['short-description'] ) && isset( $newData['short_description'] ) ){ $newData['client_data']['short-description'] = $newData['short_description']; } if ( ! isset( $newData['client_data']['description'] ) && isset( $newData['description'] ) ){ $newData['client_data']['description'] = $newData['description']; } if ( ! isset( $newData['client_data']['link-url'] ) && isset( $newData['link_url'] ) ){ $newData['client_data']['link-url'] = $newData['link_url']; } if ( $votings = $message->get('Votings') ){ $Counter->set_base_dir('event_status'); $Counter->increment( $eventData['shortcode'] ); $results = self::process_votings_list( $eventId, $votings, $sender ); $receipt->set( 'Votings-Results', $results ); } $hasChanged = FALSE; if ( $newData ){ $hasChanged = ptcp_update_object( $eventData, $Events->purge_data( $newData ) ); if ( ! $hasChanged && ! $votings ){ $receipt->set_rejection( 21, _('Request does not contain changes') ); return $receipt->dump( TRUE ); } } if ( ! $hasChanged ){ return $receipt->dump( TRUE ); } $Events->update_calculated_fields( $eventData ); $eventData['modified_on'] = date( 'c', $Timestamp ); $eventData['modified_by'] = $sender; $receipt->set( 'Event-Data', $eventData ); $fileName = date( 'Ymd-His', $Timestamp ) . '-event-update-request'; $receiptString = $receipt->dump( TRUE ); $FileSystem->put_contents( "{$eventDir}/{$fileName}", $receiptString ); if ( ! empty( $newData['issue_service'] ) ){ $Events->update_issue_service( $newData, $eventData ); } return $receiptString; } static function handle_event_status_declaration( $message, $receipt ){ global $Events, $FileSystem, $Timestamp; $eventId = $message->get('Event-Id'); $eventDir = $Events->get_dir( $eventId ); $object = [ 'type' => 'event', 'id' => $eventId, 'dir' => $eventDir ]; $sender = $message->get('From'); if ( ! ptcp_is_authorized( $sender, 'event-status-request', $object ) ){ $receipt->set_rejection( 31, _('Sender is not authorized to declare event status') ); return $receipt->dump( TRUE ); } $eventData = $Events->get_data( $eventId ); if ( ! $eventData ){ $receipt->set_rejection( 41, sprintf( _('Event %s does not exist'), $eventId ) ); return $receipt->dump( TRUE ); } $newStatus = $message->get('Status'); $result = $Events->set_status( $eventData, $newStatus, $Timestamp ); if ( $result == -1 ){ $receipt->set_rejection( 43, _('Unknown status') ); return $receipt->dump( TRUE ); } if ( $result == -2 ){ $receipt->set_rejection( 43, sprintf( _("Event with status '%s' cannot be declared as '%s'"), $eventData['status'], $newStatus ) ); return $receipt->dump( TRUE ); } $eventData['modified_on'] = date( 'c', $Timestamp ); $eventData['modified_by'] = $sender; $receipt->set( 'Message-Type', 'event-details' ); $receipt->set( 'Event-Data', $eventData ); $fileName = date( 'Ymd-His', $Timestamp ) . '-event-status-declaration'; $receiptString = $receipt->dump( TRUE ); $FileSystem->put_contents( "{$eventDir}/{$fileName}", $receiptString ); return $receiptString; } static function handle_event_list_request( $message, $receipt ){ global $Events, $FileSystem, $Groups, $Votings; if ( is_object( $Groups ) && ( $groupId = $message->get('Group-Id') ) && ( ! $groupData = $Groups->get_data( $groupId ) ) ){ $receipt->set_rejection( 41, sprintf( _('Group %s does not exist'), $groupId ) ); return $receipt->dump( TRUE ); } $receipt->set( 'Message-Type', 'event-list' ); $status = $message->get('Event-Statuses') ?: $message->get('Event-Status') ?: [ 'opened', 'closed' ]; $eventList = $Events->get_list( $groupId, $status ); if ( $message->get('Include-Subgroups') && is_object( $Groups ) ){ $groupList = $Groups->get_id_list_recursive( $groupId ); foreach ( $groupList as $groupId ){ $eventList = array_merge( $eventList, $Events->get_list( $groupId, $status ) ); } } $sender = $message->get('From'); if ( $sender ){ list ( $ptcpId, $server ) = explode( '@', $sender ) + ['','']; } $includeVotingCount = $message->get('Include-Voting-Count'); $includeOpenVotings = $message->get('Include-Open-Votings'); $includeAdminInfo = $message->get('Include-Admin-Info'); $administrableOnly = $message->get('Administrable-Only'); $finalList = []; foreach ( $eventList as $index => $event ){ $eventDir = $Events->get_dir( $event['id'] ); if ( $includeAdminInfo && $sender ){ // TODO: Verify sender identity $event['administrable'] = ptcp_is_authorized( $sender, 'event-update-request', [ 'type' => 'event', 'id' => $event['id'], 'dir' => $eventDir ] ); } if ( $includeVotingCount ){ $Votings->set_base_dir( $eventDir ); $event['voting_count'] = $Votings->count(); } if ( $includeOpenVotings ){ $Votings->set_base_dir( $eventDir ); $sortKey = $event['voting_sort_key'] ?? 'created_on'; $sortDesc = ! empty( $event['voting_sort_order'] ) && $event['voting_sort_order'] == 'descending'; $event['open_votings'] = $Votings->get_list( 'open', $sortKey, $sortDesc ); } if ( $sender ){ $event['lot_code_deposited'] = $FileSystem->exists( "{$eventDir}/lotcodes/{$ptcpId}" ); } if ( ! $sender || ! $administrableOnly || $event['administrable'] ){ $Events->update_calculated_fields( $event ); $finalList[] = $event; } } $receipt->set( 'Events', $finalList ); return $receipt->dump( TRUE ); } static function handle_event_details_request( $message, $receipt ){ global $Admins, $BaseUrl, $Counter, $DataDir, $Events, $Shortcoder, $Votings; $eventId = $message->get('Event-Id'); if ( $eventId[0] == '#' ){ $eventId = $Shortcoder->resolve( $eventId, 'events' ); if ( ! $eventId ){ $receipt->set_rejection( 42, _('Event shortcode is unknown') ); return $receipt->dump( TRUE ); } } $eventData = $Events->get_data( $eventId ); if ( ! $eventData ){ $receipt->set_rejection( 41, _('Event does not exist') ); return $receipt->dump( TRUE ); } $Counter->set_base_dir('event_status'); $pollingInfo['change_count'] = $Counter->get_value( $eventData['shortcode'] ); $file = "{$DataDir}/event_status/{$eventData['shortcode']}"; $pollingInfo['polling_interval'] = file_exists( $file ) ? (int) file_get_contents( $file ) : 60; $pollingInfo['polling_url'] = "{$BaseUrl}/modules/events/poll.php" . "?event={$eventData['shortcode']}"; $eventDir = $Events->get_dir( $eventId ); if ( $sender = $message->get('From') ){ $eventData['administrable'] = ptcp_is_authorized( $sender, 'event-details-request', [ 'type' => 'event', 'id' => $eventId, 'dir' => $eventDir ] ); } $sortKey = $eventData['voting_sort_key'] ?? 'created_on'; $sortDesc = ! empty( $eventData['voting_sort_order'] ) && $eventData['voting_sort_order'] == 'descending'; $Votings->set_base_dir( $eventDir ); $votingList = $Votings->get_list( 'all', $sortKey, $sortDesc ); $receipt->set( 'Message-Type', 'event-details' ); $receipt->set( 'Event-Data', $eventData ); $receipt->set( 'Polling-Information', $pollingInfo ); $receipt->set( 'Votings', $votingList ); $receipt->set( 'Participants', $Events->get_statistics( $eventId ) ); if ( $sender && ! empty( $eventData['is_non_anonymous'] ) && $message->get('Include-Completed-Votings') ){ $completedVotings = []; foreach ( $votingList as $voting ){ if ( $Votings->has_participant_voted( $voting['id'], $sender ) ){ $completedVotings[] = $voting['id']; } } $receipt->set( 'Completed-Votings', $completedVotings ); } if ( $message->get('Include-Admins') ){ $result = $Admins->get_responsibles( $eventDir ); $receipt->set( 'Admins', $result['list'] ); $receipt->set( 'Direct-Appointment', $result['direct'] ); } return $receipt->dump( TRUE ); } static function handle_multi_registration( $message, $receipt ){ global $Counter, $Events, $FileSystem, $LocalId, $Lots, $ServerData, $Timestamp, $Votings; $eventId = $message->get('Event-Id'); $eventData = $Events->get_data( $eventId ); if ( ! $eventData ){ $receipt->set_rejection( 41, _('Event does not exist') ); return $receipt->dump( TRUE ); } if ( ! in_array( $eventData['status'], ['planned','opened'] ) ){ $receipt->set_rejection( 42, _('Event status does not allow registrations') ); return $receipt->dump( TRUE ); } $nonAnon = ! empty( $eventData['is_non_anonymous'] ); if ( $nonAnon && ! $message->get('Participants') ){ $receipt->set_rejection( 43, _('Non-anonymous event requires list of participant IDs for registration') ); return $receipt->dump( TRUE ); } $count = (int) $message->get('Count'); if ( ! $nonAnon && ( $count < 1 || $count > 10000 ) ){ $receipt->set_rejection( 22, _('Invalid value for count') . ' (1..10000)' ); return $receipt->dump( TRUE ); } $ptcpList = (array) $message->get('Participants'); foreach ( $ptcpList ?? [] as $ptcp ){ if ( ! strpos( $ptcp['id'], '@' ) ){ $receipt->set_rejection( 44, _('Participant IDs must contain \'@\' and server name') ); return $receipt->dump( TRUE ); } if ( strpos( $ptcp['id'], '+' ) ){ $receipt->set_rejection( 45, _('Event participants not allowed here') ); return $receipt->dump( TRUE ); } } $eventDir = $Events->get_dir( $eventId ); $sender = $message->get('From'); if ( empty( $eventData['issue_service']['host'] ) || $eventData['issue_service']['host'] != $sender ){ $object = [ 'type' => 'event', 'id' => $eventId, 'dir' => $eventDir ]; if ( ! ptcp_is_authorized( $sender, 'multi-registration', $object ) ){ $receipt->set_rejection( 31, _('Sender is not authorized to register participants') ); return $receipt->dump( TRUE ); } } $LocalId->set_base_dir( $eventDir, $eventData['estimated_turnout'] ); $Lots->set_base_dir( $eventDir, $eventData['estimated_turnout'] ); $rules = $eventData['naming_rules']; set_time_limit( 30 + $count ); // Determine counter start and end $counterStart = 1; $counterEnd = $nonAnon ? count( $ptcpList ) : $count; if ( ! $nonAnon && $FileSystem->exists( "{$eventDir}/participants" ) ){ $counterStart = $FileSystem->count_items( "{$eventDir}/participants" ) + 1; $counterEnd = $counterStart + $count - 1; } if ( strlen( $counterEnd ) > $rules['counter_width'] ){ $receipt->set_rejection( 23, _('Estimated turnout does not allow that much participants') ); return $receipt->dump( TRUE ); } // Ensure that local privkey is set $eventSecret = $Events->get_secret( $eventId ); if ( ! $eventSecret ){ $localId = new ParTCP_Private_Identity( $ServerData['name'] ); ParTCP_Crypto::set_local_privkey( $localId->privKey ); } // Create participants and lots for ( $i = $counterStart; $i <= $counterEnd; $i++ ){ if ( $nonAnon ){ $ptcpId = $ptcpList[ $i - 1 ]['id']; if ( strpos( $ptcpId, '+' ) ){ $receipt->set_failure( _('Event participants not allowed here') ); return $receipt->dump( TRUE ); } $lotCode = $ptcpList[ $i - 1 ]['lotcode'] ?? NULL; if ( $lotCode ){ $lotName = hash( 'sha256', $eventId . $lotCode . $eventSecret ); $lotDir = $Lots->get_dir( $lotName ); if ( $FileSystem->exists( $lotDir ) ){ $receipt->set_rejection( 46, sprintf( _('Lot code \'%s\' is already in use'), $lotCode ) ); return $receipt->dump( TRUE ); } } $credential = $ptcpList[ $i - 1 ]['credential']; if ( ! $FileSystem->exists( "{$eventDir}/participants/{$ptcpId}" ) && ! $FileSystem->make_dir( "{$eventDir}/participants/{$ptcpId}", TRUE ) ){ $receipt->set_failure( sprintf( _('Could not create participant directory %s'), $ptcpId ) ); return $receipt->dump( TRUE ); } goto CREATE_LOT; } // generate id and create directory $id = str_replace( '+', '', $rules['prefix'] ) . sprintf( '%0' . $rules['counter_width'] . 'd', $i ); if ( $rules['group_length'] ){ $id .= str_repeat( ' ', $rules['crc_length'] ); $id = substr( chunk_split( $id, $rules['group_length'], $rules['group_separator'] ), 0, -1 ); $id = rtrim( $id, ' ' ); } if ( $rules['crc_length'] ){ $id .= substr( crc32( $id ), - $rules['crc_length'] ); } $dir = $LocalId->get_dir( $id ); if ( ! $FileSystem->make_dir( $dir, TRUE ) ){ $receipt->set_failure( sprintf( _('Could not create participant directory %s'), $id ) ); return $receipt->dump( TRUE ); } $ptcpId = "{$eventId}+{$id}"; // generate credential $credential = $Events->generate_code( $eventData['credential_rules'] ?? [] ); // create and save individual registration message $envMessage = [ 'Date' => date( 'c', $Timestamp ), 'Message-Type' => 'surrogate-registration-confirmation', 'Original-Message' => $message->rawMessage, 'Participant-Data' => [ 'id' => $id, 'credential' => hash( 'sha256', $credential ) ], ]; $file = $dir . '/' . date( 'Ymd-His', $Timestamp ) . '-surrogate-registration'; $FileSystem->put_contents( $file, ptcp_yaml_emit( $envMessage ), TRUE ); // create lot CREATE_LOT: if ( empty( $lotCode ) ){ do { $lotCode = $Events->generate_code( $eventData['lot_code_rules'] ?? [] ); if ( $eventSecret ){ $lotName = hash( 'sha256', $eventId . $lotCode . $eventSecret ); } else { $lotName = ParTCP_Crypto::generate_private_hash( $eventId . $lotCode ); } $lotDir = $Lots->get_dir( $lotName ); } while ( $FileSystem->exists( $lotDir ) ); } $lotContent = "participant_id: {$ptcpId}\n" . "credential: {$credential}"; $FileSystem->put_contents( "{$lotDir}/lot", ptcp_local_encrypt( $lotContent ), TRUE ); $lotCodes[] = $lotCode; $lotCode = NULL; $Counter->set_base_dir( $eventDir ); $Counter->increment('lots'); } // set response values sort( $lotCodes ); $receipt->set( 'Message-Type', 'multi-registration-confirmation' ); $receipt->set( 'Lot-Codes', $lotCodes, TRUE ); $file = date( 'Ymd-His', $Timestamp ) . '-multi-registration'; $receiptString = $receipt->dump( TRUE ); $FileSystem->put_contents( "{$eventDir}/{$file}", $receiptString ); return $receiptString; } static function handle_lot_request( $message, $receipt ){ global $Counter, $Events, $FileSystem, $Lots, $Shortcoder; $eventId = $message->get('Event-Id'); if ( $eventId[0] == '#' ){ $eventId = $Shortcoder->resolve( $eventId, 'events' ); if ( ! $eventId ){ $receipt->set_rejection( 45, _('Event shortcode is unknown') ); return $receipt->dump( TRUE ); } } $eventData = $Events->get_data( $eventId ); if ( ! $eventData ){ $receipt->set_rejection( 44, _('Event does not exist') ); return $receipt->dump( TRUE ); } if ( $eventData['status'] != 'opened' ){ $receipt->set_rejection( 46, _('Event status does not allow lot requests') ); return $receipt->dump( TRUE ); } $lotCode = $message->get('Lot-Code'); $eventSecret = $Events->get_secret( $eventId ); if ( $eventSecret ){ $lotName = hash( 'sha256', $eventId . $lotCode . $eventSecret ); } else { $lotName = ParTCP_Crypto::generate_private_hash( $eventId . $lotCode ); } $eventDir = $Events->get_dir( $eventId ); $lotDir = $eventDir . '/' . $Lots->get_dir( $lotName ); if ( ! $FileSystem->exists( $lotDir ) ){ $receipt->set_rejection( 41, _('Unknown lot code') ); return $receipt->dump( TRUE ); } if ( $FileSystem->exists( "{$lotDir}/lot-invalidation" ) ){ $receipt->set_rejection( 42, _('Lot has been invalidated') ); return $receipt->dump( TRUE ); } if ( $FileSystem->exists( "{$lotDir}/pick-up-note" ) && empty( $eventData['is_demo'] ) ){ $receipt->set_rejection( 43, _('Lot has been picked up already') ); return $receipt->dump( TRUE ); } $lotContent = $FileSystem->get_contents( "{$lotDir}/lot" ); if ( empty( $lotContent ) ){ $receipt->set_failure( _('Lot could not be read') ); return $receipt->dump( TRUE ); } $lotContent = ptcp_local_decrypt( $lotContent ); if ( empty( $lotContent ) ){ $receipt->set_failure( _('Lot content could not be decrypted') ); return $receipt->dump( TRUE ); } $msg = [ 'Message-Type' => 'pick-up-note', 'Event-Id' => $eventId, 'Lot-Name' => $lotName, ]; if ( empty( $eventData['is_demo'] ) ){ $result = $FileSystem->put_contents( "{$lotDir}/pick-up-note", ptcp_yaml_emit( $msg ), TRUE ); if ( ! $result ){ $receipt->set_failure( _('Could not create pick-up note') ); return $receipt->dump( TRUE ); } } $receipt->set( 'Message-Type', 'lot' ); $receipt->set( 'Lot-Content', $lotContent, TRUE ); $Counter->set_base_dir( $eventDir ); $Counter->increment('pick-ups'); return $receipt->dump( TRUE ); } static function handle_lot_invalidation( $message, $receipt ){ global $Counter, $Events, $FileSystem, $LocalId, $Lots, $Timestamp; $eventId = $message->get('Event-Id'); $eventData = $Events->get_data( $eventId ); if ( ! $eventData ){ $receipt->set_rejection( 41, _('Event does not exist') ); return $receipt->dump( TRUE ); } if ( in_array( $eventData['status'], [ 'closed', 'finished' ] ) ){ $receipt->set_rejection( 45, _('Event status does not allow lot invalidations') ); return $receipt->dump( TRUE ); } $eventDir = $Events->get_dir( $eventId ); $eventSecret = $Events->get_secret( $eventId ); $sender = $message->get('From'); if ( empty( $eventData['issue_service']['host'] ) || $eventData['issue_service']['host'] != $sender ){ $object = [ 'type' => 'event', 'id' => $eventId, 'dir' => $eventDir ]; if ( ! ptcp_is_authorized( $sender, 'lot-invalidation', $object ) ){ $receipt->set_rejection( 31, _('Sender is not authorized to invalidate lots') ); return $receipt->dump( TRUE ); } } // Retrieve lots contents $LocalId->set_base_dir( $eventDir ); $Lots->set_base_dir( $eventDir ); $lotCodes = array_merge( (array) ( $message->get('Lot-Code') ?? [] ), (array) ( $message->get('Lot-Codes') ?? [] ) ); $ptcpIds = array (); foreach ( $lotCodes as $lotCode ){ list( $lotCode ) = explode( '@', trim( $lotCode ) ); // remove server/event code if ( $eventSecret ){ $lotName = hash( 'sha256', $eventId . $lotCode . $eventSecret ); } else { $lotName = ParTCP_Crypto::generate_private_hash( $eventId . $lotCode ); } $lotDir = $Lots->get_dir( $lotName ); if ( ! $FileSystem->exists( $lotDir ) ){ $receipt->set_rejection( 42, _('Unknown lot code') ); return $receipt->dump( TRUE ); } if ( $FileSystem->exists( "{$lotDir}/lot-invalidation" ) ){ $receipt->set_rejection( 43, _('Lot has already been invalidated') ); return $receipt->dump( TRUE ); } $lotContent = $FileSystem->get_contents( "{$lotDir}/lot" ); if ( empty( $lotContent ) ){ $receipt->set_failure( _('Lot could not be read') ); return $receipt->dump( TRUE ); } $lotContent = ptcp_local_decrypt( $lotContent ); if ( empty( $lotContent ) ){ $receipt->set_failure( _('Lot content could not be decrypted') ); return $receipt->dump( TRUE ); } $lotContent = yaml_parse( $lotContent ); if ( empty( $lotContent['participant_id'] ) ){ $receipt->set_failure( _('Invalid lot content') ); return $receipt->dump( TRUE ); } list ( $ptcpId ) = explode( '@', $lotContent['participant_id'] ); if ( strpos( $ptcpId, '+' ) ){ $ptcpId = substr( strrchr( $ptcpId, '+' ), 1 ); } $ptcpDir = $LocalId->get_dir( $ptcpId ); if ( $FileSystem->exists( "{$ptcpDir}/has_voted" ) ){ $receipt->set_rejection( 44, _('Participant has voted already') ); return $receipt->dump( TRUE ); } $lotDirs[] = $lotDir; $ptcpIds[] = $ptcpId; } // Invalidate lots and participants $receiptString = $receipt->dump( TRUE ); $Counter->set_base_dir( $eventDir ); foreach ( $lotDirs as $dir ){ $FileSystem->put_contents( "{$dir}/lot-invalidation", $receiptString ); $Counter->increment('invalidations'); } foreach ( $ptcpIds as $ptcpId ){ $ptcpDir = $LocalId->get_dir( $ptcpId ); if ( $FileSystem->exists( $ptcpDir ) ){ $ptcpData = $LocalId->get_data( $ptcpId ); $ptcpData['invalidated'] = TRUE; $receipt->set( 'Participant-Data', $ptcpData ); $file = date( 'Ymd-His', $Timestamp ) . '-lot-invalidation'; $receiptString = $receipt->dump( TRUE ); $FileSystem->put_contents( "{$ptcpDir}/{$file}", $receiptString ); } } $receipt->set( 'Participant-Data' ); $receipt->set( 'Invalidated-Participants', $ptcpIds ?? [] ); return $receipt->dump( TRUE ); } static function handle_participant_invalidation( $message, $receipt ){ global $Counter, $Events, $FileSystem, $LocalId, $Timestamp; $eventId = $message->get('Event-Id'); $eventData = $Events->get_data( $eventId ); if ( ! $eventData ){ $receipt->set_rejection( 41, _('Event does not exist') ); return $receipt->dump( TRUE ); } if ( ! empty( $eventData['is_demo'] ) ){ $receipt->set_rejection( 42, _('Event is marked as demo event') ); return $receipt->dump( TRUE ); } if ( in_array( $eventData['status'], [ 'closed', 'finished' ] ) ){ $receipt->set_rejection( 46, _('Event status does not allow participant invalidations') ); return $receipt->dump( TRUE ); } $eventDir = $Events->get_dir( $eventId ); $LocalId->set_base_dir( $eventDir ); $sender = $message->get('From'); list ( $ptcpId ) = explode( '@', $sender ); if ( strpos( $ptcpId, '+' ) ){ $ptcpId = substr( strrchr( $ptcpId, '+' ), 1 ); } $ptcpDir = $LocalId->get_dir( $ptcpId ); if ( ! $FileSystem->exists( $ptcpDir ) ){ $receipt->set_rejection( 43, _('Sender is not participant of the event') ); return $receipt->dump( TRUE ); } $ptcpData = $LocalId->get_data( $ptcpId ); if ( ! empty( $ptcpData['invalidated'] ) ){ $receipt->set_rejection( 44, _('Participant has already been invalidated') ); return $receipt->dump( TRUE ); } if ( $FileSystem->exists( "{$ptcpDir}/has_voted" ) ){ $receipt->set_rejection( 45, _('Participant has voted already') ); return $receipt->dump( TRUE ); } $Counter->set_base_dir( $eventDir ); $Counter->increment('invalidations'); $ptcpData['invalidated'] = TRUE; $receipt->set( 'Participant-Data', $ptcpData ); $receipt->set( 'Message-Type', 'participant-invalidation-confirmation' ); $file = date( 'Ymd-His', $Timestamp ) . '-participant-invalidation'; $receiptString = $receipt->dump( TRUE ); $FileSystem->put_contents( "{$ptcpDir}/{$file}", $receiptString ); return $receiptString; } static function handle_lot_code_deposit( $message, $receipt ){ global $Counter, $Events, $FileSystem; $eventId = $message->get('Event-Id'); $eventData = $Events->get_data( $eventId ); if ( ! $eventData ){ $receipt->set_rejection( 41, sprintf( _('Event %s does not exist'), $eventId ) ); return $receipt->dump( TRUE ); } if ( in_array( $eventData['status'], [ 'closed', 'finished' ] ) ){ $receipt->set_rejection( 42, _('Event status does not allow lot code deposits') ); return $receipt->dump( TRUE ); } $eventDir = $Events->get_dir( $eventId ); $object = [ 'type' => 'event', 'id' => $eventId, 'dir' => $eventDir ]; if ( ! ptcp_is_authorized( $message->get('From'), 'lot-code-deposit', $object ) ){ $receipt->set_rejection( 31, _('Sender is not authorized to deposit lot codes') ); return $receipt->dump( TRUE ); } list ( $ptcpId, $server ) = explode( '@', $message->get('Participant-Id') ) + ['','']; $counter = 0; while ( TRUE ) { $suffix = $counter ? "-{$counter}" : ''; $file = "{$eventDir}/lotcodes/{$ptcpId}{$suffix}"; if ( ! $FileSystem->exists( $file ) ){ break; } $counter++; } $result = $FileSystem->put_contents( $file, $receipt->dump() ); if ( ! $result ){ $receipt->set_failure( sprintf( _('Could not deposit lot code for %s'), $ptcpId ) ); return $receipt->dump( TRUE ); } if ( ! $suffix ){ $Counter->set_base_dir( $eventDir ); $Counter->increment('deposits'); } $receipt->set( 'Message-Type', 'lot-code-deposit-confirmation' ); return $receipt->dump( TRUE ); } static function handle_lot_code_request( $message, $receipt ){ global $Events, $FileSystem; $eventId = $message->get('Event-Id'); $eventData = $Events->get_data( $eventId ); if ( ! $eventData ){ $receipt->set_rejection( 41, sprintf( _('Event %s does not exist'), $eventId ) ); return $receipt->dump( TRUE ); } if ( $eventData['status'] != 'opened' ){ $receipt->set_rejection( 43, _('Event status does not allow lot code requests') ); return $receipt->dump( TRUE ); } $sender = $message->get('From'); list ( $ptcpId, $server ) = explode( '@', $sender ) + ['','']; $eventDir = $Events->get_dir( $eventId ); $counter = 0; $file = "{$eventDir}/lotcodes/{$ptcpId}"; while ( $FileSystem->exists( $file ) ) { $counter++; $nextFile = "{$eventDir}/lotcodes/{$ptcpId}-{$counter}"; if ( ! $FileSystem->exists( $nextFile ) ){ break; } $file = $nextFile; } if ( ! $counter ){ $receipt->set_rejection( 42, sprintf( _('No lot code deposited for %s'), $ptcpId ) ); return $receipt->dump( TRUE ); } if ( ! ( $msg = yaml_parse( $FileSystem->get_contents( $file ) ) ) || empty( $msg['Original-Message'] ) || ! ( $msg = yaml_parse( $msg['Original-Message'] ) ) || ( empty ( $msg['Lot-Code'] ) && empty( $msg['Lot-Code~'] ) ) ){ $receipt->set_failure( sprintf( _('Could not load lot code for %s'), $sender ) ); return $receipt->dump( TRUE ); } $receipt->set( 'Message-Type', 'lot-code' ); $receipt->set( 'Lot-Code~', $msg['Lot-Code'] ?? $msg['Lot-Code~'] ); $receipt->set( 'Original-Sender', $msg['From'] ); $receipt->set( 'Original-Public-Key', $msg['Public-Key'] ); return $receipt->dump( TRUE ); } static function handle_event_polling_interval_declaration( $message, $receipt ){ global $DataDir, $Events, $FileSystem, $Timestamp, $Votings; $eventId = $message->get('Event-Id'); $eventData = $Events->get_data( $eventId ); if ( empty( $eventData['shortcode'] ) ){ $receipt->set_rejection( 41, sprintf( _('Event %s does not exist'), $eventId ) ); return $receipt->dump( TRUE ); } $eventDir = $Events->get_dir( $eventId ); $object = [ 'type' => 'event', 'id' => $eventId, 'dir' => $eventDir ]; $sender = $message->get('From'); if ( ! ptcp_is_authorized( $sender, 'event-polling-interval-declaration', $object ) ){ $receipt->set_rejection( 31, _('Sender is not authorized to declare polling interval') ); return $receipt->dump( TRUE ); } $file = "{$DataDir}/event_status/{$eventData['shortcode']}"; @mkdir( dirname( $file ), 0755, TRUE ); $result = file_put_contents( $file, $message->get('Interval') ); if ( ! $result ){ $receipt->set_failure( _('Could not write interval file') ); return $receipt->dump( TRUE ); } return $receipt->dump( TRUE ); } } // end of file events_messages.class.php