Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement SmartFrame embedding #21101

Merged
merged 11 commits into from
Sep 22, 2021
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: enhancement

SmartFrame Embeds: add support SmartFrame embed using URLs, embed code, and shortcodes
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
import './facebook';
import './instagram';
import './loom';
import './smartframe';
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* WordPress dependencies
*/
import { registerBlockVariation } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { SmartFrameIcon } from '../../shared/icons';
/*
* New `core/embed` block variation.
*/

const coreEmbedVariationGetty = {
name: 'smartframe',
title: 'SmartFrame',
icon: SmartFrameIcon,
keywords: [ __( 'smartframe', 'jetpack' ) ],
description: __( 'Embed a SmartFrame Image.', 'jetpack' ),
patterns: [ /^https?:\/\/(.*?).smartframe.(io|net)\/.*/i ],
attributes: { providerNameSlug: 'smartframe', responsive: true },
};
registerBlockVariation( 'core/embed', coreEmbedVariationGetty );
11 changes: 11 additions & 0 deletions projects/plugins/jetpack/extensions/shared/icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,17 @@ export const LoomIcon = {
),
};

export const SmartFrameIcon = {
foreground: getIconColor(),
src: (
<SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20.7 17" xmlSpace="preserve">
<Path
d="m20.7 12.9-.9-11c0-.5-.2-.9-.5-1.3-.3-.3-.8-.5-1.3-.5L1.9 0h-.1c-.2 0-.5 0-.7.1C.9.2.7.4.5.5.4.7.2.9.1 1.1c-.1.2-.1.5-.1.7v.1l.9 13.4c0 .5.2.9.5 1.3.3.2.8.4 1.3.4H3l16.1-2c.4 0 .9-.3 1.1-.6.3-.3.5-.8.5-1.2v-.3zm-3.1.8L4.2 15.3H4c-.4 0-.8-.1-1-.4-.3-.3-.4-.6-.5-1L1.7 3.2v-.1c0-.4.2-.8.5-1 .3-.3.7-.4 1-.4h.1l13.5.1c.4 0 .8.1 1 .4.3.3.4.6.5 1L19 12v.3c0 .4-.2.7-.4 1-.3.2-.6.4-1 .4z"
/>
</SVG>
),
};

