403Webshell
Server IP : 104.21.73.115  /  Your IP : 216.73.217.127
Web Server : Apache
System : Linux vps42439 6.8.0-136-generic #136~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Fri Jul 3 16:29:11 UTC x86_64
User : dh_7hi3h9 ( 6349477)
PHP Version : 8.3.30
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : ON
Directory :  /home/dh_7hi3h9/regalflower.com/jetpack-temp/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /home/dh_7hi3h9/regalflower.com/jetpack-temp/jp-helper-syEYZw6Aha.php
<?php /* Jetpack Backup Helper Script */

// The following placeholder will be replaced by the Transport Server. If you change / reformat it, make sure to make
// changes in the placeholder replacer code too.
define( 'JP_EXPIRES', 1695223191 );

// The following placeholder will be replaced by the Transport Server. If you change / reformat it, make sure to make
// changes in the placeholder replacer code too.
define( 'JP_SECRET', 'wLQ7L8ZK4yg0H5tTHg92QcHrM3oSjCnd' );

// This is used / replaced when the helper script is uploaded over HTTP.
define( 'WP_PATH', '/home/wp_zrpw99/regalflowers.com.ng/' );

// Error codes
define( 'COMMS_ERROR',        128 );
define( 'MYSQLI_ERROR',       129 );
define( 'MYSQL_ERROR',        130 );
define( 'NOT_FOUND_ERROR',    131 );
define( 'READ_ERROR',         132 );
define( 'INVALID_TYPE_ERROR', 133 );
define( 'MYSQL_INIT_ERROR',   134 );
define( 'CREDENTIALS_ERROR',  135 );
define( 'WRITE_ERROR',        136 );
define( 'EXPIRY_ERROR',       137 );

// disable various swift-performance-lite features that interfere with us
// this caches things so aggressively that even helper script responses over SSH are cached!
// in theory there are filters defined in the plugin that should allow us to turn it off like that
// however, I was unable to get them to do anything, so we're stuck with this jank
$_GET['swift-no-cache'] = 1;
define( 'SWIFT_PERFORMANCE_THREAD', false );

// THRESHOLDS
define( 'SMALL_FILE_THRESHOLD', 500 * 1024 );             // 500KB
define( 'LARGE_FILE_THRESHOLD', 200 * 1024 * 1024 );      // 200MB
define( 'HASH_LIMIT_CLI',       1 * 1024 * 1024 * 1024 ); // 1GB
define( 'HASH_LIMIT',           200 * 1024 * 1024 );      // 200 MB
define( 'HASH_PARTIAL_LIMIT',   200 * 1024 * 1024 );      // 200 MB

// True if Atomic
$is_dynamic_ssh = false;

/**
 * Tests whether the helper script is being run in a CLI.
 *
 * @return bool True if the helper script is being run in a CLI, false otherwise.
 */
function is_cli() {
	return 'cli' === php_sapi_name();
}

function fatal_error( $code, $message, $http_code = 200 ) {

	if ( is_cli() ) {
		fwrite( STDERR, "\n" . json_encode( array(
			'code'    => $code,
			'message' => $message,
		) ) . "\n" );
		die( $code );
	} else {
		header( 'X-VP-Ok: 0', true, $http_code );
		header( 'X-VP-Error-Code: ' . $code );
		header( 'X-VP-Error: ' . base64_encode( $message ) );
		exit;
	}
}

function success_header() {
	if ( ! is_cli() ) {
		header( 'X-VP-Ok: 1', true );
	}
}

function authenticate( $action, $json_args, $salt, $incoming_signature ) {
	$to_sign   = "{$action}:{$json_args}:{$salt}";
	$signature = hash_hmac( 'sha1', $to_sign, JP_SECRET );

	return hash_equals( $signature, $incoming_signature );
}

function jpr_action( $action, $args ) {
	$actions = array(
		'db_results'                  => 'action_db_results',
		'db_dump'                     => 'action_db_dump',
		'db_upload'                   => 'action_db_upload',
		'db_import'                   => 'action_db_import',
		'count_files'                 => 'action_count_files',
		'ls'                          => 'action_ls',
		'grep'                        => 'action_grep',
		'stat'                        => 'action_stat',
		'test'                        => 'action_test',
		'info'                        => 'action_info',
		'paths'                       => 'action_paths',
		'cleanup_helpers'             => 'action_cleanup_helpers',
		'cleanup_restore'             => 'action_cleanup_restore',
		'walk'                        => 'action_walk',
		'flush'                       => 'action_flush',
		'trigger_jp_sync'             => 'action_trigger_jp_sync',
		'delete_tree'                 => 'action_delete_tree',
		'get_active_theme'            => 'action_get_active_theme',
		'symlink'                     => 'action_symlink',
		'validate_theme'              => 'action_validate_theme',
		'woocommerce_install'         => 'action_woocommerce_install',
		'get_file'                    => 'action_get_file',
		'enable_jetpack_sso'          => 'action_enable_jetpack_sso',
		'transfer_jetpack_connection' => 'action_transfer_jetpack_connection',
		'install_extension'           => 'action_install_extension',
		'check_file_existence'        => 'action_check_file_existence',
		'upgrade_extension'           => 'action_upgrade_extension',
		'remove_waf_blocklog'         => 'action_remove_waf_blocklog',
		'dpc_receive'                 => 'action_dpc_receive',
	);

	if ( empty( $actions[ $action ] ) ) {
		fatal_error( COMMS_ERROR, 'Invalid method', 405 );
	}

	call_user_func( $actions[ $action ], $args );
}

function get_wordpress_location() {
	if ( '[wp_' . 'path]' !== WP_PATH ) {
		return rtrim( WP_PATH, '/\\' );
	} else {
		return dirname( __DIR__ );
	}
}

function localize_path( $path ) {
	return preg_replace( '/^{\$ABSPATH\}/', get_wordpress_location(), $path );
}

function load_wp( $with_plugins = false, $error_func = 'fatal_error', $short_init = false ) {
	if ( ! defined( 'WP_INSTALLING' ) && ! $with_plugins ) {
		define( 'WP_INSTALLING', true );
	}

	if ( ! defined( 'SHORTINIT' ) && $short_init ) {
		// Stop most of WordPress from being loaded if we just want the basics.
		// see https://wpengineer.com/2449/load-minimum-of-wordpress/
		// see https://stackoverflow.com/questions/5306612/using-wpdb-in-standalone-script
		define( 'SHORTINIT', true );
	}

	$wp_directory = get_wordpress_location();
	$wp_load_path = $wp_directory . '/wp-load.php';
	if ( ! file_exists( $wp_load_path ) ) {
		call_user_func( $error_func, CREDENTIALS_ERROR, "Could not find WordPress in {$wp_directory}" );
	}

	if ( ! is_readable( $wp_load_path ) ) {
		call_user_func( $error_func, CREDENTIALS_ERROR, "Can not read wp-load.php in {$wp_directory}" );
	}

	ob_start();
	require_once( $wp_load_path );
	ob_end_clean();
}

function encode_json_with_check( $obj ) {
	$json_options = 0;
	if ( defined( 'JSON_PARTIAL_OUTPUT_ON_ERROR' ) ) {
		// since PHP 5.5.0; allows us to handle more weird characters without completely failing
		// since PHP 5.4.0; gives us better output for some unicode characters that don't seem to escape otherwise
		$json_options = JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_UNESCAPED_UNICODE;
	} elseif ( defined( 'JSON_UNESCAPED_UNICODE' ) ) {
		// since PHP 5.4.0; gives us better output for some unicode characters that don't seem to escape otherwise
		$json_options = JSON_UNESCAPED_UNICODE;
	}

	$json = json_encode( $obj, $json_options );
	if ( false === $json ) {
		fatal_error( COMMS_ERROR, 'JSON error: ' . json_last_error() );
	}

	return $json;
}

function send_json_with_check( $obj, $with_newline = true, $flush_afterwards = true ) {
	$json = encode_json_with_check( $obj );

	echo $json;
	if ( $with_newline ) {
		echo "\n";
	}

	if ( $flush_afterwards ) {
		// Some webservers and PHP configurations are made to buffer the output of scripts, and we both:
		//
		// 1. Expect to get the output of various commands (e.g. SQL imports) live, and
		// 2. Don't want the webserver or some middleware (e.g. Cloudflare) to time out idle connections (which might
		//    become idle if the webserver is buffering output and not sending anything back).
		//
		flush();
	}
}

function action_test( $args ) {
	success_header();
	echo json_encode( array( 'ok' => true ) );
	exit;
}

function action_flush( $args ) {
	if ( ! empty( $args['load_full_wp'] ) ) {
		load_wp( true );
	} else {
		load_wp();
	}

	delete_option( 'rewrite_rules' );

	if ( function_exists( 'wp_cache_flush' ) ) {
		wp_cache_flush();

		success_header();
		echo json_encode( array( 'ok' => true ) );
		exit;
	}

	if ( function_exists( 'wp_cache_clean_cache' ) ) {
		global $file_prefix;
		wp_cache_clean_cache( $file_prefix, true );
	}

	fatal_error( COMMS_ERROR, 'wp_cache_flush() not loaded' );
}

function action_trigger_jp_sync( $args ) {
	load_wp( true ); // need plugins so we get the JP functions

	if ( is_callable( array( 'Automattic\Jetpack\Sync\Actions', 'do_full_sync' ) ) ) {
		\Automattic\Jetpack\Sync\Actions::do_full_sync();

		success_header();
		echo json_encode( array( 'ok' => true, 'legacy_sync_call' => false ) );
		exit;
	}

	// this call is deprecated since jetpack-7.5,
	// but we still need to use it for sites running older versions of plugin
	if ( is_callable( array( 'Jetpack_Sync_Actions', 'do_full_sync' ) ) ) {
		Jetpack_Sync_Actions::do_full_sync();

		success_header();
		echo json_encode( array( 'ok' => true, 'legacy_sync_call' => true ) );
		exit;
	}

	fatal_error( COMMS_ERROR, 'Neither Automattic\Jetpack\Sync\Actions::do_full_sync() nor Jetpack_Sync_Actions::do_full_sync() loaded' );
}

function action_upgrade_extension( $args ) {
	load_wp( true );

	$slug = $args['slug'];
	$type = $args['type'];

	if ( function_exists( 'jetpack_require_lib' ) ) {
		jetpack_require_lib( 'plugins' );
	}

	if ( class_exists( 'Automatic_Upgrader_Skin' ) ) {
		$skin = new Automatic_Upgrader_Skin();

		if ( $type === 'plugin' ) {
			$upgrader = new Plugin_Upgrader( $skin );
			$extension_path = get_plugin_path_from_slug( $slug );
		} else {
			$upgrader = new Theme_Upgrader( $skin );
			$extension_path = $slug;
		}

		$result = $upgrader->upgrade( $extension_path );

		if ( ! $result ) {
			$error_messages = print_r( $skin->get_upgrade_messages(), true );
			fatal_error( COMMS_ERROR, 'Could not upgrade extension: ' . $error_messages );
		}

		if ( is_wp_error( $result ) ) {
			fatal_error( COMMS_ERROR, 'Could not upgrade extension: ' . $result->get_error_message() );
		}

		success_header();
		echo json_encode( array( 'ok' => true ) );
		exit;
	}

	fatal_error( COMMS_ERROR, 'Automatic_Upgrader_Skin not loaded' );
}

function action_remove_waf_blocklog( $args ) {
	load_wp( true );

	$contentPath = defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR : $absPath . '/wp-content';

	@unlink( $contentPath . '/jetpack-waf/waf-blocklog');

	success_header();
	echo json_encode( array( 'ok' => true ) );
	exit;
}

function action_install_extension( $args ) {
	load_wp( true );

	if ( function_exists( 'jetpack_require_lib' ) ) {
		jetpack_require_lib( 'plugins' );
	}

	$slug = $args['slug'];
	$type = $args['type'];

	if ( class_exists( 'Jetpack_Automatic_Install_Skin' ) ) {
		$skin = new Jetpack_Automatic_Install_Skin();

		if ( $type === 'plugin' ) {
			$upgrader = new Plugin_Upgrader( $skin );
			$zipUrl = "https://downloads.wordpress.org/plugin/$slug.latest-stable.zip";
		} else {
			$upgrader = new Theme_Upgrader( $skin );
			$zipUrl = "https://downloads.wordpress.org/theme/$slug.latest-stable.zip";
		}

		$result = $upgrader->install( $zipUrl );

		if ( ! $result ) {
			fatal_error( COMMS_ERROR, 'Could not install extension' );
		}

		if ( is_wp_error( $result ) ) {
			fatal_error( COMMS_ERROR, 'Could not install extension: ' . $result->get_error_message() );
		}

		success_header();
		echo json_encode( array( 'ok' => true ) );
		exit;
	}

	fatal_error( COMMS_ERROR, 'Jetpack_Plugins::install_plugin() not loaded' );
}

function action_info( $args ) {
	if ( ! empty( $args['load_full_wp'] ) ) {
		load_wp( true );
	} else {
		load_wp();
	}
	global $wpdb, $wp_version, $wp_theme_directories;

	// get installed themes.
	$themes = array();
	$current_theme = wp_get_theme();
	foreach ( wp_get_themes() as $key => $theme ) {
		$themes[ $key ] = array(
			'Name' => $theme['Name'],
			'ThemeURI' => $theme->get( 'ThemeURI' ),
			'Version' => $theme['Version'],
			'Author' => $theme->get( 'Author' ), // use get() to get the raw value; array access uses display() not get()
			'AuthorURI' => $theme->get( 'AuthorURI'),
			'path' => base64_encode( $theme->get_stylesheet_directory() . '/style.css' ),
			'status' => $theme['Name'] === $current_theme['Name'] ? 'active': 'inactive',
		);
	}

	// get installed plugins.
	if ( ! function_exists( 'get_plugins' ) ) {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';
	}
	$plugins = get_plugins();

	// post-process so these are by slug too, like themes.
	$plugins_by_slug = array();
	foreach ( $plugins as $path => $plugin ) {
		if ( false === strpos( $path, '/' ) ) {
			$slug = explode( '.php', $path );
			$slug = $slug[0];
		} else {
			$slug = explode( '/', $path );
			$slug = $slug[0];
		}
		$plugins_by_slug[ $slug ] = $plugin;
		$plugins_by_slug[ $slug ]['path'] = base64_encode( WP_PLUGIN_DIR . '/' . $path );
		$plugins_by_slug[ $slug ]['status'] = is_plugin_active( $path ) ? 'active' : 'inactive';
	}

	// grab some useful constants
	$useful_constants = array( 'IS_PRESSABLE', 'VIP_GO_ENV' );
	$constant_values = array();
	foreach ( $useful_constants as $constant ) {
		if ( defined( $constant ) ) {
			$constant_values[ $constant ] = constant( $constant );
		}
	}

	// get info about foreign key constraints
	$fks = $wpdb->get_results( $wpdb->prepare( "
		SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
		FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
		WHERE REFERENCED_TABLE_SCHEMA = %s
		AND TABLE_NAME LIKE %s",
		$wpdb->dbname,
		$wpdb->esc_like( $wpdb->prefix ) . '%'
	) );

	$absPath = get_wordpress_location();
	$contentPath = defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR : $absPath . '/wp-content';
	$pluginsPath = defined( 'WP_PLUGIN_DIR' ) ? WP_PLUGIN_DIR : $contentPath . '/plugins';
	$uploadsPath = wp_upload_dir()['basedir'];
	if ( ! is_dir( $uploadsPath ) ) {
		$uploads_path_option = trim( get_option( 'upload_path' ) );
		if ( ! empty( $uploads_path_option ) && is_dir( $absPath . '/' . $uploads_path_option ) ) {
			$uploadsPath = $absPath . '/' . $uploads_path_option;
		} else if ( defined( 'UPLOADS' ) && is_dir( $absPath . '/' . UPLOADS ) ) {
			$uploadsPath = $absPath . '/' . UPLOADS;
		} else {
			$uploadsPath = $contentPath . '/uploads';
		}
	}

	$theme_paths = $wp_theme_directories;
	if ( ! is_array( $wp_theme_directories ) ) {
		$theme_paths = array( $contentPath . '/themes' );
	}

	// If the content directory is considered "under" the abspath it won't be
	// explicitly walked.  If it's encountered as a symlink during normal walking
	// it will be skipped if it's not under abspath.  We resolve it so that it
	// will be explicitly walked if it resolves outside of abspath.
	if ( is_link( $contentPath ) ) {
		$symlinkContent = $contentPath;
		$contentPath = realpath( $contentPath );

		if ( strpos( $pluginsPath, $symlinkContent ) == 0 ) {
			$pluginsPath = str_replace( $symlinkContent, $contentPath, $pluginsPath );
		}
		if ( strpos( $uploadsPath, $symlinkContent ) == 0 ) {
			$uploadsPath = str_replace( $symlinkContent, $contentPath, $uploadsPath );
		}
		$idx = 0;
		foreach( $theme_paths as $theme_path ) {
			if ( strpos( $theme_path, $symlinkContent ) == 0 ) {
				$theme_paths[$idx] = str_replace( $symlinkContent, $contentPath, $theme_path );
			}
			++$idx;
		}
	}

	success_header();
	send_json_with_check( array(
		'wp_version' => $wp_version,
		'php_version' => phpversion(),
		'php_settings' => array(
			'memory_limit' => ini_get( 'memory_limit' ),
			'max_execution_time' => ini_get( 'max_execution_time' ),
		),
		'locale' => get_locale(),
		'table_prefix' => $wpdb->prefix,
		'themes' => $themes,
		'plugins' => $plugins_by_slug,
		'constants' => $constant_values,
		'foreign_keys' => $fks,
		'multisite' => is_multisite(),
		'themePaths' => array_map( 'base64_encode', $theme_paths ),
		'pluginsPath' => base64_encode( $pluginsPath ),
		'contentPath' => base64_encode( $contentPath ),
		'uploadsPath' => base64_encode( $uploadsPath ),
		'abspath' => base64_encode( $absPath ),
		'baseUrl' => get_site_url(),
		'dbCredentialsInfo' => load_db( function(){}, true ), // only test connection.
	), false );
}

/**
 * This is a simplified version of info() method above that returns paths-related fields only.
 *
 * It's used by toPhpPath() function in Transport Server.
 *
 * This method applies a fallback for cases where wp-config.php file is not present on the site.
 */
function action_paths( $args ) {
	$is_wp_loaded = false;
	$absPath = get_wordpress_location();

	if ( file_exists( $absPath . '/wp-load.php' ) ) {
		global $wp_theme_directories;

		if ( ! empty( $args['load_full_wp'] ) ) {
			load_wp( true );
		} else {
			load_wp();
		}
		$is_wp_loaded = true;

		$contentPath = defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR : $absPath . '/wp-content';
		$pluginsPath = defined( 'WP_PLUGIN_DIR' ) ? WP_PLUGIN_DIR : $contentPath . '/plugins';
		$uploadsPath = wp_upload_dir()['basedir'];

		$themePaths = $wp_theme_directories;
		if ( ! is_array( $wp_theme_directories ) ) {
			$themePaths = array( $contentPath . '/themes' );
		}
	}
	else {
		// apply a fallback for cases where wp-config.php is not there
		$contentPath = $absPath . '/wp-content';
		$pluginsPath = $contentPath . '/plugins';
		$uploadsPath = $contentPath . '/uploads';
		$themePaths  = array( $contentPath . '/themes' );
	}

	success_header();
	send_json_with_check( array(
		'is_wp_loaded' => $is_wp_loaded,
		'abspath' => base64_encode( $absPath ),
		'pluginsPath' => base64_encode( $pluginsPath ),
		'contentPath' => base64_encode( $contentPath ),
		'uploadsPath' => base64_encode( $uploadsPath ),
		'themePaths' => array_map( 'base64_encode', $themePaths ),
	), false );
}

function db_query( $sql, $error_func = 'fatal_error' ) {
	global $wpdb;

	if( ! is_object( $wpdb ) ) {
		load_db( $error_func );
	}

	if ( ! $wpdb->dbh ) {
		call_user_func( $error_func, MYSQL_INIT_ERROR, 'MySQL not initialized' );
	}

	// prepend the SQL query comment with the source
	// https://www.php.net/manual/en/function.debug-backtrace.php
	$backtrace = debug_backtrace( /* $options = */ DEBUG_BACKTRACE_IGNORE_ARGS,  /* $limit = */ 2 );
	$caller = isset( $backtrace[ 1 ] ) ? $backtrace[ 1 ][ 'function' ] : __FUNCTION__;

	// allow only alphanumeric characters and the underscore
	$caller = preg_replace( '#[^a-z0-9_]#i', '', $caller );
	$sql = "/* jp-helper {$caller}() */ {$sql}";

	if ( $wpdb->use_mysqli ) {
		$result = mysqli_query( $wpdb->dbh, $sql, MYSQLI_USE_RESULT );
		if ( ! $result ) {
			call_user_func( $error_func, MYSQLI_ERROR, mysqli_error( $wpdb->dbh ) );
		}
	} else {
		$result = mysql_unbuffered_query( $sql, $wpdb->dbh );
		if ( ! $result ) {
			call_user_func( $error_func, MYSQL_ERROR, mysql_error( $wpdb->dbh ) );
		}
	}

	return $result;
}

function action_db_results( $args ) {
	global $wpdb;

	$args = array_merge( array(
		'query' => null,
	), $args );

	$query = db_query( $args['query'] );

	success_header();

	$fields = null;
	$types = [];

	$flush_every_x_rows = 100;
	$row_number = 0;

	while ( ! empty( $row = ( $wpdb->use_mysqli ? mysqli_fetch_assoc( $query ) : mysql_fetch_assoc( $query ) ) ) ) {
		// First row; detect the names of the fields, send as an array.
		if ( empty( $fields ) ) {
			$fields = array_keys( $row );
			send_json_with_check( array_map( 'base64_encode', $fields ), true, false );

			// detect columns types to provide better typing of dumped values
			// for instance do not base64-encode numeric values in BIT columns
			if ( $wpdb->use_mysqli ) {
				foreach ( $fields as $field_mame ) {
					// @see https://www.php.net/manual/en/mysqli-result.fetch-field.php
					$info = mysqli_fetch_field( $query );

					if ( is_object( $info ) && ( $field_mame === $info->name ) ) {
						$types[$info->name] = [
							'flags'      => $info->flags,
							// for, now apply the numeric encoding to BIT columns only
							// @see https://www.php.net/manual/en/mysqli.constants.php
							'is_numeric' => in_array( $info->type, [
								16, // MYSQLI_TYPE_BIT
							] ),
						];
					}
				}
			}
		}

		$values = array_map(
			function( $field ) use ( &$row, $types ) {
				if ( is_null( $row[$field] ) ) {
					return null;
				}

				if ( !empty( $types[ $field ][ 'is_numeric'] ) ) {
					return (int) $row[$field];
				}

				return base64_encode( $row[$field] );
			},
			$fields
		);
		send_json_with_check( $values, true, false );

		++$row_number;
		if ( 0 === ( $row_number % $flush_every_x_rows ) ) {
			flush();
		}
	}

	flush();

	if ( $wpdb->use_mysqli ) {
		@mysqli_free_result( $query );
	} else {
		@mysql_free_result( $query );
	}
}

/**
 * This method is similar to action_db_results but adds a prefix "WPFIELDTYPE" with the type of the field.
 * This can be used by the received to take decisions about how to read it.
 *
 * @param array $args Array with the info of the query to run
 */
function action_db_dump( $args ) {
	global $wpdb;

	$args = array_merge( array(
		'query' => null,
	), $args );

	$query = db_query( $args['query'] );

	success_header();

	$fields = null;

	$flush_every_x_rows = 100;
	$row_number = 0;

	while ( ! empty( $row = ( $wpdb->use_mysqli ? mysqli_fetch_assoc( $query ) : mysql_fetch_assoc( $query ) ) ) ) {
		// First row; detect the names of the fields, send as an array.
		if ( empty( $fields ) ) {
			$fields = array_keys( $row );
			send_json_with_check( array_map( 'base64_encode', $fields ), true, false );
		}

		$index = 0;
		$values = [];
		foreach ( $fields as $field_key ) {
			$field = mysqli_fetch_field_direct( $query, $index );

			if ( is_null( $row[ $field_key ] ) ) {
				$values[] = null;
			} else {
				$values[] = base64_encode( 'WPFIELDTYPE' . $field->type . '_' . $row[ $field_key ] . '' );
			}
			$index++;
		}

		send_json_with_check( $values, true, false );

		++$row_number;
		if ( 0 === ( $row_number % $flush_every_x_rows ) ) {
			flush();
		}
	}

	flush();

	if ( $wpdb->use_mysqli ) {
		@mysqli_free_result( $query );
	} else {
		@mysql_free_result( $query );
	}
}

