mirror of
https://gh.wpcy.net/https://github.com/YahnisElsts/wp-update-server.git
synced 2026-05-26 23:45:48 +08:00
- Added some additional output escaping. - WPUP intentionally uses the server's timezone in logs, when available. - WPUP needs writable `cache` and `logs` directories, so silence warnings related to file operations. - Some warnings and errors just don't apply because WPUP can be used outside WordPress. This means that some WP functions like wp_strip_all_tags() may not be available to be used. - IE has dropped to less than 1% usage, so it should be safe to remove the IE-specific Zlib workaround. Prompted by #107
777 lines
23 KiB
PHP
777 lines
23 KiB
PHP
<?php
|
|
|
|
class Wpup_UpdateServer {
|
|
const FILE_PER_DAY = 'Y-m-d';
|
|
const FILE_PER_MONTH = 'Y-m';
|
|
|
|
protected $serverDirectory;
|
|
protected $packageDirectory;
|
|
protected $bannerDirectory;
|
|
protected $assetDirectories = array();
|
|
|
|
protected $logDirectory;
|
|
protected $logRotationEnabled = false;
|
|
protected $logDateSuffix = null;
|
|
protected $logBackupCount = 0;
|
|
|
|
protected $cache;
|
|
protected $serverUrl;
|
|
protected $startTime = 0;
|
|
protected $packageFileLoader = array('Wpup_Package', 'fromArchive');
|
|
|
|
protected $ipAnonymizationEnabled = false;
|
|
protected $ip4Mask = '';
|
|
protected $ip6Mask = '';
|
|
|
|
public function __construct($serverUrl = null, $serverDirectory = null) {
|
|
if ( $serverDirectory === null ) {
|
|
$serverDirectory = realpath(__DIR__ . '/../..');
|
|
}
|
|
$this->serverDirectory = $this->normalizeFilePath($serverDirectory);
|
|
if ( $serverUrl === null ) {
|
|
$serverUrl = self::guessServerUrl();
|
|
}
|
|
|
|
$this->serverUrl = $serverUrl;
|
|
$this->packageDirectory = $serverDirectory . '/packages';
|
|
$this->logDirectory = $serverDirectory . '/logs';
|
|
|
|
$this->bannerDirectory = $serverDirectory . '/package-assets/banners';
|
|
$this->assetDirectories = array(
|
|
'banners' => $this->bannerDirectory,
|
|
'icons' => $serverDirectory . '/package-assets/icons',
|
|
);
|
|
|
|
//Set up the IP anonymization masks.
|
|
//For 32-bit addresses, replace the last 8 bits with zeros.
|
|
$this->ip4Mask = pack('H*', 'ffffff00');
|
|
//For 128-bit addresses, zero out the last 80 bits.
|
|
$this->ip6Mask = pack('H*', 'ffffffffffff00000000000000000000');
|
|
|
|
$this->cache = new Wpup_FileCache($serverDirectory . '/cache');
|
|
}
|
|
|
|
/**
|
|
* Guess the Server Url based on the current request.
|
|
*
|
|
* Defaults to the current URL minus the query and "index.php".
|
|
*
|
|
* @static
|
|
*
|
|
* @return string Url
|
|
*/
|
|
public static function guessServerUrl() {
|
|
if ( !isset($_SERVER['HTTP_HOST']) || !isset($_SERVER['SCRIPT_NAME']) ) {
|
|
return '/';
|
|
}
|
|
|
|
$serverUrl = (self::isSsl() ? 'https' : 'http');
|
|
//phpcs:disable WordPress.Security.ValidatedSanitizedInput -- Converting to string should be enough.
|
|
/** @noinspection PhpUnnecessaryStringCastInspection -- Let's make the cast explicit. */
|
|
$serverUrl .= '://' . strval($_SERVER['HTTP_HOST']);
|
|
$path = strval($_SERVER['SCRIPT_NAME']);
|
|
//phpcs:enable
|
|
|
|
if ( basename($path) === 'index.php' ) {
|
|
$path = dirname($path);
|
|
if ( DIRECTORY_SEPARATOR === '/' ) {
|
|
//Normalize Windows paths.
|
|
$path = str_replace('\\', '/', $path);
|
|
}
|
|
//Make sure there's a trailing slash.
|
|
if ( substr($path, -1) !== '/' ) {
|
|
$path .= '/';
|
|
}
|
|
}
|
|
|
|
$serverUrl .= $path;
|
|
return $serverUrl;
|
|
}
|
|
|
|
/**
|
|
* Determine if ssl is used.
|
|
*
|
|
* @return bool True if SSL, false if not used.
|
|
* @see WP core - wp-includes/functions.php
|
|
*
|
|
*/
|
|
public static function isSsl() {
|
|
if ( isset($_SERVER['HTTPS']) ) {
|
|
//Sanitization is not needed here because the value is only checked against known values.
|
|
//phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
|
if ( ($_SERVER['HTTPS'] == '1') || (strtolower($_SERVER['HTTPS']) === 'on') ) {
|
|
return true;
|
|
}
|
|
} elseif ( isset($_SERVER['SERVER_PORT']) && ('443' == $_SERVER['SERVER_PORT']) ) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Process an update API request.
|
|
*
|
|
* @param array|null $query Query parameters. Defaults to the current GET request parameters.
|
|
* @param array|null $headers HTTP headers. Defaults to the headers received for the current request.
|
|
*/
|
|
public function handleRequest($query = null, $headers = null) {
|
|
$this->startTime = microtime(true);
|
|
|
|
$request = $this->initRequest($query, $headers);
|
|
$this->logRequest($request);
|
|
|
|
$this->loadPackageFor($request);
|
|
$this->validateRequest($request);
|
|
$this->checkAuthorization($request);
|
|
$this->dispatch($request);
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Set up a request instance.
|
|
*
|
|
* @param array $query
|
|
* @param array $headers
|
|
* @return Wpup_Request
|
|
*/
|
|
protected function initRequest($query = null, $headers = null) {
|
|
if ( $query === null ) {
|
|
//Nonce verification doesn't apply to the update server. It doesn't
|
|
//process forms at all, or deal with stateful requests.
|
|
//phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
$query = $_GET;
|
|
}
|
|
if ( $headers === null ) {
|
|
$headers = Wpup_Headers::parseCurrent();
|
|
}
|
|
|
|
//As of this writing, the client IP is only used for logging. Any more
|
|
//advanced uses should implement additional sanitization.
|
|
//phpcs:ignore WordPressVIPMinimum.Variables.ServerVariables.UserControlledHeaders,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
|
$clientIp = isset($_SERVER['REMOTE_ADDR']) ? strval($_SERVER['REMOTE_ADDR']) : '0.0.0.0';
|
|
|
|
//Ensure that the HTTP method is always a string. That should be enough
|
|
//sanitization for our purposes.
|
|
//phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
|
$httpMethod = isset($_SERVER['REQUEST_METHOD']) ? strval($_SERVER['REQUEST_METHOD']) : 'GET';
|
|
|
|
return new Wpup_Request($query, $headers, $clientIp, $httpMethod);
|
|
}
|
|
|
|
/**
|
|
* Load the requested package into the request instance.
|
|
*
|
|
* @param Wpup_Request $request
|
|
*/
|
|
protected function loadPackageFor($request) {
|
|
if ( empty($request->slug) ) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$request->package = $this->findPackage($request->slug);
|
|
} catch (Wpup_InvalidPackageException $ex) {
|
|
$this->exitWithError(sprintf(
|
|
'Package "%s" exists, but it is not a valid plugin or theme. ' .
|
|
'Make sure it has the right format (Zip) and directory structure.',
|
|
htmlentities($request->slug)
|
|
));
|
|
exit;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Basic request validation. Every request must specify an action and a valid package slug.
|
|
*
|
|
* @param Wpup_Request $request
|
|
*/
|
|
protected function validateRequest($request) {
|
|
if ( $request->action === '' ) {
|
|
$this->exitWithError('You must specify an action.', 400);
|
|
}
|
|
if ( $request->slug === '' ) {
|
|
$this->exitWithError('You must specify a package slug.', 400);
|
|
}
|
|
if ( $request->package === null ) {
|
|
$this->exitWithError('Package not found', 404);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run the requested action.
|
|
*
|
|
* @param Wpup_Request $request
|
|
*/
|
|
protected function dispatch($request) {
|
|
if ( $request->action === 'get_metadata' ) {
|
|
$this->actionGetMetadata($request);
|
|
} else if ( $request->action === 'download' ) {
|
|
$this->actionDownload($request);
|
|
} else {
|
|
$this->exitWithError(sprintf('Invalid action "%s".', htmlentities($request->action)), 400);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve package metadata as JSON. This is the primary function of the custom update API.
|
|
*
|
|
* @param Wpup_Request $request
|
|
*/
|
|
protected function actionGetMetadata(Wpup_Request $request) {
|
|
$meta = $request->package->getMetadata();
|
|
$meta['download_url'] = $this->generateDownloadUrl($request->package);
|
|
$meta['banners'] = $this->getBanners($request->package);
|
|
$meta['icons'] = $this->getIcons($request->package);
|
|
|
|
$meta = $this->filterMetadata($meta, $request);
|
|
|
|
//For debugging. The update checker ignores unknown fields, so this is safe.
|
|
$meta['request_time_elapsed'] = sprintf('%.3f', microtime(true) - $this->startTime);
|
|
|
|
$this->outputAsJson($meta);
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Filter plugin metadata before output.
|
|
*
|
|
* Override this method to customize update API responses. For example, you could use it
|
|
* to conditionally exclude the download_url based on query parameters.
|
|
*
|
|
* @param array $meta
|
|
* @param Wpup_Request $request
|
|
* @return array Filtered metadata.
|
|
*/
|
|
protected function filterMetadata($meta, /** @noinspection PhpUnusedParameterInspection */ $request) {
|
|
//By convention, un-set properties are omitted.
|
|
$meta = array_filter($meta, function ($value) {
|
|
return $value !== null;
|
|
});
|
|
return $meta;
|
|
}
|
|
|
|
/**
|
|
* Process a download request.
|
|
*
|
|
* Typically this occurs when a user attempts to install a plugin/theme update
|
|
* from the WordPress dashboard, but technically they could also download and
|
|
* install it manually.
|
|
*
|
|
* @param Wpup_Request $request
|
|
*/
|
|
protected function actionDownload(Wpup_Request $request) {
|
|
$package = $request->package;
|
|
header('Content-Type: application/zip');
|
|
header('Content-Disposition: attachment; filename="' . $package->slug . '.zip"');
|
|
header('Content-Transfer-Encoding: binary');
|
|
header('Content-Length: ' . $package->getFileSize());
|
|
|
|
readfile($package->getFilename());
|
|
}
|
|
|
|
/**
|
|
* Find a plugin or theme by slug.
|
|
*
|
|
* @param string $slug
|
|
* @return Wpup_Package A package object or NULL if the plugin/theme was not found.
|
|
*/
|
|
protected function findPackage($slug) {
|
|
//Check if there's a slug.zip file in the package directory.
|
|
$safeSlug = preg_replace('@[^a-z0-9\-_\.,+!]@i', '', $slug);
|
|
$filename = $this->packageDirectory . '/' . $safeSlug . '.zip';
|
|
if ( !is_file($filename) || !is_readable($filename) ) {
|
|
return null;
|
|
}
|
|
|
|
return call_user_func($this->packageFileLoader, $filename, $slug, $this->cache);
|
|
}
|
|
|
|
/**
|
|
* Stub. You can override this in a subclass to show update info only to
|
|
* users with a valid license key (for example).
|
|
*
|
|
* @param $request
|
|
*/
|
|
protected function checkAuthorization($request) {
|
|
//Stub.
|
|
}
|
|
|
|
/**
|
|
* Create a download URL for a plugin.
|
|
*
|
|
* @param Wpup_Package $package
|
|
* @return string URL
|
|
*/
|
|
protected function generateDownloadUrl(Wpup_Package $package) {
|
|
$query = array(
|
|
'action' => 'download',
|
|
'slug' => $package->slug,
|
|
);
|
|
return self::addQueryArg($query, $this->serverUrl);
|
|
}
|
|
|
|
/**
|
|
* Find plugin banners.
|
|
*
|
|
* See WordPress repository docs for more information on banners:
|
|
* https://wordpress.org/plugins/about/faq/#banners
|
|
*
|
|
* @param Wpup_Package $package
|
|
* @return array|null
|
|
*/
|
|
protected function getBanners(Wpup_Package $package) {
|
|
//Find the normal banner first. The file name should be slug-772x250.ext.
|
|
$smallBanner = $this->findFirstAsset($package, 'banners', '-772x250');
|
|
if ( !empty($smallBanner) ) {
|
|
$banners = array('low' => $smallBanner);
|
|
|
|
//Then find the high-DPI banner.
|
|
$bigBanner = $this->findFirstAsset($package, 'banners', '-1544x500');
|
|
if ( !empty($bigBanner) ) {
|
|
$banners['high'] = $bigBanner;
|
|
}
|
|
|
|
return $banners;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get a publicly accessible URL for a plugin banner.
|
|
*
|
|
* @param string $relativeFileName Banner file name relative to the "banners" subdirectory.
|
|
* @return string
|
|
* @deprecated Use generateAssetUrl() instead.
|
|
*/
|
|
protected function generateBannerUrl($relativeFileName) {
|
|
return $this->generateAssetUrl('banners', $relativeFileName);
|
|
}
|
|
|
|
/**
|
|
* Find plugin icons.
|
|
*
|
|
* @param Wpup_Package $package
|
|
* @return array|null
|
|
*/
|
|
protected function getIcons(Wpup_Package $package) {
|
|
$icons = array(
|
|
'1x' => $this->findFirstAsset($package, 'icons', '-128x128'),
|
|
'2x' => $this->findFirstAsset($package, 'icons', '-256x256'),
|
|
'svg' => $this->findFirstAsset($package, 'icons', '', 'svg'),
|
|
);
|
|
|
|
$icons = array_filter($icons);
|
|
if ( !empty($icons) ) {
|
|
return $icons;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the first asset that has the specified suffix and file name extension.
|
|
*
|
|
* @param Wpup_Package $package
|
|
* @param string $assetType Either 'icons' or 'banners'.
|
|
* @param string $suffix Optional file name suffix. For example, "-128x128" for plugin icons.
|
|
* @param array|string $extensions Optional. Defaults to common image file formats.
|
|
* @return null|string Asset URL, or NULL if there are no matching assets.
|
|
*/
|
|
protected function findFirstAsset(
|
|
Wpup_Package $package,
|
|
$assetType = 'banners',
|
|
$suffix = '',
|
|
$extensions = array('png', 'jpg', 'jpeg')
|
|
) {
|
|
$pattern = $this->assetDirectories[$assetType] . '/' . $package->slug . $suffix;
|
|
|
|
if ( is_array($extensions) ) {
|
|
$extensionPattern = '{' . implode(',', $extensions) . '}';
|
|
} else {
|
|
$extensionPattern = $extensions;
|
|
}
|
|
|
|
$assets = glob($pattern . '.' . $extensionPattern, GLOB_BRACE | GLOB_NOESCAPE);
|
|
if ( !empty($assets) ) {
|
|
$firstFile = basename(reset($assets));
|
|
return $this->generateAssetUrl($assetType, $firstFile);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get a publicly accessible URL for a plugin asset.
|
|
*
|
|
* @param string $assetType Either 'icons' or 'banners'.
|
|
* @param string $relativeFileName File name relative to the asset directory.
|
|
* @return string
|
|
*/
|
|
protected function generateAssetUrl($assetType, $relativeFileName) {
|
|
//The current implementation is trivially simple, but you could override this method
|
|
//to (for example) create URLs that don't rely on the directory being public.
|
|
$directory = $this->normalizeFilePath($this->assetDirectories[$assetType]);
|
|
if ( strpos($directory, $this->serverDirectory) === 0 ) {
|
|
$subDirectory = substr($directory, strlen($this->serverDirectory) + 1);
|
|
} else {
|
|
$subDirectory = basename($directory);
|
|
}
|
|
$subDirectory = trim($subDirectory, '/\\');
|
|
return $this->serverUrl . $subDirectory . '/' . $relativeFileName;
|
|
}
|
|
|
|
/**
|
|
* Convert all directory separators to forward slashes.
|
|
*
|
|
* @param string $path
|
|
* @return string
|
|
*/
|
|
protected function normalizeFilePath($path) {
|
|
if ( !is_string($path) ) {
|
|
return $path;
|
|
}
|
|
return str_replace(array(DIRECTORY_SEPARATOR, '\\'), '/', $path);
|
|
}
|
|
|
|
/**
|
|
* Log an API request.
|
|
*
|
|
* @param Wpup_Request $request
|
|
*/
|
|
protected function logRequest($request) {
|
|
$logFile = $this->getLogFileName();
|
|
|
|
//If the log file is new, we should rotate old logs.
|
|
$mustRotate = $this->logRotationEnabled && !file_exists($logFile);
|
|
|
|
$handle = fopen($logFile, 'a');
|
|
if ( $handle && flock($handle, LOCK_EX) ) {
|
|
|
|
$loggedIp = $request->clientIp;
|
|
if ( $this->ipAnonymizationEnabled ) {
|
|
$loggedIp = $this->anonymizeIp($loggedIp);
|
|
}
|
|
|
|
$columns = array(
|
|
'ip' => $loggedIp,
|
|
'http_method' => $request->httpMethod,
|
|
'action' => $request->param('action', '-'),
|
|
'slug' => $request->param('slug', '-'),
|
|
'installed_version' => $request->param('installed_version', '-'),
|
|
'wp_version' => isset($request->wpVersion) ? $request->wpVersion : '-',
|
|
'site_url' => isset($request->wpSiteUrl) ? $request->wpSiteUrl : '-',
|
|
'query' => http_build_query($request->query, '', '&'),
|
|
);
|
|
$columns = $this->filterLogInfo($columns, $request);
|
|
$columns = $this->escapeLogInfo($columns);
|
|
|
|
if ( isset($columns['ip']) ) {
|
|
$columns['ip'] = str_pad($columns['ip'], 15, ' ');
|
|
}
|
|
if ( isset($columns['http_method']) ) {
|
|
$columns['http_method'] = str_pad($columns['http_method'], 4, ' ');
|
|
}
|
|
|
|
//Set the time zone to whatever the default is to avoid PHP notices.
|
|
//Will default to UTC if it's not set properly in php.ini.
|
|
$configuredTz = ini_get('date.timezone');
|
|
if ( empty($configuredTz) ) {
|
|
//The update server can be used outside WP, so it can't rely on WordPress's timezone support.
|
|
//phpcs:ignore WordPress.DateTime.RestrictedFunctions.timezone_change_date_default_timezone_set
|
|
date_default_timezone_set(@date_default_timezone_get());
|
|
}
|
|
|
|
//Use date() instead of gmdate() because the person reading the log file will probably
|
|
//find it more convenient to see a timestamp in their own (server's) time zone.
|
|
//phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
|
|
$line = date('[Y-m-d H:i:s O]') . ' ' . implode("\t", $columns) . "\n";
|
|
|
|
fwrite($handle, $line);
|
|
|
|
if ( $mustRotate ) {
|
|
$this->rotateLogs();
|
|
}
|
|
flock($handle, LOCK_UN);
|
|
}
|
|
if ( $handle ) {
|
|
fclose($handle);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
protected function getLogFileName() {
|
|
$path = $this->logDirectory . '/request';
|
|
if ( $this->logRotationEnabled ) {
|
|
//phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- Similar to above.
|
|
$path .= '-' . date($this->logDateSuffix);
|
|
}
|
|
return $path . '.log';
|
|
}
|
|
|
|
/**
|
|
* Adjust information that will be logged.
|
|
* Intended to be overridden in child classes.
|
|
*
|
|
* @param array $columns List of columns in the log entry.
|
|
* @param Wpup_Request|null $request
|
|
* @return array
|
|
*/
|
|
protected function filterLogInfo($columns, /** @noinspection PhpUnusedParameterInspection */ $request = null) {
|
|
return $columns;
|
|
}
|
|
|
|
/**
|
|
* Escapes passed log data so it can be safely written into a plain text file.
|
|
*
|
|
* @param string[] $columns List of columns in the log entry.
|
|
* @return string[] Escaped $columns.
|
|
*/
|
|
protected function escapeLogInfo(array $columns) {
|
|
return array_map(array($this, 'escapeLogValue'), $columns);
|
|
}
|
|
|
|
/**
|
|
* Escapes passed value to be safely written into a plain text file.
|
|
*
|
|
* @param string|null $value Value to escape.
|
|
* @return string|null Escaped value.
|
|
*/
|
|
protected function escapeLogValue($value) {
|
|
|
|
if ( !isset($value) ) {
|
|
return null;
|
|
}
|
|
|
|
$value = (string)$value;
|
|
|
|
$regex = '/[[:^graph:]]/';
|
|
|
|
//preg_replace_callback will return NULL if the input contains invalid Unicode sequences,
|
|
//so only enable the Unicode flag if the input encoding looks valid.
|
|
/** @noinspection PhpComposerExtensionStubsInspection */
|
|
if ( function_exists('mb_check_encoding') && mb_check_encoding($value, 'UTF-8') ) {
|
|
$regex = $regex . 'u';
|
|
}
|
|
|
|
$value = str_replace('\\', '\\\\', $value);
|
|
$value = preg_replace_callback(
|
|
$regex,
|
|
function (array $matches) {
|
|
$length = strlen($matches[0]);
|
|
$escaped = '';
|
|
for ($i = 0; $i < $length; $i++) {
|
|
//Convert the character to a hexadecimal escape sequence.
|
|
$hexCode = dechex(ord($matches[0][$i]));
|
|
$escaped .= '\x' . strtoupper(str_pad($hexCode, 2, '0', STR_PAD_LEFT));
|
|
}
|
|
return $escaped;
|
|
},
|
|
$value
|
|
);
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* Enable basic log rotation.
|
|
* Defaults to monthly rotation.
|
|
*
|
|
* @param string|null $rotationPeriod Either Wpup_UpdateServer::FILE_PER_DAY or Wpup_UpdateServer::FILE_PER_MONTH.
|
|
* @param int $filesToKeep The max number of log files to keep. Zero = unlimited.
|
|
*/
|
|
public function enableLogRotation($rotationPeriod = null, $filesToKeep = 10) {
|
|
if ( !isset($rotationPeriod) ) {
|
|
$rotationPeriod = self::FILE_PER_MONTH;
|
|
}
|
|
|
|
$this->logDateSuffix = $rotationPeriod;
|
|
$this->logBackupCount = $filesToKeep;
|
|
$this->logRotationEnabled = true;
|
|
}
|
|
|
|
/**
|
|
* Delete old log files.
|
|
*/
|
|
protected function rotateLogs() {
|
|
//Skip GC of old files if the backup count is unlimited.
|
|
if ( $this->logBackupCount === 0 ) {
|
|
return;
|
|
}
|
|
|
|
//Find log files.
|
|
$logFiles = glob($this->logDirectory . '/request*.log', GLOB_NOESCAPE);
|
|
if ( count($logFiles) <= $this->logBackupCount ) {
|
|
return;
|
|
}
|
|
|
|
//Sort the files by name. Due to the date suffix format, this also sorts them by date.
|
|
usort($logFiles, 'strcmp');
|
|
//Put them in descending order.
|
|
$logFiles = array_reverse($logFiles);
|
|
|
|
//Keep the most recent $logBackupCount files, delete the rest.
|
|
foreach (array_slice($logFiles, $this->logBackupCount) as $fileName) {
|
|
@unlink($fileName);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enable basic IP address anonymization.
|
|
*/
|
|
public function enableIpAnonymization() {
|
|
$this->ipAnonymizationEnabled = true;
|
|
}
|
|
|
|
/**
|
|
* Anonymize an IP address by replacing the last byte(s) with zeros.
|
|
*
|
|
* @param string $ip A valid IP address such as "12.45.67.89" or "2001:db8:85a3::8a2e:370:7334".
|
|
* @return string
|
|
*/
|
|
protected function anonymizeIp($ip) {
|
|
$binaryIp = @inet_pton($ip);
|
|
if ( strlen($binaryIp) === 4 ) {
|
|
//IPv4
|
|
$anonBinaryIp = $binaryIp & $this->ip4Mask;
|
|
} else if ( strlen($binaryIp) === 16 ) {
|
|
//IPv6
|
|
$anonBinaryIp = $binaryIp & $this->ip6Mask;
|
|
} else {
|
|
//The input is not a valid IPv4 or IPv6 address. Return it unmodified.
|
|
return $ip;
|
|
}
|
|
return inet_ntop($anonBinaryIp);
|
|
}
|
|
|
|
/**
|
|
* Output something as JSON.
|
|
*
|
|
* @param mixed $response
|
|
*/
|
|
protected function outputAsJson($response) {
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
if ( defined('JSON_PRETTY_PRINT') ) {
|
|
$output = $this->jsonEncode($response, JSON_PRETTY_PRINT);
|
|
} elseif ( function_exists('wsh_pretty_json') ) {
|
|
$output = wsh_pretty_json($this->jsonEncode($response));
|
|
} else {
|
|
$output = $this->jsonEncode($response);
|
|
}
|
|
//phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The output is JSON, not HTML.
|
|
echo $output;
|
|
}
|
|
|
|
protected function jsonEncode($value, $flags = 0) {
|
|
if ( function_exists('wp_json_encode') ) {
|
|
return wp_json_encode($value, $flags);
|
|
} else {
|
|
//Fall back to the native json_encode() when running outside of WordPress.
|
|
//phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
|
|
return json_encode($value, $flags);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop script execution with an error message.
|
|
*
|
|
* @param string $message Error message. It should already be HTML-escaped. This method will not sanitize it.
|
|
* @param int $httpStatus Optional HTTP status code. Defaults to 500 (Internal Server Error).
|
|
*/
|
|
protected function exitWithError($message = '', $httpStatus = 500) {
|
|
$statusMessages = array(
|
|
// This is not a full list of HTTP status messages. We only need the errors.
|
|
// [Client Error 4xx]
|
|
400 => '400 Bad Request',
|
|
401 => '401 Unauthorized',
|
|
402 => '402 Payment Required',
|
|
403 => '403 Forbidden',
|
|
404 => '404 Not Found',
|
|
405 => '405 Method Not Allowed',
|
|
406 => '406 Not Acceptable',
|
|
407 => '407 Proxy Authentication Required',
|
|
408 => '408 Request Timeout',
|
|
409 => '409 Conflict',
|
|
410 => '410 Gone',
|
|
411 => '411 Length Required',
|
|
412 => '412 Precondition Failed',
|
|
413 => '413 Request Entity Too Large',
|
|
414 => '414 Request-URI Too Long',
|
|
415 => '415 Unsupported Media Type',
|
|
416 => '416 Requested Range Not Satisfiable',
|
|
417 => '417 Expectation Failed',
|
|
// [Server Error 5xx]
|
|
500 => '500 Internal Server Error',
|
|
501 => '501 Not Implemented',
|
|
502 => '502 Bad Gateway',
|
|
503 => '503 Service Unavailable',
|
|
504 => '504 Gateway Timeout',
|
|
505 => '505 HTTP Version Not Supported',
|
|
);
|
|
|
|
if ( !isset($_SERVER['SERVER_PROTOCOL']) || ($_SERVER['SERVER_PROTOCOL'] === '') ) {
|
|
$protocol = 'HTTP/1.1';
|
|
} else {
|
|
//We'll just return the same protocol as the client used.
|
|
//phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
|
$protocol = strval($_SERVER['SERVER_PROTOCOL']);
|
|
}
|
|
|
|
//Output an HTTP status header.
|
|
if ( isset($statusMessages[$httpStatus]) ) {
|
|
header($protocol . ' ' . $statusMessages[$httpStatus]);
|
|
$title = $statusMessages[$httpStatus];
|
|
} else {
|
|
header('X-Ws-Update-Server-Error: ' . $httpStatus, true, $httpStatus);
|
|
$title = 'HTTP ' . $httpStatus;
|
|
}
|
|
|
|
if ( $message === '' ) {
|
|
$message = $title;
|
|
}
|
|
|
|
//And a basic HTML error message.
|
|
printf(
|
|
'<html>
|
|
<head> <title>%1$s</title> </head>
|
|
<body> <h1>%1$s</h1> <p>%2$s</p> </body>
|
|
</html>',
|
|
//phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc_html() might not be available here.
|
|
htmlentities($title),
|
|
//phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Should already be escaped.
|
|
$message
|
|
);
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Add one or more query arguments to a URL.
|
|
* You can also set an argument to NULL to remove it.
|
|
*
|
|
* @param array $args An associative array of query arguments.
|
|
* @param string $url The old URL. Optional, defaults to the request url without query arguments.
|
|
* @return string New URL.
|
|
*/
|
|
protected static function addQueryArg($args, $url = null) {
|
|
if ( !isset($url) ) {
|
|
$url = self::guessServerUrl();
|
|
}
|
|
if ( strpos($url, '?') !== false ) {
|
|
$parts = explode('?', $url, 2);
|
|
$base = $parts[0] . '?';
|
|
parse_str($parts[1], $query);
|
|
} else {
|
|
$base = $url . '?';
|
|
$query = array();
|
|
}
|
|
|
|
$query = array_merge($query, $args);
|
|
|
|
//Remove null/false arguments.
|
|
$query = array_filter($query, function ($value) {
|
|
return ($value !== null) && ($value !== false);
|
|
});
|
|
|
|
return $base . http_build_query($query, '', '&');
|
|
}
|
|
}
|