asset_ver( 'assets/css/flipset-admin-ui.css' ) ); wp_enqueue_style( 'flipset-admin', FLIPSET_URL . 'assets/css/flipset-admin.css', array( 'flipset-admin-ui' ), $this->asset_ver( 'assets/css/flipset-admin.css' ) ); if ( $is_dashboard ) { wp_enqueue_style( 'flipset-dashboard', FLIPSET_URL . 'assets/css/flipset-dashboard.css', array( 'flipset-admin-ui' ), $this->asset_ver( 'assets/css/flipset-dashboard.css' ) ); } if ( $is_settings ) { wp_enqueue_style( 'flipset-settings', FLIPSET_URL . 'assets/css/flipset-settings.css', array( 'flipset-admin-ui' ), $this->asset_ver( 'assets/css/flipset-settings.css' ) ); } if ( $is_docs ) { wp_enqueue_style( 'flipset-docs', FLIPSET_URL . 'assets/css/flipset-docs.css', array( 'flipset-admin-ui' ), $this->asset_ver( 'assets/css/flipset-docs.css' ) ); } if ( $is_settings || $is_builder ) { wp_enqueue_style( 'flipset-viewer', FLIPSET_URL . 'assets/css/flipset-viewer.css', array(), $this->asset_ver( 'assets/css/flipset-viewer.css' ) ); Flipset_Viewer::register_pdfjs(); wp_enqueue_script( 'flipset-pdfjs' ); wp_enqueue_script( 'flipset-viewer', FLIPSET_URL . 'assets/js/flipset-viewer.js', array( 'flipset-pdfjs' ), $this->asset_ver( 'assets/js/flipset-viewer.js' ), 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' => Flipset_Viewer::viewer_i18n(), ) ); } if ( $is_docs && current_user_can( 'upload_files' ) ) { Flipset_Viewer::register_pdfjs(); wp_enqueue_script( 'flipset-pdfjs' ); wp_enqueue_script( 'flipset-thumbnail-gen', FLIPSET_URL . 'assets/js/flipset-thumbnail-gen.js', array( 'flipset-pdfjs' ), $this->asset_ver( 'assets/js/flipset-thumbnail-gen.js' ), true ); wp_localize_script( 'flipset-thumbnail-gen', 'FlipsetThumbGen', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'flipset_admin' ), 'workerSrc' => FLIPSET_URL . 'assets/vendor/pdfjs/pdf.worker.min.js', 'maxPages' => 40, 'maxChars' => 80000, 'i18n' => array( 'indexed' => __( 'Indexed', 'flipset' ), 'noText' => __( 'No text', 'flipset' ), 'tagged' => __( 'Tagged', 'flipset' ), 'untagged' => __( 'Untagged', 'flipset' ), 'untaggedHint' => __( 'This PDF has no accessibility tags. Export it from Word, InDesign, or Acrobat as a tagged PDF so screen readers can follow headings and reading order.', 'flipset' ), 'taggedHint' => __( 'Tagged PDF. Screen readers can follow its structure.', 'flipset' ), 'indexedHint' => __( 'Text extracted. WordPress search can find words in this PDF.', 'flipset' ), 'noTextHint' => __( 'No selectable text was found (a scanned image PDF), so it cannot be searched.', 'flipset' ), ), ) ); } if ( $is_dashboard || $is_docs || $is_builder ) { wp_enqueue_media(); } wp_enqueue_script( 'flipset-admin', FLIPSET_URL . 'assets/js/flipset-admin.js', array(), $this->asset_ver( 'assets/js/flipset-admin.js' ), true ); wp_localize_script( 'flipset-admin', 'FlipsetAdmin', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'flipset_admin' ), 'i18n' => array( 'selectPdf' => __( 'Select PDF', 'flipset' ), 'usePdf' => __( 'Use this PDF', 'flipset' ), 'copied' => __( 'Copied!', 'flipset' ), 'copiedToast' => __( 'Shortcode copied', 'flipset' ), 'codeCopied' => __( 'Code snippet copied to clipboard', 'flipset' ), 'dismissed' => __( 'Dismissed', 'flipset' ), /* translators: 1: completed steps, 2: total steps. */ 'stepsComplete' => __( '%1$s of %2$s steps complete', 'flipset' ), 'setupDone' => __( 'You are set. New documents go in Documents or the Media Library.', 'flipset' ), 'dropHint' => __( 'Drop a PDF to embed it', 'flipset' ), 'uploading' => __( 'Uploading…', 'flipset' ), 'creating' => __( 'Creating page…', 'flipset' ), 'testPageReady' => __( 'Test page ready', 'flipset' ), 'createFail' => __( 'Could not create the test page.', 'flipset' ), 'copyShortcode' => __( 'Copy shortcode', 'flipset' ), 'notPdf' => __( 'Please drop a PDF file.', 'flipset' ), 'uploadFail' => __( 'Upload failed. Try the Media Library.', 'flipset' ), 'presetApplied' => __( 'Preset applied', 'flipset' ), 'selectPdfFirst' => __( 'Please select a PDF file.', 'flipset' ), 'previewEmpty' => __( 'Select or upload a PDF above to see live interactive preview.', 'flipset' ), 'saving' => __( 'Saving…', 'flipset' ), 'updateEmbed' => __( 'Update Embed & Copy', 'flipset' ), 'savedCopiedBtn' => __( '✓ Saved! Copied', 'flipset' ), /* translators: %s: shortcode that was copied. */ 'toastSaved' => __( 'Saved! %s copied to clipboard.', 'flipset' ), 'saveFailed' => __( 'Could not save embed.', 'flipset' ), 'testPageFailed' => __( 'Could not create test page.', 'flipset' ), /* translators: %s: text that was copied. */ 'copiedItem' => __( 'Copied: %s', 'flipset' ), 'embedDuplicated' => __( 'Embed duplicated successfully.', 'flipset' ), 'duplicateFailed' => __( 'Could not duplicate embed.', 'flipset' ), /* translators: %s: embed ID. */ 'confirmDelete' => __( 'Are you sure you want to delete this embed? Any shortcode using id="%s" will no longer work.', 'flipset' ), 'embedDeleted' => __( 'Embed deleted.', 'flipset' ), 'deleteFailed' => __( 'Could not delete embed.', 'flipset' ), 'selectCover' => __( 'Choose a cover image', 'flipset' ), 'useCover' => __( 'Use as cover', 'flipset' ), 'coverSaved' => __( 'Cover updated', 'flipset' ), 'coverReset' => __( 'Cover reset to the first page', 'flipset' ), 'coverFailed' => __( 'Could not change the cover.', 'flipset' ), ), 'uploadNonce' => wp_create_nonce( 'media-form' ), 'canUpload' => current_user_can( 'upload_files' ), ) ); if ( $is_settings ) { wp_enqueue_script( 'flipset-settings', FLIPSET_URL . 'assets/js/flipset-settings.js', array( 'flipset-viewer' ), $this->asset_ver( 'assets/js/flipset-settings.js' ), true ); wp_localize_script( 'flipset-settings', 'FlipsetSettings', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'flipset_admin' ), 'i18n' => array( /* translators: 1: number of switches turned on, 2: number of switches in the section. */ 'countOn' => __( '%1$s/%2$s on', 'flipset' ), 'saved' => __( 'Settings saved', 'flipset' ), 'confirmReset' => __( 'Reset this section to the default values? Nothing is saved until you click Save changes.', 'flipset' ), 'previewFail' => __( 'Could not load the preview.', 'flipset' ), /* translators: %s: configured viewer height, for example 600px. */ 'heightChip' => __( 'Height %s · scaled to fit', 'flipset' ), 'loading' => __( 'Loading preview…', 'flipset' ), ), ) ); } } /** * Asset version query. With WP_DEBUG on, the file's modification time is * added so edited admin assets are never served from the browser cache. * * @param string $relative Path inside the plugin, e.g. assets/css/flipset-docs.css. * @return string */ private function asset_ver( $relative ) { $file = FLIPSET_DIR . ltrim( $relative, '/' ); if ( defined( 'WP_DEBUG' ) && WP_DEBUG && file_exists( $file ) ) { return FLIPSET_VERSION . '.' . filemtime( $file ); } return FLIPSET_VERSION; } public function register_settings() { register_setting( self::OPTION_GROUP, self::OPTION_NAME, array( $this, 'sanitize_settings' ) ); add_settings_section( 'flipset_general', __( 'Viewer Defaults', 'flipset' ), '__return_false', self::PAGE_SLUG ); add_settings_field( 'default_width', __( 'Default width', 'flipset' ), array( $this, 'field_default_width' ), self::PAGE_SLUG, 'flipset_general' ); add_settings_field( 'default_height', __( 'Default height', 'flipset' ), array( $this, 'field_default_height' ), self::PAGE_SLUG, 'flipset_general' ); add_settings_field( 'allow_download', __( 'Download button', 'flipset' ), array( $this, 'field_allow_download' ), self::PAGE_SLUG, 'flipset_general' ); add_settings_field( 'allow_print', __( 'Print button', 'flipset' ), array( $this, 'field_allow_print' ), self::PAGE_SLUG, 'flipset_general' ); add_settings_field( 'default_theme', __( 'Default theme', 'flipset' ), array( $this, 'field_default_theme' ), self::PAGE_SLUG, 'flipset_general' ); add_settings_field( 'lazy_loading', __( 'Lazy loading', 'flipset' ), array( $this, 'field_lazy_loading' ), self::PAGE_SLUG, 'flipset_general' ); add_settings_field( 'show_branding', __( 'Credit link', 'flipset' ), array( $this, 'field_show_branding' ), self::PAGE_SLUG, 'flipset_general' ); add_settings_section( 'flipset_toolbar', __( 'Toolbar Controls', 'flipset' ), array( $this, 'section_toolbar' ), self::PAGE_SLUG ); add_settings_field( 'allow_search', __( 'Search in document', 'flipset' ), array( $this, 'field_allow_search' ), self::PAGE_SLUG, 'flipset_toolbar' ); add_settings_field( 'allow_theme', __( 'Theme switcher', 'flipset' ), array( $this, 'field_allow_theme' ), self::PAGE_SLUG, 'flipset_toolbar' ); add_settings_field( 'allow_share', __( 'Share button', 'flipset' ), array( $this, 'field_allow_share' ), self::PAGE_SLUG, 'flipset_toolbar' ); add_settings_field( 'allow_shortcuts', __( 'Keyboard shortcuts guide', 'flipset' ), array( $this, 'field_allow_shortcuts' ), self::PAGE_SLUG, 'flipset_toolbar' ); add_settings_field( 'allow_fullscreen', __( 'Full screen button', 'flipset' ), array( $this, 'field_allow_fullscreen' ), self::PAGE_SLUG, 'flipset_toolbar' ); add_settings_field( 'allow_presentation', __( 'Presentation button', 'flipset' ), array( $this, 'field_allow_presentation' ), self::PAGE_SLUG, 'flipset_toolbar' ); add_settings_section( 'flipset_discover', __( 'Search and sharing', 'flipset' ), array( $this, 'section_discover' ), self::PAGE_SLUG ); add_settings_field( 'index_pdf_text', __( 'Site search', 'flipset' ), array( $this, 'field_index_pdf_text' ), self::PAGE_SLUG, 'flipset_discover' ); add_settings_field( 'social_preview', __( 'Social preview', 'flipset' ), array( $this, 'field_social_preview' ), self::PAGE_SLUG, 'flipset_discover' ); add_settings_field( 'check_pdf_a11y', __( 'Accessibility tags', 'flipset' ), array( $this, 'field_check_pdf_a11y' ), self::PAGE_SLUG, 'flipset_discover' ); add_settings_field( 'autoembed_url', __( 'Direct PDF auto-embed', 'flipset' ), array( $this, 'field_autoembed_url' ), self::PAGE_SLUG, 'flipset_discover' ); add_settings_field( 'embed_attachment_pages', __( 'Attachment pages', 'flipset' ), array( $this, 'field_embed_attachment_pages' ), self::PAGE_SLUG, 'flipset_discover' ); } /** * Bound only to OPTION_NAME (`flipset_settings`). Returning a fresh * array of free-plugin keys means extra POST keys cannot persist * here, and a separately registered option such as * `flipset_pro_settings` is never read or written. * * @param mixed $input Raw submitted option value. * @return array Sanitized settings. */ public function sanitize_settings( $input ) { $existing = get_option( self::OPTION_NAME, array() ); if ( ! is_array( $existing ) ) { $existing = array(); } if ( ! is_array( $input ) ) { $input = array(); } // Only the Settings screen posts the marker. Every other write // (activation, AJAX, add-ons, the add_option() re-run of this // callback) is programmatic: keys it omits keep their current value, // or their default when never stored, instead of turning off. $is_form = ! empty( $input[ self::FORM_MARKER ] ); $flag_defaults = $this->flag_defaults(); $valid_themes = array( 'light', 'dark', 'sepia' ); $theme = isset( $input['default_theme'] ) && in_array( $input['default_theme'], $valid_themes, true ) ? $input['default_theme'] : ( $existing['default_theme'] ?? 'light' ); $clean = array( 'default_width' => isset( $input['default_width'] ) ? $this->sanitize_css_dimension( $input['default_width'], $existing['default_width'] ?? '100%' ) : ( $existing['default_width'] ?? '100%' ), 'default_height' => isset( $input['default_height'] ) ? $this->sanitize_css_dimension( $input['default_height'], $existing['default_height'] ?? '600px' ) : ( $existing['default_height'] ?? '600px' ), 'default_theme' => $theme, ); foreach ( $flag_defaults as $key => $default ) { // 'setup_copied_shortcode' is never a form field. if ( $is_form && 'setup_copied_shortcode' !== $key ) { $clean[ $key ] = ! empty( $input[ $key ] ); } elseif ( array_key_exists( $key, $input ) ) { $clean[ $key ] = ! empty( $input[ $key ] ); } elseif ( array_key_exists( $key, $existing ) ) { $clean[ $key ] = ! empty( $existing[ $key ] ); } else { $clean[ $key ] = $default; } } return $clean; } /** * Validate a user-supplied CSS dimension value. Accepts common units: * px, %, em, rem, vh, vw, svh, dvh, lvh. Returns $default on failure. * * @param string $value Raw value from the settings form. * @param string $default Fallback if the value is not a valid CSS dimension. * @return string */ private function sanitize_css_dimension( $value, $default ) { $value = trim( sanitize_text_field( (string) $value ) ); // Allow a positive number followed by a recognised CSS length unit. if ( preg_match( '/^\d+(\.\d+)?(px|%|em|rem|vh|vw|svh|dvh|lvh)$/', $value ) ) { return $value; } // Also allow bare "0" (no unit required for zero). if ( '0' === $value ) { return $value; } return $default; } public function section_toolbar() { echo '

' . esc_html__( 'Enable or disable specific buttons across all embedded PDF viewers. Individual embeds can also customize controls via the "hide" attribute.', 'flipset' ) . '

'; } public function field_default_theme() { $this->render_control( 'default_theme' ); } public function field_show_branding() { $this->render_control( 'show_branding' ); } public function field_lazy_loading() { $this->render_control( 'lazy_loading' ); } public function field_allow_search() { $this->render_control( 'allow_search' ); } public function field_allow_theme() { $this->render_control( 'allow_theme' ); } public function field_allow_share() { $this->render_control( 'allow_share' ); } public function field_allow_shortcuts() { $this->render_control( 'allow_shortcuts' ); } public function field_allow_fullscreen() { $this->render_control( 'allow_fullscreen' ); } public function field_allow_presentation() { $this->render_control( 'allow_presentation' ); } public function field_default_width() { $this->render_control( 'default_width' ); } public function field_default_height() { $this->render_control( 'default_height' ); } public function field_allow_download() { $this->render_control( 'allow_download' ); } public function field_allow_print() { $this->render_control( 'allow_print' ); } public function field_index_pdf_text() { $this->render_control( 'index_pdf_text' ); } public function field_social_preview() { $this->render_control( 'social_preview' ); } public function field_check_pdf_a11y() { $this->render_control( 'check_pdf_a11y' ); } public function field_autoembed_url() { $this->render_control( 'autoembed_url' ); } public function field_embed_attachment_pages() { $this->render_control( 'embed_attachment_pages' ); } public function section_discover() { echo '

' . esc_html__( 'Make embedded PDFs findable in WordPress search and nicer when a page is shared.', 'flipset' ) . '

'; } public function render_dashboard() { if ( ! current_user_can( 'manage_options' ) ) { return; } $pretty = (bool) get_option( 'permalink_structure' ); $pdf_count = $this->count_pdfs(); $page_count = $this->count_embeds(); $saved_embeds = Flipset_Embeds::get_all(); $saved_count = count( $saved_embeds ); $recent_saved = array_slice( $saved_embeds, 0, 5 ); $settings = get_option( self::OPTION_NAME, array() ); $copied = ! empty( $settings['setup_copied_shortcode'] ); $recent = $this->get_recent_pdfs( 4 ); $prefill = ! empty( $recent[0] ) ? $recent[0] : null; $prefill_url = $prefill ? $prefill['url'] : ''; $prefill_code = $prefill_url ? sprintf( '[flipset file="%s"]', esc_url_raw( $prefill_url ) ) : ''; $steps = array( 'uploaded' => $pdf_count > 0, 'copied' => $copied, 'embedded' => $page_count > 0, ); $done = count( array_filter( $steps ) ); $total_steps = count( $steps ); $percent = (int) round( ( $done / $total_steps ) * 100 ); $setup_done = $done === $total_steps; $docs_url = admin_url( 'admin.php?page=' . self::DOCS_SLUG ); $guide_url = admin_url( 'admin.php?page=' . self::DOCS_SLUG . '&tab=docs' ); $settings_url = admin_url( 'admin.php?page=' . self::SETTINGS_SLUG ); $builder_url = admin_url( 'admin.php?page=' . self::BUILDER_SLUG ); $embeds_url = add_query_arg( 'view', 'embeds', $builder_url ); $open_url = ( $prefill && ! empty( $prefill['id'] ) ) ? add_query_arg( 'pdf_id', (int) $prefill['id'], $builder_url ) : $builder_url; $show_welcome = ! $setup_done && ! get_user_meta( get_current_user_id(), 'flipset_welcome_dismissed', true ); $title_attr = $prefill ? $prefill['title'] : ''; ?>

$item ) : ?>

→

*/ private function flag_defaults() { return array( 'lazy_loading' => false, 'show_branding' => false, 'allow_download' => true, 'allow_print' => true, 'allow_search' => true, 'allow_theme' => true, 'allow_share' => true, 'allow_shortcuts' => true, 'allow_fullscreen' => true, 'allow_presentation' => true, 'index_pdf_text' => true, 'social_preview' => true, 'check_pdf_a11y' => true, 'autoembed_url' => true, 'embed_attachment_pages' => true, 'setup_copied_shortcode' => false, ); } /** * Every field the Settings form posts, with its default value. * * @return array */ private function settings_defaults() { $flags = $this->flag_defaults(); unset( $flags['setup_copied_shortcode'] ); return array_merge( array( 'default_width' => '100%', 'default_height' => '600px', 'default_theme' => 'light', ), $flags ); } /** * Presentation data for the Settings screen: control type, label, help * text, per-embed override hint and toolbar icon for each field. Labels * match the add_settings_field() titles. * * @return array> */ private function settings_schema() { return array( 'default_width' => array( 'type' => 'dimension', 'label' => __( 'Default width', 'flipset' ), 'help' => __( 'Width of every viewer. Use a number with a unit, for example 100% or 800px.', 'flipset' ), 'presets' => array( '100%', '800px', '960px' ), ), 'default_height' => array( 'type' => 'dimension', 'label' => __( 'Default height', 'flipset' ), 'help' => __( 'Height of the reading area, for example 600px or 80vh.', 'flipset' ), 'presets' => array( '420px', '600px', '80vh' ), ), 'default_theme' => array( 'type' => 'theme', 'label' => __( 'Default theme', 'flipset' ), 'help' => __( 'Default reader color scheme for visitors.', 'flipset' ), ), 'lazy_loading' => array( 'type' => 'switch', 'label' => __( 'Lazy loading', 'flipset' ), 'help' => __( 'Only load PDF documents when they scroll into the visitor viewport (faster initial page load).', 'flipset' ), ), 'allow_download' => array( 'type' => 'switch', 'label' => __( 'Download button', 'flipset' ), 'help' => __( 'Show the download button on the viewer toolbar.', 'flipset' ), 'hint' => 'download="false"', ), 'allow_print' => array( 'type' => 'switch', 'label' => __( 'Print button', 'flipset' ), 'help' => __( 'Show the print button on the viewer toolbar.', 'flipset' ), 'hint' => 'print="false"', ), 'show_branding' => array( 'type' => 'switch', 'label' => __( 'Credit link', 'flipset' ), 'help' => __( 'Show a small “Powered by Flipset” link under each viewer. Off by default. Turn it on to support the plugin.', 'flipset' ), ), 'allow_search' => array( 'type' => 'switch', 'label' => __( 'Search in document', 'flipset' ), 'help' => __( 'Show the search/find input on the toolbar.', 'flipset' ), 'hint' => 'hide="search"', 'icon' => 'search', ), 'allow_theme' => array( 'type' => 'switch', 'label' => __( 'Theme switcher', 'flipset' ), 'help' => __( 'Show the Dark / Light / Sepia theme toggle button.', 'flipset' ), 'hint' => 'hide="theme"', 'icon' => 'theme', ), 'allow_share' => array( 'type' => 'switch', 'label' => __( 'Share button', 'flipset' ), 'help' => __( 'Show the share and copy page link button.', 'flipset' ), 'hint' => 'hide="share"', 'icon' => 'share', ), 'allow_shortcuts' => array( 'type' => 'switch', 'label' => __( 'Keyboard shortcuts guide', 'flipset' ), 'help' => __( 'Show the keyboard shortcuts guide button (?) and enable keyboard navigation.', 'flipset' ), 'hint' => 'hide="shortcuts"', 'icon' => 'shortcuts', ), 'allow_fullscreen' => array( 'type' => 'switch', 'label' => __( 'Full screen button', 'flipset' ), 'help' => __( 'Show the full screen toggle button.', 'flipset' ), 'hint' => 'hide="fullscreen"', 'icon' => 'fs', ), 'allow_presentation' => array( 'type' => 'switch', 'label' => __( 'Presentation button', 'flipset' ), 'help' => __( 'Show the distraction-free presentation mode button.', 'flipset' ), 'hint' => 'hide="presentation"', 'icon' => 'present', ), 'index_pdf_text' => array( 'type' => 'switch', 'label' => __( 'Site search', 'flipset' ), 'help' => __( 'Include text from embedded PDFs in WordPress site search (and in the document library search). Visit Flipset → Documents to extract text.', 'flipset' ), ), 'check_pdf_a11y' => array( 'type' => 'switch', 'label' => __( 'Accessibility tags', 'flipset' ), 'help' => __( 'Warn on Flipset → Documents when a PDF is not tagged. Tagged PDFs include structure (headings, reading order) that screen readers can follow.', 'flipset' ), ), 'social_preview' => array( 'type' => 'switch', 'label' => __( 'Social preview', 'flipset' ), 'help' => __( 'Use the PDF first-page thumbnail as the Open Graph / Twitter image when a post or page embeds Flipset and no SEO plugin already set an image.', 'flipset' ), ), 'autoembed_url' => array( 'type' => 'switch', 'label' => __( 'Direct PDF auto-embed', 'flipset' ), 'help' => __( 'Automatically convert standalone .pdf URLs pasted on their own line into an interactive Flipset viewer.', 'flipset' ), ), 'embed_attachment_pages' => array( 'type' => 'switch', 'label' => __( 'Attachment pages', 'flipset' ), 'help' => __( 'Automatically display the interactive Flipset PDF viewer on standard WordPress PDF attachment pages.', 'flipset' ), ), ); } /** * Current value of a setting (stored value, or its default). * * @param string $key Setting key. * @return mixed */ private function setting_value( $key ) { $settings = get_option( self::OPTION_NAME, array() ); $defaults = $this->settings_defaults(); if ( is_array( $settings ) && array_key_exists( $key, $settings ) ) { return $settings[ $key ]; } return $defaults[ $key ] ?? ''; } /** * One on/off switch. The real checkbox stays in the accessibility tree * (role="switch"); only its look is replaced. * * @param string $key Setting key. * @param string $labelled_by Element id that names the switch. * @param string $described Element id of the help text. */ private function render_switch( $key, $labelled_by = '', $described = '' ) { printf( '', esc_attr( str_replace( '_', '-', $key ) ), esc_attr( self::OPTION_NAME ), esc_attr( $key ), checked( ! empty( $this->setting_value( $key ) ), true, false ), '' !== $labelled_by ? ' aria-labelledby="' . esc_attr( $labelled_by ) . '"' : '', '' !== $described ? ' aria-describedby="' . esc_attr( $described ) . '"' : '' ); } /** * Control markup for one field, shared by the registered field callbacks * and the Settings screen renderer. * * @param string $key Setting key. * @param string $described Element id of the help text. */ private function render_control( $key, $described = '' ) { $schema = $this->settings_schema(); if ( ! isset( $schema[ $key ] ) ) { return; } $field = $schema[ $key ]; $value = $this->setting_value( $key ); $id = 'flipset-' . str_replace( '_', '-', $key ); if ( 'dimension' === $field['type'] ) { ?>
array( __( 'Light', 'flipset' ), '☀️' ), 'dark' => array( __( 'Dark Mode', 'flipset' ), '🌙' ), 'sepia' => array( __( 'Sepia (Warm)', 'flipset' ), '☕' ), ); ?>
> $theme ) : ?>
render_switch( $key, $id . '-label', $described ); } /** * Inline code chip for an attribute override. * * @param string $code Attribute text such as hide="search". */ private function render_hint( $code ) { printf( '%s', esc_html( $code ) ); } /** * A standard label + help + control row. * * @param string $key Setting key. * @param string $status Optional HTML for a status line (already escaped). */ private function render_field_row( $key, $status = '' ) { $schema = $this->settings_schema(); if ( ! isset( $schema[ $key ] ) ) { return; } $field = $schema[ $key ]; $id = 'flipset-' . str_replace( '_', '-', $key ); $is_flag = 'switch' === $field['type']; $tag = $is_flag ? 'label' : 'div'; ?>
< class="fa-field__text"> < class="fa-field__label" id="">> render_hint( $field['hint'] ); ?> array( 'class' => true ), 'a' => array( 'class' => true, 'href' => true, ), ) ); } ?> >
render_control( $key, $id . '-help' ); ?>
settings_schema(); if ( ! isset( $schema[ $key ] ) ) { return; } $field = $schema[ $key ]; $id = 'flipset-' . str_replace( '_', '-', $key ); $on = ! empty( $this->setting_value( $key ) ); ?> 'attachment', 'post_status' => 'inherit', 'post_mime_type' => 'application/pdf', 'posts_per_page' => 1, 'fields' => 'ids', ); if ( $meta_query ) { $args['meta_query'] = $meta_query; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- one bounded count on an admin screen. } $query = new WP_Query( $args ); return (int) $query->found_posts; }; return array( 'total' => $count(), 'indexed' => $count( array( array( 'key' => Flipset_SEO::META_INDEXED, 'value' => '1', ), ) ), 'checked' => $count( array( array( 'key' => Flipset_A11y::META_CHECKED, 'compare' => 'EXISTS', ), ) ), 'tagged' => $count( array( array( 'key' => Flipset_A11y::META_TAGGED, 'value' => '1', ), ) ), ); } /** * Preview viewer markup. Every toolbar button is rendered so the screen * can show or hide it live; the saved settings decide the initial state. * * @param string $file PDF URL. * @param string $width CSS width. * @param string $height CSS height. * @param string $theme light|dark|sepia. * @return string */ private function render_settings_preview_viewer( $file, $width, $height, $theme ) { return Flipset::instance()->viewer->render_shortcode( array( 'file' => $file, 'width' => $width, 'height' => $height, 'theme' => $theme, 'loading' => 'eager', 'download' => 'true', 'print' => 'true', 'search' => 'true', 'theme_btn' => 'true', 'share' => 'true', 'shortcuts' => 'true', 'presentation' => 'true', 'fullscreen' => 'true', ) ); } /** * AJAX: viewer markup for another preview PDF (Settings live preview). */ public function ajax_settings_preview() { check_ajax_referer( 'flipset_admin', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'You are not allowed to do this.', 'flipset' ) ), 403 ); } $pdf_id = isset( $_POST['pdf_id'] ) ? absint( wp_unslash( $_POST['pdf_id'] ) ) : 0; if ( ! $pdf_id || 'application/pdf' !== get_post_mime_type( $pdf_id ) ) { wp_send_json_error( array( 'message' => __( 'Please select a PDF file.', 'flipset' ) ), 400 ); } $url = wp_get_attachment_url( $pdf_id ); if ( ! $url ) { wp_send_json_error( array( 'message' => __( 'Please select a PDF file.', 'flipset' ) ), 400 ); } $raw_width = isset( $_POST['width'] ) ? sanitize_text_field( wp_unslash( $_POST['width'] ) ) : ''; $raw_height = isset( $_POST['height'] ) ? sanitize_text_field( wp_unslash( $_POST['height'] ) ) : ''; $raw_theme = isset( $_POST['theme'] ) ? sanitize_key( wp_unslash( $_POST['theme'] ) ) : 'light'; $theme = in_array( $raw_theme, array( 'light', 'dark', 'sepia' ), true ) ? $raw_theme : 'light'; wp_send_json_success( array( 'html' => $this->render_settings_preview_viewer( $url, $this->sanitize_css_dimension( $raw_width, '100%' ), $this->sanitize_css_dimension( $raw_height, '600px' ), $theme ), ) ); } public function render_settings_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $width = (string) $this->setting_value( 'default_width' ); $height = (string) $this->setting_value( 'default_height' ); $theme = (string) $this->setting_value( 'default_theme' ); $recent_pdfs = $this->get_recent_pdfs( 12 ); $preview = ''; $dash_url = admin_url( 'admin.php?page=' . self::PAGE_SLUG ); $docs_url = admin_url( 'admin.php?page=' . self::DOCS_SLUG ); $stats = $this->settings_pdf_stats(); if ( $recent_pdfs ) { $preview = $this->render_settings_preview_viewer( $recent_pdfs[0]['url'], $width, $height, $theme ); } $toolbar_keys = array( 'allow_search', 'allow_theme', 'allow_share', 'allow_shortcuts', 'allow_fullscreen', 'allow_presentation' ); $tabs = array( 'viewer' => array( 'label' => __( 'Viewer defaults', 'flipset' ), 'emoji' => '🖥', 'keys' => array( 'allow_download', 'allow_print', 'lazy_loading', 'show_branding' ), ), 'toolbar' => array( 'label' => __( 'Toolbar', 'flipset' ), 'emoji' => '🧰', 'keys' => $toolbar_keys, ), 'sharing' => array( 'label' => __( 'Search & sharing', 'flipset' ), 'emoji' => '🔎', 'keys' => array( 'index_pdf_text', 'check_pdf_a11y', 'social_preview', 'autoembed_url', 'embed_attachment_pages' ), ), ); // Status lines under the search settings, built from escaped parts. $index_status = sprintf( '%1$s %3$s', esc_html( sprintf( /* translators: 1: number of indexed PDFs, 2: total PDFs. */ __( '%1$s of %2$s PDFs indexed', 'flipset' ), number_format_i18n( $stats['indexed'] ), number_format_i18n( $stats['total'] ) ) ), esc_url( $docs_url ), esc_html__( 'Manage in Documents →', 'flipset' ) ); $a11y_status = sprintf( '%1$s %3$s', esc_html( sprintf( /* translators: 1: number of tagged PDFs, 2: number of checked PDFs. */ __( '%1$s of %2$s checked PDFs are tagged', 'flipset' ), number_format_i18n( $stats['tagged'] ), number_format_i18n( $stats['checked'] ) ) ), esc_url( add_query_arg( 'status', 'attention', $docs_url ) ), esc_html__( 'Manage in Documents →', 'flipset' ) ); $form_id = 'flipset-settings-form'; $defaults = $this->settings_defaults(); ?>

