PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.9.3
WooCommerce v10.9.3
10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / src / Internal / Admin / Logging / FileV2 / File.php
woocommerce / src / Internal / Admin / Logging / FileV2 Last commit date
File.php 2 years ago FileController.php 1 year ago FileExporter.php 2 years ago FileListTable.php 2 years ago SearchListTable.php 2 years ago
File.php
536 lines
1 <?php
2 declare( strict_types = 1 );
3
4 namespace Automattic\WooCommerce\Internal\Admin\Logging\FileV2;
5
6 use Automattic\Jetpack\Constants;
7 use Automattic\WooCommerce\Internal\Utilities\FilesystemUtil;
8 use Exception;
9
10 /**
11 * File class.
12 *
13 * An object representation of a single log file.
14 */
15 class File {
16 /**
17 * The absolute path of the file.
18 *
19 * @var string
20 */
21 protected $path;
22
23 /**
24 * The source property of the file, derived from the filename.
25 *
26 * @var string
27 */
28 protected $source = '';
29
30 /**
31 * The 0-based increment of the file, if it has been rotated. Derived from the filename. Can only be 0-9.
32 *
33 * @var int|null
34 */
35 protected $rotation;
36
37 /**
38 * The date the file was created, as a Unix timestamp, derived from the filename.
39 *
40 * @var int
41 */
42 protected $created = 0;
43
44 /**
45 * The hash property of the file, derived from the filename.
46 *
47 * @var string
48 */
49 protected $hash = '';
50
51 /**
52 * The file's resource handle when it is open.
53 *
54 * @var resource
55 */
56 protected $stream;
57
58 /**
59 * Class File
60 *
61 * @param string $path The absolute path of the file.
62 */
63 public function __construct( $path ) {
64 $this->path = $path;
65 $this->ingest_path();
66 }
67
68 /**
69 * Make sure open streams are closed.
70 */
71 public function __destruct() {
72 if ( is_resource( $this->stream ) ) {
73 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose -- No suitable alternative.
74 fclose( $this->stream );
75 }
76 }
77
78 /**
79 * Parse a path to a log file to determine if it uses the standard filename structure and various properties.
80 *
81 * This makes assumptions about the structure of the log file's name. Using `-` to separate the name into segments,
82 * if there are at least 5 segments, it assumes that the last segment is the hash, and the three segments before
83 * that make up the date when the file was created in YYYY-MM-DD format. Any segments left after that are the
84 * "source" that generated the log entries. If the filename doesn't have enough segments, it falls back to the
85 * source and the hash both being the entire filename, and using the inode change time as the creation date.
86 *
87 * Example:
88 * my-custom-plugin.2-2025-01-01-a1b2c3d4e5f.log
89 * | | | |
90 * 'my-custom-plugin' | '2025-01-01' |
91 * (source) | (created) |
92 * '2' 'a1b2c3d4e5f'
93 * (rotation) (hash)
94 *
95 * @param string $path The full path of the log file.
96 *
97 * @return array {
98 * @type string $dirname The directory structure containing the file. See pathinfo().
99 * @type string $basename The filename with extension. See pathinfo().
100 * @type string $extension The file extension. See pathinfo().
101 * @type string $filename The filename without extension. See pathinfo().
102 * @type string $source The source of the log entries contained in the file.
103 * @type int|null $rotation The 0-based incremental rotation marker, if the file has been rotated.
104 * Should only be a single digit.
105 * @type int $created The date the file was created, as a Unix timestamp.
106 * @type string $hash The hash suffix of the filename that protects from direct access.
107 * @type string $file_id The public ID of the log file (filename without the hash).
108 * }
109 */
110 public static function parse_path( string $path ): array {
111 $defaults = array(
112 'dirname' => '',
113 'basename' => '',
114 'extension' => '',
115 'filename' => '',
116 'source' => '',
117 'rotation' => null,
118 'created' => 0,
119 'hash' => '',
120 'file_id' => '',
121 );
122
123 $parsed = array_merge( $defaults, pathinfo( $path ) );
124
125 $segments = explode( '-', $parsed['filename'] );
126 $timestamp = strtotime( implode( '-', array_slice( $segments, -4, 3 ) ) );
127
128 if ( count( $segments ) >= 5 && false !== $timestamp ) {
129 $parsed['source'] = implode( '-', array_slice( $segments, 0, -4 ) );
130 $parsed['created'] = $timestamp;
131 $parsed['hash'] = array_slice( $segments, -1 )[0];
132 } else {
133 $parsed['source'] = implode( '-', $segments );
134 }
135
136 $rotation_marker = strrpos( $parsed['source'], '.', -1 );
137 if ( false !== $rotation_marker ) {
138 $rotation = substr( $parsed['source'], -1 );
139 if ( is_numeric( $rotation ) ) {
140 $parsed['rotation'] = intval( $rotation );
141 }
142
143 $parsed['source'] = substr( $parsed['source'], 0, $rotation_marker );
144 }
145
146 $parsed['file_id'] = static::generate_file_id(
147 $parsed['source'],
148 $parsed['rotation'],
149 $parsed['created']
150 );
151
152 return $parsed;
153 }
154
155 /**
156 * Generate a public ID for a log file based on its properties.
157 *
158 * The file ID is the basename of the file without the hash part. It allows us to identify a file without revealing
159 * its full name in the filesystem, so that it's difficult to access the file directly with an HTTP request.
160 *
161 * @param string $source The source of the log entries contained in the file.
162 * @param int|null $rotation Optional. The 0-based incremental rotation marker, if the file has been rotated.
163 * Should only be a single digit.
164 * @param int $created Optional. The date the file was created, as a Unix timestamp.
165 *
166 * @return string
167 */
168 public static function generate_file_id( string $source, ?int $rotation = null, int $created = 0 ): string {
169 $file_id = static::sanitize_source( $source );
170
171 if ( ! is_null( $rotation ) ) {
172 $file_id .= '.' . $rotation;
173 }
174
175 if ( $created > 0 ) {
176 $file_id .= '-' . gmdate( 'Y-m-d', $created );
177 }
178
179 return $file_id;
180 }
181
182 /**
183 * Generate a hash to use as the suffix on a log filename.
184 *
185 * @param string $file_id A file ID (file basename without the hash).
186 *
187 * @return string
188 */
189 public static function generate_hash( string $file_id ): string {
190 $key = Constants::get_constant( 'AUTH_SALT' ) ?? 'wc-logs';
191
192 return hash_hmac( 'md5', $file_id, $key );
193 }
194
195 /**
196 * Sanitize the source property of a log file.
197 *
198 * @param string $source The source of the log entries contained in the file.
199 *
200 * @return string
201 */
202 public static function sanitize_source( string $source ): string {
203 return sanitize_file_name( $source );
204 }
205
206 /**
207 * Parse the log file path and assign various properties to this class instance.
208 *
209 * @return void
210 */
211 protected function ingest_path(): void {
212 $parsed_path = static::parse_path( $this->path );
213 $this->source = $parsed_path['source'];
214 $this->rotation = $parsed_path['rotation'];
215 $this->created = $parsed_path['created'];
216 $this->hash = $parsed_path['hash'];
217 }
218
219 /**
220 * Check if the filename structure is in the expected format.
221 *
222 * @see parse_path().
223 *
224 * @return bool
225 */
226 public function has_standard_filename(): bool {
227 return ! ! $this->get_hash();
228 }
229
230 /**
231 * Check if the file represented by the class instance is a file and is readable.
232 *
233 * @return bool
234 */
235 public function is_readable(): bool {
236 try {
237 $filesystem = FilesystemUtil::get_wp_filesystem();
238 $is_readable = $filesystem->is_file( $this->path ) && $filesystem->is_readable( $this->path );
239 } catch ( Exception $exception ) {
240 return false;
241 }
242
243 return $is_readable;
244 }
245
246 /**
247 * Check if the file represented by the class instance is a file and is writable.
248 *
249 * @return bool
250 */
251 public function is_writable(): bool {
252 try {
253 $filesystem = FilesystemUtil::get_wp_filesystem();
254 $is_writable = $filesystem->is_file( $this->path ) && $filesystem->is_writable( $this->path );
255 } catch ( Exception $exception ) {
256 return false;
257 }
258
259 return $is_writable;
260 }
261
262 /**
263 * Open a read-only stream for this file.
264 *
265 * @return resource|false
266 */
267 public function get_stream() {
268 if ( ! $this->is_readable() ) {
269 return false;
270 }
271
272 if ( ! is_resource( $this->stream ) ) {
273 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen -- No suitable alternative.
274 $this->stream = fopen( $this->path, 'rb' );
275 }
276
277 return $this->stream;
278 }
279
280 /**
281 * Close the stream for this file.
282 *
283 * The stream will also close automatically when the class instance destructs, but this can be useful for
284 * avoiding having a large number of streams open simultaneously.
285 *
286 * @return bool
287 */
288 public function close_stream(): bool {
289 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose -- No suitable alternative.
290 return fclose( $this->stream );
291 }
292
293 /**
294 * Get the full absolute path of the file.
295 *
296 * @return string
297 */
298 public function get_path(): string {
299 return $this->path;
300 }
301
302 /**
303 * Get the name of the file, with extension, but without full path.
304 *
305 * @return string
306 */
307 public function get_basename(): string {
308 return basename( $this->path );
309 }
310
311 /**
312 * Get the file's source property.
313 *
314 * @return string
315 */
316 public function get_source(): string {
317 return $this->source;
318 }
319
320 /**
321 * Get the file's rotation property.
322 *
323 * @return int|null
324 */
325 public function get_rotation(): ?int {
326 return $this->rotation;
327 }
328
329 /**
330 * Get the file's hash property.
331 *
332 * @return string
333 */
334 public function get_hash(): string {
335 return $this->hash;
336 }
337
338 /**
339 * Get the file's public ID.
340 *
341 * @return string
342 */
343 public function get_file_id(): string {
344 $created = 0;
345 if ( $this->has_standard_filename() ) {
346 $created = $this->get_created_timestamp();
347 }
348
349 $file_id = static::generate_file_id(
350 $this->get_source(),
351 $this->get_rotation(),
352 $created
353 );
354
355 return $file_id;
356 }
357
358 /**
359 * Get the file's created property.
360 *
361 * @return int
362 */
363 public function get_created_timestamp(): int {
364 if ( ! $this->created && $this->is_readable() ) {
365 $this->created = filectime( $this->path );
366 }
367
368 return $this->created;
369 }
370
371 /**
372 * Get the time of the last modification of the file, as a Unix timestamp. Or false if the file isn't readable.
373 *
374 * @return int|false
375 */
376 public function get_modified_timestamp() {
377 try {
378 $filesystem = FilesystemUtil::get_wp_filesystem();
379 $timestamp = $filesystem->mtime( $this->path );
380 } catch ( Exception $exception ) {
381 return false;
382 }
383
384 return $timestamp;
385 }
386
387 /**
388 * Get the size of the file in bytes. Or false if the file isn't readable.
389 *
390 * @return int|false
391 */
392 public function get_file_size() {
393 try {
394 $filesystem = FilesystemUtil::get_wp_filesystem();
395
396 if ( ! $filesystem->is_readable( $this->path ) ) {
397 return false;
398 }
399
400 $size = $filesystem->size( $this->path );
401 } catch ( Exception $exception ) {
402 return false;
403 }
404
405 return $size;
406 }
407
408 /**
409 * Create and set permissions on the file.
410 *
411 * @return bool
412 */
413 protected function create(): bool {
414 try {
415 $filesystem = FilesystemUtil::get_wp_filesystem();
416 $created = $filesystem->touch( $this->path );
417 $modded = $filesystem->chmod( $this->path );
418 } catch ( Exception $exception ) {
419 return false;
420 }
421
422 return $created && $modded;
423 }
424
425 /**
426 * Write content to the file, appending it to the end.
427 *
428 * @param string $text The content to add to the file.
429 *
430 * @return bool
431 */
432 public function write( string $text ): bool {
433 if ( '' === $text ) {
434 return false;
435 }
436
437 if ( ! $this->is_writable() ) {
438 $created = $this->create();
439
440 if ( ! $created || ! $this->is_writable() ) {
441 return false;
442 }
443 }
444
445 // Ensure content ends with a line ending.
446 $eol_pos = strrpos( $text, PHP_EOL );
447 if ( false === $eol_pos || strlen( $text ) !== $eol_pos + 1 ) {
448 $text .= PHP_EOL;
449 }
450
451 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen -- No suitable alternative.
452 $resource = fopen( $this->path, 'ab' );
453
454 mbstring_binary_safe_encoding();
455 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite -- No suitable alternative.
456 $bytes_written = fwrite( $resource, $text );
457 reset_mbstring_encoding();
458
459 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose -- No suitable alternative.
460 fclose( $resource );
461
462 if ( strlen( $text ) !== $bytes_written ) {
463 return false;
464 }
465
466 return true;
467 }
468
469 /**
470 * Rename this file with an incremented rotation number.
471 *
472 * @return bool True if the file was successfully rotated.
473 */
474 public function rotate(): bool {
475 if ( ! $this->is_writable() ) {
476 return false;
477 }
478
479 $created = 0;
480 if ( $this->has_standard_filename() ) {
481 $created = $this->get_created_timestamp();
482 }
483
484 if ( is_null( $this->get_rotation() ) ) {
485 $new_rotation = 0;
486 } else {
487 $new_rotation = $this->get_rotation() + 1;
488 }
489
490 $new_file_id = static::generate_file_id( $this->get_source(), $new_rotation, $created );
491
492 $search = array( $this->get_file_id() );
493 $replace = array( $new_file_id );
494 if ( $this->has_standard_filename() ) {
495 $search[] = $this->get_hash();
496 $replace[] = static::generate_hash( $new_file_id );
497 }
498
499 $old_filename = $this->get_basename();
500 $new_filename = str_replace( $search, $replace, $old_filename );
501 $new_path = str_replace( $old_filename, $new_filename, $this->path );
502
503 try {
504 $filesystem = FilesystemUtil::get_wp_filesystem();
505 $moved = $filesystem->move( $this->path, $new_path, true );
506 } catch ( Exception $exception ) {
507 return false;
508 }
509
510 if ( ! $moved ) {
511 return false;
512 }
513
514 $this->path = $new_path;
515 $this->ingest_path();
516
517 return $this->is_readable();
518 }
519
520 /**
521 * Delete the file from the filesystem.
522 *
523 * @return bool True on success, false on failure.
524 */
525 public function delete(): bool {
526 try {
527 $filesystem = FilesystemUtil::get_wp_filesystem();
528 $deleted = $filesystem->delete( $this->path, false, 'f' );
529 } catch ( Exception $exception ) {
530 return false;
531 }
532
533 return $deleted;
534 }
535 }
536