Icons. */ class AdminPage { const MENU_SLUG = 'icon-library'; /** * Collection registry. * * @var CollectionRegistry */ private $collection_registry; /** * SVG sanitizer. * * @var SvgSanitizer */ private $svg_sanitizer; /** * Constructor. * * @param CollectionRegistry $collection_registry Collection registry. * @param SvgSanitizer $svg_sanitizer SVG sanitizer. */ public function __construct( CollectionRegistry $collection_registry, SvgSanitizer $svg_sanitizer ) { $this->collection_registry = $collection_registry; $this->svg_sanitizer = $svg_sanitizer; } /** * Registers admin hooks. */ public function register() { add_filter( 'plugin_action_links_' . plugin_basename( ICON_LIBRARY_FILE ), array( $this, 'plugin_action_links' ) ); add_action( 'admin_menu', array( $this, 'register_menu' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); add_action( 'wp_ajax_icon_library_preview_page', array( $this, 'preview_page' ) ); } /** * Adds a shortcut to icon management on the Plugins screen. * * @param array $links Existing plugin action links. * @return array */ public function plugin_action_links( $links ) { if ( ! current_user_can( 'manage_options' ) ) { return $links; } $settings_link = sprintf( '%s', esc_url( admin_url( 'themes.php?page=' . self::MENU_SLUG ) ), esc_html__( 'Settings', 'aculect-icon-library' ) ); array_unshift( $links, $settings_link ); return $links; } /** * Registers the Appearance submenu. */ public function register_menu() { add_theme_page( __( 'Icon Library', 'aculect-icon-library' ), __( 'Icons', 'aculect-icon-library' ), 'manage_options', self::MENU_SLUG, array( $this, 'render' ) ); } /** * Enqueues screen-specific admin styles. * * @param string $hook_suffix Current admin page hook suffix. */ public function enqueue_assets( $hook_suffix ) { if ( 'appearance_page_' . self::MENU_SLUG !== $hook_suffix ) { return; } wp_enqueue_style( 'icon-library-admin', ICON_LIBRARY_URL . 'assets/admin.css', array(), $this->asset_version( 'assets/admin.css' ) ); wp_enqueue_script( 'icon-library-admin', ICON_LIBRARY_URL . 'assets/admin.js', array( 'wp-api-fetch' ), $this->asset_version( 'assets/admin.js' ), true ); $dataviews_asset = require ICON_LIBRARY_DIR . 'assets/build/custom-icons-dataviews.asset.php'; foreach ( $dataviews_asset['dependencies'] as $dependency ) { wp_enqueue_script( $dependency ); } wp_enqueue_style( 'wp-components' ); wp_localize_script( 'icon-library-admin', 'iconLibraryAdmin', array( 'nonce' => wp_create_nonce( 'wp_rest' ), 'previewUrl' => admin_url( 'admin-ajax.php' ), 'previewNonce' => wp_create_nonce( 'icon_library_preview' ), 'restPath' => '/' . Plugin::REST_NAMESPACE . '/collections/', 'customPath' => '/' . Plugin::REST_NAMESPACE . '/custom-icons', 'dataViewsUrl' => ICON_LIBRARY_URL . 'assets/build/custom-icons-dataviews.js?ver=' . rawurlencode( $dataviews_asset['version'] ), 'dataViewsEnqueued' => 'custom' === $this->get_active_tab(), 'i18n' => array( 'updating' => __( 'Updating library...', 'aculect-icon-library' ), 'error' => __( 'The library could not be updated. Try again.', 'aculect-icon-library' ), 'uploading' => __( 'Validating and storing icon...', 'aculect-icon-library' ), 'updated' => __( 'Icon library updated.', 'aculect-icon-library' ), 'deleteConfirm' => __( 'Permanently delete this icon and its SVG file? Existing blocks using it will no longer render.', 'aculect-icon-library' ), 'purgeConfirm' => __( 'Permanently delete this archived icon and its SVG file? Existing blocks will no longer render it.', 'aculect-icon-library' ), 'fileTooLarge' => __( 'SVG files must be 64 KB or smaller.', 'aculect-icon-library' ), 'loadingMore' => __( 'Loading more icons...', 'aculect-icon-library' ), 'loadMoreError' => __( 'More icons could not be loaded. Try again.', 'aculect-icon-library' ), /* translators: 1: number of icons loaded, 2: total matching icons. */ 'loadMoreStatus' => __( 'Loaded %1$s of %2$s icons.', 'aculect-icon-library' ), ), ) ); if ( 'custom' === $this->get_active_tab() ) { wp_enqueue_script( 'icon-library-custom-dataviews', ICON_LIBRARY_URL . 'assets/build/custom-icons-dataviews.js', array_merge( array( 'icon-library-admin' ), $dataviews_asset['dependencies'] ), $dataviews_asset['version'], true ); } } /** * Renders the admin page. */ public function render() { if ( ! current_user_can( 'manage_options' ) ) { return; } $active_tab = $this->get_active_tab(); $collections = 'custom' === $active_tab ? array() : $this->collection_registry->get_collections(); ?>

render_tabs( $active_tab ); ?>
render_notice(); ?> render_custom_tab(); ?> render_install_tab( $this->get_filters( $collections ), $collections ); ?> render_library_tab( array_filter( $collections, static function ( $collection ) { return ! empty( $collection['installed'] ); } ) ); ?>
__( 'Choose an SVG file to upload.', 'aculect-icon-library' ), 'file_failed' => __( 'The SVG upload did not finish. Choose the file and try again.', 'aculect-icon-library' ), 'file_type' => __( 'Choose a file with an .svg extension.', 'aculect-icon-library' ), 'file_size' => __( 'SVG files must be 64 KB or smaller.', 'aculect-icon-library' ), 'icon_library_svg_empty' => __( 'The SVG file is empty. Choose an SVG with visible artwork.', 'aculect-icon-library' ), 'icon_library_svg_too_large' => __( 'SVG files must be 64 KB or smaller.', 'aculect-icon-library' ), 'icon_library_svg_invalid' => __( 'The file is not a valid SVG document. Export it as SVG and try again.', 'aculect-icon-library' ), 'icon_library_svg_declaration' => __( 'This SVG uses an unsupported declaration or processing instruction. Export a plain SVG and try again.', 'aculect-icon-library' ), 'icon_library_svg_namespace' => __( 'This SVG uses an unsupported namespace. Export a plain SVG and try again.', 'aculect-icon-library' ), 'icon_library_svg_element' => __( 'This SVG contains unsupported elements. Simplify the artwork and try again.', 'aculect-icon-library' ), 'icon_library_svg_attribute' => __( 'This SVG contains unsupported attributes. Simplify the artwork and try again.', 'aculect-icon-library' ), 'icon_library_svg_reference' => __( 'External or executable SVG references are not allowed. Remove them and try again.', 'aculect-icon-library' ), 'icon_library_svg_geometry' => __( 'The SVG has no supported visible artwork. Add a visible shape and try again.', 'aculect-icon-library' ), 'icon_library_custom_name_invalid' => __( 'Use a lowercase icon name containing letters, numbers, and hyphens.', 'aculect-icon-library' ), 'icon_library_custom_label_invalid' => __( 'Enter an icon label.', 'aculect-icon-library' ), 'icon_library_custom_duplicate' => __( 'An icon with that name already exists. Choose a different name.', 'aculect-icon-library' ), 'icon_library_custom_limit' => __( 'The custom icon limit has been reached. Remove an icon before uploading another.', 'aculect-icon-library' ), 'icon_library_custom_busy' => __( 'Another icon change is in progress. Try again.', 'aculect-icon-library' ), 'icon_library_custom_cleanup_failed' => __( 'The icon metadata could not be saved and its unpublished SVG could not be removed. Ask your site administrator to check the uploads directory before retrying.', 'aculect-icon-library' ), 'icon_library_custom_metadata_failed' => __( 'The icon change could not be confirmed. Refresh the icon list before retrying.', 'aculect-icon-library' ), ); if ( $upload_error ) { $message = isset( $upload_messages[ $upload_error ] ) ? $upload_messages[ $upload_error ] : __( 'The icon could not be stored. Check that uploads are available, then try again.', 'aculect-icon-library' ); echo ''; return; } // This read-only query parameter controls feedback after a verified mutation. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET['icon-library-updated'] ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $updated = absint( $_GET['icon-library-updated'] ); $class = $updated ? 'notice-success' : 'notice-error'; $message = $updated ? __( 'Library settings updated.', 'aculect-icon-library' ) : __( 'Library settings could not be updated.', 'aculect-icon-library' ); ?>

__( 'Library', 'aculect-icon-library' ), 'install' => __( 'Install Library', 'aculect-icon-library' ), 'custom' => _x( 'Upload', 'noun', 'aculect-icon-library' ), ); ?>

render_collection_detail( $collections[ $selected_slug ] ); return; } $install_url = add_query_arg( array( 'page' => self::MENU_SLUG, 'tab' => 'install', ), admin_url( 'themes.php' ) ); ?>