', esc_attr( self::OPTION_NAME ), esc_attr( self::FORM_MARKER ) ); ?>

render_field_row( 'default_width' ); ?> render_field_row( 'default_height' ); ?>

render_field_row( 'default_theme' ); ?> render_field_row( 'lazy_loading' ); ?>

render_field_row( 'allow_download' ); ?> render_field_row( 'allow_print' ); ?>
settings_schema()['show_branding']['help'] ); ?>
render_switch( 'show_branding', 'flipset-show-branding-label', 'flipset-show-branding-help' ); ?>

render_toolbar_tile( $key ); } ?>

render_field_row( 'index_pdf_text', $index_status ); ?> render_field_row( 'check_pdf_a11y', $a11y_status ); ?>

render_field_row( 'social_preview' ); ?>

render_field_row( 'autoembed_url' ); ?> render_field_row( 'embed_attachment_pages' ); ?>
get_recent_pdfs( 8 ); if ( $editing_embed ) { $default_pdf = $editing_embed['file'] ?? ''; $default_title = $editing_embed['title'] ?? ''; $default_name = $editing_embed['name'] ?? ''; $default_width = $editing_embed['width'] ?? '100%'; $default_height = $editing_embed['height'] ?? '520px'; $default_theme = $editing_embed['theme'] ?? 'light'; $default_page = $editing_embed['page'] ?? 1; $default_view = $editing_embed['view'] ?? 'page'; $default_hide = array_filter( array_map( 'trim', explode( ',', strtolower( $editing_embed['hide'] ?? '' ) ) ) ); $default_lazy = 'lazy' === ( $editing_embed['loading'] ?? '' ); $default_hash = 'false' !== ( $editing_embed['hash'] ?? 'true' ); $default_resume = 'false' !== ( $editing_embed['resume'] ?? 'true' ); } else { $preview_url = $this->get_latest_pdf_url(); $default_pdf = $preview_url ? $preview_url : ''; $default_title = ! empty( $recent_pdfs[0]['title'] ) ? $recent_pdfs[0]['title'] : ''; // Read-only prefill from the dashboard's "Open in Builder" link. $pdf_param = isset( $_GET['pdf_id'] ) ? absint( wp_unslash( $_GET['pdf_id'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only prefill, validated below. if ( $pdf_param && 'application/pdf' === get_post_mime_type( $pdf_param ) ) { $pdf_param_url = wp_get_attachment_url( $pdf_param ); if ( $pdf_param_url ) { $default_pdf = $pdf_param_url; $default_title = get_the_title( $pdf_param ); } } $default_name = ''; $default_width = '100%'; $default_height = '520px'; $default_theme = $settings['default_theme'] ?? 'light'; $default_page = 1; $default_view = 'page'; $default_hide = array(); $default_lazy = ! empty( $settings['lazy_loading'] ); $default_hash = true; $default_resume = true; } $initial_preview = ''; if ( $default_pdf ) { $initial_preview = Flipset::instance()->viewer->render_shortcode( array( 'file' => $default_pdf, 'width' => $default_width, 'height' => $default_height, 'theme' => $default_theme, 'view' => $default_view, 'page' => $default_page, 'download' => 'true', 'print' => 'true', 'search' => 'true', 'theme_btn' => 'true', 'share' => 'true', 'shortcuts' => 'true', 'presentation' => 'true', 'fullscreen' => 'true', ) ); } ?>

' . esc_html( $editing_embed['name'] ) . '', absint( $editing_id ) ); ?>



0, 'ready' => 0, 'pending' => 0, 'attention' => 0, ); $counts_ok = true; $lib_total = 0; if ( 'documents' === $current_tab ) { $lib_total = $this->count_pdfs(); $base_args = array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'post_mime_type' => 'application/pdf', 's' => $search, 'flipset_content_search' => ( '' !== $search ), ); // One light pass over the matching IDs powers the status chips and filter. $id_query = new WP_Query( array_merge( $base_args, array( 'fields' => 'ids', 'posts_per_page' => 1000, // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page -- IDs only, bounded. 'no_found_rows' => true, 'orderby' => 'date', 'order' => 'DESC', ) ) ); $ids = array_map( 'intval', $id_query->posts ); $counts_ok = count( $ids ) < 1000; update_meta_cache( 'post', $ids ); $by_status = array(); foreach ( $ids as $doc_id ) { $st = $this->doc_status( $doc_id, $index_on, $a11y_on ); $by_status[ $doc_id ] = $st; ++$counts[ $st ]; } $counts['all'] = count( $ids ); if ( 'all' === $status ) { $query = new WP_Query( array_merge( $base_args, array( 'posts_per_page' => 20, 'paged' => $paged, 'orderby' => 'date', 'order' => 'DESC', ) ) ); $total = (int) $query->found_posts; $max_pages = (int) $query->max_num_pages; // A page number past the end falls back to the last page. if ( $paged > 1 && ! $query->have_posts() ) { $paged = $counts_ok ? max( 1, (int) ceil( $counts['all'] / 20 ) ) : 1; $query = new WP_Query( array_merge( $base_args, array( 'posts_per_page' => 20, 'paged' => $paged, 'orderby' => 'date', 'order' => 'DESC', ) ) ); $total = (int) $query->found_posts; $max_pages = max( 1, (int) $query->max_num_pages ); } } else { $filtered = array_keys( array_filter( $by_status, fn( $st ) => $st === $status ) ); $total = count( $filtered ); $max_pages = max( 1, (int) ceil( $total / 20 ) ); $paged = min( $paged, $max_pages ); $slice = array_slice( $filtered, ( $paged - 1 ) * 20, 20 ); $query = new WP_Query( array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'post_mime_type' => 'application/pdf', 'post__in' => $slice ? $slice : array( 0 ), 'orderby' => 'post__in', 'posts_per_page' => 20, 'no_found_rows' => true, ) ); } } $untagged_n = 0; if ( $a11y_on && $query && $query->have_posts() ) { foreach ( $query->posts as $attachment ) { if ( Flipset_A11y::is_checked( $attachment->ID ) && ! Flipset_A11y::is_tagged( $attachment->ID ) ) { ++$untagged_n; } } } $ledes = array( 'documents' => __( 'Every PDF in your Media Library, ready to embed. No duplicate storage.', 'flipset' ), 'docs' => __( 'Shortcodes, blocks, deep links and page builders, all in one reference.', 'flipset' ), 'features' => __( 'Everything Flipset can do. Fast, local and accessible.', 'flipset' ), ); ?>

