From 65e52ed04620f2e1731e1aba5e7ef32e99c1ee65 Mon Sep 17 00:00:00 2001 From: Subodh Rajpopat Date: Wed, 29 Jul 2026 17:07:18 +0530 Subject: [PATCH 1/6] Fix GoDAM 2.1 QA: non-PDF documents, audio overflow, stray media icon Document block (QA 3, 5, 6) render.php emitted `` for whatever file was selected. Given a non-PDF the browser gets a type it cannot display, so it either paints an empty box, suppressing the `` fallback and leaving nothing at all on screen, or hands the file to the download manager, which starts a download on every page load. Reproduced both: a .docx rendered a 0px-wide object with no fallback, and merely loading the page wrote the file into ~/Downloads without any click. PDF is the only format the block supports, so add godam_is_supported_document() and gate output on it. The shared render.php backs the block, the [godam_document] shortcode and the Elementor widget, so one guard covers all three front ends; each editor now shows an "unsupported format" notice instead of a silently empty element. Also close a hole in the block's selection guard, where a missing MIME type passed straight through. Audio player (QA 2) .godam-audio-player is a nowrap flex row, and the scrubber had flex:1 1 auto without min-width:0. input[type=range] carries a ~129px intrinsic min-content width, so the track refused to shrink. In a narrow card, such as a 3-column layout, play + track + time then needed 241px in a 198px row and the non-shrinking time label was pushed outside the card and the column. Matches how __body and __play in the same file already handle it. Elementor media control (QA 7) Elementor centres .eicon-video-camera over the media area as its empty-state affordance and never hides it, because its own control only ever sets `src`. Ours also sets a poster, which left that icon stranded on top of the thumbnail. Hide it while a poster is showing; keep it for the empty state. Rename "GoDAM Image" to "Image" in core, Elementor and WPBakery, matching the Video, Audio, Document and Video Gallery labels already in the GoDAM category. Tests godam_is_supported_document() is the single gate protecting the embed, so cover its edge cases: MIME beats a misleading .pdf URL, query strings and fragments, a deleted attachment falling through to the URL, and "pdf" appearing outside the extension. --- assets/src/blocks/godam-image/block.json | 2 +- assets/src/blocks/godam-pdf/edit.js | 69 +++++-- assets/src/blocks/godam-pdf/editor.scss | 47 +++++ assets/src/blocks/godam-pdf/render.php | 18 ++ assets/src/blocks/godam-pdf/style.scss | 19 ++ assets/src/css/_godam-audio-card.scss | 6 + assets/src/css/godam-elementor-preview.scss | 16 +- .../src/js/elementor/controls/godam-media.js | 7 + .../class-godam-document.php | 44 +++++ .../elementor-widgets/class-godam-image.php | 2 +- .../shortcodes/class-godam-document.php | 18 ++ .../class-wpb-godam-image.php | 2 +- inc/helpers/custom-functions.php | 45 +++++ tests/bootstrap.php | 30 +++ tests/php/DocumentSupportTest.php | 183 ++++++++++++++++++ 15 files changed, 483 insertions(+), 25 deletions(-) create mode 100644 tests/php/DocumentSupportTest.php diff --git a/assets/src/blocks/godam-image/block.json b/assets/src/blocks/godam-image/block.json index 158f83a79..421489ca9 100644 --- a/assets/src/blocks/godam-image/block.json +++ b/assets/src/blocks/godam-image/block.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3, "name": "godam/image", - "title": "GoDAM Image", + "title": "Image", "category": "media", "icon": "format-image", "description": "Display an image with interactive GoDAM hotspot and product layers.", diff --git a/assets/src/blocks/godam-pdf/edit.js b/assets/src/blocks/godam-pdf/edit.js index e4fc04e70..6c4ab4194 100644 --- a/assets/src/blocks/godam-pdf/edit.js +++ b/assets/src/blocks/godam-pdf/edit.js @@ -95,10 +95,10 @@ function PdfEdit( { const [ uploadingFile, setUploadingFile ] = useState( null ); const { createErrorNotice, createWarningNotice } = useDispatch( noticesStore ); - // Fetch attachment metadata — auto-generated cover and file size. - const { autoGeneratedCover, fileSize } = useSelect( ( select ) => { + // Fetch attachment metadata: auto-generated cover, file size and MIME type. + const { autoGeneratedCover, fileSize, mimeType } = useSelect( ( select ) => { if ( ! id ) { - return { autoGeneratedCover: '', fileSize: 0 }; + return { autoGeneratedCover: '', fileSize: 0, mimeType: '' }; } const media = select( coreStore ).getEntityRecord( 'postType', 'attachment', id ); return { @@ -109,6 +109,7 @@ function PdfEdit( { media?.meta?.rtgodam_media_pdf_thumbnail || '', fileSize: media?.media_details?.filesize || 0, + mimeType: media?.mime_type || '', }; }, [ id ] ); @@ -188,14 +189,33 @@ function PdfEdit( { return; } - // Validate MIME type — reject anything that isn't a PDF. + if ( isBlobURL( media.url ) ) { + // An in-flight upload: the MIME type is not populated yet, so validation + // is deferred to the follow-up call that carries the finished attachment. + setTemporaryURL( media.url ); + // Capture file name + size so the upload UI can display them. + setUploadingFile( { + name: media.filename || media.title || __( 'Document', 'godam' ), + size: media.filesizeInBytes || 0, + } ); + return; + } + + // Validate the format: reject anything that isn't a PDF. // The MediaUpload allowedTypes filter restricts the library UI, but a // runtime check is needed as a safety net (e.g. drag-and-drop bypasses it). // Use the MIME string, not `media.type`: library selections expose // `mime` ("application/pdf") while uploads expose `mime_type`; `media.type` // is the REST post type ("attachment") for uploads and would wrongly reject them. + // Testing for equality (rather than inequality) matters: a missing MIME type + // used to pass straight through, so fall back to the extension instead of + // letting an unknown format in. const mime = media.mime || media.mime_type || ''; - if ( mime && mime !== 'application/pdf' ) { + const isPdf = mime + ? 'application/pdf' === mime + : /\.pdf(?:[?#]|$)/i.test( media.url || '' ); + + if ( ! isPdf ) { createWarningNotice( __( 'Only PDF files can be attached to this block. Please select a PDF document.', 'godam' ), { type: 'snackbar', isDismissible: true }, @@ -203,16 +223,6 @@ function PdfEdit( { return; } - if ( isBlobURL( media.url ) ) { - setTemporaryURL( media.url ); - // Capture file name + size so the upload UI can display them. - setUploadingFile( { - name: media.filename || media.title || __( 'Document', 'godam' ), - size: media.filesizeInBytes || 0, - } ); - return; - } - // Normalize the description to a string. Library selections expose it as // a plain string, but freshly uploaded files come from the REST API where // `description` is an object ( { raw, rendered } ). Storing the object @@ -285,6 +295,18 @@ function PdfEdit( { const isCardView = previewMode === 'card'; const hasDocument = !! ( src || temporaryURL ); + // Content saved before the format was validated (or added by URL) can still hold + // a non-PDF. The front end renders nothing for those, so flag it here rather than + // showing an that just paints blank. + // Only flag on positive evidence: a loaded attachment whose MIME is not PDF, or, + // for a URL-only document, a URL that is not a .pdf. Staying quiet while the + // attachment record is still loading avoids flashing the notice on a good PDF. + const isUnsupported = hasDocument && ! temporaryURL && ( + id + ? ( !! mimeType && 'application/pdf' !== mimeType ) + : ! /\.pdf(?:[?#]|$)/i.test( src || '' ) + ); + const classes = clsx( className, { 'is-transient': !! temporaryURL, [ `is-preview-${ previewMode || 'default' }` ]: true, @@ -384,6 +406,20 @@ function PdfEdit( { ); } + // ── Editor canvas: unsupported format ───────────────────────────────────── + const UnsupportedCanvas = ( +
+ +

+ { __( 'Unsupported file format', 'godam' ) } +

+

+ { __( 'Only PDF files are supported. Replace this file with a PDF; it will not be shown on your page.', 'godam' ) } +

+

{ fileName }

+
+ ); + // ── Editor canvas: Default View (embedded PDF) ──────────────────────────── // Wrap in when the block isn't focused so the embedded PDF // doesn't capture clicks/scroll, making the block selectable normally. @@ -675,7 +711,8 @@ function PdfEdit( { { /* ── Block canvas ─────────────────────────────────────────────── */ }
- { isCardView ? CardViewCanvas : DefaultViewCanvas } + { /* eslint-disable-next-line no-nested-ternary */ } + { isUnsupported ? UnsupportedCanvas : ( isCardView ? CardViewCanvas : DefaultViewCanvas ) } pointing at a file the browser cannot display, + * which either paints an empty box (the fallback is suppressed, so the + * visitor sees nothing) or starts a file download on every page load. Emit + * nothing instead; the editors surface an "unsupported format" notice so the + * author can see and fix it. + * + * Checked against the original attachment id / src rather than $godam_sources[0], + * because that may be a transcoded URL whose extension no longer reflects the + * source file. A numeric id resolves via its stored MIME type, so $godam_src is + * only consulted for GoDAM tab media and URL-only documents, exactly the cases + * where $godam_src is guaranteed set (see the source-building block above). + */ +if ( ! godam_is_supported_document( $godam_attachment_id, $godam_src ) ) { + return; +} + $godam_file_name = basename( $godam_sources[0] ); // Root wrapper: in block context merge WordPress' block-support attributes diff --git a/assets/src/blocks/godam-pdf/style.scss b/assets/src/blocks/godam-pdf/style.scss index 355f59ee9..3402c45a9 100644 --- a/assets/src/blocks/godam-pdf/style.scss +++ b/assets/src/blocks/godam-pdf/style.scss @@ -124,3 +124,22 @@ } } } + +// ── Unsupported-format notice ─────────────────────────────────────────────── +// Shown only inside WPBakery's front-end editor (see GoDAM_Document::render()). +// The published page renders nothing at all for a non-PDF, so visitors never +// see this. +.godam-document-unsupported { + padding: 16px 20px; + border: 1px dashed #c3c4c7; + border-radius: 8px; + background: #f6f7f7; + text-align: center; + + p { + margin: 0; + font-size: 13px; + line-height: 1.5; + color: #646970; + } +} diff --git a/assets/src/css/_godam-audio-card.scss b/assets/src/css/_godam-audio-card.scss index 10a86558d..f177ada81 100644 --- a/assets/src/css/_godam-audio-card.scss +++ b/assets/src/css/_godam-audio-card.scss @@ -124,6 +124,12 @@ &__scrubber { flex: 1 1 auto; + // input[type=range] carries an intrinsic min-content width (~129px in + // Chrome), and the flex default of min-width:auto pins the track to it. In a + // narrow card, such as a 3-column layout, play + track + time then need more + // room than the row has, and the non-shrinking time label is pushed outside + // the card and the column. Allow the track to shrink so the row always fits. + min-width: 0; appearance: none; -webkit-appearance: none; padding: 0; diff --git a/assets/src/css/godam-elementor-preview.scss b/assets/src/css/godam-elementor-preview.scss index c63127f5b..1884b7527 100644 --- a/assets/src/css/godam-elementor-preview.scss +++ b/assets/src/css/godam-elementor-preview.scss @@ -16,12 +16,13 @@ * the relevant element. */ -/* ── Empty-state placeholders ─────────────────────────────────────────────── */ +/* ── Empty-state / unsupported-state placeholders ─────────────────────────── */ .godam-image-elementor-empty, .godam-video-elementor-empty, .godam-audio-elementor-empty, -.godam-gallery-elementor-empty { +.godam-gallery-elementor-empty, +.godam-document-elementor-unsupported { display: flex; flex-direction: column; align-items: center; @@ -42,7 +43,8 @@ .godam-image-elementor-empty__title, .godam-video-elementor-empty__title, .godam-audio-elementor-empty__title, -.godam-gallery-elementor-empty__title { +.godam-gallery-elementor-empty__title, +.godam-document-elementor-unsupported__title { margin: 0; font-size: 15px; font-weight: 600; @@ -52,7 +54,8 @@ .godam-image-elementor-empty__desc, .godam-video-elementor-empty__desc, .godam-audio-elementor-empty__desc, -.godam-gallery-elementor-empty__desc { +.godam-gallery-elementor-empty__desc, +.godam-document-elementor-unsupported__desc { margin: 0; max-width: 340px; font-size: 13px; @@ -84,8 +87,9 @@ aspect-ratio: 16 / 9; } -/* Audio: a small glyph (no framed box). */ -.godam-audio-elementor-empty__icon { +/* Audio / Document: a small glyph (no framed box). */ +.godam-audio-elementor-empty__icon, +.godam-document-elementor-unsupported__icon { width: 40px; height: 40px; margin-bottom: 4px; diff --git a/assets/src/js/elementor/controls/godam-media.js b/assets/src/js/elementor/controls/godam-media.js index 94aac1571..61b0f4104 100644 --- a/assets/src/js/elementor/controls/godam-media.js +++ b/assets/src/js/elementor/controls/godam-media.js @@ -115,6 +115,13 @@ if ( window.elementor ) { } else if ( 'video' === mediaType ) { this.ui.mediaVideo.attr( 'poster', icon ?? '' ); this.ui.mediaVideo.attr( 'src', url ); + + // Elementor centres .eicon-video-camera over the media area as the + // empty-state affordance and never hides it, because its own control + // only ever sets `src`, never a poster. We do set one, so without + // this the icon is left stranded on top of the thumbnail. Hide it + // whenever a poster is showing, and keep it for the empty state. + this.$el.find( '.eicon-video-camera' ).toggle( ! icon ); } else if ( 'string' === typeof value ) { const fileName = url ? url.split( '/' ).pop() : ''; this.ui.fileName.text( fileName ); diff --git a/inc/classes/elementor-widgets/class-godam-document.php b/inc/classes/elementor-widgets/class-godam-document.php index 8645e9a89..507ee8742 100644 --- a/inc/classes/elementor-widgets/class-godam-document.php +++ b/inc/classes/elementor-widgets/class-godam-document.php @@ -189,6 +189,14 @@ protected function render() { return; } + // PDF is the only supported format. The shared render.php emits nothing for + // anything else, which would leave a silently empty widget, so flag it in + // the editor instead, where the author can act on it. + if ( ! godam_is_supported_document( isset( $document_file['id'] ) ? $document_file['id'] : 0, $document_file['url'] ) ) { + $this->render_unsupported_state(); + return; + } + $doc_title = $this->get_settings_for_display( 'doc_title' ) ?? ''; $description = $this->get_settings_for_display( 'description' ) ?? ''; $preview_mode = $this->get_settings_for_display( 'preview_mode' ) ?? 'default'; @@ -218,4 +226,40 @@ protected function render() { // its own script/style and returns escaped HTML. echo \RTGODAM\Inc\Shortcodes\GoDAM_Document::get_instance()->render( $shortcode_atts ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- render() returns escaped HTML. } + + /** + * Show an "unsupported format" notice for a non-PDF document. + * + * Only rendered while editing or previewing in Elementor: on the published page + * the widget outputs nothing at all, so visitors never see this. + * + * @since n.e.x.t + * + * @access protected + */ + protected function render_unsupported_state() { + if ( ! class_exists( '\Elementor\Plugin' ) ) { + return; + } + + $plugin = \Elementor\Plugin::$instance; + $is_editing = isset( $plugin->editor ) && $plugin->editor->is_edit_mode(); + $is_previewing = isset( $plugin->preview ) && $plugin->preview->is_preview_mode(); + if ( ! $is_editing && ! $is_previewing ) { + return; + } + + $icon_style = '--godam-preview-icon:url(' . esc_url( RTGODAM_URL . 'assets/images/godam-pdf.svg' ) . ');'; + ?> +
+ +

