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