Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [4.5.2] - 2026-10-27
### Added
- oEmbed responses of type `photo` without `html` now produce an `<img>` embed code from their `url`, `width` and `height` [#584]

## [4.5.1] - 2026-10-26
### Security
- JSON-LD remote contexts are never fetched. A page-supplied `@context` is not requested, so it cannot bypass the URL policy or follow redirects. schema.org contexts are resolved from a bundled vocabulary; any other remote context is skipped [#573]
Expand Down Expand Up @@ -319,6 +323,7 @@ Full library refactoring.
[#571]: https://github.com/oscarotero/Embed/issues/571
[#572]: https://github.com/oscarotero/Embed/issues/572
[#573]: https://github.com/oscarotero/Embed/issues/573
[#584]: https://github.com/php-embed/Embed/issues/584
[#574]: https://github.com/oscarotero/Embed/issues/574
[#577]: https://github.com/oscarotero/Embed/issues/577

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ $oembed->int('width'); //Return the value as integer
$oembed->url('url'); //Return the value as full url (converts relative urls to absolutes)
```

When an oEmbed response has type `photo` and no `html`, `$info->code` is built as an `<img>` tag from its `url` (with `width` and `height` when available), so photo providers like GIPHY or Flickr also return embed code.

Additional oEmbed parameters (like instagrams `hidecaption`) can also be provided:
```php
$embed = new Embed();
Expand Down
29 changes: 28 additions & 1 deletion src/Detectors/Code.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ private function detectFromEmbed(): ?EmbedCode
$html = $oembed->html('html');

if ($html === null) {
return null;
return $this->detectFromOEmbedPhoto();
}

return new EmbedCode(
Expand All @@ -48,6 +48,33 @@ private function detectFromEmbed(): ?EmbedCode
);
}

private function detectFromOEmbedPhoto(): ?EmbedCode
{
$oembed = $this->extractor->getOEmbed();

if ($oembed->str('type') !== 'photo') {
return null;
}

$url = $oembed->url('url');

if ($url === null) {
return null;
}

$width = $oembed->int('width');
$height = $oembed->int('height');

$code = html('img', [
'src' => $url,
'alt' => $oembed->str('title'),
'width' => $width,
'height' => $height,
]);

return new EmbedCode($code, $width, $height);
}

private function detectFromOpenGraph(): ?EmbedCode
{
$metas = $this->extractor->getMetas();
Expand Down
106 changes: 106 additions & 0 deletions tests/OEmbedPhotoCodeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php
declare(strict_types = 1);

namespace Embed\Tests;

use Embed\Embed;
use Embed\Http\Crawler;
use Embed\Http\UrlPolicy;
use Nyholm\Psr7\Factory\Psr17Factory;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
* oEmbed responses of type "photo" carry no "html", only "url".
*/
class OEmbedPhotoCodeTest extends TestCase
{
public function testPhotoWithoutHtmlBuildsAnImage(): void
{
$info = $this->embed([
'type' => 'photo',
'version' => '1.0',
'title' => 'Funny "cat" GIF',
'url' => 'https://media.public.test/cat.gif',
'width' => 480,
'height' => 270,
])->get('http://public.test/gifs/cat');

$code = $info->code;
$this->assertNotNull($code);
$this->assertSame(
'<img src="https://media.public.test/cat.gif" alt="Funny &quot;cat&quot; GIF" width="480" height="270" />',
$code->html
);
$this->assertSame(480, $code->width);
$this->assertSame(270, $code->height);
}

public function testPhotoWithoutSizeOmitsDimensions(): void
{
$info = $this->embed([
'type' => 'photo',
'version' => '1.0',
'url' => 'https://media.public.test/cat.gif',
])->get('http://public.test/gifs/cat');

$code = $info->code;
$this->assertNotNull($code);
$this->assertSame('<img src="https://media.public.test/cat.gif" />', $code->html);
$this->assertNull($code->width);
$this->assertNull($code->height);
}

public function testHtmlStillWinsOverPhotoUrl(): void
{
$info = $this->embed([
'type' => 'photo',
'version' => '1.0',
'url' => 'https://media.public.test/cat.gif',
'html' => '<blockquote>cat</blockquote>',
])->get('http://public.test/gifs/cat');

$code = $info->code;
$this->assertNotNull($code);
$this->assertSame('<blockquote>cat</blockquote>', $code->html);
}

public function testNonPhotoWithoutHtmlHasNoCode(): void
{
$info = $this->embed([
'type' => 'link',
'version' => '1.0',
'url' => 'https://media.public.test/cat.gif',
])->get('http://public.test/gifs/cat');

$this->assertNull($info->code);
}

/**
* @param array<string, mixed> $oembed
*/
private function embed(array $oembed): Embed
{
$json = (string) json_encode($oembed, JSON_UNESCAPED_SLASHES);
$page = '<!DOCTYPE html><html><head><title>Page title</title>'
.'<link rel="alternate" type="application/json+oembed" href="http://public.test/oembed?url=cat">'
.'</head><body></body></html>';

$client = new RecordingClient(static function (RequestInterface $request) use ($json, $page): ResponseInterface {
$factory = new Psr17Factory();
$isOEmbed = $request->getUri()->getPath() === '/oembed';

return $factory->createResponse(200)
->withHeader('Content-Type', $isOEmbed ? 'application/json' : 'text/html')
->withBody($factory->createStream($isOEmbed ? $json : $page));
});

$crawler = new Crawler($client);
$crawler->setUrlPolicy(UrlPolicy::default()->withResolver(static function (string $host): array {
return ['1.1.1.1'];
}));

return new Embed($crawler);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
'authorName' => 'pachunka',
'authorUrl' => 'https://www.deviantart.com/pachunka',
'cms' => null,
'code' => null,
'code' => [
'html' => '<img src="https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/2b5a457d-dde7-459a-badf-774e8e506025/d2enxz7-431ca9c3-987f-4b2d-ae6a-cbbdd1c0e2bf.jpg?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1cm46YXBwOjdlMGQxODg5ODIyNjQzNzNhNWYwZDQxNWVhMGQyNmUwIiwiaXNzIjoidXJuOmFwcDo3ZTBkMTg4OTgyMjY0MzczYTVmMGQ0MTVlYTBkMjZlMCIsIm9iaiI6W1t7InBhdGgiOiJcL2ZcLzJiNWE0NTdkLWRkZTctNDU5YS1iYWRmLTc3NGU4ZTUwNjAyNVwvZDJlbnh6Ny00MzFjYTljMy05ODdmLTRiMmQtYWU2YS1jYmJkZDFjMGUyYmYuanBnIn1dXSwiYXVkIjpbInVybjpzZXJ2aWNlOmZpbGUuZG93bmxvYWQiXX0.pCEF4M_tMVZI92ZKClZdcMu5UqpQ3131DrHsx1WxefY" alt="Cope" width="448" height="672" />',
'width' => 448,
'height' => 672,
'ratio' => 150.0
],
'description' => null,
'favicon' => 'https://st.deviantart.net/eclipse/icons/da_favicon_v2.ico',
'feeds' => [],
Expand Down
7 changes: 6 additions & 1 deletion tests/fixtures/www.23hq.com.zzleeper-photo-16600737.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
'authorName' => 'Zzleeper',
'authorUrl' => 'https://www.23hq.com/Zzleeper/',
'cms' => null,
'code' => null,
'code' => [
'html' => '<img src="https://www.23hq.com/16171062/16600737_ac392e8a4d667e4726fbafc8a21728d5_large.jpg" alt="På god väns inrådan kommer det även bultförband vid pilarna. Låter bra tycker jag. (Better safe than sorry)" width="756" height="567" />',
'width' => 756,
'height' => 567,
'ratio' => 75.0
],
'description' => 'På god väns inrådan kommer det även bultförband vid pilarna. Låter bra tycker jag. (Better safe than sorry)',
'favicon' => 'https://www.23hq.com/favicon.ico',
'feeds' => [],
Expand Down
7 changes: 6 additions & 1 deletion tests/fixtures/www.deviantart.com.art-misty-510056679.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
'authorName' => 'sakimichan',
'authorUrl' => 'https://www.deviantart.com/sakimichan',
'cms' => null,
'code' => null,
'code' => [
'html' => '<img src="https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/9ad8fbc8-aee8-4e92-a04b-6abf7a036a51/d8foa93-8e1fade9-1853-4644-af82-2ec21590bf94.jpg?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1cm46YXBwOjdlMGQxODg5ODIyNjQzNzNhNWYwZDQxNWVhMGQyNmUwIiwiaXNzIjoidXJuOmFwcDo3ZTBkMTg4OTgyMjY0MzczYTVmMGQ0MTVlYTBkMjZlMCIsIm9iaiI6W1t7InBhdGgiOiJcL2ZcLzlhZDhmYmM4LWFlZTgtNGU5Mi1hMDRiLTZhYmY3YTAzNmE1MVwvZDhmb2E5My04ZTFmYWRlOS0xODUzLTQ2NDQtYWY4Mi0yZWMyMTU5MGJmOTQuanBnIn1dXSwiYXVkIjpbInVybjpzZXJ2aWNlOmZpbGUuZG93bmxvYWQiXX0._JiXuDZWqsoAgateXxPBaC3_EPfHdri7pJCeKFhXnjA" alt="Misty" width="695" height="900" />',
'width' => 695,
'height' => 900,
'ratio' => 129.496
],
'description' => null,
'favicon' => 'https://st.deviantart.net/eclipse/icons/da_favicon_v2.ico',
'feeds' => [],
Expand Down
Loading