admin_notice( sprintf( __( 'Flipset: Embed ID #%d not found.', 'flipset' ), $embed_id ) );
}
}
$atts = shortcode_atts(
array(
'id' => '',
'file' => '',
'width' => $settings['default_width'] ?? '100%',
'height' => $settings['default_height'] ?? '600px',
'mode' => 'pdf', // Reserved for add-ons: read it in flipset/pre_render_viewer.
'page' => '1',
'title' => '',
'theme' => $settings['default_theme'] ?? 'light',
'download' => '',
'print' => '',
'search' => '',
'theme_btn' => '',
'share' => '',
'shortcuts' => '',
'presentation' => '',
'fullscreen' => '',
'loading' => ! empty( $settings['lazy_loading'] ) ? 'lazy' : 'eager',
'hide' => '',
'hash' => 'true',
'view' => 'page',
'resume' => 'true',
),
$atts,
'flipset'
);
/**
* Filter: flipset/viewer_atts
*
* Override resolved shortcode/block attributes (every key of the
* shortcode_atts() list: id, file, width, height, mode, page, title, theme,
* download, print, search, theme_btn, share, shortcuts, presentation,
* fullscreen, loading, hide, hash, view, resume) without replacing the
* entire viewer. Runs after defaults are applied and before the
* empty-file check. See docs/HOOKS.md for the value lists.
*
* @param array $atts Resolved attributes.
*/
$atts = apply_filters( 'flipset/viewer_atts', $atts );
if ( ! is_array( $atts ) ) {
$atts = array();
}
// width and height end up in an inline style attribute: allow only a
// number with a CSS length unit, never free text.
$atts['width'] = self::sanitize_dimension( $atts['width'] ?? '', self::sanitize_dimension( $settings['default_width'] ?? '', '100%' ) );
$atts['height'] = self::sanitize_dimension( $atts['height'] ?? '', self::sanitize_dimension( $settings['default_height'] ?? '', '600px' ) );
if ( empty( $atts['file'] ) ) {
return $this->admin_notice( __( 'Flipset: no file URL was provided. Use [flipset file="https://example.com/document.pdf"].', 'flipset' ) );
}
/**
* Filter: flipset/pre_render_viewer
*
* Lets an add-on take over rendering completely — e.g. an EPUB
* viewer, or the same PDF viewer wrapped in its bookmark / reader
* toolbar — without this free class needing to know add-on
* internals. Return any non-null value to short-circuit the
* default PDF.js output below.
*
* @param string|null $output Default null (let free plugin render).
* @param array $atts Resolved shortcode/block attributes.
*/
$pro_output = apply_filters( 'flipset/pre_render_viewer', null, $atts );
if ( null !== $pro_output ) {
return wp_kses( (string) apply_filters( 'flipset/viewer_html', $pro_output, $atts ), self::allowed_html() );
}
$this->enqueue_assets();
$instance_id = 'flipset-viewer-' . wp_unique_id();
/**
* Action: flipset/before_viewer
*
* An add-on can hook here to inject a toolbar (bookmarks, reader
* fonts, etc.) above the base viewer without free plugin code
* needing to know the add-on exists beyond this hook.
*/
ob_start();
do_action( 'flipset/before_viewer', $instance_id, $atts );
$before = wp_kses( (string) ob_get_clean(), self::allowed_html() );
$toolbar = $this->render_toolbar( $instance_id, $atts, $settings );
$caption = '';
$title = isset( $atts['title'] ) ? sanitize_text_field( (string) $atts['title'] ) : '';
if ( '' !== $title ) {
$caption = sprintf( '
%s
', esc_html( $title ) );
}
$start_page = max( 1, absint( $atts['page'] ?? 1 ) );
$sync_hash = $this->att_flag( $atts['hash'] ?? 'true', true );
$resume = $this->att_flag( $atts['resume'] ?? 'true', true );
$view = strtolower( (string) ( $atts['view'] ?? 'page' ) );
if ( ! in_array( $view, array( 'page', 'scroll', 'spread', 'flip' ), true ) ) {
$view = 'page';
}
$initial_theme = strtolower( (string) ( $atts['theme'] ?? 'light' ) );
if ( ! in_array( $initial_theme, array( 'light', 'dark', 'sepia' ), true ) ) {
$initial_theme = 'light';
}
$is_lazy = 'lazy' === strtolower( (string) ( $atts['loading'] ?? 'eager' ) );
$branding = '';
// Off by default (WordPress.org guideline 10); an administrator opts in under Flipset → Settings.
if ( apply_filters( 'flipset/show_branding', ! empty( $settings['show_branding'] ) ) ) {
$branding = sprintf(
'%2$s
',
esc_url( 'https://flipset.thereadscope.com' ),
esc_html__( 'Powered by Flipset', 'flipset' )
);
}
$file_src = apply_filters( 'flipset/viewer_file_url', $atts['file'], $atts );
$noscript = sprintf(
'',
esc_url( $file_src ),
sprintf(
/* translators: %s: document title or fallback */
esc_html__( 'Open document: %s (PDF)', 'flipset' ),
esc_html( '' !== $title ? $title : wp_basename( (string) $file_src ) )
)
);
$modals = $this->render_modals();
$container = sprintf(
'',
esc_attr( $atts['width'] ),
esc_attr( $atts['height'] ),
$before,
$toolbar,
esc_attr( $instance_id ),
esc_url( $file_src ),
esc_attr__( 'PDF viewer', 'flipset' ),
$branding,
$caption,
esc_attr( (string) $start_page ),
$sync_hash ? '1' : '0',
esc_attr( $view ),
$resume ? '1' : '0',
esc_attr( $initial_theme ),
$is_lazy ? 'lazy' : 'eager',
$modals,
$noscript
);
/**
* Filter: flipset/viewer_html
*
* Last chance to wrap or replace the finished viewer markup.
* Also applied to non-null `flipset/pre_render_viewer` output.
*
* @param string $html Rendered viewer HTML.
* @param array $atts Resolved attributes.
*/
// Escape at the single point where the markup leaves the renderer, after
// every filter has had its say.
return wp_kses( (string) apply_filters( 'flipset/viewer_html', $container, $atts ), self::allowed_html() );
}
/**
* Chrome for the free PDF viewer. An add-on's toolbar is
* printed into the same .flipset-chrome row via flipset/before_viewer.
*
* @param string $instance_id Viewer element id.
* @param array $atts Resolved attributes.
* @param array $settings Plugin settings.
* @return string
*/
private function render_toolbar( $instance_id, $atts, $settings ) {
$file_url = isset( $atts['file'] ) ? (string) $atts['file'] : '';
$hide_list = array_filter( array_map( 'trim', explode( ',', strtolower( (string) ( $atts['hide'] ?? '' ) ) ) ) );
$show_nav = ! in_array( 'nav', $hide_list, true );
$show_zoom = ! in_array( 'zoom', $hide_list, true );
$show_fit = ! in_array( 'fit', $hide_list, true );
$show_rotate = ! in_array( 'rotate', $hide_list, true );
$show_view = ! in_array( 'view', $hide_list, true );
$show_pan = ! in_array( 'pan', $hide_list, true );
$download = ! in_array( 'download', $hide_list, true ) && $this->att_flag(
$atts['download'] ?? '',
! isset( $settings['allow_download'] ) || ! empty( $settings['allow_download'] )
);
$print = ! in_array( 'print', $hide_list, true ) && $this->att_flag(
$atts['print'] ?? '',
! isset( $settings['allow_print'] ) || ! empty( $settings['allow_print'] )
);
$search = ! in_array( 'search', $hide_list, true ) && $this->att_flag(
$atts['search'] ?? '',
! isset( $settings['allow_search'] ) || ! empty( $settings['allow_search'] )
);
$theme_btn = ! in_array( 'theme', $hide_list, true ) && $this->att_flag(
$atts['theme_btn'] ?? '',
! isset( $settings['allow_theme'] ) || ! empty( $settings['allow_theme'] )
);
$share = ! in_array( 'share', $hide_list, true ) && $this->att_flag(
$atts['share'] ?? '',
! isset( $settings['allow_share'] ) || ! empty( $settings['allow_share'] )
);
$shortcuts = ! in_array( 'shortcuts', $hide_list, true ) && $this->att_flag(
$atts['shortcuts'] ?? '',
! isset( $settings['allow_shortcuts'] ) || ! empty( $settings['allow_shortcuts'] )
);
$present = ! in_array( 'presentation', $hide_list, true ) && $this->att_flag(
$atts['presentation'] ?? '',
! isset( $settings['allow_presentation'] ) || ! empty( $settings['allow_presentation'] )
);
$fullscreen = ! in_array( 'fullscreen', $hide_list, true ) && $this->att_flag(
$atts['fullscreen'] ?? '',
! isset( $settings['allow_fullscreen'] ) || ! empty( $settings['allow_fullscreen'] )
);
$download_url = apply_filters( 'flipset/download_url', $file_url, $atts );
ob_start();
?>
→ ← / J K
+ -
0
R
D
/ or Ctrl+F
H
F
P
Home End
Esc
>
*/
public static function allowed_html() {
$g = array(
'class' => true,
'id' => true,
'hidden' => true,
'role' => true,
'tabindex' => true,
'title' => true,
'style' => true,
'lang' => true,
'dir' => true,
'data-*' => true,
'aria-label' => true,
'aria-labelledby' => true,
'aria-describedby' => true,
'aria-hidden' => true,
'aria-live' => true,
'aria-busy' => true,
'aria-expanded' => true,
'aria-pressed' => true,
'aria-controls' => true,
'aria-haspopup' => true,
'aria-current' => true,
'aria-modal' => true,
'aria-valuemin' => true,
'aria-valuemax' => true,
'aria-valuenow' => true,
);
$tags = array(
'div' => array(),
'span' => array(),
'p' => array(),
'nav' => array(),
'ul' => array(),
'li' => array(),
'h2' => array(),
'h3' => array(),
'strong' => array(),
'kbd' => array(),
'noscript' => array(),
'figure' => array(),
'a' => array(
'href' => true,
'target' => true,
'rel' => true,
'download' => true,
),
'button' => array(
'type' => true,
'disabled' => true,
'name' => true,
'value' => true,
),
'form' => array(
'method' => true,
'action' => true,
),
'label' => array(
'for' => true,
),
'input' => array(
'type' => true,
'name' => true,
'value' => true,
'placeholder' => true,
'min' => true,
'max' => true,
'checked' => true,
'readonly' => true,
'disabled' => true,
'autocomplete' => true,
),
'img' => array(
'src' => true,
'srcset' => true,
'sizes' => true,
'alt' => true,
'width' => true,
'height' => true,
'loading' => true,
'decoding' => true,
),
);
$allowed = array();
foreach ( $tags as $tag => $attrs ) {
$allowed[ $tag ] = array_merge( $g, $attrs );
}
$allowed = array_merge( $allowed, self::svg_allowed_html() );
/**
* Filter: flipset/allowed_html
*
* Add-ons that print extra markup inside the viewer or library output
* (through flipset/before_viewer or flipset/viewer_html) extend this list.
*
* @param array $allowed Tag => attributes map used with wp_kses().
*/
$filtered = apply_filters( 'flipset/allowed_html', $allowed );
return is_array( $filtered ) ? $filtered : $allowed;
}
/**
* Tags and attributes of the static line icons (inline SVG).
*
* @return array>
*/
public static function svg_allowed_html() {
$paint = array(
'class' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'stroke-linecap' => true,
'stroke-linejoin' => true,
'aria-hidden' => true,
);
return array(
'svg' => array_merge(
$paint,
array(
'xmlns' => true,
'width' => true,
'height' => true,
'viewbox' => true,
'focusable' => true,
)
),
'path' => array_merge( $paint, array( 'd' => true ) ),
'polyline' => array_merge( $paint, array( 'points' => true ) ),
'line' => array_merge(
$paint,
array(
'x1' => true,
'y1' => true,
'x2' => true,
'y2' => true,
)
),
'circle' => array_merge(
$paint,
array(
'cx' => true,
'cy' => true,
'r' => true,
)
),
'rect' => array_merge(
$paint,
array(
'x' => true,
'y' => true,
'width' => true,
'height' => true,
'rx' => true,
'ry' => true,
)
),
);
}
/**
* Print one toolbar icon, escaped at output.
*
* @param string $name Icon key.
*/
private function the_icon( $name ) {
echo wp_kses( $this->toolbar_icon( $name ), self::svg_allowed_html() );
}
/**
* Toolbar glyph markup for admin screens that show the same icons.
*
* @param string $name Icon key.
* @return string SVG markup.
*/
public function get_toolbar_icon( $name ) {
return $this->toolbar_icon( $name );
}
/**
* Inline 16px stroke icons for the viewer chrome.
*
* @param string $name Icon key.
* @return string
*/
private function toolbar_icon( $name ) {
$paths = array(
'prev' => '',
'next' => '',
'minus' => '',
'plus' => '',
'fit' => '',
'rotate' => '',
'search' => '',
'up' => '',
'down' => '',
'download' => '',
'print' => '',
'fs' => '',
'toc' => '',
'thumbs' => '',
'view' => '',
'pan' => '',
'more' => '',
'present' => '',
'theme' => '',
'share' => '',
'shortcuts' => '',
'close' => '',
'copy' => '',
'check' => '',
);
$body = $paths[ $name ] ?? '';
return '';
}
/**
* Strings shared by the front-end viewer and the settings live preview.
*
* @return array
*/
public static function viewer_i18n() {
return array(
'loading' => __( 'Loading document…', 'flipset' ),
/* translators: %s: percent loaded. */
'loadingPct' => __( 'Loading %s…', 'flipset' ),
'error' => __( 'This document could not be loaded.', 'flipset' ),
/* translators: 1: current page number, 2: total pages. */
'pageStatus' => __( 'Page %1$s of %2$s', 'flipset' ),
'passwordPrompt' => __( 'This document is password-protected.', 'flipset' ),
'passwordWrong' => __( 'Incorrect password.', 'flipset' ),
'passwordUnlock' => __( 'Unlock', 'flipset' ),
'passwordLabel' => __( 'Password', 'flipset' ),
'toc' => __( 'Table of contents', 'flipset' ),
'thumbs' => __( 'Page thumbnails', 'flipset' ),
'outlineNav' => __( 'Document outline', 'flipset' ),
'thumbsNav' => __( 'Page thumbnails', 'flipset' ),
'closePanel' => __( 'Close panel', 'flipset' ),
'fitPage' => __( 'Fit page', 'flipset' ),
'fitWidth' => __( 'Fit width', 'flipset' ),
/* translators: 1: current match, 2: total matches. */
'findStatus' => __( '%1$s of %2$s', 'flipset' ),
'findNone' => __( 'No matches', 'flipset' ),
'viewPage' => __( 'Single page', 'flipset' ),
'viewScroll' => __( 'Continuous scroll', 'flipset' ),
'viewSpread' => __( 'Two-page spread', 'flipset' ),
'viewFlip' => __( '3D FlipBook', 'flipset' ),
'pan' => __( 'Hand tool', 'flipset' ),
'select' => __( 'Text select', 'flipset' ),
'more' => __( 'More tools', 'flipset' ),
'present' => __( 'Presentation', 'flipset' ),
'copied' => __( 'Link copied!', 'flipset' ),
'clickToLoad' => __( 'Click to load document', 'flipset' ),
'corsTitle' => __( 'Unable to load document (Cross-Origin Restriction)', 'flipset' ),
/* translators: %s: remote host name */
'corsDesc' => __( 'This PDF is hosted on an external domain (%s) which restricts cross-origin access (CORS).', 'flipset' ),
'corsDownload' => __( 'Open / Download PDF directly', 'flipset' ),
'corsAdminHint' => __( 'Site Admin: To enable direct embedding, configure Access-Control-Allow-Origin headers on the host server or upload the PDF to your WordPress Media Library.', 'flipset' ),
);
}
/**
* Version of the bundled assets/vendor/pdfjs build. The capture snippet only
* accepts a global with this exact version as "ours", so a different
* plugin's PDF.js is never mistaken for Flipset's. Update it together with
* the vendor files.
*/
const PDFJS_VERSION = '3.11.174';
/**
* Run just BEFORE the bundle: remember whatever PDF.js another plugin
* already put on the page, because the bundle assigns window.pdfjsLib
* unconditionally.
*/
const PDFJS_BEFORE = '(function(w){var f=w.Flipset=w.Flipset||{};f._prevPdfjs={g:w.pdfjsLib,u:w["pdfjs-dist/build/pdf"]};})(window);';
/**
* Run just AFTER the bundle: keep a private reference to our own PDF.js
* (window.Flipset.pdfjsLib) and put window.pdfjsLib back exactly as it was
* before our bundle ran (another plugin's PDF.js, or unset). Flipset then
* leaves no global footprint and other plugins that look for a global
* PDF.js load their own. Each bundle is self-contained, so nothing is shared.
*/
const PDFJS_AFTER = '(function(w){var f=w.Flipset=w.Flipset||{},p=f._prevPdfjs||{},c=w.pdfjsLib,k="pdfjs-dist/build/pdf";delete f._prevPdfjs;if(!f.pdfjsLib&&c&&c!==p.g&&c.version==="' . self::PDFJS_VERSION . '"){f.pdfjsLib=c;if(p.g){w.pdfjsLib=p.g;}else{try{delete w.pdfjsLib;}catch(e){w.pdfjsLib=undefined;}}if(p.u){w[k]=p.u;}else{try{delete w[k];}catch(e){w[k]=undefined;}}}})(window);';
/**
* Register the bundled PDF.js once, with the capture snippets attached.
* Safe to call from any enqueue path; later calls are no-ops.
*/
public static function register_pdfjs() {
if ( wp_script_is( 'flipset-pdfjs', 'registered' ) ) {
return;
}
wp_register_script(
'flipset-pdfjs',
FLIPSET_URL . 'assets/vendor/pdfjs/pdf.min.js',
array(),
FLIPSET_VERSION,
true
);
wp_add_inline_script( 'flipset-pdfjs', self::PDFJS_BEFORE, 'before' );
wp_add_inline_script( 'flipset-pdfjs', self::PDFJS_AFTER, 'after' );
}
/**
* Enqueue the bundled PDF.js build + our thin wrapper script once per
* request.
*/
public function enqueue_assets() {
if ( $this->assets_enqueued ) {
return;
}
$this->assets_enqueued = true;
wp_enqueue_style(
'flipset-viewer',
FLIPSET_URL . 'assets/css/flipset-viewer.css',
array(),
FLIPSET_VERSION
);
self::register_pdfjs();
wp_enqueue_script( 'flipset-pdfjs' );
wp_enqueue_script(
'flipset-viewer',
FLIPSET_URL . 'assets/js/flipset-viewer.js',
array( 'flipset-pdfjs' ),
FLIPSET_VERSION,
true
);
wp_localize_script(
'flipset-viewer',
'FlipsetConfig',
array(
'workerSrc' => FLIPSET_URL . 'assets/vendor/pdfjs/pdf.worker.min.js',
'isPro' => Flipset_Loader::is_pro_active(),
'i18n' => self::viewer_i18n(),
)
);
}
private function admin_notice( $message ) {
if ( ! current_user_can( 'edit_posts' ) ) {
return '';
}
return '' . esc_html( $message ) . '
';
}
}