「アイコン」ブロックに独自SVGアイコンを追加する方法

Cocoonカスタマイズ集
この記事は約18分で読めます。
記事内に広告が含まれています。

はじめに

WordPress 7.0でアイコンブロックが追加され、さらにWordPress 7.1では独自のSVGアイコンを追加できる仕組みが実装されました。

そこで、自作のSVGアイコンを管理画面から手軽に登録してエディター内で利用できるようにします。

この記事では、この方法について説明します。

完成イメージ

カスタムアイコン管理
アイコンライブラリ

実装手順

以下のコードをfunctions.phpに追加します。

// 管理画面にメニューを追加
add_action('admin_menu', function () {
  $gutenberg_icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" aria-hidden="true" focusable="false"><path fill="currentColor" d="M6 9.5h3.5V6H6v3.5Zm5 .5a1 1 0 0 1-.898.995L10 11H5.5l-.103-.005a1 1 0 0 1-.892-.893L4.5 10V5.5a1 1 0 0 1 1-1H10a1 1 0 0 1 1 1V10ZM18.25 7.75a2 2 0 1 0-4 0 2 2 0 0 0 4 0Zm1.5 0a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0ZM6.88 13.535a1 1 0 0 1 1.74 0l2.534 4.472a1 1 0 0 1-.87 1.493H5.216a1 1 0 0 1-.87-1.493l2.534-4.472ZM6.074 18h3.352L7.75 15.041l-1.676 2.96ZM14.952 13h2.596a1 1 0 0 1 .866.5l1.298 2.25a1 1 0 0 1 0 1L18.414 19l-.074.11a1 1 0 0 1-.792.39h-2.596a1 1 0 0 1-.792-.39l-.074-.11-1.298-2.25a1 1 0 0 1 0-1l1.298-2.25a1 1 0 0 1 .866-.5Zm-.72 3.25 1.01 1.75h2.017l1.009-1.75-1.01-1.75h-2.017l-1.01 1.75Z"></path></svg>';

  add_menu_page(
    'カスタムアイコン',
    'カスタムアイコン',
    'manage_options',
    'icons-manage',
    'render_icon_manage',
    'data:image/svg+xml;base64,' . base64_encode($gutenberg_icon_svg),
    60
  );
});


/**
 * 管理画面ページを出力
 *
 * @return void
 */
if (!function_exists('render_icon_manage')):
function render_icon_manage() {
  // ラベル情報(slug => label)
  $labels = (array) get_option('my_custom_icons_labels', []);
  $files = glob(icon_manage_dir() . '*.svg') ?: [];
  $action_url = admin_url('admin-post.php');
  ?>
  <div class="wrap">
    <h1>カスタムアイコン管理</h1>


    <h2>登録済みアイコン一覧</h2>

    <?php if (empty($files) === false) : ?>
      <table class="widefat striped" style="width: 100%;">
        <thead>
          <tr>
            <th style="width:50px;"></th>
            <th>名前</th>
            <th>スラッグ</th>
            <th style="width:80px;">操作</th>
          </tr>
        </thead>
        <tbody>
          <?php foreach ($files as $path) :
            $slug = basename($path, '.svg');
            $name = 'custom/' . $slug;
            $label = $labels[$slug] ?? $slug;
          ?>
            <tr>
              <td>
                <?php if (function_exists('wp_get_icon')) { echo wp_get_icon($name, ['size' => 24]); } ?>
              </td>
              <td><?php echo esc_html($label); ?></td>
              <td><?php echo esc_html($slug); ?></td>
              <td>
                <form method="post" action="<?php echo esc_url($action_url); ?>" onsubmit="return confirm('「<?php echo esc_js($label); ?>」を削除しますか?\n(使用中のアイコンブロックは表示されなくなります)');">
                  <?php wp_nonce_field('icon_manage_action', 'icon_manage_nonce'); ?>
                  <input type="hidden" name="action" value="my_custom_icons">
                  <input type="hidden" name="task" value="delete_icon">
                  <input type="hidden" name="icon_slug" value="<?php echo esc_attr($slug); ?>">
                  <button type="submit" class="button button-secondary button-small">削除</button>
                </form>
              </td>
            </tr>
          <?php endforeach; ?>
        </tbody>
      </table>
    <?php else : ?>
      <p>登録されているアイコンはありません。</p>
    <?php endif; ?>

    <h3>新しいアイコンを追加</h3>

    <form method="post" action="<?php echo esc_url($action_url); ?>" enctype="multipart/form-data">
      <input type="hidden" name="action" value="my_custom_icons">
      <input type="hidden" name="task" value="add_icon">

      <?php wp_nonce_field('icon_manage_action', 'icon_manage_nonce'); ?>

      <table class="form-table">
        <tr>
          <th>名前</th>
          <td><input type="text" name="label" class="regular-text" placeholder="Star" required></td>
        </tr>
        <tr>
          <th>スラッグ</th>
          <td>
            <input type="text" name="icon_slug" class="regular-text" placeholder="star" required>
          </td>
        </tr>
        <tr>
          <th>SVGファイル</th>
          <td>
            <input type="file" name="icon_file" accept=".svg,image/svg+xml" required>
          </td>
        </tr>
      </table>

      <?php submit_button('アイコンを追加'); ?>
    </form>
  </div>
  <?php
}
endif;


