initial commit

This commit is contained in:
rodolfomartinez 2026-02-02 14:17:16 -05:00
parent e468202f95
commit 423b9a25fb
24 changed files with 6670 additions and 0 deletions

View file

@ -0,0 +1,224 @@
<?php
/**
* Admin Page class - Handles the settings page.
*
* @package MapleIcons
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class MI_Admin_Page
*
* Handles the plugin settings page in WordPress admin.
*/
class MI_Admin_Page {
/**
* Constructor - Register admin hooks.
*/
public function __construct() {
add_action( 'admin_menu', array( $this, 'register_menu' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
}
/**
* Register the admin menu item.
*/
public function register_menu() {
add_options_page(
__( 'Maple Icons', 'maple-icons' ),
__( 'Maple Icons', 'maple-icons' ),
'manage_options',
'maple-icons',
array( $this, 'render_page' )
);
}
/**
* Enqueue admin assets.
*
* @param string $hook_suffix The current admin page.
*/
public function enqueue_assets( $hook_suffix ) {
if ( 'settings_page_maple-icons' !== $hook_suffix ) {
return;
}
wp_enqueue_style(
'mi-admin',
MI_PLUGIN_URL . 'assets/admin.css',
array(),
MI_VERSION
);
wp_enqueue_script(
'mi-admin',
MI_PLUGIN_URL . 'assets/admin.js',
array( 'jquery' ),
MI_VERSION,
true
);
wp_localize_script(
'mi-admin',
'miAdmin',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'mi_admin_nonce' ),
'strings' => array(
'downloading' => __( 'Downloading...', 'maple-icons' ),
'deleting' => __( 'Deleting...', 'maple-icons' ),
'activating' => __( 'Activating...', 'maple-icons' ),
'confirmDelete' => __( 'Are you sure you want to delete this icon set?', 'maple-icons' ),
'downloadError' => __( 'Download failed. Please try again.', 'maple-icons' ),
'deleteError' => __( 'Delete failed. Please try again.', 'maple-icons' ),
'activateError' => __( 'Activation failed. Please try again.', 'maple-icons' ),
'downloadSuccess'=> __( 'Icon set downloaded successfully!', 'maple-icons' ),
'deleteSuccess' => __( 'Icon set deleted successfully!', 'maple-icons' ),
'activateSuccess'=> __( 'Icon set activated!', 'maple-icons' ),
),
)
);
}
/**
* Render the settings page.
*/
public function render_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$registry = MI_Icon_Registry::get_instance();
$all_sets = MI_Icon_Sets::get_all();
$downloaded = $registry->get_downloaded_sets();
$active_set = $registry->get_active_set();
?>
<div class="wrap mi-admin-wrap">
<h1><?php esc_html_e( 'Maple Icons', 'maple-icons' ); ?></h1>
<div class="mi-admin-intro">
<p><?php esc_html_e( 'Download icon sets from CDN and use them in the Gutenberg block editor. Only one icon set can be active at a time.', 'maple-icons' ); ?></p>
</div>
<div class="mi-icon-sets">
<h2><?php esc_html_e( 'Available Icon Sets', 'maple-icons' ); ?></h2>
<div class="mi-sets-grid">
<?php foreach ( $all_sets as $slug => $set ) : ?>
<?php
$is_downloaded = isset( $downloaded[ $slug ] );
$is_active = $active_set === $slug;
$icon_count = $is_downloaded ? $downloaded[ $slug ]['icon_count'] : 0;
$download_date = $is_downloaded ? $downloaded[ $slug ]['downloaded_at'] : '';
?>
<div class="mi-set-card <?php echo $is_active ? 'mi-set-active' : ''; ?> <?php echo $is_downloaded ? 'mi-set-downloaded' : ''; ?>" data-slug="<?php echo esc_attr( $slug ); ?>">
<div class="mi-set-header">
<h3 class="mi-set-name"><?php echo esc_html( $set['name'] ); ?></h3>
<?php if ( $is_active ) : ?>
<span class="mi-badge mi-badge-active"><?php esc_html_e( 'Active', 'maple-icons' ); ?></span>
<?php elseif ( $is_downloaded ) : ?>
<span class="mi-badge mi-badge-downloaded"><?php esc_html_e( 'Downloaded', 'maple-icons' ); ?></span>
<?php endif; ?>
</div>
<div class="mi-set-meta">
<span class="mi-set-license">
<?php
/* translators: %s: License name */
printf( esc_html__( 'License: %s', 'maple-icons' ), esc_html( $set['license'] ) );
?>
</span>
<span class="mi-set-styles">
<?php
/* translators: %s: Style names */
printf( esc_html__( 'Styles: %s', 'maple-icons' ), esc_html( implode( ', ', $set['styles'] ) ) );
?>
</span>
<?php if ( $is_downloaded && $icon_count > 0 ) : ?>
<span class="mi-set-count">
<?php
/* translators: %d: Number of icons */
printf( esc_html( _n( '%d icon', '%d icons', $icon_count, 'maple-icons' ) ), intval( $icon_count ) );
?>
</span>
<?php endif; ?>
</div>
<?php if ( ! empty( $set['url'] ) ) : ?>
<a href="<?php echo esc_url( $set['url'] ); ?>" class="mi-set-link" target="_blank" rel="noopener noreferrer">
<?php esc_html_e( 'View on website', 'maple-icons' ); ?> &rarr;
</a>
<?php endif; ?>
<div class="mi-set-actions">
<?php if ( ! $is_downloaded ) : ?>
<button type="button" class="button button-primary mi-download-btn" data-slug="<?php echo esc_attr( $slug ); ?>">
<?php esc_html_e( 'Download', 'maple-icons' ); ?>
</button>
<?php else : ?>
<?php if ( ! $is_active ) : ?>
<button type="button" class="button button-primary mi-activate-btn" data-slug="<?php echo esc_attr( $slug ); ?>">
<?php esc_html_e( 'Set Active', 'maple-icons' ); ?>
</button>
<?php else : ?>
<button type="button" class="button mi-deactivate-btn" data-slug="">
<?php esc_html_e( 'Deactivate', 'maple-icons' ); ?>
</button>
<?php endif; ?>
<button type="button" class="button mi-delete-btn" data-slug="<?php echo esc_attr( $slug ); ?>">
<?php esc_html_e( 'Delete', 'maple-icons' ); ?>
</button>
<?php endif; ?>
</div>
<div class="mi-set-progress" style="display: none;">
<div class="mi-progress-bar">
<div class="mi-progress-fill"></div>
</div>
<span class="mi-progress-text"></span>
</div>
<div class="mi-set-message" style="display: none;"></div>
</div>
<?php endforeach; ?>
</div>
</div>
<div class="mi-admin-usage">
<h2><?php esc_html_e( 'How to Use', 'maple-icons' ); ?></h2>
<ol>
<li><?php esc_html_e( 'Download one or more icon sets above.', 'maple-icons' ); ?></li>
<li><?php esc_html_e( 'Set one icon set as active.', 'maple-icons' ); ?></li>
<li><?php esc_html_e( 'In the Gutenberg editor, add a "Maple Icon" block.', 'maple-icons' ); ?></li>
<li><?php esc_html_e( 'Search and select an icon from your active set.', 'maple-icons' ); ?></li>
<li><?php esc_html_e( 'Customize size, color, and other settings in the block sidebar.', 'maple-icons' ); ?></li>
</ol>
</div>
<div class="mi-admin-info">
<h2><?php esc_html_e( 'About', 'maple-icons' ); ?></h2>
<p>
<?php esc_html_e( 'Maple Icons downloads SVG icons from CDN and stores them locally in your WordPress installation. Icons are sanitized for security and normalized for consistent rendering.', 'maple-icons' ); ?>
</p>
<p>
<?php esc_html_e( 'All icons use currentColor for styling, which means they automatically inherit text color from your theme or block settings.', 'maple-icons' ); ?>
</p>
<p>
<?php
printf(
/* translators: %s: Directory path */
esc_html__( 'Icons are stored in: %s', 'maple-icons' ),
'<code>' . esc_html( MI_ICONS_DIR ) . '</code>'
);
?>
</p>
</div>
</div>
<?php
}
}