function action_db_upload( $args ) {
	$args = array_merge( array(
		'sql' => null,
	), $args );

	foreach ( explode( ";\n", $args['sql'] ) as $line ) {
		$line = trim( $line );
		if ( empty( $line ) ) {
			continue;
		}

		db_query( $line );
	}

	success_header();
	echo "Success\n";
}

function streamed_error( $code, $message ) {
	// no cli/http switch, just send the content in whatever stream this is
	send_json_with_check( array( 'error' => true, 'code' => $code, 'message' => $message ) );
	exit;
}


/**
 * action_db_import()'s "executor", i.e. class that imports SQL queries or reports statuses.
 *
 * Implementations might choose to do the actual import / report back errors in a HTTP response, or log imported
 * queries / statuses for testing purposes.
 */
interface Action_DB_Import_Executor {

	/**
	 * Import a SQL query.
	 *
	 * @param string $sql        SQL query.
	 * @param string $error_func Function to call on errors.
	 *
	 * @return bool|mysqli_result|resource Return value of either mysqli_query() or mysql_query().
	 */
	public function db_query( $sql, $error_func );

	/**
	 * Report back import status.
	 *
	 * @param mixed $obj Free-form status message to be encoded to JSON and sent back to the caller.
	 *
	 * @return void
	 */
	public function status_report( $obj );

}


/**
 * action_db_import()'s default "executor" which executes queries in MySQL and sends back JSON responses.
 */
class Action_DB_Import_Default_Executor implements Action_DB_Import_Executor {

	/** @noinspection PhpDeprecationInspection */
	public function db_query( $sql, $error_func ) {

		// Same as the db_query() global function, just doesn't use unbuffered functions to avoid the need to free the
		// result, e.g. if we were to use mysqli_query( ..., MYSQLI_USE_RESULT ), one of the queries returned
		// something, and we forgot to read/free that returned value, the subsequent queries would stop working.

		global $wpdb;

		if ( ! is_object( $wpdb ) ) {
			load_db( $error_func );
		}

		if ( ! $wpdb->dbh ) {
			call_user_func( $error_func, MYSQL_INIT_ERROR, 'MySQL not initialized' );
		}

		if ( $wpdb->use_mysqli ) {
			$result = mysqli_query( $wpdb->dbh, $sql );
			if ( ! $result ) {
				call_user_func( $error_func, MYSQLI_ERROR, mysqli_error( $wpdb->dbh ) );
			}
		} else {
			$result = mysql_query( $sql, $wpdb->dbh );
			if ( ! $result ) {
				call_user_func( $error_func, MYSQL_ERROR, mysql_error( $wpdb->dbh ) );
			}
		}

		return $result;
	}

	public function status_report( $obj ) {
		send_json_with_check( $obj );
	}

}



/**
 * action_db_import()'s "stopper", i.e. class that decides when we should stop importing a part of an SQL dump.
 *
 * Implementations might count number of queries or time elapsed since the start of the import.
 */
interface Action_DB_Import_Stopper {

	/**
	 * @param string $line SQL line that was just imported.
	 *
	 * @return bool If true, SQL import should be stopped as we've imported enough for this action_db_import() call.
	 */
	public function should_stop_after_importing_line( $line );

}


/**
 * "Stopper" that counts the number of seconds elapsed, and stops importing after some time.
 */
class Action_DB_Import_Time_Stopper implements Action_DB_Import_Stopper {

	/**
	 * Number of seconds to let the import go on for.
	 *
	 * @var int
	 */
	private $seconds;

	/**
	 * UNIX timestamp on when the import has started.
	 *
	 * @var int
	 */
	private $started;

	/**
	 * Constructor.
	 *
	 * @param int $seconds Number of seconds to let the import go on for.
	 */
	public function __construct( $seconds ) {
		$this->seconds = $seconds;
		$this->started = time();
	}

	public function should_stop_after_importing_line( $line ) {
		if ( time() - $this->started >= $this->seconds ) {
			return true;
		}

		return false;
	}

}


/**
 * Import a part of the SQL dump using the "executor", stop importing when "stopper" decides so.
 *
 * Won't stop importing when executing queries between "--block-sql-start;" and "--block-sql-end;" markers.
 *
 * Calls streamed_error() on errors.
 *
 * @param resource                  $handle                File handle to read the SQL dump from.
 * @param int                       $lines_imported_so_far Number of SQL statements imported so far.
 * @param int                       $sql_dump_byte_offset  Byte offset in the dump file from which to start importing.
 * @param Action_DB_Import_Executor $executor              "Executor", i.e. the object that will execute SQL queries
 *                                                         and report statuses.
 * @param Action_DB_Import_Stopper  $stopper               "Stopper", i.e. the object that will decide when to start
 *                                                         importing the dump.
 * @param int                       $chunk_size            How many bytes to read from the SQL dump file in one go.
 *
 * @return void
 */
function do_action_db_import(
	$handle,
	$lines_imported_so_far,
	$sql_dump_byte_offset,
	$executor,
	$stopper,
	$chunk_size = 100 * 1024
) {

	$indivisible_block_start_marker = '-- block-sql-start;';
	$indivisible_block_end_marker   = '-- block-sql-end;';

	$inside_indivisible_block = false;

	$stopped_by_stopper = false;

	$buffer_remainder = '';

	fseek( $handle, $sql_dump_byte_offset );

	while ( ( ! feof( $handle ) ) && ( ! $stopped_by_stopper ) ) {

		$buffer = fread( $handle, $chunk_size );
		if ( false === $buffer ) {
			streamed_error( READ_ERROR, 'Unable to fread() from the dump file' );

			return;
		}

		$buffer = $buffer_remainder . $buffer;

		// We use regexps here to handle optional "\r\n" style newlines as well as sane "\n" newlines.
		// mysqldump generates these, sadly.
		$lines_and_offsets = preg_split( "/;\r?\n/", $buffer, - 1, PREG_SPLIT_OFFSET_CAPTURE );

		// Keep track of the offset of the next match, so that if the "stopper" decides that it's time for us to stop
		// importing after we execute the current line, the offset of the next line can be sent back to the caller for
		// the purpose of resuming the import at the right location.
		$next_offset = null;

		// Skip the last line as it will be a part of the next buffer (buffer remainder).
		for ( $x = 0; $x < count( $lines_and_offsets ) - 1; ++ $x ) {

			$current_line_and_offset = $lines_and_offsets[ $x ];
			$current_line            = trim( $current_line_and_offset[0] );

			$next_line_and_offset = $lines_and_offsets[ $x + 1 ];
			$next_offset          = $next_line_and_offset[1];

			if ( empty( $current_line ) ) {
				continue;
			}

			// Add back the ";" that the preg_split() has removed.
			$current_line .= ';';

			if ( $indivisible_block_start_marker === $current_line ) {
				$inside_indivisible_block = true;
			} elseif ( $indivisible_block_end_marker === $current_line ) {
				$inside_indivisible_block = false;
			} else {
				$executor->db_query( $current_line, 'streamed_error' );
				++ $lines_imported_so_far;
			}

			if ( $stopper->should_stop_after_importing_line( $current_line ) ) {

				if ( ! $inside_indivisible_block ) {
					$stopped_by_stopper = true;
					break;
				}
			}
		}

		// Next offset might be null if preg_split() didn't find any newlines.
		if ( ! is_null( $next_offset ) ) {
			$sql_dump_byte_offset += $next_offset;
		}

		$last_line_and_offset = end( $lines_and_offsets );
		$last_line            = $last_line_and_offset[0];

		$buffer_remainder = $last_line;
	}

	if ( $stopped_by_stopper ) {

		$executor->status_report(
			array(
				'error'    => false,
				'message'  => "Imported $lines_imported_so_far queries so far",
				'offset'   => $sql_dump_byte_offset,
				'imported' => $lines_imported_so_far,
			)
		);

	} else {

		// Import the remainder only if not stopped by the stopper, i.e. we're at the end of the dump. Let's hope the
		// remainder doesn't take too much time to import.
		$buffer_remainder = trim( $buffer_remainder );
		if ( strlen( $buffer_remainder ) > 0 ) {

			$executor->db_query( $buffer_remainder, 'streamed_error' );

			if ( ! in_array(
				$buffer_remainder,
				array(
					$indivisible_block_start_marker,
					$indivisible_block_end_marker,
				)
			) ) {
				++ $lines_imported_so_far;
			}
		}

		$executor->status_report(
			array(
				'error'    => false,
				'message'  => "Success, $lines_imported_so_far queries overall",
				'imported' => $lines_imported_so_far,
			)
		);

	}

}


/**
 * Import a part of the SQL dump, stop after a few seconds, return an offset.
 *
 * The function assumes that each and every statement is on its own line.
 *
 * Will stop importing SQL queries after it has imported 4 seconds worth of queries. However, the function will not
 * stop importing in the middle of the indivisible block, i.e. a block of SQL enclosed within "--block-sql-start;" and
 * "--block-sql-end;" markers (each on their very own line, and with the semicolon at the end).
 *
 * @param array $args Argument array:
 *                    * "importPath" (string, required) - path to the SQL dump to import;
 *                    * "imported" (integer, optional) - number of SQL statements imported so far;
 *                    * "offset" (integer, optional) - byte offset in the dump file from which to start importing.
 *
 * @return void
 * @throws Exception
 */
function action_db_import( $args ) {
	// Start reporting back right away, we stream back errors and status via the body.
	success_header();

	$stop_after_seconds = 4;

	if ( empty( $args['importPath'] ) ) {
		streamed_error( COMMS_ERROR, 'Invalid path' );
	}

	$import_path    = localize_path( base64_decode( $args['importPath'] ) );
	$lines          = ! empty( $args['imported'] ) && is_numeric( $args['imported'] ) ? $args['imported'] : 0;
	$current_offset = ! empty( $args['offset'] ) && is_numeric( $args['offset'] ) ? $args['offset'] : 0;

	if ( ! file_exists( $import_path ) ) {
		streamed_error( NOT_FOUND_ERROR, "File not found: $import_path" );
	}

	$handle = fopen( $import_path, 'rb' );
	if ( false === $handle ) {
		streamed_error( READ_ERROR, "Failed to open file $import_path for import." );
	}

	do_action_db_import(
		$handle,
		$lines,
		$current_offset,
		new Action_DB_Import_Default_Executor(),
		new Action_DB_Import_Time_Stopper( $stop_after_seconds )
	);

	fclose( $handle );
}


function clean_pathname_string( $path ) {
	// paths are arbitrary bytes, send them in base-64 so JSON doesn't choke on them
	return base64_encode( $path );
}

function get_username( $stat ) {
	$info = false;
	if ( function_exists( 'posix_getpwuid' ) ) {
		$info = posix_getpwuid( $stat['uid'] );
	}

	if ( $info ) {
		return $info['name'];
	} else {
		return $stat['uid'];
	}
}

function get_groupname( $stat ) {
	$info = false;
	if ( function_exists( 'posix_getgrgid' ) ) {
		$info = posix_getgrgid( $stat['gid'] );
	}

	if ( $info ) {
		return $info['name'];
	} else {
		return $stat['gid'];
	}
}

/**
 * Calculate the hash of a file using parts of the file.
 *
 * @param string $algo The hashing algorithm to use (e.g., 'sha1', 'md5').
 * @param string $full_path The full path of the file.
 *
 * @return string The calculated hash value.
 */
function partial_hash_file( $algo, $full_path ) {
	// Note: If you want to modify this, check lib/hash.ts
	$partSize = 512 * 1024; // 512KB in bytes
	$numSamples = 80;

	$fileSize = filesize( $full_path );
	$sampleOffset = floor( $fileSize / $numSamples );

	$fileHandle  = fopen( $full_path, 'rb' );

	if ( ! $fileHandle ) {
		return false;
	}

	$hashContext = hash_init( $algo );
	$offset      = 0;

	for ( $i = 0; $i <= $numSamples; $i++ ) {
		if ( $i === $numSamples ) {
			// read the last part of the file
			$offset = $fileSize - $sampleOffset;
		}
		fseek( $fileHandle, $offset );
		$chunk = fread( $fileHandle, $partSize );
		hash_update( $hashContext, $chunk );
		$offset += $sampleOffset;
	}

	$hash = hash_final( $hashContext );
	fclose( $fileHandle );

	return $hash;
}

function get_ls_entry( &$args, $path, $file, $skip_hashes = false, $force_full_hashes = false  ) {
	$full_path = $path . '/' . $file;
	$entry = array(
		'name' => clean_pathname_string( $file ),
	);

	if ( is_link( $full_path ) ) {
		$entry['is_link'] = 1;
		$entry['canonical'] = clean_pathname_string( readlink( $full_path ) );
		$entry['absolute'] = clean_pathname_string( realpath( $full_path ) );
	} else {
		$entry['canonical'] = clean_pathname_string( realpath( $full_path ) );
		$entry['absolute'] = $entry['canonical'];
	}

	// TODO: Replace this with the special path (ie. {$PLUGINS}/...)
	$entry['relative'] = clean_pathname_string( str_replace( get_wordpress_location(), '', $full_path ) );

	if ( ! is_readable( $full_path ) ) {
		$entry['unreadable'] = true;
	}

	if ( ! empty( $args['stat'] ) ) {
		$entry['stat'] = stat( $full_path );
		$entry['stat']['username'] = get_username( $entry['stat'] );
		$entry['stat']['groupname'] = get_groupname( $entry['stat'] );
	}

	if ( ! empty( $args['lstat'] ) ) {
		$entry['stat'] = lstat( $full_path );
		$entry['stat']['username'] = get_username( $entry['stat'] );
		$entry['stat']['groupname'] = get_groupname( $entry['stat'] );
	}

	// Remove duplicate data from stat.  We reference the associative values only.
	$numeric_stat_array_total = 13;
	for ( $i = 0; $i < $numeric_stat_array_total; $i++ ) {
		unset( $entry['stat'][$i] );
	}

	if ( isset( $args['window'] ) && floatval( $args['window'] > 1 ) ) {
		// if the caller is windowing the hashes, let them know that the file is unchanged in that window
		// thus, they will not expect the hash to be set for it
		$entry['unchanged'] = ( $entry['stat']['mtime'] < floatval( $args['window'] ) );
	}

	if ( is_dir( $full_path ) ) {
		$entry['is_dir'] = 1;
	} else if ( ! $skip_hashes ) {
		if ( ! is_array( $args['hashes'] ) ) {
			if ( ! empty( $args['hashes'] ) ) {
				$args['hashes'] = array( $args['hashes'] );
			} else {
				$args['hashes'] = array();
			}
		}

		if ( ! $args['window'] || ! $entry['unchanged'] ) {
			// only hash files if the caller didn't specify a window to do that in, or if the file changed in the window
			foreach ( $args['hashes'] as $algo ) {
				if ( in_array( $algo, hash_algos(), true ) ) {
					if ( ! $force_full_hashes && filesize( $full_path ) > HASH_PARTIAL_LIMIT ) {
						$entry[ $algo . '_partial' ] = partial_hash_file( $algo, $full_path );
						$entry[ $algo ] = null;
					} else {
						$entry[ $algo ] = hash_file( $algo, $full_path );
					}
				}
			}
		}
	}

	if ( ! empty( $entry['is_dir'] ) ) {
		// check if this is a WP directory
		if ( is_file( $full_path . '/wp-config.php' ) && is_dir( $full_path . '/wp-content' ) ) {
			$entry['is_wp_root'] = 1;
		}

		// check if this directory contains a donotbackup file
		if ( is_file( $full_path . '/.donotbackup' ) ) {
			$entry['do_not_backup'] = 1;
		}
	}

	return $entry;
}

function locale_safe_basename( $path ) {
	$parts = explode( '/', $path );
	$ret = $parts[ count( $parts ) - 1 ]; // last element
	if ( $ret === '' && count( $parts ) > 1 ) {
		// path ended with a slash; to match dirname() + basename() we need to return the last directory element, not blank
		// make sure we have another choice though
		$ret = $parts[ count( $parts ) - 2 ];
	}
	return $ret;
}

function action_check_file_existence( $args ) {
	$args = array_merge( array(
		'path'   => '/',
		'hashes' => array(),
	), $args );

	$path = localize_path( base64_decode( $args['path'] ) );

	if ( ! file_exists( $path ) ) {
		success_header();
		send_json_with_check( array(
			'found' => false,
		) );
		exit;
	}

	if ( ! $args['lstat'] ) {
		$args['stat'] = true;
	}
	$entry = get_ls_entry( $args, dirname( $path ), locale_safe_basename( $path ) );

	$output = array( 'found' => true );

	foreach ( $args['hashes'] as $algo ) {
			$output[ $algo ] = $entry[ $algo ];
	}

	success_header();
	send_json_with_check( $output, false );
	exit;
}

function action_stat( $args ) {
	$args = array_merge( array(
		'path'              => '/',
		'hashes'            => array(),
		'window'            => false,
		'lstat'             => false,
		'force_full_hashes' => false,
	), $args );

	$path = localize_path( base64_decode( $args['path'] ) );

	if ( ! file_exists( $path ) ) {
		fatal_error( NOT_FOUND_ERROR, "File not found: {$path}" );
	}

	if ( ! $args['lstat'] ) {
		$args['stat'] = true;
	}
	$entry = get_ls_entry( $args, dirname( $path ), locale_safe_basename( $path ), false, $args['force_full_hashes'] );

	success_header();
	send_json_with_check( $entry, false );
	exit;
}

function delete_tree( $path ) {
	$entries_deleted = 1;

	if ( ! is_dir( $path ) ) {
		fatal_error( INVALID_TYPE_ERROR, 'Not a directory: ' . $path );
	}

	foreach ( scandir( $path ) as $name ) {
		if ( $name == '.' || $name == '..' ) {
			continue;
		}

		$child = $path . '/' . $name;
		if ( is_dir( $child ) ) {
			$entries_deleted += delete_tree( $child );
		} else {
			if ( ! @unlink( $child ) ) {
				fatal_error( WRITE_ERROR, "Failed to delete file: {$child}" );
			}
			$entries_deleted++;
		}
	}

	if ( ! @rmdir( $path ) ) {
		fatal_error( WRITE_ERROR, "Failed to delete folder: {$path}" );
	}

	return $entries_deleted;
}

function action_symlink( $args ) {
	if ( empty( $args['path'] ) ) {
		fatal_error( INVALID_TYPE_ERROR, 'Invalid path' );
	}

	if ( empty( $args['target'] ) ) {
		fatal_error( INVALID_TYPE_ERROR, 'Invalid target' );
	}

	$path = localize_path( base64_decode( $args['path'] ) ); // name of the symlink
	$target = localize_path( base64_decode( $args['target'] ) ); // where it points

	$ret = symlink( $target, $path );

	if ( ! $ret ) {
		fatal_error( WRITE_ERROR, 'Symlink failed' );
	}

	success_header();
	echo json_encode( array( 'ok' => true ) );
	exit;
}

function action_delete_tree( $args ) {
	if ( empty( $args['path'] ) ) {
		fatal_error( INVALID_TYPE_ERROR, 'Invalid path' );
	}

	$path = localize_path( base64_decode( $args['path'] ) );
	$entries_deleted = delete_tree( $path );

	success_header();
	send_json_with_check( array(
		'entries' => $entries_deleted,
	) );
	exit;
}

function action_count_files( $args ) {
	if ( empty( $args['path'] ) ) {
		fatal_error( COMMS_ERROR, 'Missing $args[\'path\']' );
	}

	$queue = [ localize_path( base64_decode( $args['path'] ) ) ];
	$result = 0;

	while ( count( $queue ) > 0 ) {
		$parent = array_shift( $queue );
		$dh = opendir( $parent );

		if ( ! $dh ) {
			// not fatal (like /filesystem/walk)
			continue;
		}

		while ( ( $child = readdir( $dh ) ) !== false ) {
			if ( '.' === $child || '..' === $child ) {
				continue;
			}

			$result++;
			$path = "$parent/$child";

			if ( is_dir( $path ) && ! is_link( $path ) ) {
				if ( ! in_array( $path, $queue ) ) {
					array_push( $queue, $path );
				}
			}
		}

		closedir( $dh );
	}

	success_header();
	send_json_with_check( [ 'count' => $result ], false );
	exit;
}

function action_ls( $args ) {
	$args = array_merge( array(
		'path'   => '/',
		'hashes' => array(),
		'stat'   => false,
		'window' => false,
		'include_special_dirs' => false,
	), $args );

	$path = localize_path( base64_decode( $args['path'] ) );

	if ( ! is_dir( $path ) ) {
		fatal_error( INVALID_TYPE_ERROR, "Not a directory: {$path}" );
	}

	$dh = opendir( $path );
	if ( ! $dh ) {
		fatal_error( READ_ERROR, "Failed to read directory: {$path}" );
	}

	success_header();
	while ( ( $file = readdir( $dh ) ) !== false ) {
		if ( ( '.' === $file || '..' === $file ) && ! $args['include_special_dirs'] ) {
			continue;
		}

		$entry = get_ls_entry( $args, $path, $file );

		send_json_with_check( $entry );
	}

	closedir( $dh );
	exit;
}

function action_grep( $args ) {
	$args = array_merge( array(
		'phrase' => '',
		'stat'   => false,
	), $args );

	$phrase = escapeshellarg( base64_decode( $args['phrase'] ) );
	$wp_path = get_wordpress_location() . '/*';
	$output = [];

	exec( "grep --recursive --files-with-matches --exclude-dir=jetpack-temp {$phrase} {$wp_path}", $output );

	if ( false === $output ) {
		fatal_error( READ_ERROR, "Failed to run grep" );
	}

	success_header();
	foreach ( $output as $file ) {
		$absolute_path = dirname( $file );
		$filename = basename( $file );

		$entry = get_ls_entry( $args, $absolute_path, $filename );

		send_json_with_check( $entry );
	}

	exit;
}

/**
 *
 * Decide if we should skip hashing a file based on these conditions:
 * 1. When the file size is larger than our LARGE_FILE_THRESHOLD, always skip
 * 2. When the flag skip_large_hashes is true and the file size is larger than our SMALL_FILE_THRESHOLD and the hash limit
 *
 * @param bool $skip_large_hashes - whether to skip hashing large files
 * @param int $file_size          - size of file in bytes
 * @param int $hash_limit         - size of files to hash before skipping
 * @return bool
 *
 */
function should_skip_hash( $skip_large_hashes, $file_size, $hash_limit ) {

	if ( $file_size <= SMALL_FILE_THRESHOLD ) {
		// Small files are not affected by the hash limit.
		return false;
	} else if ( $file_size > $LARGE_FILE_THRESHOLD ) {
		// This is used to explicitly exclude large files and have them individually hashed
		// so they won't impact walk limits.  Should be based on the larger 1% of files but
		// 200MB was the initial guess, can be adjusted when we have more data.
		// This is checked against regardless of $skip_large_hashes, which applies to total
		// accumulated file hashes.
		return true;
	} else if ( $skip_large_hashes ) {
		// If it was requested to skip large hashes, skip files that are larger than the small file threshold
		// and larger than the hash limit.
		return $file_size > $small_file_threshold && $file_size > $hash_limit;
	} else {
		return false;
	}

}

