Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.45% covered (success)
92.45%
147 / 159
42.86% covered (danger)
42.86%
6 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
Throw_On_Errors
92.45% covered (success)
92.45%
147 / 159
42.86% covered (danger)
42.86%
6 / 14
40.69
0.00% covered (danger)
0.00%
0 / 1
 throw_on_warnings
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
3
 t_realpath
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 t_file_exists
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 t_is_readable
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 t_is_writable
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 t_filesize
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
 t_filemtime
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
 t_is_dir
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 t_mkdir
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
4.01
 t_scandir
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
 t_rmdir
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
3.01
 t_unlink
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
2.01
 t_file_put_contents
72.22% covered (warning)
72.22%
13 / 18
0.00% covered (danger)
0.00%
0 / 1
5.54
 t_file_get_contents
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
1<?php // phpcs:disable Squiz.Commenting.FileComment.Missing
2
3// phpcs:disable WordPress.PHP.DevelopmentFunctions.prevent_path_disclosure_error_reporting
4// phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_error_reporting
5// phpcs:disable WordPress.PHP.IniSet.display_errors_Disallowed
6
7// After changing this file, consider increasing the version number ("VXXX") in all the files using this namespace, in
8// order to ensure that the specific version of this file always get loaded. Otherwise, Jetpack autoloader might decide
9// to load an older/newer version of the class (if, for example, both the standalone and bundled versions of the plugin
10// are installed, or in some other cases).
11namespace Automattic\Jetpack\Backup\V0005;
12
13use Exception;
14use Throwable;
15
16/**
17 * Wrappers for functions which throw an exception on errors and warnings instead of silently continuing operation.
18 *
19 * PHP is pretty lax with error reporting when doing I/O operations, e.g. a typical I/O helper function returns false,
20 * null, and/or emits a warning, but the whole PHP application continues operation. It's for the caller of the I/O
21 * helper function to test the returned result of the function, and notice + act upon I/O errors.
22 *
23 * We really want to know about each and every I/O error. Therefore, this class provides wrappers for some common
24 * (mostly) I/O operations that throw an exception on errors instead of just returning false, null, or something else.
25 * This wrapper class treats warnings as errors too.
26 *
27 * Given that static method names are similar to the ones used by PHP, they're prefixed with "t_" to not erroneously
28 * trigger various security scanners.
29 */
30class Throw_On_Errors {
31
32    /**
33     * Execute a callable, throw an exception (together with a descriptive label) on PHP warnings / errors.
34     *
35     * @param callable $callable Callable to execute.
36     * @param string   $label Label to add to the thrown exception to clarify what was attempted.
37     *
38     * @return mixed Callable's return value, if any.
39     * @throws Exception On warnings thrown by the callable.
40     * @noinspection PhpUnusedParameterInspection
41     */
42    private static function throw_on_warnings( $callable, $label ) {
43        $old_error_reporting = error_reporting( - 1 );
44        $old_display_errors  = ini_set( 'display_errors', 'stderr' );
45
46        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
47        set_error_handler(
48        /**
49         * Temporary error handler.
50         *
51         * @see https://php-legacy-docs.zend.com/manual/php5/en/function.set-error-handler
52         * @see https://www.php.net/manual/en/function.set-error-handler.php
53         *
54         * @param int $errno Level of the error raised.
55         * @param string $errstr Error message.
56         * @param string|null $errfile Filename that the error was raised in.
57         * @param int|null $errline Line number where the error was raised.
58         * @param array|null $errcontext Deprecated, unused.
59         *
60         * @return never
61         * @throws Exception
62         */
63            // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
64            function ( $errno, $errstr, $errfile = null, $errline = null, $errcontext = null ) {
65                throw new Exception( "$errstr (file: $errfile; line: $errline)" );
66            }
67        );
68
69        $result        = null;
70        $error_message = null;
71        try {
72            $result = $callable();
73        } catch ( Throwable $throwable ) {
74            $error_message = $throwable->getMessage();
75        }
76
77        restore_error_handler();
78        ini_set( 'display_errors', $old_display_errors );
79        error_reporting( $old_error_reporting );
80
81        if ( $error_message !== null ) {
82            throw new Exception( "$label failed: $error_message" );
83        }
84
85        return $result;
86    }
87
88    /**
89     * Return canonicalized absolute pathname, throw on warnings / errors.
90     *
91     * @see https://php-legacy-docs.zend.com/manual/php5/en/function.realpath
92     * @see https://www.php.net/manual/en/function.realpath.php
93     *
94     * @param string $path Path being checked.
95     *
96     * @return string Canonicalized absolute pathname
97     * @throws Exception On invalid parameters, or if realpath() has returned false or thrown warnings.
98     */
99    public static function t_realpath( $path ) {
100        // PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
101        if ( ! $path ) {
102            throw new Exception( 'Filename for realpath() is unset' );
103        }
104
105        $label = "realpath( '$path' )";
106
107        $realpath_result = static::throw_on_warnings(
108            function () use ( $path ) {
109                return realpath( $path );
110            },
111            $label
112        );
113
114        if ( false === $realpath_result ) {
115            throw new Exception( "Unable to $label" );
116        }
117
118        return $realpath_result;
119    }
120
121    /**
122     * Check whether a file or directory exists, throw on warnings / errors.
123     *
124     * @see https://php-legacy-docs.zend.com/manual/php5/en/function.file-exists
125     * @see https://www.php.net/manual/en/function.file-exists.php
126     *
127     * @param string $filename Path to the file or directory.
128     *
129     * @return bool True if the file or directory specified by filename exists; false otherwise.
130     * @throws Exception On invalid parameters, or if file_exists() has thrown warnings.
131     */
132    public static function t_file_exists( $filename ) {
133        // PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
134        if ( ! $filename ) {
135            throw new Exception( 'Filename for file_exists() is unset' );
136        }
137
138        return static::throw_on_warnings(
139            function () use ( $filename ) {
140                return file_exists( $filename );
141            },
142            "file_exists( '$filename' )"
143        );
144    }
145
146    /**
147     * Tell whether the filename (or a directory) is readable, throw on warnings / errors.
148     *
149     * @see https://php-legacy-docs.zend.com/manual/php5/en/function.is-readable
150     * @see https://www.php.net/manual/en/function.is-readable.php
151     *
152     * @param string $filename Filename (or directory) to check.
153     *
154     * @return bool True if the filename (or a directory) exists and is readable, false otherwise.
155     * @throws Exception On invalid parameters, or if is_readable() has thrown warnings.
156     */
157    public static function t_is_readable( $filename ) {
158        // PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
159        if ( ! $filename ) {
160            throw new Exception( 'Filename for is_readable() is unset' );
161        }
162
163        return static::throw_on_warnings(
164            function () use ( $filename ) {
165                return is_readable( $filename );
166            },
167            "is_readable( '$filename' )"
168        );
169    }
170
171    /**
172     * Tell whether the filename (or a directory) is writable, throw on warnings / errors.
173     *
174     * @see https://php-legacy-docs.zend.com/manual/php5/en/function.is-writable
175     * @see https://www.php.net/manual/en/function.is-writable.php
176     *
177     * @param string $filename Filename (or directory) to check.
178     *
179     * @return bool True if the filename (or a directory) exists and is writable, false otherwise.
180     * @throws Exception On invalid parameters, or if is_writable() has thrown warnings.
181     */
182    public static function t_is_writable( $filename ) {
183        // PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
184        if ( ! $filename ) {
185            throw new Exception( 'Filename for is_writable() is unset' );
186        }
187
188        return static::throw_on_warnings(
189            function () use ( $filename ) {
190                // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable
191                return is_writable( $filename );
192            },
193            "is_writable( '$filename' )"
194        );
195    }
196
197    /**
198     * Get file size, throw on warnings / errors.
199     *
200     * @see https://php-legacy-docs.zend.com/manual/php5/en/function.filesize
201     * @see https://www.php.net/manual/en/function.filesize.php
202     *
203     * @param string $filename Path to the file.
204     *
205     * @return int Size of the file in bytes
206     * @throws Exception On invalid parameters, or if filesize() has thrown warnings.
207     */
208    public static function t_filesize( $filename ) {
209        // PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
210        if ( ! $filename ) {
211            throw new Exception( 'Filename for filesize() is unset' );
212        }
213
214        $label = "filesize( '$filename' )";
215
216        $filesize_result = static::throw_on_warnings(
217            function () use ( $filename ) {
218                return filesize( $filename );
219            },
220            $label
221        );
222
223        if ( false === $filesize_result ) {
224            throw new Exception( "Unable to $label" );
225        }
226
227        return $filesize_result;
228    }
229
230    /**
231     * Get file modification time, throw on warnings / errors.
232     *
233     * @see https://php-legacy-docs.zend.com/manual/php5/en/function.filemtime
234     * @see https://www.php.net/manual/en/function.filemtime.php
235     *
236     * @param string $filename Path to the file.
237     *
238     * @return int The time the file was last modified
239     * @throws Exception On invalid parameters, or if filemtime() has thrown warnings.
240     */
241    public static function t_filemtime( $filename ) {
242        // PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
243        if ( ! $filename ) {
244            throw new Exception( 'Filename for filemtime() is unset' );
245        }
246
247        $label = "filemtime( '$filename' )";
248
249        $filemtime_result = static::throw_on_warnings(
250            function () use ( $filename ) {
251                return filemtime( $filename );
252            },
253            $label
254        );
255
256        if ( false === $filemtime_result ) {
257            throw new Exception( "Unable to $label" );
258        }
259
260        return $filemtime_result;
261    }
262
263    /**
264     * Tell whether the filename is a directory (follow symlinks), throw on warnings / errors.
265     *
266     * @see https://php-legacy-docs.zend.com/manual/php5/en/function.is-dir
267     * @see https://www.php.net/manual/en/function.is-dir.php
268     *
269     * @param string $filename Path to the file.
270     *
271     * @return bool True if the filename (or the symlink's target) exists and is a directory, false otherwise.
272     * @throws Exception On invalid parameters, if is_dir() has thrown warnings, or has failed.
273     */
274    public static function t_is_dir( $filename ) {
275        // PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
276        if ( ! $filename ) {
277            throw new Exception( 'Filename for is_dir() is unset' );
278        }
279
280        return static::throw_on_warnings(
281            function () use ( $filename ) {
282                return is_dir( $filename );
283            },
284            "is_dir( '$filename' )"
285        );
286    }
287
288    /**
289     * Make a directory, throw on warnings / errors.
290     *
291     * @see https://php-legacy-docs.zend.com/manual/php5/en/function.mkdir
292     * @see https://www.php.net/manual/en/function.mkdir.php
293     *
294     * @param string $directory Directory path.
295     * @param int    $permissions Permissions of the newly created directory.
296     * @param bool   $recursive If true, then any parent directories to the directory specified will also be created,
297     *                              with the same permissions.
298     *
299     * @return void
300     * @throws Exception On invalid parameters, if mkdir() has thrown warnings, or has failed.
301     */
302    public static function t_mkdir( $directory, $permissions = 0777, $recursive = false ) {
303        // PHP 5.x won't complain about permissions being null, so let's do it ourselves.
304        if ( $permissions === null ) {
305            throw new Exception( 'Permissions for mkdir() are unset' );
306        }
307
308        $label = "mkdir( '$directory', 0" . decoct( $permissions ) . ', ' . ( $recursive ? 'true' : 'false' ) . ' )';
309
310        $mkdir_result = static::throw_on_warnings(
311            function () use ( $directory, $permissions, $recursive ) {
312                // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir
313                return mkdir( $directory, $permissions, $recursive );
314            },
315            $label
316        );
317
318        if ( false === $mkdir_result ) {
319            throw new Exception( "Unable to $label" );
320        }
321    }
322
323    /**
324     * List files and directories inside the specified path, throw on warnings / errors.
325     *
326     * @see https://php-legacy-docs.zend.com/manual/php5/en/function.scandir
327     * @see https://www.php.net/manual/en/function.scandir.php
328     *
329     * @param string $directory Directory that will be scanned.
330     *
331     * @return string[] An array of filenames.
332     * @throws Exception If scandir() has thrown warnings, or has failed.
333     */
334    public static function t_scandir( $directory ) {
335
336        // PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
337        if ( ! $directory ) {
338            throw new Exception( 'Directory for scandir() is unset' );
339        }
340
341        $label = "scandir( '$directory' )";
342
343        $scandir_result = static::throw_on_warnings(
344            function () use ( $directory ) {
345                return scandir( $directory );
346            },
347            $label
348        );
349
350        if ( false === $scandir_result ) {
351            throw new Exception( "Unable to $label" );
352        }
353
354        return $scandir_result;
355    }
356
357    /**
358     * Remove a directory, throw on warnings / errors.
359     *
360     * @see https://php-legacy-docs.zend.com/manual/php5/en/function.rmdir
361     * @see https://www.php.net/manual/en/function.rmdir.php
362     *
363     * @param string $directory Directory path.
364     *
365     * @return void
366     * @throws Exception On invalid parameters, if rmdir() has thrown warnings, or has failed.
367     */
368    public static function t_rmdir( $directory ) {
369        // PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
370        if ( ! $directory ) {
371            throw new Exception( 'Directory for mkdir() is unset' );
372        }
373
374        $label = "rmdir( '$directory' )";
375
376        $rmdir_result = static::throw_on_warnings(
377            function () use ( $directory ) {
378                // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir
379                return rmdir( $directory );
380            },
381            $label
382        );
383
384        if ( false === $rmdir_result ) {
385            throw new Exception( "Unable to $label" );
386        }
387    }
388
389    /**
390     * Delete a file, throw on warnings / errors.
391     *
392     * @see https://php-legacy-docs.zend.com/manual/php5/en/function.unlink
393     * @see https://www.php.net/manual/en/function.unlink.php
394     *
395     * @param string $filename Path to the file.
396     *
397     * @return void
398     * @throws Exception If unlink() has thrown warnings, or has failed.
399     */
400    public static function t_unlink( $filename ) {
401
402        $label = "unlink( '$filename' )";
403
404        $unlink_result = static::throw_on_warnings(
405            function () use ( $filename ) {
406                // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
407                return unlink( $filename );
408            },
409            $label
410        );
411
412        if ( false === $unlink_result ) {
413            throw new Exception( "Unable to $label" );
414        }
415    }
416
417    /**
418     * Write data to a file, throw on warnings / errors.
419     *
420     * @see https://php-legacy-docs.zend.com/manual/php5/en/function.file-put-contents
421     * @see https://www.php.net/manual/en/function.file-put-contents.php
422     *
423     * @param string $filename Path to the file where to write the data.
424     * @param string $data The data to write.
425     *
426     * @return void
427     * @throws Exception If file_put_contents() has thrown warnings, has failed, or if it didn't write all the bytes.
428     */
429    public static function t_file_put_contents( $filename, $data ) {
430
431        // PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
432        if ( ! $filename ) {
433            throw new Exception( 'Filename for f_p_c() is unset' );
434        }
435        if ( $data === null ) {
436            throw new Exception( 'Data to write is null' );
437        }
438
439        $data_length = strlen( $data );
440
441        // Weird label is intentional, otherwise security scanners find this label suspicious.
442        $label = "f_p_c( '$filename', $data_length bytes of data )";
443
444        $number_of_bytes_written = static::throw_on_warnings(
445            function () use ( $filename, $data ) {
446                // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
447                return file_put_contents( $filename, $data );
448            },
449            $label
450        );
451
452        if ( false === $number_of_bytes_written ) {
453            throw new Exception( "Unable to $label" );
454        }
455
456        if ( $number_of_bytes_written !== $data_length ) {
457            throw new Exception(
458                "$label was expected to write $data_length bytes, but wrote $number_of_bytes_written bytes"
459            );
460        }
461    }
462
463    /**
464     * Read entire file into a string, throw on warnings / errors.
465     *
466     * @see https://php-legacy-docs.zend.com/manual/php5/en/function.file-get-contents
467     * @see https://www.php.net/manual/en/function.file-get-contents.php
468     *
469     * @param string $filename Name of the file to read.
470     *
471     * @return string The read data.
472     * @throws Exception If file_get_contents() has thrown warnings, or has failed.
473     */
474    public static function t_file_get_contents( $filename ) {
475
476        // PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
477        if ( ! $filename ) {
478            throw new Exception( 'Filename for f_g_c() is unset' );
479        }
480
481        // Weird label is intentional, otherwise security scanners find this label suspicious.
482        $label = "f_g_c( '$filename' )";
483
484        $fgc_result = static::throw_on_warnings(
485            function () use ( $filename ) {
486                // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
487                return file_get_contents( $filename );
488            },
489            $label
490        );
491
492        if ( false === $fgc_result ) {
493            throw new Exception( "Unable to $label" );
494        }
495
496        return $fgc_result;
497    }
498}