View file

@ -0,0 +1,376 @@
<?php
/**
* AJAX Handler class - Handles all AJAX requests.
*
* @package MapleIcons
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class MI_Ajax_Handler
*
* Handles all AJAX requests for the plugin.
*/
class MI_Ajax_Handler {
/**
* Constructor - Register AJAX handlers.
*/
public function __construct() {
// Admin AJAX handlers.
add_action( 'wp_ajax_mi_download_set', array( $this, 'handle_download_set' ) );
add_action( 'wp_ajax_mi_delete_set', array( $this, 'handle_delete_set' ) );
add_action( 'wp_ajax_mi_set_active', array( $this, 'handle_set_active' ) );
add_action( 'wp_ajax_mi_get_progress', array( $this, 'handle_get_progress' ) );
// Block editor AJAX handlers.
add_action( 'wp_ajax_mi_search_icons', array( $this, 'handle_search_icons' ) );
add_action( 'wp_ajax_mi_get_icon_svg', array( $this, 'handle_get_icon_svg' ) );
// No nopriv handlers - all functionality requires login.
}
/**
* Handle download set request.
*/
public function handle_download_set() {
// 1. Nonce verification.
if ( ! check_ajax_referer( 'mi_admin_nonce', 'nonce', false ) ) {
wp_send_json_error(
array( 'message' => __( 'Security check failed.', 'maple-icons' ) ),
403
);
}
// 2. Capability check.
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error(
array( 'message' => __( 'You do not have permission to do this.', 'maple-icons' ) ),
403
);
}
// 3. Input validation.
$slug = isset( $_POST['slug'] ) ? sanitize_key( $_POST['slug'] ) : '';
if ( empty( $slug ) ) {
wp_send_json_error(
array( 'message' => __( 'Icon set slug is required.', 'maple-icons' ) )
);
}
if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
wp_send_json_error(
array( 'message' => __( 'Invalid icon set.', 'maple-icons' ) )
);
}
// Check if already downloaded.
$registry = MI_Icon_Registry::get_instance();
if ( $registry->is_downloaded( $slug ) ) {
wp_send_json_error(
array( 'message' => __( 'This icon set is already downloaded.', 'maple-icons' ) )
);
}
// 4. Process download.
// Increase time limit for large downloads.
set_time_limit( 600 ); // 10 minutes.
$downloader = new MI_Downloader();
$result = $downloader->download_set( $slug );
if ( is_wp_error( $result ) ) {
wp_send_json_error(
array( 'message' => $result->get_error_message() )
);
}
if ( ! $result['success'] ) {
wp_send_json_error(
array(
'message' => sprintf(
/* translators: %1$d: downloaded count, %2$d: failed count */
__( 'Download completed with errors. %1$d downloaded, %2$d failed.', 'maple-icons' ),
$result['downloaded'],
$result['failed']
),
'errors' => array_slice( $result['errors'], 0, 10 ), // Limit errors shown.
'downloaded' => $result['downloaded'],
'failed' => $result['failed'],
)
);
}
// Mark as downloaded.
$registry->mark_downloaded( $slug, $result['downloaded'] );
// If no active set, make this one active.
if ( ! $registry->get_active_set() ) {
$registry->set_active( $slug );
}
$registry->refresh();
wp_send_json_success(
array(
'message' => sprintf(
/* translators: %d: number of icons */
__( 'Successfully downloaded %d icons.', 'maple-icons' ),
$result['downloaded']
),
'icon_count' => $result['downloaded'],
'is_active' => $registry->get_active_set() === $slug,
)
);
}
/**
* Handle delete set request.
*/
public function handle_delete_set() {
// 1. Nonce verification.
if ( ! check_ajax_referer( 'mi_admin_nonce', 'nonce', false ) ) {
wp_send_json_error(
array( 'message' => __( 'Security check failed.', 'maple-icons' ) ),
403
);
}
// 2. Capability check.
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error(
array( 'message' => __( 'You do not have permission to do this.', 'maple-icons' ) ),
403
);
}
// 3. Input validation.
$slug = isset( $_POST['slug'] ) ? sanitize_key( $_POST['slug'] ) : '';
if ( empty( $slug ) ) {
wp_send_json_error(
array( 'message' => __( 'Icon set slug is required.', 'maple-icons' ) )
);
}
if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
wp_send_json_error(
array( 'message' => __( 'Invalid icon set.', 'maple-icons' ) )
);
}
// 4. Delete the set.
$registry = MI_Icon_Registry::get_instance();
$result = $registry->delete_set( $slug );
if ( is_wp_error( $result ) ) {
wp_send_json_error(
array( 'message' => $result->get_error_message() )
);
}
wp_send_json_success(
array(
'message' => __( 'Icon set deleted successfully.', 'maple-icons' ),
)
);
}
/**
* Handle set active request.
*/
public function handle_set_active() {
// 1. Nonce verification.
if ( ! check_ajax_referer( 'mi_admin_nonce', 'nonce', false ) ) {
wp_send_json_error(
array( 'message' => __( 'Security check failed.', 'maple-icons' ) ),
403
);
}
// 2. Capability check.
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error(
array( 'message' => __( 'You do not have permission to do this.', 'maple-icons' ) ),
403
);
}
// 3. Input validation.
$slug = isset( $_POST['slug'] ) ? sanitize_key( $_POST['slug'] ) : '';
// Empty slug is allowed (to deactivate).
if ( ! empty( $slug ) && ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
wp_send_json_error(
array( 'message' => __( 'Invalid icon set.', 'maple-icons' ) )
);
}
// 4. Set active.
$registry = MI_Icon_Registry::get_instance();
$result = $registry->set_active( $slug );
if ( is_wp_error( $result ) ) {
wp_send_json_error(
array( 'message' => $result->get_error_message() )
);
}
wp_send_json_success(
array(
'message' => empty( $slug )
? __( 'No icon set is now active.', 'maple-icons' )
: __( 'Icon set activated.', 'maple-icons' ),
'active_set' => $slug,
)
);
}
/**
* Handle get progress request.
*/
public function handle_get_progress() {
// 1. Nonce verification.
if ( ! check_ajax_referer( 'mi_admin_nonce', 'nonce', false ) ) {
wp_send_json_error(
array( 'message' => __( 'Security check failed.', 'maple-icons' ) ),
403
);
}
// 2. Capability check.
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error(
array( 'message' => __( 'You do not have permission to do this.', 'maple-icons' ) ),
403
);
}
// 3. Input validation.
$slug = isset( $_POST['slug'] ) ? sanitize_key( $_POST['slug'] ) : '';
if ( empty( $slug ) || ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
wp_send_json_error(
array( 'message' => __( 'Invalid icon set.', 'maple-icons' ) )
);
}
// 4. Get progress.
$downloader = new MI_Downloader();
$progress = $downloader->get_progress( $slug );
if ( false === $progress ) {
wp_send_json_success(
array(
'downloading' => false,
'completed' => 0,
'total' => 0,
)
);
}
wp_send_json_success(
array(
'downloading' => true,
'completed' => $progress['completed'],
'total' => $progress['total'],
'percentage' => $progress['total'] > 0
? round( ( $progress['completed'] / $progress['total'] ) * 100 )
: 0,
)
);
}
/**
* Handle search icons request (for block editor).
*/
public function handle_search_icons() {
// 1. Nonce verification.
if ( ! check_ajax_referer( 'mi_block_nonce', 'nonce', false ) ) {
wp_send_json_error(
array( 'message' => __( 'Security check failed.', 'maple-icons' ) ),
403
);
}
// 2. Capability check (edit_posts for block usage).
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error(
array( 'message' => __( 'You do not have permission to do this.', 'maple-icons' ) ),
403
);
}
// 3. Input validation.
$query = isset( $_POST['query'] ) ? sanitize_text_field( $_POST['query'] ) : '';
$style = isset( $_POST['style'] ) ? sanitize_key( $_POST['style'] ) : '';
$limit = isset( $_POST['limit'] ) ? absint( $_POST['limit'] ) : MI_SEARCH_LIMIT;
$offset = isset( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 0;
// Limit the limit.
if ( $limit > 100 ) {
$limit = 100;
}
// 4. Search.
$registry = MI_Icon_Registry::get_instance();
$results = $registry->search_icons( $query, $style, $limit, $offset );
wp_send_json_success( $results );
}
/**
* Handle get icon SVG request (for block editor).
*/
public function handle_get_icon_svg() {
// 1. Nonce verification.
if ( ! check_ajax_referer( 'mi_block_nonce', 'nonce', false ) ) {
wp_send_json_error(
array( 'message' => __( 'Security check failed.', 'maple-icons' ) ),
403
);
}
// 2. Capability check.
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error(
array( 'message' => __( 'You do not have permission to do this.', 'maple-icons' ) ),
403
);
}
// 3. Input validation.
$slug = isset( $_POST['set'] ) ? sanitize_key( $_POST['set'] ) : '';
$style = isset( $_POST['style'] ) ? sanitize_key( $_POST['style'] ) : '';
$name = isset( $_POST['name'] ) ? sanitize_file_name( $_POST['name'] ) : '';
if ( empty( $slug ) || empty( $style ) || empty( $name ) ) {
wp_send_json_error(
array( 'message' => __( 'Missing required parameters.', 'maple-icons' ) )
);
}
// 4. Get SVG.
$registry = MI_Icon_Registry::get_instance();
$svg = $registry->get_icon_svg( $slug, $style, $name );
if ( is_wp_error( $svg ) ) {
wp_send_json_error(
array( 'message' => $svg->get_error_message() )
);
}
wp_send_json_success(
array(
'svg' => $svg,
'set' => $slug,
'style' => $style,
'name' => $name,
)
);
}
}

