initial commit
This commit is contained in:
parent
e468202f95
commit
423b9a25fb
24 changed files with 6670 additions and 0 deletions
224
native/wordpress/maple-icons-wp/includes/class-mi-admin-page.php
Normal file
224
native/wordpress/maple-icons-wp/includes/class-mi-admin-page.php
Normal 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' ); ?> →
|
||||
</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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
505
native/wordpress/maple-icons-wp/includes/class-mi-downloader.php
Normal file
505
native/wordpress/maple-icons-wp/includes/class-mi-downloader.php
Normal 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 );
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
328
native/wordpress/maple-icons-wp/includes/class-mi-icon-sets.php
Normal file
328
native/wordpress/maple-icons-wp/includes/class-mi-icon-sets.php
Normal 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;
|
||||
}
|
||||
}
|
||||
5
native/wordpress/maple-icons-wp/includes/index.php
Normal file
5
native/wordpress/maple-icons-wp/includes/index.php
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
// Silence is golden.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue