' . $video_output . '
';
$download_url = sprintf(
diff --git a/inc/classes/gravity-forms/class-gf-field-godam-video.php b/inc/classes/gravity-forms/class-gf-field-godam-video.php
index 91fd8940f..b590c1b28 100644
--- a/inc/classes/gravity-forms/class-gf-field-godam-video.php
+++ b/inc/classes/gravity-forms/class-gf-field-godam-video.php
@@ -423,10 +423,17 @@ public function get_value_entry_detail( $value, $currency = '', $use_text = fals
if ( $is_video ) {
if ( $transcoded_url ) {
- $transcoded_url = esc_url( $transcoded_url );
+ $transcoded_url = esc_url_raw( $transcoded_url );
}
- $video_output = do_shortcode( "[godam_video src='{$file_path}' transcoded_url='{$transcoded_url}' aspectRatio='']" );
+ $video_output = rtgodam_render_shortcode_from_atts(
+ 'godam_video',
+ array(
+ 'src' => esc_url_raw( $file_path ),
+ 'transcoded_url' => (string) $transcoded_url,
+ 'aspectratio' => '',
+ )
+ );
$video_output = '' . $video_output . '
';
$output_arr[] = $video_output;
}
diff --git a/inc/classes/rest-api/class-dynamic-shortcode.php b/inc/classes/rest-api/class-dynamic-shortcode.php
index f87eefe11..7e7695b77 100644
--- a/inc/classes/rest-api/class-dynamic-shortcode.php
+++ b/inc/classes/rest-api/class-dynamic-shortcode.php
@@ -58,7 +58,7 @@ public function get_rest_routes() {
'required' => false,
'type' => 'string',
'default' => '',
- 'sanitize_callback' => 'sanitize_text_field',
+ 'sanitize_callback' => 'sanitize_key',
),
),
),
@@ -77,7 +77,8 @@ public function render_shortcode( WP_REST_Request $request ) {
$engagements = $request->get_param( 'engagements' );
- $godam_context = $request->get_param( 'godam_context' );
+ // Slug characters only, whatever reaches this method.
+ $godam_context = sanitize_key( (string) $request->get_param( 'godam_context' ) );
/**
* Fires before resolving this attachment's URL/meta/sources, so
@@ -128,9 +129,7 @@ public function render_shortcode( WP_REST_Request $request ) {
'type' => 'video/quicktime' === $video_src_type ? 'video/mp4' : $video_src_type,
);
- // Convert JSON to use custom placeholders instead of square brackets.
- $sources_json = wp_json_encode( $sources );
- $sources_with_placeholders = str_replace( array( '[', ']' ), array( '__rtgob__', '__rtgcb__' ), $sources_json );
+ $sources_json = wp_json_encode( $sources );
// Add action before shortcode rendering.
do_action( 'rtgodam_shortcode_before_render', $id );
@@ -161,18 +160,53 @@ public function render_shortcode( WP_REST_Request $request ) {
$video_date = apply_filters( 'rtgodam_shortcode_video_date', $video_date, $id );
ob_start();
- $shortcode = "[godam_video id='{$id}' engagements='" . esc_attr( $engagements ) . "' sources='{$sources_with_placeholders}'";
+ $atts = array(
+ 'id' => (string) $id,
+ 'engagements' => (string) $engagements,
+ 'sources' => $sources_json,
+ );
if ( ! empty( $godam_context ) ) {
- $shortcode .= " godam_context='" . esc_attr( $godam_context ) . "'";
+ $atts['godam_context'] = $godam_context;
}
- $shortcode .= ']';
+ /**
+ * Filters the attributes the player is rendered with.
+ *
+ * @since 2.5.1
+ *
+ * @param array $atts Attributes passed to the [godam_video] renderer.
+ * @param int $id Attachment ID.
+ */
+ $atts = apply_filters( 'rtgodam_shortcode_atts', $atts, $id );
+
+ if ( has_filter( 'rtgodam_shortcode_output' ) ) {
+ // Keep sites that still change the shortcode string working. Every value in it is
+ // an ID, a slug, 'show'/'hide' or JSON with its brackets swapped for placeholders.
+ $shortcode = "[godam_video id='" . absint( $atts['id'] ?? $id ) . "' engagements='" . esc_attr( $atts['engagements'] ?? '' ) . "' sources='" . str_replace( array( '[', ']' ), array( '__rtgob__', '__rtgcb__' ), (string) ( $atts['sources'] ?? '' ) ) . "'";
- // Add filter for shortcode.
- $shortcode = apply_filters( 'rtgodam_shortcode_output', $shortcode, $id );
+ if ( ! empty( $atts['godam_context'] ) ) {
+ $shortcode .= " godam_context='" . esc_attr( $atts['godam_context'] ) . "'";
+ }
+
+ $shortcode .= ']';
+
+ /**
+ * Filters the shortcode string the player is rendered from.
+ *
+ * @since 1.1.4
+ * @deprecated 2.5.1 Use the rtgodam_shortcode_atts filter instead.
+ *
+ * @param string $shortcode The [godam_video] shortcode.
+ * @param int $id Attachment ID.
+ */
+ $shortcode = apply_filters_deprecated( 'rtgodam_shortcode_output', array( $shortcode, $id ), '2.5.1', 'rtgodam_shortcode_atts' );
+
+ echo do_shortcode( $shortcode );
+ } else {
+ echo rtgodam_render_shortcode_from_atts( 'godam_video', $atts ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The player template escapes its output.
+ }
- echo do_shortcode( $shortcode );
$html = ob_get_clean();
// Add filter for final HTML output.
diff --git a/inc/classes/sureforms/class-form-submit.php b/inc/classes/sureforms/class-form-submit.php
index 21399817a..be64dc285 100644
--- a/inc/classes/sureforms/class-form-submit.php
+++ b/inc/classes/sureforms/class-form-submit.php
@@ -348,8 +348,6 @@ public function render_custom_field_markup( $markup, $value ) {
$transcoded_url_output = '';
if ( ! empty( $transcoded_url ) ) {
- $transcoded_url = esc_url( $transcoded_url );
- $transcoded_url = "transcoded_url={$transcoded_url}";
$transcoded_url_output = sprintf(
" esc_url_raw( $godam_thumbnail_url ),
+ 'src' => esc_url_raw( $godam_attachment_url ),
+ 'transcoded_url' => esc_url_raw( (string) $godam_transcoded_url ),
+ )
+ )
+ );
?>
diff --git a/inc/helpers/custom-functions.php b/inc/helpers/custom-functions.php
index 40f07e726..7403e6b94 100644
--- a/inc/helpers/custom-functions.php
+++ b/inc/helpers/custom-functions.php
@@ -2192,10 +2192,10 @@ function godam_preview_page_content( $video_id ) {
) . ' /-->'
);
} else {
- $godam_shortcode = $godam_is_audio
- ? '[godam_audio id="' . $video_id . '"]'
- : '[godam_video id="' . $video_id . '"]';
- $godam_media_output = do_shortcode( $godam_shortcode );
+ $godam_media_output = rtgodam_render_shortcode_from_atts(
+ $godam_is_audio ? 'godam_audio' : 'godam_video',
+ array( 'id' => (string) $video_id )
+ );
}
?>
@@ -2293,6 +2293,30 @@ function rtgodam_get_block_source_from_context( $context ) {
return isset( $map[ $context ] ) ? $map[ $context ] : $context;
}
+/**
+ * Render a registered shortcode from an attributes array.
+ *
+ * Calls the shortcode's callback directly, so each attribute value is passed to
+ * the renderer as plain data and is never parsed as shortcode syntax. Use it
+ * instead of do_shortcode() when a value comes from a request, a form
+ * submission or other stored user data.
+ *
+ * @since 2.5.1
+ *
+ * @param string $tag Shortcode tag, for example 'godam_video'.
+ * @param array $atts Attributes, keyed in lowercase as the shortcode parser would key them.
+ * @return string The shortcode output, or an empty string when the shortcode is not registered or its callback cannot be called.
+ */
+function rtgodam_render_shortcode_from_atts( $tag, $atts = array() ) {
+ global $shortcode_tags;
+
+ if ( ! shortcode_exists( $tag ) || ! is_callable( $shortcode_tags[ $tag ] ) ) {
+ return '';
+ }
+
+ return (string) call_user_func( $shortcode_tags[ $tag ], $atts, '', $tag );
+}
+
/**
* Generate HTML content for the video embed page.
*
@@ -2301,10 +2325,10 @@ function rtgodam_get_block_source_from_context( $context ) {
* making it suitable for embedding in iframes or modals.
*
* @param int $video_id The ID of the video attachment to embed.
- * @param string $godam_context Optional. The player context passed to the godam_video shortcode (e.g. 'video-only').
+ * @param string $godam_context Optional. The player context passed to the godam_video shortcode (e.g. 'video-only'). Reduced to slug characters with sanitize_key().
* @param string $bg_color Optional. Hex background color applied as --godam-video-bg-color CSS variable on the wrapper.
* @param bool $show_engagements Optional. Whether to show engagements in the embed. Default false (no engagements shown).
- * @param string $block_source Optional. Analytics placement slug forwarded to the player (e.g. 'video-gallery' for gallery iframes).
+ * @param string $block_source Optional. Analytics placement slug forwarded to the player (e.g. 'video-gallery' for gallery iframes). Reduced to slug characters with sanitize_key().
* @param int $host_post_id Optional. Post ID of the page embedding this iframe, so analytics attribute to the host page.
*
* @since 1.5.0
@@ -2319,6 +2343,10 @@ function godam_embed_page_content( $video_id, $godam_context = '', $bg_color = '
$video_id = intval( $video_id );
$engagements_value = rtgodam_is_engagement_feature_enabled() && $show_engagements ? 'show' : '';
+ // Both values arrive from the URL; keep them to slug characters.
+ $godam_context = sanitize_key( $godam_context );
+ $block_source = sanitize_key( $block_source );
+
if ( ! empty( $video_id ) ) {
/**
* Fires before resolving this attachment's post/mime data, so
@@ -2342,20 +2370,19 @@ function godam_embed_page_content( $video_id, $godam_context = '', $bg_color = '
(string) $video_id );
if ( ! empty( $godam_context ) ) {
- $godam_shortcode .= ' godam_context="' . esc_attr( $godam_context ) . '"';
+ $godam_atts['godam_context'] = $godam_context;
}
if ( ! empty( $engagements_value ) ) {
- $godam_shortcode .= ' engagements="' . esc_attr( $engagements_value ) . '"';
+ $godam_atts['engagements'] = $engagements_value;
}
if ( ! empty( $block_source ) ) {
- $godam_shortcode .= ' block_source="' . esc_attr( $block_source ) . '"';
+ $godam_atts['block_source'] = $block_source;
}
if ( ! empty( $host_post_id ) ) {
- $godam_shortcode .= ' host_post_id="' . absint( $host_post_id ) . '"';
+ $godam_atts['host_post_id'] = (string) absint( $host_post_id );
}
- $godam_shortcode .= ']';
$godam_wrapper_style = ! empty( $bg_color ) ? '--godam-video-bg-color: ' . $bg_color . ';' : '';
?>
@@ -2364,7 +2391,7 @@ class="godam-video-embed" data-godam-context=""
style=""
>
-
+
array(
+ 'mode' => 'query',
+ 'layout' => 'grid',
+ 'item_width' => 'S',
+ 'view_ratio' => '16:9',
+ 'show_title' => 'yes',
+ 'performance_mode' => 'balanced',
+ 'show_play_button' => 'yes',
+ 'interaction' => 'hover',
+ 'count' => array( 'size' => 6 ),
+ 'orderby' => 'date',
+ 'order' => 'desc',
+ 'media_folder' => array( 12, 15 ),
+ 'author' => array(),
+ ),
+ );
+ rtgodam_stub_register_player_and_marker();
+
+ $this->gallery_atts = array();
+ $atts = &$this->gallery_atts;
+ add_shortcode(
+ 'godam_video_gallery',
+ static function ( $received ) use ( &$atts ) {
+ $atts[] = $received;
+ return '';
+ }
+ );
+
+ $this->widget = ( new \ReflectionClass( Godam_Gallery::class ) )->newInstanceWithoutConstructor();
+ }
+
+ protected function tearDown(): void {
+ unset( $GLOBALS['rtgodam_stub'] );
+ parent::tearDown();
+ }
+
+ /**
+ * Render the widget.
+ *
+ * @return string
+ */
+ private function render_widget() {
+ $render = new \ReflectionMethod( Godam_Gallery::class, 'render' );
+ $render->setAccessible( true );
+
+ ob_start();
+ $render->invoke( $this->widget );
+
+ return ob_get_clean();
+ }
+
+ /** A normal widget renders the gallery with its settings as string attributes. */
+ public function test_a_normal_widget_renders_the_gallery_with_its_settings() {
+ $html = $this->render_widget();
+
+ $this->assertStringContainsString( 'godam-gallery-double', $html );
+ $this->assertCount( 1, $this->gallery_atts );
+ $this->assertSame(
+ $this->sorted(
+ array(
+ 'mode' => 'query',
+ 'layout' => 'grid',
+ 'item_width' => 'S',
+ 'view_ratio' => '16:9',
+ 'show_title' => 'true',
+ 'interaction' => 'hover',
+ 'show_play_button' => 'true',
+ 'performance_mode' => 'balanced',
+ 'engagements' => 'false',
+ 'count' => '6',
+ 'orderby' => 'date',
+ 'order' => 'desc',
+ 'media_folder' => '12,15',
+ 'author' => '',
+ 'date_range' => '',
+ 'custom_date_start' => '',
+ 'custom_date_end' => '',
+ 'enable_more_items' => 'false',
+ 'more_items_behavior' => '',
+ )
+ ),
+ $this->sorted( $this->gallery_atts[0] )
+ );
+ }
+
+ /** Handpicked videos are passed as a comma-separated list of IDs. */
+ public function test_handpicked_videos_are_passed_as_ids() {
+ $GLOBALS['rtgodam_stub']['elementor_settings']['mode'] = 'handpicked';
+ $GLOBALS['rtgodam_stub']['elementor_settings']['handpicked_items'] = array(
+ array( 'video' => array( 'id' => '31' ) ),
+ array( 'video' => array( 'id' => '32' ) ),
+ );
+
+ $this->render_widget();
+
+ $this->assertSame( '31,32', $this->gallery_atts[0]['ids'] );
+ }
+
+ /** Shortcode text in a setting is not run. */
+ public function test_shortcode_text_in_a_setting_is_not_run() {
+ $GLOBALS['rtgodam_stub']['elementor_settings']['custom_date_start'] = 'x][gx_marker]';
+
+ $html = $this->render_widget();
+
+ $this->assertSame( 0, $GLOBALS['rtgodam_stub']['marker_runs'] );
+ $this->assertStringNotContainsString( 'GX-MARKER', $html );
+ $this->assertCount( 1, $this->gallery_atts );
+ $this->assertSame( 'x][gx_marker]', $this->gallery_atts[0]['custom_date_start'] );
+ $this->assertArrayNotHasKey( 'shortcodes', $GLOBALS['rtgodam_stub'] );
+ }
+
+ /**
+ * Sort attributes by name, so the comparison does not depend on their order.
+ *
+ * @param array $atts Attributes.
+ * @return array
+ */
+ private function sorted( array $atts ) {
+ ksort( $atts );
+
+ return $atts;
+ }
+}
diff --git a/tests/php/EmbedPagePlayerAttributesTest.php b/tests/php/EmbedPagePlayerAttributesTest.php
new file mode 100644
index 000000000..2dfc75427
--- /dev/null
+++ b/tests/php/EmbedPagePlayerAttributesTest.php
@@ -0,0 +1,172 @@
+ array(
+ 7 => new \WP_Post(
+ array(
+ 'ID' => 7,
+ 'post_type' => 'attachment',
+ 'post_mime_type' => 'video/mp4',
+ )
+ ),
+ ),
+ 'mime' => 'video/mp4',
+ 'title' => 'Sample video',
+ );
+ rtgodam_stub_register_player_and_marker();
+ }
+
+ protected function tearDown(): void {
+ unset( $GLOBALS['rtgodam_stub'] );
+ parent::tearDown();
+ }
+
+ /** @return array[] Attributes each player render received. */
+ private function player_atts() {
+ return $GLOBALS['rtgodam_stub']['player_atts'];
+ }
+
+ /** A link the plugin itself builds renders the player with the same attributes as before. */
+ public function test_a_normal_link_renders_the_player_with_the_same_attributes() {
+ add_filter(
+ 'rtgodam_enable_engagement_feature',
+ static function () {
+ return true;
+ }
+ );
+
+ $html = godam_embed_page_content( 7, 'godam-video-product-gallery', '#112233', true, 'video-gallery', 12 );
+
+ $this->assertSame(
+ array(
+ array(
+ 'id' => '7',
+ 'godam_context' => 'godam-video-product-gallery',
+ 'engagements' => 'show',
+ 'block_source' => 'video-gallery',
+ 'host_post_id' => '12',
+ ),
+ ),
+ $this->player_atts()
+ );
+ $this->assertStringContainsString( 'class="godam-player-double" data-id="7"', $html );
+ $this->assertStringContainsString( 'data-godam-context="godam-video-product-gallery"', $html );
+ $this->assertStringContainsString( '--godam-video-bg-color: #112233;', $html );
+ }
+
+ /** A link with only the video ID renders the bare player. */
+ public function test_a_link_with_only_the_video_id_renders_the_bare_player() {
+ godam_embed_page_content( 7 );
+
+ $this->assertSame( array( array( 'id' => '7' ) ), $this->player_atts() );
+ }
+
+ /** Shortcode text appended to godam_context is not run. */
+ public function test_shortcode_text_in_godam_context_is_not_run() {
+ $html = godam_embed_page_content( 7, self::CONTEXT_PAYLOAD );
+
+ $this->assertSame( 0, $GLOBALS['rtgodam_stub']['marker_runs'] );
+ $this->assertStringNotContainsString( 'GX-MARKER', $html );
+ $this->assertCount( 1, $this->player_atts() );
+ $this->assertSame( 'xgx_marker', $this->player_atts()[0]['godam_context'] );
+ $this->assertStringContainsString( 'data-godam-context="xgx_marker"', $html );
+ }
+
+ /** Shortcode text appended to block_source is not run. */
+ public function test_shortcode_text_in_block_source_is_not_run() {
+ $html = godam_embed_page_content( 7, '', '', false, self::CONTEXT_PAYLOAD );
+
+ $this->assertSame( 0, $GLOBALS['rtgodam_stub']['marker_runs'] );
+ $this->assertStringNotContainsString( 'GX-MARKER', $html );
+ $this->assertCount( 1, $this->player_atts() );
+ $this->assertSame( 'xgx_marker', $this->player_atts()[0]['block_source'] );
+ }
+
+ /** A value with quotes and a second tag is cut down to slug characters and runs nothing. */
+ public function test_quotes_and_tags_are_dropped_from_both_values() {
+ $value = 'a" b="][gx_marker]["';
+
+ godam_embed_page_content( 7, $value, '', false, $value );
+
+ $this->assertSame( 0, $GLOBALS['rtgodam_stub']['marker_runs'] );
+ $this->assertSame( 'abgx_marker', $this->player_atts()[0]['godam_context'] );
+ $this->assertSame( 'abgx_marker', $this->player_atts()[0]['block_source'] );
+ }
+
+ /** The values are slugs, so upper case is lowered the way sanitize_key() does. */
+ public function test_values_are_reduced_to_slugs() {
+ godam_embed_page_content( 7, 'Video-Only', '', false, 'Video-Gallery' );
+
+ $this->assertSame( 'video-only', $this->player_atts()[0]['godam_context'] );
+ $this->assertSame( 'video-gallery', $this->player_atts()[0]['block_source'] );
+ }
+
+ /** The preview page renders a video attachment through the video player attributes. */
+ public function test_the_preview_page_renders_a_video_from_attributes() {
+ $html = godam_preview_page_content( 7 );
+
+ $this->assertSame( array( array( 'id' => '7' ) ), $this->player_atts() );
+ $this->assertStringContainsString( 'class="godam-player-double" data-id="7"', $html );
+ $this->assertArrayNotHasKey( 'shortcodes', $GLOBALS['rtgodam_stub'] );
+ }
+
+ /** The preview page renders an audio attachment through the audio shortcode's attributes. */
+ public function test_the_preview_page_renders_audio_from_attributes() {
+ $GLOBALS['rtgodam_stub']['mime'] = 'audio/mpeg';
+ $GLOBALS['rtgodam_stub']['posts'] = array(
+ 7 => new \WP_Post(
+ array(
+ 'ID' => 7,
+ 'post_type' => 'attachment',
+ 'post_mime_type' => 'audio/mpeg',
+ )
+ ),
+ );
+
+ $audio_atts = array();
+ add_shortcode(
+ 'godam_audio',
+ static function ( $atts ) use ( &$audio_atts ) {
+ $audio_atts[] = $atts;
+ return '';
+ }
+ );
+
+ $html = godam_preview_page_content( 7 );
+
+ $this->assertSame( array( array( 'id' => '7' ) ), $audio_atts );
+ $this->assertSame( array(), $this->player_atts() );
+ $this->assertStringContainsString( 'godam-audio-double', $html );
+ }
+}
diff --git a/tests/php/EverestFormsRecorderEntryPlayerAttributesTest.php b/tests/php/EverestFormsRecorderEntryPlayerAttributesTest.php
new file mode 100644
index 000000000..3eab7cd83
--- /dev/null
+++ b/tests/php/EverestFormsRecorderEntryPlayerAttributesTest.php
@@ -0,0 +1,124 @@
+ (object) array( 'meta' => array() ),
+ );
+ rtgodam_stub_register_player_and_marker();
+
+ // The single-entry screen.
+ $_GET['view-entry'] = '5';
+ $_GET['form_id'] = '3';
+
+ $this->field = ( new \ReflectionClass( Everest_Forms_Field_GoDAM_Video::class ) )->newInstanceWithoutConstructor();
+ }
+
+ protected function tearDown(): void {
+ unset( $GLOBALS['rtgodam_stub'], $_GET['view-entry'], $_GET['form_id'] );
+ parent::tearDown();
+ }
+
+ /**
+ * Render the entry value the way Everest Forms asks for it.
+ *
+ * @param string $value Stored recording URL.
+ * @return string
+ */
+ private function entry_markup( $value ) {
+ return $this->field->update_entry_meta_display_godam_recorder( 'unchanged', $value, array( 'godam_record_1' => $value ), 'entry-single' );
+ }
+
+ /** @return array[] Attributes each player render received. */
+ private function player_atts() {
+ return $GLOBALS['rtgodam_stub']['player_atts'];
+ }
+
+ /** A normal entry shows the player with the recording and the same attributes as before. */
+ public function test_a_normal_entry_shows_the_player_with_the_recording() {
+ $html = $this->entry_markup( self::RECORDING_URL );
+
+ $this->assertCount( 1, $this->player_atts() );
+ $this->assertSame( self::RECORDING_URL, $this->player_atts()[0]['src'] );
+ $this->assertSame( '', $this->player_atts()[0]['transcoded_url'] ?? '' );
+ $this->assertStringContainsString( 'evf-godam-video-preview', $html );
+ $this->assertStringContainsString( 'data-src="' . self::RECORDING_URL . '"', $html );
+ }
+
+ /** A transcoded copy stored with the entry is passed to the player and noted above it. */
+ public function test_a_transcoded_copy_is_passed_to_the_player() {
+ $GLOBALS['rtgodam_stub']['evf_entry']->meta['rtgodam_transcoded_url_everestforms_3_5'] = 'https://cdn.example.test/rec.mpd';
+
+ $html = $this->entry_markup( self::RECORDING_URL );
+
+ $this->assertSame( 'https://cdn.example.test/rec.mpd', $this->player_atts()[0]['transcoded_url'] );
+ $this->assertStringContainsString( 'godam-transcoded-url-info', $html );
+ }
+
+ /** Shortcode text in the stored recording is not run. */
+ public function test_shortcode_text_in_the_recording_is_not_run() {
+ $html = $this->entry_markup( self::PAYLOAD_URL );
+
+ $this->assertSame( 0, $GLOBALS['rtgodam_stub']['marker_runs'] );
+ $this->assertStringNotContainsString( 'GX-MARKER', $html );
+ $this->assertCount( 1, $this->player_atts() );
+ $this->assertSame( self::PAYLOAD_URL, $this->player_atts()[0]['src'] );
+ $this->assertArrayNotHasKey( 'shortcodes', $GLOBALS['rtgodam_stub'] );
+ }
+
+ /** Shortcode text in the stored transcoded URL is not run. */
+ public function test_shortcode_text_in_the_transcoded_url_is_not_run() {
+ $GLOBALS['rtgodam_stub']['evf_entry']->meta['rtgodam_transcoded_url_everestforms_3_5'] = "https://cdn.example.test/rec'][gx_marker]['.mpd";
+
+ $this->entry_markup( self::RECORDING_URL );
+
+ $this->assertSame( 0, $GLOBALS['rtgodam_stub']['marker_runs'] );
+ }
+
+ /** An audio recording is still shown with an audio element, not the player. */
+ public function test_an_audio_recording_still_uses_the_audio_element() {
+ $GLOBALS['rtgodam_stub']['evf_entry']->meta['rtgodam_transcoded_url_everestforms_3_5'] = 'https://cdn.example.test/audio.mp3';
+
+ $html = $this->entry_markup( 'https://example.test/wp-content/uploads/godam/everest/audio-1.webm' );
+
+ $this->assertSame( array(), $this->player_atts() );
+ $this->assertStringContainsString( '', $html );
+ }
+}
diff --git a/tests/php/FluentFormsRecorderEntryPlayerAttributesTest.php b/tests/php/FluentFormsRecorderEntryPlayerAttributesTest.php
new file mode 100644
index 000000000..7e2100959
--- /dev/null
+++ b/tests/php/FluentFormsRecorderEntryPlayerAttributesTest.php
@@ -0,0 +1,127 @@
+query_vars = array( 'rest_route' => '/fluentform/v1/submissions/9' );
+
+ $this->field = ( new \ReflectionClass( Recorder_Field::class ) )->newInstanceWithoutConstructor();
+ }
+
+ protected function tearDown(): void {
+ unset( $GLOBALS['rtgodam_stub'], $GLOBALS['wp'] );
+ parent::tearDown();
+ }
+
+ /**
+ * Render the entry the way Fluent Forms' admin screen asks for it.
+ *
+ * @param array $response The stored recorder value.
+ * @return string
+ */
+ private function entry_markup( array $response ) {
+ return $this->field->render_recorder_field_markup( $response, array(), 3, true );
+ }
+
+ /** @return array[] Attributes each player render received. */
+ private function player_atts() {
+ return $GLOBALS['rtgodam_stub']['player_atts'];
+ }
+
+ /** A normal entry shows the player with the recording URL and the same attributes as before. */
+ public function test_a_normal_entry_shows_the_player_with_the_recording() {
+ $html = $this->entry_markup( array( self::RECORDING_URL ) );
+
+ $this->assertSame(
+ array(
+ array(
+ 'src' => self::RECORDING_URL,
+ 'transcoded_url' => '',
+ 'aspectratio' => '',
+ ),
+ ),
+ $this->player_atts()
+ );
+ $this->assertStringContainsString( 'class="gf-godam-video-preview"', $html );
+ $this->assertStringContainsString( 'data-src="' . self::RECORDING_URL . '"', $html );
+ $this->assertStringContainsString( 'href="' . self::RECORDING_URL . '"', $html );
+ }
+
+ /** The entry uses the first value of the stored array. */
+ public function test_the_first_value_of_the_stored_array_is_used() {
+ $this->entry_markup( array( self::RECORDING_URL, 'https://example.test/other.webm' ) );
+
+ $this->assertSame( self::RECORDING_URL, $this->player_atts()[0]['src'] );
+ }
+
+ /** A transcoded copy is passed to the player and noted above it. */
+ public function test_a_transcoded_copy_is_passed_to_the_player() {
+ $GLOBALS['rtgodam_stub']['fluent_meta'] = (object) array( 'value' => 'https://cdn.example.test/rec.mpd' );
+
+ $html = $this->entry_markup( array( self::RECORDING_URL ) );
+
+ $this->assertSame( 'https://cdn.example.test/rec.mpd', $this->player_atts()[0]['transcoded_url'] );
+ $this->assertStringContainsString( 'godam-transcoded-url-info', $html );
+ }
+
+ /** Shortcode text in the stored recording is not run. */
+ public function test_shortcode_text_in_the_recording_is_not_run() {
+ $html = $this->entry_markup( array( self::PAYLOAD_URL ) );
+
+ $this->assertSame( 0, $GLOBALS['rtgodam_stub']['marker_runs'] );
+ $this->assertStringNotContainsString( 'GX-MARKER', $html );
+ $this->assertCount( 1, $this->player_atts() );
+ $this->assertSame( self::PAYLOAD_URL, $this->player_atts()[0]['src'] );
+ $this->assertArrayNotHasKey( 'shortcodes', $GLOBALS['rtgodam_stub'] );
+ }
+
+ /** Shortcode text in the stored transcoded URL is not run either. */
+ public function test_shortcode_text_in_the_transcoded_url_is_not_run() {
+ $GLOBALS['rtgodam_stub']['fluent_meta'] = (object) array( 'value' => "https://cdn.example.test/rec'][gx_marker]['.mpd" );
+
+ $this->entry_markup( array( self::RECORDING_URL ) );
+
+ $this->assertSame( 0, $GLOBALS['rtgodam_stub']['marker_runs'] );
+ }
+}
diff --git a/tests/php/GravityFormsRecorderEntryPlayerAttributesTest.php b/tests/php/GravityFormsRecorderEntryPlayerAttributesTest.php
new file mode 100644
index 000000000..8570c06be
--- /dev/null
+++ b/tests/php/GravityFormsRecorderEntryPlayerAttributesTest.php
@@ -0,0 +1,115 @@
+field = new GF_Field_GoDAM_Video();
+ $this->field->id = 4;
+ }
+
+ protected function tearDown(): void {
+ unset( $GLOBALS['rtgodam_stub'], $_GET['lid'] );
+ parent::tearDown();
+ }
+
+ /** @return array[] Attributes each player render received. */
+ private function player_atts() {
+ return $GLOBALS['rtgodam_stub']['player_atts'];
+ }
+
+ /** A normal entry shows the player with the recording and the same attributes as before. */
+ public function test_a_normal_entry_shows_the_player_with_the_recording() {
+ $html = $this->field->get_value_entry_detail( self::RECORDING_URL );
+
+ $this->assertSame(
+ array(
+ array(
+ 'src' => self::RECORDING_URL,
+ 'transcoded_url' => '',
+ 'aspectratio' => '',
+ ),
+ ),
+ $this->player_atts()
+ );
+ $this->assertStringContainsString( 'gf-godam-video-preview', $html );
+ $this->assertStringContainsString( 'data-src="' . self::RECORDING_URL . '"', $html );
+ }
+
+ /** A transcoded copy stored with the entry is passed to the player and noted above it. */
+ public function test_a_transcoded_copy_is_passed_to_the_player() {
+ $_GET['lid'] = '5';
+ $GLOBALS['rtgodam_stub']['gform_meta'] = array( 'rtgodam_transcoded_url_4_0' => 'https://cdn.example.test/rec.mpd' );
+
+ $html = $this->field->get_value_entry_detail( self::RECORDING_URL );
+
+ $this->assertSame( 'https://cdn.example.test/rec.mpd', $this->player_atts()[0]['transcoded_url'] );
+ $this->assertStringContainsString( 'godam-transcoded-url-info', $html );
+ }
+
+ /** Shortcode text in the stored recording is not run. */
+ public function test_shortcode_text_in_the_recording_is_not_run() {
+ $html = $this->field->get_value_entry_detail( self::PAYLOAD_URL );
+
+ $this->assertSame( 0, $GLOBALS['rtgodam_stub']['marker_runs'] );
+ $this->assertStringNotContainsString( 'GX-MARKER', $html );
+ $this->assertCount( 1, $this->player_atts() );
+ $this->assertSame( self::PAYLOAD_URL, $this->player_atts()[0]['src'] );
+ $this->assertArrayNotHasKey( 'shortcodes', $GLOBALS['rtgodam_stub'] );
+ }
+
+ /** Shortcode text in the stored transcoded URL is not run. */
+ public function test_shortcode_text_in_the_transcoded_url_is_not_run() {
+ $_GET['lid'] = '5';
+ $GLOBALS['rtgodam_stub']['gform_meta'] = array( 'rtgodam_transcoded_url_4_0' => "https://cdn.example.test/rec'][gx_marker]['.mpd" );
+
+ $this->field->get_value_entry_detail( self::RECORDING_URL );
+
+ $this->assertSame( 0, $GLOBALS['rtgodam_stub']['marker_runs'] );
+ }
+
+ /** A recording stored as a JSON list with one file shows the player for it. */
+ public function test_a_json_list_with_one_file_shows_the_player() {
+ $this->field->get_value_entry_detail( wp_json_encode( array( self::RECORDING_URL ) ) );
+
+ $this->assertSame( array( self::RECORDING_URL ), array_column( $this->player_atts(), 'src' ) );
+ }
+}
diff --git a/tests/php/RenderShortcodeFromAttsTest.php b/tests/php/RenderShortcodeFromAttsTest.php
new file mode 100644
index 000000000..840447a25
--- /dev/null
+++ b/tests/php/RenderShortcodeFromAttsTest.php
@@ -0,0 +1,104 @@
+assertSame( 1, $GLOBALS['rtgodam_stub']['marker_runs'] );
+ }
+
+ /** A value that looks like shortcode syntax reaches the callback as it is and runs nothing. */
+ public function test_shortcode_text_in_a_value_is_passed_as_data() {
+ $value = 'https://example.test/a.webm][gx_marker]';
+
+ $html = rtgodam_render_shortcode_from_atts( 'godam_video', array( 'src' => $value ) );
+
+ $this->assertSame( 0, $GLOBALS['rtgodam_stub']['marker_runs'] );
+ $this->assertStringNotContainsString( 'GX-MARKER', $html );
+ $this->assertSame( array( array( 'src' => $value ) ), $GLOBALS['rtgodam_stub']['player_atts'] );
+ }
+
+ /** Normal values come back out of the callback exactly as they went in, and the callback's output is returned. */
+ public function test_attributes_and_output_pass_through() {
+ $atts = array(
+ 'id' => '7',
+ 'src' => 'https://example.test/a.webm',
+ 'aspectratio' => '',
+ );
+
+ $html = rtgodam_render_shortcode_from_atts( 'godam_video', $atts );
+
+ $this->assertSame( array( $atts ), $GLOBALS['rtgodam_stub']['player_atts'] );
+ $this->assertStringContainsString( 'data-src="https://example.test/a.webm"', $html );
+ }
+
+ /** The tag is looked up in the shortcode registry, so a shortcode another plugin registered works too. */
+ public function test_the_callback_receives_the_tag() {
+ $received = array();
+ add_shortcode(
+ 'gx_other',
+ static function ( $atts, $content, $tag ) use ( &$received ) {
+ $received = array( $atts, $content, $tag );
+ return 'other';
+ }
+ );
+
+ $html = rtgodam_render_shortcode_from_atts( 'gx_other', array( 'a' => 'b' ) );
+
+ $this->assertSame( 'other', $html );
+ $this->assertSame( array( array( 'a' => 'b' ), '', 'gx_other' ), $received );
+ }
+
+ /** Rendering never goes through do_shortcode(), so no string is built or parsed. */
+ public function test_do_shortcode_is_not_called() {
+ rtgodam_render_shortcode_from_atts( 'godam_video', array( 'src' => 'https://example.test/a.webm' ) );
+
+ $this->assertArrayNotHasKey( 'shortcodes', $GLOBALS['rtgodam_stub'] );
+ }
+
+ /** A shortcode whose callback can no longer be called renders nothing. */
+ public function test_an_uncallable_shortcode_renders_nothing() {
+ add_shortcode( 'gx_broken', array( 'GxMissingClass', 'render' ) );
+
+ $this->assertSame( '', rtgodam_render_shortcode_from_atts( 'gx_broken', array( 'a' => 'b' ) ) );
+ }
+
+ /** An unregistered shortcode renders nothing, rather than printing the tag or its attributes. */
+ public function test_an_unregistered_shortcode_renders_nothing() {
+ $this->assertSame( '', rtgodam_render_shortcode_from_atts( 'gx_missing', array( 'src' => 'https://example.test/a.webm' ) ) );
+ }
+}
diff --git a/tests/php/SureFormsRecorderEntryPlayerAttributesTest.php b/tests/php/SureFormsRecorderEntryPlayerAttributesTest.php
new file mode 100644
index 000000000..02838f95c
--- /dev/null
+++ b/tests/php/SureFormsRecorderEntryPlayerAttributesTest.php
@@ -0,0 +1,113 @@
+ array( 'form_id' => 3 ),
+ );
+ rtgodam_stub_register_player_and_marker();
+
+ // The entry screen.
+ $_GET['entry_id'] = '5';
+
+ $this->submit = ( new \ReflectionClass( Form_Submit::class ) )->newInstanceWithoutConstructor();
+ }
+
+ protected function tearDown(): void {
+ unset( $GLOBALS['rtgodam_stub'], $_GET['entry_id'] );
+ parent::tearDown();
+ }
+
+ /** @return array[] Attributes each player render received. */
+ private function player_atts() {
+ return $GLOBALS['rtgodam_stub']['player_atts'];
+ }
+
+ /** A normal entry shows the player with the recording and the same attributes as before. */
+ public function test_a_normal_entry_shows_the_player_with_the_recording() {
+ $html = $this->submit->render_custom_field_markup( 'unchanged', self::RECORDING_URL );
+
+ $this->assertCount( 1, $this->player_atts() );
+ $this->assertSame( self::RECORDING_URL, $this->player_atts()[0]['src'] );
+ $this->assertSame( '', $this->player_atts()[0]['transcoded_url'] ?? '' );
+ $this->assertStringContainsString( 'srfm-godam-video-preview', $html );
+ $this->assertStringContainsString( 'data-src="' . self::RECORDING_URL . '"', $html );
+ }
+
+ /** A transcoded copy stored with the form is passed to the player and noted above it. */
+ public function test_a_transcoded_copy_is_passed_to_the_player() {
+ $GLOBALS['rtgodam_stub']['post_meta']['rtgodam_transcoded_url_sureforms_3_5'] = 'https://cdn.example.test/rec.mpd';
+
+ $html = $this->submit->render_custom_field_markup( 'unchanged', self::RECORDING_URL );
+
+ $this->assertSame( 'https://cdn.example.test/rec.mpd', $this->player_atts()[0]['transcoded_url'] );
+ $this->assertStringContainsString( 'godam-transcoded-url-info', $html );
+ }
+
+ /** Shortcode text in the stored recording is not run. */
+ public function test_shortcode_text_in_the_recording_is_not_run() {
+ $html = $this->submit->render_custom_field_markup( 'unchanged', self::PAYLOAD_URL );
+
+ $this->assertSame( 0, $GLOBALS['rtgodam_stub']['marker_runs'] );
+ $this->assertStringNotContainsString( 'GX-MARKER', $html );
+ $this->assertCount( 1, $this->player_atts() );
+ $this->assertSame( self::PAYLOAD_URL, $this->player_atts()[0]['src'] );
+ $this->assertArrayNotHasKey( 'shortcodes', $GLOBALS['rtgodam_stub'] );
+ }
+
+ /** Shortcode text in the stored transcoded URL is not run. */
+ public function test_shortcode_text_in_the_transcoded_url_is_not_run() {
+ $GLOBALS['rtgodam_stub']['post_meta']['rtgodam_transcoded_url_sureforms_3_5'] = "https://cdn.example.test/rec'][gx_marker]['.mpd";
+
+ $this->submit->render_custom_field_markup( 'unchanged', self::RECORDING_URL );
+
+ $this->assertSame( 0, $GLOBALS['rtgodam_stub']['marker_runs'] );
+ }
+
+ /** An audio recording is still shown with an audio element, not the player. */
+ public function test_an_audio_recording_still_uses_the_audio_element() {
+ $GLOBALS['rtgodam_stub']['post_meta']['rtgodam_transcoded_url_sureforms_3_5'] = 'https://cdn.example.test/audio.mp3';
+
+ $html = $this->submit->render_custom_field_markup( 'unchanged', 'https://example.test/wp-content/uploads/godam/sureforms/audio-1.webm' );
+
+ $this->assertSame( array(), $this->player_atts() );
+ $this->assertStringContainsString( '', $html );
+ }
+}
diff --git a/tests/php/VideoShortcodeRoutePlayerAttributesTest.php b/tests/php/VideoShortcodeRoutePlayerAttributesTest.php
new file mode 100644
index 000000000..13033725d
--- /dev/null
+++ b/tests/php/VideoShortcodeRoutePlayerAttributesTest.php
@@ -0,0 +1,254 @@
+ array(
+ 7 => new \WP_Post(
+ array(
+ 'ID' => 7,
+ 'post_type' => 'attachment',
+ )
+ ),
+ ),
+ 'post_meta' => array(
+ 'rtgodam_transcoded_url' => 'https://cdn.example.test/7/master.mpd',
+ 'rtgodam_hls_transcoded_url' => 'https://cdn.example.test/7/master.m3u8',
+ ),
+ 'attachment_url' => 'https://example.test/wp-content/uploads/sample.mp4',
+ 'mime' => 'video/mp4',
+ 'title' => 'Sample video',
+ );
+ rtgodam_stub_register_player_and_marker();
+
+ $this->route = ( new \ReflectionClass( Dynamic_Shortcode::class ) )->newInstanceWithoutConstructor();
+ }
+
+ protected function tearDown(): void {
+ unset( $GLOBALS['rtgodam_stub'] );
+ parent::tearDown();
+ }
+
+ /**
+ * Call the route the way WordPress does.
+ *
+ * @param array $params Request params.
+ * @return array The response data.
+ */
+ private function call( array $params ) {
+ $response = $this->route->render_shortcode( new \WP_REST_Request( $params ) );
+
+ return $response->get_data();
+ }
+
+ /** @return array[] Attributes each player render received. */
+ private function player_atts() {
+ return $GLOBALS['rtgodam_stub']['player_atts'];
+ }
+
+ /**
+ * The sources attribute as the player reads it, whatever way the placeholders were written.
+ *
+ * @param string $sources The attribute value.
+ * @return array
+ */
+ private function decoded_sources( $sources ) {
+ return json_decode( str_replace( array( '__rtgob__', '__rtgcb__' ), array( '[', ']' ), $sources ), true );
+ }
+
+ /** A normal call returns the player HTML and renders it with the same attributes as before. */
+ public function test_a_normal_call_renders_the_player_with_the_same_attributes() {
+ $data = $this->call(
+ array(
+ 'id' => 7,
+ 'engagements' => '',
+ 'godam_context' => 'godam-video-product-gallery',
+ )
+ );
+
+ $this->assertSame( 'success', $data['status'] );
+ $this->assertStringContainsString( 'class="godam-player-double" data-id="7"', $data['html'] );
+ $this->assertSame( 'Sample video', $data['title'] );
+
+ $this->assertCount( 1, $this->player_atts() );
+ $atts = $this->player_atts()[0];
+ $this->assertSame( '7', $atts['id'] );
+ $this->assertSame( '', $atts['engagements'] );
+ $this->assertSame( 'godam-video-product-gallery', $atts['godam_context'] );
+ $this->assertSame(
+ array(
+ array(
+ 'src' => 'https://cdn.example.test/7/master.mpd',
+ 'type' => 'application/dash+xml',
+ ),
+ array(
+ 'src' => 'https://cdn.example.test/7/master.m3u8',
+ 'type' => 'application/x-mpegURL',
+ ),
+ array(
+ 'src' => 'https://example.test/wp-content/uploads/sample.mp4',
+ 'type' => 'video/mp4',
+ ),
+ ),
+ $this->decoded_sources( $atts['sources'] )
+ );
+ }
+
+ /** A call without a context renders the player without one. */
+ public function test_a_call_without_a_context_has_no_context_attribute() {
+ $this->call(
+ array(
+ 'id' => 7,
+ 'engagements' => 'show',
+ )
+ );
+
+ $atts = $this->player_atts()[0];
+ $this->assertArrayNotHasKey( 'godam_context', $atts );
+ $this->assertSame( 'show', $atts['engagements'] );
+ }
+
+ /** Shortcode text appended to godam_context is not run. */
+ public function test_shortcode_text_in_godam_context_is_not_run() {
+ $data = $this->call(
+ array(
+ 'id' => 7,
+ 'engagements' => '',
+ 'godam_context' => 'x][gx_marker]',
+ )
+ );
+
+ $this->assertSame( 0, $GLOBALS['rtgodam_stub']['marker_runs'] );
+ $this->assertStringNotContainsString( 'GX-MARKER', $data['html'] );
+ $this->assertSame( 'success', $data['status'] );
+ $this->assertCount( 1, $this->player_atts() );
+ $this->assertSame( 'xgx_marker', $this->player_atts()[0]['godam_context'] );
+ }
+
+ /** The route argument reduces the value to slug characters before the callback runs. */
+ public function test_the_route_argument_is_sanitized_as_a_key() {
+ $routes = $this->route->get_rest_routes();
+ $arg = $routes[0]['args']['args']['godam_context'];
+
+ $this->assertSame( 'sanitize_key', $arg['sanitize_callback'] );
+ $this->assertSame( 'x_gx_marker', call_user_func( $arg['sanitize_callback'], 'X][_gx_marker]' ) );
+ }
+
+ /** A video the site does not have is still reported as not found, and nothing is rendered. */
+ public function test_a_missing_video_is_not_found() {
+ $response = $this->route->render_shortcode( new \WP_REST_Request( array( 'id' => 99 ) ) );
+
+ $this->assertSame( 404, $response->get_status() );
+ $this->assertSame( array(), $this->player_atts() );
+ }
+
+ /** The new attributes filter can change what the player renders with. */
+ public function test_the_attributes_filter_applies() {
+ add_filter(
+ 'rtgodam_shortcode_atts',
+ static function ( $atts ) {
+ $atts['godam_context'] = 'from-filter';
+ return $atts;
+ }
+ );
+
+ $this->call(
+ array(
+ 'id' => 7,
+ 'engagements' => '',
+ )
+ );
+
+ $this->assertSame( 'from-filter', $this->player_atts()[0]['godam_context'] );
+ }
+
+ /** A site that still filters the old shortcode string keeps working, and gets a deprecation notice. */
+ public function test_the_deprecated_shortcode_filter_still_applies() {
+ $seen = array();
+ add_filter(
+ 'rtgodam_shortcode_output',
+ static function ( $shortcode ) use ( &$seen ) {
+ $seen[] = $shortcode;
+ return substr( $shortcode, 0, -1 ) . " aspectratio='16:9']";
+ }
+ );
+
+ $data = $this->call(
+ array(
+ 'id' => 7,
+ 'engagements' => 'show',
+ 'godam_context' => 'godam-video-product-gallery',
+ )
+ );
+
+ $this->assertSame( 'success', $data['status'] );
+ $this->assertSame( array( 'rtgodam_shortcode_output' ), $GLOBALS['rtgodam_stub']['deprecated_hooks'] );
+ $this->assertCount( 1, $seen );
+ $this->assertStringStartsWith( "[godam_video id='7' engagements='show' sources='", $seen[0] );
+ $this->assertStringEndsWith( " godam_context='godam-video-product-gallery']", $seen[0] );
+
+ $atts = $this->player_atts()[0];
+ $this->assertSame( '16:9', $atts['aspectratio'] );
+ $this->assertSame( 'godam-video-product-gallery', $atts['godam_context'] );
+ $this->assertCount( 3, $this->decoded_sources( $atts['sources'] ) );
+ }
+
+ /** With the deprecated filter hooked, shortcode text in godam_context still does not run. */
+ public function test_shortcode_text_in_godam_context_is_not_run_with_the_deprecated_filter() {
+ add_filter(
+ 'rtgodam_shortcode_output',
+ static function ( $shortcode ) {
+ return $shortcode;
+ }
+ );
+
+ $data = $this->call(
+ array(
+ 'id' => 7,
+ 'engagements' => '',
+ 'godam_context' => "x'][gx_marker]['",
+ )
+ );
+
+ $this->assertSame( 0, $GLOBALS['rtgodam_stub']['marker_runs'] );
+ $this->assertStringNotContainsString( 'GX-MARKER', $data['html'] );
+ $this->assertSame( 'xgx_marker', $this->player_atts()[0]['godam_context'] );
+ }
+}
diff --git a/tests/php/WPFormsRecorderEntryPlayerAttributesTest.php b/tests/php/WPFormsRecorderEntryPlayerAttributesTest.php
new file mode 100644
index 000000000..a47827bc7
--- /dev/null
+++ b/tests/php/WPFormsRecorderEntryPlayerAttributesTest.php
@@ -0,0 +1,170 @@
+ array( 'rtgodam_transcoded_status_5_0' => 'in-progress' ),
+ );
+ rtgodam_stub_register_player_and_marker();
+ }
+
+ protected function tearDown(): void {
+ unset( $GLOBALS['rtgodam_stub'], $_GET['entry_id'] );
+ parent::tearDown();
+ }
+
+ /**
+ * Render the template WPForms includes on the View Entry page.
+ *
+ * @param string $value Stored recording URL.
+ * @return string
+ */
+ private function render_entry_view( $value ) {
+ $form_data = array( 'id' => 3 );
+ $field = array( 'id' => 5 );
+ $mime_type = 'video/webm';
+
+ $_GET['entry_id'] = '9';
+
+ ob_start();
+ include dirname( __DIR__, 2 ) . '/inc/classes/wpforms/wpforms-field-godam-record-entry-view.php';
+
+ return ob_get_clean();
+ }
+
+ /**
+ * Render the template WPForms includes on the Edit Entry page.
+ *
+ * @param string $value Stored recording URL.
+ * @return string
+ */
+ private function render_entry_edit( $value ) {
+ $field = array(
+ 'type' => 'godam_record',
+ 'properties' => array(
+ 'inputs' => array(
+ 'primary' => array(
+ 'id' => 'wpforms-field-5',
+ 'class' => array(),
+ 'data' => array(),
+ 'required' => '',
+ 'attr' => array( 'value' => $value ),
+ ),
+ ),
+ ),
+ );
+ $mime_type = 'video/webm';
+
+ ob_start();
+ include dirname( __DIR__, 2 ) . '/inc/classes/wpforms/wpforms-field-godam-record-entry-edit.php';
+
+ return ob_get_clean();
+ }
+
+ /** @return array[] Attributes each player render received. */
+ private function player_atts() {
+ return $GLOBALS['rtgodam_stub']['player_atts'];
+ }
+
+ /** The View Entry page shows the player with the recording and the same attributes as before. */
+ public function test_the_entry_view_shows_the_player_with_the_recording() {
+ $html = $this->render_entry_view( self::RECORDING_URL );
+
+ $this->assertSame(
+ array(
+ array(
+ 'poster' => '',
+ 'src' => self::RECORDING_URL,
+ 'transcoded_url' => '',
+ ),
+ ),
+ $this->player_atts()
+ );
+ $this->assertStringContainsString( 'data-src="' . self::RECORDING_URL . '"', $html );
+ }
+
+ /** A transcoded copy stored with the entry is passed to the player. */
+ public function test_the_entry_view_passes_the_transcoded_copy() {
+ $GLOBALS['rtgodam_stub']['wpforms_meta']['rtgodam_transcoded_url_5_0'] = 'https://cdn.example.test/rec.mpd';
+
+ $this->render_entry_view( self::RECORDING_URL );
+
+ $this->assertSame( 'https://cdn.example.test/rec.mpd', $this->player_atts()[0]['transcoded_url'] );
+ }
+
+ /** Shortcode text in the stored recording is not run on the View Entry page. */
+ public function test_shortcode_text_in_the_recording_is_not_run_on_the_entry_view() {
+ $html = $this->render_entry_view( self::PAYLOAD_URL );
+
+ $this->assertSame( 0, $GLOBALS['rtgodam_stub']['marker_runs'] );
+ $this->assertStringNotContainsString( 'GX-MARKER', $html );
+ $this->assertCount( 1, $this->player_atts() );
+ $this->assertSame( self::PAYLOAD_URL, $this->player_atts()[0]['src'] );
+ $this->assertArrayNotHasKey( 'shortcodes', $GLOBALS['rtgodam_stub'] );
+ }
+
+ /** Shortcode text in the stored transcoded URL is not run on the View Entry page. */
+ public function test_shortcode_text_in_the_transcoded_url_is_not_run_on_the_entry_view() {
+ $GLOBALS['rtgodam_stub']['wpforms_meta']['rtgodam_transcoded_url_5_0'] = "https://cdn.example.test/rec'][gx_marker]['.mpd";
+
+ $this->render_entry_view( self::RECORDING_URL );
+
+ $this->assertSame( 0, $GLOBALS['rtgodam_stub']['marker_runs'] );
+ }
+
+ /** The Edit Entry page shows the player with the recording and the same attributes as before. */
+ public function test_the_entry_edit_shows_the_player_with_the_recording() {
+ $html = $this->render_entry_edit( self::RECORDING_URL );
+
+ $this->assertSame(
+ array(
+ array(
+ 'poster' => '',
+ 'src' => self::RECORDING_URL,
+ 'transcoded_url' => '',
+ ),
+ ),
+ $this->player_atts()
+ );
+ $this->assertStringContainsString( 'data-src="' . self::RECORDING_URL . '"', $html );
+ }
+
+ /** Shortcode text in the stored recording is not run on the Edit Entry page. */
+ public function test_shortcode_text_in_the_recording_is_not_run_on_the_entry_edit() {
+ $html = $this->render_entry_edit( self::PAYLOAD_URL );
+
+ $this->assertSame( 0, $GLOBALS['rtgodam_stub']['marker_runs'] );
+ $this->assertStringNotContainsString( 'GX-MARKER', $html );
+ $this->assertCount( 1, $this->player_atts() );
+ $this->assertSame( self::PAYLOAD_URL, $this->player_atts()[0]['src'] );
+ }
+}
diff --git a/tests/stubs/gravity-forms-fields.php b/tests/stubs/gravity-forms-fields.php
index 7220a1fdb..62cfd3746 100644
--- a/tests/stubs/gravity-forms-fields.php
+++ b/tests/stubs/gravity-forms-fields.php
@@ -4,7 +4,8 @@
*
* GF_Field_FileUpload mirrors only the submission-files cache of Gravity Forms 2.9.18+:
* get_submission_files() returns what set_submission_files() stored, or the files the test
- * queued in $queued_files.
+ * queued in $queued_files. It also answers what the entry view reads from the field: its
+ * ID, get_modifiers() and get_download_url().
*
* @package GoDAM
*/
@@ -15,7 +16,15 @@
/**
* Stub of Gravity Forms' base field.
*/
- class GF_Field {}
+ class GF_Field {
+
+ /**
+ * The field ID.
+ *
+ * @var int
+ */
+ public $id = 0;
+ }
}
if ( ! class_exists( 'GF_Field_FileUpload' ) ) {
@@ -67,5 +76,26 @@ public function get_submission_files() {
public function set_submission_files( $files ) {
$this->stored_files = $files;
}
+
+ /**
+ * The merge-tag modifiers of the field; none in a test.
+ *
+ * @return string[]
+ */
+ public function get_modifiers() {
+ return array();
+ }
+
+ /**
+ * The download URL of an uploaded file; the stub links the file itself.
+ *
+ * @param string $file File URL.
+ * @param bool $force_download Whether to force a download. Unused.
+ *
+ * @return string
+ */
+ public function get_download_url( $file, $force_download = false ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- signature mirrors Gravity Forms.
+ return $file;
+ }
}
}
diff --git a/tests/stubs/player-entry-stubs.php b/tests/stubs/player-entry-stubs.php
new file mode 100644
index 000000000..51a49241e
--- /dev/null
+++ b/tests/stubs/player-entry-stubs.php
@@ -0,0 +1,330 @@
+...->first() returns, or null.
+ * - `evf_entry`: the object evf_get_entry() returns.
+ * - `srfm_entry`: the array SRFM\Inc\Database\Tables\Entries::get() returns.
+ * - `gform_meta`: Gravity Forms entry meta, by key.
+ * - `wpforms_meta`: WPForms entry meta values, by meta type.
+ * - `elementor_settings`: what the Elementor widget's get_settings_for_display() returns, by control.
+ *
+ * @package GoDAM
+ */
+
+// phpcs:disable Universal.Namespaces.DisallowCurlyBraceSyntax.Forbidden, Universal.Namespaces.DisallowDeclarationWithoutName.Forbidden, Universal.Files.SeparateFunctionsFromOO.Mixed, Universal.Namespaces.OneDeclarationPerFile.MultipleFound, Generic.Files.OneObjectStructurePerFile.MultipleFound, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound, WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid, Generic.CodeAnalysis.UnusedFunctionParameter.Found, Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- test stubs mirroring WP / third-party APIs.
+
+namespace FluentForm\App\Services\FormBuilder {
+
+ if ( ! class_exists( __NAMESPACE__ . '\BaseFieldManager' ) ) {
+ /**
+ * Fluent Forms' base field manager; the recorder field only needs it to exist.
+ */
+ class BaseFieldManager {}
+ }
+}
+
+namespace Elementor {
+
+ if ( ! class_exists( __NAMESPACE__ . '\Widget_Base' ) ) {
+ /**
+ * Elementor's widget base; a widget only reads its settings through it.
+ */
+ class Widget_Base {
+
+ /**
+ * @param string $setting Control name.
+ * @return mixed The value the test set for the control, or ''.
+ */
+ public function get_settings_for_display( $setting = '' ) {
+ return $GLOBALS['rtgodam_stub']['elementor_settings'][ $setting ] ?? '';
+ }
+ }
+ }
+}
+
+namespace SRFM\Inc\Database\Tables {
+
+ if ( ! class_exists( __NAMESPACE__ . '\Entries' ) ) {
+ /**
+ * SureForms' entries table.
+ */
+ class Entries {
+
+ /**
+ * @param int $entry_id Entry ID.
+ * @return array|null The entry the test set up.
+ */
+ public static function get( $entry_id ) {
+ return $GLOBALS['rtgodam_stub']['srfm_entry'] ?? null;
+ }
+ }
+ }
+}
+
+namespace {
+
+ if ( ! class_exists( 'EVF_Form_Fields_Upload' ) ) {
+ /**
+ * Everest Forms' upload field; the GoDAM field only needs it to exist.
+ */
+ class EVF_Form_Fields_Upload {}
+ }
+
+ if ( ! class_exists( 'GFCommon' ) ) {
+ /**
+ * Gravity Forms' common helpers.
+ */
+ class GFCommon {
+
+ /**
+ * @return bool
+ */
+ public static function is_ssl() {
+ return false;
+ }
+ }
+ }
+
+ /*
+ * Gravity Forms' field classes come from gravity-forms-fields.php, which the test requires
+ * itself; the helpers below are the ones the entry view reads.
+ */
+
+ if ( ! function_exists( 'rgar' ) ) {
+
+ /**
+ * @param array $source Array to read.
+ * @param string $name Key.
+ * @return mixed
+ */
+ function rgar( $source, $name ) {
+ return isset( $source[ $name ] ) ? $source[ $name ] : '';
+ }
+ }
+
+ if ( ! function_exists( 'rgget' ) ) {
+
+ /**
+ * @param string $name Query-string key.
+ * @return mixed
+ */
+ function rgget( $name ) {
+ return $_GET[ $name ] ?? ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- stub.
+ }
+ }
+
+ if ( ! function_exists( 'gform_get_meta' ) ) {
+
+ /**
+ * @param int $entry_id Entry ID.
+ * @param string $meta_key Meta key.
+ * @return mixed False when the test set nothing up for the key.
+ */
+ function gform_get_meta( $entry_id, $meta_key ) {
+ return $GLOBALS['rtgodam_stub']['gform_meta'][ $meta_key ] ?? false;
+ }
+ }
+
+ if ( ! function_exists( 'evf_get_entry' ) ) {
+
+ /**
+ * @param int $entry_id Entry ID.
+ * @param bool $meta Whether to load meta.
+ * @return object|false
+ */
+ function evf_get_entry( $entry_id, $meta = true ) {
+ return $GLOBALS['rtgodam_stub']['evf_entry'] ?? false;
+ }
+ }
+
+ if ( ! function_exists( 'wpFluent' ) ) {
+
+ /**
+ * A query builder that answers every chain with the `fluent_meta` object.
+ *
+ * @return object
+ */
+ function wpFluent() {
+ return new class() {
+
+ /**
+ * @param string $table Table name.
+ * @return $this
+ */
+ public function table( $table ) {
+ return $this;
+ }
+
+ /**
+ * @param string $column Column.
+ * @param mixed $value Value.
+ * @return $this
+ */
+ public function where( $column, $value ) {
+ return $this;
+ }
+
+ /**
+ * @return object|null
+ */
+ public function first() {
+ return $GLOBALS['rtgodam_stub']['fluent_meta'] ?? null;
+ }
+ };
+ }
+ }
+
+ if ( ! function_exists( 'wpforms' ) ) {
+
+ /**
+ * A WPForms instance whose entry meta answers from the `wpforms_meta` values.
+ *
+ * @return object
+ */
+ function wpforms() {
+ return (object) array(
+ 'entry_meta' => new class() {
+
+ /**
+ * @param array $args Query; only `type` is read.
+ * @return array|false One row per meta type the test set up.
+ */
+ public function get_meta( $args ) {
+ $value = $GLOBALS['rtgodam_stub']['wpforms_meta'][ $args['type'] ] ?? null;
+
+ return null === $value ? false : array( (object) array( 'data' => $value ) );
+ }
+ },
+ );
+ }
+ }
+
+ if ( ! function_exists( 'wpforms_html_attributes' ) ) {
+
+ /**
+ * @return string
+ */
+ function wpforms_html_attributes() {
+ return '';
+ }
+ }
+
+ if ( ! function_exists( 'wp_unslash' ) ) {
+
+ /**
+ * @param string|array $value Value to unslash.
+ * @return string|array
+ */
+ function wp_unslash( $value ) {
+ return is_array( $value ) ? array_map( 'wp_unslash', $value ) : stripslashes( (string) $value );
+ }
+ }
+
+ if ( ! function_exists( 'esc_html_e' ) ) {
+
+ /**
+ * @param string $text Text to translate.
+ * @param string $domain Text domain.
+ * @return void
+ */
+ function esc_html_e( $text, $domain = 'default' ) {
+ echo esc_html__( $text, $domain ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc_html__() escapes.
+ }
+ }
+
+ if ( ! function_exists( 'force_balance_tags' ) ) {
+
+ /**
+ * @param string $text Markup.
+ * @return string
+ */
+ function force_balance_tags( $text ) {
+ return $text;
+ }
+ }
+
+ if ( ! function_exists( 'wp_basename' ) ) {
+
+ /**
+ * @param string $path Path.
+ * @return string
+ */
+ function wp_basename( $path ) {
+ return basename( $path );
+ }
+ }
+
+ if ( ! function_exists( 'wp_check_filetype' ) ) {
+
+ /**
+ * Like core, takes the extension from the end of the whole name.
+ *
+ * @param string $filename File name or URL.
+ * @return array{ext: string|false, type: string|false}
+ */
+ function wp_check_filetype( $filename ) {
+ $types = array(
+ 'webm' => 'video/webm',
+ 'mp4' => 'video/mp4',
+ 'mp3' => 'audio/mpeg',
+ 'ogg' => 'audio/ogg',
+ );
+
+ if ( preg_match( '!\.(' . implode( '|', array_keys( $types ) ) . ')$!i', (string) $filename, $matches ) ) {
+ $ext = strtolower( $matches[1] );
+
+ return array(
+ 'ext' => $ext,
+ 'type' => $types[ $ext ],
+ );
+ }
+
+ return array(
+ 'ext' => false,
+ 'type' => false,
+ );
+ }
+ }
+
+ if ( ! function_exists( 'get_the_date' ) ) {
+
+ /**
+ * @param string $format Date format.
+ * @param mixed $post Post.
+ * @return string
+ */
+ function get_the_date( $format = '', $post = null ) {
+ return 'January 1, 2026';
+ }
+ }
+
+ if ( ! function_exists( 'sanitize_key' ) ) {
+
+ /**
+ * @param string $key Input.
+ * @return string
+ */
+ function sanitize_key( $key ) {
+ return preg_replace( '/[^a-z0-9_\-]/', '', strtolower( (string) $key ) );
+ }
+ }
+
+ if ( ! function_exists( 'esc_url_raw' ) ) {
+
+ /**
+ * @param string $url URL.
+ * @return string
+ */
+ function esc_url_raw( $url ) {
+ return (string) $url;
+ }
+ }
+}
diff --git a/tests/stubs/shortcode-runtime.php b/tests/stubs/shortcode-runtime.php
new file mode 100644
index 000000000..e017a33cf
--- /dev/null
+++ b/tests/stubs/shortcode-runtime.php
@@ -0,0 +1,173 @@
+&/\[\]\x00-\x20=]++)@', $content, $matches );
+ $tagnames = array_intersect( array_keys( $shortcode_tags ), $matches[1] );
+
+ if ( empty( $tagnames ) ) {
+ return $content;
+ }
+
+ return preg_replace_callback( '/' . get_shortcode_regex( $tagnames ) . '/', 'rtgodam_stub_do_shortcode_tag', $content );
+ }
+}
+
+if ( ! function_exists( 'apply_filters_deprecated' ) ) {
+
+ /**
+ * Runs a deprecated filter when something is hooked to it, and records the hook name.
+ *
+ * @param string $hook_name Hook name.
+ * @param array $args Arguments passed to the callbacks.
+ * @param string $version Version it was deprecated in.
+ * @param string $replacement Hook to use instead.
+ * @return mixed
+ */
+ function apply_filters_deprecated( $hook_name, $args, $version, $replacement = '' ) {
+ if ( ! has_filter( $hook_name ) ) {
+ return $args[0];
+ }
+
+ $GLOBALS['rtgodam_stub']['deprecated_hooks'][] = $hook_name;
+
+ return apply_filters( $hook_name, ...$args );
+ }
+}
+
+if ( ! function_exists( 'rtgodam_stub_register_player_and_marker' ) ) {
+
+ /**
+ * Start a case with only the player double and the marker shortcode registered.
+ *
+ * @return void
+ */
+ function rtgodam_stub_register_player_and_marker() {
+ $GLOBALS['shortcode_tags'] = array();
+ $GLOBALS['rtgodam_stub']['player_atts'] = array();
+ $GLOBALS['rtgodam_stub']['marker_runs'] = 0;
+
+ add_shortcode(
+ 'godam_video',
+ static function ( $atts ) {
+ $GLOBALS['rtgodam_stub']['player_atts'][] = $atts;
+
+ $atts = is_array( $atts ) ? $atts : array();
+
+ return '';
+ }
+ );
+
+ add_shortcode(
+ 'gx_marker',
+ static function () {
+ ++$GLOBALS['rtgodam_stub']['marker_runs'];
+
+ return 'GX-MARKER';
+ }
+ );
+ }
+}