function action_walk( $args ) {
	$args = array_merge( array(
		'root'              => '/',
		'paths'             => array(),
		'hashes'            => array(),
		'stat'              => false,
		'window'            => false,
		'skip_large_hashes' => false,
		'limit'             => 0,
		'offset'            => 0,
	), $args );

	$paths = array_map( 'base64_decode', $args['paths'] );
	$root = localize_path( base64_decode( $args['root'] ) );
	$soft_limit = 3000;

	// Use execution time to scale up soft time limit
	// It's possible for ini_get to return false
	$max_execution_time    = ini_get('max_execution_time') ? intval( ini_get('max_execution_time') ) : 0;
	$soft_time_window_base = 7;
	$soft_time_window      = $max_execution_time > 30 ? floor( $max_execution_time / 30 * $soft_time_window_base ) : $soft_time_window_base;
	$soft_time_limit       = time() + 7;
	$entries               = 0;
	$first_path            = true;
	success_header();

	// TODO: Rename this through the stack to be more clear.  This deals more with aggregating file sizes
	// and we have a simiarly named client callback that works differently and is unrelated.  We also
	// $large_file_threshold that actually skips individual large file hashes.
	$skip_large_hashes = $args['skip_large_hashes'];

	// Track how much data to hash before giving up on non-small-files.
	// CLI can have much more generous timeouts, as its executed over SSH.
	if ( is_cli() ) {
		$hash_limit = HASH_LIMIT_CLI; // 1 GB
	} else {
		$hash_limit = HASH_LIMIT; // 200 MB
	}

	if ( substr( $root, -1 ) != '/' ) {
		$root .= '/';
	}

	while ( count( $paths ) > 0 && ( $first_path || time() < $soft_time_limit ) ) {
		$relative_path = array_shift( $paths );
		$absolute_path = $root . $relative_path;

		// Fetch information about the path, prepare a header for it.
		$path_details = get_ls_entry( $args, dirname( $absolute_path ), locale_safe_basename( $absolute_path ) );
		$path_details['ls'] = clean_pathname_string( $relative_path );
		$path_header = encode_json_with_check( $path_details ) . "\n";

		$dh = opendir( $absolute_path );
		if ( ! $dh ) {
			echo $path_header . json_encode( array( 'error' => 'Failed to read ' . $absolute_path ) ) . "\n";
			continue;
		}

		// Sort files for pagination
		$files = array();
		while ( false !== ( $file = readdir( $dh ) ) ) {
			if ( '.' === $file || '..' === $file ) {
				continue;
			}

			array_push( $files, $file );
		}

		sort($files);
		closedir( $dh );

		// Apply limits to first directory only, additional directories will be limited by soft_limits
		// Default limit of 0 lists all files
		if ( $first_path ) {
			$start = intval( $args['offset'] );
			if ( 0 === intval( $args['limit'] ) ) {
				$end = count( $files );
			} else {
				$end = min( $start + intval( $args['limit'] ), count( $files ) );
			}
		} else {
			$start = 0;
			$end = count( $files );
		}

		// Send a header for the first path after successfully reading an entry.
		// Don't send if we're not at the start of pagination
		// Prevents masking errors behind successful-looking headers.
		if ( $first_path && 0 === $start && ! empty( $path_header ) ) {
			echo $path_header;
			$path_header = '';
		}

		for ( $i = $start; $i < $end; $i++ ) {
			$file = $files[ $i ];

			// Figure out if this file should not be hashed (ie; if it's too big).
			$size = filesize( $absolute_path . '/' . $file );

			$skip_hash = should_skip_hash( $skip_large_hashes, $size, $hash_limit );

			if ( ! $skip_hash && $hash_limit > 0 ) {
				$hash_limit -= $size;
			}

			// Apply soft-limits on all but the first path (including bailing if we hit the hash limit)
			$entries++;
			if ( ! $first_path && ( $hash_limit < 0 || $entries > $soft_limit || time() > $soft_time_limit ) ) {
				closedir( $dh );
				$entry_buffer = array();
				flush();
				return;
			}

			$force_full_hashes = false; // // walker never force full hashes for large files
			$entry = get_ls_entry( $args, $absolute_path, $file, $skip_hash, $force_full_hashes );

			if ( $skip_hash && ! empty( $args[ 'hashes' ] ) && empty( $entry['unchanged' ] ) ) {
				$entry['hash_skipped'] = 1;
			}

			// Keep track of paths to auto-recurse into
			// not symlinks or unreadable dirs
			$is_readable = ! ( isset( $entry['unreadable'] ) && $entry['unreadable'] );
			if ( ( isset( $entry['is_dir'] ) && $entry['is_dir'] ) && $is_readable && ( isset( $entry['is_link'] ) && ! $entry['is_link'] ) && count( $paths ) < 1000 && $entries < $soft_limit ) {
				// Do not track into .donotbackup folders.
				if ( empty( $entry['do_not_backup'] ) ) {
					$explore_path = empty( $relative_path ) ? $file : $relative_path . '/' . $file;
					if ( ! in_array( $explore_path, $paths ) ) {
						array_push( $paths, $explore_path );
					}
				}
			}

			// Buffer all but the first path, to help enforce soft-limits
			if ( $first_path ) {
				send_json_with_check( $entry, true, false );
			} else{
				array_push( $entry_buffer, $entry );
			}
		}

		// If buffering, output entry buffer; finished a directory before hitting a soft-limit.
		if ( ! $first_path ) {
			echo $path_header;
			foreach ( $entry_buffer as $entry ) {
				send_json_with_check( $entry, true, false );
			}
		}

		// End of directory or just end of batch?
		$end_of_batch = false;
		if ( $end === count( $files ) ) {
			send_json_with_check( [ 'eod' => true ], true, false );
		} else {
			$end_of_batch = true;
			send_json_with_check( [ 'next' => $end ], true, false );
		}

		// Send a footer at the bottom of each directory to confirm it is complete.
		flush();

		$first_path = false;
		$entry_buffer = array();

		if ( $entries > $soft_limit || $end_of_batch ) {
			exit;
		}
	}
}

function action_cleanup_restore( $args ) {
	$files   = glob( localize_path( '{$ABSPATH}/vp-sql-upload-*.sql' ) );
	$deleted = 0;

	foreach ( $files as $file ) {
		if ( @unlink( $file ) ) {
			$deleted++;
		}
	}

	success_header();
	echo json_encode( array(
		'found'   => count( $files ),
		'deleted' => $deleted,
	) );
}

function action_cleanup_helpers( $args ) {
	$args = array_merge( array(
		'ageThreshold' => 21600, // 6 hours
	), $args );

	$dir = opendir( __DIR__ );
	if ( ! is_resource( $dir ) ) {
		fatal_error( READ_ERROR, 'Failed to open directory: ' . __DIR__ );
	}

	$self = realpath( __FILE__ );

	// Find leftover old helpers and delete them.
	$helpers_deleted = 0;
	$helpers_found = 0;
	while ( false !== ( $entry = readdir( $dir ) ) ) {
		// Skip files that don't look like helpers.
		if ( 0 != strncmp( $entry, 'jp-helper-', 10 ) ) {
			continue;
		}

		$helpers_found++;
		$full_path = realpath( implode( '/', array( __DIR__, $entry ) ) );

		// Skip entries that aren't files, or are myself.
		if ( $full_path == $self || ! is_file( $full_path ) ) {
			continue;
		}

		// Only delete helpers over the threshold
		$age = time() - filemtime( $full_path );
		if ( $age < $args['ageThreshold'] ) {
			continue;
		}

		// Check file header
		if ( file_get_contents( $full_path, false, NULL, 0, 40 ) !== '<?php /* Jetpack Backup Helper Script */' ) {
			continue;
		}

		// Finally delete.
		$helpers_deleted++;
		unlink( $full_path );
	}

	success_header();
	echo json_encode( array(
		'found'   => $helpers_found,
		'deleted' => $helpers_deleted,
	 ) );
}

function action_get_active_theme( $args ) {
	if ( ! empty( $args['load_full_wp'] ) ) {
		load_wp( true );
	} else {
		load_wp();
	}

	$theme = wp_get_theme();

	if ( $theme ) {
		success_header();
		echo json_encode( array(
			'slug' => $theme->get_template(),
			'path' => $theme->get_theme_root(),
		) );
	} else {
		fatal_error( READ_ERROR, 'wp_get_theme() failed' );
	}
}

function action_validate_theme() {
	// Forces a theme switch if necessary (we may have deleted active theme).
	load_wp( true );
	$theme_validated = validate_current_theme();

	success_header();
	send_json_with_check( array(
		'theme_validated' => $theme_validated,
	) );
	exit;
}

function action_woocommerce_install() {
	load_wp( true );

	global $wpdb;

	success_header();

	if ( class_exists( 'WC_Install' ) && method_exists( 'WC_Install', 'install' ) ) {
		$sql = sprintf( '
			ALTER TABLE `vp_backup_%swc_download_log`
			DROP FOREIGN KEY `fk_%swc_download_log_permission_id`',
			$wpdb->prefix,
			$wpdb->prefix
		);

		$wpdb->query( $sql );

		WC_Install::install();

		echo json_encode( array( 'installed' => true, 'sql' => $sql ) );
	} else {
		echo json_encode( array( 'installed' => false, 'message' => 'woocommerce not available' ) );
	}
}

function action_get_file( $args ) {
	$args = array_merge( array(
		'path'              => null,
		'previous_attempts' => [],
	), $args );

	if ( empty( $args['path'] ) ) {
		fatal_error( COMMS_ERROR, 'Invalid args', 400 );
	}

	$path = localize_path( base64_decode( $args['path'] ) );

	if ( ! file_exists( $path ) ) {
		fatal_error( NOT_FOUND_ERROR, 'File not found: ' . $args['path'] );
	}

	$handle = fopen( $path, 'r' );
	if ( ! $handle ) {
		fatal_error( READ_ERROR, 'Unable to open file' );
	}

	// Fast forward past previous attempts, checking their hashes match.
	foreach ( $args['previous_attempts'] as $attempt ) {
		fast_forward_handle( $handle, intval( $attempt->size ), $attempt->hash );
	}

	$filesize = filesize( $path );

	success_header();
	header( 'Content-Length: ' . ( $filesize - ftell( $handle ) + 1 ) );
	header( 'x-file-size: ' . $filesize );

	// Send an extra prepended byte to avoid WAFs/reverse proxies from treating the HTTP body as a truthy value.
	echo 'b';

	$result = pass_through( $handle );
	if ( false === $result ) {
		fatal_error( READ_ERROR, 'File read error' );
	}

	exit;
}

function action_enable_jetpack_sso( $args ) {
	load_wp( true );

	if ( ! \Jetpack::is_module_active( 'sso' ) ) {
		\Jetpack::activate_module( 'sso', false, false );
		if ( ! \Jetpack::is_module_active( 'sso' ) ) {
			fatal_error( WRITE_ERROR, 'Failed to activate Jetpack SSO module.' );
		}
	}

	if ( intval( get_option( 'jetpack_sso_match_by_email' ) ) !== 1 ) {
		if ( ! update_option( 'jetpack_sso_match_by_email', 1 ) ) {
			fatal_error( WRITE_ERROR, 'Failed to set Jetpack SSO to match by email.' );
		}
	}

	success_header();
	echo json_encode( [
		'ok' => true,
	] );
}

function action_transfer_jetpack_connection( $args ) {
	global $wpdb;

	// Ensure required args have been specified
	if ( empty( $args['imported_prefix'] ) || empty( $args['master_user_id'] ) ) {
		fatal_error( COMMS_ERROR, 'Imported prefix and new master_user_id required to transfer Jetpack connection' );
	}
	$imported_prefix = $args['imported_prefix'];
	$new_master_user_id = intval( $args['master_user_id'] );

	if ( ! empty( $args['load_full_wp'] ) ) {
		load_wp( true );
	} else {
		load_wp();
	}

	// Verify specified master_user_id is present and an administrator.
	$get_roles_query = $wpdb->prepare( "select meta_value from `{$imported_prefix}usermeta` where user_id = %d and meta_key like %s limit 1;", $new_master_user_id, '%capabilities' );
	$new_master_roles_serialized = $wpdb->get_var( $get_roles_query );
	if ( empty( $new_master_roles_serialized ) ) {
		fatal_error( NOT_FOUND_ERROR, 'Unable to determine roles for new master user' );
	}

	$new_master_roles = maybe_unserialize( $new_master_roles_serialized );
	if ( ! is_array( $new_master_roles ) || ! in_array( 'administrator', array_keys( $new_master_roles ) ) ) {
		fatal_error( COMMS_ERROR, 'Specified master_user_id does not have the administrator role' );
	}

	// Gather current Jetpack settings together for import.
	$jetpack_options = get_option( 'jetpack_options' );
	$jetpack_private_options = get_option( 'jetpack_private_options' );
	if ( ! is_array( $jetpack_options ) || ! is_array( $jetpack_private_options ) || empty( $jetpack_options['master_user'] ) ) {
		fatal_error( NOT_FOUND_ERROR, 'Jetpack Options not found for connection transfer' );
	}
	$old_master_user_id = intval( $jetpack_options['master_user'] );

	// Modify Jetpack Options to reflect new master_user_id.
	if ( $old_master_user_id !== $new_master_user_id ) {
		// Update master user in Jetpack Options and user tokens, if they have changed.
		$jetpack_options['master_user'] = $new_master_user_id;

		// Replace the master user id in the tokens array.
		if ( empty( $jetpack_private_options['user_tokens'][ $old_master_user_id ] ) ) {
			fatal_error( NOT_FOUND_ERROR, 'Master User tokens not found in Jetpack Private Options' );
		}

		$old_token = $jetpack_private_options['user_tokens'][ $old_master_user_id ];
		$new_token = replace_user_id_in_user_token( $old_token, $new_master_user_id );
		$jetpack_private_options['user_tokens'] = [ $new_master_user_id => $new_token ];
	}

	// Write options to the newly imported tables.
	$wpdb->update( $imported_prefix . 'options', [ 'option_value' => serialize( $jetpack_options ) ], [ 'option_name' => 'jetpack_options' ] );
	$wpdb->update( $imported_prefix . 'options', [ 'option_value' => serialize( $jetpack_private_options ) ], [ 'option_name' => 'jetpack_private_options' ] );

	success_header();
	echo json_encode( [
		'old_master_user_id' => $old_master_user_id,
		'new_master_user_id' => $new_master_user_id,
	] );
}

function fast_forward_handle( $handle, $size, $md5 ) {
	$ctx = hash_init( 'md5' );
	$read = 0;

	while ( $read < $size ) {
		$data = fread( $handle, min( 2048, $size - $read ) );
		if ( false == $data ) {
			fatal_error( READ_ERROR, 'Unable to read to fast forward point' );
		}

		$read += strlen( $data );
		hash_update( $ctx, $data );
	}

	$hash = hash_final( $ctx );
	if ( $hash !== $md5 ) {
		fatal_error( READ_ERROR, 'Hash mis-match while fast-forwarding file handle' );
	}
}

function pass_through( $handle ) {
	while ( ! feof( $handle ) ) {
		$data = fread( $handle, 2048 );
		if ( false === $data ) {
			return false;
		}

		print $data;
	}

	return true;
}

function replace_user_id_in_user_token( $user_token, $new_user_id ) {
	$user_token_without_suffix = strip_user_id_from_user_token( $user_token );
	return add_user_id_to_user_token( $user_token_without_suffix, $new_user_id  );
}

function strip_user_id_from_user_token( $user_token ) {
	$pos_of_last_period = strrpos( $user_token, '.' );
	return substr( $user_token, 0, $pos_of_last_period );
}

function add_user_id_to_user_token( $user_token_without_suffix, $user_id ) {
	return $user_token_without_suffix . '.' . $user_id;
}

function get_plugin_path_from_slug( $slug ) {
	if ( ! function_exists( 'get_plugins' ) ) {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';
	}

	$plugins = get_plugins();

	if ( strstr( $slug, '/' ) ) {
		// The slug is already a plugin path.
		return $slug;
	}

	foreach ( $plugins as $plugin_path => $data ) {
		$path_parts = explode( '/', $plugin_path );
		if ( $path_parts[0] === $slug ) {
			return $plugin_path;
		}
	}

	return false;
}


/**
 * Wrappers for various PHP string functions that ensure that PHP will treat strings as bytes and not as characters.
 *
 * PHP might have "mbstring.func_overload" enabled, in which case string functions will treat strings as arrays of
 * (potentially multibyte) characters and not bytes, e.g.
 *
 *     strlen('😀') === 1
 *
 * The wrapper functions in the class ensure that parameter strings will always be treated as bytes:
 *
 *     DPC_Bytes::strlen('😀') === 4
 *
 * The class also normalizes some differences between PHP 5.x-7.x and 8.x.
 *
 * Given that static method names are similar to the ones used by PHP, they're prefixed with "b_" to not erroneously
 * trigger various security scanners.
 */
class DPC_Bytes {

	/**
	 * If true, "mbstring.func_overload" is enabled. If null, "mbstring.func_overload" hasn't been checked yet.
	 *
	 * @var boolean|null
	 */
	static $cached_mbstring_func_overload_enabled = null;

	/**
	 * Check if "mbstring.func_overload" is enabled.
	 *
	 * @return bool True if "mbstring.func_overload" is enabled.
	 */
	private static function mbstring_func_overload_is_enabled() {
		if ( is_null( static::$cached_mbstring_func_overload_enabled ) ) {
			static::$cached_mbstring_func_overload_enabled = false;

			if ( function_exists( 'mb_substr' ) ) {
				if ( ( (int) ini_get( 'mbstring.func_overload' ) ) > 0 ) {
					static::$cached_mbstring_func_overload_enabled = true;
				}
			}
		}

		return static::$cached_mbstring_func_overload_enabled;
	}

	/**
	 * Get string length (in bytes).
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.strlen
	 * @see https://www.php.net/manual/en/function.strlen.php
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.mb-strlen
	 * @see https://www.php.net/manual/en/function.mb-strlen.php
	 *
	 * @param string $string String being measured for length.
	 *
	 * @return int Length of the string.
	 * @throws Exception On invalid parameters.
	 */
	public static function b_strlen( $string ) {

		// Passing nulls here is deprecated in recent versions of PHP, and a null here is 99% an error.
		if ( is_null( $string ) ) {
			throw new Exception( 'Input string is null' );
		}

		if ( static::mbstring_func_overload_is_enabled() ) {
			$size = mb_strlen( $string, '8bit' );
		} else {
			$size = strlen( $string );
		}

		return $size;
	}

	/**
	 * Return part of a string (in bytes).
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.substr
	 * @see https://www.php.net/manual/en/function.substr.php
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.mb-substr
	 * @see https://www.php.net/manual/en/function.mb-substr.php
	 *
	 * @param string   $string Input string.
	 * @param int      $offset Offset.
	 * @param int|null $length Length.
	 *
	 * @return string Extracted part of string.
	 * @throws Exception On invalid parameters.
	 */
	public static function b_substr( $string, $offset, $length = null ) {

		// Passing nulls here is deprecated in recent versions of PHP, and a null here is 99% an error.
		if ( is_null( $string ) ) {
			throw new Exception( 'Input string is null' );
		}
		if ( is_null( $offset ) ) {
			throw new Exception( 'Input offset is null' );
		}

		// Behavior of substr() differs between PHP 5.x-7.x and PHP 8.x:
		//
		// * PHP 5.x-7.x: "If length is given and is 0, FALSE or NULL, an empty string will be returned";
		// * PHP 8.x: "If length is omitted or null, the substring starting from offset until the end of the string
		//   will be returned".
		//
		// Therefore, always calculate and provide length to have the unified, PHP 8.x-like behavior.
		if ( is_null( $length ) ) {
			$length = static::b_strlen( $string ) - $offset;
		}

		if ( static::mbstring_func_overload_is_enabled() ) {
			$substring = mb_substr( $string, $offset, $length, '8bit' );
		} else {
			$substring = substr( $string, $offset, $length );
		}

		return $substring;
	}

	/**
	 * Find the numeric position of the first occurrence of needle in the haystack string (in bytes).
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.strpos
	 * @see https://www.php.net/manual/en/function.strpos.php
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.mb-strpos
	 * @see https://www.php.net/manual/en/function.mb-strpos.php
	 *
	 * @param string $haystack String to search in.
	 * @param string $needle   String to search for.
	 * @param int    $offset   If specified, search will start this number of characters counted from the beginning of
	 *                         the string.
	 *
	 * @return int|false Position of where the needle exists, or false if the needle was not found.
	 * @throws Exception On invalid parameters.
	 */
	public static function b_strpos( $haystack, $needle, $offset = 0 ) {

		// Discourage deprecated pre-PHP 7.3 behavior.
		if ( ! is_string( $needle ) ) {
			throw new Exception( 'Needle is not a string: ' . print_r( $needle, true ) );
		}

		if ( static::mbstring_func_overload_is_enabled() ) {
			$position = mb_strpos( $haystack, $needle, $offset, '8bit' );
		} else {
			$position = strpos( $haystack, $needle, $offset );
		}

		return $position;
	}
}


/**
 * Encode an array / object to JSON, add some human-readable indenting.
 *
 * @param mixed $value Value that is being encoded.
 *
 * @return string|false JSON representation of the value, or false if the encoding has failed.
 */
