PluginProbe
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant / 2.0.2
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant v2.0.2
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 / Fonts / Storage.php

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

246 lines 5.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Font cache storage.
4 *
5 * All writes are atomic (tmp + rename) so a concurrent request never serves a
6 * half-written CSS or font file.
7 *
8 * @package EasyFonts
9 */
10
11 namespace EasyFonts\Fonts;
12
13 defined( 'ABSPATH' ) || exit;
14
15 /**
16 * Reads/writes files in the cache directory.
17 */
18 class Storage {
19
20 /**
21 * Ensure the cache directory exists (and is shielded from listing).
22 *
23 * @return bool
24 */
25 public function ensure_dir(): bool {
26 if ( is_dir( EASYFONTS_CACHE_DIR ) ) {
27 return true;
28 }
29
30 $made = wp_mkdir_p( EASYFONTS_CACHE_DIR );
31
32 if ( $made ) {
33 $index = trailingslashit( EASYFONTS_CACHE_DIR ) . 'index.html';
34
35 if ( ! file_exists( $index ) ) {
36 @file_put_contents( $index, '' ); // phpcs:ignore WordPress.WP.AlternativeFunctions
37 }
38 }
39
40 return $made;
41 }
42
43 /**
44 * Absolute path for a cache-relative filename.
45 *
46 * Defence-in-depth: `..` segments are stripped so a filename can never
47 * escape the cache directory, even though filenames are internally
48 * generated (hashes / slugs) and not user-supplied.
49 *
50 * @param string $filename Filename.
51 * @return string
52 */
53 public function path( string $filename ): string {
54 $filename = str_replace( array( '../', '..\\', "\0" ), '', ltrim( $filename, '/' ) );
55
56 return trailingslashit( EASYFONTS_CACHE_DIR ) . $filename;
57 }
58
59 /**
60 * Public URL for a cache-relative filename.
61 *
62 * When a CDN base is configured (Settings → cdn_url), the site origin is
63 * swapped for the CDN origin while the path is preserved — the standard
64 * pull-zone model. This one method is the single emission point for the
65 * consolidated stylesheet, every @font-face src, localised external CSS,
66 * and all preloads, so the CDN applies everywhere consistently.
67 *
68 * @param string $filename Filename.
69 * @return string
70 */
71 public function url( string $filename ): string {
72 $url = trailingslashit( EASYFONTS_CACHE_URL ) . ltrim( $filename, '/' );
73
74 return self::cdn_rewrite( $url );
75 }
76
77 /**
78 * Rewrite a same-origin asset URL onto the configured CDN origin.
79 *
80 * The CDN value is treated as an origin (scheme + host[/optional base]);
81 * the asset path is preserved. Returns the URL unchanged when no CDN is set,
82 * the CDN is invalid, or the URL isn't same-origin.
83 *
84 * @param string $url Absolute same-origin URL.
85 * @return string
86 */
87 public static function cdn_rewrite( string $url ): string {
88 $cdn = self::cdn_base();
89
90 if ( '' === $cdn || '' === $url ) {
91 return $url;
92 }
93
94 $home = wp_parse_url( home_url() );
95
96 if ( empty( $home['host'] ) ) {
97 return $url;
98 }
99
100 $origin = ( isset( $home['scheme'] ) ? $home['scheme'] : 'https' ) . '://' . $home['host']
101 . ( isset( $home['port'] ) ? ':' . $home['port'] : '' );
102
103 // Only rewrite our own origin; never touch third-party URLs.
104 if ( 0 === strpos( $url, $origin ) ) {
105 return $cdn . substr( $url, strlen( $origin ) );
106 }
107
108 // Protocol-relative or root-relative cache URLs (rare) → prefix the CDN.
109 if ( 0 === strpos( $url, '//' ) ) {
110 return $cdn . substr( $url, strpos( $url, '/', 2 ) );
111 }
112
113 return $url;
114 }
115
116 /**
117 * Normalised CDN origin from settings ('' when unset/invalid). Cached.
118 *
119 * @return string
120 */
121 private static function cdn_base(): string {
122 static $base = null;
123
124 if ( null !== $base ) {
125 return $base;
126 }
127
128 $raw = trim( (string) \EasyFonts\Settings::get( 'cdn_url', '' ) );
129
130 if ( '' === $raw ) {
131 return $base = '';
132 }
133
134 // Accept protocol-relative input by adopting the current scheme.
135 if ( 0 === strpos( $raw, '//' ) ) {
136 $raw = ( is_ssl() ? 'https:' : 'http:' ) . $raw;
137 }
138
139 if ( ! preg_match( '#^https?://#i', $raw ) ) {
140 return $base = '';
141 }
142
143 return $base = untrailingslashit( esc_url_raw( $raw ) );
144 }
145
146 /**
147 * Does a cache file exist?
148 *
149 * @param string $filename Filename.
150 * @return bool
151 */
152 public function exists( string $filename ): bool {
153 return is_file( $this->path( $filename ) );
154 }
155
156 /**
157 * File size in bytes (0 if missing).
158 *
159 * @param string $filename Filename.
160 * @return int
161 */
162 public function size( string $filename ): int {
163 $path = $this->path( $filename );
164
165 return is_file( $path ) ? (int) filesize( $path ) : 0;
166 }
167
168 /**
169 * Atomically write content to a cache file.
170 *
171 * @param string $filename Filename (may include a subdir).
172 * @param string $content Bytes.
173 * @return bool
174 */
175 public function write( string $filename, string $content ): bool {
176 $this->ensure_dir();
177
178 $path = $this->path( $filename );
179 $dir = dirname( $path );
180
181 if ( ! is_dir( $dir ) ) {
182 wp_mkdir_p( $dir );
183 }
184
185 $tmp = $path . '.' . wp_generate_password( 6, false ) . '.tmp';
186
187 // phpcs:disable WordPress.WP.AlternativeFunctions
188 $written = file_put_contents( $tmp, $content, LOCK_EX );
189
190 if ( false === $written || $written < strlen( $content ) ) {
191 @unlink( $tmp );
192 return false;
193 }
194
195 if ( ! @rename( $tmp, $path ) ) {
196 @unlink( $tmp );
197 return false;
198 }
199 // phpcs:enable WordPress.WP.AlternativeFunctions
200
201 return true;
202 }
203
204 /**
205 * Total cache size in bytes.
206 *
207 * @return int
208 */
209 public function total_size(): int {
210 if ( ! is_dir( EASYFONTS_CACHE_DIR ) ) {
211 return 0;
212 }
213
214 $bytes = 0;
215 $iter = new \RecursiveIteratorIterator(
216 new \RecursiveDirectoryIterator( EASYFONTS_CACHE_DIR, \FilesystemIterator::SKIP_DOTS )
217 );
218
219 foreach ( $iter as $file ) {
220 $bytes += $file->getSize();
221 }
222
223 return $bytes;
224 }
225
226 /**
227 * Remove every cached file.
228 */
229 public function purge(): void {
230 if ( ! is_dir( EASYFONTS_CACHE_DIR ) ) {
231 return;
232 }
233
234 $iter = new \RecursiveIteratorIterator(
235 new \RecursiveDirectoryIterator( EASYFONTS_CACHE_DIR, \FilesystemIterator::SKIP_DOTS ),
236 \RecursiveIteratorIterator::CHILD_FIRST
237 );
238
239 // phpcs:disable WordPress.WP.AlternativeFunctions
240 foreach ( $iter as $file ) {
241 $file->isDir() ? @rmdir( $file->getRealPath() ) : @unlink( $file->getRealPath() );
242 }
243 // phpcs:enable WordPress.WP.AlternativeFunctions
244 }
245 }
246