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
94 changes: 38 additions & 56 deletions .github/ISSUE_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,83 +1,65 @@
# 🐛 Bug Report / 💡 Feature Request
# Bug Report

**Thanks for contributing to the oEmbed Craft CMS plugin!** This template helps us understand issues with the field type, embed functionality, caching, and admin interface.
Thanks for reporting an issue. Please fill this out so we can troubleshoot quickly.

## 📋 Issue Type
- [ ] 🐛 Bug report
- [ ] 💡 Feature request
- [ ] 📚 Documentation issue
- [ ] ❓ Question/Support
## Summary
<!-- What is going wrong? -->

---

## 🔍 **Bug Report** (Skip if feature request)

### What's the issue?
<!-- Clear description of what's wrong -->

### Where does it happen?
- [ ] **Admin CP Field**: Issue in the Craft control panel field interface
- [ ] **Frontend Render**: Problem with `{{ entry.field.render() }}` output
- [ ] **Caching**: Cached content not updating or cache errors
- [ ] **GDPR Compliance**: Issues with privacy settings (YouTube no-cookie, Vimeo DNT, etc.)
- [ ] **Network/Provider**: Provider-specific embed failures
- [ ] **GraphQL**: Issues with GraphQL field queries

### Steps to reproduce
## Steps to Reproduce
1.
2.
3.

### Expected vs Actual
**Expected:**
**Actual:**
## Expected Behavior
<!-- What did you expect to happen? -->

### Your Environment
- **Craft CMS**: <!-- e.g., 4.5.0 -->
- **oEmbed Plugin**: <!-- e.g., 3.1.5 -->
- **PHP**: <!-- e.g., 8.2 -->
- **Provider**: <!-- e.g., YouTube, Vimeo, Instagram, Twitter -->
- **Test URL**: <!-- The URL you're trying to embed -->
## Actual Behavior
<!-- What happened instead? Include error text if available. -->

---

## 💡 **Feature Request** (Skip if bug report)
## Environment
- **CraftCMS version:**
- **oEmbed plugin version:**
- **PHP version:**
- **Database (optional):** <!-- e.g., MySQL 8.0 / MariaDB 10.6 / PostgreSQL 15 -->
- **Web server (optional):** <!-- e.g., Nginx / Apache -->
- **OS (optional):**

### What feature would you like?
<!-- Clear description -->
## URL/Provider Details
- **Provider:** <!-- e.g., YouTube, Vimeo, Instagram -->
- **Example URL(s):**

### What problem does this solve?
<!-- Context about why this is needed -->
## Template / Code Used
<!-- Paste the template code or field usage, e.g. `entry.oembedField.render()` -->

### Suggested implementation
<!-- How you think it should work -->
## Logs / Errors
<!-- Paste relevant Craft logs, PHP errors, stack traces, or screenshots -->

---
## Error Messages / Screenshots
<!-- Paste exact error messages and attach screenshots from the CP or frontend where helpful -->

## 🔧 Additional Context
## Additional Context
<!-- Anything else that might help us reproduce or debug -->

### Admin CP Issues (if applicable)
- Field preview not showing?
- Save/validation problems?
- Settings interface issues?

### Frontend Issues (if applicable)
### Frontend / Template Context (if applicable)
- Template method used: `render()` / `embed()` / `media()` / `valid()`
- Cache enabled/disabled?
- Cache enabled or disabled?
- GDPR settings active?
- Is this GraphQL-related?

### Provider-Specific Issues
### Provider-Specific Notes (if applicable)
- Does the URL work on the provider's site?
- Using embed URL vs regular URL?
- API tokens configured (for Instagram/Facebook)?

### Error Messages/Screenshots
<!-- Paste any error messages or attach screenshots -->
- Using embed URL vs regular/shared URL?
- Any provider API tokens configured (for Instagram/Facebook)?

---

