Skip to content
Open
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
10 changes: 10 additions & 0 deletions src/Fieldtypes/Video.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,23 @@
namespace Statamic\Fieldtypes;

use Statamic\Fields\Fieldtype;
use Statamic\Fieldtypes\Video\Embed;

use function Statamic\trans as __;

class Video extends Fieldtype
{
protected $categories = ['media'];

public function augment($value)
{
if (is_null($value)) {
return null;
}

return Embed::fromValue($value);
}

protected function configFieldItems(): array
{
return [
Expand Down
205 changes: 205 additions & 0 deletions src/Fieldtypes/Video/Embed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
<?php

namespace Statamic\Fieldtypes\Video;

use Illuminate\Support\Str;
use Statamic\Fields\ArrayableString;
use Statamic\Support\FileTypes;

class Embed extends ArrayableString
{
const CLOUDFLARE = 'cloudflare';
const CLOUDFLARE_EMBED_URL = 'https://iframe.cloudflarestream.com/';
const CLOUDFLARE_ID_PATTERN = '/^[a-zA-Z0-9]+$/';
const CLOUDFLARE_PREFIX = 'cloudflare:';
const FILE = 'file';
const UNSUPPORTED = 'unsupported';
const VIMEO = 'vimeo';
const YOUTUBE = 'youtube';

public static function fromValue(?string $value): self
{
if (blank($value)) {
return static::unsupported($value);
}

if (Str::startsWith($value, self::CLOUDFLARE_PREFIX)) {
$id = Str::after($value, self::CLOUDFLARE_PREFIX);

return preg_match(self::CLOUDFLARE_ID_PATTERN, $id)
? new self(self::CLOUDFLARE, $value, self::CLOUDFLARE_EMBED_URL.$id, $id)
: static::unsupported($value);
}

if ($provider = static::oembedProvider($value)) {
return new self($provider, $value, static::embedUrl($value));
}

if (static::isVideoFile($value)) {
return new self(self::FILE, $value, $value);
}

return static::unsupported($value);
}

public static function unsupported(?string $value = null): self
{
return new self(self::UNSUPPORTED, $value);
}

public function __construct(
public readonly string $provider,
public readonly ?string $url = null,
public readonly ?string $embedUrl = null,
public readonly ?string $id = null,
) {
parent::__construct($url);
}

public function isEmbeddable(): bool
{
return ! in_array($this->provider, [self::FILE, self::UNSUPPORTED]);
}

public function isSupported(): bool
{
return $this->provider !== self::UNSUPPORTED;
}

public function toArray(): array
{
return [
'embed_url' => $this->embedUrl,
'id' => $this->id,
'provider' => $this->provider,
'url' => $this->url,
];
}

public function __toString(): string
{
return (string) $this->url;
}

#[\ReturnTypeWillChange]
public function jsonSerialize()
Comment thread
edalzell marked this conversation as resolved.
{
return (string) $this;
}

#[\ReturnTypeWillChange]
public function offsetExists(mixed $offset)
{
return array_key_exists($offset, $this->toArray());
}

#[\ReturnTypeWillChange]
public function offsetGet(mixed $offset)
{
return $this->toArray()[$offset] ?? null;
}

/**
* Turn a link that's direct to a video's page into its embeddable equivalent.
*/
public static function embedUrl(?string $url): ?string
{
if (blank($url)) {
return $url;
}

if (Str::contains($url, self::VIMEO)) {
return static::vimeoEmbedUrl($url);
}

if (Str::contains($url, 'youtu.be')) {
$url = str_replace('youtu.be', 'www.youtube.com/embed', $url);

// Check for start at point and replace it with correct parameter.
if (Str::contains($url, '?t=')) {
$url = str_replace('?t=', '?start=', $url);
}
}

if (Str::contains($url, 'youtube.com/watch?v=')) {
$url = str_replace('watch?v=', 'embed/', $url);

if (Str::contains($url, '&t=')) {
$url = str_replace('&t=', '?start=', $url);
}
}

if (Str::contains($url, 'youtube.com/shorts/')) {
$url = str_replace('shorts/', 'embed/', $url);
}

if (Str::contains($url, 'youtube.com')) {
$url = str_replace('youtube.com', 'youtube-nocookie.com', $url);
}

// This avoids SSL issues when using the non-www version
if (Str::contains($url, '//youtube-nocookie.com')) {
$url = str_replace('//youtube-nocookie.com', '//www.youtube-nocookie.com', $url);
}

if (Str::contains($url, '&') && ! Str::contains($url, '?')) {
$url = Str::replaceFirst('&', '?', $url);
}

return $url;
}

public static function isEmbeddableUrl(?string $url): bool
{
return filled($url) && Str::contains($url, ['youtu.be', 'youtube', self::VIMEO]);
}

protected static function isVideoFile(string $url): bool
{
if (blank($path = parse_url($url, PHP_URL_PATH))) {
return false;
}

return in_array(strtolower(pathinfo($path, PATHINFO_EXTENSION)), FileTypes::video());
}

protected static function oembedProvider(string $url): ?string
{
if (Str::contains($url, self::VIMEO)) {
return self::VIMEO;
}

if (Str::contains($url, ['youtu.be', 'youtube'])) {
return self::YOUTUBE;
}

return null;
}

// Unlisted vimeo urls are in the form vimeo.com/id/hash, but embeds pass the hash as a get param.
protected static function vimeoEmbedUrl(string $url): string
{
$url = str_replace('/vimeo.com', '/player.vimeo.com/video', $url);
$hash = '';

if (! Str::contains($url, 'progressive_redirect') && Str::substrCount($url, '/') > 4) {
$hash = Str::afterLast($url, '/');
$url = Str::beforeLast($url, '/');

if (Str::contains($hash, '?')) {
$url .= '?'.Str::after($hash, '?');
$hash = Str::before($hash, '?');
}
}

$paramsToAdd = '?dnt=1';

if ($hash) {
$paramsToAdd .= '&h='.$hash;
}

return Str::contains($url, '?')
? str_replace('?', $paramsToAdd.'&', $url)
: $url.$paramsToAdd;
}
}
93 changes: 21 additions & 72 deletions src/Modifiers/CoreModifiers.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@
use Statamic\Facades\Site;
use Statamic\Facades\URL;
use Statamic\Facades\YAML;
use Statamic\Fields\ArrayableString;
use Statamic\Fields\Value;
use Statamic\Fields\Values;
use Statamic\Fieldtypes\Bard;
use Statamic\Fieldtypes\Bard\Augmentor;
use Statamic\Fieldtypes\Link\ArrayableLink;
use Statamic\Fieldtypes\Video\Embed;
use Statamic\Statamic;
use Statamic\Support\Arr;
use Statamic\Support\Dumper;
Expand Down Expand Up @@ -1560,7 +1562,9 @@ public function length($value)
return $value->count();
}

if ($value instanceof Arrayable) {
// Value objects like ArrayableString are both Arrayable and Stringable.
// They stand in for a string, so measure the string, not the array.
if ($value instanceof Arrayable && ! $value instanceof ArrayableString) {
$value = $value->toArray();
}

Expand Down Expand Up @@ -3199,60 +3203,11 @@ public function yearsAgo($value, $params)
*/
public function embedUrl($url)
{
if (Str::contains($url, 'vimeo')) {
$url = str_replace('/vimeo.com', '/player.vimeo.com/video', $url);

[$url, $hash] = $this->handleUnlistedVimeoUrls($url);

$paramsToAdd = '?dnt=1';
if ($hash) {
$paramsToAdd .= '&h='.$hash;
}

if (Str::contains($url, '?')) {
$url = str_replace('?', $paramsToAdd.'&', $url);
} else {
$url .= $paramsToAdd;
}

return $url;
}

if (Str::contains($url, 'youtu.be')) {
$url = str_replace('youtu.be', 'www.youtube.com/embed', $url);

// Check for start at point and replace it with correct parameter.
if (Str::contains($url, '?t=')) {
$url = str_replace('?t=', '?start=', $url);
}
}

if (Str::contains($url, 'youtube.com/watch?v=')) {
$url = str_replace('watch?v=', 'embed/', $url);

if (Str::contains($url, '&t=')) {
$url = str_replace('&t=', '?start=', $url);
}
}

if (Str::contains($url, 'youtube.com/shorts/')) {
$url = str_replace('shorts/', 'embed/', $url);
if ($url instanceof Embed) {
return $url->embedUrl ?? $url->url;
}

if (Str::contains($url, 'youtube.com')) {
$url = str_replace('youtube.com', 'youtube-nocookie.com', $url);
}

// This avoids SSL issues when using the non-www version
if (Str::contains($url, '//youtube-nocookie.com')) {
$url = str_replace('//youtube-nocookie.com', '//www.youtube-nocookie.com', $url);
}

if (Str::contains($url, '&') && ! Str::contains($url, '?')) {
$url = Str::replaceFirst('&', '?', $url);
}

return $url;
return Embed::embedUrl($url);
}

/**
Expand All @@ -3264,6 +3219,14 @@ public function embedUrl($url)
*/
public function trackableEmbedUrl($url)
{
if ($url instanceof Embed) {
$url = $url->url;
}

if (blank($url)) {
return $url;
}
Comment thread
edalzell marked this conversation as resolved.

if (Str::contains($url, 'vimeo')) {
return str_replace('/vimeo.com', '/player.vimeo.com/video', $url);
}
Expand Down Expand Up @@ -3296,7 +3259,11 @@ public function trackableEmbedUrl($url)
*/
public function isEmbeddable($url)
{
return Str::contains($url, ['youtu.be', 'youtube', 'vimeo']);
if ($url instanceof Embed) {
return $url->isEmbeddable();
}
Comment thread
edalzell marked this conversation as resolved.

return Embed::isEmbeddableUrl($url);
}

/**
Expand Down Expand Up @@ -3381,24 +3348,6 @@ private function getFromContext($context, $params, $key = 0)
Arr::get($context, $params[$key], $params[$key]);
}

// unlisted vimeo urls are in the form vimeo.com/id/hash, but embeds pass the hash as a get param
private function handleUnlistedVimeoUrls($url)
{
$hash = '';

if (! Str::contains($url, 'progressive_redirect') && Str::substrCount($url, '/') > 4) {
$hash = Str::afterLast($url, '/');
$url = Str::beforeLast($url, '/');

if (Str::contains($hash, '?')) {
$url .= '?'.Str::after($hash, '?');
$hash = Str::before($hash, '?');
}
}

return [$url, $hash];
}

private function dumpingAllowed(array $params): bool
{
return $this->traitDumpingAllowed() || (Arr::get($params, 0) === 'force');
Expand Down
Loading
Loading