View file

@ -0,0 +1,505 @@
<?php
/**
* Downloader class - Handles fetching icons from CDN and storing locally.
*
* @package MapleIcons
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class MI_Downloader
*
* Downloads icon sets from CDN and stores them locally.
*/
class MI_Downloader {
/**
* Allowed SVG elements for sanitization.
*
* @var array
*/
private static $allowed_svg_elements = array(
'svg' => array(
'xmlns' => true,
'viewbox' => true,
'width' => true,
'height' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'stroke-linecap' => true,
'stroke-linejoin' => true,
'class' => true,
'aria-hidden' => true,
'role' => true,
'focusable' => true,
'style' => true,
),
'path' => array(
'd' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'stroke-linecap' => true,
'stroke-linejoin' => true,
'fill-rule' => true,
'clip-rule' => true,
'opacity' => true,
'fill-opacity' => true,
'stroke-opacity' => true,
),
'circle' => array(
'cx' => true,
'cy' => true,
'r' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'opacity' => true,
),
'rect' => array(
'x' => true,
'y' => true,
'width' => true,
'height' => true,
'rx' => true,
'ry' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'opacity' => true,
),
'line' => array(
'x1' => true,
'y1' => true,
'x2' => true,
'y2' => true,
'stroke' => true,
'stroke-width' => true,
'stroke-linecap' => true,
'opacity' => true,
),
'polyline' => array(
'points' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'stroke-linecap' => true,
'stroke-linejoin' => true,
'opacity' => true,
),
'polygon' => array(
'points' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'opacity' => true,
),
'ellipse' => array(
'cx' => true,
'cy' => true,
'rx' => true,
'ry' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'opacity' => true,
),
'g' => array(
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'transform' => true,
'opacity' => true,
),
'defs' => array(),
'clippath' => array(
'id' => true,
),
'use' => array(
'href' => true,
'xlink:href' => true,
),
);
/**
* Download an entire icon set.
*
* @param string $slug Icon set slug.
* @param callable|null $progress_callback Optional progress callback.
* @return array|WP_Error Download results or error.
*/
public function download_set( $slug, $progress_callback = null ) {
// Validate slug.
if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
return new WP_Error(
'invalid_set',
__( 'Invalid icon set.', 'maple-icons' )
);
}
$set_config = MI_Icon_Sets::get( $slug );
$manifest = MI_Icon_Sets::load_manifest( $slug );
if ( is_wp_error( $manifest ) ) {
return $manifest;
}
// Create base directory.
$base_dir = MI_ICONS_DIR . $slug;
if ( ! $this->ensure_directory( $base_dir ) ) {
return new WP_Error(
'directory_error',
__( 'Could not create icon directory.', 'maple-icons' )
);
}
// Create style directories.
foreach ( $set_config['styles'] as $style_slug => $style_config ) {
$style_dir = $base_dir . '/' . $style_slug;
if ( ! $this->ensure_directory( $style_dir ) ) {
return new WP_Error(
'directory_error',
__( 'Could not create style directory.', 'maple-icons' )
);
}
}
$icons = isset( $manifest['icons'] ) ? $manifest['icons'] : array();
$total_icons = count( $icons );
$downloaded = 0;
$failed = 0;
$errors = array();
$total_to_download = 0;
// Calculate total icons to download (icon × styles).
foreach ( $icons as $icon ) {
$icon_styles = isset( $icon['styles'] ) ? $icon['styles'] : array_keys( $set_config['styles'] );
$total_to_download += count( $icon_styles );
}
// Initialize progress transient.
set_transient(
'mi_download_progress_' . $slug,
array(
'completed' => 0,
'total' => $total_to_download,
'status' => 'downloading',
),
HOUR_IN_SECONDS
);
$current = 0;
// Download each icon.
foreach ( $icons as $icon ) {
$icon_name = $icon['name'];
$icon_styles = isset( $icon['styles'] ) ? $icon['styles'] : array_keys( $set_config['styles'] );
foreach ( $icon_styles as $style ) {
if ( ! isset( $set_config['styles'][ $style ] ) ) {
continue;
}
$result = $this->download_icon( $slug, $style, $icon_name );
if ( is_wp_error( $result ) ) {
$failed++;
$errors[] = sprintf( '%s/%s: %s', $style, $icon_name, $result->get_error_message() );
} else {
$downloaded++;
}
$current++;
// Update progress.
set_transient(
'mi_download_progress_' . $slug,
array(
'completed' => $current,
'total' => $total_to_download,
'status' => 'downloading',
),
HOUR_IN_SECONDS
);
// Call progress callback if provided.
if ( is_callable( $progress_callback ) ) {
call_user_func( $progress_callback, $current, $total_to_download );
}
// Allow some breathing room for the server.
if ( 0 === $current % MI_DOWNLOAD_BATCH_SIZE ) {
usleep( 100000 ); // 100ms pause every batch.
}
}
}
// Clear progress transient.
delete_transient( 'mi_download_progress_' . $slug );
return array(
'success' => $failed === 0,
'downloaded' => $downloaded,
'failed' => $failed,
'total' => $total_to_download,
'errors' => $errors,
);
}
/**
* Download a single icon from CDN.
*
* @param string $slug Icon set slug.
* @param string $style Style slug.
* @param string $name Icon name.
* @return string|WP_Error Local file path or error.
*/
public function download_icon( $slug, $style, $name ) {
// Validate inputs.
if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
return new WP_Error( 'invalid_set', __( 'Invalid icon set.', 'maple-icons' ) );
}
if ( ! MI_Icon_Sets::is_valid_style( $slug, $style ) ) {
return new WP_Error( 'invalid_style', __( 'Invalid icon style.', 'maple-icons' ) );
}
// Validate icon name (only allow alphanumeric, hyphens, underscores).
if ( ! preg_match( '/^[a-z0-9\-_]+$/i', $name ) ) {
return new WP_Error( 'invalid_name', __( 'Invalid icon name.', 'maple-icons' ) );
}
$cdn_url = MI_Icon_Sets::get_cdn_url( $slug, $style, $name );
$local_path = MI_Icon_Sets::get_local_path( $slug, $style, $name );
// Check if already downloaded.
if ( file_exists( $local_path ) ) {
return $local_path;
}
// Fetch from CDN.
$response = wp_remote_get(
$cdn_url,
array(
'timeout' => MI_DOWNLOAD_TIMEOUT,
'sslverify' => true,
)
);
if ( is_wp_error( $response ) ) {
return $response;
}
$status_code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
return new WP_Error(
'cdn_error',
sprintf(
/* translators: %d: HTTP status code */
__( 'CDN returned status %d.', 'maple-icons' ),
$status_code
)
);
}
$svg_content = wp_remote_retrieve_body( $response );
if ( empty( $svg_content ) ) {
return new WP_Error( 'empty_response', __( 'Empty response from CDN.', 'maple-icons' ) );
}
// Get set config for normalization.
$set_config = MI_Icon_Sets::get( $slug );
// Normalize and sanitize SVG.
$svg_content = $this->normalize_svg( $svg_content, $set_config );
$svg_content = $this->sanitize_svg( $svg_content );
if ( empty( $svg_content ) ) {
return new WP_Error( 'invalid_svg', __( 'Invalid or empty SVG content.', 'maple-icons' ) );
}
// Ensure directory exists.
$dir = dirname( $local_path );
if ( ! $this->ensure_directory( $dir ) ) {
return new WP_Error( 'directory_error', __( 'Could not create directory.', 'maple-icons' ) );
}
// Write file.
global $wp_filesystem;
if ( empty( $wp_filesystem ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
WP_Filesystem();
}
if ( ! $wp_filesystem->put_contents( $local_path, $svg_content, FS_CHMOD_FILE ) ) {
return new WP_Error( 'write_error', __( 'Could not write SVG file.', 'maple-icons' ) );
}
return $local_path;
}
/**
* Normalize SVG content based on set configuration.
*
* @param string $svg SVG content.
* @param array $set_config Icon set configuration.
* @return string Normalized SVG content.
*/
private function normalize_svg( $svg, $set_config ) {
// Strip XML declaration.
$svg = preg_replace( '/<\?xml[^>]*\?>/i', '', $svg );
// Strip DOCTYPE.
$svg = preg_replace( '/<!DOCTYPE[^>]*>/i', '', $svg );
// Strip comments.
$svg = preg_replace( '/<!--.*?-->/s', '', $svg );
// Strip title and desc elements.
$svg = preg_replace( '/<title[^>]*>.*?<\/title>/is', '', $svg );
$svg = preg_replace( '/<desc[^>]*>.*?<\/desc>/is', '', $svg );
// Normalize viewBox for Phosphor (256 → 24).
if ( ! empty( $set_config['normalize'] ) ) {
$svg = preg_replace(
'/viewBox=["\']0\s+0\s+256\s+256["\']/i',
'viewBox="0 0 24 24"',
$svg
);
}
// Fix hardcoded colors for Material.
if ( ! empty( $set_config['color_fix'] ) ) {
// Replace hardcoded hex colors.
$svg = preg_replace( '/fill=["\']#[0-9a-fA-F]{3,6}["\']/', 'fill="currentColor"', $svg );
$svg = preg_replace( '/fill=["\']black["\']/', 'fill="currentColor"', $svg );
$svg = preg_replace( '/fill=["\']rgb\([^)]+\)["\']/', 'fill="currentColor"', $svg );
// Same for stroke.
$svg = preg_replace( '/stroke=["\']#[0-9a-fA-F]{3,6}["\']/', 'stroke="currentColor"', $svg );
$svg = preg_replace( '/stroke=["\']black["\']/', 'stroke="currentColor"', $svg );
}
// Remove width/height attributes (let CSS control size).
$svg = preg_replace( '/\s(width|height)=["\'][^"\']*["\']/i', '', $svg );
// Ensure there's no leading/trailing whitespace.
$svg = trim( $svg );
return $svg;
}
/**
* Sanitize SVG content to remove potentially dangerous elements.
*
* @param string $svg SVG content.
* @return string Sanitized SVG content.
*/
private function sanitize_svg( $svg ) {
// Remove script tags.
$svg = preg_replace( '/<script\b[^>]*>.*?<\/script>/is', '', $svg );
// Remove event handlers.
$svg = preg_replace( '/\s+on\w+\s*=/i', ' data-removed=', $svg );
// Remove javascript: URLs.
$svg = preg_replace( '/javascript:/i', 'removed:', $svg );
// Remove data: URLs (except for certain safe uses).
$svg = preg_replace( '/data:[^"\'>\s]+/i', 'removed:', $svg );
// Use WordPress kses with our allowed tags.
$svg = wp_kses( $svg, self::$allowed_svg_elements );
// Verify it's still a valid SVG.
if ( strpos( $svg, '<svg' ) === false || strpos( $svg, '</svg>' ) === false ) {
return '';
}
return $svg;
}
/**
* Ensure a directory exists, creating it if necessary.
*
* @param string $dir Directory path.
* @return bool True if directory exists or was created.
*/
private function ensure_directory( $dir ) {
if ( file_exists( $dir ) ) {
return is_dir( $dir );
}
return wp_mkdir_p( $dir );
}
/**
* Delete all icons for a set.
*
* @param string $slug Icon set slug.
* @return bool True on success.
*/
public function delete_set( $slug ) {
if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
return false;
}
$set_dir = MI_ICONS_DIR . $slug;
// Validate the path is within our icons directory.
$real_path = realpath( $set_dir );
$allowed_base = realpath( MI_ICONS_DIR );
if ( false === $real_path || false === $allowed_base ) {
// Directory doesn't exist, nothing to delete.
return true;
}
if ( strpos( $real_path, $allowed_base ) !== 0 ) {
// Path traversal attempt.
return false;
}
// Recursively delete the directory.
return $this->delete_directory( $set_dir );
}
/**
* Recursively delete a directory.
*
* @param string $dir Directory path.
* @return bool True on success.
*/
private function delete_directory( $dir ) {
if ( ! is_dir( $dir ) ) {
return true;
}
$files = array_diff( scandir( $dir ), array( '.', '..' ) );
foreach ( $files as $file ) {
$path = $dir . '/' . $file;
if ( is_dir( $path ) ) {
$this->delete_directory( $path );
} else {
wp_delete_file( $path );
}
}
return rmdir( $dir );
}
/**
* Get download progress for a set.
*
* @param string $slug Icon set slug.
* @return array|false Progress data or false if not downloading.
*/
public function get_progress( $slug ) {
return get_transient( 'mi_download_progress_' . $slug );
}
}