+ +

+

+ +

+
+ 'godam-image', - 'title' => _x( 'GoDAM Image', 'Widget Title', 'godam' ), + 'title' => _x( 'Image', 'Widget Title', 'godam' ), 'icon' => 'godam-eicon-image', 'categories' => array( 'godam' ), 'keywords' => array( 'godam', 'image', 'hotspot', 'shoppable' ), diff --git a/inc/classes/shortcodes/class-godam-document.php b/inc/classes/shortcodes/class-godam-document.php index 359239b18..755cbfb8f 100644 --- a/inc/classes/shortcodes/class-godam-document.php +++ b/inc/classes/shortcodes/class-godam-document.php @@ -61,6 +61,24 @@ public function render( $atts ) { return ''; } + // PDF is the only supported format; render.php emits nothing for anything + // else. In WPBakery's front-end editor that would leave a silently empty + // element, so show the author why. Callers that own their own editor + // messaging (the Elementor widget) check before reaching this point. + if ( ! godam_is_supported_document( $atts['id'], $atts['src'] ) ) { + if ( function_exists( 'vc_is_inline' ) && vc_is_inline() ) { + // The notice is styled by the block's stylesheet, which is normally + // enqueued further down, so enqueue it here too since we return early. + wp_enqueue_style( 'godam-pdf-style' ); + + return '