// 管理画面からの設定データ更新
add_action('init', function () {
  if (function_exists('wp_register_icon') === false) {
    return;
  }

  $files = glob(icon_manage_dir() . '*.svg') ?: [];

  if (empty($files) === true) {
    return;
  }

  $labels = (array) get_option('my_custom_icons_labels', []);

  wp_register_icon_collection('custom', [
    'label' => 'カスタムアイコン',
    'description' => 'サイト独自のアイコンセット',
  ]);

  foreach ($files as $path) {
    $slug = basename($path, '.svg');

    if (preg_match('/^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$/', $slug) !== 1) {
      continue;
    }

    wp_register_icon('custom/' . $slug, [
      'label' => (string) ($labels[$slug] ?? $slug),
      'file_path' => $path,
    ]);
  }
});


// アイコンの追加・削除
add_action('admin_post_my_custom_icons', function () {
  if (current_user_can('manage_options') === false) {
    wp_die('権限がありません。');
  }

  $task = isset($_REQUEST['task']) ? sanitize_key(wp_unslash($_REQUEST['task'])) : '';

  switch ($task) {
    case 'add_icon':
      check_admin_referer('icon_manage_action', 'icon_manage_nonce');

      $slug = isset($_POST['icon_slug']) ? sanitize_key(wp_unslash($_POST['icon_slug'])) : '';
      $label = isset($_POST['label']) ? sanitize_text_field(wp_unslash($_POST['label'])) : '';

      if ($slug === '' || preg_match('/^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$/', $slug) !== 1) {
        icon_manage_redirect('invalid_slug');
      }

      if ($label === '') {
        icon_manage_redirect('invalid_label');
      }

      $dir = icon_manage_dir();
      $path = $dir . $slug . '.svg';

      // 既に登録されていないかチェック
      if (file_exists($path) === true) {
        icon_manage_redirect('exists_slug');
      }

      // SVGを検証して取得
      $content = icon_manage_validate_and_read_svg();

      // 保存先を作成してファイルを書き込み
      if (wp_mkdir_p($dir) === false || file_put_contents($path, $content) === false) {
        icon_manage_redirect('save_failed');
      }

      // ラベルのみDBに保存
      $labels = (array) get_option('my_custom_icons_labels', []);
      $labels[$slug] = $label;
      update_option('my_custom_icons_labels', $labels);

      icon_manage_redirect('added');
      break;

    case 'delete_icon':
      check_admin_referer('icon_manage_action', 'icon_manage_nonce');

      $slug = isset($_POST['icon_slug']) ? sanitize_key(wp_unslash($_POST['icon_slug'])) : '';

      if ($slug === '' || preg_match('/^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$/', $slug) !== 1) {
        icon_manage_redirect('delete_failed');
      }

      $path = icon_manage_dir() . $slug . '.svg';

      if (file_exists($path) === false || unlink($path) === false) {
        icon_manage_redirect('delete_failed');
      }

      $labels = (array) get_option('my_custom_icons_labels', []);
      unset($labels[$slug]);
      update_option('my_custom_icons_labels', $labels);

      icon_manage_redirect('deleted');
      break;

    default:
      icon_manage_redirect('invalid_request');
      break;
  }
});


/**
 * カスタムアイコンのSVG保存先ディレクトリの絶対パスを取得関数
 *
 * @return string 末尾スラッシュ付きの絶対パス(例: /var/www/wp-content/uploads/custom-icons/)
 */
if (!function_exists('icon_manage_dir')):
function icon_manage_dir() {
  $upload = wp_upload_dir();
  return trailingslashit($upload['basedir']) . 'custom-icons/';
}
endif;