render_features_tab(); ?> render_user_guide_tab(); ?>

__( 'All', 'flipset' ), 'ready' => __( 'Ready', 'flipset' ), 'pending' => __( 'Pending', 'flipset' ), 'attention' => __( 'Needs attention', 'flipset' ), ); foreach ( $filters as $key => $label ) : $link = add_query_arg( array_filter( array( 'status' => 'all' === $key ? '' : $key, 's' => $search, ), 'strlen' ), $docs_url ); ?> >
have_posts() ) : ?>
posts as $attachment ) : ?> ID ); $path = get_attached_file( $attachment->ID ); $bytes = ( $path && file_exists( $path ) ) ? filesize( $path ) : 0; $size = $bytes ? size_format( $bytes ) : '—'; $code = sprintf( '[flipset file="%s"]', esc_url_raw( $url ) ); $edit = admin_url( 'upload.php?item=' . (int) $attachment->ID ); $title = get_the_title( $attachment ); if ( '' === $title ) { $title = wp_basename( $url ); } $thumb_id = Flipset_Thumbnails::get_thumbnail_id( $attachment->ID ); $has_cover = (bool) Flipset_Thumbnails::get_cover_id( $attachment->ID ); $indexed = Flipset_SEO::is_indexed( $attachment->ID ); $has_text = $indexed && '' !== Flipset_SEO::get_extracted_text( $attachment->ID ); $a11y_done = Flipset_A11y::is_checked( $attachment->ID ); $a11y_tagged = $a11y_done && Flipset_A11y::is_tagged( $attachment->ID ); $open_url = add_query_arg( 'pdf_id', (int) $attachment->ID, $builder_url ); ?>
'flipset-docs-thumb-img', 'alt' => $title, ) ); ?>
1 ) : ?>
add_query_arg( 'paged', '%#%' ), 'format' => '', 'total' => $max_pages, 'current' => $paged, 'prev_text' => '«', 'next_text' => '»', ) ) ); ?>