View file

@ -0,0 +1,416 @@
<?php
/**
* Icon Registry class - Manages downloaded sets and provides icon search/retrieval.
*
* @package MapleIcons
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class MI_Icon_Registry
*
* Manages downloaded icon sets and provides search/retrieval functionality.
*/
class MI_Icon_Registry {
/**
* Singleton instance.
*
* @var MI_Icon_Registry|null
*/
private static $instance = null;
/**
* Cached settings.
*
* @var array|null
*/
private $settings = null;
/**
* Get singleton instance.
*
* @return MI_Icon_Registry
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor.
*/
private function __construct() {
$this->load_settings();
}
/**
* Load settings from database.
*/
private function load_settings() {
$this->settings = get_option(
'maple_icons_settings',
array(
'active_set' => '',
'downloaded_sets' => array(),
)
);
}
/**
* Save settings to database.
*
* @return bool True on success.
*/
private function save_settings() {
return update_option( 'maple_icons_settings', $this->settings );
}
/**
* Get all downloaded sets.
*
* @return array Array of downloaded set data.
*/
public function get_downloaded_sets() {
return isset( $this->settings['downloaded_sets'] ) ? $this->settings['downloaded_sets'] : array();
}
/**
* Get the active set slug.
*
* @return string|null Active set slug or null if none.
*/
public function get_active_set() {
$active = isset( $this->settings['active_set'] ) ? $this->settings['active_set'] : '';
return ! empty( $active ) ? $active : null;
}
/**
* Set the active icon set.
*
* @param string $slug Icon set slug.
* @return bool|WP_Error True on success, error on failure.
*/
public function set_active( $slug ) {
// Allow empty string to deactivate.
if ( empty( $slug ) ) {
$this->settings['active_set'] = '';
return $this->save_settings();
}
// Validate slug.
if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
return new WP_Error( 'invalid_set', __( 'Invalid icon set.', 'maple-icons' ) );
}
// Check if downloaded.
if ( ! $this->is_downloaded( $slug ) ) {
return new WP_Error( 'not_downloaded', __( 'Icon set is not downloaded.', 'maple-icons' ) );
}
$this->settings['active_set'] = $slug;
return $this->save_settings();
}
/**
* Check if a set is downloaded.
*
* @param string $slug Icon set slug.
* @return bool True if downloaded.
*/
public function is_downloaded( $slug ) {
$downloaded = $this->get_downloaded_sets();
return isset( $downloaded[ $slug ] );
}
/**
* Mark a set as downloaded.
*
* @param string $slug Icon set slug.
* @param int $icon_count Number of icons downloaded.
* @return bool True on success.
*/
public function mark_downloaded( $slug, $icon_count ) {
if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
return false;
}
$set_config = MI_Icon_Sets::get( $slug );
$this->settings['downloaded_sets'][ $slug ] = array(
'version' => $set_config['version'],
'downloaded_at' => current_time( 'mysql' ),
'icon_count' => $icon_count,
);
return $this->save_settings();
}
/**
* Remove a set from downloaded list.
*
* @param string $slug Icon set slug.
* @return bool True on success.
*/
public function unmark_downloaded( $slug ) {
if ( isset( $this->settings['downloaded_sets'][ $slug ] ) ) {
unset( $this->settings['downloaded_sets'][ $slug ] );
}
// If this was the active set, clear it.
if ( $this->settings['active_set'] === $slug ) {
$this->settings['active_set'] = '';
}
return $this->save_settings();
}
/**
* Get all icons for a set and style.
*
* @param string $slug Icon set slug.
* @param string $style Style slug.
* @return array Array of icon data.
*/
public function get_icons_for_set( $slug, $style ) {
if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
return array();
}
if ( ! MI_Icon_Sets::is_valid_style( $slug, $style ) ) {
return array();
}
// Load manifest.
$manifest = MI_Icon_Sets::load_manifest( $slug );
if ( is_wp_error( $manifest ) ) {
return array();
}
$icons = isset( $manifest['icons'] ) ? $manifest['icons'] : array();
$result = array();
foreach ( $icons as $icon ) {
$icon_styles = isset( $icon['styles'] ) ? $icon['styles'] : array_keys( MI_Icon_Sets::get( $slug )['styles'] );
// Only include if this icon has the requested style.
if ( in_array( $style, $icon_styles, true ) ) {
$result[] = array(
'name' => $icon['name'],
'tags' => isset( $icon['tags'] ) ? $icon['tags'] : array(),
'category' => isset( $icon['category'] ) ? $icon['category'] : '',
);
}
}
return $result;
}
/**
* Search icons in the active set.
*
* @param string $query Search query.
* @param string $style Optional style filter.
* @param int $limit Maximum results.
* @param int $offset Offset for pagination.
* @return array Search results.
*/
public function search_icons( $query = '', $style = '', $limit = MI_SEARCH_LIMIT, $offset = 0 ) {
$active_set = $this->get_active_set();
if ( ! $active_set ) {
return array(
'icons' => array(),
'total' => 0,
);
}
$set_config = MI_Icon_Sets::get( $active_set );
if ( ! $set_config ) {
return array(
'icons' => array(),
'total' => 0,
);
}
// Default to first style if not specified.
if ( empty( $style ) ) {
$style = $set_config['default_style'];
}
// Validate style.
if ( ! MI_Icon_Sets::is_valid_style( $active_set, $style ) ) {
$style = $set_config['default_style'];
}
// Load manifest.
$manifest = MI_Icon_Sets::load_manifest( $active_set );
if ( is_wp_error( $manifest ) ) {
return array(
'icons' => array(),
'total' => 0,
);
}
$icons = isset( $manifest['icons'] ) ? $manifest['icons'] : array();
$query = strtolower( trim( $query ) );
$results = array();
foreach ( $icons as $icon ) {
$icon_styles = isset( $icon['styles'] ) ? $icon['styles'] : array_keys( $set_config['styles'] );
// Skip if this icon doesn't have the requested style.
if ( ! in_array( $style, $icon_styles, true ) ) {
continue;
}
// If no query, include all.
if ( empty( $query ) ) {
$results[] = $icon;
continue;
}
// Search in name.
if ( strpos( strtolower( $icon['name'] ), $query ) !== false ) {
$results[] = $icon;
continue;
}
// Search in tags.
if ( isset( $icon['tags'] ) && is_array( $icon['tags'] ) ) {
foreach ( $icon['tags'] as $tag ) {
if ( strpos( strtolower( $tag ), $query ) !== false ) {
$results[] = $icon;
break;
}
}
}
}
$total = count( $results );
// Apply offset and limit.
$results = array_slice( $results, $offset, $limit );
// Format results.
$formatted = array();
foreach ( $results as $icon ) {
$formatted[] = array(
'name' => $icon['name'],
'tags' => isset( $icon['tags'] ) ? $icon['tags'] : array(),
'category' => isset( $icon['category'] ) ? $icon['category'] : '',
'set' => $active_set,
'style' => $style,
);
}
return array(
'icons' => $formatted,
'total' => $total,
'set' => $active_set,
'style' => $style,
'styles' => MI_Icon_Sets::get_style_labels( $active_set ),
);
}
/**
* Get SVG content for a specific icon.
*
* @param string $slug Icon set slug.
* @param string $style Style slug.
* @param string $name Icon name.
* @return string|WP_Error SVG content or error.
*/
public function get_icon_svg( $slug, $style, $name ) {
// Validate inputs.
if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
return new WP_Error( 'invalid_set', __( 'Invalid icon set.', 'maple-icons' ) );
}
if ( ! MI_Icon_Sets::is_valid_style( $slug, $style ) ) {
return new WP_Error( 'invalid_style', __( 'Invalid icon style.', 'maple-icons' ) );
}
// Validate icon name.
if ( ! preg_match( '/^[a-z0-9\-_]+$/i', $name ) ) {
return new WP_Error( 'invalid_name', __( 'Invalid icon name.', 'maple-icons' ) );
}
$local_path = MI_Icon_Sets::get_local_path( $slug, $style, $name );
// Validate path is within icons directory (prevent path traversal).
$real_path = realpath( $local_path );
$allowed_base = realpath( MI_ICONS_DIR );
if ( false === $real_path ) {
return new WP_Error( 'not_found', __( 'Icon not found.', 'maple-icons' ) );
}
if ( false === $allowed_base || strpos( $real_path, $allowed_base ) !== 0 ) {
return new WP_Error( 'invalid_path', __( 'Invalid icon path.', 'maple-icons' ) );
}
if ( ! file_exists( $real_path ) ) {
return new WP_Error( 'not_found', __( 'Icon not found.', 'maple-icons' ) );
}
$svg = file_get_contents( $real_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
if ( false === $svg ) {
return new WP_Error( 'read_error', __( 'Could not read icon file.', 'maple-icons' ) );
}
return $svg;
}
/**
* Delete a downloaded set.
*
* @param string $slug Icon set slug.
* @return bool|WP_Error True on success, error on failure.
*/
public function delete_set( $slug ) {
if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
return new WP_Error( 'invalid_set', __( 'Invalid icon set.', 'maple-icons' ) );
}
if ( ! $this->is_downloaded( $slug ) ) {
return new WP_Error( 'not_downloaded', __( 'Icon set is not downloaded.', 'maple-icons' ) );
}
// Delete files.
$downloader = new MI_Downloader();
$deleted = $downloader->delete_set( $slug );
if ( ! $deleted ) {
return new WP_Error( 'delete_error', __( 'Could not delete icon files.', 'maple-icons' ) );
}
// Update settings.
$this->unmark_downloaded( $slug );
return true;
}
/**
* Get info about a downloaded set.
*
* @param string $slug Icon set slug.
* @return array|null Set info or null if not downloaded.
*/
public function get_downloaded_info( $slug ) {
$downloaded = $this->get_downloaded_sets();
return isset( $downloaded[ $slug ] ) ? $downloaded[ $slug ] : null;
}
/**
* Refresh settings from database.
*/
public function refresh() {
$this->load_settings();
}
}