/**
 * SVGのバリデーションと読み込み関数
 *
 * @return string 検証・サニタイズ済みのSVGコード文字列
 */
if (!function_exists('icon_manage_validate_and_read_svg')):
function icon_manage_validate_and_read_svg() {
  $file = $_FILES['icon_file'] ?? null;

  // ファイルアップロードの成否チェック
  if (
    is_array($file) === false ||
    empty($file['tmp_name']) === true ||
    (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK ||
    is_uploaded_file($file['tmp_name']) === false
  ) {
    icon_manage_redirect('file_error');
  }

  // 拡張子をチェック
  $file_name = sanitize_file_name(wp_unslash($file['name']));
  if (strtolower(pathinfo($file_name, PATHINFO_EXTENSION)) !== 'svg') {
    icon_manage_redirect('file_error');
  }

  // サイズチェック(100KBまで)
  if ((int) ($file['size'] ?? 0) > 102400) {
    icon_manage_redirect('file_too_large');
  }

  $content = file_get_contents($file['tmp_name']);

  if ($content === false || trim($content) === '') {
    icon_manage_redirect('file_error');
  }

  // 危険な記述を含むSVGは拒否
  if (preg_match('/<script|<foreignObject|<!DOCTYPE|<!ENTITY|<iframe|<image|\son[a-z]+\s*=|javascript:/i', $content) === 1) {
    icon_manage_redirect('invalid_svg');
  }

  // XMLとして解析でき、ルート要素がsvgであることを確認
  $previous = libxml_use_internal_errors(true);
  $xml = simplexml_load_string($content, 'SimpleXMLElement', LIBXML_NONET);
  libxml_clear_errors();
  libxml_use_internal_errors($previous);

  if ($xml === false || strtolower($xml->getName()) !== 'svg') {
    icon_manage_redirect('invalid_svg');
  }

  return trim($content);
}
endif;


// 通知メッセージを出力
add_action('admin_notices', function () {
  if ($notice = get_transient('my_admin_notice')) {
    echo '<div class="notice notice-' . esc_attr($notice['type']) . ' is-dismissible"><p>' . esc_html($notice['message']) . '</p></div>';
    delete_transient('my_admin_notice');
  }
});


/**
 * 通知メッセージを保存、アイコン管理ページへリダイレクト関数
 *
 * @param string $code 通知コード(icon_manage_get_messages() のキー)。
 * @return never
 */
if (!function_exists('icon_manage_redirect')):
function icon_manage_redirect($code) {
  $messages = icon_manage_get_messages();

  if (isset($messages[$code])) {
    set_transient('my_admin_notice', [
      'type'    => $messages[$code][1],
      'message' => $messages[$code][0],
    ], 30);
  }

  wp_safe_redirect(
    add_query_arg(
      [
        'page' => 'icons-manage',
      ],
      admin_url('admin.php')
    )
  );
  exit;
}
endif;


/**
 * 通知コードに対応するメッセージと種別の一覧を取得関数
 *
 * @return array<string, array{0: string, 1: string}> 通知コードをキーとする連想配列。
 */
if (!function_exists('icon_manage_get_messages')):
function icon_manage_get_messages() {
  return [
    'added'             => ['アイコンを追加しました。'                         , 'success'],
    'deleted'           => ['アイコンを削除しました。'                         , 'success'],
    'invalid_slug'      => ['スラッグの形式が正しくありません。'                 , 'error'],
    'invalid_label'     => ['名前を入力してください。'                         , 'error'],
    'exists_slug'       => ['そのスラッグは既に登録されています。'               , 'error'],
    'file_error'        => ['SVGファイルのアップロードに失敗しました。'          , 'error'],
    'file_too_large'    => ['SVGファイルのサイズが大きすぎます(100KBまで)。'  , 'error'],
    'invalid_svg'       => ['SVGの内容が不正、または使用できない記述を含みます。', 'error'],
    'save_failed'       => ['ファイルの保存に失敗しました。書き込み権限を確認してください。', 'error'],
    'delete_failed'     => ['アイコンの削除に失敗しました。'                     , 'error'],
    'invalid_request'   => ['不正な操作です。'                                 , 'error'],
  ];
}
endif;

さいごに

今回紹介した方法を使えば、WordPress 7.1で新しく追加された仕組みを活用し、自作のSVGアイコンを手軽に管理・追加できるようになります。
管理画面からのアップロードやデータベースへの保存、アイコンコレクションへの登録を一貫してコードで制御できるため、サイトのカスタマイズ性が大きく広がります。

タイトルとURLをコピーしました