PluginProbe
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant / 2.0.0
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant v2.0.0
2.0.4 2.0.3 2.0.2 2.0.1 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2 1.3 2.0.0
easyfonts / src / Admin / AdminPage.php

AdminPage.php in EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant 2.0.0, at src/Admin/AdminPage.php

278 lines 9.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Admin page — hosts the React single-page app.
4 *
5 * @package EasyFonts
6 */
7
8 namespace EasyFonts\Admin;
9
10 defined( 'ABSPATH' ) || exit;
11
12 /**
13 * Registers the menu and mounts the SPA.
14 */
15 class AdminPage {
16
17 const SLUG = 'easyfonts';
18
19 /**
20 * Register hooks.
21 */
22 public function boot(): void {
23 add_action( 'admin_menu', array( $this, 'menu' ) );
24 add_action( 'admin_enqueue_scripts', array( $this, 'assets' ) );
25 add_filter( 'plugin_action_links_' . EASYFONTS_BASENAME, array( $this, 'action_links' ) );
26 add_action( 'admin_notices', array( $this, 'conflict_notice' ) );
27 }
28
29 /**
30 * Warn when OMGF (free or Pro) is active alongside Easy Fonts. Running both
31 * means two plugins rewrite the same Google Fonts — duplicate work, possible
32 * double <link>/preload, and conflicting caches. We advise deactivating OMGF.
33 * Shown on all admin screens (so it's seen), dismissible per-user.
34 */
35 public function conflict_notice(): void {
36 if ( ! current_user_can( 'activate_plugins' ) ) {
37 return;
38 }
39
40 if ( ! function_exists( 'is_plugin_active' ) ) {
41 require_once ABSPATH . 'wp-admin/includes/plugin.php';
42 }
43
44 $omgf = array(
45 'host-webfonts-local/host-webfonts-local.php', // OMGF (free)
46 'host-google-fonts-pro/host-google-fonts-pro.php', // OMGF Pro
47 );
48
49 $active = array();
50 foreach ( $omgf as $plugin ) {
51 if ( is_plugin_active( $plugin ) ) {
52 $active[] = $plugin;
53 }
54 }
55
56 if ( empty( $active ) ) {
57 return;
58 }
59
60 echo '<div class="notice notice-warning"><p>';
61 echo '<strong>' . esc_html__( 'Easy Fonts:', 'easyfonts' ) . '</strong> ';
62 echo esc_html__( 'OMGF is also active. Running two Google-Fonts optimizers at once causes duplicate processing and conflicting font output. Please deactivate OMGF and let Easy Fonts handle your fonts.', 'easyfonts' );
63 echo ' ';
64
65 // One-click deactivate link for the first detected OMGF plugin.
66 $deactivate = wp_nonce_url(
67 self_admin_url( 'plugins.php?action=deactivate&plugin=' . rawurlencode( $active[0] ) ),
68 'deactivate-plugin_' . $active[0]
69 );
70 echo '<a href="' . esc_url( $deactivate ) . '" class="button button-small" style="margin-left:4px;">'
71 . esc_html__( 'Deactivate OMGF', 'easyfonts' ) . '</a>';
72 echo '</p></div>';
73 }
74
75 /**
76 * Add the menu entry.
77 */
78 public function menu(): void {
79 add_menu_page(
80 __( 'Easy Fonts', 'easyfonts' ),
81 __( 'Easy Fonts', 'easyfonts' ),
82 'manage_options',
83 self::SLUG,
84 array( $this, 'render' ),
85 self::menu_icon(),
86 81
87 );
88 }
89
90 /**
91 * Stylized "E" mark as a data-URI SVG for the admin menu. Uses the default
92 * WordPress menu-icon grey so it sits naturally among the other sidebar
93 * icons (WordPress doesn't recolor data-URI icons per state).
94 *
95 * @return string
96 */
97 private static function menu_icon(): string {
98 $svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">'
99 . '<path fill="#a7aaad" d="M5 4H19V7.4H9V10.5H17V13.9H9V17H19V20H5Z"/></svg>';
100
101 return 'data:image/svg+xml;base64,' . base64_encode( $svg ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
102 }
103
104 /**
105 * Settings link on the plugins list.
106 *
107 * @param string[] $links Links.
108 * @return string[]
109 */
110 public function action_links( array $links ): array {
111 $url = admin_url( 'admin.php?page=' . self::SLUG );
112 array_unshift( $links, '<a href="' . esc_url( $url ) . '">' . esc_html__( 'Settings', 'easyfonts' ) . '</a>' );
113
114 return $links;
115 }
116
117 /**
118 * Mount point.
119 */
120 public function render(): void {
121 // WordPress relocates admin notices to just after the first heading
122 // inside .wrap (via its notice/updates JS). To keep stray notices
123 // (SEO plugins, etc.) OUT of our app container, we provide a decoy
124 // screen-reader heading + an explicit .wp-header-end anchor at the very
125 // top of .wrap. WordPress targets those, so notices land here — above
126 // and outside the Easy Fonts UI — instead of breaking our <h1>.
127 echo '<div class="wrap">'
128 . '<h1 class="screen-reader-text">' . esc_html__( 'Easy Fonts', 'easyfonts' ) . '</h1>'
129 . '<hr class="wp-header-end" style="margin:0;border:0;height:0;">'
130 . '<div id="easyfonts-admin-root">'
131 . '<noscript>' . esc_html__( 'Easy Fonts requires JavaScript.', 'easyfonts' ) . '</noscript>'
132 . '</div></div>';
133 }
134
135 /**
136 * Enqueue the compiled admin bundle on our page only.
137 *
138 * @param string $hook Current admin page hook.
139 */
140 public function assets( string $hook ): void {
141 if ( 'toplevel_page_' . self::SLUG !== $hook ) {
142 return;
143 }
144
145 $dist = EASYFONTS_DIR . 'assets/admin/dist/';
146 $url = EASYFONTS_URL . 'assets/admin/dist/';
147
148 $deps = array();
149 $version = EASYFONTS_VERSION;
150
151 // @wordpress/scripts convention: index.asset.php returns deps + version.
152 $asset_file = $dist . 'index.asset.php';
153
154 if ( is_readable( $asset_file ) ) {
155 $asset = include $asset_file;
156 $deps = $asset['dependencies'] ?? array();
157 $version = $asset['version'] ?? $version;
158 }
159
160 if ( is_readable( $dist . 'index.js' ) ) {
161 wp_enqueue_script( 'easyfonts-admin', $url . 'index.js', $deps, $version, true );
162
163 wp_localize_script(
164 'easyfonts-admin',
165 'EasyFontsAdmin',
166 array(
167 'root' => esc_url_raw( rest_url( 'easyfonts/v1' ) ),
168 'nonce' => wp_create_nonce( 'wp_rest' ),
169 'version' => EASYFONTS_VERSION,
170 'siteUrl' => home_url(),
171 'optimizer' => self::optimizer_cta(),
172 )
173 );
174 }
175
176 if ( is_readable( $dist . 'index.css' ) ) {
177 wp_enqueue_style( 'easyfonts-admin', $url . 'index.css', array(), $version );
178 }
179
180 if ( is_readable( $dist . 'style-index.css' ) ) {
181 wp_enqueue_style( 'easyfonts-admin-style', $url . 'style-index.css', array(), $version );
182 }
183
184 // Bundled admin fonts (Public Sans + JetBrains Mono, latin woff2), served
185 // locally from the plugin. Injected here so the URL resolves to the real
186 // plugin location; attached to whichever admin stylesheet handle exists.
187 $handle = wp_style_is( 'easyfonts-admin-style', 'enqueued' ) ? 'easyfonts-admin-style'
188 : ( wp_style_is( 'easyfonts-admin', 'enqueued' ) ? 'easyfonts-admin' : '' );
189
190 if ( '' !== $handle ) {
191 wp_add_inline_style( $handle, self::font_face_css() );
192 }
193 }
194
195 /**
196 * Smart CTA for the sibling Easy Optimizer plugin. Three states:
197 * - not installed → link to the plugin-install search (user installs it).
198 * - installed but inactive → one-click (nonce'd) activation link.
199 * - active → link straight to its settings screen.
200 *
201 * We deliberately do NOT auto-install/activate via AJAX: installing another
202 * plugin requires the user's explicit action on the standard WordPress
203 * screen (caps + nonces + .org guidelines). This just routes them correctly.
204 *
205 * @return array{label:string,url:string,state:string}
206 */
207 /**
208 * CTA for the sibling Easy Optimizer plugin. Two states:
209 * - active → link to its settings screen.
210 * - not active (not installed OR installed-inactive) → link to the
211 * plugin-install search, where the user installs and/or activates it
212 * themselves on the standard WordPress screen.
213 *
214 * URLs are NOT passed through esc_url_raw() here: that converts & into
215 * &amp; entities, which then arrive literally in the href and break the
216 * page ("link has expired"). React escapes the href safely on output.
217 *
218 * @return array{label:string,url:string,state:string}
219 */
220 private static function optimizer_cta(): array {
221 $slug = 'easy-optimizer/easy-optimizer.php';
222
223 if ( ! function_exists( 'is_plugin_active' ) ) {
224 require_once ABSPATH . 'wp-admin/includes/plugin.php';
225 }
226
227 if ( is_plugin_active( $slug ) ) {
228 return array(
229 'label' => __( 'Open Easy Optimizer', 'easyfonts' ),
230 'url' => admin_url( 'admin.php?page=easy-optimizer' ),
231 'state' => 'active',
232 );
233 }
234
235 // Not active (whether installed or not) → send them to the install /
236 // activate search screen and let them do it there.
237 return array(
238 'label' => __( 'Activate Easy Optimizer', 'easyfonts' ),
239 'url' => self_admin_url( 'plugin-install.php?s=speed%2520optimizer%2520by%2520fluxpress&tab=search&type=term' ),
240 'state' => 'install',
241 );
242 }
243
244 /**
245 * @font-face declarations for the bundled admin fonts.
246 *
247 * @return string
248 */
249 private static function font_face_css(): string {
250 $base = esc_url_raw( EASYFONTS_URL . 'assets/admin/fonts/' );
251 $out = '';
252
253 $faces = array(
254 array( 'Public Sans', 400, 'public-sans-latin-400-normal.woff2' ),
255 array( 'Public Sans', 500, 'public-sans-latin-500-normal.woff2' ),
256 array( 'Public Sans', 600, 'public-sans-latin-600-normal.woff2' ),
257 array( 'Public Sans', 700, 'public-sans-latin-700-normal.woff2' ),
258 array( 'Public Sans', 800, 'public-sans-latin-800-normal.woff2' ),
259 array( 'JetBrains Mono', 400, 'jetbrains-mono-latin-400-normal.woff2' ),
260 array( 'JetBrains Mono', 500, 'jetbrains-mono-latin-500-normal.woff2' ),
261 array( 'JetBrains Mono', 600, 'jetbrains-mono-latin-600-normal.woff2' ),
262 array( 'JetBrains Mono', 700, 'jetbrains-mono-latin-700-normal.woff2' ),
263 );
264
265 foreach ( $faces as $face ) {
266 $out .= sprintf(
267 "@font-face{font-family:'%s';font-style:normal;font-weight:%d;font-display:swap;src:url('%s%s') format('woff2');}",
268 $face[0],
269 $face[1],
270 $base,
271 $face[2]
272 );
273 }
274
275 return $out;
276 }
277 }
278