[flipset file="https://site.com/doc.pdf"]

id
Integer

[flipset id="1"]
file
URL string

file="https://example.com/doc.pdf"
view
Enum default: "page"

  • flip —
  • scroll —
  • spread —
  • page —
view="flip"
width
CSS Unit default: "100%"

width="850px"
height
CSS Unit default: "420px"

height="650px"
page
Integer default: 1

page="5"
download
Boolean default: true

download="false"
print
Boolean default: true

print="false"
search
Boolean default: true

search="false"
title
String

title="Annual Report 2026"
hide
CSV List

nav zoom fit rotate view pan download print search theme share fullscreen

hide="download,print,share"

[flipset file="https://site.com/catalog.pdf" view="flip" height="600px"]
[flipset file="https://site.com/ebook.pdf" view="scroll" height="700px"]
[flipset file="https://site.com/slides.pdf" download="false" print="false" view="spread"]

columns 2 | 3 | 4 default: 3

per_page Integer default: 12

orderby date | title | modified default: "date"

search Boolean default: true

[flipset_library columns="4" per_page="16" orderby="title" search="true"]

'dashicons-book-alt', 'tint' => 'is-indigo', 'title' => __( 'Reading Modes & 3D FlipBook Engine', 'flipset' ), 'items' => array( array( __( '16-Bone 3D FlipBook:', 'flipset' ), __( 'Ultra-realistic 3D curved page turning with natural paper inertia, dynamic specular lighting, center spine depth shadow, and stacked paper thickness.', 'flipset' ) ), array( __( 'Web Audio Paper Sound:', 'flipset' ), __( 'Synthesized real-time page-turn audio feedback (no heavy external mp3 files required).', 'flipset' ) ), array( __( 'Multiple Layout Modes:', 'flipset' ), __( 'Single Page, Two-Page Magazine Spread, Continuous Vertical Scroll, and 3D FlipBook.', 'flipset' ) ), array( __( 'Glassmorphism Thumbnails Panel:', 'flipset' ), __( 'Slide-out thumbnail card preview strip with instant jump-to-page navigation.', 'flipset' ) ), array( __( 'Touch & Mobile Gesture Engine:', 'flipset' ), __( 'Natural swipe-to-flip gestures and hardware-accelerated pinch-to-zoom.', 'flipset' ) ), array( __( 'Reading Themes:', 'flipset' ), __( 'Instant Dark, Light, and Sepia eye-care reading themes.', 'flipset' ) ), ), ), array( 'icon' => 'dashicons-admin-customizer', 'tint' => 'is-violet', 'title' => __( 'Shortcode Builder & Saved Embeds', 'flipset' ), 'items' => array( array( __( 'Clean ID Shortcodes [flipset id="1"]:', 'flipset' ), __( 'Encapsulates long attributes into a clean, easy-to-manage ID-based shortcode.', 'flipset' ) ), array( __( 'Saved Embeds Manager:', 'flipset' ), __( 'Dedicated manager with instant search, 1-click copy, duplicate, and live editing.', 'flipset' ) ), array( __( 'Live Interactive Preview:', 'flipset' ), __( 'Real-time responsive preview (Desktop, Tablet, Mobile) that updates instantly as you tweak settings.', 'flipset' ) ), array( __( '1-Click Presets:', 'flipset' ), __( 'One-click configurations for 3D FlipBook, E-Book Reader, Corporate Report, Night Reader, and Speed Optimized.', 'flipset' ) ), array( __( 'Developer Raw Code Toggle:', 'flipset' ), __( 'Switch between clean ID shortcodes, raw inline shortcodes, PHP snippets, and Gutenberg block code.', 'flipset' ) ), ), ), array( 'icon' => 'dashicons-layout', 'tint' => 'is-sky', 'title' => __( 'Page Builders & Documents Library', 'flipset' ), 'items' => array( array( __( 'Gutenberg Native Block:', 'flipset' ), __( 'Flipset Viewer block with Media picker & live first-page preview.', 'flipset' ) ), array( __( 'Auto oEmbed:', 'flipset' ), __( 'Simply paste any direct .pdf link on a new line to embed instantly.', 'flipset' ) ), array( __( 'Page Builders Compatibility:', 'flipset' ), __( 'Native widgets for Elementor, Divi and Beaver Builder; the shortcode works everywhere else.', 'flipset' ) ), array( __( 'Documents Library Grid:', 'flipset' ), __( '[flipset_library] shortcode for searchable PDF galleries.', 'flipset' ) ), array( __( 'Attachment Pages:', 'flipset' ), __( 'Automatic interactive reader on standard WordPress attachment pages.', 'flipset' ) ), ), ), array( 'icon' => 'dashicons-search', 'tint' => 'is-green', 'title' => __( 'SEO, Search & Accessibility', 'flipset' ), 'items' => array( array( __( 'Site Search Indexing:', 'flipset' ), __( 'Extracts PDF text so WordPress search finds content inside documents.', 'flipset' ) ), array( __( 'Schema.org JSON-LD:', 'flipset' ), __( 'Automatic DigitalDocument rich snippets for Google search results.', 'flipset' ) ), array( __( 'Social Card Previews:', 'flipset' ), __( 'Open Graph & Twitter Cards cover preview generated from the PDF.', 'flipset' ) ), array( __( 'Accessibility Diagnostics:', 'flipset' ), __( 'Detects untagged PDFs and tells you how to export an accessible one.', 'flipset' ) ), array( __( 'Zero CDN Dependency:', 'flipset' ), __( 'Bundled locally hosted PDF.js, no third-party tracking or remote downtime.', 'flipset' ) ), ), ), ); ?>