' + . esc_html__( 'Only PDF files are supported. This file will not be shown on your page.', 'godam' ) + . '

'; + } + + return ''; + } + // Map the flat shortcode atts to the block attribute shape expected by // the shared render.php (booleans / ints normalized). $attributes = array( diff --git a/inc/classes/wpbakery-elements/class-wpb-godam-image.php b/inc/classes/wpbakery-elements/class-wpb-godam-image.php index 7c6fc0b13..7ef67f421 100644 --- a/inc/classes/wpbakery-elements/class-wpb-godam-image.php +++ b/inc/classes/wpbakery-elements/class-wpb-godam-image.php @@ -63,7 +63,7 @@ public function godam_image() { vc_map( array( - 'name' => esc_html__( 'GoDAM Image', 'godam' ), + 'name' => esc_html__( 'Image', 'godam' ), 'base' => 'godam_image', 'category' => esc_html__( 'GoDAM', 'godam' ), 'description' => esc_html__( 'Image with interactive GoDAM hotspot & product layers', 'godam' ), diff --git a/inc/helpers/custom-functions.php b/inc/helpers/custom-functions.php index ed452e216..0e6f7d743 100644 --- a/inc/helpers/custom-functions.php +++ b/inc/helpers/custom-functions.php @@ -706,6 +706,51 @@ function godam_is_audio_file( $file_path_or_url ) { return false; } +/** + * Check whether a document can be embedded by the Document block. + * + * PDF is the only format the block supports. It embeds through + * ``, so pointing that at anything else gives the + * browser a type it cannot display: it paints an empty box (and suppresses the + * `` fallback content, so nothing at all is shown) or hands the file to + * the download manager, which starts a download on every page load. + * + * Callers use this to skip front-end output entirely and to show an "unsupported + * format" notice in the editors instead. + * + * The attachment's stored MIME type is authoritative. The URL extension is only a + * fallback, for GoDAM tab media whose id is not a local numeric attachment and for + * documents added by URL alone. + * + * @since n.e.x.t + * + * @param int|string $attachment_id Attachment ID, or a non-numeric GoDAM media id. + * @param string $url Document URL. Used when no local attachment is available. + * + * @return bool True when the document is a PDF and can be embedded, false otherwise. + */ +function godam_is_supported_document( $attachment_id = 0, $url = '' ) { + if ( ! empty( $attachment_id ) && is_numeric( $attachment_id ) ) { + $mime_type = get_post_mime_type( intval( $attachment_id ) ); + + if ( ! empty( $mime_type ) ) { + return 'application/pdf' === $mime_type; + } + + // Attachment no longer exists; fall through to the URL check so content + // that still carries a valid PDF URL keeps rendering. + } + + if ( empty( $url ) || ! is_string( $url ) ) { + return false; + } + + // Drop any query string / fragment before reading the extension. + $path = wp_parse_url( $url, PHP_URL_PATH ); + + return 'pdf' === strtolower( pathinfo( ! empty( $path ) ? $path : $url, PATHINFO_EXTENSION ) ); +} + /** * Send Video file to GoDAM for transcoding. * diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 38697c7a7..1f3d9da2a 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -18,6 +18,20 @@ define( 'ABSPATH', __DIR__ . '/' ); } +// WP core time constants, needed by the cache-TTL defines in custom-functions.php. +if ( ! defined( 'MINUTE_IN_SECONDS' ) ) { + define( 'MINUTE_IN_SECONDS', 60 ); +} +if ( ! defined( 'HOUR_IN_SECONDS' ) ) { + define( 'HOUR_IN_SECONDS', 60 * MINUTE_IN_SECONDS ); +} +if ( ! defined( 'DAY_IN_SECONDS' ) ) { + define( 'DAY_IN_SECONDS', 24 * HOUR_IN_SECONDS ); +} +if ( ! defined( 'WEEK_IN_SECONDS' ) ) { + define( 'WEEK_IN_SECONDS', 7 * DAY_IN_SECONDS ); +} + // Minimal stub so the parsing helper can run outside WordPress. if ( ! function_exists( 'wp_strip_all_tags' ) ) { @@ -113,6 +127,18 @@ function date_i18n( $format, $timestamp = null ) { } } +if ( ! function_exists( 'wp_parse_url' ) ) { + + /** + * @param string $url URL to parse. + * @param int $component Component to retrieve, per parse_url(). + * @return mixed + */ + function wp_parse_url( $url, $component = -1 ) { + return parse_url( $url, $component ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- this stub is what wp_parse_url() wraps. + } +} + // Empty stand-in for the WP core base so Video_Editor's parent chain resolves // without a WordPress install; the constructor is never run (tests build the // object with ReflectionClass::newInstanceWithoutConstructor()). @@ -125,3 +151,7 @@ class WP_REST_Controller {} // phpcs:ignore Universal.Files.SeparateFunctionsFro require_once dirname( __DIR__ ) . '/inc/classes/rest-api/class-video-editor.php'; require_once dirname( __DIR__ ) . '/inc/classes/rest-api/class-onboarding-response.php'; + +// Helper functions under test (godam_is_supported_document). The file only +// declares functions plus a few guarded define()s, so it is safe to load here. +require_once dirname( __DIR__ ) . '/inc/helpers/custom-functions.php'; diff --git a/tests/php/DocumentSupportTest.php b/tests/php/DocumentSupportTest.php new file mode 100644 index 000000000..2c1a12e41 --- /dev/null +++ b/tests/php/DocumentSupportTest.php @@ -0,0 +1,183 @@ +`, so a + * non-PDF gives the browser a type it cannot display: it paints an empty box (and + * suppresses the `` fallback, so nothing at all appears) or hands the file + * to the download manager, starting a download on every page load. This helper is + * the single gate that keeps a non-PDF from reaching that markup, so its edge + * cases are worth pinning down. + * + * get_post_mime_type() is stubbed in tests/bootstrap.php and driven by + * $GLOBALS['rtgodam_stub']['mime']. + * + * @package GoDAM + */ + +namespace RTGODAM\Tests; + +use PHPUnit\Framework\TestCase; + +/** + * @covers ::godam_is_supported_document + */ +class DocumentSupportTest extends TestCase { + + protected function tearDown(): void { + unset( $GLOBALS['rtgodam_stub'] ); + parent::tearDown(); + } + + /** + * Point the stubbed get_post_mime_type() at a given MIME type. + * + * @param string $mime MIME type to report for any attachment id. + * @return void + */ + private function stub_mime( $mime ) { + $GLOBALS['rtgodam_stub'] = array( 'mime' => $mime ); + } + + /** + * A numeric attachment id resolves via its stored MIME type, which wins over + * whatever the URL happens to look like. + * + * @return void + */ + public function test_attachment_mime_type_is_authoritative() { + $this->stub_mime( 'application/pdf' ); + $this->assertTrue( + godam_is_supported_document( 201, 'https://example.com/report.pdf' ), + 'A PDF attachment should be supported.' + ); + + // A .pdf URL must not rescue an attachment that is not actually a PDF: + // this is the case QA hit, where a .docx rendered as a blank embed. + $this->stub_mime( 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ); + $this->assertFalse( + godam_is_supported_document( 405, 'https://example.com/looks-like.pdf' ), + 'A DOCX attachment must be rejected even when the URL ends in .pdf.' + ); + } + + /** + * Other media types that share the media library are rejected too. + * + * @return void + */ + public function test_other_mime_types_are_rejected() { + foreach ( array( 'audio/wav', 'video/mp4', 'image/jpeg', 'text/plain' ) as $mime ) { + $this->stub_mime( $mime ); + $this->assertFalse( + godam_is_supported_document( 406, '' ), + $mime . ' must not be treated as an embeddable document.' + ); + } + } + + /** + * With no local attachment (GoDAM tab media keyed by a non-numeric id, or a + * document added by URL alone) the extension is the only signal available. + * + * @return void + */ + public function test_falls_back_to_url_extension_without_a_numeric_id() { + $this->stub_mime( '' ); + + $this->assertTrue( + godam_is_supported_document( 0, 'https://cdn.example.com/media/report.pdf' ), + 'A .pdf URL should be supported when there is no attachment to check.' + ); + $this->assertTrue( + godam_is_supported_document( 'abc123', 'https://cdn.example.com/media/report.PDF' ), + 'The extension check should be case-insensitive.' + ); + $this->assertFalse( + godam_is_supported_document( 'abc123', 'https://cdn.example.com/media/notes.docx' ), + 'A .docx URL should be rejected.' + ); + } + + /** + * Query strings and fragments are common on CDN URLs and must not be mistaken + * for part of the extension. + * + * @return void + */ + public function test_query_string_and_fragment_are_ignored() { + $this->stub_mime( '' ); + + $this->assertTrue( + godam_is_supported_document( 0, 'https://cdn.example.com/report.pdf?v=2&token=abc' ), + 'A query string should not defeat the extension check.' + ); + $this->assertTrue( + godam_is_supported_document( 0, 'https://cdn.example.com/report.pdf#page=3' ), + 'A fragment should not defeat the extension check.' + ); + $this->assertFalse( + godam_is_supported_document( 0, 'https://cdn.example.com/notes.docx?download=1' ), + 'A non-PDF with a query string should still be rejected.' + ); + } + + /** + * A deleted attachment reports no MIME type; content that still carries a valid + * PDF URL should keep rendering rather than silently disappearing. + * + * @return void + */ + public function test_missing_attachment_falls_through_to_the_url() { + $this->stub_mime( '' ); + + $this->assertTrue( + godam_is_supported_document( 999999, 'https://example.com/still-here.pdf' ), + 'A stale id with a valid PDF URL should remain supported.' + ); + $this->assertFalse( + godam_is_supported_document( 999999, 'https://example.com/still-here.docx' ), + 'A stale id with a non-PDF URL should be rejected.' + ); + } + + /** + * Nothing to inspect means nothing to embed. + * + * @return void + */ + public function test_empty_and_malformed_input_is_rejected() { + $this->stub_mime( '' ); + + $this->assertFalse( godam_is_supported_document(), 'No arguments should be rejected.' ); + $this->assertFalse( godam_is_supported_document( 0, '' ), 'Empty id and URL should be rejected.' ); + $this->assertFalse( godam_is_supported_document( 0, null ), 'A null URL should be rejected.' ); + $this->assertFalse( godam_is_supported_document( 0, array( 'x' ) ), 'A non-string URL should be rejected.' ); + $this->assertFalse( + godam_is_supported_document( 0, 'https://example.com/no-extension' ), + 'A URL with no extension should be rejected.' + ); + $this->assertFalse( + godam_is_supported_document( 0, 'not a url at all' ), + 'A malformed URL should be rejected.' + ); + } + + /** + * "pdf" appearing somewhere other than the extension must not pass. + * + * @return void + */ + public function test_pdf_elsewhere_in_the_url_does_not_pass() { + $this->stub_mime( '' ); + + $this->assertFalse( + godam_is_supported_document( 0, 'https://example.com/pdf/notes.docx' ), + 'A "pdf" directory should not make a .docx supported.' + ); + $this->assertFalse( + godam_is_supported_document( 0, 'https://example.com/my-pdf-report.zip' ), + '"pdf" inside the file name should not make a .zip supported.' + ); + } +} From 66b9bcfe96d9ebc0faa0dc2f8fb292d7eafb5747 Mon Sep 17 00:00:00 2001 From: Subodh Rajpopat Date: Wed, 29 Jul 2026 19:26:54 +0530 Subject: [PATCH 2/6] Fix audio play button losing its shape to theme button resets The play control is a