Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend File System Class #215

Merged
merged 32 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b5e2eb2
Update blueprint.json for branch/tag
github-actions[bot] Nov 8, 2024
802454c
Merge remote-tracking branch 'upstream/main'
namithj Nov 12, 2024
34050d5
Merge remote-tracking branch 'upstream/main'
namithj Nov 13, 2024
e964980
Merge remote-tracking branch 'upstream/main'
namithj Nov 29, 2024
a042efc
Extend File System Class
namithj Nov 29, 2024
246c8a0
The Read Routine
namithj Dec 4, 2024
3549118
PHP Doc Alignment
namithj Dec 4, 2024
4819d59
Code review suggestions
namithj Dec 20, 2024
ae2f867
Merge branch 'main' into #187
namithj Dec 20, 2024
a094256
Merge branch 'main' into #187
namithj Dec 20, 2024
a91bae8
Update includes/class-filesystem-direct.php
namithj Dec 30, 2024
911864e
Update includes/class-debug.php
namithj Dec 30, 2024
4752f27
Singleton Filesystem Object
namithj Dec 30, 2024
2ef8540
Add File Exists and readable or writable checks
namithj Jan 4, 2025
f6c47e3
Tests: Extend `Filesystem_Direct` in `AP_FakeFilesystem`.
costdev Jan 8, 2025
6f2b1f9
Tests: Account for the new static `$filesystem` property.
costdev Jan 8, 2025
472f5d1
Tests: Make some test classes run in separate processes.
costdev Jan 8, 2025
be13988
Tests: Remove coverage annotations for `Debug::verify_filesystem()`.
costdev Jan 8, 2025
40933ba
Tests: Remove references to the `$wp_filesystem` global.
costdev Jan 8, 2025
451214b
Tests: Remove testing for an unavailable filesystem.
costdev Jan 8, 2025
a4bb48e
Tests: Account for log lines being retrieved as an array.
costdev Jan 8, 2025
b8ac7b5
Tests: Refactor test that checks the order of messages.
costdev Jan 8, 2025
bf6db5c
Tests: Remove truncation message assertions.
costdev Jan 8, 2025
3aa9218
Workflows: Install SVN before running tests.
costdev Jan 8, 2025
746f364
Debug: Filter the contents array to check for an empty file.
costdev Jan 8, 2025
7870801
Filesystem: Trim all space characters to check for empty lines.
costdev Jan 8, 2025
02d4f91
Tests: Remove unneeded tests.
costdev Jan 10, 2025
f0062be
Tests: Ensure the file exists before testing its writability.
costdev Jan 10, 2025
6c7e8a2
Tests: Make some test names more accurate.
costdev Jan 10, 2025
038c97c
Tests: Add tests for `Filesystem_Direct::get_contents_array()`.
costdev Jan 10, 2025
cf34bbd
Tests: Add tests for `Filesystem_Direct::put_contents()`.
costdev Jan 10, 2025
500676e
Coding Standards: Add `mkdir()` and `rmdir()` exceptions for test files.
costdev Jan 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/phpunit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ jobs:
php-version: ${{ matrix.php-version }}
tools: phpunit-polyfills:1.1

- name: Install SVN
run: sudo apt install -y subversion

- name: Setup tests
run: bash bin/install-wp-tests.sh wordpress_tests root root 127.0.0.1 latest true