render_collection_row( $collection ); ?>
collection_registry->get_bundled_collection_slugs(), true ) ); $selected_slug = $filters['collection']; if ( $selected_slug && isset( $collections[ $selected_slug ] ) && CustomIconRepository::COLLECTION_SLUG !== $selected_slug ) { $this->render_install_collection_detail( $collections[ $selected_slug ], $filters ); return; } ?>

render_available_collection_row( $collection ); ?>
self::MENU_SLUG, 'tab' => 'install', 'collection' => $collection['slug'], ), admin_url( 'themes.php' ) ); ?>

$collection['slug'], 'variant' => $filters['variant'], 'category' => $filters['category'], 'search' => $filters['search'], 'page' => $page, 'per_page' => 72, ); $query = $this->collection_registry->query_icons( $query_args ); $total = $query['total']; $icons = $query['items']; $installed = ! empty( $collection['installed'] ); $back_url = add_query_arg( array( 'page' => self::MENU_SLUG, 'tab' => 'install', ), admin_url( 'themes.php' ) ); ?>

render_filters( $filters, array( $collection['slug'] => $collection ), $query['variant_counts'] ); ?> render_icon_grid( $icons ); ?> render_icon_pagination( $page, $total, 72, count( $icons ) ); ?>
add_query_arg( 'icon-page', '%#%', $base_url ), 'current' => min( $current_page, $total_pages ), 'total' => $total_pages, 'type' => 'list', 'prev_text' => __( 'Previous', 'aculect-icon-library' ), 'next_text' => __( 'Next', 'aculect-icon-library' ), ) ); if ( $links ) { echo ''; } if ( $current_page >= $total_pages ) { return; } ?>
self::MENU_SLUG, 'tab' => 'custom', ), admin_url( 'themes.php' ) ) : add_query_arg( array( 'page' => self::MENU_SLUG, 'tab' => 'library', 'collection' => $collection['slug'], ), admin_url( 'themes.php' ) ); ?>

