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\":\" <xf:macro id=\\\"content\\\">\"},{\"line\":4,\"text\":\"\\t{{ phrase('gram_support_alert_ticket_event', {\"},{\"line\":5,\"text\":\"\\t\\t'public_ref': $alert.extra_data.public_ref\"},{\"line\":6,\"text\":\"\\t}) }}\"},{\"line\":7,\"text\":\"</xf:macro>\"},{\"line\":8,\"text\":\"\"},{\"line\":9,\"text\":\" .gramSupport-launcher\"},{\"line\":10,\"text\":\"{\"},{\"sep\":true},{\"line\":490,\"text\":\"\"},{\"line\":491,\"text\":\"<xf:form action=\\\"{{ link('support/create') }}\\\" method=\\\"post\\\" upload=\\\"true\\\" class=\\\"block\\\">\"},{\"line\":492,\"text\":\"\\t<div class=\\\"block-container\\\">\"},{\"line\":493,\"text\":\"\\t\\t<div class=\\\"block-body\\\">\"},{\"line\":494,\"text\":\"\\t\\t\\t<xf:if is=\\\"!$xf.visitor.user_id\\\">\"},{\"line\":495,\"text\":\"\\t\\t\\t\\t<xf:textboxrow name=\\\"email\\\" type=\\\"email\\\" required=\\\"required\\\"\"},{\"line\":496,\"text\":\"\\t\\t\\t\\t\\tlabel=\\\"{{ phrase('email') }}\\\" />\"},{\"line\":497,\"text\":\"\\t\\t\\t</xf:if>\"},{\"line\":498,\"text\":\"\\t\\t\\t<xf:selectrow name=\\\"category\\\" required=\\\"required\\\"\"},{\"line\":499,\"text\":\"\\t\\t\\t\\tlabel=\\\"{{ phrase('gram_support_category') }}\\\">\"},{\"line\":500,\"text\":\"\\t\\t\\t\\t<xf:option value=\\\"technical\\\">{{ phrase('gram_support_category_technical') }}</xf:option>\"},{\"line\":501,\"text\":\"\\t\\t\\t\\t<xf:option value=\\\"account\\\">{{ phrase('gram_support_category_account') }}</xf:option>\"},{\"sep\":true},{\"line\":507,\"text\":\"\\t\\t\\t<xf:textarearow name=\\\"message\\\" required=\\\"required\\\" rows=\\\"8\\\"\"},{\"line\":508,\"text\":\"\\t\\t\\t\\tlabel=\\\"{{ phrase('gram_support_description') }}\\\" />\"},{\"line\":509,\"text\":\"\\t\\t\\t<xf:uploadrow name=\\\"screenshot\\\" accept=\\\".gif,.jpeg,.jpg,.jpe,.png,.webp\\\"\"},{\"line\":510,\"text\":\"\\t\\t\\t\\tlabel=\\\"{{ phrase('gram_support_screenshot_optional') }}\\\" />\"},{\"line\":511,\"text\":\"\\t\\t\\t<xf:if is=\\\"!$xf.visitor.user_id\\\">\"},{\"line\":512,\"text\":\"\\t\\t\\t\\t<xf:captcharow label=\\\"{{ phrase('verification') }}\\\"\"},{\"line\":513,\"text\":\"\\t\\t\\t\\t\\thint=\\\"{{ phrase('required') }}\\\" force=\\\"true\\\" />\"},{\"line\":514,\"text\":\"\\t\\t\\t</xf:if>\"},{\"line\":515,\"text\":\"\\t\\t</div>\"},{\"line\":516,\"text\":\"\\t\\t<xf:submitrow submit=\\\"{{ phrase('gram_support_submit') }}\\\" />\"},{\"line\":517,\"text\":\"\\t</div>\"},{\"sep\":true},{\"line\":548,\"text\":\"\\t\\taria-label=\\\"{{ phrase('gram_support_open_support') }}\\\">\"},{\"line\":549,\"text\":\"\\t\\t{{ phrase('gram_support_open_support') }}\"},{\"line\":550,\"text\":\"\\t</a>\"},{\"line\":551,\"text\":\"</xf:if>\"},{\"line\":552,\"text\":\"\"},{\"line\":553,\"text\":\" <xf:title>{{ phrase('gram_support_staff_queue') }}</xf:title>\"},{\"line\":554,\"text\":\"<xf:css src=\\\"gram_support.less\\\" />\"},{\"line\":555,\"text\":\"<span class=\\\"js-gramSupportPwaConfig\\\" hidden\"},{\"line\":556,\"text\":\"\\tdata-manifest-url=\\\"{{ base_url('js/gram/support/pwa/manifest.webmanifest') }}\\\"\"},{\"line\":557,\"text\":\"\\tdata-sw-url=\\\"{{ link('support/service-worker') }}\\\"></span>\"},{\"line\":558,\"text\":\"<xf:js addon=\\\"Gram/Support\\\" src=\\\"gram/support/staff-pwa.js\\\" min=\\\"1\\\" />\"},{\"line\":559,\"text\":\"\"},{\"line\":560,\"text\":\"<div class=\\\"block\\\">\"},{\"line\":561,\"text\":\"\\t<div class=\\\"block-container\\\">\"},{\"line\":562,\"text\":\"\\t\\t<div class=\\\"block-body\\\">\"},{\"line\":563,\"text\":\"\\t\\t\\t<xf:foreach loop=\\\"$tickets\\\" value=\\\"$ticket\\\">\"},{\"line\":564,\"text\":\"\\t\\t\\t\\t<div class=\\\"block-row\\\">\"},{\"line\":565,\"text\":\"\\t\\t\\t\\t\\t<a href=\\\"{{ link('support/staff/ticket', null, {'ref': $ticket.public_ref}) }}\\\">\"},{\"line\":566,\"text\":\"\\t\\t\\t\\t\\t\\t{{ $ticket.public_ref }} \\u2014 {{ $ticket.subject }}\"},{\"line\":567,\"text\":\"\\t\\t\\t\\t\\t</a>\"},{\"line\":568,\"text\":\"\\t\\t\\t\\t\\t<span class=\\\"u-muted\\\">{{ $ticket.status }}</span>\"},{\"line\":569,\"text\":\"\\t\\t\\t\\t</div>\"},{\"line\":570,\"text\":\"\\t\\t\\t</xf:foreach>\"},{\"line\":571,\"text\":\"\\t\\t</div>\"},{\"line\":572,\"text\":\"\\t</div>\"},{\"line\":573,\"text\":\"</div>\"},{\"line\":574,\"text\":\"\"},{\"line\":575,\"text\":\" 'use strict';\"},{\"line\":576,\"text\":\"\"},{\"line\":577,\"text\":\"const OFFLINE_URL = '{{ base_url('js/gram/support/pwa/offline.html')|escape('js') }}';\"},{\"line\":578,\"text\":\"const STATIC_CACHE = 'gram-support-staff-static-v1';\"},{\"line\":579,\"text\":\"\"},{\"line\":580,\"text\":\"self.addEventListener('install', event => {\"},{\"line\":581,\"text\":\"\\tevent.waitUntil(\"},{\"line\":582,\"text\":\"\\t\\tcaches.open(STATIC_CACHE)\"},{\"line\":583,\"text\":\"\\t\\t\\t.then(cache => cache.addAll([OFFLINE_URL]))\"},{\"sep\":true},{\"line\":587,\"text\":\"\"},{\"line\":588,\"text\":\"self.addEventListener('activate', event => {\"},{\"line\":589,\"text\":\"\\tevent.waitUntil(\"},{\"line\":590,\"text\":\"\\t\\tcaches.keys().then(keys =>\"},{\"line\":591,\"text\":\"\\t\\t\\tPromise.all(keys\"},{\"line\":592,\"text\":\"\\t\\t\\t\\t.filter(key => key.startsWith('gram-support-staff-static-') && key !== STATIC_CACHE)\"},{\"line\":593,\"text\":\"\\t\\t\\t\\t.map(key => caches.delete(key))\"},{\"line\":594,\"text\":\"\\t\\t\\t)\"},{\"line\":595,\"text\":\"\\t\\t).then(() => self.clients.claim())\"},{\"line\":596,\"text\":\"\\t);\"},{\"line\":597,\"text\":\"});\"},{\"sep\":true},{\"line\":607,\"text\":\"\\t\\treturn;\"},{\"line\":608,\"text\":\"\\t}\"},{\"line\":609,\"text\":\"\"},{\"line\":610,\"text\":\"\\t// SECURITY: all support/ticket/account/admin-like content is network-only.\"},{\"line\":611,\"text\":\"\\t// Never cache HTML/API responses that can contain ticket data, identities,\"},{\"line\":612,\"text\":\"\\t// access state, internal notes, attachments or staff-only information.\"},{\"line\":613,\"text\":\"\\tconst sensitive =\"},{\"line\":614,\"text\":\"\\t\\turl.pathname.startsWith('/support/') ||\"},{\"line\":615,\"text\":\"\\t\\turl.pathname.startsWith('/account/') ||\"},{\"line\":616,\"text\":\"\\t\\turl.pathname.startsWith('/admin.php') ||\"},{\"line\":617,\"text\":\"\\t\\trequest.headers.get('accept')?.includes('text/html');\"},{\"sep\":true},{\"line\":642,\"text\":\"\\t\\t\\t})\"},{\"line\":643,\"text\":\"\\t\\t)\"},{\"line\":644,\"text\":\"\\t);\"},{\"line\":645,\"text\":\"});\"},{\"line\":646,\"text\":\"\"},{\"line\":647,\"text\":\" <xf:title>{{ phrase('gram_support_staff_ticket') }} {{ $ticket.public_ref }}</xf:title>\"},{\"line\":648,\"text\":\"<xf:css src=\\\"gram_support.less\\\" />\"},{\"line\":649,\"text\":\"<span class=\\\"js-gramSupportPwaConfig\\\" hidden\"},{\"line\":650,\"text\":\"\\tdata-manifest-url=\\\"{{ base_url('js/gram/support/pwa/manifest.webmanifest') }}\\\"\"},{\"line\":651,\"text\":\"\\tdata-sw-url=\\\"{{ link('support/service-worker') }}\\\"></span>\"},{\"line\":652,\"text\":\"<xf:js addon=\\\"Gram/Support\\\" src=\\\"gram/support/staff-pwa.js\\\" min=\\\"1\\\" />\"},{\"line\":653,\"text\":\"\"},{\"line\":654,\"text\":\"<div class=\\\"block\\\">\"},{\"line\":655,\"text\":\"\\t<div class=\\\"block-container\\\">\"},{\"line\":656,\"text\":\"\\t\\t<h2 class=\\\"block-header\\\">{{ $ticket.public_ref }} \\u2014 {{ $ticket.subject }}</h2>\"},{\"line\":657,\"text\":\"\\t\\t<div class=\\\"block-body\\\">\"},{\"sep\":true},{\"line\":682,\"text\":\"\\t\\t\\t</div>\"},{\"line\":683,\"text\":\"\\t\\t</div>\"},{\"line\":684,\"text\":\"\\t</div>\"},{\"line\":685,\"text\":\"</xf:if>\"},{\"line\":686,\"text\":\"\"},{\"line\":687,\"text\":\"<xf:form action=\\\"{{ link('support/staff/reply', null, {'ref': $ticket.public_ref}) }}\\\" method=\\\"post\\\" class=\\\"block\\\">\"},{\"line\":688,\"text\":\"\\t<div class=\\\"block-container\\\">\"},{\"line\":689,\"text\":\"\\t\\t<div class=\\\"block-body\\\">\"},{\"line\":690,\"text\":\"\\t\\t\\t<xf:textarearow name=\\\"message\\\" required=\\\"required\\\" rows=\\\"6\\\" label=\\\"{{ phrase('gram_support_reply') }}\\\" />\"},{\"line\":691,\"text\":\"\\t\\t\\t<xf:if is=\\\"$xf.visitor.hasPermission('gramSupport', 'internalNotes')\\\">\"},{\"line\":692,\"text\":\"\\t\\t\\t\\t<xf:checkboxrow>\"},{\"line\":693,\"text\":\"\\t\\t\\t\\t\\t<xf:option name=\\\"is_internal\\\" value=\\\"1\\\">{{ phrase('gram_support_internal_note') }}</xf:option>\"},{\"line\":694,\"text\":\"\\t\\t\\t\\t</xf:checkboxrow>\"},{\"line\":695,\"text\":\"\\t\\t\\t</xf:if>\"},{\"line\":696,\"text\":\"\\t\\t</div>\"},{\"line\":697,\"text\":\"\\t\\t<xf:submitrow submit=\\\"{{ phrase('gram_support_send') }}\\\" />\"},{\"line\":698,\"text\":\"\\t</div>\"},{\"line\":699,\"text\":\"</xf:form>\"},{\"line\":700,\"text\":\"\"},{\"line\":701,\"text\":\"<xf:form action=\\\"{{ link('support/staff/update', null, {'ref': $ticket.public_ref}) }}\\\" method=\\\"post\\\" class=\\\"block\\\">\"},{\"line\":702,\"text\":\"\\t<div class=\\\"block-container\\\">\"},{\"line\":703,\"text\":\"\\t\\t<div class=\\\"block-body\\\">\"},{\"line\":704,\"text\":\"\\t\\t\\t<xf:selectrow name=\\\"status\\\" value=\\\"{$ticket.status}\\\" label=\\\"{{ phrase('gram_support_status') }}\\\">\"},{\"line\":705,\"text\":\"\\t\\t\\t\\t<xf:foreach loop=\\\"$statusOptions\\\" value=\\\"$status\\\">\"},{\"line\":706,\"text\":\"\\t\\t\\t\\t\\t<xf:option value=\\\"{$status}\\\">{$status}</xf:option>\"},{\"sep\":true},{\"line\":748,\"text\":\"\\t\\t</div>\"},{\"line\":749,\"text\":\"\\t</div>\"},{\"line\":750,\"text\":\"</xf:if>\"},{\"line\":751,\"text\":\"\"},{\"line\":752,\"text\":\"<xf:if is=\\\"$ticket.status != 'CLOSED'\\\">\"},{\"line\":753,\"text\":\"\\t<xf:form action=\\\"{{ link('support/reply', null, {'ref': $ticket.public_ref}) }}\\\" method=\\\"post\\\" class=\\\"block\\\">\"},{\"line\":754,\"text\":\"\\t\\t<div class=\\\"block-container\\\">\"},{\"line\":755,\"text\":\"\\t\\t\\t<div class=\\\"block-body\\\">\"},{\"line\":756,\"text\":\"\\t\\t\\t\\t<xf:textarearow name=\\\"message\\\" required=\\\"required\\\" rows=\\\"6\\\"\"},{\"line\":757,\"text\":\"\\t\\t\\t\\t\\tlabel=\\\"{{ phrase('gram_support_reply') }}\\\" />\"},{\"line\":758,\"text\":\"\\t\\t\\t</div>\"},{\"line\":759,\"text\":\"\\t\\t\\t<xf:submitrow submit=\\\"{{ phrase('gram_support_send') }}\\\" />\"},{\"line\":760,\"text\":\"\\t\\t</div>\"},{\"line\":761,\"text\":\"\\t</xf:form>\"},{\"line\":762,\"text\":\"</xf:if>\"},{\"line\":763,\"text\":\"\"},{\"line\":764,\"text\":\" <mail:subject>\"},{\"line\":765,\"text\":\"\\t{{ phrase('gram_support_email_ticket_event_subject', {'public_ref': $ticket.public_ref}) }}\"},{\"line\":766,\"text\":\"</mail:subject>\"},{\"line\":767,\"text\":\"\"},{\"line\":768,\"text\":\"{{ phrase('gram_support_email_ticket_event_body_registered', {\"},{\"line\":769,\"text\":\"\\t'public_ref': $ticket.public_ref\"},{\"line\":770,\"text\":\"}) }}\"},{\"line\":771,\"text\":\"\"},{\"line\":772,\"text\":\" <mail:subject>\"},{\"line\":773,\"text\":\"\\t{{ phrase('gram_support_email_ticket_event_subject', {'public_ref': $ticket.public_ref}) }}\"},{\"line\":774,\"text\":\"</mail:subject>\"},{\"line\":775,\"text\":\"\"},{\"line\":776,\"text\":\"{{ phrase('gram_support_email_ticket_event_body_guest', {\"},{\"line\":777,\"text\":\"\\t'public_ref': $ticket.public_ref,\"},{\"line\":778,\"text\":\"\\t'access_url': $accessUrl\"},{\"line\":779,\"text\":\"}) }}\"},{\"line\":780,\"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":" <xf:macro id=\"content\">"},{"line":4,"text":"\t{{ phrase('gram_support_alert_ticket_event', {"},{"line":5,"text":"\t\t'public_ref': $alert.extra_data.public_ref"},{"line":6,"text":"\t}) }}"},{"line":7,"text":"</xf:macro>"},{"line":8,"text":""},{"line":9,"text":" .gramSupport-launcher"},{"line":10,"text":"{"},{"sep":true},{"line":490,"text":""},{"line":491,"text":"<xf:form action=\"{{ link('support/create') }}\" method=\"post\" upload=\"true\" class=\"block\">"},{"line":492,"text":"\t<div class=\"block-container\">"},{"line":493,"text":"\t\t<div class=\"block-body\">"},{"line":494,"text":"\t\t\t<xf:if is=\"!$xf.visitor.user_id\">"},{"line":495,"text":"\t\t\t\t<xf:textboxrow name=\"email\" type=\"email\" required=\"required\""},{"line":496,"text":"\t\t\t\t\tlabel=\"{{ phrase('email') }}\" />"},{"line":497,"text":"\t\t\t</xf:if>"},{"line":498,"text":"\t\t\t<xf:selectrow name=\"category\" required=\"required\""},{"line":499,"text":"\t\t\t\tlabel=\"{{ phrase('gram_support_category') }}\">"},{"line":500,"text":"\t\t\t\t<xf:option value=\"technical\">{{ phrase('gram_support_category_technical') }}</xf:option>"},{"line":501,"text":"\t\t\t\t<xf:option value=\"account\">{{ phrase('gram_support_category_account') }}</xf:option>"},{"sep":true},{"line":507,"text":"\t\t\t<xf:textarearow name=\"message\" required=\"required\" rows=\"8\""},{"line":508,"text":"\t\t\t\tlabel=\"{{ phrase('gram_support_description') }}\" />"},{"line":509,"text":"\t\t\t<xf:uploadrow name=\"screenshot\" accept=\".gif,.jpeg,.jpg,.jpe,.png,.webp\""},{"line":510,"text":"\t\t\t\tlabel=\"{{ phrase('gram_support_screenshot_optional') }}\" />"},{"line":511,"text":"\t\t\t<xf:if is=\"!$xf.visitor.user_id\">"},{"line":512,"text":"\t\t\t\t<xf:captcharow label=\"{{ phrase('verification') }}\""},{"line":513,"text":"\t\t\t\t\thint=\"{{ phrase('required') }}\" force=\"true\" />"},{"line":514,"text":"\t\t\t</xf:if>"},{"line":515,"text":"\t\t</div>"},{"line":516,"text":"\t\t<xf:submitrow submit=\"{{ phrase('gram_support_submit') }}\" />"},{"line":517,"text":"\t</div>"},{"sep":true},{"line":548,"text":"\t\taria-label=\"{{ phrase('gram_support_open_support') }}\">"},{"line":549,"text":"\t\t{{ phrase('gram_support_open_support') }}"},{"line":550,"text":"\t</a>"},{"line":551,"text":"</xf:if>"},{"line":552,"text":""},{"line":553,"text":" <xf:title>{{ phrase('gram_support_staff_queue') }}</xf:title>"},{"line":554,"text":"<xf:css src=\"gram_support.less\" />"},{"line":555,"text":"<span class=\"js-gramSupportPwaConfig\" hidden"},{"line":556,"text":"\tdata-manifest-url=\"{{ base_url('js/gram/support/pwa/manifest.webmanifest') }}\""},{"line":557,"text":"\tdata-sw-url=\"{{ link('support/service-worker') }}\"></span>"},{"line":558,"text":"<xf:js addon=\"Gram/Support\" src=\"gram/support/staff-pwa.js\" min=\"1\" />"},{"line":559,"text":""},{"line":560,"text":"<div class=\"block\">"},{"line":561,"text":"\t<div class=\"block-container\">"},{"line":562,"text":"\t\t<div class=\"block-body\">"},{"line":563,"text":"\t\t\t<xf:foreach loop=\"$tickets\" value=\"$ticket\">"},{"line":564,"text":"\t\t\t\t<div class=\"block-row\">"},{"line":565,"text":"\t\t\t\t\t<a href=\"{{ link('support/staff/ticket', null, {'ref': $ticket.public_ref}) }}\">"},{"line":566,"text":"\t\t\t\t\t\t{{ $ticket.public_ref }} \u2014 {{ $ticket.subject }}"},{"line":567,"text":"\t\t\t\t\t</a>"},{"line":568,"text":"\t\t\t\t\t<span class=\"u-muted\">{{ $ticket.status }}</span>"},{"line":569,"text":"\t\t\t\t</div>"},{"line":570,"text":"\t\t\t</xf:foreach>"},{"line":571,"text":"\t\t</div>"},{"line":572,"text":"\t</div>"},{"line":573,"text":"</div>"},{"line":574,"text":""},{"line":575,"text":" 'use strict';"},{"line":576,"text":""},{"line":577,"text":"const OFFLINE_URL = '{{ base_url('js/gram/support/pwa/offline.html')|escape('js') }}';"},{"line":578,"text":"const STATIC_CACHE = 'gram-support-staff-static-v1';"},{"line":579,"text":""},{"line":580,"text":"self.addEventListener('install', event => {"},{"line":581,"text":"\tevent.waitUntil("},{"line":582,"text":"\t\tcaches.open(STATIC_CACHE)"},{"line":583,"text":"\t\t\t.then(cache => cache.addAll([OFFLINE_URL]))"},{"sep":true},{"line":587,"text":""},{"line":588,"text":"self.addEventListener('activate', event => {"},{"line":589,"text":"\tevent.waitUntil("},{"line":590,"text":"\t\tcaches.keys().then(keys =>"},{"line":591,"text":"\t\t\tPromise.all(keys"},{"line":592,"text":"\t\t\t\t.filter(key => key.startsWith('gram-support-staff-static-') && key !== STATIC_CACHE)"},{"line":593,"text":"\t\t\t\t.map(key => caches.delete(key))"},{"line":594,"text":"\t\t\t)"},{"line":595,"text":"\t\t).then(() => self.clients.claim())"},{"line":596,"text":"\t);"},{"line":597,"text":"});"},{"sep":true},{"line":607,"text":"\t\treturn;"},{"line":608,"text":"\t}"},{"line":609,"text":""},{"line":610,"text":"\t// SECURITY: all support/ticket/account/admin-like content is network-only."},{"line":611,"text":"\t// Never cache HTML/API responses that can contain ticket data, identities,"},{"line":612,"text":"\t// access state, internal notes, attachments or staff-only information."},{"line":613,"text":"\tconst sensitive ="},{"line":614,"text":"\t\turl.pathname.startsWith('/support/') ||"},{"line":615,"text":"\t\turl.pathname.startsWith('/account/') ||"},{"line":616,"text":"\t\turl.pathname.startsWith('/admin.php') ||"},{"line":617,"text":"\t\trequest.headers.get('accept')?.includes('text/html');"},{"sep":true},{"line":642,"text":"\t\t\t})"},{"line":643,"text":"\t\t)"},{"line":644,"text":"\t);"},{"line":645,"text":"});"},{"line":646,"text":""},{"line":647,"text":" <xf:title>{{ phrase('gram_support_staff_ticket') }} {{ $ticket.public_ref }}</xf:title>"},{"line":648,"text":"<xf:css src=\"gram_support.less\" />"},{"line":649,"text":"<span class=\"js-gramSupportPwaConfig\" hidden"},{"line":650,"text":"\tdata-manifest-url=\"{{ base_url('js/gram/support/pwa/manifest.webmanifest') }}\""},{"line":651,"text":"\tdata-sw-url=\"{{ link('support/service-worker') }}\"></span>"},{"line":652,"text":"<xf:js addon=\"Gram/Support\" src=\"gram/support/staff-pwa.js\" min=\"1\" />"},{"line":653,"text":""},{"line":654,"text":"<div class=\"block\">"},{"line":655,"text":"\t<div class=\"block-container\">"},{"line":656,"text":"\t\t<h2 class=\"block-header\">{{ $ticket.public_ref }} \u2014 {{ $ticket.subject }}</h2>"},{"line":657,"text":"\t\t<div class=\"block-body\">"},{"sep":true},{"line":682,"text":"\t\t\t</div>"},{"line":683,"text":"\t\t</div>"},{"line":684,"text":"\t</div>"},{"line":685,"text":"</xf:if>"},{"line":686,"text":""},{"line":687,"text":"<xf:form action=\"{{ link('support/staff/reply', null, {'ref': $ticket.public_ref}) }}\" method=\"post\" class=\"block\">"},{"line":688,"text":"\t<div class=\"block-container\">"},{"line":689,"text":"\t\t<div class=\"block-body\">"},{"line":690,"text":"\t\t\t<xf:textarearow name=\"message\" required=\"required\" rows=\"6\" label=\"{{ phrase('gram_support_reply') }}\" />"},{"line":691,"text":"\t\t\t<xf:if is=\"$xf.visitor.hasPermission('gramSupport', 'internalNotes')\">"},{"line":692,"text":"\t\t\t\t<xf:checkboxrow>"},{"line":693,"text":"\t\t\t\t\t<xf:option name=\"is_internal\" value=\"1\">{{ phrase('gram_support_internal_note') }}</xf:option>"},{"line":694,"text":"\t\t\t\t</xf:checkboxrow>"},{"line":695,"text":"\t\t\t</xf:if>"},{"line":696,"text":"\t\t</div>"},{"line":697,"text":"\t\t<xf:submitrow submit=\"{{ phrase('gram_support_send') }}\" />"},{"line":698,"text":"\t</div>"},{"line":699,"text":"</xf:form>"},{"line":700,"text":""},{"line":701,"text":"<xf:form action=\"{{ link('support/staff/update', null, {'ref': $ticket.public_ref}) }}\" method=\"post\" class=\"block\">"},{"line":702,"text":"\t<div class=\"block-container\">"},{"line":703,"text":"\t\t<div class=\"block-body\">"},{"line":704,"text":"\t\t\t<xf:selectrow name=\"status\" value=\"{$ticket.status}\" label=\"{{ phrase('gram_support_status') }}\">"},{"line":705,"text":"\t\t\t\t<xf:foreach loop=\"$statusOptions\" value=\"$status\">"},{"line":706,"text":"\t\t\t\t\t<xf:option value=\"{$status}\">{$status}</xf:option>"},{"sep":true},{"line":748,"text":"\t\t</div>"},{"line":749,"text":"\t</div>"},{"line":750,"text":"</xf:if>"},{"line":751,"text":""},{"line":752,"text":"<xf:if is=\"$ticket.status != 'CLOSED'\">"},{"line":753,"text":"\t<xf:form action=\"{{ link('support/reply', null, {'ref': $ticket.public_ref}) }}\" method=\"post\" class=\"block\">"},{"line":754,"text":"\t\t<div class=\"block-container\">"},{"line":755,"text":"\t\t\t<div class=\"block-body\">"},{"line":756,"text":"\t\t\t\t<xf:textarearow name=\"message\" required=\"required\" rows=\"6\""},{"line":757,"text":"\t\t\t\t\tlabel=\"{{ phrase('gram_support_reply') }}\" />"},{"line":758,"text":"\t\t\t</div>"},{"line":759,"text":"\t\t\t<xf:submitrow submit=\"{{ phrase('gram_support_send') }}\" />"},{"line":760,"text":"\t\t</div>"},{"line":761,"text":"\t</xf:form>"},{"line":762,"text":"</xf:if>"},{"line":763,"text":""},{"line":764,"text":" <mail:subject>"},{"line":765,"text":"\t{{ phrase('gram_support_email_ticket_event_subject', {'public_ref': $ticket.public_ref}) }}"},{"line":766,"text":"</mail:subject>"},{"line":767,"text":""},{"line":768,"text":"{{ phrase('gram_support_email_ticket_event_body_registered', {"},{"line":769,"text":"\t'public_ref': $ticket.public_ref"},{"line":770,"text":"}) }}"},{"line":771,"text":""},{"line":772,"text":" <mail:subject>"},{"line":773,"text":"\t{{ phrase('gram_support_email_ticket_event_subject', {'public_ref': $ticket.public_ref}) }}"},{"line":774,"text":"</mail:subject>"},{"line":775,"text":""},{"line":776,"text":"{{ phrase('gram_support_email_ticket_event_body_guest', {"},{"line":777,"text":"\t'public_ref': $ticket.public_ref,"},{"line":778,"text":"\t'access_url': $accessUrl"},{"line":779,"text":"}) }}"},{"line":780,"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\":\" <xf:macro id=\\\"content\\\">\"},{\"line\":4,\"text\":\"\\t{{ phrase('gram_support_alert_ticket_event', {\"},{\"line\":5,\"text\":\"\\t\\t'public_ref': $alert.extra_data.public_ref\"},{\"line\":6,\"text\":\"\\t}) }}\"},{\"line\":7,\"text\":\"</xf:macro>\"},{\"line\":8,\"text\":\"\"},{\"line\":9,\"text\":\" .gramSupport-launcher\"},{\"line\":10,\"text\":\"{\"},{\"sep\":true},{\"line\":490,\"text\":\"\"},{\"line\":491,\"text\":\"<xf:form action=\\\"{{ link('support/create') }}\\\" method=\\\"post\\\" upload=\\\"true\\\" class=\\\"block\\\">\"},{\"line\":492,\"text\":\"\\t<div class=\\\"block-container\\\">\"},{\"line\":493,\"text\":\"\\t\\t<div class=\\\"block-body\\\">\"},{\"line\":494,\"text\":\"\\t\\t\\t<xf:if is=\\\"!$xf.visitor.user_id\\\">\"},{\"line\":495,\"text\":\"\\t\\t\\t\\t<xf:textboxrow name=\\\"email\\\" type=\\\"email\\\" required=\\\"required\\\"\"},{\"line\":496,\"text\":\"\\t\\t\\t\\t\\tlabel=\\\"{{ phrase('email') }}\\\" />\"},{\"line\":497,\"text\":\"\\t\\t\\t</xf:if>\"},{\"line\":498,\"text\":\"\\t\\t\\t<xf:selectrow name=\\\"category\\\" required=\\\"required\\\"\"},{\"line\":499,\"text\":\"\\t\\t\\t\\tlabel=\\\"{{ phrase('gram_support_category') }}\\\">\"},{\"line\":500,\"text\":\"\\t\\t\\t\\t<xf:option value=\\\"technical\\\">{{ phrase('gram_support_category_technical') }}</xf:option>\"},{\"line\":501,\"text\":\"\\t\\t\\t\\t<xf:option value=\\\"account\\\">{{ phrase('gram_support_category_account') }}</xf:option>\"},{\"sep\":true},{\"line\":507,\"text\":\"\\t\\t\\t<xf:textarearow name=\\\"message\\\" required=\\\"required\\\" rows=\\\"8\\\"\"},{\"line\":508,\"text\":\"\\t\\t\\t\\tlabel=\\\"{{ phrase('gram_support_description') }}\\\" />\"},{\"line\":509,\"text\":\"\\t\\t\\t<xf:uploadrow name=\\\"screenshot\\\" accept=\\\".gif,.jpeg,.jpg,.jpe,.png,.webp\\\"\"},{\"line\":510,\"text\":\"\\t\\t\\t\\tlabel=\\\"{{ phrase('gram_support_screenshot_optional') }}\\\" />\"},{\"line\":511,\"text\":\"\\t\\t\\t<xf:if is=\\\"!$xf.visitor.user_id\\\">\"},{\"line\":512,\"text\":\"\\t\\t\\t\\t<xf:captcharow label=\\\"{{ phrase('verification') }}\\\"\"},{\"line\":513,\"text\":\"\\t\\t\\t\\t\\thint=\\\"{{ phrase('required') }}\\\" force=\\\"true\\\" />\"},{\"line\":514,\"text\":\"\\t\\t\\t</xf:if>\"},{\"line\":515,\"text\":\"\\t\\t</div>\"},{\"line\":516,\"text\":\"\\t\\t<xf:submitrow submit=\\\"{{ phrase('gram_support_submit') }}\\\" />\"},{\"line\":517,\"text\":\"\\t</div>\"},{\"sep\":true},{\"line\":548,\"text\":\"\\t\\taria-label=\\\"{{ phrase('gram_support_open_support') }}\\\">\"},{\"line\":549,\"text\":\"\\t\\t{{ phrase('gram_support_open_support') }}\"},{\"line\":550,\"text\":\"\\t</a>\"},{\"line\":551,\"text\":\"</xf:if>\"},{\"line\":552,\"text\":\"\"},{\"line\":553,\"text\":\" <xf:title>{{ phrase('gram_support_staff_queue') }}</xf:title>\"},{\"line\":554,\"text\":\"<xf:css src=\\\"gram_support.less\\\" />\"},{\"line\":555,\"text\":\"<span class=\\\"js-gramSupportPwaConfig\\\" hidden\"},{\"line\":556,\"text\":\"\\tdata-manifest-url=\\\"{{ base_url('js/gram/support/pwa/manifest.webmanifest') }}\\\"\"},{\"line\":557,\"text\":\"\\tdata-sw-url=\\\"{{ link('support/service-worker') }}\\\"></span>\"},{\"line\":558,\"text\":\"<xf:js addon=\\\"Gram/Support\\\" src=\\\"gram/support/staff-pwa.js\\\" min=\\\"1\\\" />\"},{\"line\":559,\"text\":\"\"},{\"line\":560,\"text\":\"<div class=\\\"block\\\">\"},{\"line\":561,\"text\":\"\\t<div class=\\\"block-container\\\">\"},{\"line\":562,\"text\":\"\\t\\t<div class=\\\"block-body\\\">\"},{\"line\":563,\"text\":\"\\t\\t\\t<xf:foreach loop=\\\"$tickets\\\" value=\\\"$ticket\\\">\"},{\"line\":564,\"text\":\"\\t\\t\\t\\t<div class=\\\"block-row\\\">\"},{\"line\":565,\"text\":\"\\t\\t\\t\\t\\t<a href=\\\"{{ link('support/staff/ticket', null, {'ref': $ticket.public_ref}) }}\\\">\"},{\"line\":566,\"text\":\"\\t\\t\\t\\t\\t\\t{{ $ticket.public_ref }} \\u2014 {{ $ticket.subject }}\"},{\"line\":567,\"text\":\"\\t\\t\\t\\t\\t</a>\"},{\"line\":568,\"text\":\"\\t\\t\\t\\t\\t<span class=\\\"u-muted\\\">{{ $ticket.status }}</span>\"},{\"line\":569,\"text\":\"\\t\\t\\t\\t</div>\"},{\"line\":570,\"text\":\"\\t\\t\\t</xf:foreach>\"},{\"line\":571,\"text\":\"\\t\\t</div>\"},{\"line\":572,\"text\":\"\\t</div>\"},{\"line\":573,\"text\":\"</div>\"},{\"line\":574,\"text\":\"\"},{\"line\":575,\"text\":\" 'use strict';\"},{\"line\":576,\"text\":\"\"},{\"line\":577,\"text\":\"const OFFLINE_URL = '{{ base_url('js/gram/support/pwa/offline.html')|escape('js') }}';\"},{\"line\":578,\"text\":\"const STATIC_CACHE = 'gram-support-staff-static-v1';\"},{\"line\":579,\"text\":\"\"},{\"line\":580,\"text\":\"self.addEventListener('install', event => {\"},{\"line\":581,\"text\":\"\\tevent.waitUntil(\"},{\"line\":582,\"text\":\"\\t\\tcaches.open(STATIC_CACHE)\"},{\"line\":583,\"text\":\"\\t\\t\\t.then(cache => cache.addAll([OFFLINE_URL]))\"},{\"sep\":true},{\"line\":587,\"text\":\"\"},{\"line\":588,\"text\":\"self.addEventListener('activate', event => {\"},{\"line\":589,\"text\":\"\\tevent.waitUntil(\"},{\"line\":590,\"text\":\"\\t\\tcaches.keys().then(keys =>\"},{\"line\":591,\"text\":\"\\t\\t\\tPromise.all(keys\"},{\"line\":592,\"text\":\"\\t\\t\\t\\t.filter(key => key.startsWith('gram-support-staff-static-') && key !== STATIC_CACHE)\"},{\"line\":593,\"text\":\"\\t\\t\\t\\t.map(key => caches.delete(key))\"},{\"line\":594,\"text\":\"\\t\\t\\t)\"},{\"line\":595,\"text\":\"\\t\\t).then(() => self.clients.claim())\"},{\"line\":596,\"text\":\"\\t);\"},{\"line\":597,\"text\":\"});\"},{\"sep\":true},{\"line\":607,\"text\":\"\\t\\treturn;\"},{\"line\":608,\"text\":\"\\t}\"},{\"line\":609,\"text\":\"\"},{\"line\":610,\"text\":\"\\t// SECURITY: all support/ticket/account/admin-like content is network-only.\"},{\"line\":611,\"text\":\"\\t// Never cache HTML/API responses that can contain ticket data, identities,\"},{\"line\":612,\"text\":\"\\t// access state, internal notes, attachments or staff-only information.\"},{\"line\":613,\"text\":\"\\tconst sensitive =\"},{\"line\":614,\"text\":\"\\t\\turl.pathname.startsWith('/support/') ||\"},{\"line\":615,\"text\":\"\\t\\turl.pathname.startsWith('/account/') ||\"},{\"line\":616,\"text\":\"\\t\\turl.pathname.startsWith('/admin.php') ||\"},{\"line\":617,\"text\":\"\\t\\trequest.headers.get('accept')?.includes('text/html');\"},{\"sep\":true},{\"line\":642,\"text\":\"\\t\\t\\t})\"},{\"line\":643,\"text\":\"\\t\\t)\"},{\"line\":644,\"text\":\"\\t);\"},{\"line\":645,\"text\":\"});\"},{\"line\":646,\"text\":\"\"},{\"line\":647,\"text\":\" <xf:title>{{ phrase('gram_support_staff_ticket') }} {{ $ticket.public_ref }}</xf:title>\"},{\"line\":648,\"text\":\"<xf:css src=\\\"gram_support.less\\\" />\"},{\"line\":649,\"text\":\"<span class=\\\"js-gramSupportPwaConfig\\\" hidden\"},{\"line\":650,\"text\":\"\\tdata-manifest-url=\\\"{{ base_url('js/gram/support/pwa/manifest.webmanifest') }}\\\"\"},{\"line\":651,\"text\":\"\\tdata-sw-url=\\\"{{ link('support/service-worker') }}\\\"></span>\"},{\"line\":652,\"text\":\"<xf:js addon=\\\"Gram/Support\\\" src=\\\"gram/support/staff-pwa.js\\\" min=\\\"1\\\" />\"},{\"line\":653,\"text\":\"\"},{\"line\":654,\"text\":\"<div class=\\\"block\\\">\"},{\"line\":655,\"text\":\"\\t<div class=\\\"block-container\\\">\"},{\"line\":656,\"text\":\"\\t\\t<h2 class=\\\"block-header\\\">{{ $ticket.public_ref }} \\u2014 {{ $ticket.subject }}</h2>\"},{\"line\":657,\"text\":\"\\t\\t<div class=\\\"block-body\\\">\"},{\"sep\":true},{\"line\":682,\"text\":\"\\t\\t\\t</div>\"},{\"line\":683,\"text\":\"\\t\\t</div>\"},{\"line\":684,\"text\":\"\\t</div>\"},{\"line\":685,\"text\":\"</xf:if>\"},{\"line\":686,\"text\":\"\"},{\"line\":687,\"text\":\"<xf:form action=\\\"{{ link('support/staff/reply', null, {'ref': $ticket.public_ref}) }}\\\" method=\\\"post\\\" class=\\\"block\\\">\"},{\"line\":688,\"text\":\"\\t<div class=\\\"block-container\\\">\"},{\"line\":689,\"text\":\"\\t\\t<div class=\\\"block-body\\\">\"},{\"line\":690,\"text\":\"\\t\\t\\t<xf:textarearow name=\\\"message\\\" required=\\\"required\\\" rows=\\\"6\\\" label=\\\"{{ phrase('gram_support_reply') }}\\\" />\"},{\"line\":691,\"text\":\"\\t\\t\\t<xf:if is=\\\"$xf.visitor.hasPermission('gramSupport', 'internalNotes')\\\">\"},{\"line\":692,\"text\":\"\\t\\t\\t\\t<xf:checkboxrow>\"},{\"line\":693,\"text\":\"\\t\\t\\t\\t\\t<xf:option name=\\\"is_internal\\\" value=\\\"1\\\">{{ phrase('gram_support_internal_note') }}</xf:option>\"},{\"line\":694,\"text\":\"\\t\\t\\t\\t</xf:checkboxrow>\"},{\"line\":695,\"text\":\"\\t\\t\\t</xf:if>\"},{\"line\":696,\"text\":\"\\t\\t</div>\"},{\"line\":697,\"text\":\"\\t\\t<xf:submitrow submit=\\\"{{ phrase('gram_support_send') }}\\\" />\"},{\"line\":698,\"text\":\"\\t</div>\"},{\"line\":699,\"text\":\"</xf:form>\"},{\"line\":700,\"text\":\"\"},{\"line\":701,\"text\":\"<xf:form action=\\\"{{ link('support/staff/update', null, {'ref': $ticket.public_ref}) }}\\\" method=\\\"post\\\" class=\\\"block\\\">\"},{\"line\":702,\"text\":\"\\t<div class=\\\"block-container\\\">\"},{\"line\":703,\"text\":\"\\t\\t<div class=\\\"block-body\\\">\"},{\"line\":704,\"text\":\"\\t\\t\\t<xf:selectrow name=\\\"status\\\" value=\\\"{$ticket.status}\\\" label=\\\"{{ phrase('gram_support_status') }}\\\">\"},{\"line\":705,\"text\":\"\\t\\t\\t\\t<xf:foreach loop=\\\"$statusOptions\\\" value=\\\"$status\\\">\"},{\"line\":706,\"text\":\"\\t\\t\\t\\t\\t<xf:option value=\\\"{$status}\\\">{$status}</xf:option>\"},{\"sep\":true},{\"line\":748,\"text\":\"\\t\\t</div>\"},{\"line\":749,\"text\":\"\\t</div>\"},{\"line\":750,\"text\":\"</xf:if>\"},{\"line\":751,\"text\":\"\"},{\"line\":752,\"text\":\"<xf:if is=\\\"$ticket.status != 'CLOSED'\\\">\"},{\"line\":753,\"text\":\"\\t<xf:form action=\\\"{{ link('support/reply', null, {'ref': $ticket.public_ref}) }}\\\" method=\\\"post\\\" class=\\\"block\\\">\"},{\"line\":754,\"text\":\"\\t\\t<div class=\\\"block-container\\\">\"},{\"line\":755,\"text\":\"\\t\\t\\t<div class=\\\"block-body\\\">\"},{\"line\":756,\"text\":\"\\t\\t\\t\\t<xf:textarearow name=\\\"message\\\" required=\\\"required\\\" rows=\\\"6\\\"\"},{\"line\":757,\"text\":\"\\t\\t\\t\\t\\tlabel=\\\"{{ phrase('gram_support_reply') }}\\\" />\"},{\"line\":758,\"text\":\"\\t\\t\\t</div>\"},{\"line\":759,\"text\":\"\\t\\t\\t<xf:submitrow submit=\\\"{{ phrase('gram_support_send') }}\\\" />\"},{\"line\":760,\"text\":\"\\t\\t</div>\"},{\"line\":761,\"text\":\"\\t</xf:form>\"},{\"line\":762,\"text\":\"</xf:if>\"},{\"line\":763,\"text\":\"\"},{\"line\":764,\"text\":\" <mail:subject>\"},{\"line\":765,\"text\":\"\\t{{ phrase('gram_support_email_ticket_event_subject', {'public_ref': $ticket.public_ref}) }}\"},{\"line\":766,\"text\":\"</mail:subject>\"},{\"line\":767,\"text\":\"\"},{\"line\":768,\"text\":\"{{ phrase('gram_support_email_ticket_event_body_registered', {\"},{\"line\":769,\"text\":\"\\t'public_ref': $ticket.public_ref\"},{\"line\":770,\"text\":\"}) }}\"},{\"line\":771,\"text\":\"\"},{\"line\":772,\"text\":\" <mail:subject>\"},{\"line\":773,\"text\":\"\\t{{ phrase('gram_support_email_ticket_event_subject', {'public_ref': $ticket.public_ref}) }}\"},{\"line\":774,\"text\":\"</mail:subject>\"},{\"line\":775,\"text\":\"\"},{\"line\":776,\"text\":\"{{ phrase('gram_support_email_ticket_event_body_guest', {\"},{\"line\":777,\"text\":\"\\t'public_ref': $ticket.public_ref,\"},{\"line\":778,\"text\":\"\\t'access_url': $accessUrl\"},{\"line\":779,\"text\":\"}) }}\"},{\"line\":780,\"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":" <xf:macro id=\"content\">"},{"line":4,"text":"\t{{ phrase('gram_support_alert_ticket_event', {"},{"line":5,"text":"\t\t'public_ref': $alert.extra_data.public_ref"},{"line":6,"text":"\t}) }}"},{"line":7,"text":"</xf:macro>"},{"line":8,"text":""},{"line":9,"text":" .gramSupport-launcher"},{"line":10,"text":"{"},{"sep":true},{"line":490,"text":""},{"line":491,"text":"<xf:form action=\"{{ link('support/create') }}\" method=\"post\" upload=\"true\" class=\"block\">"},{"line":492,"text":"\t<div class=\"block-container\">"},{"line":493,"text":"\t\t<div class=\"block-body\">"},{"line":494,"text":"\t\t\t<xf:if is=\"!$xf.visitor.user_id\">"},{"line":495,"text":"\t\t\t\t<xf:textboxrow name=\"email\" type=\"email\" required=\"required\""},{"line":496,"text":"\t\t\t\t\tlabel=\"{{ phrase('email') }}\" />"},{"line":497,"text":"\t\t\t</xf:if>"},{"line":498,"text":"\t\t\t<xf:selectrow name=\"category\" required=\"required\""},{"line":499,"text":"\t\t\t\tlabel=\"{{ phrase('gram_support_category') }}\">"},{"line":500,"text":"\t\t\t\t<xf:option value=\"technical\">{{ phrase('gram_support_category_technical') }}</xf:option>"},{"line":501,"text":"\t\t\t\t<xf:option value=\"account\">{{ phrase('gram_support_category_account') }}</xf:option>"},{"sep":true},{"line":507,"text":"\t\t\t<xf:textarearow name=\"message\" required=\"required\" rows=\"8\""},{"line":508,"text":"\t\t\t\tlabel=\"{{ phrase('gram_support_description') }}\" />"},{"line":509,"text":"\t\t\t<xf:uploadrow name=\"screenshot\" accept=\".gif,.jpeg,.jpg,.jpe,.png,.webp\""},{"line":510,"text":"\t\t\t\tlabel=\"{{ phrase('gram_support_screenshot_optional') }}\" />"},{"line":511,"text":"\t\t\t<xf:if is=\"!$xf.visitor.user_id\">"},{"line":512,"text":"\t\t\t\t<xf:captcharow label=\"{{ phrase('verification') }}\""},{"line":513,"text":"\t\t\t\t\thint=\"{{ phrase('required') }}\" force=\"true\" />"},{"line":514,"text":"\t\t\t</xf:if>"},{"line":515,"text":"\t\t</div>"},{"line":516,"text":"\t\t<xf:submitrow submit=\"{{ phrase('gram_support_submit') }}\" />"},{"line":517,"text":"\t</div>"},{"sep":true},{"line":548,"text":"\t\taria-label=\"{{ phrase('gram_support_open_support') }}\">"},{"line":549,"text":"\t\t{{ phrase('gram_support_open_support') }}"},{"line":550,"text":"\t</a>"},{"line":551,"text":"</xf:if>"},{"line":552,"text":""},{"line":553,"text":" <xf:title>{{ phrase('gram_support_staff_queue') }}</xf:title>"},{"line":554,"text":"<xf:css src=\"gram_support.less\" />"},{"line":555,"text":"<span class=\"js-gramSupportPwaConfig\" hidden"},{"line":556,"text":"\tdata-manifest-url=\"{{ base_url('js/gram/support/pwa/manifest.webmanifest') }}\""},{"line":557,"text":"\tdata-sw-url=\"{{ link('support/service-worker') }}\"></span>"},{"line":558,"text":"<xf:js addon=\"Gram/Support\" src=\"gram/support/staff-pwa.js\" min=\"1\" />"},{"line":559,"text":""},{"line":560,"text":"<div class=\"block\">"},{"line":561,"text":"\t<div class=\"block-container\">"},{"line":562,"text":"\t\t<div class=\"block-body\">"},{"line":563,"text":"\t\t\t<xf:foreach loop=\"$tickets\" value=\"$ticket\">"},{"line":564,"text":"\t\t\t\t<div class=\"block-row\">"},{"line":565,"text":"\t\t\t\t\t<a href=\"{{ link('support/staff/ticket', null, {'ref': $ticket.public_ref}) }}\">"},{"line":566,"text":"\t\t\t\t\t\t{{ $ticket.public_ref }} \u2014 {{ $ticket.subject }}"},{"line":567,"text":"\t\t\t\t\t</a>"},{"line":568,"text":"\t\t\t\t\t<span class=\"u-muted\">{{ $ticket.status }}</span>"},{"line":569,"text":"\t\t\t\t</div>"},{"line":570,"text":"\t\t\t</xf:foreach>"},{"line":571,"text":"\t\t</div>"},{"line":572,"text":"\t</div>"},{"line":573,"text":"</div>"},{"line":574,"text":""},{"line":575,"text":" 'use strict';"},{"line":576,"text":""},{"line":577,"text":"const OFFLINE_URL = '{{ base_url('js/gram/support/pwa/offline.html')|escape('js') }}';"},{"line":578,"text":"const STATIC_CACHE = 'gram-support-staff-static-v1';"},{"line":579,"text":""},{"line":580,"text":"self.addEventListener('install', event => {"},{"line":581,"text":"\tevent.waitUntil("},{"line":582,"text":"\t\tcaches.open(STATIC_CACHE)"},{"line":583,"text":"\t\t\t.then(cache => cache.addAll([OFFLINE_URL]))"},{"sep":true},{"line":587,"text":""},{"line":588,"text":"self.addEventListener('activate', event => {"},{"line":589,"text":"\tevent.waitUntil("},{"line":590,"text":"\t\tcaches.keys().then(keys =>"},{"line":591,"text":"\t\t\tPromise.all(keys"},{"line":592,"text":"\t\t\t\t.filter(key => key.startsWith('gram-support-staff-static-') && key !== STATIC_CACHE)"},{"line":593,"text":"\t\t\t\t.map(key => caches.delete(key))"},{"line":594,"text":"\t\t\t)"},{"line":595,"text":"\t\t).then(() => self.clients.claim())"},{"line":596,"text":"\t);"},{"line":597,"text":"});"},{"sep":true},{"line":607,"text":"\t\treturn;"},{"line":608,"text":"\t}"},{"line":609,"text":""},{"line":610,"text":"\t// SECURITY: all support/ticket/account/admin-like content is network-only."},{"line":611,"text":"\t// Never cache HTML/API responses that can contain ticket data, identities,"},{"line":612,"text":"\t// access state, internal notes, attachments or staff-only information."},{"line":613,"text":"\tconst sensitive ="},{"line":614,"text":"\t\turl.pathname.startsWith('/support/') ||"},{"line":615,"text":"\t\turl.pathname.startsWith('/account/') ||"},{"line":616,"text":"\t\turl.pathname.startsWith('/admin.php') ||"},{"line":617,"text":"\t\trequest.headers.get('accept')?.includes('text/html');"},{"sep":true},{"line":642,"text":"\t\t\t})"},{"line":643,"text":"\t\t)"},{"line":644,"text":"\t);"},{"line":645,"text":"});"},{"line":646,"text":""},{"line":647,"text":" <xf:title>{{ phrase('gram_support_staff_ticket') }} {{ $ticket.public_ref }}</xf:title>"},{"line":648,"text":"<xf:css src=\"gram_support.less\" />"},{"line":649,"text":"<span class=\"js-gramSupportPwaConfig\" hidden"},{"line":650,"text":"\tdata-manifest-url=\"{{ base_url('js/gram/support/pwa/manifest.webmanifest') }}\""},{"line":651,"text":"\tdata-sw-url=\"{{ link('support/service-worker') }}\"></span>"},{"line":652,"text":"<xf:js addon=\"Gram/Support\" src=\"gram/support/staff-pwa.js\" min=\"1\" />"},{"line":653,"text":""},{"line":654,"text":"<div class=\"block\">"},{"line":655,"text":"\t<div class=\"block-container\">"},{"line":656,"text":"\t\t<h2 class=\"block-header\">{{ $ticket.public_ref }} \u2014 {{ $ticket.subject }}</h2>"},{"line":657,"text":"\t\t<div class=\"block-body\">"},{"sep":true},{"line":682,"text":"\t\t\t</div>"},{"line":683,"text":"\t\t</div>"},{"line":684,"text":"\t</div>"},{"line":685,"text":"</xf:if>"},{"line":686,"text":""},{"line":687,"text":"<xf:form action=\"{{ link('support/staff/reply', null, {'ref': $ticket.public_ref}) }}\" method=\"post\" class=\"block\">"},{"line":688,"text":"\t<div class=\"block-container\">"},{"line":689,"text":"\t\t<div class=\"block-body\">"},{"line":690,"text":"\t\t\t<xf:textarearow name=\"message\" required=\"required\" rows=\"6\" label=\"{{ phrase('gram_support_reply') }}\" />"},{"line":691,"text":"\t\t\t<xf:if is=\"$xf.visitor.hasPermission('gramSupport', 'internalNotes')\">"},{"line":692,"text":"\t\t\t\t<xf:checkboxrow>"},{"line":693,"text":"\t\t\t\t\t<xf:option name=\"is_internal\" value=\"1\">{{ phrase('gram_support_internal_note') }}</xf:option>"},{"line":694,"text":"\t\t\t\t</xf:checkboxrow>"},{"line":695,"text":"\t\t\t</xf:if>"},{"line":696,"text":"\t\t</div>"},{"line":697,"text":"\t\t<xf:submitrow submit=\"{{ phrase('gram_support_send') }}\" />"},{"line":698,"text":"\t</div>"},{"line":699,"text":"</xf:form>"},{"line":700,"text":""},{"line":701,"text":"<xf:form action=\"{{ link('support/staff/update', null, {'ref': $ticket.public_ref}) }}\" method=\"post\" class=\"block\">"},{"line":702,"text":"\t<div class=\"block-container\">"},{"line":703,"text":"\t\t<div class=\"block-body\">"},{"line":704,"text":"\t\t\t<xf:selectrow name=\"status\" value=\"{$ticket.status}\" label=\"{{ phrase('gram_support_status') }}\">"},{"line":705,"text":"\t\t\t\t<xf:foreach loop=\"$statusOptions\" value=\"$status\">"},{"line":706,"text":"\t\t\t\t\t<xf:option value=\"{$status}\">{$status}</xf:option>"},{"sep":true},{"line":748,"text":"\t\t</div>"},{"line":749,"text":"\t</div>"},{"line":750,"text":"</xf:if>"},{"line":751,"text":""},{"line":752,"text":"<xf:if is=\"$ticket.status != 'CLOSED'\">"},{"line":753,"text":"\t<xf:form action=\"{{ link('support/reply', null, {'ref': $ticket.public_ref}) }}\" method=\"post\" class=\"block\">"},{"line":754,"text":"\t\t<div class=\"block-container\">"},{"line":755,"text":"\t\t\t<div class=\"block-body\">"},{"line":756,"text":"\t\t\t\t<xf:textarearow name=\"message\" required=\"required\" rows=\"6\""},{"line":757,"text":"\t\t\t\t\tlabel=\"{{ phrase('gram_support_reply') }}\" />"},{"line":758,"text":"\t\t\t</div>"},{"line":759,"text":"\t\t\t<xf:submitrow submit=\"{{ phrase('gram_support_send') }}\" />"},{"line":760,"text":"\t\t</div>"},{"line":761,"text":"\t</xf:form>"},{"line":762,"text":"</xf:if>"},{"line":763,"text":""},{"line":764,"text":" <mail:subject>"},{"line":765,"text":"\t{{ phrase('gram_support_email_ticket_event_subject', {'public_ref': $ticket.public_ref}) }}"},{"line":766,"text":"</mail:subject>"},{"line":767,"text":""},{"line":768,"text":"{{ phrase('gram_support_email_ticket_event_body_registered', {"},{"line":769,"text":"\t'public_ref': $ticket.public_ref"},{"line":770,"text":"}) }}"},{"line":771,"text":""},{"line":772,"text":" <mail:subject>"},{"line":773,"text":"\t{{ phrase('gram_support_email_ticket_event_subject', {'public_ref': $ticket.public_ref}) }}"},{"line":774,"text":"</mail:subject>"},{"line":775,"text":""},{"line":776,"text":"{{ phrase('gram_support_email_ticket_event_body_guest', {"},{"line":777,"text":"\t'public_ref': $ticket.public_ref,"},{"line":778,"text":"\t'access_url': $accessUrl"},{"line":779,"text":"}) }}"},{"line":780,"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