diff --git a/runtime/history/SUPPORT-260918-GRAM-SUPPORT-NOTIFY-CAPTCHA-SOURCE-READONLY-516R1R3.json b/runtime/history/SUPPORT-260918-GRAM-SUPPORT-NOTIFY-CAPTCHA-SOURCE-READONLY-516R1R3.json new file mode 100644 index 00000000..1578d4f1 --- /dev/null +++ b/runtime/history/SUPPORT-260918-GRAM-SUPPORT-NOTIFY-CAPTCHA-SOURCE-READONLY-516R1R3.json @@ -0,0 +1,38 @@ +{ + "schema_version": 1, + "channel": "homelab-runtime", + "command_id": "SUPPORT-260918-GRAM-SUPPORT-NOTIFY-CAPTCHA-SOURCE-READONLY-516R1R3", + "status": "OK", + "rc": 0, + "host": "pve01", + "mode": "read-only", + "component": "gram-support-notify-captcha-source-readonly", + "started_at_utc": "2026-09-18T05:06:58Z", + "finished_at_utc": "2026-09-18T05:07:00Z", + "reference_register_checked": true, + "reference_sha256": "5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66", + "error_register_checked": true, + "error_register_sha256": "3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0", + "command_sha256": "0f7a476d59191ec993679cb3fe3a569eb7e5b72805ab8c6e63c390f6a14915fc", + "duplicate_failed_command_blocked": false, + "block_reason": null, + "execution_started": true, + "change_declared": false, + "result_contract_valid": true, + "result_contract_status": null, + "result_contract_error": null, + "command_rc": 0, + "changes_made": false, + "rollback_started": false, + "rollback_restored": null, + "mutation_outcome": "NO_MUTATION", + "sanitized": true, + "secrets_included": false, + "private_addresses_included": false, + "raw_evidence_retained_locally": true, + "raw_evidence_sha256": "6eb6b9ac7ab7b4c61eb06b40dd1f2390fcac654e496097728a954feacd2b3c9c", + "sanitized_output_sha256": "6eb6b9ac7ab7b4c61eb06b40dd1f2390fcac654e496097728a954feacd2b3c9c", + "output_truncated_in_json": false, + "full_sanitized_output_url": "https://git.gram1.ru/.well-known/homelab-runtime/latest.txt", + "output": "GRAM516R1R3_SAFE_REPORT={\"err_truncated\":false,\"guest_stderr_present\":false,\"inner_rc\":0,\"ok\":true,\"out_truncated\":false,\"source\":{\"notification_files\":[\"Notification/AlertConsumer.php\",\"Notification/DeliveryGate.php\",\"Notification/Dispatcher.php\",\"Notification/EmailConsumer.php\",\"Notification/EntityDeliveryGate.php\",\"Notification/EventDispatcher.php\",\"Notification/Policy.php\",\"Notification/RecipientKey.php\"],\"ok\":true,\"root_tree_files\":[\"Attachment/TicketHandler.php\",\"Entity/AttachmentMeta.php\",\"Entity/Delivery.php\",\"Entity/Message.php\",\"Entity/RateLimit.php\",\"Entity/SupportEvent.php\",\"Entity/Ticket.php\",\"Listener/SourcePageContext.php\",\"Notification/AlertConsumer.php\",\"Notification/DeliveryGate.php\",\"Notification/Dispatcher.php\",\"Notification/EmailConsumer.php\",\"Notification/EntityDeliveryGate.php\",\"Notification/EventDispatcher.php\",\"Notification/Policy.php\",\"Notification/RecipientKey.php\",\"Pub/Controller/AccountSupport.php\",\"Pub/Controller/Pwa.php\",\"Pub/Controller/StaffSupport.php\",\"Pub/Controller/Support.php\",\"Pub/View/PwaServiceWorker.php\",\"Repository/Message.php\",\"Repository/Ticket.php\",\"Security/GuestTicketSession.php\",\"Security/TicketAccess.php\",\"Service/GuestScreenshotService.php\",\"Service/GuestTicketCreateAttachmentFlow.php\",\"Service/RateLimiter.php\",\"Service/SourceContextResolver.php\",\"Service/Ticket/Creator.php\",\"Service/Ticket/Reply.php\",\"Service/Ticket/StaffUpdate.php\",\"Setup.php\",\"Util/GuestToken.php\",\"Util/PublicRef.php\",\"Util/RateKey.php\",\"Util/SafeContext.php\",\"Util/SiteConfig.php\",\"Util/StatusPolicy.php\",\"_data/code_event_listeners.xml\",\"_data/content_type_fields.xml\",\"_data/option_groups.xml\",\"_data/options.xml\",\"_data/permission_interface_groups.xml\",\"_data/permissions.xml\",\"_data/phrases.xml\",\"_data/routes.xml\",\"_data/template_modifications.xml\",\"_data/templates.xml\",\"addon.json\",\"hashes.json\"],\"targets\":{\"Notification/AlertConsumer.php\":{\"line_count\":74,\"matched_context\":[{\"sep\":true},{\"line\":1,\"text\":\"gate = $gate;\"},{\"line\":16,\"text\":\" }\"},{\"line\":17,\"text\":\"\"},{\"line\":18,\"text\":\" public function consumeForUser(SupportEvent $event, Ticket $ticket, User $recipient): bool\"},{\"line\":19,\"text\":\" {\"},{\"line\":20,\"text\":\" if (!Policy::mayEmitExternal($event) || !$recipient->user_id)\"},{\"line\":21,\"text\":\" {\"},{\"line\":22,\"text\":\" return false;\"},{\"line\":23,\"text\":\" }\"},{\"line\":24,\"text\":\"\"},{\"line\":25,\"text\":\" // Never alert a user about a ticket they cannot access.\"},{\"line\":26,\"text\":\" $isOwner = $ticket->user_id && (int)$ticket->user_id === (int)$recipient->user_id;\"},{\"line\":27,\"text\":\" $isStaff = $recipient->hasPermission('gramSupport', 'viewAll')\"},{\"line\":28,\"text\":\" || $recipient->hasPermission('gramSupport', 'manage');\"},{\"line\":29,\"text\":\"\"},{\"line\":30,\"text\":\" if (!$isOwner && !$isStaff)\"},{\"line\":31,\"text\":\" {\"},{\"line\":32,\"text\":\" return false;\"},{\"line\":33,\"text\":\" }\"},{\"line\":34,\"text\":\"\"},{\"line\":35,\"text\":\" $recipientKey = RecipientKey::user((int)$recipient->user_id);\"},{\"line\":36,\"text\":\" if (!$this->gate->acquire((int)$event->event_id, 'xf_alert', $recipientKey))\"},{\"line\":37,\"text\":\" {\"},{\"line\":38,\"text\":\" return false;\"},{\"line\":39,\"text\":\" }\"},{\"line\":40,\"text\":\"\"},{\"line\":41,\"text\":\" try\"},{\"line\":42,\"text\":\" {\"},{\"line\":43,\"text\":\" /** @var \\\\XF\\\\Repository\\\\UserAlertRepository $alerts */\"},{\"line\":44,\"text\":\" $alerts = \\\\XF::repository('XF:UserAlert');\"},{\"line\":45,\"text\":\"\"},{\"line\":46,\"text\":\" // Back the alert with the recipient's own User content so the\"},{\"line\":47,\"text\":\" // core alert handler remains valid/viewable. Ticket access is\"},{\"line\":48,\"text\":\" // separately enforced before emission and again on ticket route.\"},{\"line\":49,\"text\":\" $alerts->alert(\"},{\"line\":50,\"text\":\" $recipient,\"},{\"line\":51,\"text\":\" 0,\"},{\"line\":52,\"text\":\" 'Support',\"},{\"line\":53,\"text\":\" 'user',\"},{\"line\":54,\"text\":\" (int)$recipient->user_id,\"},{\"line\":55,\"text\":\" 'gram_support_evt',\"},{\"line\":56,\"text\":\" Policy::alertExtraData($event),\"},{\"line\":57,\"text\":\" ['usePush' => false]\"},{\"line\":58,\"text\":\" );\"},{\"line\":59,\"text\":\"\"},{\"line\":60,\"text\":\" $this->gate->markSent((int)$event->event_id, 'xf_alert', $recipientKey);\"},{\"line\":61,\"text\":\" return true;\"},{\"line\":62,\"text\":\" }\"},{\"line\":63,\"text\":\" catch (\\\\Throwable $e)\"},{\"line\":64,\"text\":\" {\"},{\"line\":65,\"text\":\" $this->gate->markFailed(\"},{\"line\":66,\"text\":\" (int)$event->event_id,\"},{\"line\":67,\"text\":\" 'xf_alert',\"},{\"line\":68,\"text\":\" $recipientKey,\"},{\"line\":69,\"text\":\" get_class($e)\"},{\"line\":70,\"text\":\" );\"},{\"line\":71,\"text\":\" throw $e;\"},{\"line\":72,\"text\":\" }\"}],\"present\":true,\"sha256\":\"e22d0cdb0a0806c6a231504409bd17cd781caab0f28181aff7246b0295d8cd98\"},\"Notification/Dispatcher.php\":{\"line_count\":67,\"matched_context\":[{\"sep\":true},{\"line\":1,\"text\":\"alerts = $alerts;\"},{\"line\":17,\"text\":\" $this->email = $email;\"},{\"line\":18,\"text\":\" }\"},{\"line\":19,\"text\":\"\"},{\"line\":20,\"text\":\" /**\"},{\"line\":21,\"text\":\" * Owner-facing delivery. Staff alert fan-out is intentionally provided by\"},{\"line\":22,\"text\":\" * dispatchStaff() so staff recipients must still pass Gram Support ACL.\"},{\"line\":23,\"text\":\" */\"},{\"line\":24,\"text\":\" public function dispatchOwner(SupportEvent $event, Ticket $ticket, ?User $owner): void\"},{\"line\":25,\"text\":\" {\"},{\"line\":26,\"text\":\" if (!Policy::mayEmitExternal($event))\"},{\"line\":27,\"text\":\" {\"},{\"line\":28,\"text\":\" return;\"},{\"line\":29,\"text\":\" }\"},{\"line\":30,\"text\":\"\"},{\"line\":31,\"text\":\" if ($owner && $owner->user_id)\"},{\"line\":32,\"text\":\" {\"},{\"line\":33,\"text\":\" $this->alerts->consumeForUser($event, $ticket, $owner);\"},{\"line\":34,\"text\":\" $this->email->consumeForRegistered($event, $ticket, $owner);\"},{\"line\":35,\"text\":\" return;\"},{\"line\":36,\"text\":\" }\"},{\"line\":37,\"text\":\"\"},{\"line\":38,\"text\":\" $this->email->consumeForGuest($event, $ticket);\"},{\"line\":39,\"text\":\" }\"},{\"line\":40,\"text\":\"\"},{\"line\":41,\"text\":\" /**\"},{\"line\":42,\"text\":\" * @param User[] $staffUsers\"},{\"line\":43,\"text\":\" */\"},{\"line\":44,\"text\":\" public function dispatchStaff(SupportEvent $event, Ticket $ticket, array $staffUsers): void\"},{\"line\":45,\"text\":\" {\"},{\"line\":46,\"text\":\" if (!Policy::mayEmitExternal($event))\"},{\"line\":47,\"text\":\" {\"},{\"line\":48,\"text\":\" return;\"},{\"line\":49,\"text\":\" }\"},{\"line\":50,\"text\":\"\"},{\"line\":51,\"text\":\" foreach ($staffUsers as $staff)\"},{\"line\":52,\"text\":\" {\"},{\"line\":53,\"text\":\" if (!$staff instanceof User || !$staff->user_id)\"},{\"line\":54,\"text\":\" {\"},{\"line\":55,\"text\":\" continue;\"},{\"line\":56,\"text\":\" }\"},{\"line\":57,\"text\":\"\"},{\"line\":58,\"text\":\" if (!$staff->hasPermission('gramSupport', 'viewAll')\"},{\"line\":59,\"text\":\" && !$staff->hasPermission('gramSupport', 'manage'))\"},{\"line\":60,\"text\":\" {\"},{\"line\":61,\"text\":\" continue;\"},{\"line\":62,\"text\":\" }\"},{\"line\":63,\"text\":\"\"},{\"line\":64,\"text\":\" $this->alerts->consumeForUser($event, $ticket, $staff);\"},{\"line\":65,\"text\":\" }\"},{\"line\":66,\"text\":\" }\"},{\"line\":67,\"text\":\"}\"}],\"present\":true,\"sha256\":\"a043d94bc91feeed0969ad218566cebac76b2d804e7c69cc2b87b94d42f5bb85\"},\"Pub/Controller/Support.php\":{\"line_count\":236,\"matched_context\":[{\"sep\":true},{\"line\":1,\"text\":\"isPost())\"},{\"line\":37,\"text\":\" {\"},{\"line\":38,\"text\":\" $this->assertPostOnly();\"},{\"line\":39,\"text\":\"\"},{\"line\":40,\"text\":\" $email = $this->filter('email', 'str');\"},{\"line\":41,\"text\":\" $category = $this->filter('category', 'str');\"},{\"line\":42,\"text\":\" $subject = $this->filter('subject', 'str');\"},{\"line\":43,\"text\":\" $message = $this->filter('message', 'str');\"},{\"line\":44,\"text\":\"\"},{\"line\":45,\"text\":\" if (!$visitor->user_id)\"},{\"line\":46,\"text\":\" {\"},{\"line\":47,\"text\":\" if (!$this->captchaIsValid(true))\"},{\"line\":48,\"text\":\" {\"},{\"line\":49,\"text\":\" return $this->error(\"},{\"line\":50,\"text\":\" \\\\XF::phrase('did_not_complete_the_captcha_verification_properly')\"},{\"line\":51,\"text\":\" );\"},{\"line\":52,\"text\":\" }\"},{\"line\":53,\"text\":\"\"},{\"line\":54,\"text\":\" /** @var RateLimiter $rateLimiter */\"},{\"line\":55,\"text\":\" $rateLimiter = $this->service(RateLimiter::class);\"},{\"line\":56,\"text\":\" $allowed = $rateLimiter->consumeGuest(\"},{\"line\":57,\"text\":\" SiteConfig::siteId(),\"},{\"line\":58,\"text\":\" $email,\"},{\"line\":59,\"text\":\" SiteConfig::guestRateLimitCount(),\"},{\"line\":60,\"text\":\" SiteConfig::guestRateLimitWindowSeconds()\"},{\"line\":61,\"text\":\" );\"},{\"line\":62,\"text\":\"\"},{\"line\":63,\"text\":\" if (!$allowed)\"},{\"sep\":true},{\"line\":74,\"text\":\" if ($upload)\"},{\"line\":75,\"text\":\" {\"},{\"line\":76,\"text\":\" $attachmentId = $attachmentFlow->prepare($upload, $tempHash);\"},{\"line\":77,\"text\":\" }\"},{\"line\":78,\"text\":\"\"},{\"line\":79,\"text\":\" /** @var Creator $creator */\"},{\"line\":80,\"text\":\" $creator = $this->service(Creator::class);\"},{\"line\":81,\"text\":\" $created = $creator->create([\"},{\"line\":82,\"text\":\" 'email' => $email,\"},{\"line\":83,\"text\":\" 'category' => $category,\"},{\"line\":84,\"text\":\" 'subject' => $subject,\"},{\"line\":85,\"text\":\" 'message' => $message\"},{\"line\":86,\"text\":\" ]);\"},{\"line\":87,\"text\":\"\"},{\"sep\":true},{\"line\":127,\"text\":\" }\"},{\"line\":128,\"text\":\"\"},{\"line\":129,\"text\":\" return $this->view(\"},{\"line\":130,\"text\":\" 'Gram\\\\Support:Create',\"},{\"line\":131,\"text\":\" 'gram_support_create',\"},{\"line\":132,\"text\":\" ['forceCaptcha' => !$visitor->user_id]\"},{\"line\":133,\"text\":\" );\"},{\"line\":134,\"text\":\" }\"},{\"line\":135,\"text\":\"\"},{\"line\":136,\"text\":\" public function actionAccess()\"},{\"line\":137,\"text\":\" {\"},{\"sep\":true},{\"line\":183,\"text\":\" 'messages' => $messages\"},{\"line\":184,\"text\":\" ]\"},{\"line\":185,\"text\":\" );\"},{\"line\":186,\"text\":\" }\"},{\"line\":187,\"text\":\"\"},{\"line\":188,\"text\":\" public function actionReply()\"},{\"line\":189,\"text\":\" {\"},{\"line\":190,\"text\":\" $this->assertPostOnly();\"},{\"line\":191,\"text\":\"\"},{\"line\":192,\"text\":\" $ticket = $this->assertViewableTicketByRef($this->filter('ref', 'str'));\"},{\"line\":193,\"text\":\" $visitor = \\\\XF::visitor();\"},{\"line\":194,\"text\":\"\"},{\"line\":195,\"text\":\" if ($visitor->user_id)\"},{\"line\":196,\"text\":\" {\"},{\"line\":197,\"text\":\" if ((int)$ticket->user_id !== (int)$visitor->user_id\"},{\"line\":198,\"text\":\" || !$visitor->hasPermission('gramSupport', 'replyOwn'))\"},{\"line\":199,\"text\":\" {\"},{\"line\":200,\"text\":\" return $this->noPermission();\"},{\"line\":201,\"text\":\" }\"},{\"line\":202,\"text\":\" }\"},{\"line\":203,\"text\":\" elseif (!GuestTicketSession::hasGrant($ticket))\"},{\"sep\":true},{\"line\":205,\"text\":\" return $this->noPermission();\"},{\"line\":206,\"text\":\" }\"},{\"line\":207,\"text\":\"\"},{\"line\":208,\"text\":\" $message = $this->filter('message', 'str');\"},{\"line\":209,\"text\":\"\"},{\"line\":210,\"text\":\" /** @var Reply $reply */\"},{\"line\":211,\"text\":\" $reply = $this->service(Reply::class);\"},{\"line\":212,\"text\":\" $reply->add($ticket, $message, false, false);\"},{\"line\":213,\"text\":\"\"},{\"line\":214,\"text\":\" return $this->redirect(\"},{\"line\":215,\"text\":\" $this->buildLink('support/ticket', null, ['ref' => $ticket->public_ref])\"},{\"line\":216,\"text\":\" );\"},{\"line\":217,\"text\":\" }\"}],\"present\":true,\"sha256\":\"6cb34b1d27e9f0132263637cefa62fe0e998a9a86620b7ac33199f41ee86f9d4\"},\"Service/Ticket/Creator.php\":{\"line_count\":139,\"matched_context\":[{\"sep\":true},{\"line\":8,\"text\":\"use Gram\\\\Support\\\\Util\\\\PublicRef;\"},{\"line\":9,\"text\":\"use Gram\\\\Support\\\\Util\\\\SafeContext;\"},{\"line\":10,\"text\":\"use Gram\\\\Support\\\\Util\\\\SiteConfig;\"},{\"line\":11,\"text\":\"use Gram\\\\Support\\\\Util\\\\StatusPolicy;\"},{\"line\":12,\"text\":\"use XF\\\\Service\\\\AbstractService;\"},{\"line\":13,\"text\":\"use Gram\\\\Support\\\\Notification\\\\EventDispatcher;\"},{\"line\":14,\"text\":\"\"},{\"line\":15,\"text\":\"class Creator extends AbstractService\"},{\"line\":16,\"text\":\"{\"},{\"line\":17,\"text\":\" public function create(array $input, ?int $now = null): array\"},{\"line\":18,\"text\":\" {\"},{\"line\":19,\"text\":\" $now = $now ?? \\\\XF::$time;\"},{\"line\":20,\"text\":\" $visitor = \\\\XF::visitor();\"},{\"line\":21,\"text\":\" $siteId = SiteConfig::siteId();\"},{\"line\":22,\"text\":\"\"},{\"line\":23,\"text\":\" $email = strtolower(trim((string)($input['email'] ?? '')));\"},{\"line\":24,\"text\":\" $category = trim((string)($input['category'] ?? ''));\"},{\"line\":25,\"text\":\" $subject = trim((string)($input['subject'] ?? ''));\"},{\"line\":26,\"text\":\" $messageText = trim((string)($input['message'] ?? ''));\"},{\"line\":27,\"text\":\"\"},{\"line\":28,\"text\":\" if ($category === '' || $messageText === '')\"},{\"line\":29,\"text\":\" {\"},{\"line\":30,\"text\":\" throw new \\\\InvalidArgumentException('GRAM_SUPPORT_REQUIRED_FIELDS');\"},{\"line\":31,\"text\":\" }\"},{\"line\":32,\"text\":\"\"},{\"line\":33,\"text\":\" if (!$visitor->user_id && !filter_var($email, FILTER_VALIDATE_EMAIL))\"},{\"line\":34,\"text\":\" {\"},{\"line\":35,\"text\":\" throw new \\\\InvalidArgumentException('GRAM_SUPPORT_VALID_EMAIL_REQUIRED');\"},{\"line\":36,\"text\":\" }\"},{\"line\":37,\"text\":\"\"},{\"line\":38,\"text\":\" if ($subject === '')\"},{\"line\":39,\"text\":\" {\"},{\"line\":40,\"text\":\" $subject = function_exists('mb_substr')\"},{\"sep\":true},{\"line\":68,\"text\":\" /** @var Ticket $ticket */\"},{\"line\":69,\"text\":\" $ticket = $this->em()->create('Gram\\\\Support:Ticket');\"},{\"line\":70,\"text\":\" $ticket->public_ref = 'TMP-' . bin2hex(random_bytes(12));\"},{\"line\":71,\"text\":\" $ticket->site_id = $siteId;\"},{\"line\":72,\"text\":\" $ticket->user_id = (int)$visitor->user_id;\"},{\"line\":73,\"text\":\" $ticket->guest_email = $visitor->user_id ? '' : $email;\"},{\"line\":74,\"text\":\" $ticket->guest_access_token_hash = $guest ? $guest['hash'] : '';\"},{\"line\":75,\"text\":\" $ticket->guest_access_expires = $guest\"},{\"line\":76,\"text\":\" ? $now + SiteConfig::guestTokenTtlSeconds()\"},{\"line\":77,\"text\":\" : 0;\"},{\"line\":78,\"text\":\" $ticket->node_id = (int)($context['node_id'] ?? 0);\"},{\"sep\":true},{\"line\":96,\"text\":\" $ticket->save();\"},{\"line\":97,\"text\":\"\"},{\"line\":98,\"text\":\" $message = $this->em()->create('Gram\\\\Support:Message');\"},{\"line\":99,\"text\":\" $message->ticket_id = (int)$ticket->ticket_id;\"},{\"line\":100,\"text\":\" $message->user_id = (int)$visitor->user_id;\"},{\"line\":101,\"text\":\" $message->is_staff = 0;\"},{\"line\":102,\"text\":\" $message->is_internal = 0;\"},{\"line\":103,\"text\":\" $message->message = $messageText;\"},{\"line\":104,\"text\":\" $message->message_date = $now;\"},{\"line\":105,\"text\":\" $message->save();\"},{\"line\":106,\"text\":\"\"},{\"line\":107,\"text\":\" $ticket->last_message_id = (int)$message->message_id;\"},{\"line\":108,\"text\":\" $ticket->save();\"},{\"line\":109,\"text\":\"\"},{\"line\":110,\"text\":\" $event = $this->em()->create('Gram\\\\Support:SupportEvent');\"},{\"line\":111,\"text\":\" $event->ticket_id = (int)$ticket->ticket_id;\"},{\"line\":112,\"text\":\" $event->site_id = $siteId;\"},{\"line\":113,\"text\":\" $event->actor_user_id = (int)$visitor->user_id;\"},{\"line\":114,\"text\":\" $event->event_type = 'ticket_created';\"},{\"line\":115,\"text\":\" $event->event_date = $now;\"},{\"line\":116,\"text\":\" $event->safe_payload_json = json_encode([\"},{\"line\":117,\"text\":\" 'public_ref' => (string)$ticket->public_ref,\"},{\"line\":118,\"text\":\" 'category' => (string)$ticket->category,\"},{\"line\":119,\"text\":\" 'is_internal' => false\"},{\"sep\":true},{\"line\":126,\"text\":\" {\"},{\"line\":127,\"text\":\" $db->rollback();\"},{\"line\":128,\"text\":\" throw $e;\"},{\"line\":129,\"text\":\" }\"},{\"line\":130,\"text\":\"\"},{\"line\":131,\"text\":\" (new EventDispatcher())->dispatchSafely($event, $ticket);\"},{\"line\":132,\"text\":\"\"},{\"line\":133,\"text\":\" return [\"},{\"line\":134,\"text\":\" 'ticket' => $ticket,\"},{\"line\":135,\"text\":\" 'event' => $event,\"},{\"line\":136,\"text\":\" 'guest_secret' => $guest ? $guest['secret'] : null\"}],\"present\":true,\"sha256\":\"7a84cd77761a7943495f9d3ef11269987f85265170ad6cf927a1661b8bf0a99a\"},\"Service/Ticket/Reply.php\":{\"line_count\":68,\"matched_context\":[{\"sep\":true},{\"line\":3,\"text\":\"namespace Gram\\\\Support\\\\Service\\\\Ticket;\"},{\"line\":4,\"text\":\"\"},{\"line\":5,\"text\":\"use Gram\\\\Support\\\\Entity\\\\Ticket;\"},{\"line\":6,\"text\":\"use Gram\\\\Support\\\\Util\\\\SiteConfig;\"},{\"line\":7,\"text\":\"use XF\\\\Service\\\\AbstractService;\"},{\"line\":8,\"text\":\"use Gram\\\\Support\\\\Notification\\\\EventDispatcher;\"},{\"line\":9,\"text\":\"\"},{\"line\":10,\"text\":\"class Reply extends AbstractService\"},{\"line\":11,\"text\":\"{\"},{\"line\":12,\"text\":\" public function add(Ticket $ticket, string $messageText, bool $isStaff, bool $isInternal = false): array\"},{\"line\":13,\"text\":\" {\"},{\"line\":14,\"text\":\" $messageText = trim($messageText);\"},{\"line\":15,\"text\":\" if ($messageText === '')\"},{\"line\":16,\"text\":\" {\"},{\"line\":17,\"text\":\" throw new \\\\InvalidArgumentException('GRAM_SUPPORT_EMPTY_REPLY');\"},{\"line\":18,\"text\":\" }\"},{\"line\":19,\"text\":\" if ($isInternal && !$isStaff)\"},{\"line\":20,\"text\":\" {\"},{\"line\":21,\"text\":\" throw new \\\\LogicException('INTERNAL_REQUIRES_STAFF');\"},{\"line\":22,\"text\":\" }\"},{\"line\":23,\"text\":\"\"},{\"line\":24,\"text\":\" $visitor = \\\\XF::visitor();\"},{\"line\":25,\"text\":\" $now = \\\\XF::$time;\"},{\"line\":26,\"text\":\" $db = \\\\XF::db();\"},{\"sep\":true},{\"line\":29,\"text\":\" try\"},{\"line\":30,\"text\":\" {\"},{\"line\":31,\"text\":\" $message = $this->em()->create('Gram\\\\Support:Message');\"},{\"line\":32,\"text\":\" $message->ticket_id = (int)$ticket->ticket_id;\"},{\"line\":33,\"text\":\" $message->user_id = (int)$visitor->user_id;\"},{\"line\":34,\"text\":\" $message->is_staff = $isStaff ? 1 : 0;\"},{\"line\":35,\"text\":\" $message->is_internal = $isInternal ? 1 : 0;\"},{\"line\":36,\"text\":\" $message->message = $messageText;\"},{\"line\":37,\"text\":\" $message->message_date = $now;\"},{\"line\":38,\"text\":\" $message->save();\"},{\"line\":39,\"text\":\"\"},{\"line\":40,\"text\":\" $ticket->last_message_id = (int)$message->message_id;\"},{\"line\":41,\"text\":\" $ticket->updated_date = $now;\"},{\"line\":42,\"text\":\" $ticket->save();\"},{\"line\":43,\"text\":\"\"},{\"line\":44,\"text\":\" $event = $this->em()->create('Gram\\\\Support:SupportEvent');\"},{\"line\":45,\"text\":\" $event->ticket_id = (int)$ticket->ticket_id;\"},{\"line\":46,\"text\":\" $event->site_id = (string)$ticket->site_id;\"},{\"line\":47,\"text\":\" $event->actor_user_id = (int)$visitor->user_id;\"},{\"line\":48,\"text\":\" $event->event_type = $isInternal ? 'internal_note_created' : 'ticket_reply_created';\"},{\"line\":49,\"text\":\" $event->event_date = $now;\"},{\"line\":50,\"text\":\" $event->safe_payload_json = json_encode([\"},{\"line\":51,\"text\":\" 'public_ref' => (string)$ticket->public_ref,\"},{\"line\":52,\"text\":\" 'is_internal' => $isInternal\"},{\"line\":53,\"text\":\" ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);\"},{\"sep\":true},{\"line\":59,\"text\":\" {\"},{\"line\":60,\"text\":\" $db->rollback();\"},{\"line\":61,\"text\":\" throw $e;\"},{\"line\":62,\"text\":\" }\"},{\"line\":63,\"text\":\"\"},{\"line\":64,\"text\":\" (new EventDispatcher())->dispatchSafely($event, $ticket);\"},{\"line\":65,\"text\":\"\"},{\"line\":66,\"text\":\" return ['message' => $message, 'event' => $event];\"},{\"line\":67,\"text\":\" }\"},{\"line\":68,\"text\":\"}\"}],\"present\":true,\"sha256\":\"482571a2ccf07a3c8672f34940243264a8271ef8c3cbc4fadbb46112afd36830\"},\"_data/templates.xml\":{\"line_count\":781,\"matched_context\":[{\"sep\":true},{\"line\":1,\"text\":\"\"},{\"line\":2,\"text\":\"\"},{\"line\":3,\"text\":\" \"},{\"line\":9,\"text\":\" \"},{\"line\":553,\"text\":\" \"},{\"line\":575,\"text\":\" \"},{\"line\":647,\"text\":\" \"},{\"line\":764,\"text\":\" \"},{\"line\":772,\"text\":\" \"},{\"line\":781,\"text\":\"\"}],\"present\":true,\"sha256\":\"1a344bd306135486d2019bf0a5cafff5eb472882f7b3433ef13af4556a4c791e\"}}},\"transport_rc\":0,\"transport_stderr_bytes\":0,\"transport_stderr_sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}\nGRAM516R1R3_NOTIFICATION_FILES=[\"Notification/AlertConsumer.php\",\"Notification/DeliveryGate.php\",\"Notification/Dispatcher.php\",\"Notification/EmailConsumer.php\",\"Notification/EntityDeliveryGate.php\",\"Notification/EventDispatcher.php\",\"Notification/Policy.php\",\"Notification/RecipientKey.php\"]\nGRAM516R1R3_EMAIL_CONSUMER_FILE_PRESENT=true\nGRAM516R1R3_DECISION=PASS_NOTIFY_CAPTCHA_SOURCE_EVIDENCE_CAPTURED\nGRAM516R1R3_NEXT_GATE=BUILD_EXACT_TURNSTILE_AND_EMAIL_NOTIFY_CHANGE\nHOMELAB_RESULT_CONTRACT={\"version\":1,\"command_id\":\"SUPPORT-260918-GRAM-SUPPORT-NOTIFY-CAPTCHA-SOURCE-READONLY-516R1R3\",\"status\":\"OK\",\"changes_made\":false,\"rollback_started\":false,\"rollback_restored\":null}\n" +} diff --git a/runtime/history/SUPPORT-260918-GRAM-SUPPORT-NOTIFY-CAPTCHA-SOURCE-READONLY-516R1R3.txt b/runtime/history/SUPPORT-260918-GRAM-SUPPORT-NOTIFY-CAPTCHA-SOURCE-READONLY-516R1R3.txt new file mode 100644 index 00000000..b05940fc --- /dev/null +++ b/runtime/history/SUPPORT-260918-GRAM-SUPPORT-NOTIFY-CAPTCHA-SOURCE-READONLY-516R1R3.txt @@ -0,0 +1,38 @@ +CHAT_OUTPUT_BEGIN +COMMAND_ID=SUPPORT-260918-GRAM-SUPPORT-NOTIFY-CAPTCHA-SOURCE-READONLY-516R1R3 +STATUS=OK +RC=0 +HOST=pve01 +MODE=read-only +COMPONENT=gram-support-notify-captcha-source-readonly +REFERENCE_REGISTER_CHECK=OK +REFERENCE_SHA256=5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66 +ERROR_REGISTER_CHECK=OK +ERROR_REGISTER_SHA256=3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0 +COMMAND_SHA256=0f7a476d59191ec993679cb3fe3a569eb7e5b72805ab8c6e63c390f6a14915fc +DUPLICATE_FAILED_COMMAND_BLOCKED=false +EXECUTION_STARTED=true +CHANGE_DECLARED=false +RESULT_CONTRACT_VALID=true +RESULT_CONTRACT_STATUS=NOT_APPLICABLE +RESULT_CONTRACT_ERROR=NONE +COMMAND_RC=0 +CHANGES_MADE=false +ROLLBACK_STARTED=false +ROLLBACK_RESTORED=null +MUTATION_OUTCOME=NO_MUTATION +SANITIZED=yes +SECRETS_INCLUDED=no +PRIVATE_ADDRESSES_INCLUDED=no +RAW_EVIDENCE_SHA256=6eb6b9ac7ab7b4c61eb06b40dd1f2390fcac654e496097728a954feacd2b3c9c +SANITIZED_OUTPUT_SHA256=6eb6b9ac7ab7b4c61eb06b40dd1f2390fcac654e496097728a954feacd2b3c9c +OUTPUT_BEGIN +GRAM516R1R3_SAFE_REPORT={"err_truncated":false,"guest_stderr_present":false,"inner_rc":0,"ok":true,"out_truncated":false,"source":{"notification_files":["Notification/AlertConsumer.php","Notification/DeliveryGate.php","Notification/Dispatcher.php","Notification/EmailConsumer.php","Notification/EntityDeliveryGate.php","Notification/EventDispatcher.php","Notification/Policy.php","Notification/RecipientKey.php"],"ok":true,"root_tree_files":["Attachment/TicketHandler.php","Entity/AttachmentMeta.php","Entity/Delivery.php","Entity/Message.php","Entity/RateLimit.php","Entity/SupportEvent.php","Entity/Ticket.php","Listener/SourcePageContext.php","Notification/AlertConsumer.php","Notification/DeliveryGate.php","Notification/Dispatcher.php","Notification/EmailConsumer.php","Notification/EntityDeliveryGate.php","Notification/EventDispatcher.php","Notification/Policy.php","Notification/RecipientKey.php","Pub/Controller/AccountSupport.php","Pub/Controller/Pwa.php","Pub/Controller/StaffSupport.php","Pub/Controller/Support.php","Pub/View/PwaServiceWorker.php","Repository/Message.php","Repository/Ticket.php","Security/GuestTicketSession.php","Security/TicketAccess.php","Service/GuestScreenshotService.php","Service/GuestTicketCreateAttachmentFlow.php","Service/RateLimiter.php","Service/SourceContextResolver.php","Service/Ticket/Creator.php","Service/Ticket/Reply.php","Service/Ticket/StaffUpdate.php","Setup.php","Util/GuestToken.php","Util/PublicRef.php","Util/RateKey.php","Util/SafeContext.php","Util/SiteConfig.php","Util/StatusPolicy.php","_data/code_event_listeners.xml","_data/content_type_fields.xml","_data/option_groups.xml","_data/options.xml","_data/permission_interface_groups.xml","_data/permissions.xml","_data/phrases.xml","_data/routes.xml","_data/template_modifications.xml","_data/templates.xml","addon.json","hashes.json"],"targets":{"Notification/AlertConsumer.php":{"line_count":74,"matched_context":[{"sep":true},{"line":1,"text":"gate = $gate;"},{"line":16,"text":" }"},{"line":17,"text":""},{"line":18,"text":" public function consumeForUser(SupportEvent $event, Ticket $ticket, User $recipient): bool"},{"line":19,"text":" {"},{"line":20,"text":" if (!Policy::mayEmitExternal($event) || !$recipient->user_id)"},{"line":21,"text":" {"},{"line":22,"text":" return false;"},{"line":23,"text":" }"},{"line":24,"text":""},{"line":25,"text":" // Never alert a user about a ticket they cannot access."},{"line":26,"text":" $isOwner = $ticket->user_id && (int)$ticket->user_id === (int)$recipient->user_id;"},{"line":27,"text":" $isStaff = $recipient->hasPermission('gramSupport', 'viewAll')"},{"line":28,"text":" || $recipient->hasPermission('gramSupport', 'manage');"},{"line":29,"text":""},{"line":30,"text":" if (!$isOwner && !$isStaff)"},{"line":31,"text":" {"},{"line":32,"text":" return false;"},{"line":33,"text":" }"},{"line":34,"text":""},{"line":35,"text":" $recipientKey = RecipientKey::user((int)$recipient->user_id);"},{"line":36,"text":" if (!$this->gate->acquire((int)$event->event_id, 'xf_alert', $recipientKey))"},{"line":37,"text":" {"},{"line":38,"text":" return false;"},{"line":39,"text":" }"},{"line":40,"text":""},{"line":41,"text":" try"},{"line":42,"text":" {"},{"line":43,"text":" /** @var \\XF\\Repository\\UserAlertRepository $alerts */"},{"line":44,"text":" $alerts = \\XF::repository('XF:UserAlert');"},{"line":45,"text":""},{"line":46,"text":" // Back the alert with the recipient's own User content so the"},{"line":47,"text":" // core alert handler remains valid/viewable. Ticket access is"},{"line":48,"text":" // separately enforced before emission and again on ticket route."},{"line":49,"text":" $alerts->alert("},{"line":50,"text":" $recipient,"},{"line":51,"text":" 0,"},{"line":52,"text":" 'Support',"},{"line":53,"text":" 'user',"},{"line":54,"text":" (int)$recipient->user_id,"},{"line":55,"text":" 'gram_support_evt',"},{"line":56,"text":" Policy::alertExtraData($event),"},{"line":57,"text":" ['usePush' => false]"},{"line":58,"text":" );"},{"line":59,"text":""},{"line":60,"text":" $this->gate->markSent((int)$event->event_id, 'xf_alert', $recipientKey);"},{"line":61,"text":" return true;"},{"line":62,"text":" }"},{"line":63,"text":" catch (\\Throwable $e)"},{"line":64,"text":" {"},{"line":65,"text":" $this->gate->markFailed("},{"line":66,"text":" (int)$event->event_id,"},{"line":67,"text":" 'xf_alert',"},{"line":68,"text":" $recipientKey,"},{"line":69,"text":" get_class($e)"},{"line":70,"text":" );"},{"line":71,"text":" throw $e;"},{"line":72,"text":" }"}],"present":true,"sha256":"e22d0cdb0a0806c6a231504409bd17cd781caab0f28181aff7246b0295d8cd98"},"Notification/Dispatcher.php":{"line_count":67,"matched_context":[{"sep":true},{"line":1,"text":"alerts = $alerts;"},{"line":17,"text":" $this->email = $email;"},{"line":18,"text":" }"},{"line":19,"text":""},{"line":20,"text":" /**"},{"line":21,"text":" * Owner-facing delivery. Staff alert fan-out is intentionally provided by"},{"line":22,"text":" * dispatchStaff() so staff recipients must still pass Gram Support ACL."},{"line":23,"text":" */"},{"line":24,"text":" public function dispatchOwner(SupportEvent $event, Ticket $ticket, ?User $owner): void"},{"line":25,"text":" {"},{"line":26,"text":" if (!Policy::mayEmitExternal($event))"},{"line":27,"text":" {"},{"line":28,"text":" return;"},{"line":29,"text":" }"},{"line":30,"text":""},{"line":31,"text":" if ($owner && $owner->user_id)"},{"line":32,"text":" {"},{"line":33,"text":" $this->alerts->consumeForUser($event, $ticket, $owner);"},{"line":34,"text":" $this->email->consumeForRegistered($event, $ticket, $owner);"},{"line":35,"text":" return;"},{"line":36,"text":" }"},{"line":37,"text":""},{"line":38,"text":" $this->email->consumeForGuest($event, $ticket);"},{"line":39,"text":" }"},{"line":40,"text":""},{"line":41,"text":" /**"},{"line":42,"text":" * @param User[] $staffUsers"},{"line":43,"text":" */"},{"line":44,"text":" public function dispatchStaff(SupportEvent $event, Ticket $ticket, array $staffUsers): void"},{"line":45,"text":" {"},{"line":46,"text":" if (!Policy::mayEmitExternal($event))"},{"line":47,"text":" {"},{"line":48,"text":" return;"},{"line":49,"text":" }"},{"line":50,"text":""},{"line":51,"text":" foreach ($staffUsers as $staff)"},{"line":52,"text":" {"},{"line":53,"text":" if (!$staff instanceof User || !$staff->user_id)"},{"line":54,"text":" {"},{"line":55,"text":" continue;"},{"line":56,"text":" }"},{"line":57,"text":""},{"line":58,"text":" if (!$staff->hasPermission('gramSupport', 'viewAll')"},{"line":59,"text":" && !$staff->hasPermission('gramSupport', 'manage'))"},{"line":60,"text":" {"},{"line":61,"text":" continue;"},{"line":62,"text":" }"},{"line":63,"text":""},{"line":64,"text":" $this->alerts->consumeForUser($event, $ticket, $staff);"},{"line":65,"text":" }"},{"line":66,"text":" }"},{"line":67,"text":"}"}],"present":true,"sha256":"a043d94bc91feeed0969ad218566cebac76b2d804e7c69cc2b87b94d42f5bb85"},"Pub/Controller/Support.php":{"line_count":236,"matched_context":[{"sep":true},{"line":1,"text":"isPost())"},{"line":37,"text":" {"},{"line":38,"text":" $this->assertPostOnly();"},{"line":39,"text":""},{"line":40,"text":" $email = $this->filter('email', 'str');"},{"line":41,"text":" $category = $this->filter('category', 'str');"},{"line":42,"text":" $subject = $this->filter('subject', 'str');"},{"line":43,"text":" $message = $this->filter('message', 'str');"},{"line":44,"text":""},{"line":45,"text":" if (!$visitor->user_id)"},{"line":46,"text":" {"},{"line":47,"text":" if (!$this->captchaIsValid(true))"},{"line":48,"text":" {"},{"line":49,"text":" return $this->error("},{"line":50,"text":" \\XF::phrase('did_not_complete_the_captcha_verification_properly')"},{"line":51,"text":" );"},{"line":52,"text":" }"},{"line":53,"text":""},{"line":54,"text":" /** @var RateLimiter $rateLimiter */"},{"line":55,"text":" $rateLimiter = $this->service(RateLimiter::class);"},{"line":56,"text":" $allowed = $rateLimiter->consumeGuest("},{"line":57,"text":" SiteConfig::siteId(),"},{"line":58,"text":" $email,"},{"line":59,"text":" SiteConfig::guestRateLimitCount(),"},{"line":60,"text":" SiteConfig::guestRateLimitWindowSeconds()"},{"line":61,"text":" );"},{"line":62,"text":""},{"line":63,"text":" if (!$allowed)"},{"sep":true},{"line":74,"text":" if ($upload)"},{"line":75,"text":" {"},{"line":76,"text":" $attachmentId = $attachmentFlow->prepare($upload, $tempHash);"},{"line":77,"text":" }"},{"line":78,"text":""},{"line":79,"text":" /** @var Creator $creator */"},{"line":80,"text":" $creator = $this->service(Creator::class);"},{"line":81,"text":" $created = $creator->create(["},{"line":82,"text":" 'email' => $email,"},{"line":83,"text":" 'category' => $category,"},{"line":84,"text":" 'subject' => $subject,"},{"line":85,"text":" 'message' => $message"},{"line":86,"text":" ]);"},{"line":87,"text":""},{"sep":true},{"line":127,"text":" }"},{"line":128,"text":""},{"line":129,"text":" return $this->view("},{"line":130,"text":" 'Gram\\Support:Create',"},{"line":131,"text":" 'gram_support_create',"},{"line":132,"text":" ['forceCaptcha' => !$visitor->user_id]"},{"line":133,"text":" );"},{"line":134,"text":" }"},{"line":135,"text":""},{"line":136,"text":" public function actionAccess()"},{"line":137,"text":" {"},{"sep":true},{"line":183,"text":" 'messages' => $messages"},{"line":184,"text":" ]"},{"line":185,"text":" );"},{"line":186,"text":" }"},{"line":187,"text":""},{"line":188,"text":" public function actionReply()"},{"line":189,"text":" {"},{"line":190,"text":" $this->assertPostOnly();"},{"line":191,"text":""},{"line":192,"text":" $ticket = $this->assertViewableTicketByRef($this->filter('ref', 'str'));"},{"line":193,"text":" $visitor = \\XF::visitor();"},{"line":194,"text":""},{"line":195,"text":" if ($visitor->user_id)"},{"line":196,"text":" {"},{"line":197,"text":" if ((int)$ticket->user_id !== (int)$visitor->user_id"},{"line":198,"text":" || !$visitor->hasPermission('gramSupport', 'replyOwn'))"},{"line":199,"text":" {"},{"line":200,"text":" return $this->noPermission();"},{"line":201,"text":" }"},{"line":202,"text":" }"},{"line":203,"text":" elseif (!GuestTicketSession::hasGrant($ticket))"},{"sep":true},{"line":205,"text":" return $this->noPermission();"},{"line":206,"text":" }"},{"line":207,"text":""},{"line":208,"text":" $message = $this->filter('message', 'str');"},{"line":209,"text":""},{"line":210,"text":" /** @var Reply $reply */"},{"line":211,"text":" $reply = $this->service(Reply::class);"},{"line":212,"text":" $reply->add($ticket, $message, false, false);"},{"line":213,"text":""},{"line":214,"text":" return $this->redirect("},{"line":215,"text":" $this->buildLink('support/ticket', null, ['ref' => $ticket->public_ref])"},{"line":216,"text":" );"},{"line":217,"text":" }"}],"present":true,"sha256":"6cb34b1d27e9f0132263637cefa62fe0e998a9a86620b7ac33199f41ee86f9d4"},"Service/Ticket/Creator.php":{"line_count":139,"matched_context":[{"sep":true},{"line":8,"text":"use Gram\\Support\\Util\\PublicRef;"},{"line":9,"text":"use Gram\\Support\\Util\\SafeContext;"},{"line":10,"text":"use Gram\\Support\\Util\\SiteConfig;"},{"line":11,"text":"use Gram\\Support\\Util\\StatusPolicy;"},{"line":12,"text":"use XF\\Service\\AbstractService;"},{"line":13,"text":"use Gram\\Support\\Notification\\EventDispatcher;"},{"line":14,"text":""},{"line":15,"text":"class Creator extends AbstractService"},{"line":16,"text":"{"},{"line":17,"text":" public function create(array $input, ?int $now = null): array"},{"line":18,"text":" {"},{"line":19,"text":" $now = $now ?? \\XF::$time;"},{"line":20,"text":" $visitor = \\XF::visitor();"},{"line":21,"text":" $siteId = SiteConfig::siteId();"},{"line":22,"text":""},{"line":23,"text":" $email = strtolower(trim((string)($input['email'] ?? '')));"},{"line":24,"text":" $category = trim((string)($input['category'] ?? ''));"},{"line":25,"text":" $subject = trim((string)($input['subject'] ?? ''));"},{"line":26,"text":" $messageText = trim((string)($input['message'] ?? ''));"},{"line":27,"text":""},{"line":28,"text":" if ($category === '' || $messageText === '')"},{"line":29,"text":" {"},{"line":30,"text":" throw new \\InvalidArgumentException('GRAM_SUPPORT_REQUIRED_FIELDS');"},{"line":31,"text":" }"},{"line":32,"text":""},{"line":33,"text":" if (!$visitor->user_id && !filter_var($email, FILTER_VALIDATE_EMAIL))"},{"line":34,"text":" {"},{"line":35,"text":" throw new \\InvalidArgumentException('GRAM_SUPPORT_VALID_EMAIL_REQUIRED');"},{"line":36,"text":" }"},{"line":37,"text":""},{"line":38,"text":" if ($subject === '')"},{"line":39,"text":" {"},{"line":40,"text":" $subject = function_exists('mb_substr')"},{"sep":true},{"line":68,"text":" /** @var Ticket $ticket */"},{"line":69,"text":" $ticket = $this->em()->create('Gram\\Support:Ticket');"},{"line":70,"text":" $ticket->public_ref = 'TMP-' . bin2hex(random_bytes(12));"},{"line":71,"text":" $ticket->site_id = $siteId;"},{"line":72,"text":" $ticket->user_id = (int)$visitor->user_id;"},{"line":73,"text":" $ticket->guest_email = $visitor->user_id ? '' : $email;"},{"line":74,"text":" $ticket->guest_access_token_hash = $guest ? $guest['hash'] : '';"},{"line":75,"text":" $ticket->guest_access_expires = $guest"},{"line":76,"text":" ? $now + SiteConfig::guestTokenTtlSeconds()"},{"line":77,"text":" : 0;"},{"line":78,"text":" $ticket->node_id = (int)($context['node_id'] ?? 0);"},{"sep":true},{"line":96,"text":" $ticket->save();"},{"line":97,"text":""},{"line":98,"text":" $message = $this->em()->create('Gram\\Support:Message');"},{"line":99,"text":" $message->ticket_id = (int)$ticket->ticket_id;"},{"line":100,"text":" $message->user_id = (int)$visitor->user_id;"},{"line":101,"text":" $message->is_staff = 0;"},{"line":102,"text":" $message->is_internal = 0;"},{"line":103,"text":" $message->message = $messageText;"},{"line":104,"text":" $message->message_date = $now;"},{"line":105,"text":" $message->save();"},{"line":106,"text":""},{"line":107,"text":" $ticket->last_message_id = (int)$message->message_id;"},{"line":108,"text":" $ticket->save();"},{"line":109,"text":""},{"line":110,"text":" $event = $this->em()->create('Gram\\Support:SupportEvent');"},{"line":111,"text":" $event->ticket_id = (int)$ticket->ticket_id;"},{"line":112,"text":" $event->site_id = $siteId;"},{"line":113,"text":" $event->actor_user_id = (int)$visitor->user_id;"},{"line":114,"text":" $event->event_type = 'ticket_created';"},{"line":115,"text":" $event->event_date = $now;"},{"line":116,"text":" $event->safe_payload_json = json_encode(["},{"line":117,"text":" 'public_ref' => (string)$ticket->public_ref,"},{"line":118,"text":" 'category' => (string)$ticket->category,"},{"line":119,"text":" 'is_internal' => false"},{"sep":true},{"line":126,"text":" {"},{"line":127,"text":" $db->rollback();"},{"line":128,"text":" throw $e;"},{"line":129,"text":" }"},{"line":130,"text":""},{"line":131,"text":" (new EventDispatcher())->dispatchSafely($event, $ticket);"},{"line":132,"text":""},{"line":133,"text":" return ["},{"line":134,"text":" 'ticket' => $ticket,"},{"line":135,"text":" 'event' => $event,"},{"line":136,"text":" 'guest_secret' => $guest ? $guest['secret'] : null"}],"present":true,"sha256":"7a84cd77761a7943495f9d3ef11269987f85265170ad6cf927a1661b8bf0a99a"},"Service/Ticket/Reply.php":{"line_count":68,"matched_context":[{"sep":true},{"line":3,"text":"namespace Gram\\Support\\Service\\Ticket;"},{"line":4,"text":""},{"line":5,"text":"use Gram\\Support\\Entity\\Ticket;"},{"line":6,"text":"use Gram\\Support\\Util\\SiteConfig;"},{"line":7,"text":"use XF\\Service\\AbstractService;"},{"line":8,"text":"use Gram\\Support\\Notification\\EventDispatcher;"},{"line":9,"text":""},{"line":10,"text":"class Reply extends AbstractService"},{"line":11,"text":"{"},{"line":12,"text":" public function add(Ticket $ticket, string $messageText, bool $isStaff, bool $isInternal = false): array"},{"line":13,"text":" {"},{"line":14,"text":" $messageText = trim($messageText);"},{"line":15,"text":" if ($messageText === '')"},{"line":16,"text":" {"},{"line":17,"text":" throw new \\InvalidArgumentException('GRAM_SUPPORT_EMPTY_REPLY');"},{"line":18,"text":" }"},{"line":19,"text":" if ($isInternal && !$isStaff)"},{"line":20,"text":" {"},{"line":21,"text":" throw new \\LogicException('INTERNAL_REQUIRES_STAFF');"},{"line":22,"text":" }"},{"line":23,"text":""},{"line":24,"text":" $visitor = \\XF::visitor();"},{"line":25,"text":" $now = \\XF::$time;"},{"line":26,"text":" $db = \\XF::db();"},{"sep":true},{"line":29,"text":" try"},{"line":30,"text":" {"},{"line":31,"text":" $message = $this->em()->create('Gram\\Support:Message');"},{"line":32,"text":" $message->ticket_id = (int)$ticket->ticket_id;"},{"line":33,"text":" $message->user_id = (int)$visitor->user_id;"},{"line":34,"text":" $message->is_staff = $isStaff ? 1 : 0;"},{"line":35,"text":" $message->is_internal = $isInternal ? 1 : 0;"},{"line":36,"text":" $message->message = $messageText;"},{"line":37,"text":" $message->message_date = $now;"},{"line":38,"text":" $message->save();"},{"line":39,"text":""},{"line":40,"text":" $ticket->last_message_id = (int)$message->message_id;"},{"line":41,"text":" $ticket->updated_date = $now;"},{"line":42,"text":" $ticket->save();"},{"line":43,"text":""},{"line":44,"text":" $event = $this->em()->create('Gram\\Support:SupportEvent');"},{"line":45,"text":" $event->ticket_id = (int)$ticket->ticket_id;"},{"line":46,"text":" $event->site_id = (string)$ticket->site_id;"},{"line":47,"text":" $event->actor_user_id = (int)$visitor->user_id;"},{"line":48,"text":" $event->event_type = $isInternal ? 'internal_note_created' : 'ticket_reply_created';"},{"line":49,"text":" $event->event_date = $now;"},{"line":50,"text":" $event->safe_payload_json = json_encode(["},{"line":51,"text":" 'public_ref' => (string)$ticket->public_ref,"},{"line":52,"text":" 'is_internal' => $isInternal"},{"line":53,"text":" ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);"},{"sep":true},{"line":59,"text":" {"},{"line":60,"text":" $db->rollback();"},{"line":61,"text":" throw $e;"},{"line":62,"text":" }"},{"line":63,"text":""},{"line":64,"text":" (new EventDispatcher())->dispatchSafely($event, $ticket);"},{"line":65,"text":""},{"line":66,"text":" return ['message' => $message, 'event' => $event];"},{"line":67,"text":" }"},{"line":68,"text":"}"}],"present":true,"sha256":"482571a2ccf07a3c8672f34940243264a8271ef8c3cbc4fadbb46112afd36830"},"_data/templates.xml":{"line_count":781,"matched_context":[{"sep":true},{"line":1,"text":""},{"line":2,"text":""},{"line":3,"text":" "},{"line":9,"text":" "},{"line":553,"text":" "},{"line":575,"text":" "},{"line":647,"text":" "},{"line":764,"text":" "},{"line":772,"text":" "},{"line":781,"text":""}],"present":true,"sha256":"1a344bd306135486d2019bf0a5cafff5eb472882f7b3433ef13af4556a4c791e"}}},"transport_rc":0,"transport_stderr_bytes":0,"transport_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"} +GRAM516R1R3_NOTIFICATION_FILES=["Notification/AlertConsumer.php","Notification/DeliveryGate.php","Notification/Dispatcher.php","Notification/EmailConsumer.php","Notification/EntityDeliveryGate.php","Notification/EventDispatcher.php","Notification/Policy.php","Notification/RecipientKey.php"] +GRAM516R1R3_EMAIL_CONSUMER_FILE_PRESENT=true +GRAM516R1R3_DECISION=PASS_NOTIFY_CAPTCHA_SOURCE_EVIDENCE_CAPTURED +GRAM516R1R3_NEXT_GATE=BUILD_EXACT_TURNSTILE_AND_EMAIL_NOTIFY_CHANGE +HOMELAB_RESULT_CONTRACT={"version":1,"command_id":"SUPPORT-260918-GRAM-SUPPORT-NOTIFY-CAPTCHA-SOURCE-READONLY-516R1R3","status":"OK","changes_made":false,"rollback_started":false,"rollback_restored":null} + +OUTPUT_END +CHAT_OUTPUT_END diff --git a/runtime/latest.json b/runtime/latest.json index db89c3a6..1578d4f1 100644 --- a/runtime/latest.json +++ b/runtime/latest.json @@ -1,19 +1,19 @@ { "schema_version": 1, "channel": "homelab-runtime", - "command_id": "SUPPORT-260918-GRAM-SUPPORT-TURNSTILE-MAIL-RUNTIME-SPLIT-AUDIT-516R1R2", - "status": "FAIL", - "rc": 3, + "command_id": "SUPPORT-260918-GRAM-SUPPORT-NOTIFY-CAPTCHA-SOURCE-READONLY-516R1R3", + "status": "OK", + "rc": 0, "host": "pve01", "mode": "read-only", - "component": "gram-support-turnstile-mail-runtime-split-audit", - "started_at_utc": "2026-09-18T05:04:05Z", - "finished_at_utc": "2026-09-18T05:04:16Z", + "component": "gram-support-notify-captcha-source-readonly", + "started_at_utc": "2026-09-18T05:06:58Z", + "finished_at_utc": "2026-09-18T05:07:00Z", "reference_register_checked": true, "reference_sha256": "5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66", "error_register_checked": true, "error_register_sha256": "3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0", - "command_sha256": "2ed6543ad5cca0f2d786defe844f878e2d2482f61b4ff069c1e76e4e6e32ed00", + "command_sha256": "0f7a476d59191ec993679cb3fe3a569eb7e5b72805ab8c6e63c390f6a14915fc", "duplicate_failed_command_blocked": false, "block_reason": null, "execution_started": true, @@ -21,7 +21,7 @@ "result_contract_valid": true, "result_contract_status": null, "result_contract_error": null, - "command_rc": 3, + "command_rc": 0, "changes_made": false, "rollback_started": false, "rollback_restored": null, @@ -30,9 +30,9 @@ "secrets_included": false, "private_addresses_included": false, "raw_evidence_retained_locally": true, - "raw_evidence_sha256": "5c96de3e0b329ea04af8767fc89b1caade18678b60512c5cea24d334e445f26c", - "sanitized_output_sha256": "5c96de3e0b329ea04af8767fc89b1caade18678b60512c5cea24d334e445f26c", + "raw_evidence_sha256": "6eb6b9ac7ab7b4c61eb06b40dd1f2390fcac654e496097728a954feacd2b3c9c", + "sanitized_output_sha256": "6eb6b9ac7ab7b4c61eb06b40dd1f2390fcac654e496097728a954feacd2b3c9c", "output_truncated_in_json": false, "full_sanitized_output_url": "https://git.gram1.ru/.well-known/homelab-runtime/latest.txt", - "output": "GRAM516R1R2_SAFE_REPORT={\"classification\":{\"changes_made\":false,\"codevipe_source_scan_ok\":false,\"non_turnstile_site_count\":5,\"notification_directory_present\":false,\"runtime_probe_7of7\":true,\"send_attempts\":0,\"site_count_7\":true,\"support_captcha_signal_present\":false,\"turnstile_site_count\":2},\"codevipe_deployed_source\":null,\"non_turnstile_sites\":[{\"captcha\":{\"hcaptcha_secret_nonempty\":false,\"hcaptcha_sitekey_nonempty\":false,\"provider\":\"OTHER_question\",\"shape\":[],\"turnstile_secret_nonempty\":false,\"turnstile_sitekey_nonempty\":false},\"provider\":\"OTHER_question\",\"slug\":\"codevipe\"},{\"captcha\":{\"hcaptcha_secret_nonempty\":false,\"hcaptcha_sitekey_nonempty\":false,\"provider\":\"OTHER_question\",\"shape\":[],\"turnstile_secret_nonempty\":false,\"turnstile_sitekey_nonempty\":false},\"provider\":\"OTHER_question\",\"slug\":\"dsmods\"},{\"captcha\":{\"hcaptcha_secret_nonempty\":false,\"hcaptcha_sitekey_nonempty\":false,\"provider\":\"OTHER_question\",\"shape\":[],\"turnstile_secret_nonempty\":false,\"turnstile_sitekey_nonempty\":false},\"provider\":\"OTHER_question\",\"slug\":\"gamevipe\"},{\"captcha\":{\"hcaptcha_secret_nonempty\":false,\"hcaptcha_sitekey_nonempty\":false,\"provider\":\"OTHER_question\",\"shape\":[],\"turnstile_secret_nonempty\":false,\"turnstile_sitekey_nonempty\":false},\"provider\":\"OTHER_question\",\"slug\":\"hkmods\"},{\"captcha\":{\"hcaptcha_secret_nonempty\":false,\"hcaptcha_sitekey_nonempty\":false,\"provider\":\"OTHER_question\",\"shape\":[],\"turnstile_secret_nonempty\":false,\"turnstile_sitekey_nonempty\":false},\"provider\":\"OTHER_question\",\"slug\":\"zakrutim\"}],\"schema_version\":1,\"scope\":\"READ_ONLY_GRAM_SUPPORT_TURNSTILE_MAIL_RUNTIME_SPLIT_AUDIT\",\"secret_policy\":\"NO_CAPTCHA_OR_MAIL_SECRET_VALUES_HASHES_OR_LENGTHS_EXPORTED\",\"sites\":[{\"captcha\":{\"hcaptcha_secret_nonempty\":false,\"hcaptcha_sitekey_nonempty\":false,\"provider\":\"OTHER_question\",\"shape\":[],\"turnstile_secret_nonempty\":false,\"turnstile_sitekey_nonempty\":false},\"gram_option_ids\":[\"gramSupportGuestRateLimitCount\",\"gramSupportGuestRateLimitWindowSeconds\",\"gramSupportGuestTokenTtlDays\",\"gramSupportSiteId\"],\"mail\":{\"sendmail_exec\":true,\"transport\":\"sendmail\"},\"slug\":\"codevipe\"},{\"captcha\":{\"hcaptcha_secret_nonempty\":false,\"hcaptcha_sitekey_nonempty\":false,\"provider\":\"OTHER_question\",\"shape\":[],\"turnstile_secret_nonempty\":false,\"turnstile_sitekey_nonempty\":false},\"gram_option_ids\":[\"gramSupportGuestRateLimitCount\",\"gramSupportGuestRateLimitWindowSeconds\",\"gramSupportGuestTokenTtlDays\",\"gramSupportSiteId\"],\"mail\":{\"sendmail_exec\":true,\"transport\":\"sendmail\"},\"slug\":\"dsmods\"},{\"captcha\":{\"hcaptcha_secret_nonempty\":false,\"hcaptcha_sitekey_nonempty\":false,\"provider\":\"OTHER_question\",\"shape\":[],\"turnstile_secret_nonempty\":false,\"turnstile_sitekey_nonempty\":false},\"gram_option_ids\":[\"gramSupportGuestRateLimitCount\",\"gramSupportGuestRateLimitWindowSeconds\",\"gramSupportGuestTokenTtlDays\",\"gramSupportSiteId\"],\"mail\":{\"sendmail_exec\":true,\"transport\":\"sendmail\"},\"slug\":\"gamevipe\"},{\"captcha\":{\"hcaptcha_secret_nonempty\":false,\"hcaptcha_sitekey_nonempty\":false,\"provider\":\"OTHER_question\",\"shape\":[],\"turnstile_secret_nonempty\":false,\"turnstile_sitekey_nonempty\":false},\"gram_option_ids\":[\"gramSupportGuestRateLimitCount\",\"gramSupportGuestRateLimitWindowSeconds\",\"gramSupportGuestTokenTtlDays\",\"gramSupportSiteId\"],\"mail\":{\"sendmail_exec\":true,\"transport\":\"sendmail\"},\"slug\":\"hkmods\"},{\"captcha\":{\"hcaptcha_secret_nonempty\":false,\"hcaptcha_sitekey_nonempty\":false,\"provider\":\"CLOUDFLARE_TURNSTILE\",\"shape\":[{\"kind\":\"string\",\"nonempty\":true,\"path\":\"turnstileSiteKey\"},{\"kind\":\"string\",\"nonempty\":true,\"path\":\"turnstileSecretKey\"}],\"turnstile_secret_nonempty\":true,\"turnstile_sitekey_nonempty\":true},\"gram_option_ids\":[\"gramSupportGuestRateLimitCount\",\"gramSupportGuestRateLimitWindowSeconds\",\"gramSupportGuestTokenTtlDays\",\"gramSupportSiteId\"],\"mail\":{\"sendmail_exec\":true,\"transport\":\"sendmail\"},\"slug\":\"kingofwolk\"},{\"captcha\":{\"hcaptcha_secret_nonempty\":false,\"hcaptcha_sitekey_nonempty\":false,\"provider\":\"OTHER_question\",\"shape\":[],\"turnstile_secret_nonempty\":false,\"turnstile_sitekey_nonempty\":false},\"gram_option_ids\":[\"gramSupportGuestRateLimitCount\",\"gramSupportGuestRateLimitWindowSeconds\",\"gramSupportGuestTokenTtlDays\",\"gramSupportSiteId\"],\"mail\":{\"sendmail_exec\":true,\"transport\":\"sendmail\"},\"slug\":\"zakrutim\"},{\"captcha\":{\"hcaptcha_secret_nonempty\":false,\"hcaptcha_sitekey_nonempty\":false,\"provider\":\"CLOUDFLARE_TURNSTILE\",\"shape\":[{\"kind\":\"string\",\"nonempty\":true,\"path\":\"turnstileSiteKey\"},{\"kind\":\"string\",\"nonempty\":true,\"path\":\"turnstileSecretKey\"}],\"turnstile_secret_nonempty\":true,\"turnstile_sitekey_nonempty\":true},\"gram_option_ids\":[\"gramSupportGuestRateLimitCount\",\"gramSupportGuestRateLimitWindowSeconds\",\"gramSupportGuestTokenTtlDays\",\"gramSupportSiteId\"],\"mail\":{\"sendmail_exec\":true,\"transport\":\"sendmail\"},\"slug\":\"newfi\"}],\"source_transport\":{\"err_truncated\":false,\"error\":\"GUEST_JSON_JSONDecodeError\",\"guest_stderr_present\":true,\"inner_rc\":1,\"ok\":false,\"out_truncated\":false,\"transport_rc\":0,\"transport_stderr_bytes\":0,\"transport_stderr_sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"},\"target_notification_email\":\"aleisaev@yandex.ru\",\"transport_evidence\":[{\"slug\":\"codevipe\",\"transport\":{\"err_truncated\":false,\"guest_stderr_present\":false,\"inner_rc\":0,\"ok\":true,\"out_truncated\":false,\"transport_rc\":0,\"transport_stderr_bytes\":0,\"transport_stderr_sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}},{\"slug\":\"dsmods\",\"transport\":{\"err_truncated\":false,\"guest_stderr_present\":false,\"inner_rc\":0,\"ok\":true,\"out_truncated\":false,\"transport_rc\":0,\"transport_stderr_bytes\":0,\"transport_stderr_sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}},{\"slug\":\"gamevipe\",\"transport\":{\"err_truncated\":false,\"guest_stderr_present\":false,\"inner_rc\":0,\"ok\":true,\"out_truncated\":false,\"transport_rc\":0,\"transport_stderr_bytes\":0,\"transport_stderr_sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}},{\"slug\":\"hkmods\",\"transport\":{\"err_truncated\":false,\"guest_stderr_present\":false,\"inner_rc\":0,\"ok\":true,\"out_truncated\":false,\"transport_rc\":0,\"transport_stderr_bytes\":0,\"transport_stderr_sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}},{\"slug\":\"kingofwolk\",\"transport\":{\"err_truncated\":false,\"guest_stderr_present\":false,\"inner_rc\":0,\"ok\":true,\"out_truncated\":false,\"transport_rc\":0,\"transport_stderr_bytes\":0,\"transport_stderr_sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}},{\"slug\":\"zakrutim\",\"transport\":{\"err_truncated\":false,\"guest_stderr_present\":false,\"inner_rc\":0,\"ok\":true,\"out_truncated\":false,\"transport_rc\":0,\"transport_stderr_bytes\":0,\"transport_stderr_sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}},{\"slug\":\"newfi\",\"transport\":{\"err_truncated\":false,\"guest_stderr_present\":false,\"inner_rc\":0,\"ok\":true,\"out_truncated\":false,\"transport_rc\":0,\"transport_stderr_bytes\":0,\"transport_stderr_sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}}],\"turnstile_sites\":[\"kingofwolk\",\"newfi\"]}\nGRAM516R1R2_DECISION=HOLD_AUDIT_INCOMPLETE\nGRAM516R1R2_NEXT_GATE=STOP_NO_MUTATION\nHOMELAB_RESULT_CONTRACT={\"version\":1,\"command_id\":\"SUPPORT-260918-GRAM-SUPPORT-TURNSTILE-MAIL-RUNTIME-SPLIT-AUDIT-516R1R2\",\"status\":\"FAIL\",\"changes_made\":false,\"rollback_started\":false,\"rollback_restored\":null}\n" + "output": "GRAM516R1R3_SAFE_REPORT={\"err_truncated\":false,\"guest_stderr_present\":false,\"inner_rc\":0,\"ok\":true,\"out_truncated\":false,\"source\":{\"notification_files\":[\"Notification/AlertConsumer.php\",\"Notification/DeliveryGate.php\",\"Notification/Dispatcher.php\",\"Notification/EmailConsumer.php\",\"Notification/EntityDeliveryGate.php\",\"Notification/EventDispatcher.php\",\"Notification/Policy.php\",\"Notification/RecipientKey.php\"],\"ok\":true,\"root_tree_files\":[\"Attachment/TicketHandler.php\",\"Entity/AttachmentMeta.php\",\"Entity/Delivery.php\",\"Entity/Message.php\",\"Entity/RateLimit.php\",\"Entity/SupportEvent.php\",\"Entity/Ticket.php\",\"Listener/SourcePageContext.php\",\"Notification/AlertConsumer.php\",\"Notification/DeliveryGate.php\",\"Notification/Dispatcher.php\",\"Notification/EmailConsumer.php\",\"Notification/EntityDeliveryGate.php\",\"Notification/EventDispatcher.php\",\"Notification/Policy.php\",\"Notification/RecipientKey.php\",\"Pub/Controller/AccountSupport.php\",\"Pub/Controller/Pwa.php\",\"Pub/Controller/StaffSupport.php\",\"Pub/Controller/Support.php\",\"Pub/View/PwaServiceWorker.php\",\"Repository/Message.php\",\"Repository/Ticket.php\",\"Security/GuestTicketSession.php\",\"Security/TicketAccess.php\",\"Service/GuestScreenshotService.php\",\"Service/GuestTicketCreateAttachmentFlow.php\",\"Service/RateLimiter.php\",\"Service/SourceContextResolver.php\",\"Service/Ticket/Creator.php\",\"Service/Ticket/Reply.php\",\"Service/Ticket/StaffUpdate.php\",\"Setup.php\",\"Util/GuestToken.php\",\"Util/PublicRef.php\",\"Util/RateKey.php\",\"Util/SafeContext.php\",\"Util/SiteConfig.php\",\"Util/StatusPolicy.php\",\"_data/code_event_listeners.xml\",\"_data/content_type_fields.xml\",\"_data/option_groups.xml\",\"_data/options.xml\",\"_data/permission_interface_groups.xml\",\"_data/permissions.xml\",\"_data/phrases.xml\",\"_data/routes.xml\",\"_data/template_modifications.xml\",\"_data/templates.xml\",\"addon.json\",\"hashes.json\"],\"targets\":{\"Notification/AlertConsumer.php\":{\"line_count\":74,\"matched_context\":[{\"sep\":true},{\"line\":1,\"text\":\"gate = $gate;\"},{\"line\":16,\"text\":\" }\"},{\"line\":17,\"text\":\"\"},{\"line\":18,\"text\":\" public function consumeForUser(SupportEvent $event, Ticket $ticket, User $recipient): bool\"},{\"line\":19,\"text\":\" {\"},{\"line\":20,\"text\":\" if (!Policy::mayEmitExternal($event) || !$recipient->user_id)\"},{\"line\":21,\"text\":\" {\"},{\"line\":22,\"text\":\" return false;\"},{\"line\":23,\"text\":\" }\"},{\"line\":24,\"text\":\"\"},{\"line\":25,\"text\":\" // Never alert a user about a ticket they cannot access.\"},{\"line\":26,\"text\":\" $isOwner = $ticket->user_id && (int)$ticket->user_id === (int)$recipient->user_id;\"},{\"line\":27,\"text\":\" $isStaff = $recipient->hasPermission('gramSupport', 'viewAll')\"},{\"line\":28,\"text\":\" || $recipient->hasPermission('gramSupport', 'manage');\"},{\"line\":29,\"text\":\"\"},{\"line\":30,\"text\":\" if (!$isOwner && !$isStaff)\"},{\"line\":31,\"text\":\" {\"},{\"line\":32,\"text\":\" return false;\"},{\"line\":33,\"text\":\" }\"},{\"line\":34,\"text\":\"\"},{\"line\":35,\"text\":\" $recipientKey = RecipientKey::user((int)$recipient->user_id);\"},{\"line\":36,\"text\":\" if (!$this->gate->acquire((int)$event->event_id, 'xf_alert', $recipientKey))\"},{\"line\":37,\"text\":\" {\"},{\"line\":38,\"text\":\" return false;\"},{\"line\":39,\"text\":\" }\"},{\"line\":40,\"text\":\"\"},{\"line\":41,\"text\":\" try\"},{\"line\":42,\"text\":\" {\"},{\"line\":43,\"text\":\" /** @var \\\\XF\\\\Repository\\\\UserAlertRepository $alerts */\"},{\"line\":44,\"text\":\" $alerts = \\\\XF::repository('XF:UserAlert');\"},{\"line\":45,\"text\":\"\"},{\"line\":46,\"text\":\" // Back the alert with the recipient's own User content so the\"},{\"line\":47,\"text\":\" // core alert handler remains valid/viewable. Ticket access is\"},{\"line\":48,\"text\":\" // separately enforced before emission and again on ticket route.\"},{\"line\":49,\"text\":\" $alerts->alert(\"},{\"line\":50,\"text\":\" $recipient,\"},{\"line\":51,\"text\":\" 0,\"},{\"line\":52,\"text\":\" 'Support',\"},{\"line\":53,\"text\":\" 'user',\"},{\"line\":54,\"text\":\" (int)$recipient->user_id,\"},{\"line\":55,\"text\":\" 'gram_support_evt',\"},{\"line\":56,\"text\":\" Policy::alertExtraData($event),\"},{\"line\":57,\"text\":\" ['usePush' => false]\"},{\"line\":58,\"text\":\" );\"},{\"line\":59,\"text\":\"\"},{\"line\":60,\"text\":\" $this->gate->markSent((int)$event->event_id, 'xf_alert', $recipientKey);\"},{\"line\":61,\"text\":\" return true;\"},{\"line\":62,\"text\":\" }\"},{\"line\":63,\"text\":\" catch (\\\\Throwable $e)\"},{\"line\":64,\"text\":\" {\"},{\"line\":65,\"text\":\" $this->gate->markFailed(\"},{\"line\":66,\"text\":\" (int)$event->event_id,\"},{\"line\":67,\"text\":\" 'xf_alert',\"},{\"line\":68,\"text\":\" $recipientKey,\"},{\"line\":69,\"text\":\" get_class($e)\"},{\"line\":70,\"text\":\" );\"},{\"line\":71,\"text\":\" throw $e;\"},{\"line\":72,\"text\":\" }\"}],\"present\":true,\"sha256\":\"e22d0cdb0a0806c6a231504409bd17cd781caab0f28181aff7246b0295d8cd98\"},\"Notification/Dispatcher.php\":{\"line_count\":67,\"matched_context\":[{\"sep\":true},{\"line\":1,\"text\":\"alerts = $alerts;\"},{\"line\":17,\"text\":\" $this->email = $email;\"},{\"line\":18,\"text\":\" }\"},{\"line\":19,\"text\":\"\"},{\"line\":20,\"text\":\" /**\"},{\"line\":21,\"text\":\" * Owner-facing delivery. Staff alert fan-out is intentionally provided by\"},{\"line\":22,\"text\":\" * dispatchStaff() so staff recipients must still pass Gram Support ACL.\"},{\"line\":23,\"text\":\" */\"},{\"line\":24,\"text\":\" public function dispatchOwner(SupportEvent $event, Ticket $ticket, ?User $owner): void\"},{\"line\":25,\"text\":\" {\"},{\"line\":26,\"text\":\" if (!Policy::mayEmitExternal($event))\"},{\"line\":27,\"text\":\" {\"},{\"line\":28,\"text\":\" return;\"},{\"line\":29,\"text\":\" }\"},{\"line\":30,\"text\":\"\"},{\"line\":31,\"text\":\" if ($owner && $owner->user_id)\"},{\"line\":32,\"text\":\" {\"},{\"line\":33,\"text\":\" $this->alerts->consumeForUser($event, $ticket, $owner);\"},{\"line\":34,\"text\":\" $this->email->consumeForRegistered($event, $ticket, $owner);\"},{\"line\":35,\"text\":\" return;\"},{\"line\":36,\"text\":\" }\"},{\"line\":37,\"text\":\"\"},{\"line\":38,\"text\":\" $this->email->consumeForGuest($event, $ticket);\"},{\"line\":39,\"text\":\" }\"},{\"line\":40,\"text\":\"\"},{\"line\":41,\"text\":\" /**\"},{\"line\":42,\"text\":\" * @param User[] $staffUsers\"},{\"line\":43,\"text\":\" */\"},{\"line\":44,\"text\":\" public function dispatchStaff(SupportEvent $event, Ticket $ticket, array $staffUsers): void\"},{\"line\":45,\"text\":\" {\"},{\"line\":46,\"text\":\" if (!Policy::mayEmitExternal($event))\"},{\"line\":47,\"text\":\" {\"},{\"line\":48,\"text\":\" return;\"},{\"line\":49,\"text\":\" }\"},{\"line\":50,\"text\":\"\"},{\"line\":51,\"text\":\" foreach ($staffUsers as $staff)\"},{\"line\":52,\"text\":\" {\"},{\"line\":53,\"text\":\" if (!$staff instanceof User || !$staff->user_id)\"},{\"line\":54,\"text\":\" {\"},{\"line\":55,\"text\":\" continue;\"},{\"line\":56,\"text\":\" }\"},{\"line\":57,\"text\":\"\"},{\"line\":58,\"text\":\" if (!$staff->hasPermission('gramSupport', 'viewAll')\"},{\"line\":59,\"text\":\" && !$staff->hasPermission('gramSupport', 'manage'))\"},{\"line\":60,\"text\":\" {\"},{\"line\":61,\"text\":\" continue;\"},{\"line\":62,\"text\":\" }\"},{\"line\":63,\"text\":\"\"},{\"line\":64,\"text\":\" $this->alerts->consumeForUser($event, $ticket, $staff);\"},{\"line\":65,\"text\":\" }\"},{\"line\":66,\"text\":\" }\"},{\"line\":67,\"text\":\"}\"}],\"present\":true,\"sha256\":\"a043d94bc91feeed0969ad218566cebac76b2d804e7c69cc2b87b94d42f5bb85\"},\"Pub/Controller/Support.php\":{\"line_count\":236,\"matched_context\":[{\"sep\":true},{\"line\":1,\"text\":\"isPost())\"},{\"line\":37,\"text\":\" {\"},{\"line\":38,\"text\":\" $this->assertPostOnly();\"},{\"line\":39,\"text\":\"\"},{\"line\":40,\"text\":\" $email = $this->filter('email', 'str');\"},{\"line\":41,\"text\":\" $category = $this->filter('category', 'str');\"},{\"line\":42,\"text\":\" $subject = $this->filter('subject', 'str');\"},{\"line\":43,\"text\":\" $message = $this->filter('message', 'str');\"},{\"line\":44,\"text\":\"\"},{\"line\":45,\"text\":\" if (!$visitor->user_id)\"},{\"line\":46,\"text\":\" {\"},{\"line\":47,\"text\":\" if (!$this->captchaIsValid(true))\"},{\"line\":48,\"text\":\" {\"},{\"line\":49,\"text\":\" return $this->error(\"},{\"line\":50,\"text\":\" \\\\XF::phrase('did_not_complete_the_captcha_verification_properly')\"},{\"line\":51,\"text\":\" );\"},{\"line\":52,\"text\":\" }\"},{\"line\":53,\"text\":\"\"},{\"line\":54,\"text\":\" /** @var RateLimiter $rateLimiter */\"},{\"line\":55,\"text\":\" $rateLimiter = $this->service(RateLimiter::class);\"},{\"line\":56,\"text\":\" $allowed = $rateLimiter->consumeGuest(\"},{\"line\":57,\"text\":\" SiteConfig::siteId(),\"},{\"line\":58,\"text\":\" $email,\"},{\"line\":59,\"text\":\" SiteConfig::guestRateLimitCount(),\"},{\"line\":60,\"text\":\" SiteConfig::guestRateLimitWindowSeconds()\"},{\"line\":61,\"text\":\" );\"},{\"line\":62,\"text\":\"\"},{\"line\":63,\"text\":\" if (!$allowed)\"},{\"sep\":true},{\"line\":74,\"text\":\" if ($upload)\"},{\"line\":75,\"text\":\" {\"},{\"line\":76,\"text\":\" $attachmentId = $attachmentFlow->prepare($upload, $tempHash);\"},{\"line\":77,\"text\":\" }\"},{\"line\":78,\"text\":\"\"},{\"line\":79,\"text\":\" /** @var Creator $creator */\"},{\"line\":80,\"text\":\" $creator = $this->service(Creator::class);\"},{\"line\":81,\"text\":\" $created = $creator->create([\"},{\"line\":82,\"text\":\" 'email' => $email,\"},{\"line\":83,\"text\":\" 'category' => $category,\"},{\"line\":84,\"text\":\" 'subject' => $subject,\"},{\"line\":85,\"text\":\" 'message' => $message\"},{\"line\":86,\"text\":\" ]);\"},{\"line\":87,\"text\":\"\"},{\"sep\":true},{\"line\":127,\"text\":\" }\"},{\"line\":128,\"text\":\"\"},{\"line\":129,\"text\":\" return $this->view(\"},{\"line\":130,\"text\":\" 'Gram\\\\Support:Create',\"},{\"line\":131,\"text\":\" 'gram_support_create',\"},{\"line\":132,\"text\":\" ['forceCaptcha' => !$visitor->user_id]\"},{\"line\":133,\"text\":\" );\"},{\"line\":134,\"text\":\" }\"},{\"line\":135,\"text\":\"\"},{\"line\":136,\"text\":\" public function actionAccess()\"},{\"line\":137,\"text\":\" {\"},{\"sep\":true},{\"line\":183,\"text\":\" 'messages' => $messages\"},{\"line\":184,\"text\":\" ]\"},{\"line\":185,\"text\":\" );\"},{\"line\":186,\"text\":\" }\"},{\"line\":187,\"text\":\"\"},{\"line\":188,\"text\":\" public function actionReply()\"},{\"line\":189,\"text\":\" {\"},{\"line\":190,\"text\":\" $this->assertPostOnly();\"},{\"line\":191,\"text\":\"\"},{\"line\":192,\"text\":\" $ticket = $this->assertViewableTicketByRef($this->filter('ref', 'str'));\"},{\"line\":193,\"text\":\" $visitor = \\\\XF::visitor();\"},{\"line\":194,\"text\":\"\"},{\"line\":195,\"text\":\" if ($visitor->user_id)\"},{\"line\":196,\"text\":\" {\"},{\"line\":197,\"text\":\" if ((int)$ticket->user_id !== (int)$visitor->user_id\"},{\"line\":198,\"text\":\" || !$visitor->hasPermission('gramSupport', 'replyOwn'))\"},{\"line\":199,\"text\":\" {\"},{\"line\":200,\"text\":\" return $this->noPermission();\"},{\"line\":201,\"text\":\" }\"},{\"line\":202,\"text\":\" }\"},{\"line\":203,\"text\":\" elseif (!GuestTicketSession::hasGrant($ticket))\"},{\"sep\":true},{\"line\":205,\"text\":\" return $this->noPermission();\"},{\"line\":206,\"text\":\" }\"},{\"line\":207,\"text\":\"\"},{\"line\":208,\"text\":\" $message = $this->filter('message', 'str');\"},{\"line\":209,\"text\":\"\"},{\"line\":210,\"text\":\" /** @var Reply $reply */\"},{\"line\":211,\"text\":\" $reply = $this->service(Reply::class);\"},{\"line\":212,\"text\":\" $reply->add($ticket, $message, false, false);\"},{\"line\":213,\"text\":\"\"},{\"line\":214,\"text\":\" return $this->redirect(\"},{\"line\":215,\"text\":\" $this->buildLink('support/ticket', null, ['ref' => $ticket->public_ref])\"},{\"line\":216,\"text\":\" );\"},{\"line\":217,\"text\":\" }\"}],\"present\":true,\"sha256\":\"6cb34b1d27e9f0132263637cefa62fe0e998a9a86620b7ac33199f41ee86f9d4\"},\"Service/Ticket/Creator.php\":{\"line_count\":139,\"matched_context\":[{\"sep\":true},{\"line\":8,\"text\":\"use Gram\\\\Support\\\\Util\\\\PublicRef;\"},{\"line\":9,\"text\":\"use Gram\\\\Support\\\\Util\\\\SafeContext;\"},{\"line\":10,\"text\":\"use Gram\\\\Support\\\\Util\\\\SiteConfig;\"},{\"line\":11,\"text\":\"use Gram\\\\Support\\\\Util\\\\StatusPolicy;\"},{\"line\":12,\"text\":\"use XF\\\\Service\\\\AbstractService;\"},{\"line\":13,\"text\":\"use Gram\\\\Support\\\\Notification\\\\EventDispatcher;\"},{\"line\":14,\"text\":\"\"},{\"line\":15,\"text\":\"class Creator extends AbstractService\"},{\"line\":16,\"text\":\"{\"},{\"line\":17,\"text\":\" public function create(array $input, ?int $now = null): array\"},{\"line\":18,\"text\":\" {\"},{\"line\":19,\"text\":\" $now = $now ?? \\\\XF::$time;\"},{\"line\":20,\"text\":\" $visitor = \\\\XF::visitor();\"},{\"line\":21,\"text\":\" $siteId = SiteConfig::siteId();\"},{\"line\":22,\"text\":\"\"},{\"line\":23,\"text\":\" $email = strtolower(trim((string)($input['email'] ?? '')));\"},{\"line\":24,\"text\":\" $category = trim((string)($input['category'] ?? ''));\"},{\"line\":25,\"text\":\" $subject = trim((string)($input['subject'] ?? ''));\"},{\"line\":26,\"text\":\" $messageText = trim((string)($input['message'] ?? ''));\"},{\"line\":27,\"text\":\"\"},{\"line\":28,\"text\":\" if ($category === '' || $messageText === '')\"},{\"line\":29,\"text\":\" {\"},{\"line\":30,\"text\":\" throw new \\\\InvalidArgumentException('GRAM_SUPPORT_REQUIRED_FIELDS');\"},{\"line\":31,\"text\":\" }\"},{\"line\":32,\"text\":\"\"},{\"line\":33,\"text\":\" if (!$visitor->user_id && !filter_var($email, FILTER_VALIDATE_EMAIL))\"},{\"line\":34,\"text\":\" {\"},{\"line\":35,\"text\":\" throw new \\\\InvalidArgumentException('GRAM_SUPPORT_VALID_EMAIL_REQUIRED');\"},{\"line\":36,\"text\":\" }\"},{\"line\":37,\"text\":\"\"},{\"line\":38,\"text\":\" if ($subject === '')\"},{\"line\":39,\"text\":\" {\"},{\"line\":40,\"text\":\" $subject = function_exists('mb_substr')\"},{\"sep\":true},{\"line\":68,\"text\":\" /** @var Ticket $ticket */\"},{\"line\":69,\"text\":\" $ticket = $this->em()->create('Gram\\\\Support:Ticket');\"},{\"line\":70,\"text\":\" $ticket->public_ref = 'TMP-' . bin2hex(random_bytes(12));\"},{\"line\":71,\"text\":\" $ticket->site_id = $siteId;\"},{\"line\":72,\"text\":\" $ticket->user_id = (int)$visitor->user_id;\"},{\"line\":73,\"text\":\" $ticket->guest_email = $visitor->user_id ? '' : $email;\"},{\"line\":74,\"text\":\" $ticket->guest_access_token_hash = $guest ? $guest['hash'] : '';\"},{\"line\":75,\"text\":\" $ticket->guest_access_expires = $guest\"},{\"line\":76,\"text\":\" ? $now + SiteConfig::guestTokenTtlSeconds()\"},{\"line\":77,\"text\":\" : 0;\"},{\"line\":78,\"text\":\" $ticket->node_id = (int)($context['node_id'] ?? 0);\"},{\"sep\":true},{\"line\":96,\"text\":\" $ticket->save();\"},{\"line\":97,\"text\":\"\"},{\"line\":98,\"text\":\" $message = $this->em()->create('Gram\\\\Support:Message');\"},{\"line\":99,\"text\":\" $message->ticket_id = (int)$ticket->ticket_id;\"},{\"line\":100,\"text\":\" $message->user_id = (int)$visitor->user_id;\"},{\"line\":101,\"text\":\" $message->is_staff = 0;\"},{\"line\":102,\"text\":\" $message->is_internal = 0;\"},{\"line\":103,\"text\":\" $message->message = $messageText;\"},{\"line\":104,\"text\":\" $message->message_date = $now;\"},{\"line\":105,\"text\":\" $message->save();\"},{\"line\":106,\"text\":\"\"},{\"line\":107,\"text\":\" $ticket->last_message_id = (int)$message->message_id;\"},{\"line\":108,\"text\":\" $ticket->save();\"},{\"line\":109,\"text\":\"\"},{\"line\":110,\"text\":\" $event = $this->em()->create('Gram\\\\Support:SupportEvent');\"},{\"line\":111,\"text\":\" $event->ticket_id = (int)$ticket->ticket_id;\"},{\"line\":112,\"text\":\" $event->site_id = $siteId;\"},{\"line\":113,\"text\":\" $event->actor_user_id = (int)$visitor->user_id;\"},{\"line\":114,\"text\":\" $event->event_type = 'ticket_created';\"},{\"line\":115,\"text\":\" $event->event_date = $now;\"},{\"line\":116,\"text\":\" $event->safe_payload_json = json_encode([\"},{\"line\":117,\"text\":\" 'public_ref' => (string)$ticket->public_ref,\"},{\"line\":118,\"text\":\" 'category' => (string)$ticket->category,\"},{\"line\":119,\"text\":\" 'is_internal' => false\"},{\"sep\":true},{\"line\":126,\"text\":\" {\"},{\"line\":127,\"text\":\" $db->rollback();\"},{\"line\":128,\"text\":\" throw $e;\"},{\"line\":129,\"text\":\" }\"},{\"line\":130,\"text\":\"\"},{\"line\":131,\"text\":\" (new EventDispatcher())->dispatchSafely($event, $ticket);\"},{\"line\":132,\"text\":\"\"},{\"line\":133,\"text\":\" return [\"},{\"line\":134,\"text\":\" 'ticket' => $ticket,\"},{\"line\":135,\"text\":\" 'event' => $event,\"},{\"line\":136,\"text\":\" 'guest_secret' => $guest ? $guest['secret'] : null\"}],\"present\":true,\"sha256\":\"7a84cd77761a7943495f9d3ef11269987f85265170ad6cf927a1661b8bf0a99a\"},\"Service/Ticket/Reply.php\":{\"line_count\":68,\"matched_context\":[{\"sep\":true},{\"line\":3,\"text\":\"namespace Gram\\\\Support\\\\Service\\\\Ticket;\"},{\"line\":4,\"text\":\"\"},{\"line\":5,\"text\":\"use Gram\\\\Support\\\\Entity\\\\Ticket;\"},{\"line\":6,\"text\":\"use Gram\\\\Support\\\\Util\\\\SiteConfig;\"},{\"line\":7,\"text\":\"use XF\\\\Service\\\\AbstractService;\"},{\"line\":8,\"text\":\"use Gram\\\\Support\\\\Notification\\\\EventDispatcher;\"},{\"line\":9,\"text\":\"\"},{\"line\":10,\"text\":\"class Reply extends AbstractService\"},{\"line\":11,\"text\":\"{\"},{\"line\":12,\"text\":\" public function add(Ticket $ticket, string $messageText, bool $isStaff, bool $isInternal = false): array\"},{\"line\":13,\"text\":\" {\"},{\"line\":14,\"text\":\" $messageText = trim($messageText);\"},{\"line\":15,\"text\":\" if ($messageText === '')\"},{\"line\":16,\"text\":\" {\"},{\"line\":17,\"text\":\" throw new \\\\InvalidArgumentException('GRAM_SUPPORT_EMPTY_REPLY');\"},{\"line\":18,\"text\":\" }\"},{\"line\":19,\"text\":\" if ($isInternal && !$isStaff)\"},{\"line\":20,\"text\":\" {\"},{\"line\":21,\"text\":\" throw new \\\\LogicException('INTERNAL_REQUIRES_STAFF');\"},{\"line\":22,\"text\":\" }\"},{\"line\":23,\"text\":\"\"},{\"line\":24,\"text\":\" $visitor = \\\\XF::visitor();\"},{\"line\":25,\"text\":\" $now = \\\\XF::$time;\"},{\"line\":26,\"text\":\" $db = \\\\XF::db();\"},{\"sep\":true},{\"line\":29,\"text\":\" try\"},{\"line\":30,\"text\":\" {\"},{\"line\":31,\"text\":\" $message = $this->em()->create('Gram\\\\Support:Message');\"},{\"line\":32,\"text\":\" $message->ticket_id = (int)$ticket->ticket_id;\"},{\"line\":33,\"text\":\" $message->user_id = (int)$visitor->user_id;\"},{\"line\":34,\"text\":\" $message->is_staff = $isStaff ? 1 : 0;\"},{\"line\":35,\"text\":\" $message->is_internal = $isInternal ? 1 : 0;\"},{\"line\":36,\"text\":\" $message->message = $messageText;\"},{\"line\":37,\"text\":\" $message->message_date = $now;\"},{\"line\":38,\"text\":\" $message->save();\"},{\"line\":39,\"text\":\"\"},{\"line\":40,\"text\":\" $ticket->last_message_id = (int)$message->message_id;\"},{\"line\":41,\"text\":\" $ticket->updated_date = $now;\"},{\"line\":42,\"text\":\" $ticket->save();\"},{\"line\":43,\"text\":\"\"},{\"line\":44,\"text\":\" $event = $this->em()->create('Gram\\\\Support:SupportEvent');\"},{\"line\":45,\"text\":\" $event->ticket_id = (int)$ticket->ticket_id;\"},{\"line\":46,\"text\":\" $event->site_id = (string)$ticket->site_id;\"},{\"line\":47,\"text\":\" $event->actor_user_id = (int)$visitor->user_id;\"},{\"line\":48,\"text\":\" $event->event_type = $isInternal ? 'internal_note_created' : 'ticket_reply_created';\"},{\"line\":49,\"text\":\" $event->event_date = $now;\"},{\"line\":50,\"text\":\" $event->safe_payload_json = json_encode([\"},{\"line\":51,\"text\":\" 'public_ref' => (string)$ticket->public_ref,\"},{\"line\":52,\"text\":\" 'is_internal' => $isInternal\"},{\"line\":53,\"text\":\" ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);\"},{\"sep\":true},{\"line\":59,\"text\":\" {\"},{\"line\":60,\"text\":\" $db->rollback();\"},{\"line\":61,\"text\":\" throw $e;\"},{\"line\":62,\"text\":\" }\"},{\"line\":63,\"text\":\"\"},{\"line\":64,\"text\":\" (new EventDispatcher())->dispatchSafely($event, $ticket);\"},{\"line\":65,\"text\":\"\"},{\"line\":66,\"text\":\" return ['message' => $message, 'event' => $event];\"},{\"line\":67,\"text\":\" }\"},{\"line\":68,\"text\":\"}\"}],\"present\":true,\"sha256\":\"482571a2ccf07a3c8672f34940243264a8271ef8c3cbc4fadbb46112afd36830\"},\"_data/templates.xml\":{\"line_count\":781,\"matched_context\":[{\"sep\":true},{\"line\":1,\"text\":\"\"},{\"line\":2,\"text\":\"\"},{\"line\":3,\"text\":\" \"},{\"line\":9,\"text\":\" \"},{\"line\":553,\"text\":\" \"},{\"line\":575,\"text\":\" \"},{\"line\":647,\"text\":\" \"},{\"line\":764,\"text\":\" \"},{\"line\":772,\"text\":\" \"},{\"line\":781,\"text\":\"\"}],\"present\":true,\"sha256\":\"1a344bd306135486d2019bf0a5cafff5eb472882f7b3433ef13af4556a4c791e\"}}},\"transport_rc\":0,\"transport_stderr_bytes\":0,\"transport_stderr_sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}\nGRAM516R1R3_NOTIFICATION_FILES=[\"Notification/AlertConsumer.php\",\"Notification/DeliveryGate.php\",\"Notification/Dispatcher.php\",\"Notification/EmailConsumer.php\",\"Notification/EntityDeliveryGate.php\",\"Notification/EventDispatcher.php\",\"Notification/Policy.php\",\"Notification/RecipientKey.php\"]\nGRAM516R1R3_EMAIL_CONSUMER_FILE_PRESENT=true\nGRAM516R1R3_DECISION=PASS_NOTIFY_CAPTCHA_SOURCE_EVIDENCE_CAPTURED\nGRAM516R1R3_NEXT_GATE=BUILD_EXACT_TURNSTILE_AND_EMAIL_NOTIFY_CHANGE\nHOMELAB_RESULT_CONTRACT={\"version\":1,\"command_id\":\"SUPPORT-260918-GRAM-SUPPORT-NOTIFY-CAPTCHA-SOURCE-READONLY-516R1R3\",\"status\":\"OK\",\"changes_made\":false,\"rollback_started\":false,\"rollback_restored\":null}\n" } diff --git a/runtime/latest.txt b/runtime/latest.txt index 06a16d7a..b05940fc 100644 --- a/runtime/latest.txt +++ b/runtime/latest.txt @@ -1,22 +1,22 @@ CHAT_OUTPUT_BEGIN -COMMAND_ID=SUPPORT-260918-GRAM-SUPPORT-TURNSTILE-MAIL-RUNTIME-SPLIT-AUDIT-516R1R2 -STATUS=FAIL -RC=3 +COMMAND_ID=SUPPORT-260918-GRAM-SUPPORT-NOTIFY-CAPTCHA-SOURCE-READONLY-516R1R3 +STATUS=OK +RC=0 HOST=pve01 MODE=read-only -COMPONENT=gram-support-turnstile-mail-runtime-split-audit +COMPONENT=gram-support-notify-captcha-source-readonly REFERENCE_REGISTER_CHECK=OK REFERENCE_SHA256=5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66 ERROR_REGISTER_CHECK=OK ERROR_REGISTER_SHA256=3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0 -COMMAND_SHA256=2ed6543ad5cca0f2d786defe844f878e2d2482f61b4ff069c1e76e4e6e32ed00 +COMMAND_SHA256=0f7a476d59191ec993679cb3fe3a569eb7e5b72805ab8c6e63c390f6a14915fc DUPLICATE_FAILED_COMMAND_BLOCKED=false EXECUTION_STARTED=true CHANGE_DECLARED=false RESULT_CONTRACT_VALID=true RESULT_CONTRACT_STATUS=NOT_APPLICABLE RESULT_CONTRACT_ERROR=NONE -COMMAND_RC=3 +COMMAND_RC=0 CHANGES_MADE=false ROLLBACK_STARTED=false ROLLBACK_RESTORED=null @@ -24,13 +24,15 @@ MUTATION_OUTCOME=NO_MUTATION SANITIZED=yes SECRETS_INCLUDED=no PRIVATE_ADDRESSES_INCLUDED=no -RAW_EVIDENCE_SHA256=5c96de3e0b329ea04af8767fc89b1caade18678b60512c5cea24d334e445f26c -SANITIZED_OUTPUT_SHA256=5c96de3e0b329ea04af8767fc89b1caade18678b60512c5cea24d334e445f26c +RAW_EVIDENCE_SHA256=6eb6b9ac7ab7b4c61eb06b40dd1f2390fcac654e496097728a954feacd2b3c9c +SANITIZED_OUTPUT_SHA256=6eb6b9ac7ab7b4c61eb06b40dd1f2390fcac654e496097728a954feacd2b3c9c OUTPUT_BEGIN -GRAM516R1R2_SAFE_REPORT={"classification":{"changes_made":false,"codevipe_source_scan_ok":false,"non_turnstile_site_count":5,"notification_directory_present":false,"runtime_probe_7of7":true,"send_attempts":0,"site_count_7":true,"support_captcha_signal_present":false,"turnstile_site_count":2},"codevipe_deployed_source":null,"non_turnstile_sites":[{"captcha":{"hcaptcha_secret_nonempty":false,"hcaptcha_sitekey_nonempty":false,"provider":"OTHER_question","shape":[],"turnstile_secret_nonempty":false,"turnstile_sitekey_nonempty":false},"provider":"OTHER_question","slug":"codevipe"},{"captcha":{"hcaptcha_secret_nonempty":false,"hcaptcha_sitekey_nonempty":false,"provider":"OTHER_question","shape":[],"turnstile_secret_nonempty":false,"turnstile_sitekey_nonempty":false},"provider":"OTHER_question","slug":"dsmods"},{"captcha":{"hcaptcha_secret_nonempty":false,"hcaptcha_sitekey_nonempty":false,"provider":"OTHER_question","shape":[],"turnstile_secret_nonempty":false,"turnstile_sitekey_nonempty":false},"provider":"OTHER_question","slug":"gamevipe"},{"captcha":{"hcaptcha_secret_nonempty":false,"hcaptcha_sitekey_nonempty":false,"provider":"OTHER_question","shape":[],"turnstile_secret_nonempty":false,"turnstile_sitekey_nonempty":false},"provider":"OTHER_question","slug":"hkmods"},{"captcha":{"hcaptcha_secret_nonempty":false,"hcaptcha_sitekey_nonempty":false,"provider":"OTHER_question","shape":[],"turnstile_secret_nonempty":false,"turnstile_sitekey_nonempty":false},"provider":"OTHER_question","slug":"zakrutim"}],"schema_version":1,"scope":"READ_ONLY_GRAM_SUPPORT_TURNSTILE_MAIL_RUNTIME_SPLIT_AUDIT","secret_policy":"NO_CAPTCHA_OR_MAIL_SECRET_VALUES_HASHES_OR_LENGTHS_EXPORTED","sites":[{"captcha":{"hcaptcha_secret_nonempty":false,"hcaptcha_sitekey_nonempty":false,"provider":"OTHER_question","shape":[],"turnstile_secret_nonempty":false,"turnstile_sitekey_nonempty":false},"gram_option_ids":["gramSupportGuestRateLimitCount","gramSupportGuestRateLimitWindowSeconds","gramSupportGuestTokenTtlDays","gramSupportSiteId"],"mail":{"sendmail_exec":true,"transport":"sendmail"},"slug":"codevipe"},{"captcha":{"hcaptcha_secret_nonempty":false,"hcaptcha_sitekey_nonempty":false,"provider":"OTHER_question","shape":[],"turnstile_secret_nonempty":false,"turnstile_sitekey_nonempty":false},"gram_option_ids":["gramSupportGuestRateLimitCount","gramSupportGuestRateLimitWindowSeconds","gramSupportGuestTokenTtlDays","gramSupportSiteId"],"mail":{"sendmail_exec":true,"transport":"sendmail"},"slug":"dsmods"},{"captcha":{"hcaptcha_secret_nonempty":false,"hcaptcha_sitekey_nonempty":false,"provider":"OTHER_question","shape":[],"turnstile_secret_nonempty":false,"turnstile_sitekey_nonempty":false},"gram_option_ids":["gramSupportGuestRateLimitCount","gramSupportGuestRateLimitWindowSeconds","gramSupportGuestTokenTtlDays","gramSupportSiteId"],"mail":{"sendmail_exec":true,"transport":"sendmail"},"slug":"gamevipe"},{"captcha":{"hcaptcha_secret_nonempty":false,"hcaptcha_sitekey_nonempty":false,"provider":"OTHER_question","shape":[],"turnstile_secret_nonempty":false,"turnstile_sitekey_nonempty":false},"gram_option_ids":["gramSupportGuestRateLimitCount","gramSupportGuestRateLimitWindowSeconds","gramSupportGuestTokenTtlDays","gramSupportSiteId"],"mail":{"sendmail_exec":true,"transport":"sendmail"},"slug":"hkmods"},{"captcha":{"hcaptcha_secret_nonempty":false,"hcaptcha_sitekey_nonempty":false,"provider":"CLOUDFLARE_TURNSTILE","shape":[{"kind":"string","nonempty":true,"path":"turnstileSiteKey"},{"kind":"string","nonempty":true,"path":"turnstileSecretKey"}],"turnstile_secret_nonempty":true,"turnstile_sitekey_nonempty":true},"gram_option_ids":["gramSupportGuestRateLimitCount","gramSupportGuestRateLimitWindowSeconds","gramSupportGuestTokenTtlDays","gramSupportSiteId"],"mail":{"sendmail_exec":true,"transport":"sendmail"},"slug":"kingofwolk"},{"captcha":{"hcaptcha_secret_nonempty":false,"hcaptcha_sitekey_nonempty":false,"provider":"OTHER_question","shape":[],"turnstile_secret_nonempty":false,"turnstile_sitekey_nonempty":false},"gram_option_ids":["gramSupportGuestRateLimitCount","gramSupportGuestRateLimitWindowSeconds","gramSupportGuestTokenTtlDays","gramSupportSiteId"],"mail":{"sendmail_exec":true,"transport":"sendmail"},"slug":"zakrutim"},{"captcha":{"hcaptcha_secret_nonempty":false,"hcaptcha_sitekey_nonempty":false,"provider":"CLOUDFLARE_TURNSTILE","shape":[{"kind":"string","nonempty":true,"path":"turnstileSiteKey"},{"kind":"string","nonempty":true,"path":"turnstileSecretKey"}],"turnstile_secret_nonempty":true,"turnstile_sitekey_nonempty":true},"gram_option_ids":["gramSupportGuestRateLimitCount","gramSupportGuestRateLimitWindowSeconds","gramSupportGuestTokenTtlDays","gramSupportSiteId"],"mail":{"sendmail_exec":true,"transport":"sendmail"},"slug":"newfi"}],"source_transport":{"err_truncated":false,"error":"GUEST_JSON_JSONDecodeError","guest_stderr_present":true,"inner_rc":1,"ok":false,"out_truncated":false,"transport_rc":0,"transport_stderr_bytes":0,"transport_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},"target_notification_email":"aleisaev@yandex.ru","transport_evidence":[{"slug":"codevipe","transport":{"err_truncated":false,"guest_stderr_present":false,"inner_rc":0,"ok":true,"out_truncated":false,"transport_rc":0,"transport_stderr_bytes":0,"transport_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}},{"slug":"dsmods","transport":{"err_truncated":false,"guest_stderr_present":false,"inner_rc":0,"ok":true,"out_truncated":false,"transport_rc":0,"transport_stderr_bytes":0,"transport_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}},{"slug":"gamevipe","transport":{"err_truncated":false,"guest_stderr_present":false,"inner_rc":0,"ok":true,"out_truncated":false,"transport_rc":0,"transport_stderr_bytes":0,"transport_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}},{"slug":"hkmods","transport":{"err_truncated":false,"guest_stderr_present":false,"inner_rc":0,"ok":true,"out_truncated":false,"transport_rc":0,"transport_stderr_bytes":0,"transport_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}},{"slug":"kingofwolk","transport":{"err_truncated":false,"guest_stderr_present":false,"inner_rc":0,"ok":true,"out_truncated":false,"transport_rc":0,"transport_stderr_bytes":0,"transport_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}},{"slug":"zakrutim","transport":{"err_truncated":false,"guest_stderr_present":false,"inner_rc":0,"ok":true,"out_truncated":false,"transport_rc":0,"transport_stderr_bytes":0,"transport_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}},{"slug":"newfi","transport":{"err_truncated":false,"guest_stderr_present":false,"inner_rc":0,"ok":true,"out_truncated":false,"transport_rc":0,"transport_stderr_bytes":0,"transport_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}}],"turnstile_sites":["kingofwolk","newfi"]} -GRAM516R1R2_DECISION=HOLD_AUDIT_INCOMPLETE -GRAM516R1R2_NEXT_GATE=STOP_NO_MUTATION -HOMELAB_RESULT_CONTRACT={"version":1,"command_id":"SUPPORT-260918-GRAM-SUPPORT-TURNSTILE-MAIL-RUNTIME-SPLIT-AUDIT-516R1R2","status":"FAIL","changes_made":false,"rollback_started":false,"rollback_restored":null} +GRAM516R1R3_SAFE_REPORT={"err_truncated":false,"guest_stderr_present":false,"inner_rc":0,"ok":true,"out_truncated":false,"source":{"notification_files":["Notification/AlertConsumer.php","Notification/DeliveryGate.php","Notification/Dispatcher.php","Notification/EmailConsumer.php","Notification/EntityDeliveryGate.php","Notification/EventDispatcher.php","Notification/Policy.php","Notification/RecipientKey.php"],"ok":true,"root_tree_files":["Attachment/TicketHandler.php","Entity/AttachmentMeta.php","Entity/Delivery.php","Entity/Message.php","Entity/RateLimit.php","Entity/SupportEvent.php","Entity/Ticket.php","Listener/SourcePageContext.php","Notification/AlertConsumer.php","Notification/DeliveryGate.php","Notification/Dispatcher.php","Notification/EmailConsumer.php","Notification/EntityDeliveryGate.php","Notification/EventDispatcher.php","Notification/Policy.php","Notification/RecipientKey.php","Pub/Controller/AccountSupport.php","Pub/Controller/Pwa.php","Pub/Controller/StaffSupport.php","Pub/Controller/Support.php","Pub/View/PwaServiceWorker.php","Repository/Message.php","Repository/Ticket.php","Security/GuestTicketSession.php","Security/TicketAccess.php","Service/GuestScreenshotService.php","Service/GuestTicketCreateAttachmentFlow.php","Service/RateLimiter.php","Service/SourceContextResolver.php","Service/Ticket/Creator.php","Service/Ticket/Reply.php","Service/Ticket/StaffUpdate.php","Setup.php","Util/GuestToken.php","Util/PublicRef.php","Util/RateKey.php","Util/SafeContext.php","Util/SiteConfig.php","Util/StatusPolicy.php","_data/code_event_listeners.xml","_data/content_type_fields.xml","_data/option_groups.xml","_data/options.xml","_data/permission_interface_groups.xml","_data/permissions.xml","_data/phrases.xml","_data/routes.xml","_data/template_modifications.xml","_data/templates.xml","addon.json","hashes.json"],"targets":{"Notification/AlertConsumer.php":{"line_count":74,"matched_context":[{"sep":true},{"line":1,"text":"gate = $gate;"},{"line":16,"text":" }"},{"line":17,"text":""},{"line":18,"text":" public function consumeForUser(SupportEvent $event, Ticket $ticket, User $recipient): bool"},{"line":19,"text":" {"},{"line":20,"text":" if (!Policy::mayEmitExternal($event) || !$recipient->user_id)"},{"line":21,"text":" {"},{"line":22,"text":" return false;"},{"line":23,"text":" }"},{"line":24,"text":""},{"line":25,"text":" // Never alert a user about a ticket they cannot access."},{"line":26,"text":" $isOwner = $ticket->user_id && (int)$ticket->user_id === (int)$recipient->user_id;"},{"line":27,"text":" $isStaff = $recipient->hasPermission('gramSupport', 'viewAll')"},{"line":28,"text":" || $recipient->hasPermission('gramSupport', 'manage');"},{"line":29,"text":""},{"line":30,"text":" if (!$isOwner && !$isStaff)"},{"line":31,"text":" {"},{"line":32,"text":" return false;"},{"line":33,"text":" }"},{"line":34,"text":""},{"line":35,"text":" $recipientKey = RecipientKey::user((int)$recipient->user_id);"},{"line":36,"text":" if (!$this->gate->acquire((int)$event->event_id, 'xf_alert', $recipientKey))"},{"line":37,"text":" {"},{"line":38,"text":" return false;"},{"line":39,"text":" }"},{"line":40,"text":""},{"line":41,"text":" try"},{"line":42,"text":" {"},{"line":43,"text":" /** @var \\XF\\Repository\\UserAlertRepository $alerts */"},{"line":44,"text":" $alerts = \\XF::repository('XF:UserAlert');"},{"line":45,"text":""},{"line":46,"text":" // Back the alert with the recipient's own User content so the"},{"line":47,"text":" // core alert handler remains valid/viewable. Ticket access is"},{"line":48,"text":" // separately enforced before emission and again on ticket route."},{"line":49,"text":" $alerts->alert("},{"line":50,"text":" $recipient,"},{"line":51,"text":" 0,"},{"line":52,"text":" 'Support',"},{"line":53,"text":" 'user',"},{"line":54,"text":" (int)$recipient->user_id,"},{"line":55,"text":" 'gram_support_evt',"},{"line":56,"text":" Policy::alertExtraData($event),"},{"line":57,"text":" ['usePush' => false]"},{"line":58,"text":" );"},{"line":59,"text":""},{"line":60,"text":" $this->gate->markSent((int)$event->event_id, 'xf_alert', $recipientKey);"},{"line":61,"text":" return true;"},{"line":62,"text":" }"},{"line":63,"text":" catch (\\Throwable $e)"},{"line":64,"text":" {"},{"line":65,"text":" $this->gate->markFailed("},{"line":66,"text":" (int)$event->event_id,"},{"line":67,"text":" 'xf_alert',"},{"line":68,"text":" $recipientKey,"},{"line":69,"text":" get_class($e)"},{"line":70,"text":" );"},{"line":71,"text":" throw $e;"},{"line":72,"text":" }"}],"present":true,"sha256":"e22d0cdb0a0806c6a231504409bd17cd781caab0f28181aff7246b0295d8cd98"},"Notification/Dispatcher.php":{"line_count":67,"matched_context":[{"sep":true},{"line":1,"text":"alerts = $alerts;"},{"line":17,"text":" $this->email = $email;"},{"line":18,"text":" }"},{"line":19,"text":""},{"line":20,"text":" /**"},{"line":21,"text":" * Owner-facing delivery. Staff alert fan-out is intentionally provided by"},{"line":22,"text":" * dispatchStaff() so staff recipients must still pass Gram Support ACL."},{"line":23,"text":" */"},{"line":24,"text":" public function dispatchOwner(SupportEvent $event, Ticket $ticket, ?User $owner): void"},{"line":25,"text":" {"},{"line":26,"text":" if (!Policy::mayEmitExternal($event))"},{"line":27,"text":" {"},{"line":28,"text":" return;"},{"line":29,"text":" }"},{"line":30,"text":""},{"line":31,"text":" if ($owner && $owner->user_id)"},{"line":32,"text":" {"},{"line":33,"text":" $this->alerts->consumeForUser($event, $ticket, $owner);"},{"line":34,"text":" $this->email->consumeForRegistered($event, $ticket, $owner);"},{"line":35,"text":" return;"},{"line":36,"text":" }"},{"line":37,"text":""},{"line":38,"text":" $this->email->consumeForGuest($event, $ticket);"},{"line":39,"text":" }"},{"line":40,"text":""},{"line":41,"text":" /**"},{"line":42,"text":" * @param User[] $staffUsers"},{"line":43,"text":" */"},{"line":44,"text":" public function dispatchStaff(SupportEvent $event, Ticket $ticket, array $staffUsers): void"},{"line":45,"text":" {"},{"line":46,"text":" if (!Policy::mayEmitExternal($event))"},{"line":47,"text":" {"},{"line":48,"text":" return;"},{"line":49,"text":" }"},{"line":50,"text":""},{"line":51,"text":" foreach ($staffUsers as $staff)"},{"line":52,"text":" {"},{"line":53,"text":" if (!$staff instanceof User || !$staff->user_id)"},{"line":54,"text":" {"},{"line":55,"text":" continue;"},{"line":56,"text":" }"},{"line":57,"text":""},{"line":58,"text":" if (!$staff->hasPermission('gramSupport', 'viewAll')"},{"line":59,"text":" && !$staff->hasPermission('gramSupport', 'manage'))"},{"line":60,"text":" {"},{"line":61,"text":" continue;"},{"line":62,"text":" }"},{"line":63,"text":""},{"line":64,"text":" $this->alerts->consumeForUser($event, $ticket, $staff);"},{"line":65,"text":" }"},{"line":66,"text":" }"},{"line":67,"text":"}"}],"present":true,"sha256":"a043d94bc91feeed0969ad218566cebac76b2d804e7c69cc2b87b94d42f5bb85"},"Pub/Controller/Support.php":{"line_count":236,"matched_context":[{"sep":true},{"line":1,"text":"isPost())"},{"line":37,"text":" {"},{"line":38,"text":" $this->assertPostOnly();"},{"line":39,"text":""},{"line":40,"text":" $email = $this->filter('email', 'str');"},{"line":41,"text":" $category = $this->filter('category', 'str');"},{"line":42,"text":" $subject = $this->filter('subject', 'str');"},{"line":43,"text":" $message = $this->filter('message', 'str');"},{"line":44,"text":""},{"line":45,"text":" if (!$visitor->user_id)"},{"line":46,"text":" {"},{"line":47,"text":" if (!$this->captchaIsValid(true))"},{"line":48,"text":" {"},{"line":49,"text":" return $this->error("},{"line":50,"text":" \\XF::phrase('did_not_complete_the_captcha_verification_properly')"},{"line":51,"text":" );"},{"line":52,"text":" }"},{"line":53,"text":""},{"line":54,"text":" /** @var RateLimiter $rateLimiter */"},{"line":55,"text":" $rateLimiter = $this->service(RateLimiter::class);"},{"line":56,"text":" $allowed = $rateLimiter->consumeGuest("},{"line":57,"text":" SiteConfig::siteId(),"},{"line":58,"text":" $email,"},{"line":59,"text":" SiteConfig::guestRateLimitCount(),"},{"line":60,"text":" SiteConfig::guestRateLimitWindowSeconds()"},{"line":61,"text":" );"},{"line":62,"text":""},{"line":63,"text":" if (!$allowed)"},{"sep":true},{"line":74,"text":" if ($upload)"},{"line":75,"text":" {"},{"line":76,"text":" $attachmentId = $attachmentFlow->prepare($upload, $tempHash);"},{"line":77,"text":" }"},{"line":78,"text":""},{"line":79,"text":" /** @var Creator $creator */"},{"line":80,"text":" $creator = $this->service(Creator::class);"},{"line":81,"text":" $created = $creator->create(["},{"line":82,"text":" 'email' => $email,"},{"line":83,"text":" 'category' => $category,"},{"line":84,"text":" 'subject' => $subject,"},{"line":85,"text":" 'message' => $message"},{"line":86,"text":" ]);"},{"line":87,"text":""},{"sep":true},{"line":127,"text":" }"},{"line":128,"text":""},{"line":129,"text":" return $this->view("},{"line":130,"text":" 'Gram\\Support:Create',"},{"line":131,"text":" 'gram_support_create',"},{"line":132,"text":" ['forceCaptcha' => !$visitor->user_id]"},{"line":133,"text":" );"},{"line":134,"text":" }"},{"line":135,"text":""},{"line":136,"text":" public function actionAccess()"},{"line":137,"text":" {"},{"sep":true},{"line":183,"text":" 'messages' => $messages"},{"line":184,"text":" ]"},{"line":185,"text":" );"},{"line":186,"text":" }"},{"line":187,"text":""},{"line":188,"text":" public function actionReply()"},{"line":189,"text":" {"},{"line":190,"text":" $this->assertPostOnly();"},{"line":191,"text":""},{"line":192,"text":" $ticket = $this->assertViewableTicketByRef($this->filter('ref', 'str'));"},{"line":193,"text":" $visitor = \\XF::visitor();"},{"line":194,"text":""},{"line":195,"text":" if ($visitor->user_id)"},{"line":196,"text":" {"},{"line":197,"text":" if ((int)$ticket->user_id !== (int)$visitor->user_id"},{"line":198,"text":" || !$visitor->hasPermission('gramSupport', 'replyOwn'))"},{"line":199,"text":" {"},{"line":200,"text":" return $this->noPermission();"},{"line":201,"text":" }"},{"line":202,"text":" }"},{"line":203,"text":" elseif (!GuestTicketSession::hasGrant($ticket))"},{"sep":true},{"line":205,"text":" return $this->noPermission();"},{"line":206,"text":" }"},{"line":207,"text":""},{"line":208,"text":" $message = $this->filter('message', 'str');"},{"line":209,"text":""},{"line":210,"text":" /** @var Reply $reply */"},{"line":211,"text":" $reply = $this->service(Reply::class);"},{"line":212,"text":" $reply->add($ticket, $message, false, false);"},{"line":213,"text":""},{"line":214,"text":" return $this->redirect("},{"line":215,"text":" $this->buildLink('support/ticket', null, ['ref' => $ticket->public_ref])"},{"line":216,"text":" );"},{"line":217,"text":" }"}],"present":true,"sha256":"6cb34b1d27e9f0132263637cefa62fe0e998a9a86620b7ac33199f41ee86f9d4"},"Service/Ticket/Creator.php":{"line_count":139,"matched_context":[{"sep":true},{"line":8,"text":"use Gram\\Support\\Util\\PublicRef;"},{"line":9,"text":"use Gram\\Support\\Util\\SafeContext;"},{"line":10,"text":"use Gram\\Support\\Util\\SiteConfig;"},{"line":11,"text":"use Gram\\Support\\Util\\StatusPolicy;"},{"line":12,"text":"use XF\\Service\\AbstractService;"},{"line":13,"text":"use Gram\\Support\\Notification\\EventDispatcher;"},{"line":14,"text":""},{"line":15,"text":"class Creator extends AbstractService"},{"line":16,"text":"{"},{"line":17,"text":" public function create(array $input, ?int $now = null): array"},{"line":18,"text":" {"},{"line":19,"text":" $now = $now ?? \\XF::$time;"},{"line":20,"text":" $visitor = \\XF::visitor();"},{"line":21,"text":" $siteId = SiteConfig::siteId();"},{"line":22,"text":""},{"line":23,"text":" $email = strtolower(trim((string)($input['email'] ?? '')));"},{"line":24,"text":" $category = trim((string)($input['category'] ?? ''));"},{"line":25,"text":" $subject = trim((string)($input['subject'] ?? ''));"},{"line":26,"text":" $messageText = trim((string)($input['message'] ?? ''));"},{"line":27,"text":""},{"line":28,"text":" if ($category === '' || $messageText === '')"},{"line":29,"text":" {"},{"line":30,"text":" throw new \\InvalidArgumentException('GRAM_SUPPORT_REQUIRED_FIELDS');"},{"line":31,"text":" }"},{"line":32,"text":""},{"line":33,"text":" if (!$visitor->user_id && !filter_var($email, FILTER_VALIDATE_EMAIL))"},{"line":34,"text":" {"},{"line":35,"text":" throw new \\InvalidArgumentException('GRAM_SUPPORT_VALID_EMAIL_REQUIRED');"},{"line":36,"text":" }"},{"line":37,"text":""},{"line":38,"text":" if ($subject === '')"},{"line":39,"text":" {"},{"line":40,"text":" $subject = function_exists('mb_substr')"},{"sep":true},{"line":68,"text":" /** @var Ticket $ticket */"},{"line":69,"text":" $ticket = $this->em()->create('Gram\\Support:Ticket');"},{"line":70,"text":" $ticket->public_ref = 'TMP-' . bin2hex(random_bytes(12));"},{"line":71,"text":" $ticket->site_id = $siteId;"},{"line":72,"text":" $ticket->user_id = (int)$visitor->user_id;"},{"line":73,"text":" $ticket->guest_email = $visitor->user_id ? '' : $email;"},{"line":74,"text":" $ticket->guest_access_token_hash = $guest ? $guest['hash'] : '';"},{"line":75,"text":" $ticket->guest_access_expires = $guest"},{"line":76,"text":" ? $now + SiteConfig::guestTokenTtlSeconds()"},{"line":77,"text":" : 0;"},{"line":78,"text":" $ticket->node_id = (int)($context['node_id'] ?? 0);"},{"sep":true},{"line":96,"text":" $ticket->save();"},{"line":97,"text":""},{"line":98,"text":" $message = $this->em()->create('Gram\\Support:Message');"},{"line":99,"text":" $message->ticket_id = (int)$ticket->ticket_id;"},{"line":100,"text":" $message->user_id = (int)$visitor->user_id;"},{"line":101,"text":" $message->is_staff = 0;"},{"line":102,"text":" $message->is_internal = 0;"},{"line":103,"text":" $message->message = $messageText;"},{"line":104,"text":" $message->message_date = $now;"},{"line":105,"text":" $message->save();"},{"line":106,"text":""},{"line":107,"text":" $ticket->last_message_id = (int)$message->message_id;"},{"line":108,"text":" $ticket->save();"},{"line":109,"text":""},{"line":110,"text":" $event = $this->em()->create('Gram\\Support:SupportEvent');"},{"line":111,"text":" $event->ticket_id = (int)$ticket->ticket_id;"},{"line":112,"text":" $event->site_id = $siteId;"},{"line":113,"text":" $event->actor_user_id = (int)$visitor->user_id;"},{"line":114,"text":" $event->event_type = 'ticket_created';"},{"line":115,"text":" $event->event_date = $now;"},{"line":116,"text":" $event->safe_payload_json = json_encode(["},{"line":117,"text":" 'public_ref' => (string)$ticket->public_ref,"},{"line":118,"text":" 'category' => (string)$ticket->category,"},{"line":119,"text":" 'is_internal' => false"},{"sep":true},{"line":126,"text":" {"},{"line":127,"text":" $db->rollback();"},{"line":128,"text":" throw $e;"},{"line":129,"text":" }"},{"line":130,"text":""},{"line":131,"text":" (new EventDispatcher())->dispatchSafely($event, $ticket);"},{"line":132,"text":""},{"line":133,"text":" return ["},{"line":134,"text":" 'ticket' => $ticket,"},{"line":135,"text":" 'event' => $event,"},{"line":136,"text":" 'guest_secret' => $guest ? $guest['secret'] : null"}],"present":true,"sha256":"7a84cd77761a7943495f9d3ef11269987f85265170ad6cf927a1661b8bf0a99a"},"Service/Ticket/Reply.php":{"line_count":68,"matched_context":[{"sep":true},{"line":3,"text":"namespace Gram\\Support\\Service\\Ticket;"},{"line":4,"text":""},{"line":5,"text":"use Gram\\Support\\Entity\\Ticket;"},{"line":6,"text":"use Gram\\Support\\Util\\SiteConfig;"},{"line":7,"text":"use XF\\Service\\AbstractService;"},{"line":8,"text":"use Gram\\Support\\Notification\\EventDispatcher;"},{"line":9,"text":""},{"line":10,"text":"class Reply extends AbstractService"},{"line":11,"text":"{"},{"line":12,"text":" public function add(Ticket $ticket, string $messageText, bool $isStaff, bool $isInternal = false): array"},{"line":13,"text":" {"},{"line":14,"text":" $messageText = trim($messageText);"},{"line":15,"text":" if ($messageText === '')"},{"line":16,"text":" {"},{"line":17,"text":" throw new \\InvalidArgumentException('GRAM_SUPPORT_EMPTY_REPLY');"},{"line":18,"text":" }"},{"line":19,"text":" if ($isInternal && !$isStaff)"},{"line":20,"text":" {"},{"line":21,"text":" throw new \\LogicException('INTERNAL_REQUIRES_STAFF');"},{"line":22,"text":" }"},{"line":23,"text":""},{"line":24,"text":" $visitor = \\XF::visitor();"},{"line":25,"text":" $now = \\XF::$time;"},{"line":26,"text":" $db = \\XF::db();"},{"sep":true},{"line":29,"text":" try"},{"line":30,"text":" {"},{"line":31,"text":" $message = $this->em()->create('Gram\\Support:Message');"},{"line":32,"text":" $message->ticket_id = (int)$ticket->ticket_id;"},{"line":33,"text":" $message->user_id = (int)$visitor->user_id;"},{"line":34,"text":" $message->is_staff = $isStaff ? 1 : 0;"},{"line":35,"text":" $message->is_internal = $isInternal ? 1 : 0;"},{"line":36,"text":" $message->message = $messageText;"},{"line":37,"text":" $message->message_date = $now;"},{"line":38,"text":" $message->save();"},{"line":39,"text":""},{"line":40,"text":" $ticket->last_message_id = (int)$message->message_id;"},{"line":41,"text":" $ticket->updated_date = $now;"},{"line":42,"text":" $ticket->save();"},{"line":43,"text":""},{"line":44,"text":" $event = $this->em()->create('Gram\\Support:SupportEvent');"},{"line":45,"text":" $event->ticket_id = (int)$ticket->ticket_id;"},{"line":46,"text":" $event->site_id = (string)$ticket->site_id;"},{"line":47,"text":" $event->actor_user_id = (int)$visitor->user_id;"},{"line":48,"text":" $event->event_type = $isInternal ? 'internal_note_created' : 'ticket_reply_created';"},{"line":49,"text":" $event->event_date = $now;"},{"line":50,"text":" $event->safe_payload_json = json_encode(["},{"line":51,"text":" 'public_ref' => (string)$ticket->public_ref,"},{"line":52,"text":" 'is_internal' => $isInternal"},{"line":53,"text":" ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);"},{"sep":true},{"line":59,"text":" {"},{"line":60,"text":" $db->rollback();"},{"line":61,"text":" throw $e;"},{"line":62,"text":" }"},{"line":63,"text":""},{"line":64,"text":" (new EventDispatcher())->dispatchSafely($event, $ticket);"},{"line":65,"text":""},{"line":66,"text":" return ['message' => $message, 'event' => $event];"},{"line":67,"text":" }"},{"line":68,"text":"}"}],"present":true,"sha256":"482571a2ccf07a3c8672f34940243264a8271ef8c3cbc4fadbb46112afd36830"},"_data/templates.xml":{"line_count":781,"matched_context":[{"sep":true},{"line":1,"text":""},{"line":2,"text":""},{"line":3,"text":" "},{"line":9,"text":" "},{"line":553,"text":" "},{"line":575,"text":" "},{"line":647,"text":" "},{"line":764,"text":" "},{"line":772,"text":" "},{"line":781,"text":""}],"present":true,"sha256":"1a344bd306135486d2019bf0a5cafff5eb472882f7b3433ef13af4556a4c791e"}}},"transport_rc":0,"transport_stderr_bytes":0,"transport_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"} +GRAM516R1R3_NOTIFICATION_FILES=["Notification/AlertConsumer.php","Notification/DeliveryGate.php","Notification/Dispatcher.php","Notification/EmailConsumer.php","Notification/EntityDeliveryGate.php","Notification/EventDispatcher.php","Notification/Policy.php","Notification/RecipientKey.php"] +GRAM516R1R3_EMAIL_CONSUMER_FILE_PRESENT=true +GRAM516R1R3_DECISION=PASS_NOTIFY_CAPTCHA_SOURCE_EVIDENCE_CAPTURED +GRAM516R1R3_NEXT_GATE=BUILD_EXACT_TURNSTILE_AND_EMAIL_NOTIFY_CHANGE +HOMELAB_RESULT_CONTRACT={"version":1,"command_id":"SUPPORT-260918-GRAM-SUPPORT-NOTIFY-CAPTCHA-SOURCE-READONLY-516R1R3","status":"OK","changes_made":false,"rollback_started":false,"rollback_restored":null} OUTPUT_END CHAT_OUTPUT_END