Skip to content

Commit 926f1b9

Browse files
authored
Resolve relative oEmbed endpoints (#88)
1 parent 2e06df5 commit 926f1b9

3 files changed

Lines changed: 77 additions & 3 deletions

File tree

‎docs/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,8 @@ $response = $discovery->discover($url, maxWidth: 640, maxHeight: 480);
479479
$response = $discovery->fetch('https://example.com/oembed?url=...');
480480
```
481481

482+
Discovered oEmbed endpoint URLs may be absolute, protocol-relative, root-relative, or path-relative. Relative endpoint URLs are resolved against the source page URL before they are fetched.
483+
482484
The `OEmbedResponse` provides typed access to all oEmbed fields:
483485

484486
```php

‎src/OEmbed/OEmbedDiscovery.php‎

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public function discoverEndpoint(string $url): ?string {
6565
return null;
6666
}
6767

68-
return $this->parseOEmbedLink($html);
68+
return $this->parseOEmbedLink($html, $url);
6969
}
7070

7171
/**
@@ -109,9 +109,10 @@ public function fetch(string $endpointUrl, ?int $maxWidth = null, ?int $maxHeigh
109109
* Looks for: <link rel="alternate" type="application/json+oembed" href="..." />
110110
*
111111
* @param string $html The HTML to parse.
112+
* @param string $baseUrl Source page URL.
112113
* @return string|null The oEmbed URL or null if not found.
113114
*/
114-
private function parseOEmbedLink(string $html): ?string {
115+
private function parseOEmbedLink(string $html, string $baseUrl): ?string {
115116
// Match link tags with oembed type
116117
$pattern = '/<link[^>]+type=["\']application\/json\+oembed["\'][^>]*>/i';
117118
if (!preg_match($pattern, $html, $match)) {
@@ -132,7 +133,45 @@ private function parseOEmbedLink(string $html): ?string {
132133
$href = $hrefMatch[1];
133134

134135
// Decode HTML entities
135-
return html_entity_decode($href, ENT_QUOTES | ENT_HTML5);
136+
$href = html_entity_decode($href, ENT_QUOTES | ENT_HTML5);
137+
138+
return $this->resolveEndpointUrl($href, $baseUrl);
139+
}
140+
141+
/**
142+
* Resolve oEmbed href values against the source page URL.
143+
*
144+
* @param string $href Link href from the oEmbed tag.
145+
* @param string $baseUrl Source page URL.
146+
* @return string
147+
*/
148+
private function resolveEndpointUrl(string $href, string $baseUrl): string {
149+
if (parse_url($href, PHP_URL_SCHEME) !== null) {
150+
return $href;
151+
}
152+
153+
$base = parse_url($baseUrl);
154+
if (empty($base['scheme']) || empty($base['host'])) {
155+
return $href;
156+
}
157+
158+
$authority = $base['scheme'] . '://' . $base['host'];
159+
if (!empty($base['port'])) {
160+
$authority .= ':' . $base['port'];
161+
}
162+
163+
if (str_starts_with($href, '//')) {
164+
return $base['scheme'] . ':' . $href;
165+
}
166+
167+
if (str_starts_with($href, '/')) {
168+
return $authority . $href;
169+
}
170+
171+
$path = $base['path'] ?? '/';
172+
$directory = rtrim(substr($path, 0, (int)strrpos($path, '/') + 1), '/');
173+
174+
return $authority . $directory . '/' . $href;
136175
}
137176

138177
}

‎tests/OEmbed/OEmbedTest.php‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,39 @@ public function testOEmbedDiscoveryParseEndpoint(): void {
8282
$this->assertSame('https://example.com/oembed?url=test', $endpoint);
8383
}
8484

85+
public function testOEmbedDiscoveryResolvesRootRelativeEndpoint(): void {
86+
$mockClient = $this->createStub(HttpClientInterface::class);
87+
$mockClient->method('get')
88+
->willReturn('<html><head><link rel="alternate" type="application/json+oembed" href="/oembed?url=test" /></head></html>');
89+
90+
$discovery = new OEmbedDiscovery($mockClient);
91+
$endpoint = $discovery->discoverEndpoint('https://example.com/video/123');
92+
93+
$this->assertSame('https://example.com/oembed?url=test', $endpoint);
94+
}
95+
96+
public function testOEmbedDiscoveryResolvesPathRelativeEndpoint(): void {
97+
$mockClient = $this->createStub(HttpClientInterface::class);
98+
$mockClient->method('get')
99+
->willReturn('<html><head><link rel="alternate" type="application/json+oembed" href="oembed?url=test" /></head></html>');
100+
101+
$discovery = new OEmbedDiscovery($mockClient);
102+
$endpoint = $discovery->discoverEndpoint('https://example.com/videos/123');
103+
104+
$this->assertSame('https://example.com/videos/oembed?url=test', $endpoint);
105+
}
106+
107+
public function testOEmbedDiscoveryResolvesProtocolRelativeEndpoint(): void {
108+
$mockClient = $this->createStub(HttpClientInterface::class);
109+
$mockClient->method('get')
110+
->willReturn('<html><head><link rel="alternate" type="application/json+oembed" href="//cdn.example.com/oembed?url=test" /></head></html>');
111+
112+
$discovery = new OEmbedDiscovery($mockClient);
113+
$endpoint = $discovery->discoverEndpoint('https://example.com/video/123');
114+
115+
$this->assertSame('https://cdn.example.com/oembed?url=test', $endpoint);
116+
}
117+
85118
public function testOEmbedDiscoveryNoEndpoint(): void {
86119
$mockClient = $this->createStub(HttpClientInterface::class);
87120
$mockClient->method('get')

0 commit comments

Comments
 (0)