self::MENU_SLUG, 'tab' => 'library', ), admin_url( 'themes.php' ) ); ?>

get_categories( $filters['collection'], $filters['variant'], $filters['search'] ); if ( null === $variant_counts ) { $variant_counts = $this->collection_registry->count_icons_by_variant( array( 'collection' => isset( $selected_collection['slug'] ) ? $selected_collection['slug'] : '', 'category' => $filters['category'], 'search' => $filters['search'], ) ); } ?>
collection_registry->get_enabled_collection_slugs(); $counts = $this->collection_registry->count_icons_by_category( array( 'collection' => $collection_slug, 'variant' => $variant, 'search' => $search, ) ); foreach ( $slugs as $slug ) { $manifest = $this->collection_registry->get_manifest( $slug ); if ( ! is_array( $manifest ) ) { continue; } if ( ! empty( $manifest['categories'] ) && is_array( $manifest['categories'] ) ) { foreach ( $manifest['categories'] as $category ) { if ( ! is_array( $category ) || empty( $category['slug'] ) || empty( $category['label'] ) ) { continue; } $category_slug = sanitize_key( $category['slug'] ); if ( '' === $category_slug ) { continue; } if ( ! isset( $categories[ $category_slug ] ) ) { $categories[ $category_slug ] = array( 'slug' => $category_slug, 'label' => sanitize_text_field( $category['label'] ), 'iconCount' => 0, ); } $categories[ $category_slug ]['iconCount'] = $counts[ $category_slug ] ?? 0; } continue; } if ( empty( $manifest['icons'] ) || ! is_array( $manifest['icons'] ) ) { continue; } foreach ( $manifest['icons'] as $icon ) { if ( ! empty( $icon['categories'] ) && is_array( $icon['categories'] ) ) { foreach ( $icon['categories'] as $category ) { $category_slug = sanitize_key( $category ); if ( '' === $category_slug ) { continue; } if ( ! isset( $categories[ $category_slug ] ) ) { $categories[ $category_slug ] = array( 'slug' => $category_slug, 'label' => ucwords( str_replace( '-', ' ', $category_slug ) ), 'iconCount' => 0, ); } $categories[ $category_slug ]['iconCount'] = $counts[ $category_slug ] ?? 0; } } } } $categories = array_values( $categories ); usort( $categories, static function ( $left, $right ) { return strnatcasecmp( (string) ( $left['label'] ?? '' ), (string) ( $right['label'] ?? '' ) ); } ); return $categories; } /** Sends only one preview grid, without rendering the admin shell or facets. */ public function preview_page() { if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'code' => 'icon_library_forbidden' ), 403 ); return; } check_ajax_referer( 'icon_library_preview', '_ajax_nonce' ); $filters = $this->get_filters( array() ); if ( '' === $filters['collection'] ) { wp_send_json_error( array( 'code' => 'icon_library_invalid_collection' ), 400 ); return; } $page = isset( $_GET['icon-page'] ) && is_scalar( $_GET['icon-page'] ) ? min( CollectionRegistry::MAX_PAGE, max( 1, absint( $_GET['icon-page'] ) ) ) : 1; $query = $this->collection_registry->query_icons( array_merge( $filters, array( 'page' => $page, 'per_page' => 72, ) ) ); ob_start(); $this->render_icon_grid( $query['items'] ); $html = ob_get_clean(); wp_send_json_success( array( 'html' => $html, 'page' => $page, 'total' => $query['total'], 'has_more' => $page * 72 < $query['total'], ) ); } /** * Renders preview icons. * * @param array[] $icons Icons. */ private function render_icon_grid( $icons ) { if ( empty( $icons ) ) { echo '

' . esc_html__( 'No icons match the current filters.', 'aculect-icon-library' ) . '

'; return; } ?>
collection_registry->get_svg_content( $icon['collection'], $icon['path'] ?? '' ); $svg = $this->svg_sanitizer->sanitize( $svg ); ?>
isset( $_GET['collection'] ) && is_string( $_GET['collection'] ) ? sanitize_key( wp_unslash( $_GET['collection'] ) ) : '', 'variant' => isset( $_GET['variant'] ) && is_string( $_GET['variant'] ) ? sanitize_key( wp_unslash( $_GET['variant'] ) ) : '', 'category' => isset( $_GET['category'] ) && is_string( $_GET['category'] ) ? sanitize_key( wp_unslash( $_GET['category'] ) ) : '', 'search' => isset( $_GET['search'] ) && is_string( $_GET['search'] ) ? sanitize_text_field( wp_unslash( $_GET['search'] ) ) : '', // phpcs:enable WordPress.Security.NonceVerification.Recommended ); } /** * Returns the active page tab. * * @return string */ private function get_active_tab() { // Read-only navigation state; no nonce is required. // phpcs:ignore WordPress.Security.NonceVerification.Recommended $tab = isset( $_GET['tab'] ) && is_string( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'library'; return in_array( $tab, array( 'library', 'install', 'custom' ), true ) ? $tab : 'library'; } }