View file

@ -0,0 +1,328 @@
<?php
/**
* Icon Sets class - Static definitions of all preset icon sets.
*
* @package MapleIcons
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class MI_Icon_Sets
*
* Provides static definitions and utilities for preset icon sets.
*/
class MI_Icon_Sets {
/**
* Get all available preset icon sets.
*
* @return array Array of icon set configurations.
*/
public static function get_all() {
return array(
'heroicons' => array(
'slug' => 'heroicons',
'name' => 'Heroicons',
'version' => '2.1.1',
'license' => 'MIT',
'url' => 'https://heroicons.com',
'cdn_base' => 'https://cdn.jsdelivr.net/npm/heroicons@2.1.1/',
'styles' => array(
'outline' => array(
'path' => '24/outline',
'label' => 'Outline',
),
'solid' => array(
'path' => '24/solid',
'label' => 'Solid',
),
'mini' => array(
'path' => '20/solid',
'label' => 'Mini',
),
),
'default_style' => 'outline',
'viewbox' => '0 0 24 24',
'normalize' => false,
'color_fix' => false,
'description' => 'Beautiful hand-crafted SVG icons by the makers of Tailwind CSS.',
'icon_count' => 292,
),
'lucide' => array(
'slug' => 'lucide',
'name' => 'Lucide',
'version' => '0.303.0',
'license' => 'ISC',
'url' => 'https://lucide.dev',
'cdn_base' => 'https://cdn.jsdelivr.net/npm/lucide-static@0.303.0/',
'styles' => array(
'icons' => array(
'path' => 'icons',
'label' => 'Default',
),
),
'default_style' => 'icons',
'viewbox' => '0 0 24 24',
'normalize' => false,
'color_fix' => false,
'description' => 'Beautiful & consistent icon toolkit made by the community.',
'icon_count' => 1411,
),
'feather' => array(
'slug' => 'feather',
'name' => 'Feather',
'version' => '4.29.1',
'license' => 'MIT',
'url' => 'https://feathericons.com',
'cdn_base' => 'https://cdn.jsdelivr.net/npm/feather-icons@4.29.1/',
'styles' => array(
'icons' => array(
'path' => 'dist/icons',
'label' => 'Default',
),
),
'default_style' => 'icons',
'viewbox' => '0 0 24 24',
'normalize' => false,
'color_fix' => false,
'description' => 'Simply beautiful open source icons.',
'icon_count' => 287,
),
'phosphor' => array(
'slug' => 'phosphor',
'name' => 'Phosphor',
'version' => '2.1.1',
'license' => 'MIT',
'url' => 'https://phosphoricons.com',
'cdn_base' => 'https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2.1.1/',
'styles' => array(
'regular' => array(
'path' => 'assets/regular',
'label' => 'Regular',
),
'bold' => array(
'path' => 'assets/bold',
'label' => 'Bold',
),
'light' => array(
'path' => 'assets/light',
'label' => 'Light',
),
'thin' => array(
'path' => 'assets/thin',
'label' => 'Thin',
),
'fill' => array(
'path' => 'assets/fill',
'label' => 'Fill',
),
'duotone' => array(
'path' => 'assets/duotone',
'label' => 'Duotone',
),
),
'default_style' => 'regular',
'viewbox' => '0 0 256 256',
'normalize' => true, // Needs viewBox normalization to 24x24.
'color_fix' => false,
'description' => 'A flexible icon family for interfaces, diagrams, presentations, and more.',
'icon_count' => 1248,
),
'material' => array(
'slug' => 'material',
'name' => 'Material Design Icons',
'version' => '0.14.13',
'license' => 'Apache-2.0',
'url' => 'https://fonts.google.com/icons',
'cdn_base' => 'https://cdn.jsdelivr.net/npm/@material-design-icons/svg@0.14.13/',
'styles' => array(
'filled' => array(
'path' => 'filled',
'label' => 'Filled',
),
'outlined' => array(
'path' => 'outlined',
'label' => 'Outlined',
),
'round' => array(
'path' => 'round',
'label' => 'Round',
),
'sharp' => array(
'path' => 'sharp',
'label' => 'Sharp',
),
'two-tone' => array(
'path' => 'two-tone',
'label' => 'Two Tone',
),
),
'default_style' => 'filled',
'viewbox' => '0 0 24 24',
'normalize' => false,
'color_fix' => true, // Needs hardcoded color replacement.
'description' => 'Material Design Icons by Google. Beautiful, delightful, and easy to use.',
'icon_count' => 2189,
),
);
}
/**
* Get a specific icon set by slug.
*
* @param string $slug Icon set slug.
* @return array|null Icon set configuration or null if not found.
*/
public static function get( $slug ) {
$sets = self::get_all();
return isset( $sets[ $slug ] ) ? $sets[ $slug ] : null;
}
/**
* Get all available set slugs.
*
* @return array Array of set slugs.
*/
public static function get_slugs() {
return array_keys( self::get_all() );
}
/**
* Validate that a slug is a valid preset.
*
* @param string $slug Slug to validate.
* @return bool True if valid, false otherwise.
*/
public static function is_valid_slug( $slug ) {
return in_array( $slug, self::get_slugs(), true );
}
/**
* Validate that a style is valid for a given set.
*
* @param string $slug Icon set slug.
* @param string $style Style slug.
* @return bool True if valid, false otherwise.
*/
public static function is_valid_style( $slug, $style ) {
$set = self::get( $slug );
if ( ! $set ) {
return false;
}
return isset( $set['styles'][ $style ] );
}
/**
* Get the CDN URL for a specific icon.
*
* @param string $slug Icon set slug.
* @param string $style Style slug.
* @param string $name Icon name.
* @return string|null CDN URL or null if invalid.
*/
public static function get_cdn_url( $slug, $style, $name ) {
$set = self::get( $slug );
if ( ! $set || ! isset( $set['styles'][ $style ] ) ) {
return null;
}
$style_config = $set['styles'][ $style ];
return $set['cdn_base'] . $style_config['path'] . '/' . $name . '.svg';
}
/**
* Get the local file path for a specific icon.
*
* @param string $slug Icon set slug.
* @param string $style Style slug.
* @param string $name Icon name.
* @return string Local file path.
*/
public static function get_local_path( $slug, $style, $name ) {
return MI_ICONS_DIR . $slug . '/' . $style . '/' . $name . '.svg';
}
/**
* Get the local URL for a specific icon.
*
* @param string $slug Icon set slug.
* @param string $style Style slug.
* @param string $name Icon name.
* @return string Local URL.
*/
public static function get_local_url( $slug, $style, $name ) {
return MI_ICONS_URL . $slug . '/' . $style . '/' . $name . '.svg';
}
/**
* Get the manifest file path for a set.
*
* @param string $slug Icon set slug.
* @return string Manifest file path.
*/
public static function get_manifest_path( $slug ) {
return MI_PRESETS_DIR . $slug . '.json';
}
/**
* Load and parse a manifest file.
*
* @param string $slug Icon set slug.
* @return array|WP_Error Manifest data or error.
*/
public static function load_manifest( $slug ) {
$path = self::get_manifest_path( $slug );
if ( ! file_exists( $path ) ) {
return new WP_Error(
'manifest_not_found',
sprintf(
/* translators: %s: Icon set name */
__( 'Manifest file not found for %s.', 'maple-icons' ),
$slug
)
);
}
$content = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
if ( false === $content ) {
return new WP_Error(
'manifest_read_error',
__( 'Could not read manifest file.', 'maple-icons' )
);
}
$manifest = json_decode( $content, true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
return new WP_Error(
'manifest_parse_error',
__( 'Could not parse manifest file.', 'maple-icons' )
);
}
return $manifest;
}
/**
* Get style labels for a set.
*
* @param string $slug Icon set slug.
* @return array Array of style labels.
*/
public static function get_style_labels( $slug ) {
$set = self::get( $slug );
if ( ! $set ) {
return array();
}
$labels = array();
foreach ( $set['styles'] as $style_slug => $style_config ) {
$labels[ $style_slug ] = $style_config['label'];
}
return $labels;
}
}

View file

@ -0,0 +1,5 @@
<?php
// Silence is golden.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}