4 Gutenberg · Elementor · Divi · Beaver 0

__( 'Forbidden.', 'flipset' ) ), 403 ); } $settings = get_option( self::OPTION_NAME, array() ); if ( ! is_array( $settings ) ) { $settings = array(); } $settings['setup_copied_shortcode'] = true; update_option( self::OPTION_NAME, $settings ); wp_send_json_success(); } public function ajax_create_test_page() { check_ajax_referer( 'flipset_admin', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Forbidden.', 'flipset' ) ), 403 ); } $file = isset( $_POST['file'] ) ? esc_url_raw( wp_unslash( $_POST['file'] ) ) : ''; if ( '' === $file ) { wp_send_json_error( array( 'message' => __( 'Select a PDF first.', 'flipset' ) ) ); } // Ensure the URL resolves to a PDF attachment in this site's Media Library // so an arbitrary external URL cannot be published via this AJAX endpoint. $attachment_id = Flipset_Compat::attachment_id_from_url( $file ); if ( ! $attachment_id ) { wp_send_json_error( array( 'message' => __( 'Only PDFs from the Media Library can be used here.', 'flipset' ) ) ); } $mime = get_post_mime_type( $attachment_id ); if ( 'application/pdf' !== $mime ) { wp_send_json_error( array( 'message' => __( 'That file is not a PDF.', 'flipset' ) ) ); } $shortcode = isset( $_POST['shortcode'] ) ? sanitize_text_field( wp_unslash( $_POST['shortcode'] ) ) : ''; $content = ( '' !== $shortcode ) ? $shortcode : '[flipset file="' . $file . '"]'; $existing = get_page_by_path( 'flipset-test' ); if ( $existing ) { $result = wp_update_post( array( 'ID' => $existing->ID, 'post_content' => $content, 'post_status' => 'publish', ), true ); if ( is_wp_error( $result ) || ! $result ) { wp_send_json_error( array( 'message' => is_wp_error( $result ) ? $result->get_error_message() : __( 'Could not update the test page.', 'flipset' ) ) ); } $page_id = $existing->ID; } else { $page_id = wp_insert_post( array( 'post_type' => 'page', 'post_status' => 'publish', 'post_title' => __( 'Flipset Test', 'flipset' ), 'post_name' => 'flipset-test', 'post_content' => $content, ), true ); if ( is_wp_error( $page_id ) ) { wp_send_json_error( array( 'message' => $page_id->get_error_message() ) ); } } wp_send_json_success( array( 'id' => $page_id, 'url' => get_permalink( $page_id ), ) ); } public function ajax_dismiss_welcome() { check_ajax_referer( 'flipset_admin', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Forbidden.', 'flipset' ) ), 403 ); } update_user_meta( get_current_user_id(), 'flipset_welcome_dismissed', 1 ); wp_send_json_success(); } private function get_latest_pdf_url() { $query = new WP_Query( array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'post_mime_type' => 'application/pdf', 'posts_per_page' => 1, 'orderby' => 'date', 'order' => 'DESC', 'fields' => 'ids', 'no_found_rows' => true, ) ); if ( empty( $query->posts ) ) { return ''; } $url = wp_get_attachment_url( (int) $query->posts[0] ); return $url ? $url : ''; } /** * Most recently uploaded PDFs for the dashboard picker. * * @param int $limit Number of attachments. * @return array */ private function get_recent_pdfs( $limit = 4 ) { $query = new WP_Query( array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'post_mime_type' => 'application/pdf', 'posts_per_page' => max( 1, absint( $limit ) ), 'orderby' => 'date', 'order' => 'DESC', 'fields' => 'ids', 'no_found_rows' => true, ) ); $items = array(); foreach ( $query->posts as $id ) { $id = (int) $id; $url = wp_get_attachment_url( $id ); if ( ! $url ) { continue; } $title = get_the_title( $id ); if ( '' === $title ) { $title = wp_basename( $url ); } $items[] = array( 'id' => $id, 'url' => $url, 'title' => $title, 'thumb' => Flipset_Thumbnails::get_thumbnail_url( $id, 'thumbnail' ), ); } return $items; } private function count_pdfs() { // wp_count_attachments() is keyed by MIME type (plus 'trash'), not by // post_status, so it can't be read with ->inherit. A direct query also // avoids a cached value going stale when a PDF is added or deleted. $query = new WP_Query( array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'post_mime_type' => 'application/pdf', 'posts_per_page' => 1, 'fields' => 'ids', ) ); return (int) $query->found_posts; } private function count_embeds() { $cached = wp_cache_get( 'embed_count', 'flipset' ); if ( false !== $cached ) { return (int) $cached; } global $wpdb; $like_shortcode = '%' . $wpdb->esc_like( '[flipset' ) . '%'; $like_block = '%' . $wpdb->esc_like( 'wp:flipset/' ) . '%'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- LIKE across post_content is not available via WP_Query; result is cached. $count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_status IN ('publish','draft','private') AND post_type IN ('post','page') AND (post_content LIKE %s OR post_content LIKE %s)", $like_shortcode, $like_block ) ); wp_cache_set( 'embed_count', $count, 'flipset', 5 * MINUTE_IN_SECONDS ); return $count; } }