**💡 Pro Tips:**
- Many providers need **embed URLs** not regular URLs (check provider's share → embed option)
- Check your **cache settings** - try disabling cache temporarily to test
- For Instagram: requires Facebook API token in plugin settings
- For GDPR: check if privacy settings are affecting embeds
## Pro Tips
- Some providers require **embed URLs** instead of regular watch/share URLs.
- Try disabling cache temporarily to check whether this is cache-related.
- For Instagram, confirm Facebook API token configuration.
- For GDPR mode, check whether privacy settings are blocking the expected embed behavior.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# oEmbed Changelog

## 3.2.1 - 2026-03-03

### Fixed

- URLs without scheme (e.g., `youtube.com/watch?v=xxx`) are now automatically normalized with `https://` instead of being silently cleared on save (fixes #177)
- Fixed inconsistent return type in `normalizeValue()` when input was already an `OembedModel` - now consistently returns `OembedModel` instead of a string

### Added

- Unit tests for URL normalization in `OembedFieldTest`

## 3.2.0 - 2026-01-14

### Fixed
Expand Down Expand Up @@ -50,6 +61,7 @@
### Fixed

- Fixed DOM parsing errors by properly escaping unencoded ampersands before parsing HTML. Thanks @mofman
- Fixed issue from #178 craft\web\View::renderJsFile() in "oembed/settings"

## 3.1.5 - 2024-05-22

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "wrav/oembed",
"description": "A simple plugin to extract media information from websites, like youtube videos, twitter statuses or blog articles.",
"type": "craft-plugin",
"version": "3.2.0",
"version": "3.2.1",
"keywords": [
"craft",
"cms",
Expand Down
5 changes: 3 additions & 2 deletions src/assetbundles/oembed/dist/js/oembed.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ $('body').on('click', '.oembed-header', function () {
}
});

$('body').on('keyup blur change', 'input.oembed-field', function () {
$('body').on('input paste keyup blur change', 'input.oembed-field', function () {
var that = $(this);

if (oembedOnChangeTimeout != null) {
Expand All @@ -39,9 +39,10 @@ $('body').on('keyup blur change', 'input.oembed-field', function () {
var cpTrigger = Craft && Craft.cpTrigger ? Craft.cpTrigger : 'admin';

if(val) {
var encodedVal = encodeURIComponent(val);
$.ajax({
type: "GET",
url: "/"+cpTrigger.toString()+"/oembed/preview?url=" + val + "&options[]=",
url: "/"+cpTrigger.toString()+"/oembed/preview?url=" + encodedVal + "&options[]=",
async: true
}).done(function (res) {
var preview = that.parent().find('.oembed-preview');
Expand Down
40 changes: 37 additions & 3 deletions src/fields/OembedField.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,34 @@ public function getContentGqlType(): \GraphQL\Type\Definition\Type|array
];
}

/**
* Normalize a URL by adding https:// scheme if missing.
*
* @param string $url The URL to normalize
* @return string The normalized URL
*/
private function normalizeUrl(string $url): string
{
$url = trim($url);

if (empty($url)) {
return $url;
}

// If URL already has a scheme, return as-is
if (preg_match('#^https?://#i', $url)) {
return $url;
}

// If URL starts with //, add https:
if (str_starts_with($url, '//')) {
return 'https:' . $url;
}

// Add https:// to URLs without a scheme
return 'https://' . $url;
}

/**
* @param mixed $value The raw field value
* @param ElementInterface|null $element The element the field is associated with, if there is one
Expand All @@ -153,8 +181,9 @@ public function normalizeValue(mixed $value, ?craft\base\ElementInterface $eleme

// If an instance of `OembedModel` and URL is set, return it
if ($value instanceof OembedModel && $value->url) {
if (UrlHelper::isFullUrl($value->url)) {
return $this->value = $value->url;
$normalizedUrl = $this->normalizeUrl($value->url);
if (UrlHelper::isFullUrl($normalizedUrl)) {
return $this->value = new OembedModel($normalizedUrl);
} else {
// If we get here, something’s gone wrong
return new OembedModel(null);
Expand All @@ -170,7 +199,12 @@ public function normalizeValue(mixed $value, ?craft\base\ElementInterface $eleme
$value = ArrayHelper::getValue($value, 'url');
}

// If URL stri ng, return an instance of `OembedModel`
// Normalize URL by adding scheme if missing (fixes #177)
if (is_string($value)) {
$value = $this->normalizeUrl($value);
}

// If URL string, return an instance of `OembedModel`
if (is_string($value) && UrlHelper::isFullUrl($value)) {
return $this->value = new OembedModel($value);
}
Expand Down
26 changes: 26 additions & 0 deletions src/services/OembedService.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,28 @@ public function render($url, array $options = [], array $cacheProps = [])
return Template::raw($code);
}

/**
* Normalize a URL by adding https:// when no scheme is present.
*/
private function normalizeUrl(string $url): string
{
$url = trim($url);

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

if (preg_match('#^https?://#i', $url)) {
return $url;
}

if (str_starts_with($url, '//')) {
return 'https:' . $url;
}

return 'https://' . $url;
}

/**
* @param string $input
* @param array $options
Expand Down Expand Up @@ -555,6 +577,10 @@ public function embed($url, array $options = [], array $cacheProps = [], $factor
// Normalize null/empty URLs immediately to prevent type errors
$url = $url ?: '';

if (is_string($url)) {
$url = $this->normalizeUrl($url);
}

// Check cache first
$cacheKey = $this->generateCacheKey($url, $options, $cacheProps);
$cachedResult = $this->getCachedEmbed($cacheKey);
Expand Down
3 changes: 0 additions & 3 deletions src/templates/settings.twig
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,3 @@
name: 'facebookKey',
value: settings.facebookKey,
}) }}


{{ craft.app.view.renderJsFile('@wrav/oembed/js/settings.js') }}
96 changes: 96 additions & 0 deletions tests/unit/fields/OembedFieldTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

namespace wrav\oembed\tests\unit\fields;

use Codeception\Test\Unit;
use wrav\oembed\fields\OembedField;
use wrav\oembed\models\OembedModel;
use craft\test\TestCase;

/**
* Unit test for OembedField
*
* Tests URL normalization feature (fixes #177)
*/
class OembedFieldTest extends TestCase
{
private OembedField $field;

protected function setUp(): void
{
parent::setUp();
$this->field = new OembedField();
}

/**
* Test that URLs without scheme get https:// prepended
* @dataProvider urlNormalizationProvider
*/
public function testNormalizeValueAddsScheme(string $input, string $expectedUrl)
{
$result = $this->field->normalizeValue($input, null);

$this->assertInstanceOf(OembedModel::class, $result);
$this->assertEquals($expectedUrl, $result->url);
}

public static function urlNormalizationProvider(): array
{
return [
'youtube without scheme' => [
'youtube.com/watch?v=dQw4w9WgXcQ',
'https://youtube.com/watch?v=dQw4w9WgXcQ'
],
'youtube with www without scheme' => [
'www.youtube.com/watch?v=dQw4w9WgXcQ',
'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
],
'youtube with https scheme' => [
'https://youtube.com/watch?v=dQw4w9WgXcQ',
'https://youtube.com/watch?v=dQw4w9WgXcQ'
],
'youtube with http scheme' => [
'http://youtube.com/watch?v=dQw4w9WgXcQ',
'http://youtube.com/watch?v=dQw4w9WgXcQ'
],
'vimeo without scheme' => [
'vimeo.com/123456789',
'https://vimeo.com/123456789'
],
'protocol-relative URL' => [
'//youtube.com/watch?v=dQw4w9WgXcQ',
'https://youtube.com/watch?v=dQw4w9WgXcQ'
],
'url with whitespace' => [
' youtube.com/watch?v=dQw4w9WgXcQ ',
'https://youtube.com/watch?v=dQw4w9WgXcQ'
],
];
}

public function testNormalizeValueWithNull()
{
$result = $this->field->normalizeValue(null, null);

$this->assertInstanceOf(OembedModel::class, $result);
$this->assertEquals('', $result->url);
}

public function testNormalizeValueWithOembedModel()
{
$model = new OembedModel('youtube.com/watch?v=dQw4w9WgXcQ');
$result = $this->field->normalizeValue($model, null);

$this->assertInstanceOf(OembedModel::class, $result);
$this->assertEquals('https://youtube.com/watch?v=dQw4w9WgXcQ', $result->url);
}

public function testNormalizeValueWithJsonString()
{
$json = json_encode(['url' => 'youtube.com/watch?v=dQw4w9WgXcQ']);
$result = $this->field->normalizeValue($json, null);

$this->assertInstanceOf(OembedModel::class, $result);
$this->assertEquals('https://youtube.com/watch?v=dQw4w9WgXcQ', $result->url);
}
}
Loading