function dpc_json_encode_pretty( $value ) {
	$json = json_encode( $value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
	if ( false === $json ) {
		return false;
	}

	return $json . PHP_EOL;
}


/**
 * Signature ("magic bytes").
 *
 * Those will be the first bytes of every chunk that is being sent.
 */
const DPC_DATA_FORMAT_SIGNATURE = 'DPC';

/**
 * Version of data format.
 *
 * If you make changes to the data format, increase the version number on both reader and writer.
 *
 * Can't be bigger than 65535.
 */
const DPC_DATA_FORMAT_VERSION = 2;

/**
 * MIME type that the chunks will be sent as.
 *
 * Docs say that since PHP 5.3.4 (I think), "post_max_size = 0 will not disable the limit when the content type is
 * application/x-www-form-urlencoded or is not registered with PHP", so the MIME type has to be something that PHP is
 * aware of (i.e. can't be something fancy like "application/x-a8c-jpb-whatever").
 *
 * @see https://www.php.net/manual/en/ini.core.php#ini.post-max-size
 */
const DPC_CHUNK_MIME_TYPE = 'application/octet-stream';

/**
 * A "special" permissions value that means that permissions are not to be set.
 *
 * The max. permissions value will be 0777 (octal) / 511 (decimal), so this value has to be bigger than that but still
 * fit into the unsigned short.
 *
 * All pack()s signed types are of a machine byte order, so we can't switch to a signed type and use something like -1
 * for the value of this constant.
 */
const DPC_PERMISSIONS_UNSET = 65000;

/**
 * Maximum size (in bytes) that the Metadata could be.
 *
 * Should be able to accommodate directory that is being written to.
 */
const DPC_METADATA_JSON_MAX_SIZE = 128 * 1024;

/**
 * Maximum size (in bytes) that the State could be.
 *
 * Should be able to accommodate MAX_PATH_SIZE, MAX_SYMLINK_PATH_SIZE, and other partial data that might be
 * contained in the state.
 */
const DPC_STATE_JSON_MAX_SIZE = 128 * 1024;


/**
 * Integer entry type representations.
 */
class DPC_Supported_Entry_Types {
	const FILE      = 1;
	const DIRECTORY = 2;
	const SYMLINK   = 3;
}


/**
 * Interface that configures how various elements of the stream get encoded.
 */
interface DPC_Pack_Type {

	/**
	 * Return format argument to pack() / unpack() that will be used to encode / decode a value.
	 *
	 * @see https://www.php.net/manual/en/function.pack.php
	 *
	 * @return string format argument to pack() / unpack() to be used to encode / decode a value.
	 */
	public static function pack_argument();

	/**
	 * Return size of the encoded (pack()'ed) value in bytes.
	 *
	 * It's Bytes::strlen( pack( static::pack_argument(), ... ) ).
	 *
	 * @see https://www.php.net/manual/en/function.pack.php
	 *
	 * @return int Size of the encoded (pack()'ed) value in bytes.
	 */
	public static function byte_size();

}


/**
 * unsigned char
 */
class DPC_Unsigned_Char_Pack_Type implements DPC_Pack_Type {

	public static function pack_argument() {
		return 'C';
	}

	public static function byte_size() {
		return 1;
	}
}

/**
 * unsigned short (always 16 bit, little endian byte order)
 */
class DPC_Unsigned_Short_LE_Pack_Type implements DPC_Pack_Type {

	public static function pack_argument() {
		return 'v';
	}

	public static function byte_size() {
		return 2;
	}
}

/**
 * unsigned long (always 32 bit, little endian byte order)
 */
class DPC_Unsigned_Long_LE_Pack_Type implements DPC_Pack_Type {

	public static function pack_argument() {
		return 'V';
	}

	public static function byte_size() {
		return 4;
	}
}

/**
 * unsigned long long (always 64 bit, little endian byte order)
 */
class DPC_Unsigned_Long_Long_LE_Pack_Type implements DPC_Pack_Type {

	public static function pack_argument() {
		return 'P';
	}

	static function byte_size() {
		return 8;
	}
}


/**
 * Encoding configuration for Version.
 */
class DPC_Data_Format_Version_Pack_Type extends DPC_Unsigned_Short_LE_Pack_Type {
}

/**
 * Encoding configuration for Metadata JSON size.
 */
class DPC_Metadata_JSON_Size_Pack_Type extends DPC_Unsigned_Long_LE_Pack_Type {
}

/**
 * Encoding configuration for State JSON size.
 */
class DPC_State_JSON_Size_Pack_Type extends DPC_Unsigned_Long_LE_Pack_Type {
}

/**
 * Encoding configuration for Entry Type.
 */
class DPC_Entry_Type_Pack_Type extends DPC_Unsigned_Char_Pack_Type {
}

/**
 * Encoding configuration for Entry Path size.
 */
class DPC_Entry_Path_Size_Pack_Type extends DPC_Unsigned_Long_LE_Pack_Type {
}

/**
 * Encoding configuration for Entry Permissions.
 */
class DPC_Entry_Permissions_Pack_Type extends DPC_Unsigned_Short_LE_Pack_Type {
}

/**
 * Encoding configuration for Total file size when the Entry is a File.
 */
class DPC_Entry_File_Total_Size_Pack_Type extends DPC_Unsigned_Long_Long_LE_Pack_Type {
}

/**
 * Encoding configuration for Total file written so far size when the Entry is a File.
 */
class DPC_Entry_File_Total_Written_So_Far_Size_Pack_Type extends DPC_Unsigned_Long_Long_LE_Pack_Type {
}

/**
 * Encoding configuration for File part size when the Entry is a File.
 */
class DPC_Entry_File_Part_Size_Pack_Type extends DPC_Unsigned_Long_Long_LE_Pack_Type {
}

/**
 * Encoding configuration for Symlink pack size when the Entry is a Symlink.
 */
class DPC_Entry_Symlink_Path_Size_Pack_Type extends DPC_Unsigned_Long_LE_Pack_Type {
}



/**
 * Wrappers for functions which throw an exception on errors and warnings instead of silently continuing operation.
 *
 * PHP is pretty lax with error reporting when doing I/O operations, e.g. a typical I/O helper function returns false,
 * null, and/or emits a warning, but the whole PHP application continues operation. It's for the caller of the I/O
 * helper function to test the returned result of the function, and notice + act upon I/O errors.
 *
 * That approach isn't ideal for the DPC parsing needs, as we really want to know about each and every I/O error, and
 * either stop parsing the incoming DPC stream altogether, or try-catch the error and recover from it gracefully.
 *
 * Therefore, this class provides wrappers for some common (mostly) I/O operations that throw an exception on errors
 * instead of just returning false, null, or something else. The thrown exception then either gets caught by the
 * exception handler (try-catch block), and the error then gets sent back to the Sender as an HTTP 4xx/5xx response.
 * This wrapper class treats warnings as errors too.
 *
 * Some parameters between PHP 5.x and PHP 7.x/8.x functions are incompatible in some weird way, so not all parameters
 * are added to every wrapper function. Keep that in mind if you're editing this class.
 *
 * Given that static method names are similar to the ones used by PHP, they're prefixed with "t_" to not erroneously
 * trigger various security scanners.
 */
class DPC_Throw_On_Errors {

	/**
	 * Try to determine and return a path to a stream.
	 *
	 * Useful for debugging purposes.
	 *
	 * @param resource $stream Stream to determine the path for.
	 *
	 * @return string Path to the stream, or 'unknown' if the path couldn't be determined.
	 */
	private static function stream_file_path( $stream ) {
		$stream_file_path = 'unknown';

		if ( is_resource( $stream ) ) {
			$stream_file_path = stream_get_meta_data( $stream )['uri'];
		}

		return $stream_file_path;
	}

	/**
	 * Execute a callable, throw an exception (together with a descriptive label) on PHP warnings / errors.
	 *
	 * @param callable $callable Callable to execute.
	 * @param string   $label    Label to add to the thrown exception to clarify what was attempted.
	 *
	 * @return mixed Callable's return value, if any.
	 * @throws Exception On warnings thrown by the callable.
	 * @noinspection PhpUnusedParameterInspection
	 */
	private static function throw_on_warnings( $callable, $label ) {
		$old_error_reporting = error_reporting( - 1 );
		$old_display_errors  = ini_set( 'display_errors', 'stderr' );
		set_error_handler(
		/**
		 * Temporary error handler.
		 *
		 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.set-error-handler
		 * @see https://www.php.net/manual/en/function.set-error-handler.php
		 *
		 * @param int         $errno      Level of the error raised.
		 * @param string      $errstr     Error message.
		 * @param string|null $errfile    Filename that the error was raised in.
		 * @param int|null    $errline    Line number where the error was raised.
		 * @param array|null  $errcontext Deprecated, unused.
		 *
		 * @return mixed
		 * @throws Exception
		 */
			function ( $errno, $errstr, $errfile = null, $errline = null, $errcontext = null ) {
				throw new Exception( "$errstr (file: $errfile; line: $errline)" );
			}
		);

		$result        = null;
		$error_message = null;
		if ( PHP_MAJOR_VERSION >= 7 ) {
			// On PHP 7.x, all Exception and Error are subclasses of Throwable.
			try {
				$result = $callable();
			} catch ( Throwable $throwable ) {
				$error_message = $throwable->getMessage();
			}
		} else {
			// On PHP 5.x, there's only Exception.
			try {
				$result = $callable();
			} catch ( Exception $exception ) {
				$error_message = $exception->getMessage();
			}
		}

		restore_error_handler();
		ini_set( 'display_errors', $old_display_errors );
		error_reporting( $old_error_reporting );

		if ( ! is_null( $error_message ) ) {
			throw new Exception( "$label failed: $error_message" );
		}

		return $result;
	}

	/**
	 * Create a temporary file, throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.tmpfile
	 * @see https://www.php.net/manual/en/function.tmpfile.php
	 *
	 * @return resource File handle to the temporary file.
	 * @throws Exception When tmpfile() has thrown warnings or has failed.
	 */
	public static function t_tmpfile() {
		$label            = 'tmpfile()';
		$temp_file_handle = static::throw_on_warnings(
			function () {
				return tmpfile();
			},
			$label
		);

		if ( false === $temp_file_handle ) {
			throw new Exception( "Unable to $label" );
		}

		return $temp_file_handle;
	}

	/**
	 * Decode data encoded with MIME base64, throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.base64-decode
	 * @see https://www.php.net/manual/en/function.base64-decode.php
	 *
	 * @param string $string Encoded data.
	 *
	 * @return string Decoded data.
	 * @throws Exception If base64_decode() failed, has thrown warnings, or on invalid parameters.
	 */
	public static function t_base64_decode( $string ) {

		// PHP 5.x will happily "decode" null input without complaining, so let's check it here.
		if ( is_null( $string ) ) {
			throw new Exception( 'Base64-encoded string is null' );
		}

		$label = 'base64_decode( ' . DPC_Bytes::b_strlen( $string ) . ' bytes )';

		$base64_encoded_data = static::throw_on_warnings(
			function () use ( $string ) {
				return base64_decode( $string );
			},
			$label
		);

		if ( false === $base64_encoded_data ) {
			// Typically it's the input data that we're trying to decode.
			throw new Exception( "Unable to $label" );
		}

		return $base64_encoded_data;
	}

	/**
	 * Check whether a file or directory exists, throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.file-exists
	 * @see https://www.php.net/manual/en/function.file-exists.php
	 *
	 * @param string $filename Path to the file or directory.
	 *
	 * @return bool True if the file or directory specified by filename exists; false otherwise.
	 * @throws Exception On invalid parameters, or if file_exists() has thrown warnings.
	 */
	public static function t_file_exists( $filename ) {
		// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
		if ( ! $filename ) {
			throw new Exception( 'Filename for file_exists() is unset' );
		}

		return static::throw_on_warnings(
			function () use ( $filename ) {
				return file_exists( $filename );
			},
			"file_exists( '$filename' )"
		);
	}

	/**
	 * Determine whether the filename is a symbolic link, throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.is-link
	 * @see https://www.php.net/manual/en/function.is-link.php
	 *
	 * @param string $filename Path to the file.
	 *
	 * @return bool Returns true if the filename exists and is a symbolic link, false otherwise.
	 * @throws Exception On invalid parameters, or if is_link() has thrown warnings.
	 */
	public static function t_is_link( $filename ) {
		// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
		if ( ! $filename ) {
			throw new Exception( 'Filename for is_link() is unset' );
		}

		return static::throw_on_warnings(
			function () use ( $filename ) {
				return is_link( $filename );
			},
			"is_link( '$filename' )"
		);
	}

	/**
	 * Get file size, throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.filesize
	 * @see https://www.php.net/manual/en/function.filesize.php
	 *
	 * @param string $filename Path to the file.
	 *
	 * @return int Size of the file in bytes
	 * @throws Exception On invalid parameters, or if filesize() has thrown warnings.
	 */
	public static function t_filesize( $filename ) {
		// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
		if ( ! $filename ) {
			throw new Exception( 'Filename for filesize() is unset' );
		}

		return static::throw_on_warnings(
			function () use ( $filename ) {
				return filesize( $filename );
			},
			"filesize( '$filename' )"
		);
	}

	/**
	 * Get file permissions, throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.fileperms
	 * @see https://www.php.net/manual/en/function.fileperms.php
	 *
	 * @param string $filename Path to the file.
	 *
	 * @return int File's permissions as a numeric mode.
	 * @throws Exception On invalid parameters, or if fileperms() has thrown warnings, or has failed.
	 */
	public static function t_fileperms( $filename ) {
		// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
		if ( ! $filename ) {
			throw new Exception( 'Filename for fileperms() is unset' );
		}

		$label = "fileperms( '$filename' )";

		$fileperms_result = static::throw_on_warnings(
			function () use ( $filename ) {
				return fileperms( $filename );
			},
			$label
		);

		if ( false === $fileperms_result ) {
			throw new Exception( "Unable to $label" );
		}

		return $fileperms_result;
	}

	/**
	 * Gets file owner, throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.fileowner
	 * @see https://www.php.net/manual/en/function.fileowner.php
	 *
	 * @param string $filename Path to the file.
	 *
	 * @return int User ID of the owner of the file.
	 * @throws Exception On invalid parameters, if fileowner() has thrown warnings, or has failed.
	 */
	public static function t_fileowner( $filename ) {
		// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
		if ( ! $filename ) {
			throw new Exception( 'Filename for fileowner() is unset' );
		}

		$label = "fileowner( '$filename' )";

		$owner = static::throw_on_warnings(
			function () use ( $filename ) {
				return fileowner( $filename );
			},
			$label
		);

		if ( false === $owner ) {
			throw new Exception( "Unable to $label" );
		}

		return $owner;
	}

	/**
	 * Get file group, throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.filegroup
	 * @see https://www.php.net/manual/en/function.filegroup.php
	 *
	 * @param string $filename Path to the file.
	 *
	 * @return int Group ID of the owner of the file.
	 * @throws Exception On invalid parameters, if filegroup() has thrown warnings, or has failed.
	 */
	public static function t_filegroup( $filename ) {
		// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
		if ( ! $filename ) {
			throw new Exception( 'Filename for filegroup() is unset' );
		}

		$label = "filegroup( '$filename' )";

		$group = static::throw_on_warnings(
			function () use ( $filename ) {
				return filegroup( $filename );
			},
			$label
		);

		if ( false === $group ) {
			throw new Exception( "Unable to $label" );
		}

		return $group;
	}

	/**
	 * Test for end-of-file on a file pointer, throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.feof
	 * @see https://www.php.net/manual/en/function.feof.php
	 *
	 * @param resource $stream Stream resource.
	 *
	 * @return bool True if the file pointer is at EOF, otherwise false.
	 * @throws Exception If feof() has thrown warnings.
	 */
	public static function t_feof( $stream ) {
		return static::throw_on_warnings(
			function () use ( $stream ) {
				return feof( $stream );
			},
			"feof( resource for '" . static::stream_file_path( $stream ) . "')"
		);
	}

	/**
	 * Open file or URL, throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.fopen
	 * @see https://www.php.net/manual/en/function.fopen.php
	 *
	 * @param string $filename Path to the file / stream.
	 * @param string $mode     Type of access you require to the stream.
	 *
	 * @return resource File pointer resource.
	 * @throws Exception If fopen() has thrown warnings, or has failed.
	 */
	public static function t_fopen( $filename, $mode ) {

		$label = "fopen( '$filename', '$mode' )";

		$handler = static::throw_on_warnings(
			function () use ( $filename, $mode ) {
				return fopen( $filename, $mode );
			},
			$label
		);

		if ( false === $handler ) {
			throw new Exception( "Unable to $label" );
		}

		return $handler;
	}

	/**
	 * Binary-safe file read, throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.fread
	 * @see https://www.php.net/manual/en/function.fread.php
	 *
	 * @param resource $stream File system pointer resource.
	 * @param int      $length Number of bytes to read.
	 *
	 * @return string String that was read.
	 * @throws Exception If fread() has thrown warnings, or has failed.
	 */
	public static function t_fread( $stream, $length ) {

		$label = "fread( resource for '" . static::stream_file_path( $stream ) . "', $length )";

		$data = static::throw_on_warnings(
			function () use ( $stream, $length ) {
				return fread( $stream, $length );
			},
			$label
		);

		if ( false === $data ) {
			throw new Exception( "Unable to $label" );
		}

		return $data;
	}

	/**
	 * Binary-safe file write, throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.fwrite
	 * @see https://www.php.net/manual/en/function.fwrite.php
	 *
	 * @param resource $stream File system pointer resource.
	 * @param string   $data   String that is to be written.
	 *
	 * @return int Number of bytes written.
	 * @throws Exception On invalid parameters, if fwrite() has thrown warnings, or has failed.
	 */
	public static function t_fwrite( $stream, $data ) {
		// PHP 5.x won't complain about data being null, so let's do it ourselves.
		if ( is_null( $data ) ) {
			throw new Exception( 'Data for fwrite() is null' );
		}

		$label = "fwrite( resource for '" . static::stream_file_path( $stream ) . "', "
		         . DPC_Bytes::b_strlen( $data ) . " bytes )";

		$bytes_written = static::throw_on_warnings(
			function () use ( $stream, $data ) {
				return fwrite( $stream, $data );
			},
			$label
		);

		if ( false === $bytes_written ) {
			throw new Exception( "Unable to $label" );
		}

		return $bytes_written;
	}

	/**
	 * Seek on a file pointer, throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.fseek
	 * @see https://www.php.net/manual/en/function.fseek.php
	 *
	 * @param resource $stream File system pointer resource.
	 * @param int      $offset Offset.
	 *
	 * @return void
	 * @throws Exception On invalid parameters, if fseek() has thrown warnings, or has failed.
	 */
	public static function t_fseek( $stream, $offset ) {
		// PHP 5.x won't complain about offset being null, so let's do it ourselves.
		if ( is_null( $offset ) ) {
			throw new Exception( 'Offset for fseek() is null' );
		}

		$label = "fseek( resource for '" . static::stream_file_path( $stream ) . "', $offset )";

		$fseek_result = static::throw_on_warnings(
			function () use ( $stream, $offset ) {
				return fseek( $stream, $offset );
			},
			$label
		);

		if ( 0 !== $fseek_result ) {
			throw new Exception( "Unable to $label" );
		}
	}

	/**
	 * Close an open file pointer, throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.fclose
	 * @see https://www.php.net/manual/en/function.fclose.php
	 *
	 * @param resource $stream File system pointer resource.
	 *
	 * @return void
	 * @throws Exception If fclose() has thrown warnings, or has failed.
	 */
	public static function t_fclose( $stream ) {

		$label = "fclose( resource for '" . static::stream_file_path( $stream ) . "' )";

		$fclose_result = static::throw_on_warnings(
			function () use ( $stream ) {
				return fclose( $stream );
			},
			$label
		);

		if ( true !== $fclose_result ) {
			throw new Exception( "Unable to $label" );
		}
	}

	/**
	 * Create an empty file if it doesn't exist, throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.touch
	 * @see https://www.php.net/manual/en/function.touch.php
	 *
	 * @param string $filename Name of the file that is to be created.
	 *
	 * @return void
	 * @throws Exception On invalid parameters, if touch() has thrown warnings, or has failed.
	 */
	public static function t_touch( $filename ) {
		// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
		if ( ! $filename ) {
			throw new Exception( 'Filename for touch() is unset' );
		}

		$label = "touch( '$filename' )";

		$touch_result = static::throw_on_warnings(
			function () use ( $filename ) {
				return touch( $filename );
			},
			$label
		);

		if ( false === $touch_result ) {
			throw new Exception( "Unable to $label" );
		}
	}

	/**
	 * Change file mode, throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.chmod
	 * @see https://www.php.net/manual/en/function.chmod.php
	 *
	 * @param string $filename    Path to the file.
	 * @param int    $permissions Permissions.
	 *
	 * @return void
	 * @throws Exception On invalid parameters, if chmod() has thrown warnings, or has failed.
	 */
	public static function t_chmod( $filename, $permissions ) {
		// PHP 5.x won't complain about permissions being null, so let's do it ourselves.
		if ( ! $filename ) {
			throw new Exception( 'Filename for unpack() is unset' );
		}
		if ( is_null( $permissions ) ) {
			throw new Exception( 'Permissions for chmod() are null' );
		}

		$label = "chmod( '$filename', 0" . decoct( $permissions ) . ' )';

		$chmod_result = static::throw_on_warnings(
			function () use ( $filename, $permissions ) {
				return chmod( $filename, $permissions );
			},
			$label
		);

		if ( false === $chmod_result ) {
			throw new Exception( "Unable to $label" );
		}
	}

	/**
	 * Tell whether the filename is a regular file (follow symlinks), throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.is-file
	 * @see https://www.php.net/manual/en/function.is-file.php
	 *
	 * @param string $filename Path to the file.
	 *
	 * @return bool True if the filename (or the symlink's target) exists and is a regular file, false otherwise.
	 *
	 * @throws Exception On invalid parameters, if is_file() has thrown warnings, or has failed.
	 */
	public static function t_is_file( $filename ) {
		// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
		if ( ! $filename ) {
			throw new Exception( 'Filename for is_file() is unset' );
		}

		return static::throw_on_warnings(
			function () use ( $filename ) {
				return is_file( $filename );
			},
			"is_file( '$filename' )"
		);
	}

	/**
	 * Tell whether the filename is a directory (follow symlinks), throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.is-dir
	 * @see https://www.php.net/manual/en/function.is-dir.php
	 *
	 * @param string $filename Path to the file.
	 *
	 * @return bool True if the filename (or the symlink's target) exists and is a directory, false otherwise.
	 * @throws Exception On invalid parameters, if is_dir() has thrown warnings, or has failed.
	 */
	public static function t_is_dir( $filename ) {
		// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
		if ( ! $filename ) {
			throw new Exception( 'Filename for is_dir() is unset' );
		}

		return static::throw_on_warnings(
			function () use ( $filename ) {
				return is_dir( $filename );
			},
			"is_dir( '$filename' )"
		);
	}

	/**
	 * Make a directory, throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.mkdir
	 * @see https://www.php.net/manual/en/function.mkdir.php
	 *
	 * @param string $directory   Directory path.
	 * @param int    $permissions Permissions of the newly created directory.
	 * @param bool   $recursive   If true, then any parent directories to the directory specified will also be created,
	 *                            with the same permissions.
	 *
	 * @return void
	 * @throws Exception On invalid parameters, if mkdir() has thrown warnings, or has failed.
	 */
	public static function t_mkdir( $directory, $permissions = 0777, $recursive = false ) {
		// PHP 5.x won't complain about permissions being null, so let's do it ourselves.
		if ( is_null( $permissions ) ) {
			throw new Exception( 'Permissions for mkdir() are unset' );
		}

		$label = "mkdir( '$directory', 0" . decoct( $permissions ) . ', ' . ( $recursive ? 'true' : 'false' ) . ' )';

		$mkdir_result = static::throw_on_warnings(
			function () use ( $directory, $permissions, $recursive ) {
				return mkdir( $directory, $permissions, $recursive );
			},
			$label
		);

		if ( false === $mkdir_result ) {
			throw new Exception( "Unable to $label" );
		}
	}

	/**
	 * Create a symbolic link, throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.symlink
	 * @see https://www.php.net/manual/en/function.symlink.php
	 *
	 * @param string $target Target of the link ("target").
	 * @param string $link   Link name ("source").
	 *
	 * @return void
	 * @throws Exception If symlink() has thrown warnings, or has failed.
	 */
	public static function t_symlink( $target, $link ) {

		$label = "symlink( '$target', '$link' )";

		$symlink_result = static::throw_on_warnings(
			function () use ( $target, $link ) {
				return symlink( $target, $link );
			},
			$label
		);

		if ( false === $symlink_result ) {
			throw new Exception( "Unable to $label" );
		}
	}

	/**
	 * Delete a file, throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.unlink
	 * @see https://www.php.net/manual/en/function.unlink.php
	 *
	 * @param string $filename Path to the file.
	 *
	 * @return void
	 * @throws Exception If unlink() has thrown warnings, or has failed.
	 */
	public static function t_unlink( $filename ) {

		$label = "unlink( '$filename' )";

		$unlink_result = static::throw_on_warnings(
			function () use ( $filename ) {
				return unlink( $filename );
			},
			$label
		);

		if ( false === $unlink_result ) {
			throw new Exception( "Unable to $label" );
		}
	}

	/**
	 * Return the target of a symbolic link, throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.readlink
	 * @see https://www.php.net/manual/en/function.readlink.php
	 *
	 * @param string $path Symbolic link path.
	 *
	 * @return string Contents of the symbolic link path (symlink "target").
	 * @throws Exception If readlink() has thrown warnings, or has failed.
	 */
	public static function t_readlink( $path ) {

		$label = "readlink( '$path' )";

		$readlink_result = static::throw_on_warnings(
			function () use ( $path ) {
				return readlink( $path );
			},
			$label
		);

		if ( false === $readlink_result ) {
			throw new Exception( "Unable to $label" );
		}

		return $readlink_result;
	}

	/**
	 * Decode a non-"null" JSON string, throw on warnings / errors.
	 *
	 * Will throw an error on recoding a string "null" (which is valid JSON).
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.json-decode
	 * @see https://www.php.net/manual/en/function.json-decode.php
	 *
	 * @param string $json        JSON string being decoded.
	 * @param bool   $associative When true, JSON objects will be returned as associative arrays; when false, JSON
	 *                            objects will be returned as objects.
	 *
	 * @return mixed Returns the value encoded in JSON in appropriate PHP type (never null though).
	 * @throws Exception On JSON decoding failures, or if json_decode() has thrown warnings.
	 */
	public static function t_json_decode( $json, $associative = false ) {
		// PHP 5.x won't complain about JSON being null, so let's do it ourselves.
		if ( is_null( $json ) ) {
			throw new Exception( 'JSON for json_decode() is unset' );
		}

		// Invalid JSON might be very long, and/or it might contain binary data, so get just the first X bytes.
		$json_sample_max_length = 1024;
		$json_sample            = DPC_Bytes::b_substr( $json, 0, $json_sample_max_length );
		$json_sample_length     = DPC_Bytes::b_strlen( $json_sample );
		$json_sample_base64     = base64_encode( $json_sample );

		$label = "json_decode( " . DPC_Bytes::b_strlen( $json ) . " bytes; " .
		         "first $json_sample_length bytes: base64_decode( '$json_sample_base64' ) )";

		$decoded_json = static::throw_on_warnings(
			function () use ( $json, $associative ) {
				return json_decode( $json, $associative );
			},
			$label
		);

		// JSON_THROW_ON_ERROR was introduced in PHP 7.3, and we need to be compatible with 5.6, so we'll assume that
		// all "null" values are errors.
		if ( null === $decoded_json ) {
			throw new Exception( "Unable to $label" );
		}

		return $decoded_json;
	}

	/**
	 * Unpack data from binary string, throw on warnings / errors.
	 *
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.unpack
	 * @see https://www.php.net/manual/en/function.unpack.php
	 *
	 * @see https://www.php.net/manual/en/function.pack.php#refsect1-function.pack-parameters
	 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.pack
	 *
	 * @param string $format Format code.
	 * @param string $string Packed data.
	 *
	 * @return array Associative array containing unpacked elements of binary string.
	 * @throws Exception On unpack() has failed, or if unpack() has thrown warnings.
	 */
	public static function t_unpack( $format, $string ) {
		// PHP 5.x won't complain about parameters being unset, so let's do it ourselves.
		if ( ! $format ) {
			throw new Exception( 'Format for unpack() is unset' );
		}
		if ( ! $string ) {
			throw new Exception( 'String for unpack() is unset' );
		}

		$label = "unpack( '$format', base64_decode( '" . base64_encode( $string ) . "' ) )";

		$unpack_result = static::throw_on_warnings(
			function () use ( $format, $string ) {
				return unpack( $format, $string );
			},
			$label
		);

		// PHP 7.x returns false on errors, 5.x doesn't (at least it doesn't promise to return false).
		if ( false === $unpack_result ) {
			throw new Exception( "Unable to $label" );
		}

		return $unpack_result;
	}
}



/**
 * Run unpack(), expect and return only a single value that was unpacked.
 *
 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.unpack
 * @see https://www.php.net/manual/en/function.unpack.php
 *
 * @see https://www.php.net/manual/en/function.pack.php#refsect1-function.pack-parameters
 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.pack
 *
 * @param string $format Format code.
 * @param string $string Packed data.
 * @param string $label  Human-readable label for the data that is being unpacked, to be used in an exception.
 *
 * @return mixed Unpacked value (typically an integer).
 * @throws Exception If unpack() has failed, or if more than one value has been unpacked, or if unpack() has thrown
 *                   warnings.
 */
function dpc_unpack_single_value( $format, $string, $label ) {
	$unpacked = DPC_Throw_On_Errors::t_unpack( $format, $string );
	if ( 1 !== count( $unpacked ) || ( ! array_key_exists( 1, $unpacked ) ) ) {
		// String will contain binary data, so we don't want it to end up in the exception.
		$base64_encoded_string = base64_encode( $string );
		throw new Exception(
			"unpack( '$format', base64_decode( '$base64_encoded_string' ) " .
			"returned more than one value when decoding '$label'"
		);
	}

	return $unpacked[1];
}



/**
 * Test whether a filesystem object exists, and if so, is it a symlink, a file, or a directory.
 *
 * Needed because:
 *
 * * We need to know specifically whether a filesystem object is a file/directory/symlink, and not a, say, block
 *   device, or a socket;
 * * file_exists() / is_file() / is_dir() resolve symlinks and give out information about the symlink's target, which is
 *   error-prone, i.e. we want to find out whether a filesystem object itself is a symlink, and not the type of the
 *   filesystem object that the symlink points to;
 * * file_exists() follows symlinks and returns false for broken symlinks, i.e. the target of which doesn't exist.
 */
class DPC_Filesystem_Object {

	/**
	 * True if the filesystem object exists; it might be a file, directory, symlink, block object, socket, etc.
	 *
	 * @var bool
	 */
	public $exists = true;

	/**
	 * True if the filesystem object is a symlink (broken or not).
	 *
	 * @var bool
	 */
	public $is_link = false;

	/**
	 * True if the filesystem object is a directory.
	 *
	 * @var bool
	 */
	public $is_dir = false;

	/**
	 * True if the filesystem object is a file.
	 *
	 * @var bool
	 */
	public $is_file = false;

	/**
	 * Collect filesystem object state.
	 *
	 * @param string $path Path to a filesystem object.
	 *
	 * @throws Exception On I/O errors.
	 */
	function __construct( $path ) {

		// We could have made some filesystem changes recently, so make sure that we're not reading any cached data.
		clearstatcache();

		if ( DPC_Throw_On_Errors::t_is_link( $path ) ) {
			$this->is_link = true;
		} elseif ( DPC_Throw_On_Errors::t_file_exists( $path ) ) {
			if ( DPC_Throw_On_Errors::t_is_dir( $path ) ) {
				$this->is_dir = true;
			} elseif ( DPC_Throw_On_Errors::t_is_file( $path ) ) {
				$this->is_file = true;
			}
		} else {
			$this->exists = false;
		}
	}
}



/**
 * Exception that includes an HTTP status code to report to the Sender.
 */
class DPC_HTTP_Status_Code_Exception extends Exception {

	/**
	 * HTTP status code, typically 4xx or 5xx.
	 *
	 * @var int
	 */
	public $http_status_code;

	/**
	 * @param string $message          Exception message.
	 * @param int    $http_status_code HTTP status code.
	 */
	public function __construct( $message, $http_status_code ) {
		parent::__construct( $message );
		$this->http_status_code = $http_status_code;
	}

	/**
	 * Return a human-readable string for an exception.
	 *
	 * Useful for debugging.
	 *
	 * @return string Human-readable string for an exception.
	 */
	public function __toString() {
		return __CLASS__ . ': [' . $this->http_status_code . ']: ' . $this->message . PHP_EOL;
	}
}

/**
 * Exception that was caused my incorrect input data being passed to the Receiver (and thus the Sender is "to blame").
 */
class DPC_Invalid_Input_Exception extends DPC_HTTP_Status_Code_Exception {

	/**
	 * @param string $message Exception message.
	 */
	public function __construct( $message ) {
		parent::__construct( $message, 400 );
	}
}

/**
 * Exception that was caused by the Receiver not being able to process valid Chunk data (and thus the Receiver is
 * "to blame").
 */
class DPC_Internal_Error_Exception extends DPC_HTTP_Status_Code_Exception {

	/**
	 * @param string $message Exception message.
	 */
	public function __construct( $message ) {
		parent::__construct( $message, 500 );
	}
}



/**
 * Metadata that came together with the Chunk.
 */
class DPC_Input_Metadata {

	/**
	 * Output path, i.e. directory to write to; must be absolute and end with a "/".
	 *
	 * @var string
	 */
	public $path;

	/**
	 * Constructor.
	 *
	 * @param string $path Output path, i.e. directory to write to; must be absolute and end with a "/".
	 *
	 * @throws DPC_Invalid_Input_Exception|Exception On invalid arguments.
	 */
	public function __construct( $path ) {
		if ( 0 === DPC_Bytes::b_strlen( $path ) ) {
			throw new DPC_Invalid_Input_Exception( 'Output path is unset.' );
		}
		if ( '/' !== DPC_Bytes::b_substr( $path, 0, 1 ) ) {
			throw new DPC_Invalid_Input_Exception( "Output path '$path' must be absolute." );
		}
		if ( '/' !== DPC_Bytes::b_substr( $path, - 1 ) ) {
			throw new DPC_Invalid_Input_Exception( "Output path '$path' must end with a '/'." );
		}

		// FIXME not sure if this is the most secure way to do it.
		if ( false !== DPC_Bytes::b_strpos( $path, '../' ) ) {
			throw new DPC_Invalid_Input_Exception( "Output path '$path' can't have relative parts" );
		}

		$this->path = $path;
	}

	/**
	 * Create Metadata object from decoded JSON array coming in from Chunk.
	 *
	 * @param array $array Decoded JSON array coming in from Chunk.
	 *
	 * @return DPC_Input_Metadata Metadata object.
	 * @throws DPC_Invalid_Input_Exception|Exception On missing required keys, unsupported extra keys in parameter
	 *                                               array, or invalid arguments.
	 */
	public static function from_array( $array ) {

		$path_key      = 'path-urlencoded';
		$required_keys = array( $path_key );
		$actual_keys   = array_keys( $array );

		// Complain if not all required keys have been found.
		$missing_required_keys = array_diff( $required_keys, $actual_keys );
		if ( ! empty( $missing_required_keys ) ) {
			throw new DPC_Invalid_Input_Exception(
				'Some required keys were not found in Metadata array: ' .
				implode( ', ', $missing_required_keys ) . '; ' .
				'Metadata array: ' . print_r( $array, true )
			);
		}

		// If some extra keys have been added, but they're not supported by this class, it's better to fail early and
		// let the caller know.
		$supported_keys         = $required_keys;
		$extra_unsupported_keys = array_diff( $actual_keys, $supported_keys );
		if ( ! empty( $extra_unsupported_keys ) ) {
			throw new DPC_Invalid_Input_Exception(
				'Extra unsupported keys were found in Metadata array: ' .
				implode( ', ', $extra_unsupported_keys ) . '; ' .
				'Metadata array: ' . print_r( $array, true )
			);
		}

		return new static( rawurldecode( $array[ $path_key ] ) );
	}

}



/**
 * Object that gets passed around between the states, and holds data about the Entry that has been read so far.
 */
class DPC_Entry_Data_Read_So_Far {

	/**
	 * Entry Type, or null if Type hasn't been fully read yet.
	 *
	 * @see DPC_Supported_Entry_Types
	 * @see DPC_Entry_Type_Read_State
	 *
	 * @var int|null
	 */
	public $entry_type = null;

	/**
	 * Entry Permissions, or null if Permissions haven't been fully read yet.
	 *
	 * @see DPC_Entry_Permissions_Read_State
	 *
	 * @var int|null
	 */
	public $permissions = null;

	/**
	 * Entry Path size, or null if Path size hasn't been fully read yet.
	 *
	 * @see DPC_Entry_Path_Size_Read_State
	 *
	 * @var int|null
	 */
	public $path_size = null;

	/**
	 * Entry Path, or null if Path hasn't been fully read yet.
	 *
	 * @see DPC_Entry_Path_Read_State
	 *
	 * @var string|null
	 */
	public $path = null;

	/**
	 * Entry File total size, or null if File total size hasn't been fully read yet / not present in this particular
	 * Type of Entry.
	 *
	 * @see DPC_Entry_File_Total_Size_Read_State
	 *
	 * @var int|null
	 */
	public $file_total_size = null;

	/**
	 * Entry File total written so far size, or null if File total written so far size hasn't been fully read yet / not
	 * present in this particular Type of Entry.
	 *
	 * @see DPC_Entry_File_Total_Written_So_Far_Size_Read_State
	 *
	 * @var int|null
	 */
	public $file_total_written_so_far_size = null;

	/**
	 * Entry File part size, or null if File part size hasn't been fully read yet / not present in this particular Type
	 * of Entry.
	 *
	 * @see DPC_Entry_File_Part_Size_Read_State
	 *
	 * @var int|null
	 */
	public $file_part_size = null;

	/**
	 * Entry Symlink path size, or null if Symlink path size hasn't been fully read / not present in this particular
	 * Type of Entry.
	 *
	 * @see DPC_Entry_Symlink_Path_Size_Read_State
	 *
	 * @var string|null
	 */
	public $symlink_path_size = null;

	/**
	 * Serialize this object to an array that could be further serialized into JSON.
	 *
	 * @return array<string, int|string|null> Object's array representation.
	 *
	 * @see DPC_Entry_Data_Read_So_Far::from_array()
	 */
	public function to_array() {
		return get_object_vars( $this );
	}

	/**
	 * Unserialize object from an array that was itself unserialized from JSON.
	 *
	 * @param array $array Object's array representation.
	 *
	 * @return DPC_Entry_Data_Read_So_Far Unserialized object.
	 *
	 * @see DPC_Entry_Data_Read_So_Far::to_array()
	 */
	public static function from_array( $array ) {
		$result = new DPC_Entry_Data_Read_So_Far();
		foreach ( $array as $key => $value ) {
			$result->$key = $value;
		}

		return $result;
	}
}



/**
 * Abstract read state that concrete read states should implement.
 */
abstract class DPC_Read_State {

	/**
	 * Data about the Entry that has been read so far, passed from the previous state and amended by the current state.
	 *
	 * @var DPC_Entry_Data_Read_So_Far
	 */
	protected $entry_data_read_so_far;

	/**
	 * Bytes of state's data that have been read so far.
	 *
	 * States that read more than one byte may use this as a buffer.
	 *
	 * @var string
	 */
	protected $bytes_read_so_far;

	/**
	 * Constructor.
	 *
	 * @param DPC_Entry_Data_Read_So_Far $entry_data_read_so_far Data about the Entry that has been read so far.
	 * @param string                     $bytes_read_so_far      Bytes of state's data that have been read so far.
	 *
	 * @throws DPC_Internal_Error_Exception On invalid parameters.
	 */
	public function __construct( $entry_data_read_so_far, $bytes_read_so_far = '' ) {

		if ( ! $entry_data_read_so_far ) {
			throw new DPC_Internal_Error_Exception( 'Object for entry data read so far must be set' );
		}

		$this->entry_data_read_so_far = $entry_data_read_so_far;
		$this->bytes_read_so_far      = $bytes_read_so_far;
	}

	/**
	 * Read the state's data, store/process it, and return next state, or null if the state ran out of data.
	 *
	 * Each concrete state:
	 *
	 * 1. Attempts to read its data, e.g. an Entry Type, from the Chunk handle;
	 * 2. If the data has been read fully:
	 *     1. Processes the data if needed, e.g. creates a directory/a symlink, writes data to a file, etc.;
	 *     2. Returns an instance of next state to run;
	 * 3. If the state has run out of data while reading it:
	 *     1. Stores the data that has been read so far;
	 *     2. Returns null to let the caller know that the state's object needs to be serialized and sent back to the
	 *     Sender.
	 *
	 * @param resource           $chunk_handle Resource, e.g. an opened file, to read the state's data from.
	 * @param DPC_Input_Metadata $metadata     Metadata object.
	 *
	 * @return DPC_Read_State|null An instance of the next state to run if the current state's data has been
	 *                             read/processed fully, *or* null if the current state ran out of data and needs to be
	 *                             serialized + resumed later.
	 * @throws DPC_Invalid_Input_Exception On errors caused by invalid Chunk data where the Sender is "to blame".
	 * @throws DPC_Internal_Error_Exception On errors caused by a processing error where the Receiver is "to blame".
	 * @throws Exception|\Exception On other errors.
	 */
	abstract public function process_and_return_next_state( $chunk_handle, $metadata );

	/**
	 * Return unique state name that this state should be serialized as.
	 *
	 * Will later be used to find the right class for the serialized state.
	 *
	 * @return string Unique state name to be later used to find the right clas for the serialized state.
	 * @throws Exception If concrete class doesn't implement this method.
	 */
	public static function state_name() {

		// PHP 5.6's strict mode doesn't like abstract static methods:
		//
		//     https://stackoverflow.com/questions/41611058/why-does-php-allow-abstract-static-functions
		//
		// so implement a stub.

		throw new Exception( 'Subclasses should implement ' . __FUNCTION__ );
	}

	/**
	 * Serialize this state to an array that could be further serialized into JSON.
	 *
	 * @return array State's array representation.
	 *
	 * @see DPC_Read_State::from_saved_state()
	 */
	public function to_saved_state() {
		return array(
			'entry_data_read_so_far' => $this->entry_data_read_so_far->to_array(),
			'bytes_read_so_far'      => base64_encode( $this->bytes_read_so_far ),
		);
	}

	/**
	 * Unserialize state from an array that was itself unserialized from JSON.
	 *
	 * @param array $saved_state State's array representation.
	 *
	 * @return DPC_Read_State Unserialized state.
	 * @throws DPC_Internal_Error_Exception|Exception On errors while unserializing the state.
	 *
	 * @see DPC_Read_State::to_saved_state()
	 */
	public static function from_saved_state( $saved_state ) {
		return new static(
			DPC_Entry_Data_Read_So_Far::from_array( $saved_state['entry_data_read_so_far'] ),
			DPC_Throw_On_Errors::t_base64_decode( $saved_state['bytes_read_so_far'] )
		);
	}
}



/**
 * State that reads the Entry Type.
 *
 * If the Receiver is at this state, we're at the beginning of a new Entry, i.e. the next 1 byte (if not EOF) would be
 * the Entry Type.
 */
class DPC_Entry_Type_Read_State extends DPC_Read_State {

	/**
	 * Map of valid Entry Type integer values.
	 *
	 * @see DPC_Supported_Entry_Types
	 *
	 * @var array<int, boolean>|null Map with integer Entry Types as keys, true as values; or null if the map hasn't
	 *      been initialized yet.
	 */
	private static $entry_type_int_values;

	/**
	 * Validate integer Entry Type.
	 *
	 * @param int $entry_type Integer Entry Type.
	 *
	 * @return bool True if the Entry Type is valid, false otherwise.
	 *
	 * @see DPC_Supported_Entry_Types
	 *
	 */
	private static function entry_type_is_valid( $entry_type ) {

		// Build an entry type existence map if it doesn't exist yet.
		if ( is_null( static::$entry_type_int_values ) ) {

			$class = new ReflectionClass( DPC_Supported_Entry_Types::class );

			static::$entry_type_int_values = array();
			foreach ( $class->getConstants() as $constant_value ) {
				static::$entry_type_int_values[ $constant_value ] = true;
			}
		}

		return array_key_exists( $entry_type, static::$entry_type_int_values );
	}

	/**
	 * Constructor.
	 *
	 * @throws DPC_Internal_Error_Exception On invalid parameters.
	 */
	public function __construct() {
		parent::__construct( new DPC_Entry_Data_Read_So_Far() );
	}

	public static function state_name() {
		return 'type';
	}

	public function process_and_return_next_state( $chunk_handle, $metadata ) {

		$entry_type_encoded = DPC_Throw_On_Errors::t_fread( $chunk_handle, DPC_Entry_Type_Pack_Type::byte_size() );

		if ( DPC_Bytes::b_strlen( $entry_type_encoded ) < DPC_Entry_Type_Pack_Type::byte_size() ) {

			// Entry type not read in full because we ran out of data in a Chunk.
			return null;
		}

		$this->entry_data_read_so_far->entry_type = dpc_unpack_single_value(
			DPC_Entry_Type_Pack_Type::pack_argument(),
			$entry_type_encoded,
			'entry type'
		);

		if ( ! static::entry_type_is_valid( $this->entry_data_read_so_far->entry_type ) ) {
			throw new DPC_Invalid_Input_Exception(
				"Entry type {$this->entry_data_read_so_far->entry_type} is not valid."
			);
		}

		return new DPC_Entry_Permissions_Read_State( $this->entry_data_read_so_far );
	}
}



/**
 * State that reads the Entry Permissions.
 *
 * If the Receiver is at this state, we've read zero or more of the bytes that make up the Permissions, and some bytes
 * that make up Permissions are still left to read.
 */
class DPC_Entry_Permissions_Read_State extends DPC_Read_State {

	/**
	 * Maximum permissions value.
	 *
	 * We don't support setuid bits and such, because PHP typically runs as an unprivileged user, so we wouldn't be
	 * able to set those.
	 */
	const MAX_PERMISSIONS_VALUE = 0777;

	public static function state_name() {
		return 'permissions';
	}

	public function process_and_return_next_state( $chunk_handle, $metadata ) {

		$this->bytes_read_so_far .= DPC_Throw_On_Errors::t_fread(
			$chunk_handle,
			DPC_Entry_Permissions_Pack_Type::byte_size() - DPC_Bytes::b_strlen( $this->bytes_read_so_far )
		);

		if ( DPC_Bytes::b_strlen( $this->bytes_read_so_far ) < DPC_Entry_Permissions_Pack_Type::byte_size() ) {
			// Permissions not read in full because we ran out of data in a chunk.
			return null;
		}

		// Permissions read in full at this point.

		$this->entry_data_read_so_far->permissions = dpc_unpack_single_value(
			DPC_Entry_Permissions_Pack_Type::pack_argument(),
			$this->bytes_read_so_far,
			'permissions'
		);

		if ( $this->entry_data_read_so_far->permissions !== DPC_PERMISSIONS_UNSET ) {

			if ( $this->entry_data_read_so_far->permissions < 0 ||
			     $this->entry_data_read_so_far->permissions > static::MAX_PERMISSIONS_VALUE ) {
				throw new DPC_Invalid_Input_Exception(
					'Invalid permissions: 0' . decoct( $this->entry_data_read_so_far->permissions )
				);
			}

			// Symlinks can't have permissions, so if they do, it must be an error on the Sender's side.
			if ( DPC_Supported_Entry_Types::SYMLINK === $this->entry_data_read_so_far->entry_type ) {
				throw new DPC_Invalid_Input_Exception(
					'Entry "' . $this->entry_data_read_so_far->path . '" is a symlink, ' .
					'but has permissions set to 0' . decoct( $this->entry_data_read_so_far->permissions )
				);
			}
		}

		return new DPC_Entry_Path_Size_Read_State( $this->entry_data_read_so_far );
	}
}



/**
 * State that reads the Entry Path size.
 *
 * If the Receiver is at this state, we've read zero or more of the bytes that make up the Path size, and some bytes
 * that make up the Path size are still left to read.
 */
class DPC_Entry_Path_Size_Read_State extends DPC_Read_State {

	/**
	 * Maximum size of a path.
	 *
	 * PHP_MAXPATHLEN is sometimes unreliable, e.g. it's been observed that on systems where this value is 1024, PHP is
	 * able to use paths that are 1013 bytes in length. So, set the limit to 1000 bytes here rather arbitrarily.
	 */
	const MAX_PATH_SIZE = 1000;

	public static function state_name() {
		return 'path-size';
	}

	public function process_and_return_next_state( $chunk_handle, $metadata ) {

		$this->bytes_read_so_far .= DPC_Throw_On_Errors::t_fread(
			$chunk_handle,
			DPC_Entry_Path_Size_Pack_Type::byte_size() - DPC_Bytes::b_strlen( $this->bytes_read_so_far )
		);

		if ( DPC_Bytes::b_strlen( $this->bytes_read_so_far ) < DPC_Entry_Path_Size_Pack_Type::byte_size() ) {
			// Path size not read in full because we ran out of data in a chunk.
			return null;
		}

		// Path size read in full at this point.

		$this->entry_data_read_so_far->path_size = dpc_unpack_single_value(
			DPC_Entry_Path_Size_Pack_Type::pack_argument(),
			$this->bytes_read_so_far,
			'path size'
		);

		if ( $this->entry_data_read_so_far->path_size < 1 ||
		     $this->entry_data_read_so_far->path_size > static::MAX_PATH_SIZE ) {
			throw new DPC_Invalid_Input_Exception( "Invalid path size: {$this->entry_data_read_so_far->path_size}" );
		}

		return new DPC_Entry_Path_Read_State( $this->entry_data_read_so_far );
	}
}



/**
 * State that reads the Entry Path.
 *
 * If the Receiver is at this state, we've read zero or more of the bytes that make up the Path, and some bytes that
 * make up the Path are still left to read.
 */
class DPC_Entry_Path_Read_State extends DPC_Read_State {

	/**
	 * Default permissions for directories that will get created when the permissions of the directory are not set.
	 *
	 * PHP's own default permissions of directories created with mkdir() are 0777 which seems to be a bit too liberal.
	 */
	const DIR_DEFAULT_PERMISSIONS = 0755;

	public static function state_name() {
		return 'path';
	}

	public function process_and_return_next_state( $chunk_handle, $metadata ) {

		$this->bytes_read_so_far .= DPC_Throw_On_Errors::t_fread(
			$chunk_handle,
			$this->entry_data_read_so_far->path_size - DPC_Bytes::b_strlen( $this->bytes_read_so_far )
		);

		if ( DPC_Bytes::b_strlen( $this->bytes_read_so_far ) < $this->entry_data_read_so_far->path_size ) {
			// Path not read in full because we ran out of data in a chunk.
			return null;
		}

		// Path read in full at this point.

		$this->entry_data_read_so_far->path = $this->bytes_read_so_far;

		// FIXME not sure if this is the most secure way to do it.
		if ( false !== DPC_Bytes::b_strpos( $this->entry_data_read_so_far->path, '../' ) ) {
			throw new DPC_Invalid_Input_Exception(
				"Path '{$this->entry_data_read_so_far->path}' can't have relative parts."
			);
		}

		switch ( $this->entry_data_read_so_far->entry_type ) {

			case DPC_Supported_Entry_Types::DIRECTORY:

				// Nothing else left to read about the directory at this point, so just create it.

				$full_directory_path = $metadata->path . $this->entry_data_read_so_far->path;

				$fs_object = new DPC_Filesystem_Object( $full_directory_path );

				if ( $fs_object->exists ) {
					if ( $fs_object->is_dir ) {
						if ( $this->entry_data_read_so_far->permissions !== DPC_PERMISSIONS_UNSET ) {
							// The directory might have been created by the file part reader, or it might have existed
							// before we started writing anything anywhere, so we just need it to have the right
							// permissions.
							DPC_Throw_On_Errors::t_chmod(
								$full_directory_path,
								$this->entry_data_read_so_far->permissions
							);
						}
					} else {
						throw new DPC_Invalid_Input_Exception(
							"Path '$full_directory_path' exists already but is not a directory"
						);
					}
				} else {
					$permissions = $this->entry_data_read_so_far->permissions;
					if ( $permissions === DPC_PERMISSIONS_UNSET ) {
						$permissions = static::DIR_DEFAULT_PERMISSIONS;
					}

					DPC_Throw_On_Errors::t_mkdir( $full_directory_path, $permissions, true );
				}

				// Next bytes (if there's still data to read in the chunk) will make up a new entry.
				$next_state = new DPC_Entry_Type_Read_State();

				break;

			case DPC_Supported_Entry_Types::FILE:

				$next_state = new DPC_Entry_File_Total_Size_Read_State( $this->entry_data_read_so_far );

				break;

			case DPC_Supported_Entry_Types::SYMLINK:

				$next_state = new DPC_Entry_Symlink_Path_Size_Read_State( $this->entry_data_read_so_far );

				break;

			default:
				throw new DPC_Invalid_Input_Exception(
					"Unsupported entry type: {$this->entry_data_read_so_far->entry_type}"
				);
		}

		return $next_state;
	}
}



/**
 * State that reads the Entry File's total size.
 *
 * If the Receiver is at this state, we've read zero or more of the bytes that make up the File's total size, and some
 * bytes that make up the File's total size are still left to read.
 */
class DPC_Entry_File_Total_Size_Read_State extends DPC_Read_State {

	/**
	 * Maximum total file size.
	 *
	 * We can technically support files up to 2^64 bytes in size, but we don't expect to encounter those, and we need a
	 * sanity check to make sure that we've read the right bytes for the total file size.
	 */
	const MAX_FILE_TOTAL_SIZE = 1024 * 1024 * 1024 * 1024;

	public static function state_name() {
		return 'file-total-size';
	}

	public function process_and_return_next_state( $chunk_handle, $metadata ) {

		$this->bytes_read_so_far .= DPC_Throw_On_Errors::t_fread(
			$chunk_handle,
			( DPC_Entry_File_Total_Size_Pack_Type::byte_size() - DPC_Bytes::b_strlen( $this->bytes_read_so_far ) )
		);

		if ( DPC_Bytes::b_strlen( $this->bytes_read_so_far ) < DPC_Entry_File_Total_Size_Pack_Type::byte_size() ) {
			// File's total size not read in full because we ran out of data in a chunk.
			return null;
		}

		// File's total size read in full at this point.

		$this->entry_data_read_so_far->file_total_size = dpc_unpack_single_value(
			DPC_Entry_File_Total_Size_Pack_Type::pack_argument(),
			$this->bytes_read_so_far,
			'file total size'
		);

		if ( $this->entry_data_read_so_far->file_total_size < 0 /* File can be empty */ ||
		     $this->entry_data_read_so_far->file_total_size > static::MAX_FILE_TOTAL_SIZE ) {
			throw new DPC_Invalid_Input_Exception(
				"Invalid file total size: {$this->entry_data_read_so_far->file_total_size}"
			);
		}

		return new DPC_Entry_File_Total_Written_So_Far_Size_Read_State( $this->entry_data_read_so_far );
	}
}



/**
 * State that reads the Entry File's written so far size.
 *
 * If the Receiver is at this state, we've read zero or more of the bytes that make up the File's total written so far
 * size, and some bytes that make up the File's total written so far size are still left to read.
 */
class DPC_Entry_File_Total_Written_So_Far_Size_Read_State extends DPC_Read_State {

	/**
	 * Maximum total written so far file size.
	 *
	 * We can technically support files up to 2^64 bytes in size, but we don't expect to encounter those, and we need a
	 * sanity check to make sure that we've read the right bytes for the file's total written so far size.
	 */
	const MAX_FILE_TOTAL_WRITTEN_SO_FAR_SIZE = 1024 * 1024 * 1024 * 1024;

	public static function state_name() {
		return 'file-total-written-so-far-size';
	}

	public function process_and_return_next_state( $chunk_handle, $metadata ) {

		$this->bytes_read_so_far .= DPC_Throw_On_Errors::t_fread(
			$chunk_handle,
			(
				DPC_Entry_File_Total_Written_So_Far_Size_Pack_Type::byte_size() -
				DPC_Bytes::b_strlen( $this->bytes_read_so_far )
			)
		);

		if ( DPC_Bytes::b_strlen( $this->bytes_read_so_far ) <
		     DPC_Entry_File_Total_Written_So_Far_Size_Pack_Type::byte_size() ) {
			// File's total written so far size not read in full because we ran out of data in a chunk.
			return null;
		}

		// File's total written so far size read in full at this point.

		$this->entry_data_read_so_far->file_total_written_so_far_size = dpc_unpack_single_value(
			DPC_Entry_File_Total_Written_So_Far_Size_Pack_Type::pack_argument(),
			$this->bytes_read_so_far,
			'file total written so far size'
		);

		if (
			$this->entry_data_read_so_far->file_total_written_so_far_size < 0 /* File can be empty */ || (
				$this->entry_data_read_so_far->file_total_written_so_far_size >
				static::MAX_FILE_TOTAL_WRITTEN_SO_FAR_SIZE
			)
		) {
			throw new DPC_Invalid_Input_Exception(
				'Invalid file total written so far size: ' .
				$this->entry_data_read_so_far->file_total_written_so_far_size
			);
		}

		if ( $this->entry_data_read_so_far->file_total_written_so_far_size >
		     $this->entry_data_read_so_far->file_total_size ) {
			// Most likely bogus input.
			throw new DPC_Invalid_Input_Exception(
				'So far we have written more data ' .
				"({$this->entry_data_read_so_far->file_total_written_so_far_size} bytes) " .
				'than the total file size of the file ' .
				"({$this->entry_data_read_so_far->file_total_size} bytes)"
			);
		}

		return new DPC_Entry_File_Part_Size_Read_State( $this->entry_data_read_so_far );
	}
}



/**
 * State that reads the Entry File part size.
 *
 * If the Receiver is at this state, we've read zero or more of the bytes that make up the File part size, and some
 * bytes that make up the File part's size are still left to read.
 */
class DPC_Entry_File_Part_Size_Read_State extends DPC_Read_State {

	/**
	 * Maximum file part size.
	 *
	 * We can technically support files up to 2^64 bytes in size, but we don't expect to encounter those, and we need a
	 * sanity check to make sure that we've read the right bytes for the file part's size.
	 */
	const MAX_FILE_PART_SIZE = 1024 * 1024 * 1024 * 1024;

	public static function state_name() {
		return 'file-part-size';
	}

	public function process_and_return_next_state( $chunk_handle, $metadata ) {

		$this->bytes_read_so_far .= DPC_Throw_On_Errors::t_fread(
			$chunk_handle,
			( DPC_Entry_File_Part_Size_Pack_Type::byte_size() - DPC_Bytes::b_strlen( $this->bytes_read_so_far ) )
		);

		if ( DPC_Bytes::b_strlen( $this->bytes_read_so_far ) < DPC_Entry_File_Part_Size_Pack_Type::byte_size() ) {
			// File part's size not read in full because we ran out of data in a chunk.
			return null;
		}

		// File part's size read in full at this point.

		$this->entry_data_read_so_far->file_part_size = dpc_unpack_single_value(
			DPC_Entry_File_Part_Size_Pack_Type::pack_argument(),
			$this->bytes_read_so_far,
			'file part size'
		);

		if (
			$this->entry_data_read_so_far->file_part_size < 0 /* File can be empty */ ||
			$this->entry_data_read_so_far->file_part_size > static::MAX_FILE_PART_SIZE
		) {
			throw new DPC_Invalid_Input_Exception(
				'Invalid file part size: ' . $this->entry_data_read_so_far->file_part_size
			);
		}

		if ( $this->entry_data_read_so_far->file_part_size > $this->entry_data_read_so_far->file_total_size ) {
			// Most likely bogus input.
			throw new DPC_Invalid_Input_Exception(
				"File part ({$this->entry_data_read_so_far->file_part_size} bytes) is bigger than " .
				"the total file size of the file ({$this->entry_data_read_so_far->file_total_size} bytes)"
			);
		}

		return new DPC_Entry_File_Part_Data_Read_State( $this->entry_data_read_so_far );
	}
}



/**
 * State that reads the Entry File part's data.
 *
 * If the Receiver is at this state, we've read zero or more of the bytes that make up the File part's data, and some
 * bytes that make up the File part's data are still left to read.
 */
class DPC_Entry_File_Part_Data_Read_State extends DPC_Read_State {

	/**
	 * How many bytes of file part data to read at once and buffer into RAM.
	 */
	const FILE_WRITE_BUFFER_SIZE = 100 * 1024;

	/**
	 * Default permissions for directories that get created to place files in.
	 *
	 * If the directory that is to hold the file doesn't exist yet, it will get created. However, at this point we
	 * don't know the permissions that the directory should get created with, so these are the default permissions that
	 * this file-holding directory will be created with.
	 */
	const FILE_DIR_DEFAULT_PERMISSIONS = 0755;

	/**
	 * How many bytes of this specific file part's data have been written so far.
	 *
	 * Chunk might get cut off in the middle of the file part's data, so we keep track of this number, and
	 * store/restore it as part of the State.
	 *
	 * @var int
	 */
	private $file_part_written_so_far_size;

	/**
	 * Constructor.
	 *
	 * @param DPC_Entry_Data_Read_So_Far $entry_data_read_so_far        Data about the Entry that has been read so far.
	 * @param string                     $bytes_read_so_far             Bytes of state's data that have been read so
	 *                                                                  far.
	 * @param int                        $file_part_written_so_far_size How many bytes of this specific file part's
	 *                                                                  data have been written so far.
	 *
	 * @throws DPC_Internal_Error_Exception On invalid parameters.
	 */
	public function __construct(
		$entry_data_read_so_far,
		$bytes_read_so_far = '',
		$file_part_written_so_far_size = 0
	) {
		parent::__construct( $entry_data_read_so_far, $bytes_read_so_far );
		$this->file_part_written_so_far_size = $file_part_written_so_far_size;
	}

	/**
	 * Serialize this state to an array, including "file_part_written_so_far_size".
	 *
	 * @return array State's array representation.
	 *
	 * @see DPC_Entry_File_Part_Data_Read_State::from_saved_state()
	 */
	public function to_saved_state() {
		$extra_data = array(
			'file_part_written_so_far_size' => $this->file_part_written_so_far_size,
		);

		return array_merge( parent::to_saved_state(), $extra_data );
	}

	/**
	 * Unserialize state from an array, including "file_part_written_so_far_size".
	 *
	 * @param array $saved_state State's array representation.
	 *
	 * @return DPC_Read_State Unserialized state.
	 *
	 * @throws DPC_Internal_Error_Exception|Exception On errors while unserializing the state.
	 *
	 * @see DPC_Entry_File_Part_Data_Read_State::to_saved_state()
	 */
	public static function from_saved_state( $saved_state ) {
		$immediate_object = parent::from_saved_state( $saved_state );

		return new static(
			$immediate_object->entry_data_read_so_far,
			$immediate_object->bytes_read_so_far,
			$saved_state['file_part_written_so_far_size']
		);
	}

	public static function state_name() {
		return 'file-part-data';
	}

	/**
	 * Create a directory leading to a file.
	 *
	 * @param string $full_file_path Full path to a file.
	 *
	 * @return void
	 * @throws DPC_Internal_Error_Exception|Exception On errors while creating a directory leading to a file.
	 */
	private static function create_directory_leading_to_file( $full_file_path ) {

		$file_directory        = dirname( $full_file_path );
		$file_directory_object = new DPC_Filesystem_Object( $file_directory );
		if ( $file_directory_object->exists ) {
			if ( ! $file_directory_object->is_dir ) {
				throw new DPC_Internal_Error_Exception(
					"Directory leading to a file '$file_directory' is not a directory"
				);
			}
		} else {
			DPC_Throw_On_Errors::t_mkdir( $file_directory, static::FILE_DIR_DEFAULT_PERMISSIONS, true );
		}
	}

	/**
	 * Create a file if it doesn't exist yet.
	 *
	 * Create the file with the right permissions first so that we don't end up potentially sensitive user data to a
	 * file with wrong permissions (even if it's for a few milliseconds).
	 *
	 * @param string $full_file_path Full path to a file.
	 *
	 * @return void
	 * @throws DPC_Internal_Error_Exception|Exception On errors while creating a file.
	 */
	private function create_file_if_it_does_not_exist( $full_file_path ) {

		$file_object = new DPC_Filesystem_Object( $full_file_path );
		if ( $file_object->exists ) {

			if ( $file_object->is_file ) {

				// The cache was just cleared by DPC_Filesystem_Object, so no need to call clearstatcache() again.

				$existing_file_size = DPC_Throw_On_Errors::t_filesize( $full_file_path );

				// The file might already exist at the target location, so complain about file size mismatch only if
				// this is not the first file part coming in; otherwise, just overwrite it.
				if ( $this->entry_data_read_so_far->file_total_written_so_far_size > 0 ) {

					if ( $existing_file_size !== $this->entry_data_read_so_far->file_total_written_so_far_size ) {
						throw new DPC_Internal_Error_Exception(
							"Existing file's '$full_file_path' size ($existing_file_size bytes) " .
							'does not match the bytes written so far ' .
							"({$this->entry_data_read_so_far->file_total_written_so_far_size} bytes)"
						);
					}

					if ( $existing_file_size === $this->entry_data_read_so_far->file_total_size ) {
						throw new DPC_Internal_Error_Exception(
							"File '$full_file_path' has been written in full ($existing_file_size bytes), " .
							'so a new file part coming in to append to it is unexpected'
						);
					}
				}

			} else {
				throw new DPC_Internal_Error_Exception( "Path '$full_file_path' already exists but is not a file" );
			}

		} else {

			if ( $this->entry_data_read_so_far->file_total_written_so_far_size > 0 ) {
				throw new DPC_Internal_Error_Exception(
					"File '$full_file_path' does not exist, but we expect to have " .
					"{$this->entry_data_read_so_far->file_total_written_so_far_size} bytes already written to it"
				);
			}

			DPC_Throw_On_Errors::t_touch( $full_file_path );
		}

		if ( $this->entry_data_read_so_far->permissions !== DPC_PERMISSIONS_UNSET ) {
			DPC_Throw_On_Errors::t_chmod( $full_file_path, $this->entry_data_read_so_far->permissions );
		}
	}

	public function process_and_return_next_state( $chunk_handle, $metadata ) {

		$full_file_path = $metadata->path . $this->entry_data_read_so_far->path;

		static::create_directory_leading_to_file( $full_file_path );

		$this->create_file_if_it_does_not_exist( $full_file_path );

		// Previous functions might have changed permissions and such, so make sure we're reading the most up-to-date
		// permissions here.
		clearstatcache();

		if ( ! is_writable( $full_file_path ) ) {

			// File's permissions from a Chunk might have disallowed to write to a file ourselves, so if that is the
			// case, temporarily set the permissions to permissive ones to be able to write File Part data to a file.
			// Later we'll set them back to what they're supposed to be.

			$current_permissions = DPC_Throw_On_Errors::t_fileperms( $full_file_path ) & 0777;

			// It's 0666, not 0600, because we're not sure if we own the file.
			DPC_Throw_On_Errors::t_chmod( $full_file_path, $current_permissions | 0666 );

			// Clear permissions cache if PHP managed to cache them up again.
			clearstatcache();
		}

		if ( 0 === $this->entry_data_read_so_far->file_total_written_so_far_size ) {
			// Overwrite existing file at the start of the first file part.
			$file_open_mode = 'wb';
		} else {
			// Append to whatever has been written so far.
			$file_open_mode = 'ab';
		}

		$file_handle = DPC_Throw_On_Errors::t_fopen( $full_file_path, $file_open_mode );

		while ( $this->file_part_written_so_far_size < $this->entry_data_read_so_far->file_part_size ) {

			$file_part_chunk_bytes_to_read = min(
				( $this->entry_data_read_so_far->file_part_size - $this->file_part_written_so_far_size ),
				static::FILE_WRITE_BUFFER_SIZE
			);

			$file_part_chunk            = DPC_Throw_On_Errors::t_fread( $chunk_handle, $file_part_chunk_bytes_to_read );
			$file_part_chunk_bytes_read = DPC_Bytes::b_strlen( $file_part_chunk );

			DPC_Throw_On_Errors::t_fwrite( $file_handle, $file_part_chunk );
			$this->file_part_written_so_far_size                          += $file_part_chunk_bytes_read;
			$this->entry_data_read_so_far->file_total_written_so_far_size += $file_part_chunk_bytes_read;

			if ( $file_part_chunk_bytes_read < $file_part_chunk_bytes_to_read ) {
				// File part data not read in full because we ran out of data in a chunk.
				break;
			}
		}

		DPC_Throw_On_Errors::t_fclose( $file_handle );

		if ( $this->entry_data_read_so_far->permissions !== DPC_PERMISSIONS_UNSET ) {
			// Either set permissions to the newly created file, or reset the temporary write permissions added
			// previously.
			DPC_Throw_On_Errors::t_chmod( $full_file_path, $this->entry_data_read_so_far->permissions );
		}

		if ( $this->file_part_written_so_far_size === $this->entry_data_read_so_far->file_part_size ) {
			// File part written in full -- next bytes (if there's still data to read in the chunk) will make up a new
			// entry.
			$next_state = new DPC_Entry_Type_Read_State();
		} else {
			// File part not written in full because we ran out of data in a chunk.
			$next_state = null;
		}

		return $next_state;
	}

}



/**
 * State that reads the Entry Symlink path size.
 *
 * If the Receiver is at this state, we've read zero or more of the bytes that make up the Symlink path size, and some
 * bytes that make up the Symlink path's size are still left to read.
 */
class DPC_Entry_Symlink_Path_Size_Read_State extends DPC_Read_State {

	/**
	 * Maximum size of a symlink path.
	 *
	 * PHP_MAXPATHLEN is sometimes unreliable, e.g. it's been observed that on systems where this value is 1024, PHP is
	 * able to use paths that are 1013 bytes in length. So, set the limit to 1000 bytes here rather arbitrarily.
	 */
	const MAX_SYMLINK_PATH_SIZE = 1000;

	public static function state_name() {
		return 'symlink-path-size';
	}

	public function process_and_return_next_state( $chunk_handle, $metadata ) {

		$this->bytes_read_so_far .= DPC_Throw_On_Errors::t_fread(
			$chunk_handle,
			DPC_Entry_Symlink_Path_Size_Pack_Type::byte_size() - DPC_Bytes::b_strlen( $this->bytes_read_so_far )
		);

		if ( DPC_Bytes::b_strlen( $this->bytes_read_so_far ) < DPC_Entry_Symlink_Path_Size_Pack_Type::byte_size() ) {
			// Symlink path size not read in full because we ran out of data in a chunk.
			return null;
		}

		// Symlink path size read in full at this point.

		$this->entry_data_read_so_far->symlink_path_size = dpc_unpack_single_value(
			DPC_Entry_Symlink_Path_Size_Pack_Type::pack_argument(),
			$this->bytes_read_so_far,
			'symlink path size'
		);

		if ( $this->entry_data_read_so_far->symlink_path_size < 1 ||
		     $this->entry_data_read_so_far->symlink_path_size > static::MAX_SYMLINK_PATH_SIZE ) {
			throw new DPC_Invalid_Input_Exception(
				"Invalid symlink path size: {$this->entry_data_read_so_far->symlink_path_size}"
			);
		}

		return new DPC_Entry_Symlink_Path_Read_State( $this->entry_data_read_so_far );
	}
}



/**
 * State that reads the Entry Symlink path.
 *
 * If the Receiver is at this state, we've read zero or more of the bytes that make up the Symlink path, and some bytes
 * that make up the Symlink path are still left to read.
 */
class DPC_Entry_Symlink_Path_Read_State extends DPC_Read_State {

	/**
	 * Default permissions for directories that get created to place symlinks in.
	 *
	 * If the directory that is to hold the symlink doesn't exist yet, it will get created. However, at this point we
	 * don't know the permissions that the directory should get created with, so these are the default permissions that
	 * this symlink-holding directory will be created with.
	 */
	const SYMLINK_DIR_DEFAULT_PERMISSIONS = 0755;

	public static function state_name() {
		return 'symlink-path';
	}

	public function process_and_return_next_state( $chunk_handle, $metadata ) {

		$this->bytes_read_so_far .= DPC_Throw_On_Errors::t_fread(
			$chunk_handle,
			$this->entry_data_read_so_far->symlink_path_size - DPC_Bytes::b_strlen( $this->bytes_read_so_far )
		);

		if ( DPC_Bytes::b_strlen( $this->bytes_read_so_far ) < $this->entry_data_read_so_far->symlink_path_size ) {
			// Symlink path not read in full because we ran out of data in a chunk.
			return null;
		}

		// Symlink path read in full at this point.

		$symlink_dst = $this->bytes_read_so_far;
		$symlink_src = $metadata->path . $this->entry_data_read_so_far->path;

		// Create a directory that leads to the symlink.
		$symlink_directory        = dirname( $symlink_src );
		$symlink_directory_object = new DPC_Filesystem_Object( $symlink_directory );
		if ( $symlink_directory_object->exists ) {
			if ( ! $symlink_directory_object->is_dir ) {
				throw new DPC_Internal_Error_Exception(
					"Directory leading to a symlink '$symlink_directory' is not a directory"
				);
			}
		} else {
			DPC_Throw_On_Errors::t_mkdir( $symlink_directory, static::SYMLINK_DIR_DEFAULT_PERMISSIONS, true );
		}

		// Create symlink itself.
		$create_symlink = true;
		$fs_object      = new DPC_Filesystem_Object( $symlink_src );
		if ( $fs_object->exists ) {

			if ( $fs_object->is_link ) {
				// Object exists, and is a symlink -- make sure it points to the right target.
				// (file_exists() would return false on existing but broken symlinks.)
				$current_symlink_target = DPC_Throw_On_Errors::t_readlink( $symlink_src );
				if ( $current_symlink_target === $symlink_dst ) {
					// No need to recreate the symlink.
					$create_symlink = false;
				} else {
					// Symlink already exists, but points to the wrong path.
					DPC_Throw_On_Errors::t_unlink( $symlink_src );
				}

			} else {
				throw new DPC_Internal_Error_Exception( "Path '$symlink_src' already exists but is not a symlink" );
			}
		}

		if ( $create_symlink ) {
			DPC_Throw_On_Errors::t_symlink( $symlink_dst, $symlink_src );
		}

		// Next bytes (if there's still data to read in the chunk) will make up a new entry.
		return new DPC_Entry_Type_Read_State();
	}
}



/**
 * Receiver, i.e. class that parses an incoming Chunk, and stores the Chunk's data in the filesystem.
 */
class DPC_Receiver {

	/**
	 * Recreate a read state from a state stored as an array (that itself was decoded from State JSON in Chunk).
	 *
	 * @param array $state_array Array representation of a state.
	 *
	 * @return DPC_Read_State|null State object, or null if the state couldn't be found/initialized.
	 * @throws DPC_Internal_Error_Exception On errors initializing a state object from a state array.
	 * @throws Exception On an incompletely implemented state, i.e. the one that has implementation stubs left.
	 *
	 * @see DPC_Receiver::state_array_from_state()
	 */
	private static function state_from_state_array( $state_array ) {
		$state_name = $state_array['name'];
		$state_args = $state_array['args'];

		$read_state_class_names = array();
		foreach ( get_declared_classes() as $class ) {
			if ( is_subclass_of( $class, DPC_Read_State::class ) ) {
				$read_state_class_names[] = $class;
			}
		}

		if ( empty( $read_state_class_names ) ) {
			throw new DPC_Internal_Error_Exception( 'Unable to find any ' . DPC_Read_State::class . ' subclasses' );
		}

		foreach ( $read_state_class_names as $read_state_class_name ) {

			try {
				$test_class = new ReflectionClass( $read_state_class_name );
			} catch ( ReflectionException $ex ) {
				throw new DPC_Internal_Error_Exception(
					"Unable to create reflection class for class '$read_state_class_name': " . $ex->getMessage()
				);
			}

			if ( ! $test_class->isAbstract() ) {
				if ( method_exists( $read_state_class_name, 'state_name' ) ) {
					if ( method_exists( $read_state_class_name, 'from_saved_state' ) ) {
						if ( $state_name === $read_state_class_name::state_name() ) {
							return $read_state_class_name::from_saved_state( $state_args );
						}
					}
				}
			}
		}

		return null;
	}

	/**
	 * Generate an array representation of state from a state object (which will later be encoded into JSON).
	 *
	 * @param DPC_Read_State $state State object.
	 *
	 * @return array Array representation of a state.
	 * @throws Exception On an incompletely implemented state, i.e. the one that has implementation stubs left.
	 *
	 * @see DPC_Receiver::state_from_state_array()
	 */
	private static function state_array_from_state( $state ) {
		return array(
			'name' => $state::state_name(),
			'args' => $state->to_saved_state(),
		);
	}

	/**
	 * Read and validate Signature bytes.
	 *
	 * @param resource $chunk_handle Resource, e.g. an opened file, to read the Signature from.
	 *
	 * @return void
	 *
	 * @throws DPC_Invalid_Input_Exception If Signature is invalid, e.g. when the input doesn't look like a Chunk.
	 * @throws Exception On read errors.
	 */
	private static function read_and_validate_signature( $chunk_handle ) {
		$data_format_signature = DPC_Throw_On_Errors::t_fread(
			$chunk_handle,
			DPC_Bytes::b_strlen( DPC_DATA_FORMAT_SIGNATURE )
		);
		if ( $data_format_signature !== DPC_DATA_FORMAT_SIGNATURE ) {
			throw new DPC_Invalid_Input_Exception( 'Invalid data format signature' );
		}
	}

	/**
	 * Read and validate Version bytes.
	 *
	 * @param resource $chunk_handle Resource, e.g. an opened file, to read the Version from.
	 *
	 * @return void
	 *
	 * @throws DPC_Invalid_Input_Exception If Version is invalid, e.g. when the Reader doesn't support the version of a
	 *                                     Chunk that is to be parsed.
	 * @throws Exception On read errors.
	 */
	private static function read_and_validate_version( $chunk_handle ) {
		$data_format_version_encoded = DPC_Throw_On_Errors::t_fread(
			$chunk_handle,
			DPC_Data_Format_Version_Pack_Type::byte_size()
		);
		if ( DPC_Bytes::b_strlen( $data_format_version_encoded ) < DPC_Data_Format_Version_Pack_Type::byte_size() ) {
			throw new DPC_Invalid_Input_Exception( 'Unable to read data format version' );
		}

		$data_format_version = dpc_unpack_single_value(
			DPC_Data_Format_Version_Pack_Type::pack_argument(),
			$data_format_version_encoded,
			'data format version'
		);

		if ( $data_format_version !== DPC_DATA_FORMAT_VERSION ) {
			throw new DPC_Invalid_Input_Exception(
				'Incompatible data format version ' .
				'(expected: ' . DPC_DATA_FORMAT_VERSION . "; got: $data_format_version)"
			);
		}
	}

	/**
	 * Read and parse Metadata.
	 *
	 * @param resource $chunk_handle Resource, e.g. an opened file, to read the Metadata from.
	 *
	 * @return DPC_Input_Metadata Metadata object.
	 *
	 * @throws DPC_Invalid_Input_Exception When Metadata JSON doesn't look valid, e.g. on invalid input.
	 * @throws Exception On read errors.
	 */
	private static function read_metadata( $chunk_handle ) {
		$metadata_json_size_encoded = DPC_Throw_On_Errors::t_fread(
			$chunk_handle, DPC_Metadata_JSON_Size_Pack_Type::byte_size()
		);
		if ( DPC_Bytes::b_strlen( $metadata_json_size_encoded ) < DPC_Metadata_JSON_Size_Pack_Type::byte_size() ) {
			throw new DPC_Invalid_Input_Exception( 'Unable to read metadata JSON size' );
		}

		$metadata_json_size = dpc_unpack_single_value(
			DPC_Metadata_JSON_Size_Pack_Type::pack_argument(),
			$metadata_json_size_encoded,
			'metadata JSON size'
		);

		if ( $metadata_json_size < 1 || $metadata_json_size > DPC_METADATA_JSON_MAX_SIZE ) {
			throw new DPC_Invalid_Input_Exception( "Invalid metadata JSON path size: $metadata_json_size" );
		}

		$metadata_json = DPC_Throw_On_Errors::t_fread( $chunk_handle, $metadata_json_size );
		if ( DPC_Bytes::b_strlen( $metadata_json ) < $metadata_json_size ) {
			throw new DPC_Invalid_Input_Exception( 'Unable to read metadata JSON' );
		}

		$metadata_array = DPC_Throw_On_Errors::t_json_decode( $metadata_json, JSON_OBJECT_AS_ARRAY );
		if ( ! is_array( $metadata_array ) ) {
			throw new DPC_Invalid_Input_Exception(
				'Metadata decoded from this JSON is not an array: ' . print_r( $metadata_array, true )
			);
		}

		return DPC_Input_Metadata::from_array( $metadata_array );
	}

	/**
	 * Read and parse State.
	 *
	 * @param resource $chunk_handle Resource, e.g. an opened file, to read the State from.
	 *
	 * @return DPC_Read_State State object.
	 *
	 * @throws DPC_Invalid_Input_Exception When State JSON doesn't look valid, e.g. on invalid input.
	 * @throws Exception On read errors.
	 */
	private static function read_state( $chunk_handle ) {
		$state_json_size_encoded = DPC_Throw_On_Errors::t_fread(
			$chunk_handle, DPC_State_JSON_Size_Pack_Type::byte_size()
		);
		if ( DPC_Bytes::b_strlen( $state_json_size_encoded ) < DPC_State_JSON_Size_Pack_Type::byte_size() ) {
			throw new DPC_Invalid_Input_Exception( 'Unable to read state JSON size' );
		}

		$state_json_size = dpc_unpack_single_value(
			DPC_State_JSON_Size_Pack_Type::pack_argument(),
			$state_json_size_encoded,
			'state JSON size'
		);

		if ( $state_json_size < 0 || $state_json_size > DPC_STATE_JSON_MAX_SIZE ) {
			throw new DPC_Invalid_Input_Exception( "Invalid state JSON path size: $state_json_size" );
		}

		if ( 0 === $state_json_size ) {
			// Empty (initial) state.
			$state = new DPC_Entry_Type_Read_State();
		} else {
			$state_json = DPC_Throw_On_Errors::t_fread( $chunk_handle, $state_json_size );
			if ( DPC_Bytes::b_strlen( $state_json ) < $state_json_size ) {
				throw new DPC_Invalid_Input_Exception( 'Unable to read state JSON' );
			}

			$state_array = DPC_Throw_On_Errors::t_json_decode( $state_json, JSON_OBJECT_AS_ARRAY );
			if ( ! is_array( $state_array ) ) {
				throw new DPC_Invalid_Input_Exception( "State decoded from this JSON is not an array: $state_json" );
			}

			$state = static::state_from_state_array( $state_array );
			if ( is_null( $state ) ) {
				throw new DPC_Invalid_Input_Exception(
					'Unable to decode state array: ' . print_r( $state_array, true )
				);
			}
		}

		return $state;
	}

	/**
	 * Process Chunk data.
	 *
	 * Read Chunk data, create the needed files/directories/symlinks.
	 *
	 * @param resource $chunk_handle Resource, e.g. an opened file, to read the Chunk data from.
	 *
	 * @return DPC_Read_State State that the state machine was left in after reading a Chunk.
	 *
	 * @throws DPC_Invalid_Input_Exception On invalid Chunk data.
	 * @throws DPC_Internal_Error_Exception On Chunk processing errors, e.g. write failures.
	 * @throws Exception On read/write failures and other errors.
	 */
	private static function do_process_chunk( $chunk_handle ) {

		self::read_and_validate_signature( $chunk_handle );

		self::read_and_validate_version( $chunk_handle );

		$metadata = self::read_metadata( $chunk_handle );

		$state = self::read_state( $chunk_handle );

		while ( null !== $state ) {
			$next_state = $state->process_and_return_next_state( $chunk_handle, $metadata );

			if ( null === $next_state ) {

				if ( DPC_Throw_On_Errors::t_feof( $chunk_handle ) ) {

					// Reached the end of the chunk.
					return $state;

				} else {
					throw new DPC_Invalid_Input_Exception(
						'We expect to be at the EOF of the Chunk; last State: ' . print_r( $state, true )
					);
				}
			}

			$state = $next_state;
		}

		throw new DPC_Internal_Error_Exception( 'Should not have reached this point' );
	}

	/**
	 * Prepare and Process Chunk data.
	 *
	 * Copy Chunk data to a temporary file, validate data size, catch all errors, serialize the resulting State.
	 *
	 * @param string $input_path          Path to read the Chunk data from, e.g. "/var/tmp/foo" or "php://input".
	 * @param ?int   $input_expected_size Expected size of the Chunk data, or null if size is not known.
	 *
	 * @return array State (in an array form) that the state machine was left in after reading a Chunk.
	 *
	 * @throws DPC_Invalid_Input_Exception On invalid Chunk data.
	 * @throws DPC_Internal_Error_Exception On Chunk processing errors, e.g. write failures.
	 * @throws Exception On read/write failures and other errors.
	 *
	 * @noinspection PhpUnusedParameterInspection
	 * @noinspection PhpDocMissingThrowsInspection
	 */
	public static function process_chunk( $input_path, $input_expected_size = null ) {

		// We want *all* errors (including E_WARNING, E_NOTICE, and E_DEPRECATED) to be reported back to us.
		$old_error_reporting = error_reporting( - 1 );
		$old_display_errors  = ini_set( 'display_errors', 'stderr' );
		set_error_handler(
		/**
		 * Error handler.
		 *
		 * Catch PHP errors, warnings, exceptions, and throw everything as Exception class-derived exception.
		 *
		 * @see https://php-legacy-docs.zend.com/manual/php5/en/function.set-error-handler
		 * @see https://www.php.net/manual/en/function.set-error-handler.php
		 *
		 * @param int         $errno      Level of the error raised.
		 * @param string      $errstr     Error message.
		 * @param string|null $errfile    Filename that the error was raised in.
		 * @param int|null    $errline    Line number where the error was raised.
		 * @param array|null  $errcontext Deprecated, unused.
		 *
		 * @return mixed
		 * @throws DPC_Internal_Error_Exception
		 */
			function ( $errno, $errstr, $errfile = null, $errline = null, $errcontext = null ) {
				throw new DPC_Internal_Error_Exception( "$errstr (file: $errfile; line: $errline)" );
			}
		);

		// Copy chunk to a temporary file because for whatever reason we're unable to use "php://input" / "php://stdin"
		// directly (e.g. it reports wrong data sizes, returns EOF at arbitrary locations).
		$chunk_handle = DPC_Throw_On_Errors::t_tmpfile();

		// Don't use file_get_contents() because a chunk might get large.
		$input_data_bytes_written = 0;
		$input_path_handle        = DPC_Throw_On_Errors::t_fopen( $input_path, 'rb' );
		while ( ! DPC_Throw_On_Errors::t_feof( $input_path_handle ) ) {
			$input_data_chunk = DPC_Throw_On_Errors::t_fread( $input_path_handle, 100 * 1024 );
			DPC_Throw_On_Errors::t_fwrite( $chunk_handle, $input_data_chunk );
			$input_data_bytes_written += DPC_Bytes::b_strlen( $input_data_chunk );
		}

		if ( ! is_null( $input_expected_size ) ) {
			if ( $input_expected_size !== $input_data_bytes_written ) {
				throw new DPC_Invalid_Input_Exception(
					"Didn't receive all the data; expected: $input_expected_size bytes; got: $input_data_bytes_written"
				);
			}
		}

		DPC_Throw_On_Errors::t_fseek( $chunk_handle, 0 );

		$state = null;
		$error = null;

		if ( PHP_MAJOR_VERSION >= 7 ) {
			// On PHP 7.x, all Exception and Error are subclasses of Throwable.
			try {
				$state = static::do_process_chunk( $chunk_handle );
			} catch ( Throwable $throwable ) {
				$error = $throwable;
			}
		} else {
			// On PHP 5.x, there's only Exception.
			try {
				$state = static::do_process_chunk( $chunk_handle );
			} catch ( Exception $exception ) {
				$error = $exception;
			}
		}

		// Clean up temporary file (PHP will do it itself eventually, but we're just speeding up things here to free up
		// disk space as soon as we can).
		DPC_Throw_On_Errors::t_fclose( $chunk_handle );

		restore_error_handler();
		ini_set( 'display_errors', $old_display_errors );
		error_reporting( $old_error_reporting );

		if ( is_null( $state ) ) {
			if ( is_null( $error ) ) {
				throw new DPC_Internal_Error_Exception( 'State is null but no error has been reported' );
			}

			if ( is_a( $error, DPC_HTTP_Status_Code_Exception::class ) ) {
				/** @noinspection PhpUnhandledExceptionInspection */
				throw $error;
			} else {
				throw new DPC_Internal_Error_Exception( "Error while processing Chunk: " . $error->getMessage() );
			}

		} else {
			if ( ! is_null( $error ) ) {
				throw new DPC_Internal_Error_Exception(
					'State has been returned but an error has been reported as well: ' . $error->getMessage()
				);
			}
		}

		return static::state_array_from_state( $state );
	}

}



/**
 * Receiver that parses an incoming Chunk from an HTTP POST payload.
 */
class DPC_HTTP_Receiver {

	/**
	 * Send JSON response back to the caller.
	 *
	 * @param array $response         Free-form response that is to be serialized to JSON.
	 * @param int   $http_status_code HTTP status code to send together with the response.
	 *
	 * @return void
	 */
	private static function send_http_response( $response, $http_status_code ) {

		$content_type_header = 'Content-Type: application/json';

		$json_response = dpc_json_encode_pretty( $response );

		if ( false === $json_response ) {
			http_response_code( 500 );
			header( $content_type_header );
			echo '{"error": "Unable to encode response to JSON"}' . PHP_EOL;

		} else {
			http_response_code( $http_status_code );
			header( $content_type_header );
			echo $json_response;
		}
	}

	/**
	 * Process Chunk data coming in as HTTP POST payload.
	 *
	 * @return void
	 */
	public static function process_chunk_from_http_post() {

		$state      = null;
		$request_id = null;

		try {

			$expected_http_method = 'POST';
			$actual_http_method   = $_SERVER['REQUEST_METHOD'];
			if ( $expected_http_method !== $actual_http_method ) {
				throw new DPC_Invalid_Input_Exception(
					"Expected $expected_http_method request, got $actual_http_method"
				);
			}

			$actual_content_type = $_SERVER['CONTENT_TYPE'];
			if ( DPC_CHUNK_MIME_TYPE !== $actual_content_type ) {
				throw new DPC_Invalid_Input_Exception(
					'Expected ' . DPC_CHUNK_MIME_TYPE . " 'Content-Type' value, got $actual_content_type"
				);
			}

			$input_expected_size = isset( $_SERVER['CONTENT_LENGTH'] ) ? (int) $_SERVER['CONTENT_LENGTH'] : 0;
			if ( $input_expected_size < 1 ) {
				throw new DPC_Invalid_Input_Exception( 'Unable to read Content-Length' );
			}

			$request_id = isset( $_GET['request_id'] ) ? $_GET['request_id'] : null;

			// Both null and empty are bad.
			if ( ! $request_id ) {
				throw new DPC_Invalid_Input_Exception( 'Request ID is unset' );
			}

			$state = DPC_Receiver::process_chunk( 'php://input', $input_expected_size );

		} catch ( Exception $exception ) {

			$http_status_code = 500;
			if ( is_a( $exception, DPC_HTTP_Status_Code_Exception::class ) ) {
				$http_status_code = $exception->http_status_code;
			}

			$error_message = $exception->getMessage();

			$traces = $exception->getTrace();

			foreach ( $traces as &$trace ) {
				// json_encode() doesn't support / gets weird when trying to encode resources and such, so filter those
				// out.
				unset( $trace['args'] );
				unset( $trace['type'] );
			}

			$response = array(
				'error'  => $error_message,
				'traces' => $traces,
			);

			static::send_http_response( $response, $http_status_code );
		}

		if ( ! is_null( $state ) ) {
			static::send_http_response(
				array(
					'request_id' => $request_id,
					'state'      => $state,
				),
				200
			);
		}
	}

}


/**
 * Receive and process an incoming DPC Chunk.
 *
 * *Not* called by Transport Server, so doesn't send back X-Vp-* headers (via success_header() / fatal_error()).
 *
 * @param array $args Argument array (unused).
 *
 * @return void
 * @throws Exception
 *
 * @noinspection PhpUnusedParameterInspection
 */
function action_dpc_receive( $args ) {
	if ( is_cli() ) {
		$state = DPC_Receiver::process_chunk( 'php://stdin' );

		echo dpc_json_encode_pretty( array( 'state' => $state ) );
	} else {
		DPC_HTTP_Receiver::process_chunk_from_http_post();
	}
}

/**
 * Scrapes given php-file and returns an associative array of constants defined.
 *
 * @return array - of constants with name as key and value
 */
function scrape_defined_constants( $phpfile ) {
	if ( ! extension_loaded( 'tokenizer' ) ) {
		throw new Exception( 'Tokenizer module is not loaded.' );
	}
	$source         = file_get_contents( $phpfile );
	$tokens         = token_get_all( $source );
	$constants      = array();
	$constant_name  = $constant_value = null;
	$constant_found = false;

	foreach ( $tokens as $token ) {
		if ( is_array( $token ) ) {
			list($token_name, $token_text) = $token;
		} else {
			$token_name = $token;
			$token_text = null;
		}

		// found a constant definition
		if ( T_STRING === $token_name && $token_text === 'define' ) {
			$constant_found = true;
			$constant_name  = $constant_value = null;
			continue;
		}

		if ( $constant_found ) {
			// found constant name.
			if ( empty( $constant_name ) && T_CONSTANT_ENCAPSED_STRING === $token_name ) {
				$constant_name = trim( $token_text, '\'"' );
				continue;
			}

			// found constant value.
			if ( empty( $constant_value ) && in_array( $token_name, array( T_STRING, T_CONSTANT_ENCAPSED_STRING ) ) ) {
				$constant_value = $token_text;
				continue;
			}

			// found constant definition end.
			if ( ! empty( $constant_value ) && $token_name === ';' ) {
				$trimmed_value = trim( $constant_value, '\'"' );
				// this check is done to eliminate function calls/other defines inside value part
				if ( $trimmed_value !== $constant_value ) {
					$constants[ $constant_name ] = $trimmed_value;
				}
				$constant_found = $constant_name = $constant_value = null;
				continue;
			}

			// found tokens while parsing value part, if any token apart from the list below is encountered,
			// it is not a valid constant definition for our use case. Currently supports string value type only.
			if ( ! in_array( $token_name, array( '\'', '"', '(', ',', ')', T_WHITESPACE ) ) ) {
				$constant_found = $constant_name = $constant_value = null;
			}
		}
	}
	return $constants;
}

/**
 * Loads db credentials from wp-config.php by scraping it.
 *
 * It first tries to find wp-config.php, if found returns credentials
 * else throws exception.
 *
 * @return object {
 *     object containing the user, password, dbname, host, charset and collate.
 *     @type string $user       DB username.
 *     @type string $password   DB password.
 *     @type string $name       DB name.
 *     @type string $host       DB host/port.
 *     @type string $charset    DB charset. default utf-8.
 *     @type string $collate    DB collation. default blank, later auto-determined.
 * }
 *
 * @throws Exception
 */
function load_db_creds_from_config() {
	$absPath    = get_wordpress_location();
	$configPath = '';
	// 1. Determine wp-config.php location
	if ( file_exists( $absPath . '/wp-config.php' ) ) {
		/** The config file resides in $absPath */
		$configPath = $absPath . '/wp-config.php';
	} elseif ( @file_exists( dirname( $absPath ) . '/wp-config.php' ) && ! @file_exists( dirname( $absPath ) . '/wp-settings.php' ) ) {
		/** The config file resides one level above $absPath but is not part of another installation */
		$configPath = dirname( $absPath ) . '/wp-config.php';
	} else {
		throw new Exception( 'wp-config.php file not found.' );
	}

	// 2. Scrape from wp-config file
	$constants = scrape_defined_constants( $configPath );
	$user      = isset( $constants['DB_USER'] ) ? $constants['DB_USER'] : null;
	$password  = isset( $constants['DB_PASSWORD'] ) ? $constants['DB_PASSWORD'] : null;
	$name      = isset( $constants['DB_NAME'] ) ? $constants['DB_NAME'] : null;
	$host      = isset( $constants['DB_HOST'] ) ? $constants['DB_HOST'] : null;
	$charset   = isset( $constants['DB_CHARSET'] ) ? $constants['DB_CHARSET'] : 'utf8';
	$collate   = isset( $constants['DB_COLLATE'] ) ? $constants['DB_COLLATE'] : '';

	if ( $user && $password && $name && $host ) {
		return (object) compact( 'user', 'password', 'name', 'host', 'charset', 'collate' );
	}

	throw new Exception( 'Invalid db credentials or credentials not found.' );
}

/**
 * Loads db credentials from environment variables.
 *
 * It tries to find db creds in environment variables, otherwise throws exception.
 *
 * @return object {
 *     object containing the user, password, dbname, host, charset and collate.
 *     @type string $user       DB username.
 *     @type string $password   DB password.
 *     @type string $name       DB name.
 *     @type string $host       DB host/port.
 *     @type string $charset    DB charset. default utf-8.
 *     @type string $collate    DB collation. default blank, later auto-determined.
 * }
 *
 * @throws Exception
 */
function load_db_creds_from_env() {
	$user     = getenv( 'DB_USER' ) ? getenv( 'DB_USER' ) : null;
	$password = getenv( 'DB_PASSWORD' ) ? getenv( 'DB_PASSWORD' ) : null;
	$name     = getenv( 'DB_NAME' ) ? getenv( 'DB_NAME' ) : null;
	$host     = getenv( 'DB_HOST' ) ? getenv( 'DB_HOST' ) : '127.0.0.1';
	$charset  = getenv( 'DB_CHARSET' ) ? getenv( 'DB_CHARSET' ) : 'utf8';
	$collate  = getenv( 'DB_COLLATE' ) ? getenv( 'DB_COLLATE' ) : '';

	if ( $user && $password && $name && $host ) {
		return (object) compact( 'user', 'password', 'name', 'host', 'charset', 'collate' );
	}

	throw new Exception( 'Invalid db credentials or credentials not found in the environment.' );
}

/**
 * Connect to WP db by loading credentials in below order:
 * 1. Load credentials from environment (if Atomic)
 * 2. Scrape credentials from wp-config.php
 * 3. Finally fallback to old load_wp() with SHORTINIT
 *
 * Note: Never use load_db() in the flow where load_wp() is used! If you want to use it anyway,
 * you can only use it to test connection by calling load_db(function(){}, true) the second
 * parameter denotes just test DB connection and don't over-write global $wpdb object.
 * 
 * @return array {
 *      @type string $source - indicates the source from where DB creds were loaded.
 * }
 *
 * @throws Exception
 */
function load_db( $error_func = 'fatal_error', $test_connection = false ) {
	// Don't update the global $wpdb when told so and just try to test connection.
	if ( ! $test_connection ) {
		global $wpdb;
	}
	global $is_dynamic_ssh;

	// Try to load from env only if Atomic
	if ( $is_dynamic_ssh ) {
		try {
			// 1. try loading db credentials from environment
			$db_config = load_db_creds_from_env();
			$wpdb      = new WP_DB_Wrapper( $db_config->user, $db_config->password, $db_config->name, $db_config->host, $db_config->charset, $db_config->collate );
			return array( 'source' => 'environment' );
		} catch ( Exception $exception ) {
			// don't throw the exception yet, try loading from env next
		}
	}

	try {
		// 2. try scraping db credentials from wp-config.php
		$db_config = load_db_creds_from_config();
		$wpdb      = new WP_DB_Wrapper( $db_config->user, $db_config->password, $db_config->name, $db_config->host, $db_config->charset, $db_config->collate );
		return array( 'source' => 'parsed wp-config.php' );
	} catch ( Exception $exception ) {
		// 3. finally if all fails, try loading WP with SHORTINIT
		load_wp( false, $error_func, true );
		return array( 'source' => 'WordPress using load_wp fallback' );
	}
}

if ( ! function_exists( 'str_contains' ) ) {
	/**
	 * Polyfill for `str_contains()` function added in PHP 8.0.
	 *
	 * Performs a case-sensitive check indicating if needle is
	 * contained in haystack.
	 *
	 * @param string $haystack The string to search in.
	 * @param string $needle   The substring to search for in the `$haystack`.
	 * @return bool True if `$needle` is in `$haystack`, otherwise false.
	 */
	function str_contains( $haystack, $needle ) {
		if ( '' === $needle ) {
			return true;
		}

		return false !== strpos( $haystack, $needle );
	}
}

/**
 * WordPress database access abstraction class. Forked from original WP code for use in helper-script.
 * Stripped all the html rendering parts and kept only the minimum version required.
 *
 * Taken from WP version: '6.3-alpha-55872'
 * Minimum PHP version: '5.6.20'
 *
 * This class is used to interact with a database without needing to use raw SQL statements.
 */
class WP_DB_Wrapper {

	/**
	 * Whether the database queries are ready to start executing.
	 *
	 * @since 2.3.2
	 *
	 * @var bool
	 */
	public $ready = false;
	/**
	 * Database table columns charset.
	 *
	 * @since 2.2.0
	 *
	 * @var string
	 */
	public $charset;

	/**
	 * Database table columns collate.
	 *
	 * @since 2.2.0
	 *
	 * @var string
	 */
	public $collate;

	/**
	 * Database Username.
	 *
	 * @since 2.9.0
	 *
	 * @var string
	 */
	protected $dbuser;

	/**
	 * Database Password.
	 *
	 * @since 3.1.0
	 *
	 * @var string
	 */
	protected $dbpassword;

	/**
	 * Database Name.
	 *
	 * @since 3.1.0
	 *
	 * @var string
	 */
	protected $dbname;

	/**
	 * Database Host.
	 *
	 * @since 3.1.0
	 *
	 * @var string
	 */
	protected $dbhost;

	/**
	 * Database handle.
	 *
	 * Possible values:
	 *
	 * - `mysqli` instance when the `mysqli` driver is in use
	 * - `resource` when the older `mysql` driver is in use
	 * - `null` if the connection is yet to be made or has been closed
	 * - `false` if the connection has failed
	 *
	 * @since 0.71
	 *
	 * @var mysqli|resource|false|null
	 */
	public $dbh;
	/**
	 * Whether MySQL is used as the database engine.
	 *
	 * Set in wpdb::db_connect() to true, by default. This is used when checking
	 * against the required MySQL version for WordPress. Normally, a replacement
	 * database drop-in (db.php) will skip these checks, but setting this to true
	 * will force the checks to occur.
	 *
	 * @since 3.3.0
	 *
	 * @var bool
	 */
	public $is_mysql = null;

	/**
	 * A list of incompatible SQL modes.
	 *
	 * @since 3.9.0
	 *
	 * @var string[]
	 */
	protected $incompatible_modes = array(
		'NO_ZERO_DATE',
		'ONLY_FULL_GROUP_BY',
		'STRICT_TRANS_TABLES',
		'STRICT_ALL_TABLES',
		'TRADITIONAL',
		'ANSI',
	);
	/**
	 * Whether to use mysqli over mysql. Default false.
	 *
	 * @since 3.9.0
	 *
	 * @var bool
	 */
	public $use_mysqli = false;

	/**
	 * Whether we've managed to successfully connect at some point.
	 *
	 * @since 3.9.0
	 *
	 * @var bool
	 */
	private $has_connected = false;
	/**
	 * Connects to the database server and selects a database.
	 *
	 * Does the actual setting up
	 * of the class properties and connection to the database.
	 *
	 * @since 2.0.8
	 *
	 * @link https://core.trac.wordpress.org/ticket/3354
	 *
	 * @param string $dbuser     Database user.
	 * @param string $dbpassword Database password.
	 * @param string $dbname     Database name.
	 * @param string $dbhost     Database host.
	 * @param string $dbcharset  Database charset, default utf-8.
	 * @param string $dbcollate  Database collate, default blank.
	 */
	public function __construct( $dbuser, $dbpassword, $dbname, $dbhost, $dbcharset = 'utf-8', $dbcollate = '' ) {
		// Use the `mysqli` extension if it exists unless `WP_USE_EXT_MYSQL` is defined as true.
		if ( function_exists( 'mysqli_connect' ) ) {
			$this->use_mysqli = true;

			if ( defined( 'WP_USE_EXT_MYSQL' ) ) {
				$this->use_mysqli = ! WP_USE_EXT_MYSQL;
			}
		}

		$this->dbuser     = $dbuser;
		$this->dbpassword = $dbpassword;
		$this->dbname     = $dbname;
		$this->dbhost     = $dbhost;
		$this->collate    = $dbcollate;
		$this->charset    = $dbcharset;

		$this->db_connect();
	}
	/**
	 * Connects to and selects database.
	 *
	 * @return bool True with a successful connection, false on failure.
	 */
	public function db_connect() {
		$this->is_mysql = true;

		/*
		 * Deprecated in 3.9+ when using MySQLi. No equivalent
		 * $new_link parameter exists for mysqli_* functions.
		 */
		$new_link     = defined( 'MYSQL_NEW_LINK' ) ? MYSQL_NEW_LINK : true;
		$client_flags = defined( 'MYSQL_CLIENT_FLAGS' ) ? MYSQL_CLIENT_FLAGS : 0;

		if ( $this->use_mysqli ) {
			/*
			 * Set the MySQLi error reporting off because WordPress handles its own.
			 * This is due to the default value change from `MYSQLI_REPORT_OFF`
			 * to `MYSQLI_REPORT_ERROR|MYSQLI_REPORT_STRICT` in PHP 8.1.
			 */
			mysqli_report( MYSQLI_REPORT_OFF );

			$this->dbh = mysqli_init();

			$host    = $this->dbhost;
			$port    = null;
			$socket  = null;
			$is_ipv6 = false;

			$host_data = $this->parse_db_host( $this->dbhost );
			if ( $host_data ) {
				list($host, $port, $socket, $is_ipv6) = $host_data;
			}

			/*
			 * If using the `mysqlnd` library, the IPv6 address needs to be enclosed
			 * in square brackets, whereas it doesn't while using the `libmysqlclient` library.
			 * @see https://bugs.php.net/bug.php?id=67563
			 */
			if ( $is_ipv6 && extension_loaded( 'mysqlnd' ) ) {
				$host = "[$host]";
			}

			mysqli_real_connect( $this->dbh, $host, $this->dbuser, $this->dbpassword, null, $port, $socket, $client_flags );

			if ( $this->dbh->connect_errno ) {
				$this->dbh = null;

				/*
				 * It's possible ext/mysqli is misconfigured. Fall back to ext/mysql if:
				 *  - We haven't previously connected, and
				 *  - WP_USE_EXT_MYSQL isn't set to false, and
				 *  - ext/mysql is loaded.
				 */
				$attempt_fallback = true;

				if ( $this->has_connected ) {
					$attempt_fallback = false;
				} elseif ( defined( 'WP_USE_EXT_MYSQL' ) && ! WP_USE_EXT_MYSQL ) {
					$attempt_fallback = false;
				} elseif ( ! function_exists( 'mysql_connect' ) ) {
					$attempt_fallback = false;
				}

				if ( $attempt_fallback ) {
					$this->use_mysqli = false;
					return $this->db_connect();
				}
			}
		} else {
			$this->dbh = mysql_connect( $this->dbhost, $this->dbuser, $this->dbpassword, $new_link, $client_flags );
		}

		if ( ! $this->dbh ) {
			$message = 'Error establishing a database connection.';
			$this->bail( $message, 'db_connect_fail' );

			return false;
		} elseif ( $this->dbh ) {
			if ( ! $this->has_connected ) {
				$this->init_charset();
			}

			$this->has_connected = true;

			$this->set_charset( $this->dbh );

			$this->ready = true;
			$this->set_sql_mode();
			$this->select( $this->dbname, $this->dbh );

			return true;
		}

		return false;
	}
	/**
	 * Parses the DB_HOST setting to interpret it for mysqli_real_connect().
	 *
	 * mysqli_real_connect() doesn't support the host param including a port or socket
	 * like mysql_connect() does. This duplicates how mysql_connect() detects a port
	 * and/or socket file.
	 *
	 * @since 4.9.0
	 *
	 * @param string $host The DB_HOST setting to parse.
	 * @return array|false {
	 *     Array containing the host, the port, the socket and
	 *     whether it is an IPv6 address, in that order.
	 *     False if the host couldn't be parsed.
	 *
	 *     @type string      $0 Host name.
	 *     @type string|null $1 Port.
	 *     @type string|null $2 Socket.
	 *     @type bool        $3 Whether it is an IPv6 address.
	 * }
	 */
	public function parse_db_host( $host ) {
		$socket  = null;
		$is_ipv6 = false;

		// First peel off the socket parameter from the right, if it exists.
		$socket_pos = strpos( $host, ':/' );
		if ( false !== $socket_pos ) {
			$socket = substr( $host, $socket_pos + 1 );
			$host   = substr( $host, 0, $socket_pos );
		}

		// We need to check for an IPv6 address first.
		// An IPv6 address will always contain at least two colons.
		if ( substr_count( $host, ':' ) > 1 ) {
			$pattern = '#^(?:\[)?(?P<host>[0-9a-fA-F:]+)(?:\]:(?P<port>[\d]+))?#';
			$is_ipv6 = true;
		} else {
			// We seem to be dealing with an IPv4 address.
			$pattern = '#^(?P<host>[^:/]*)(?::(?P<port>[\d]+))?#';
		}

		$matches = array();
		$result  = preg_match( $pattern, $host, $matches );

		if ( 1 !== $result ) {
			// Couldn't parse the address, bail.
			return false;
		}

		$host = ! empty( $matches['host'] ) ? $matches['host'] : '';
		// MySQLi port cannot be a string; must be null or an integer.
		$port = ! empty( $matches['port'] ) ? abs( $matches['port'] ) : null;

		return array( $host, $port, $socket, $is_ipv6 );
	}
	/**
	 * Sets $this->charset and $this->collate.
	 *
	 * @since 3.1.0
	 */
	public function init_charset() {
		$charset = $this->charset;
		$collate = $this->collate;

		if ( function_exists( 'is_multisite' ) && is_multisite() ) {
			$charset = 'utf8';
			if ( empty( $collate ) ) {
				$collate = 'utf8_general_ci';
			}
		}

		$charset_collate = $this->determine_charset( $charset, $collate );

		$this->charset = $charset_collate['charset'];
		$this->collate = $charset_collate['collate'];
	}

	/**
	 * Determines the best charset and collation to use given a charset and collation.
	 *
	 * For example, when able, utf8mb4 should be used instead of utf8.
	 *
	 * @since 4.6.0
	 *
	 * @param string $charset The character set to check.
	 * @param string $collate The collation to check.
	 * @return array {
	 *     The most appropriate character set and collation to use.
	 *
	 *     @type string $charset Character set.
	 *     @type string $collate Collation.
	 * }
	 */
	public function determine_charset( $charset, $collate ) {
		if ( ( $this->use_mysqli && ! ( $this->dbh instanceof mysqli ) ) || empty( $this->dbh ) ) {
			return compact( 'charset', 'collate' );
		}

		if ( 'utf8' === $charset && $this->has_cap( 'utf8mb4' ) ) {
			$charset = 'utf8mb4';
		}

		if ( 'utf8mb4' === $charset && ! $this->has_cap( 'utf8mb4' ) ) {
			$charset = 'utf8';
			$collate = str_replace( 'utf8mb4_', 'utf8_', $collate );
		}

		if ( 'utf8mb4' === $charset ) {
			// _general_ is outdated, so we can upgrade it to _unicode_, instead.
			if ( ! $collate || 'utf8_general_ci' === $collate ) {
				$collate = 'utf8mb4_unicode_ci';
			} else {
				$collate = str_replace( 'utf8_', 'utf8mb4_', $collate );
			}
		}

		// _unicode_520_ is a better collation, we should use that when it's available.
		if ( $this->has_cap( 'utf8mb4_520' ) && 'utf8mb4_unicode_ci' === $collate ) {
			$collate = 'utf8mb4_unicode_520_ci';
		}

		return compact( 'charset', 'collate' );
	}
	/**
	 * Sets the connection's character set.
	 *
	 * @since 3.1.0
	 *
	 * @param mysqli|resource $dbh     The connection returned by `mysqli_connect()` or `mysql_connect()`.
	 * @param string          $charset Optional. The character set. Default null.
	 * @param string          $collate Optional. The collation. Default null.
	 */
	public function set_charset( $dbh, $charset = null, $collate = null ) {
		if ( ! isset( $charset ) ) {
			$charset = $this->charset;
		}
		if ( ! isset( $collate ) ) {
			$collate = $this->collate;
		}
		if ( $this->has_cap( 'collation' ) && ! empty( $charset ) ) {
			$set_charset_succeeded = true;

			if ( $this->use_mysqli ) {
				if ( function_exists( 'mysqli_set_charset' ) && $this->has_cap( 'set_charset' ) ) {
					$set_charset_succeeded = mysqli_set_charset( $dbh, $charset );
				}

				if ( $set_charset_succeeded ) {
					$query = sprintf( 'SET NAMES %s', $charset );
					if ( ! empty( $collate ) ) {
						$query .= sprintf( ' COLLATE %s', $collate );
					}
					mysqli_query( $dbh, $query );
				}
			} else {
				if ( function_exists( 'mysql_set_charset' ) && $this->has_cap( 'set_charset' ) ) {
					$set_charset_succeeded = mysql_set_charset( $charset, $dbh );
				}
				if ( $set_charset_succeeded ) {
					$query = sprintf( 'SET NAMES %s', $charset );
					if ( ! empty( $collate ) ) {
						$query .= sprintf( ' COLLATE %s', $collate );
					}
					mysql_query( $query, $dbh );
				}
			}
		}
	}
	/**
	 * Changes the current SQL mode, and ensures its WordPress compatibility.
	 *
	 * If no modes are passed, it will ensure the current MySQL server modes are compatible.
	 *
	 * @since 3.9.0
	 *
	 * @param array $modes Optional. A list of SQL modes to set. Default empty array.
	 */
	public function set_sql_mode( $modes = array() ) {
		if ( empty( $modes ) ) {
			if ( $this->use_mysqli ) {
				$res = mysqli_query( $this->dbh, 'SELECT @@SESSION.sql_mode' );
			} else {
				$res = mysql_query( 'SELECT @@SESSION.sql_mode', $this->dbh );
			}

			if ( empty( $res ) ) {
				return;
			}

			if ( $this->use_mysqli ) {
				$modes_array = mysqli_fetch_array( $res );
				if ( empty( $modes_array[0] ) ) {
					return;
				}
				$modes_str = $modes_array[0];
			} else {
				$modes_str = mysql_result( $res, 0 );
			}

			if ( empty( $modes_str ) ) {
				return;
			}

			$modes = explode( ',', $modes_str );
		}

		$modes              = array_change_key_case( $modes, CASE_UPPER );
		$incompatible_modes = $this->incompatible_modes;
		if ( function_exists( 'apply_filters' ) ) {
			/**
			 * Filters the list of incompatible SQL modes to exclude.
			 *
			 * @since 3.9.0
			 *
			 * @param array $incompatible_modes An array of incompatible modes.
			 */
			$incompatible_modes = (array) apply_filters( 'incompatible_sql_modes', $this->incompatible_modes );
		}
		foreach ( $modes as $i => $mode ) {
			if ( in_array( $mode, $incompatible_modes, true ) ) {
				unset( $modes[ $i ] );
			}
		}

		$modes_str = implode( ',', $modes );

		if ( $this->use_mysqli ) {
			mysqli_query( $this->dbh, "SET SESSION sql_mode='$modes_str'" );
		} else {
			mysql_query( "SET SESSION sql_mode='$modes_str'", $this->dbh );
		}
	}
	/**
	 * Selects a database using the current or provided database connection.
	 *
	 * The database name will be changed based on the current database connection.
	 * On failure, the execution will bail and display a DB error.
	 *
	 * @since 0.71
	 *
	 * @param string          $db  Database name.
	 * @param mysqli|resource $dbh Optional. Database connection.
	 *                             Defaults to the current database handle.
	 */
	public function select( $db, $dbh = null ) {
		if ( is_null( $dbh ) ) {
			$dbh = $this->dbh;
		}

		if ( $this->use_mysqli ) {
			$success = mysqli_select_db( $dbh, $db );
		} else {
			$success = mysql_select_db( $db, $dbh );
		}
		if ( ! $success ) {
			$this->ready = false;
			$message     = sprintf(
				"Cannot select database '%s'.",
				htmlspecialchars( $db, ENT_QUOTES )
			);
			$this->bail( $message, 'db_select_fail' );
		}
	}
	/**
	 * Wraps errors in a nice header and footer and dies.
	 *
	 * Will not die if wpdb::$show_errors is false.
	 *
	 * @since 1.5.0
	 *
	 * @param string $message    The error message.
	 * @param string $error_code Optional. A computer-readable string to identify the error.
	 *                           Default '500'.
	 * @throws Exception
	 */
	public function bail( $message, $error_code = '500' ) {
		$error = '';

		if ( $this->use_mysqli ) {
			if ( $this->dbh instanceof mysqli ) {
				$error = mysqli_error( $this->dbh );
			} elseif ( mysqli_connect_errno() ) {
				$error = mysqli_connect_error();
			}
		} else {
			if ( is_resource( $this->dbh ) ) {
				$error = mysql_error( $this->dbh );
			} else {
				$error = mysql_error();
			}
		}

		if ( $error ) {
			$message = $message . ' Reason: ' . $error;
		}

		throw new Exception( $message );
	}

	/**
	 * Closes the current database connection.
	 *
	 * @since 4.5.0
	 *
	 * @return bool True if the connection was successfully closed,
	 *              false if it wasn't, or if the connection doesn't exist.
	 */
	public function close() {
		if ( ! $this->dbh ) {
			return false;
		}

		if ( $this->use_mysqli ) {
			$closed = mysqli_close( $this->dbh );
		} else {
			$closed = mysql_close( $this->dbh );
		}

		if ( $closed ) {
			$this->dbh           = null;
			$this->ready         = false;
			$this->has_connected = false;
		}

		return $closed;
	}
	/**
	 * Determines whether the database or WPDB supports a particular feature.
	 *
	 * Capability sniffs for the database server and current version of WPDB.
	 *
	 * Database sniffs are based on the version of MySQL the site is using.
	 *
	 * WPDB sniffs are added as new features are introduced to allow theme and plugin
	 * developers to determine feature support. This is to account for drop-ins which may
	 * introduce feature support at a different time to WordPress.
	 *
	 * @since 2.7.0
	 * @since 4.1.0 Added support for the 'utf8mb4' feature.
	 * @since 4.6.0 Added support for the 'utf8mb4_520' feature.
	 * @since 6.2.0 Added support for the 'identifier_placeholders' feature.
	 *
	 * @see wpdb::db_version()
	 *
	 * @param string $db_cap The feature to check for. Accepts 'collation', 'group_concat',
	 *                       'subqueries', 'set_charset', 'utf8mb4', 'utf8mb4_520',
	 *                       or 'identifier_placeholders'.
	 * @return bool True when the database feature is supported, false otherwise.
	 */
	public function has_cap( $db_cap ) {
		$db_version     = $this->db_version();
		$db_server_info = $this->db_server_info();

		// Account for MariaDB version being prefixed with '5.5.5-' on older PHP versions.
		if (
			'5.5.5' === $db_version && str_contains( $db_server_info, 'MariaDB' )
			&& PHP_VERSION_ID < 80016 // PHP 8.0.15 or older.
		) {
			// Strip the '5.5.5-' prefix and set the version to the correct value.
			$db_server_info = preg_replace( '/^5\.5\.5-(.*)/', '$1', $db_server_info );
			$db_version     = preg_replace( '/[^0-9.].*/', '', $db_server_info );
		}

		switch ( strtolower( $db_cap ) ) {
			case 'collation':    // @since 2.5.0
			case 'group_concat': // @since 2.7.0
			case 'subqueries':   // @since 2.7.0
				return version_compare( $db_version, '4.1', '>=' );
			case 'set_charset':
				return version_compare( $db_version, '5.0.7', '>=' );
			case 'utf8mb4':      // @since 4.1.0
				if ( version_compare( $db_version, '5.5.3', '<' ) ) {
					return false;
				}
				if ( $this->use_mysqli ) {
					$client_version = mysqli_get_client_info();
				} else {
					$client_version = mysql_get_client_info();
				}

				/*
				 * libmysql has supported utf8mb4 since 5.5.3, same as the MySQL server.
				 * mysqlnd has supported utf8mb4 since 5.0.9.
				 */
				if ( false !== strpos( $client_version, 'mysqlnd' ) ) {
					$client_version = preg_replace( '/^\D+([\d.]+).*/', '$1', $client_version );
					return version_compare( $client_version, '5.0.9', '>=' );
				} else {
					return version_compare( $client_version, '5.5.3', '>=' );
				}
			case 'utf8mb4_520': // @since 4.6.0
				return version_compare( $db_version, '5.6', '>=' );
			case 'identifier_placeholders': // @since 6.2.0
				/*
				 * As of WordPress 6.2, wpdb::prepare() supports identifiers via '%i',
				 * e.g. table/field names.
				 */
				return true;
		}

		return false;
	}
	/**
	 * Retrieves the database server version.
	 *
	 * @since 2.7.0
	 *
	 * @return string|null Version number on success, null on failure.
	 */
	public function db_version() {
		return preg_replace( '/[^0-9.].*/', '', $this->db_server_info() );
	}

	/**
	 * Retrieves full database server information.
	 *
	 * @since 5.5.0
	 *
	 * @return string|false Server info on success, false on failure.
	 */
	public function db_server_info() {
		if ( $this->use_mysqli ) {
			$server_info = mysqli_get_server_info( $this->dbh );
		} else {
			$server_info = mysql_get_server_info( $this->dbh );
		}

		return $server_info;
	}
}

/**
 * Main entrypoint to the helper script.
 *
 * @return void
 */
function main() {

	ini_set( 'error_reporting', 0 );

	// Ensure no output buffering
	while ( ob_get_level() ) {
		ob_end_clean();
	}

	// Unpack arguments; support CLI or web.
	if ( is_cli() ) {
		if ( count( $_SERVER['argv'] ) !== 3 ) {
			fatal_error( COMMS_ERROR, 'Invalid args', 400 );
		}

		list( $script, $action, $base64_args ) = $_SERVER['argv'];
	} else {
		$action      = array_key_exists( 'action', $_REQUEST ) ? $_REQUEST['action'] : null;
		$base64_args = array_key_exists( 'args', $_REQUEST ) ? $_REQUEST['args'] : null;
		$salt        = array_key_exists( 'salt', $_REQUEST ) ? $_REQUEST['salt'] : null;
		$signature   = array_key_exists( 'signature', $_REQUEST ) ? (string) $_REQUEST['signature'] : '';
	}

	$json_args = base64_decode( $base64_args );

	if ( ! is_cli() ) {
		// Check expiry
		if ( time() > JP_EXPIRES ) {
			fatal_error( EXPIRY_ERROR, 'Expired', 419 );
		}

		// Check signature.
		if ( ! authenticate( $action, $json_args, $salt, $signature ) ) {
			fatal_error( COMMS_ERROR, 'Forbidden', 403 );
		}

		// Set an opaque Content-Type by default, to avoid tripping up broken web servers.
		header( 'Content-Type: application/octet-stream' );
	}

	$args = (array) json_decode( $json_args );
	// Save flag to later identiy if this is an Atomic site
	global $is_dynamic_ssh;
	$is_dynamic_ssh = isset( $args['is_dynamic_ssh'] ) && $args['is_dynamic_ssh'];
	// Execute action.
	jpr_action( $action, $args );
}

// Run main() unless this file is being included from the test suite.
if ( ! defined( 'JPB_TEST_SUITE' ) ) {
	main();
}

Youez - 2016 - github.com/yon3zu
LinuXploit