export const DonationsIcon = {
foreground: getIconColor(),
src: (
Expand Down
114 changes: 114 additions & 0 deletions projects/plugins/jetpack/modules/shortcodes/smartframe.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php
/**
* Smartframe.io embed
*
* Example URL: https://mikael-korpela.smartframe.io/p/mantymetsa_1630927773870/7673dc41a775fb845cc26acf24f1fe4?t=rql1c6dbpv2
* Example embed code: <script src="https://embed.smartframe.io/6ae67829d1264ee0ea6071a788940eae.js" data-image-id="mantymetsa_1630927773870" data-width="100%" data-max-width="1412px"></script>
*
* @package automattic/jetpack
*/

if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
add_action( 'init', 'jetpack_smartframe_enable_embeds' );
} else {
jetpack_smartframe_enable_embeds();
}

/**
* Register smartframe as oembed provider. Add filter to reverse iframes to shortcode. Register [smartframe] shortcode.
*
* @since 10.2.0
*/
function jetpack_smartframe_enable_embeds() {
// Support their oEmbed Endpoint.
wp_oembed_add_provider( '#https?://(.*?)\.smartframe\.(io|net)/.*#i', 'https://oembed.smartframe.io/', true );

// Allow script to be filtered to short code (so direct copy+paste can be done).
add_filter( 'pre_kses', 'jetpack_shortcodereverse_smartframe' );

// Actually display the smartframe Embed.
add_shortcode( 'smartframe', 'jetpack_smartframe_shortcode' );
jeherve marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Compose shortcode based on smartframe iframes.
*
* @since 10.2.0
*
* @param string $content Post content.
*
* @return mixed
*/
function jetpack_shortcodereverse_smartframe( $content ) {
if ( ! is_string( $content ) || false === stripos( $content, 'embed.smartframe' ) ) {
return $content;
}

// phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
$regexp = '!<script\ssrc="https://embed\.smartframe\.(?:io|net)/(\w+)\.js"\sdata-image-id="(.*?)"(?:\sdata-width="(?:\d+(?:%|px))"\s)?(?:data-max-width="(\d+(%|px)))?"></script>!i';
$regexp_ent = str_replace( '&amp;#0*58;', '&amp;#0*58;|&#0*58;', htmlspecialchars( $regexp, ENT_NOQUOTES ) );

foreach ( compact( 'regexp', 'regexp_ent' ) as $regexp ) {
if ( ! preg_match_all( $regexp, $content, $matches, PREG_SET_ORDER ) ) {
continue;
}

foreach ( $matches as $match ) {
// We need at least a script ID and an image ID.
if ( ! isset( $match[1], $match[2] ) ) {
continue;
}
$shortcode = sprintf(
'[smartframe script-id="%1$s" image-id="%2$s"%3$s]',
esc_attr( $match[1] ),
esc_attr( $match[2] ),
! empty( $match[3] ) ? ' max-width="' . esc_attr( $match[3] ) . '"' : ''
);
$content = str_replace( $match[0], $shortcode, $content );
}
}
/** This action is documented in modules/widgets/social-media-icons.php */
do_action( 'jetpack_bump_stats_extras', 'html_to_shortcode', 'smartframe' );

return $content;
}

/**
* Parse shortcode arguments and render its output.
*
* @since 10.2.0
*
* @param array $atts Shortcode parameters.
*
* @return string
*/
function jetpack_smartframe_shortcode( $atts ) {
if ( ! empty( $atts['image-id'] ) ) {
$image_id = $atts['image-id'];
} else {
return '<!-- Missing smartframe image-id -->';
}
if ( ! empty( $atts['script-id'] ) ) {
$script_id = $atts['script-id'];
} else {
return '<!-- Missing smartframe script-id -->';
}

$params = array(
// ignore width for now, smartframe embed code has it "100%". % isn't allowed in oembed, making it 100px.
// 'width' => isset( $atts['width'] ) ? (int) $atts['width'] : null,.
'max-width' => isset( $atts['max-width'] ) ? (int) $atts['max-width'] : null,
);

$embed_url = sprintf(
'https://imagecards.smartframe.io/%1$s/%2$s',
esc_attr( $script_id ),
esc_attr( $image_id )
);

// wrap the embed with wp-block-embed__wrapper, otherwise it would be aligned to the very left of the viewport.
return sprintf(
'<div class="wp-block-embed__wrapper">%1$s</div>',
wp_oembed_get( $embed_url, array_filter( $params ) )
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{
"https://oembed.smartframe.io/?maxwidth=500&maxheight=750&url=https%3A%2F%2Fimagecards.smartframe.io%2F6ae67829d1264ee0ea6071a788940eae%2Fmantymetsa_1630927773870&dnt=1&caller=example.org&format=json": [
{
"args": {
"body": null,
"method": "GET"
},
"response": {
"body": "{\"type\":\"rich\",\"version\":\"1.0\",\"html\":\"<script data-image-id='mantymetsa_1630927773870' src='https://embed.smartframe.io/6ae67829d1264ee0ea6071a788940eae.js' data-width='500' data-height='354\"></script>\",\"width\":500,\"height\":354,\"thumbnail_url\":\"https://thumbs.smartframe.io/6ae67829d1264ee0ea6071a788940eae/d9cb5f3e6787557531b960586f59388d/mantymetsa.jpg\",\"thumbnail_width\":400,\"thumbnail_height\":283,\"author_name\":\"\"}",
"response": {
"code": 200,
"message": "OK"
}
}
}
],
"https://oembed.smartframe.io/?maxwidth=640&maxheight=960&url=https%3A%2F%2Fimagecards.smartframe.io%2F6ae67829d1264ee0ea6071a788940eae%2Fmantymetsa_1630927773870&dnt=1&caller=example.org": [
{
"args": {
"body": null,
"method": "GET"
},
"response": {
"body": "{\"type\":\"rich\",\"version\":\"1.0\",\"html\":\"<script data-image-id='mantymetsa_1630927773870' src='https://embed.smartframe.io/6ae67829d1264ee0ea6071a788940eae.js' data-width='640' data-height='453'></script>\",\"width\":640,\"height\":453,\"thumbnail_url\":\"https://thumbs.smartframe.io/6ae67829d1264ee0ea6071a788940eae/d9cb5f3e6787557531b960586f59388d/mantymetsa.jpg\",\"thumbnail_width\":400,\"thumbnail_height\":283,\"author_name\":\"\"}",
"response": {
"code": 200,
"message": "OK"
}
}
}
],
"https://oembed.smartframe.io/?maxwidth=640&maxheight=960&url=https%3A%2F%2Fimagecards.smartframe.io%2F6ae67829d1264ee0ea6071a788940eae%2Fmantymetsa_1630927773870&dnt=1&caller=example.org&format=json": [
{
"args": {
"body": null,
"method": "GET"
},
"response": {
"body": "{\"type\":\"rich\",\"version\":\"1.0\",\"html\":\"<script data-image-id='mantymetsa_1630927773870' src='https://embed.smartframe.io/6ae67829d1264ee0ea6071a788940eae.js' data-width='640' data-height='453'></script>\",\"width\":640,\"height\":453,\"thumbnail_url\":\"https://thumbs.smartframe.io/6ae67829d1264ee0ea6071a788940eae/d9cb5f3e6787557531b960586f59388d/mantymetsa.jpg\",\"thumbnail_width\":400,\"thumbnail_height\":283,\"author_name\":\"\"}",
"response": {
"code": 200,
"message": "OK"
}
}
}
],
"https://oembed.smartframe.io/?maxwidth=640&maxheight=960&url=https%3A%2F%2Fimagecards.smartframe.io%2F6ae67829d1264ee0ea6071a788940eae%2Fmantymetsa_1630927773870&dnt=1&format=json": [
{
"args": {
"body": null,
"method": "GET"
},
"response": {
"body": "{\"type\":\"rich\",\"version\":\"1.0\",\"html\":\"<script data-image-id='mantymetsa_1630927773870' src='https://embed.smartframe.io/6ae67829d1264ee0ea6071a788940eae.js' data-width='640' data-height='453'></script>\",\"width\":640,\"height\":453,\"thumbnail_url\":\"https://thumbs.smartframe.io/6ae67829d1264ee0ea6071a788940eae/d9cb5f3e6787557531b960586f59388d/mantymetsa.jpg\",\"thumbnail_width\":400,\"thumbnail_height\":283,\"author_name\":\"\"}",
"response": {
"code": 200,
"message": "OK"
}
}
}
],
"https://oembed.smartframe.io/?maxwidth=640&maxheight=960&url=https%3A%2F%2Fimagecards.smartframe.io%2F6ae67829d1264ee0ea6071a788940eae%2Fmantymetsa_1630927773870&dnt=1F 2 / 2 (100%)omarhttps://oembed.smartframe.io/?maxwidth=640&maxheight=960&url=https%3A%2F%2Fimagecards.smartframe.io%2F6ae67829d1264ee0ea6071a788940eae%2Fmantymetsa_1630927773870&dnt=1": [
{
"args": {
"body": null,
"method": "GET"
},
"response": {
"body": "{\"type\":\"rich\",\"version\":\"1.0\",\"html\":\"<script data-image-id='mantymetsa_1630927773870' src='https://embed.smartframe.io/6ae67829d1264ee0ea6071a788940eae.js' data-width='640' data-height='453'></script>\",\"width\":640,\"height\":453,\"thumbnail_url\":\"https://thumbs.smartframe.io/6ae67829d1264ee0ea6071a788940eae/d9cb5f3e6787557531b960586f59388d/mantymetsa.jpg\",\"thumbnail_width\":400,\"thumbnail_height\":283,\"author_name\":\"\"}",
"response": {
"code": 200,
"message": "OK"
}
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php
/**
* Unit tests for smartframe embedding
*
* Tests smartframe shortcodes and embed code
*
* @package automattic/jetpack
*/

/**
* Shortcodes need external HTML requests to be converted to valid embed code (using smartframe's oembed endpoint)
*/
require_once __DIR__ . '/trait.http-request-cache.php';

/**
* Implements unit tests for smartframe embedding
*
* @covers ::shortcode_smartframe
*/
class WP_Test_Jetpack_Shortcodes_SmartFrame extends WP_UnitTestCase {
use Automattic\Jetpack\Tests\HttpRequestCacheTrait;

const SMARTFRAME_IDENTIFIER = 'mantymetsa_1630927773870';
const SMARTFRAME_SCRIPT_ID = '6ae67829d1264ee0ea6071a788940eae';

const SMARTFRAME_SHORTCODE = '[smartframe script-id="6ae67829d1264ee0ea6071a788940eae" image-id="mantymetsa_1630927773870" max-width="1412px"]';
// phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
const SMARTFRAME_EMBED = '<script src="https://embed.smartframe.io/6ae67829d1264ee0ea6071a788940eae.js" data-image-id="mantymetsa_1630927773870" data-width="100%" data-max-width="1412px"></script>';

/**
* Check for external HTTP requests and register filter
*/
public function setUp() {
parent::setUp();

if ( in_array( 'external-http', $this->getGroups(), true ) ) {
// Used by WordPress.com - does nothing in Jetpack.
add_filter( 'tests_allow_http_request', '__return_true' );
} else {
/*
* We normally make an HTTP request to SmartFrame's oEmbed endpoint to generate
* the shortcode output.
* This filter bypasses that HTTP request for these tests
*/
add_filter( 'pre_oembed_result', array( $this, 'smartframe_oembed_response' ), 10, 3 );
}
}

/**
* Mocks matching HTML for an embedded smartframe item
*
* @param string $html Post content.
* @param string $url found URL.
*
* @since 10.2.0
*/
public function smartframe_oembed_response( $html, $url ) {
if ( 0 !== strpos( $url, 'smartframe.io' ) ) {
return $html;
}
return self::SMARTFRAME_EMBED;
}

/**
* Verify that [smartframe] exists.
*
* @since 10.2.0
*/
public function test_shortcodes_smartframe_exists() {
$this->assertEquals( shortcode_exists( 'smartframe' ), true );
}

/**
* See if the shortcode is converted to valid embedding code
*
* @group external-http
*
* @since 10.2.0
*/
public function test_smartframe_shortcode() {
$parsed = do_shortcode( self::SMARTFRAME_SHORTCODE );

$doc = new DOMDocument();
$doc->loadHTML( $parsed );
$links = $doc->getElementsByTagName( 'script' );

foreach ( $links as $link ) {
$this->assertTrue( $link->hasAttribute( 'data-image-id' ) );
$this->assertContains( self::SMARTFRAME_IDENTIFIER, $link->getAttribute( 'data-image-id' ) );
}
}

/**
* Verify that embedding code is reversed into a valid shortcode
*
* @since 10.2.0
*/
public function test_smartframe_reverse_shortcode() {
$shortcode = jetpack_shortcodereverse_smartframe( self::SMARTFRAME_EMBED );
$this->assertEquals( self::SMARTFRAME_SHORTCODE, $shortcode );
}

/**
* Uses a real HTTP request to SmartFrame's oEmbed endpoint to
* verify that rendering the shortcode returns a SmartFrame image.
*
* @group external-http
*
* @since 10.2.0
*/
public function test_shortcodes_smartframe_image_via_oembed_http_request() {
$image_id = self::SMARTFRAME_IDENTIFIER;
$script_id = self::SMARTFRAME_SCRIPT_ID;
$content = "[smartframe script-id='$script_id' image-id='$image_id']";
$shortcode_content = do_shortcode( $content );

$this->assertContains( $image_id, $shortcode_content );
}
}