From b7b05ffec30683b6a668a6b1b0dfdbb0666da212 Mon Sep 17 00:00:00 2001 From: anubhav-intercom Date: Tue, 6 Oct 2026 16:26:21 +0100 Subject: [PATCH] Encode IDs when building Legacy request paths Route caller-supplied IDs through a helper that percent-encodes the value and rejects empty, "." and ".." so each ID stays a single path segment. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/Legacy/IntercomAdmins.php | 2 +- src/Legacy/IntercomCompanies.php | 8 +-- src/Legacy/IntercomContacts.php | 2 +- src/Legacy/IntercomConversations.php | 4 +- src/Legacy/IntercomLeads.php | 2 +- src/Legacy/IntercomNotes.php | 2 +- src/Legacy/IntercomResource.php | 16 ++++++ src/Legacy/IntercomSegments.php | 2 +- src/Legacy/IntercomTeams.php | 2 +- src/Legacy/IntercomUsers.php | 2 +- src/Legacy/IntercomVisitors.php | 2 +- tests/Legacy/IntercomResourcePathTest.php | 65 +++++++++++++++++++++++ 12 files changed, 95 insertions(+), 14 deletions(-) create mode 100644 tests/Legacy/IntercomResourcePathTest.php diff --git a/src/Legacy/IntercomAdmins.php b/src/Legacy/IntercomAdmins.php index fc2b977e..24177392 100644 --- a/src/Legacy/IntercomAdmins.php +++ b/src/Legacy/IntercomAdmins.php @@ -43,6 +43,6 @@ public function getAdmin($id, $options = []) */ public function adminPath($id) { - return 'admins/' . $id; + return 'admins/' . self::pathSegment($id); } } diff --git a/src/Legacy/IntercomCompanies.php b/src/Legacy/IntercomCompanies.php index 97290285..9da6088f 100644 --- a/src/Legacy/IntercomCompanies.php +++ b/src/Legacy/IntercomCompanies.php @@ -116,7 +116,7 @@ public function getCompanyUsers($id, $options = []) */ public function companyPath($id) { - return 'companies/' . $id; + return 'companies/' . self::pathSegment($id); } /** @@ -125,7 +125,7 @@ public function companyPath($id) */ public function companyUsersPath($id) { - return 'companies/' . $id . '/users'; + return 'companies/' . self::pathSegment($id) . '/users'; } /** @@ -134,7 +134,7 @@ public function companyUsersPath($id) */ public function companyAttachPath(string $contactId) { - return 'contacts/' . $contactId . '/companies'; + return 'contacts/' . self::pathSegment($contactId) . '/companies'; } /** @@ -144,6 +144,6 @@ public function companyAttachPath(string $contactId) */ public function companyDetachPath(string $contactId, string $companyId) { - return 'contacts/' . $contactId . '/companies/' . $companyId; + return 'contacts/' . self::pathSegment($contactId) . '/companies/' . self::pathSegment($companyId); } } diff --git a/src/Legacy/IntercomContacts.php b/src/Legacy/IntercomContacts.php index 1f0de82e..8946d7a4 100644 --- a/src/Legacy/IntercomContacts.php +++ b/src/Legacy/IntercomContacts.php @@ -128,6 +128,6 @@ public function nextCursor($pages) */ public function contactPath(string $id) { - return 'contacts/' . $id; + return 'contacts/' . self::pathSegment($id); } } diff --git a/src/Legacy/IntercomConversations.php b/src/Legacy/IntercomConversations.php index e5f6de4a..a78403ad 100644 --- a/src/Legacy/IntercomConversations.php +++ b/src/Legacy/IntercomConversations.php @@ -129,7 +129,7 @@ public function markConversationAsRead($id) */ public function conversationPath($id) { - return 'conversations/' . $id; + return 'conversations/' . self::pathSegment($id); } /** @@ -140,6 +140,6 @@ public function conversationPath($id) */ public function conversationReplyPath($id) { - return 'conversations/' . $id . '/reply'; + return 'conversations/' . self::pathSegment($id) . '/reply'; } } diff --git a/src/Legacy/IntercomLeads.php b/src/Legacy/IntercomLeads.php index 330ccd08..43135525 100644 --- a/src/Legacy/IntercomLeads.php +++ b/src/Legacy/IntercomLeads.php @@ -97,7 +97,7 @@ public function convertLead($options) */ public function leadPath($id) { - return "contacts/" . $id; + return "contacts/" . self::pathSegment($id); } /** diff --git a/src/Legacy/IntercomNotes.php b/src/Legacy/IntercomNotes.php index f3f8dd42..5745ea35 100644 --- a/src/Legacy/IntercomNotes.php +++ b/src/Legacy/IntercomNotes.php @@ -43,6 +43,6 @@ public function getNotes($options) */ public function getNote($id) { - return $this->client->get("notes/" . $id, []); + return $this->client->get("notes/" . self::pathSegment($id), []); } } diff --git a/src/Legacy/IntercomResource.php b/src/Legacy/IntercomResource.php index 5895ebfd..ae4ac11d 100644 --- a/src/Legacy/IntercomResource.php +++ b/src/Legacy/IntercomResource.php @@ -18,4 +18,20 @@ public function __construct(IntercomClient $client) { $this->client = $client; } + + /** + * Returns $id as a single URL path segment. + * + * @param string|int $id + * @return string + * @throws \InvalidArgumentException if the value is empty, "." or ".." + */ + protected static function pathSegment($id) + { + $id = (string) $id; + if ($id === "" || $id === "." || $id === "..") { + throw new \InvalidArgumentException("Invalid ID for request path."); + } + return rawurlencode($id); + } } diff --git a/src/Legacy/IntercomSegments.php b/src/Legacy/IntercomSegments.php index 458fa160..c4f7d5fd 100644 --- a/src/Legacy/IntercomSegments.php +++ b/src/Legacy/IntercomSegments.php @@ -18,7 +18,7 @@ class IntercomSegments extends IntercomResource */ public function getSegment($id, array $options = []) { - return $this->client->get('segments/' . $id, $options); + return $this->client->get('segments/' . self::pathSegment($id), $options); } /** diff --git a/src/Legacy/IntercomTeams.php b/src/Legacy/IntercomTeams.php index ed35060f..b945b4ba 100644 --- a/src/Legacy/IntercomTeams.php +++ b/src/Legacy/IntercomTeams.php @@ -43,6 +43,6 @@ public function getTeam($id, $options = []) */ public function teamPath($id) { - return 'teams/' . $id; + return 'teams/' . self::pathSegment($id); } } diff --git a/src/Legacy/IntercomUsers.php b/src/Legacy/IntercomUsers.php index 27659673..13f1cb95 100644 --- a/src/Legacy/IntercomUsers.php +++ b/src/Legacy/IntercomUsers.php @@ -124,6 +124,6 @@ public function permanentlyDeleteUser(string $id) */ public function userPath(string $id) { - return 'users/' . $id; + return 'users/' . self::pathSegment($id); } } diff --git a/src/Legacy/IntercomVisitors.php b/src/Legacy/IntercomVisitors.php index d44f493f..ced4f914 100644 --- a/src/Legacy/IntercomVisitors.php +++ b/src/Legacy/IntercomVisitors.php @@ -72,6 +72,6 @@ public function convertVisitor($options) */ public function visitorPath($id) { - return "visitors/" . $id; + return "visitors/" . self::pathSegment($id); } } diff --git a/tests/Legacy/IntercomResourcePathTest.php b/tests/Legacy/IntercomResourcePathTest.php new file mode 100644 index 00000000..67bbcd78 --- /dev/null +++ b/tests/Legacy/IntercomResourcePathTest.php @@ -0,0 +1,65 @@ +mockHandler = new MockHandler(); + $httpClient = new Client(['handler' => HandlerStack::create($this->mockHandler)]); + + $this->client = new IntercomClient('test_token'); + $this->client->setHttpClient($httpClient); + } + + public function testEncodesIdInPath(): void + { + $this->mockHandler->append(new Response(200, [], json_encode([]))); + + $this->client->contacts->getContact('a/b?c#d'); + + $uri = $this->mockHandler->getLastRequest()->getUri(); + $this->assertSame('/contacts/a%2Fb%3Fc%23d', $uri->getPath()); + $this->assertSame('', $uri->getQuery()); + $this->assertSame('', $uri->getFragment()); + } + + public function testEncodesEveryIdInCompanyDetachPath(): void + { + $this->assertSame( + 'contacts/a%2Fb/companies/c%3Fd', + $this->client->companies->companyDetachPath('a/b', 'c?d') + ); + } + + public function testKeepsPlainIdUnchanged(): void + { + $this->assertSame('users/abc123', $this->client->users->userPath('abc123')); + } + + /** + * @dataProvider invalidIdProvider + */ + public function testRejectsDotSegmentId(string $id): void + { + $this->expectException(InvalidArgumentException::class); + $this->client->contacts->getContact($id); + } + + public function invalidIdProvider(): array + { + return [[''], ['.'], ['..']]; + } +}