Expand Down
2 changes: 1 addition & 1 deletion assets/js/aspire-update.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class ViewLog {
jQuery.ajax(parameters)
.done(function (response) {
if ((true == response.success) && ('' != response.data.content)) {
let lines = response.data.content.split(aspireupdate.line_ending);
let lines = response.data.content;
jQuery.each(lines, function (index, line) {
jQuery('<div>')
.append(
Expand Down
118 changes: 40 additions & 78 deletions includes/class-debug.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ class Debug {
*/
private static $log_file = 'debug-aspire-update.log';

/**
* The filesystem.
*
* @var Filesystem_Direct
*/
private static $filesystem;

/**
* Get the Log file path.
*
Expand All @@ -31,79 +38,41 @@ private static function get_file_path() {
/**
* Initializes the WordPress Filesystem.
*
* @return WP_Filesystem_Base|false The filesystem object or false on failure.
* @return Filesystem_Direct The filesystem object.
*/
private static function init_filesystem() {
global $wp_filesystem;

if ( ! $wp_filesystem ) {
if ( ! self::$filesystem instanceof Filesystem_Direct ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
WP_Filesystem();
self::$filesystem = new Filesystem_Direct( false );
}

return $wp_filesystem;
}

/**
* Checks the filesystem status and logs error to debug log.
*
* @param WP_Filesystem_Base $wp_filesystem The filesystem object.
*
* @return boolean true on success and false on failure.
*/
private static function verify_filesystem( $wp_filesystem ) {
if ( ! $wp_filesystem ) {
if (
defined( 'WP_DEBUG' ) &&
( true === WP_DEBUG ) &&
defined( 'WP_DEBUG_LOG' ) &&
( true === WP_DEBUG_LOG )
) {
// phpcs:disable WordPress.PHP.DevelopmentFunctions
/**
* Log error in file write fails only if debug is set to true. This is a valid use case.
*/
error_log( 'AspireUpdate - Could not open or write to the file system. Check file system permissions to debug log directory.' ); // @codeCoverageIgnore
// phpcs:enable
}
return false;
}
return true;
return self::$filesystem;
}

/**
* Get the content of the log file truncated upto N number of lines.
*
* @param integer $limit Max no of lines to return. Defaults to a 1000 lines.
*
* @return string|WP_Error The File content truncate upto the number of lines set in the limit parameter.
* @return array|WP_Error An array of lines in the file, limited to $limit, or a WP_Error object on failure.
*/
public static function read( $limit = 1000 ) {
$wp_filesystem = self::init_filesystem();
$file_path = self::get_file_path();
if ( ! self::verify_filesystem( $wp_filesystem ) || ! $wp_filesystem->exists( $file_path ) || ! $wp_filesystem->is_readable( $file_path ) ) {

if ( ! $wp_filesystem->exists( $file_path ) || ! $wp_filesystem->is_readable( $file_path ) ) {
return new \WP_Error( 'not_readable', __( 'Error: Unable to read the log file.', 'aspireupdate' ) );
}

$file_content = $wp_filesystem->get_contents_array( $file_path );
$content = '';
$index = 0;
foreach ( $file_content as $file_content_lines ) {
if ( ( $index < $limit ) ) {
$content .= $file_content_lines . PHP_EOL;
++$index;
}
}
if ( '' === trim( $content ) ) {
$content = esc_html__( '*****Log file is empty.*****', 'aspireupdate' );
} elseif ( $limit < count( $file_content ) ) {
$content .= PHP_EOL . sprintf(
/* translators: 1: The number of lines at which the content was truncated. */
esc_html__( '*****Log truncated at %s lines.*****', 'aspireupdate' ),
$limit
);
$file_content = $wp_filesystem->get_contents_array( $file_path, $limit, true );

if ( ( false === $file_content ) || ( 0 === count( array_filter( $file_content ) ) ) ) {
$file_content = [ esc_html__( '*****Log file is empty.*****', 'aspireupdate' ) ];
}
return $content;

return $file_content;
costdev marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand All @@ -114,7 +83,8 @@ public static function read( $limit = 1000 ) {
public static function clear() {
$wp_filesystem = self::init_filesystem();
$file_path = self::get_file_path();
if ( ! self::verify_filesystem( $wp_filesystem ) || ! $wp_filesystem->exists( $file_path ) || ! $wp_filesystem->is_writable( $file_path ) ) {

if ( ! $wp_filesystem->exists( $file_path ) || ! $wp_filesystem->is_writable( $file_path ) ) {
return new \WP_Error( 'not_accessible', __( 'Error: Unable to access the log file.', 'aspireupdate' ) );
}

Expand All @@ -133,30 +103,22 @@ public static function clear() {
* @param string $type The log level ('string', 'request', 'response').
*/
public static function log( $message, $type = 'string' ) {
$wp_filesystem = self::init_filesystem();
if ( self::verify_filesystem( $wp_filesystem ) ) {
$timestamp = gmdate( 'Y-m-d H:i:s' );
$formatted_message = sprintf(
'[%s] [%s]: %s',
$timestamp,
strtoupper( $type ),
self::format_message( $message )
) . PHP_EOL;

$file_path = self::get_file_path();

$content = '';
if ( $wp_filesystem->exists( $file_path ) ) {
if ( $wp_filesystem->is_readable( $file_path ) ) {
$content = $wp_filesystem->get_contents( $file_path );
}
}
$wp_filesystem->put_contents(
$file_path,
$formatted_message . $content,
FS_CHMOD_FILE
);
}
$wp_filesystem = self::init_filesystem();
$timestamp = gmdate( 'Y-m-d H:i:s' );
$formatted_message = sprintf(
'[%s] [%s]: %s',
$timestamp,
strtoupper( $type ),
self::format_message( $message )
) . PHP_EOL;

$file_path = self::get_file_path();
$wp_filesystem->put_contents(
$file_path,
$formatted_message,
FS_CHMOD_FILE,
'a'
);
}

/**
Expand Down
104 changes: 104 additions & 0 deletions includes/class-filesystem-direct.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php
/**
* The Class for WordPress Direct Filesystem with optimized read and write routines.
*
* @package aspire-update
*/

namespace AspireUpdate;

/**
* The Class for WordPress Direct Filesystem with optimized read and write routines.
*/
class Filesystem_Direct extends \WP_Filesystem_Direct {

/**
* Reads entire file into an array with options for limiting the number of lines and direction from the the lines are counted.
*
* @param string $file Path to the file.
* @param int $number_of_lines The number of lines to read. Default is -1 (read all lines).
* @param bool $count_bottom_to_top Count the lines from the bottom up. Default is false (count from top to bottom).
*
* @return array|false File contents in an array on success, false on failure.
*/
public function get_contents_array( $file, $number_of_lines = -1, $count_bottom_to_top = false ) {
if ( ! $this->exists( $file ) ) {
return false;
}

if ( -1 === $number_of_lines ) {
return @file( $file );
}

$handle = @fopen( $file, 'r' );
if ( ! $handle ) {
return false;
}

$lines = [];
$line_count = 0;

// phpcs:disable Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
/**
* This is a valid and intentional use.
*/
while ( ( $line = fgets( $handle ) ) !== false ) {
namithj marked this conversation as resolved.
Show resolved Hide resolved
$lines[] = trim( $line );
++$line_count;

if ( $count_bottom_to_top ) {
if ( $number_of_lines > 0 && $line_count > $number_of_lines ) {
array_shift( $lines );
costdev marked this conversation as resolved.
Show resolved Hide resolved
}
} elseif ( $number_of_lines > 0 && $line_count >= $number_of_lines ) {
break;
}
}
// phpcs:enable

fclose( $handle );

return $lines;
}

/**
* Write contents to a file with additional modes.
*
* @param string $file The path to the file.
* @param string $contents The content to write.
* @param int|false $mode Optional. The file permissions as octal number, usually 0644.
* Default false.
* @param string $write_mode The write mode:
* 'w' - Overwrite the file (default).
* 'a' - Append to the file.
* 'x' - Create a new file and write, fail if the file exists.
* 'c' - Open the file for writing, but do not truncate.
* @return bool True on success, false on failure.
*/
public function put_contents( $file, $contents, $mode = false, $write_mode = 'w' ) {
$valid_write_modes = [ 'w', 'a', 'x', 'c' ];
if ( ! in_array( $write_mode, $valid_write_modes, true ) ) {
return false;
}

$handle = @fopen( $file, $write_mode );
if ( ! $handle ) {
return false;
}

mbstring_binary_safe_encoding();
$data_length = strlen( $contents );
$bytes_written = fwrite( $handle, $contents );
reset_mbstring_encoding();

fclose( $handle );

if ( $data_length !== $bytes_written ) {
return false;
}

$this->chmod( $file, $mode );

return true;
}
}
23 changes: 23 additions & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,23 @@
<!-- Enforce short syntax arrays. -->
<rule ref="Generic.Arrays.DisallowLongArraySyntax"/>

<!-- Exclude custom Filesystem API implementations from certain sniffs. -->
<rule ref="WordPress.WP.AlternativeFunctions.file_system_operations_fopen">
<exclude-pattern>includes/class-filesystem-direct\.php</exclude-pattern>
</rule>

<rule ref="WordPress.WP.AlternativeFunctions.file_system_operations_fclose">
<exclude-pattern>includes/class-filesystem-direct\.php</exclude-pattern>
</rule>

<rule ref="WordPress.WP.AlternativeFunctions.file_system_operations_fwrite">
<exclude-pattern>includes/class-filesystem-direct\.php</exclude-pattern>
</rule>

<rule ref="WordPress.PHP.NoSilencedErrors.Discouraged">
<exclude-pattern>includes/class-filesystem-direct\.php</exclude-pattern>
</rule>

<!-- Exclude test classes from naming conventions. -->
<rule ref="WordPress.Files.FileName.InvalidClassFileName">
<exclude-pattern>/tests/phpunit/*</exclude-pattern>
Expand All @@ -82,4 +99,10 @@
<rule ref="WordPress.WP.AlternativeFunctions.unlink_unlink">
<exclude-pattern>/tests/phpunit/*</exclude-pattern>
</rule>
<rule ref="WordPress.WP.AlternativeFunctions.file_system_operations_mkdir">
<exclude-pattern>/tests/phpunit/*</exclude-pattern>
</rule>
<rule ref="WordPress.WP.AlternativeFunctions.file_system_operations_rmdir">
<exclude-pattern>/tests/phpunit/*</exclude-pattern>
</rule>
</ruleset>
30 changes: 15 additions & 15 deletions tests/phpunit/includes/Debug_UnitTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,26 @@ abstract class Debug_UnitTestCase extends WP_UnitTestCase {
protected static $log_file;

/**
* Previously created filesystems.
* The class for use with the Reflection API.
*
* @var array
* @var \ReflectionClass
*/
protected static $filesystems = [];
protected static $reflection;

/**
* The original value of $wp_filesystem before any tests run.
* The default filesystem.
*
* @var WP_Filesystem_Base|null|false False if not already set.
* @var \AspireUpdate\Filesystem_Direct|null
*/
protected static $default_filesystem;

/**
* Previously created filesystems.
*
* @var array
*/
protected static $filesystems = [];

/**
* Gets the log file's path, and deletes if it exists before any tests run.
* Backs up the default filesystem.
Expand All @@ -48,11 +55,8 @@ public static function set_up_before_class() {
unlink( self::$log_file );
}

if ( isset( $GLOBALS['wp_filesystem'] ) ) {
self::$default_filesystem = $GLOBALS['wp_filesystem'];
} else {
self::$default_filesystem = false;
}
self::$reflection = new ReflectionClass( '\AspireUpdate\Debug' );
self::$default_filesystem = self::$reflection->getStaticPropertyValue( 'filesystem' );
}

/**
Expand Down Expand Up @@ -83,11 +87,7 @@ public function tear_down() {
unlink( self::$log_file );
}

if ( false === self::$default_filesystem ) {
unset( $GLOBALS['wp_filesystem'] );
} else {
$GLOBALS['wp_filesystem'] = self::$default_filesystem;
}
self::$reflection->setStaticPropertyValue( 'filesystem', self::$default_filesystem );

parent::tear_down();
}
Expand Down
2 changes: 1 addition & 1 deletion tests/phpunit/includes/class-ap-fake-filesystem.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';

class AP_FakeFilesystem extends WP_Filesystem_Direct {
class AP_FakeFilesystem extends AspireUpdate\Filesystem_Direct {
/**
* Whether paths should exist.
*
Expand Down
Loading
Loading